feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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) {
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return '--'
|
||||
}
|
||||
if (numericValue < 1000) {
|
||||
@@ -15,18 +18,122 @@ function formatFlowDuration(ms) {
|
||||
}
|
||||
|
||||
function parseFlowTimestamp(value) {
|
||||
const timestamp = new Date(value || '').getTime()
|
||||
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 = parseFlowTimestamp(run?.started_at)
|
||||
const runStart = resolveStartedTimestamp(run)
|
||||
const toolCalls = Array.isArray(run?.tool_calls) ? run.tool_calls : []
|
||||
const firstToolStartedAt = toolCalls
|
||||
.map((item) => parseFlowTimestamp(item?.created_at))
|
||||
.map((item) => resolveStartedTimestamp(item))
|
||||
.filter((value) => value > 0)
|
||||
.sort((left, right) => left - right)[0] || 0
|
||||
const runFinishedAt = parseFlowTimestamp(run?.finished_at)
|
||||
const runFinishedAt = resolveFinishedTimestamp(run)
|
||||
const semanticFinishedAt = firstToolStartedAt || runFinishedAt
|
||||
|
||||
if (!runStart || !semanticFinishedAt || semanticFinishedAt <= runStart) {
|
||||
@@ -43,18 +150,24 @@ function resolveSemanticPhaseDurations(run) {
|
||||
}
|
||||
|
||||
function resolveToolCallDurationMs(toolCall, index, toolCalls, run) {
|
||||
const explicitDuration = Number(toolCall?.duration_ms)
|
||||
if (Number.isFinite(explicitDuration) && explicitDuration > 0) {
|
||||
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 = parseFlowTimestamp(toolCall?.created_at)
|
||||
const startedAt = resolveStartedTimestamp(toolCall)
|
||||
if (!startedAt) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextStartedAt = parseFlowTimestamp(toolCalls[index + 1]?.created_at)
|
||||
const runFinishedAt = parseFlowTimestamp(run?.finished_at)
|
||||
const nextStartedAt = resolveStartedTimestamp(toolCalls[index + 1])
|
||||
const runFinishedAt = resolveFinishedTimestamp(run)
|
||||
const finishedAt = nextStartedAt > startedAt ? nextStartedAt : (runFinishedAt > startedAt ? runFinishedAt : 0)
|
||||
|
||||
if (!finishedAt || finishedAt <= startedAt) {
|
||||
@@ -64,6 +177,19 @@ function resolveToolCallDurationMs(toolCall, index, toolCalls, run) {
|
||||
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,
|
||||
@@ -238,7 +364,8 @@ export function useTravelReimbursementFlow({
|
||||
startedAt: normalizedPatch.startedAt || 0,
|
||||
finishedAt: normalizedPatch.finishedAt || 0,
|
||||
error: normalizedPatch.error || '',
|
||||
deferredCompletion: Boolean(normalizedPatch.deferredCompletion)
|
||||
deferredCompletion: Boolean(normalizedPatch.deferredCompletion),
|
||||
syntheticTiming: Boolean(normalizedPatch.syntheticTiming)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +403,8 @@ export function useTravelReimbursementFlow({
|
||||
startedAt,
|
||||
finishedAt: 0,
|
||||
durationMs: null,
|
||||
error: ''
|
||||
error: '',
|
||||
syntheticTiming: Boolean(normalizedPatch.syntheticTiming)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -286,16 +414,22 @@ export function useTravelReimbursementFlow({
|
||||
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)
|
||||
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: hasExplicitDuration ? explicitDuration : Math.max(0, now - startedAt),
|
||||
durationMs: measuredDuration,
|
||||
error: '',
|
||||
deferredCompletion: false
|
||||
deferredCompletion: false,
|
||||
syntheticTiming: false
|
||||
})
|
||||
if (
|
||||
flowSteps.value.length
|
||||
@@ -323,15 +457,21 @@ export function useTravelReimbursementFlow({
|
||||
}
|
||||
|
||||
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)
|
||||
if (currentStep?.status === FLOW_STEP_STATUS_COMPLETED) {
|
||||
return
|
||||
}
|
||||
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, { ...patch, deferredCompletion: true })
|
||||
startFlowStep(key, { ...patchObject, deferredCompletion: true, syntheticTiming: !hasMeasuredDuration })
|
||||
const completionTimer = window.setTimeout(() => {
|
||||
completeFlowStep(
|
||||
key,
|
||||
@@ -343,7 +483,7 @@ export function useTravelReimbursementFlow({
|
||||
flowSimulationTimers.push(completionTimer)
|
||||
return
|
||||
}
|
||||
completeFlowStep(key, detail, hasMeasuredDuration ? normalizedDuration : null, patch)
|
||||
completeFlowStep(key, detail, hasMeasuredDuration ? normalizedDuration : null, patchObject)
|
||||
}
|
||||
|
||||
function failCurrentFlowStep(error) {
|
||||
@@ -527,7 +667,51 @@ export function useTravelReimbursementFlow({
|
||||
})
|
||||
}
|
||||
|
||||
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'
|
||||
@@ -535,17 +719,31 @@ export function useTravelReimbursementFlow({
|
||||
: {}
|
||||
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 { key, title: toolName.includes('standard') ? '差旅补助标准查询' : 'MCP 服务调用', tool: toolCall?.tool_name || 'MCPService' }
|
||||
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: toolCall?.tool_name || 'user_agent.expense_review_preview' }
|
||||
return { key: 'expense-review-preview', title: '报销信息核对', tool: 'ExpenseReview' }
|
||||
}
|
||||
if (toolName.includes('expense_claim') || toolName.includes('save_or_submit')) {
|
||||
if (
|
||||
@@ -564,39 +762,45 @@ export function useTravelReimbursementFlow({
|
||||
}
|
||||
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' }
|
||||
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 `已完成财务规则、风险规则和审批路径校验,提交至${response.approval_stage || '审批环节'}`
|
||||
return isApplicationSessionActive()
|
||||
? '申请单提交成功'
|
||||
: `已完成财务规则、风险规则和审批路径校验,提交至${response.approval_stage || '审批环节'}`
|
||||
}
|
||||
if (response.submission_blocked) {
|
||||
return String(response.message || '').trim() || 'AI预审发现待补充项,暂未提交审批'
|
||||
return summarizeVisibleToolText(response.message) || 'AI预审发现待补充项,暂未提交审批'
|
||||
}
|
||||
return (
|
||||
String(response.message || response.summary || response.result_summary || '').trim()
|
||||
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)
|
||||
const intentStep = flowSteps.value.find((step) => step.key === 'intent')
|
||||
const extractionStep = flowSteps.value.find((step) => step.key === 'extraction')
|
||||
completePendingFlowStep(
|
||||
'intent',
|
||||
summarizeSemanticIntentDetail(run.semantic_parse, {
|
||||
@@ -605,17 +809,26 @@ export function useTravelReimbursementFlow({
|
||||
expenseTypeLabels: EXPENSE_TYPE_LABELS,
|
||||
fallbackText: FLOW_STEP_FALLBACKS.intent.completedText
|
||||
}),
|
||||
intentStep?.startedAt ? null : semanticDurations.intentMs
|
||||
semanticDurations.intentMs,
|
||||
{ refreshCompleted: true }
|
||||
)
|
||||
completePendingFlowStep(
|
||||
'extraction',
|
||||
summarizeSemanticParseDetail(run.semantic_parse, run?.ontology_json || {}),
|
||||
extractionStep?.startedAt ? null : semanticDurations.extractionMs
|
||||
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)
|
||||
@@ -625,7 +838,7 @@ export function useTravelReimbursementFlow({
|
||||
meta.key,
|
||||
summarizeFlowToolCall(toolCall),
|
||||
toolDurationMs,
|
||||
meta
|
||||
{ ...meta, refreshCompleted: true }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -634,6 +847,13 @@ export function useTravelReimbursementFlow({
|
||||
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) {
|
||||
@@ -651,11 +871,20 @@ export function useTravelReimbursementFlow({
|
||||
: 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
|
||||
: Date.now()
|
||||
: runFinishedAt || Date.now()
|
||||
}
|
||||
|
||||
async function refreshFlowRunDetail() {
|
||||
|
||||
Reference in New Issue
Block a user