Merge pull request #332 from pikasTech/fix/code-agent-timeout-224

fix: classify Code Agent timeout failures
This commit is contained in:
Lyon
2026-05-23 19:17:10 +08:00
committed by GitHub
10 changed files with 449 additions and 29 deletions
+52
View File
@@ -1397,6 +1397,58 @@ test("cloud api /v1/agent/chat accepts provider success delayed beyond legacy 45
}
});
test("cloud api /v1/agent/chat keeps delayed provider failure structured beyond legacy 4500ms UI timeout", async () => {
const server = createCloudApiServer({
env: {
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_PROVIDER: "openai",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1/provider-fixture"
},
callCodeAgentProvider: async ({ providerPlan }) => {
await delay(4600);
const error = new Error("OpenAI Responses returned HTTP 503: slow upstream rejected");
error.code = "provider_unavailable";
error.provider = providerPlan.provider;
error.model = providerPlan.model;
error.backend = providerPlan.backend;
error.providerStatus = 503;
throw error;
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const startedAt = Date.now();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_server-test-agent-chat-delayed-failure"
},
body: JSON.stringify({
conversationId: "cnv_server-test-agent-chat-delayed-failure",
message: "请等待后返回 provider 失败分类"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(Date.now() - startedAt >= 4500, true);
assert.equal(payload.status, "failed");
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed-failure");
assert.equal(payload.provider, "openai-responses");
assert.equal(payload.error.code, "provider_unavailable");
assert.equal(payload.error.providerStatus, 503);
assert.match(payload.error.message, /HTTP 503/u);
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat reports provider timeout as failed without a reply", async () => {
const providerServer = createHttpServer((request, response) => {
request.resume();
+10 -1
View File
@@ -109,7 +109,9 @@ export function listen(server, { serviceId, host, port }) {
export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFAULT_PROXY_TIMEOUT_MS }) {
const target = new URL(request.url || "/", upstream);
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
proxy.destroy(new Error(`upstream timed out after ${timeoutMs}ms`));
}, timeoutMs);
const proxy = httpRequest(
@@ -138,8 +140,15 @@ export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFA
}
settled = true;
sendJson(response, 502, {
error: "upstream_unavailable",
status: "failed",
error: {
code: timedOut ? "proxy_timeout" : "upstream_unavailable",
category: timedOut ? "timeout" : "proxy",
message: error.message,
timeoutMs: timedOut ? timeoutMs : null
},
upstream,
traceId: request.headers["x-trace-id"] ?? null,
message: error.message
});
});
+9 -2
View File
@@ -71,12 +71,19 @@ test("dev entrypoint proxy returns 502 when hard timeout expires", async () => {
try {
const response = await fetch(`${serverUrl(proxy)}/v1/agent/chat`, {
method: "POST",
headers: { "content-type": "application/json" },
headers: {
"content-type": "application/json",
"x-trace-id": "trc_dev-entrypoint-proxy-timeout"
},
body: JSON.stringify({ message: "slow" })
});
assert.equal(response.status, 502);
const payload = await response.json();
assert.equal(payload.error, "upstream_unavailable");
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "proxy_timeout");
assert.equal(payload.error.category, "timeout");
assert.equal(payload.error.timeoutMs, 50);
assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-timeout");
assert.match(payload.message, /timed out after 50ms/u);
} finally {
await close(proxy);
+9 -1
View File
@@ -461,10 +461,18 @@ async function proxyCloudApi(request, response, url) {
});
response.end(upstream.body);
} catch (error) {
const timedOut = /timed out after \d+ms/iu.test(error.message);
sendJson(response, 502, {
error: "cloud_api_proxy_failed",
status: "failed",
error: {
code: timedOut ? "cloud_api_proxy_timeout" : "cloud_api_proxy_failed",
category: timedOut ? "timeout" : "proxy",
message: error.message,
timeoutMs: timedOut ? cloudApiProxyTimeoutMs : null
},
serviceId,
target: target.href.replace(/\/\/[^/@]*@/u, "//***@"),
traceId: request.headers["x-trace-id"] || null,
reason: error.message
});
}
+87 -1
View File
@@ -98,6 +98,24 @@ test("Code Agent browser failure classifier emits Chinese timeout/provider/brows
assert.match(provider.chineseSummary, /Provider 故障/u);
assert.match(provider.chineseSummary, /trc_provider/u);
const runnerBusy = classifyCodeAgentBrowserFailure(new Error("session_busy: runner already busy"), { traceId: "trc_busy" });
assert.equal(runnerBusy.category, "runner_busy");
assert.equal(runnerBusy.blocker, "runner-busy");
assert.match(runnerBusy.chineseSummary, /Runner 忙碌/u);
assert.match(runnerBusy.chineseSummary, /trc_busy/u);
const sessionBlocked = classifyCodeAgentBrowserFailure(new Error("session_expired: session blocked"), { traceId: "trc_session" });
assert.equal(sessionBlocked.category, "session_blocked");
assert.equal(sessionBlocked.blocker, "session-blocked");
assert.match(sessionBlocked.chineseSummary, /Session 受阻/u);
assert.match(sessionBlocked.chineseSummary, /trc_session/u);
const apiError = classifyCodeAgentBrowserFailure(new Error("API HTTP 500 request_failed"), { traceId: "trc_api" });
assert.equal(apiError.category, "api_error");
assert.equal(apiError.blocker, "api-error");
assert.match(apiError.chineseSummary, /API 错误/u);
assert.match(apiError.chineseSummary, /trc_api/u);
const browser = classifyCodeAgentBrowserFailure(new Error("locator #command-input not found"), { traceId: "trc_browser" });
assert.equal(browser.category, "browser");
assert.match(browser.chineseSummary, /浏览器故障/u);
@@ -367,6 +385,74 @@ test("Code Agent browser classifier treats sanitized 服务受阻 UI as blocked
assert.equal(classification.blocker, "provider-upstream");
});
test("Code Agent browser classifier distinguishes runner busy, session blocked, and API error payloads", () => {
const base = {
status: "failed",
provider: "codex-readonly-runner",
model: "read-only-tools",
backend: "hwlab-cloud-api/codex-readonly-runner",
traceId: "trc_runtime_block"
};
const runnerBusy = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
...base,
error: {
code: "session_busy",
message: "Code Agent runner session is already busy"
}
}, { httpStatus: 200 }),
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "服务受阻",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(runnerBusy.blocker, "runner-busy");
assert.equal(runnerBusy.category, "runner_busy");
const sessionBlocked = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
...base,
error: {
code: "session_expired",
message: "Code Agent runner session expired"
}
}, { httpStatus: 200 }),
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "服务受阻",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(sessionBlocked.blocker, "session-blocked");
assert.equal(sessionBlocked.category, "session_blocked");
const apiError = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
status: "failed",
traceId: "trc_proxy_timeout",
error: {
code: "cloud_api_proxy_failed",
message: "cloud web proxy failed"
}
}, { httpStatus: 502 }),
httpOk: false,
httpStatus: 502,
ui: {
agentChatStatus: "发送失败",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(apiError.blocker, "api-error");
assert.equal(apiError.category, "api_error");
});
test("Code Agent readiness classifier blocks completed payloads over any non-2xx HTTP status", () => {
const completedPayload = {
conversationId: "cnv_completed_non_2xx",
@@ -476,7 +562,7 @@ test("local Code Agent timeout fixture keeps failed UI state, trace evidence, an
assert.equal(report.devLive, false);
const timeoutCheck = report.checks.find((check) => check.id === "local-agent-timeout-fixture-failed-state");
assert.equal(timeoutCheck?.status, "pass");
assert.equal(timeoutCheck.observations.ui.agentChatStatus, "发送失败");
assert.equal(timeoutCheck.observations.ui.agentChatStatus, "等待超时");
assert.equal(timeoutCheck.observations.ui.failedMessageVisible, true);
assert.equal(timeoutCheck.observations.ui.failedMessageHasTrace, true);
assert.equal(timeoutCheck.observations.ui.failedMessageHasChineseTimeout, true);
+100 -1
View File
@@ -2,7 +2,17 @@ const trustedLiveProviders = new Set(["openai-responses", "codex-cli", "codex-re
const trustedRunnerProviders = new Set(["codex-readonly-runner"]);
const readonlySessionCapabilityLevels = new Set(["read-only-tools", "read-only-session-tools"]);
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]);
const blockedCodeAgentUiLabels = new Set([
"发送失败",
"服务受阻",
"等待超时",
"Provider 不可用",
"Runner 忙碌",
"Session 受阻",
"Runner 受阻",
"API 错误",
"BLOCKED 凭证缺口"
]);
const timeoutPattern = /\b(?:timeout|timed out|abort|aborted|超时|请求超过)\b/iu;
export function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStatus = null } = {}) {
@@ -345,6 +355,7 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = false, httpStatus = null, ui = null } = {}) {
const providerBlock = classifyCodeAgentProviderBlock(responseSummary, { httpStatus });
const runtimeBlock = classifyCodeAgentRuntimeBlock(responseSummary, { httpStatus });
const uiCompleted = ui?.agentChatStatus === "DEV-LIVE 回复" || ui?.agentChatStatus === "已回复" || ui?.completedMessageVisible === true;
const uiFailed = ui?.failedMessageVisible === true || blockedCodeAgentUiLabels.has(ui?.agentChatStatus);
const evidence = inspectSanitizedCompletionEvidence(responseSummary);
@@ -363,6 +374,10 @@ export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = fals
};
}
if (runtimeBlock.blocked) {
return runtimeBlock;
}
if (providerBlock.blocked) {
return {
status: "blocked",
@@ -387,6 +402,63 @@ export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = fals
};
}
export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {}) {
const errorCode = stringOrNull(payload?.error?.code);
const payloadStatus = stringOrNull(payload?.status);
if (errorCode === "session_busy") {
return {
blocked: true,
status: "blocked",
blocker: "runner-busy",
category: "runner_busy",
reason: "Code Agent read-only runner session is already busy; the UI must keep the input retryable."
};
}
if (["session_expired", "session_reuse_conflict"].includes(errorCode)) {
return {
blocked: true,
status: "blocked",
blocker: "session-blocked",
category: "session_blocked",
reason: `Code Agent session is blocked by ${errorCode}; fallback must not be reported as a Codex session.`
};
}
if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked"].includes(errorCode)) {
return {
blocked: true,
status: "blocked",
blocker: "runner-blocked",
category: "runner_blocked",
reason: `Code Agent read-only runner returned ${errorCode}; this is blocked runner evidence, not a completed fallback.`
};
}
if (["invalid_params", "parse_error", "request_failed", "cloud_api_proxy_failed", "upstream_unavailable"].includes(errorCode)) {
return {
blocked: true,
status: "blocked",
blocker: "api-error",
category: "api_error",
reason: `Code Agent API returned ${errorCode}; the request is retryable and must remain distinguishable from provider failure.`
};
}
if (payloadStatus === "failed" && errorCode && errorCode !== "provider_unavailable") {
return {
blocked: true,
status: "blocked",
blocker: "api-error",
category: "api_error",
reason: `Code Agent returned failed status with ${errorCode}.`
};
}
return { blocked: false };
}
export function classifyCodeAgentBrowserFailure(error, { traceId = null } = {}) {
const message = error instanceof Error ? error.message : String(error ?? "");
const observedTraceId = traceId ?? error?.traceId ?? null;
@@ -399,6 +471,24 @@ export function classifyCodeAgentBrowserFailure(error, { traceId = null } = {})
reason: message
};
}
if (/session[_ -]?busy|runner busy|already busy/iu.test(message)) {
return {
status: "blocked",
blocker: "runner-busy",
category: "runner_busy",
chineseSummary: `Runner 忙碌:Code Agent 正在处理上一轮请求,可稍后重试${observedTraceId ? `traceId=${observedTraceId}` : ""}`,
reason: message
};
}
if (/session[_ -]?(expired|blocked|reuse_conflict)|conversation.*session/iu.test(message)) {
return {
status: "blocked",
blocker: "session-blocked",
category: "session_blocked",
chineseSummary: `Session 受阻:Code Agent 会话无法复用或已过期${observedTraceId ? `traceId=${observedTraceId}` : ""}`,
reason: message
};
}
if (/provider|upstream|openai|codex|502|503|504|429|provider_unavailable/iu.test(message)) {
return {
status: "blocked",
@@ -408,6 +498,15 @@ export function classifyCodeAgentBrowserFailure(error, { traceId = null } = {})
reason: message
};
}
if (/api|http \d{3}|invalid_params|parse_error|request_failed|cloud_api_proxy_failed/iu.test(message)) {
return {
status: "blocked",
blocker: "api-error",
category: "api_error",
chineseSummary: `API 错误:Code Agent 接口返回失败,输入应保留可重试${observedTraceId ? `traceId=${observedTraceId}` : ""}`,
reason: message
};
}
return {
status: "blocked",
blocker: "browser-runtime",
+14 -5
View File
@@ -223,7 +223,16 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
"message-evidence-chip"
]);
const blockedCodeAgentUiLabels = Object.freeze(["服务受阻", "BLOCKED 凭证缺口"]);
const blockedCodeAgentUiLabels = Object.freeze([
"服务受阻",
"等待超时",
"Provider 不可用",
"Runner 忙碌",
"Session 受阻",
"Runner 受阻",
"API 错误",
"BLOCKED 凭证缺口"
]);
const forbiddenWritePatterns = Object.freeze([
/callRpc\(\s*["']hardware\./u,
@@ -507,7 +516,7 @@ function runStaticSmoke() {
evidence: [
"DEV-LIVE 回复",
"SOURCE 回复",
"发送失败",
"等待超时/API 错误/发送失败",
"服务受阻 or legacy BLOCKED 凭证缺口",
"message-evidence/details/chips"
]
@@ -2047,7 +2056,7 @@ function trustedRecordGroups({ html, app, styles }) {
/state\.liveSurface/u.test(app) &&
/renderRecords\(state\.liveSurface\)/u.test(app) &&
/traceId: error\.traceId \|\| traceId/u.test(app) &&
/error\.traceId = traceId/u.test(app) &&
/error\.traceId = (?:traceId|response\.data\?\.traceId \|\| traceId)/u.test(app) &&
requiredTrustedRecordTerms.every((term) => source.includes(term)) &&
/\.record-group\s*\{/u.test(styles) &&
/\.record-group-title\s*\{/u.test(styles) &&
@@ -3623,7 +3632,7 @@ async function runLocalAgentFixtureSmoke({
await page.locator("#command-input").fill(codeAgentE2ePrompts[0].text);
await page.locator("#command-send").click();
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "发送失败",
() => ["等待超时", "发送失败"].includes(document.querySelector("#agent-chat-status")?.textContent?.trim() ?? ""),
null,
{ timeout: 8000 }
);
@@ -3641,7 +3650,7 @@ async function runLocalAgentFixtureSmoke({
responseSummary = response ? sanitizeAgentChatBody(responseBody, { httpStatus: response.status() }) : null;
const pass = expectTimeout
? (
ui.agentChatStatus === "发送失败" &&
["等待超时", "发送失败"].includes(ui.agentChatStatus) &&
ui.failedMessageVisible &&
ui.retryInputPreserved &&
ui.failedMessageHasTrace &&
+11 -1
View File
@@ -44,7 +44,17 @@ const evidenceLevels = new Set(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCK
const devCloudWorkbenchRuntimeIdentityMaxSkewMs = 15 * 60 * 1000;
const syntheticAcceptedCodeAgentModels = new Set(["gpt-5"]);
const deploymentPreflightCodeAgentErrorCode = "deployment_identity_preflight";
const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]);
const blockedCodeAgentUiLabels = new Set([
"发送失败",
"服务受阻",
"等待超时",
"Provider 不可用",
"Runner 忙碌",
"Session 受阻",
"Runner 受阻",
"API 错误",
"BLOCKED 凭证缺口"
]);
const requiredValidationCommands = [
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
+143 -16
View File
@@ -558,21 +558,23 @@ function initCommandBar() {
renderAgentChatStatus(status, result);
} catch (error) {
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const presentation = agentFailurePresentation(error, { traceId });
state.chatMessages[index] = {
...pendingMessage,
title: "发送失败",
text: `发送失败:${error.message}`,
title: presentation.title,
text: presentation.text,
status: "failed",
traceId: error.traceId || traceId,
updatedAt: new Date().toISOString(),
error: {
code: error.code || "request_failed",
category: presentation.category,
message: error.message,
timeoutMs: error.timeoutMs
}
};
el.commandInput.value = value;
renderAgentChatStatus("failed");
renderAgentChatStatus("failed", state.chatMessages[index]);
} finally {
state.chatPending = false;
renderConversation();
@@ -658,12 +660,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI
if (isBlockedAgentResponse(response.data, response.status)) {
return normalizeBlockedAgentResult(response.data, response.status, traceId, response.error);
}
const error = new Error(response.timeout
? `Code Agent 请求超过 ${response.timeoutMs}ms 未完成;已保留输入,可稍后重试。`
: response.error || "Code Agent 请求失败");
error.traceId = traceId;
error.code = response.timeout ? "client_timeout" : "request_failed";
error.timeoutMs = response.timeoutMs;
const error = agentErrorFromHttpResponse(response, traceId);
throw error;
}
return {
@@ -2067,7 +2064,7 @@ function renderAgentChatStatus(status, result = null) {
failed: "发送失败",
blocked: "服务受阻"
};
el.agentChatStatus.textContent = labels[status] ?? statusLabel(status);
el.agentChatStatus.textContent = agentStatusLabel(status, result, labels);
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`;
el.commandInput.disabled = status === "running";
el.commandSend.disabled = status === "running";
@@ -2079,6 +2076,21 @@ function renderAgentChatStatus(status, result = null) {
}
}
function agentStatusLabel(status, result, labels) {
if (status !== "failed") return labels[status] ?? statusLabel(status);
const presentation = agentFailurePresentation(result?.error, { result });
return (
{
timeout: "等待超时",
provider: "Provider 不可用",
runner_busy: "Runner 忙碌",
session_blocked: "Session 受阻",
runner_blocked: "Runner 受阻",
api_error: "API 错误"
}[presentation.category] ?? labels.failed
);
}
function sourceTitle(result) {
return `Code Agent 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
}
@@ -2108,11 +2120,15 @@ function classifyCodeAgentCompletion(result) {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: result?.status === "completed" ? "Code Agent 完成证据不足" : "Code Agent 返回失败"
title: result?.status === "completed" ? "Code Agent 完成证据不足" : agentFailurePresentation(result?.error, { result }).title
};
}
function failureMessage(result) {
const presentation = agentFailurePresentation(result?.error, { result });
if (presentation.category !== "unknown") {
return presentation.text;
}
if (result?.error?.code === "provider_unavailable" || result?.availability?.status === "blocked") {
const availability = result.error?.providerStatus || result.availability?.status !== "blocked"
? codeAgentAvailabilityFromResult(result)
@@ -2127,6 +2143,109 @@ function failureMessage(result) {
return `Code Agent 调用失败:${result.error?.message ?? "未知错误"}${suffix}请稍后重试或联系维护者补齐后端 provider。`;
}
function agentErrorFromHttpResponse(response, traceId) {
const payloadError = response.data?.error;
const code = response.timeout
? "client_timeout"
: normalizeErrorCode(
(payloadError && typeof payloadError === "object" ? payloadError.code : null) ??
(typeof payloadError === "string" ? payloadError : null) ??
response.data?.code ??
"request_failed"
);
const error = new Error(response.timeout
? `Code Agent 请求超过 ${response.timeoutMs}ms 未完成;已保留输入,可稍后重试。`
: response.error || "Code Agent 请求失败");
error.traceId = response.data?.traceId || traceId;
error.code = code;
error.timeoutMs = response.timeoutMs ?? (payloadError && typeof payloadError === "object" ? payloadError.timeoutMs : undefined);
error.providerStatus = payloadError && typeof payloadError === "object" ? payloadError.providerStatus : response.status;
error.category = payloadError && typeof payloadError === "object" ? payloadError.category : undefined;
return error;
}
function agentFailurePresentation(error, { result = null, traceId = null } = {}) {
const code = normalizeErrorCode(error?.code);
const message = String(error?.message ?? result?.error?.message ?? "").trim();
const observedTraceId = result?.traceId || error?.traceId || traceId || null;
const traceSuffix = observedTraceId ? ` trace=${observedTraceId}` : "";
const timeoutMs = error?.timeoutMs ?? result?.error?.timeoutMs ?? timeoutMsFromText(message);
const providerStatus = error?.providerStatus ?? result?.error?.providerStatus;
if (isTimeoutFailure(code, message)) {
return {
category: "timeout",
title: "Code Agent 等待超时",
text: `Code Agent 请求${timeoutMs ? `超过 ${timeoutMs}ms ` : ""}未完成;输入已保留,可稍后重试或缩小问题范围。${traceSuffix}`
};
}
if (code === "session_busy") {
return {
category: "runner_busy",
title: "Code Agent Runner 忙碌",
text: `受控只读 runner 正在处理上一轮请求;输入已保留,稍后重试即可。${traceSuffix}`
};
}
if (["session_expired", "session_reuse_conflict"].includes(code)) {
return {
category: "session_blocked",
title: "Code Agent Session 受阻",
text: `当前 session 已过期或与 conversation 绑定不一致;输入已保留,可重新发送建立新的受控只读 session。${traceSuffix}`
};
}
if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked"].includes(code)) {
return {
category: "runner_blocked",
title: "Code Agent Runner 受阻",
text: `受控只读 runner 返回 ${code};本次不会 fallback 冒充 Codex session。${message ? `原因:${message}` : ""}${traceSuffix}`
};
}
if (["invalid_params", "parse_error", "request_failed", "cloud_api_proxy_failed", "upstream_unavailable"].includes(code)) {
return {
category: "api_error",
title: "Code Agent API 错误",
text: `Code Agent API 请求失败;输入已保留,可稍后重试。${message ? `原因:${message}` : ""}${traceSuffix}`
};
}
if (code === "provider_unavailable" || providerStatus) {
return {
category: "provider",
title: "Code Agent Provider 不可用",
text: providerStatus
? `Code Agent provider 返回 HTTP ${providerStatus};本次未生成回复,输入已保留,可稍后重试。${traceSuffix}`
: `Code Agent provider 暂不可用;本次未生成回复,输入已保留,可稍后重试。${traceSuffix}`
};
}
return {
category: "unknown",
title: "Code Agent 返回失败",
text: `Code Agent 调用失败:${message || "未知错误"}。请稍后重试或联系维护者补齐后端 provider。${traceSuffix}`
};
}
function normalizeErrorCode(code) {
const normalized = String(code ?? "").trim();
return normalized || "code_agent_failed";
}
function isTimeoutFailure(code, message) {
return ["client_timeout", "proxy_timeout", "cloud_api_proxy_timeout"].includes(code) ||
/\b(?:timeout|timed out|abort|aborted|超时|请求超过)\b/iu.test(message);
}
function timeoutMsFromText(message) {
const match = String(message ?? "").match(/(?:timed out after|timeout after|请求超时|请求超过)\s*(\d+)\s*ms/iu);
if (!match) return null;
const value = Number.parseInt(match[1], 10);
return Number.isInteger(value) && value > 0 ? value : null;
}
function messageCard(message) {
const article = document.createElement("article");
article.className = `message-card message-${toneClass(message.role)} status-${toneClass(message.status)}`;
@@ -2326,16 +2445,24 @@ function runtimeSummaryFrom(live) {
function messageFromPayload(payload) {
if (!payload || typeof payload !== "object") return "非 JSON 或空响应";
return payload.error?.message || payload.error?.code || payload.message || JSON.stringify(payload).slice(0, 160);
if (payload.error && typeof payload.error === "object") {
return payload.error.message || payload.error.code || payload.reason || payload.message || JSON.stringify(payload.error).slice(0, 160);
}
if (typeof payload.error === "string") {
return payload.message ? `${payload.error}${payload.message}` : payload.error;
}
return payload.code || payload.message || payload.reason || JSON.stringify(payload).slice(0, 160);
}
function normalizeBlockedAgentResult(payload, httpStatus, traceId, fallbackMessage) {
const { reply, ...safePayload } = payload;
const providerStatus = payload.error?.providerStatus ?? httpStatus;
const payloadError = payload.error && typeof payload.error === "object" ? payload.error : {};
const payloadErrorCode = payloadError.code ?? (typeof payload.error === "string" ? payload.error : null);
const providerStatus = payloadError.providerStatus ?? httpStatus;
const error = {
...(payload.error && typeof payload.error === "object" ? payload.error : {}),
code: payload.error?.code ?? "provider_unavailable",
message: payload.error?.message ?? fallbackMessage ?? `Code Agent provider HTTP ${httpStatus}`,
...payloadError,
code: payloadErrorCode ?? "provider_unavailable",
message: payloadError.message ?? fallbackMessage ?? `Code Agent provider HTTP ${httpStatus}`,
providerStatus
};
return {
+14 -1
View File
@@ -251,7 +251,7 @@ assert.match(app, /function liveFailureCard/);
assert.match(app, /state\.liveSurface/);
assert.match(app, /renderRecords\(state\.liveSurface\)/);
assert.match(app, /traceId: error\.traceId \|\| traceId/);
assert.match(app, /error\.traceId = traceId/);
assert.match(app, /error\.traceId = (?:traceId|response\.data\?\.traceId \|\| traceId)/);
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*150000/);
assert.match(app, /async function sendAgentMessage[\s\S]*?timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
@@ -629,6 +629,17 @@ assert.match(app, /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/);
assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/);
assert.match(app, /失败时会保留输入供重试/);
assert.match(app, /Code Agent 请求超过/);
assert.match(app, /agentFailurePresentation/);
assert.match(app, /agentStatusLabel/);
assert.match(app, /Code Agent 等待超时/);
assert.match(app, /Code Agent Runner 忙碌/);
assert.match(app, /Code Agent Session 受阻/);
assert.match(app, /Code Agent API 错误/);
assert.match(app, /等待超时/);
assert.match(app, /Provider 不可用/);
assert.match(app, /Runner 忙碌/);
assert.match(app, /Session 受阻/);
assert.match(app, /API 错误/);
assert.doesNotMatch(html, /M3 Diagnostics Console/);
assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
@@ -671,6 +682,8 @@ assert.match(artifactPublisher, /"audit\.event\.query"/);
assert.match(artifactPublisher, /"evidence\.record\.query"/);
assert.match(artifactPublisher, /readonly_rpc_required/);
assert.match(artifactPublisher, /cloud_api_proxy_failed/);
assert.match(artifactPublisher, /cloud_api_proxy_timeout/);
assert.match(artifactPublisher, /traceId:\s*request\.headers\["x-trace-id"\]/);
assert.match(artifactPublisher, /url\.pathname\.replace\(\/\\\/\+\$\/,\s*""\) \|\| "\/"/);
assert.match(artifactPublisher, /routePath === "\/gate"/);
assert.match(artifactPublisher, /routePath === "\/diagnostics\/gate"/);