feat(ai): 新增上游请求日志自查工具

This commit is contained in:
Syngnat
2026-06-11 12:00:17 +08:00
parent 2d562ccfd6
commit 4265d7cfa9
11 changed files with 472 additions and 1 deletions

View File

@@ -44,6 +44,9 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_ai_providers');
expect(markup).toContain('排查聊天发送状态');
expect(markup).toContain('inspect_ai_chat_readiness');
expect(markup).toContain('追踪 AI 上游请求');
expect(markup).toContain('inspect_ai_upstream_logs');
expect(markup).toContain('请求体预览');
expect(markup).toContain('排查 MCP 接入状态');
expect(markup).toContain('inspect_mcp_setup');
expect(markup).toContain('新增 MCP 填写指引');

View File

@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from 'vitest';
import type { AIToolCall } from '../../types';
import { executeLocalAIToolCall } from './aiLocalToolExecutor';
const buildToolCall = (
name: string,
args: Record<string, unknown>,
): AIToolCall => ({
id: `call-${name}`,
type: 'function',
function: {
name,
arguments: JSON.stringify(args),
},
});
describe('aiLocalToolExecutor inspect_ai_upstream_logs', () => {
it('returns sanitized upstream request payloads and request lifecycle summaries from gonavi.log', async () => {
const readAppLogTail = vi.fn().mockResolvedValue({
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
keyword: 'openai',
requestedLineLimit: 160,
fileWindowTruncated: false,
matchedLinesTruncated: false,
lines: [
'2026/06/11 11:20:00.000000 [INFO] AI 上游请求开始requestId=openai-123 provider=openai method=POST endpoint=https://api.example.com/v1/chat/completions?key=[REDACTED] body={"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}],"api_key":"[REDACTED]"}',
'2026/06/11 11:20:01.000000 [INFO] AI 上游请求完成requestId=openai-123 provider=openai endpoint=https://api.example.com/v1/chat/completions?key=[REDACTED] status=200 duration=981ms',
'2026/06/11 11:20:02.000000 [WARN] AI 上游请求失败requestId=gemini-456 provider=gemini endpoint=https://generativelanguage.googleapis.com duration=2s err=upstream Authorization Bearer abcdefghijklmnopqrstuvwxyz failed',
],
},
});
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_upstream_logs', {
provider: 'openai',
includeBody: true,
}),
connections: [],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
readAppLogTail,
},
});
expect(result.success).toBe(true);
expect(readAppLogTail).toHaveBeenCalledWith(160, 'openai');
expect(result.content).toContain('"logPath":"C:/Users/demo/.GoNavi/Logs/gonavi.log"');
expect(result.content).toContain('"upstreamEventCount":2');
expect(result.content).toContain('"requestId":"openai-123"');
expect(result.content).toContain('"provider":"openai"');
expect(result.content).toContain('"state":"completed"');
expect(result.content).toContain('"status":200');
expect(result.content).toContain('"bodyPreview"');
expect(result.content).toContain('gpt-5.5');
expect(result.content).not.toContain('abcdefghijklmnopqrstuvwxyz');
});
it('returns an actionable empty-state message when no upstream request log is available', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_upstream_logs', {
requestId: 'openai-missing',
}),
connections: [],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
readAppLogTail: vi.fn().mockResolvedValue({
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
keyword: 'openai-missing',
requestedLineLimit: 160,
lines: [],
},
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"upstreamEventCount":0');
expect(result.content).toContain('请先发送一次 AI 消息');
expect(result.content).toContain('扩大 lineLimit');
});
});

View File

@@ -27,6 +27,7 @@ import { buildSavedConnectionsSnapshot } from './aiSavedConnectionInsights';
import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
import { buildExternalSQLDirectoriesSnapshot } from './aiExternalSqlInsights';
import { buildAppLogSnapshot } from './aiAppLogInsights';
import { buildAIUpstreamLogSnapshot } from './aiUpstreamLogInsights';
import { findBestMatchingExternalSQLDirectory } from './aiExternalSqlPathUtils';
import {
buildRecentSqlActivitySnapshot,
@@ -356,6 +357,32 @@ export async function executeSnapshotInspectionToolCall(
success: true,
};
}
case 'inspect_ai_upstream_logs': {
const keyword = String(args.requestId || args.provider || args.keyword || 'AI 上游请求').trim();
const readResult = typeof runtime?.readAppLogTail === 'function'
? await runtime.readAppLogTail(Number(args.lineLimit) || 160, keyword)
: { success: false, message: '当前环境暂不支持读取 GoNavi 应用日志' };
if (!readResult?.success) {
return {
content: `读取 AI 上游请求日志失败: ${readResult?.message || '未知错误'}`,
success: false,
};
}
return {
content: JSON.stringify(buildAIUpstreamLogSnapshot({
readResult,
provider: args.provider,
requestId: args.requestId,
keyword: args.keyword,
lineLimit: args.lineLimit,
requestLimit: args.requestLimit,
includeBody: args.includeBody !== false,
includeLines: args.includeLines === true,
bodyPreviewLimit: args.bodyPreviewLimit,
})),
success: true,
};
}
case 'inspect_recent_connection_failures': {
const readResult = typeof runtime?.readAppLogTail === 'function'
? await runtime.readAppLogTail(Number(args.lineLimit) || 120, String(args.keyword || ''))
@@ -443,6 +470,7 @@ export async function executeSnapshotInspectionToolCall(
inspect_sql_editor_transaction: '读取 SQL 编辑器事务状态失败',
inspect_sql_risk: '检查 SQL 风险失败',
inspect_app_logs: '读取 GoNavi 应用日志失败',
inspect_ai_upstream_logs: '读取 AI 上游请求日志失败',
inspect_recent_connection_failures: '汇总最近连接失败记录失败',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常失败',
inspect_ai_message_flow: '读取 AI 消息流诊断失败',

View File

@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
connections: [connections[0]],
tabs: [],
activeTabId: null,
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'],
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_upstream_logs', '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,
});
@@ -82,6 +82,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_upstream_logs 读取脱敏后的真实请求日志');
expect(joined).toContain('inspect_ai_tool_catalog 按关键词读取真实工具目录');
expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置');
expect(joined).toContain('inspect_mcp_authoring_guide 读取真实新增指引和模板');

View File

@@ -86,6 +86,12 @@ export const appendDatabaseInspectionGuidanceMessages = (
'inspect_ai_chat_readiness',
'如果用户提到“为什么现在不能发送”“当前 AI 聊天到底缺什么配置”“输入区准备好了没有”,优先调用 inspect_ai_chat_readiness 读取真实发送前置状态,不要只凭界面现象或记忆判断。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,
'inspect_ai_upstream_logs',
'如果用户提到“AI 上游请求”“请求入参/请求体”“requestId”“发给模型的 payload”“上游接口报错具体传了什么”优先调用 inspect_ai_upstream_logs 读取脱敏后的真实请求日志,再结合 inspect_ai_providers 或 inspect_ai_message_flow 继续定位。',
);
appendGuidanceIfToolAvailable(
messages,
availableToolNames,

View File

@@ -17,6 +17,13 @@ const normalizeLimit = (value: unknown): number =>
const matchesAnyText = (keyword: string, values: unknown[]): boolean =>
!keyword || values.some((value) => normalizeText(value).includes(keyword));
const scoreKeywordMatch = (keyword: string, values: Array<{ value: unknown; weight: number }>): number =>
!keyword
? 0
: values.reduce((score, item) => (
normalizeText(item.value).includes(keyword) ? score + item.weight : score
), 0);
const readMCPToolParameterSummary = (tool: AIMCPToolDescriptor) => {
const schema = tool.inputSchema && typeof tool.inputSchema === 'object'
? tool.inputSchema as Record<string, any>
@@ -57,6 +64,17 @@ export const buildAIToolCatalogSnapshot = (params: {
const matchedFlows = BUILTIN_TOOL_FLOWS
.filter((flow) => matchesAnyText(keyword, [flow.title, flow.steps, flow.description]))
.map((flow, index) => ({
flow,
index,
score: scoreKeywordMatch(keyword, [
{ value: flow.title, weight: 100 },
{ value: flow.steps, weight: 60 },
{ value: flow.description, weight: 20 },
]),
}))
.sort((left, right) => (right.score - left.score) || (left.index - right.index))
.map((item) => item.flow)
.slice(0, limit);
const matchedBuiltinTools = builtinTools
@@ -76,6 +94,25 @@ export const buildAIToolCatalogSnapshot = (params: {
]),
]);
})
.map((tool, index) => ({
tool,
index,
score: normalizedToolName
? 0
: scoreKeywordMatch(keyword, [
{ value: tool.name, weight: 120 },
{ value: tool.desc, weight: 100 },
{ value: tool.params, weight: 60 },
{ value: tool.detail, weight: 20 },
...describeBuiltinToolParameters(tool).flatMap((param) => [
{ value: param.name, weight: 40 },
{ value: param.description, weight: 20 },
{ value: param.enumValues.join(' '), weight: 20 },
]),
]),
}))
.sort((left, right) => (right.score - left.score) || (left.index - right.index))
.map((item) => item.tool)
.slice(0, limit)
.map((tool) => ({
name: tool.name,

View File

@@ -0,0 +1,249 @@
const DEFAULT_AI_UPSTREAM_LOG_LIMIT = 160;
const MAX_AI_UPSTREAM_LOG_LIMIT = 300;
const DEFAULT_AI_UPSTREAM_REQUEST_LIMIT = 12;
const MAX_AI_UPSTREAM_REQUEST_LIMIT = 40;
const DEFAULT_AI_UPSTREAM_BODY_LIMIT = 6000;
const MAX_AI_UPSTREAM_BODY_LIMIT = 12000;
const secretLikeValuePatterns = [
/bearer\s+[a-z0-9._~+/=-]{8,}/gi,
/\bsk-[a-z0-9._-]{8,}/gi,
/\bgh[pousr]_[a-z0-9_]{8,}/gi,
/\bxox[baprs]-[a-z0-9-]{8,}/gi,
];
type AIUpstreamLogEventType = 'started' | 'completed' | 'failed' | 'other';
interface AIUpstreamLogEvent {
type: AIUpstreamLogEventType;
requestId: string;
provider: string;
method: string;
endpoint: string;
status?: number;
duration?: string;
bodyPreview?: string;
error?: string;
line: string;
}
interface AIUpstreamRequestSummary {
requestId: string;
provider: string;
method: string;
endpoint: string;
state: 'started' | 'completed' | 'failed' | 'unknown';
status?: number;
duration?: string;
bodyPreview?: string;
error?: string;
eventCount: number;
hasBody: boolean;
}
const clampNumber = (value: unknown, fallback: number, min: number, max: number): number => {
const normalized = Math.floor(Number(value) || fallback);
if (normalized < min) return min;
if (normalized > max) return max;
return normalized;
};
const normalizeLogLines = (input: unknown): string[] =>
Array.isArray(input)
? input.map((line) => String(line || '').trim()).filter(Boolean)
: [];
const redactAIUpstreamLogPreview = (value: string): string => {
let result = value;
secretLikeValuePatterns.forEach((pattern) => {
result = result.replace(pattern, '[REDACTED]');
});
return result.replace(/data:([^;,]+);base64,[a-z0-9+/=._-]+/gi, 'data:$1;base64,[REDACTED]');
};
const truncateText = (value: string, limit: number): string => {
if (value.length <= limit) {
return value;
}
return `${value.slice(0, limit)}...[truncated ${value.length - limit} chars]`;
};
const extractField = (line: string, field: string): string => {
const match = line.match(new RegExp(`(?:^|[\\s])${field}=([^\\s]+)`));
return match?.[1] || '';
};
const extractTailField = (line: string, field: string): string => {
const marker = ` ${field}=`;
const index = line.indexOf(marker);
if (index < 0) {
return '';
}
return line.slice(index + marker.length).trim();
};
const resolveEventType = (line: string): AIUpstreamLogEventType => {
if (line.includes('AI 上游请求开始')) return 'started';
if (line.includes('AI 上游请求完成')) return 'completed';
if (line.includes('AI 上游请求失败')) return 'failed';
return 'other';
};
const parseAIUpstreamLogEvent = (
line: string,
bodyPreviewLimit: number,
): AIUpstreamLogEvent | null => {
if (!line.includes('AI 上游请求')) {
return null;
}
const type = resolveEventType(line);
const requestId = extractField(line, 'requestId');
const provider = extractField(line, 'provider');
const method = extractField(line, 'method');
const endpoint = extractField(line, 'endpoint');
const statusText = extractField(line, 'status');
const bodyText = extractTailField(line, 'body');
const errorText = extractTailField(line, 'err');
const duration = extractField(line, 'duration');
return {
type,
requestId,
provider,
method,
endpoint,
status: statusText ? Number(statusText) : undefined,
duration,
bodyPreview: bodyText ? truncateText(redactAIUpstreamLogPreview(bodyText), bodyPreviewLimit) : undefined,
error: errorText ? truncateText(redactAIUpstreamLogPreview(errorText), 2000) : undefined,
line: truncateText(redactAIUpstreamLogPreview(line), 4000),
};
};
const matchesFilter = (event: AIUpstreamLogEvent, params: {
provider?: unknown;
requestId?: unknown;
keyword?: unknown;
}): boolean => {
const provider = String(params.provider || '').trim().toLowerCase();
if (provider && event.provider.toLowerCase() !== provider) {
return false;
}
const requestId = String(params.requestId || '').trim().toLowerCase();
if (requestId && event.requestId.toLowerCase() !== requestId) {
return false;
}
const keyword = String(params.keyword || '').trim().toLowerCase();
if (!keyword) {
return true;
}
return [
event.requestId,
event.provider,
event.method,
event.endpoint,
event.bodyPreview,
event.error,
].some((value) => String(value || '').toLowerCase().includes(keyword));
};
const summarizeAIUpstreamRequests = (
events: AIUpstreamLogEvent[],
includeBody: boolean,
requestLimit: number,
): AIUpstreamRequestSummary[] => {
const requestMap = new Map<string, AIUpstreamRequestSummary>();
events.forEach((event) => {
const requestKey = event.requestId || `line-${requestMap.size + 1}`;
const existing = requestMap.get(requestKey) || {
requestId: event.requestId,
provider: event.provider,
method: event.method,
endpoint: event.endpoint,
state: 'unknown' as const,
eventCount: 0,
hasBody: false,
};
existing.provider = event.provider || existing.provider;
existing.method = event.method || existing.method;
existing.endpoint = event.endpoint || existing.endpoint;
existing.eventCount += 1;
if (event.type === 'started') existing.state = 'started';
if (event.type === 'completed') existing.state = 'completed';
if (event.type === 'failed') existing.state = 'failed';
if (event.status !== undefined) existing.status = event.status;
if (event.duration) existing.duration = event.duration;
if (event.bodyPreview) {
existing.hasBody = true;
if (includeBody) existing.bodyPreview = event.bodyPreview;
}
if (event.error) existing.error = event.error;
requestMap.set(requestKey, existing);
});
return Array.from(requestMap.values()).slice(-requestLimit);
};
export const buildAIUpstreamLogSnapshot = (params: {
readResult?: any;
provider?: unknown;
requestId?: unknown;
keyword?: unknown;
lineLimit?: unknown;
requestLimit?: unknown;
includeBody?: unknown;
includeLines?: unknown;
bodyPreviewLimit?: unknown;
}) => {
const data = params.readResult?.data && typeof params.readResult.data === 'object'
? params.readResult.data as Record<string, unknown>
: {};
const requestedLineLimit = clampNumber(data.requestedLineLimit ?? params.lineLimit, DEFAULT_AI_UPSTREAM_LOG_LIMIT, 1, MAX_AI_UPSTREAM_LOG_LIMIT);
const requestLimit = clampNumber(params.requestLimit, DEFAULT_AI_UPSTREAM_REQUEST_LIMIT, 1, MAX_AI_UPSTREAM_REQUEST_LIMIT);
const bodyPreviewLimit = clampNumber(params.bodyPreviewLimit, DEFAULT_AI_UPSTREAM_BODY_LIMIT, 200, MAX_AI_UPSTREAM_BODY_LIMIT);
const includeBody = params.includeBody !== false;
const includeLines = params.includeLines === true;
const lines = normalizeLogLines(data.lines);
const parsedEvents = lines
.map((line) => parseAIUpstreamLogEvent(line, bodyPreviewLimit))
.filter((event): event is AIUpstreamLogEvent => Boolean(event))
.filter((event) => matchesFilter(event, params));
const eventBreakdown = {
started: parsedEvents.filter((event) => event.type === 'started').length,
completed: parsedEvents.filter((event) => event.type === 'completed').length,
failed: parsedEvents.filter((event) => event.type === 'failed').length,
other: parsedEvents.filter((event) => event.type === 'other').length,
};
const providers = Array.from(new Set(parsedEvents.map((event) => event.provider).filter(Boolean)));
const requestIds = Array.from(new Set(parsedEvents.map((event) => event.requestId).filter(Boolean)));
const requests = summarizeAIUpstreamRequests(parsedEvents, includeBody, requestLimit);
return {
logPath: String(data.logPath || ''),
keyword: String(data.keyword || params.requestId || params.provider || params.keyword || '').trim(),
providerFilter: String(params.provider || '').trim(),
requestIdFilter: String(params.requestId || '').trim(),
requestedLineLimit,
returnedLineCount: lines.length,
upstreamEventCount: parsedEvents.length,
requestCount: requests.length,
eventBreakdown,
providers,
requestIds,
requests,
lines: includeLines ? parsedEvents.map((event) => event.line) : undefined,
message: parsedEvents.length > 0
? ''
: '最近日志里没有找到 AI 上游请求记录;请先发送一次 AI 消息,或扩大 lineLimit 后重试。',
nextActions: parsedEvents.length > 0
? [
'如需核对完整入参,先用 requestId 精确过滤,再查看 bodyPreview 是否已被截断。',
'如果只有开始没有完成/失败,继续查看 inspect_app_logs 或扩大 lineLimit 排查请求是否超时。',
]
: [
'确认当前构建已包含 AI 上游请求日志能力。',
'发送一次 AI 聊天消息后再调用本工具。',
'如果仍没有记录,调用 inspect_app_logs 读取最近 WARN/ERROR 原文。',
],
};
};

View File

@@ -148,6 +148,35 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_ai_upstream_logs",
icon: "📡",
desc: "查看 AI 上游请求入参与状态",
detail:
"从 gonavi.log 读取最近的 AI 上游请求开始/完成/失败记录,按 provider、requestId 或关键词过滤,返回请求体 body 预览、endpoint、状态码、耗时和错误摘要。适合用户想核对发给上游模型的真实入参、排查请求参数兼容、确认脱敏日志是否写入时先调用。",
params: "provider?, requestId?, keyword?, lineLimit?(默认 160), requestLimit?(默认 12), includeBody?(默认 true), includeLines?(默认 false)",
tool: {
type: "function",
function: {
name: "inspect_ai_upstream_logs",
description:
"读取 GoNavi 应用日志中的 AI 上游请求记录,返回 requestId、provider、method、endpoint、请求 body 预览、状态码、耗时和错误摘要。适用于用户提到 AI 请求入参、上游请求体、requestId、provider 请求参数、模型接口报错、或需要核对刚才发给上游模型的真实 payload 时,先读取该工具,不要只凭界面响应推断。",
parameters: {
type: "object",
properties: {
provider: { type: "string", description: "可选,只看某个供应商,例如 openai、anthropic、gemini大小写不敏感" },
requestId: { type: "string", description: "可选,按日志里的 requestId 精确过滤,适合从错误日志继续追踪同一次请求" },
keyword: { type: "string", description: "可选,在 requestId、provider、endpoint、bodyPreview 或 error 中继续过滤,例如模型名、接口路径、参数名" },
lineLimit: { type: "number", description: "可选,最多读取多少行日志尾部,默认 160最大 300" },
requestLimit: { type: "number", description: "可选,最多返回多少个请求摘要,默认 12最大 40" },
includeBody: { type: "boolean", description: "可选,是否返回已脱敏的请求 body 预览,默认 true只看状态时可设为 false" },
includeLines: { type: "boolean", description: "可选,是否附带脱敏后的原始日志行,默认 false需要引用原文时再开启" },
bodyPreviewLimit: { type: "number", description: "可选,单个 body 预览最大字符数,默认 6000最大 12000" },
},
},
},
},
},
{
name: "inspect_ai_tool_catalog",
icon: "🧭",

View File

@@ -79,6 +79,11 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [
steps: 'inspect_ai_chat_readiness -> inspect_ai_providers',
description: '适合先确认当前聊天输入区到底缺什么前置条件,例如没选活动供应商、缺密钥、缺接口地址、没选模型,避免只凭界面现象猜测。',
},
{
title: '追踪 AI 上游请求',
steps: 'inspect_ai_upstream_logs -> inspect_ai_providers / inspect_ai_message_flow',
description: '适合用户想看发给上游模型的真实入参、requestId、状态码、耗时或请求体预览时先读脱敏后的 gonavi.log 请求记录,再结合供应商配置和当前消息流继续排查。',
},
{
title: '排查 MCP 接入状态',
steps: 'inspect_mcp_setup -> inspect_ai_runtime',

View File

@@ -12,6 +12,10 @@ describe('ai composer notice helpers', () => {
tone: 'warning',
title: '还没有可用供应商',
description: '先在 AI 设置里添加并启用一个模型供应商。',
action: {
key: 'open-settings',
label: '打开 AI 设置',
},
});
});
@@ -20,6 +24,10 @@ describe('ai composer notice helpers', () => {
tone: 'warning',
title: '先选择一个模型',
description: '打开下方模型下拉并选择模型;如果列表为空,请检查供应商入口和 API Key。',
action: {
key: 'reload-models',
label: '重新加载模型',
},
});
});
@@ -28,6 +36,10 @@ describe('ai composer notice helpers', () => {
tone: 'error',
title: '模型列表加载失败',
description: '当前接口未返回可用模型',
action: {
key: 'reload-models',
label: '重新加载模型',
},
});
});
});

View File

@@ -84,6 +84,14 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('当前 AI 聊天输入区');
});
it('registers the ai-upstream-log inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_upstream_logs');
expect(info).toBeTruthy();
expect(info?.desc).toContain('上游请求入参');
expect(info?.tool.function.description).toContain('请求 body 预览');
expect(info?.tool.function.parameters?.properties?.requestId?.description).toContain('requestId');
});
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();
@@ -234,6 +242,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_upstream_logs')).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);