fix: make codex queue storage pg authoritative

This commit is contained in:
Codex
2026-05-11 14:54:32 +00:00
parent 688376abc4
commit ae462ed9ef
21 changed files with 1397 additions and 578 deletions
File diff suppressed because one or more lines are too long
+1 -56
View File
@@ -1767,8 +1767,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
min-height: 0;
max-height: calc(100vh - 318px);
}
.codex-task-pagination,
.codex-lazy-detail-callout {
.codex-task-pagination {
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -1782,17 +1781,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
color: var(--muted);
font-size: 12px;
}
.codex-lazy-detail-callout {
margin: 8px 0;
}
.codex-lazy-detail-callout > div {
display: grid;
gap: 2px;
min-width: min(100%, 280px);
}
.codex-lazy-detail-callout strong {
color: var(--text);
}
.codex-task-pagination code {
color: var(--accent-2);
}
@@ -1862,17 +1850,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
}
.codex-task-card.unread-terminal {
padding-right: 23px;
border-color: rgba(215, 161, 58, 0.32);
background:
linear-gradient(135deg, rgba(78, 183, 168, 0.09), #0a1015 42%),
#0a1015;
}
.codex-task-card.unread-terminal.selected,
.codex-task-card.unread-terminal:hover {
border-color: rgba(215, 161, 58, 0.62);
background:
linear-gradient(135deg, rgba(78, 183, 168, 0.18), #0c181c 42%),
#0c181c;
}
.codex-task-card:focus-visible {
outline: 2px solid var(--accent);
@@ -2561,30 +2538,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
.codex-detail-grid {
grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr);
}
.codex-prompt-panel {
grid-column: 1 / -1;
}
.codex-prompt-detail {
display: grid;
gap: 9px;
min-width: 0;
}
.codex-prompt-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
min-width: 0;
}
.codex-prompt-meta span:not(.status-badge) {
max-width: 100%;
padding: 3px 7px;
border: 1px solid var(--line-soft);
color: var(--muted);
background: rgba(255,255,255,0.025);
font-size: 11px;
overflow-wrap: anywhere;
}
.codex-prompt-full {
max-height: 360px;
margin: 0;
@@ -2629,14 +2582,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
flex: 0 0 auto;
color: var(--muted);
}
.codex-prompt-reference-full {
max-height: 460px;
border-color: rgba(78, 183, 168, 0.28);
border-left-color: rgba(78, 183, 168, 0.62);
background:
linear-gradient(90deg, rgba(78, 183, 168, 0.08), transparent 34%),
rgba(6, 10, 13, 0.86);
}
.codex-prompt-final-full {
max-height: 560px;
border-color: rgba(215, 161, 58, 0.34);
+8 -11
View File
@@ -1,4 +1,5 @@
import React from "react";
import { BEIJING_TIME_LABEL, fmtClock, fmtDate } from "./time";
import { createRoot } from "react-dom/client";
import { ClaudeQqPage } from "./claudeqq";
import { CodexQueuePage } from "./codex-queue";
@@ -73,16 +74,6 @@ function taskRawButtonData(task: any): any {
return task?._summaryOnly ? { ...task, _loadRaw: () => loadTaskRawData(task) } : task;
}
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function fmtDuration(seconds: number): string {
if (!Number.isFinite(seconds)) return "--";
@@ -191,6 +182,12 @@ function summarizeValue(value: any): string {
}
function summarizeFieldValue(key: string, value: any): string {
const normalizedKey = key.replace(/[-_\s]/g, "").toLowerCase();
const looksLikeTimeKey = normalizedKey === "ts" || normalizedKey.endsWith("at") || normalizedKey.endsWith("timestamp") || normalizedKey.endsWith("heartbeat");
if ((typeof value === "string" || typeof value === "number") && looksLikeTimeKey) {
const formatted = fmtDate(value);
if (formatted !== "--") return formatted;
}
if (key === "bodyText" && typeof value === "string") {
const kind = /^\s*[{[]/.test(value) ? "JSON" : "HTTP";
return `${kind} body ${value.length} chars`;
@@ -504,7 +501,7 @@ function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock,
{ key: "core", label: "核心", value: connection.text, tone: connection.ok ? "ok" : "fail", testId: "conn-text" },
...(Array.isArray(activeStatusItems) ? activeStatusItems : []),
{ key: "refresh", label: "刷新", value: lastRefresh ? fmtClock(lastRefresh) : "未刷新" },
{ key: "clock", label: "时间", value: fmtClock(clock) },
{ key: "clock", label: BEIJING_TIME_LABEL, value: fmtClock(clock) },
{ key: "user", label: "用户", value: session?.user?.username || "--", tone: "user" },
];
return h("header", { className: "topbar" },
+1 -10
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate } from "./time";
import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -9,16 +10,6 @@ const { useEffect } = React;
const useState: any = React.useState;
const PRIMARY_PRIVATE_CHAT = { label: "主用户私聊账号", userId: 645275593 };
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function numberText(value: any): string {
const number = Number(value);
+202 -159
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate } from "./time";
import { TraceView, codexTracePort } from "./trace";
import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -11,7 +12,6 @@ const useState: any = React.useState;
const codexTranscriptChunkLimit = 120;
const codexInitialTaskLimit = 24;
const codexMoreTaskLimit = 48;
const codexLocalReadRetention = 300;
const queueErrorPreviewLength = 1200;
function isDocumentVisible(): boolean {
@@ -22,16 +22,6 @@ function errorText(error: unknown, fallback = "操作失败"): string {
return errorMessage(error, fallback);
}
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function fmtDuration(ms: any): string {
const value = Number(ms);
@@ -45,6 +35,44 @@ function fmtDuration(ms: any): string {
return `${seconds}s`;
}
function timestampMs(value: any): number | null {
if (value === null || value === undefined || value === "") return null;
const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
return Number.isFinite(ms) ? ms : null;
}
function fmtRelativeAge(value: any, nowMs = Date.now()): string {
const atMs = timestampMs(value);
if (atMs === null) return "--";
const totalSeconds = Math.max(0, Math.floor((nowMs - atMs) / 1000));
if (totalSeconds < 1) return "刚刚";
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days > 0) return `${days}${hours > 0 ? `${hours}小时` : ""}`;
if (hours > 0) return `${hours}小时${minutes > 0 ? `${minutes}分钟` : ""}`;
if (minutes > 0) return `${minutes}分钟${seconds}秒前`;
return `${seconds}秒前`;
}
function latestTimestampValue(...values: any[]): string {
let best = "";
let bestMs = -Infinity;
for (const value of values) {
const text = String(value || "");
if (text.length === 0) continue;
const ms = timestampMs(value);
if (ms !== null && ms >= bestMs) {
best = text;
bestMs = ms;
} else if (best.length === 0) {
best = text;
}
}
return best;
}
function fmtPreciseMs(ms: any): string {
const value = Number(ms);
if (!Number.isFinite(value) || value < 0) return "--";
@@ -174,7 +202,6 @@ function activeTaskIds(queue: any): string[] {
const allQueuesId = "__all__";
const queueMobileMediaQuery = "(max-width: 760px)";
const queueDesktopMediaQuery = "(min-width: 761px)";
const codexReadAtStorageKey = "unidesk:codex-queue:read-at:v1";
function isAllQueues(queueId: string): boolean {
return !queueId || queueId === allQueuesId;
@@ -571,6 +598,25 @@ function attemptExecutionSummary(task: any, attempt: any): AnyRecord {
return objectRecord(attempt?.execution) || taskExecutionSummary(task);
}
function attemptExecutionUpdatedAt(task: any, attempt: any, attemptIndex: any, execution: AnyRecord): string {
const summary = taskTraceSummary(task);
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
const numericAttempt = Number(attemptIndex);
const isCurrentAttempt = Number.isFinite(numericAttempt) && numericAttempt > 0 && numericAttempt === currentAttempt;
const activeTaskUpdatedAt = latestTimestampValue(task?.updatedAt, summary?.updatedAt);
if (isCurrentAttempt && !attempt?.finishedAt && activeTaskUpdatedAt.length > 0) return activeTaskUpdatedAt;
return String(
attempt?.updatedAt
|| attempt?.finishedAt
|| execution.effectiveEndAt
|| (isCurrentAttempt ? activeTaskUpdatedAt : "")
|| activeTaskUpdatedAt
|| task?.finishedAt
|| task?.startedAt
|| "",
);
}
function attemptFinalResponseText(task: any, attempt: any): string {
const text = String(attempt?.finalResponse || attempt?.finalResponsePreview || "");
if (Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponse") || Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponsePreview")) return text.trimEnd();
@@ -632,6 +678,104 @@ function taskTraceSteps(task: any, attemptIndex: any = null): any[] {
return Array.isArray(task?._traceSteps) ? task._traceSteps : [];
}
function traceStepSummaryLines(step: any): string[] {
return (Array.isArray(step?.summaryLines) ? step.summaryLines : []).map((line: any) => String(line || ""));
}
function traceStepFileChangeMethod(step: any): string {
const status = String(step?.status || "").trim();
if (status.length > 0) return status;
const text = traceStepSummaryLines(step).join("\n");
return /^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(text)?.[1] || "";
}
function isFileChangeLifecycleSummaryLine(line: string): boolean {
return /^item\/(?:started|completed): file changes status=/u.test(String(line || "").trim());
}
function fileChangeStatusFromStep(step: any): string {
const lines = traceStepSummaryLines(step);
for (let index = lines.length - 1; index >= 0; index -= 1) {
const status = /file changes status=([A-Za-z0-9_-]+)/u.exec(lines[index] || "")?.[1];
if (status) return status;
}
const method = traceStepFileChangeMethod(step);
if (method === "item/fileChange/outputDelta") return "updated";
if (method === "item/started") return "started";
if (method === "item/completed") return "completed";
return method.replace(/^item\//u, "") || String(step?.status || "changed");
}
function isFileChangeTraceStep(step: any): boolean {
if (String(step?.kind || "") !== "edited") return false;
const title = String(step?.title || "");
const status = String(step?.status || "");
const text = traceStepSummaryLines(step).join("\n");
if (title === "Edited files") return true;
if (/^item\/fileChange\//u.test(status)) return true;
if ((status === "item/started" || status === "item/completed") && /file changes status=/u.test(text)) return true;
if (/^Success\. Updated the following files:/mu.test(text)) return true;
if (/^diff --git /mu.test(text)) return true;
return /^([AMDRCU?]{1,2})\s+\S+/mu.test(text);
}
function mergeFileChangeTraceStepGroup(group: any[]): any {
if (group.length <= 1) return group[0];
const outputStep = group.find((step) => traceStepFileChangeMethod(step) === "item/fileChange/outputDelta") || group.find((step) => traceStepSummaryLines(step).some((line) => !isFileChangeLifecycleSummaryLine(line))) || group.at(-1) || group[0];
const rawSeqs = group.flatMap((step) => Array.isArray(step?.rawSeqs) ? step.rawSeqs : [step?.seq]).filter((seq) => seq !== undefined);
const summaryLines = group
.flatMap(traceStepSummaryLines)
.filter((line) => line.trim().length > 0 && !isFileChangeLifecycleSummaryLine(line));
const finalStep = group[group.length - 1] || outputStep;
return {
...outputStep,
at: outputStep?.at || finalStep?.at,
title: String(outputStep?.title || "Edited files"),
status: fileChangeStatusFromStep(finalStep),
summaryLines: summaryLines.length > 0 ? summaryLines : traceStepSummaryLines(outputStep),
rawSeqs,
};
}
function coalesceFileChangeTraceSteps(steps: any[]): any[] {
const rows = Array.isArray(steps) ? steps : [];
const merged: any[] = [];
let group: any[] = [];
const flush = () => {
if (group.length > 0) merged.push(mergeFileChangeTraceStepGroup(group));
group = [];
};
for (const step of rows) {
if (isFileChangeTraceStep(step)) {
if (traceStepFileChangeMethod(step) === "item/started" && group.length > 0) flush();
group.push(step);
if (traceStepFileChangeMethod(step) === "item/completed") flush();
continue;
}
flush();
merged.push(step);
}
flush();
return merged;
}
function executionSummaryWithDisplayedSteps(execution: AnyRecord, steps: any[]): AnyRecord {
if (steps.length === 0) return execution;
const counts = steps.reduce((memo: AnyRecord, step: any) => {
const kind = String(step?.kind || "");
if (kind === "explored") memo.readCount += 1;
else if (kind === "edited") memo.editCount += 1;
else if (kind === "ran") memo.runCount += 1;
return memo;
}, { readCount: 0, editCount: 0, runCount: 0 });
return {
...execution,
...counts,
toolCallCount: counts.readCount + counts.editCount + counts.runCount,
stepCount: steps.length,
};
}
function taskTraceStepsLoaded(task: any, attemptIndex: any = null): boolean {
if (attemptIndex !== null && attemptIndex !== undefined) {
const byAttempt = objectRecord(task?._traceStepsLoadedByAttempt) || {};
@@ -644,6 +788,20 @@ function taskTraceStepDetails(task: any): AnyRecord {
return task?._traceStepDetails && typeof task._traceStepDetails === "object" && !Array.isArray(task._traceStepDetails) ? task._traceStepDetails : {};
}
function attemptSegmentIndex(attempt: any, position: number): number {
const raw = Number(attempt?.index);
return Number.isFinite(raw) ? raw : position + 1;
}
function isSyntheticAttemptSegment(attempt: any, attemptIndex: any): boolean {
return Boolean(attempt?.synthetic) || Number(attemptIndex) <= 0;
}
function attemptDataIndex(attemptIndex: any): string | undefined {
const value = Number(attemptIndex);
return Number.isFinite(value) ? String(value) : undefined;
}
function taskDurationLabel(task: any): string {
const timing = task?.timing && typeof task.timing === "object" ? task.timing : {};
const status = String(task?.status || "");
@@ -685,46 +843,6 @@ function taskIsUnreadTerminal(task: any): boolean {
return !task?.readAt;
}
function loadLocalReadAt(): AnyRecord {
if (typeof window === "undefined") return {};
try {
const parsed = JSON.parse(window.localStorage.getItem(codexReadAtStorageKey) || "{}");
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? trimLocalReadAt(parsed) : {};
} catch {
return {};
}
}
function trimLocalReadAt(readAtByTask: AnyRecord): AnyRecord {
const entries = Object.entries(readAtByTask || {})
.filter(([, value]) => typeof value === "string" && value.length > 0)
.sort((left, right) => {
const leftTime = Date.parse(String(left[1] || ""));
const rightTime = Date.parse(String(right[1] || ""));
return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
})
.slice(0, codexLocalReadRetention);
return Object.fromEntries(entries);
}
function saveLocalReadAt(readAtByTask: AnyRecord): AnyRecord {
const retained = trimLocalReadAt(readAtByTask);
if (typeof window === "undefined") return retained;
try {
window.localStorage.setItem(codexReadAtStorageKey, JSON.stringify(retained));
} catch {
// Best-effort fallback only; backend readAt remains authoritative when deployed.
}
return retained;
}
function applyLocalReadState(task: any, readAtByTask: AnyRecord): any {
const taskId = String(task?.id || "");
const localReadAt = String(readAtByTask?.[taskId] || "");
if (!taskIsTerminal(task) || localReadAt.length === 0) return task;
return { ...task, readAt: task?.readAt || localReadAt, terminalUnread: false };
}
function countNumber(value: any): number {
const number = Number(value || 0);
return Number.isFinite(number) ? number : 0;
@@ -1035,18 +1153,21 @@ function ProgressivePromptBlock({ task, loading, onLoadPromptPart, testId = "cod
}
function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onLoadSteps, onLoadStep, testId = "codex-execution-summary" }: AnyRecord) {
const execution = attemptExecutionSummary(task, attempt);
const steps = taskTraceSteps(task, attemptIndex);
const steps = coalesceFileChangeTraceSteps(taskTraceSteps(task, attemptIndex));
const execution = executionSummaryWithDisplayedSteps(attemptExecutionSummary(task, attempt), steps);
const stepDetails = taskTraceStepDetails(task);
const stepsLoaded = taskTraceStepsLoaded(task, attemptIndex);
const toolCount = Number(execution.toolCallCount || 0);
const editedFiles = Array.isArray(execution.editedFiles) ? execution.editedFiles : [];
const commands = Array.isArray(execution.commands) ? execution.commands : [];
const labelSuffix = attemptIndex ? ` #${attemptIndex}` : "";
const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex);
const labelSuffix = synthetic ? ` · ${String(attempt?.label || "recovered thread execution")}` : attemptIndex ? ` #${attemptIndex}` : "";
const updatedAt = attemptExecutionUpdatedAt(task, attempt, attemptIndex, execution);
const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`;
return h("details", {
className: "codex-progressive-card codex-execution-summary",
"data-testid": testId,
"data-attempt-index": attemptIndex ? String(attemptIndex) : undefined,
"data-attempt-index": attemptDataIndex(attemptIndex),
onToggle: (event: any) => {
if (event.currentTarget?.open && !stepsLoaded) onLoadSteps?.(attemptIndex);
},
@@ -1055,7 +1176,8 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
h("div", { className: "codex-progressive-card-head" },
h("span", { className: "codex-output-channel" }, "Summary"),
h("strong", null, `执行过程摘要${labelSuffix}`),
h("code", null, `${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${toolCount} tools`),
h("code", { title: updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel },
`${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${toolCount} tools / ${recentUpdateLabel}`),
),
h("div", { className: "codex-execution-digest" },
h("span", null, `read ${Number(execution.readCount || 0)}`),
@@ -1122,7 +1244,7 @@ function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex
const text = attemptFinalResponseText(task, attempt);
const chars = Number(attempt?.finalResponseChars || text.length);
const labelSuffix = attemptIndex ? ` #${attemptIndex}` : "";
return h("section", { className: "codex-progressive-card codex-final-response", "data-testid": testId, "data-attempt-index": attemptIndex ? String(attemptIndex) : undefined },
return h("section", { className: "codex-progressive-card codex-final-response", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) },
h("div", { className: "codex-progressive-card-head" },
h("span", { className: "codex-output-channel" }, "Final"),
h("strong", null, `最终 response${labelSuffix}`),
@@ -1135,7 +1257,7 @@ function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex
function ProgressiveJudge({ task, attempt, attemptIndex, testId = "codex-progressive-judge" }: AnyRecord) {
const judge = attemptJudge(task, attempt);
const labelSuffix = attemptIndex ? ` #${attemptIndex}` : "";
return h("section", { className: "codex-progressive-card codex-progressive-judge", "data-testid": testId, "data-attempt-index": attemptIndex ? String(attemptIndex) : undefined },
return h("section", { className: "codex-progressive-card codex-progressive-judge", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) },
h("div", { className: "codex-progressive-card-head" },
h("span", { className: "codex-output-channel" }, "Judge"),
h("strong", null, `完成判定${labelSuffix}`),
@@ -1165,7 +1287,7 @@ function ProgressiveJudgeFeedbackPrompt({ task, attempt, attemptIndex, loading,
return h("details", {
className: "codex-progressive-card codex-judge-feedback-prompt",
"data-testid": testId,
"data-attempt-index": attemptIndex ? String(attemptIndex) : undefined,
"data-attempt-index": attemptDataIndex(attemptIndex),
onToggle: (event: any) => {
if (event.currentTarget?.open && !detailText) onLoadPromptPart?.("feedback", attemptIndex);
},
@@ -1184,11 +1306,13 @@ function ProgressiveJudgeFeedbackPrompt({ task, attempt, attemptIndex, loading,
}
function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromptPart, onLoadSteps, onLoadStep }: AnyRecord) {
const attemptIndex = Number(attempt?.index || position + 1);
const attemptIndex = attemptSegmentIndex(attempt, position);
const first = position === 0;
const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex);
const title = synthetic ? String(attempt?.label || "Recovered thread execution") : `Attempt ${attemptIndex}`;
return h("section", { className: "codex-attempt-cycle", "data-testid": `codex-attempt-cycle-${attemptIndex}` },
h("div", { className: "codex-attempt-cycle-head" },
h("span", { className: "codex-output-channel" }, `Attempt ${attemptIndex}`),
h("span", { className: "codex-output-channel" }, title),
h("strong", null, String(attempt?.mode || (attemptIndex <= 1 ? "initial" : "retry"))),
attempt?.terminalStatus ? h(StatusBadge, { status: attempt.terminalStatus }, attempt.terminalStatus) : null,
h("code", null, `${fmtDate(attempt?.startedAt)} -> ${fmtDate(attempt?.finishedAt)}`),
@@ -1202,19 +1326,19 @@ function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromp
onLoadStep,
testId: first ? "codex-execution-summary" : `codex-execution-summary-attempt-${attemptIndex}`,
}),
h(ProgressiveFinalResponse, {
synthetic ? null : h(ProgressiveFinalResponse, {
task,
attempt,
attemptIndex,
testId: first ? "codex-final-response" : `codex-final-response-attempt-${attemptIndex}`,
}),
h(ProgressiveJudge, {
synthetic ? null : h(ProgressiveJudge, {
task,
attempt,
attemptIndex,
testId: first ? "codex-progressive-judge" : `codex-progressive-judge-attempt-${attemptIndex}`,
}),
h(ProgressiveJudgeFeedbackPrompt, {
synthetic ? null : h(ProgressiveJudgeFeedbackPrompt, {
task,
attempt,
attemptIndex,
@@ -1242,63 +1366,6 @@ function ProgressiveTrace({ task, loading, onLoadPromptPart, onLoadSteps, onLoad
}
function PromptDetail({ task, loading, onLoadPromptPart }: AnyRecord) {
if (!task) return h(EmptyState, { title: "未选择任务", text: "选择队列或历史 session 后,这里显示完整 prompt、模型和工作目录。" });
const promptSummary = taskPromptSummary(task);
const promptDetails = taskPromptDetails(task);
const visiblePrompt = taskBasePromptText(task).trimEnd();
const promptText = String(promptDetails.full?.text || "");
const hasReference = taskHasReferencePrompt(task);
const visibleLines = Number(promptSummary.basePromptLines || promptLineCount(visiblePrompt));
const totalLines = Number(promptSummary.promptLines || promptLineCount(promptText));
const referenceLines = Number(promptSummary.referencePromptLines || 0);
const fullChars = Number(promptSummary.promptChars || task?.promptChars || promptText.length);
return h("div", { className: "codex-prompt-detail", "data-testid": "codex-task-prompt-detail" },
h("div", { className: "codex-prompt-meta" },
h(StatusBadge, { status: task?.status }, task?.status || "unknown"),
h("span", null, `model=${task?.model || "--"}`),
h("span", null, `cwd=${task?.cwd || "--"}`),
h("span", null, `created=${fmtDate(task?.createdAt)}`),
h("span", null, hasReference
? `task ${visibleLines} lines / total ${Number.isFinite(totalLines) && totalLines > 0 ? totalLines : "--"} lines`
: `${visibleLines} lines / ${visiblePrompt.length} chars`),
),
h("div", { className: "codex-lazy-detail-callout", "data-testid": "codex-task-summary-callout" },
h("div", null,
h("strong", null, "渐进式 Trace"),
h("span", null, "首屏使用后端 Summary;展开 prompt / 步骤时只按需拉取对应片段,不一次性拉取完整 transcript。"),
),
),
hasReference ? h("details", {
className: "codex-reference-injection codex-final-prompt-injection",
"data-testid": "codex-final-prompt-full",
onToggle: (event: any) => {
if (event.currentTarget?.open && !promptText) onLoadPromptPart?.("full");
},
},
h("summary", null,
h("span", null, "最终传入 Codex 的真实完整 prompt"),
h("code", null, promptText ? `${totalLines || promptLineCount(promptText)} lines / ${promptText.length} chars` : `${Number.isFinite(fullChars) && fullChars > 0 ? fullChars : "--"} chars`),
),
h("pre", { className: "codex-prompt-full codex-prompt-final-full", "data-testid": "codex-task-final-prompt-full" }, promptText || (loading ? "正在按需拉取完整 prompt..." : "展开后将只请求完整 prompt。")),
) : null,
hasReference ? h("details", {
className: "codex-reference-injection",
"data-testid": "codex-reference-injection",
onToggle: (event: any) => {
if (event.currentTarget?.open && !promptDetails.reference?.text) onLoadPromptPart?.("reference");
},
},
h("summary", null,
h("span", null, "引用注入已折叠"),
h("code", null, promptDetails.reference?.text ? `${promptLineCount(String(promptDetails.reference.text || ""))} lines / ${String(promptDetails.reference.text || "").length} chars` : `${referenceLines || "--"} lines`),
),
h("pre", { className: "codex-prompt-full codex-prompt-reference-full", "data-testid": "codex-task-reference-full" }, String(promptDetails.reference?.text || "") || (loading ? "正在按需拉取引用注入..." : "展开后将只请求引用注入片段。")),
) : null,
h("pre", { className: "codex-prompt-full", "data-testid": "codex-task-prompt-full" }, visiblePrompt || "空 prompt"),
);
}
function RawTranscript({ task }: AnyRecord) {
const output = taskOutput(task);
if (!task || output.length === 0) return h(EmptyState, { title: "暂无原始消息", text: "原始 Codex app-server 消息会保留在任务 JSON 中。" });
@@ -1395,7 +1462,6 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
const [copiedTaskId, setCopiedTaskId] = useState("");
const [markingReadTaskId, setMarkingReadTaskId] = useState("");
const [markingAllRead, setMarkingAllRead] = useState(false);
const [localReadAt, setLocalReadAt] = useState(loadLocalReadAt);
const [loadStats, setLoadStats] = useState(initialTasksData ? {
phase: "complete",
taskId: initialSelectedId,
@@ -1410,7 +1476,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
const [refreshedAt, setRefreshedAt] = useState(initialTasksData ? new Date() : null);
const [loadingMoreTasks, setLoadingMoreTasks] = useState(false);
const tasks = taskRows(tasksData).map((task: any) => applyLocalReadState(task, localReadAt));
const tasks = taskRows(tasksData);
const unreadTerminalTasks = tasks.filter(taskIsUnreadTerminal);
const queuedTasks = tasks.filter((task: any) => !taskIsTerminal(task));
const historyTasks = tasks.filter((task: any) => taskIsTerminal(task) && !taskIsUnreadTerminal(task));
@@ -1429,7 +1495,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
const globalCounts = queueCounts(queue);
const overallQueuedCount = queueWaitingCount(globalCounts);
const overallRunningCount = Math.max(queueRunningCount(globalCounts), globalActiveIds.length);
const overallUnreadTerminalCount = countNumber(queue?.unreadTerminal ?? unreadTerminalTasks.length);
const overallUnreadTerminalCount = countNumber((isAllQueues(selectedQueueId) ? queue?.unreadTerminal : viewQueueRow?.unreadTerminal) ?? unreadTerminalTasks.length);
const selectedQueueName = isAllQueues(selectedQueueId) ? "All queues" : selectedQueueId;
const runtime = service ? microserviceRuntime(service) : {};
const repository = service ? microserviceRepository(service) : {};
@@ -1478,16 +1544,6 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
}
}
function rememberLocalRead(taskIds: string[], readAt: string): void {
const ids = taskIds.map((id) => String(id || "")).filter(Boolean);
if (ids.length === 0) return;
setLocalReadAt((previous: AnyRecord) => {
const next = { ...(previous || {}) };
for (const id of ids) next[id] = readAt;
return saveLocalReadAt(next);
});
}
useEffect(() => {
setBatchConfirmed(false);
}, [prompt, repeatCount, referenceTaskId]);
@@ -1867,6 +1923,11 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
}
setRefreshedAt(new Date());
if (trackLoad) trackedLoadInFlightRef.current = false;
// Overview is intentionally small and may only contain the latest attempt.
// Load the canonical Trace Summary as a follow-up so retry/judge feedback
// cards are shown for historical multi-attempt sessions too.
void ensureTraceSummary(nextId, false, trackLoad ? startedAt : undefined, trackLoad ? queueMs : undefined)
.catch((err) => setError(errorText(err, "加载 Codex Trace Summary 失败")));
return;
}
if (trackLoad) {
@@ -1993,18 +2054,11 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
if (!service || !taskId) return;
setMarkingReadTaskId(taskId);
await guarded(async () => {
let result: any = null;
let localOnly = false;
try {
result = await markTaskReadRequest(apiBaseUrl, taskId);
} catch {
localOnly = true;
}
const result = await markTaskReadRequest(apiBaseUrl, taskId);
const task = result?.task || { id: taskId, readAt: new Date().toISOString(), terminalUnread: false };
const readAt = String(task?.readAt || new Date().toISOString());
rememberLocalRead([taskId], readAt);
patchLoadedReadState([taskId], readAt, result?.queue || null, task);
setNotice(localOnly ? `已在本浏览器将任务 ${taskId} 标为已读;后端升级后会同步持久化` : `已将任务 ${taskId} 标为已读`);
setNotice(`已将任务 ${taskId} 标为已读`);
}, "标记 Codex task 已读失败");
setMarkingReadTaskId((value: string) => value === taskId ? "" : value);
}
@@ -2013,27 +2067,19 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
if (!service || markingAllRead) return;
setMarkingAllRead(true);
await guarded(async () => {
let result: any = null;
let localOnly = false;
try {
result = await markAllTerminalReadRequest(apiBaseUrl);
} catch {
localOnly = true;
}
const result = await markAllTerminalReadRequest(apiBaseUrl);
const readAt = String(result?.readAt || new Date().toISOString());
const loadedUnreadIds = taskRows(tasksDataRef.current)
.map((task: any) => applyLocalReadState(task, localReadAt))
.filter(taskIsUnreadTerminal)
.map((task: any) => String(task?.id || ""))
.filter(Boolean);
const cachedUnreadIds = Array.from(sessionCacheRef.current.entries())
.filter(([, value]) => taskIsUnreadTerminal(applyLocalReadState(value?.task, localReadAt)))
.filter(([, value]) => taskIsUnreadTerminal(value?.task))
.map(([id]) => id);
const readIds = Array.from(new Set([...loadedUnreadIds, ...cachedUnreadIds]));
rememberLocalRead(readIds, readAt);
patchLoadedReadState(readIds, readAt, result?.queue || null);
const markedCount = localOnly ? readIds.length : Number(result?.count || readIds.length);
setNotice(localOnly ? `已在本浏览器将 ${markedCount} 个已结束未读任务标为已读;后端升级后会同步持久化` : `已将 ${markedCount} 个已结束未读任务标为已读`);
const markedCount = Number(result?.count || readIds.length);
setNotice(`已将 ${markedCount} 个已结束未读任务标为已读`);
}, "全部标为已读失败");
setMarkingAllRead(false);
}
@@ -2446,9 +2492,6 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init
),
h("div", { className: "codex-main-stage" },
h("div", { className: "codex-detail-grid" },
h(Panel, { title: "Prompt 全量", eyebrow: selectedTask ? String(selectedTask.id) : "selected task", className: "codex-prompt-panel" },
h(PromptDetail, { task: selectedTask, loading: selectedDetailLoading, onLoadPromptPart: ensurePromptPart }),
),
h(Panel, { title: "运行控制", eyebrow: selectedCanSteer ? "Active turn steer" : "Steer when running" },
h("div", { className: "codex-run-control-stack" },
h(TaskQueueMoveControl, { task: selectedTask, queueRows, busy, onMove: moveSelectedTaskQueue }),
+1 -10
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate } from "./time";
import { errorMessage, requestJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -8,16 +9,6 @@ const h = React.createElement;
const { useEffect } = React;
const useState: any = React.useState;
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function StatusBadge({ status, children }: AnyRecord) {
const normalized = String(status || "unknown").toLowerCase();
+1 -10
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate } from "./time";
import { errorMessage, requestJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -8,16 +9,6 @@ const h = React.createElement;
const { useEffect } = React;
const useState: any = React.useState;
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function fmtPercent(value: any): string {
const number = Number(value);
+1 -10
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate } from "./time";
import { Background, BaseEdge, Controls, Handle, MarkerType, Position, ReactFlow, type Edge, type Node } from "@xyflow/react";
import { TraceView, opencodeTracePort } from "./trace";
import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error";
@@ -192,16 +193,6 @@ function PipelineCurveEdge({ id, sourceX, sourceY, targetX, targetY, targetPosit
const pipelineEdgeTypes: any = { pipelineCurve: PipelineCurveEdge };
const pipelineNodeTypes: any = { pipelineNode: PipelineFlowNode };
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function fmtClockValue(value: any): string {
if (!value) return "--";
@@ -1,4 +1,5 @@
import React from "react";
import { beijingDateStamp, fmtClock, fmtDate } from "./time";
import { errorMessage, requestBlob, requestJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -19,16 +20,6 @@ const EMPTY_FORM = {
notes: "",
};
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function StatusBadge({ status, children }: AnyRecord) {
const normalized = String(status || "unknown").toLowerCase();
@@ -230,7 +221,7 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" }
const href = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = href;
anchor.download = `project-manager-${new Date().toISOString().slice(0, 10)}.xlsx`;
anchor.download = `project-manager-${beijingDateStamp()}.xlsx`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
+77
View File
@@ -0,0 +1,77 @@
export const BEIJING_TIME_ZONE = "Asia/Shanghai";
export const BEIJING_TIME_LABEL = "北京时间";
const BEIJING_UTC_OFFSET_HOURS = 8;
const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
timeZone: BEIJING_TIME_ZONE,
hour12: false,
};
const CLOCK_OPTIONS: Intl.DateTimeFormatOptions = {
timeZone: BEIJING_TIME_ZONE,
hour12: false,
};
const INPUT_PARTS_FORMATTER = new Intl.DateTimeFormat("en-CA", {
timeZone: BEIJING_TIME_ZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
});
function coerceDate(value: any): Date | null {
if (value === null || value === undefined || value === "") return null;
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function beijingInputParts(value: any): Record<string, string> | null {
const date = coerceDate(value);
if (!date) return null;
return INPUT_PARTS_FORMATTER.formatToParts(date).reduce((parts: Record<string, string>, part) => {
if (part.type !== "literal") parts[part.type] = part.value;
return parts;
}, {});
}
export function fmtDate(value: any): string {
const date = coerceDate(value);
return date ? date.toLocaleString("zh-CN", DATE_TIME_OPTIONS) : "--";
}
export function fmtClock(value: any): string {
const date = coerceDate(value);
return date ? date.toLocaleTimeString("zh-CN", CLOCK_OPTIONS) : "--";
}
export function fmtDateTimeLocalInput(value: any): string {
const parts = beijingInputParts(value);
if (!parts) return "";
const hour = parts.hour === "24" ? "00" : parts.hour;
return `${parts.year}-${parts.month}-${parts.day}T${hour}:${parts.minute}`;
}
export function beijingDateStamp(value: any = new Date()): string {
const parts = beijingInputParts(value);
if (!parts) return "";
return `${parts.year}-${parts.month}-${parts.day}`;
}
export function localDateTimeInputToBeijingIso(value: string): string | null {
if (!value) return null;
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
if (!match) return null;
const [, year, month, day, hour, minute, second = "00"] = match;
const utcMs = Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hour) - BEIJING_UTC_OFFSET_HOURS,
Number(minute),
Number(second),
);
const date = new Date(utcMs);
const normalized = fmtDateTimeLocalInput(date);
return Number.isNaN(date.getTime()) || normalized !== `${year}-${month}-${day}T${hour}:${minute}` ? null : date.toISOString();
}
+2 -26
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtClock, fmtDate, fmtDateTimeLocalInput, localDateTimeInputToBeijingIso } from "./time";
import { errorMessage, requestJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
@@ -9,17 +10,6 @@ const h = React.createElement;
const { useEffect } = React;
const useState: any = React.useState;
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtClock(value: Date): string {
return value.toLocaleTimeString("zh-CN", { hour12: false });
}
function StatusBadge({ status, children }: AnyRecord) {
const normalized = String(status || "unknown").toLowerCase();
return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown");
@@ -102,20 +92,6 @@ function todoVisible(todo: any, filter: string): boolean {
return selfVisible || children.some((child: any) => todoVisible(child, filter));
}
function todoReminderLocal(value: any): string {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}
function localReminderIso(value: string): string | null {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function todoRegistryRows(registry: any): any[] {
return Array.isArray(registry?.instances) ? registry.instances : [];
}
@@ -430,7 +406,7 @@ function TodoRow(props: AnyRecord): ReactNode {
todo.reminderAt ? h("span", { className: "todo-reminder" }, `提醒 ${fmtDate(todo.reminderAt)}`) : h("span", null, "无提醒"),
),
),
h("input", { className: "todo-reminder-input", type: "datetime-local", value: todoReminderLocal(todo.reminderAt), onChange: (event: any) => applyTodoAction({ type: "setTodoReminder", todoId: todo.id, reminderAt: localReminderIso(event.target.value) }) }),
h("input", { className: "todo-reminder-input", type: "datetime-local", value: fmtDateTimeLocalInput(todo.reminderAt), onChange: (event: any) => applyTodoAction({ type: "setTodoReminder", todoId: todo.id, reminderAt: localDateTimeInputToBeijingIso(event.target.value) }) }),
h("div", { className: "todo-row-actions" },
h("button", { type: "button", className: "ghost-btn", onClick: () => beginEdit(todo) }, "编辑"),
h("button", { type: "button", className: "ghost-btn", onClick: () => addChild(todo.id) }, "子项"),
+1 -7
View File
@@ -1,4 +1,5 @@
import React from "react";
import { fmtDate } from "./time";
type AnyRecord = Record<string, any>;
@@ -73,13 +74,6 @@ export function traceFromPort<Input>(port: TracePort<Input>, input: Input): Trac
return normalizeTraceItems(port.toTrace(input));
}
function fmtDate(value: any): string {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return date.toLocaleString("zh-CN", { hour12: false });
}
function fmtDuration(ms: any): string {
const value = Number(ms);
if (!Number.isFinite(value) || value < 0) return "--";
+3 -1
View File
@@ -1,3 +1,5 @@
import { BEIJING_TIME_LABEL, fmtDate } from "./time";
type AnyRecord = Record<string, any>;
export type UniDeskFailureField = string | false;
@@ -231,7 +233,7 @@ function fmtDateTime(value: string | undefined): string {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return `${date.toLocaleString("zh-CN", { hour12: false })} / ${date.toISOString()}`;
return `${fmtDate(date)} ${BEIJING_TIME_LABEL}`;
}
export function describeUniDeskError(error: unknown, fallback = "操作失败"): UniDeskErrorView {
File diff suppressed because it is too large Load Diff