feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user