feat(expense): add persistent zero-entry receipt association

This commit is contained in:
caoxiaozhu
2026-07-16 10:23:23 +08:00
parent 54754b5502
commit ae3f02c35a
30 changed files with 4450 additions and 810 deletions

View File

@@ -1,5 +1,13 @@
import * as aiAttachmentAssociationModel from '../../utils/aiAttachmentAssociationModel.js'
import { fetchAttachmentAssociationJob } from '../../services/attachmentAssociationJobs.js'
import {
buildAttachmentAssociationCandidateActions,
isAttachmentAssociationConfirmationResult,
resolveAttachmentAssociationTarget
} from '../../utils/attachmentAssociationJobModel.js'
import {
fetchAttachmentAssociationJob,
normalizeAttachmentAssociationJob
} from '../../services/attachmentAssociationJobs.js'
const ATTACHMENT_ASSOCIATION_JOB_POLL_INTERVAL_MS = 1200
const ATTACHMENT_ASSOCIATION_JOB_MAX_POLLS = 90
@@ -25,23 +33,7 @@ export function useWorkbenchAiAttachmentAssociationJobs({
}
function normalizeJob(job = {}) {
const jobId = String(job?.job_id || job?.jobId || '').trim()
if (!jobId) {
return null
}
return {
jobId,
status: String(job?.status || 'queued').trim() || 'queued',
message: String(job?.message || '').trim(),
receiptIds: (Array.isArray(job?.receipt_ids) ? job.receipt_ids : job?.receiptIds || [])
.map((item) => String(item || '').trim())
.filter(Boolean),
claimId: String(job?.claim_id || job?.claimId || '').trim(),
claimNo: String(job?.claim_no || job?.claimNo || '').trim(),
uploadedCount: Number(job?.uploaded_count ?? job?.uploadedCount ?? 0) || 0,
skippedCount: Number(job?.skipped_count ?? job?.skippedCount ?? 0) || 0,
error: String(job?.error || '').trim()
}
return normalizeAttachmentAssociationJob(job)
}
function isPending(job = {}) {
@@ -112,12 +104,39 @@ export function useWorkbenchAiAttachmentAssociationJobs({
if (!findMessage(messageId)) {
return true
}
if (isAttachmentAssociationConfirmationResult(normalizedJob)) {
const confirmationMessage = aiAttachmentAssociationModel.buildAiAttachmentAssociationConfirmationResultMessage({
job: normalizedJob,
fileNames
})
await streamOrSetInlineAssistantContent(messageId, confirmationMessage)
replaceJobMessage(messageId, confirmationMessage, {
attachmentAssociationJob: normalizedJob,
attachmentOcrDetails,
stewardPlan: {
streamStatus: 'completed',
thinkingEvents: buildThinkingEvents('completed')
},
suggestedActions: buildAttachmentAssociationCandidateActions(normalizedJob)
})
persistCurrentConversation()
return true
}
if (normalizedJob.status === 'succeeded') {
const target = resolveAttachmentAssociationTarget(normalizedJob)
const finalMessageText = aiAttachmentAssociationModel.buildAiAttachmentAssociationResultMessage({
claimNo: normalizedJob.claimNo,
claimNo: target.claimNo,
fileNames,
uploadedCount: normalizedJob.uploadedCount,
skippedCount: normalizedJob.skippedCount
skippedCount: normalizedJob.skippedCount,
applicationClaimNo: normalizedJob.applicationClaimNo,
confidence: normalizedJob.confidence,
matchReasons: normalizedJob.matchReasons,
riskItems: normalizedJob.riskItems,
confirmationRequired: Boolean(
normalizedJob.draftPayload?.confirmationRequired
?? normalizedJob.draftPayload?.confirmation_required
)
})
await streamOrSetInlineAssistantContent(messageId, finalMessageText)
replaceJobMessage(messageId, finalMessageText, {
@@ -128,13 +147,13 @@ export function useWorkbenchAiAttachmentAssociationJobs({
thinkingEvents: buildThinkingEvents('completed')
},
suggestedActions: buildDetailActions({
claimId: normalizedJob.claimId,
claimNo: normalizedJob.claimNo
claimId: target.claimId,
claimNo: target.claimNo
})
})
notifyRequestUpdated?.({
claimId: normalizedJob.claimId,
claimNo: normalizedJob.claimNo,
claimId: target.claimId,
claimNo: target.claimNo,
source: 'ai-workbench-attachment-association-job',
uploadedCount: normalizedJob.uploadedCount,
skippedCount: normalizedJob.skippedCount
@@ -244,7 +263,7 @@ export function useWorkbenchAiAttachmentAssociationJobs({
extractReceiptIdsFromOcrDocuments,
normalizeJob,
pollJob,
resumePendingJobs
resumePendingJobs,
updateJobMessage
}
}