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

@@ -109,6 +109,22 @@ OpenClaw、Hermans 这类部署在云端或远端 Linux 的 Agent不能直接
4. 在 OpenClaw / Hermans 中添加远程 MCP Servertransport 选择 Streamable HTTPURL 指向 `/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 客户端配置示例

View File

@@ -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<HTMLDivElement | null>(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 字体。')}
</div>
{linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && (
<div
style={{
marginTop: 8,
padding: '9px 10px',
borderRadius: 8,
border: darkMode ? '1px solid rgba(250,204,21,0.28)' : '1px solid rgba(217,119,6,0.22)',
background: darkMode ? 'rgba(250,204,21,0.08)' : 'rgba(251,191,36,0.12)',
color: darkMode ? 'rgba(254,249,195,0.92)' : '#92400e',
fontSize: 12,
lineHeight: 1.7,
}}
>
Ubuntu/Linux CJK
<span style={{ fontFamily: 'var(--gn-font-mono)', marginLeft: 6 }}>{linuxCJKFontInstallHint}</span>
GoNavi
</div>
)}
</div>
<div>
<div style={{ marginBottom: 8, fontWeight: 500 }}> (Mono Font Family)</div>

View File

@@ -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 当前能力');

View File

@@ -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<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)) : [],
};
});
};
export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
darkMode,
overlayTheme,
@@ -257,7 +45,7 @@ export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
))}
</div>
{BUILTIN_AI_TOOL_INFO.map((tool) => {
const parameterDetails = describeToolParameters(tool);
const parameterDetails = describeBuiltinToolParameters(tool);
return (
<div
key={tool.name}

View File

@@ -230,6 +230,47 @@ describe('aiLocalToolExecutor AI config inspection tools', () => {
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', {}),

View File

@@ -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 新增填写指引失败',

View File

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

View File

@@ -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,

View File

@@ -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<string, any>
: {};
const properties = schema.properties && typeof schema.properties === 'object'
? schema.properties as Record<string, any>
: {};
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 工具目录摘要',
};
};

View File

@@ -27,6 +27,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
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 新增草稿',

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