export const FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS = 3000 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'] export 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 } 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 } export function resolveStartedTimestamp(source) { return readFirstTimestampField(source, FLOW_STARTED_AT_FIELDS) } export 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 } export 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 } } export 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 } export 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 }