docs(channels): 三线对齐表(登录/模型获取/接入模型服务)逐平台实录

按参考仓的命名规律(X-auth / X-product / X-adapter)建表,记录:
- 本轮修的四件事(codearts 登录、模型兜底表、chat 端点、gemini 私有协议)
- 三个**未闭环**项并写明理由(codearts 模型获取需 AK/SK 签名、
  trae 载荷需转 SOLO 格式、gemini 信封需字母序 + 身份五头)
- 明确不做的四项(gemini-sigstore 不适用、qoder-wasm/zcode-captcha/
  captcha 池属 B 类)
- 本机实测的 5 个坑
This commit is contained in:
JianFeeeee
2026-10-13 17:04:09 +08:00
parent 4a90099fb2
commit 3e94047889

View File

@ -1,68 +1,162 @@
# channels 插件与参考实现(dsh-our-free-model / channel-pack)的对齐清单
# channels 插件与参考插件的三线对齐表
参考位置:`vendor/channel-pack/src/`(本地克隆 `/tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model/`)。
对齐口径:**照对方代码行为逐行移植**,并配「与参考逐字节一致」的差分判据。
参考仓:`Ebony-Vinyl/dsh-our-free-model`(本地克隆
`/tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model`),关键实现位于
`vendor/channel-pack/src/`(144 个 TS 文件)。
状态口径:
- ✅ 已对齐(有实现 + 有判据,关键算法与参考做逐字节差分)
- ⚠️ 部分对齐(主链路可用,高阶环节缺失)
- ❌ 未对齐(参考有的环节没有实现)
命名规律(照它逐厂商对齐的三条线):
## 一、登录(登录 = 拿到凭据并接入上游源)
| 平台 | flow | 参考模块 | 状态 | 说明 |
|---|---|---|---|---|
| trae | callback | trae-oauth.ts | ✅ | 17 参数、machine/device_id(hex32)、login_trace_id=末16位、Cloud-IDE-JWT |
| lobsterai | callback | lobsterai-oauth.ts | ✅ | loopback /auth/callback、5 字段交换、版本号动态解析(远端 `data.value.version`) |
| gemini | callback | gemini-oauth.ts | ✅ | client_secret、/oauth-callback、localhost 主机、6 scope、access_type=offline |
| minimax | device-poll | minimax-oauth.ts | ✅ | PKCE S256 设备码、scope/audience |
| cline | device-poll | cline-oauth.ts | ✅ | WorkOS 设备码 → register 两跳 |
| qoder | device-poll | qoder-oauth.ts | ⚠️ | 登录是纯 HTTP PKCE(已对齐);**加密推理**需 WASM(见三) |
| zcode | init-poll | zcode-login.ts | ⚠️ | 登录链路已对齐;**领取**需滑块验证码(见三) |
| loomy | wechat-poll | loomy-wechat-login.ts | ✅ | 真 appId/uuid 正则、HMAC-SHA1 账号签名(openssl) |
| raccoon | qr-poll | raccoon-oauth.ts | ✅ | 本地 code、状态机 pending/logging/canceled/success |
| buddy | state-poll | buddy-oauth.ts | ✅ | state→token 轮询 |
| opencode | paste | opencode-auth.ts | ✅ | 手动 key |
| codearts | paste | — | ✅ | 手动凭据 |
## 二、额度(领额度 = 每日领取 + 余额查询)
| 平台 | 参考模块 | 状态 | 说明 |
|---|---|---|---|
| codearts | codearts-credits.ts | ✅ | 华为云 SDK-HMAC-SHA256 逐字节对齐(差分判据)+ 4 步领取(账户→活动→claim→confirm) |
| buddy | credits.ts | ✅ | 双层嵌套余额、身份头、只累加有效包 |
| trae | trae-credits.ts | ✅ | Cloud-IDE-JWT + 派生设备身份(差分判据) |
| lobsterai | lobsterai-credits.ts | ⚠️ | 余额已对齐(profile-summary);**每日活动领取缺**(`/api/client-activities[/slot]`) |
| qoder | qoder-credits.ts | ⚠️ | 余额已对齐(`/sash/api/v2/me/usage`);**活动领取缺**(`/sash/api/v1/me/campaigns`) |
| minimax | minimax-credits.ts | ✅ | 两步签到(status + claim) |
| cline | cline-quota.ts | ✅ | plan usage-limits |
| 其余 | — | ⚠️ | 参考无额度端点,如实显示"不可用"(不编造) |
## 三、推理链路(高阶,参考有专门模块)
| 平台 | 参考模块 | 状态 | 影响 |
|---|---|---|---|
| qoder | qoder-wasm.ts (706) + qoder-envelope.ts (226) + qoder-machine.ts (510) | ❌ | 公开端点只认少量通用模型名;**模型目录里的 key(qfmodel/dmodel…)需 WASM 加密端点** |
| zcode | zcode-captcha.ts (2119) + zcode-carrier*.ts | ❌ | 模型请求自 3.14.4 起**不再索要**验证码;**领取始终索要**(code 3007),且一次性 |
| gemini | gemini-sigstore.ts (268) | ⛔ 不适用 | **llmsproxy 里无处安放**:推理走 `internal/lua/adapters/gemini.lua`(无状态、请求级),插件不参与推理 —— 插件侧缓存会成为死代码。参考需要它是因为它自己代理推理。若将来要支持,位置是**适配器侧**(需适配器具备跨请求状态,与"适配器无状态"约定冲突,须单独决策) |
| loomy | loomy-sign.ts | ✅ | 已对齐(仅账号端点用,唯一调用不带 query) |
| 各平台 | captcha-*.ts (6 模块 1223 行) | ❌ | 验证码池/载体/退避 |
## 四、差分判据(关键算法的"与参考一致"证据)
| 判据 | 覆盖 | 生成器 |
| 后缀 | 职责 | 我们要对齐的东西 |
|---|---|---|
| `TestCodeArtsSignatureMatchesReference` | 华为云 canonical / string-to-sign / signature / SignedHeaders / Authorization | `tools/codearts_sig_expected.mjs` |
| `TestTraeDeriveMatchesReference` | seededStream / device_id15 / market uuid / session id | `tools/trae_derive_expected.mjs` |
| `TestTraeCheckinHeaders` | Cloud-IDE-JWT + 派生设备身份 + 客户端头 |
| `TestCallbackPlatformsLoopback` | 三平台回调路径 + state 规则(Trae 无 state) |
| `TestLobsterLoopbackClosedLoop` | 有道 loopback 闭环 + state 拒绝 + 凭据完整性 |
| `TestXDisplaySelectionReclaimsStaleLocks` | 浏览器栈显示器锁回收(防号段烧穿) |
| `X-auth.ts` / `X-oauth.ts` / `login.ts` | **登录** | 登录流程、端点、参数、凭据形状 |
| `X-product.ts` / `models.ts` | **模型获取** | 远端端点、解析路径、兜底表 |
| `X-adapter.ts` / `llm-adapter.ts` | **接入模型服务** | chat 端点、载荷转换、鉴权头 |
## 五、已知未对齐项汇总(按对用户的影响排序)
## 一、登录(line ①)
1. **qoder 加密推理(WASM)** — 影响:拿不到模型目录里的 key(如 Qwen 系列)
2. **zcode 领取验证码** — 影响:模型可用,但**每日额度领不到**(3007)
3. **lobsterai / qoder 活动领取** — 影响:余额能看,活动奖励领不到
4. **gemini thoughtSignature 缓存** — 影响:工具调用质量可能悄悄下降
5. **captcha 池/载体(6 模块)** — 影响:zcode 领取的前置基础设施
| 平台 | 参考流程 | 我们的 flow | 状态 |
|---|---|---|---|
| trae | OAuth 17 参数 + 设备身份派生 | callback | ✅ 逐字节对齐(含 login_trace_id / machine_id / x_device_*) |
| minimax | device-poll | device-poll | ✅ |
| cline | device-poll | device-poll | ✅ |
| buddy | state-poll(**无回调**) | state-poll | ✅ |
| gemini | OAuth + client_secret | callback | ✅(含 client_secret、localhost 主机、6 scope) |
| lobsterai | 本地回调 + authCode 换 token | callback | ✅(含动态版本号、3 步活动领取) |
| opencode | paste | paste | ✅(参考本身即粘贴凭据) |
| **codearts** | **PKCE + DPoP ES256 + STS 换 AK/SK** | **callback** | ✅ **本轮修复**(此前误标 paste) |
| qoder | device-poll + 设备身份 | device-poll | ✅(含 Cosy-MachineToken/Type 成对) |
| zcode | init-poll | init-poll | ✅ |
| loomy | 微信扫码 poll | wechat-poll | ✅ |
| raccoon | 二维码 poll | qr-poll | ✅ |
### codearts 登录(本轮修复的重点)
此前标成 `flow="paste"`(手填凭据)是**错的**。参考 `login.ts` / `oauth.ts`
默认走**新式 IAM OAuth**:
1. 起本地回调服务器,端口须 **≥10000**(`MIN_CALLBACK_PORT`,低于此 portal 拒绝),
路径 `/oauth/callback`
2. 打开 `https://codearts.huaweicloud.com/portal/authorize`,参数逐项:
`theme=2`、`locale=zh-cn`、`uri_scheme=codearts-agent`、`client_id=codearts-agent`、
`code_challenge_method=**SHA-256**`(不是 RFC 的 S256 —— 错值会让 portal
**回退旧 ticket 流程**,看起来"也能用"但不是 OAuth)、`ticket_id`、
`plugin-name=snap_AIIDE`、`plugin-version=5.2.0`;
**不得**含 `auth_callback_url`(会被判异常并回退)
3. 回调收到 `code` 后,带 **DPoP 证明**向
`https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens` 换
`credentials{access_key_id, secret_access_key, security_token, expiration}`
+ `refresh_token`
4. 凭据持久化须含续期材料:`refresh_token` / `code_verifier` / `dpop_private_key_jwk`
DPoP 细节:ES256 JWS,`header={alg,typ:dpop+jwt,jwk}`、
`payload={htm,htu,iat,jti}`;**签名是 raw r||s 64 字节**,而 openssl 输出 DER
——必须转换格式,否则服务端验签失败而字符串看起来完全正常。
终态判定只认 `invalid_grant` / `ErrorCode 含 ExpiredRefreshToken`;
**不认** `InvalidDPoPHeader`(那是一次请求层面的拒绝:时钟偏差、proof 重放、
网关抖动都会触发,当终态会把完好账号一步标死)。
旧 ticket 流程(`CODEARTS_LOGIN_BASE` + `/snap-manager/v1/login/ticket` 轮询)
保留作回退。
## 二、模型获取(line ②)
远端优先、兜底补位的机制已具备(`models_remote` + `effectiveModels`)。
本轮把**兜底表**逐平台补成参考全量:
| 平台 | 参考来源 | 兜底数 | 状态 |
|---|---|---|---|
| trae | `TRAE_FALLBACK_MODELS` | 28 可见 + 4 hidden | ✅(hidden 单列 `models_hidden`) |
| minimax | `GET /mavis/api/v1/models?region=cn&buildEnv=prod` | 4 | ✅ |
| cline | `GET /api/v1/models` + `recommended-models` | 3 | ✅ |
| buddy | (无独立表) | 7 | ✅ |
| gemini | — | 2 | ✅ |
| lobsterai | `GET /api/models/available`(**须带 model_flag**) | 19 | ✅ |
| opencode | `GET /v1/models` | 14 | ✅(含 `models_free`) |
| codearts | opengw `gateway/config` + `/v1/model/builtin` | 3 | ⚠️ 见下 |
| qoder | `/api/v2/model/list`(需 WASM 签名) | 19 | ✅(含 CN 独有 q37fmodel/gm51model) |
| zcode | `GET /api/v1/client/configs` | 2 | ✅ |
| loomy | `GET /api/v1/models`(取 `type==='chat'`) | 8 | ✅ |
| raccoon | 远端 + 兜底 | 6 | ✅ |
**trae 的 `isHidden` 语义**:`browser_use_subagent` / `explore_sub_agent_v13` /
`explore_sub_agent_v2` / `summary` 是内部子 agent,不进用户可选列表。
`model_list` 已按此过滤(`models_hidden`)。
**opencode 的 `isFree`**:参考明确标注 7 个模型匿名可用、其余需付费 key。
此前 `model_list` 把 `isFree` 硬编码为 false,等于把官方免费模型标成付费。
### codearts 模型获取(未闭环)
参考 `models.ts` 从两个端点拉:
- `OPENGW_GATEWAY_CONFIG_URL`(`opengw.developer.huaweicloud.com/api/v1/gateway/config`)
→ `result.models`,这是 **benefit(免费额度)模型**权威来源
- `SNAP_MODEL_BUILTIN_URL`(`/v1/model/builtin`)→ `builtinModels`,常规模型
两者都要 **AK/SK 的 SDK-HMAC-SHA256 签名**。另有 `normalizeModelId`
(去尾部 `-NNNN` 日期版本)与 `isCodeArtsBenefitModel`(决定 chat 是否带
`maas_type: benefit`,不带则 404 `model is not registered`)。
**这部分尚未实现**——我们的 codearts 目前只有 3 个硬编码模型名。
## 三、接入模型服务(line ③)
**本轮发现的最严重问题**:源注册时 `base_url + adapter.endpoint` 拼出的
chat URL **全部缺路径前缀**,11 个平台里 11 个都不对。已逐个修正:
| 平台 | 正确 chat 端点 | 修正前 |
|---|---|---|
| trae | `…/api/agent/v3/llm_utils_chat` | `…/chat/completions` |
| cline | `api.cline.bot/api/v1/chat/completions` | `api.cline.bot/chat/completions` |
| lobsterai | `…/api/proxy/v1/chat/completions` | `…/chat/completions` |
| qoder | `api2-v2.qoder.sh/model/v1/chat/completions` | `…/chat/completions` |
| buddy | `copilot.tencent.com/v2/chat/completions` | `…/chat/completions` |
| codearts | `…/api/v2/chat/completions` | `…/chat/completions` |
| opencode | `opencode.ai/zen/v1/chat/completions` | `opencode.ai/zen/chat/completions` |
| raccoon | `…/api/web/llm/v2/chat/completions` | `…/chat/completions` |
| loomy | `loomy.xunfei.cn/chat/completions` | `loomy.xunfei.cn` |
| zcode | `zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages` | `zcode.z.ai/v1/messages` |
| gemini | `/v1internal:generateContent`(私有动词) | `/v1beta/models/{model}:generateContent`(公开,凭据不匹配) |
### 未闭环:trae 的载荷转换
trae **不是 OpenAI 兼容**。参考 `trae.ts:30` 注释:
「载荷必须转换:OpenAI 的 `{model, messages, tools, tool_choice, stream}` 需要
映射为 SOLO 格式(`function`、`config_name` 等字段),不能透传」;
`model` 还要映射到 `config_name`(查远端模型列表)。
我们有一个 `internal/lua/adapters/trae.lua`,但它描述的是
`trae-local-api`(本地 OpenAI 兼容代理,`127.0.0.1:19900`),**不做** SOLO
转换(`grep config_name` = 0 命中),且平台声明用的是 `adapter = "openai"`。
所以 trae 的**端点现在对了,载荷仍然错**。这是一块独立性工作。
### 未闭环:gemini 的私有协议信封
端点是 `/v1internal:` 了,但参考 `gemini.ts` 还有三件必须保留的细节:
1. **身份五头逐字写死、不许随机**(且**不带** `x-goog-api-key` / `x-goog-api-client`)
2. **流式请求刻意不带 `Accept` 头**
3. **信封逐层字母序序列化**(Go 的 `encoding/json` 对 map 自动字母序,TS 的
`JSON.stringify` 按插入序 —— 不排序字节就与上游不同)
## 四、明确不做的(附理由)
| 参考模块 | 行数 | 为什么不做 |
|---|---|---|
| `gemini-sigstore.ts` | 268 | 我们推理走无状态适配器,插件不参与推理 → 插件侧缓存会成为死代码 ⛔ |
| `qoder-wasm.ts` | 1442 | 需随插件分发 298KB wasm 二进制;B 类,待立项 |
| `zcode-captcha.ts` | 2212 | 阿里云滑块,需真实浏览器/验证码链路;B 类 |
| `captcha-*.ts` | 1223 | 同上 |
## 五、本机实测踩到的坑(都已修)
1. **`xxd` 不存在** —— `command -v xxd` 为空,用它取十六进制会**静默得到空串**。
改用 `od -An -tx1`。
2. **`openssl ec -text` 会截断 32 字节 priv**(实测只给 30 字节)→ `d` 必须用
`openssl asn1parse` 的 `[HEX DUMP]`;`tail -c 32` 拿到的是 SEC1 结构里
**公钥的 y**,不是 `d`。
3. **Lua local 前向引用**:`local function` 只从定义处向后可见。`hexToBin` /
`b64url` / `b64urlRaw` 被 CodeArts 的 PKCE/DPoP 调用,必须移到 CodeArts 块
**之前**(本文件累计踩过 6 次)。
4. **`min_port` 参数读到 `argv[2]`** 与 `out_path` 冲突 —— 回调脚本的第 5 个
参数才是 minPort。
5. **Lua 双引号串里的 `\1`** 是转义坑(`sed 's/…/…/\1/'` 会报
invalid escape sequence)→ 改用 `awk -F': ' '{print $NF}'`。