|
|
@@ -457,6 +457,32 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 登录对话框 -->
|
|
|
+ <div id="login-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
|
+ <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-2xl">
|
|
|
+ <div class="flex items-center justify-between mb-4">
|
|
|
+ <h3 class="text-xl font-bold text-gray-800" id="login-title">登录 Novel Platform</h3>
|
|
|
+ <button id="login-close-x" class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
|
|
+ </div>
|
|
|
+ <p id="login-description" class="text-sm text-gray-600 mb-4">请输入您的账号密码以访问需要认证的 MCP 工具</p>
|
|
|
+ <form id="login-form">
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="login-username" class="block text-sm font-medium text-gray-700 mb-1">用户名/邮箱</label>
|
|
|
+ <input type="text" id="login-username" placeholder="请输入用户名或邮箱" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="login-password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
|
|
+ <input type="password" id="login-password" placeholder="请输入密码" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
|
+ </div>
|
|
|
+ <p id="login-error" class="text-red-500 text-sm mb-3 hidden"></p>
|
|
|
+ <div class="flex justify-end gap-2">
|
|
|
+ <button type="button" id="login-cancel" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">取消</button>
|
|
|
+ <button type="submit" id="login-submit" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">登录</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
// State
|
|
|
let conversationHistory = [];
|
|
|
@@ -473,7 +499,203 @@
|
|
|
// API Base URL
|
|
|
const API_BASE = window.location.origin;
|
|
|
|
|
|
- // Format message content (handle code blocks, etc.)
|
|
|
+ // ========== MCP 服务器配置 ==========
|
|
|
+ const MCP_SERVERS = {
|
|
|
+ "novel-translator": {
|
|
|
+ "name": "Novel Translator MCP",
|
|
|
+ "auth_type": "none"
|
|
|
+ },
|
|
|
+ "novel-platform-user": {
|
|
|
+ "name": "Novel Platform User MCP",
|
|
|
+ "url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/",
|
|
|
+ "auth_type": "jwt",
|
|
|
+ "login_api": "/api/v1/auth/login",
|
|
|
+ "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun"
|
|
|
+ },
|
|
|
+ "novel-platform-admin": {
|
|
|
+ "name": "Novel Platform Admin MCP",
|
|
|
+ "url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/",
|
|
|
+ "auth_type": "jwt",
|
|
|
+ "login_api": "/api/v1/auth/admin-login",
|
|
|
+ "base_url": "https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun"
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // ========== Token 管理器 ==========
|
|
|
+ const TokenManager = {
|
|
|
+ // 保存 token
|
|
|
+ saveToken(mcpType, token, username = null) {
|
|
|
+ localStorage.setItem(`mcp_token_${mcpType}`, token);
|
|
|
+ localStorage.setItem(`mcp_token_${mcpType}_time`, Date.now());
|
|
|
+ if (username) {
|
|
|
+ localStorage.setItem(`mcp_username_${mcpType}`, username);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取 token
|
|
|
+ getToken(mcpType) {
|
|
|
+ return localStorage.getItem(`mcp_token_${mcpType}`);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取用户名
|
|
|
+ getUsername(mcpType) {
|
|
|
+ return localStorage.getItem(`mcp_username_${mcpType}`);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清除 token
|
|
|
+ clearToken(mcpType) {
|
|
|
+ localStorage.removeItem(`mcp_token_${mcpType}`);
|
|
|
+ localStorage.removeItem(`mcp_token_${mcpType}_time`);
|
|
|
+ localStorage.removeItem(`mcp_username_${mcpType}`);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 检查是否已登录
|
|
|
+ isLoggedIn(mcpType) {
|
|
|
+ return !!this.getToken(mcpType);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取 token 存储时长(毫秒)
|
|
|
+ getTokenAge(mcpType) {
|
|
|
+ const time = localStorage.getItem(`mcp_token_${mcpType}_time`);
|
|
|
+ return time ? (Date.now() - parseInt(time)) : null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // ========== 登录相关 ==========
|
|
|
+ let currentLoginMcpType = null;
|
|
|
+ const loginModal = document.getElementById('login-modal');
|
|
|
+ const loginForm = document.getElementById('login-form');
|
|
|
+ const loginUsername = document.getElementById('login-username');
|
|
|
+ const loginPassword = document.getElementById('login-password');
|
|
|
+ const loginError = document.getElementById('login-error');
|
|
|
+ const loginTitle = document.getElementById('login-title');
|
|
|
+ const loginSubmit = document.getElementById('login-submit');
|
|
|
+
|
|
|
+ // 显示登录对话框
|
|
|
+ function showLoginModal(mcpType) {
|
|
|
+ currentLoginMcpType = mcpType;
|
|
|
+ const config = MCP_SERVERS[mcpType];
|
|
|
+ if (!config) return;
|
|
|
+
|
|
|
+ loginTitle.textContent = `登录 ${config.name}`;
|
|
|
+ loginUsername.value = '';
|
|
|
+ loginPassword.value = '';
|
|
|
+ loginError.classList.add('hidden');
|
|
|
+ loginSubmit.disabled = false;
|
|
|
+ loginModal.classList.remove('hidden');
|
|
|
+ loginUsername.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏登录对话框
|
|
|
+ function hideLoginModal() {
|
|
|
+ loginModal.classList.add('hidden');
|
|
|
+ currentLoginMcpType = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行登录 - 使用本地后端代理避免 CORS 问题
|
|
|
+ async function performLogin(mcpType, username, password) {
|
|
|
+ const config = MCP_SERVERS[mcpType];
|
|
|
+ if (!config || config.auth_type !== 'jwt') {
|
|
|
+ return { success: false, error: '该 MCP 不需要登录' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用本地后端代理端点,避免直接调用外部 API 导致 CORS 错误
|
|
|
+ const is_admin = mcpType === 'novel-platform-admin';
|
|
|
+ const proxyUrl = is_admin ? '/api/auth/admin-login' : '/api/auth/login';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(proxyUrl, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ email: username,
|
|
|
+ password: password
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ if (response.ok && data.success) {
|
|
|
+ const token = data.token;
|
|
|
+ TokenManager.saveToken(mcpType, token, data.username || username);
|
|
|
+ return { success: true, token, username: data.username || username };
|
|
|
+ } else {
|
|
|
+ return { success: false, error: data.detail || data.error || '登录失败' };
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ return { success: false, error: e.message };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 登录表单提交处理
|
|
|
+ loginForm.addEventListener('submit', async (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ if (!currentLoginMcpType) return;
|
|
|
+
|
|
|
+ const username = loginUsername.value.trim();
|
|
|
+ const password = loginPassword.value;
|
|
|
+
|
|
|
+ if (!username || !password) {
|
|
|
+ loginError.textContent = '请输入用户名和密码';
|
|
|
+ loginError.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loginSubmit.disabled = true;
|
|
|
+ loginError.classList.add('hidden');
|
|
|
+
|
|
|
+ const result = await performLogin(currentLoginMcpType, username, password);
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ hideLoginModal();
|
|
|
+ loadMCPServers(); // 刷新 MCP 服务器列表
|
|
|
+ } else {
|
|
|
+ loginError.textContent = result.error;
|
|
|
+ loginError.classList.remove('hidden');
|
|
|
+ loginSubmit.disabled = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 登录对话框事件绑定
|
|
|
+ document.getElementById('login-cancel').addEventListener('click', hideLoginModal);
|
|
|
+ document.getElementById('login-close-x').addEventListener('click', hideLoginModal);
|
|
|
+
|
|
|
+ // 点击背景关闭
|
|
|
+ loginModal.addEventListener('click', (e) => {
|
|
|
+ if (e.target === loginModal) {
|
|
|
+ hideLoginModal();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // ========== Token 传递到后端 ==========
|
|
|
+ // 为每个请求添加 Authorization header
|
|
|
+ async function fetchWithAuth(url, options = {}) {
|
|
|
+ // 检查 URL 路径,确定是哪个 MCP
|
|
|
+ let mcpType = null;
|
|
|
+ if (url.includes('/chat') || url.includes('/mcp')) {
|
|
|
+ // 对于需要认证的 MCP,检查本地存储的 token
|
|
|
+ for (const [type, config] of Object.entries(MCP_SERVERS)) {
|
|
|
+ if (config.auth_type === 'jwt' && TokenManager.isLoggedIn(type)) {
|
|
|
+ mcpType = type;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加 token 到请求头
|
|
|
+ if (mcpType) {
|
|
|
+ const token = TokenManager.getToken(mcpType);
|
|
|
+ if (token) {
|
|
|
+ options.headers = options.headers || {};
|
|
|
+ options.headers['X-MCP-Token'] = token;
|
|
|
+ options.headers['X-MCP-Type'] = mcpType;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return fetch(url, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Format message content (handle code blocks, etc.) ==========
|
|
|
function formatMessage(content) {
|
|
|
// Simple markdown-like formatting
|
|
|
let formatted = content
|
|
|
@@ -715,9 +937,23 @@
|
|
|
const events = [];
|
|
|
|
|
|
try {
|
|
|
+ // 收集所有已登录 MCP 的 tokens
|
|
|
+ const mcpTokens = {};
|
|
|
+ for (const [type, config] of Object.entries(MCP_SERVERS)) {
|
|
|
+ if (config.auth_type === 'jwt' && TokenManager.isLoggedIn(type)) {
|
|
|
+ mcpTokens[type] = TokenManager.getToken(type);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const headers = { 'Content-Type': 'application/json' };
|
|
|
+ // 将 tokens 作为 JSON 字符串传递
|
|
|
+ if (Object.keys(mcpTokens).length > 0) {
|
|
|
+ headers['X-MCP-Tokens'] = JSON.stringify(mcpTokens);
|
|
|
+ }
|
|
|
+
|
|
|
const response = await fetch('/api/chat/stream', {
|
|
|
method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
+ headers: headers,
|
|
|
body: JSON.stringify({ message, history: conversationHistory })
|
|
|
});
|
|
|
|
|
|
@@ -949,18 +1185,59 @@
|
|
|
const response = await fetch(`${API_BASE}/api/mcp/servers`);
|
|
|
const data = await response.json();
|
|
|
|
|
|
- mcpServers.innerHTML = data.servers.map(server => `
|
|
|
- <div class="mcp-server-tag ${server.enabled ? 'enabled' : 'disabled'}">
|
|
|
- <span class="mcp-status-dot ${server.enabled ? 'online' : 'offline'}"></span>
|
|
|
- <span class="font-medium">${server.name}</span>
|
|
|
- ${server.auth_type === 'jwt' ? '<span>🔒</span>' : ''}
|
|
|
- </div>
|
|
|
- `).join('');
|
|
|
+ mcpServers.innerHTML = data.servers.map(server => {
|
|
|
+ const mcpType = server.id;
|
|
|
+ const isLoggedIn = TokenManager.isLoggedIn(mcpType);
|
|
|
+ const username = TokenManager.getUsername(mcpType);
|
|
|
+
|
|
|
+ let authStatus = '';
|
|
|
+ let clickHandler = '';
|
|
|
+
|
|
|
+ if (server.auth_type === 'jwt') {
|
|
|
+ if (isLoggedIn) {
|
|
|
+ authStatus = `<span class="text-green-600 font-medium" title="已登录为 ${username}">✅ ${username || '已登录'}</span>`;
|
|
|
+ clickHandler = `onclick="handleMcpClick('${mcpType}', '${server.auth_type}')"`;
|
|
|
+ } else {
|
|
|
+ authStatus = '<span class="text-amber-600" title="需要登录">🔒 需要登录</span>';
|
|
|
+ clickHandler = `onclick="handleMcpClick('${mcpType}', '${server.auth_type}')"`;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ authStatus = '<span class="text-gray-500">🔓 公开</span>';
|
|
|
+ }
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="mcp-server-tag ${server.enabled ? 'enabled' : 'disabled'}" ${clickHandler} style="cursor: ${server.auth_type === 'jwt' ? 'pointer' : 'default'}">
|
|
|
+ <span class="mcp-status-dot ${server.enabled ? 'online' : 'offline'}"></span>
|
|
|
+ <span class="font-medium">${server.name}</span>
|
|
|
+ ${authStatus}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
} catch (error) {
|
|
|
console.error('Failed to load MCP servers:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 处理 MCP 服务器点击
|
|
|
+ function handleMcpClick(mcpType, authType) {
|
|
|
+ if (authType !== 'jwt') return;
|
|
|
+
|
|
|
+ if (TokenManager.isLoggedIn(mcpType)) {
|
|
|
+ // 已登录,询问是否登出
|
|
|
+ const username = TokenManager.getUsername(mcpType);
|
|
|
+ if (confirm(`当前已登录为 ${username}\n\n是否要登出?`)) {
|
|
|
+ TokenManager.clearToken(mcpType);
|
|
|
+ loadMCPServers();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 未登录,显示登录对话框
|
|
|
+ showLoginModal(mcpType);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 handleMcpClick 暴露到全局作用域
|
|
|
+ window.handleMcpClick = handleMcpClick;
|
|
|
+
|
|
|
// Check health
|
|
|
async function checkHealth() {
|
|
|
try {
|