Files
qiming/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/approvals.ts
2026-06-29 19:53:34 +08:00

122 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ApprovalDecisionView,
GovernanceCommandResponse,
UserNotification,
} from '@/types/api';
import { governanceCommand } from './api';
export type ApprovalDecisionResolution = 'approve' | 'reject';
const REJECT_ACTION_IDS = new Set(['reject', 'rejected', 'defer', 'deferred']);
const APPROVE_SHORTCUTS = new Set(['同意', '批准', '通过', '同意执行', '批准执行', 'approve', 'approved', 'yes', 'ok']);
const REJECT_SHORTCUTS = new Set(['拒绝', '驳回', '不同意', '否决', '拒绝执行', '驳回执行', 'reject', 'rejected', 'no']);
export function isApprovalOpen(status?: string | null): boolean {
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
}
export function approvalDecisionId(decision: ApprovalDecisionView): string {
return decision.approval_id || decision.id;
}
export function approvalResolutionFromAction(actionId: string): ApprovalDecisionResolution {
return REJECT_ACTION_IDS.has(actionId.toLowerCase()) ? 'reject' : 'approve';
}
export function parseApprovalShortcut(input: string): ApprovalDecisionResolution | null {
const normalized = input
.trim()
.toLowerCase()
.replace(/[\s。.!?]/g, '');
if (APPROVE_SHORTCUTS.has(normalized)) return 'approve';
if (REJECT_SHORTCUTS.has(normalized)) return 'reject';
return null;
}
export function notificationToApprovalDecision(
notification: UserNotification,
): ApprovalDecisionView | null {
if (notification.kind !== 'need_approval' || notification.status !== 'unread') {
return null;
}
const approvalId = notification.approval_id || notification.correlation_id;
if (!approvalId) return null;
return {
id: approvalId,
approval_id: approvalId,
plan_id: notification.plan_id || null,
task_id: notification.task_id || null,
run_id: null,
target_kind: notification.task_id ? 'task' : null,
target_id: notification.task_id || notification.plan_id || approvalId,
title: notification.title || '真实执行事项需要确认',
scenario: notification.body || '请确认下一步处理方式。',
status: 'requested',
status_label: '待决策',
plan_title: notification.plan_id || '真实计划',
task_title: notification.task_id || approvalId,
actions: [
{ id: 'approve', label: '同意', tone: 'primary' },
{ id: 'reject', label: '驳回', tone: 'secondary' },
],
};
}
export function mergeApprovalDecisions(
canonical: ApprovalDecisionView[],
legacy: ApprovalDecisionView[],
): ApprovalDecisionView[] {
const merged = new Map<string, ApprovalDecisionView>();
canonical.forEach((decision) => {
merged.set(approvalDecisionId(decision), decision);
});
legacy.forEach((decision) => {
const id = approvalDecisionId(decision);
if (!merged.has(id)) merged.set(id, decision);
});
return Array.from(merged.values());
}
export async function submitApprovalDecision(
decision: ApprovalDecisionView,
actionIdOrResolution: string,
options: {
source: string;
comment?: string;
correlationPrefix?: string;
},
): Promise<GovernanceCommandResponse> {
const approvalId = approvalDecisionId(decision);
if (!approvalId) {
throw new Error('缺少 approval_id无法处理审批。');
}
const resolution = approvalResolutionFromAction(actionIdOrResolution);
const action = decision.actions.find((item) => item.id === actionIdOrResolution);
const comment = options.comment || action?.label || (resolution === 'reject' ? '驳回' : '同意');
const stamp = Date.now();
const prefix = (options.correlationPrefix || options.source || 'approval')
.replace(/[^a-zA-Z0-9_-]/g, '-')
.replace(/-+/g, '-');
const response = await governanceCommand({
command_kind: resolution === 'reject' ? 'reject_approval' : 'approve_approval',
context_refs: {
approval_id: approvalId,
plan_id: decision.plan_id || undefined,
task_id: decision.task_id || undefined,
run_id: decision.run_id || undefined,
},
payload: {
source: options.source,
comment,
target_kind: decision.target_kind || undefined,
target_id: decision.target_id || undefined,
},
correlation_id: `${prefix}-${approvalId}-${resolution}-${stamp}`,
idempotency_key: `${prefix}-${approvalId}-${resolution}-${stamp}`,
});
if (!response.accepted) {
throw new Error(response.message || `审批命令未被接受:${response.command_id || approvalId}`);
}
return response;
}