feat(web): 工作台 AI 模式报销预审与文档查询模型拆分

- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出
- PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示
- 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试
- 新增 AI 文档卡片背景资源
This commit is contained in:
caoxiaozhu
2026-06-20 10:17:37 +08:00
parent 3d69f8501f
commit 304bbe1fd4
26 changed files with 3974 additions and 117 deletions

View File

@@ -0,0 +1,127 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
AI_APPLICATION_ACTION_SAVE_DRAFT,
AI_APPLICATION_ACTION_SUBMIT,
buildAiApplicationPreviewActionPayload
} from '../src/services/aiApplicationPreviewActions.js'
import {
applyApplicationPolicyEstimateResult,
buildApplicationPolicyEstimateRequest,
buildLocalApplicationPreview
} from '../src/utils/expenseApplicationPreview.js'
const applicationPreview = {
fields: {
applicationType: '差旅费用申请',
applicant: '曹笑竹',
grade: 'P5',
department: '技术部',
position: '财务智能化产品经理',
managerName: '向万红',
time: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '辅助国网仿生产服务器部署',
days: '4天',
transportMode: '火车',
lodgingDailyCap: '250元/天',
subsidyDailyCap: '100元/天',
transportPolicy: '按交通费用预估表暂估',
policyEstimate: '交通 720元 + 住宿 1,000元 + 补贴 400元 = 2,120元4天',
amount: '2,120元'
}
}
const currentUser = {
username: 'caoxiaozhu@xf.com',
name: '曹笑竹',
departmentName: '技术部',
position: '财务智能化产品经理',
grade: 'P5',
managerName: '向万红',
roleCodes: ['employee']
}
test('save application preview payload uses save draft action without submit wording', () => {
const payload = buildAiApplicationPreviewActionPayload({
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
applicationPreview,
currentUser,
conversationId: 'inline-1'
})
assert.equal(payload.user_id, 'caoxiaozhu@xf.com')
assert.equal(payload.conversation_id, 'inline-1')
assert.equal(payload.context_json.session_type, 'application')
assert.equal(payload.context_json.review_action, undefined)
assert.equal(payload.context_json.application_action, 'save_draft')
assert.equal(payload.context_json.application_preview.fields.transportMode, '火车')
assert.match(payload.message, /费用申请保存草稿/)
assert.match(payload.message, /保存草稿/)
assert.doesNotMatch(payload.message, /确认提交/)
})
test('submit application preview payload keeps existing draft id for resubmission', () => {
const payload = buildAiApplicationPreviewActionPayload({
actionType: AI_APPLICATION_ACTION_SUBMIT,
applicationPreview,
currentUser,
conversationId: 'inline-1',
draftPayload: {
claim_id: 'draft-001',
claim_no: 'AP-202602200001'
}
})
assert.equal(payload.context_json.review_action, undefined)
assert.equal(payload.context_json.application_edit_claim_id, 'draft-001')
assert.equal(payload.context_json.draft_claim_id, 'draft-001')
assert.match(payload.message, /费用申请确认提交/)
assert.match(payload.message, /确认提交/)
})
test('travel application preview calculates base standards before transport mode is selected', () => {
const preview = buildLocalApplicationPreview(
'2月20-23日去上海出差辅助国网仿生产服务器部署',
{ name: '曹笑竹', grade: 'P5', location: '武汉' },
{ today: '2026-06-20' }
)
const request = buildApplicationPolicyEstimateRequest(preview, { grade: 'P5', location: '武汉' })
assert.equal(request.canCalculate, true)
assert.deepEqual(request.payload, {
days: 4,
location: '上海',
grade: 'P5',
transport_mode: null,
origin_location: '武汉',
travel_date: '2026-02-20'
})
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
days: 4,
location: '上海',
matched_city: '上海',
grade: 'P5',
hotel_rate: 450,
hotel_amount: 1800,
total_allowance_rate: 100,
allowance_amount: 400,
transport_mode: '火车',
transport_origin: '武汉',
transport_destination: '上海',
transport_estimated_amount: 720,
total_amount: 2200,
rule_name: '公司差旅费报销规则',
rule_version: 'v1.0.0'
}, { grade: 'P5', location: '武汉' })
assert.equal(estimatedPreview.fields.transportMode, '')
assert.equal(estimatedPreview.missingFields.includes('出行方式'), true)
assert.equal(estimatedPreview.fields.lodgingDailyCap, '450元/天')
assert.equal(estimatedPreview.fields.subsidyDailyCap, '100元/天')
assert.equal(estimatedPreview.fields.transportPolicy, '选择火车、飞机或轮船后自动预估交通费用')
assert.equal(estimatedPreview.fields.policyEstimate, '交通待补充 + 住宿 1,800元 + 补贴 400元 = 2,200元4天不含交通')
assert.equal(estimatedPreview.fields.amount, '2,200元不含交通')
})