吸收数字员工PlanStep阻塞处理入口
This commit is contained in:
@@ -6,6 +6,7 @@ import { basePath } from '@/lib/basePath';
|
||||
import {
|
||||
dispatchPlanNow,
|
||||
getDigitalEmployeeWorkday,
|
||||
getPlanStepGraph,
|
||||
getSchedulerJobs,
|
||||
governanceCommand,
|
||||
postDigitalEmployeeWorkdayAction,
|
||||
@@ -26,6 +27,8 @@ import type {
|
||||
DigitalEmployeeWorkEvent,
|
||||
DigitalEmployeeWorkdayProjection,
|
||||
GovernanceCommandKind,
|
||||
PlanStepBusinessRow,
|
||||
PlanStepDependencyDetail,
|
||||
} from '@/types/api';
|
||||
import type { DigitalNavigationTarget } from './navigation';
|
||||
|
||||
@@ -38,6 +41,7 @@ type EmployeeVisualState = 'skill' | 'browser' | 'call' | 'redial' | 'evidence'
|
||||
type EventStatusFilter = 'all' | 'running' | 'success' | 'danger';
|
||||
type TaskListFilter = 'all' | 'selected' | 'pending';
|
||||
type TaskAgentControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task'>;
|
||||
type BlockedPlanStepControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task' | 'provide_task_input'>;
|
||||
type ApprovalActionKind = 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk';
|
||||
|
||||
interface DigitalPlanItem {
|
||||
@@ -60,6 +64,11 @@ interface DigitalPlanItem {
|
||||
source: 'plan_template';
|
||||
}
|
||||
|
||||
interface BlockedPlanStepInputTarget {
|
||||
step: PlanStepBusinessRow;
|
||||
dependency: PlanStepDependencyDetail | null;
|
||||
}
|
||||
|
||||
interface ManagementSyncStatus {
|
||||
running: boolean;
|
||||
syncing: boolean;
|
||||
@@ -796,6 +805,26 @@ function taskAgentControlLabel(command: TaskAgentControlCommand): string {
|
||||
}
|
||||
}
|
||||
|
||||
function blockedPlanStepControlLabel(command: BlockedPlanStepControlCommand): string {
|
||||
switch (command) {
|
||||
case 'retry_task': return '重试前置';
|
||||
case 'skip_task': return '跳过前置';
|
||||
case 'cancel_task': return '取消此步骤';
|
||||
case 'provide_task_input': return '补充输入';
|
||||
default: return command;
|
||||
}
|
||||
}
|
||||
|
||||
function firstActionableBlockedDependency(step: PlanStepBusinessRow): PlanStepDependencyDetail | null {
|
||||
return (step.blocked_dependency_details ?? []).find((dependency) => Boolean(dependency.task_id)) ?? null;
|
||||
}
|
||||
|
||||
function blockedPlanStepSummary(step: PlanStepBusinessRow): string {
|
||||
const dependency = firstActionableBlockedDependency(step) ?? step.blocked_dependency_details?.[0] ?? null;
|
||||
const dependencyLabel = dependency ? `${dependency.title} ${dependency.status}` : `阻塞依赖 ${step.blocked_dependencies.length} 个`;
|
||||
return `${step.title} ${step.dependency_status || step.status},${dependencyLabel}`;
|
||||
}
|
||||
|
||||
function isDecisionOpen(status?: string): boolean {
|
||||
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
|
||||
}
|
||||
@@ -916,6 +945,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
||||
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
||||
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
|
||||
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
|
||||
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
|
||||
const [blockedInputDraft, setBlockedInputDraft] = useState('');
|
||||
const [profileForm, setProfileForm] = useState({
|
||||
name: '',
|
||||
company: '',
|
||||
@@ -986,7 +1019,19 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
setManagementSyncStatus(status);
|
||||
});
|
||||
|
||||
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise]);
|
||||
const planStepGraphPromise = getPlanStepGraph({ status: 'blocked', page_size: 6 })
|
||||
.then((response) => {
|
||||
if (!mountedRef.current) return;
|
||||
setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked'));
|
||||
setBlockedPlanStepError(null);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!mountedRef.current) return;
|
||||
setBlockedPlanSteps([]);
|
||||
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
|
||||
});
|
||||
|
||||
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise]);
|
||||
if (mountedRef.current) {
|
||||
setProjectionLoading(false);
|
||||
}
|
||||
@@ -1523,6 +1568,71 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
}
|
||||
}, [fetchProjection]);
|
||||
|
||||
const controlBlockedPlanStep = useCallback(async (
|
||||
step: PlanStepBusinessRow,
|
||||
command: BlockedPlanStepControlCommand,
|
||||
options: { input?: string } = {},
|
||||
) => {
|
||||
const dependency = firstActionableBlockedDependency(step);
|
||||
const target = command === 'retry_task' || command === 'skip_task' ? dependency : null;
|
||||
const targetStepId = target?.step_id ?? step.step_id;
|
||||
const targetTaskId = target?.task_id ?? step.task_id ?? null;
|
||||
const targetRunId = target?.latest_run_id ?? step.latest_run_id ?? null;
|
||||
if (!targetTaskId) {
|
||||
setActionError('当前阻塞步骤缺少可治理任务,无法发送处理命令');
|
||||
return;
|
||||
}
|
||||
const actionKey = `blocked-plan-step:${step.step_id}:${command}`;
|
||||
setActionBusy(actionKey);
|
||||
setActionError(null);
|
||||
setActionNotice(null);
|
||||
try {
|
||||
const response = await governanceCommand({
|
||||
command_kind: command,
|
||||
context_refs: {
|
||||
plan_id: step.plan_id,
|
||||
task_id: targetTaskId,
|
||||
run_id: targetRunId,
|
||||
step_id: targetStepId,
|
||||
blocked_step_id: step.step_id,
|
||||
},
|
||||
payload: {
|
||||
source: 'digital-home-blocked-plan-step',
|
||||
plan_id: step.plan_id,
|
||||
task_id: targetTaskId,
|
||||
run_id: targetRunId,
|
||||
step_id: targetStepId,
|
||||
blocked_step_id: step.step_id,
|
||||
blocked_step_title: step.title,
|
||||
blocked_dependency_step_id: target?.step_id ?? null,
|
||||
requested_action: command,
|
||||
input: options.input,
|
||||
},
|
||||
correlation_id: `digital-home-blocked-step-${step.step_id}-${command}-${Date.now()}`,
|
||||
});
|
||||
if (!response.accepted) {
|
||||
throw new Error(response.message || `阻塞步骤处理未被接受:${response.command_id || command}`);
|
||||
}
|
||||
setActionNotice(`阻塞步骤已${blockedPlanStepControlLabel(command)}:${step.title}。`);
|
||||
setBlockedInputTarget(null);
|
||||
setBlockedInputDraft('');
|
||||
await fetchProjection();
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
setActionError(error instanceof Error ? error.message : '阻塞步骤处理失败');
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) setActionBusy(null);
|
||||
}
|
||||
}, [fetchProjection]);
|
||||
|
||||
const submitBlockedPlanStepInput = useCallback(async () => {
|
||||
const target = blockedInputTarget;
|
||||
const input = blockedInputDraft.trim();
|
||||
if (!target || !input) return;
|
||||
await controlBlockedPlanStep(target.step, 'provide_task_input', { input });
|
||||
}, [blockedInputDraft, blockedInputTarget, controlBlockedPlanStep]);
|
||||
|
||||
const restartService = useCallback(async (service: DigitalEmployeeManagedService) => {
|
||||
const actionKey = `managed-service:${service.service_id}:restart`;
|
||||
setActionBusy(actionKey);
|
||||
@@ -2115,6 +2225,72 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(blockedPlanSteps.length > 0 || blockedPlanStepError) && (
|
||||
<div className="de-task-agent-control-strip de-blocked-step-control-strip" aria-label="PlanStep 阻塞处理">
|
||||
{blockedPlanStepError && <p className="de-empty-text">{blockedPlanStepError}</p>}
|
||||
{blockedPlanSteps.slice(0, 3).map((step) => {
|
||||
const dependency = firstActionableBlockedDependency(step);
|
||||
const retryBusy = actionBusy === `blocked-plan-step:${step.step_id}:retry_task`;
|
||||
const skipBusy = actionBusy === `blocked-plan-step:${step.step_id}:skip_task`;
|
||||
const cancelBusy = actionBusy === `blocked-plan-step:${step.step_id}:cancel_task`;
|
||||
const inputBusy = actionBusy === `blocked-plan-step:${step.step_id}:provide_task_input`;
|
||||
const dependencyActionDisabled = Boolean(actionBusy) || !dependency?.task_id;
|
||||
const stepActionDisabled = Boolean(actionBusy) || !step.task_id;
|
||||
return (
|
||||
<div key={step.step_id} className="de-task-agent-control de-blocked-step-control de-live-feed-row--danger">
|
||||
<div className="de-task-agent-control-main">
|
||||
<span>Blocked PlanStep</span>
|
||||
<strong>{step.title}</strong>
|
||||
<small>{blockedPlanStepSummary(step)}</small>
|
||||
</div>
|
||||
<div className="de-task-agent-control-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-secondary de-btn-sm"
|
||||
disabled={dependencyActionDisabled}
|
||||
title={dependency?.task_id ? '重试首个阻塞前置任务' : '缺少可重试的前置任务'}
|
||||
onClick={() => { void controlBlockedPlanStep(step, 'retry_task'); }}
|
||||
>
|
||||
<RefreshCw size={13} aria-hidden="true" />
|
||||
<span>{retryBusy ? '重试中' : '重试前置'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-secondary de-btn-sm"
|
||||
disabled={dependencyActionDisabled}
|
||||
title={dependency?.task_id ? '跳过首个阻塞前置任务' : '缺少可跳过的前置任务'}
|
||||
onClick={() => { void controlBlockedPlanStep(step, 'skip_task'); }}
|
||||
>
|
||||
<SkipForward size={13} aria-hidden="true" />
|
||||
<span>{skipBusy ? '跳过中' : '跳过前置'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-secondary de-btn-sm"
|
||||
disabled={stepActionDisabled}
|
||||
title={step.task_id ? '补充当前阻塞步骤所需输入' : '当前步骤缺少可补充输入的任务'}
|
||||
onClick={() => { setBlockedInputTarget({ step, dependency: null }); setBlockedInputDraft(''); }}
|
||||
>
|
||||
<MessageSquare size={13} aria-hidden="true" />
|
||||
<span>{inputBusy ? '提交中' : '补充输入'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-secondary de-btn-sm de-task-agent-cancel-btn"
|
||||
disabled={stepActionDisabled}
|
||||
title={step.task_id ? '取消当前阻塞步骤' : '当前步骤缺少可取消的任务'}
|
||||
onClick={() => { void controlBlockedPlanStep(step, '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} 条
|
||||
@@ -2292,6 +2468,46 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</div>
|
||||
)}
|
||||
|
||||
{blockedInputTarget && (
|
||||
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setBlockedInputTarget(null)}>
|
||||
<div
|
||||
className="de-profile-settings-modal de-blocked-input-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="补充阻塞步骤输入"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="de-task-manager-modal-head">
|
||||
<div>
|
||||
<span className="de-drawer-eyebrow">BLOCKED PLANSTEP</span>
|
||||
<h3>补充输入</h3>
|
||||
<p>{blockedInputTarget.step.title}</p>
|
||||
</div>
|
||||
<button type="button" className="de-schedule-close" onClick={() => setBlockedInputTarget(null)}>×</button>
|
||||
</div>
|
||||
<label className="de-blocked-input-field">
|
||||
<span>补充内容</span>
|
||||
<textarea
|
||||
value={blockedInputDraft}
|
||||
onChange={(event) => setBlockedInputDraft(event.target.value)}
|
||||
placeholder="输入用于解除当前阻塞的信息"
|
||||
/>
|
||||
</label>
|
||||
<div className="de-event-modal-actions">
|
||||
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => setBlockedInputTarget(null)}>取消</button>
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-primary de-btn-sm"
|
||||
disabled={!blockedInputDraft.trim() || Boolean(actionBusy)}
|
||||
onClick={() => { void submitBlockedPlanStepInput(); }}
|
||||
>
|
||||
提交输入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profileSettingsOpen && (
|
||||
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setProfileSettingsOpen(false)}>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user