magicrew-auth 0.3.26 → 0.3.29
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 +6 -2
- package/CHANGELOG.md +373 -0
- package/README.md +133 -59
- package/docs/internals.md +139 -3
- package/package.json +1 -1
- package/src/captcha.mjs +146 -43
- package/src/headless.mjs +79 -29
- package/src/hooks/loader.mjs +258 -7
- package/src/hooks/methods.mjs +238 -147
- package/src/hooks/refresh.mjs +14 -7
- package/src/keys.mjs +150 -11
- package/src/vision.mjs +83 -25
package/README.md
CHANGED
|
@@ -12,30 +12,120 @@
|
|
|
12
12
|
凭证 登录一次 30 天全自动:api_key 两小时自续,refresh_token 自滚
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
>
|
|
15
|
+
> 模型清单是 2026-10-27 从两站 `GET /api/v1/flows/models` 拉下来核对的。清单会随订阅档位和平台上下架变 —— 所以插件里**没有一张写死的模型清单**,账户能选什么就显示什么。
|
|
16
16
|
|
|
17
17
|
> **改代码 / 发版本之前**:先看 [AGENTS.md](./AGENTS.md) —— 按「你在哪一步」给阅读顺序,
|
|
18
|
-
>
|
|
19
|
-
> [CHECKLIST.md](./CHECKLIST.md)
|
|
20
|
-
> 发布是**七步**,`git push` 只是第 4 步。
|
|
18
|
+
> 外加本仓库那些**会静默出错**的坑(症状 → 判据 → 出处)。交付与发版逐条走
|
|
19
|
+
> [CHECKLIST.md](./CHECKLIST.md) 的两张表。发布是**七步**,`git push` 只是第 4 步。
|
|
21
20
|
|
|
22
21
|
---
|
|
23
22
|
|
|
24
23
|
## 功能
|
|
25
24
|
|
|
26
25
|
- **两个供应商分得开** —— 国内、国际各一行,图标、名字、模型、积分、登录态全部独立
|
|
27
|
-
-
|
|
26
|
+
- **登录全程不开浏览器** —— 国际站有**邮箱验证码**和**手机号验证码**两条纯 HTTP 路;国内站有手机号验证码。另外两条(浏览器登录 / 手填登录态)谁都能用
|
|
27
|
+
- **已有的账号加进来,只填两次就完事** —— 填邮箱(或手机号)→ 填验证码。**那张「拖动滑块使图片角度为正」的旋转图根本不会出现**:服务端对已注册的账号直接放行,插件用的就是这条通道
|
|
28
|
+
- **本机浏览器登过就直接认** —— 你平时那个 Edge / Chrome / Brave 里登过 magicrew,点「浏览器登录」时插件读它的 localStorage 接上:不开窗口、不用重登
|
|
28
29
|
- **模型跟着账号走** —— 每次登录后按 `/api/v1/flows/models` 实时拉,账户当前能选什么就展示什么,生图和 embedding 不会混进来
|
|
29
30
|
- **积分算得准** —— 余额读的是 `/api/v1/quota/points/total`;空号就显示空,不会被优先派活
|
|
30
|
-
- **积分耗尽自动换号** —— 上游用 HTTP 400 报「可用积分已不足」时,插件把它改写为语义正确的 **402 Payment Required**,magpie 会 rest 这个账号并继续问同 provider 的下一个账号 /
|
|
31
|
+
- **积分耗尽自动换号** —— 上游用 HTTP 400 报「可用积分已不足」时,插件把它改写为语义正确的 **402 Payment Required**,magpie 会 rest 这个账号并继续问同 provider 的下一个账号 / 路由组的下一个成员
|
|
31
32
|
- **登录一次管 30 天** —— 网页 JWT 顶 30 天,`mgw_` api_key 每 2 小时自续,refresh_token 每次续期时自滚
|
|
32
|
-
-
|
|
33
|
-
- **登录方式还有两条备选** —— 手机号+密码走 API;或者直接粘 JWT + 组织码
|
|
34
|
-
- **新手奖励自动领** —— 国内站 8 项里 7 项 PUT 即到账,每天跑一次幂等
|
|
33
|
+
- **新手奖励 8 项全自动** —— 国内站 8 项 500 积分全部拿得到;第 8 项(完成第一次 AI 工作)由插件替你发一条消息,花掉约 2 积分
|
|
35
34
|
- **邀请码自动滚** —— 第一次注册带作者的码,之后每次登录都记住「上一个登录账号」的码,拿到新的就覆盖旧的
|
|
36
35
|
|
|
37
36
|
---
|
|
38
37
|
|
|
38
|
+
## 登录
|
|
39
|
+
|
|
40
|
+
**国际站四条路,国内站三条。** 带「验证码」的那两条是**纯 HTTP、全程不开浏览器** —— 你填一个邮箱或手机号,剩下的插件自己走完。
|
|
41
|
+
|
|
42
|
+
| # | 方式 | 你要做什么 | 国际站 | 国内站 |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| ① | **邮箱验证码** | 填邮箱 → 填 6 位验证码 | 第 1 条 | —(国内站登录页没有邮箱入口) |
|
|
45
|
+
| ② | **手机号验证码** | 填手机号 → 填 6 位验证码 | 第 2 条 | 第 1 条 |
|
|
46
|
+
| ③ | **浏览器登录** | 点一下,在弹出的官方页面里用任何方式登 | 第 3 条 | 第 2 条 |
|
|
47
|
+
| ④ | **登录态(JWT + 组织码)** | 从 DevTools 粘两个字段 | 第 4 条 | 第 3 条 |
|
|
48
|
+
|
|
49
|
+
**国际站把邮箱排第一**,因为那边邮箱注册的账号更多。
|
|
50
|
+
|
|
51
|
+
**为什么国内站没有邮箱那条?** 因为国内站的官方登录页压根没有邮箱入口(只有微信 / 苹果 / 密码 / 手机 / 私有部署),也就没有邮箱注册的账号;而且同一个发信请求在国内站的服务端会回「服务异常,请联系管理员」。摆出来只会让你填完邮箱撞一个看不懂的错。
|
|
52
|
+
|
|
53
|
+
两条验证码路是同一套逻辑(插件里就是同一份代码,只是问的字段和调的两个端点不同),不会出现「手机那条修好了、邮箱那条没跟上」。
|
|
54
|
+
|
|
55
|
+
### 关于那张旋转验证图
|
|
56
|
+
|
|
57
|
+
官方登录页上只有一处需要「看图」:一张圆形照片被随机旋转过,要你拖滑块把它转正。**它只在一种情况下出现:这个手机号 / 邮箱在平台上还从没注册过。**
|
|
58
|
+
|
|
59
|
+
| 你要加的号 | 会出现旋转图吗 | 你需要做什么 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| 平台上**已有**的账号(哪怕第一次加进 magpie) | **不会** —— 服务端直接放行 | 填号码/邮箱 + 填验证码 |
|
|
62
|
+
| 平台上**从没注册过**的新号(首次注册) | 会 | 配了识图模型就全自动;没配就自己转一下 |
|
|
63
|
+
|
|
64
|
+
原理:`POST /captcha/generate` 对已注册的账号**直接回一个 `business_token`**,连图都不给(实测:已注册邮箱 → 免图;换一个没注册过的邮箱 → 给 token + image)。插件用的就是这条通道,所以**绝大多数情况下识图模型根本用不上**。
|
|
65
|
+
|
|
66
|
+
真碰上要过图时(新号注册),插件这么办:
|
|
67
|
+
|
|
68
|
+
1. **配了识图模型**(magpie 设置 → 识图模型)→ 用它判断「主体自己的上方向指向钟面几点」,换算成拖动角度,自动过掉
|
|
69
|
+
2. **没配** → 回退到浏览器,打开官方登录页,你自己把图片转正,剩下的插件接住
|
|
70
|
+
|
|
71
|
+
> 为什么不瞎猜角度:服务端容差 ±10°,瞎猜命中率只有约 5.6%(20/360),平均要试十几次,而它有「验证提交过于频繁」和「验证次数已超限」两道风控 —— 等不到猜中,先被限流。
|
|
72
|
+
>
|
|
73
|
+
> 为什么不问模型「转了多少度」:实测直接问角度 6 次错 5 次,问「指向钟面几点」3 轮中 2。而且同一张图要问两遍比分歧 —— 有一次两个判官给出 1.7 和 7.5 钟点(相差 174°),那是有人把方向看反了,只能换图重来。
|
|
74
|
+
|
|
75
|
+
### 手机号与邮箱的协议差异(给改代码的人)
|
|
76
|
+
|
|
77
|
+
| | 手机号 | 邮箱 |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| 取图 | `POST /captcha/generate` `{account_type:"mobile", mobile, state_code}` | 同端点 `{account_type:"email", email}`(**无** `state_code`) |
|
|
80
|
+
| 过验证 | `POST /captcha/verify` `{token, angle, account_type, mobile, state_code}` | 同端点,换成 `email` |
|
|
81
|
+
| 发验证码 | `POST /v4/user/send-sms` `{type:"account_login", phone, state_code}` | `POST /v4/user/send-email` `{email, type:"account_login"}` |
|
|
82
|
+
| 换登录态 | `POST /v4/user/login`,`type:"phone_captcha"`,验证码字段叫 **`code`** | `POST /v4/user/login-email`,验证码字段叫 **`captcha`** |
|
|
83
|
+
|
|
84
|
+
两个 `type` 都是 `"account_login"`(填错会回「服务异常,请联系管理员」—— 后端内部炸了,从报错完全看不出是它)。邮箱那条**必须**走 `login-email`:往 `/v4/user/login` 发任何 type 都回「phone 字段是必须的」。
|
|
85
|
+
|
|
86
|
+
### 浏览器登录
|
|
87
|
+
|
|
88
|
+
点第三条第 ③ 个方式时,它先看你平时用的那个浏览器:
|
|
89
|
+
|
|
90
|
+
**① 那边已经登录过 magicrew** → 插件直接读它的 localStorage 接上,**不开窗口、不用再登一次**。
|
|
91
|
+
凭据就明文躺在 `Local Storage/leveldb` 里(Edge / Chrome / Chromium / Brave / 360 / QQ / 搜狗 都行)。
|
|
92
|
+
|
|
93
|
+
**② 没登录过,或者这个账号已经加过** → 插件自己拉起一个窗口:
|
|
94
|
+
|
|
95
|
+
1. 浏览器窗口弹出,停在官方登录页
|
|
96
|
+
2. 用你平时的方式登进去(手机号 / 密码 / Google / 邮箱验证码 / 飞书 / 钉钉都行)
|
|
97
|
+
3. **登录完成那一刻它自己就接上了** —— 不用回来点任何按钮,不用开 DevTools
|
|
98
|
+
|
|
99
|
+
原理:插件不是调用系统浏览器,而是自己起一个 Chromium 内核的浏览器(独立 profile + `--remote-debugging-port=0`),所以登录成功后能用 CDP 在页面上下文里把它自己的 localStorage 读走,拿到 JWT 和组织码,签完密钥就关窗。
|
|
100
|
+
|
|
101
|
+
**用哪个浏览器?** 按「谁更干净」排,不是按名气:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
① Chrome / Chromium 自己装的,无企业策略、无强制登录 ← 首选
|
|
105
|
+
② 国产 Chromium 系 360 / QQ / 搜狗 / 2345 / 猎豹 / UC / 夸克
|
|
106
|
+
③ Edge 垫底(策略最多、最容易被拦住调试端口)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
前一个起不来(策略禁用调试端口、启动就退出)会自动换下一个,**第一个真正打开调试通道的才算数**。窗口标题/提示里会写明这次用的是哪个。
|
|
110
|
+
|
|
111
|
+
**每次登录都是一个新的 profile,用完就删。** 这是必须的:共用一个 profile 的话,添加第二个账号时窗口一打开,页面里已经躺着上一个账号的凭据 —— 插件会以为登录完成、立刻关窗,而 magpie 拿到的还是旧账号。
|
|
112
|
+
|
|
113
|
+
> **为什么零浏览器那两条要自己写协议?** magicrew 没有 OAuth 回调通道 —— 登录类型只有十几种、全是「拿凭据换 token」,没有设备码、没有插件授权、没有一次性 ticket。凭据只可能落在三个地方:用户中心的 HTTP 接口(插件走的就是这条)、你自己浏览器的 localStorage、或者插件自己起的那个窗口。
|
|
114
|
+
|
|
115
|
+
### 登录态(JWT + 组织码)
|
|
116
|
+
|
|
117
|
+
不想走上面任何一条的话,手填两个字段:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
DevTools → Application → Local Storage → https://www.magicrew.ai
|
|
121
|
+
MAGIC:user:authorization → JSON,取 value = JWT
|
|
122
|
+
MAGIC:user:organizationCode → JSON,取 value = 组织码(12 位小写字母数字)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> 组织码**不能从 JWT 推**。JWT 里只有 `cus.tenant_id`(18 位数字),组织码是另一套短串。缺了它 `/api/v1/*` 回 `2182 用户所在组织不存在`,给错了回 `3035 登录失败`。
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
39
129
|
## 模型
|
|
40
130
|
|
|
41
131
|
**国内的 GLM、DeepSeek、Qwen、Kimi、MiniMax、MiMo、豆包;国际站再加 Claude、GPT、Gemini 三家。**
|
|
@@ -70,11 +160,27 @@
|
|
|
70
160
|
|
|
71
161
|
## 新手奖励
|
|
72
162
|
|
|
73
|
-
|
|
163
|
+
**8 项,合计 500 积分,全自动 —— 登录后和每天签到各跑一次,幂等。**
|
|
164
|
+
|
|
165
|
+
| 项 | 分数 | 插件怎么拿 |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| 完成第一次 AI 工作 | 100 | **替你在话题里发一条消息**(花掉约 2 积分)—— 服务端只认这个 |
|
|
168
|
+
| 了解工作台 | 50 | PUT 即到账 |
|
|
169
|
+
| 了解如何使用 AI PPT | 50 | PUT 即到账 |
|
|
170
|
+
| AI 录音 | 50 | PUT 即到账 |
|
|
171
|
+
| 雇佣数字员工 | 50 | PUT 即到账 |
|
|
172
|
+
| 添加 / 管理技能 | 50 | PUT 即到账 |
|
|
173
|
+
| 微应用 | 50 | PUT 即到账 |
|
|
174
|
+
| Magiclaw | 50 | PUT 即到账 |
|
|
175
|
+
|
|
176
|
+
第 8 项是唯一 PUT 不掉的:`PUT /onboarding/tasks/first_ai_work/completion` 服务端回 `9303「该新人引导任务不支持手动完成」`—— 它认死产品内真的跑完过一次对话。所以插件替它**发一条消息**(走话题的 socket 协议,不开浏览器),这也是**邀请奖励的触发条件** —— 被邀请人发过一条消息,邀请人才拿到那笔分。
|
|
74
177
|
|
|
75
|
-
|
|
178
|
+
这道「发消息」有闸门:**每个账号一辈子一次**(`isAutoChatSettled`),发出去就落永久标记。所以重复登录、换天签到、重启 magpie 都不会再发第二条、不会再白烧积分。
|
|
76
179
|
|
|
77
|
-
|
|
180
|
+
失败方向是「宁可晚一轮」:查不到判据(比如话题接口抖了一下)时这次不发、不落标记、不计数 —— 消息发出去是花钱且不可撤的。真失败才计数,3 次之后放弃,免得一个永远失败的号每次签到撞一遍。
|
|
181
|
+
|
|
182
|
+
> 国际站 `/api/v1/onboarding/tasks` 恒返回空列表 —— 那是「这个账号没被登记进引导计划」,不是「已经领完了」。
|
|
183
|
+
> 但邀请奖励的判据是一样的:官方文案写「被邀请人完成首个任务或对话后,即可获得积分奖励」,所以国际站这条就等于**发过一条消息**。插件用「有没有话题」判断(不是有没有项目 —— 项目打开工作台就会自动建),没话题就补一条。
|
|
78
184
|
|
|
79
185
|
---
|
|
80
186
|
|
|
@@ -97,7 +203,7 @@
|
|
|
97
203
|
```
|
|
98
204
|
|
|
99
205
|
> ③ 不是每次都能成。`/api/v1/invitations/info` 只认带 `device_id` 的 JWT —— 同一个账号同一时刻签的两把,差别就在这一个字段:桌面客户端 438 字符(`device_id` = 64 位 hex)能拿到码,网页版 350 字符(`device_id` 空)回 `2155 用户不存在`。
|
|
100
|
-
> 密码登录走 API、带 device
|
|
206
|
+
> 密码登录走 API、带 device 指纹,滚得动;浏览器登录和验证码登录拿到的是网页那把,滚不动 —— 那就保留旧值,不报错、不挡登录。
|
|
101
207
|
|
|
102
208
|
奖励口径(上游前端常量,2026-10-27):
|
|
103
209
|
|
|
@@ -112,53 +218,11 @@
|
|
|
112
218
|
|
|
113
219
|
在 magpie 的 **插件 → 发现** 搜 `magicrew`,点**安装**(npm 上的包名是 `magicrew-auth`)。
|
|
114
220
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
**① 那边已经登录过 magicrew** → 插件直接读它的 localStorage 接上,**不开窗口、不用再登一次**。
|
|
118
|
-
凭据就明文躺在 `Local Storage/leveldb` 里(Edge / Chrome / Chromium / Brave / 360 / QQ / 搜狗 都行)。
|
|
119
|
-
|
|
120
|
-
**② 没登录过,或者这个账号已经加过** → 插件自己拉起一个窗口:
|
|
121
|
-
|
|
122
|
-
1. 浏览器窗口弹出,停在官方登录页
|
|
123
|
-
2. 用你平时的方式登进去(手机号 / 密码 / Google / 邮箱验证码 / 飞书 / 钉钉都行)
|
|
124
|
-
3. **登录完成那一刻它自己就接上了** —— 不用回来点任何按钮,不用开 DevTools
|
|
125
|
-
|
|
126
|
-
原理:插件不是调用系统浏览器,而是自己起一个 Chromium 内核的浏览器(独立 profile +
|
|
127
|
-
`--remote-debugging-port=0`),所以登录成功后能用 CDP 在页面上下文里把它自己的
|
|
128
|
-
localStorage 读走,拿到 JWT 和组织码,签完密钥就关窗。
|
|
129
|
-
|
|
130
|
-
**用哪个浏览器?** 按「谁更干净」排,不是按名气:
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
① Chrome / Chromium 自己装的,无企业策略、无强制登录 ← 首选
|
|
134
|
-
② 国产 Chromium 系 360 / QQ / 搜狗 / 2345 / 猎豹 / UC / 夸克
|
|
135
|
-
③ Edge 垫底(策略最多、最容易被拦住调试端口)
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
前一个起不来(策略禁用调试端口、启动就退出)会自动换下一个,
|
|
139
|
-
**第一个真正打开调试通道的才算数**,不用你操心。
|
|
140
|
-
窗口标题/提示里会写明这次用的是哪个。
|
|
141
|
-
|
|
142
|
-
**每次登录都是一个新的 profile,用完就删。** 这是必须的:共用一个 profile 的话,
|
|
143
|
-
添加第二个账号时窗口一打开,页面里已经躺着上一个账号的凭据 —— 插件会以为登录完成、
|
|
144
|
-
立刻关窗,而 magpie 拿到的还是旧账号(同账号 → 替换,不新增)。
|
|
145
|
-
|
|
146
|
-
> **为什么两条路都要?** magicrew 没有 OAuth 回调通道 —— 登录类型只有 13 种、全是「拿凭据换 token」,没有设备码、没有插件授权、没有一次性 ticket。凭据只可能落在两个地方:你自己浏览器的 localStorage(插件读文件就能拿到,所以优先),或者插件自己起的那个窗口(读不到时才需要)。
|
|
147
|
-
>
|
|
148
|
-
> 本机没有任何 Chromium 内核浏览器时自动降级:改用系统浏览器打开,登录完把凭证送进剪贴板,插件从剪贴板读。
|
|
149
|
-
|
|
150
|
-
不想碰 Console 的话,另外两条:
|
|
221
|
+
装完选站点,然后挑一条登录路:
|
|
151
222
|
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
DevTools → Application → Local Storage → https://www.magicrew.ai
|
|
157
|
-
MAGIC:user:authorization → JSON,取 value = JWT
|
|
158
|
-
MAGIC:user:organizationCode → JSON,取 value = 组织码(12 位小写字母数字)
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
> 组织码**不能从 JWT 推**。JWT 里只有 `cus.tenant_id`(18 位数字),组织码是另一套短串。缺了它 `/api/v1/*` 回 `2182 用户所在组织不存在`,给错了回 `3035 登录失败`。
|
|
223
|
+
- **最省事(推荐)** → 点**「浏览器登录」**:它先看你本机浏览器有没有登过,没有就自己拉一个窗口起来,你在里面用任何方式登(连验证码都不用填)
|
|
224
|
+
- **国际站全自动登录** → 点**「邮箱验证码」**(排第一):填邮箱 → 填验证码。已有账号的话,中间那张旋转图不会出现
|
|
225
|
+
- **国内站全自动登录** → 点**「手机号验证码」**(排第一):同理
|
|
162
226
|
|
|
163
227
|
### 命令行
|
|
164
228
|
|
|
@@ -190,6 +254,14 @@ magpie plugin list
|
|
|
190
254
|
|
|
191
255
|
**两个站要装两次吗?** 不用。一个包导出两个插件,装一次两个供应商都在,各自登录。
|
|
192
256
|
|
|
257
|
+
**邮箱验证码能注册新号吗?** 能 —— 但**只有国际站有这条方法**。这条链和手机号那条一样带 `auto_register`,第一次用的邮箱会自动建号,而且注册请求里带着邀请码。新号注册那一次会要求过旋转图(服务端的规定)—— 配了识图模型就全自动,没配就自己转一下。
|
|
258
|
+
|
|
259
|
+
**为什么国内站没有「邮箱验证码」?** 因为国内站的官方登录页就没有邮箱入口(只有微信 / 苹果 / 密码 / 手机 / 私有部署),也就没有邮箱注册的账号;而且同一个发信请求在国内站服务端回「服务异常,请联系管理员」。所以国内站只给三条路:手机号验证码 / 浏览器登录 / 手填登录态。
|
|
260
|
+
|
|
261
|
+
**旋转验证图能不能不出现?** 加**已有**的账号时它本来就不出现(服务端直接放行)。只有注册全新账号才需要过它。
|
|
262
|
+
|
|
263
|
+
**看图那一步为什么有点慢?** 因为识图模型是**推理模型**的话,它的「思考」也算 token —— 实测这个模型的思考链能写到三千多字。插件已经做了两件事:让它跑最低思考档(`reasoning_effort: low`,实测耗时 −59%、token −57%),以及**两个判官并行**问(耗时等于较慢的那一次)。想更快,就在 magpie 设置里换一个**非推理**的视觉模型 —— 那是唯一能真正「不思考」的办法,参数关不掉。
|
|
264
|
+
|
|
193
265
|
**我在自己浏览器里登了 magicrew,插件能自动认出来吗?** 能。点「浏览器登录」时插件先去本机 Edge / Chrome / Chromium / Brave / 360 / QQ / 搜狗 等浏览器的 localStorage 里找**这个站点**的登录态,找到就直接接上,不弹窗口。读不到、或者这个账号已经加过,才会开窗口让你登新的。
|
|
194
266
|
|
|
195
267
|
**点了「浏览器登录」,网页上明明登好并跳到 agent 界面了,magpie 里却一直转圈等?** 0.3.12 及更早会这样:插件拉起浏览器的判断依赖一个写死的文件名 `msedge.exe`,而有些机器上微软落盘的名字是 **`Microsoft Edge.exe`**(带空格、带大写;落盘名随安装方式和版本变),于是每次都判定「本机没装浏览器」,永远走「让你去 Console 粘一行」的降级路径 —— 而你不知道有这一步。0.3.15 起:两种落盘名都认、把 360 / QQ / 搜狗 等国产 Chromium 也纳入候选、并且逐个试启动(第一个真能开调试通道的才用)。判据在 `~/.cache/magpie/magicrew-login-<站点>.log`,出问题先看它最后一行。
|
|
@@ -198,6 +270,8 @@ magpie plugin list
|
|
|
198
270
|
|
|
199
271
|
**npm 上叫 `magicrew-auth`,为什么卡片显示 `Magicrew`?** 卡片标题取包内 `package.json` 的 `name`(`Magicrew`),而 npm 拒绝含大写字母的新包名。发布时用 `publish-npm.mjs` 临时换成 `magicrew-auth`、发完还原 —— 所以 npm 页面是小写包名,git 源装出来是 `Magicrew`。功能完全一样。
|
|
200
272
|
|
|
273
|
+
**验证码登录报「验证码已发送」但没收到?** 先看是不是发到别的账号上了(同一个手机号/邮箱在两个站点是两套账号)。再确认这个号是不是**还没注册** —— 那种情况要先过旋转图,图没过,短信 / 邮件根本不会发出去,界面会直接报「旋转验证连试 4 张图都没过」。
|
|
274
|
+
|
|
201
275
|
**一个账号积分用完了,会换到另一个还有积分的账号吗?** 会。0.3.26 起插件把上游的 400「可用积分已不足」改写成 402 Payment Required —— magpie 对 402 一律视为可重试:先 rest 这个账号 30 分钟,再问同 provider 的下一个账号;如果整个组都没有额度,才会把错误交给客户端。
|
|
202
276
|
|
|
203
277
|
**为什么模型列表会变?** 因为清单是从 `/api/v1/flows/models` 实时拉的。订阅档位变了、平台方上下架变了,清单就跟着变 —— 插件里没有需要维护的模型表。
|
package/docs/internals.md
CHANGED
|
@@ -822,7 +822,7 @@ store 槽 a expires 2026-10-28T14:41:03.000Z → 200
|
|
|
822
822
|
不管吊销机制是什么,**只留一把、只有一个出口**这个设计依然成立 ——
|
|
823
823
|
理由从「否则互相吊销」改成「否则本地会分叉,说不清手里那把是不是服务端认的」。
|
|
824
824
|
|
|
825
|
-
### 保活链路(2026-10-
|
|
825
|
+
### 保活链路(0.3.29 起;2026-10-29 逐版本核对)
|
|
826
826
|
|
|
827
827
|
```
|
|
828
828
|
每次请求前 ensureKey(c, getAuth) ← src/hooks/loader.mjs
|
|
@@ -831,13 +831,27 @@ store 槽 a expires 2026-10-28T14:41:03.000Z → 200
|
|
|
831
831
|
├─ 有 refresh_token → PUT 换新的 → 覆盖落盘
|
|
832
832
|
└─ 没有 / 失败 → 退回 magpie 存的那把
|
|
833
833
|
|
|
834
|
+
keyCandidates(c, auth) ← 同一账号的 store 两槽 + auth.access
|
|
835
|
+
└─ 逐把打 /v1/chat/completions;被拒换下一把,命中写回 store(adoptKey)
|
|
836
|
+
|
|
834
837
|
到期前 10 分钟 auth.refresh(auth) ← magpie 按 refreshLead 主动调度
|
|
835
|
-
└─ src/hooks/refresh.mjs
|
|
836
|
-
|
|
838
|
+
└─ src/hooks/refresh.mjs → keys.mjs 的 renewViaStore()
|
|
839
|
+
与 ensureKey 共用 renewOnce 锁和同一个 key-store;
|
|
840
|
+
refresh_token 先试 store 那份、再试 magpie 那份(升级兼容)
|
|
837
841
|
|
|
838
842
|
每天一次 checkin → runCheckin() ← 领积分 / 补首任务 / 新手福利,**不碰 key**
|
|
839
843
|
```
|
|
840
844
|
|
|
845
|
+
⚠ 0.3.29 之前 refresh.mjs 自己调 `api.refreshGatewayKey(auth.refresh)`、不写 key-store:
|
|
846
|
+
两条签发路径各存一把 key,而 key 只活 2 小时,两条路各自续到哪把、哪把已经过期,
|
|
847
|
+
本地并不一致。2026-10-29 实测 5 个国际站账号里 3 个「静态挑一把」踩空,表现是
|
|
848
|
+
403 `token无效` 且 `retryable=false`(magpie 不换号,整轮直接抛给客户端)。
|
|
849
|
+
`keyCandidates()` 的逐把试与 `renewViaStore()` 的收敛就是为这两条路分别兜底。
|
|
850
|
+
|
|
851
|
+
⚠ 别把这里读成「服务端同一账号只认最新签发的那把」—— 2026-10-28 的受控实验
|
|
852
|
+
已经推翻过:同账号相隔 67 秒签的两把同时有效;12 把逐把探活,403 全部对应
|
|
853
|
+
`expiresAt` 已过。真正失效的是**手里那把过期了**,不是后来签的把它顶掉了。
|
|
854
|
+
|
|
841
855
|
### checkin 现在做四件事(0.3.11 起)
|
|
842
856
|
|
|
843
857
|
```
|
|
@@ -1169,6 +1183,128 @@ HTTP 400
|
|
|
1169
1183
|
`raw → 400 tools.40.../items`;过 `rewriteRequestBody` 后 `→ 200`。
|
|
1170
1184
|
国内站、国际站都验过。
|
|
1171
1185
|
|
|
1186
|
+
### 三、`items` **子树内部**的 union / 空 schema → `400 invalid JSON Schema`
|
|
1187
|
+
|
|
1188
|
+
**症状**(2026-10-28,DSH 经 magpie 网关调 DeepSeek-V4.1-Flash):
|
|
1189
|
+
|
|
1190
|
+
```
|
|
1191
|
+
本轮运行失败 400: {"code":null,"message":"超级麦吉: HTTP 400 请求错误:
|
|
1192
|
+
{\"error\":{\"code\":\"InvalidParameter\",\"message\":\"tools.40.function.parameters/
|
|
1193
|
+
properties/functions/items: invalid JSON Schema; check the schema keywords
|
|
1194
|
+
and references Request id: ...\",\"param\":\"\",\"type\":\"BadRequest\"}}",
|
|
1195
|
+
"param":null,"type":"invalid_request_error"}
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
**报错路径与上面第二节一模一样,但请求里根本没有空的 `items`** —— 这是本节存在的理由。
|
|
1199
|
+
0.3.24 的 `loosenEmptyItems()` 只删 `items` **自己**的空对象,管不到 `items` **内部**。
|
|
1200
|
+
|
|
1201
|
+
#### 根因:上游对数组元素的 schema 比对根节点更严
|
|
1202
|
+
|
|
1203
|
+
**实测**(2026-10-28~29,直打上游、同一把 key、同一份请求、只改这一个字段;
|
|
1204
|
+
复跑脚本 `_probe/items-subtree-upstream.mjs`,稳定性统计 `_probe/items-subtree-stability.mjs`):
|
|
1205
|
+
|
|
1206
|
+
| 位置 | 形态 | 结果 |
|
|
1207
|
+
|---|---|---|
|
|
1208
|
+
| `items` 内部 | `anyOf` / `oneOf` / `allOf` | **400** |
|
|
1209
|
+
| `items` 内部 | `not` / `const` | **400** |
|
|
1210
|
+
| `items` 内部 | `if` + `then`(`else` 同理) | **400** |
|
|
1211
|
+
| `items` 内部 | `{}`(空 schema) | **400** |
|
|
1212
|
+
| `items` 内部 | `items` 本身是**数组**(draft-04 tuple 形式) | 国内站 200 / **国际站 400**(两站不一致) |
|
|
1213
|
+
| `items` 内部 | `type` / `enum` / `pattern` / `minLength` / `minItems` / `format` / `$ref` / 数值边界 | 200 |
|
|
1214
|
+
| `items` 内部 | `type:["object","string","number","boolean","array","null"]` | 200(**「任意值」的合法写法**) |
|
|
1215
|
+
| **顶层 parameters** | `anyOf` / `const` / `not` / `{}` | **200** |
|
|
1216
|
+
|
|
1217
|
+
⇒ **同样形态放在顶层上游收,放进 `items` 子树就炸**。这条边界是本节的关键:
|
|
1218
|
+
改写必须精确落在 `items` 之下,全局摊平 union 会白白丢掉顶层那些上游**认**的约束。
|
|
1219
|
+
|
|
1220
|
+
⇒ 还有一条纪律:**「任意值」不能写成 `{type:"object"}`** —— 那会把「任意」收紧成
|
|
1221
|
+
「只能是对象」。`{}` 与 `true` 在这层都被拒,只能落成显式的六类型 `type` 数组。
|
|
1222
|
+
|
|
1223
|
+
#### 命中来源:MCP 工具的 schema
|
|
1224
|
+
|
|
1225
|
+
不是客户端手写,而是 **MCP 服务器**声明的工具 schema。REA MCP 的
|
|
1226
|
+
`mcp__rea__build_call_path`、`mcp__rea__compare_application_versions` 等
|
|
1227
|
+
**7 个工具**的 `functions` 数组参数,元素 schema 用了:
|
|
1228
|
+
|
|
1229
|
+
- `anyOf: [{…}, {type:"null"}]` —— 可选参数的标准写法;
|
|
1230
|
+
- `oneOf` —— 同类互斥分支;
|
|
1231
|
+
- `{}` —— 空 schema(如 `normalized_result`、`raw_result` 的分支)。
|
|
1232
|
+
|
|
1233
|
+
一份 65 工具的请求里 7 个工具中招 ⇒ **整轮 400**(上游一次只报第一个坏位置,
|
|
1234
|
+
所以报错里的 `tools.N` 序号会随工具表顺序变 —— 别拿序号定位问题)。
|
|
1235
|
+
|
|
1236
|
+
#### 修法:`loosenItemsSchema()`
|
|
1237
|
+
|
|
1238
|
+
只作用于 `items` 子树:
|
|
1239
|
+
|
|
1240
|
+
```
|
|
1241
|
+
anyOf / oneOf / allOf → 取一个分支内联(分支本身先递归修)
|
|
1242
|
+
⚠ 优先取**非纯 null** 的分支 —— `anyOf:[{…},{type:"null"}]`
|
|
1243
|
+
取第一个正好;顺序反过来时照抄第一个会把「可选」压成
|
|
1244
|
+
「只能 null」,那是收紧
|
|
1245
|
+
not / const → 删除
|
|
1246
|
+
if / then / else → 删除
|
|
1247
|
+
{} → {type:["object","string","number","boolean","array","null"]}
|
|
1248
|
+
items 是数组(tuple) → {type:[同上]} ← 国际站对数组形式回 400
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
全部是**降级放宽**,方向安全(宁可约束松一点,也不要整轮失败):
|
|
1252
|
+
|
|
1253
|
+
- union 取一个分支:`anyOf:[{…},{type:"null"}]` 的分支正是「非空」那个;
|
|
1254
|
+
- `const` 删除 ⇒ 「只能是这一个值」变「任意值」,放宽;
|
|
1255
|
+
- `not` / `if+then+else` 无等价物,删掉同样是放宽;
|
|
1256
|
+
- `{}` → 六类型 `type` 数组:**与空 schema 严格等价**(六种 JSON 类型全覆盖 = 任意值),
|
|
1257
|
+
不是「等价于 `{type:"object"}`」—— 那会收紧。
|
|
1258
|
+
|
|
1259
|
+
⚠ **删除会删空节点**:`{const:"x"}`、`{not:{…}}`、`{if:…,then:…}` 删完就成 `{}`,
|
|
1260
|
+
而空 schema 在 `items` 子树里**仍然 400**。所以删完必须在同一个函数里再查一次空对象并补成
|
|
1261
|
+
「任意值」—— 漏了这一步,改写看起来生效、实际仍然整轮 400(PR #10 初版就漏在这里,
|
|
1262
|
+
`not` / `const` 两例改写后实测仍 400)。
|
|
1263
|
+
|
|
1264
|
+
递归同样遵守 schema 位置纪律(`walkSchema` 那套 `SCHEMA_MAPS` / `SCHEMA_LIST` /
|
|
1265
|
+
`SCHEMA_ONE`):只进 `properties` 的**值**、union 的**分支**、`items` 的**值**……
|
|
1266
|
+
不碰 `enum` / `default` / `examples` 里的字面值。
|
|
1267
|
+
|
|
1268
|
+
#### 端到端验证(2026-10-29)
|
|
1269
|
+
|
|
1270
|
+
真实报障形态的 65 工具请求体(58 个填充 + 7 个中招的 REA 工具,脚本里现成构造):
|
|
1271
|
+
|
|
1272
|
+
| 站点 | 原样 | 过 `rewriteRequestBody` |
|
|
1273
|
+
|---|---|---|
|
|
1274
|
+
| 国内站 `i-magic-service.letsmagic.cn`(deepseek-v4.1-flash) | `400 … invalid JSON Schema` | **200** |
|
|
1275
|
+
| 国际站 `magic-service.magicrew.ai`(deepseek-v4.1-flash) | `400 … tools.58…/items` | **200** |
|
|
1276
|
+
|
|
1277
|
+
单点矩阵(21 个形态 × raw / 改写后,两站各跑一遍):**所有 `items` 子树非法形态
|
|
1278
|
+
400 → 200**,顶层对照形态保持 200 不动,合法关键字(`enum` / `pattern` / `$ref`)原样保留。
|
|
1279
|
+
|
|
1280
|
+
复跑方式:
|
|
1281
|
+
|
|
1282
|
+
```bash
|
|
1283
|
+
# 国内站(node 直连即可)
|
|
1284
|
+
node ./_probe/items-subtree-upstream.mjs
|
|
1285
|
+
# 国际站(node 连不上该域名,用 magpie 自带的 bun)
|
|
1286
|
+
~/.cache/magpie/bun/1.4.4/magpie-bun.exe ./_probe/items-subtree-upstream.mjs intl
|
|
1287
|
+
# 稳定性统计(同一形态连发 N 轮,判「恒 400」还是「偶发」)
|
|
1288
|
+
node ./_probe/items-subtree-stability.mjs 3 cn
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
⚠ **上游的校验会变,别把单次探针当定论**。2026-10-29 这一夜实测到的完整时间线:
|
|
1292
|
+
|
|
1293
|
+
| 本地时间(UTC+8) | 站点 | `items` 子树非法形态(A 组 7 个) | tuple 形式 / `if+then` |
|
|
1294
|
+
|---|---|---|---|
|
|
1295
|
+
| 00:19 | 国内 | **稳定 400**(7/7) | 200 / 400 |
|
|
1296
|
+
| 00:24 | 国际 | **稳定 400**(7/7) | 200 / 400 |
|
|
1297
|
+
| 00:29(3 轮 ×21 形态统计) | 国内 | 已全变 **200** | 200 |
|
|
1298
|
+
| 00:35 | 国内 | 仍全 **200**(含真实报障形态的 raw) | 200 |
|
|
1299
|
+
|
|
1300
|
+
⇒ 上游在 00:25~00:29 之间放宽了这层校验(灰度或发布,未确认)。
|
|
1301
|
+
**这不改变本节的结论**:改写现在是「保险」而不是「必需」——上游随时可能收回去
|
|
1302
|
+
(同一夜里已经出现过波动),而改写的代价只是一次遍历。
|
|
1303
|
+
|
|
1304
|
+
所以判「某个形态要不要改写」要看多轮统计,别信单次探针:
|
|
1305
|
+
`_probe/items-subtree-stability.mjs` 就是为这件事写的(`N 轮 × 每形态 raw/改写各一次`,
|
|
1306
|
+
输出 `400 次数 / 轮数`)。
|
|
1307
|
+
|
|
1172
1308
|
### 为什么算插件的事,不算客户端的事
|
|
1173
1309
|
|
|
1174
1310
|
按规范 `{}` 与 `true` 等价,客户端没写错;**上游的兼容性不足**。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magicrew-auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
4
4
|
"description": "超级麦吉 / MagiCrew for magpie and OpenCode — one package, two providers: letsmagic.cn (国内) and magicrew.ai (international). Password or web-JWT sign-in, the account's own live model list, points balance, newcomer rewards, invite codes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|