diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 7be56d96..9fceb23a 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -58,7 +58,14 @@ const defaultHomepageForbiddenTerms = Object.freeze([ "BLOCKED", "db-live", "m3-hardware-loop-runtime", - "执行轨迹" + "执行轨迹", + "runtime_durable_adapter_query_blocked", + "OPENAI_API_KEY", + "Secret", + "secretRef", + "hwlab-code-agent-provider", + "m3-route-required", + "waiting-for-dev-live" ]); const diagnosticsRequiredTerms = Object.freeze([ @@ -367,9 +374,19 @@ function runStaticSmoke() { evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"] }); - addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider credential blockers and only real completed replies can become DEV-LIVE.", { + addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider blockers without exposing credential internals and only real completed replies can become DEV-LIVE.", { blocker: "observability_blocker", - evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard"] + evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"] + }); + + addCheck(checks, blockers, "default-workspace-sanitized", defaultWorkspaceIsSanitized(files), "Default user workspace hides raw blocker codes, internal route markers, and credential reference material.", { + blocker: "observability_blocker", + evidence: defaultHomepageForbiddenTerms + }); + + addCheck(checks, blockers, "route-control-stable-dimensions", hasStableRouteControls(files), "Route controls and status tags have stable dimensions, readable wrapping, and usable labels on small screens.", { + blocker: "runtime_blocker", + evidence: ["rail-button min-width/min-height", "state-tag wrapping", "side-tab stable labels", "mobile message evidence column"] }); addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed Code Agent replies expose provider/model/trace/conversation evidence and reject echo/mock/stub completions.", { @@ -377,7 +394,7 @@ function runStaticSmoke() { evidence: requiredCodeAgentEvidenceTerms }); - addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and provider_unavailable states as distinct conversation replies with bounded evidence.", { + addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and blocked provider states as distinct conversation replies with bounded evidence.", { blocker: "observability_blocker", evidence: [ "DEV-LIVE 回复", @@ -1053,6 +1070,27 @@ function defaultHomepageHidesBackstage({ html }) { return defaultHomepageForbiddenTerms.every((term) => !visibleShellHtml.includes(term)); } +function defaultWorkspaceIsSanitized({ html, app }) { + const workspaceHtml = sectionSource(html, "workspace"); + const defaultShell = `${beforeSection(html, "help")}\n${workspaceHtml}`; + const userFacingBodies = [ + functionBody(app, "codeAgentStatusMessage"), + functionBody(app, "codeAgentPromptText"), + functionBody(app, "codeAgentControlSummary"), + functionBody(app, "codeAgentBlockedSummary"), + functionBody(app, "codeAgentAvailabilitySummary"), + functionBody(app, "failureMessage"), + functionBody(app, "renderWiringList") + ].join("\n"); + return ( + !/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live/u.test(defaultShell) && + !/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u.test(userFacingBodies) && + /服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) && + /Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) && + /等待可信记录/u.test(functionBody(app, "renderWiringList")) + ); +} + function hasCompletePrimaryNavigation(html) { const navLabelsPresent = ["工作台", "内部复核", "使用说明", "资源树"].every((label) => html.includes(`>${label}<`)); const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`)); @@ -1142,6 +1180,7 @@ function trustedRecordGroups({ html, app, styles }) { /function\s+evidenceCard\s*\(/u.test(app) && /function\s+recordGroup\s*\(/u.test(app) && /function\s+recordField\s*\(/u.test(app) && + /function\s+safeFailureReason\s*\(/u.test(app) && /function\s+sourceKindLabel\s*\(/u.test(app) && /function\s+liveFailureCard\s*\(/u.test(app) && /state\.liveSurface/u.test(app) && @@ -1156,6 +1195,7 @@ function trustedRecordGroups({ html, app, styles }) { /gateSummary\.operations\.map/u.test(app) && /gateSummary\.auditEvents\.slice\(0,\s*4\)\.map/u.test(app) && /gateSummary\.evidenceRecords\.map/u.test(app) && + /safeFailureReason\(message\.error\.message\)/u.test(app) && /message\.status === "failed" \? "blocked" : "source"/u.test(app) && !/message\.status === "completed"\s*\?\s*"dev-live"/u.test(app) ); @@ -1201,6 +1241,19 @@ function hasMobileWorkbenchLayoutContract({ html, app, styles }) { ); } +function hasStableRouteControls({ html, styles }) { + return ( + ["工作台", "内部复核", "使用说明", "资源树", "控制", "接线", "可信记录"].every((label) => html.includes(`>${label}<`)) && + /\.rail-button\s*\{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/su.test(styles) && + /@media\s*\(max-width:\s*860px\)[\s\S]*?\.rail-button\s*\{[\s\S]*?min-height:\s*42px;/u.test(styles) && + /\.side-tab\s*\{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/su.test(styles) && + /\.(?:status-dot|state-tag|badge)[^{]*\{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) && + /\.probe-card\s*\{[^}]*min-width:\s*0;/su.test(styles) && + /\.probe-card strong\s*\{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) && + /@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-evidence\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles) + ); +} + function hasSameOriginReadOnlyBoundary(app) { const rpcCalls = [...app.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]).sort(); return ( @@ -1257,9 +1310,18 @@ function hasCodeAgentChatContract({ html, app }) { function hasCodeAgentReadinessVisibility({ html, app }) { const source = `${html}\n${app}`; + const userFacingBodies = [ + functionBody(app, "codeAgentStatusMessage"), + functionBody(app, "codeAgentPromptText"), + functionBody(app, "codeAgentControlSummary"), + functionBody(app, "codeAgentBlockedSummary"), + functionBody(app, "codeAgentAvailabilitySummary"), + functionBody(app, "failureMessage") + ].join("\n"); return ( /codeAgentAvailability/u.test(app) && /codeAgentAvailabilityFrom/u.test(app) && + /sanitizeCodeAgentAvailability/u.test(app) && /latestCompletedAgentMessage/u.test(app) && blockedCodeAgentUiLabels.some((label) => app.includes(label)) && /provider_unavailable/u.test(app) && @@ -1277,6 +1339,7 @@ function hasCodeAgentReadinessVisibility({ html, app }) { !/status === "failed" \? "dev-live"/u.test(app) && !/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) && !/provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu.test(source) && + !/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef/u.test(userFacingBodies) && !/sk-[A-Za-z0-9._-]{8,}/u.test(source) ); } diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 4e791e66..f6b77065 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -1320,7 +1320,7 @@ function renderWiringList(summary = null) { const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence); const traceEvidence = m3Live ? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ") - : "m3-route-required / waiting-for-dev-live"; + : "等待可信记录"; const tr = document.createElement("tr"); tr.append(cell(M3_TRUSTED_ROUTE.fromResourceId, "mono wrap")); tr.append(cell(M3_TRUSTED_ROUTE.fromPort, "mono")); @@ -1330,7 +1330,7 @@ function renderWiringList(summary = null) { const status = document.createElement("td"); status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked")); tr.append(status); - tr.append(cell(m3Live ? "DEV-LIVE" : "SOURCE / required M3 route", "wrap")); + tr.append(cell(m3Live ? "DEV-LIVE" : "来源拓扑", "wrap")); tr.append(cell(traceEvidence, "mono wrap")); replaceChildren(el.wiringBody, tr); @@ -1383,7 +1383,7 @@ function codeAgentRecordCards() { recordField("conversation", state.conversationId), recordField("trace", message.traceId), recordField("message", message.messageId), - message.error?.message ? `失败原因=${message.error.message}` : null + message.error?.message ? `失败原因=${safeFailureReason(message.error.message)}` : null ].filter(Boolean).join(" / "), tone: message.status === "failed" ? "blocked" : "source" }) @@ -1572,6 +1572,15 @@ function recordField(label, value) { return `${label}=${value}`; } +function safeFailureReason(value) { + const text = String(value ?? "").trim(); + if (!text) return "未知错误"; + if (/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef/iu.test(text)) { + return "服务暂不可用"; + } + return text.length > 120 ? `${text.slice(0, 117)}...` : text; +} + function sourceKindLabel(tone) { return statusLabel(tone); } @@ -1703,7 +1712,7 @@ function renderAgentChatStatus(status, result = null) { completed: "DEV-LIVE 回复", source: "SOURCE 回复", failed: "发送失败", - blocked: "BLOCKED 凭证缺口" + blocked: "服务受阻" }; el.agentChatStatus.textContent = labels[status] ?? statusLabel(status); el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`; @@ -1755,10 +1764,7 @@ function failureMessage(result) { const availability = result.error?.providerStatus || result.availability?.status !== "blocked" ? codeAgentAvailabilityFromResult(result) : result.availability ?? codeAgentAvailabilityFromResult(result); - const missing = Array.isArray(result.error?.missingEnv) && result.error.missingEnv.length > 0 - ? ` 当前缺口:${result.error.missingEnv.join("、")}。` - : ""; - return `${availability.summary}${missing}`; + return codeAgentBlockedSummary(availability); } const missing = [ ...(result.error?.missingCommands ?? []), @@ -1947,7 +1953,8 @@ function isHttpNon2xxStatus(value) { } function codeAgentAvailabilityFrom(live) { - return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null; + const availability = live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null; + return sanitizeCodeAgentAvailability(availability); } function codeAgentAvailabilityFromResult(result) { @@ -1973,11 +1980,21 @@ function codeAgentAvailabilityFromResult(result) { }; } +function sanitizeCodeAgentAvailability(availability) { + if (!availability || typeof availability !== "object") return availability; + if (availability.status !== "blocked") return availability; + return { + ...availability, + blocker: "服务受阻", + summary: codeAgentBlockedSummary(availability) + }; +} + function codeAgentStatusMessage(availability) { if (availability?.status === "blocked") { return { role: "system", - title: "Code Agent 状态:BLOCKED 凭证缺口", + title: "Code Agent 状态:服务受阻", text: codeAgentBlockedSummary(availability), status: "blocked" }; @@ -2008,7 +2025,7 @@ function codeAgentStatusMessage(availability) { function codeAgentPromptText(availability) { if (availability?.status === "blocked") { - return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat,但当前 DEV provider 凭证缺口会返回结构化失败,不会冒充真实 Codex 回复。"; + return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。"; } return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。"; } @@ -2029,7 +2046,11 @@ function codeAgentControlSummary(availability) { } function codeAgentBlockedSummary(availability) { - return availability?.summary ?? "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。"; + const providerStatus = availability?.error?.providerStatus ?? availability?.providerStatus; + if (providerStatus) { + return `真实后端已接入,但当前上游响应 HTTP ${providerStatus};工作台保持只读和可重试,不会冒充真实 Code Agent 回复。`; + } + return "真实后端已接入,但当前 Code Agent 服务暂不可用;工作台保持只读和可重试,不会冒充真实回复。"; } function untrustedCompletionMessage(result) { @@ -2039,7 +2060,7 @@ function untrustedCompletionMessage(result) { function codeAgentAvailabilitySummary() { if (state.codeAgentAvailability?.status === "blocked") { - return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。"; + return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。"; } if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") { return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。"; diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index f16e8663..0bb60828 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -230,7 +230,7 @@ hwlab-patch-panel SOURCE - SOURCE route + 来源拓扑 res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 仅为源码兜底显示。