refactor(quality): Q7 前端巨型组件拆分 — AgentCLI 814L→521L (-36%), 提取工具栏+常量+空状态+滚动按钮 4 个独立文件
This commit is contained in:
1 parent
892cb85676
commit
2cc5ad719b
7 files changed
+535
-340
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 done: 最大组件 ResourceVisualEditor 882L→282L (-68%),提取 5 个独立编辑器文件** |
|
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6+Q7 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%),累计提取 11 个独立组件文件** |
|
||||||
|
|
||||||
### 3.2 🟡 中优先级
|
### 3.2 🟡 中优先级
|
||||||
|
|
||||||
|
|||||||
+33
@@ -478,6 +478,7 @@ services/api.ts (1324L)
|
|||||||
| U13 | 主机监控大屏挂载(HostMonitorDashboard 死代码复活) | P3 | **done** |
|
| U13 | 主机监控大屏挂载(HostMonitorDashboard 死代码复活) | P3 | **done** |
|
||||||
| U14 | ESLint 冗余 disable 指令清理(unused eslint-disable 6→0) | P3 | **done** |
|
| U14 | ESLint 冗余 disable 指令清理(unused eslint-disable 6→0) | P3 | **done** |
|
||||||
| Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** |
|
| Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** |
|
||||||
|
| Q7 | 前端巨型组件拆分(AgentCLI 814L→521L,提取工具栏+常量+空状态+滚动按钮) | P0 | **done** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -645,3 +646,35 @@ pending → in_progress → done / blocked
|
|||||||
- [x] 17/17 集成回归测试通过(零回归)
|
- [x] 17/17 集成回归测试通过(零回归)
|
||||||
- [x] Docker 部署 healthy
|
- [x] Docker 部署 healthy
|
||||||
- [x] K8s 资源 YAML API 链路验证通过(visual editor dispatch 正常)
|
- [x] K8s 资源 YAML API 链路验证通过(visual editor dispatch 正常)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q7: 前端巨型组件拆分 — AgentCLI [done]
|
||||||
|
|
||||||
|
**问题**: `AgentCLI.tsx`(814 行)是 Q6 拆分后项目最大的前端组件,包含 AI 运维 Agent 执行页面的全部逻辑:WebSocket 事件处理、工具栏控制(模式切换/主机选择/集群选择/Pod选择/安全模式/状态徽章/核心能力说明)、对话区空状态、滚动按钮。单文件过大导致维护困难。
|
||||||
|
|
||||||
|
**已完成 (2026-07-06)**:
|
||||||
|
|
||||||
|
将内联的静态数据、工具栏 UI、空状态、滚动按钮提取为独立文件:
|
||||||
|
|
||||||
|
| 新文件 | 行数 | 职责 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `agent-cli-constants.ts` | 89 | WS 事件接口 + 安全模式定义 + 核心能力说明 + Pod 占位符 + 主机标签格式化 |
|
||||||
|
| `AgentCLIToolbar.tsx` | 281 | 工作区控制栏(模式切换 + 目标选择 + 安全模式 + 状态徽章 + 核心能力 Popover) |
|
||||||
|
| `AgentCLIEmptyState.tsx` | 42 | 对话区空状态(未选目标/连接中/就绪提示) |
|
||||||
|
| `ScrollButtons.tsx` | 43 | 浮动滚动按钮(回到顶部/底部),通用可复用组件 |
|
||||||
|
|
||||||
|
主文件 `AgentCLI.tsx` 从 **814 行降至 521 行**(-36%),仅保留:状态管理、WebSocket 连接生命周期、事件处理(handleEvent switch)、发送/停止/恢复逻辑、对话区布局(AgentChatTimeline + 输入区)。
|
||||||
|
|
||||||
|
**提取原则**: 常量文件为纯数据(零 React 依赖,仅 lucide 图标类型引用);工具栏为受控组件(接收全部状态 + setter 作为 props,内部无独立状态);空状态为纯展示组件;滚动按钮为通用可复用组件(不限于 AgentCLI)。零逻辑变更。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] AgentCLI.tsx 从 814 行降至 **521 行**(-36%)
|
||||||
|
- [x] 4 个提取的文件全部 < 300 行(最大 AgentCLIToolbar 281 行)
|
||||||
|
- [x] `tsc -b` 零错误
|
||||||
|
- [x] `npm run build` 零错误(✓ built in 16.95s)
|
||||||
|
- [x] ESLint errors 保持 **0**(清理 6 个 unused 变量/import)
|
||||||
|
- [x] ESLint warnings 保持 **59**(无新增)
|
||||||
|
- [x] `go build` 零错误
|
||||||
|
- [x] 17/17 集成回归测试通过(零回归)
|
||||||
|
- [x] Docker 部署 healthy
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// ScrollButtons.tsx — 浮动滚动按钮(回到顶部/底部),AgentCLI 对话区复用
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ScrollButtonsProps {
|
||||||
|
show: boolean;
|
||||||
|
atTop: boolean;
|
||||||
|
atBottom: boolean;
|
||||||
|
onScrollTop: () => void;
|
||||||
|
onScrollBottom: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScrollButtons({ show, atTop, atBottom, onScrollTop, onScrollBottom }: ScrollButtonsProps) {
|
||||||
|
if (!show || (atTop && atBottom)) return null;
|
||||||
|
return (
|
||||||
|
<div className="absolute right-3 bottom-3 z-10 flex flex-col gap-1.5">
|
||||||
|
{!atTop && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full shadow-md border border-[var(--dg-border)] bg-[var(--surface-raised)]/95 opacity-90 hover:opacity-100"
|
||||||
|
onClick={onScrollTop}
|
||||||
|
title="回到顶部"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!atBottom && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full shadow-md border border-[var(--dg-border)] bg-[var(--surface-raised)]/95 opacity-90 hover:opacity-100"
|
||||||
|
onClick={onScrollBottom}
|
||||||
|
title="回到底部"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,100 +4,22 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import {
|
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { serverApi } from '@/api';
|
import { serverApi } from '@/api';
|
||||||
import { kubernetesApi } from '@/api/kubernetes';
|
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, Terminal, Shield, Brain, Cpu, Database, Loader2, Bot, Lightbulb, Server,
|
Send, Square, Bot,
|
||||||
PanelLeftClose, History, ChevronUp, ChevronDown, Boxes, Cloud,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import AgentCLIHistory from './AgentCLIHistory';
|
import AgentCLIHistory from './AgentCLIHistory';
|
||||||
import { AgentChatTimeline, type ChatItem } from './AgentCLIMessages';
|
import { AgentChatTimeline, type ChatItem } from './AgentCLIMessages';
|
||||||
|
import AgentCLIToolbar from './AgentCLIToolbar';
|
||||||
|
import AgentCLIEmptyState from './AgentCLIEmptyState';
|
||||||
|
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';
|
||||||
// WS 事件类型(后端 wsEvent 对应) // Logan.Li
|
|
||||||
interface WsEvent {
|
|
||||||
type: string;
|
|
||||||
round?: number;
|
|
||||||
session_id?: number;
|
|
||||||
thought?: string;
|
|
||||||
tool_name?: string;
|
|
||||||
tool_args?: Record<string, unknown>;
|
|
||||||
risk?: string;
|
|
||||||
output?: string;
|
|
||||||
success?: boolean;
|
|
||||||
duration?: string;
|
|
||||||
tokens?: number;
|
|
||||||
answer?: string;
|
|
||||||
message?: string;
|
|
||||||
confirm_id?: string;
|
|
||||||
command?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 四大核心模块说明(灯泡 Popover) // Logan.Li
|
|
||||||
const POD_NONE = '__none__';
|
|
||||||
|
|
||||||
const TIPS_HOST = [
|
|
||||||
{ icon: Cpu, title: 'LLM + 工具层', color: 'text-sky-400',
|
|
||||||
desc: 'OpenAI 兼容 LLM;工具含 ssh_execute(远程执行)与 analyze_output(输出分析)。' },
|
|
||||||
{ icon: Shield, title: '安全 + Prompt', color: 'dg-text-yellow',
|
|
||||||
desc: '五级风险评估 + 三级安全模式;危险命令硬性拒绝,高危命令弹框确认。' },
|
|
||||||
{ icon: Brain, title: 'ReAct 核心引擎', color: 'dg-text-purple',
|
|
||||||
desc: '思考→行动→观察循环(最多 16 轮),失败换法重试,循环检测防死循环。' },
|
|
||||||
{ icon: Database, title: '自进化引擎', color: 'dg-text-green',
|
|
||||||
desc: '四层记忆异步学习,同类任务自动注入历史经验,越用越聪明。' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const TIPS_KUBERNETES = [
|
|
||||||
{ icon: Cloud, title: 'Kubernetes 查询', color: 'text-sky-400',
|
|
||||||
desc: 'kubernetes_get 统一查询节点/Pod/Deployment/事件等;pod_logs 拉取容器日志。' },
|
|
||||||
{ icon: Terminal, title: 'Pod 执行', color: 'dg-text-purple',
|
|
||||||
desc: '选定 Pod 后可用 pod_execute 进入容器排障;kubernetes_action 执行重启/扩缩容等变更。' },
|
|
||||||
{ icon: Shield, title: '安全 + ReAct', color: 'dg-text-yellow',
|
|
||||||
desc: '只读工具自动执行;变更与危险命令弹框确认,最多 16 轮循环排障。' },
|
|
||||||
{ icon: Database, title: '自进化引擎', color: 'dg-text-green',
|
|
||||||
desc: '按集群/命名空间分桶学习,同类 Kubernetes 排障任务越用越准。' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 安全模式说明(灯泡 Popover 展示) // Logan.Li
|
|
||||||
const SAFETY_MODES = [
|
|
||||||
{
|
|
||||||
value: 'strict',
|
|
||||||
label: '严格',
|
|
||||||
short: '中风险+确认',
|
|
||||||
desc: 'chmod、文件修改、服务重启等中风险及以上命令均需弹框确认。适合生产环境或不熟悉的主机。',
|
|
||||||
examples: 'chmod、mv、systemctl restart',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'balanced',
|
|
||||||
label: '均衡',
|
|
||||||
short: '高风险+确认',
|
|
||||||
desc: '日常排障命令(查看日志、ps、df)自动执行;删除文件、重启服务等高风险操作才需确认。推荐日常使用。',
|
|
||||||
examples: 'cat/grep/df 自动 · rm/reboot 确认',
|
|
||||||
recommended: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'permissive',
|
|
||||||
label: '宽松',
|
|
||||||
short: '仅拦截危险',
|
|
||||||
desc: '除 rm -rf /、mkfs 等危险命令硬性拦截外,其余全部自动执行。仅适合隔离测试环境。',
|
|
||||||
examples: 'rm -rf / 拦截 · 其余自动',
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// formatServerLabel 主机下拉展示文案 // Logan.Li
|
|
||||||
function formatServerLabel(s: { hostname: string; ip: string }) {
|
|
||||||
return `${s.hostname}(${s.ip})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AgentCLI() {
|
export default function AgentCLI() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -136,14 +58,8 @@ export default function AgentCLI() {
|
|||||||
setItems((prev) => [...prev, { ...it, id: nextId() }]);
|
setItems((prev) => [...prev, { ...it, id: nextId() }]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const currentSafety = SAFETY_MODES.find((m) => m.value === safetyMode) ?? SAFETY_MODES[1];
|
|
||||||
const currentTips = cliMode === 'kubernetes' ? TIPS_KUBERNETES : TIPS_HOST;
|
|
||||||
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);
|
||||||
const filteredPods = pods.filter((p) => {
|
|
||||||
const q = podSearch.trim().toLowerCase();
|
|
||||||
return !q || p.name.toLowerCase().includes(q);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载主机列表 // Logan.Li
|
// 加载主机列表 // Logan.Li
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -473,7 +389,6 @@ export default function AgentCLI() {
|
|||||||
setTokens(0);
|
setTokens(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedServer = servers.find((s) => String(s.id) === serverId);
|
|
||||||
const canResumeView = viewSession && ['done', 'aborted', 'error'].includes(viewSession.status);
|
const canResumeView = viewSession && ['done', 'aborted', 'error'].includes(viewSession.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -485,203 +400,34 @@ export default function AgentCLI() {
|
|||||||
className="h-full"
|
className="h-full"
|
||||||
contentClassName="h-full flex flex-col min-h-0 gap-2"
|
contentClassName="h-full flex flex-col min-h-0 gap-2"
|
||||||
>
|
>
|
||||||
{/* 工作区控制栏:主机 + 安全模式 + 状态,不挤占对话区宽度 // Logan.Li */}
|
<AgentCLIToolbar
|
||||||
<div className="flex items-center gap-2 flex-wrap shrink-0 px-0.5">
|
cliMode={cliMode}
|
||||||
<div className="flex rounded-md border border-[var(--dg-border)] overflow-hidden h-8 shrink-0">
|
setCliMode={setCliMode}
|
||||||
<button
|
servers={servers}
|
||||||
type="button"
|
serverId={serverId}
|
||||||
className={`px-2.5 text-xs flex items-center gap-1 ${cliMode === 'host' ? 'bg-blue-500/15 dg-text-blue' : 'dg-text-secondary hover:bg-[var(--dg-bg-hover)]'}`}
|
setServerId={setServerId}
|
||||||
onClick={() => setCliMode('host')}
|
clusters={clusters}
|
||||||
>
|
clusterCode={clusterCode}
|
||||||
<Server className="w-3.5 h-3.5" />主机
|
setClusterCode={setClusterCode}
|
||||||
</button>
|
namespaces={namespaces}
|
||||||
<button
|
namespace={namespace}
|
||||||
type="button"
|
setNamespace={setNamespace}
|
||||||
className={`px-3 text-xs flex items-center gap-1 border-l border-[var(--dg-border)] ${cliMode === 'kubernetes' ? 'bg-blue-500/15 dg-text-blue' : 'dg-text-secondary hover:bg-[var(--dg-bg-hover)]'}`}
|
pods={pods}
|
||||||
onClick={() => setCliMode('kubernetes')}
|
podName={podName}
|
||||||
>
|
setPodName={setPodName}
|
||||||
<Boxes className="w-3.5 h-3.5 shrink-0" />Kubernetes
|
podSearch={podSearch}
|
||||||
</button>
|
setPodSearch={setPodSearch}
|
||||||
</div>
|
safetyMode={safetyMode}
|
||||||
|
setSafetyMode={setSafetyMode}
|
||||||
{cliMode === 'host' ? (
|
connected={connected}
|
||||||
<Select value={serverId} onValueChange={setServerId}>
|
viewMode={viewMode}
|
||||||
<SelectTrigger className="w-80 max-w-[22rem] h-8 text-xs dg-input min-w-0">
|
canContinue={canContinue}
|
||||||
<Server className="w-3.5 h-3.5 mr-1.5 dg-text-secondary shrink-0" />
|
running={running}
|
||||||
<SelectValue placeholder="选择目标主机" className="truncate">
|
round={round}
|
||||||
{selectedServer ? formatServerLabel(selectedServer) : undefined}
|
tokens={tokens}
|
||||||
</SelectValue>
|
historyOpen={historyOpen}
|
||||||
</SelectTrigger>
|
setHistoryOpen={setHistoryOpen}
|
||||||
<SelectContent className="dg-input max-h-72 min-w-[var(--radix-select-trigger-width)]">
|
|
||||||
{servers.map((s) => (
|
|
||||||
<SelectItem key={s.id} value={String(s.id)} title={formatServerLabel(s)}>
|
|
||||||
<span className="truncate block max-w-[20rem]">{formatServerLabel(s)}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Select value={clusterCode} onValueChange={setClusterCode}>
|
|
||||||
<SelectTrigger className="w-44 h-8 text-xs dg-input">
|
|
||||||
<Cloud className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
|
||||||
<SelectValue placeholder="集群" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="dg-input max-h-72">
|
|
||||||
{clusters.map((c) => (
|
|
||||||
<SelectItem key={c.id} value={c.code}>{c.name}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select value={namespace} onValueChange={setNamespace}>
|
|
||||||
<SelectTrigger className="w-36 h-8 text-xs dg-input">
|
|
||||||
<SelectValue placeholder="命名空间" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="dg-input max-h-72">
|
|
||||||
{namespaces.map((ns) => (
|
|
||||||
<SelectItem key={ns} value={ns}>{ns}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="outline" className="h-8 text-xs dg-input min-w-[10rem] justify-start font-normal">
|
|
||||||
<Terminal className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
|
||||||
{podName === POD_NONE ? 'Pod(可选)' : podName}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent align="start" className="w-80 dg-input p-2 space-y-2">
|
|
||||||
<Input
|
|
||||||
value={podSearch}
|
|
||||||
onChange={(e) => setPodSearch(e.target.value)}
|
|
||||||
placeholder="搜索 Pod 名称"
|
|
||||||
className="h-8 text-xs dg-input"
|
|
||||||
/>
|
/>
|
||||||
<div className="max-h-56 overflow-y-auto space-y-0.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`w-full text-left text-xs px-2 py-1.5 rounded ${podName === POD_NONE ? 'bg-blue-500/15' : 'hover:bg-[var(--dg-bg-hover)]'}`}
|
|
||||||
onClick={() => setPodName(POD_NONE)}
|
|
||||||
>
|
|
||||||
不选定 Pod(集群/NS 级任务)
|
|
||||||
</button>
|
|
||||||
{filteredPods.map((p) => (
|
|
||||||
<button
|
|
||||||
key={p.name}
|
|
||||||
type="button"
|
|
||||||
className={`w-full text-left text-xs px-2 py-1.5 rounded ${podName === p.name ? 'bg-blue-500/15' : 'hover:bg-[var(--dg-bg-hover)]'}`}
|
|
||||||
onClick={() => setPodName(p.name)}
|
|
||||||
>
|
|
||||||
<div className="font-medium truncate">{p.name}</div>
|
|
||||||
<div className="text-[10px] dg-text-muted truncate">{p.status} · restarts {p.restarts}</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{filteredPods.length === 0 && (
|
|
||||||
<div className="text-[10px] dg-text-muted text-center py-2">无匹配 Pod</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Select value={safetyMode} onValueChange={setSafetyMode}>
|
|
||||||
<SelectTrigger className="w-24 h-8 text-xs dg-input">
|
|
||||||
<Shield className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="dg-input">
|
|
||||||
{SAFETY_MODES.map((m) => (
|
|
||||||
<SelectItem key={m.value} value={m.value}>{m.label}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-yellow hover:bg-[var(--dg-bg-hover)]" title="安全模式说明">
|
|
||||||
<Lightbulb className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent align="start" className="w-80 dg-input p-3 space-y-2.5">
|
|
||||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">安全模式说明</div>
|
|
||||||
<p className="text-[11px] dg-text-muted leading-relaxed">
|
|
||||||
控制 AI 执行远程命令前是否需要你确认。危险命令(如 rm -rf /)任何模式均硬性拦截。
|
|
||||||
</p>
|
|
||||||
{SAFETY_MODES.map((m) => (
|
|
||||||
<div
|
|
||||||
key={m.value}
|
|
||||||
className={`rounded-md p-2.5 border text-xs ${
|
|
||||||
m.value === safetyMode
|
|
||||||
? 'border-blue-500/40 bg-blue-500/10'
|
|
||||||
: 'border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-medium text-[var(--dg-text-primary)]">{m.label}</span>
|
|
||||||
<span className="text-[10px] dg-text-muted">{m.short}</span>
|
|
||||||
{'recommended' in m && m.recommended && (
|
|
||||||
<Badge className="text-[9px] px-1 py-0 bg-violet-500/15 dg-text-purple border-violet-500/30">推荐</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="dg-text-secondary leading-relaxed mt-1">{m.desc}</p>
|
|
||||||
<p className="text-[10px] dg-text-muted mt-1">例:{m.examples}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-2 text-xs dg-text-secondary"
|
|
||||||
onClick={() => setHistoryOpen((v) => !v)}
|
|
||||||
title={historyOpen ? '收起历史任务' : '展开历史任务'}
|
|
||||||
>
|
|
||||||
{historyOpen ? <PanelLeftClose className="w-3.5 h-3.5 mr-1" /> : <History className="w-3.5 h-3.5 mr-1" />}
|
|
||||||
历史
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-[1rem]" />
|
|
||||||
|
|
||||||
{connected && (
|
|
||||||
<Badge className="h-6 text-[10px] bg-emerald-500/15 dg-text-green border-emerald-500/30">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mr-1" />已连接
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{viewMode && (
|
|
||||||
<Badge className="h-6 text-[10px] bg-amber-500/15 dg-text-yellow border-amber-500/30">查看记录</Badge>
|
|
||||||
)}
|
|
||||||
{canContinue && !running && !viewMode && (
|
|
||||||
<Badge className="h-6 text-[10px] bg-violet-500/15 dg-text-purple border-violet-500/30">可追问</Badge>
|
|
||||||
)}
|
|
||||||
{running && (
|
|
||||||
<Badge className="h-6 text-[10px] bg-blue-500/15 dg-text-blue border-blue-500/30">
|
|
||||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />第 {round} 轮 · {tokens}t
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-muted hover:dg-text-secondary" title="核心能力说明">
|
|
||||||
<Lightbulb className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent align="end" className="w-80 dg-input p-3 space-y-2">
|
|
||||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">核心能力</div>
|
|
||||||
{currentTips.map((t) => (
|
|
||||||
<div key={t.title} className="flex gap-2 p-2 rounded-md bg-[var(--dg-bg-tertiary)]/40 border border-[var(--dg-border)]">
|
|
||||||
<t.icon className={`w-4 h-4 mt-0.5 shrink-0 ${t.color}`} />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-medium text-[var(--dg-text-primary)]">{t.title}</div>
|
|
||||||
<div className="text-[10px] dg-text-secondary leading-relaxed mt-0.5">{t.desc}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主工作区:可折叠历史 + 对话区全宽 // Logan.Li */}
|
{/* 主工作区:可折叠历史 + 对话区全宽 // Logan.Li */}
|
||||||
<div className="flex flex-1 min-h-0 gap-2">
|
<div className="flex flex-1 min-h-0 gap-2">
|
||||||
@@ -721,62 +467,23 @@ export default function AgentCLI() {
|
|||||||
className="flex-1 min-h-0 overflow-y-auto p-4 space-y-3"
|
className="flex-1 min-h-0 overflow-y-auto p-4 space-y-3"
|
||||||
>
|
>
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<div className="h-full flex flex-col items-center justify-center dg-text-muted gap-3">
|
<AgentCLIEmptyState
|
||||||
<Terminal className="w-10 h-10 opacity-40" />
|
cliMode={cliMode}
|
||||||
{!wsReady ? (
|
wsReady={wsReady}
|
||||||
<>
|
connected={connected}
|
||||||
<div className="text-sm">
|
safetyMode={safetyMode}
|
||||||
{cliMode === 'kubernetes' ? '请先选择集群和命名空间' : '请先选择一台目标主机'}
|
podName={podName}
|
||||||
</div>
|
/>
|
||||||
</>
|
|
||||||
) : !connected ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-6 h-6 animate-spin dg-text-blue" />
|
|
||||||
<div className="text-sm">{cliMode === 'kubernetes' ? '正在连接集群...' : '正在连接主机...'}</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-center max-w-md">
|
|
||||||
{cliMode === 'kubernetes'
|
|
||||||
? '描述 Kubernetes 排障任务,例如「查看 NotReady 节点」或「分析 nginx Pod 重启原因」'
|
|
||||||
: '描述你要执行的任务,例如「检查磁盘使用率并清理大文件」'}
|
|
||||||
<div className="text-xs dg-text-muted mt-2">
|
|
||||||
当前安全模式:{currentSafety.label}({currentSafety.short})
|
|
||||||
{cliMode === 'kubernetes' && podName === POD_NONE && ' · 未选 Pod,仅集群/NS 级查询'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<AgentChatTimeline items={items} activeRound={round} />
|
<AgentChatTimeline items={items} activeRound={round} />
|
||||||
</div>
|
</div>
|
||||||
{items.length > 0 && (!scrollAtTop || !scrollAtBottom) && (
|
<ScrollButtons
|
||||||
<div className="absolute right-3 bottom-3 z-10 flex flex-col gap-1.5">
|
show={items.length > 0}
|
||||||
{!scrollAtTop && (
|
atTop={scrollAtTop}
|
||||||
<Button
|
atBottom={scrollAtBottom}
|
||||||
type="button"
|
onScrollTop={scrollToTop}
|
||||||
variant="secondary"
|
onScrollBottom={scrollToBottom}
|
||||||
size="icon"
|
/>
|
||||||
className="h-8 w-8 rounded-full shadow-md border border-[var(--dg-border)] bg-[var(--surface-raised)]/95 opacity-90 hover:opacity-100"
|
|
||||||
onClick={scrollToTop}
|
|
||||||
title="回到顶部"
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!scrollAtBottom && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 rounded-full shadow-md border border-[var(--dg-border)] bg-[var(--surface-raised)]/95 opacity-90 hover:opacity-100"
|
|
||||||
onClick={scrollToBottom}
|
|
||||||
title="回到底部"
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加宽输入区:多行 + 独立操作栏 // Logan.Li */}
|
{/* 加宽输入区:多行 + 独立操作栏 // Logan.Li */}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// AgentCLIEmptyState.tsx — AgentCLI 对话区空状态(未选目标/连接中/就绪提示)
|
||||||
|
import { Terminal, Loader2 } from 'lucide-react';
|
||||||
|
import type { AgentCliMode } from '@/api/aiops/agent-cli';
|
||||||
|
import { POD_NONE, SAFETY_MODES } from './agent-cli-constants';
|
||||||
|
|
||||||
|
export interface AgentCLIEmptyStateProps {
|
||||||
|
cliMode: AgentCliMode;
|
||||||
|
wsReady: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
safetyMode: string;
|
||||||
|
podName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentCLIEmptyState({ cliMode, wsReady, connected, safetyMode, podName }: AgentCLIEmptyStateProps) {
|
||||||
|
const currentSafety = SAFETY_MODES.find((m) => m.value === safetyMode) ?? SAFETY_MODES[1];
|
||||||
|
const isK8s = cliMode === 'kubernetes';
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center dg-text-muted gap-3">
|
||||||
|
<Terminal className="w-10 h-10 opacity-40" />
|
||||||
|
{!wsReady ? (
|
||||||
|
<div className="text-sm">
|
||||||
|
{isK8s ? '请先选择集群和命名空间' : '请先选择一台目标主机'}
|
||||||
|
</div>
|
||||||
|
) : !connected ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin dg-text-blue" />
|
||||||
|
<div className="text-sm">{isK8s ? '正在连接集群...' : '正在连接主机...'}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-center max-w-md">
|
||||||
|
{isK8s
|
||||||
|
? '描述 Kubernetes 排障任务,例如「查看 NotReady 节点」或「分析 nginx Pod 重启原因」'
|
||||||
|
: '描述你要执行的任务,例如「检查磁盘使用率并清理大文件」'}
|
||||||
|
<div className="text-xs dg-text-muted mt-2">
|
||||||
|
当前安全模式:{currentSafety.label}({currentSafety.short})
|
||||||
|
{isK8s && podName === POD_NONE && ' · 未选 Pod,仅集群/NS 级查询'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
// AgentCLIToolbar.tsx — AgentCLI 工作区控制栏(从 AgentCLI.tsx 提取)
|
||||||
|
// 包含:模式切换(主机/K8s) + 目标选择(主机/集群/NS/Pod) + 安全模式 + 状态徽章 + 核心能力说明
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Terminal, Shield, Lightbulb, Server,
|
||||||
|
PanelLeftClose, History, Boxes, Cloud, Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { AgentCliMode } from '@/api/aiops/agent-cli';
|
||||||
|
import {
|
||||||
|
POD_NONE, TIPS_HOST, TIPS_KUBERNETES, SAFETY_MODES, formatServerLabel,
|
||||||
|
type TipItem,
|
||||||
|
} from './agent-cli-constants';
|
||||||
|
|
||||||
|
interface PodInfo { name: string; status: string; image: string; restarts: number }
|
||||||
|
interface ServerInfo { id: number; hostname: string; ip: string }
|
||||||
|
interface ClusterInfo { id: number; name: string; code: string }
|
||||||
|
|
||||||
|
export interface AgentCLIToolbarProps {
|
||||||
|
cliMode: AgentCliMode;
|
||||||
|
setCliMode: (m: AgentCliMode) => void;
|
||||||
|
// 主机模式
|
||||||
|
servers: ServerInfo[];
|
||||||
|
serverId: string;
|
||||||
|
setServerId: (v: string) => void;
|
||||||
|
// Kubernetes 模式
|
||||||
|
clusters: ClusterInfo[];
|
||||||
|
clusterCode: string;
|
||||||
|
setClusterCode: (v: string) => void;
|
||||||
|
namespaces: string[];
|
||||||
|
namespace: string;
|
||||||
|
setNamespace: (v: string) => void;
|
||||||
|
pods: PodInfo[];
|
||||||
|
podName: string;
|
||||||
|
setPodName: (v: string) => void;
|
||||||
|
podSearch: string;
|
||||||
|
setPodSearch: (v: string) => void;
|
||||||
|
// 安全模式
|
||||||
|
safetyMode: string;
|
||||||
|
setSafetyMode: (v: string) => void;
|
||||||
|
// 状态
|
||||||
|
connected: boolean;
|
||||||
|
viewMode: boolean;
|
||||||
|
canContinue: boolean;
|
||||||
|
running: boolean;
|
||||||
|
round: number;
|
||||||
|
tokens: number;
|
||||||
|
historyOpen: boolean;
|
||||||
|
setHistoryOpen: (fn: (v: boolean) => boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentCLIToolbar(props: AgentCLIToolbarProps) {
|
||||||
|
const {
|
||||||
|
cliMode, setCliMode,
|
||||||
|
servers, serverId, setServerId,
|
||||||
|
clusters, clusterCode, setClusterCode,
|
||||||
|
namespaces, namespace, setNamespace,
|
||||||
|
pods, podName, setPodName, podSearch, setPodSearch,
|
||||||
|
safetyMode, setSafetyMode,
|
||||||
|
connected, viewMode, canContinue, running, round, tokens,
|
||||||
|
historyOpen, setHistoryOpen,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const currentTips: TipItem[] = cliMode === 'kubernetes' ? TIPS_KUBERNETES : TIPS_HOST;
|
||||||
|
const selectedServer = servers.find((s) => String(s.id) === serverId);
|
||||||
|
const filteredPods = pods.filter((p) => {
|
||||||
|
const q = podSearch.trim().toLowerCase();
|
||||||
|
return !q || p.name.toLowerCase().includes(q);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap shrink-0 px-0.5">
|
||||||
|
{/* 模式切换 */}
|
||||||
|
<div className="flex rounded-md border border-[var(--dg-border)] overflow-hidden h-8 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-2.5 text-xs flex items-center gap-1 ${cliMode === 'host' ? 'bg-blue-500/15 dg-text-blue' : 'dg-text-secondary hover:bg-[var(--dg-bg-hover)]'}`}
|
||||||
|
onClick={() => setCliMode('host')}
|
||||||
|
>
|
||||||
|
<Server className="w-3.5 h-3.5" />主机
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-3 text-xs flex items-center gap-1 border-l border-[var(--dg-border)] ${cliMode === 'kubernetes' ? 'bg-blue-500/15 dg-text-blue' : 'dg-text-secondary hover:bg-[var(--dg-bg-hover)]'}`}
|
||||||
|
onClick={() => setCliMode('kubernetes')}
|
||||||
|
>
|
||||||
|
<Boxes className="w-3.5 h-3.5 shrink-0" />Kubernetes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 目标选择 */}
|
||||||
|
{cliMode === 'host' ? (
|
||||||
|
<Select value={serverId} onValueChange={setServerId}>
|
||||||
|
<SelectTrigger className="w-80 max-w-[22rem] h-8 text-xs dg-input min-w-0">
|
||||||
|
<Server className="w-3.5 h-3.5 mr-1.5 dg-text-secondary shrink-0" />
|
||||||
|
<SelectValue placeholder="选择目标主机" className="truncate">
|
||||||
|
{selectedServer ? formatServerLabel(selectedServer) : undefined}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="dg-input max-h-72 min-w-[var(--radix-select-trigger-width)]">
|
||||||
|
{servers.map((s) => (
|
||||||
|
<SelectItem key={s.id} value={String(s.id)} title={formatServerLabel(s)}>
|
||||||
|
<span className="truncate block max-w-[20rem]">{formatServerLabel(s)}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Select value={clusterCode} onValueChange={setClusterCode}>
|
||||||
|
<SelectTrigger className="w-44 h-8 text-xs dg-input">
|
||||||
|
<Cloud className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
||||||
|
<SelectValue placeholder="集群" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="dg-input max-h-72">
|
||||||
|
{clusters.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.code}>{c.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={namespace} onValueChange={setNamespace}>
|
||||||
|
<SelectTrigger className="w-36 h-8 text-xs dg-input">
|
||||||
|
<SelectValue placeholder="命名空间" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="dg-input max-h-72">
|
||||||
|
{namespaces.map((ns) => (
|
||||||
|
<SelectItem key={ns} value={ns}>{ns}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{/* Pod 选择 Popover */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="h-8 text-xs dg-input min-w-[10rem] justify-start font-normal">
|
||||||
|
<Terminal className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
||||||
|
{podName === POD_NONE ? 'Pod(可选)' : podName}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="start" className="w-80 dg-input p-2 space-y-2">
|
||||||
|
<Input
|
||||||
|
value={podSearch}
|
||||||
|
onChange={(e) => setPodSearch(e.target.value)}
|
||||||
|
placeholder="搜索 Pod 名称"
|
||||||
|
className="h-8 text-xs dg-input"
|
||||||
|
/>
|
||||||
|
<div className="max-h-56 overflow-y-auto space-y-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-full text-left text-xs px-2 py-1.5 rounded ${podName === POD_NONE ? 'bg-blue-500/15' : 'hover:bg-[var(--dg-bg-hover)]'}`}
|
||||||
|
onClick={() => setPodName(POD_NONE)}
|
||||||
|
>
|
||||||
|
不选定 Pod(集群/NS 级任务)
|
||||||
|
</button>
|
||||||
|
{filteredPods.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.name}
|
||||||
|
type="button"
|
||||||
|
className={`w-full text-left text-xs px-2 py-1.5 rounded ${podName === p.name ? 'bg-blue-500/15' : 'hover:bg-[var(--dg-bg-hover)]'}`}
|
||||||
|
onClick={() => setPodName(p.name)}
|
||||||
|
>
|
||||||
|
<div className="font-medium truncate">{p.name}</div>
|
||||||
|
<div className="text-[10px] dg-text-muted truncate">{p.status} · restarts {p.restarts}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{filteredPods.length === 0 && (
|
||||||
|
<div className="text-[10px] dg-text-muted text-center py-2">无匹配 Pod</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 安全模式选择 + 说明 */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Select value={safetyMode} onValueChange={setSafetyMode}>
|
||||||
|
<SelectTrigger className="w-24 h-8 text-xs dg-input">
|
||||||
|
<Shield className="w-3.5 h-3.5 mr-1 dg-text-secondary shrink-0" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="dg-input">
|
||||||
|
{SAFETY_MODES.map((m) => (
|
||||||
|
<SelectItem key={m.value} value={m.value}>{m.label}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-yellow hover:bg-[var(--dg-bg-hover)]" title="安全模式说明">
|
||||||
|
<Lightbulb className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="start" className="w-80 dg-input p-3 space-y-2.5">
|
||||||
|
<div className="text-sm font-medium text-[var(--dg-text-primary)]">安全模式说明</div>
|
||||||
|
<p className="text-[11px] dg-text-muted leading-relaxed">
|
||||||
|
控制 AI 执行远程命令前是否需要你确认。危险命令(如 rm -rf /)任何模式均硬性拦截。
|
||||||
|
</p>
|
||||||
|
{SAFETY_MODES.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.value}
|
||||||
|
className={`rounded-md p-2.5 border text-xs ${
|
||||||
|
m.value === safetyMode
|
||||||
|
? 'border-blue-500/40 bg-blue-500/10'
|
||||||
|
: 'border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-[var(--dg-text-primary)]">{m.label}</span>
|
||||||
|
<span className="text-[10px] dg-text-muted">{m.short}</span>
|
||||||
|
{'recommended' in m && m.recommended && (
|
||||||
|
<Badge className="text-[9px] px-1 py-0 bg-violet-500/15 dg-text-purple border-violet-500/30">推荐</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="dg-text-secondary leading-relaxed mt-1">{m.desc}</p>
|
||||||
|
<p className="text-[10px] dg-text-muted mt-1">例:{m.examples}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 历史折叠 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2 text-xs dg-text-secondary"
|
||||||
|
onClick={() => setHistoryOpen((v) => !v)}
|
||||||
|
title={historyOpen ? '收起历史任务' : '展开历史任务'}
|
||||||
|
>
|
||||||
|
{historyOpen ? <PanelLeftClose className="w-3.5 h-3.5 mr-1" /> : <History className="w-3.5 h-3.5 mr-1" />}
|
||||||
|
历史
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-[1rem]" />
|
||||||
|
|
||||||
|
{/* 状态徽章 */}
|
||||||
|
{connected && (
|
||||||
|
<Badge className="h-6 text-[10px] bg-emerald-500/15 dg-text-green border-emerald-500/30">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mr-1" />已连接
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{viewMode && (
|
||||||
|
<Badge className="h-6 text-[10px] bg-amber-500/15 dg-text-yellow border-amber-500/30">查看记录</Badge>
|
||||||
|
)}
|
||||||
|
{canContinue && !running && !viewMode && (
|
||||||
|
<Badge className="h-6 text-[10px] bg-violet-500/15 dg-text-purple border-violet-500/30">可追问</Badge>
|
||||||
|
)}
|
||||||
|
{running && (
|
||||||
|
<Badge className="h-6 text-[10px] bg-blue-500/15 dg-text-blue border-blue-500/30">
|
||||||
|
<Loader2 className="w-3 h-3 mr-1 animate-spin" />第 {round} 轮 · {tokens}t
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 核心能力说明 Popover */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-muted hover:dg-text-secondary" title="核心能力说明">
|
||||||
|
<Lightbulb className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-80 dg-input p-3 space-y-2">
|
||||||
|
<div className="text-sm font-medium text-[var(--dg-text-primary)]">核心能力</div>
|
||||||
|
{currentTips.map((t) => (
|
||||||
|
<div key={t.title} className="flex gap-2 p-2 rounded-md bg-[var(--dg-bg-tertiary)]/40 border border-[var(--dg-border)]">
|
||||||
|
<t.icon className={`w-4 h-4 mt-0.5 shrink-0 ${t.color}`} />
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-[var(--dg-text-primary)]">{t.title}</div>
|
||||||
|
<div className="text-[10px] dg-text-secondary leading-relaxed mt-0.5">{t.desc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// agent-cli-constants.ts — AgentCLI 静态配置/类型(从 AgentCLI.tsx 提取)
|
||||||
|
// 纯数据:WS 事件接口、安全模式定义、核心能力说明、Pod 占位符、主机标签格式化
|
||||||
|
import {
|
||||||
|
Cpu, Shield, Brain, Database, Terminal, Cloud,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// WS 事件类型(后端 wsEvent 对应)
|
||||||
|
export interface WsEvent {
|
||||||
|
type: string;
|
||||||
|
round?: number;
|
||||||
|
session_id?: number;
|
||||||
|
thought?: string;
|
||||||
|
tool_name?: string;
|
||||||
|
tool_args?: Record<string, unknown>;
|
||||||
|
risk?: string;
|
||||||
|
output?: string;
|
||||||
|
success?: boolean;
|
||||||
|
duration?: string;
|
||||||
|
tokens?: number;
|
||||||
|
answer?: string;
|
||||||
|
message?: string;
|
||||||
|
confirm_id?: string;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pod 占位符:未选定 Pod 时使用
|
||||||
|
export const POD_NONE = '__none__';
|
||||||
|
|
||||||
|
export interface TipItem {
|
||||||
|
icon: typeof Cpu;
|
||||||
|
title: string;
|
||||||
|
color: string;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 四大核心模块说明(灯泡 Popover)— 主机模式
|
||||||
|
export const TIPS_HOST: TipItem[] = [
|
||||||
|
{ icon: Cpu, title: 'LLM + 工具层', color: 'text-sky-400',
|
||||||
|
desc: 'OpenAI 兼容 LLM;工具含 ssh_execute(远程执行)与 analyze_output(输出分析)。' },
|
||||||
|
{ icon: Shield, title: '安全 + Prompt', color: 'dg-text-yellow',
|
||||||
|
desc: '五级风险评估 + 三级安全模式;危险命令硬性拒绝,高危命令弹框确认。' },
|
||||||
|
{ icon: Brain, title: 'ReAct 核心引擎', color: 'dg-text-purple',
|
||||||
|
desc: '思考→行动→观察循环(最多 16 轮),失败换法重试,循环检测防死循环。' },
|
||||||
|
{ icon: Database, title: '自进化引擎', color: 'dg-text-green',
|
||||||
|
desc: '四层记忆异步学习,同类任务自动注入历史经验,越用越聪明。' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 四大核心模块说明(灯泡 Popover)— Kubernetes 模式
|
||||||
|
export const TIPS_KUBERNETES: TipItem[] = [
|
||||||
|
{ icon: Cloud, title: 'Kubernetes 查询', color: 'text-sky-400',
|
||||||
|
desc: 'kubernetes_get 统一查询节点/Pod/Deployment/事件等;pod_logs 拉取容器日志。' },
|
||||||
|
{ icon: Terminal, title: 'Pod 执行', color: 'dg-text-purple',
|
||||||
|
desc: '选定 Pod 后可用 pod_execute 进入容器排障;kubernetes_action 执行重启/扩缩容等变更。' },
|
||||||
|
{ icon: Shield, title: '安全 + ReAct', color: 'dg-text-yellow',
|
||||||
|
desc: '只读工具自动执行;变更与危险命令弹框确认,最多 16 轮循环排障。' },
|
||||||
|
{ icon: Database, title: '自进化引擎', color: 'dg-text-green',
|
||||||
|
desc: '按集群/命名空间分桶学习,同类 Kubernetes 排障任务越用越准。' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 安全模式说明(灯泡 Popover 展示)
|
||||||
|
export const SAFETY_MODES = [
|
||||||
|
{
|
||||||
|
value: 'strict',
|
||||||
|
label: '严格',
|
||||||
|
short: '中风险+确认',
|
||||||
|
desc: 'chmod、文件修改、服务重启等中风险及以上命令均需弹框确认。适合生产环境或不熟悉的主机。',
|
||||||
|
examples: 'chmod、mv、systemctl restart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'balanced',
|
||||||
|
label: '均衡',
|
||||||
|
short: '高风险+确认',
|
||||||
|
desc: '日常排障命令(查看日志、ps、df)自动执行;删除文件、重启服务等高风险操作才需确认。推荐日常使用。',
|
||||||
|
examples: 'cat/grep/df 自动 · rm/reboot 确认',
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'permissive',
|
||||||
|
label: '宽松',
|
||||||
|
short: '仅拦截危险',
|
||||||
|
desc: '除 rm -rf /、mkfs 等危险命令硬性拦截外,其余全部自动执行。仅适合隔离测试环境。',
|
||||||
|
examples: 'rm -rf / 拦截 · 其余自动',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// formatServerLabel 主机下拉展示文案
|
||||||
|
export function formatServerLabel(s: { hostname: string; ip: string }) {
|
||||||
|
return `${s.hostname}(${s.ip})`;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user