mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-16 19:49:51 +08:00
✨ feat(ai): 完善远程 MCP 指引与排障体验
This commit is contained in:
@@ -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;');
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 怎么填');
|
||||
|
||||
@@ -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('"type": "streamable-http"');
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ export async function executeSnapshotInspectionToolCall(
|
||||
args,
|
||||
activeContext,
|
||||
aiContexts,
|
||||
aiChatHistory,
|
||||
aiChatSessions,
|
||||
activeSessionId,
|
||||
connections,
|
||||
tabs,
|
||||
activeTabId,
|
||||
|
||||
179
frontend/src/components/ai/aiSupportBundleInsights.ts
Normal file
179
frontend/src/components/ai/aiSupportBundleInsights.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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 读取真实安全边界');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 新增草稿',
|
||||
|
||||
@@ -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: "🩺",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '填一段随机长 token,Windows 启动命令和云端 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',
|
||||
|
||||
Reference in New Issue
Block a user