mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
✨ feat(ai): 完善工具目录与远程 MCP 接入指引
- 新增 inspect_ai_tool_catalog 工具,返回内置探针流程、参数提示和 MCP 工具摘要 - 拆分 AI 内置工具目录配置,降低 AIBuiltinToolsCatalog 体积 - 补充 OpenClaw/Hermans 远程 MCP Streamable HTTP 配置说明 - 增加 Linux CJK 字体缺失检测与 Ubuntu 安装提示
This commit is contained in:
@@ -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 客户端配置示例
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 当前能力');
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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', {}),
|
||||
|
||||
@@ -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 新增填写指引失败',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
156
frontend/src/components/ai/aiToolCatalogInsights.ts
Normal file
156
frontend/src/components/ai/aiToolCatalogInsights.ts
Normal 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 工具目录摘要',
|
||||
};
|
||||
};
|
||||
@@ -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 新增草稿',
|
||||
|
||||
@@ -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: "🪛",
|
||||
|
||||
232
frontend/src/utils/aiBuiltinToolCatalog.ts
Normal file
232
frontend/src/utils/aiBuiltinToolCatalog.ts
Normal 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)) : [],
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
33
frontend/src/utils/fontFamilies.test.ts
Normal file
33
frontend/src/utils/fontFamilies.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 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');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user