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 8716412a..f2b99ecc 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, + PlanStepDispatchPolicyPullResult, ReadyStepDispatchResult, NotificationPreferences, UserNotification, @@ -612,6 +613,12 @@ export function patchPlanStepDispatchPolicy( }); } +export function pullPlanStepDispatchPolicy(): Promise { + return apiFetch('/api/plan-step/dispatch-policy/pull', { + method: 'POST', + }); +} + export function getPlanSteps(params: Record = {}): Promise> { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { 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 c27efdb5..8f4e7611 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 @@ -36,6 +36,7 @@ import type { PlanDispatchResponse, ReadyStepDispatchResult, PlanStepDispatchPolicy, + PlanStepDispatchPolicyPullResult, PlanStepSummary, RouteDecisionRecord, RouteDecisionTimelineResponse, @@ -60,6 +61,7 @@ declare global { saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record }) => Promise; getSyncStatus?: () => Promise; flushSync?: () => Promise; + pullPlanStepDispatchPolicy?: () => Promise; getCronSettings?: () => Promise; saveCronSettings?: (patch: Partial) => Promise; runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>; @@ -791,6 +793,9 @@ export async function handleQimingclawDigitalApi( if (method === 'GET' && pathname === '/api/plan-step/dispatch-policy') { return planStepDispatchPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T; } + if (method === 'POST' && pathname === '/api/plan-step/dispatch-policy/pull') { + return await handlePlanStepDispatchPolicyPull(await readQimingclawSnapshot()) as T; + } if (method === 'PATCH' && pathname === '/api/plan-step/dispatch-policy') { return await handlePlanStepDispatchPolicyPatch( parseJsonBody>(options.body), @@ -3883,6 +3888,10 @@ function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy { max_per_sweep: 3, auto_dispatch_ready_steps: true, updated_at: null, + source: 'local', + remote_updated_at: null, + last_pulled_at: null, + last_pull_error: null, }; } @@ -3915,6 +3924,10 @@ function planStepDispatchPolicy(state: Partial | null | undefined) max_per_sweep: Number.isFinite(maxPerSweep) ? Math.max(1, Math.min(Math.floor(maxPerSweep), 10)) : fallback.max_per_sweep, auto_dispatch_ready_steps: stored.auto_dispatch_ready_steps !== false, updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at, + source: stored.source === 'remote' ? 'remote' : 'local', + remote_updated_at: typeof stored.remote_updated_at === 'string' ? stored.remote_updated_at : null, + last_pulled_at: typeof stored.last_pulled_at === 'string' ? stored.last_pulled_at : null, + last_pull_error: typeof stored.last_pull_error === 'string' ? stored.last_pull_error : null, }; } @@ -4021,6 +4034,27 @@ async function handlePlanStepDispatchPolicyPatch( return next; } +async function handlePlanStepDispatchPolicyPull( + snapshot: QimingclawSnapshot | null, +): Promise { + const fallbackPolicy = planStepDispatchPolicy(snapshot?.uiState ?? readState()); + const bridgeResult = await window.QimingClawBridge?.digital?.pullPlanStepDispatchPolicy?.().catch((error) => ({ + ok: false, + endpoint: null, + pulled_at: new Date().toISOString(), + policy: fallbackPolicy, + error: error instanceof Error ? error.message : 'plan step dispatch policy pull failed', + })); + if (bridgeResult) return bridgeResult; + return { + ok: false, + endpoint: null, + pulled_at: new Date().toISOString(), + policy: fallbackPolicy, + error: 'qimingclaw_bridge_unavailable', + }; +} + function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] { const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval])); return buildApprovals(snapshot) 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 3af9a393..b23afe7d 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 @@ -11,6 +11,7 @@ import { getSchedulerJobs, governanceCommand, postDigitalEmployeeWorkdayAction, + pullPlanStepDispatchPolicy, recordApprovalAction, restartManagedService, } from '@/lib/api'; @@ -1368,6 +1369,28 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } }, [selectedDate]); + const refreshPlanStepDispatchPolicy = useCallback(async () => { + setActionBusy('pull_plan_step_dispatch_policy'); + setActionError(null); + setActionNotice(null); + try { + const result = await pullPlanStepDispatchPolicy(); + if (!mountedRef.current) return; + await fetchProjection(); + if (!mountedRef.current) return; + const policy = result.policy; + setActionNotice(result.ok + ? `PlanStep 派发策略已拉取:${policy.enabled ? '启用' : '停用'},每轮最多 ${policy.max_per_sweep} 个。` + : `PlanStep 派发策略拉取失败,已沿用本地策略:${result.error || '未知错误'}。`); + } catch (error) { + if (mountedRef.current) { + setActionError(error instanceof Error ? error.message : '拉取PlanStep派发策略失败'); + } + } finally { + if (mountedRef.current) setActionBusy(null); + } + }, [fetchProjection]); + const copySyncFailureDiagnostic = useCallback(async (failure: ManagementSyncFailure) => { const diagnostic = buildSyncFailureDiagnostic(failure, managementSyncStatus); try { @@ -2016,6 +2039,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit