Files
ModelRouter/docs/channel-repair-plan.md
dev d3bb47bd54 docs(qoder): wasm 接入评估 —— 事实固化 + 可复现取证工具(未实现)
对应主管任务 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…),只是不在**我们**仓库。文档已记,本轮不改
代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
2026-10-18 07:36:49 +08:00

22 KiB
Raw Permalink Blame History

渠道链路修复计划与现状

本文是 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

④ 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
3. 本地存储 / 缓存 / 凭据加解密 8 `credential_storage_encrypt
4. 静态常量 / HTTPDNS / 环境探测 6 `get_httpdns_config
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
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 代码变更时递增。