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:
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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, /编写任务/);
|
||||
|
||||
Reference in New Issue
Block a user