import { computed, ref } from 'vue' function formatFlowDuration(ms) { 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) { const timestamp = new Date(value || '').getTime() return Number.isFinite(timestamp) ? timestamp : 0 } function resolveSemanticPhaseDurations(run) { const runStart = parseFlowTimestamp(run?.started_at) const toolCalls = Array.isArray(run?.tool_calls) ? run.tool_calls : [] const firstToolStartedAt = toolCalls .map((item) => parseFlowTimestamp(item?.created_at)) .filter((value) => value > 0) .sort((left, right) => left - right)[0] || 0 const runFinishedAt = parseFlowTimestamp(run?.finished_at) 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 explicitDuration = Number(toolCall?.duration_ms) if (Number.isFinite(explicitDuration) && explicitDuration > 0) { return explicitDuration } const startedAt = parseFlowTimestamp(toolCall?.created_at) if (!startedAt) { return null } const nextStartedAt = parseFlowTimestamp(toolCalls[index + 1]?.created_at) const runFinishedAt = parseFlowTimestamp(run?.finished_at) const finishedAt = nextStartedAt > startedAt ? nextStartedAt : (runFinishedAt > startedAt ? runFinishedAt : 0) if (!finishedAt || finishedAt <= startedAt) { return null } return finishedAt - startedAt } 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) } } 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: '' }) } 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) : now) upsertFlowStep(key, { ...patch, status: FLOW_STEP_STATUS_COMPLETED, detail: detail || definition?.completedText || '', startedAt, finishedAt: now, durationMs: hasExplicitDuration ? explicitDuration : Math.max(0, now - startedAt), error: '', deferredCompletion: 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 currentStep = flowSteps.value.find((step) => step.key === key) if (currentStep?.status === FLOW_STEP_STATUS_COMPLETED) { return } const normalizedDuration = Number(durationMs) const hasMeasuredDuration = Number.isFinite(normalizedDuration) && normalizedDuration > 0 if (!currentStep || currentStep.status === FLOW_STEP_STATUS_PENDING) { const revealOrder = flowSteps.value.length startFlowStep(key, { ...patch, deferredCompletion: true }) 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, patch) } 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 resolveToolCallFlowMeta(toolCall, index) { 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 (toolType.includes('rule')) { return { key, title: '规则引擎校验', tool: toolCall?.tool_name || 'RuleEngine' } } if (toolType.includes('mcp')) { return { key, title: toolName.includes('standard') ? '差旅补助标准查询' : 'MCP 服务调用', tool: toolCall?.tool_name || 'MCPService' } } if (toolName.includes('knowledge')) { return { key, title: '知识库检索', tool: toolCall?.tool_name || 'KnowledgeSearch' } } if (toolName.includes('expense_review_preview') || response.preview_only) { return { key: 'expense-review-preview', title: '报销信息核对', tool: toolCall?.tool_name || 'user_agent.expense_review_preview' } } 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' } } if (toolType.includes('database')) { return { key, title: '数据查询/字段处理', tool: toolCall?.tool_name || 'DatabaseTool' } } if (toolType.includes('llm') || toolName.includes('user_agent')) { return { key, title: '智能体生成', tool: toolCall?.tool_name || 'UserAgent' } } return { key, title: '智能体工具调用', tool: toolCall?.tool_name || toolCall?.tool_type || 'AgentTool' } } function summarizeFlowToolCall(toolCall) { const response = toolCall?.response_json && typeof toolCall.response_json === 'object' ? toolCall.response_json : {} if (String(response.status || '').trim() === 'submitted') { return `已完成财务规则、风险规则和审批路径校验,提交至${response.approval_stage || '审批环节'}` } if (response.submission_blocked) { return String(response.message || '').trim() || 'AI预审发现待补充项,暂未提交审批' } return ( String(response.message || response.summary || response.result_summary || '').trim() || String(toolCall?.tool_name || '').trim() || '工具调用完成' ) } function mergeFlowRunDetail(run) { 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) const intentStep = flowSteps.value.find((step) => step.key === 'intent') const extractionStep = flowSteps.value.find((step) => step.key === 'extraction') completePendingFlowStep( 'intent', summarizeSemanticIntentDetail(run.semantic_parse, { scenarioLabels: SCENARIO_LABELS, intentLabels: INTENT_LABELS, expenseTypeLabels: EXPENSE_TYPE_LABELS, fallbackText: FLOW_STEP_FALLBACKS.intent.completedText }), intentStep?.startedAt ? null : semanticDurations.intentMs ) completePendingFlowStep( 'extraction', summarizeSemanticParseDetail(run.semantic_parse, run?.ontology_json || {}), extractionStep?.startedAt ? null : semanticDurations.extractionMs ) } toolCalls.forEach((toolCall, index) => { const meta = resolveToolCallFlowMeta(toolCall, index) 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 ) } }) if (String(run?.status || '').toLowerCase() === 'failed') { failCurrentFlowStep({ message: run?.error_message || '智能体调用失败' }) return } } 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) }) flowFinishedAt.value = flowSteps.value.some( (step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status) ) ? 0 : 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 } }