Commit Graph

3 Commits

Author SHA1 Message Date
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
4174310421 fix(channels): 渠道鉴权头三修 + 源名契约 + 夹具形状判据
P1-1 anthropic 基座补 build_headers(x-api-key + anthropic-version)
  根因:基座只有静态 anthropic-version、没有 build_headers ⇒ 内核兜底发
  Authorization: Bearer,而 Anthropic 协议要 x-api-key。
  ★ 证据冲突(计划 P1-1 原文一半有误):zcode 的
  /zcode-plan/anthropic/v1/messages 虽然**说 Anthropic 协议**,鉴权却是
  Bearer <zcode_jwt>(参考 zcode-transport.ts:121-125;全仓 grep x-api-key
  = 0 次命中)。故 zcode 必须**显式覆盖** build_headers 回到 Bearer,
  否则基座的正向修复会把它从"对"改成"错"。
  内核改动:internal/provider/provider.go —— 兜底 Bearer 让位给 x-api-key,
  避免同一请求带两套鉴权(部分 Anthropic 兼容上游对歧义鉴权直接 401)。

P1-2 channels-cline 的 build_headers 补 workos: 前缀
  根因:参考 cline.ts 发 `Bearer workos:<jwt>`,而池里存的是第二步 register
  换回的裸 token ⇒ 之前无 build_headers,内核兜底发裸 Bearer。
  幂等:已带前缀(含大小写变体)不重复拼。

P1-3 channels-gemini 只确认不改
  参考 gemini.ts:353-366 同样是 OAuth Bearer + 身份五头、刻意不发
  x-goog-api-key ⇒ 我们现状本来就对。加判据把这个结论钉住,防下轮照
  P1-1 的模子把它改坏。

P2 源名契约:仅 [A-Za-z0-9_-],禁 : 与 /
  根因:: 与 / 是限定名分隔符,出现在源名里会让 `源名:模型名` 有两种都
  自圆其说的切法,同一请求串在不同部署下可能路由到不同源,且**不报错**。
  校验加在写入口 core.normalizeSource(硬拒,API 400);启动时只告警,
  避免存量带 : 的源名让升级 brick 掉整个网关。
  解析入口 splitPinned 不做校验(解析不该拒绝已有数据)。

P3 夹具形状判据(本轮最重要的一条)
  根因:trae 的 Result 信封缺陷在扁平夹具下所有判据全绿,真实登录却报
  「缺少 Token」—— 夹具形状错 ⇒ 判据必然虚假通过。
  通用形状层:shapeFamilies 登记表,每族给 real/naive/fact/eval/assert,
  断言 fact(real) 且 !fact(naive) 且 assert(eval(real))。
  刻意**不比较 eval 输出形状**:trae 余额实现合法保留顶层兼容路径,
  拿输出比对会把合法兼容判成错误;能区分真假的是 fact(额度在哪一层)。
  另有仓库扫描:对特征明确的退化形状静态反查,需退化夹具时写 SHAPE-OK 豁免。

判据:11 个文件 / 30 用例。全部变异已验证会报红(9 处)。
2026-10-18 00:38:49 +08:00
fc5326402f fix(channels,provider): 渠道模型不可用 + 额度异常的三个根因
## 背景
用户报三问题:① channel 注册的模型无法使用;② 额度显示依旧异常;
③ 其他渠道应也存在类似问题。三条全部核实并给出结论(③ 见文档)。

## ① 网关解不开 `源名:模型名` 限定名(internal/provider/registry.go)

`ResolvePinned` 用 `strings.IndexAny(model, "-:/")` 找**第一个**分隔符。
而渠道源名形如 `channels-trae-b8dd6b3df30e`(**含 `-`**),于是
`channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro` 被切成

    src="channels"、model="trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro"

⇒ 源名永不匹配 ⇒ `Resolve()` 返回 nil
⇒ 客户端看到 `model "..." is not configured`。

**为什么这个写法必须支持**:本实例有 **44 个模型被多源同时提供**
(`glm-5.2` 由 6 个源提供),WebUI 的模型下拉框用 `源名:模型名` 作为
option value 是**有意设计** —— 用户必须能指定"从哪个源调用"。

修法:抽 `splitPinned()`,**按已知源名做最长前缀匹配**(源名是已知集合,
这是唯一可靠办法),保留旧 `-:/` 兜底以兼容非源名形态。
`EffectiveModel` 同步修(同一拆分规则,避免两处漂移)。

## ② trae 适配器三连错(internal/lua/plugins/channels.lua)

三个错误逐层叠加,每个都被上一层掩盖:

**(a) 没有 `build_headers`** ⇒ 内核兜底发 `Authorization: Bearer`,
而 trae 推理端点要 `Cloud-IDE-JWT` + 设备身份头 ⇒
401「We're sorry, but we are not able to authenticate you」。
→ 与上一轮「额度不可用」是**同一根因的两个出口**(额度端点修了、推理端点漏了)。
教训:修"某一族请求的鉴权"时,必须把该族的**所有出口**列出来逐个对齐。

**(b) header 常量的值是错的**(照参考 trae-product.ts 的 TRAE 配置逐项核对):
  · `X-App-Id` 抄成 `6eefa6c9`,真实是 `6eefa01c-1036-4c7e-9ca5-d891f63bfcd8`
  · `app_version_code` 用了 `0.1.52`,真实是**日期式** `20260811`
⇒ 上游 `4001 bad request: binding: expr_path=app_version_code,
   cause=parameter type does not match binding data`

**(c) 流式转换完全不对**:TRAE 是 **SOLO 自定义 SSE**:
    event:output → data:{"response":"###","reasoning_content":null,...}
正文在 `response`,推理在 `reasoning_content`。
  · 套 `__prelude.openai_stream_chunk` ⇒ 无 `choices` ⇒ `empty stream`
  · 改成 OpenAI 外壳 `{choices:[{delta}]}` **仍然空** —— 内核
    `json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构**,外壳字段全被丢弃
⇒ 必须输出扁平 `{content, reasoning_content, usage, done, finish_reason}`。

⚠️ 内核只把 `data:` 行交给适配器(`event:` 行在 provider.go 被丢弃),
   因此判别帧类型只能靠**内容特征**(`response` / `reasoning_content` /
   `finish_reason` / `prompt_tokens`),拿不到 event 名。

## ③ 余额恒为 0(同文件 balance.total_fn)

读的是 `p.credits_limit`(pack **顶层**)—— 该字段**根本不存在**。
真实路径是 `pack.entitlement_base_info.quota.credits_limit`
(参考 trae-credits.ts:341)+ `pack.usage.credits_amount`。
实测上游:`免费`(无该字段) / `每月登录赠送`(500) / `签到奖励`(100) ⇒ 实际 600。
修法:读嵌套路径 + 保留顶层兼容 + `usage_summary.{total,consumed}_amount` 兜底。
修后余额 600 → 实测 598.516(真实扣减)。

## ④ 夹具与生产形状不一致(internal/core/channels_trae_test.go)

夹具原先返回**扁平** `{"Token":...}`,与生产 `{"Result":{"Token":...}}` 不符
⇒ 「代码只读顶层」这个缺陷在夹具里**恰好是对的**,所有 trae 判据都绿。
**夹具形状错 = 判据必然虚假通过**。这是 ExchangeToken 缺陷藏了两轮的原因。

## 判据(新增,变异 4/4 咬住)
- `internal/provider/registry_pinned_name_test.go`(4 用例)
- `internal/core/channels_trae_chat_headers_test.go`(3 条)
- `internal/core/channels_trae_solo_sse_test.go`(2 条)
- `internal/core/channels_trae_credits_hdr_test.go`(1 条)
- `internal/core/channels_trae_exchange_shape_test.go`(2 条)

## 文档
`docs/channel-repair-plan.md`:现状快照(生产实测)+ 各渠道缺陷矩阵 +
后续计划(P0-P4)。**如实记录**:其余 4 个渠道(zcode/cline/gemini/qoder)
有同类缺陷风险,但**账号数均为 0、无法做生产验证**。

## 生产已实测
- `channels-trae-...:Doubao-Seed-2.1-Pro` → 80 帧,正文 27 + 推理 299
- `channels-trae-...:glm-5.2` / `channels-buddy-...:hy4-preview` → OK
- `credits/status` → active:true, dailyCredit:100, todayCheckedIn:true
- `credits/balances` → total:598.516 credits
2026-10-17 17:38:58 +08:00