diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index d36a1c62..f0028847 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -608,6 +608,11 @@ function runStaticSmoke() { evidence: ["API 正常", "API 错误", "等待验证", "只读模式", "hwlab-cloud-api /health/live", "/v1/agent/chat", "/v1/m3/io", "raw degraded kept as internalRawStatuses"] }); + addCheck(checks, blockers, "workbench-live-surface-timeout-contract", hasWorkbenchLiveSurfaceTimeoutContract(files), "Workbench live status uses a dedicated live-surface timeout instead of the 4500ms light API budget.", { + blocker: "runtime_blocker", + evidence: ["DEFAULT_LIVE_SURFACE_TIMEOUT_MS=12000", "liveSurfaceFetch", "liveSurfaceRpc", "/v1/m3/status", "system.health"] + }); + addCheck(checks, blockers, "code-agent-chat-primary-flow", hasCodeAgentChatContract(files), "Center Agent conversation sends to the controlled Code Agent chat endpoint and no longer uses 添加草稿 as the primary flow.", { blocker: "runtime_blocker", evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"] @@ -2641,6 +2646,25 @@ function hasCodeAgentLongTimeoutContract(files) { ); } +function hasWorkbenchLiveSurfaceTimeoutContract(files) { + const app = files.app; + return ( + /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) && + /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/u.test(app) && + /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/u.test(app) && + /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceFetch")) && + /timeoutName:\s*`工作台实况 \$\{path\}`/u.test(functionBody(app, "liveSurfaceFetch")) && + /callRpc\(method,\s*params,\s*\{/u.test(functionBody(app, "liveSurfaceRpc")) && + /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceRpc")) && + /timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/u.test(functionBody(app, "callRpc")) && + /liveSurfaceFetch\("\/health\/live"\)/u.test(functionBody(app, "loadLiveSurface")) && + /liveSurfaceFetch\("\/v1\/m3\/status"\)/u.test(functionBody(app, "loadLiveSurface")) && + /liveSurfaceRpc\("system\.health"\)/u.test(functionBody(app, "loadLiveSurface")) && + /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "loadGateDiagnostics")) && + /timeoutName:\s*"工作台 M3 状态刷新"/u.test(functionBody(app, "runM3IoAction")) + ); +} + function hasCodeAgentTraceReplayDisclosure({ app, styles }) { const tracePanelBody = functionBody(app, "messageTracePanel"); const traceToolbarBody = functionBody(app, "messageTraceToolbar"); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index fb3ebece..aafe9c60 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -18,6 +18,7 @@ import { } from "./code-agent-m3-evidence.mjs"; const DEFAULT_API_TIMEOUT_MS = 4500; +const DEFAULT_LIVE_SURFACE_TIMEOUT_MS = 12000; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000; const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000; const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000; @@ -28,6 +29,7 @@ const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const FULL_TRACE_REPLAY_TIMEOUT_MS = 15000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); +const LIVE_SURFACE_TIMEOUT_MS = resolveTimeoutMs("liveSurfaceTimeoutMs", DEFAULT_LIVE_SURFACE_TIMEOUT_MS, { min: 5000, max: 60000 }); let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs(); let GATEWAY_SHELL_TIMEOUT_MS = resolveUserGatewayShellTimeoutMs(); const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: configuredTimeoutMinMs("codeAgentSubmitTimeoutMs", 5000), max: 120000 }); @@ -1593,15 +1595,15 @@ function wait(ms) { async function loadLiveSurface() { const projectId = gateSummary.topology.projectId; const [healthLive, liveBuilds, restIndex, m3Control, m3Status, health, adapter, audit, evidence] = await Promise.all([ - fetchJson("/health/live"), - fetchJson("/v1/live-builds"), - fetchJson("/v1"), - fetchJson("/v1/m3/io"), - fetchJson("/v1/m3/status"), - callRpc("system.health"), - callRpc("cloud.adapter.describe"), - callRpc("audit.event.query", { projectId, limit: 6 }), - callRpc("evidence.record.query", { projectId, limit: 6 }) + liveSurfaceFetch("/health/live"), + liveSurfaceFetch("/v1/live-builds"), + liveSurfaceFetch("/v1"), + liveSurfaceFetch("/v1/m3/io"), + liveSurfaceFetch("/v1/m3/status"), + liveSurfaceRpc("system.health"), + liveSurfaceRpc("cloud.adapter.describe"), + liveSurfaceRpc("audit.event.query", { projectId, limit: 6 }), + liveSurfaceRpc("evidence.record.query", { projectId, limit: 6 }) ]); return { @@ -1618,13 +1620,27 @@ async function loadLiveSurface() { }; } +function liveSurfaceFetch(path) { + return fetchJson(path, { + timeoutMs: LIVE_SURFACE_TIMEOUT_MS, + timeoutName: `工作台实况 ${path}` + }); +} + +function liveSurfaceRpc(method, params = {}) { + return callRpc(method, params, { + timeoutMs: LIVE_SURFACE_TIMEOUT_MS, + timeoutName: `工作台实况 /json-rpc ${method}` + }); +} + async function loadGateDiagnostics() { if (state.gateDiagnostics.loading) return; state.gateDiagnostics.loading = true; state.gateDiagnostics.error = null; renderGateTable(); const response = await fetchJson("/v1/diagnostics/gate", { - timeoutMs: API_TIMEOUT_MS, + timeoutMs: LIVE_SURFACE_TIMEOUT_MS, timeoutName: "内部复核 live 聚合" }); state.gateDiagnostics.loading = false; @@ -1733,7 +1749,7 @@ function readActivityRef(activityRef, fallbackMs = Date.now()) { }; } -async function callRpc(method, params = {}) { +async function callRpc(method, params = {}, options = {}) { const id = nextProtocolId("req"); const traceId = nextProtocolId("trc"); const response = await fetchJson("/json-rpc", { @@ -1751,7 +1767,9 @@ async function callRpc(method, params = {}) { serviceId: "hwlab-cloud-web", environment: "dev" } - }) + }), + timeoutMs: options?.timeoutMs ?? API_TIMEOUT_MS, + timeoutName: options?.timeoutName ?? `/json-rpc ${method}` }); if (!response.ok) { @@ -1837,6 +1855,8 @@ async function runM3IoAction(action) { "X-Trace-Id": traceId, "X-Actor-Id": "usr_hwlab_cloud_web" }, + timeoutMs: LIVE_SURFACE_TIMEOUT_MS, + timeoutName: `工作台 M3 IO ${action}`, body: JSON.stringify(body) }); @@ -1857,7 +1877,10 @@ async function runM3IoAction(action) { sourceKind: response.data?.evidenceState?.status === "green" ? "DEV-LIVE" : "BLOCKED" }; } - const refreshedStatus = await fetchJson("/v1/m3/status"); + const refreshedStatus = await fetchJson("/v1/m3/status", { + timeoutMs: LIVE_SURFACE_TIMEOUT_MS, + timeoutName: "工作台 M3 状态刷新" + }); if (refreshedStatus.ok) { state.m3Control.status = refreshedStatus.data; } diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index c3060f9b..39f298d4 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -401,6 +401,8 @@ assert.match(app, /renderRecords\(state\.liveSurface\)/); assert.match(app, /traceId: 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_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/); +assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/); assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/); assert.match(app, /DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/); assert.match(app, /CODE_AGENT_TIMEOUT_STORAGE_KEY/); @@ -414,6 +416,11 @@ assert.match(styles, /\.agent-timeout-control\s*{/); assert.match(app, /activityRef:\s*\(\)\s*=>\s*state\.currentRequest/); assert.match(functionBody(app, "fetchJson"), /timeoutMs\s*=\s*API_TIMEOUT_MS/); assert.match(functionBody(app, "fetchJson"), /无新事件/); +assert.match(functionBody(app, "liveSurfaceFetch"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/); +assert.match(functionBody(app, "liveSurfaceRpc"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/); +assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceFetch\("\/v1\/m3\/status"\)/); +assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceRpc\("system\.health"\)/); +assert.match(functionBody(app, "callRpc"), /timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/); for (const hardwareTerm of [ "Gateway 在线", "BOX 在线", @@ -1014,11 +1021,17 @@ assert.match(app, /createdAt/); assert.match(app, /updatedAt/); assert.match(app, /error\?\.message/); assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/); +assert.match(app, /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/); +assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/); assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/); assert.match(app, /CODE_AGENT_SUBMIT_TIMEOUT_MS/); assert.match(app, /CODE_AGENT_TIMEOUT_MS/); assert.match(functionBody(app, "sendAgentMessage"), /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/); assert.match(functionBody(app, "waitForAgentMessageResult"), /CODE_AGENT_TIMEOUT_MS/); +assert.match(functionBody(app, "liveSurfaceFetch"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/); +assert.match(functionBody(app, "liveSurfaceRpc"), /timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/); +assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceFetch\("\/health\/live"\)/); +assert.match(functionBody(app, "loadLiveSurface"), /liveSurfaceRpc\("system\.health"\)/); assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/); assert.match(app, /sourceKind:\s*"PENDING"/); assert.match(app, /旧 4500ms/);