From 4a944ad23ff28e29f674fe74e37b4e18aaffdd61 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 11 Jun 2026 08:31:20 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=20MCP=20=E6=8C=87=E5=BC=95=E4=B8=8E=E6=8E=92?= =?UTF-8?q?=E9=9A=9C=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tool-center.test.ts | 3 + frontend/src/App.tsx | 56 ++++++ .../ai/AIBuiltinToolsCatalog.test.tsx | 3 + .../ai/AIMCPClientInstallPanel.test.tsx | 8 + .../components/ai/AIMCPClientInstallPanel.tsx | 45 +++++ ...ToolExecutor.localAssetsInspection.test.ts | 115 +++++++++++ ...SnapshotInspectionAppHealthToolExecutor.ts | 55 +++++- .../ai/aiSnapshotInspectionToolExecutor.ts | 3 + .../components/ai/aiSupportBundleInsights.ts | 179 ++++++++++++++++++ .../ai/aiSystemContextMessages.test.ts | 3 +- .../ai/aiSystemInspectionGuidance.ts | 6 + .../messageBubble/AIMessageStatusBlocks.tsx | 1 + .../src/utils/aiBuiltinInspectionToolInfo.ts | 37 ++++ frontend/src/utils/aiBuiltinToolCatalog.ts | 5 + frontend/src/utils/aiToolRegistry.test.ts | 9 + frontend/src/utils/mcpClientInstallStatus.ts | 52 +++++ 16 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ai/aiSupportBundleInsights.ts diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 09e4bc6..c951e85 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -250,6 +250,9 @@ describe('global appearance tokens', () => { expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'mono\', installedFontFamilies)'); expect(appSource).toContain('ListInstalledFontFamilies()'); expect(appSource).toContain('const [installedFontFamilies, setInstalledFontFamilies] = useState(EMPTY_INSTALLED_FONT_FAMILIES);'); + expect(appSource).toContain('data-gonavi-linux-cjk-font-banner="true"'); + expect(appSource).toContain('Linux CJK fonts missing / Ubuntu 中文字体缺失'); + expect(appSource).toContain('setIsLinuxCJKFontBannerDismissed(true)'); expect(appSource).toContain('matchFontFamilyOption'); expect(appSource).toContain('showSearch'); expect(appSource).toContain('const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false;'); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5773610..41e26c7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2252,6 +2252,7 @@ function App() { const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme'); + const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false); @@ -3334,6 +3335,13 @@ function App() { ), [darkMode]); + const showLinuxCJKFontBanner = Boolean( + linuxCJKFontInstallHint && + hasLoadedInstalledFontsRef.current && + !isFontFamiliesLoading && + !fontFamiliesLoadError && + !isLinuxCJKFontBannerDismissed, + ); return ( + {showLinuxCJKFontBanner && ( +
+ +
+
+ Linux CJK fonts missing / Ubuntu 中文字体缺失 +
+
+ Chinese text may render as □□□. Install fonts, then restart GoNavi: + + {linuxCJKFontInstallHint} + +
+
+ + +
+ )} + { expect(markup).toContain('inspect_database_bundle'); expect(markup).toContain('AI 应用健康总览'); expect(markup).toContain('inspect_app_health'); + expect(markup).toContain('导出 AI 排障支持包'); + expect(markup).toContain('inspect_ai_support_bundle'); + expect(markup).toContain('不含密钥和数据库密码'); expect(markup).toContain('选择 AI 工具路线'); expect(markup).toContain('inspect_ai_tool_catalog'); expect(markup).toContain('每个工具 arguments 怎么填'); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx index a895972..41bf539 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx @@ -193,6 +193,14 @@ describe('AIMCPClientInstallPanel', () => { expect(markup).toContain('远程接入边界'); expect(markup).toContain('云端 Agent 只通过 MCP 工具读取连接摘要、库表和 DDL'); expect(markup).toContain('OpenClaw 远程 MCP 快速配置'); + expect(markup).toContain('公网/隧道 URL'); + expect(markup).toContain('云端 Agent 能访问到的 Streamable HTTP MCP 地址'); + expect(markup).toContain('不要填 Windows 本机的 127.0.0.1'); + expect(markup).toContain('Bearer Token'); + expect(markup).toContain('Windows 启动命令和云端 Agent 配置必须一致'); + expect(markup).toContain('不要把数据库密码当 token 填进去'); + expect(markup).toContain('本机监听地址'); + expect(markup).toContain('MCP 路径'); expect(markup).toContain('配置到云端 Agent'); expect(markup).toContain('无 GUI / CLI 生成配置'); expect(markup).toContain('"type": "streamable-http"'); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx index 4aa4a7f..8026c00 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx @@ -7,6 +7,7 @@ import { buildRemoteMCPClientQuickStart, isMCPClientKey, isRemoteMCPClientStatus, + REMOTE_MCP_PARAMETER_GUIDES, type MCPClientKey, } from '../../utils/mcpClientInstallStatus'; import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; @@ -312,6 +313,50 @@ const AIMCPClientInstallPanel: React.FC = ({
下面分别给云端 Agent、无 GUI/CLI 场景和 Windows GoNavi 使用。云端只保存 MCP URL 和 Bearer Token,不保存数据库账号密码。
+
+ {REMOTE_MCP_PARAMETER_GUIDES.map((item) => ( +
+
+
+ {item.title} +
+ + {item.required ? '必填' : '可选'} + +
+
+ 应填:{item.fill} +
+
+ 示例:{item.example} +
+
+ 避免:{item.avoid} +
+
+ ))} +
{ expect(result.content).toContain('最近工具结果较长'); }); + it('returns an ai support bundle with health, message flow, context budget, and remote MCP evidence', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_ai_support_bundle', { + keyword: 'mcp', + publicUrl: 'https://agent.example.com/mcp', + tokenConfigured: false, + }), + connections: [buildConnection()], + mcpTools: [{ + alias: 'remote_probe', + originalName: 'remote_probe', + serverId: 'server-1', + serverName: '远程工具', + description: '读取远程信息', + inputSchema: { + type: 'object', + required: ['keyword'], + properties: { + keyword: { type: 'string', description: '关键词' }, + }, + }, + }], + toolContextMap: new Map(), + aiChatSessions: [ + { id: 'session-1', title: 'AI 稳定性排障', updatedAt: 200 }, + ], + aiChatHistory: { + 'session-1': [ + { id: 'msg-1', role: 'user', content: 'AI 最近不稳定', timestamp: 101 }, + { + id: 'msg-2', + role: 'assistant', + content: '先看支持包', + timestamp: 102, + tool_calls: [{ id: 'tool-1', type: 'function', function: { name: 'inspect_app_health', arguments: '{}' } }], + }, + { id: 'msg-3', role: 'assistant', content: '继续分析', timestamp: 103 }, + ], + }, + activeSessionId: 'session-1', + aiContexts: { + 'conn-1:crm': [ + { dbName: 'crm', tableName: 'orders', ddl: 'CREATE TABLE orders(id bigint);' }, + ], + }, + skills: [{ + id: 'skill-1', + name: 'SQL 审查', + systemPrompt: '先检查风险', + enabled: true, + scopes: ['database'], + }], + runtime: { + getAIRuntimeState: vi.fn(async () => ({ + providers: [{ + id: 'provider-1', + name: 'OpenAI', + type: 'openai' as const, + apiKey: '', + baseUrl: 'https://api.example.com', + model: 'gpt-5', + hasSecret: true, + maxTokens: 4096, + temperature: 0.2, + }], + activeProviderId: 'provider-1', + safetyLevel: 'readonly', + contextLevel: 'schema_only', + })), + getMCPServers: vi.fn(async () => [{ + id: 'server-1', + name: '远程工具', + transport: 'stdio' as const, + command: 'node', + args: ['server.js'], + env: { TOKEN: 'secret-value' }, + enabled: true, + timeoutSeconds: 20, + }]), + getMCPClientInstallStatuses: vi.fn(async () => [{ + client: 'openclaw', + displayName: 'OpenClaw', + installMode: 'remote' as const, + installed: false, + matchesCurrent: false, + clientDetected: false, + message: 'OpenClaw 走远程 MCP 桥接', + }]), + readAppLogTail: vi.fn(async () => ({ + success: true, + logPath: 'C:/Users/mock/.GoNavi/Logs/gonavi.log', + lines: [ + '2026/06/11 10:00:00 [WARN] MCP mock warning', + '2026/06/11 10:00:01 [ERROR] MCP mock error', + ], + fileWindowTruncated: false, + matchedLinesTruncated: false, + })), + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"kind":"ai_support_bundle"'); + expect(result.content).toContain('"databasePasswordsIncluded":false'); + expect(result.content).toContain('"providerSecretsIncluded":false'); + expect(result.content).toContain('"mcpEnvValuesIncluded":false'); + expect(result.content).toContain('"unresolvedToolCallCount":1'); + expect(result.content).toContain('"consecutiveAssistantPairCount":1'); + expect(result.content).toContain('"remoteMCPPublicUrl":"https://agent.example.com/mcp"'); + expect(result.content).toContain('尚未确认 Bearer Token'); + expect(result.content).not.toContain('secret-value'); + }); + it('returns sql snippets so the model can inspect local query templates', async () => { const result = await executeLocalAIToolCall({ toolCall: buildToolCall('inspect_sql_snippets', { diff --git a/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts index 8dc0d75..f104977 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts @@ -1,4 +1,5 @@ import type { + AIChatMessage, AIContextItem, AIMCPToolDescriptor, AISkillConfig, @@ -9,6 +10,7 @@ import type { import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; import { buildAIAppHealthSnapshot } from './aiAppHealthInsights'; import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights'; +import { buildAISupportBundleSnapshot } from './aiSupportBundleInsights'; import type { AISnapshotInspectionRuntime, AISnapshotInspectionRuntimeState, @@ -24,6 +26,9 @@ interface ExecuteAppHealthSnapshotToolCallOptions { args: Record; activeContext?: { connectionId: string; dbName: string } | null; aiContexts?: Record; + aiChatHistory?: Record; + aiChatSessions?: Array<{ id: string; title: string; updatedAt: number }>; + activeSessionId?: string | null; connections: SavedConnection[]; tabs?: TabData[]; activeTabId?: string | null; @@ -75,6 +80,9 @@ export async function executeAppHealthSnapshotToolCall( args, activeContext = null, aiContexts = {}, + aiChatHistory = {}, + aiChatSessions = [], + activeSessionId = null, connections, tabs = [], activeTabId = null, @@ -85,7 +93,7 @@ export async function executeAppHealthSnapshotToolCall( runtime, } = options; - if (toolName !== 'inspect_app_health') { + if (toolName !== 'inspect_app_health' && toolName !== 'inspect_ai_support_bundle') { return null; } @@ -101,6 +109,49 @@ export async function executeAppHealthSnapshotToolCall( ]); const [mcpServers, mcpClientInstallStatuses] = mcpState; + if (toolName === 'inspect_ai_support_bundle') { + return { + content: JSON.stringify(buildAISupportBundleSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + safetyLevel: runtimeState?.safetyLevel, + contextLevel: runtimeState?.contextLevel, + skills, + mcpServers: Array.isArray(mcpServers) ? mcpServers : [], + mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [], + mcpTools, + dynamicModels, + builtinTools: BUILTIN_AI_TOOL_INFO, + builtinToolNames: BUILTIN_AI_TOOL_NAMES, + userPromptSettings, + activeContext, + aiContexts, + aiChatHistory, + aiChatSessions, + activeSessionId, + sessionId: args.sessionId, + connections, + tabs, + activeTabId, + appLogReadResult, + connectionFailureReadResult, + lastRenderErrorSnapshot: buildAILastRenderErrorSnapshot(), + keyword, + connectionKeyword, + lineLimit, + includeLogLines: args.includeLogLines === true, + includeMessageContent: args.includeMessageContent === true, + includeDetails: args.includeDetails === true, + publicUrl: args.publicUrl, + localAddr: args.localAddr, + path: args.path, + exposeStrategy: args.exposeStrategy, + tokenConfigured: args.tokenConfigured, + })), + success: true, + }; + } + return { content: JSON.stringify(buildAIAppHealthSnapshot({ providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], @@ -131,7 +182,7 @@ export async function executeAppHealthSnapshotToolCall( }; } catch (error: any) { return { - content: `读取 AI 应用健康总览失败: ${error?.message || error}`, + content: `${toolName === 'inspect_ai_support_bundle' ? '生成 AI 支持包失败' : '读取 AI 应用健康总览失败'}: ${error?.message || error}`, success: false, }; } diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index 8566ca8..d2b7a83 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -101,6 +101,9 @@ export async function executeSnapshotInspectionToolCall( args, activeContext, aiContexts, + aiChatHistory, + aiChatSessions, + activeSessionId, connections, tabs, activeTabId, diff --git a/frontend/src/components/ai/aiSupportBundleInsights.ts b/frontend/src/components/ai/aiSupportBundleInsights.ts new file mode 100644 index 0000000..9b7584e --- /dev/null +++ b/frontend/src/components/ai/aiSupportBundleInsights.ts @@ -0,0 +1,179 @@ +import type { + AIChatMessage, + AIContextItem, + AIMCPClientInstallStatus, + AIMCPServerConfig, + AIMCPToolDescriptor, + AIProviderConfig, + AISafetyLevel, + AISkillConfig, + AIUserPromptSettings, + SavedConnection, + TabData, +} from '../../types'; +import type { AIBuiltinToolInfo } from '../../utils/aiBuiltinToolInfo.types'; +import { buildAIAppHealthSnapshot } from './aiAppHealthInsights'; +import { buildAIMessageFlowSnapshot } from './aiChatSessionInsights'; +import { buildAIContextBudgetSnapshot } from './aiContextBudgetInsights'; +import { buildMCPRemoteAccessSnapshot } from './aiMCPRemoteAccessInsights'; +import { buildAIToolCatalogSnapshot } from './aiToolCatalogInsights'; + +const appendUnique = (items: string[], value: string) => { + const trimmed = String(value || '').trim(); + if (!trimmed || items.includes(trimmed)) { + return; + } + items.push(trimmed); +}; + +export const buildAISupportBundleSnapshot = (params: { + providers?: AIProviderConfig[]; + activeProviderId?: string | null; + safetyLevel?: AISafetyLevel | string; + contextLevel?: string; + skills?: AISkillConfig[]; + mcpServers?: AIMCPServerConfig[]; + mcpClientStatuses?: AIMCPClientInstallStatus[]; + mcpTools?: AIMCPToolDescriptor[]; + dynamicModels?: string[]; + builtinTools?: AIBuiltinToolInfo[]; + builtinToolNames?: string[]; + userPromptSettings?: AIUserPromptSettings; + activeContext?: { connectionId?: string | null; dbName?: string | null } | null; + aiContexts?: Record; + aiChatHistory?: Record; + aiChatSessions?: Array<{ id: string; title: string; updatedAt: number }>; + activeSessionId?: string | null; + sessionId?: unknown; + connections?: SavedConnection[]; + tabs?: TabData[]; + activeTabId?: string | null; + appLogReadResult?: any; + connectionFailureReadResult?: any; + lastRenderErrorSnapshot?: any; + keyword?: unknown; + connectionKeyword?: unknown; + lineLimit?: unknown; + includeLogLines?: boolean; + includeMessageContent?: boolean; + includeDetails?: boolean; + publicUrl?: string; + localAddr?: string; + path?: string; + exposeStrategy?: string; + tokenConfigured?: boolean; +}) => { + const aiChatHistory = params.aiChatHistory || {}; + const aiChatSessions = params.aiChatSessions || []; + const requestedSessionId = String(params.sessionId || params.activeSessionId || '').trim(); + const appHealth = buildAIAppHealthSnapshot({ + providers: params.providers, + activeProviderId: params.activeProviderId, + safetyLevel: params.safetyLevel, + contextLevel: params.contextLevel, + skills: params.skills, + mcpServers: params.mcpServers, + mcpClientStatuses: params.mcpClientStatuses, + mcpTools: params.mcpTools, + dynamicModels: params.dynamicModels, + builtinToolNames: params.builtinToolNames, + userPromptSettings: params.userPromptSettings, + activeContext: params.activeContext, + aiContexts: params.aiContexts, + connections: params.connections, + tabs: params.tabs, + activeTabId: params.activeTabId, + appLogReadResult: params.appLogReadResult, + connectionFailureReadResult: params.connectionFailureReadResult, + lastRenderErrorSnapshot: params.lastRenderErrorSnapshot, + keyword: params.keyword, + connectionKeyword: params.connectionKeyword, + lineLimit: params.lineLimit, + includeLogLines: params.includeLogLines === true, + }); + const messageFlow = buildAIMessageFlowSnapshot({ + aiChatSessions, + aiChatHistory, + activeSessionId: params.activeSessionId, + sessionId: requestedSessionId, + limit: 32, + includeContent: params.includeMessageContent === true, + previewLimit: 240, + }); + const contextBudget = buildAIContextBudgetSnapshot({ + aiContexts: params.aiContexts, + aiChatHistory, + aiChatSessions, + activeSessionId: params.activeSessionId, + sessionId: requestedSessionId, + messageLimit: 50, + includeDetails: params.includeDetails === true, + mcpTools: params.mcpTools, + skills: params.skills, + userPromptSettings: params.userPromptSettings, + }); + const remoteAccess = buildMCPRemoteAccessSnapshot({ + mcpClientStatuses: params.mcpClientStatuses, + publicUrl: params.publicUrl, + localAddr: params.localAddr, + path: params.path, + exposeStrategy: params.exposeStrategy, + tokenConfigured: params.tokenConfigured, + }); + const toolCatalog = buildAIToolCatalogSnapshot({ + builtinTools: params.builtinTools || [], + mcpTools: params.mcpTools, + keyword: String(params.keyword || 'ai mcp 日志 连接 上下文').trim(), + includeMCPTools: true, + limit: 10, + }); + + const warnings: string[] = []; + const nextActions: string[] = []; + appHealth.warnings.forEach((item) => appendUnique(warnings, item)); + contextBudget.warnings.forEach((item) => appendUnique(warnings, item)); + messageFlow.warnings.forEach((item) => appendUnique(warnings, item)); + remoteAccess.warnings.forEach((item) => appendUnique(warnings, item)); + appHealth.nextActions.forEach((item) => appendUnique(nextActions, item)); + contextBudget.nextActions.forEach((item) => appendUnique(nextActions, item)); + messageFlow.nextActions.forEach((item) => appendUnique(nextActions, item)); + remoteAccess.nextActions.forEach((item) => appendUnique(nextActions, item)); + + return { + kind: 'ai_support_bundle', + message: '已生成 GoNavi AI 支持包快照,可用于排查 AI、MCP、日志、连接和上下文体量问题', + privacy: { + databasePasswordsIncluded: false, + providerSecretsIncluded: false, + mcpEnvValuesIncluded: false, + logLinesIncluded: params.includeLogLines === true, + messageContentIncluded: params.includeMessageContent === true, + note: '默认只返回摘要和结构化计数;只有显式开启 includeLogLines/includeMessageContent 时才附带日志或消息内容预览。', + }, + summary: { + appHealthStatus: appHealth.status, + appHealthReady: appHealth.ready, + aiSetupStatus: appHealth.summary.aiSetupStatus, + chatReady: appHealth.summary.chatReady, + contextRiskLevel: contextBudget.riskLevel, + estimatedInputChars: contextBudget.estimatedInputChars, + messageFlowWarningCount: messageFlow.warnings.length, + unresolvedToolCallCount: messageFlow.unresolvedToolCallCount, + consecutiveAssistantPairCount: messageFlow.consecutiveAssistantPairCount, + appLogErrorCount: appHealth.summary.appLogErrorCount, + appLogWarnCount: appHealth.summary.appLogWarnCount, + recentConnectionFailureCount: appHealth.summary.recentConnectionFailureCount, + mcpServerCount: appHealth.summary.mcpServerCount, + discoveredMCPToolCount: appHealth.summary.discoveredMCPToolCount, + remoteMCPPublicUrl: remoteAccess.endpoint.publicUrl, + toolCatalogReturned: toolCatalog.returned, + }, + warnings, + nextActions, + appHealth, + messageFlow, + contextBudget, + remoteAccess, + toolCatalog, + }; +}; diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index fdfc0d0..134e9ae 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => { connections: [connections[0]], tabs: [], activeTabId: null, - availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_ai_tool_catalog', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_support_bundle', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_ai_tool_catalog', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); @@ -76,6 +76,7 @@ describe('buildAISystemContextMessages', () => { const joined = messages.map((message) => message.content).join('\n'); expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区'); expect(joined).toContain('inspect_app_health 获取 AI 配置、应用日志、连接失败、回复气泡渲染异常和工作区页签的全局健康总览'); + expect(joined).toContain('inspect_ai_support_bundle 生成不含密钥和数据库密码的支持包'); expect(joined).toContain('inspect_ai_setup_health 先拿到整体现状'); expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态'); expect(joined).toContain('inspect_ai_safety 读取真实安全边界'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index 9047638..9274a7f 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -61,6 +61,12 @@ export const appendDatabaseInspectionGuidanceMessages = ( 'inspect_app_health', '如果用户提到“AI 不稳定”“整体帮我看看”“GoNavi AI 现在还有哪些明显问题”“连接、MCP、日志一起排查”或“AI 回复气泡显示异常”,优先调用 inspect_app_health 获取 AI 配置、应用日志、连接失败、回复气泡渲染异常和工作区页签的全局健康总览,再决定下钻 inspect_ai_setup_health、inspect_app_logs、inspect_recent_connection_failures 或 inspect_ai_last_render_error。', ); + appendGuidanceIfToolAvailable( + messages, + availableToolNames, + 'inspect_ai_support_bundle', + '如果用户提到“AI 不成熟/不稳定”“帮我导出排障材料”“MCP、连接、日志、上下文一起看”或准备把问题交给开发定位,优先调用 inspect_ai_support_bundle 生成不含密钥和数据库密码的支持包,再根据 warnings 和 nextActions 下钻。', + ); appendGuidanceIfToolAvailable( messages, availableToolNames, diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index efbc796..9f7cbcb 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -28,6 +28,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_ai_providers: '读取当前 AI 供应商与模型配置', inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态', inspect_ai_tool_catalog: '读取 AI 工具目录和参数提示', + inspect_ai_support_bundle: '生成 AI 排障支持包', inspect_mcp_setup: '读取当前 MCP 配置状态', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引', inspect_mcp_draft: '校验 MCP 新增草稿', diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts index 708bb00..0acd1b7 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -26,6 +26,43 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_ai_support_bundle", + icon: "📦", + desc: "导出 AI 排障支持包", + detail: + "一次性汇总 AI 应用健康、供应商与 MCP 状态、应用日志摘要、连接失败摘要、消息流结构、上下文体量、远程 MCP 接入和工具目录索引。适合用户反馈“AI 不稳定”“MCP/连接/日志一起看”“要给开发排障材料”时先生成一份不含密钥和数据库密码的支持包。", + params: "keyword?, sessionId?, lineLimit?(默认 120), includeLogLines?(默认 false), includeMessageContent?(默认 false), publicUrl?, tokenConfigured?", + tool: { + type: "function", + function: { + name: "inspect_ai_support_bundle", + description: + "生成 GoNavi AI 排障支持包,汇总 AI 应用健康、供应商和发送前置、MCP 配置和远程接入、应用日志摘要、数据库连接失败摘要、当前 AI 消息流、上下文体量风险和工具目录索引。默认不包含数据库密码、供应商密钥、MCP 环境变量值、日志原文或完整消息内容。适用于用户反馈 AI 不稳定、MCP/连接/日志问题交织、需要一次性导出排障证据或准备给开发定位时优先调用。", + parameters: { + type: "object", + properties: { + keyword: { type: "string", description: "可选,按关键词过滤日志和工具目录,例如 ai、mcp、mysql、error、openclaw" }, + connectionKeyword: { type: "string", description: "可选,分析连接失败日志时使用的关键词;不传时复用 keyword" }, + sessionId: { type: "string", description: "可选,指定要诊断的 AI 会话 ID;不传时使用当前活动会话" }, + lineLimit: { type: "number", description: "可选,最多分析多少行应用日志,默认 120,最大 240" }, + includeLogLines: { type: "boolean", description: "可选,是否附带日志原文行,默认 false;需要引用原文时再开启" }, + includeMessageContent: { type: "boolean", description: "可选,是否附带消息内容预览,默认 false;排查气泡内容时再开启" }, + includeDetails: { type: "boolean", description: "可选,是否附带上下文体量明细,默认 false" }, + publicUrl: { type: "string", description: "可选,云端 Agent 访问 GoNavi MCP 的公网/隧道 URL,用于远程 MCP 支持包" }, + localAddr: { type: "string", description: "可选,Windows 本机 HTTP MCP 监听地址,默认 127.0.0.1:8765" }, + path: { type: "string", description: "可选,Streamable HTTP MCP 路径,默认 /mcp" }, + exposeStrategy: { + type: "string", + enum: ["reverse_proxy", "ssh_reverse_tunnel", "cloudflare_tunnel", "tailscale", "custom"], + description: "可选,远程暴露方式,用于生成对应安全提醒", + }, + tokenConfigured: { type: "boolean", description: "可选,是否已经准备随机 Bearer Token;传 false 会返回鉴权告警" }, + }, + }, + }, + }, + }, { name: "inspect_ai_setup_health", icon: "🩺", diff --git a/frontend/src/utils/aiBuiltinToolCatalog.ts b/frontend/src/utils/aiBuiltinToolCatalog.ts index be53a42..13e7945 100644 --- a/frontend/src/utils/aiBuiltinToolCatalog.ts +++ b/frontend/src/utils/aiBuiltinToolCatalog.ts @@ -44,6 +44,11 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ steps: 'inspect_app_health -> inspect_ai_setup_health / inspect_app_logs / inspect_recent_connection_failures / inspect_ai_last_render_error / inspect_ai_message_flow', description: '适合用户反馈 AI 不稳定、连接和 MCP 问题交织、回复气泡显示异常,或需要先看整体健康状态时,一次汇总配置、日志、连接失败、渲染异常、消息流和工作区现场。', }, + { + title: '导出 AI 排障支持包', + steps: 'inspect_ai_support_bundle -> inspect_app_health / inspect_ai_context_budget / inspect_ai_message_flow / inspect_mcp_remote_access', + description: '适合需要一次性带走排障证据,或用户反馈 AI 不成熟、不稳定、MCP/连接/日志/上下文都可能相关时,先生成不含密钥和数据库密码的支持包。', + }, { title: '选择 AI 工具路线', steps: 'inspect_ai_tool_catalog -> inspect_ai_runtime / inspect_mcp_setup', diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index 53790b3..c62c9fc 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -17,6 +17,14 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('聊天发送前置'); }); + it('registers the ai-support-bundle inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_support_bundle'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('排障支持包'); + expect(info?.tool.function.description).toContain('默认不包含数据库密码'); + expect(info?.tool.function.parameters?.properties?.includeMessageContent?.description).toContain('默认 false'); + }); + it('registers the ai-safety inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_safety'); expect(info).toBeTruthy(); @@ -222,6 +230,7 @@ describe('aiToolRegistry', () => { expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_setup_health')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_ai_support_bundle')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_safety')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_providers')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_chat_readiness')).toBe(true); diff --git a/frontend/src/utils/mcpClientInstallStatus.ts b/frontend/src/utils/mcpClientInstallStatus.ts index 7c2836c..13349ca 100644 --- a/frontend/src/utils/mcpClientInstallStatus.ts +++ b/frontend/src/utils/mcpClientInstallStatus.ts @@ -18,6 +18,58 @@ export interface RemoteMCPClientQuickStart { securityNotes: string[]; } +export interface RemoteMCPParameterGuide { + key: string; + title: string; + required: boolean; + fill: string; + example: string; + avoid: string; +} + +export const REMOTE_MCP_PARAMETER_GUIDES: RemoteMCPParameterGuide[] = [ + { + key: 'publicUrl', + title: '公网/隧道 URL', + required: true, + fill: '填云端 Agent 能访问到的 Streamable HTTP MCP 地址,通常以 /mcp 结尾。', + example: 'https://agent-gateway.example.com/mcp', + avoid: '不要填 Windows 本机的 127.0.0.1;云端 Linux 访问不到这个地址。', + }, + { + key: 'bearerToken', + title: 'Bearer Token', + required: true, + fill: '填一段随机长 token,Windows 启动命令和云端 Agent 配置必须一致。', + example: 'Authorization: Bearer gnv_xxx', + avoid: '不要使用空 token、短 token,也不要把数据库密码当 token 填进去。', + }, + { + key: 'localAddr', + title: '本机监听地址', + required: true, + fill: 'Windows GoNavi HTTP MCP 默认监听 127.0.0.1:8765,再交给隧道或反向代理转发。', + example: DEFAULT_REMOTE_MCP_LOCAL_ADDR, + avoid: '没有网关隔离时不要直接绑定 0.0.0.0 暴露到公网。', + }, + { + key: 'path', + title: 'MCP 路径', + required: true, + fill: '本机启动命令、隧道 URL 和云端 Agent 配置里的路径要保持一致。', + example: DEFAULT_REMOTE_MCP_PATH, + avoid: '不要一边用 /mcp,另一边配置 /api/mcp,路径不一致会 404。', + }, + { + key: 'serverId', + title: '服务 ID', + required: false, + fill: '给云端 Agent 识别这条 MCP 服务的名称,默认 gonavi 即可。', + example: 'gonavi', + avoid: '不要频繁改名,否则 Agent 里已有的工具引用可能失效。', + }, +]; + export const EMPTY_MCP_CLIENT_STATUSES: AIMCPClientInstallStatus[] = [ { client: 'claude-code',