Files
ModelRouter/docs/channels-parity.md
JianFeeeee c1733feeaa feat(channels): Qoder 每日活动领取(多活动逐个领)+ 三条误报陷阱全部钉死
## 参考依据
`qoder-credits.ts` 的 `claimQoderDailyCheckin` / `parseQoderCampaigns` /
`claimQoderCampaign` / `hasQoderCampaignRefreshedToday`。
参考用**抓包 + 用户报障**定位过三条会「误报」的陷阱,全部在实现与判据里钉死:

### ① 「无可领活动」必须是 inactive,不能报 already-claimed
旧实现 targets 为空就说"今天已领取" ⇒ 只要服务端没下发可领项(含**请求头不完整**、
未到刷新时间、本就无活动三种情形),界面就谎报已领,与官方 IDE 的"可领取"直接矛盾。
用户报障原文:「没领过却显示今日已领取,去 IDE 看还能领」。

### ② 每日 10:00(UTC+8)刷新;刷新前的 CLAIMED 属于**昨天**
此时说"今天已领取"是谎报,且不可逆(用户以为今天不必再领)。
必须用**算术平移**判 UTC+8 —— 用本机时区在出差/改时区时会判错方向
(偏东提前把当天记为已处理、真漏领;偏西一天判两次)。

### ③ 未开通要给 action_required 的可操作提示
判据是**两条同时**满足(无 CLAIM_BENEFIT + 用量响应里 `addOnQuota` **字段缺失**)。
⚠️ 用「字段是否存在」而非「remaining 是否为 0」—— 后者对"额度用光"与"从未开通"
不可区分,会把用光额度的老账号误报成未开通。文案要可操作(去官方客户端登录一次),
并作为显式字段透传给 UI,而不是让 UI 猜文案。

### 其他要点
- 一个账号可能同时有**多个** CLAIM_BENEFIT 活动 → 逐个领取(实测有每日 100 与运营活动)
- 幂等判据是响应体的 **`replayed`**,不是 HTTP 状态码:重复领取同样回 200,
  但 `replayed:true` 且不含 benefit(只看状态码会把"今天已领"误报成"成功 +100")
- 请求体必须是**空串**(抓包实测 `content-length: 0`)
- 缺 `Cosy-MachineToken/MachineType` 时服务端不下发可领活动(见上一个提交)

## 判据
- `TestQoderClaimDecisionTable`:6 个用例覆盖四类结果 + claim 次数 + body 必须为空
- `TestQoderRefreshUsesUTC8NotLocalTime`:**注入 UTC 时刻**验证刷新判定

★ 修判据自身两处缺陷(首轮变异存活):
1. `TestMain` 把测试进程 TZ 钉成 **UTC** —— 本机恰是 HKT(+0800),于是
   「UTC 平移」与「本机时区」结果**完全相同**,误用本机时区的实现抓不到(判据不可达)。
2. 决策表缺「无任何活动且已开通(addOnQuota 存在)」用例 →
   把 inactive 误报成 already-claimed 的变异得以存活。补齐后两个变异都被抓。

## 统一前向声明(Lua 作用域,本文件第 N 次)
把 `qoderMachineIdentity / rfc3986Escape / newToken / lobsteraiClientVersion`
统一在 PLATFORMS 之前前向声明。此前逐个补、位置还放错过一次,导致运行时报
`attempt to call global 'x' (a nil value)` 而编译期静默。

## 文档
`docs/channels-parity.md` 更新:gemini-sigstore 标注为**⛔ 不适用**
(llmsproxy 的推理走无状态适配器,插件不参与推理 —— 插件侧缓存会成为死代码;
若将来要支持,位置在适配器侧,与"适配器无状态"约定冲突,须单独决策)。
2026-10-13 16:11:11 +08:00

69 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# channels 插件与参考实现(dsh-our-free-model / channel-pack)的对齐清单
参考位置:`vendor/channel-pack/src/`(本地克隆 `/tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model/`)。
对齐口径:**照对方代码行为逐行移植**,并配「与参考逐字节一致」的差分判据。
状态口径:
- ✅ 已对齐(有实现 + 有判据,关键算法与参考做逐字节差分)
- ⚠️ 部分对齐(主链路可用,高阶环节缺失)
- ❌ 未对齐(参考有的环节没有实现)
## 一、登录(登录 = 拿到凭据并接入上游源)
| 平台 | 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` | 浏览器栈显示器锁回收(防号段烧穿) |
## 五、已知未对齐项汇总(按对用户的影响排序)
1. **qoder 加密推理(WASM)** — 影响:拿不到模型目录里的 key(如 Qwen 系列)
2. **zcode 领取验证码** — 影响:模型可用,但**每日额度领不到**(3007)
3. **lobsterai / qoder 活动领取** — 影响:余额能看,活动奖励领不到
4. **gemini thoughtSignature 缓存** — 影响:工具调用质量可能悄悄下降
5. **captcha 池/载体(6 模块)** — 影响:zcode 领取的前置基础设施