feat(ai): 完善远程 MCP 指引与排障体验

This commit is contained in:
Syngnat
2026-06-11 08:31:20 +08:00
parent 26fb650e04
commit 4a944ad23f
16 changed files with 577 additions and 3 deletions

View File

@@ -250,6 +250,9 @@ describe('global appearance tokens', () => {
expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'mono\', installedFontFamilies)');
expect(appSource).toContain('ListInstalledFontFamilies()');
expect(appSource).toContain('const [installedFontFamilies, setInstalledFontFamilies] = useState<InstalledFontFamily[]>(EMPTY_INSTALLED_FONT_FAMILIES);');
expect(appSource).toContain('data-gonavi-linux-cjk-font-banner="true"');
expect(appSource).toContain('Linux CJK fonts missing / Ubuntu 中文字体缺失');
expect(appSource).toContain('setIsLinuxCJKFontBannerDismissed(true)');
expect(appSource).toContain('matchFontFamilyOption');
expect(appSource).toContain('showSearch');
expect(appSource).toContain('const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false;');

View File

@@ -2252,6 +2252,7 @@ function App() {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [isThemeModalOpen, setIsThemeModalOpen] = useState(false);
const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme');
const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false);
const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false);
const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false);
const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false);
@@ -3334,6 +3335,13 @@ function App() {
</span>
</div>
), [darkMode]);
const showLinuxCJKFontBanner = Boolean(
linuxCJKFontInstallHint &&
hasLoadedInstalledFontsRef.current &&
!isFontFamiliesLoading &&
!fontFamiliesLoadError &&
!isLinuxCJKFontBannerDismissed,
);
return (
<ConfigProvider
@@ -3407,6 +3415,54 @@ function App() {
)}
</div>
{showLinuxCJKFontBanner && (
<div
data-gonavi-linux-cjk-font-banner="true"
style={{
flexShrink: 0,
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 14px',
borderBottom: darkMode ? '1px solid rgba(250,204,21,0.20)' : '1px solid rgba(217,119,6,0.18)',
background: darkMode ? 'rgba(250,204,21,0.10)' : 'rgba(255,247,237,0.92)',
color: darkMode ? 'rgba(254,249,195,0.96)' : '#7c2d12',
fontSize: 12,
lineHeight: 1.55,
}}
>
<InfoCircleOutlined style={{ flexShrink: 0, color: darkMode ? '#facc15' : '#d97706' }} />
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ fontWeight: 700 }}>
Linux CJK fonts missing / Ubuntu
</div>
<div>
Chinese text may render as . Install fonts, then restart GoNavi:
<code style={{ marginLeft: 6, fontFamily: 'var(--gn-font-mono)', wordBreak: 'break-all' }}>
{linuxCJKFontInstallHint}
</code>
</div>
</div>
<Button
size="small"
onClick={() => {
setThemeModalSection('appearance');
setIsThemeModalOpen(true);
}}
>
Font Settings
</Button>
<Button
size="small"
type="text"
onClick={() => setIsLinuxCJKFontBannerDismissed(true)}
style={{ color: 'inherit' }}
>
Close
</Button>
</div>
)}
<Layout style={{ flex: 1, minHeight: 0, minWidth: 0 }}>
<Sider
ref={siderRef}

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_support_bundle');
expect(markup).toContain('不含密钥和数据库密码');
expect(markup).toContain('选择 AI 工具路线');
expect(markup).toContain('inspect_ai_tool_catalog');
expect(markup).toContain('每个工具 arguments 怎么填');

View File

@@ -193,6 +193,14 @@ describe('AIMCPClientInstallPanel', () => {
expect(markup).toContain('远程接入边界');
expect(markup).toContain('云端 Agent 只通过 MCP 工具读取连接摘要、库表和 DDL');
expect(markup).toContain('OpenClaw 远程 MCP 快速配置');
expect(markup).toContain('公网/隧道 URL');
expect(markup).toContain('云端 Agent 能访问到的 Streamable HTTP MCP 地址');
expect(markup).toContain('不要填 Windows 本机的 127.0.0.1');
expect(markup).toContain('Bearer Token');
expect(markup).toContain('Windows 启动命令和云端 Agent 配置必须一致');
expect(markup).toContain('不要把数据库密码当 token 填进去');
expect(markup).toContain('本机监听地址');
expect(markup).toContain('MCP 路径');
expect(markup).toContain('配置到云端 Agent');
expect(markup).toContain('无 GUI / CLI 生成配置');
expect(markup).toContain('&quot;type&quot;: &quot;streamable-http&quot;');

View File

@@ -7,6 +7,7 @@ import {
buildRemoteMCPClientQuickStart,
isMCPClientKey,
isRemoteMCPClientStatus,
REMOTE_MCP_PARAMETER_GUIDES,
type MCPClientKey,
} from '../../utils/mcpClientInstallStatus';
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
@@ -312,6 +313,50 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
Agent GUI/CLI Windows GoNavi 使 MCP URL Bearer Token
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 10 }}>
{REMOTE_MCP_PARAMETER_GUIDES.map((item) => (
<div
key={item.key}
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(15,23,42,0.42)' : 'rgba(255,255,255,0.72)',
display: 'flex',
flexDirection: 'column',
gap: 5,
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}>
{item.title}
</div>
<span
style={{
padding: '2px 7px',
borderRadius: 999,
fontSize: 11,
color: item.required ? '#dc2626' : overlayTheme.mutedText,
background: item.required
? (darkMode ? 'rgba(248,113,113,0.12)' : 'rgba(254,226,226,0.7)')
: (darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(15,23,42,0.05)'),
}}
>
{item.required ? '必填' : '可选'}
</span>
</div>
<div style={{ fontSize: 12, color: overlayTheme.titleText, lineHeight: 1.6 }}>
{item.fill}
</div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
<code style={{ fontFamily: 'var(--gn-font-mono)' }}>{item.example}</code>
</div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
{item.avoid}
</div>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 10 }}>
<div
style={{

View File

@@ -194,6 +194,121 @@ describe('aiLocalToolExecutor local asset inspection tools', () => {
expect(result.content).toContain('最近工具结果较长');
});
it('returns an ai support bundle with health, message flow, context budget, and remote MCP evidence', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_support_bundle', {
keyword: 'mcp',
publicUrl: 'https://agent.example.com/mcp',
tokenConfigured: false,
}),
connections: [buildConnection()],
mcpTools: [{
alias: 'remote_probe',
originalName: 'remote_probe',
serverId: 'server-1',
serverName: '远程工具',
description: '读取远程信息',
inputSchema: {
type: 'object',
required: ['keyword'],
properties: {
keyword: { type: 'string', description: '关键词' },
},
},
}],
toolContextMap: new Map(),
aiChatSessions: [
{ id: 'session-1', title: 'AI 稳定性排障', updatedAt: 200 },
],
aiChatHistory: {
'session-1': [
{ id: 'msg-1', role: 'user', content: 'AI 最近不稳定', timestamp: 101 },
{
id: 'msg-2',
role: 'assistant',
content: '先看支持包',
timestamp: 102,
tool_calls: [{ id: 'tool-1', type: 'function', function: { name: 'inspect_app_health', arguments: '{}' } }],
},
{ id: 'msg-3', role: 'assistant', content: '继续分析', timestamp: 103 },
],
},
activeSessionId: 'session-1',
aiContexts: {
'conn-1:crm': [
{ dbName: 'crm', tableName: 'orders', ddl: 'CREATE TABLE orders(id bigint);' },
],
},
skills: [{
id: 'skill-1',
name: 'SQL 审查',
systemPrompt: '先检查风险',
enabled: true,
scopes: ['database'],
}],
runtime: {
getAIRuntimeState: vi.fn(async () => ({
providers: [{
id: 'provider-1',
name: 'OpenAI',
type: 'openai' as const,
apiKey: '',
baseUrl: 'https://api.example.com',
model: 'gpt-5',
hasSecret: true,
maxTokens: 4096,
temperature: 0.2,
}],
activeProviderId: 'provider-1',
safetyLevel: 'readonly',
contextLevel: 'schema_only',
})),
getMCPServers: vi.fn(async () => [{
id: 'server-1',
name: '远程工具',
transport: 'stdio' as const,
command: 'node',
args: ['server.js'],
env: { TOKEN: 'secret-value' },
enabled: true,
timeoutSeconds: 20,
}]),
getMCPClientInstallStatuses: vi.fn(async () => [{
client: 'openclaw',
displayName: 'OpenClaw',
installMode: 'remote' as const,
installed: false,
matchesCurrent: false,
clientDetected: false,
message: 'OpenClaw 走远程 MCP 桥接',
}]),
readAppLogTail: vi.fn(async () => ({
success: true,
logPath: 'C:/Users/mock/.GoNavi/Logs/gonavi.log',
lines: [
'2026/06/11 10:00:00 [WARN] MCP mock warning',
'2026/06/11 10:00:01 [ERROR] MCP mock error',
],
fileWindowTruncated: false,
matchedLinesTruncated: false,
})),
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"kind":"ai_support_bundle"');
expect(result.content).toContain('"databasePasswordsIncluded":false');
expect(result.content).toContain('"providerSecretsIncluded":false');
expect(result.content).toContain('"mcpEnvValuesIncluded":false');
expect(result.content).toContain('"unresolvedToolCallCount":1');
expect(result.content).toContain('"consecutiveAssistantPairCount":1');
expect(result.content).toContain('"remoteMCPPublicUrl":"https://agent.example.com/mcp"');
expect(result.content).toContain('尚未确认 Bearer Token');
expect(result.content).not.toContain('secret-value');
});
it('returns sql snippets so the model can inspect local query templates', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_sql_snippets', {

View File

@@ -1,4 +1,5 @@
import type {
AIChatMessage,
AIContextItem,
AIMCPToolDescriptor,
AISkillConfig,
@@ -9,6 +10,7 @@ import type {
import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry';
import { buildAIAppHealthSnapshot } from './aiAppHealthInsights';
import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights';
import { buildAISupportBundleSnapshot } from './aiSupportBundleInsights';
import type {
AISnapshotInspectionRuntime,
AISnapshotInspectionRuntimeState,
@@ -24,6 +26,9 @@ interface ExecuteAppHealthSnapshotToolCallOptions {
args: Record<string, any>;
activeContext?: { connectionId: string; dbName: string } | null;
aiContexts?: Record<string, AIContextItem[]>;
aiChatHistory?: Record<string, AIChatMessage[]>;
aiChatSessions?: Array<{ id: string; title: string; updatedAt: number }>;
activeSessionId?: string | null;
connections: SavedConnection[];
tabs?: TabData[];
activeTabId?: string | null;
@@ -75,6 +80,9 @@ export async function executeAppHealthSnapshotToolCall(
args,
activeContext = null,
aiContexts = {},
aiChatHistory = {},
aiChatSessions = [],
activeSessionId = null,
connections,
tabs = [],
activeTabId = null,
@@ -85,7 +93,7 @@ export async function executeAppHealthSnapshotToolCall(
runtime,
} = options;
if (toolName !== 'inspect_app_health') {
if (toolName !== 'inspect_app_health' && toolName !== 'inspect_ai_support_bundle') {
return null;
}
@@ -101,6 +109,49 @@ export async function executeAppHealthSnapshotToolCall(
]);
const [mcpServers, mcpClientInstallStatuses] = mcpState;
if (toolName === 'inspect_ai_support_bundle') {
return {
content: JSON.stringify(buildAISupportBundleSnapshot({
providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [],
activeProviderId: runtimeState?.activeProviderId || '',
safetyLevel: runtimeState?.safetyLevel,
contextLevel: runtimeState?.contextLevel,
skills,
mcpServers: Array.isArray(mcpServers) ? mcpServers : [],
mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [],
mcpTools,
dynamicModels,
builtinTools: BUILTIN_AI_TOOL_INFO,
builtinToolNames: BUILTIN_AI_TOOL_NAMES,
userPromptSettings,
activeContext,
aiContexts,
aiChatHistory,
aiChatSessions,
activeSessionId,
sessionId: args.sessionId,
connections,
tabs,
activeTabId,
appLogReadResult,
connectionFailureReadResult,
lastRenderErrorSnapshot: buildAILastRenderErrorSnapshot(),
keyword,
connectionKeyword,
lineLimit,
includeLogLines: args.includeLogLines === true,
includeMessageContent: args.includeMessageContent === true,
includeDetails: args.includeDetails === true,
publicUrl: args.publicUrl,
localAddr: args.localAddr,
path: args.path,
exposeStrategy: args.exposeStrategy,
tokenConfigured: args.tokenConfigured,
})),
success: true,
};
}
return {
content: JSON.stringify(buildAIAppHealthSnapshot({
providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [],
@@ -131,7 +182,7 @@ export async function executeAppHealthSnapshotToolCall(
};
} catch (error: any) {
return {
content: `读取 AI 应用健康总览失败: ${error?.message || error}`,
content: `${toolName === 'inspect_ai_support_bundle' ? '生成 AI 支持包失败' : '读取 AI 应用健康总览失败'}: ${error?.message || error}`,
success: false,
};
}

View File

@@ -101,6 +101,9 @@ export async function executeSnapshotInspectionToolCall(
args,
activeContext,
aiContexts,
aiChatHistory,
aiChatSessions,
activeSessionId,
connections,
tabs,
activeTabId,

View File

@@ -0,0 +1,179 @@
import type {
AIChatMessage,
AIContextItem,
AIMCPClientInstallStatus,
AIMCPServerConfig,
AIMCPToolDescriptor,
AIProviderConfig,
AISafetyLevel,
AISkillConfig,
AIUserPromptSettings,
SavedConnection,
TabData,
} from '../../types';
import type { AIBuiltinToolInfo } from '../../utils/aiBuiltinToolInfo.types';
import { buildAIAppHealthSnapshot } from './aiAppHealthInsights';
import { buildAIMessageFlowSnapshot } from './aiChatSessionInsights';
import { buildAIContextBudgetSnapshot } from './aiContextBudgetInsights';
import { buildMCPRemoteAccessSnapshot } from './aiMCPRemoteAccessInsights';
import { buildAIToolCatalogSnapshot } from './aiToolCatalogInsights';
const appendUnique = (items: string[], value: string) => {
const trimmed = String(value || '').trim();
if (!trimmed || items.includes(trimmed)) {
return;
}
items.push(trimmed);
};
export const buildAISupportBundleSnapshot = (params: {
providers?: AIProviderConfig[];
activeProviderId?: string | null;
safetyLevel?: AISafetyLevel | string;
contextLevel?: string;
skills?: AISkillConfig[];
mcpServers?: AIMCPServerConfig[];
mcpClientStatuses?: AIMCPClientInstallStatus[];
mcpTools?: AIMCPToolDescriptor[];
dynamicModels?: string[];
builtinTools?: AIBuiltinToolInfo[];
builtinToolNames?: string[];
userPromptSettings?: AIUserPromptSettings;
activeContext?: { connectionId?: string | null; dbName?: string | null } | null;
aiContexts?: Record<string, AIContextItem[]>;
aiChatHistory?: Record<string, AIChatMessage[]>;
aiChatSessions?: Array<{ id: string; title: string; updatedAt: number }>;
activeSessionId?: string | null;
sessionId?: unknown;
connections?: SavedConnection[];
tabs?: TabData[];
activeTabId?: string | null;
appLogReadResult?: any;
connectionFailureReadResult?: any;
lastRenderErrorSnapshot?: any;
keyword?: unknown;
connectionKeyword?: unknown;
lineLimit?: unknown;
includeLogLines?: boolean;
includeMessageContent?: boolean;
includeDetails?: boolean;
publicUrl?: string;
localAddr?: string;
path?: string;
exposeStrategy?: string;
tokenConfigured?: boolean;
}) => {
const aiChatHistory = params.aiChatHistory || {};
const aiChatSessions = params.aiChatSessions || [];
const requestedSessionId = String(params.sessionId || params.activeSessionId || '').trim();
const appHealth = buildAIAppHealthSnapshot({
providers: params.providers,
activeProviderId: params.activeProviderId,
safetyLevel: params.safetyLevel,
contextLevel: params.contextLevel,
skills: params.skills,
mcpServers: params.mcpServers,
mcpClientStatuses: params.mcpClientStatuses,
mcpTools: params.mcpTools,
dynamicModels: params.dynamicModels,
builtinToolNames: params.builtinToolNames,
userPromptSettings: params.userPromptSettings,
activeContext: params.activeContext,
aiContexts: params.aiContexts,
connections: params.connections,
tabs: params.tabs,
activeTabId: params.activeTabId,
appLogReadResult: params.appLogReadResult,
connectionFailureReadResult: params.connectionFailureReadResult,
lastRenderErrorSnapshot: params.lastRenderErrorSnapshot,
keyword: params.keyword,
connectionKeyword: params.connectionKeyword,
lineLimit: params.lineLimit,
includeLogLines: params.includeLogLines === true,
});
const messageFlow = buildAIMessageFlowSnapshot({
aiChatSessions,
aiChatHistory,
activeSessionId: params.activeSessionId,
sessionId: requestedSessionId,
limit: 32,
includeContent: params.includeMessageContent === true,
previewLimit: 240,
});
const contextBudget = buildAIContextBudgetSnapshot({
aiContexts: params.aiContexts,
aiChatHistory,
aiChatSessions,
activeSessionId: params.activeSessionId,
sessionId: requestedSessionId,
messageLimit: 50,
includeDetails: params.includeDetails === true,
mcpTools: params.mcpTools,
skills: params.skills,
userPromptSettings: params.userPromptSettings,
});
const remoteAccess = buildMCPRemoteAccessSnapshot({
mcpClientStatuses: params.mcpClientStatuses,
publicUrl: params.publicUrl,
localAddr: params.localAddr,
path: params.path,
exposeStrategy: params.exposeStrategy,
tokenConfigured: params.tokenConfigured,
});
const toolCatalog = buildAIToolCatalogSnapshot({
builtinTools: params.builtinTools || [],
mcpTools: params.mcpTools,
keyword: String(params.keyword || 'ai mcp 日志 连接 上下文').trim(),
includeMCPTools: true,
limit: 10,
});
const warnings: string[] = [];
const nextActions: string[] = [];
appHealth.warnings.forEach((item) => appendUnique(warnings, item));
contextBudget.warnings.forEach((item) => appendUnique(warnings, item));
messageFlow.warnings.forEach((item) => appendUnique(warnings, item));
remoteAccess.warnings.forEach((item) => appendUnique(warnings, item));
appHealth.nextActions.forEach((item) => appendUnique(nextActions, item));
contextBudget.nextActions.forEach((item) => appendUnique(nextActions, item));
messageFlow.nextActions.forEach((item) => appendUnique(nextActions, item));
remoteAccess.nextActions.forEach((item) => appendUnique(nextActions, item));
return {
kind: 'ai_support_bundle',
message: '已生成 GoNavi AI 支持包快照,可用于排查 AI、MCP、日志、连接和上下文体量问题',
privacy: {
databasePasswordsIncluded: false,
providerSecretsIncluded: false,
mcpEnvValuesIncluded: false,
logLinesIncluded: params.includeLogLines === true,
messageContentIncluded: params.includeMessageContent === true,
note: '默认只返回摘要和结构化计数;只有显式开启 includeLogLines/includeMessageContent 时才附带日志或消息内容预览。',
},
summary: {
appHealthStatus: appHealth.status,
appHealthReady: appHealth.ready,
aiSetupStatus: appHealth.summary.aiSetupStatus,
chatReady: appHealth.summary.chatReady,
contextRiskLevel: contextBudget.riskLevel,
estimatedInputChars: contextBudget.estimatedInputChars,
messageFlowWarningCount: messageFlow.warnings.length,
unresolvedToolCallCount: messageFlow.unresolvedToolCallCount,
consecutiveAssistantPairCount: messageFlow.consecutiveAssistantPairCount,
appLogErrorCount: appHealth.summary.appLogErrorCount,
appLogWarnCount: appHealth.summary.appLogWarnCount,
recentConnectionFailureCount: appHealth.summary.recentConnectionFailureCount,
mcpServerCount: appHealth.summary.mcpServerCount,
discoveredMCPToolCount: appHealth.summary.discoveredMCPToolCount,
remoteMCPPublicUrl: remoteAccess.endpoint.publicUrl,
toolCatalogReturned: toolCatalog.returned,
},
warnings,
nextActions,
appHealth,
messageFlow,
contextBudget,
remoteAccess,
toolCatalog,
};
};

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_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'],
availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_support_bundle', '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,
});
@@ -76,6 +76,7 @@ describe('buildAISystemContextMessages', () => {
const joined = messages.map((message) => message.content).join('\n');
expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区');
expect(joined).toContain('inspect_app_health 获取 AI 配置、应用日志、连接失败、回复气泡渲染异常和工作区页签的全局健康总览');
expect(joined).toContain('inspect_ai_support_bundle 生成不含密钥和数据库密码的支持包');
expect(joined).toContain('inspect_ai_setup_health 先拿到整体现状');
expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态');
expect(joined).toContain('inspect_ai_safety 读取真实安全边界');

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_support_bundle',
'如果用户提到“AI 不成熟/不稳定”“帮我导出排障材料”“MCP、连接、日志、上下文一起看”或准备把问题交给开发定位优先调用 inspect_ai_support_bundle 生成不含密钥和数据库密码的支持包,再根据 warnings 和 nextActions 下钻。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,

View File

@@ -28,6 +28,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_ai_providers: '读取当前 AI 供应商与模型配置',
inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态',
inspect_ai_tool_catalog: '读取 AI 工具目录和参数提示',
inspect_ai_support_bundle: '生成 AI 排障支持包',
inspect_mcp_setup: '读取当前 MCP 配置状态',
inspect_mcp_authoring_guide: '读取 MCP 新增填写指引',
inspect_mcp_draft: '校验 MCP 新增草稿',

View File

@@ -26,6 +26,43 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_ai_support_bundle",
icon: "📦",
desc: "导出 AI 排障支持包",
detail:
"一次性汇总 AI 应用健康、供应商与 MCP 状态、应用日志摘要、连接失败摘要、消息流结构、上下文体量、远程 MCP 接入和工具目录索引。适合用户反馈“AI 不稳定”“MCP/连接/日志一起看”“要给开发排障材料”时先生成一份不含密钥和数据库密码的支持包。",
params: "keyword?, sessionId?, lineLimit?(默认 120), includeLogLines?(默认 false), includeMessageContent?(默认 false), publicUrl?, tokenConfigured?",
tool: {
type: "function",
function: {
name: "inspect_ai_support_bundle",
description:
"生成 GoNavi AI 排障支持包,汇总 AI 应用健康、供应商和发送前置、MCP 配置和远程接入、应用日志摘要、数据库连接失败摘要、当前 AI 消息流、上下文体量风险和工具目录索引。默认不包含数据库密码、供应商密钥、MCP 环境变量值、日志原文或完整消息内容。适用于用户反馈 AI 不稳定、MCP/连接/日志问题交织、需要一次性导出排障证据或准备给开发定位时优先调用。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按关键词过滤日志和工具目录,例如 ai、mcp、mysql、error、openclaw" },
connectionKeyword: { type: "string", description: "可选,分析连接失败日志时使用的关键词;不传时复用 keyword" },
sessionId: { type: "string", description: "可选,指定要诊断的 AI 会话 ID不传时使用当前活动会话" },
lineLimit: { type: "number", description: "可选,最多分析多少行应用日志,默认 120最大 240" },
includeLogLines: { type: "boolean", description: "可选,是否附带日志原文行,默认 false需要引用原文时再开启" },
includeMessageContent: { type: "boolean", description: "可选,是否附带消息内容预览,默认 false排查气泡内容时再开启" },
includeDetails: { type: "boolean", description: "可选,是否附带上下文体量明细,默认 false" },
publicUrl: { type: "string", description: "可选,云端 Agent 访问 GoNavi MCP 的公网/隧道 URL用于远程 MCP 支持包" },
localAddr: { type: "string", description: "可选Windows 本机 HTTP MCP 监听地址,默认 127.0.0.1:8765" },
path: { type: "string", description: "可选Streamable HTTP MCP 路径,默认 /mcp" },
exposeStrategy: {
type: "string",
enum: ["reverse_proxy", "ssh_reverse_tunnel", "cloudflare_tunnel", "tailscale", "custom"],
description: "可选,远程暴露方式,用于生成对应安全提醒",
},
tokenConfigured: { type: "boolean", description: "可选,是否已经准备随机 Bearer Token传 false 会返回鉴权告警" },
},
},
},
},
},
{
name: "inspect_ai_setup_health",
icon: "🩺",

View File

@@ -44,6 +44,11 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
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_support_bundle -> inspect_app_health / inspect_ai_context_budget / inspect_ai_message_flow / inspect_mcp_remote_access',
description: '适合需要一次性带走排障证据,或用户反馈 AI 不成熟、不稳定、MCP/连接/日志/上下文都可能相关时,先生成不含密钥和数据库密码的支持包。',
},
{
title: '选择 AI 工具路线',
steps: 'inspect_ai_tool_catalog -> inspect_ai_runtime / inspect_mcp_setup',

View File

@@ -17,6 +17,14 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('聊天发送前置');
});
it('registers the ai-support-bundle inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_support_bundle');
expect(info).toBeTruthy();
expect(info?.desc).toContain('排障支持包');
expect(info?.tool.function.description).toContain('默认不包含数据库密码');
expect(info?.tool.function.parameters?.properties?.includeMessageContent?.description).toContain('默认 false');
});
it('registers the ai-safety inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_safety');
expect(info).toBeTruthy();
@@ -222,6 +230,7 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_setup_health')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_support_bundle')).toBe(true);
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);

View File

@@ -18,6 +18,58 @@ export interface RemoteMCPClientQuickStart {
securityNotes: string[];
}
export interface RemoteMCPParameterGuide {
key: string;
title: string;
required: boolean;
fill: string;
example: string;
avoid: string;
}
export const REMOTE_MCP_PARAMETER_GUIDES: RemoteMCPParameterGuide[] = [
{
key: 'publicUrl',
title: '公网/隧道 URL',
required: true,
fill: '填云端 Agent 能访问到的 Streamable HTTP MCP 地址,通常以 /mcp 结尾。',
example: 'https://agent-gateway.example.com/mcp',
avoid: '不要填 Windows 本机的 127.0.0.1;云端 Linux 访问不到这个地址。',
},
{
key: 'bearerToken',
title: 'Bearer Token',
required: true,
fill: '填一段随机长 tokenWindows 启动命令和云端 Agent 配置必须一致。',
example: 'Authorization: Bearer gnv_xxx',
avoid: '不要使用空 token、短 token也不要把数据库密码当 token 填进去。',
},
{
key: 'localAddr',
title: '本机监听地址',
required: true,
fill: 'Windows GoNavi HTTP MCP 默认监听 127.0.0.1:8765再交给隧道或反向代理转发。',
example: DEFAULT_REMOTE_MCP_LOCAL_ADDR,
avoid: '没有网关隔离时不要直接绑定 0.0.0.0 暴露到公网。',
},
{
key: 'path',
title: 'MCP 路径',
required: true,
fill: '本机启动命令、隧道 URL 和云端 Agent 配置里的路径要保持一致。',
example: DEFAULT_REMOTE_MCP_PATH,
avoid: '不要一边用 /mcp另一边配置 /api/mcp路径不一致会 404。',
},
{
key: 'serverId',
title: '服务 ID',
required: false,
fill: '给云端 Agent 识别这条 MCP 服务的名称,默认 gonavi 即可。',
example: 'gonavi',
avoid: '不要频繁改名,否则 Agent 里已有的工具引用可能失效。',
},
];
export const EMPTY_MCP_CLIENT_STATUSES: AIMCPClientInstallStatus[] = [
{
client: 'claude-code',