Files
X-Financial/web/src/views/scripts/useTravelReimbursementFlow.js
caoxiaozhu b1a9c8a194 fix: 优化报销创建页面样式与洞察面板交互
修复侧边栏和审计视图样式细节,完善差旅报销洞察面板和消息
组件布局,优化报销创建页面会话管理和流程状态持久化,增强
申请预览工具函数和导航图标,补充单元测试。
2026-05-27 10:32:08 +08:00

740 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 completedFlowStepCount = computed(
() => flowSteps.value.filter((step) => step.status === FLOW_STEP_STATUS_COMPLETED).length
)
const runningFlowStep = computed(
() => flowSteps.value.find((step) => step.status === FLOW_STEP_STATUS_RUNNING) || null
)
const visibleFlowSteps = computed(() => {
const visibleSteps = []
for (const step of flowSteps.value) {
visibleSteps.push(step)
if (step.status !== FLOW_STEP_STATUS_COMPLETED) {
break
}
}
return visibleSteps
})
const flowOverallStatusTone = computed(() => {
if (flowSteps.value.some((step) => step.status === FLOW_STEP_STATUS_FAILED)) {
return 'failed'
}
if (runningFlowStep.value) {
return 'running'
}
if (flowSteps.value.length && completedFlowStepCount.value === flowSteps.value.length && flowStartedAt.value) {
return 'completed'
}
return 'pending'
})
const flowOverallStatusText = computed(() => {
const total = flowSteps.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 = flowSteps.value.reduce((total, step) => {
const duration = Number(step.durationMs)
return total + (Number.isFinite(duration) && duration > 0 ? duration : 0)
}, 0)
return measuredDuration ? formatFlowDuration(measuredDuration) : '--'
})
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 = ''
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 = {}) {
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 = runningFlowStep.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,
flowStartedAt,
flowFinishedAt,
flowSteps,
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
}
}