function normalizeNumber(value, fallback) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : fallback; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function formatElapsedSeconds(seconds) { const safeSeconds = Math.max(0, Math.floor(seconds)); if (safeSeconds < 60) { return `${safeSeconds} 秒`; } const minutes = Math.floor(safeSeconds / 60); const remainingSeconds = safeSeconds % 60; return remainingSeconds > 0 ? `${minutes} 分 ${remainingSeconds} 秒` : `${minutes} 分钟`; } function normalizeStepStatus(value, fallback = "pending") { return value === "done" || value === "running" || value === "failed" || value === "pending" ? value : fallback; } function normalizeSteps(steps) { if (!Array.isArray(steps)) { return []; } return steps .map((step, index) => { const text = typeof step?.text === "string" ? step.text.trim() : ""; if (!text) { return null; } return { id: typeof step?.id === "string" && step.id.trim() ? step.id.trim() : `step-${index + 1}`, text, status: normalizeStepStatus(step?.status), }; }) .filter(Boolean) .slice(0, 10); } function buildDefaultLongRunningSteps(elapsedSeconds) { const elapsedText = formatElapsedSeconds(elapsedSeconds); return [ { id: "receive-task", text: "接收对话任务", status: "done" }, { id: "locate-thread", text: "定位目标 Codex 线程", status: "done" }, { id: "write-desktop-thread", text: "写入 Codex 桌面线程记录", status: "done" }, { id: "await-thread-reply", text: `等待目标线程回复,已等待 ${elapsedText}`, status: "running" }, { id: "write-back-boss", text: "回写 Boss 对话窗口", status: "pending" }, ]; } export function normalizeLongRunningProgressIntervalMs(value) { const numeric = normalizeNumber(value, 20_000); if (numeric <= 0) { return 0; } return clamp(Math.floor(numeric), 5_000, 60_000); } export function buildLongRunningCodexProgressSnapshot({ task = {}, startedAtMs, nowMs = Date.now(), phase = "awaiting_reply", baseProgress, heartbeatCount = 0, } = {}) { const started = normalizeNumber(startedAtMs, nowMs); const elapsedSeconds = Math.max(0, Math.round((nowMs - started) / 1000)); const liveSteps = normalizeSteps(baseProgress?.steps); const steps = liveSteps.length > 0 ? liveSteps : buildDefaultLongRunningSteps(elapsedSeconds); const warnings = Array.isArray(baseProgress?.warnings) ? baseProgress.warnings.filter(Boolean).slice(0, 8) : []; if (!warnings.some((warning) => warning?.id === "codex-turn-long-running")) { warnings.unshift({ id: "codex-turn-long-running", severity: "info", message: `Codex 桌面线程仍在执行,已等待 ${formatElapsedSeconds(elapsedSeconds)}。`, }); } return { ...(baseProgress && typeof baseProgress === "object" ? baseProgress : {}), phase, status: "running", steps, warnings, longRunning: { taskId: typeof task?.taskId === "string" ? task.taskId : undefined, targetThreadDisplayName: typeof task?.targetThreadDisplayName === "string" ? task.targetThreadDisplayName : undefined, elapsedSeconds, heartbeatCount: Math.max(0, Math.floor(normalizeNumber(heartbeatCount, 0))), }, }; }