feat(client): control task agents from digital deck

This commit is contained in:
baiyanyun
2026-06-08 18:06:11 +08:00
parent 76a82f6203
commit 820b1b6f13
7 changed files with 315 additions and 132 deletions

View File

@@ -1939,6 +1939,65 @@
gap: 6px;
min-width: 0;
}
.de-task-agent-control-strip {
display: grid;
gap: 6px;
min-width: 0;
}
.de-task-agent-control {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
padding: 7px 8px;
border-radius: 10px;
border: 1px solid rgba(79,172,254,0.14);
background: rgba(247,251,255,0.9);
}
.de-task-agent-control-main {
min-width: 0;
display: grid;
gap: 2px;
}
.de-task-agent-control-main span {
color: #0c5a85;
font-size: 10px;
font-weight: 800;
line-height: 1;
}
.de-task-agent-control-main strong {
min-width: 0;
overflow: hidden;
color: #143450;
font-size: 12px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.de-task-agent-control-main small {
min-width: 0;
overflow: hidden;
color: #58728e;
font-size: 10px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.de-task-agent-control-actions {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
}
.de-task-agent-control-actions .de-btn-sm {
min-width: 58px;
justify-content: center;
}
.de-task-agent-cancel-btn {
color: #b42318;
border-color: rgba(180,35,24,0.22);
}
.de-run-detail-list-scroll {
overscroll-behavior: contain;
scrollbar-gutter: stable;

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react';
import { Clipboard, Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide-react';
import { Clipboard, Clock3, Download, Eye, PlayCircle, RefreshCw, SkipForward, Square } from 'lucide-react';
import Avatar3D from '@/components/digital/Avatar3D';
import { basePath } from '@/lib/basePath';
import {
@@ -22,6 +22,7 @@ import type {
DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkEvent,
DigitalEmployeeWorkdayProjection,
GovernanceCommandKind,
} from '@/types/api';
import type { DigitalNavigationTarget } from './navigation';
@@ -33,6 +34,7 @@ const DIGITAL_EMPLOYEE_TIME_ZONE = 'Asia/Shanghai';
type EmployeeVisualState = 'skill' | 'browser' | 'call' | 'redial' | 'evidence' | 'decision' | 'reporting';
type EventStatusFilter = 'all' | 'running' | 'success' | 'danger';
type TaskListFilter = 'all' | 'selected' | 'pending';
type TaskAgentControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task'>;
interface DigitalPlanItem {
id: string;
@@ -723,6 +725,15 @@ function taskAgentSummary(agent: DigitalEmployeeTaskAgentState): string {
return `${agent.title} ${agent.status_label},下一步 ${next}`;
}
function taskAgentControlLabel(command: TaskAgentControlCommand): string {
switch (command) {
case 'retry_task': return '重试';
case 'skip_task': return '跳过';
case 'cancel_task': return '取消';
default: return command;
}
}
function isDecisionOpen(status?: string): boolean {
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
}
@@ -1330,6 +1341,50 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
}, [selectedDate]);
const controlTaskAgent = useCallback(async (
agent: DigitalEmployeeTaskAgentState,
command: TaskAgentControlCommand,
) => {
const actionKey = `task-agent:${agent.task_id}:${command}`;
setActionBusy(actionKey);
setActionError(null);
setActionNotice(null);
try {
const response = await governanceCommand({
command_kind: command,
context_refs: {
plan_id: agent.plan_id,
task_id: agent.task_id,
run_id: agent.latest_run_id,
task_agent_id: agent.agent_id,
},
payload: {
source: 'digital-home-task-agent',
task_agent_id: agent.agent_id,
task_id: agent.task_id,
plan_id: agent.plan_id,
latest_run_id: agent.latest_run_id,
title: agent.title,
requested_action: command,
previous_state: agent.execution_state,
next_action: agent.next_action,
},
correlation_id: `digital-home-task-agent-${agent.task_id}-${command}-${Date.now()}`,
});
if (!response.accepted) {
throw new Error(response.message || `Task Agent 命令未被接受:${response.command_id || command}`);
}
setActionNotice(`Task Agent 已${taskAgentControlLabel(command)}${agent.title}`);
await fetchProjection();
} catch (error) {
if (mountedRef.current) {
setActionError(error instanceof Error ? error.message : 'Task Agent 控制失败');
}
} finally {
if (mountedRef.current) setActionBusy(null);
}
}, [fetchProjection]);
const openDuty = useCallback((planItem: DigitalPlanItem) => {
onNavigate({ tab: 'missions', date: selectedDate, missionId: planItem.conversationId || planItem.id });
}, [onNavigate, selectedDate]);
@@ -1781,6 +1836,63 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
</div>
</div>
{actionableTaskAgents.length > 0 && (
<div className="de-task-agent-control-strip" aria-label="Task Agent 控制台">
{actionableTaskAgents.slice(0, 2).map((agent) => {
const retryBusy = actionBusy === `task-agent:${agent.task_id}:retry_task`;
const skipBusy = actionBusy === `task-agent:${agent.task_id}:skip_task`;
const cancelBusy = actionBusy === `task-agent:${agent.task_id}:cancel_task`;
return (
<div key={agent.agent_id} className={`de-task-agent-control de-live-feed-row--${agent.status_tone || 'info'}`}>
<div className="de-task-agent-control-main">
<span>Task Agent</span>
<strong>{agent.title}</strong>
<small>{agent.status_label} · {agent.next_action}</small>
</div>
<div className="de-task-agent-control-actions">
{agent.can_retry && (
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={Boolean(actionBusy)}
title="记录重试任务命令"
onClick={() => { void controlTaskAgent(agent, 'retry_task'); }}
>
<RefreshCw size={13} aria-hidden="true" />
<span>{retryBusy ? '重试中' : '重试'}</span>
</button>
)}
{agent.can_skip && (
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={Boolean(actionBusy)}
title="记录跳过任务命令"
onClick={() => { void controlTaskAgent(agent, 'skip_task'); }}
>
<SkipForward size={13} aria-hidden="true" />
<span>{skipBusy ? '跳过中' : '跳过'}</span>
</button>
)}
{agent.can_cancel && (
<button
type="button"
className="de-btn-secondary de-btn-sm de-task-agent-cancel-btn"
disabled={Boolean(actionBusy)}
title="记录取消任务命令"
onClick={() => { void controlTaskAgent(agent, 'cancel_task'); }}
>
<Square size={12} aria-hidden="true" />
<span>{cancelBusy ? '取消中' : '取消'}</span>
</button>
)}
</div>
</div>
);
})}
</div>
)}
<div className="de-stage-feed-tools">
<span>
{visibleEvents.length} / {allEvents.length}