From b11b662071f11e7e0aa3b60d96f1422bc225e8e7 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 23:52:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9B=AE=E5=BD=95=E4=B8=8E=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=20MCP=20=E6=8E=A5=E5=85=A5=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 inspect_ai_tool_catalog 工具,返回内置探针流程、参数提示和 MCP 工具摘要 - 拆分 AI 内置工具目录配置,降低 AIBuiltinToolsCatalog 体积 - 补充 OpenClaw/Hermans 远程 MCP Streamable HTTP 配置说明 - 增加 Linux CJK 字体缺失检测与 Ubuntu 安装提示 --- cmd/gonavi-mcp-server/README.md | 16 ++ frontend/src/App.tsx | 27 +- .../ai/AIBuiltinToolsCatalog.test.tsx | 3 + .../components/ai/AIBuiltinToolsCatalog.tsx | 224 +---------------- ...calToolExecutor.aiConfigInspection.test.ts | 41 ++++ ...iSnapshotInspectionAIConfigToolExecutor.ts | 14 ++ .../ai/aiSystemContextMessages.test.ts | 3 +- .../ai/aiSystemInspectionGuidance.ts | 6 + .../components/ai/aiToolCatalogInsights.ts | 156 ++++++++++++ .../messageBubble/AIMessageStatusBlocks.tsx | 1 + .../src/utils/aiBuiltinInspectionToolInfo.ts | 25 ++ frontend/src/utils/aiBuiltinToolCatalog.ts | 232 ++++++++++++++++++ frontend/src/utils/aiToolRegistry.test.ts | 10 + frontend/src/utils/fontFamilies.test.ts | 33 +++ frontend/src/utils/fontFamilies.ts | 58 +++++ .../src/utils/mcpClientInstallStatus.test.ts | 3 + frontend/src/utils/mcpClientInstallStatus.ts | 20 ++ 17 files changed, 650 insertions(+), 222 deletions(-) create mode 100644 frontend/src/components/ai/aiToolCatalogInsights.ts create mode 100644 frontend/src/utils/aiBuiltinToolCatalog.ts create mode 100644 frontend/src/utils/fontFamilies.test.ts diff --git a/cmd/gonavi-mcp-server/README.md b/cmd/gonavi-mcp-server/README.md index 24d240a..8e88217 100644 --- a/cmd/gonavi-mcp-server/README.md +++ b/cmd/gonavi-mcp-server/README.md @@ -109,6 +109,22 @@ OpenClaw、Hermans 这类部署在云端或远端 Linux 的 Agent,不能直接 4. 在 OpenClaw / Hermans 中添加远程 MCP Server,transport 选择 Streamable HTTP,URL 指向 `/mcp` 地址,并设置请求头 `Authorization: Bearer <随机token>`。 5. 先调用 `get_connections` 获取 `connectionId`,再调用 `get_databases`、`get_tables`、`get_columns`、`get_table_ddl` 等工具读取结构。 +如果目标 Agent 支持 `mcpServers` JSON,可按下面的通用片段配置: + +```json +{ + "mcpServers": { + "gonavi": { + "type": "streamable-http", + "url": "https://<你的域名或隧道地址>/mcp", + "headers": { + "Authorization": "Bearer <随机token>" + } + } + } +} +``` + 不要把数据库 `host/user/password` 写入云端 Agent 的配置文件。`execute_sql` 写操作仍受 GoNavi AI 安全设置控制,且必须显式传 `allowMutating=true`。 ## MCP 客户端配置示例 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dc752bd..5773610 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,7 +20,7 @@ import SecurityUpdateSettingsModal from './components/SecurityUpdateSettingsModa import { DEFAULT_APPEARANCE, useStore } from './store'; import { SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; import { blurToFilter, isMacLikePlatform, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; -import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies'; +import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, getLinuxCJKFontInstallHint, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies'; import { DENSITY_OPTIONS, sanitizeDataTableDensity, @@ -393,6 +393,7 @@ function App() { () => buildFontFamilyOptions(runtimePlatform, 'mono', installedFontFamilies), [installedFontFamilies, runtimePlatform], ); + const linuxCJKFontInstallHint = getLinuxCJKFontInstallHint(runtimePlatform, installedFontFamilies); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false); const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? 1280 : window.innerWidth || 1280)); @@ -2258,7 +2259,9 @@ function App() { const tabDisplaySettingsPanelRef = useRef(null); const [tabDisplaySettingsFocusRequest, setTabDisplaySettingsFocusRequest] = useState(0); useEffect(() => { - if (!isThemeModalOpen || themeModalSection !== 'appearance') { + const shouldLoadInstalledFonts = + runtimePlatform === 'linux' || (isThemeModalOpen && themeModalSection === 'appearance'); + if (!shouldLoadInstalledFonts) { return; } if (hasLoadedInstalledFontsRef.current || isFontFamiliesLoading) { @@ -2306,7 +2309,7 @@ function App() { return () => { cancelled = true; }; - }, [isThemeModalOpen, themeModalSection]); + }, [isThemeModalOpen, runtimePlatform, themeModalSection]); useEffect(() => { if (!isThemeModalOpen || themeModalSection !== 'appearance' || tabDisplaySettingsFocusRequest === 0) { @@ -4400,6 +4403,24 @@ function App() { ? `已读取当前系统 ${installedFontFamilies.length} 个字体族,支持输入搜索匹配。清空后回退默认 UI 字体。` : '按当前系统实时加载已安装字体,支持输入搜索匹配。清空后回退默认 UI 字体。')} + {linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && ( +
+ Ubuntu/Linux 未检测到中文 CJK 字体,界面可能显示方框。请安装: + {linuxCJKFontInstallHint} + ,然后重启 GoNavi。 +
+ )}
代码字体 (Mono Font Family)
diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index a29f9e1..567a942 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -28,6 +28,9 @@ describe('AIBuiltinToolsCatalog', () => { 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_tool_catalog'); + expect(markup).toContain('每个工具 arguments 怎么填'); expect(markup).toContain('一键体检 AI 配置'); expect(markup).toContain('inspect_ai_setup_health'); expect(markup).toContain('查看 AI 当前能力'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 14aefae..dbec53f 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { ToolOutlined } from '@ant-design/icons'; -import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; import { - BUILTIN_AI_TOOL_INFO, - type AIBuiltinToolInfo, -} from '../../utils/aiToolRegistry'; + BUILTIN_TOOL_FLOWS, + describeBuiltinToolParameters, +} from '../../utils/aiBuiltinToolCatalog'; +import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; +import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; interface AIBuiltinToolsCatalogProps { darkMode: boolean; @@ -14,219 +15,6 @@ interface AIBuiltinToolsCatalogProps { cardBorder: string; } -const BUILTIN_TOOL_FLOWS = [ - { - title: '定位表与字段', - steps: 'get_connections → get_databases → get_tables → get_columns', - description: '适合先找连接、找库、找表,再确认真实字段名后生成 SQL。', - }, - { - title: '字段反查表', - steps: 'get_databases → get_all_columns', - description: '适合只知道字段名、业务含义或注释关键词,但还不确定具体落在哪张表。', - }, - { - title: '结构深挖', - steps: 'get_columns → get_indexes → get_foreign_keys → get_triggers → get_table_ddl', - description: '适合做索引优化、关系梳理、隐式副作用排查和 DDL 审查。', - }, - { - title: '一键结构快照', - steps: 'inspect_table_bundle', - description: '适合一次带回字段、索引、外键、触发器和 DDL;必要时还能附带样例行,减少来回调用。', - }, - { - title: '全库快速摸底', - steps: 'inspect_database_bundle → inspect_table_bundle', - description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。', - }, - { - title: 'AI 应用健康总览', - 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_setup_health → inspect_ai_providers / inspect_mcp_setup / inspect_ai_guidance', - description: '适合先拿到一份 AI 配置健康快照,看清当前是供应商没配好、聊天发送前置没满足、MCP 没接入,还是提示词 / Skills / 上下文还不完整,再决定往哪条探针继续下钻。', - }, - { - title: '查看 AI 当前能力', - steps: 'inspect_ai_runtime → inspect_ai_context / inspect_current_connection', - description: '适合先确认当前模型、安全级别、上下文级别、Skills 和 MCP 工具,再决定让 AI 走哪条探针链路。', - }, - { - title: '核对写入安全边界', - steps: 'inspect_ai_safety → inspect_ai_runtime → inspect_current_connection', - description: '适合先确认当前是不是只读、DDL/DML 到底允不允许、MCP 写操作是否还需要 allowMutating,再决定后续该走查询、改数据还是改结构。', - }, - { - title: '排查供应商与模型', - steps: 'inspect_ai_providers → inspect_ai_runtime', - description: '适合先确认当前到底配置了哪些供应商、哪个在生效、有没有缺密钥或没选模型,再解释为什么 AI 不能发送、为什么模型列表为空。', - }, - { - title: '排查聊天发送状态', - steps: 'inspect_ai_chat_readiness → inspect_ai_providers', - description: '适合先确认当前聊天输入区到底缺什么前置条件,例如没选活动供应商、缺密钥、缺接口地址、没选模型,避免只凭界面现象猜测。', - }, - { - title: '排查 MCP 接入状态', - steps: 'inspect_mcp_setup → inspect_ai_runtime', - description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。', - }, - { - title: '远程 Agent 接入 GoNavi MCP', - steps: 'inspect_mcp_remote_access → inspect_mcp_setup → inspect_ai_safety', - description: '适合 OpenClaw/Hermans 部署在云端 Linux,但数据库连接和密码只在 Windows GoNavi 本机时,先生成 HTTP MCP、Bearer Token、隧道和安全边界指引。', - }, - { - title: '新增 MCP 填写指引', - steps: 'inspect_mcp_authoring_guide → inspect_mcp_draft → inspect_mcp_setup', - description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再把用户贴出的命令或草稿交给真实校验器试算,最后结合当前 MCP 配置现状判断应该新增哪种启动方式。', - }, - { - title: '查看 MCP 工具参数', - steps: 'inspect_mcp_setup → inspect_mcp_tool_schema', - description: '适合先找到当前真实发现到的 MCP 工具 alias,再读取对应 inputSchema、必填字段、枚举和嵌套参数路径,避免调用外部 MCP 工具时乱填 arguments。', - }, - { - title: '查看当前提示与 Skills', - steps: 'inspect_ai_guidance → inspect_ai_runtime', - description: '适合先确认当前自定义提示词、启用的 Skills、依赖工具和生效范围,再解释为什么 AI 当前会这样回答或为什么某个规则没有触发。', - }, - { - title: '查看当前 AI 上下文', - steps: 'inspect_ai_context → inspect_table_bundle / get_columns', - description: '适合先确认这轮对话当前到底挂了哪些表结构,再继续做字段核对、表设计评审或 SQL 生成。', - }, - { - title: '查看当前连接', - steps: 'inspect_current_connection → get_databases / get_tables', - description: '适合先确认当前活动数据源的类型、地址、当前库和 SSH/代理状态,再继续做库表探索或连接问题排查。', - }, - { - title: '核对数据源能力边界', - steps: 'inspect_connection_capabilities → inspect_current_connection', - description: '适合先确认当前连接到底支不支持建库、删库、结果编辑、SQL 导出或近似计数,再解释为什么某些按钮没出现或某类操作只能只读。', - }, - { - title: '盘点本地连接资产', - steps: 'inspect_saved_connections → inspect_current_connection / get_databases', - description: '适合先按关键词或类型筛出本地保存的数据源,再挑目标连接继续看当前状态或库表结构。', - }, - { - title: '盘点外部 SQL 目录', - steps: 'inspect_external_sql_directories → inspect_workspace_tabs / inspect_active_tab', - description: '适合先确认本地配置了哪些外部 SQL 目录、目录绑定到哪个连接/库,以及当前打开的 SQL 文件来自哪里,再继续分析脚本内容。', - }, - { - title: '读取外部 SQL 文件', - steps: 'inspect_external_sql_directories → inspect_external_sql_file → inspect_active_tab', - description: '适合先定位具体脚本路径,再直接读取目录中的 SQL 文件内容;如果这个文件已经在编辑器里打开,再继续结合当前页签草稿一起分析。', - }, - { - title: '读取当前页签', - steps: 'inspect_active_tab → get_columns / get_indexes / execute_sql', - description: '适合先读取当前编辑器里的 SQL 草稿或当前表页签,再继续做字段核对、索引分析和只读验证。', - }, - { - title: '盘点当前工作区', - steps: 'inspect_workspace_tabs → inspect_active_tab → get_columns / execute_sql', - description: '适合先看当前打开了哪些 SQL / 表 / 命令页签,再切到目标页签继续做字段核对、对比分析和只读验证。', - }, - { - title: '查看当前快捷键配置', - steps: 'inspect_shortcuts → inspect_active_tab / inspect_workspace_tabs', - description: '适合先确认当前 Win / Mac 快捷键、是否改过默认值,以及结果区、AI 面板、查询执行等动作到底该怎么按,再结合当前页签解释具体使用场景。', - }, - { - title: '回看最近执行记录', - steps: 'inspect_recent_sql_logs → get_columns / get_indexes / execute_sql', - description: '适合追查刚刚执行失败的 SQL、慢查询耗时,或基于真实执行历史继续让 AI 给解释和优化建议。', - }, - { - title: '总结最近 SQL 活动', - steps: 'inspect_recent_sql_activity → inspect_recent_sql_logs → inspect_current_connection', - description: '适合先看最近到底以读还是写为主、有没有 DDL 或删除、哪个库最近报错最多,再决定继续下钻哪条日志或哪个连接。', - }, - { - title: '核对 SQL 编辑器事务', - steps: 'inspect_sql_editor_transaction → inspect_recent_sql_activity → inspect_sql_risk', - description: '适合先确认 SQL 编辑器 DML 是否会进入托管事务、当前是手动还是自动提交、有没有待提交事务,再解释 update/insert/delete 执行后的提交语义。', - }, - { - title: 'SQL 风险预检', - steps: 'inspect_sql_risk → inspect_ai_safety → execute_sql', - description: '适合用户要求执行、删除、更新、DDL 或批量 SQL 前,先检查语句数量、写入/DDL 风险、WHERE 条件和当前安全策略,再决定是否需要用户确认。', - }, - { - title: '排查应用日志', - steps: 'inspect_app_logs → inspect_mcp_setup / inspect_saved_connections / inspect_current_connection', - description: '适合先回看 gonavi.log 尾部的 ERROR/WARN,再结合 MCP、连接和当前数据源状态继续定位启动异常、连接失败或外部工具拉起问题。', - }, - { - title: '排查连接失败与冷却', - steps: 'inspect_recent_connection_failures → inspect_current_connection / inspect_saved_connections / inspect_app_logs', - description: '适合用户直接问“为什么连接不上”或已经看到冷却/验证失败提示时,先拿到结构化根因、最新地址和下一步建议,再决定回到连接配置还是看更长日志。', - }, - { - title: '排查 AI 气泡渲染异常', - steps: 'inspect_ai_last_render_error → inspect_active_tab / inspect_ai_runtime', - description: '适合用户反馈 AI 某条消息空白、气泡局部报错但整个面板没挂时,先拿到最近一次被隔离的渲染异常快照,再回到具体会话和运行时上下文继续缩小范围。', - }, - { - title: '诊断 AI 消息流', - steps: 'inspect_ai_message_flow → inspect_ai_last_render_error / inspect_app_logs', - description: '适合用户反馈回复被拆成多个气泡、工具调用后没继续回答、消息流状态不对时,先读取当前会话的真实消息结构和异常信号。', - }, - { - title: '复用历史 SQL', - steps: 'inspect_saved_queries → get_columns / execute_sql', - description: '适合先找本地保存过的查询脚本,再核对字段和只读验证,避免把之前写过的 SQL 重新手打一遍。', - }, - { - title: '回看 AI 历史对话', - steps: 'inspect_ai_sessions → inspect_active_tab / inspect_saved_queries', - description: '适合先定位之前聊过的 AI 会话、首条问题和最近回复,再继续复用当前页签或历史 SQL 上下文。', - }, - { - title: '查找模板片段', - steps: 'inspect_sql_snippets', - description: '适合先找团队已有的 SQL 片段模板、补全前缀和常用骨架,再决定是否继续改写。', - }, - { - title: '理解样例数据', - steps: 'get_columns → preview_table_rows', - description: '适合先确认字段,再直接查看前几行真实样例数据和空值形态。', - }, - { - title: '只读验证', - steps: 'get_columns → preview_table_rows → execute_sql', - description: '适合生成 SQL 后做小范围结果核对,仍会受 AI 安全级别控制。', - }, -]; - -const describeToolParameters = (tool: AIBuiltinToolInfo) => { - const schema = tool.tool.function.parameters; - const properties = schema && typeof schema === 'object' && typeof schema.properties === 'object' - ? schema.properties - : {}; - const required = new Set( - Array.isArray(schema?.required) ? schema.required.map((item) => String(item)) : [], - ); - - return Object.entries(properties).map(([name, config]) => { - const normalized = config && typeof config === 'object' ? config as Record : {}; - return { - name, - required: required.has(name), - description: typeof normalized.description === 'string' ? normalized.description : '', - enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [], - }; - }); -}; - export const AIBuiltinToolsCatalog: React.FC = ({ darkMode, overlayTheme, @@ -257,7 +45,7 @@ export const AIBuiltinToolsCatalog: React.FC = ({ ))}
{BUILTIN_AI_TOOL_INFO.map((tool) => { - const parameterDetails = describeToolParameters(tool); + const parameterDetails = describeBuiltinToolParameters(tool); return (
{ expect(result.content).toContain('OpenAI 主账号'); }); + it('returns the ai tool catalog so the model can choose probes and build arguments', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_ai_tool_catalog', { + keyword: 'mcp', + limit: 8, + }), + connections: [buildConnection()], + mcpTools: [{ + alias: 'github_create_issue', + originalName: 'create_issue', + serverId: 'github-server', + serverName: 'GitHub', + title: '创建 Issue', + description: 'Create a GitHub issue', + inputSchema: { + type: 'object', + required: ['owner', 'repo', 'title'], + properties: { + owner: { type: 'string', description: '仓库 owner' }, + repo: { type: 'string', description: '仓库名' }, + title: { type: 'string', description: 'Issue 标题' }, + }, + }, + }], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"keyword":"mcp"'); + expect(result.content).toContain('新增 MCP 填写指引'); + expect(result.content).toContain('"name":"inspect_mcp_draft"'); + expect(result.content).toContain('"name":"fullCommand"'); + expect(result.content).toContain('"alias":"github_create_issue"'); + expect(result.content).toContain('"requiredParameters":["owner","repo","title"]'); + expect(result.content).toContain('调用带参数工具前'); + }); + it('returns the current mcp setup snapshot so the model can inspect configured servers and client install state', async () => { const result = await executeLocalAIToolCall({ toolCall: buildToolCall('inspect_mcp_setup', {}), diff --git a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts index 838f227..953bd8f 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts @@ -12,6 +12,7 @@ import { buildAIGuidanceSnapshot } from './aiPromptInsights'; import { buildAIProviderSnapshot } from './aiProviderInsights'; import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; import { buildAISafetySnapshot } from './aiSafetyInsights'; +import { buildAIToolCatalogSnapshot } from './aiToolCatalogInsights'; import { buildMCPAuthoringGuideSnapshot } from './aiMCPAuthoringGuideInsights'; import { buildMCPDraftInspectionSnapshot } from './aiMCPDraftInspectionInsights'; import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; @@ -162,6 +163,18 @@ export async function executeAIConfigSnapshotToolCall( success: true, }; } + case 'inspect_ai_tool_catalog': + return { + content: JSON.stringify(buildAIToolCatalogSnapshot({ + builtinTools: BUILTIN_AI_TOOL_INFO, + mcpTools, + keyword: args.keyword, + toolName: args.toolName, + includeMCPTools: args.includeMCPTools !== false, + limit: args.limit, + })), + success: true, + }; case 'inspect_mcp_setup': { const [mcpServers, mcpClientInstallStatuses] = await loadMCPSetupState(runtime); return { @@ -227,6 +240,7 @@ export async function executeAIConfigSnapshotToolCall( inspect_ai_safety: '读取当前 AI 安全边界失败', inspect_ai_providers: '读取当前 AI 供应商配置失败', inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败', + inspect_ai_tool_catalog: '读取 AI 工具目录失败', inspect_mcp_setup: '读取 MCP 配置状态失败', inspect_mcp_remote_access: '读取 MCP 远程接入指引失败', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引失败', diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 8541bb9..1573734 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_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_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + 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_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); @@ -81,6 +81,7 @@ describe('buildAISystemContextMessages', () => { expect(joined).toContain('inspect_ai_safety 读取真实安全边界'); expect(joined).toContain('inspect_ai_providers 读取真实供应商配置'); expect(joined).toContain('inspect_ai_chat_readiness 读取真实发送前置状态'); + expect(joined).toContain('inspect_ai_tool_catalog 按关键词读取真实工具目录'); expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置'); expect(joined).toContain('inspect_mcp_authoring_guide 读取真实新增指引和模板'); expect(joined).toContain('inspect_mcp_draft 返回自动拆分、启动预览、配置错误/告警和 nextActions'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index 7e510b7..5a7edb0 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_tool_catalog', + '如果用户问题横跨多个功能、你不确定该先调用哪个内置工具,或用户问“你有哪些工具/这个工具参数怎么填/某类问题该用哪个探针”,优先调用 inspect_ai_tool_catalog 按关键词读取真实工具目录、推荐流程和参数提示,再选择具体探针。', + ); appendGuidanceIfToolAvailable( messages, availableToolNames, diff --git a/frontend/src/components/ai/aiToolCatalogInsights.ts b/frontend/src/components/ai/aiToolCatalogInsights.ts new file mode 100644 index 0000000..536b3eb --- /dev/null +++ b/frontend/src/components/ai/aiToolCatalogInsights.ts @@ -0,0 +1,156 @@ +import type { AIMCPToolDescriptor } from '../../types'; +import { + BUILTIN_TOOL_FLOWS, + describeBuiltinToolParameters, +} from '../../utils/aiBuiltinToolCatalog'; +import type { AIBuiltinToolInfo } from '../../utils/aiBuiltinToolInfo.types'; + +const DEFAULT_LIMIT = 12; +const MAX_LIMIT = 40; + +const normalizeText = (value: unknown): string => + String(value || '').trim().toLowerCase(); + +const normalizeLimit = (value: unknown): number => + Math.max(1, Math.min(MAX_LIMIT, Number(value) || DEFAULT_LIMIT)); + +const matchesAnyText = (keyword: string, values: unknown[]): boolean => + !keyword || values.some((value) => normalizeText(value).includes(keyword)); + +const readMCPToolParameterSummary = (tool: AIMCPToolDescriptor) => { + const schema = tool.inputSchema && typeof tool.inputSchema === 'object' + ? tool.inputSchema as Record + : {}; + const properties = schema.properties && typeof schema.properties === 'object' + ? schema.properties as Record + : {}; + const required = Array.isArray(schema.required) + ? schema.required.map((item) => String(item)).filter(Boolean) + : []; + + return { + hasInputSchema: Object.keys(schema).length > 0, + parameterCount: Object.keys(properties).length, + requiredParameters: required, + }; +}; + +export const buildAIToolCatalogSnapshot = (params: { + builtinTools: AIBuiltinToolInfo[]; + mcpTools?: AIMCPToolDescriptor[]; + keyword?: string; + toolName?: string; + includeMCPTools?: boolean; + limit?: number; +}) => { + const { + builtinTools, + mcpTools = [], + toolName = '', + includeMCPTools = true, + } = params; + const keyword = normalizeText(params.keyword); + const normalizedToolName = normalizeText(toolName); + const limit = normalizeLimit(params.limit); + const mcpKeyword = keyword || normalizedToolName; + const shouldReturnAllMCPTools = !mcpKeyword || mcpKeyword.includes('mcp') || mcpKeyword.includes('工具'); + + const matchedFlows = BUILTIN_TOOL_FLOWS + .filter((flow) => matchesAnyText(keyword, [flow.title, flow.steps, flow.description])) + .slice(0, limit); + + const matchedBuiltinTools = builtinTools + .filter((tool) => { + if (normalizedToolName) { + return normalizeText(tool.name) === normalizedToolName; + } + return matchesAnyText(keyword, [ + tool.name, + tool.desc, + tool.detail, + tool.params, + ...describeBuiltinToolParameters(tool).flatMap((param) => [ + param.name, + param.description, + param.enumValues.join(' '), + ]), + ]); + }) + .slice(0, limit) + .map((tool) => ({ + name: tool.name, + desc: tool.desc, + detail: tool.detail, + params: tool.params, + parameters: describeBuiltinToolParameters(tool), + })); + + const matchedMCPTools = includeMCPTools + ? mcpTools + .filter((tool) => shouldReturnAllMCPTools || matchesAnyText(mcpKeyword, [ + tool.alias, + tool.originalName, + tool.title, + tool.description, + tool.serverId, + tool.serverName, + ])) + .slice(0, limit) + .map((tool) => ({ + alias: tool.alias, + originalName: tool.originalName, + title: tool.title || tool.originalName || tool.alias, + description: tool.description || '', + serverId: tool.serverId, + serverName: tool.serverName, + ...readMCPToolParameterSummary(tool), + })) + : []; + + const warnings: string[] = []; + const nextActions: string[] = []; + + if (!keyword && !normalizedToolName) { + nextActions.push('先按用户问题关键词过滤,例如 mcp、连接失败、事务、快捷键、schema 或日志。'); + } + if (includeMCPTools && mcpTools.length === 0) { + warnings.push('当前没有发现外部 MCP 工具;如果用户需要外部能力,先检查 MCP 服务配置和工具发现状态。'); + nextActions.push('调用 inspect_mcp_setup 查看 MCP 服务和外部客户端接入状态。'); + } + if (keyword && matchedFlows.length === 0 && matchedBuiltinTools.length === 0 && matchedMCPTools.length === 0) { + warnings.push('没有找到匹配的工具或推荐流程。'); + nextActions.push('改用更宽泛关键词,或先调用 inspect_ai_runtime 查看当前完整工具清单。'); + } + if (matchedBuiltinTools.some((tool) => tool.parameters.length > 0)) { + nextActions.push('调用带参数工具前,优先按 parameters.description 组装 arguments;缺少上下文时先向用户确认。'); + } + + return { + query: { + keyword: params.keyword || '', + toolName: toolName || '', + includeMCPTools, + limit, + }, + totals: { + builtinToolCount: builtinTools.length, + flowCount: BUILTIN_TOOL_FLOWS.length, + mcpToolCount: Array.isArray(mcpTools) ? mcpTools.length : 0, + }, + returned: { + flowCount: matchedFlows.length, + builtinToolCount: matchedBuiltinTools.length, + mcpToolCount: matchedMCPTools.length, + }, + flows: matchedFlows, + builtinTools: matchedBuiltinTools, + mcpTools: matchedMCPTools, + warnings, + nextActions, + message: normalizedToolName + ? `已按工具名 ${toolName} 返回目录信息` + : keyword + ? `已按关键词 ${params.keyword} 返回工具目录建议` + : '已返回 GoNavi AI 工具目录摘要', + }; +}; diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 917c64c..4411a19 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -27,6 +27,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_ai_safety: '读取当前 AI 安全边界', inspect_ai_providers: '读取当前 AI 供应商与模型配置', inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态', + inspect_ai_tool_catalog: '读取 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 08446f6..ccfbbe0 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -111,6 +111,31 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_ai_tool_catalog", + icon: "🧭", + desc: "查看 AI 内置工具目录和参数提示", + detail: + "按关键词或工具名返回 GoNavi AI 内置工具、推荐探针流程、参数说明和当前 MCP 工具摘要。适合用户问“你该用哪个工具”“这个工具参数怎么填”“有哪些内置工具”或 AI 需要先选择探针路线时调用。", + params: "keyword?, toolName?, includeMCPTools?(默认 true), limit?(默认 12)", + tool: { + type: "function", + function: { + name: "inspect_ai_tool_catalog", + description: + "读取 GoNavi AI 工具目录快照,可按关键词或工具名筛选,返回推荐工具调用流程、内置工具说明、参数提示和当前已发现 MCP 工具摘要。适用于用户询问当前有哪些内置工具、某类问题该先调用哪个探针、工具 arguments 怎么填、或 AI 在处理复杂问题前需要先选择工具路线时优先调用。", + parameters: { + type: "object", + properties: { + keyword: { type: "string", description: "可选,按问题关键词过滤工具和流程,例如 mcp、连接失败、事务、快捷键、schema、日志" }, + toolName: { type: "string", description: "可选,按内置工具名精确查询,例如 inspect_mcp_draft 或 inspect_sql_risk" }, + includeMCPTools: { type: "boolean", description: "可选,是否同时返回当前已发现的 MCP 工具摘要,默认 true" }, + limit: { type: "number", description: "可选,最多返回多少条流程、内置工具和 MCP 工具,默认 12,最大 40" }, + }, + }, + }, + }, + }, { name: "inspect_mcp_setup", icon: "🪛", diff --git a/frontend/src/utils/aiBuiltinToolCatalog.ts b/frontend/src/utils/aiBuiltinToolCatalog.ts new file mode 100644 index 0000000..3b57f71 --- /dev/null +++ b/frontend/src/utils/aiBuiltinToolCatalog.ts @@ -0,0 +1,232 @@ +import type { AIBuiltinToolInfo } from './aiBuiltinToolInfo.types'; + +export interface AIBuiltinToolFlow { + title: string; + steps: string; + description: string; +} + +export interface AIBuiltinToolParameterHint { + name: string; + required: boolean; + description: string; + enumValues: string[]; +} + +export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ + { + title: '定位表与字段', + steps: 'get_connections -> get_databases -> get_tables -> get_columns', + description: '适合先找连接、找库、找表,再确认真实字段名后生成 SQL。', + }, + { + title: '字段反查表', + steps: 'get_databases -> get_all_columns', + description: '适合只知道字段名、业务含义或注释关键词,但还不确定具体落在哪张表。', + }, + { + title: '结构深挖', + steps: 'get_columns -> get_indexes -> get_foreign_keys -> get_triggers -> get_table_ddl', + description: '适合做索引优化、关系梳理、隐式副作用排查和 DDL 审查。', + }, + { + title: '一键结构快照', + steps: 'inspect_table_bundle', + description: '适合一次带回字段、索引、外键、触发器和 DDL;必要时还能附带样例行,减少来回调用。', + }, + { + title: '全库快速摸底', + steps: 'inspect_database_bundle -> inspect_table_bundle', + description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。', + }, + { + title: 'AI 应用健康总览', + 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_tool_catalog -> inspect_ai_runtime / inspect_mcp_setup', + description: '适合先按关键词确认该用哪些内置探针、每个工具 arguments 怎么填,以及当前有没有外部 MCP 工具可用。', + }, + { + title: '一键体检 AI 配置', + steps: 'inspect_ai_setup_health -> inspect_ai_providers / inspect_mcp_setup / inspect_ai_guidance', + description: '适合先拿到一份 AI 配置健康快照,看清当前是供应商没配好、聊天发送前置没满足、MCP 没接入,还是提示词 / Skills / 上下文还不完整,再决定往哪条探针继续下钻。', + }, + { + title: '查看 AI 当前能力', + steps: 'inspect_ai_runtime -> inspect_ai_context / inspect_current_connection', + description: '适合先确认当前模型、安全级别、上下文级别、Skills 和 MCP 工具,再决定让 AI 走哪条探针链路。', + }, + { + title: '核对写入安全边界', + steps: 'inspect_ai_safety -> inspect_ai_runtime -> inspect_current_connection', + description: '适合先确认当前是不是只读、DDL/DML 到底允不允许、MCP 写操作是否还需要 allowMutating,再决定后续该走查询、改数据还是改结构。', + }, + { + title: '排查供应商与模型', + steps: 'inspect_ai_providers -> inspect_ai_runtime', + description: '适合先确认当前到底配置了哪些供应商、哪个在生效、有没有缺密钥或没选模型,再解释为什么 AI 不能发送、为什么模型列表为空。', + }, + { + title: '排查聊天发送状态', + steps: 'inspect_ai_chat_readiness -> inspect_ai_providers', + description: '适合先确认当前聊天输入区到底缺什么前置条件,例如没选活动供应商、缺密钥、缺接口地址、没选模型,避免只凭界面现象猜测。', + }, + { + title: '排查 MCP 接入状态', + steps: 'inspect_mcp_setup -> inspect_ai_runtime', + description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。', + }, + { + title: '远程 Agent 接入 GoNavi MCP', + steps: 'inspect_mcp_remote_access -> inspect_mcp_setup -> inspect_ai_safety', + description: '适合 OpenClaw/Hermans 部署在云端 Linux,但数据库连接和密码只在 Windows GoNavi 本机时,先生成 HTTP MCP、Bearer Token、隧道和安全边界指引。', + }, + { + title: '新增 MCP 填写指引', + steps: 'inspect_mcp_authoring_guide -> inspect_mcp_draft -> inspect_mcp_setup', + description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再把用户贴出的命令或草稿交给真实校验器试算,最后结合当前 MCP 配置现状判断应该新增哪种启动方式。', + }, + { + title: '查看 MCP 工具参数', + steps: 'inspect_mcp_setup -> inspect_mcp_tool_schema', + description: '适合先找到当前真实发现到的 MCP 工具 alias,再读取对应 inputSchema、必填字段、枚举和嵌套参数路径,避免调用外部 MCP 工具时乱填 arguments。', + }, + { + title: '查看当前提示与 Skills', + steps: 'inspect_ai_guidance -> inspect_ai_runtime', + description: '适合先确认当前自定义提示词、启用的 Skills、依赖工具和生效范围,再解释为什么 AI 当前会这样回答或为什么某个规则没有触发。', + }, + { + title: '查看当前 AI 上下文', + steps: 'inspect_ai_context -> inspect_table_bundle / get_columns', + description: '适合先确认这轮对话当前到底挂了哪些表结构,再继续做字段核对、表设计评审或 SQL 生成。', + }, + { + title: '查看当前连接', + steps: 'inspect_current_connection -> get_databases / get_tables', + description: '适合先确认当前活动数据源的类型、地址、当前库和 SSH/代理状态,再继续做库表探索或连接问题排查。', + }, + { + title: '核对数据源能力边界', + steps: 'inspect_connection_capabilities -> inspect_current_connection', + description: '适合先确认当前连接到底支不支持建库、删库、结果编辑、SQL 导出或近似计数,再解释为什么某些按钮没出现或某类操作只能只读。', + }, + { + title: '盘点本地连接资产', + steps: 'inspect_saved_connections -> inspect_current_connection / get_databases', + description: '适合先按关键词或类型筛出本地保存的数据源,再挑目标连接继续看当前状态或库表结构。', + }, + { + title: '盘点外部 SQL 目录', + steps: 'inspect_external_sql_directories -> inspect_workspace_tabs / inspect_active_tab', + description: '适合先确认本地配置了哪些外部 SQL 目录、目录绑定到哪个连接/库,以及当前打开的 SQL 文件来自哪里,再继续分析脚本内容。', + }, + { + title: '读取外部 SQL 文件', + steps: 'inspect_external_sql_directories -> inspect_external_sql_file -> inspect_active_tab', + description: '适合先定位具体脚本路径,再直接读取目录中的 SQL 文件内容;如果这个文件已经在编辑器里打开,再继续结合当前页签草稿一起分析。', + }, + { + title: '读取当前页签', + steps: 'inspect_active_tab -> get_columns / get_indexes / execute_sql', + description: '适合先读取当前编辑器里的 SQL 草稿或当前表页签,再继续做字段核对、索引分析和只读验证。', + }, + { + title: '盘点当前工作区', + steps: 'inspect_workspace_tabs -> inspect_active_tab -> get_columns / execute_sql', + description: '适合先看当前打开了哪些 SQL / 表 / 命令页签,再切到目标页签继续做字段核对、对比分析和只读验证。', + }, + { + title: '查看当前快捷键配置', + steps: 'inspect_shortcuts -> inspect_active_tab / inspect_workspace_tabs', + description: '适合先确认当前 Win / Mac 快捷键、是否改过默认值,以及结果区、AI 面板、查询执行等动作到底该怎么按,再结合当前页签解释具体使用场景。', + }, + { + title: '回看最近执行记录', + steps: 'inspect_recent_sql_logs -> get_columns / get_indexes / execute_sql', + description: '适合追查刚刚执行失败的 SQL、慢查询耗时,或基于真实执行历史继续让 AI 给解释和优化建议。', + }, + { + title: '总结最近 SQL 活动', + steps: 'inspect_recent_sql_activity -> inspect_recent_sql_logs -> inspect_current_connection', + description: '适合先看最近到底以读还是写为主、有没有 DDL 或删除、哪个库最近报错最多,再决定继续下钻哪条日志或哪个连接。', + }, + { + title: '核对 SQL 编辑器事务', + steps: 'inspect_sql_editor_transaction -> inspect_recent_sql_activity -> inspect_sql_risk', + description: '适合先确认 SQL 编辑器 DML 是否会进入托管事务、当前是手动还是自动提交、有没有待提交事务,再解释 update/insert/delete 执行后的提交语义。', + }, + { + title: 'SQL 风险预检', + steps: 'inspect_sql_risk -> inspect_ai_safety -> execute_sql', + description: '适合用户要求执行、删除、更新、DDL 或批量 SQL 前,先检查语句数量、写入/DDL 风险、WHERE 条件和当前安全策略,再决定是否需要用户确认。', + }, + { + title: '排查应用日志', + steps: 'inspect_app_logs -> inspect_mcp_setup / inspect_saved_connections / inspect_current_connection', + description: '适合先回看 gonavi.log 尾部的 ERROR/WARN,再结合 MCP、连接和当前数据源状态继续定位启动异常、连接失败或外部工具拉起问题。', + }, + { + title: '排查连接失败与冷却', + steps: 'inspect_recent_connection_failures -> inspect_current_connection / inspect_saved_connections / inspect_app_logs', + description: '适合用户直接问“为什么连接不上”或已经看到冷却/验证失败提示时,先拿到结构化根因、最新地址和下一步建议,再决定回到连接配置还是看更长日志。', + }, + { + title: '排查 AI 气泡渲染异常', + steps: 'inspect_ai_last_render_error -> inspect_active_tab / inspect_ai_runtime', + description: '适合用户反馈 AI 某条消息空白、气泡局部报错但整个面板没挂时,先拿到最近一次被隔离的渲染异常快照,再回到具体会话和运行时上下文继续缩小范围。', + }, + { + title: '诊断 AI 消息流', + steps: 'inspect_ai_message_flow -> inspect_ai_last_render_error / inspect_app_logs', + description: '适合用户反馈回复被拆成多个气泡、工具调用后没继续回答、消息流状态不对时,先读取当前会话的真实消息结构和异常信号。', + }, + { + title: '复用历史 SQL', + steps: 'inspect_saved_queries -> get_columns / execute_sql', + description: '适合先找本地保存过的查询脚本,再核对字段和只读验证,避免把之前写过的 SQL 重新手打一遍。', + }, + { + title: '回看 AI 历史对话', + steps: 'inspect_ai_sessions -> inspect_active_tab / inspect_saved_queries', + description: '适合先定位之前聊过的 AI 会话、首条问题和最近回复,再继续复用当前页签或历史 SQL 上下文。', + }, + { + title: '查找模板片段', + steps: 'inspect_sql_snippets', + description: '适合先找团队已有的 SQL 片段模板、补全前缀和常用骨架,再决定是否继续改写。', + }, + { + title: '理解样例数据', + steps: 'get_columns -> preview_table_rows', + description: '适合先确认字段,再直接查看前几行真实样例数据和空值形态。', + }, + { + title: '只读验证', + steps: 'get_columns -> preview_table_rows -> execute_sql', + description: '适合生成 SQL 后做小范围结果核对,仍会受 AI 安全级别控制。', + }, +]; + +export const describeBuiltinToolParameters = (tool: AIBuiltinToolInfo): AIBuiltinToolParameterHint[] => { + const schema = tool.tool.function.parameters; + const properties = schema && typeof schema === 'object' && typeof schema.properties === 'object' + ? schema.properties + : {}; + const required = new Set( + Array.isArray(schema?.required) ? schema.required.map((item) => String(item)) : [], + ); + + return Object.entries(properties).map(([name, config]) => { + const normalized = config && typeof config === 'object' ? config as Record : {}; + return { + name, + required: required.has(name), + description: typeof normalized.description === 'string' ? normalized.description : '', + enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [], + }; + }); +}; diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index 52cb54b..3c3c3e7 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -76,6 +76,15 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('当前 AI 聊天输入区'); }); + it('registers the ai-tool-catalog inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_tool_catalog'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('内置工具目录'); + expect(info?.tool.function.description).toContain('推荐工具调用流程'); + expect(info?.tool.function.parameters?.properties?.keyword?.description).toContain('连接失败'); + expect(info?.tool.function.parameters?.properties?.includeMCPTools?.description).toContain('MCP 工具摘要'); + }); + it('registers the ai-guidance inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_guidance'); expect(info).toBeTruthy(); @@ -208,6 +217,7 @@ describe('aiToolRegistry', () => { 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); + expect(tools.some((item) => item.function.name === 'inspect_ai_tool_catalog')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_remote_access')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_authoring_guide')).toBe(true); diff --git a/frontend/src/utils/fontFamilies.test.ts b/frontend/src/utils/fontFamilies.test.ts new file mode 100644 index 0000000..87fcfca --- /dev/null +++ b/frontend/src/utils/fontFamilies.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { + getLinuxCJKFontInstallHint, + hasInstalledCJKFontFamily, +} from './fontFamilies'; + +describe('fontFamilies helpers', () => { + it('detects installed CJK font families on Linux', () => { + expect(hasInstalledCJKFontFamily([ + { family: 'Ubuntu' }, + { family: 'Noto Sans CJK SC' }, + ])).toBe(true); + expect(hasInstalledCJKFontFamily([ + { family: 'DejaVu Sans' }, + { family: 'Liberation Sans' }, + ])).toBe(false); + }); + + it('returns an Ubuntu CJK font install hint only when Linux lacks CJK fonts', () => { + expect(getLinuxCJKFontInstallHint('linux', [ + { family: 'DejaVu Sans' }, + ])).toBe('sudo apt install fonts-noto-cjk fonts-wqy-microhei && fc-cache -fv'); + + expect(getLinuxCJKFontInstallHint('linux', [ + { family: 'Source Han Sans SC' }, + ])).toBeNull(); + + expect(getLinuxCJKFontInstallHint('windows', [ + { family: 'DejaVu Sans' }, + ])).toBeNull(); + }); +}); diff --git a/frontend/src/utils/fontFamilies.ts b/frontend/src/utils/fontFamilies.ts index cd2e4e6..7766352 100644 --- a/frontend/src/utils/fontFamilies.ts +++ b/frontend/src/utils/fontFamilies.ts @@ -4,6 +4,33 @@ export const DEFAULT_MONO_FONT_FAMILY = '"JetBrains Mono", "Noto Sans Mono CJK SC", "Noto Sans Mono", ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace'; const MAX_FONT_FAMILY_LENGTH = 512; +const LINUX_CJK_FONT_INSTALL_COMMAND = 'sudo apt install fonts-noto-cjk fonts-wqy-microhei && fc-cache -fv'; + +const CJK_FONT_KEYWORDS = [ + 'noto sans cjk', + 'noto sans sc', + 'noto serif cjk', + 'noto serif sc', + 'source han sans', + 'source han serif', + '思源', + 'wenquanyi', + '文泉驿', + 'sarasa', + '更纱', + 'lxgw', + '霞鹜', + 'microsoft yahei', + '微软雅黑', + 'simsun', + '宋体', + 'simhei', + '黑体', + 'pingfang', + '苹方', + 'hiragino', + '冬青', +]; export type FontFamilyOption = { value: string; @@ -46,6 +73,11 @@ const normalizeFontSearchToken = (value: string): string => String(value || '') .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fff]+/gi, ''); +const normalizeInstalledFontNameForCJK = (entry: string | InstalledFontFamily): string => { + const raw = typeof entry === 'string' ? entry : entry.family; + return String(raw || '').trim().toLowerCase(); +}; + const insertFontNameWordBreaks = (value: string): string => value .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') .replace(/([a-z\d])([A-Z])/g, '$1 $2'); @@ -267,6 +299,32 @@ export const resolveMonoFontFamily = (customValue: unknown): string => { return sanitizeFontFamilyInput(customValue) ?? DEFAULT_MONO_FONT_FAMILY; }; +export const hasInstalledCJKFontFamily = ( + installedFamilies: Array, +): boolean => { + return installedFamilies.some((entry) => { + const family = normalizeInstalledFontNameForCJK(entry); + if (!family) { + return false; + } + const compactFamily = normalizeFontSearchToken(family); + return CJK_FONT_KEYWORDS.some((keyword) => { + const normalizedKeyword = keyword.toLowerCase(); + return family.includes(normalizedKeyword) || compactFamily.includes(normalizeFontSearchToken(normalizedKeyword)); + }); + }); +}; + +export const getLinuxCJKFontInstallHint = ( + platform: string, + installedFamilies: Array, +): string | null => { + if (String(platform || '').toLowerCase() !== 'linux') { + return null; + } + return hasInstalledCJKFontFamily(installedFamilies) ? null : LINUX_CJK_FONT_INSTALL_COMMAND; +}; + export const getPlatformFontFamilyOptions = ( platform: string, kind: "ui" | "mono", diff --git a/frontend/src/utils/mcpClientInstallStatus.test.ts b/frontend/src/utils/mcpClientInstallStatus.test.ts index 144ef7a..d3a5fdc 100644 --- a/frontend/src/utils/mcpClientInstallStatus.test.ts +++ b/frontend/src/utils/mcpClientInstallStatus.test.ts @@ -134,5 +134,8 @@ describe('mcpClientInstallStatus helpers', () => { expect(guide).toContain('云端 Agent 不需要保存数据库密码'); expect(guide).toContain('不能直接使用 Windows 本地 stdio 命令'); expect(guide).toContain('allowMutating=true'); + expect(guide).toContain('"type": "streamable-http"'); + expect(guide).toContain('"Authorization": "Bearer <随机token>"'); + expect(guide).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>'); }); }); diff --git a/frontend/src/utils/mcpClientInstallStatus.ts b/frontend/src/utils/mcpClientInstallStatus.ts index ae98798..39f5b4e 100644 --- a/frontend/src/utils/mcpClientInstallStatus.ts +++ b/frontend/src/utils/mcpClientInstallStatus.ts @@ -161,6 +161,19 @@ export const buildRemoteMCPClientGuide = ( status?: Pick | null, ): string => { const displayName = String(status?.displayName || '远程 Agent').trim(); + const streamableHTTPConfig = [ + '{', + ' "mcpServers": {', + ' "gonavi": {', + ' "type": "streamable-http",', + ' "url": "https://<你的域名或隧道地址>/mcp",', + ' "headers": {', + ' "Authorization": "Bearer <随机token>"', + ' }', + ' }', + ' }', + '}', + ]; return [ `GoNavi MCP 远程接入说明 - ${displayName}`, '', @@ -179,6 +192,13 @@ export const buildRemoteMCPClientGuide = ( `3. 在 ${displayName} 中添加远程 MCP Server,transport 选择 Streamable HTTP,URL 填隧道/反向代理后的 /mcp 地址,并设置 Authorization: Bearer <随机token>。`, '4. 先调用 get_connections 获取 connectionId,再调用表结构工具;不要把数据库 host/user/password 写进云端 Agent 配置。', '', + '可复制配置片段(适用于支持 mcpServers JSON 的 Agent):', + ...streamableHTTPConfig, + '', + 'CLI / 服务启动命令:', + 'GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>', + '或设置环境变量:GONAVI_MCP_HTTP_TOKEN=<随机token> 后运行 gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp', + '', status?.message ? `当前提示:${status.message}` : '', ].filter((line, index, lines) => line || index < lines.length - 1).join('\n'); };