import type { ProcessArtifact, ProcessEvent, ProcessRun, ProcessRunStatus, Session, } from '@/types'; import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core'; const TERMINAL_STATUSES = new Set(['done', 'error', 'cancelled']); const STALE_WAITING_MS = 2 * 60 * 1000; export type TaskRuntimeStatus = ProcessRunStatus | 'blocked'; export interface TaskRuntimeProgressView { label: string; value: number | null; max: number | null; } export interface TaskRuntimeStatsView { totalRuns: number; activeRuns: number; artifactCount: number; alertCount: number; } export interface TaskRuntimeNodeView { taskId: string; runId: string; actorName: string; title: string; status: TaskRuntimeStatus; stageLabel: string | null; summary: string | null; updatedAt: string; childTaskIds: string[]; isRoot: boolean; } export interface TaskRuntimeView { taskId: string; sessionId: string | null; title: string; status: TaskRuntimeStatus; createdAt: string; updatedAt: string; finishedAt: string | null; durationMs: number | null; sourceSessionLabel: string; rootRunId: string; rootActorName: string; progress: TaskRuntimeProgressView; stats: TaskRuntimeStatsView; tasks: TaskRuntimeNodeView[]; } type BuildTaskRuntimeInput = { sessions: Session[]; processRuns: ProcessRun[]; processEvents: ProcessEvent[]; processArtifacts: ProcessArtifact[]; }; function toTime(value?: string | null): number | null { if (!value) return null; const parsed = new Date(value).getTime(); return Number.isFinite(parsed) ? parsed : null; } function compareIsoDesc(a?: string | null, b?: string | null): number { return (toTime(b) ?? 0) - (toTime(a) ?? 0); } function firstString(value: unknown): string | null { return typeof value === 'string' && value.trim() ? value.trim() : null; } function firstNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null; } function readMetadataString(metadata: Record | undefined, keys: string[]): string | null { for (const key of keys) { const value = firstString(metadata?.[key]); if (value) return value; } return null; } function readMetadataNumber(metadata: Record | undefined, keys: string[]): number | null { for (const key of keys) { const value = firstNumber(metadata?.[key]); if (value !== null) return value; } return null; } function latestTimestamp(values: Array): string | null { let selected: string | null = null; let selectedTime = -1; for (const value of values) { const time = toTime(value); if (time === null || time <= selectedTime) continue; selected = value ?? null; selectedTime = time; } return selected; } function getSessionLabel(sessions: Session[], sessionId: string | null, locale: AppLocale): string { if (!sessionId) return pickAppText(locale, '未关联会话', 'No session linked'); const session = sessions.find((item) => item.key === sessionId); if (!session) return sessionId; return session.path?.trim() || session.key; } function groupByRunId(items: T[]): Map { const map = new Map(); for (const item of items) { const collection = map.get(item.run_id); if (collection) { collection.push(item); continue; } map.set(item.run_id, [item]); } return map; } function buildChildrenMap(processRuns: ProcessRun[]): Map { const map = new Map(); for (const run of processRuns) { if (!run.parent_run_id) continue; const children = map.get(run.parent_run_id); if (children) { children.push(run); continue; } map.set(run.parent_run_id, [run]); } return map; } function collectRunTree(rootRun: ProcessRun, childrenMap: Map): ProcessRun[] { const collected: ProcessRun[] = []; const stack = [rootRun]; const seen = new Set(); while (stack.length > 0) { const current = stack.pop(); if (!current || seen.has(current.run_id)) continue; seen.add(current.run_id); collected.push(current); const children = childrenMap.get(current.run_id) ?? []; for (let index = children.length - 1; index >= 0; index -= 1) { stack.push(children[index]); } } return collected; } function getRunUpdatedAt( run: ProcessRun, eventsByRun: Map, artifactsByRun: Map, ): string { const eventTimes = (eventsByRun.get(run.run_id) ?? []).map((event) => event.created_at); const artifactTimes = (artifactsByRun.get(run.run_id) ?? []).map((artifact) => artifact.created_at); return ( latestTimestamp([ ...eventTimes, ...artifactTimes, run.finished_at, run.started_at, ]) ?? run.started_at ); } function deriveStageLabel( run: ProcessRun, runEvents: ProcessEvent[], fallbackStatus: TaskRuntimeStatus, locale: AppLocale, ): string | null { const runMetadataLabel = readMetadataString(run.metadata, [ 'stage_label', 'stage', 'phase_label', 'step_label', ]); if (runMetadataLabel) return runMetadataLabel; const sortedEvents = [...runEvents].sort((a, b) => compareIsoDesc(a.created_at, b.created_at)); for (const event of sortedEvents) { const label = readMetadataString(event.metadata, [ 'stage_label', 'stage', 'phase_label', 'step_label', ]); if (label) return label; } if (fallbackStatus === 'running') return pickAppText(locale, '执行中', 'Running'); if (fallbackStatus === 'waiting') return pickAppText(locale, '等待中', 'Waiting'); if (fallbackStatus === 'queued') return pickAppText(locale, '排队中', 'Queued'); if (fallbackStatus === 'done') return pickAppText(locale, '已完成', 'Done'); if (fallbackStatus === 'error') return pickAppText(locale, '失败', 'Error'); if (fallbackStatus === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled'); if (fallbackStatus === 'blocked') return pickAppText(locale, '阻塞', 'Blocked'); return null; } function deriveRunStatus( run: ProcessRun, updatedAt: string, now: number, ): TaskRuntimeStatus { if (run.status !== 'waiting') return run.status; const updatedTime = toTime(updatedAt); if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) { return 'blocked'; } return 'waiting'; } function deriveProgress( rootRun: ProcessRun, taskRuns: ProcessRun[], locale: AppLocale, ): TaskRuntimeProgressView { const stageValue = readMetadataNumber(rootRun.metadata, ['stage_index', 'step_index', 'phase_index']); const stageMax = readMetadataNumber(rootRun.metadata, ['stage_total', 'step_total', 'phase_total']); if (stageValue !== null && stageMax !== null && stageMax > 0) { return { label: pickAppText( locale, `阶段 ${Math.min(stageValue, stageMax)} / ${stageMax}`, `Stage ${Math.min(stageValue, stageMax)} / ${stageMax}` ), value: stageValue, max: stageMax, }; } const doneRuns = taskRuns.filter((run) => run.status === 'done').length; if (taskRuns.length > 0) { return { label: pickAppText( locale, `已完成子任务 ${doneRuns} / ${taskRuns.length}`, `Subtasks completed ${doneRuns} / ${taskRuns.length}` ), value: doneRuns, max: taskRuns.length, }; } return { label: pickAppText(locale, '等待任务数据', 'Waiting for task data'), value: null, max: null, }; } function countAlerts(taskViews: TaskRuntimeNodeView[]): number { return taskViews.filter((task) => task.status === 'error' || task.status === 'blocked').length; } export function isTaskRuntimeTerminal(status: TaskRuntimeStatus): boolean { return TERMINAL_STATUSES.has(status); } export function taskRuntimeStatusLabel(status: TaskRuntimeStatus, locale: AppLocale = getCurrentAppLocale()): string { if (status === 'queued') return pickAppText(locale, '排队中', 'Queued'); if (status === 'running') return pickAppText(locale, '进行中', 'In Progress'); if (status === 'waiting') return pickAppText(locale, '等待中', 'Waiting'); if (status === 'blocked') return pickAppText(locale, '阻塞', 'Blocked'); if (status === 'done') return pickAppText(locale, '已完成', 'Done'); if (status === 'error') return pickAppText(locale, '失败', 'Error'); return pickAppText(locale, '已取消', 'Cancelled'); } export function buildTaskRuntimeView( taskId: string, input: BuildTaskRuntimeInput, locale: AppLocale = getCurrentAppLocale(), ): TaskRuntimeView | null { const { sessions, processRuns, processEvents, processArtifacts } = input; const runById = new Map(processRuns.map((run) => [run.run_id, run])); const rootRun = runById.get(taskId); if (!rootRun) return null; const childrenMap = buildChildrenMap(processRuns); const taskRuns = collectRunTree(rootRun, childrenMap); const taskRunIds = new Set(taskRuns.map((run) => run.run_id)); const taskEvents = processEvents.filter((event) => taskRunIds.has(event.run_id)); const taskArtifacts = processArtifacts.filter((artifact) => taskRunIds.has(artifact.run_id)); const eventsByRun = groupByRunId(taskEvents); const artifactsByRun = groupByRunId(taskArtifacts); const now = Date.now(); const taskViews: TaskRuntimeNodeView[] = taskRuns .map((run) => { const runEvents = eventsByRun.get(run.run_id) ?? []; const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun); const status = deriveRunStatus(run, updatedAt, now); const stageLabel = deriveStageLabel(run, runEvents, status, locale); const childTaskIds = (childrenMap.get(run.run_id) ?? []) .filter((child) => taskRunIds.has(child.run_id)) .map((child) => child.run_id); return { taskId: run.run_id, runId: run.run_id, actorName: run.actor_name, title: run.title, status, stageLabel, summary: firstString(run.summary), updatedAt, childTaskIds, isRoot: run.run_id === rootRun.run_id, }; }) .sort((a, b) => { if (a.isRoot !== b.isRoot) return a.isRoot ? -1 : 1; if (isTaskRuntimeTerminal(a.status) !== isTaskRuntimeTerminal(b.status)) { return isTaskRuntimeTerminal(a.status) ? 1 : -1; } return compareIsoDesc(a.updatedAt, b.updatedAt); }); const sessionId = rootRun.session_id ?? taskRuns.find((run) => run.session_id)?.session_id ?? null; const updatedAt = latestTimestamp([ ...taskViews.map((task) => task.updatedAt), rootRun.finished_at, rootRun.started_at, ]) ?? rootRun.started_at; const derivedRootStatus = deriveRunStatus(rootRun, updatedAt, now); const alertCount = countAlerts(taskViews); const progress = deriveProgress(rootRun, taskRuns, locale); const sourceSessionLabel = getSessionLabel(sessions, sessionId, locale); const createdAt = rootRun.started_at; const finishedAt = rootRun.finished_at ?? null; const durationStart = toTime(createdAt); const durationEnd = toTime(finishedAt ?? updatedAt); const durationMs = durationStart !== null && durationEnd !== null && durationEnd >= durationStart ? durationEnd - durationStart : null; return { taskId: rootRun.run_id, sessionId, title: rootRun.title || pickAppText(locale, `任务 ${rootRun.run_id.slice(0, 8)}`, `Task ${rootRun.run_id.slice(0, 8)}`), status: derivedRootStatus, createdAt, updatedAt, finishedAt, durationMs, sourceSessionLabel, rootRunId: rootRun.run_id, rootActorName: rootRun.actor_name, progress, stats: { totalRuns: taskRuns.length, activeRuns: taskViews.filter((task) => !isTaskRuntimeTerminal(task.status)).length, artifactCount: taskArtifacts.length, alertCount, }, tasks: taskViews, }; }