From 909f11dae5c721323ab1ff222bb35f9be25fbb07 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Wed, 10 Jun 2026 21:00:59 +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=E8=B7=A8=E4=BB=BB=E5=8A=A1=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../embedded/sgrobot-digital/src/lib/api.ts | 8 + .../src/lib/qimingclawAdapter.ts | 89 +++++++++ .../src/pages/digital/CommandDeck.tsx | 116 +++++++++-- .../embedded/sgrobot-digital/src/types/api.ts | 7 + .../sgrobot-digital/assets/index-BRZXqRQH.js | 184 ------------------ .../sgrobot-digital/assets/index-DxiObdwW.js | 184 ++++++++++++++++++ .../public/sgrobot-digital/index.html | 2 +- .../scripts/check-digital-employee.js | 19 ++ .../src/main/ipc/digitalEmployeeHandlers.ts | 5 + .../digitalEmployee/schedulerService.test.ts | 75 ++++++- .../digitalEmployee/schedulerService.ts | 89 ++++++++- .../digitalEmployee/stateService.test.ts | 63 ++++++ .../services/digitalEmployee/stateService.ts | 7 +- .../digitalEmployee/syncService.test.ts | 4 + .../services/digitalEmployee/syncService.ts | 2 + .../src/preload/webviewPerfBridge.ts | 3 + ...ital-employee-frontend-integration-plan.md | 6 +- 17 files changed, 646 insertions(+), 217 deletions(-) delete mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-BRZXqRQH.js create mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-DxiObdwW.js 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 f701a7ea..8716412a 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 @@ -16,6 +16,7 @@ import type { BusinessViewResponse, PlanStepBusinessRow, PlanStepDispatchPolicy, + ReadyStepDispatchResult, NotificationPreferences, UserNotification, DebugStoreResponse, @@ -713,6 +714,13 @@ export function dispatchPlanNow(planId: string): Promise { }); } +export function dispatchPlanReadySteps(planId: string): Promise { + return apiFetch(`/api/digital-employee/plans/${encodeURIComponent(planId)}/dispatch-ready-steps`, { + method: 'POST', + timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS, + }); +} + export function getDebugPlans(): Promise { return apiFetch<{ plans: DebugPlan[] }>('/api/debug/plans?limit=80') .then((data) => data.plans ?? []); 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 5f8acce7..d3783014 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 @@ -34,6 +34,7 @@ import type { MemoryEntry, NotificationPreferences, PlanDispatchResponse, + ReadyStepDispatchResult, PlanStepDispatchPolicy, PlanStepSummary, RouteDecisionRecord, @@ -63,6 +64,7 @@ declare global { saveCronSettings?: (patch: Partial) => Promise; runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>; runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>; + dispatchPlanReadySteps?: (planId: string) => Promise; getScheduleRuns?: (scheduleId: string, limit?: number) => Promise; listRouteDecisions?: (options?: { limit?: number }) => Promise; recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise; @@ -925,6 +927,10 @@ export async function handleQimingclawDigitalApi( if (method === 'GET' && pathname.startsWith('/api/debug/task/') && pathname.endsWith('/flow')) { return buildTaskFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T; } + const planReadyDispatchMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)\/dispatch-ready-steps$/); + if (method === 'POST' && planReadyDispatchMatch) { + return await dispatchPlanReadySteps(decodeURIComponent(planReadyDispatchMatch[1] || ''), await readQimingclawSnapshot()) as T; + } if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) { return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T; } @@ -6123,6 +6129,89 @@ async function dispatchPlan( }; } +async function dispatchPlanReadySteps( + planId: string, + snapshot: QimingclawSnapshot | null = null, +): Promise { + const targetPlanId = stringValue(planId); + const bridgeResult = targetPlanId ? await window.QimingClawBridge?.digital?.dispatchPlanReadySteps?.(targetPlanId).catch(() => null) : null; + if (bridgeResult) return bridgeResult; + const now = new Date().toISOString(); + const orchestrationId = `manual-ready-steps-${slugifyIdPart(targetPlanId || 'unknown')}-${slugifyIdPart(now)}`; + const rows = planStepRows(snapshot).filter((row) => stringValue(row.plan_id) === targetPlanId && stringValue(row.status).toLowerCase() === 'ready'); + const state = notificationAdapterState(snapshot); + const policy = planStepDispatchPolicy(state); + const result: ReadyStepDispatchResult = { + ok: true, + plan_id: targetPlanId || null, + trigger_source: 'manual', + orchestration_id: orchestrationId, + candidates: rows.length, + dispatched: 0, + skipped: 0, + failed: 0, + dispatched_step_ids: [], + skipped_step_ids: [], + failed_step_ids: [], + errors: [], + }; + if (!targetPlanId || !policy.enabled) { + result.skipped = rows.length; + result.skipped_step_ids = rows.map((row) => stringValue(row.step_id)).filter(Boolean); + return result; + } + const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10)); + let attempts = 0; + for (const row of rows) { + const stepId = stringValue(row.step_id); + const taskId = stringValue(row.task_id); + if (attempts >= maxPerSweep || row.dispatchable !== true || !taskId) { + result.skipped += 1; + if (stepId) result.skipped_step_ids?.push(stepId); + continue; + } + attempts += 1; + try { + const runId = stringValue(row.latest_run_id) || `${taskId}:run:start_run:${slugifyIdPart(orchestrationId)}`; + const commandId = `manual-ready-step-${slugifyIdPart(stepId)}-${Date.now()}`; + await recordGovernanceCommand( + { + command_kind: 'start_run', + context_refs: { plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId }, + payload: { + source: 'digital-home-plan-orchestration', + plan_id: targetPlanId, + task_id: taskId, + run_id: runId, + step_id: stepId, + trigger_source: 'manual', + orchestration_id: orchestrationId, + candidate_count: rows.length, + }, + correlation_id: commandId, + }, + governanceAccepted(commandId, commandId, '已手动推进 Ready PlanStep。', { + plan_id: targetPlanId, + task_id: taskId, + run_id: runId, + step_id: stepId, + status: 'running', + trigger_source: 'manual', + orchestration_id: orchestrationId, + source: 'qimingclaw-plan-orchestration', + }), + ); + result.dispatched += 1; + if (stepId) result.dispatched_step_ids?.push(stepId); + } catch (error) { + result.failed += 1; + if (stepId) result.failed_step_ids?.push(stepId); + result.errors?.push({ stepId, error: error instanceof Error ? error.message : 'dispatch failed' }); + } + } + return result; +} + function buildPlanMessages(planId: string, snapshot: QimingclawSnapshot | null = null): Array<{ role: string; content: string; created_at?: string | null }> { const runtimePlan = runtimePlans(snapshot).find((candidate) => candidate.id === planId); if (runtimePlan) { 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 6662d648..73b8b85e 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 @@ -5,6 +5,7 @@ import Avatar3D from '@/components/digital/Avatar3D'; import { basePath } from '@/lib/basePath'; import { dispatchPlanNow, + dispatchPlanReadySteps, getDigitalEmployeeWorkday, getPlanStepGraph, getSchedulerJobs, @@ -12,7 +13,6 @@ import { postDigitalEmployeeWorkdayAction, recordApprovalAction, restartManagedService, - triggerCronCheck, } from '@/lib/api'; import { isTauri } from '@/lib/tauri'; import type { @@ -60,10 +60,19 @@ interface DigitalPlanItem { conversationId?: string | null; stepCount: number; taskCount: number; + readyStepCount: number; + runningStepCount: number; + blockedStepCount: number; schedulerJob?: CronJob | null; source: 'plan_template'; } +interface PlanStepOrchestrationSummary { + ready: number; + running: number; + blocked: number; +} + interface BlockedPlanStepInputTarget { step: PlanStepBusinessRow; dependency: PlanStepDependencyDetail | null; @@ -405,6 +414,9 @@ function buildPlanTemplateItem(job: CronJob, duties: DigitalEmployeeDuty[], sele conversationId: linkedDuty?.conversation_id ?? templateId, stepCount, taskCount: skillCount, + readyStepCount: 0, + runningStepCount: 0, + blockedStepCount: 0, schedulerJob: job, source: 'plan_template', }; @@ -419,6 +431,26 @@ function planExecutionDisabledReason(planItem: DigitalPlanItem): string { return ''; } +function planReadyStepActionLabel(planItem: DigitalPlanItem): string { + if (planItem.readyStepCount > 0) return `推进 ${planItem.readyStepCount}`; + return '推进'; +} + +function planStepOrchestrationSummary(rows: PlanStepBusinessRow[]): Map { + const summary = new Map(); + rows.forEach((step) => { + const planId = step.plan_id || ''; + if (!planId) return; + const current = summary.get(planId) ?? { ready: 0, running: 0, blocked: 0 }; + const status = step.status.toLowerCase(); + if (status === 'ready') current.ready += 1; + else if (['running', 'active', 'leased', 'queued'].includes(status)) current.running += 1; + else if (status === 'blocked') current.blocked += 1; + summary.set(planId, current); + }); + return summary; +} + function eventMatchesPlanItem(event: DigitalEmployeeWorkEvent, planItem: DigitalPlanItem): boolean { const haystack = `${event.conversation_id ?? ''} ${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`; return haystack.includes(planItem.id) || haystack.includes(planItem.title); @@ -946,6 +978,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit const [reportPreviewOpen, setReportPreviewOpen] = useState(false); const [selectedSyncFailure, setSelectedSyncFailure] = useState(null); const [blockedPlanSteps, setBlockedPlanSteps] = useState([]); + const [planStepGraphRows, setPlanStepGraphRows] = useState([]); const [blockedPlanStepError, setBlockedPlanStepError] = useState(null); const [blockedInputTarget, setBlockedInputTarget] = useState(null); const [blockedInputDraft, setBlockedInputDraft] = useState(''); @@ -1019,14 +1052,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit setManagementSyncStatus(status); }); - const planStepGraphPromise = getPlanStepGraph({ status: 'blocked', page_size: 6 }) + const planStepGraphPromise = getPlanStepGraph({ page_size: 80 }) .then((response) => { if (!mountedRef.current) return; - setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked')); + setPlanStepGraphRows(response.items); + setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked').slice(0, 6)); setBlockedPlanStepError(null); }) .catch((error: unknown) => { if (!mountedRef.current) return; + setPlanStepGraphRows([]); setBlockedPlanSteps([]); setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败'); }); @@ -1066,6 +1101,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit [isLiveProjection, workdayProjection?.duties], ); const planItems = useMemo(() => { + const orchestrationByPlan = planStepOrchestrationSummary(planStepGraphRows); return schedulerJobs .slice() .sort((left, right) => { @@ -1084,9 +1120,20 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit }) .map((job) => { const templateId = templateIdForSchedulerJob(job); - return buildPlanTemplateItem(job, liveProjectionDuties, selectedDutyIds.has(templateId)); + const item = buildPlanTemplateItem(job, liveProjectionDuties, selectedDutyIds.has(templateId)); + const orchestration = orchestrationByPlan.get(item.planId) ?? orchestrationByPlan.get(templateId); + if (!orchestration) return item; + return { + ...item, + readyStepCount: orchestration.ready, + runningStepCount: orchestration.running, + blockedStepCount: orchestration.blocked, + resultText: orchestration.ready > 0 || orchestration.running > 0 || orchestration.blocked > 0 + ? `Ready ${orchestration.ready} · 运行 ${orchestration.running} · 阻塞 ${orchestration.blocked}` + : item.resultText, + }; }); - }, [liveProjectionDuties, schedulerJobs, selectedDutyIds]); + }, [liveProjectionDuties, planStepGraphRows, schedulerJobs, selectedDutyIds]); const selectedDuties = useMemo( () => planItems.filter((planItem) => selectedDutyIds.has(planItem.id)), [planItems, selectedDutyIds], @@ -1717,13 +1764,14 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit throw new Error(response.error || '执行接口未接受请求'); } const dispatchedCount = response.dispatched_tasks?.length ?? 0; - const schedulerResult = await triggerCronCheck(); - const readyDispatched = schedulerResult.readyStepDispatch?.dispatched ?? 0; - const readyFailed = schedulerResult.readyStepDispatch?.failed ?? 0; + const readyResult = await dispatchPlanReadySteps(planItem.planId); + const readyDispatched = readyResult.dispatched ?? 0; + const readySkipped = readyResult.skipped ?? 0; + const readyFailed = readyResult.failed ?? 0; setExecutePlanItem(null); setActionNotice( - readyDispatched > 0 || readyFailed > 0 - ? `已触发执行:${planItem.title},新建 ${dispatchedCount} 个步骤,Ready 步骤自动派发 ${readyDispatched} 个${readyFailed > 0 ? `,失败 ${readyFailed} 个` : ''}。` + readyDispatched > 0 || readySkipped > 0 || readyFailed > 0 + ? `已触发执行:${planItem.title},新建 ${dispatchedCount} 个步骤,推进 ${readyDispatched} 个${readySkipped > 0 ? `,跳过 ${readySkipped} 个` : ''}${readyFailed > 0 ? `,失败 ${readyFailed} 个` : ''}。` : `已触发执行:${planItem.title},已派发 ${dispatchedCount} 个步骤。`, ); await fetchProjection(); @@ -1736,6 +1784,32 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } }, [fetchProjection]); + const controlPlanReadySteps = useCallback(async (planItem: DigitalPlanItem) => { + if (!isPlanItemExecutable(planItem)) { + setActionError(planExecutionDisabledReason(planItem)); + setActionNotice(null); + return; + } + const actionKey = `orchestrate:${planItem.id}`; + setActionBusy(actionKey); + setActionError(null); + setActionNotice(null); + try { + const result = await dispatchPlanReadySteps(planItem.planId); + if (result.ok === false) { + throw new Error(result.errors?.[0]?.error || '计划推进未被接受'); + } + setActionNotice(`已推进:${planItem.title},派发 ${result.dispatched ?? 0} 个,跳过 ${result.skipped ?? 0} 个${(result.failed ?? 0) > 0 ? `,失败 ${result.failed}` : ''}。`); + await fetchProjection(); + } catch (error) { + if (mountedRef.current) { + setActionError(error instanceof Error ? error.message : '计划推进失败'); + } + } finally { + if (mountedRef.current) setActionBusy(null); + } + }, [fetchProjection]); + const dismissWorkdayGuide = useCallback(() => { setGuideOpen(false); setGuideDismissedDate(selectedDate); @@ -1782,6 +1856,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit 进入处理
+
); - }, [actionBusy, openDuty, selectedDutyIds, toggleDuty]); + }, [actionBusy, controlPlanReadySteps, openDuty, selectedDutyIds, toggleDuty]); const openEventDetail = useCallback((event: DigitalEmployeeWorkEvent) => { setSelectedEvent(event); @@ -2043,6 +2127,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit {planItem.resultText}
+