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

@@ -13,6 +13,7 @@ import {
buildModelRefinedApplicationPreview,
shouldUseLocalApplicationPreview
} from '../../utils/expenseApplicationPreview.js'
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
import { fetchOntologyParse } from '../../services/ontology.js'
import { buildExpenseApplicationOntologyContext } from '../../utils/expenseApplicationOntology.js'
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
@@ -79,6 +80,7 @@ export function useTravelReimbursementSubmitComposer(ctx) {
persistSessionState,
props,
recognizeOcrFiles,
refreshCurrentUserFromBackend,
refreshFlowRunDetail,
rememberFilePreviews,
replaceMessage,
@@ -339,8 +341,18 @@ export function useTravelReimbursementSubmitComposer(ctx) {
]
}
async function buildApplicationPreviewWithModelReview(rawText) {
async function resolveApplicationPreviewUser() {
const user = currentUser.value || {}
if (String(user.position || '').trim() || typeof refreshCurrentUserFromBackend !== 'function') {
return user
}
await refreshCurrentUserFromBackend({ silent: true })
return currentUser.value || user
}
async function buildApplicationPreviewWithModelReview(rawText) {
const user = await resolveApplicationPreviewUser()
const localPreview = buildLocalApplicationPreview(rawText, user)
const enrichWithPolicyEstimate = async (preview) => {
@@ -349,6 +361,12 @@ export function useTravelReimbursementSubmitComposer(ctx) {
return preview
}
try {
const fields = preview?.fields || {}
await waitForMockApplicationTransportQuote({
transportMode: fields.transportMode,
location: fields.location,
time: fields.time
})
const result = await calculateTravelReimbursement(estimateRequest.payload)
return applyApplicationPolicyEstimateResult(preview, result, user)
} catch (error) {
@@ -548,14 +566,14 @@ export function useTravelReimbursementSubmitComposer(ctx) {
startFlowStep('application-review-preview', {
title: '申请信息核对',
tool: 'ontology.application_review',
detail: '正在进行申请信息模型复核...'
detail: '正在复核申请信息,并查询交通票价...'
})
if (!options.skipUserMessage) {
messages.value.push(createMessage('user', userText, fileNames))
}
const pendingMessage = createMessage(
'assistant',
'正在进行申请信息模型复核。本步骤只识别意图和抽取字段,不会创建、更新或保存草稿。',
'正在复核申请信息,并查询交通票价,请稍候。',
[],
{
meta: ['模型复核中']
@@ -770,7 +788,7 @@ export function useTravelReimbursementSubmitComposer(ctx) {
isKnowledgeSession.value
? '正在整理财务知识答案...'
: activeSessionType.value === 'application'
? '正在识别并整理申请核对信息...'
? '正在识别申请信息并查询交通票价...'
: activeSessionType.value === 'approval'
? '正在查询审核上下文并整理风险提示...'
: '正在识别并整理右侧核对信息...'
@@ -1037,20 +1055,29 @@ export function useTravelReimbursementSubmitComposer(ctx) {
nextTick(scrollToBottom)
const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
if (!isKnowledgeSession.value && resolvedDraftClaimId && files.length) {
void syncComposerFilesToDraft(resolvedDraftClaimId, files)
.then((syncResult) => {
const persistComposerFilesToDraft = async () => {
try {
const syncResult = await syncComposerFilesToDraft(resolvedDraftClaimId, files)
persistSessionState()
if (detailScopedUpload && Number(syncResult?.uploadedCount || 0) > 0) {
if (detailScopedUpload) {
emitRequestUpdated?.({
claimId: resolvedDraftClaimId,
source: 'detail-smart-entry-attachment-sync'
source: 'detail-smart-entry-attachment-sync',
uploadedCount: Number(syncResult?.uploadedCount || 0),
skippedCount: Number(syncResult?.skippedCount || 0)
})
}
})
.catch((error) => {
} catch (error) {
console.warn('Failed to persist composer attachments to draft claim:', error)
toast(error?.message || '票据已归集到草稿,但附件原件保存失败,请在单据详情中重新上传。')
})
}
}
const persistTask = persistComposerFilesToDraft()
if (detailScopedUpload) {
await persistTask
} else {
void persistTask
}
}
} catch (error) {
clearFlowSimulationTimers()