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(); 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 { 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; }