From ddd1a1e90db14847f3e0023ea51fd32598efe531 Mon Sep 17 00:00:00 2001 From: tizerluo <192086140+tizerluo@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:27:54 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=20CLI=200.17.0?= =?UTF-8?q?=20(App=203.16.2)=20=E2=80=94=20list=20=E5=8E=BB=E9=94=AE/?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20ID=20=E6=8A=98=E5=8F=A0/generateText=20sel?= =?UTF-8?q?ection=20=E6=98=A0=E5=B0=84=20+=20=E6=96=87=E6=A1=A3=E5=9B=9B?= =?UTF-8?q?=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 探测面: 核心链路 (create/subscribe/send/流式/list)、删除面 12 项、信封、 反向调用、headless CLI 与 0.16.9 逐项一致; 三处破坏修复: 1. session/list 去 includeArchived — 0.17.0 strict Zod 拒收致桥 -32603。 0.16.9 (npm 3.15.3-37 vendor 实测) {} 与 {includeArchived:false/true} 逐项等价, era 无关去键; 客户端传 true 不再透传 (archived 概念疑似 已移除), docstring 记取舍。 2. 模型 ID 大小写折叠 — 0.17.0 注册表身份改名 (builtin:zai-coding-plan/ GLM-5.3 → zai/glm-5.3, 精确大小写敏感) 且显式选模型强制 options.reasoningLevel。catalog 解析失败后按 casefold 第二轮匹配, 命中用 catalog 规范 ID (调用方原始串绝不下发); 旧习惯 'GLM-5.3' 在 0.17.0 自动解析到 'glm-5.3', 0.16 后端精确轮先命中不受影响。 不做 providerId 前缀映射魔法 (无法从 catalog 泛化推导, 危险)。 3. workspace/generateText modelRef→selection 键映射 — 首发 selection (0.17.0), -32602 且键级拒收特征 (modelRef/selection+unrecognized) 时一次性重发 modelRef 形态 (0.16 回退); 不双发不发明默认值。 maxOutputTokens 之谜实证: 服务端 selection 构建不再提供默认值 (同号 0.16.9 构建已分叉, 同号漂移再添实锤), 可通组合 = 显式 maxOutputTokens ∈ [1024, catalog max] + reasoningLevel 取 defaultLevel。 文档: recheck-3.16.2.md (新, 含最小环境引导验证: #57 原配方在 0.17.0 失效, 两条替代路径已记录)/README 兼容表+模型面+setModel 注记/agent-help version→0.17.0+旗标面+口径/SKILL 版本表。 测试: +15 (606 passed + 58 subtests); 真机 E2E: session/new('GLM-5.3') 折叠生效、session/list 恢复 (50 会话在列映射正确)。 --- README.md | 17 +- docs/recheck-3.16.2.md | 104 ++++++++++ packages/acp-bridge/zcode-acp-bridge | 168 +++++++++++++--- packages/agent-help/zcode-agent-help | 53 +++-- skills/zcode-bridge-guide/SKILL.md | 9 +- tests/test_app_server_methods.py | 289 ++++++++++++++++++++++++++- 6 files changed, 590 insertions(+), 50 deletions(-) create mode 100644 docs/recheck-3.16.2.md diff --git a/README.md b/README.md index 75bdb8f..9945c55 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 | `session/setModel` | 切换会话模型 | 0.14.8 | `{sessionId, model}` — model 是 ModelSelection 对象 `{providerId, modelId, options?: {reasoningLevel}}`(0.16 后端 schema 要对象;旧 `modelId` 字符串形态从未通过 0.16 schema,已移除)。缺 `reasoningLevel` 时部分模型 turn 阶段报 `ModelProtocolError: Reasoning level is required` | | `session/setMode` | 切换会话权限模式 | 0.14.8 | `{sessionId, mode}` | -> **session/new 可选 `model` 参数**:`{cwd, mode?, model?}` —— model 可传 catalog 里的 modelId 字符串(如 `"GLM-5.3"`,按 create 快照解析成完整 ModelSelection;catalog 条目带 `reasoning.defaultLevel` 时补默认 reasoningLevel)或完整对象(`{providerId, modelId, options?}`,两者须为非空字符串;`model: null` 或省略等同未提供)。create 成功后桥内部经 `session/setModel` 应用,失败则 session/new 整体报错(文案带已创建的 sessionId,且该会话不登记、不可经 session/prompt 使用)。会话创建即锁定模型,不依赖 App 侧「上次使用」默认(headless 场景刚需)。不透传给 create 本身:0.16.9 实测 create 的初始 model 形参丢 options。 +> **session/new 可选 `model` 参数**:`{cwd, mode?, model?}` —— model 可传 catalog 里的 modelId 字符串(如 `"GLM-5.3"`,按 create 快照解析成完整 ModelSelection;catalog 条目带 `reasoning.defaultLevel` 时补默认 reasoningLevel;0.17.0 起精确未命中再按 casefold 折叠匹配并取 catalog 规范 ID——旧大写习惯 `"GLM-5.3"` 在 0.17.0 小写 catalog 上自动解析到 `glm-5.3`,0.16 大写 catalog 精确先命中、行为不变)或完整对象(`{providerId, modelId, options?}`,两者须为非空字符串,原样透传不做 ID 改写;`model: null` 或省略等同未提供)。create 成功后桥内部经 `session/setModel` 应用,失败则 session/new 整体报错(文案带已创建的 sessionId,且该会话不登记、不可经 session/prompt 使用)。会话创建即锁定模型,不依赖 App 侧「上次使用」默认(headless 场景刚需)。不透传给 create 本身:0.16.9 实测 create 的初始 model 形参丢 options。 > **session/new 可选 `toolAllowlist` / `toolDenylist` 参数**:字符串数组,在会话创建时按名过滤工具(**工具集注册级物理过滤**:先于权限层、与 mode 无关,hook/权限确认都覆盖不了)。键缺省 → 不写入 create 参数(引擎默认全量工具,与旧行为一致);键存在 → 每个元素须为 strip 后非空的字符串,非法(非数组、元素非字符串、空串,**含显式 `null`**)本地 `-32602` 且不触碰后端——fail-closed,安全参数不静默忽略。条目 strip 后透传,字面 `[]` 原样透传(空 allowlist = 全部禁用,空 denylist = 无限制)。allow+deny 同传时引擎取**交集且 deny 优先**。用于只读监督:0.16 stdio 下 `mode=plan` 只是 advisory(0.16.1 实测工具照常自动执行,快照 diff 只是事后发现),名单是唯一的会话级前置只读手段;建议同时 deny Node REPL 族(`js` / `js_reset` / `js_add_node_module_dir` / `mcp__node_repl__js*`),否则可经 `execSync` 打穿 `Bash` 黑名单(先例见上文 `zcode_review` 只读原理)。 @@ -182,7 +182,7 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 | 扩展方法 | 作用 | params | |----------|------|--------| | `workspace/readState` | 读工作区状态(settings/modelCatalog/slashCommands) | `{workspace, runtimeModel?}` | -| `workspace/generateText` | 一次性文本生成(不建会话) | `{workspace, modelRef, prompt, querySource, maxOutputTokens?, temperature?}` | +| `workspace/generateText` | 一次性文本生成(不建会话) | `{workspace, modelRef, prompt, querySource, maxOutputTokens?, temperature?}`(桥把 modelRef 映射为后端的 `selection` 键发送——selection 构建起 `maxOutputTokens` 必填、`options.reasoningLevel` 必填(zai 系)、`temperature` 已删;见下行注) | | `workspace/setDefaultModel` | 设工作区默认模型(持久化) | `{workspace, model, runtimeModel?, expectedWorkspaceRevision?}` | | `workspace/setDefaultMode` | 设工作区默认权限模式(持久化) | `{workspace, mode, expectedWorkspaceRevision?}` | | `workspace/setDefaultThoughtLevel` | 设工作区默认思考强度(持久化) | `{workspace, thoughtLevel, expectedWorkspaceRevision?}` | @@ -190,7 +190,9 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 | `workspace/removeModelProvider` | 移除模型供应商 | `{workspace, providerId, expectedWorkspaceRevision?}` | | `workspace/updateProviderRegistry` | 批量更新供应商注册表 | `{workspace, registry, includeWorkspaceState?}` | -> ❌ **App 3.12.3 的 0.16.5 构建已删**:除 `workspace/generateText`(存活)外,上表 7 个 workspace/* 方法均已从 app-server 删除(2026-09-17 实测后端 -32601,且未搬家到 session/ 命名空间);`updateInteractionPreferences` 同批删除。0.16.9(App 3.14.0)复测同面(2026-09-19)。上表语义适用于 0.15.0 – App 3.10.2 的 0.16.5 构建。桥透传调用会收到「当前 ZCode 版本已移除该能力」降级文案(见下方降级行为)。 +> ❌ **App 3.12.3 的 0.16.5 构建已删**:除 `workspace/generateText`(存活)外,上表 7 个 workspace/* 方法均已从 app-server 删除(2026-09-17 实测后端 -32601,且未搬家到 session/ 命名空间);`updateInteractionPreferences` 同批删除。0.16.9(App 3.14.0)复测同面(2026-09-19);0.17.0(App 3.16.2)复测同面(2026-10-20)。上表语义适用于 0.15.0 – App 3.10.2 的 0.16.5 构建。桥透传调用会收到「当前 ZCode 版本已移除该能力」降级文案(见下方降级行为)。 +> +> ⚠️ **`workspace/generateText` 的 selection 语义(早于 0.17.0 引入,0.17.0 延续)**:后端参数键已由 `modelRef` 改名 `selection`,且(实测 App 3.15.3 的同号 0.16.9 构建与 0.17.0 一致)`maxOutputTokens` **必填**(省略报 `maxOutputTokens is outside the model option range`;实测 1024 ≤ 值 ≤ catalog maxOutputTokens 可用)、zai 系模型 `selection.options.reasoningLevel` **必填**(取 catalog `reasoning.defaultLevel`)、`temperature` **已删**。调用方接口保持 `modelRef` 不变(桥负责映射),老构建(≤0.16.5 只收 modelRef)由桥按后端错误特征一次性回退。省略必填项时错误透传并附修法指引;桥不替调用方发明默认值。 **prompt 级**(提示词增强,App 3.3.0 引入;❌ **0.16 已全部移除**,无替代): @@ -230,6 +232,7 @@ PR 自动审查闸门守护进程(第 4 组件,experimental):常驻轮 **canonical model id = `~/.zcode/v2/config.json` 里 `models` 的 key 原样**(如 `GLM-5.3`),**不加 provider 前缀**。`shared/credentials.py`、MCP server、ACP bridge、agent-help 四处统一用原始 id。实测(0.16.1 时代)`zai/GLM-5.2` 前缀形式也兼容,但非 canonical,本项目不使用。 模型面现状(App 3.12.3 的 0.16.5 构建实测,2026-09-17;0.16.9/App 3.14.0 于 2026-09-19 在完整桌面配置机器上复测一致;最小 headless 环境的模型引导见下方新节):当前 enabled provider(`builtin:zai-coding-plan`)的 models 为 `GLM-5.3` / `GLM-5.3-Flash`(`GLM-5-Turbo` 已由服务端移除;3.10.2 时代为三者)。 +**0.17.0(App 3.16.2)更新(2026-10-20)**:headless env / config 的模型 key 口径不变(仍 `GLM-5.3` 大写,实测可用);但 **app-server 的模型目录(create 快照 `settings.model.available` / setModel / generateText)已改名小写 `zai/glm-5.3`**(providerId `zai`,精确大小写敏感),且 **`glm-5-turbo` 回归** catalog(200k 上下文 / maxOutputTokens 64,000 / reasoningLevel enabled)。zai 系模型显式选模型强制 `options.reasoningLevel`。桥的 session/new 字符串 model 两侧写法都能解析(精确命中优先,未命中按 casefold 折叠并取 catalog 规范 ID)。 ### 凭证注入:显式环境变量优先 @@ -257,7 +260,8 @@ ZCODE_BASE_URL=https://api.z.ai/api/anthropic ./packages/mcp-server/zcode-mcp-se 在有完整桌面配置的机器上,模型选择由 `~/.zcode` 下的配置文件兜底,桥的 env 注入是否生效不可分辨;但在**最小环境**(空 HOME、仅入口旁有内置 provider 文件)实测中,官方 runtime **完全不读取** `ZCODE_MODEL` / `ZCODE_BASE_URL` / `ANTHROPIC_API_KEY`,`--prompt` 直接失败退出。服务器/容器部署需要按本节准备: -**前置一步:内置 provider 配置文件**。官方 runtime 按**调用入口路径(不解析符号链接)**探测 `<入口目录>/provider/zcode-builtin.json`。部署时需把官方发行物中的该文件(连同 `provider/` 目录)放到调用入口旁,否则 `--prompt` 报 `无法定位 CLI ZCode Built-in Provider Config`。 +**前置一步:内置 provider 配置文件**。官方 runtime 按**调用入口路径**探测 `<入口目录>/provider/zcode-builtin.json`。部署时需把官方发行物中的该文件(连同 `provider/` 目录)放到调用入口旁,否则 `--prompt` 报 `无法定位 CLI ZCode Built-in Provider Config`。 +> ⚠️ **0.17.0(App 3.16.2)机制变化(2026-10-20 实测)**:runtime 把入口解析到**真实路径**,「不解析符号链接」的老结论不再成立——符号链接入口 + 邻置 `provider/` 的组合失效(报错原文里的候选路径即真实路径)。两条实测可行路径:① 入口用**真实文件**(复制而非链接 `zcode.cjs`)+ 邻置 `provider/`(原配方,入口换真实文件即可);② 桌面 App 同款显式指路 env:`ZCODE_BUILTIN_PROVIDER_CONFIG_FILE=` + `ZCODE_PERSONAL_PROVIDER_CONFIG_FILE=`(入口可以是符号链接)。另外复测「最小环境」务必用**仅 HOME/PATH 的干净 env**——桌面 App 会话里的 `ZCODE_*` 注入变量会把失败 case 污染成假阳性。 **模型选择两条可行路径**: 1. **宿主机先完成一次模型选择**(推荐):桌面 App 登录并选择模型会生成 `~/.zcode/v2/provider_config.json` 注册表(含加密凭证),把整个 `~/.zcode` 配置带到服务器即可;也可以在该文件的 `config.defaultModelSelection` 写 `{"providerId": ..., "modelId": ...}` 指定默认模型。 @@ -333,6 +337,7 @@ ACP bridge 侧另有一个 env(不在上两表,仅 ACP 用):`ZCODE_ACP_D | ZCode CLI 版本 | 支持情况 | ACP bridge 流式 | 扩展方法 | |:--------------:|:--------:|:---------------:|:--------:| +| **0.17.0**(App 3.16.2) | ✅ 完整(三处面破坏已由桥适配:session/list 去 `includeArchived`、模型 ID 改名 `zai/glm-5.3` 小写 + 字符串 model 大小写折叠回退、generateText `modelRef`→`selection` 键映射,见[^bootstrap]与 [docs/recheck-3.16.2.md](docs/recheck-3.16.2.md)) | **真流式**(事件驱动) | ✅ session/*(workspace/* 仍删 7/8 仅 generateText 存活;CLI 旗标面唯一确证新增 `--toolsearch-mode`,`--disallowed-tools` 标签带 camelCase 别名,见 agent-help) | | **0.16.9**(App 3.14.0) | ✅ 完整(核心面与 3.12.3 的 0.16.5 构建一致;最小 headless 环境的模型引导需配置文件,见[^bootstrap]) | **真流式**(事件驱动) | ✅ session/*(workspace/* 仍删 7/8 仅 generateText 存活;CLI 旗标面增删:`--permission-mode`/`--allow-main-worktree-yolo` 已移除,新增 `--cwd`/`--target-replace` 等,见 agent-help) | | **0.16.5**(App 3.12.3,同版本号构建漂移) | ✅ 完整(核心面) | **真流式**(事件驱动) | ✅ session/*(workspace/* 删 7/8 仅 generateText 存活;updateRuntimeModelConfig/updateInteractionPreferences 已删,透传优雅降级) | | **0.16.5**(App 3.10.2) | ✅ 完整 | **真流式**(事件驱动) | ✅ session/* + workspace/*(与 0.16.1 同面;`automation/*` 未实现不受其删除影响) | @@ -345,9 +350,9 @@ ACP bridge 侧另有一个 env(不在上两表,仅 ACP 用):`ZCODE_ACP_D | **0.14.5 ~ 0.14.7** | ✅ 兼容 | 伪流式(自动降级轮询) | ❌(旧版协议未实现) | | **< 0.14.5** | ⚠️ 未测 | — | — | -> 注:CLI 版本号相同不代表协议面相同——`prompt/enhance` 是 App 3.3.0 引入的协议方法(CLI 同为 0.15.0,仅 App 3.3.0+ 的 app-server 支持),又于 0.16 整体移除,仅 0.15.0 + App ≥ 3.3.0 的组合可用。0.16.1(App 3.6.5)协议面大改——真正断点是反向调用必须应答、事件模型调整、删除 steer/rewind/enhance(信封去 `jsonrpc`/方法 rename/`deliveryKind` 必填同为协议事实,但桥对内本就用这套调用面),详见 [docs/upgrade-0.16.1-spec.md](docs/upgrade-0.16.1-spec.md)(含勘误)。0.16.5 已于 2026-09-01 全链路复测(协议面兼容、桥无需代码改动),详见 [docs/recheck-0.16.5.md](docs/recheck-0.16.5.md)。App 3.12.3 的内嵌 CLI `--version` 仍为 0.16.5 但**构建内容漂移**(同号删了 workspace/* 7/8 等,`--version` 不再是唯一兼容性判据),已于 2026-09-17 复测,详见 [docs/recheck-3.12.3.md](docs/recheck-3.12.3.md)。0.16.9(App 3.14.0)已于 2026-09-19 复测:协议面与 3.12.3 的 0.16.5 构建完全一致、桥零改动;变化仅在 CLI 旗标面(`--allowed-tools`/`--max-turns` 连帮助文案一并消失,`--permission-mode`/`--allow-main-worktree-yolo` 移除,新增 `--cwd`/`--target-replace`/`--browser-use` 等),详见 [docs/recheck-3.14.0.md](docs/recheck-3.14.0.md)。 +> 注:CLI 版本号相同不代表协议面相同——`prompt/enhance` 是 App 3.3.0 引入的协议方法(CLI 同为 0.15.0,仅 App 3.3.0+ 的 app-server 支持),又于 0.16 整体移除,仅 0.15.0 + App ≥ 3.3.0 的组合可用。0.16.1(App 3.6.5)协议面大改——真正断点是反向调用必须应答、事件模型调整、删除 steer/rewind/enhance(信封去 `jsonrpc`/方法 rename/`deliveryKind` 必填同为协议事实,但桥对内本就用这套调用面),详见 [docs/upgrade-0.16.1-spec.md](docs/upgrade-0.16.1-spec.md)(含勘误)。0.16.5 已于 2026-09-01 全链路复测(协议面兼容、桥无需代码改动),详见 [docs/recheck-0.16.5.md](docs/recheck-0.16.5.md)。App 3.12.3 的内嵌 CLI `--version` 仍为 0.16.5 但**构建内容漂移**(同号删了 workspace/* 7/8 等,`--version` 不再是唯一兼容性判据),已于 2026-09-17 复测,详见 [docs/recheck-3.12.3.md](docs/recheck-3.12.3.md)。0.16.9(App 3.14.0)已于 2026-09-19 复测:协议面与 3.12.3 的 0.16.5 构建完全一致、桥零改动;变化仅在 CLI 旗标面(`--allowed-tools`/`--max-turns` 连帮助文案一并消失,`--permission-mode`/`--allow-main-worktree-yolo` 移除,新增 `--cwd`/`--target-replace`/`--browser-use` 等),详见 [docs/recheck-3.14.0.md](docs/recheck-3.14.0.md)。0.17.0(App 3.16.2)已于 2026-10-20 复测:核心链路零改动兼容,但有三处面破坏(`session/list` 拒收 `includeArchived`、模型 ID 改名 `zai/glm-5.3` 小写、`workspace/generateText` 参数键 `modelRef`→`selection`),桥已适配,详见 [docs/recheck-3.16.2.md](docs/recheck-3.16.2.md)。 > -> ⚠️ **`session/setModel` 对象形态的适用范围**:仅在 0.16.x 验证(0.16.9 实测可用);≤0.15 后端未验证该形态(历史 `{modelId}` 字符串形态在 0.16 schema 下必拒,故桥已切换为对象形态——这是破坏性变更,旧版调用方需注意)。 +> ⚠️ **`session/setModel` 对象形态的适用范围**:仅在 0.16.x 验证(0.16.9 实测可用);≤0.15 后端未验证该形态(历史 `{modelId}` 字符串形态在 0.16 schema 下必拒,故桥已切换为对象形态——这是破坏性变更,旧版调用方需注意)。**0.17.0(App 3.16.2)起两处收紧**:模型身份改名——旧 `builtin:zai-coding-plan/GLM-5.3` → 新 **`zai/glm-5.3`**(小写、精确大小写敏感,大写报「不存在 Model」);zai 系模型 **`options.reasoningLevel` 强制必填**(缺它报 `Reasoning level is required for zai/glm-5.3`,可取 create 快照 catalog 的 `reasoning.defaultLevel`)。桥的 `session/new` 字符串 model 有大小写折叠回退(用 catalog 规范 ID),旧习惯 `"GLM-5.3"` 在 0.17.0 上自动解析;对象形态不做任何 ID 改写,传旧 providerId 由后端报错透传。 [^bootstrap]: 最小 headless 环境不能依赖 env 注入完成模型引导(0.16.9 官方 runtime 实测 env 不被读取),需按「最小 headless 环境的模型引导」准备 `provider/zcode-builtin.json` 与 `~/.zcode/v2/provider_config.json`;否则 mcp-server 返回结构化 `bootstrap` 键、review-gate 按终态告警。 diff --git a/docs/recheck-3.16.2.md b/docs/recheck-3.16.2.md new file mode 100644 index 0000000..487965c --- /dev/null +++ b/docs/recheck-3.16.2.md @@ -0,0 +1,104 @@ +# zcode-open-bridge 复测报告 — ZCode App 3.16.2 / CLI 0.17.0 + +日期:2026-10-20 +调研人:主代理 + 实现代理(Mac studio) +仓库: + +## 结论先行 + +- **App 3.16.2 内嵌 CLI `--version` = 0.17.0**(版本号自 0.16.9 再动)。**核心链路零改动兼容**:信封行为、反向调用、create→subscribe(deliveryKind=desktop-continuous)→send→事件流→stop→resume、删除面 12 项、headless CLI 全部实测通过;但**三处面破坏,本 PR 已在桥内适配**(见下): + 1. `session/list` 的 strict Zod 拒收 `includeArchived` 键(桥原无条件发 `false` 使 list 整体失败)→ 桥去键(era 无关); + 2. 模型身份改名 `builtin:zai-coding-plan/GLM-5.3` → **`zai/glm-5.3`**(小写、精确大小写敏感)→ 桥的 session/new 字符串 model 增加 casefold 折叠回退(catalog 中介,用规范 ID); + 3. `workspace/generateText` 参数键 `modelRef` → **`selection`**(且该改名**早于 0.17.0**)→ 桥先发 selection、键级拒收特征时一次性回退 modelRef。 +- 社区包装器 `zcode-app-cli` 3.16.2-38 已 vendor `zcode-runtime` 0.17.0(2026-10-20 当天升级),与官方 App 3.16.2 在桥相关面上实测行为一致(`includeArchived` 同样被拒、`{}`/`{limit}`/`{workspace}` 同样接受)。「同号构建漂移」再添实锤:`zcode-app-cli` 3.15.3-37 vendor 的 0.16.9 与官方 App 3.14.0 的 0.16.9 构建内容不同(前者 generateText 已收 selection、help 已列 `--mode guarded` 与 `/expert`)。 +- CLI 旗标面:与 App 3.15.3 的 vendor 0.16.9 逐项 diff 后**唯一确证新增 `--toolsearch-mode `**(默认 off);`--disallowed-tools` 的 help 标签现带 camelCase 别名形态(`--disallowed-tools, --disallowedTools `)。slash 命令 15 个不变。 +- 最小 headless 环境:**模型引导配方在 0.17.0 上仍可用,但机制注记需修正**——0.17.0 runtime 把入口解析到真实路径(符号链接不再保持入口目录),符号链接入口 + 邻置 `provider/` 的组合失效;两条实测可行路径见 §7。 + +## 实证环境 + +| 平台 | App | CLI | CLI 路径 | 备注 | +| --- | --- | --- | --- | --- | +| macOS arm64(Mac studio) | 3.16.2(CFBundleShortVersionString,build 3.16.2.8769) | 0.17.0 | `/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs` | 官方构建,经 `~/.local/bin/zcode-app` 符号链接可用 | +| (PATH 干扰项) | — | zcode-app-cli 3.16.2-38(社区,vendor zcode-runtime 0.17.0) | `~/.npm-global` 的 `zcode` | 2026-10-20 午间起与官方同 runtime;当日早些时候的「社区 0.16.9 后端」指认已失效 | +| (回溯用) | — | zcode-app-cli 3.15.3-37(社区,vendor zcode-runtime 0.16.9) | npm 可复装(`npm i zcode-app-cli@3.15.3-37`) | 用于 0.16.9 等价性实证与旗标 diff;注意 3.14.0-27 的 vendor runtime 在本机当前 `~/.zcode` 状态下无法启动(其内置 `provider/zcode-builtin.json` 的 `providerRules[8].access.mode` 被 0.16.9 schema 拒收——包装器打包 artifact,与本仓无关) | + +## ✅ 实测验证通过(零改动兼容面) + +### 1. 测试套件 + +适配后 `python3.14 -m pytest tests/ -q` → **606 passed + 58 subtests**(适配前基线 591 + 56;+15 用例见 §4)、`ruff check packages/ tests/` 全绿。 + +### 2. app-server 协议(活体探测,stdio 发无 jsonrpc 信封的 NDJSON) + +- **旧信封探测**:含 `jsonrpc` 键的请求 → `-32600`,错误帧 id 仍为固定 `"invalid-message"`——桥的纪元探测不受影响。 +- **反向调用**仍是 `session/requestRuntimePreferences`,必须以**无 jsonrpc 键**的三布尔体应答(带 jsonrpc 键被 -32600 拒收)——桥现役应答体直接可用。 +- **`session/create`**(workspace 必须 object `{workspacePath, workspaceKey}`):result keys 含 messages/projection/protocol/runtime/session/settings/slashCommands/todos/todoGroups;`protocol = {name:"ZCode Protocol", version:1}`;sessionId 在 `result.session.sessionId`。 +- **核心链路**(原始协议 + 经桥 E2E 两种方式实测):create → subscribe(deliveryKind=desktop-continuous + includeSnapshot + afterSeq)→ send `{sessionId, content}` → 事件流(turn.started/model.streaming/…)→ stop/list/resume 全通。`session/list` 裸参数 `{}`、`{workspace}`、`{limit}` 三形态全接受;result.session 条目 keys = [createdAt, mode, sessionId, sessionKind, status, title, titleSource, traceId, updatedAt, workspace]。 +- **新通知**(桥走通用忽略路径,无害):`process/mcpTelemetry`、`v4/telemetry/event`、`computer-use/operation-event`、`session.titleUpdated`。 +- **headless CLI**:`--prompt/--mode yolo/--json` 全通;`--json` 顶层 keys 不变,usage 新 keys = `source`/`modelRequestCount`/`webFetchRequests`/`webSearchRequests`/`reasoningTokens`(`totalTokens`/`cacheRead/WriteTokens` 仍在)。实测 `response="收到"`(完整桌面配置机器)。 + +### 3. 方法存活性(空 params → -32602 即存活 / -32601 即已删) + +- **存活**:`session/setModel`、`setMode`、`setThoughtLevel`、`fork`、`compact`、`goal`、`cancelBackgroundTask`、`workspace/generateText`、`session/list`、`session/update`——与 0.16.9 基线一致。 +- **已删(-32601,删除面 12 项不变)**:`workspace/readState`、`setDefaultModel`、`setDefaultMode`、`setDefaultThoughtLevel`、`upsertModelProvider`、`removeModelProvider`、`updateProviderRegistry`(workspace/* 8 删 7);`session/updateRuntimeModelConfig`;`updateInteractionPreferences`;`session/steer`、`session/rewind`、`prompt/enhance`。桥的泛化降级文案无需调整。 + +## ⚠️ 三处破坏与修法(本 PR 适配) + +### ① `session/list` 拒收 `includeArchived` + +- 0.17.0 strict Zod:`{"includeArchived": ...}` → `-32602 Invalid params — (root): Unrecognized key: "includeArchived"`。桥原代码无条件发该键 → **session/list 自 0.17.0 起整体失败**(真机复现:桥报 -32603 "zcode list failed")。 +- **era 无关性实证**(2026-10-20,`zcode-app-cli` 3.15.3-37 vendor 的真 0.16.9 runtime):`{}`、`{includeArchived: false}`、`{includeArchived: true}` 三形态返回**逐项等价**(建会话前后各测一轮:50/51 条、result 形状相同、键均被接受)——0.16 默认行为与显式 false 无差,去键安全。 +- **修法**:桥直接去掉该键(`_on_session_list`),客户端传入的 `includeArchived` 不再透传(archived 会话概念疑似已从 0.17.0 移除,传 true 也按默认行为处理;注释写明取舍)。`workspace`/`limit` 透传与 result 映射(sessionId / workspace.workspacePath→cwd / title / updatedAt)不变。 + +### ② 模型身份改名 `builtin:zai-coding-plan/GLM-5.3` → `zai/glm-5.3` + +- 0.17.0 create 快照 catalog(`settings.model.available`,30 项)条目形态:`{ref:{providerId, modelId}, label, providerLabel, contextWindow, maxOutputTokens, reasoning:{defaultLevel, levels}}`。zai 条目:`glm-5.3` / `glm-5.3-flash`(contextWindow 1,000,000,maxOutputTokens 128,000,defaultLevel 均 max)+ **`glm-5-turbo` 回归**(contextWindow 200,000,maxOutputTokens 64,000,defaultLevel enabled;该模型曾于 App 3.12.3 时代从服务端下架)。 +- 新 ID **精确大小写敏感**:`glm-5.3` 可用,`GLM-5.3` 报「不存在 Model」。显式选模型**强制要求 `options.reasoningLevel`**(实测 setModel `{providerId:"zai", modelId:"glm-5.3", options:{reasoningLevel:"max"}}` → ok;不带 options → 报 `Reasoning level is required for zai/glm-5.3`)。 +- **桥的行为**:session/new 字符串形态传新 ID `"glm-5.3"` **全通**(catalog 解析 + defaultLevel 补全设计天然适应改名);传旧习惯 `"GLM-5.3"` 原本报「不在本会话的模型目录中」。**修法**:精确匹配失败后按 casefold 对 catalog 的 `ref.modelId` 做第二轮匹配,命中则用 **catalog 规范 ID** 构建 selection(绝不用调用方原始串)——旧习惯 "GLM-5.3" 在 0.17.0 自动解析到 "glm-5.3",0.16 大写 catalog 精确先命中、折叠轮不触发,行为不变(真机 E2E 复核:0.17.0 上 `model: "GLM-5.3"` 创建会话成功)。 +- **不做** providerId 前缀映射魔法(`builtin:zai-coding-plan`→`zai` 无法从 catalog 泛化推导):对象形态传旧 providerId 继续由后端报错透传(文案清晰)。 +- 两条命名空间并存:headless env 的 `ZCODE_MODEL` 仍用 config models key 原样(`GLM-5.3` 大写,0.17.0 实测可用);app-server 的 catalog/setModel/generateText 用小写 `glm-5.3`。 + +### ③ `workspace/generateText` 参数键 `modelRef` → `selection` + +- **改名早于 0.17.0**:实测 App 3.12.3 的 0.16.5 构建仍收 `modelRef`(本仓 2026-09-17 记录),而 `zcode-app-cli` 3.15.3-37 vendor 的 0.16.9 **已只收 selection**(`modelRef` → `-32602 Unrecognized key`)——改名发生在两个同号 0.16.9 构建之间的某次漂移。桥按「先发 selection、键级拒收特征时一次性回退 modelRef」覆盖两端。 +- **maxOutputTokens 之谜解开**:省略 `maxOutputTokens` → `-32603 maxOutputTokens is outside the model option range`(0.16.9 selection 构建与 0.17.0 **同样如此**,非 0.17.0 新增——服务端不再提供可用默认值,不是客户端参数越界)。实测能通组合:**1024 ≤ maxOutputTokens ≤ catalog maxOutputTokens(glm-5.3 / glm-5.3-flash 均 128,000)+ `options.reasoningLevel` 必填**(取 catalog `reasoning.defaultLevel`);显式 128,000 与 1024 均通过并返回真实模型输出;超上限(对 128,000 的模型发 131,072)同样报 range 错。 +- **temperature 已删**:selection 构建发 `temperature` → `-32602 Unrecognized key: "temperature"`(0.16.9 selection 构建同样拒收)。桥照旧透传让错误指名道姓,不静默丢弃。 +- **era 取舍**:不做双发(strict Zod 拒未知键,selection+modelRef 并列必 -32602);也**不替调用方发明 maxOutputTokens/reasoningLevel 默认值**(桥在无会话上下文时拿不到 catalog,发明默认值会悄悄改变输出截断/推理语义)——省略时让后端错误透传并附加一句修法指引(错误文案可读)。回退判定只认 `-32602` 且错误文本提及 `modelRef`、或 `selection`+`unrecognized`(键级拒收形态);selection 内容非法("expected object" 型)不触发重试,避免把内容错误改写成第二次更离谱的 `Unrecognized key: "modelRef"`。 +- result 键随构建不同:selection 构建返回 `{text, selection, finishReason, usage}`(老构建为 `{text, modelRef}`),桥原样透传。 + +### 适配后的测试清单(+15 用例,tests/test_app_server_methods.py) + +- **L1–L4(session/list)**:空参数发 `{}` 无 includeArchived 键 + result 映射不变;cwd/limit 照常透传;客户端传 includeArchived 不透传;后端错误保持 `-32603 "zcode list failed"` 透传。 +- **C4o–C4s(casefold 折叠)**:0.17 小写 catalog + 旧习惯 "GLM-5.3" 命中规范 ID 并补 defaultLevel;精确命中不触发折叠;折叠无命中报原错误(带原始串);catalog 同存仅大小写不同的两个 ID 时精确优先不串 ID;0.16 大写 catalog 反向折叠可用。 +- **M7a 改写 + M7f–M7j(generateText)**:首发 selection 键(值取自客户端 modelRef);键级拒收 → modelRef 一次性重试(全量参数)且重试失败透传第二次错误;内容型 -32602 不重试;maxOutputTokens range 错透传 + 追加「必填」指引(后端原文不改写);`Reasoning level is required` 透传 + 指引;temperature 拒收不重试、错误指名道姓。 + +## CLI 旗标面(0.17.0 vs 0.16.9) + +同方法提取 `--help` 旗标逐项 diff(0.17.0 官方 vs 3.15.3-37 vendor 0.16.9): + +- **新增**:`--toolsearch-mode `(默认 off,仅本次启动生效)——唯一确证增量。 +- **标签形态变化**:`--disallowed-tools` 的 help 标签现为 `--disallowed-tools, --disallowedTools `(camelCase 别名上标签;别名此前已兼容可用)。 +- **其余全部一致**:`-h/-v/-p/--prompt`、`--memory-bench`、`--browser-use`+`--browser-executable`、`--surface`、`--attach`、`--cwd`、`--force-mcs`、`--locale`、`--mode`、`--resume`、`--target`+`--target-replace`、`--workflow-mode`、`-c/--continue`、`--json`、`--no-browser`、`--no-color`、`--verbose`。`--mode` 帮助文案两代均列 `build, edit, plan, yolo, or guarded`(`guarded` 未实测,勿盲用;0.16.1 实测的 `auto` 保留值未实现结论继续有效)。`--allowed-tools`/`--max-turns`/`--permission-mode`/`--allow-main-worktree-yolo` 继续不存在。 +- **与主代理按本仓 0.16.9(App 3.14.0)记录对比出的 6 项「新增」(--attach/--force-mcs/--surface/--toolsearch-mode/--verbose/--workflow-mode)说明**:后 5 项在 App 3.15.3 的 vendor 0.16.9 help 里已存在——同号构建漂移使「新增」的归因依赖基准构建,唯一两代构建都确认新增的只有 `--toolsearch-mode`。子命令 9 个不变(`login` 自 0.16.9 起带 `[zai|bigmodel]` 标签);slash 命令 15 个不变(`/expert` 在 vendor 0.16.9 已在列,不能归因 0.17.0)。 + +## 最小 headless 环境的模型引导(0.17.0 复测,#57 配方) + +**控制变量下复测**(`HOME=/tmp/…` 全新环境 + 官方 0.17.0 二进制,`--prompt "回复两个字:收到" --mode yolo --json`): + +- **配方仍可用,但机制注记修正**:0.17.0 runtime 把调用入口解析到**真实路径**(`/Applications/.../glm/zcode.cjs`),README 原记录的「按入口路径(不解析符号链接)探测」不再成立——符号链接入口 + 邻置 `provider/zcode-builtin.json` 的组合失效,报 `无法定位 CLI ZCode Built-in Provider Config:/Applications/ZCode.app/Contents/Resources/glm/provider/zcode-builtin.json, /config/provider/zcode-builtin.json`(候选路径暴露了真实路径解析)。实测可行的两条路径(均 exit 0、`response="收到"`、usage 新 keys 在列): + 1. **真实文件入口**:把 `zcode.cjs` 复制(非符号链接)到部署目录,`provider/zcode-builtin.json` 邻置于其旁 + `~/.zcode/v2/provider_config.json`(#57 原配方,入口换成真实文件即可); + 2. **显式 env 指路**(桌面 App 同款机制):`ZCODE_BUILTIN_PROVIDER_CONFIG_FILE=` + `ZCODE_PERSONAL_PROVIDER_CONFIG_FILE=`,入口可以是符号链接。 +- **env-only 注入仍不被读取**:仅 `ZCODE_MODEL`/`ZCODE_BASE_URL`/`ANTHROPIC_API_KEY`(无 provider_config.json)在最小环境照旧失败(卡在 builtin provider config 定位,报错同上)——0.16.9 的结论在 0.17.0 不变。 +- 复测方法备注:验证「最小环境」时必须用**真正干净的环境变量**(仅 HOME/PATH)。桌面 App 会话里跑会继承 `ZCODE_BUILTIN_PROVIDER_CONFIG_FILE` 等 App 注入变量,把失败 case 污染成假阳性通过(本轮实测踩过)。 + +## 端到端(桥 ↔ 官方 0.17.0) + +`ZCODE_BIN` 指向官方构建后:initialize → session/new(`model: "GLM-5.3"` 旧习惯)→ **创建成功**(casefold→zai/glm-5.3,setModel 被接受)→ session/list **恢复可用**(50 条、新会话在列、cwd/title/updatedAt 映射正确)。session/prompt 的 turn 管线在原始协议层实测通畅(turn.started 等事件正常流动);当晚实测中的 turn 失败系本机凭证状态所致(业主当晚把 App 切到已过期的 OpenCode Go plan,报错 `This API key has expired. Create a new API key in OpenCode Console`),主代理早间在同一台机器的 zai plan 上已验证完整 turn.completed 流式链路——非桥或 0.17.0 协议缺陷。 + +## 复测方法(可复现) + +- **环境**:`export ZCODE_BIN=~/.local/bin/zcode-app`(官方 3.16.2 构建的符号链接),确认 `--version` = 0.17.0 后再探测(PATH 里的社区包装器 3.16.2-38 现已同为 0.17.0;如需 0.16.9 对照,`npm i --prefix zcode-app-cli@3.15.3-37` 后直接 `node /node_modules/zcode-app-cli/vendor/zcode.cjs`)。 +- **活体探测**:spawn ` app-server --stdio`,发无 `jsonrpc` 键的 NDJSON 帧;反向调用以现役三布尔应答体应答;方法存在性用空 params(-32602 存活 / -32601 已删),参数级兼容性必须按真实 payload 实测(generateText 的 selection/maxOutputTokens 语义即被空参探测掩盖)。 +- **0.16.9 等价性**:同一 probe 脚本对 `{}` / `{includeArchived:false}` / `{includeArchived:true}` 建会话前后各比一轮(数量、result 形状)。 +- **headless**:` --prompt "…" --mode yolo --json`;最小环境验证须用仅 HOME/PATH 的干净 env。 +- **桥端到端**:`ZCODE_BIN` 指官方构建后按 README 的 ACP bridge 用法跑 initialize → session/new → session/prompt → session/list。 diff --git a/packages/acp-bridge/zcode-acp-bridge b/packages/acp-bridge/zcode-acp-bridge index 94e2262..ff83513 100755 --- a/packages/acp-bridge/zcode-acp-bridge +++ b/packages/acp-bridge/zcode-acp-bridge @@ -56,11 +56,29 @@ CLI 0.16.1 适配 (依据 docs/upgrade-0.16.1-spec.md, 全部经实测复核): - state.updated 的 patch 合并为状态投影 (供事件流停滞检查); v4/telemetry/event 忽略; session.titleUpdated 转 stderr 日志。 +CLI 0.17.0 (App 3.16.2) 适配 (依据 docs/recheck-3.16.2.md, 全部经实测复核): + - session/list 不再发 includeArchived (0.17.0 strict Zod 拒未知键 -32602, + 桥原无条件 false 使 list 整体失败; 0.16.9 实测 {} 与 {includeArchived: + false} 等价, 去键 era 无关)。核心链路 (create/subscribe/send/事件流/ + stop/list/resume) 与删除面均零改动兼容。 + - 模型身份改名 builtin:zai-coding-plan/GLM-5.3 → zai/glm-5.3 (小写, 精确 + 大小写敏感): session/new 字符串 model 在 catalog 精确未命中后按 casefold + 回退, 用 catalog 规范 ID 构建 selection — 旧习惯 "GLM-5.3" 自动适配, + 0.16 大写 catalog 行为不变 (精确先命中)。zai 系模型 reasoningLevel 强制 + 必填 (缺它 turn/generateText 报 "Reasoning level is required"), 字符串 + 形态的 defaultLevel 补全设计天然满足。 + - workspace/generateText 参数键 modelRef → selection (该改名早于 0.17.0: + App 3.15.3 的同号 0.16.9 构建已只收 selection; maxOutputTokens 自改名起 + 必填, temperature 已删): 桥先发 selection, -32602 键级拒收特征时一次性 + 回退 modelRef (老构建), 见 _on_workspace_generate_text。 + 能力范围: ✅ initialize 握手 ✅ session/new → session/create (可选 model: "GLM-5.3" 字符串按 create 快照 - catalog 解析并补默认 reasoningLevel, 或完整 ModelSelection 对象; 经 - session/setModel 应用, 会话创建即锁定模型 — 0.16.9 实测) + catalog 解析并补默认 reasoningLevel (0.17.0 起精确未命中再按 casefold + 折叠匹配, 用 catalog 规范 ID — 旧大写习惯自动适配 zai/glm-5.3 小写改名), + 或完整 ModelSelection 对象; 经 session/setModel 应用, 会话创建即锁定 + 模型 — 0.16.9 实测) ✅ session/prompt → 事件驱动真流式 (0.14.8+) / 轮询伪流式 (旧协议降级) ✅ 流式文本 (model.streaming → agent_message_chunk 逐段) ✅ 工具调用展示 (tool.updated → tool_call/tool_call_update 实时) @@ -76,7 +94,8 @@ CLI 0.16.1 适配 (依据 docs/upgrade-0.16.1-spec.md, 全部经实测复核): ✅ workspace/* 方法 (0.15.0+): readState/generateText/setDefault{Model,Mode,ThoughtLevel}/ upsertModelProvider/removeModelProvider/updateProviderRegistry (App 3.12.3 的 0.16.5 构建删 7/8, 仅 generateText 存活, 见 - docs/recheck-3.12.3.md) + docs/recheck-3.12.3.md; generateText 的 modelRef→selection 键映射与 + maxOutputTokens/temperature 语义变化见 _on_workspace_generate_text) ✅ prompt/* 方法 (App 3.3.0+): enhance (同步) / enhance/start+result+cancel (异步 job) (于 0.16 移除) @@ -1528,6 +1547,13 @@ class ACPBridge: 匹配 (多 provider 同名取第一个; 需要指定 provider 的调用方应直接传 对象), catalog 条目带 reasoning.defaultLevel 时补 options.reasoningLevel (0.16.9 实测部分模型缺它会 ModelProtocolError)。 + 0.17.0 (App 3.16.2) 适配: 精确匹配失败后再按 casefold 做一轮大小写 + 不敏感匹配, 命中则用 catalog 的规范 ID 构建 selection (绝不用调用方 + 原始串) —— 0.17.0 把模型身份从 builtin:zai-coding-plan/GLM-5.3 改为 + zai/glm-5.3 (小写, 精确大小写敏感), 旧习惯 "GLM-5.3" 借此自动解析到 + "glm-5.3"; 0.16 系后端 (大写 ID) 精确匹配先命中, 折叠轮不触发, 行为 + 不变。不做 providerId 前缀映射魔法 (builtin:zai-coding-plan→zai 无法 + 从 catalog 泛化推导): 对象形态传旧 providerId 继续由后端报错透传。 无法解析时返回错误字符串, 「catalog 不可用」与「未命中」分开报 (调用 方转 -32602 并附上已创建的 sessionId)。 """ @@ -1546,12 +1572,9 @@ class ACPBridge: # AttributeError 把 -32602 变成 -32603 内部错) if not isinstance(available, list): return "本会话模型目录不可用 (create 快照缺少 settings.model.available)" - for entry in available: - if not isinstance(entry, dict): - continue + + def _build_selection(entry): ref = entry.get("ref") - if not isinstance(ref, dict) or ref.get("modelId") != wanted: - continue selection = {"providerId": ref.get("providerId"), "modelId": ref.get("modelId")} reasoning = entry.get("reasoning") @@ -1560,13 +1583,48 @@ class ACPBridge: if default_level: selection["options"] = {"reasoningLevel": default_level} return selection + + # 第一轮: 精确匹配 (大小写敏感; 与旧行为逐字一致) + for entry in available: + if not isinstance(entry, dict): + continue + ref = entry.get("ref") + if not isinstance(ref, dict) or ref.get("modelId") != wanted: + continue + return _build_selection(entry) + # 第二轮: casefold 折叠匹配 (仅精确未命中时触发)。多 provider 折叠 + # 同名同精确轮规则取第一个 — 两个仅大小写不同的规范 ID 共存时命中者 + # 依赖 catalog 顺序, 与既有「同名取第一个」口径一致。 + folded = wanted.casefold() + for entry in available: + if not isinstance(entry, dict): + continue + ref = entry.get("ref") + if not isinstance(ref, dict): + continue + model_id = ref.get("modelId") + if not isinstance(model_id, str): + continue + if model_id.casefold() != folded: + continue + return _build_selection(entry) return f"modelId '{wanted}' 不在本会话的模型目录中" def _on_session_list(self, msg_id, params): - """ACP session/list → zcode session/list, 转成 ACP SessionInfo[]""" + """ACP session/list → zcode session/list, 转成 ACP SessionInfo[] + + 0.17.0 (App 3.16.2) 适配: 不再向后端发 includeArchived —— strict Zod + 对未知键 -32602 "Unrecognized key" (桥此前的无条件 `includeArchived: + false` 在 0.17.0 上使 session/list 整体失败, 真机复现)。0.16.9 后端 + 实测 (2026-10-20, 社区包装器 vendor 0.16.9): `{}` 与 + `{includeArchived: false}` 返回逐项等价 (数量与 result 形状), 故直接 + 去键是 era 无关方案, 0.16 默认行为不变。取舍: ACP 客户端传入的 + includeArchived 不再透传 (0.17.0 无法透传; archived 会话概念疑似已 + 从 0.17.0 移除), 传 true 也按默认行为处理。 + """ self.ensure_backend() cwd = params.get("cwd") - zc_params = {"includeArchived": params.get("includeArchived", False)} + zc_params = {} if cwd: zc_params["workspace"] = {"workspacePath": cwd, "workspaceKey": cwd} limit = params.get("limit") @@ -1977,13 +2035,51 @@ class ACPBridge: log("workspace/readState → ok") return {"jsonrpc": "2.0", "id": msg_id, "result": result} + @staticmethod + def _generate_text_needs_legacy_model_ref(err): + """判定 generateText 的 -32602 是否为「后端不认识 selection 键」的旧构建特征。 + + selection 键的引入早于 0.17.0 (实测 App 3.12.3 的 0.16.5 构建仍收 + modelRef, App 3.15.3 的同号 0.16.9 构建已只收 selection), 桥无法预知 + 对端构建, 只能先发 selection、按错误特征识别旧构建后重试 modelRef。 + 特征: -32602 且文本提到 modelRef, 或 selection + unrecognized (键级 + 拒收形态)。selection 内容非法时后端报 "expected object" 型 -32602、 + 无 unrecognized, 不触发重试 (避免把内容错误改写成第二次更离谱的 + "Unrecognized key: modelRef")。 + """ + if err.get("code") != -32602: + return False + low = str(err.get("message") or "").lower() + return "modelref" in low or ("selection" in low and "unrecognized" in low) + def _on_workspace_generate_text(self, msg_id, params): """workspace/generateText → 同名: 一次性文本生成 (不创建会话)。 params: {workspace, modelRef:{providerId,modelId,variant?}, prompt, querySource, maxOutputTokens?, temperature?} - 返回: {text, modelRef} + 返回: 后端原样 (selection 构建实测 result 键为 {text, selection, + finishReason, usage}; 老 modelRef 构建为 {text, modelRef}) 注意: 涉及模型调用, timeout 设 60s。 + + 0.17.0 (App 3.16.2) 适配 — 参数键 modelRef 已更名 selection (strict + Zod 对 modelRef 报 Unrecognized key), 桥先按 selection 形态发送: + - options 原样透传。0.17.0 对 zai 系模型强制要求 + options.reasoningLevel, 缺它报 -32603 "Reasoning level is + required for /" (setModel 同规则); 需要 catalog + 默认值的调用方走 session/new 的字符串 model 解析。 + - maxOutputTokens: selection 构建起必填 —— 省略时后端报 -32603 + "maxOutputTokens is outside the model option range" (服务端不再 + 提供可用默认; 0.16.9 的 selection 构建实测同样如此, 非 0.17.0 + 新增)。实测能通组合: 1024 ≤ 值 ≤ catalog maxOutputTokens + (zai/glm-5.3 与 zai/glm-5.3-flash 均 128000), 省略或超上限都报 + range 错。桥不替调用方发明默认值 (那会悄悄改变输出截断语义), + 省略时让错误透传并附加指引。 + - temperature: selection 构建已删该键 (Unrecognized key), 照旧 + 透传让错误指名道姓, 不静默丢弃。 + era 回退: 后端 -32602 且特征是「不认识 selection 键」(老构建只收 + modelRef) 时, 用 modelRef 形态一次性重试 (全量原始参数); 重试仍失败 + 则透传第二次错误。不做双发 (selection+modelRef 并列): strict Zod 拒 + 未知键, 必 -32602。 """ self.ensure_backend() ws, err = self._resolve_workspace(msg_id, params) @@ -1995,20 +2091,46 @@ class ACPBridge: return self._error(msg_id, -32602, "generateText 需要 modelRef") if not prompt: return self._error(msg_id, -32602, "generateText 需要 prompt") - zc_params = { - "workspace": ws, - "modelRef": model_ref, - "prompt": prompt, - "querySource": params.get("querySource", "bridge"), - } - if params.get("maxOutputTokens") is not None: - zc_params["maxOutputTokens"] = params["maxOutputTokens"] - if params.get("temperature") is not None: - zc_params["temperature"] = params["temperature"] + query_source = params.get("querySource", "bridge") + max_output_tokens = params.get("maxOutputTokens") + temperature = params.get("temperature") + + def _params(model_key): + # selection / modelRef 两形态同构, 仅模型键名不同 + zc = { + "workspace": ws, + model_key: model_ref, + "prompt": prompt, + "querySource": query_source, + } + if max_output_tokens is not None: + zc["maxOutputTokens"] = max_output_tokens + if temperature is not None: + zc["temperature"] = temperature + return zc + zc_id = self._next_id() - resp, _ = self.backend.request(zc_id, "workspace/generateText", zc_params, timeout=60) + resp, _ = self.backend.request( + zc_id, "workspace/generateText", _params("selection"), timeout=60) + if "error" in resp and self._generate_text_needs_legacy_model_ref(resp["error"]): + # 旧构建 (只收 modelRef) 的一次性重试; 结果无论成败都按原样透传 + zc_id = self._next_id() + resp, _ = self.backend.request( + zc_id, "workspace/generateText", _params("modelRef"), timeout=60) if "error" in resp: - return self._passthrough_error(msg_id, "workspace/generateText", resp) + perr = self._passthrough_error(msg_id, "workspace/generateText", resp) + # selection 构建的两类 -32603 后端文案不含修法, 附加一句指引 + # (只追加、不改写后端原文; 其他错误原样透传) + emsg = str(resp["error"].get("message") or "") + if "maxOutputTokens" in emsg and "range" in emsg: + perr["error"]["message"] += ( + " (selection 构建起 maxOutputTokens 必填: 显式传 ≤ 目标模型" + " catalog maxOutputTokens 的值, 如 4096)") + elif "Reasoning level is required" in emsg: + perr["error"]["message"] += ( + " (selection 构建下 zai 系模型需要 options.reasoningLevel," + " 可取 catalog reasoning.defaultLevel)") + return perr result = resp.get("result", {}) log(f"workspace/generateText → ok (text {len(result.get('text', ''))} chars)") return {"jsonrpc": "2.0", "id": msg_id, "result": result} diff --git a/packages/agent-help/zcode-agent-help b/packages/agent-help/zcode-agent-help index fafe5ea..fbf786d 100755 --- a/packages/agent-help/zcode-agent-help +++ b/packages/agent-help/zcode-agent-help @@ -32,6 +32,11 @@ ZCODE_BIN = os.environ.get("ZCODE_BIN", "zcode") # 模型 ID 的 canonical 格式: config 里 models 的 key 原样 (如 GLM-5.3), 不加 provider 前缀。 # 实测 "zai/GLM-5.2" 也兼容, 但与 shared/credentials.py 权威实现不一致, 故统一用原始 id。 +# 注意两条命名空间 (0.17.0 / App 3.16.2 起): headless env 的 ZCODE_MODEL 仍用 +# config key 原样 (GLM-5.3, 大写, 实测可用); app-server 的模型目录 +# (session/create 快照 settings.model.available / setModel / generateText) +# 已改名小写 zai/glm-5.3 (精确大小写敏感, 大写报「不存在 Model」)。桥的 +# session/new 字符串 model 有 casefold 回退, 两个命名空间的写法都能解析。 ZCODE_CREDS_PATH = Path.home() / ".zcode" / "v2" / "config.json" # issue #55: 模型解析面只读诊断用的官方 runtime 模型注册表 (桌面 App 完成 @@ -230,15 +235,17 @@ def print_injected_env(config_path=None, provider_config_path=None): # ============================================================ OVERVIEW = { "tool": "zcode", - "version_described": "0.16.9", + "version_described": "0.17.0", # 整体 review P2-6: version_described 是"静态清单基于该版本实测"的意思; # 实际安装版本以 environment.zcode_version (动态探测) 为准, 两者可能不一致。 # App 3.12.3 起 --version 同号构建内容漂移 (workspace/* 7 个与 # updateRuntimeModelConfig 被删, 见 key_methods 各 ❌ 注记), 故不绑定具体 # App 版本号 — 描述对象是 CLI 能力面, 可用性以各条目注记为准。 - # 0.16.9 (App 3.14.0) 复测: 协议面与 3.12.3 的 0.16.5 构建一致 (零代码改动), - # 仅 CLI 旗标面有增删 (见 agency/input/permission 各条目 0.16.9 注记)。 - "tested_against": "0.16.9 (App 3.14.0 复测 2026-09-19; 0.16.5 面 App 3.10.2 / 3.12.3 复测)", + # 0.17.0 (App 3.16.2) 复测 (2026-10-20): 三处破坏已在桥内适配 (session/list + # 的 includeArchived、模型 ID 改名 zai/glm-5.3、generateText 的 selection + # 键), 详见 docs/recheck-3.16.2.md。旗标面唯一确证新增 --toolsearch-mode; + # --disallowed-tools 帮助标签现带 camelCase 别名 (--disallowedTools)。 + "tested_against": "0.17.0 (App 3.16.2 复测 2026-10-20; 0.16.9 面 App 3.14.0 复测 2026-09-19; 0.16.5 面 App 3.10.2 / 3.12.3 复测)", "what_it_is": ( "ZCode 是智谱(Z.AI)的 Agentic Coding CLI, 由 GLM 系列模型驱动。" "它能自主完成代码编写/审查/调试/分析等任务, 可调用工具(读写文件、" @@ -414,7 +421,8 @@ NATIVE_CLI = { "arg": "--allowed-tools ", "since": "0.16.1 (仅文案)"}, {"category": "agency", "name": "tool-denylist", "supported": True, - "description": "工具黑名单: 禁止列出的工具 (逗号/空格分隔; camelCase 别名 --disallowedTools)。" + "description": "工具黑名单: 禁止列出的工具 (逗号/空格分隔; camelCase 别名 --disallowedTools," + "0.17.0 起该别名直接显示在 help 标签)。" "实测确认: 工具集级物理移除, 先于权限层, yolo 也绕不过;" "禁 Bash 时必须连 js / mcp__node_repl__js* 一族同禁, 否则可被 execSync 打穿", "arg": "--disallowed-tools ", @@ -471,8 +479,14 @@ NATIVE_CLI = { "description": "随 --prompt 启用自动 Memory 提取并在退出前等待 " "(需 Memory 功能已启用)", "arg": "--memory-bench", "since": "0.16.9"}, + # 0.17.0 (App 3.16.2) help 新增的旗标, 2026-10-20 实测在列 (与 + # App 3.15.3 的同号 0.16.9 构建逐项 diff 后的唯一旗标增量) + {"category": "agency", "name": "toolsearch-mode", "supported": True, + "description": "ToolSearch 模式: auto|native|local|off (默认 off, 仅本次启动生效)", + "arg": "--toolsearch-mode ", "since": "0.17.0"}, ], - # 子命令 (zcode , 实测自 0.16.9 --help 复核; 9 个子命令与 0.16.5 一致, 无新增/删除) + # 子命令 (zcode , 实测自 0.17.0 --help 复核; 9 个子命令与 0.16.5 一致, 无新增/删除; + # login 自 0.16.9 起 help 标签带 [zai|bigmodel] 参数) "subcommands": [ {"name": "tui", "usage": "zcode tui", "description": "打开全屏 TUI (无参数直接运行 zcode 同效)", @@ -499,7 +513,8 @@ NATIVE_CLI = { "description": "打印 CLI 版本"}, ], # slash 命令 (TUI 会话内使用; /side /btw 未见于 0.16.1/0.16.5/0.16.9 --help, 出自 App 3.6.5 changelog; - # 0.16.5 help 实测 slash 列表 14 个; 0.16.9 help 实测 15 个 (新增 /dwf), 与本清单除 /side /btw 外一致) + # 0.16.5 help 实测 slash 列表 14 个; 0.16.9 help 实测 15 个 (新增 /dwf); 0.17.0 help 实测 15 个不变 + # (与 App 3.15.3 的同号 0.16.9 构建逐项一致), 与本清单除 /side /btw 外一致) "slash_commands": [ {"command": "/help [command]", "description": "slash 命令帮助"}, {"command": "/login", "description": "Z.AI OAuth 登录", "since": "0.16.1"}, @@ -578,8 +593,10 @@ APP_SERVER = { "note": "查状态用; status: idle/running"}, {"method": "session/messages", "params": "sessionId", "result": "{messages:[{info, parts}]}", "note": "parts 类型: text/tool/reasoning/patch/step-start 等"}, - {"method": "session/list", "params": "workspace?, includeArchived?, limit?", - "result": "{sessions:[{sessionId, workspace, title, status...}]}"}, + {"method": "session/list", "params": "workspace?, limit?", + "result": "{sessions:[{sessionId, workspace, title, status...}]}", + "note": "0.17.0 起 includeArchived 键被 strict Zod 拒收 (-32602 " + "Unrecognized key, archived 概念疑似已移除) — 桥已不再发送"}, {"method": "session/resume", "params": "sessionId, workspace?", "result": "完整快照 Om", "note": "不回放历史"}, {"method": "session/setMode", "params": "sessionId, mode", "result": "ok"}, @@ -607,9 +624,12 @@ APP_SERVER = { {"method": "session/goal", "params": "sessionId, goal?", "note": "0.14.8+: 读/设 session 目标 (新)"}, {"method": "session/compact", "params": "sessionId", "note": "0.14.8+: 压缩对话上下文 (新)"}, {"method": "session/setModel", - "params": "sessionId, model{providerId, modelId, options?:{reasoningLevel}}", + "params": "sessionId, model{providerId, modelId, options:{reasoningLevel}}", "note": "0.14.8+: 切换模型 (新); 0.16 起 schema 要 model 对象 — 旧 modelId 字符串形态必报 " - "-32602 (0.16.9 实测), 缺 reasoningLevel 时部分模型 turn 阶段报 ModelProtocolError"}, + "-32602 (0.16.9 实测); 0.17.0 起模型身份改名 (builtin:zai-coding-plan/GLM-5.3 → " + "zai/glm-5.3, 小写, 精确大小写敏感) 且 zai 系模型 options.reasoningLevel 强制必填 " + "(缺它报 \"Reasoning level is required for /\", 可取 catalog " + "reasoning.defaultLevel)"}, {"method": "session/cancelBackgroundTask", "params": "sessionId, taskId", "note": "0.14.8+: 取消后台 Bash 任务 (新)"}, # ----- 0.15.0 (App 3.2.0) 新增 session 级方法 ----- @@ -624,14 +644,19 @@ APP_SERVER = { {"method": "session/setMode", "params": "sessionId, mode", "note": "0.15.0+ 暴露: 切换会话权限模式 (mode: plan|build|edit|yolo|auto); " "⚠ auto 为保留值未实现 — 切 auto 后该会话所有工具调用被 " - "mode.auto.unimplemented 拒绝, 勿当'智能自动批准'用"}, + "mode.auto.unimplemented 拒绝, 勿当'智能自动批准'用; " + "0.17.0 help 的 --mode 文案另列 guarded (与 auto 同样未实测, 勿盲用)"}, # ----- 0.15.0 (App 3.2.0) 新增 workspace/* 方法 (按工作区定位, 非 session) ----- {"method": "workspace/readState", "params": "workspace:{workspacePath,workspaceKey}, runtimeModel?", "note": "0.15.0+: 读工作区状态 (settings/modelCatalog/slashCommands); " "❌ 已于 App 3.12.3 的 0.16.5 构建移除 (2026-09-17 实测返 -32601, 未搬家到 session/)"}, - {"method": "workspace/generateText", "params": "workspace, modelRef, prompt, querySource, maxOutputTokens?, temperature?", + {"method": "workspace/generateText", "params": "workspace, selection{providerId, modelId, options:{reasoningLevel}}, prompt, querySource, maxOutputTokens", "note": "0.15.0+: 一次性文本生成 (不建会话, 无 messages/tools); " - "App 3.12.3 的 0.16.5 构建实测仍存活 (workspace/* 8 删 7 中唯一幸存)"}, + "App 3.12.3 的 0.16.5 构建实测仍存活 (workspace/* 8 删 7 中唯一幸存); " + "selection 构建起 (早于 0.17.0, App 3.15.3 的同号 0.16.9 已如此): 参数键 " + "modelRef → selection、maxOutputTokens 必填 (省略报 \"outside the model " + "option range\", 实测 1024 ≤ 值 ≤ catalog maxOutputTokens 可用)、temperature " + "已删 (Unrecognized key); 桥先发 selection、键级拒收时一次性回退 modelRef"}, {"method": "workspace/setDefaultModel", "params": "workspace, model, runtimeModel?, expectedWorkspaceRevision?", "note": "0.15.0+: 设工作区默认模型 (持久化, 乐观锁); " "❌ 已于 App 3.12.3 的 0.16.5 构建移除 (实测返 -32601)"}, diff --git a/skills/zcode-bridge-guide/SKILL.md b/skills/zcode-bridge-guide/SKILL.md index f9b5433..ef0f33a 100644 --- a/skills/zcode-bridge-guide/SKILL.md +++ b/skills/zcode-bridge-guide/SKILL.md @@ -1,14 +1,14 @@ --- name: zcode-bridge-guide version: 1.3.0 -description: 驱动 ZCode(智谱 GLM 系列 coding agent)的通用说明书。覆盖三种接入模式(CLI --prompt / ACP bridge / MCP tools)、凭证配置、真流式/伪流式双模式、扩展协议方法(session 级 + workspace 级)、思考强度控制、任务书模板、已知坑。需要把 ZCode 当子代理编排、跑编码/审查任务、或集成进编辑器时用。兼容 ZCode CLI 0.14.5 ~ 0.16.9(App 3.14.0)。 +description: 驱动 ZCode(智谱 GLM 系列 coding agent)的通用说明书。覆盖三种接入模式(CLI --prompt / ACP bridge / MCP tools)、凭证配置、真流式/伪流式双模式、扩展协议方法(session 级 + workspace 级)、思考强度控制、任务书模板、已知坑。需要把 ZCode 当子代理编排、跑编码/审查任务、或集成进编辑器时用。兼容 ZCode CLI 0.14.5 ~ 0.17.0(App 3.16.2)。 user-invocable: true --- # 驱动 ZCode(三模式通用说明书) > 本 skill 是 [zcode-open-bridge](https://github.com/tizerluo/zcode-open-bridge) 项目的配套说明书。 -> 兼容 ZCode CLI **0.14.5 ~ 0.16.9**(实测含 App 3.2.1~3.14.0)。新版功能(事件驱动真流式、fork/goal/compact、workspace/*、setThoughtLevel 思考强度控制)在旧版上自动降级或返回 `-32603`;`session/steer`、`session/rewind*`、`prompt/enhance*` 已于 0.16 移除,调用返回 `-32601`。⚠️ App 3.12.3 的 0.16.5 构建(版本号未变、内容漂移)进一步删除了 workspace/* 面 7/8 方法与 `session/updateRuntimeModelConfig`(0.16.9/App 3.14.0 复测同面,桥零改动;CLI 旗标面有增删,详见下文方法表注记与版本表)。 +> 兼容 ZCode CLI **0.14.5 ~ 0.17.0**(实测含 App 3.2.1~3.16.2)。新版功能(事件驱动真流式、fork/goal/compact、workspace/*、setThoughtLevel 思考强度控制)在旧版上自动降级或返回 `-32603`;`session/steer`、`session/rewind*`、`prompt/enhance*` 已于 0.16 移除,调用返回 `-32601`。⚠️ App 3.12.3 的 0.16.5 构建(版本号未变、内容漂移)进一步删除了 workspace/* 面 7/8 方法与 `session/updateRuntimeModelConfig`(0.16.9/App 3.14.0 复测同面,桥零改动;CLI 旗标面有增删,详见下文方法表注记与版本表)。⚠️ 0.17.0(App 3.16.2)三处面变化(桥已适配):`session/list` 拒收 `includeArchived` 键;模型目录 ID 改名小写 `zai/glm-5.3`(字符串 model 传旧 `"GLM-5.3"` 由桥 casefold 解析);`workspace/generateText` 参数键 `modelRef`→`selection`(调用方接口不变,桥负责映射),详见 docs/recheck-3.16.2.md。 ## 前置条件 @@ -259,7 +259,7 @@ ACP bridge 暴露的 ZCode 新版协议方法,按定位维度分组。**sessio | `session/compact` | 压缩对话上下文 | 0.14.8 | `{sessionId}` | | `session/steer` ❌ | turn 进行中追加指令(**0.16 已移除**,语义并入 `session/send`) | 0.14.8 | `{sessionId, content}` | | `session/setThoughtLevel` | ⭐ 设置思考强度 | 0.15.0 | `{sessionId, thoughtLevel}` | -| `session/setModel` / `setMode` | 切换模型 / 权限模式 | 0.14.8 | `{sessionId, model}` / `{sessionId, mode}`(model 是 ModelSelection 对象 `{providerId, modelId, options?: {reasoningLevel}}`;0.16 后端 schema 要对象,旧 `modelId` 字符串必 -32602,且对象形态仅在 0.16.x 验证、≤0.15 未验证。另 `session/new` 支持可选 `model`(字符串按 catalog 解析,条目带 `reasoning.defaultLevel` 时补;或完整对象;`model: null`/省略等同未提供),创建会话即锁定模型;另支持 `toolAllowlist`/`toolDenylist` 工具名单,见下方注) | +| `session/setModel` / `setMode` | 切换模型 / 权限模式 | 0.14.8 | `{sessionId, model}` / `{sessionId, mode}`(model 是 ModelSelection 对象 `{providerId, modelId, options?: {reasoningLevel}}`;0.16 后端 schema 要对象,旧 `modelId` 字符串必 -32602,且对象形态仅在 0.16.x 验证、≤0.15 未验证;0.17.0 起 ID 改名小写 `zai/glm-5.3`(精确大小写敏感)且 zai 系 `options.reasoningLevel` 强制必填。另 `session/new` 支持可选 `model`(字符串按 catalog 解析,条目带 `reasoning.defaultLevel` 时补,0.17.0 起大小写折叠回退、旧 `"GLM-5.3"` 自动解析;或完整对象;`model: null`/省略等同未提供),创建会话即锁定模型;另支持 `toolAllowlist`/`toolDenylist` 工具名单,见下方注) | | `session/cancelBackgroundTask` | 取消后台 Bash 任务 | 0.14.8 | `{sessionId, taskId}` | | `session/rewindCascade` ❌ | 级联回退(同 rewind schema,**0.16 已移除**) | 0.15.0 | `{sessionId, target?, scope?, expectedRevision?}` | | `session/updateRuntimeModelConfig` ❌ | 运行时覆盖模型配置 | 0.15.0 | `{sessionId, runtimeModel, applyModelSelection?}`(0.16 起 `runtimeModel.revision` 必填) | @@ -275,7 +275,7 @@ ACP bridge 暴露的 ZCode 新版协议方法,按定位维度分组。**sessio | 方法 | 作用 | params | |------|------|--------| | `workspace/readState` | 读工作区状态(模型目录/设置)❌ 3.12.3 已删 | `{workspace, runtimeModel?}` | -| `workspace/generateText` | 一次性文本生成(不建会话)—— 3.12.3 唯一幸存 | `{workspace, modelRef, prompt, querySource, ...}` | +| `workspace/generateText` | 一次性文本生成(不建会话)—— 3.12.3 唯一幸存 | `{workspace, modelRef, prompt, querySource, ...}`(0.17.0 适配:桥把 modelRef 映射为后端 `selection` 键;selection 构建起 maxOutputTokens 必填、zai 系需 options.reasoningLevel、temperature 已删——省略必填项时后端错误透传并附指引) | | `workspace/setDefaultModel` / `setDefaultMode` / `setDefaultThoughtLevel` | 设工作区默认值(持久化)❌ 3.12.3 已删 | `{workspace, model\|mode\|thoughtLevel, expectedWorkspaceRevision?}` | | `workspace/upsertModelProvider` / `removeModelProvider` / `updateProviderRegistry` | 管理模型供应商(含 apiKey,敏感)❌ 3.12.3 已删 | `{workspace, provider\|providerId\|registry, ...}` | @@ -429,6 +429,7 @@ npm test # 全量,看实际数字 | ZCode CLI 版本 | 支持情况 | 差异 | |:--------------:|:--------:|------| +| **0.17.0** (App 3.16.2) | ✅ 完整 | 核心链路零改动兼容;三处面破坏由桥适配(2026-10-20 复测):`session/list` 拒收 `includeArchived`(桥已去键)、模型目录 ID 改名小写 `zai/glm-5.3` + `glm-5-turbo` 回归 catalog(zai 系强制 reasoningLevel;字符串 model 桥做 casefold 解析)、`workspace/generateText` 参数键 `modelRef`→`selection`(桥映射 + 老构建一次性回退)。CLI 旗标面唯一确证新增 `--toolsearch-mode`,`--disallowed-tools` 标签带 camelCase 别名(见 docs/recheck-3.16.2.md) | | **0.16.9** (App 3.14.0) | ✅ 完整 | 协议面与 3.12.3 的 0.16.5 构建一致(桥零改动,2026-09-19 复测);变化仅在 CLI 旗标面:`--permission-mode`/`--allow-main-worktree-yolo` 已移除,`--allowed-tools`/`--max-turns` 连帮助文案一并消失,新增 `--cwd`/`--target-replace`/`--browser-use` 等(见 docs/recheck-3.14.0.md) | | **0.16.5** (App 3.12.3,同号构建漂移) | ✅ 完整(核心面) | session/* 核心面不变;workspace/* 删 7/8 仅 `generateText` 幸存,`session/updateRuntimeModelConfig`、`updateInteractionPreferences` 已删——桥统一翻译为 `-32601`「已移除该能力」(见 docs/recheck-3.12.3.md) | | **0.16.5** (App 3.10.2) | ✅ 完整 | 与 0.16.1 同面 + `automation/*` 全族删除(桥未使用,无影响) | diff --git a/tests/test_app_server_methods.py b/tests/test_app_server_methods.py index 377680d..ff00757 100644 --- a/tests/test_app_server_methods.py +++ b/tests/test_app_server_methods.py @@ -609,6 +609,98 @@ def test_c4n_new_string_model_malformed_settings_32602(self): self.assertNotIn("sess_badsettings", bridge.session_map, "model 未生效的会话不得登记") + # ---------- C4o+: 大小写折叠回退 (0.17.0 模型 ID 改名 GLM-5.3 → glm-5.3) ---------- + # 0.17.0 (App 3.16.2) 的 catalog 形态: providerId "zai" + 小写 modelId + _CATALOG_017 = { + "sessionId": "sess_m17", + "settings": {"model": {"available": [ + {"ref": {"providerId": "zai", "modelId": "glm-5.3"}, + "label": "GLM-5.3", "providerLabel": "Z.ai Coding Plan", + "reasoning": {"levels": [{"value": "low", "label": "low"}, + {"value": "high", "label": "high"}, + {"value": "max", "label": "max"}], + "defaultLevel": "max"}}, + {"ref": {"providerId": "zai", "modelId": "glm-5.3-flash"}, + "label": "GLM-5.3-Flash", "providerLabel": "Z.ai Coding Plan"}, + ]}}, + } + + def test_c4o_new_model_casefold_hits_catalog_canonical(self): + """C4o: 0.17.0 小写 catalog + 旧习惯 "GLM-5.3" → casefold 折叠命中, + selection 用 catalog 规范 ID (zai/glm-5.3), 绝不透传调用方原始串; + defaultLevel 照补 (0.17.0 对 zai 系强制 reasoningLevel)""" + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": dict(self._CATALOG_017)}}, + }) + resp = self._call(bridge, "session/new", {"cwd": "/p", "model": "GLM-5.3"}) + self._assert_ok(resp) + set_calls = [c for c in fake.calls if c["method"] == "session/setModel"] + self.assertEqual(set_calls[0]["params"]["model"], + {"providerId": "zai", "modelId": "glm-5.3", + "options": {"reasoningLevel": "max"}}, + "折叠命中必须用 catalog 规范 ID + 补 defaultLevel") + + def test_c4p_new_model_exact_hit_no_fold(self): + """C4p: 精确命中不触发折叠轮 (小写请求在小写 catalog 上精确命中)""" + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": dict(self._CATALOG_017)}}, + }) + resp = self._call(bridge, "session/new", {"cwd": "/p", "model": "glm-5.3"}) + self._assert_ok(resp) + set_calls = [c for c in fake.calls if c["method"] == "session/setModel"] + self.assertEqual(set_calls[0]["params"]["model"]["modelId"], "glm-5.3") + + def test_c4q_new_model_casefold_miss_original_error(self): + """C4q: 折叠也无命中 → 报原「不在本会话的模型目录中」错误 (文案带 + 调用方原始串), 不发 setModel、不登记""" + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": dict(self._CATALOG_017)}}, + }) + resp = self._call(bridge, "session/new", {"cwd": "/p", "model": "NOPE"}) + self._assert_error_code(resp, -32602) + self.assertIn("NOPE", resp["error"]["message"]) + self.assertEqual([c for c in fake.calls + if c["method"] == "session/setModel"], []) + self.assertNotIn("sess_m17", bridge.session_map) + + def test_c4r_new_model_exact_takes_precedence_over_fold(self): + """C4r: 精确命中优先 — catalog 同时有仅大小写不同的两个规范 ID 时, + 精确轮命中对应条目, 折叠轮不参与 (不串 ID)""" + mixed = {"sessionId": "sess_mixed", "settings": {"model": {"available": [ + {"ref": {"providerId": "zai", "modelId": "glm-5.3"}, + "label": "glm-5.3", "providerLabel": "zai"}, + {"ref": {"providerId": "legacy", "modelId": "GLM-5.3"}, + "label": "GLM-5.3", "providerLabel": "legacy"}, + ]}}} + for wanted, expect_provider in (("GLM-5.3", "legacy"), + ("glm-5.3", "zai")): + with self.subTest(wanted=wanted): + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": dict(mixed)}}, + }) + resp = self._call(bridge, "session/new", + {"cwd": "/p", "model": wanted}) + self._assert_ok(resp, msg=f"model={wanted}") + set_calls = [c for c in fake.calls + if c["method"] == "session/setModel"] + self.assertEqual(set_calls[0]["params"]["model"]["providerId"], + expect_provider, + "精确命中必须取完全同名的条目") + + def test_c4s_new_model_fold_works_on_upper_catalog_too(self): + """C4s: 0.16 风格大写 catalog + 小写请求 → 折叠命中大写规范 ID + (era 双向便利; 大写请求在大写 catalog 上精确命中、折叠不触发, + 由既有 C4 覆盖)""" + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": dict(self._CATALOG)}}, + }) + resp = self._call(bridge, "session/new", {"cwd": "/p", "model": "glm-5.3"}) + self._assert_ok(resp) + set_calls = [c for c in fake.calls if c["method"] == "session/setModel"] + self.assertEqual(set_calls[0]["params"]["model"], + {"providerId": "bigmodel-api", "modelId": "GLM-5.3", + "options": {"reasoningLevel": "max"}}) + # ---------- C5: session/new 工具名单透传 (只读监督的会话级前置手段) ---------- def test_c5_new_toollists_normalized_into_create(self): """C5: 合法 toolAllowlist/toolDenylist → 归一 (逐条 strip) 后并入 @@ -711,6 +803,76 @@ def test_c5e_new_toollists_with_model(self): self.assertEqual(len(set_calls), 1, "model 与名单并存时 setModel 照常走") self.assertIn("sess_m", bridge.session_map) + # ---------- L: session/list (0.17.0 适配: 不发 includeArchived) ---------- + # 0.17.0 strict Zod 对 includeArchived 报 -32602 Unrecognized key, 桥原无条件 + # false 使 list 整体失败; 0.16.9 实测 {} 与 {includeArchived: false} 等价 + # (2026-10-20, 社区包装器 vendor 0.16.9), 故去键是 era 无关方案。 + _LIST_RESULT = {"result": {"sessions": [ + {"sessionId": "sess_a", "cwd": None, + "workspace": {"workspacePath": "/w/a", "workspaceKey": "/w/a"}, + "title": "fix: bug", "updatedAt": 1754460000000, "status": "idle", + "sessionKind": "cli", "mode": "build", "traceId": "t1", + "createdAt": 1754459000000, "titleSource": "ai"}, + {"sessionId": "sess_b", + "workspace": {"workspacePath": "/w/b", "workspaceKey": "/w/b"}, + "title": None, "updatedAt": None}, + ]}} + + def test_l1_list_no_include_archived_key(self): + """L1: 空参数 → 后端收到 {} (无 includeArchived 键); result 映射不变 + (sessionId/workspace→cwd/title/updatedAt)""" + bridge, fake = self._new_bridge({ + "session/list": {"response": dict(self._LIST_RESULT)}, + }) + resp = self._call(bridge, "session/list", {}) + self._assert_ok(resp) + self.assertEqual(len(fake.calls), 1) + self.assertEqual(fake.calls[0]["method"], "session/list") + self.assertEqual(fake.calls[0]["params"], {}, + "0.17.0 拒 includeArchived 键, 空参数必须发 {}") + sessions = resp["result"]["sessions"] + self.assertEqual(len(sessions), 2) + self.assertEqual(sessions[0], { + "sessionId": "sess_a", "cwd": "/w/a", + "title": "fix: bug", "updatedAt": "2025-08-06T06:00:00+00:00", + }, "SessionInfo 映射 (workspace.workspacePath → cwd, 毫秒→ISO) 不得变") + self.assertEqual(sessions[1], + {"sessionId": "sess_b", "cwd": "/w/b", + "title": None, "updatedAt": None}, + "title/updatedAt 缺失按 None 透出 (旧行为)") + + def test_l2_list_workspace_limit_passthrough(self): + """L2: cwd/limit 照常构造透传 (workspace object + limit), 不夹带其他键""" + bridge, fake = self._new_bridge({ + "session/list": {"response": {"result": {"sessions": []}}}, + }) + self._call(bridge, "session/list", {"cwd": "/w/x", "limit": 5}) + self.assertEqual(fake.calls[0]["params"], { + "workspace": {"workspacePath": "/w/x", "workspaceKey": "/w/x"}, + "limit": 5, + }) + + def test_l3_list_client_include_archived_not_forwarded(self): + """L3: 客户端传 includeArchived → 不透传 (0.17.0 无法透传; archived + 概念疑似已从 0.17.0 移除, 传 true 也按默认行为处理)""" + bridge, fake = self._new_bridge({ + "session/list": {"response": {"result": {"sessions": []}}}, + }) + resp = self._call(bridge, "session/list", {"includeArchived": True}) + self._assert_ok(resp) + self.assertNotIn("includeArchived", fake.calls[0]["params"]) + + def test_l4_list_backend_error_passthrough(self): + """L4: 后端错误 → -32603 "zcode list failed: <原文>" (核心路径不套 + 「已移除」文案, 与既有边界一致)""" + bridge, _ = self._new_bridge({ + "session/list": {"response": {"error": { + "code": -32602, "message": 'Unrecognized key: "includeArchived"'}}}, + }) + resp = self._call(bridge, "session/list", {}) + self._assert_error_code(resp, -32603) + self.assertIn("zcode list failed", resp["error"]["message"]) + # ---------- EV: 事件/轮询模式选择 · 事件分支 ---------- def test_ev1_event_branch_subscribe_deliverykind(self): """EV1: subscribe 带 deliveryKind 成功 → 事件模式 (不触发轮询); 轮询分支见 PF3""" @@ -1360,7 +1522,8 @@ def test_m7_read_state_passthrough(self): self.assertEqual(fake.calls[0]["params"]["runtimeModel"], rm) def test_m7_generate_text_passthrough(self): - """M7a: generateText 透传 modelRef/prompt/querySource/maxOutputTokens/temperature""" + """M7a: generateText 首发 selection 形态 (0.17.0 适配: modelRef 键改名), + 透传 prompt/querySource/maxOutputTokens/temperature""" bridge, fake = self._new_bridge() resp = self._call(bridge, "workspace/generateText", { "workspacePath": "/p", @@ -1369,13 +1532,17 @@ def test_m7_generate_text_passthrough(self): "maxOutputTokens": 512, "temperature": 0.5, }) self._assert_ok(resp) + self.assertEqual(len(fake.calls), 1, "selection 形态直发, 无重试") p = fake.calls[0]["params"] self.assertEqual(fake.calls[0]["method"], "workspace/generateText") - self.assertEqual(p["modelRef"], {"providerId": "zai", "modelId": "glm-5.2"}) + self.assertNotIn("modelRef", p, "0.17.0 strict Zod 拒 modelRef 键, 不得再发") + self.assertEqual(p["selection"], {"providerId": "zai", "modelId": "glm-5.2"}, + "客户端 modelRef 的值应放到 selection 键下") self.assertEqual(p["prompt"], "say hi") self.assertEqual(p["querySource"], "editor") self.assertEqual(p["maxOutputTokens"], 512) - self.assertEqual(p["temperature"], 0.5) + self.assertEqual(p["temperature"], 0.5, + "temperature 原样透传 (后端拒收时错误指名道姓, 不静默丢弃)") def test_m7_generate_text_timeout_60(self): """M7b: generateText timeout=60 (涉及模型调用)""" @@ -1385,6 +1552,8 @@ def test_m7_generate_text_timeout_60(self): "modelRef": {"providerId": "zai", "modelId": "glm-5.2"}, "prompt": "x", }) self.assertEqual(fake.calls[0]["timeout"], 60) + self.assertEqual(fake.calls[0]["params"]["selection"], + {"providerId": "zai", "modelId": "glm-5.2"}) def test_m7_generate_text_default_query_source(self): """M7c: generateText 缺 querySource → 默认 "bridge" """ @@ -1410,6 +1579,120 @@ def test_m7_generate_text_missing_prompt(self): "modelRef": {"providerId": "zai", "modelId": "glm-5.2"}}) self._assert_error_code(resp, -32602) + # ----- M7f+: generateText era 回退与错误指引 (0.17.0 适配) ----- + # 老构建 (只收 modelRef, ≤App 3.12.3 的 0.16.5) 对 selection 形态的键级拒收 + # 形态 (参照同族 strict Zod 实测: Unrecognized key "selection") + _LEGACY_REJECT = {"error": {"code": -32602, "message": + 'Invalid params — (root): Unrecognized key: "selection"'}} + _OK_RESULT = {"result": {"text": "收到", "selection": {"providerId": "zai", + "modelId": "glm-5.3"}, "finishReason": "stop", + "usage": {"outputTokens": 45}}} + + def test_m7f_generate_text_legacy_fallback_model_ref(self): + """M7f: 后端键级拒收 selection (-32602 Unrecognized key) → 一次性用 + modelRef 形态重试 (全量原始参数), 重试成功即成功""" + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(self._LEGACY_REJECT), + dict(self._OK_RESULT)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "GLM-5.3"}, + "prompt": "say hi", "querySource": "editor", + "maxOutputTokens": 512, "temperature": 0.5, + }) + self._assert_ok(resp, "老构建 modelRef 重试应成功") + self.assertEqual(len(fake.calls), 2, "应恰为 selection + modelRef 两次") + self.assertEqual(fake.calls[1]["params"]["modelRef"], + {"providerId": "zai", "modelId": "GLM-5.3"}) + self.assertNotIn("selection", fake.calls[1]["params"]) + self.assertEqual(fake.calls[1]["params"]["temperature"], 0.5, + "重试带全量原始参数 (老构建 temperature 未删)") + self.assertEqual(fake.calls[1]["params"]["maxOutputTokens"], 512) + + def test_m7f_generate_text_legacy_fallback_both_fail(self): + """M7f2: 重试仍失败 → 透传第二次错误 (modelRef 形态的后端裁定)""" + second = {"error": {"code": -32602, + "message": 'Invalid params — (root): Unrecognized key: "modelRef"'}} + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(self._LEGACY_REJECT), + dict(second)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "m"}, "prompt": "x"}) + self._assert_error_code(resp, -32603, + msg="重试仍失败经 _passthrough_error 包装 -32603") + self.assertIn("modelRef", resp["error"]["message"], + "应透传重试 (第二次) 的错误原文") + + def test_m7g_generate_text_no_fallback_on_content_error(self): + """M7g: -32602 但特征是 selection 内容非法 (expected object 型, 无 + unrecognized) → 不重试 (避免改写成第二次更离谱的错误), 透传原错误""" + content_err = {"error": {"code": -32602, "message": + "Invalid params — selection: Invalid input: expected object, " + "received string"}} + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(content_err)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "m"}, "prompt": "x"}) + self._assert_error_code(resp, -32603, msg="经 _passthrough_error 包装") + self.assertEqual(len(fake.calls), 1, "内容错误不触发 modelRef 重试") + self.assertIn("expected object", resp["error"]["message"]) + + def test_m7h_generate_text_max_tokens_error_enriched(self): + """M7h: 省略 maxOutputTokens → 后端 -32603 range 错透传 + 附加指引 + (不改写后端原文; 不重试 — -32603 非键级拒收)""" + range_err = {"error": {"code": -32603, + "message": "maxOutputTokens is outside the model option range"}} + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(range_err)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "glm-5.3"}, "prompt": "x"}) + self._assert_error_code(resp, -32603) + self.assertEqual(len(fake.calls), 1) + msg = resp["error"]["message"] + self.assertIn("zcode generateText failed", msg, "保留透传形态") + self.assertIn("maxOutputTokens is outside the model option range", msg, + "后端原文不改写") + self.assertIn("必填", msg, "附加 selection 构建必填指引") + + def test_m7i_generate_text_reasoning_level_error_enriched(self): + """M7i: 缺 options.reasoningLevel → -32603 透传 + 附加指引""" + lvl_err = {"error": {"code": -32603, + "message": "Reasoning level is required for zai/glm-5.3"}} + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(lvl_err)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "glm-5.3"}, + "prompt": "x", "maxOutputTokens": 1024}) + self._assert_error_code(resp, -32603) + msg = resp["error"]["message"] + self.assertIn("Reasoning level is required for zai/glm-5.3", msg) + self.assertIn("reasoningLevel", msg) + + def test_m7j_generate_text_temperature_rejected_passthrough(self): + """M7j: temperature 在 selection 构建已删 → 后端 -32602 Unrecognized key + temperature (文本无 selection) → 不重试, 错误指名道姓透传""" + temp_err = {"error": {"code": -32602, "message": + 'Invalid params — (root): Unrecognized key: "temperature"'}} + bridge, fake = self._new_bridge({ + "workspace/generateText": {"response": [dict(temp_err)]}, + }) + resp = self._call(bridge, "workspace/generateText", { + "workspacePath": "/p", + "modelRef": {"providerId": "zai", "modelId": "glm-5.3"}, + "prompt": "x", "temperature": 0.5}) + self._assert_error_code(resp, -32603, msg="经 _passthrough_error 包装") + self.assertEqual(len(fake.calls), 1, "temperature 拒收不触发 modelRef 重试") + self.assertIn("temperature", resp["error"]["message"]) + # ---------- M8: workspace/setDefault* 三件套 ---------- def test_m8_set_default_model(self): """M8: setDefaultModel 透传 model + 乐观锁 expectedWorkspaceRevision""" From 7f3ea190669b457bb974d51a4265e8990b93def9 Mon Sep 17 00:00:00 2001 From: tizerluo <192086140+tizerluo@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:55:58 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20R1=20=E5=85=A8=E4=BF=AE=20(7P2+8P3)?= =?UTF-8?q?=20+=20=E7=B2=BE=E7=AE=80=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2: 重试判定收紧为键名精确匹配 (嵌套键拒收如 selection: Unrecognized key "variant" 不再误触发, M7k 9 子测试钉死); includeArchived 丢弃加 log (裁定不加回退——0.17 已删 archived 概念); casefold 折叠歧义显式 报错并建议对象形态 (静默绑定任意 provider 危险); recheck 漂移证据链 修正 (/expert 与 --mode guarded 非漂移证据, 'App 3.15.3' 实为 zcode-app-cli 包装器版本——全仓误标清零, 结论降级为 vendor vs 官方 参数面差异); 「0.16.5 仍收 modelRef」如实标推断; toolAllowlist/ toolDenylist 0.17 真机探测补记 (字符串数组通含 * 通配, 桥 #53 本就 只发字符串——双代兼容零改动)。 P3: 折叠措辞四处 (0.16 既有大写调用方不变/新增小写解析)/README maxOutputTokens 必填+temperature 拒收+下限 1024/最小环境标题双版本/ 指引文案'当前构建'/折叠与重试各加 log/agent-help guarded 注记归位 --mode 条目/0.17 mock 拟真度 (嵌套 sessionId)/返回键变化 README 提醒 (裁定不做 alias)。 精简: 折叠轮单趟合并 -12 行/includeArchived 日志压 2 行/测试措辞 ×2/recheck 测试数字实测核实。 608 passed + 67 subtests; ruff 0 告警。 --- README.md | 10 ++- docs/recheck-3.16.2.md | 24 +++--- packages/acp-bridge/zcode-acp-bridge | 106 +++++++++++++++++---------- packages/agent-help/zcode-agent-help | 19 +++-- tests/test_app_server_methods.py | 87 ++++++++++++++++++++-- 5 files changed, 178 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 9945c55..b5f965b 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 | `session/setModel` | 切换会话模型 | 0.14.8 | `{sessionId, model}` — model 是 ModelSelection 对象 `{providerId, modelId, options?: {reasoningLevel}}`(0.16 后端 schema 要对象;旧 `modelId` 字符串形态从未通过 0.16 schema,已移除)。缺 `reasoningLevel` 时部分模型 turn 阶段报 `ModelProtocolError: Reasoning level is required` | | `session/setMode` | 切换会话权限模式 | 0.14.8 | `{sessionId, mode}` | -> **session/new 可选 `model` 参数**:`{cwd, mode?, model?}` —— model 可传 catalog 里的 modelId 字符串(如 `"GLM-5.3"`,按 create 快照解析成完整 ModelSelection;catalog 条目带 `reasoning.defaultLevel` 时补默认 reasoningLevel;0.17.0 起精确未命中再按 casefold 折叠匹配并取 catalog 规范 ID——旧大写习惯 `"GLM-5.3"` 在 0.17.0 小写 catalog 上自动解析到 `glm-5.3`,0.16 大写 catalog 精确先命中、行为不变)或完整对象(`{providerId, modelId, options?}`,两者须为非空字符串,原样透传不做 ID 改写;`model: null` 或省略等同未提供)。create 成功后桥内部经 `session/setModel` 应用,失败则 session/new 整体报错(文案带已创建的 sessionId,且该会话不登记、不可经 session/prompt 使用)。会话创建即锁定模型,不依赖 App 侧「上次使用」默认(headless 场景刚需)。不透传给 create 本身:0.16.9 实测 create 的初始 model 形参丢 options。 +> **session/new 可选 `model` 参数**:`{cwd, mode?, model?}` —— model 可传 catalog 里的 modelId 字符串(如 `"GLM-5.3"`,按 create 快照解析成完整 ModelSelection;catalog 条目带 `reasoning.defaultLevel` 时补默认 reasoningLevel;0.17.0 起精确未命中再按 casefold 折叠匹配并取 catalog 规范 ID——旧大写习惯 `"GLM-5.3"` 在 0.17.0 小写 catalog 上自动解析到 `glm-5.3`;0.16 既有大写调用方行为不变,新增的是小写/混合大小写写法的解析能力;折叠命中多个条目时显式报错建议改对象形态,不静默绑定)或完整对象(`{providerId, modelId, options?}`,两者须为非空字符串,原样透传不做 ID 改写;`model: null` 或省略等同未提供)。create 成功后桥内部经 `session/setModel` 应用,失败则 session/new 整体报错(文案带已创建的 sessionId,且该会话不登记、不可经 session/prompt 使用)。会话创建即锁定模型,不依赖 App 侧「上次使用」默认(headless 场景刚需)。不透传给 create 本身:0.16.9 实测 create 的初始 model 形参丢 options。 > **session/new 可选 `toolAllowlist` / `toolDenylist` 参数**:字符串数组,在会话创建时按名过滤工具(**工具集注册级物理过滤**:先于权限层、与 mode 无关,hook/权限确认都覆盖不了)。键缺省 → 不写入 create 参数(引擎默认全量工具,与旧行为一致);键存在 → 每个元素须为 strip 后非空的字符串,非法(非数组、元素非字符串、空串,**含显式 `null`**)本地 `-32602` 且不触碰后端——fail-closed,安全参数不静默忽略。条目 strip 后透传,字面 `[]` 原样透传(空 allowlist = 全部禁用,空 denylist = 无限制)。allow+deny 同传时引擎取**交集且 deny 优先**。用于只读监督:0.16 stdio 下 `mode=plan` 只是 advisory(0.16.1 实测工具照常自动执行,快照 diff 只是事后发现),名单是唯一的会话级前置只读手段;建议同时 deny Node REPL 族(`js` / `js_reset` / `js_add_node_module_dir` / `mcp__node_repl__js*`),否则可经 `execSync` 打穿 `Bash` 黑名单(先例见上文 `zcode_review` 只读原理)。 @@ -182,7 +182,7 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 | 扩展方法 | 作用 | params | |----------|------|--------| | `workspace/readState` | 读工作区状态(settings/modelCatalog/slashCommands) | `{workspace, runtimeModel?}` | -| `workspace/generateText` | 一次性文本生成(不建会话) | `{workspace, modelRef, prompt, querySource, maxOutputTokens?, temperature?}`(桥把 modelRef 映射为后端的 `selection` 键发送——selection 构建起 `maxOutputTokens` 必填、`options.reasoningLevel` 必填(zai 系)、`temperature` 已删;见下行注) | +| `workspace/generateText` | 一次性文本生成(不建会话) | `{workspace, modelRef, prompt, querySource, maxOutputTokens, temperature?}`(`maxOutputTokens` 在 selection 构建起**必填**(去问号);`temperature` 已被拒收——见下行注。桥把 modelRef 映射为后端的 `selection` 键发送) | | `workspace/setDefaultModel` | 设工作区默认模型(持久化) | `{workspace, model, runtimeModel?, expectedWorkspaceRevision?}` | | `workspace/setDefaultMode` | 设工作区默认权限模式(持久化) | `{workspace, mode, expectedWorkspaceRevision?}` | | `workspace/setDefaultThoughtLevel` | 设工作区默认思考强度(持久化) | `{workspace, thoughtLevel, expectedWorkspaceRevision?}` | @@ -192,7 +192,9 @@ ACP bridge 额外暴露了 ZCode 新版协议方法,供编辑器/脚本调用 > ❌ **App 3.12.3 的 0.16.5 构建已删**:除 `workspace/generateText`(存活)外,上表 7 个 workspace/* 方法均已从 app-server 删除(2026-09-17 实测后端 -32601,且未搬家到 session/ 命名空间);`updateInteractionPreferences` 同批删除。0.16.9(App 3.14.0)复测同面(2026-09-19);0.17.0(App 3.16.2)复测同面(2026-10-20)。上表语义适用于 0.15.0 – App 3.10.2 的 0.16.5 构建。桥透传调用会收到「当前 ZCode 版本已移除该能力」降级文案(见下方降级行为)。 > -> ⚠️ **`workspace/generateText` 的 selection 语义(早于 0.17.0 引入,0.17.0 延续)**:后端参数键已由 `modelRef` 改名 `selection`,且(实测 App 3.15.3 的同号 0.16.9 构建与 0.17.0 一致)`maxOutputTokens` **必填**(省略报 `maxOutputTokens is outside the model option range`;实测 1024 ≤ 值 ≤ catalog maxOutputTokens 可用)、zai 系模型 `selection.options.reasoningLevel` **必填**(取 catalog `reasoning.defaultLevel`)、`temperature` **已删**。调用方接口保持 `modelRef` 不变(桥负责映射),老构建(≤0.16.5 只收 modelRef)由桥按后端错误特征一次性回退。省略必填项时错误透传并附修法指引;桥不替调用方发明默认值。 +> ⚠️ **`workspace/generateText` 的 selection 语义(早于 0.17.0 引入,0.17.0 延续)**:后端参数键已由 `modelRef` 改名 `selection`,且(实测 `zcode-app-cli` 3.15.3-37 包装器 vendor 的 0.16.9 与 0.17.0 一致;更老构建「仍收 modelRef」系推断,本仓 0.15/3.12.3 记录只有空 params 存活性、无参数级实测,见 [docs/recheck-3.16.2.md](docs/recheck-3.16.2.md) §③)`maxOutputTokens` **必填**(省略报 `maxOutputTokens is outside the model option range`;实测 1024 ≤ 值 ≤ catalog maxOutputTokens 可用)、zai 系模型 `selection.options.reasoningLevel` **必填**(取 catalog `reasoning.defaultLevel`)、`temperature` **已删**。调用方接口保持 `modelRef` 不变(桥负责映射),老构建(推断 ≤0.16.5 只收 modelRef)由桥按后端键级拒收错误特征一次性回退。省略必填项时错误透传并附修法指引;桥不替调用方发明默认值(fail-loud,不静默截断)。 +> +> ⚠️ **返回键随构建不同(R1 仲裁:只提醒,不做 alias)**:selection 构建返回 `{text, selection, finishReason, usage}`(0.16.9/0.17.0 实测),老 modelRef 构建返回 `{text, modelRef}`——**读 `result.modelRef` 的老调用方在 0.17.0 上会拿到 `undefined`**,请改读 `result.selection`(或只依赖 `text`)。 **prompt 级**(提示词增强,App 3.3.0 引入;❌ **0.16 已全部移除**,无替代): @@ -256,7 +258,7 @@ ZCODE_BASE_URL=https://api.z.ai/api/anthropic ./packages/mcp-server/zcode-mcp-se > ⚠️ **env 注入的边界**:以上 env 覆盖在**有完整 `~/.zcode` 配置的机器上无法验证效果**(配置文件兜底掩盖了 env 是否真的被读取);而**最小环境实测中 env 完全不被读取**(0.16.9 官方 runtime 报 `Model creation failed`)。桥保留现有注入行为不变(对既有用户无害);服务器/容器等最小环境还需要完成模型引导,见下一节。 -### 最小 headless 环境的模型引导(0.16.9 官方 runtime) +### 最小 headless 环境的模型引导(0.16.9/0.17.0 官方 runtime) 在有完整桌面配置的机器上,模型选择由 `~/.zcode` 下的配置文件兜底,桥的 env 注入是否生效不可分辨;但在**最小环境**(空 HOME、仅入口旁有内置 provider 文件)实测中,官方 runtime **完全不读取** `ZCODE_MODEL` / `ZCODE_BASE_URL` / `ANTHROPIC_API_KEY`,`--prompt` 直接失败退出。服务器/容器部署需要按本节准备: diff --git a/docs/recheck-3.16.2.md b/docs/recheck-3.16.2.md index 487965c..3f4968f 100644 --- a/docs/recheck-3.16.2.md +++ b/docs/recheck-3.16.2.md @@ -10,8 +10,8 @@ 1. `session/list` 的 strict Zod 拒收 `includeArchived` 键(桥原无条件发 `false` 使 list 整体失败)→ 桥去键(era 无关); 2. 模型身份改名 `builtin:zai-coding-plan/GLM-5.3` → **`zai/glm-5.3`**(小写、精确大小写敏感)→ 桥的 session/new 字符串 model 增加 casefold 折叠回退(catalog 中介,用规范 ID); 3. `workspace/generateText` 参数键 `modelRef` → **`selection`**(且该改名**早于 0.17.0**)→ 桥先发 selection、键级拒收特征时一次性回退 modelRef。 -- 社区包装器 `zcode-app-cli` 3.16.2-38 已 vendor `zcode-runtime` 0.17.0(2026-10-20 当天升级),与官方 App 3.16.2 在桥相关面上实测行为一致(`includeArchived` 同样被拒、`{}`/`{limit}`/`{workspace}` 同样接受)。「同号构建漂移」再添实锤:`zcode-app-cli` 3.15.3-37 vendor 的 0.16.9 与官方 App 3.14.0 的 0.16.9 构建内容不同(前者 generateText 已收 selection、help 已列 `--mode guarded` 与 `/expert`)。 -- CLI 旗标面:与 App 3.15.3 的 vendor 0.16.9 逐项 diff 后**唯一确证新增 `--toolsearch-mode `**(默认 off);`--disallowed-tools` 的 help 标签现带 camelCase 别名形态(`--disallowed-tools, --disallowedTools `)。slash 命令 15 个不变。 +- 社区包装器 `zcode-app-cli` 3.16.2-38 已 vendor `zcode-runtime` 0.17.0(2026-10-20 当天升级),与官方 App 3.16.2 在桥相关面上实测行为一致(`includeArchived` 同样被拒、`{}`/`{limit}`/`{workspace}` 同样接受)。**generateText 参数面漂移**:`zcode-app-cli` 3.15.3-37 包装器 vendor 的 0.16.9 与官方 0.16.9(App 3.14.0,本仓记录)的 generateText 参数面不同(前者已只收 `selection`)——注意官方 0.16.9 当轮只做过空 params 存活性复测、未做参数级复测,差异归因(官方 0.16.9 构建 vs vendor 提取过程)无法进一步分辨(R1 P2-4)。 +- CLI 旗标面:与 `zcode-app-cli` 3.15.3-37 包装器 vendor 的 0.16.9 逐项 diff 后**唯一确证新增 `--toolsearch-mode `**(默认 off);`--disallowed-tools` 的 help 标签现带 camelCase 别名形态(`--disallowed-tools, --disallowedTools `)。slash 命令 15 个不变。 - 最小 headless 环境:**模型引导配方在 0.17.0 上仍可用,但机制注记需修正**——0.17.0 runtime 把入口解析到真实路径(符号链接不再保持入口目录),符号链接入口 + 邻置 `provider/` 的组合失效;两条实测可行路径见 §7。 ## 实证环境 @@ -26,7 +26,7 @@ ### 1. 测试套件 -适配后 `python3.14 -m pytest tests/ -q` → **606 passed + 58 subtests**(适配前基线 591 + 56;+15 用例见 §4)、`ruff check packages/ tests/` 全绿。 +适配后 `python3.14 -m pytest tests/ -q` → **608 passed + 67 subtests**(适配前基线 591 + 56;净增 17 用例 + 11 子测试,其中 R1 审查轮 +2 用例 +9 子测试,见「适配后的测试清单」)、`ruff check packages/ tests/` 全绿。 ### 2. app-server 协议(活体探测,stdio 发无 jsonrpc 信封的 NDJSON) @@ -42,6 +42,10 @@ - **存活**:`session/setModel`、`setMode`、`setThoughtLevel`、`fork`、`compact`、`goal`、`cancelBackgroundTask`、`workspace/generateText`、`session/list`、`session/update`——与 0.16.9 基线一致。 - **已删(-32601,删除面 12 项不变)**:`workspace/readState`、`setDefaultModel`、`setDefaultMode`、`setDefaultThoughtLevel`、`upsertModelProvider`、`removeModelProvider`、`updateProviderRegistry`(workspace/* 8 删 7);`session/updateRuntimeModelConfig`;`updateInteractionPreferences`;`session/steer`、`session/rewind`、`prompt/enhance`。桥的泛化降级文案无需调整。 +### 4. toolAllowlist/toolDenylist(#53 工具名单,零代码改动确认) + +主代理真机实测(2026-10-20,官方 0.17.0):session/create 的 `toolAllowlist`/`toolDenylist` **字符串数组形态全通**(含 `"*"` 通配条目);**对象条目被拒**(`expected string, received object`)。桥 #53 的本地校验本就只接受字符串数组且 fail-closed(元素非 strip 后非空字符串即本地 -32602、不触碰后端),发往后端的一律是字符串数组——两代构建自动兼容,本 PR 零代码改动(R1 P2-7)。 + ## ⚠️ 三处破坏与修法(本 PR 适配) ### ① `session/list` 拒收 `includeArchived` @@ -54,23 +58,23 @@ - 0.17.0 create 快照 catalog(`settings.model.available`,30 项)条目形态:`{ref:{providerId, modelId}, label, providerLabel, contextWindow, maxOutputTokens, reasoning:{defaultLevel, levels}}`。zai 条目:`glm-5.3` / `glm-5.3-flash`(contextWindow 1,000,000,maxOutputTokens 128,000,defaultLevel 均 max)+ **`glm-5-turbo` 回归**(contextWindow 200,000,maxOutputTokens 64,000,defaultLevel enabled;该模型曾于 App 3.12.3 时代从服务端下架)。 - 新 ID **精确大小写敏感**:`glm-5.3` 可用,`GLM-5.3` 报「不存在 Model」。显式选模型**强制要求 `options.reasoningLevel`**(实测 setModel `{providerId:"zai", modelId:"glm-5.3", options:{reasoningLevel:"max"}}` → ok;不带 options → 报 `Reasoning level is required for zai/glm-5.3`)。 -- **桥的行为**:session/new 字符串形态传新 ID `"glm-5.3"` **全通**(catalog 解析 + defaultLevel 补全设计天然适应改名);传旧习惯 `"GLM-5.3"` 原本报「不在本会话的模型目录中」。**修法**:精确匹配失败后按 casefold 对 catalog 的 `ref.modelId` 做第二轮匹配,命中则用 **catalog 规范 ID** 构建 selection(绝不用调用方原始串)——旧习惯 "GLM-5.3" 在 0.17.0 自动解析到 "glm-5.3",0.16 大写 catalog 精确先命中、折叠轮不触发,行为不变(真机 E2E 复核:0.17.0 上 `model: "GLM-5.3"` 创建会话成功)。 +- **桥的行为**:session/new 字符串形态传新 ID `"glm-5.3"` **全通**(catalog 解析 + defaultLevel 补全设计天然适应改名);传旧习惯 `"GLM-5.3"` 原本报「不在本会话的模型目录中」。**修法**:精确匹配失败后按 casefold 对 catalog 的 `ref.modelId` 做第二轮匹配,命中则用 **catalog 规范 ID** 构建 selection(绝不用调用方原始串)——旧习惯 "GLM-5.3" 在 0.17.0 自动解析到 "glm-5.3"(0.16 既有大写调用方行为不变——精确轮先命中;新增的是小写/混合大小写写法的解析能力;折叠命中多个条目时显式报歧义错,建议对象形态指定 providerId,不静默绑定;真机 E2E 复核:0.17.0 上 `model: "GLM-5.3"` 创建会话成功)。 - **不做** providerId 前缀映射魔法(`builtin:zai-coding-plan`→`zai` 无法从 catalog 泛化推导):对象形态传旧 providerId 继续由后端报错透传(文案清晰)。 - 两条命名空间并存:headless env 的 `ZCODE_MODEL` 仍用 config models key 原样(`GLM-5.3` 大写,0.17.0 实测可用);app-server 的 catalog/setModel/generateText 用小写 `glm-5.3`。 ### ③ `workspace/generateText` 参数键 `modelRef` → `selection` -- **改名早于 0.17.0**:实测 App 3.12.3 的 0.16.5 构建仍收 `modelRef`(本仓 2026-09-17 记录),而 `zcode-app-cli` 3.15.3-37 vendor 的 0.16.9 **已只收 selection**(`modelRef` → `-32602 Unrecognized key`)——改名发生在两个同号 0.16.9 构建之间的某次漂移。桥按「先发 selection、键级拒收特征时一次性回退 modelRef」覆盖两端。 +- **改名早于 0.17.0**:`zcode-app-cli` 3.15.3-37 包装器 vendor 的 0.16.9 **已只收 selection**(实测 `modelRef` → `-32602 Unrecognized key`),而更老构建「仍收 `modelRef`」系**推断**(本仓 0.15 时代参数表与 App 3.12.3 的 0.16.5 复测都只有空 params 存活性记录,无参数级实测;R1 P2-5)——桥的 modelRef 回退路径建立在该推断上,故回退判定只认键级拒收错误特征、重试失败透传第二次错误,推断不成立时行为退化为「错误透传」,无害。桥按「先发 selection、键级拒收特征时一次性回退 modelRef」覆盖两端。 - **maxOutputTokens 之谜解开**:省略 `maxOutputTokens` → `-32603 maxOutputTokens is outside the model option range`(0.16.9 selection 构建与 0.17.0 **同样如此**,非 0.17.0 新增——服务端不再提供可用默认值,不是客户端参数越界)。实测能通组合:**1024 ≤ maxOutputTokens ≤ catalog maxOutputTokens(glm-5.3 / glm-5.3-flash 均 128,000)+ `options.reasoningLevel` 必填**(取 catalog `reasoning.defaultLevel`);显式 128,000 与 1024 均通过并返回真实模型输出;超上限(对 128,000 的模型发 131,072)同样报 range 错。 - **temperature 已删**:selection 构建发 `temperature` → `-32602 Unrecognized key: "temperature"`(0.16.9 selection 构建同样拒收)。桥照旧透传让错误指名道姓,不静默丢弃。 - **era 取舍**:不做双发(strict Zod 拒未知键,selection+modelRef 并列必 -32602);也**不替调用方发明 maxOutputTokens/reasoningLevel 默认值**(桥在无会话上下文时拿不到 catalog,发明默认值会悄悄改变输出截断/推理语义)——省略时让后端错误透传并附加一句修法指引(错误文案可读)。回退判定只认 `-32602` 且错误文本提及 `modelRef`、或 `selection`+`unrecognized`(键级拒收形态);selection 内容非法("expected object" 型)不触发重试,避免把内容错误改写成第二次更离谱的 `Unrecognized key: "modelRef"`。 - result 键随构建不同:selection 构建返回 `{text, selection, finishReason, usage}`(老构建为 `{text, modelRef}`),桥原样透传。 -### 适配后的测试清单(+15 用例,tests/test_app_server_methods.py) +### 适配后的测试清单(+17 用例,tests/test_app_server_methods.py) -- **L1–L4(session/list)**:空参数发 `{}` 无 includeArchived 键 + result 映射不变;cwd/limit 照常透传;客户端传 includeArchived 不透传;后端错误保持 `-32603 "zcode list failed"` 透传。 -- **C4o–C4s(casefold 折叠)**:0.17 小写 catalog + 旧习惯 "GLM-5.3" 命中规范 ID 并补 defaultLevel;精确命中不触发折叠;折叠无命中报原错误(带原始串);catalog 同存仅大小写不同的两个 ID 时精确优先不串 ID;0.16 大写 catalog 反向折叠可用。 -- **M7a 改写 + M7f–M7j(generateText)**:首发 selection 键(值取自客户端 modelRef);键级拒收 → modelRef 一次性重试(全量参数)且重试失败透传第二次错误;内容型 -32602 不重试;maxOutputTokens range 错透传 + 追加「必填」指引(后端原文不改写);`Reasoning level is required` 透传 + 指引;temperature 拒收不重试、错误指名道姓。 +- **L1–L4(session/list)**:空参数发 `{}` 无 includeArchived 键 + result 映射不变;cwd/limit 照常透传;客户端传 includeArchived 不透传**且打丢弃日志**(R1 P2-2);后端错误保持 `-32603 "zcode list failed"` 透传。 +- **C4o–C4t(casefold 折叠)**:0.17 小写 catalog + 旧习惯 "GLM-5.3" 命中规范 ID 并补 defaultLevel;精确命中不触发折叠;折叠无命中报原错误(带原始串);catalog 同存仅大小写不同的两个 ID 时精确优先不串 ID;0.16 大写 catalog 反向折叠可用;**折叠命中多个条目(跨 provider 同名)显式报歧义错、不静默绑定**(R1 P2-3)。 +- **M7a 改写 + M7f–M7k(generateText)**:首发 selection 键(值取自客户端 modelRef);键级拒收 → modelRef 一次性重试(全量参数)且重试失败透传第二次错误;内容型 -32602 不重试;maxOutputTokens range 错透传 + 追加「必填」指引(后端原文不改写);`Reasoning level is required` 透传 + 指引;temperature 拒收不重试、错误指名道姓;**重试判定 helper 直接单测**:键级拒收(`Unrecognized key: "selection"`/复数键列首 selection/modelRef required/missing)才触发,嵌套键(`selection: Unrecognized key: "variant"`)、其他键(temperature)、内容型、非 -32602 一律不触发(R1 P2-1)。 ## CLI 旗标面(0.17.0 vs 0.16.9) @@ -79,7 +83,7 @@ - **新增**:`--toolsearch-mode `(默认 off,仅本次启动生效)——唯一确证增量。 - **标签形态变化**:`--disallowed-tools` 的 help 标签现为 `--disallowed-tools, --disallowedTools `(camelCase 别名上标签;别名此前已兼容可用)。 - **其余全部一致**:`-h/-v/-p/--prompt`、`--memory-bench`、`--browser-use`+`--browser-executable`、`--surface`、`--attach`、`--cwd`、`--force-mcs`、`--locale`、`--mode`、`--resume`、`--target`+`--target-replace`、`--workflow-mode`、`-c/--continue`、`--json`、`--no-browser`、`--no-color`、`--verbose`。`--mode` 帮助文案两代均列 `build, edit, plan, yolo, or guarded`(`guarded` 未实测,勿盲用;0.16.1 实测的 `auto` 保留值未实现结论继续有效)。`--allowed-tools`/`--max-turns`/`--permission-mode`/`--allow-main-worktree-yolo` 继续不存在。 -- **与主代理按本仓 0.16.9(App 3.14.0)记录对比出的 6 项「新增」(--attach/--force-mcs/--surface/--toolsearch-mode/--verbose/--workflow-mode)说明**:后 5 项在 App 3.15.3 的 vendor 0.16.9 help 里已存在——同号构建漂移使「新增」的归因依赖基准构建,唯一两代构建都确认新增的只有 `--toolsearch-mode`。子命令 9 个不变(`login` 自 0.16.9 起带 `[zai|bigmodel]` 标签);slash 命令 15 个不变(`/expert` 在 vendor 0.16.9 已在列,不能归因 0.17.0)。 +- **与主代理按本仓 0.16.9(App 3.14.0)记录对比出的 6 项「新增」(--attach/--force-mcs/--surface/--toolsearch-mode/--verbose/--workflow-mode)说明**:后 5 项在 `zcode-app-cli` 3.15.3-37 包装器 vendor 的 0.16.9 help 里已存在——「新增」的归因依赖基准构建,唯一两代构建都确认新增的只有 `--toolsearch-mode`。子命令 9 个不变(`login` 自 0.16.9 起带 `[zai|bigmodel]` 标签);slash 命令 15 个不变(`/expert`、`--mode guarded` 两代 help 均列,**不是**漂移证据——R1 P2-4)。 ## 最小 headless 环境的模型引导(0.17.0 复测,#57 配方) diff --git a/packages/acp-bridge/zcode-acp-bridge b/packages/acp-bridge/zcode-acp-bridge index ff83513..b4114f6 100755 --- a/packages/acp-bridge/zcode-acp-bridge +++ b/packages/acp-bridge/zcode-acp-bridge @@ -63,14 +63,16 @@ CLI 0.17.0 (App 3.16.2) 适配 (依据 docs/recheck-3.16.2.md, 全部经实测 stop/list/resume) 与删除面均零改动兼容。 - 模型身份改名 builtin:zai-coding-plan/GLM-5.3 → zai/glm-5.3 (小写, 精确 大小写敏感): session/new 字符串 model 在 catalog 精确未命中后按 casefold - 回退, 用 catalog 规范 ID 构建 selection — 旧习惯 "GLM-5.3" 自动适配, - 0.16 大写 catalog 行为不变 (精确先命中)。zai 系模型 reasoningLevel 强制 + 回退, 用 catalog 规范 ID 构建 selection — 旧习惯 "GLM-5.3" 自动适配 + (0.16 既有大写调用方行为不变 — 精确轮先命中; 新增的是小写/混合大小写 + 写法的解析能力; 折叠歧义显式报错)。zai 系模型 reasoningLevel 强制 必填 (缺它 turn/generateText 报 "Reasoning level is required"), 字符串 形态的 defaultLevel 补全设计天然满足。 - workspace/generateText 参数键 modelRef → selection (该改名早于 0.17.0: - App 3.15.3 的同号 0.16.9 构建已只收 selection; maxOutputTokens 自改名起 - 必填, temperature 已删): 桥先发 selection, -32602 键级拒收特征时一次性 - 回退 modelRef (老构建), 见 _on_workspace_generate_text。 + zcode-app-cli 3.15.3-37 包装器 vendor 的 0.16.9 已只收 selection, 推断 + 为 0.16.x 期间的漂移; maxOutputTokens 自改名起必填, temperature 已删): + 桥先发 selection, -32602 键级拒收特征时一次性回退 modelRef (老构建), + 见 _on_workspace_generate_text。 能力范围: ✅ initialize 握手 @@ -1551,11 +1553,14 @@ class ACPBridge: 不敏感匹配, 命中则用 catalog 的规范 ID 构建 selection (绝不用调用方 原始串) —— 0.17.0 把模型身份从 builtin:zai-coding-plan/GLM-5.3 改为 zai/glm-5.3 (小写, 精确大小写敏感), 旧习惯 "GLM-5.3" 借此自动解析到 - "glm-5.3"; 0.16 系后端 (大写 ID) 精确匹配先命中, 折叠轮不触发, 行为 - 不变。不做 providerId 前缀映射魔法 (builtin:zai-coding-plan→zai 无法 - 从 catalog 泛化推导): 对象形态传旧 providerId 继续由后端报错透传。 - 无法解析时返回错误字符串, 「catalog 不可用」与「未命中」分开报 (调用 - 方转 -32602 并附上已创建的 sessionId)。 + "glm-5.3" (0.16 既有大写调用方行为不变 — 精确轮先命中; 新增的是 + 小写/混合大小写写法的解析能力)。折叠命中多个条目 (跨 provider 仅 + 大小写不同的同名 ID 共存) 时报错并建议改用对象形态指定 providerId — + 静默绑定任意 provider 危险 (R1 P2-3)。不做 providerId 前缀映射魔法 + (builtin:zai-coding-plan→zai 无法从 catalog 泛化推导): 对象形态传旧 + providerId 继续由后端报错透传。无法解析时返回错误字符串, 「catalog + 不可用」「未命中」「折叠歧义」分开报 (调用方转 -32602 并附上已创建 + 的 sessionId)。 """ if isinstance(model_param, dict): return model_param @@ -1584,18 +1589,14 @@ class ACPBridge: selection["options"] = {"reasoningLevel": default_level} return selection - # 第一轮: 精确匹配 (大小写敏感; 与旧行为逐字一致) - for entry in available: - if not isinstance(entry, dict): - continue - ref = entry.get("ref") - if not isinstance(ref, dict) or ref.get("modelId") != wanted: - continue - return _build_selection(entry) - # 第二轮: casefold 折叠匹配 (仅精确未命中时触发)。多 provider 折叠 - # 同名同精确轮规则取第一个 — 两个仅大小写不同的规范 ID 共存时命中者 - # 依赖 catalog 顺序, 与既有「同名取第一个」口径一致。 + # 单趟双轮语义: 精确命中立即返回 (大小写敏感, 逐字一致优先, 多 + # provider 同名取第一个 — 与旧行为一致); 否则 casefold 命中收进 + # fold_hits, 趟后统一裁决。折叠命中多个条目 (跨 provider 仅大小写 + # 不同的同名 ID 共存) 视为歧义: 静默绑定任意 provider 会把请求打到 + # 错误供应商, 显式报错让调用方改对象形态指定 providerId (R1 P2-3; + # 精确命中的「同名取第一个」不受影响 — 那是逐字一致的 ID)。 folded = wanted.casefold() + fold_hits = [] for entry in available: if not isinstance(entry, dict): continue @@ -1603,11 +1604,20 @@ class ACPBridge: if not isinstance(ref, dict): continue model_id = ref.get("modelId") - if not isinstance(model_id, str): - continue - if model_id.casefold() != folded: - continue + if model_id == wanted: + return _build_selection(entry) + if isinstance(model_id, str) and model_id.casefold() == folded: + fold_hits.append((entry, ref)) + if len(fold_hits) == 1: + entry, hit_ref = fold_hits[0] + log(f' model "{wanted}" → catalog {hit_ref.get("modelId")} ' + f"({hit_ref.get('providerId')}) (casefold 折叠命中)") return _build_selection(entry) + if len(fold_hits) > 1: + pairs = ", ".join(f"{r.get('providerId')}/{r.get('modelId')}" + for _, r in fold_hits) + return (f"modelId '{wanted}' 大小写折叠后在模型目录中匹配到多个条目 " + f"({pairs}); 请改用对象形态 ({{providerId, modelId}}) 指定 providerId") return f"modelId '{wanted}' 不在本会话的模型目录中" def _on_session_list(self, msg_id, params): @@ -1620,10 +1630,14 @@ class ACPBridge: `{includeArchived: false}` 返回逐项等价 (数量与 result 形状), 故直接 去键是 era 无关方案, 0.16 默认行为不变。取舍: ACP 客户端传入的 includeArchived 不再透传 (0.17.0 无法透传; archived 会话概念疑似已 - 从 0.17.0 移除), 传 true 也按默认行为处理。 + 从 0.17.0 移除), 传 true 也按默认行为处理并打一行日志 — 不做回退机制 + (0.17 已删 archived 概念, 垂死能力不值得一条重试路径; R1 仲裁)。 """ self.ensure_backend() cwd = params.get("cwd") + if "includeArchived" in params: + log(f"session/list: 忽略客户端 includeArchived=" + f"{params['includeArchived']!r} (0.17.0 strict Zod 拒收该键, 不透传)") zc_params = {} if cwd: zc_params["workspace"] = {"workspacePath": cwd, "workspaceKey": cwd} @@ -2039,18 +2053,26 @@ class ACPBridge: def _generate_text_needs_legacy_model_ref(err): """判定 generateText 的 -32602 是否为「后端不认识 selection 键」的旧构建特征。 - selection 键的引入早于 0.17.0 (实测 App 3.12.3 的 0.16.5 构建仍收 - modelRef, App 3.15.3 的同号 0.16.9 构建已只收 selection), 桥无法预知 - 对端构建, 只能先发 selection、按错误特征识别旧构建后重试 modelRef。 - 特征: -32602 且文本提到 modelRef, 或 selection + unrecognized (键级 - 拒收形态)。selection 内容非法时后端报 "expected object" 型 -32602、 - 无 unrecognized, 不触发重试 (避免把内容错误改写成第二次更离谱的 - "Unrecognized key: modelRef")。 + selection 键的引入早于 0.17.0 (zcode-app-cli 3.15.3-37 包装器 vendor + 的 0.16.9 已只收 selection; 更老构建「仍收 modelRef」系推断 —— 本仓 + 0.15/3.12.3 时代记录只有参数表与空 params 存活性, 无参数级实测, 见 + docs/recheck-3.16.2.md §③), 桥无法预知对端构建, 只能先发 selection、 + 按错误特征识别旧构建后重试 modelRef。 + + 特征只认**键级拒收** (R1 P2-1): "Unrecognized key(s)" 后紧跟的键名 + 恰为 selection/modelRef 才触发 —— 嵌套键拒收 (如 `selection: + Unrecognized key: "variant"`, 键名是 variant) 与其他键被拒 + (如 temperature) 都不得触发, 否则会把内容错误改写成第二次更离谱的 + "Unrecognized key: modelRef"。selection 内容非法时后端报 "expected + object" 型 -32602 (无 Unrecognized key), 同样不触发。另一分支兼容 + 老构建的必填缺失文案 (modelRef ... required/missing)。 """ if err.get("code") != -32602: return False low = str(err.get("message") or "").lower() - return "modelref" in low or ("selection" in low and "unrecognized" in low) + if re.search(r"unrecognized key[s]?:?\s*\"?(selection|modelref)\"?", low): + return True + return bool(re.search(r"modelref.*(required|missing)", low)) def _on_workspace_generate_text(self, msg_id, params): """workspace/generateText → 同名: 一次性文本生成 (不创建会话)。 @@ -2113,22 +2135,26 @@ class ACPBridge: resp, _ = self.backend.request( zc_id, "workspace/generateText", _params("selection"), timeout=60) if "error" in resp and self._generate_text_needs_legacy_model_ref(resp["error"]): - # 旧构建 (只收 modelRef) 的一次性重试; 结果无论成败都按原样透传 + # 旧构建 (只收 modelRef, 推断 ≤0.16.5) 的一次性重试; 结果无论成败 + # 都按原样透传 + log("workspace/generateText: selection 键被拒 → modelRef 形态重试") zc_id = self._next_id() resp, _ = self.backend.request( zc_id, "workspace/generateText", _params("modelRef"), timeout=60) if "error" in resp: perr = self._passthrough_error(msg_id, "workspace/generateText", resp) - # selection 构建的两类 -32603 后端文案不含修法, 附加一句指引 - # (只追加、不改写后端原文; 其他错误原样透传) + # 当前构建的两类 -32603 后端文案不含修法, 附加一句指引 (只追加、 + # 不改写后端原文; 其他错误原样透传)。措辞用「当前构建」而非 + # 「selection 构建」: 回退到 modelRef 形态后后端也可能报同两类错 + # (R1 P3-4)。 emsg = str(resp["error"].get("message") or "") if "maxOutputTokens" in emsg and "range" in emsg: perr["error"]["message"] += ( - " (selection 构建起 maxOutputTokens 必填: 显式传 ≤ 目标模型" - " catalog maxOutputTokens 的值, 如 4096)") + " (当前构建 maxOutputTokens 必填: 实测 1024 ≤ 值 ≤ 目标模型" + " catalog maxOutputTokens 可用, 如 4096)") elif "Reasoning level is required" in emsg: perr["error"]["message"] += ( - " (selection 构建下 zai 系模型需要 options.reasoningLevel," + " (当前构建下 zai 系模型需要 options.reasoningLevel," " 可取 catalog reasoning.defaultLevel)") return perr result = resp.get("result", {}) diff --git a/packages/agent-help/zcode-agent-help b/packages/agent-help/zcode-agent-help index fbf786d..cc0934a 100755 --- a/packages/agent-help/zcode-agent-help +++ b/packages/agent-help/zcode-agent-help @@ -382,7 +382,8 @@ NATIVE_CLI = { "yolo": "全自动, 跳过确认 (--prompt 默认)", }, "note": "0.16.1 headless 仅接受 build/edit/plan/yolo; auto 是 app-server schema " - "保留值未实现 (setMode 切 auto 会全拒工具调用, mode.auto.unimplemented)", + "保留值未实现 (setMode 切 auto 会全拒工具调用, mode.auto.unimplemented); " + "0.16.9/0.17.0 两代 help 文案另列 guarded (未实测, 勿盲用)", "example": "zcode --prompt \"review\" --mode plan"}, # 浏览器 {"category": "browser", "name": "browser-use", "supported": True, @@ -480,7 +481,8 @@ NATIVE_CLI = { "(需 Memory 功能已启用)", "arg": "--memory-bench", "since": "0.16.9"}, # 0.17.0 (App 3.16.2) help 新增的旗标, 2026-10-20 实测在列 (与 - # App 3.15.3 的同号 0.16.9 构建逐项 diff 后的唯一旗标增量) + # zcode-app-cli 3.15.3-37 包装器 vendor 的 0.16.9 逐项 diff 后的 + # 唯一旗标增量) {"category": "agency", "name": "toolsearch-mode", "supported": True, "description": "ToolSearch 模式: auto|native|local|off (默认 off, 仅本次启动生效)", "arg": "--toolsearch-mode ", "since": "0.17.0"}, @@ -514,7 +516,7 @@ NATIVE_CLI = { ], # slash 命令 (TUI 会话内使用; /side /btw 未见于 0.16.1/0.16.5/0.16.9 --help, 出自 App 3.6.5 changelog; # 0.16.5 help 实测 slash 列表 14 个; 0.16.9 help 实测 15 个 (新增 /dwf); 0.17.0 help 实测 15 个不变 - # (与 App 3.15.3 的同号 0.16.9 构建逐项一致), 与本清单除 /side /btw 外一致) + # (与 zcode-app-cli 3.15.3-37 包装器 vendor 的 0.16.9 逐项一致), 与本清单除 /side /btw 外一致) "slash_commands": [ {"command": "/help [command]", "description": "slash 命令帮助"}, {"command": "/login", "description": "Z.AI OAuth 登录", "since": "0.16.1"}, @@ -644,8 +646,8 @@ APP_SERVER = { {"method": "session/setMode", "params": "sessionId, mode", "note": "0.15.0+ 暴露: 切换会话权限模式 (mode: plan|build|edit|yolo|auto); " "⚠ auto 为保留值未实现 — 切 auto 后该会话所有工具调用被 " - "mode.auto.unimplemented 拒绝, 勿当'智能自动批准'用; " - "0.17.0 help 的 --mode 文案另列 guarded (与 auto 同样未实测, 勿盲用)"}, + "mode.auto.unimplemented 拒绝, 勿当'智能自动批准'用 " + "(guarded 注记见 --mode 条目)"}, # ----- 0.15.0 (App 3.2.0) 新增 workspace/* 方法 (按工作区定位, 非 session) ----- {"method": "workspace/readState", "params": "workspace:{workspacePath,workspaceKey}, runtimeModel?", "note": "0.15.0+: 读工作区状态 (settings/modelCatalog/slashCommands); " @@ -653,10 +655,13 @@ APP_SERVER = { {"method": "workspace/generateText", "params": "workspace, selection{providerId, modelId, options:{reasoningLevel}}, prompt, querySource, maxOutputTokens", "note": "0.15.0+: 一次性文本生成 (不建会话, 无 messages/tools); " "App 3.12.3 的 0.16.5 构建实测仍存活 (workspace/* 8 删 7 中唯一幸存); " - "selection 构建起 (早于 0.17.0, App 3.15.3 的同号 0.16.9 已如此): 参数键 " + "selection 构建起 (早于 0.17.0: zcode-app-cli 3.15.3-37 包装器 vendor 的 " + "0.16.9 已如此; 更老构建仍收 modelRef 系推断, 无参数级实测): 参数键 " "modelRef → selection、maxOutputTokens 必填 (省略报 \"outside the model " "option range\", 实测 1024 ≤ 值 ≤ catalog maxOutputTokens 可用)、temperature " - "已删 (Unrecognized key); 桥先发 selection、键级拒收时一次性回退 modelRef"}, + "已删 (Unrecognized key); 桥先发 selection、键级拒收时一次性回退 modelRef; " + "result 键随构建不同: 新构建 {text, selection, finishReason, usage}, " + "老构建 {text, modelRef}"}, {"method": "workspace/setDefaultModel", "params": "workspace, model, runtimeModel?, expectedWorkspaceRevision?", "note": "0.15.0+: 设工作区默认模型 (持久化, 乐观锁); " "❌ 已于 App 3.12.3 的 0.16.5 构建移除 (实测返 -32601)"}, diff --git a/tests/test_app_server_methods.py b/tests/test_app_server_methods.py index ff00757..b12aab7 100644 --- a/tests/test_app_server_methods.py +++ b/tests/test_app_server_methods.py @@ -610,9 +610,10 @@ def test_c4n_new_string_model_malformed_settings_32602(self): "model 未生效的会话不得登记") # ---------- C4o+: 大小写折叠回退 (0.17.0 模型 ID 改名 GLM-5.3 → glm-5.3) ---------- - # 0.17.0 (App 3.16.2) 的 catalog 形态: providerId "zai" + 小写 modelId + # 0.17.0 (App 3.16.2) 的 catalog 形态: providerId "zai" + 小写 modelId; + # sessionId 在 result.session.sessionId (0.17 实测嵌套形态, mock 按真) _CATALOG_017 = { - "sessionId": "sess_m17", + "session": {"sessionId": "sess_m17"}, "settings": {"model": {"available": [ {"ref": {"providerId": "zai", "modelId": "glm-5.3"}, "label": "GLM-5.3", "providerLabel": "Z.ai Coding Plan", @@ -701,6 +702,31 @@ def test_c4s_new_model_fold_works_on_upper_catalog_too(self): {"providerId": "bigmodel-api", "modelId": "GLM-5.3", "options": {"reasoningLevel": "max"}}) + def test_c4t_new_model_fold_ambiguous_reports_error(self): + """C4t (R1 P2-3): 折叠命中多个条目 (跨 provider 仅大小写不同的同名 ID) + → 显式报错不猜 (静默绑定任意 provider 会打到错误供应商), 文案列出 + 命中对并建议对象形态指定 providerId; 不发 setModel、不登记""" + ambiguous = {"sessionId": "sess_amb", "settings": {"model": {"available": [ + {"ref": {"providerId": "legacy", "modelId": "Glm-5.3"}, + "label": "Glm-5.3", "providerLabel": "legacy"}, + {"ref": {"providerId": "zai", "modelId": "glm-5.3"}, + "label": "glm-5.3", "providerLabel": "zai"}, + ]}}} + bridge, fake = self._new_bridge({ + "session/create": {"response": {"result": ambiguous}}, + }) + resp = self._call(bridge, "session/new", {"cwd": "/p", "model": "GLM-5.3"}) + self._assert_error_code(resp, -32602) + msg = resp["error"]["message"] + self.assertIn("多个条目", msg, f"应报折叠歧义: {msg}") + self.assertIn("legacy/Glm-5.3", msg, "应列出全部命中对") + self.assertIn("zai/glm-5.3", msg) + self.assertIn("providerId", msg, "应建议对象形态指定 providerId") + self.assertIn("sess_amb", msg, "文案带已创建 sessionId 供收编") + self.assertEqual([c for c in fake.calls + if c["method"] == "session/setModel"], []) + self.assertNotIn("sess_amb", bridge.session_map) + # ---------- C5: session/new 工具名单透传 (只读监督的会话级前置手段) ---------- def test_c5_new_toollists_normalized_into_create(self): """C5: 合法 toolAllowlist/toolDenylist → 归一 (逐条 strip) 后并入 @@ -807,8 +833,10 @@ def test_c5e_new_toollists_with_model(self): # 0.17.0 strict Zod 对 includeArchived 报 -32602 Unrecognized key, 桥原无条件 # false 使 list 整体失败; 0.16.9 实测 {} 与 {includeArchived: false} 等价 # (2026-10-20, 社区包装器 vendor 0.16.9), 故去键是 era 无关方案。 + # 条目键集按 0.17.0 实测: [createdAt, mode, sessionId, sessionKind, status, + # title, titleSource, traceId, updatedAt, workspace] — 无 cwd 键。 _LIST_RESULT = {"result": {"sessions": [ - {"sessionId": "sess_a", "cwd": None, + {"sessionId": "sess_a", "workspace": {"workspacePath": "/w/a", "workspaceKey": "/w/a"}, "title": "fix: bug", "updatedAt": 1754460000000, "status": "idle", "sessionKind": "cli", "mode": "build", "traceId": "t1", @@ -854,13 +882,20 @@ def test_l2_list_workspace_limit_passthrough(self): def test_l3_list_client_include_archived_not_forwarded(self): """L3: 客户端传 includeArchived → 不透传 (0.17.0 无法透传; archived - 概念疑似已从 0.17.0 移除, 传 true 也按默认行为处理)""" + 概念疑似已从 0.17.0 移除, 传 true 也按默认行为处理), 且打一行日志 + 说明丢弃原因 (R1 P2-2: 显式传入的调用方值得一句 stderr, 不做回退)""" bridge, fake = self._new_bridge({ "session/list": {"response": {"result": {"sessions": []}}}, }) - resp = self._call(bridge, "session/list", {"includeArchived": True}) + captured = io.StringIO() + with contextlib.redirect_stderr(captured): + resp = self._call(bridge, "session/list", {"includeArchived": True}) self._assert_ok(resp) self.assertNotIn("includeArchived", fake.calls[0]["params"]) + err_out = captured.getvalue() + self.assertIn("includeArchived", err_out, "应打丢弃日志") + self.assertIn("True", err_out, "日志应含传入的值") + self.assertIn("0.17", err_out, "日志应说明 0.17 已移除该概念") def test_l4_list_backend_error_passthrough(self): """L4: 后端错误 → -32603 "zcode list failed: <原文>" (核心路径不套 @@ -1580,7 +1615,8 @@ def test_m7_generate_text_missing_prompt(self): self._assert_error_code(resp, -32602) # ----- M7f+: generateText era 回退与错误指引 (0.17.0 适配) ----- - # 老构建 (只收 modelRef, ≤App 3.12.3 的 0.16.5) 对 selection 形态的键级拒收 + # 老构建 (推断只收 modelRef, ≤App 3.12.3 的 0.16.5; 见 recheck §③) 对 + # selection 形态的键级拒收 # 形态 (参照同族 strict Zod 实测: Unrecognized key "selection") _LEGACY_REJECT = {"error": {"code": -32602, "message": 'Invalid params — (root): Unrecognized key: "selection"'}} @@ -1659,7 +1695,7 @@ def test_m7h_generate_text_max_tokens_error_enriched(self): self.assertIn("zcode generateText failed", msg, "保留透传形态") self.assertIn("maxOutputTokens is outside the model option range", msg, "后端原文不改写") - self.assertIn("必填", msg, "附加 selection 构建必填指引") + self.assertIn("必填", msg, "附加当前构建必填指引") def test_m7i_generate_text_reasoning_level_error_enriched(self): """M7i: 缺 options.reasoningLevel → -32603 透传 + 附加指引""" @@ -1693,6 +1729,43 @@ def test_m7j_generate_text_temperature_rejected_passthrough(self): self.assertEqual(len(fake.calls), 1, "temperature 拒收不触发 modelRef 重试") self.assertIn("temperature", resp["error"]["message"]) + def test_m7k_needs_legacy_model_ref_key_level_matching(self): + """M7k (R1 P2-1): 重试判定只认「Unrecognized key 后紧跟的键名恰为 + selection/modelRef」的键级拒收 — 嵌套键 (selection.variant) 与其他键 + (temperature) 被拒不得触发; modelRef required/missing 分支保留; + 非 -32602 一律 False (helper 直接单测, 不经 handler)""" + needs = self.Bridge._generate_text_needs_legacy_model_ref + cases = [ + # (error, 期望, 说明) + ({"code": -32602, + "message": 'Invalid params — (root): Unrecognized key: "selection"'}, + True, "键级拒收 selection"), + ({"code": -32602, + "message": 'Invalid params — (root): Unrecognized keys: "selection", "temperature"'}, + True, "复数键列首为 selection"), + ({"code": -32602, "message": "Invalid params — modelRef: Required"}, + True, "老构建必填缺失文案 (required 分支)"), + ({"code": -32602, "message": "Invalid params — modelRef is missing"}, + True, "老构建必填缺失文案 (missing 分支)"), + ({"code": -32602, + "message": 'Invalid params — (root): Unrecognized key: "temperature"'}, + False, "temperature 被拒不是 modelRef 回退信号"), + ({"code": -32602, + "message": 'Invalid params — selection: Unrecognized key: "variant"'}, + False, "嵌套键拒收 (键名 variant) 不是 modelRef 回退信号"), + ({"code": -32602, + "message": "Invalid params — selection: Invalid input: expected object, received string"}, + False, "selection 内容非法 (expected object 型) 不重试"), + ({"code": -32603, + "message": 'Invalid params — (root): Unrecognized key: "selection"'}, + False, "非 -32602 一律不重试"), + ({"code": -32602, "message": "Internal selection error somewhere"}, + False, "自由文本提到 selection 但非键级拒收, 不触发"), + ] + for err, expected, why in cases: + with self.subTest(why=why, message=err["message"][:50]): + self.assertEqual(needs(err), expected) + # ---------- M8: workspace/setDefault* 三件套 ---------- def test_m8_set_default_model(self): """M8: setDefaultModel 透传 model + 乐观锁 expectedWorkspaceRevision"""