magpie-ima-local-auth 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +24 -23
- package/CHANGELOG.md +23 -10
- package/README.md +10 -7
- package/docs/evaluation.md +145 -92
- package/docs/internals.md +21 -13
- package/package.json +1 -1
- package/scripts/import-cdp.mjs +115 -113
- package/src/api.mjs +2 -2
- package/src/chat.mjs +7 -6
- package/src/context.mjs +12 -4
- package/src/login.mjs +240 -238
- package/src/models.mjs +16 -0
package/AGENTS.md
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
# AGENTS.md —— 给维护者的入口
|
|
2
|
-
|
|
3
|
-
这个仓库是 `magpie-plugin-scaffold` 的派生插件。动手前先读:
|
|
4
|
-
|
|
5
|
-
1. `README.md` —— 用户看什么、支持哪些模型、怎么登录。
|
|
6
|
-
2. `docs/internals.md` —— 协议证据、IUA 的必要性、被否掉的方案。
|
|
7
|
-
3. `docs/evaluation.md` —— 反代价值评估与实测数字。
|
|
8
|
-
4. `node scripts/check.mjs` —— 合规定稿检查,退出码必须 0。
|
|
9
|
-
5. `node --test` —— 语义锁(bkn / 模型 / SSE / DSH 上下文过滤)。
|
|
10
|
-
|
|
11
|
-
## 会静默出错的四条
|
|
12
|
-
|
|
13
|
-
| 症状 | 判据 | 出处 |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| 模型目录退回
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
1
|
+
# AGENTS.md —— 给维护者的入口
|
|
2
|
+
|
|
3
|
+
这个仓库是 `magpie-plugin-scaffold` 的派生插件。动手前先读:
|
|
4
|
+
|
|
5
|
+
1. `README.md` —— 用户看什么、支持哪些模型、怎么登录。
|
|
6
|
+
2. `docs/internals.md` —— 协议证据、IUA 的必要性、被否掉的方案。
|
|
7
|
+
3. `docs/evaluation.md` —— 反代价值评估与实测数字。
|
|
8
|
+
4. `node scripts/check.mjs` —— 合规定稿检查,退出码必须 0。
|
|
9
|
+
5. `node --test` —— 语义锁(bkn / 模型 / SSE / DSH 上下文过滤)。
|
|
10
|
+
|
|
11
|
+
## 会静默出错的四条
|
|
12
|
+
|
|
13
|
+
| 症状 | 判据 | 出处 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 模型目录退回 V3.2 / HY2.0,Copilot `scene:1` 只有默认模型 | `CLIENT-TYPE=256021` 指纹缺失;`src/api.mjs` 的 `cookieFor()` 默认带 | `docs/internals.md` 第 3 节 |
|
|
16
|
+
| 模型目录退回 V4.1 / Hy3 / Hy4 / GLM(匿名默认),问答 200 但没正文 | `IMA-IUA` 缺失;`defaultIua()` 永远补 | `docs/internals.md` 第 3 节 |
|
|
17
|
+
| 回复答非所问,像是在回答“技能目录” | DSH 把 AGENTS/技能目录作为 user 消息追加;`isContextMessage()` 必须把它们从问题/历史里滤掉 | `src/chat.mjs`、`docs/internals.md` |
|
|
18
|
+
| 流式客户端报 `Stream ended without finish_reason` | `finish()` 必须先发带 `finish_reason:"stop"` 的帧,再置 `closed`;`frame()` 不能提前 return | `src/chat.mjs` |
|
|
19
|
+
| 登录后模型 0 个 / 401 | 用 `importLocalLogin()` 的种子或 profile;`provider.models` 调用 `ensureFresh` 后续期 | `src/login.mjs`、`src/api.mjs` |
|
|
20
|
+
|
|
21
|
+
## 发版
|
|
22
|
+
|
|
23
|
+
`scripts/release.mjs`(预演 → `--yes`),或按 scaffold 的 `UPDATING.md` 七步走。
|
|
24
|
+
`git push` 只是第 4 步;npm publish、tag + GitHub Release、四条验证一个都不能少。
|
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
# 更新记录
|
|
2
|
-
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- **修复客户端指纹**:`x-ima-cookie` 的 `CLIENT-TYPE` 改为 ima 2.6.15 实际值 `256021`。没有它,服务端静默退回旧目录(DeepSeek V3.2 / HY 2.0,Copilot 只有默认模型);有了它,免费目录变为 **DeepSeek-V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash(各带 Think 变体)**,Copilot `scene:1` 也能读到完整积分目录。
|
|
6
|
+
- 静态兜底模型同步为 V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash。
|
|
7
|
+
- `scripts/import-cdp.mjs` 写入 `clientType`;`IMA_CLIENT_TYPE` 可覆盖指纹。
|
|
8
|
+
- README / docs 更正“V4.1 标签实际是 V3.2”的错误归因:那是旧 `CLIENT-TYPE` 造成的降级。
|
|
9
|
+
|
|
10
|
+
## 0.1.1 — 2026-10-28
|
|
11
|
+
|
|
12
|
+
- 兼容 ima 客户端 UI 新版的 `STRUCTURED_BLOCK` 流事件(`Data.text_message` 正文 / `Data.thinking_message` 思考),与旧版 `MESSAGE` / `THINKING` 同时支持。
|
|
13
|
+
- 新增 `scripts/import-cdp.mjs`:从带调试端口的 ima.copilot 走 CDP + native bridge 导入登录态。
|
|
14
|
+
- 补充 `docs/evaluation.md`:UI 的 V4.1-Flash 标签在测试账号实际发 `official_3` / `official_1`,账号目录对应 V3.2 / V3.2 Think。
|
|
15
|
+
|
|
16
|
+
## 0.1.0 — 2026-10-28
|
|
17
|
+
|
|
18
|
+
- 首个版本:从本机 ima.copilot 客户端导入登录态(Windows DPAPI / 种子文件),
|
|
19
|
+
把账号的免费知识库模型(DeepSeek V3.2 / Tencent HY 2.0,含 Think 档)接进 magpie。
|
|
20
|
+
- 流式 `MESSAGE` 与思考 `THINKING` 转 OpenAI Chat Completions;支持非流式。
|
|
21
|
+
- 多轮 session(30 分钟 TTL、60 轮兜底)、`auth.refresh` 续期、算力余额展示。
|
|
22
|
+
- 不列 Copilot:ima 的 Copilot 是云端 agent,本机 2.6.15 对它的问答返回「当前版本暂不支持」。
|
|
23
|
+
- 不依赖 cookie 粘贴、不开 CDP;`IMA-IUA` 缺失会让 ima 把请求当匿名,插件默认补最小 IUA。
|
package/README.md
CHANGED
|
@@ -28,16 +28,19 @@
|
|
|
28
28
|
|
|
29
29
|
| 发行商 | 代表模型 | 思考 | 计费 |
|
|
30
30
|
|---|---|---|---|
|
|
31
|
-
| DeepSeek | `deepseek-
|
|
32
|
-
|
|
|
33
|
-
| Tencent | `
|
|
34
|
-
|
|
|
31
|
+
| DeepSeek | `deepseek-v4.1-flash` | 客户端可切快速/深度 | 免费(知识库问答) |
|
|
32
|
+
| Tencent | `hy3` | 同上 | 免费(知识库问答) |
|
|
33
|
+
| Tencent | `hy4-preview` | 同上 | 免费(知识库问答) |
|
|
34
|
+
| 智谱 | `glm-5.3-flash` | 同上 | 免费(知识库问答) |
|
|
35
35
|
|
|
36
|
-
>
|
|
36
|
+
> 每个基础模型在 ima 目录里还带一个深度思考变体(`deepseek-v4.1-flash-think` 等);
|
|
37
|
+
> 插件会从 `sub_model_infos` 自动生成。真实目录由 `provider.models` 在登录后整份替换;
|
|
37
38
|
> 装好以后 `magpie plugin list` / 模型选择器里看到的才是账号当前的全部。
|
|
38
39
|
|
|
39
|
-
**Copilot 积分模型默认不列。**
|
|
40
|
-
|
|
40
|
+
**Copilot 积分模型默认不列。** 用 `CLIENT-TYPE=256021` 的客户端指纹可以读到完整 Copilot
|
|
41
|
+
目录(Hy3 ×0.2、Hy4 preview ×0.9、DeepSeek-V4.1-Flash ×0.4 峰时、MiMo / GLM / MiniMax / Kimi…),
|
|
42
|
+
但 Copilot 是 ima 的云端 agent(`robot_type=15`),不是模型通道;本插件按“不做云端 agent”的
|
|
43
|
+
定位不接它。免费目录已经包含 V4.1-Flash / Hy3 / Hy4 / GLM-5.3,不需要为了这些模型开 Copilot。
|
|
41
44
|
|
|
42
45
|
---
|
|
43
46
|
|
package/docs/evaluation.md
CHANGED
|
@@ -1,92 +1,145 @@
|
|
|
1
|
-
# ima.copilot 反代价值评估
|
|
2
|
-
|
|
3
|
-
> 评估对象:`E:\ima.copilot\ima.copilot.exe`(本机 2.6.15.5280 / Chromium 150.0.7871.5469)
|
|
4
|
-
> 评估时间:2026-10-28。所有数字都是本机实测,不是查资料估的。
|
|
5
|
-
|
|
6
|
-
## 结论
|
|
7
|
-
|
|
8
|
-
| 反代对象 | 价值 | 判定 |
|
|
9
|
-
|---|---|---|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
| 知识库文件 / 文档管理 | 低(本次不做) | 是 ima 的知识库业务,不是模型;对 magpie 供应商没有直接价值 |
|
|
13
|
-
| 图片生成 / 播客等 | 低(本次不做) | 与 magpie 的 chat
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
|
24
|
-
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
# ima.copilot 反代价值评估
|
|
2
|
+
|
|
3
|
+
> 评估对象:`E:\ima.copilot\ima.copilot.exe`(本机 2.6.15.5280 / Chromium 150.0.7871.5469)
|
|
4
|
+
> 评估时间:2026-10-28。所有数字都是本机实测,不是查资料估的。
|
|
5
|
+
|
|
6
|
+
## 结论
|
|
7
|
+
|
|
8
|
+
| 反代对象 | 价值 | 判定 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| **免费模型**(DeepSeek-V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash + 4 个 Think 变体) | **高** | 4 个基础模型 + 4 个思考变体、不扣算力、支持流式;质量已接近/覆盖 CodeBuddy 免费 HY3 这一档 |
|
|
11
|
+
| **Copilot 积分模型**(Hy3 / Hy4 / V4.1 / MiMo / GLM / MiniMax / Kimi…) | **模型能力高,但本插件不接** | Copilot 是 ima 的云端 agent(`robot_type=15` + `command_info.type=2000`),正是 today 式结果的来源;即使能拿到完整目录也默认不列 |
|
|
12
|
+
| 知识库文件 / 文档管理 | 低(本次不做) | 是 ima 的知识库业务,不是模型;对 magpie 供应商没有直接价值 |
|
|
13
|
+
| 图片生成 / 播客等 | 低(本次不做) | 与 magpie 的 chat 供应商无关 |
|
|
14
|
+
|
|
15
|
+
**一句话:免费模型这一层的价值是真实的(V4.1-Flash / Hy3 / Hy4 / GLM-5.3 免费);
|
|
16
|
+
Copilot 积分模型能力更强,但它是云端 agent,按“不能是云端 agent”的约束不接。**
|
|
17
|
+
|
|
18
|
+
## 关键发现:`CLIENT-TYPE=256021` 是指纹钥匙
|
|
19
|
+
|
|
20
|
+
同一账号、同一 token,只改 `x-ima-cookie` 里的 `CLIENT-TYPE`,`model_manage/get_models`
|
|
21
|
+
返回的目录完全不同:
|
|
22
|
+
|
|
23
|
+
| `CLIENT-TYPE` | `{}`(免费目录) | `{"scene":1}`(Copilot 目录) |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `windows` | DeepSeek V3.2 / V3.2 Think / Tencent HY 2.0 / HY 2.0 Think | 只有 `默认模型 ×1.0` |
|
|
26
|
+
| **`256021`**(客户端实际值) | **DeepSeek-V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash** | **完整:Hy3 ×0.2、Hy4 preview ×0.9、DeepSeek-V4.1-Flash ×0.4 峰时、Xiaomi MiMo、GLM-5.3、MiniMax-M3、Kimi…共 12 个** |
|
|
27
|
+
|
|
28
|
+
这正对应“像超级麦吉要组织指纹 key”的猜测:ima 的客户端类型/指纹就是 `CLIENT-TYPE=256021`。
|
|
29
|
+
没有它,服务端不报错,只会退回旧目录,Copilot 也只给默认模型——这就是之前
|
|
30
|
+
「本机 Copilot 暂不支持 / 模型只有 V3.2」现象的根因。
|
|
31
|
+
|
|
32
|
+
> 2026-10-28 从 ima 2.6.15 客户端真实请求头抓包得到;不同客户端版本可能变化,
|
|
33
|
+
> 可用 `IMA_CLIENT_TYPE` 覆盖。
|
|
34
|
+
|
|
35
|
+
## 实测证据
|
|
36
|
+
|
|
37
|
+
### 免费模型目录(`POST /cgi-bin/model_manage/get_models`,body `{}`,`CLIENT-TYPE=256021`)
|
|
38
|
+
|
|
39
|
+
| model_type | model_id | 显示名 | 倍率字段 |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| 3 | official_3 | DeepSeek-V4.1-Flash | 空 |
|
|
42
|
+
| 1 | official_1 | DeepSeek-V4.1-Flash Think | 空 |
|
|
43
|
+
| 0 | official_0 | Hy3 | 空 |
|
|
44
|
+
| 2 | official_2 | Hy3 Think | 空 |
|
|
45
|
+
| 1001 | official_1001 | Hy4 preview | 空 |
|
|
46
|
+
| 1002 | official_1002 | Hy4 preview Think | 空 |
|
|
47
|
+
| 3000 | official_3000 | GLM-5.3-Flash | 空 |
|
|
48
|
+
| 3001 | official_3001 | GLM-5.3-Flash Think | 空 |
|
|
49
|
+
|
|
50
|
+
`model_token_copywriting` 为空 = 不扣算力。实测问答后 `token_manager/get_balance`
|
|
51
|
+
仍为 500。
|
|
52
|
+
|
|
53
|
+
### Copilot 目录(`{"scene":1}`,`CLIENT-TYPE=256021`)
|
|
54
|
+
|
|
55
|
+
`自动选择最优模型 ×1.0`、`Hy3 ×0.2`、`Hy4 preview ×0.9`、`DeepSeek-V4.1-Flash ×0.4 峰时`、
|
|
56
|
+
`Xiaomi MiMo-V2.6-Flash ×0.2`、`Xiaomi MiMo-V2.6-Pro ×0.6`、`GLM-5.3-Flash ×0.2`、
|
|
57
|
+
`GLM-5.3 ×2.2`、`MiniMax-M3.1-Flash ×0.2`、`MiniMax-M3 ×0.6`、
|
|
58
|
+
`Kimi-K2.8-Preview ×1.9`、`Kimi-K3.1-Pro ×3.8`。
|
|
59
|
+
|
|
60
|
+
Copilot 的实际请求(抓包):
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
POST /cgi-bin/assistant/qa
|
|
64
|
+
{"robot_type":15,"command_info":{"type":2000,"copilot_qa_info":{}},
|
|
65
|
+
"model_info":{"model_id":"official_paid_ep-i2azx8ko","model_type":110000,"enable_enhancement":false}}
|
|
66
|
+
→ SSE STRUCTURED_BLOCK(blockThinking / text_message)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
DeepSeek-V4.1-Flash 在 Copilot 里是真实模型(`official_paid_ep-i2azx8ko` / type 110000),
|
|
70
|
+
`STRUCTURED_BLOCK` 里能看到流式思考;但它始终是 Copilot agent。
|
|
71
|
+
|
|
72
|
+
### 问答与流式
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
POST /cgi-bin/session_logic/init_session
|
|
76
|
+
{"env_info":{"interact_type":2,"robot_type":10000},"name":"…","msgs_limit":20}
|
|
77
|
+
→ {"code":0,"session_id":"…"}
|
|
78
|
+
|
|
79
|
+
POST /cgi-bin/assistant/qa (SSE)
|
|
80
|
+
{"session_id":"…","robot_type":10000,"question":"…","question_type":2,
|
|
81
|
+
"command_info":{"question_info":{}},"model_info":{"model_type":3,"model_id":"official_3"}}
|
|
82
|
+
→ MESSAGE {"Text":"…"} / STRUCTURED_BLOCK {"Data":{"text_message":{"Text":"…"}}}
|
|
83
|
+
→ COMPLETED {"Code":0}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- 多轮:同一 session 第一问“记住 7391”、第二问“刚才的数字”返回 7391。
|
|
87
|
+
- 长回答:多个增量事件拼出完整 1..20。
|
|
88
|
+
- 思考:`THINKING {"Message":"…","Status":1}` / `STRUCTURED_BLOCK.blockThinking` 增量;
|
|
89
|
+
最后一条 `Status:2` 是结束标记。
|
|
90
|
+
- `SUGGEST_QUESTION` 是建议列表,不是答案。
|
|
91
|
+
|
|
92
|
+
### 登录态
|
|
93
|
+
|
|
94
|
+
ima 的 token 有两份:运行中客户端的内存(native bridge),以及客户端正常退出时写进
|
|
95
|
+
profile 的 `tencent.wxlogin.account_secret_encrypted`(DPAPI)。插件读后者,不读内存、
|
|
96
|
+
不开 CDP、不要求粘贴 cookie。读到的 `token` 约 2 小时,`refresh_token` 约一年;
|
|
97
|
+
`/auth_login/refresh` 实测可换新 token(返回相对 7200 秒)。
|
|
98
|
+
|
|
99
|
+
## UI 标签 vs 账号目录(修正)
|
|
100
|
+
|
|
101
|
+
之前用 `CLIENT-TYPE=windows` 抓包时,UI 选 V4.1-Flash 发出的 `official_3` 被账号目录标成
|
|
102
|
+
V3.2;把客户端真实的 `CLIENT-TYPE=256021` 带上后,同一 `official_3` 的目录名是
|
|
103
|
+
**DeepSeek-V4.1-Flash**。所以:
|
|
104
|
+
|
|
105
|
+
- **UI 的 V4.1-Flash 是真 V4.1**;之前的“背后是 V3.2”是**我们的请求缺客户端指纹**造成的
|
|
106
|
+
服务端降级,不是 UI 造假。
|
|
107
|
+
- Copilot 的 V4.1(`official_paid_ep-i2azx8ko`)也是真 V4.1;两条通道不同,但都真实存在。
|
|
108
|
+
|
|
109
|
+
## 与 CodeBuddy / 其它免费源的对比
|
|
110
|
+
|
|
111
|
+
| 免费源 | 档位 | 接入成本 | 结论 |
|
|
112
|
+
|---|---|---|---|
|
|
113
|
+
| CodeBuddy 免费 HY3 | HY3 | 现成插件/账号 | 可作主力免费源 |
|
|
114
|
+
| ima 免费目录(本插件) | V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash | 本插件 + 本机客户端 | **同档,多模型;有真实价值** |
|
|
115
|
+
| ima Copilot(积分) | V4.1 / Hy3 / Hy4 / MiMo / GLM / MiniMax / Kimi | 需要开 Copilot agent | 能力更高,但 agent 约束下不接 |
|
|
116
|
+
|
|
117
|
+
## 与现有实现的关系
|
|
118
|
+
|
|
119
|
+
| 实现 | 路径 | 问题 |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `magpie-ima-auth`(npm) | 浏览器 cookie → 云端 `assistant/qa` | 登录要手工复制 `x-ima-cookie`;本质是把 ima 的云端 agent 会话包装成 chat(单问、无工具、system prompt 受限) |
|
|
122
|
+
| `opencli-plugin-ima` | 直接 API + WebContents/UI | 主要面向知识库问答;Windows 直接 API 遇到 600001;需要客户端/UI 自动化 |
|
|
123
|
+
| 本插件 | 本机客户端 profile → DPAPI 登录态 → 伪装请求头(含 `CLIENT-TYPE=256021`) | 不粘贴 cookie、运行时不需要 CDP;只走 `robot_type=10000` 的普通问答;模型目录/流式/思考/续期都按模型供应商做 |
|
|
124
|
+
|
|
125
|
+
## 限制(如实写)
|
|
126
|
+
|
|
127
|
+
- **单问 session**:`assistant/qa` 一次只收一个问题,没有 messages 数组;插件用 session
|
|
128
|
+
保持多轮,但 system prompt 只在 session 第一问里发。
|
|
129
|
+
- **没有工具调用**:ima 的问答接口没有 tool 通道;模型条目如实声明 `toolcall: false`。
|
|
130
|
+
- **没有 token 计数**:ima 不返回 usage;插件不按字符数编,返回 0。
|
|
131
|
+
- **没有上下文窗口**:模型目录不下发;`limit.context` 留 0,交给 magpie 的模型库。
|
|
132
|
+
- **私有接口 + 指纹**:`CLIENT-TYPE=256021` 是抓包值,客户端升级可能变;可用
|
|
133
|
+
`IMA_CLIENT_TYPE` 覆盖。API 本身也可能随时变。
|
|
134
|
+
- **登录导入平台**:DPAPI 解密目前只做 Windows;macOS 用种子文件 / `IMA_COPILOT_LOGIN_FILE`。
|
|
135
|
+
- **Copilot 不进默认目录**:它可用、流式思考也正常,但它是 agent,不是模型通道。
|
|
136
|
+
|
|
137
|
+
## 已完成的验收
|
|
138
|
+
|
|
139
|
+
- `node scripts/check.mjs`:通过 29 · 警告 0 · 失败 0
|
|
140
|
+
- `node --test`:10/10
|
|
141
|
+
- magpie 0.1.1814 安装后 `ima (ima, 8 models) signed in as 001ab…`
|
|
142
|
+
- 网关:`POST /v1/chat/completions`(`ima/deepseek-v4.1-flash`)返回 `GATEWAY-V41`;
|
|
143
|
+
流式返回 role/content/`finish_reason:"stop"`;思考模型返回 `reasoning_content`。
|
|
144
|
+
- DSH headless(`dsh --profile headless` + patch,模型 `ima/hy3` 或 `ima/deepseek-v4.1-flash`)
|
|
145
|
+
返回 `DSH-IMA-OK`。
|
package/docs/internals.md
CHANGED
|
@@ -42,7 +42,7 @@ ima 的 token 有两份:
|
|
|
42
42
|
实测(2026-10-28,ima 2.6.15.5280 / Chromium 150.0.7871.5469):
|
|
43
43
|
|
|
44
44
|
```
|
|
45
|
-
x-ima-cookie: PLATFORM=H5; CLIENT-TYPE=
|
|
45
|
+
x-ima-cookie: PLATFORM=H5; CLIENT-TYPE=256021; WEB-VERSION=5.16.12;
|
|
46
46
|
IMA-GUID=...; IMA-UID=...; IMA-TOKEN=...; IMA-REFRESH-TOKEN=...;
|
|
47
47
|
UID-TYPE=2; TOKEN-TYPE=14; IMA-IUA=PR=IMA&PP=com.tencent.imawin&PPVN=...
|
|
48
48
|
from_browser_ima: 1
|
|
@@ -50,7 +50,12 @@ extension_version: 5.16.12
|
|
|
50
50
|
x-ima-bkn: djb2(IMA-TOKEN) & 0x7fffffff
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
**`
|
|
53
|
+
**`CLIENT-TYPE=256021` 是指纹钥匙。** 它是 ima 2.6.15 客户端抓包得到的客户端类型;
|
|
54
|
+
用它 `model_manage/get_models` 才会返回账号当前目录(V4.1-Flash / Hy3 / Hy4 / GLM-5.3),
|
|
55
|
+
`{"scene":1}` 才会返回完整 Copilot 目录。用 `windows` 会静默退回旧目录
|
|
56
|
+
(V3.2 / HY2.0,Copilot 只有默认模型)。可用 `IMA_CLIENT_TYPE` 覆盖。
|
|
57
|
+
|
|
58
|
+
**`IMA-IUA` 也关键。** 少了它,服务端不会报错,只会把整套请求当成匿名:
|
|
54
59
|
`model_manage/get_models` 返回默认目录(DeepSeek-V4.1-Flash / Hy3 / Hy4 / GLM),
|
|
55
60
|
`assistant/qa` 返回 200 但没有 `MESSAGE` 正文。实测只要 IUA 以
|
|
56
61
|
`PR=IMA&PP=com.tencent.imawin&PPVN=<版本>&PL=WIN&COVC=<Chromium 版本>&EV=` 的形式存在,
|
|
@@ -82,14 +87,15 @@ HEARTBEAT
|
|
|
82
87
|
|
|
83
88
|
| model_type | model_id | 名字 | model_token_copywriting |
|
|
84
89
|
|---|---|---|---|
|
|
85
|
-
| 3 | official_3 | DeepSeek
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
90
|
+
| 3 | official_3 | DeepSeek-V4.1-Flash | 空 |
|
|
91
|
+
| 0 | official_0 | Hy3 | 空 |
|
|
92
|
+
| 1001 | official_1001 | Hy4 preview | 空 |
|
|
93
|
+
| 3000 | official_3000 | GLM-5.3-Flash | 空 |
|
|
89
94
|
|
|
90
95
|
`model_token_copywriting` 为空 = 不扣算力,插件标 `free: true`;`×1.0` 之类是积分模型,
|
|
91
|
-
插件标 `rate
|
|
92
|
-
|
|
96
|
+
插件标 `rate`。`CLIENT-TYPE=256021` 下 `scene:1` 返回完整 Copilot 目录(12 个,含
|
|
97
|
+
V4.1-Flash `official_paid_ep-i2azx8ko`);但 Copilot 走 `robot_type=15`,是 ima 的云端
|
|
98
|
+
agent,所以默认不列。
|
|
93
99
|
|
|
94
100
|
## 6. 被否掉的方案
|
|
95
101
|
|
|
@@ -99,16 +105,18 @@ HEARTBEAT
|
|
|
99
105
|
实例是否拿到登录态不稳定;每次使用还要拉起/重启客户端。不用。
|
|
100
106
|
- **扫描客户端进程内存**:能找到字符串常量,但 token 不在可检索的明文里;且需要
|
|
101
107
|
ReadProcessMemory 权限,跨版本极脆。不用。
|
|
102
|
-
- **Copilot
|
|
103
|
-
|
|
108
|
+
- **Copilot 积分模型**:`CLIENT-TYPE=256021` 下已确认可用(V4.1-Flash / Hy3 / Hy4 / MiMo /
|
|
109
|
+
GLM / MiniMax / Kimi,SSE `STRUCTURED_BLOCK` 流式思考)。但它是 agent 通道
|
|
110
|
+
(`robot_type=15` + `command_info.type=2000`,today 式结果),按当前约束默认不接。
|
|
104
111
|
|
|
105
112
|
## 7. 已验证
|
|
106
113
|
|
|
107
114
|
2026-10-28,本机 ima 2.6.15.5280:
|
|
108
115
|
|
|
109
|
-
- 从 profile 导入登录态 → `model_manage/get_models` 返回
|
|
110
|
-
-
|
|
111
|
-
- `
|
|
116
|
+
- 从 profile 导入登录态 → `CLIENT-TYPE=256021` 下 `model_manage/get_models` 返回
|
|
117
|
+
V4.1-Flash / Hy3 / Hy4 preview / GLM-5.3-Flash 四个免费模型
|
|
118
|
+
- `assistant/qa`(robot 10000, model_type=3)返回 `V41-OK`
|
|
119
|
+
- `THINKING` / `STRUCTURED_BLOCK.blockThinking` 进 `reasoning_content`;`COMPLETED Code 0` 正常结束
|
|
112
120
|
- `token_manager/get_balance` 返回算力余额
|
|
113
121
|
- `auth.refresh` 成功换到新 access token(`token_valid_time` 为相对 7200 秒)
|
|
114
122
|
- 插件 factory 的 loader 流式/非流式都返回 200
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-ima-local-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "magpie / OpenCode provider plugin for the ima.copilot desktop app: its free DeepSeek and Tencent HY models, imported from the client already signed in on this machine, without pasting cookies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/scripts/import-cdp.mjs
CHANGED
|
@@ -1,113 +1,115 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// 从正在运行的 ima.copilot(带 CDP 端口)导入登录态 —— CDP + native bridge 路径。
|
|
3
|
-
//
|
|
4
|
-
// node scripts/import-cdp.mjs --port 9222
|
|
5
|
-
//
|
|
6
|
-
// 它做的事:
|
|
7
|
-
// 1. 连 http://127.0.0.1:<port>/json,找 chrome://home/ 页面
|
|
8
|
-
// 2. 在该页面里执行 chrome.imaFrame.invokeWithCallback({action:'getAccountInfo'})
|
|
9
|
-
// 和 getDeviceInfo —— 和 ima 官方前端完全同一条 native bridge
|
|
10
|
-
// 3. 把 token / refresh_token / uid / guid / QUA 写成种子文件
|
|
11
|
-
//
|
|
12
|
-
// 这个脚本用于:profile 里还没有 tencent.wxlogin 凭证,但客户端正开着调试端口。
|
|
13
|
-
// 只打印账号与有效期,不打印 token。
|
|
14
|
-
|
|
15
|
-
import { saveSeed, SEED_FILE } from "../src/login.mjs"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
pending.
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ws.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 从正在运行的 ima.copilot(带 CDP 端口)导入登录态 —— CDP + native bridge 路径。
|
|
3
|
+
//
|
|
4
|
+
// node scripts/import-cdp.mjs --port 9222
|
|
5
|
+
//
|
|
6
|
+
// 它做的事:
|
|
7
|
+
// 1. 连 http://127.0.0.1:<port>/json,找 chrome://home/ 页面
|
|
8
|
+
// 2. 在该页面里执行 chrome.imaFrame.invokeWithCallback({action:'getAccountInfo'})
|
|
9
|
+
// 和 getDeviceInfo —— 和 ima 官方前端完全同一条 native bridge
|
|
10
|
+
// 3. 把 token / refresh_token / uid / guid / QUA 写成种子文件
|
|
11
|
+
//
|
|
12
|
+
// 这个脚本用于:profile 里还没有 tencent.wxlogin 凭证,但客户端正开着调试端口。
|
|
13
|
+
// 只打印账号与有效期,不打印 token。
|
|
14
|
+
|
|
15
|
+
import { saveSeed, SEED_FILE } from "../src/login.mjs"
|
|
16
|
+
import { CLIENT_TYPE } from "../src/context.mjs"
|
|
17
|
+
|
|
18
|
+
const argv = process.argv.slice(2)
|
|
19
|
+
const get = (name, fallback = "") => {
|
|
20
|
+
const i = argv.indexOf(name)
|
|
21
|
+
return i >= 0 ? argv[i + 1] : fallback
|
|
22
|
+
}
|
|
23
|
+
const port = Number(get("--port", "9222"))
|
|
24
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
25
|
+
console.error("用法:node scripts/import-cdp.mjs --port 9222")
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json()
|
|
30
|
+
const target = targets.find((t) => t.url === "chrome://home/") || targets.find((t) => t.type === "page")
|
|
31
|
+
if (!target?.webSocketDebuggerUrl) {
|
|
32
|
+
console.error(`端口 ${port} 上没有 ima 的 chrome://home/ 页面。确认客户端用 --remote-debugging-port=${port} 启动。`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ws = new WebSocket(target.webSocketDebuggerUrl)
|
|
37
|
+
let id = 0
|
|
38
|
+
const pending = new Map()
|
|
39
|
+
ws.onmessage = (e) => {
|
|
40
|
+
const m = JSON.parse(e.data)
|
|
41
|
+
if (m.id && pending.has(m.id)) {
|
|
42
|
+
pending.get(m.id)(m)
|
|
43
|
+
pending.delete(m.id)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await new Promise((res, rej) => {
|
|
47
|
+
ws.onopen = res
|
|
48
|
+
ws.onerror = rej
|
|
49
|
+
})
|
|
50
|
+
const cmd = (method, params = {}) =>
|
|
51
|
+
new Promise((res) => {
|
|
52
|
+
const i = ++id
|
|
53
|
+
pending.set(i, res)
|
|
54
|
+
ws.send(JSON.stringify({ id: i, method, params }))
|
|
55
|
+
})
|
|
56
|
+
const evaluate = async (expression) => {
|
|
57
|
+
const r = await cmd("Runtime.evaluate", {
|
|
58
|
+
expression,
|
|
59
|
+
returnByValue: true,
|
|
60
|
+
awaitPromise: true,
|
|
61
|
+
userGesture: true,
|
|
62
|
+
})
|
|
63
|
+
if (r.result?.exceptionDetails) throw new Error(r.result.exceptionDetails.text)
|
|
64
|
+
return r.result?.result?.value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const raw = await evaluate(`(async () => {
|
|
68
|
+
const invoke = (action, params = {}) => new Promise((resolve) => {
|
|
69
|
+
const bridge = globalThis.chrome && globalThis.chrome.imaFrame
|
|
70
|
+
if (!bridge || !bridge.invokeWithCallback) return resolve({ code: -1, msg: "imaFrame unavailable", data: "" })
|
|
71
|
+
const timer = setTimeout(() => resolve({ code: 996, msg: "timeout", data: "" }), 10000)
|
|
72
|
+
bridge.invokeWithCallback({ action, params: JSON.stringify(params) }, (r) => {
|
|
73
|
+
clearTimeout(timer)
|
|
74
|
+
resolve(r || {})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
const parse = (r) => { try { return typeof r.data === "string" ? JSON.parse(r.data || "{}") : (r.data || {}) } catch { return {} } }
|
|
78
|
+
const account = parse(await invoke("getAccountInfo"))
|
|
79
|
+
const device = parse(await invoke("getDeviceInfo"))
|
|
80
|
+
const version = chrome.runtime.getManifest().version
|
|
81
|
+
return JSON.stringify({ account, device, version })
|
|
82
|
+
})()`)
|
|
83
|
+
ws.close()
|
|
84
|
+
|
|
85
|
+
const { account, device, version } = JSON.parse(raw)
|
|
86
|
+
if (!account?.is_login || !account?.token) {
|
|
87
|
+
console.error("这个 ima.copilot 页面没有登录:先在客户端里登录,再重试。")
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
const state = {
|
|
91
|
+
access: account.token,
|
|
92
|
+
refresh: account.refresh_token,
|
|
93
|
+
expires: Number(account.token_valid_time) * 1000,
|
|
94
|
+
uid: account.user_id,
|
|
95
|
+
idType: String(account.id_type ?? ""),
|
|
96
|
+
tokenType: String(account.token_type ?? ""),
|
|
97
|
+
guid: device?.guid || "",
|
|
98
|
+
q36: device?.q36 || "",
|
|
99
|
+
iua: device?.qua || "",
|
|
100
|
+
extensionVersion: version,
|
|
101
|
+
clientType: process.env.IMA_CLIENT_TYPE || CLIENT_TYPE,
|
|
102
|
+
accountId: account?.user_info?.open_info?.nickname || "ima.copilot",
|
|
103
|
+
nickname: account?.user_info?.open_info?.nickname || "",
|
|
104
|
+
source: `cdp:${port}`,
|
|
105
|
+
}
|
|
106
|
+
const ok = saveSeed(state)
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
imported: true,
|
|
109
|
+
seed: SEED_FILE,
|
|
110
|
+
saved: ok,
|
|
111
|
+
uid: state.uid,
|
|
112
|
+
account: state.accountId,
|
|
113
|
+
expiresAt: new Date(state.expires).toISOString(),
|
|
114
|
+
hasRefresh: Boolean(state.refresh),
|
|
115
|
+
}, null, 2))
|
package/src/api.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 这一层把所有网络与错误语义收在一处,钩子层不再判断 code。
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto"
|
|
8
|
-
import { BASE, CLIENT_VERSION, HOST, IMA_APP_VERSION, IMA_CHROMIUM_VERSION } from "./context.mjs"
|
|
8
|
+
import { BASE, CLIENT_TYPE, CLIENT_VERSION, HOST, IMA_APP_VERSION, IMA_CHROMIUM_VERSION } from "./context.mjs"
|
|
9
9
|
|
|
10
10
|
export const AUTH_CODES = new Set([41, 600001])
|
|
11
11
|
export const SESSION_CODES = new Set([5, 51])
|
|
@@ -48,7 +48,7 @@ export function defaultIua() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** 拼 x-ima-cookie。空字段不出现(ima 对空字段比缺字段更敏感) */
|
|
51
|
-
export function cookieFor(auth, clientType = process.
|
|
51
|
+
export function cookieFor(auth, clientType = auth?.clientType || process.env.IMA_CLIENT_TYPE || CLIENT_TYPE) {
|
|
52
52
|
const fields = {
|
|
53
53
|
PLATFORM: "H5",
|
|
54
54
|
"CLIENT-TYPE": clientType,
|
package/src/chat.mjs
CHANGED
|
@@ -244,16 +244,17 @@ export function streamOpenAI(res, modelId) {
|
|
|
244
244
|
if (closed) return
|
|
245
245
|
const bad = eventError(name, raw)
|
|
246
246
|
if (bad) return fail(new Error(bad.message))
|
|
247
|
-
|
|
247
|
+
// 旧版/知识库模式是 MESSAGE/THINKING;客户端 UI 的新版是 STRUCTURED_BLOCK
|
|
248
|
+
// 包着 Data.text_message / Data.thinking_message。两种都要认。
|
|
249
|
+
if (name === "THINKING" || name === "STRUCTURED_BLOCK") {
|
|
248
250
|
const t = eventThinking(raw)
|
|
249
251
|
if (t) delta({ reasoning_content: t })
|
|
250
|
-
return
|
|
251
252
|
}
|
|
252
|
-
if (name === "MESSAGE") {
|
|
253
|
+
if (name === "MESSAGE" || name === "STRUCTURED_BLOCK") {
|
|
253
254
|
const t = eventText(raw)
|
|
254
255
|
if (t) delta({ content: t })
|
|
255
|
-
return
|
|
256
256
|
}
|
|
257
|
+
if (name === "THINKING" || name === "MESSAGE" || name === "STRUCTURED_BLOCK") return
|
|
257
258
|
if (CONTROL.has(name)) finish()
|
|
258
259
|
}).then(finish, fail)
|
|
259
260
|
},
|
|
@@ -280,8 +281,8 @@ export async function aggregateOpenAI(res, modelId) {
|
|
|
280
281
|
failed = new Error(bad.message)
|
|
281
282
|
return
|
|
282
283
|
}
|
|
283
|
-
if (name === "THINKING") thinking += eventThinking(raw)
|
|
284
|
-
|
|
284
|
+
if (name === "THINKING" || name === "STRUCTURED_BLOCK") thinking += eventThinking(raw)
|
|
285
|
+
if (name === "MESSAGE" || name === "STRUCTURED_BLOCK") text += eventText(raw)
|
|
285
286
|
})
|
|
286
287
|
if (failed) throw failed
|
|
287
288
|
return {
|
package/src/context.mjs
CHANGED
|
@@ -11,6 +11,10 @@ export const SDK = "@ai-sdk/openai-compatible"
|
|
|
11
11
|
export const REFRESH_LEAD_MS = 10 * 60_000
|
|
12
12
|
export const TOKEN_TTL_SECONDS = 7200
|
|
13
13
|
export const CLIENT_VERSION = "5.16.12"
|
|
14
|
+
// ima 客户端的 CLIENT-TYPE 指纹。没有它,服务端会退回旧目录(V3.2/HY2.0),
|
|
15
|
+
// 有了它才返回当前账号的 V4.1-Flash / Hy3 / Hy4 / GLM-5.3 目录。
|
|
16
|
+
// 2026-10-28 从 ima 2.6.15 客户端实际请求头抓包得到;可用 IMA_CLIENT_TYPE 覆盖。
|
|
17
|
+
export const CLIENT_TYPE = "256021"
|
|
14
18
|
export const IMA_APP_VERSION = "2.6.15.5280"
|
|
15
19
|
export const IMA_CHROMIUM_VERSION = "150.0.7871.5469"
|
|
16
20
|
|
|
@@ -26,10 +30,14 @@ export const ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA
|
|
|
26
30
|
* 那些模型在当前账号上不一定能调,列出来反而会 404。
|
|
27
31
|
*/
|
|
28
32
|
export const MODELS = [
|
|
29
|
-
["deepseek-
|
|
30
|
-
["deepseek-
|
|
31
|
-
["
|
|
32
|
-
["
|
|
33
|
+
["deepseek-v4.1-flash", "DeepSeek-V4.1-Flash", 3, "official_3", false],
|
|
34
|
+
["deepseek-v4.1-flash-think", "DeepSeek-V4.1-Flash Think", 1, "official_1", true],
|
|
35
|
+
["hy3", "Hy3", 0, "official_0", false],
|
|
36
|
+
["hy3-think", "Hy3 Think", 2, "official_2", true],
|
|
37
|
+
["hy4-preview", "Hy4 preview", 1001, "official_1001", false],
|
|
38
|
+
["hy4-preview-think", "Hy4 preview Think", 1002, "official_1002", true],
|
|
39
|
+
["glm-5.3-flash", "GLM-5.3-Flash", 3000, "official_3000", false],
|
|
40
|
+
["glm-5.3-flash-think", "GLM-5.3-Flash Think", 3001, "official_3001", true],
|
|
33
41
|
]
|
|
34
42
|
|
|
35
43
|
/** 静态模型条目(magpie 的 config.models 必须有内容,未登录也要显示) */
|
package/src/login.mjs
CHANGED
|
@@ -1,238 +1,240 @@
|
|
|
1
|
-
// src/login.mjs —— 从本机 ima.copilot 客户端导入登录态
|
|
2
|
-
//
|
|
3
|
-
// ima 把登录态放在两个地方:
|
|
4
|
-
// 1. 正在运行的客户端内存里(native bridge,官方前端用 chrome.imaFrame 读它);
|
|
5
|
-
// 2. 客户端正常退出时,把账号凭证写进 profile 的 Preferences
|
|
6
|
-
// (tencent.wxlogin.account_secret_encrypted,DPAPI 加密)。
|
|
7
|
-
//
|
|
8
|
-
// 这个文件读第 2 种,外加一个可选的明文 JSON 种子文件(IMA_COPILOT_LOGIN_FILE)。
|
|
9
|
-
// 不读进程内存、不开 CDP、不要求用户粘贴 cookie。
|
|
10
|
-
//
|
|
11
|
-
// ⚠ 这里会碰到 token 明文。绝不 console.log / 写进错误消息。
|
|
12
|
-
|
|
13
|
-
import { execFileSync } from "node:child_process"
|
|
14
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, chmodSync } from "node:fs"
|
|
15
|
-
import { homedir } from "node:os"
|
|
16
|
-
import { join, delimiter } from "node:path"
|
|
17
|
-
import { CLIENT_VERSION } from "./context.mjs"
|
|
18
|
-
|
|
19
|
-
const CACHE_DIR = join(homedir(), ".cache", "magpie", "ima-local-auth")
|
|
20
|
-
export const SEED_FILE = join(CACHE_DIR, "login.json")
|
|
21
|
-
|
|
22
|
-
const readJson = (file) => {
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(readFileSync(file, "utf8"))
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const number = (v) => {
|
|
31
|
-
const n = Number(v)
|
|
32
|
-
return Number.isFinite(n) ? n : 0
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* DPAPI 解密(Windows 当前用户)。account_secret_encrypted 是 base64 的
|
|
37
|
-
* CryptProtectData 输出;解出来是 { token, refresh_token, token_valid_time,
|
|
38
|
-
* refresh_token_valid_time } 的 JSON。
|
|
39
|
-
*/
|
|
40
|
-
export function dpapiDecrypt(base64) {
|
|
41
|
-
if (process.platform !== "win32") throw new Error("ima 的账号凭证解密目前只支持 Windows")
|
|
42
|
-
const script = [
|
|
43
|
-
"Add-Type -AssemblyName System.Security",
|
|
44
|
-
"$b=[Convert]::FromBase64String($env:IMA_SECRET_B64)",
|
|
45
|
-
"$p=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser')",
|
|
46
|
-
"[Convert]::ToBase64String($p)",
|
|
47
|
-
].join("; ")
|
|
48
|
-
const out = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
49
|
-
encoding: "utf8",
|
|
50
|
-
windowsHide: true,
|
|
51
|
-
timeout: 15_000,
|
|
52
|
-
env: { ...process.env, IMA_SECRET_B64: base64 },
|
|
53
|
-
})
|
|
54
|
-
const line = String(out ?? "")
|
|
55
|
-
.split(/\r?\n/)
|
|
56
|
-
.map((s) => s.trim())
|
|
57
|
-
.filter(Boolean)
|
|
58
|
-
.pop()
|
|
59
|
-
if (!line) throw new Error("DPAPI 解密没有输出")
|
|
60
|
-
return Buffer.from(line, "base64").toString("utf8")
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 把 Preferences 里的 wxlogin 段变成插件要的登录态 */
|
|
64
|
-
export function loginFromPreferences(prefsPath) {
|
|
65
|
-
const prefs = readJson(prefsPath)
|
|
66
|
-
const wx = prefs?.tencent?.wxlogin
|
|
67
|
-
if (!wx?.account_secret_encrypted) return null
|
|
68
|
-
const meta = (() => {
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(wx.account_meta || "{}")
|
|
71
|
-
} catch {
|
|
72
|
-
return {}
|
|
73
|
-
}
|
|
74
|
-
})()
|
|
75
|
-
const secret = JSON.parse(dpapiDecrypt(String(wx.account_secret_encrypted)))
|
|
76
|
-
const q36 = (() => {
|
|
77
|
-
const groups = prefs?.sync?.transport_data_per_account
|
|
78
|
-
if (!groups || typeof groups !== "object") return ""
|
|
79
|
-
for (const v of Object.values(groups)) {
|
|
80
|
-
if (v && typeof v === "object" && v["sync.cache_guid"]) return String(v["sync.cache_guid"])
|
|
81
|
-
}
|
|
82
|
-
return ""
|
|
83
|
-
})()
|
|
84
|
-
return normalize({
|
|
85
|
-
...secret,
|
|
86
|
-
uid: meta.user_id,
|
|
87
|
-
id_type: meta.id_type,
|
|
88
|
-
token_type: meta.token_type,
|
|
89
|
-
guid: meta?.user_info?.open_info?.guid,
|
|
90
|
-
q36,
|
|
91
|
-
accountId: meta?.user_info?.open_info?.nickname || "ima.copilot",
|
|
92
|
-
extensionVersion: extensionVersionOf(prefsPath) || CLIENT_VERSION,
|
|
93
|
-
source: prefsPath,
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 同目录的 Secure Preferences 里,copilot-v2 扩展的版本 */
|
|
98
|
-
function extensionVersionOf(prefsPath) {
|
|
99
|
-
try {
|
|
100
|
-
const secure = join(prefsPath, "..", "Secure Preferences")
|
|
101
|
-
const d = readJson(secure)
|
|
102
|
-
const settings = d?.extensions?.settings
|
|
103
|
-
if (!settings) return ""
|
|
104
|
-
for (const [id, v] of Object.entries(settings)) {
|
|
105
|
-
const name = String(v?.manifest?.name ?? "")
|
|
106
|
-
if (id === "ebbbfiigemhmjadjbckcodajdmnceamb" || /copilot/i.test(name)) {
|
|
107
|
-
const ver = String(v?.manifest?.version ?? "")
|
|
108
|
-
if (ver) return ver
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
// 读不到就用默认版本
|
|
113
|
-
}
|
|
114
|
-
return ""
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 候选种子文件(明文 JSON) */
|
|
118
|
-
function seedPaths() {
|
|
119
|
-
const out = []
|
|
120
|
-
if (process.env.IMA_COPILOT_LOGIN_FILE) out.push(process.env.IMA_COPILOT_LOGIN_FILE)
|
|
121
|
-
out.push(SEED_FILE, join(homedir(), ".config", "magpie", "ima-local-auth", "login.json"))
|
|
122
|
-
return out
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** 候选 profile 目录(每个目录下找 Default/Preferences 或 Preferences) */
|
|
126
|
-
function profileDirs() {
|
|
127
|
-
const out = []
|
|
128
|
-
const env = process.env.IMA_COPILOT_PROFILE_DIRS
|
|
129
|
-
if (env) out.push(...env.split(delimiter).map((s) => s.trim()).filter(Boolean))
|
|
130
|
-
const local = process.env.LOCALAPPDATA
|
|
131
|
-
if (local) {
|
|
132
|
-
out.push(join(local, "ima.copilot", "User Data"))
|
|
133
|
-
// 某些安装会多一个 Application 子目录;Preferences 仍然在 User Data 下。
|
|
134
|
-
out.push(join(local, "ima.copilot", "User Data", "Default"))
|
|
135
|
-
}
|
|
136
|
-
out.push(join(CACHE_DIR, "profile"))
|
|
137
|
-
return [...new Set(out)]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function preferencesIn(dir) {
|
|
141
|
-
for (const p of [join(dir, "Default", "Preferences"), join(dir, "Preferences")]) {
|
|
142
|
-
if (existsSync(p)) return p
|
|
143
|
-
}
|
|
144
|
-
return ""
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** 登录态归一化:返回插件内部形状(expires 用毫秒时间戳) */
|
|
148
|
-
export function normalize(raw) {
|
|
149
|
-
if (!raw || typeof raw !== "object") return null
|
|
150
|
-
const access = String(raw.access ?? raw.token ?? "")
|
|
151
|
-
const refresh = String(raw.refresh ?? raw.refresh_token ?? "")
|
|
152
|
-
if (!access) return null
|
|
153
|
-
let expires = number(raw.expires)
|
|
154
|
-
if (!expires) {
|
|
155
|
-
const t = number(raw.token_valid_time)
|
|
156
|
-
if (t > 1e12) expires = t
|
|
157
|
-
else if (t > 1e9) expires = t * 1000
|
|
158
|
-
else if (t > 0) expires = Date.now() + t * 1000
|
|
159
|
-
}
|
|
160
|
-
if (!expires) expires = Date.now() + 7200 * 1000
|
|
161
|
-
const uid = String(raw.uid ?? raw.user_id ?? "")
|
|
162
|
-
return {
|
|
163
|
-
access,
|
|
164
|
-
refresh,
|
|
165
|
-
expires,
|
|
166
|
-
uid,
|
|
167
|
-
idType: String(raw.idType ?? raw.id_type ?? ""),
|
|
168
|
-
tokenType: String(raw.tokenType ?? raw.token_type ?? ""),
|
|
169
|
-
guid: String(raw.guid ?? ""),
|
|
170
|
-
q36: String(raw.q36 ?? ""),
|
|
171
|
-
iua: String(raw.iua ?? ""),
|
|
172
|
-
extensionVersion: String(raw.extensionVersion ?? raw.extension_version ?? CLIENT_VERSION),
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1
|
+
// src/login.mjs —— 从本机 ima.copilot 客户端导入登录态
|
|
2
|
+
//
|
|
3
|
+
// ima 把登录态放在两个地方:
|
|
4
|
+
// 1. 正在运行的客户端内存里(native bridge,官方前端用 chrome.imaFrame 读它);
|
|
5
|
+
// 2. 客户端正常退出时,把账号凭证写进 profile 的 Preferences
|
|
6
|
+
// (tencent.wxlogin.account_secret_encrypted,DPAPI 加密)。
|
|
7
|
+
//
|
|
8
|
+
// 这个文件读第 2 种,外加一个可选的明文 JSON 种子文件(IMA_COPILOT_LOGIN_FILE)。
|
|
9
|
+
// 不读进程内存、不开 CDP、不要求用户粘贴 cookie。
|
|
10
|
+
//
|
|
11
|
+
// ⚠ 这里会碰到 token 明文。绝不 console.log / 写进错误消息。
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from "node:child_process"
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, chmodSync } from "node:fs"
|
|
15
|
+
import { homedir } from "node:os"
|
|
16
|
+
import { join, delimiter } from "node:path"
|
|
17
|
+
import { CLIENT_TYPE, CLIENT_VERSION } from "./context.mjs"
|
|
18
|
+
|
|
19
|
+
const CACHE_DIR = join(homedir(), ".cache", "magpie", "ima-local-auth")
|
|
20
|
+
export const SEED_FILE = join(CACHE_DIR, "login.json")
|
|
21
|
+
|
|
22
|
+
const readJson = (file) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(file, "utf8"))
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const number = (v) => {
|
|
31
|
+
const n = Number(v)
|
|
32
|
+
return Number.isFinite(n) ? n : 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* DPAPI 解密(Windows 当前用户)。account_secret_encrypted 是 base64 的
|
|
37
|
+
* CryptProtectData 输出;解出来是 { token, refresh_token, token_valid_time,
|
|
38
|
+
* refresh_token_valid_time } 的 JSON。
|
|
39
|
+
*/
|
|
40
|
+
export function dpapiDecrypt(base64) {
|
|
41
|
+
if (process.platform !== "win32") throw new Error("ima 的账号凭证解密目前只支持 Windows")
|
|
42
|
+
const script = [
|
|
43
|
+
"Add-Type -AssemblyName System.Security",
|
|
44
|
+
"$b=[Convert]::FromBase64String($env:IMA_SECRET_B64)",
|
|
45
|
+
"$p=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser')",
|
|
46
|
+
"[Convert]::ToBase64String($p)",
|
|
47
|
+
].join("; ")
|
|
48
|
+
const out = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
timeout: 15_000,
|
|
52
|
+
env: { ...process.env, IMA_SECRET_B64: base64 },
|
|
53
|
+
})
|
|
54
|
+
const line = String(out ?? "")
|
|
55
|
+
.split(/\r?\n/)
|
|
56
|
+
.map((s) => s.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.pop()
|
|
59
|
+
if (!line) throw new Error("DPAPI 解密没有输出")
|
|
60
|
+
return Buffer.from(line, "base64").toString("utf8")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 把 Preferences 里的 wxlogin 段变成插件要的登录态 */
|
|
64
|
+
export function loginFromPreferences(prefsPath) {
|
|
65
|
+
const prefs = readJson(prefsPath)
|
|
66
|
+
const wx = prefs?.tencent?.wxlogin
|
|
67
|
+
if (!wx?.account_secret_encrypted) return null
|
|
68
|
+
const meta = (() => {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(wx.account_meta || "{}")
|
|
71
|
+
} catch {
|
|
72
|
+
return {}
|
|
73
|
+
}
|
|
74
|
+
})()
|
|
75
|
+
const secret = JSON.parse(dpapiDecrypt(String(wx.account_secret_encrypted)))
|
|
76
|
+
const q36 = (() => {
|
|
77
|
+
const groups = prefs?.sync?.transport_data_per_account
|
|
78
|
+
if (!groups || typeof groups !== "object") return ""
|
|
79
|
+
for (const v of Object.values(groups)) {
|
|
80
|
+
if (v && typeof v === "object" && v["sync.cache_guid"]) return String(v["sync.cache_guid"])
|
|
81
|
+
}
|
|
82
|
+
return ""
|
|
83
|
+
})()
|
|
84
|
+
return normalize({
|
|
85
|
+
...secret,
|
|
86
|
+
uid: meta.user_id,
|
|
87
|
+
id_type: meta.id_type,
|
|
88
|
+
token_type: meta.token_type,
|
|
89
|
+
guid: meta?.user_info?.open_info?.guid,
|
|
90
|
+
q36,
|
|
91
|
+
accountId: meta?.user_info?.open_info?.nickname || "ima.copilot",
|
|
92
|
+
extensionVersion: extensionVersionOf(prefsPath) || CLIENT_VERSION,
|
|
93
|
+
source: prefsPath,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 同目录的 Secure Preferences 里,copilot-v2 扩展的版本 */
|
|
98
|
+
function extensionVersionOf(prefsPath) {
|
|
99
|
+
try {
|
|
100
|
+
const secure = join(prefsPath, "..", "Secure Preferences")
|
|
101
|
+
const d = readJson(secure)
|
|
102
|
+
const settings = d?.extensions?.settings
|
|
103
|
+
if (!settings) return ""
|
|
104
|
+
for (const [id, v] of Object.entries(settings)) {
|
|
105
|
+
const name = String(v?.manifest?.name ?? "")
|
|
106
|
+
if (id === "ebbbfiigemhmjadjbckcodajdmnceamb" || /copilot/i.test(name)) {
|
|
107
|
+
const ver = String(v?.manifest?.version ?? "")
|
|
108
|
+
if (ver) return ver
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// 读不到就用默认版本
|
|
113
|
+
}
|
|
114
|
+
return ""
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 候选种子文件(明文 JSON) */
|
|
118
|
+
function seedPaths() {
|
|
119
|
+
const out = []
|
|
120
|
+
if (process.env.IMA_COPILOT_LOGIN_FILE) out.push(process.env.IMA_COPILOT_LOGIN_FILE)
|
|
121
|
+
out.push(SEED_FILE, join(homedir(), ".config", "magpie", "ima-local-auth", "login.json"))
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 候选 profile 目录(每个目录下找 Default/Preferences 或 Preferences) */
|
|
126
|
+
function profileDirs() {
|
|
127
|
+
const out = []
|
|
128
|
+
const env = process.env.IMA_COPILOT_PROFILE_DIRS
|
|
129
|
+
if (env) out.push(...env.split(delimiter).map((s) => s.trim()).filter(Boolean))
|
|
130
|
+
const local = process.env.LOCALAPPDATA
|
|
131
|
+
if (local) {
|
|
132
|
+
out.push(join(local, "ima.copilot", "User Data"))
|
|
133
|
+
// 某些安装会多一个 Application 子目录;Preferences 仍然在 User Data 下。
|
|
134
|
+
out.push(join(local, "ima.copilot", "User Data", "Default"))
|
|
135
|
+
}
|
|
136
|
+
out.push(join(CACHE_DIR, "profile"))
|
|
137
|
+
return [...new Set(out)]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function preferencesIn(dir) {
|
|
141
|
+
for (const p of [join(dir, "Default", "Preferences"), join(dir, "Preferences")]) {
|
|
142
|
+
if (existsSync(p)) return p
|
|
143
|
+
}
|
|
144
|
+
return ""
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 登录态归一化:返回插件内部形状(expires 用毫秒时间戳) */
|
|
148
|
+
export function normalize(raw) {
|
|
149
|
+
if (!raw || typeof raw !== "object") return null
|
|
150
|
+
const access = String(raw.access ?? raw.token ?? "")
|
|
151
|
+
const refresh = String(raw.refresh ?? raw.refresh_token ?? "")
|
|
152
|
+
if (!access) return null
|
|
153
|
+
let expires = number(raw.expires)
|
|
154
|
+
if (!expires) {
|
|
155
|
+
const t = number(raw.token_valid_time)
|
|
156
|
+
if (t > 1e12) expires = t
|
|
157
|
+
else if (t > 1e9) expires = t * 1000
|
|
158
|
+
else if (t > 0) expires = Date.now() + t * 1000
|
|
159
|
+
}
|
|
160
|
+
if (!expires) expires = Date.now() + 7200 * 1000
|
|
161
|
+
const uid = String(raw.uid ?? raw.user_id ?? "")
|
|
162
|
+
return {
|
|
163
|
+
access,
|
|
164
|
+
refresh,
|
|
165
|
+
expires,
|
|
166
|
+
uid,
|
|
167
|
+
idType: String(raw.idType ?? raw.id_type ?? ""),
|
|
168
|
+
tokenType: String(raw.tokenType ?? raw.token_type ?? ""),
|
|
169
|
+
guid: String(raw.guid ?? ""),
|
|
170
|
+
q36: String(raw.q36 ?? ""),
|
|
171
|
+
iua: String(raw.iua ?? ""),
|
|
172
|
+
extensionVersion: String(raw.extensionVersion ?? raw.extension_version ?? CLIENT_VERSION),
|
|
173
|
+
clientType: String(raw.clientType ?? raw.client_type ?? process.env.IMA_CLIENT_TYPE ?? CLIENT_TYPE),
|
|
174
|
+
accountId: String(raw.accountId ?? uid ?? "ima.copilot"),
|
|
175
|
+
nickname: String(raw.nickname ?? ""),
|
|
176
|
+
source: String(raw.source ?? ""),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 找一个能用的登录态:种子文件 → 本机 ima profile。
|
|
182
|
+
* @returns {object|null}
|
|
183
|
+
*/
|
|
184
|
+
export function importLocalLogin() {
|
|
185
|
+
for (const file of seedPaths()) {
|
|
186
|
+
if (!file || !existsSync(file)) continue
|
|
187
|
+
const state = normalize(readJson(file))
|
|
188
|
+
if (state) return state
|
|
189
|
+
}
|
|
190
|
+
for (const dir of profileDirs()) {
|
|
191
|
+
const prefs = preferencesIn(dir)
|
|
192
|
+
if (!prefs) continue
|
|
193
|
+
try {
|
|
194
|
+
const state = loginFromPreferences(prefs)
|
|
195
|
+
if (state) return state
|
|
196
|
+
} catch {
|
|
197
|
+
// 这个 profile 没登录 / 解密失败:试下一个
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 把登录态写成种子文件(600),供下次登录导入。 */
|
|
204
|
+
export function saveSeed(state) {
|
|
205
|
+
if (!state?.access) return false
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(CACHE_DIR, { recursive: true })
|
|
208
|
+
writeFileSync(
|
|
209
|
+
SEED_FILE,
|
|
210
|
+
JSON.stringify(
|
|
211
|
+
{
|
|
212
|
+
access: state.access,
|
|
213
|
+
refresh: state.refresh,
|
|
214
|
+
expires: state.expires,
|
|
215
|
+
uid: state.uid,
|
|
216
|
+
id_type: state.idType,
|
|
217
|
+
token_type: state.tokenType,
|
|
218
|
+
guid: state.guid,
|
|
219
|
+
q36: state.q36,
|
|
220
|
+
iua: state.iua,
|
|
221
|
+
extensionVersion: state.extensionVersion,
|
|
222
|
+
clientType: state.clientType,
|
|
223
|
+
accountId: state.accountId,
|
|
224
|
+
nickname: state.nickname,
|
|
225
|
+
savedAt: new Date().toISOString(),
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
chmodSync(SEED_FILE, 0o600)
|
|
233
|
+
} catch {}
|
|
234
|
+
return true
|
|
235
|
+
} catch {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const _loginInternal = { CACHE_DIR, SEED_FILE, seedPaths, profileDirs, preferencesIn }
|
package/src/models.mjs
CHANGED
|
@@ -84,6 +84,22 @@ export function buildModels(rows = []) {
|
|
|
84
84
|
const thinking = thinkingName(name)
|
|
85
85
|
models[id] = entry(id, name, thinking, { free, rate })
|
|
86
86
|
index.set(id, { id, name, type, modelId, thinking })
|
|
87
|
+
|
|
88
|
+
// ima 的免费模型用 sub_model_infos 带一个深度思考变体:
|
|
89
|
+
// {"0":{model_type:3,...},"1":{model_type:1,...}}
|
|
90
|
+
// 把它也做成独立模型,用户才能在 magpie 里选思考档。
|
|
91
|
+
for (const sub of Object.values(row?.sub_model_infos ?? {})) {
|
|
92
|
+
const subType = Number(sub?.model_type)
|
|
93
|
+
const subModelId = String(sub?.model_id ?? "")
|
|
94
|
+
if (!Number.isFinite(subType) || !subModelId) continue
|
|
95
|
+
if (subType === type && subModelId === modelId) continue
|
|
96
|
+
let subId = `${slug(name)}-think`
|
|
97
|
+
if (used.has(subId)) subId = `${subId}-${subType}`
|
|
98
|
+
used.add(subId)
|
|
99
|
+
const subName = `${name} Think`
|
|
100
|
+
models[subId] = entry(subId, subName, true, { free, rate })
|
|
101
|
+
index.set(subId, { id: subId, name: subName, type: subType, modelId: subModelId, thinking: true })
|
|
102
|
+
}
|
|
87
103
|
}
|
|
88
104
|
return { models, index }
|
|
89
105
|
}
|