mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 参考依据
`qoder-machine.ts`。参考用**抓包 + 消融实验**定位过一个真实缺陷:用户报
「Qoder 没领过积分却显示『今日已领取』,去 IDE 看还能领」。
根因:`/sash/` 端点不只要 `Cosy-ClientType`,还校验**设备身份**。消融表(同账号、只改头):
| 请求头 | /sash/api/v1/me/campaigns |
|---|---|
| 仅 Cosy-ClientType: 10 | `claimable:false`,1 条 VIEW_DETAILS |
| + MachineToken + MachineType | **`claimable:true`,2 条,含 CLAIM_BENEFIT/amount:100** |
| 全套 − MachineToken(或 − MachineType) | 退回 1 条(失效) |
⇒ 两值**必须成对**,`Cosy-ClientType` 是必要但不充分条件。
## 实现(三级,与参考一致)
1. **主路径**:实时调 `runtime-info`,参数顺序必须是 `[environment, '--account-stdin']`,
`environment='3'`(对应 asar 的 `'global'` 分支)。**漏环境值会拿到另一套身份**
(参考实测:漏了只回 1 条 VIEW_DETAILS)。stdin 传 `{"account":""}`,
从输出第一个 `{` 起解析 `machineToken`/`machineType`。
定位方式照参考:`<home>/.qoder[|-cn]/.bin/umid-*/runtime-info`(目录名带哈希,必须枚举)。
2. **退路**:磁盘缓存 `machine_token.json` 的 `token`/`type`(候选路径含 Linux
`~/.config/Qoder/…`,并支持 `QODER_MACHINE_TOKEN_PATH` 覆盖)。
3. **降级**:都拿不到时**照常发请求、不带这两个头**(保守降级)——
纯插件登录、未装桌面端的账号没有该文件,不能让整个积分功能报错。
注意:**不写空串**(发空头可能被按非法设备处理)。
结果缓存(拿不到也记 null),不反复读盘/spawn。
## 接线
qoder 平台加 `headers_fn`:`Cosy-ClientType: 10` + (拿到时)成对的身份头,
余额与(后续的)活动领取共用。
## 判据
`TestQoderMachineIdentityPairingAndDegradation`:有缓存 → 成对带上;无任何来源 →
降级且不报错、仍带 clienttype。变异验证:只发 token(不成对)、拿不到时写空串
—— 两个都被抓。
## Lua 前向引用(本文件第 N 次)
`qoderMachineIdentity` 定义在文件后段,而 qoder 平台表在前面 —— 前向声明必须
放在 **PLATFORMS 之前**(第一次放错位置,等于没声明)。
85 lines
2.8 KiB
Go
85 lines
2.8 KiB
Go
package core
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
// Qoder 的设备身份两个头**必须成对**出现:参考用抓包 + 消融实验定位过 ——
|
||
// 缺任一个,服务端就不下发「可领取」的活动(只回 VIEW_DETAILS),
|
||
// 症状是用户报的「没领过却显示今日已领取」。
|
||
// 同时:拿不到身份时必须**保守降级**(照常发请求、不带这两个头),
|
||
// 不能让纯插件登录(未装桌面端)的账号整个积分功能报错。
|
||
func TestQoderMachineIdentityPairingAndDegradation(t *testing.T) {
|
||
t.Run("行1 有 machine_token.json → 成对带上两个头", func(t *testing.T) {
|
||
dir := t.TempDir()
|
||
p := filepath.Join(dir, "machine_token.json")
|
||
if err := os.WriteFile(p, []byte(`{"token":"T-abc","type":"TY-def","updateAt":1}`), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Setenv("QODER_MACHINE_TOKEN_PATH", p)
|
||
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := ps.Call("channels", "__qoder_machine_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("probe: %v", err)
|
||
}
|
||
w, _ := got.(map[string]interface{})
|
||
m, _ := w["json"].(map[string]interface{})
|
||
if m == nil {
|
||
t.Fatalf("无 json: %#v", got)
|
||
}
|
||
if m["token"] != "T-abc" || m["type"] != "TY-def" {
|
||
t.Fatalf("应从磁盘缓存读出 token/type,实得 %v", m)
|
||
}
|
||
if m["client_type"] != "10" {
|
||
t.Fatalf("Cosy-ClientType 应为 10(参考实测值),实得 %v", m["client_type"])
|
||
}
|
||
})
|
||
|
||
t.Run("行2 无任何来源 → 降级:不带这两个头,且不报错", func(t *testing.T) {
|
||
// 指向不存在的路径,且把 HOME 指到空目录(找不到 runtime-info)
|
||
t.Setenv("QODER_MACHINE_TOKEN_PATH", filepath.Join(t.TempDir(), "nope.json"))
|
||
t.Setenv("HOME", t.TempDir())
|
||
t.Setenv("APPDATA", "")
|
||
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := ps.Call("channels", "__qoder_machine_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("probe 不应报错(必须保守降级): %v", err)
|
||
}
|
||
w, _ := got.(map[string]interface{})
|
||
m, _ := w["json"].(map[string]interface{})
|
||
if m == nil {
|
||
t.Fatalf("无 json: %#v", got)
|
||
}
|
||
if m["token"] != nil && m["token"] != "" {
|
||
t.Errorf("拿不到身份时不应带 token,实得 %v", m["token"])
|
||
}
|
||
if m["type"] != nil && m["type"] != "" {
|
||
t.Errorf("拿不到身份时不应带 type,实得 %v", m["type"])
|
||
}
|
||
// 头仍然要有 clienttype(那是必要但不充分条件,与身份无关)
|
||
if m["client_type"] != "10" {
|
||
t.Errorf("降级时仍应带 Cosy-ClientType=10,实得 %v", m["client_type"])
|
||
}
|
||
})
|
||
}
|