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

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);