Merge pull request #1001 from pikasTech/feat/skill-health-size-check

feat: 增加 skill health 尺寸检查
This commit is contained in:
Lyon
2026-06-26 15:53:59 +08:00
committed by GitHub
22 changed files with 2849 additions and 2280 deletions
+18 -301
View File
@@ -3,315 +3,32 @@ name: unidesk-cicd
description: UniDesk CI/CD 控制面 — `hwlab g14``agentrun` 子命令,覆盖 PR 监控自动合并、Tekton/Argo 控制面、git-mirror、Secret、observability、CI tools image、PipelineRun 清理、AgentRun v0.1 部署和 AgentRun YAML-only lane 部署。用户提到 CI/CD、deploy、rollout、PipelineRun、trigger、git-mirror、control-plane、k3s 部署、agentrun 部署、hwlab g14、monitor-prs、trigger-current 时使用。任何需要把代码变更推送部署到 G14 k3s 的操作都必须走本 skill。
---
# UniDesk HWLAB G14 CI/CD CLI
# UniDesk CI/CD
HWLAB G14 的 PR → CI → CD 控制面和运维入口,统一通过 `bun scripts/cli.ts hwlab g14 ...` 管理
HWLAB G14 和 AgentRun CI/CD 的受控入口。任何 PR 监控、Tekton/Argo、git-mirror、Secret、observability、CI tools image、PipelineRun 清理或 AgentRun 部署都必须走 `bun scripts/cli.ts`
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts hwlab g14 ...`
---
## PR 监控与自动合并
### G14 主线
## 高频入口
```bash
bun scripts/cli.ts hwlab g14 monitor-prs \
[--lane g14|v02] [--once] [--dry-run] \
[--interval-seconds N] [--max-cycles N] [--timeout-seconds N]
```
后台 worker 监控 `pikasTech/HWLAB` 的 open PR → preflight → 自动合并 → 观察 CI/CD 直到 DEV `Synced/Healthy`。成功 rollout 后自动追加指挥简报。状态指针按用途分离(`latest-monitor-job.json` / `latest-once-job.json` 等)。
### v0.2 lane
```bash
bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 [--once] [--dry-run]
```
只监控 base=`v0.2` 的 PR。CD 采用 latest-only:旧 PipelineRun 不取消不等待,stale commit 以 superseded/no-op 收口。合并后在原 PR 下追加语义化状态评论(含起止时间、source commit、PipelineRun、targetValidation、git mirror 状态)。
### v0.3 lane
```bash
bun scripts/cli.ts hwlab g14 monitor-prs --lane v03 [--once] [--dry-run]
```
只监控 base=`v0.3` 的 PR。ready PR 经 UniDesk `gh pr merge` 合并后触发 runtime lane CD,检查 PipelineRun、Argo、`hwlab-v03` runtime public probes 和 Git mirror flush,并对失败 check、冲突、CD failure/timeout 创建或更新 failure issue。public probe 必须使用 `config/hwlab-node-lanes.yaml` 选中 node/lane 的 formal public URLD601 `v0.3` 当前是 `https://hwlab.pikapython.com`,裸 IP、FRP 端口和 legacy `20666/20667` 只作为边缘诊断证据,不能作为用户入口验收口径。
---
## 控制面(Tekton/Argo
### 状态查询
```bash
# 最新 head
bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 --once --dry-run
bun scripts/cli.ts hwlab g14 control-plane status --lane v02
# 定点 PipelineRun
bun scripts/cli.ts hwlab g14 control-plane status \
--lane v02 --pipeline-run hwlab-v02-ci-poll-<short-sha>
# 定点 source commit
bun scripts/cli.ts hwlab g14 control-plane status \
--lane v02 --source-commit <full-sha>
bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm --wait
bun scripts/cli.ts hwlab g14 git-mirror status --lane v02
bun scripts/cli.ts agentrun control-plane status
```
定点 status 输出 `targetValidation.state=passed|superseded`,只检查指定 target 的证据
完整 PR monitor、control-plane、git-mirror、Secret、observability、platform-infra、CI tools image、PipelineRun 清理、rollout 补记和 AgentRun v0.1 部署矩阵见 [references/full.md](references/full.md)
### 手动触发
## P0 边界
```bash
bun scripts/cli.ts hwlab g14 control-plane trigger-current \
--lane v02|v03 [--dry-run|--confirm]
```
- CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 部署必须走受控 CLI;不要用裸 `kubectl``argo``tkn``curl` 当正式控制入口。
- 触发或验收 rollout 时必须绑定 lane、source commit、PipelineRun/GitOps revision 和用户入口验证结果。
- Secret 只通过 YAML sourceRef/targetKey 和受控 CLI 下发;输出只披露 presence/fingerprint。
- 长命令用异步 job 或短轮询;不要长时间挂住 trans/ssh。
`/root/hwlab-v02-cicd.git` 解析当前 `origin/v0.2` full SHA,创建 commit-pinned PipelineRun。confirmed trigger 创建异步 job 并立即返回 `job.id`
## 何时读取 reference
### 应用 RBAC/Pipeline/Argo
```bash
bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 [--dry-run|--confirm]
```
server-side apply v02 的 Tekton RBAC、Pipeline 和 Argo Application。
### D601 节点本地 infra bootstrap
```bash
bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --dry-run
bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra tools-image status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra tools-image build --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image preload --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image logs --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra argo status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane status --node D601 --lane v03 [--pipeline-run <name>|--source-commit <sha>] [--full|--raw]
bun scripts/cli.ts hwlab nodes control-plane trigger-current --node D601 --lane v03 --confirm --wait
bun scripts/cli.ts hwlab nodes control-plane sync --node D601 --lane v03 --confirm
```
`config/hwlab-node-control-plane.yaml` 渲染 D601 HWLAB v03 的节点本地 CI/CD、git-mirror、Tekton、runtime dependency image preload 和 Argo 前置对象。confirmed apply 只做 control-plane bootstrap,不触发 runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。node-local registry 镜像只能作为 tools image 或 runtime dependency 的输出 artifact;输入 base/pull image 必须是 YAML 中声明的公开 registry 来源,缺失 output image 时通过 `status.next.blockers``runtime-image status` 暴露。D601 Argo CD 安装也必须由 YAML 声明:官方 manifest URL、版本、镜像 rewrite/preload、CRD、期望 workload 和 AppProject/Application 都来自 YAML,不能使用手工 kubectl/argo CLI 作为正式安装路径。
`hwlab nodes control-plane status` 默认返回 compact commander summary,只保留 source commit、PipelineRun、Argo、runtime readiness、public probe 和 next action;完整 expected YAML/render target、kubectl result tail、Secret/sourceRef 详情和 probe 原始结果只在 `--full``--raw` 下展开。
`hwlab nodes control-plane sync --confirm` 是 Argo runtime 收敛修复入口:会先按 YAML `runtimeStore.postgres.mode=local-k3s` 同步本地 postgres bootstrap Secret,再终止卡住的 running Argo operation、删除失败 hook Job,并在 StatefulSet template 已更新但旧 controller-revision pod 因 `ImagePullBackOff` / `ErrImagePull` / `CrashLoopBackOff` 卡住时受控删除该旧 pod,让 StatefulSet 按最新 revision 重建。不要手工裸删 pod;需要解除这类死锁时走该入口。
`hwlab nodes control-plane trigger-current --node <node> --lane <lane> --confirm --wait` 是 node/lane CI/CD 一键入口:按 YAML 解析 source head,执行 git-mirror pre-sync/pre-flush,刷新 control-plane,创建或复用 commit-pinned PipelineRun,等待 PipelineRun 终态,并在终态成功后执行 post-flush。默认输出必须是低噪声 CICD 表格摘要;完整 JSON 只能通过 `--full``--raw` 展开。120 秒是严重超时阈值:PipelineRun wait 或 `trigger-current` total elapsed 超过 120 秒时,即使最终 status=ok/completed,也必须输出并在 closeout 中记录 `node-runtime-trigger-over-120s` warning、total elapsed、pipeline wait、git mirror status,并从 env-reuse 和 git-mirror/control-plane path 着手排查;未到终态时 CLI 返回 `pending` warning,不继续长时间阻塞,也不把仍在运行误报为构建失败。小范围 PR 触发 120s 时必须看 `plan-artifacts``affectedServices/buildServices/reusedServices`:如果 source diff 很小却出现所有 envreuse 服务都在 `buildServices``reusedServices=[]`,优先怀疑 current GitOps artifact catalog 没有 hydrate 到 source plan 阶段,而不是继续盲目重跑 PipelineRun。
### G14 v0.3 runtime base image
```bash
bun scripts/cli.ts hwlab nodes control-plane runtime-image status --node G14 --lane v03
bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node G14 --lane v03 --confirm
```
G14 v0.3 的 Tekton/BuildKit base image 也走 `config/hwlab-node-lanes.yaml``baseImageSource` 是公开来源,`baseImage` 是 node-local registry 目标。缺失 base image 时先用 `runtime-image status` 判断 `registryTagPresent`,再用 `preload --confirm` seed;不要手工 `docker tag/push``trigger-current` 后若 PipelineRun 已越过 base image 阶段但卡在某个 service build task,按 TaskRun 单独提 issue/修复,不把它并回 base-image preload 问题。长期边界见 `docs/reference/g14.md`
D601/v03 env-reuse service build task 失败时,先看 `build-<service>` TaskRun 的 `step-publish` 日志;Debian apt、npm、Go module 等外部依赖下载通过 lane YAML 注入 egress proxy 后可能出现 502、reset 或超时。先用 `platform-infra sub2api status|validate` 区分 proxy 整体故障和单个上游 transientproxy 健康但单次下载 transient 时可以受控 `trigger-current --rerun`,重复失败应修 HWLAB `scripts/artifact-publish.mjs` / envRecipe 的有限 retry 后合并发布,不手工 patch pod 或裸删 PipelineRun。若 Pod 内 unset `HTTP_PROXY/HTTPS_PROXY/ALL_PROXY` 后外部 registry/DNS 不可达,说明该 lane 的外部依赖下载依赖 egress proxy;此时 npm/Bun retry 只能降噪,根因仍是 proxy upstream 或 catalog/plan 误触发的过量 build。
---
## Git Mirror
```bash
bun scripts/cli.ts hwlab g14 git-mirror status
bun scripts/cli.ts hwlab g14 git-mirror apply [--dry-run|--confirm]
bun scripts/cli.ts hwlab g14 git-mirror sync [--dry-run|--confirm]
bun scripts/cli.ts hwlab g14 git-mirror flush [--dry-run|--confirm]
# D601 node-local v0.3 lane
bun scripts/cli.ts hwlab nodes git-mirror status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait
bun scripts/cli.ts hwlab nodes git-mirror flush --node D601 --lane v03 --confirm --wait
```
- `apply`: 渲染并 apply `devops-infra/git-mirror.yaml`
- `sync`: 把当前配置声明的 GitHub refs 拉入本地 mirror
- `flush`: 把本地 lane GitOps ref 快进推回 GitHub
PipelineRun `gitops-promote` 如果报 git mirror 控制面漂移、refs 不一致或 flush/publish 未完成,优先按当前 `devops-infra/git-mirror.yaml` 收敛:先 `git-mirror apply --confirm`,再 `git-mirror sync --confirm --wait`,然后用 `control-plane cleanup-runs --pipeline-run <failed-run> --confirm` 受控清理失败 PipelineRun 后重试。旧 branch/path allowlist gate 已删除,不要恢复旧 hook、直接 `kubectl delete`、手工 patch pod 内 hook 或绕过 `flush`
手动 trigger closeout 不能只看 PipelineRun `Completed`。必须继续查 `control-plane status --pipeline-run <name>``git-mirror status`node-scoped `trigger-current --confirm --wait` 会自动做必要的 mirror pre/post flush,但 closeout 仍要确认最终 `pendingFlush=false``githubInSync=true`。如果 lower-level 手工路径或旧 job 留下 `pendingFlush=true`,执行 `git-mirror flush --confirm --wait``githubInSync=true`
node-scoped lane 可能在本次 PR 合并后又被后续 PR 推进。`control-plane status --pipeline-run <name>` 是定点观察某个 PipelineRun,但输出里的当前 `sourceHead` / `summary.sourceCommit` 可能已经是最新 branch tip,而不是该 PipelineRun 名称对应的 merge commit。closeout 必须同时记录 PR merge commit、PipelineRun 名称/状态、Argo sync revision、当前 branch tip,并用 `git merge-base --is-ancestor <merge-commit> HEAD` 或等价证据说明最新 tip 包含本次 PR;不要只凭当前 source head 判断本次 rollout。
`trigger-current --node D601 --lane v03 --confirm --wait` 可能先解析到一个 source commit,随后在 control-plane apply 的 `source-render` / `local-git-clone-worktree` 阶段因 `v0.3` 被并行 PR 推进而 clone 到更新 head,触发 `rev-parse HEAD == source_commit` 校验失败。这不是业务代码构建失败;先执行 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`,再快进固定 worktree,确认原 PR merge commit 是最新 `origin/v0.3` 的祖先,然后按最新 branch tip 重新运行 `trigger-current`。收口时同时记录原 PR merge commit、最新 source head 和 ancestor 证据。
D601 `v0.3` 固定 worktree 的 fetch remote 是 node-local git mirror。GitHub PR 合并后,如果 `D601:/home/ubuntu/workspace/hwlab-v03``git fetch origin v0.3` 仍看不到最新 merge commit,先执行 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`,再在固定 worktree `git fetch origin v0.3 && git pull --ff-only origin v0.3`。D601/v03 `git-mirror` 的 GitHub upstream 标准传输固定为 YAML 声明的 SSH:`githubTransport.mode=ssh`,脚本通过 `GIT_SSH` wrapper 访问 `ssh://git@ssh.github.com:443/...`node-global HTTP proxy 只作为 SSH CONNECT tunnel,不是 GitHub HTTPS auth/token transport。若 CLI 输出 `transport=https``GITHUB_TOKEN``git-mirror-github-token` 或 HTTPS token sourceRef,按 control-plane drift/配置回归处理:先修 `config/hwlab-node-control-plane.yaml` 并执行 `hwlab nodes control-plane apply --node D601 --lane v03 --confirm`,不要改走 HTTPS、不要增加 fallback、不要用 host workspace repair。`sync/flush` 的 retry 只消费 SSH upstream transient,并在耗尽后输出 stopped/exhausted`trigger-current --lane v03` 会为 PipelineRun 做 mirror pre-sync,但不替代固定 worktree 的 fetch hygiene。promotion 后若 node-local `git-mirror status` 显示 `pendingFlush=true`,执行 node-local flush 并等到 `pendingFlush=false``githubInSync=true`
D601/node-scoped mirror status 的 `githubGitops` 来自本地 mirror cache 的 `refs/mirror-stage/...``status` 输出应通过 `refSources.githubFieldsAreMirrorStageCache=true` 显示这一点。`hwlab nodes git-mirror flush --node D601 --lane v03 --confirm --wait` 如果已经显示 `v0.3-gitops -> v0.3-gitops` 推送成功,但随后因 GitHub SSH `kex_exchange_identification` 或 fetch 确认失败导致命令非零退出,会标记 `partialSuccess=push-succeeded-fetch-failed`。当前 CLI 会自动做一次受控 sync/recheck;恢复后输出 `partialSuccessRecovered=true``postPushRecovery` 且整体 `ok=true`,未恢复时才把下一步指向 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`。不要连续盲目 flush;先刷新 mirror-stage,再用 status 确认 `localGitops=githubGitops``pendingFlush=false``githubInSync=true`
---
## Secret 管理
```bash
# 查看
bun scripts/cli.ts hwlab g14 secret status --lane v02 \
--name hwlab-v02-openfga|hwlab-v02-master-server-admin-api-key
# 确保
bun scripts/cli.ts hwlab g14 secret ensure --lane v02 \
--name hwlab-v02-master-server-admin-api-key [--dry-run|--confirm]
# 删除废弃 Secret
bun scripts/cli.ts hwlab g14 secret delete --lane v02 \
--name <obsolete-secret> [--dry-run|--confirm]
```
---
## 运行时迁移
```bash
bun scripts/cli.ts hwlab g14 control-plane runtime-migration \
--lane v02 [--dry-run|--confirm]
```
通过 `deployment/hwlab-cloud-api` 容器内 migration CLI 执行。
---
## Observability
```bash
bun scripts/cli.ts hwlab g14 observability status|apply|query|targets|boundary|closeout \
[--lane v02] [--promql <expr>] [--expect-count N] [--expect-value V] [--dry-run|--confirm]
```
管理 G14 Prometheus 基础设施和 HWLAB v0.2 监控 closeout。
---
## Platform Infra
```bash
bun scripts/cli.ts platform-infra sub2api plan|apply|status|validate
bun scripts/cli.ts platform-infra sub2api codex-pool plan|sync|validate|expose|configure-local
bun scripts/cli.ts platform-infra wechat-archive plan|apply|status|validate|pull
bun scripts/cli.ts platform-infra wechat-archive wcf-host-status|collector-plan|collector-apply|collector-status
```
- `platform-infra` 是 G14 k3s 上 UniDesk 运维的平台基础设施 namespace;新增平台服务优先进入该 namespace,旧 `devops-infra` 只作为渐进迁移来源。
- Sub2API 的日常部署、Codex pool、FRP 暴露、master `~/.codex` 配置、验收和排障统一使用 `$unidesk-sub2api`UniDesk 仓库 `.agents/skills/unidesk-sub2api/SKILL.md`)。
- WeChat archive 是 platform-infra 的 YAML-first 工作流入口;D601 personal WeChat upstream 必须复用既有 D601 `platform-infra` namespace`createNamespace=false`,只读 collector 的副本、镜像、WCF host、端口和版本 pin 都以 `config/platform-infra/wechat-archive.yaml` 为准。
- 如果 WeChatFerry 配套的 PC 微信版本被微信服务端拒绝登录,按上游兼容阻塞处理:把 collector 的 YAML 副本数调为 `0` 并通过 `collector-apply --confirm --wait` 同步,保留 Secret/ConfigMap/PVC 和 Windows 准备态;不要手工 `kubectl scale`、新建 namespace 或采用版本检查绕过工具作为长期路径。
- UniDesk 仓库 `docs/reference/platform-infra.md` 只保留开发边界、YAML-first 真相和探针口径,不重复日常操作手册。
---
## CI Tools Image
```bash
bun scripts/cli.ts hwlab g14 tools-image status
bun scripts/cli.ts hwlab g14 tools-image build \
--name ci-node-tools --tag <tag> \
[--dockerfile deploy/ci/hwlab-ci-node-tools.Dockerfile] [--dry-run|--confirm]
```
在 G14 host 构建并 push 到本地 registry。
---
## PipelineRun 清理
```bash
# 清理已完成 PipelineRun
bun scripts/cli.ts hwlab g14 control-plane cleanup-runs \
--lane v02|g14|all [--min-age-minutes N] [--limit N] [--dry-run|--confirm]
# D601/G14 node-scoped runtime lane retention
bun scripts/cli.ts hwlab nodes control-plane cleanup-runs \
--node D601 --lane v03 [--min-age-minutes N] [--limit N] [--dry-run|--confirm --wait]
# 补充清理 Released PV
bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs \
--lane all [--limit N] [--dry-run|--confirm]
```
---
## 手动补记 rollout
```bash
bun scripts/cli.ts hwlab g14 record-rollout --pr <number> --source-commit <sha>
```
手动补记 CI/CD 耗时、TaskRun 指标和语义化 changelog 到指挥简报。
---
## AgentRun 控制面
YAML-only lane 以 `config/agentrun.yaml` 为部署真相;node/lane、source workspace/branch、image build、GitOps branch/path、runtime namespace、Secret、外置数据库、manager env、git-mirror 和 edge 暴露都从 YAML 进入 CLI。AgentRun service repo 的 `deploy/deploy.json` 不能作为 UniDesk deployment truth;新 lane 不再维护该文件。
```bash
bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02
bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 [--force-active] [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 [--pipeline-run <name>|--source-commit <sha>] [--full|--raw]
```
- `plan`: 只读解析 YAML,输出控制面、source、image build、GitOps、runtime 和 Secret plan,不打印 Secret value
- `apply`: 按 YAML 渲染并 apply Tekton RBAC/Pipeline、Argo AppProject/Application 和 runtime namespace
- `secret-sync`: 按 YAML 的 Secret sourceRef/keyMapping 同步 runtime Secret 和外置 DB Secret,只输出 fingerprint
- `restart`: patch manager Deployment 的 restart annotation 并等待 rollout,用于 Secret export/DB 连接串变化后让 workload 读取新 Secret;不要手工删除 Pod
- `trigger-current`: 确保 source branch/workspace,删除新 lane source branch 的 `deploy/deploy.json`,构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalog,触发 git-mirror sync 和 provenance PipelineRunconfirmed 运行可返回异步 job,必须用 `job status <jobId> --tail-bytes 12000``agentrun-yaml-lane-trigger` progress,再用 `status --pipeline-run <name>` 轮询收口;confirmed 收尾会尝试恢复 fixed source workspace 到 lane branch,恢复失败只作为 warning 披露
- `cleanup-runners`: 只清 YAML 选中 lane runtime namespace 中匹配 `deployment.runner.retention.selectors` 的 runner Job/Podrunner 上限、最后活跃排序、active heartbeat 窗口、age-based cleanup 开关和 selector 都以 YAML 为准。`dry-run` 必须先看 manager facts、inactive candidates、selected 和 `activeRunRisk`;普通 `--confirm` 只删除 selected inactive runner,不替代 CI PipelineRun 清理。`--force-active` 只用于 operator 明确决定强杀所有匹配 runner pod/job 的资源恢复场景,必须先 dry-run 确认 `criteria.forceActive=true` 和 selection 范围;它会中断 active run/command/session,但仍优先于裸 `kubectl delete pod/job`
- `status`: 默认返回 compact commander JSON,关键结论在 `.data.summary``.data.alignment`,完整 YAML target、原始 source/runtime/gitMirror payload 和成功 probe tail 只在 `--full|--raw` 展开
YAML-only lane 的长步骤必须由 CLI 拆成短提交和状态轮询:source bootstrap、image build、GitOps publish、git-mirror sync 和 PipelineRun 创建不得塞进一个顶层 `trans` 长连接。GitOps publish 必须使用隔离临时 clone/worktree,不能切换或污染 YAML 声明的固定 source workspace;如果历史失败发布留下 dirty/detached/GitOps branch 状态,只清理已知发布残留并恢复到 lane source branch 后再重试。后台步骤的 `status``ok` 要共同判定,`status=succeeded``ok=false` 是终态失败,不继续轮询到超时。
AgentRun YAML-only lane closeout 必须同时看当前 source branch tip、目标 PipelineRun、GitOps revision、Argo revision 和 manager source commit。发布过程中如果 source branch 被并行 PR 推进,`status --pipeline-run <name>` 会通过 `summary.branchDrift` / `alignment.branchDrift` 标记目标 PipelineRun 是否已被当前 branch tip supersede;先确认最新 tip 包含本次修复,再按最新 tip 重新 `trigger-current`。最终只用最新 PipelineRun 的 `status``aligned=true``blockers=[]``argoSyncedToGitops=true``managerSourceMatchesExpected=true` 收口。`trigger-current` 会尝试恢复 YAML 声明的固定 source workspace 到 lane branch;若返回 `source-worktree-restore-failed` warning、workspace dirty,或后续 `status` 仍显示 `summary.source.workspaceDetached=true`,先按 `sourceWorkspaceRestore.failureKind` 修复 fixed workspace,再继续下一轮开发或部署。
Runner egress proxy 只从 `config/agentrun.yaml``deployment.runner.egressProxyUrl``deployment.runner.noProxyExtra` 进入部署;manager Deployment 必须带 `AGENTRUN_RUNNER_EGRESS_PROXY_URL``AGENTRUN_RUNNER_NO_PROXY_EXTRA`,验收时还要用真实 `create/apply/send` 触发 runner Job,检查 Pod env、event/trace 和 final response。GitOps 已更新但 Argo 仍在旧 revision 时,走 `agentrun control-plane refresh --node <node> --lane <lane> --confirm`,不要手工 patch runtime。
Runner 持久化、idle timeout 和 runner retention 只从 `config/agentrun.yaml``deployment.runner.*` / `deployment.runner.retention` 进入部署,不在 HWLAB 仓库放运维 YAML。验收不能只看 manager Deployment/Pod env;必须用 HWLAB/AgentRun 原入口创建新 turn 或 runner Job,并检查新 runner Job env、session PVC、`AGENTRUN_SOURCE_COMMIT` 和 trace/result 是否复用同一 run 且没有 `reuse-blocked`runner retention closeout 还要用 `cleanup-runners --dry-run` 证明 over-limit selection 不触碰 active runner。
Provider credential 的 `config.toml` 变更同样走 YAML `sourceRef``secret-sync``restart`lane config 只声明该 lane 需要的 Codex CLI runtime options。不要复制指挥机全局 `~/.codex/config.toml` 作为长期事实,也不要在没有同 lane `auth.json` / API key source 验证的情况下覆盖 provider endpoint。
### AgentRun v0.1 兼容入口
```bash
bun scripts/cli.ts agentrun control-plane status \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane trigger-current \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane refresh \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-runs \
[--min-age-minutes N] [--limit N] [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-released-pvs \
[--limit N] [--dry-run|--confirm]
```
- `status`: 只读汇总 source commit、PipelineRun、Argo、manager image、git mirror 和 `aligned` 结论
- `trigger-current`: 快进 `G14:/root/agentrun-v01` → mirror sync → 创建 `agentrun-v01-ci-<short12>` PipelineRun
- `refresh`: Argo hard refresh(不 patch runtime workload
- `cleanup-runs`: 只清理 `agentrun-ci` 中已完成 PipelineRun + 临时 PVC;不清理 `agentrun-v01` runtime runner Job/Pod/Secret
- `cleanup-released-pvs`: 回收 Released PV
AgentRun `control-plane status` 的 compact JSON 关键字段在 `.data.summary.sourceCommit``.data.summary.expectedPipelineRun``.data.summary.runtimeAlignment``.data.summary.gitMirror``.data.summary.ci.pipelineRun``.data.summary.argo``.data.alignment`,不要假设存在 `.data.status`。触发部署后如果 GitOps 已 promotion 但 git mirror `pendingFlush=true`,先执行 `bun scripts/cli.ts agentrun git-mirror flush --confirm --wait`,再 `control-plane refresh --confirm`,最后用 `control-plane status --full` 证明 `.data.summary.runtimeAlignment.argoSyncedToGitops=true``.data.summary.runtimeAlignment.managerSourceMatchesExpected=true``.data.summary.ci.pipelineRun.status=True`
## AgentRun v0.1 Git Mirror
```bash
bun scripts/cli.ts agentrun git-mirror status [--full|--raw]
bun scripts/cli.ts agentrun git-mirror sync [--dry-run|--confirm] [--wait]
bun scripts/cli.ts agentrun git-mirror flush [--dry-run|--confirm] [--wait]
```
- `status`: 返回 `localV01`/`githubV01`/`localGitops`/`githubGitops`/`pendingFlush`/`githubInSync`
- `sync`: 拉取 GitHub `v0.1` + `v0.1-gitops` refs
- `flush`: 推送本地 `v0.1-gitops` → GitHub
与 HWLAB v0.2 mirror 共用 `devops-infra` 服务和 cache PVC。
- PR 自动合并、v0.2/v0.3 lane 差异:读 [references/full.md](references/full.md) 的 PR 监控段。
- 手动触发、定点 PipelineRun/source commit、RBAC/Pipeline/Argo:读控制面段。
- git-mirror、Secret、observability、CI tools image、PipelineRun/PV 清理:读对应段。
- AgentRun v0.1 或 YAML-only lane 部署:读 AgentRun 控制面段。
@@ -0,0 +1,317 @@
---
name: unidesk-cicd
description: UniDesk CI/CD 控制面 — `hwlab g14``agentrun` 子命令,覆盖 PR 监控自动合并、Tekton/Argo 控制面、git-mirror、Secret、observability、CI tools image、PipelineRun 清理、AgentRun v0.1 部署和 AgentRun YAML-only lane 部署。用户提到 CI/CD、deploy、rollout、PipelineRun、trigger、git-mirror、control-plane、k3s 部署、agentrun 部署、hwlab g14、monitor-prs、trigger-current 时使用。任何需要把代码变更推送部署到 G14 k3s 的操作都必须走本 skill。
---
# UniDesk HWLAB G14 CI/CD CLI
HWLAB G14 的 PR → CI → CD 控制面和运维入口,统一通过 `bun scripts/cli.ts hwlab g14 ...` 管理。
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts hwlab g14 ...`
---
## PR 监控与自动合并
### G14 主线
```bash
bun scripts/cli.ts hwlab g14 monitor-prs \
[--lane g14|v02] [--once] [--dry-run] \
[--interval-seconds N] [--max-cycles N] [--timeout-seconds N]
```
后台 worker 监控 `pikasTech/HWLAB` 的 open PR → preflight → 自动合并 → 观察 CI/CD 直到 DEV `Synced/Healthy`。成功 rollout 后自动追加指挥简报。状态指针按用途分离(`latest-monitor-job.json` / `latest-once-job.json` 等)。
### v0.2 lane
```bash
bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 [--once] [--dry-run]
```
只监控 base=`v0.2` 的 PR。CD 采用 latest-only:旧 PipelineRun 不取消不等待,stale commit 以 superseded/no-op 收口。合并后在原 PR 下追加语义化状态评论(含起止时间、source commit、PipelineRun、targetValidation、git mirror 状态)。
### v0.3 lane
```bash
bun scripts/cli.ts hwlab g14 monitor-prs --lane v03 [--once] [--dry-run]
```
只监控 base=`v0.3` 的 PR。ready PR 经 UniDesk `gh pr merge` 合并后触发 runtime lane CD,检查 PipelineRun、Argo、`hwlab-v03` runtime public probes 和 Git mirror flush,并对失败 check、冲突、CD failure/timeout 创建或更新 failure issue。public probe 必须使用 `config/hwlab-node-lanes.yaml` 选中 node/lane 的 formal public URLD601 `v0.3` 当前是 `https://hwlab.pikapython.com`,裸 IP、FRP 端口和 legacy `20666/20667` 只作为边缘诊断证据,不能作为用户入口验收口径。
---
## 控制面(Tekton/Argo
### 状态查询
```bash
# 最新 head
bun scripts/cli.ts hwlab g14 control-plane status --lane v02
# 定点 PipelineRun
bun scripts/cli.ts hwlab g14 control-plane status \
--lane v02 --pipeline-run hwlab-v02-ci-poll-<short-sha>
# 定点 source commit
bun scripts/cli.ts hwlab g14 control-plane status \
--lane v02 --source-commit <full-sha>
```
定点 status 输出 `targetValidation.state=passed|superseded`,只检查指定 target 的证据。
### 手动触发
```bash
bun scripts/cli.ts hwlab g14 control-plane trigger-current \
--lane v02|v03 [--dry-run|--confirm]
```
`/root/hwlab-v02-cicd.git` 解析当前 `origin/v0.2` full SHA,创建 commit-pinned PipelineRun。confirmed trigger 创建异步 job 并立即返回 `job.id`
### 应用 RBAC/Pipeline/Argo
```bash
bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 [--dry-run|--confirm]
```
server-side apply v02 的 Tekton RBAC、Pipeline 和 Argo Application。
### D601 节点本地 infra bootstrap
```bash
bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --dry-run
bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra tools-image status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra tools-image build --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image preload --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane infra runtime-image logs --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra argo status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --confirm
bun scripts/cli.ts hwlab nodes control-plane status --node D601 --lane v03 [--pipeline-run <name>|--source-commit <sha>] [--full|--raw]
bun scripts/cli.ts hwlab nodes control-plane trigger-current --node D601 --lane v03 --confirm --wait
bun scripts/cli.ts hwlab nodes control-plane sync --node D601 --lane v03 --confirm
```
`config/hwlab-node-control-plane.yaml` 渲染 D601 HWLAB v03 的节点本地 CI/CD、git-mirror、Tekton、runtime dependency image preload 和 Argo 前置对象。confirmed apply 只做 control-plane bootstrap,不触发 runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。node-local registry 镜像只能作为 tools image 或 runtime dependency 的输出 artifact;输入 base/pull image 必须是 YAML 中声明的公开 registry 来源,缺失 output image 时通过 `status.next.blockers``runtime-image status` 暴露。D601 Argo CD 安装也必须由 YAML 声明:官方 manifest URL、版本、镜像 rewrite/preload、CRD、期望 workload 和 AppProject/Application 都来自 YAML,不能使用手工 kubectl/argo CLI 作为正式安装路径。
`hwlab nodes control-plane status` 默认返回 compact commander summary,只保留 source commit、PipelineRun、Argo、runtime readiness、public probe 和 next action;完整 expected YAML/render target、kubectl result tail、Secret/sourceRef 详情和 probe 原始结果只在 `--full``--raw` 下展开。
`hwlab nodes control-plane sync --confirm` 是 Argo runtime 收敛修复入口:会先按 YAML `runtimeStore.postgres.mode=local-k3s` 同步本地 postgres bootstrap Secret,再终止卡住的 running Argo operation、删除失败 hook Job,并在 StatefulSet template 已更新但旧 controller-revision pod 因 `ImagePullBackOff` / `ErrImagePull` / `CrashLoopBackOff` 卡住时受控删除该旧 pod,让 StatefulSet 按最新 revision 重建。不要手工裸删 pod;需要解除这类死锁时走该入口。
`hwlab nodes control-plane trigger-current --node <node> --lane <lane> --confirm --wait` 是 node/lane CI/CD 一键入口:按 YAML 解析 source head,执行 git-mirror pre-sync/pre-flush,刷新 control-plane,创建或复用 commit-pinned PipelineRun,等待 PipelineRun 终态,并在终态成功后执行 post-flush。默认输出必须是低噪声 CICD 表格摘要;完整 JSON 只能通过 `--full``--raw` 展开。120 秒是严重超时阈值:PipelineRun wait 或 `trigger-current` total elapsed 超过 120 秒时,即使最终 status=ok/completed,也必须输出并在 closeout 中记录 `node-runtime-trigger-over-120s` warning、total elapsed、pipeline wait、git mirror status,并从 env-reuse 和 git-mirror/control-plane path 着手排查;未到终态时 CLI 返回 `pending` warning,不继续长时间阻塞,也不把仍在运行误报为构建失败。小范围 PR 触发 120s 时必须看 `plan-artifacts``affectedServices/buildServices/reusedServices`:如果 source diff 很小却出现所有 envreuse 服务都在 `buildServices``reusedServices=[]`,优先怀疑 current GitOps artifact catalog 没有 hydrate 到 source plan 阶段,而不是继续盲目重跑 PipelineRun。
### G14 v0.3 runtime base image
```bash
bun scripts/cli.ts hwlab nodes control-plane runtime-image status --node G14 --lane v03
bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node G14 --lane v03 --confirm
```
G14 v0.3 的 Tekton/BuildKit base image 也走 `config/hwlab-node-lanes.yaml``baseImageSource` 是公开来源,`baseImage` 是 node-local registry 目标。缺失 base image 时先用 `runtime-image status` 判断 `registryTagPresent`,再用 `preload --confirm` seed;不要手工 `docker tag/push``trigger-current` 后若 PipelineRun 已越过 base image 阶段但卡在某个 service build task,按 TaskRun 单独提 issue/修复,不把它并回 base-image preload 问题。长期边界见 `docs/reference/g14.md`
D601/v03 env-reuse service build task 失败时,先看 `build-<service>` TaskRun 的 `step-publish` 日志;Debian apt、npm、Go module 等外部依赖下载通过 lane YAML 注入 egress proxy 后可能出现 502、reset 或超时。先用 `platform-infra sub2api status|validate` 区分 proxy 整体故障和单个上游 transientproxy 健康但单次下载 transient 时可以受控 `trigger-current --rerun`,重复失败应修 HWLAB `scripts/artifact-publish.mjs` / envRecipe 的有限 retry 后合并发布,不手工 patch pod 或裸删 PipelineRun。若 Pod 内 unset `HTTP_PROXY/HTTPS_PROXY/ALL_PROXY` 后外部 registry/DNS 不可达,说明该 lane 的外部依赖下载依赖 egress proxy;此时 npm/Bun retry 只能降噪,根因仍是 proxy upstream 或 catalog/plan 误触发的过量 build。
---
## Git Mirror
```bash
bun scripts/cli.ts hwlab g14 git-mirror status
bun scripts/cli.ts hwlab g14 git-mirror apply [--dry-run|--confirm]
bun scripts/cli.ts hwlab g14 git-mirror sync [--dry-run|--confirm]
bun scripts/cli.ts hwlab g14 git-mirror flush [--dry-run|--confirm]
# D601 node-local v0.3 lane
bun scripts/cli.ts hwlab nodes git-mirror status --node D601 --lane v03
bun scripts/cli.ts hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait
bun scripts/cli.ts hwlab nodes git-mirror flush --node D601 --lane v03 --confirm --wait
```
- `apply`: 渲染并 apply `devops-infra/git-mirror.yaml`
- `sync`: 把当前配置声明的 GitHub refs 拉入本地 mirror
- `flush`: 把本地 lane GitOps ref 快进推回 GitHub
PipelineRun `gitops-promote` 如果报 git mirror 控制面漂移、refs 不一致或 flush/publish 未完成,优先按当前 `devops-infra/git-mirror.yaml` 收敛:先 `git-mirror apply --confirm`,再 `git-mirror sync --confirm --wait`,然后用 `control-plane cleanup-runs --pipeline-run <failed-run> --confirm` 受控清理失败 PipelineRun 后重试。旧 branch/path allowlist gate 已删除,不要恢复旧 hook、直接 `kubectl delete`、手工 patch pod 内 hook 或绕过 `flush`
手动 trigger closeout 不能只看 PipelineRun `Completed`。必须继续查 `control-plane status --pipeline-run <name>``git-mirror status`node-scoped `trigger-current --confirm --wait` 会自动做必要的 mirror pre/post flush,但 closeout 仍要确认最终 `pendingFlush=false``githubInSync=true`。如果 lower-level 手工路径或旧 job 留下 `pendingFlush=true`,执行 `git-mirror flush --confirm --wait``githubInSync=true`
node-scoped lane 可能在本次 PR 合并后又被后续 PR 推进。`control-plane status --pipeline-run <name>` 是定点观察某个 PipelineRun,但输出里的当前 `sourceHead` / `summary.sourceCommit` 可能已经是最新 branch tip,而不是该 PipelineRun 名称对应的 merge commit。closeout 必须同时记录 PR merge commit、PipelineRun 名称/状态、Argo sync revision、当前 branch tip,并用 `git merge-base --is-ancestor <merge-commit> HEAD` 或等价证据说明最新 tip 包含本次 PR;不要只凭当前 source head 判断本次 rollout。
`trigger-current --node D601 --lane v03 --confirm --wait` 可能先解析到一个 source commit,随后在 control-plane apply 的 `source-render` / `local-git-clone-worktree` 阶段因 `v0.3` 被并行 PR 推进而 clone 到更新 head,触发 `rev-parse HEAD == source_commit` 校验失败。这不是业务代码构建失败;先执行 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`,再快进固定 worktree,确认原 PR merge commit 是最新 `origin/v0.3` 的祖先,然后按最新 branch tip 重新运行 `trigger-current`。收口时同时记录原 PR merge commit、最新 source head 和 ancestor 证据。
D601 `v0.3` 固定 worktree 的 fetch remote 是 node-local git mirror。GitHub PR 合并后,如果 `D601:/home/ubuntu/workspace/hwlab-v03``git fetch origin v0.3` 仍看不到最新 merge commit,先执行 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`,再在固定 worktree `git fetch origin v0.3 && git pull --ff-only origin v0.3`。D601/v03 `git-mirror` 的 GitHub upstream 标准传输固定为 YAML 声明的 SSH:`githubTransport.mode=ssh`,脚本通过 `GIT_SSH` wrapper 访问 `ssh://git@ssh.github.com:443/...`node-global HTTP proxy 只作为 SSH CONNECT tunnel,不是 GitHub HTTPS auth/token transport。若 CLI 输出 `transport=https``GITHUB_TOKEN``git-mirror-github-token` 或 HTTPS token sourceRef,按 control-plane drift/配置回归处理:先修 `config/hwlab-node-control-plane.yaml` 并执行 `hwlab nodes control-plane apply --node D601 --lane v03 --confirm`,不要改走 HTTPS、不要增加 fallback、不要用 host workspace repair。`sync/flush` 的 retry 只消费 SSH upstream transient,并在耗尽后输出 stopped/exhausted`trigger-current --lane v03` 会为 PipelineRun 做 mirror pre-sync,但不替代固定 worktree 的 fetch hygiene。promotion 后若 node-local `git-mirror status` 显示 `pendingFlush=true`,执行 node-local flush 并等到 `pendingFlush=false``githubInSync=true`
D601/node-scoped mirror status 的 `githubGitops` 来自本地 mirror cache 的 `refs/mirror-stage/...``status` 输出应通过 `refSources.githubFieldsAreMirrorStageCache=true` 显示这一点。`hwlab nodes git-mirror flush --node D601 --lane v03 --confirm --wait` 如果已经显示 `v0.3-gitops -> v0.3-gitops` 推送成功,但随后因 GitHub SSH `kex_exchange_identification` 或 fetch 确认失败导致命令非零退出,会标记 `partialSuccess=push-succeeded-fetch-failed`。当前 CLI 会自动做一次受控 sync/recheck;恢复后输出 `partialSuccessRecovered=true``postPushRecovery` 且整体 `ok=true`,未恢复时才把下一步指向 `hwlab nodes git-mirror sync --node D601 --lane v03 --confirm --wait`。不要连续盲目 flush;先刷新 mirror-stage,再用 status 确认 `localGitops=githubGitops``pendingFlush=false``githubInSync=true`
---
## Secret 管理
```bash
# 查看
bun scripts/cli.ts hwlab g14 secret status --lane v02 \
--name hwlab-v02-openfga|hwlab-v02-master-server-admin-api-key
# 确保
bun scripts/cli.ts hwlab g14 secret ensure --lane v02 \
--name hwlab-v02-master-server-admin-api-key [--dry-run|--confirm]
# 删除废弃 Secret
bun scripts/cli.ts hwlab g14 secret delete --lane v02 \
--name <obsolete-secret> [--dry-run|--confirm]
```
---
## 运行时迁移
```bash
bun scripts/cli.ts hwlab g14 control-plane runtime-migration \
--lane v02 [--dry-run|--confirm]
```
通过 `deployment/hwlab-cloud-api` 容器内 migration CLI 执行。
---
## Observability
```bash
bun scripts/cli.ts hwlab g14 observability status|apply|query|targets|boundary|closeout \
[--lane v02] [--promql <expr>] [--expect-count N] [--expect-value V] [--dry-run|--confirm]
```
管理 G14 Prometheus 基础设施和 HWLAB v0.2 监控 closeout。
---
## Platform Infra
```bash
bun scripts/cli.ts platform-infra sub2api plan|apply|status|validate
bun scripts/cli.ts platform-infra sub2api codex-pool plan|sync|validate|expose|configure-local
bun scripts/cli.ts platform-infra wechat-archive plan|apply|status|validate|pull
bun scripts/cli.ts platform-infra wechat-archive wcf-host-status|collector-plan|collector-apply|collector-status
```
- `platform-infra` 是 G14 k3s 上 UniDesk 运维的平台基础设施 namespace;新增平台服务优先进入该 namespace,旧 `devops-infra` 只作为渐进迁移来源。
- Sub2API 的日常部署、Codex pool、FRP 暴露、master `~/.codex` 配置、验收和排障统一使用 `$unidesk-sub2api`UniDesk 仓库 `.agents/skills/unidesk-sub2api/SKILL.md`)。
- WeChat archive 是 platform-infra 的 YAML-first 工作流入口;D601 personal WeChat upstream 必须复用既有 D601 `platform-infra` namespace`createNamespace=false`,只读 collector 的副本、镜像、WCF host、端口和版本 pin 都以 `config/platform-infra/wechat-archive.yaml` 为准。
- 如果 WeChatFerry 配套的 PC 微信版本被微信服务端拒绝登录,按上游兼容阻塞处理:把 collector 的 YAML 副本数调为 `0` 并通过 `collector-apply --confirm --wait` 同步,保留 Secret/ConfigMap/PVC 和 Windows 准备态;不要手工 `kubectl scale`、新建 namespace 或采用版本检查绕过工具作为长期路径。
- UniDesk 仓库 `docs/reference/platform-infra.md` 只保留开发边界、YAML-first 真相和探针口径,不重复日常操作手册。
---
## CI Tools Image
```bash
bun scripts/cli.ts hwlab g14 tools-image status
bun scripts/cli.ts hwlab g14 tools-image build \
--name ci-node-tools --tag <tag> \
[--dockerfile deploy/ci/hwlab-ci-node-tools.Dockerfile] [--dry-run|--confirm]
```
在 G14 host 构建并 push 到本地 registry。
---
## PipelineRun 清理
```bash
# 清理已完成 PipelineRun
bun scripts/cli.ts hwlab g14 control-plane cleanup-runs \
--lane v02|g14|all [--min-age-minutes N] [--limit N] [--dry-run|--confirm]
# D601/G14 node-scoped runtime lane retention
bun scripts/cli.ts hwlab nodes control-plane cleanup-runs \
--node D601 --lane v03 [--min-age-minutes N] [--limit N] [--dry-run|--confirm --wait]
# 补充清理 Released PV
bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs \
--lane all [--limit N] [--dry-run|--confirm]
```
---
## 手动补记 rollout
```bash
bun scripts/cli.ts hwlab g14 record-rollout --pr <number> --source-commit <sha>
```
手动补记 CI/CD 耗时、TaskRun 指标和语义化 changelog 到指挥简报。
---
## AgentRun 控制面
YAML-only lane 以 `config/agentrun.yaml` 为部署真相;node/lane、source workspace/branch、image build、GitOps branch/path、runtime namespace、Secret、外置数据库、manager env、git-mirror 和 edge 暴露都从 YAML 进入 CLI。AgentRun service repo 的 `deploy/deploy.json` 不能作为 UniDesk deployment truth;新 lane 不再维护该文件。
```bash
bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02
bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 [--force-active] [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 [--pipeline-run <name>|--source-commit <sha>] [--full|--raw]
```
- `plan`: 只读解析 YAML,输出控制面、source、image build、GitOps、runtime 和 Secret plan,不打印 Secret value
- `apply`: 按 YAML 渲染并 apply Tekton RBAC/Pipeline、Argo AppProject/Application 和 runtime namespace
- `secret-sync`: 按 YAML 的 Secret sourceRef/keyMapping 同步 runtime Secret 和外置 DB Secret,只输出 fingerprint
- `restart`: patch manager Deployment 的 restart annotation 并等待 rollout,用于 Secret export/DB 连接串变化后让 workload 读取新 Secret;不要手工删除 Pod
- `trigger-current`: 确保 source branch/workspace,删除新 lane source branch 的 `deploy/deploy.json`,构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalog,触发 git-mirror sync 和 provenance PipelineRunconfirmed 运行可返回异步 job,必须用 `job status <jobId> --tail-bytes 12000``agentrun-yaml-lane-trigger` progress,再用 `status --pipeline-run <name>` 轮询收口;confirmed 收尾会尝试恢复 fixed source workspace 到 lane branch,恢复失败只作为 warning 披露
- `cleanup-runners`: 只清 YAML 选中 lane runtime namespace 中匹配 `deployment.runner.retention.selectors` 的 runner Job/Podrunner 上限、最后活跃排序、active heartbeat 窗口、age-based cleanup 开关和 selector 都以 YAML 为准。`dry-run` 必须先看 manager facts、inactive candidates、selected 和 `activeRunRisk`;普通 `--confirm` 只删除 selected inactive runner,不替代 CI PipelineRun 清理。`--force-active` 只用于 operator 明确决定强杀所有匹配 runner pod/job 的资源恢复场景,必须先 dry-run 确认 `criteria.forceActive=true` 和 selection 范围;它会中断 active run/command/session,但仍优先于裸 `kubectl delete pod/job`
- `status`: 默认返回 compact commander JSON,关键结论在 `.data.summary``.data.alignment`,完整 YAML target、原始 source/runtime/gitMirror payload 和成功 probe tail 只在 `--full|--raw` 展开
YAML-only lane 的长步骤必须由 CLI 拆成短提交和状态轮询:source bootstrap、image build、GitOps publish、git-mirror sync 和 PipelineRun 创建不得塞进一个顶层 `trans` 长连接。GitOps publish 必须使用隔离临时 clone/worktree,不能切换或污染 YAML 声明的固定 source workspace;如果历史失败发布留下 dirty/detached/GitOps branch 状态,只清理已知发布残留并恢复到 lane source branch 后再重试。后台步骤的 `status``ok` 要共同判定,`status=succeeded``ok=false` 是终态失败,不继续轮询到超时。
AgentRun YAML-only lane closeout 必须同时看当前 source branch tip、目标 PipelineRun、GitOps revision、Argo revision 和 manager source commit。发布过程中如果 source branch 被并行 PR 推进,`status --pipeline-run <name>` 会通过 `summary.branchDrift` / `alignment.branchDrift` 标记目标 PipelineRun 是否已被当前 branch tip supersede;先确认最新 tip 包含本次修复,再按最新 tip 重新 `trigger-current`。最终只用最新 PipelineRun 的 `status``aligned=true``blockers=[]``argoSyncedToGitops=true``managerSourceMatchesExpected=true` 收口。`trigger-current` 会尝试恢复 YAML 声明的固定 source workspace 到 lane branch;若返回 `source-worktree-restore-failed` warning、workspace dirty,或后续 `status` 仍显示 `summary.source.workspaceDetached=true`,先按 `sourceWorkspaceRestore.failureKind` 修复 fixed workspace,再继续下一轮开发或部署。
Runner egress proxy 只从 `config/agentrun.yaml``deployment.runner.egressProxyUrl``deployment.runner.noProxyExtra` 进入部署;manager Deployment 必须带 `AGENTRUN_RUNNER_EGRESS_PROXY_URL``AGENTRUN_RUNNER_NO_PROXY_EXTRA`,验收时还要用真实 `create/apply/send` 触发 runner Job,检查 Pod env、event/trace 和 final response。GitOps 已更新但 Argo 仍在旧 revision 时,走 `agentrun control-plane refresh --node <node> --lane <lane> --confirm`,不要手工 patch runtime。
Runner 持久化、idle timeout 和 runner retention 只从 `config/agentrun.yaml``deployment.runner.*` / `deployment.runner.retention` 进入部署,不在 HWLAB 仓库放运维 YAML。验收不能只看 manager Deployment/Pod env;必须用 HWLAB/AgentRun 原入口创建新 turn 或 runner Job,并检查新 runner Job env、session PVC、`AGENTRUN_SOURCE_COMMIT` 和 trace/result 是否复用同一 run 且没有 `reuse-blocked`runner retention closeout 还要用 `cleanup-runners --dry-run` 证明 over-limit selection 不触碰 active runner。
Provider credential 的 `config.toml` 变更同样走 YAML `sourceRef``secret-sync``restart`lane config 只声明该 lane 需要的 Codex CLI runtime options。不要复制指挥机全局 `~/.codex/config.toml` 作为长期事实,也不要在没有同 lane `auth.json` / API key source 验证的情况下覆盖 provider endpoint。
### AgentRun v0.1 兼容入口
```bash
bun scripts/cli.ts agentrun control-plane status \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane trigger-current \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane refresh \
[--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-runs \
[--min-age-minutes N] [--limit N] [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-released-pvs \
[--limit N] [--dry-run|--confirm]
```
- `status`: 只读汇总 source commit、PipelineRun、Argo、manager image、git mirror 和 `aligned` 结论
- `trigger-current`: 快进 `G14:/root/agentrun-v01` → mirror sync → 创建 `agentrun-v01-ci-<short12>` PipelineRun
- `refresh`: Argo hard refresh(不 patch runtime workload
- `cleanup-runs`: 只清理 `agentrun-ci` 中已完成 PipelineRun + 临时 PVC;不清理 `agentrun-v01` runtime runner Job/Pod/Secret
- `cleanup-released-pvs`: 回收 Released PV
AgentRun `control-plane status` 的 compact JSON 关键字段在 `.data.summary.sourceCommit``.data.summary.expectedPipelineRun``.data.summary.runtimeAlignment``.data.summary.gitMirror``.data.summary.ci.pipelineRun``.data.summary.argo``.data.alignment`,不要假设存在 `.data.status`。触发部署后如果 GitOps 已 promotion 但 git mirror `pendingFlush=true`,先执行 `bun scripts/cli.ts agentrun git-mirror flush --confirm --wait`,再 `control-plane refresh --confirm`,最后用 `control-plane status --full` 证明 `.data.summary.runtimeAlignment.argoSyncedToGitops=true``.data.summary.runtimeAlignment.managerSourceMatchesExpected=true``.data.summary.ci.pipelineRun.status=True`
## AgentRun v0.1 Git Mirror
```bash
bun scripts/cli.ts agentrun git-mirror status [--full|--raw]
bun scripts/cli.ts agentrun git-mirror sync [--dry-run|--confirm] [--wait]
bun scripts/cli.ts agentrun git-mirror flush [--dry-run|--confirm] [--wait]
```
- `status`: 返回 `localV01`/`githubV01`/`localGitops`/`githubGitops`/`pendingFlush`/`githubInSync`
- `sync`: 拉取 GitHub `v0.1` + `v0.1-gitops` refs
- `flush`: 推送本地 `v0.1-gitops` → GitHub
与 HWLAB v0.2 mirror 共用 `devops-infra` 服务和 cache PVC。
+18 -221
View File
@@ -5,233 +5,30 @@ description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy
# UniDesk Code Queue / AgentRun CLI
旧 Code Queue 已冻结新任务和写入口。`bun scripts/cli.ts codex ...` 现在只作为历史归档、只读排障和残留任务停止入口;新的指挥官派单、Aipod/Artificer 执行、events/logs/result、ack/cancel、dispatch、session follow-up 必须走 AgentRun 资源原语,并按 cli-spec 渐进披露。UniDesk 是 render-only client:默认输出是低噪声 human 表格/摘要,脚本读取显式使用 `-o json|yaml` 的稳定客户端 schema`--raw` 只用于查看直连 AgentRun REST envelope
新任务、Aipod/Artificer 派单、session follow-up、events/logs/result、ack/cancel、dispatch 和 run/command/runner drill-down 走 AgentRun 资源原语。legacy `codex` 只保留历史只读、未读积压和残留任务停止
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts agentrun ...`
---
## 新任务入口
## 高频入口
```bash
# 查看 AgentRun 指挥官队列
bun scripts/cli.ts agentrun get tasks --queue commander --limit 20
bun scripts/cli.ts agentrun get tasks --queue commander -o wide
# 查看一个 task/run/command/runnerjob/session 生命周期
bun scripts/cli.ts agentrun describe task/<taskId>
bun scripts/cli.ts agentrun events run/<runId> --after-seq <lastSeq> --limit 100
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100
bun scripts/cli.ts agentrun result run/<runId> --command <commandId>
bun scripts/cli.ts agentrun describe command/<commandId> --run <runId>
bun scripts/cli.ts agentrun describe runnerjob/<runnerJobId> --run <runId>
# 查看 / 渲染 AipodSpecArtificer 是默认分布式开发 agent
bun scripts/cli.ts agentrun get aipodspecs
bun scripts/cli.ts agentrun describe aipodspec/Artificer
bun scripts/cli.ts agentrun aipod-specs render Artificer --prompt-stdin
# AipodSpec apply/delete 仍由客户端资源原语发起,服务端只返回 REST 业务事实
bun scripts/cli.ts agentrun aipod-specs apply --yaml-stdin --dry-run
# 提交 AgentRun task manifest
bun scripts/cli.ts agentrun apply -f - --dry-run <<'YAML'
kind: Task
spec:
tenantId: unidesk
projectId: example
queue: commander
title: 任务标题
payload:
prompt: 任务说明
YAML
# 用 AipodSpec 提交,优先用于新任务和 Artificer
bun scripts/cli.ts agentrun create task --aipod Artificer \
--prompt-stdin --idempotency-key <key>
# 查看/控制 AgentRun session
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100
bun scripts/cli.ts agentrun ack session/<sessionId>
bun scripts/cli.ts agentrun dispatch task/<taskId>
bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin
bun scripts/cli.ts agentrun cancel session/<sessionId> --reason <text> --dry-run
bun scripts/cli.ts agentrun cancel command/<commandId> --run <runId> --reason <text> --dry-run
bun scripts/cli.ts agentrun get task --limit 20
bun scripts/cli.ts agentrun describe task <task>
bun scripts/cli.ts agentrun events <task> --limit 80
bun scripts/cli.ts agentrun logs <task> --tail 120
bun scripts/cli.ts agentrun result <task>
bun scripts/cli.ts agentrun send <session> --prompt-stdin
```
日常 task manifest 优先使用 YAML heredoc`agentrun apply -f -`;单 prompt 派单优先 `agentrun create task --aipod Artificer --prompt-stdin`;同 session 续跑只使用 `agentrun send session/<sessionId>`。UniDesk 客户端按 `config/agentrun.yaml` 直连 AgentRun REST API,不经过 HWLAB runtime、SSH official CLI 或旧 bridge wrapper`send` 是唯一用户级 session follow-up 写入口,服务端按 durable session/run/command 状态自动决定内部 `steer` 或新 `turn`,旧 CLI `turn/steer` 路径不保留兼容。`--json-file``--prompt-file``--runner-json-file` 只是客户端输入来源,用于已审阅且可复用的受控文件。它不是旧 Code Queue adapter,不双写,也不迁移旧历史
AipodSpec/Artificer、task manifest、session 控制、queue 渐进披露、HWLAB Code Agent 整合、trace/output 分页、judge、中断/取消和旧队列归档见 [references/full.md](references/full.md)
`AipodSpec` 是 AgentRun v0.1 的声明式 agent 装配:模型 profile、gitbundle、skills/tools、SecretRef 和 tool credential 都从 YAML 规格渲染。`Artificer` 默认用于 UniDesk 分布式开发任务,使用 `sub2api` provider、`gpt-5.5``reasoningEffort=xhigh`,并通过 SecretRef 注入 GitHub PR token、GitHub SSH 和 UniDesk SSH 透传能力。更新规格时使用 `agentrun aipod-specs apply --yaml-stdin --dry-run` 先看计划,确认后再去掉 `--dry-run`;不得把 API key、SSH key 或 token 写入 prompt、payload、YAML 或 issue。
## P0 边界
AgentRun task payload 需要 runner 内使用 UniDesk SSH 透传时,只通过 `executionPolicy.secretScope.toolCredentials[].tool=unidesk-ssh` 请求 `agentrun-v01-tool-unidesk-ssh/UNIDESK_SSH_CLIENT_TOKEN` SecretRef;不要把 token 写入 prompt、payload 或 `transientEnv`。非敏感 endpoint 使用 `UNIDESK_MAIN_SERVER_IP``UNIDESK_MAIN_SERVER_HOST``UNIDESK_FRONTEND_URL`,可由 runner-job `transientEnv` 显式提供;G14 `agentrun-v01` manager 也会在缺省时按受控默认值自动补齐,并在 trace 中只显示 env name/count/hash 与 `valuesPrinted=false`
- 新写入口不要用 `codex submit/steer/resume`;这些 legacy mutation 已冻结
- 默认输出保持低噪声表格/摘要;机器消费显式 `-o json|yaml``--raw`
- Session follow-up 使用 `agentrun send`,由服务端决定内部 steer vs turn。
- HWLAB Code Agent/CaseRun follow-up 也优先落到 AgentRun resource primitive,不绕过队列状态。
## Queue 渐进披露
## 何时读取 reference
AgentRun queue 生命周期不是一个单独的 `queue lifecycle` 命令,而是一组资源原语的渐进披露组合:
1. 默认总览用 `get tasks --queue commander --limit 20`,只看 task state、queue/lane、run/cmd/rjob/session ref、age 和 attention
2. 单任务用 `describe task/<taskId>`,读取 `latestAttempt.runId``commandId``runnerJobId``sessionId/sessionPath` 和少量 `Next:`
3. Run 级状态用 `events run/<runId>``result run/<runId> --command <commandId>`,判断 terminalClassification、failureKind、provider interruption、timeoutBudget 和 recoveryActions。
4. Command 级状态用 `describe command/<commandId> --run <runId>``result command/<commandId> --run <runId>`,确认 command state、ack、terminal status 和结果摘要;确认为单个 active command 卡住时,先用 `cancel command/<commandId> --run <runId> --reason <text> --dry-run` 核对 `CancelLifecycle` 的 authority、cascade、runner abort 和 fencing,再去掉 `--dry-run` 清理该 command,保留同一个 session 后再用 `send session/<sessionId>` 续跑。
5. Runner job 只读状态用 `describe runnerjob/<runnerJobId> --run <runId>`,确认 env image reuse、jobName、namespace、phase、exitCode、retention 和 `valuesPrinted=false`。不要为了这些字段手动调用 `trans G14:k3s kubectl ...`
6. Runtime runner Job/Pod retention 或 operator 明确要求强杀 runner 时,不属于单个 task/session 资源原语;使用 `bun scripts/cli.ts agentrun control-plane cleanup-runners --node <node> --lane <lane> [--force-active] --dry-run|--confirm`。普通 cleanup 只删 inactive selected runner`--force-active` 会中断 active run/command/session,必须先 dry-run 确认 selection,并且仍应优先于裸 `kubectl delete pod/job`
7. Session trace/output 只在 `describe task` 或 result 里有实际 `sessionId` 时使用 `logs|ack|send|cancel session/<sessionId>``sessionRef=null` 时不要猜 session 命令。用户级 follow-up 一律使用 `send session/<sessionId>`,不要回到旧 `turn/steer``sessions ...` 兼容路径。
8. 已创建但尚未运行的 task 使用 `dispatch task/<taskId>` 派发,不再退回旧 bridge `queue dispatch`
默认视图必须低噪声且不是 JSON envelope`-o json|yaml` 才输出稳定机器结构,`--raw` 才保留直连 AgentRun REST envelope;命令返回里的下一步应优先是 `bun scripts/cli.ts agentrun ...` 资源原语,不得把人工 k8s 查询作为日常下一步。
AgentRun cancel 策略由 `config/agentrun.yaml` 的 lane 级 `deployment.runner.cancelLifecycle` 管理;操作 D601、G14 或其他非默认 lane 时必须带 `--node/--lane --dry-run` 先确认 YAML policy,不要依赖全局默认或手动 k8s 强杀来替代资源原语。
## HWLAB Code Agent 入口整合
HWLAB Code Agent / CaseRun follow-up 的日常派单也归入 AgentRun 资源原语:新任务用 `create task --aipod Artificer` 或包含 HWLAB gitbundle 的 `apply -f -`;运行中纠偏用 `send session/<sessionId> --aipod Artificer`。需要验证 HWLAB Web/Cloud API 原入口时,仍按 `$hwlab-code-agent` 使用 G14 `/root/hwlab-v02``hwlab-cli client agent ...` 拉取同一 trace/result/inspect;不要回到旧 `codex submit/resume/steer`
排查 HWLAB v0.3 / D601 Code Agent trace 时,HWLAB `client agent result|trace|inspect` 仍是跨层事实锚点:它会暴露 AgentRun namespace、runId、commandId、jobName 和 providerProfile。若 `bun scripts/cli.ts agentrun events|result run/<runId>` 在默认 direct-http 面返回 `run not found`,不要据此判定 run 已丢失;先核对该 run 是否属于 D601 `agentrun-v02` 或其他非默认 lane,并继续用 HWLAB trace/result 或对应 `agentrun control-plane ... --node <node> --lane <lane>` 查询运行面。Codex provider config 的 context/auto-compact 修复属于 AgentRun lane Secret `sourceRef` 问题,应回到 UniDesk `config/agentrun.yaml``secrets[].sourceRef``.state/secrets/agentrun/*config.toml``agentrun control-plane secret-sync|restart` 收敛,不要在 prompt、HWLAB adapter 或 Kubernetes Secret 里手补。
---
## 本地 Codex Trace 采集
`bun scripts/cli.ts codex trace ...` 是本机 Codex 运行痕迹的只读诊断入口,默认扫描 `~/.codex`,也可以用 `--root <dir>` 指向其他 Codex home 或已拷贝的 trace 目录。它不提交新任务、不续跑 session,也不恢复旧 Code Queue 写入口。
```bash
bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]
bun scripts/cli.ts codex trace active [--root ~/.codex]
bun scripts/cli.ts codex trace grep --session <id> --pattern 'playwright|auth-login-failed' [--since ISO]
bun scripts/cli.ts codex trace grep --session <id> --messages --pattern 'Playwright|web-probe'
bun scripts/cli.ts codex trace grep --session <id> --failed-only [--tool exec_command]
bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]
bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]
bun scripts/cli.ts codex trace show --session <id> [--root ~/.codex] [--tail-bytes 12000]
```
默认只收集 `sessions/*.jsonl``history.jsonl``shell_snapshots``*.log` 和 trace 命名文本;`auth.json``config.toml`、sqlite、cache、`.tmp`、generated images、plugins 和 skills 默认跳过。`active``/proc` 找正在被 Codex 进程打开的 session JSONL,不依赖外部 `lsof`,并输出可复制的 `SESSION-ID``grep/show` 优先用 `--session <id>` 定位,避免复制长 JSONL 路径;session id 支持完整 UUID、短前缀和文件名片段,歧义时会提示候选。`grep` 默认只扫 active/recent session,并用 `rg`/raw-line prefilter 先定位候选行再解析 JSONL;需要全量文件域时显式加 `--all-files`,需要跳过 raw 预筛、逐行深度解析 decoded 字段时显式加 `--deep`。排查对话脉络优先 `--messages --pattern ...`,排查工具链优先 `--tools --tool <name>`,只看失败调用用 `--failed-only [--tool <name>]` 且可以不带 pattern。默认输出优先 message 和 tool-call inputtool output 默认折叠,只给失败状态、错误摘要和对应输入,不打印整段 transcript;需要查看命中输出片段时显式加 `--include-output``-o wide`。它优先用于排查 `playwright``auth-login-failed``UNIDESK_*HINT` 等高噪声 trace。默认输出是类似 k8s 的简洁 text/table;脚本消费时显式加 `-o json|yaml|name|wide`。需要本地审阅敏感/数据库文件时必须显式加 `--include-sensitive``--include-sqlite``collect` 只复制有界文件并写 manifest,不压缩、不上传、不删除源文件。
---
## 冻结的旧写入口
以下命令必须返回 `ok=false``frozen=true``degradedReason=legacy-code-queue-frozen`,并提示 AgentRun 替代命令:
```bash
bun scripts/cli.ts codex submit ...
bun scripts/cli.ts codex enqueue ...
bun scripts/cli.ts codex steer <taskId> ...
bun scripts/cli.ts codex resume <taskId> ...
bun scripts/cli.ts codex queue create <queueId>
bun scripts/cli.ts codex queue merge <sourceQueueId> --into <targetQueueId>
bun scripts/cli.ts codex move <taskId> --queue <queueId>
```
不要用兼容开关、legacy mode、adapter、双写或 fallback 绕开冻结边界。
---
## 历史任务视图
### Commander(低噪声轮询)
```bash
bun scripts/cli.ts codex tasks --view commander [--limit N]
```
返回旧 Code Queue 历史/残留任务的有界 action mapactive runners 计数、少量 active item、queued/retry_wait 计数、terminal-unread 总数、关键风险计数、分类计数和 drill-down 命令。新任务队列状态用 `agentrun get tasks --queue commander`
### Supervisor(分区视图)
```bash
bun scripts/cli.ts codex tasks --view supervisor \
[--status succeeded|running|queued|failed|canceled|judging|retry_wait] \
[--unread] [--limit N] [--before-id id]
```
返回 `activeRunning``running``completedUnread``recentCompleted``queued` 分区。状态 alias`completed|successful → succeeded``cancelled → canceled``pending → queued`
### 单任务
```bash
bun scripts/cli.ts codex task <taskId> [--detail|--full] [--trace]
```
---
## 未读积压
```bash
# 摘要
bun scripts/cli.ts codex unread [--queue id] [--repo owner/name] [--issue N]
# 详细列表
bun scripts/cli.ts codex unread list [--view full] [--limit N]
# 标记已读
bun scripts/cli.ts codex unread mark-read --confirm [--queue id]
```
`mark-read` 必须 `--confirm`,单任务审阅优先用 `codex read <taskId>`
---
## Trace / Output 分页
```bash
# 逻辑 trace(分页)
bun scripts/cli.ts codex task <taskId> --trace \
[--tail|--from-start|--after-seq N|--before-seq N] [--limit N] [--full]
# 原始 output(分页补取)
bun scripts/cli.ts codex output <taskId> \
[--tail|--from-start|--after-seq N|--before-seq N] [--limit N] [--full-text]
```
---
## 标记已读
```bash
bun scripts/cli.ts codex read <taskId>
```
标记单个终态任务已读,同时返回任务身份、终态 attempt 摘要和最终 response。
---
## Judge(复现评判)
```bash
bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]
```
按指定 attempt 单步复现 judge,诊断入口。
---
## 中断/取消
```bash
bun scripts/cli.ts codex interrupt <taskId>
bun scripts/cli.ts codex cancel <taskId>
```
仅用于停止旧 Code Queue 残留任务;新 AgentRun session 使用 `bun scripts/cli.ts agentrun cancel session/<sessionId>`,单个卡住 command 使用 `bun scripts/cli.ts agentrun cancel command/<commandId> --run <runId>`
---
## 旧队列归档
```bash
bun scripts/cli.ts codex queues [--full|--all] [--limit N] [--page N]
```
只读查看旧队列摘要。旧 queue create/merge 和 move 已冻结。
---
## Dev Ready / PR Preflight
```bash
bun scripts/cli.ts codex dev-ready
bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run ...] [--pr-create-dry-run ...] [--issue N] [--full|--raw]
```
---
- 创建/应用 AgentRun task 或 AipodSpec:读 [references/full.md](references/full.md) 的新任务入口段。
- 查历史 `codex tasks/unread/output/trace`:读历史任务、未读积压和 Trace/Output 段。
- 需要 judge、取消、中断、Dev Ready 或 PR preflight:读对应段
@@ -0,0 +1,237 @@
---
name: unidesk-code-queue
description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy `codex` 子命令只保留历史只读和残留停止;新任务提交、Aipod/Artificer 派单、session follow-up、events/logs/result、ack/cancel、dispatch、run/command/runner 状态 drill-down 和 HWLAB Code Agent/CaseRun follow-up 必须使用 `agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send` 资源原语。用户提到 codex、Code Queue、submit、send、resume、tasks、unread、code-queue、aipod、Artificer、HWLAB Code Agent 时使用。
---
# UniDesk Code Queue / AgentRun CLI
旧 Code Queue 已冻结新任务和写入口。`bun scripts/cli.ts codex ...` 现在只作为历史归档、只读排障和残留任务停止入口;新的指挥官派单、Aipod/Artificer 执行、events/logs/result、ack/cancel、dispatch、session follow-up 必须走 AgentRun 资源原语,并按 cli-spec 渐进披露。UniDesk 是 render-only client:默认输出是低噪声 human 表格/摘要,脚本读取显式使用 `-o json|yaml` 的稳定客户端 schema`--raw` 只用于查看直连 AgentRun REST envelope。
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts agentrun ...`
---
## 新任务入口
```bash
# 查看 AgentRun 指挥官队列
bun scripts/cli.ts agentrun get tasks --queue commander --limit 20
bun scripts/cli.ts agentrun get tasks --queue commander -o wide
# 查看一个 task/run/command/runnerjob/session 生命周期
bun scripts/cli.ts agentrun describe task/<taskId>
bun scripts/cli.ts agentrun events run/<runId> --after-seq <lastSeq> --limit 100
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100
bun scripts/cli.ts agentrun result run/<runId> --command <commandId>
bun scripts/cli.ts agentrun describe command/<commandId> --run <runId>
bun scripts/cli.ts agentrun describe runnerjob/<runnerJobId> --run <runId>
# 查看 / 渲染 AipodSpecArtificer 是默认分布式开发 agent
bun scripts/cli.ts agentrun get aipodspecs
bun scripts/cli.ts agentrun describe aipodspec/Artificer
bun scripts/cli.ts agentrun aipod-specs render Artificer --prompt-stdin
# AipodSpec apply/delete 仍由客户端资源原语发起,服务端只返回 REST 业务事实
bun scripts/cli.ts agentrun aipod-specs apply --yaml-stdin --dry-run
# 提交 AgentRun task manifest
bun scripts/cli.ts agentrun apply -f - --dry-run <<'YAML'
kind: Task
spec:
tenantId: unidesk
projectId: example
queue: commander
title: 任务标题
payload:
prompt: 任务说明
YAML
# 用 AipodSpec 提交,优先用于新任务和 Artificer
bun scripts/cli.ts agentrun create task --aipod Artificer \
--prompt-stdin --idempotency-key <key>
# 查看/控制 AgentRun session
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100
bun scripts/cli.ts agentrun ack session/<sessionId>
bun scripts/cli.ts agentrun dispatch task/<taskId>
bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin
bun scripts/cli.ts agentrun cancel session/<sessionId> --reason <text> --dry-run
bun scripts/cli.ts agentrun cancel command/<commandId> --run <runId> --reason <text> --dry-run
```
日常 task manifest 优先使用 YAML heredoc`agentrun apply -f -`;单 prompt 派单优先 `agentrun create task --aipod Artificer --prompt-stdin`;同 session 续跑只使用 `agentrun send session/<sessionId>`。UniDesk 客户端按 `config/agentrun.yaml` 直连 AgentRun REST API,不经过 HWLAB runtime、SSH official CLI 或旧 bridge wrapper`send` 是唯一用户级 session follow-up 写入口,服务端按 durable session/run/command 状态自动决定内部 `steer` 或新 `turn`,旧 CLI `turn/steer` 路径不保留兼容。`--json-file``--prompt-file``--runner-json-file` 只是客户端输入来源,用于已审阅且可复用的受控文件。它不是旧 Code Queue adapter,不双写,也不迁移旧历史。
`AipodSpec` 是 AgentRun v0.1 的声明式 agent 装配:模型 profile、gitbundle、skills/tools、SecretRef 和 tool credential 都从 YAML 规格渲染。`Artificer` 默认用于 UniDesk 分布式开发任务,使用 `sub2api` provider、`gpt-5.5``reasoningEffort=xhigh`,并通过 SecretRef 注入 GitHub PR token、GitHub SSH 和 UniDesk SSH 透传能力。更新规格时使用 `agentrun aipod-specs apply --yaml-stdin --dry-run` 先看计划,确认后再去掉 `--dry-run`;不得把 API key、SSH key 或 token 写入 prompt、payload、YAML 或 issue。
AgentRun task payload 需要 runner 内使用 UniDesk SSH 透传时,只通过 `executionPolicy.secretScope.toolCredentials[].tool=unidesk-ssh` 请求 `agentrun-v01-tool-unidesk-ssh/UNIDESK_SSH_CLIENT_TOKEN` SecretRef;不要把 token 写入 prompt、payload 或 `transientEnv`。非敏感 endpoint 使用 `UNIDESK_MAIN_SERVER_IP``UNIDESK_MAIN_SERVER_HOST``UNIDESK_FRONTEND_URL`,可由 runner-job `transientEnv` 显式提供;G14 `agentrun-v01` manager 也会在缺省时按受控默认值自动补齐,并在 trace 中只显示 env name/count/hash 与 `valuesPrinted=false`
## Queue 渐进披露
AgentRun queue 生命周期不是一个单独的 `queue lifecycle` 命令,而是一组资源原语的渐进披露组合:
1. 默认总览用 `get tasks --queue commander --limit 20`,只看 task state、queue/lane、run/cmd/rjob/session ref、age 和 attention。
2. 单任务用 `describe task/<taskId>`,读取 `latestAttempt.runId``commandId``runnerJobId``sessionId/sessionPath` 和少量 `Next:`
3. Run 级状态用 `events run/<runId>``result run/<runId> --command <commandId>`,判断 terminalClassification、failureKind、provider interruption、timeoutBudget 和 recoveryActions。
4. Command 级状态用 `describe command/<commandId> --run <runId>``result command/<commandId> --run <runId>`,确认 command state、ack、terminal status 和结果摘要;确认为单个 active command 卡住时,先用 `cancel command/<commandId> --run <runId> --reason <text> --dry-run` 核对 `CancelLifecycle` 的 authority、cascade、runner abort 和 fencing,再去掉 `--dry-run` 清理该 command,保留同一个 session 后再用 `send session/<sessionId>` 续跑。
5. Runner job 只读状态用 `describe runnerjob/<runnerJobId> --run <runId>`,确认 env image reuse、jobName、namespace、phase、exitCode、retention 和 `valuesPrinted=false`。不要为了这些字段手动调用 `trans G14:k3s kubectl ...`
6. Runtime runner Job/Pod retention 或 operator 明确要求强杀 runner 时,不属于单个 task/session 资源原语;使用 `bun scripts/cli.ts agentrun control-plane cleanup-runners --node <node> --lane <lane> [--force-active] --dry-run|--confirm`。普通 cleanup 只删 inactive selected runner`--force-active` 会中断 active run/command/session,必须先 dry-run 确认 selection,并且仍应优先于裸 `kubectl delete pod/job`
7. Session trace/output 只在 `describe task` 或 result 里有实际 `sessionId` 时使用 `logs|ack|send|cancel session/<sessionId>``sessionRef=null` 时不要猜 session 命令。用户级 follow-up 一律使用 `send session/<sessionId>`,不要回到旧 `turn/steer``sessions ...` 兼容路径。
8. 已创建但尚未运行的 task 使用 `dispatch task/<taskId>` 派发,不再退回旧 bridge `queue dispatch`
默认视图必须低噪声且不是 JSON envelope`-o json|yaml` 才输出稳定机器结构,`--raw` 才保留直连 AgentRun REST envelope;命令返回里的下一步应优先是 `bun scripts/cli.ts agentrun ...` 资源原语,不得把人工 k8s 查询作为日常下一步。
AgentRun cancel 策略由 `config/agentrun.yaml` 的 lane 级 `deployment.runner.cancelLifecycle` 管理;操作 D601、G14 或其他非默认 lane 时必须带 `--node/--lane --dry-run` 先确认 YAML policy,不要依赖全局默认或手动 k8s 强杀来替代资源原语。
## HWLAB Code Agent 入口整合
HWLAB Code Agent / CaseRun follow-up 的日常派单也归入 AgentRun 资源原语:新任务用 `create task --aipod Artificer` 或包含 HWLAB gitbundle 的 `apply -f -`;运行中纠偏用 `send session/<sessionId> --aipod Artificer`。需要验证 HWLAB Web/Cloud API 原入口时,仍按 `$hwlab-code-agent` 使用 G14 `/root/hwlab-v02``hwlab-cli client agent ...` 拉取同一 trace/result/inspect;不要回到旧 `codex submit/resume/steer`
排查 HWLAB v0.3 / D601 Code Agent trace 时,HWLAB `client agent result|trace|inspect` 仍是跨层事实锚点:它会暴露 AgentRun namespace、runId、commandId、jobName 和 providerProfile。若 `bun scripts/cli.ts agentrun events|result run/<runId>` 在默认 direct-http 面返回 `run not found`,不要据此判定 run 已丢失;先核对该 run 是否属于 D601 `agentrun-v02` 或其他非默认 lane,并继续用 HWLAB trace/result 或对应 `agentrun control-plane ... --node <node> --lane <lane>` 查询运行面。Codex provider config 的 context/auto-compact 修复属于 AgentRun lane Secret `sourceRef` 问题,应回到 UniDesk `config/agentrun.yaml``secrets[].sourceRef``.state/secrets/agentrun/*config.toml``agentrun control-plane secret-sync|restart` 收敛,不要在 prompt、HWLAB adapter 或 Kubernetes Secret 里手补。
---
## 本地 Codex Trace 采集
`bun scripts/cli.ts codex trace ...` 是本机 Codex 运行痕迹的只读诊断入口,默认扫描 `~/.codex`,也可以用 `--root <dir>` 指向其他 Codex home 或已拷贝的 trace 目录。它不提交新任务、不续跑 session,也不恢复旧 Code Queue 写入口。
```bash
bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]
bun scripts/cli.ts codex trace active [--root ~/.codex]
bun scripts/cli.ts codex trace grep --session <id> --pattern 'playwright|auth-login-failed' [--since ISO]
bun scripts/cli.ts codex trace grep --session <id> --messages --pattern 'Playwright|web-probe'
bun scripts/cli.ts codex trace grep --session <id> --failed-only [--tool exec_command]
bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]
bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]
bun scripts/cli.ts codex trace show --session <id> [--root ~/.codex] [--tail-bytes 12000]
```
默认只收集 `sessions/*.jsonl``history.jsonl``shell_snapshots``*.log` 和 trace 命名文本;`auth.json``config.toml`、sqlite、cache、`.tmp`、generated images、plugins 和 skills 默认跳过。`active``/proc` 找正在被 Codex 进程打开的 session JSONL,不依赖外部 `lsof`,并输出可复制的 `SESSION-ID``grep/show` 优先用 `--session <id>` 定位,避免复制长 JSONL 路径;session id 支持完整 UUID、短前缀和文件名片段,歧义时会提示候选。`grep` 默认只扫 active/recent session,并用 `rg`/raw-line prefilter 先定位候选行再解析 JSONL;需要全量文件域时显式加 `--all-files`,需要跳过 raw 预筛、逐行深度解析 decoded 字段时显式加 `--deep`。排查对话脉络优先 `--messages --pattern ...`,排查工具链优先 `--tools --tool <name>`,只看失败调用用 `--failed-only [--tool <name>]` 且可以不带 pattern。默认输出优先 message 和 tool-call inputtool output 默认折叠,只给失败状态、错误摘要和对应输入,不打印整段 transcript;需要查看命中输出片段时显式加 `--include-output``-o wide`。它优先用于排查 `playwright``auth-login-failed``UNIDESK_*HINT` 等高噪声 trace。默认输出是类似 k8s 的简洁 text/table;脚本消费时显式加 `-o json|yaml|name|wide`。需要本地审阅敏感/数据库文件时必须显式加 `--include-sensitive``--include-sqlite``collect` 只复制有界文件并写 manifest,不压缩、不上传、不删除源文件。
---
## 冻结的旧写入口
以下命令必须返回 `ok=false``frozen=true``degradedReason=legacy-code-queue-frozen`,并提示 AgentRun 替代命令:
```bash
bun scripts/cli.ts codex submit ...
bun scripts/cli.ts codex enqueue ...
bun scripts/cli.ts codex steer <taskId> ...
bun scripts/cli.ts codex resume <taskId> ...
bun scripts/cli.ts codex queue create <queueId>
bun scripts/cli.ts codex queue merge <sourceQueueId> --into <targetQueueId>
bun scripts/cli.ts codex move <taskId> --queue <queueId>
```
不要用兼容开关、legacy mode、adapter、双写或 fallback 绕开冻结边界。
---
## 历史任务视图
### Commander(低噪声轮询)
```bash
bun scripts/cli.ts codex tasks --view commander [--limit N]
```
返回旧 Code Queue 历史/残留任务的有界 action mapactive runners 计数、少量 active item、queued/retry_wait 计数、terminal-unread 总数、关键风险计数、分类计数和 drill-down 命令。新任务队列状态用 `agentrun get tasks --queue commander`
### Supervisor(分区视图)
```bash
bun scripts/cli.ts codex tasks --view supervisor \
[--status succeeded|running|queued|failed|canceled|judging|retry_wait] \
[--unread] [--limit N] [--before-id id]
```
返回 `activeRunning``running``completedUnread``recentCompleted``queued` 分区。状态 alias`completed|successful → succeeded``cancelled → canceled``pending → queued`
### 单任务
```bash
bun scripts/cli.ts codex task <taskId> [--detail|--full] [--trace]
```
---
## 未读积压
```bash
# 摘要
bun scripts/cli.ts codex unread [--queue id] [--repo owner/name] [--issue N]
# 详细列表
bun scripts/cli.ts codex unread list [--view full] [--limit N]
# 标记已读
bun scripts/cli.ts codex unread mark-read --confirm [--queue id]
```
`mark-read` 必须 `--confirm`,单任务审阅优先用 `codex read <taskId>`
---
## Trace / Output 分页
```bash
# 逻辑 trace(分页)
bun scripts/cli.ts codex task <taskId> --trace \
[--tail|--from-start|--after-seq N|--before-seq N] [--limit N] [--full]
# 原始 output(分页补取)
bun scripts/cli.ts codex output <taskId> \
[--tail|--from-start|--after-seq N|--before-seq N] [--limit N] [--full-text]
```
---
## 标记已读
```bash
bun scripts/cli.ts codex read <taskId>
```
标记单个终态任务已读,同时返回任务身份、终态 attempt 摘要和最终 response。
---
## Judge(复现评判)
```bash
bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]
```
按指定 attempt 单步复现 judge,诊断入口。
---
## 中断/取消
```bash
bun scripts/cli.ts codex interrupt <taskId>
bun scripts/cli.ts codex cancel <taskId>
```
仅用于停止旧 Code Queue 残留任务;新 AgentRun session 使用 `bun scripts/cli.ts agentrun cancel session/<sessionId>`,单个卡住 command 使用 `bun scripts/cli.ts agentrun cancel command/<commandId> --run <runId>`
---
## 旧队列归档
```bash
bun scripts/cli.ts codex queues [--full|--all] [--limit N] [--page N]
```
只读查看旧队列摘要。旧 queue create/merge 和 move 已冻结。
---
## Dev Ready / PR Preflight
```bash
bun scripts/cli.ts codex dev-ready
bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run ...] [--pr-create-dry-run ...] [--issue N] [--full|--raw]
```
---
+21 -418
View File
@@ -5,430 +5,33 @@ description: UniDesk GitHub CLI - 通过 `bun scripts/cli.ts gh ...` 管理 GitH
# UniDesk GitHub CLI
UniDesk 受控 GitHub 操作入口,底层 issue/PR REST 读写`bun scripts/cli.ts gh <subcommand>`,不依赖原生 `gh` binary,不手写 `curl`/GraphQL。Issue/PR 正文局部修补的人工首选入口是 `trans gh:/owner/repo/issue/<number> apply-patch``trans gh:/owner/repo/pr/<number> apply-patch`,底层 `gh issue patch` 只作为受控底座或兼容入口
GitHub issue/PR 正式读写必须`bun scripts/cli.ts gh ...``trans gh:/... apply-patch`,不依赖原生 `gh` binary,不手写 GitHub API
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts gh ...`
## 高频规则
## 实现边界
- Issue/PR 正文、评论、关闭/重开、PR 描述和 merge closeout 默认中文。
- 新 issue 正文必须包含 `目标合并分支: <repo branch/lane>`;不需要合并时写 `目标合并分支: 不适用`
- 大计划、后续阶段和独立改进方向创建新 issue;已有 issue 评论只写短进展、证据、阻塞和链接。
- 多行正文使用 quoted heredoc`--body-stdin <<'EOF'`;不要把长 Markdown 塞进 shell 参数。
- PR merge 只走 guarded `gh pr merge``gh pr create` 的 Next 默认是 `--merge --delete-branch`,只有确认 ancestry 可丢弃时才显式 `--squash`
GitHub 受控入口的长期架构权威是 `project-management/PJ2026-01/specs/PJ2026-010606-github-controlled-entry.md``scripts/src/gh.ts` 只保留兼容 re-export;新增或修改 GitHub 子命令必须落到 `scripts/src/gh/` 下对应职责模块(client/options/refs/body/issue/pr/board/attachments/escape-scan/help/index 等),不得把业务实现重新堆回根入口。
拆分后的 gh 模块文件头应保留 `SPEC: PJ2026-010606 GitHub入口 <实现引用版本>` 追溯,单个模块超过 3000 行时继续拆分。PR closeout 除常规 smoke 外,应覆盖至少一条 `trans gh:/... apply-patch --dry-run` 或等价写回路径,证明虚拟正文仍走同一 guard。
CLI 自检使用 `bun scripts/cli.ts check --syntax-only`、针对被改模块的 `bun --check <file>` 和必要 `gh ... --dry-run`/read smoke。不要把 `bun --check scripts/cli.ts` 当作低噪声自检;它可能执行根 help 并触发 dump。需要 scripts 类型检查时走 `bun scripts/cli.ts check --scripts-typecheck ...`,让 CLI 输出 heartbeat 和 bounded timeout 结果。
## 写入语言
- Issue/PR 的正式写入一律使用中文,包括标题、正文、评论、关闭/重开说明、PR 描述、PR 评论和 merge closeout 说明。
- 命令、路径、trace/session/job id、API path、代码标识符、英文专有名词、原始日志摘录和外部错误原文可以保留原样;解释性文字、结论、风险、计划和验收说明必须中文。
- 除非用户明确要求外文,不要用英文撰写 issue/PR closeout、进展评论或阻塞说明。
- Markdown 正文、issue/PR 正文和评论里的 issue/PR 引用必须写成 `[#<number>](https://github.com/<owner>/<repo>/issues/<number>)``[#<number>](https://github.com/<owner>/<repo>/pull/<number>)`,显示短号、链接目标保留完整 URL;不要显示裸长链接、裸井号编号或 `owner/repo` 加井号编号。`owner/repo#number` 只允许作为 CLI 命令参数 shorthand。
---
## Issue 目标分支 / Lane
- 创建 issue、补充正式 issue 评论、关闭/重开说明或把 trace 发现的问题登记成 issue 时,正文必须显式写明目标合并分支或运行 lane。
- 推荐固定字段:`目标合并分支: <repo> <branch/lane>`;例如 `目标合并分支: HWLAB v0.2``目标合并分支: AgentRun v0.1``目标合并分支: UniDesk master`
- 如果该 issue 只记录外部依赖、运维观察或不需要代码合并,仍必须写 `目标合并分支: 不适用`,并用一句话说明原因。
- 看到用户或上下文已明确目标 lane 时必须照写;不明确时按目标仓库长期 source truth 选择:HWLAB 按当前 runtime lane(如 `v0.2`/`v0.3`)、AgentRun 默认 `v0.1`、UniDesk 默认 `master`。不能确定时写 `目标合并分支: 待确认` 并说明待确认点,不能省略字段。
---
## 计划与评论分流
- 大计划、改进计划、后续工作拆解、独立验收标准或会形成多阶段执行的内容,必须创建新的 issue 承载;不要在已有 issue 评论区开长计划。
- 既有 issue 评论只写短进展、关键证据、当前结论、阻塞和新 issue 链接;评论用于串联时间线,不承载新的大范围计划正文。
- 如果调查中发现了独立改进方向,应先用 `gh issue create --body-stdin` 创建新 issue,标题和正文写清目标合并分支/lane、背景、计划、验收标准;然后在原 issue 评论中用 1-3 句说明已拆出,并链接到新 issue。
- 只有用户明确要求把计划写回当前 issue 正文,或当前 issue 本身就是唯一的专题计划 issue,才允许更新当前 issue 正文;即便如此,评论仍保持短小,不复制整篇计划。
---
## 认证探测
## 常用入口
```bash
bun scripts/cli.ts gh auth status [--repo owner/name]
bun scripts/cli.ts gh auth status --repo pikasTech/unidesk
bun scripts/cli.ts gh issue list --repo pikasTech/unidesk --state open --limit 30
bun scripts/cli.ts gh issue view <number> --repo pikasTech/unidesk --json body,title,state
bun scripts/cli.ts gh issue create --repo pikasTech/unidesk --title "标题" --body-stdin
bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title "标题" --body-stdin --base master --head <branch>
bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk
bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk --merge --delete-branch
```
探测 token 来源(`GH_TOKEN`/`GITHUB_TOKEN`/`gh auth token`)、GitHub REST egress、repo 可见性、issue 可读性。不打印 token
完整 issue/PR CRUD、comment、patch、stale-close、scan-escape、PR files/preflight/merge、看板命令和 `trans gh:` 虚拟文件系统见 [references/full.md](references/full.md)
## 聚焦帮助
## 何时读取 reference
具体子命令用法优先直接查 scoped help,不要先打开顶层长 help 再人工搜索:
```bash
bun scripts/cli.ts gh issue close --help
bun scripts/cli.ts gh issue comment --help
bun scripts/cli.ts gh pr merge --help
```
`gh <subcommand> --help``gh <subcommand> -h``gh <subcommand> help` 只输出匹配命令或命令组的 bounded JSON,包括相关 usage、短 notes 和完整 help 入口;`gh --help` / `gh help` 才输出完整顶层命令索引。
---
## Issue 命令
### 列表
```bash
bun scripts/cli.ts gh issue list [owner/repo] \
[--state open|closed|all] [--limit N] [--search text] [--title-prefix text] \
[--label label[,label...]] [--repo owner/name] \
[--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]
```
默认 `state=open``limit=30``owner/repo` 位置参数等价 `--repo``--search` 走 GitHub Search API 做查重。`--title-prefix` 在当前有界结果内按 issue 标题前缀做本地过滤,输出 `titleFilter` 的输入/输出/过滤数量,适合 `[FEEDBACK]` 去重:
```bash
bun scripts/cli.ts gh issue list --repo pikasTech/unidesk --state all \
--search "[FEEDBACK]" --title-prefix "[FEEDBACK]" \
--json number,title,state,url
```
### 查看
```bash
bun scripts/cli.ts gh issue view <number|url|owner/repo#number> \
[--repo owner/name] [--json body,title,state,closed,closedAt,comments,commentCount] [--raw|--full]
```
`read` 是兼容别名。支持 `owner/repo#number` shorthand(如 `pikasTech/HWLAB#1024`)。
人工读取 issue/PR 正文优先走 `trans gh:/owner/repo/issue/<number> cat|rg``trans gh:/owner/repo/pr/<number> cat|rg``gh issue view/read` 主要保留为结构化 JSON 底座、metadata 读取和兼容入口。
`--json comments` 默认只返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview`--full` 仍保持评论列表有界,只有 `--raw` 会显式展开所有评论正文。读取单条完整 comment body 使用 `gh issue comment view <commentId> --full`,显式 `--json` 路径的 comments 位于 `.data.json.comments`,不要依赖顶层重复 comments。
### 创建
```bash
bun scripts/cli.ts gh issue create \
--repo owner/name \
--title "标题" \
--label "bug,v0.2" \
--body-stdin <<'EOF'
目标合并分支: HWLAB v0.2
正文(Markdown,支持换行、反引号、表格)
EOF
```
`--body-stdin` 是 heredoc/stdin 第一等入口。`--dry-run` 只输出计划不写 GitHub。
### 更新正文
```bash
# 替换正文
bun scripts/cli.ts gh issue update <number> --repo owner/name \
--mode replace --body-stdin <<'EOF'
新正文
EOF
# 追加正文
bun scripts/cli.ts gh issue update <number> --repo owner/name \
--mode append --body-stdin <<'EOF'
追加内容
EOF
```
`edit``update --mode replace` 的兼容别名。正式写入默认先读当前 issue 做 body guard。
### 局部修补正文(优先走 `trans gh:`
```bash
# Issue 正文:route 未写 /1 时默认一楼正文
trans gh:/owner/name/issue/<number> apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
# PR 正文同样使用 body.md
trans gh:/owner/name/pr/<number> apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
```
GitHub issue/PR 正文局部修补必须优先走 `trans gh:/owner/repo/issue/<number> apply-patch``trans gh:/owner/repo/pr/<number> apply-patch`;虚拟文件固定是 `body.md``apply_patch``patch-apply` 仅作为兼容别名。底层 `bun scripts/cli.ts gh issue patch ... --body-patch-stdin` 是受控写回底座或兼容入口,不作为人工首选。`trans gh:` 会先读取当前正文,把 Codex `apply_patch` envelope 应用到 `body.md`,再通过现有 `gh issue/pr update` guard 写回;普通小补丁不需要固定先跑 `--dry-run`,高风险正文预览时才加 `--dry-run`
### 评论
```bash
# 创建评论
bun scripts/cli.ts gh issue comment create <number|owner/repo#number> \
--repo owner/name --body-stdin <<'EOF'
评论正文
EOF
# 读取单条评论
bun scripts/cli.ts gh issue comment view <commentId> \
--repo owner/name [--full|--raw]
# 原地修正评论
bun scripts/cli.ts gh issue comment update <commentId> \
--repo owner/name --body-stdin <<'EOF'
新的评论正文
EOF
# 局部修补评论
bun scripts/cli.ts gh issue comment patch <commentId> \
--repo owner/name --body-patch-stdin <<'PATCH'
*** Begin Patch
*** Update File: comment.md
@@
-old text
+new text
*** End Patch
PATCH
# 删除评论
bun scripts/cli.ts gh issue comment delete <commentId>
```
`view` 默认返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview`--full` 只展开这一条 comment body。`edit``comment update` 的兼容别名。`--body <short-text>` 仅适合短单行。日常修正文案优先用 `patch``update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。`comment patch` 会先读取 comment id 对应的当前正文,把 envelope 应用到虚拟文件 `comment.md`,再 PATCH 单条评论;上下文不匹配时失败且不写入。
评论区不要写新的大计划。若评论草稿已经包含多阶段计划、改进清单或验收标准,改为创建新 issue,并在评论中只留下新 issue 链接和短结论。
### 关闭/重开
```bash
bun scripts/cli.ts gh issue close <number|owner/repo#number> \
--repo owner/name [--comment "关闭原因"]
bun scripts/cli.ts gh issue reopen <number|owner/repo#number> \
--repo owner/name [--comment "重开原因"]
```
附长证据时先用 `comment create` 写证据评论,再用 `close --comment <短引用>` 关闭。不支持 `delete`(走 `close`)。
### 批量过期关闭
```bash
bun scripts/cli.ts gh issue stale-close \
--repo owner/name [--inactive-hours 48] [--limit N] \
[--label label] [--dry-run]
```
关闭 `updatedAt < observedAt - inactiveHours` 的 open issue。先 `--dry-run` 观察,再正式执行。
### 转义扫描
```bash
bun scripts/cli.ts gh issue scan-escape \
--repo owner/name [--limit N] [--dry-run]
```
只读扫描 issue 正文/评论中的字面量 `\n``\t`、ANSI escape 等污染。
---
## PR 命令
### 列表
```bash
bun scripts/cli.ts gh pr list [owner/repo] \
[--state open|closed|all] [--json ...] [--raw|--full]
```
### 查看
```bash
bun scripts/cli.ts gh pr view <number|url|owner/repo#number> \
[--repo owner/name] \
[--json body,title,state,stateDetail,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] \
[--raw|--full]
```
`stateDetail` 归一化为 `open|closed|merged``mergeable`/`mergeStateStatus`/`statusCheckRollup` 走 GraphQL。
### 文件变更
```bash
bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]
```
返回 changed files 统计,不输出 raw diff。`gh pr diff <number> --stat` 是兼容别名。
### 收口预检
```bash
bun scripts/cli.ts gh pr preflight <number|owner/repo#number> \
[--repo owner/name] [--full|--raw]
```
`preflight`/`closeout` 是别名。只读检查 state/draft/conflict/mergeable/statusCheck,不写 GitHub。默认只给 status check 计数与失败预览。
### 创建
```bash
bun scripts/cli.ts gh pr create \
--repo owner/name \
--title "标题" \
--body-stdin --base master --head <branch> \
[--draft] [--dry-run] <<'EOF'
PR 正文
EOF
```
`pr create` 成功后的 Next 默认提示使用 `gh pr merge ... --merge --delete-branch`。只有确认不需要保留 ancestry 或 merge parent history 时,才显式改用 `--squash`
### 合并
```bash
bun scripts/cli.ts gh pr merge <number> \
[--repo owner/name] [--merge|--squash|--rebase] \
[--delete-branch] [--dry-run]
```
一键 guarded merge`gh pr merge` 自己读取 closeout metadata、mergeability、mergeStateStatus 和 status checks,不要求用户先跑 `gh pr preflight``preflight`/`closeout` 只作为只读诊断入口保留。
当 GitHub GraphQL 返回 `mergeable=UNKNOWN/null``mergeStateStatus=UNKNOWN/null` 或 status rollup unknown 且没有明确 blockers 时,`gh pr merge` 会按 `config/unidesk-cli.yaml``github.prMerge.unknownRetry` 做指数退避重试,并在默认表格 `RETRY` 列显示 `1/5``2/5` 等尝试次数。重试耗尽仍 unknown 时,命令自动停止且不合并;冲突、draft、失败检查、pending check 等不可合并状态不会无意义重试。已 merged 返回 `alreadyMerged=true`
### 开发任务收口
完成开发任务后必须完整收口:创建 PR、通过受控 `gh pr merge` 合并 PR、删除远程任务分支、删除本地任务 worktree,并把主 worktree 快进到最新 `master`。远程分支优先通过 `gh pr merge ... --delete-branch` 删除;若合并后远端分支仍存在,再显式 `git push origin --delete <branch>`
删除本地 worktree 前先确认工作区 clean,且任务变更已经进入 `origin/master`squash merge 场景下不能只看本地分支是否是 `origin/master` 祖先,还要用 PR merged 状态、`git cherry` 等价补丁或关键文件 diff 确认语义已吸收。确认后执行 `git worktree remove .worktree/<task>`;如本地任务分支仅用于该 worktree 且已语义合入,可同步删除本地分支。
最后在主 worktree 执行 `git fetch origin master``git pull --ff-only origin master`,让主 worktree 回到最新 `master`。如果主 worktree 存在并行脏改、非 `master` 分支或其他原因导致不能安全 fast-forward,禁止 `reset``stash``checkout --` 或清理他人改动;改用干净 `origin/master` worktree 继续后续工具命令,并在最终回复中明确说明主 worktree 未更新的原因。
### 编辑 / 评论 / 关闭 / 重开
```bash
bun scripts/cli.ts gh pr update <number> --mode replace|append --body-stdin [--title ...]
bun scripts/cli.ts gh pr comment create <number> --body-stdin
bun scripts/cli.ts gh pr comment update <commentId> --body-stdin
bun scripts/cli.ts gh pr comment edit <commentId> --body-stdin
bun scripts/cli.ts gh pr comment delete <commentId>
bun scripts/cli.ts gh pr close <number> [--comment ...]
bun scripts/cli.ts gh pr reopen <number> [--comment ...]
```
`pr comment edit``pr comment update` 的兼容别名。与 issue 对应命令行为一致;PR 评论也是 GitHub issue comment,更新目标使用 commentId。
---
## 虚拟文件系统 (`trans gh:`)
```bash
# 列出 repo
trans gh:/pikasTech/HWLAB ls
# 列出 PR / Issue
trans gh:/pikasTech/HWLAB/pr ls [--state open|closed|all] [--limit N] [--full]
trans gh:/pikasTech/HWLAB/issue ls [--state open|closed|all] [--limit N] [--search TEXT] [--label L] [--full]
# 读取 / 检索正文
trans gh:/pikasTech/HWLAB/issue/1236 cat
trans gh:/pikasTech/HWLAB/issue/1236 rg 'API_KEY|YAML-first'
# 查看单个条目
trans gh:/pikasTech/HWLAB/pr/507 ls
trans gh:/pikasTech/HWLAB/505/1 cat
# apply-patch 写回正文(走 gh issue/pr update
trans gh:/pikasTech/HWLAB/pr/507 apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
old line
-old line
+new line
more context
*** End Patch
PATCH
```
正文一楼映射为 `body.md`route 未写 `/1` 时默认一楼正文。`issue ls --search/--state` 用于替代常见 `bun scripts/cli.ts gh issue list ... --search/--state` 读取;单条正文优先用 `cat``rg`,不要为了看 body 再走 `gh issue view --json body --full`。写回走 `gh issue/pr update` 的 guard 规则。`apply-patch` 是首选 operation`apply_patch``patch-apply` 只作为兼容别名;普通小补丁在已读取上下文后可以直接 apply,`--dry-run` 只作为高风险正文的可选预览。`rm` 对正文结构化拒绝。
---
## 看板命令 ([#20](https://github.com/pikasTech/unidesk/issues/20) 总看板)
```bash
# 结构审计
bun scripts/cli.ts gh issue board-audit \
--repo pikasTech/unidesk --board-issue 20 [--dry-run]
# 行列表
bun scripts/cli.ts gh issue board-row list \
--board-issue 20 [--state open|closed|all]
# 行查看
bun scripts/cli.ts gh issue board-row get <issueNumber> --board-issue 20
# 行更新(单字段)
bun scripts/cli.ts gh issue board-row update <issueNumber> \
--board-issue 20 \
--field progress|status|validation|branch|tasks|focus \
--value <text> \
[--expect-body-sha <sha>|--expect-updated-at <ts>]
# 行新增/插入(upsert
bun scripts/cli.ts gh issue board-row upsert <issueNumber> \
--board-issue 20 --section open|closed \
--branch <branch> --tasks <task> --summary <text> \
--focus <text> --validation <text> --progress <text> \
[--expect-body-sha <sha>|--expect-updated-at <ts>]
# 行移动 / 删除
bun scripts/cli.ts gh issue board-row move <issueNumber> \
--board-issue 20 --to open|closed [--status OPEN|CLOSED]
bun scripts/cli.ts gh issue board-row delete <issueNumber> \
--board-issue 20
```
写操作默认 dry-run,正式 PATCH 必须带 `--expect-body-sha``--expect-updated-at`
---
## 关键约定
### heredoc 优先
多行正文/评论一律用 `--body-stdin <<'EOF'`quoted heredoc),不用 `--body` 内联长文本、不用临时文件、不用 `gh api -f body=...`
```bash
bun scripts/cli.ts gh issue create --repo pikasTech/HWLAB \
--title "标题" --body-stdin <<'EOF'
正文,支持 `code`、**bold**、表格等 Markdown
EOF
```
### owner/repo#number shorthand
所有接受 issue/PR number 的命令都支持:
```bash
bun scripts/cli.ts gh issue view pikasTech/HWLAB#1024
bun scripts/cli.ts gh pr preflight pikasTech/HWLAB#1020
bun scripts/cli.ts gh issue comment create pikasTech/HWLAB#1024 --body-stdin <<'EOF'
...
EOF
```
shorthand 与显式 `--repo` 冲突时结构化失败。
### 高风险写入预览
整篇 replace、批量 lifecycle、PR merge 或并发敏感正文写入可先 `--dry-run``trans gh:/... apply-patch` 的普通小补丁在已读取上下文后不需要固定 dry-run:
```bash
bun scripts/cli.ts gh issue update 20 --mode append --body-stdin --dry-run <<'EOF'
...
EOF
```
### debug 任务入口
```bash
bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run ...] [--pr-create-dry-run ...] [--issue N] [--full|--raw]
```
用于 PR 型派单 admission,检查 scheduler auth、runner GH token、git worktree、GitHub egress 等。
- 需要具体 issue/PR/comment 命令参数、`--json` 字段或 body guard:读 [references/full.md](references/full.md) 的 Issue/PR 命令段。
- 需要局部修补正文或评论:读 `trans gh:` 和 apply-patch 段。
- 需要维护总看板 [#20](https://github.com/pikasTech/unidesk/issues/20):读看板命令段。
- 需要 closeout、preflight、merge 或 ancestry/squash 判断:读 PR 命令和关键约定段。
@@ -0,0 +1,434 @@
---
name: unidesk-gh
description: UniDesk GitHub CLI - 通过 `bun scripts/cli.ts gh ...` 管理 GitHub issue/PR,不依赖原生 `gh` binary。用户提到 gh、GitHub issue、GitHub PR、创建 issue、评论 issue、合并 PR、preflight、看板操作时使用。
---
# UniDesk GitHub CLI
UniDesk 受控 GitHub 操作入口,底层 issue/PR REST 读写走 `bun scripts/cli.ts gh <subcommand>`,不依赖原生 `gh` binary,不手写 `curl`/GraphQL。Issue/PR 正文局部修补的人工首选入口是 `trans gh:/owner/repo/issue/<number> apply-patch``trans gh:/owner/repo/pr/<number> apply-patch`,底层 `gh issue patch` 只作为受控底座或兼容入口。
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts gh ...`
## 实现边界
GitHub 受控入口的长期架构权威是 `project-management/PJ2026-01/specs/PJ2026-010606-github-controlled-entry.md``scripts/src/gh.ts` 只保留兼容 re-export;新增或修改 GitHub 子命令必须落到 `scripts/src/gh/` 下对应职责模块(client/options/refs/body/issue/pr/board/attachments/escape-scan/help/index 等),不得把业务实现重新堆回根入口。
拆分后的 gh 模块文件头应保留 `SPEC: PJ2026-010606 GitHub入口 <实现引用版本>` 追溯,单个模块超过 3000 行时继续拆分。PR closeout 除常规 smoke 外,应覆盖至少一条 `trans gh:/... apply-patch --dry-run` 或等价写回路径,证明虚拟正文仍走同一 guard。
CLI 自检使用 `bun scripts/cli.ts check --syntax-only`、针对被改模块的 `bun --check <file>` 和必要 `gh ... --dry-run`/read smoke。不要把 `bun --check scripts/cli.ts` 当作低噪声自检;它可能执行根 help 并触发 dump。需要 scripts 类型检查时走 `bun scripts/cli.ts check --scripts-typecheck ...`,让 CLI 输出 heartbeat 和 bounded timeout 结果。
## 写入语言
- Issue/PR 的正式写入一律使用中文,包括标题、正文、评论、关闭/重开说明、PR 描述、PR 评论和 merge closeout 说明。
- 命令、路径、trace/session/job id、API path、代码标识符、英文专有名词、原始日志摘录和外部错误原文可以保留原样;解释性文字、结论、风险、计划和验收说明必须中文。
- 除非用户明确要求外文,不要用英文撰写 issue/PR closeout、进展评论或阻塞说明。
- Markdown 正文、issue/PR 正文和评论里的 issue/PR 引用必须写成 `[#<number>](https://github.com/<owner>/<repo>/issues/<number>)``[#<number>](https://github.com/<owner>/<repo>/pull/<number>)`,显示短号、链接目标保留完整 URL;不要显示裸长链接、裸井号编号或 `owner/repo` 加井号编号。`owner/repo#number` 只允许作为 CLI 命令参数 shorthand。
---
## Issue 目标分支 / Lane
- 创建 issue、补充正式 issue 评论、关闭/重开说明或把 trace 发现的问题登记成 issue 时,正文必须显式写明目标合并分支或运行 lane。
- 推荐固定字段:`目标合并分支: <repo> <branch/lane>`;例如 `目标合并分支: HWLAB v0.2``目标合并分支: AgentRun v0.1``目标合并分支: UniDesk master`
- 如果该 issue 只记录外部依赖、运维观察或不需要代码合并,仍必须写 `目标合并分支: 不适用`,并用一句话说明原因。
- 看到用户或上下文已明确目标 lane 时必须照写;不明确时按目标仓库长期 source truth 选择:HWLAB 按当前 runtime lane(如 `v0.2`/`v0.3`)、AgentRun 默认 `v0.1`、UniDesk 默认 `master`。不能确定时写 `目标合并分支: 待确认` 并说明待确认点,不能省略字段。
---
## 计划与评论分流
- 大计划、改进计划、后续工作拆解、独立验收标准或会形成多阶段执行的内容,必须创建新的 issue 承载;不要在已有 issue 评论区开长计划。
- 既有 issue 评论只写短进展、关键证据、当前结论、阻塞和新 issue 链接;评论用于串联时间线,不承载新的大范围计划正文。
- 如果调查中发现了独立改进方向,应先用 `gh issue create --body-stdin` 创建新 issue,标题和正文写清目标合并分支/lane、背景、计划、验收标准;然后在原 issue 评论中用 1-3 句说明已拆出,并链接到新 issue。
- 只有用户明确要求把计划写回当前 issue 正文,或当前 issue 本身就是唯一的专题计划 issue,才允许更新当前 issue 正文;即便如此,评论仍保持短小,不复制整篇计划。
---
## 认证探测
```bash
bun scripts/cli.ts gh auth status [--repo owner/name]
```
探测 token 来源(`GH_TOKEN`/`GITHUB_TOKEN`/`gh auth token`)、GitHub REST egress、repo 可见性、issue 可读性。不打印 token。
## 聚焦帮助
具体子命令用法优先直接查 scoped help,不要先打开顶层长 help 再人工搜索:
```bash
bun scripts/cli.ts gh issue close --help
bun scripts/cli.ts gh issue comment --help
bun scripts/cli.ts gh pr merge --help
```
`gh <subcommand> --help``gh <subcommand> -h``gh <subcommand> help` 只输出匹配命令或命令组的 bounded JSON,包括相关 usage、短 notes 和完整 help 入口;`gh --help` / `gh help` 才输出完整顶层命令索引。
---
## Issue 命令
### 列表
```bash
bun scripts/cli.ts gh issue list [owner/repo] \
[--state open|closed|all] [--limit N] [--search text] [--title-prefix text] \
[--label label[,label...]] [--repo owner/name] \
[--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]
```
默认 `state=open``limit=30``owner/repo` 位置参数等价 `--repo``--search` 走 GitHub Search API 做查重。`--title-prefix` 在当前有界结果内按 issue 标题前缀做本地过滤,输出 `titleFilter` 的输入/输出/过滤数量,适合 `[FEEDBACK]` 去重:
```bash
bun scripts/cli.ts gh issue list --repo pikasTech/unidesk --state all \
--search "[FEEDBACK]" --title-prefix "[FEEDBACK]" \
--json number,title,state,url
```
### 查看
```bash
bun scripts/cli.ts gh issue view <number|url|owner/repo#number> \
[--repo owner/name] [--json body,title,state,closed,closedAt,comments,commentCount] [--raw|--full]
```
`read` 是兼容别名。支持 `owner/repo#number` shorthand(如 `pikasTech/HWLAB#1024`)。
人工读取 issue/PR 正文优先走 `trans gh:/owner/repo/issue/<number> cat|rg``trans gh:/owner/repo/pr/<number> cat|rg``gh issue view/read` 主要保留为结构化 JSON 底座、metadata 读取和兼容入口。
`--json comments` 默认只返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview`--full` 仍保持评论列表有界,只有 `--raw` 会显式展开所有评论正文。读取单条完整 comment body 使用 `gh issue comment view <commentId> --full`,显式 `--json` 路径的 comments 位于 `.data.json.comments`,不要依赖顶层重复 comments。
### 创建
```bash
bun scripts/cli.ts gh issue create \
--repo owner/name \
--title "标题" \
--label "bug,v0.2" \
--body-stdin <<'EOF'
目标合并分支: HWLAB v0.2
正文(Markdown,支持换行、反引号、表格)
EOF
```
`--body-stdin` 是 heredoc/stdin 第一等入口。`--dry-run` 只输出计划不写 GitHub。
### 更新正文
```bash
# 替换正文
bun scripts/cli.ts gh issue update <number> --repo owner/name \
--mode replace --body-stdin <<'EOF'
新正文
EOF
# 追加正文
bun scripts/cli.ts gh issue update <number> --repo owner/name \
--mode append --body-stdin <<'EOF'
追加内容
EOF
```
`edit``update --mode replace` 的兼容别名。正式写入默认先读当前 issue 做 body guard。
### 局部修补正文(优先走 `trans gh:`
```bash
# Issue 正文:route 未写 /1 时默认一楼正文
trans gh:/owner/name/issue/<number> apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
# PR 正文同样使用 body.md
trans gh:/owner/name/pr/<number> apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
```
GitHub issue/PR 正文局部修补必须优先走 `trans gh:/owner/repo/issue/<number> apply-patch``trans gh:/owner/repo/pr/<number> apply-patch`;虚拟文件固定是 `body.md``apply_patch``patch-apply` 仅作为兼容别名。底层 `bun scripts/cli.ts gh issue patch ... --body-patch-stdin` 是受控写回底座或兼容入口,不作为人工首选。`trans gh:` 会先读取当前正文,把 Codex `apply_patch` envelope 应用到 `body.md`,再通过现有 `gh issue/pr update` guard 写回;普通小补丁不需要固定先跑 `--dry-run`,高风险正文预览时才加 `--dry-run`
### 评论
```bash
# 创建评论
bun scripts/cli.ts gh issue comment create <number|owner/repo#number> \
--repo owner/name --body-stdin <<'EOF'
评论正文
EOF
# 读取单条评论
bun scripts/cli.ts gh issue comment view <commentId> \
--repo owner/name [--full|--raw]
# 原地修正评论
bun scripts/cli.ts gh issue comment update <commentId> \
--repo owner/name --body-stdin <<'EOF'
新的评论正文
EOF
# 局部修补评论
bun scripts/cli.ts gh issue comment patch <commentId> \
--repo owner/name --body-patch-stdin <<'PATCH'
*** Begin Patch
*** Update File: comment.md
@@
-old text
+new text
*** End Patch
PATCH
# 删除评论
bun scripts/cli.ts gh issue comment delete <commentId>
```
`view` 默认返回 comment id、URL、作者、时间、正文字符数、body SHA 和短 preview`--full` 只展开这一条 comment body。`edit``comment update` 的兼容别名。`--body <short-text>` 仅适合短单行。日常修正文案优先用 `patch``update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。`comment patch` 会先读取 comment id 对应的当前正文,把 envelope 应用到虚拟文件 `comment.md`,再 PATCH 单条评论;上下文不匹配时失败且不写入。
评论区不要写新的大计划。若评论草稿已经包含多阶段计划、改进清单或验收标准,改为创建新 issue,并在评论中只留下新 issue 链接和短结论。
### 关闭/重开
```bash
bun scripts/cli.ts gh issue close <number|owner/repo#number> \
--repo owner/name [--comment "关闭原因"]
bun scripts/cli.ts gh issue reopen <number|owner/repo#number> \
--repo owner/name [--comment "重开原因"]
```
附长证据时先用 `comment create` 写证据评论,再用 `close --comment <短引用>` 关闭。不支持 `delete`(走 `close`)。
### 批量过期关闭
```bash
bun scripts/cli.ts gh issue stale-close \
--repo owner/name [--inactive-hours 48] [--limit N] \
[--label label] [--dry-run]
```
关闭 `updatedAt < observedAt - inactiveHours` 的 open issue。先 `--dry-run` 观察,再正式执行。
### 转义扫描
```bash
bun scripts/cli.ts gh issue scan-escape \
--repo owner/name [--limit N] [--dry-run]
```
只读扫描 issue 正文/评论中的字面量 `\n``\t`、ANSI escape 等污染。
---
## PR 命令
### 列表
```bash
bun scripts/cli.ts gh pr list [owner/repo] \
[--state open|closed|all] [--json ...] [--raw|--full]
```
### 查看
```bash
bun scripts/cli.ts gh pr view <number|url|owner/repo#number> \
[--repo owner/name] \
[--json body,title,state,stateDetail,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] \
[--raw|--full]
```
`stateDetail` 归一化为 `open|closed|merged``mergeable`/`mergeStateStatus`/`statusCheckRollup` 走 GraphQL。
### 文件变更
```bash
bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]
```
返回 changed files 统计,不输出 raw diff。`gh pr diff <number> --stat` 是兼容别名。
### 收口预检
```bash
bun scripts/cli.ts gh pr preflight <number|owner/repo#number> \
[--repo owner/name] [--full|--raw]
```
`preflight`/`closeout` 是别名。只读检查 state/draft/conflict/mergeable/statusCheck,不写 GitHub。默认只给 status check 计数与失败预览。
### 创建
```bash
bun scripts/cli.ts gh pr create \
--repo owner/name \
--title "标题" \
--body-stdin --base master --head <branch> \
[--draft] [--dry-run] <<'EOF'
PR 正文
EOF
```
`pr create` 成功后的 Next 默认提示使用 `gh pr merge ... --merge --delete-branch`。只有确认不需要保留 ancestry 或 merge parent history 时,才显式改用 `--squash`
### 合并
```bash
bun scripts/cli.ts gh pr merge <number> \
[--repo owner/name] [--merge|--squash|--rebase] \
[--delete-branch] [--dry-run]
```
一键 guarded merge`gh pr merge` 自己读取 closeout metadata、mergeability、mergeStateStatus 和 status checks,不要求用户先跑 `gh pr preflight``preflight`/`closeout` 只作为只读诊断入口保留。
当 GitHub GraphQL 返回 `mergeable=UNKNOWN/null``mergeStateStatus=UNKNOWN/null` 或 status rollup unknown 且没有明确 blockers 时,`gh pr merge` 会按 `config/unidesk-cli.yaml``github.prMerge.unknownRetry` 做指数退避重试,并在默认表格 `RETRY` 列显示 `1/5``2/5` 等尝试次数。重试耗尽仍 unknown 时,命令自动停止且不合并;冲突、draft、失败检查、pending check 等不可合并状态不会无意义重试。已 merged 返回 `alreadyMerged=true`
### 开发任务收口
完成开发任务后必须完整收口:创建 PR、通过受控 `gh pr merge` 合并 PR、删除远程任务分支、删除本地任务 worktree,并把主 worktree 快进到最新 `master`。远程分支优先通过 `gh pr merge ... --delete-branch` 删除;若合并后远端分支仍存在,再显式 `git push origin --delete <branch>`
删除本地 worktree 前先确认工作区 clean,且任务变更已经进入 `origin/master`squash merge 场景下不能只看本地分支是否是 `origin/master` 祖先,还要用 PR merged 状态、`git cherry` 等价补丁或关键文件 diff 确认语义已吸收。确认后执行 `git worktree remove .worktree/<task>`;如本地任务分支仅用于该 worktree 且已语义合入,可同步删除本地分支。
最后在主 worktree 执行 `git fetch origin master``git pull --ff-only origin master`,让主 worktree 回到最新 `master`。如果主 worktree 存在并行脏改、非 `master` 分支或其他原因导致不能安全 fast-forward,禁止 `reset``stash``checkout --` 或清理他人改动;改用干净 `origin/master` worktree 继续后续工具命令,并在最终回复中明确说明主 worktree 未更新的原因。
### 编辑 / 评论 / 关闭 / 重开
```bash
bun scripts/cli.ts gh pr update <number> --mode replace|append --body-stdin [--title ...]
bun scripts/cli.ts gh pr comment create <number> --body-stdin
bun scripts/cli.ts gh pr comment update <commentId> --body-stdin
bun scripts/cli.ts gh pr comment edit <commentId> --body-stdin
bun scripts/cli.ts gh pr comment delete <commentId>
bun scripts/cli.ts gh pr close <number> [--comment ...]
bun scripts/cli.ts gh pr reopen <number> [--comment ...]
```
`pr comment edit``pr comment update` 的兼容别名。与 issue 对应命令行为一致;PR 评论也是 GitHub issue comment,更新目标使用 commentId。
---
## 虚拟文件系统 (`trans gh:`)
```bash
# 列出 repo
trans gh:/pikasTech/HWLAB ls
# 列出 PR / Issue
trans gh:/pikasTech/HWLAB/pr ls [--state open|closed|all] [--limit N] [--full]
trans gh:/pikasTech/HWLAB/issue ls [--state open|closed|all] [--limit N] [--search TEXT] [--label L] [--full]
# 读取 / 检索正文
trans gh:/pikasTech/HWLAB/issue/1236 cat
trans gh:/pikasTech/HWLAB/issue/1236 rg 'API_KEY|YAML-first'
# 查看单个条目
trans gh:/pikasTech/HWLAB/pr/507 ls
trans gh:/pikasTech/HWLAB/505/1 cat
# apply-patch 写回正文(走 gh issue/pr update
trans gh:/pikasTech/HWLAB/pr/507 apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
old line
-old line
+new line
more context
*** End Patch
PATCH
```
正文一楼映射为 `body.md`route 未写 `/1` 时默认一楼正文。`issue ls --search/--state` 用于替代常见 `bun scripts/cli.ts gh issue list ... --search/--state` 读取;单条正文优先用 `cat``rg`,不要为了看 body 再走 `gh issue view --json body --full`。写回走 `gh issue/pr update` 的 guard 规则。`apply-patch` 是首选 operation`apply_patch``patch-apply` 只作为兼容别名;普通小补丁在已读取上下文后可以直接 apply,`--dry-run` 只作为高风险正文的可选预览。`rm` 对正文结构化拒绝。
---
## 看板命令 ([#20](https://github.com/pikasTech/unidesk/issues/20) 总看板)
```bash
# 结构审计
bun scripts/cli.ts gh issue board-audit \
--repo pikasTech/unidesk --board-issue 20 [--dry-run]
# 行列表
bun scripts/cli.ts gh issue board-row list \
--board-issue 20 [--state open|closed|all]
# 行查看
bun scripts/cli.ts gh issue board-row get <issueNumber> --board-issue 20
# 行更新(单字段)
bun scripts/cli.ts gh issue board-row update <issueNumber> \
--board-issue 20 \
--field progress|status|validation|branch|tasks|focus \
--value <text> \
[--expect-body-sha <sha>|--expect-updated-at <ts>]
# 行新增/插入(upsert
bun scripts/cli.ts gh issue board-row upsert <issueNumber> \
--board-issue 20 --section open|closed \
--branch <branch> --tasks <task> --summary <text> \
--focus <text> --validation <text> --progress <text> \
[--expect-body-sha <sha>|--expect-updated-at <ts>]
# 行移动 / 删除
bun scripts/cli.ts gh issue board-row move <issueNumber> \
--board-issue 20 --to open|closed [--status OPEN|CLOSED]
bun scripts/cli.ts gh issue board-row delete <issueNumber> \
--board-issue 20
```
写操作默认 dry-run,正式 PATCH 必须带 `--expect-body-sha``--expect-updated-at`
---
## 关键约定
### heredoc 优先
多行正文/评论一律用 `--body-stdin <<'EOF'`quoted heredoc),不用 `--body` 内联长文本、不用临时文件、不用 `gh api -f body=...`
```bash
bun scripts/cli.ts gh issue create --repo pikasTech/HWLAB \
--title "标题" --body-stdin <<'EOF'
正文,支持 `code`、**bold**、表格等 Markdown
EOF
```
### owner/repo#number shorthand
所有接受 issue/PR number 的命令都支持:
```bash
bun scripts/cli.ts gh issue view pikasTech/HWLAB#1024
bun scripts/cli.ts gh pr preflight pikasTech/HWLAB#1020
bun scripts/cli.ts gh issue comment create pikasTech/HWLAB#1024 --body-stdin <<'EOF'
...
EOF
```
shorthand 与显式 `--repo` 冲突时结构化失败。
### 高风险写入预览
整篇 replace、批量 lifecycle、PR merge 或并发敏感正文写入可先 `--dry-run``trans gh:/... apply-patch` 的普通小补丁在已读取上下文后不需要固定 dry-run:
```bash
bun scripts/cli.ts gh issue update 20 --mode append --body-stdin --dry-run <<'EOF'
...
EOF
```
### debug 任务入口
```bash
bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run ...] [--pr-create-dry-run ...] [--issue N] [--full|--raw]
```
用于 PR 型派单 admission,检查 scheduler auth、runner GH token、git worktree、GitHub egress 等。
+15 -65
View File
@@ -5,74 +5,24 @@ description: UniDesk 项目管理运行技能。用户提到 UniDesk 项目管
# UniDesk OA
使用本技能时,把 HWLAB Cloud M1 的项目管理稳定地锚定在 UniDesk 仓库内的项目编号目录和相互链接的 GitHub issue 执行记录上。
HWLAB Cloud M1 / UniDesk / AgentRun 跨仓项目治理锚定在 UniDesk 仓库的 `project-management/` 项目编号目录和 GitHub issue 执行记录上。
## 仓库
## 高频规则
- UniDesk 项目管理真相源:`/root/unidesk/project-management/PJ2026-01`,其中 `specs/PJ2026-01-HWLAB.md` 是 L0 总规格和项目索引,随 `pikasTech/unidesk``master` 分支版本化。AgentRun 作为 HWLAB Agent编排的执行基础设施归入 `PJ2026-0102` 及其平台运维支撑规格,不单独创建新的 L0 项目
- 迁移前 HWLabOA 仓库:`/root/HWLabOA` 只作为历史材料和对照来源,不再作为 HWLAB Cloud M1 规格真相源
- 使用 UniDesk GitHub CLI、修改本技能或修改项目管理文档前,必须在 `/root/unidesk` 执行 `git status --short --branch``git pull --ff-only origin master`
- 项目管理 stable source of truth 在 `project-management/PJ2026-01/`GitHub issue 承载执行流和证据索引
- 重大项目/方向/课题/子课题层级、任务书、实施方案、测试规格、阶段报告和项目偏离按项目编号目录维护
- 形成多阶段实施、架构或长期 API/数据模型时,先 SPEC,再实现和 issue 阶段计划
- GitHub issue/PR 写入仍走 `$unidesk-gh`;正文必须写目标合并分支/lane。
## 运行模型
## Reference 路由
- `project-management/PJ2026-01` 下的 Markdown 视为长期规格真相源
- 将 GitHub issue 视为执行控制面、历史讨论入口和长证据承载处:状态、讨论、跨仓引用、PR 链接和收口证据可以在 issue 中流转,但规格正文不得只写在 issue 中
- 长证据、trace、CaseRun registry、运行日志、历史 issue 摘要和证据索引保留在 GitHub issue 或具体执行 issue 中,禁止以 `evidence/` 目录或长证据文件形式污染 `project-management/PJ2026-01`
- `project-management/PJ2026-01/specs/*.md` 规格文件不保留单独的迁移来源块;历史来源只写在修改历史 `v0.1` 的变更说明中,格式为 `迁移来源 <owner>/<repo>#<number>`。规格文件引用其他规格必须使用同目录相对路径 Markdown 链接,禁止引用其他规格的 GitHub issue、证据 issue、PR 或裸 `#<number>`;唯一例外是第 7 章过程控制可保留跨 L1 阶段验证 issue 索引,用于内测、灰度和用户反馈分流,不承载问卷正文、参与者资料、原始反馈、长证据或执行流水。
- AgentRun 仓库内 `docs/reference/spec-v01-*.md``docs/reference/architecture.md` 不再承载规格正文;迁移后只保留指向 UniDesk OA 规格的交叉引用 stub,避免 AgentRun repo 与 `project-management/PJ2026-01` 双份规格漂移。AgentRun 专项内容按职责落到 `project-management/PJ2026-01/specs/PJ2026-0102-agent-orchestration.md``project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md``project-management/PJ2026-01/specs/PJ2026-010602-source-sync.md``project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md``project-management/PJ2026-01/specs/PJ2026-010604-public-entry.md``project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md` 及其 L3,不创建 `PJ2026-02`
- HWLAB v0.2/v0.3 仓库内 `docs/reference/spec-*` 以及 `cloud-workbench.md``code-agent-chat-readiness.md``g14-gitops-cicd.md` / `node-gitops-cicd.md``dev-runtime-boundary.md``gateway-outbound-demo.md``MVP-e2e-acceptance.md``architecture.md` 已收编为 UniDesk OA 交叉引用或历史 stub;这些路径只保留链接兼容和运行参考,不能再作为需求规格正文来源。需要变更 Web/CLI/API、Code Agent、runtime boundary、公开入口、测试大纲或 Gateway 主动出站需求时,先更新 `project-management/PJ2026-01/specs/` 和相应测试/过程 issue。
- 写入 GitHub issue/PR 正文或评论时,issue/PR 引用必须写成 `[#<number>](https://github.com/<owner>/<repo>/issues/<number>)``[#<number>](https://github.com/<owner>/<repo>/pull/<number>)`,显示短号、链接目标保留完整 URL;不要显示裸长链接、裸井号编号或 `owner/repo` 加井号编号。CLI 参数中的 `owner/repo#number` shorthand 只作为命令输入例外。
- 不要让日报或阶段报告成为总规划。阶段报告只总结相对总规格的移动,不能替代中心规划。
- 当任务缺少上级方向、验收标准或原始验证入口时,先归类为规划/调查,不要直接变成实现任务。
- L1 方向必须是直接服务 L0 使命的能力域。文档整理、阶段报告、项目管理机制、看板维护、技能维护、仓库名、工具名和临时执行路径都不能作为 L1 方向。
- L1 是验收主责,不是宽泛标签。跨方向 issue 可以列关联 L1,但必须且只能有一个主责 L1,选择依据是哪个方向定义完成标准。
- 重大规划型 issue 必须 P0 SPEC-firstP0 阶段先在 `project-management/PJ2026-01/specs/` 维护对应 SPEC,明确 SPEC 编号、上级/关联规格、架构图、数据流图、关键时序图和代码引用规则,再进入后续实现。issue 正文只能承载执行计划和证据,不能替代 SPEC 正文。
- 项目规划、层级职责、issue 锚点和工作流的完整旧规则在 [references/full.md](references/full.md)
- 项目规划细则读 [references/project-planning.md](references/project-planning.md)
- 模板读 [references/templates.md](references/templates.md)
## 需求规格写作规则
## 常用检查
- 需求规格是对外系统能力、边界和职责说明,不写内部治理、issue 关闭规则、长期面板、阶段报告、执行控制面、单次验证流水、PR 过程或证据堆叠。
- 文档控制表只保留规格自身必要字段;状态只能写 `已生效``已废弃``未生效`。不要在文档控制表里写长期面板、阶段规格、L1 划分、规格来源或迁移来源。
- 修改历史只记录已经定稿的版本。用户未明确说“可以定稿”前,不新增 `待提交` 版本号,也不把每次小编辑拆成一个版本。
- L0、L1、L2 需求规格正文固定使用裁剪后的 1-7 章结构:文档控制、目的和范围、术语表、系统边界和接口、内部分工与规格索引、原子需求、过程控制。第 7 章只索引阶段验证、内测、灰度或用户反馈分流 issue;不要在 SPEC 正文追加假设和风险、变更控制、开放缺口、验收标准或执行过程流水。
- 涉及新能力、跨模块架构、用户可见工作流、长期 API/数据模型、平台运维能力或多阶段实施的 SPEC,必须在正文中包含目标架构图、数据流图和关键时序图;图形优先使用 Markdown 内嵌 `mermaid`,并和原子需求编号互相对应。
- L0 系统边界必须把 HWLAB 作为完整系统看待,描述外部使用者、外部输入、受控资源、外部输出、用户接口和系统责任边界,不写内部治理材料。
- 稳定概念用术语表表达;不要写没有判定价值的“运行概念”流水。
- L0 的 L1 方向树和项目规格索引合并为内部模块分工与规格索引,用相对路径索引到每个内部模块规格文档。
- 原子需求每条独立成节,信息表使用横向表格且只放短元数据,推荐列为 `编号 | 短名 | 主责模块 | 关联模块`。主责模块必须写下一层主责单元并带项目编号:L0 原子需求写 L1,L1 原子需求写 L2,L2 原子需求写 L3;禁止在 L1/L2 原子需求里把主责模块写成本规格本级。禁止把需求句、职责划分、验收口径或大段说明塞进表格,正文必须承载需求定义、职责边界、意图和范围。
- 不要为了凑全局原子需求条数自动添加次要派生能力。证据收集、硬编码诊断、硬编码建议、任务状态收集、结果包/结果收集等需求,除非用户明确授意,不得写入 OA 规格或 L0 原子需求。
- 平台运维在 L0 中只能作为支撑后勤职责出现;对外需求应写成系统可用性、可恢复、资源可控等用户可感知能力,不写成“HWLAB 对外提供平台运维能力”。
- 单一主责、关闭验收、规格沉淀、回写和偏离判定是治理规则,不得伪装成 L0 原子需求。
- L2 是稳定能力域,不是仓库文件名或原始 spec 文档一比一搬运目录。单个 L1 下的 L2 通常不超过 6 个;从仓库参考文档迁移过来的大量细项应合并到少量 L2,并把更细的服务、lane、source truth、验证切片放到 L3 或正文分工中。
- 运维交付内容必须按职责归位:通用 CI/CD、发布判定、镜像 promotion 放到平台运维的发布流水;通用 Git mirror、source truth、GitOps branch、artifact catalog 放到源码同步;Secret/YAML/sourceRef/targetKey/fingerprint 放到 YAML运维。只有能抽象到多服务共用的规则才放通用 L2;AgentRun 固定 branch、namespace、Pipeline、GitOps path、真实 provider turn 等专项细节放到这些通用规格下的 AgentRun 专项 L3`PJ2026-0102 Agent编排` 只引用这些支撑规格,不把运维后勤 L2 塞进 Agent编排。
- 大规划型 issue 范围内新增或修改的源码文件,文件头部必须标注遵循的 SPEC 编号、短名和实现引用版本,例如 `SPEC: PJ2026-01060505 Workbench性能 draft-2026-06-17-p0`;不得只写 issue 编号、`latest``current`。自动生成文件、第三方 vendored 文件、纯配置、锁文件和无法承载注释头的二进制产物可例外,但对应生成器、渲染器或配置入口必须可追溯到 SPEC。
## HWLAB 标准 issue 锚点
- L0 总规格:`project-management/PJ2026-01/specs/PJ2026-01-HWLAB.md`,历史 issue `[#1194](https://github.com/pikasTech/HWLAB/issues/1194)`
- AgentRun 规格入口:`project-management/PJ2026-01/specs/PJ2026-0102-agent-orchestration.md`;核心 L2 为 `PJ2026-010201` AgentRun核心、`PJ2026-010202` Runtime装配、`PJ2026-010203` 队列会话、`PJ2026-010204` 后端Profile、`PJ2026-010205` HWLAB接入。平台运维支撑入口为 `PJ2026-010601` 发布流水、`PJ2026-010602` 源码同步、`PJ2026-010603` YAML运维、`PJ2026-010604` 公开入口和 `PJ2026-010605` 可观测监控;其中 AgentRun 专项 L3 只承载 AgentRun 固定 lane/source 事实,D601 v0.3 用户入口等跨模块 public entry truth 归 `PJ2026-010604`
- 长期导航面板:`[#645](https://github.com/pikasTech/HWLAB/issues/645)``HWLAB 长期总面板`)。这是面板,不是规格书。
- 当前 Cloud M1 阶段规格:`project-management/PJ2026-01/specs/stage-cloud-spec-20260601.md`,历史 issue `[#644](https://github.com/pikasTech/HWLAB/issues/644)`
- 创建或更新 HWLAB 项目管理 issue 时,非平凡 L1/L2/L3/L4 工作都要链接回 `project-management/PJ2026-01` 中的对应规格文件,并保持 `[#645](https://github.com/pikasTech/HWLAB/issues/645)` 只作为导航索引。
- 所有管理 issue 和规格都使用 HWLAB 层级编号:项目编号格式为 `PJ<YYYY>-<path>``PJ` 表示项目,`2026` 是立项年份,HWLAB L0 是 `PJ2026-01`L1 编号是 `PJ2026-0101``PJ2026-0102` 等;L2 继续追加两位,例如 `PJ2026-010102` 表示 HWLAB L1 第 1 个方向下的第 2 个 L2 课题。稳定节点短名通常控制在 8 个中文汉字以内,解释写在正文,不写进名称。
## 工作流
1. 做规划类问题时,读取 `references/project-planning.md`
2. 创建或更新总规格、issue 正文时,读取 `references/templates.md`
3. 写入 GitHub issue/PR 评论时,同时使用 `unidesk-gh`;所有写入必须从 `/root/unidesk` 通过 `bun scripts/cli.ts gh ...` 完成。
4. 修改长期项目管理文档时,把稳定规划内容放到 `project-management/PJ2026-01` 的总规格/规格文件,阶段报告只保留带日期的总结。
5. 修订 L1/L2 层级时,先按完成标准判断主责,再看实现表面。工具、CLI、Web、API 形态可以形成关联 L1,但主责 L1 必须是定义完成标准的能力域。
6. 更新重大规划型 issue 时,先把 P0 SPEC 文件和图维护到位,再用 `unidesk-gh` 回写 issue;后续代码任务必须在 issue 中引用 SPEC 编号和实现引用版本。
## 必需层级和职责
除非用户给出更严格的层级,否则使用以下任务树:
- L0:重大项目 / 总规格。负责使命、范围、系统边界、内部模块分工与规格索引、全局原子需求。
- L1:方向。负责一个稳定能力域、范围边界、成功标准、L2 课题清单和子工作验收定义权。
- L2:课题。负责某个 L1 内的具体工作计划,包括交付物、阻塞项和验证计划。
- L3:子课题 / 验收切片。负责一个有界验收路径,例如一条 CaseRun 线、Web 检查、CLI smoke 或部署/控制面验证。
- L4:执行任务 / PR / CaseRun / smoke / 文档收口。负责具体执行和证据收集,不能重新定义父级范围。
每个非平凡 issue 都应写明上级链接、目标分支/lane、验收标准、验证入口和收口回写目标。
重大规划型 issue 还必须把“更新并确认 SPEC”列为 P0,且 P0 未完成前不得推进代码实现。
```bash
find project-management/PJ2026-01 -maxdepth 3 -type f | sort
bun scripts/cli.ts gh issue list --repo pikasTech/unidesk --state open --limit 30
```
@@ -0,0 +1,78 @@
---
name: unidesk-oa
description: UniDesk 项目管理运行技能。用户提到 UniDesk 项目管理目录、HWLAB OA、总项目规格、总规划、重大项目/方向/课题/子课题层级、任务书、实施方案、测试规格、阶段报告、项目偏离,或通过 project-management 与 GitHub issue 管理 HWLAB Cloud M1 / UniDesk / AgentRun 跨仓项目治理时使用。
---
# UniDesk OA
使用本技能时,把 HWLAB Cloud M1 的项目管理稳定地锚定在 UniDesk 仓库内的项目编号目录和相互链接的 GitHub issue 执行记录上。
## 仓库
- UniDesk 项目管理真相源:`/root/unidesk/project-management/PJ2026-01`,其中 `specs/PJ2026-01-HWLAB.md` 是 L0 总规格和项目索引,随 `pikasTech/unidesk``master` 分支版本化。AgentRun 作为 HWLAB Agent编排的执行基础设施归入 `PJ2026-0102` 及其平台运维支撑规格,不单独创建新的 L0 项目。
- 迁移前 HWLabOA 仓库:`/root/HWLabOA` 只作为历史材料和对照来源,不再作为 HWLAB Cloud M1 规格真相源。
- 使用 UniDesk GitHub CLI、修改本技能或修改项目管理文档前,必须在 `/root/unidesk` 执行 `git status --short --branch``git pull --ff-only origin master`
## 运行模型
-`project-management/PJ2026-01` 下的 Markdown 视为长期规格真相源。
- 将 GitHub issue 视为执行控制面、历史讨论入口和长证据承载处:状态、讨论、跨仓引用、PR 链接和收口证据可以在 issue 中流转,但规格正文不得只写在 issue 中。
- 长证据、trace、CaseRun registry、运行日志、历史 issue 摘要和证据索引保留在 GitHub issue 或具体执行 issue 中,禁止以 `evidence/` 目录或长证据文件形式污染 `project-management/PJ2026-01`
- `project-management/PJ2026-01/specs/*.md` 规格文件不保留单独的迁移来源块;历史来源只写在修改历史 `v0.1` 的变更说明中,格式为 `迁移来源 <owner>/<repo>#<number>`。规格文件引用其他规格必须使用同目录相对路径 Markdown 链接,禁止引用其他规格的 GitHub issue、证据 issue、PR 或裸 `#<number>`;唯一例外是第 7 章过程控制可保留跨 L1 阶段验证 issue 索引,用于内测、灰度和用户反馈分流,不承载问卷正文、参与者资料、原始反馈、长证据或执行流水。
- AgentRun 仓库内 `docs/reference/spec-v01-*.md``docs/reference/architecture.md` 不再承载规格正文;迁移后只保留指向 UniDesk OA 规格的交叉引用 stub,避免 AgentRun repo 与 `project-management/PJ2026-01` 双份规格漂移。AgentRun 专项内容按职责落到 `project-management/PJ2026-01/specs/PJ2026-0102-agent-orchestration.md``project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md``project-management/PJ2026-01/specs/PJ2026-010602-source-sync.md``project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md``project-management/PJ2026-01/specs/PJ2026-010604-public-entry.md``project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md` 及其 L3,不创建 `PJ2026-02`
- HWLAB v0.2/v0.3 仓库内 `docs/reference/spec-*` 以及 `cloud-workbench.md``code-agent-chat-readiness.md``g14-gitops-cicd.md` / `node-gitops-cicd.md``dev-runtime-boundary.md``gateway-outbound-demo.md``MVP-e2e-acceptance.md``architecture.md` 已收编为 UniDesk OA 交叉引用或历史 stub;这些路径只保留链接兼容和运行参考,不能再作为需求规格正文来源。需要变更 Web/CLI/API、Code Agent、runtime boundary、公开入口、测试大纲或 Gateway 主动出站需求时,先更新 `project-management/PJ2026-01/specs/` 和相应测试/过程 issue。
- 写入 GitHub issue/PR 正文或评论时,issue/PR 引用必须写成 `[#<number>](https://github.com/<owner>/<repo>/issues/<number>)``[#<number>](https://github.com/<owner>/<repo>/pull/<number>)`,显示短号、链接目标保留完整 URL;不要显示裸长链接、裸井号编号或 `owner/repo` 加井号编号。CLI 参数中的 `owner/repo#number` shorthand 只作为命令输入例外。
- 不要让日报或阶段报告成为总规划。阶段报告只总结相对总规格的移动,不能替代中心规划。
- 当任务缺少上级方向、验收标准或原始验证入口时,先归类为规划/调查,不要直接变成实现任务。
- L1 方向必须是直接服务 L0 使命的能力域。文档整理、阶段报告、项目管理机制、看板维护、技能维护、仓库名、工具名和临时执行路径都不能作为 L1 方向。
- L1 是验收主责,不是宽泛标签。跨方向 issue 可以列关联 L1,但必须且只能有一个主责 L1,选择依据是哪个方向定义完成标准。
- 重大规划型 issue 必须 P0 SPEC-firstP0 阶段先在 `project-management/PJ2026-01/specs/` 维护对应 SPEC,明确 SPEC 编号、上级/关联规格、架构图、数据流图、关键时序图和代码引用规则,再进入后续实现。issue 正文只能承载执行计划和证据,不能替代 SPEC 正文。
## 需求规格写作规则
- 需求规格是对外系统能力、边界和职责说明,不写内部治理、issue 关闭规则、长期面板、阶段报告、执行控制面、单次验证流水、PR 过程或证据堆叠。
- 文档控制表只保留规格自身必要字段;状态只能写 `已生效``已废弃``未生效`。不要在文档控制表里写长期面板、阶段规格、L1 划分、规格来源或迁移来源。
- 修改历史只记录已经定稿的版本。用户未明确说“可以定稿”前,不新增 `待提交` 版本号,也不把每次小编辑拆成一个版本。
- L0、L1、L2 需求规格正文固定使用裁剪后的 1-7 章结构:文档控制、目的和范围、术语表、系统边界和接口、内部分工与规格索引、原子需求、过程控制。第 7 章只索引阶段验证、内测、灰度或用户反馈分流 issue;不要在 SPEC 正文追加假设和风险、变更控制、开放缺口、验收标准或执行过程流水。
- 涉及新能力、跨模块架构、用户可见工作流、长期 API/数据模型、平台运维能力或多阶段实施的 SPEC,必须在正文中包含目标架构图、数据流图和关键时序图;图形优先使用 Markdown 内嵌 `mermaid`,并和原子需求编号互相对应。
- L0 系统边界必须把 HWLAB 作为完整系统看待,描述外部使用者、外部输入、受控资源、外部输出、用户接口和系统责任边界,不写内部治理材料。
- 稳定概念用术语表表达;不要写没有判定价值的“运行概念”流水。
- L0 的 L1 方向树和项目规格索引合并为内部模块分工与规格索引,用相对路径索引到每个内部模块规格文档。
- 原子需求每条独立成节,信息表使用横向表格且只放短元数据,推荐列为 `编号 | 短名 | 主责模块 | 关联模块`。主责模块必须写下一层主责单元并带项目编号:L0 原子需求写 L1,L1 原子需求写 L2,L2 原子需求写 L3;禁止在 L1/L2 原子需求里把主责模块写成本规格本级。禁止把需求句、职责划分、验收口径或大段说明塞进表格,正文必须承载需求定义、职责边界、意图和范围。
- 不要为了凑全局原子需求条数自动添加次要派生能力。证据收集、硬编码诊断、硬编码建议、任务状态收集、结果包/结果收集等需求,除非用户明确授意,不得写入 OA 规格或 L0 原子需求。
- 平台运维在 L0 中只能作为支撑后勤职责出现;对外需求应写成系统可用性、可恢复、资源可控等用户可感知能力,不写成“HWLAB 对外提供平台运维能力”。
- 单一主责、关闭验收、规格沉淀、回写和偏离判定是治理规则,不得伪装成 L0 原子需求。
- L2 是稳定能力域,不是仓库文件名或原始 spec 文档一比一搬运目录。单个 L1 下的 L2 通常不超过 6 个;从仓库参考文档迁移过来的大量细项应合并到少量 L2,并把更细的服务、lane、source truth、验证切片放到 L3 或正文分工中。
- 运维交付内容必须按职责归位:通用 CI/CD、发布判定、镜像 promotion 放到平台运维的发布流水;通用 Git mirror、source truth、GitOps branch、artifact catalog 放到源码同步;Secret/YAML/sourceRef/targetKey/fingerprint 放到 YAML运维。只有能抽象到多服务共用的规则才放通用 L2;AgentRun 固定 branch、namespace、Pipeline、GitOps path、真实 provider turn 等专项细节放到这些通用规格下的 AgentRun 专项 L3`PJ2026-0102 Agent编排` 只引用这些支撑规格,不把运维后勤 L2 塞进 Agent编排。
- 大规划型 issue 范围内新增或修改的源码文件,文件头部必须标注遵循的 SPEC 编号、短名和实现引用版本,例如 `SPEC: PJ2026-01060505 Workbench性能 draft-2026-06-17-p0`;不得只写 issue 编号、`latest``current`。自动生成文件、第三方 vendored 文件、纯配置、锁文件和无法承载注释头的二进制产物可例外,但对应生成器、渲染器或配置入口必须可追溯到 SPEC。
## HWLAB 标准 issue 锚点
- L0 总规格:`project-management/PJ2026-01/specs/PJ2026-01-HWLAB.md`,历史 issue `[#1194](https://github.com/pikasTech/HWLAB/issues/1194)`
- AgentRun 规格入口:`project-management/PJ2026-01/specs/PJ2026-0102-agent-orchestration.md`;核心 L2 为 `PJ2026-010201` AgentRun核心、`PJ2026-010202` Runtime装配、`PJ2026-010203` 队列会话、`PJ2026-010204` 后端Profile、`PJ2026-010205` HWLAB接入。平台运维支撑入口为 `PJ2026-010601` 发布流水、`PJ2026-010602` 源码同步、`PJ2026-010603` YAML运维、`PJ2026-010604` 公开入口和 `PJ2026-010605` 可观测监控;其中 AgentRun 专项 L3 只承载 AgentRun 固定 lane/source 事实,D601 v0.3 用户入口等跨模块 public entry truth 归 `PJ2026-010604`
- 长期导航面板:`[#645](https://github.com/pikasTech/HWLAB/issues/645)``HWLAB 长期总面板`)。这是面板,不是规格书。
- 当前 Cloud M1 阶段规格:`project-management/PJ2026-01/specs/stage-cloud-spec-20260601.md`,历史 issue `[#644](https://github.com/pikasTech/HWLAB/issues/644)`
- 创建或更新 HWLAB 项目管理 issue 时,非平凡 L1/L2/L3/L4 工作都要链接回 `project-management/PJ2026-01` 中的对应规格文件,并保持 `[#645](https://github.com/pikasTech/HWLAB/issues/645)` 只作为导航索引。
- 所有管理 issue 和规格都使用 HWLAB 层级编号:项目编号格式为 `PJ<YYYY>-<path>``PJ` 表示项目,`2026` 是立项年份,HWLAB L0 是 `PJ2026-01`L1 编号是 `PJ2026-0101``PJ2026-0102` 等;L2 继续追加两位,例如 `PJ2026-010102` 表示 HWLAB L1 第 1 个方向下的第 2 个 L2 课题。稳定节点短名通常控制在 8 个中文汉字以内,解释写在正文,不写进名称。
## 工作流
1. 做规划类问题时,读取 `references/project-planning.md`
2. 创建或更新总规格、issue 正文时,读取 `references/templates.md`
3. 写入 GitHub issue/PR 评论时,同时使用 `unidesk-gh`;所有写入必须从 `/root/unidesk` 通过 `bun scripts/cli.ts gh ...` 完成。
4. 修改长期项目管理文档时,把稳定规划内容放到 `project-management/PJ2026-01` 的总规格/规格文件,阶段报告只保留带日期的总结。
5. 修订 L1/L2 层级时,先按完成标准判断主责,再看实现表面。工具、CLI、Web、API 形态可以形成关联 L1,但主责 L1 必须是定义完成标准的能力域。
6. 更新重大规划型 issue 时,先把 P0 SPEC 文件和图维护到位,再用 `unidesk-gh` 回写 issue;后续代码任务必须在 issue 中引用 SPEC 编号和实现引用版本。
## 必需层级和职责
除非用户给出更严格的层级,否则使用以下任务树:
- L0:重大项目 / 总规格。负责使命、范围、系统边界、内部模块分工与规格索引、全局原子需求。
- L1:方向。负责一个稳定能力域、范围边界、成功标准、L2 课题清单和子工作验收定义权。
- L2:课题。负责某个 L1 内的具体工作计划,包括交付物、阻塞项和验证计划。
- L3:子课题 / 验收切片。负责一个有界验收路径,例如一条 CaseRun 线、Web 检查、CLI smoke 或部署/控制面验证。
- L4:执行任务 / PR / CaseRun / smoke / 文档收口。负责具体执行和证据收集,不能重新定义父级范围。
每个非平凡 issue 都应写明上级链接、目标分支/lane、验收标准、验证入口和收口回写目标。
重大规划型 issue 还必须把“更新并确认 SPEC”列为 P0,且 P0 未完成前不得推进代码实现。
+17 -258
View File
@@ -3,274 +3,33 @@ name: unidesk-ops
description: UniDesk 手动运维 CLI — `server``gc` 和 PK01 `platform-db postgres` 子命令,覆盖主 server 启停、健康检查、swap、日志、Docker 镜像清理、磁盘 GC、服务重建/重启和 PK01 host PostgreSQL 运维。用户提到 server start、server status、server swap、server rebuild、server restart、gc、磁盘清理、platform-db、PK01 PostgreSQL、运维时使用。
---
# UniDesk 手动运维 CLI
# UniDesk Ops
主 server 运维入口,通过 `bun scripts/cli.ts server ...``bun scripts/cli.ts gc ...` 操作
主 server、GC 和 PK01 PostgreSQL 的手动运维入口。常规操作走 `bun scripts/cli.ts server ...``gc ...``platform-db postgres ...`
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts ...`
---
## 启停
## 高频入口
```bash
bun scripts/cli.ts server status
bun scripts/cli.ts server start
bun scripts/cli.ts server stop
bun scripts/cli.ts server restart <service>
```
异步 job 模式,返回 `job.id`、日志路径。`start` 执行 Docker 构建+启动,`stop` 停止 Compose project 全部服务。
`restart` 是无构建单服务维护重启,使用现有镜像执行 `--no-build --no-deps --force-recreate` 并等待容器 `healthy/running`,适合刷新 provider-gateway 这类运行态异常,不能替代镜像发布或源码构建。
---
## 健康检查
```bash
bun scripts/cli.ts server status
```
返回公开端口、受限宿主端口、内部端口、swap 摘要、Compose 容器状态、各服务健康检查和访问 URL。
低内存时 `swap.warning` 非空,先执行 `server swap ensure`
## 节点资源指标同步
资源面板显示旧 CPU/内存/磁盘值时,先对照 provider 上报、backend-core 落库和内部 API 三段:
```bash
docker logs --tail 120 unidesk-provider-gateway-main | rg 'system_status_sent|docker_status_sent|error'
docker logs --tail 200 unidesk-backend-core | rg 'provider_system_status|provider_docker_status|provider_message_failed|serializing parameter'
docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/api/nodes/system-status?limit=5'
```
provider 已发送但 backend-core 没有 `provider_system_status` / `provider_docker_status`,优先查 backend-core ingest/DB 写入错误;provider 自身停发时才用 `server restart provider-gateway` 做无构建维护重启。修复 backend-core 后必须用 `server rebuild backend-core` 上线并验证 `stale=false``currentCollectedAt` 为当前采样。长期语义见 `docs/reference/observability.md#node-resource-status`
---
## Swap 管理
```bash
bun scripts/cli.ts server swap status
bun scripts/cli.ts server swap ensure [--path /swapfile] [--size 2GiB] [--dry-run]
```
`ensure` 在无 active swap 时创建 swapfile`chmod 600``mkswap``swapon`、写 `/etc/fstab`)。已有 swap 时 no-op。fstab 写入失败返回 `degraded`
---
## 日志
```bash
bun scripts/cli.ts server logs
```
返回文件日志和 Docker 容器日志尾部,默认限制输出大小。
---
## Docker 镜像清理
```bash
bun scripts/cli.ts server cleanup plan [--min-age-hours 24] [--limit N]
bun scripts/cli.ts server cleanup run --confirm [--min-age-hours 24] [--limit N]
```
`plan` 只生成 dry-run 计划;`run --confirm` 只删除同一 classifier 选出的 stale Docker images。保守白名单:保留 running/stopped 容器镜像、UniDesk YAML/GitOps/image catalog 声明的 commit-pinned artifact、Compose stable image。禁止 `docker system prune``docker image prune``docker volume rm``docker compose down -v` 和数据库清理。高风险候选必须额外显式 `--include-high-risk` 才会执行。
---
## 磁盘 GC
```bash
bun scripts/cli.ts server logs --tail-bytes 12000
bun scripts/cli.ts gc plan
bun scripts/cli.ts gc run --confirm
bun scripts/cli.ts gc db-trace
bun scripts/cli.ts gc policy
bun scripts/cli.ts gc remote <providerId> [--target-use-percent N] [--dry-run|--confirm]
bun scripts/cli.ts platform-db postgres status
```
主 server 和 provider 磁盘高水位缓解。`plan` 只读输出候选、风险、估算收益和保护对象。`run` 必须 `--confirm``remote` 通过 SSH 透传执行远端 GC
启停、健康检查、节点资源指标、swap、日志、Docker 镜像清理、磁盘 GC、服务重建/重启、PK01 PostgreSQL、YAML-first 运维、Moon Bridge、profile smoke 和 MiniMax recovery 细节见 [references/full.md](references/full.md)
常用显式候选和目标口径:
## P0 边界
```bash
bun scripts/cli.ts gc plan --target-use-percent 69 \
--include-tool-caches \
--include-stale-tmp \
--include-vscode-stale-servers \
--include-vscode-stale-extensions \
--include-vscode-cached-vsix \
--include-baidu-staging \
--include-vpn-diagnostic-logs
```
- backend-core 运行面恢复 healthy 后,除非用户明确要求,不主动 rebuild/restart/替换 backend-core。
- Master server 不作为通用构建机;Docker/Rust/Go/前端高 CPU 构建必须走批准的 CI/运行面。
- GC 默认 plan 只读,真实删除必须显式 `run --confirm` 并遵循 allowlist/retention。
- Secret、database URL 和 API key 不打印完整值。
`--target-use-percent``df` 显示口径估算 shortfall。主 server GC 的默认 include、保留窗口、输出 limit、Codex session root、worktree main/root/baseRef、worktree 扫描预算和 `.state` allowlist roots 由 `config/unidesk-cli.yaml#gc` 拥有;CLI 参数只做一次性显式覆盖。工具缓存、`/tmp` 非 allowlist 直接子项、VS Code 历史 server/extension 版本、VS Code CachedExtensionVSIXs 下载缓存、Baidu staging 旧 PGDATA tarball、UniDesk `.state` 历史诊断/部署产物、`.state` stale scratch、Codex inactive sessions、merged worktrees、VPN 诊断 ring pcap 均默认不启用;必须显式 include 后才进入候选,且执行时仍受路径断言保护。stale `/tmp` 扫描按 `--limit` 有界枚举候选,避免为了估算全量临时目录而长时间无输出。`.state` retention 通过 `--include-state-artifacts``--include-state-stale-scratch` 读取 YAML allowlist;不得把 `.state` 根目录当成通用清理对象。Codex session 清理只删除 YAML root 下超过 keepHours 的普通 session 文件,永远不删除 auth/config。Worktree 清理只扫描 YAML root 下 inactive 且已合入 YAML baseRef 或 cherry-equivalent 的 worktreerun 删除前重新校验 full clean 状态并使用 `git worktree remove`。VS Code cached VSIX 只选择 `/root/.vscode-server/data/CachedExtensionVSIXs` 下超过 `--vscode-cached-vsix-keep-days` 的顶层普通缓存文件,执行前检查 active fd;不删除已安装 extensions、server 或 user data。VPN 诊断日志只选择 `/root/vpn-server/logs/hy2-udp-ring-*.pcap``hy2-monitor-ring-*.pcap` 中超过 `--vpn-diagnostic-log-keep-hours` 的普通文件,执行前检查 active fd;不删除 evidence JSONL。默认 GC 不触碰 `.state/recovery``.state/codex-queue/codex-home``.state/deploy/work``.state/baidu-netdisk`、PGDATA、Docker volumes/images、Codex auth/config state、active/unmerged/dirty worktree、runtime image/snapshot state、Baidu staging 根目录、VPN 日志根目录或 VS Code user data。
## 何时读取 reference
`gc policy install` 的每日 timer 从 `config/unidesk-cli.yaml#gc.policyTimer` 渲染 VPN 诊断 pcap retention、UniDesk `.state` artifact retention 和 VS Code CachedExtensionVSIXs retention,用于限制长期诊断/部署产物、tcpdump ring 文件与 VS Code 下载缓存增长;手动 `gc plan/run` 仍必须显式 `--include-vpn-diagnostic-logs` / `--include-state-artifacts` / `--include-vscode-cached-vsix` 才会列出或删除这些对象
---
## 服务重建
```bash
bun scripts/cli.ts server rebuild <service>
```
service 可选:`backend-core` | `frontend` | `dev-frontend-proxy` | `provider-gateway` | `todo-note` | `code-queue-mgr` | `project-manager` | `baidu-netdisk` | `oa-event-flow`
异步 job:构建镜像 → `.state/locks/server-compose.lock` 串行保护 → `--no-deps --force-recreate` 替换容器 → 等待 `healthy/running`
启动后必须轮询 job,不要把提交 job 当成已经完成:
```bash
bun scripts/cli.ts server rebuild backend-core
bun scripts/cli.ts job status <jobId> --tail-bytes 12000
```
backend-core 重建完成后再做运行面验证:
```bash
bun scripts/cli.ts server status
docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/health --require-ok'
```
**禁止事项**
- backend-core 常规迭代不得在 master server 编译;只有已提交修复需要上线主 server Compose runtime 时,才用 `server rebuild backend-core` 受控异步 job
- D601 Code Queue 执行面不由 `server rebuild` 管理
- 不重建/删除 database 命名卷
---
## PK01 Host PostgreSQL
PK01 host-native PostgreSQL 是平台外置状态库样板,声明文件是 `config/platform-db/postgres-pk01.yaml`,受控入口是:
```bash
bun scripts/cli.ts platform-db postgres plan --config config/platform-db/postgres-pk01.yaml
bun scripts/cli.ts platform-db postgres status --config config/platform-db/postgres-pk01.yaml
bun scripts/cli.ts platform-db postgres export-secrets --config config/platform-db/postgres-pk01.yaml --confirm
bun scripts/cli.ts platform-db postgres apply --config config/platform-db/postgres-pk01.yaml --confirm
bun scripts/cli.ts platform-db postgres apply --config config/platform-db/postgres-pk01.yaml --confirm --wait
```
- `plan` / `status` 只读;`apply --confirm` 默认创建本地异步 job`apply --confirm --wait` 会启动 PK01 侧 root-owned job 并短轮询。
- `export-secrets --confirm` 只按 YAML 重新物化本地 Secret source/export 文件,不触碰 PK01 远端 PostgreSQL;连接串格式、consumer target 或 Secret export 变更优先用它,再走对应消费者的 Secret sync。
- 输出只显示 Secret key 名、presence、fingerprint、连接 host、SSL 状态和状态摘要;禁止打印密码或完整 `DATABASE_URL`
- 同一个 PK01 PostgreSQL 实例可承载多个 YAML 声明的 role/database;新增消费者按 `secrets.entries``objects.roles``objects.databases``postgres.auth.pgHba``exports.connectionStrings` 成套声明,不新开 PostgreSQL 实例,也不默认用 schema 隔离应用状态。
- 跨节点消费者必须直连 YAML 的 `postgres.network.connectionHost`,当前是 PK01 公网 endpoint;不要让 D601/G14/Sub2API/HWLAB/AgentRun 通过 master server 中转 PostgreSQL。
- 当前 TLS 口径是 PostgreSQL native TLS + `sslmode=require``publicDns` 只是可选 alias;只要 `connectionHost` 是可达 IP,DNS 未解析不作为切库 blocker。
- 远端 PostgreSQL 配置或 `pg_hba` 来源 CIDR 变化后,先跑 `apply --confirm --wait`,再跑 `status`;若消费者公网出口 IP 变化,必须先更新 YAML `allowSources` 和对应 `pg_hba`
- `status` 验收要看 `roles[]``databases[]``appConnections[]`;不要只看旧的 `roleExists` / `databaseExists` 标量。
日常复验建议:
```bash
bun scripts/cli.ts platform-db postgres status --config config/platform-db/postgres-pk01.yaml
trans PK01 sh <<'SH'
systemctl is-active postgresql
systemctl is-enabled postgresql
systemctl is-active unidesk-pk01-sub2api-pgdump.timer
SH
```
长期边界见 `docs/reference/pk01.md`Sub2API 消费侧边界见 `docs/reference/platform-infra.md`
## YAML-First 分布式运维边界
UniDesk 自有分布式运维以 `config/**/*.yaml` 为 desired-state truth。服务仓库里的 `deploy.json` 不能作为 UniDesk deployment truthnode/lane、runtime namespace、GitOps branch/path、image artifact、public exposure、Secret、外置数据库、probe 和 rollout 等运维选择必须进入所属 UniDesk YAML,并通过受控 CLI 渲染或同步。
AgentRun v0.2/D601 这类 YAML-only lane 的控制面、Secret 同步、外置 DB wiring 和状态检查使用:
```bash
bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02
bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --confirm
bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 --full
```
部署触发和 GitOps promotion 入口归 `$unidesk-cicd`;本 skill 只记录手动运维边界和长期排障入口。长期架构见 `docs/reference/yaml-first-ops.md`AgentRun 细则见 `docs/reference/agentrun.md`
---
## Moon Bridge 管理
Moon Bridge 是 Codex ↔ 上游 provider 的桥接服务,通过 profile 级 wrapper 管理:
```bash
# DeepSeek profile
dscx bridge-start
dscx bridge-status
dscx bridge-smoke dscx-bridge-ok
dscx bridge-stop
# MiniMax profile
mxcx bridge-start
mxcx bridge-status
mxcx bridge-smoke mxcx-bridge-ok
mxcx bridge-stop
```
- `dscx``127.0.0.1:38440`Codex custom provider `deepseek`DeepSeek V4 Pro
- `mxcx``127.0.0.1:38441`Codex custom provider `minimax`MiniMax-M3
- 启动用 `setsid` + profile-local PID file,进程不随 CLI 退出
- 日志在 `<CODEX_HOME>/logs/moonbridge/`
---
## Codex Profile Smoke
```bash
# DeepSeek
dscx doctor
dscx bridge-smoke dscx-bridge-ok
dscx exec --skip-git-repo-check 'Reply exactly: dscx-codex-ok'
# MiniMax
mxcx doctor
mxcx bridge-smoke mxcx-bridge-ok
mxcx exec --skip-git-repo-check 'Reply exactly: mxcx-codex-ok'
# ACX GPT direct profiles
acx status
acx gpt-only exec --json 'Reply exactly: acx-only-ok'
acx gpt-sub2api exec --json 'Reply exactly: acx-sub2api-ok'
acx exec --json 'Reply exactly: acx-default-ok'
```
`bridge-smoke` 验证 Moon Bridge → provider 链路。`exec` 验证完整 Codex CLI → bridge → provider 全链路。
`acx` 的 GPT aliases 是 Codex custom provider 直连 Responses 上游,不经过本地 `127.0.0.1:38448` router。GPT 模式下 `acx status` 应输出 `mode=gpt-direct``routerRequired=false``portPids=[]`;小真实调用应返回期望文本,重复或 resume 流量应能看到非零 `cached_input_tokens`。OpenCode Zen Go aliases 仍通过 `acx route-start|route-status` 走 router → `gocx`/Moon Bridge 路径。长期边界见 `docs/reference/master-server-ops.md`
---
## MiniMax Session Recovery
MiniMax 会话因无效 tool-call arguments 导致 `resume` 反复失败时的恢复流程:
```bash
# 1. 清理无效 tool arguments
mxcx session-clean <session-id-or-jsonl>
# 2. 确认幂等(应返回 changed=false
mxcx session-clean <session-id-or-jsonl>
# 3. 注入 guard 防止复发
mxcx session-guard <session-id-or-jsonl>
# 4. 非交互 smoke 验证恢复
mxcx exec resume <session-id> 'Reply exactly: recovered-ok'
# 5. apply-patch smoke(如涉及远端编辑)
# 验证使用 trans <route> apply-patch,非 download/upload/sed
```
`mxcx resume <session-id>` 自动执行 `session-clean` + `session-guard` 后再调用 Codex。修复最小化:只修无效 `function_call.arguments`,不压缩/截断/重排 transcript。
---
## 参考文档
- **主 server 架构与行为规范**: `docs/reference/master-server-ops.md`Execution Boundary、Codex Provider Profile 架构、Moon Bridge 内部规则、MiniMax session-clean 行为约束、apply-patch 策略)
- **磁盘 GC 长期规则**: `docs/reference/gc.md`
- **部署边界**: `docs/reference/deployment.md`
- 需要服务 rebuild/restart、日志、swap 或 health 判定:读 [references/full.md](references/full.md) 的对应段
- 需要磁盘/镜像/trace GC:读 Docker 镜像清理和磁盘 GC 段。
- 需要 PK01 PostgreSQL 或 YAML-first 运维:读对应段。
- 需要 profile smoke、Moon Bridge 或 session recovery:读对应段。
@@ -0,0 +1,276 @@
---
name: unidesk-ops
description: UniDesk 手动运维 CLI — `server``gc` 和 PK01 `platform-db postgres` 子命令,覆盖主 server 启停、健康检查、swap、日志、Docker 镜像清理、磁盘 GC、服务重建/重启和 PK01 host PostgreSQL 运维。用户提到 server start、server status、server swap、server rebuild、server restart、gc、磁盘清理、platform-db、PK01 PostgreSQL、运维时使用。
---
# UniDesk 手动运维 CLI
主 server 运维入口,通过 `bun scripts/cli.ts server ...``bun scripts/cli.ts gc ...` 操作。
**固定入口前缀**: `cd /root/unidesk && bun scripts/cli.ts ...`
---
## 启停
```bash
bun scripts/cli.ts server start
bun scripts/cli.ts server stop
bun scripts/cli.ts server restart <service>
```
异步 job 模式,返回 `job.id`、日志路径。`start` 执行 Docker 构建+启动,`stop` 停止 Compose project 全部服务。
`restart` 是无构建单服务维护重启,使用现有镜像执行 `--no-build --no-deps --force-recreate` 并等待容器 `healthy/running`,适合刷新 provider-gateway 这类运行态异常,不能替代镜像发布或源码构建。
---
## 健康检查
```bash
bun scripts/cli.ts server status
```
返回公开端口、受限宿主端口、内部端口、swap 摘要、Compose 容器状态、各服务健康检查和访问 URL。
低内存时 `swap.warning` 非空,先执行 `server swap ensure`
## 节点资源指标同步
资源面板显示旧 CPU/内存/磁盘值时,先对照 provider 上报、backend-core 落库和内部 API 三段:
```bash
docker logs --tail 120 unidesk-provider-gateway-main | rg 'system_status_sent|docker_status_sent|error'
docker logs --tail 200 unidesk-backend-core | rg 'provider_system_status|provider_docker_status|provider_message_failed|serializing parameter'
docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/api/nodes/system-status?limit=5'
```
provider 已发送但 backend-core 没有 `provider_system_status` / `provider_docker_status`,优先查 backend-core ingest/DB 写入错误;provider 自身停发时才用 `server restart provider-gateway` 做无构建维护重启。修复 backend-core 后必须用 `server rebuild backend-core` 上线并验证 `stale=false``currentCollectedAt` 为当前采样。长期语义见 `docs/reference/observability.md#node-resource-status`
---
## Swap 管理
```bash
bun scripts/cli.ts server swap status
bun scripts/cli.ts server swap ensure [--path /swapfile] [--size 2GiB] [--dry-run]
```
`ensure` 在无 active swap 时创建 swapfile`chmod 600``mkswap``swapon`、写 `/etc/fstab`)。已有 swap 时 no-op。fstab 写入失败返回 `degraded`
---
## 日志
```bash
bun scripts/cli.ts server logs
```
返回文件日志和 Docker 容器日志尾部,默认限制输出大小。
---
## Docker 镜像清理
```bash
bun scripts/cli.ts server cleanup plan [--min-age-hours 24] [--limit N]
bun scripts/cli.ts server cleanup run --confirm [--min-age-hours 24] [--limit N]
```
`plan` 只生成 dry-run 计划;`run --confirm` 只删除同一 classifier 选出的 stale Docker images。保守白名单:保留 running/stopped 容器镜像、UniDesk YAML/GitOps/image catalog 声明的 commit-pinned artifact、Compose stable image。禁止 `docker system prune``docker image prune``docker volume rm``docker compose down -v` 和数据库清理。高风险候选必须额外显式 `--include-high-risk` 才会执行。
---
## 磁盘 GC
```bash
bun scripts/cli.ts gc plan
bun scripts/cli.ts gc run --confirm
bun scripts/cli.ts gc db-trace
bun scripts/cli.ts gc policy
bun scripts/cli.ts gc remote <providerId> [--target-use-percent N] [--dry-run|--confirm]
```
主 server 和 provider 磁盘高水位缓解。`plan` 只读输出候选、风险、估算收益和保护对象。`run` 必须 `--confirm``remote` 通过 SSH 透传执行远端 GC。
常用显式候选和目标口径:
```bash
bun scripts/cli.ts gc plan --target-use-percent 69 \
--include-tool-caches \
--include-stale-tmp \
--include-vscode-stale-servers \
--include-vscode-stale-extensions \
--include-vscode-cached-vsix \
--include-baidu-staging \
--include-vpn-diagnostic-logs
```
`--target-use-percent``df` 显示口径估算 shortfall。主 server GC 的默认 include、保留窗口、输出 limit、Codex session root、worktree main/root/baseRef、worktree 扫描预算和 `.state` allowlist roots 由 `config/unidesk-cli.yaml#gc` 拥有;CLI 参数只做一次性显式覆盖。工具缓存、`/tmp` 非 allowlist 直接子项、VS Code 历史 server/extension 版本、VS Code CachedExtensionVSIXs 下载缓存、Baidu staging 旧 PGDATA tarball、UniDesk `.state` 历史诊断/部署产物、`.state` stale scratch、Codex inactive sessions、merged worktrees、VPN 诊断 ring pcap 均默认不启用;必须显式 include 后才进入候选,且执行时仍受路径断言保护。stale `/tmp` 扫描按 `--limit` 有界枚举候选,避免为了估算全量临时目录而长时间无输出。`.state` retention 通过 `--include-state-artifacts``--include-state-stale-scratch` 读取 YAML allowlist;不得把 `.state` 根目录当成通用清理对象。Codex session 清理只删除 YAML root 下超过 keepHours 的普通 session 文件,永远不删除 auth/config。Worktree 清理只扫描 YAML root 下 inactive 且已合入 YAML baseRef 或 cherry-equivalent 的 worktreerun 删除前重新校验 full clean 状态并使用 `git worktree remove`。VS Code cached VSIX 只选择 `/root/.vscode-server/data/CachedExtensionVSIXs` 下超过 `--vscode-cached-vsix-keep-days` 的顶层普通缓存文件,执行前检查 active fd;不删除已安装 extensions、server 或 user data。VPN 诊断日志只选择 `/root/vpn-server/logs/hy2-udp-ring-*.pcap``hy2-monitor-ring-*.pcap` 中超过 `--vpn-diagnostic-log-keep-hours` 的普通文件,执行前检查 active fd;不删除 evidence JSONL。默认 GC 不触碰 `.state/recovery``.state/codex-queue/codex-home``.state/deploy/work``.state/baidu-netdisk`、PGDATA、Docker volumes/images、Codex auth/config state、active/unmerged/dirty worktree、runtime image/snapshot state、Baidu staging 根目录、VPN 日志根目录或 VS Code user data。
`gc policy install` 的每日 timer 从 `config/unidesk-cli.yaml#gc.policyTimer` 渲染 VPN 诊断 pcap retention、UniDesk `.state` artifact retention 和 VS Code CachedExtensionVSIXs retention,用于限制长期诊断/部署产物、tcpdump ring 文件与 VS Code 下载缓存增长;手动 `gc plan/run` 仍必须显式 `--include-vpn-diagnostic-logs` / `--include-state-artifacts` / `--include-vscode-cached-vsix` 才会列出或删除这些对象。
---
## 服务重建
```bash
bun scripts/cli.ts server rebuild <service>
```
service 可选:`backend-core` | `frontend` | `dev-frontend-proxy` | `provider-gateway` | `todo-note` | `code-queue-mgr` | `project-manager` | `baidu-netdisk` | `oa-event-flow`
异步 job:构建镜像 → `.state/locks/server-compose.lock` 串行保护 → `--no-deps --force-recreate` 替换容器 → 等待 `healthy/running`
启动后必须轮询 job,不要把提交 job 当成已经完成:
```bash
bun scripts/cli.ts server rebuild backend-core
bun scripts/cli.ts job status <jobId> --tail-bytes 12000
```
backend-core 重建完成后再做运行面验证:
```bash
bun scripts/cli.ts server status
docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/health --require-ok'
```
**禁止事项**
- backend-core 常规迭代不得在 master server 编译;只有已提交修复需要上线主 server Compose runtime 时,才用 `server rebuild backend-core` 受控异步 job
- D601 Code Queue 执行面不由 `server rebuild` 管理
- 不重建/删除 database 命名卷
---
## PK01 Host PostgreSQL
PK01 host-native PostgreSQL 是平台外置状态库样板,声明文件是 `config/platform-db/postgres-pk01.yaml`,受控入口是:
```bash
bun scripts/cli.ts platform-db postgres plan --config config/platform-db/postgres-pk01.yaml
bun scripts/cli.ts platform-db postgres status --config config/platform-db/postgres-pk01.yaml
bun scripts/cli.ts platform-db postgres export-secrets --config config/platform-db/postgres-pk01.yaml --confirm
bun scripts/cli.ts platform-db postgres apply --config config/platform-db/postgres-pk01.yaml --confirm
bun scripts/cli.ts platform-db postgres apply --config config/platform-db/postgres-pk01.yaml --confirm --wait
```
- `plan` / `status` 只读;`apply --confirm` 默认创建本地异步 job`apply --confirm --wait` 会启动 PK01 侧 root-owned job 并短轮询。
- `export-secrets --confirm` 只按 YAML 重新物化本地 Secret source/export 文件,不触碰 PK01 远端 PostgreSQL;连接串格式、consumer target 或 Secret export 变更优先用它,再走对应消费者的 Secret sync。
- 输出只显示 Secret key 名、presence、fingerprint、连接 host、SSL 状态和状态摘要;禁止打印密码或完整 `DATABASE_URL`
- 同一个 PK01 PostgreSQL 实例可承载多个 YAML 声明的 role/database;新增消费者按 `secrets.entries``objects.roles``objects.databases``postgres.auth.pgHba``exports.connectionStrings` 成套声明,不新开 PostgreSQL 实例,也不默认用 schema 隔离应用状态。
- 跨节点消费者必须直连 YAML 的 `postgres.network.connectionHost`,当前是 PK01 公网 endpoint;不要让 D601/G14/Sub2API/HWLAB/AgentRun 通过 master server 中转 PostgreSQL。
- 当前 TLS 口径是 PostgreSQL native TLS + `sslmode=require``publicDns` 只是可选 alias;只要 `connectionHost` 是可达 IP,DNS 未解析不作为切库 blocker。
- 远端 PostgreSQL 配置或 `pg_hba` 来源 CIDR 变化后,先跑 `apply --confirm --wait`,再跑 `status`;若消费者公网出口 IP 变化,必须先更新 YAML `allowSources` 和对应 `pg_hba`
- `status` 验收要看 `roles[]``databases[]``appConnections[]`;不要只看旧的 `roleExists` / `databaseExists` 标量。
日常复验建议:
```bash
bun scripts/cli.ts platform-db postgres status --config config/platform-db/postgres-pk01.yaml
trans PK01 sh <<'SH'
systemctl is-active postgresql
systemctl is-enabled postgresql
systemctl is-active unidesk-pk01-sub2api-pgdump.timer
SH
```
长期边界见 `docs/reference/pk01.md`Sub2API 消费侧边界见 `docs/reference/platform-infra.md`
## YAML-First 分布式运维边界
UniDesk 自有分布式运维以 `config/**/*.yaml` 为 desired-state truth。服务仓库里的 `deploy.json` 不能作为 UniDesk deployment truthnode/lane、runtime namespace、GitOps branch/path、image artifact、public exposure、Secret、外置数据库、probe 和 rollout 等运维选择必须进入所属 UniDesk YAML,并通过受控 CLI 渲染或同步。
AgentRun v0.2/D601 这类 YAML-only lane 的控制面、Secret 同步、外置 DB wiring 和状态检查使用:
```bash
bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02
bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --confirm
bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 --full
```
部署触发和 GitOps promotion 入口归 `$unidesk-cicd`;本 skill 只记录手动运维边界和长期排障入口。长期架构见 `docs/reference/yaml-first-ops.md`AgentRun 细则见 `docs/reference/agentrun.md`
---
## Moon Bridge 管理
Moon Bridge 是 Codex ↔ 上游 provider 的桥接服务,通过 profile 级 wrapper 管理:
```bash
# DeepSeek profile
dscx bridge-start
dscx bridge-status
dscx bridge-smoke dscx-bridge-ok
dscx bridge-stop
# MiniMax profile
mxcx bridge-start
mxcx bridge-status
mxcx bridge-smoke mxcx-bridge-ok
mxcx bridge-stop
```
- `dscx``127.0.0.1:38440`Codex custom provider `deepseek`DeepSeek V4 Pro
- `mxcx``127.0.0.1:38441`Codex custom provider `minimax`MiniMax-M3
- 启动用 `setsid` + profile-local PID file,进程不随 CLI 退出
- 日志在 `<CODEX_HOME>/logs/moonbridge/`
---
## Codex Profile Smoke
```bash
# DeepSeek
dscx doctor
dscx bridge-smoke dscx-bridge-ok
dscx exec --skip-git-repo-check 'Reply exactly: dscx-codex-ok'
# MiniMax
mxcx doctor
mxcx bridge-smoke mxcx-bridge-ok
mxcx exec --skip-git-repo-check 'Reply exactly: mxcx-codex-ok'
# ACX GPT direct profiles
acx status
acx gpt-only exec --json 'Reply exactly: acx-only-ok'
acx gpt-sub2api exec --json 'Reply exactly: acx-sub2api-ok'
acx exec --json 'Reply exactly: acx-default-ok'
```
`bridge-smoke` 验证 Moon Bridge → provider 链路。`exec` 验证完整 Codex CLI → bridge → provider 全链路。
`acx` 的 GPT aliases 是 Codex custom provider 直连 Responses 上游,不经过本地 `127.0.0.1:38448` router。GPT 模式下 `acx status` 应输出 `mode=gpt-direct``routerRequired=false``portPids=[]`;小真实调用应返回期望文本,重复或 resume 流量应能看到非零 `cached_input_tokens`。OpenCode Zen Go aliases 仍通过 `acx route-start|route-status` 走 router → `gocx`/Moon Bridge 路径。长期边界见 `docs/reference/master-server-ops.md`
---
## MiniMax Session Recovery
MiniMax 会话因无效 tool-call arguments 导致 `resume` 反复失败时的恢复流程:
```bash
# 1. 清理无效 tool arguments
mxcx session-clean <session-id-or-jsonl>
# 2. 确认幂等(应返回 changed=false
mxcx session-clean <session-id-or-jsonl>
# 3. 注入 guard 防止复发
mxcx session-guard <session-id-or-jsonl>
# 4. 非交互 smoke 验证恢复
mxcx exec resume <session-id> 'Reply exactly: recovered-ok'
# 5. apply-patch smoke(如涉及远端编辑)
# 验证使用 trans <route> apply-patch,非 download/upload/sed
```
`mxcx resume <session-id>` 自动执行 `session-clean` + `session-guard` 后再调用 Codex。修复最小化:只修无效 `function_call.arguments`,不压缩/截断/重排 transcript。
---
## 参考文档
- **主 server 架构与行为规范**: `docs/reference/master-server-ops.md`Execution Boundary、Codex Provider Profile 架构、Moon Bridge 内部规则、MiniMax session-clean 行为约束、apply-patch 策略)
- **磁盘 GC 长期规则**: `docs/reference/gc.md`
- **部署边界**: `docs/reference/deployment.md`
+14 -160
View File
@@ -5,172 +5,26 @@ description: UniDesk OpenTelemetry/Tempo 链路追踪运维技能。用户提到
# UniDesk OTel
Skill(cli-spec)
Skill(cli-spec)。用于 UniDesk OpenTelemetry/Tempo 链路追踪运维、按 traceId 查 span、Code Agent/AgentRun/HWLAB 跨服务追踪,以及 provider-stream-disconnected 等问题定位。
UniDesk 的 OTel 运行面在 `platform-infra` namespaceOTel Collector 负责接收 OTLP tracesTempo 负责查询。操作入口统一走 UniDesk YAML-first CLI,不直接 `kubectl port-forward`、手写 Tempo API 或裸 `curl`
**固定入口**: `cd /root/unidesk && bun scripts/cli.ts platform-infra observability ...`
## 基本状态
## 高频入口
```bash
bun scripts/cli.ts platform-infra observability status --target D601
bun scripts/cli.ts platform-infra observability validate --target D601
bun scripts/cli.ts platform-infra observability status
bun scripts/cli.ts platform-infra observability trace <traceId>
bun scripts/cli.ts platform-infra observability search --service <service> --limit 20
```
- `status` 检查 `platform-infra` namespace、`otel-collector``tempo` Deployment/Service/Pod 和 readiness probe
- `validate` 生成一条测试 trace,经 Collector 写入 Tempo,再通过受控 service proxy 查询,证明采集和查询闭环可用。
- `--full` 只在需要展开远端 stdout/stderr 或完整 status payload 时使用;默认输出必须保持低噪声。
基本状态、trace 查询、噪声压制、业务 trace 映射、Code Agent/AgentRun 排障、codex-stdio 追穿、读取窗口与乱序调查、何时先改进 OTel 的细节见 [references/full.md](references/full.md)
## 查询 Trace
## P0 边界
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId>
```
- 可见性问题优先修复;状态、耗时、失败原因、trace、命令结果或关键证据不可见时,先补 CLI/日志/状态输出。
- OTel 查询默认低噪声摘要;完整 span/context 显式 `--full`/`--raw`
- 不把 trace 缺口误判成业务成功;缺少 span 或窗口不完整时,先说明观测边界。
`--trace-id` 是 32 位 hex OpenTelemetry trace id,不是业务 traceId。默认输出只返回有界摘要:
## 何时读取 reference
- `spanCount``serviceCount``services`
- `businessTraceIds`
- `errorSpanCount`
- `spanNameCounts`
- `errorSpans`
- 去重后的关键 spans
- 下一步 drill-down 命令
默认输出不得展开完整 Tempo JSON;需要原始响应时才用 `--raw`
当只有错误文案而没有 OTel trace id 时,先用 `search` 从 Tempo 最近 trace 中反查候选,再进入 `trace`
```bash
bun scripts/cli.ts platform-infra observability search \
--target D601 \
--grep 'no rollout found' \
--lookback-minutes 360 \
--candidate-limit 80 \
--limit 20
```
`search` 会通过受控 service proxy 调 Tempo `/api/search` 取候选 trace,并逐条拉 trace 做本地 grep 摘要;默认只输出匹配 trace、服务、业务 traceId、错误 span 和下一步命令。扩大时间窗或候选数必须显式传 `--lookback-minutes` / `--candidate-limit`,避免大 trace 输出淹没上下文。
## 噪声压制
按错误文案、span 名、failureKind 或关键属性定位时,优先用 `--grep`
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep provider-stream-disconnected \
--limit 20
```
- `--grep <text>` 在 span 名、status message 和关键 attributes 的摘要 JSON 中过滤。
- `--limit <N>` 控制返回 span 数,避免大 trace 淹没上下文。
- `--full` 展开完整 span 摘要,但仍不输出 Tempo raw body。
- `--raw` 仅用于排查 Tempo 响应结构、CLI 解析器或后端返回本身。
## 业务 Trace 映射
HWLAB/Code Agent 的业务 traceId 通常形如 `trc_...`。当已知 OTel trace id 时,直接用 `trace --trace-id` 查询;当只知道业务 traceId 时,优先从 HWLAB trace/result、Code Agent result 或已记录的 issue 证据中取得对应 OTel trace id。不要为了找 OTel trace id 去打印 Secret、Authorization header、完整 DSN 或运行面 raw transcript。
OTel trace 内常见业务关联属性:
- `traceId`: HWLAB 业务 traceId
- `otel.trace_id`: OTel trace id
- `runId` / `commandId`: AgentRun run/command
- `sessionId` / `turnId` / `threadId`: HWLAB/AgentRun 会话与 turn 关联
- `failureKind` / `willRetry` / `terminalStatus`: 错误与终态判断
## Code Agent / AgentRun 排障
`Code Agent 代理暂时无法连接上游``provider-stream-disconnected`、Workbench 加载/转圈、turn idle 报错、AgentRun command terminal 状态时:
优先用一条诊断命令汇总业务 trace、OTel trace、服务追穿、AgentRun 终态、HWLAB 读模型和 HTTP 403/401/5xx 根因:
```bash
bun scripts/cli.ts platform-infra observability diagnose-code-agent \
--target D601 \
--business-trace-id <trc_...>
```
默认输出必须保持有界低噪声,重点看:
- `mapping.businessTraceId` / `mapping.otelTraceId`
- `servicePath` 是否同时到达 `hwlab-cloud-api``agentrun-manager``agentrun-runner`
- `identity` 里的 `runId``commandId``sessionId``runnerJobId``runnerId``backendProfile``sourceCommit`
- `agentrun.terminalStatus``terminalEventType``runnerProviderClassification`
- `hwlabReadModel.sourceEventCount``requestedSinceSeq``turnStatusCounts`
- `http.problemCounts``projectionLag.status`
- `summary.rootCause` 与按置信度排序的 `rootCauseCandidates`
只有需要展开 span 明细时使用 `--full`;只有排查 Tempo raw 响应或 CLI 解析器时使用 `--raw`。默认输出不得包含 Secret、Authorization header、DSN、可复制凭据或完整运行 transcript。
若没有业务 traceId 或诊断结果还需要 drill-down,再使用低层 trace/search
1. 先确认 OTel backend ready
`bun scripts/cli.ts platform-infra observability status --target D601`
2. 查业务 trace 对应 OTel trace
`bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <otelTraceId>`
3. 用错误关键词过滤:
`bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <otelTraceId> --grep <failureKind-or-message> --limit 20`
4. 对照 `errorSpanCount``matchedSpanCount``terminalStatus``willRetry``runId``commandId` 判断是 terminal failure、retryable transient 还是旧 trace 缺 instrumentation。
旧 trace 不会因为后续 instrumentation 修复自动回填。若旧 trace 查不到错误 span,但新的 canary/真实 trace 能查到同类 `runner_error.*` span,应把旧 trace 结论写成“当时未采集到该事件”,不要倒推出运行面没有发生过错误。
### AgentRun codex-stdio 追穿检查
追 HWLAB Workbench turn idle、`waitingFor=code-agent`、工具调用后无 terminal、`provider-stream-disconnected`,或用户怀疑 AgentRun/codex-stdio 仍在运行但 OTel 没追到时,必须在同一 OTel trace 里同时看到 `hwlab-cloud-api``agentrun-manager``agentrun-runner`。只看到 manager dispatch 或 HWLAB business trace 不算追穿 runner。
优先用业务入口拿到 OTel trace id,再按 codex span 过滤:
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep codex_stdio \
--limit 120 \
--full
```
通过证据应能看到 `codex_app_server.starting``codex_app_server.started`、需要进程退出时的 `codex_app_server.exit``codex_stdio.thread_start.*``thread_resume.*``codex_stdio.turn_start.*``codex_stdio.tool_call.started|completed|failed``codex_stdio.turn_completed`,以及问题相关的 `idle_warning``idle_timeout``provider_stream_disconnected``missing_terminal_after_tool`。这些 span 应带 `runId``commandId``runnerJobId``runnerId``sessionId``backendProfile``sourceCommit``traceId``otel.trace_id``valuesPrinted=false`。若默认摘要或 grep 看不到 `runnerJobId`,先用当前 UniDesk CLI 执行 `platform-infra observability trace --grep runnerJobId --full` 复查摘要器输出,必要时用 `--raw` 只排查 Tempo/CLI 解析结构;只有 raw 或更新后的摘要仍缺 `runnerJobId`,或同一 trace 没有 `agentrun-runner` service,才回 AgentRun runner-side instrumentation 排查。
如果需要确认工具调用 started/completed 的归一化,canary 应要求一次只读 shell 工具调用。Codex notification 的 `item/started` + `status=inProgress` 应落为 `codex_stdio.tool_call.started`,不得和 completed 混在一起。长期规则见 `docs/reference/agentrun.md#agentrun--hwlab-otel-追踪口径`
## Trace 读取窗口与乱序调查
排查 HWLAB/Workbench trace 乱序、分页缺口、`--after-seq`/`--tail` 不生效、旧 trace 只返回局部事件或 read model 是否完整时,优先查 `trace_events_read` span
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep trace_events_read \
--limit 20 \
--full
```
摘要里重点看 `returnedEvents``sinceSeq``limit``fromSeq``toSeq``totalEvents``hasMore``fullTraceLoaded``rawEventCount``maxSeq``traceLastSeq``endSeq``commandFiltered`。这些字段用于判断“查询窗口是否正确传到后端”“后端是否只返回了局部事件”“read model 是否已经加载完整 trace”。若 `errorSpanCount=0` 但用户可见 timeline 仍乱序,先把结论写成展示/投影/renderer 调查 issue,不要把它定性为后端错误 span。
旧业务 trace 在 runtime 重启或保留策略后可能只剩局部事件;OTel 只能证明读取窗口、span 和当时观测到的字段,不能自动恢复业务事件流。需要验证新 instrumentation 时,使用新 canary 或仍可完整读取的真实 trace。
## 何时先改进 OTel
遇到以下情况,先修 OTel CLI 或 instrumentation,再继续业务排障:
- trace 命令只能返回 raw/tail,不能给出可读 span 摘要。
- 大 trace 输出淹没上下文,缺少 `--grep``--limit`、错误 span 汇总或下一步 drill-down。
- 关键 runner/backend/projection 事件只存在业务事件流,不进入 OTel。
- error span 缺 `failureKind``willRetry``terminalStatus``runId``commandId` 等定位字段。
- CLI 默认输出 Secret、Authorization header、DSN 或其他敏感值。
改进后必须用一条 canary 或真实 trace 证明新 span/摘要可查询,再继续定位原业务问题。
## 交付边界
- OTel 平台配置真相是 `config/platform-infra/observability.yaml`
- OTel CLI 实现在 `scripts/src/platform-infra-observability.ts`,帮助入口由 `scripts/src/platform-infra.ts` 暴露。
- 修改 OTel CLI 属于 UniDesk 轻量 CLI 变更:默认只做语法检查、命令形态验证和真实 trace 查询,不新增合同测试。
- 修改 AgentRun/HWLAB instrumentation 属于对应仓库/运行面的代码变更,必须按目标 repo 的 source truth、PR/CD 和原入口验收规则执行。
- 查具体 traceId、span、服务过滤或噪声压制时,读 [references/full.md](references/full.md) 的查询和噪声段。
- 排障 Code Agent/AgentRun/HWLAB 链路时,读业务映射和 Code Agent/AgentRun 段。
- 判断是否需要先改进 OTel 或交付边界时,读对应段。
@@ -0,0 +1,176 @@
---
name: unidesk-otel
description: UniDesk OpenTelemetry/Tempo 链路追踪运维技能。用户提到 OTel、OpenTelemetry、Tempo、trace backend、platform-infra observability、链路追踪、按 traceId 查 span、provider-stream-disconnected、Code Agent/AgentRun/HWLAB 跨服务追踪、或要求“用 otel 查/改进 otel”时使用。
---
# UniDesk OTel
Skill(cli-spec)
UniDesk 的 OTel 运行面在 `platform-infra` namespaceOTel Collector 负责接收 OTLP tracesTempo 负责查询。操作入口统一走 UniDesk YAML-first CLI,不直接 `kubectl port-forward`、手写 Tempo API 或裸 `curl`
**固定入口**: `cd /root/unidesk && bun scripts/cli.ts platform-infra observability ...`
## 基本状态
```bash
bun scripts/cli.ts platform-infra observability status --target D601
bun scripts/cli.ts platform-infra observability validate --target D601
```
- `status` 检查 `platform-infra` namespace、`otel-collector``tempo` Deployment/Service/Pod 和 readiness probe。
- `validate` 生成一条测试 trace,经 Collector 写入 Tempo,再通过受控 service proxy 查询,证明采集和查询闭环可用。
- `--full` 只在需要展开远端 stdout/stderr 或完整 status payload 时使用;默认输出必须保持低噪声。
## 查询 Trace
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId>
```
`--trace-id` 是 32 位 hex OpenTelemetry trace id,不是业务 traceId。默认输出只返回有界摘要:
- `spanCount``serviceCount``services`
- `businessTraceIds`
- `errorSpanCount`
- `spanNameCounts`
- `errorSpans`
- 去重后的关键 spans
- 下一步 drill-down 命令
默认输出不得展开完整 Tempo JSON;需要原始响应时才用 `--raw`
当只有错误文案而没有 OTel trace id 时,先用 `search` 从 Tempo 最近 trace 中反查候选,再进入 `trace`
```bash
bun scripts/cli.ts platform-infra observability search \
--target D601 \
--grep 'no rollout found' \
--lookback-minutes 360 \
--candidate-limit 80 \
--limit 20
```
`search` 会通过受控 service proxy 调 Tempo `/api/search` 取候选 trace,并逐条拉 trace 做本地 grep 摘要;默认只输出匹配 trace、服务、业务 traceId、错误 span 和下一步命令。扩大时间窗或候选数必须显式传 `--lookback-minutes` / `--candidate-limit`,避免大 trace 输出淹没上下文。
## 噪声压制
按错误文案、span 名、failureKind 或关键属性定位时,优先用 `--grep`
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep provider-stream-disconnected \
--limit 20
```
- `--grep <text>` 在 span 名、status message 和关键 attributes 的摘要 JSON 中过滤。
- `--limit <N>` 控制返回 span 数,避免大 trace 淹没上下文。
- `--full` 展开完整 span 摘要,但仍不输出 Tempo raw body。
- `--raw` 仅用于排查 Tempo 响应结构、CLI 解析器或后端返回本身。
## 业务 Trace 映射
HWLAB/Code Agent 的业务 traceId 通常形如 `trc_...`。当已知 OTel trace id 时,直接用 `trace --trace-id` 查询;当只知道业务 traceId 时,优先从 HWLAB trace/result、Code Agent result 或已记录的 issue 证据中取得对应 OTel trace id。不要为了找 OTel trace id 去打印 Secret、Authorization header、完整 DSN 或运行面 raw transcript。
OTel trace 内常见业务关联属性:
- `traceId`: HWLAB 业务 traceId
- `otel.trace_id`: OTel trace id
- `runId` / `commandId`: AgentRun run/command
- `sessionId` / `turnId` / `threadId`: HWLAB/AgentRun 会话与 turn 关联
- `failureKind` / `willRetry` / `terminalStatus`: 错误与终态判断
## Code Agent / AgentRun 排障
`Code Agent 代理暂时无法连接上游``provider-stream-disconnected`、Workbench 加载/转圈、turn idle 报错、AgentRun command terminal 状态时:
优先用一条诊断命令汇总业务 trace、OTel trace、服务追穿、AgentRun 终态、HWLAB 读模型和 HTTP 403/401/5xx 根因:
```bash
bun scripts/cli.ts platform-infra observability diagnose-code-agent \
--target D601 \
--business-trace-id <trc_...>
```
默认输出必须保持有界低噪声,重点看:
- `mapping.businessTraceId` / `mapping.otelTraceId`
- `servicePath` 是否同时到达 `hwlab-cloud-api``agentrun-manager``agentrun-runner`
- `identity` 里的 `runId``commandId``sessionId``runnerJobId``runnerId``backendProfile``sourceCommit`
- `agentrun.terminalStatus``terminalEventType``runnerProviderClassification`
- `hwlabReadModel.sourceEventCount``requestedSinceSeq``turnStatusCounts`
- `http.problemCounts``projectionLag.status`
- `summary.rootCause` 与按置信度排序的 `rootCauseCandidates`
只有需要展开 span 明细时使用 `--full`;只有排查 Tempo raw 响应或 CLI 解析器时使用 `--raw`。默认输出不得包含 Secret、Authorization header、DSN、可复制凭据或完整运行 transcript。
若没有业务 traceId 或诊断结果还需要 drill-down,再使用低层 trace/search
1. 先确认 OTel backend ready
`bun scripts/cli.ts platform-infra observability status --target D601`
2. 查业务 trace 对应 OTel trace
`bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <otelTraceId>`
3. 用错误关键词过滤:
`bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id <otelTraceId> --grep <failureKind-or-message> --limit 20`
4. 对照 `errorSpanCount``matchedSpanCount``terminalStatus``willRetry``runId``commandId` 判断是 terminal failure、retryable transient 还是旧 trace 缺 instrumentation。
旧 trace 不会因为后续 instrumentation 修复自动回填。若旧 trace 查不到错误 span,但新的 canary/真实 trace 能查到同类 `runner_error.*` span,应把旧 trace 结论写成“当时未采集到该事件”,不要倒推出运行面没有发生过错误。
### AgentRun codex-stdio 追穿检查
追 HWLAB Workbench turn idle、`waitingFor=code-agent`、工具调用后无 terminal、`provider-stream-disconnected`,或用户怀疑 AgentRun/codex-stdio 仍在运行但 OTel 没追到时,必须在同一 OTel trace 里同时看到 `hwlab-cloud-api``agentrun-manager``agentrun-runner`。只看到 manager dispatch 或 HWLAB business trace 不算追穿 runner。
优先用业务入口拿到 OTel trace id,再按 codex span 过滤:
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep codex_stdio \
--limit 120 \
--full
```
通过证据应能看到 `codex_app_server.starting``codex_app_server.started`、需要进程退出时的 `codex_app_server.exit``codex_stdio.thread_start.*``thread_resume.*``codex_stdio.turn_start.*``codex_stdio.tool_call.started|completed|failed``codex_stdio.turn_completed`,以及问题相关的 `idle_warning``idle_timeout``provider_stream_disconnected``missing_terminal_after_tool`。这些 span 应带 `runId``commandId``runnerJobId``runnerId``sessionId``backendProfile``sourceCommit``traceId``otel.trace_id``valuesPrinted=false`。若默认摘要或 grep 看不到 `runnerJobId`,先用当前 UniDesk CLI 执行 `platform-infra observability trace --grep runnerJobId --full` 复查摘要器输出,必要时用 `--raw` 只排查 Tempo/CLI 解析结构;只有 raw 或更新后的摘要仍缺 `runnerJobId`,或同一 trace 没有 `agentrun-runner` service,才回 AgentRun runner-side instrumentation 排查。
如果需要确认工具调用 started/completed 的归一化,canary 应要求一次只读 shell 工具调用。Codex notification 的 `item/started` + `status=inProgress` 应落为 `codex_stdio.tool_call.started`,不得和 completed 混在一起。长期规则见 `docs/reference/agentrun.md#agentrun--hwlab-otel-追踪口径`
## Trace 读取窗口与乱序调查
排查 HWLAB/Workbench trace 乱序、分页缺口、`--after-seq`/`--tail` 不生效、旧 trace 只返回局部事件或 read model 是否完整时,优先查 `trace_events_read` span
```bash
bun scripts/cli.ts platform-infra observability trace \
--target D601 \
--trace-id <otelTraceId> \
--grep trace_events_read \
--limit 20 \
--full
```
摘要里重点看 `returnedEvents``sinceSeq``limit``fromSeq``toSeq``totalEvents``hasMore``fullTraceLoaded``rawEventCount``maxSeq``traceLastSeq``endSeq``commandFiltered`。这些字段用于判断“查询窗口是否正确传到后端”“后端是否只返回了局部事件”“read model 是否已经加载完整 trace”。若 `errorSpanCount=0` 但用户可见 timeline 仍乱序,先把结论写成展示/投影/renderer 调查 issue,不要把它定性为后端错误 span。
旧业务 trace 在 runtime 重启或保留策略后可能只剩局部事件;OTel 只能证明读取窗口、span 和当时观测到的字段,不能自动恢复业务事件流。需要验证新 instrumentation 时,使用新 canary 或仍可完整读取的真实 trace。
## 何时先改进 OTel
遇到以下情况,先修 OTel CLI 或 instrumentation,再继续业务排障:
- trace 命令只能返回 raw/tail,不能给出可读 span 摘要。
- 大 trace 输出淹没上下文,缺少 `--grep``--limit`、错误 span 汇总或下一步 drill-down。
- 关键 runner/backend/projection 事件只存在业务事件流,不进入 OTel。
- error span 缺 `failureKind``willRetry``terminalStatus``runId``commandId` 等定位字段。
- CLI 默认输出 Secret、Authorization header、DSN 或其他敏感值。
改进后必须用一条 canary 或真实 trace 证明新 span/摘要可查询,再继续定位原业务问题。
## 交付边界
- OTel 平台配置真相是 `config/platform-infra/observability.yaml`
- OTel CLI 实现在 `scripts/src/platform-infra-observability.ts`,帮助入口由 `scripts/src/platform-infra.ts` 暴露。
- 修改 OTel CLI 属于 UniDesk 轻量 CLI 变更:默认只做语法检查、命令形态验证和真实 trace 查询,不新增合同测试。
- 修改 AgentRun/HWLAB instrumentation 属于对应仓库/运行面的代码变更,必须按目标 repo 的 source truth、PR/CD 和原入口验收规则执行。
+17 -271
View File
@@ -5,283 +5,29 @@ description: UniDesk Sub2API 平台运维技能。用户提到 Sub2API、sub2api
# UniDesk Sub2API
UniDesk 在 k3s `platform-infra` namespace 运维 Sub2API。D601 是当前默认 target,使用外置 PostgreSQL 和目标级 public exposureD518 可作为独立 external-active target 与 D601 同时对外服务;G14 由同一 YAML/CLI 控制为 standby predeploy,默认缩容且不运行 sentinel、FRP 或 HTTPS egress proxy。日常操作统一使用 UniDesk CLI,不直接写 Kubernetes 资源手工调用 Sub2API 管理 API。
UniDesk 在 k3s `platform-infra` namespace 运维 Sub2API。日常操作统一使用 UniDesk CLI,不直接写 Kubernetes 资源、不手工调用 Sub2API 管理 API、不打印 Secret
**固定入口**: `cd /root/unidesk && bun scripts/cli.ts platform-infra sub2api ...`
## 先看报表
查 Codex pool 哨兵状态、账号冻结/恢复、marker 命中、下一次 probe、最近 CronJob run、token/cost 账本时,优先使用这个低噪声报表入口,不要先翻 ConfigMap、CronJob 日志或 Sub2API UI
## 高频入口
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report
bun scripts/cli.ts platform-infra sub2api report
bun scripts/cli.ts platform-infra sub2api status --target D601
bun scripts/cli.ts platform-infra sub2api plan --target D601
bun scripts/cli.ts platform-infra sub2api apply --target D601 --dry-run
```
需要机器处理或完整字段时再加 `--raw`;需要更多最近运行记录时加 `--events N`
先看报表和状态,再做计划或变更。部署、D601 egress proxy、镜像升级、Codex pool、账号代理、FRP 暴露、master Codex 消费端配置和排障细节见 [references/full.md](references/full.md)
追溯某个 Codex/Sub2API request id 的中断、上游账号、切号、临时不可调度、账号选择失败和同窗口账号池信号时,优先使用低噪声 trace 报表,不要先手写 `kubectl logs | grep`
## 边界
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
```
- YAML 是 source of truthtarget、public exposure、Secret sourceRef、Codex pool 和 sentinel 配置都从 YAML 进入 CLI。
- Secret 只输出对象名、key 名、presence、fingerprint 或 redacted prefix;禁止打印完整 token/key。
- D601 是默认 active targetD518/G14 等 target 以 YAML 和 issue 明确目标为准。
- Codex pool、统一 API key、master `~/.codex` 配置、FRP/Caddy 暴露、账号增删都必须走本技能的受控 CLI。
默认输出类似 k8s/ps 的短表;机器处理用 `--raw` 读取 `.data.trace.*`;需要审计原始匹配日志时加 `--show-lines`;需要扩大搜索范围时使用 `--since 24h --tail 50000`。该命令只读:读取 Sub2API 日志、账号快照和 admin API 元数据,不改 `schedulable`、不清 runtime backoff、不中断请求。
## 何时读取 reference
## 先读边界
- 仓库长期开发边界见 `docs/reference/platform-infra.md`,本 skill 承担日常操作手册
- 配置真相是 YAML`config/platform-infra/sub2api.yaml``config/platform-infra/sub2api-codex-pool.yaml`
- 业务策略和具体数值只以 YAML 为准。已有字段的数值调整只改 YAML 并跑 `plan` / `sync --confirm` / `validate`;不要自动补代码硬编码、schema 硬范围、合同测试、单元测试或长期参考文档。配置校验只校验格式、类型、必填和可渲染性,不判断数值策略是否“合理”。
- 本 skill 目录下若存在 `agents/*.yaml`,只作为 skill/agent 展示与调用元数据,不是 Sub2API 或 Codex pool 运行配置;不要在 skill 目录维护第二份账号、capacity、priority、endpoint 或 Secret 配置。
- Runtime target 由 `config/platform-infra/sub2api.yaml` 声明;默认 target 是 `D601:k3s``D518:k3s` 这类 external-active target 必须通过显式 `--target` 选择,`G14:k3s` 是 standby target。master server 只是控制端和消费者,不部署 Sub2API/PostgreSQL/Redis。
- Standby target 不部署本地 PostgreSQL,不运行 sentinel、FRP 管理入口或 HTTPS egress proxy;只能预部署 namespace、NetworkPolicy、Service,以及 replicas=0 的 Sub2API/Redis Deployment。Redis 激活后也只允许 ephemeral cache。External-active target 仍不部署本地 PostgreSQL,必须直连 YAML 声明的外置 DB,使用本地 ephemeral Redis,并且只有在 YAML 启用时才运行 frpc、egress proxy 和目标级 sentinel;多个 external-active target 可以并存,不得把一个 target 的 public exposure 当作另一个 target 的替代或回退。
- Secret、`~/.codex/config.toml*``~/.codex/auth.json*` 是运行时输入或本地状态,不提交。
- 默认 `~/.codex/config.toml``~/.codex/auth.json` 只作为统一 Sub2API consumer 使用;`config.toml` 必须指向 YAML-selected active target 的 consumer URL,当前由 `codex-pool configure-local --confirm` 写入 D601 target-level public URL`auth.json` 必须使用统一 pool API key。新增上游账号不得覆盖这两个默认文件,只能新增 `config.toml.<profile>` / `auth.json.<profile>` 并在 YAML 里声明。
- 输出只能包含 Secret 路径、key 名、presence、fingerprint 和 `valuesPrinted=false`;禁止打印完整 API key、admin password、JWT secret、TOTP key、base64 payload 或可复制的 preview。
## 部署与状态
```bash
bun scripts/cli.ts platform-infra sub2api plan
bun scripts/cli.ts platform-infra sub2api plan --target G14
bun scripts/cli.ts platform-infra sub2api apply --dry-run
bun scripts/cli.ts platform-infra sub2api apply --target G14 --dry-run
bun scripts/cli.ts platform-infra sub2api apply --confirm
bun scripts/cli.ts platform-infra sub2api apply --target G14 --confirm
bun scripts/cli.ts platform-infra sub2api status
bun scripts/cli.ts platform-infra sub2api status --target G14
bun scripts/cli.ts platform-infra sub2api validate
bun scripts/cli.ts platform-infra sub2api validate --target G14
```
- `plan` 读取 `config/platform-infra/sub2api.yaml`,渲染 `src/components/platform-infra/sub2api/sub2api.k8s.yaml`,检查 no Ingress/NodePort/LoadBalancer/hostPort/hostNetwork/resource limits,并要求 `NetworkPolicy/allow-all` 随 manifest 受控创建。
- `apply --confirm` 默认创建异步 job;按返回的 `job status` 命令轮询,再跑 `status``validate`
- `status --full|--raw` 只在需要展开远端 stdout/stderr 或原始 JSON 时使用。
- `validate` 是按需验收,不是连续可用性探针。对 standby target`validate --target <id>` 验证预部署形态,不要求外置 DB 当前可连接;对 external-active target,必须验证外置 DB、ephemeral Redis、Sub2API service、YAML egress proxy 和目标级 public exposure。
## D601 Egress Proxy
D601 的目标级 `egressProxy` 完全由 `config/platform-infra/sub2api.yaml` 控制。当前成熟形态是 master Docker `shadowsocks-rust` 作为加密出站源,D601 k3s 内 `sing-box` 暴露 HTTP/mixed ClusterIP proxy 给 Sub2API 和按 YAML 启用的 sentinel 使用。不要把 endpoint、端口、密码、健康探针或镜像 tag 写进 skill;只以 YAML 和 `config/platform-infra/sub2api-master-egress-proxy.compose.yaml` 为准。
master 侧 proxy 由 UniDesk checkout 内的 compose 文件管理:
```bash
docker compose -f config/platform-infra/sub2api-master-egress-proxy.compose.yaml up -d --force-recreate
bun scripts/cli.ts platform-infra sub2api apply --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api validate --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
```
proxy secret/config 文件只允许放在受控 Secret/state 路径,输出只能披露路径、presence、fingerprint 或摘要,不能打印密码、完整订阅或生成配置。若 D601 到上游的 TLS/SNI 路径被 reset,不要用临时 JS 或简陋 HTTP CONNECT proxy 作为最终方案;通过 YAML/compose 更换或修复成熟加密 proxy source,再跑上面的 apply/validate/sync/validate 闭环。
## 镜像升级
1. 修改 `config/platform-infra/sub2api.yaml``image.repository``image.tag``pullPolicy`
2. 执行 `sub2api plan`,确认策略检查通过。
3. 执行 `sub2api apply --confirm`,轮询 job 完成。
4. 执行 `sub2api status`,确认运行镜像等于 YAML 声明。
5. 执行 `sub2api validate``codex-pool validate` 做入口验收。
不要把镜像版本写进脚本常量、JSON 或 manifest 模板。
## Codex Pool
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool plan
bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --target D601 --account unidesk-codex-hy --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D601 --confirm
```
`config/platform-infra/sub2api-codex-pool.yaml` 控制:
- `pool.groupName`: Sub2API group 名称。
- `pool.apiKeySecretName` / `pool.apiKeySecretKey`: 统一消费 API key 的 k3s Secret 位置,默认 `platform-infra/sub2api-codex-pool-api-key.API_KEY`
- `pool.minOwnerBalanceUsd`: pool key owner 最低余额,sync/validate 会补齐。
- `pool.minOwnerConcurrency`: 可选统一消费 API key owner 最低并发;省略时 CLI 自动使用所有已解析账号 capacity 的总和,sync/validate 会补齐。显式 YAML 值只作为 override,仍必须不小于账号 capacity 总和;未显式写 `profiles.entries[].capacity` 的账号会使用 `pool.defaultAccountCapacity` 参与求和,不要用提高某个 provider capacity 来掩盖用户并发层 WS 1013。
- `pool.defaultTempUnschedulable`: Sub2API 内置请求路径临时不可调度开关和 YAML 规则列表。当前要求是按 YAML 开启通用规则;sync 把 `temp_unschedulable_enabled` / `temp_unschedulable_rules` 渲染到 managed accounts,让匹配的 400/5xx/超时/模型路由/加密内容错误短暂冷却当前账号并触发同组 failover。
- `pool.defaultTempUnschedulable` 与外部 `sentinel.*` 分开配置、互不驱动。内置规则负责 near-real-time request-path cooling/failover;哨兵负责 marker health、账号级隔离/恢复和 probe 退避。
- 外部 sentinel 的写入面只允许通过 Sub2API admin `schedulable` 接口冻结/恢复账号;不能写入、清理或间接清理 `temp_unschedulable_until` / `temp_unschedulable_reason`、rate-limit、overload、model-rate-limit 等 Sub2API 请求路径 runtime 状态,也不能调用 `recover-state` 作为恢复动作。看到 UI 里的“触发时间/解除时间/规则序号/匹配关键词”临时不可调度状态时,默认先归因到 Sub2API 内置 request-path temp-unschedulable,而不是 sentinel。
- YAML 只选择和配置 Codex 上游,不声明 `schedulable` 长期字段;`codex-pool sync --confirm` 不负责把既有账号恢复为 `schedulable=true`。既有账号的 `schedulable=false` 必须由哨兵先同步 Sub2API runtime 状态,再在 marker probe 命中后恢复。
- `profiles.entries`: 从 master `~/.codex/` 选择上游 profile 并映射到 Sub2API account。
- `profiles.entries[].capacity`: 可选 per-account concurrency override;不写则使用 `pool.defaultAccountCapacity`。具体数值只以 `config/platform-infra/sub2api-codex-pool.yaml` 为准,skill 和长期参考只描述规则,不重复写当前值。
- `profiles.entries[].loadFactor`: 可选 per-account Sub2API `load_factor` override;不写则使用 `pool.defaultAccountLoadFactor`。具体数值只以 `config/platform-infra/sub2api-codex-pool.yaml` 为准,修改后必须 `codex-pool sync --confirm``codex-pool validate`
- `profiles.entries[].trustUpstream`: 可选账号级哨兵信任标记;默认 `false`。可信账号使用 `sentinel.cadence.trustedSuccessMaxIntervalMinutes` 作为连续成功后的最大探测退避,不可信账号使用 `sentinel.cadence.untrustedSuccessMaxIntervalMinutes`。它只影响哨兵探测频率和状态可见性,不改变 Sub2API account priority/capacity/loadFactor。
- `pool.defaultSentinelProtect`: 账号级哨兵保护默认策略;是否启用、连续确认次数、初始延迟、最大延迟和退避倍率都只以 YAML 为准。marker probe 或 gateway failure 触发冻结前会先按该策略做连续 marker 确认,只有全部失败才进入冻结状态机。
- `profiles.entries[].sentinelProtect`: 可选账号级哨兵保护覆盖;只用于明确偏离 pool 默认策略。它只影响哨兵冻结判定和 `sentinel-report` 可见性,不改变 Sub2API account priority/capacity/loadFactor。
- 除非用户明确要求修改配置,不要仅凭推断改账号 membership、priority、capacity、loadFactor、WebSocket mode 或其他调度策略;先保留 YAML,完成 provenance/runtime evidence 溯源,并把结论写回相关 issue 或 runbook 后再提出变更。
- Sub2API 是 UniDesk 可读源码和可观测运行面的受控组件;排查 Sub2API 调度、failover、错误传播、临时不可调度或 account selection 时,默认先读当前 Sub2API 源码实现,再用真实 request id、Sub2API 日志和原入口流量验证。不要用 mock upstream、临时 probe account 或测试桩作为默认结论来源;这类探针最多是显式 debug 辅助,不能替代源码链路和真实运行证据。
- `profiles.entries[].tempUnschedulable`: 可选 per-account Sub2API 内置临时不可调度覆盖;只用于明确偏离 pool 默认规则,不用它给某个账号特殊优先级或临时绕过通用 failover。
- `profiles.entries[].openaiResponsesWebSocketsV2Mode`: 需要 Responses WebSocket v2 的上游才设置,值为 `off``ctx_pool``passthrough`
- `profiles.entries[].upstreamUserAgent`: 少数要求 Codex CLI User-Agent 的上游才设置,不能含换行。
- `manualAccounts.protected`: 已在 Sub2API 手动创建/维护、且必须排除在 UniDesk-managed Codex pool credentials 和 sentinel 控制之外的账号。默认不得改 credentials/status/schedulable/priority/capacity/loadFactor;只有显式声明 `proxyBinding` 时,`sync --confirm` 才允许把该账号的 `proxy_id` 对齐到 YAML 目标的 egress proxy;只有显式声明 `groupBinding.source: pool-group` 时,才允许把该账号加入统一消费 API key 使用的 pool group。
- `sentinel.monitor.enabled`: 账号级 marker 哨兵监控开关;开启后 `codex-pool sync --confirm` 会在 `platform-infra` 创建/更新 k8s CronJob、ConfigMap、Secret、ServiceAccount、Role 和 RoleBinding。CronJob 直打 YAML-managed 上游账号的 OpenAI Responses `gpt-5.5`,用确定 marker 作为唯一健康标准,并在独立 state ConfigMap 中记录 token/cost 账本。
- `sentinel.actions.enabled`: 账号级哨兵冻结/恢复动作开关;当前 marker-only guard 要求开启。动作关闭时只记录 `would-freeze`,不会调用 Sub2API admin API 改 `schedulable`。动作开启后,只要不满足 marker match,不论是 HTTP 200 私货、4xx/5xx、非 JSON、连接错误还是空输出,都进入同一个冻结/恢复状态机。
- `sentinel.sdk.openaiPythonVersion`: 哨兵容器使用的 OpenAI Python SDK 固定版本;模型请求必须通过标准 SDK `responses.create`,不要手工拼 `/v1/responses` 请求体或手写响应解析。后续升级 SDK 只改 YAML 并 `sync --confirm`
- `sentinel.probe.maxOutputTokens`: 哨兵本地流式 delta 收集上限,必须保持小值;它不作为上游 `max_output_tokens` 字段发送,以保持与 Sub2API WebUI 默认账号连接测试的 Responses SSE 请求形态一致。哨兵不限制并发和每轮账号数,所有到期账号会在同一轮并发探测。
- `sentinel.probe.userAgent`: 哨兵 direct upstream probe 的默认 User-Agent,通过 OpenAI SDK `extra_headers` 传递;默认贴近 Sub2API `net/http` 账号连接测试形态,个别账号仍可用 `profiles.entries[].upstreamUserAgent` 覆盖。
- `sentinel.cadence`: 成功信任指数退避配置。当前口径是从 1 分钟开始,连续成功后按账号 `trustUpstream` 选择可信/不可信最大退避;任意非 marker match 清零成功信任并进入冻结退避。可信/不可信最大退避数值只写 YAML。
- `sentinel.freeze`: 失败冻结 TTL 指数退避配置。当前口径是初始 1 分钟,失败后 `1m -> 2m -> 4m -> 8m -> 10m`,最大 10 分钟;失败 probe 基本不消耗有效输出 token,因此冻结窗口保持短周期。冻结到期后只做恢复 probe,通过才自动恢复,不能仅靠 TTL 到期解封。
- `sentinel.pricing`: 直打上游时哨兵自己的 token/cost 估算价格。因为 direct upstream probe 不经过 Sub2API 普通用量账本,哨兵必须自己记录全局与 per-account token/cost;这些账本只用于观察,不作为跳过探测的预算门禁。
`sync --confirm` 会登录 Sub2API admin、创建/更新 group、创建/更新 YAML 中的 `unidesk-codex-*` accounts、创建/复用统一 API key Secret,并部署/更新哨兵资源;它不把既有 managed account 直接恢复为 `schedulable=true`。恢复只由哨兵在读取 Sub2API runtime `schedulable=false` 后触发 recovery probe,并在 marker 命中时执行。`sync` 默认不删除 YAML 中缺席的 managed account。只有明确退役上游时才使用 `sync --confirm --prune-removed` 删除缺席且 `extra.unidesk_managed=true``unidesk-codex-*` account。对 `manualAccounts.protected``sync` 只执行 YAML 显式允许的窄同步;当前允许项是从目标 `egressProxy` 创建/更新 Sub2API internal proxy 记录并绑定 `proxy_id`,以及把受保护手动账号加入当前 `pool.groupName`。它仍不接管该账号凭据、status、schedulable、priority/capacity/loadFactor 或哨兵状态。
`sentinel-image status|build` 管理哨兵 Python 运行环境镜像。镜像由 YAML 的 `sentinel.image` 基础镜像和 `sentinel.sdk.openaiPythonVersion` 派生,发布到目标 runtime 的本地 registry`build --confirm` 会先检查 registry tag,存在则快速复用,不存在才在目标 host 构建并 push。CronJob 启动时只校验 SDK 版本,不在运行时 `pip install`。目标是否启用哨兵以 `config/platform-infra/sub2api.yaml``sentinel.enabledOnTargets` 为准;未启用的 target 在 `sync`/`validate` 中应显示 `skipped-target-disabled`,不得要求镜像构建、CronJob、Secret 或 state ConfigMap 存在。
`sync --confirm` 同时会按 YAML 渲染账号级哨兵资源,并在 monitor 开启时先确保可复用哨兵镜像存在。当前目标是 `sentinel.monitor.enabled=true` + `sentinel.actions.enabled=true` 的 marker-only 自动冻结/恢复;不要手工 patch CronJob、Secret 或 Sub2API account。若 YAML 新增账号或修改 profile/base URL/API key fingerprint/upstream User-Agent/Responses WebSocket modesync 会从变更前 runtime state 写入 pending probe 记录并立即安排 sentinel probe,但不会把既有账号直接恢复为可调度;只有 sentinel 读取到 Sub2API runtime `schedulable=false` 后执行 recovery probe,且 marker 命中,才恢复 `schedulable=true`。sentinel 冻结/恢复只改 `schedulable=false|true`,不得顺手调用 Sub2API `recover-state` 清除请求路径临时不可调度或其他 runtime backoff。无关账号的既有成功/失败退避不能被重置。若 YAML 下调失败冻结最大窗口,sync 会把仍 active 的旧冻结状态迁移到当前最大窗口内并立即安排 recovery probe,但不会直接解冻。若怀疑某个账号被误判,先用 `codex-pool sentinel-probe --account <accountName> --confirm` 立即触发该账号测量;该命令从现有 CronJob 模板派生一次性 Job,复用同一份 Secret、ConfigMap、OpenAI SDK probe、token/cost 账本和冻结/恢复状态机。
`trace --request-id <requestId>` 是只读 request 追溯报表,不触发 probe、不修改账号。默认输出请求开始/最终状态、failover、`account_select_failed`、窗口内 `account_temp_unschedulable`、admin schedulable 写入计数和当前账号快照;`reason=failover-attempted-no-candidate` 表示 Sub2API 已进入自动切号,但排除当前失败账号后没有可用候选。需要机器处理时使用 `--raw`,需要原始匹配行时加 `--show-lines`
`sentinel-report` 是只读低噪声报表,不触发 probe、不修改账号。默认输出类似 `ps` 的文本表,展示每个账号的探测次数、Sub2API runtime `schedulable`、最近 marker/HTTP/动作、冻结 TTL、成功退避、下一次 probe 和最近 run 事件;`SCH` 展示 Sub2API runtime schedulable`PROT` 展示账号级保护阈值,`P_FAIL` 展示最近一次保护确认中的失败次数/阈值;需要机器处理时使用 `sentinel-report --raw`
`sync --confirm``validate` 可能超过单次 SSH/runtime 短连接窗口。必须继续使用 `bun scripts/cli.ts platform-infra sub2api codex-pool ...`,由 CLI 在 G14 远端提交作业并短轮询状态;不要改用裸 `trans G14:k3s sh` 等一个长连接等待完整结果。若看到 `UNIDESK_SSH_RUNTIME_TIMEOUT`,先按 `docs/reference/platform-infra.md` 的规则处理为控制面可见性问题,修 CLI/job/poll 或重跑受控命令,不要手工 patch Sub2API credentials 或源码。
不要给 UniDesk-managed Codex accounts 开 Sub2API `pool_mode`。UniDesk 期望的 failover 是把失败账号临时标记为 unschedulable,让同组其他账号接手;`pool_mode` 会重试同一个 account path。
WebSocket v2 是账号能力集合,不是调度 pin。`openaiResponsesWebSocketsV2Mode` 只声明该账号可承担 Codex Responses WSv2 链路;只有 `localCodex.supportsWebSockets=true` / `localCodex.responsesWebSocketsV2=true` 时,`codex-pool validate` 才必须看到至少一个 `webSocketsV2.schedulableEnabled` 账号。真实可用性仍以 direct Codex WSv2 probe、Sub2API 日志和原入口 Codex smoke 为准。
Codex 启动时反复出现 WebSocket reconnect、HTTPS fallback、`websocket closed by server before response.completed`,或 Sub2API 日志出现 `openai.websocket_proxy_failed` / `openai.websocket_account_select_failed` / 上游 WS handshake 4xx/5xx 时,先按运行证据定位具体 account 和 transport。若账号的 WSv2 握手失败,优先只在 YAML 中把该账号的 `openaiResponsesWebSocketsV2Mode` 收敛为 `off`;若没有任何 direct Codex WSv2 probe 通过,则同时把 `localCodex.supportsWebSockets``localCodex.responsesWebSocketsV2` 收敛为 `false`,再 `codex-pool sync --confirm`。不要顺手改 membership、priority、capacity、Secret 或代码 fallback。
## 受保护手动账号代理与分组绑定
Sub2API 管理 UI 的账号连接测试使用账号级 `ProxyID` / proxy URL 配置上游 HTTP transport;账号没有绑定 proxy 时会直接出站,即使 Sub2API Pod 已经有 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量。看到 WebUI 账号测试连 `chatgpt.com` 超时、但 Pod 内显式走目标 proxy 可通时,先检查该账号是否属于 `manualAccounts.protected` 并声明了 `proxyBinding`。如果同一账号用 `gpt-5.2-pro` 返回 ChatGPT OAuth 不支持 Codex 的模型能力错误,但默认/受支持模型能完成 `hi``/v1/responses` smoke,这不是代理失败;按模型映射/账号能力另行处理。
WebUI 账号连接测试也不经过统一消费 API key 的 pool group 选择器;账号测试正常不代表 PC Codex 客户端能选中该账号。看到 WebUI 账号测试正常、但 `/responses``/v1/responses``account-select-failed` / `no available accounts` 返回 503 时,先检查该手动账号是否声明了 `groupBinding.source: pool-group`,并通过 `sync --confirm` 加入当前 `pool.groupName`
受保护手动账号仍由人工在 Sub2API UI 维护 credentials/status 等字段;UniDesk 只允许通过 YAML 做代理和分组窄绑定:
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
```
`sync` 输出应显示 `manualAccounts.ok=true``proxySync.ok=true``groupSync.ok=true`,且该账号的 proxy/group `bindingAligned=true``sentinel-probe --account <manual-account> --confirm` 对受保护手动账号必须继续拒绝,通常返回 `account-protected-manual`;不要为了测试而把该账号移入 `profiles.entries` 或取消保护。需要证明 WebUI 同款账号测试恢复时,用 Sub2API admin account test 原入口测最小 `hi` 和默认/受支持模型,并只记录 account id、proxy id、event types、HTTP status 和短 output preview,不记录 OAuth token 或 Secret 明文。若指定模型返回 “model is not supported when using Codex with a ChatGPT account” 一类能力错误,先归因到模型能力/映射,而不是 proxy。
## 添加上游
1. 在 master `~/.codex/` 准备带后缀的上游 profile 文件,例如 `config.toml.<profile>``auth.json.<profile>`;禁止覆盖默认 `config.toml` / `auth.json`
2.`config/platform-infra/sub2api-codex-pool.yaml` 添加 `profiles.entries` 项,指定 `profile``accountName``configFile``authFile`
3. 如需要,给该项加 `priority``capacity``loadFactor``trustUpstream``sentinelProtect``openaiResponsesWebSocketsV2Mode``upstreamUserAgent`capacity/loadFactor/信任退避/保护阈值的具体数值只写在 YAML。只有显式恢复 Sub2API 内置临时不可调度时才添加 per-account `tempUnschedulable`
4. 如果新增账号会提高声明 capacity 总和,默认让省略的 `pool.minOwnerConcurrency` 继续按 capacity 总和自动解析;只有 YAML 已经显式写了该 override 时,才同步提高到不低于总 capacity,或删除 override 回到自动解析。
5.`codex-pool plan`,确认 profile 可读、`base_url` 和 API key 来源有效,且 stdout 未泄露完整 key。
6.`codex-pool sync --confirm`
7.`codex-pool validate`
普通新增上游是 YAML 操作,不走 CI/CD,不改代码。只有需要渲染或校验上游 Sub2API 已经存在的可复用能力时才修改 `scripts/src/platform-infra-sub2api-codex.ts`;Sub2API 本身不支持的能力不在 UniDesk 侧魔改实现。
## 删除上游
删除上游只用于明确退役、凭据所有权变更或用户明确要求移除 provider;不能作为上游 5xx、compact 失败、限流、模型路由失败或哨兵隔离/恢复问题的处理手段。
1.`config/platform-infra/sub2api-codex-pool.yaml` 删除对应 `profiles.entries` 项。
2.`codex-pool plan` 检查 desired 列表。
3.`codex-pool sync --confirm --prune-removed`
4. 确认输出 `accounts.pruned` 只包含期望删除项。
5.`codex-pool validate`
CLI 默认保留缺席账号,避免把可用性问题误处理成删除;只有显式 `--prune-removed` 才会 prune `name``unidesk-codex-` 开头且 `extra.unidesk_managed=true` 的缺席账号。
## FRP 暴露
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool expose
bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm
```
- 由 YAML `publicExposure` 控制。Codex pool 默认公共端是 `publicBaseUrl`master 本地消费端是 `masterBaseUrl`external-active target 的目标级 public exposure 在 `config/platform-infra/sub2api.yaml` 中声明。
- `expose --confirm` 只为 YAML 指定的 `remotePort` 补 master `frps` allow port,并在 G14 创建/更新 `sub2api-frpc`
- master Caddy site 也由 `publicExposure.masterCaddy` 渲染;`responseHeaderTimeoutSeconds` 必须足够覆盖 Codex `/responses/compact` 长请求,避免 Caddy 先返回 504 而 Sub2API 后台实际稍后成功。具体数值只改 `config/platform-infra/sub2api-codex-pool.yaml`,修改后跑 `codex-pool expose --confirm`,再核对 Caddyfile 中渲染出的 `response_header_timeout`
- master Caddy 的短窗口边缘重试由 `publicExposure.masterCaddy.edgeRetry` 渲染;用于吸收 FRP remotePort 短暂关闭、`connect: connection refused`、EOF 或 connection reset 这类请求尚未稳定到达 Sub2API 的 502。具体 retry 时长、间隔和 `retryMatch` 范围只写 YAML,修改后跑 `codex-pool expose --confirm`,再核对 Caddyfile 中渲染出的 `lb_try_duration``lb_try_interval``lb_retry_match`。不要手工 patch `/etc/caddy/Caddyfile`
- PK01 `/etc/caddy/Caddyfile` 是 Sub2API、LangBot、n8n、HWLAB 等多 YAML 来源共享的 edge artifact。Sub2API apply/expose 只能更新自己的 managed block 并保留其他 blocks;同一 Sub2API 服务暴露多个 target 时,D601 保留既有 `# BEGIN unidesk managed sub2api`,非默认 target 必须使用 target-scoped owner(例如 `sub2api-d518`),避免 `api.pikapython.com``api2.pikapython.com` 互相覆盖。若 apply 输出显示 managed block 数异常,先停止 closeout,检查 PK01 Caddy 合并与 validation 结果,不要手工整文件覆盖。
- 非幂等 POST 的 round-trip retry 必须收窄到 YAML `retryMatch` 声明的安全路径;普通 `/responses` 上游账号错误仍归 Sub2API failover / temp-unschedulable / sentinel 处理,不用 Caddy 重放整段推理请求来掩盖账号池问题。
- 同一个 FRP TCP 入口同时暴露 OpenAI-compatible API 和 Sub2API 管理 UI `/login`。不要另开第二个管理端口,除非 YAML 明确声明新的暴露决策。
- Sub2API Kubernetes Service 继续保持 ClusterIP。
- External-active target 的公开路径是 `client -> PK01 Caddy -> PK01 frps remotePort -> target frpc -> Sub2API`,不经过 pikanode,也不经过 master server 反代。PK01 Caddy 下载必须使用 YAML `publicExposure.pk01.caddyDownloadProxyUrl` 指定的 proxy;如果 Caddy 下载慢,先确认 apply 输出里是 `downloadProxy.mode=curl-proxy`。目标域名必须先解析到 YAML 声明的 PK01 公网地址,HTTPS 才能作为最终验证;D601 `api.pikapython.com` 与 D518 `api2.pikapython.com` 应分别验收。
## 配置 master Codex 消费端
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool configure-local
bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm
```
`configure-local --confirm` 会:
-`platform-infra/<apiKeySecretName>.<apiKeySecretKey>` 读取统一 API key。
- 把当前 `~/.codex/config.toml``~/.codex/auth.json` 备份为 `.<backupSuffix>`,默认 `.pre-sub2api`
- 重写默认 `~/.codex` 消费端,固定指向 `https://sub2api.74-48-78-17.nip.io/`provider 名称和 wire API 来自 `localCodex`
-`localCodex.modelContextWindow` / `localCodex.modelAutoCompactTokenLimit` 写入 `model_context_window` / `model_auto_compact_token_limit`,用于统一控制 Codex auto compact 触发窗口,避免 GPT-5.5 消费端生成过大的 `/responses/compact` 长请求。
-`localCodex` 写入 Codex transport 标记:`supports_websockets``[features] responses_websockets_v2` 必须同开同关。只有至少一个上游通过 direct Codex WSv2 probe 时才启用;否则保持 HTTP Responses,避免每次原入口先经历无效 WS reconnect。
- 用统一 key 做一次 gateway 验证。
防递归规则:默认 `config.toml` / `auth.json` 是 Sub2API consumer,不得作为上游账号导回 pool;上游账号必须使用带后缀 profile 文件,并通过 `config/platform-infra/sub2api-codex-pool.yaml``profiles.entries` 增删。
## 验收口径
部署 closeout 至少包含:
- `sub2api status`Deployment/StatefulSet/Service/Secret/NetworkPolicy 可见,运行镜像与 YAML 一致,`NetworkPolicy/allow-all` 符合 `podSelector: {}`、Ingress/Egress 全放行。
- `sub2api validate`app、PostgreSQL、Redis、service proxy、`NetworkPolicy/allow-all` 和临时跨 Pod PostgreSQL/Redis 连通性检查通过。
- `codex-pool validate`:统一 key 的 `GET /v1/models` 成功,并用 `localCodex.responsesSmokeModel` 跑一次小的 `POST /v1/responses` smokeowner balance / owner concurrency 已满足 YAML 最小值,capacity、WebSocket v2、Sub2API 内置 temporary-unschedulable 开关/规则和 sentinel runtime 状态与 YAML 对齐;`validation.gatewayResponsesRecent` 汇总最近 6 小时普通 `/responses``/v1/responses` 的 failover、forward failure、最终 4xx/5xx、慢 final error 与 `context canceled` 证据,`validation.gatewayCompactRecent` 单独汇总 `/responses/compact` 证据。若当前 Responses smoke `ok=true` 但 recent 字段 `degraded=true`,先区分是历史窗口残留还是新的 request id 正在失败;长期判定见 `docs/reference/platform-infra.md`
-`publicExposure.enabled=true`,确认 FRP path 可用;`expose --confirm` 会用未带 key 的 public `/v1/models` 401 作为网关可达性探针。
- 多 target 同时启用 public exposure 时,必须分别验证每个 target 的 root、`/health`、未带 key `/v1/models` 401,以及各自 `codex-pool validate --target <id>`;一个域名可用不能替代另一个域名的验收。
- 若目标声明了 `egressProxy.enabled=true`,确认 proxy Deployment/Service readySub2API 和 sentinel env 与 YAML 对齐,并通过 YAML 声明的 health URL 完成代理出站探针。
如果要证明真实模型请求可用,使用最小 `/v1/responses` 或等价 Codex smoke。不要把 group-level `/v1/models` 成功解释成每个上游 account 都健康。
## 排障
- Codex pool 哨兵、账号冻结/恢复、marker-only 判断或 probe 周期看不清:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report`。这个报表是主观察面;只有报表缺字段或需要底层证据时,才继续看 `--raw`、CronJob log、state ConfigMap 或 Sub2API 管理 UI。若看到“临时不可调度状态”且包含规则序号/匹配关键词,检查 Sub2API `account_temp_unschedulable` 日志和账号 `temp_unschedulable_*` 字段;sentinel 只解释 `schedulable=false` 的 active quarantine,不解释这类内置临时冷却。
- 只加强监控、不让哨兵自动冻结账号时,把 YAML `sentinel.actions.enabled=false``codex-pool sync --confirm`。此时 marker probe 和 gateway failure monitor 仍记录 `would-freeze` / observe-only 证据,但不会通过 Sub2API admin 写 `schedulable=false``/responses/compact``codex.remote_compact.failed` 和 compact 上游 5xx failover 只作为 `gateway-compact-*` 观察事件记录,不作为哨兵自动切换触发器。
- 单个 request id 报 502/503/中断/没有自动切号:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>`。先看 `outcome``reason``FAILOVER``SELECT-FAILED``ACCOUNT SIGNALS``WINDOW STATS`;只有 trace 报表缺字段或需要审计原始日志时,才加 `--show-lines``--raw`。若 `reason=failover-attempted-no-candidate`,说明切号动作已发生,但 scheduler 在排除失败账号后没有可用候选;继续用 `sentinel-report``validate --full` 区分 sentinel quarantine、request-path temp-unschedulable、账号 status 或容量耗尽。
- profile invalid:先修 `~/.codex/config.toml.<profile>``base_url``wire_api``model``auth.json.<profile>` 的 API key;不要在 YAML 中写密钥。
- 手动 OAuth/API-key 账号的 WebUI account test 连 `chatgpt.com` 超时,但同一 Pod 显式 HTTP proxy 探针可通:不要只看 Pod `HTTP_PROXY` env,按“受保护手动账号代理与分组绑定”小节确认 `manualAccounts.protected[].proxyBinding`,跑 `codex-pool sync --target D601 --confirm` 后再用原账号测试复测。若复测不再 reset/timeout,而是 `gpt-5.2-pro` 这类指定模型返回 ChatGPT OAuth Codex 不支持的能力错误,用默认/受支持模型或统一 key smoke 验证代理,不要把模型错误当作代理仍坏。
- 手动 OAuth/API-key 账号 WebUI account test 正常,但 PC Codex 客户端通过统一 key 访问 `/responses` 返回 503 且 trace 是 `account-select-failed` / `no available accounts`:按“受保护手动账号代理与分组绑定”小节确认 `manualAccounts.protected[].groupBinding.source: pool-group`,跑 `codex-pool sync --target D601 --confirm` 后用 `codex-pool validate --target D601 --full` 复测统一 key。
- Sub2API 卡在 `wait-postgres` / `wait-redis` 或服务内大量 `context deadline exceeded`:先跑 `sub2api status``networkPolicy.ok`,再跑 `sub2api validate``postgresCrossPodPgIsReady` / `redisCrossPodPing`;缺失或异常时用 `sub2api apply --confirm` 恢复受控 `NetworkPolicy/allow-all`,不要保留手工 iptables bypass 作为长期修复。
- pool key 401:跑 `codex-pool sync --confirm` 重建 Sub2API key 与 k3s Secret 绑定,再跑 `codex-pool validate`
- pool key、admin password 或 k8s Secret `.data` 被 stdout、日志、issue 或本地 transcript 打印时,按泄露处理:撤销对应 Sub2API key 或 token,删除/重建受影响的 target Secret,通过 `codex-pool sync --target <id> --confirm` 或相应 YAML sourceRef 重新下发,再用 fingerprint、presence 和 `valuesPrinted=false` 作为 closeout 证据;不要复述旧值或新值。
- 运行中过去的验证探针残留:只用 `codex-pool cleanup-probes --confirm` 清理 `unidesk-probe-*` 临时资源;不要把真实 managed account 删除当作探针清理或可用性恢复。
- FRP 不通:先看 `codex-pool expose --confirm` 输出的 `masterFrps``masterCaddy``sub2api-frpc` 和 public 401 probe;需要低层证据时只用 `trans G14:k3s` 做 bounded 查询。
- D601 external-active 的 `api.pikapython.com` 不通:先区分 DNS/TLS/Caddy/FRP/Sub2API。DNS 未解析到 YAML 声明的 PK01 地址时,Caddy ACME 会失败,`https://api.pikapython.com` 不能算完成;可用 PK01 loopback FRP 端口和 PK01 公网 remotePort 证明 D601 FRP 数据路径,但最终仍要等 DNS 生效后重跑 HTTPS health、`/v1/models``/v1/responses`
- D601 external-active apply 后其他 PK01 HTTPS 服务消失:优先怀疑共享 Caddy managed block 合并失败或旧整文件写入路径复现。用受控 Sub2API apply 输出和 PK01 Caddy managed block markers 取证,再通过各服务自己的 YAML apply/public-exposure 入口恢复;不要手工复制某一份 Caddyfile 作为长期修复。
- Caddy 下载慢或失败:先确认 `config/platform-infra/sub2api.yaml` 已设置 `publicExposure.pk01.caddyDownloadProxyUrl`,并重跑 `sub2api apply --target D601 --confirm` 看 PK01 apply summary 中的 `downloadProxy.mode=curl-proxy`。不要反复裸连 GitHub release。
- `/responses/compact` 在接近 master Caddy `response_header_timeout` 的固定时长后返回 504,或 Sub2API 日志稍后记录 `codex.remote_compact.succeeded` 时,优先检查 master Caddy `response_header_timeout` 是否由 YAML `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` 渲染,修正后跑 `codex-pool expose --confirm`;这类边缘代理超时不会触发 Sub2API 账号级临时下线。reload 前已经在途的 compact 请求仍可能按旧 timeout 结束,判断修复是否生效时只看 reload 之后新发起的请求。
- `/responses/compact` 或普通 public URL 在几秒窗口内出现 502Caddy 日志显示 `dial tcp 127.0.0.1:<remotePort>: connect: connection refused``EOF``connection reset by peer`,同时 frps 日志出现 `platform-infra-sub2api proxy closing` / `listener is closed` / `new proxy ... success`,说明失败在 master Caddy 与 FRP remotePort 边缘层,Sub2API 和 sentinel 可能完全看不到。先确认 `publicExposure.masterCaddy.edgeRetry` 已按 YAML 渲染并 `codex-pool expose --confirm` 生效;若仍频繁发生,再继续查 G14 `sub2api-frpc` 到 master `frps` 的控制连接稳定性。不要把这类边缘 502 误判成账号池上游错误,也不要通过禁用账号恢复。
- default profile 递归:检查 YAML default entry 是否使用 `*.pre-sub2api` 备份文件;必要时恢复备份后重新 `configure-local --confirm`
- 上游需要 WebSocket v2:先做 direct Codex WSv2 probe;通过后才给该 profile 配 `openaiResponsesWebSocketsV2Mode: ctx_pool|passthrough` 并跑 `sync --confirm`;把它当 capability candidate,容量仍以 YAML 中的 `capacity` 或默认值为准。
- Codex 启动 WebSocket 回退:用原入口 Codex smoke 复现,再用 bounded Sub2API 日志确认 account;对 WS handshake 4xx/5xx、`openai.websocket_account_select_failed` 或 close-before-`response.completed` 的账号关闭 YAML WSv2 能力后同步。若没有剩余 WSv2-capable account,把 `localCodex.supportsWebSockets``localCodex.responsesWebSocketsV2` 一起关掉,不把临时可用性推断写成调度配置。
- 上游要求 Codex User-Agent:只给该 profile 配 `upstreamUserAgent`,跑 `sync --confirm`
- 上游报 capacity/rate-limit/overload/Bad Gateway/Gateway Timeout 后没有隔离或频繁先失败再恢复:先看 `codex-pool sentinel-report` 的 marker、动作、冻结 TTL 和下一次 probe,也看 `codex-pool validate --full` 的 recent gateway failover/forward failure 证据;同时对照当前 Sub2API 源码里 `/v1/responses` handler、`Forward``shouldFailoverOpenAIUpstreamResponse``handleOpenAIAccountUpstreamError` 的真实传播路径。不要手动禁用账号、删除账号、改 membership/priority/capacity/loadFactor 或从 YAML 移除问题账号来替代通用 failover 与哨兵隔离/恢复。
- `codex-pool sync --confirm``codex-pool validate` 超时:先区分 CLI 传输超时和 Sub2API 运行失败。受控 CLI 应返回远端作业进度和 stdout/stderr tail;如果只是低层 `trans` 60s 超时,不能据此判定 Sub2API failover 不工作。改用或修复 CLI 的远端 job/poll 路径后重跑,并以最终结构化结果作为证据。
- Codex 报 weekly-limit、`less than 10% of your weekly limit left``Run /status for a breakdown` 等账号状态/软配额提示并要求切号:不要把新关键词写成 Sub2API 内置临时不可调度策略来恢复可用性;由 marker-only 哨兵按非 marker 响应统一冻结,并用 `sentinel-report` / `sentinel-probe` 验证。
- 上游 400/503 响应体出现 `invalid_encrypted_content``bad_response_status_code``invalid_request_error` + 稳定 unsupported-model 文案、unsupported-model、`暂不支持` / `可用模型``model_not_found``No available channel for model ...` 或同类稳定模型路由 / Responses encrypted-content 兼容性失败:按通用 temp-unschedulable/failover 加哨兵 marker 证据处理,不用 account membership、priority、capacity、loadFactor、WebSocket mode、User-Agent 或 provider pinning 掩盖该错误族。
- 上游错误反复触发:`invalid_encrypted_content`、unsupported-model、`Recovered upstream error ...``Bad Gateway``Gateway Timeout`、Cloudflare `524`、Codex-facing `Upstream request failed``Unknown error``context deadline exceeded``context canceled``model_not_found``No available channel for model`、大上下文 `413``openai_error` 这类稳定包装文案,先确认 YAML temp-unschedulable 已同步、Sub2API 源码会把该错误族传播成 `UpstreamFailoverError`、运行日志出现 `openai.upstream_failover_switching`。若匹配规则后仍只看到 `openai.forward_failed`,根因是 Sub2API HTTP `/responses` 没把该错误传播成 `UpstreamFailoverError`,应修 Sub2API failover classifier/error propagation,不硬编码账号或给 `only` 特权。
- Codex auto compact 后丢上下文:先确认 YAML `localCodex` 是否声明启用 WSv2;若启用,再确认本机 `~/.codex/config.toml` 是否有 `supports_websockets = true``responses_websockets_v2 = true`,并看 `codex-pool validate` 的 WSv2 candidate 和 Sub2API 日志里的 `transport=responses_websockets_v2`。若 YAML 当前禁用 WSv2,则按 HTTP Responses 稳定性排查,不把旧 WS 口径当成验收要求。
- Codex smoke 有 reconnect/1013:这是上游并发/可用性问题,和 HTTP-only compact context-loss 分开处理;记录 session/log 证据并关联专项 issue,不要用运行时手补覆盖 YAML 容量。
## 禁止事项
- 不用原生 `kubectl apply/delete/patch` 作为正式操作入口。
- 不在 master server 部署或运行 Sub2API/PostgreSQL/Redis。
- 不新增 Ingress、NodePort、LoadBalancer、hostPort、hostNetwork 或宽 FRP 端口段。
- 不用 Sub2API 的 YAML 渲染结果整文件覆盖共享 PK01 Caddyfile;只能通过 managed block merge 更新 Sub2API 自己的块。
- 不给 Sub2API manifest 添加 CPU/memory limits,除非有新的 YAML 化明确决策。
- 不打印完整 API key、admin password 或 Secret 明文。
- 不把普通上游增删做成代码变更、CI/CD、feature flag 或兼容双路径。
- 不把手动禁用账号、删除账号、移除 YAML entry、降低 membership、临时改 priority/capacity/loadFactor、provider pinning 或给某个账号特权当作通用 failover / 哨兵隔离恢复问题的修复。
- 不魔改 Sub2API:Sub2API 本身不支持的能力就不做,不通过 UniDesk 脚本、k8s 原地热补、本地 fork、YAML 伪声明或隐藏 fallback 代替上游实现。
- 添加/删除上游、受保护账号代理、分组绑定:读 [references/full.md](references/full.md) 的账号管理段。
- 部署/状态/镜像升级/FRP 暴露:读部署、镜像、FRP 段。
- master Codex 消费端、`/v1/models`、Codex pool 验收:读 Codex Pool 和验收口径段
- 排障或禁止事项不确定时,读排障和禁止事项段
@@ -0,0 +1,287 @@
---
name: unidesk-sub2api
description: UniDesk Sub2API 平台运维技能。用户提到 Sub2API、sub2api、platform-infra sub2api、Codex pool、统一 API key、Sub2API FRP 暴露、Sub2API 管理 UI、配置 master ~/.codex 走 Sub2API、添加/删除 Codex 上游账号、校验 Sub2API /v1/models 时使用。
---
# UniDesk Sub2API
UniDesk 在 k3s `platform-infra` namespace 运维 Sub2API。D601 是当前默认 target,使用外置 PostgreSQL 和目标级 public exposureD518 可作为独立 external-active target 与 D601 同时对外服务;G14 由同一 YAML/CLI 控制为 standby predeploy,默认缩容且不运行 sentinel、FRP 或 HTTPS egress proxy。日常操作统一使用 UniDesk CLI,不直接写 Kubernetes 资源或手工调用 Sub2API 管理 API。
**固定入口**: `cd /root/unidesk && bun scripts/cli.ts platform-infra sub2api ...`
## 先看报表
查 Codex pool 哨兵状态、账号冻结/恢复、marker 命中、下一次 probe、最近 CronJob run、token/cost 账本时,优先使用这个低噪声报表入口,不要先翻 ConfigMap、CronJob 日志或 Sub2API UI
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report
```
需要机器处理或完整字段时再加 `--raw`;需要更多最近运行记录时加 `--events N`
追溯某个 Codex/Sub2API request id 的中断、上游账号、切号、临时不可调度、账号选择失败和同窗口账号池信号时,优先使用低噪声 trace 报表,不要先手写 `kubectl logs | grep`
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
```
默认输出类似 k8s/ps 的短表;机器处理用 `--raw` 读取 `.data.trace.*`;需要审计原始匹配日志时加 `--show-lines`;需要扩大搜索范围时使用 `--since 24h --tail 50000`。该命令只读:读取 Sub2API 日志、账号快照和 admin API 元数据,不改 `schedulable`、不清 runtime backoff、不中断请求。
## 先读边界
- 仓库长期开发边界见 `docs/reference/platform-infra.md`,本 skill 承担日常操作手册。
- 配置真相是 YAML`config/platform-infra/sub2api.yaml``config/platform-infra/sub2api-codex-pool.yaml`
- 业务策略和具体数值只以 YAML 为准。已有字段的数值调整只改 YAML 并跑 `plan` / `sync --confirm` / `validate`;不要自动补代码硬编码、schema 硬范围、合同测试、单元测试或长期参考文档。配置校验只校验格式、类型、必填和可渲染性,不判断数值策略是否“合理”。
- 本 skill 目录下若存在 `agents/*.yaml`,只作为 skill/agent 展示与调用元数据,不是 Sub2API 或 Codex pool 运行配置;不要在 skill 目录维护第二份账号、capacity、priority、endpoint 或 Secret 配置。
- Runtime target 由 `config/platform-infra/sub2api.yaml` 声明;默认 target 是 `D601:k3s``D518:k3s` 这类 external-active target 必须通过显式 `--target` 选择,`G14:k3s` 是 standby target。master server 只是控制端和消费者,不部署 Sub2API/PostgreSQL/Redis。
- Standby target 不部署本地 PostgreSQL,不运行 sentinel、FRP 管理入口或 HTTPS egress proxy;只能预部署 namespace、NetworkPolicy、Service,以及 replicas=0 的 Sub2API/Redis Deployment。Redis 激活后也只允许 ephemeral cache。External-active target 仍不部署本地 PostgreSQL,必须直连 YAML 声明的外置 DB,使用本地 ephemeral Redis,并且只有在 YAML 启用时才运行 frpc、egress proxy 和目标级 sentinel;多个 external-active target 可以并存,不得把一个 target 的 public exposure 当作另一个 target 的替代或回退。
- Secret、`~/.codex/config.toml*``~/.codex/auth.json*` 是运行时输入或本地状态,不提交。
- 默认 `~/.codex/config.toml``~/.codex/auth.json` 只作为统一 Sub2API consumer 使用;`config.toml` 必须指向 YAML-selected active target 的 consumer URL,当前由 `codex-pool configure-local --confirm` 写入 D601 target-level public URL`auth.json` 必须使用统一 pool API key。新增上游账号不得覆盖这两个默认文件,只能新增 `config.toml.<profile>` / `auth.json.<profile>` 并在 YAML 里声明。
- 输出只能包含 Secret 路径、key 名、presence、fingerprint 和 `valuesPrinted=false`;禁止打印完整 API key、admin password、JWT secret、TOTP key、base64 payload 或可复制的 preview。
## 部署与状态
```bash
bun scripts/cli.ts platform-infra sub2api plan
bun scripts/cli.ts platform-infra sub2api plan --target G14
bun scripts/cli.ts platform-infra sub2api apply --dry-run
bun scripts/cli.ts platform-infra sub2api apply --target G14 --dry-run
bun scripts/cli.ts platform-infra sub2api apply --confirm
bun scripts/cli.ts platform-infra sub2api apply --target G14 --confirm
bun scripts/cli.ts platform-infra sub2api status
bun scripts/cli.ts platform-infra sub2api status --target G14
bun scripts/cli.ts platform-infra sub2api validate
bun scripts/cli.ts platform-infra sub2api validate --target G14
```
- `plan` 读取 `config/platform-infra/sub2api.yaml`,渲染 `src/components/platform-infra/sub2api/sub2api.k8s.yaml`,检查 no Ingress/NodePort/LoadBalancer/hostPort/hostNetwork/resource limits,并要求 `NetworkPolicy/allow-all` 随 manifest 受控创建。
- `apply --confirm` 默认创建异步 job;按返回的 `job status` 命令轮询,再跑 `status``validate`
- `status --full|--raw` 只在需要展开远端 stdout/stderr 或原始 JSON 时使用。
- `validate` 是按需验收,不是连续可用性探针。对 standby target`validate --target <id>` 验证预部署形态,不要求外置 DB 当前可连接;对 external-active target,必须验证外置 DB、ephemeral Redis、Sub2API service、YAML egress proxy 和目标级 public exposure。
## D601 Egress Proxy
D601 的目标级 `egressProxy` 完全由 `config/platform-infra/sub2api.yaml` 控制。当前成熟形态是 master Docker `shadowsocks-rust` 作为加密出站源,D601 k3s 内 `sing-box` 暴露 HTTP/mixed ClusterIP proxy 给 Sub2API 和按 YAML 启用的 sentinel 使用。不要把 endpoint、端口、密码、健康探针或镜像 tag 写进 skill;只以 YAML 和 `config/platform-infra/sub2api-master-egress-proxy.compose.yaml` 为准。
master 侧 proxy 由 UniDesk checkout 内的 compose 文件管理:
```bash
docker compose -f config/platform-infra/sub2api-master-egress-proxy.compose.yaml up -d --force-recreate
bun scripts/cli.ts platform-infra sub2api apply --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api validate --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
```
proxy secret/config 文件只允许放在受控 Secret/state 路径,输出只能披露路径、presence、fingerprint 或摘要,不能打印密码、完整订阅或生成配置。若 D601 到上游的 TLS/SNI 路径被 reset,不要用临时 JS 或简陋 HTTP CONNECT proxy 作为最终方案;通过 YAML/compose 更换或修复成熟加密 proxy source,再跑上面的 apply/validate/sync/validate 闭环。
## 镜像升级
1. 修改 `config/platform-infra/sub2api.yaml``image.repository``image.tag``pullPolicy`
2. 执行 `sub2api plan`,确认策略检查通过。
3. 执行 `sub2api apply --confirm`,轮询 job 完成。
4. 执行 `sub2api status`,确认运行镜像等于 YAML 声明。
5. 执行 `sub2api validate``codex-pool validate` 做入口验收。
不要把镜像版本写进脚本常量、JSON 或 manifest 模板。
## Codex Pool
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool plan
bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --target D601 --account unidesk-codex-hy --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D601 --confirm
```
`config/platform-infra/sub2api-codex-pool.yaml` 控制:
- `pool.groupName`: Sub2API group 名称。
- `pool.apiKeySecretName` / `pool.apiKeySecretKey`: 统一消费 API key 的 k3s Secret 位置,默认 `platform-infra/sub2api-codex-pool-api-key.API_KEY`
- `pool.minOwnerBalanceUsd`: pool key owner 最低余额,sync/validate 会补齐。
- `pool.minOwnerConcurrency`: 可选统一消费 API key owner 最低并发;省略时 CLI 自动使用所有已解析账号 capacity 的总和,sync/validate 会补齐。显式 YAML 值只作为 override,仍必须不小于账号 capacity 总和;未显式写 `profiles.entries[].capacity` 的账号会使用 `pool.defaultAccountCapacity` 参与求和,不要用提高某个 provider capacity 来掩盖用户并发层 WS 1013。
- `pool.defaultTempUnschedulable`: Sub2API 内置请求路径临时不可调度开关和 YAML 规则列表。当前要求是按 YAML 开启通用规则;sync 把 `temp_unschedulable_enabled` / `temp_unschedulable_rules` 渲染到 managed accounts,让匹配的 400/5xx/超时/模型路由/加密内容错误短暂冷却当前账号并触发同组 failover。
- `pool.defaultTempUnschedulable` 与外部 `sentinel.*` 分开配置、互不驱动。内置规则负责 near-real-time request-path cooling/failover;哨兵负责 marker health、账号级隔离/恢复和 probe 退避。
- 外部 sentinel 的写入面只允许通过 Sub2API admin `schedulable` 接口冻结/恢复账号;不能写入、清理或间接清理 `temp_unschedulable_until` / `temp_unschedulable_reason`、rate-limit、overload、model-rate-limit 等 Sub2API 请求路径 runtime 状态,也不能调用 `recover-state` 作为恢复动作。看到 UI 里的“触发时间/解除时间/规则序号/匹配关键词”临时不可调度状态时,默认先归因到 Sub2API 内置 request-path temp-unschedulable,而不是 sentinel。
- YAML 只选择和配置 Codex 上游,不声明 `schedulable` 长期字段;`codex-pool sync --confirm` 不负责把既有账号恢复为 `schedulable=true`。既有账号的 `schedulable=false` 必须由哨兵先同步 Sub2API runtime 状态,再在 marker probe 命中后恢复。
- `profiles.entries`: 从 master `~/.codex/` 选择上游 profile 并映射到 Sub2API account。
- `profiles.entries[].capacity`: 可选 per-account concurrency override;不写则使用 `pool.defaultAccountCapacity`。具体数值只以 `config/platform-infra/sub2api-codex-pool.yaml` 为准,skill 和长期参考只描述规则,不重复写当前值。
- `profiles.entries[].loadFactor`: 可选 per-account Sub2API `load_factor` override;不写则使用 `pool.defaultAccountLoadFactor`。具体数值只以 `config/platform-infra/sub2api-codex-pool.yaml` 为准,修改后必须 `codex-pool sync --confirm``codex-pool validate`
- `profiles.entries[].trustUpstream`: 可选账号级哨兵信任标记;默认 `false`。可信账号使用 `sentinel.cadence.trustedSuccessMaxIntervalMinutes` 作为连续成功后的最大探测退避,不可信账号使用 `sentinel.cadence.untrustedSuccessMaxIntervalMinutes`。它只影响哨兵探测频率和状态可见性,不改变 Sub2API account priority/capacity/loadFactor。
- `pool.defaultSentinelProtect`: 账号级哨兵保护默认策略;是否启用、连续确认次数、初始延迟、最大延迟和退避倍率都只以 YAML 为准。marker probe 或 gateway failure 触发冻结前会先按该策略做连续 marker 确认,只有全部失败才进入冻结状态机。
- `profiles.entries[].sentinelProtect`: 可选账号级哨兵保护覆盖;只用于明确偏离 pool 默认策略。它只影响哨兵冻结判定和 `sentinel-report` 可见性,不改变 Sub2API account priority/capacity/loadFactor。
- 除非用户明确要求修改配置,不要仅凭推断改账号 membership、priority、capacity、loadFactor、WebSocket mode 或其他调度策略;先保留 YAML,完成 provenance/runtime evidence 溯源,并把结论写回相关 issue 或 runbook 后再提出变更。
- Sub2API 是 UniDesk 可读源码和可观测运行面的受控组件;排查 Sub2API 调度、failover、错误传播、临时不可调度或 account selection 时,默认先读当前 Sub2API 源码实现,再用真实 request id、Sub2API 日志和原入口流量验证。不要用 mock upstream、临时 probe account 或测试桩作为默认结论来源;这类探针最多是显式 debug 辅助,不能替代源码链路和真实运行证据。
- `profiles.entries[].tempUnschedulable`: 可选 per-account Sub2API 内置临时不可调度覆盖;只用于明确偏离 pool 默认规则,不用它给某个账号特殊优先级或临时绕过通用 failover。
- `profiles.entries[].openaiResponsesWebSocketsV2Mode`: 需要 Responses WebSocket v2 的上游才设置,值为 `off``ctx_pool``passthrough`
- `profiles.entries[].upstreamUserAgent`: 少数要求 Codex CLI User-Agent 的上游才设置,不能含换行。
- `manualAccounts.protected`: 已在 Sub2API 手动创建/维护、且必须排除在 UniDesk-managed Codex pool credentials 和 sentinel 控制之外的账号。默认不得改 credentials/status/schedulable/priority/capacity/loadFactor;只有显式声明 `proxyBinding` 时,`sync --confirm` 才允许把该账号的 `proxy_id` 对齐到 YAML 目标的 egress proxy;只有显式声明 `groupBinding.source: pool-group` 时,才允许把该账号加入统一消费 API key 使用的 pool group。
- `sentinel.monitor.enabled`: 账号级 marker 哨兵监控开关;开启后 `codex-pool sync --confirm` 会在 `platform-infra` 创建/更新 k8s CronJob、ConfigMap、Secret、ServiceAccount、Role 和 RoleBinding。CronJob 直打 YAML-managed 上游账号的 OpenAI Responses `gpt-5.5`,用确定 marker 作为唯一健康标准,并在独立 state ConfigMap 中记录 token/cost 账本。
- `sentinel.actions.enabled`: 账号级哨兵冻结/恢复动作开关;当前 marker-only guard 要求开启。动作关闭时只记录 `would-freeze`,不会调用 Sub2API admin API 改 `schedulable`。动作开启后,只要不满足 marker match,不论是 HTTP 200 私货、4xx/5xx、非 JSON、连接错误还是空输出,都进入同一个冻结/恢复状态机。
- `sentinel.sdk.openaiPythonVersion`: 哨兵容器使用的 OpenAI Python SDK 固定版本;模型请求必须通过标准 SDK `responses.create`,不要手工拼 `/v1/responses` 请求体或手写响应解析。后续升级 SDK 只改 YAML 并 `sync --confirm`
- `sentinel.probe.maxOutputTokens`: 哨兵本地流式 delta 收集上限,必须保持小值;它不作为上游 `max_output_tokens` 字段发送,以保持与 Sub2API WebUI 默认账号连接测试的 Responses SSE 请求形态一致。哨兵不限制并发和每轮账号数,所有到期账号会在同一轮并发探测。
- `sentinel.probe.userAgent`: 哨兵 direct upstream probe 的默认 User-Agent,通过 OpenAI SDK `extra_headers` 传递;默认贴近 Sub2API `net/http` 账号连接测试形态,个别账号仍可用 `profiles.entries[].upstreamUserAgent` 覆盖。
- `sentinel.cadence`: 成功信任指数退避配置。当前口径是从 1 分钟开始,连续成功后按账号 `trustUpstream` 选择可信/不可信最大退避;任意非 marker match 清零成功信任并进入冻结退避。可信/不可信最大退避数值只写 YAML。
- `sentinel.freeze`: 失败冻结 TTL 指数退避配置。当前口径是初始 1 分钟,失败后 `1m -> 2m -> 4m -> 8m -> 10m`,最大 10 分钟;失败 probe 基本不消耗有效输出 token,因此冻结窗口保持短周期。冻结到期后只做恢复 probe,通过才自动恢复,不能仅靠 TTL 到期解封。
- `sentinel.pricing`: 直打上游时哨兵自己的 token/cost 估算价格。因为 direct upstream probe 不经过 Sub2API 普通用量账本,哨兵必须自己记录全局与 per-account token/cost;这些账本只用于观察,不作为跳过探测的预算门禁。
`sync --confirm` 会登录 Sub2API admin、创建/更新 group、创建/更新 YAML 中的 `unidesk-codex-*` accounts、创建/复用统一 API key Secret,并部署/更新哨兵资源;它不把既有 managed account 直接恢复为 `schedulable=true`。恢复只由哨兵在读取 Sub2API runtime `schedulable=false` 后触发 recovery probe,并在 marker 命中时执行。`sync` 默认不删除 YAML 中缺席的 managed account。只有明确退役上游时才使用 `sync --confirm --prune-removed` 删除缺席且 `extra.unidesk_managed=true``unidesk-codex-*` account。对 `manualAccounts.protected``sync` 只执行 YAML 显式允许的窄同步;当前允许项是从目标 `egressProxy` 创建/更新 Sub2API internal proxy 记录并绑定 `proxy_id`,以及把受保护手动账号加入当前 `pool.groupName`。它仍不接管该账号凭据、status、schedulable、priority/capacity/loadFactor 或哨兵状态。
`sentinel-image status|build` 管理哨兵 Python 运行环境镜像。镜像由 YAML 的 `sentinel.image` 基础镜像和 `sentinel.sdk.openaiPythonVersion` 派生,发布到目标 runtime 的本地 registry`build --confirm` 会先检查 registry tag,存在则快速复用,不存在才在目标 host 构建并 push。CronJob 启动时只校验 SDK 版本,不在运行时 `pip install`。目标是否启用哨兵以 `config/platform-infra/sub2api.yaml``sentinel.enabledOnTargets` 为准;未启用的 target 在 `sync`/`validate` 中应显示 `skipped-target-disabled`,不得要求镜像构建、CronJob、Secret 或 state ConfigMap 存在。
`sync --confirm` 同时会按 YAML 渲染账号级哨兵资源,并在 monitor 开启时先确保可复用哨兵镜像存在。当前目标是 `sentinel.monitor.enabled=true` + `sentinel.actions.enabled=true` 的 marker-only 自动冻结/恢复;不要手工 patch CronJob、Secret 或 Sub2API account。若 YAML 新增账号或修改 profile/base URL/API key fingerprint/upstream User-Agent/Responses WebSocket modesync 会从变更前 runtime state 写入 pending probe 记录并立即安排 sentinel probe,但不会把既有账号直接恢复为可调度;只有 sentinel 读取到 Sub2API runtime `schedulable=false` 后执行 recovery probe,且 marker 命中,才恢复 `schedulable=true`。sentinel 冻结/恢复只改 `schedulable=false|true`,不得顺手调用 Sub2API `recover-state` 清除请求路径临时不可调度或其他 runtime backoff。无关账号的既有成功/失败退避不能被重置。若 YAML 下调失败冻结最大窗口,sync 会把仍 active 的旧冻结状态迁移到当前最大窗口内并立即安排 recovery probe,但不会直接解冻。若怀疑某个账号被误判,先用 `codex-pool sentinel-probe --account <accountName> --confirm` 立即触发该账号测量;该命令从现有 CronJob 模板派生一次性 Job,复用同一份 Secret、ConfigMap、OpenAI SDK probe、token/cost 账本和冻结/恢复状态机。
`trace --request-id <requestId>` 是只读 request 追溯报表,不触发 probe、不修改账号。默认输出请求开始/最终状态、failover、`account_select_failed`、窗口内 `account_temp_unschedulable`、admin schedulable 写入计数和当前账号快照;`reason=failover-attempted-no-candidate` 表示 Sub2API 已进入自动切号,但排除当前失败账号后没有可用候选。需要机器处理时使用 `--raw`,需要原始匹配行时加 `--show-lines`
`sentinel-report` 是只读低噪声报表,不触发 probe、不修改账号。默认输出类似 `ps` 的文本表,展示每个账号的探测次数、Sub2API runtime `schedulable`、最近 marker/HTTP/动作、冻结 TTL、成功退避、下一次 probe 和最近 run 事件;`SCH` 展示 Sub2API runtime schedulable`PROT` 展示账号级保护阈值,`P_FAIL` 展示最近一次保护确认中的失败次数/阈值;需要机器处理时使用 `sentinel-report --raw`
`sync --confirm``validate` 可能超过单次 SSH/runtime 短连接窗口。必须继续使用 `bun scripts/cli.ts platform-infra sub2api codex-pool ...`,由 CLI 在 G14 远端提交作业并短轮询状态;不要改用裸 `trans G14:k3s sh` 等一个长连接等待完整结果。若看到 `UNIDESK_SSH_RUNTIME_TIMEOUT`,先按 `docs/reference/platform-infra.md` 的规则处理为控制面可见性问题,修 CLI/job/poll 或重跑受控命令,不要手工 patch Sub2API credentials 或源码。
不要给 UniDesk-managed Codex accounts 开 Sub2API `pool_mode`。UniDesk 期望的 failover 是把失败账号临时标记为 unschedulable,让同组其他账号接手;`pool_mode` 会重试同一个 account path。
WebSocket v2 是账号能力集合,不是调度 pin。`openaiResponsesWebSocketsV2Mode` 只声明该账号可承担 Codex Responses WSv2 链路;只有 `localCodex.supportsWebSockets=true` / `localCodex.responsesWebSocketsV2=true` 时,`codex-pool validate` 才必须看到至少一个 `webSocketsV2.schedulableEnabled` 账号。真实可用性仍以 direct Codex WSv2 probe、Sub2API 日志和原入口 Codex smoke 为准。
Codex 启动时反复出现 WebSocket reconnect、HTTPS fallback、`websocket closed by server before response.completed`,或 Sub2API 日志出现 `openai.websocket_proxy_failed` / `openai.websocket_account_select_failed` / 上游 WS handshake 4xx/5xx 时,先按运行证据定位具体 account 和 transport。若账号的 WSv2 握手失败,优先只在 YAML 中把该账号的 `openaiResponsesWebSocketsV2Mode` 收敛为 `off`;若没有任何 direct Codex WSv2 probe 通过,则同时把 `localCodex.supportsWebSockets``localCodex.responsesWebSocketsV2` 收敛为 `false`,再 `codex-pool sync --confirm`。不要顺手改 membership、priority、capacity、Secret 或代码 fallback。
## 受保护手动账号代理与分组绑定
Sub2API 管理 UI 的账号连接测试使用账号级 `ProxyID` / proxy URL 配置上游 HTTP transport;账号没有绑定 proxy 时会直接出站,即使 Sub2API Pod 已经有 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量。看到 WebUI 账号测试连 `chatgpt.com` 超时、但 Pod 内显式走目标 proxy 可通时,先检查该账号是否属于 `manualAccounts.protected` 并声明了 `proxyBinding`。如果同一账号用 `gpt-5.2-pro` 返回 ChatGPT OAuth 不支持 Codex 的模型能力错误,但默认/受支持模型能完成 `hi``/v1/responses` smoke,这不是代理失败;按模型映射/账号能力另行处理。
WebUI 账号连接测试也不经过统一消费 API key 的 pool group 选择器;账号测试正常不代表 PC Codex 客户端能选中该账号。看到 WebUI 账号测试正常、但 `/responses``/v1/responses``account-select-failed` / `no available accounts` 返回 503 时,先检查该手动账号是否声明了 `groupBinding.source: pool-group`,并通过 `sync --confirm` 加入当前 `pool.groupName`
受保护手动账号仍由人工在 Sub2API UI 维护 credentials/status 等字段;UniDesk 只允许通过 YAML 做代理和分组窄绑定:
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
```
`sync` 输出应显示 `manualAccounts.ok=true``proxySync.ok=true``groupSync.ok=true`,且该账号的 proxy/group `bindingAligned=true``sentinel-probe --account <manual-account> --confirm` 对受保护手动账号必须继续拒绝,通常返回 `account-protected-manual`;不要为了测试而把该账号移入 `profiles.entries` 或取消保护。需要证明 WebUI 同款账号测试恢复时,用 Sub2API admin account test 原入口测最小 `hi` 和默认/受支持模型,并只记录 account id、proxy id、event types、HTTP status 和短 output preview,不记录 OAuth token 或 Secret 明文。若指定模型返回 “model is not supported when using Codex with a ChatGPT account” 一类能力错误,先归因到模型能力/映射,而不是 proxy。
## 添加上游
1. 在 master `~/.codex/` 准备带后缀的上游 profile 文件,例如 `config.toml.<profile>``auth.json.<profile>`;禁止覆盖默认 `config.toml` / `auth.json`
2.`config/platform-infra/sub2api-codex-pool.yaml` 添加 `profiles.entries` 项,指定 `profile``accountName``configFile``authFile`
3. 如需要,给该项加 `priority``capacity``loadFactor``trustUpstream``sentinelProtect``openaiResponsesWebSocketsV2Mode``upstreamUserAgent`capacity/loadFactor/信任退避/保护阈值的具体数值只写在 YAML。只有显式恢复 Sub2API 内置临时不可调度时才添加 per-account `tempUnschedulable`
4. 如果新增账号会提高声明 capacity 总和,默认让省略的 `pool.minOwnerConcurrency` 继续按 capacity 总和自动解析;只有 YAML 已经显式写了该 override 时,才同步提高到不低于总 capacity,或删除 override 回到自动解析。
5.`codex-pool plan`,确认 profile 可读、`base_url` 和 API key 来源有效,且 stdout 未泄露完整 key。
6.`codex-pool sync --confirm`
7.`codex-pool validate`
普通新增上游是 YAML 操作,不走 CI/CD,不改代码。只有需要渲染或校验上游 Sub2API 已经存在的可复用能力时才修改 `scripts/src/platform-infra-sub2api-codex.ts`;Sub2API 本身不支持的能力不在 UniDesk 侧魔改实现。
## 删除上游
删除上游只用于明确退役、凭据所有权变更或用户明确要求移除 provider;不能作为上游 5xx、compact 失败、限流、模型路由失败或哨兵隔离/恢复问题的处理手段。
1.`config/platform-infra/sub2api-codex-pool.yaml` 删除对应 `profiles.entries` 项。
2.`codex-pool plan` 检查 desired 列表。
3.`codex-pool sync --confirm --prune-removed`
4. 确认输出 `accounts.pruned` 只包含期望删除项。
5.`codex-pool validate`
CLI 默认保留缺席账号,避免把可用性问题误处理成删除;只有显式 `--prune-removed` 才会 prune `name``unidesk-codex-` 开头且 `extra.unidesk_managed=true` 的缺席账号。
## FRP 暴露
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool expose
bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm
```
- 由 YAML `publicExposure` 控制。Codex pool 默认公共端是 `publicBaseUrl`master 本地消费端是 `masterBaseUrl`external-active target 的目标级 public exposure 在 `config/platform-infra/sub2api.yaml` 中声明。
- `expose --confirm` 只为 YAML 指定的 `remotePort` 补 master `frps` allow port,并在 G14 创建/更新 `sub2api-frpc`
- master Caddy site 也由 `publicExposure.masterCaddy` 渲染;`responseHeaderTimeoutSeconds` 必须足够覆盖 Codex `/responses/compact` 长请求,避免 Caddy 先返回 504 而 Sub2API 后台实际稍后成功。具体数值只改 `config/platform-infra/sub2api-codex-pool.yaml`,修改后跑 `codex-pool expose --confirm`,再核对 Caddyfile 中渲染出的 `response_header_timeout`
- master Caddy 的短窗口边缘重试由 `publicExposure.masterCaddy.edgeRetry` 渲染;用于吸收 FRP remotePort 短暂关闭、`connect: connection refused`、EOF 或 connection reset 这类请求尚未稳定到达 Sub2API 的 502。具体 retry 时长、间隔和 `retryMatch` 范围只写 YAML,修改后跑 `codex-pool expose --confirm`,再核对 Caddyfile 中渲染出的 `lb_try_duration``lb_try_interval``lb_retry_match`。不要手工 patch `/etc/caddy/Caddyfile`
- PK01 `/etc/caddy/Caddyfile` 是 Sub2API、LangBot、n8n、HWLAB 等多 YAML 来源共享的 edge artifact。Sub2API apply/expose 只能更新自己的 managed block 并保留其他 blocks;同一 Sub2API 服务暴露多个 target 时,D601 保留既有 `# BEGIN unidesk managed sub2api`,非默认 target 必须使用 target-scoped owner(例如 `sub2api-d518`),避免 `api.pikapython.com``api2.pikapython.com` 互相覆盖。若 apply 输出显示 managed block 数异常,先停止 closeout,检查 PK01 Caddy 合并与 validation 结果,不要手工整文件覆盖。
- 非幂等 POST 的 round-trip retry 必须收窄到 YAML `retryMatch` 声明的安全路径;普通 `/responses` 上游账号错误仍归 Sub2API failover / temp-unschedulable / sentinel 处理,不用 Caddy 重放整段推理请求来掩盖账号池问题。
- 同一个 FRP TCP 入口同时暴露 OpenAI-compatible API 和 Sub2API 管理 UI `/login`。不要另开第二个管理端口,除非 YAML 明确声明新的暴露决策。
- Sub2API Kubernetes Service 继续保持 ClusterIP。
- External-active target 的公开路径是 `client -> PK01 Caddy -> PK01 frps remotePort -> target frpc -> Sub2API`,不经过 pikanode,也不经过 master server 反代。PK01 Caddy 下载必须使用 YAML `publicExposure.pk01.caddyDownloadProxyUrl` 指定的 proxy;如果 Caddy 下载慢,先确认 apply 输出里是 `downloadProxy.mode=curl-proxy`。目标域名必须先解析到 YAML 声明的 PK01 公网地址,HTTPS 才能作为最终验证;D601 `api.pikapython.com` 与 D518 `api2.pikapython.com` 应分别验收。
## 配置 master Codex 消费端
```bash
bun scripts/cli.ts platform-infra sub2api codex-pool configure-local
bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm
```
`configure-local --confirm` 会:
-`platform-infra/<apiKeySecretName>.<apiKeySecretKey>` 读取统一 API key。
- 把当前 `~/.codex/config.toml``~/.codex/auth.json` 备份为 `.<backupSuffix>`,默认 `.pre-sub2api`
- 重写默认 `~/.codex` 消费端,固定指向 `https://sub2api.74-48-78-17.nip.io/`provider 名称和 wire API 来自 `localCodex`
-`localCodex.modelContextWindow` / `localCodex.modelAutoCompactTokenLimit` 写入 `model_context_window` / `model_auto_compact_token_limit`,用于统一控制 Codex auto compact 触发窗口,避免 GPT-5.5 消费端生成过大的 `/responses/compact` 长请求。
-`localCodex` 写入 Codex transport 标记:`supports_websockets``[features] responses_websockets_v2` 必须同开同关。只有至少一个上游通过 direct Codex WSv2 probe 时才启用;否则保持 HTTP Responses,避免每次原入口先经历无效 WS reconnect。
- 用统一 key 做一次 gateway 验证。
防递归规则:默认 `config.toml` / `auth.json` 是 Sub2API consumer,不得作为上游账号导回 pool;上游账号必须使用带后缀 profile 文件,并通过 `config/platform-infra/sub2api-codex-pool.yaml``profiles.entries` 增删。
## 验收口径
部署 closeout 至少包含:
- `sub2api status`Deployment/StatefulSet/Service/Secret/NetworkPolicy 可见,运行镜像与 YAML 一致,`NetworkPolicy/allow-all` 符合 `podSelector: {}`、Ingress/Egress 全放行。
- `sub2api validate`app、PostgreSQL、Redis、service proxy、`NetworkPolicy/allow-all` 和临时跨 Pod PostgreSQL/Redis 连通性检查通过。
- `codex-pool validate`:统一 key 的 `GET /v1/models` 成功,并用 `localCodex.responsesSmokeModel` 跑一次小的 `POST /v1/responses` smokeowner balance / owner concurrency 已满足 YAML 最小值,capacity、WebSocket v2、Sub2API 内置 temporary-unschedulable 开关/规则和 sentinel runtime 状态与 YAML 对齐;`validation.gatewayResponsesRecent` 汇总最近 6 小时普通 `/responses``/v1/responses` 的 failover、forward failure、最终 4xx/5xx、慢 final error 与 `context canceled` 证据,`validation.gatewayCompactRecent` 单独汇总 `/responses/compact` 证据。若当前 Responses smoke `ok=true` 但 recent 字段 `degraded=true`,先区分是历史窗口残留还是新的 request id 正在失败;长期判定见 `docs/reference/platform-infra.md`
-`publicExposure.enabled=true`,确认 FRP path 可用;`expose --confirm` 会用未带 key 的 public `/v1/models` 401 作为网关可达性探针。
- 多 target 同时启用 public exposure 时,必须分别验证每个 target 的 root、`/health`、未带 key `/v1/models` 401,以及各自 `codex-pool validate --target <id>`;一个域名可用不能替代另一个域名的验收。
- 若目标声明了 `egressProxy.enabled=true`,确认 proxy Deployment/Service readySub2API 和 sentinel env 与 YAML 对齐,并通过 YAML 声明的 health URL 完成代理出站探针。
如果要证明真实模型请求可用,使用最小 `/v1/responses` 或等价 Codex smoke。不要把 group-level `/v1/models` 成功解释成每个上游 account 都健康。
## 排障
- Codex pool 哨兵、账号冻结/恢复、marker-only 判断或 probe 周期看不清:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report`。这个报表是主观察面;只有报表缺字段或需要底层证据时,才继续看 `--raw`、CronJob log、state ConfigMap 或 Sub2API 管理 UI。若看到“临时不可调度状态”且包含规则序号/匹配关键词,检查 Sub2API `account_temp_unschedulable` 日志和账号 `temp_unschedulable_*` 字段;sentinel 只解释 `schedulable=false` 的 active quarantine,不解释这类内置临时冷却。
- 只加强监控、不让哨兵自动冻结账号时,把 YAML `sentinel.actions.enabled=false``codex-pool sync --confirm`。此时 marker probe 和 gateway failure monitor 仍记录 `would-freeze` / observe-only 证据,但不会通过 Sub2API admin 写 `schedulable=false``/responses/compact``codex.remote_compact.failed` 和 compact 上游 5xx failover 只作为 `gateway-compact-*` 观察事件记录,不作为哨兵自动切换触发器。
- 单个 request id 报 502/503/中断/没有自动切号:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>`。先看 `outcome``reason``FAILOVER``SELECT-FAILED``ACCOUNT SIGNALS``WINDOW STATS`;只有 trace 报表缺字段或需要审计原始日志时,才加 `--show-lines``--raw`。若 `reason=failover-attempted-no-candidate`,说明切号动作已发生,但 scheduler 在排除失败账号后没有可用候选;继续用 `sentinel-report``validate --full` 区分 sentinel quarantine、request-path temp-unschedulable、账号 status 或容量耗尽。
- profile invalid:先修 `~/.codex/config.toml.<profile>``base_url``wire_api``model``auth.json.<profile>` 的 API key;不要在 YAML 中写密钥。
- 手动 OAuth/API-key 账号的 WebUI account test 连 `chatgpt.com` 超时,但同一 Pod 显式 HTTP proxy 探针可通:不要只看 Pod `HTTP_PROXY` env,按“受保护手动账号代理与分组绑定”小节确认 `manualAccounts.protected[].proxyBinding`,跑 `codex-pool sync --target D601 --confirm` 后再用原账号测试复测。若复测不再 reset/timeout,而是 `gpt-5.2-pro` 这类指定模型返回 ChatGPT OAuth Codex 不支持的能力错误,用默认/受支持模型或统一 key smoke 验证代理,不要把模型错误当作代理仍坏。
- 手动 OAuth/API-key 账号 WebUI account test 正常,但 PC Codex 客户端通过统一 key 访问 `/responses` 返回 503 且 trace 是 `account-select-failed` / `no available accounts`:按“受保护手动账号代理与分组绑定”小节确认 `manualAccounts.protected[].groupBinding.source: pool-group`,跑 `codex-pool sync --target D601 --confirm` 后用 `codex-pool validate --target D601 --full` 复测统一 key。
- Sub2API 卡在 `wait-postgres` / `wait-redis` 或服务内大量 `context deadline exceeded`:先跑 `sub2api status``networkPolicy.ok`,再跑 `sub2api validate``postgresCrossPodPgIsReady` / `redisCrossPodPing`;缺失或异常时用 `sub2api apply --confirm` 恢复受控 `NetworkPolicy/allow-all`,不要保留手工 iptables bypass 作为长期修复。
- pool key 401:跑 `codex-pool sync --confirm` 重建 Sub2API key 与 k3s Secret 绑定,再跑 `codex-pool validate`
- pool key、admin password 或 k8s Secret `.data` 被 stdout、日志、issue 或本地 transcript 打印时,按泄露处理:撤销对应 Sub2API key 或 token,删除/重建受影响的 target Secret,通过 `codex-pool sync --target <id> --confirm` 或相应 YAML sourceRef 重新下发,再用 fingerprint、presence 和 `valuesPrinted=false` 作为 closeout 证据;不要复述旧值或新值。
- 运行中过去的验证探针残留:只用 `codex-pool cleanup-probes --confirm` 清理 `unidesk-probe-*` 临时资源;不要把真实 managed account 删除当作探针清理或可用性恢复。
- FRP 不通:先看 `codex-pool expose --confirm` 输出的 `masterFrps``masterCaddy``sub2api-frpc` 和 public 401 probe;需要低层证据时只用 `trans G14:k3s` 做 bounded 查询。
- D601 external-active 的 `api.pikapython.com` 不通:先区分 DNS/TLS/Caddy/FRP/Sub2API。DNS 未解析到 YAML 声明的 PK01 地址时,Caddy ACME 会失败,`https://api.pikapython.com` 不能算完成;可用 PK01 loopback FRP 端口和 PK01 公网 remotePort 证明 D601 FRP 数据路径,但最终仍要等 DNS 生效后重跑 HTTPS health、`/v1/models``/v1/responses`
- D601 external-active apply 后其他 PK01 HTTPS 服务消失:优先怀疑共享 Caddy managed block 合并失败或旧整文件写入路径复现。用受控 Sub2API apply 输出和 PK01 Caddy managed block markers 取证,再通过各服务自己的 YAML apply/public-exposure 入口恢复;不要手工复制某一份 Caddyfile 作为长期修复。
- Caddy 下载慢或失败:先确认 `config/platform-infra/sub2api.yaml` 已设置 `publicExposure.pk01.caddyDownloadProxyUrl`,并重跑 `sub2api apply --target D601 --confirm` 看 PK01 apply summary 中的 `downloadProxy.mode=curl-proxy`。不要反复裸连 GitHub release。
- `/responses/compact` 在接近 master Caddy `response_header_timeout` 的固定时长后返回 504,或 Sub2API 日志稍后记录 `codex.remote_compact.succeeded` 时,优先检查 master Caddy `response_header_timeout` 是否由 YAML `publicExposure.masterCaddy.responseHeaderTimeoutSeconds` 渲染,修正后跑 `codex-pool expose --confirm`;这类边缘代理超时不会触发 Sub2API 账号级临时下线。reload 前已经在途的 compact 请求仍可能按旧 timeout 结束,判断修复是否生效时只看 reload 之后新发起的请求。
- `/responses/compact` 或普通 public URL 在几秒窗口内出现 502Caddy 日志显示 `dial tcp 127.0.0.1:<remotePort>: connect: connection refused``EOF``connection reset by peer`,同时 frps 日志出现 `platform-infra-sub2api proxy closing` / `listener is closed` / `new proxy ... success`,说明失败在 master Caddy 与 FRP remotePort 边缘层,Sub2API 和 sentinel 可能完全看不到。先确认 `publicExposure.masterCaddy.edgeRetry` 已按 YAML 渲染并 `codex-pool expose --confirm` 生效;若仍频繁发生,再继续查 G14 `sub2api-frpc` 到 master `frps` 的控制连接稳定性。不要把这类边缘 502 误判成账号池上游错误,也不要通过禁用账号恢复。
- default profile 递归:检查 YAML default entry 是否使用 `*.pre-sub2api` 备份文件;必要时恢复备份后重新 `configure-local --confirm`
- 上游需要 WebSocket v2:先做 direct Codex WSv2 probe;通过后才给该 profile 配 `openaiResponsesWebSocketsV2Mode: ctx_pool|passthrough` 并跑 `sync --confirm`;把它当 capability candidate,容量仍以 YAML 中的 `capacity` 或默认值为准。
- Codex 启动 WebSocket 回退:用原入口 Codex smoke 复现,再用 bounded Sub2API 日志确认 account;对 WS handshake 4xx/5xx、`openai.websocket_account_select_failed` 或 close-before-`response.completed` 的账号关闭 YAML WSv2 能力后同步。若没有剩余 WSv2-capable account,把 `localCodex.supportsWebSockets``localCodex.responsesWebSocketsV2` 一起关掉,不把临时可用性推断写成调度配置。
- 上游要求 Codex User-Agent:只给该 profile 配 `upstreamUserAgent`,跑 `sync --confirm`
- 上游报 capacity/rate-limit/overload/Bad Gateway/Gateway Timeout 后没有隔离或频繁先失败再恢复:先看 `codex-pool sentinel-report` 的 marker、动作、冻结 TTL 和下一次 probe,也看 `codex-pool validate --full` 的 recent gateway failover/forward failure 证据;同时对照当前 Sub2API 源码里 `/v1/responses` handler、`Forward``shouldFailoverOpenAIUpstreamResponse``handleOpenAIAccountUpstreamError` 的真实传播路径。不要手动禁用账号、删除账号、改 membership/priority/capacity/loadFactor 或从 YAML 移除问题账号来替代通用 failover 与哨兵隔离/恢复。
- `codex-pool sync --confirm``codex-pool validate` 超时:先区分 CLI 传输超时和 Sub2API 运行失败。受控 CLI 应返回远端作业进度和 stdout/stderr tail;如果只是低层 `trans` 60s 超时,不能据此判定 Sub2API failover 不工作。改用或修复 CLI 的远端 job/poll 路径后重跑,并以最终结构化结果作为证据。
- Codex 报 weekly-limit、`less than 10% of your weekly limit left``Run /status for a breakdown` 等账号状态/软配额提示并要求切号:不要把新关键词写成 Sub2API 内置临时不可调度策略来恢复可用性;由 marker-only 哨兵按非 marker 响应统一冻结,并用 `sentinel-report` / `sentinel-probe` 验证。
- 上游 400/503 响应体出现 `invalid_encrypted_content``bad_response_status_code``invalid_request_error` + 稳定 unsupported-model 文案、unsupported-model、`暂不支持` / `可用模型``model_not_found``No available channel for model ...` 或同类稳定模型路由 / Responses encrypted-content 兼容性失败:按通用 temp-unschedulable/failover 加哨兵 marker 证据处理,不用 account membership、priority、capacity、loadFactor、WebSocket mode、User-Agent 或 provider pinning 掩盖该错误族。
- 上游错误反复触发:`invalid_encrypted_content`、unsupported-model、`Recovered upstream error ...``Bad Gateway``Gateway Timeout`、Cloudflare `524`、Codex-facing `Upstream request failed``Unknown error``context deadline exceeded``context canceled``model_not_found``No available channel for model`、大上下文 `413``openai_error` 这类稳定包装文案,先确认 YAML temp-unschedulable 已同步、Sub2API 源码会把该错误族传播成 `UpstreamFailoverError`、运行日志出现 `openai.upstream_failover_switching`。若匹配规则后仍只看到 `openai.forward_failed`,根因是 Sub2API HTTP `/responses` 没把该错误传播成 `UpstreamFailoverError`,应修 Sub2API failover classifier/error propagation,不硬编码账号或给 `only` 特权。
- Codex auto compact 后丢上下文:先确认 YAML `localCodex` 是否声明启用 WSv2;若启用,再确认本机 `~/.codex/config.toml` 是否有 `supports_websockets = true``responses_websockets_v2 = true`,并看 `codex-pool validate` 的 WSv2 candidate 和 Sub2API 日志里的 `transport=responses_websockets_v2`。若 YAML 当前禁用 WSv2,则按 HTTP Responses 稳定性排查,不把旧 WS 口径当成验收要求。
- Codex smoke 有 reconnect/1013:这是上游并发/可用性问题,和 HTTP-only compact context-loss 分开处理;记录 session/log 证据并关联专项 issue,不要用运行时手补覆盖 YAML 容量。
## 禁止事项
- 不用原生 `kubectl apply/delete/patch` 作为正式操作入口。
- 不在 master server 部署或运行 Sub2API/PostgreSQL/Redis。
- 不新增 Ingress、NodePort、LoadBalancer、hostPort、hostNetwork 或宽 FRP 端口段。
- 不用 Sub2API 的 YAML 渲染结果整文件覆盖共享 PK01 Caddyfile;只能通过 managed block merge 更新 Sub2API 自己的块。
- 不给 Sub2API manifest 添加 CPU/memory limits,除非有新的 YAML 化明确决策。
- 不打印完整 API key、admin password 或 Secret 明文。
- 不把普通上游增删做成代码变更、CI/CD、feature flag 或兼容双路径。
- 不把手动禁用账号、删除账号、移除 YAML entry、降低 membership、临时改 priority/capacity/loadFactor、provider pinning 或给某个账号特权当作通用 failover / 哨兵隔离恢复问题的修复。
- 不魔改 Sub2API:Sub2API 本身不支持的能力就不做,不通过 UniDesk 脚本、k8s 原地热补、本地 fork、YAML 伪声明或隐藏 fallback 代替上游实现。
+16 -336
View File
@@ -3,351 +3,31 @@ name: unidesk-trans
description: UniDesk SSH 透传与 apply-patch 语法 — `trans <route> <operation>` 分布式执行入口,包含 route 语法、workspace/k3s/Windows 路由、apply-patch envelope 格式、sh/bash/py/upload/download operation 和 60s 短连接约束。用户提到 trans、tran、ssh 透传、远端执行、apply-patch、apply_patch、远端 patch、k3s route、workspace route 时使用。
---
# UniDesk TransSSH 透传 + apply-patch
# UniDesk Trans
UniDesk 分布式 SSH 透传入口,统一通过 `trans <route> <operation>` 在远端 host/k3s/Windows 上执行命令或文本 patch
分布式 SSH 透传入口`trans <route> <operation>` 中 route 只定位目标,后续 token 都属于 operation parser
**固定入口**: `trans <route> ...`wrapper 位于 `/root/.local/bin/trans`,委托 repo 内 ssh-only 启动入口 `bun scripts/ssh-cli.ts ssh "$@"`,避免被无关 CLI 子命令模块解析失败拖垮)
---
## Route 语法
```
trans <providerId>[/absolute/workspace] <operation> [args...]
trans <providerId>:<plane>[:<namespace>:<resource>[:<container>]] <operation> [args...]
```
### Host workspace route
## 高频 route
```bash
trans G14:/root/hwlab git status --short --branch
trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'
trans D601:/home/ubuntu/workspace/unidesk-dev sh <<'SH'
...
SH
```
**规则**: workspace 路径写在 route 第一个 token,不写进 `cd` 串。反面示例:~~`trans G14 sh -- 'cd /root/hwlab && git status'`~~
### k3s route
```bash
# 控制面
trans G14:k3s kubectl get pods -n hwlab-dev
trans D601:/home/ubuntu/workspace/unidesk-dev git status --short --branch
trans D601:k3s kubectl get pods -A
# 指定 namespace + workload
trans G14:k3s:hwlab-dev:hwlab-cloud-web:app exec --cwd /app -- cat /app/version.txt
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /workspace <<'PATCH'
...
PATCH
```
CLI 自动注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。k3s route 中 `:` 是分布式路由分隔符,`/` 只表示容器内文件系统 cwd;容器选择必须写 `:<container>` 或 operation 参数 `--container <container>`,不要写成 `pod/<pod>/<container>`
### Windows route
```bash
trans D601:k3s:namespace:workload[:container] logs --tail 120
trans D601:win ps <<'PS'
Get-ChildItem C:\test
PS
trans D601:win/c/test cmd cd
trans gh:/owner/repo/issue/<number> cat
```
Windows operation 必须显式区分:`ps` 走 PowerShell`cmd` 走 cmd.exe
Host workspace、k3s、Windows、GitHub issue/PR routesh/bash/argv/apply-patch/py/upload/download/playwright/kubectl/logs/skills/tcp-pool 操作,以及 apply-patch envelope 语法和 quoting 陷阱见 [references/full.md](references/full.md)
### GitHub issue/PR route
## P0 边界
```bash
trans gh:/pikasTech/HWLAB/issue ls --state all --limit 20 --search 'YAML-first 测试账号 API_KEY 准备 PJ2026-0105' --full
trans gh:/pikasTech/HWLAB/issue/1236 cat
trans gh:/pikasTech/HWLAB/issue/1236 rg 'API_KEY|YAML-first'
trans gh:/pikasTech/HWLAB/issue/1236 apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
```
- 远端文本修改优先 `trans <route> apply-patch`;不要 download/upload/sed 拼临时 diff 代替 patch。
- `sh`/`bash` 必须显式声明 shell;单进程命令优先 direct argv 或已知 operation。
- 普通 trans/ssh 短连接硬预算 60s;长 CI/CD、trace、logs、build、硬件流程必须 submit-and-poll。
- Windows route 使用 `win ps``win cmd`;不要把 POSIX shell 当 Windows shell。
GitHub issue/PR 正文读写也走 `trans gh:/owner/repo/...`。常见读取优先用 `issue ls --search/--state``cat``rg`,不要为了看正文先回到 `bun scripts/cli.ts gh issue view --json body --full`;普通小补丁可直接 `apply-patch``--dry-run` 只作为高风险正文预览。
## 何时读取 reference
---
## Operation
### sh / bash(显式 shell heredoc
```bash
# POSIX /bin/sh heredoc
trans G14:/root/hwlab sh <<'SH'
echo "step 1"
git status --short --branch
echo "step 2"
SH
# Bash heredoc:只有用到 pipefail、数组、[[ ... ]]、${var:0:8} 等 Bash 语法时使用
trans G14:/root/hwlab bash <<'BASH'
set -euo pipefail
short="${GITHUB_SHA:0:8}"
printf '%s\n' "$short"
BASH
# one-liner
trans G14:/root/hwlab sh -- 'git fetch origin G14 && git pull --ff-only origin G14'
# 单进程 direct argv 不放在 sh/bash 下,直接用已知子命令或 argv
trans D601:/path sed -n '1,20p' AGENTS.md
trans D601:/path argv sed -n '1,20p' AGENTS.md
```
`script``shell` operation 已移除并会失败;必须在 operation 位置明确写 `sh``bash`,让调用者和 Agent 都知道脚本语法边界。Windows PowerShell 必须用 `ps`
### argv(单命令)
```bash
trans G14:/root/hwlab git status --short --branch
trans D601 hostname
```
### apply-patch(远端文本 patch
- **P0: 所有远端文本/源码修改必须优先使用 `apply-patch`,禁止用 `py`python3 写文件)或 `sed` heredoc 拼接大段文本替代。** 只有当 apply-patch 本身不可用或需处理非文本/批量机械生成文件时,才使用其他受控方式;使用前必须在注释中说明原因,修改后立即用 `git diff` 或文件尾部检查确认没有截断或污染。
```bash
trans G14:/root/hwlab apply-patch <<'PATCH'
*** Begin Patch
*** Update File: AGENTS.md
@@
## Heading
context line
-old line
+new line
more context
*** End Patch
PATCH
# 从文件读
trans G14:/root/hwlab apply-patch < patch.diff
```
v2 引擎(默认):本地 TypeScript 解析 hunk,远端只读写文件。v1 legacy 入口:`apply-patch-v1`
### py(远端 Python 脚本)
```bash
trans D601 py -- arg1 arg2 < script.py
```
自动写到远端临时 `.py` 文件,`python3 -u` 执行后清理。
### upload / download(整文件传输)
```bash
trans G14:/root/hwlab upload ./local-file.txt /root/remote-file.txt
trans G14:/root/hwlab download /root/remote-file.txt ./local-file.txt
```
自动校验 SHA-256,结果中 `verified=true` 即完整性证明。
### playwright(远端浏览器验收)
`trans <route> playwright` 只保留为跨 host 短生命周期浏览器执行和截图/PDF 回传底座。UniDesk/HWLAB Web 开发、HWLAB `web-probe run|script`、fake-server Playwright、fixture 脱敏、截图 artifact 与 Workbench/Performance 判定口径统一见 `$unidesk-webdev`;本 SSH 透传 skill 不维护第二套 Web 测试流程。
### kubectl / logsk3s 诊断)
```bash
trans G14:k3s kubectl get pods -n hwlab-dev
trans G14:k3s logs deploy/hwlab-cloud-api -n hwlab-dev --tail 50
trans G14:k3s:hwlab-dev:hwlab-cloud-web-abc logs --tail 100
```
### skills(远端 skill 发现)
```bash
trans G14 skills [--scope all|wsl|windows] [--limit N]
```
### tcp-pool 状态与并发 smoke
查看 Provider 是否使用 SSH TCP data pool
```bash
bun scripts/cli.ts debug health
```
在输出或 frontend 原始 JSON 里看这些 labels`providerGatewaySshDataTransport=tcp-pool``providerGatewaySshDataPoolReady``providerGatewaySshDataPoolClaimed``providerGatewaySshDataPoolDesired``providerGatewaySshDataPoolLastError`
单个 provider 的低噪声池状态:
```bash
bun scripts/cli.ts debug ssh-pool D601
```
如果 `trans`/`tran` stderr 出现 `UNIDESK_SSH_TCP_POOL_HINT`,优先按其中的 `failureKind` 处理:`provider-data-channel-closed` 表示会话中途 data channel 断开,`provider-data-channel-missing` 表示 core/provider 对 channel 状态不一致,`provider-data-pool-exhausted` 表示没有空闲 channel。对幂等受控操作先查 `debug ssh-pool <provider>`,再重试原受控 CLI;不要把这类 hint 单独定性为远端 runtime 配置失败。
快速验证 D601 维护桥:
```bash
trans D601 argv true
trans D601 argv hostname
```
并发 smoke 用短命令并给每路加本地 timeout:
```bash
tmp=$(mktemp -d /tmp/trans-d601-pool-10.XXXXXX)
for i in $(seq 1 10); do
timeout 30s trans D601 sh -- 'printf start:'"$i"':%s\\n "$(date +%s%3N)"; sleep 2; printf end:'"$i"':%s\\n "$(date +%s%3N)"' >"$tmp/$i.out" 2>"$tmp/$i.err" &
done
wait
for i in $(seq 1 10); do echo "[$i]"; cat "$tmp/$i.out"; cat "$tmp/$i.err" >&2; done
```
期望是每一路 rc=0、stderr 为空、stdout 同时包含 start/end;结束后 pool labels 回到 `claimed=0`
---
## apply-patch 语法(Envelope 格式)
**不是 unified diff**,是 Codex 专用 envelope。
```
*** Begin Patch
*** Update File: <relative-path>
@@
context line
-old line
+new line
more context
*** End Patch
```
### 关键规则
| 规则 | 正确 | 错误 |
|------|------|------|
| 路径必须相对 | `AGENTS.md` | `/root/unidesk/AGENTS.md` |
| hunk 分隔符 | `@@` | `@@ -N,M +K,L @@` |
| 空行 | ` \n`(空格+换行) | 裸 `\n` |
| 首行/尾行 | `*** Begin Patch` / `*** End Patch` | 外层不能有空行 |
### 操作类型
```bash
*** Update File: path/to/file.ext # 修改现有文件
*** Add File: path/to/new.ext # 新增文件(可覆盖已有)
*** Delete File: path/to/old.ext # 删除文件
*** Move File: old/path -> new/path # 移动/重命名
```
### 上下文定位
默认 3 行上文 + 3 行下文。同一 hunk 在文件中重复时用 `@@` 跳到 class/function
```
*** Update File: src/app.ts
@@ class MyComponent
old context
-old code
+new code
more context
```
### 常见失败
| 症状 | 原因 | 处理 |
|------|------|------|
| `failed to find expected lines` | 上下文不匹配(文件已变) | 重读目标块,缩小 hunk |
| 空 stdout + stderr 报错 | 首行/路径格式错误 | 检查相对路径、envelope |
| partialChanges | 前序 hunk 成功,当前失败 | 基于当前文件状态补小 patch |
成功 stdout`Success. Updated the following files:` + 文件列表。失败 stdout 为空,stderr 写原因。
### 远端 patch 正确姿势
第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 operation 参数 `--cwd /path`,不要从 host 生成 diff 再改路径上传:
```bash
# ✅ 正确
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /web <<'PATCH'
*** Begin Patch
*** Update File: app.mjs
@@
-old
+new
*** End Patch
PATCH
# ❌ 错误:host 生成 diff → 本地 sed 改路径 → 管道到 pod
```
---
## 60s 硬超时与短连接
`trans` 有 60 秒硬超时(不可调大)。长任务必须拆成 submit-and-poll
```bash
# ✅ 异步启动 + 短轮询
trans G14:/root/hwlab sh -- 'hwlab-cli case run start d601-f103-v2-compile'
trans G14:/root/hwlab sh -- 'hwlab-cli case run status <runId>'
# ❌ 长时间挂着等
trans G14:/root/hwlab sh -- 'long_running_command && wait'
```
### HINT 信号
| stderr 输出 | 含义 |
|-------------|------|
| `UNIDESK_SSH_HINT` | SSH 握手/超时摩擦,提示改用 `sh`/`bash` stdin heredoc |
| `UNIDESK_TRAN_TIMEOUT_HINT` | 60s 硬超时触发,提示改用短查询+轮询 |
| `UNIDESK_SSH_TIMING` | 慢命令 warning>10s),用于性能回归监控 |
| `UNIDESK_APPLY_PATCH_TIMING` | apply-patch 耗时摘要 |
---
## 关键约定
### heredoc 优先
多行远端脚本优先用 heredoc,不拼接 shell 字符串:
```bash
# ✅ 推荐
trans G14:/root/hwlab sh <<'SH'
cmd1
cmd2
SH
# ❌ 避免
trans G14 sh -- 'cmd1 && cmd2 && cmd3'
```
### 本地 shell 运算符陷阱
`&&` / `;` / `|``trans` 后面会被本地 shell 截获。需要远端执行多条命令时用 `sh``bash` 包裹:
```bash
# ❌ 第二个 sed 在 master server 本地执行
trans G14:/root/hwlab sed -n '1,20p' a && sed -n '1,20p' b
# ✅ 两个 sed 都在远端
trans G14:/root/hwlab sh -- 'sed -n "1,20p" a && sed -n "1,20p" b'
```
### Windows quoting
Windows PowerShell heredoc 用单引号 `<<'PS'`
```bash
trans D601:win ps <<'PS'
Write-Output "hello"
PS
```
- 不确定 route 语法、k3s/workspace/Windows 定位时,读 [references/full.md](references/full.md) 的 Route 语法段。
- 编写远端 patch 前,读 apply-patch 语法、上下文定位和常见失败段。
- 需要 shell heredoc、Python、upload/download、Playwright 或超时处理时,读 Operation 和 60s 段。
@@ -0,0 +1,353 @@
---
name: unidesk-trans
description: UniDesk SSH 透传与 apply-patch 语法 — `trans <route> <operation>` 分布式执行入口,包含 route 语法、workspace/k3s/Windows 路由、apply-patch envelope 格式、sh/bash/py/upload/download operation 和 60s 短连接约束。用户提到 trans、tran、ssh 透传、远端执行、apply-patch、apply_patch、远端 patch、k3s route、workspace route 时使用。
---
# UniDesk TransSSH 透传 + apply-patch
UniDesk 分布式 SSH 透传入口,统一通过 `trans <route> <operation>` 在远端 host/k3s/Windows 上执行命令或文本 patch。
**固定入口**: `trans <route> ...`wrapper 位于 `/root/.local/bin/trans`,委托 repo 内 ssh-only 启动入口 `bun scripts/ssh-cli.ts ssh "$@"`,避免被无关 CLI 子命令模块解析失败拖垮)
---
## Route 语法
```
trans <providerId>[/absolute/workspace] <operation> [args...]
trans <providerId>:<plane>[:<namespace>:<resource>[:<container>]] <operation> [args...]
```
### Host workspace route
```bash
trans G14:/root/hwlab git status --short --branch
trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'
trans D601:/home/ubuntu/workspace/unidesk-dev sh <<'SH'
...
SH
```
**规则**: workspace 路径写在 route 第一个 token,不写进 `cd` 串。反面示例:~~`trans G14 sh -- 'cd /root/hwlab && git status'`~~
### k3s route
```bash
# 控制面
trans G14:k3s kubectl get pods -n hwlab-dev
trans D601:k3s kubectl get pods -A
# 指定 namespace + workload
trans G14:k3s:hwlab-dev:hwlab-cloud-web:app exec --cwd /app -- cat /app/version.txt
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /workspace <<'PATCH'
...
PATCH
```
CLI 自动注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。k3s route 中 `:` 是分布式路由分隔符,`/` 只表示容器内文件系统 cwd;容器选择必须写 `:<container>` 或 operation 参数 `--container <container>`,不要写成 `pod/<pod>/<container>`
### Windows route
```bash
trans D601:win ps <<'PS'
Get-ChildItem C:\test
PS
trans D601:win/c/test cmd cd
```
Windows operation 必须显式区分:`ps` 走 PowerShell`cmd` 走 cmd.exe。
### GitHub issue/PR route
```bash
trans gh:/pikasTech/HWLAB/issue ls --state all --limit 20 --search 'YAML-first 测试账号 API_KEY 准备 PJ2026-0105' --full
trans gh:/pikasTech/HWLAB/issue/1236 cat
trans gh:/pikasTech/HWLAB/issue/1236 rg 'API_KEY|YAML-first'
trans gh:/pikasTech/HWLAB/issue/1236 apply-patch <<'PATCH'
*** Begin Patch
*** Update File: body.md
@@
-old text
+new text
*** End Patch
PATCH
```
GitHub issue/PR 正文读写也走 `trans gh:/owner/repo/...`。常见读取优先用 `issue ls --search/--state``cat``rg`,不要为了看正文先回到 `bun scripts/cli.ts gh issue view --json body --full`;普通小补丁可直接 `apply-patch``--dry-run` 只作为高风险正文预览。
---
## Operation
### sh / bash(显式 shell heredoc
```bash
# POSIX /bin/sh heredoc
trans G14:/root/hwlab sh <<'SH'
echo "step 1"
git status --short --branch
echo "step 2"
SH
# Bash heredoc:只有用到 pipefail、数组、[[ ... ]]、${var:0:8} 等 Bash 语法时使用
trans G14:/root/hwlab bash <<'BASH'
set -euo pipefail
short="${GITHUB_SHA:0:8}"
printf '%s\n' "$short"
BASH
# one-liner
trans G14:/root/hwlab sh -- 'git fetch origin G14 && git pull --ff-only origin G14'
# 单进程 direct argv 不放在 sh/bash 下,直接用已知子命令或 argv
trans D601:/path sed -n '1,20p' AGENTS.md
trans D601:/path argv sed -n '1,20p' AGENTS.md
```
`script``shell` operation 已移除并会失败;必须在 operation 位置明确写 `sh``bash`,让调用者和 Agent 都知道脚本语法边界。Windows PowerShell 必须用 `ps`
### argv(单命令)
```bash
trans G14:/root/hwlab git status --short --branch
trans D601 hostname
```
### apply-patch(远端文本 patch
- **P0: 所有远端文本/源码修改必须优先使用 `apply-patch`,禁止用 `py`python3 写文件)或 `sed` heredoc 拼接大段文本替代。** 只有当 apply-patch 本身不可用或需处理非文本/批量机械生成文件时,才使用其他受控方式;使用前必须在注释中说明原因,修改后立即用 `git diff` 或文件尾部检查确认没有截断或污染。
```bash
trans G14:/root/hwlab apply-patch <<'PATCH'
*** Begin Patch
*** Update File: AGENTS.md
@@
## Heading
context line
-old line
+new line
more context
*** End Patch
PATCH
# 从文件读
trans G14:/root/hwlab apply-patch < patch.diff
```
v2 引擎(默认):本地 TypeScript 解析 hunk,远端只读写文件。v1 legacy 入口:`apply-patch-v1`
### py(远端 Python 脚本)
```bash
trans D601 py -- arg1 arg2 < script.py
```
自动写到远端临时 `.py` 文件,`python3 -u` 执行后清理。
### upload / download(整文件传输)
```bash
trans G14:/root/hwlab upload ./local-file.txt /root/remote-file.txt
trans G14:/root/hwlab download /root/remote-file.txt ./local-file.txt
```
自动校验 SHA-256,结果中 `verified=true` 即完整性证明。
### playwright(远端浏览器验收)
`trans <route> playwright` 只保留为跨 host 短生命周期浏览器执行和截图/PDF 回传底座。UniDesk/HWLAB Web 开发、HWLAB `web-probe run|script`、fake-server Playwright、fixture 脱敏、截图 artifact 与 Workbench/Performance 判定口径统一见 `$unidesk-webdev`;本 SSH 透传 skill 不维护第二套 Web 测试流程。
### kubectl / logsk3s 诊断)
```bash
trans G14:k3s kubectl get pods -n hwlab-dev
trans G14:k3s logs deploy/hwlab-cloud-api -n hwlab-dev --tail 50
trans G14:k3s:hwlab-dev:hwlab-cloud-web-abc logs --tail 100
```
### skills(远端 skill 发现)
```bash
trans G14 skills [--scope all|wsl|windows] [--limit N]
```
### tcp-pool 状态与并发 smoke
查看 Provider 是否使用 SSH TCP data pool
```bash
bun scripts/cli.ts debug health
```
在输出或 frontend 原始 JSON 里看这些 labels`providerGatewaySshDataTransport=tcp-pool``providerGatewaySshDataPoolReady``providerGatewaySshDataPoolClaimed``providerGatewaySshDataPoolDesired``providerGatewaySshDataPoolLastError`
单个 provider 的低噪声池状态:
```bash
bun scripts/cli.ts debug ssh-pool D601
```
如果 `trans`/`tran` stderr 出现 `UNIDESK_SSH_TCP_POOL_HINT`,优先按其中的 `failureKind` 处理:`provider-data-channel-closed` 表示会话中途 data channel 断开,`provider-data-channel-missing` 表示 core/provider 对 channel 状态不一致,`provider-data-pool-exhausted` 表示没有空闲 channel。对幂等受控操作先查 `debug ssh-pool <provider>`,再重试原受控 CLI;不要把这类 hint 单独定性为远端 runtime 配置失败。
快速验证 D601 维护桥:
```bash
trans D601 argv true
trans D601 argv hostname
```
并发 smoke 用短命令并给每路加本地 timeout:
```bash
tmp=$(mktemp -d /tmp/trans-d601-pool-10.XXXXXX)
for i in $(seq 1 10); do
timeout 30s trans D601 sh -- 'printf start:'"$i"':%s\\n "$(date +%s%3N)"; sleep 2; printf end:'"$i"':%s\\n "$(date +%s%3N)"' >"$tmp/$i.out" 2>"$tmp/$i.err" &
done
wait
for i in $(seq 1 10); do echo "[$i]"; cat "$tmp/$i.out"; cat "$tmp/$i.err" >&2; done
```
期望是每一路 rc=0、stderr 为空、stdout 同时包含 start/end;结束后 pool labels 回到 `claimed=0`
---
## apply-patch 语法(Envelope 格式)
**不是 unified diff**,是 Codex 专用 envelope。
```
*** Begin Patch
*** Update File: <relative-path>
@@
context line
-old line
+new line
more context
*** End Patch
```
### 关键规则
| 规则 | 正确 | 错误 |
|------|------|------|
| 路径必须相对 | `AGENTS.md` | `/root/unidesk/AGENTS.md` |
| hunk 分隔符 | `@@` | `@@ -N,M +K,L @@` |
| 空行 | ` \n`(空格+换行) | 裸 `\n` |
| 首行/尾行 | `*** Begin Patch` / `*** End Patch` | 外层不能有空行 |
### 操作类型
```bash
*** Update File: path/to/file.ext # 修改现有文件
*** Add File: path/to/new.ext # 新增文件(可覆盖已有)
*** Delete File: path/to/old.ext # 删除文件
*** Move File: old/path -> new/path # 移动/重命名
```
### 上下文定位
默认 3 行上文 + 3 行下文。同一 hunk 在文件中重复时用 `@@` 跳到 class/function
```
*** Update File: src/app.ts
@@ class MyComponent
old context
-old code
+new code
more context
```
### 常见失败
| 症状 | 原因 | 处理 |
|------|------|------|
| `failed to find expected lines` | 上下文不匹配(文件已变) | 重读目标块,缩小 hunk |
| 空 stdout + stderr 报错 | 首行/路径格式错误 | 检查相对路径、envelope |
| partialChanges | 前序 hunk 成功,当前失败 | 基于当前文件状态补小 patch |
成功 stdout`Success. Updated the following files:` + 文件列表。失败 stdout 为空,stderr 写原因。
### 远端 patch 正确姿势
第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 operation 参数 `--cwd /path`,不要从 host 生成 diff 再改路径上传:
```bash
# ✅ 正确
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /web <<'PATCH'
*** Begin Patch
*** Update File: app.mjs
@@
-old
+new
*** End Patch
PATCH
# ❌ 错误:host 生成 diff → 本地 sed 改路径 → 管道到 pod
```
---
## 60s 硬超时与短连接
`trans` 有 60 秒硬超时(不可调大)。长任务必须拆成 submit-and-poll
```bash
# ✅ 异步启动 + 短轮询
trans G14:/root/hwlab sh -- 'hwlab-cli case run start d601-f103-v2-compile'
trans G14:/root/hwlab sh -- 'hwlab-cli case run status <runId>'
# ❌ 长时间挂着等
trans G14:/root/hwlab sh -- 'long_running_command && wait'
```
### HINT 信号
| stderr 输出 | 含义 |
|-------------|------|
| `UNIDESK_SSH_HINT` | SSH 握手/超时摩擦,提示改用 `sh`/`bash` stdin heredoc |
| `UNIDESK_TRAN_TIMEOUT_HINT` | 60s 硬超时触发,提示改用短查询+轮询 |
| `UNIDESK_SSH_TIMING` | 慢命令 warning>10s),用于性能回归监控 |
| `UNIDESK_APPLY_PATCH_TIMING` | apply-patch 耗时摘要 |
---
## 关键约定
### heredoc 优先
多行远端脚本优先用 heredoc,不拼接 shell 字符串:
```bash
# ✅ 推荐
trans G14:/root/hwlab sh <<'SH'
cmd1
cmd2
SH
# ❌ 避免
trans G14 sh -- 'cmd1 && cmd2 && cmd3'
```
### 本地 shell 运算符陷阱
`&&` / `;` / `|``trans` 后面会被本地 shell 截获。需要远端执行多条命令时用 `sh``bash` 包裹:
```bash
# ❌ 第二个 sed 在 master server 本地执行
trans G14:/root/hwlab sed -n '1,20p' a && sed -n '1,20p' b
# ✅ 两个 sed 都在远端
trans G14:/root/hwlab sh -- 'sed -n "1,20p" a && sed -n "1,20p" b'
```
### Windows quoting
Windows PowerShell heredoc 用单引号 `<<'PS'`
```bash
trans D601:win ps <<'PS'
Write-Output "hello"
PS
```
+21 -250
View File
@@ -5,264 +5,35 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H
# UniDesk WebDev
本技能是 UniDesk/HWLAB Web 开发、浏览器复测和 Playwright 回归的唯一操作面。需求真相源仍是 UniDesk OA 规格,例如 `project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md``PJ2026-01060505-workbench-performance.md`;本技能只规定如何开发、采集、复现和验收。
本技能是 UniDesk/HWLAB Web 开发、浏览器复测和 Playwright 回归的入口。需求真相源仍是 `project-management/PJ2026-01/specs/`;本技能只规定如何开发、采集、复现和验收。
## P0 防分叉原
## 快速规
- 单一权威入口:先按 issue/CLI 明确的 node + lane 解析 workspace、web-probe origin、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML;不得把 D601、G14、v0.2、v0.3、旧端口、ClusterIP 或本地 dev server 写成隐藏默认
- 防分叉原则:Web/Workbench 修复必须先确定唯一 authority、唯一 API 契约和唯一状态投影入口;不得写成“当字段缺失/请求失败/状态不符/超时/刷新后,再改走另一个 endpoint、旧 workspace/conversation 模型、localStorage、trace polling、result polling、GET read-through repair、内部 manager、legacy route 或测试专用后门”。字段缺失、状态滞后或投影不完整时,修 schema、projector、read model 或正式 mutation 源头;代码里只能保留单一路径内的输入规范化、校验和错误暴露,不能用 `A ?? B``if missing then fallback`、兼容分支或双写双读把缺口遮住
- 禁止状态竞争行为:不得在代码、API、reducer、read model、fake-server 或测试中保留两个事实源,再用“覆盖、压过、override、优先级、优先采用、以 A 为准否则 B、A 赢过 B”等仲裁规则决定展示状态;这就是条件分叉和竞争投影。必须先定义唯一投影对象、唯一归属字段和唯一生成时机,让 UI/API 只消费该投影对象的单一状态;其他原始字段只能作为审计输入、诊断字段或待收敛数据,不能参与展示状态仲裁
- 严禁读侧推理:REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 和测试断言都不能从 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot 或 localStorage 推断 turn/session/message 的 lifecycle、terminal、final response 或 running 状态。读侧只能读取唯一投影对象已经写好的字段;字段缺失、投影滞后或多字段矛盾时必须暴露 diagnostic/blocker 并修 projection writer/finalizer/schema,不得用 `result?.status ?? trace?.status`、最后一条 event `completed`、message fallback、session fallback、elapsed timeout 或 UI heuristic 补造事实
- Sealed final response 不可被读侧诊断覆盖:assistant final response 一旦由唯一 durable projection 写入 terminal/sealed 字段,主消息正文、finalResponse、message status 和 turn terminal 就是 sealed 用户结果;turn polling、trace hydration、SSE gap、realtime timeout、transport close、compat wrapper error 或 read model lag 只能进入 trace detail、transport diagnostics、projection diagnostics、session health 或消息详情入口,不能替换主 timeline 正文。
- 无多路径、无 fallback:这是本技能的核心原则。Web、CLI、fake-server 和线上 web-probe 必须复用同一套 API 契约、同一 route/session/conversation/trace authority 和同一状态投影;不得为了让测试通过增加第二套 dispatcher、localStorage 真相、workspace snapshot 真相、内部 manager 调用、旧 lane 端口、raw request、临时兼容路径或“测试专用后门”。
- 严禁事后 repair / 0repairWeb、Workbench、fake-server 和 web-probe 不得通过 reload、切换 session、realign、`sessionRepair`、workspace selection repair、active tab repair、GET read-through、SSE gap repair、localStorage truth 或测试 helper 自动点击,把已经分裂的 route/session/message/trace 状态补成看起来正确。合法页面必须在首次进入、刷新、deep link、session 切换和 SSE 重连时按同一权威自然收敛;不一致必须暴露为 blocker/diagnostic,并修 schema、projector、read model、reducer 或 route authority 源头。允许 session-bound hydrate/gap-fill 只写目标 session 自己的 cache/read model,不得改变 active selection、URL 或当前消息区。
- Workbench GET 纯读投影:`GET /v1/workbench/*` 只能读取已持久化的 Workbench 投影事实并组装摘要;不得在 GET 中调用 AgentRun、Code Agent 或其他上游执行面做同步、补事件、修 trace、finalize terminal、改 running/completed 或写 message/session。投影滞后必须显式暴露 `projectionStatus``lastProjectedSeq``sourceRunId``blocker` 或等价诊断字段,并由后台 projector/finalizer 或显式受控 mutation 处理;刷新页面、切换 session 或打开详情不能成为事实推进入口。
- Workbench session lifecycle:空消息/无活动 session 的回收只能由后端 lifecycle GC、后台 finalizer 或显式受控 mutation 完成,TTL 和开关从选中 node/lane 的受控配置进入运行面;Web、fake-server 和 web-probe 不得靠隐藏 tab、前端 DELETE timer、GET read-through 或 deep link repair 清理。归档/删除后的 session deep link 必须观察同一 authority 的 404/archived diagnostic,不能补建或复活 session。
- Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutationWeb reducer/selectors、route hydrate、GET/list/detail/messages/SSE consumer、fake-server 和测试 helper 都不能由读侧失败、404、空列表、网络瞬断或 late response 触发这些 mutation。只有用户显式 mutation 成功,或后端 canonical lifecycle projection,能改变 session lifecycle。需求真相见 UniDesk OA `PJ2026-010401 Web工作台` 的“破坏性投影权”。
- fake-server 不是第二后端:它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*``/v1/*``/health*` 请求应失败并暴露 path;不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。
- 真实数据优先:fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界,并标明 `derivedFrom``syntheticReason`
- 原入口闭环:fake-server Playwright 用例负责可重复红灯和源码回归;线上 `web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。
- Master server 禁重型验证:不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。
- 禁止裸写 PlaywrightUniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`;不得直接写 `trans <route> playwright` heredoc、`playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有非 HWLAB 外站截图/PDF 等 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。
- 线上 Web bug、Workbench、Performance、前端状态投影、截图、fake-server 和 web-probe 任务必须先用本技能
- 真实用户入口验证优先;源码检查、构建通过或截图局部正常不能替代原入口验收
- Web probe、Playwright、fake-server 的详细命令和历史判定口径见 [references/full.md](references/full.md)
- 前端改动遵循仓库既有设计系统和 `$frontend-design` 全局 UI 规则;不要做营销式落地页替代真实工具页面
## 工作流
## 高频工作流
1. 定位目标:确认 repo、node、lane、workspace、public origin 和目标 SPEC。HWLAB D601 v0.3 例子是 `/home/ubuntu/workspace/hwlab-v03` + `https://hwlab.pikapython.com`,但只能在 issue/CLI 指向该目标时使用
2.规则:进入目标 workspace 前读取目标 `AGENTS.md`;涉及规格或测试设计时读取 UniDesk OA 对应 SPEC
3. 线上复现:短动作用 `web-probe run|script`,长程 Workbench/session 观测用 `web-probe observe start|command|status|stop|analyze`;保存 `scriptSha256``runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id
4. 建红灯:修 Workbench/Performance 用户可见 bug 前,在目标 HWLAB workspace 的 `web/hwlab-cloud-web` fake-server Playwright 套件中补确定性用例;fixture 从真实采集样本脱敏产生
5. 修源码:保持状态读写单一路径。状态投影类修复优先收敛 server-state/reducer/projection,不在 UI 组件、trace polling、result polling 或 localStorage 中新增竞争事实
6. 验证:先跑 fake-server Playwright 目标用例,再回到同一 node/lane public origin 用 web-probe 复测;多轮任务必须用同一个 observer/session 采样到终态。截图、report hash 和 analyze finding 作为证据回传,不进入源码仓库。
7. 写 issue/closeout:问题登记和收口必须写明目标合并分支、node/lane、fixture 来源、Playwright fake-server 用例、线上 web-probe 命令/脚本 SHA、截图 SHA 和剩余边界。
1. 先确认目标:repo、分支/lane、node、用户入口 URL、预期页面或 workflow
2.取对应 SPEC 或 issue,明确验收口径;Workbench 和 Performance 页不要只凭局部 DOM 判断
3. 用受控 CLI 或 Playwright wrapper 复现:优先 `bun scripts/cli.ts web-probe ...`,必要时用 `bun scripts/playwright-cli.ts ...`
4. 修改前端代码后执行最小相关验证,再用同一原入口复测
5. 需要关闭用户反馈/issue 时,记录真实入口证据、截图/trace/session 和失败或通过结论
## HWLAB Web Probe
线上 Cloud Web DOM 验收优先使用受控入口:
## 常用入口
```bash
bun scripts/cli.ts web-probe run --node D601 --lane v03
bun scripts/cli.ts web-probe --help
bun scripts/playwright-cli.ts --help
bun scripts/cli.ts check --syntax-only
```
web-probe 入口分三类:
HWLAB Web probe、fake-server Playwright、fixture 采集脱敏、Workbench/Performance 判定、Web E2E 和线上复测的完整命令矩阵在 [references/full.md](references/full.md)。
- `run`repo-owned 标准 DOM probe,适合固定 P4 验收和已有脚本。
- `script`:受控 Playwright 托管脚本,适合一轮 55 秒内完成的 DOM/API 断言、截图、route/intercept 和边界采样。
- `observe`:纯客户端长程观测,适合同一 Workbench session 多轮任务、realtime/projection 问题、长时间 trace/DOM/network 采样和无副作用报告生成。长程 Workbench 观测默认同时打开两个浏览器页面:control 页面只执行显式 `observe command` 用户动作,observer 页面只打开同一个 session 做被动观察,并默认每 3 分钟整页刷新一次同一 session,模拟用户离开后返回,用来抓多用户/多页面下同一 session 的投影差异、历史 trace 丢失、耗时跳变和 loading 差异。
## 何时读取 reference
工测优先级:Workbench trace 乱序、完成行位置、耗时不一致、时间跳变、final response flicker、session 不刷新、性能慢路径和多轮可靠性问题,默认先用 `observe` 采样并跑 `observe analyze`;不要把临时 Playwright spec、裸 DOM 调试脚本或单次 `script` 结果当作主要工测证据。`script/run` 只作为短路径断言、截图/API 摘要或 observe/analyze 后的补充复核
需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright
```bash
bun scripts/cli.ts web-probe script --node D601 --lane v03 <<'JS'
export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => {
await gotoStable('/workbench');
const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5');
const firstSession = sessionsResponse.body?.sessions?.[0] ?? sessionsResponse.body?.items?.[0] ?? null;
const sessionId = firstSession?.sessionId ?? firstSession?.id ?? null;
const apiMatrix = await fetchApiMatrix([
'/v1/workbench/sessions?limit=5',
...(sessionId ? [
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`,
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages`
] : []),
'/auth/session'
]);
recordStep('workbench-ready', { sessionsOk: sessionsResponse.ok, sessionId, apiMatrixOk: apiMatrix.ok });
return {
sessionsOk: sessionsResponse.ok,
sessionCount: sessionsResponse.body?.sessions?.length ?? sessionsResponse.body?.items?.length ?? null,
sessionId,
screenshot: await screenshot('workbench.png')
};
};
JS
```
长程 Workbench 观测使用 `observe` 命令组:
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type newSession
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectProvider --provider codex-api
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type cancel
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'
long prompt
EOF
bun scripts/cli.ts web-probe observe status webobs-xxxx --tail-lines 6
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --command-id cmd-xxxx
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42
bun scripts/cli.ts web-probe observe stop webobs-xxxx
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image build --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run
bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .state/web-probe-sentinel-smoke --scheduler-disabled --once
```
`observe analyze` 的 duplicate final response 判定必须以 trace-frame 可见行事实为准。`observe collect --view trace-frame` 固定渲染的 `Final Response` 区块是 summary,不是第二条业务 assistant message;只有同一 trace-frame 中出现两个可见 assistant final rows 且内容重复时,才应报告 duplicate finding,并在证据中写明 `finalResponseSummaryBlockCounted=false`
`observe collect --view timeline` 用于 issue closeout 前的 artifact drill-down:它只读取现有 `control.jsonl``samples.jsonl``commands/{pending,processing,done,failed,abandoned}/*.json`,默认输出 bounded timeline、关键 metadata、脱敏 disclosure 和下一步命令。按 `--command-id``--turn``--trace-id``--sample-seq``--timestamp``--window-ms` 缩小窗口;需要完整原始内容时必须显式使用 `--raw``--view files --file ...`
项目管理 / MDTODO 页面同样优先使用 `observe`,不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`,同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type gotoProjectMdtodo
bun scripts/cli.ts web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/
bun scripts/cli.ts web-probe observe command webobs-xxxx --type probeMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type reindexMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed
bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
```
`observe command --type newSession` 是显式用户/control action:它通过当前 Workbench UI 点击 `#session-create` 创建新 session,等待 active session id 改变和 composer ready,并把前后 snapshot 写入 control log。它只能用于采样开始时建立目标 session,或作为用户明确的新建会话动作;不得在 route/session mismatch 后当作 repair 手段。
约束:
- `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`
- `web-probe run|script|observe start` 的默认 URL、browser proxy mode、observe/analyze 报警阈值和 project-management 采样/命令 allowlist 必须来自 `config/hwlab-node-lanes.yaml``webProbe`;需要排除公网/FRP/跨国 proxy 抖动时,在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin,不要在命令行长期手写 `--url` 或裸 Playwright。
- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 commandobserver 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole``pageId``sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession``selectProvider``sendPrompt``steer``cancel``goto``screenshot``mark``stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin``steer --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。
- `observe command --type steer``--type cancel` 是显式用户/control actionsteer 复用当前 Workbench composer 的运行中 turn 引导路径,cancel 复用同一 composer 主按钮的取消路径。二者必须进入 `control.jsonl`,不能用后端私有 API、AgentRun direct cancel 或测试后门替代。`configureMdtodoHwpodSource``probeMdtodoSource``reindexMdtodoSource``selectMdtodoSource``selectMdtodoFile``selectMdtodoTask``editMdtodoTaskTitle``editMdtodoTaskBody``toggleMdtodoTaskStatus``addMdtodoSubTask``continueMdtodoTask``deleteMdtodoTask``launchWorkbenchFromMdtodo` 也是显式用户/control action,只能使用页面公开 `data-*` id、正式按钮和 YAML 允许的自然 API;它们通过 public source/file/task id 与 Workbench 关联,不能读取内部 store、私有后端或把 MDTODO 页面包含进 Workbench。
- `observe collect --view turn-summary` 是第一层 CLI 阅读视图:只从 `samples.jsonl``control.jsonl` 和已有 `analysis/report.json` 按需渲染同一 session 的多 turn 摘要,包含用户消息 preview/hash、traceId、状态、耗时/最近更新时间、steer/cancel 标记和 Final Response 摘要。`observe collect --view trace-frame --trace-id <id> --sample-seq <n>` 是第二层 CLI 阅读视图:从同一采样帧渲染单帧 trace 文字截图,并固定输出 `Final Response` 区块。`observe collect --view project-summary|project-mdtodo-summary` 从同一 artifact 渲染项目管理 / MDTODO DOM 采样、Source/File/Task 计数、command/mutation 结果、Workbench launch、捕获到的 `x-hwlab-otel-trace-id` 和 OTel/Tempo drill-down 线索;project collect 的远端 payload 必须保持 bounded compact rows,由本地 renderer 生成表格,避免 `trans` stdout 截断后 JSON parse 失败。collect 视图不是采样器新增保存物,不构成第二事实源。
- `observe start/status/command/collect/analyze` 默认输出包含 `Wrapper contract` 区块;该区块证明 Web 哨兵只能 wrap 现有 observe CLI verb、现有 runner/analyzer 和既有 artifact contract,不新增第二套 Playwright runner、analyzer、状态机或私有 web-probe API。
- `web-probe sentinel plan|status` 只读取 `observability.webProbe.sentinel.enabled/configRefs` 和 owning YAML,渲染 redacted 配置引用图、文件 hash、缺失字段和跨 ref 冲突;`web-probe sentinel image|control-plane` 继续从 owning YAML 渲染 image、GitOps、Argo 和 manifest 计划,并在远端 publish job 接通前拒绝报告部署 mutation。它不启动浏览器、不读取 Secret 值、不保存采样结果,也不是第二套 runner/analyzer。真正的采样和判定仍以 `observe start|command|collect|analyze` artifacts 为准。
- Web 哨兵 public dashboard/origin 必须以 issue/SPEC/YAML 既定计划为准;当前 P6 计划沿用 `monitor.pikapython.com`,不要未经明确变更改成 `hwlab-monitor.pikapython.com` 或其他新域名。验证 report 时记录 `publicOrigin`,但不要把域名硬编码到 runner/analyzer 逻辑里。
- `scripts/web-probe-sentinel-service.ts` 是 Web 哨兵 Pod entrypoint`--once` 只做 config/PVC/SQLite/scheduler/analyzer-command health 快照,`--scheduler-disabled` 仅用于本地服务健康冒烟,不能作为生产运行参数。HTTP 服务只提供 `/api/health``/api/status``/api/runs``/api/maintenance``/metrics` 和 redacted dashboard 外壳,底层采样仍只能经 observe CLI adapter。
- `trace-frame` 出现 `(无 trace rows;这是 blocker...)` 时,必须先看同一输出中的 `TRACE DIAGNOSTIC`:记录 pageRole/pageId、traceRows/turns/messages 数量、sampleTraceIds、尾部 traceRow/turn/message 归属。若目标 trace 的 turn/message/final 存在但 traceRows 全部属于旧 trace,应按 Workbench read model authority 分裂登记到架构/业务 issue(例:HWLAB #2124),不得把旧 traceRows 当作新 turn 通过证据,也不得让 analyzer 的聚合计数压过 CLI trace 视图。
- analyzer finding 不得压过 CLI `trace-frame` 人工视图。尤其 `trace-assistant-message-duplicates-final-response` 只有在 `trace-frame` 中同一 completed turn 可见多条相同 assistant final rows 时才按业务 bug 处理;如果 `trace-frame` 只有一条 assistant final row、后面固定 `Final Response` 区块正确且 API messages/turns 对齐,该 amber 归类为 analyzer 精度问题,应登记/修工具,不得阻止业务 closeout。
-`observe status` 显示 PID still alive 但 heartbeat/sample 不推进、`commands/pending/*.json` 不被消费,或 `observe stop --force` 只是继续排队 stop command,应先按 web-probe runner 工具缺陷处理(例:UniDesk #874),用 route 只读确认 PID/heartbeat 后清理进程;不要把 pending command、未触发的 cancel 或 runner stale 混入 Workbench 业务结论。
- `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要;不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。
- 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次;每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal,应同时登记“执行终态”和“Workbench 投影未收敛”,不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。
- `observe command --type sendPrompt` 是普通新 turn 路径,composer 主按钮应处于 `data-action=turn`;如果仍是 `steer``cancel`,必须作为工具/页面状态不一致失败暴露,不能点击后再等待错误接口。空输入状态下 submit disabled 可以为 true,不能把 disabled 当作新 turn 不可用的唯一依据;应先看 input present/enabled、warning absent 和 action=turn,再由正式填入 prompt 后提交。
- `observe analyze` 是离线分析,只读取 artifact JSONL 并写 `analysis/report.md``analysis/report.json`,不访问 Workbench API、不驱动浏览器。`observe start` 每次启动必须先把同一 stateDir 中已有的根目录 JSONL 轮转到带时间戳的 `archive/` 文件;`observe analyze` 默认只分析当前根目录 JSONL,不扫描历史 archive,只有显式指定 archive prefix 时才分析历史轮转窗口。报告必须输出采样点 vs 每个 turn 的总耗时/最近更新时间表、trace row 视觉顺序异常、terminal/轮次完成 row 是否最后、Code Agent 卡片耗时与 trace/轮次完成总耗时一致性、可见“加载中”的数量/归属/并发 owner/连续出现区间、DOM diagnostic/HTTP/console/requestfailed/runtime execution error 分组、page asset provenance segment、同源 API Resource Timing 分位表和超过 YAML `webProbe.alertThresholds` budget 的慢路径 finding;项目管理页还必须输出 DOM readiness、source/file/task 计数、缺失 public task ref、Workbench launch success/failure、captured OTel trace header、自然 project-management API 分组和超过 YAML `webProbe.projectManagement.slowApiBudgetMs` 的慢路径 finding。页面/API 加载、可见“加载中”、长连接打开耗时、turn timing 跳变、trace row 顺序、卡片耗时/轮次完成耗时一致性、session fallback 标题比例和项目管理 API 慢路径阈值只能改 YAML,不能在 analyzer/renderer 中写死。修复必须降低真实请求、投影、渲染或后端路径耗时,禁止为了减少“加载中”出现时间而提前展示未加载完的内容,也不能靠下游 retry/reload/fallback 掩盖。报告里的 `trace-row-order-nonmonotonic``trace-completion-row-not-last``round-completion-elapsed-mismatch``code-agent-card-duration-underreported``final-response-flicker``uncommanded-visible-state-change``mdtodo-workbench-launch-otel-trace-missing``project-management-api-slow`、session changed、network 503 等 finding 是排障线索;用于 closeout 时必须结合原始 session/trace/DOM 证据解释,避免把采样噪声直接当作业务结论。
- `sentinel validate --quick-verify` 超过 120s 是严重超时,必须保持 warning/red 并优先从 envreuse、git mirror、warm runner 复用、first tool execution、Workbench 投影和 observe/analyze 开销排查;不要通过提高 budget、减少轮数、放宽 analyzer 或绕过 CLI trace 视图来让场景变绿。quick-verify 等待每轮终态时应读取既有 sampler artifacts 和 bounded collect 视图,不能反复启动完整 collect 或新增第二份“trace 截图”保存来源。
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内;能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内,并减少无界 selector/network 等待。确需等待更久时,改用 `web-probe run` 的异步 job/status 语义,或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath``reportSha256`、screenshots 或 DOM steps,先按远端报告判断脚本/页面实际状态;最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence``summary.issueEvidence`;只有需要展开调查时才粘贴 `probe.script.result``probe.steps` 或完整 `reportPath`,避免 stdout、summary 和 report 多层重复同一证据。
- stdin heredoc 与 `--script-file` 都按 ES module 加载,脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`;不要在模块顶层直接写 `return`。失败为 `Illegal return statement``does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时,先按 probe 脚本入口误用处理,不要归因成 Cloud Web 行为失败。
- 自定义脚本需要主动失败时,优先返回 `{ ok: false, failedCondition: "..." }` 或抛出错误;兼容返回 `{ pass: false }``{ success: false }`,且 `{ ok: true, pass: false }` 仍按失败处理。`failedCondition``errorMessage``message``error``reason``summary` 会进入失败摘要;不要只把失败埋在普通业务字段里。
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
- 需要禁用 `EventSource`、mock `Date`/clock、注入 preload hook 或修改浏览器启动前全局对象时,`page.addInitScript()` 必须在目标页面整页加载前注册,并用 `page.goto(new URL(path, origin).toString(), { waitUntil: "domcontentloaded" })` 进入目标 deep link;不要依赖 `gotoStable()` 复用当前 SPA 页面后再期待 init script 生效。若只在已加载页面上 route/block 请求,既有 SSE 或 store 状态可能继续更新,不能作为“无后端刷新依赖”的验收。
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`;不要拼出 `//v1/...`
- 先通过 `gotoStable``waitWorkbenchReady` 或等价导航进入目标 origin,再用 `fetchJson` 读同源 API;不要在 `about:blank` 上请求 `/v1/...``fetchJson` 返回包装对象 `{ ok, status, body, failureKind, ... }`,业务字段必须从 `.body` 读取,并同时记录 `ok/status/failureKind`;如果返回 `status=0` 且页面还不是目标 origin,先按 probe 脚本误用处理,不要归因成 Cloud API 或 Workbench 投影失败。
- Workbench 有 SSE/长轮询时不要用 `networkidle` 判断通过;用明确 DOM/API 条件,如 final URL、route sessionId、active tab、message card、trace row 或 durable session detail/messages。
- 当前 HWLAB v0.3 Workbench durable read model 的验收端点是 `/v1/workbench/sessions``/v1/workbench/sessions/{sessionId}``/v1/workbench/sessions/{sessionId}/messages``/v1/workbench/turns/{traceId}``/v1/workbench/traces/{traceId}/events`;提交 mutation 可用正式 `/v1/agent/chat` 产生真实 turn。`/v1/workbench/workspace` 只在当前 API 契约声明为选择/config 快照时采集,不作为消息或 trace authority`/v1/agent/conversations` 属于旧模型,不得作为 v0.3 Workbench durable projection 的通过条件。
- 需要稳定 running message、timing、SSE 或 projection 样本时,优先通过正式同源 mutation 创建样本:`POST /v1/agent/sessions` 建 session,再按前端同一路径提交 `/v1/agent/chat`,轮询 `/v1/workbench/sessions/{sessionId}/messages` 等待 durable projection。不要把历史 session 是否仍 running 当作验收前提,也不要用旧 conversation/workspace 端点造样本。验收结束后能取消的 turn 必须调用 `/v1/agent/chat/cancel` 清理,并在 issue evidence 只记录 trace/session、status、script/report SHA 和必要摘要,避免粘贴完整 provider payload。
- HWLAB v0.3 `fa2ad4845a111e0f9e86d473eb4204ed1b2b0a3c` 后,repo-owned `scripts/web-live-dom-probe.mjs` 的默认 `web-probe run --fresh-session` 已使用 durable Workbench session 端点,不应再依赖 `/v1/workbench/workspace``/v1/agent/conversations` 作为消息/trace authority。若 `session-not-selected` 复现,先确认目标 workspace 已包含该提交,再对 helper 做旧端点扫描;只有扫描仍命中旧端点时才按工具链回归处理。
- `web-probe run --fresh-session` 空消息路径的基线证据是 D601 v0.3 job `web-live-dom-probe-1781842424721-44066``freshSessionAligned=true``messageCount=0`、report SHA `d34c6765943e14bbd639b91bfc17139d582f670ed5cf409a957acd40fae7871f`、screenshot SHA `8d341b7c30293baa9be8f1e63e147ea0fdafbdbcfc43f0b05e4fb9f316ef996c`
- 若带 `--message` 的 fresh-session 路径已经达到 `freshSessionAligned=true``promptSubmitted=true``messageCount>0` 和 session API 200,但随后报 `trace-id-missing``Workbench turn is not visible to the current actor` 或 trace 面板同类不可见文案,应登记为 Workbench turn/trace actor visibility 问题,不再归因到 fresh-session 选择 helper 或旧端点。
- Playwright `page.evaluate` 只能传一个可序列化参数;多个值包成对象,或用 `safeEvaluate(fn, { a, b })`
- `safeEvaluate` 返回结构化包装对象;脚本断言前必须解包,例如 `const evaluated = await safeEvaluate(...); const dom = evaluated?.value ?? evaluated;``recordStep` 可以记录包装对象用于诊断,但行为断言不要直接读包装对象的业务字段,避免把已通过的 DOM 误判成 `undefined`
- 脚本用 `page.evaluate` 模拟滚动、resize、click 或派发 DOM event 后,若断言 Vue/React 绑定出的 `data-*`、ARIA、按钮状态或 scroll-follow 状态,必须先等待一次浏览器/框架更新,例如 `await wait(300)``page.waitForFunction(...)` 或下一次明确 DOM 条件;不要在同一个 evaluate 内写完事件就立刻读取状态,否则会把尚未 flush 的响应式状态误判为业务失败。验证用户滚动暂停/恢复时优先记录操作前后 scroll metrics 和 `data-following` 的延迟稳定值。
- `web-probe script` 顶层/default export 运行在 Node 上下文,浏览器全局 `window``document``location``CSS` 只在 `safeEvaluate`/`page.evaluate` 回调内可用。Node 侧 deep link 用 `const origin = new URL(page.url()).origin; await page.goto(new URL('/workbench/sessions/' + encodeURIComponent(id), origin).toString())`,或直接用 `gotoStable('/workbench/sessions/<id>')`;不要在 Node 侧写 `location.origin``CSS.escape(...)`,也不要用没有 baseURL 的 `page.goto('/relative/path')`。若失败为 `location is not defined``CSS is not defined` 或相对 URL invalid,先按脚本误用处理;工具提示改进跟踪见 [pikasTech/unidesk#479](https://github.com/pikasTech/unidesk/issues/479)。
- 脚本中用 `recordStep(name, data)` 保存关键 DOM/API partial evidenceAPI 批量探测优先用 `fetchApiMatrix(paths)`,单个 API 失败不应让后续证据丢失。
- deleted/archived/not-found session deep link 验收用 `web-probe script` 直接 `gotoStable('/workbench/sessions/<id>')`,再用同源 `fetchJson('/v1/workbench/sessions/<id>')`、messages 和 list API 断言 404/不在列表;不要调用要求 active session selection、composer readiness 或 workspace repair 的 helper 作为通过条件。
- 验证 Cloud Web runtime config 或 HTML 注入时,优先加 cache-busting query 和 `cache-control: no-cache`,并按层拆开判断:Deployment env、Pod 内 `127.0.0.1` HTML、公网原始 HTML、浏览器 DOM。不要只凭一次 web-probe DOM 缺字段就判定 rollout 失败;先确认是运行面未渲染、Pod 未滚动、边缘缓存,还是脚本读取层级错误。
- web-probe 只能观察、截图和断言,不得用 `sessionRepair``realignFreshSession`、自动点击 session、reload 循环或 workspace selection repair 作为通过条件。发现 `routeSessionId``activeSessionId``activeConversationId`、message session 或 trace session 不一致时,必须记录 mismatch 并失败;截图和 API 摘要是证据,不是修复动作。
- 失败证据至少保留 `failureKind``errorMessage``scriptSha256``runDir``lastUrl``lastScreenshot``probe.summary``reportPath`/`reportSha256`;默认失败截图是 `failure.png`
## Fake-Server Playwright
Workbench 回归入口在目标 HWLAB repo 的 `web/hwlab-cloud-web`
```bash
bun run e2e:workbench -- --project=chromium
```
运行面要求:
- 在 issue/CLI 选中的 node/lane workspace 或其独立 worktree 上运行,不在 master server 本地运行。
- 同源 fake-server 只提供正式 API 契约,覆盖 `/auth/*`、session list/detail/messages、turn snapshot、trace event page、Workbench event stream、Performance summary 等当前用例所需端点。
- 测试断言优先使用用户可见文本、ARIA role 和稳定 DOM 标识;不要通过内部 store、localStorage、数据库或 live API 直接判定通过。
- 非终态瞬时不变量必须用立即 DOM 快照断言:例如 running/pending 阶段要求 final response 为空时,先定位当前 agent card,再用 `locator.evaluate` / `page.evaluate` 直接读取 `.message-markdown.message-text` 数组并立刻判断;不要用会自动等待的 `expect(locator).toHaveCount(0)``not.toContainText` 或类似负向 locator 断言,否则测试可能等到 terminal DOM 替换后误通过,掩盖 running 阶段污染。
- 失败必须保留截图、Playwright trace 或等价 artifact;关键路径可生成命名截图。
Workbench 至少覆盖:
- session 切换后主工作区 loading 和目标 conversation 恢复;
- 刷新页面后以同一 message/turn/trace 标识还原 timeline
- SSE 断线、丢事件或重连后通过 REST snapshot 与 trace page 补洞;
- session tab、workspace card、message card、composer 主按钮、Trace detail 的 `running`/`completed`/`failed`/`canceled` 一致性;
- completed assistant 已 sealed 后,turn snapshot、trace event page、SSE 或 realtime diagnostic 失败不覆盖主消息正文,诊断只进入详情/感叹号/transport health
- Trace 阅读按事件顺序渲染可读 row,正确处理分页、终态、失败、自动展开和终态折叠;
- deep link 与普通点击 session 走同一 authority path
- Performance 页的摘要、错误态、刷新、移动端表格可达性和低基数脱敏展示。
## Fixture 采集与脱敏
- 采集范围至少包括 session list/detail/messages、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summaryworkspace selection/config 只有在当前 API 契约仍声明该读模型时才采集。
- 产物必须记录 `capturedFrom``capturedAt`、schema/redaction 版本和 `valuesPrinted=false`
- 使用稳定伪 ID 映射保留 conversation、session、thread、turn、trace、message、sourceSeq 的关系。
- 删除或替换 `HWLAB_API_KEY`、cookie、Authorization header、DB DSN、provider token、真实用户身份、非公开 prompt、stdout/stderr 大段原文和完整 provider payload。
- 真实样本难以覆盖的延迟响应、SSE 断线、分页缺口、列表缺少当前选中项、可选字段异常、空集合和特定 HTTP 错误可以合成,但必须说明来源和原因。
## Workbench 判定口径
- Workbench API 的 GET 路径不是修复入口。若 session/messages/turn/trace 出现状态不一致,应登记为投影唯一性或投影延迟问题,修 backend projector/read model;不得用前端轮询、session 切换、reload、read-through sync 或 GET 内部修复把问题掩盖。
- Workbench 读侧不得推理生命周期。`turn.status``message.status``session.running``trace terminal``finalResponse``projectionStatus` 只能来自唯一 durable projectiontrace event row 的 `completed` 只表示该 event/tool row 完成,不能终结 turn;message 文本为空只表示无可展示正文,不能生成占位 final responselist summary、session detail、turn snapshot、trace page 之间的差异只能作为 diagnostic,不得用优先级函数在读侧“选一个看起来合理的状态”。
- Workbench sealed final response 必须与读侧诊断分仓。`messageProjection` 承载 role/status/text/finalResponse/sealedAt`traceDetail` 承载 events/page hydration/trace diagnostics`sessionStatus` 承载 rail/card 运行摘要,`transportDiagnostics` 承载 SSE/poll/hydration timeout 等可见性诊断;等价结构可以使用不同命名,但主消息投影和 diagnostic 不能竞争同一字段。`showMessageText()`、message diagnostic selector 和组件模板不得让 diagnostic 压过 sealed final response。
- Workbench trace 视觉顺序的唯一权威是 durable projection 分配的 `projectedSeq``projectedSeq` 必须由持久化 runtime store 在 trace 内全局递增分配,并按 `traceId + sourceEventId` 幂等复用;本地 `event.seq``sourceSeq`、事件到达顺序、时间戳、DOM 顺序和 renderer 输入顺序只能作为审计输入或源游标,不能直接派生视觉位置或参与仲裁。重复源事件不能分配新 `projectedSeq`;不同源事件即使局部 `event.seq` 重置为 1 也必须分配新的全局 `projectedSeq`,这个幂等性需要有最小单元测试覆盖。
- Workbench trace 时间字段的 source truth 是后端和 trace event page 暴露的 ISO/UTC 时间戳;后端 projector、durable read model 和共享 renderer 默认行为不得改成本地时区字符串。Web 展示北京时间或其他用户时区时,只能在 Vue/Web 渲染层从 runtime `displayTime` 注入 formatterCLI、CaseRun 和原始 trace API 仍显示/返回 UTC,不通过多字段仲裁、默认 fallback 或后端补写时区字符串修显示。
- Trace event page 检测到同一 trace 内历史 `projectedSeq` collision 时必须暴露 `projectionStatus=blocked``projectionHealth=degraded` 和稳定 blocker code,不能把损坏序列交给 CLI/Web renderer 继续展示。CLI/Web trace renderer 只能按 projection 已给定的位置一次渲染;不得通过 assistant 文本相等/包含、final response 匹配、尾部排序、terminal row 合并或 completion row 追加来事后修正视觉顺序。
- Workbench durable projection 验收必须覆盖同一 session/trace 的 list、detail、messages、turn、trace events 和 DOM session tab/message card;尤其要用 fresh browser 或 runtime 重启后的历史 terminal 样本证明 read model 能从 durable trace events 恢复终态。若内存 trace 仍显示 running 而 durable trace 已 terminal,应修唯一 read model 的投影对象,不得在 GET handler、前端 reducer 或 probe 脚本里做临时状态仲裁。
- HWLAB #1585/#1596/#1690 是 Workbench 投影禁令的固定判例:AgentRun 已 terminal 但 Workbench 停在旧 seq 时,不得用 GET/read-side 补 resultturn/card completed 但 TraceEventPage seq/range 不合法时,不得用 completed 状态压过 trace 不可读事实;Trace timeline 乱序或 terminal row 重排时,不得让 `sourceSeq`、局部 `event.seq`、输入顺序和 renderer 文本匹配多来源仲裁。合法修复是后台 projector/resumer 补 durable projection,在同一 trace event 快照内修正分页/顺序契约,或暴露 historical projection blocker;不得推断 lifecycle、补写 GET、切 session/reload 修页面、添加多来源优先级或在 renderer 做事后 repair。
- Session rail 会话集合只消费 `/v1/workbench/sessions`;当前 session detail 只消费 `/v1/workbench/sessions/{sessionId}`;消息本体只消费 `/v1/workbench/sessions/{sessionId}/messages`turn 状态只消费 `/v1/workbench/turns/{traceId}`trace 阅读只消费 `/v1/workbench/traces/{traceId}/events` 或正式 trace event page。不要让 workspace snapshot、列表摘要、localStorage、trace polling 或 result polling 互相覆盖。
- 空消息 session 回收验收必须同时证明:未超过 TTL 的空 session 保留,有消息、running/admitting、terminal result 或 active trace 的 session 不被回收;超过 TTL 的空 idle session 由后端 GC 归档/删除后,rail/list/detail/messages/deep link 都按同一 canonical lifecycle 状态表现。
- UI transient state 只保存 route、显式选择、composer draft、scroll、展开状态和临时交互状态;服务端事实进入 server-state/reducer/projection。
- running turn 必须同时反映在主任务区、composer 主按钮、session tab 和可取消入口;清空、切换、刷新不得把 running trace 隐藏成空态。
- 显式新建或选择 session 是 active selection 的 mutation authority`POST /v1/agent/sessions` 返回 201 或用户点击目标 session 后,URL、active tab、当前消息区和 composer 必须立即归属目标 session。目标 session detail/messages 慢只能表现为该目标 session 的 loading/empty 态;旧 session 的 list refresh、detail、turn、trace、SSE 迟到事件只能更新旧 session 自己的 cache/status,不得继续占据或夺回 active route、message area、composer/cancel 状态。
- 读侧没有破坏性投影权。`GET /v1/workbench/sessions`、session detail、messages、SSE、route hydrate 或 refreshSessions 的失败、404、空列表、网络错误和迟到响应,只能显示 loading/degraded/unknown/blocker 或保留上一份成功投影;不得 `forgetSession()``replaceActiveSessionSelection(null)`、清空 messages/tabs,或把 composer 改成 `session_required`。deleted/archived/not-found 必须来自后端 canonical lifecycle projection,或来自用户显式 DELETE mutation 成功后的展示更新。
- terminal failure、blocked、canceled 都是最终结果;若 API 返回可读 error/blockerfinal response 展示区必须直接显示,不能只放在 trace rows 或详情面板中。
- completed、failed、canceled、blocked 的 sealed final result 一旦显示,后续 polling/hydration/SSE/realtime 的 timeout、500、gap、close 和 lag 只能改变诊断区,不得把已显示 final response 闪回 `Code Agent 代理暂时无法连接上游``turn 超时无新活动``projection-resume:sync-failed` 或同类读侧错误态。
- session 切换验收必须覆盖点击态和持久化恢复态:选择目标 session、确认 route/session list/detail/messages 归属同一 `sessionId`、刷新后同一 session 仍 active。
- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/<sessionId>`,不能只用同页 reload。
- 0repair 验收必须用 fresh context 直达 session/deep link,或从当前 session 显式切换到目标 session;不得依赖测试 helper 选择、reload、切走再切回、`sessionRepair``realignFreshSession` 修正页面后再判通过。旧 session 的延迟 detail、turn、trace、SSE 或 refreshSessions 晚到,只能更新该 session cache/list,不能改变 active URL、active tab、current messages 或 composer。
- Workbench reducer/selectors/0repair 收敛类 closeout 必须做负向源码扫描并在 issue/PR 证据里写明结果;至少覆盖直接写服务端事实和旧 repair 名称,例如 `messages.value =``messages.value.push``sessions.value =``previousMessages``previousSessionId``restorePrevious``restoreMessagesTraceAuthority``repairWorkbenchMessagePage``findMessageTextFallback``projectionCatchup``PROJECTION_CATCHUP` 和组件侧 `workbench.messages`。这些扫描是辅助证据,不能替代 fake-server Playwright 与 public web-probe。
- Trace 实时性要用间隔采样,区分“加载中”和“思考中”;终态卡片不得继续保留“思考中”空态。
- 用户反馈“当前 session 不刷新、切走再切回才刷新”时,证据必须成对采集:直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图;同时记录 session list、session detail、messages、turn snapshot 和 trace event page 的状态。issue 正文要明确这是事件投影/重放问题,不能只登记成普通 deep link 或单接口 404。
## Performance 页判定口径
- `/performance` 只展示低基数、脱敏的 Workbench/RUM/API/long task 汇总;不得泄露 `conversationId``sessionId``traceId``runId`、prompt、assistant 正文、tool 参数、stdout/stderr、Secret 或用户身份。
- 单位以 API `unit` 为准;后端若以 seconds 存储,前端显示 ms 时必须只做一次换算。
- 移动端和窄屏断言要检查目标表格/按钮是否可见、可点击、layout rect 非零且可触达;不要只用 desktop selector readiness 代表所有 viewport。
- 刷新按钮应有明确 loading/error/empty 状态,重复点击不能制造 stuck loading、旧数据冒充新数据或未捕获 console/page error。
## UniDesk Frontend E2E
UniDesk 主前端交付门禁仍由 `bun scripts/cli.ts e2e run` 承担;本技能规定浏览器侧如何选取和解释验证。
- 先跑最小相关选择集,例如 `bun scripts/cli.ts e2e run --only frontend:pipeline`;目标检查通过后再跑完整 `bun scripts/cli.ts e2e run`。选择集是真执行,不是输出过滤。
- 浏览器必须打开 `config.json` / publicHost 派生的公开 frontend 或 dev frontend proxy;不要用 localhost、Docker internal URL 或后端 API 直调替代用户入口。
- 前端断言优先使用用户可见文本、ARIA role、稳定 DOM 标识和 React 控件状态;禁止用 raw JSON、内部 store、localStorage 或后端数据库绕过 UI。
- 高信息密度区域如 Pipeline 右侧栏、Trace timeline、详情抽屉、甘特图坐标、资源监控表和 Performance 面板,必须显式检查总高度、横向滚动条、关键控件可见可点、文本不重叠。
- 用户服务页必须等待真实后端数据,而不是只看页面骨架;Todo Note、OA Event Flow、Code Queue、FindJob、Pipeline、MET Nonlinear、ClaudeQQ 等页面的通过条件以当前 E2E check 名称和 issue scope 为准,不在文档里复制所有字段清单。
- raw JSON 只能在用户显式点击 `查看原始JSON` 后出现;默认页面必须把 JSON 渲染为表格、卡片、徽章、树、图或其他可读控件。
- 任何 frontend E2E 失败都应保留截图路径、resultPath、failedChecks 和关键 console/page error 摘要;不要把 missing browser/deps 当作 skip,按锁文件和目标执行面补依赖后继续。
## 受控 web-probe 代替裸 Playwright
UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时,统一选择 `web-probe run|script|observe`
`web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告:可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称;点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。
`trans <route> playwright``playwright-cli` 和临时 Node Playwright 脚本只允许作为非 HWLAB 外站短生命周期截图/PDF、或 web-probe 尚未覆盖且一次性不可复用的诊断例外。例外使用前必须写明为什么 `web-probe script``web-probe observe` 不适用;同类动作出现第二次就应补进 web-probe CLI 或 repo-owned probe。
需要把截图回传到本机时,优先用 `web-probe script``screenshot(name)`,或长程观测中使用 `observe command --type screenshot --label <label>`,并在 issue 中引用截图 SHA、report SHA、observer id 和 stateDir。不要把本地 Playwright artifact 当作 HWLAB node/lane 原入口验收替代品。
- 需要 HWLAB Web probe 采集、observe/analyze、sentinel 或线上截图时,读 [references/full.md](references/full.md) 的 `HWLAB Web Probe``受控 web-probe`
- 需要 fake-server 或 Playwright 复现时,读 `Fake-Server Playwright``UniDesk Frontend E2E` 段。
- 需要 Workbench/Performance 页判定口径时,读对应段落,不要凭经验补规则。
@@ -0,0 +1,268 @@
---
name: unidesk-webdev
description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/HWLAB Cloud Web、Workbench、Performance 页、前端状态投影、Playwright、fake-server、web-probe、截图、响应式布局、Web E2E 回归或线上 Web bug 复测/提 issue 时使用。
---
# UniDesk WebDev
本技能是 UniDesk/HWLAB Web 开发、浏览器复测和 Playwright 回归的唯一操作面。需求真相源仍是 UniDesk OA 规格,例如 `project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md``PJ2026-01060505-workbench-performance.md`;本技能只规定如何开发、采集、复现和验收。
## P0 防分叉原则
- 单一权威入口:先按 issue/CLI 明确的 node + lane 解析 workspace、web-probe origin、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML;不得把 D601、G14、v0.2、v0.3、旧端口、ClusterIP 或本地 dev server 写成隐藏默认。
- 防分叉原则:Web/Workbench 修复必须先确定唯一 authority、唯一 API 契约和唯一状态投影入口;不得写成“当字段缺失/请求失败/状态不符/超时/刷新后,再改走另一个 endpoint、旧 workspace/conversation 模型、localStorage、trace polling、result polling、GET read-through repair、内部 manager、legacy route 或测试专用后门”。字段缺失、状态滞后或投影不完整时,修 schema、projector、read model 或正式 mutation 源头;代码里只能保留单一路径内的输入规范化、校验和错误暴露,不能用 `A ?? B``if missing then fallback`、兼容分支或双写双读把缺口遮住。
- 禁止状态竞争行为:不得在代码、API、reducer、read model、fake-server 或测试中保留两个事实源,再用“覆盖、压过、override、优先级、优先采用、以 A 为准否则 B、A 赢过 B”等仲裁规则决定展示状态;这就是条件分叉和竞争投影。必须先定义唯一投影对象、唯一归属字段和唯一生成时机,让 UI/API 只消费该投影对象的单一状态;其他原始字段只能作为审计输入、诊断字段或待收敛数据,不能参与展示状态仲裁。
- 严禁读侧推理:REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 和测试断言都不能从 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot 或 localStorage 推断 turn/session/message 的 lifecycle、terminal、final response 或 running 状态。读侧只能读取唯一投影对象已经写好的字段;字段缺失、投影滞后或多字段矛盾时必须暴露 diagnostic/blocker 并修 projection writer/finalizer/schema,不得用 `result?.status ?? trace?.status`、最后一条 event `completed`、message fallback、session fallback、elapsed timeout 或 UI heuristic 补造事实。
- Sealed final response 不可被读侧诊断覆盖:assistant final response 一旦由唯一 durable projection 写入 terminal/sealed 字段,主消息正文、finalResponse、message status 和 turn terminal 就是 sealed 用户结果;turn polling、trace hydration、SSE gap、realtime timeout、transport close、compat wrapper error 或 read model lag 只能进入 trace detail、transport diagnostics、projection diagnostics、session health 或消息详情入口,不能替换主 timeline 正文。
- 无多路径、无 fallback:这是本技能的核心原则。Web、CLI、fake-server 和线上 web-probe 必须复用同一套 API 契约、同一 route/session/conversation/trace authority 和同一状态投影;不得为了让测试通过增加第二套 dispatcher、localStorage 真相、workspace snapshot 真相、内部 manager 调用、旧 lane 端口、raw request、临时兼容路径或“测试专用后门”。
- 严禁事后 repair / 0repairWeb、Workbench、fake-server 和 web-probe 不得通过 reload、切换 session、realign、`sessionRepair`、workspace selection repair、active tab repair、GET read-through、SSE gap repair、localStorage truth 或测试 helper 自动点击,把已经分裂的 route/session/message/trace 状态补成看起来正确。合法页面必须在首次进入、刷新、deep link、session 切换和 SSE 重连时按同一权威自然收敛;不一致必须暴露为 blocker/diagnostic,并修 schema、projector、read model、reducer 或 route authority 源头。允许 session-bound hydrate/gap-fill 只写目标 session 自己的 cache/read model,不得改变 active selection、URL 或当前消息区。
- Workbench GET 纯读投影:`GET /v1/workbench/*` 只能读取已持久化的 Workbench 投影事实并组装摘要;不得在 GET 中调用 AgentRun、Code Agent 或其他上游执行面做同步、补事件、修 trace、finalize terminal、改 running/completed 或写 message/session。投影滞后必须显式暴露 `projectionStatus``lastProjectedSeq``sourceRunId``blocker` 或等价诊断字段,并由后台 projector/finalizer 或显式受控 mutation 处理;刷新页面、切换 session 或打开详情不能成为事实推进入口。
- Workbench session lifecycle:空消息/无活动 session 的回收只能由后端 lifecycle GC、后台 finalizer 或显式受控 mutation 完成,TTL 和开关从选中 node/lane 的受控配置进入运行面;Web、fake-server 和 web-probe 不得靠隐藏 tab、前端 DELETE timer、GET read-through 或 deep link repair 清理。归档/删除后的 session deep link 必须观察同一 authority 的 404/archived diagnostic,不能补建或复活 session。
- Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutationWeb reducer/selectors、route hydrate、GET/list/detail/messages/SSE consumer、fake-server 和测试 helper 都不能由读侧失败、404、空列表、网络瞬断或 late response 触发这些 mutation。只有用户显式 mutation 成功,或后端 canonical lifecycle projection,能改变 session lifecycle。需求真相见 UniDesk OA `PJ2026-010401 Web工作台` 的“破坏性投影权”。
- fake-server 不是第二后端:它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*``/v1/*``/health*` 请求应失败并暴露 path;不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。
- 真实数据优先:fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界,并标明 `derivedFrom``syntheticReason`
- 原入口闭环:fake-server Playwright 用例负责可重复红灯和源码回归;线上 `web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。
- Master server 禁重型验证:不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。
- 禁止裸写 PlaywrightUniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`;不得直接写 `trans <route> playwright` heredoc、`playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有非 HWLAB 外站截图/PDF 等 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。
## 工作流
1. 定位目标:确认 repo、node、lane、workspace、public origin 和目标 SPEC。HWLAB D601 v0.3 例子是 `/home/ubuntu/workspace/hwlab-v03` + `https://hwlab.pikapython.com`,但只能在 issue/CLI 指向该目标时使用。
2. 读规则:进入目标 workspace 前读取目标 `AGENTS.md`;涉及规格或测试设计时读取 UniDesk OA 对应 SPEC。
3. 线上复现:短动作用 `web-probe run|script`,长程 Workbench/session 观测用 `web-probe observe start|command|status|stop|analyze`;保存 `scriptSha256``runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id。
4. 建红灯:修 Workbench/Performance 用户可见 bug 前,在目标 HWLAB workspace 的 `web/hwlab-cloud-web` fake-server Playwright 套件中补确定性用例;fixture 从真实采集样本脱敏产生。
5. 修源码:保持状态读写单一路径。状态投影类修复优先收敛 server-state/reducer/projection,不在 UI 组件、trace polling、result polling 或 localStorage 中新增竞争事实。
6. 验证:先跑 fake-server Playwright 目标用例,再回到同一 node/lane public origin 用 web-probe 复测;多轮任务必须用同一个 observer/session 采样到终态。截图、report hash 和 analyze finding 作为证据回传,不进入源码仓库。
7. 写 issue/closeout:问题登记和收口必须写明目标合并分支、node/lane、fixture 来源、Playwright fake-server 用例、线上 web-probe 命令/脚本 SHA、截图 SHA 和剩余边界。
## HWLAB Web Probe
线上 Cloud Web DOM 验收优先使用受控入口:
```bash
bun scripts/cli.ts web-probe run --node D601 --lane v03
```
web-probe 入口分三类:
- `run`repo-owned 标准 DOM probe,适合固定 P4 验收和已有脚本。
- `script`:受控 Playwright 托管脚本,适合一轮 55 秒内完成的 DOM/API 断言、截图、route/intercept 和边界采样。
- `observe`:纯客户端长程观测,适合同一 Workbench session 多轮任务、realtime/projection 问题、长时间 trace/DOM/network 采样和无副作用报告生成。长程 Workbench 观测默认同时打开两个浏览器页面:control 页面只执行显式 `observe command` 用户动作,observer 页面只打开同一个 session 做被动观察,并默认每 3 分钟整页刷新一次同一 session,模拟用户离开后返回,用来抓多用户/多页面下同一 session 的投影差异、历史 trace 丢失、耗时跳变和 loading 差异。
工测优先级:Workbench trace 乱序、完成行位置、耗时不一致、时间跳变、final response flicker、session 不刷新、性能慢路径和多轮可靠性问题,默认先用 `observe` 采样并跑 `observe analyze`;不要把临时 Playwright spec、裸 DOM 调试脚本或单次 `script` 结果当作主要工测证据。`script/run` 只作为短路径断言、截图/API 摘要或 observe/analyze 后的补充复核。
需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright
```bash
bun scripts/cli.ts web-probe script --node D601 --lane v03 <<'JS'
export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => {
await gotoStable('/workbench');
const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5');
const firstSession = sessionsResponse.body?.sessions?.[0] ?? sessionsResponse.body?.items?.[0] ?? null;
const sessionId = firstSession?.sessionId ?? firstSession?.id ?? null;
const apiMatrix = await fetchApiMatrix([
'/v1/workbench/sessions?limit=5',
...(sessionId ? [
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`,
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages`
] : []),
'/auth/session'
]);
recordStep('workbench-ready', { sessionsOk: sessionsResponse.ok, sessionId, apiMatrixOk: apiMatrix.ok });
return {
sessionsOk: sessionsResponse.ok,
sessionCount: sessionsResponse.body?.sessions?.length ?? sessionsResponse.body?.items?.length ?? null,
sessionId,
screenshot: await screenshot('workbench.png')
};
};
JS
```
长程 Workbench 观测使用 `observe` 命令组:
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type newSession
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectProvider --provider codex-api
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type cancel
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'
long prompt
EOF
bun scripts/cli.ts web-probe observe status webobs-xxxx --tail-lines 6
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --command-id cmd-xxxx
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42
bun scripts/cli.ts web-probe observe stop webobs-xxxx
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image build --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run
bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .state/web-probe-sentinel-smoke --scheduler-disabled --once
```
`observe analyze` 的 duplicate final response 判定必须以 trace-frame 可见行事实为准。`observe collect --view trace-frame` 固定渲染的 `Final Response` 区块是 summary,不是第二条业务 assistant message;只有同一 trace-frame 中出现两个可见 assistant final rows 且内容重复时,才应报告 duplicate finding,并在证据中写明 `finalResponseSummaryBlockCounted=false`
`observe collect --view timeline` 用于 issue closeout 前的 artifact drill-down:它只读取现有 `control.jsonl``samples.jsonl``commands/{pending,processing,done,failed,abandoned}/*.json`,默认输出 bounded timeline、关键 metadata、脱敏 disclosure 和下一步命令。按 `--command-id``--turn``--trace-id``--sample-seq``--timestamp``--window-ms` 缩小窗口;需要完整原始内容时必须显式使用 `--raw``--view files --file ...`
项目管理 / MDTODO 页面同样优先使用 `observe`,不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`,同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type gotoProjectMdtodo
bun scripts/cli.ts web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/
bun scripts/cli.ts web-probe observe command webobs-xxxx --type probeMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type reindexMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed
bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
```
`observe command --type newSession` 是显式用户/control action:它通过当前 Workbench UI 点击 `#session-create` 创建新 session,等待 active session id 改变和 composer ready,并把前后 snapshot 写入 control log。它只能用于采样开始时建立目标 session,或作为用户明确的新建会话动作;不得在 route/session mismatch 后当作 repair 手段。
约束:
- `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`
- `web-probe run|script|observe start` 的默认 URL、browser proxy mode、observe/analyze 报警阈值和 project-management 采样/命令 allowlist 必须来自 `config/hwlab-node-lanes.yaml``webProbe`;需要排除公网/FRP/跨国 proxy 抖动时,在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin,不要在命令行长期手写 `--url` 或裸 Playwright。
- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 commandobserver 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole``pageId``sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession``selectProvider``sendPrompt``steer``cancel``goto``screenshot``mark``stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin``steer --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。
- `observe command --type steer``--type cancel` 是显式用户/control actionsteer 复用当前 Workbench composer 的运行中 turn 引导路径,cancel 复用同一 composer 主按钮的取消路径。二者必须进入 `control.jsonl`,不能用后端私有 API、AgentRun direct cancel 或测试后门替代。`configureMdtodoHwpodSource``probeMdtodoSource``reindexMdtodoSource``selectMdtodoSource``selectMdtodoFile``selectMdtodoTask``editMdtodoTaskTitle``editMdtodoTaskBody``toggleMdtodoTaskStatus``addMdtodoSubTask``continueMdtodoTask``deleteMdtodoTask``launchWorkbenchFromMdtodo` 也是显式用户/control action,只能使用页面公开 `data-*` id、正式按钮和 YAML 允许的自然 API;它们通过 public source/file/task id 与 Workbench 关联,不能读取内部 store、私有后端或把 MDTODO 页面包含进 Workbench。
- `observe collect --view turn-summary` 是第一层 CLI 阅读视图:只从 `samples.jsonl``control.jsonl` 和已有 `analysis/report.json` 按需渲染同一 session 的多 turn 摘要,包含用户消息 preview/hash、traceId、状态、耗时/最近更新时间、steer/cancel 标记和 Final Response 摘要。`observe collect --view trace-frame --trace-id <id> --sample-seq <n>` 是第二层 CLI 阅读视图:从同一采样帧渲染单帧 trace 文字截图,并固定输出 `Final Response` 区块。`observe collect --view project-summary|project-mdtodo-summary` 从同一 artifact 渲染项目管理 / MDTODO DOM 采样、Source/File/Task 计数、command/mutation 结果、Workbench launch、捕获到的 `x-hwlab-otel-trace-id` 和 OTel/Tempo drill-down 线索;project collect 的远端 payload 必须保持 bounded compact rows,由本地 renderer 生成表格,避免 `trans` stdout 截断后 JSON parse 失败。collect 视图不是采样器新增保存物,不构成第二事实源。
- `observe start/status/command/collect/analyze` 默认输出包含 `Wrapper contract` 区块;该区块证明 Web 哨兵只能 wrap 现有 observe CLI verb、现有 runner/analyzer 和既有 artifact contract,不新增第二套 Playwright runner、analyzer、状态机或私有 web-probe API。
- `web-probe sentinel plan|status` 只读取 `observability.webProbe.sentinel.enabled/configRefs` 和 owning YAML,渲染 redacted 配置引用图、文件 hash、缺失字段和跨 ref 冲突;`web-probe sentinel image|control-plane` 继续从 owning YAML 渲染 image、GitOps、Argo 和 manifest 计划,并在远端 publish job 接通前拒绝报告部署 mutation。它不启动浏览器、不读取 Secret 值、不保存采样结果,也不是第二套 runner/analyzer。真正的采样和判定仍以 `observe start|command|collect|analyze` artifacts 为准。
- Web 哨兵 public dashboard/origin 必须以 issue/SPEC/YAML 既定计划为准;当前 P6 计划沿用 `monitor.pikapython.com`,不要未经明确变更改成 `hwlab-monitor.pikapython.com` 或其他新域名。验证 report 时记录 `publicOrigin`,但不要把域名硬编码到 runner/analyzer 逻辑里。
- `scripts/web-probe-sentinel-service.ts` 是 Web 哨兵 Pod entrypoint`--once` 只做 config/PVC/SQLite/scheduler/analyzer-command health 快照,`--scheduler-disabled` 仅用于本地服务健康冒烟,不能作为生产运行参数。HTTP 服务只提供 `/api/health``/api/status``/api/runs``/api/maintenance``/metrics` 和 redacted dashboard 外壳,底层采样仍只能经 observe CLI adapter。
- `trace-frame` 出现 `(无 trace rows;这是 blocker...)` 时,必须先看同一输出中的 `TRACE DIAGNOSTIC`:记录 pageRole/pageId、traceRows/turns/messages 数量、sampleTraceIds、尾部 traceRow/turn/message 归属。若目标 trace 的 turn/message/final 存在但 traceRows 全部属于旧 trace,应按 Workbench read model authority 分裂登记到架构/业务 issue(例:HWLAB #2124),不得把旧 traceRows 当作新 turn 通过证据,也不得让 analyzer 的聚合计数压过 CLI trace 视图。
- analyzer finding 不得压过 CLI `trace-frame` 人工视图。尤其 `trace-assistant-message-duplicates-final-response` 只有在 `trace-frame` 中同一 completed turn 可见多条相同 assistant final rows 时才按业务 bug 处理;如果 `trace-frame` 只有一条 assistant final row、后面固定 `Final Response` 区块正确且 API messages/turns 对齐,该 amber 归类为 analyzer 精度问题,应登记/修工具,不得阻止业务 closeout。
-`observe status` 显示 PID still alive 但 heartbeat/sample 不推进、`commands/pending/*.json` 不被消费,或 `observe stop --force` 只是继续排队 stop command,应先按 web-probe runner 工具缺陷处理(例:UniDesk #874),用 route 只读确认 PID/heartbeat 后清理进程;不要把 pending command、未触发的 cancel 或 runner stale 混入 Workbench 业务结论。
- `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要;不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。
- 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次;每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal,应同时登记“执行终态”和“Workbench 投影未收敛”,不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。
- `observe command --type sendPrompt` 是普通新 turn 路径,composer 主按钮应处于 `data-action=turn`;如果仍是 `steer``cancel`,必须作为工具/页面状态不一致失败暴露,不能点击后再等待错误接口。空输入状态下 submit disabled 可以为 true,不能把 disabled 当作新 turn 不可用的唯一依据;应先看 input present/enabled、warning absent 和 action=turn,再由正式填入 prompt 后提交。
- `observe analyze` 是离线分析,只读取 artifact JSONL 并写 `analysis/report.md``analysis/report.json`,不访问 Workbench API、不驱动浏览器。`observe start` 每次启动必须先把同一 stateDir 中已有的根目录 JSONL 轮转到带时间戳的 `archive/` 文件;`observe analyze` 默认只分析当前根目录 JSONL,不扫描历史 archive,只有显式指定 archive prefix 时才分析历史轮转窗口。报告必须输出采样点 vs 每个 turn 的总耗时/最近更新时间表、trace row 视觉顺序异常、terminal/轮次完成 row 是否最后、Code Agent 卡片耗时与 trace/轮次完成总耗时一致性、可见“加载中”的数量/归属/并发 owner/连续出现区间、DOM diagnostic/HTTP/console/requestfailed/runtime execution error 分组、page asset provenance segment、同源 API Resource Timing 分位表和超过 YAML `webProbe.alertThresholds` budget 的慢路径 finding;项目管理页还必须输出 DOM readiness、source/file/task 计数、缺失 public task ref、Workbench launch success/failure、captured OTel trace header、自然 project-management API 分组和超过 YAML `webProbe.projectManagement.slowApiBudgetMs` 的慢路径 finding。页面/API 加载、可见“加载中”、长连接打开耗时、turn timing 跳变、trace row 顺序、卡片耗时/轮次完成耗时一致性、session fallback 标题比例和项目管理 API 慢路径阈值只能改 YAML,不能在 analyzer/renderer 中写死。修复必须降低真实请求、投影、渲染或后端路径耗时,禁止为了减少“加载中”出现时间而提前展示未加载完的内容,也不能靠下游 retry/reload/fallback 掩盖。报告里的 `trace-row-order-nonmonotonic``trace-completion-row-not-last``round-completion-elapsed-mismatch``code-agent-card-duration-underreported``final-response-flicker``uncommanded-visible-state-change``mdtodo-workbench-launch-otel-trace-missing``project-management-api-slow`、session changed、network 503 等 finding 是排障线索;用于 closeout 时必须结合原始 session/trace/DOM 证据解释,避免把采样噪声直接当作业务结论。
- `sentinel validate --quick-verify` 超过 120s 是严重超时,必须保持 warning/red 并优先从 envreuse、git mirror、warm runner 复用、first tool execution、Workbench 投影和 observe/analyze 开销排查;不要通过提高 budget、减少轮数、放宽 analyzer 或绕过 CLI trace 视图来让场景变绿。quick-verify 等待每轮终态时应读取既有 sampler artifacts 和 bounded collect 视图,不能反复启动完整 collect 或新增第二份“trace 截图”保存来源。
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内;能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内,并减少无界 selector/network 等待。确需等待更久时,改用 `web-probe run` 的异步 job/status 语义,或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath``reportSha256`、screenshots 或 DOM steps,先按远端报告判断脚本/页面实际状态;最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence``summary.issueEvidence`;只有需要展开调查时才粘贴 `probe.script.result``probe.steps` 或完整 `reportPath`,避免 stdout、summary 和 report 多层重复同一证据。
- stdin heredoc 与 `--script-file` 都按 ES module 加载,脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`;不要在模块顶层直接写 `return`。失败为 `Illegal return statement``does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时,先按 probe 脚本入口误用处理,不要归因成 Cloud Web 行为失败。
- 自定义脚本需要主动失败时,优先返回 `{ ok: false, failedCondition: "..." }` 或抛出错误;兼容返回 `{ pass: false }``{ success: false }`,且 `{ ok: true, pass: false }` 仍按失败处理。`failedCondition``errorMessage``message``error``reason``summary` 会进入失败摘要;不要只把失败埋在普通业务字段里。
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
- 需要禁用 `EventSource`、mock `Date`/clock、注入 preload hook 或修改浏览器启动前全局对象时,`page.addInitScript()` 必须在目标页面整页加载前注册,并用 `page.goto(new URL(path, origin).toString(), { waitUntil: "domcontentloaded" })` 进入目标 deep link;不要依赖 `gotoStable()` 复用当前 SPA 页面后再期待 init script 生效。若只在已加载页面上 route/block 请求,既有 SSE 或 store 状态可能继续更新,不能作为“无后端刷新依赖”的验收。
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`;不要拼出 `//v1/...`
- 先通过 `gotoStable``waitWorkbenchReady` 或等价导航进入目标 origin,再用 `fetchJson` 读同源 API;不要在 `about:blank` 上请求 `/v1/...``fetchJson` 返回包装对象 `{ ok, status, body, failureKind, ... }`,业务字段必须从 `.body` 读取,并同时记录 `ok/status/failureKind`;如果返回 `status=0` 且页面还不是目标 origin,先按 probe 脚本误用处理,不要归因成 Cloud API 或 Workbench 投影失败。
- Workbench 有 SSE/长轮询时不要用 `networkidle` 判断通过;用明确 DOM/API 条件,如 final URL、route sessionId、active tab、message card、trace row 或 durable session detail/messages。
- 当前 HWLAB v0.3 Workbench durable read model 的验收端点是 `/v1/workbench/sessions``/v1/workbench/sessions/{sessionId}``/v1/workbench/sessions/{sessionId}/messages``/v1/workbench/turns/{traceId}``/v1/workbench/traces/{traceId}/events`;提交 mutation 可用正式 `/v1/agent/chat` 产生真实 turn。`/v1/workbench/workspace` 只在当前 API 契约声明为选择/config 快照时采集,不作为消息或 trace authority`/v1/agent/conversations` 属于旧模型,不得作为 v0.3 Workbench durable projection 的通过条件。
- 需要稳定 running message、timing、SSE 或 projection 样本时,优先通过正式同源 mutation 创建样本:`POST /v1/agent/sessions` 建 session,再按前端同一路径提交 `/v1/agent/chat`,轮询 `/v1/workbench/sessions/{sessionId}/messages` 等待 durable projection。不要把历史 session 是否仍 running 当作验收前提,也不要用旧 conversation/workspace 端点造样本。验收结束后能取消的 turn 必须调用 `/v1/agent/chat/cancel` 清理,并在 issue evidence 只记录 trace/session、status、script/report SHA 和必要摘要,避免粘贴完整 provider payload。
- HWLAB v0.3 `fa2ad4845a111e0f9e86d473eb4204ed1b2b0a3c` 后,repo-owned `scripts/web-live-dom-probe.mjs` 的默认 `web-probe run --fresh-session` 已使用 durable Workbench session 端点,不应再依赖 `/v1/workbench/workspace``/v1/agent/conversations` 作为消息/trace authority。若 `session-not-selected` 复现,先确认目标 workspace 已包含该提交,再对 helper 做旧端点扫描;只有扫描仍命中旧端点时才按工具链回归处理。
- `web-probe run --fresh-session` 空消息路径的基线证据是 D601 v0.3 job `web-live-dom-probe-1781842424721-44066``freshSessionAligned=true``messageCount=0`、report SHA `d34c6765943e14bbd639b91bfc17139d582f670ed5cf409a957acd40fae7871f`、screenshot SHA `8d341b7c30293baa9be8f1e63e147ea0fdafbdbcfc43f0b05e4fb9f316ef996c`
- 若带 `--message` 的 fresh-session 路径已经达到 `freshSessionAligned=true``promptSubmitted=true``messageCount>0` 和 session API 200,但随后报 `trace-id-missing``Workbench turn is not visible to the current actor` 或 trace 面板同类不可见文案,应登记为 Workbench turn/trace actor visibility 问题,不再归因到 fresh-session 选择 helper 或旧端点。
- Playwright `page.evaluate` 只能传一个可序列化参数;多个值包成对象,或用 `safeEvaluate(fn, { a, b })`
- `safeEvaluate` 返回结构化包装对象;脚本断言前必须解包,例如 `const evaluated = await safeEvaluate(...); const dom = evaluated?.value ?? evaluated;``recordStep` 可以记录包装对象用于诊断,但行为断言不要直接读包装对象的业务字段,避免把已通过的 DOM 误判成 `undefined`
- 脚本用 `page.evaluate` 模拟滚动、resize、click 或派发 DOM event 后,若断言 Vue/React 绑定出的 `data-*`、ARIA、按钮状态或 scroll-follow 状态,必须先等待一次浏览器/框架更新,例如 `await wait(300)``page.waitForFunction(...)` 或下一次明确 DOM 条件;不要在同一个 evaluate 内写完事件就立刻读取状态,否则会把尚未 flush 的响应式状态误判为业务失败。验证用户滚动暂停/恢复时优先记录操作前后 scroll metrics 和 `data-following` 的延迟稳定值。
- `web-probe script` 顶层/default export 运行在 Node 上下文,浏览器全局 `window``document``location``CSS` 只在 `safeEvaluate`/`page.evaluate` 回调内可用。Node 侧 deep link 用 `const origin = new URL(page.url()).origin; await page.goto(new URL('/workbench/sessions/' + encodeURIComponent(id), origin).toString())`,或直接用 `gotoStable('/workbench/sessions/<id>')`;不要在 Node 侧写 `location.origin``CSS.escape(...)`,也不要用没有 baseURL 的 `page.goto('/relative/path')`。若失败为 `location is not defined``CSS is not defined` 或相对 URL invalid,先按脚本误用处理;工具提示改进跟踪见 [pikasTech/unidesk#479](https://github.com/pikasTech/unidesk/issues/479)。
- 脚本中用 `recordStep(name, data)` 保存关键 DOM/API partial evidenceAPI 批量探测优先用 `fetchApiMatrix(paths)`,单个 API 失败不应让后续证据丢失。
- deleted/archived/not-found session deep link 验收用 `web-probe script` 直接 `gotoStable('/workbench/sessions/<id>')`,再用同源 `fetchJson('/v1/workbench/sessions/<id>')`、messages 和 list API 断言 404/不在列表;不要调用要求 active session selection、composer readiness 或 workspace repair 的 helper 作为通过条件。
- 验证 Cloud Web runtime config 或 HTML 注入时,优先加 cache-busting query 和 `cache-control: no-cache`,并按层拆开判断:Deployment env、Pod 内 `127.0.0.1` HTML、公网原始 HTML、浏览器 DOM。不要只凭一次 web-probe DOM 缺字段就判定 rollout 失败;先确认是运行面未渲染、Pod 未滚动、边缘缓存,还是脚本读取层级错误。
- web-probe 只能观察、截图和断言,不得用 `sessionRepair``realignFreshSession`、自动点击 session、reload 循环或 workspace selection repair 作为通过条件。发现 `routeSessionId``activeSessionId``activeConversationId`、message session 或 trace session 不一致时,必须记录 mismatch 并失败;截图和 API 摘要是证据,不是修复动作。
- 失败证据至少保留 `failureKind``errorMessage``scriptSha256``runDir``lastUrl``lastScreenshot``probe.summary``reportPath`/`reportSha256`;默认失败截图是 `failure.png`
## Fake-Server Playwright
Workbench 回归入口在目标 HWLAB repo 的 `web/hwlab-cloud-web`
```bash
bun run e2e:workbench -- --project=chromium
```
运行面要求:
- 在 issue/CLI 选中的 node/lane workspace 或其独立 worktree 上运行,不在 master server 本地运行。
- 同源 fake-server 只提供正式 API 契约,覆盖 `/auth/*`、session list/detail/messages、turn snapshot、trace event page、Workbench event stream、Performance summary 等当前用例所需端点。
- 测试断言优先使用用户可见文本、ARIA role 和稳定 DOM 标识;不要通过内部 store、localStorage、数据库或 live API 直接判定通过。
- 非终态瞬时不变量必须用立即 DOM 快照断言:例如 running/pending 阶段要求 final response 为空时,先定位当前 agent card,再用 `locator.evaluate` / `page.evaluate` 直接读取 `.message-markdown.message-text` 数组并立刻判断;不要用会自动等待的 `expect(locator).toHaveCount(0)``not.toContainText` 或类似负向 locator 断言,否则测试可能等到 terminal DOM 替换后误通过,掩盖 running 阶段污染。
- 失败必须保留截图、Playwright trace 或等价 artifact;关键路径可生成命名截图。
Workbench 至少覆盖:
- session 切换后主工作区 loading 和目标 conversation 恢复;
- 刷新页面后以同一 message/turn/trace 标识还原 timeline
- SSE 断线、丢事件或重连后通过 REST snapshot 与 trace page 补洞;
- session tab、workspace card、message card、composer 主按钮、Trace detail 的 `running`/`completed`/`failed`/`canceled` 一致性;
- completed assistant 已 sealed 后,turn snapshot、trace event page、SSE 或 realtime diagnostic 失败不覆盖主消息正文,诊断只进入详情/感叹号/transport health
- Trace 阅读按事件顺序渲染可读 row,正确处理分页、终态、失败、自动展开和终态折叠;
- deep link 与普通点击 session 走同一 authority path
- Performance 页的摘要、错误态、刷新、移动端表格可达性和低基数脱敏展示。
## Fixture 采集与脱敏
- 采集范围至少包括 session list/detail/messages、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summaryworkspace selection/config 只有在当前 API 契约仍声明该读模型时才采集。
- 产物必须记录 `capturedFrom``capturedAt`、schema/redaction 版本和 `valuesPrinted=false`
- 使用稳定伪 ID 映射保留 conversation、session、thread、turn、trace、message、sourceSeq 的关系。
- 删除或替换 `HWLAB_API_KEY`、cookie、Authorization header、DB DSN、provider token、真实用户身份、非公开 prompt、stdout/stderr 大段原文和完整 provider payload。
- 真实样本难以覆盖的延迟响应、SSE 断线、分页缺口、列表缺少当前选中项、可选字段异常、空集合和特定 HTTP 错误可以合成,但必须说明来源和原因。
## Workbench 判定口径
- Workbench API 的 GET 路径不是修复入口。若 session/messages/turn/trace 出现状态不一致,应登记为投影唯一性或投影延迟问题,修 backend projector/read model;不得用前端轮询、session 切换、reload、read-through sync 或 GET 内部修复把问题掩盖。
- Workbench 读侧不得推理生命周期。`turn.status``message.status``session.running``trace terminal``finalResponse``projectionStatus` 只能来自唯一 durable projectiontrace event row 的 `completed` 只表示该 event/tool row 完成,不能终结 turn;message 文本为空只表示无可展示正文,不能生成占位 final responselist summary、session detail、turn snapshot、trace page 之间的差异只能作为 diagnostic,不得用优先级函数在读侧“选一个看起来合理的状态”。
- Workbench sealed final response 必须与读侧诊断分仓。`messageProjection` 承载 role/status/text/finalResponse/sealedAt`traceDetail` 承载 events/page hydration/trace diagnostics`sessionStatus` 承载 rail/card 运行摘要,`transportDiagnostics` 承载 SSE/poll/hydration timeout 等可见性诊断;等价结构可以使用不同命名,但主消息投影和 diagnostic 不能竞争同一字段。`showMessageText()`、message diagnostic selector 和组件模板不得让 diagnostic 压过 sealed final response。
- Workbench trace 视觉顺序的唯一权威是 durable projection 分配的 `projectedSeq``projectedSeq` 必须由持久化 runtime store 在 trace 内全局递增分配,并按 `traceId + sourceEventId` 幂等复用;本地 `event.seq``sourceSeq`、事件到达顺序、时间戳、DOM 顺序和 renderer 输入顺序只能作为审计输入或源游标,不能直接派生视觉位置或参与仲裁。重复源事件不能分配新 `projectedSeq`;不同源事件即使局部 `event.seq` 重置为 1 也必须分配新的全局 `projectedSeq`,这个幂等性需要有最小单元测试覆盖。
- Workbench trace 时间字段的 source truth 是后端和 trace event page 暴露的 ISO/UTC 时间戳;后端 projector、durable read model 和共享 renderer 默认行为不得改成本地时区字符串。Web 展示北京时间或其他用户时区时,只能在 Vue/Web 渲染层从 runtime `displayTime` 注入 formatterCLI、CaseRun 和原始 trace API 仍显示/返回 UTC,不通过多字段仲裁、默认 fallback 或后端补写时区字符串修显示。
- Trace event page 检测到同一 trace 内历史 `projectedSeq` collision 时必须暴露 `projectionStatus=blocked``projectionHealth=degraded` 和稳定 blocker code,不能把损坏序列交给 CLI/Web renderer 继续展示。CLI/Web trace renderer 只能按 projection 已给定的位置一次渲染;不得通过 assistant 文本相等/包含、final response 匹配、尾部排序、terminal row 合并或 completion row 追加来事后修正视觉顺序。
- Workbench durable projection 验收必须覆盖同一 session/trace 的 list、detail、messages、turn、trace events 和 DOM session tab/message card;尤其要用 fresh browser 或 runtime 重启后的历史 terminal 样本证明 read model 能从 durable trace events 恢复终态。若内存 trace 仍显示 running 而 durable trace 已 terminal,应修唯一 read model 的投影对象,不得在 GET handler、前端 reducer 或 probe 脚本里做临时状态仲裁。
- HWLAB #1585/#1596/#1690 是 Workbench 投影禁令的固定判例:AgentRun 已 terminal 但 Workbench 停在旧 seq 时,不得用 GET/read-side 补 resultturn/card completed 但 TraceEventPage seq/range 不合法时,不得用 completed 状态压过 trace 不可读事实;Trace timeline 乱序或 terminal row 重排时,不得让 `sourceSeq`、局部 `event.seq`、输入顺序和 renderer 文本匹配多来源仲裁。合法修复是后台 projector/resumer 补 durable projection,在同一 trace event 快照内修正分页/顺序契约,或暴露 historical projection blocker;不得推断 lifecycle、补写 GET、切 session/reload 修页面、添加多来源优先级或在 renderer 做事后 repair。
- Session rail 会话集合只消费 `/v1/workbench/sessions`;当前 session detail 只消费 `/v1/workbench/sessions/{sessionId}`;消息本体只消费 `/v1/workbench/sessions/{sessionId}/messages`turn 状态只消费 `/v1/workbench/turns/{traceId}`trace 阅读只消费 `/v1/workbench/traces/{traceId}/events` 或正式 trace event page。不要让 workspace snapshot、列表摘要、localStorage、trace polling 或 result polling 互相覆盖。
- 空消息 session 回收验收必须同时证明:未超过 TTL 的空 session 保留,有消息、running/admitting、terminal result 或 active trace 的 session 不被回收;超过 TTL 的空 idle session 由后端 GC 归档/删除后,rail/list/detail/messages/deep link 都按同一 canonical lifecycle 状态表现。
- UI transient state 只保存 route、显式选择、composer draft、scroll、展开状态和临时交互状态;服务端事实进入 server-state/reducer/projection。
- running turn 必须同时反映在主任务区、composer 主按钮、session tab 和可取消入口;清空、切换、刷新不得把 running trace 隐藏成空态。
- 显式新建或选择 session 是 active selection 的 mutation authority`POST /v1/agent/sessions` 返回 201 或用户点击目标 session 后,URL、active tab、当前消息区和 composer 必须立即归属目标 session。目标 session detail/messages 慢只能表现为该目标 session 的 loading/empty 态;旧 session 的 list refresh、detail、turn、trace、SSE 迟到事件只能更新旧 session 自己的 cache/status,不得继续占据或夺回 active route、message area、composer/cancel 状态。
- 读侧没有破坏性投影权。`GET /v1/workbench/sessions`、session detail、messages、SSE、route hydrate 或 refreshSessions 的失败、404、空列表、网络错误和迟到响应,只能显示 loading/degraded/unknown/blocker 或保留上一份成功投影;不得 `forgetSession()``replaceActiveSessionSelection(null)`、清空 messages/tabs,或把 composer 改成 `session_required`。deleted/archived/not-found 必须来自后端 canonical lifecycle projection,或来自用户显式 DELETE mutation 成功后的展示更新。
- terminal failure、blocked、canceled 都是最终结果;若 API 返回可读 error/blockerfinal response 展示区必须直接显示,不能只放在 trace rows 或详情面板中。
- completed、failed、canceled、blocked 的 sealed final result 一旦显示,后续 polling/hydration/SSE/realtime 的 timeout、500、gap、close 和 lag 只能改变诊断区,不得把已显示 final response 闪回 `Code Agent 代理暂时无法连接上游``turn 超时无新活动``projection-resume:sync-failed` 或同类读侧错误态。
- session 切换验收必须覆盖点击态和持久化恢复态:选择目标 session、确认 route/session list/detail/messages 归属同一 `sessionId`、刷新后同一 session 仍 active。
- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/<sessionId>`,不能只用同页 reload。
- 0repair 验收必须用 fresh context 直达 session/deep link,或从当前 session 显式切换到目标 session;不得依赖测试 helper 选择、reload、切走再切回、`sessionRepair``realignFreshSession` 修正页面后再判通过。旧 session 的延迟 detail、turn、trace、SSE 或 refreshSessions 晚到,只能更新该 session cache/list,不能改变 active URL、active tab、current messages 或 composer。
- Workbench reducer/selectors/0repair 收敛类 closeout 必须做负向源码扫描并在 issue/PR 证据里写明结果;至少覆盖直接写服务端事实和旧 repair 名称,例如 `messages.value =``messages.value.push``sessions.value =``previousMessages``previousSessionId``restorePrevious``restoreMessagesTraceAuthority``repairWorkbenchMessagePage``findMessageTextFallback``projectionCatchup``PROJECTION_CATCHUP` 和组件侧 `workbench.messages`。这些扫描是辅助证据,不能替代 fake-server Playwright 与 public web-probe。
- Trace 实时性要用间隔采样,区分“加载中”和“思考中”;终态卡片不得继续保留“思考中”空态。
- 用户反馈“当前 session 不刷新、切走再切回才刷新”时,证据必须成对采集:直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图;同时记录 session list、session detail、messages、turn snapshot 和 trace event page 的状态。issue 正文要明确这是事件投影/重放问题,不能只登记成普通 deep link 或单接口 404。
## Performance 页判定口径
- `/performance` 只展示低基数、脱敏的 Workbench/RUM/API/long task 汇总;不得泄露 `conversationId``sessionId``traceId``runId`、prompt、assistant 正文、tool 参数、stdout/stderr、Secret 或用户身份。
- 单位以 API `unit` 为准;后端若以 seconds 存储,前端显示 ms 时必须只做一次换算。
- 移动端和窄屏断言要检查目标表格/按钮是否可见、可点击、layout rect 非零且可触达;不要只用 desktop selector readiness 代表所有 viewport。
- 刷新按钮应有明确 loading/error/empty 状态,重复点击不能制造 stuck loading、旧数据冒充新数据或未捕获 console/page error。
## UniDesk Frontend E2E
UniDesk 主前端交付门禁仍由 `bun scripts/cli.ts e2e run` 承担;本技能规定浏览器侧如何选取和解释验证。
- 先跑最小相关选择集,例如 `bun scripts/cli.ts e2e run --only frontend:pipeline`;目标检查通过后再跑完整 `bun scripts/cli.ts e2e run`。选择集是真执行,不是输出过滤。
- 浏览器必须打开 `config.json` / publicHost 派生的公开 frontend 或 dev frontend proxy;不要用 localhost、Docker internal URL 或后端 API 直调替代用户入口。
- 前端断言优先使用用户可见文本、ARIA role、稳定 DOM 标识和 React 控件状态;禁止用 raw JSON、内部 store、localStorage 或后端数据库绕过 UI。
- 高信息密度区域如 Pipeline 右侧栏、Trace timeline、详情抽屉、甘特图坐标、资源监控表和 Performance 面板,必须显式检查总高度、横向滚动条、关键控件可见可点、文本不重叠。
- 用户服务页必须等待真实后端数据,而不是只看页面骨架;Todo Note、OA Event Flow、Code Queue、FindJob、Pipeline、MET Nonlinear、ClaudeQQ 等页面的通过条件以当前 E2E check 名称和 issue scope 为准,不在文档里复制所有字段清单。
- raw JSON 只能在用户显式点击 `查看原始JSON` 后出现;默认页面必须把 JSON 渲染为表格、卡片、徽章、树、图或其他可读控件。
- 任何 frontend E2E 失败都应保留截图路径、resultPath、failedChecks 和关键 console/page error 摘要;不要把 missing browser/deps 当作 skip,按锁文件和目标执行面补依赖后继续。
## 受控 web-probe 代替裸 Playwright
UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时,统一选择 `web-probe run|script|observe`
`web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告:可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称;点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。
`trans <route> playwright``playwright-cli` 和临时 Node Playwright 脚本只允许作为非 HWLAB 外站短生命周期截图/PDF、或 web-probe 尚未覆盖且一次性不可复用的诊断例外。例外使用前必须写明为什么 `web-probe script``web-probe observe` 不适用;同类动作出现第二次就应补进 web-probe CLI 或 repo-owned probe。
需要把截图回传到本机时,优先用 `web-probe script``screenshot(name)`,或长程观测中使用 `observe command --type screenshot --label <label>`,并在 issue 中引用截图 SHA、report SHA、observer id 和 stateDir。不要把本地 Playwright artifact 当作 HWLAB node/lane 原入口验收替代品。
+19
View File
@@ -25,6 +25,7 @@ import { isGhContentRoute, runGhContentRoute } from "./src/gh-route";
import { runGitToolsCommand } from "./src/git-tools";
import { runCommanderCommand } from "./src/commander";
import { isHelpToken, rootHelp, serverHelp, sshHelp, staticNamespaceHelp } from "./src/help";
import { healthHelp, runHealthCommand } from "./src/health";
import { runServerCleanupCommand } from "./src/server-cleanup";
import { runGcCommand } from "./src/gc";
import { runPlatformDbCommand } from "./src/platform-db";
@@ -245,6 +246,11 @@ async function main(): Promise<void> {
return;
}
if (top === "health" && isHelpToken(sub)) {
emitJson(commandName, healthHelp());
return;
}
if (top === "server" && (isHelpToken(sub) || args.slice(2).some(isHelpToken))) {
emitJson(commandName, serverHelp(isHelpToken(sub) ? undefined : sub));
return;
@@ -415,6 +421,19 @@ async function main(): Promise<void> {
return;
}
if (top === "health") {
const result = runHealthCommand(args.slice(1));
const ok = (result as { ok?: unknown }).ok !== false;
if (isRenderedCliResult(result)) {
emitText(result.renderedText, result.command || commandName);
if (!ok) process.exitCode = 1;
return;
}
emitJson(commandName, result, ok);
if (!ok) process.exitCode = 1;
return;
}
const config = readConfig();
const autoRemoteCiPublishPlan = autoRemoteCiPublishUserServiceDryRunPlan(config, args);
if (autoRemoteCiPublishPlan.enabled && autoRemoteCiPublishPlan.host !== null) {
+1
View File
@@ -29,6 +29,7 @@ const syntaxFiles = [
"scripts/src/docker.ts",
"scripts/src/e2e.ts",
"scripts/src/help.ts",
"scripts/src/health.ts",
"scripts/src/commander.ts",
"scripts/src/platform-db.ts",
"scripts/src/recovery-guardrails.ts",
+243
View File
@@ -0,0 +1,243 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, join, relative } from "node:path";
import { repoRoot, rootPath } from "./config";
import type { RenderedCliResult } from "./output";
const DEFAULT_SKILLS_DIR = ".agents/skills";
const DEFAULT_SKILL_MD_LIMIT_BYTES = 10 * 1024;
interface HealthOptions {
skillsDir: string;
thresholdBytes: number;
json: boolean;
full: boolean;
}
interface SkillHealthItem {
skillId: string;
skillPath: string;
skillMdPath: string;
referencesDir: string;
bytes: number;
lines: number;
limitBytes: number;
overBytes: number;
status: "OK" | "ALERT";
action: string;
}
interface SkillHealthResult {
ok: boolean;
command: "health";
check: "skill-md-size";
thresholdBytes: number;
skillsDir: string;
summary: {
totalSkills: number;
okSkills: number;
alertSkills: number;
maxBytes: number;
maxSkillId: string | null;
};
alerts: SkillHealthItem[];
skills: SkillHealthItem[];
policy: {
limitBytes: number;
splitTarget: ".agents/skills/<skill_id>/references/*.md";
skillMdRole: string;
};
next: string[];
}
export function healthHelp(): Record<string, unknown> {
return {
command: "health",
output: "text by default; use --json, --full, or --raw for structured output",
usage: [
"bun scripts/cli.ts health",
"bun scripts/cli.ts health --json",
"bun scripts/cli.ts health --skills-dir .agents/skills --threshold-bytes 10240",
],
checks: [
{
name: "skill-md-size",
defaultThresholdBytes: DEFAULT_SKILL_MD_LIMIT_BYTES,
scope: ".agents/skills/*/SKILL.md",
failure: "SKILL.md over threshold",
action: "move lower-frequency detail into .agents/skills/<skill_id>/references/*.md and leave SKILL.md as high-frequency routing/workflow guidance plus reference links",
},
],
};
}
export function runHealthCommand(args: string[]): SkillHealthResult | RenderedCliResult {
const options = parseHealthOptions(args);
const result = scanSkillHealth(options);
if (options.json || options.full) return result;
return {
ok: result.ok,
command: "health",
contentType: "text/plain",
renderedText: renderSkillHealth(result),
};
}
function parseHealthOptions(args: string[]): HealthOptions {
const options: HealthOptions = {
skillsDir: DEFAULT_SKILLS_DIR,
thresholdBytes: DEFAULT_SKILL_MD_LIMIT_BYTES,
json: false,
full: false,
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--json") {
options.json = true;
} else if (arg === "--full" || arg === "--raw") {
options.full = true;
options.json = true;
} else if (arg === "--skills-dir") {
options.skillsDir = stringArg(arg, args[index + 1]);
index += 1;
} else if (arg === "--threshold-bytes") {
options.thresholdBytes = positiveIntArg(arg, args[index + 1], 1024 * 1024);
index += 1;
} else {
throw new Error(`unknown health option: ${arg}`);
}
}
return options;
}
function stringArg(name: string, raw: string | undefined): string {
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
return raw;
}
function positiveIntArg(name: string, raw: string | undefined, max: number): number {
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
return Math.min(value, max);
}
function resolvePath(path: string): string {
return isAbsolute(path) ? path : rootPath(path);
}
function displayPath(path: string): string {
const rel = relative(repoRoot, path);
return rel.length > 0 && !rel.startsWith("..") ? rel : path;
}
function scanSkillHealth(options: HealthOptions): SkillHealthResult {
const skillsRoot = resolvePath(options.skillsDir);
if (!existsSync(skillsRoot)) throw new Error(`health skills directory not found: ${displayPath(skillsRoot)}`);
const skillDirs = readdirSync(skillsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => join(skillsRoot, entry.name))
.filter((dir) => existsSync(join(dir, "SKILL.md")))
.sort();
const skills = skillDirs.map((skillPath) => skillHealthItem(skillPath, options.thresholdBytes));
const alerts = skills.filter((item) => item.status === "ALERT");
const max = skills.reduce<SkillHealthItem | null>((current, item) => current === null || item.bytes > current.bytes ? item : current, null);
const ok = skills.length > 0 && alerts.length === 0;
return {
ok,
command: "health",
check: "skill-md-size",
thresholdBytes: options.thresholdBytes,
skillsDir: displayPath(skillsRoot),
summary: {
totalSkills: skills.length,
okSkills: skills.length - alerts.length,
alertSkills: alerts.length,
maxBytes: max?.bytes ?? 0,
maxSkillId: max?.skillId ?? null,
},
alerts,
skills,
policy: {
limitBytes: options.thresholdBytes,
splitTarget: ".agents/skills/<skill_id>/references/*.md",
skillMdRole: "Keep SKILL.md under the limit with only trigger-critical, highest-frequency workflow guidance; move lower-frequency detail into directly linked references.",
},
next: skills.length === 0
? [`No SKILL.md files were found under ${displayPath(skillsRoot)}; check --skills-dir.`]
: alerts.length === 0
? ["No skill split is required."]
: [
"Move lower-frequency detail into .agents/skills/<skill_id>/references/*.md.",
"Keep SKILL.md as a compact high-frequency workflow and navigation index.",
"Rerun: bun scripts/cli.ts health",
],
};
}
function skillHealthItem(skillPath: string, limitBytes: number): SkillHealthItem {
const skillMdPath = join(skillPath, "SKILL.md");
const text = readFileSync(skillMdPath, "utf8");
const bytes = statSync(skillMdPath).size;
const skillId = skillPath.split(/[\\/]/u).pop() ?? displayPath(skillPath);
const referencesDir = join(skillPath, "references");
const overBytes = Math.max(0, bytes - limitBytes);
return {
skillId,
skillPath: displayPath(skillPath),
skillMdPath: displayPath(skillMdPath),
referencesDir: displayPath(referencesDir),
bytes,
lines: text.split(/\r?\n/u).length,
limitBytes,
overBytes,
status: overBytes > 0 ? "ALERT" : "OK",
action: overBytes > 0 ? `split lower-frequency detail to ${displayPath(referencesDir)}` : "-",
};
}
function renderSkillHealth(result: SkillHealthResult): string {
const lines = [
`skill health (${result.ok ? "ok" : "alert"})`,
"",
renderTable(["SUMMARY", "VALUE"], [
["skills", String(result.summary.totalSkills)],
["limit", `${result.thresholdBytes} bytes`],
["alerts", String(result.summary.alertSkills)],
["largest", result.summary.maxSkillId === null ? "-" : `${result.summary.maxSkillId} ${result.summary.maxBytes} bytes`],
]),
"",
];
if (result.alerts.length > 0) {
lines.push(
"ALERTS",
renderTable(["SKILL", "BYTES", "OVER", "LINES", "ACTION"], result.alerts.map((item) => [
item.skillId,
String(item.bytes),
String(item.overBytes),
String(item.lines),
item.action,
])),
"",
);
} else {
lines.push("ALERTS", " none", "");
}
lines.push(
"Policy:",
` SKILL.md limit: ${result.thresholdBytes} bytes`,
" Split target: .agents/skills/<skill_id>/references/*.md",
" Keep SKILL.md to highest-frequency workflow/routing content and direct reference links.",
"",
"Next:",
...result.next.map((item) => ` ${item}`),
" bun scripts/cli.ts health --full",
);
return lines.join("\n");
}
function renderTable(headers: string[], rows: string[][]): string {
const normalized = rows.length === 0 ? [headers.map(() => "-")] : rows;
const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => (row[index] ?? "").length)));
const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
return [renderRow(headers), ...normalized.map(renderRow)].join("\n");
}
+3
View File
@@ -4,6 +4,7 @@ import { ghHelp, ghScopedHelp } from "./gh";
import { authBrokerHelp } from "./auth-broker";
import { platformDbHelp } from "./platform-db";
import { secretsHelp } from "./secrets";
import { healthHelp } from "./health";
export function rootHelp(): unknown {
return {
@@ -14,6 +15,7 @@ export function rootHelp(): unknown {
{ command: "--main-server-ip <ip> <command>", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." },
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
{ command: "check [--full|--files|--scripts-typecheck|--scripts-typecheck-timeout-ms N|--check-heartbeat-ms N|--components|--compose|--logs|--recovery-guardrails|--rust] | check recovery-guardrails", description: "Run the lightweight default syntax/config gate or the low-noise read-only D601 recovery guardrails; long command checks emit progress heartbeats and bounded timeout details." },
{ command: "health [--json|--full|--skills-dir path|--threshold-bytes N]", description: "Check repo skill SKILL.md size health; files over 10 KiB alert and must move lower-frequency detail into .agents/skills/<skill_id>/references/." },
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server user services." },
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
@@ -755,6 +757,7 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
const [top, sub] = args;
if (!args.slice(1).some(isHelpToken)) return null;
if (top === "config") return configHelp();
if (top === "health") return healthHelp();
if (top === "microservice") return microserviceHelp();
if (top === "decision" || top === "decision-center") return decisionHelp();
if (top === "provider") return providerHelp();