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

482 lines
19 KiB
JavaScript
Raw Normal View History

const EXPENSE_TYPE_LABELS = {
travel: '差旅费',
hotel: '住宿费',
transport: '交通费',
meal: '业务招待费',
entertainment: '业务招待费',
meeting: '会务费',
marketing: '市场推广费',
office: '办公用品费',
training: '培训费',
software: '软件服务费',
communication: '通讯费',
welfare: '福利费',
other: '其他费用'
}
const SLOT_LABELS = {
expense_type: '费用场景',
amount: '申请金额',
time_range: '业务时间',
location: '业务地点',
reason: '申请事由',
days: '天数',
transport_mode: '出行方式',
attachments: '附件说明',
customer_name: '客户名称',
participants: '参与人员'
}
const PRE_APPROVAL_TYPES = new Set(['travel', 'meeting', 'office', 'training'])
const ATTACHMENT_REQUIRED_TYPES = new Set(['meeting', 'training'])
const PLACEHOLDER_VALUES = new Set(['', '待补充', '暂无', '无', '未知'])
const PROMPT_FIELD_LABELS = [
'发生时间',
'业务发生时间',
'申请时间',
'时间',
'地点',
'业务地点',
'发生地点',
'事由',
'申请事由',
'出差事由',
'原因',
'用途',
'天数',
'出差天数',
'申请天数',
'出行方式',
'交通方式',
'交通工具',
'用户预估费用',
'预估费用',
'预计总费用',
'预计费用',
'预计金额',
'申请金额',
'预算',
'金额'
]
export const APPLICATION_EXAMPLES = [
'申请下周去北京做客户现场验收预计费用18000元',
'申请上海产品发布会会务费32000元需要场地和物料',
'申请部门集中采购办公用品4800元用于新员工入职'
]
export function buildExpenseApplicationOntologyContext(currentUser = {}) {
return {
document_type: 'expense_application',
application_stage: 'pre_approval',
conversation_scenario: 'expense',
entry_source: 'documents_application',
role_codes: Array.isArray(currentUser.roleCodes) ? currentUser.roleCodes : [],
is_admin: Boolean(currentUser.isAdmin),
name: currentUser.name || '',
role: currentUser.role || '',
department: currentUser.department || currentUser.departmentName || '',
department_name: currentUser.department || currentUser.departmentName || '',
position: currentUser.position || '',
grade: currentUser.grade || '',
manager_name: currentUser.managerName || currentUser.manager_name || '',
employee_no: currentUser.employeeNo || currentUser.employee_no || ''
}
}
export function resolveEntity(ontology, type) {
const entities = Array.isArray(ontology?.entities) ? ontology.entities : []
return entities.find((item) => item?.type === type) || null
}
export function resolveConstraint(ontology, field) {
const constraints = Array.isArray(ontology?.constraints) ? ontology.constraints : []
return constraints.find((item) => item?.field === field) || null
}
export function resolveExpenseTypeCode(ontology) {
const entity = resolveEntity(ontology, 'expense_type')
return String(entity?.normalized_value || entity?.value || 'other').trim() || 'other'
}
function looksLikeStructuredTravelApplication(prompt) {
const text = String(prompt || '')
return /(?:发生时间|业务发生时间|申请时间|时间)\s*[:]/.test(text)
&& /(?:地点|业务地点|发生地点|目的地)\s*[:]/.test(text)
&& /(?:天数|出差天数|申请天数)\s*[:]?\s*(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/.test(text)
}
function resolveApplicationExpenseTypeCode(ontology, prompt) {
const code = resolveExpenseTypeCode(ontology)
if (code !== 'other') return code
return looksLikeStructuredTravelApplication(prompt) ? 'travel' : code
}
export function resolveExpenseTypeLabel(code) {
return EXPENSE_TYPE_LABELS[String(code || '').trim()] || EXPENSE_TYPE_LABELS.other
}
export function resolveApplicationAmount(ontology) {
const amountEntity = resolveEntity(ontology, 'amount')
const amountConstraint = resolveConstraint(ontology, 'amount')
const rawValue = amountEntity?.normalized_value || amountEntity?.value || amountConstraint?.value || ''
const numericValue = Number(String(rawValue).replace(/[^\d.]/g, ''))
return {
raw: String(rawValue || '').trim(),
value: Number.isFinite(numericValue) ? numericValue : 0
}
}
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 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) {
return range.start_date === range.end_date
? range.start_date
: `${range.start_date}${range.end_date}`
}
return String(range.raw || '').trim()
}
function parseApplicationDate(value) {
const normalized = String(value || '')
.trim()
.replace(/日$/, '')
.replace(/年|月|\//g, '-')
.replace(/\./g, '-')
const match = normalized.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)))
if (Number.isNaN(date.getTime())) return null
return date
}
function formatApplicationDate(date) {
return date.toISOString().slice(0, 10)
}
function parseChineseNumber(value) {
const digits = {
: 1,
: 2,
: 2,
: 3,
: 4,
: 5,
: 6,
: 7,
: 8,
: 9
}
const text = String(value || '').trim()
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
}
export function resolvePromptDays(prompt) {
const labeled = resolvePromptField(prompt, ['天数', '出差天数', '申请天数'])
const source = labeled || String(prompt || '')
const match = source.match(/(?<days>\d+|[一二两三四五六七八九十]{1,3})\s*天/)
if (!match?.groups?.days) return 0
if (/^\d+$/.test(match.groups.days)) return Number(match.groups.days)
return parseChineseNumber(match.groups.days)
}
export function expandApplicationTimeWithDays(timeText, days = 0) {
const normalizedTime = String(timeText || '').trim()
const dayCount = Number(days || 0)
if (!normalizedTime || !dayCount) return normalizedTime
if (/\s*(至|到|~|--|—)\s*/.test(normalizedTime)) return normalizedTime
const match = normalizedTime.match(/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/)
const startDate = parseApplicationDate(match?.[0] || '')
if (!startDate) return normalizedTime
const endDate = new Date(startDate.getTime())
endDate.setUTCDate(endDate.getUTCDate() + Math.max(dayCount - 1, 0))
const startText = formatApplicationDate(startDate)
const endText = formatApplicationDate(endDate)
return startText === endText ? startText : `${startText}${endText}`
}
function normalizeApplicationTimeCandidate(value) {
const text = String(value || '').trim().replace(/^[,、。;;\s]+/, '')
if (!text) return ''
if (/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/.test(text)) return text
if (/今天|明天|后天|本周|下周|上周|本月|下月|月底|月初/.test(text)) return text
return ''
}
export function resolveApplicationTimeRange(ontology, prompt) {
const range = ontology?.time_range || {}
const baseTime = normalizeApplicationTimeCandidate(resolveTimeRangeText(ontology))
|| normalizeApplicationTimeCandidate(resolvePromptField(prompt, ['发生时间', '业务发生时间', '申请时间', '时间']))
if (range.start_date && range.end_date && range.start_date !== range.end_date) {
return `${range.start_date}${range.end_date}`
}
return expandApplicationTimeWithDays(baseTime, resolvePromptDays(prompt)) || baseTime
}
function escapeRegExp(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
export function resolvePromptField(prompt, labels = []) {
const text = String(prompt || '').trim()
if (!text) return ''
const labelSet = new Set(labels.map((item) => String(item || '').trim()).filter(Boolean))
for (const line of text.split(/\r?\n/)) {
const match = line.match(/^\s*([^:\s]+)\s*[:]\s*(.+?)\s*$/)
if (match && labelSet.has(match[1].trim())) {
return match[2].trim()
}
}
const labelPattern = labels.map(escapeRegExp).join('|')
const nextLabelPattern = PROMPT_FIELD_LABELS.map(escapeRegExp).join('|')
if (!labelPattern) return ''
const match = text.match(
new RegExp(`(?:${labelPattern})\\s*[:]\\s*([\\s\\S]*?)(?=\\s*(?:${nextLabelPattern})\\s*[:]|$)`)
)
return match ? match[1].trim().replace(/[,。;;]$/, '') : ''
}
function normalizeApplicationTransportMode(value) {
const text = String(value || '').trim()
if (/飞机|机票|航班/.test(text)) return '飞机'
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
return text
}
function cleanupApplicationReasonCandidate(value, location = '') {
let text = String(value || '').trim()
if (!text) return ''
text = 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
.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, '')
.replace(/(?:高铁|动车|火车|铁路|列车|飞机|机票|航班|轮船|船票|客轮|渡轮|邮轮)/gu, '')
.replace(/[,、。;;]+/g, '')
.replace(/^\s*(申请|费用申请|业务|本次|去|到|前往|赴)\s*/u, '')
.replace(/^[\s]+|[\s]+$/g, '')
.trim()
const normalizedLocation = String(location || '').trim()
if (normalizedLocation) {
const escapedLocation = escapeRegExp(normalizedLocation)
text = text
.replace(new RegExp(`^${escapedLocation}(?:出差)?(?:|,|、)?`, 'u'), '')
.replace(new RegExp(`^(?:去|到|前往|赴)${escapedLocation}(?:出差)?(?:|,|、)?`, 'u'), '')
.trim()
}
if (!text) 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
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)) {
return ''
}
return text
}
function resolveApplicationLocationText(ontology, prompt) {
const locationEntity = resolveEntity(ontology, 'location')
return locationEntity?.normalized_value
|| locationEntity?.value
|| resolvePromptField(prompt, ['地点', '业务地点', '发生地点', '目的地'])
|| ''
}
export function resolveApplicationReason(prompt, ontology = null) {
const location = resolveApplicationLocationText(ontology, prompt)
const reasonEntity = resolveEntity(ontology, 'reason') || resolveEntity(ontology, 'business_reason')
const entityReason = String(reasonEntity?.normalized_value || reasonEntity?.value || '').trim()
if (entityReason) {
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 cleanedEntityReason = cleanupApplicationReasonCandidate(entityReason, location)
if (cleanedEntityReason && !isInvalidApplicationReason(cleanedEntityReason)) {
return cleanedEntityReason
}
}
const labeled = resolvePromptField(prompt, ['事由', '申请事由', '出差事由', '原因', '用途'])
if (labeled) {
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 cleanedLabeledReason = cleanupApplicationReasonCandidate(labeled, location)
if (cleanedLabeledReason && !isInvalidApplicationReason(cleanedLabeledReason)) {
return cleanedLabeledReason
}
}
const candidates = String(prompt || '')
.split(/[\n;]+/u)
.map((item) => cleanupApplicationReasonCandidate(item, 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
.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] || ''
}
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 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')
const fromEntity = normalizeApplicationTransportMode(
transportEntity?.normalized_value || transportEntity?.value || ''
)
if (fromEntity) return fromEntity
const labeled = resolvePromptField(prompt, ['出行方式', '交通方式', '交通工具'])
const fromLabel = normalizeApplicationTransportMode(labeled)
if (fromLabel) return fromLabel
const text = String(prompt || '')
if (/飞机|机票|航班/.test(text)) return '飞机'
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
return ''
}
export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
const code = String(expenseTypeCode || '').trim()
if (ATTACHMENT_REQUIRED_TYPES.has(code)) {
return {
level: 'required',
label: '必须提交',
description: code === 'meeting'
? '需补充会议通知、议程、参会范围或预算说明。'
: '需补充培训通知、课程说明、报价或审批依据。'
}
}
if (code === 'office' && amount >= 5000) {
return {
level: 'required',
label: '必须提交',
description: '办公采购金额较高,需补充采购清单、报价或预算说明。'
}
}
if (code === 'travel') {
return {
level: 'optional',
label: '说明可选',
description: '可先提交出差目的、时间和预算;行程或邀请材料可作为补充说明。'
}
}
return {
level: 'none',
label: '无需附件',
description: '当前申请事项可先不提交附件,后续报销阶段再按票据要求补充。'
}
}
export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser = {}) {
const expenseTypeCode = resolveApplicationExpenseTypeCode(ontology, prompt)
const amount = resolveApplicationAmount(ontology)
const documentTypeEntity = resolveEntity(ontology, 'document_type')
const workflowStageEntity = resolveEntity(ontology, 'workflow_stage')
const attachmentPolicy = resolveAttachmentPolicy(expenseTypeCode, amount.value)
const timeRange = resolveApplicationTimeRange(ontology, prompt)
|| '待补充'
const location = resolveApplicationLocationText(ontology, prompt)
|| '待补充'
const reason = resolveApplicationReason(prompt, ontology) || '待补充'
const days = resolvePromptDays(prompt)
const transportMode = resolveApplicationTransportMode(ontology, prompt)
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 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',
documentTypeLabel: documentTypeEntity?.value || '费用申请',
workflowStage: workflowStageEntity?.normalized_value || 'pre_approval',
workflowStageLabel: workflowStageEntity?.value || '前置申请',
expenseTypeCode,
expenseTypeLabel: resolveExpenseTypeLabel(expenseTypeCode),
amount: amount.value,
amountDisplay: amount.value ? `¥${amount.value.toLocaleString('zh-CN')}` : '待补充',
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,
trainEstimatedAmount,
flightEstimatedAmount,
hotelAmount,
allowanceAmount,
policyTotalAmount,
reimbursementAmount,
timeRange,
location,
reason,
days: days ? `${days}` : '',
transportMode,
applicant: currentUser.name || currentUser.username || '当前用户',
department: currentUser.department || currentUser.departmentName || '待补充',
position: currentUser.position || currentUser.employeePosition || '待补充',
managerName: currentUser.managerName || currentUser.manager_name || '待补充',
preApprovalRequired: PRE_APPROVAL_TYPES.has(expenseTypeCode),
attachmentPolicy
}
return {
...fields,
missingSlots: normalizeMissingSlots(ontology?.missing_slots || [], fields)
}
}
function hasProvidedValue(value) {
const normalized = String(value || '').trim()
return !PLACEHOLDER_VALUES.has(normalized)
}
function isSlotAlreadyResolved(slot, fields = {}) {
const key = String(slot || '').trim()
if (key === 'reason') return hasProvidedValue(fields.reason)
if (key === 'time_range' || key === 'time') return hasProvidedValue(fields.timeRange)
if (key === 'location') return hasProvidedValue(fields.location)
if (key === 'amount') return Number(fields.amount || 0) > 0
if (key === 'transport_mode') return hasProvidedValue(fields.transportMode)
return false
}
export function normalizeMissingSlots(slots = [], fields = {}) {
const normalized = Array.isArray(slots) ? slots : []
return normalized.map((item) => ({
key: String(item || '').trim(),
label: SLOT_LABELS[String(item || '').trim()] || String(item || '').trim()
})).filter((item) => item.key && !isSlotAlreadyResolved(item.key, fields))
}