fix: show degraded API status in workbench

Host closeout: reviewed diff, runner validation, and clean merge state. This is a narrow user-workbench status fix for degraded API readiness and does not claim M3/M4/M5 acceptance.
This commit is contained in:
Lyon
2026-05-23 10:19:12 +08:00
committed by GitHub
3 changed files with 90 additions and 7 deletions
@@ -293,6 +293,11 @@ function runStaticSmoke() {
evidence: ["/health/live", "/v1", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
});
addCheck(checks, blockers, "api-degraded-default-status", hasApiDegradedDefaultStatus(files), "Default workbench status distinguishes reachable degraded API from fully OK without exposing backend blocker details.", {
blocker: "observability_blocker",
evidence: ["healthLive.data.status === degraded", "API 降级 / 只读模式", "no Gate/diagnostics/blocker copy in default status classifier"]
});
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"]
@@ -1067,6 +1072,18 @@ function hasSameOriginReadOnlyBoundary(app) {
);
}
function hasApiDegradedDefaultStatus({ app }) {
const statusBody = functionBody(app, "workbenchApiSurfaceStatus");
return (
/function\s+workbenchApiSurfaceStatus\s*\(/u.test(app) &&
/live\.healthLive\?\.data\?\.status/u.test(statusBody) &&
/healthLiveStatus === "degraded"/u.test(statusBody) &&
/API 降级 \/ 只读模式/u.test(statusBody) &&
/同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/u.test(statusBody) &&
!/blocker|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u.test(statusBody)
);
}
function hasCodeAgentChatContract({ html, app }) {
return (
/id=["']agent-chat-status["']/u.test(html) &&
@@ -1666,6 +1683,27 @@ async function inspectJourneyUi(page) {
}));
}
async function inspectDefaultApiStatus(page) {
return page.evaluate(() => {
const liveStatus = document.querySelector("#live-status")?.textContent?.trim() ?? "";
const liveDetail = document.querySelector("#live-detail")?.textContent?.trim() ?? "";
const methodText = [...document.querySelectorAll("#method-list .badge")].map((element) => element.textContent?.trim() ?? "");
const firstScreenText = document.querySelector(".topbar")?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const forbidden = /Gate|诊断|验收|BLOCKED|M0-M5|执行轨迹|runtime_durable_adapter_query_blocked|DB live readiness/u;
return {
liveStatus,
liveDetail,
methodText,
firstScreenText,
degradedStatusVisible:
liveStatus === "API 降级 / 只读模式" &&
liveDetail === "同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。" &&
methodText.includes("system.health"),
defaultCopyIsWorkbenchSafe: !forbidden.test(`${liveStatus} ${liveDetail} ${firstScreenText}`)
};
});
}
function sanitizeAgentChatBody(body) {
if (!body || typeof body !== "object") return null;
return {
@@ -1728,7 +1766,13 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
await page.waitForFunction(
() => document.querySelector("#live-status")?.textContent?.trim() === "API 降级 / 只读模式",
null,
{ timeout: 8000 }
);
const controls = await inspectJourneyControls(page);
const apiStatus = await inspectDefaultApiStatus(page);
await page.locator("#command-input").fill("请用一句话说明当前 HWLAB 工作台可以做什么。");
const responsePromise = page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
@@ -1761,6 +1805,12 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
summary: "Local SOURCE fixture browser smoke can locate the Code Agent input, send button, thread, and right-side evidence panels.",
observations: controls
},
{
id: "local-api-degraded-default-status",
status: apiStatus.degradedStatusVisible && apiStatus.defaultCopyIsWorkbenchSafe ? "pass" : "blocked",
summary: "Default workbench status renders reachable degraded /health/live as API degraded read-only without first-screen backend blocker details.",
observations: apiStatus
},
{
id: "local-agent-fixture-replied-state",
status: pass ? "pass" : "blocked",
+34 -7
View File
@@ -508,27 +508,54 @@ function nextProtocolId(prefix) {
function renderLiveSurface(live) {
state.liveSurface = live;
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const reachable = coreProbes.some((probe) => probe.ok);
const surfaceStatus = workbenchApiSurfaceStatus(live, coreProbes);
const runtimeSummary = runtimeSummaryFrom(live);
const methods = methodsFrom(live);
el.liveStatus.textContent = reachable ? "只读连接可用" : "离线查看";
el.liveStatus.className = `tone-${toneClass(reachable ? "dev-live" : "source")}`;
el.liveStatus.textContent = surfaceStatus.label;
el.liveStatus.className = `tone-${toneClass(surfaceStatus.tone)}`;
state.codeAgentAvailability = codeAgentAvailabilityFrom(live);
el.liveDetail.textContent = reachable
? codeAgentAvailabilitySummary()
: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。";
el.liveDetail.textContent = surfaceStatus.detail ?? codeAgentAvailabilitySummary();
renderAgentChatStatus(deriveAgentChatStatus());
renderHardwareStatus(runtimeSummary);
renderWiringList(runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded");
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, surfaceStatus.methodTone);
renderConversation();
renderDrafts();
}
function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const reachable = coreProbes.some((probe) => probe.ok);
if (!reachable) {
return {
label: "离线查看",
tone: "source",
methodTone: "degraded",
detail: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"
};
}
const healthLiveStatus = String(live.healthLive?.data?.status ?? "").trim().toLowerCase();
if (live.healthLive?.ok && healthLiveStatus === "degraded") {
return {
label: "API 降级 / 只读模式",
tone: "degraded",
methodTone: "degraded",
detail: "同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。"
};
}
return {
label: "只读连接可用",
tone: "dev-live",
methodTone: "live",
detail: null
};
}
function renderResourceTree() {
const items = [
{
+6
View File
@@ -280,6 +280,12 @@ assert.match(distContractScript, /diagnostics\/gate\/index\.html/);
assert.match(markedLicense, /MarkedJS/);
assert.match(markedLicense, /Permission is hereby granted, free of charge/);
assert.match(html, /live-status/);
assert.match(app, /function workbenchApiSurfaceStatus/);
assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /live\.healthLive\?\.data\?\.status/);
assert.match(functionBody(app, "workbenchApiSurfaceStatus"), /healthLiveStatus === "degraded"/);
assert.match(app, /API 降级 \/ 只读模式/);
assert.match(app, /同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /blocker|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
assert.match(html, /method-list/);
assert.match(html, /quick-actions/);
assert.match(html, /编写任务/);