feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -16,8 +16,13 @@ import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSu
import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js'
import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js'
import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js'
import {
buildOperationFeedbackPayload,
normalizeOperationFeedbackContext
} from '../../composables/useOperationFeedback.js'
import { recognizeOcrFiles } from '../../services/ocr.js'
import { fetchAgentRunDetail } from '../../services/agentAssets.js'
import { createOperationFeedback } from '../../services/operationFeedback.js'
import { deleteConversation, runOrchestrator } from '../../services/orchestrator.js'
import { renderMarkdown } from '../../utils/markdown.js'
import { clearAssistantSessionSnapshot } from '../../utils/assistantSessionSnapshot.js'
@@ -46,6 +51,7 @@ import {
} from '../../utils/expenseApplicationPreview.js'
import {
calculateTravelReimbursement,
createExpenseClaimItem,
fetchExpenseClaims,
fetchExpenseClaimAttachmentAsset,
fetchExpenseClaimDetail,
@@ -526,6 +532,10 @@ export default {
type: Object,
default: null
},
initialSessionType: {
type: String,
default: ''
},
entrySource: {
type: String,
default: 'requests'
@@ -543,7 +553,7 @@ export default {
default: 0
}
},
emits: ['close', 'draft-saved'],
emits: ['close', 'draft-saved', 'request-updated'],
setup(props, { emit }) {
const router = useRouter()
const { currentUser } = useSystemState()
@@ -605,14 +615,42 @@ export default {
resolveApplicationPreviewEditorControl,
resolveApplicationPreviewEditorOptions,
isApplicationPreviewEditing,
isApplicationPreviewDateEditorOpen,
openApplicationPreviewEditor,
commitApplicationPreviewEditor,
commitApplicationPreviewDateEditor,
cancelApplicationPreviewEditor,
setApplicationPreviewDateMode,
canApplyApplicationPreviewDateSelection,
handleApplicationPreviewEditorKeydown
} = useApplicationPreviewEditor({
persistSessionState,
toast
})
function applyLinkedApplicationPreviewDateSelection(selection) {
const editor = applicationPreviewEditor.value
if (editor.fieldKey !== 'time' || !editor.messageId) {
return false
}
const targetMessage = messages.value.find((item) =>
String(item.id || '') === String(editor.messageId || '')
)
if (!targetMessage?.applicationPreview) {
return false
}
applicationPreviewEditor.value = {
...editor,
dateMode: selection.mode === 'range' ? 'range' : 'single',
singleDate: selection.startDate,
rangeStartDate: selection.startDate,
rangeEndDate: selection.endDate || selection.startDate
}
return commitApplicationPreviewDateEditor(targetMessage)
}
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION)
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
@@ -884,8 +922,34 @@ export default {
buildReviewSlotMap,
isValidIsoDateString,
buildLocallySyncedReviewPayload,
formatDateInputValue
formatDateInputValue,
onComposerDateSelection: applyLinkedApplicationPreviewDateSelection
})
function syncComposerDateFromApplicationEditor() {
const editor = applicationPreviewEditor.value
const today = formatDateInputValue()
composerDateMode.value = editor.dateMode === 'range' ? 'range' : 'single'
composerSingleDate.value = editor.singleDate || today
composerRangeStartDate.value = editor.rangeStartDate || composerSingleDate.value || today
composerRangeEndDate.value = editor.rangeEndDate || composerRangeStartDate.value || today
composerDatePickerOpen.value = true
travelCalculatorOpen.value = false
}
function openApplicationPreviewEditorFromUi(message, fieldKey, value) {
openApplicationPreviewEditor(message, fieldKey, value)
if (fieldKey === 'time' && isApplicationPreviewEditing(message, 'time')) {
syncComposerDateFromApplicationEditor()
}
}
watch(composerDatePickerOpen, (open, previousOpen) => {
if (!open && previousOpen && applicationPreviewEditor.value.fieldKey === 'time') {
cancelApplicationPreviewEditor()
}
})
const canShowTravelCalculator = computed(() => activeSessionType.value === SESSION_TYPE_EXPENSE)
const {
fileInputMode,
@@ -918,6 +982,7 @@ export default {
reviewActionBusy,
toast,
fileInputRef,
createExpenseClaimItem,
fetchExpenseClaimDetail,
fetchExpenseClaimItemAttachmentMeta,
fetchExpenseClaimAttachmentAsset,
@@ -939,6 +1004,32 @@ export default {
composerFilesExpanded,
guidedFlowState
}
const promptedOperationFeedbackRunIds = new Set()
function emitOperationCompleted(payload = {}, extras = {}) {
const runId = String(payload?.run_id || payload?.runId || '').trim()
const operationStatus = String(payload?.status || '').trim()
if (!runId || promptedOperationFeedbackRunIds.has(runId) || operationStatus !== 'succeeded') {
return null
}
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
promptedOperationFeedbackRunIds.add(runId)
return normalizeOperationFeedbackContext({
run_id: runId,
conversation_id: String(payload?.conversation_id || payload?.conversationId || conversationId.value || '').trim(),
user_id: resolveCurrentUserId(),
selected_agent: String(payload?.selected_agent || payload?.selectedAgent || '').trim(),
source: 'user_message',
session_type: activeSessionType.value,
operation_type: String(extras.operationType || 'assistant_round').trim(),
operation_status: operationStatus,
status: operationStatus,
route_reason: String(payload?.route_reason || payload?.routeReason || '').trim(),
entry_source: props.entrySource,
trace_summary: payload?.trace_summary || payload?.traceSummary || null,
result_summary: String(result.answer || result.message || '').trim()
}, currentUser.value || {})
}
const {
confirmPendingAttachmentAssociationInternal,
submitComposerInternal
@@ -1016,6 +1107,8 @@ export default {
startSemanticFlowPreview,
submitting,
syncComposerFilesToDraft,
emitOperationCompleted,
emitRequestUpdated: (payload) => emit('request-updated', payload),
toast
})
const canSubmit = computed(
@@ -1757,6 +1850,121 @@ export default {
return buildApplicationPreviewFooterMessage(message.applicationPreview)
}
function isApplicationDraftPayload(draftPayload) {
return String(draftPayload?.draft_type || '').trim() === 'expense_application'
}
function resolveDraftPayloadBodyField(draftPayload, label) {
const body = String(draftPayload?.body || '')
const pattern = new RegExp(`^${label}(.+)$`, 'm')
return String(body.match(pattern)?.[1] || '').trim()
}
function resolveApplicationDraftStatusLabel(draftPayload) {
const status = String(draftPayload?.status || '').trim()
if (status === 'submitted') return '审批中'
return status || '已生成'
}
function buildApplicationDraftSummaryItems(draftPayload) {
if (!isApplicationDraftPayload(draftPayload)) {
return []
}
return [
{ label: '单号', value: String(draftPayload?.claim_no || '').trim() || '待生成' },
{ label: '类型', value: String(draftPayload?.title || '').trim() || '费用申请' },
{ label: '节点', value: String(draftPayload?.approval_stage || '').trim() || '直属领导审批' },
{ label: '时间', value: resolveDraftPayloadBodyField(draftPayload, '发生时间') },
{ label: '费用', value: resolveDraftPayloadBodyField(draftPayload, '用户预估费用') }
].filter((item) => String(item.value || '').trim() && item.value !== '待补充')
}
function updateMessageOperationFeedback(message, patch = {}) {
if (!message?.id) {
return
}
messages.value = messages.value.map((item) => (
item.id === message.id
? {
...item,
operationFeedback: {
...(item.operationFeedback || {}),
...patch
}
}
: item
))
}
function isOperationFeedbackVisible(message) {
const feedback = message?.operationFeedback || null
return Boolean(
feedback?.context
&& !feedback.dismissed
)
}
function dismissOperationFeedbackForMessage(message) {
updateMessageOperationFeedback(message, {
dismissed: true,
error: ''
})
persistSessionState()
}
async function submitOperationFeedbackForMessage(message, feedback = {}) {
const rating = Number(feedback.rating || 0)
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
updateMessageOperationFeedback(message, { error: '请选择 1 到 5 星评分。' })
return
}
const context = message?.operationFeedback?.context || null
if (!context) {
return
}
updateMessageOperationFeedback(message, {
submitting: true,
rating,
reason: String(feedback.reason || '').trim(),
error: ''
})
try {
await createOperationFeedback(
buildOperationFeedbackPayload(context, feedback, currentUser.value || {})
)
updateMessageOperationFeedback(message, {
submitting: false,
submitted: true,
dismissed: false,
rating,
reason: String(feedback.reason || '').trim(),
error: ''
})
persistSessionState()
} catch (error) {
updateMessageOperationFeedback(message, {
submitting: false,
error: error?.message || '评价提交失败,请稍后重试。'
})
}
}
async function openApplicationDraftDetail(message) {
const draftPayload = message?.draftPayload || {}
const claimId = String(draftPayload.claim_id || draftPayload.claimId || '').trim()
if (!claimId) {
toast('暂未获取到申请单据 ID稍后可在单据中心查看。')
return
}
await router.push({
name: 'app-document-detail',
params: { requestId: claimId }
})
emit('close')
}
function resolveApplicationPreviewMissingFields(message) {
if (!message?.applicationPreview) {
return []
@@ -1818,6 +2026,7 @@ export default {
pendingText: '正在提交费用申请...',
systemGenerated: true,
skipScopeGuard: true,
feedbackOperationType: 'submit_application',
extraContext: {
application_preview: applicationPreview,
user_input_text: applicationSubmitText
@@ -2181,10 +2390,21 @@ export default {
resolveApplicationPreviewEditorOptions,
resolveApplicationPreviewMissingFields,
isApplicationPreviewEditing,
openApplicationPreviewEditor,
isApplicationPreviewDateEditorOpen,
openApplicationPreviewEditor: openApplicationPreviewEditorFromUi,
commitApplicationPreviewEditor,
commitApplicationPreviewDateEditor,
setApplicationPreviewDateMode,
canApplyApplicationPreviewDateSelection,
handleApplicationPreviewEditorKeydown,
buildApplicationPreviewFooterText,
isApplicationDraftPayload,
resolveApplicationDraftStatusLabel,
buildApplicationDraftSummaryItems,
openApplicationDraftDetail,
isOperationFeedbackVisible,
dismissOperationFeedbackForMessage,
submitOperationFeedbackForMessage,
runWelcomeQuickAction: runShortcut,
handleSuggestedAction,
isSuggestedActionSelected,
@@ -2297,7 +2517,7 @@ export default {
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, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, openApplicationPreviewEditor, commitApplicationPreviewEditor, cancelApplicationPreviewEditor, handleApplicationPreviewEditorKeydown, buildApplicationPreviewFooterText, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor: openApplicationPreviewEditorFromUi, commitApplicationPreviewEditor, commitApplicationPreviewDateEditor, cancelApplicationPreviewEditor, setApplicationPreviewDateMode, canApplyApplicationPreviewDateSelection, handleApplicationPreviewEditorKeydown, buildApplicationPreviewFooterText, isApplicationDraftPayload, resolveApplicationDraftStatusLabel, buildApplicationDraftSummaryItems, openApplicationDraftDetail, isOperationFeedbackVisible, dismissOperationFeedbackForMessage, submitOperationFeedbackForMessage, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
}
}
}