Files
X-Financial/web/src/utils/expenseApplicationPreview.js
caoxiaozhu 92444e7eae feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
2026-06-01 17:07:14 +08:00

693 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
import {
buildMockApplicationTransportEstimate,
buildSystemApplicationEstimate
} from './expenseApplicationEstimate.js'
import { getTodayDateValue } from './workbenchComposerDate.js'
const APPLICATION_SESSION_TYPE = 'application'
const APPLICATION_CREATE_PATTERN = /申请|事前|前置|预算|出差|差旅|采购|会务|会议|培训|办公用品|交通|住宿/
const APPLICATION_QUERY_PATTERN = /查询|状态|进度|列表|有哪些|材料清单|需要哪些|制度|标准|规则|怎么规定/
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 = '选择火车、飞机或轮船后自动生成交通参考票价,报销阶段按真实票据复核'
function compactText(value) {
return String(value || '').replace(/\s+/g, '')
}
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)
}
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}` : ''
}
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 (/差旅|出差|高铁|动车|火车|飞机|机票|航班|酒店|住宿/.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, [
/(?:用户预估费用|预估费用|申请金额|预计金额|预计费用|预计总费用|预算|金额|费用)\s*[:]?\s*(?<value>\d+(?:\.\d+)?)\s*(?:元|块|人民币)?/u,
/(?<value>\d+(?:\.\d+)?)\s*(?:元|块|人民币)/u
])
if (labeled) return `${labeled}`
if (/不知道预算|预算不清楚|预算待定|待测算/.test(compact)) return '待测算'
return ''
}
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()
if (!mode) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
const estimate = transportEstimate || buildMockApplicationTransportEstimate({ transportMode: mode, location, time })
if (!estimate) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
return `${estimate.basisText},报销阶段按真实票据复核`
}
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])}`
}
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 = {}) {
const resolvedTime = resolveApplicationTime(text, daysText)
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
])
}
function resolveApplicationTransportMode(text) {
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 || '')
.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())
.filter(Boolean)
return segments.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item)) || ''
}
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)
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}申请`
}
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 normalizeAmountFromOntology(fields = {}, fallback = '') {
const numericAmount = Number(fields.amount || 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
}
function buildMissingFields(fields) {
return APPLICATION_PREVIEW_FIELD_DEFINITIONS
.filter((item) => item.key !== 'applicationType' && item.required !== false)
.filter((item) => !isApplicationPreviewValueProvided(fields?.[item.key]))
.map((item) => item.label)
}
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,
grade
}
}
}
export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, currentUser = {}) {
const fields = { ...(preview?.fields || {}) }
const days = Number(result?.days) || parseApplicationDaysValue(fields.days) || 1
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()
const systemEstimate = buildSystemApplicationEstimate({
transportMode: fields.transportMode,
location: matchedCity || fields.location,
time: fields.time,
lodgingAmount: result?.hotel_amount,
allowanceAmount: result?.allowance_amount
})
const transportEstimate = systemEstimate.transportEstimate
const queryLabel = transportEstimate?.queryDate || '出行日期待确认'
const transportText = transportEstimate
? `交通 ${systemEstimate.transportAmountDisplay}元(按 ${queryLabel} 参考票价) + `
: ''
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 queryLabel = transportEstimate.queryDate || '出行日期待确认'
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}元(按 ${queryLabel} 参考票价) + 住宿 ${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 = {}) {
if (String(options.sessionType || '').trim() !== APPLICATION_SESSION_TYPE) return false
if (options.systemGenerated || options.reviewAction || Number(options.attachmentCount || 0) > 0) return false
const compact = compactText(rawText)
if (!compact || APPLICATION_QUERY_PATTERN.test(compact)) return false
return APPLICATION_CREATE_PATTERN.test(compact)
}
export function normalizeApplicationPreview(preview = {}) {
const fields = ensureApplicationPolicyFields(preview?.fields || {})
const missingFields = buildMissingFields(fields)
return {
...preview,
fields,
missingFields,
readyToSubmit: missingFields.length === 0
}
}
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: normalizeTransportModeOption(
ontologyFields.transportMode,
currentFields.transportMode
),
amount: normalizeAmountFromOntology(ontologyFields, currentFields.amount),
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.map((item) => {
const rawValue = fields[item.key]
const value = String(rawValue || '').trim() || '待补充'
return {
...item,
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 : []
if (missingFields.length) {
return `当前还需要补充:${missingFields.join('、')}。补齐后我再帮您提交申请。`
}
return '请确认上述的信息是否填写正确?如果准确无误,点击 [确认](#application-submit) 进入审批环节。'
}