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 a2b83daa..0667367d 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 @@ -861,7 +861,7 @@ export function setArtifactRetention( export function recordApprovalAction( approvalId: string, - action: 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk', + action: 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk' | 'approve' | 'reject', options: Record = {}, ): Promise { return apiFetch(`/api/digital-employee/approvals/${encodeURIComponent(approvalId)}/actions`, { @@ -870,6 +870,13 @@ export function recordApprovalAction( }); } +export function pullApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> { + return apiFetch<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>('/api/approval/updates/pull', { + method: 'POST', + body: JSON.stringify({}), + }); +} + export function getDebugConversations(): Promise { return apiFetch<{ conversations: any[] }>('/api/debug/conversations') .then((data) => data.conversations ?? []); 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 cb7f4a1c..2f141f68 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 @@ -56,6 +56,8 @@ declare global { setSkillEnabled?: (skillId: string, enabled: boolean) => Promise<{ ok?: boolean; skill_id?: string; enabled?: boolean; error?: string }>; pullSkillPolicies?: () => Promise<{ ok?: boolean; skipped?: boolean; error?: string | null }>; pullRiskApprovalPolicy?: () => Promise<{ ok?: boolean; skipped?: boolean; error?: string | null }>; + pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>; + submitApprovalDecision?: (input: Record) => Promise<{ ok?: boolean; error?: string | null }>; evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise; getPlanTemplates?: () => Promise; listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise; @@ -323,6 +325,16 @@ interface QimingclawApprovalActionResult { payload: Record; } +interface ApprovalWorkflowSummary { + workflow_id: string | null; + current_step_id: string | null; + current_step_label: string | null; + current_participant: string | null; + step_index: number | null; + step_count: number | null; + next_step_label: string | null; +} + interface QimingclawManagedServiceActionResponse { ok: boolean; service_id: string; @@ -1010,6 +1022,9 @@ export async function handleQimingclawDigitalApi( if (method === 'POST' && pathname === '/api/risk-approval/policy/pull') { return await pullBridgeRiskApprovalPolicy() as T; } + if (method === 'POST' && pathname === '/api/approval/updates/pull') { + return await pullBridgeApprovalUpdates() as T; + } if (method === 'POST' && pathname.startsWith('/api/skill/') && pathname.endsWith('/enabled')) { const skillId = decodeURIComponent(pathname.split('/')[3] || ''); const body = parseJsonBody<{ enabled?: boolean }>(options.body); @@ -1162,6 +1177,25 @@ async function pullBridgeRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?: } } +async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> { + const bridge = window.QimingClawBridge?.digital; + if (!bridge?.pullApprovalUpdates) { + return { ok: false, error: 'qimingclaw_bridge_unavailable' }; + } + try { + const result = await bridge.pullApprovalUpdates(); + return { + ok: result?.ok !== false, + applied: Number(result?.applied ?? 0), + ignored: Number(result?.ignored ?? 0), + ...(result?.skipped ? { skipped: true } : {}), + ...(result?.error ? { error: result.error } : {}), + }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + async function evaluateBridgeRiskPolicy(input: QimingclawRiskPolicyInput): Promise { const bridge = window.QimingClawBridge?.digital; if (!bridge?.evaluateRiskPolicy) { @@ -3281,6 +3315,7 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView const approvalId = stringValue(approval.approval_id); const runtime = runtimeById.get(approvalId); const summary = approvalActionSummary(snapshot, approvalId, approval); + const workflow = approvalWorkflowSummary(approval); const entity = businessEntityFromRefs({ plan_id: approval.plan_id, task_id: approval.task_id, @@ -3302,6 +3337,13 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView last_actor: summary.last_actor, comment_count: summary.comment_count, delegate_to: summary.delegate_to, + workflow_id: workflow.workflow_id, + current_step_id: workflow.current_step_id, + current_step_label: workflow.current_step_label, + current_participant: workflow.current_participant, + approval_step_index: workflow.step_index, + approval_step_count: workflow.step_count, + next_step_label: workflow.next_step_label, }; }); } @@ -3423,6 +3465,25 @@ function filterApprovalHistoryRows(rows: BusinessViewRow[], searchParams: URLSea .filter((row) => !riskLevel || stringValue(row.risk_level) === riskLevel); } +function approvalWorkflowSummary(approval: Record): ApprovalWorkflowSummary { + const payload = asRecord(approval.payload) ?? {}; + const workflow = asRecord(payload.workflow) ?? {}; + const steps = Array.isArray(workflow.steps) ? workflow.steps.map((step) => asRecord(step) ?? {}) : []; + const currentStepId = stringValue(workflow.current_step_id ?? workflow.currentStepId ?? payload.current_step_id ?? payload.currentStepId); + const currentIndex = Math.max(0, steps.findIndex((step) => stringValue(step.step_id ?? step.stepId ?? step.id) === currentStepId)); + const current = steps[currentIndex] ?? null; + const next = steps.slice(currentIndex + 1).find((step) => ['pending', 'delegated', 'timeout'].includes(stringValue(step.status) || 'pending')) ?? null; + return { + workflow_id: stringValue(workflow.workflow_id ?? workflow.workflowId) || null, + current_step_id: currentStepId || null, + current_step_label: stringValue(current?.label ?? current?.title ?? current?.name) || null, + current_participant: stringValue(current?.participant_label ?? current?.participantLabel ?? current?.participant_id ?? current?.participantId) || null, + step_index: steps.length > 0 ? currentIndex + 1 : null, + step_count: steps.length || null, + next_step_label: stringValue(next?.label ?? next?.title ?? next?.name) || null, + }; +} + function approvalActionSummary( snapshot: QimingclawSnapshot | null, approvalId: string, @@ -4581,6 +4642,13 @@ function buildWorkdayDecisions( last_actor: stringValue(summary?.last_actor) || null, comment_count: numberValue(summary?.comment_count) ?? 0, delegate_to: stringValue(summary?.delegate_to) || null, + workflow_id: stringValue(summary?.workflow_id) || null, + current_step_id: stringValue(summary?.current_step_id) || null, + current_step_label: stringValue(summary?.current_step_label) || null, + current_participant: stringValue(summary?.current_participant) || null, + approval_step_index: numberValue(summary?.approval_step_index), + approval_step_count: numberValue(summary?.approval_step_count), + next_step_label: stringValue(summary?.next_step_label) || null, actions: isDecisionOpenStatus(status) ? [ { id: 'approved', label: '同意', tone: 'primary' }, @@ -5375,6 +5443,16 @@ async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, sn : undefined, ); const nextSnapshot = { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState }; + if (body.action === 'decide_approval') { + await window.QimingClawBridge?.digital?.submitApprovalDecision?.({ + approval_id: body.decision_id, + decision: body.decision, + date, + actor: 'digital_employee_operator', + source: 'sgrobot-digital-adapter', + acp_permission: acpPermissionResult, + }).catch(() => null); + } const projection = buildWorkday(date, nextSnapshot); if (body.action === 'generate_daily_report' && projection.daily_report) { await recordDailyReportArtifact(projection.daily_report); 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 84fc6670..003c72bc 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, + pullApprovalUpdates, pullPlanStepDispatchPolicy, pullRiskApprovalPolicy, recordApprovalAction, @@ -1413,6 +1414,27 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } }, [fetchProjection]); + const refreshApprovalUpdates = useCallback(async () => { + setActionBusy('pull_approval_updates'); + setActionError(null); + setActionNotice(null); + try { + const result = await pullApprovalUpdates(); + if (!mountedRef.current) return; + await fetchProjection(); + if (!mountedRef.current) return; + setActionNotice(result.ok + ? `审批更新已拉取:应用 ${result.applied ?? 0} 条,忽略 ${result.ignored ?? 0} 条。` + : `审批更新拉取失败:${result.error || '未知错误'}。`); + } catch (error) { + if (mountedRef.current) { + setActionError(error instanceof Error ? error.message : '拉取审批更新失败'); + } + } finally { + if (mountedRef.current) setActionBusy(null); + } + }, [fetchProjection]); + const copySyncFailureDiagnostic = useCallback(async (failure: ManagementSyncFailure) => { const diagnostic = buildSyncFailureDiagnostic(failure, managementSyncStatus); try { @@ -2081,6 +2103,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit