Files
X-Financial/web/src/utils/expenseApplicationPreview.js
caoxiaozhu 304bbe1fd4 feat(web): 工作台 AI 模式报销预审与文档查询模型拆分
- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出
- PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示
- 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试
- 新增 AI 文档卡片背景资源
2026-06-20 10:17:37 +08:00

1193 lines
50 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 { evaluateLocalApplicationIntentGate } from './expenseApplicationIntentGate.js'
import {
buildMockApplicationTransportEstimate,
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)
}
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)
}
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, [
/(?:用户预估费用|预估费用|申请金额|预计金额|预计费用|预计总费用|预算|金额|费用)\s*[:]?\s*(?<value>\d+(?:\.\d+)?\s*(?:万|千|k|K)?)/u,
/(?<value>\d+(?:\.\d+)?\s*(?:(?:万|千|k|K)\s*(?:元|块|人民币)?|(?:元|块|人民币)))/u
])
const normalized = normalizeApplicationAmountText(labeled)
if (normalized) return normalized
if (/不知道预算|预算不清楚|预算待定|待测算/.test(compact)) return '待测算'
return ''
}
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
}
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])}`
}
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 = {}) {
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
])
}
function looksLikeTransportPromptText(text) {
const compact = compactText(text)
return /(?:还需要补充|当前还缺|缺少|请先补充|请选择|可以选择|可以选|打算怎么出行|怎么出行).{0,24}(?:出行方式|交通方式|交通工具)/u.test(compact)
|| /(?:出行方式|交通方式|交通工具).{0,24}(?:待补充|缺失|请选择|可以选择|可以选)/u.test(compact)
}
function resolveApplicationTransportMode(text) {
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 || '')
.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())
.filter((item) => item && !isSystemGeneratedReasonText(item))
return segments.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item)) || ''
}
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)
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 = '') {
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
}
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,
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) 进入审批环节。'
}