From a6e0246418efa4e329f97ac34613809f9826d29e Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Wed, 10 Jun 2026 20:37:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=B8=E6=94=B6=E6=95=B0=E5=AD=97=E5=91=98?= =?UTF-8?q?=E5=B7=A5PlanStep=E9=98=BB=E5=A1=9E=E5=A4=84=E7=90=86=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../embedded/sgrobot-digital/src/index.css | 35 +++ .../embedded/sgrobot-digital/src/lib/api.ts | 10 +- .../src/lib/qimingclawAdapter.ts | 42 +++- .../src/pages/digital/CommandDeck.tsx | 218 +++++++++++++++++- .../embedded/sgrobot-digital/src/types/api.ts | 40 ++++ ...{index-DqdYYbRq.css => index-4TVkOCsf.css} | 2 +- .../sgrobot-digital/assets/index-BRZXqRQH.js | 184 +++++++++++++++ .../sgrobot-digital/assets/index-Dar5kQHh.js | 184 --------------- .../public/sgrobot-digital/index.html | 4 +- .../scripts/check-digital-employee.js | 15 ++ ...ital-employee-frontend-integration-plan.md | 6 +- 11 files changed, 543 insertions(+), 197 deletions(-) rename qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/{index-DqdYYbRq.css => index-4TVkOCsf.css} (60%) create mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-BRZXqRQH.js delete mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-Dar5kQHh.js diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/index.css b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/index.css index 41dca196..ed180fd9 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/index.css +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/index.css @@ -2248,6 +2248,41 @@ color: #b42318; border-color: rgba(180,35,24,0.22); } +.de-blocked-step-control-strip { + border-top: 1px solid rgba(180,35,24,0.12); + padding-top: 6px; +} +.de-blocked-step-control { + border-color: rgba(180,35,24,0.18); + background: rgba(255,248,247,0.92); +} +.de-blocked-step-control .de-task-agent-control-actions { + flex-wrap: wrap; + justify-content: flex-end; +} +.de-blocked-input-modal { max-width: 520px; } +.de-blocked-input-field { + display: grid; + gap: 8px; + margin-top: 12px; + color: #17344f; + font-size: 12px; + font-weight: 700; +} +.de-blocked-input-field textarea { + min-height: 120px; + resize: vertical; + border: 1px solid rgba(79,172,254,0.2); + border-radius: 10px; + padding: 10px 12px; + color: #12344f; + background: rgba(255,255,255,0.82); +} +.de-blocked-input-field textarea:focus { + outline: none; + border-color: rgba(79,172,254,0.55); + box-shadow: 0 0 0 3px rgba(79,172,254,0.14); +} .de-run-detail-list-scroll { overscroll-behavior: contain; scrollbar-gutter: stable; diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts index bebeea79..f701a7ea 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts @@ -13,6 +13,8 @@ import type { ChannelDetail, BrowserSession, ApprovalSubscriptionPreferences, + BusinessViewResponse, + PlanStepBusinessRow, PlanStepDispatchPolicy, NotificationPreferences, UserNotification, @@ -609,22 +611,22 @@ export function patchPlanStepDispatchPolicy( }); } -export function getPlanSteps(params: Record = {}): Promise { +export function getPlanSteps(params: Record = {}): Promise> { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`); }); const suffix = query.toString(); - return apiFetch(`/api/digital-employee/plan-steps${suffix ? `?${suffix}` : ''}`); + return apiFetch>(`/api/digital-employee/plan-steps${suffix ? `?${suffix}` : ''}`); } -export function getPlanStepGraph(params: Record = {}): Promise { +export function getPlanStepGraph(params: Record = {}): Promise> { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`); }); const suffix = query.toString(); - return apiFetch(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`); + return apiFetch>(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`); } export function markNotificationRead(id: string): Promise { diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts index e832b217..5f8acce7 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts @@ -2593,11 +2593,13 @@ function planStepRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] { dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies), blocked_dependencies: blockedDependencies, blocked_dependency_count: blockedDependencies.length, + blocked_dependency_details: planStepDependencyDetails(blockedDependencies, snapshot), waiting_dependencies: waitingDependencies, waiting_dependency_count: waitingDependencies.length, satisfied_dependencies: satisfiedDependencies, preferred_skill_ids: step.preferredSkillIds, task_id: readiness.taskId, + latest_run_id: readiness.latestRunId, dispatchable: readiness.dispatchable, skip_reason: readiness.skipReason, last_dispatch_status: stringValue(lastDispatch.status) || null, @@ -2626,8 +2628,11 @@ function planStepGraphRows(snapshot: QimingclawSnapshot | null): BusinessViewRow depends_on: arrayValue(row.depends_on), dependency_status: stringValue(row.dependency_status) || null, blocked_dependencies: arrayValue(row.blocked_dependencies), + blocked_dependency_details: arrayValue(row.blocked_dependency_details), waiting_dependencies: arrayValue(row.waiting_dependencies), satisfied_dependencies: arrayValue(row.satisfied_dependencies), + task_id: stringValue(row.task_id) || null, + latest_run_id: stringValue(row.latest_run_id) || null, dispatchable: row.dispatchable === true, skip_reason: stringValue(row.skip_reason) || null, sync_status: stringValue(row.sync_status) || null, @@ -2661,12 +2666,13 @@ function planStepDispatchReadiness( step: QimingclawPlanStepRecord, snapshot: QimingclawSnapshot | null, payload: Record, -): { taskId: string | null; dispatchable: boolean; skipReason: string | null } { +): { taskId: string | null; latestRunId: string | null; dispatchable: boolean; skipReason: string | null } { const tasks = runtimePlanTasks(snapshot, step.planId); const task = tasks.find((candidate) => taskStepId(candidate) === step.id) ?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status)) ?? null; const taskId = task?.id ?? null; + const latestRunId = task ? latestRunForTask(runtimeRuns(snapshot), task.id)?.id ?? null : null; let skipReason: string | null = null; if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready'; else if (!task) skipReason = 'missing_task'; @@ -2674,7 +2680,39 @@ function planStepDispatchReadiness( else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running'; else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending'; else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled'; - return { taskId, dispatchable: !skipReason, skipReason }; + return { taskId, latestRunId, dispatchable: !skipReason, skipReason }; +} + +function planStepDependencyDetails(dependencyIds: string[], snapshot: QimingclawSnapshot | null): BusinessViewRow[] { + const stepsById = new Map(runtimePlanSteps(snapshot).map((step) => [step.id, step])); + return dependencyIds.map((dependencyId) => { + const dependency = stepsById.get(dependencyId); + if (!dependency) { + return { + step_id: dependencyId, + task_id: null, + latest_run_id: null, + title: dependencyId, + status: 'missing', + dependency_status: 'missing', + dispatchable: false, + skip_reason: 'missing_step', + }; + } + const payload = asRecord(dependency.payload) ?? {}; + const graph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {}; + const readiness = planStepDispatchReadiness(dependency, snapshot, payload); + return { + step_id: dependency.id, + task_id: readiness.taskId, + latest_run_id: readiness.latestRunId, + title: dependency.title, + status: dependency.status, + dependency_status: stringValue(graph.status) || dependency.status, + dispatchable: readiness.dispatchable, + skip_reason: readiness.skipReason, + }; + }); } function isTerminalRuntimeTaskStatus(status: string): boolean { diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx index b14f382a..6662d648 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx @@ -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; +type BlockedPlanStepControlCommand = Extract; 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(null); + const [blockedPlanSteps, setBlockedPlanSteps] = useState([]); + const [blockedPlanStepError, setBlockedPlanStepError] = useState(null); + const [blockedInputTarget, setBlockedInputTarget] = useState(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 )} + {(blockedPlanSteps.length > 0 || blockedPlanStepError) && ( +
+ {blockedPlanStepError &&

{blockedPlanStepError}

} + {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 ( +
+
+ Blocked PlanStep + {step.title} + {blockedPlanStepSummary(step)} +
+
+ + + + +
+
+ ); + })} +
+ )} +
展示 {visibleEvents.length} / 共 {allEvents.length} 条 @@ -2292,6 +2468,46 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
)} + {blockedInputTarget && ( +
setBlockedInputTarget(null)}> +
event.stopPropagation()} + > +
+
+ BLOCKED PLANSTEP +

补充输入

+

{blockedInputTarget.step.title}

+
+ +
+