吸收数字员工多步审批跨端回写

This commit is contained in:
baiyanyun
2026-06-11 10:44:12 +08:00
parent e9c76c59ca
commit 43418d0453
16 changed files with 1240 additions and 200 deletions

View File

@@ -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<string, string | null | undefined> = {},
): Promise<any> {
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<any[]> {
return apiFetch<{ conversations: any[] }>('/api/debug/conversations')
.then((data) => data.conversations ?? []);

View File

@@ -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<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise<QimingclawRiskPolicyResult>;
getPlanTemplates?: () => Promise<QimingclawPlanTemplateList>;
listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise<QimingclawMemoryRecord[]>;
@@ -323,6 +325,16 @@ interface QimingclawApprovalActionResult {
payload: Record<string, unknown>;
}
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<T>(
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<QimingclawRiskPolicyResult> {
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<string, unknown>): 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);

View File

@@ -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
<ShieldAlert size={13} aria-hidden="true" />
<span>{actionBusy === 'pull_risk_approval_policy' ? '拉取中...' : '风险策略'}</span>
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
disabled={projectionLoading || actionBusy === 'pull_approval_updates'}
onClick={refreshApprovalUpdates}
title="从管理端拉取审批更新"
>
<RefreshCw size={13} aria-hidden="true" />
<span>{actionBusy === 'pull_approval_updates' ? '拉取中...' : '审批更新'}</span>
</button>
</div>
<div className={`de-management-sync-strip de-management-sync-strip--${managementSyncTone}`} title={managementSyncError || managementSyncSummary}>
@@ -3227,6 +3259,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span>{decision.comment_count ? `评论 ${decision.comment_count}` : '暂无评论'}</span>
<span>{decision.delegate_to ? `转交 ${decision.delegate_to}` : '未转交'}</span>
<span>{decision.last_action ? `最近 ${approvalActionLabel(decision.last_action)} · ${decision.last_actor || '未知'}` : '暂无审批动作'}</span>
<span>{decision.approval_step_count ? `步骤 ${decision.approval_step_index || 1}/${decision.approval_step_count}` : '单步审批'}</span>
<span>{decision.current_step_label ? `当前 ${decision.current_step_label}` : '当前步骤待确认'}</span>
<span>{decision.current_participant ? `处理人 ${decision.current_participant}` : '未指定处理人'}</span>
<span>{decision.next_step_label ? `下一步 ${decision.next_step_label}` : '无后续步骤'}</span>
</div>
<div className="de-approval-action-fields">
<input

View File

@@ -874,6 +874,13 @@ export interface DigitalEmployeeDecision {
last_actor?: string | null;
comment_count?: number | null;
delegate_to?: string | null;
workflow_id?: string | null;
current_step_id?: string | null;
current_step_label?: string | null;
current_participant?: string | null;
approval_step_index?: number | null;
approval_step_count?: number | null;
next_step_label?: string | null;
actions: DigitalEmployeeDecisionAction[];
}