feat(ai): 完善工具目录与远程 MCP 接入指引

- 新增 inspect_ai_tool_catalog 工具,返回内置探针流程、参数提示和 MCP 工具摘要

- 拆分 AI 内置工具目录配置,降低 AIBuiltinToolsCatalog 体积

- 补充 OpenClaw/Hermans 远程 MCP Streamable HTTP 配置说明

- 增加 Linux CJK 字体缺失检测与 Ubuntu 安装提示
This commit is contained in:
Syngnat
2026-06-10 23:52:19 +08:00
parent 11156c941c
commit b11b662071
17 changed files with 650 additions and 222 deletions

View File

@@ -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: "🪛",

View File

@@ -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<string, any> : {};
return {
name,
required: required.has(name),
description: typeof normalized.description === 'string' ? normalized.description : '',
enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [],
};
});
};

View File

@@ -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);

View File

@@ -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();
});
});

View File

@@ -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<string | InstalledFontFamily>,
): 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 | InstalledFontFamily>,
): 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",

View File

@@ -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>');
});
});

View File

@@ -161,6 +161,19 @@ export const buildRemoteMCPClientGuide = (
status?: Pick<AIMCPClientInstallStatus, 'displayName' | 'message'> | 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 Servertransport 选择 Streamable HTTPURL 填隧道/反向代理后的 /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');
};