feat(web): 工作台 AI 模式与差旅/风险建议交互优化
- 新增 PersonalWorkbenchAiMode 组件、AI 侧边栏与 orb 机器人视觉资源 - 新增 aiApplicationDraftModel / aiExpenseDraftModel / aiWorkbenchConversationStore 及业务准入 aiSidebarBusinessAccess,支撑 AI 模式下的申请与报销草稿 - 顶栏、侧边栏、工作台样式重构,适配 AI 模式切换与响应式布局 - 同步 steward plan/off_topic、差旅报销引导流、风险建议卡片等测试
This commit is contained in:
@@ -5,12 +5,15 @@ import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
|
||||
attachReceiptFolderIdsToFiles,
|
||||
buildAttachmentAssociationConfirmationMessage,
|
||||
buildOcrFilePreviews,
|
||||
buildReviewFilePreviewsFromReviewPayload,
|
||||
buildUnsavedDraftAttachmentConfirmationMessage,
|
||||
collectReceiptFiles,
|
||||
filterPersistableFilePreviews,
|
||||
mergeFilePreviews
|
||||
mergeFilePreviews,
|
||||
normalizeOcrDocuments
|
||||
} from '../src/views/scripts/travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
buildDraftAssociationQueryPayload,
|
||||
@@ -171,6 +174,90 @@ test('OCR preview builders keep hotel receipt image previews when preview kind i
|
||||
assert.deepEqual(reviewPreviews, [{ filename: 'hotel.png', kind: 'image', url: dataUrl }])
|
||||
})
|
||||
|
||||
test('OCR receipt folder ids are kept for final draft attachment association', () => {
|
||||
const files = [
|
||||
{ name: 'invoice.png' },
|
||||
{ name: 'taxi.pdf' }
|
||||
]
|
||||
const ocrPayload = {
|
||||
documents: [
|
||||
{
|
||||
filename: 'invoice.png',
|
||||
receipt_id: 'receipt-1',
|
||||
receipt_status: 'unlinked',
|
||||
receipt_preview_url: '/receipt-folder/receipt-1/preview',
|
||||
receipt_source_url: '/receipt-folder/receipt-1/source'
|
||||
},
|
||||
{
|
||||
filename: 'taxi.pdf',
|
||||
receipt_id: 'receipt-2',
|
||||
receipt_status: 'unlinked',
|
||||
receipt_preview_url: '/receipt-folder/receipt-2/preview',
|
||||
receipt_source_url: '/receipt-folder/receipt-2/source'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const documents = normalizeOcrDocuments(ocrPayload)
|
||||
|
||||
assert.equal(documents[0].receipt_id, 'receipt-1')
|
||||
assert.equal(documents[0].receipt_status, 'unlinked')
|
||||
assert.equal(documents[0].receipt_preview_url, '/receipt-folder/receipt-1/preview')
|
||||
assert.equal(documents[0].receipt_source_url, '/receipt-folder/receipt-1/source')
|
||||
assert.equal(attachReceiptFolderIdsToFiles(files, ocrPayload), 2)
|
||||
assert.equal(files[0].receiptId, 'receipt-1')
|
||||
assert.equal(files[1].receiptId, 'receipt-2')
|
||||
assert.equal(Object.getOwnPropertyDescriptor(files[0], 'receiptId')?.enumerable, false)
|
||||
})
|
||||
|
||||
test('receipt files are collected through a single OCR persistence entry before draft association', async () => {
|
||||
const files = [
|
||||
{ name: 'invoice.png' }
|
||||
]
|
||||
let recognizeCallCount = 0
|
||||
|
||||
const collected = await collectReceiptFiles({
|
||||
files,
|
||||
recognizeOcrFiles: async (inputFiles, options) => {
|
||||
recognizeCallCount += 1
|
||||
assert.equal(inputFiles, files)
|
||||
assert.equal(options.timeoutMs, 90000)
|
||||
return {
|
||||
documents: [
|
||||
{
|
||||
filename: 'invoice.png',
|
||||
summary: '发票金额 100 元',
|
||||
preview_kind: 'image',
|
||||
preview_data_url: 'data:image/png;base64,abc123',
|
||||
receipt_id: 'receipt-collect-1',
|
||||
receipt_status: 'unlinked',
|
||||
receipt_preview_url: '/receipt-folder/receipt-collect-1/preview',
|
||||
receipt_source_url: '/receipt-folder/receipt-collect-1/source'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(recognizeCallCount, 1)
|
||||
assert.equal(files[0].receiptId, 'receipt-collect-1')
|
||||
assert.equal(collected.ocrPayload.documents[0].receipt_id, 'receipt-collect-1')
|
||||
assert.equal(collected.ocrDocuments[0].receipt_id, 'receipt-collect-1')
|
||||
assert.equal(collected.ocrSummary, 'invoice.png:发票金额 100 元')
|
||||
assert.deepEqual(collected.ocrFilePreviews, [
|
||||
{ filename: 'invoice.png', kind: 'image', url: 'data:image/png;base64,abc123' }
|
||||
])
|
||||
|
||||
const submitComposerSource = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
assert.match(submitComposerSource, /collectReceiptFiles\(/)
|
||||
assert.doesNotMatch(submitComposerSource, /recognizeOcrFiles\(files,[\s\S]*attachReceiptFolderIdsToFiles/)
|
||||
assert.doesNotMatch(submitComposerSource, /ocrDocuments = normalizeOcrDocuments\(ocrPayload\)/)
|
||||
assert.doesNotMatch(submitComposerSource, /ocrFilePreviews = buildOcrFilePreviews\(ocrPayload\)/)
|
||||
})
|
||||
|
||||
test('file preview cache replaces temporary object urls and never persists them', () => {
|
||||
const merged = mergeFilePreviews(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user