From 781a80e03fa94a4954ea82727925f17bd38ef7c0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 12 Jun 2026 08:24:13 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=83=AD=E7=82=B9=E6=8B=86=E5=88=86=E8=AF=8A?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueryEditor.external-sql-save.test.tsx | 23 +- frontend/src/components/QueryEditor.tsx | 174 ++++----------- .../src/components/QueryEditorToolbar.tsx | 204 ++++++++++++++++++ .../ai/aiCodebaseHotspotInsights.ts | 91 +++++++- ...LocalToolExecutor.codebaseHotspots.test.ts | 5 + .../aiBuiltinInspectionDiagnosticsToolInfo.ts | 4 +- 6 files changed, 352 insertions(+), 149 deletions(-) create mode 100644 frontend/src/components/QueryEditorToolbar.tsx diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 5cf99bf..72fce89 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -349,6 +349,8 @@ vi.mock('antd', () => { ); Button.Group = ({ children }: any) =>
{children}
; + const Space: any = ({ children }: any) =>
{children}
; + Space.Compact = ({ children, className }: any) =>
{children}
; const Form: any = ({ children }: any) =>
{children}
; Form.Item = ({ children }: any) => <>{children}; @@ -356,6 +358,7 @@ vi.mock('antd', () => { return { Button, + Space, message: messageApi, Modal: ({ children, open, onOk, okText = '确认' }: any) => (open ? (
@@ -3707,16 +3710,18 @@ describe('QueryEditor external SQL save', () => { it('keeps the v2 query editor toolbar grouped and compact', () => { const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); + const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8'); const transactionSettingsSource = readFileSync(new URL('./QueryEditorTransactionSettings.tsx', import.meta.url), 'utf8'); const transactionToolbarSource = readFileSync(new URL('./QueryEditorTransactionToolbar.tsx', import.meta.url), 'utf8'); const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); - expect(source).toContain('gn-v2-query-toolbar-selects'); - expect(source).toContain('gn-v2-query-toolbar-actions'); - expect(source).toContain('gn-v2-query-toolbar-connection-select'); - expect(source).toContain('gn-v2-query-toolbar-database-select'); - expect(source).toContain('gn-v2-query-toolbar-max-rows-select'); - expect(source).toContain('QueryEditorTransactionSettings'); + expect(source).toContain('QueryEditorToolbar'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-selects'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-actions'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-connection-select'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-database-select'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-max-rows-select'); + expect(toolbarSource).toContain('QueryEditorTransactionSettings'); expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-mode-select'); expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-delay-select'); expect(transactionSettingsSource).toContain('参考 DBeaver'); @@ -3729,10 +3734,10 @@ describe('QueryEditor external SQL save', () => { expect(transactionToolbarSource).toContain('未提交 ${statementCount} 条变更语句'); expect(transactionToolbarSource).toContain('事务执行成功${pendingCountText},正在自动提交'); expect(transactionToolbarSource).toContain('onFinish'); - expect(source).toContain('gn-v2-query-toolbar-action-group'); + expect(toolbarSource).toContain('gn-v2-query-toolbar-action-group'); expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 160 }}'); - expect(source).toContain('style={isV2Ui ? undefined : { width: 200 }}'); - expect(source).toContain('style={isV2Ui ? undefined : { width: 170 }}'); + expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 200 }}'); + expect(toolbarSource).toContain('style={isV2Ui ? undefined : { width: 170 }}'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-actions'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index e8dcf5f..710d7b1 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import Editor, { type OnMount } from './MonacoEditor'; -import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select } from 'antd'; -import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, StopOutlined, RobotOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; +import { message, Modal, Input, Form, MenuProps } from 'antd'; import { format } from 'sql-formatter'; import { v4 as uuidv4 } from 'uuid'; import { TabData, ColumnDefinition, IndexDefinition } from '../types'; @@ -41,10 +40,9 @@ import { getColumnDefinitionName, } from '../utils/columnDefinition'; import QueryEditorResultsPanel, { type QueryEditorResultSet } from './QueryEditorResultsPanel'; -import QueryEditorTransactionSettings, { - SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS, -} from './QueryEditorTransactionSettings'; +import { SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS } from './QueryEditorTransactionSettings'; import QueryEditorTransactionToolbar from './QueryEditorTransactionToolbar'; +import QueryEditorToolbar from './QueryEditorToolbar'; import { useSqlEditorTransactionController } from './useSqlEditorTransactionController'; // HMR 重载时释放旧注册避免补全和 hover 内容重复 @@ -5134,134 +5132,44 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc className={isV2Ui ? 'gn-v2-query-editor-pane' : undefined} style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: isResultPanelVisible ? '0 0 auto' : '1 1 auto' }} > -
-
- ({ label: db, value: db }))} - showSearch - /> - - ({ label: c.name, value: c.id }))} + showSearch + /> + onMaxRowsChange(Number(val))} + options={[ + { label: '最大行数:500', value: 500 }, + { label: '最大行数:1000', value: 1000 }, + { label: '最大行数:5000', value: 5000 }, + { label: '最大行数:20000', value: 20000 }, + { label: '最大行数:不限', value: 0 }, + ]} + /> + + + {pendingTransactionToolbar} +
+
+ + + + + {loading && ( + + )} + + + + + + + + + + + + + + + + + + + , onClick: () => onAIAction('generate') }, + { key: 'ai-explain', label: '解释 SQL', icon: , onClick: () => onAIAction('explain') }, + { key: 'ai-optimize', label: '优化 SQL', icon: , onClick: () => onAIAction('optimize') }, + { type: 'divider' as const }, + { key: 'ai-schema', label: 'Schema 分析', icon: , onClick: () => onAIAction('schema') }, + ] }} placement="bottomRight"> + + +
+
+); + +export default QueryEditorToolbar; diff --git a/frontend/src/components/ai/aiCodebaseHotspotInsights.ts b/frontend/src/components/ai/aiCodebaseHotspotInsights.ts index 7aac86b..f4ba02c 100644 --- a/frontend/src/components/ai/aiCodebaseHotspotInsights.ts +++ b/frontend/src/components/ai/aiCodebaseHotspotInsights.ts @@ -3,9 +3,13 @@ export interface CodebaseHotspotEntry { lines: number; area: string; riskLevel: 'medium' | 'high' | 'critical'; + readiness: 'readyToExtract' | 'needsCharacterizationTests'; why: string; + preferredNextSlice: string; + safeSeam: string; suggestedSlices: string[]; testTargets: string[]; + verificationPlan: string[]; } export interface CodebaseHotspotSnapshotOptions { @@ -18,84 +22,156 @@ export interface CodebaseHotspotSnapshotOptions { const CODEBASE_HOTSPOT_SNAPSHOT: CodebaseHotspotEntry[] = [ { path: 'frontend/src/components/Sidebar.tsx', - lines: 8910, + lines: 8901, area: 'workspace-navigation', riskLevel: 'critical', + readiness: 'needsCharacterizationTests', why: '左侧树、命令面板、上下文菜单和连接动作集中在单文件,修改入口多且回归面大。', + preferredNextSlice: '外部 SQL 目录弹窗', + safeSeam: '优先抽出无状态弹窗/菜单配置,再处理依赖连接树状态的动作分发。', suggestedSlices: ['V2 命令面板', '外部 SQL 目录弹窗', '连接树动作', '批量操作弹窗'], testTargets: ['Sidebar.locate-toolbar.test.tsx', 'sidebarV2Utils.test.ts'], + verificationPlan: [ + 'npm --prefix frontend test -- Sidebar.locate-toolbar.test.tsx sidebarV2Utils.test.ts', + 'npm --prefix frontend run build', + '浏览器打开侧边栏,验证连接树、右键菜单和外部 SQL 目录入口可用。', + ], }, { path: 'frontend/src/components/DataGrid.tsx', lines: 8080, area: 'result-grid', riskLevel: 'critical', + readiness: 'needsCharacterizationTests', why: '结果展示、编辑、DDL、导出和列操作耦合,容易让单点修复影响查询结果区。', + preferredNextSlice: '结果导出工具栏', + safeSeam: '优先抽出纯展示工具栏和菜单项生成,不先移动数据编辑事务状态。', suggestedSlices: ['结果导出工具栏', '列头菜单', 'DDL 视图', '单元格编辑事务提示'], testTargets: ['DataGrid.layout.test.tsx', 'DataGrid.ddl.test.tsx'], + verificationPlan: [ + 'npm --prefix frontend test -- DataGrid.layout.test.tsx DataGrid.ddl.test.tsx', + 'npm --prefix frontend run build', + '浏览器执行查询并验证结果表、导出、列菜单和表格编辑入口。', + ], }, { path: 'frontend/src/components/ConnectionModal.tsx', - lines: 7462, + lines: 6811, area: 'connection-form', riskLevel: 'critical', + readiness: 'readyToExtract', why: '多数据源连接表单仍集中在一个组件,新增数据源或密钥规则时容易互相影响。', + preferredNextSlice: 'TLS 配置区', + safeSeam: '连接表单已有 presentation utils,可先抽出按数据源显示的配置分区组件。', suggestedSlices: ['SSH/代理配置区', 'TLS 配置区', 'MongoDB 配置区', 'JVM 配置区'], testTargets: ['ConnectionModal.edit-password.test.tsx', 'connectionModalPresentation.test.ts'], + verificationPlan: [ + 'npm --prefix frontend test -- ConnectionModal.edit-password.test.tsx connectionModalPresentation.test.ts', + 'npm --prefix frontend run build', + '浏览器打开新增/编辑连接弹窗,切换 MySQL、Oracle、MongoDB、Redis 表单。', + ], }, { path: 'frontend/src/components/QueryEditor.tsx', - lines: 5367, + lines: 5275, area: 'sql-editor', riskLevel: 'critical', + readiness: 'readyToExtract', why: 'SQL 编辑、执行、事务、结果区布局和快捷键状态集中,事务和结果区回归风险高。', + preferredNextSlice: '编辑器工具栏', + safeSeam: '工具栏 JSX 可通过 props 透传状态和回调,避免触碰 SQL 执行、事务和结果分页逻辑。', suggestedSlices: ['结果区工具栏', '事务状态条', '执行日志提示', '编辑器快捷键绑定'], testTargets: ['QueryEditor.result-panel.test.tsx', 'useSqlEditorTransactionController.test.ts'], + verificationPlan: [ + 'npm --prefix frontend test -- QueryEditor.external-sql-save.test.tsx useSqlEditorTransactionController.test.tsx', + 'npm --prefix frontend run build', + '浏览器打开 SQL 编辑器,验证连接/库选择、运行、保存、美化、结果区显隐和 AI 菜单。', + ], }, { path: 'frontend/src/components/TableDesigner.tsx', lines: 3549, area: 'table-designer', riskLevel: 'high', + readiness: 'needsCharacterizationTests', why: '字段编辑、索引、外键、分区和 DDL 生成集中,数据库方言差异容易扩散。', + preferredNextSlice: '字段编辑表格', + safeSeam: '先补字段类型/长度/NULL/默认值快照测试,再抽字段编辑表格。', suggestedSlices: ['字段编辑表格', '索引配置面板', '外键配置面板', '方言 DDL 预览'], testTargets: ['TableDesigner.*.test.tsx', 'tableDesignerSchemaSql.test.ts'], + verificationPlan: [ + 'npm --prefix frontend test -- tableDesignerSchemaSql.test.ts', + 'npm --prefix frontend run build', + '浏览器打开对象设计,验证字段、索引、外键和 DDL 预览。', + ], }, { path: 'frontend/src/components/RedisViewer.tsx', lines: 2120, area: 'redis-browser', riskLevel: 'high', + readiness: 'readyToExtract', why: 'Key 浏览、不同数据结构编辑、TTL、编码显示和新增弹窗集中,Redis Cluster/Sentinel 后续验证面较宽。', + preferredNextSlice: 'Key 搜索栏', + safeSeam: '先抽出搜索栏和拓扑提示,避免提前改动各数据结构编辑器。', suggestedSlices: ['Key 搜索栏', 'String/List/Set/ZSet/Hash/Stream 编辑器', '新增 Key 弹窗'], testTargets: ['redisViewerTree.test.ts', 'RedisViewer.*.test.tsx'], + verificationPlan: [ + 'npm --prefix frontend test -- redisViewerTree.test.ts', + 'npm --prefix frontend run build', + '浏览器打开 Redis 连接,验证 key 搜索、刷新、TTL 和新增入口。', + ], }, { path: 'frontend/src/components/DriverManagerModal.tsx', lines: 1729, area: 'driver-manager', riskLevel: 'high', + readiness: 'readyToExtract', why: '驱动安装、状态展示、下载和可选代理逻辑较多,适合继续拆出状态卡片和操作区。', + preferredNextSlice: '驱动状态列表', + safeSeam: '状态列表是展示型组件,可先抽出再保留安装/下载动作在父组件。', suggestedSlices: ['驱动状态列表', '安装操作区', '下载日志区'], testTargets: ['DriverManagerModal.*.test.tsx'], + verificationPlan: [ + 'npm --prefix frontend test -- DriverManagerModal.*.test.tsx', + 'npm --prefix frontend run build', + '浏览器打开驱动管理,验证状态展示、安装按钮和日志区域。', + ], }, { path: 'frontend/src/components/DataSyncModal.tsx', lines: 1526, area: 'data-sync', riskLevel: 'high', + readiness: 'needsCharacterizationTests', why: '数据同步连接、表映射、预检查和执行结果集中,数据库方言问题容易隐藏。', + preferredNextSlice: '同步预检查结果', + safeSeam: '先把预检查结果做成纯展示组件,保留连接选择和执行动作在父组件。', suggestedSlices: ['连接选择区', '表映射区', '同步预检查结果', '执行日志区'], testTargets: ['DataSyncModal.*.test.tsx'], + verificationPlan: [ + 'npm --prefix frontend test -- DataSyncModal.*.test.tsx', + 'npm --prefix frontend run build', + '浏览器打开数据同步,验证连接选择、表映射、预检查和执行日志。', + ], }, { path: 'frontend/src/components/JVMDiagnosticConsole.tsx', lines: 1146, area: 'jvm-diagnostics', riskLevel: 'medium', + readiness: 'readyToExtract', why: '诊断命令、输出块、权限提示和会话状态可继续拆分,降低 JVM 诊断回归面。', + preferredNextSlice: '诊断输出区', + safeSeam: '输出区主要依赖命令结果数组,可先抽成展示组件。', suggestedSlices: ['命令输入区', '诊断输出区', '权限提示区'], testTargets: ['JVMDiagnosticConsole.*.test.tsx'], + verificationPlan: [ + 'npm --prefix frontend test -- JVMDiagnosticConsole.*.test.tsx', + 'npm --prefix frontend run build', + '浏览器打开 JVM 诊断面板,验证命令输入、输出和权限提示。', + ], }, ]; @@ -144,7 +220,7 @@ export const buildCodebaseHotspotSnapshot = ({ source: 'static_maintainability_snapshot', evidence: { measuredAt: '2026-06-12', - note: '基于当前仓库前端文件行数热点快照;提交前仍应用 git diff 和定向测试核对具体改动。', + note: '基于当前仓库前端文件行数热点快照;拆分前应优先选择 readyToExtract 且已有测试覆盖的 slice。', }, filters: { keyword: normalizedKeyword || undefined, @@ -165,13 +241,18 @@ export const buildCodebaseHotspotSnapshot = ({ lines: entry.lines, area: entry.area, riskLevel: entry.riskLevel, + readiness: entry.readiness, why: entry.why, + preferredNextSlice: includeRecommendations ? entry.preferredNextSlice : undefined, + safeSeam: includeRecommendations ? entry.safeSeam : undefined, suggestedSlices: includeRecommendations ? entry.suggestedSlices : [], testTargets: includeRecommendations ? entry.testTargets : [], + verificationPlan: includeRecommendations ? entry.verificationPlan : [], })), nextActions: includeRecommendations ? [ - '优先选择已有测试覆盖的 slice 做小步拆分,避免直接重写整个大组件。', + '优先选择 readiness=readyToExtract 且已有测试覆盖的 slice 做小步拆分,避免直接重写整个大组件。', + '每次拆分都要先确认 safeSeam,不跨越 SQL 执行、事务、连接密钥或数据库方言边界。', '拆分后至少运行对应组件测试、相关 utils 测试和 npm --prefix frontend run build。', '涉及可见 UI 的拆分需要用浏览器打开真实页面做一次冒烟验证。', ] diff --git a/frontend/src/components/ai/aiLocalToolExecutor.codebaseHotspots.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.codebaseHotspots.test.ts index 1232ec8..818e63d 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.codebaseHotspots.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.codebaseHotspots.test.ts @@ -45,8 +45,13 @@ describe('aiLocalToolExecutor inspect_codebase_hotspots', () => { expect(result.content).toContain('"kind":"codebase_hotspots"'); expect(result.content).toContain('frontend/src/components/QueryEditor.tsx'); expect(result.content).toContain('"riskLevel":"critical"'); + expect(result.content).toContain('"readiness":"readyToExtract"'); + expect(result.content).toContain('"preferredNextSlice":"编辑器工具栏"'); + expect(result.content).toContain('工具栏 JSX 可通过 props 透传状态和回调'); expect(result.content).toContain('事务状态条'); expect(result.content).toContain('QueryEditor.result-panel.test.tsx'); + expect(result.content).toContain('QueryEditor.external-sql-save.test.tsx'); + expect(result.content).toContain('浏览器打开 SQL 编辑器'); expect(result.content).not.toContain('import React'); }); }); diff --git a/frontend/src/utils/aiBuiltinInspectionDiagnosticsToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionDiagnosticsToolInfo.ts index 7d6dc03..eabac6f 100644 --- a/frontend/src/utils/aiBuiltinInspectionDiagnosticsToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionDiagnosticsToolInfo.ts @@ -168,14 +168,14 @@ export const BUILTIN_AI_INSPECTION_DIAGNOSTICS_TOOL_INFO: AIBuiltinToolInfo[] = icon: "🧱", desc: "查看前端大文件和拆分热点", detail: - "返回当前 GoNavi 前端代码中的大文件热点、行数、风险等级、建议拆分切片和应该运行的回归测试。适合用户要求继续治理几千行大文件、评估下一步该拆哪个组件,或 AI 在修改前需要先判断改动风险时调用。", + "返回当前 GoNavi 前端代码中的大文件热点、行数、风险等级、拆分成熟度、安全边界、建议拆分切片和应该运行的回归测试。适合用户要求继续治理几千行大文件、评估下一步该拆哪个组件,或 AI 在修改前需要先判断改动风险时调用。", params: "keyword?, minLines?(默认 1000), limit?(默认 8), includeRecommendations?(默认 true)", tool: { type: "function", function: { name: "inspect_codebase_hotspots", description: - "读取 GoNavi 前端大文件和拆分热点快照,返回文件路径、行数、风险等级、建议拆分切片和测试目标。适用于用户提到几千行文件太臃肿、需要继续拆分组件、评估下一个重构切入点或在改 UI/AI/MCP 前需要先判断代码热点风险时优先调用。", + "读取 GoNavi 前端大文件和拆分热点快照,返回文件路径、行数、风险等级、拆分成熟度、首选切片、安全拆分边界、建议拆分切片、测试目标和验证计划。适用于用户提到几千行文件太臃肿、需要继续拆分组件、评估下一个重构切入点或在改 UI/AI/MCP 前需要先判断代码热点风险时优先调用。", parameters: { type: "object", properties: {