对应主管任务 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…),只是不在**我们**仓库。文档已记,本轮不改 代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
24 KiB
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)→ Luaadapter.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 陷阱(都实测过)
generate_runtime_auth_fields(userJSON)→{encrypt_user_info, key}(各 172 字符)qodercontext_new(sp, machineId, len, version, len, userInfo, len, clientMeta, len)- 返回布局是
ptr/errIdx/isErr(与字符串类的ptr/len/valIdx/isErr不同,混用会得到null pointer passed to rust)
- 返回布局是
qodercontext_prepareInferRequest(sp, ctxPtr, host, len, body, len, key, len, source, len)- 同样是
ptr/errIdx/isErr
- 同样是
requestresult_headers(resPtr)→ Maprequestresult_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 分支承担它会把「已修好的三条头缺陷」
一起拖成不可发布状态。⇒ 本轮不做,只交付本文 + 可复现工具。
建议的推进顺序(若主管决定做)
- 先决策 §5.2(wasm 从哪来)—— 这是唯一的前置阻塞。
internal/qoderwasm落地 + 用tools/qoder-wasm-probe的用例做单测 (离线可测:不需要账号,断言头集合形状与 sha256 校验)。- 内核桥
wasm.headers+ 插件build_headers接线,只走明文端点先跑通 头生成链路(不切端点)。 - 拿到 1 个真实账号后,验证「签名头被接受」→ 再决定是否切加密端点。
- 切端点那一轮再处理 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(sha2566419471e…),只是不在我们仓库。接入时应顺手更正这句, 但不要为了「改注释」而放宽判据。