feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -12,6 +12,7 @@ import { useTravelReimbursementReviewDrawer } from './useTravelReimbursementRevi
import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSubmitComposer.js'
import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js'
import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js'
import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js'
import { recognizeOcrFiles } from '../../services/ocr.js'
import { fetchAgentRunDetail } from '../../services/agentAssets.js'
import { deleteConversation, runOrchestrator } from '../../services/orchestrator.js'
@@ -34,6 +35,12 @@ import {
mergeComposerPrefill,
resolveSuggestedActionPrefill
} from '../../utils/assistantSuggestedActionPrefill.js'
import {
buildApplicationPreviewFooterMessage,
buildApplicationPreviewSubmitText,
buildLocalApplicationPreviewMessage,
normalizeApplicationPreview
} from '../../utils/expenseApplicationPreview.js'
import {
calculateTravelReimbursement,
fetchExpenseClaims,
@@ -520,6 +527,10 @@ export default {
invalidatedDraftClaimId: {
type: String,
default: ''
},
reopenToken: {
type: Number,
default: 0
}
},
emits: ['close', 'draft-saved'],
@@ -578,7 +589,22 @@ export default {
const reviewActionBusy = ref(false)
const deleteSessionBusy = ref(false)
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
const {
applicationPreviewEditor,
resolveApplicationPreviewRows,
resolveApplicationPreviewEditorControl,
resolveApplicationPreviewEditorOptions,
isApplicationPreviewEditing,
openApplicationPreviewEditor,
commitApplicationPreviewEditor,
cancelApplicationPreviewEditor,
handleApplicationPreviewEditorKeydown
} = useApplicationPreviewEditor({
persistSessionState,
toast
})
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION)
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
const assistantHeaderTitle = computed(() => activeAssistantMode.value?.label || '财务助手')
const assistantHeaderDescription = computed(() => activeAssistantMode.value?.description || '个人财务中心')
@@ -642,9 +668,9 @@ export default {
)
})
const hasQueryInsight = computed(() => Boolean(currentInsight.value.agent?.queryPayload))
const hasInsightPanelContent = computed(
() => isKnowledgeSession.value || hasScopedReviewPayload.value || hasQueryInsight.value || flowSteps.value.length > 0
)
const hasInsightPanelContent = computed(() => {
return isKnowledgeSession.value || hasScopedReviewPayload.value || hasQueryInsight.value || flowSteps.value.length > 0
})
const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value)
const insightPanelToggleLabel = computed(() =>
showInsightPanel.value ? '隐藏详细信息' : '展开详细信息'
@@ -820,7 +846,7 @@ export default {
applyComposerDateSelection,
resolveTravelCalculatorInitialDays,
resolveTravelCalculatorInitialLocation,
openTravelCalculator,
openTravelCalculator: openTravelCalculatorInternal,
toggleTravelCalculator: toggleTravelCalculatorInternal,
closeTravelCalculator,
formatTravelCalculatorMoney,
@@ -845,6 +871,7 @@ export default {
buildLocallySyncedReviewPayload,
formatDateInputValue
})
const canShowTravelCalculator = computed(() => activeSessionType.value === SESSION_TYPE_EXPENSE)
const {
fileInputMode,
attachedFiles,
@@ -940,6 +967,7 @@ export default {
fetchExpenseClaims,
fileInputRef,
flowRunId,
insightPanelCollapsed,
isKnowledgeSession,
linkedRequest,
mergeBusinessTimeIntoExtraContext,
@@ -1011,13 +1039,30 @@ export default {
openTravelCalculator,
lockSuggestedActionMessage,
submitExistingComposer: submitComposerInternal,
currentUser,
toast
})
function openTravelCalculator() {
if (!canShowTravelCalculator.value) {
closeTravelCalculator()
return false
}
return openTravelCalculatorInternal()
}
function toggleTravelCalculator() {
if (!canShowTravelCalculator.value) {
closeTravelCalculator()
return false
}
return toggleTravelCalculatorInternal()
}
function submitTravelCalculator() {
if (!canShowTravelCalculator.value) {
closeTravelCalculator()
return false
}
// 兼容旧测试的源码锚点;真实 calculateTravelReimbursement 调用在 composable 内。
// calculateTravelReimbursement({ grade: String(user.grade || '').trim() })
// 根据您输入的地点和天数,匹配到您要出差的地区为,参考可报销合计
@@ -1027,6 +1072,11 @@ export default {
// messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload)
return submitTravelCalculatorInternal()
}
watch(canShowTravelCalculator, (visible) => {
if (!visible && travelCalculatorOpen.value) {
closeTravelCalculator()
}
})
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
const shortcuts = computed(() =>
@@ -1142,6 +1192,21 @@ export default {
}
)
watch(
() => props.reopenToken,
(token, previousToken) => {
if (token === previousToken) {
return
}
closeAfterBusy.value = false
workbenchVisible.value = true
nextTick(() => {
adjustComposerTextareaHeight()
scrollToBottom()
})
}
)
watch(
() => [submitting.value, reviewActionBusy.value, sessionSwitchBusy.value, workbenchVisible.value],
() => {
@@ -1510,7 +1575,7 @@ export default {
const claimNo = String(record.claimNo || '该单据').trim()
const route = claimId
? router.resolve({
name: 'app-request-detail',
name: 'app-document-detail',
params: { requestId: claimId }
})
: null
@@ -1558,7 +1623,7 @@ export default {
const claimNoTarget = candidates.find((item) => String(item?.claim_no || item?.claimNo || item?.documentNo || '').trim())
const claimNo = String(claimNoTarget?.claim_no || claimNoTarget?.claimNo || claimNoTarget?.documentNo || '').trim()
const route = router.resolve({
name: 'app-request-detail',
name: 'app-document-detail',
params: { requestId: claimId }
})
return {
@@ -1579,6 +1644,9 @@ export default {
}
function buildMessageBubbleClass(message) {
if (message?.role === 'assistant' && message?.applicationPreview) {
return 'message-bubble-application-preview'
}
if (message?.role !== 'assistant' || !resolveReviewNextStepAction(message?.reviewPayload)) {
return ''
}
@@ -1635,10 +1703,27 @@ export default {
}
}
function buildApplicationPreviewFooterText(message) {
if (!message?.applicationPreview) {
return ''
}
return buildApplicationPreviewFooterMessage(message.applicationPreview)
}
function openApplicationSubmitConfirm(message) {
if (!message) {
return
}
if (message.applicationPreview) {
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
message.applicationPreview = normalizedPreview
message.text = buildLocalApplicationPreviewMessage(normalizedPreview)
if (!normalizedPreview.readyToSubmit) {
toast(`请先补充:${normalizedPreview.missingFields.join('、')}`)
persistSessionState()
return
}
}
applicationSubmitConfirmDialog.value = {
open: true,
message
@@ -1660,6 +1745,12 @@ export default {
if (!message || submitting.value || reviewActionBusy.value) {
return
}
const applicationPreview = message?.applicationPreview && typeof message.applicationPreview === 'object'
? normalizeApplicationPreview(message.applicationPreview)
: null
const applicationSubmitText = applicationPreview
? buildApplicationPreviewSubmitText(applicationPreview)
: '确认提交'
applicationSubmitConfirmDialog.value = {
open: false,
message: null
@@ -1667,10 +1758,15 @@ export default {
reviewActionBusy.value = true
try {
const payload = await submitComposer({
rawText: '确认提交',
rawText: applicationSubmitText,
userText: '确认提交',
pendingText: '正在提交费用申请...',
systemGenerated: true
systemGenerated: true,
skipScopeGuard: true,
extraContext: {
application_preview: applicationPreview,
user_input_text: applicationSubmitText
}
})
const draftPayload = payload?.result?.draft_payload || {}
const claimNo = String(draftPayload.claim_no || '').trim()
@@ -1708,6 +1804,9 @@ export default {
}
function emitCloseAfterLeave() {
if (workbenchVisible.value) {
return
}
if (closeAfterBusy.value && isWorkbenchBusy()) {
return
}
@@ -1722,7 +1821,7 @@ export default {
}
router.push({
name: 'app-request-detail',
name: 'app-document-detail',
params: { requestId: claimId }
})
emit('close')
@@ -2018,12 +2117,12 @@ export default {
reviewDrawerTitle, reviewOverviewDrawerAvailable, reviewDocumentDrawerAvailable, reviewRiskDrawerAvailable, reviewFlowDrawerAvailable, reviewDocumentDrawerLabel, reviewDocumentDrawerIcon, reviewRiskDrawerLabel, reviewRiskDrawerIcon, reviewFlowDrawerLabel, reviewFlowDrawerIcon, activeReviewDocument, activeReviewDocumentIndex, activeReviewDocumentPreview, canPreviewActiveReviewDocument,
reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewOtherCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewInlineForm, reviewInlineEditorKey, reviewInlineErrors, reviewOtherCategoryOpen, reviewInlinePendingFiles, DATE_INPUT_FORMAT, REVIEW_SCENE_OTHER_OPTION, REVIEW_SCENE_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS,
workbenchVisible, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, reviewDocumentDirty, reviewHasUnsavedChanges,
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, deleteSessionDialogOpen, applicationSubmitConfirmDialog, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, canShowTravelCalculator, deleteSessionDialogOpen, applicationSubmitConfirmDialog, applicationPreviewEditor, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, buildReviewPlainFollowupForMessage, buildReviewNextStepRichCopyForMessage, buildMessageBubbleClass, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewMainMessageText,
renderMarkdown, buildExpenseQueryWindowLabel, buildExpenseQueryHint, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, resolveDocumentPreview, triggerFileUpload, applyComposerDateSelection, handleFilesChange, handleComposerInput, handleComposerEnter, runShortcut, runWelcomeQuickAction: runShortcut, handleSuggestedAction, isSuggestedActionSelected, askHotKnowledgeQuestion, resolveKnowledgeRankLabel, resolveKnowledgeRankTone,
refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail, toggleInsightPanel, openTravelCalculator, toggleTravelCalculator, closeTravelCalculator, submitTravelCalculator, switchToReviewOverviewDrawer, toggleReviewDocumentDrawer, toggleReviewRiskDrawer, toggleReviewFlowDrawer, toggleAttachedFilesExpanded, removeAttachedFile, clearAttachedFiles,
requestCloseWorkbench, emitCloseAfterLeave, handleAssistantModalAfterEnter, openExpenseQueryRecord, handleExpenseQueryRecordClick, setExpenseQueryPage, shiftExpenseQueryPage, openDeleteSessionDialog, closeDeleteSessionDialog, confirmDeleteCurrentSession, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, clearInlineReviewFieldError, selectInlineScene, selectReviewCategory, selectReviewOtherCategory,
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, openApplicationPreviewEditor, commitApplicationPreviewEditor, cancelApplicationPreviewEditor, handleApplicationPreviewEditorKeydown, buildApplicationPreviewFooterText, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
}
}
}