# 渠道链路修复计划与现状 > 本文是 `plan.md` 中「渠道链路修复计划(2026-10-17 起)」章节的**可提交版本**。 > `plan.md` 本身被 `.gitignore` 忽略(本地工作台账),故关键计划与现状单独落在这里, > 以便随代码一起进 git、供他人查阅。 > > **写作原则:只写实测到的,标注"推断"的必须写明。** 每条缺陷都附可复现的观测。 > 记录 **2026-10-17 用户报障三连修**(模型不可用 / 额度异常 / 其他渠道同类) > 的现状与后续计划。上接 `plan.md` §9(v2 契约收尾)。 ## 1. 现状快照(2026-10-17 14:00 实测,非推断) ### 1.1 生产实例 | 项 | 值 | |---|---| | 服务 | `active`,NRestarts=0 | | 二进制 sha256 | `56396f79bfc2433a`(与仓库逐字节一致) | | 插件 sha256 | `c15cb4bf5d5c0f21`(与仓库逐字节一致) | | 插件 | 4 个(billing/channels/quota/usage)**全部 loaded** | | 源 / 适配器 | 21 源 / 23 适配器 | | 账号池 | **仅 trae、buddy 各有 1 个有凭据账号**;其余 10 个平台**账号数 0** | ### 1.2 各渠道缺陷矩阵(本文件写入时的实测) 自装适配器(插件在加载时经 `__llmsproxy_adapters` 装入): | 适配器 | `build_headers` | 静态 `adapter.headers` | 流式转换 | 鉴权是否可能错 | |---|---|---|---|---| | `codearts` | 有 | 无 | 复合 openai | 有(SDK-HMAC 自签,已实现) | | `channels-trae` | **有(本次新加)** | 无 | **SOLO 自定义(本次新写)** | 已修 | | `channels-gemini` | **★ 缺** | 有 | 自定义 | 静态头 + Bearer,**待确认** | | `channels-zcode` | **★ 缺** | 无 | 无(继承 anthropic 基座) | **★ 高度可疑** | | `channels-cline` | **★ 缺** | 无 | 套 openai | **★ 可疑**(要 `Bearer workos:`) | | `channels-qoder` | **★ 缺** | 无 | 套 openai | **★ 可疑**(头由 WASM 生成) | 内置适配器里 `build_headers` 只有 3 个有(buddy / kimicode / opencodezen), 其余 11 个(含 **anthropic 基座**)靠静态头 + 内核兜底 `Bearer`。 > **★ anthropic 基座本身没有 `build_headers`**,而 anthropic 协议实际要 > `x-api-key` 而非 `Authorization: Bearer`。`channels-zcode` 继承它 ⇒ > 一旦有账号就会立刻暴露,与 trae 修复前完全同型。 ### 1.3 已验证可用的通道(生产实测,不是单测) | 模型(源限定名) | 结果 | |---|---| | `channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro` | ✅ 80 帧,正文 27 + 推理 299 | | `channels-trae-b8dd6b3df30e:glm-5.2` | ✅ OK | | `channels-buddy-buddy-main:hy4-preview` | ✅ OK | | 端点 | 结果 | |---|---| | `channels/credits/status` (trae) | ✅ `active:true, dailyCredit:100, todayCheckedIn:true` | | `channels/credits/balances` (trae) | ✅ `total:598.516 credits` | ### 1.4 本轮已修(代码已部署生产,**尚未提交 git**) **① 网关解不开 `源名:模型名` 限定名**(`internal/provider/registry.go`) - 根因:`ResolvePinned` 用 `strings.IndexAny(model, "-:/")` 找**第一个**分隔符。 渠道源名形如 `channels-trae-b8dd6b3df30e`(**含 `-`**)⇒ 切成 `src="channels"` ⇒ 永不匹配 ⇒ `Resolve()` 返回 nil ⇒ `model "..." is not configured`。 - 为什么必须支持:本实例 **44 个模型被多源同时提供**(`glm-5.2` 由 6 个源提供), WebUI 下拉框用 `源名:模型名` 作 option value 是**有意设计**。 - 修法:抽 `splitPinned()`,**按已知源名做最长前缀匹配**(源名是已知集合, 唯一可靠办法),保留旧 `-:/` 兜底。`EffectiveModel` 同步修。 - 判据:`internal/provider/registry_pinned_name_test.go`(4 用例,变异 1/1 咬住)。 **② trae 适配器三连错**(`internal/lua/plugins/channels.lua`,逐层剥开) 1. **没有 `build_headers`** ⇒ 内核兜底发 `Bearer`,而 trae 推理端点要 `Cloud-IDE-JWT` + 设备身份头 ⇒ 401「not able to authenticate you」。 → 与「额度不可用」是**同一根因的两个出口**(额度端点当轮修了、推理端点漏了)。 2. **header 常量值是错的**:`X-App-Id` 抄成 `6eefa6c9`(真实 `6eefa01c-1036-4c7e-9ca5-d891f63bfcd8`)、`app_version_code` 用 `0.1.52` (真实是**日期式** `20260811`)⇒ 上游 `4001 binding: expr_path=app_version_code, cause=parameter type does not match binding data`。 3. **流式转换完全不对**:TRAE 是 **SOLO 自定义 SSE** (`event:output` → `data:{"response":"###","reasoning_content":...}`)。 套 `openai_stream_chunk` ⇒ 无 `choices` ⇒ `empty stream`; 改成 OpenAI 外壳 `{choices:[{delta}]}` **仍不对** —— 内核 `json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构**,外壳字段全丢。 ⇒ 必须输出扁平 `{content, reasoning_content, usage, done, finish_reason}`。 - 判据:`channels_trae_chat_headers_test.go`(3)、`channels_trae_solo_sse_test.go`(2), 变异 2/2 咬住。 **③ 余额恒为 0**(同文件 `balance.total_fn`) - 根因:读 `p.credits_limit`(pack **顶层**)—— 该字段**根本不存在**。 真实路径是 `pack.entitlement_base_info.quota.credits_limit` (参考 `trae-credits.ts:341`)+ `pack.usage.credits_amount`。 - 实测上游:`免费`(无 credits_limit) / `每月登录赠送`(500) / `签到奖励`(100) = **600**。 - 修法:读嵌套路径 + 保留顶层兼容 + `usage_summary.{total,consumed}_amount` 兜底。 - 判据:`channels_trae_credits_hdr_test.go`(1)。 - 修后余额 **600 → 实测 598.516**(真实扣减)。 **④ 附带**:`channels_trae_test.go` 夹具从**扁平** `{"Token":...}` 改为真实 `{"Result":{"Token":...}}` —— 原来夹具形状与生产不一致,**判据必然虚假通过** (这是 ExchangeToken 缺陷藏了很久的原因)。 ## 2. 后续计划 ### P0 — 提交本轮修复(必须先做) 代码已部署生产但**未进 git**。必须: 1. 在 `release/v2.0.x` 上提交(含 3 个源文件 + 6 个判据文件) 2. 打 tag `v2.0.40`(**这是内核 Go 改动**:`registry.go` ⇒ 真核心版本) 3. 回流 main(cherry-pick,不 merge) 4. 复核 tag 落点 + release vs main 零差异 + 生产 sha256 一致 > 判据:`git tag --points-at ` 非空;`git diff release/v2.0.x main` 为空。 ### P1 — 其余渠道的同类缺陷(**账号 0,验证不了**) 按"风险 × 可验证性"排序。**核心限制:这 4 个平台账号数均为 0, 无法做生产实测** —— 只能靠静态对齐参考 + 装好适配器后看 `__adapter_install_probe`。 | 优先 | 项 | 缺陷 | 修法 | 可验证性 | |---|---|---|---|---| | P1-1 | `channels-zcode` | 走 anthropic 基座,而**基座无 `build_headers`**,anthropic 要 `x-api-key` 不是 Bearer | 给基座补 `build_headers`(`x-api-key` + `anthropic-version`),zcode 继承即得 | 静态可验;运行需账号 | | P1-2 | `channels-cline` | 参考要 `Bearer workos:`,我们的 token 无 `workos:` 前缀 | 登录/续期处拼前缀,或适配器 `build_headers` 补 | 静态可验;运行需账号 | | P1-3 | `channels-gemini` | 有静态头 + Bearer(参考注释也说 Cloud Code 用 OAuth Bearer)⇒ **可能本来就对** | 只需确认,不改 | 静态可验 | | P1-4 | `channels-qoder` | 头由 **WASM 生成必须原样透传**,我们拿不到 `.wasm` | **阻塞**:二进制不在仓库任何位置 | 不可验证 | ### P2 — 内核对"限定名"的契约收口 `ResolvePinned` / `EffectiveModel` 现在各自调 `splitPinned`。建议: - 把"源名可含哪些字符"写进 `docs/`,并加判据:新增源名若含 `:` 或 `/` 必须报警 (否则限定名会歧义)。 - 现状:源名由插件生成 `channels--`,只含 `-`/字母数字,安全。 ### P3 — 判据防回归(防"夹具与生产不一致"再犯) 本轮最大的教训不是某个 bug,而是**夹具形状错 ⇒ 判据虚假通过**(trae 活了两轮)。 建议加一条**通用**判据:`TestAdaptersFixturesMatchRealUpstreamShape` —— 对每个有夹具的渠道,把夹具响应体与 `/tmp/pi-github-repos/.../vendor/channel-pack/src/*.ts` 里注释记录的真实响应形状 做**结构对照**(键集合),不一致即报红。做不到全自动,至少把已知的 "Result 信封 / SOLO SSE / 嵌套 quota" 三类钉住。 ### P4 — 待用户决策 | 项 | 需要什么 | |---|---| | zcode 真实登录 | 用户点授权(身份块 + init-poll 流已就绪) | | cline / qoder / gemini / codearts / minimax / lobsterai / opencode | 需各平台账号;`device-poll` 系列要用户去官方页输码 | | raccoon / loomy | 需扫码 | | qoder WASM | 用户提供 `.wasm` | ## 3. 风险与纪律(本轮新增) 1. **`luac -p` 对长字符串内的适配器代码无效**(只看外层)⇒ 会得到假绿。 **必须**用 `__adapter_install_probe` 看真实装载结果。本轮我因此把 TRAE 的 SSE 代码误塞进 CODEARTS 适配器,导致 codearts 装载失败而 `luac` 一直通过。 2. **Python 字符串替换会静默不生效**:锚文本缩进不符 ⇒ 匹配 0 次。 规矩:① 先 `cp` 备份 ② **行号定位** ③ 写后 `luac -p` + `grep -c` 双确认 ④ 长字符串内的改动必须验装载。 3. **`function` 是 Lua 关键字**:`cc.function` 是语法错误,必须 `cc["function"]`; 但批量替换时别把 `cc.function_call` 一起改坏(本轮踩过,导致 `')' expected near '_call'`)。 4. **判据要咬被测对象**:本轮 `channels_trae_credits_hdr_test.go` 第一版造 HTTP 假上游, 因 URL 覆盖/凭据 ref/路径匹配任一环错,"没发请求"伪装成"头不对"(耗了三轮)。 改成直接咬 `spec.headers_fn` 的真实产出,一轮就准。 5. **只改插件不必换二进制**;改内核 Go 必须换。混着发会把"插件改动"记成"核心版本" (v2.0.21–v2.0.39 有 18 个 tag 是这么来的,见 §4)。 6. **`/tmp` 是 8.8G tmpfs**,Go 链接失败常见于它满(`no space left on device`)。 元凶通常是 `/tmp/gocache`(可安全清)。 ## 4. 账务实况(必须记住) | 事实 | 值 | |---|---| | 内核 Go 代码自 v2.0.20 起的**全部**改动 | 仅 `internal/lua/adapter_prelude.go`、`internal/provider/provider.go`、`internal/provider/registry.go` | | 只改插件却发了核心 tag 的版本 | v2.0.21–v2.0.39 中的 **18 个** | | 当前最新 tag | `v2.0.39`(`091f790`),落点正确 | | main | `56ea269`(与 release 内容零差异) | | **未提交的改动** | 本轮三个修复(见 §1.4),已部署生产 | > ⇒ **版本号语义教训**:以后"改插件不打核心 tag",插件自身维护版本; > 内核版本只在 Go 代码变更时递增。