From 9b08caa1a2efef800ecb14906777a368a9b581d3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 17:06:22 +0000 Subject: [PATCH 1/9] fix: stabilize code agent trace and gateway flow --- cmd/hwlab-cloud-api/main.mjs | 4 +- cmd/hwlab-edge-proxy/main.mjs | 4 +- deploy/deploy.json | 6 +- deploy/k8s/base/workloads.yaml | 6 +- docs/reference/code-agent-chat-readiness.md | 29 ++ docs/reference/dev-runtime-hotfix-runbook.md | 35 ++ .../code-agent-session-registry.test.mjs | 7 + internal/cloud/codex-stdio-session.mjs | 89 ++++- internal/dev-entrypoint/cloud-web-proxy.mjs | 6 +- .../dev-entrypoint/cloud-web-proxy.test.mjs | 41 +++ internal/dev-entrypoint/cloud-web-routes.mjs | 12 +- internal/dev-entrypoint/http.test.mjs | 10 +- .../artifact-runtime-readiness-guard.test.mjs | 28 +- scripts/dev-artifact-publish.mjs | 8 +- .../src/artifact-runtime-readiness-guard.mjs | 4 +- scripts/src/deploy-contract-plan.mjs | 4 +- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 12 +- tools/hwlab-gateway-shell.mjs | 61 +++- web/hwlab-cloud-web/app.mjs | 306 ++++++++++++++---- web/hwlab-cloud-web/index.html | 30 +- web/hwlab-cloud-web/scripts/check.mjs | 70 +++- web/hwlab-cloud-web/styles.css | 249 +++++++++----- 22 files changed, 795 insertions(+), 226 deletions(-) diff --git a/cmd/hwlab-cloud-api/main.mjs b/cmd/hwlab-cloud-api/main.mjs index c4e16be0..6b19241e 100644 --- a/cmd/hwlab-cloud-api/main.mjs +++ b/cmd/hwlab-cloud-api/main.mjs @@ -4,9 +4,9 @@ import { createCloudApiServer } from "../../internal/cloud/server.mjs"; const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0"; const port = Number.parseInt(process.env.HWLAB_CLOUD_API_PORT || process.env.PORT || "6667", 10); const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA; -const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 150000, { +const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 600000, { min: 1000, - max: 300000 + max: 900000 }); const codeAgentHardTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_HARD_TIMEOUT_MS, Math.max(600000, codeAgentTimeoutMs * 4), { min: codeAgentTimeoutMs, diff --git a/cmd/hwlab-edge-proxy/main.mjs b/cmd/hwlab-edge-proxy/main.mjs index a9051043..38ecd5c0 100644 --- a/cmd/hwlab-edge-proxy/main.mjs +++ b/cmd/hwlab-edge-proxy/main.mjs @@ -10,9 +10,9 @@ import { createServer } from "node:http"; const serviceId = "hwlab-edge-proxy"; const upstream = process.env.HWLAB_EDGE_UPSTREAM || "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"; -const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 180000, { +const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 660000, { min: 1000, - max: 300000 + max: 900000 }); const { host, port } = resolveHostPort({ listenEnv: "HWLAB_EDGE_LISTEN", diff --git a/deploy/deploy.json b/deploy/deploy.json index af685e4a..99d5a6f4 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -204,7 +204,7 @@ "HWLAB_M3_PATCH_PANEL_URL": "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301", "HWLAB_CODE_AGENT_PROVIDER": "codex-stdio", "HWLAB_CODE_AGENT_MODEL": "gpt-5.5", - "HWLAB_CODE_AGENT_TIMEOUT_MS": "150000", + "HWLAB_CODE_AGENT_TIMEOUT_MS": "600000", "HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://172.26.26.227:17680/v1/responses", "HWLAB_CODE_AGENT_HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1", @@ -230,7 +230,7 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "180000", + "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", "HWLAB_COMMIT_ID": "4d478a1", "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", "HWLAB_IMAGE_TAG": "4d478a1" @@ -368,7 +368,7 @@ "env": { "HWLAB_EDGE_LISTEN": "0.0.0.0:6667", "HWLAB_EDGE_UPSTREAM": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_EDGE_PROXY_TIMEOUT_MS": "180000" + "HWLAB_EDGE_PROXY_TIMEOUT_MS": "660000" } }, { diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index a44992a4..b7904e34 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -115,7 +115,7 @@ }, { "name": "HWLAB_CODE_AGENT_TIMEOUT_MS", - "value": "150000" + "value": "600000" }, { "name": "HWLAB_CODE_AGENT_OPENAI_BASE_URL", @@ -293,7 +293,7 @@ }, { "name": "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" }, { "name": "HWLAB_COMMIT_ID", @@ -870,7 +870,7 @@ }, { "name": "HWLAB_EDGE_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" } ], "readinessProbe": { diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index d3dc8e12..3e3124bb 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -86,6 +86,35 @@ runner 二进制/协议适配、session supervisor 生命周期、workspace moun 注入边界、trace/cancel/reap 机制,以及与 cloud-api/workbench 的持久 session 映射。缺任一项时,必须 在 `codexStdioFeasibility` 中报告 blocker。 +## PC Gateway Windows Skill 调用 + +Code Agent 通过已登记 PC gateway 执行 Windows 侧命令时,必须让 Codex turn 自己调用仓库 wrapper,不能由 cloud-api 字符串匹配短路到 gateway,也不能直接访问 gateway URL: + +```sh +node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin <<'PS1' + +PS1 +``` + +PowerShell 默认使用 wrapper 的 `-EncodedCommand` 路径;不要手写 `cmd /c powershell ...` 的管道、引号或中文转义。简单 `cmd` 命令仍可用 `--command "cmd /d /s /c ..."`,但涉及目录枚举、Unicode、管道、排序或 JSON 输出时优先用 `--powershell-stdin`。 + +Windows 文件系统探测必须是有界小输出: + +- 对 `F:\work`、`F:\work\ConStart` 或同类目录先做顶层目录/项目标记探测,不要读取 Secret、env、kubeconfig、DB URL 或完整源码内容。 +- 使用 `-LiteralPath`、`Select-Object -First `、`ConvertTo-Json -Compress -Depth `,stdout 目标控制在约 12 KB 内。 +- 不要先输出完整目录 JSON 再依赖终端截断;需要更多信息时按明确候选项目二次查询。 +- 如果命令已经到达 gateway 但因脚本语法或输出大小失败,只允许简化修正一次;最终回复要记录失败 `operationId`、修正后的成功 `operationId` 和有界输出摘要。 + +Keil 编译、下载或探测请求必须优先使用 Windows 侧 skill,而不是在 prompt 中重写 Keil 调用逻辑: + +```sh +cd C:\Users\liang\.agents\skills\keil +py -3 keil-cli.py build -p -t +py -3 keil-cli.py job-status +``` + +多 probe、烧录和 reset-run 的具体参数以 Windows 侧 `C:\Users\liang\.agents\skills\keil\SKILL.md` 为准;Code Agent 只负责通过 repo wrapper 调用该 skill CLI 并返回 trace、operation/evidence 和 bounded stdout/stderr 摘要。 + ## Smoke Gate 本地合同检查: diff --git a/docs/reference/dev-runtime-hotfix-runbook.md b/docs/reference/dev-runtime-hotfix-runbook.md index 5d3b70d6..80e6a70e 100644 --- a/docs/reference/dev-runtime-hotfix-runbook.md +++ b/docs/reference/dev-runtime-hotfix-runbook.md @@ -24,6 +24,41 @@ kubectl get nodes -o jsonpath='{.items[*].metadata.name}' 结果必须包含 `d601`。出现 `docker-desktop`、`desktop-control-plane`、`127.0.0.1:11700` 或第二套 `hwlab-dev` 控制面时,停止 hotfix、回滚和验收判断。此边界与 [dev-runtime-boundary.md](dev-runtime-boundary.md) 和 [deployment-publish.md](deployment-publish.md) 保持一致。 +## kubectl 热修命令形状 + +运行面热修应优先使用小补丁、短连接和远端临时文件,避免把大对象穿过 shell argv、CLI 输出截断或 `kubectl apply` 的 last-applied annotation。 + +推荐的 ConfigMap data 局部覆盖方式是:在本地生成只包含本次 key 的 merge patch,通过 D601 短连接写入远端临时文件,再让 `kubectl` 从真实文件读取: + +```sh +node - <<'NODE' | bun scripts/cli.ts ssh D601 argv bash -lc ' +set -e +export KUBECONFIG=/etc/rancher/k3s/k3s.yaml +kubectl get nodes -o jsonpath="{.items[*].metadata.name}" | grep -w d601 >/dev/null +patch_file=/tmp/hwlab-hotfix.patch.json +cat > "$patch_file" +kubectl -n hwlab-dev patch configmap --type merge --patch-file "$patch_file" +rm -f "$patch_file" +kubectl -n hwlab-dev rollout restart deployment/ +kubectl -n hwlab-dev rollout status deployment/ --timeout=180s +' +const fs = require("node:fs"); +process.stdout.write(JSON.stringify({ + metadata: { annotations: { "hwlab.pikastech.com/hotfix-reason": "" } }, + data: { "": fs.readFileSync("", "utf8") } +})); +NODE +``` + +固定避坑规则: + +- 不要用 `kubectl patch --patch-file -` 假设 stdin 可用;目标 `kubectl` 版本可能把 `-` 当普通文件名。用 `/tmp/*.patch.json` 再 `--patch-file`。 +- 不要把大 JSON patch 放进 `kubectl patch -p "$patch"` 或 shell argv;ConfigMap 覆盖源码文件时很容易触发 `Argument list too long`。用 stdin 写远端临时文件。 +- 不要对已经很大的 hotfix ConfigMap 使用 `kubectl apply -f -`;`apply` 会尝试写入 `kubectl.kubernetes.io/last-applied-configuration`,可能超过 Kubernetes annotation 256 KiB 限制。局部更新用 `patch --type merge --patch-file`。 +- 不要通过会截断 stdout 的控制 CLI 把 `kubectl get configmap -o json` 回读到本地再 `replace`;大 data 可能被日志/截断污染成非法 JSON。只有在确认完整无截断的原生通道中,才允许对完整对象做 `kubectl replace -f -`。 +- 每次 `rollout restart` 后立即执行 `rollout status`,再用 running pod 做最小只读验证,例如 `node --check`、`grep` marker、`curl` health 或公开入口 trace/result 轮询。 +- 临时补丁文件必须放在远端 `/tmp`,命名包含 hotfix 目标,命令结束后删除;issue 里只记录 ConfigMap 名、key、mountPath、operationId/traceId 和验证方法,不粘贴完整 data。 + ## 必须记录 每次 DEV hotfix 至少记录以下字段,写入 PR/issue/comment,不写入仓库 `reports/**`: diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index e3b492fb..e440dcb1 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1883,6 +1883,11 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u); assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills/u); assert.match(turn.args.prompt, /cmd \/c echo HWLAB-PC-GW && hostname/u); + assert.match(turn.args.prompt, /--powershell-stdin/u); + assert.match(turn.args.prompt, /-EncodedCommand/u); + assert.match(turn.args.prompt, /Windows filesystem inventory/u); + assert.match(turn.args.prompt, /Select-Object -First/u); + assert.match(turn.args.prompt, /ConvertTo-Json -Compress/u); } finally { await rm(fakeCodex.root, { recursive: true, force: true }); await rm(codexHome, { recursive: true, force: true }); @@ -1947,6 +1952,8 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn const turn = calls.find((call) => call.method === "turn/start"); assert.ok(turn, "Codex stdio turn/start should be called"); assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u); + assert.match(turn.args.prompt, /--powershell-stdin/u); + assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\keil/u); assert.match(turn.args.prompt, /keil-cli\.py build/u); assert.match(turn.args.prompt, /job-status/u); assert.match(turn.args.prompt, /FREQ_Controller_FW\.uvprojx/u); diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index aa36507e..4813f2f3 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -31,7 +31,7 @@ export const DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS = 30 * 60 * 1000; export const DEFAULT_CODEX_STDIO_MAX_SESSIONS = 64; export const DEFAULT_CODEX_STDIO_COMMAND = "codex"; export const DEFAULT_CODEX_STDIO_PROBE_TTL_MS = 30 * 1000; -const DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS = 150000; +const DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; const DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS = 10 * 60 * 1000; export const CODEX_STDIO_SESSION_STATUSES = Object.freeze([ "creating", @@ -57,6 +57,7 @@ const CODEX_STDIO_SKILL_SUMMARY_LIMIT = 180; const CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS = 3000; const CODEX_STDIO_COMMAND_PROBE_TRACE_ID = "trc_codex_stdio_command_probe"; const CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID = "cnv_codex_stdio_command_probe"; +const CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS = 15 * 1000; const EXTERNAL_NETWORK_TOOL_NAME = "external.network.check"; const EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS = 8000; const EXTERNAL_NETWORK_DNS_TIMEOUT_MS = 1500; @@ -95,6 +96,10 @@ const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [ "Do not call gateway, box-simu, or patch-panel directly.", "Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.", "For registered PC gateway Windows cmd or Keil requests, invoke the repo-owned wrapper with the Codex exec tool: node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --command \"cmd /c ...\".", + "For PowerShell through the PC gateway, do not hand-escape cmd pipes or quotes; use node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin with a single-quoted heredoc. The wrapper sends powershell.exe -EncodedCommand for Unicode-safe execution.", + "For Windows filesystem inventory, start with one small bounded PowerShell stdin script: use -LiteralPath, Select-Object -First, ConvertTo-Json -Compress, and keep stdout under about 12 KB. Do not dump full directory JSON and then retry.", + "For F:\\work or F:\\work\\ConStart discovery, first list top-level project markers and *.uvprojx candidates with bounded output. For Keil build/program requests, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\keil with py -3 keil-cli.py; do not reimplement Keil build logic.", + "If a Windows gateway command reaches the gateway but fails due script syntax or output size, simplify once and report the failed operationId plus the corrected bounded command evidence. Do not spend multiple turns on exploratory rewrites.", "Use the Windows-side skill CLIs under C:\\Users\\liang\\.agents\\skills\\ from that cmd command when they exist; do not reimplement those tools in the prompt.", "Do not satisfy PC gateway requests by string matching, assistant-only claims, internal shortcut dispatch, or direct gateway URLs; the Codex turn must run the wrapper and report command evidence.", "Do not claim M3, M4, or M5 acceptance unless the response includes live HWLAB evidence." @@ -1581,6 +1586,35 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { return appended; } + function appendProgressTrace(event = {}) { + return traceRecorder?.append(event); + } + + function hasAssistantOutput() { + return Boolean(firstNonEmpty(finalResponse, assistantText)); + } + + function appendFirstTokenProgressTrace(referenceNow = Date.now()) { + const activity = activitySnapshot(referenceNow); + if (hasAssistantOutput() || !turnId || terminal) return null; + if (!["assistant-message", "turn/completed"].includes(activity.waitingFor)) return null; + return appendProgressTrace({ + type: "turn", + status: "running", + label: "turn:waiting:first_assistant_token", + message: "Codex app-server is waiting for the first assistant token; this progress trace does not reset the backend idle timer.", + progressOnly: true, + idleMs: activity.idleMs, + lastActivityAt: activity.lastActivityAt, + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message" + }); + } + function finish(status, error = null, extra = {}) { if (terminal) return; terminal = { @@ -1728,11 +1762,33 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { async function wait(timeoutMs, closedPromise, { hardTimeoutMs = null } = {}) { let activityTimer; let hardTimer; + let progressTimer; const timeoutPromise = new Promise((_, reject) => { const checkActivity = () => { const now = Date.now(); const activity = activitySnapshot(now); if (activity.idleMs >= timeoutMs) { + if (hasAssistantOutput()) { + appendTrace({ + type: "turn", + status: "completed", + label: "turn:completed:idle_timeout_after_assistant", + message: "Codex app-server became idle after assistant output; preserving the completed assistant response.", + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message-complete" + }); + finish("completed", null, { + threadId, + turnId, + idleTimeoutAfterAssistant: true, + ...activity + }); + return; + } const error = appServerActivityTimeoutError({ timeoutMs, idleMs: activity.idleMs, @@ -1765,6 +1821,13 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { }; activityTimer = setTimeout(checkActivity, timeoutMs); }); + const progressPromise = new Promise(() => { + const tick = () => { + appendFirstTokenProgressTrace(); + progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS); + }; + progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS); + }); const hardTimeoutPromise = hardTimeoutMs ? new Promise((_, reject) => { hardTimer = setTimeout(() => { @@ -1804,6 +1867,27 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { if (closedPromise && typeof closedPromise.then === "function") { candidates.push(closedPromise.then((exit) => { if (terminal) return terminal; + if (hasAssistantOutput()) { + appendTrace({ + type: "turn", + status: "completed", + label: "turn:completed:transport_closed_after_assistant", + message: "Codex app-server transport closed after assistant message completion; preserving the completed assistant response.", + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message-complete" + }); + finish("completed", null, { + threadId, + turnId, + appServerExit: exit, + transportClosedAfterAssistant: true + }); + return terminal; + } return { terminalStatus: "failed", terminalError: "Codex app-server transport closed before turn/completed.", @@ -1815,10 +1899,11 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { })); } try { - return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise].filter(Boolean)); + return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise, progressPromise].filter(Boolean)); } finally { clearTimeout(activityTimer); clearTimeout(hardTimer); + clearTimeout(progressTimer); } } diff --git a/internal/dev-entrypoint/cloud-web-proxy.mjs b/internal/dev-entrypoint/cloud-web-proxy.mjs index 0634e584..79d74d60 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.mjs @@ -93,7 +93,11 @@ export function proxyCloudApiRequest({ settle(resolve); }); upstreamResponse.on("error", (error) => { - if (response.headersSent) response.destroy(error); + if (response.headersSent) { + response.destroy(error); + settle(resolve); + return; + } settle(reject, error); }); upstreamResponse.on("close", () => { diff --git a/internal/dev-entrypoint/cloud-web-proxy.test.mjs b/internal/dev-entrypoint/cloud-web-proxy.test.mjs index 80dad275..3fcd1e8b 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.test.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.test.mjs @@ -71,6 +71,47 @@ test("cloud web proxy streams SSE chunks without fixed content-length", async () } }); +test("cloud web proxy does not reject after SSE headers are already sent", async () => { + const upstream = createServer((request, response) => { + request.resume(); + response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8" }); + response.write("event: snapshot\ndata: {\"events\":[{\"seq\":1}]}\n\n"); + setTimeout(() => response.destroy(new Error("simulated upstream stream failure")), 40); + }); + await listen(upstream); + + let rejected = false; + const proxy = createServer((request, response) => { + const url = new URL(request.url || "/", "http://cloud-web.local"); + proxyCloudApiRequest({ + target: new URL(url.pathname + url.search, serverUrl(upstream)), + request, + response, + timeoutMs: 1000, + forceStream: isCloudWebSseRoute(url.pathname) + }).catch((error) => { + rejected = true; + if (!response.headersSent) response.destroy(error); + }); + }); + await listen(proxy); + + try { + const response = await fetch(`${serverUrl(proxy)}/v1/agent/chat/trace/trc_stream_proxy_error/stream`, { + headers: { accept: "text/event-stream" } + }); + assert.equal(response.status, 200); + const reader = response.body.getReader(); + await reader.read(); + await reader.read().catch(() => {}); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.equal(rejected, false, "stream errors after headers must not bubble into JSON error writers"); + } finally { + await close(proxy); + await close(upstream); + } +}); + test("cloud web proxy buffers normal JSON and sets content-length", async () => { const upstream = createServer((request, response) => { request.resume(); diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index 0f91eeda..61568031 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -15,7 +15,10 @@ export function cloudWebProxyRoutePolicy(method, pathname) { const normalizedPath = normalizePathname(pathname); const routeKey = `${normalizedMethod} ${normalizedPath}`; const proxy = isCloudApiProxyRoute(normalizedMethod, normalizedPath); - const publicRoute = proxy && PUBLIC_PROXY_ROUTES.has(routeKey); + const publicRoute = proxy && ( + PUBLIC_PROXY_ROUTES.has(routeKey) || + isPublicCodeAgentPollRoute(normalizedMethod, normalizedPath) + ); return { proxy, authRequired: proxy && !publicRoute, @@ -37,6 +40,13 @@ export function isCloudApiProxyRoute(method, pathname) { return false; } +function isPublicCodeAgentPollRoute(method, pathname) { + return method === "GET" && ( + pathname.startsWith("/v1/agent/chat/result/") || + pathname.startsWith("/v1/agent/chat/trace/") + ); +} + function normalizeMethod(method) { return String(method || "GET").trim().toUpperCase() || "GET"; } diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index e34c5f83..8c04bc11 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -32,10 +32,16 @@ test("cloud web route policy proxies public Code Agent chat without gating other }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/trace/trc_live"), { proxy: true, - authRequired: true, - publicRoute: false, + authRequired: false, + publicRoute: true, routeKey: "GET /v1/agent/chat/trace/trc_live" }); + assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/result/trc_live"), { + proxy: true, + authRequired: false, + publicRoute: true, + routeKey: "GET /v1/agent/chat/result/trc_live" + }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.mjs"), { proxy: false, authRequired: false, diff --git a/scripts/artifact-runtime-readiness-guard.test.mjs b/scripts/artifact-runtime-readiness-guard.test.mjs index 1052d690..111af27f 100644 --- a/scripts/artifact-runtime-readiness-guard.test.mjs +++ b/scripts/artifact-runtime-readiness-guard.test.mjs @@ -258,10 +258,10 @@ test("compact commander summary exposes canPublish/canApply and dry-run devLiveC cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, @@ -292,10 +292,10 @@ test("compact summary blocks publish on stale Cloud Web source dist", () => { cloudWebDistFreshness: { status: "blocked", mismatches: ["index.html"] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, @@ -326,9 +326,9 @@ test("compact summary separates Code Agent timeout and durable runtime blockers codeAgentTimeoutReadiness: { status: "blocked", frontendTimeoutMs: 4500, - backendTimeoutMs: 150000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: false, summary: "Code Agent browser timeout is too short." }, @@ -373,10 +373,10 @@ test("compact summary blocks publish when Code Agent transport timeout does not cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "blocked", - frontendTimeoutMs: 150000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 600000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: false, summary: "Code Agent timeout layering is unsafe." }, @@ -405,10 +405,10 @@ test("compact summary keeps provider-secret blockers out of publish but blocks a cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index 2fa7fb06..9da6101e 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -333,9 +333,9 @@ const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder"; const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || ""; const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10); const cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || ""; -const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 180000, { +const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 660000, { min: 1000, - max: 300000 + max: 900000 }); const authUsername = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_USERNAME", "admin"); const authPassword = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_PASSWORD", "hwlab2026"); @@ -649,6 +649,10 @@ async function proxyCloudApi(request, response, url) { try { await requestUpstream(target, request, response, request.method === "GET" ? "" : body, url); } catch (error) { + if (response.headersSent || response.writableEnded) { + if (!response.writableEnded) response.destroy(error); + return; + } const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message); const code = timedOut ? "cloud_api_proxy_timeout" : "cloud_api_proxy_failed"; const category = timedOut ? "timeout" : "proxy"; diff --git a/scripts/src/artifact-runtime-readiness-guard.mjs b/scripts/src/artifact-runtime-readiness-guard.mjs index 698e579b..4ba43df7 100644 --- a/scripts/src/artifact-runtime-readiness-guard.mjs +++ b/scripts/src/artifact-runtime-readiness-guard.mjs @@ -22,8 +22,8 @@ const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`; const digestPattern = /^sha256:[a-f0-9]{64}$/u; const commitPattern = /^[a-f0-9]{7,40}$/u; const minCodeAgentFrontendTimeoutMs = 35000; -const defaultCodeAgentBackendTimeoutMs = 150000; -const minCodeAgentBackendTimeoutMs = 150000; +const defaultCodeAgentBackendTimeoutMs = 600000; +const minCodeAgentBackendTimeoutMs = 600000; export function normalizeCommit(value) { if (typeof value !== "string") return "unknown"; diff --git a/scripts/src/deploy-contract-plan.mjs b/scripts/src/deploy-contract-plan.mjs index fe2c6b43..cee6f6c5 100644 --- a/scripts/src/deploy-contract-plan.mjs +++ b/scripts/src/deploy-contract-plan.mjs @@ -13,8 +13,8 @@ import { const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const expectedPorts = Object.freeze({ frontend: 16666, api: 16667 }); -const codeAgentBackendTimeoutMs = "150000"; -const codeAgentTransportTimeoutMs = "180000"; +const codeAgentBackendTimeoutMs = "600000"; +const codeAgentTransportTimeoutMs = "660000"; const legacyPublicPorts = new Set([6666, 6667]); const duplicateMappingAllowed = new Set(["hwlab-box-simu", "hwlab-gateway-simu"]); const tomlBareKeyPattern = /^[A-Za-z0-9_-]+$/u; diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index e2764307..eaf8a50b 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -23,7 +23,7 @@ const webRoot = path.join(repoRoot, "web/hwlab-cloud-web"); const defaultLiveUrl = "http://74.48.78.17:16666/"; const helpOwner = "codex_1779444232735_1"; const legacyFailureWindowMs = 4500; -const codeAgentLongTimeoutMs = 180000; +const codeAgentLongTimeoutMs = 600000; const localAgentFixtureDelayMs = 5200; const codeAgentE2ePrompts = Object.freeze([ { @@ -2631,8 +2631,10 @@ function hasCodeAgentLongTimeoutContract(files) { const app = files.app; return ( /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) && - /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) && - /async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) && + /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) && + /DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/u.test(app) && + /async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) && + /shortConnection:\s*true/u.test(functionBody(app, "sendAgentMessage")) && /timeoutMs\s*=\s*API_TIMEOUT_MS/u.test(functionBody(app, "fetchJson")) && /timeoutName\s*=\s*path/u.test(functionBody(app, "fetchJson")) && /HWLAB_CLOUD_WEB_CONFIG/u.test(app) && @@ -2719,8 +2721,8 @@ function hasCodeAgentConversationUxStates({ app, styles }) { !/function\s+messageAttributionPanel\s*\(/u.test(app) && !/codeAgentAttributionFromMessage/u.test(app) && /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) && - /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) && - /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) && + /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) && + /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) && /HWLAB_CLOUD_WEB_CONFIG/u.test(app) && /正在处理这次 Code Agent 请求/u.test(app) && /旧 4500ms/u.test(app) && diff --git a/tools/hwlab-gateway-shell.mjs b/tools/hwlab-gateway-shell.mjs index 197001a2..45234f97 100644 --- a/tools/hwlab-gateway-shell.mjs +++ b/tools/hwlab-gateway-shell.mjs @@ -2,16 +2,19 @@ import http from "node:http"; import https from "node:https"; import { randomUUID } from "node:crypto"; +import { readFileSync } from "node:fs"; const DEFAULT_API_BASE_URL = "http://127.0.0.1:6667"; const DEFAULT_PROJECT_ID = "prj_mvp_topology"; const DEFAULT_GATEWAY_SESSION_ID = "gws_DESKTOP-1MHOD9I"; const DEFAULT_RESOURCE_ID = "res_windows_host"; const DEFAULT_CAPABILITY_ID = "cap_windows_cmd_exec"; +const POWERSHELL_PROLOGUE = "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [Console]::OutputEncoding;"; async function main() { const args = parseArgs(process.argv.slice(2)); - if (args.help || !args.command) { + const command = await resolveCommand(args); + if (args.help || !command) { printHelp(); process.exit(args.help ? 0 : 2); } @@ -36,7 +39,7 @@ async function main() { resourceId: args.resourceId || DEFAULT_RESOURCE_ID, capabilityId: args.capabilityId || DEFAULT_CAPABILITY_ID, input: { - command: args.command, + command, cwd: args.cwd || undefined, timeoutMs } @@ -58,19 +61,22 @@ async function main() { function parseArgs(argv) { const args = {}; + const booleanFlags = new Set(["json", "help", "h", "powershellStdin", "psStdin"]); for (let index = 0; index < argv.length; index += 1) { const item = argv[index]; - if (item === "--help" || item === "-h") { + if ((item === "--help") || (item === "-h")) { args.help = true; - } else if (item === "--json") { - args.json = true; } else if (item.startsWith("--") && item.includes("=")) { const [key, ...rest] = item.slice(2).split("="); args[toCamel(key)] = rest.join("="); } else if (item.startsWith("--")) { const key = toCamel(item.slice(2)); - args[key] = argv[index + 1] ?? ""; - index += 1; + if (booleanFlags.has(key)) { + args[key] = true; + } else { + args[key] = argv[index + 1] ?? ""; + index += 1; + } } } return args; @@ -83,10 +89,16 @@ function toCamel(value) { function printHelp() { process.stdout.write([ "Usage: node tools/hwlab-gateway-shell.mjs --command \"cmd /c ...\" [--json]", + " node tools/hwlab-gateway-shell.mjs --powershell-command \"Get-ChildItem ...\" [--json]", + " node tools/hwlab-gateway-shell.mjs --powershell-stdin [--json] < script.ps1", "", "Options:", " --api-base-url URL Cloud API/edge base URL, default http://127.0.0.1:6667", " --command CMD Windows cmd command to execute through the registered PC gateway", + " --powershell-command PS1 Encode PS1 as UTF-16LE and run powershell.exe -EncodedCommand", + " --powershell-file PATH Read a local PS1 file, encode it, and run through the gateway", + " --powershell-stdin Read PS1 from stdin, encode it, and run through the gateway", + " --powershell-exe NAME Default powershell.exe", " --timeout-ms N Short dispatch timeout in milliseconds", " --project-id ID Default prj_mvp_topology", " --gateway-session-id ID Default gws_DESKTOP-1MHOD9I", @@ -96,6 +108,41 @@ function printHelp() { ].join("\n") + "\n"); } +async function resolveCommand(args) { + if (args.powershellStdin || args.psStdin) { + return powershellEncodedCommand(await readStdin(), args); + } + if (args.powershellFile || args.psFile) { + return powershellEncodedCommand(readFileSync(args.powershellFile || args.psFile, "utf8"), args); + } + if (args.powershellCommand || args.psCommand || args.powershell) { + return powershellEncodedCommand(args.powershellCommand || args.psCommand || args.powershell, args); + } + return args.command || ""; +} + +function powershellEncodedCommand(script, args = {}) { + const source = String(script || "").trim(); + if (!source) return ""; + const shell = args.powershellExe || "powershell.exe"; + const prologue = args.noPowershellPrologue ? "" : `${POWERSHELL_PROLOGUE}\n`; + const encoded = Buffer.from(`${prologue}${source}`, "utf16le").toString("base64"); + return `${shell} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`; +} + +function readStdin() { + return new Promise((resolve, reject) => { + let text = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => { + text += chunk; + }); + process.stdin.on("end", () => resolve(text)); + process.stdin.on("error", reject); + process.stdin.resume(); + }); +} + function requestJson(url, { method, headers, body, timeoutMs }) { return new Promise((resolve, reject) => { const target = new URL(url); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 48faee47..27ee5839 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -18,11 +18,14 @@ import { } from "./code-agent-m3-evidence.mjs"; const DEFAULT_API_TIMEOUT_MS = 4500; -const DEFAULT_CODE_AGENT_TIMEOUT_MS = 180000; +const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000; +const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000; +const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); -const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 }); +let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs(); +const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: 5000, max: 120000 }); const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; const rpcReadMethods = Object.freeze([ "system.health", @@ -113,8 +116,6 @@ const el = { liveDetail: byId("live-detail"), liveBuildSummary: byId("live-build-summary"), liveBuildToggle: byId("live-build-toggle"), - liveBuildPopover: byId("live-build-popover"), - liveBuildClose: byId("live-build-close"), liveBuildLatest: byId("live-build-latest"), liveBuildList: byId("live-build-list"), agentChatStatus: byId("agent-chat-status"), @@ -125,7 +126,6 @@ const el = { codeAgentSummaryTrace: byId("code-agent-summary-trace"), codeAgentSummaryDetail: byId("code-agent-summary-detail"), conversationList: byId("conversation-list"), - taskList: byId("task-list"), gateSourceStatus: byId("gate-source-status"), gateStatusFilter: byId("gate-status-filter"), gateSearch: byId("gate-search"), @@ -133,6 +133,7 @@ const el = { gateReviewBody: byId("gate-review-body"), commandForm: byId("command-form"), commandInput: byId("command-input"), + codeAgentTimeout: byId("code-agent-timeout"), commandSend: byId("command-send"), commandClear: byId("command-clear"), hardwareSourceStatus: byId("hardware-source-status"), @@ -159,6 +160,8 @@ const el = { helpStatus: byId("help-status") }; +let activeWorkbenchDialog = null; + await ensureWorkbenchAuth({ loginShell: el.loginShell, appShell: el.shell @@ -207,6 +210,7 @@ initRightSidebarResize(); initSideTabs(); initHardwareTabs(); initQuickActions(); +initCodeAgentTimeoutControl(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); @@ -241,33 +245,125 @@ function resolveTimeoutMs(name, fallback, { min, max }) { return Math.min(Math.max(Math.trunc(configured), min), max); } -function initLiveBuildOverlay() { - syncLiveBuildOverlayState(); - el.liveBuildSummary.addEventListener("toggle", syncLiveBuildOverlayState); - el.liveBuildClose.addEventListener("click", () => closeLiveBuildOverlay({ restoreFocus: true })); - document.addEventListener("keydown", (event) => { - if (event.key === "Escape" && el.liveBuildSummary.open) { - event.preventDefault(); - closeLiveBuildOverlay({ restoreFocus: true }); +function clampTimeoutMs(value, { min, max }) { + if (value === null || value === undefined) return null; + if (typeof value === "string" && value.trim() === "") return null; + const numeric = Number(value); + if (!Number.isFinite(numeric)) return null; + return Math.min(Math.max(Math.trunc(numeric), min), max); +} + +function resolveUserCodeAgentTimeoutMs() { + let stored = null; + try { + stored = window.localStorage?.getItem(CODE_AGENT_TIMEOUT_STORAGE_KEY) ?? null; + } catch { + stored = null; + } + return clampTimeoutMs(stored, { min: 30000, max: 1200000 }) + ?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 30000, max: 1200000 }); +} + +function initCodeAgentTimeoutControl() { + syncCodeAgentTimeoutControl(); + el.codeAgentTimeout.addEventListener("change", () => { + const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: 1200000 }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS; + CODE_AGENT_TIMEOUT_MS = next; + try { + window.localStorage?.setItem(CODE_AGENT_TIMEOUT_STORAGE_KEY, String(next)); + } catch { + // localStorage can be unavailable in hardened browser modes. } - }); - document.addEventListener("click", (event) => { - if (!el.liveBuildSummary.open || el.liveBuildSummary.contains(event.target)) return; - closeLiveBuildOverlay(); + syncCodeAgentTimeoutControl(); + renderCodeAgentSummary(); + renderConversation(); }); } -function syncLiveBuildOverlayState() { - const expanded = el.liveBuildSummary.open === true; - el.liveBuildToggle.setAttribute("aria-expanded", expanded ? "true" : "false"); - el.liveBuildPopover.setAttribute("aria-hidden", expanded ? "false" : "true"); +function syncCodeAgentTimeoutControl() { + el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS); + el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`; } -function closeLiveBuildOverlay(options = {}) { - if (!el.liveBuildSummary.open) return; +function initLiveBuildOverlay() { el.liveBuildSummary.open = false; - syncLiveBuildOverlayState(); - if (options.restoreFocus === true) el.liveBuildToggle.focus(); + el.liveBuildToggle.addEventListener("click", (event) => { + event.preventDefault(); + el.liveBuildSummary.open = false; + openWorkbenchDialog({ + title: "构建版本明细", + body: cloneLiveBuildList(), + restoreFocus: el.liveBuildToggle + }); + }); +} + +function cloneLiveBuildList() { + const clone = el.liveBuildList.cloneNode(true); + clone.id = ""; + clone.hidden = false; + clone.removeAttribute("hidden"); + clone.tabIndex = 0; + clone.classList.remove("live-build-list-template"); + return clone; +} + +function openWorkbenchDialog({ title, body, restoreFocus = null }) { + closeWorkbenchDialog({ restoreFocus: false }); + const layer = document.createElement("div"); + layer.className = "workbench-dialog-layer"; + const backdrop = document.createElement("button"); + backdrop.type = "button"; + backdrop.className = "workbench-dialog-backdrop"; + backdrop.setAttribute("aria-label", "关闭弹窗"); + const dialog = document.createElement("section"); + dialog.className = "workbench-dialog"; + dialog.setAttribute("role", "dialog"); + dialog.setAttribute("aria-modal", "true"); + dialog.setAttribute("aria-label", title); + const head = document.createElement("div"); + head.className = "workbench-dialog-head"; + const heading = document.createElement("strong"); + heading.textContent = title; + const close = document.createElement("button"); + close.type = "button"; + close.className = "workbench-dialog-close"; + close.textContent = "关闭"; + close.setAttribute("aria-label", `关闭${title}`); + const content = document.createElement("div"); + content.className = "workbench-dialog-content"; + if (body) { + body.hidden = false; + body.removeAttribute("hidden"); + content.append(body); + } + head.append(heading, close); + dialog.append(head, content); + layer.append(backdrop, dialog); + + const closeDialog = () => closeWorkbenchDialog({ restoreFocus: true }); + const keydown = (event) => { + if (event.key !== "Escape") return; + event.preventDefault(); + closeDialog(); + }; + close.addEventListener("click", closeDialog); + backdrop.addEventListener("click", closeDialog); + document.addEventListener("keydown", keydown); + document.body.append(layer); + activeWorkbenchDialog = { layer, keydown, restoreFocus }; + close.focus(); +} + +function closeWorkbenchDialog({ restoreFocus = true } = {}) { + if (!activeWorkbenchDialog) return; + const { layer, keydown, restoreFocus: target } = activeWorkbenchDialog; + document.removeEventListener("keydown", keydown); + layer.remove(); + activeWorkbenchDialog = null; + if (restoreFocus && target && typeof target.focus === "function") { + target.focus(); + } } function initRoutes() { @@ -711,7 +807,7 @@ async function submitAgentMessage(value, options = {}) { id: nextProtocolId("msg"), role: "agent", title: options.retryOf ? "Code Agent 重试中" : "Code Agent 处理中", - text: `正在处理这次 Code Agent 请求;复杂问题可能需要 1-2 分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`, + text: `正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`, status: "running", traceId, conversationId: activeConversationId, @@ -1306,7 +1402,6 @@ function renderStaticWorkbench() { el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。"; renderConversation(); - renderTaskList(); renderHardwareStatus(null); renderDrafts(); renderM3ControlStatus(); @@ -1335,7 +1430,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI "Prefer": "respond-async", "X-HWLAB-Short-Connection": "1" }, - timeoutMs: Math.min(API_TIMEOUT_MS, 5000), + timeoutMs: CODE_AGENT_SUBMIT_TIMEOUT_MS, timeoutName: "Code Agent", body: JSON.stringify({ message, @@ -1343,6 +1438,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI sessionId, threadId, traceId, + shortConnection: true, projectId: gateSummary.topology.projectId }) }); @@ -1888,27 +1984,6 @@ function renderConversation() { replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); } -function renderTaskList() { - const rows = [ - { - title: "1. 描述目标", - detail: "例如要检查哪个 BOX-SIMU、哪个端口、期望看到什么信号。", - tone: "source" - }, - { - title: "2. 核对接线", - detail: "打开右侧“接线”表,确认 res_boxsimu_1 与 res_boxsimu_2 的 DO/DI 对应关系,以及接线盘和轨迹/证据摘要。", - tone: "source" - }, - { - title: "3. 留存记录", - detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。", - tone: "source" - } - ]; - replaceChildren(el.taskList, ...rows.map(infoCard)); -} - function renderHardwareStatus(m3Status) { syncHardwareTabs(); const status = m3Status ?? sourceFallbackM3Status(); @@ -3744,11 +3819,6 @@ function messageSessionContinuityPanel(message) { ["missing", continuity.missing?.join("、")], ["changed", continuity.changed?.join("、")] ]; - const section = document.createElement("section"); - section.className = `message-session-context tone-border-${toneClass(continuity.tone)}`; - const header = document.createElement("div"); - header.className = "message-session-head"; - header.append(badge(continuity.label, continuity.tone), textSpan(continuity.summary, "message-session-summary")); const list = document.createElement("div"); list.className = "message-session-grid"; for (const [label, value] of fields) { @@ -3761,8 +3831,15 @@ function messageSessionContinuityPanel(message) { } list.append(item); } - section.append(header, list); - return section; + return messageCompactDetails({ + className: "message-session-context", + tone: continuity.tone, + badgeLabel: continuity.label, + badgeTone: continuity.tone, + summary: continuity.summary, + dialogTitle: "会话明细", + body: list + }); } function messagePendingContextPanel(message) { @@ -3781,7 +3858,7 @@ function messagePendingContextPanel(message) { ["thread", message.threadId ?? message.runnerTrace?.threadId ?? "等待后端分配"], ["sessionStatus", message.runnerTrace?.sessionStatus], ["lifecycle", message.runnerTrace?.sessionLifecycleStatus], - ["activityTimeout", `${CODE_AGENT_TIMEOUT_MS}ms 无新事件`] + ["activityTimeout", `${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 无新事件`] ]; const section = document.createElement("section"); section.className = "message-pending-context tone-border-pending"; @@ -3823,15 +3900,6 @@ function messageRuntimePathPanel(message) { if (message.role !== "agent") return null; const runtimePath = codeAgentRuntimePathFromMessage(message); if (!runtimePath) return null; - const section = document.createElement("section"); - section.className = `message-runtime-path tone-border-${toneClass(runtimePath.tone)}`; - section.dataset.runtimePathKind = runtimePath.kind; - const header = document.createElement("div"); - header.className = "message-runtime-head"; - header.append( - badge(runtimePath.label, runtimePath.tone), - textSpan(runtimePath.summary, "message-runtime-summary") - ); const rows = document.createElement("div"); rows.className = "message-runtime-grid"; for (const row of runtimePath.rows) { @@ -3852,7 +3920,38 @@ function messageRuntimePathPanel(message) { ); rows.append(item); } - section.append(header, rows); + const details = messageCompactDetails({ + className: "message-runtime-path", + tone: runtimePath.tone, + badgeLabel: runtimePath.label, + badgeTone: runtimePath.tone, + summary: runtimePath.summary, + dialogTitle: "运行路径明细", + body: rows + }); + details.dataset.runtimePathKind = runtimePath.kind; + return details; +} + +function messageCompactDetails({ className, tone, badgeLabel, badgeTone, summary, dialogTitle, body }) { + const section = document.createElement("section"); + section.className = `${className} message-compact-details tone-border-${toneClass(tone)}`; + const trigger = document.createElement("button"); + trigger.type = "button"; + trigger.className = "message-compact-summary"; + trigger.setAttribute("aria-haspopup", "dialog"); + trigger.append( + badge(badgeLabel, badgeTone), + textSpan(summary, "message-compact-summary-text") + ); + trigger.addEventListener("click", () => { + openWorkbenchDialog({ + title: dialogTitle, + body, + restoreFocus: trigger + }); + }); + section.append(trigger); return section; } @@ -3987,14 +4086,76 @@ function renderTraceEventList(list, rows) { function traceDisplayRows(trace, events) { const rows = []; + let noisyRun = []; + const flushNoisyRun = () => { + if (noisyRun.length === 0) return; + const row = traceNoiseSummaryRow(trace, noisyRun); + if (row) rows.push(row); + noisyRun = []; + }; for (const event of events) { + if (isNoisyTraceEvent(event)) { + noisyRun.push(event); + continue; + } + flushNoisyRun(); const row = traceDisplayRow(trace, event); if (!row) continue; rows.push(row); } + flushNoisyRun(); return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean); } +function traceNoiseSummaryRow(trace, events) { + const visibleEvents = events.filter(Boolean); + if (visibleEvents.length === 0) return null; + const last = visibleEvents.at(-1); + const clock = traceClock(last.createdAt); + const total = formatTraceDuration(traceRelativeMs(trace, last)); + const assistantChunks = visibleEvents.filter((event) => isAssistantChunkTraceEvent(event)); + const label = assistantChunks.length === visibleEvents.length + ? `assistant stream x${visibleEvents.length}` + : `trace noise x${visibleEvents.length}`; + return { + seq: last.seq ?? null, + tone: "source", + header: `${clock} total=${total} stream ${label}`, + body: traceNoiseSummaryBody(visibleEvents, assistantChunks) + }; +} + +function traceNoiseSummaryBody(events, assistantChunks) { + if (assistantChunks.length > 0) { + const text = assistantChunks + .map((event) => cleanTraceText(event.chunk)) + .join("") + .trim(); + const waitingFor = events.at(-1)?.waitingFor ? `waiting=${events.at(-1).waitingFor}` : null; + const recent = text ? compactTraceTextTail(text, 900) : null; + return [ + `compressed=${assistantChunks.length} assistant chunks`, + waitingFor, + recent ? `recent=${recent}` : null + ].filter(Boolean).join("\n"); + } + const labels = new Map(); + for (const event of events) { + const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`); + labels.set(label, (labels.get(label) ?? 0) + 1); + } + return [...labels.entries()] + .slice(0, 6) + .map(([label, count]) => `${readableTraceLabel({ label })} x${count}`) + .join("\n"); +} + +function compactTraceTextTail(text, maxLength) { + const clean = cleanTraceText(text); + if (clean.length <= maxLength) return clean; + return `...${clean.slice(-maxLength)}`; +} + function traceDisplayRow(trace, event, options = {}) { if (!options.includeNoise && isNoisyTraceEvent(event)) return null; const clock = traceClock(event.createdAt); @@ -4019,13 +4180,19 @@ function traceDisplayRow(trace, event, options = {}) { function isNoisyTraceEvent(event) { const label = String(event?.label ?? ""); - if (/assistant:chunk|token_count|outputDelta:chunk/iu.test(label)) return true; - if (event?.type === "assistant_message" && event?.status === "chunk") return true; + if (isAssistantChunkTraceEvent(event)) return true; + if (/token_count|outputDelta:chunk/iu.test(label)) return true; if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true; if (event?.label === "request:accepted" && !event.message) return true; return false; } +function isAssistantChunkTraceEvent(event) { + const label = String(event?.label ?? ""); + return /assistant:chunk/iu.test(label) || + (event?.type === "assistant_message" && event?.status === "chunk"); +} + function readableTraceLabel(event) { const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`); if (label === "request:accepted-short-connection") return "submit short-connection"; @@ -4033,6 +4200,7 @@ function readableTraceLabel(event) { if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start"; if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume"; if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output"; + if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token"; if (label.startsWith("turn:completed")) return "turn completed"; if (label.startsWith("result:completed")) return "result ready"; if (label.startsWith("result:failed")) return "result failed"; diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 92a03072..c3db2311 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -72,17 +72,11 @@ 等待验证 正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。
-
+ @@ -109,16 +103,6 @@
-
-
-
-

工作清单

-

下一步任务草稿

-
- 浏览器本地 -
-
-
@@ -217,6 +201,16 @@ data-prompt-text="通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。" >HWLAB API:DO1=false 复核 DI1 +
hwlab =\s*CODE_AGENT_TIMEOUT_MS/); +assert.match(html, /id="code-agent-timeout"/); +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"), /无新事件/); @@ -443,7 +453,6 @@ for (const workbenchElement of [ "code-agent-summary-label", "code-agent-summary-capability", "code-agent-summary-trace", - "task-list", "right-sidebar", "right-sidebar-resize", "command-form", @@ -794,6 +803,12 @@ assert.match(app, /下载 trace/); assert.match(app, /function renderTraceEventList/); assert.match(app, /function traceDisplayRows/); assert.match(app, /function traceDisplayRow/); +assert.match(app, /function traceNoiseSummaryRow/); +assert.match(app, /assistant stream x\$\{visibleEvents\.length\}/); +assert.match(app, /compressed=\$\{assistantChunks\.length\} assistant chunks/); +assert.match(app, /waiting first assistant token/); +assert.match(app, /function compactTraceTextTail/); +assert.match(app, /function isAssistantChunkTraceEvent/); assert.match(app, /function downloadTraceJson/); assert.match(styles, /\.message-trace-toolbar\s*\{/); assert.match(styles, /\.message-trace-count\s*\{/); @@ -898,6 +913,11 @@ assert.match(styles, /\.message-user\s*{/); assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/); assert.match(styles, /\.message-pending-context\s*{/); assert.match(styles, /\.message-session-context\s*{/); +assert.match(styles, /\.message-compact-details\s*{/); +assert.match(styles, /\.workbench-dialog-layer\s*{/); +assert.match(styles, /\.workbench-dialog\s*{/); +assert.match(app, /function openWorkbenchDialog/); +assert.match(app, /document\.body\.append\(layer\)/); assert.match(styles, /\.message-m3-evidence\s*{/); assert.match(styles, /\.message-m3-rows\s*{/); assert.match(styles, /\.message-m3-row\s*{/); @@ -949,9 +969,11 @@ 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_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/); +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(app, /timeoutMs:\s*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(app, /HWLAB_CLOUD_WEB_CONFIG/); assert.match(app, /sourceKind:\s*"PENDING"/); assert.match(app, /旧 4500ms/); @@ -980,7 +1002,10 @@ assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hid assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s); assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s); assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); -assert.match(styles, /\.(?:compact-list|conversation-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); +assert.match(styles, /\.(?:compact-list|conversation-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); +assert.doesNotMatch(html, /task-panel|task-list|下一步任务草稿/); +assert.doesNotMatch(app, /taskList|renderTaskList/); +assert.doesNotMatch(styles, /\.task-panel|\.task-list/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/); @@ -990,12 +1015,10 @@ assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/); assert.match(app, /fetchJson\("\/v1\/live-builds"\)/); assert.match(html, /id="live-build-latest"/); assert.match(html, /id="live-build-list"/); -assert.match(html, /id="live-build-toggle"[^>]*aria-controls="live-build-popover"[^>]*aria-expanded="false"/); -assert.match(html, /id="live-build-popover"[^>]*role="dialog"[^>]*aria-modal="false"/); -assert.match(html, /id="live-build-close"/); +assert.match(html, /id="live-build-toggle"[^>]*aria-haspopup="dialog"[^>]*aria-expanded="false"/); assert.match(styles, /\.live-build-summary\s*{/); assert.match(app, /initLiveBuildOverlay/); -assert.match(app, /closeLiveBuildOverlay/); +assert.match(app, /openWorkbenchDialog/); assert.match(app, /aria-expanded/); assert.match(app, /formatBeijingTime/); assert.match(app, /Asia\/Shanghai/); @@ -1009,11 +1032,8 @@ assert.match(app, /外部镜像或非 HWLAB 构建产物/); assert.doesNotMatch(app, /artifact report|artifact-report/); assert.match(app, /live health/); assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s); -assert.match(styles, /\.live-build-popover\s*{[^}]*position:\s*absolute;[^}]*max-height:\s*min\(420px,\s*58dvh\);[^}]*overflow:\s*hidden;/s); -assert.match(styles, /\.live-build-summary\[open\]\s+\.live-build-popover\s*{[^}]*display:\s*grid;/s); assert.match(styles, /\.live-build-list\s*{[^}]*min-width:\s*0;[^}]*max-height:\s*min\(352px,\s*calc\(58dvh - 54px\)\);[^}]*overflow:\s*auto;/s); assert.match(styles, /\.live-build-row-head\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*0\.8fr\)\s*minmax\(0,\s*1\.2fr\);/s); -assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-popover\s*{[\s\S]*?width:\s*100%;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*{[\s\S]*?grid-template-columns:\s*1fr;/); assert.match(styles, /\.live-build-meta span\s*{[^}]*overflow-wrap:\s*anywhere;/s); assert.match(app, /fetchJson\("\/health\/live"\)/); @@ -1034,15 +1054,39 @@ assert.match(cloudWebProxy, /text\\\/event-stream/); assert.match(cloudWebProxy, /x-accel-buffering/); assert.match(cloudWebProxy, /flushHeaders/); assert.match(cloudWebProxy, /response\.write\(chunk\)/); +assert.match(cloudWebProxy, /response\.headersSent\)[\s\S]*?response\.destroy\(error\);[\s\S]*?settle\(resolve\);[\s\S]*?return;/); +assert.match(artifactPublisher, /response\.headersSent \|\| response\.writableEnded/); +assert.match(artifactPublisher, /if \(!response\.writableEnded\) response\.destroy\(error\);/); assert.doesNotMatch(artifactPublisher, /await fetch\(target/); assert.match(artifactPublisher, /cloudWebProxyRoutePolicy\(request\.method,\s*url\.pathname\)/); assert.match(cloudWebRoutes, /"\/v1\/agent\/chat"/); assert.match(cloudWebRoutes, /"\/v1\/agent\/chat\/cancel"/); assert.match(cloudWebRoutes, /"\/v1\/m3\/io"/); assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat"/); +assert.match(cloudWebRoutes, /isPublicCodeAgentPollRoute/); +assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/result\//); +assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/trace\//); assert.match(cloudWebRoutes, /const GET_PROXY_PREFIXES = Object\.freeze\(\["\/v1\/"\]\)/); assert.match(cloudWebRoutes, /const GET_PROXY_ROUTES = new Set\(\["\/v1"\]\)/); assert.match(cloudWebRoutes, /authRequired: proxy && !publicRoute/); +assert.match(codexStdioSession, /turn:completed:transport_closed_after_assistant/); +assert.match(codexStdioSession, /turn:completed:idle_timeout_after_assistant/); +assert.match(codexStdioSession, /firstNonEmpty\(finalResponse,\s*assistantText\)/); +assert.match(codexStdioSession, /CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS/); +assert.match(codexStdioSession, /turn:waiting:first_assistant_token/); +assert.match(codexStdioSession, /progressOnly:\s*true/); +assert.match(codexStdioSession, /\["assistant-message", "turn\/completed"\]\.includes\(activity\.waitingFor\)/); +assert.match(codexStdioSession, /does not reset the backend idle timer/); +assert.match(codexStdioSession, /--powershell-stdin/); +assert.match(codexStdioSession, /-EncodedCommand/); +assert.match(codexStdioSession, /Windows filesystem inventory/); +assert.match(codexStdioSession, /Select-Object -First/); +assert.match(codexStdioSession, /ConvertTo-Json -Compress/); +assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\keil/); +assert.match(gatewayShellTool, /function powershellEncodedCommand/); +assert.match(gatewayShellTool, /Buffer\.from\(`\$\{prologue\}\$\{source\}`,\s*"utf16le"\)\.toString\("base64"\)/); +assert.match(gatewayShellTool, /--powershell-stdin/); +assert.match(gatewayShellTool, /powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand/); assert.match(artifactPublisher, /"system\.health"/); assert.match(artifactPublisher, /"cloud\.adapter\.describe"/); assert.match(artifactPublisher, /"audit\.event\.query"/); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index fc27e947..779a1bee 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -461,7 +461,6 @@ h3 { .compact-list, .conversation-list, -.task-list, .hardware-list { min-height: 0; overflow: auto; @@ -564,66 +563,6 @@ h3 { overflow-wrap: anywhere; } -.live-build-popover { - position: absolute; - z-index: 30; - top: calc(100% + 8px); - right: 0; - width: min(100%, 560px); - max-width: 100%; - max-height: min(420px, 58dvh); - display: none; - grid-template-rows: auto minmax(0, 1fr); - gap: 8px; - padding: 10px; - background: rgba(20, 22, 20, 0.99); - border: 1px solid var(--line-strong); - box-shadow: 0 18px 42px rgba(0, 0, 0, 0.42); - overflow: hidden; -} - -.live-build-summary[open] .live-build-popover { - display: grid; -} - -.live-build-popover-head { - min-width: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.live-build-popover-head strong { - min-width: 0; - color: var(--text); - font-family: var(--mono); - font-size: 11px; - line-height: 1.2; - text-transform: none; - overflow-wrap: anywhere; -} - -.live-build-close { - flex: 0 0 auto; - min-height: 26px; - padding: 3px 8px; - border: 1px solid var(--line); - background: var(--surface-2); - color: var(--muted); - font-family: var(--mono); - font-size: 10px; - font-weight: 760; - cursor: pointer; -} - -.live-build-close:hover, -.live-build-close:focus-visible { - border-color: var(--line-strong); - color: var(--text); - outline: 0; -} - .live-build-list { min-width: 0; max-height: min(352px, calc(58dvh - 54px)); @@ -633,6 +572,10 @@ h3 { overscroll-behavior: contain; } +.live-build-list-template { + display: none; +} + .live-build-row { min-width: 0; max-width: 100%; @@ -692,6 +635,89 @@ h3 { overflow-wrap: anywhere; } +.workbench-dialog-layer { + position: fixed; + z-index: 1000; + inset: 0; + display: grid; + place-items: center; + padding: 18px; +} + +.workbench-dialog-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(0, 0, 0, 0.58); + cursor: pointer; +} + +.workbench-dialog { + position: relative; + z-index: 1; + width: min(820px, calc(100vw - 36px)); + max-height: min(720px, calc(100dvh - 36px)); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 10px; + padding: 12px; + background: rgba(20, 22, 20, 0.99); + border: 1px solid var(--line-strong); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.54); + overflow: hidden; +} + +.workbench-dialog-head { + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.workbench-dialog-head strong { + min-width: 0; + color: var(--text); + font-family: var(--mono); + font-size: 11px; + line-height: 1.2; + overflow-wrap: anywhere; +} + +.workbench-dialog-close { + flex: 0 0 auto; + min-height: 28px; + padding: 3px 9px; + border: 1px solid var(--line); + background: var(--surface-2); + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + cursor: pointer; +} + +.workbench-dialog-close:hover, +.workbench-dialog-close:focus-visible { + border-color: var(--line-strong); + color: var(--text); + outline: 0; +} + +.workbench-dialog-content { + min-width: 0; + min-height: 0; + overflow: auto; + overscroll-behavior: contain; +} + +.workbench-dialog-content .live-build-list, +.workbench-dialog-content .message-runtime-grid, +.workbench-dialog-content .message-session-grid { + max-height: none; + overflow: visible; +} + .view { min-height: 0; overflow: auto; @@ -714,7 +740,7 @@ h3 { .conversation-column { min-height: 100%; display: grid; - grid-template-rows: minmax(280px, 1fr) minmax(170px, 0.45fr); + grid-template-rows: minmax(0, 1fr); gap: 10px; } @@ -730,10 +756,6 @@ h3 { grid-template-rows: auto auto minmax(0, 1fr); } -.task-panel { - grid-template-rows: auto minmax(0, 1fr); -} - .help-panel { height: 100%; grid-template-rows: auto minmax(0, 1fr); @@ -741,8 +763,7 @@ h3 { } .conversation-list, -.compact-list, -.task-list { +.compact-list { display: grid; align-content: start; gap: 8px; @@ -993,6 +1014,44 @@ h3 { border-left-width: 3px; } +.message-compact-details { + display: block; + padding: 0; +} + +.message-compact-summary { + width: 100%; + min-width: 0; + min-height: 30px; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 7px; + align-items: center; + padding: 6px 8px; + border: 0; + background: transparent; + color: var(--muted); + text-align: left; + cursor: pointer; +} + +.message-compact-summary-text { + min-width: 0; + color: var(--muted); + font-size: 10px; + line-height: 1.35; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-compact-summary:hover, +.message-compact-summary:focus-visible { + background: rgba(255, 255, 255, 0.03); + color: var(--text); + outline: 0; +} + .message-trace { background: rgba(20, 24, 23, 0.76); border-left-width: 3px; @@ -1514,6 +1573,34 @@ h3 { outline: 0; } +.agent-timeout-control { + grid-column: 1 / -1; + justify-self: start; + display: inline-grid; + grid-template-columns: auto auto; + align-items: center; + gap: 8px; + min-height: 30px; + padding: 4px 8px; + border: 1px solid rgba(94, 142, 231, 0.38); + background: rgba(25, 31, 39, 0.78); + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + line-height: 1.2; +} + +.agent-timeout-control select { + min-width: 88px; + min-height: 24px; + border: 1px solid var(--line); + background: var(--surface); + color: var(--text); + font: inherit; + cursor: pointer; +} + .input-shell { min-width: 0; display: grid; @@ -2399,7 +2486,7 @@ tbody tr:last-child td { } .conversation-column { - grid-template-rows: minmax(210px, 1fr) minmax(150px, 0.45fr); + grid-template-rows: minmax(0, 1fr); gap: 8px; } @@ -2418,6 +2505,11 @@ tbody tr:last-child td { min-height: 30px; } + .agent-timeout-control { + width: 100%; + justify-content: start; + } + .input-shell { padding: 0 8px; } @@ -2441,16 +2533,8 @@ tbody tr:last-child td { gap: 2px; } - .live-build-popover { - right: 0; - left: auto; - width: 100%; - max-width: 100%; - max-height: min(420px, 56dvh); - } - .live-build-list { - max-height: min(352px, calc(56dvh - 54px)); + max-height: min(352px, calc(70dvh - 54px)); } .live-build-time { @@ -2552,6 +2636,15 @@ tbody tr:last-child td { flex-basis: 100%; } + .agent-timeout-control { + grid-template-columns: minmax(0, 1fr); + justify-items: start; + } + + .agent-timeout-control select { + width: 100%; + } + .prompt-mark { display: none; } From aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 17:17:40 +0000 Subject: [PATCH 2/9] fix: stabilize code agent trace cancel and gateway flow --- docs/reference/code-agent-chat-readiness.md | 7 ++ internal/cloud/codex-stdio-session.mjs | 40 +++++++++-- internal/cloud/server.mjs | 26 +++++++- internal/dev-entrypoint/cloud-web-routes.mjs | 3 +- internal/dev-entrypoint/http.test.mjs | 4 +- tools/hwlab-gateway-shell.mjs | 1 + web/hwlab-cloud-web/app.mjs | 70 ++++++++++++-------- web/hwlab-cloud-web/scripts/check.mjs | 34 ++++++++-- web/hwlab-cloud-web/styles.css | 7 -- 9 files changed, 143 insertions(+), 49 deletions(-) diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index 3e3124bb..66a6c392 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -98,6 +98,13 @@ PS1 PowerShell 默认使用 wrapper 的 `-EncodedCommand` 路径;不要手写 `cmd /c powershell ...` 的管道、引号或中文转义。简单 `cmd` 命令仍可用 `--command "cmd /d /s /c ..."`,但涉及目录枚举、Unicode、管道、排序或 JSON 输出时优先用 `--powershell-stdin`。 +Windows 侧 skill、编译器、脚本工具和多参数命令都应走同一个通用传输模式,不新增某个工具的专用 wrapper 子命令: + +- 工作目录优先用 wrapper 的 `--cwd ` 或脚本内 `Set-Location -LiteralPath ` 表达,不要在 prompt 里拼 `cmd /c "cd ... && ..."`。 +- 命令参数在 PowerShell 脚本里用数组或显式变量传递,例如 `$args = @("subcommand", "-p", $projectPath)` 后 `& $exe @args`,避免让模型手动嵌套引号。 +- 对所有 Windows skill 都先复用 `C:\Users\liang\.agents\skills\\SKILL.md` 和该 skill 自带 CLI;Code Agent 只负责通过 repo wrapper 调用,不把 skill 行为复制到 cloud-api 或 wrapper。 +- 一次探测命令失败后,只做一次更窄、更结构化的修正;若仍失败,返回失败 `operationId`、stderr 摘要和下一步,而不是连续试错。 + Windows 文件系统探测必须是有界小输出: - 对 `F:\work`、`F:\work\ConStart` 或同类目录先做顶层目录/项目标记探测,不要读取 Secret、env、kubeconfig、DB URL 或完整源码内容。 diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index 4813f2f3..db06fd5a 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -95,10 +95,11 @@ const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [ "Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.", "Do not call gateway, box-simu, or patch-panel directly.", "Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.", - "For registered PC gateway Windows cmd or Keil requests, invoke the repo-owned wrapper with the Codex exec tool: node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --command \"cmd /c ...\".", - "For PowerShell through the PC gateway, do not hand-escape cmd pipes or quotes; use node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin with a single-quoted heredoc. The wrapper sends powershell.exe -EncodedCommand for Unicode-safe execution.", + "For registered PC gateway Windows command or skill requests, invoke the repo-owned wrapper with the Codex exec tool: node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin. Put scripts on stdin and set working directory with wrapper --cwd or PowerShell Set-Location -LiteralPath; do not hand-build compound cmd /c, cd &&, pipes, or nested quotes.", + "For PowerShell through the PC gateway, do not hand-escape cmd pipes or quotes; use a single-quoted heredoc with --powershell-stdin. The wrapper sends powershell.exe -EncodedCommand for Unicode-safe execution.", "For Windows filesystem inventory, start with one small bounded PowerShell stdin script: use -LiteralPath, Select-Object -First, ConvertTo-Json -Compress, and keep stdout under about 12 KB. Do not dump full directory JSON and then retry.", - "For F:\\work or F:\\work\\ConStart discovery, first list top-level project markers and *.uvprojx candidates with bounded output. For Keil build/program requests, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\keil with py -3 keil-cli.py; do not reimplement Keil build logic.", + "For Windows-side skills under C:\\Users\\liang\\.agents\\skills\\, read the skill manifest when needed and call its CLI from a PowerShell stdin script using explicit paths or argument arrays. Do not reimplement skill logic, and do not use shell working-directory tricks like cd &&.", + "For F:\\work or F:\\work\\ConStart discovery, first list top-level project markers and *.uvprojx candidates with bounded output. For Keil build/program requests, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\keil with py -3 keil-cli.py from the generic PowerShell stdin wrapper path; do not reimplement Keil build logic.", "If a Windows gateway command reaches the gateway but fails due script syntax or output size, simplify once and report the failed operationId plus the corrected bounded command evidence. Do not spend multiple turns on exploratory rewrites.", "Use the Windows-side skill CLIs under C:\\Users\\liang\\.agents\\skills\\ from that cmd command when they exist; do not reimplement those tools in the prompt.", "Do not satisfy PC gateway requests by string matching, assistant-only claims, internal shortcut dispatch, or direct gateway URLs; the Codex turn must run the wrapper and report command evidence.", @@ -1225,9 +1226,11 @@ function tomlString(value) { export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, args = null, env = process.env, cwd = repoRoot, onNotification = null } = {}) { const spawnArgs = Array.isArray(args) ? args : codexAppServerArgs(env); + const childDetached = process.platform !== "win32"; const child = spawn(command, spawnArgs, { cwd, env, + detached: childDetached, stdio: ["pipe", "pipe", "pipe"] }); let nextId = 1; @@ -1402,7 +1405,7 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD function close() { if (closed) return; - child.kill("SIGTERM"); + terminateChildProcess(child, { detached: childDetached }); } return { @@ -1416,6 +1419,35 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD }; } +function terminateChildProcess(child, { detached = false } = {}) { + if (!child || child.killed) return; + try { + if (detached && Number.isInteger(child.pid)) { + process.kill(-child.pid, "SIGTERM"); + } else { + child.kill("SIGTERM"); + } + } catch { + try { + child.kill("SIGTERM"); + } catch { + return; + } + } + setTimeout(() => { + if (child.killed) return; + try { + if (detached && Number.isInteger(child.pid)) { + process.kill(-child.pid, "SIGKILL"); + } else { + child.kill("SIGKILL"); + } + } catch { + // Process already exited or the runtime does not allow group signaling. + } + }, 2000).unref?.(); +} + async function runAppServerTurn({ client, session, diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index b6bbe945..f0b79e13 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -1337,6 +1337,7 @@ function submitCodeAgentChatTurn({ params, options, traceId }) { const run = async () => { try { const payload = await runCodeAgentChat(params, options); + if (isCodeAgentResultCanceled(results.get(traceId))) return; results.set(traceId, payload); traceStore.append(traceId, { type: "result", @@ -1350,6 +1351,7 @@ function submitCodeAgentChatTurn({ params, options, traceId }) { terminal: true }); } catch (error) { + if (isCodeAgentResultCanceled(results.get(traceId))) return; const payload = { status: "failed", traceId, @@ -1575,7 +1577,7 @@ async function handleCodeAgentCancelHttp(request, response, options) { ...runnerTraceSnapshot, sessionLifecycleStatus: sessionSummary.status }; - sendJson(response, 200, { + const payload = { accepted: true, canceled: true, status: "canceled", @@ -1589,8 +1591,26 @@ async function handleCodeAgentCancelHttp(request, response, options) { runnerTrace, lastTraceEvent: runnerTrace.lastEvent, retryable: true, - userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。" - }); + error: { + code: "codex_stdio_canceled", + layer: "session", + category: "canceled", + retryable: true, + message: "user canceled current Code Agent request", + userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。", + traceId, + route: "/v1/agent/chat/cancel", + toolName: "codex-stdio.cancel" + }, + userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。", + updatedAt: new Date().toISOString() + }; + options.codeAgentChatResults?.set(traceId, payload); + sendJson(response, 200, payload); +} + +function isCodeAgentResultCanceled(result) { + return result?.status === "canceled" || result?.canceled === true || result?.error?.code === "codex_stdio_canceled"; } function cancelBlockedPayload({ diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index 61568031..05d7c04e 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -7,7 +7,8 @@ const POST_PROXY_ROUTES = new Set([ "/v1/m3/io" ]); const PUBLIC_PROXY_ROUTES = new Set([ - "POST /v1/agent/chat" + "POST /v1/agent/chat", + "POST /v1/agent/chat/cancel" ]); export function cloudWebProxyRoutePolicy(method, pathname) { diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index 8c04bc11..3b60eff9 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -14,8 +14,8 @@ test("cloud web route policy proxies public Code Agent chat without gating other }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/cancel"), { proxy: true, - authRequired: true, - publicRoute: false, + authRequired: false, + publicRoute: true, routeKey: "POST /v1/agent/chat/cancel" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/m3/io"), { diff --git a/tools/hwlab-gateway-shell.mjs b/tools/hwlab-gateway-shell.mjs index 45234f97..1d390335 100644 --- a/tools/hwlab-gateway-shell.mjs +++ b/tools/hwlab-gateway-shell.mjs @@ -99,6 +99,7 @@ function printHelp() { " --powershell-file PATH Read a local PS1 file, encode it, and run through the gateway", " --powershell-stdin Read PS1 from stdin, encode it, and run through the gateway", " --powershell-exe NAME Default powershell.exe", + " --cwd WINDOWS_PATH Gateway-side working directory; prefer this over shell-level cd &&", " --timeout-ms N Short dispatch timeout in milliseconds", " --project-id ID Default prj_mvp_topology", " --gateway-session-id ID Default gws_DESKTOP-1MHOD9I", diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 27ee5839..d5000d79 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -20,13 +20,14 @@ import { const DEFAULT_API_TIMEOUT_MS = 4500; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000; const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000; +const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000; const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs(); const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: 5000, max: 120000 }); -const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; +const CODE_AGENT_CANCEL_TIMEOUT_MS = resolveTimeoutMs("codeAgentCancelTimeoutMs", DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS, { min: 5000, max: 120000 }); const rpcReadMethods = Object.freeze([ "system.health", "cloud.adapter.describe", @@ -174,6 +175,7 @@ const state = { codeAgentAvailability: null, chatMessages: [], traceStreams: new Map(), + traceDetailsOpen: new Map(), canceledTraces: new Set(), currentRequest: null, sessionStatus: null, @@ -760,6 +762,7 @@ function initCommandBar() { el.commandClear.addEventListener("click", () => { for (const close of state.traceStreams.values()) close(); state.traceStreams.clear(); + state.traceDetailsOpen.clear(); state.chatMessages = []; state.conversationId = null; state.sessionId = null; @@ -3692,6 +3695,7 @@ async function cancelAgentMessage(messageId) { const runnerTrace = payload.runnerTrace ?? latestTraceSnapshot(traceId); if (response.ok && payload.canceled === true) { state.canceledTraces.add(traceId); + stopRunningTraceStream(traceId); state.chatPending = false; state.currentRequest = null; state.sessionId = payload.sessionId || sessionId || state.sessionId; @@ -3743,14 +3747,23 @@ async function cancelAgentMessage(messageId) { renderRecords(state.liveSurface); } +function stopRunningTraceStream(traceId) { + const id = nonEmptyString(traceId); + if (!id) return; + const close = state.traceStreams.get(id); + if (typeof close === "function") close(); + state.traceStreams.delete(id); +} + async function cancelAgentRequest({ traceId, conversationId, sessionId, threadId }) { return fetchJson("/v1/agent/chat/cancel", { method: "POST", headers: { "Content-Type": "application/json", - "X-Trace-Id": traceId + "X-Trace-Id": traceId, + "Prefer": "respond-async" }, - timeoutMs: Math.min(API_TIMEOUT_MS, 5000), + timeoutMs: CODE_AGENT_CANCEL_TIMEOUT_MS, timeoutName: "Code Agent cancel", body: JSON.stringify({ traceId, @@ -4011,7 +4024,14 @@ function messageTracePanel(message) { if (!trace && !message.traceId) return null; const details = document.createElement("details"); details.className = "message-trace"; - details.open = ["running", "completed", "source", "failed", "timeout", "canceled", "error"].includes(message.status); + const traceUiKey = messageTraceUiKey(message); + const storedOpen = traceUiKey ? state.traceDetailsOpen.get(traceUiKey) : undefined; + details.open = typeof storedOpen === "boolean" ? storedOpen : defaultTraceDetailsOpen(message); + if (traceUiKey) { + details.addEventListener("toggle", () => { + state.traceDetailsOpen.set(traceUiKey, details.open); + }); + } const summary = document.createElement("summary"); summary.className = "message-meta"; summary.textContent = runnerTraceHeadline(message, trace); @@ -4024,9 +4044,9 @@ function messageTracePanel(message) { item.textContent = `trace=${message.traceId ?? "pending"};等待后端事件。`; list.append(item); } else { - const toolbar = messageTraceToolbar(message, trace, events, rows, list); - list.dataset.traceMode = rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? "tail" : "all"; - renderTraceEventList(list, tracePreviewRows(rows)); + const toolbar = messageTraceToolbar(message, trace, events, rows); + list.dataset.traceMode = "all"; + renderTraceEventList(list, rows); details.append(summary, toolbar, list); return details; } @@ -4034,35 +4054,29 @@ function messageTracePanel(message) { return details; } -function messageTraceToolbar(message, trace, events, rows, list) { +function messageTraceUiKey(message) { + const traceId = nonEmptyString(message.traceId); + const messageId = nonEmptyString(message.id ?? message.messageId); + if (!traceId && !messageId) return null; + return `${messageId ?? "message"}:${traceId ?? "trace"}`; +} + +function defaultTraceDetailsOpen(message) { + return ["running", "completed", "source", "failed", "timeout", "canceled", "error"].includes(message.status); +} + +function messageTraceToolbar(message, trace, events, rows) { const toolbar = document.createElement("div"); toolbar.className = "message-trace-toolbar"; - const count = textSpan(messageTraceCountText(events.length, rows.length, false), "message-trace-count"); + const count = textSpan(messageTraceCountText(events.length, rows.length), "message-trace-count"); toolbar.append(count); - if (rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT) { - let expanded = false; - const toggle = traceActionButton("展开全部", () => { - expanded = !expanded; - renderTraceEventList(list, expanded ? rows : tracePreviewRows(rows)); - list.classList.toggle("message-trace-events-full", expanded); - list.dataset.traceMode = expanded ? "all" : "tail"; - count.textContent = messageTraceCountText(events.length, rows.length, expanded); - toggle.textContent = expanded ? "收起为最近 14" : "展开全部"; - }, "在面板内查看完整 trace 时间线"); - toolbar.append(toggle); - } toolbar.append(traceActionButton("复制 JSON", () => copyTextToClipboard(messageTraceJson(message, trace, events)), "复制完整 trace JSON")); toolbar.append(traceActionButton("下载 trace", () => downloadTraceJson(message, trace, events), "下载完整 trace JSON 文件")); return toolbar; } -function tracePreviewRows(rows) { - return rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? rows.slice(-CODE_AGENT_TRACE_PREVIEW_LIMIT) : rows; -} - -function messageTraceCountText(rawTotal, displayTotal, expanded) { - if (displayTotal <= CODE_AGENT_TRACE_PREVIEW_LIMIT || expanded) return `显示全部 ${displayTotal} / 原始 ${rawTotal}`; - return `显示最近 ${CODE_AGENT_TRACE_PREVIEW_LIMIT} / 可读 ${displayTotal} / 原始 ${rawTotal}`; +function messageTraceCountText(rawTotal, displayTotal) { + return `显示全部 ${displayTotal} / 原始 ${rawTotal}`; } function renderTraceEventList(list, rows) { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 4aa861e1..756308d2 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -66,6 +66,7 @@ const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/ const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8"); const cloudWebRoutes = fs.readFileSync(path.resolve(repoRoot, "internal/dev-entrypoint/cloud-web-routes.mjs"), "utf8"); const cloudWebProxy = fs.readFileSync(path.resolve(repoRoot, "internal/dev-entrypoint/cloud-web-proxy.mjs"), "utf8"); +const cloudApiServer = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/server.mjs"), "utf8"); const codexStdioSession = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/codex-stdio-session.mjs"), "utf8"); const gatewayShellTool = fs.readFileSync(path.resolve(repoRoot, "tools/hwlab-gateway-shell.mjs"), "utf8"); const frontendSource = `${html}\n${auth}\n${app}\n${artifactPublisher}`; @@ -790,14 +791,27 @@ assert.match(app, /codexStdioFeasibility:\s*result\.codexStdioFeasibility/); assert.match(app, /longLivedSessionGate:\s*result\.longLivedSessionGate/); assert.match(app, /conversationFacts:\s*result\.conversationFacts/); assert.match(app, /"Prefer":\s*"respond-async"/); +assert.match(app, /DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS\s*=\s*30000/); +assert.match(app, /CODE_AGENT_CANCEL_TIMEOUT_MS/); +assert.match(functionBody(app, "cancelAgentRequest"), /timeoutMs:\s*CODE_AGENT_CANCEL_TIMEOUT_MS/); +assert.match(app, /function stopRunningTraceStream/); +assert.match(functionBody(app, "cancelAgentMessage"), /stopRunningTraceStream\(traceId\)/); assert.match(app, /function waitForAgentMessageResult/); assert.match(app, /\/v1\/agent\/chat\/result\/\$\{encodeURIComponent\(traceId\)\}/); assert.match(app, /function messageTracePanel/); -assert.match(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/); assert.match(app, /function messageTraceToolbar/); assert.match(app, /function messageTraceCountText/); -assert.match(app, /显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+可读\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/); -assert.match(app, /展开全部/); +assert.match(app, /显示全部\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/); +assert.match(app, /traceDetailsOpen:\s*new Map\(\)/); +assert.match(app, /state\.traceDetailsOpen\.clear\(\)/); +assert.match(app, /function messageTraceUiKey/); +assert.match(app, /function defaultTraceDetailsOpen/); +assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.get\(traceUiKey\)/); +assert.match(functionBody(app, "messageTracePanel"), /defaultTraceDetailsOpen\(message\)/); +assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.set\(traceUiKey,\s*details\.open\)/); +assert.match(functionBody(app, "messageTracePanel"), /list\.dataset\.traceMode\s*=\s*"all"/); +assert.match(functionBody(app, "messageTracePanel"), /renderTraceEventList\(list,\s*rows\)/); +assert.doesNotMatch(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT|tracePreviewRows|收起为最近|展开全部|data-trace-mode="tail"/); assert.match(app, /复制 JSON/); assert.match(app, /下载 trace/); assert.match(app, /function renderTraceEventList/); @@ -813,7 +827,7 @@ assert.match(app, /function downloadTraceJson/); assert.match(styles, /\.message-trace-toolbar\s*\{/); assert.match(styles, /\.message-trace-count\s*\{/); assert.match(styles, /\.message-trace-action\s*\{/); -assert.match(styles, /\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/); +assert.doesNotMatch(styles, /\.message-trace-events-full/); assert.match(app, /function messageRuntimePathPanel/); assert.match(app, /codeAgentRuntimePathFromMessage/); assert.match(app, /function runnerTraceHeadline/); @@ -1063,6 +1077,7 @@ assert.match(cloudWebRoutes, /"\/v1\/agent\/chat"/); assert.match(cloudWebRoutes, /"\/v1\/agent\/chat\/cancel"/); assert.match(cloudWebRoutes, /"\/v1\/m3\/io"/); assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat"/); +assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat\/cancel"/); assert.match(cloudWebRoutes, /isPublicCodeAgentPollRoute/); assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/result\//); assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/trace\//); @@ -1079,6 +1094,8 @@ assert.match(codexStdioSession, /\["assistant-message", "turn\/completed"\]\.inc assert.match(codexStdioSession, /does not reset the backend idle timer/); assert.match(codexStdioSession, /--powershell-stdin/); assert.match(codexStdioSession, /-EncodedCommand/); +assert.match(codexStdioSession, /do not hand-build compound cmd \/c, cd &&, pipes, or nested quotes/); +assert.match(codexStdioSession, /Set-Location -LiteralPath/); assert.match(codexStdioSession, /Windows filesystem inventory/); assert.match(codexStdioSession, /Select-Object -First/); assert.match(codexStdioSession, /ConvertTo-Json -Compress/); @@ -1086,7 +1103,16 @@ assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\k assert.match(gatewayShellTool, /function powershellEncodedCommand/); assert.match(gatewayShellTool, /Buffer\.from\(`\$\{prologue\}\$\{source\}`,\s*"utf16le"\)\.toString\("base64"\)/); assert.match(gatewayShellTool, /--powershell-stdin/); +assert.match(gatewayShellTool, /--cwd WINDOWS_PATH/); assert.match(gatewayShellTool, /powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand/); +assert.doesNotMatch(gatewayShellTool, /keil-find|keil-build|keil-job-status/); +assert.match(codexStdioSession, /detached:\s*childDetached/); +assert.match(codexStdioSession, /function terminateChildProcess/); +assert.match(codexStdioSession, /process\.kill\(-child\.pid,\s*"SIGTERM"\)/); +assert.match(codexStdioSession, /process\.kill\(-child\.pid,\s*"SIGKILL"\)/); +assert.match(cloudApiServer, /function isCodeAgentResultCanceled/); +assert.match(cloudApiServer, /options\.codeAgentChatResults\?\.set\(traceId,\s*payload\)/); +assert.match(cloudApiServer, /if \(isCodeAgentResultCanceled\(results\.get\(traceId\)\)\) return;/); assert.match(artifactPublisher, /"system\.health"/); assert.match(artifactPublisher, /"cloud\.adapter\.describe"/); assert.match(artifactPublisher, /"audit\.event\.query"/); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 779a1bee..0a451060 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -1416,13 +1416,6 @@ h3 { list-style: none; } -.message-trace-events-full { - max-height: min(360px, 42vh); - overflow: auto; - overscroll-behavior: contain; - padding-right: 4px; -} - .message-trace-row { min-width: 0; display: grid; From 6409e108771007b1f020970ed85b93c642d82520 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 17:43:35 +0000 Subject: [PATCH 3/9] docs: clarify dev cd promotion after publish --- docs/reference/deployment-publish.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index 1769d7bc..5488f149 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -526,6 +526,15 @@ node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-rep node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check ``` +`ci-publish` only proves that images for its `sourceCommitId` were built and +pushed; it does not change the rollout target by itself. Before any formal +`dev-cd-apply`, operators must refresh `deploy/artifact-catalog.dev.json`, +`deploy/k8s/base/workloads.yaml`, and `deploy/deploy.json`, commit/push that +promotion, and verify the desired-state plan points at the intended +`sourceCommitId`. If `deploy/deploy.json` still names an older commit, CD will +correctly keep deploying that older promotion even when a newer publish report +exists. Do not treat the newest CI publish report as implicit CD desired state. + If publish is blocked, keep the report blocked rather than inventing digests: ```sh From 0463bb8b8e3039e5096392302c83a3425a824292 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 25 May 2026 01:45:12 +0800 Subject: [PATCH 4/9] chore: promote dev artifacts for aef1a00 --- deploy/artifact-catalog.dev.json | 160 +++++++++++++++---------------- deploy/deploy.json | 42 ++++---- deploy/k8s/base/workloads.yaml | 40 ++++---- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 1a06bd8f..e36fba8f 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,12 +5,12 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "4d478a1", + "commitId": "aef1a00", "artifactState": "published", "publish": { "ciPublished": true, "registryVerified": true, - "provenance": "ci-artifact:ci-publish-20260524T143621-684be5", + "provenance": "ci-artifact:ci-publish-20260524T174119-e7ea3b", "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." }, "healthContract": { @@ -66,10 +66,10 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:3e55a7f9b0c50bd3747e6ad6cd6b271afc3fd02c19ec0b1ac8efa2995a3e70c8", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:d92691e66e6f0e201a9b0a84459caef467681928fd12e4e2ddab72f419dd2904", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -79,15 +79,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-cloud-web", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:e5bd7b1f4b8153d6d26396bed9b29af173cedd9caf4ef00f77624f60f16ac7ce", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:5e5f70400cb560f28bfedbb9e05f88cec000017e5156a80f15683b0b56a3f0f5", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -97,15 +97,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-agent-mgr", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:eb4128008a69a259c9c270d06d74b6cd4b1bd29b141b4c43d99be96644c9b6f0", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:013477352f1d28b292302cb07ce26e9727a5db3952ddc811cfe5fe503e737b70", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -115,15 +115,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-agent-worker", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:c4bdab9dcc5ed396281d1611e603e698cb387586da95a026f6cf74f977badc2b", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:dcd207350c1b7e282ec4dba59ab59a647b8a75c936ce50b88a3ed005a92547cd", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -133,15 +133,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-gateway", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:1c9d78026999e3ba7cfddb2a1948a51ec8e04f7dc73dd3fb97e00fd474b8332d", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:e3f85b61395312b8d25588354ccac47448147ec69681e448023298dc5d5b5121", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -151,15 +151,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-gateway-simu", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:bb6e366a54e78cec7165d77adbafe74c811f6dbcd71353875488c0a68b82428f", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:62eb72a7d8b93d131f9adf438e78d8fd399f1e026d87bb516f784220061bfb62", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -169,15 +169,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-box-simu", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:81e45e2a58cf30889ed10ba79bac3676a4ef88d9b35a0bc4001c16e0ccfeb228", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:3c7c9e124a4ffb27b2b9ac3d91ecda7b77723fe407f3972559707ccab3f33d59", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -187,15 +187,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-patch-panel", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:929db2773987804782b77cec01c922d8e530909839ae7426bfefc80dd7e55793", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:9e3659bdbf6ec22203498e5ceb9147a2020f6ec96154922843b601ddb68b6a53", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -205,15 +205,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-router", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:def49d50e855154a4c6f3e7187c620c4a407a81cca0ea7551c341521049979d4", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:7c54855e6025f7d600dcf319751914a0333c415e6dd787551b28ff6ec66cc516", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -223,15 +223,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-tunnel-client", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:9f04b587d9f27ae484a20aa71f2def5ac56207174cebb0ddd741b0747c0c129d", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:97c3392509c215cff0fbccbb991037b4a9a4adc0b41ba2d31efdd3d21109adcc", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -241,15 +241,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-edge-proxy", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:08d442ae21dd4f15b1676361dd8549d5e0378cb02f711003754e49bbe8b4a3f2", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:c0e70d434aa21bdeaba9de4204717569a1067c5b6cbb5a6beb0327a2bd513c8d", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -259,15 +259,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-cli", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:11a2605ca145dd9ea4d9790924fd0c57764ea940661023759b9f0f0ce60c4ace", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:641d0114b3979835be93f81c9c0269880e747dde72b14e9b212b44b998ce9ccd", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -277,15 +277,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" }, { "serviceId": "hwlab-agent-skills", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:97bbb94cadd6b21011cb65719e642bc9a385d3cc5c1fe47cac75f5626b4e6734", + "commitId": "aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", + "imageTag": "aef1a00", + "digest": "sha256:ce4ff079941ffcf9fb0627a9e95d35947ba38b0b383536dcdb71a6c10e7d0d95", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -295,8 +295,8 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T17:41:21.664Z", + "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" } ], "serviceInventory": { diff --git a/deploy/deploy.json b/deploy/deploy.json index 99d5a6f4..d6e9b2bd 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "4d478a1", + "commitId": "aef1a00", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,9 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_COMMIT_ID": "4d478a1", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1", - "HWLAB_IMAGE_TAG": "4d478a1", + "HWLAB_COMMIT_ID": "aef1a00", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", + "HWLAB_IMAGE_TAG": "aef1a00", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -222,7 +222,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -231,14 +231,14 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", - "HWLAB_COMMIT_ID": "4d478a1", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", - "HWLAB_IMAGE_TAG": "4d478a1" + "HWLAB_COMMIT_ID": "aef1a00", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", + "HWLAB_IMAGE_TAG": "aef1a00" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -250,7 +250,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -262,7 +262,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -274,7 +274,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -294,7 +294,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -313,7 +313,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -334,7 +334,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -346,7 +346,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -360,7 +360,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -373,7 +373,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -384,13 +384,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "4d478a1" + "HWLAB_SKILLS_COMMIT_ID": "aef1a00" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index b7904e34..67e70095 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", "ports": [ { "name": "http", @@ -49,15 +49,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "4d478a1" + "value": "aef1a00" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00" }, { "name": "HWLAB_IMAGE_TAG", - "value": "4d478a1" + "value": "aef1a00" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -279,7 +279,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", "ports": [ { "name": "http", @@ -297,15 +297,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "4d478a1" + "value": "aef1a00" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00" }, { "name": "HWLAB_IMAGE_TAG", - "value": "4d478a1" + "value": "aef1a00" } ], "readinessProbe": { @@ -355,7 +355,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", "ports": [ { "name": "http", @@ -411,7 +411,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -454,7 +454,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", "ports": [ { "name": "http", @@ -515,7 +515,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", "ports": [ { "name": "http", @@ -588,7 +588,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", "ports": [ { "name": "http", @@ -656,7 +656,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", "ports": [ { "name": "http", @@ -720,7 +720,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", "ports": [ { "name": "http", @@ -780,7 +780,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", "ports": [ { "name": "http", @@ -852,7 +852,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", "ports": [ { "name": "http", @@ -917,7 +917,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -959,7 +959,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", "ports": [ { "name": "http", @@ -969,7 +969,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "4d478a1" + "value": "aef1a00" } ], "readinessProbe": { From 3ebbbbf6254f776b589f16a4c50a48d76c3804c2 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 17:52:34 +0000 Subject: [PATCH 5/9] fix: remove unmanaged hotfix overrides during dev cd --- docs/reference/deployment-publish.md | 9 + docs/reference/dev-runtime-hotfix-runbook.md | 4 +- scripts/src/dev-deploy-apply.mjs | 302 ++++++++++++++++++- scripts/src/dev-deploy-apply.test.mjs | 59 ++++ 4 files changed, 369 insertions(+), 5 deletions(-) diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index 5488f149..9449ac3a 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -169,6 +169,15 @@ node tools/hwlab-cli/bin/hwlab-cli.mjs cicd report 非阻塞 DEV CD job,产出通过报告。 - CI 产物事实以 registry manifest 和 artifact catalog 为准;临时 `/tmp/hwlab-dev-gate/*.json` 只用于审计和耗时排障,不得成为 CD 放行硬依赖。 +- `kubectl apply -k` 不会自动移除由运行面 `kubectl patch`、临时 ConfigMap + 热修或其他 field manager 写入、且不属于 desired manifest / last-applied 的 + Deployment `volumes`、`volumeMounts` 或 template annotations。正式 CD 不能把 + “镜像 tag 已更新”误判为“热修覆盖已撤销”。DEV CD apply 必须在 apply 前比对 + desired Deployment 与 live Deployment:若 live 存在 desired 不包含、且明确带 + `hotfix`/`override` 语义的额外覆盖字段,应在 `hwlab-dev` 内删除该 desired + Deployment,再由同一次 `kubectl apply -k deploy/k8s/dev` 从源码 desired-state + 重新创建;报告中记录 cleanup 计划和实际动作。不要把这种 cleanup 做成某个服务 + 或某个 skill 的特例。 ## DEV CD Transaction diff --git a/docs/reference/dev-runtime-hotfix-runbook.md b/docs/reference/dev-runtime-hotfix-runbook.md index 80e6a70e..c621e581 100644 --- a/docs/reference/dev-runtime-hotfix-runbook.md +++ b/docs/reference/dev-runtime-hotfix-runbook.md @@ -112,7 +112,9 @@ node scripts/dev-runtime-hotfix-audit.mjs --collect-readonly --pretty ## 回滚口径 -优先回滚方式是用源码化 artifact/CD 覆盖 runtime hotfix:#460/#461 合并并发布后,Deployment 应消费正式镜像,不再通过 ConfigMap 覆盖 `/app/internal/cloud/code-agent-chat.mjs`。只读确认口径是: +优先回滚方式是用源码化 artifact/CD 覆盖 runtime hotfix:#460/#461 合并并发布后,Deployment 应消费正式镜像,不再通过 ConfigMap 覆盖 `/app/internal/cloud/code-agent-chat.mjs`。注意 `kubectl apply -k` 可能保留运行面 patch 写入、且源码 desired-state 不拥有的 hotfix `volumes`、`volumeMounts` 或 template annotations;正式 DEV CD apply 应先识别这种 unmanaged hotfix 覆盖,删除对应 desired Deployment,再由同一次 apply 从源码重新创建。 + +只读确认口径是: ```sh export KUBECONFIG=/etc/rancher/k3s/k3s.yaml diff --git a/scripts/src/dev-deploy-apply.mjs b/scripts/src/dev-deploy-apply.mjs index 773757a0..d6745a2a 100755 --- a/scripts/src/dev-deploy-apply.mjs +++ b/scripts/src/dev-deploy-apply.mjs @@ -71,6 +71,14 @@ const devLegacySimulatorDeploymentCleanupPolicy = { const cleanableLegacySimulatorDeploymentNames = new Set( devLegacySimulatorDeploymentCleanupPolicy.allowedDeployments.map((deployment) => deployment.name) ); +const devUnmanagedHotfixDeploymentCleanupPolicy = { + status: "active", + namespace, + scope: "DEV-only unmanaged live Deployment hotfix/override fields", + reason: + "kubectl apply preserves fields owned by a live hotfix field manager; delete desired Deployments with unmanaged hotfix fields so apply recreates them from source truth", + matchPatterns: ["hotfix", "override"] +}; const requiredValidationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs", @@ -515,6 +523,75 @@ function isDesiredLegacySimulatorReplacement(item) { ); } +function deploymentKey(item) { + return `${itemNamespace(item)}/${item?.metadata?.name ?? "unknown"}`; +} + +function isDesiredDeployment(item) { + return item?.kind === "Deployment" && itemNamespace(item) === namespace && typeof item?.metadata?.name === "string"; +} + +function objectTextContainsHotfixMarker(value) { + if (value === null || value === undefined) return false; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return /\bhotfix\b|override/iu.test(String(value)); + } + if (Array.isArray(value)) return value.some((entry) => objectTextContainsHotfixMarker(entry)); + if (typeof value === "object") { + return Object.entries(value).some(([key, entry]) => + objectTextContainsHotfixMarker(key) || objectTextContainsHotfixMarker(entry) + ); + } + return false; +} + +function volumeIdentity(volume) { + const parts = [volume?.name ?? ""]; + if (volume?.configMap?.name) parts.push(`configMap:${volume.configMap.name}`); + if (volume?.secret?.secretName) parts.push(`secret:${volume.secret.secretName}`); + if (volume?.persistentVolumeClaim?.claimName) parts.push(`pvc:${volume.persistentVolumeClaim.claimName}`); + return parts.join("|"); +} + +function volumeMountIdentity(containerName, mount) { + return [ + containerName ?? "", + mount?.name ?? "", + mount?.mountPath ?? "", + mount?.subPath ?? "", + mount?.readOnly === true ? "ro" : "rw" + ].join("|"); +} + +function templateAnnotationIdentity(key, value) { + return `${key}=${String(value ?? "")}`; +} + +function desiredDeploymentIndex(workloads) { + return new Map( + listItems(workloads) + .filter(isDesiredDeployment) + .map((item) => [deploymentKey(item), item]) + ); +} + +function deploymentVolumes(item) { + return item?.spec?.template?.spec?.volumes ?? []; +} + +function deploymentContainerMounts(item) { + return (item?.spec?.template?.spec?.containers ?? []).flatMap((container) => + (container.volumeMounts ?? []).map((mount) => ({ + containerName: container.name, + mount + })) + ); +} + +function deploymentTemplateAnnotations(item) { + return item?.spec?.template?.metadata?.annotations ?? {}; +} + export function decideDevTemplateJobReplacement({ jobName, jobNamespace = namespace, @@ -651,6 +728,99 @@ export function decideDevLegacySimulatorDeploymentCleanup({ }; } +export function decideDevUnmanagedHotfixDeploymentCleanup({ + deploymentName, + deploymentNamespace = namespace, + desiredDeployment, + liveDeployment +}) { + const base = { + namespace: deploymentNamespace, + deploymentName, + desiredExists: Boolean(desiredDeployment), + liveExists: Boolean(liveDeployment), + cleanup: false, + result: "not_needed", + reason: "Live Deployment has no unmanaged hotfix or override fields outside desired source.", + unmanagedVolumes: [], + unmanagedVolumeMounts: [], + unmanagedTemplateAnnotations: [] + }; + + if (deploymentNamespace !== namespace) { + return { + ...base, + result: "blocked", + reason: `Unmanaged hotfix cleanup is DEV-only and refuses namespace ${deploymentNamespace}.` + }; + } + if (!desiredDeployment) { + return { + ...base, + result: "ignored", + reason: "No desired Deployment exists in source; cleanup must not delete unknown live Deployments." + }; + } + if (!liveDeployment) { + return { + ...base, + result: "not_found", + reason: "Live Deployment is absent; kubectl apply can create it from desired source." + }; + } + + const desiredVolumeIdentities = new Set(deploymentVolumes(desiredDeployment).map(volumeIdentity)); + const desiredMountIdentities = new Set( + deploymentContainerMounts(desiredDeployment).map(({ containerName, mount }) => volumeMountIdentity(containerName, mount)) + ); + const desiredAnnotationIdentities = new Set( + Object.entries(deploymentTemplateAnnotations(desiredDeployment)).map(([key, value]) => + templateAnnotationIdentity(key, value) + ) + ); + const unmanagedVolumes = deploymentVolumes(liveDeployment) + .filter((volume) => !desiredVolumeIdentities.has(volumeIdentity(volume)) && objectTextContainsHotfixMarker(volume)) + .map((volume) => ({ + name: volume.name ?? null, + configMap: volume.configMap?.name ?? null, + secret: volume.secret?.secretName ?? null, + identity: volumeIdentity(volume) + })); + const unmanagedVolumeMounts = deploymentContainerMounts(liveDeployment) + .filter(({ containerName, mount }) => + !desiredMountIdentities.has(volumeMountIdentity(containerName, mount)) && + objectTextContainsHotfixMarker({ containerName, mount }) + ) + .map(({ containerName, mount }) => ({ + containerName, + name: mount.name ?? null, + mountPath: mount.mountPath ?? null, + subPath: mount.subPath ?? null, + identity: volumeMountIdentity(containerName, mount) + })); + const unmanagedTemplateAnnotations = Object.entries(deploymentTemplateAnnotations(liveDeployment)) + .filter(([key, value]) => + !desiredAnnotationIdentities.has(templateAnnotationIdentity(key, value)) && + objectTextContainsHotfixMarker({ key, value }) + ) + .map(([key, value]) => ({ key, value: String(value ?? "") })); + + if (unmanagedVolumes.length === 0 && unmanagedVolumeMounts.length === 0 && unmanagedTemplateAnnotations.length === 0) { + return base; + } + + return { + ...base, + cleanup: true, + result: "planned", + reason: + "Live Deployment contains unmanaged hotfix/override fields that kubectl apply may preserve; delete before apply so desired source recreates it exactly.", + unmanagedVolumes, + unmanagedVolumeMounts, + unmanagedTemplateAnnotations + }; +} + function replicaPlan(item) { if (item?.kind === "Job") { return item?.spec?.suspend === true ? 0 : 1; @@ -1887,6 +2057,27 @@ async function readLiveLegacySimulatorDeployment(kubectl, deploymentName, blocke } } +async function readLiveDeployment(kubectl, deploymentName, blockers, scopePrefix) { + if (kubectl.status !== "ready") { + return { status: "not_evaluated", reason: kubectl.reason }; + } + const get = await kubectlResult(kubectl, ["-n", namespace, "get", "deployment", deploymentName, "-o", "json"], 15000); + if (!get.ok) { + const output = commandOutput(get); + if (isNotFoundOutput(output)) { + return { status: "not_found", liveDeployment: null }; + } + addBlocker(blockers, "environment_blocker", `${scopePrefix}-${deploymentName}`, `Cannot read live DEV Deployment ${deploymentName}: ${output}`); + return { status: "blocked", reason: oneLine(output), liveDeployment: null }; + } + try { + return { status: "found", liveDeployment: JSON.parse(get.stdout) }; + } catch (error) { + addBlocker(blockers, "contract_blocker", `${scopePrefix}-${deploymentName}`, `Cannot parse live DEV Deployment ${deploymentName}: ${error.message}`); + return { status: "blocked", reason: oneLine(error.message), liveDeployment: null }; + } +} + async function buildDevTemplateJobReplacementPlan(kubectl, workloads, blockers) { const desiredJobs = listItems(workloads).filter(isAllowedDesiredTemplateJob); const replacements = []; @@ -2013,6 +2204,56 @@ async function buildDevLegacySimulatorDeploymentCleanupPlan(kubectl, workloads, return cleanups; } +async function buildDevUnmanagedHotfixDeploymentCleanupPlan(kubectl, workloads, blockers) { + const desiredDeployments = desiredDeploymentIndex(workloads); + const cleanups = []; + for (const [key, desiredDeployment] of desiredDeployments.entries()) { + const deploymentName = desiredDeployment.metadata.name; + const live = await readLiveDeployment(kubectl, deploymentName, blockers, "unmanaged-hotfix-deployment-read"); + if (live.status === "not_evaluated") { + cleanups.push({ + namespace, + deploymentName, + desiredExists: true, + liveExists: null, + cleanup: false, + result: "not_evaluated", + reason: live.reason, + unmanagedVolumes: [], + unmanagedVolumeMounts: [], + unmanagedTemplateAnnotations: [] + }); + continue; + } + if (live.status === "blocked") { + cleanups.push({ + namespace, + deploymentName, + desiredExists: true, + liveExists: null, + cleanup: false, + result: "blocked", + reason: live.reason, + unmanagedVolumes: [], + unmanagedVolumeMounts: [], + unmanagedTemplateAnnotations: [] + }); + continue; + } + const decision = decideDevUnmanagedHotfixDeploymentCleanup({ + deploymentName, + deploymentNamespace: key.split("/")[0], + desiredDeployment, + liveDeployment: live.liveDeployment + }); + if (decision.result === "blocked") { + addBlocker(blockers, "safety_blocker", `unmanaged-hotfix-deployment-cleanup-${deploymentName}`, decision.reason); + } + cleanups.push(decision); + } + return cleanups; +} + async function executeDevTemplateJobReplacements(kubectl, replacements, blockers, concurrency = defaultCdConcurrency) { const planned = replacements.filter((replacement) => replacement.replace && replacement.result === "planned"); await mapWithConcurrency(planned, concurrency, async (replacement) => { @@ -2067,6 +2308,33 @@ async function executeDevLegacySimulatorDeploymentCleanups(kubectl, cleanups, bl return planned.length; } +async function executeDevUnmanagedHotfixDeploymentCleanups(kubectl, cleanups, blockers, concurrency = defaultCdConcurrency) { + const planned = cleanups.filter((cleanup) => cleanup.cleanup && cleanup.result === "planned"); + await mapWithConcurrency(planned, concurrency, async (cleanup) => { + const commandArgs = [ + "-n", + cleanup.namespace, + "delete", + "deployment", + cleanup.deploymentName, + "--ignore-not-found=true" + ]; + const result = await kubectlResult(kubectl, commandArgs, 20000); + cleanup.deleteCommand = kubectlCommand(kubectl, commandArgs); + cleanup.deleteStdout = result.redactedStdout ?? result.stdout; + cleanup.deleteStderr = result.redactedStderr ?? result.stderr; + if (!result.ok) { + cleanup.result = "delete_failed"; + cleanup.reason = `Failed to delete live Deployment with unmanaged hotfix fields before apply: ${oneLine(commandOutput(result))}`; + addBlocker(blockers, "environment_blocker", `unmanaged-hotfix-deployment-cleanup-${cleanup.deploymentName}`, cleanup.reason); + return; + } + cleanup.result = "deleted_pending_apply"; + cleanup.reason = "Live Deployment with unmanaged hotfix fields was deleted; kubectl apply must recreate it from desired source."; + }); + return planned.length; +} + function rolloutApiName(kind) { if (kind === "Deployment") return "deployment"; if (kind === "StatefulSet") return "statefulset"; @@ -2181,7 +2449,14 @@ function isExpectedTemplateJobImmutableFailure(result, replacementsNeeded) { ); } -async function runApplyStep(args, kubectl, blockers, templateJobReplacements, legacySimulatorDeploymentCleanups) { +async function runApplyStep( + args, + kubectl, + blockers, + templateJobReplacements, + legacySimulatorDeploymentCleanups, + unmanagedHotfixDeploymentCleanups +) { if (blockers.length > 0) { return { status: "not_run", summary: "Skipped because preflight blockers are open" }; } @@ -2191,7 +2466,13 @@ async function runApplyStep(args, kubectl, blockers, templateJobReplacements, le if (args.apply) { const replaceCount = await executeDevTemplateJobReplacements(kubectl, templateJobReplacements, blockers, args.rolloutConcurrency); const cleanupCount = await executeDevLegacySimulatorDeploymentCleanups(kubectl, legacySimulatorDeploymentCleanups, blockers, args.rolloutConcurrency); - mutationAttempted = replaceCount > 0 || cleanupCount > 0; + const hotfixCleanupCount = await executeDevUnmanagedHotfixDeploymentCleanups( + kubectl, + unmanagedHotfixDeploymentCleanups, + blockers, + args.rolloutConcurrency + ); + mutationAttempted = replaceCount > 0 || cleanupCount > 0 || hotfixCleanupCount > 0; if (blockers.length > 0) { return { status: "blocked", @@ -2227,6 +2508,14 @@ async function runApplyStep(args, kubectl, blockers, templateJobReplacements, le : "Live stale legacy simulator Deployment was deleted, but kubectl apply failed before confirming desired state."; } } + for (const cleanup of unmanagedHotfixDeploymentCleanups) { + if (cleanup.result === "deleted_pending_apply") { + cleanup.result = result.ok ? "deleted" : "delete_succeeded_apply_failed"; + cleanup.reason = result.ok + ? "Live Deployment with unmanaged hotfix fields was deleted and recreated by kubectl apply." + : "Live Deployment with unmanaged hotfix fields was deleted, but kubectl apply failed before confirming recreation."; + } + } return { status: result.ok || expectedImmutableDryRun ? "pass" : "blocked", command: kubectlCommand(kubectl, commandArgs), @@ -2314,12 +2603,14 @@ export async function runDevDeployApply(argv, io = {}) { const liveProbe = args.skipLiveProbe ? { status: "not_run", reason: "--skip-live-probe" } : await probeDevEndpoint(blockers); const templateJobReplacements = await buildDevTemplateJobReplacementPlan(kubectl, workloads, blockers); const legacySimulatorDeploymentCleanups = await buildDevLegacySimulatorDeploymentCleanupPlan(kubectl, workloads, blockers); + const unmanagedHotfixDeploymentCleanups = await buildDevUnmanagedHotfixDeploymentCleanupPlan(kubectl, workloads, blockers); const applyStep = await runApplyStep( args, kubectl, blockers, templateJobReplacements, - legacySimulatorDeploymentCleanups + legacySimulatorDeploymentCleanups, + unmanagedHotfixDeploymentCleanups ); let rolloutStatusAfterApply; let cloudWebRolloutAfterApply; @@ -2418,7 +2709,8 @@ export async function runDevDeployApply(argv, io = {}) { `code agent provider live env before apply: ${codeAgentProviderLiveBeforeApply.status}`, `rollout status concurrency: ${args.rolloutConcurrency}`, `template job replacements: ${templateJobReplacements.filter((replacement) => replacement.replace).length}`, - `legacy simulator deployment cleanups: ${legacySimulatorDeploymentCleanups.filter((cleanup) => cleanup.cleanup).length}` + `legacy simulator deployment cleanups: ${legacySimulatorDeploymentCleanups.filter((cleanup) => cleanup.cleanup).length}`, + `unmanaged hotfix deployment cleanups: ${unmanagedHotfixDeploymentCleanups.filter((cleanup) => cleanup.cleanup).length}` ], summary: status === "blocked" ? "DEV apply is blocked before mutation." : "DEV dry-run preflight passed." }, @@ -2451,6 +2743,8 @@ export async function runDevDeployApply(argv, io = {}) { templateJobReplacements, legacySimulatorDeploymentCleanupPolicy: devLegacySimulatorDeploymentCleanupPolicy, legacySimulatorDeploymentCleanups, + unmanagedHotfixDeploymentCleanupPolicy: devUnmanagedHotfixDeploymentCleanupPolicy, + unmanagedHotfixDeploymentCleanups, cloudWebRollout, cloudWebRolloutBeforeApply, cloudWebRolloutAfterApply, diff --git a/scripts/src/dev-deploy-apply.test.mjs b/scripts/src/dev-deploy-apply.test.mjs index 6acc9c82..af6deddd 100644 --- a/scripts/src/dev-deploy-apply.test.mjs +++ b/scripts/src/dev-deploy-apply.test.mjs @@ -6,6 +6,7 @@ import { compareRuntimeIdentityEnv, decideDevLegacySimulatorDeploymentCleanup, decideDevTemplateJobReplacement, + decideDevUnmanagedHotfixDeploymentCleanup, formatKubeconfigAccessFailure, inspectCodeAgentProviderDesiredState, inspectCodeAgentProviderLiveDeployment, @@ -321,6 +322,64 @@ test("legacy simulator cleanup refuses non-DEV or missing replacement cleanup", assert.equal(ignored.result, "ignored"); }); +test("unmanaged hotfix Deployment cleanup plans recreation when live has extra hotfix mounts", () => { + const desired = codeAgentWorkloads().items[0]; + const live = structuredClone(desired); + live.spec.template.metadata = { + annotations: { + "hwlab.pikastech.local/runtime-hotfix": "2026-05-24" + } + }; + live.spec.template.spec.volumes.unshift({ + name: "hwlab-code-agent-hotfix-20260524", + configMap: { name: "hwlab-code-agent-hotfix-20260524" } + }); + live.spec.template.spec.containers[0].volumeMounts.unshift({ + name: "hwlab-code-agent-hotfix-20260524", + mountPath: "/app/internal/cloud/server.mjs", + subPath: "server.mjs", + readOnly: true + }); + + const decision = decideDevUnmanagedHotfixDeploymentCleanup({ + deploymentName: "hwlab-cloud-api", + deploymentNamespace: "hwlab-dev", + desiredDeployment: desired, + liveDeployment: live + }); + + assert.equal(decision.cleanup, true); + assert.equal(decision.result, "planned"); + assert.equal(decision.unmanagedVolumes[0].configMap, "hwlab-code-agent-hotfix-20260524"); + assert.equal(decision.unmanagedVolumeMounts[0].mountPath, "/app/internal/cloud/server.mjs"); + assert.equal(decision.unmanagedTemplateAnnotations[0].key, "hwlab.pikastech.local/runtime-hotfix"); +}); + +test("unmanaged hotfix Deployment cleanup ignores fields already present in desired source", () => { + const desired = codeAgentWorkloads().items[0]; + desired.spec.template.spec.volumes.unshift({ + name: "hwlab-code-agent-hotfix-20260524", + configMap: { name: "hwlab-code-agent-hotfix-20260524" } + }); + desired.spec.template.spec.containers[0].volumeMounts.unshift({ + name: "hwlab-code-agent-hotfix-20260524", + mountPath: "/app/internal/cloud/server.mjs", + subPath: "server.mjs", + readOnly: true + }); + const live = structuredClone(desired); + + const decision = decideDevUnmanagedHotfixDeploymentCleanup({ + deploymentName: "hwlab-cloud-api", + deploymentNamespace: "hwlab-dev", + desiredDeployment: desired, + liveDeployment: live + }); + + assert.equal(decision.cleanup, false); + assert.equal(decision.result, "not_needed"); +}); + test("apply source commit follows deploy/catalog artifact identity", () => { assert.equal( resolveApplySourceCommit({ commitId: "73b379f" }, { commitId: "73b379f" }, "cb35ada68606"), From b23a800318df2ba2820e49e898041fcf0b9c045e Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 25 May 2026 01:56:52 +0800 Subject: [PATCH 6/9] chore: promote dev artifacts for 3ebbbbf --- deploy/artifact-catalog.dev.json | 160 +++++++++++++++---------------- deploy/deploy.json | 42 ++++---- deploy/k8s/base/workloads.yaml | 40 ++++---- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index e36fba8f..530c4cc6 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,12 +5,12 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "aef1a00", + "commitId": "3ebbbbf", "artifactState": "published", "publish": { "ciPublished": true, "registryVerified": true, - "provenance": "ci-artifact:ci-publish-20260524T174119-e7ea3b", + "provenance": "ci-artifact:ci-publish-20260524T175559-decd8a", "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." }, "healthContract": { @@ -66,10 +66,10 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:d92691e66e6f0e201a9b0a84459caef467681928fd12e4e2ddab72f419dd2904", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:49bd2880a025c0356c8ee88fc62b91d6ce6b62907a20f6c08f97b0384a231bdd", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -79,15 +79,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-cloud-web", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:5e5f70400cb560f28bfedbb9e05f88cec000017e5156a80f15683b0b56a3f0f5", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:fe3221b6e6e7811d1756f3489dcf9ab754b9daf2232297bec8ca19b8de4b3325", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -97,15 +97,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-agent-mgr", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:013477352f1d28b292302cb07ce26e9727a5db3952ddc811cfe5fe503e737b70", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:a052cf0b89509aaba519460a28506f82121f2aff478ea1298ed44ce4e402bddf", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -115,15 +115,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-agent-worker", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:dcd207350c1b7e282ec4dba59ab59a647b8a75c936ce50b88a3ed005a92547cd", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:087a3a579b74ce9bfed77238743391e935237d4c01d4f2db0aa4204a3ace7502", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -133,15 +133,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-gateway", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:e3f85b61395312b8d25588354ccac47448147ec69681e448023298dc5d5b5121", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:69bbe0e1e1e580b8c2a913af2147e126a93b2d2d29e0ebda18661fa8e542f532", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -151,15 +151,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-gateway-simu", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:62eb72a7d8b93d131f9adf438e78d8fd399f1e026d87bb516f784220061bfb62", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:403f41a3a6687a2633f30591fac87c196d22f4059bef4fd447fa75753127817f", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -169,15 +169,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-box-simu", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:3c7c9e124a4ffb27b2b9ac3d91ecda7b77723fe407f3972559707ccab3f33d59", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:8a29708233ee87fcd5309016e350dc7d134bb66640150627862f85ad3dc2ccfb", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -187,15 +187,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-patch-panel", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:9e3659bdbf6ec22203498e5ceb9147a2020f6ec96154922843b601ddb68b6a53", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:e6b36f3562d24cadc3dff6c628f2e59808c0990024ffa82012bcad1f863c95c3", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -205,15 +205,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-router", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:7c54855e6025f7d600dcf319751914a0333c415e6dd787551b28ff6ec66cc516", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:66ebbdb42b9bacbfc536b9bc1cec555da3b48cb78a52e4991fbfc00b69b50c48", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -223,15 +223,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-tunnel-client", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:97c3392509c215cff0fbccbb991037b4a9a4adc0b41ba2d31efdd3d21109adcc", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:45432d75861406846ee160fb94605a8374e3bcac60b7f5dfc26f29322700f42c", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -241,15 +241,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-edge-proxy", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:c0e70d434aa21bdeaba9de4204717569a1067c5b6cbb5a6beb0327a2bd513c8d", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:329ed48321003937fe35ef0f0162baf275bce484f9528e7c712e8e6ce56b3a72", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -259,15 +259,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-cli", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:641d0114b3979835be93f81c9c0269880e747dde72b14e9b212b44b998ce9ccd", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:50316c4528f3da8c17fdae7b897f0dae0d3e7336eba8058f7c948a5bf8421ebc", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -277,15 +277,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" }, { "serviceId": "hwlab-agent-skills", - "commitId": "aef1a00", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", - "imageTag": "aef1a00", - "digest": "sha256:ce4ff079941ffcf9fb0627a9e95d35947ba38b0b383536dcdb71a6c10e7d0d95", + "commitId": "3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", + "imageTag": "3ebbbbf", + "digest": "sha256:a1112c24e76775097e2a5db10aed080c0aa780c1b386d7cc0dd1d1908fb71edc", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -295,8 +295,8 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:41:21.664Z", - "buildSource": "pikasTech/HWLAB@aef1a00cb1f62150583daf4b9acbbfb6fdcd54c4" + "buildCreatedAt": "2026-05-24T17:56:01.662Z", + "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" } ], "serviceInventory": { diff --git a/deploy/deploy.json b/deploy/deploy.json index d6e9b2bd..20c45b15 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "aef1a00", + "commitId": "3ebbbbf", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,9 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_COMMIT_ID": "aef1a00", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", - "HWLAB_IMAGE_TAG": "aef1a00", + "HWLAB_COMMIT_ID": "3ebbbbf", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", + "HWLAB_IMAGE_TAG": "3ebbbbf", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -222,7 +222,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -231,14 +231,14 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", - "HWLAB_COMMIT_ID": "aef1a00", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", - "HWLAB_IMAGE_TAG": "aef1a00" + "HWLAB_COMMIT_ID": "3ebbbbf", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", + "HWLAB_IMAGE_TAG": "3ebbbbf" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -250,7 +250,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -262,7 +262,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -274,7 +274,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -294,7 +294,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -313,7 +313,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -334,7 +334,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -346,7 +346,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -360,7 +360,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -373,7 +373,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -384,13 +384,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "aef1a00" + "HWLAB_SKILLS_COMMIT_ID": "3ebbbbf" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index 67e70095..b2f719fc 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", "ports": [ { "name": "http", @@ -49,15 +49,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "aef1a00" + "value": "3ebbbbf" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:aef1a00" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf" }, { "name": "HWLAB_IMAGE_TAG", - "value": "aef1a00" + "value": "3ebbbbf" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -279,7 +279,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", "ports": [ { "name": "http", @@ -297,15 +297,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "aef1a00" + "value": "3ebbbbf" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:aef1a00" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf" }, { "name": "HWLAB_IMAGE_TAG", - "value": "aef1a00" + "value": "3ebbbbf" } ], "readinessProbe": { @@ -355,7 +355,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", "ports": [ { "name": "http", @@ -411,7 +411,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -454,7 +454,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", "ports": [ { "name": "http", @@ -515,7 +515,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", "ports": [ { "name": "http", @@ -588,7 +588,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", "ports": [ { "name": "http", @@ -656,7 +656,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", "ports": [ { "name": "http", @@ -720,7 +720,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", "ports": [ { "name": "http", @@ -780,7 +780,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", "ports": [ { "name": "http", @@ -852,7 +852,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", "ports": [ { "name": "http", @@ -917,7 +917,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -959,7 +959,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:aef1a00", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", "ports": [ { "name": "http", @@ -969,7 +969,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "aef1a00" + "value": "3ebbbbf" } ], "readinessProbe": { From 89b57733493e914c061f6ec081bb85f07a75b066 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 17:57:32 +0000 Subject: [PATCH 7/9] docs: record cd stale origin guard --- docs/reference/deployment-publish.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index 9449ac3a..fff9bc93 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -117,6 +117,11 @@ kubeconfig、registry、Docker socket、Secret/ConfigMap mount 和 CLI 行为。 2. 通过受控维护入口进入该容器,在容器内建立干净 HWLAB worktree,并快进到 `origin/main`;不得直接使用 `/home/ubuntu/hwlab` 等历史任务集合目录作为 发布真相。 + 如果 worktree 是从另一个本地目录 clone 出来的,必须先确认 `origin` 指向 + `git@github.com:pikasTech/HWLAB.git` 或等价 GitHub source of truth,再 + `git fetch origin main`。DEV CD 的 target-ref guard 会比较 HEAD 与 + `origin/main`;本地 clone 的 stale `origin/main` 会触发 + `target-ref-head` blocker,不能通过跳过 guard 处理。 3. 在同一个容器内执行 `node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit ...`, 用 `status`、`logs`、`report` 查询后台 job,而不是前台直接跑长脚本。 4. 至少完成一次 CI artifact publish 和一次 DEV CD apply,再用公开 From fa216f1cd86e65b6c500b2047672b8335f6bf16a Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 18:03:09 +0000 Subject: [PATCH 8/9] fix: harden cloud api port env handling --- cmd/hwlab-cloud-api/main.mjs | 8 +++++++- deploy/deploy.json | 1 + deploy/k8s/base/workloads.yaml | 4 ++++ docs/reference/deployment-publish.md | 5 +++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cmd/hwlab-cloud-api/main.mjs b/cmd/hwlab-cloud-api/main.mjs index 6b19241e..38b43d3a 100644 --- a/cmd/hwlab-cloud-api/main.mjs +++ b/cmd/hwlab-cloud-api/main.mjs @@ -2,7 +2,7 @@ import { createCloudApiServer } from "../../internal/cloud/server.mjs"; const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0"; -const port = Number.parseInt(process.env.HWLAB_CLOUD_API_PORT || process.env.PORT || "6667", 10); +const port = parsePort(process.env.HWLAB_CLOUD_API_PORT, parsePort(process.env.PORT, 6667)); const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA; const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 600000, { min: 1000, @@ -32,6 +32,12 @@ function parseTimeout(value, fallback, { min, max }) { return Math.min(Math.max(parsed, min), max); } +function parsePort(value, fallback) { + const parsed = Number.parseInt(value || "", 10); + if (!Number.isInteger(parsed) || parsed < 0 || parsed >= 65536) return fallback; + return parsed; +} + server.listen(port, host, () => { const address = server.address(); const resolvedPort = typeof address === "object" && address ? address.port : port; diff --git a/deploy/deploy.json b/deploy/deploy.json index 20c45b15..bdac5595 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -189,6 +189,7 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", + "HWLAB_CLOUD_API_PORT": "6667", "HWLAB_COMMIT_ID": "3ebbbbf", "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", "HWLAB_IMAGE_TAG": "3ebbbbf", diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index b2f719fc..d29e9341 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -47,6 +47,10 @@ "name": "HWLAB_PUBLIC_ENDPOINT", "value": "http://74.48.78.17:16667" }, + { + "name": "HWLAB_CLOUD_API_PORT", + "value": "6667" + }, { "name": "HWLAB_COMMIT_ID", "value": "3ebbbbf" diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index fff9bc93..157d625e 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -183,6 +183,11 @@ node tools/hwlab-cli/bin/hwlab-cli.mjs cicd report Deployment,再由同一次 `kubectl apply -k deploy/k8s/dev` 从源码 desired-state 重新创建;报告中记录 cleanup 计划和实际动作。不要把这种 cleanup 做成某个服务 或某个 skill 的特例。 +- Kubernetes service links 会为同 namespace Service 自动注入形如 + `HWLAB_CLOUD_API_PORT=tcp://...` 的环境变量。应用入口不得直接把这类 env 当作 + 数字端口解析;desired-state 应显式声明服务自身监听端口 + `HWLAB_CLOUD_API_PORT=6667`,入口代码也必须只接受合法数值端口,否则回落到 + 内置默认值。 ## DEV CD Transaction From b2a26d4c81d905a345a0d8a87a4f84c1c3865e24 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 25 May 2026 02:06:29 +0800 Subject: [PATCH 9/9] chore: promote dev artifacts for fa216f1 --- deploy/artifact-catalog.dev.json | 160 +++++++++++++++---------------- deploy/deploy.json | 42 ++++---- deploy/k8s/base/workloads.yaml | 40 ++++---- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 530c4cc6..7d4db3da 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,12 +5,12 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "3ebbbbf", + "commitId": "fa216f1", "artifactState": "published", "publish": { "ciPublished": true, "registryVerified": true, - "provenance": "ci-artifact:ci-publish-20260524T175559-decd8a", + "provenance": "ci-artifact:ci-publish-20260524T180538-ab5150", "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." }, "healthContract": { @@ -66,10 +66,10 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:49bd2880a025c0356c8ee88fc62b91d6ce6b62907a20f6c08f97b0384a231bdd", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:67f2e5c5f775f3904182bc8b375ec8db9f6a750837ce9f5395f1c793b8c6db80", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -79,15 +79,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-cloud-web", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:fe3221b6e6e7811d1756f3489dcf9ab754b9daf2232297bec8ca19b8de4b3325", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:61951fce0fc5efa70bfe20b91a7d0caa00f8f4f4d0f28cf36d40bff0c1cfb643", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -97,15 +97,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-mgr", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:a052cf0b89509aaba519460a28506f82121f2aff478ea1298ed44ce4e402bddf", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:801210386c260b55787e75d4f35d285c33bfd4a3c688fda688ec2f11ab224621", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -115,15 +115,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-worker", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:087a3a579b74ce9bfed77238743391e935237d4c01d4f2db0aa4204a3ace7502", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:a6f0a1e19421728f957b0002755d4a65d53ec4fae40a7fb37f77e9993da5d807", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -133,15 +133,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-gateway", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:69bbe0e1e1e580b8c2a913af2147e126a93b2d2d29e0ebda18661fa8e542f532", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:f5a27d5a54f3190f228063258ec05654d70371fea8e36b31e4de29ed073f1ab5", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -151,15 +151,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-gateway-simu", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:403f41a3a6687a2633f30591fac87c196d22f4059bef4fd447fa75753127817f", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:38c05ab740b2cbc8d162897eb6864f2614f3530407aa7530a6b50ce780fc38e0", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -169,15 +169,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-box-simu", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:8a29708233ee87fcd5309016e350dc7d134bb66640150627862f85ad3dc2ccfb", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:b9d3beddf98e250b0b63881465fbaa93b5eefd609521f947779ebdcef02c0834", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -187,15 +187,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-patch-panel", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:e6b36f3562d24cadc3dff6c628f2e59808c0990024ffa82012bcad1f863c95c3", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:0b0b5899aa1d8b23839121d14a4b601786f1a17ec786063ca5554adb966cb26b", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -205,15 +205,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-router", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:66ebbdb42b9bacbfc536b9bc1cec555da3b48cb78a52e4991fbfc00b69b50c48", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:66d3a456f3af6cc93b52ae4ad84f7c55c0a011991677e642b2044d30c088cd63", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -223,15 +223,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-tunnel-client", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:45432d75861406846ee160fb94605a8374e3bcac60b7f5dfc26f29322700f42c", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:f24dd2056647de7d55c4855708246752831b5960fa07b17e34612fc4e77c3e11", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -241,15 +241,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-edge-proxy", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:329ed48321003937fe35ef0f0162baf275bce484f9528e7c712e8e6ce56b3a72", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:71d7c66a67693b72bfbe2055cd54bb6340c083faaa5a187a43cf934e1d0d1ded", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -259,15 +259,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-cli", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:50316c4528f3da8c17fdae7b897f0dae0d3e7336eba8058f7c948a5bf8421ebc", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:3e4fbf11719141a7013cc6230b7364e7e1acfc8282df14592d562dd438ddd9b4", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -277,15 +277,15 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-skills", - "commitId": "3ebbbbf", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", - "imageTag": "3ebbbbf", - "digest": "sha256:a1112c24e76775097e2a5db10aed080c0aa780c1b386d7cc0dd1d1908fb71edc", + "commitId": "fa216f1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:fa216f1", + "imageTag": "fa216f1", + "digest": "sha256:bf0f1a2a0236fb120f66bcc4f2b25fdd765b970578ebb8bf11f8254c812122c1", "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", @@ -295,8 +295,8 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-24T17:56:01.662Z", - "buildSource": "pikasTech/HWLAB@3ebbbbf6254f776b589f16a4c50a48d76c3804c2" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" } ], "serviceInventory": { diff --git a/deploy/deploy.json b/deploy/deploy.json index bdac5595..a5d477d4 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "3ebbbbf", + "commitId": "fa216f1", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -190,9 +190,9 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", "HWLAB_CLOUD_API_PORT": "6667", - "HWLAB_COMMIT_ID": "3ebbbbf", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", - "HWLAB_IMAGE_TAG": "3ebbbbf", + "HWLAB_COMMIT_ID": "fa216f1", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", + "HWLAB_IMAGE_TAG": "fa216f1", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -223,7 +223,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -232,14 +232,14 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", - "HWLAB_COMMIT_ID": "3ebbbbf", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", - "HWLAB_IMAGE_TAG": "3ebbbbf" + "HWLAB_COMMIT_ID": "fa216f1", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1", + "HWLAB_IMAGE_TAG": "fa216f1" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -251,7 +251,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -263,7 +263,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -275,7 +275,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -295,7 +295,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -314,7 +314,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -335,7 +335,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-router:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -347,7 +347,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -361,7 +361,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -374,7 +374,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -385,13 +385,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "3ebbbbf" + "HWLAB_SKILLS_COMMIT_ID": "fa216f1" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index d29e9341..7927cc97 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", "ports": [ { "name": "http", @@ -53,15 +53,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "3ebbbbf" + "value": "fa216f1" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:3ebbbbf" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1" }, { "name": "HWLAB_IMAGE_TAG", - "value": "3ebbbbf" + "value": "fa216f1" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -283,7 +283,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1", "ports": [ { "name": "http", @@ -301,15 +301,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "3ebbbbf" + "value": "fa216f1" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:3ebbbbf" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1" }, { "name": "HWLAB_IMAGE_TAG", - "value": "3ebbbbf" + "value": "fa216f1" } ], "readinessProbe": { @@ -359,7 +359,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:fa216f1", "ports": [ { "name": "http", @@ -415,7 +415,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:fa216f1", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -458,7 +458,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:fa216f1", "ports": [ { "name": "http", @@ -519,7 +519,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:fa216f1", "ports": [ { "name": "http", @@ -592,7 +592,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:fa216f1", "ports": [ { "name": "http", @@ -660,7 +660,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:fa216f1", "ports": [ { "name": "http", @@ -724,7 +724,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-router:fa216f1", "ports": [ { "name": "http", @@ -784,7 +784,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:fa216f1", "ports": [ { "name": "http", @@ -856,7 +856,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:fa216f1", "ports": [ { "name": "http", @@ -921,7 +921,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:fa216f1", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -963,7 +963,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:3ebbbbf", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:fa216f1", "ports": [ { "name": "http", @@ -973,7 +973,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "3ebbbbf" + "value": "fa216f1" } ], "readinessProbe": {