Files
X-Financial/web/src/utils/expenseApplicationPreview.js
caoxiaozhu c4b5fcc067 feat(web): AI 工作台多 task 串行推进与会话适配
- useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiActionRouter/useWorkbenchAiCommandIntents 支持 task1 完成后自动推进 task2,确认按钮直接拉起申请预览,草稿/提交成功后继续推进下一 task
- workbenchAiIntentPlannerModel/workbenchAiMessageModel/workbenchAiCommandIntentModel 适配多 task 意图规划与消息结构
- aiApplicationPreviewActions/aiApplicationPrecheckModel/aiExpenseDraftModel/aiWorkbenchConversationStore 草稿与会话存储适配
- PersonalWorkbenchAiMode 与样式适配,更新 preview-actions/expense-draft/conversation-store/fast-preview/action-router/command-intent/intent-planner 测试
2026-06-26 22:42:23 +08:00

560 lines
23 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 {
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,
normalizeApplicationLocationBoundary,
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) || parseApplicationDaysValue(resolveDaysFromDateRange(fields.time))
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)
const blockingLocationIssue = (normalized.validationIssues || []).find((issue) => issue?.field === 'location')
if (blockingLocationIssue) {
return {
canCalculate: false,
reason: blockingLocationIssue.message || '地点需修正',
payload: null
}
}
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,
days: parseApplicationDaysValue(fields.days) ? fields.days : `${days}`,
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,
days: parseApplicationDaysValue(fields.days) ? fields.days : `${days}`,
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)
]
const editableFields = Array.isArray(preview?.editableFields)
? preview.editableFields
: Array.isArray(preview?.editable_fields)
? preview.editable_fields
: null
return {
...preview,
...(editableFields
? {
editableFields: editableFields
.map((field) => String(field || '').trim())
.filter(Boolean)
}
: {}),
fields,
missingFields,
validationIssues,
readyToSubmit: missingFields.length === 0 && validationIssues.length === 0
}
}
function resolveApplicationPreviewEditableFields(preview = {}) {
const source = Array.isArray(preview?.editableFields)
? preview.editableFields
: Array.isArray(preview?.editable_fields)
? preview.editable_fields
: null
if (!Array.isArray(source)) {
return null
}
const fields = new Set(
source
.map((field) => String(field || '').trim())
.filter(Boolean)
)
if (fields.has('time')) {
fields.add('time_return')
}
return fields
}
function isApplicationPreviewFieldEditable(preview = {}, item = {}, rowKey = '') {
if (item.editable === false) {
return false
}
const editableFields = resolveApplicationPreviewEditableFields(preview)
if (!editableFields) {
return true
}
return editableFields.has(rowKey)
}
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: normalizeApplicationLocationBoundary(
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: isApplicationPreviewFieldEditable(normalized, item, 'time'),
highlight: Boolean(item.highlight),
missing
},
{
key: 'time_return',
label: '返回时间',
value: tripDates.endDate || '待补充',
editable: isApplicationPreviewFieldEditable(normalized, item, 'time_return'),
highlight: Boolean(item.highlight),
missing
}
]
}
const rawValue = fields[item.key]
const value = String(rawValue || '').trim() || '待补充'
return [{
...item,
label: resolveApplicationFieldLabel(item, fields),
value,
editable: isApplicationPreviewFieldEditable(normalized, item, item.key),
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()
const editMode = Boolean(normalized.applicationEditMode || normalized.application_edit_mode)
if (editMode) {
return '我已载入原申请信息。请只修改事由、时间、地点和出行方式;职级、负责人、标准和费用会按规则带入或重新测算。'
}
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) 进入审批环节。'
}