import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js' import { evaluateLocalApplicationIntentGate } from './expenseApplicationIntentGate.js' import { formatApplicationEstimateMoney, parseApplicationEstimateMoney, buildSystemApplicationEstimate } from './expenseApplicationEstimate.js' import { APPLICATION_POLICY_PENDING_TEXT, APPLICATION_PREVIEW_FIELD_DEFINITIONS, APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT, buildMissingFields, buildTransportEstimateFromPolicyResult, buildTransportPolicyText, ensureApplicationPolicyFields, formatDailyPolicyMoney, formatPolicyMoney, isApplicationPreviewValueProvided, isTravelApplicationType, normalizeAmountFromOntology, normalizeApplicationTypeLabel, normalizeTypedOntologyAmount, parseApplicationDaysValue, parseMoneyNumber, resolveApplicationAmount, resolveApplicationDays, resolveApplicationFieldLabel, resolveApplicationLocation, resolveApplicationReason, resolveApplicationSourceValidationIssues, resolveApplicationTimeWithDefault, resolveApplicationTransportMode, resolveApplicationTripDateParts, resolveApplicationType, resolveApplicationValidationIssues, resolveCurrentUserDepartment, resolveCurrentUserGrade, resolveCurrentUserManagerName, resolveCurrentUserPosition, resolveDaysFromDateRange, resolveModelRefinedTransportMode, resolveProvidedValue } from './expenseApplicationPreviewParsing.js' export { APPLICATION_PREVIEW_FIELD_DEFINITIONS, APPLICATION_TRANSPORT_MODE_OPTIONS, applicationDateRangesOverlap, normalizeTransportModeOption, resolveApplicationDateRange, resolveApplicationDaysFromDateRange, resolveApplicationTimeLabel, shouldRequireApplicationModelReview } from './expenseApplicationPreviewParsing.js' 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, 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 = {}) { const fields = { ...(preview?.fields || {}) } 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() 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) : '' 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, 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}元(不含交通)` : '' }, 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 }) 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 = {}) { return evaluateLocalApplicationIntentGate(rawText, options).allowed } export function normalizeApplicationPreview(preview = {}) { const fields = ensureApplicationPolicyFields(preview?.fields || {}) const missingFields = buildMissingFields(fields) const validationIssues = [ ...resolveApplicationValidationIssues(fields), ...resolveApplicationSourceValidationIssues(preview?.sourceText, fields, preview) ] return { ...preview, fields, missingFields, 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), 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 : [] const validationIssues = Array.isArray(normalized.validationIssues) ? normalized.validationIssues : [] if (validationIssues.length) { return `${validationIssues[0].message} 请先修正后再提交申请。` } if (missingFields.length) { return `当前还需要补充:${missingFields.join('、')}。补齐后我再帮您提交申请。` } return '请确认上述的信息是否填写正确?如果准确无误,点击 [确认](#application-submit) 进入审批环节。' }