diff --git a/src/components/frontend/src/code-queue.tsx b/src/components/frontend/src/code-queue.tsx index cda23150..ea89f33a 100644 --- a/src/components/frontend/src/code-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -317,14 +317,14 @@ function queueRunnableTaskId(queueRows: any[], queueId: string, rows: any[]): st async function loadTaskList(apiBaseUrl: string, queueId = allQueuesId, searchQuery = ""): Promise { return requestJson(codexApi( apiBaseUrl, - `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0${taskListQuerySuffix(queueId, searchQuery)}`, + `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=1&compact=1&selected=0&skipTrace=1${taskListQuerySuffix(queueId, searchQuery)}`, ), codexNoCacheOptions()); } -async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = ""): Promise { +async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = "", skipTrace = false): Promise { return requestJson(codexApi( apiBaseUrl, - `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${taskListQuerySuffix(queueId, searchQuery)}`, + `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${skipTrace ? "&skipTrace=1" : ""}${taskListQuerySuffix(queueId, searchQuery)}`, ), codexNoCacheOptions()); } @@ -2528,7 +2528,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const requestedTranscript = Array.isArray(requestedCached?.task?.transcript) ? requestedCached.task.transcript : []; const overviewAfterSeq = transcriptResumeSeq(requestedTranscript); let tasksResult = null; - tasksResult = await loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId); + tasksResult = await loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId, "", true); if (token !== queueLoadTokenRef.current) { if (trackLoad) trackedLoadInFlightRef.current = false; return; @@ -2608,6 +2608,19 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi // 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 失败"))); + // Background refresh with trace stats to populate stepCount in task list + void loadTaskOverview(apiBaseUrl, requestedId, overviewAfterSeq, queueFilterId, "", false) + .then((full: any) => { + if (token !== queueLoadTokenRef.current) return; + const fullRows = taskRows(full); + if (fullRows.length > 0) { + setTasksData((previous: any) => { + const mergedRows = mergeTaskRowsPreferLatest([taskRows(previous), fullRows], activeSortId); + return { ...previous, tasks: applyLocalReadStateToRows(mergedRows) }; + }); + } + }) + .catch(() => {}); return; } if (trackLoad) { diff --git a/src/components/microservices/code-queue/src/oa-events.ts b/src/components/microservices/code-queue/src/oa-events.ts index 1ec9d917..56b43506 100644 --- a/src/components/microservices/code-queue/src/oa-events.ts +++ b/src/components/microservices/code-queue/src/oa-events.ts @@ -433,14 +433,37 @@ export function publishCodeQueueJudgeEvent(task: QueueTask, queueId: string, typ }); } +const traceStatsCache = new Map(); +let traceStatsCacheLastPrune = 0; + +function pruneTraceStatsCache(): void { + const now = Date.now(); + if (now - traceStatsCacheLastPrune < 10_000) return; + traceStatsCacheLastPrune = now; + for (const [key, entry] of traceStatsCache) { + if (entry.expiresAt <= now) traceStatsCache.delete(key); + } +} + export async function readOaTraceStatsForScopeIds(scopeIds: string[]): Promise> { const result = new Map(); const uniqueScopeIds = Array.from(new Set(scopeIds.map((id) => String(id || "").trim()).filter(Boolean))); if (uniqueScopeIds.length === 0) return result; + const now = Date.now(); + const uncachedIds: string[] = []; + for (const id of uniqueScopeIds) { + const cached = traceStatsCache.get(id); + if (cached !== undefined && cached.expiresAt > now) { + result.set(cached.stats.scopeId, cached.stats); + } else { + uncachedIds.push(id); + } + } + if (uncachedIds.length === 0) { pruneTraceStatsCache(); return result; } const runtime = ctx(); const url = new URL(`${runtime.baseUrl}/api/stats/trace`); - url.searchParams.set("scopeIds", uniqueScopeIds.join(",")); - url.searchParams.set("limit", String(Math.max(100, uniqueScopeIds.length))); + url.searchParams.set("scopeIds", uncachedIds.join(",")); + url.searchParams.set("limit", String(Math.max(100, uncachedIds.length))); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 2500); try { @@ -448,17 +471,20 @@ export async function readOaTraceStatsForScopeIds(scopeIds: string[]): Promise; const stats = Array.isArray(body.stats) ? body.stats : []; + const cacheTtl = 8_000; for (const item of stats) { if (typeof item !== "object" || item === null || Array.isArray(item)) continue; const record = item as OaTraceStats; if (typeof record.scopeId !== "string") continue; result.set(record.scopeId, record); + traceStatsCache.set(record.scopeId, { expiresAt: now + cacheTtl, stats: record }); } } catch (error) { - runtime.logger("warn", "oa_trace_stats_read_failed", { scopeCount: uniqueScopeIds.length, error: error instanceof Error ? error.message : String(error) }); + runtime.logger("warn", "oa_trace_stats_read_failed", { scopeCount: uncachedIds.length, error: error instanceof Error ? error.message : String(error) }); } finally { clearTimeout(timer); } + pruneTraceStatsCache(); return result; } diff --git a/src/components/microservices/code-queue/src/queue-api.ts b/src/components/microservices/code-queue/src/queue-api.ts index 14534f8e..70ffa213 100644 --- a/src/components/microservices/code-queue/src/queue-api.ts +++ b/src/components/microservices/code-queue/src/queue-api.ts @@ -733,9 +733,15 @@ function databaseStatsRange(url: URL): { days: number; startDate: string; endDat }; } +const statisticsCache = new Map(); + async function databaseTaskStatisticsSummary(queueId: string | null, url: URL): Promise { - const generatedAt = new Date().toISOString(); const range = databaseStatsRange(url); + const cacheKey = `${queueId ?? "all"}:${range.days}:${range.startDate}`; + const now = Date.now(); + const cached = statisticsCache.get(cacheKey); + if (cached !== undefined && cached.expiresAt > now) return cached.value; + const generatedAt = new Date().toISOString(); const buckets = new Map>(); for (let offset = 0; offset < range.days; offset += 1) { const date = shiftStatsDateKey(range.startDate, offset); @@ -853,7 +859,7 @@ async function databaseTaskStatisticsSummary(queueId: string | null, url: URL): totalDurationMs: 0, durationSamples: 0, }); - return { + const result = { generatedAt, timezone: statsTimeZone, days: range.days, @@ -864,6 +870,13 @@ async function databaseTaskStatisticsSummary(queueId: string | null, url: URL): }, daily, } as unknown as JsonValue; + statisticsCache.set(cacheKey, { expiresAt: now + 10_000, value: result }); + if (statisticsCache.size > 20) { + for (const [key, entry] of statisticsCache) { + if (entry.expiresAt <= now) statisticsCache.delete(key); + } + } + return result; } async function databaseTaskTotal(queueId: string | null): Promise { @@ -1062,7 +1075,10 @@ async function databaseTasksOverviewResponse(url: URL): Promise } as unknown as JsonValue; } const queueContextTasks = [...ctx().tasks(), ...rowsSource]; - const traceStats = await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask)); + const includeTrace = url.searchParams.get("skipTrace") !== "1"; + const traceStats = includeTrace + ? await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask)) + : new Map(); if (selectedTask !== null && selected !== null && typeof selected === "object" && !Array.isArray(selected)) { const selectedRecord = selected as Record; selectedRecord.task = applyStats(selectedRecord.task, traceStats, selectedTask); @@ -1130,7 +1146,10 @@ async function tasksOverviewResponse(url: URL): Promise { maxSeq, } as unknown as JsonValue; } - const traceStats = await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask)); + const includeTrace = url.searchParams.get("skipTrace") !== "1"; + const traceStats = includeTrace + ? await ctx().traceStatsForTasks(taskIdsForStats(rowsSource, selectedTask)) + : new Map(); if (selectedTask !== null && selected !== null && typeof selected === "object" && !Array.isArray(selected)) { const selectedRecord = selected as Record; selectedRecord.task = applyStats(selectedRecord.task, traceStats, selectedTask);