# 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 `」的写法都会覆盖签名,得到 > `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 输入 / 输出边界 ```go // 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 Cosy-MachineId Cosy-MachineToken Cosy-MachineType 5 Cosy-Scene chat Cosy-User Cosy-Version 1.1.49 Login-Version v2 X-Model-Key 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)。 ```bash 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 key -- 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… ...(共 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…`),只是**不在我们仓库**。接入时应顺手更正这句, > 但**不要**为了「改注释」而放宽判据。