import { log } from '../../utils/logger.js?v=0205'; // ========================================== // MCP 工具管理逻辑 // ========================================== // 全局变量 let mcpTools = []; let mcpEditingIndex = null; let mcpProperties = []; let websocket = null; // 将从外部设置 /** * 设置 WebSocket 实例 * @param {WebSocket} ws - WebSocket 连接实例 */ export function setWebSocket(ws) { websocket = ws; } /** * 初始化 MCP 工具 */ export async function initMcpTools() { // 加载默认工具数据 const defaultMcpTools = await fetch("js/config/default-mcp-tools.json").then(res => res.json()); const savedTools = localStorage.getItem('mcpTools'); if (savedTools) { try { const parsedTools = JSON.parse(savedTools); // 合并默认工具和用户保存的工具,保留用户自定义的工具 const defaultToolNames = new Set(defaultMcpTools.map(t => t.name)); // 添加默认工具中不存在的新工具 parsedTools.forEach(tool => { if (!defaultToolNames.has(tool.name)) { defaultMcpTools.push(tool); } }); mcpTools = defaultMcpTools; } catch (e) { log('加载MCP工具失败,使用默认工具', 'warning'); mcpTools = [...defaultMcpTools]; } } else { mcpTools = [...defaultMcpTools]; } renderMcpTools(); setupMcpEventListeners(); } /** * 渲染工具列表 */ function renderMcpTools() { const container = document.getElementById('mcpToolsContainer'); const countSpan = document.getElementById('mcpToolsCount'); if (!container) { return; // Container not found, skip rendering } if (countSpan) { countSpan.textContent = `${mcpTools.length} 个工具`; } if (mcpTools.length === 0) { container.innerHTML = '
暂无工具,点击下方按钮添加新工具
'; return; } container.innerHTML = mcpTools.map((tool, index) => { const paramCount = tool.inputSchema.properties ? Object.keys(tool.inputSchema.properties).length : 0; const requiredCount = tool.inputSchema.required ? tool.inputSchema.required.length : 0; const hasMockResponse = tool.mockResponse && Object.keys(tool.mockResponse).length > 0; return `
${tool.name}
${tool.description}
参数数量: ${paramCount} 个 ${requiredCount > 0 ? `(${requiredCount} 个必填)` : ''}
模拟返回: ${hasMockResponse ? '✅ 已配置: ' + JSON.stringify(tool.mockResponse) : '⚪ 使用默认'}
`; }).join(''); } /** * 渲染参数列表 */ function renderMcpProperties() { const container = document.getElementById('mcpPropertiesContainer'); const emptyState = document.getElementById('mcpEmptyState'); if (!container) { return; // Container not found, skip rendering } if (mcpProperties.length === 0) { if (emptyState) { emptyState.style.display = 'block'; } container.innerHTML = ''; return; } if (emptyState) { emptyState.style.display = 'none'; } container.innerHTML = mcpProperties.map((prop, index) => `
参数名称 ${prop.name}${prop.required ? ' [必填]' : ''}
数据类型 ${getTypeLabel(prop.type)}
描述 ${prop.description || '-'}
`).join(''); } /** * 获取数据类型标签 */ function getTypeLabel(type) { const typeMap = { 'string': '字符串', 'integer': '整数', 'number': '数字', 'boolean': '布尔值', 'array': '数组', 'object': '对象' }; return typeMap[type] || type; } /** * 添加参数 - 打开参数编辑模态框 */ function addMcpProperty() { openPropertyModal(); } /** * 编辑参数 - 打开参数编辑模态框 */ function editMcpProperty(index) { openPropertyModal(index); } /** * 打开参数编辑模态框 */ function openPropertyModal(index = null) { const form = document.getElementById('mcpPropertyForm'); const title = document.getElementById('mcpPropertyModalTitle'); document.getElementById('mcpPropertyIndex').value = index !== null ? index : -1; if (index !== null) { const prop = mcpProperties[index]; title.textContent = '编辑参数'; document.getElementById('mcpPropertyName').value = prop.name; document.getElementById('mcpPropertyType').value = prop.type || 'string'; document.getElementById('mcpPropertyMinimum').value = prop.minimum !== undefined ? prop.minimum : ''; document.getElementById('mcpPropertyMaximum').value = prop.maximum !== undefined ? prop.maximum : ''; document.getElementById('mcpPropertyDescription').value = prop.description || ''; document.getElementById('mcpPropertyRequired').checked = prop.required || false; } else { title.textContent = '添加参数'; form.reset(); document.getElementById('mcpPropertyName').value = `param_${mcpProperties.length + 1}`; document.getElementById('mcpPropertyType').value = 'string'; document.getElementById('mcpPropertyMinimum').value = ''; document.getElementById('mcpPropertyMaximum').value = ''; document.getElementById('mcpPropertyDescription').value = ''; document.getElementById('mcpPropertyRequired').checked = false; } updatePropertyRangeVisibility(); document.getElementById('mcpPropertyModal').style.display = 'flex'; } /** * 关闭参数编辑模态框 */ function closePropertyModal() { document.getElementById('mcpPropertyModal').style.display = 'none'; } /** * 更新数值范围输入框的可见性 */ function updatePropertyRangeVisibility() { const type = document.getElementById('mcpPropertyType').value; const rangeGroup = document.getElementById('mcpPropertyRangeGroup'); if (type === 'integer' || type === 'number') { rangeGroup.style.display = 'block'; } else { rangeGroup.style.display = 'none'; } } /** * 处理参数表单提交 */ function handlePropertySubmit(e) { e.preventDefault(); const index = parseInt(document.getElementById('mcpPropertyIndex').value); const name = document.getElementById('mcpPropertyName').value.trim(); const type = document.getElementById('mcpPropertyType').value; const minimum = document.getElementById('mcpPropertyMinimum').value; const maximum = document.getElementById('mcpPropertyMaximum').value; const description = document.getElementById('mcpPropertyDescription').value.trim(); const required = document.getElementById('mcpPropertyRequired').checked; // 检查名称重复 const isDuplicate = mcpProperties.some((p, i) => i !== index && p.name === name); if (isDuplicate) { alert('参数名称已存在,请使用不同的名称'); return; } const propData = { name, type, description, required }; // 数值类型添加范围限制 if (type === 'integer' || type === 'number') { if (minimum !== '') { propData.minimum = parseFloat(minimum); } if (maximum !== '') { propData.maximum = parseFloat(maximum); } } if (index >= 0) { mcpProperties[index] = propData; } else { mcpProperties.push(propData); } renderMcpProperties(); closePropertyModal(); } /** * 删除参数 */ function deleteMcpProperty(index) { mcpProperties.splice(index, 1); renderMcpProperties(); } /** * 设置事件监听 */ function setupMcpEventListeners() { const panel = document.getElementById('mcpToolsPanel'); const addBtn = document.getElementById('addMcpToolBtn'); const modal = document.getElementById('mcpToolModal'); const closeBtn = document.getElementById('closeMcpModalBtn'); const cancelBtn = document.getElementById('cancelMcpBtn'); const form = document.getElementById('mcpToolForm'); const addPropertyBtn = document.getElementById('addMcpPropertyBtn'); // 参数编辑模态框相关元素 const propertyModal = document.getElementById('mcpPropertyModal'); const closePropertyBtn = document.getElementById('closeMcpPropertyModalBtn'); const cancelPropertyBtn = document.getElementById('cancelMcpPropertyBtn'); const propertyForm = document.getElementById('mcpPropertyForm'); const propertyTypeSelect = document.getElementById('mcpPropertyType'); // Return early if required elements don't exist (e.g., in test environment) if (!panel || !addBtn || !modal || !closeBtn || !cancelBtn || !form || !addPropertyBtn) { return; } addBtn.addEventListener('click', () => openMcpModal()); closeBtn.addEventListener('click', closeMcpModal); cancelBtn.addEventListener('click', closeMcpModal); addPropertyBtn.addEventListener('click', addMcpProperty); form.addEventListener('submit', handleMcpSubmit); // 参数编辑模态框事件 if (propertyModal && closePropertyBtn && cancelPropertyBtn && propertyForm && propertyTypeSelect) { closePropertyBtn.addEventListener('click', closePropertyModal); cancelPropertyBtn.addEventListener('click', closePropertyModal); propertyForm.addEventListener('submit', handlePropertySubmit); propertyTypeSelect.addEventListener('change', updatePropertyRangeVisibility); } } /** * 打开模态框 */ function openMcpModal(index = null) { const isConnected = websocket && websocket.readyState === WebSocket.OPEN; if (isConnected) { alert('WebSocket 已连接,无法编辑工具'); return; } mcpEditingIndex = index; const errorContainer = document.getElementById('mcpErrorContainer'); errorContainer.innerHTML = ''; if (index !== null) { document.getElementById('mcpModalTitle').textContent = '编辑工具'; const tool = mcpTools[index]; document.getElementById('mcpToolName').value = tool.name; document.getElementById('mcpToolDescription').value = tool.description; document.getElementById('mcpMockResponse').value = tool.mockResponse ? JSON.stringify(tool.mockResponse, null, 2) : ''; mcpProperties = []; const schema = tool.inputSchema; if (schema.properties) { Object.keys(schema.properties).forEach(key => { const prop = schema.properties[key]; mcpProperties.push({ name: key, type: prop.type || 'string', minimum: prop.minimum, maximum: prop.maximum, description: prop.description || '', required: schema.required && schema.required.includes(key) }); }); } } else { document.getElementById('mcpModalTitle').textContent = '添加工具'; document.getElementById('mcpToolForm').reset(); mcpProperties = []; } renderMcpProperties(); document.getElementById('mcpToolModal').style.display = 'flex'; } /** * 关闭模态框 */ function closeMcpModal() { document.getElementById('mcpToolModal').style.display = 'none'; mcpEditingIndex = null; document.getElementById('mcpToolForm').reset(); mcpProperties = []; document.getElementById('mcpErrorContainer').innerHTML = ''; } /** * 处理表单提交 */ function handleMcpSubmit(e) { e.preventDefault(); const errorContainer = document.getElementById('mcpErrorContainer'); errorContainer.innerHTML = ''; const name = document.getElementById('mcpToolName').value.trim(); const description = document.getElementById('mcpToolDescription').value.trim(); const mockResponseText = document.getElementById('mcpMockResponse').value.trim(); // 检查名称重复 const isDuplicate = mcpTools.some((tool, index) => tool.name === name && index !== mcpEditingIndex); if (isDuplicate) { showMcpError('工具名称已存在,请使用不同的名称'); return; } // 解析模拟返回结果 let mockResponse = null; if (mockResponseText) { try { mockResponse = JSON.parse(mockResponseText); } catch (e) { showMcpError('模拟返回结果不是有效的 JSON 格式: ' + e.message); return; } } // 构建 inputSchema const inputSchema = { type: "object", properties: {}, required: [] }; mcpProperties.forEach(prop => { const propSchema = { type: prop.type }; if (prop.description) { propSchema.description = prop.description; } if ((prop.type === 'integer' || prop.type === 'number')) { if (prop.minimum !== undefined && prop.minimum !== '') { propSchema.minimum = prop.minimum; } if (prop.maximum !== undefined && prop.maximum !== '') { propSchema.maximum = prop.maximum; } } inputSchema.properties[prop.name] = propSchema; if (prop.required) { inputSchema.required.push(prop.name); } }); if (inputSchema.required.length === 0) { delete inputSchema.required; } const tool = { name, description, inputSchema, mockResponse }; if (mcpEditingIndex !== null) { mcpTools[mcpEditingIndex] = tool; log(`已更新工具: ${name}`, 'success'); } else { mcpTools.push(tool); log(`已添加工具: ${name}`, 'success'); } saveMcpTools(); renderMcpTools(); closeMcpModal(); } /** * 显示错误 */ function showMcpError(message) { const errorContainer = document.getElementById('mcpErrorContainer'); errorContainer.innerHTML = `
${message}
`; } /** * 编辑工具 */ function editMcpTool(index) { openMcpModal(index); } /** * 删除工具 */ function deleteMcpTool(index) { const isConnected = websocket && websocket.readyState === WebSocket.OPEN; if (isConnected) { alert('WebSocket 已连接,无法编辑工具'); return; } if (confirm(`确定要删除工具 "${mcpTools[index].name}" 吗?`)) { const toolName = mcpTools[index].name; mcpTools.splice(index, 1); saveMcpTools(); renderMcpTools(); log(`已删除工具: ${toolName}`, 'info'); } } /** * 保存工具 */ function saveMcpTools() { localStorage.setItem('mcpTools', JSON.stringify(mcpTools)); } /** * 获取工具列表 */ export function getMcpTools() { return mcpTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); } /** * 执行工具调用 */ export async function executeMcpTool(toolName, toolArgs) { const tool = mcpTools.find(t => t.name === toolName); if (!tool) { log(`未找到工具: ${toolName}`, 'error'); return { success: false, error: `未知工具: ${toolName}` }; } // 处理拍照工具 if (toolName === 'self_camera_take_photo') { if (typeof window.takePhoto === 'function') { const question = toolArgs && toolArgs.question ? toolArgs.question : '描述一下看到的物品'; log(`正在执行拍照: ${question}`, 'info'); const result = await window.takePhoto(question); return result; } else { log('拍照功能不可用', 'warning'); return { success: false, error: '摄像头未启动或不支持拍照功能' }; } } // 如果有模拟返回结果,使用它 if (tool.mockResponse) { // 替换模板变量 let responseStr = JSON.stringify(tool.mockResponse); // 替换 ${paramName} 格式的变量 if (toolArgs) { Object.keys(toolArgs).forEach(key => { const regex = new RegExp(`\\$\\{${key}\\}`, 'g'); responseStr = responseStr.replace(regex, toolArgs[key]); }); } try { const response = JSON.parse(responseStr); log(`工具 ${toolName} 执行成功,返回模拟结果: ${responseStr}`, 'success'); return response; } catch (e) { log(`解析模拟返回结果失败: ${e.message}`, 'error'); return tool.mockResponse; } } // 没有模拟返回结果,返回默认成功消息 log(`工具 ${toolName} 执行成功,返回默认结果`, 'success'); return { success: true, message: `工具 ${toolName} 执行成功`, tool: toolName, arguments: toolArgs }; } // 暴露全局方法供 HTML 内联事件调用 window.mcpModule = { addMcpProperty, editMcpProperty, deleteMcpProperty, editMcpTool, deleteMcpTool };