吸收数字员工审批历史联动

This commit is contained in:
baiyanyun
2026-06-10 10:46:28 +08:00
parent ed2f19d257
commit 047651e404
15 changed files with 893 additions and 192 deletions

View File

@@ -65,6 +65,7 @@ declare global {
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise<QimingclawArtifactAccessResponse>;
recordArtifactLifecycle?: (input: QimingclawArtifactLifecycleInput) => Promise<QimingclawArtifactLifecycleResult | null>;
recordApprovalAction?: (input: QimingclawApprovalActionInput) => Promise<QimingclawApprovalActionResult | null>;
restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise<QimingclawManagedServiceActionResponse>;
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
};
@@ -271,6 +272,28 @@ interface QimingclawArtifactLifecycleResult {
payload: Record<string, unknown>;
}
interface QimingclawApprovalActionInput {
approvalId?: string | null;
action: string;
comment?: string | null;
delegateTo?: string | null;
delegateLabel?: string | null;
dueAt?: string | null;
riskLevel?: string | null;
reason?: string | null;
actor?: string | null;
source?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
}
interface QimingclawApprovalActionResult {
eventId: string;
kind: string;
payload: Record<string, unknown>;
}
interface QimingclawManagedServiceActionResponse {
ok: boolean;
service_id: string;
@@ -727,6 +750,11 @@ export async function handleQimingclawDigitalApi<T>(
.find((row) => stringValue(row.plan_id) === planId || stringValue(row.remote_id) === planId) ?? null;
return { item, source: BUSINESS_VIEW_SOURCE } as T;
}
const approvalActionMatch = pathname.match(/^\/api\/digital-employee\/approvals\/([^/]+)\/actions$/);
if (method === 'POST' && approvalActionMatch) {
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals)$/);
if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
@@ -1485,6 +1513,42 @@ async function handleArtifactRetention(
};
}
async function handleApprovalAction(
approvalId: string,
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown>> {
const bridge = window.QimingClawBridge?.digital;
const action = stringValue(body.action) || 'comment';
const approval = approvalDetailRow(snapshot, approvalId);
if (!bridge?.recordApprovalAction) {
return { ok: false, approval_id: approvalId, action, status: 'rejected', error: 'bridge_unavailable' };
}
const result = await bridge.recordApprovalAction({
approvalId,
action,
comment: stringValue(body.comment) || null,
delegateTo: stringValue(body.delegate_to ?? body.delegateTo) || null,
delegateLabel: stringValue(body.delegate_label ?? body.delegateLabel) || null,
dueAt: stringValue(body.due_at ?? body.dueAt) || null,
riskLevel: stringValue(body.risk_level ?? body.riskLevel) || null,
reason: stringValue(body.reason) || null,
actor: stringValue(body.actor) || 'digital_employee_ui',
source: stringValue(body.source) || 'sgrobot-digital-adapter',
planId: approval ? stringValue(approval.plan_id) || null : null,
taskId: approval ? stringValue(approval.task_id) || null : null,
runId: approval ? stringValue(approval.run_id) || null : null,
});
return {
ok: Boolean(result && result.kind !== 'approval_action_rejected'),
approval_id: approvalId,
action,
status: result?.kind === 'approval_action_rejected' ? 'rejected' : 'recorded',
event_id: result?.eventId ?? null,
item: approval,
};
}
function syncLine(snapshot: QimingclawSnapshot | null): string {
const sync = snapshot?.sync;
if (!sync) return '管理端同步:未连接';
@@ -2491,6 +2555,7 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
return buildApprovals(snapshot).map((approval) => {
const approvalId = stringValue(approval.approval_id);
const runtime = runtimeById.get(approvalId);
const summary = approvalActionSummary(snapshot, approvalId, approval);
const entity = businessEntityFromRefs({
plan_id: approval.plan_id,
task_id: approval.task_id,
@@ -2505,10 +2570,96 @@ function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessView
entity_type: entity.entity_type,
entity_id: entity.entity_id,
sync_status: (runtime?.syncStatus ?? stringValue(approval.sync_status)) || null,
risk_level: summary.risk_level,
sla_status: summary.sla_status,
due_at: summary.due_at,
last_action: summary.last_action,
last_actor: summary.last_actor,
comment_count: summary.comment_count,
delegate_to: summary.delegate_to,
};
});
}
function approvalDetailRow(snapshot: QimingclawSnapshot | null, approvalId: string): BusinessViewRow | null {
const row = businessApprovalRows(snapshot)
.find((approval) => stringValue(approval.approval_id) === approvalId || stringValue(approval.remote_id) === approvalId) ?? null;
if (!row) return null;
return {
...row,
timeline: buildApprovalTimeline(stringValue(row.approval_id), snapshot).events,
};
}
function approvalActionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['approval_action_recorded', 'approval_action_rejected'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const delegate = asRecord(payload.delegate_summary);
const comment = asRecord(payload.comment_summary);
const sla = asRecord(payload.sla_summary);
const risk = asRecord(payload.risk_summary);
return {
action_id: event.id,
event_id: event.id,
kind: event.kind,
approval_id: stringValue(payload.approval_id) || null,
action: stringValue(payload.action) || null,
status: stringValue(payload.status) || null,
comment_preview: stringValue(comment?.preview) || null,
delegate_to: stringValue(delegate?.label ?? delegate?.target) || null,
sla_status: stringValue(sla?.sla_status) || null,
due_at: stringValue(sla?.due_at) || null,
risk_level: stringValue(risk?.risk_level) || null,
actor: stringValue(payload.actor) || null,
source: stringValue(payload.source) || null,
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function approvalActionSummary(
snapshot: QimingclawSnapshot | null,
approvalId: string,
approval: Record<string, unknown>,
): Record<string, unknown> {
const actions = approvalActionRows(snapshot).filter((event) => stringValue(event.approval_id) === approvalId);
const latest = actions[0];
const latestRisk = actions.find((event) => stringValue(event.risk_level));
const latestSla = actions.find((event) => {
const status = stringValue(event.sla_status);
return Boolean(status && status !== 'untracked');
});
const latestDelegate = actions.find((event) => stringValue(event.delegate_to));
const payload = asRecord(approval.payload);
const dueAt = stringValue(latestSla?.due_at) || stringValue(payload?.due_at ?? payload?.dueAt ?? payload?.timeout_at ?? payload?.timeoutAt);
const slaStatus = stringValue(latestSla?.sla_status) || approvalSlaStatus(dueAt);
return {
risk_level: stringValue(latestRisk?.risk_level) || 'normal',
sla_status: slaStatus,
due_at: dueAt || null,
last_action: stringValue(latest?.action) || null,
last_actor: stringValue(latest?.actor) || null,
comment_count: actions.filter((event) => stringValue(event.action) === 'comment').length,
delegate_to: stringValue(latestDelegate?.delegate_to) || null,
};
}
function approvalSlaStatus(dueAt: string): string {
if (!dueAt) return 'untracked';
const dueMs = Date.parse(dueAt);
if (!Number.isFinite(dueMs)) return 'untracked';
return dueMs < Date.now() ? 'timeout' : 'tracked';
}
function skillCallAuditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['skill_call_allowed', 'skill_call_rejected', 'skill_call_observed'].includes(event.kind))
@@ -3087,6 +3238,7 @@ function buildNotificationsResponse(status: string | null, snapshot: QimingclawS
function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
const notifications = dedupeNotifications([
...approvalNotifications(snapshot),
...approvalActionNotifications(snapshot),
...syncFailureNotifications(snapshot),
...managedServiceNotifications(snapshot),
...taskNotifications(snapshot),
@@ -3098,19 +3250,24 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
}
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
return buildApprovals(snapshot)
.filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime')
.map((approval) => {
const status = stringValue(approval.status) || 'pending';
const open = isDecisionOpenStatus(status);
const summary = summaries.get(stringValue(approval.approval_id));
const slaStatus = stringValue(summary?.sla_status);
const riskLevel = stringValue(summary?.risk_level);
const warn = open && (slaStatus === 'timeout' || riskLevel === 'high');
return {
notification_id: `approval:${stringValue(approval.approval_id)}`,
plan_id: stringValue(approval.plan_id),
task_id: stringValue(approval.task_id),
kind: open ? 'need_approval' : 'confirmation_resolved',
title: open ? '审批待处理' : '审批已处理',
title: warn ? '审批需要关注' : open ? '审批待处理' : '审批已处理',
body: stringValue(approval.title) || '待确认事项',
level: open ? 'action' : 'info',
level: warn ? 'warn' : open ? 'action' : 'info',
status: open ? 'unread' : 'resolved',
correlation_id: stringValue(approval.approval_id),
created_at: stringValue(approval.created_at) || new Date().toISOString(),
@@ -3118,6 +3275,30 @@ function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotific
});
}
function approvalActionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return approvalActionRows(snapshot)
.filter((action) => stringValue(action.kind) === 'approval_action_recorded')
.filter((action) => ['delegate', 'mark_timeout', 'mark_risk'].includes(stringValue(action.action)))
.map((action) => {
const actionKind = stringValue(action.action);
const approvalId = stringValue(action.approval_id);
const isTimeout = actionKind === 'mark_timeout';
const isRisk = actionKind === 'mark_risk';
return {
notification_id: `approval-action:${approvalId}:${actionKind}:${slugifyIdPart(stringValue(action.occurred_at))}`,
plan_id: stringValue(action.plan_id),
task_id: stringValue(action.task_id),
kind: 'need_approval',
title: isTimeout ? '审批已超时' : isRisk ? '审批已标记风险' : '审批已转交',
body: stringValue(action.message) || approvalId,
level: isTimeout || isRisk ? 'warn' : 'action',
status: 'unread',
correlation_id: approvalId,
created_at: stringValue(action.occurred_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function syncFailureNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return (snapshot?.sync?.recentFailures ?? []).map((failure) => ({
notification_id: `sync_failure:${failure.id}`,
@@ -3233,6 +3414,7 @@ function buildWorkdayDecisions(
): DigitalEmployeeDecision[] {
const plansById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
const tasksById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task]));
const approvalSummaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
const resultByDecisionId = state.decisionResultsByDate?.[date] ?? {};
return buildApprovals(snapshot).filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime').map((approval) => {
const payload = asRecord(approval.payload);
@@ -3244,6 +3426,7 @@ function buildWorkdayDecisions(
const taskId = stringValue(approval.task_id);
const plan = planId ? plansById.get(planId) : null;
const task = taskId ? tasksById.get(taskId) : null;
const summary = approvalSummaries.get(decisionId);
return {
id: decisionId,
title: stringValue(approval.title) || stringValue(payload?.title) || '待确认事项',
@@ -3252,6 +3435,13 @@ function buildWorkdayDecisions(
status_label: decisionStatusLabel(status, result),
plan_title: plan?.title || stringValue(payload?.plan_title) || stringValue(approval.plan_id) || 'qimingclaw 本地记录',
task_title: task?.title || stringValue(payload?.task_title) || stringValue(approval.task_id) || '待确认任务',
risk_level: stringValue(summary?.risk_level) || 'normal',
sla_status: stringValue(summary?.sla_status) || 'untracked',
due_at: stringValue(summary?.due_at) || null,
last_action: stringValue(summary?.last_action) || null,
last_actor: stringValue(summary?.last_actor) || null,
comment_count: numberValue(summary?.comment_count) ?? 0,
delegate_to: stringValue(summary?.delegate_to) || null,
actions: isDecisionOpenStatus(status)
? [
{ id: 'approved', label: '同意', tone: 'primary' },