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:
1 parent
c7d050f1bc
commit
4283fb93c1
6 files changed
+221
-149
No files matched your search
@@ -129,7 +129,7 @@
|
|||||||
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
|
| ~~`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) |
|
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
|
||||||
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
|
| ~~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 🟡 中优先级
|
### 3.2 🟡 中优先级
|
||||||
|
|
||||||
|
|||||||
@@ -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** |
|
| 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** |
|
| 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** |
|
| 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** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { kubernetesApi } from '@/api/kubernetes';
|
|||||||
import { buildAgentCliWsUrl, type AgentCliMode } from '@/api/aiops/agent-cli';
|
import { buildAgentCliWsUrl, type AgentCliMode } from '@/api/aiops/agent-cli';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Send, Square, Bot,
|
Bot,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import AgentCLIHistory from './AgentCLIHistory';
|
import AgentCLIHistory from './AgentCLIHistory';
|
||||||
import { AgentChatTimeline, type ChatItem } from './AgentCLIMessages';
|
import { AgentChatTimeline, type ChatItem } from './AgentCLIMessages';
|
||||||
@@ -20,6 +20,8 @@ import ScrollButtons from '@/components/ScrollButtons';
|
|||||||
import type { AgentCliSession } from '@/api/aiops/agent-cli';
|
import type { AgentCliSession } from '@/api/aiops/agent-cli';
|
||||||
import { PageShell } from '@/components/layout/PageShell';
|
import { PageShell } from '@/components/layout/PageShell';
|
||||||
import { POD_NONE, type WsEvent } from './agent-cli-constants';
|
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() {
|
export default function AgentCLI() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -58,6 +60,13 @@ export default function AgentCLI() {
|
|||||||
setItems((prev) => [...prev, { ...it, id: nextId() }]);
|
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 selectedCluster = clusters.find((c) => c.code === clusterCode);
|
||||||
const wsReady = cliMode === 'host' ? Boolean(serverId) : Boolean(clusterCode && namespace);
|
const wsReady = cliMode === 'host' ? Boolean(serverId) : Boolean(clusterCode && namespace);
|
||||||
|
|
||||||
@@ -214,123 +223,6 @@ export default function AgentCLI() {
|
|||||||
return () => ro.disconnect();
|
return () => ro.disconnect();
|
||||||
}, [items.length, updateScrollState]);
|
}, [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
|
// 发送任务 // Logan.Li
|
||||||
const send = () => {
|
const send = () => {
|
||||||
const q = input.trim();
|
const q = input.trim();
|
||||||
@@ -486,35 +378,21 @@ export default function AgentCLI() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加宽输入区:多行 + 独立操作栏 // Logan.Li */}
|
{/* 输入区(提取到 ChatInputArea.tsx) // Logan.Li */}
|
||||||
<div className="border-t border-[var(--dg-border)] p-3 space-y-2 shrink-0">
|
<ChatInputArea
|
||||||
<textarea
|
input={input}
|
||||||
value={input}
|
setInput={setInput}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onSend={send}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
|
onStop={stop}
|
||||||
placeholder={
|
running={running}
|
||||||
viewMode ? '查看模式中,点击「继续追问」或「新建任务」'
|
viewMode={viewMode}
|
||||||
: canContinue ? '继续追问,回车发送(Shift+回车换行)'
|
placeholder={
|
||||||
: wsReady ? '输入任务描述,回车发送(Shift+回车换行)'
|
viewMode ? '查看模式中,点击「继续追问」或「新建任务」'
|
||||||
: (cliMode === 'kubernetes' ? '请先选择集群和命名空间' : '请先选择目标主机')
|
: canContinue ? '继续追问,回车发送(Shift+回车换行)'
|
||||||
}
|
: wsReady ? '输入任务描述,回车发送(Shift+回车换行)'
|
||||||
rows={3}
|
: (cliMode === 'kubernetes' ? '请先选择集群和命名空间' : '请先选择目标主机')
|
||||||
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>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageShell>
|
</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 { Button } from '@/components/ui/button';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
interface ConfirmOptions {
|
export interface ConfirmOptions {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
|
|||||||
Reference in new issue
Block a user