From 76758b58fdc8a368ff614f1b741b5a65e1a35898 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Jul 2026 05:29:30 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B8=85=E7=90=86=20trans=20=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=97=A7=E5=85=A5=E5=8F=A3=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/cli.md | 49 +++++++++++++++++++++--- docs/reference/code-queue-supervision.md | 7 +++- docs/reference/master-server-ops.md | 2 +- scripts/src/apply-patch-v2.test.ts | 29 ++++++++++++++ scripts/src/apply-patch-v2.ts | 2 +- scripts/src/help.ts | 13 ++++--- scripts/src/ssh-help.test.ts | 12 ++++-- 7 files changed, 96 insertions(+), 18 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a680f5f6..8025bc1a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -116,8 +116,23 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status - `server rebuild ` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`。Todo Note 的 CC01/旧 Compose 回退入口已在迁移验收后删除;正式发布只走 NC01 PaC/Argo。D601 Code Queue 执行面不由 `server rebuild` 管理;Rust backend-core 常规迭代不得用该命令在 master server 编译,只有明确的 backend-core 主 server 上线例外可以按限流、异步轮询和 health 证据执行,规则见 `docs/reference/dev-environment.md`。 - `provider attach [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap`、`service-degraded`、`provider-degraded` 或 `global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full` 或 `--raw`;推荐交叉验证命令仍包含 `debug health`、`debug dispatch host.ssh --wait-ms 15000`、`trans argv true`、`artifact-registry health --provider-id `、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。 - `platform-db postgres plan|status|export-secrets|apply --config config/platform-db/postgres-pk01.yaml` 管理 YAML 声明的 PK01 host-native PostgreSQL。`plan` 和 `status` 只读采集远端 host、PostgreSQL、TLS、DNS alias 和 Secret 形态;`export-secrets --confirm` 只按 YAML 物化本地 Secret source/export 文件,不触碰远端 PostgreSQL;`apply --confirm` 默认创建本地异步 job,`apply --confirm --wait` 用远端 root-owned job 收敛 systemd PostgreSQL、TLS、`pg_hba`、role/database、Secret export 和备份 timer。输出不得打印密码或完整 `DATABASE_URL`。跨节点消费者使用 YAML 中的 `connectionHost` 直连 PK01 公网 endpoint,DNS alias 不作为 `sslmode=require` 切库 blocker,PK01 规则见 `docs/reference/pk01.md`。 -- `trans [operation args...]` / `tran [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601` 或 `G14`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s::`。WSL provider 的 Windows plane 固定使用 `win`,不得使用 `win32`;Windows route 中 `win` 只负责定位,operation 直接写 `ps`、`cmd`、`git` 或 fs helper,不要写成 `trans D601:win/... win ps`。Windows operation 必须显式区分:`ps` 执行 Windows PowerShell heredoc 或一行 PowerShell 命令,`cmd` 执行 cmd.exe/batch,`skills` 发现 Windows skill 目录。需要 Windows cwd 时用 `trans D601:win/c/test ps`、`trans D601:win/c/test cmd cd`、`trans D601:win/c/test git diff --check` 或 `trans D601:win/c/test git commit -m 'message'`,CLI 自动设置 UTF-8/Python 编码默认值;`cmd` 额外设置 `chcp 65001`。非交互远端命令优先使用 `trans argv ...`;需要 POSIX shell 脚本、管道、变量或循环时必须在 operation 位置显式写 `sh` 或 `bash`,例如 `trans G14 sh <<'SH'`、`trans G14:k3s sh <<'SH'`、`trans G14:k3s:: sh <<'SH'` 或 `trans D601:/workspace bash <<'BASH'`。`sh` 明确表示目标 `/bin/sh`,`bash` 明确表示目标 Bash;`script` 和 `shell` operation 已移除并会失败,避免隐藏 shell 方言。Windows PowerShell 必须写 `trans :win ps <<'PS'`。一行远端 shell 逻辑使用 `sh -- '<单个字符串>'` 或 `bash -- '<单个字符串>'`;顶层 remote option parser 必须保留命令已经开始后的 `--`,不得把它吞成全局选项结束符。需要远端改文本文件时默认优先使用 ` apply-patch < patch.diff`;需要可靠传输非文本或整文件时使用 ` upload ` 和 ` download `,CLI 会按字节数与 SHA-256 自动校验并在 provider-gateway stdin/argv 限制下切换客户端分块策略;需要旧 helper 时显式使用 `:k3s:: apply-patch-v1` 或 ` apply-patch-v1`。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示改用 `sh`/`bash` stdin heredoc、argv 或 provider triage 交叉验证。 -- `trans apply-patch < patch.diff` 是默认推荐的远端 patch 入口:本地 TypeScript line-based engine 解析和计算新文件内容,远端 route 只负责读写文件;支持 host workspace、k3s pod workspace、Windows workspace route(例如 `D601:win/c/test`)和 frontend transport,并优先处理长中文/Unicode、低上下文插入、重复块 `@@` 定位等旧 helper 容易失败的场景。`apply-patch` 输出按 Codex 标准文本口径,不套 UniDesk JSON 限制:成功 stdout 为 `Success. Updated the following files:`,失败 stdout 为空、stderr 写失败原因;多文件补丁中途失败时,stderr 只列出第一个失败前已成功执行的 hunk 和失败 hunk,随后按 Codex 语义停止,不继续尝试后续 hunk。v2 兼容常见 MiniMax/MXCX 非标准补丁输入,例如重复 nested `*** Begin Patch` / `*** End Patch` envelope、unified-diff hunk header、Add/Delete 误加 `@@`、Update context 漏掉前导空格,并在 stderr 给出 canonical 写法 hint;parser 或上下文失败时仍坚持唯一 v2 引擎,只提示修正 patch 文本或 hunk context,不自动重试或切换到 `apply-patch-v1`;大块/函数替换因上下文过期失败时,正确动作是重新读取当前目标块、缩小或拆分 Update File hunk 后继续用 `apply-patch`,不得改走 `download`/`upload`、远端 Python/Perl/sed heredoc 或整文件重写。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`trans apply-patch-v1 [tool args...] < patch.diff` 保留为显式 legacy 入口,直接调用远端注入的 `apply_patch` sh/perl helper;默认 `apply-patch` 不把 v1 当 fallback。 +- `trans [operation args...]` / `tran [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点: + - `route` 基础形态是 provider id,例如 `D601` 或 `G14`;也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s::`。 + - WSL provider 的 Windows plane 固定使用 `win`,不得使用 `win32`;Windows route 中 `win` 只负责定位,operation 直接写 `ps`、`cmd`、`git` 或 fs helper。 + - Windows operation 中,`ps` 执行 PowerShell heredoc 或一行 PowerShell 命令,`cmd` 执行 cmd.exe/batch,`skills` 发现 Windows skill 目录;CLI 自动设置 UTF-8/Python 编码默认值,`cmd` 额外设置 `chcp 65001`。 + - 非交互远端命令优先使用 `trans argv ...`;需要 POSIX shell 脚本、管道、变量或循环时,在 operation 位置显式写 `sh` 或 `bash`,并通过 quoted heredoc 传入正文。 + - 远端文本修改默认使用 `trans apply-patch <<'PATCH'`;已知文本路径不得改走 `upload` / `download`,只有整体搬运生成文本产物时才显式加 `--allow-text-transfer`。 + - 非文本或二进制文件使用 ` upload ` 和 ` download `,CLI 自动核对字节数与 SHA-256。 + - 只有人工确认需要旧 helper 时,才显式使用 `:k3s:: apply-patch-v1` 或 ` apply-patch-v1`,输入仍使用 quoted heredoc。 + - ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 在 stderr 追加 `UNIDESK_SSH_HINT` JSON,提示改用 `sh` / `bash` stdin heredoc、argv 或 provider triage 交叉验证。 +- `trans apply-patch <<'PATCH'` 是默认推荐的远端 patch 入口: + - 本地 TypeScript line-based engine 解析并计算新文件内容,远端 route 只负责读写文件;支持 host workspace、k3s pod workspace、Windows workspace route 和 frontend transport。 + - quoted heredoc 中直接放置标准 `*** Begin Patch` / `*** End Patch` envelope,不先创建 patch 临时文件。 + - 成功 stdout 为 `Success. Updated the following files:`;失败 stdout 为空,stderr 写失败原因。多文件补丁中途失败时按 Codex 语义停止,保留失败前已提交的变更。 + - v2 可规范化常见非标准补丁输入并给出 canonical 写法 hint;parser 或上下文失败时,只提示修正 patch 文本或 hunk context,不自动重试或切换到 `apply-patch-v1`。 + - 大块替换因上下文过期失败时,重新读取当前目标块并缩小或拆分 Update File hunk;不得改走 `download` / `upload`、远端 Python/Perl/sed heredoc 或整文件重写。 + - Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口。 + - `trans apply-patch-v1 [tool args...] <<'PATCH'` 是显式 legacy 入口,直接调用远端注入的 `apply_patch` sh/perl helper;默认 `apply-patch` 不把 v1 当 fallback。 - GitHub issue/PR 正文局部修补必须优先走 `trans gh:/owner/repo/issue/ apply-patch` 或 `trans gh:/owner/repo/pr/ apply-patch`,而不是人工优先整篇 `gh issue update --mode replace` 或底层 `gh issue patch`。`gh:/` 路由把 issue/PR body 暴露为一楼虚拟文件 `body.md`;route 未显式写 `/1` 时默认一楼正文,`apply_patch` 与 `patch-apply` 只作为兼容别名。典型补丁写法是 `*** Update File: body.md`;普通小补丁在已用 `trans gh:/... cat|rg|ls` 确认上下文后可以直接 apply,`--dry-run` 只作为高风险大段修改、关闭前验收文案或并发敏感正文的可选预览。写回仍通过 UniDesk `gh issue/pr update` guard 和 REST API,不使用原生 `gh`、手写 REST 或整篇 shell 拼接正文。单条 issue comment 的局部修补当前仍使用 `bun scripts/cli.ts gh issue comment patch --body-patch-stdin`,直到评论楼层 route 成为稳定入口。 - `apply-patch` v2 每次结束都会在 stderr 追加一行 `UNIDESK_APPLY_PATCH_TIMING {json}`,字段包含 `durationMs`、`patchBytes`、`fileCount`、`hunkCount`、`changedCount`、`remoteOperationCount`、`remoteOperationCounts`、`remoteElapsedMs`、`remoteFailureCount`、`providerId`、`route` 和 `transport`(可得时)。普通 POSIX host/k3s 和 Windows workspace 远端的多文件 `Update File` patch 会优先合并成 bulk read/write,避免每个文件单独 stat/read/write 的 SSH 往返;Add/Delete/Move 等复杂 patch 保持原有逐步语义。timing 摘要只用于定位慢在 patch 解析、远端 stat/read/write 或 bulk read/write、provider session 还是传输层,不能替代 Codex 标准 stdout/stderr 成功失败文本,也不是门禁或自动判断。 - `trans py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。 @@ -154,7 +169,11 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status - `secrets plan|sync|status --config config/secrets-distribution.yaml --scope platform-infra` 是平台服务本地 Secret sourceRef 到 Kubernetes Secret key 的受控下发入口。`plan` 只读展示 sourceRef、必需 key、目标 Secret/key、missingKeys 和 fingerprint;`sync --confirm` 只按 YAML 声明创建允许生成的本地 key 并下发到声明的目标 Secret;`status` 只验证 live Secret key presence。该入口禁止从 live pod 或 Kubernetes Secret 反推密码、API key、JWT secret、n8n encryption key 或 `DATABASE_URL`,输出也不得打印 base64、解码值或远端 raw transcript;即使显式 `--raw` 也只返回脱敏 summary 和 raw omission 标记。LangBot/n8n Secret 轮换和缺 key 修复规则见 `docs/reference/platform-infra.md#secret-distribution-boundary`。 - `hwlab g14 observability status|apply|query|targets|boundary|closeout [--lane v02] [--promql ] [--expect-count N] [--expect-value V] [--dry-run|--confirm]` 是 G14 `devops-infra` 共享监控基础设施和 HWLAB v0.2 监控 closeout 的受控入口。`apply` 固定安装 Prometheus Operator `v0.91.0`、Prometheus `v3.12.0`、Prometheus 发现 RBAC、`devops-infra` 内 Prometheus 实例和 ClusterIP query Service,并给被允许发现的 workload namespace 打低风险 label;它不把 Prometheus、Grafana 或 Alertmanager 部署到 `hwlab-v02`,也不接管 HWLAB runtime Deployment/Service。`status` 只读汇总 CRD、operator Deployment、Prometheus CR/pod/service、`hwlab-v02` ServiceMonitor/PrometheusRule 和 bounded `up` 查询;`query` 只通过 Kubernetes service proxy 查询 Prometheus,支持 `--expect-count` / `--expect-value` 输出 `assertion`、bad values 和 missing/extra series;`targets` 汇总 ServiceMonitor/PrometheusRule、metrics sidecar readiness/restart、三层指标值和 `metrics.k8s.io` 当前 CPU/内存资源快照;`boundary` 验证 workload namespace 没有 Prometheus/Alertmanager,并对 `19666/19667` 公网 `/metrics` 做负向验证;`closeout` 聚合平台 ready、scrape reachable、sidecar serving、business health probe、resource snapshot、namespace boundary 和 public metrics exposure 语义结论。长期边界见 `docs/reference/g14-observability-infra.md`。 - `hwlab g14 tools-image status|build --name ci-node-tools --tag [--dockerfile deploy/ci/hwlab-ci-node-tools.Dockerfile] [--dry-run|--confirm]` 是 G14 固定 HWLAB CI tools image 的受控 host build/push 入口;构建和 push 只发生在 G14 host 与本地 registry,不在 master server 构建,也不把 `apk add`/runtime install 塞进 Tekton PipelineRun。 -- `trans gh:/owner/repo ...` 把 GitHub issue/PR 映射成只读/受控写入的虚拟文本目录,适合日报、PR 正文和 issue 正文的小补丁维护:`trans gh:/pikasTech/HWLAB ls` 展示 `pr/` 与 `issue/`,`trans gh:/pikasTech/HWLAB/pr ls [--state open|closed|all] [--limit N] [--full]` 展示 PR 条目状态、楼层数、正文长度、URL 和标题,`trans gh:/pikasTech/HWLAB/issue ls [--state open|closed|all] [--limit N] [--search TEXT] [--label L] [--full]` 替代常见 `gh issue list --search/--state` 读取并输出 URL,`trans gh:/pikasTech/HWLAB/pr/507 ls` 展示单个 PR 的一楼正文文件,`trans gh:/pikasTech/HWLAB/issue/1236 cat` 读取 issue 正文,`trans gh:/pikasTech/HWLAB/issue/1236 rg 'API_KEY|YAML-first'` 在正文内检索,`trans gh:/pikasTech/HWLAB/505/1 cat|rg|patch-apply` 兼容旧式 issue/PR number route。`apply-patch` 使用 UniDesk 默认 apply-patch v2 的虚拟文件 executor,把正文一楼映射为 `body.md`,写回仍走 `bun scripts/cli.ts gh issue/pr update` 的 guard/concurrency 规则;普通小补丁不需要固定先跑 `--dry-run`,高风险正文可用 `--dry-run` 预览。`rm` 对正文一楼结构化拒绝,避免误删 issue/PR 正文。大正文读取必须展开 UniDesk gh dump 文件,否则 `cat/rg/apply-patch` 会误读为空,这是 `gh:` 虚拟文件接口的 P0 可见性契约。 +- `trans gh:/owner/repo ...` 把 GitHub issue/PR 映射成只读、受控写入的虚拟文本目录: + - `ls` 展示 PR / issue 目录和有界条目摘要;`cat` 直接读取一楼正文;`rg` 在正文内检索。 + - `apply-patch` 使用默认 v2 虚拟文件 executor,把正文一楼映射为 `body.md`,写回仍走 `bun scripts/cli.ts gh issue/pr update` 的 guard/concurrency 规则。 + - 普通小补丁不固定要求先跑 `--dry-run`;高风险正文可用 `--dry-run` 预览;`rm` 对正文一楼结构化拒绝。 + - 大正文默认不创建或依赖 dump;接口必须直接消费单份正文。超过显示预算时返回语义化窄查询,调用方继续用同一 `gh:` route 的 `rg`、`cat` 或 `apply-patch`,不得展开临时 dump 文件。 - `hwlab cd status|audit|preflight|apply --env dev [--dry-run]` 是旧 D601 HWLAB DEV CD 指挥侧 wrapper,仅用于显式 legacy 诊断和迁移对照。默认通过 UniDesk provider `host.ssh` 进入 D601,再调用 HWLAB repo-owned `scripts/dev-cd-apply.mjs`,不内嵌发布 kubectl 逻辑:`status` 汇总固定 CD mirror、Git clean/main/origin-main、`deploy/deploy.json`/artifact catalog/report、D601 native k3s guard 和 CD Lease lock,并用 `scripts/dev-cd-apply.mjs --status --skip-live-verify` 取得 target/promotion 摘要;`audit` 在 k3s/CD 恢复后做只读健康审计,返回有界 JSON 的 blocker 分类、D601 guard/node、SecretRef 存在性、registry 可达性、Lease phase/holder/staleness、deploy.json 与 artifact/workload image 收敛、current Deployment image/revision/rollout、16666/16667 public health commit/readiness 和 DB/runtime durability 摘要;`preflight` 进一步检查必需 SecretRef 对象/键存在性并运行 HWLAB `scripts/dev-cd-apply.mjs --dry-run --skip-live-verify` 受控事务摘要。完整远端 stdout/stderr 写入 D601 `~/.state/unidesk-hwlab-cd//` 和本地 `.state/hwlab-cd//` task dump,stdout 只返回有界摘要。默认 HWLAB CD repo 是 `/home/ubuntu/hwlab_cd`,`/home/ubuntu/hwlab` runner 历史目录不得作为发布真相。wrapper 强制 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并只以这个显式目标作为 gate;显式目标出现 `docker-desktop`、`desktop-control-plane` 或 `127.0.0.1:11700` 信号会结构化拒绝,audit/preflight/apply --dry-run 都必须观察到 node `d601`。真实 apply 只暴露 `scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report` 命令形状并标注 host-commander-only,本 runner 不执行 live apply、rollout、Lease mutation 或 DEV deploy apply。长期规则见 `docs/reference/hwlab.md`。 - `gh auth status [--repo owner/name]` 探测 GitHub 操作前置条件并输出脱敏 JSON:是否存在 `gh` binary、是否存在 `GH_TOKEN`/`GITHUB_TOKEN` 或可用 `gh auth token` fallback、REST API 是否可达、目标 repo 是否可见、issue 是否可读。degraded reason 必须归类为 `missing-binary`、`missing-token`、`auth-failed`、`github-transient`、`network-proxy-failed`、`permission-denied`、`repo-not-found`、`repo-forbidden`、`issue-not-found`、`pr-not-found`、`scope-insufficient`、`validation-failed`、`invalid-response` 或 `unsupported-command`,不得打印 token;失败对象必须包含 `runnerDisposition=infra-blocked|business-failed`,runner 应优先用该字段分流。`github-transient` 表示 GitHub DNS/API 连接在收到 HTTP 状态前失败,输出应带 `retryable=true` 或等价 commander action;这不是缺 token、认证失败、权限不足或 PR 语义失败。 - `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]` 通过 GitHub REST 列出 issue,默认 `state=open`、`limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 是 CLI 返回上限,不等同 GitHub 单页 `per_page`:当 `--limit > 100` 或默认页中混入 PR 时,CLI 必须分页抓取 GitHub REST/Search page,过滤 PR 后再返回 issue,并在输出中披露 `pagination.fetchedPages/rawCount/hasMore`;`hasMore=true` 时只能说明当前有界扫描未穷尽,禁止把它当作“仓库没有更多 issue”。`--search` 使用 GitHub Search Issues API,并自动追加 `repo:/`、`type:issue` 和 state qualifier,用于创建新 issue 前做低摩擦查重;`--title-prefix` 在当前有界结果内按 issue 标题 `startsWith` 做本地过滤,输出 `titleFilter.inputCount/outputCount/filteredOut`,适合 `[FEEDBACK]` 等标题规范去重;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--label` 是 GitHub REST `labels=label1,label2` 或 Search `label:` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read`、`pr list`)必须结构化失败并指明 `gh issue create/list/stale-close` 才是合法作用域。GitHub issues API 可能混入 PR,CLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full` 在 `gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true`,`output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。 @@ -333,7 +352,12 @@ exec /root/unidesk/scripts/trans "$@" ### Standard Workspace-Prefixed Passthrough - 长期参考、AGENTS 索引、CLI help、Codex 任务脚本、CI/CD 排障和人工远端操作必须统一把已知的远端 workspace 写在 route 的第一个 token,而不是塞进 `cd` 串。`route` 段只表达分布式定位,operation 段才执行命令;workspace 路径是定位信息,不是命令。 -- 标准形态是 `trans :/absolute/workspace [args...]`:例如 `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'`、`trans D601:/home/ubuntu/workspace/hwlab-v03 bash <<'BASH'`、`trans G14:/root/hwlab apply-patch < patch.diff` 和 `trans G14:/root/hwlab glob --root . --pattern 'web/hwlab-cloud-web/*.ts' --contains session-tabs`。 +- 标准形态是 `trans :/absolute/workspace [args...]`: + - 只读命令:`trans G14:/root/hwlab git status --short --branch`。 + - 单行 shell:`trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'`。 + - 多行 shell:`trans D601:/home/ubuntu/workspace/unidesk-dev sh <<'SH'` 或 `trans D601:/home/ubuntu/workspace/hwlab-v03 bash <<'BASH'`。 + - 文本修改:`trans G14:/root/hwlab apply-patch <<'PATCH'`。 + - 结构化检索:`trans G14:/root/hwlab glob --root . --pattern 'web/hwlab-cloud-web/*.ts' --contains session-tabs`。 - 反面形态必须删除或迁移:`trans G14 sh -- 'cd /root/hwlab && git status --short --branch'`、`trans G14 sh <<'SH' cd /root/hwlab-v02 git fetch origin v0.2 SH`、`tran G14 sh -- 'cd /home/ubuntu/workspace/unidesk-dev && ...'`、route 后继续写旧的 `script` / `shell` operation、`bun scripts/cli.ts ssh G14 -- 'cd /root/hwlab && ...'`。这些写法把已知 workspace 写进 command 字符串,破坏 route/operation 分离,引入本地 shell 二次解析、远端 cwd 漂移和并行 worktree 切换摩擦,或继续使用已移除的模糊 shell 旧名。 - 例外只限于一次性探测、临时 heredoc 草稿或旧文档复用;任何被复用第二次的 `cd && ...` 都必须重写成 `trans :/absolute/workspace` 形式。 - 当远端存在多个并行 workspace(例如 `G14:/root/hwlab` 与 `G14:/root/hwlab-v02`)时,route 必须显式带 workspace,CLI 的 `pwd` 输出、后续 `apply-patch` 的相对路径和 `sh`/`bash` 的 cwd 全部跟随该 workspace;切换 workspace 必须切换 route,不允许在同一次 `trans` 链里再 `cd`。 @@ -376,13 +400,26 @@ core 只允许声明了 `host.ssh` capability 的 provider 使用 `host.ssh` dis ssh-like 远端命令如果出现 `kex_exchange_identification`、`Connection closed by remote host`、provider session timeout 或 exit code 255,CLI 会在原始 stderr 后追加一行 `UNIDESK_SSH_HINT { ... }`。该 JSON 不回显原始远端命令,只包含 `code=ssh-like-command-friction`、`trigger`、`try` 和 `triage`;`try` 固定指向显式 `sh` stdin heredoc 形态,避免把一次 ssh-like 解析/握手摩擦误读成 D601 SSH 整体不可用。`ssh`/`trans`/`tran` 在失败路径识别到 tcp-pool 数据面问题时会追加 `UNIDESK_SSH_TCP_POOL_HINT { ... }`,`failureKind` 固定分为 `provider-data-channel-closed`、`provider-data-channel-missing` 和 `provider-data-pool-exhausted`;这类 hint 表示 transport/data-pool transient,幂等受控操作应先运行 `bun scripts/cli.ts debug ssh-pool ` 查看 labels,再重试原受控 CLI,不能单独定性为远端 runtime 配置失败。backend-core/provider 控制面返回结构化 `ssh.error` 时 broker 还会输出 `UNIDESK_SSH_ERROR { ... }`,供 `job status` 从旧日志或异步 job 日志中恢复 failureKind。`ssh`/`trans`/`tran` 运行时硬超时会输出 `UNIDESK_SSH_RUNTIME_TIMEOUT { ... }` 或 wrapper 层 `UNIDESK_TRAN_TIMEOUT_HINT { ... }`;这不是远端业务失败,而是调用方需要改成短查询/轮询。`ssh`/`trans`/`tran` 只有在运行耗时超过默认 10000ms 时才会在 stderr 追加一行 `UNIDESK_SSH_TIMING { ... }`,且 `level=warning`;正常短调用不输出 timing 噪声。慢成功命令也必须保留该 warning,因为它是 provider session、远端命令成本、helper bootstrap 和 `trans`/`tran`/远端 patch 性能回归的重要监控信号。warning 包含 `elapsedMs`、`elapsedSeconds`、`transport`、`invocationKind` 和 `exitCode`,提示优先排查 provider/session 延迟、远端命令自身耗时、helper bootstrap 或工具层回归。阈值可用 `UNIDESK_SSH_SLOW_WARNING_MS=` 临时调节,提示同样不回显原始远端命令。 -非交互 `ssh`/`trans`/`tran` 远端命令的流式 stdout/stderr 服从统一 CLI dump/preview 策略,默认预览预算来自 `config/unidesk-cli.yaml#output.maxStdoutBytes`,完整流写入同一 YAML 声明的 dump 目录;交互登录 shell 不套该上限。超过预算时,CLI 继续读取远端流并把完整内容写入 `/tmp/unidesk-cli-output/*.stdout.bin` 或 `*.stderr.bin`,本地 stderr 追加 `UNIDESK_SSH_STDOUT_TRUNCATED { ... }` 或 `UNIDESK_SSH_STDERR_TRUNCATED { ... }`,其中包含 `stream`、`thresholdBytes`、`forwardedBytes`、`observedBytesAtTruncation`、`dumpPath`、`dumpError`、`disclosurePolicy` 和 `recommendedRerun`;被截断的本地流只保留 YAML 字节预算内的开头预览。需要一次性扩大字节预算时可显式设置 `UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES` / `UNIDESK_TRAN_STDOUT_STREAM_MAX_BYTES` 或 `UNIDESK_SSH_STDERR_STREAM_MAX_BYTES` / `UNIDESK_TRAN_STDERR_STREAM_MAX_BYTES`,仍受最小 4KiB、最大 16MiB 的工具保护。该机制只做渐进披露和完整 dump,不替代远端命令失败判断;看到 hint 时应优先改成 `rg -m`、`sed -n`、`tail`、`--limit`、`--tail-bytes`、`--raw/--full` 等更窄的结构化 drill-down。 +非交互 `ssh` / `trans` / `tran` 远端命令的流式 stdout/stderr 使用统一的有界预览策略: + +- 默认预览预算来自 `config/unidesk-cli.yaml#output.maxStdoutBytes`;超过预算后继续消费远端流以取得最终 exit code,但超出部分不落盘,`dumpPath=null`。 +- 本地 stderr 追加 `UNIDESK_SSH_STDOUT_TRUNCATED { ... }` 或 `UNIDESK_SSH_STDERR_TRUNCATED { ... }`,披露 stream、阈值、已观察字节数、`dumpPath`、disclosure policy 和推荐的窄查询。 +- 只有用户显式设置 `UNIDESK_TRAN_STREAM_DUMP=1` 时才把完整流写入受保护的 dump;`--full` / `--raw` 属于上层命令的显式完整披露,不应由默认 `trans` 静默推导。 +- 需要一次性扩大预览预算时可显式设置 `UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES` / `UNIDESK_TRAN_STDOUT_STREAM_MAX_BYTES` 或对应的 stderr 变量,仍受最小 4KiB、最大 16MiB 的工具保护。 +- 看到截断 hint 时优先改成 `rg -m`、`sed -n`、`tail`、`--limit` 或 `--tail-bytes` 等更窄的结构化 drill-down;交互登录 shell 不套该上限。 `trans ` 透传只在当前 operation 需要 helper 时才注入 `/tmp/unidesk-ssh-tools`,普通 `argv`、`sh`/`bash`、`kubectl`、`logs` 和默认 `apply-patch` 等路径不得传输无关工具源码。`apply-patch-v1` 只注入 `apply_patch`;`glob` 只注入 `glob`;`skills`/`skill discover` 只注入 `skill-discover`。`apply_patch` 接受标准 `*** Begin Patch` / `*** End Patch` patch 格式,便于通过 SSH 透传编辑远端仓库文件;远端存在 `perl` 时必须走快速精确匹配路径,避免大文件 hunk 被 sh 模式匹配拖成几十秒,缺少 `perl` 时才退回 sh-only 实现。`glob` 和 `skill-discover` 需要远端 `python3`。注入工具只写 `/tmp/unidesk-ssh-tools`,不修改目标仓库。 远端文本 patch 默认使用 `apply-patch` 的 v2 引擎:它不把 hunk 解析交给远端 shell/perl helper,而是在本地按行序列匹配,支持长中文/Unicode 行、纯新增 hunk、低上下文插入和 `@@` 上下文定位,再把完整新内容写回远端。v2 的文件操作提交顺序按 Codex 标准 `apply_patch` 语义执行:空 patch 会失败;删除不存在的文件会失败;`Add File` 可覆盖已有文件;`Move to` 可覆盖目标文件;当大 patch 后续 hunk 不匹配时,已成功提交的前序文件操作会保留,并在错误详情中记录 `partialChanges`,调用方应基于当前文件内容继续补一个更小的 patch,而不是期待全量事务回滚。若 stderr 报 `failed to find expected lines` 且显示 partial context match,尤其是大块/函数替换,调用方必须先重读目标文件当前块,再用更少稳定上下文、`@@ ` 或多个小 hunk 重试;该失败不构成改用 `download`/`upload`、远端脚本整文件替换或 `apply-patch-v1` 的理由。`apply_patch` 旧 helper 默认拒绝低上下文 update hunk:空搜索/纯插入无锚点、只在插入点前有上下文而没有插入点后上下文、或同一 hunk search 在目标文件中匹配多个位置时,都会结构化失败并提示补充上下文。成功应用时每个 hunk 会在 stderr 输出 `apply_patch: hunk N matched path:line`,用于复核实际落点;只有人工确认确实需要旧 helper 行为或 `--allow-loose` 时,才显式调用 `apply-patch-v1 --allow-loose`。 -如果只是远端打文本补丁,不需要再手写 `ssh D601 'apply_patch' < patch.diff` 这种命令拼接;正式默认入口是 `trans D601:/absolute/workspace apply-patch < patch.diff`、`trans D601:k3s::[:] apply-patch --cwd /workspace < patch.diff` 或 `trans D601:win/c/test apply-patch < patch.diff`。旧 helper 只有 `apply-patch-v1` 一个入口,附加参数会原样透传给远端 `apply_patch`,例如 `trans D601 apply-patch-v1 --help` 或 `trans D601 apply-patch-v1 --allow-loose < reviewed.patch`。标准单命令用法如下,不需要先创建本地 patch 临时文件: +远端文本补丁统一通过 stdin quoted heredoc 直送,不先创建、下载或上传 patch 临时文件: + +- host workspace 使用 `trans D601:/absolute/workspace apply-patch <<'PATCH'`。 +- k3s workload 使用 `trans D601:k3s::[:] apply-patch --cwd /workspace <<'PATCH'`。 +- Windows workspace 使用 `trans D601:win/c/test apply-patch <<'PATCH'`。 +- 旧 helper 只有 `apply-patch-v1` 一个显式入口;附加参数原样透传,补丁正文仍使用 quoted heredoc,例如 `trans D601 apply-patch-v1 --allow-loose <<'PATCH'`。 + +标准单命令用法如下: pod 内文本热修的反面案例是:先在 host/source worktree 生成 `git diff`,再用本地 `sed` 改路径、拼 `*** Begin Patch` 包头,最后管道到 `G14:k3s::`。这种做法把 source workspace、local shell、远端 shell 和 pod workspace 四层混在一起,容易出现 patch 格式错误、路径漂移、部分成功后误判、以及“看起来在热修 pod,实际主要在搬运 host diff”的错误行为。正确做法是把第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 `--cwd /path` 表达,然后在同一条 route 上写标准 Codex patch: diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 2d145c53..51c02708 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -147,7 +147,12 @@ MiniMax prompt 必须自包含:目标、背景、写入范围、禁止动作 MiniMax 风险控制必须固定包含:禁止 prod/重启/密钥/DB 写入;禁止 release/v1 runtime 修改;禁止 heavy check/E2E/Playwright,除非任务明确改为 GPT-5.5 且用户授权;必须给出可验证证据,包括修改文件、命令、测试输出、commit 和未覆盖风险;完成后保持未读,由指挥官用 `codex task ` 审阅后再单独 `codex read `。 -MiniMax 任务涉及远端文本修改时,prompt 必须显式指定标准写入入口和失败恢复动作:默认使用 workspace-prefixed `trans apply-patch < patch.diff` 或对应业务 CLI 的 `apply-patch` 子命令;若 stderr 报 `failed to find expected lines`,必须重新读取当前目标块,缩小或拆分 `Update File` hunk 后继续 `apply-patch`。不得把上下文过期当作改用 `download`/`upload`、远端 Python/Perl/sed heredoc、`cat >`/`tee` 整文件重写或 `apply-patch-v1` 的理由。指挥官验收 MiniMax trace 时应搜索这些 forbidden command 形态;若出现,优先修 CLI hint/prompt 或拆小任务后复测,而不是把绕行作为合格完成。 +MiniMax 任务涉及远端文本修改时,prompt 必须显式指定标准写入入口和失败恢复动作: + +- 默认使用 workspace-prefixed `trans apply-patch <<'PATCH'`,或对应业务 CLI 的 `apply-patch` 子命令;补丁正文直接进入 quoted heredoc,不先创建 patch 文件。 +- 若 stderr 报 `failed to find expected lines`,必须重新读取当前目标块,缩小或拆分 `Update File` hunk 后继续 `apply-patch`。 +- 不得把上下文过期当作改用 `download` / `upload`、远端 Python/Perl/sed heredoc、`cat >` / `tee` 整文件重写或 `apply-patch-v1` 的理由。 +- 指挥官验收 MiniMax trace 时应搜索这些 forbidden command 形态;若出现,优先修 CLI hint/prompt 或拆小任务后复测,不把绕行作为合格完成。 对 MiniMax 的验收不能看 final response 自述。指挥官至少核验 diff、commit 是否在声明目标分支可 fetch、轻量验证命令是否真实存在、输出是否和任务范围匹配;遇到推测、遗漏、伪造证据或把失败写成成功时,先拆成更小任务或改派 GPT-5.5。 diff --git a/docs/reference/master-server-ops.md b/docs/reference/master-server-ops.md index 55390829..60cbbbf3 100644 --- a/docs/reference/master-server-ops.md +++ b/docs/reference/master-server-ops.md @@ -391,7 +391,7 @@ wrapper 负责 bridge 命令语法,本文只记录运行边界。 - route 在前,operation 在后; - 默认使用 `apply-patch` v2。 - 稳定写入形式: - - `trans :/absolute/workspace apply-patch < patch.diff`。 + - `trans :/absolute/workspace apply-patch <<'PATCH'`。 - 只读检查: - 使用 `trans :/absolute/workspace nl -ba file` 或同等有界命令。 - `apply-patch` 报告 `failed to find expected lines` 时: diff --git a/scripts/src/apply-patch-v2.test.ts b/scripts/src/apply-patch-v2.test.ts index e9999517..3e41f172 100644 --- a/scripts/src/apply-patch-v2.test.ts +++ b/scripts/src/apply-patch-v2.test.ts @@ -3,6 +3,7 @@ import { Readable, Writable } from "node:stream"; import { describe, expect, test } from "bun:test"; import { ApplyPatchV2Error, + applyPatchV2HelpPayload, runApplyPatchV2, type ApplyPatchV2BulkReplacementWritePlan, type ApplyPatchV2FileSystem, @@ -35,6 +36,34 @@ function applyReplacementPlan(original: string, plan: ApplyPatchV2BulkReplacemen } describe("apply-patch v2 stdin preflight", () => { + test("documents quoted heredoc input without a temporary patch file", () => { + const help = applyPatchV2HelpPayload(); + const serialized = JSON.stringify(help); + + expect(help.command).toContain("<<'PATCH'"); + expect(help.usage.every((example) => example.includes("<<'PATCH'"))).toBe(true); + expect(serialized).not.toMatch(/<\s+patch[.]diff/); + expect(serialized).not.toMatch(/\/tmp\/patch[.]diff/); + }); + + test("malformed input hint points directly to a quoted heredoc", async () => { + const stdout = new CaptureWritable(); + const stderr = new CaptureWritable(); + + const exitCode = await runApplyPatchV2({ + executor: {}, + stdin: Readable.from(["not a patch\n"]), + stdout, + stderr, + }); + + expect(exitCode).toBe(1); + expect(stdout.text()).toBe(""); + expect(stderr.text()).toContain("trans apply-patch <<'PATCH'"); + expect(stderr.text()).toContain("without a temporary patch file"); + expect(stderr.text()).not.toMatch(/\/tmp\/patch[.]diff/); + }); + test("fails immediately on an interactive TTY and points to a quoted heredoc", async () => { const stdin = Readable.from([]) as Readable & { isTTY?: boolean }; Object.defineProperty(stdin, "isTTY", { value: true }); diff --git a/scripts/src/apply-patch-v2.ts b/scripts/src/apply-patch-v2.ts index 2f72c870..b9fb1dd6 100644 --- a/scripts/src/apply-patch-v2.ts +++ b/scripts/src/apply-patch-v2.ts @@ -780,7 +780,7 @@ function appendParserFailureHint(lines: string[], message: string, details: Reco return; } if (message.startsWith("invalid patch:")) { - lines.push("Hint: apply-patch expects a single stdin patch whose first non-wrapper line is *** Begin Patch and last line is *** End Patch. Quoted heredocs such as `cat <<'PATCH' > /tmp/patch.diff` preserve the required leading spaces."); + lines.push("Hint: apply-patch expects a single stdin patch whose first non-wrapper line is *** Begin Patch and last line is *** End Patch. Send it directly with `trans apply-patch <<'PATCH'`; quoted heredocs preserve required leading spaces without a temporary patch file."); lines.push("Hint: apply-patch remains the v2 engine only; v1 is never auto-selected for malformed MiniMax/MXCX patch envelopes."); } } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 30e677be..b80f248b 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -33,7 +33,7 @@ export function rootHelp(): unknown { { command: "trans gh:/owner/repo[/pr|/issue][/number[/1]] ls|cat|rg|apply-patch", description: "Treat GitHub PRs/issues as virtual text directories; `issue ls --search/--state` covers filtered reads, `cat|rg` reads first-floor body text, and `apply-patch` updates `body.md` through UniDesk gh plus apply-patch v2." }, { command: "trans apply-patch <<'PATCH'", description: "Default remote text patch entry: send a quoted stdin heredoc directly to the local TypeScript v2 engine while the remote route only reads and writes files." }, { command: "trans upload|download [--allow-text-transfer] ...", description: "Transfer binary files or generated artifacts through SSH passthrough with automatic endpoint byte/SHA-256 verification; known text paths fail before transfer unless explicitly overridden." }, - { command: "trans apply-patch-v1 [tool args...] < patch.diff", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." }, + { command: "trans apply-patch-v1 [tool args...] <<'PATCH'", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from a quoted stdin heredoc." }, { command: "trans py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." }, { command: "trans sh|bash [arg...] <<'SH|BASH' ...", description: "Run a remote POSIX sh or Bash body from local stdin; the operation name declares the shell dialect, and removed `script`/`shell` operations fail with a migration hint." }, { command: "trans skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." }, @@ -264,7 +264,7 @@ const SSH_OPERATION_HELP: Record = { "apply-patch-v1": { group: "patch", description: "显式调用 legacy remote apply_patch helper;只作为人工确认后的兼容入口。", - usage: ["trans apply-patch-v1 [--allow-loose] < patch.diff"], + usage: ["trans apply-patch-v1 [--allow-loose] <<'PATCH'"], }, upload: { group: "transfer", @@ -429,10 +429,11 @@ function sshDownloadHelp(): unknown { }, }, examples: { - host: "trans D601:/tmp download /tmp/trace.json ./trace.json", - k3sWorkload: "trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json", - windowsDrive: "trans D601:win download 'D:\\tmp\\trace.json' ./trace.json", - explicitWorktree: `UNIDESK_TRANS_REPO_ROOT=${repoRoot} trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json`, + host: "trans D601:/tmp download /tmp/firmware.bin ./firmware.bin", + k3sWorkload: "trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/runtime-bundle.tar.gz ./runtime-bundle.tar.gz", + windowsDrive: "trans D601:win download 'D:\\tmp\\capture.zip' ./capture.zip", + explicitWorktree: `UNIDESK_TRANS_REPO_ROOT=${repoRoot} trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/runtime-bundle.tar.gz ./runtime-bundle.tar.gz`, + generatedTextOverride: "trans D601:/tmp download --allow-text-transfer /tmp/trace.json ./trace.json", }, notes: [ "A k3s download requires a workload route; the control-plane-only :k3s route cannot name a remote file inside a workload.", diff --git a/scripts/src/ssh-help.test.ts b/scripts/src/ssh-help.test.ts index ef19c85e..961d886d 100644 --- a/scripts/src/ssh-help.test.ts +++ b/scripts/src/ssh-help.test.ts @@ -79,11 +79,17 @@ describe("ssh bounded progressive help", () => { expect(scoped.usage[0]).toBe("trans cat "); expect(serialized).toContain("text-transfer-discouraged"); expect(serialized).toContain("--allow-text-transfer"); - expect(scoped.examples.host).toContain("download /tmp/trace.json ./trace.json"); + expect(scoped.examples.host).toContain("download /tmp/firmware.bin ./firmware.bin"); expect(scoped.examples.k3sWorkload).toBe( - "trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json", + "trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/runtime-bundle.tar.gz ./runtime-bundle.tar.gz", ); - expect(scoped.examples.windowsDrive).toContain("D:\\tmp\\trace.json"); + expect(scoped.examples.windowsDrive).toContain("D:\\tmp\\capture.zip"); + expect(scoped.examples.generatedTextOverride).toBe( + "trans D601:/tmp download --allow-text-transfer /tmp/trace.json ./trace.json", + ); + expect(scoped.examples.host).not.toMatch(/\.(?:json|md|txt|ya?ml)\b/); + expect(scoped.examples.k3sWorkload).not.toMatch(/\.(?:json|md|txt|ya?ml)\b/); + expect(scoped.examples.windowsDrive).not.toMatch(/\.(?:json|md|txt|ya?ml)\b/); }); test("renders compact top-level and scoped help without dump fallback", () => {