feat(client): reply digital approvals to acp

This commit is contained in:
baiyanyun
2026-06-08 10:23:06 +08:00
parent ee51fcdbeb
commit f4af77cb2b
8 changed files with 228 additions and 130 deletions

View File

@@ -36,6 +36,7 @@ declare global {
getSyncStatus?: () => Promise<QimingclawSyncStatus>;
flushSync?: () => Promise<QimingclawSyncStatus>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
};
};
}
@@ -1769,17 +1770,86 @@ async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, sn
};
}
const acpPermissionResult = body.action === 'decide_approval'
? await respondAcpPermissionForDecision(body, snapshot)
: null;
const savedState = await saveState(
state,
body.action,
date,
body.action === 'decide_approval'
? { decision_id: body.decision_id, decision: body.decision }
? {
decision_id: body.decision_id,
decision: body.decision,
acp_permission: acpPermissionResult,
}
: undefined,
);
return buildWorkday(date, { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState });
}
async function respondAcpPermissionForDecision(
body: DigitalEmployeeWorkdayActionRequest,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown> | null> {
if (!body.decision_id || !body.decision) return null;
const approval = findApprovalForDecision(body.decision_id, snapshot);
if (!approval) return null;
const refs = acpPermissionRefs(approval);
if (!refs) return null;
const response = acpResponseForDecision(body.decision);
const respondPermission = window.QimingClawBridge?.digital?.respondPermission;
if (!respondPermission) return null;
try {
const result = await respondPermission(refs.sessionId, refs.permissionId, response);
return {
permission_id: refs.permissionId,
session_id: refs.sessionId,
response,
success: result?.success !== false,
error: result?.error,
};
} catch (error) {
return {
permission_id: refs.permissionId,
session_id: refs.sessionId,
response,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function findApprovalForDecision(decisionId: string, snapshot: QimingclawSnapshot | null): ApprovalEntry | null {
return buildApprovals(snapshot).find((approval) => stringValue(approval.approval_id) === decisionId) ?? null;
}
function acpPermissionRefs(approval: ApprovalEntry): { permissionId: string; sessionId: string } | null {
const payload = asRecord(approval.payload);
const nestedPayload = asRecord(payload?.payload);
const permissionId = stringValue(payload?.permissionId)
|| stringValue(payload?.permission_id)
|| stringValue(payload?.id)
|| stringValue(nestedPayload?.permissionId)
|| stringValue(nestedPayload?.permission_id)
|| stringValue(nestedPayload?.id);
const sessionId = stringValue(payload?.sessionId)
|| stringValue(payload?.session_id)
|| stringValue(nestedPayload?.sessionId)
|| stringValue(nestedPayload?.session_id);
const source = stringValue(payload?.source);
if (!permissionId || !sessionId || (source && source !== 'acp-permission')) return null;
return { permissionId, sessionId };
}
function acpResponseForDecision(decision: string): 'once' | 'always' | 'reject' {
const normalized = decision.toLowerCase();
if (normalized.includes('always')) return 'always';
if (normalized.includes('reject') || normalized.includes('deny')) return 'reject';
return 'once';
}
async function handleGovernanceCommand(
body: GovernanceCommandRequest,
snapshot: QimingclawSnapshot | null,