feat(ai): 增强 MCP 远程接入与上下文诊断

This commit is contained in:
Syngnat
2026-06-11 07:29:04 +08:00
parent d3278bb4c4
commit 26fb650e04
25 changed files with 923 additions and 8 deletions

View File

@@ -88,6 +88,9 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_ai_last_render_error');
expect(markup).toContain('诊断 AI 消息流');
expect(markup).toContain('inspect_ai_message_flow');
expect(markup).toContain('诊断 AI 上下文体量');
expect(markup).toContain('inspect_ai_context_budget');
expect(markup).toContain('messageLimit');
expect(markup).toContain('复用历史 SQL');
expect(markup).toContain('inspect_saved_queries');
expect(markup).toContain('回看 AI 历史对话');

View File

@@ -194,9 +194,11 @@ describe('AIMCPClientInstallPanel', () => {
expect(markup).toContain('云端 Agent 只通过 MCP 工具读取连接摘要、库表和 DDL');
expect(markup).toContain('OpenClaw 远程 MCP 快速配置');
expect(markup).toContain('配置到云端 Agent');
expect(markup).toContain('无 GUI / CLI 生成配置');
expect(markup).toContain('"type": "streamable-http"');
expect(markup).toContain('"url": "https://<你的域名或隧道地址>/mcp"');
expect(markup).toContain('"Authorization": "Bearer <随机token>"');
expect(markup).toContain('GoNavi.exe mcp-server remote-config --client openclaw --url https://<你的域名或隧道地址>/mcp --token <随机token>');
expect(markup).toContain('Windows 启动 GoNavi MCP HTTP');
expect(markup).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(markup).toContain('独立二进制gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');

View File

@@ -310,7 +310,7 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
{remoteQuickStart.displayName} MCP
</div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
Agent Windows GoNavi 使 MCP URL Bearer Token
Agent GUI/CLI Windows GoNavi 使 MCP URL Bearer Token
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 10 }}>
<div
@@ -338,6 +338,34 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
{remoteQuickStart.configJson}
</code>
</div>
<div
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(15,23,42,0.55)' : 'rgba(255,255,255,0.78)',
}}
>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}>
GUI / CLI
</div>
<code
style={{
display: 'block',
marginTop: 8,
fontFamily: 'var(--gn-font-mono)',
fontSize: 11,
color: overlayTheme.titleText,
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
}}
>
{remoteQuickStart.configCommand}
</code>
<div style={{ marginTop: 8, fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
{remoteQuickStart.displayName} MCP
</div>
</div>
<div
style={{
padding: '10px 12px',

View File

@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest';
import { buildAIContextBudgetSnapshot } from './aiContextBudgetInsights';
describe('aiContextBudgetInsights', () => {
it('summarizes context budget sources and warns when schema/tool results are oversized', () => {
const longToolResult = 'x'.repeat(22000);
const longDDL = `CREATE TABLE big_table (${Array.from({ length: 300 }, (_, index) => `c${index} varchar(255)`).join(',')})`;
const snapshot = buildAIContextBudgetSnapshot({
activeSessionId: 'session-1',
aiChatSessions: [{ id: 'session-1', title: '上下文体量排查', updatedAt: 1 }],
aiChatHistory: {
'session-1': [
{ id: 'msg-1', role: 'user', content: 'AI 变慢了', timestamp: 1 },
{
id: 'msg-2',
role: 'assistant',
content: '先看日志',
timestamp: 2,
tool_calls: [{ id: 'tool-1', type: 'function', function: { name: 'inspect_app_logs', arguments: '{}' } }],
},
{ id: 'msg-3', role: 'tool', content: longToolResult, timestamp: 3, tool_call_id: 'tool-1' },
],
},
aiContexts: {
'conn-1:crm': [
{ dbName: 'crm', tableName: 'big_table', ddl: longDDL },
],
},
mcpTools: [{
alias: 'remote_probe',
serverId: 'mcp-1',
serverName: 'demo',
originalName: 'probe',
inputSchema: {
type: 'object',
properties: Object.fromEntries(Array.from({ length: 20 }, (_, index) => [`field${index}`, { type: 'string' }])),
},
}],
skills: [{
id: 'skill-1',
name: '结构审查',
systemPrompt: '先看结构',
enabled: true,
scopes: ['database'],
}],
userPromptSettings: {
global: '全局提示',
database: '数据库提示',
jvm: '',
jvmDiagnostic: '',
},
});
expect(snapshot.foundSession).toBe(true);
expect(snapshot.title).toBe('上下文体量排查');
expect(snapshot.messageWindow.toolResultChars).toBe(22000);
expect(snapshot.messageWindow.unresolvedToolCallCount).toBe(0);
expect(snapshot.schemaContext.tableCount).toBe(1);
expect(snapshot.schemaContext.largestTables[0]).toMatchObject({ tableName: 'big_table' });
expect(snapshot.toolCatalog.mcpToolCount).toBe(1);
expect(snapshot.promptsAndSkills.enabledSkillNames).toContain('结构审查');
expect(snapshot.warnings).toContain('最近工具结果较长,可能导致后续回答被日志或大结果集稀释');
expect(snapshot.nextActions).toContain('降低 inspect_app_logs / inspect_recent_sql_logs / includeDDL / includeLogLines 的返回量');
});
it('reports missing sessions and unresolved tool calls', () => {
const snapshot = buildAIContextBudgetSnapshot({
activeSessionId: 'missing',
aiChatSessions: [],
aiChatHistory: {
missing: [
{
id: 'msg-1',
role: 'assistant',
content: '调用工具',
timestamp: 1,
tool_calls: [{ id: 'tool-1', type: 'function', function: { name: 'inspect_ai_runtime', arguments: '{}' } }],
},
],
},
includeDetails: false,
});
expect(snapshot.foundSession).toBe(false);
expect(snapshot.messageWindow.unresolvedToolCallCount).toBe(1);
expect(snapshot.warnings).toContain('未找到目标 AI 会话,消息体量统计只覆盖空窗口');
expect(snapshot.warnings).toContain('最近消息窗口内有 1 个未闭环工具调用');
expect(snapshot.nextActions).toContain('先调用 inspect_ai_message_flow 确认工具调用是否缺少 tool 结果消息');
});
});

View File

@@ -0,0 +1,246 @@
import type {
AIChatMessage,
AIContextItem,
AIMCPToolDescriptor,
AISkillConfig,
AIUserPromptSettings,
} from '../../types';
type ContextRiskLevel = 'low' | 'medium' | 'high' | 'critical';
interface BuildAIContextBudgetSnapshotOptions {
aiContexts?: Record<string, AIContextItem[]>;
aiChatHistory?: Record<string, AIChatMessage[]>;
aiChatSessions?: Array<{ id: string; title: string; updatedAt: number }>;
activeSessionId?: string | null;
sessionId?: unknown;
messageLimit?: unknown;
includeDetails?: unknown;
mcpTools?: AIMCPToolDescriptor[];
skills?: AISkillConfig[];
userPromptSettings?: AIUserPromptSettings;
}
const DEFAULT_MESSAGE_LIMIT = 40;
const MAX_MESSAGE_LIMIT = 120;
const MESSAGE_PREVIEW_LIMIT = 160;
const clampNumber = (value: unknown, fallback: number, min: number, max: number): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.min(max, Math.max(min, Math.round(parsed)));
};
const charCount = (value: unknown): number => String(value || '').length;
const estimateTokens = (chars: number): number => Math.ceil(Math.max(0, chars) / 3);
const previewText = (value: unknown, limit = MESSAGE_PREVIEW_LIMIT): string => {
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
if (normalized.length <= limit) {
return normalized;
}
return `${normalized.slice(0, limit)}...`;
};
const classifyRisk = (estimatedInputChars: number): ContextRiskLevel => {
if (estimatedInputChars >= 120000) {
return 'critical';
}
if (estimatedInputChars >= 70000) {
return 'high';
}
if (estimatedInputChars >= 30000) {
return 'medium';
}
return 'low';
};
const appendUnique = (items: string[], item: string) => {
if (!items.includes(item)) {
items.push(item);
}
};
const getMessagePayloadChars = (message: AIChatMessage): number =>
charCount(message.content)
+ charCount(message.thinking)
+ charCount(message.reasoning_content)
+ (message.tool_calls ? charCount(JSON.stringify(message.tool_calls)) : 0);
export const buildAIContextBudgetSnapshot = ({
aiContexts = {},
aiChatHistory = {},
aiChatSessions = [],
activeSessionId = null,
sessionId,
messageLimit,
includeDetails,
mcpTools = [],
skills = [],
userPromptSettings,
}: BuildAIContextBudgetSnapshotOptions) => {
const requestedSessionId = String(sessionId || activeSessionId || '').trim();
const allMessages = requestedSessionId ? (aiChatHistory[requestedSessionId] || []) : [];
const effectiveMessageLimit = clampNumber(messageLimit, DEFAULT_MESSAGE_LIMIT, 1, MAX_MESSAGE_LIMIT);
const inspectedMessages = allMessages.slice(-effectiveMessageLimit);
const shouldIncludeDetails = includeDetails !== false;
const session = aiChatSessions.find((item) => item.id === requestedSessionId);
const messageRoleCounts = inspectedMessages.reduce<Record<string, number>>((acc, message) => {
acc[message.role] = (acc[message.role] || 0) + 1;
return acc;
}, {});
const messagePayloadChars = inspectedMessages.reduce((sum, message) => sum + getMessagePayloadChars(message), 0);
const toolResultChars = inspectedMessages
.filter((message) => message.role === 'tool')
.reduce((sum, message) => sum + charCount(message.content), 0);
const thinkingChars = inspectedMessages.reduce(
(sum, message) => sum + charCount(message.thinking) + charCount(message.reasoning_content),
0,
);
const unresolvedToolCallIds = new Set<string>();
inspectedMessages.forEach((message) => {
(message.tool_calls || []).forEach((toolCall) => unresolvedToolCallIds.add(toolCall.id));
if (message.role === 'tool' && message.tool_call_id) {
unresolvedToolCallIds.delete(message.tool_call_id);
}
});
const contextEntries = Object.entries(aiContexts).flatMap(([contextKey, items]) => (
(items || []).map((item) => ({ contextKey, item }))
));
const ddlChars = contextEntries.reduce((sum, entry) => sum + charCount(entry.item.ddl), 0);
const largestTables = contextEntries
.map((entry) => ({
contextKey: entry.contextKey,
dbName: entry.item.dbName,
tableName: entry.item.tableName,
ddlChars: charCount(entry.item.ddl),
}))
.sort((left, right) => right.ddlChars - left.ddlChars)
.slice(0, shouldIncludeDetails ? 8 : 3);
const mcpSchemaChars = mcpTools.reduce((sum, tool) => (
sum
+ charCount(tool.alias)
+ charCount(tool.description)
+ charCount(tool.inputSchema ? JSON.stringify(tool.inputSchema) : '')
), 0);
const largestMCPTools = [...mcpTools]
.map((tool) => ({
alias: tool.alias,
serverName: tool.serverName,
schemaChars: charCount(tool.inputSchema ? JSON.stringify(tool.inputSchema) : ''),
}))
.sort((left, right) => right.schemaChars - left.schemaChars)
.slice(0, shouldIncludeDetails ? 8 : 3);
const enabledSkills = skills.filter((skill) => skill.enabled);
const skillPromptChars = enabledSkills.reduce((sum, skill) => (
sum + charCount(skill.name) + charCount(skill.description) + charCount(skill.systemPrompt)
), 0);
const userPromptChars = userPromptSettings
? charCount(userPromptSettings.global)
+ charCount(userPromptSettings.database)
+ charCount(userPromptSettings.jvm)
+ charCount(userPromptSettings.jvmDiagnostic)
: 0;
const estimatedInputChars = messagePayloadChars + ddlChars + mcpSchemaChars + skillPromptChars + userPromptChars;
const riskLevel = classifyRisk(estimatedInputChars);
const warnings: string[] = [];
const nextActions: string[] = [];
if (!requestedSessionId || !session) {
appendUnique(warnings, '未找到目标 AI 会话,消息体量统计只覆盖空窗口');
appendUnique(nextActions, '先打开或选中目标 AI 会话,再重新调用 inspect_ai_context_budget');
}
if (riskLevel === 'critical') {
appendUnique(warnings, '当前 AI 输入上下文体量达到 critical可能导致回复慢、截断或模型忽略关键约束');
} else if (riskLevel === 'high') {
appendUnique(warnings, '当前 AI 输入上下文体量偏高,复杂问题前建议先收窄上下文');
}
if (ddlChars >= 60000 || contextEntries.length >= 30) {
appendUnique(warnings, '已挂载表结构较多或 DDL 较长,可能挤占用户问题和工具结果空间');
appendUnique(nextActions, '只保留本轮相关表,必要时改用 inspect_table_bundle 按需读取目标表');
}
if (messagePayloadChars >= 40000) {
appendUnique(warnings, '当前会话最近消息内容较长,可能影响后续回复稳定性');
appendUnique(nextActions, '新开会话或先让 AI 总结当前结论,再继续下一轮复杂任务');
}
if (toolResultChars >= 20000) {
appendUnique(warnings, '最近工具结果较长,可能导致后续回答被日志或大结果集稀释');
appendUnique(nextActions, '降低 inspect_app_logs / inspect_recent_sql_logs / includeDDL / includeLogLines 的返回量');
}
if (mcpTools.length >= 40 || mcpSchemaChars >= 30000) {
appendUnique(warnings, '当前暴露的 MCP 工具或 schema 较多,模型选择工具时可能更容易走偏');
appendUnique(nextActions, '临时禁用无关 MCP 服务,或先调用 inspect_ai_tool_catalog 按关键词收窄工具路线');
}
if (enabledSkills.length >= 8 || skillPromptChars >= 16000) {
appendUnique(warnings, '当前启用 Skills 较多或提示词较长,可能叠加冲突约束');
appendUnique(nextActions, '仅保留本轮任务相关 Skills完成后再恢复其它 Skills');
}
if (unresolvedToolCallIds.size > 0) {
appendUnique(warnings, `最近消息窗口内有 ${unresolvedToolCallIds.size} 个未闭环工具调用`);
appendUnique(nextActions, '先调用 inspect_ai_message_flow 确认工具调用是否缺少 tool 结果消息');
}
if (warnings.length === 0) {
appendUnique(nextActions, '当前上下文体量可控,可继续按具体问题调用更窄的结构、日志或 SQL 风险探针');
}
const largestMessages = inspectedMessages
.map((message) => ({
id: message.id,
role: message.role,
chars: getMessagePayloadChars(message),
preview: shouldIncludeDetails ? previewText(message.content) : undefined,
}))
.sort((left, right) => right.chars - left.chars)
.slice(0, shouldIncludeDetails ? 8 : 3);
return {
requestedSessionId: requestedSessionId || null,
activeSessionId,
foundSession: Boolean(session),
title: session?.title || '',
riskLevel,
estimatedInputChars,
estimatedInputTokens: estimateTokens(estimatedInputChars),
messageWindow: {
totalMessages: allMessages.length,
inspectedMessages: inspectedMessages.length,
limit: effectiveMessageLimit,
roleCounts: messageRoleCounts,
payloadChars: messagePayloadChars,
thinkingChars,
toolResultChars,
unresolvedToolCallCount: unresolvedToolCallIds.size,
largestMessages,
},
schemaContext: {
contextCount: Object.keys(aiContexts).length,
tableCount: contextEntries.length,
ddlChars,
estimatedTokens: estimateTokens(ddlChars),
largestTables,
},
toolCatalog: {
mcpToolCount: mcpTools.length,
mcpSchemaChars,
estimatedTokens: estimateTokens(mcpSchemaChars),
largestMCPTools,
},
promptsAndSkills: {
userPromptChars,
enabledSkillCount: enabledSkills.length,
skillPromptChars,
estimatedTokens: estimateTokens(userPromptChars + skillPromptChars),
enabledSkillNames: enabledSkills.map((skill) => skill.name).slice(0, shouldIncludeDetails ? 20 : 8),
},
warnings,
nextActions,
};
};

View File

@@ -139,6 +139,61 @@ describe('aiLocalToolExecutor local asset inspection tools', () => {
expect(result.content).toContain('回复拆成多个气泡');
});
it('returns ai context budget diagnostics for the active session', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_context_budget', {
messageLimit: 10,
}),
connections: [buildConnection()],
mcpTools: [{
alias: 'remote_probe',
originalName: 'remote_probe',
serverId: 'server-1',
serverName: '远程工具',
inputSchema: {
type: 'object',
properties: {
keyword: { type: 'string' },
},
},
}],
toolContextMap: new Map(),
aiChatSessions: [
{ id: 'session-1', title: '上下文预算排查', updatedAt: 200 },
],
aiChatHistory: {
'session-1': [
{ id: 'msg-1', role: 'user', content: 'AI 变慢是不是上下文太大', timestamp: 101 },
{ id: 'msg-2', role: 'tool', content: 'x'.repeat(21000), timestamp: 102, tool_call_id: 'tool-1' },
],
},
activeSessionId: 'session-1',
aiContexts: {
'conn-1:crm': [
{ dbName: 'crm', tableName: 'orders', ddl: 'CREATE TABLE orders(id bigint, amount decimal(10,2));' },
],
},
skills: [{
id: 'skill-1',
name: 'SQL 审查',
systemPrompt: '先检查风险',
enabled: true,
scopes: ['database'],
}],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"title":"上下文预算排查"');
expect(result.content).toContain('"toolResultChars":21000');
expect(result.content).toContain('"tableName":"orders"');
expect(result.content).toContain('"mcpToolCount":1');
expect(result.content).toContain('最近工具结果较长');
});
it('returns sql snippets so the model can inspect local query templates', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_sql_snippets', {

View File

@@ -16,6 +16,7 @@ import {
buildAIChatSessionsSnapshot,
buildAIMessageFlowSnapshot,
} from './aiChatSessionInsights';
import { buildAIContextBudgetSnapshot } from './aiContextBudgetInsights';
import { buildConnectionCapabilitiesSnapshot } from './aiConnectionCapabilitiesInsights';
import { buildCurrentConnectionSnapshot } from './aiConnectionInsights';
import {
@@ -280,6 +281,22 @@ export async function executeSnapshotInspectionToolCall(
})),
success: true,
};
case 'inspect_ai_context_budget':
return {
content: JSON.stringify(buildAIContextBudgetSnapshot({
aiContexts,
aiChatHistory,
aiChatSessions,
activeSessionId,
sessionId: args.sessionId,
messageLimit: args.messageLimit,
includeDetails: args.includeDetails,
mcpTools,
skills,
userPromptSettings,
})),
success: true,
};
case 'inspect_recent_sql_logs':
return {
content: JSON.stringify(buildRecentSqlLogsSnapshot({
@@ -426,6 +443,7 @@ export async function executeSnapshotInspectionToolCall(
inspect_recent_connection_failures: '汇总最近连接失败记录失败',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常失败',
inspect_ai_message_flow: '读取 AI 消息流诊断失败',
inspect_ai_context_budget: '读取 AI 上下文体量诊断失败',
inspect_saved_queries: '读取已保存查询失败',
inspect_sql_snippets: '读取 SQL 片段失败',
inspect_shortcuts: '读取快捷键配置失败',

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_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'],
availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_ai_tool_catalog', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'],
skills,
userPromptSettings,
});
@@ -100,6 +100,7 @@ describe('buildAISystemContextMessages', () => {
expect(joined).toContain('inspect_app_logs 读取真实应用日志尾部');
expect(joined).toContain('inspect_ai_last_render_error 读取最近一次被隔离的前端渲染异常记录');
expect(joined).toContain('inspect_ai_message_flow 读取当前会话的真实消息结构');
expect(joined).toContain('inspect_ai_context_budget 读取消息、DDL、MCP schema、提示词和 Skills 的体量风险');
expect(joined).toContain('inspect_saved_queries');
expect(joined).toContain('inspect_ai_sessions');
expect(joined).toContain('inspect_sql_snippets');

View File

@@ -146,6 +146,12 @@ export const appendDatabaseInspectionGuidanceMessages = (
'inspect_ai_message_flow',
'如果用户提到“AI 回复被拆成多个气泡”“工具调用后没继续回答”“消息流状态不对”“同一轮回答没有追加到同一个气泡”,优先调用 inspect_ai_message_flow 读取当前会话的真实消息结构、连续 assistant 消息和未闭环工具调用,不要只凭界面现象猜测。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,
'inspect_ai_context_budget',
'如果用户提到“AI 变慢”“上下文太大”“表结构挂太多”“工具结果太长”“模型开始乱答”或复杂任务前需要判断是否该拆小上下文,优先调用 inspect_ai_context_budget 读取消息、DDL、MCP schema、提示词和 Skills 的体量风险,再决定收窄上下文或拆任务。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,

View File

@@ -59,6 +59,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_recent_connection_failures: '总结最近连接失败记录',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常',
inspect_ai_message_flow: '诊断当前 AI 消息流',
inspect_ai_context_budget: '诊断 AI 上下文体量风险',
inspect_saved_queries: '检索本地已保存查询',
inspect_sql_snippets: '读取 SQL 片段模板',
inspect_shortcuts: '读取当前快捷键配置',

View File

@@ -701,6 +701,30 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_ai_context_budget",
icon: "📦",
desc: "诊断 AI 上下文体量与稳定性风险",
detail:
"统计当前或指定 AI 会话的最近消息、工具结果、已挂载表结构、MCP 工具 schema、用户提示词和 Skills 体量,返回 low/medium/high/critical 风险、主要膨胀来源和收窄建议。适合用户反馈 AI 变慢、乱答、上下文太大、工具结果过长或表结构挂太多时先做预算体检。",
params: "sessionId?(默认当前会话), messageLimit?(默认 40), includeDetails?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_ai_context_budget",
description:
"读取当前 AI 上下文体量与稳定性风险快照包括最近消息窗口、tool 结果长度、已挂载表结构 DDL、MCP 工具 schema、用户提示词和启用 Skills 的估算体量,并返回风险级别、告警和收窄建议。适用于用户提到 AI 回复变慢、上下文过大、表结构带太多、工具结果过长、模型开始乱答或复杂任务前需要判断是否应拆小上下文时优先调用。",
parameters: {
type: "object",
properties: {
sessionId: { type: "string", description: "可选,指定要诊断的 AI 会话 ID不传时读取当前活动会话" },
messageLimit: { type: "number", description: "可选,最多统计最近多少条消息,默认 40最大 120" },
includeDetails: { type: "boolean", description: "可选,是否返回最大消息、最大 DDL 表和最大 MCP schema 明细,默认 true" },
},
},
},
},
},
{
name: "inspect_sql_snippets",
icon: "🧩",

View File

@@ -184,6 +184,11 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
steps: 'inspect_ai_message_flow -> inspect_ai_last_render_error / inspect_app_logs',
description: '适合用户反馈回复被拆成多个气泡、工具调用后没继续回答、消息流状态不对时,先读取当前会话的真实消息结构和异常信号。',
},
{
title: '诊断 AI 上下文体量',
steps: 'inspect_ai_context_budget -> inspect_ai_context / inspect_ai_message_flow / inspect_ai_tool_catalog',
description: '适合用户反馈 AI 变慢、乱答、上下文太大、工具结果过长或表结构挂太多时先看消息、DDL、MCP schema、提示词和 Skills 的体量来源,再决定收窄上下文或拆任务。',
},
{
title: '复用历史 SQL',
steps: 'inspect_saved_queries -> get_columns / execute_sql',

View File

@@ -162,6 +162,14 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('连续 assistant 消息');
});
it('registers the ai-context-budget inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_context_budget');
expect(info).toBeTruthy();
expect(info?.desc).toContain('上下文体量');
expect(info?.tool.function.description).toContain('MCP 工具 schema');
expect(info?.tool.function.parameters?.properties?.messageLimit?.description).toContain('最大 120');
});
it('registers the recent-sql-activity, saved-query, and sql-snippet inspectors as builtin tools', () => {
const recentActivityTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_sql_activity');
const sqlEditorTransactionTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_sql_editor_transaction');
@@ -236,6 +244,7 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_recent_connection_failures')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_last_render_error')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_message_flow')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_context_budget')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_sessions')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true);

View File

@@ -137,11 +137,13 @@ describe('mcpClientInstallStatus helpers', () => {
expect(guide).toContain('allowMutating=true');
expect(guide).toContain('"type": "streamable-http"');
expect(guide).toContain('"Authorization": "Bearer <随机token>"');
expect(guide).toContain('GoNavi.exe mcp-server remote-config --client openclaw --url https://<你的域名或隧道地址>/mcp --token <随机token>');
expect(guide).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
});
it('builds remote quick-start snippets for cloud agents without database secrets', () => {
const quickStart = buildRemoteMCPClientQuickStart({
client: 'hermans',
displayName: 'OpenClaw',
});
@@ -150,6 +152,7 @@ describe('mcpClientInstallStatus helpers', () => {
expect(quickStart.configJson).toContain('"url": "https://<你的域名或隧道地址>/mcp"');
expect(quickStart.configJson).toContain('"Authorization": "Bearer <随机token>"');
expect(quickStart.configJson).not.toContain('password');
expect(quickStart.configCommand).toBe('GoNavi.exe mcp-server remote-config --client hermans --url https://<你的域名或隧道地址>/mcp --token <随机token>');
expect(quickStart.launchCommand).toBe('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(quickStart.standaloneCommand).toBe('gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(quickStart.verificationSteps.join('\n')).toContain('get_connections');

View File

@@ -11,6 +11,7 @@ const DEFAULT_REMOTE_MCP_PATH = '/mcp';
export interface RemoteMCPClientQuickStart {
displayName: string;
configJson: string;
configCommand: string;
launchCommand: string;
standaloneCommand: string;
verificationSteps: string[];
@@ -170,7 +171,7 @@ export const formatMCPLaunchCommand = (
};
export const buildRemoteMCPClientGuide = (
status?: Pick<AIMCPClientInstallStatus, 'displayName' | 'message'> | null,
status?: Partial<Pick<AIMCPClientInstallStatus, 'client' | 'displayName' | 'message'>> | null,
): string => {
const quickStart = buildRemoteMCPClientQuickStart(status);
return [
@@ -194,6 +195,9 @@ export const buildRemoteMCPClientGuide = (
'可复制配置片段(适用于支持 mcpServers JSON 的 Agent',
...quickStart.configJson.split('\n'),
'',
'无 GUI / CLI 生成配置命令:',
quickStart.configCommand,
'',
'CLI / 服务启动命令:',
quickStart.launchCommand,
`或设置环境变量GONAVI_MCP_HTTP_TOKEN=<随机token> 后运行 ${quickStart.standaloneCommand.replace(' --token <随机token>', '')}`,
@@ -203,11 +207,13 @@ export const buildRemoteMCPClientGuide = (
};
export const buildRemoteMCPClientQuickStart = (
status?: Pick<AIMCPClientInstallStatus, 'displayName'> | null,
status?: Partial<Pick<AIMCPClientInstallStatus, 'client' | 'displayName'>> | null,
): RemoteMCPClientQuickStart => {
const displayName = String(status?.displayName || '远程 Agent').trim();
const client = isMCPClientKey(String(status?.client || '')) ? String(status?.client || '').trim() : 'openclaw';
const launchCommand = `GoNavi.exe mcp-server http --addr ${DEFAULT_REMOTE_MCP_LOCAL_ADDR} --path ${DEFAULT_REMOTE_MCP_PATH} --token <随机token>`;
const standaloneCommand = `gonavi-mcp-server http --addr ${DEFAULT_REMOTE_MCP_LOCAL_ADDR} --path ${DEFAULT_REMOTE_MCP_PATH} --token <随机token>`;
const configCommand = `GoNavi.exe mcp-server remote-config --client ${client} --url ${DEFAULT_REMOTE_MCP_PUBLIC_URL} --token <随机token>`;
const configJson = JSON.stringify({
mcpServers: {
gonavi: {
@@ -223,6 +229,7 @@ export const buildRemoteMCPClientQuickStart = (
return {
displayName,
configJson,
configCommand,
launchCommand,
standaloneCommand,
verificationSteps: [