feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

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

View File

@@ -1,4 +1,8 @@
import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
import {
buildMockApplicationTransportEstimate,
buildSystemApplicationEstimate
} from './expenseApplicationEstimate.js'
import { getTodayDateValue } from './workbenchComposerDate.js'
const APPLICATION_SESSION_TYPE = 'application'
@@ -7,7 +11,11 @@ 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: '事由' },
@@ -17,13 +25,13 @@ export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
{ 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 }
{ 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 = '车票、机票暂无实时价格接口,按真实票据实报实销'
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '选择火车、飞机或轮船后自动生成交通参考票价,报销阶段按真实票据复核'
function compactText(value) {
return String(value || '').replace(/\s+/g, '')
@@ -114,6 +122,38 @@ function resolveCurrentUserGrade(currentUser = {}) {
).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)
@@ -165,10 +205,12 @@ function formatDailyPolicyMoney(value) {
return display ? `${display}元/天` : APPLICATION_POLICY_PENDING_TEXT
}
function buildTransportPolicyText(transportMode) {
function buildTransportPolicyText(transportMode, location = '', transportEstimate = null, time = '') {
const mode = String(transportMode || '').trim()
if (!mode) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
return `${mode}票据暂无实时价格接口,按真实票据实报实销`
const estimate = transportEstimate || buildMockApplicationTransportEstimate({ transportMode: mode, location, time })
if (!estimate) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
return `${estimate.basisText},报销阶段按真实票据复核`
}
function ensureApplicationPolicyFields(fields = {}) {
@@ -180,7 +222,7 @@ function ensureApplicationPolicyFields(fields = {}) {
nextFields.subsidyDailyCap = APPLICATION_POLICY_PENDING_TEXT
}
if (!String(nextFields.transportPolicy || '').trim() || /实报实销/.test(String(nextFields.transportPolicy || ''))) {
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode)
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode, nextFields.location, null, nextFields.time)
}
if (!String(nextFields.policyEstimate || '').trim()) {
nextFields.policyEstimate = APPLICATION_POLICY_PENDING_TEXT
@@ -371,9 +413,22 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
const hotelAmount = formatPolicyMoney(result?.hotel_amount)
const allowanceRate = formatPolicyMoney(result?.total_allowance_rate)
const allowanceAmount = formatPolicyMoney(result?.allowance_amount)
const totalAmount = formatPolicyMoney(result?.total_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,
@@ -382,24 +437,85 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
grade,
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
transportPolicy: buildTransportPolicyText(fields.transportMode),
policyEstimate: `住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days},不含交通票据`,
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
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()
@@ -408,7 +524,7 @@ export function applyApplicationPolicyEstimateError(preview = {}, error = null,
fields: {
...fields,
grade: fields.grade || resolveCurrentUserGrade(currentUser),
transportPolicy: buildTransportPolicyText(fields.transportMode),
transportPolicy: buildTransportPolicyText(fields.transportMode, fields.location, null, fields.time),
policyEstimate: message ? `规则中心暂未完成测算:${message}` : APPLICATION_POLICY_PENDING_TEXT
},
policyEstimateStatus: message ? 'failed' : 'pending'
@@ -456,7 +572,12 @@ export function buildModelRefinedApplicationPreview(localPreview = {}, ontology
amount: normalizeAmountFromOntology(ontologyFields, currentFields.amount),
grade: resolveProvidedValue(currentFields.grade, resolveCurrentUserGrade(currentUser)),
applicant: resolveProvidedValue(ontologyFields.applicant, currentFields.applicant),
department: resolveProvidedValue(ontologyFields.department, currentFields.department)
department: resolveProvidedValue(ontologyFields.department, currentFields.department || resolveCurrentUserDepartment(currentUser)),
position: resolveProvidedValue(currentFields.position, resolveCurrentUserPosition(currentUser)),
managerName: resolveProvidedValue(
ontologyFields.managerName,
currentFields.managerName || resolveCurrentUserManagerName(currentUser)
)
}
return normalizeApplicationPreview({
@@ -511,7 +632,9 @@ export function buildLocalApplicationPreview(rawText, currentUser = {}, options
amount: resolveApplicationAmount(sourceText),
grade: resolveCurrentUserGrade(currentUser),
applicant: currentUser.name || currentUser.username || '当前用户',
department: currentUser.department || currentUser.departmentName || '待补充'
department: resolveCurrentUserDepartment(currentUser) || '待补充',
position: resolveCurrentUserPosition(currentUser) || '待补充',
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
}
return normalizeApplicationPreview({
@@ -534,7 +657,9 @@ export function buildApplicationTemplatePreview(currentUser = {}) {
amount: '',
grade: resolveCurrentUserGrade(currentUser),
applicant: currentUser.name || currentUser.username || '当前用户',
department: currentUser.department || currentUser.departmentName || '待补充'
department: resolveCurrentUserDepartment(currentUser) || '待补充',
position: resolveCurrentUserPosition(currentUser) || '待补充',
managerName: resolveCurrentUserManagerName(currentUser) || '待补充'
},
modelReviewStatus: 'template'
})