agent-cli
This commit is contained in:
1 parent
e0d652d211
commit
0690285147
4 files changed
+192
-51
No files matched your search
@@ -27,6 +27,8 @@ import { PageShell } from '@/components/layout/PageShell';
|
||||
// WS 事件类型(后端 wsEvent 对应) // Logan.Li
|
||||
interface WsEvent {
|
||||
type: string;
|
||||
phase?: string;
|
||||
summary?: string;
|
||||
round?: number;
|
||||
session_id?: number;
|
||||
thought?: string;
|
||||
@@ -36,6 +38,7 @@ interface WsEvent {
|
||||
output?: string;
|
||||
success?: boolean;
|
||||
duration?: string;
|
||||
output_size?: number;
|
||||
tokens?: number;
|
||||
answer?: string;
|
||||
message?: string;
|
||||
@@ -99,6 +102,27 @@ function formatServerLabel(s: { hostname: string; ip: string }) {
|
||||
return `${s.hostname}(${s.ip})`;
|
||||
}
|
||||
|
||||
function formatAgentPhase(phase: string) {
|
||||
const map: Record<string, string> = {
|
||||
idle: '空闲',
|
||||
accepted: '已接收',
|
||||
context: '加载上下文',
|
||||
thinking: '思考中',
|
||||
planning: '规划中',
|
||||
safety_check: '安全检查',
|
||||
approval_required: '等待审批',
|
||||
tool_running: '执行工具',
|
||||
observing: '观察结果',
|
||||
validating: '验证中',
|
||||
summarizing: '总结中',
|
||||
done: '已完成',
|
||||
blocked: '已阻断',
|
||||
error: '失败',
|
||||
aborted: '已中断',
|
||||
};
|
||||
return map[phase] || phase || '运行中';
|
||||
}
|
||||
|
||||
export default function AgentCLI() {
|
||||
const confirm = useConfirm();
|
||||
|
||||
@@ -116,6 +140,7 @@ export default function AgentCLI() {
|
||||
const [items, setItems] = useState<ChatItem[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [phase, setPhase] = useState('idle');
|
||||
const [round, setRound] = useState(0);
|
||||
const [tokens, setTokens] = useState(0);
|
||||
const [connected, setConnected] = useState(false);
|
||||
@@ -209,7 +234,7 @@ export default function AgentCLI() {
|
||||
if (cliMode === 'host' && !serverId) return;
|
||||
if (cliMode === 'kubernetes' && (!clusterCode || !namespace)) return;
|
||||
setItems([]);
|
||||
setRound(0); setTokens(0); setRunning(false);
|
||||
setRound(0); setTokens(0); setRunning(false); setPhase('idle');
|
||||
setConnected(false);
|
||||
setViewMode(false);
|
||||
setViewSession(null);
|
||||
@@ -242,6 +267,7 @@ export default function AgentCLI() {
|
||||
window.clearTimeout(connectTimer);
|
||||
setConnected(false);
|
||||
setRunning(false);
|
||||
setPhase('idle');
|
||||
if (!everConnected) {
|
||||
push({ kind: 'error', content: cliMode === 'kubernetes'
|
||||
? '集群连接被拒绝:请检查集群 kubeconfig/token 配置与大模型是否启用。'
|
||||
@@ -310,11 +336,25 @@ export default function AgentCLI() {
|
||||
setCanContinue(true);
|
||||
}
|
||||
break;
|
||||
case 'status':
|
||||
if (ev.phase) setPhase(ev.phase);
|
||||
if (ev.round) setRound(ev.round);
|
||||
if (ev.tokens) setTokens(ev.tokens);
|
||||
push({
|
||||
kind: 'status',
|
||||
round: ev.round,
|
||||
phase: ev.phase,
|
||||
content: ev.summary || ev.message || ev.phase || '',
|
||||
toolName: ev.tool_name,
|
||||
risk: ev.risk,
|
||||
});
|
||||
break;
|
||||
case 'progress':
|
||||
if (ev.round) setRound(ev.round);
|
||||
if (ev.tokens) setTokens(ev.tokens);
|
||||
break;
|
||||
case 'thought_delta':
|
||||
setPhase('thinking');
|
||||
setItems((prev) => {
|
||||
const next = [...prev];
|
||||
const last = next[next.length - 1];
|
||||
@@ -327,6 +367,7 @@ export default function AgentCLI() {
|
||||
});
|
||||
break;
|
||||
case 'thought':
|
||||
setPhase('planning');
|
||||
setItems((prev) => {
|
||||
const next = [...prev];
|
||||
const last = next[next.length - 1];
|
||||
@@ -339,6 +380,7 @@ export default function AgentCLI() {
|
||||
});
|
||||
break;
|
||||
case 'tool_call': {
|
||||
setPhase(ev.phase || 'tool_running');
|
||||
const args = ev.tool_args || {};
|
||||
push({
|
||||
kind: 'tool', round: ev.round, toolName: ev.tool_name,
|
||||
@@ -347,13 +389,14 @@ export default function AgentCLI() {
|
||||
break;
|
||||
}
|
||||
case 'tool_result':
|
||||
setPhase(ev.phase || 'observing');
|
||||
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,
|
||||
duration: ev.duration, outputSize: ev.output_size, pending: false,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -362,9 +405,11 @@ export default function AgentCLI() {
|
||||
});
|
||||
break;
|
||||
case 'confirm':
|
||||
setPhase(ev.phase || 'approval_required');
|
||||
handleConfirm(ev);
|
||||
break;
|
||||
case 'done': {
|
||||
setPhase('done');
|
||||
const text = (ev.answer || ev.message || '').trim();
|
||||
setItems((prev) => {
|
||||
const norm = (s?: string) => (s || '').trim().replace(/\s+/g, ' ');
|
||||
@@ -394,6 +439,7 @@ export default function AgentCLI() {
|
||||
}
|
||||
case 'error':
|
||||
case 'aborted':
|
||||
setPhase(type === 'aborted' ? 'aborted' : 'error');
|
||||
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
|
||||
setRunning(false);
|
||||
setCanContinue(type !== 'aborted');
|
||||
@@ -433,6 +479,7 @@ export default function AgentCLI() {
|
||||
wsRef.current.send(JSON.stringify({ type: msgType, query: q, safety_mode: safetyMode }));
|
||||
setInput('');
|
||||
setRunning(true);
|
||||
setPhase('accepted');
|
||||
if (msgType === 'start') setCanContinue(false);
|
||||
};
|
||||
|
||||
@@ -451,6 +498,7 @@ export default function AgentCLI() {
|
||||
...replayItems,
|
||||
]);
|
||||
setRunning(false);
|
||||
setPhase('idle');
|
||||
};
|
||||
|
||||
// resumeHistorySession 恢复历史会话上下文,进入可追问状态 // Logan.Li
|
||||
@@ -471,6 +519,7 @@ export default function AgentCLI() {
|
||||
setItems([]);
|
||||
setRound(0);
|
||||
setTokens(0);
|
||||
setPhase('idle');
|
||||
};
|
||||
|
||||
const selectedServer = servers.find((s) => String(s.id) === serverId);
|
||||
@@ -658,7 +707,7 @@ export default function AgentCLI() {
|
||||
)}
|
||||
{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
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />{formatAgentPhase(phase)} · 第 {round} 轮 · {tokens}t
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -812,4 +861,4 @@ export default function AgentCLI() {
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@ import i18n from '@/i18n';
|
||||
// 对话流条目模型 // Logan.Li
|
||||
export interface ChatItem {
|
||||
id: number;
|
||||
kind: 'user' | 'thought' | 'tool' | 'done' | 'error' | 'info';
|
||||
kind: 'user' | 'thought' | 'tool' | 'done' | 'error' | 'info' | 'status';
|
||||
phase?: string;
|
||||
round?: number;
|
||||
content?: string;
|
||||
toolName?: string;
|
||||
@@ -21,6 +22,7 @@ export interface ChatItem {
|
||||
output?: string;
|
||||
success?: boolean;
|
||||
duration?: string;
|
||||
outputSize?: number;
|
||||
pending?: boolean;
|
||||
streaming?: boolean;
|
||||
}
|
||||
@@ -51,6 +53,9 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
||||
case 'thought':
|
||||
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
||||
break;
|
||||
case 'status':
|
||||
items.push({ id: nextId(), kind: 'status', round: m.round, phase: m.duration || undefined, content: m.content, toolName: m.tool_name, risk: m.risk });
|
||||
break;
|
||||
case 'tool_call': {
|
||||
let command = '';
|
||||
try {
|
||||
@@ -124,12 +129,12 @@ function groupChatItems(items: ChatItem[]): TimelineSegment[] {
|
||||
let i = 0;
|
||||
while (i < items.length) {
|
||||
const it = items[i];
|
||||
if (it.round != null && (it.kind === 'thought' || it.kind === 'tool')) {
|
||||
if (it.round != null && (it.kind === 'thought' || it.kind === 'tool' || it.kind === 'status')) {
|
||||
const r = it.round;
|
||||
const group: ChatItem[] = [];
|
||||
while (i < items.length) {
|
||||
const cur = items[i];
|
||||
if (cur.round !== r || (cur.kind !== 'thought' && cur.kind !== 'tool')) break;
|
||||
if (cur.round !== r || (cur.kind !== 'thought' && cur.kind !== 'tool' && cur.kind !== 'status')) break;
|
||||
group.push(cur);
|
||||
i++;
|
||||
}
|
||||
@@ -234,6 +239,9 @@ export function MessageView({ item, inRoundGroup = false }: { item: ChatItem; in
|
||||
if (item.kind === 'tool') {
|
||||
return <ToolBlock item={item} inRoundGroup={inRoundGroup} />;
|
||||
}
|
||||
if (item.kind === 'status') {
|
||||
return <StatusLine item={item} />;
|
||||
}
|
||||
if (item.kind === 'done') {
|
||||
const text = item.content?.trim() || i18n.t('aiops.taskDone');
|
||||
return (
|
||||
@@ -257,6 +265,44 @@ export function MessageView({ item, inRoundGroup = false }: { item: ChatItem; in
|
||||
);
|
||||
}
|
||||
|
||||
function phaseLabel(phase?: string) {
|
||||
const map: Record<string, string> = {
|
||||
accepted: '已接收',
|
||||
context: '上下文',
|
||||
thinking: '思考中',
|
||||
planning: '规划',
|
||||
safety_check: '安全检查',
|
||||
approval_required: '等待审批',
|
||||
tool_running: '工具执行中',
|
||||
observing: '观察结果',
|
||||
validating: '验证',
|
||||
summarizing: '总结',
|
||||
done: '完成',
|
||||
blocked: '已阻断',
|
||||
error: '失败',
|
||||
};
|
||||
return map[phase || ''] || phase || '状态';
|
||||
}
|
||||
|
||||
function StatusLine({ item }: { item: ChatItem }) {
|
||||
const danger = item.phase === 'blocked' || item.phase === 'error';
|
||||
return (
|
||||
<div className={`flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs ${danger ? 'border-red-500/25 bg-red-500/10' : 'border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/35'}`}>
|
||||
{item.phase === 'thinking' || item.phase === 'tool_running' ? (
|
||||
<Loader2 className="w-3.5 h-3.5 dg-text-blue animate-spin shrink-0" />
|
||||
) : danger ? (
|
||||
<AlertTriangle className="w-3.5 h-3.5 dg-text-red shrink-0" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-3.5 h-3.5 dg-text-secondary shrink-0" />
|
||||
)}
|
||||
<span className="font-medium text-[var(--dg-text-primary)]">{phaseLabel(item.phase)}</span>
|
||||
{item.toolName && <span className="dg-text-muted">tool={item.toolName}</span>}
|
||||
{item.content && <span className="dg-text-muted truncate">{item.content}</span>}
|
||||
{item.risk && <Badge className={`ml-auto text-[10px] border ${riskClass(item.risk)}`}>{item.risk}</Badge>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ThoughtBlock AI 思考(可折叠,灰底斜体,支持流式) // Logan.Li
|
||||
function ThoughtBlock({
|
||||
text, round, streaming, hideRoundLabel,
|
||||
@@ -305,7 +351,13 @@ function ToolBlock({ item, inRoundGroup }: { item: ChatItem; inRoundGroup?: bool
|
||||
)}
|
||||
{!item.pending && item.output != null && (
|
||||
<>
|
||||
{item.duration && <div className="text-[10px] dg-text-muted">{i18n.t('aiops.durationLabel', { duration: item.duration })}</div>}
|
||||
{(item.duration || item.outputSize != null) && (
|
||||
<div className="text-[10px] dg-text-muted">
|
||||
{item.duration && i18n.t('aiops.durationLabel', { duration: item.duration })}
|
||||
{item.duration && item.outputSize != null ? ' · ' : ''}
|
||||
{item.outputSize != null ? `输出 ${formatBytes(item.outputSize)}` : ''}
|
||||
</div>
|
||||
)}
|
||||
{(item.toolName === 'analyze_output' || looksLikeMarkdown(item.output)) ? (
|
||||
<div className="bg-[var(--dg-bg-primary)]/40 rounded px-2 py-1.5 max-h-80 overflow-y-auto">
|
||||
<AgentMarkdown content={item.output || i18n.t('aiops.agentclimessage.s216caee5')} />
|
||||
@@ -319,4 +371,10 @@ function ToolBlock({ item, inRoundGroup }: { item: ChatItem; inRoundGroup?: bool
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(n: number) {
|
||||
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`;
|
||||
if (n >= 1024) return `${(n / 1024).toFixed(1)}KB`;
|
||||
return `${n}B`;
|
||||
}
|
||||
Reference in new issue
Block a user