Files
ModelRouter/docs/qoder-wasm-integration.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

24 KiB
Raw Permalink Blame History

Qoder WASM 接入评估(qoder-auth-wasm.wasm)

状态:未实现(not implemented)。 本文给出已实测的可行性结论与可执行的接入方案,但我们仓库里 没有任何 wasm 运行时、也没有任何代码加载这个 wasm。 本文写于 fix/channel-auth-headers 分支,仅文档 + 评测工具,不动内核。

判据 ≠ 功能可用。 本文所有「PASS」都是本机静态/离线结论: qoder 账号数 0,所以「签名头是否被 Qoder 上游接受」没有做过运行时验证。


1. 结论先行

问题 结论 依据
wasm 能不能被加载? 能,且不需要 JS 引擎 —— 纯 Go(wazero)即可实例化 31 个 import 全部由 Go 侧桩提供,实例化阶段真实调用 0 次
能不能真正跑出鉴权头? 能,且已实测做到 prepareInferRequest 产出 20 个头,含 Authorization: Bearer COSY.<载荷>.<签名>,body 为 60 字节加密串
31 个 import 要全部实现吗? 不用。实际路径只触碰 18 个,其中 5 个是 node/JS 环境探针(process/node/require/versions/call),本路径一次都没调用 逐 import 计数(§4 表)
最小可行接入是什么? 新增一个 build_headers 后端(wazero),由适配器把 meta 交给它,拿回 headers 原样透传 §5
本轮能做进 fix/ 分支吗? 不能。见 §7 改动面与风险 —— 这属于主分支级改动 §7
风险最高的是什么? 不是 wasm 调用,而是二进制来源(不在我们仓库,也不可再分发)与账号数为 0 无法端到端验证 §6
性能可以接受吗? 可以。实测 ≈ 2.2 ms/请求,线性内存 ≈ 1.25 MiB,实例可复用 §8

⚠️ 一条红线:qoder 加密端点要求 Authorization 原样透传。 任何「适配器里再兜底一次 Bearer <api_key>」的写法都会覆盖签名,得到 403 Signature invalid。这必须在接入时由代码结构保证,不能只靠注释 (见 §5.4)。


2. 目标物:abs 路径 / 大小 / sha256

项 值
绝对路径 /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 条

同一目录下的相关文件(都不在我们仓库里):

文件 作用
pack.js(2.8 MB) 插件运行时实际加载的单文件 bundle
src/qoder-wasm.ts(706 行) 手写的 wasm-bindgen glue —— 31 个 import 的逐个实现 + ABI 助手。本文的 Go 实现即照它翻译
NOTICE.md / LICENSE 出处与许可(MIT)

qoder_auth_wasm_bg.js(wasm-bindgen 本该生成的那份 glue)在该目录中 不存在 —— 上游改用手写的 qoder-wasm.ts 替代。这一点决定了接入代价 落在宿主侧,而这正好是可以由 Go 承担的部分。


3. 为什么它是「wasm-bindgen 产物,给 JS 用」

解析二进制各 section(不是推断):

  • 只有 1 个 import module:./qoder_auth_wasm_bg.js,31 个 import 全部来自它;
  • import 名同时带有 wasm-bindgen 哈希后缀(__wbg_set_08463b1df38a7e29)与 __wbindgen_* ABI 前缀;
  • 导出含 ABI plumbing:memory、__wbindgen_add_to_stack_pointer、__wbindgen_export1..4;
  • 返回多值靠写栈槽传递(ptr/len/valIdx/isErr),这是 wasm-bindgen 的约定。

⇒ 它不是「喂 WASI 就能跑」的模块。它要求宿主实现 JS 值语义 (对象表 + 索引、Uint8Array、Map、crypto.getRandomValues、Date.now)。 --export-all 之类参数改不了这一点:ABI 就写在 import 表里。


4. 31 import / 34 export 的四类用途(附实测触碰次数)

4.1 imports(31 个,全部来自 ./qoder_auth_wasm_bg.js)

类 # 成员 纯 Go 可否 实测触碰
A. bindgen 对象表 ABI 4 __wbindgen_object_drop_ref、object_clone_ref、cast_0000000000000001、cast_0000000000000002 ✅ 自建 []any 索引表 3(cast_..2×40、clone×2、drop×65)
B. 字节缓冲 / Map 6 new_with_length、length、subarray、set、prototypesetcall、new_99cabae(Map) ✅ []byte + map 6(全部触碰)
C. 随机源与时间 4 getRandomValues 两个方向相反的变体、randomFillSync、now ✅ crypto/rand、time.Now 3(randomFillSync 未被调用)
D. JS/Node 环境探针 17 crypto、msCrypto、process、versions、node、require、call、globalThis、self、window、throw、Error、is_object、is_string、is_function、is_undefined 部分:self/window 返 0;process/node 等从不被调用即可 4(crypto×1、globalThis×1、self×1、window×1、is_object×1、is_undefined×1)

实测合计:31 个中 13 个「从未被调用」。 未触达的 13 个:

__wbg_process_44c7a14e11e9f69e          __wbg_versions_276b2795b1c6a219
__wbg_node_84ea875411254db1             __wbg_require_b4edbdcf3e2a1ef0
__wbg_msCrypto_bd5a034af96bcba6         __wbg_randomFillSync_6c25eac9869eb53c
__wbg_call_d578befcc3145dee             __wbg_static_accessor_GLOBAL_f2e0f995a21329ff
__wbg___wbindgen_throw_81fc77679af83bc6 __wbg_Error_2e59b1b37a9a34c3
__wbg___wbindgen_is_string_b29b5c5a8065ba1a
__wbg___wbindgen_is_function_49868bde5eb1e745
__wbindgen_cast_0000000000000001

这是本路径的观测(generate_runtime_auth_fields + qodercontext_new

  • prepareInferRequest)。错误分支会用到 throw/Error,所以实现里 仍应给它们一个真实语义,而不是留空。

4.2 exports(34 个)

类 # 成员 与本接入的关系
1. 鉴权字段生成 1 generate_runtime_auth_fields 必需(encrypt_user_info / key 是 qodercontext_new 的前置)
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、__wbg_qodercontext_free、__wbg_requestresult_free 核心:本接入只需其中 6 个
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 必需

即:34 个 export 里,接入只需要 1 + 6 + plumbing。 其余 27 个不影响「能不能发出一条被签名的 qoder 请求」。


5. 最小可行接入方案(设计,未实现)

5.1 依赖选型

候选 结论 理由
github.com/tetratelabs/wazero v1.9.0 ✅ 推荐 纯 Go、CGO_ENABLED=0 可交叉编译、零传递依赖(go list -m all 只有它自己);go 1.22 下限,本仓 go 1.25 无冲突
wasmtime-go ❌ 需要 CGO + 预编译动态库 ⇒ 破坏 go build 的单二进制分发与 Windows 交叉编译
wasmer-go ❌ 同上,且维护活跃度低
自己写解释器 ❌ 不值当
用 Node 子进程跑上游 pack.js ⚠️ 备选 现有 node 可跑,但把「一个 Go 单二进制网关」变成「依赖宿主 node + 一份 2.8 MB 外部 bundle」,运维形态倒退

体积实测(CGO_ENABLED=0,空 main):

构建 大小
空程序 1,895,775 B
+ wazero(仅引用 NewRuntime) 3,106,358 B
增量 ≈ +1.16 MiB

交叉编译已验证:GOOS=windows GOARCH=amd64 CGO_ENABLED=0 出 3,050,496 B, 与现有 gui-win 流程兼容。

5.2 二进制从哪来(必须先解决)

wasm 不入库会带来三个问题:换机失效、插件升级后 sha256 漂移、离线构建不安全。 三条可选路径,推荐 ①:

方案 做法 代价
① 运维侧提供,启动时校验(推荐) 新增配置项 qoder_wasm_path;启动/首次使用时读文件,校验 sha256 必须等于 6419471e…55b43d,不符则拒绝启用并报错 需要运维放一份文件;但不引入许可问题,且校验把「用错版本」变成显式失败
② 我们仓库 go:embed 把 wasm 放进 internal/qoderwasm/assets/ 并 embed ✅ 单二进制、无外部依赖;❌ 许可与再分发需先确认(上游 MIT,但需保留 NOTICE),且 291 KiB 进 git
③ 运行时从上游插件目录读(我们现在的路径) 直接读 /opt/dsh-plugins/... ❌ 对别人安装目录硬依赖,不建议进主干

本轮不选任何一条 —— 选型本身就是需要主管决策的事项(涉及再分发与 运维约定),不该由一条 fix 分支暗定。

5.3 接入点:放在哪

现架构事实(grep 实测):

  • 适配器是 Lua(internal/lua/adapters/*.lua + 插件自装 channels-qoder);
  • channels-qoder 目前没有 build_headers,靠静态头 + 内核兜底 Bearer;
  • 内核统一入口是 Provider.buildHeaders(internal/provider/provider.go:1104) → VM.BuildHeaders(internal/lua/vm.go:1077)→ Lua adapter.build_headers(meta);
  • go.mod 里没有任何 wasm 运行时;internal/ 下 wasm 相关命中全部是注释;
  • Lua 侧没有 wasm 能力(channels.lua:141 自己写明「无法在当前 Lua 环境完成」)。

⇒ 既然桥接面是 build_headers(meta) → map[string]string,最干净的接法是在 这个桥后面加一个 Go 侧后端,而不是把 wasm 塞进 Lua:

Provider.buildHeaders  (internal/provider/provider.go)
  └─ VM.BuildHeaders   (internal/lua/vm.go)
       └─ Lua adapter.build_headers(meta)        ← channels-qoder 适配器
            └─ __llmsproxy_kernel("qoder.wasm_headers", meta)   ← 新增内核动作
                 └─ internal/qoderwasm.PrepareHeaders(meta)     ← 新增包(wazero)
                      └─ qoder-auth-wasm.wasm

为什么走 __llmsproxy_kernel 而不是新增一个 Lua 全局: internal/lua/plugin_bridges.go:146 已有 registerKernelBridge,它是 按插件声明授权的(插件在 plugin.kernel_actions 里列白名单)。 新增一个 Lua 全局等于给所有插件开能力;走 kernel action 则保持「谁声明 谁获得」。这与现有 plugin_http.go 的收窄思路一致。

为什么不在 provider 里直接 if-else 厂商名:本仓的既有约束是 「内核零厂商名」(见 channels.lua:1017 的注释与 P2 段)。wasm 调用应作为 一个通用内核能力暴露(wasm.headers),由插件把 qoder 语义喂给它。

5.4 输入 / 输出边界

// internal/qoderwasm(新包,示例形参;未实现)
type Credentials struct {
    UID                 string   `json:"uid"`
    SecurityOauthToken  string   `json:"security_oauth_token"`
    OrganizationID      string   `json:"organization_id"`
    OrganizationTags    []string `json:"organization_tags"`
    DataPolicyAgreed    bool     `json:"data_policy_agreed"`
    MachineID           string   `json:"machine_id"`   // 必填:缺失会 AES-GCM 失败
    ClientVersion       string   `json:"client_version"` // 默认 "1.1.49"
}

type Metadata struct {  // 写入 QoderContext
    ClientType      string `json:"client_type"`
    BusinessProduct string `json:"business_product"`
    BusinessType    string `json:"business_type"`
    Scene           string `json:"scene"`
}

type InferRequest struct {
    Host     string          // ⚠️ 必须 api2.qoder.sh 系,不能是 api2-v2
    Body     json.RawMessage // 明文 payload(由插件构造)
    ModelKey string          // 目录 key,如 qfmodel
    Source   string
}

// PrepareHeaders 返回**必须原样透传**的头集合。
func PrepareHeaders(ctx context.Context, c Credentials, m Metadata, r InferRequest) (map[string]string, error)

输出实测形态(20 个头):

Accept text/event-stream                Authorization Bearer COSY.<载荷>.<签名>(len=209)
Cache-Control no-cache                  Connection keep-alive
Content-Type application/json           Cosy-Business-Product qoder
Cosy-Business-Type default              Cosy-ClientType 10
Cosy-Data-Policy disagree               Cosy-Date 1792279699
Cosy-Key <modelKey>                     Cosy-MachineId <machineId>
Cosy-MachineToken <machineId>           Cosy-MachineType 5
Cosy-Scene chat                         Cosy-User <uid>
Cosy-Version 1.1.49                     Login-Version v2
X-Model-Key <modelKey>                  X-Model-Source system

其中只有 Authorization / Cosy-Key / Cosy-MachineId / Cosy-User 等是 动态的;其余来自 Metadata/常量。注意 Cosy-Date 并非当前时间 (实测 1792279699,是 WASM 内部计算),不要用 Go 时间覆盖。

红线(结构保证,不是注释):Provider.buildHeaders (provider.go:1141)当前逻辑是 if h.Get("Authorization")=="" && h.Get("X-Api-Key")=="" { 补 Bearer }。 qoder 拿到签名 Authorization 后自然跳过兜底 —— 这个已有行为正好安全, 但新接入必须有一个判据钉住:签名头存在时兜底 Bearer 不得生效。

5.5 31 个 import 的 Go 实现映射表

import Go 实现 备注
__wbindgen_object_drop_ref no-op(不释放槽位) ⚠️ 实测该模块会 drop 一个 Map 之后继续用同一索引 set。wasm-bindgen 新版把 drop 退化为空操作(表交给 JS GC)。按「释放槽位」实现会得到 set on undefined
__wbindgen_object_clone_ref push(obj[idx])
__wbindgen_cast_..._1 wasm slice → []byte 本路径未触碰
__wbindgen_cast_..._2 wasm str → Go string 最高频(40 次):字符串返回靠它
__wbg_new_with_length make([]byte, n)
__wbg_length len(buf)
__wbg_subarray buf[lo:hi](共享底层数组,不要 copy)
__wbg_set 两种形态:[]byte.set(src, off) / Map.set(k,v) 全靠 arg0 的动态类型分派
__wbg_prototypesetcall 直接写 wasm 内存
__wbg_new_99cabae new Map() → 有序 {keys,vals} requestresult_headers 的容器
__wbg_getRandomValues_d49329ff89a07af1 crypto/rand 写 wasm 内存 (ptr,len) ⚠️ 与下面那个签名方向相反
__wbg_getRandomValues_c44a50d8cfdaebeb 对 crypto 对象 (objIdx, bufIdx) 填随机 ⚠️ 写反 ⇒ Rust unreachable panic
__wbg_randomFillSync crypto/rand 填 []byte 本路径未触碰
__wbg_now float64(time.Now().UnixMilli()) 返回 f64
__wbg_crypto / __wbg_msCrypto 返回一个「crypto 对象」标记值 只为喂 getRandomValues 变体 B
__wbg_static_accessor_GLOBAL_THIS / GLOBAL 返回任意非 0 索引
__wbg_static_accessor_SELF / WINDOW 返回 0 服务端无 window/self,wasm-bindgen 约定 0 = 缺席
__wbg___wbindgen_is_object 类型判断([]byte/Map/crypto)
__wbg___wbindgen_is_string / is_function / is_undefined 类型判断 后两个本路径未触碰
__wbg___wbindgen_throw 记日志(或抛 panic 转 error) 错误分支
__wbg_Error fmt.Errorf(读出的字符串) 错误分支
__wbg_process / versions / node / require / call 返回 undefined / 0 实测从未被调用;但若被调用说明换了 wasm 版本,应告警
__wbindgen_add_to_stack_pointer / export1..4 由 wasm 自己提供(是 export,不是 import)

5.6 调用顺序与 ABI 陷阱(都实测过)

  1. generate_runtime_auth_fields(userJSON) → {encrypt_user_info, key}(各 172 字符)
  2. qodercontext_new(sp, machineId, len, version, len, userInfo, len, clientMeta, len)
    • 返回布局是 ptr/errIdx/isErr(与字符串类的 ptr/len/valIdx/isErr 不同,混用会得到 null pointer passed to rust)
  3. qodercontext_prepareInferRequest(sp, ctxPtr, host, len, body, len, key, len, source, len)
    • 同样是 ptr/errIdx/isErr
  4. requestresult_headers(resPtr) → Map
  5. requestresult_url(sp, resPtr) / requestresult_body(sp, resPtr)
    • ⚠️ 参数顺序是 (栈指针, ptr),与直觉相反

另外两条实测纪律:

  • ⚠️ machineId 是必填:漏传得到 AES-GCM decrypt failed: aead::Error, 看起来像「密文损坏」,实际是缺参数。
  • ⚠️ host 必须 api2.qoder.sh 系;传 api2-v2.qoder.sh(公开 OpenAI 兼容端点) 会 404。

5.7 并发模型

  • wasm 实例无状态可复用,实测 -repeat 50 全部 PASS;
  • 但上游注释明确 QoderContext 不是线程安全的,且「一个账号一个实例」;
  • ⇒ 设计为 每个账号一个 QoderContext + 一把互斥锁(复用现有 pool 模式), 或按 (uid, machineId) 做 key 的 sync.Map。不要单实例全局共享。
  • 兜底:objectHeap 是只增不回收的(见 §5.5 drop_ref),长跑需设 qodercontext_free + 定期重建实例,否则对象表单调增长。

6. 风险点

# 风险 级别 说明 / 缓解
R1 二进制不在我们仓库,也不可再分发 🔴 高 需先定 §5.2 的三种来源方案之一;go:embed 需法务/许可确认
R2 账号数 0 ⇒ 端到端不可验证 🔴 高 本机能证明「产出了签名头」,不能证明「上游接受」。必须拿到 1 个 qoder 账号才能做真实验收
R3 wasm 版本漂移 🟠 中 上游换版本 ⇒ sha256 变,import 表可能变(尤其那 13 个未触碰项)。必须启动时校验 sha256 并在版本不符时显式拒绝
R4 drop_ref 语义与 wasm-bindgen 版本相关 🟠 中 本次实测必须 no-op;换 wasm 后需重跑 tools/qoder-wasm-probe 复验,否则会退化成 set on undefined
R5 对象表只增不回收 ⇒ 内存缓涨 🟠 中 每个 QoderContext 定期重建;requestresult_* 用后 __wbg_requestresult_free
R6 加密端点意味着响应也是密文 🟠 中 需 decrypt_server_response 解包,且要处理 SSE 逐帧解密(上游走 Encode=1)。这是第二条未实现链路,工作量不小
R7 端点语义与现有明文路径并存 🟡 低-中 channels_upstream_gaps_test.go 现有判据钉住「走明文端点且无信封解包」。接入加密端点必须同时改那条判据,否则两套逻辑互相打脸
R8 引入 wazero 抬升内核版本 🟡 低 按 §4 的版本纪律,新增 Go 依赖 = 核心版本递增(不是 patch)
R9 体积 +1.16 MiB 🟡 低 桌面端/服务端均可接受,但需确认 packaging/ 的体积校验阈值

7. 预计改动面(为什么不该放进 fix/ 分支)

层 文件(预计) 性质
新依赖 go.mod / go.sum(wazero) 内核级
新包 internal/qoderwasm/{glue.go,abi.go,context.go,headers.go} + 测试 新代码 ≈ 600–900 行(glue 706 行 TS 的 Go 翻译)
内核桥 internal/lua/plugin_bridges.go(新增 kernel action)、internal/lua/kernel_api.go 白名单 内核级
插件 internal/lua/plugins/channels.lua(channels-qoder 增加 build_headers + 密文响应解包) 插件级
判据 internal/core/channels_upstream_gaps_test.go(必须改,见 R7)、新增 adapter 判据 判据级
部署 packaging/(wasm 放置 + 校验)、config(qoder_wasm_path) 运维级
文档 本文 + docs/channel-repair-plan.md P1-4 段 文档级

结论:这是一个跨 7 个层、含新内核依赖、且需要账号才能验收的主分支级改动。 一条 fix/channel-auth-headers 分支承担它会把「已修好的三条头缺陷」 一起拖成不可发布状态。⇒ 本轮不做,只交付本文 + 可复现工具。

建议的推进顺序(若主管决定做)

  1. 先决策 §5.2(wasm 从哪来)—— 这是唯一的前置阻塞。
  2. internal/qoderwasm 落地 + 用 tools/qoder-wasm-probe 的用例做单测 (离线可测:不需要账号,断言头集合形状与 sha256 校验)。
  3. 内核桥 wasm.headers + 插件 build_headers 接线,只走明文端点先跑通 头生成链路(不切端点)。
  4. 拿到 1 个真实账号后,验证「签名头被接受」→ 再决定是否切加密端点。
  5. 切端点那一轮再处理 R6(响应/SSE 解密)与 R7(判据改写)。

8. 实测记录(可复现)

工具:tools/qoder-wasm-probe(独立 module,主仓 go.mod 不含 wazero)。

cd tools/qoder-wasm-probe
go run . -wasm /opt/dsh-plugins/dsh-our-free-model/2.0.0/vendor/channel-pack/qoder-auth-wasm.wasm
# 加 -json 出结构化结果;-repeat N 测实例复用;-strict 让未实现 import 被调用即失败

本次输出(2026-10-18,go1.27.1 linux/amd64,wazero v1.9.0):

wasm       : .../qoder-auth-wasm.wasm
size       : 298606 bytes
imports    : 31 (all from "./qoder_auth_wasm_bg.js")
exports    : 33
instantiate: true  calls-at-instantiate=0

-- LEVEL 2: generate_runtime_auth_fields --
  encrypt_user_info    <string len=172>
  key                  <string len=172>

-- LEVEL 3: qodercontext_new + prepareInferRequest --
  url  = api2.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1
  body = 60 bytes (encrypted, not logged)
  hdr  Authorization        Bearer COSY.eyJ2ZXJzaW9u…<len=209>
  ...(共 20 个头)

-- IMPORT USAGE (18 of 31 touched) --
RESULT: PASS

量化(-repeat 50 -json):

指标 值
50 次 prepareInferRequest 端到端 ≈ 108 ms ⇒ ≈ 2.2 ms/请求
wasm 线性内存(跑完后) 20 页 = 1.25 MiB
未触碰 import 13 / 31
实例复用 50 次全部 PASS(无状态泄漏迹象)

再次声明:以上全部是离线静态/单机结论。 「签名头被 qoder 上游接受」未验证 —— 本机 qoder 账号数 0。


9. 与现有实现的冲突(必须在动手前处理)

internal/core/channels_upstream_gaps_test.go 当前把明文端点写成了判据:

「qoder 的 chat 端点变了 —— 若改成加密端点 agent_chat_generation, 则必须同时实现信封解包(qoder-envelope)与 WASM 签名」

这条判据是对的(它防的是「只改一半」)。接入 wasm 时它必须同步演进, 否则会出现「新代码已走加密端点、旧判据仍断言没有信封解包」的自相矛盾。

注意该判据注释里的一处过时事实:它写「该二进制在参考仓库里也不存在」。 实测:文件存在于 vendor/channel-pack/qoder-auth-wasm.wasm (sha256 6419471e…),只是不在我们仓库。接入时应顺手更正这句, 但不要为了「改注释」而放宽判据。