feat(ai): 增强代码热点拆分诊断

This commit is contained in:
Syngnat
2026-06-12 08:24:13 +08:00
parent 1058da653d
commit 781a80e03f
6 changed files with 352 additions and 149 deletions

View File

@@ -349,6 +349,8 @@ vi.mock('antd', () => {
</button>
);
Button.Group = ({ children }: any) => <div>{children}</div>;
const Space: any = ({ children }: any) => <div>{children}</div>;
Space.Compact = ({ children, className }: any) => <div className={className}>{children}</div>;
const Form: any = ({ children }: any) => <form>{children}</form>;
Form.Item = ({ children }: any) => <>{children}</>;
@@ -356,6 +358,7 @@ vi.mock('antd', () => {
return {
Button,
Space,
message: messageApi,
Modal: ({ children, open, onOk, okText = '确认' }: any) => (open ? (
<section>
@@ -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');

View File

@@ -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' }}
>
<div className={isV2Ui ? 'gn-v2-query-toolbar' : undefined} style={{ padding: '4px 8px 8px', display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-selects' : undefined}
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
>
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-connection-select' : undefined}
style={isV2Ui ? undefined : { width: 150 }}
placeholder="选择连接"
value={currentConnectionId}
onChange={(val) => {
setCurrentConnectionId(val);
setCurrentDb('');
}}
options={queryCapableConnections.map(c => ({ label: c.name, value: c.id }))}
showSearch
/>
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-database-select' : undefined}
style={isV2Ui ? undefined : { width: 200 }}
placeholder="选择数据库"
value={currentDb}
onChange={setCurrentDb}
options={dbList.map(db => ({ label: db, value: db }))}
showSearch
/>
<Tooltip title="最大返回行数(会对 SELECT 自动加 LIMIT防止大结果集卡死">
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-max-rows-select' : undefined}
style={isV2Ui ? undefined : { width: 170 }}
value={queryOptions?.maxRows ?? 5000}
onChange={(val) => setQueryOptions({ maxRows: Number(val) })}
options={[
{ label: '最大行数500', value: 500 },
{ label: '最大行数1000', value: 1000 },
{ label: '最大行数5000', value: 5000 },
{ label: '最大行数20000', value: 20000 },
{ label: '最大行数:不限', value: 0 },
]}
/>
</Tooltip>
<QueryEditorTransactionSettings
isV2Ui={isV2Ui}
commitMode={sqlEditorCommitMode}
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
onCommitModeChange={(mode) => setSqlEditorTransactionOptions(
mode === 'auto'
? { commitMode: mode, autoCommitDelayMs: 0 }
: { commitMode: mode },
)}
onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })}
/>
{pendingSqlTransaction && sqlEditorTransactionToolbar}
</div>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-actions' : undefined}
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
>
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip
title={
runQueryShortcutBinding.enabled && runQueryShortcutBinding.combo
? `运行(${getShortcutDisplayLabel(runQueryShortcutBinding.combo, activeShortcutPlatform)}`
: '运行'
}
>
<Button className={isV2Ui ? 'gn-v2-query-toolbar-run-action' : undefined} type="primary" icon={<PlayCircleOutlined />} onMouseDown={captureEditorCursorPosition} onClick={handleRun} loading={loading}>
</Button>
</Tooltip>
{loading && (
<Button type="primary" danger icon={<StopOutlined />} onClick={handleCancel}>
</Button>
)}
</Button.Group>
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip
title={
saveQueryShortcutBinding.enabled && saveQueryShortcutBinding.combo
? `保存(${getShortcutDisplayLabel(saveQueryShortcutBinding.combo, activeShortcutPlatform)}`
: '保存'
}
>
<Button icon={<SaveOutlined />} onClick={handleQuickSave}>
</Button>
</Tooltip>
<Dropdown menu={{ items: saveMoreMenuItems }} placement="bottomRight">
<Button></Button>
</Dropdown>
</Button.Group>
<Button.Group className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip title="美化 SQL">
<Button icon={<FormatPainterOutlined />} onClick={handleFormat}></Button>
</Tooltip>
<Dropdown menu={{ items: formatSettingsMenu }} placement="bottomRight">
<Button className={isV2Ui ? 'gn-v2-query-toolbar-icon-action' : undefined} icon={<SettingOutlined />} />
</Dropdown>
</Button.Group>
<Tooltip
title={
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
? `${isResultPanelVisible ? '隐藏结果区' : '显示结果区'}${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)}`
: (isResultPanelVisible ? '隐藏结果区' : '显示结果区')
}
>
<Button
icon={isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
onClick={toggleResultPanelVisibility}
>
</Button>
</Tooltip>
<Dropdown menu={{ items: [
{ key: 'ai-generate', label: '生成 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('generate') },
{ key: 'ai-explain', label: '解释 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('explain') },
{ key: 'ai-optimize', label: '优化 SQL', icon: <RobotOutlined />, onClick: () => handleAIAction('optimize') },
{ type: 'divider' as const },
{ key: 'ai-schema', label: 'Schema 分析', icon: <RobotOutlined />, onClick: () => handleAIAction('schema') },
] }} placement="bottomRight">
<Button className={isV2Ui ? 'gn-v2-query-toolbar-ai-action' : undefined} icon={<RobotOutlined />} style={{ color: '#818cf8' }}>AI</Button>
</Dropdown>
</div>
</div>
<QueryEditorToolbar
isV2Ui={isV2Ui}
currentConnectionId={currentConnectionId}
currentDb={currentDb}
queryCapableConnections={queryCapableConnections}
dbList={dbList}
maxRows={queryOptions?.maxRows ?? 5000}
sqlEditorCommitMode={sqlEditorCommitMode}
sqlEditorAutoCommitDelayMs={sqlEditorAutoCommitDelayMs}
pendingTransactionToolbar={pendingSqlTransaction ? sqlEditorTransactionToolbar : null}
runQueryShortcutBinding={runQueryShortcutBinding}
saveQueryShortcutBinding={saveQueryShortcutBinding}
toggleQueryResultsPanelShortcutBinding={toggleQueryResultsPanelShortcutBinding}
activeShortcutPlatform={activeShortcutPlatform}
isResultPanelVisible={isResultPanelVisible}
loading={loading}
saveMoreMenuItems={saveMoreMenuItems}
formatSettingsMenu={formatSettingsMenu}
onConnectionChange={(val) => {
setCurrentConnectionId(val);
setCurrentDb('');
}}
onDatabaseChange={setCurrentDb}
onMaxRowsChange={(maxRows) => setQueryOptions({ maxRows })}
onCommitModeChange={(mode) => setSqlEditorTransactionOptions(
mode === 'auto'
? { commitMode: mode, autoCommitDelayMs: 0 }
: { commitMode: mode },
)}
onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })}
onCaptureEditorCursorPosition={captureEditorCursorPosition}
onRun={handleRun}
onCancel={handleCancel}
onQuickSave={handleQuickSave}
onFormat={handleFormat}
onToggleResultPanelVisibility={toggleResultPanelVisibility}
onAIAction={handleAIAction}
/>
<div
ref={editorShellRef}

View File

@@ -0,0 +1,204 @@
import React from 'react';
import { Button, Dropdown, Select, Space, Tooltip, type MenuProps } from 'antd';
import {
EyeInvisibleOutlined,
EyeOutlined,
FormatPainterOutlined,
PlayCircleOutlined,
RobotOutlined,
SaveOutlined,
SettingOutlined,
StopOutlined,
} from '@ant-design/icons';
import type { SavedConnection } from '../types';
import { getShortcutDisplayLabel, type ShortcutPlatform, type ShortcutPlatformBinding } from '../utils/shortcuts';
import QueryEditorTransactionSettings, { type SqlEditorCommitMode } from './QueryEditorTransactionSettings';
type QueryEditorToolbarProps = {
isV2Ui: boolean;
currentConnectionId: string;
currentDb: string;
queryCapableConnections: SavedConnection[];
dbList: string[];
maxRows: number;
sqlEditorCommitMode: SqlEditorCommitMode;
sqlEditorAutoCommitDelayMs: number;
pendingTransactionToolbar: React.ReactNode;
runQueryShortcutBinding: ShortcutPlatformBinding;
saveQueryShortcutBinding: ShortcutPlatformBinding;
toggleQueryResultsPanelShortcutBinding: ShortcutPlatformBinding;
activeShortcutPlatform: ShortcutPlatform;
isResultPanelVisible: boolean;
loading: boolean;
saveMoreMenuItems: MenuProps['items'];
formatSettingsMenu: MenuProps['items'];
onConnectionChange: (connectionId: string) => void;
onDatabaseChange: (dbName: string) => void;
onMaxRowsChange: (maxRows: number) => void;
onCommitModeChange: (mode: SqlEditorCommitMode) => void;
onAutoCommitDelayMsChange: (delayMs: number) => void;
onCaptureEditorCursorPosition: () => void;
onRun: () => void;
onCancel: () => void;
onQuickSave: () => void;
onFormat: () => void;
onToggleResultPanelVisibility: () => void;
onAIAction: (action: 'generate' | 'explain' | 'optimize' | 'schema') => void;
};
const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
isV2Ui,
currentConnectionId,
currentDb,
queryCapableConnections,
dbList,
maxRows,
sqlEditorCommitMode,
sqlEditorAutoCommitDelayMs,
pendingTransactionToolbar,
runQueryShortcutBinding,
saveQueryShortcutBinding,
toggleQueryResultsPanelShortcutBinding,
activeShortcutPlatform,
isResultPanelVisible,
loading,
saveMoreMenuItems,
formatSettingsMenu,
onConnectionChange,
onDatabaseChange,
onMaxRowsChange,
onCommitModeChange,
onAutoCommitDelayMsChange,
onCaptureEditorCursorPosition,
onRun,
onCancel,
onQuickSave,
onFormat,
onToggleResultPanelVisibility,
onAIAction,
}) => (
<div className={isV2Ui ? 'gn-v2-query-toolbar' : undefined} style={{ padding: '4px 8px 8px', display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-selects' : undefined}
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
>
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-connection-select' : undefined}
style={isV2Ui ? undefined : { width: 150 }}
placeholder="选择连接"
value={currentConnectionId}
onChange={onConnectionChange}
options={queryCapableConnections.map(c => ({ label: c.name, value: c.id }))}
showSearch
/>
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-database-select' : undefined}
style={isV2Ui ? undefined : { width: 200 }}
placeholder="选择数据库"
value={currentDb}
onChange={onDatabaseChange}
options={dbList.map(db => ({ label: db, value: db }))}
showSearch
/>
<Tooltip title="最大返回行数(会对 SELECT 自动加 LIMIT防止大结果集卡死">
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-max-rows-select' : undefined}
style={isV2Ui ? undefined : { width: 170 }}
value={maxRows}
onChange={(val) => onMaxRowsChange(Number(val))}
options={[
{ label: '最大行数500', value: 500 },
{ label: '最大行数1000', value: 1000 },
{ label: '最大行数5000', value: 5000 },
{ label: '最大行数20000', value: 20000 },
{ label: '最大行数:不限', value: 0 },
]}
/>
</Tooltip>
<QueryEditorTransactionSettings
isV2Ui={isV2Ui}
commitMode={sqlEditorCommitMode}
autoCommitDelayMs={sqlEditorAutoCommitDelayMs}
onCommitModeChange={onCommitModeChange}
onAutoCommitDelayMsChange={onAutoCommitDelayMsChange}
/>
{pendingTransactionToolbar}
</div>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-actions' : undefined}
style={{ display: 'flex', gap: '8px', flexShrink: 0, alignItems: 'center' }}
>
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip
title={
runQueryShortcutBinding.enabled && runQueryShortcutBinding.combo
? `运行(${getShortcutDisplayLabel(runQueryShortcutBinding.combo, activeShortcutPlatform)}`
: '运行'
}
>
<Button className={isV2Ui ? 'gn-v2-query-toolbar-run-action' : undefined} type="primary" icon={<PlayCircleOutlined />} onMouseDown={onCaptureEditorCursorPosition} onClick={onRun} loading={loading}>
</Button>
</Tooltip>
{loading && (
<Button type="primary" danger icon={<StopOutlined />} onClick={onCancel}>
</Button>
)}
</Space.Compact>
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip
title={
saveQueryShortcutBinding.enabled && saveQueryShortcutBinding.combo
? `保存(${getShortcutDisplayLabel(saveQueryShortcutBinding.combo, activeShortcutPlatform)}`
: '保存'
}
>
<Button icon={<SaveOutlined />} onClick={onQuickSave}>
</Button>
</Tooltip>
<Dropdown menu={{ items: saveMoreMenuItems }} placement="bottomRight">
<Button></Button>
</Dropdown>
</Space.Compact>
<Space.Compact className={isV2Ui ? 'gn-v2-query-toolbar-action-group' : undefined}>
<Tooltip title="美化 SQL">
<Button icon={<FormatPainterOutlined />} onClick={onFormat}></Button>
</Tooltip>
<Dropdown menu={{ items: formatSettingsMenu }} placement="bottomRight">
<Button className={isV2Ui ? 'gn-v2-query-toolbar-icon-action' : undefined} icon={<SettingOutlined />} />
</Dropdown>
</Space.Compact>
<Tooltip
title={
toggleQueryResultsPanelShortcutBinding.enabled && toggleQueryResultsPanelShortcutBinding.combo
? `${isResultPanelVisible ? '隐藏结果区' : '显示结果区'}${getShortcutDisplayLabel(toggleQueryResultsPanelShortcutBinding.combo, activeShortcutPlatform)}`
: (isResultPanelVisible ? '隐藏结果区' : '显示结果区')
}
>
<Button
icon={isResultPanelVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
onClick={onToggleResultPanelVisibility}
>
</Button>
</Tooltip>
<Dropdown menu={{ items: [
{ key: 'ai-generate', label: '生成 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('generate') },
{ key: 'ai-explain', label: '解释 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('explain') },
{ key: 'ai-optimize', label: '优化 SQL', icon: <RobotOutlined />, onClick: () => onAIAction('optimize') },
{ type: 'divider' as const },
{ key: 'ai-schema', label: 'Schema 分析', icon: <RobotOutlined />, onClick: () => onAIAction('schema') },
] }} placement="bottomRight">
<Button className={isV2Ui ? 'gn-v2-query-toolbar-ai-action' : undefined} icon={<RobotOutlined />} style={{ color: '#818cf8' }}>AI</Button>
</Dropdown>
</div>
</div>
);
export default QueryEditorToolbar;

View File

@@ -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 的拆分需要用浏览器打开真实页面做一次冒烟验证。',
]

View File

@@ -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');
});
});

View File

@@ -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: {