feat(approval): add safe risk disposition workflow
This commit is contained in:
66
web/src/services/approvalWorkbench.js
Normal file
66
web/src/services/approvalWorkbench.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { apiRequest } from './api.js'
|
||||
|
||||
function toNumber(value, fallback = 0) {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : fallback
|
||||
}
|
||||
|
||||
function toArray(value) {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
export function normalizeApprovalWorkbenchItem(item = {}) {
|
||||
const evidence = item.evidence && typeof item.evidence === 'object' ? item.evidence : {}
|
||||
const suggestion = item.suggestion && typeof item.suggestion === 'object' ? item.suggestion : {}
|
||||
return {
|
||||
taskKey: String(item.task_key || item.taskKey || '').trim(),
|
||||
claim: item.claim && typeof item.claim === 'object' ? item.claim : {},
|
||||
priorityScore: Math.max(0, Math.min(toNumber(item.priority_score ?? item.priorityScore), 100)),
|
||||
priorityTier: String(item.priority_tier || item.priorityTier || 'normal').trim(),
|
||||
priorityReasons: toArray(item.priority_reasons || item.priorityReasons).map((reason) => ({
|
||||
code: String(reason?.code || '').trim(),
|
||||
label: String(reason?.label || '').trim(),
|
||||
weight: toNumber(reason?.weight),
|
||||
tone: String(reason?.tone || 'normal').trim()
|
||||
})).filter((reason) => reason.label),
|
||||
riskLevel: String(item.risk_level || item.riskLevel || 'low').trim(),
|
||||
openRiskCount: Math.max(0, toNumber(item.open_risk_count ?? item.openRiskCount)),
|
||||
budgetUsageRate: item.budget_usage_rate == null && item.budgetUsageRate == null
|
||||
? null
|
||||
: toNumber(item.budget_usage_rate ?? item.budgetUsageRate),
|
||||
waitingHours: Math.max(0, toNumber(item.waiting_hours ?? item.waitingHours)),
|
||||
slaDueAt: String(item.sla_due_at || item.slaDueAt || '').trim(),
|
||||
slaOverdue: Boolean(item.sla_overdue ?? item.slaOverdue),
|
||||
evidence: {
|
||||
completeness: Math.max(0, Math.min(toNumber(evidence.completeness), 1)),
|
||||
presentCount: Math.max(0, toNumber(evidence.present_count ?? evidence.presentCount)),
|
||||
requiredCount: Math.max(0, toNumber(evidence.required_count ?? evidence.requiredCount)),
|
||||
missingLabels: toArray(evidence.missing_labels || evidence.missingLabels)
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean),
|
||||
historicalLabels: toArray(evidence.historical_labels || evidence.historicalLabels)
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
},
|
||||
suggestion: {
|
||||
action: String(suggestion.action || 'manual_review').trim(),
|
||||
label: String(suggestion.label || '人工复核').trim(),
|
||||
reason: String(suggestion.reason || '').trim(),
|
||||
advisoryOnly: suggestion.advisory_only !== false && suggestion.advisoryOnly !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchApprovalWorkbenchItems(options = {}) {
|
||||
const limit = Math.max(1, Math.min(toNumber(options.limit || 100), 200))
|
||||
const payload = await apiRequest(`/approval-workbench/items?limit=${limit}`, {
|
||||
timeoutMs: toNumber(options.timeoutMs || 5000),
|
||||
timeoutMessage: '审批优先队列加载超时,请稍后重试。'
|
||||
})
|
||||
return {
|
||||
items: toArray(payload?.items).map(normalizeApprovalWorkbenchItem),
|
||||
total: Math.max(0, toNumber(payload?.total)),
|
||||
generatedAt: String(payload?.generated_at || payload?.generatedAt || '').trim(),
|
||||
scoringVersion: String(payload?.scoring_version || payload?.scoringVersion || '').trim()
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,15 @@ function createPreReviewRequestId() {
|
||||
return `pre-review:${Date.now()}:${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
export function createExpenseClaimActionRequestId(action, claimId) {
|
||||
const normalizedAction = String(action || 'action').trim() || 'action'
|
||||
const normalizedClaimId = String(claimId || 'claim').trim() || 'claim'
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return `${normalizedAction}:${normalizedClaimId}:${globalThis.crypto.randomUUID()}`
|
||||
}
|
||||
return `${normalizedAction}:${normalizedClaimId}:${Date.now()}:${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
export function preReviewExpenseClaim(claimId, requestId = '') {
|
||||
const normalizedRequestId = String(requestId || '').trim() || createPreReviewRequestId()
|
||||
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/pre-review`, {
|
||||
@@ -261,10 +270,10 @@ export function approveExpenseClaim(claimId, payload = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
export function payExpenseClaim(claimId) {
|
||||
export function payExpenseClaim(claimId, payload = {}) {
|
||||
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/pay`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({})
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,41 @@ function toObject(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
|
||||
export function createRiskDispositionRequestId(action, observationId) {
|
||||
const normalizedAction = String(action || 'action').trim() || 'action'
|
||||
const normalizedObservationId = String(observationId || 'risk').trim() || 'risk'
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return `risk:${normalizedAction}:${normalizedObservationId}:${globalThis.crypto.randomUUID()}`
|
||||
}
|
||||
return `risk:${normalizedAction}:${normalizedObservationId}:${Date.now()}:${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
export function normalizeRiskDisposition(item = {}) {
|
||||
if (!item || typeof item !== 'object' || !String(item.id || '').trim()) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
id: String(item.id || '').trim(),
|
||||
observationId: String(item.observation_id || item.observationId || '').trim(),
|
||||
adjudication: String(item.adjudication || 'unreviewed').trim(),
|
||||
lifecycleStatus: String(item.lifecycle_status || item.lifecycleStatus || 'open').trim(),
|
||||
version: Math.max(0, toNumber(item.version)),
|
||||
assignee: String(item.assignee || '').trim(),
|
||||
dueAt: String(item.due_at || item.dueAt || '').trim(),
|
||||
resolution: String(item.resolution || '').trim(),
|
||||
createdAt: String(item.created_at || item.createdAt || '').trim(),
|
||||
updatedAt: String(item.updated_at || item.updatedAt || '').trim(),
|
||||
events: toArray(item.events).map((event) => ({
|
||||
id: String(event?.id || '').trim(),
|
||||
action: String(event?.action || '').trim(),
|
||||
version: Math.max(0, toNumber(event?.version)),
|
||||
actorName: String(event?.actor_name || event?.actorName || '').trim(),
|
||||
comment: String(event?.comment || '').trim(),
|
||||
createdAt: String(event?.created_at || event?.createdAt || '').trim()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeRiskObservation(item = {}) {
|
||||
return {
|
||||
id: String(item.id || '').trim(),
|
||||
@@ -33,16 +68,29 @@ export function normalizeRiskObservation(item = {}) {
|
||||
algorithmVersion: String(item.algorithm_version || item.algorithmVersion || '').trim(),
|
||||
status: String(item.status || '').trim(),
|
||||
feedbackStatus: String(item.feedback_status || item.feedbackStatus || '').trim(),
|
||||
contributionScores: toObject(item.contribution_scores || item.contributionScores),
|
||||
baseline: toObject(item.baseline),
|
||||
evidence: toArray(item.evidence),
|
||||
graphNodeKeys: toArray(item.graph_node_keys || item.graphNodeKeys),
|
||||
graphEdgeKeys: toArray(item.graph_edge_keys || item.graphEdgeKeys),
|
||||
policyRefs: toArray(item.policy_refs || item.policyRefs),
|
||||
similarCaseClaimIds: toArray(item.similar_case_claim_ids || item.similarCaseClaimIds),
|
||||
contributionScores: toObject(
|
||||
item.contribution_scores_json || item.contribution_scores || item.contributionScores
|
||||
),
|
||||
baseline: toObject(item.baseline_json || item.baseline),
|
||||
evidence: toArray(item.evidence_json || item.evidence),
|
||||
graphNodeKeys: toArray(
|
||||
item.graph_node_keys_json || item.graph_node_keys || item.graphNodeKeys
|
||||
),
|
||||
graphEdgeKeys: toArray(
|
||||
item.graph_edge_keys_json || item.graph_edge_keys || item.graphEdgeKeys
|
||||
),
|
||||
policyRefs: toArray(item.policy_refs_json || item.policy_refs || item.policyRefs),
|
||||
similarCaseClaimIds: toArray(
|
||||
item.similar_case_claim_ids_json
|
||||
|| item.similar_case_claim_ids
|
||||
|| item.similarCaseClaimIds
|
||||
),
|
||||
ontology: toObject(item.ontology_json || item.ontologyJson),
|
||||
decisionTrace: toObject(item.decision_trace || item.decisionTrace),
|
||||
decisionTrace: toObject(
|
||||
item.decision_trace_json || item.decision_trace || item.decisionTrace
|
||||
),
|
||||
feedbackItems: toArray(item.feedback_items || item.feedbackItems),
|
||||
disposition: normalizeRiskDisposition(item.disposition),
|
||||
createdAt: String(item.created_at || item.createdAt || '').trim(),
|
||||
updatedAt: String(item.updated_at || item.updatedAt || '').trim()
|
||||
}
|
||||
@@ -145,3 +193,42 @@ export async function fetchRunRiskObservations(runId, options = {}) {
|
||||
|
||||
return toArray(payload.items || payload).map(normalizeRiskObservation)
|
||||
}
|
||||
|
||||
export async function executeRiskDispositionAction(observationId, payload = {}) {
|
||||
const normalizedObservationId = String(observationId || '').trim()
|
||||
const action = String(payload.action || '').trim()
|
||||
if (!normalizedObservationId || !action) {
|
||||
throw new Error('风险处置缺少观察标识或动作。')
|
||||
}
|
||||
const requestId = String(payload.requestId || payload.request_id || '').trim()
|
||||
|| createRiskDispositionRequestId(action, normalizedObservationId)
|
||||
const body = {
|
||||
action,
|
||||
expected_version: Math.max(0, toNumber(payload.expectedVersion ?? payload.expected_version)),
|
||||
request_id: requestId
|
||||
}
|
||||
const comment = String(payload.comment || '').trim()
|
||||
const resolution = String(payload.resolution || '').trim()
|
||||
const assignee = String(payload.assignee || '').trim()
|
||||
const dueAt = String(payload.dueAt || payload.due_at || '').trim()
|
||||
if (comment) body.comment = comment
|
||||
if (resolution) body.resolution = resolution
|
||||
if (assignee) body.assignee = assignee
|
||||
if (dueAt) body.due_at = dueAt
|
||||
|
||||
const result = await apiRequest(
|
||||
`/risk-observations/${encodeURIComponent(normalizedObservationId)}/disposition/actions`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
timeoutMs: toNumber(payload.timeoutMs || 5000),
|
||||
timeoutMessage: '风险处置提交超时,请刷新状态后重试。'
|
||||
}
|
||||
)
|
||||
return {
|
||||
disposition: normalizeRiskDisposition(result?.disposition),
|
||||
event: result?.event && typeof result.event === 'object' ? result.event : {},
|
||||
replayed: Boolean(result?.replayed),
|
||||
requestId
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user