mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-01 07:39:37 +08:00
✨ feat(ai): 发布全新 AI Copilot 助手面板与工作区智能打通
- 核心架构:新增独立 AI 会话中枢,集成主流大模型生态(含私有部署中继版)的无感衔接发问 - 智能诊断:打破信息孤岛,大模型可通过关联工作区实时数据表 DDL 和错误栈,充当专属 DBA 排错及代码编写 - 视觉与多模态:支持极简发图读图交互体验,智能补全模型所需的缺省预警 Prompt,并兼容不规范中转端点图文并茂 - UI 与性能:重构聊天浮层挂靠逻辑与渲染阻断,应对长时间巨量问答引发的卡段内存泄漏,会话自动保存归档
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user