feat(ai): 发布全新 AI Copilot 助手面板与工作区智能打通

- 核心架构:新增独立 AI 会话中枢,集成主流大模型生态(含私有部署中继版)的无感衔接发问
- 智能诊断:打破信息孤岛,大模型可通过关联工作区实时数据表 DDL 和错误栈,充当专属 DBA 排错及代码编写
- 视觉与多模态:支持极简发图读图交互体验,智能补全模型所需的缺省预警 Prompt,并兼容不规范中转端点图文并茂
- UI 与性能:重构聊天浮层挂靠逻辑与渲染阻断,应对长时间巨量问答引发的卡段内存泄漏,会话自动保存归档
This commit is contained in:
Syngnat
2026-03-26 16:02:08 +08:00
parent 82369b4070
commit 98e9e5686d
27 changed files with 4902 additions and 745 deletions

View File

@@ -480,7 +480,9 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
contextMenuOrder: 1,
run: (ed: any) => {
const selection = ed.getModel()?.getValueInRange(ed.getSelection());
let prompt = action.prompt;
const conn = connectionsRef.current.find(c => c.id === currentConnectionIdRef.current);
const ctxText = conn ? `【上下文环境:${conn.config?.type || '数据库'} "${conn.name}", 当前库选定为 "${currentDbRef.current || '默认'}"】\n` : '';
let prompt = ctxText + action.prompt;
if (action.useSelection && selection) {
prompt = prompt.replace('{SQL}', selection);
}
@@ -853,7 +855,92 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
return { suggestions };
}
});
// 注册 / 斜杠命令 AI 快捷补全
const slashCmdDefs = [
{ cmd: '/query', label: '🔍 自然语言查询', desc: '用中文描述你想查什么', prompt: '帮我写一条 SQL 查询:' },
{ cmd: '/sql', label: '📝 生成 SQL', desc: '描述需求自动生成语句', prompt: '请根据以下需求生成 SQL' },
{ cmd: '/explain', label: '💡 解释 SQL', desc: '解释选中 SQL 的逻辑', prompt: '请解释以下 SQL 的执行逻辑和每一步的作用:\n```sql\n{SQL}\n```', useSelection: true },
{ cmd: '/optimize', label: '⚡ 优化分析', desc: '分析 SQL 性能瓶颈', prompt: '请分析以下 SQL 的性能问题,并给出优化后的版本:\n```sql\n{SQL}\n```', useSelection: true },
{ cmd: '/schema', label: '🏗️ 表设计评审', desc: '评审表结构设计质量', prompt: '请全面评审当前关联表的设计,包括字段类型、范式、索引策略等方面的改进建议:' },
{ cmd: '/index', label: '📊 索引建议', desc: '推荐最优索引方案', prompt: '请基于当前表结构和常见查询场景,推荐最优的索引方案并给出建表语句:' },
{ cmd: '/diff', label: '🔄 表对比', desc: '对比两表差异生成变更', prompt: '请对比以下两张表的结构差异,并生成从旧版本迁移到新版本的 ALTER 语句:' },
{ cmd: '/mock', label: '🎲 造测试数据', desc: '生成 INSERT 测试数据', prompt: '请为当前关联的表生成 10 条符合业务语义的测试数据 INSERT 语句:' },
];
// 全局变量存储命令定义,供 onDidChangeModelContent 使用
(window as any).__gonaviSlashCmdDefs = slashCmdDefs;
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['/'],
provideCompletionItems: (model: any, position: any) => {
const lineContent = model.getLineContent(position.lineNumber);
const textBefore = lineContent.substring(0, position.column - 1).trimStart();
if (!textBefore.startsWith('/')) {
return { suggestions: [] };
}
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: position.column - textBefore.length,
endColumn: position.column,
};
return {
suggestions: slashCmdDefs.map((c, i) => ({
label: `${c.cmd} ${c.label}`,
kind: monaco.languages.CompletionItemKind.Event,
detail: c.desc,
insertText: `__AI_${c.cmd.slice(1).toUpperCase()}__`,
range,
sortText: String(i).padStart(2, '0'),
})),
};
},
});
} // end sqlCompletionRegistered guard
// 每个编辑器实例都注册内容变化监听(检测斜杠命令标记)
let _handlingSlash = false;
editor.onDidChangeModelContent(() => {
if (_handlingSlash) return;
const model = editor.getModel();
if (!model) return;
const content = model.getValue();
const markerMatch = content.match(/__AI_(\w+)__/);
if (!markerMatch) return;
const cmdKey = markerMatch[1].toLowerCase();
const defs = (window as any).__gonaviSlashCmdDefs || [];
const cmdDef = defs.find((c: any) => c.cmd === `/${cmdKey}`);
if (!cmdDef) return;
// 清除标记文本(带递归保护)
_handlingSlash = true;
const fullText = model.getValue();
const newText = fullText.replace(markerMatch[0], '').replace(/^\s*\n/, '');
model.setValue(newText);
_handlingSlash = false;
// 组装 prompt
const conn = connectionsRef.current.find(c => c.id === currentConnectionIdRef.current);
const ctxText = conn ? `【上下文环境:${conn.config?.type || '数据库'} "${conn.name}", 当前库选定为 "${currentDbRef.current || '默认'}"】\n` : '';
let finalPrompt = ctxText + cmdDef.prompt;
if (cmdDef.useSelection) {
const sel = editor.getSelection();
const selText = sel ? model.getValueInRange(sel) : '';
finalPrompt = finalPrompt.replace('{SQL}', selText || getCurrentQuery());
}
// 打开 AI 面板并注入 prompt
const store = useStore.getState();
if (!store.aiPanelVisible) {
store.setAIPanelVisible(true);
}
setTimeout(() => {
window.dispatchEvent(new CustomEvent('gonavi:ai:inject-prompt', { detail: { prompt: finalPrompt } }));
}, store.aiPanelVisible ? 0 : 350);
});
};
const handleFormat = () => {
@@ -870,11 +957,14 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
const selection = editor?.getModel()?.getValueInRange(editor.getSelection()) || '';
const fullSQL = getCurrentQuery();
const conn = connections.find(c => c.id === currentConnectionId);
const ctxText = conn ? `【上下文环境:${conn.config?.type || '数据库'} "${conn.name}", 当前库选定为 "${currentDb || '默认'}"】\n` : '';
const prompts: Record<string, string> = {
generate: '请根据当前数据库表结构生成查询语句:',
explain: `请解释以下 SQL 语句的执行逻辑:\n\`\`\`sql\n${selection || fullSQL}\n\`\`\``,
optimize: `请分析以下 SQL 语句的性能并给出优化建议:\n\`\`\`sql\n${selection || fullSQL}\n\`\`\``,
schema: '请分析当前数据库的表结构并给出优化建议。',
generate: `${ctxText}请根据当前数据库表结构生成查询语句:`,
explain: `${ctxText}请解释以下 SQL 语句的执行逻辑:\n\`\`\`sql\n${selection || fullSQL}\n\`\`\``,
optimize: `${ctxText}请分析以下 SQL 语句的性能并给出优化建议:\n\`\`\`sql\n${selection || fullSQL}\n\`\`\``,
schema: `${ctxText}请针对当前数据库的表结构进行系统分析,并给出性能和设计上的优化建议。`,
};
const store = useStore.getState();
@@ -1932,41 +2022,73 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
};
}, [activeTabId, tab.id, handleRun]);
// 监听并处理外部注入的 SQL 代码 (如 AI 面板)
// 监听由 TabManager 分发的专用注入事件
useEffect(() => {
const handleInsertSql = (e: CustomEvent) => {
if (activeTabId !== tab.id || !e.detail?.sql) return;
const sqlText = e.detail.sql;
const handleInsertSql = (e: any) => {
if (e.detail?.tabId !== tab.id || !e.detail?.sql) return;
const { sql: sqlText, connectionId, dbName } = e.detail;
// 同步更新 ref防止异步 fetchDbs 竞态覆盖正确的 dbName
if (connectionId && connectionId !== currentConnectionId) {
if (dbName) {
currentDbRef.current = dbName;
setCurrentDb(dbName);
}
setCurrentConnectionId(connectionId);
} else if (dbName && dbName !== currentDb) {
currentDbRef.current = dbName;
setCurrentDb(dbName);
}
const editor = editorRef.current;
if (editor && (window as any).monaco) {
const position = editor.getPosition();
const monaco = monacoRef.current;
if (editor && monaco) {
let position = editor.getPosition();
const model = editor.getModel();
if (!position && model) {
const lineCount = model.getLineCount();
const maxCol = model.getLineMaxColumn(lineCount);
position = new monaco.Position(lineCount, maxCol);
}
if (position) {
const mText = (sqlText.endsWith('\n') ? sqlText : sqlText + '\n');
const startRange = new (window as any).monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column);
const startRange = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column);
editor.executeEdits('ai-insert', [{
range: startRange,
text: '\n' + mText,
text: (position.column > 1 ? '\n' : '') + mText,
forceMoveMarkers: true
}]);
// 定位并滚动到可见区域
const targetLine = position.lineNumber + (position.column > 1 ? 1 : 0);
editor.revealLineInCenterIfOutsideViewport(targetLine);
editor.setPosition({ lineNumber: targetLine + mText.split('\n').length - 1, column: 1 });
editor.focus();
if (!e.detail.runImmediately) {
message.success('代码已在当前光标处成功插入');
}
if (e.detail.runImmediately) {
const endPosition = editor.getPosition();
editor.setSelection(new (window as any).monaco.Range(
position.lineNumber + 1, 1,
editor.setSelection(new monaco.Range(
targetLine, 1,
endPosition.lineNumber, endPosition.column
));
setTimeout(() => handleRun(), 50);
// 🔧 延迟 500ms 等待连接/数据库切换的 setState 生效后再执行
setTimeout(() => handleRun(), 500);
}
}
} else {
setQuery((prev: string) => prev ? prev + '\n' + sqlText : sqlText);
message.success('代码已追加');
}
};
window.addEventListener('gonavi:insert-sql', handleInsertSql as EventListener);
return () => window.removeEventListener('gonavi:insert-sql', handleInsertSql as EventListener);
}, [activeTabId, tab.id, handleRun]);
window.addEventListener('gonavi:insert-sql-to-tab', handleInsertSql as EventListener);
return () => window.removeEventListener('gonavi:insert-sql-to-tab', handleInsertSql as EventListener);
}, [tab.id, handleRun]);
const resolveDefaultQueryName = () => {
const rawTitle = String(tab.title || '').trim();