refactor: Q35 AgentCLI 巨型组件拆分 — 521L→399L (-23%)

提取 agent-cli/ 子目录 2 个文件:
- event-handler.ts (149L): createWsEventHandler 工厂函数封装 8 种 WS 事件分发
  (session/progress/thought_delta/thought/tool_call/tool_result/confirm/done/error/aborted)
  + 高危命令确认逻辑 (handleConfirm)
- ChatInputArea.tsx (44L): 受控输入区组件 (textarea + 发送/停止按钮)

附: 导出 ConfirmOptions 接口供跨模块引用

验证: tsc -b=0, npm build✓ (17.67s), ESLint=0/66/0, go build=0, 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-10 07:53:25 +08:00
1 parent c7d050f1bc
commit 4283fb93c1
6 files changed
+221 -149

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)** + **PromDsFormDialog 528L→294L (-44%,提取 6 个文件: CollapsibleSection 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection + HeadersSection + AlertingSection + MiscSection + TestResultBanner)**,累计提取 112 个独立组件文件 + **AuditOperationsTab 527L→275L (-48%,提取 audit-operations/ 3 个文件: types(normalizeLog 纯函数+AuditStatistics 接口) + LogDetailDialog(85L 详情弹窗+DetailRow) + StatisticsDialog(108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading)**,累计提取 115 个独立组件文件** |
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q27 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + TopologyView 596L→472L (-21%) + DeploymentEditAdvanced 590L→289L (-51%) + WorkloadViewPage 572L→348L (-39%) + CloudAccountManager 572L→308L + ExcelServerImport 560L→385L + PipelineCreateDialog 559L→498L + PodLogsViewer 553L→331L (-40%) + HostMonitor 547L→224L (-59%) + KubernetesClusterDetail 547L→130L (-76%) + **AlertCenter 542L→312L (-42%,提取 4 个受控组件: AlertsToolbar + AlertsTab + ContactsTab + ContactGroupsTab,客户端搜索过滤+删除确认逻辑下沉到子组件)** + **CloudBilling 534L→295L (-45%,提取 cloud-billing/ 3 个文件: OverviewTab + RecordsTab(含 RecordsFilterToolbar) + AlertRulesTab)** + **PromDsFormDialog 528L→294L (-44%,提取 6 个文件: CollapsibleSection 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection + HeadersSection + AlertingSection + MiscSection + TestResultBanner)**,累计提取 112 个独立组件文件 + **AuditOperationsTab 527L→275L (-48%,提取 audit-operations/ 3 个文件: types(normalizeLog 纯函数+AuditStatistics 接口) + LogDetailDialog(85L 详情弹窗+DetailRow) + StatisticsDialog(108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading)**,累计提取 115 个独立组件文件** + **AgentCLI 521L→399L (-23%,提取 agent-cli/ 2 个文件: event-handler.ts createWsEventHandler 工厂函数封装 8 种 WS 事件分发+高危命令确认 + ChatInputArea.tsx 受控输入区组件),累计提取 117 个独立组件文件** |
### 3.2 🟡 中优先级
+1
View File
@@ -506,6 +506,7 @@ services/api.ts (1324L)
| Q32 | 前端巨型组件拆分(AuditOperationsTab 527L→275L (-48%),提取 audit-operations/ 3 个文件 (types 43L normalizeLog 纯函数+AuditStatistics 接口 + LogDetailDialog 85L 详情弹窗+DetailRow + StatisticsDialog 108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading,ESLint errors=0/any=0,tsc -b=0,npm build✓,go build=0,17/17 测试通过) | P0 | **done** |
| Q33 | 前端巨型组件拆分(DashboardRecent 518L→25L (-95%),提取 dashboard-recent/ 7 个文件 (types 48L + shared-components(StatusDot/SeverityBadge) 35L + AlertMessagesCard 72L + CertificateExpiryCard 99L + ReleaseHistoryCard 99L + OpsOverviewCard 77L + KubernetesClusterCard 97L),主文件改为 barrel re-export 保持向后兼容,tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** |
| Q34 | 前端巨型组件拆分(AIOpsAssistant 505L→288L (-43%),提取 aiops-assistant/ 5 个文件 (types 39L Message/QuickAction/defaultConfig/fmtTime + context-loaders.ts 69L loadAlerts/loadMetrics/loadReport 3 个平台数据加载纯函数 + quick-actions.tsx 17L + ChatMessage 47L 消息气泡(含 streaming/thinking/error 状态) + AssistantSidebar 76L 快捷操作+能力卡片),tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** |
| Q35 | 前端巨型组件拆分(AgentCLI 521L→399L (-23%),提取 agent-cli/ 2 个文件 (event-handler.ts 149L createWsEventHandler 工厂函数封装 8 种 WS 事件分发+高危命令确认 + ChatInputArea.tsx 44L 受控输入区组件),导出 ConfirmOptions 接口供跨模块引用,tsc -b=0,npm build✓,ESLint=0/66/0,go build=0,17/17 测试通过。最后一个 ≥500L 业务组件拆分完成,剩余最大业务组件 UnifiedQueryPanel 529L(已从 682L 拆分)) | P0 | **done** |
---
+25 -147
View File
@@ -10,7 +10,7 @@ import { kubernetesApi } from '@/api/kubernetes';
import { buildAgentCliWsUrl, type AgentCliMode } from '@/api/aiops/agent-cli';
import { toast } from 'sonner';
import {
Send, Square, Bot,
Bot,
} from 'lucide-react';
import AgentCLIHistory from './AgentCLIHistory';
import { AgentChatTimeline, type ChatItem } from './AgentCLIMessages';
@@ -20,6 +20,8 @@ import ScrollButtons from '@/components/ScrollButtons';
import type { AgentCliSession } from '@/api/aiops/agent-cli';
import { PageShell } from '@/components/layout/PageShell';
import { POD_NONE, type WsEvent } from './agent-cli-constants';
import { createWsEventHandler } from './agent-cli/event-handler';
import ChatInputArea from './agent-cli/ChatInputArea';
export default function AgentCLI() {
const confirm = useConfirm();
@@ -58,6 +60,13 @@ export default function AgentCLI() {
setItems((prev) => [...prev, { ...it, id: nextId() }]);
}, []);
// WebSocket 事件处理(提取到 event-handler.ts) // Logan.Li
const { handleEvent } = createWsEventHandler({
push, nextId, setRound, setTokens, setRunning, setCanContinue,
setItems, setHistoryRefresh, setViewMode,
confirm, wsRef,
});
const selectedCluster = clusters.find((c) => c.code === clusterCode);
const wsReady = cliMode === 'host' ? Boolean(serverId) : Boolean(clusterCode && namespace);
@@ -214,123 +223,6 @@ export default function AgentCLI() {
return () => ro.disconnect();
}, [items.length, updateScrollState]);
// 处理后端事件 // Logan.Li
const handleEvent = (ev: WsEvent) => {
const type = ev.type;
switch (type) {
case 'session':
push({ kind: 'info', content: ev.message || '' });
// 仅「恢复历史会话」时进入可追问(新任务开始的 session 事件文案不同) // Logan.Li
if (ev.message?.includes('已恢复历史会话')) {
setViewMode(false);
setCanContinue(true);
}
break;
case 'progress':
if (ev.round) setRound(ev.round);
if (ev.tokens) setTokens(ev.tokens);
break;
case 'thought_delta':
setItems((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last?.kind === 'thought' && last.streaming && last.round === ev.round) {
next[next.length - 1] = { ...last, content: (last.content || '') + (ev.thought || '') };
} else {
next.push({ id: nextId(), kind: 'thought', round: ev.round, content: ev.thought || '', streaming: true });
}
return next;
});
break;
case 'thought':
setItems((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last?.kind === 'thought' && last.streaming && last.round === ev.round) {
next[next.length - 1] = { ...last, content: ev.thought || last.content, streaming: false };
} else {
next.push({ id: nextId(), kind: 'thought', round: ev.round, content: ev.thought || '' });
}
return next;
});
break;
case 'tool_call': {
const args = ev.tool_args || {};
push({
kind: 'tool', round: ev.round, toolName: ev.tool_name,
command: String(args.command || (args.action ? `${args.action} ${args.name || ''}` : '')), risk: ev.risk, pending: true,
});
break;
}
case 'tool_result':
setItems((prev) => {
const next = [...prev];
for (let i = next.length - 1; i >= 0; i--) {
if (next[i].kind === 'tool' && next[i].pending && next[i].toolName === ev.tool_name) {
next[i] = {
...next[i], output: ev.output, success: ev.success,
duration: ev.duration, pending: false,
};
break;
}
}
return next;
});
break;
case 'confirm':
handleConfirm(ev);
break;
case 'done': {
const text = (ev.answer || ev.message || '').trim();
setItems((prev) => {
const norm = (s?: string) => (s || '').trim().replace(/\s+/g, ' ');
// 移除与 done 同文的 thought,避免终局总结显示两遍 // Logan.Li
const next = prev.filter((it) => {
if (it.kind !== 'thought' || !text) return true;
return norm(it.content) !== norm(text);
});
const last = next[next.length - 1];
if (last?.kind === 'done') {
if (text) {
next[next.length - 1] = { ...last, content: text, round: ev.round ?? last.round };
}
return next;
}
return [...next, {
id: nextId(),
kind: 'done',
content: text || '任务已完成,请查看上方工具执行结果。',
round: ev.round,
}];
});
setRunning(false);
setCanContinue(true);
setHistoryRefresh((k) => k + 1);
break;
}
case 'error':
case 'aborted':
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
setRunning(false);
setCanContinue(type !== 'aborted');
setHistoryRefresh((k) => k + 1);
break;
}
};
// 高危命令确认(后端驱动 → 弹框 → 回传) // Logan.Li
const handleConfirm = async (ev: WsEvent) => {
const ok = await confirm({
title: '⚠️ 确认执行高危操作',
description: `工具:${ev.tool_name}\n风险:${ev.risk}\n命令:${ev.command || '(无)'}`,
confirmText: '确认执行',
cancelText: '拒绝',
variant: 'destructive',
});
wsRef.current?.send(JSON.stringify({ type: ok ? 'confirm' : 'deny', confirm_id: ev.confirm_id }));
if (!ok) push({ kind: 'info', content: `已拒绝执行:${ev.command || ev.tool_name}` });
};
// 发送任务 // Logan.Li
const send = () => {
const q = input.trim();
@@ -486,35 +378,21 @@ export default function AgentCLI() {
/>
</div>
{/* 加宽输入区:多行 + 独立操作栏 // Logan.Li */}
<div className="border-t border-[var(--dg-border)] p-3 space-y-2 shrink-0">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
placeholder={
viewMode ? '查看模式中,点击「继续追问」或「新建任务」'
: canContinue ? '继续追问,回车发送(Shift+回车换行)'
: wsReady ? '输入任务描述,回车发送(Shift+回车换行)'
: (cliMode === 'kubernetes' ? '请先选择集群和命名空间' : '请先选择目标主机')
}
rows={3}
disabled={viewMode}
className="w-full resize-none dg-input min-h-[5rem] max-h-40 text-sm leading-relaxed disabled:opacity-50"
/>
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] dg-text-muted">Shift+回车换行 · 回车发送</span>
{running ? (
<Button variant="destructive" size="sm" onClick={stop} className="bg-red-600 hover:bg-red-700">
<Square className="w-3.5 h-3.5 mr-1" /> 停止
</Button>
) : (
<Button size="sm" onClick={send} disabled={!input.trim() || viewMode} className="bg-blue-600 hover:bg-blue-700">
<Send className="w-3.5 h-3.5 mr-1" /> 执行
</Button>
)}
</div>
</div>
{/* 输入区(提取到 ChatInputArea.tsx) // Logan.Li */}
<ChatInputArea
input={input}
setInput={setInput}
onSend={send}
onStop={stop}
running={running}
viewMode={viewMode}
placeholder={
viewMode ? '查看模式中,点击「继续追问」或「新建任务」'
: canContinue ? '继续追问,回车发送(Shift+回车换行)'
: wsReady ? '输入任务描述,回车发送(Shift+回车换行)'
: (cliMode === 'kubernetes' ? '请先选择集群和命名空间' : '请先选择目标主机')
}
/>
</Card>
</div>
</PageShell>
@@ -0,0 +1,44 @@
// ChatInputArea.tsx — AgentCLI 输入区(textarea + 发送/停止按钮)
// 从 AgentCLI.tsx 提取:受控展示组件,接收输入值 + 状态 + 回调
import { Send, Square } from 'lucide-react';
import { Button } from '@/components/ui/button';
export interface ChatInputAreaProps {
input: string;
setInput: (v: string) => void;
onSend: () => void;
onStop: () => void;
running: boolean;
viewMode: boolean;
placeholder: string;
}
export default function ChatInputArea({
input, setInput, onSend, onStop, running, viewMode, placeholder,
}: ChatInputAreaProps) {
return (
<div className="border-t border-[var(--dg-border)] p-3 space-y-2 shrink-0">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }}
placeholder={placeholder}
rows={3}
disabled={viewMode}
className="w-full resize-none dg-input min-h-[5rem] max-h-40 text-sm leading-relaxed disabled:opacity-50"
/>
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] dg-text-muted">Shift+回车换行 · 回车发送</span>
{running ? (
<Button variant="destructive" size="sm" onClick={onStop} className="bg-red-600 hover:bg-red-700">
<Square className="w-3.5 h-3.5 mr-1" /> 停止
</Button>
) : (
<Button size="sm" onClick={onSend} disabled={!input.trim() || viewMode} className="bg-blue-600 hover:bg-blue-700">
<Send className="w-3.5 h-3.5 mr-1" /> 执行
</Button>
)}
</div>
</div>
);
}
@@ -0,0 +1,149 @@
// event-handler.ts — AgentCLI WebSocket 事件处理工厂函数
// 从 AgentCLI.tsx 提取:handleEvent(8 种事件分发)+ handleConfirm(高危命令确认)
// 纯函数工厂:接收 state setters + confirm 工具,返回事件处理函数
import type { Dispatch, SetStateAction, RefObject } from 'react';
import type { ChatItem } from '../AgentCLIMessages';
import type { WsEvent } from '../agent-cli-constants';
import type { ConfirmOptions } from '@/components/ui/confirm-dialog';
export interface WsEventHandlerDeps {
push: (it: Omit<ChatItem, 'id'>) => void;
nextId: () => number;
setRound: (v: number) => void;
setTokens: (v: number) => void;
setRunning: (v: boolean) => void;
setCanContinue: (v: boolean) => void;
setItems: Dispatch<SetStateAction<ChatItem[]>>;
setHistoryRefresh: Dispatch<SetStateAction<number>>;
setViewMode: (v: boolean) => void;
confirm: (opts: ConfirmOptions) => Promise<boolean>;
wsRef: RefObject<WebSocket | null>;
}
/** 创建 WebSocket 事件处理函数(含高危命令确认逻辑) */
export function createWsEventHandler(deps: WsEventHandlerDeps) {
const {
push, nextId, setRound, setTokens, setRunning, setCanContinue,
setItems, setHistoryRefresh, setViewMode,
confirm, wsRef,
} = deps;
// 高危命令确认(后端驱动 → 弹框 → 回传)
async function handleConfirm(ev: WsEvent) {
const ok = await confirm({
title: '⚠️ 确认执行高危操作',
description: `工具:${ev.tool_name}\n风险:${ev.risk}\n命令:${ev.command || '(无)'}`,
confirmText: '确认执行',
cancelText: '拒绝',
variant: 'destructive',
});
wsRef.current?.send(JSON.stringify({ type: ok ? 'confirm' : 'deny', confirm_id: ev.confirm_id }));
if (!ok) push({ kind: 'info', content: `已拒绝执行:${ev.command || ev.tool_name}` });
}
// 处理后端事件
function handleEvent(ev: WsEvent) {
const type = ev.type;
switch (type) {
case 'session':
push({ kind: 'info', content: ev.message || '' });
// 仅「恢复历史会话」时进入可追问(新任务开始的 session 事件文案不同)
if (ev.message?.includes('已恢复历史会话')) {
setViewMode(false);
setCanContinue(true);
}
break;
case 'progress':
if (ev.round) setRound(ev.round);
if (ev.tokens) setTokens(ev.tokens);
break;
case 'thought_delta':
setItems((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last?.kind === 'thought' && last.streaming && last.round === ev.round) {
next[next.length - 1] = { ...last, content: (last.content || '') + (ev.thought || '') };
} else {
next.push({ id: nextId(), kind: 'thought', round: ev.round, content: ev.thought || '', streaming: true });
}
return next;
});
break;
case 'thought':
setItems((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last?.kind === 'thought' && last.streaming && last.round === ev.round) {
next[next.length - 1] = { ...last, content: ev.thought || last.content, streaming: false };
} else {
next.push({ id: nextId(), kind: 'thought', round: ev.round, content: ev.thought || '' });
}
return next;
});
break;
case 'tool_call': {
const args = ev.tool_args || {};
push({
kind: 'tool', round: ev.round, toolName: ev.tool_name,
command: String(args.command || (args.action ? `${args.action} ${args.name || ''}` : '')), risk: ev.risk, pending: true,
});
break;
}
case 'tool_result':
setItems((prev) => {
const next = [...prev];
for (let i = next.length - 1; i >= 0; i--) {
if (next[i].kind === 'tool' && next[i].pending && next[i].toolName === ev.tool_name) {
next[i] = {
...next[i], output: ev.output, success: ev.success,
duration: ev.duration, pending: false,
};
break;
}
}
return next;
});
break;
case 'confirm':
handleConfirm(ev);
break;
case 'done': {
const text = (ev.answer || ev.message || '').trim();
setItems((prev) => {
const norm = (s?: string) => (s || '').trim().replace(/\s+/g, ' ');
// 移除与 done 同文的 thought,避免终局总结显示两遍
const next = prev.filter((it) => {
if (it.kind !== 'thought' || !text) return true;
return norm(it.content) !== norm(text);
});
const last = next[next.length - 1];
if (last?.kind === 'done') {
if (text) {
next[next.length - 1] = { ...last, content: text, round: ev.round ?? last.round };
}
return next;
}
return [...next, {
id: nextId(),
kind: 'done',
content: text || '任务已完成,请查看上方工具执行结果。',
round: ev.round,
}];
});
setRunning(false);
setCanContinue(true);
setHistoryRefresh((k) => k + 1);
break;
}
case 'error':
case 'aborted':
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
setRunning(false);
setCanContinue(type !== 'aborted');
setHistoryRefresh((k) => k + 1);
break;
}
}
return { handleEvent, handleConfirm };
}
@@ -7,7 +7,7 @@ import {
import { Button } from '@/components/ui/button';
import i18n from '@/i18n';
interface ConfirmOptions {
export interface ConfirmOptions {
title?: string;
description?: string;
confirmText?: string;