Files
X-Financial/web/src/utils/expenseApplicationPreview.js

1193 lines
50 KiB
JavaScript
Raw Normal View History

import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
import { evaluateLocalApplicationIntentGate } from './expenseApplicationIntentGate.js'
import {
buildMockApplicationTransportEstimate,
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
formatApplicationEstimateMoney,
parseApplicationEstimateMoney,
buildSystemApplicationEstimate
} from './expenseApplicationEstimate.js'
import { getTodayDateValue } from './workbenchComposerDate.js'
export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
{ key: 'applicationType', label: '申请类型' },
{ key: 'applicant', label: '姓名', editable: false, required: false },
{ key: 'grade', label: '职级', highlight: true, editable: false, required: false },
{ key: 'department', label: '部门', editable: false, required: false },
{ key: 'position', label: '岗位', editable: false, required: false },
{ key: 'managerName', label: '直属领导', editable: false, required: false },
{ key: 'time', label: '申请时间' },
{ key: 'location', label: '地点' },
{ key: 'reason', label: '事由' },
{ key: 'days', label: '天数' },
{ key: 'transportMode', label: '出行方式' },
{ key: 'lodgingDailyCap', label: '住宿上限/天', highlight: true, editable: false, required: false },
{ key: 'subsidyDailyCap', label: '补贴标准/天', highlight: true, editable: false, required: false },
{ key: 'transportPolicy', label: '交通费用口径', highlight: true, editable: false, required: false },
{ key: 'policyEstimate', label: '规则测算参考', highlight: true, editable: false, required: false },
{ key: 'amount', label: '系统预估费用', highlight: true, editable: false, required: false }
]
export const APPLICATION_TRANSPORT_MODE_OPTIONS = ['火车', '飞机', '轮船']
const APPLICATION_POLICY_PENDING_TEXT = '填写地点和天数后自动测算'
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '选择火车、飞机或轮船后自动预估交通费用'
export function resolveApplicationTimeLabel(applicationType = '') {
const label = String(applicationType || '').trim()
if (/差旅|出差/.test(label)) return '出发时间'
if (/招待|宴请|餐饮/.test(label)) return '招待时间'
return '申请时间'
}
function resolveApplicationFieldLabel(item, fields = {}) {
if (item.key === 'time') {
return resolveApplicationTimeLabel(fields.applicationType)
}
return item.label
}
function isTravelApplicationType(applicationType = '') {
return /差旅|出差/.test(String(applicationType || '').trim())
}
function resolveApplicationTripDateParts(fields = {}) {
const timeText = String(fields.time || '').trim()
const matchedDates = timeText.match(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g) || []
const startDate = normalizeDateText(matchedDates[0] || timeText)
const explicitEndDate = normalizeDateText(matchedDates[matchedDates.length - 1] || '')
const inferredEndDate = explicitEndDate && explicitEndDate !== startDate
? explicitEndDate
: buildEndDateFromDays(startDate, fields.days)
return {
startDate,
endDate: inferredEndDate || explicitEndDate || startDate
}
}
function compactText(value) {
return String(value || '').replace(/\s+/g, '')
}
function looksLikeStructuredTravelApplication(text) {
const source = String(text || '')
return /(?:发生时间|业务发生时间|申请时间|时间)\s*[:]/.test(source)
&& /(?:地点|业务地点|发生地点|目的地)\s*[:]/.test(source)
&& /(?:天数|出差天数|申请天数)\s*[:]?\s*(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/.test(source)
}
function resolveFirstMatch(text, patterns = []) {
for (const pattern of patterns) {
const match = text.match(pattern)
const value = String(match?.groups?.value || match?.[1] || '').trim()
if (value) return value.replace(/[,。;;]$/, '')
}
return ''
}
function normalizeDateText(value) {
return String(value || '').replace(/[/.]/g, '-').replace(/\s+/g, '')
}
function parseIsoDate(value) {
const match = normalizeDateText(value).match(/^(20\d{2})-(\d{1,2})-(\d{1,2})$/)
if (!match) return null
const [, year, month, day] = match
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)))
return Number.isNaN(date.getTime()) ? null : date
}
function formatIsoDate(date) {
return date.toISOString().slice(0, 10)
}
function buildEndDateFromDays(startText, daysText = '') {
const days = parseApplicationDaysValue(daysText)
const start = parseIsoDate(startText)
if (!days || !start) return ''
const end = new Date(start.getTime())
end.setUTCDate(end.getUTCDate() + Math.max(days - 1, 0))
return formatIsoDate(end)
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function buildDateFromMonthDay(year, month, day) {
const normalized = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
return parseIsoDate(normalized) ? normalized : ''
}
function resolveShortMonthDayRange(text, options = {}) {
const match = String(text || '').match(
/(?<startMonth>\d{1,2})月(?<startDay>\d{1,2})日?\s*(?:至|到|~|—||--|-)\s*(?:(?<endMonth>\d{1,2})月)?(?<endDay>\d{1,2})日/u
)
if (!match?.groups) return ''
const referenceYear = Number(resolvePreviewToday(options).slice(0, 4))
const startMonth = Number(match.groups.startMonth)
const startDay = Number(match.groups.startDay)
const endMonth = Number(match.groups.endMonth || match.groups.startMonth)
const endDay = Number(match.groups.endDay)
const startDate = buildDateFromMonthDay(referenceYear, startMonth, startDay)
const endYear = endMonth < startMonth ? referenceYear + 1 : referenceYear
const endDate = buildDateFromMonthDay(endYear, endMonth, endDay)
if (!startDate || !endDate) return ''
return startDate === endDate ? startDate : `${startDate}${endDate}`
}
function resolveDaysFromDateRange(rangeText) {
const match = String(rangeText || '').match(/(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})\s*至\s*(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/)
if (!match) return ''
const start = parseIsoDate(match[1])
const end = parseIsoDate(match[2])
if (!start || !end) return ''
const diffDays = Math.round((end.getTime() - start.getTime()) / 86400000)
return diffDays >= 0 ? `${diffDays + 1}` : ''
}
export function resolveApplicationDaysFromDateRange(rangeText) {
return resolveDaysFromDateRange(rangeText)
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function resolveApplicationValidationIssues(fields = {}) {
const issues = []
const rangeDaysText = resolveDaysFromDateRange(fields.time)
const rangeDays = parseApplicationDaysValue(rangeDaysText)
const explicitDays = parseApplicationDaysValue(fields.days)
if (rangeDays && explicitDays && rangeDays !== explicitDays) {
issues.push({
code: 'time_days_conflict',
field: 'days',
message: `申请时间 ${fields.time} 按自然日为 ${rangeDays} 天,但填写的是 ${explicitDays} 天。`
})
}
return issues
}
function shouldTrustModelApplicationFields(preview = {}) {
const status = String(preview?.modelReviewStatus || '').trim()
const strategy = String(preview?.parseStrategy || preview?.parse_strategy || '').trim()
return Boolean(preview?.modelRefined)
|| status === 'completed'
|| strategy === 'llm_primary'
}
function resolveApplicationSourceValidationIssues(sourceText = '', fields = {}, preview = {}) {
if (shouldTrustModelApplicationFields(preview)) {
return []
}
const issues = []
const locationCandidates = extractApplicationLocationCandidates(sourceText)
if (locationCandidates.length > 1) {
issues.push({
code: 'location_candidates_conflict',
field: 'location',
message: `当前输入里同时出现多个地点:${locationCandidates.join('、')}。请确认本次申请地点。`
})
}
const transportCandidates = extractApplicationTransportCandidates(sourceText)
if (transportCandidates.length > 1) {
issues.push({
code: 'transport_candidates_conflict',
field: 'transportMode',
message: `当前输入里同时出现多个出行方式:${transportCandidates.join('、')}。请确认本次申请使用哪一种。`
})
}
const amountCandidates = extractApplicationAmountCandidates(sourceText)
if (amountCandidates.length > 1) {
issues.push({
code: 'amount_candidates_conflict',
field: 'amount',
message: `当前输入里同时出现多个金额:${amountCandidates.join('、')}。请确认本次申请金额。`
})
}
return issues
}
export function shouldRequireApplicationModelReview(rawText = '') {
const text = String(rawText || '').trim()
const compact = compactText(text)
if (!compact) return false
const hasDateRange = /(?:20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}|(?:\d{1,2}月)?\d{1,2}日?)\s*(?:至|到|~|—||--|-)\s*(?:20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}|\d{1,2}月?\d{1,2}日?)/u.test(text)
const hasTravelIntent = /差旅|出差|去|到|赴|前往/.test(compact)
const hasTransport = /高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮/.test(compact)
const hasBusinessPurpose = /服务|支撑|支持|辅助|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(compact)
const hasInlinePurpose = hasBusinessPurpose && !/(?:事由|申请事由|出差事由|原因|用途)\s*[:]/.test(text)
const hasMultipleClauses = /[,。;;\n]/.test(text) || compact.length >= 24
const signalCount = [hasDateRange, hasTravelIntent, hasTransport, hasBusinessPurpose].filter(Boolean).length
return (hasInlinePurpose && signalCount >= 2) || (hasMultipleClauses && signalCount >= 3)
}
export function resolveApplicationDateRange(rangeText, daysText = '') {
const matchedDates = String(rangeText || '').match(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g) || []
const startDate = normalizeDateText(matchedDates[0] || '')
const explicitEndDate = normalizeDateText(matchedDates[matchedDates.length - 1] || '')
const inferredEndDate = explicitEndDate && explicitEndDate !== startDate
? explicitEndDate
: buildEndDateFromDays(startDate, daysText)
const endDate = inferredEndDate || explicitEndDate || startDate
const start = parseIsoDate(startDate)
const end = parseIsoDate(endDate)
if (!start || !end) {
return null
}
const orderedStart = start.getTime() <= end.getTime() ? start : end
const orderedEnd = start.getTime() <= end.getTime() ? end : start
return {
startDate: formatIsoDate(orderedStart),
endDate: formatIsoDate(orderedEnd),
startTime: orderedStart.getTime(),
endTime: orderedEnd.getTime()
}
}
export function applicationDateRangesOverlap(leftRange, rightRange) {
if (!leftRange || !rightRange) {
return false
}
return leftRange.startTime <= rightRange.endTime && rightRange.startTime <= leftRange.endTime
}
function resolvePreviewToday(options = {}) {
const explicitToday = String(options.today || options.currentDate || '').trim()
if (parseIsoDate(explicitToday)) return normalizeDateText(explicitToday)
if (options.now instanceof Date && !Number.isNaN(options.now.getTime())) {
return getTodayDateValue(options.now)
}
return getTodayDateValue()
}
function resolveApplicationType(text) {
const compact = compactText(text)
if (looksLikeStructuredTravelApplication(text)) return '差旅费用申请'
if (/差旅|出差|高铁|动车|火车|飞机|机票|航班|酒店|住宿/.test(compact)) return '差旅费用申请'
if (/交通|出租车|的士|网约车|打车|通勤/.test(compact)) return '交通费用申请'
if (/住宿|酒店/.test(compact)) return '住宿费用申请'
if (/会务|会议|发布会|展会/.test(compact)) return '会务费用申请'
if (/采购|办公用品|文具|设备|耗材/.test(compact)) return '采购费用申请'
if (/培训|课程|学习/.test(compact)) return '培训费用申请'
return '费用申请'
}
function resolveApplicationAmount(text) {
const compact = compactText(text)
const labeled = resolveFirstMatch(text, [
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
/(?:用户预估费用|预估费用|申请金额|预计金额|预计费用|预计总费用|预算|金额|费用)\s*[:]?\s*(?<value>\d+(?:\.\d+)?\s*(?:万|千|k|K)?)/u,
/(?<value>\d+(?:\.\d+)?\s*(?:(?:万|千|k|K)\s*(?:元|块|人民币)?|(?:元|块|人民币)))/u
])
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const normalized = normalizeApplicationAmountText(labeled)
if (normalized) return normalized
if (/不知道预算|预算不清楚|预算待定|待测算/.test(compact)) return '待测算'
return ''
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function normalizeApplicationAmountText(value) {
const text = String(value || '').replace(/[,]/g, '').trim()
const match = text.match(/(?<number>\d+(?:\.\d+)?)\s*(?<unit>万|千|k|K)?/u)
if (!match?.groups) return ''
let amount = Number(match.groups.number)
if (!Number.isFinite(amount) || amount <= 0) return ''
const unit = String(match.groups.unit || '').toLowerCase()
if (unit === '万') amount *= 10000
if (unit === '千' || unit === 'k') amount *= 1000
return `${Number.isInteger(amount) ? amount : Number(amount.toFixed(2))}`
}
function extractApplicationLocationCandidates(text) {
const candidates = []
const labeled = resolveFirstMatch(text, [
/(?:地点|业务地点|发生地点|目的地)\s*[:]\s*(?<value>[^。;;\n,]+)/u
])
if (labeled) candidates.push(normalizeLocationCandidate(labeled))
const compact = compactText(text)
const patterns = [
/(?:去|到|赴|前往)(?<value>[\u4e00-\u9fa5]{1,24})/gu,
/(?<value>[\u4e00-\u9fa5]{1,12})(?:出差|驻场)/gu
]
for (const pattern of patterns) {
for (const match of compact.matchAll(pattern)) {
candidates.push(normalizeLocationCandidate(match.groups?.value || ''))
}
}
return uniqueApplicationCandidates(candidates)
.filter((item) => !isInvalidApplicationLocationCandidate(item))
}
function normalizeLocationCandidate(value) {
let cleaned = String(value || '').replace(/\s+/g, '')
for (const marker of ['前往', '去', '到', '赴']) {
if (cleaned.includes(marker)) {
cleaned = cleaned.slice(cleaned.lastIndexOf(marker) + marker.length)
break
}
}
cleaned = cleaned
.replace(/^(?:去|到|赴|前往)/u, '')
.replace(/(?:出差|驻场|现场|支撑|支持|部署|上线|实施|拜访|验收|会议|采购|培训|协助|处理|办理|参加|进行).*$/u, '')
.replace(/[:,。;;、\s]/g, '')
return cleaned.replace(/^(北京|上海|天津|重庆)市$/u, '$1')
}
function isInvalidApplicationLocationCandidate(value) {
const compact = compactText(value)
if (!compact) return true
if (/^(?:类型|申请类型|费用类型|报销类型)$/.test(compact)) return true
if (/^(?:费用申请|差旅费用申请|交通费用申请|住宿费用申请|会务费用申请|采购费用申请|培训费用申请|报销申请|申请)$/.test(compact)) return true
if (/(?:类型|申请类型|费用类型|报销类型)/.test(compact)) return true
if (/(?:交通方式|出行方式|交通工具|预算|金额|费用|票据|附件|天数|时间|事由|原因|用途)/.test(compact)) return true
return false
}
function extractApplicationTransportCandidates(text) {
const compact = compactText(text)
return uniqueApplicationCandidates([
resolveApplicationTransportMode(resolveFirstMatch(text, [
/(?:出行方式|交通方式|交通工具|出行工具)\s*[:]\s*(?<value>[^。;;\n,]+)/u
])),
/高铁|动车|火车|铁路/.test(compact) ? '火车' : '',
/飞机|机票|航班/.test(compact) ? '飞机' : '',
/轮船|船票|客轮|渡轮|邮轮/.test(compact) ? '轮船' : ''
])
}
function extractApplicationAmountCandidates(text) {
const candidates = []
const source = String(text || '')
const labelPattern = /(?:用户预估费用|预估费用|申请金额|预计金额|预计费用|预计总费用|预算|金额|费用)\s*[:]?\s*(?<value>\d+(?:\.\d+)?\s*(?:万|千|k|K)?\s*(?:元|块|人民币)?)/gu
for (const match of source.matchAll(labelPattern)) {
candidates.push(normalizeApplicationAmountText(match.groups?.value || ''))
}
const amountPattern = /(?<value>\d+(?:\.\d+)?\s*(?:(?:万|千|k|K)\s*(?:元|块|人民币)?|(?:元|块|人民币)))/gu
for (const match of source.matchAll(amountPattern)) {
candidates.push(normalizeApplicationAmountText(match.groups?.value || ''))
}
return uniqueApplicationCandidates(candidates)
}
function uniqueApplicationCandidates(values) {
return values
.map((item) => String(item || '').trim())
.filter(Boolean)
.filter((item, index, list) => list.indexOf(item) === index)
}
function resolveCurrentUserGrade(currentUser = {}) {
return String(
currentUser.grade
|| currentUser.employeeGrade
|| currentUser.employee_grade
|| currentUser.profileGrade
|| ''
).trim()
}
function resolveCurrentUserDepartment(currentUser = {}) {
return String(
currentUser.department
|| currentUser.departmentName
|| currentUser.department_name
|| ''
).trim()
}
function resolveCurrentUserPosition(currentUser = {}) {
return String(
currentUser.position
|| currentUser.employeePosition
|| currentUser.employee_position
|| currentUser.jobTitle
|| currentUser.job_title
|| ''
).trim()
}
function resolveCurrentUserManagerName(currentUser = {}) {
return String(
currentUser.managerName
|| currentUser.manager_name
|| currentUser.directManagerName
|| currentUser.direct_manager_name
|| currentUser.leaderName
|| currentUser.leader_name
|| ''
).trim()
}
function parseApplicationDaysValue(value) {
const match = String(value || '').match(/\d+/)
const days = match ? Number(match[0]) : parseChineseNumber(value)
return Number.isFinite(days) && days > 0 ? Math.max(1, Math.floor(days)) : 0
}
function parseChineseNumber(value) {
const digits = {
: 1,
: 2,
: 2,
: 3,
: 4,
: 5,
: 6,
: 7,
: 8,
: 9
}
const text = String(value || '').match(/[一二两三四五六七八九十]{1,3}/)?.[0] || ''
if (!text) return 0
if (text === '十') return 10
if (text.includes('十')) {
const [left, right] = text.split('十')
const tens = left ? digits[left] || 0 : 1
const ones = right ? digits[right] || 0 : 0
return tens * 10 + ones
}
return digits[text] || 0
}
function parseMoneyNumber(value) {
const normalized = String(value ?? '').replace(/[^\d.-]/g, '')
const amount = Number(normalized)
return Number.isFinite(amount) ? amount : null
}
function formatPolicyMoney(value) {
const amount = parseMoneyNumber(value)
if (amount === null) return String(value || '').trim()
return new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
}).format(amount)
}
function formatDailyPolicyMoney(value) {
const display = formatPolicyMoney(value)
return display ? `${display}元/天` : APPLICATION_POLICY_PENDING_TEXT
}
function buildTransportPolicyText(transportMode, location = '', transportEstimate = null, time = '') {
const mode = String(transportMode || '').trim()
const estimate = transportEstimate || buildMockApplicationTransportEstimate({ transportMode: mode, location, time })
if (!estimate) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
return estimate.basisText
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function buildTransportEstimateFromPolicyResult(result = {}, fields = {}) {
const amount = parseMoneyNumber(result?.transport_estimated_amount)
if (!amount || amount <= 0) return null
const amountDisplay = formatPolicyMoney(amount)
const mode = String(result?.transport_mode || fields.transportMode || '').trim()
const origin = String(result?.transport_origin || '').trim()
const destination = String(result?.transport_destination || result?.matched_city || fields.location || '').trim()
const basis = String(result?.transport_estimate_basis || '').trim()
const ruleName = String(result?.transport_estimate_rule_name || '交通费用预估表').trim()
const routeText = [origin, destination].filter(Boolean).join('-')
const modeText = mode ? `${mode}往返` : '往返'
const routeModeText = routeText ? `${routeText}${modeText}` : modeText
const displayBasis = routeModeText && basis.startsWith(routeModeText)
? basis.slice(routeModeText.length).trim()
: basis
const basisSuffix = displayBasis ? `${displayBasis}` : ''
return {
mode,
amount,
amountDisplay,
routeType: '往返',
origin,
destination,
queryDate: String(result?.travel_date || '').trim(),
source: String(result?.transport_estimate_source || 'basic_rule_transport_estimate').trim(),
confidence: String(result?.transport_estimate_confidence || 'basic_rule').trim(),
basis,
ruleCode: String(result?.transport_estimate_rule_code || '').trim(),
ruleName,
ruleVersion: String(result?.transport_estimate_rule_version || '').trim(),
basisText: `当前尚未接通实时票务价格查询 API无法获取当前实际票价先按《${ruleName}${routeModeText}${basisSuffix}暂估 ${amountDisplay}元用于申请阶段预算占用,最终报销以实际票据金额为准`
}
}
function ensureApplicationPolicyFields(fields = {}) {
const nextFields = { ...fields }
if (!String(nextFields.lodgingDailyCap || '').trim()) {
nextFields.lodgingDailyCap = APPLICATION_POLICY_PENDING_TEXT
}
if (!String(nextFields.subsidyDailyCap || '').trim()) {
nextFields.subsidyDailyCap = APPLICATION_POLICY_PENDING_TEXT
}
if (!String(nextFields.transportPolicy || '').trim() || /实报实销/.test(String(nextFields.transportPolicy || ''))) {
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode, nextFields.location, null, nextFields.time)
}
if (!String(nextFields.policyEstimate || '').trim()) {
nextFields.policyEstimate = APPLICATION_POLICY_PENDING_TEXT
}
return nextFields
}
function resolveApplicationDays(text) {
const value = resolveFirstMatch(text, [
/(?:出差|申请)?(?<value>\d+)\s*天/u,
/(?<value>\d+)\s*(?:个)?工作日/u
])
return value ? `${value}` : ''
}
function resolveApplicationTime(text, daysText = '', options = {}) {
const range = text.match(
/(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})\s*(?:至|到|~|—||--)\s*(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u
)
if (range) {
return `${normalizeDateText(range[1])}${normalizeDateText(range[2])}`
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const shortMonthDayRange = resolveShortMonthDayRange(text, options)
if (shortMonthDayRange) {
return shortMonthDayRange
}
const single = resolveFirstMatch(text, [
/(?:发生时间|业务发生时间|申请时间|时间)\s*[:]\s*(?<value>20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u,
/(?<value>20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u
])
if (!single) return ''
const normalized = normalizeDateText(single)
const endDate = buildEndDateFromDays(normalized, daysText)
return endDate && endDate !== normalized ? `${normalized}${endDate}` : normalized
}
function resolveApplicationTimeWithDefault(text, daysText = '', options = {}) {
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const resolvedTime = resolveApplicationTime(text, daysText, options)
if (resolvedTime || !parseApplicationDaysValue(daysText)) {
return resolvedTime
}
const startDate = resolvePreviewToday(options)
const endDate = buildEndDateFromDays(startDate, daysText)
return endDate && endDate !== startDate ? `${startDate}${endDate}` : startDate
}
function resolveApplicationLocation(text) {
return resolveFirstMatch(text, [
/(?:地点|业务地点|发生地点|目的地)\s*[:]\s*(?<value>[^。;;\n]+)/u,
/(?:去|到|前往)(?<value>[\u4e00-\u9fa5,、]{2,24}?)(?:出差|支撑|支持|部署|开会|培训|拜访|验收|项目|客户|。|\s|$)/u
])
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function looksLikeTransportPromptText(text) {
const compact = compactText(text)
return /(?:还需要补充|当前还缺|缺少|请先补充|请选择|可以选择|可以选|打算怎么出行|怎么出行).{0,24}(?:出行方式|交通方式|交通工具)/u.test(compact)
|| /(?:出行方式|交通方式|交通工具).{0,24}(?:待补充|缺失|请选择|可以选择|可以选)/u.test(compact)
}
function resolveApplicationTransportMode(text) {
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const labeled = resolveFirstMatch(text, [
/(?:出行方式|交通方式|交通工具|出行工具)\s*[:]\s*(?<value>[^。;;\n,]+)/u
])
const labeledMode = normalizeTransportModeOption(labeled, '')
if (labeledMode && !looksLikeTransportPromptText(labeled) && !/(?:请选择|可以选择|可以选)/u.test(String(labeled || ''))) {
return labeledMode
}
const fullTextLooksLikePrompt = looksLikeTransportPromptText(text)
const segments = String(text || '')
.split(/[\n,。;;]+/u)
.map((item) => item.trim())
.filter(Boolean)
for (const segment of segments) {
if (looksLikeTransportPromptText(segment)) continue
const compactSegment = compactText(segment)
if (
fullTextLooksLikePrompt
&& !/(?:申请|出差|去|到|赴|前往|服务|支撑|支持|部署|上线|实施|拜访|会议)/u.test(compactSegment)
) {
continue
}
if (/高铁|动车|火车|铁路/.test(compactSegment)) return '火车'
if (/飞机|机票|航班/.test(compactSegment)) return '飞机'
if (/轮船|船票|客轮|渡轮/.test(compactSegment)) return '轮船'
}
if (fullTextLooksLikePrompt) return ''
const compact = compactText(text)
if (/高铁|动车|火车|铁路/.test(compact)) return '火车'
if (/飞机|机票|航班/.test(compact)) return '飞机'
if (/轮船|船票|客轮|渡轮/.test(compact)) return '轮船'
return ''
}
function stripKnownContextFromReason(value, context = {}) {
const location = String(context.location || '').trim()
let cleaned = String(value || '')
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
.replace(/(?:请直接生成申请单核对结果|信息足够时生成申请单|但在入库或提交审批前仍需让我确认|请直接生成报销核对结果|需要创建草稿、绑定附件或提交审批前仍需让我确认)[\s\S]*$/u, '')
.replace(/(?:发生时间|业务发生时间|申请时间|时间)\s*[:]\s*(?=[,、。;;\s]|$)/gu, '')
.replace(/(?:地点|业务地点|发生地点|目的地)\s*[:]\s*(?=[,、。;;\s]|$)/gu, '')
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}\s*(?:至|到|~|—||--)\s*20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
.replace(/(?:出差|申请)?\d+\s*天/gu, '')
.replace(/(?:用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)\s*[:]?\s*\d+(?:\.\d+)?\s*(?:元|块|人民币)?/gu, '')
.replace(/(?:高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮|出租车|的士|网约车|打车|自驾)/gu, '')
.replace(/[,、。;;]+/g, '')
.replace(/^\s*(去|到|前往)/u, '')
.replace(/^[\s]+|[\s]+$/g, '')
.trim()
if (location) {
const escapedLocation = location.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
cleaned = cleaned
.replace(new RegExp(`^${escapedLocation}(?:出差)?(?:|,|、)?`, 'u'), '')
.replace(new RegExp(`^(?:去|到|前往)${escapedLocation}(?:出差)?(?:|,|、)?`, 'u'), '')
.trim()
}
return cleaned
}
function pickBusinessReasonSegment(text) {
const segments = String(text || '')
.split(/[,、。;;\n]+/u)
.map((item) => item.trim())
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
.filter((item) => item && !isSystemGeneratedReasonText(item))
return segments.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item)) || ''
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function isSystemGeneratedReasonText(value = '') {
const compact = compactText(value)
return compact.startsWith('小财管家继续执行')
|| /请直接生成申请单核对结果|信息足够时生成申请单|入库或提交审批前|请直接生成报销核对结果/.test(compact)
|| compact.startsWith('处理要求')
|| compact.startsWith('已识别信息')
|| compact.startsWith('用户已补充')
|| /^(?:类型|申请类型|费用类型|报销类型)[:]?(?:差旅费用申请|交通费用申请|住宿费用申请|费用申请|差旅费|交通费|住宿费)?$/.test(compact)
}
function resolveApplicationReason(text, context = {}) {
const labeled = resolveFirstMatch(text, [
/(?:事由|申请事由|出差事由|原因|用途)\s*[:]\s*(?<value>[^,。;;\n]+)/u
])
if (labeled) return stripKnownContextFromReason(labeled, context)
const cleaned = String(text || '')
.replace(/^\s*(我想|我要|帮我)?\s*(先)?\s*(发起|提交|申请)?\s*(一笔)?\s*(费用申请|申请)\s*/u, '')
const withoutContext = stripKnownContextFromReason(cleaned, context)
const businessSegment = pickBusinessReasonSegment(withoutContext)
if (businessSegment) return stripKnownContextFromReason(businessSegment, context)
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
if (isSystemGeneratedReasonText(withoutContext)) return ''
return withoutContext
}
function isApplicationPreviewValueProvided(value) {
const normalized = String(value || '').trim()
return Boolean(normalized) && !['待测算', '待补充', '未知'].includes(normalized)
}
function resolveProvidedValue(value, fallback = '') {
return isApplicationPreviewValueProvided(value) ? String(value).trim() : fallback
}
function normalizeApplicationTypeLabel(value, fallback = '') {
const label = String(value || '').trim()
if (!label || label === '其他费用') return fallback || '费用申请'
if (label.endsWith('费用申请') || label.endsWith('申请')) return label
if (label.endsWith('费用')) return `${label}申请`
if (label.endsWith('费')) return `${label.slice(0, -1)}费用申请`
return `${label}申请`
}
export function normalizeTransportModeOption(value, fallback = '') {
const text = String(value || '').trim()
if (/飞机|机票|航班/.test(text)) return '飞机'
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
return APPLICATION_TRANSPORT_MODE_OPTIONS.includes(text) ? text : fallback
}
function resolveModelRefinedTransportMode(ontologyFields = {}, rawText = '', currentFields = {}) {
const currentTransportMode = isApplicationPreviewValueProvided(currentFields.transportMode)
? String(currentFields.transportMode).trim()
: ''
const explicitTransportMode = resolveApplicationTransportMode(rawText)
if (!explicitTransportMode) {
return currentTransportMode
}
const ontologyTransportMode = normalizeTransportModeOption(ontologyFields.transportMode, '')
if (ontologyTransportMode && ontologyTransportMode === explicitTransportMode) {
return ontologyTransportMode
}
return currentTransportMode || explicitTransportMode
}
function normalizeAmountFromOntology(fields = {}, fallback = '') {
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const numericAmount = Number(fields.amount || fields.policyTotalAmount || fields.reimbursementAmount || 0)
if (Number.isFinite(numericAmount) && numericAmount > 0) {
return `${numericAmount}`
}
const display = String(fields.amountDisplay || '').trim()
if (display && display !== '待补充') {
const normalized = display.replace(/^¥\s*/, '').replace(/,/g, '').trim()
return normalized.endsWith('元') ? normalized : `${normalized}`
}
return fallback
}
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
function normalizeTypedOntologyAmount(value, fallback = '') {
const amount = Number(value || 0)
if (Number.isFinite(amount) && amount > 0) {
return `${amount}`
}
return fallback
}
function buildMissingFields(fields) {
return APPLICATION_PREVIEW_FIELD_DEFINITIONS
.filter((item) => item.key !== 'applicationType' && item.required !== false)
.filter((item) => !isApplicationPreviewValueProvided(fields?.[item.key]))
.map((item) => resolveApplicationFieldLabel(item, fields))
}
export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser = {}) {
const normalized = normalizeApplicationPreview(preview)
const fields = normalized.fields || {}
const days = parseApplicationDaysValue(fields.days)
const location = String(fields.location || '').trim()
const grade = String(fields.grade || resolveCurrentUserGrade(currentUser)).trim()
const applicationType = String(fields.applicationType || '').trim()
const transportMode = String(fields.transportMode || '').trim()
const shouldEstimate = /差旅|住宿|交通/.test(applicationType) || Boolean(transportMode)
if (!shouldEstimate || !days || !location) {
return {
canCalculate: false,
reason: '缺少地点或天数',
payload: null
}
}
return {
canCalculate: true,
reason: '',
payload: {
days,
location,
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
grade,
transport_mode: transportMode || null,
origin_location: String(
currentUser.location
|| currentUser.officeLocation
|| currentUser.office_location
|| currentUser.baseCity
|| currentUser.base_city
|| ''
).trim() || null,
travel_date: resolveApplicationTripDateParts(fields).startDate || null
}
}
}
export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, currentUser = {}) {
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const fields = {
...(preview?.fields || {})
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
}
const hotelRate = formatPolicyMoney(result?.hotel_rate)
const hotelAmount = formatPolicyMoney(result?.hotel_amount)
const allowanceRate = formatPolicyMoney(result?.total_allowance_rate)
const allowanceAmount = formatPolicyMoney(result?.allowance_amount)
const matchedCity = String(result?.matched_city || fields.location || '').trim()
const grade = String(result?.grade || fields.grade || resolveCurrentUserGrade(currentUser)).trim()
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
if (isTravelApplicationType(fields.applicationType) && !String(fields.transportMode || '').trim()) {
const days = Number(result?.days) || parseApplicationDaysValue(fields.days) || 1
const baseTotalAmount = parseMoneyNumber(result?.hotel_amount) + parseMoneyNumber(result?.allowance_amount)
const baseTotalDisplay = Number.isFinite(baseTotalAmount) && baseTotalAmount > 0
? formatPolicyMoney(baseTotalAmount)
: ''
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
return normalizeApplicationPreview({
...preview,
fields: {
...fields,
grade,
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
transportPolicy: APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT,
policyEstimate: baseTotalDisplay
? `交通待补充 + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${baseTotalDisplay}元(${days}天,不含交通)`
: APPLICATION_POLICY_PENDING_TEXT,
amount: baseTotalDisplay ? `${baseTotalDisplay}元(不含交通)` : fields.amount,
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
matchedCity,
ruleName: String(result?.rule_name || '').trim(),
ruleVersion: String(result?.rule_version || '').trim(),
hotelAmount: hotelAmount ? `${hotelAmount}` : '',
allowanceAmount: allowanceAmount ? `${allowanceAmount}` : '',
transportEstimatedAmount: '',
transportEstimateDate: '',
transportQueryLatencyMs: '',
transportEstimateSource: '',
transportEstimateConfidence: '',
policyTotalAmount: baseTotalDisplay ? `${baseTotalDisplay}元(不含交通)` : ''
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
},
policyEstimateStatus: 'pending'
})
}
const days = Number(result?.days) || parseApplicationDaysValue(fields.days) || 1
let systemEstimate = buildSystemApplicationEstimate({
transportMode: fields.transportMode,
location: matchedCity || fields.location,
time: fields.time,
lodgingAmount: result?.hotel_amount,
allowanceAmount: result?.allowance_amount
})
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const policyTransportEstimate = buildTransportEstimateFromPolicyResult(result, fields)
if (policyTransportEstimate) {
const lodging = parseApplicationEstimateMoney(result?.hotel_amount)
const allowance = parseApplicationEstimateMoney(result?.allowance_amount)
const backendTotal = parseApplicationEstimateMoney(result?.total_amount)
const totalAmount = backendTotal > 0
? backendTotal
: policyTransportEstimate.amount + lodging + allowance
systemEstimate = {
transportEstimate: policyTransportEstimate,
transportAmount: policyTransportEstimate.amount,
lodgingAmount: lodging,
allowanceAmount: allowance,
totalAmount,
transportAmountDisplay: policyTransportEstimate.amountDisplay,
lodgingAmountDisplay: formatApplicationEstimateMoney(lodging),
allowanceAmountDisplay: formatApplicationEstimateMoney(allowance),
totalAmountDisplay: formatApplicationEstimateMoney(totalAmount)
}
}
const transportEstimate = systemEstimate.transportEstimate
const transportText = transportEstimate
? `交通 ${systemEstimate.transportAmountDisplay}元 + `
: ''
const totalAmount = systemEstimate.totalAmountDisplay
const amount = totalAmount ? `${totalAmount}` : fields.amount
return normalizeApplicationPreview({
...preview,
fields: {
...fields,
grade,
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
transportPolicy: buildTransportPolicyText(fields.transportMode, matchedCity || fields.location, transportEstimate, fields.time),
policyEstimate: `${transportText}住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`,
amount,
matchedCity,
ruleName: String(result?.rule_name || '').trim(),
ruleVersion: String(result?.rule_version || '').trim(),
hotelAmount: hotelAmount ? `${hotelAmount}` : '',
allowanceAmount: allowanceAmount ? `${allowanceAmount}` : '',
transportEstimatedAmount: systemEstimate.transportAmountDisplay ? `${systemEstimate.transportAmountDisplay}` : '',
transportEstimateDate: transportEstimate?.queryDate || '',
transportQueryLatencyMs: transportEstimate?.simulatedLatencyMs ? `${transportEstimate.simulatedLatencyMs}ms` : '',
transportEstimateSource: transportEstimate?.source || '',
transportEstimateConfidence: transportEstimate?.confidence || '',
policyTotalAmount: totalAmount ? `${totalAmount}` : ''
},
policyEstimate: {
...result,
grade,
matchedCity,
transport_estimate: transportEstimate,
system_total_amount: systemEstimate.totalAmount
},
policyEstimateStatus: 'completed'
})
}
export function refreshApplicationPreviewTransportEstimate(preview = {}) {
const normalized = normalizeApplicationPreview(preview)
const fields = { ...(normalized.fields || {}) }
const policyResult = normalized.policyEstimate && typeof normalized.policyEstimate === 'object'
? normalized.policyEstimate
: {}
const location = String(fields.matchedCity || policyResult.matched_city || fields.location || '').trim()
const hotelAmountSource = fields.hotelAmount || policyResult.hotel_amount || 0
const allowanceAmountSource = fields.allowanceAmount || policyResult.allowance_amount || 0
const systemEstimate = buildSystemApplicationEstimate({
transportMode: fields.transportMode,
location,
time: fields.time,
lodgingAmount: hotelAmountSource,
allowanceAmount: allowanceAmountSource
})
const transportEstimate = systemEstimate.transportEstimate
if (!transportEstimate) return normalized
const hotelAmount = formatPolicyMoney(hotelAmountSource)
const allowanceAmount = formatPolicyMoney(allowanceAmountSource)
const hasPolicyAmounts = parseMoneyNumber(hotelAmountSource) > 0 || parseMoneyNumber(allowanceAmountSource) > 0
const nextFields = {
...fields,
transportPolicy: buildTransportPolicyText(fields.transportMode, location, transportEstimate, fields.time),
transportEstimatedAmount: systemEstimate.transportAmountDisplay ? `${systemEstimate.transportAmountDisplay}` : '',
transportEstimateDate: transportEstimate.queryDate || '',
transportQueryLatencyMs: transportEstimate.simulatedLatencyMs ? `${transportEstimate.simulatedLatencyMs}ms` : '',
transportEstimateSource: transportEstimate.source || '',
transportEstimateConfidence: transportEstimate.confidence || ''
}
if (hasPolicyAmounts) {
const days = Number(policyResult.days) || parseApplicationDaysValue(fields.days) || 1
const totalAmount = systemEstimate.totalAmountDisplay
nextFields.policyEstimate = `交通 ${systemEstimate.transportAmountDisplay}元 + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`
nextFields.amount = totalAmount ? `${totalAmount}` : nextFields.amount
nextFields.policyTotalAmount = totalAmount ? `${totalAmount}` : ''
}
return normalizeApplicationPreview({
...normalized,
fields: nextFields,
policyEstimate: {
...policyResult,
matchedCity: location,
transport_estimate: transportEstimate,
system_total_amount: systemEstimate.totalAmount
}
})
}
export function applyApplicationPolicyEstimateError(preview = {}, error = null, currentUser = {}) {
const fields = { ...(preview?.fields || {}) }
const message = String(error?.message || error || '').trim()
return normalizeApplicationPreview({
...preview,
fields: {
...fields,
grade: fields.grade || resolveCurrentUserGrade(currentUser),
transportPolicy: buildTransportPolicyText(fields.transportMode, fields.location, null, fields.time),
policyEstimate: message ? `规则中心暂未完成测算:${message}` : APPLICATION_POLICY_PENDING_TEXT
},
policyEstimateStatus: message ? 'failed' : 'pending'
})
}
export function shouldUseLocalApplicationPreview(rawText, options = {}) {
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
return evaluateLocalApplicationIntentGate(rawText, options).allowed
}
export function normalizeApplicationPreview(preview = {}) {
const fields = ensureApplicationPolicyFields(preview?.fields || {})
const missingFields = buildMissingFields(fields)
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const validationIssues = [
...resolveApplicationValidationIssues(fields),
...resolveApplicationSourceValidationIssues(preview?.sourceText, fields, preview)
]
return {
...preview,
fields,
missingFields,
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
validationIssues,
readyToSubmit: missingFields.length === 0 && validationIssues.length === 0
}
}
export function applyApplicationBusinessTimeContext(preview = {}, businessTimeContext = null) {
if (!businessTimeContext || typeof businessTimeContext !== 'object') {
return normalizeApplicationPreview(preview)
}
const startDate = String(businessTimeContext.start_date || '').trim()
const endDate = String(businessTimeContext.end_date || startDate).trim()
const displayValue = String(
businessTimeContext.business_time ||
businessTimeContext.time_range ||
businessTimeContext.display_value ||
''
).trim()
const time = startDate && endDate
? (startDate === endDate ? startDate : `${startDate}${endDate}`)
: displayValue
if (!time) {
return normalizeApplicationPreview(preview)
}
const normalized = normalizeApplicationPreview(preview)
const fields = normalized.fields || {}
return normalizeApplicationPreview({
...normalized,
fields: {
...fields,
time,
days: resolveDaysFromDateRange(time) || fields.days
}
})
}
export function buildModelRefinedApplicationPreview(localPreview = {}, ontology = {}, rawText = '', currentUser = {}) {
const currentFields = localPreview?.fields || {}
const ontologyFields = buildApplicationFieldsFromOntology(ontology || {}, rawText, currentUser)
const parseStrategy = String(ontology?.parse_strategy || '').trim()
const refinedFields = {
...currentFields,
applicationType: normalizeApplicationTypeLabel(
ontologyFields.expenseTypeLabel,
currentFields.applicationType
),
time: resolveProvidedValue(ontologyFields.timeRange, currentFields.time),
location: resolveProvidedValue(ontologyFields.location, currentFields.location),
reason: resolveProvidedValue(ontologyFields.reason, currentFields.reason),
days: resolveProvidedValue(ontologyFields.days, currentFields.days),
transportMode: resolveModelRefinedTransportMode(ontologyFields, rawText, currentFields),
amount: normalizeAmountFromOntology(ontologyFields, currentFields.amount),
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
transportEstimatedAmount: normalizeTypedOntologyAmount(
ontologyFields.transportEstimatedAmount || ontologyFields.trainEstimatedAmount || ontologyFields.flightEstimatedAmount,
currentFields.transportEstimatedAmount
),
trainEstimatedAmount: normalizeTypedOntologyAmount(ontologyFields.trainEstimatedAmount, currentFields.trainEstimatedAmount),
flightEstimatedAmount: normalizeTypedOntologyAmount(ontologyFields.flightEstimatedAmount, currentFields.flightEstimatedAmount),
hotelAmount: normalizeTypedOntologyAmount(ontologyFields.hotelAmount, currentFields.hotelAmount),
allowanceAmount: normalizeTypedOntologyAmount(ontologyFields.allowanceAmount, currentFields.allowanceAmount),
policyTotalAmount: normalizeTypedOntologyAmount(ontologyFields.policyTotalAmount, currentFields.policyTotalAmount),
reimbursementAmount: normalizeTypedOntologyAmount(ontologyFields.reimbursementAmount, currentFields.reimbursementAmount),
grade: resolveProvidedValue(currentFields.grade, resolveCurrentUserGrade(currentUser)),
applicant: resolveProvidedValue(ontologyFields.applicant, currentFields.applicant),
department: resolveProvidedValue(ontologyFields.department, currentFields.department || resolveCurrentUserDepartment(currentUser)),
position: resolveProvidedValue(currentFields.position, resolveCurrentUserPosition(currentUser)),
managerName: resolveProvidedValue(
ontologyFields.managerName,
currentFields.managerName || resolveCurrentUserManagerName(currentUser)
)
}
return normalizeApplicationPreview({
...localPreview,
sourceText: String(rawText || localPreview.sourceText || '').trim(),
fields: refinedFields,
modelRefined: true,
parseStrategy,
modelReviewStatus: parseStrategy === 'llm_primary' ? 'completed' : 'fallback'
})
}
export function buildApplicationPreviewRows(preview = {}) {
const normalized = normalizeApplicationPreview(preview)
const fields = normalized.fields || {}
return APPLICATION_PREVIEW_FIELD_DEFINITIONS.flatMap((item) => {
if (item.key === 'time' && isTravelApplicationType(fields.applicationType)) {
const tripDates = resolveApplicationTripDateParts(fields)
const rawValue = fields[item.key]
const missing = item.required !== false && !isApplicationPreviewValueProvided(rawValue)
return [
{
...item,
label: '出发时间',
value: tripDates.startDate || '待补充',
editable: item.editable !== false,
highlight: Boolean(item.highlight),
missing
},
{
key: 'time_return',
label: '返回时间',
value: tripDates.endDate || '待补充',
editable: false,
highlight: Boolean(item.highlight),
missing
}
]
}
const rawValue = fields[item.key]
const value = String(rawValue || '').trim() || '待补充'
return [{
...item,
label: resolveApplicationFieldLabel(item, fields),
value,
editable: item.editable !== false,
highlight: Boolean(item.highlight),
missing: item.required !== false && !isApplicationPreviewValueProvided(rawValue)
}]
})
}
export function buildApplicationPreviewSubmitText(preview = {}) {
const rows = buildApplicationPreviewRows(preview)
return [
'费用申请确认提交',
...rows.map((row) => `${row.label}${row.value}`),
'',
'确认提交'
].join('\n')
}
export function buildLocalApplicationPreview(rawText, currentUser = {}, options = {}) {
const sourceText = String(rawText || '').trim()
const explicitDays = resolveApplicationDays(sourceText)
const time = resolveApplicationTimeWithDefault(sourceText, explicitDays, options)
const days = explicitDays || resolveDaysFromDateRange(time)
const location = resolveApplicationLocation(sourceText)
const fields = {
applicationType: resolveApplicationType(sourceText),
time,
location,
reason: resolveApplicationReason(sourceText, { location }),
days,
transportMode: resolveApplicationTransportMode(sourceText),
amount: resolveApplicationAmount(sourceText),
grade: resolveCurrentUserGrade(currentUser),
applicant: currentUser.name || currentUser.username || '当前用户',
department: resolveCurrentUserDepartment(currentUser) || '待补充',
position: resolveCurrentUserPosition(currentUser) || '待补充',
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
}
return normalizeApplicationPreview({
sourceText,
fields,
modelReviewStatus: 'local'
})
}
export function buildApplicationTemplatePreview(currentUser = {}) {
return normalizeApplicationPreview({
sourceText: '快速发起申请',
fields: {
applicationType: '费用申请',
time: '',
location: '',
reason: '',
days: '',
transportMode: '',
amount: '',
grade: resolveCurrentUserGrade(currentUser),
applicant: currentUser.name || currentUser.username || '当前用户',
department: resolveCurrentUserDepartment(currentUser) || '待补充',
position: resolveCurrentUserPosition(currentUser) || '待补充',
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
},
modelReviewStatus: 'template'
})
}
export function buildLocalApplicationPreviewMessage(preview) {
const normalized = normalizeApplicationPreview(preview)
const modelReviewStatus = String(normalized.modelReviewStatus || '').trim()
return [
modelReviewStatus === 'completed'
? '我已完成模型复核,并整理成下方表格。请核查识别结果;点击对应行即可直接编辑。'
: modelReviewStatus === 'fallback'
? '模型复核没有返回稳定结果,我已先按规则兜底整理成下方表格。请重点核查识别结果;点击对应行即可直接编辑。'
: modelReviewStatus === 'failed'
? '模型复核暂时失败,我先保留一份临时核对表,方便您核查和补充信息。点击对应行即可直接编辑。'
: modelReviewStatus === 'template'
? '我已为你准备好费用申请模板。本步骤不调用大模型,也不会保存草稿;请点击对应行直接填写。'
: '我先整理出下方表格,请核查识别结果。点击对应行即可直接编辑。'
].join('\n')
}
export function buildApplicationPreviewFooterMessage(preview) {
const normalized = normalizeApplicationPreview(preview)
const missingFields = Array.isArray(normalized.missingFields) ? normalized.missingFields : []
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 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
const validationIssues = Array.isArray(normalized.validationIssues) ? normalized.validationIssues : []
if (validationIssues.length) {
return `${validationIssues[0].message} 请先修正后再提交申请。`
}
if (missingFields.length) {
return `当前还需要补充:${missingFields.join('、')}。补齐后我再帮您提交申请。`
}
return '请确认上述的信息是否填写正确?如果准确无误,点击 [确认](#application-submit) 进入审批环节。'
}