mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 23:23:52 +08:00
✨ feat(ai-tools): 新增当前连接探针并拆分 AIChatPanel 运行时模块
This commit is contained in:
28
frontend/src/utils/aiChatRuntime.test.ts
Normal file
28
frontend/src/utils/aiChatRuntime.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { compressContextIfNeeded, getDynamicMaxContextChars, sanitizeErrorMsg } from './aiChatRuntime';
|
||||
|
||||
describe('aiChatRuntime', () => {
|
||||
it('maps modern model families to practical context windows', () => {
|
||||
expect(getDynamicMaxContextChars('gemini-2.5-pro')).toBe(5000000);
|
||||
expect(getDynamicMaxContextChars('gpt-5')).toBe(1000000);
|
||||
expect(getDynamicMaxContextChars('claude-4-sonnet')).toBe(1000000);
|
||||
expect(getDynamicMaxContextChars('gpt-4o')).toBe(128000);
|
||||
expect(getDynamicMaxContextChars()).toBe(258000);
|
||||
});
|
||||
|
||||
it('sanitizes html gateway errors and truncates oversized plain text errors', () => {
|
||||
expect(sanitizeErrorMsg('<html><head><title>502 Bad Gateway</title></head></html>')).toBe('HTTP 502: 502 Bad Gateway');
|
||||
expect(sanitizeErrorMsg('x'.repeat(320))).toBe(`${'x'.repeat(280)}...(已截断)`);
|
||||
expect(sanitizeErrorMsg('permission denied')).toBe('permission denied');
|
||||
});
|
||||
|
||||
it('skips compression when the payload is still within the configured limit', async () => {
|
||||
const result = await compressContextIfNeeded('session-1', [
|
||||
{ role: 'user', content: 'short prompt' },
|
||||
{ role: 'assistant', content: 'short answer' },
|
||||
], 1000);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
90
frontend/src/utils/aiChatRuntime.ts
Normal file
90
frontend/src/utils/aiChatRuntime.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useStore } from '../store';
|
||||
|
||||
const genCompressionMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
export const getDynamicMaxContextChars = (modelName?: string) => {
|
||||
if (!modelName) return 258000;
|
||||
const lower = modelName.toLowerCase();
|
||||
|
||||
if (lower.includes('gemini-1.5-pro') || lower.includes('gemini-2') || lower.includes('gemini-3')) {
|
||||
return 5000000;
|
||||
}
|
||||
if (lower.includes('glm-5') || lower.includes('claude-4') || lower.includes('claude-3.7') || lower.includes('gpt-5') || lower.includes('qwen3') || lower.includes('deepseek-v4')) {
|
||||
return 1000000;
|
||||
}
|
||||
if (lower.includes('claude-3-opus') || lower.includes('claude-3.5') || lower.includes('glm-4-long') || lower.includes('qwen-long')) {
|
||||
return 1000000;
|
||||
}
|
||||
if (lower.includes('claude') || lower.includes('deepseek') || lower.includes('gpt-4.5') || lower.includes('qwen2.5')) {
|
||||
return 258000;
|
||||
}
|
||||
if (lower.includes('gpt-4') || lower.includes('gpt-4o') || lower.includes('glm') || lower.includes('z-ai')) {
|
||||
return 128000;
|
||||
}
|
||||
if (lower.includes('qwen')) {
|
||||
return 128000;
|
||||
}
|
||||
return 258000;
|
||||
};
|
||||
|
||||
export const compressContextIfNeeded = async (sid: string, messagesPayload: any[], maxLimit: number) => {
|
||||
try {
|
||||
const chars = messagesPayload.reduce((sum, message) =>
|
||||
sum + (message.content?.length || 0) + (message.reasoning_content?.length || 0) + JSON.stringify(message.tool_calls || []).length, 0);
|
||||
if (chars < maxLimit) return null;
|
||||
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service?.AIChatSend) return null;
|
||||
|
||||
const connectingMsgId = genCompressionMessageId();
|
||||
useStore.getState().addAIChatMessage(sid, {
|
||||
id: connectingMsgId,
|
||||
role: 'assistant',
|
||||
phase: 'connecting',
|
||||
content: '⚙️ 对话已超载,正在启动记忆压缩...',
|
||||
timestamp: Date.now(),
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const summaryPrompt = `这是一段超长对话的历史记录。为了释放上下文空间同时保留你的记忆核心,请你仔细阅读并以“技术事实、已探索出的数据结构状态、用户的中心诉求、当前进展”为准则,进行高度浓缩的结构化总结。
|
||||
注意:
|
||||
1. 客观准确,不能遗漏关键业务逻辑或探索出的表名/字段。
|
||||
2. 剔除无效执行过程、客套话、JSON返回值本身。
|
||||
3. 请控制在 1000-2000 字左右,输出纯干货 Markdown。
|
||||
4. 开头直接输出总结,不要带寒暄。`;
|
||||
|
||||
const result = await Service.AIChatSend([
|
||||
{ role: 'system', content: summaryPrompt },
|
||||
...messagesPayload,
|
||||
]);
|
||||
|
||||
if (result?.success && result.content) {
|
||||
useStore.getState().deleteAIChatMessage(sid, connectingMsgId);
|
||||
return result.content;
|
||||
}
|
||||
|
||||
useStore.getState().updateAIChatMessage(sid, connectingMsgId, {
|
||||
loading: false,
|
||||
phase: 'idle',
|
||||
content: '❌ 记忆压缩失败,将尝试原样接续...',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Compression exception:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const sanitizeErrorMsg = (raw: string): string => {
|
||||
if (!raw || typeof raw !== 'string') return '未知错误';
|
||||
if (raw.includes('<html') || raw.includes('<!DOCTYPE') || raw.includes('<head')) {
|
||||
const titleMatch = raw.match(/<title[^>]*>([^<]+)<\/title>/i);
|
||||
const codeMatch = raw.match(/\b(4\d{2}|5\d{2})\b/);
|
||||
const title = titleMatch?.[1]?.trim();
|
||||
const code = codeMatch?.[1];
|
||||
if (title) return code ? `HTTP ${code}: ${title}` : title;
|
||||
if (code) return `HTTP ${code} 服务端错误`;
|
||||
return '服务端返回了异常 HTML 响应(可能是网关超时或服务不可用)';
|
||||
}
|
||||
if (raw.length > 300) return `${raw.substring(0, 280)}...(已截断)`;
|
||||
return raw;
|
||||
};
|
||||
32
frontend/src/utils/aiToolRegistry.test.ts
Normal file
32
frontend/src/utils/aiToolRegistry.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BUILTIN_AI_TOOL_INFO, buildAvailableAIChatTools } from './aiToolRegistry';
|
||||
|
||||
describe('aiToolRegistry', () => {
|
||||
it('registers the current-connection inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_current_connection');
|
||||
expect(info).toBeTruthy();
|
||||
expect(info?.desc).toContain('当前活动连接');
|
||||
expect(info?.tool.function.description).toContain('SSH/代理/HTTP 隧道状态');
|
||||
});
|
||||
|
||||
it('keeps builtin tools and MCP tools in the unified runtime tool chain', () => {
|
||||
const tools = buildAvailableAIChatTools([{
|
||||
alias: 'custom_probe',
|
||||
originalName: 'custom_probe',
|
||||
serverId: 'server-1',
|
||||
serverName: 'demo',
|
||||
title: '自定义探针',
|
||||
description: '读取额外环境信息',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'custom_probe')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -332,6 +332,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_current_connection",
|
||||
icon: "🛰️",
|
||||
desc: "查看当前活动连接/数据源摘要",
|
||||
detail:
|
||||
"返回当前活动连接的类型、地址、端口、当前数据库、是否启用 SSH/代理/HTTP 隧道,以及当前活动页签绑定的表信息。适合用户问“我现在连的是哪个库”“这个连接走没走 SSH”“当前数据源是什么类型”时先读取真实连接状态。",
|
||||
params: "无参数",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_current_connection",
|
||||
description:
|
||||
"读取当前活动连接或当前页签对应数据源的真实摘要,包括连接类型、地址、端口、当前数据库、SSH/代理/HTTP 隧道状态,以及当前页签绑定的表上下文。适用于用户提到当前连接、当前数据源、当前库地址、是否走 SSH、当前连的是哪种数据库时,先读取真实界面上下文,避免模型猜测。",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_active_tab",
|
||||
icon: "📍",
|
||||
|
||||
Reference in New Issue
Block a user