对应主管任务 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…),只是不在**我们**仓库。文档已记,本轮不改 代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
22 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 | 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/= 0zcode 是「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_objectstring ④ 34 个 export 按用途分四类。
类 数量 成员(去 bindgen plumbing) 1. 鉴权字段生成 1 generate_runtime_auth_fields2. 请求构造 / 加解密(红线所在) 13 qodercontext_new·qodercontext_prepareInferRequest·qodercontext_prepareRequest·qodercontext_refreshAuthFields·qodercontext_get_external_providers_access· `requestresult_url3. 本地存储 / 缓存 / 凭据加解密 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.v3go.sum中 wasm 相关grep -ci wasm go.sum0 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. 风险与纪律(本轮新增)
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 代码变更时递增。