feat(client): control task agents from digital deck
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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} 条
|
||||
|
||||
Reference in New Issue
Block a user