fix: strengthen apply-patch v2 mxcx hints
This commit is contained in:
@@ -33,7 +33,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P
|
||||
- `server rebuild <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` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `dev-frontend-proxy` 只更新主 server dev 入口薄代理,`todo-note`、`code-queue-mgr`、`project-manager`、`baidu-netdisk` 和 `oa-event-flow` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。D601 Code Queue 执行面不由 `server rebuild` 管理,Rust backend-core 迭代不得用 `server rebuild backend-core` 在 master server 编译,规则见 `docs/reference/dev-environment.md`。
|
||||
- `provider attach <providerId> [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-<ID>.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-<ID>.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage <providerId> [--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 <providerId> host.ssh --wait-ms 15000`、`trans <providerId> argv true`、`artifact-registry health --provider-id <providerId>`、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。
|
||||
- `trans <route> [operation args...]` / `tran <route> [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:<namespace>:<workload>`。WSL provider 的 Windows cmd 入口固定写 `trans D601:win cmd <command-line>`,需要 Windows cwd 时用 `trans D601:win/c/test cmd cd`,由 CLI 自动设置 `chcp 65001`、`PYTHONUTF8=1` 和 `PYTHONIOENCODING=utf-8`;命名只允许 `win`,不得使用 `win32`。非交互远端命令优先使用 `trans <providerId> argv ...`;需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `trans G14 script <<'SCRIPT'`、`trans G14:k3s script <<'SCRIPT'` 或 `trans G14:k3s:<namespace>:<workload> script <<'SCRIPT'`,把脚本走 stdin。`script -- '<单个字符串>'` 是无需 stdin 的远端 shell one-liner,例如 `trans G14:/root/hwlab script -- 'cd /root/hwlab && git status --short --branch'`;`script -- <多个 argv>` 才是 direct argv,适合 `trans D601:/path script -- sed -n '1,20p' file` 这类带短横线的单进程命令。顶层 remote option parser 必须保留命令已经开始后的 `--`,不得把它吞成全局选项结束符。需要远端改文本文件时默认优先使用 `<route> apply-patch < patch.diff`;需要可靠传输非文本或整文件时使用 `<route> upload <local-file> <remote-file>` 和 `<route> download <remote-file> <local-file>`,CLI 会按字节数与 SHA-256 自动校验并在 provider-gateway stdin/argv 限制下切换客户端分块策略;需要旧 helper 时显式使用 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 或 `<providerId> apply-patch-v1`。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。
|
||||
- `trans <route> 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。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`trans <providerId> apply-patch-v1 [tool args...] < patch.diff` 保留为 v1 fallback,直接调用远端注入的 `apply_patch` sh/perl helper;只有默认 v2 引擎出现问题、需要复用旧 helper 行为或人工确认 `--allow-loose` 时才优先使用 v1。
|
||||
- `trans <route> 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`。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`trans <providerId> apply-patch-v1 [tool args...] < patch.diff` 保留为显式 legacy 入口,直接调用远端注入的 `apply_patch` sh/perl helper;默认 `apply-patch` 不把 v1 当 fallback。
|
||||
- `trans <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。
|
||||
- `trans <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。
|
||||
- `trans <providerId>:k3s[:namespace:workload[:container]] <operation> ...` 是原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec、logs 和 pod workspace 读写参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号;D601 与 G14 都有 provider-specific guard,分别校验 `d601` 和 G14 k3s 节点身份。
|
||||
|
||||
@@ -126,7 +126,8 @@ export function applyPatchV2HelpPayload() {
|
||||
"A blank line in Add File is a line containing only +.",
|
||||
"Update File uses @@ or @@ context markers, followed by context lines starting with one extra space prefix and changed lines starting with - or +; for a column-0 source line `const x`, write ` const x`, and for a two-space-indented source line write three spaces total. Unified-diff line-range headers are accepted with hints for MiniMax compatibility.",
|
||||
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content.",
|
||||
"MiniMax compatibility: stray @@ or unprefixed content inside Add File, unprefixed Update File context lines, and extra hunk/body lines after Delete File, are accepted with stderr hints."
|
||||
"MiniMax compatibility: stray @@ or unprefixed content inside Add File, unprefixed Update File context lines, and extra hunk/body lines after Delete File, are accepted with stderr hints.",
|
||||
"MiniMax/MXCX concatenated patch compatibility: repeated nested Begin Patch / End Patch markers are accepted with stderr hints, but the CLI still uses only the v2 engine and never auto-falls back to apply-patch-v1."
|
||||
],
|
||||
examples: {
|
||||
addFile: [
|
||||
@@ -151,6 +152,7 @@ export function applyPatchV2HelpPayload() {
|
||||
commonPitfalls: [
|
||||
"Do not put @@ after `*** Add File:`; @@ is only for Update File.",
|
||||
"Prefer canonical @@ or @@ context over unified diff headers such as `@@ -1,3 +1,4 @@`; v2 accepts those headers with a hint.",
|
||||
"If multiple printf/heredoc fragments were concatenated, keep one outer Begin/End envelope or include complete nested envelopes; v2 will hint on nested markers instead of trying the legacy helper.",
|
||||
"Do not use remote Python/Perl/sed heredocs for text patches when `trans <route> apply-patch` is available."
|
||||
],
|
||||
note: "apply-patch reads patch text from stdin and uses the v2 engine by default. Use `apply-patch-v1` only for the legacy helper."
|
||||
@@ -420,6 +422,7 @@ function formatApplyPatchFailure(error: unknown): string {
|
||||
const details = error instanceof ApplyPatchV2Error ? error.details : {};
|
||||
const outcomes = Array.isArray(details.outcomes) ? details.outcomes as ApplyPatchV2Outcome[] : [];
|
||||
const lines = [`${message.trimEnd()}`];
|
||||
appendParserFailureHint(lines, message, details);
|
||||
appendExpectedLinesFailureHint(lines, details);
|
||||
if (outcomes.length > 1 || outcomes.some((item) => item.status === "applied")) {
|
||||
lines.push("Patch status:");
|
||||
@@ -429,6 +432,21 @@ function formatApplyPatchFailure(error: unknown): string {
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
function appendParserFailureHint(lines: string[], message: string, details: Record<string, unknown>): void {
|
||||
const text = typeof details.text === "string" ? details.text : "";
|
||||
const parserLine = typeof details.line === "number" ? `line ${details.line}` : "";
|
||||
if (message === "invalid hunk header") {
|
||||
const shown = text.length === 0 ? "" : ` (${JSON.stringify(text)})`;
|
||||
lines.push(`Hint: unexpected patch body ${parserLine}${shown}. For concatenated MiniMax/MXCX fragments, keep exactly one outer *** Begin Patch / *** End Patch envelope or make each nested fragment a complete Begin/End pair.`);
|
||||
lines.push("Hint: apply-patch remains the v2 engine only; fix the patch text or run `trans <route> apply-patch --help`, do not retry by switching to apply-patch-v1.");
|
||||
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 remains the v2 engine only; v1 is never auto-selected for malformed MiniMax/MXCX patch envelopes.");
|
||||
}
|
||||
}
|
||||
|
||||
function appendExpectedLinesFailureHint(lines: string[], details: Record<string, unknown>): void {
|
||||
const cause = recordValue(details.cause) ?? details;
|
||||
const expected = typeof cause.expected === "string" ? cause.expected : "";
|
||||
@@ -439,6 +457,7 @@ function appendExpectedLinesFailureHint(lines: string[], details: Record<string,
|
||||
appendQuotedBlock(lines, expected, 20, 1600);
|
||||
appendExpectedLineDiagnostics(lines, cause);
|
||||
lines.push("Hint: re-read the target file around this hunk. In Update File hunks, every context line needs a leading space prefix; for a column-0 source line like `]);`, write ` ]);`.");
|
||||
lines.push("Hint: this is still handled by apply-patch v2. Correct the hunk context or prefixes; do not switch the same patch to apply-patch-v1.");
|
||||
}
|
||||
|
||||
function appendExpectedLineDiagnostics(lines: string[], cause: Record<string, unknown>): void {
|
||||
|
||||
@@ -1071,11 +1071,37 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
missingPlusLargeInsertVisibleV2.stderr.includes("First expected line appears near target line(s): 2, 5")
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("Best partial context match: 2 expected line(s) matched")
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("large insertion whose new lines were written as context")
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("regenerate the patch instead of editing it with sed"),
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("regenerate the patch instead of editing it with sed")
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("handled by apply-patch v2")
|
||||
&& missingPlusLargeInsertVisibleV2.stderr.includes("do not switch the same patch to apply-patch-v1"),
|
||||
"v2 missing-plus failure should diagnose the MiniMax sed-regression pattern explicitly",
|
||||
missingPlusLargeInsertVisibleV2.stderr,
|
||||
);
|
||||
|
||||
const fragmentedEnvelopeFailureV2 = await applyPatchV2FixtureAttempt([
|
||||
"*** Begin Patch",
|
||||
"*** Update File: fragment.txt",
|
||||
"@@",
|
||||
"-alpha",
|
||||
"+ALPHA",
|
||||
"*** End Patch",
|
||||
"printf '%s\\n' '*** Begin Patch'",
|
||||
"*** End Patch",
|
||||
"",
|
||||
].join("\n"), {
|
||||
"fragment.txt": "alpha\n",
|
||||
}, { stderrOutput: true });
|
||||
assertCondition(fragmentedEnvelopeFailureV2.exitCode === 1 && fragmentedEnvelopeFailureV2.error === null, "v2 should reject non-patch shell fragments with a parser hint", fragmentedEnvelopeFailureV2);
|
||||
assertCondition(
|
||||
fragmentedEnvelopeFailureV2.stderr.includes("invalid hunk header")
|
||||
&& fragmentedEnvelopeFailureV2.stderr.includes("concatenated MiniMax/MXCX fragments")
|
||||
&& fragmentedEnvelopeFailureV2.stderr.includes("exactly one outer *** Begin Patch / *** End Patch envelope")
|
||||
&& fragmentedEnvelopeFailureV2.stderr.includes("v2 engine only")
|
||||
&& fragmentedEnvelopeFailureV2.stderr.includes("do not retry by switching to apply-patch-v1"),
|
||||
"v2 parser failure should hint the MXCX printf/heredoc envelope fix without falling back to v1",
|
||||
fragmentedEnvelopeFailureV2.stderr,
|
||||
);
|
||||
|
||||
const nestedEnvelopeV2 = await applyPatchV2FixtureAttempt([
|
||||
"*** Begin Patch",
|
||||
"*** Update File: nested-envelope.txt",
|
||||
|
||||
Reference in New Issue
Block a user