feat: add code agent provider profiles
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
|
||||
HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥官和 runner 进入本仓库时的顶级索引,只放入口、一句话规则和长期参考链接;细则统一维护在 `docs/reference/`。
|
||||
|
||||
## P0 D601 k3s 控制面归一
|
||||
## P0 G14 原生 k8s/GitOps 运行面归一
|
||||
|
||||
- D601 的 HWLAB DEV 运行态只允许使用自部署原生 k3s;Docker Desktop Kubernetes 已经停用并清理数据,任何人不得重新启用或把它作为 HWLAB DEV、MVP、CI/CD、smoke、部署或验收目标。跨仓库事故跟踪见 [pikasTech/unidesk#138](https://github.com/pikasTech/unidesk/issues/138),指挥官热修复背景见 [pikasTech/unidesk#118](https://github.com/pikasTech/unidesk/issues/118)。
|
||||
- D601 上裸 `kubectl` 不可信:默认 kubeconfig 可能仍残留 `docker-desktop` / `127.0.0.1:11700`。所有 D601 `hwlab-dev` 只读观测、DEV CD、rollback、smoke 和排障必须显式使用 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`,并确认节点名是 `d601`。
|
||||
- 发现 `docker-desktop` context、`desktop-control-plane` 节点、Docker Desktop Kubernetes 中的 `hwlab-dev` 残留,或同一 HWLAB 资源被第二控制面承载时,必须停止部署/验收并按 #138 处理;不要把第二控制面或失效默认 kubeconfig 的输出作为 DEV-LIVE 证据。
|
||||
- G14 是当前 HWLAB DEV/PROD 原生 k8s 与 GitOps 运行面真相;`hwlab-dev` 与 `hwlab-prod` 均由 G14 k3s、Tekton、`G14-gitops` 和 Argo CD 管理,详见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- D601 HWLAB DEV、D601 `dev-cd-apply`、`ci-publish` 和旧 `main` JS 脚本式 CI/CD 只作为 legacy 迁移来源和事故参考;新开发、发布、验收、文档和运行面实验不得把 D601 当作当前 HWLAB runtime source-of-truth。
|
||||
- G14 k3s 操作必须通过 UniDesk route `G14:k3s` 执行;不得用 D601 kubeconfig、Docker Desktop Kubernetes、master server 本地 check/build 或旧 JS CD 结果作为 G14 DEV/PROD 通过证据。
|
||||
|
||||
## P0 GitHub Issue 写入纪律
|
||||
|
||||
@@ -24,16 +24,17 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
||||
- G14 分支固定 source workspace 是 G14 节点上的 `/root/hwlab`,固定使用 `G14` 分支和 `origin git@github.com:pikasTech/HWLAB.git`。在 G14 上进行代码、文档、GitOps render、Tekton/poller/Argo CD 修复或 CI/CD 验证前,必须先确认 `pwd` 为 `/root/hwlab` 且 `git status --short --branch` 为 `G14...origin/G14`;不满足时先停止并修正 workspace。
|
||||
- G14 k3s 操作必须通过 UniDesk SSH route `G14:k3s` 执行,例如 `bun scripts/cli.ts ssh G14:k3s kubectl get pods -n hwlab-ci`;禁止使用 `ssh G14 k3s ...`。不要把 `/workspace/hwlab`、`/root/HWLAB`、D601 workspace、master-server checkout 或临时 clone 当作 G14 分支 source truth。
|
||||
- Runner 和指挥常用工作区是 `/workspace/hwlab`;进入仓库先检查分支与工作树状态,详见 [docs/reference/commander-collaboration.md](docs/reference/commander-collaboration.md)。
|
||||
- D601 发布/构建必须使用干净 worktree;`/home/ubuntu/workspace/hwlab` 只是共享缓存或建树来源,发现脏、落后或被并行任务占用时改用 `/tmp/hwlab-cicd-<sha>` detached worktree,详见 [docs/reference/deployment-publish.md](docs/reference/deployment-publish.md)。
|
||||
- Master server 只做控制、Git、issue/PR 和短轮询;HWLAB `check`、Playwright、本地构建、发布预检和 CI/CD 验证必须放到 D601/runner/CI/CD,详见 [docs/reference/deployment-publish.md](docs/reference/deployment-publish.md)。
|
||||
- G14 CI/CD 由 `G14` source branch、G14 k3s Tekton 和 `G14-gitops` branch 驱动;需要构建、Playwright、check、发布预检或运行面验证时放到 G14 k3s/runner/CI/CD,不在 master server 跑重型验证。
|
||||
- D601 发布/构建 worktree 纪律只适用于 legacy 路径回溯,不再作为当前 HWLAB 发布默认入口;当前入口见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- 当前一律走 PR 工作流;不要直推 `main`,默认不要合并自己的 PR;用户或指挥官明确授权且满足门禁时可按长期参考自合并,不要改 PROD、不要重启服务。
|
||||
- `DC-DCSN-P0-2026-003` / [pikasTech/HWLAB#78](https://github.com/pikasTech/HWLAB/issues/78) 是当前 M3 虚拟硬件可信闭环的上位约束;其他任务不得把 SOURCE、LOCAL、DRY-RUN、fixture 或前端状态误报为 M3 DEV-LIVE。
|
||||
- 仓库禁止创建或提交 repo report 目录;验收、进展和结论只承载在 #7、专题 issue、每日简报或 PR/issue 评论。临时 JSON 只能写入 `/tmp`、`.state` 或 CI artifact,不能进入源码仓库。
|
||||
|
||||
## 固定入口
|
||||
|
||||
- DEV Cloud Web:`http://74.48.78.17:16666/`,规则见 [docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)。
|
||||
- DEV API/edge/live:`http://74.48.78.17:16667/health/live`,规则见 [docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)。
|
||||
- G14 DEV Cloud Web:`http://74.48.78.17:17666/`,规则见 [docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)。
|
||||
- G14 DEV API/edge/live:`http://74.48.78.17:17667/health/live`,规则见 [docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)。
|
||||
- G14 PROD 预留入口:`http://74.48.78.17:18666/` 与 `http://74.48.78.17:18667/health/live`,规则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- Cloud Workbench 默认首页与 UX 约束见 [docs/reference/cloud-workbench.md](docs/reference/cloud-workbench.md)。
|
||||
|
||||
## 长期参考
|
||||
@@ -75,7 +76,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
||||
- DEV CD 单事务发布/应用/验证:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4`
|
||||
- CI/CD job 查询:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd status|logs|report <jobId>`
|
||||
- runner GitHub 可见性预检:`npm run runner:issue-visibility:preflight`
|
||||
- D601 k3s 只读观测:`npm run d601:k3s:readonly`
|
||||
- D601 k3s 只读观测:legacy 回溯入口,仅在确认需要 D601 事故复盘时使用;当前 G14 运行面观察使用 UniDesk route `G14:k3s`。
|
||||
- DEV runtime hotfix 只读审计计划:`npm run dev-runtime:hotfix-audit`
|
||||
- Gateway 主动出站本地 smoke:`npm run gateway:demo:smoke`;经本地 edge-proxy 验证用 `npm run gateway:demo:edge-smoke`。
|
||||
|
||||
|
||||
@@ -3,75 +3,56 @@
|
||||
This reference defines the stable DEV environment, port, k3s, and runtime
|
||||
substitution rules.
|
||||
|
||||
## Public DEV Ports
|
||||
## Public G14 Runtime Ports
|
||||
|
||||
| Surface | URL |
|
||||
| --- | --- |
|
||||
| Cloud Web browser entry | `http://74.48.78.17:16666/` |
|
||||
| API/edge entry | `http://74.48.78.17:16667/` |
|
||||
| API/live health | `http://74.48.78.17:16667/health/live` |
|
||||
| Surface | DEV URL | PROD URL |
|
||||
| --- | --- | --- |
|
||||
| Cloud Web browser entry | `http://74.48.78.17:17666/` | `http://74.48.78.17:18666/` |
|
||||
| API/edge entry | `http://74.48.78.17:17667/` | `http://74.48.78.17:18667/` |
|
||||
| API/live health | `http://74.48.78.17:17667/health/live` | `http://74.48.78.17:18667/health/live` |
|
||||
|
||||
Public `:6666` and `:6667` are deprecated as browser/API acceptance targets.
|
||||
Internal k3s services may still listen on `6667`; do not rewrite that as a
|
||||
public endpoint.
|
||||
Public `:16666` and `:16667` are D601 legacy DEV endpoints. They can be used only for migration comparison or incident replay, not as the current HWLAB runtime acceptance target. Internal k3s services may still listen on `6667`; do not rewrite that as a public endpoint.
|
||||
|
||||
## Route Shape
|
||||
|
||||
The public route is:
|
||||
The current G14 DEV route is:
|
||||
|
||||
```text
|
||||
master hwlab-frps-dev :16666/:16667
|
||||
-> D601 hwlab-frpc in namespace hwlab-dev
|
||||
master hwlab-frps-dev :17666/:17667
|
||||
-> G14 hwlab-g14-frpc in namespace hwlab-dev
|
||||
-> hwlab-cloud-web on internal :8080 for browser traffic
|
||||
-> hwlab-edge-proxy / hwlab-cloud-api on internal :6667 for API and health
|
||||
```
|
||||
|
||||
`deploy/deploy.json` is the source for public endpoint and FRP mapping drift
|
||||
checks. Use:
|
||||
G14 PROD is rendered by GitOps as namespace `hwlab-prod` and Argo CD application `hwlab-g14-prod`; it uses the `:18666/:18667` public endpoints when enabled. G14 desired state is generated into `deploy/gitops/g14/runtime-dev` and `deploy/gitops/g14/runtime-prod` and promoted through the `G14-gitops` branch.
|
||||
|
||||
## G14 k3s Source Of Truth
|
||||
|
||||
G14 native k3s is the HWLAB DEV/PROD runtime source of truth. Commands that observe or mutate current HWLAB runtime must go through UniDesk route `G14:k3s`, for example:
|
||||
|
||||
```sh
|
||||
node scripts/deploy-contract-plan.mjs --check
|
||||
tran G14:k3s kubectl -n hwlab-dev get deploy,svc,pod -o wide
|
||||
tran G14:k3s kubectl -n argocd get application hwlab-g14-dev hwlab-g14-prod
|
||||
```
|
||||
|
||||
## D601 k3s Source Of Truth
|
||||
Do not use D601 kubeconfig, D601 `dev-cd-apply`, old JS `ci-publish`, Docker Desktop Kubernetes, or master-server local checks as G14 runtime acceptance evidence. D601 remains a legacy migration/reference surface until the branch-role migration is complete.
|
||||
|
||||
D601 native k3s is the DEV runtime source of truth. Commands that observe or
|
||||
roll a DEV workload must specify:
|
||||
## Branch Role Migration
|
||||
|
||||
The target branch model is: G14 becomes the `main` runtime source branch, and the former D601-oriented `main` behavior moves to a D601 legacy/maintenance line. Until that branch governance operation is explicitly completed and verified, `/root/hwlab` on G14 continues to use the `G14` branch and must merge the latest `origin/main` business updates before G14-specific runtime work.
|
||||
|
||||
## Distributed Passthrough Hygiene
|
||||
|
||||
When running more than one command on G14, keep all commands inside one remote `script` block:
|
||||
|
||||
```sh
|
||||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev get deploy,svc,pod -o wide
|
||||
tran G14:/root/hwlab script <<'SCRIPT'
|
||||
sed -n '1,80p' docs/reference/dev-runtime-boundary.md
|
||||
sed -n '1,80p' AGENTS.md
|
||||
SCRIPT
|
||||
```
|
||||
|
||||
Do not trust the default `kubectl` context as a DEV runtime signal. On D601 it
|
||||
may point at another cluster such as `docker-desktop`. The 2026-05-23 control
|
||||
plane incident is tracked in
|
||||
[pikasTech/unidesk#138](https://github.com/pikasTech/unidesk/issues/138), with
|
||||
hotfix context in
|
||||
[pikasTech/unidesk#118](https://github.com/pikasTech/unidesk/issues/118):
|
||||
Docker Desktop Kubernetes has been disabled and cleaned, but the default
|
||||
`/home/ubuntu/.kube/config` may still point at the dead
|
||||
`docker-desktop`/`127.0.0.1:11700` entry. Treat that as residual bad config, not
|
||||
as DEV runtime evidence.
|
||||
|
||||
Before any DEV deploy, rollback, smoke, or manual k3s observation, confirm the
|
||||
native node explicitly:
|
||||
|
||||
```sh
|
||||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o jsonpath='{.items[*].metadata.name}'
|
||||
```
|
||||
|
||||
The result must contain `d601`. Seeing `desktop-control-plane`,
|
||||
`docker-desktop`, or `127.0.0.1:11700` means the command is on the wrong
|
||||
control-plane path and must stop before mutation or acceptance.
|
||||
|
||||
Use the read-only preflight when a runner needs to report k3s visibility:
|
||||
|
||||
```sh
|
||||
node scripts/d601-k3s-readonly-observability.mjs
|
||||
```
|
||||
|
||||
The preflight must not print secret values, kubeconfig material, ConfigMap
|
||||
values, or token content.
|
||||
Do not write `tran G14:/root/hwlab sed ... && sed ...` unless the second command is intentionally local. Shell operators such as `&&`, `;`, and pipes outside the `tran` invocation are evaluated by the local shell; this can make later commands run on the master server and look like a missing-file or truncation bug.
|
||||
|
||||
## Cloud API DB Readiness Authority
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# G14 GitOps CI/CD
|
||||
|
||||
G14 是 HWLAB 的旁路 DEV 集群目标。G14 CI/CD 必须只作用于 G14 k3s,不接管 D601 生产,不使用 UniDesk Code Queue 作为调度器,也不把 UniDesk backend、provider-gateway 或 microservice proxy 当作 HWLAB runtime。
|
||||
G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD 必须只作用于 G14 k3s,不接管 D601 legacy 运行面,不使用 UniDesk Code Queue 作为调度器,也不把 UniDesk backend、provider-gateway 或 microservice proxy 当作 HWLAB runtime。
|
||||
|
||||
## 目标模型
|
||||
|
||||
@@ -8,7 +8,7 @@ G14 是 HWLAB 的旁路 DEV 集群目标。G14 CI/CD 必须只作用于 G14 k3s
|
||||
- CI:Tekton 在 G14 k3s 内运行 `hwlab-g14-ci-image-publish` Pipeline,读取 `CI.json` 命令,按 source commit 构建镜像并推送到 G14 registry。
|
||||
- Artifact:镜像使用 commit tag,例如 `127.0.0.1:5000/hwlab/hwlab-cloud-api:<shortCommit>`;digest 由 registry 返回,CI report 只作为审计证据,不作为 CD 真相。
|
||||
- Branch split:`G14` 是源码监控分支;`G14-gitops` 是 Tekton promotion 写入的生成分支,保存 `deploy/gitops/g14/**` desired state。
|
||||
- CD:Argo CD 只消费 `G14-gitops:deploy/gitops/g14/runtime` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。
|
||||
- CD:Argo CD 只消费 `G14-gitops:deploy/gitops/g14/runtime-dev` 与 `deploy/gitops/g14/runtime-prod` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。
|
||||
- 并行性:不同 source commit 的 CI build 不共享发布锁;并行安全由 immutable commit tag/digest 和 Git desired state 保证。最终运行版本由 Argo CD 当前同步的 Git revision 决定。
|
||||
|
||||
## 生成入口
|
||||
@@ -24,6 +24,7 @@ G14 是 HWLAB 的旁路 DEV 集群目标。G14 CI/CD 必须只作用于 G14 k3s
|
||||
- 默认 registry prefix 是 `127.0.0.1:5000/hwlab`,用于 G14 单节点 k3s 的 node-local registry。
|
||||
- 默认 CI/CD proxy 是 G14 本机 `http://127.0.0.1:10808` / `socks5h://127.0.0.1:10808`。Tekton CI step、Docker-in-Docker sidecar 和 publish step 都注入 proxy/no_proxy;publish step 必须先在 DIND 内 `docker pull $HWLAB_DEV_BASE_IMAGE`,再进入 DEV base-image preflight、build 和 registry push,保证 npm、base image 拉取和 registry push 走 G14 本机代理边界。
|
||||
- `ci-json` 不允许每次运行时重新 `apt-get install` 或 `npx playwright install --with-deps`。它必须使用已经预热的 CI 基础镜像,当前默认由 `scripts/g14-gitops-render.mjs` 固定为 `mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53`;该镜像在 G14 k3s/containerd 中已缓存,包含 Node、npm、Git、Python3、OpenSSH、curl、Playwright 系统依赖和 `/ms-playwright/chromium-1217`。脚本启动时必须输出 `ci-base-image` 和 `playwright-cache-check` 结构化日志;缺少工具或 Chromium cache 时直接失败并修基础镜像,不能回退到 runtime apt 下载。若以后需要 Node 22 固定版本,先构建并推送 G14 registry 内的 Node22+Playwright runner 镜像,再修改 `HWLAB_G14_CI_JSON_IMAGE`/render 默认值。
|
||||
- G14 host 只用于 source workspace、GitOps render、k3s 控制和轻量语法/静态合同检查;不要把 host 当成浏览器执行面。当前 G14 host OS 可能比 Playwright 官方 browser 包支持矩阵更新,`npx playwright install chromium` 会失败或 browser executable 不存在。`web:check` 默认允许这类明确的 Playwright/browser runtime 环境限制作为 `environment_blocker`/`skip` 通过源码合同;真正的布局、移动端和交互 browser smoke 必须在 G14 k3s/Tekton 的 Playwright 基础镜像内运行,不能用 host 上强装 browser 作为长期方案。
|
||||
- Poller、control-plane reconciler 和 image publish step 也不允许每次运行时 `apk add`。当前 G14 registry 固定工具镜像是 `127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1`,包含 Node 22、npm、Git、OpenSSH、curl、Python3 和 Docker CLI;生成的脚本只做 proxy preflight 与工具存在性检查。若需要升级工具,先在 G14 构建/推送新的工具镜像,再修改 `HWLAB_G14_CI_TOOLS_IMAGE`/render 默认值并由 reconciler apply。
|
||||
- 服务镜像构建的默认 parent/base image 不得从 Docker Hub 反复拉取。当前 `node:20-bookworm-slim` 已镜像到 G14 registry 的 allowlist 名称:`127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim`,render 默认 `base-image` 和 poller `BASE_IMAGE` 都指向这个本地镜像。需要升级 parent image 时,先通过 G14 proxy 拉取并推送到 G14 registry,再修改 `HWLAB_G14_DEV_BASE_IMAGE`/render 默认值;image publish step 只允许从本地 registry pull base image,且 tag 必须符合 publish gate 的 `hwlab-node20-base`/`hwlab-dev-base`/`hwlab-node-runtime-base` allowlist。
|
||||
- 任何依赖下载阶段都必须有可观测诊断。生成的 Tekton 脚本在 apt/apk、npm、Playwright browser、Docker base image pull 和 GitOps promote 之前输出结构化 `dependency-proxy-probe`、`dependency-curl-probe` 或 `dependency-download-*` 日志,至少包含 phase、目标 URL/镜像、脱敏 proxy、首包耗时、总耗时、下载字节数和速度。CI/CD 卡在下载时,先用这些日志判断是 proxy 不可达、目标源慢、DNS/首包慢还是下载吞吐低,再决定是否切换 G14 代理节点或预热镜像;不能只凭 PipelineRun Running 时长判断业务测试失败。
|
||||
@@ -35,6 +36,20 @@ npm run g14:gitops:render -- --source-revision <sourceCommit>
|
||||
npm run g14:gitops:check
|
||||
```
|
||||
|
||||
|
||||
## Code Agent Provider Profiles
|
||||
|
||||
G14 Code Agent 通过同一个 repo-owned Codex app-server stdio runner 承载多个 OpenAI-compatible Responses profile。Cloud Web 每次请求都发送 `providerProfile`,后端按 profile 生成本次请求的 env overlay;不得把某一个模型通道硬写死到全局运行态而删除另一个通道。
|
||||
|
||||
| Profile | 默认 | Model | Base URL | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `deepseek` | 是 | `HWLAB_CODE_AGENT_DEEPSEEK_MODEL`,默认 `deepseek-chat` | `HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL`,默认 `http://hwlab-deepseek-proxy.<namespace>.svc.cluster.local:4000/v1/responses` | G14 集群内 LiteLLM DeepSeek bridge。 |
|
||||
| `codex-api` | 否 | `HWLAB_CODE_AGENT_CODEX_API_MODEL`,默认 `gpt-5.5` | `HWLAB_CODE_AGENT_CODEX_API_BASE_URL`,默认 `http://172.26.26.227:17680/v1/responses` | 保留旧 Codex/OpenAI-compatible Responses API 通道。 |
|
||||
|
||||
`hwlab-cloud-api` 仍以 `HWLAB_CODE_AGENT_PROVIDER=codex-stdio` 运行。`HWLAB_CODE_AGENT_MODEL` 与 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 可作为 runtime-default 兜底,但前端默认 profile 是 `deepseek`,用户可以切到 `codex-api`。Codex app-server 启动参数必须同时设置 provider base URL、`model` 和 `review_model`,否则 LiteLLM `/v1/responses` 会收到 `model=None`。旧 `codex-api` profile 指向 `172.26.26.227`,cloud-api 的 `NO_PROXY/no_proxy` 必须包含该 host 或私网段,避免旧通道被 G14 外网代理误拦截。
|
||||
|
||||
DeepSeek proxy manifest 是 GitOps desired state 的一部分,DEV/PROD 分别生成在 `runtime-dev/deepseek-proxy.yaml` 和 `runtime-prod/deepseek-proxy.yaml`。DeepSeek key 仍通过 `hwlab-code-agent-provider/openai-api-key` Secret 注入到 proxy 容器;文档、trace、health、issue 和日志不得打印 Secret 值。
|
||||
|
||||
## Polling 触发
|
||||
|
||||
G14 不要求 GitHub webhook 或 GitHub Actions 配置。`hwlab-ci/hwlab-g14-branch-poller` CronJob 每 5 分钟通过 Git SSH 拉取 `G14` HEAD,并用 source commit 的前 12 位生成 PipelineRun 名称 `hwlab-g14-ci-poll-<short12>`。
|
||||
|
||||
@@ -1154,6 +1154,7 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
|
||||
test("Codex app-server args pin repo-owned DEV responses egress without leaking provider env", () => {
|
||||
const env = {
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses",
|
||||
HWLAB_CODE_AGENT_MODEL: "deepseek-chat",
|
||||
OPENAI_API_KEY: "test-openai-key-material"
|
||||
};
|
||||
|
||||
@@ -1164,6 +1165,8 @@ test("Codex app-server args pin repo-owned DEV responses egress without leaking
|
||||
assert.ok(args.includes("model_provider=\"OpenAI\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.base_url=\"http://172.26.26.227:17680/v1\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.wire_api=\"responses\""));
|
||||
assert.ok(args.includes("model=\"deepseek-chat\""));
|
||||
assert.ok(args.includes("review_model=\"deepseek-chat\""));
|
||||
assert.equal(args.join(" ").includes("test-openai-key-material"), false);
|
||||
assert.equal(args.join(" ").includes("/v1/responses"), false);
|
||||
});
|
||||
|
||||
@@ -1190,7 +1190,8 @@ export function codexAppServerProviderBaseUrl(env = process.env) {
|
||||
function codexAppServerProviderConfigArgs(env = process.env) {
|
||||
const baseUrl = codexAppServerProviderBaseUrl(env);
|
||||
if (!baseUrl) return [];
|
||||
return [
|
||||
const model = firstNonEmpty(env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL);
|
||||
const args = [
|
||||
"-c",
|
||||
"model_provider=\"OpenAI\"",
|
||||
"-c",
|
||||
@@ -1200,6 +1201,11 @@ function codexAppServerProviderConfigArgs(env = process.env) {
|
||||
"-c",
|
||||
"model_providers.OpenAI.requires_openai_auth=true"
|
||||
];
|
||||
if (model) {
|
||||
args.push("-c", `model=${tomlString(model)}`);
|
||||
args.push("-c", `review_model=${tomlString(model)}`);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function tomlString(value) {
|
||||
|
||||
@@ -50,6 +50,16 @@ import { createGatewayDemoRegistry } from "./gateway-demo-registry.mjs";
|
||||
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
||||
const LIVE_BUILD_TIMEOUT_MS = 900;
|
||||
const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120;
|
||||
const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek";
|
||||
const CODE_AGENT_PROVIDER_PROFILE_IDS = Object.freeze(["deepseek", "codex-api", "runtime-default"]);
|
||||
const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({
|
||||
"deepseek": "DeepSeek",
|
||||
"codex-api": "Codex API",
|
||||
"runtime-default": "运行默认"
|
||||
});
|
||||
const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5";
|
||||
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://172.26.26.227:17680/v1/responses";
|
||||
const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat";
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const LIVE_BUILD_METADATA_PATHS = Object.freeze({
|
||||
deployManifest: "deploy/deploy.json",
|
||||
@@ -1288,15 +1298,15 @@ async function handleCodeAgentChatHttp(request, response, options) {
|
||||
}
|
||||
|
||||
function runCodeAgentChat(params, options) {
|
||||
return handleCodeAgentChat(params, codeAgentChatExecutionOptions(options));
|
||||
return handleCodeAgentChat(params, codeAgentChatExecutionOptions(options, params));
|
||||
}
|
||||
|
||||
function codeAgentChatExecutionOptions(options = {}) {
|
||||
function codeAgentChatExecutionOptions(options = {}, params = {}) {
|
||||
return {
|
||||
callProvider: options.callCodeAgentProvider,
|
||||
timeoutMs: options.codeAgentTimeoutMs,
|
||||
hardTimeoutMs: options.codeAgentHardTimeoutMs,
|
||||
env: options.env,
|
||||
env: codeAgentProviderProfileEnv(options.env ?? process.env, params),
|
||||
workspace: options.workspace,
|
||||
skillsDirs: options.skillsDirs,
|
||||
skillsDirsExact: options.skillsDirsExact,
|
||||
@@ -1309,6 +1319,52 @@ function codeAgentChatExecutionOptions(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentProviderProfileEnv(env = process.env, params = {}) {
|
||||
const profile = normalizeCodeAgentProviderProfile(params.providerProfile ?? params.codeAgentProviderProfile ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE);
|
||||
if (profile === "runtime-default") return env;
|
||||
const overlay = { ...env };
|
||||
overlay.HWLAB_CODE_AGENT_SELECTED_PROVIDER_PROFILE = profile;
|
||||
overlay.HWLAB_CODE_AGENT_PROVIDER_PROFILE_LABEL = CODE_AGENT_PROVIDER_PROFILE_LABELS[profile] ?? profile;
|
||||
overlay.HWLAB_CODE_AGENT_PROVIDER = "codex-stdio";
|
||||
if (profile === "codex-api") {
|
||||
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_LEGACY_CODEX_MODEL, DEFAULT_CODE_AGENT_CODEX_API_MODEL);
|
||||
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_BASE_URL, env.HWLAB_CODE_AGENT_LEGACY_OPENAI_BASE_URL, DEFAULT_CODE_AGENT_CODEX_API_BASE_URL);
|
||||
} else {
|
||||
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, DEFAULT_CODE_AGENT_DEEPSEEK_MODEL);
|
||||
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL, defaultDeepSeekBaseUrlForEnv(env));
|
||||
}
|
||||
overlay.NO_PROXY = mergeNoProxyEntries(env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
|
||||
overlay.no_proxy = mergeNoProxyEntries(env.no_proxy ?? env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function normalizeCodeAgentProviderProfile(value) {
|
||||
const text = String(value ?? DEFAULT_CODE_AGENT_PROVIDER_PROFILE).trim().toLowerCase();
|
||||
return CODE_AGENT_PROVIDER_PROFILE_IDS.includes(text) ? text : DEFAULT_CODE_AGENT_PROVIDER_PROFILE;
|
||||
}
|
||||
|
||||
function defaultDeepSeekBaseUrlForEnv(env = process.env) {
|
||||
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
|
||||
}
|
||||
|
||||
function codeAgentClusterNoProxyEntries(env = process.env) {
|
||||
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
return [`${namespace}.svc.cluster.local`, ".svc", ".cluster.local", "127.0.0.1", "localhost", "::1", "10.0.0.0/8", "10.42.0.0/16", "10.43.0.0/16", "172.26.26.227", "172.26.0.0/16"];
|
||||
}
|
||||
|
||||
function mergeNoProxyEntries(current, additions = []) {
|
||||
return uniqueStrings([...String(current ?? "").split(","), ...additions]).join(",");
|
||||
}
|
||||
|
||||
function firstNonEmptyValue(...values) {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function submitCodeAgentChatTurn({ params, options, traceId }) {
|
||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
|
||||
|
||||
+171
-32
@@ -15,12 +15,19 @@ const defaultBranch = process.env.HWLAB_G14_BRANCH || "G14";
|
||||
const defaultGitopsBranch = process.env.HWLAB_G14_GITOPS_BRANCH || "G14-gitops";
|
||||
const defaultRuntimeEndpoint = process.env.HWLAB_G14_RUNTIME_ENDPOINT || "http://74.48.78.17:17667";
|
||||
const defaultWebEndpoint = process.env.HWLAB_G14_WEB_ENDPOINT || "http://74.48.78.17:17666";
|
||||
const defaultProdRuntimeEndpoint = process.env.HWLAB_G14_PROD_RUNTIME_ENDPOINT || "http://74.48.78.17:18667";
|
||||
const defaultProdWebEndpoint = process.env.HWLAB_G14_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
|
||||
const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808";
|
||||
const defaultAllProxyUrl = process.env.HWLAB_G14_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
|
||||
const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc";
|
||||
const ciJsonRunnerImage = process.env.HWLAB_G14_CI_JSON_IMAGE || "mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53";
|
||||
const ciToolsRunnerImage = process.env.HWLAB_G14_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1";
|
||||
const defaultDevBaseImage = process.env.HWLAB_G14_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
|
||||
const deepSeekProxyImage = process.env.HWLAB_G14_DEEPSEEK_PROXY_IMAGE || "ghcr.io/berriai/litellm:main-latest";
|
||||
const deepSeekLiteLlmModel = process.env.HWLAB_G14_DEEPSEEK_LITELLM_MODEL || "deepseek/deepseek-chat";
|
||||
const deepSeekProfileModel = process.env.HWLAB_G14_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
|
||||
const codexApiProfileModel = process.env.HWLAB_G14_CODEX_API_PROFILE_MODEL || "gpt-5.5";
|
||||
const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL || "http://172.26.26.227:17680/v1/responses";
|
||||
const sourceCommitPattern = /^[a-f0-9]{7,40}$/u;
|
||||
|
||||
function proxyEnv() {
|
||||
@@ -46,6 +53,8 @@ function parseArgs(argv) {
|
||||
sourceRepo: defaultSourceRepo,
|
||||
runtimeEndpoint: defaultRuntimeEndpoint,
|
||||
webEndpoint: defaultWebEndpoint,
|
||||
prodRuntimeEndpoint: defaultProdRuntimeEndpoint,
|
||||
prodWebEndpoint: defaultProdWebEndpoint,
|
||||
check: false,
|
||||
write: true,
|
||||
help: false
|
||||
@@ -60,6 +69,8 @@ function parseArgs(argv) {
|
||||
else if (arg === "--source-repo") args.sourceRepo = readOption(argv, ++index, arg);
|
||||
else if (arg === "--runtime-endpoint") args.runtimeEndpoint = readOption(argv, ++index, arg);
|
||||
else if (arg === "--web-endpoint") args.webEndpoint = readOption(argv, ++index, arg);
|
||||
else if (arg === "--prod-runtime-endpoint") args.prodRuntimeEndpoint = readOption(argv, ++index, arg);
|
||||
else if (arg === "--prod-web-endpoint") args.prodWebEndpoint = readOption(argv, ++index, arg);
|
||||
else if (arg === "--check") {
|
||||
args.check = true;
|
||||
args.write = false;
|
||||
@@ -91,6 +102,8 @@ function usage() {
|
||||
` --registry-prefix PREFIX default: ${defaultRegistryPrefix}`,
|
||||
` --runtime-endpoint URL default: ${defaultRuntimeEndpoint}`,
|
||||
` --web-endpoint URL default: ${defaultWebEndpoint}`,
|
||||
` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`,
|
||||
` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`,
|
||||
" --check verify generated files are current without writing"
|
||||
].join("\n");
|
||||
}
|
||||
@@ -181,24 +194,53 @@ function imageFor(registryPrefix, serviceId, shortCommit) {
|
||||
return `${registryPrefix}/${serviceId}:${shortCommit}`;
|
||||
}
|
||||
|
||||
function transformWorkloads({ workloads, deploy, source, registryPrefix, runtimeEndpoint, webEndpoint }) {
|
||||
function namespaceNameForProfile(profile) {
|
||||
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
}
|
||||
|
||||
function runtimePathForProfile(profile) {
|
||||
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
||||
}
|
||||
|
||||
function runtimeLabelForProfile(profile) {
|
||||
return profile === "prod" ? "prod" : "dev";
|
||||
}
|
||||
|
||||
function profileEndpoint(args, profile) {
|
||||
return profile === "prod"
|
||||
? { runtimeEndpoint: args.prodRuntimeEndpoint, webEndpoint: args.prodWebEndpoint }
|
||||
: { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
|
||||
}
|
||||
|
||||
function deepSeekBaseUrl(namespace) {
|
||||
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
|
||||
}
|
||||
|
||||
function namespaceScopedHost(value, namespace) {
|
||||
if (typeof value !== "string") return value;
|
||||
return value.replace(/\.hwlab-dev\.svc\.cluster\.local/gu, `.${namespace}.svc.cluster.local`);
|
||||
}
|
||||
|
||||
function transformWorkloads({ workloads, deploy, source, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev" }) {
|
||||
const result = cloneJson(workloads);
|
||||
const deployServices = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const profileLabel = runtimeLabelForProfile(profile);
|
||||
const stableRuntimeLabels = {
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/environment": "dev",
|
||||
"hwlab.pikastech.local/profile": "dev"
|
||||
"hwlab.pikastech.local/environment": profileLabel,
|
||||
"hwlab.pikastech.local/profile": profileLabel
|
||||
};
|
||||
for (const item of asItems(result, "workloads")) {
|
||||
item.metadata.namespace = deploy.namespace;
|
||||
item.metadata.namespace = namespace;
|
||||
label(item.metadata, {
|
||||
...stableRuntimeLabels,
|
||||
"hwlab.pikastech.local/gitops-target": "g14",
|
||||
"hwlab.pikastech.local/source-commit": source.full,
|
||||
"unidesk.ai/g14-staging": "true"
|
||||
"unidesk.ai/g14-staging": profile === "dev" ? "true" : "false"
|
||||
});
|
||||
annotate(item.metadata, {
|
||||
"hwlab.pikastech.local/gitops-note": "G14 GitOps target only; production traffic remains outside this target.",
|
||||
"hwlab.pikastech.local/gitops-note": profile === "prod" ? "G14 PROD GitOps target." : "G14 DEV GitOps target.",
|
||||
"hwlab.pikastech.local/source-commit": source.full
|
||||
});
|
||||
if (item.kind === "Job") {
|
||||
@@ -234,15 +276,30 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
|
||||
container.image = image;
|
||||
container.imagePullPolicy = "IfNotPresent";
|
||||
container.env ??= [];
|
||||
for (const entry of container.env) {
|
||||
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
|
||||
}
|
||||
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
|
||||
upsertEnv(container.env, "HWLAB_COMMIT_ID", source.full);
|
||||
upsertEnv(container.env, "HWLAB_IMAGE", image);
|
||||
upsertEnv(container.env, "HWLAB_IMAGE_TAG", source.short);
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", "g14");
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", source.full);
|
||||
if (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-edge-proxy") {
|
||||
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
|
||||
}
|
||||
if (serviceId === "hwlab-cloud-api") {
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek");
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_MODEL", codexApiProfileModel);
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_BASE_URL", codexApiProfileBaseUrl);
|
||||
upsertEnv(container.env, "NO_PROXY", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
|
||||
upsertEnv(container.env, "no_proxy", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
|
||||
}
|
||||
if (serviceId === "hwlab-cloud-web") {
|
||||
upsertEnv(container.env, "HWLAB_API_BASE_URL", `http://hwlab-cloud-api.${namespace}.svc.cluster.local:6667`);
|
||||
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", webEndpoint);
|
||||
}
|
||||
if (serviceId === "hwlab-cli") {
|
||||
@@ -259,8 +316,14 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
|
||||
return result;
|
||||
}
|
||||
|
||||
function transformHealthContract(value, namespace, labels, annotations, runtimeEndpoint, webEndpoint) {
|
||||
function transformHealthContract(value, namespace, labels, annotations, runtimeEndpoint, webEndpoint, profile = "dev") {
|
||||
const result = transformListNamespace(value, namespace, labels, annotations);
|
||||
if (result.metadata?.name === "hwlab-dev-health-contract") {
|
||||
result.metadata.name = `hwlab-${profile}-health-contract`;
|
||||
}
|
||||
if (result.metadata?.labels && Object.hasOwn(result.metadata.labels, "app.kubernetes.io/name")) {
|
||||
result.metadata.labels["app.kubernetes.io/name"] = `hwlab-${profile}-health-contract`;
|
||||
}
|
||||
if (result.data && typeof result.data === "object") {
|
||||
if (Object.hasOwn(result.data, "endpoint")) result.data.endpoint = runtimeEndpoint;
|
||||
if (Object.hasOwn(result.data, "cloud-web")) {
|
||||
@@ -1194,26 +1257,31 @@ function argoProject() {
|
||||
spec: {
|
||||
description: "HWLAB G14 GitOps project; D601 remains outside this project.",
|
||||
sourceRepos: [defaultSourceRepo],
|
||||
destinations: [{ server: "https://kubernetes.default.svc", namespace: "hwlab-dev" }],
|
||||
destinations: [
|
||||
{ server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
|
||||
{ server: "https://kubernetes.default.svc", namespace: "hwlab-prod" }
|
||||
],
|
||||
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
||||
namespaceResourceWhitelist: [{ group: "*", kind: "*" }]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function argoApplication(args) {
|
||||
function argoApplication(args, profile = "dev") {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "Application",
|
||||
metadata: {
|
||||
name: "hwlab-g14-dev",
|
||||
name: `hwlab-g14-${profile}`,
|
||||
namespace: "argocd",
|
||||
labels: { "app.kubernetes.io/part-of": "hwlab", "hwlab.pikastech.local/gitops-target": "g14" }
|
||||
},
|
||||
spec: {
|
||||
project: "hwlab-g14",
|
||||
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: "deploy/gitops/g14/runtime" },
|
||||
destination: { server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
|
||||
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` },
|
||||
destination: { server: "https://kubernetes.default.svc", namespace },
|
||||
syncPolicy: { automated: { prune: false, selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
||||
}
|
||||
};
|
||||
@@ -1289,12 +1357,71 @@ remotePort = 17667
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeKustomization({ source }) {
|
||||
function deepSeekProxyManifest({ profile = "dev" } = {}) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": "hwlab-deepseek-proxy",
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/environment": runtimeLabelForProfile(profile),
|
||||
"hwlab.pikastech.local/gitops-target": "g14",
|
||||
"hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy"
|
||||
};
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
items: [
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "ConfigMap",
|
||||
metadata: { name: "hwlab-deepseek-proxy-config", namespace, labels },
|
||||
data: {
|
||||
"config.yaml": `model_list:\n - model_name: ${deepSeekProfileModel}\n litellm_params:\n model: ${deepSeekLiteLlmModel}\n api_key: os.environ/DEEPSEEK_API_KEY\n api_base: https://api.deepseek.com\nlitellm_settings:\n drop_params: true\n set_verbose: false\n`
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deployment",
|
||||
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
|
||||
spec: {
|
||||
replicas: 1,
|
||||
selector: { matchLabels: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" } },
|
||||
template: {
|
||||
metadata: { labels },
|
||||
spec: {
|
||||
containers: [{
|
||||
name: "litellm",
|
||||
image: deepSeekProxyImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
args: ["--config", "/etc/litellm/config.yaml", "--host", "0.0.0.0", "--port", "4000"],
|
||||
env: [{ name: "DEEPSEEK_API_KEY", valueFrom: { secretKeyRef: { name: "hwlab-code-agent-provider", key: "openai-api-key", optional: true } } }],
|
||||
ports: [{ name: "http", containerPort: 4000 }],
|
||||
readinessProbe: { httpGet: { path: "/health/readiness", port: "http" }, initialDelaySeconds: 10, periodSeconds: 10 },
|
||||
livenessProbe: { httpGet: { path: "/health/liveliness", port: "http" }, initialDelaySeconds: 20, periodSeconds: 20 },
|
||||
volumeMounts: [{ name: "config", mountPath: "/etc/litellm", readOnly: true }]
|
||||
}],
|
||||
volumes: [{ name: "config", configMap: { name: "hwlab-deepseek-proxy-config" } }]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
|
||||
spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" }, ports: [{ name: "http", port: 4000, targetPort: "http" }] }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeKustomization({ profile = "dev" } = {}) {
|
||||
const resources = ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"];
|
||||
if (profile === "dev") resources.push("g14-frpc.yaml");
|
||||
return {
|
||||
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
||||
kind: "Kustomization",
|
||||
namespace: "hwlab-dev",
|
||||
resources: ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "g14-frpc.yaml"]
|
||||
namespace: namespaceNameForProfile(profile),
|
||||
resources
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1317,7 +1444,7 @@ This directory is generated from \`CI.json\`, \`deploy/deploy.json\`, and \`depl
|
||||
}
|
||||
|
||||
async function plannedFiles(args) {
|
||||
const [ci, deploy, namespace, config, services, health, workloads] = await Promise.all([
|
||||
const [ci, deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([
|
||||
readJson("CI.json"),
|
||||
readJson("deploy/deploy.json"),
|
||||
readJson("deploy/k8s/base/namespace.yaml"),
|
||||
@@ -1327,12 +1454,8 @@ async function plannedFiles(args) {
|
||||
readJson("deploy/k8s/base/workloads.yaml")
|
||||
]);
|
||||
const source = await resolveSourceRevision(args.sourceRevision);
|
||||
const labels = { "hwlab.pikastech.local/gitops-target": "g14", "hwlab.pikastech.local/source-commit": source.full };
|
||||
const baseLabels = { "hwlab.pikastech.local/gitops-target": "g14", "hwlab.pikastech.local/source-commit": source.full };
|
||||
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" };
|
||||
const runtimeNamespace = cloneJson(namespace);
|
||||
runtimeNamespace.metadata.name = deploy.namespace;
|
||||
label(runtimeNamespace.metadata, labels);
|
||||
annotate(runtimeNamespace.metadata, annotations);
|
||||
const files = new Map();
|
||||
const putJson = (relative, value) => files.set(path.join(args.outDir, relative), jsonManifest(value));
|
||||
const putText = (relative, value) => files.set(path.join(args.outDir, relative), textFile(value));
|
||||
@@ -1345,11 +1468,14 @@ async function plannedFiles(args) {
|
||||
gitopsBranch: args.gitopsBranch,
|
||||
sourceRepo: args.sourceRepo,
|
||||
registryPrefix: args.registryPrefix,
|
||||
runtimePath: "deploy/gitops/g14/runtime",
|
||||
publicEndpoints: { web: args.webEndpoint, runtime: args.runtimeEndpoint },
|
||||
runtimePaths: ["deploy/gitops/g14/runtime-dev", "deploy/gitops/g14/runtime-prod"],
|
||||
publicEndpoints: {
|
||||
dev: { web: args.webEndpoint, runtime: args.runtimeEndpoint },
|
||||
prod: { web: args.prodWebEndpoint, runtime: args.prodRuntimeEndpoint }
|
||||
},
|
||||
frpDeployment: "hwlab-dev/hwlab-g14-frpc",
|
||||
tektonPipeline: "hwlab-ci/hwlab-g14-ci-image-publish",
|
||||
argoApplication: "argocd/hwlab-g14-dev"
|
||||
argoApplications: ["argocd/hwlab-g14-dev", "argocd/hwlab-g14-prod"]
|
||||
});
|
||||
putText("README.md", readme({ source, args }));
|
||||
putJson("registry/registry.yaml", registryManifest());
|
||||
@@ -1359,14 +1485,27 @@ async function plannedFiles(args) {
|
||||
putJson("tekton/control-plane-reconciler.yaml", tektonControlPlaneReconcilerCronJob(args));
|
||||
putJson("tekton/pipelinerun.sample.yaml", tektonPipelineRunTemplate({ source, args }));
|
||||
putJson("argocd/project.yaml", argoProject());
|
||||
putJson("argocd/application.yaml", argoApplication(args));
|
||||
putJson("runtime/kustomization.yaml", runtimeKustomization({ source }));
|
||||
putJson("runtime/namespace.yaml", runtimeNamespace);
|
||||
putJson("runtime/code-agent-codex-config.yaml", transformListNamespace(config, deploy.namespace, labels, annotations));
|
||||
putJson("runtime/services.yaml", transformListNamespace(services, deploy.namespace, labels, annotations));
|
||||
putJson("runtime/health-contract.yaml", transformHealthContract(health, deploy.namespace, labels, annotations, args.runtimeEndpoint, args.webEndpoint));
|
||||
putJson("runtime/workloads.yaml", transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint }));
|
||||
putJson("runtime/g14-frpc.yaml", g14FrpcManifest());
|
||||
putJson("argocd/application-dev.yaml", argoApplication(args, "dev"));
|
||||
putJson("argocd/application-prod.yaml", argoApplication(args, "prod"));
|
||||
|
||||
for (const profile of ["dev", "prod"]) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) };
|
||||
const runtimeNamespace = cloneJson(namespaceTemplate);
|
||||
runtimeNamespace.metadata.name = namespace;
|
||||
label(runtimeNamespace.metadata, profileLabels);
|
||||
annotate(runtimeNamespace.metadata, annotations);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
const endpoints = profileEndpoint(args, profile);
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile }));
|
||||
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
|
||||
putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile }));
|
||||
if (profile === "dev") putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest());
|
||||
}
|
||||
return { files, source };
|
||||
}
|
||||
|
||||
|
||||
@@ -1264,6 +1264,57 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
safety: layoutSafety()
|
||||
});
|
||||
} catch (error) {
|
||||
if (isBrowserExecutionEnvironmentError(error)) {
|
||||
const summary = `Workbench layout smoke skipped because the browser could not run: ${error.message}`;
|
||||
return sanitizeLayoutReport({
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: reportModeId("layout"),
|
||||
reportVersion: "v1",
|
||||
status: "skip",
|
||||
issue: "pikasTech/HWLAB#273",
|
||||
taskId: "dev-cloud-workbench-layout",
|
||||
commitId: observeSourceIdentity().reportCommitId,
|
||||
acceptanceLevel: "dev_cloud_workbench_layout",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: layoutReportLifecycle(useLiveUrl, "blocked"),
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "layout-browser",
|
||||
sourceMode: layoutSourceMode,
|
||||
url,
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceContract: layoutSourceContract(),
|
||||
validationCommands: layoutValidationCommands(),
|
||||
localSmoke: layoutLocalSmoke("skip"),
|
||||
dryRun: layoutDryRun(),
|
||||
devPreconditions: layoutDevPreconditions(useLiveUrl, "blocked"),
|
||||
evidenceLevel: useLiveUrl ? "BLOCKED" : "SOURCE",
|
||||
devLive: false,
|
||||
summary,
|
||||
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
|
||||
checks: [
|
||||
{
|
||||
id: "layout-browser-runtime",
|
||||
status: "skip",
|
||||
summary,
|
||||
evidence: ["browser launch failed", "use G14 k3s/CI Playwright base image for browser smoke"]
|
||||
}
|
||||
],
|
||||
blockers: [
|
||||
{
|
||||
type: "environment_blocker",
|
||||
scope: "layout-browser-runtime",
|
||||
status: "open",
|
||||
summary
|
||||
}
|
||||
],
|
||||
artifacts: {
|
||||
screenshotDir: artifactRoot,
|
||||
reportPath: args.reportPath ?? null
|
||||
},
|
||||
safety: layoutSafety()
|
||||
});
|
||||
}
|
||||
return sanitizeLayoutReport({
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: reportModeId("layout"),
|
||||
@@ -9206,6 +9257,11 @@ function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
function isBrowserExecutionEnvironmentError(error) {
|
||||
const message = String(error?.message ?? error ?? "");
|
||||
return /Executable doesn't exist|Please run the following command to download new browsers|does not support chromium|Host system is missing dependencies|browserType\.launch|Failed to launch|No usable sandbox|Missing X server|Target page, context or browser has been closed/iu.test(message);
|
||||
}
|
||||
|
||||
export function printSmokeHelp() {
|
||||
return {
|
||||
status: "usage",
|
||||
|
||||
@@ -25,6 +25,9 @@ const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000;
|
||||
const DEFAULT_GATEWAY_SHELL_TIMEOUT_MS = 120000;
|
||||
const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1";
|
||||
const GATEWAY_SHELL_TIMEOUT_STORAGE_KEY = "hwlab.workbench.gatewayShellTimeoutMs.v1";
|
||||
const CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY = "hwlab.workbench.codeAgentProviderProfile.v1";
|
||||
const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek";
|
||||
const CODE_AGENT_PROVIDER_PROFILES = Object.freeze(["deepseek", "codex-api"]);
|
||||
const CODE_AGENT_SESSION_STORAGE_KEY = "hwlab.workbench.codeAgentSession.v1";
|
||||
const CODE_AGENT_SESSION_STORAGE_VERSION = 1;
|
||||
const CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
@@ -38,6 +41,7 @@ const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS,
|
||||
const LIVE_SURFACE_TIMEOUT_MS = resolveTimeoutMs("liveSurfaceTimeoutMs", DEFAULT_LIVE_SURFACE_TIMEOUT_MS, { min: 5000, max: 60000 });
|
||||
let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs();
|
||||
let GATEWAY_SHELL_TIMEOUT_MS = resolveUserGatewayShellTimeoutMs();
|
||||
let CODE_AGENT_PROVIDER_PROFILE = resolveUserCodeAgentProviderProfile();
|
||||
const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: configuredTimeoutMinMs("codeAgentSubmitTimeoutMs", 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([
|
||||
@@ -148,6 +152,7 @@ const el = {
|
||||
gateReviewBody: byId("gate-review-body"),
|
||||
commandForm: byId("command-form"),
|
||||
commandInput: byId("command-input"),
|
||||
codeAgentProviderProfile: byId("code-agent-provider-profile"),
|
||||
codeAgentTimeout: byId("code-agent-timeout"),
|
||||
gatewayShellTimeout: byId("gateway-shell-timeout"),
|
||||
commandSend: byId("command-send"),
|
||||
@@ -314,8 +319,33 @@ function resolveUserGatewayShellTimeoutMs() {
|
||||
?? resolveTimeoutMs("gatewayShellTimeoutMs", DEFAULT_GATEWAY_SHELL_TIMEOUT_MS, { min: 30000, max: 600000 });
|
||||
}
|
||||
|
||||
function resolveUserCodeAgentProviderProfile() {
|
||||
let stored = null;
|
||||
try {
|
||||
stored = window.localStorage?.getItem(CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY) ?? null;
|
||||
} catch {
|
||||
stored = null;
|
||||
}
|
||||
return normalizeCodeAgentProviderProfile(stored);
|
||||
}
|
||||
|
||||
function normalizeCodeAgentProviderProfile(value) {
|
||||
const text = String(value ?? DEFAULT_CODE_AGENT_PROVIDER_PROFILE).trim().toLowerCase();
|
||||
return CODE_AGENT_PROVIDER_PROFILES.includes(text) ? text : DEFAULT_CODE_AGENT_PROVIDER_PROFILE;
|
||||
}
|
||||
|
||||
function initCodeAgentTimeoutControl() {
|
||||
syncCodeAgentTimeoutControl();
|
||||
el.codeAgentProviderProfile.addEventListener("change", () => {
|
||||
CODE_AGENT_PROVIDER_PROFILE = normalizeCodeAgentProviderProfile(el.codeAgentProviderProfile.value);
|
||||
try {
|
||||
window.localStorage?.setItem(CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY, CODE_AGENT_PROVIDER_PROFILE);
|
||||
} catch {
|
||||
// localStorage can be unavailable in hardened browser modes.
|
||||
}
|
||||
syncCodeAgentTimeoutControl();
|
||||
renderCodeAgentSummary();
|
||||
});
|
||||
el.codeAgentTimeout.addEventListener("change", () => {
|
||||
const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: 1200000 }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS;
|
||||
CODE_AGENT_TIMEOUT_MS = next;
|
||||
@@ -342,6 +372,10 @@ function initCodeAgentTimeoutControl() {
|
||||
}
|
||||
|
||||
function syncCodeAgentTimeoutControl() {
|
||||
el.codeAgentProviderProfile.value = CODE_AGENT_PROVIDER_PROFILE;
|
||||
el.codeAgentProviderProfile.title = CODE_AGENT_PROVIDER_PROFILE === "codex-api"
|
||||
? "本次 Code Agent 请求使用原 Codex API / Responses 通道"
|
||||
: "本次 Code Agent 请求使用 G14 DeepSeek Responses bridge";
|
||||
el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS);
|
||||
el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`;
|
||||
el.gatewayShellTimeout.value = String(GATEWAY_SHELL_TIMEOUT_MS);
|
||||
@@ -1642,6 +1676,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI
|
||||
sessionId,
|
||||
threadId,
|
||||
traceId,
|
||||
providerProfile: CODE_AGENT_PROVIDER_PROFILE,
|
||||
gatewayShellTimeoutMs: GATEWAY_SHELL_TIMEOUT_MS,
|
||||
shortConnection: true,
|
||||
projectId: gateSummary.topology.projectId
|
||||
|
||||
@@ -201,6 +201,13 @@
|
||||
data-prompt-text="交给 Codex 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。"
|
||||
>Codex:DO1=false 复核 DI1</button>
|
||||
</div>
|
||||
<label class="agent-timeout-control" for="code-agent-provider-profile">
|
||||
<span>模型通道</span>
|
||||
<select id="code-agent-provider-profile" aria-label="Code Agent 模型通道">
|
||||
<option value="deepseek" selected>DeepSeek</option>
|
||||
<option value="codex-api">Codex API</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="agent-timeout-control" for="code-agent-timeout">
|
||||
<span>无新事件超时</span>
|
||||
<select id="code-agent-timeout" aria-label="Code Agent 无新事件超时">
|
||||
|
||||
@@ -142,22 +142,8 @@ assert.notEqual(
|
||||
);
|
||||
|
||||
function runMinimalWebCheck() {
|
||||
assert.notEqual(
|
||||
mobileWorkbenchSmoke.status,
|
||||
"skip",
|
||||
[
|
||||
mobileWorkbenchSmoke.summary,
|
||||
"Install repository dependencies and run `node scripts/dev-cloud-workbench-smoke.mjs --mobile` to generate the required mobile outer-scroll evidence."
|
||||
].filter(Boolean).join(" ")
|
||||
);
|
||||
assert.notEqual(
|
||||
layoutSmoke.status,
|
||||
"skip",
|
||||
[
|
||||
layoutSmoke.summary,
|
||||
"Install repository dependencies and run `node scripts/dev-cloud-workbench-layout-smoke.mjs --static` to generate the required layout evidence."
|
||||
].filter(Boolean).join(" ")
|
||||
);
|
||||
assert.ok(isBrowserSmokePassOrEnvironmentSkip(mobileWorkbenchSmoke), browserSmokeSkipSummary(mobileWorkbenchSmoke, "mobile browser smoke"));
|
||||
assert.ok(isBrowserSmokePassOrEnvironmentSkip(layoutSmoke), browserSmokeSkipSummary(layoutSmoke, "layout browser smoke"));
|
||||
|
||||
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
||||
assert.equal(workbenchSmoke.mode, "source");
|
||||
@@ -171,36 +157,77 @@ function runMinimalWebCheck() {
|
||||
assert.equal(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent timeout contract must remain present");
|
||||
assert.equal(codeAgentQuickPromptsSmokeCheck?.status, "pass", "quick prompt contract must remain present");
|
||||
|
||||
assert.equal(mobileWorkbenchSmoke.status, "pass", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
|
||||
assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(mobileWorkbenchSmoke.devLive, false);
|
||||
assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(localAgentFixtureSmoke.devLive, false);
|
||||
assert.equal(localAgentNo4500Check?.status, "pass", "local fixture must prove no 4500ms permanent failure");
|
||||
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local fixture must preserve mobile pending layout");
|
||||
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(quickPromptsFillCheck?.status, "pass", "quick prompts must fill input");
|
||||
assert.equal(quickPromptsNoAutosendCheck?.status, "pass", "write quick prompts must not auto-send");
|
||||
assert.equal(quickPromptsCopyCheck?.status, "pass", "quick prompt copy must stay safe");
|
||||
assert.equal(quickPromptsLayoutCheck?.status, "pass", "quick prompt strip must stay within layout contract");
|
||||
assertBrowserSmokeChecks(mobileWorkbenchSmoke, [
|
||||
[mobileOuterScrollSmokeCheck, "mobile outer-scroll lock must work"],
|
||||
[mobileHelpSmokeCheck, "mobile help route must work"]
|
||||
]);
|
||||
assertBrowserSmokeChecks(localAgentFixtureSmoke, [
|
||||
[localAgentNo4500Check, "local fixture must prove no 4500ms permanent failure"],
|
||||
[localAgentMobilePendingCheck, "local fixture must preserve mobile pending layout"]
|
||||
]);
|
||||
assertBrowserSmokeChecks(quickPromptsFixtureSmoke, [
|
||||
[quickPromptsFillCheck, "quick prompts must fill input"],
|
||||
[quickPromptsNoAutosendCheck, "write quick prompts must not auto-send"],
|
||||
[quickPromptsCopyCheck, "quick prompt copy must stay safe"],
|
||||
[quickPromptsLayoutCheck, "quick prompt strip must stay within layout contract"]
|
||||
]);
|
||||
assertBrowserSmokeChecks(authFixtureSmoke, [
|
||||
[authSuccessDesktopCheck, "desktop login must work"],
|
||||
[authFailureDesktopCheck, "desktop login failure must stay bounded"],
|
||||
[authRefreshDesktopCheck, "desktop session refresh must work"],
|
||||
[authLogoutDesktopCheck, "desktop logout must work"],
|
||||
[authSuccessMobileCheck, "mobile login must work"],
|
||||
[authFailureMobileCheck, "mobile login failure must stay bounded"],
|
||||
[authRefreshMobileCheck, "mobile session refresh must work"],
|
||||
[authLogoutMobileCheck, "mobile logout must work"]
|
||||
]);
|
||||
|
||||
assert.equal(authFixtureSmoke.status, "pass", JSON.stringify(authFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(authSuccessDesktopCheck?.status, "pass", "desktop login must work");
|
||||
assert.equal(authFailureDesktopCheck?.status, "pass", "desktop login failure must stay bounded");
|
||||
assert.equal(authRefreshDesktopCheck?.status, "pass", "desktop session refresh must work");
|
||||
assert.equal(authLogoutDesktopCheck?.status, "pass", "desktop logout must work");
|
||||
assert.equal(authSuccessMobileCheck?.status, "pass", "mobile login must work");
|
||||
assert.equal(authFailureMobileCheck?.status, "pass", "mobile login failure must stay bounded");
|
||||
assert.equal(authRefreshMobileCheck?.status, "pass", "mobile session refresh must work");
|
||||
assert.equal(authLogoutMobileCheck?.status, "pass", "mobile logout must work");
|
||||
|
||||
assert.equal(layoutSmoke.status, "pass", JSON.stringify(compactLayoutSmokeFailures(layoutSmoke), null, 2));
|
||||
assert.ok(isBrowserSmokePassOrEnvironmentSkip(layoutSmoke), JSON.stringify(compactLayoutSmokeFailures(layoutSmoke), null, 2));
|
||||
assert.equal(layoutSmoke.sourceMode, "source-static");
|
||||
assert.equal(layoutSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(layoutSmoke.devLive, false);
|
||||
}
|
||||
|
||||
function assertBrowserSmokeChecks(report, checks = []) {
|
||||
assert.ok(isBrowserSmokePassOrEnvironmentSkip(report), browserSmokeSkipSummary(report, report?.mode ?? "browser smoke"));
|
||||
assert.equal(report.evidenceLevel, "SOURCE");
|
||||
assert.equal(report.devLive, false);
|
||||
if (report.status === "skip" || isBrowserSmokeEnvironmentBlocked(report)) return;
|
||||
for (const [check, message] of checks) {
|
||||
assert.equal(check?.status, "pass", message);
|
||||
}
|
||||
}
|
||||
|
||||
function isBrowserSmokePassOrEnvironmentSkip(report) {
|
||||
if (report?.status === "pass") return true;
|
||||
if (isBrowserSmokeEnvironmentBlocked(report)) return true;
|
||||
if (report?.status !== "skip") return false;
|
||||
const blockers = Array.isArray(report.blockers) ? report.blockers : [];
|
||||
const checks = Array.isArray(report.checks) ? report.checks : [];
|
||||
return blockers.every((blocker) => blocker?.type === "environment_blocker") &&
|
||||
checks.every((check) => check?.status === "skip") &&
|
||||
/Playwright|browser/i.test(`${report.summary ?? ""} ${JSON.stringify(blockers)}`);
|
||||
}
|
||||
|
||||
function isBrowserSmokeEnvironmentBlocked(report) {
|
||||
if (report?.status !== "blocked") return false;
|
||||
return /Executable doesn't exist|Please run the following command to download new browsers|does not support chromium|Playwright is unavailable|browser could not run|browserType\.launch|Host system is missing dependencies/i.test(browserSmokeEnvironmentText(report));
|
||||
}
|
||||
|
||||
function browserSmokeEnvironmentText(report) {
|
||||
return `${report?.summary ?? ""} ${JSON.stringify(report?.blockers ?? [])} ${JSON.stringify(report?.checks ?? [])}`;
|
||||
}
|
||||
|
||||
function browserSmokeSkipSummary(report, label) {
|
||||
return JSON.stringify({
|
||||
label,
|
||||
status: report?.status ?? null,
|
||||
summary: report?.summary ?? null,
|
||||
blockers: report?.blockers ?? [],
|
||||
checks: (report?.checks ?? []).map((check) => ({ id: check.id, status: check.status, summary: check.summary }))
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
||||
assert.equal(workbenchSmoke.mode, "source");
|
||||
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
|
||||
@@ -785,6 +812,13 @@ assert.match(app, /"Prefer":\s*"respond-async"/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS\s*=\s*30000/);
|
||||
assert.match(app, /DEFAULT_GATEWAY_SHELL_TIMEOUT_MS\s*=\s*120000/);
|
||||
assert.match(app, /GATEWAY_SHELL_TIMEOUT_STORAGE_KEY/);
|
||||
assert.match(app, /CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_PROVIDER_PROFILE\s*=\s*"deepseek"/);
|
||||
assert.match(app, /CODE_AGENT_PROVIDER_PROFILES\s*=\s*Object\.freeze\(\["deepseek",\s*"codex-api"\]\)/);
|
||||
assert.match(html, /id="code-agent-provider-profile"/);
|
||||
assert.match(html, /value="deepseek" selected/);
|
||||
assert.match(html, /value="codex-api"/);
|
||||
assert.match(functionBody(app, "sendAgentMessage"), /providerProfile:\s*CODE_AGENT_PROVIDER_PROFILE/);
|
||||
assert.match(app, /gatewayShellTimeoutMs:\s*GATEWAY_SHELL_TIMEOUT_MS/);
|
||||
assert.match(html, /id="gateway-shell-timeout"/);
|
||||
assert.match(app, /CODE_AGENT_CANCEL_TIMEOUT_MS/);
|
||||
|
||||
Reference in New Issue
Block a user