// 设置相关功能
let currentConfig = null;
let allTools = [];
// 全局工具状态映射,用于保存用户在所有页面的修改
// key: tool.name, value: { enabled: boolean, is_external: boolean, external_mcp: string }
let toolStateMap = new Map();
// 从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) {
retrievalWeightInput.value = knowledge.retrieval?.hybrid_weight || 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 => {
if (!toolStateMap.has(tool.name)) {
toolStateMap.set(tool.name, {
enabled: tool.enabled,
is_external: tool.is_external || false,
external_mcp: tool.external_mcp || ''
});
}
});
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 toolName = item.dataset.toolName;
const isExternal = item.dataset.isExternal === 'true';
const externalMcp = item.dataset.externalMcp || '';
if (toolName && checkbox) {
toolStateMap.set(toolName, {
enabled: checkbox.checked,
is_external: isExternal,
external_mcp: externalMcp
});
}
});
}
// 搜索工具
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 toolItem = document.createElement('div');
toolItem.className = 'tool-item';
toolItem.dataset.toolName = tool.name; // 保存原始工具名称
toolItem.dataset.isExternal = tool.is_external ? 'true' : 'false';
toolItem.dataset.externalMcp = tool.external_mcp || '';
// 从全局状态映射获取工具状态,如果不存在则使用服务器返回的状态
const toolState = toolStateMap.get(tool.name) || {
enabled: tool.enabled,
is_external: tool.is_external || false,
external_mcp: tool.external_mcp || ''
};
// 外部工具标签,显示来源信息
let externalBadge = '';
if (toolState.is_external) {
const externalMcpName = toolState.external_mcp || '';
const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部';
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
externalBadge = `${badgeText}`;
}
toolItem.innerHTML = `
`;
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 = `
`;
toolsList.appendChild(pagination);
}
// 处理工具checkbox状态变化
function handleToolCheckboxChange(toolName, enabled) {
// 更新全局状态映射
const toolItem = document.querySelector(`.tool-item[data-tool-name="${toolName}"]`);
if (toolItem) {
const isExternal = toolItem.dataset.isExternal === 'true';
const externalMcp = toolItem.dataset.externalMcp || '';
toolStateMap.set(toolName, {
enabled: enabled,
is_external: isExternal,
external_mcp: externalMcp
});
}
updateToolsStats();
}
// 全选工具
function selectAllTools() {
document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
// 更新全局状态映射
const toolItem = checkbox.closest('.tool-item');
if (toolItem) {
const toolName = toolItem.dataset.toolName;
const isExternal = toolItem.dataset.isExternal === 'true';
const externalMcp = toolItem.dataset.externalMcp || '';
if (toolName) {
toolStateMap.set(toolName, {
enabled: true,
is_external: isExternal,
external_mcp: externalMcp
});
}
}
});
updateToolsStats();
}
// 全不选工具
function deselectAllTools() {
document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
// 更新全局状态映射
const toolItem = checkbox.closest('.tool-item');
if (toolItem) {
const toolName = toolItem.dataset.toolName;
const isExternal = toolItem.dataset.isExternal === 'true';
const externalMcp = toolItem.dataset.externalMcp || '';
if (toolName) {
toolStateMap.set(toolName, {
enabled: false,
is_external: isExternal,
external_mcp: externalMcp
});
}
}
});
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 savedState = toolStateMap.get(tool.name);
if (savedState !== undefined) {
return savedState.enabled;
}
const checkbox = document.getElementById(`tool-${tool.name}`);
return checkbox ? checkbox.checked : tool.enabled;
}).length;
} else {
// 没有搜索时,需要获取所有工具的状态
// 先使用全局状态映射和当前页的checkbox状态
const localStateMap = new Map();
// 从当前页的checkbox获取状态(如果全局映射中没有)
allTools.forEach(tool => {
const savedState = toolStateMap.get(tool.name);
if (savedState !== undefined) {
localStateMap.set(tool.name, savedState.enabled);
} else {
const checkbox = document.getElementById(`tool-${tool.name}`);
if (checkbox) {
localStateMap.set(tool.name, checkbox.checked);
} else {
// 如果checkbox不存在(不在当前页),使用工具原始状态
localStateMap.set(tool.name, 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 => {
// 优先使用全局状态映射,否则使用服务器返回的状态
if (!localStateMap.has(tool.name)) {
const savedState = toolStateMap.get(tool.name);
localStateMap.set(tool.name, 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: parseFloat(document.getElementById('knowledge-retrieval-similarity-threshold')?.value) || 0.7,
hybrid_weight: parseFloat(document.getElementById('knowledge-retrieval-hybrid-weight')?.value) || 0.7
}
};
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 savedState = toolStateMap.get(tool.name);
allToolsMap.set(tool.name, {
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 => {
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, toolName) => {
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 savedState = toolStateMap.get(tool.name);
allToolsMap.set(tool.name, {
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 => {
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, toolName) => {
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 += `
${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
}
};
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模式)`;
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;
}
}
// 清除错误提示
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();
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();
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();
return;
}
}
} catch (error) {
console.error('检查状态失败:', error);
}
// 如果还未连接,开始轮询
await pollExternalMCPStatus(name, 30); // 最多轮询30次(约30秒)
} else {
// 停止操作,直接刷新
await loadExternalMCPs();
}
} 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();
}
}
// 轮询外部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();
return;
} else if (status === 'error' || status === 'disconnected') {
// 连接失败,刷新列表并显示错误
await loadExternalMCPs();
if (status === 'error') {
alert('连接失败,请检查配置和网络连接');
}
return;
} else if (status === 'connecting') {
// 仍在连接中,继续轮询
attempts++;
continue;
}
}
} catch (error) {
console.error('轮询状态失败:', error);
}
attempts++;
}
// 超时,刷新列表
await loadExternalMCPs();
alert('连接超时,请检查配置和网络连接');
}
// 在打开设置时加载外部MCP列表
const originalOpenSettings = openSettings;
openSettings = async function() {
await originalOpenSettings();
await loadExternalMCPs();
};