magicrew-auth 0.3.16 → 0.3.23
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 +34 -2
- package/CHANGELOG.md +280 -0
- package/docs/internals.md +155 -4
- package/docs/upstream-issue-3950.md +90 -0
- package/package.json +1 -1
- package/src/captcha.mjs +275 -0
- package/src/headless.mjs +287 -0
- package/src/hooks/loader.mjs +178 -93
- package/src/hooks/methods.mjs +223 -2
- package/src/hooks/usage.mjs +124 -22
- package/src/keys.mjs +48 -1
- package/src/vision.mjs +193 -0
package/AGENTS.md
CHANGED
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## 零、magpie 升级时:只看稳定版
|
|
12
|
+
|
|
13
|
+
**判断「要不要跟着 magpie 改这个插件」看稳定渠道,不看每版本渠道。**
|
|
14
|
+
magpie 设置里有「更新渠道:每个版本 / 稳定版」,用户默认跑后者,插件要跟的也是后者。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl -s "https://usemagpie.ai/api/latest?channel=stable" | grep -o '"version":"[^"]*"'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
实测(2026-10-28):两档差 **400 多个版本**(每版本 0.1.1763 / 稳定版 0.1.1327),
|
|
21
|
+
但把两边内嵌契约都提取出来逐字段比对后 —— **插件相关的字段一条不差**。
|
|
22
|
+
多出来的段是 GUI 历史,不是插件契约。⇒ 追每版本几乎全是白跑。
|
|
23
|
+
|
|
24
|
+
**稳定版版本号变了**,才需要重提取契约(脚手架的 `extract-contract.py`)→
|
|
25
|
+
跑 `contract-diff.mjs` → 看差集里有没有「不写会关掉某个能力」的字段。
|
|
26
|
+
完整判据见脚手架的
|
|
27
|
+
[HOOKS.md](https://github.com/MYCF711/magpie-plugin-scaffold/blob/main/HOOKS.md) 顶部
|
|
28
|
+
与 [UPDATING.md](https://github.com/MYCF711/magpie-plugin-scaffold/blob/main/UPDATING.md) 第七节。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
11
32
|
## 一、先定位你在哪一步
|
|
12
33
|
|
|
13
34
|
| 你在做什么 | 必读(按顺序) | 做完的判据 |
|
|
@@ -32,7 +53,7 @@
|
|
|
32
53
|
|---|---|---|---|
|
|
33
54
|
| 1 | 改好了国内站,国际站用户报同样的错 | **两站是同一个 Go 服务、逐字符同构**,改一处就要两站都验;`SITES.cn` / `SITES.intl` 只是 host 不同 | internals 一 |
|
|
34
55
|
| 2 | 国际站 `/v1/chat/completions` 回 `HTTP 500` + 22 字节裸文本(不是 JSON) | 国际站网关**强制**要 `organization-code` 头,缺了就是这个 500 | internals 一 / 五 |
|
|
35
|
-
| 3 | 换了 key 之后老 key 立刻失效、用户被要求重登 |
|
|
56
|
+
| 3 | 换了 key 之后老 key 立刻失效、用户被要求重登 | **「签新 key 会吊销旧 key」已被实测推翻(2026-10-28)** —— 见下方「最容易误信的旧说法」。仍然只留一把、只走 `src/keys.mjs` 的唯一出口,理由是**本地别分叉**,不是互相吊销 | internals 十四 |
|
|
36
57
|
| 4 | 刷新 token 之后两处 refresh_token 不一致、用户莫名掉登录 | 只允许**一个**出口持有 refresh_token;`src/hooks/refresh.mjs` 绕过出口就会分叉 | internals 二 / 十四 |
|
|
37
58
|
| 5 | agent 说「上下文放不下」,而实际模型能吃更长 | 上下文窗口**必须查 magpie 的模型库**,不能填保守值(早前一刀切 256K,实际 1M) | internals 十六 |
|
|
38
59
|
| 6 | 流式回复拿到了,但 `usage` 永远是空、额度不扣 | 流式请求必须补 `stream_options.include_usage`,否则上游不回 usage | internals 十二 |
|
|
@@ -43,12 +64,23 @@
|
|
|
43
64
|
| 11 | Windows 上点登录,浏览器窗口拉不起来 | 浏览器可执行文件名**随版本变**(`msedge.exe` / Chrome 各版本目录不同),要逐个试候选,别写死一个名字 | internals 十一 |
|
|
44
65
|
| 12 | 用户点「检查更新」显示有新提交,点了更新却没变化 | 本仓库是 **git 源**:magpie 比默认分支 HEAD 的 commit,只提示不自动更新,要用户点那一行的「更新」;更新后 `magpie plugin list` 的 commit 应变化 | 脚手架 UPDATING 一 |
|
|
45
66
|
| 13 | 想让卡片标题 / 「源码」按钮对,去改了 magpie 的缓存 | 插件对 magpie 目录**只有读的份**;唯一载体是 `registry-entry.json`(提交给 `registry.json`) | internals 二 · README |
|
|
67
|
+
| 14 | **卡片上额度正常,路由页里这个号却是「剩余额度未知」** | 窗口**不能标 `aside`**:magpie 的 `allowanceOf()` 把 `Aside`/`Text` 的窗口整条跳过,一条不剩 ⇒ 该号 `Known=false`(官网文档 issue #3650 明写)。判据:`curl -s http://127.0.0.1:3425/v1/magpie/trace`,看这个 provider 的 `known` 是 `true` 还是**字段缺失** | HOOKS 三 · `src/hooks/usage.mjs` 头 |
|
|
68
|
+
| 15 | 接 ZCode / Claude Code 系客户端,带工具的请求回 `400 {"error":{"message":"模型响应失败","code":4017}}` | **上游的 `additionalProperties` 只认布尔值,任何对象形态都拒** —— 不只是 `{}`,`{type:"string"}` 这种有约束的 schema 一样 400(两个模型逐行一致)。判据:同一请求只把该字段换成 `true` → 200。改写已在 `rewriteRequestBody` 里做(非布尔一律换 true;**这是降级放宽、会丢约束**,取舍见 internals 十七) | internals 十七 · `tests/body-rewrite.mjs` |
|
|
69
|
+
| 16 | 网关路径(agent 从 magpie 的 `/v1` 进来)报 `502 did not stream: token无效`,而 provider 显示已登录 | auth 里没带 org → `accountOf()` 落 `"default"` → 读不到真实账号的 key 槽 → 只能用 `auth.access`,它过了 2 小时就 403。`accountFor()` 已在**恰好一个真实账号**时兜到它;**多账号不猜**(猜错就是拿别人的 key 扣别人的积分) | internals 十四 · 十五 · `tests/account-fallback.mjs` |
|
|
70
|
+
|
|
71
|
+
**坑 14 的两个附带判据**(都实测过):
|
|
72
|
+
|
|
73
|
+
- **改了 `src/hooks/usage.mjs` 要重启 magpie 才生效** —— 插件跑在 `magpie-bun` 子进程里,
|
|
74
|
+
模块缓存不随文件变化失效。改完先 `Stop-Process` 再 `Start-Process` 同一个 exe,
|
|
75
|
+
然后 `curl /v1/magpie/quotas` 看窗口里的 `spans`:`[{}]` = 没标 aside,`[{"aside":true}]` = 还标着。
|
|
76
|
+
- **额度读取有 1 分钟缓存**,改完立刻看可能还是旧值。
|
|
46
77
|
|
|
47
78
|
补充一条最容易误信的旧说法:
|
|
48
79
|
|
|
49
80
|
| 症状 | 判据 | 出处 |
|
|
50
81
|
|---|---|---|
|
|
51
82
|
| 以为 checkin 会「顺带续 key」 | **从来没有**。`src/hooks/checkin.mjs` → `src/tasks.mjs` 全链路没 import 过 `ensureKey`;保活由 magpie 按 `refreshLead` 主动调 `auth.refresh` | internals 十四 · CHANGELOG 0.x |
|
|
83
|
+
| 以为「签一把顶一把」、`key-store` 里的 key 会「时间没到就被顶掉」 | **已实测推翻(2026-10-28)**:同账号两把相隔 67 秒签的 key 同时有效;12 把 key 逐把探活,403 **全部**对应 `expiresAt` 已过(剩 -173~-541 分),没有一个「时间没到就失效」的样本。仍只留一把、只走唯一出口的理由是「本地别分叉」,不是「互相吊销」。据此加「跨账号挑活 key」的兜底 = 把别人的 key 交出去 | internals 十四 · 十五 |
|
|
52
84
|
|
|
53
85
|
---
|
|
54
86
|
|
|
@@ -102,7 +134,7 @@ git tag v0.3.16 && git push origin v0.3.16
|
|
|
102
134
|
| 文件 | 管什么 | 什么时候读 |
|
|
103
135
|
|---|---|---|
|
|
104
136
|
| [README](./README.md) | 用户看的门面:功能、模型、安装 | 改用户可见行为时 |
|
|
105
|
-
| [docs/internals.md](./docs/internals.md) |
|
|
137
|
+
| [docs/internals.md](./docs/internals.md) | 17 章实测:两站同构、三层凭证、接口清单、协议要点、工具 schema 兼容 | 改代码前按章查 |
|
|
106
138
|
| [CHANGELOG.md](./CHANGELOG.md) | 每版一节 | 写新版本时 |
|
|
107
139
|
| [CHECKLIST.md](./CHECKLIST.md) | 两张勾选表 | 两个交付门 |
|
|
108
140
|
| 脚手架 [AGENTS.md](../magpie-plugin-scaffold/AGENTS.md) | 通用坑(九项清单、三条铁律、check 能查什么) | 第一次接触这个插件族时 |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,285 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.3.23 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
**修工具 schema 兼容 + 网关路径拿错 key,顺手把「签一把顶一把」测清楚了**
|
|
6
|
+
|
|
7
|
+
### 一、上游不认 `additionalProperties: {}`
|
|
8
|
+
|
|
9
|
+
接 ZCode / Claude Code 系客户端时,一次带 54 个工具定义的请求必回
|
|
10
|
+
`400 {"error":{"message":"模型响应失败","code":4017}}`。根因是这些客户端把
|
|
11
|
+
"允许任意属性"写成空对象,而上游适配层只认布尔值。
|
|
12
|
+
|
|
13
|
+
同一把 key、同一份请求、只改这一个字段(2026-10-28 实测,glm-5.3-flash 与
|
|
14
|
+
deepseek-v4.1-flash **逐行一致**):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
additionalProperties: 缺省 → 200
|
|
18
|
+
additionalProperties: false → 200
|
|
19
|
+
additionalProperties: true → 200
|
|
20
|
+
additionalProperties: {} → 400 code 4017
|
|
21
|
+
additionalProperties: {type:"string"} → 400 code 4017
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**上游不是「只认布尔」,是根本不支持 schema 形态** —— 只处理空对象不够。这一点是拿
|
|
25
|
+
改动端到端跑一遍才发现的:对照组 400 → 只改空对象**仍然 400** → 非布尔全换 true 才 200。
|
|
26
|
+
|
|
27
|
+
`rewriteRequestBody` 里加递归规范化,把**所有非布尔值**换成 `true`。
|
|
28
|
+
`{}` → `true` 是严格等价;`{type:"string"}` → `true` 是**降级放宽**、丢了约束,
|
|
29
|
+
但上游无法表达它,留原值就是整个请求失败。方向安全(放宽不是收紧)。
|
|
30
|
+
|
|
31
|
+
请求体三处改写(max_tokens / stream_options / additionalProperties)合并成**一次**
|
|
32
|
+
`JSON.parse`,没改到时返回原始字符串(54 个工具的请求体可达上百 KB)。
|
|
33
|
+
|
|
34
|
+
> 这处改动来自社区 PR #8(@baijiu666),按其最小复现验证后采纳。
|
|
35
|
+
|
|
36
|
+
### 二、网关路径下拿不到正确账号的 key
|
|
37
|
+
|
|
38
|
+
agent 从 magpie 的 `/v1/chat/completions` 进来时,`getAuth()` 返回的 auth
|
|
39
|
+
**可能不带 org**,`accountOf()` 于是落到 `"default"` —— 读不到真实账号的 key 槽,
|
|
40
|
+
`ensureKey` 只能退回 `auth.access`;它过了 2 小时就 403,表现成
|
|
41
|
+
`502 {"message":"超级麦吉 did not stream: token无效"}`。
|
|
42
|
+
|
|
43
|
+
新增 `accountFor()`:拿不到标识时,**只有该站点恰好一个真实账号**才退到它。
|
|
44
|
+
0 个沿用原来的「首次接管」;**≥2 个不猜** —— 用错账号的 key 是拿别人的积分、
|
|
45
|
+
别人的组织出话,本地看不出来,不是报错是错账。
|
|
46
|
+
|
|
47
|
+
⚠ 多账号 + 网关路径这个组合仍然无解:需要 magpie 把 provider 实例 id 传给插件
|
|
48
|
+
(`plugin-auth.json` 里已经有 `magicrew#p4nwa7` 这种 key),插件侧拿不到。
|
|
49
|
+
在那之前只能保守 —— 宁可这次请求失败,也不借身份。
|
|
50
|
+
|
|
51
|
+
### 三、「签一把顶一把」实测推翻(不涉及代码,只更正文档)
|
|
52
|
+
|
|
53
|
+
`docs/internals.md` 第十四节一直挂着「要判定需要一次受控实验」。做完了:
|
|
54
|
+
本机 12 把 key 逐把打 `GET /v1/models`(只读、不签发、不吊销),结果是
|
|
55
|
+
|
|
56
|
+
- 同账号两把相隔 **67 秒**签发的 key **同时有效**
|
|
57
|
+
- 所有 403 都对应 `expiresAt` 已过(剩 -173~-541 分),**没有**「时间没到就失效」的样本
|
|
58
|
+
|
|
59
|
+
⇒ 后签的**不吊销**先签的;唯一失效条件是 2 小时到期。`AGENTS.md` 坑 3 与
|
|
60
|
+
internals 十三节据此更正。「只留一把、只走唯一出口」的设计保留,理由改为
|
|
61
|
+
「本地别分叉」而不是「互相吊销」。
|
|
62
|
+
|
|
63
|
+
### 测试
|
|
64
|
+
|
|
65
|
+
- `tests/body-rewrite.mjs` —— 20 个断言:只换空对象、非空 schema 不动、
|
|
66
|
+
其余字段一字不动、没改就不重新序列化
|
|
67
|
+
- `tests/account-fallback.mjs` —— 10 个断言:单账号才兜、多账号不猜、空壳文件不算账号
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 0.3.22 — 2026-10-28
|
|
72
|
+
|
|
73
|
+
**余额窗口保留「今日 / 限时 / 永久」拆分展示**
|
|
74
|
+
|
|
75
|
+
0.3.21 只发一条总余额,路由对了,但看不出每日 23:59 到期的额度。现在在
|
|
76
|
+
`积分余额` 余额窗口之后追加三条只展示的 `aside` 文本行:
|
|
77
|
+
|
|
78
|
+
- `今日积分`:`185 / 200 积分 · 今日 23:59 到期`
|
|
79
|
+
- `限时积分`:剩余限时批次 + 最早到期日
|
|
80
|
+
- `永久积分`:不过期部分
|
|
81
|
+
|
|
82
|
+
它们不带 `used` / `limit` / `balance`,宿主把它们当 `aside` 文本,不参与
|
|
83
|
+
`Allowance.Left`,路由仍只按 `积分余额` 排序;总余额不会被拆分窗口压低。
|
|
84
|
+
|
|
85
|
+
## 0.3.21 — 2026-10-28
|
|
86
|
+
|
|
87
|
+
**积分改为 magpie 的「余额型窗口」:不再编造百分比**
|
|
88
|
+
|
|
89
|
+
magpie 0.1.1790(issue #3950)起支持 `usage.windows[].balance`:只报余额和单位,
|
|
90
|
+
不写 `used` / `limit` / `aside`。本版把原来「今日 / 限时 / 永久」三条假百分比窗口
|
|
91
|
+
合并成一条 `积分余额`:
|
|
92
|
+
|
|
93
|
+
- `balance = total_points`、`unit = "积分"`;
|
|
94
|
+
- `runsOutAt` / `runsOut` 取最早过期的一批,让 smart 路由知道谁先失效;
|
|
95
|
+
- 余额 0 也报 `balance: 0`,由宿主判定为用完,不再合成 `used: 100`。
|
|
96
|
+
|
|
97
|
+
宿主现在按同 provider、同单位的余额从多到少排;不再显示「总量 = 余额」的
|
|
98
|
+
100% 假进度条。旧版三窗口写法是宿主支持余额前的过渡,已删除。
|
|
99
|
+
|
|
100
|
+
## 0.3.20 — 2026-10-28
|
|
101
|
+
|
|
102
|
+
**积分按「什么时候失效」拆成三块:今日 / 限时 / 永久**
|
|
103
|
+
|
|
104
|
+
0.3.19 把整池积分压成一条窗口,百分比是「总量 = 余额」的恒等式,没有信息量。
|
|
105
|
+
现在拆成三块(数据都来自 `/api/v1/quota/points/total`):
|
|
106
|
+
|
|
107
|
+
| 窗口 | 是什么 | 百分比 | 参与路由 |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| **今日积分** | 今天 23:59:59 到期的批次(每日登录赠送,国内 200 / 国际 50) | **真的**(分母是每日额度) | 是 |
|
|
110
|
+
| **限时积分** | 其余会过期批次的剩余之和 | 无(服务端不给原始额度) | 是(带 `runsOutAt`) |
|
|
111
|
+
| **永久积分** | `total - Σ限时`,不过期 | — | 否(`aside` 只展示) |
|
|
112
|
+
|
|
113
|
+
三块加起来正好等于 `total_points`,实测四个号都对得上。
|
|
114
|
+
|
|
115
|
+
**为什么「限时积分」条目不标 `aside`**:标了会被 `allowanceOf()` 整条跳过,
|
|
116
|
+
`runsOutAt` 就进不了路由 —— 这个号又会变回「未知」。所以它给 `used: 0` +
|
|
117
|
+
`limit = 当前余量`,让 magpie 收下它并读到 `runsOutAt`。百分比(恒 100%)无意义,
|
|
118
|
+
但路由能按「谁的积分先过期」排。
|
|
119
|
+
|
|
120
|
+
**关于「7 个依次递减的到期时间」**:`runsOutAt` 是单值字段(`*time.Time`),
|
|
121
|
+
语义就是「最早失效的那一批」,所以 7 个批次也只会出现一条提醒,不会堆成 7 条。
|
|
122
|
+
magpie 自己的 WorkBuddy 插件就是这么用的。magicrew 的每日登录积分是
|
|
123
|
+
「当天 23:59:59 到期」(`expire_type: 1`,不累积),更不存在滚动多批。
|
|
124
|
+
|
|
125
|
+
**为什么还是要给 magpie 提 issue**:magpie 的契约把「有进度条」和「参与路由」
|
|
126
|
+
焊在同一个窗口上 —— 要进度条就得给 `used`+`limit`,钱包没有分母就只能编;
|
|
127
|
+
不编就判成纯文本 + `aside`,路由又当未知。三条不可能同时满足。
|
|
128
|
+
真正的解是宿主支持「余额型窗口」:有读数、能参与路由,但不参与百分比。
|
|
129
|
+
|
|
130
|
+
## 0.3.19 — 2026-10-28
|
|
131
|
+
|
|
132
|
+
**路由页不再说「剩余额度未知」:窗口不能标 `aside`**
|
|
133
|
+
|
|
134
|
+
0.3.18 把积分窗口改成 `aside: true` + 只给 `display` —— 卡片好了,
|
|
135
|
+
**路由页还是「剩余额度未知」**。
|
|
136
|
+
|
|
137
|
+
根因在 magpie 宿主侧,读源码可证:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
provider/routing.go allowanceOf() if (w.Aside || w.Text) { continue } ← 整条跳过
|
|
141
|
+
provider/routing.go routedAllowance() if len(a) == 0 { return nil, false } ← 一条不剩 ⇒ 不 ok
|
|
142
|
+
gateway/trace.go weighed() if l, ok := wg.lefts[key]; ok { w.Known = true }
|
|
143
|
+
routing/client.js w.known 为假且 kind == "account" ⇒ 「剩余额度未知」
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
官方文档也逐字写着(`docs/subsystems/provider-plugins.md`,issue #3650):
|
|
147
|
+
|
|
148
|
+
> An account whose windows all tell no share has no allowance, so routing counts
|
|
149
|
+
> it not known ... as one never read. A window that says `used: 0` is a reading
|
|
150
|
+
> at 0, as before.
|
|
151
|
+
|
|
152
|
+
⇒ **`aside` 的代价不是「不参与排序」,是这个号在路由里整个消失**
|
|
153
|
+
(unknown 一律排在已知的之后)。
|
|
154
|
+
|
|
155
|
+
现在改成钱包的恒等式:`limit = 余额`、`amount = 0`、`used = 0`,**不加 `aside`**。
|
|
156
|
+
`used` 仍然是「已用 0」而不是缺省 —— 缺省会被 magpie 判成「没有份额的文本行」,
|
|
157
|
+
又回到未知。
|
|
158
|
+
|
|
159
|
+
判据(2026-10-28 实测,两站四个号):
|
|
160
|
+
|
|
161
|
+
| | 修复前 | 修复后 |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `/v1/magpie/trace` 的 `known` | **字段缺失** | `true`,`limit` = 余额 |
|
|
164
|
+
| 路由页那一行 | 剩余额度未知 | `1,363 / 1,363 积分 · 剩余 100%` |
|
|
165
|
+
|
|
166
|
+
A/B 都跑过:把 `aside: true` 加回去、重启 magpie,`known` 立刻变回缺失;
|
|
167
|
+
换回来就恢复 `true`。
|
|
168
|
+
|
|
169
|
+
⚠ 代价说清楚:这样算出来的百分比**没有信息量**(分母就是分子)——
|
|
170
|
+
magpie 的额度契约是为「周期配额」设计的,钱包型额度它没有专门表达。
|
|
171
|
+
真正该修的是宿主侧,插件侧只能给出最接近的形态。
|
|
172
|
+
|
|
173
|
+
## 0.3.18 — 2026-10-28
|
|
174
|
+
|
|
175
|
+
- **AGENTS 新增「零、magpie 升级时:只看稳定版」** —— magpie 有「更新渠道:每个版本 / 稳定版」,用户默认跑稳定版。两渠道实测差 **400 多个版本**(每版本 0.1.1763 / 稳定版 0.1.1327),但插件相关的契约字段**一条不差** ⇒ 追每版本几乎全是白跑。判断依据只有一条:**稳定版版本号变了**才需要重新同步
|
|
176
|
+
|
|
177
|
+
## 0.3.17
|
|
178
|
+
|
|
179
|
+
**不用开浏览器也能登录了 —— 手机号 + 短信验证码,纯 HTTP 走完**
|
|
180
|
+
|
|
181
|
+
那个卡了很久的「拖动滑块使图片角度为正」现在能自动过:用 magpie 设置里你自己选的
|
|
182
|
+
识图模型(`settings.json` 的 `vision`)判断旋转角度,然后纯 HTTP 把整条链走完。
|
|
183
|
+
|
|
184
|
+
### 之前为什么做不了,以及真相
|
|
185
|
+
|
|
186
|
+
旧文档(internals 第九节)写着「magicrew 没有这条路」。**那是错的,原因是找错了域名。**
|
|
187
|
+
登录用的用户中心不在 `i-magic-service.letsmagic.cn`,而在另一套 keewood 服务上:
|
|
188
|
+
|
|
189
|
+
| 站点 | 用户中心 |
|
|
190
|
+
|---|---|
|
|
191
|
+
| 国内 letsmagic.cn | `https://i-keewood-service.teamshare.cn` |
|
|
192
|
+
| 国际 magicrew.ai | `https://keewood-service.magicrew.ai`(**没有 `i-` 前缀**) |
|
|
193
|
+
|
|
194
|
+
换成这个域名,整条链一个 404 都没有。
|
|
195
|
+
|
|
196
|
+
### 五步链路(全部实测)
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
POST /captcha/generate {account_type:"mobile", mobile, state_code}
|
|
200
|
+
→ {token, image(data:image/webp;base64,…), expire_time} ← 纯 JSON,无 cookie、无指纹
|
|
201
|
+
POST /captcha/verify {token, angle, account_type, mobile, state_code}
|
|
202
|
+
→ {verified:true, business_token:"biz_…"}
|
|
203
|
+
POST /v4/user/send-sms {type:"account_login", phone, state_code}
|
|
204
|
+
+ header X-Captcha-Token: <business_token> ← 这个 type 填错会炸
|
|
205
|
+
POST /v4/user/login {type:"phone_captcha", phone, state_code, code, device:{id,name}}
|
|
206
|
+
→ jwt + 组织
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**`angle` 不是一个裸角度,是加密过的整数**,前端那么算:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
l = deg, u = n (n = Date.now()%90000+10000)
|
|
213
|
+
d = 52249517 × (l + u) + 5976906165
|
|
214
|
+
angle = String(d & 0xFFFFFFFF) + String(n)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`deg` 就是「图片要顺时针转多少度」,**1 px = 1.6°,满行程 225 px = 360°**。
|
|
218
|
+
验证方式:抓浏览器真实提交的 4 组 angle,对每组正向穷举 0…359 **都唯一命中**,
|
|
219
|
+
命中值与拖动像素严格成 1.6 倍;另用图像互相关独立验算,拖 100px 前后帧相差 160°±0.25°。
|
|
220
|
+
|
|
221
|
+
### 识图判角度:问钟点,不问角度
|
|
222
|
+
|
|
223
|
+
**不要问模型「这图歪了多少度」** —— 实测填空 6 次错 5 次。改问
|
|
224
|
+
「主体自己的『上方向』指向钟面的几点」(12=正上、3=正右),再 `deg = (12 - clock) × 30`。
|
|
225
|
+
|
|
226
|
+
而且**必须问两遍比分歧**:实测的失败样本是两个判官给出 1.7 和 7.5 钟点(相差 174°),
|
|
227
|
+
其中一个把方向看反了 —— 这种情况取平均毫无意义,只能换图重来。
|
|
228
|
+
分歧阈值 1.5 钟点(45°)。
|
|
229
|
+
|
|
230
|
+
服务端**容差实测 ≥ ±10°**(正确答案 120°,故意提交 130° 仍然通过),所以钟点法的
|
|
231
|
+
精度是够的:24 格 15° 网格的那点误差根本不影响。
|
|
232
|
+
|
|
233
|
+
⚠ `Bun.Image.rotate()` **只支持 90° 的倍数**(`rotate: only multiples of 90 are supported`),
|
|
234
|
+
`bytes()`/`buffer()` 给的是编码后的 PNG、拿不到原始像素 —— 所以「旋转 24 格网格」
|
|
235
|
+
这条路在插件里走不通,才改用钟点法。
|
|
236
|
+
|
|
237
|
+
### 新增
|
|
238
|
+
|
|
239
|
+
- `src/captcha.mjs` —— 取图 / 角度加密 / 验证 / 发短信 / 验证码换登录态 / 三站域名映射
|
|
240
|
+
- `src/vision.mjs` —— 读 `settings.json` 的 `vision`、找 magpie 自己的网关
|
|
241
|
+
(默认 `127.0.0.1:3425`,`MAGPIE_GATEWAY_PORT` 可覆盖)、webp→png、钟点双问与分歧判定
|
|
242
|
+
- `src/headless.mjs` —— 编排:最多换 4 张图、每张只提交一次(风控不允许反复试同一 token)、
|
|
243
|
+
失败把每轮原因带出来
|
|
244
|
+
- 登录方法「手机号验证码」—— `prompts` 问手机号,`authorize` 里过验证并发短信,
|
|
245
|
+
然后 `method: "code"` 等用户填 6 位验证码,`callback(code)` 换登录态并建号
|
|
246
|
+
|
|
247
|
+
### 两个坑,都是实测撞出来的
|
|
248
|
+
|
|
249
|
+
1. **`send-sms` 的 `type` 必须是 `"account_login"`**。填 `"login"` / `"phone_captcha"`
|
|
250
|
+
/ `"sms_login"` 全部返回 `9999 服务异常,请联系管理员` —— 后端内部炸了,不是参数校验,
|
|
251
|
+
所以从错误信息上完全看不出是 type 的问题。这个值是从浏览器真实请求里抓出来的。
|
|
252
|
+
2. **`device` 是必填且结构要试**:缺 → 9999;给字符串 → 9999;
|
|
253
|
+
`{device_id}` → 「id 字段是必须的」;`{id, name}` → 放行。
|
|
254
|
+
⇒ `{id: 64 位 hex, name: "web"}`。
|
|
255
|
+
|
|
256
|
+
### 另外两个更贵的修正(第一版发出去就踩了)
|
|
257
|
+
|
|
258
|
+
1. **`opened` 必须是 `true`。** 这条链路全在后台走完,不需要任何浏览器;写成 `false`
|
|
259
|
+
会让 magpie 拿 `url` 去开**系统默认浏览器** —— 而那个浏览器里往往本来就登着这个站,
|
|
260
|
+
用户看到的是已登录界面,进不了「添加新号」,还以为插件把他登进了旧账号。
|
|
261
|
+
这正是浏览器登录那条路修过的坑,这次在新方法里又踩了一遍。
|
|
262
|
+
2. **新号注册要带 `auto_register: true` 和 `redirect`,而邀请码是靠 `redirect` 传的。**
|
|
263
|
+
网页表单的初始值就是
|
|
264
|
+
`{ phone, state_code, code, auto_register: true, type: "phone_captcha", redirect: window.location.href }` —
|
|
265
|
+
服务端从 `redirect` 上取 `?invitation=<码>`,`invite_code` 不是独立字段。
|
|
266
|
+
⇒ 无头这条路没有「打开带邀请码的页面」这一步,所以必须自己拼出带邀请码的
|
|
267
|
+
`redirect: ${web}/login?invitation=<码>`。少了它,新注册的号就没有邀请关系。
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
### 没配识图模型时
|
|
271
|
+
|
|
272
|
+
**不瞎猜。** 容差 ±10° 意味着瞎猜命中率只有约 5.6%(20/360),平均要试十几次,
|
|
273
|
+
先撞上的会是「验证提交过于频繁」。这种情况直接报错并指路:去 magpie 设置里配一个
|
|
274
|
+
识图模型,或者改用「浏览器登录」。
|
|
275
|
+
|
|
276
|
+
### ⚠ 未实测的一步
|
|
277
|
+
|
|
278
|
+
`/v4/user/login` 成功之后**响应里 jwt / 组织码的确切结构没验到** —— 手上没有能收短信的
|
|
279
|
+
号码,只能验到 `2168 验证码错误`(这已经证明参数结构对了)。所以那一步写成:
|
|
280
|
+
按 `eyJ` 三段式特征找 JWT、组织码找不着就用拿到的 JWT 调
|
|
281
|
+
`/api/v1/aggregation/login-data` 补;两条都拿不到时把原始响应带进错误信息。
|
|
282
|
+
|
|
3
283
|
## 0.3.16
|
|
4
284
|
|
|
5
285
|
**加了给 AI 执行者的入口 —— 因为「发布」这一步真的被漏过一次**
|
package/docs/internals.md
CHANGED
|
@@ -340,6 +340,29 @@ cat "<安装目录>/resources/app-update.yml"
|
|
|
340
340
|
|
|
341
341
|
## 九、为什么做不到「点一下全自动」
|
|
342
342
|
|
|
343
|
+
> ### ⚠ 更正(2026-10-28):这一节原来的结论是**错的**
|
|
344
|
+
>
|
|
345
|
+
> 原文断言「magicrew 没有这条路」,理由是「登录类型全是拿凭据换 token,没有设备码、
|
|
346
|
+
> 没有一次性 ticket」。**真正的错因是找错了域名** —— 登录用的用户中心不在
|
|
347
|
+
> `i-magic-service.letsmagic.cn`,而在 keewood:
|
|
348
|
+
>
|
|
349
|
+
> | 站点 | 用户中心 |
|
|
350
|
+
> |---|---|
|
|
351
|
+
> | 国内 | `https://i-keewood-service.teamshare.cn` |
|
|
352
|
+
> | 国际 | `https://keewood-service.magicrew.ai`(**没有 `i-` 前缀**) |
|
|
353
|
+
>
|
|
354
|
+
> 换成这个域名,整条链一个 404 都没有:`/captcha/generate` → `/captcha/verify` →
|
|
355
|
+
> `/v4/user/send-sms` → `/v4/user/login`,**全程纯 HTTP、不开浏览器**。
|
|
356
|
+
> 那个「拖动滑块使图片角度为正」的答案就是提交参数里的 `angle`(加密过的整数),
|
|
357
|
+
> 用 magpie 设置里的识图模型判「主体上方向指向钟面几点」就能算出来。
|
|
358
|
+
>
|
|
359
|
+
> 实现见 `src/captcha.mjs` / `src/vision.mjs` / `src/headless.mjs`,
|
|
360
|
+
> 以及登录方法「手机号验证码」。下面保留原文,只为了说明**当时为什么判断错了**。
|
|
361
|
+
|
|
362
|
+
下面的分析**在换对域名之前**成立,其中「① 13 种登录类型」和「③ 试过的三个端点」
|
|
363
|
+
仍然是事实,只是它们不能推出「没有这条路」——真正缺的那个域名当时没找到。
|
|
364
|
+
|
|
365
|
+
|
|
343
366
|
对比 OfficeAce:它能做到「浏览器登录完,magpie 窗口自动继续」,因为华为云 OAuth 的
|
|
344
367
|
`redirect_uri` 指向一个**可控的服务**(`${CLAW_BASE}/v1/claw/auth/callback`),
|
|
345
368
|
插件轮询那个服务取结果。magicrew 没有这条通道:
|
|
@@ -737,15 +760,62 @@ mgw_8f9a…(18:14 又一次续期换的) → 网关请求 HTTP 200 正
|
|
|
737
760
|
|
|
738
761
|
两把都活着。所以「一签就吊销」不成立。
|
|
739
762
|
|
|
740
|
-
那第十三节那次 `mgw_63d4…` 为什么 403
|
|
763
|
+
那第十三节那次 `mgw_63d4…` 为什么 403?当时只能说**不确定**,三种可能没分辨开:
|
|
741
764
|
|
|
742
765
|
1. 它真的过期了(2 小时),而本地 `expires` 字段显示还没到 —— 说明 `expires` 不可信
|
|
743
766
|
2. `PUT`(用 refresh_token 续期)会轮换/吊销旧的 `api_key`,而 `POST`(签发)不会
|
|
744
767
|
3. 别的吊销条件(重新登录、并发续期)
|
|
745
768
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
769
|
+
### ✅ 结论(2026-10-28 晚,受控实验做完了)
|
|
770
|
+
|
|
771
|
+
上面说「要判定需要一次受控实验」。实验做了,**答案落在第 1 条:只有「真的超过 2 小时」会 403,
|
|
772
|
+
「签一把顶一把」不成立,`expires` 也只是个理论值**。
|
|
773
|
+
|
|
774
|
+
做法:把本机**所有** key(magpie 的 `plugin-auth.json` 的 access + key-store 每个账号每个槽)
|
|
775
|
+
逐把打 `GET /v1/models`——只读、不签发、不吊销、不花钱。探针留在仓库外,
|
|
776
|
+
结论是这一张表(`剩余` = `expiresAt - now`,单位分钟):
|
|
777
|
+
|
|
778
|
+
```
|
|
779
|
+
来源 站点 账号 槽 剩余(分) 判定
|
|
780
|
+
magpie magicrew-intl 67u4oe6fbenj access 116 HTTP 200 MODELS=65
|
|
781
|
+
magpie magicrew 7fli52t2tc2b access 115 HTTP 200 MODELS=44
|
|
782
|
+
magpie magicrew 66p0q56qo47j access 115 HTTP 200 MODELS=44
|
|
783
|
+
magpie magicrew h97gkbu8bruj access 115 HTTP 200 MODELS=44
|
|
784
|
+
store magicrew-intl 67u4oe6fbenj a* 116 HTTP 200 MODELS=65
|
|
785
|
+
store magicrew-intl 67u4oe6fbenj b -181 code=403 "token无效"
|
|
786
|
+
store magicrew 66p0q56qo47j a* -227 code=403 "token无效"
|
|
787
|
+
store magicrew 66p0q56qo47j b -541 code=403 "token无效"
|
|
788
|
+
store magicrew 7fli52t2tc2b a* -173 code=403 "token无效"
|
|
789
|
+
store magicrew 7fli52t2tc2b b -286 code=403 "token无效"
|
|
790
|
+
store magicrew h97gkbu8bruj a* 117 HTTP 200 MODELS=44
|
|
791
|
+
store magicrew h97gkbu8bruj b -228 code=403 "token无效"
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
两条判据:
|
|
795
|
+
|
|
796
|
+
**① 403 与「剩余分钟数」完全一致** —— 每一个 403 的剩余都是负数(-173 到 -541),
|
|
797
|
+
每一个 200 的剩余都是正数。**没有一个「时间没到却不认」的样本。**
|
|
798
|
+
所以「本地时间没到但服务端已经吊销」这个状态,本次实测里根本不存在。
|
|
799
|
+
|
|
800
|
+
**② 同一账号同时有两把活 key,签发时刻相差 67 秒:**
|
|
801
|
+
|
|
802
|
+
```
|
|
803
|
+
magpie access expires 2026-10-28T14:39:56.898Z → 200
|
|
804
|
+
store 槽 a expires 2026-10-28T14:41:03.000Z → 200
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
两把都是 2 小时有效期,也就是一把签于 12:39:56、另一把签于 12:41:03 —— **后签的没有顶掉先签的**。
|
|
808
|
+
|
|
809
|
+
⇒ **一条账号一把活 key 这个说法彻底作废**。`POST` 与 `PUT` 都不吊销已签出的 key,
|
|
810
|
+
唯一的失效条件是 2 小时到期。
|
|
811
|
+
|
|
812
|
+
### 那这条为什么必须写下来
|
|
813
|
+
|
|
814
|
+
2026-10-28 的 PR #8 就是被「时间没到但已被顶掉」这个前提带偏的:它据此把
|
|
815
|
+
`ensureKey` 改成「优先用 `auth.access`」,并加了一个跨账号挑活 key 的兜底。
|
|
816
|
+
前提不成立 ⇒ 改动没有依据;而那个兜底会**把别的账号的 key 交出去**
|
|
817
|
+
(实测:auth 无 org 时 PR 分支返回了 `h97gkbu8bruj` 账号的 key,
|
|
818
|
+
而 main 返回调用方自己的)。教训不是「多加一层兜底」,是**先把前提测了再改代码**。
|
|
749
819
|
|
|
750
820
|
### 保活的正确说法
|
|
751
821
|
|
|
@@ -982,3 +1052,84 @@ GET /v1/models → id / object / created_at / owner_by
|
|
|
982
1052
|
|
|
983
1053
|
magicrew 既不返回 `maxInputTokens`(`flows/models` 的 `configs` 只有 temperature),
|
|
984
1054
|
`/v1/models` 也只有 `id / object / created_at / owner_by` —— 所以走模型库这条路。
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## 十七、上游不认 `additionalProperties: {}` —— 工具 schema 要在请求里兜住
|
|
1059
|
+
|
|
1060
|
+
**症状**:接 ZCode / Claude Code 系客户端时,一次带 54 个工具定义的请求必定失败:
|
|
1061
|
+
|
|
1062
|
+
```
|
|
1063
|
+
POST /v1/chat/completions
|
|
1064
|
+
→ HTTP 400 {"error":{"message":"模型响应失败","code":4017","request_id":"..."}}
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
换成简单请求(一两个工具)就正常 —— 所以看起来像"某些模型不行"或"工具太多"。
|
|
1068
|
+
|
|
1069
|
+
### 根因
|
|
1070
|
+
|
|
1071
|
+
这些客户端生成工具 schema 时,会把"允许任意额外属性"写成**空对象**:
|
|
1072
|
+
|
|
1073
|
+
```json
|
|
1074
|
+
"parameters": { "type": "object", "properties": {...}, "additionalProperties": {} }
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
按 JSON Schema(draft-07)这是合法的 —— `additionalProperties` 的值是 schema 时,
|
|
1078
|
+
`{}` 表示"任意,不加约束",与 `true` 语义等价。但**上游的适配层只认布尔值**,
|
|
1079
|
+
收到一个对象就整个请求拒掉,还只回一句"模型响应失败",不说是哪个字段。
|
|
1080
|
+
|
|
1081
|
+
### 最小复现(2026-10-28,同一把 key、同一份请求、只改这一个字段)
|
|
1082
|
+
|
|
1083
|
+
| `additionalProperties` | glm-5.3-flash | deepseek-v4.1-flash |
|
|
1084
|
+
|---|---|---|
|
|
1085
|
+
| 缺省 | 200 | 200 |
|
|
1086
|
+
| `false` | 200 | 200 |
|
|
1087
|
+
| `true` | 200 | 200 |
|
|
1088
|
+
| `{}` | **400 code 4017** | **400 code 4017** |
|
|
1089
|
+
| `{type:"string"}` | **400 code 4017** | **400 code 4017** |
|
|
1090
|
+
| `{type:"string",description:"x"}` | **400 code 4017** | **400 code 4017** |
|
|
1091
|
+
|
|
1092
|
+
**两个模型逐行一致。结论比「上游只认布尔」更狠:它根本不支持 schema 形态** ——
|
|
1093
|
+
不只是空对象,连 `{type:"string"}` 这种有约束的 schema 也一律拒。这一点很关键:
|
|
1094
|
+
只处理空对象的改写在真实请求里仍会 400(实测过,见下方「修法」)。
|
|
1095
|
+
|
|
1096
|
+
### 修法:请求改写里做等价替换
|
|
1097
|
+
|
|
1098
|
+
`src/hooks/loader.mjs` 的 `rewriteRequestBody` 里,`loosenAdditionalProperties()`
|
|
1099
|
+
递归扫 `tools[].function.parameters`,把**所有非布尔值**换成 `true`:
|
|
1100
|
+
|
|
1101
|
+
```js
|
|
1102
|
+
"additionalProperties": 缺省 → 不动 // 等价于 true
|
|
1103
|
+
"additionalProperties": true → 不动 // 上游认
|
|
1104
|
+
"additionalProperties": false → 不动 // 「不许额外属性」,换掉就是反向放开
|
|
1105
|
+
"additionalProperties": {} → true // 等价改写
|
|
1106
|
+
"additionalProperties": {type:"x"} → true // ⚠ 降级放宽,丢了约束
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
**为什么接受那个不等价**:`{}` → `true` 是严格等价(都表示「任意,不加约束」);
|
|
1110
|
+
`{type:"string"}` → `true` 丢掉了「额外属性必须是 string」这条约束。但上游无法表达它,
|
|
1111
|
+
保留原值就是整个请求失败。两害相权,让工具调用能跑(可能多出未声明的字段)
|
|
1112
|
+
比让 agent 完全不可用要好;方向也安全 —— 是放宽不是收紧,不会让原本合法的调用被拒。
|
|
1113
|
+
|
|
1114
|
+
它与 `max_tokens`、`stream_options` 三处改写共用**一次** `JSON.parse`;
|
|
1115
|
+
没改到时返回原始字符串(省一次序列化 —— 54 工具的请求体可达上百 KB)。
|
|
1116
|
+
|
|
1117
|
+
边界由 `tests/body-rewrite.mjs` 钉住(20 个断言):非布尔一律换、布尔一律不动、
|
|
1118
|
+
其余字段一字不动、没改到就不重新序列化。
|
|
1119
|
+
|
|
1120
|
+
### 为什么算插件的事,不算客户端的事
|
|
1121
|
+
|
|
1122
|
+
按规范 `{}` 与 `true` 等价,客户端没写错;**上游的兼容性不足**。
|
|
1123
|
+
但插件是唯一能改请求体的地方(见 `src/hooks/loader.mjs` 顶部),
|
|
1124
|
+
在这里兜住比让每个用户去改 ZCode 的源码现实。
|
|
1125
|
+
|
|
1126
|
+
### 同一处的第三个改写,以及它的教训
|
|
1127
|
+
|
|
1128
|
+
同一次排查里还确认了两件事,都值得记住:
|
|
1129
|
+
|
|
1130
|
+
1. **上游对"token 无效"回的是 HTTP 200**,body `{"code":403,"message":"token无效"}`,
|
|
1131
|
+
且 `error` 字段是空数组(falsy)。**只看 HTTP 状态码会把失败当成功** ——
|
|
1132
|
+
判断得用 `j.code !== 200`。排查时在这上面绕了很久。
|
|
1133
|
+
2. **插件宿主是运行时从 `~/.cache/magpie/bun/install-cache/` 加载的**,
|
|
1134
|
+
不在 `plugin-host/host-*.js` 里;magpie 更新插件后会换目录(目录名带 commit hash),
|
|
1135
|
+
要改当前实际加载的那个。
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
## TL;DR
|
|
2
|
+
|
|
3
|
+
A provider that sells **prepaid credit** — a wallet — cannot be described by the current usage contract:
|
|
4
|
+
|
|
5
|
+
- **With windows**, `used` must be a share of a periodic allowance. A wallet has no such denominator, so any `used` we send is fabricated.
|
|
6
|
+
- **Without windows**, `routedAllowance` returns not-ok, `weighed()` leaves `Known` unset, and the Routing page says 「剩余额度未知」 — the account ranks after every known one (#3650).
|
|
7
|
+
- **`aside: true` does not help**: `allowanceOf()` skips those windows whole, with the same result.
|
|
8
|
+
|
|
9
|
+
What is missing is a **balance-shaped reading**: "this much is left", in a unit, with no percentage. Two fields would carry it — `QuotaWindow.Balance` and `routes.Seat.Left`.
|
|
10
|
+
|
|
11
|
+
## The provider (real numbers, 2026-10-28)
|
|
12
|
+
|
|
13
|
+
SuperMagic (超级麦吉 / MagiCrew), community plugin `magpie-magicrew-auth`.
|
|
14
|
+
|
|
15
|
+
`GET /api/v1/quota/points/total` answers:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{"total_points": 6291,
|
|
19
|
+
"expiring_quota_details": [
|
|
20
|
+
{"remaining_points": 185, "expires_at": "2026-10-28 23:59:59"},
|
|
21
|
+
{"remaining_points": 100, "expires_at": "2026-11-26 16:45:13"},
|
|
22
|
+
{"remaining_points": 956, "expires_at": "2027-11-03 19:11:01"}],
|
|
23
|
+
"organization_limit_points": null,
|
|
24
|
+
"user_limit_points": null,
|
|
25
|
+
"next_cycle_grant": null}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- No total, no consumed: `*_limit_points` are `null` on personal accounts.
|
|
29
|
+
- The ledger endpoint `POST /api/v1/quota/points/records/queries` answers **HTTP 500** on both sites (five body shapes tried), so "how much was spent" is not readable either.
|
|
30
|
+
- The only fixed number is the daily login gift — **200 points/day (CN), 50 (intl)** — which expires at 23:59:59 the same day.
|
|
31
|
+
|
|
32
|
+
## What the plugin tried, and what magpie did
|
|
33
|
+
|
|
34
|
+
| plugin sends | magpie | result |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `aside: true, used: 0, display: "6,291 积分"` | `allowanceOf()` skips Aside/Text whole; none left → `routedAllowance` not ok → `weighed()` leaves `Known` unset | Routing page 「剩余额度未知」; the account ranks after every known one |
|
|
37
|
+
| `used: 0, display: "6,291 积分"` (no `limit`) | known, but routing's `quota()` shows the count only when `limit > 0` | row reads `0% used`, no figure |
|
|
38
|
+
| `used: 0, amount: 0, limit: 6291` | known, figure shown | percentage is `0 / 6291` — a tautology (denominator = remainder); the "fake share" of #3353 |
|
|
39
|
+
|
|
40
|
+
## What it costs the router
|
|
41
|
+
|
|
42
|
+
`routes.Seat` carries `Used float64` (a 0–100 share), `Soon []time.Time`, `Lapse`, `Pace`, … — **nothing that says how much is left**.
|
|
43
|
+
|
|
44
|
+
- **`pace`**: `(100 - used) / hours until resetsAt`. A wallet has no `resetsAt` and its `used` is always 0, so every account of this provider gets **the same pace**. 6,291 points and 750 points are indistinguishable.
|
|
45
|
+
- **`smart`**: the fine tier is ordered by `Soon` (`Allowance.Renewal`, which does fold `runsOutAt` in), then `Lapse` — it can say *whose points expire first*, never *who has more left*.
|
|
46
|
+
|
|
47
|
+
Measured on 4 accounts: 1,363 / 1,693 / 6,291 / 750 points; earliest expiries 178 / 29 / 29 / 372 days out. Under `smart` all four tie on `Soon` whenever the expiry days match; under `pace` they are **exactly** identical.
|
|
48
|
+
|
|
49
|
+
## Suggested shape
|
|
50
|
+
|
|
51
|
+
**`QuotaWindow`**
|
|
52
|
+
|
|
53
|
+
```go
|
|
54
|
+
// Balance is how much of this allowance is left, in Unit — a reading of a
|
|
55
|
+
// prepaid balance, with no share to tell (Used stays nil for it).
|
|
56
|
+
Balance *float64 `json:"balance,omitempty"`
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`allowanceOf` keeps such a window (`l.Balance = *w.Balance`), so `Known` is true, without inventing a `Used`.
|
|
60
|
+
|
|
61
|
+
**`routes.Seat`**
|
|
62
|
+
|
|
63
|
+
```go
|
|
64
|
+
// Left is the absolute remainder in the provider's own unit: comparable
|
|
65
|
+
// within one provider, 0 when the vendor doesn't say.
|
|
66
|
+
Left float64
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Filled from the windows' `Balance` (or the fullest window's `Amount`/`Of` when the vendor does count).
|
|
70
|
+
|
|
71
|
+
**Ordering**: `smart` / `pace` fall back to `Left` (most first) when shares are unknown or equal. A prepaid provider then becomes rankable by both dimensions that matter: *who runs out first* (`runsOutAt`) and *who has the most left* (`balance`).
|
|
72
|
+
|
|
73
|
+
## Today's plugin-side workaround (for reference)
|
|
74
|
+
|
|
75
|
+
Three windows, so the account is at least known and carries `runsOutAt`:
|
|
76
|
+
|
|
77
|
+
1. **今日积分** — today's batch, **real percentage** (denominator = the 200/50 daily gift), `resetsAt` = next midnight.
|
|
78
|
+
2. **限时积分** — the rest of the expiring batches, `used: 0`, `limit` = current remainder, `runsOutAt` = earliest.
|
|
79
|
+
3. **永久积分** — `total − Σexpiring`, `aside`, display only.
|
|
80
|
+
|
|
81
|
+
That is the best the current contract allows — and it still cannot express "how much".
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- #3428 — the seat knows *when* but not *how much* is lost (`w.RunsOut` dropped in `allowanceOf`). This issue is the same gap one layer up: the seat also cannot see *how much is left at all*.
|
|
86
|
+
- #3352 — a `Windows: []` reading with a balance is dropped entirely, so the account is unknown.
|
|
87
|
+
- #3353 — a window that never moves reads as `used: 0` and distorts smart ordering.
|
|
88
|
+
- #3650 — a window without `used` becomes text + aside, no share.
|
|
89
|
+
|
|
90
|
+
Happy to send a PR for the two fields if the shape looks right — I can test it against this provider directly.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magicrew-auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
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",
|