# 渠道链路修复计划与现状 > 本文是 `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 | `anthropic` 基座 | 基座**无 `build_headers`**,Anthropic 协议要 `x-api-key` 不是 Bearer | 给基座补 `build_headers`(`x-api-key` + `anthropic-version`) | ✅ 已修+已判据;运行需账号 | | P1-1b | `channels-zcode` | **★ 证据冲突**:zcode 的 Messages 端点虽然**说 Anthropic 协议**,鉴权却是 `Bearer `(参考 `zcode-transport.ts:121-125`,全仓 `x-api-key` 命中 **0** 次)。基座补了 x-api-key 后它**继承就会变错** | zcode **显式覆盖** `build_headers` 发 Bearer,不用继承 | ✅ 已修+已判据;运行需账号 | | P1-2 | `channels-cline` | 参考要 `Bearer workos:`,我们的 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 代码变更时递增。