mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 20:27:23 +00:00
对应主管任务 A/B。**没有实现 wasm 接入**,只交付事实结论与可执行方案。 ## A. P1-4 结论固化(docs/channel-repair-plan.md) 原 P1-4 只有一句「阻塞:二进制不在仓库任何位置」,现补成可审计的事实段: - 绝对路径 / 298606 字节 / sha256 6419471e…55b43d(位于第三方插件 `dsh-our-free-model` 的 vendor 目录,**不随本仓分发**;本仓 `git ls-files | grep -i wasm` = 0); - 它是 wasm-bindgen 产物:31 个 import 全来自单一 JS glue 模块 `./qoder_auth_wasm_bg.js`,且该 glue **上游也没提供**(改为手写 `src/qoder-wasm.ts` 706 行)⇒ 代价在宿主侧; - 31 import / 34 export 各分四类,逐类给出用途与成员; - **我们仓库没有 wasm 运行时入口**:`grep -rniE 'wazero|wasmtime|wasmer| tetratelabs'` = 0 命中,`go.sum` wasm 命中 = 0,Lua 侧只有注释。 只写事实,未写「已修复」。 ## B. 可行性判定(docs/qoder-wasm-integration.md,新文件) 实测推翻了一个此前的假定:**不需要 JS 引擎,纯 Go(wazero)可以驱动它**。 证据来自新增的 tools/qoder-wasm-probe(独立 module,主仓 go.mod 不动): - 实例化:31 桩,实例化阶段真实调用 0 次; - level 2 `generate_runtime_auth_fields` → encrypt_user_info/key 各 172 字符; - level 3 `qodercontext_new` + `prepareInferRequest` → 20 个头,其中 `Authorization: Bearer COSY.<载荷>.<签名>`(len=209),body 60 字节密文; - 逐 import 计数:31 个里 **13 个从未被调用**(node/JS 探针居多); - 量化:≈2.2 ms/请求,线性内存 1.25 MiB,实例可复用(-repeat 50 全 PASS)。 文档含:依赖选型(wazero vs wasmtime-go/wasmer-go,+1.16 MiB 实测)、 接入点设计(走 `__llmsproxy_kernel` 而非新开 Lua 全局,保持「按插件声明授权」)、 31 import 逐个的 Go 实现映射表、6 条 ABI 陷阱(两个 getRandomValues 方向相反、 两套返回布局、requestresult_url 参数顺序、drop_ref 必须 no-op、machineId 必填、 host 不能是 api2-v2)、9 条风险、7 层改动面。 ## 为什么本轮不实现 跨 7 层、需新增内核依赖、且三个前置阻塞都不该由 fix 分支暗定: ① wasm 从哪来(不可再分发 / 需运维决策);② 加密端点的**响应也要解密**且是 SSE 逐帧(第二条未实现链路);③ 账号数 0 ⇒ 端到端不可验证。 ## 纪律声明 - 未动 main、未开 PR、未部署、未 restart; - 主仓 `go.mod`/`go.sum` **零改动**(tools/ 是嵌套 module,`go build ./...` 跳过); - `go test -tags luajit ./...` 全绿(core 420s / gateway 206s); - **本机 qoder 账号数 0**:能证明「产出了签名头」,不能证明「上游接受」。 文档内每处 PASS 都标注是离线结论。 ## 顺带更正一处过时事实 `internal/core/channels_upstream_gaps_test.go` 注释称 wasm「在参考仓库里也不存在」。 实测:文件存在(sha256 6419471e…),只是不在**我们**仓库。文档已记,本轮不改 代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
334 lines
22 KiB
Markdown
334 lines
22 KiB
Markdown
# 渠道链路修复计划与现状
|
||
|
||
> 本文是 `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`,逐层剥开)
|
||
|
||
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 <release HEAD>` 非空;`git diff release/v2.0.x main` 为空。
|
||
|
||
### P1 — 其余渠道的同类缺陷(**账号 0,验证不了运行**)
|
||
|
||
按"风险 × 可验证性"排序。**核心限制:这 4 个平台账号数均为 0,
|
||
无法做生产实测** —— 只能靠静态对齐参考 + 装好适配器后看 `__adapter_install_probe`。
|
||
|
||
| 优先 | 项 | 缺陷 | 修法 | 可验证性 |
|
||
|---|---|---|---|---|
|
||
| P1-1 | `anthropic` 基座 | 基座**无 `build_headers`**,Anthropic 协议要 `x-api-key` 不是 Bearer | 给基座补 `build_headers`(`x-api-key` + `anthropic-version`) | ✅ 已修+已判据;运行需账号 |
|
||
| P1-1b | `channels-zcode` | **★ 证据冲突**:zcode 的 Messages 端点虽然**说 Anthropic 协议**,鉴权却是 `Bearer <zcode_jwt>`(参考 `zcode-transport.ts:121-125`,全仓 `x-api-key` 命中 **0** 次)。基座补了 x-api-key 后它**继承就会变错** | zcode **显式覆盖** `build_headers` 发 Bearer,不用继承 | ✅ 已修+已判据;运行需账号 |
|
||
| P1-2 | `channels-cline` | 参考要 `Bearer workos:<jwt>`,我们的 token 无 `workos:` 前缀 | 适配器 `build_headers` 补前缀(幂等,已带不重复) | ✅ 已修+已判据;运行需账号 |
|
||
| P1-3 | `channels-gemini` | 有静态头 + Bearer ⇒ **本来就对**(参考 `gemini.ts:353-366` 也是 OAuth Bearer,且刻意不发 `x-goog-api-key`) | **不改**,只加判据把这个结论钉住 | ✅ 已确认+已判据 |
|
||
| P1-4 | `channels-qoder` | 头由 **WASM 生成必须原样透传**,我们拿不到 `.wasm` | **仍阻塞**:二进制不在我们仓库,且本仓**没有 wasm 运行时入口**。结论与接入方案见下节与 `docs/qoder-wasm-integration.md` | 本机账号 0 ⇒ 运行时不可验证 |
|
||
|
||
> #### ★ P1-1 与 P1-1b 的证据冲突(本轮最重要的一条)
|
||
>
|
||
> 计划原文把 P1-1 写成「anthropic 基座没 `build_headers` ⇒ zcode 继承就 401」。
|
||
> 逆向参考后**两半各对一半**:
|
||
>
|
||
> | 断言 | 结论 | 证据 |
|
||
> |---|---|---|
|
||
> | Anthropic 协议(真 · api.anthropic.com)要 `x-api-key` | ✅ 对 | 协议本身 |
|
||
> | zcode 的 `/zcode-plan/anthropic/v1/messages` 也要 `x-api-key` | ❌ **错** | `zcode-transport.ts:121-125` 发 `Authorization: Bearer ${zcode_jwt}`;`zcode-upstream.ts:83-108` 的 `buildZcodeHeaders` 只有可选 Authorization;全仓 `grep -rn "x-api-key" src/` = **0** |
|
||
>
|
||
> zcode 是「**Anthropic 请求体形状 + Bearer 鉴权**」——协议与鉴权是两个独立维度,
|
||
> 被这家上游拆开了。所以修复不能"把基座改成 x-api-key 就完事":
|
||
> 那会把 zcode 从「对」改成「错」。正确形态是**基座发 x-api-key + zcode 显式覆盖成 Bearer**。
|
||
>
|
||
> 附带约束:`anthropic` 基座**同时服务生产里的其它源**(`/etc/llmsproxy/config.yaml`
|
||
> 里 `justwoker` 就是 `adapter: anthropic`)。给基座补 `x-api-key` 后,内核
|
||
> `buildHeaders` 的兜底 Bearer **必须让位**(否则同一请求带两套鉴权),
|
||
> 这处改动在 `internal/provider/provider.go`(内核改动)。
|
||
|
||
> #### ★ P1-4 的结论:qoder 为什么卡住(事实陈述,**未修复**)
|
||
>
|
||
> 本段只记录查证结果,不代表任何修复已完成。
|
||
>
|
||
> **① 二进制客观存在,但不在我们仓库里。**
|
||
>
|
||
> | 项 | 值 |
|
||
> |---|---|
|
||
> | 绝对路径 | `/opt/dsh-plugins/dsh-our-free-model/2.0.0/vendor/channel-pack/qoder-auth-wasm.wasm` |
|
||
> | 大小 | `298606` 字节(291.6 KiB) |
|
||
> | sha256 | `6419471effa631519def7797d76d7ede38b9fcfa9a83c2148c8ef5d43355b43d` |
|
||
> | 格式 | `WebAssembly (wasm) binary version 0x1 (MVP module)` |
|
||
> | 归属 | 第三方插件 `dsh-our-free-model` 的 vendored 依赖(`NOTICE.md` 记明来自 MIT 许可的上游 channel-pack,**不随本仓分发**) |
|
||
>
|
||
> ⇒ 我们仓库 `git ls-files | grep -i wasm` = **0 条**。任何「我们直接调用它」的实现都是
|
||
> 对**别人安装目录**的硬依赖,换机/升级插件即失效。这是 P1-4 的第一层阻塞。
|
||
>
|
||
> **② 它是 wasm-bindgen 产物,为 JS 宿主生成,不是 self-contained wasm。**
|
||
>
|
||
> 证据(解析二进制 section 得到,非推断):
|
||
> - 只有 **1 个 import module**:`./qoder_auth_wasm_bg.js`,31 个 import **全部**来自它;
|
||
> - import 名带 wasm-bindgen 的哈希与 `__wbindgen_*` ABI 前缀
|
||
> (`__wbindgen_object_drop_ref` / `__wbindgen_object_clone_ref` /
|
||
> `__wbindgen_cast_0000000000000001|2` / `__wbg_static_accessor_GLOBAL_THIS_*`);
|
||
> - 导出含 `__wbindgen_add_to_stack_pointer` / `__wbindgen_export1..4`、`memory`。
|
||
>
|
||
> 结论:它**不是**「喂 WASI 就能跑」的模块,而是要求宿主**自己实现一套 JS 值语义**
|
||
> (对象表 + 索引、`Uint8Array`、`Map`、`crypto.getRandomValues`、`Date.now`)。
|
||
> wasm-bindgen 的标准做法是「随 wasm 一起生成 12 KB 的 JS glue」,而该目录里
|
||
> **没有** `qoder_auth_wasm_bg.js` —— 上游改为手写了一份等价 glue
|
||
> (`vendor/channel-pack/src/qoder-wasm.ts`,706 行,含 31 个 import 的逐个实现)。
|
||
> 这是 P1-4 的第二层阻塞:**代价在宿主侧,不在 wasm 侧**。
|
||
>
|
||
> **③ 31 个 import 按用途分四类。**
|
||
>
|
||
> | 类 | 数量 | 代表 | 纯 Go 宿主能否实现 |
|
||
> |---|---|---|---|
|
||
> | A. bindgen 对象表 ABI | 5 | `__wbindgen_object_drop_ref` / `object_clone_ref` / `cast_..._1` / `cast_..._2` | 能(Go 侧自建 `[]any` 索引表) |
|
||
> | B. 字节缓冲原语 | 6 | `__wbg_new_with_length` / `length` / `subarray` / `set` / `prototypesetcall` / `new_...`(Map) | 能(`[]byte` + `map[string]any`) |
|
||
> | C. 随机源与时间 | 4 | `getRandomValues` **两个方向相反的变体** / `randomFillSync` / `now` | 能(`crypto/rand`、`time.Now`),**但两个变体必须区分**:一个写 wasm 内存,一个调 JS crypto 对象 |
|
||
> | D. JS/Node 环境探针与调用 | 16 | `crypto` / `msCrypto` / `process` / `versions` / `node` / `require` / `call` / `globalThis` / `self` / `window` / `throw` / `Error` / `is_object|string|function|undefined` | 部分能:`self`/`window` 按 JS 惯例返 `0`;`process`/`node`/`require` 返回 `undefined` 即可 —— **前提是从未被调用** |
|
||
>
|
||
> **④ 34 个 export 按用途分四类。**
|
||
>
|
||
> | 类 | 数量 | 成员(去 bindgen plumbing) |
|
||
> |---|---|---|
|
||
> | 1. 鉴权字段生成 | 1 | `generate_runtime_auth_fields` |
|
||
> | 2. 请求构造 / 加解密(**红线所在**) | 13 | `qodercontext_new` · `qodercontext_prepareInferRequest` · `qodercontext_prepareRequest` · `qodercontext_refreshAuthFields` · `qodercontext_get_external_providers_access` · `requestresult_url|body|headers|headerCount` · `decrypt_server_response` · `profile_encrypt` · 2 个 `__wbg_*_free` |
|
||
> | 3. 本地存储 / 缓存 / 凭据加解密 | 8 | `credential_storage_encrypt|decrypt` · `model_cache_encrypt|decrypt` · `profileencryptor_new|encryptChunk|header` · `__wbg_profileencryptor_free` |
|
||
> | 4. 静态常量 / HTTPDNS / 环境探测 | 6 | `get_httpdns_config|account_id|secret_key` · `build_httpdns_url` · `get_external_providers_feature_gate_fallback` · `get_profile_key_fingerprint` |
|
||
> | plumbing | 6 | `memory` · `__wbindgen_export1..4` · `__wbindgen_add_to_stack_pointer` |
|
||
>
|
||
> 只有第 2 类与 qoder 的**推理鉴权**直接相关(`prepareInferRequest` 产出的
|
||
> `Authorization` 是 `Bearer COSY.<载荷>.<签名>`,**必须原样透传**);
|
||
> 第 1 类是它的前置(`encrypt_user_info` / `key`);第 3、4 类在推理路径上不需要。
|
||
>
|
||
> **⑤ 我们仓库当前没有 wasm 运行时入口 —— 这是 P1-4 卡住的直接原因。**
|
||
>
|
||
> | 查证项 | 命令 | 结果 |
|
||
> |---|---|---|
|
||
> | Go 侧 wasm 运行时依赖 | `grep -rniE 'wazero|wasmtime|wasmer|tetratelabs' --include=*.go --include=*.mod --include=*.sum .` | **0 命中** |
|
||
> | `go.mod` 直接依赖 | `cat go.mod` | 仅 `aarzilli/golua` + `yaml.v3` |
|
||
> | `go.sum` 中 wasm 相关 | `grep -ci wasm go.sum` | **0** |
|
||
> | Lua 侧 wasm 能力 | `grep -rniE 'wasm' internal/lua/` | 仅 **注释**(`plugins/channels.lua:141` 明写「WASM 签名(Qoder) 这类无法在当前 Lua 环境完成」) |
|
||
> | 插件 VM 外呼能力 | `internal/lua/plugin_http.go` | 只有受限 HTTP(host 白名单 + 禁重定向 + 4 MiB 上限),**无任意代码执行/无 wasm** |
|
||
>
|
||
> 即:现状是「**Go 内核无 wasm 依赖、Lua 插件无 wasm 能力**」,而 qoder 的
|
||
> 加密端点头必须由该 wasm 生成。我们目前走的是**明文端点**
|
||
> `api2-v2.qoder.sh` + `/model/v1/chat/completions`(见
|
||
> `internal/core/channels_upstream_gaps_test.go` 的判据),因此**不需要** wasm ——
|
||
> 代价是该端点只认一小撮通用模型名,拿不到目录里 `qmodel_*` 系列的完整能力。
|
||
>
|
||
> **⑥ 本机不可验证。** qoder 账号数 **0**(`plan.md` §1.1 快照),
|
||
> 因此「签名头是否被上游接受」这一层**无法做运行时验证**。
|
||
> 能验证的只有静态层:wasm 可实例化、能被驱动产出请求头(见方案文档的实测节),
|
||
> **但「判据通过」≠「功能可用」**。
|
||
>
|
||
> 可执行的最小路径方案与实测记录另见 **`docs/qoder-wasm-integration.md`**。
|
||
|
||
### P2 — 内核对"限定名"的契约收口
|
||
|
||
已落地(详见 **`docs/source-name-contract.md`**):
|
||
|
||
| 位置 | 行为 |
|
||
|---|---|
|
||
| `config.ValidateSourceName` / `SourceNameWarning` | 纯函数校验器(单一定义) |
|
||
| `core.normalizeSource`(`AddSource` 入口) | 新增/修改源**硬拒**(API 400)—— 新数据从源头合法 |
|
||
| `config.ApplyDefaults`(启动) | 存量带 `:` 的源名**只告警不报错**(避免升级 brick 掉整个网关) |
|
||
|
||
字符集:**仅 `[A-Za-z0-9_-]`**,禁 `:` 与 `/`(限定名分隔符 ⇒ 切分歧义且**不报错**)。
|
||
`-` 允许(渠道源名 `channels-<平台>-<账号id>` 含 `-`,分隔符在源名之后不歧义)。
|
||
|
||
解析入口 `internal/provider/splitPinned` **不做校验**(解析不该拒绝已有数据),
|
||
只做最长前缀匹配 —— 校验责任全在写入口,保持解析路径纯函数无副作用。
|
||
|
||
### P3 — 判据防回归(防"夹具与生产不一致"再犯)
|
||
|
||
本轮最大的教训不是某个 bug,而是**夹具形状错 ⇒ 判据虚假通过**(trae 活了两轮)。
|
||
|
||
已落地,分两层(文件:`internal/lua/channels_fixture_shapes_test.go` 行为层 +
|
||
`internal/lua/channels_fixture_shapes_general_test.go` 通用形状层):
|
||
|
||
**① 形状登记表 `shapeFamilies`** —— 每族给出四项,缺一不可:
|
||
|
||
| 字段 | 含义 | 为什么必须有 |
|
||
|---|---|---|
|
||
| `real` | 生产原文(引自实测抓包/参考注释) | 夹具的真值来源 |
|
||
| `naive` | 错误的「方便测试版」 | 证明判据能区分真假,不是恒等式 |
|
||
| `fact(raw)` | 对**原始 JSON 的结构事实** | 判定「这段 body 是不是生产形状」 |
|
||
| `eval` / `assert` | 用**真实实现**处理并断言行为 | 防判据自算一份解包逻辑 |
|
||
|
||
断言链:`fact(real)` 为真 **且** `fact(naive)` 为假 **且** `assert(eval(real))` 通过。
|
||
|
||
> ⚠️ 刻意**不比较 `eval(real)` 与 `eval(naive)` 的输出形状**:trae 余额实现
|
||
> **合法保留**了「顶层 credits_limit」兼容兜底路径(实测两种上都返回 600),
|
||
> 拿输出比对会把合法兼容判成错误。真正能区分真假的是 `fact`
|
||
> —— 它问的是「生产响应里额度在哪一层」。
|
||
|
||
已登记三族(基线,元判据强制不可删):
|
||
- **Result 信封** —— 凭据在 `Result` 里,顶层只有 `ResponseMetadata`
|
||
- **SOLO 自定义 SSE** —— 正文在顶层 `response`,**无** `choices`
|
||
- **嵌套 quota** —— `pack.entitlement_base_info.quota.credits_limit`(顶层**没**该字段)
|
||
|
||
**② 仓库扫描 `TestNoDegenerateFixturesInRepo`** —— 扫 `internal/**/*_test.go`,
|
||
对**特征明确**的退化形状做静态反查(当前只留「额度挪到 pack 顶层」一条规则)。
|
||
需要退化夹具来测兼容路径时,必须在文件里写 `// SHAPE-OK: <理由>` 显式豁免。
|
||
|
||
> 为什么扫描只留一条规则:文本匹配对另两族**必然误报** —— SOLO 判据自己会写
|
||
> `Fatalf("不得返回 choices…")`,天然含 choices。那两族的精确钉法
|
||
> 是 ① 的 `fact` 判据,不是文本匹配。
|
||
|
||
#### 本轮修复的判据清单(P1/P2/P3)
|
||
|
||
| 判据文件 | 用例数 | 钉住什么 |
|
||
|---|---|---|
|
||
| `internal/lua/adapter_anthropic_headers_test.go` | 3 | 基座发 `x-api-key`+`anthropic-version`(恰好 2 键)、不发 Authorization、无凭据不发空头、读 `source.meta.api_key` 兜底 |
|
||
| `internal/lua/adapter_zcode_build_headers_test.go` | 3 | zcode **覆盖**成 `Bearer`、无 `x-api-key`;基座 vs zcode 鉴权**真的不同**;无凭据不发空头 |
|
||
| `internal/lua/channels_cline_workos_test.go` | 3 | 无前缀补上 / 已有不重复 / 大小写变体不重复;无 token 不发 `Bearer workos:` |
|
||
| `internal/lua/channels_gemini_auth_check_test.go` | 2 | **P1-3 结论**:gemini 不自带鉴权(身份五头 + 兜底 Bearer 即正确),不得发 Authorization/x-api-key/x-goog-api-key |
|
||
| `internal/provider/build_headers_fallback_test.go` | 2 | 内核兜底让位给 x-api-key;其它适配器 Bearer 行为**不变** |
|
||
| `internal/config/source_name_contract_test.go` | 4 | 允许字符集、`:`/`/` 点名拒绝、其它非法字符、存量只告警 |
|
||
| `internal/core/source_name_contract_test.go` | 2 | **真实写入口** `AddSource` 硬拒且拒绝发生在写盘之前;合法渠道名照常接受 |
|
||
| `internal/lua/channels_fixture_shapes_test.go` | 4 | 三族行为层:Result 信封 / SOLO SSE / 嵌套 quota + 元判据 |
|
||
| `internal/lua/channels_fixture_shapes_general_test.go` | 3 | 通用形状层:`fact(real)&&!fact(naive)&&assert(eval(real))`;仓库扫描;登记表自检 |
|
||
| `internal/core/adapter_install_probe_test.go` | 1 | `__adapter_install_probe` 报告 6/6 自装适配器全绿(`luac -p` 对长字符串无效) |
|
||
| `internal/lua/adapter_zcode_harness_test.go` | —(辅助) | 兄弟目录测试台:自带 zcode 装载断言,装不上就响亮失败 |
|
||
|
||
### 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 代码变更时递增。
|