refactor(travel): split reimbursement create workflow

完整修改内容:

- 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。
- 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。
- 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。
- 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。
- 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。

验证:

- node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块
- npm --prefix web run build
- node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs

说明:

- 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。
- 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。
- TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
This commit is contained in:
Codex
2026-06-13 14:52:26 +00:00
parent 336fee9d93
commit 8b952c9a26
28 changed files with 4510 additions and 2730 deletions

View File

@@ -1,4 +1,7 @@
import ontologyBusinessContract from '../../../shared/ontology_business_contract.json' with { type: 'json' }
export const ASSISTANT_SCOPE_ACTION_SWITCH = 'switch_assistant_session'
export const ASSISTANT_SCOPE_ACTION_UNSUPPORTED = 'unsupported_business_intent'
export const ASSISTANT_SCOPE_SESSION_APPLICATION = 'application'
export const ASSISTANT_SCOPE_SESSION_EXPENSE = 'expense'
@@ -6,7 +9,7 @@ export const ASSISTANT_SCOPE_SESSION_APPROVAL = 'approval'
export const ASSISTANT_SCOPE_SESSION_KNOWLEDGE = 'knowledge'
export const ASSISTANT_SCOPE_SESSION_STEWARD = 'steward'
const SESSION_SCOPE_CONFIG = {
const FALLBACK_SESSION_SCOPE_CONFIG = {
[ASSISTANT_SCOPE_SESSION_STEWARD]: {
label: '小财管家',
icon: 'mdi mdi-account-tie-outline',
@@ -34,7 +37,10 @@ const SESSION_SCOPE_CONFIG = {
}
}
const ONTOLOGY_BUSINESS_CONTRACT = ontologyBusinessContract || {}
const SESSION_SCOPE_CONFIG = ONTOLOGY_BUSINESS_CONTRACT.sessions || FALLBACK_SESSION_SCOPE_CONFIG
const SESSION_SCOPE_TYPES = Object.keys(SESSION_SCOPE_CONFIG)
const BUSINESS_SIGNAL_GROUPS = ONTOLOGY_BUSINESS_CONTRACT.businessSignals || {}
const APPLICATION_PATTERN =
/费用申请|发起申请|申请单|事前申请|事前审批|前置审批|出差申请|申请出差|差旅申请|申请差旅|采购申请|用款申请|预算申请|申请材料|材料清单|先申请|立项申请/
@@ -56,6 +62,45 @@ const KNOWLEDGE_PATTERN =
/制度|政策|标准|规则|规定|流程|口径|依据|上限|额度|补贴|住宿标准|差旅标准|报销标准|票据要求|可不可以|能不能|怎么规定|如何计算|怎么算/
const EXPENSE_OPERATION_PATTERN = /发起报销|报销单|票据|发票|火车票|高铁票|机票|的士票|草稿|归集|上传|关联单据|继续下一步/
const CURRENT_CLAIM_RISK_PATTERN = /这张|当前|本单|该单|单据|风险|超标|异常|重复|待补/
const FINANCE_OPERATING_PATTERN = buildKeywordPattern([
...(BUSINESS_SIGNAL_GROUPS.budget || []),
...(BUSINESS_SIGNAL_GROUPS.accounts_receivable || []),
...(BUSINESS_SIGNAL_GROUPS.accounts_payable || [])
])
const CONTEXTUAL_FOLLOW_UP_PATTERN = buildExactKeywordPattern(ONTOLOGY_BUSINESS_CONTRACT.contextualFollowUps || [])
export const SUPPORTED_BUSINESS_SCOPE_TEXT = Array.isArray(ONTOLOGY_BUSINESS_CONTRACT.supportedBusinessScopes)
? ONTOLOGY_BUSINESS_CONTRACT.supportedBusinessScopes
: [
'费用申请/事前审批',
'报销与票据识别',
'审批审核与风险解释',
'财务制度、报销标准和流程规则问答',
'预算、应收、应付等财务经营查询',
'小财管家多任务拆解和附件归集'
]
function escapeRegExp(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function buildKeywordPattern(keywords = []) {
const source = keywords
.map((keyword) => String(keyword || '').trim())
.filter(Boolean)
.map(escapeRegExp)
.join('|')
return source ? new RegExp(source) : /$a/
}
function buildExactKeywordPattern(keywords = []) {
const source = keywords
.map((keyword) => String(keyword || '').trim())
.filter(Boolean)
.map(escapeRegExp)
.join('|')
return source ? new RegExp(`^(${source})$`) : /$a/
}
function normalizeSessionType(sessionType) {
const normalized = String(sessionType || '').trim()
@@ -131,6 +176,10 @@ export function inferAssistantScopeTarget(rawText, options = {}) {
return ASSISTANT_SCOPE_SESSION_APPLICATION
}
if (FINANCE_OPERATING_PATTERN.test(text)) {
return ASSISTANT_SCOPE_SESSION_STEWARD
}
if (knowledgeMatched && !expenseMatched && !approvalMatched && !applicationMatched) {
return ASSISTANT_SCOPE_SESSION_KNOWLEDGE
}
@@ -198,10 +247,58 @@ function buildScopeBoundaryText(currentSessionType, targetSessionType) {
].join('\n')
}
function shouldAllowContextualFollowUp(rawText, currentSessionType, options = {}) {
const text = normalizeText(rawText)
if (options.hasActiveReviewPayload && CURRENT_CLAIM_RISK_PATTERN.test(text)) {
return true
}
if (!text || !CONTEXTUAL_FOLLOW_UP_PATTERN.test(text)) {
return false
}
return Boolean(
options.hasActiveReviewPayload ||
options.hasPendingApplicationPreview ||
options.reviewAction ||
currentSessionType
)
}
function buildUnsupportedBusinessScopeText() {
const message = ONTOLOGY_BUSINESS_CONTRACT.unsupportedIntentMessage || {}
return [
message.title || '此意图系统不支持。',
'',
`当前系统支持的业务范围:${SUPPORTED_BUSINESS_SCOPE_TEXT.join('、')}`,
'',
message.body || '你这条内容没有识别到相关财务业务意图,系统暂不支持处理。',
'',
message.retryHint || '请重新描述你的财务业务要求,例如“申请下周去上海出差”“查询我的报销单进度”或“解释差旅住宿标准”。'
].join('\n')
}
function buildUnsupportedBusinessScopeGuard() {
return {
targetSessionType: '',
targetLabel: '不支持的意图',
blocked: true,
text: buildUnsupportedBusinessScopeText(),
meta: ['意图不支持'],
suggestedActions: [],
actionType: ASSISTANT_SCOPE_ACTION_UNSUPPORTED
}
}
export function resolveAssistantScopeGuard(rawText, currentSessionType, options = {}) {
const normalizedCurrent = normalizeSessionType(currentSessionType)
const targetSessionType = inferAssistantScopeTarget(rawText, options)
if (!targetSessionType || targetSessionType === normalizedCurrent) {
if (!targetSessionType) {
if (shouldAllowContextualFollowUp(rawText, normalizedCurrent, options)) {
return null
}
return normalizeText(rawText) ? buildUnsupportedBusinessScopeGuard() : null
}
if (targetSessionType === normalizedCurrent) {
return null
}