diff --git a/cmd/hwlab-cloud-api/main.mjs b/cmd/hwlab-cloud-api/main.mjs index c4e16be0..38b43d3a 100644 --- a/cmd/hwlab-cloud-api/main.mjs +++ b/cmd/hwlab-cloud-api/main.mjs @@ -2,11 +2,11 @@ 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, 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, @@ -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/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/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 1a06bd8f..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": "4d478a1", + "commitId": "fa216f1", "artifactState": "published", "publish": { "ciPublished": true, "registryVerified": true, - "provenance": "ci-artifact:ci-publish-20260524T143621-684be5", + "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": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:3e55a7f9b0c50bd3747e6ad6cd6b271afc3fd02c19ec0b1ac8efa2995a3e70c8", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-cloud-web", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:e5bd7b1f4b8153d6d26396bed9b29af173cedd9caf4ef00f77624f60f16ac7ce", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-mgr", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:eb4128008a69a259c9c270d06d74b6cd4b1bd29b141b4c43d99be96644c9b6f0", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-worker", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:c4bdab9dcc5ed396281d1611e603e698cb387586da95a026f6cf74f977badc2b", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-gateway", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:1c9d78026999e3ba7cfddb2a1948a51ec8e04f7dc73dd3fb97e00fd474b8332d", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-gateway-simu", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:bb6e366a54e78cec7165d77adbafe74c811f6dbcd71353875488c0a68b82428f", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-box-simu", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:81e45e2a58cf30889ed10ba79bac3676a4ef88d9b35a0bc4001c16e0ccfeb228", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-patch-panel", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:929db2773987804782b77cec01c922d8e530909839ae7426bfefc80dd7e55793", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-router", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:def49d50e855154a4c6f3e7187c620c4a407a81cca0ea7551c341521049979d4", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-tunnel-client", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:9f04b587d9f27ae484a20aa71f2def5ac56207174cebb0ddd741b0747c0c129d", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-edge-proxy", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:08d442ae21dd4f15b1676361dd8549d5e0378cb02f711003754e49bbe8b4a3f2", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-cli", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:11a2605ca145dd9ea4d9790924fd0c57764ea940661023759b9f0f0ce60c4ace", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" }, { "serviceId": "hwlab-agent-skills", - "commitId": "4d478a1", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:4d478a1", - "imageTag": "4d478a1", - "digest": "sha256:97bbb94cadd6b21011cb65719e642bc9a385d3cc5c1fe47cac75f5626b4e6734", + "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-24T14:36:22.870Z", - "buildSource": "pikasTech/HWLAB@4d478a1646ed5c5cd40f1f4f09886aa3f7ad5825" + "buildCreatedAt": "2026-05-24T18:05:40.725Z", + "buildSource": "pikasTech/HWLAB@fa216f1cd86e65b6c500b2047672b8335f6bf16a" } ], "serviceInventory": { diff --git a/deploy/deploy.json b/deploy/deploy.json index af685e4a..a5d477d4 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "4d478a1", + "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:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,10 @@ "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_CLOUD_API_PORT": "6667", + "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", @@ -204,7 +205,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", @@ -222,7 +223,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -230,15 +231,15 @@ "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_COMMIT_ID": "4d478a1", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", - "HWLAB_IMAGE_TAG": "4d478a1" + "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", + "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:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -250,7 +251,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -262,7 +263,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -274,7 +275,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -294,7 +295,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -313,7 +314,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -334,7 +335,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -346,7 +347,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -360,7 +361,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -368,12 +369,12 @@ "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" } }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -384,13 +385,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:fa216f1", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "4d478a1" + "HWLAB_SKILLS_COMMIT_ID": "fa216f1" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index a44992a4..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:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1", "ports": [ { "name": "http", @@ -47,17 +47,21 @@ "name": "HWLAB_PUBLIC_ENDPOINT", "value": "http://74.48.78.17:16667" }, + { + "name": "HWLAB_CLOUD_API_PORT", + "value": "6667" + }, { "name": "HWLAB_COMMIT_ID", - "value": "4d478a1" + "value": "fa216f1" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:4d478a1" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:fa216f1" }, { "name": "HWLAB_IMAGE_TAG", - "value": "4d478a1" + "value": "fa216f1" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -115,7 +119,7 @@ }, { "name": "HWLAB_CODE_AGENT_TIMEOUT_MS", - "value": "150000" + "value": "600000" }, { "name": "HWLAB_CODE_AGENT_OPENAI_BASE_URL", @@ -279,7 +283,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:fa216f1", "ports": [ { "name": "http", @@ -293,19 +297,19 @@ }, { "name": "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" }, { "name": "HWLAB_COMMIT_ID", - "value": "4d478a1" + "value": "fa216f1" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:fa216f1" }, { "name": "HWLAB_IMAGE_TAG", - "value": "4d478a1" + "value": "fa216f1" } ], "readinessProbe": { @@ -355,7 +359,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:fa216f1", "ports": [ { "name": "http", @@ -411,7 +415,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:fa216f1", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -454,7 +458,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:fa216f1", "ports": [ { "name": "http", @@ -515,7 +519,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:fa216f1", "ports": [ { "name": "http", @@ -588,7 +592,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:fa216f1", "ports": [ { "name": "http", @@ -656,7 +660,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:fa216f1", "ports": [ { "name": "http", @@ -720,7 +724,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:fa216f1", "ports": [ { "name": "http", @@ -780,7 +784,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:fa216f1", "ports": [ { "name": "http", @@ -852,7 +856,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:fa216f1", "ports": [ { "name": "http", @@ -870,7 +874,7 @@ }, { "name": "HWLAB_EDGE_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" } ], "readinessProbe": { @@ -917,7 +921,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:4d478a1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:fa216f1", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -959,7 +963,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:fa216f1", "ports": [ { "name": "http", @@ -969,7 +973,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "4d478a1" + "value": "fa216f1" } ], "readinessProbe": { diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index d3dc8e12..66a6c392 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -86,6 +86,42 @@ 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 侧 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 或完整源码内容。 +- 使用 `-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/deployment-publish.md b/docs/reference/deployment-publish.md index 1769d7bc..157d625e 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,再用公开 @@ -169,6 +174,20 @@ 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 的特例。 +- Kubernetes service links 会为同 namespace Service 自动注入形如 + `HWLAB_CLOUD_API_PORT=tcp://...` 的环境变量。应用入口不得直接把这类 env 当作 + 数字端口解析;desired-state 应显式声明服务自身监听端口 + `HWLAB_CLOUD_API_PORT=6667`,入口代码也必须只接受合法数值端口,否则回落到 + 内置默认值。 ## DEV CD Transaction @@ -526,6 +545,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 diff --git a/docs/reference/dev-runtime-hotfix-runbook.md b/docs/reference/dev-runtime-hotfix-runbook.md index 5d3b70d6..c621e581 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/**`: @@ -77,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/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..db06fd5a 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; @@ -94,7 +95,12 @@ 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 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 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.", "Do not claim M3, M4, or M5 acceptance unless the response includes live HWLAB evidence." @@ -1220,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; @@ -1397,7 +1405,7 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD function close() { if (closed) return; - child.kill("SIGTERM"); + terminateChildProcess(child, { detached: childDetached }); } return { @@ -1411,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, @@ -1581,6 +1618,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 +1794,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 +1853,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 +1899,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 +1931,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/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-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..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) { @@ -15,7 +16,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 +41,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..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"), { @@ -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/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 1de2e54d..923d4835 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -91,7 +91,7 @@ test("source/default workbench report cannot claim DEV-LIVE and documents the co assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass"); const traceDisclosure = report.checks.find((check) => check.id === "code-agent-trace-replay-disclosure"); assert.equal(traceDisclosure?.status, "pass"); - assert.deepEqual(traceDisclosure?.evidence, ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"]); + assert.deepEqual(traceDisclosure?.evidence, ["显示全部 N / 原始 M", "assistant stream xN", "compressed=N assistant chunks", "复制 JSON", "下载 trace"]); }); test("Code Agent browser failure classifier emits Chinese timeout/provider/browser categories with traceId", () => { 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..57f986dc 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([ { @@ -296,7 +296,7 @@ const workbenchMarkers = Object.freeze([ "center-workspace", "conversation-list", "agent-chat-status", - "task-list", + "code-agent-timeout", "right-sidebar", "hardware-list", "command-form" @@ -622,14 +622,14 @@ function runStaticSmoke() { evidence: codeAgentQuickPromptFixtures.flatMap((prompt) => [prompt.id, prompt.label, prompt.prompt]) }); - addCheck(checks, blockers, "code-agent-long-timeout-contract", hasCodeAgentLongTimeoutContract(files), "Code Agent chat uses a dedicated long timeout and does not reuse the old 4500ms API timeout.", { + addCheck(checks, blockers, "code-agent-long-timeout-contract", hasCodeAgentLongTimeoutContract(files, artifactPublisher), "Code Agent chat uses a dedicated long timeout and does not reuse the old 4500ms API timeout.", { blocker: "runtime_blocker", evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"] }); - addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose tail preview counts and provide full UI/JSON/download access for long traces.", { + addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose full trace rows, compressed noisy assistant streams, and JSON/download access for long traces.", { blocker: "observability_blocker", - evidence: ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"] + evidence: ["显示全部 N / 原始 M", "assistant stream xN", "compressed=N assistant chunks", "复制 JSON", "下载 trace"] }); addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", { @@ -2627,20 +2627,23 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) { ); } -function hasCodeAgentLongTimeoutContract(files) { +function hasCodeAgentLongTimeoutContract(files, artifactPublisher = "") { 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) && + /fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(functionBody(app, "sendAgentMessage")) && + /shortConnection:\s*true/u.test(functionBody(app, "sendAgentMessage")) && + /idleMs\s*>=\s*CODE_AGENT_TIMEOUT_MS/u.test(functionBody(app, "waitForAgentMessageResult")) && /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) && /codeAgentTimeoutMs/u.test(app) && - /Code Agent 代理等待 cloud-api 超过/u.test(files.artifactPublisher ?? "") && - /输入已保留,可稍后重试/u.test(files.artifactPublisher ?? "") && - /blocker:\s*\{/u.test(files.artifactPublisher ?? "") && - /retryable:\s*true/u.test(files.artifactPublisher ?? "") + /Code Agent 代理等待 cloud-api 超过/u.test(artifactPublisher) && + /输入已保留,可稍后重试/u.test(artifactPublisher) && + /blocker:\s*\{/u.test(artifactPublisher) && + /retryable:\s*true/u.test(artifactPublisher) ); } @@ -2648,13 +2651,20 @@ function hasCodeAgentTraceReplayDisclosure({ app, styles }) { const tracePanelBody = functionBody(app, "messageTracePanel"); const traceToolbarBody = functionBody(app, "messageTraceToolbar"); return ( - /CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/u.test(app) && /function\s+messageTraceCountText\s*\(/u.test(app) && - /显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+总计\s+\$\{total\}/u.test(app) && + /显示全部\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/u.test(app) && /function\s+renderTraceEventList\s*\(/u.test(app) && - /tracePreviewEvents\(events\)/u.test(tracePanelBody) && - /messageTraceToolbar\(message,\s*trace,\s*events,\s*list\)/u.test(tracePanelBody) && - /展开全部/u.test(traceToolbarBody) && + /function\s+messageTraceUiKey\s*\(/u.test(app) && + /function\s+defaultTraceDetailsOpen\s*\(/u.test(app) && + /state\.traceDetailsOpen\.get\(traceUiKey\)/u.test(tracePanelBody) && + /defaultTraceDetailsOpen\(message\)/u.test(tracePanelBody) && + /state\.traceDetailsOpen\.set\(traceUiKey,\s*details\.open\)/u.test(tracePanelBody) && + /messageTraceToolbar\(message,\s*trace,\s*events,\s*rows\)/u.test(tracePanelBody) && + /list\.dataset\.traceMode\s*=\s*"all"/u.test(tracePanelBody) && + /renderTraceEventList\(list,\s*rows\)/u.test(tracePanelBody) && + /function\s+traceNoiseSummaryRow\s*\(/u.test(app) && + /assistant stream x\$\{visibleEvents\.length\}/u.test(app) && + /compressed=\$\{assistantChunks\.length\} assistant chunks/u.test(app) && /复制 JSON/u.test(traceToolbarBody) && /下载 trace/u.test(traceToolbarBody) && /function\s+messageTraceJson\s*\(/u.test(app) && @@ -2662,7 +2672,8 @@ function hasCodeAgentTraceReplayDisclosure({ app, styles }) { /\.message-trace-toolbar\s*\{/u.test(styles) && /\.message-trace-count\s*\{/u.test(styles) && /\.message-trace-action\s*\{/u.test(styles) && - /\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/u.test(styles) + !/CODE_AGENT_TRACE_PREVIEW_LIMIT|tracePreviewRows|收起为最近|展开全部|data-trace-mode="tail"/u.test(app) && + !/\.message-trace-events-full/u.test(styles) ); } @@ -2719,8 +2730,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) && @@ -2805,10 +2816,22 @@ function m3RenderedWorkbenchNotM5Fixture(app) { } function functionBody(source, functionName) { - const match = source.match(new RegExp(`(?:async\\s+)?function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u")); + const match = source.match(new RegExp(`(?:async\\s+)?function\\s+${escapeRegExp(functionName)}\\s*\\(`, "u")); if (!match) return ""; - let depth = 0; + let parenDepth = 0; + let bodyStart = -1; for (let index = match.index + match[0].length - 1; index < source.length; index += 1) { + const char = source[index]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (parenDepth === 0) { + bodyStart = source.indexOf("{", index + 1); + break; + } + } + if (bodyStart < 0) return ""; + let depth = 0; + for (let index = bodyStart; index < source.length; index += 1) { const char = source[index]; if (char === "{") depth += 1; if (char === "}") { 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"), diff --git a/tools/hwlab-gateway-shell.mjs b/tools/hwlab-gateway-shell.mjs index 197001a2..1d390335 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,17 @@ 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", + " --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", @@ -96,6 +109,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 c58df734..d5000d79 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -18,12 +18,16 @@ 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 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 }); -const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 }); -const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; +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_CANCEL_TIMEOUT_MS = resolveTimeoutMs("codeAgentCancelTimeoutMs", DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS, { min: 5000, max: 120000 }); const rpcReadMethods = Object.freeze([ "system.health", "cloud.adapter.describe", @@ -113,8 +117,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 +127,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 +134,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 +161,8 @@ const el = { helpStatus: byId("help-status") }; +let activeWorkbenchDialog = null; + await ensureWorkbenchAuth({ loginShell: el.loginShell, appShell: el.shell @@ -171,6 +175,7 @@ const state = { codeAgentAvailability: null, chatMessages: [], traceStreams: new Map(), + traceDetailsOpen: new Map(), canceledTraces: new Set(), currentRequest: null, sessionStatus: null, @@ -207,6 +212,7 @@ initRightSidebarResize(); initSideTabs(); initHardwareTabs(); initQuickActions(); +initCodeAgentTimeoutControl(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); @@ -241,33 +247,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() { @@ -664,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; @@ -711,7 +810,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 +1405,6 @@ function renderStaticWorkbench() { el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。"; renderConversation(); - renderTaskList(); renderHardwareStatus(null); renderDrafts(); renderM3ControlStatus(); @@ -1335,15 +1433,15 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI "Prefer": "respond-async", "X-HWLAB-Short-Connection": "1" }, - timeoutMs: CODE_AGENT_TIMEOUT_MS, + timeoutMs: CODE_AGENT_SUBMIT_TIMEOUT_MS, timeoutName: "Code Agent", - activityRef: () => state.currentRequest, body: JSON.stringify({ message, conversationId, sessionId, threadId, traceId, + shortConnection: true, projectId: gateSummary.topology.projectId }) }); @@ -1889,27 +1987,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(); @@ -3618,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; @@ -3669,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, @@ -3745,11 +3832,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) { @@ -3762,8 +3844,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) { @@ -3782,7 +3871,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"; @@ -3824,15 +3913,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) { @@ -3853,7 +3933,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; } @@ -3913,22 +4024,29 @@ 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); const list = document.createElement("ol"); list.className = "message-trace-events"; const events = Array.isArray(trace?.events) ? trace.events : []; - const rows = traceDisplayRows(trace, tracePreviewEvents(events)); + const rows = traceDisplayRows(trace, events); if (events.length === 0) { const item = document.createElement("li"); item.textContent = `trace=${message.traceId ?? "pending"};等待后端事件。`; list.append(item); } else { - const toolbar = messageTraceToolbar(message, trace, events, 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; } @@ -3936,40 +4054,29 @@ function messageTracePanel(message) { return details; } -function messageTraceToolbar(message, trace, events, 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 rows = traceDisplayRows(trace, tracePreviewEvents(events)); - 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 tracePreviewEvents(events) { - return Array.isArray(events) ? events : []; -} - -function tracePreviewRows(rows) { - return rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? rows.slice(-CODE_AGENT_TRACE_PREVIEW_LIMIT) : rows; -} - -function messageTraceCountText(total, displayTotal, expanded) { - if (displayTotal <= CODE_AGENT_TRACE_PREVIEW_LIMIT || expanded) return `显示全部 ${displayTotal} / 总计 ${total}`; - return `显示最近 ${CODE_AGENT_TRACE_PREVIEW_LIMIT} / 总计 ${total}`; +function messageTraceCountText(rawTotal, displayTotal) { + return `显示全部 ${displayTotal} / 原始 ${rawTotal}`; } function renderTraceEventList(list, rows) { @@ -3993,14 +4100,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); @@ -4025,13 +4194,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"; @@ -4039,6 +4214,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 +454,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", @@ -781,24 +791,43 @@ 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+\$\{total\}/); -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/); 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*\{/); 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/); @@ -898,6 +927,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 +983,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 +1016,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 +1029,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 +1046,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 +1068,51 @@ 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, /"POST \/v1\/agent\/chat\/cancel"/); +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, /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/); +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, /--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 fc27e947..0a451060 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; @@ -1357,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; @@ -1514,6 +1566,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 +2479,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 +2498,11 @@ tbody tr:last-child td { min-height: 30px; } + .agent-timeout-control { + width: 100%; + justify-content: start; + } + .input-shell { padding: 0 8px; } @@ -2441,16 +2526,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 +2629,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; }