Files
X-Financial/web/tests/ai-attachment-association-model.test.mjs

282 lines
9.3 KiB
JavaScript
Raw Normal View History

import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildAiAttachmentAssociationActions,
buildAiAttachmentAssociationMessage,
buildAiAttachmentAssociationResultMessage,
resolveAiAttachmentAssociationMatch
} from '../src/utils/aiAttachmentAssociationModel.js'
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
function findAction(actions, actionType) {
return actions.find((action) => action.action_type === actionType)
}
test('根据票据日期和行程自动匹配最可能关联的报销单', () => {
const claims = [
{
id: 'claim-wuhan-shanghai',
claim_no: 'BX-20260220-001',
expense_type: 'travel',
status: 'draft',
reason: '辅助国网仿生产服务器部署,武汉往返上海',
location: '上海',
occurred_at: '2026-02-20'
},
{
id: 'claim-nanjing',
claim_no: 'BX-20260301-002',
expense_type: 'travel',
status: 'draft',
reason: '南京客户拜访',
location: '南京',
occurred_at: '2026-03-01'
}
]
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: '武汉至上海高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-20' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
},
{
filename: '2月23 上海-武汉.pdf',
summary: '上海至武汉高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-23' },
{ key: 'route', label: '行程', value: '上海-武汉' }
]
}
]
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
assert.equal(match.highConfidence, true)
assert.equal(match.best.record.claimId, 'claim-wuhan-shanghai')
assert.match(match.best.reasons.join('、'), /日期/)
assert.match(match.best.reasons.join('、'), /地点|行程/)
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ocrDocuments.map((item) => item.filename),
ocrDocuments
})
assert.match(message, /BX-20260220-001/)
assert.match(message, /确认是否自动归集/)
assert.match(message, /ai-trusted-html:start/)
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
const html = renderAiConversationHtml(message)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /可能关联单据/)
assert.doesNotMatch(html, /26429165800002785705/)
const actions = buildAiAttachmentAssociationActions(match, 'assoc-1', { includeOcrDetails: true })
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.association_id, 'assoc-1')
assert.ok(findAction(actions, 'open_application_detail'))
})
test('自动归集消息展示票面 OCR 关键字段', () => {
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-wuhan-shanghai',
claim_no: 'BX-20260220-001',
expense_type: 'travel',
status: 'draft',
reason: '辅助国网仿生产服务器部署,武汉往返上海',
location: '上海',
occurred_at: '2026-02-20'
}
], [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
])
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments: [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
]
})
const html = renderAiConversationHtml(message)
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
assert.match(message, /票面识别/)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /金额354元/)
assert.match(html, /列车出发时间2026-02-20 07:55/)
assert.match(html, /行程:武汉-上海/)
})
test('自动归集卡片不把票据数字残片当成单据事项', () => {
const dirtyReason = ':26429165800002785705; :2026; 05'
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
document_fields: [
{ key: 'invoice_number', label: '', value: '26429165800002785705' },
{ key: 'year', label: '', value: '2026' },
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
]
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-dirty-reason',
claim_no: 'R74CB7C2R',
expense_type: 'travel',
status: 'draft',
reason: dirtyReason,
location: '上海',
occurred_at: '2026-02-20'
}
], ocrDocuments)
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments
})
const html = renderAiConversationHtml(message)
assert.match(html, /R74CB7C2R/)
assert.match(html, /ai-ocr-recognition-card/)
assert.match(html, /票面识别/)
assert.match(html, /金额354元/)
assert.match(html, /行程:武汉-上海/)
assert.doesNotMatch(html, /单据事项/)
assert.doesNotMatch(html, /26429165800002785705/)
assert.doesNotMatch(html, /:2026/)
})
test('没有可关联草稿时给出清晰提示', () => {
const match = resolveAiAttachmentAssociationMatch([
{
id: 'claim-submitted',
claim_no: 'BX-20260220-001',
status: 'submitted',
reason: '已提交报销单',
location: '上海',
occurred_at: '2026-02-20'
}
], [
{
filename: '2月20 武汉-上海.pdf',
summary: '武汉至上海高铁票',
document_fields: [
{ key: 'date', label: '日期', value: '2026-02-20' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
}
])
assert.equal(match.highConfidence, false)
assert.equal(match.rankedRecords.length, 0)
const message = buildAiAttachmentAssociationMessage({ match, fileNames: ['2月20 武汉-上海.pdf'] })
assert.match(message, /没有查到可关联的报销草稿/)
})
test('自动归集结果用卡片告知上传结果', () => {
const message = buildAiAttachmentAssociationResultMessage({
claimNo: 'BX-20260220-001',
uploadedCount: 2,
skippedCount: 0,
fileNames: ['2月20 武汉-上海.pdf', '2月23 上海-武汉.pdf']
})
const html = renderAiConversationHtml(message)
assert.match(message, /已完成自动归集/)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /2 份/)
assert.match(html, /BX-20260220-001/)
})
test('低置信候选也提供快捷确认关联动作', () => {
const claims = [
{
id: 'claim-shanghai',
claim_no: 'R74CB7C2R',
expense_type: 'travel',
status: 'draft',
reason: '出差报销',
location: '上海',
occurred_at: '2026-05-18'
}
]
const ocrDocuments = [
{
filename: '2月20 武汉-上海.pdf',
summary: 'G458 Wuhan Shanghaihongqiao',
document_fields: [
{ key: 'route', label: '行程', value: 'Wuhan Shanghaihongqiao' }
]
}
]
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
assert.equal(match.highConfidence, false)
assert.equal(match.recommended.record.claimId, 'claim-shanghai')
const message = buildAiAttachmentAssociationMessage({
match,
fileNames: ['2月20 武汉-上海.pdf'],
ocrDocuments
})
const html = renderAiConversationHtml(message)
assert.match(html, /候选单据待核对/)
assert.match(html, /ai-attachment-association-card/)
const actions = buildAiAttachmentAssociationActions(match, 'assoc-low-confidence', { includeOcrDetails: true })
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.claim_no, 'R74CB7C2R')
})
test('旧版纯文本关联消息也渲染为卡片', () => {
const legacyMessage = [
'### 我已先识别票据,并匹配到最可能的报销单',
'',
'本次附件1 份2月20 武汉-上海.pdf',
'',
'识别摘要2月20 武汉-上海.pdfG458 Wuhan Shanghaihongqiao',
'',
'推荐关联R74CB7C2R',
'',
'单据事项:上海差旅报销',
'',
'匹配依据:地点或行程包含 上海;当前单据仍是可归集草稿',
'',
'你可以直接点下方“查看匹配单据”核对详情,不需要再手动查找。'
].join('\n')
const html = renderAiConversationHtml(legacyMessage)
assert.match(html, /ai-attachment-association-card/)
assert.match(html, /R74CB7C2R/)
assert.doesNotMatch(html, /\*\*R74CB7C2R\*\*/)
assert.doesNotMatch(html, /ai-html-title/)
})