From 29ee21e9d719e426eaf085abbd31b8acb2dde333 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 3 Jul 2026 20:44:33 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(query-editor):=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20SQL=20=E7=BC=96=E8=BE=91=E5=99=A8=20AI=20=E5=86=85?= =?UTF-8?q?=E8=81=94=E8=A1=A5=E5=85=A8=E4=B8=8E=E7=8B=AC=E7=AB=8B=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增独立内联补全模型配置与服务端透传 - 优化 SQL 编辑器 Alt+\\ 触发、ghost 延续与对象位补全 - 引入基于已存查询和执行日志的 SQL 记忆补全 - 补充前后端本地化、快捷键与补全回归测试 --- frontend/package.json.md5 | 2 +- frontend/src/App.css | 23 + frontend/src/components/AISettingsModal.tsx | 13 +- .../QueryEditor.external-sql-save.test.tsx | 973 +++++++++++++- frontend/src/components/QueryEditor.tsx | 1167 +++++++++++++++-- .../QueryEditorToolbar.ai-trigger.test.tsx | 17 + .../src/components/QueryEditorToolbar.tsx | 55 +- .../ai/AISettingsProvidersSection.test.tsx | 6 + .../ai/AISettingsProvidersSection.tsx | 49 + .../queryEditor/QueryEditorAiAssist.test.ts | 656 ++++++++- .../queryEditor/QueryEditorAiAssist.ts | 1039 ++++++++++++++- frontend/src/i18n/catalog.test.ts | 2 + frontend/src/main.tsx | 61 +- frontend/src/types.ts | 1 + frontend/src/utils/shortcuts.test.ts | 69 + frontend/src/utils/shortcuts.ts | 127 +- frontend/wailsjs/go/aiservice/Service.d.ts | 2 + frontend/wailsjs/go/aiservice/Service.js | 4 + frontend/wailsjs/go/app/App.d.ts | 6 + frontend/wailsjs/go/app/App.js | 12 + frontend/wailsjs/go/models.ts | 66 +- internal/ai/provider/openai.go | 2 + internal/ai/provider/openai_test.go | 46 + internal/ai/service/service.go | 101 +- internal/ai/service/service_test.go | 39 + internal/ai/types.go | 34 +- shared/i18n/de-DE.json | 6 + shared/i18n/en-US.json | 6 + shared/i18n/ja-JP.json | 6 + shared/i18n/ru-RU.json | 6 + shared/i18n/zh-CN.json | 6 + shared/i18n/zh-TW.json | 6 + 32 files changed, 4399 insertions(+), 209 deletions(-) create mode 100644 frontend/src/components/QueryEditorToolbar.ai-trigger.test.tsx diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 50f303f7..72cd1564 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -84ec3a6d42105c92f224232f0d83a33b \ No newline at end of file +1d8f9adbde8018f90d013cc740e0405b \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index 51cc7c71..ae9b9451 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -715,6 +715,29 @@ body[data-theme='dark'] .gonavi-query-editor-db-token { color: #c4b5fd; } +.gonavi-query-editor-ai-inline-ghost { + color: rgba(100, 116, 139, 0.7); + font-style: italic; + pointer-events: none; +} + +.gonavi-query-editor-ai-inline-ghost-overlay { + color: rgba(100, 116, 139, 0.7); + font-style: italic; + pointer-events: none; + position: absolute; + white-space: pre; + z-index: 6; +} + +body[data-theme='dark'] .gonavi-query-editor-ai-inline-ghost { + color: rgba(203, 213, 225, 0.56); +} + +body[data-theme='dark'] .gonavi-query-editor-ai-inline-ghost-overlay { + color: rgba(203, 213, 225, 0.56); +} + /* Legacy sidebar resize bounds — mirror v2 .gn-v2-app-sider so Ant Design inline width locks do not collapse drag range. */ body[data-ui-version="legacy"] .ant-layout-sider { min-width: 232px !important; diff --git a/frontend/src/components/AISettingsModal.tsx b/frontend/src/components/AISettingsModal.tsx index 888bf208..be0a6b2e 100644 --- a/frontend/src/components/AISettingsModal.tsx +++ b/frontend/src/components/AISettingsModal.tsx @@ -374,6 +374,7 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo valuesModel: values.model, customModels: values.models, }); + const inlineCompletionModel = String(values.inlineCompletionModel || '').trim(); // 内置供应商自动使用 preset label 作为名称 const finalName = isCustomLike ? (values.name || localizedPreset.label) : localizedPreset.label; @@ -398,6 +399,7 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo apiKey: secretDraft.apiKey, hasSecret: secretDraft.hasSecret, model: finalModel, + inlineCompletionModel, models: resolvedModels, baseUrl: finalBaseUrl, apiFormat: resolvedTransport.apiFormat, @@ -669,6 +671,7 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo hasSecret: secretDraft.hasSecret, baseUrl: finalBaseUrl, model: finalModel, + inlineCompletionModel: String(values.inlineCompletionModel || '').trim(), models: resolvedModels, maxTokens: Number(values.maxTokens) || 4096, temperature: Number(values.temperature) ?? 0.7, @@ -687,12 +690,20 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo presetFixedApiFormat: preset.fixedApiFormat, valuesApiFormat: form.getFieldValue('apiFormat'), }); + const { model: presetModel, models: presetModels } = resolvePresetModelSelection({ + presetKey, + presetDefaultModel: preset.defaultModel, + presetModels: preset.models, + customModels: preset.models, + }); form.setFieldsValue({ presetKey, type: resolvedTransport.type, apiFormat: resolvedTransport.apiFormat || 'openai', baseUrl: preset.defaultBaseUrl, - model: preset.defaultModel, + model: presetModel, + models: presetModels, + inlineCompletionModel: '', }); }; diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 2f9dbd35..9df1edbd 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -186,7 +186,13 @@ const editorState = vi.hoisted(() => { const state = { value: '', editor: null as any, - domNode: { style: { cursor: '' }, addEventListener: vi.fn(), removeEventListener: vi.fn() }, + domNode: { + style: { cursor: '' }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + appendChild: vi.fn(), + removeChild: vi.fn(), + }, position: { lineNumber: 1, column: 1 }, selection: null as any, providers: [] as any[], @@ -194,6 +200,7 @@ const editorState = vi.hoisted(() => { contentChangeListeners: [] as Array<() => void>, cursorPositionListeners: [] as Array<(event: any) => void>, modelContentListeners: [] as Array<(event: any) => void>, + keyDownListeners: [] as Array<(event: any) => void>, mouseMoveListeners: [] as Array<(event: any) => void>, mouseDownListeners: [] as Array<(event: any) => void>, mouseLeaveListeners: [] as Array<() => void>, @@ -277,6 +284,7 @@ const editorState = vi.hoisted(() => { }); }), addAction: vi.fn(), + addCommand: vi.fn(), onDidChangeModelContent: vi.fn((listener: (event?: any) => void) => { state.contentChangeListeners.push(listener); state.modelContentListeners.push(listener); @@ -286,6 +294,10 @@ const editorState = vi.hoisted(() => { state.cursorPositionListeners.push(listener); return { dispose: vi.fn() }; }), + onKeyDown: vi.fn((listener: (event: any) => void) => { + state.keyDownListeners.push(listener); + return { dispose: vi.fn() }; + }), onMouseMove: vi.fn((listener: (event: any) => void) => { state.mouseMoveListeners.push(listener); return { dispose: vi.fn() }; @@ -298,6 +310,8 @@ const editorState = vi.hoisted(() => { state.mouseLeaveListeners.push(listener); return { dispose: vi.fn() }; }), + onDidScrollChange: vi.fn(() => ({ dispose: vi.fn() })), + onDidLayoutChange: vi.fn(() => ({ dispose: vi.fn() })), deltaDecorations: vi.fn((oldDecorations: string[], newDecorations: any[]) => { state.decorationIds = newDecorations.map((_: any, index: number) => `decoration-${index + 1}`); return state.decorationIds; @@ -311,6 +325,13 @@ const editorState = vi.hoisted(() => { layout: vi.fn(), focus: vi.fn(), trigger: vi.fn(), + createContextKey: vi.fn((_key: string, initialValue: boolean) => ({ + set: vi.fn(), + get: vi.fn(() => initialValue), + reset: vi.fn(), + })), + getScrolledVisiblePosition: vi.fn(() => ({ left: 0, top: 0, height: 20 })), + getOption: vi.fn(() => null), }; return state; }); @@ -348,7 +369,7 @@ vi.mock('@monaco-editor/react', () => ({ const mountEditor = () => onMount?.(editorState.editor, { editor: { setTheme: vi.fn() }, KeyMod: { CtrlCmd: 2048, WinCtrl: 256, Alt: 512, Shift: 1024 }, - KeyCode: { KeyD: 68, KeyE: 69, KeyF: 70, KeyM: 77, KeyQ: 81, KeyS: 83 }, + KeyCode: { KeyD: 68, KeyE: 69, KeyF: 70, KeyM: 77, KeyQ: 81, KeyS: 83, RightArrow: 39 }, languages: { CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 }, CompletionItemInsertTextRule: { InsertAsSnippet: 1 }, @@ -443,6 +464,7 @@ vi.mock('@ant-design/icons', () => { RobotOutlined: Icon, SearchOutlined: Icon, DatabaseOutlined: Icon, + DownOutlined: Icon, EyeOutlined: Icon, EyeInvisibleOutlined: Icon, }; @@ -478,6 +500,10 @@ vi.mock('antd', () => { ); const Empty = ({ description }: { description?: React.ReactNode }) =>
{description}
; (Empty as any).PRESENTED_IMAGE_SIMPLE = 'simple'; + const Input: any = ({ value, onChange, placeholder }: any) => ; + Input.TextArea = ({ value, onChange, placeholder, disabled }: any) => ( +