272 lines
9.5 KiB
JavaScript
272 lines
9.5 KiB
JavaScript
|
|
import { ATTACHMENT_ASSOCIATION_CONFIRM_HREF } from './travelReimbursementAttachmentModel.js'
|
||
|
|
|
||
|
|
export function createSubmitAttachmentAssociationFlow({
|
||
|
|
activeReviewPayload,
|
||
|
|
buildReviewFormContextFromPayload,
|
||
|
|
createMessage,
|
||
|
|
draftClaimId,
|
||
|
|
emitDraftSaved,
|
||
|
|
fetchReceiptFolderItems,
|
||
|
|
isKnowledgeSession,
|
||
|
|
messages,
|
||
|
|
nextTick,
|
||
|
|
persistSessionState,
|
||
|
|
resetFlowRun,
|
||
|
|
reviewInlineForm,
|
||
|
|
scrollToBottom,
|
||
|
|
sessionSwitchBusy,
|
||
|
|
submitComposer,
|
||
|
|
submitting,
|
||
|
|
toast
|
||
|
|
}) {
|
||
|
|
const pendingAttachmentAssociations = new Map()
|
||
|
|
|
||
|
|
function createPendingAttachmentAssociationId() {
|
||
|
|
return `attachment-association-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||
|
|
}
|
||
|
|
|
||
|
|
function emitSavedDraftRefresh(draftPayload) {
|
||
|
|
if (!emitDraftSaved || isKnowledgeSession.value || !draftPayload?.claim_no) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const draftType = String(draftPayload.draft_type || '').trim()
|
||
|
|
emitDraftSaved({
|
||
|
|
claimId: String(draftPayload.claim_id || draftPayload.claimId || '').trim(),
|
||
|
|
claimNo: String(draftPayload.claim_no || draftPayload.claimNo || '').trim(),
|
||
|
|
status: String(draftPayload.status || '').trim(),
|
||
|
|
approvalStage: String(draftPayload.approval_stage || draftPayload.approvalStage || '').trim(),
|
||
|
|
documentType: draftType === 'expense_application' ? 'application' : 'reimbursement'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeRecognizedAttachmentData(data) {
|
||
|
|
if (!data || typeof data !== 'object') {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
const documents = Array.isArray(data.ocrDocuments) ? data.ocrDocuments : []
|
||
|
|
if (!documents.length) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
ocrPayload: data.ocrPayload || null,
|
||
|
|
ocrSummary: String(data.ocrSummary || '').trim(),
|
||
|
|
ocrDocuments: documents,
|
||
|
|
ocrFilePreviews: Array.isArray(data.ocrFilePreviews) ? data.ocrFilePreviews : []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasReceiptFolderSourceFile(files) {
|
||
|
|
return files.some((file) => String(file?.receiptId || '').trim())
|
||
|
|
}
|
||
|
|
|
||
|
|
async function promptUnlinkedReceiptFolderIfNeeded({
|
||
|
|
detailScopedClaimId,
|
||
|
|
files,
|
||
|
|
fileNames,
|
||
|
|
options,
|
||
|
|
rawText,
|
||
|
|
resolvedUploadDisposition,
|
||
|
|
reviewAction,
|
||
|
|
systemGenerated,
|
||
|
|
userText
|
||
|
|
}) {
|
||
|
|
if (
|
||
|
|
isKnowledgeSession.value ||
|
||
|
|
systemGenerated ||
|
||
|
|
!files.length ||
|
||
|
|
detailScopedClaimId ||
|
||
|
|
resolvedUploadDisposition ||
|
||
|
|
options.skipReceiptFolderUnlinkedPrompt ||
|
||
|
|
options.skipDraftAssociationPrompt ||
|
||
|
|
reviewAction ||
|
||
|
|
hasReceiptFolderSourceFile(files)
|
||
|
|
) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
let unlinkedReceipts = []
|
||
|
|
try {
|
||
|
|
unlinkedReceipts = await fetchReceiptFolderItems('unlinked')
|
||
|
|
} catch (error) {
|
||
|
|
console.warn('Failed to load unlinked receipt folder items before attachment upload:', error)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
const count = Array.isArray(unlinkedReceipts) ? unlinkedReceipts.length : 0
|
||
|
|
if (!count) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
resetFlowRun()
|
||
|
|
if (!options.skipUserMessage) {
|
||
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
||
|
|
}
|
||
|
|
messages.value.push(createMessage(
|
||
|
|
'assistant',
|
||
|
|
`票据夹中还有 ${count} 份未关联票据。建议先处理这些票据再上传新附件,避免重复保存或遗漏关联。`,
|
||
|
|
[],
|
||
|
|
{
|
||
|
|
meta: ['票据夹待关联'],
|
||
|
|
suggestedActions: [
|
||
|
|
{
|
||
|
|
action_type: 'open_receipt_folder',
|
||
|
|
label: '去票据夹关联',
|
||
|
|
icon: 'mdi mdi-folder-open-outline',
|
||
|
|
payload: { target_view: 'receiptFolder' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
action_type: 'continue_upload_with_unlinked_receipts',
|
||
|
|
label: '继续上传新附件',
|
||
|
|
icon: 'mdi mdi-upload-outline',
|
||
|
|
payload: { raw_text: rawText }
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
))
|
||
|
|
nextTick(scrollToBottom)
|
||
|
|
persistSessionState()
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildConfirmedAssociationText(message) {
|
||
|
|
return String(message?.text || '')
|
||
|
|
.replace(`[确认](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确认')
|
||
|
|
.replace(`[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确定')
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveReviewPanelScope({
|
||
|
|
reviewPayload = null,
|
||
|
|
reviewAction = '',
|
||
|
|
fileCount = 0,
|
||
|
|
rawText = ''
|
||
|
|
} = {}) {
|
||
|
|
if (!reviewPayload || typeof reviewPayload !== 'object') {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalizedAction = String(reviewAction || '').trim()
|
||
|
|
const documentCount = Array.isArray(reviewPayload.document_cards) ? reviewPayload.document_cards.length : 0
|
||
|
|
const riskCount = Array.isArray(reviewPayload.risk_briefs) ? reviewPayload.risk_briefs.length : 0
|
||
|
|
const asksRisk = /风险|隐患|超标|异常|重复|待整改|风险项|高风险|中风险|低风险/.test(String(rawText || ''))
|
||
|
|
|
||
|
|
if (fileCount > 0 && documentCount > 0) {
|
||
|
|
return 'documents'
|
||
|
|
}
|
||
|
|
if (riskCount > 0 && (asksRisk || ['next_step', 'submit', 'submit_claim'].includes(normalizedAction))) {
|
||
|
|
return 'risk'
|
||
|
|
}
|
||
|
|
if (!normalizedAction && fileCount === 0) {
|
||
|
|
return 'overview'
|
||
|
|
}
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
async function confirmPendingAttachmentAssociation(message) {
|
||
|
|
if (submitting.value || sessionSwitchBusy.value) return null
|
||
|
|
|
||
|
|
const pending = message?.pendingAttachmentAssociation && typeof message.pendingAttachmentAssociation === 'object'
|
||
|
|
? message.pendingAttachmentAssociation
|
||
|
|
: null
|
||
|
|
const associationId = String(pending?.id || '').trim()
|
||
|
|
if (!associationId || pending?.status === 'confirmed') {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
const runtime = pendingAttachmentAssociations.get(associationId)
|
||
|
|
if (!runtime || !Array.isArray(runtime.files) || !runtime.files.length) {
|
||
|
|
toast('当前会话里没有可归集的附件原件,请重新上传票据后再确认。')
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
pending.status = 'confirmed'
|
||
|
|
message.pendingAttachmentAssociation = pending
|
||
|
|
message.text = buildConfirmedAssociationText(message)
|
||
|
|
message.meta = ['已确认归集']
|
||
|
|
persistSessionState()
|
||
|
|
|
||
|
|
if (pending.mode === 'save_then_associate') {
|
||
|
|
const inheritedReviewContext = buildReviewFormContextFromPayload(
|
||
|
|
activeReviewPayload.value,
|
||
|
|
reviewInlineForm.value
|
||
|
|
)
|
||
|
|
const savePayload = await submitComposer({
|
||
|
|
rawText: '请先把当前已识别的报销信息保存为草稿,随后继续归集本次上传的附件。',
|
||
|
|
userText: '',
|
||
|
|
files: [],
|
||
|
|
skipUserMessage: true,
|
||
|
|
pendingText: '正在先保存未保存单据...',
|
||
|
|
systemGenerated: true,
|
||
|
|
extraContext: {
|
||
|
|
...runtime.extraContext,
|
||
|
|
...inheritedReviewContext,
|
||
|
|
review_action: 'save_draft'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
const savedClaimId = String(savePayload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
||
|
|
const savedClaimNo = String(savePayload?.result?.draft_payload?.claim_no || '').trim()
|
||
|
|
if (!savedClaimId) {
|
||
|
|
toast('当前单据还没有保存成功,请稍后重试。')
|
||
|
|
return savePayload
|
||
|
|
}
|
||
|
|
|
||
|
|
return submitComposer({
|
||
|
|
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${savedClaimNo || '当前草稿'}`,
|
||
|
|
userText: `保存草稿并归集 ${runtime.fileNames.length} 份票据`,
|
||
|
|
files: runtime.files,
|
||
|
|
uploadDisposition: 'continue_existing',
|
||
|
|
skipDraftAssociationPrompt: true,
|
||
|
|
skipUserMessage: true,
|
||
|
|
appendToCurrentFlow: true,
|
||
|
|
systemGenerated: true,
|
||
|
|
pendingText: savedClaimNo
|
||
|
|
? `草稿 ${savedClaimNo} 已保存,正在识别并归集附件...`
|
||
|
|
: '草稿已保存,正在识别并归集附件...',
|
||
|
|
associationConfirmed: true,
|
||
|
|
extraContext: {
|
||
|
|
...runtime.extraContext,
|
||
|
|
review_action: 'link_to_existing_draft',
|
||
|
|
draft_claim_id: savedClaimId,
|
||
|
|
selected_claim_id: savedClaimId,
|
||
|
|
selected_claim_no: savedClaimNo,
|
||
|
|
attachment_association_confirmed: true
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return submitComposer({
|
||
|
|
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${runtime.claimNo || '当前草稿'}`,
|
||
|
|
userText: `确认归集到草稿 ${runtime.claimNo || '当前草稿'}`,
|
||
|
|
files: runtime.files,
|
||
|
|
uploadDisposition: 'continue_existing',
|
||
|
|
skipDraftAssociationPrompt: true,
|
||
|
|
pendingText: runtime.claimNo
|
||
|
|
? `正在将票据归集到草稿 ${runtime.claimNo}...`
|
||
|
|
: '正在将票据归集到当前草稿...',
|
||
|
|
associationConfirmed: true,
|
||
|
|
recognizedAttachmentData: {
|
||
|
|
ocrPayload: runtime.ocrPayload,
|
||
|
|
ocrSummary: runtime.ocrSummary,
|
||
|
|
ocrDocuments: runtime.ocrDocuments,
|
||
|
|
ocrFilePreviews: runtime.ocrFilePreviews
|
||
|
|
},
|
||
|
|
extraContext: {
|
||
|
|
...runtime.extraContext,
|
||
|
|
review_action: 'link_to_existing_draft',
|
||
|
|
draft_claim_id: runtime.claimId,
|
||
|
|
selected_claim_id: runtime.claimId,
|
||
|
|
selected_claim_no: runtime.claimNo,
|
||
|
|
attachment_association_confirmed: true
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
confirmPendingAttachmentAssociation,
|
||
|
|
createPendingAttachmentAssociationId,
|
||
|
|
emitSavedDraftRefresh,
|
||
|
|
normalizeRecognizedAttachmentData,
|
||
|
|
pendingAttachmentAssociations,
|
||
|
|
promptUnlinkedReceiptFolderIfNeeded,
|
||
|
|
resolveReviewPanelScope
|
||
|
|
}
|
||
|
|
}
|