fix: sync code queue retry trace display
This commit is contained in:
+322
-1
@@ -155,6 +155,9 @@ const FRONTEND_CHECK_NAMES = [
|
||||
"frontend:code-queue-trace-full-load",
|
||||
"frontend:code-queue-judge-wrap",
|
||||
"frontend:code-queue-error-red-markers",
|
||||
"frontend:code-queue-retry-attempt-trace-current",
|
||||
"frontend:code-queue-step-missing-diagnostic",
|
||||
"frontend:code-queue-judge-feedback-attempt-order",
|
||||
"frontend:claudeqq-integrated-visible",
|
||||
"frontend:url-route-deeplink",
|
||||
"frontend:pipeline-integrated-visible",
|
||||
@@ -978,6 +981,288 @@ async function runCodeQueueLongPromptObservation(page: Page): Promise<any> {
|
||||
};
|
||||
}
|
||||
|
||||
async function runCodeQueueRetryTraceFixture(page: Page, frontendUrl: string): Promise<any> {
|
||||
const taskId = "codex_2000000000000_issue12_retry_fixture";
|
||||
const now = new Date().toISOString();
|
||||
const attempt1Started = "2026-05-20T03:00:00.000Z";
|
||||
const attempt1Finished = "2026-05-20T03:04:00.000Z";
|
||||
const attempt2Started = "2026-05-20T03:05:00.000Z";
|
||||
const traceRequests: string[] = [];
|
||||
const promptRequests: string[] = [];
|
||||
const task = {
|
||||
id: taskId,
|
||||
queueId: "issue12-fixture",
|
||||
queueEnteredAt: attempt2Started,
|
||||
displayPrompt: "Issue #12 retry current attempt fixture",
|
||||
basePrompt: "Issue #12 retry current attempt fixture",
|
||||
displayPromptPreview: "Issue #12 retry current attempt fixture",
|
||||
providerId: "D601-dev",
|
||||
executionMode: "default",
|
||||
cwd: "/workspace-dev",
|
||||
model: "gpt-5.5",
|
||||
maxAttempts: 99,
|
||||
status: "running",
|
||||
createdAt: "2026-05-20T02:59:00.000Z",
|
||||
updatedAt: now,
|
||||
startedAt: attempt1Started,
|
||||
finishedAt: null,
|
||||
currentAttempt: 2,
|
||||
currentMode: "retry",
|
||||
codexThreadId: "thread_issue12_fixture",
|
||||
activeTurnId: "turn_issue12_attempt_2",
|
||||
finalResponse: "",
|
||||
lastJudge: {
|
||||
decision: "retry",
|
||||
confidence: 0.82,
|
||||
reason: "Attempt 1 judge: missing live verification.",
|
||||
continuePrompt: "Continue with attempt 2 and gather live trace evidence.",
|
||||
source: "fixture",
|
||||
},
|
||||
stepCount: 0,
|
||||
llmStepCount: 0,
|
||||
outputCount: 0,
|
||||
eventCount: 0,
|
||||
attemptCount: 1,
|
||||
attempts: [{
|
||||
index: 1,
|
||||
mode: "initial",
|
||||
startedAt: attempt1Started,
|
||||
finishedAt: attempt1Finished,
|
||||
terminalStatus: "completed",
|
||||
finalResponsePreview: "Attempt 1 stopped after planning.",
|
||||
finalResponseChars: 33,
|
||||
judge: {
|
||||
decision: "retry",
|
||||
confidence: 0.82,
|
||||
reason: "Attempt 1 judge: missing live verification.",
|
||||
continuePrompt: "Continue with attempt 2 and gather live trace evidence.",
|
||||
source: "fixture",
|
||||
},
|
||||
feedbackPromptPreview: "Continue with attempt 2 and gather live trace evidence.",
|
||||
feedbackPromptChars: 56,
|
||||
feedbackPromptLines: 1,
|
||||
feedbackPromptSource: "judge-continue-prompt",
|
||||
feedbackPromptForAttempt: 2,
|
||||
statsSource: "unavailable",
|
||||
traceStats: null,
|
||||
execution: {
|
||||
durationMs: 240000,
|
||||
traceLineCount: 5,
|
||||
stepCount: 5,
|
||||
editedFiles: [],
|
||||
commands: ["bun test"],
|
||||
statsSource: "unavailable",
|
||||
traceStats: null,
|
||||
},
|
||||
}],
|
||||
summaryOnly: true,
|
||||
promptEditable: false,
|
||||
timing: { durationMs: 330000, totalElapsedMs: 390000 },
|
||||
};
|
||||
const summary = {
|
||||
id: taskId,
|
||||
taskId,
|
||||
queueId: "issue12-fixture",
|
||||
status: "running",
|
||||
providerId: "D601-dev",
|
||||
executionMode: "default",
|
||||
model: "gpt-5.5",
|
||||
cwd: "/workspace-dev",
|
||||
createdAt: task.createdAt,
|
||||
startedAt: task.startedAt,
|
||||
finishedAt: null,
|
||||
updatedAt: now,
|
||||
currentAttempt: 2,
|
||||
maxAttempts: 99,
|
||||
stepCount: null,
|
||||
llmStepCount: null,
|
||||
retainedStepCount: 0,
|
||||
traceStats: null,
|
||||
statsSource: "unavailable",
|
||||
prompt: {
|
||||
basePrompt: task.basePrompt,
|
||||
basePromptLines: 1,
|
||||
promptChars: task.basePrompt.length,
|
||||
promptLines: 1,
|
||||
},
|
||||
execution: {
|
||||
durationMs: 390000,
|
||||
traceLineCount: 0,
|
||||
stepCount: null,
|
||||
editedFiles: [],
|
||||
commands: [],
|
||||
statsSource: "unavailable",
|
||||
traceStats: null,
|
||||
},
|
||||
finalResponse: "",
|
||||
lastJudge: task.lastJudge,
|
||||
attempts: task.attempts,
|
||||
timing: task.timing,
|
||||
};
|
||||
const overview = {
|
||||
ok: true,
|
||||
queue: {
|
||||
activeTaskId: taskId,
|
||||
activeTaskIds: [taskId],
|
||||
total: 1,
|
||||
unreadTerminal: 0,
|
||||
counts: { running: 1, queued: 0, retry_wait: 0, judging: 0 },
|
||||
queues: [{ id: "issue12-fixture", name: "Issue 12 Fixture", total: 1, activeTaskId: taskId, counts: { running: 1 } }],
|
||||
mainProviderId: "D601-dev",
|
||||
defaultProviderId: "D601-dev",
|
||||
defaultWorkdir: "/workspace-dev",
|
||||
defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" },
|
||||
codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"],
|
||||
executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }],
|
||||
executionModes: [{ id: "default", label: "默认容器/本机" }],
|
||||
},
|
||||
statistics: { timezone: "Asia/Shanghai", range: { startDate: "2026-05-20", endDate: "2026-05-20" }, totals: {}, daily: [] },
|
||||
tasks: [task],
|
||||
pagination: { returned: 1, total: 1, hasMore: false },
|
||||
selected: { task, transcript: [], hasMore: false, preview: true },
|
||||
};
|
||||
const attempt2Steps = [
|
||||
{
|
||||
seq: 201,
|
||||
at: "2026-05-20T03:05:20.000Z",
|
||||
kind: "message",
|
||||
title: "Assistant message",
|
||||
status: "streaming",
|
||||
rawSeqs: [201],
|
||||
summaryLines: ["Attempt 2 running: fixture trace is visible."],
|
||||
hasDetail: true,
|
||||
},
|
||||
{
|
||||
seq: 202,
|
||||
at: "2026-05-20T03:05:40.000Z",
|
||||
kind: "ran",
|
||||
title: "Ran bun --check target files",
|
||||
status: "running",
|
||||
rawSeqs: [202],
|
||||
summaryLines: ["Attempt 2 running command trace should appear before old judge."],
|
||||
hasDetail: true,
|
||||
},
|
||||
];
|
||||
const routePattern = "**/api/microservices/code-queue/proxy/api/**";
|
||||
const handler = async (route: any, request: any): Promise<void> => {
|
||||
if (request.method() !== "GET") {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, "");
|
||||
if (path === "/api/tasks/overview") {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) });
|
||||
return;
|
||||
}
|
||||
if (path === "/api/workdirs") {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) });
|
||||
return;
|
||||
}
|
||||
if (path === "/api/queues") {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) });
|
||||
return;
|
||||
}
|
||||
if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary }) });
|
||||
return;
|
||||
}
|
||||
if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-steps` || path === `/api/tasks/${taskId}/trace-steps`) {
|
||||
traceRequests.push(url.search);
|
||||
const attempt = url.searchParams.get("attempt") || "";
|
||||
const steps = attempt === "2" ? attempt2Steps : [];
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, taskId, attempt: attempt ? Number(attempt) : null, source: "fixture", steps, total: steps.length, hasMore: false, nextAfterSeq: steps.at(-1)?.seq || 0 }) });
|
||||
return;
|
||||
}
|
||||
if (path === `/api/tasks/${encodeURIComponent(taskId)}/prompt` || path === `/api/tasks/${taskId}/prompt`) {
|
||||
promptRequests.push(url.search);
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, part: url.searchParams.get("part"), attempt: Number(url.searchParams.get("attempt") || 0), text: "Continue with attempt 2 and gather live trace evidence.", chars: 56, lines: 1, forAttempt: 2 }) });
|
||||
return;
|
||||
}
|
||||
await route.continue();
|
||||
};
|
||||
|
||||
await page.route(routePattern, handler);
|
||||
try {
|
||||
await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000 });
|
||||
await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 15000 });
|
||||
await page.waitForSelector('[data-testid="codex-execution-summary-current"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => {
|
||||
const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLDetailsElement | null;
|
||||
const text = current?.textContent || "";
|
||||
return current?.getAttribute("data-attempt-index") === "2"
|
||||
&& current?.getAttribute("data-running") === "true"
|
||||
&& text.includes("Attempt 2 running");
|
||||
}, undefined, { timeout: 15000 });
|
||||
const beforeExpand = await page.evaluate((id) => {
|
||||
const card = document.querySelector(`[data-testid="codex-task-${id}"]`) as HTMLElement | null;
|
||||
const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLElement | null;
|
||||
const attempt1Judge = document.querySelector('[data-testid="codex-progressive-judge-attempt-1"]') as HTMLElement | null;
|
||||
const feedback = document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLElement | null;
|
||||
const currentStep = document.querySelector('[data-testid="codex-execution-summary-current-step-count"]') as HTMLElement | null;
|
||||
const root = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null;
|
||||
const order = ["codex-execution-summary-current", "codex-progressive-judge-attempt-1", "codex-judge-feedback-prompt-attempt-1"]
|
||||
.map((testId) => {
|
||||
const element = document.querySelector(`[data-testid="${testId}"]`);
|
||||
return element && root ? Array.prototype.indexOf.call(root.querySelectorAll("[data-testid]"), element) : -1;
|
||||
});
|
||||
return {
|
||||
cardText: card?.innerText || "",
|
||||
cardAttemptVisible: (card?.innerText || "").includes("2/99"),
|
||||
cardStepText: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.textContent || "",
|
||||
cardStepState: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.getAttribute("data-step-state") || "",
|
||||
currentText: current?.textContent || "",
|
||||
currentAttempt: current?.getAttribute("data-attempt-index") || "",
|
||||
currentRunning: current?.getAttribute("data-running") || "",
|
||||
currentStepText: currentStep?.textContent || "",
|
||||
currentStepState: currentStep?.getAttribute("data-step-state") || "",
|
||||
attempt1JudgeText: attempt1Judge?.textContent || "",
|
||||
feedbackText: feedback?.textContent || "",
|
||||
currentBeforeJudge: order[0] >= 0 && order[1] >= 0 && order[0] < order[1],
|
||||
judgeBeforeFeedback: order[1] >= 0 && order[2] >= 0 && order[1] < order[2],
|
||||
currentJudgeMissing: document.querySelector('[data-testid="codex-progressive-judge-current"]') === null,
|
||||
legacyCurrentAliasMissing: document.querySelector('[data-testid="codex-progressive-judge"]') === null || (document.querySelector('[data-testid="codex-progressive-judge"]') as HTMLElement | null)?.getAttribute("data-attempt-index") !== "2",
|
||||
};
|
||||
}, taskId);
|
||||
await page.locator('[data-testid="codex-judge-feedback-prompt-attempt-1"] summary').click();
|
||||
await page.waitForFunction(() => (document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLDetailsElement | null)?.open === true, undefined, { timeout: 5000 });
|
||||
const feedbackText = await page.getByTestId("codex-judge-feedback-prompt-attempt-1-text").innerText({ timeout: 5000 });
|
||||
const currentStepRows = await page.locator('[data-testid="codex-execution-summary-current"] .codex-trace-step').count();
|
||||
return {
|
||||
checked: true,
|
||||
taskId,
|
||||
fixture: {
|
||||
cardAttempt: "2/99",
|
||||
summaryAttempts: summary.attempts.length,
|
||||
attempt2Steps: attempt2Steps.length,
|
||||
},
|
||||
beforeExpand,
|
||||
currentStepRows,
|
||||
traceRequests,
|
||||
promptRequests,
|
||||
feedbackText,
|
||||
ok: beforeExpand.cardAttemptVisible === true
|
||||
&& beforeExpand.currentAttempt === "2"
|
||||
&& beforeExpand.currentRunning === "true"
|
||||
&& beforeExpand.currentText.includes("Attempt 2 running")
|
||||
&& beforeExpand.currentBeforeJudge === true
|
||||
&& beforeExpand.judgeBeforeFeedback === true
|
||||
&& beforeExpand.currentJudgeMissing === true
|
||||
&& beforeExpand.cardStepText !== "STEP --"
|
||||
&& beforeExpand.currentStepText !== "STEP --"
|
||||
&& ["syncing", "raw-trace"].includes(beforeExpand.cardStepState)
|
||||
&& ["syncing", "raw-trace"].includes(beforeExpand.currentStepState)
|
||||
&& currentStepRows >= 2
|
||||
&& traceRequests.some((search) => search.includes("attempt=2"))
|
||||
&& promptRequests.some((search) => search.includes("part=feedback") && search.includes("attempt=1"))
|
||||
&& feedbackText.includes("Continue with attempt 2"),
|
||||
};
|
||||
} finally {
|
||||
await page.unroute(routePattern, handler).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } {
|
||||
const result = runCommand([
|
||||
"docker",
|
||||
@@ -1885,6 +2170,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
"frontend:code-queue-trace-full-load",
|
||||
"frontend:code-queue-judge-wrap",
|
||||
"frontend:code-queue-error-red-markers",
|
||||
"frontend:code-queue-retry-attempt-trace-current",
|
||||
"frontend:code-queue-step-missing-diagnostic",
|
||||
"frontend:code-queue-judge-feedback-attempt-order",
|
||||
]);
|
||||
const needCodeQueueFullSurface = wantsAny([
|
||||
"frontend:code-queue-integrated-visible",
|
||||
@@ -1895,6 +2183,11 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
"frontend:code-queue-judge-wrap",
|
||||
"frontend:code-queue-error-red-markers",
|
||||
]);
|
||||
const needCodeQueueRetryTraceFixture = wantsAny([
|
||||
"frontend:code-queue-retry-attempt-trace-current",
|
||||
"frontend:code-queue-step-missing-diagnostic",
|
||||
"frontend:code-queue-judge-feedback-attempt-order",
|
||||
]);
|
||||
const needClaudeqq = wants("frontend:claudeqq-integrated-visible");
|
||||
const needRouteDeepLink = wants("frontend:url-route-deeplink");
|
||||
const needPipeline = wantsAny([
|
||||
@@ -2004,6 +2297,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
let codexTraceFullMetrics: any = { candidateFound: false };
|
||||
let codexJudgeWrapMetrics: any = { checked: false };
|
||||
let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false };
|
||||
let codeQueueRetryTraceFixtureMetrics: any = { checked: false };
|
||||
let claudeqqText = "";
|
||||
let routeDeepLinkText = "";
|
||||
let routeInitialPath = "";
|
||||
@@ -3002,10 +3296,13 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
const cards = Array.from(document.querySelectorAll('[data-testid^="codex-task-codex_"]')).map((node) => (node as HTMLElement).innerText);
|
||||
return cards.length === 0 || cards.every((text) => text.includes(`queue=${queueId}`));
|
||||
}, targetQueueId, { timeout: 15000 });
|
||||
codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true };
|
||||
codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true };
|
||||
await page.getByTestId("code-queue-filter-select").selectOption("__all__");
|
||||
}
|
||||
}
|
||||
if (needCodeQueueRetryTraceFixture) {
|
||||
codeQueueRetryTraceFixtureMetrics = await runCodeQueueRetryTraceFixture(page, urls.frontendUrl);
|
||||
}
|
||||
if (needClaudeqq) {
|
||||
await page.getByRole("button", { name: /ClaudeQQ/ }).click();
|
||||
await page.waitForSelector('[data-testid="claudeqq-page"]', { timeout: 10000 });
|
||||
@@ -3664,6 +3961,30 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
|
||||
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
|
||||
{ codexJudgeWrapMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current",
|
||||
codeQueueRetryTraceFixtureMetrics.checked === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.ok === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardAttemptVisible === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentAttempt === "2"
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentRunning === "true"
|
||||
&& Number(codeQueueRetryTraceFixtureMetrics.currentStepRows || 0) >= 2
|
||||
&& (codeQueueRetryTraceFixtureMetrics.traceRequests || []).some((search: string) => String(search).includes("attempt=2")),
|
||||
{ codeQueueRetryTraceFixtureMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-step-missing-diagnostic",
|
||||
codeQueueRetryTraceFixtureMetrics.checked === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepText !== "STEP --"
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepText !== "STEP --"
|
||||
&& ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepState || ""))
|
||||
&& ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepState || "")),
|
||||
{ codeQueueRetryTraceFixtureMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-judge-feedback-attempt-order",
|
||||
codeQueueRetryTraceFixtureMetrics.checked === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentBeforeJudge === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.judgeBeforeFeedback === true
|
||||
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentJudgeMissing === true
|
||||
&& String(codeQueueRetryTraceFixtureMetrics.feedbackText || "").includes("Continue with attempt 2")
|
||||
&& (codeQueueRetryTraceFixtureMetrics.promptRequests || []).some((search: string) => String(search).includes("part=feedback") && String(search).includes("attempt=1")),
|
||||
{ codeQueueRetryTraceFixtureMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("D601 k3s Service") && claudeqqText.includes("k3s://unidesk/claudeqq") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) });
|
||||
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2983,6 +2983,22 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
background: rgba(78, 183, 168, 0.08);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.codex-task-step-count.syncing,
|
||||
.codex-task-step-count.unavailable {
|
||||
border-color: rgba(215, 161, 58, 0.38);
|
||||
color: #f0cf8a;
|
||||
background: rgba(215, 161, 58, 0.08);
|
||||
}
|
||||
.codex-task-step-count.failed {
|
||||
border-color: rgba(207, 106, 84, 0.52);
|
||||
color: var(--danger);
|
||||
background: rgba(207, 106, 84, 0.08);
|
||||
}
|
||||
.codex-task-step-count.fallback {
|
||||
border-color: rgba(148, 190, 255, 0.40);
|
||||
color: #b9d0ff;
|
||||
background: rgba(148, 190, 255, 0.08);
|
||||
}
|
||||
.codex-output-panel .panel-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -681,6 +681,82 @@ function canonicalTaskStepCount(task: any): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function rawTraceStepCountFallback(...values: any[]): number | null {
|
||||
for (const value of values) {
|
||||
const count = statCount(value);
|
||||
if (count !== null) return count;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function traceStepCountLabel(state: AnyRecord): string {
|
||||
if (state.state === "ready" || state.state === "fallback") return `STEP ${state.count}${state.state === "fallback" ? " raw" : ""}`;
|
||||
if (state.state === "syncing") return "STEP sync";
|
||||
if (state.state === "failed") return "STEP failed";
|
||||
return "STEP N/A";
|
||||
}
|
||||
|
||||
function traceStepCountState(task: any): AnyRecord {
|
||||
const oaStats = taskOaTraceStats(task);
|
||||
const oaCount = statCount(oaStats?.stepCount ?? oaStats?.llmStepCount);
|
||||
if (oaCount !== null) {
|
||||
return {
|
||||
state: "ready",
|
||||
count: oaCount,
|
||||
label: `STEP ${oaCount}`,
|
||||
title: "STEP 来自 OA Event Flow 统计中心",
|
||||
source: "oa-event-flow",
|
||||
};
|
||||
}
|
||||
const summary = taskTraceSummary(task);
|
||||
const activeWithoutRawRows = taskIsActive(task)
|
||||
&& task?._traceSummaryLoaded !== true
|
||||
&& statCount(task?.outputCount ?? task?.retainedOutputCount) === 0;
|
||||
const rawCount = rawTraceStepCountFallback(
|
||||
summary?.retainedStepCount,
|
||||
summary?.execution?.traceLineCount,
|
||||
summary?.execution?.stepCount,
|
||||
task?.outputCount,
|
||||
task?.retainedOutputCount,
|
||||
activeWithoutRawRows ? null : task?.stepCount,
|
||||
activeWithoutRawRows ? null : task?.llmStepCount,
|
||||
);
|
||||
if (rawCount !== null) {
|
||||
return {
|
||||
state: "fallback",
|
||||
count: rawCount,
|
||||
label: `STEP ${rawCount} raw`,
|
||||
title: "OA STEP 统计不可用,当前显示 raw trace fallback 行数",
|
||||
source: "raw-trace",
|
||||
};
|
||||
}
|
||||
if (summary?.traceStatsError || task?.traceStatsError) {
|
||||
return {
|
||||
state: "failed",
|
||||
count: null,
|
||||
label: "STEP failed",
|
||||
title: "STEP 统计读取失败;请查看 trace summary 原始 JSON",
|
||||
source: "stats-error",
|
||||
};
|
||||
}
|
||||
if (taskIsActive(task) || task?._traceSummaryLoaded !== true) {
|
||||
return {
|
||||
state: "syncing",
|
||||
count: null,
|
||||
label: "STEP sync",
|
||||
title: "STEP 统计同步中;Trace Summary 或 OA 统计尚未返回",
|
||||
source: "syncing",
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: "unavailable",
|
||||
count: null,
|
||||
label: "STEP N/A",
|
||||
title: "当前任务没有可用 STEP 统计或 raw trace fallback",
|
||||
source: "unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function traceSummaryStepCount(summary: AnyRecord | null): number | null {
|
||||
const stats = objectRecord(summary?.traceStats);
|
||||
if (!stats || (summary?.statsSource !== "oa-event-flow" && stats.source !== "oa-event-flow")) return null;
|
||||
@@ -764,12 +840,12 @@ function JudgeFailureDetails({ judge, testId = "codex-judge-failure-details" }:
|
||||
|
||||
function taskProgressiveAttempts(task: any): any[] {
|
||||
const summaryAttempts = taskTraceSummary(task)?.attempts;
|
||||
if (Array.isArray(summaryAttempts) && summaryAttempts.length > 0) return summaryAttempts;
|
||||
if (Array.isArray(summaryAttempts) && summaryAttempts.length > 0) return currentFirstAttempts(task, withCurrentAttemptSegment(task, summaryAttempts));
|
||||
const execution = taskExecutionSummary(task);
|
||||
const finalResponse = taskFinalResponseText(task);
|
||||
const judge = taskLastJudge(task);
|
||||
if (Object.keys(execution).length === 0 && finalResponse.length === 0 && judge === null) return [];
|
||||
return [{
|
||||
if (Object.keys(execution).length === 0 && finalResponse.length === 0 && judge === null) return currentFirstAttempts(task, withCurrentAttemptSegment(task, []));
|
||||
return currentFirstAttempts(task, withCurrentAttemptSegment(task, [{
|
||||
index: Number(task?.currentAttempt || 1),
|
||||
mode: task?.currentMode || "initial",
|
||||
startedAt: task?.startedAt,
|
||||
@@ -779,7 +855,56 @@ function taskProgressiveAttempts(task: any): any[] {
|
||||
finalResponse,
|
||||
finalResponseChars: finalResponse.length,
|
||||
judge,
|
||||
}];
|
||||
}]));
|
||||
}
|
||||
|
||||
function attemptIsCurrent(task: any, attemptIndex: any): boolean {
|
||||
const summary = taskTraceSummary(task);
|
||||
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
|
||||
const numericAttempt = Number(attemptIndex);
|
||||
return Number.isFinite(currentAttempt) && currentAttempt > 0 && Number.isFinite(numericAttempt) && numericAttempt === currentAttempt;
|
||||
}
|
||||
|
||||
function attemptIsActiveCurrent(task: any, attemptIndex: any): boolean {
|
||||
return taskIsActive(task) && attemptIsCurrent(task, attemptIndex);
|
||||
}
|
||||
|
||||
function withCurrentAttemptSegment(task: any, attempts: any[]): any[] {
|
||||
const rows = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
|
||||
const summary = taskTraceSummary(task);
|
||||
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
|
||||
if (!taskIsActive(task) || !Number.isFinite(currentAttempt) || currentAttempt <= 0) return rows;
|
||||
if (rows.some((attempt) => Number(attempt?.index) === currentAttempt)) return rows;
|
||||
const activeAt = latestTimestampValue(task?.startedAt, summary?.startedAt, task?.updatedAt, summary?.updatedAt);
|
||||
return [
|
||||
...rows,
|
||||
{
|
||||
index: currentAttempt,
|
||||
mode: task?.currentMode || summary?.currentMode || (currentAttempt <= 1 ? "initial" : "retry"),
|
||||
startedAt: activeAt || task?.startedAt || summary?.startedAt,
|
||||
finishedAt: null,
|
||||
terminalStatus: null,
|
||||
execution: {},
|
||||
finalResponse: "",
|
||||
finalResponsePreview: "",
|
||||
finalResponseChars: 0,
|
||||
judge: null,
|
||||
currentPlaceholder: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function currentFirstAttempts(task: any, attempts: any[]): any[] {
|
||||
const rows = [...(Array.isArray(attempts) ? attempts : [])];
|
||||
if (!taskIsActive(task)) return rows;
|
||||
return rows.sort((left, right) => {
|
||||
const leftIndex = Number(left?.index);
|
||||
const rightIndex = Number(right?.index);
|
||||
const leftCurrent = attemptIsCurrent(task, leftIndex) ? 0 : 1;
|
||||
const rightCurrent = attemptIsCurrent(task, rightIndex) ? 0 : 1;
|
||||
if (leftCurrent !== rightCurrent) return leftCurrent - rightCurrent;
|
||||
return leftIndex - rightIndex;
|
||||
});
|
||||
}
|
||||
|
||||
function attemptExecutionSummary(task: any, attempt: any): AnyRecord {
|
||||
@@ -807,11 +932,13 @@ function attemptExecutionUpdatedAt(task: any, attempt: any, attemptIndex: any, e
|
||||
|
||||
function attemptFinalResponseText(task: any, attempt: any): string {
|
||||
const text = String(attempt?.finalResponse || attempt?.finalResponsePreview || "");
|
||||
if (attemptIsActiveCurrent(task, attempt?.index)) return "";
|
||||
if (Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponse") || Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponsePreview")) return text.trimEnd();
|
||||
return text.length > 0 ? text.trimEnd() : taskFinalResponseText(task);
|
||||
}
|
||||
|
||||
function attemptJudge(task: any, attempt: any): AnyRecord | null {
|
||||
function attemptJudge(task: any, attempt: any, attemptIndex: any = attempt?.index): AnyRecord | null {
|
||||
if (attemptIsActiveCurrent(task, attemptIndex)) return null;
|
||||
if (Object.prototype.hasOwnProperty.call(attempt || {}, "judge")) return objectRecord(attempt?.judge);
|
||||
return taskLastJudge(task);
|
||||
}
|
||||
@@ -821,12 +948,9 @@ function attemptExecutionIsRunning(task: any, attempt: any, attemptIndex: any):
|
||||
if (isSyntheticAttemptSegment(attempt, attemptIndex)) return false;
|
||||
if (attempt?.finishedAt) return false;
|
||||
if (["succeeded", "failed", "canceled"].includes(String(attempt?.terminalStatus || ""))) return false;
|
||||
const summary = taskTraceSummary(task);
|
||||
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
|
||||
const numericAttempt = Number(attemptIndex);
|
||||
if (Number.isFinite(numericAttempt) && numericAttempt > 0 && Number.isFinite(currentAttempt) && currentAttempt > 0) {
|
||||
return numericAttempt === currentAttempt;
|
||||
}
|
||||
if (attemptIsCurrent(task, attemptIndex)) return true;
|
||||
const currentAttempt = Number(taskTraceSummary(task)?.currentAttempt || task?.currentAttempt || 0);
|
||||
if (Number.isFinite(currentAttempt) && currentAttempt > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -850,7 +974,7 @@ function attemptFeedbackPrompt(task: any, attempt: any, attemptIndex: any): AnyR
|
||||
truncated: Boolean(attempt?.feedbackPromptTruncated),
|
||||
};
|
||||
}
|
||||
const judge = attemptJudge(task, attempt);
|
||||
const judge = attemptJudge(task, attempt, attemptIndex);
|
||||
const continuePrompt = String(judge?.continuePrompt || "").trimEnd();
|
||||
if (judge?.decision === "retry" && continuePrompt.length > 0) {
|
||||
return {
|
||||
@@ -1485,9 +1609,8 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
|
||||
const unread = taskIsUnreadTerminal(task);
|
||||
const updatedAt = latestTimestampValue(task?.updatedAt, taskTraceSummary(task)?.updatedAt);
|
||||
const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`;
|
||||
const stepCount = taskStepCount(task);
|
||||
const stepLabel = stepCount === null ? "--" : String(stepCount);
|
||||
const stepTitle = stepCount === null ? "STEP 统计中心同步中" : "STEP 来自 OA Event Flow 统计中心";
|
||||
const stepState = traceStepCountState(task);
|
||||
const stepTitle = stepState.title;
|
||||
return h("article", {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
@@ -1551,7 +1674,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
|
||||
h("span", null, taskDurationLabel(task)),
|
||||
),
|
||||
h("div", { className: "codex-task-meta codex-task-update-meta" },
|
||||
h("span", { className: "codex-task-recent-update codex-task-step-count", title: stepTitle, "data-testid": `codex-task-step-count-${taskId || "unknown"}` }, `STEP ${stepLabel}`),
|
||||
h("span", { className: `codex-task-recent-update codex-task-step-count ${stepState.state}`, title: stepTitle, "data-testid": `codex-task-step-count-${taskId || "unknown"}`, "data-step-state": stepState.state, "data-step-source": stepState.source }, traceStepCountLabel(stepState)),
|
||||
h("span", { className: "codex-task-recent-update", title: updatedAt ? `更新时间: ${fmtDate(updatedAt)}` : recentUpdateLabel, "data-testid": `codex-task-recent-update-${taskId || "unknown"}` }, recentUpdateLabel),
|
||||
h("span", null, fmtDate(updatedAt || task?.updatedAt)),
|
||||
),
|
||||
@@ -1871,13 +1994,30 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
|
||||
const stepDetails = taskTraceStepDetails(task);
|
||||
const stepsLoaded = taskTraceStepsLoaded(task, attemptIndex);
|
||||
const errorCount = statCount(stats?.errorCount);
|
||||
const toolCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
|
||||
const fallbackStepCount = rawTraceStepCountFallback(
|
||||
steps.length > 0 ? steps.length : null,
|
||||
attempt?.stepCount,
|
||||
attempt?.execution?.stepCount,
|
||||
execution.traceLineCount,
|
||||
execution.stepCount,
|
||||
attempt?.outputCount,
|
||||
);
|
||||
const statsStepCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
|
||||
const toolCount = statsStepCount ?? fallbackStepCount;
|
||||
const readCount = statCount(stats?.readCount);
|
||||
const editCount = statCount(stats?.editCount);
|
||||
const runCount = statCount(stats?.runCount);
|
||||
const stepCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
|
||||
const toolsLabel = toolCount === null ? "--" : String(toolCount);
|
||||
const stepLabel = stepCount === null ? "--" : String(stepCount);
|
||||
const stepCount = statsStepCount ?? fallbackStepCount;
|
||||
const countSource = statsStepCount !== null ? "oa-event-flow" : fallbackStepCount !== null ? "raw-trace" : taskIsActive(task) ? "syncing" : "unavailable";
|
||||
const toolsLabel = toolCount === null ? (countSource === "syncing" ? "sync" : "N/A") : String(toolCount);
|
||||
const stepLabel = stepCount === null ? (countSource === "syncing" ? "sync" : "N/A") : `${stepCount}${countSource === "raw-trace" ? " raw" : ""}`;
|
||||
const stepTitle = countSource === "oa-event-flow"
|
||||
? "STEP 来自 OA Event Flow 统计中心"
|
||||
: countSource === "raw-trace"
|
||||
? "OA STEP 统计不可用,当前显示 raw trace fallback 行数"
|
||||
: countSource === "syncing"
|
||||
? "STEP 统计同步中;展开后会按需读取当前 attempt trace"
|
||||
: "当前 attempt 没有可用 STEP 统计";
|
||||
const editedFiles = Array.isArray(execution.editedFiles) ? execution.editedFiles : [];
|
||||
const commands = Array.isArray(execution.commands) ? execution.commands : [];
|
||||
const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex);
|
||||
@@ -1899,14 +2039,14 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
|
||||
h("span", { className: "codex-output-channel" }, "Summary"),
|
||||
h("strong", null, `执行过程摘要${labelSuffix}`),
|
||||
running ? h("span", { className: "codex-summary-running-pill", "data-testid": `${testId}-running` }, "执行中") : null,
|
||||
h("code", { title: updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel },
|
||||
h("code", { title: `${updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel};${stepTitle}` },
|
||||
`${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${toolsLabel} tools / ${recentUpdateLabel}`),
|
||||
),
|
||||
h("div", { className: "codex-execution-digest" },
|
||||
h("span", { title: "来自 OA Event Flow 统计中心" }, `read ${readCount === null ? "--" : readCount}`),
|
||||
h("span", { title: "来自 OA Event Flow 统计中心" }, `edit ${editCount === null ? "--" : editCount}`),
|
||||
h("span", { title: "来自 OA Event Flow 统计中心" }, `run ${runCount === null ? "--" : runCount}`),
|
||||
h("span", { title: "来自 OA Event Flow 统计中心" }, `STEP ${stepLabel}`),
|
||||
h("span", { title: stepTitle, "data-testid": `${testId}-step-count`, "data-step-state": countSource }, `STEP ${stepLabel}`),
|
||||
errorCount !== null && errorCount > 0 ? h("span", { className: "codex-execution-error-pill", "data-testid": `${testId}-error-count` }, `Error ${errorCount}`) : null,
|
||||
),
|
||||
),
|
||||
@@ -1982,7 +2122,7 @@ function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex
|
||||
}
|
||||
|
||||
function ProgressiveJudge({ task, attempt, attemptIndex, testId = "codex-progressive-judge" }: AnyRecord) {
|
||||
const judge = attemptJudge(task, attempt);
|
||||
const judge = attemptJudge(task, attempt, attemptIndex);
|
||||
if (!judge?.decision) return null;
|
||||
const labelSuffix = attemptIndex ? ` #${attemptIndex}` : "";
|
||||
return h("section", { className: "codex-progressive-card codex-progressive-judge", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) },
|
||||
@@ -2036,7 +2176,8 @@ function ProgressiveJudgeFeedbackPrompt({ task, attempt, attemptIndex, loading,
|
||||
|
||||
function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromptPart, onLoadSteps, onLoadStep }: AnyRecord) {
|
||||
const attemptIndex = attemptSegmentIndex(attempt, position);
|
||||
const first = position === 0;
|
||||
const current = attemptIsActiveCurrent(task, attemptIndex);
|
||||
const first = position === 0 && !current;
|
||||
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}` },
|
||||
@@ -2053,19 +2194,19 @@ function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromp
|
||||
loading,
|
||||
onLoadSteps,
|
||||
onLoadStep,
|
||||
testId: first ? "codex-execution-summary" : `codex-execution-summary-attempt-${attemptIndex}`,
|
||||
testId: current ? "codex-execution-summary-current" : first ? "codex-execution-summary" : `codex-execution-summary-attempt-${attemptIndex}`,
|
||||
}),
|
||||
synthetic ? null : h(ProgressiveFinalResponse, {
|
||||
task,
|
||||
attempt,
|
||||
attemptIndex,
|
||||
testId: first ? "codex-final-response" : `codex-final-response-attempt-${attemptIndex}`,
|
||||
testId: current ? "codex-final-response-current" : first ? "codex-final-response" : `codex-final-response-attempt-${attemptIndex}`,
|
||||
}),
|
||||
synthetic ? null : h(ProgressiveJudge, {
|
||||
task,
|
||||
attempt,
|
||||
attemptIndex,
|
||||
testId: first ? "codex-progressive-judge" : `codex-progressive-judge-attempt-${attemptIndex}`,
|
||||
testId: current ? "codex-progressive-judge-current" : first ? "codex-progressive-judge" : `codex-progressive-judge-attempt-${attemptIndex}`,
|
||||
}),
|
||||
synthetic ? null : h(ProgressiveJudgeFeedbackPrompt, {
|
||||
task,
|
||||
@@ -2073,7 +2214,7 @@ function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromp
|
||||
attemptIndex,
|
||||
loading,
|
||||
onLoadPromptPart,
|
||||
testId: first ? "codex-judge-feedback-prompt" : `codex-judge-feedback-prompt-attempt-${attemptIndex}`,
|
||||
testId: current ? "codex-judge-feedback-prompt-current" : first ? "codex-judge-feedback-prompt" : `codex-judge-feedback-prompt-attempt-${attemptIndex}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -3682,7 +3823,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
if (selectedIdRef.current === taskId && (normalizedEvent?.type === "trace-step-created" || normalizedEvent?.type === "task-updated" || normalizedEvent?.type === "trace-stats-updated")) {
|
||||
const previousStepCount = canonicalTaskStepCount(previousTask);
|
||||
const shouldRefreshSteps = normalizedEvent?.type === "trace-step-created"
|
||||
|| (normalizedEvent?.type === "trace-stats-updated" && !isAttemptStats && Number.isFinite(Number(normalizedEvent?.stepCount)) && (previousStepCount === null || stepCount > previousStepCount));
|
||||
|| (normalizedEvent?.type === "trace-stats-updated" && Number.isFinite(Number(normalizedEvent?.stepCount)) && (isAttemptStats || previousStepCount === null || stepCount > previousStepCount));
|
||||
scheduleSelectedTraceEventRefresh(taskId, shouldRefreshSteps);
|
||||
}
|
||||
}
|
||||
@@ -3741,6 +3882,18 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
void ensureTraceSummary(taskId, true).catch((err) => setError(errorText(err, "自动加载 Trace Summary 失败")));
|
||||
}, [service?.id, selectedTask?.id, selectedTask?.updatedAt, selectedTask?.traceStats?.statsRevision, selectedTask?._traceSummaryUpdatedAt, selectedTask?._traceSummaryLoaded, selectedDetailLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!service || !selectedTask || selectedDetailLoading) return;
|
||||
const taskId = String(selectedTask.id || "");
|
||||
const currentAttempt = Number(taskTraceSummary(selectedTask)?.currentAttempt || selectedTask?.currentAttempt || 0);
|
||||
if (!taskId || !taskIsExecuting(selectedTask) || !Number.isFinite(currentAttempt) || currentAttempt <= 0) return;
|
||||
if (taskTraceStepsLoaded(selectedTask, currentAttempt)) return;
|
||||
const key = `${taskId}:current-attempt:${currentAttempt}:${String(selectedTask?.updatedAt || "")}`;
|
||||
if (autoTraceLoadKeysRef.current.has(key)) return;
|
||||
autoTraceLoadKeysRef.current.add(key);
|
||||
void ensureTraceSteps(currentAttempt).catch((err) => setError(errorText(err, "自动加载当前 Attempt Trace Steps 失败")));
|
||||
}, [service?.id, selectedTask?.id, selectedTask?.status, selectedTask?.currentAttempt, selectedTask?.updatedAt, selectedTask?._traceSummaryUpdatedAt, selectedDetailLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!service) return undefined;
|
||||
void loadWorkdirs().catch((err) => setError(errorText(err, "加载工作目录失败")));
|
||||
|
||||
Reference in New Issue
Block a user