// 设置相关功能 let currentConfig = null; let allTools = []; // 全局工具状态映射,用于保存用户在所有页面的修改 // key: 唯一工具标识符(toolKey),value: { enabled: boolean, is_external: boolean, external_mcp: string } let toolStateMap = new Map(); // 生成工具的唯一标识符,用于区分同名但来源不同的工具 function getToolKey(tool) { // 如果是外部工具,使用 external_mcp::tool.name 作为唯一标识 // 如果是内部工具,使用 tool.name 作为标识 if (tool.is_external && tool.external_mcp) { return `${tool.external_mcp}::${tool.name}`; } return tool.name; } // 从localStorage读取每页显示数量,默认为20 const getToolsPageSize = () => { const saved = localStorage.getItem('toolsPageSize'); return saved ? parseInt(saved, 10) : 20; }; let toolsPagination = { page: 1, pageSize: getToolsPageSize(), total: 0, totalPages: 0 }; // 切换设置分类 function switchSettingsSection(section) { // 更新导航项状态 document.querySelectorAll('.settings-nav-item').forEach(item => { item.classList.remove('active'); }); const activeNavItem = document.querySelector(`.settings-nav-item[data-section="${section}"]`); if (activeNavItem) { activeNavItem.classList.add('active'); } // 更新内容区域显示 document.querySelectorAll('.settings-section-content').forEach(content => { content.classList.remove('active'); }); const activeContent = document.getElementById(`settings-section-${section}`); if (activeContent) { activeContent.classList.add('active'); } } // 打开设置 async function openSettings() { // 切换到设置页面 if (typeof switchPage === 'function') { switchPage('settings'); } // 每次打开时清空全局状态映射,重新加载最新配置 toolStateMap.clear(); // 每次打开时重新加载最新配置(系统设置页面不需要加载工具列表) await loadConfig(false); // 清除之前的验证错误状态 document.querySelectorAll('.form-group input').forEach(input => { input.classList.remove('error'); }); // 默认显示基本设置 switchSettingsSection('basic'); } // 关闭设置(保留函数以兼容旧代码,但现在不需要关闭功能) function closeSettings() { // 不再需要关闭功能,因为现在是页面而不是模态框 // 如果需要,可以切换回对话页面 if (typeof switchPage === 'function') { switchPage('chat'); } } // 点击模态框外部关闭(只保留MCP详情模态框) window.onclick = function(event) { const mcpModal = document.getElementById('mcp-detail-modal'); if (event.target === mcpModal) { closeMCPDetail(); } } // 加载配置 async function loadConfig(loadTools = true) { try { const response = await apiFetch('/api/config'); if (!response.ok) { throw new Error('获取配置失败'); } currentConfig = await response.json(); // 填充OpenAI配置 document.getElementById('openai-api-key').value = currentConfig.openai.api_key || ''; document.getElementById('openai-base-url').value = currentConfig.openai.base_url || ''; document.getElementById('openai-model').value = currentConfig.openai.model || ''; // 填充Agent配置 document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; // 填充知识库配置 const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled'); if (knowledgeEnabledCheckbox) { knowledgeEnabledCheckbox.checked = currentConfig.knowledge?.enabled !== false; } // 填充知识库详细配置 if (currentConfig.knowledge) { const knowledge = currentConfig.knowledge; // 基本配置 const basePathInput = document.getElementById('knowledge-base-path'); if (basePathInput) { basePathInput.value = knowledge.base_path || 'knowledge_base'; } // 嵌入模型配置 const embeddingProviderSelect = document.getElementById('knowledge-embedding-provider'); if (embeddingProviderSelect) { embeddingProviderSelect.value = knowledge.embedding?.provider || 'openai'; } const embeddingModelInput = document.getElementById('knowledge-embedding-model'); if (embeddingModelInput) { embeddingModelInput.value = knowledge.embedding?.model || ''; } const embeddingBaseUrlInput = document.getElementById('knowledge-embedding-base-url'); if (embeddingBaseUrlInput) { embeddingBaseUrlInput.value = knowledge.embedding?.base_url || ''; } const embeddingApiKeyInput = document.getElementById('knowledge-embedding-api-key'); if (embeddingApiKeyInput) { embeddingApiKeyInput.value = knowledge.embedding?.api_key || ''; } // 检索配置 const retrievalTopKInput = document.getElementById('knowledge-retrieval-top-k'); if (retrievalTopKInput) { retrievalTopKInput.value = knowledge.retrieval?.top_k || 5; } const retrievalThresholdInput = document.getElementById('knowledge-retrieval-similarity-threshold'); if (retrievalThresholdInput) { retrievalThresholdInput.value = knowledge.retrieval?.similarity_threshold || 0.7; } const retrievalWeightInput = document.getElementById('knowledge-retrieval-hybrid-weight'); if (retrievalWeightInput) { const hybridWeight = knowledge.retrieval?.hybrid_weight; // 允许0.0值,只有undefined/null时才使用默认值 retrievalWeightInput.value = (hybridWeight !== undefined && hybridWeight !== null) ? hybridWeight : 0.7; } } // 只有在需要时才加载工具列表(MCP管理页面需要,系统设置页面不需要) if (loadTools) { // 设置每页显示数量(会在分页控件渲染时设置) const savedPageSize = getToolsPageSize(); toolsPagination.pageSize = savedPageSize; // 加载工具列表(使用分页) toolsSearchKeyword = ''; await loadToolsList(1, ''); } } catch (error) { console.error('加载配置失败:', error); alert('加载配置失败: ' + error.message); } } // 工具搜索关键词 let toolsSearchKeyword = ''; // 加载工具列表(分页) async function loadToolsList(page = 1, searchKeyword = '') { try { // 在加载新页面之前,先保存当前页的状态到全局映射 saveCurrentPageToolStates(); const pageSize = toolsPagination.pageSize; let url = `/api/config/tools?page=${page}&page_size=${pageSize}`; if (searchKeyword) { url += `&search=${encodeURIComponent(searchKeyword)}`; } const response = await apiFetch(url); if (!response.ok) { throw new Error('获取工具列表失败'); } const result = await response.json(); allTools = result.tools || []; toolsPagination = { page: result.page || page, pageSize: result.page_size || pageSize, total: result.total || 0, totalPages: result.total_pages || 1 }; // 初始化工具状态映射(如果工具不在映射中,使用服务器返回的状态) allTools.forEach(tool => { const toolKey = getToolKey(tool); if (!toolStateMap.has(toolKey)) { toolStateMap.set(toolKey, { enabled: tool.enabled, is_external: tool.is_external || false, external_mcp: tool.external_mcp || '', name: tool.name // 保存原始工具名称 }); } }); renderToolsList(); renderToolsPagination(); } catch (error) { console.error('加载工具列表失败:', error); const toolsList = document.getElementById('tools-list'); if (toolsList) { toolsList.innerHTML = `
加载工具列表失败: ${escapeHtml(error.message)}
`; } } } // 保存当前页的工具状态到全局映射 function saveCurrentPageToolStates() { document.querySelectorAll('#tools-list .tool-item').forEach(item => { const checkbox = item.querySelector('input[type="checkbox"]'); const toolKey = item.dataset.toolKey; // 使用唯一标识符 const toolName = item.dataset.toolName; const isExternal = item.dataset.isExternal === 'true'; const externalMcp = item.dataset.externalMcp || ''; if (toolKey && checkbox) { toolStateMap.set(toolKey, { enabled: checkbox.checked, is_external: isExternal, external_mcp: externalMcp, name: toolName // 保存原始工具名称 }); } }); } // 搜索工具 function searchTools() { const searchInput = document.getElementById('tools-search'); const keyword = searchInput ? searchInput.value.trim() : ''; toolsSearchKeyword = keyword; // 搜索时重置到第一页 loadToolsList(1, keyword); } // 清除搜索 function clearSearch() { const searchInput = document.getElementById('tools-search'); if (searchInput) { searchInput.value = ''; } toolsSearchKeyword = ''; loadToolsList(1, ''); } // 处理搜索框回车事件 function handleSearchKeyPress(event) { if (event.key === 'Enter') { searchTools(); } } // 渲染工具列表 function renderToolsList() { const toolsList = document.getElementById('tools-list'); if (!toolsList) return; // 只渲染列表部分,分页控件单独渲染 const listContainer = toolsList.querySelector('.tools-list-items') || document.createElement('div'); listContainer.className = 'tools-list-items'; listContainer.innerHTML = ''; if (allTools.length === 0) { listContainer.innerHTML = '
暂无工具
'; if (!toolsList.contains(listContainer)) { toolsList.appendChild(listContainer); } // 更新统计 updateToolsStats(); return; } allTools.forEach(tool => { const toolKey = getToolKey(tool); // 生成唯一标识符 const toolItem = document.createElement('div'); toolItem.className = 'tool-item'; toolItem.dataset.toolKey = toolKey; // 保存唯一标识符 toolItem.dataset.toolName = tool.name; // 保存原始工具名称 toolItem.dataset.isExternal = tool.is_external ? 'true' : 'false'; toolItem.dataset.externalMcp = tool.external_mcp || ''; // 从全局状态映射获取工具状态,如果不存在则使用服务器返回的状态 const toolState = toolStateMap.get(toolKey) || { enabled: tool.enabled, is_external: tool.is_external || false, external_mcp: tool.external_mcp || '' }; // 外部工具标签,显示来源信息 let externalBadge = ''; if (toolState.is_external || tool.is_external) { const externalMcpName = toolState.external_mcp || tool.external_mcp || ''; const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部'; const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具'; externalBadge = `${badgeText}`; } // 生成唯一的checkbox id,使用工具唯一标识符 const checkboxId = `tool-${escapeHtml(toolKey).replace(/::/g, '--')}`; toolItem.innerHTML = `
${escapeHtml(tool.name)} ${externalBadge}
${escapeHtml(tool.description || '无描述')}
`; listContainer.appendChild(toolItem); }); if (!toolsList.contains(listContainer)) { toolsList.appendChild(listContainer); } // 更新统计 updateToolsStats(); } // 渲染工具列表分页控件 function renderToolsPagination() { const toolsList = document.getElementById('tools-list'); if (!toolsList) return; // 移除旧的分页控件 const oldPagination = toolsList.querySelector('.tools-pagination'); if (oldPagination) { oldPagination.remove(); } // 如果只有一页或没有数据,不显示分页 if (toolsPagination.totalPages <= 1) { return; } const pagination = document.createElement('div'); pagination.className = 'tools-pagination'; const { page, totalPages, total } = toolsPagination; const startItem = (page - 1) * toolsPagination.pageSize + 1; const endItem = Math.min(page * toolsPagination.pageSize, total); const savedPageSize = getToolsPageSize(); pagination.innerHTML = `
显示 ${startItem}-${endItem} / 共 ${total} 个工具${toolsSearchKeyword ? ` (搜索: "${escapeHtml(toolsSearchKeyword)}")` : ''}
第 ${page} / ${totalPages} 页
`; toolsList.appendChild(pagination); } // 处理工具checkbox状态变化 function handleToolCheckboxChange(toolKey, enabled) { // 更新全局状态映射 const toolItem = document.querySelector(`.tool-item[data-tool-key="${toolKey}"]`); if (toolItem) { const toolName = toolItem.dataset.toolName; const isExternal = toolItem.dataset.isExternal === 'true'; const externalMcp = toolItem.dataset.externalMcp || ''; toolStateMap.set(toolKey, { enabled: enabled, is_external: isExternal, external_mcp: externalMcp, name: toolName // 保存原始工具名称 }); } updateToolsStats(); } // 全选工具 function selectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = true; // 更新全局状态映射 const toolItem = checkbox.closest('.tool-item'); if (toolItem) { const toolKey = toolItem.dataset.toolKey; const toolName = toolItem.dataset.toolName; const isExternal = toolItem.dataset.isExternal === 'true'; const externalMcp = toolItem.dataset.externalMcp || ''; if (toolKey) { toolStateMap.set(toolKey, { enabled: true, is_external: isExternal, external_mcp: externalMcp, name: toolName // 保存原始工具名称 }); } } }); updateToolsStats(); } // 全不选工具 function deselectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = false; // 更新全局状态映射 const toolItem = checkbox.closest('.tool-item'); if (toolItem) { const toolKey = toolItem.dataset.toolKey; const toolName = toolItem.dataset.toolName; const isExternal = toolItem.dataset.isExternal === 'true'; const externalMcp = toolItem.dataset.externalMcp || ''; if (toolKey) { toolStateMap.set(toolKey, { enabled: false, is_external: isExternal, external_mcp: externalMcp, name: toolName // 保存原始工具名称 }); } } }); updateToolsStats(); } // 改变每页显示数量 async function changeToolsPageSize() { // 尝试从两个位置获取选择器(顶部或分页区域) const pageSizeSelect = document.getElementById('tools-page-size') || document.getElementById('tools-page-size-pagination'); if (!pageSizeSelect) return; const newPageSize = parseInt(pageSizeSelect.value, 10); if (isNaN(newPageSize) || newPageSize < 1) { return; } // 保存到localStorage localStorage.setItem('toolsPageSize', newPageSize.toString()); // 更新分页配置 toolsPagination.pageSize = newPageSize; // 同步更新另一个选择器(如果存在) const otherSelect = document.getElementById('tools-page-size') || document.getElementById('tools-page-size-pagination'); if (otherSelect && otherSelect !== pageSizeSelect) { otherSelect.value = newPageSize; } // 重新加载第一页 await loadToolsList(1, toolsSearchKeyword); } // 更新工具统计信息 async function updateToolsStats() { const statsEl = document.getElementById('tools-stats'); if (!statsEl) return; // 先保存当前页的状态到全局映射 saveCurrentPageToolStates(); // 计算当前页的启用工具数 const currentPageEnabled = Array.from(document.querySelectorAll('#tools-list input[type="checkbox"]:checked')).length; const currentPageTotal = document.querySelectorAll('#tools-list input[type="checkbox"]').length; // 计算所有工具的启用数 let totalEnabled = 0; let totalTools = toolsPagination.total || 0; try { // 如果有搜索关键词,只统计搜索结果 if (toolsSearchKeyword) { totalTools = allTools.length; totalEnabled = allTools.filter(tool => { // 优先使用全局状态映射,否则使用checkbox状态,最后使用服务器返回的状态 const toolKey = getToolKey(tool); const savedState = toolStateMap.get(toolKey); if (savedState !== undefined) { return savedState.enabled; } const checkboxId = `tool-${toolKey.replace(/::/g, '--')}`; const checkbox = document.getElementById(checkboxId); return checkbox ? checkbox.checked : tool.enabled; }).length; } else { // 没有搜索时,需要获取所有工具的状态 // 先使用全局状态映射和当前页的checkbox状态 const localStateMap = new Map(); // 从当前页的checkbox获取状态(如果全局映射中没有) allTools.forEach(tool => { const toolKey = getToolKey(tool); const savedState = toolStateMap.get(toolKey); if (savedState !== undefined) { localStateMap.set(toolKey, savedState.enabled); } else { const checkboxId = `tool-${toolKey.replace(/::/g, '--')}`; const checkbox = document.getElementById(checkboxId); if (checkbox) { localStateMap.set(toolKey, checkbox.checked); } else { // 如果checkbox不存在(不在当前页),使用工具原始状态 localStateMap.set(toolKey, tool.enabled); } } }); // 如果总工具数大于当前页,需要获取所有工具的状态 if (totalTools > allTools.length) { // 遍历所有页面获取完整状态 let page = 1; let hasMore = true; const pageSize = 100; // 使用较大的页面大小以减少请求次数 while (hasMore && page <= 10) { // 限制最多10页,避免无限循环 const url = `/api/config/tools?page=${page}&page_size=${pageSize}`; const pageResponse = await apiFetch(url); if (!pageResponse.ok) break; const pageResult = await pageResponse.json(); pageResult.tools.forEach(tool => { // 优先使用全局状态映射,否则使用服务器返回的状态 const toolKey = getToolKey(tool); if (!localStateMap.has(toolKey)) { const savedState = toolStateMap.get(toolKey); localStateMap.set(toolKey, savedState ? savedState.enabled : tool.enabled); } }); if (page >= pageResult.total_pages) { hasMore = false; } else { page++; } } } // 计算启用的工具数 totalEnabled = Array.from(localStateMap.values()).filter(enabled => enabled).length; } } catch (error) { console.warn('获取工具统计失败,使用当前页数据', error); // 如果获取失败,使用当前页的数据 totalTools = totalTools || currentPageTotal; totalEnabled = currentPageEnabled; } statsEl.innerHTML = ` ✅ 当前页已启用: ${currentPageEnabled} / ${currentPageTotal} 📊 总计已启用: ${totalEnabled} / ${totalTools} `; } // 过滤工具(已废弃,现在使用服务端搜索) // 保留此函数以防其他地方调用,但实际功能已由searchTools()替代 function filterTools() { // 不再使用客户端过滤,改为触发服务端搜索 // 可以保留为空函数或移除oninput事件 } // 应用设置 async function applySettings() { try { // 清除之前的验证错误状态 document.querySelectorAll('.form-group input').forEach(input => { input.classList.remove('error'); }); // 验证必填字段 const apiKey = document.getElementById('openai-api-key').value.trim(); const baseUrl = document.getElementById('openai-base-url').value.trim(); const model = document.getElementById('openai-model').value.trim(); let hasError = false; if (!apiKey) { document.getElementById('openai-api-key').classList.add('error'); hasError = true; } if (!baseUrl) { document.getElementById('openai-base-url').classList.add('error'); hasError = true; } if (!model) { document.getElementById('openai-model').classList.add('error'); hasError = true; } if (hasError) { alert('请填写所有必填字段(标记为 * 的字段)'); return; } // 收集配置 const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled'); const knowledgeEnabled = knowledgeEnabledCheckbox ? knowledgeEnabledCheckbox.checked : true; // 收集知识库配置 const knowledgeConfig = { enabled: knowledgeEnabled, base_path: document.getElementById('knowledge-base-path')?.value.trim() || 'knowledge_base', embedding: { provider: document.getElementById('knowledge-embedding-provider')?.value || 'openai', model: document.getElementById('knowledge-embedding-model')?.value.trim() || '', base_url: document.getElementById('knowledge-embedding-base-url')?.value.trim() || '', api_key: document.getElementById('knowledge-embedding-api-key')?.value.trim() || '' }, retrieval: { top_k: parseInt(document.getElementById('knowledge-retrieval-top-k')?.value) || 5, similarity_threshold: (() => { const val = parseFloat(document.getElementById('knowledge-retrieval-similarity-threshold')?.value); return isNaN(val) ? 0.7 : val; })(), hybrid_weight: (() => { const val = parseFloat(document.getElementById('knowledge-retrieval-hybrid-weight')?.value); return isNaN(val) ? 0.7 : val; // 允许0.0值,只有NaN时才使用默认值 })() } }; const config = { openai: { api_key: apiKey, base_url: baseUrl, model: model }, agent: { max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30 }, knowledge: knowledgeConfig, tools: [] }; // 收集工具启用状态 // 先保存当前页的状态到全局映射 saveCurrentPageToolStates(); // 获取所有工具列表以获取完整状态(遍历所有页面) // 注意:无论是否在搜索状态下,都要获取所有工具的状态,以确保完整保存 try { const allToolsMap = new Map(); let page = 1; let hasMore = true; const pageSize = 100; // 使用合理的页面大小 // 遍历所有页面获取所有工具(不使用搜索关键词,获取全部工具) while (hasMore) { const url = `/api/config/tools?page=${page}&page_size=${pageSize}`; const pageResponse = await apiFetch(url); if (!pageResponse.ok) { throw new Error('获取工具列表失败'); } const pageResult = await pageResponse.json(); // 将工具添加到映射中 // 优先使用全局状态映射中的状态(用户修改过的),否则使用服务器返回的状态 pageResult.tools.forEach(tool => { const toolKey = getToolKey(tool); const savedState = toolStateMap.get(toolKey); allToolsMap.set(toolKey, { name: tool.name, enabled: savedState ? savedState.enabled : tool.enabled, is_external: savedState ? savedState.is_external : (tool.is_external || false), external_mcp: savedState ? savedState.external_mcp : (tool.external_mcp || '') }); }); // 检查是否还有更多页面 if (page >= pageResult.total_pages) { hasMore = false; } else { page++; } } // 将所有工具添加到配置中 allToolsMap.forEach((tool, toolKey) => { config.tools.push({ name: tool.name, enabled: tool.enabled, is_external: tool.is_external, external_mcp: tool.external_mcp }); }); } catch (error) { console.warn('获取所有工具列表失败,仅使用全局状态映射', error); // 如果获取失败,使用全局状态映射 toolStateMap.forEach((toolData, toolKey) => { // toolData.name 保存了原始工具名称 const toolName = toolData.name || toolKey.split('::').pop(); config.tools.push({ name: toolName, enabled: toolData.enabled, is_external: toolData.is_external, external_mcp: toolData.external_mcp }); }); } // 更新配置 const updateResponse = await apiFetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!updateResponse.ok) { const error = await updateResponse.json(); throw new Error(error.error || '更新配置失败'); } // 应用配置 const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' }); if (!applyResponse.ok) { const error = await applyResponse.json(); throw new Error(error.error || '应用配置失败'); } alert('配置已成功应用!'); closeSettings(); } catch (error) { console.error('应用配置失败:', error); alert('应用配置失败: ' + error.message); } } // 保存工具配置(独立函数,用于MCP管理页面) async function saveToolsConfig() { try { // 先保存当前页的状态到全局映射 saveCurrentPageToolStates(); // 获取当前配置(只获取工具部分) const response = await apiFetch('/api/config'); if (!response.ok) { throw new Error('获取配置失败'); } const currentConfig = await response.json(); // 构建只包含工具配置的配置对象 const config = { openai: currentConfig.openai || {}, agent: currentConfig.agent || {}, tools: [] }; // 收集工具启用状态(与applySettings中的逻辑相同) try { const allToolsMap = new Map(); let page = 1; let hasMore = true; const pageSize = 100; // 遍历所有页面获取所有工具 while (hasMore) { const url = `/api/config/tools?page=${page}&page_size=${pageSize}`; const pageResponse = await apiFetch(url); if (!pageResponse.ok) { throw new Error('获取工具列表失败'); } const pageResult = await pageResponse.json(); // 将工具添加到映射中 pageResult.tools.forEach(tool => { const toolKey = getToolKey(tool); const savedState = toolStateMap.get(toolKey); allToolsMap.set(toolKey, { name: tool.name, enabled: savedState ? savedState.enabled : tool.enabled, is_external: savedState ? savedState.is_external : (tool.is_external || false), external_mcp: savedState ? savedState.external_mcp : (tool.external_mcp || '') }); }); // 检查是否还有更多页面 if (page >= pageResult.total_pages) { hasMore = false; } else { page++; } } // 将所有工具添加到配置中 allToolsMap.forEach((tool, toolKey) => { config.tools.push({ name: tool.name, enabled: tool.enabled, is_external: tool.is_external, external_mcp: tool.external_mcp }); }); } catch (error) { console.warn('获取所有工具列表失败,仅使用全局状态映射', error); // 如果获取失败,使用全局状态映射 toolStateMap.forEach((toolData, toolKey) => { // toolData.name 保存了原始工具名称 const toolName = toolData.name || toolKey.split('::').pop(); config.tools.push({ name: toolName, enabled: toolData.enabled, is_external: toolData.is_external, external_mcp: toolData.external_mcp }); }); } // 更新配置 const updateResponse = await apiFetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!updateResponse.ok) { const error = await updateResponse.json(); throw new Error(error.error || '更新配置失败'); } // 应用配置 const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' }); if (!applyResponse.ok) { const error = await applyResponse.json(); throw new Error(error.error || '应用配置失败'); } alert('工具配置已成功保存!'); // 重新加载工具列表以反映最新状态 if (typeof loadToolsList === 'function') { await loadToolsList(toolsPagination.page, toolsSearchKeyword); } } catch (error) { console.error('保存工具配置失败:', error); alert('保存工具配置失败: ' + error.message); } } function resetPasswordForm() { const currentInput = document.getElementById('auth-current-password'); const newInput = document.getElementById('auth-new-password'); const confirmInput = document.getElementById('auth-confirm-password'); [currentInput, newInput, confirmInput].forEach(input => { if (input) { input.value = ''; input.classList.remove('error'); } }); } async function changePassword() { const currentInput = document.getElementById('auth-current-password'); const newInput = document.getElementById('auth-new-password'); const confirmInput = document.getElementById('auth-confirm-password'); const submitBtn = document.querySelector('.change-password-submit'); [currentInput, newInput, confirmInput].forEach(input => input && input.classList.remove('error')); const currentPassword = currentInput?.value.trim() || ''; const newPassword = newInput?.value.trim() || ''; const confirmPassword = confirmInput?.value.trim() || ''; let hasError = false; if (!currentPassword) { currentInput?.classList.add('error'); hasError = true; } if (!newPassword || newPassword.length < 8) { newInput?.classList.add('error'); hasError = true; } if (newPassword !== confirmPassword) { confirmInput?.classList.add('error'); hasError = true; } if (hasError) { alert('请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。'); return; } if (submitBtn) { submitBtn.disabled = true; } try { const response = await apiFetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldPassword: currentPassword, newPassword: newPassword }) }); const result = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(result.error || '修改密码失败'); } alert('密码已更新,请使用新密码重新登录。'); resetPasswordForm(); handleUnauthorized({ message: '密码已更新,请使用新密码重新登录。', silent: false }); closeSettings(); } catch (error) { console.error('修改密码失败:', error); alert('修改密码失败: ' + error.message); } finally { if (submitBtn) { submitBtn.disabled = false; } } } // ==================== 外部MCP管理 ==================== let currentEditingMCPName = null; // 加载外部MCP列表 async function loadExternalMCPs() { try { const response = await apiFetch('/api/external-mcp'); if (!response.ok) { throw new Error('获取外部MCP列表失败'); } const data = await response.json(); renderExternalMCPList(data.servers || {}); renderExternalMCPStats(data.stats || {}); } catch (error) { console.error('加载外部MCP列表失败:', error); const list = document.getElementById('external-mcp-list'); if (list) { list.innerHTML = `
加载失败: ${escapeHtml(error.message)}
`; } } } // 渲染外部MCP列表 function renderExternalMCPList(servers) { const list = document.getElementById('external-mcp-list'); if (!list) return; if (Object.keys(servers).length === 0) { list.innerHTML = '
📋 暂无外部MCP配置
点击"添加外部MCP"按钮开始配置
'; return; } let html = '
'; for (const [name, server] of Object.entries(servers)) { const status = server.status || 'disconnected'; const statusClass = status === 'connected' ? 'status-connected' : status === 'connecting' ? 'status-connecting' : status === 'error' ? 'status-error' : status === 'disabled' ? 'status-disabled' : 'status-disconnected'; const statusText = status === 'connected' ? '已连接' : status === 'connecting' ? '连接中...' : status === 'error' ? '连接失败' : status === 'disabled' ? '已禁用' : '未连接'; const transport = server.config.transport || (server.config.command ? 'stdio' : 'http'); const transportIcon = transport === 'stdio' ? '⚙️' : '🌐'; html += `

${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `🔧 ${server.tool_count}` : ''}

${statusText}
${status === 'connected' || status === 'disconnected' || status === 'error' ? `` : status === 'connecting' ? `` : ''}
${status === 'error' && server.error ? `
❌ 连接错误:${escapeHtml(server.error)}
` : ''}
传输模式 ${transportIcon} ${escapeHtml(transport.toUpperCase())}
${server.tool_count !== undefined && server.tool_count > 0 ? `
工具数量 🔧 ${server.tool_count} 个工具
` : server.tool_count === 0 && status === 'connected' ? `
工具数量 暂无工具
` : ''} ${server.config.description ? `
描述 ${escapeHtml(server.config.description)}
` : ''} ${server.config.timeout ? `
超时时间 ${server.config.timeout} 秒
` : ''} ${transport === 'stdio' && server.config.command ? `
命令 ${escapeHtml(server.config.command)}
` : ''} ${transport === 'http' && server.config.url ? `
URL ${escapeHtml(server.config.url)}
` : ''}
`; } html += '
'; list.innerHTML = html; } // 渲染外部MCP统计信息 function renderExternalMCPStats(stats) { const statsEl = document.getElementById('external-mcp-stats'); if (!statsEl) return; const total = stats.total || 0; const enabled = stats.enabled || 0; const disabled = stats.disabled || 0; const connected = stats.connected || 0; statsEl.innerHTML = ` 📊 总数: ${total} ✅ 已启用: ${enabled} ⏸ 已停用: ${disabled} 🔗 已连接: ${connected} `; } // 显示添加外部MCP模态框 function showAddExternalMCPModal() { currentEditingMCPName = null; document.getElementById('external-mcp-modal-title').textContent = '添加外部MCP'; document.getElementById('external-mcp-json').value = ''; document.getElementById('external-mcp-json-error').style.display = 'none'; document.getElementById('external-mcp-json-error').textContent = ''; document.getElementById('external-mcp-json').classList.remove('error'); document.getElementById('external-mcp-modal').style.display = 'block'; } // 关闭外部MCP模态框 function closeExternalMCPModal() { document.getElementById('external-mcp-modal').style.display = 'none'; currentEditingMCPName = null; } // 编辑外部MCP async function editExternalMCP(name) { try { const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`); if (!response.ok) { throw new Error('获取外部MCP配置失败'); } const server = await response.json(); currentEditingMCPName = name; document.getElementById('external-mcp-modal-title').textContent = '编辑外部MCP'; // 将配置转换为对象格式(key为名称) const config = { ...server.config }; // 移除tool_count、external_mcp_enable等前端字段,但保留enabled/disabled用于向后兼容 delete config.tool_count; delete config.external_mcp_enable; // 包装成对象格式:{ "name": { config } } const configObj = {}; configObj[name] = config; // 格式化JSON const jsonStr = JSON.stringify(configObj, null, 2); document.getElementById('external-mcp-json').value = jsonStr; document.getElementById('external-mcp-json-error').style.display = 'none'; document.getElementById('external-mcp-json-error').textContent = ''; document.getElementById('external-mcp-json').classList.remove('error'); document.getElementById('external-mcp-modal').style.display = 'block'; } catch (error) { console.error('编辑外部MCP失败:', error); alert('编辑失败: ' + error.message); } } // 格式化JSON function formatExternalMCPJSON() { const jsonTextarea = document.getElementById('external-mcp-json'); const errorDiv = document.getElementById('external-mcp-json-error'); try { const jsonStr = jsonTextarea.value.trim(); if (!jsonStr) { errorDiv.textContent = 'JSON不能为空'; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } const parsed = JSON.parse(jsonStr); const formatted = JSON.stringify(parsed, null, 2); jsonTextarea.value = formatted; errorDiv.style.display = 'none'; jsonTextarea.classList.remove('error'); } catch (error) { errorDiv.textContent = 'JSON格式错误: ' + error.message; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); } } // 加载示例 function loadExternalMCPExample() { const example = { "hexstrike-ai": { command: "python3", args: [ "/path/to/script.py", "--server", "http://example.com" ], description: "示例描述", timeout: 300 }, "cyberstrike-ai-http": { transport: "http", url: "http://127.0.0.1:8081/mcp" }, "cyberstrike-ai-sse": { transport: "sse", url: "http://127.0.0.1:8081/mcp/sse" } }; document.getElementById('external-mcp-json').value = JSON.stringify(example, null, 2); document.getElementById('external-mcp-json-error').style.display = 'none'; document.getElementById('external-mcp-json').classList.remove('error'); } // 保存外部MCP async function saveExternalMCP() { const jsonTextarea = document.getElementById('external-mcp-json'); const jsonStr = jsonTextarea.value.trim(); const errorDiv = document.getElementById('external-mcp-json-error'); if (!jsonStr) { errorDiv.textContent = 'JSON配置不能为空'; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); jsonTextarea.focus(); return; } let configObj; try { configObj = JSON.parse(jsonStr); } catch (error) { errorDiv.textContent = 'JSON格式错误: ' + error.message; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); jsonTextarea.focus(); return; } // 验证必须是对象格式 if (typeof configObj !== 'object' || Array.isArray(configObj) || configObj === null) { errorDiv.textContent = '配置错误: 必须是JSON对象格式,key为配置名称,value为配置内容'; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } // 获取所有配置名称 const names = Object.keys(configObj); if (names.length === 0) { errorDiv.textContent = '配置错误: 至少需要一个配置项'; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } // 验证每个配置 for (const name of names) { if (!name || name.trim() === '') { errorDiv.textContent = '配置错误: 配置名称不能为空'; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } const config = configObj[name]; if (typeof config !== 'object' || Array.isArray(config) || config === null) { errorDiv.textContent = `配置错误: "${name}" 的配置必须是对象`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } // 移除 external_mcp_enable 字段(由按钮控制,但保留 enabled/disabled 用于向后兼容) delete config.external_mcp_enable; // 验证配置内容 const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : ''); if (!transport) { errorDiv.textContent = `配置错误: "${name}" 需要指定command(stdio模式)或url(http/sse模式)`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } if (transport === 'stdio' && !config.command) { errorDiv.textContent = `配置错误: "${name}" stdio模式需要command字段`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } if (transport === 'http' && !config.url) { errorDiv.textContent = `配置错误: "${name}" http模式需要url字段`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } if (transport === 'sse' && !config.url) { errorDiv.textContent = `配置错误: "${name}" sse模式需要url字段`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } } // 清除错误提示 errorDiv.style.display = 'none'; jsonTextarea.classList.remove('error'); try { // 如果是编辑模式,只更新当前编辑的配置 if (currentEditingMCPName) { if (!configObj[currentEditingMCPName]) { errorDiv.textContent = `配置错误: 编辑模式下,JSON必须包含配置名称 "${currentEditingMCPName}"`; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); return; } const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(currentEditingMCPName)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ config: configObj[currentEditingMCPName] }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '保存失败'); } } else { // 添加模式:保存所有配置 for (const name of names) { const config = configObj[name]; const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ config }), }); if (!response.ok) { const error = await response.json(); throw new Error(`保存 "${name}" 失败: ${error.error || '未知错误'}`); } } } closeExternalMCPModal(); await loadExternalMCPs(); // 刷新对话界面的工具列表,使新添加的MCP工具立即可用 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } alert('保存成功'); } catch (error) { console.error('保存外部MCP失败:', error); errorDiv.textContent = '保存失败: ' + error.message; errorDiv.style.display = 'block'; jsonTextarea.classList.add('error'); } } // 删除外部MCP async function deleteExternalMCP(name) { if (!confirm(`确定要删除外部MCP "${name}" 吗?`)) { return; } try { const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '删除失败'); } await loadExternalMCPs(); // 刷新对话界面的工具列表,移除已删除的MCP工具 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } alert('删除成功'); } catch (error) { console.error('删除外部MCP失败:', error); alert('删除失败: ' + error.message); } } // 切换外部MCP启停 async function toggleExternalMCP(name, currentStatus) { const action = currentStatus === 'connected' ? 'stop' : 'start'; const buttonId = `btn-toggle-${name}`; const button = document.getElementById(buttonId); // 如果是启动操作,显示加载状态 if (action === 'start' && button) { button.disabled = true; button.style.opacity = '0.6'; button.style.cursor = 'not-allowed'; button.innerHTML = '⏳ 连接中...'; } try { const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}/${action}`, { method: 'POST', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '操作失败'); } const result = await response.json(); // 如果是启动操作,先立即检查一次状态 if (action === 'start') { // 立即检查一次状态(可能已经连接) try { const statusResponse = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`); if (statusResponse.ok) { const statusData = await statusResponse.json(); const status = statusData.status || 'disconnected'; if (status === 'connected') { // 已经连接,立即刷新 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } return; } } } catch (error) { console.error('检查状态失败:', error); } // 如果还未连接,开始轮询 await pollExternalMCPStatus(name, 30); // 最多轮询30次(约30秒) } else { // 停止操作,直接刷新 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } } } catch (error) { console.error('切换外部MCP状态失败:', error); alert('操作失败: ' + error.message); // 恢复按钮状态 if (button) { button.disabled = false; button.style.opacity = '1'; button.style.cursor = 'pointer'; button.innerHTML = '▶ 启动'; } // 刷新状态 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } } } // 轮询外部MCP状态 async function pollExternalMCPStatus(name, maxAttempts = 30) { let attempts = 0; const pollInterval = 1000; // 1秒轮询一次 while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, pollInterval)); try { const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`); if (response.ok) { const data = await response.json(); const status = data.status || 'disconnected'; // 更新按钮状态 const buttonId = `btn-toggle-${name}`; const button = document.getElementById(buttonId); if (status === 'connected') { // 连接成功,刷新列表 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } return; } else if (status === 'error' || status === 'disconnected') { // 连接失败,刷新列表并显示错误 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } if (status === 'error') { alert('连接失败,请检查配置和网络连接'); } return; } else if (status === 'connecting') { // 仍在连接中,继续轮询 attempts++; continue; } } } catch (error) { console.error('轮询状态失败:', error); } attempts++; } // 超时,刷新列表 await loadExternalMCPs(); // 刷新对话界面的工具列表 if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') { window.refreshMentionTools(); } alert('连接超时,请检查配置和网络连接'); } // 在打开设置时加载外部MCP列表 const originalOpenSettings = openSettings; openSettings = async function() { await originalOpenSettings(); await loadExternalMCPs(); };