import { computed, ref } from 'vue' function formatFlowDuration(ms) { if (ms === null || ms === undefined || ms === '') { return '--' } const numericValue = Number(ms) if (!Number.isFinite(numericValue) || numericValue <= 0) { return '--' } if (numericValue < 1000) { return `${Math.max(0.1, numericValue / 1000).toFixed(1)}s` } if (numericValue < 10000) { return `${(numericValue / 1000).toFixed(1)}s` } return `${Math.round(numericValue / 1000)}s` } function parseFlowTimestamp(value) { if (value === null || value === undefined || value === '') { return 0 } if (typeof value === 'number' && Number.isFinite(value)) { return value > 0 && value < 10000000000 ? Math.round(value * 1000) : Math.round(value) } const timestamp = new Date(value).getTime() return Number.isFinite(timestamp) ? timestamp : 0 } const FLOW_DURATION_MS_FIELDS = [ 'duration_ms', 'elapsed_ms', 'latency_ms', 'total_duration_ms', 'execution_time_ms' ] const FLOW_DURATION_SECOND_FIELDS = [ 'duration_seconds', 'elapsed_seconds', 'latency_seconds', 'execution_time_seconds' ] const FLOW_DURATION_AUTO_FIELDS = ['duration', 'elapsed', 'latency', 'execution_time'] const FLOW_STARTED_AT_FIELDS = ['started_at', 'start_time', 'created_at', 'queued_at'] const FLOW_FINISHED_AT_FIELDS = ['finished_at', 'completed_at', 'ended_at', 'end_time', 'updated_at'] function normalizeDurationValue(value, unit = 'ms') { if (value === null || value === undefined || value === '') { return null } let numericValue = Number(value) let normalizedUnit = unit if (typeof value === 'string') { const text = value.trim() const match = text.match(/^(\d+(?:\.\d+)?)\s*(ms|毫秒|s|秒)?$/i) if (match) { numericValue = Number(match[1]) if (match[2]) { normalizedUnit = ['s', '秒'].includes(match[2].toLowerCase()) ? 'seconds' : 'ms' } } } if (!Number.isFinite(numericValue) || numericValue <= 0) { return null } if (normalizedUnit === 'seconds') { return Math.round(numericValue * 1000) } if (normalizedUnit === 'auto') { return Math.round(numericValue <= 300 ? numericValue * 1000 : numericValue) } return Math.round(numericValue) } function readFirstDurationField(source, fields, unit) { if (!source || typeof source !== 'object') { return null } for (const field of fields) { if (!Object.prototype.hasOwnProperty.call(source, field)) { continue } const durationMs = normalizeDurationValue(source[field], unit) if (durationMs) { return durationMs } } return null } function resolveDurationFromFields(source) { return ( readFirstDurationField(source, FLOW_DURATION_MS_FIELDS, 'ms') || readFirstDurationField(source, FLOW_DURATION_SECOND_FIELDS, 'seconds') || readFirstDurationField(source, FLOW_DURATION_AUTO_FIELDS, 'auto') ) } function readFirstTimestampField(source, fields) { if (!source || typeof source !== 'object') { return 0 } for (const field of fields) { const timestamp = parseFlowTimestamp(source[field]) if (timestamp) { return timestamp } } return 0 } function resolveStartedTimestamp(source) { return readFirstTimestampField(source, FLOW_STARTED_AT_FIELDS) } function resolveFinishedTimestamp(source) { return readFirstTimestampField(source, FLOW_FINISHED_AT_FIELDS) } function resolveTimeRangeDurationMs(source) { const startedAt = resolveStartedTimestamp(source) const finishedAt = resolveFinishedTimestamp(source) return finishedAt > startedAt ? finishedAt - startedAt : null } function resolveSemanticPhaseDurations(run) { const runStart = resolveStartedTimestamp(run) const toolCalls = Array.isArray(run?.tool_calls) ? run.tool_calls : [] const firstToolStartedAt = toolCalls .map((item) => resolveStartedTimestamp(item)) .filter((value) => value > 0) .sort((left, right) => left - right)[0] || 0 const runFinishedAt = resolveFinishedTimestamp(run) const semanticFinishedAt = firstToolStartedAt || runFinishedAt if (!runStart || !semanticFinishedAt || semanticFinishedAt <= runStart) { return { intentMs: null, extractionMs: null } } const totalMs = semanticFinishedAt - runStart const intentMs = Math.max(120, Math.round(totalMs * 0.35)) const extractionMs = Math.max(160, totalMs - intentMs) return { intentMs, extractionMs } } function resolveToolCallDurationMs(toolCall, index, toolCalls, run) { const response = toolCall?.response_json && typeof toolCall.response_json === 'object' ? toolCall.response_json : {} const explicitDuration = resolveDurationFromFields(toolCall) || resolveTimeRangeDurationMs(toolCall) || resolveDurationFromFields(response) || resolveTimeRangeDurationMs(response) if (explicitDuration) { return explicitDuration } const startedAt = resolveStartedTimestamp(toolCall) if (!startedAt) { return null } const nextStartedAt = resolveStartedTimestamp(toolCalls[index + 1]) const runFinishedAt = resolveFinishedTimestamp(run) const finishedAt = nextStartedAt > startedAt ? nextStartedAt : (runFinishedAt > startedAt ? runFinishedAt : 0) if (!finishedAt || finishedAt <= startedAt) { return null } return finishedAt - startedAt } function summarizeVisibleToolText(value) { const text = String(value || '') .replace(/\|[^\n]*\|/g, '') .replace(/\*\*/g, '') .split('\n') .map((line) => line.trim()) .find(Boolean) || '' if (!text) { return '' } return text.length > 80 ? `${text.slice(0, 80)}...` : text } export function useTravelReimbursementFlow({ activeSessionType, reviewDrawerMode, insightPanelCollapsed, isKnowledgeSession, fetchAgentRunDetail, buildLocalIntentPreview, buildLocalExtractionProgressMessages, summarizeSemanticIntentDetail, summarizeSemanticParseDetail, SCENARIO_LABELS, INTENT_LABELS, EXPENSE_TYPE_LABELS, FLOW_STEP_FALLBACKS, REVIEW_DRAWER_MODE_FLOW, REVIEW_DRAWER_MODE_REVIEW, FLOW_STEP_STATUS_PENDING, FLOW_STEP_STATUS_RUNNING, FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED }) { const flowRunId = ref('') const flowSessionType = ref('') const flowStartedAt = ref(0) const flowFinishedAt = ref(0) const flowSteps = ref([]) const flowRefreshBusy = ref(false) const flowTick = ref(Date.now()) let flowTickTimer = 0 const flowSimulationTimers = [] const activeFlowSteps = computed(() => ( flowSessionType.value === resolveCurrentFlowSessionType() ? flowSteps.value : [] )) const completedFlowStepCount = computed( () => activeFlowSteps.value.filter((step) => step.status === FLOW_STEP_STATUS_COMPLETED).length ) const rawRunningFlowStep = computed( () => flowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_RUNNING) || null ) const runningFlowStep = computed( () => activeFlowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_RUNNING) || null ) const visibleFlowSteps = computed(() => { const visibleSteps = [] for (const step of activeFlowSteps.value) { visibleSteps.push(step) if (step.status !== FLOW_STEP_STATUS_COMPLETED) { break } } return visibleSteps }) const flowOverallStatusTone = computed(() => { if (activeFlowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) { return 'failed' } if (runningFlowStep.value) { return 'running' } if ( activeFlowSteps.value.length && completedFlowStepCount.value === activeFlowSteps.value.length && flowStartedAt.value ) { return 'completed' } return 'pending' }) const flowOverallStatusText = computed(() => { const total = activeFlowSteps.value.length const completed = completedFlowStepCount.value if (flowOverallStatusTone.value === 'failed') { return `异常 ${completed}/${total}` } if (flowOverallStatusTone.value === 'completed') { return `已完成 ${total}/${total}` } if (flowOverallStatusTone.value === 'running') { return `执行中 ${completed}/${total}` } return total ? `待执行 0/${total}` : '暂无流程' }) const flowTotalDurationText = computed(() => { if (!flowStartedAt.value) { return '--' } const finishedAt = flowFinishedAt.value || (runningFlowStep.value ? flowTick.value : 0) if (finishedAt > flowStartedAt.value) { return formatFlowDuration(finishedAt - flowStartedAt.value) } const measuredDuration = activeFlowSteps.value.reduce((total, step) => { const duration = Number(step.durationMs) return total + (Number.isFinite(duration) && duration > 0 ? duration : 0) }, 0) return measuredDuration ? formatFlowDuration(measuredDuration) : '--' }) function resolveCurrentFlowSessionType() { return String(activeSessionType?.value || '').trim() } function startFlowTick() { if (flowTickTimer) { return } flowTickTimer = window.setInterval(() => { flowTick.value = Date.now() }, 250) } function stopFlowRuntime() { if (flowTickTimer) { window.clearInterval(flowTickTimer) flowTickTimer = 0 } clearFlowSimulationTimers() } function clearFlowSimulationTimers() { while (flowSimulationTimers.length) { const timerId = flowSimulationTimers.pop() window.clearTimeout(timerId) window.clearInterval(timerId) } } function resetFlowRun(options = {}) { clearFlowSimulationTimers() const shouldOpenDrawer = options.openDrawer !== false const startedAt = Number(options.startedAt) flowRunId.value = '' flowSessionType.value = String(options.sessionType || resolveCurrentFlowSessionType()).trim() flowStartedAt.value = Number.isFinite(startedAt) && startedAt >= 0 ? startedAt : Date.now() flowFinishedAt.value = 0 if (shouldOpenDrawer) { reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW insightPanelCollapsed.value = false } flowSteps.value = [] } function findFlowDefinition(key) { return FLOW_STEP_FALLBACKS[key] || null } function normalizeFlowStepPatch(key, patch = {}) { const definition = findFlowDefinition(key) || {} const normalizedPatch = typeof patch === 'string' ? { detail: patch } : { ...patch } return { title: normalizedPatch.title || definition.title || '智能体工具调用', tool: normalizedPatch.tool || definition.tool || 'AgentTool', detail: normalizedPatch.detail || definition.runningText || '', ...normalizedPatch } } function createFlowStep(key, patch = {}) { const normalizedPatch = normalizeFlowStepPatch(key, patch) return { key, index: flowSteps.value.length + 1, title: normalizedPatch.title, tool: normalizedPatch.tool, status: normalizedPatch.status || FLOW_STEP_STATUS_PENDING, detail: normalizedPatch.detail || '', durationMs: normalizedPatch.durationMs ?? null, startedAt: normalizedPatch.startedAt || 0, finishedAt: normalizedPatch.finishedAt || 0, error: normalizedPatch.error || '', deferredCompletion: Boolean(normalizedPatch.deferredCompletion), syntheticTiming: Boolean(normalizedPatch.syntheticTiming) } } function normalizeFlowStepIndexes(steps) { return steps.map((step, index) => ({ ...step, index: index + 1 })) } function upsertFlowStep(key, patch) { const existingStep = flowSteps.value.find((step) => step.key === key) if (!existingStep) { const nextStep = createFlowStep(key, patch) flowSteps.value = normalizeFlowStepIndexes([...flowSteps.value, nextStep]) return } const normalizedPatch = normalizeFlowStepPatch(key, patch) flowSteps.value = flowSteps.value.map((step) => ( step.key === key ? { ...step, ...normalizedPatch } : step )) } function startFlowStep(key, patch = {}) { if (!flowSessionType.value) { flowSessionType.value = resolveCurrentFlowSessionType() } const normalizedPatch = normalizeFlowStepPatch(key, patch) const explicitStartedAt = Number(normalizedPatch.startedAt) const startedAt = Number.isFinite(explicitStartedAt) && explicitStartedAt > 0 ? explicitStartedAt : Date.now() flowFinishedAt.value = 0 upsertFlowStep(key, { ...normalizedPatch, status: FLOW_STEP_STATUS_RUNNING, detail: normalizedPatch.detail, startedAt, finishedAt: 0, durationMs: null, error: '', syntheticTiming: Boolean(normalizedPatch.syntheticTiming) }) } function completeFlowStep(key, detail = '', durationMs = null, patch = {}) { const now = Date.now() const definition = findFlowDefinition(key) const currentStep = flowSteps.value.find((step) => step.key === key) const explicitDuration = Number(durationMs) const hasExplicitDuration = Number.isFinite(explicitDuration) && explicitDuration >= 0 const startedAt = currentStep?.startedAt || (hasExplicitDuration ? Math.max(0, now - explicitDuration) : 0) const measuredDuration = hasExplicitDuration ? explicitDuration : startedAt && !currentStep?.syntheticTiming ? Math.max(0, now - startedAt) : null upsertFlowStep(key, { ...patch, status: FLOW_STEP_STATUS_COMPLETED, detail: detail || definition?.completedText || '', startedAt, finishedAt: now, durationMs: measuredDuration, error: '', deferredCompletion: false, syntheticTiming: false }) if ( flowSteps.value.length && flowSteps.value.every((step) => [FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)) ) { flowFinishedAt.value = now } } function failFlowStep(key, detail = '', error = '', patch = {}) { const now = Date.now() const definition = findFlowDefinition(key) const currentStep = flowSteps.value.find((step) => step.key === key) const startedAt = currentStep?.startedAt || now upsertFlowStep(key, { ...patch, status: FLOW_STEP_STATUS_FAILED, detail: detail || error || '调用失败', startedAt, finishedAt: now, durationMs: now - startedAt, error: String(error || definition?.title || '').trim() }) flowFinishedAt.value = now } function completePendingFlowStep(key, detail = '', durationMs = null, patch = {}) { const patchObject = patch && typeof patch === 'object' ? { ...patch } : {} const refreshCompleted = Boolean(patchObject.refreshCompleted) delete patchObject.refreshCompleted const currentStep = flowSteps.value.find((step) => step.key === key) const normalizedDuration = Number(durationMs) const hasMeasuredDuration = Number.isFinite(normalizedDuration) && normalizedDuration > 0 if (currentStep?.status === FLOW_STEP_STATUS_COMPLETED) { if (refreshCompleted && hasMeasuredDuration) { completeFlowStep(key, detail, normalizedDuration, patchObject) } return } if (!currentStep || currentStep.status === FLOW_STEP_STATUS_PENDING) { const revealOrder = flowSteps.value.length startFlowStep(key, { ...patchObject, deferredCompletion: true, syntheticTiming: !hasMeasuredDuration }) const completionTimer = window.setTimeout(() => { completeFlowStep( key, detail || findFlowDefinition(key)?.completedText || '', hasMeasuredDuration ? normalizedDuration : null, { deferredCompletion: false } ) }, 280 + Math.min(revealOrder, 4) * 180) flowSimulationTimers.push(completionTimer) return } completeFlowStep(key, detail, hasMeasuredDuration ? normalizedDuration : null, patchObject) } function failCurrentFlowStep(error) { clearFlowSimulationTimers() const currentStep = rawRunningFlowStep.value || flowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_PENDING) failFlowStep( currentStep?.key || 'orchestrator-error', error?.message || '智能体调用失败', error?.message || '', currentStep ? {} : { title: '流程调用', tool: 'Orchestrator' } ) } function startSemanticFlowPreview(rawText, options = {}) { clearFlowSimulationTimers() const intentPreview = buildLocalIntentPreview(rawText, activeSessionType.value, { intentLabels: INTENT_LABELS }) const extractionMessages = buildLocalExtractionProgressMessages(rawText, options) const completeIntentTimer = window.setTimeout(() => { const currentStep = flowSteps.value.find((step) => step.key === 'intent') if (currentStep?.status === FLOW_STEP_STATUS_COMPLETED || currentStep?.status === FLOW_STEP_STATUS_FAILED) { return } completePendingFlowStep('intent', intentPreview, null) }, 260) flowSimulationTimers.push(completeIntentTimer) const startExtractionTimer = window.setTimeout(() => { const currentStep = flowSteps.value.find((step) => step.key === 'extraction') if (currentStep?.status === FLOW_STEP_STATUS_COMPLETED || currentStep?.status === FLOW_STEP_STATUS_FAILED) { return } startFlowStep('extraction', extractionMessages[0] || FLOW_STEP_FALLBACKS.extraction.runningText) if (extractionMessages.length <= 1) { return } let index = 1 const detailTimer = window.setInterval(() => { const runningStep = flowSteps.value.find((step) => step.key === 'extraction') if (!runningStep || runningStep.status !== FLOW_STEP_STATUS_RUNNING) { window.clearInterval(detailTimer) return } upsertFlowStep('extraction', { detail: extractionMessages[index] || extractionMessages[extractionMessages.length - 1] }) index = Math.min(index + 1, extractionMessages.length - 1) }, 650) flowSimulationTimers.push(detailTimer) }, 420) flowSimulationTimers.push(startExtractionTimer) } function startExpenseSceneSelectionFlowPreview(rawText) { clearFlowSimulationTimers() const intentPreview = buildLocalIntentPreview(rawText, activeSessionType.value, { intentLabels: INTENT_LABELS }) const completeIntentTimer = window.setTimeout(() => { completePendingFlowStep('intent', intentPreview, null) }, 220) flowSimulationTimers.push(completeIntentTimer) const startSelectionTimer = window.setTimeout(() => { startFlowStep('expense-scene-selection', { detail: '报销意图已确认,但费用场景还不明确;暂停信息抽取,等待用户先选择报销场景。' }) }, 320) flowSimulationTimers.push(startSelectionTimer) } function startExpenseIntentConfirmationFlowPreview(rawText) { clearFlowSimulationTimers() const intentPreview = buildLocalIntentPreview(rawText, activeSessionType.value, { intentLabels: INTENT_LABELS }) const completeIntentTimer = window.setTimeout(() => { completePendingFlowStep('intent', intentPreview, null) }, 220) flowSimulationTimers.push(completeIntentTimer) const startConfirmationTimer = window.setTimeout(() => { startFlowStep('expense-intent-confirmation', { detail: '识别到业务事项描述,但是否发起报销还不明确;暂停信息抽取,等待用户确认。' }) }, 320) flowSimulationTimers.push(startConfirmationTimer) } function startExpenseSceneSelectionAfterIntentConfirmation(rawText) { clearFlowSimulationTimers() completePendingFlowStep('expense-intent-confirmation', '用户已确认要发起报销', null) startFlowStep('expense-scene-selection', { detail: '报销意图已确认,但费用场景还不明确;暂停信息抽取,等待用户先选择报销场景。' }) if (reviewDrawerMode.value !== REVIEW_DRAWER_MODE_FLOW) { reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW } } function isExpenseSceneSelectionResult(payload) { const result = payload?.result && typeof payload.result === 'object' ? payload.result : {} if (result.review_payload) { return false } return (Array.isArray(result.suggested_actions) ? result.suggested_actions : []).some( (item) => String(item?.action_type || '').trim() === 'select_expense_type' ) } function startReviewActionFlowStep(reviewAction) { if (reviewAction !== 'next_step') { return } startFlowStep('pre-submit-review', { title: 'AI预审与风险识别', tool: 'ExpenseClaimService.submit_claim', detail: '正在校验财务规则、风险规则和审批路径...' }) } function startExpenseClaimDraftFlowStep(reviewAction, options = {}) { if (isKnowledgeSession.value) { return } if (options.waitForSceneSelection) { return } if (reviewAction === 'next_step') { startReviewActionFlowStep(reviewAction) return } const attachmentCount = Math.max(0, Number(options.attachmentCount || 0)) const configs = { save_draft: { key: 'expense-claim-draft', title: '保存报销草稿', tool: 'database.expense_claims.save_or_submit', detail: '正在把已确认信息保存为草稿...' }, link_to_existing_draft: { key: 'attachment-association', title: '票据关联草稿', tool: 'database.expense_claims.save_or_submit', detail: '正在把本次票据关联到现有草稿...' }, create_new_claim_from_documents: { key: 'expense-claim-draft', title: '新建报销草稿', tool: 'database.expense_claims.save_or_submit', detail: '正在根据当前票据新建报销草稿...' } } const defaultConfigBySessionType = { application: { key: 'application-review-preview', title: '申请信息核对', tool: 'user_agent.application_review_preview', detail: '正在整理申请事项和待补充信息...' }, approval: { key: 'approval-review-preview', title: '审核信息核对', tool: 'user_agent.approval_review_preview', detail: '正在整理待审核单据、风险点和审核建议...' } } const config = configs[reviewAction] || defaultConfigBySessionType[String(activeSessionType.value || '').trim()] || { key: 'expense-review-preview', title: '报销信息核对', tool: 'user_agent.expense_review_preview', detail: attachmentCount ? '正在根据 OCR 结果整理核对信息...' : '正在整理识别结果和右侧核对信息...' } startFlowStep(config.key, { title: config.title, tool: config.tool, detail: config.detail }) } function isApplicationSessionActive() { return String(activeSessionType?.value || '').trim() === 'application' } function isSubmittedApplicationPayload(payload) { const result = payload?.result && typeof payload.result === 'object' ? payload.result : {} const draftPayload = result.draft_payload && typeof result.draft_payload === 'object' ? result.draft_payload : payload?.draft_payload && typeof payload.draft_payload === 'object' ? payload.draft_payload : null return Boolean( draftPayload && String(draftPayload.draft_type || '').trim() === 'expense_application' && String(draftPayload.status || '').trim() === 'submitted' ) } function buildApplicationSubmitSuccessDetail(payload) { const result = payload?.result && typeof payload.result === 'object' ? payload.result : {} const draftPayload = result.draft_payload && typeof result.draft_payload === 'object' ? result.draft_payload : {} const claimNo = String(draftPayload.claim_no || '').trim() const approvalStage = String(draftPayload.approval_stage || '').trim() || '直属领导审批' return claimNo ? `申请单 ${claimNo} 已提交成功,当前节点:${approvalStage}` : `申请单提交成功,当前节点:${approvalStage}` } function shouldHideToolCall(toolCall) { const toolType = String(toolCall?.tool_type || '').toLowerCase() const toolName = String(toolCall?.tool_name || '').toLowerCase() return ( toolName.includes('semantic_ontology') || toolName.includes('ontology.') || toolType.includes('semantic_ontology') || toolType.includes('ontology') ) } function resolveToolCallFlowMeta(toolCall, index) { if (shouldHideToolCall(toolCall)) { return null } const toolType = String(toolCall?.tool_type || '').toLowerCase() const toolName = String(toolCall?.tool_name || '').toLowerCase() const response = toolCall?.response_json && typeof toolCall.response_json === 'object' ? toolCall.response_json : {} const responseMessage = String(response.message || '').trim() const key = `tool-${toolCall?.id || `${index}-${toolType}-${toolName}`}` if ( isApplicationSessionActive() && ( String(response.status || '').trim() === 'submitted' || String(response?.draft_payload?.status || '').trim() === 'submitted' ) ) { return { key: 'application-submit-success', title: '申请单提交成功', tool: 'ApplicationSubmit' } } if (toolType.includes('rule')) { return { key, title: '规则引擎校验', tool: toolCall?.tool_name || 'RuleEngine' } } if (toolType.includes('mcp')) { return toolName.includes('standard') ? { key, title: '差旅补助标准查询', tool: 'TravelStandard' } : null } if (toolName.includes('knowledge')) { return { key, title: '知识库检索', tool: toolCall?.tool_name || 'KnowledgeSearch' } } if (toolName.includes('application_review_preview')) { return { key: 'application-review-preview', title: '申请信息核对', tool: 'ApplicationReview' } } if (toolName.includes('expense_review_preview') || response.preview_only) { return { key: 'expense-review-preview', title: '报销信息核对', tool: 'ExpenseReview' } } if (toolName.includes('expense_claim') || toolName.includes('save_or_submit')) { if ( response.submission_blocked || String(response.status || '').trim() === 'submitted' || responseMessage.includes('AI预审') || responseMessage.includes('审批') ) { return { key: 'pre-submit-review', title: 'AI预审与风险识别', tool: 'ExpenseClaimService.submit_claim' } } if (responseMessage.includes('关联')) { return { key: 'attachment-association', title: '票据关联草稿', tool: toolCall?.tool_name || 'ExpenseClaimService' } } if (responseMessage.includes('新建')) { return { key: 'expense-claim-draft', title: '新建报销草稿', tool: toolCall?.tool_name || 'ExpenseClaimService' } } return { key: 'expense-claim-draft', title: '保存报销草稿', tool: toolCall?.tool_name || 'ExpenseClaimService' } } return null } function summarizeFlowToolCall(toolCall) { const response = toolCall?.response_json && typeof toolCall.response_json === 'object' ? toolCall.response_json : {} const toolName = String(toolCall?.tool_name || '').toLowerCase() if (toolName.includes('application_review_preview')) { return '已整理申请核对信息' } if (toolName.includes('expense_review_preview') || response.preview_only) { return '已整理报销核对信息' } if (String(response.status || '').trim() === 'submitted') { return isApplicationSessionActive() ? '申请单提交成功' : `已完成财务规则、风险规则和审批路径校验,提交至${response.approval_stage || '审批环节'}` } if (response.submission_blocked) { return summarizeVisibleToolText(response.message) || 'AI预审发现待补充项,暂未提交审批' } return ( summarizeVisibleToolText(response.message || response.summary || response.result_summary) || String(toolCall?.tool_name || '').trim() || '工具调用完成' ) } function mergeFlowRunDetail(run) { const runStartedAt = resolveStartedTimestamp(run) const runFinishedAt = resolveFinishedTimestamp(run) if (runStartedAt) { flowStartedAt.value = runStartedAt } const toolCalls = Array.isArray(run?.tool_calls) ? run.tool_calls : [] if (run?.semantic_parse && flowSteps.value.some((step) => step.key === 'intent')) { clearFlowSimulationTimers() const semanticDurations = resolveSemanticPhaseDurations(run) completePendingFlowStep( 'intent', summarizeSemanticIntentDetail(run.semantic_parse, { scenarioLabels: SCENARIO_LABELS, intentLabels: INTENT_LABELS, expenseTypeLabels: EXPENSE_TYPE_LABELS, fallbackText: FLOW_STEP_FALLBACKS.intent.completedText }), semanticDurations.intentMs, { refreshCompleted: true } ) completePendingFlowStep( 'extraction', summarizeSemanticParseDetail(run.semantic_parse, run?.ontology_json || {}), semanticDurations.extractionMs, { refreshCompleted: true } ) } const hasApplicationSubmitSuccess = flowSteps.value.some((step) => step.key === 'application-submit-success') toolCalls.forEach((toolCall, index) => { const meta = resolveToolCallFlowMeta(toolCall, index) if (!meta) { return } if (hasApplicationSubmitSuccess && isApplicationSessionActive() && meta.key !== 'application-submit-success') { return } const failed = String(toolCall?.status || '').toLowerCase() === 'failed' if (failed) { failFlowStep(meta.key, toolCall?.error_message || summarizeFlowToolCall(toolCall), toolCall?.error_message || '', meta) } else { const toolDurationMs = resolveToolCallDurationMs(toolCall, index, toolCalls, run) completePendingFlowStep( meta.key, summarizeFlowToolCall(toolCall), toolDurationMs, { ...meta, refreshCompleted: true } ) } }) if (String(run?.status || '').toLowerCase() === 'failed') { failCurrentFlowStep({ message: run?.error_message || '智能体调用失败' }) return } if ( runFinishedAt && flowSteps.value.length && flowSteps.value.every((step) => [FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)) ) { flowFinishedAt.value = runFinishedAt } } function completeFlowResult(payload, run = null) { const answer = String(payload?.result?.answer || payload?.result?.message || '').trim() if (!answer && !payload?.result) { return } const sceneSelectionPending = isExpenseSceneSelectionResult(payload) flowSteps.value .filter((step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)) .filter((step) => !step.deferredCompletion) .forEach((step) => { const detail = sceneSelectionPending && step.key === 'expense-scene-selection' ? '已暂停后续识别,请先在主对话中选择报销场景。' : resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED }) completeFlowStep(step.key, detail) }) if (isSubmittedApplicationPayload(payload)) { completePendingFlowStep( 'application-submit-success', buildApplicationSubmitSuccessDetail(payload), null, { title: '申请单提交成功', tool: 'ApplicationSubmit' } ) } const runFinishedAt = resolveFinishedTimestamp(run) flowFinishedAt.value = flowSteps.value.some( (step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status) ) ? 0 : runFinishedAt || Date.now() } async function refreshFlowRunDetail() { if (!flowRunId.value || flowRefreshBusy.value) { return null } flowRefreshBusy.value = true try { const run = await fetchAgentRunDetail(flowRunId.value) mergeFlowRunDetail(run) return run } catch (error) { console.warn('Failed to refresh agent run detail:', error) return null } finally { flowRefreshBusy.value = false } } function formatFlowStepDuration(step) { if (step?.status === FLOW_STEP_STATUS_RUNNING && step.startedAt) { return formatFlowDuration(flowTick.value - step.startedAt) } return formatFlowDuration(step?.durationMs) } function resolveFlowStepStatusLabel(step) { const status = String(step?.status || '').trim() if (status === FLOW_STEP_STATUS_COMPLETED) { return '完成' } if (status === FLOW_STEP_STATUS_RUNNING) { return '执行中' } if (status === FLOW_STEP_STATUS_FAILED) { return '异常' } return '待执行' } function resolveFlowStepDetail(step) { const detail = String(step?.detail || '').trim() if (detail) { return detail } const definition = findFlowDefinition(step?.key) if (step?.status === FLOW_STEP_STATUS_COMPLETED) { return definition?.completedText || '步骤已完成' } if (step?.status === FLOW_STEP_STATUS_RUNNING) { return definition?.runningText || '正在执行当前步骤...' } if (step?.status === FLOW_STEP_STATUS_FAILED) { return step?.error || '步骤执行异常' } return definition?.runningText ? `等待${definition.title || '当前步骤'}...` : '等待智能体调度...' } return { flowRunId, flowSessionType, flowStartedAt, flowFinishedAt, flowSteps, activeFlowSteps, visibleFlowSteps, flowRefreshBusy, flowTick, completedFlowStepCount, runningFlowStep, flowOverallStatusTone, flowOverallStatusText, flowTotalDurationText, clearFlowSimulationTimers, resetFlowRun, findFlowDefinition, normalizeFlowStepPatch, createFlowStep, normalizeFlowStepIndexes, upsertFlowStep, startFlowTick, stopFlowRuntime, startFlowStep, completeFlowStep, failFlowStep, completePendingFlowStep, failCurrentFlowStep, startSemanticFlowPreview, startExpenseSceneSelectionFlowPreview, startExpenseIntentConfirmationFlowPreview, startExpenseSceneSelectionAfterIntentConfirmation, isExpenseSceneSelectionResult, startReviewActionFlowStep, startExpenseClaimDraftFlowStep, resolveToolCallFlowMeta, summarizeFlowToolCall, mergeFlowRunDetail, completeFlowResult, refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail } }