fix code queue stats panel
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2365,6 +2365,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
min-height: 260px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background:
|
||||
@@ -2378,6 +2379,49 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
height: clamp(180px, 24vw, 260px);
|
||||
overflow: visible;
|
||||
}
|
||||
.codex-stats-chart.no-data svg {
|
||||
opacity: 0.42;
|
||||
}
|
||||
.codex-stats-chart-empty {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 5px;
|
||||
padding: 14px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.codex-stats-chart-empty strong {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
}
|
||||
.codex-stats-chart-empty span {
|
||||
max-width: 360px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.codex-stats-diagnostic {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(215, 161, 58, 0.36);
|
||||
background: linear-gradient(135deg, rgba(215, 161, 58, 0.10), rgba(207, 106, 84, 0.06));
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.codex-stats-diagnostic b {
|
||||
color: var(--accent);
|
||||
}
|
||||
.codex-stats-diagnostic code {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--accent-2);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.codex-stats-chart .axis,
|
||||
.codex-stats-chart .grid {
|
||||
stroke: rgba(255,255,255,0.14);
|
||||
@@ -2582,6 +2626,18 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
border-color: rgba(215, 161, 58, 0.34);
|
||||
background: rgba(215, 161, 58, 0.07);
|
||||
}
|
||||
.codex-stats-daily-row.empty {
|
||||
grid-template-columns: minmax(90px, auto) minmax(0, 1fr) auto auto;
|
||||
}
|
||||
.codex-stats-daily-row.empty b {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.codex-stats-daily-row.empty.unavailable,
|
||||
.codex-stats-daily-row.empty.degraded {
|
||||
border-color: rgba(215, 161, 58, 0.28);
|
||||
background: rgba(215, 161, 58, 0.05);
|
||||
}
|
||||
.codex-task-move-control {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
||||
@@ -389,6 +389,13 @@ async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq =
|
||||
), codexNoCacheOptions());
|
||||
}
|
||||
|
||||
async function loadTaskStatistics(apiBaseUrl: string, queueId = allQueuesId): Promise<any> {
|
||||
return requestJson(codexApi(
|
||||
apiBaseUrl,
|
||||
`/api/tasks/stats?days=14${queueQuerySuffix(queueId)}`,
|
||||
), codexNoCacheOptions());
|
||||
}
|
||||
|
||||
async function loadTaskPage(apiBaseUrl: string, queueId: string, beforeId: string, limit = codexMoreTaskLimit, searchQuery = ""): Promise<any> {
|
||||
return requestJson(codexApi(
|
||||
apiBaseUrl,
|
||||
@@ -1304,12 +1311,150 @@ function taskStatistics(data: any, fallbackQueue: any): AnyRecord {
|
||||
return objectRecord(data?.statistics) || objectRecord(fallbackQueue?.statistics) || {};
|
||||
}
|
||||
|
||||
function taskStatisticsRows(stats: any): any[] {
|
||||
return Array.isArray(stats?.daily) ? stats.daily : [];
|
||||
function firstDefined(...values: any[]): any {
|
||||
return values.find((value) => value !== undefined && value !== null);
|
||||
}
|
||||
|
||||
function taskStatisticsTotals(stats: any): AnyRecord {
|
||||
return objectRecord(stats?.totals) || {};
|
||||
function statsNumber(value: any): number | null {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number : null;
|
||||
}
|
||||
|
||||
function statsCountValue(...values: any[]): number {
|
||||
for (const value of values) {
|
||||
const number = statsNumber(value);
|
||||
if (number !== null) return Math.floor(number);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function statsDurationValue(...values: any[]): number | null {
|
||||
for (const value of values) {
|
||||
const number = statsNumber(value);
|
||||
if (number !== null) return Math.round(number);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function taskStatisticsRawRows(stats: any): any[] {
|
||||
for (const value of [
|
||||
stats?.daily,
|
||||
stats?.dailyBuckets,
|
||||
stats?.daily_buckets,
|
||||
stats?.buckets,
|
||||
stats?.rows,
|
||||
stats?.series,
|
||||
]) {
|
||||
if (Array.isArray(value)) return value;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function taskStatisticsRows(stats: any): any[] {
|
||||
return taskStatisticsRawRows(stats)
|
||||
.map((row: any, index: number) => {
|
||||
const record = objectRecord(row) || {};
|
||||
const date = String(firstDefined(record.date, record.day, record.bucketDate, record.bucket, `day-${index}`) || "");
|
||||
return {
|
||||
...record,
|
||||
date,
|
||||
executedTasks: statsCountValue(record.executedTasks, record.tasks, record.taskCount, record.startedTasks, record.started, record.executed, record.count),
|
||||
completedTasks: statsCountValue(record.completedTasks, record.terminalTasks, record.terminalCount, record.finishedTasks, record.finished, record.completed),
|
||||
retryAttempts: statsCountValue(record.retryAttempts, record.retries, record.retryCount, record.retryTasks),
|
||||
succeededTasks: statsCountValue(record.succeededTasks, record.succeeded, record.success),
|
||||
failedTasks: statsCountValue(record.failedTasks, record.failed, record.failures),
|
||||
canceledTasks: statsCountValue(record.canceledTasks, record.cancelledTasks, record.canceled, record.cancelled),
|
||||
totalDurationMs: statsCountValue(record.totalDurationMs, record.durationTotalMs, record.totalCompletionDurationMs),
|
||||
durationSamples: statsCountValue(record.durationSamples, record.avgDurationSamples, record.samples, record.sampleCount),
|
||||
avgDurationMs: statsDurationValue(record.avgDurationMs, record.averageDurationMs, record.avgCompletedDurationMs, record.averageCompletionMs, record.averageDuration, record.durationMs),
|
||||
};
|
||||
})
|
||||
.filter((row: any) => row.date.length > 0);
|
||||
}
|
||||
|
||||
function sumStatRows(rows: any[], key: string): number {
|
||||
return rows.reduce((sum, row) => sum + countNumber(row?.[key]), 0);
|
||||
}
|
||||
|
||||
function weightedAverageDuration(rows: any[]): number | null {
|
||||
let totalDuration = 0;
|
||||
let samples = 0;
|
||||
for (const row of rows) {
|
||||
const rowSamples = countNumber(row?.durationSamples);
|
||||
const average = statsDurationValue(row?.avgDurationMs);
|
||||
if (rowSamples > 0 && average !== null) {
|
||||
totalDuration += average * rowSamples;
|
||||
samples += rowSamples;
|
||||
}
|
||||
}
|
||||
return samples > 0 ? Math.round(totalDuration / samples) : null;
|
||||
}
|
||||
|
||||
function taskStatisticsTotals(stats: any, rows: any[] = taskStatisticsRows(stats)): AnyRecord {
|
||||
const totals = objectRecord(stats?.totals) || objectRecord(stats?.summary) || {};
|
||||
const durationSamples = statsCountValue(totals.durationSamples, totals.avgDurationSamples, totals.samples, stats?.durationSamples, stats?.samples, sumStatRows(rows, "durationSamples"));
|
||||
const totalDurationMs = statsCountValue(totals.totalDurationMs, totals.durationTotalMs, stats?.totalDurationMs, sumStatRows(rows, "totalDurationMs"));
|
||||
const avgDurationMs = statsDurationValue(
|
||||
totals.avgDurationMs,
|
||||
totals.averageDurationMs,
|
||||
totals.avgCompletedDurationMs,
|
||||
stats?.avgDurationMs,
|
||||
stats?.averageDurationMs,
|
||||
durationSamples > 0 && totalDurationMs > 0 ? totalDurationMs / durationSamples : null,
|
||||
weightedAverageDuration(rows),
|
||||
);
|
||||
return {
|
||||
...totals,
|
||||
executedTasks: statsCountValue(totals.executedTasks, totals.tasks, totals.taskCount, stats?.executedTasks, stats?.tasks, stats?.taskCount, sumStatRows(rows, "executedTasks")),
|
||||
completedTasks: statsCountValue(totals.completedTasks, totals.terminalTasks, totals.terminalCount, totals.finishedTasks, stats?.completedTasks, stats?.terminalTasks, stats?.terminalCount, sumStatRows(rows, "completedTasks")),
|
||||
retryAttempts: statsCountValue(totals.retryAttempts, totals.retries, totals.retryCount, stats?.retryAttempts, stats?.retries, stats?.retryCount, sumStatRows(rows, "retryAttempts")),
|
||||
succeededTasks: statsCountValue(totals.succeededTasks, stats?.succeededTasks, sumStatRows(rows, "succeededTasks")),
|
||||
failedTasks: statsCountValue(totals.failedTasks, stats?.failedTasks, sumStatRows(rows, "failedTasks")),
|
||||
canceledTasks: statsCountValue(totals.canceledTasks, totals.cancelledTasks, stats?.canceledTasks, stats?.cancelledTasks, sumStatRows(rows, "canceledTasks")),
|
||||
durationSamples,
|
||||
totalDurationMs,
|
||||
avgDurationMs,
|
||||
};
|
||||
}
|
||||
|
||||
function taskStatisticsReason(stats: any): string {
|
||||
const raw = firstDefined(stats?.reason, stats?.degradedReason, stats?.statsReason, stats?.error, stats?.message, stats?.detail);
|
||||
if (typeof raw === "string" && raw.trim().length > 0) return shortText(raw, 180);
|
||||
if (stats?.skipped === true) return "统计 API 返回 skipped,当前后端未提供 daily buckets。";
|
||||
if (stats?.unavailable === true) return "统计 API 当前不可用。";
|
||||
return "统计 API 未返回可用的 daily buckets。";
|
||||
}
|
||||
|
||||
function taskStatisticsState(stats: any, rows: any[], totals: AnyRecord): AnyRecord {
|
||||
const hasStatsObject = objectRecord(stats) !== null;
|
||||
const explicitUnavailable = stats?.unavailable === true || stats?._unavailable === true || stats?.available === false;
|
||||
const skipped = stats?.skipped === true;
|
||||
const degraded = stats?.degraded === true || stats?.isDegraded === true || explicitUnavailable || skipped;
|
||||
const totalActivity = countNumber(totals.executedTasks) + countNumber(totals.retryAttempts) + countNumber(totals.completedTasks);
|
||||
if (!hasStatsObject) {
|
||||
return { state: "loading", label: "统计同步中", reason: "正在读取 Code Queue stats API。" };
|
||||
}
|
||||
if (explicitUnavailable || skipped || (degraded && rows.length === 0)) {
|
||||
return { state: "unavailable", label: "统计不可用", reason: taskStatisticsReason(stats), degraded: true };
|
||||
}
|
||||
if (degraded) {
|
||||
return { state: "degraded", label: "统计降级", reason: taskStatisticsReason(stats), degraded: true };
|
||||
}
|
||||
if (rows.length === 0 && totalActivity === 0) {
|
||||
return { state: "empty", label: "暂无统计", reason: "任务开始执行后会生成按天汇总的曲线。" };
|
||||
}
|
||||
return { state: "ready", label: "统计就绪", reason: "" };
|
||||
}
|
||||
|
||||
function taskStatisticsUnavailable(error: unknown): AnyRecord {
|
||||
return {
|
||||
source: "frontend-stats-fetch",
|
||||
unavailable: true,
|
||||
degraded: true,
|
||||
reason: errorText(error, "统计 API 不可用"),
|
||||
daily: [],
|
||||
totals: null,
|
||||
};
|
||||
}
|
||||
|
||||
function statMetric(row: any, key: string): number {
|
||||
@@ -1779,13 +1924,19 @@ function CodeQueueLivenessPanel({ diagnostics, queue, onRaw }: AnyRecord) {
|
||||
|
||||
function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord) {
|
||||
const rows = taskStatisticsRows(stats);
|
||||
const totals = taskStatisticsTotals(stats);
|
||||
const totals = taskStatisticsTotals(stats, rows);
|
||||
const latest = rows.at(-1) || {};
|
||||
const executedMax = statMetricMax(rows, "executedTasks");
|
||||
const retryMax = statMetricMax(rows, "retryAttempts");
|
||||
const durationMax = statMetricMax(rows, "avgDurationMs");
|
||||
const hasRows = rows.length > 0;
|
||||
const statsRange = objectRecord(stats?.range) || {};
|
||||
const statsState = taskStatisticsState(stats, rows, totals);
|
||||
const statsStateLabel = String(statsState.label || "");
|
||||
const statsStateReason = String(statsState.reason || "");
|
||||
const chartDescription = statsState.state === "ready" || statsState.state === "degraded"
|
||||
? "将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"
|
||||
: statsStateReason || "等待统计数据。";
|
||||
const [hoveredStatPoint, setHoveredStatPoint] = useState(null);
|
||||
const [pinnedStatPoint, setPinnedStatPoint] = useState(null);
|
||||
const maxLabelParts: string[] = [];
|
||||
@@ -1846,18 +1997,23 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
|
||||
return h(Panel, {
|
||||
title: "统计曲线",
|
||||
eyebrow: `Daily task stats / ${activeQueueName}`,
|
||||
className: "codex-stats-panel",
|
||||
summary: h("span", null, `${statDateLabel(statsRange.startDate)} -> ${statDateLabel(statsRange.endDate)} · ${stats?.timezone || "Asia/Shanghai"}`),
|
||||
className: `codex-stats-panel ${statsState.state}`,
|
||||
summary: h("span", null, `${statDateLabel(statsRange.startDate)} -> ${statDateLabel(statsRange.endDate)} · ${stats?.timezone || "Asia/Shanghai"} · ${statsStateLabel}`),
|
||||
actions: objectRecord(stats) ? h(RawButton, { title: "Code Queue Stats", data: stats, onOpen: onRaw, testId: "raw-codex-stats" }) : null,
|
||||
},
|
||||
h("div", { className: "codex-stats-hero", "data-testid": "codex-stats-panel" },
|
||||
h("div", { className: "codex-stats-hero", "data-testid": "codex-stats-panel", "data-stats-state": statsState.state, "data-stats-degraded": statsState.degraded === true ? "true" : "false" },
|
||||
h(CodexStatsIcon),
|
||||
h("div", null,
|
||||
h("strong", null, `${countNumber(totals.executedTasks)} tasks / ${countNumber(totals.retryAttempts)} retries`),
|
||||
h("span", null, `平均完成耗时 ${fmtDuration(totals.avgDurationMs ?? undefined)} · 终态 ${countNumber(totals.completedTasks)} 个`),
|
||||
),
|
||||
),
|
||||
hasRows ? h("div", { className: "codex-stats-chart", "data-testid": "codex-stats-chart", onMouseLeave: () => setHoveredStatPoint(null) },
|
||||
statsState.degraded === true ? h("div", { className: `codex-stats-diagnostic ${statsState.state}`, "data-testid": "codex-stats-diagnostic" },
|
||||
h("b", null, statsStateLabel),
|
||||
h("span", null, statsStateReason),
|
||||
stats?.source ? h("code", null, `source=${String(stats.source)}`) : null,
|
||||
) : null,
|
||||
h("div", { className: `codex-stats-chart ${hasRows ? "has-data" : "no-data"}`, "data-testid": "codex-stats-chart", "data-empty": hasRows ? "false" : "true", onMouseLeave: () => setHoveredStatPoint(null) },
|
||||
h("svg", { viewBox: `0 0 ${statChartWidth} ${statChartHeight}`, preserveAspectRatio: "none", role: "img", "aria-label": "Code Queue daily task statistics" },
|
||||
h("line", { className: "axis", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartBaselineY, y2: statChartBaselineY }),
|
||||
h("line", { className: "grid", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartTopY + statChartValueHeight / 2, y2: statChartTopY + statChartValueHeight / 2 }),
|
||||
@@ -1874,7 +2030,11 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
|
||||
activeStatPoint ? h("div", { className: "codex-stats-tooltip active", style: activeTooltipStyle, "data-testid": "codex-stats-tooltip" },
|
||||
h("b", null, statDateLabel(activeStatPoint.row?.date)),
|
||||
h("span", null, `${activeStatPoint.label} · ${activeStatPoint.valueLabel}`),
|
||||
h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec / ${countNumber(activeStatPoint.row?.retryAttempts)} retry / ${fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)}`),
|
||||
h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec / ${countNumber(activeStatPoint.row?.retryAttempts)} retry / ${fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)}`),
|
||||
) : null,
|
||||
!hasRows ? h("div", { className: "codex-stats-chart-empty", "data-testid": "codex-stats-empty" },
|
||||
h("strong", null, statsStateLabel),
|
||||
h("span", null, chartDescription),
|
||||
) : null,
|
||||
h("div", { className: "codex-stats-legend" },
|
||||
h("span", { className: "tasks" }, "执行任务"),
|
||||
@@ -1897,16 +2057,21 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
|
||||
h("code", null, `${countNumber(activeStatPoint.row?.retryAttempts)} retry`),
|
||||
h("code", null, fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)),
|
||||
),
|
||||
) : h("span", null, "将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"),
|
||||
) : h("span", null, chartDescription),
|
||||
),
|
||||
) : h(EmptyState, { title: "暂无统计", text: "任务开始执行后会生成按天汇总的曲线。" }),
|
||||
),
|
||||
h("div", { className: "codex-stats-summary-grid" },
|
||||
h("article", null, h("span", null, "今日执行"), h("strong", null, String(countNumber(latest.executedTasks))), h("code", null, statDateLabel(latest.date))),
|
||||
h("article", null, h("span", null, "今日重试"), h("strong", null, String(countNumber(latest.retryAttempts))), h("code", null, `累计 ${countNumber(totals.retryAttempts)}`)),
|
||||
h("article", null, h("span", null, "平均耗时"), h("strong", null, fmtDuration(totals.avgDurationMs ?? undefined)), h("code", null, `${countNumber(totals.durationSamples)} samples`)),
|
||||
),
|
||||
h("div", { className: "codex-stats-daily-list", "data-testid": "codex-stats-daily-list" },
|
||||
rows.slice(-7).map((row: any) => h("div", { key: String(row?.date || ""), className: `codex-stats-daily-row ${activeDate === String(row?.date || "") ? "active" : ""}`, "data-testid": `codex-stats-day-${String(row?.date || "unknown")}` },
|
||||
rows.length === 0 ? h("div", { className: `codex-stats-daily-row empty ${statsState.state}`, "data-testid": "codex-stats-day-empty" },
|
||||
h("span", null, statsStateLabel),
|
||||
h("b", null, statsStateReason),
|
||||
h("b", null, "--"),
|
||||
h("code", null, statsState.state === "empty" ? "true empty" : "diagnostic"),
|
||||
) : rows.slice(-7).map((row: any) => h("div", { key: String(row?.date || ""), className: `codex-stats-daily-row ${activeDate === String(row?.date || "") ? "active" : ""}`, "data-testid": `codex-stats-day-${String(row?.date || "unknown")}` },
|
||||
h("span", null, statDateLabel(row?.date)),
|
||||
h("b", null, `${countNumber(row?.executedTasks)} exec`),
|
||||
h("b", null, `${countNumber(row?.retryAttempts)} retry`),
|
||||
@@ -2317,6 +2482,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
const tasksDataRef = useRef(initialTasksData);
|
||||
const [health, setHealth] = useState(null);
|
||||
const [tasksData, setTasksDataState] = useState(initialTasksData);
|
||||
const [statisticsData, setStatisticsData] = useState(objectRecord(initialTasksData?.statistics) as AnyRecord | null);
|
||||
const [selectedId, setSelectedId] = useState(initialSelectedId);
|
||||
const [selectedTask, setSelectedTask] = useState(initialSelectedTask);
|
||||
const [selectedDetailLoading, setSelectedDetailLoading] = useState(false);
|
||||
@@ -2372,7 +2538,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
const loadedUnreadTerminalTasks = tasks.filter(taskIsUnreadTerminal);
|
||||
const queue = tasksData?.queue || health?.body?.queue || health?.queue || {};
|
||||
const executionDiagnostics = executionDiagnosticsFromQueue(queue, health);
|
||||
const statistics = taskStatistics(tasksData, queue);
|
||||
const statistics = statisticsData || taskStatistics(tasksData, queue);
|
||||
const pagination = taskPagination(tasksData);
|
||||
const queueRows = knownQueueRows(queue, queueId);
|
||||
const mergeTargetQueueId = String(queueId || "default").trim() || "default";
|
||||
@@ -2408,7 +2574,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
const promptParts = useMemo(() => splitPromptTasks(prompt), [prompt]);
|
||||
const enqueueItems = useMemo(() => {
|
||||
const count = repeatCountValue(repeatCount);
|
||||
return promptParts.flatMap((text) => Array.from({ length: count }, () => withReferenceHint(text, referenceTaskId)));
|
||||
return promptParts.flatMap((text: string) => Array.from({ length: count }, () => withReferenceHint(text, referenceTaskId)));
|
||||
}, [promptParts, repeatCount, referenceTaskId]);
|
||||
const enqueueCount = enqueueItems.length;
|
||||
const batchNeedsConfirmation = enqueueCount > 1 && !batchConfirmed;
|
||||
@@ -2428,10 +2594,26 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
function setTasksData(nextOrUpdater: any): any {
|
||||
const next = typeof nextOrUpdater === "function" ? nextOrUpdater(tasksDataRef.current) : nextOrUpdater;
|
||||
tasksDataRef.current = next;
|
||||
const nextStats = objectRecord(next?.statistics);
|
||||
if (nextStats) setStatisticsData(nextStats);
|
||||
setTasksDataState(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
async function refreshStatistics(queueFilterId = selectedQueueId): Promise<void> {
|
||||
if (!service) return;
|
||||
try {
|
||||
const result = await loadTaskStatistics(apiBaseUrl, queueFilterId);
|
||||
const nextStats = objectRecord(result?.statistics) || objectRecord(result?.stats) || objectRecord(result);
|
||||
if (nextStats) setStatisticsData(nextStats);
|
||||
if (result?.queue) {
|
||||
setTasksData((previous: any) => previous ? { ...previous, queue: result.queue } : { ok: true, queue: result.queue, tasks: [], pagination: { returned: 0, total: 0, hasMore: false } });
|
||||
}
|
||||
} catch (err) {
|
||||
setStatisticsData(taskStatisticsUnavailable(err));
|
||||
}
|
||||
}
|
||||
|
||||
function rememberLocalReadState(taskIds: string[], readAt: string, hideFromSidebar = true): string[] {
|
||||
const ids = Array.from(new Set(taskIds.map((id) => String(id || "")).filter(Boolean)));
|
||||
for (const id of ids) {
|
||||
@@ -3183,6 +3365,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
}
|
||||
setRefreshedAt(new Date());
|
||||
if (trackLoad) trackedLoadInFlightRef.current = false;
|
||||
void refreshStatistics(queueFilterId).catch((err) => setStatisticsData(taskStatisticsUnavailable(err)));
|
||||
}
|
||||
|
||||
async function loadMoreTasks(): Promise<void> {
|
||||
@@ -3345,7 +3528,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
.filter(taskIsUnreadTerminal)
|
||||
.map((task: any) => String(task?.id || ""))
|
||||
.filter(Boolean),
|
||||
...Array.from(sessionCacheRef.current.entries())
|
||||
...(Array.from(sessionCacheRef.current.entries()) as [string, AnyRecord][])
|
||||
.filter(([, value]) => taskIsUnreadTerminal(value?.task))
|
||||
.map(([id]) => id),
|
||||
]));
|
||||
@@ -3359,7 +3542,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
.filter(taskIsUnreadTerminal)
|
||||
.map((task: any) => String(task?.id || ""))
|
||||
.filter(Boolean);
|
||||
const cachedUnreadIds = Array.from(sessionCacheRef.current.entries())
|
||||
const cachedUnreadIds = (Array.from(sessionCacheRef.current.entries()) as [string, AnyRecord][])
|
||||
.filter(([, value]) => taskIsUnreadTerminal(value?.task))
|
||||
.map(([id]) => id);
|
||||
const readIds = Array.from(new Set([...optimisticReadIds, ...loadedUnreadIds, ...cachedUnreadIds]));
|
||||
@@ -3900,6 +4083,12 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
return undefined;
|
||||
}, [service?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!service) return undefined;
|
||||
void refreshStatistics(selectedQueueId);
|
||||
return undefined;
|
||||
}, [service?.id, apiBaseUrl, selectedQueueId]);
|
||||
|
||||
const taskListContent = sidebarTasks.length === 0 ? h(EmptyState, {
|
||||
title: searchActive ? (searchLoading ? "搜索中" : "没有匹配任务") : "队列为空",
|
||||
text: searchActive ? (searchLoading ? `正在搜索包含“${normalizedSearchQuery}”的 task...` : `未找到包含“${normalizedSearchQuery}”的 task;可换个关键词或切换 queue。`) : "提交一个任务后,Codex 会串行执行并保存输出。",
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM ${CODE_QUEUE_BASE_IMAGE}
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
ENV UNIDESK_SKILLS_PATH=/root/.agents/skills
|
||||
|
||||
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1) \
|
||||
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && test -x "$PLAYWRIGHT_BROWSERS_PATH/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell") \
|
||||
|| (apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
|
||||
Reference in New Issue
Block a user