refactor(travel): split reimbursement create workflow
完整修改内容: - 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。 - 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。 - 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。 - 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。 - 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。 验证: - node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块 - npm --prefix web run build - node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs 说明: - 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。 - 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。 - TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
This commit is contained in:
@@ -127,6 +127,13 @@ export function resolveApplicationAmount(ontology) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApplicationTypedAmount(ontology, type) {
|
||||
const entity = resolveEntity(ontology, type)
|
||||
const rawValue = entity?.normalized_value || entity?.value || ''
|
||||
const numericValue = Number(String(rawValue).replace(/[^\d.]/g, ''))
|
||||
return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : 0
|
||||
}
|
||||
|
||||
export function resolveTimeRangeText(ontology) {
|
||||
const range = ontology?.time_range || {}
|
||||
if (range.start_date && range.end_date) {
|
||||
@@ -261,7 +268,8 @@ function cleanupApplicationReasonCandidate(value, location = '') {
|
||||
if (!text) return ''
|
||||
|
||||
text = text
|
||||
.replace(/^(?:发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|天数|出差天数|申请天数|出行方式|交通方式|交通工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额)\s*[::]\s*/u, '')
|
||||
.replace(/(?:请直接生成申请单核对结果|信息足够时生成申请单|但在入库或提交审批前仍需让我确认|请直接生成报销核对结果|需要创建草稿、绑定附件或提交审批前仍需让我确认)[\s\S]*$/u, '')
|
||||
.replace(/^(?:类型|申请类型|费用类型|报销类型|发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|天数|出差天数|申请天数|出行方式|交通方式|交通工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额)\s*[::]\s*/u, '')
|
||||
.replace(/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/gu, '')
|
||||
.replace(/(?:出差|申请)?(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/gu, '')
|
||||
.replace(/(?:¥|¥)?\s*\d+(?:\.\d+)?\s*(?:元|块|万元|人民币)?/gu, '')
|
||||
@@ -281,6 +289,7 @@ function cleanupApplicationReasonCandidate(value, location = '') {
|
||||
}
|
||||
|
||||
if (!text) return ''
|
||||
if (isInvalidApplicationReason(text)) return ''
|
||||
if (/^20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?$/.test(text)) return ''
|
||||
if (/^(?:\d+|[一二两三四五六七八九十]{1,3})\s*天$/.test(text)) return ''
|
||||
if (/^[\u4e00-\u9fa5]{1,8}$/.test(text) && !/服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(text)) {
|
||||
@@ -303,22 +312,44 @@ export function resolveApplicationReason(prompt, ontology = null) {
|
||||
const reasonEntity = resolveEntity(ontology, 'reason') || resolveEntity(ontology, 'business_reason')
|
||||
const entityReason = String(reasonEntity?.normalized_value || reasonEntity?.value || '').trim()
|
||||
if (entityReason) {
|
||||
return cleanupApplicationReasonCandidate(entityReason, location) || entityReason
|
||||
const cleanedEntityReason = cleanupApplicationReasonCandidate(entityReason, location)
|
||||
if (cleanedEntityReason && !isInvalidApplicationReason(cleanedEntityReason)) {
|
||||
return cleanedEntityReason
|
||||
}
|
||||
}
|
||||
|
||||
const labeled = resolvePromptField(prompt, ['事由', '申请事由', '出差事由', '原因', '用途'])
|
||||
if (labeled) {
|
||||
return cleanupApplicationReasonCandidate(labeled, location) || labeled
|
||||
const cleanedLabeledReason = cleanupApplicationReasonCandidate(labeled, location)
|
||||
if (cleanedLabeledReason && !isInvalidApplicationReason(cleanedLabeledReason)) {
|
||||
return cleanedLabeledReason
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = String(prompt || '')
|
||||
.split(/[\n,。;;]+/u)
|
||||
.map((item) => cleanupApplicationReasonCandidate(item, location))
|
||||
.filter(Boolean)
|
||||
.filter((item) => item && !isSystemGeneratedApplicationReason(item) && !isInvalidApplicationReason(item))
|
||||
const businessCandidate = candidates.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item))
|
||||
return businessCandidate || candidates.sort((left, right) => right.length - left.length)[0] || ''
|
||||
}
|
||||
|
||||
function isInvalidApplicationReason(value = '') {
|
||||
const compact = String(value || '').replace(/\s+/g, '')
|
||||
if (!compact) return true
|
||||
if (/^(?:类型|申请类型|费用类型|报销类型)[::]?/.test(compact)) return true
|
||||
if (/^(?:差旅费用申请|交通费用申请|住宿费用申请|费用申请|差旅费|交通费|住宿费)$/.test(compact)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function isSystemGeneratedApplicationReason(value = '') {
|
||||
const compact = String(value || '').replace(/\s+/g, '')
|
||||
return compact.startsWith('小财管家继续执行')
|
||||
|| compact.startsWith('处理要求')
|
||||
|| compact.startsWith('已识别信息')
|
||||
|| compact.startsWith('用户已补充')
|
||||
}
|
||||
|
||||
function resolveApplicationTransportMode(ontology, prompt) {
|
||||
const transportEntity = resolveEntity(ontology, 'transport_mode')
|
||||
|| resolveEntity(ontology, 'transport')
|
||||
@@ -383,6 +414,13 @@ export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser
|
||||
const reason = resolveApplicationReason(prompt, ontology) || '待补充'
|
||||
const days = resolvePromptDays(prompt)
|
||||
const transportMode = resolveApplicationTransportMode(ontology, prompt)
|
||||
const transportEstimatedAmount = resolveApplicationTypedAmount(ontology, 'transport_estimated_amount')
|
||||
const trainEstimatedAmount = resolveApplicationTypedAmount(ontology, 'train_estimated_amount')
|
||||
const flightEstimatedAmount = resolveApplicationTypedAmount(ontology, 'flight_estimated_amount')
|
||||
const hotelAmount = resolveApplicationTypedAmount(ontology, 'hotel_amount')
|
||||
const allowanceAmount = resolveApplicationTypedAmount(ontology, 'allowance_amount')
|
||||
const policyTotalAmount = resolveApplicationTypedAmount(ontology, 'policy_total_amount')
|
||||
const reimbursementAmount = resolveApplicationTypedAmount(ontology, 'reimbursement_amount')
|
||||
|
||||
const fields = {
|
||||
documentType: documentTypeEntity?.normalized_value || 'expense_application',
|
||||
@@ -393,6 +431,13 @@ export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser
|
||||
expenseTypeLabel: resolveExpenseTypeLabel(expenseTypeCode),
|
||||
amount: amount.value,
|
||||
amountDisplay: amount.value ? `¥${amount.value.toLocaleString('zh-CN')}` : '待补充',
|
||||
transportEstimatedAmount,
|
||||
trainEstimatedAmount,
|
||||
flightEstimatedAmount,
|
||||
hotelAmount,
|
||||
allowanceAmount,
|
||||
policyTotalAmount,
|
||||
reimbursementAmount,
|
||||
timeRange,
|
||||
location,
|
||||
reason,
|
||||
|
||||
Reference in New Issue
Block a user