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} 条
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,8 +16,8 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-C9vE8PTK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BgRGGB2I.css">
|
||||
<script type="module" crossorigin src="./assets/index-BBxsxEp7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CKIQdvco.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user