吸收数字员工审批时间线

This commit is contained in:
baiyanyun
2026-06-09 17:33:00 +08:00
parent 5160a73254
commit dc8cd09535
8 changed files with 333 additions and 147 deletions

View File

@@ -1,5 +1,6 @@
import type {
ApprovalEntry,
ApprovalTimelineResponse,
ArtifactEntry,
CanonicalEvent,
CronJob,
@@ -639,6 +640,10 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'GET' && pathname === '/api/artifact') {
return { artifacts: buildArtifacts(await readQimingclawSnapshot()) } as T;
}
const approvalTimelineMatch = pathname.match(/^\/api\/approval\/([^/]+)\/timeline$/);
if (method === 'GET' && approvalTimelineMatch) {
return buildApprovalTimeline(decodeURIComponent(approvalTimelineMatch[1] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/memory') {
return { entries: await readMemoryEntries(url.searchParams.get('query'), url.searchParams.get('category')) } as T;
}
@@ -1963,6 +1968,107 @@ function buildApprovals(snapshot: QimingclawSnapshot | null): ApprovalEntry[] {
return [...runtimeApprovalEntries, ...syncReviewEntries];
}
function buildApprovalTimeline(approvalId: string, snapshot: QimingclawSnapshot | null): ApprovalTimelineResponse {
const approval = buildApprovals(snapshot).find((candidate) => stringValue(candidate.approval_id) === approvalId) ?? null;
const runtimeApproval = runtimeApprovals(snapshot).find((candidate) => candidate.id === approvalId) ?? null;
const approvalPayload = asRecord(approval?.payload ?? runtimeApproval?.payload);
const approvalAcpPermission = acpPermissionFromPayload(approvalPayload);
const refs = {
approvalId,
planId: stringValue(approval?.plan_id) || runtimeApproval?.planId || '',
taskId: stringValue(approval?.task_id) || runtimeApproval?.taskId || '',
runId: stringValue(approval?.run_id) || runtimeApproval?.runId || '',
permissionId: approvalAcpPermission.permissionId,
sessionId: approvalAcpPermission.sessionId,
};
const approvalCreatedAt = stringValue(approval?.created_at) || runtimeApproval?.createdAt || runtimeApproval?.updatedAt || '';
const entries: ApprovalTimelineResponse['events'] = [];
if (approval || runtimeApproval) {
const title = stringValue(approval?.title) || runtimeApproval?.title || '待确认事项';
const status = stringValue(approval?.status) || runtimeApproval?.status || 'pending';
entries.push({
event_id: `${approvalId}:approval_record`,
kind: 'approval_record',
status,
message: `审批记录:${title} -> ${status}`,
occurred_at: approvalCreatedAt || new Date().toISOString(),
payload: approvalPayload,
source_type: 'approval',
});
}
for (const event of runtimeEvents(snapshot)) {
if (!isApprovalTimelineEvent(event, refs)) continue;
const payload = asRecord(event.payload);
entries.push({
event_id: event.id,
kind: event.kind,
status: approvalTimelineEventStatus(event, payload),
message: event.message || event.kind,
occurred_at: event.occurredAt,
payload,
source_type: stringValue(payload?.source) || 'runtime_event',
});
}
return {
approval_id: approvalId,
approval,
events: dedupeApprovalTimelineEvents(entries)
.sort((left, right) => left.occurred_at.localeCompare(right.occurred_at) || left.event_id.localeCompare(right.event_id)),
};
}
function acpPermissionFromPayload(payload: Record<string, unknown> | null): { permissionId: string; sessionId: string } {
const nested = asRecord(payload?.acpPermission) ?? asRecord(payload?.acp_permission) ?? asRecord(payload?.permission);
return {
permissionId: stringValue(nested?.permission_id) || stringValue(nested?.permissionId) || stringValue(payload?.permission_id) || stringValue(payload?.permissionId),
sessionId: stringValue(nested?.session_id) || stringValue(nested?.sessionId) || stringValue(payload?.session_id) || stringValue(payload?.sessionId),
};
}
function isApprovalTimelineEvent(
event: QimingclawEventRecord,
refs: { approvalId: string; planId: string; taskId: string; runId: string; permissionId: string; sessionId: string },
): boolean {
const payload = asRecord(event.payload);
const payloadApprovalId = stringValue(payload?.approvalId)
|| stringValue(payload?.approval_id)
|| stringValue(payload?.decision_id)
|| stringValue(payload?.decisionId);
if (payloadApprovalId === refs.approvalId) return true;
if (event.id.startsWith(`${refs.approvalId}:`)) return true;
const acpPermission = acpPermissionFromPayload(payload);
if (refs.permissionId && acpPermission.permissionId === refs.permissionId) return true;
if (refs.sessionId && acpPermission.sessionId === refs.sessionId) return true;
const approvalLikeKind = event.kind.includes('approval') || event.kind.includes('permission');
if (!approvalLikeKind) return false;
if (refs.runId && event.runId === refs.runId) return true;
if (refs.taskId && event.taskId === refs.taskId) return true;
if (refs.planId && event.planId === refs.planId) return true;
return false;
}
function approvalTimelineEventStatus(event: QimingclawEventRecord, payload: Record<string, unknown> | null): string | null {
return stringValue(payload?.nextStatus)
|| stringValue(payload?.next_status)
|| stringValue(payload?.status)
|| stringValue(payload?.decision)
|| (event.kind === 'approval_decision' ? 'completed' : null);
}
function dedupeApprovalTimelineEvents(events: ApprovalTimelineResponse['events']): ApprovalTimelineResponse['events'] {
const seen = new Set<string>();
return events.filter((event) => {
if (seen.has(event.event_id)) return false;
seen.add(event.event_id);
return true;
});
}
function buildWorkdayDecisions(
state: AdapterState,
date: string,