## 背景
用户报三问题:① channel 注册的模型无法使用;② 额度显示依旧异常;
③ 其他渠道应也存在类似问题。三条全部核实并给出结论(③ 见文档)。
## ① 网关解不开 `源名:模型名` 限定名(internal/provider/registry.go)
`ResolvePinned` 用 `strings.IndexAny(model, "-:/")` 找**第一个**分隔符。
而渠道源名形如 `channels-trae-b8dd6b3df30e`(**含 `-`**),于是
`channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro` 被切成
src="channels"、model="trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro"
⇒ 源名永不匹配 ⇒ `Resolve()` 返回 nil
⇒ 客户端看到 `model "..." is not configured`。
**为什么这个写法必须支持**:本实例有 **44 个模型被多源同时提供**
(`glm-5.2` 由 6 个源提供),WebUI 的模型下拉框用 `源名:模型名` 作为
option value 是**有意设计** —— 用户必须能指定"从哪个源调用"。
修法:抽 `splitPinned()`,**按已知源名做最长前缀匹配**(源名是已知集合,
这是唯一可靠办法),保留旧 `-:/` 兜底以兼容非源名形态。
`EffectiveModel` 同步修(同一拆分规则,避免两处漂移)。
## ② trae 适配器三连错(internal/lua/plugins/channels.lua)
三个错误逐层叠加,每个都被上一层掩盖:
**(a) 没有 `build_headers`** ⇒ 内核兜底发 `Authorization: Bearer`,
而 trae 推理端点要 `Cloud-IDE-JWT` + 设备身份头 ⇒
401「We're sorry, but we are not able to authenticate you」。
→ 与上一轮「额度不可用」是**同一根因的两个出口**(额度端点修了、推理端点漏了)。
教训:修"某一族请求的鉴权"时,必须把该族的**所有出口**列出来逐个对齐。
**(b) header 常量的值是错的**(照参考 trae-product.ts 的 TRAE 配置逐项核对):
· `X-App-Id` 抄成 `6eefa6c9`,真实是 `6eefa01c-1036-4c7e-9ca5-d891f63bfcd8`
· `app_version_code` 用了 `0.1.52`,真实是**日期式** `20260811`
⇒ 上游 `4001 bad request: binding: expr_path=app_version_code,
cause=parameter type does not match binding data`
**(c) 流式转换完全不对**:TRAE 是 **SOLO 自定义 SSE**:
event:output → data:{"response":"###","reasoning_content":null,...}
正文在 `response`,推理在 `reasoning_content`。
· 套 `__prelude.openai_stream_chunk` ⇒ 无 `choices` ⇒ `empty stream`
· 改成 OpenAI 外壳 `{choices:[{delta}]}` **仍然空** —— 内核
`json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构**,外壳字段全被丢弃
⇒ 必须输出扁平 `{content, reasoning_content, usage, done, finish_reason}`。
⚠️ 内核只把 `data:` 行交给适配器(`event:` 行在 provider.go 被丢弃),
因此判别帧类型只能靠**内容特征**(`response` / `reasoning_content` /
`finish_reason` / `prompt_tokens`),拿不到 event 名。
## ③ 余额恒为 0(同文件 balance.total_fn)
读的是 `p.credits_limit`(pack **顶层**)—— 该字段**根本不存在**。
真实路径是 `pack.entitlement_base_info.quota.credits_limit`
(参考 trae-credits.ts:341)+ `pack.usage.credits_amount`。
实测上游:`免费`(无该字段) / `每月登录赠送`(500) / `签到奖励`(100) ⇒ 实际 600。
修法:读嵌套路径 + 保留顶层兼容 + `usage_summary.{total,consumed}_amount` 兜底。
修后余额 600 → 实测 598.516(真实扣减)。
## ④ 夹具与生产形状不一致(internal/core/channels_trae_test.go)
夹具原先返回**扁平** `{"Token":...}`,与生产 `{"Result":{"Token":...}}` 不符
⇒ 「代码只读顶层」这个缺陷在夹具里**恰好是对的**,所有 trae 判据都绿。
**夹具形状错 = 判据必然虚假通过**。这是 ExchangeToken 缺陷藏了两轮的原因。
## 判据(新增,变异 4/4 咬住)
- `internal/provider/registry_pinned_name_test.go`(4 用例)
- `internal/core/channels_trae_chat_headers_test.go`(3 条)
- `internal/core/channels_trae_solo_sse_test.go`(2 条)
- `internal/core/channels_trae_credits_hdr_test.go`(1 条)
- `internal/core/channels_trae_exchange_shape_test.go`(2 条)
## 文档
`docs/channel-repair-plan.md`:现状快照(生产实测)+ 各渠道缺陷矩阵 +
后续计划(P0-P4)。**如实记录**:其余 4 个渠道(zcode/cline/gemini/qoder)
有同类缺陷风险,但**账号数均为 0、无法做生产验证**。
## 生产已实测
- `channels-trae-...:Doubao-Seed-2.1-Pro` → 80 帧,正文 27 + 推理 299
- `channels-trae-...:glm-5.2` / `channels-buddy-...:hy4-preview` → OK
- `credits/status` → active:true, dailyCredit:100, todayCheckedIn:true
- `credits/balances` → total:598.516 credits
11 KiB
渠道链路修复计划与现状
本文是
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:<jwt>) |
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,逐层剥开)
- 没有
build_headers⇒ 内核兜底发Bearer,而 trae 推理端点要Cloud-IDE-JWT+ 设备身份头 ⇒ 401「not able to authenticate you」。 → 与「额度不可用」是同一根因的两个出口(额度端点当轮修了、推理端点漏了)。 - 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。 - 流式转换完全不对: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。必须:
- 在
release/v2.0.x上提交(含 3 个源文件 + 6 个判据文件) - 打 tag
v2.0.40(这是内核 Go 改动:registry.go⇒ 真核心版本) - 回流 main(cherry-pick,不 merge)
- 复核 tag 落点 + release vs main 零差异 + 生产 sha256 一致
判据:
git tag --points-at <release HEAD>非空;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:<jwt>,我们的 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-<platform>-<accountId>,只含-/字母数字,安全。
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. 风险与纪律(本轮新增)
luac -p对长字符串内的适配器代码无效(只看外层)⇒ 会得到假绿。 必须用__adapter_install_probe看真实装载结果。本轮我因此把 TRAE 的 SSE 代码误塞进 CODEARTS 适配器,导致 codearts 装载失败而luac一直通过。- Python 字符串替换会静默不生效:锚文本缩进不符 ⇒ 匹配 0 次。
规矩:① 先
cp备份 ② 行号定位 ③ 写后luac -p+grep -c双确认 ④ 长字符串内的改动必须验装载。 function是 Lua 关键字:cc.function是语法错误,必须cc["function"]; 但批量替换时别把cc.function_call一起改坏(本轮踩过,导致')' expected near '_call')。- 判据要咬被测对象:本轮
channels_trae_credits_hdr_test.go第一版造 HTTP 假上游, 因 URL 覆盖/凭据 ref/路径匹配任一环错,"没发请求"伪装成"头不对"(耗了三轮)。 改成直接咬spec.headers_fn的真实产出,一轮就准。 - 只改插件不必换二进制;改内核 Go 必须换。混着发会把"插件改动"记成"核心版本" (v2.0.21–v2.0.39 有 18 个 tag 是这么来的,见 §4)。
/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 代码变更时递增。