magpie-plugin-loomy-auth 0.1.7 → 0.1.8

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.
Files changed (4) hide show
  1. package/README.md +153 -144
  2. package/docs/internals.md +50 -24
  3. package/index.mjs +136 -58
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,144 +1,153 @@
1
- # Loomy for magpie
2
-
3
- **10 个模型 · 三种登录 · 三笔积分一分不落**
4
-
5
- 讯飞 Loomy 的编程订阅接进 magpie:对话、看图、工具调用、思考档位;
6
- 积分按 magpie 的调度规则如实上报,该领的新手任务和邀请奖励自动领。
7
-
8
- ```
9
- 今日积分 ████████░░ 4014 / 5000 ← 决定现在能不能用这个号
10
- 永久积分 5500 (附加) ← 不会过期
11
- 新手任务 可领 9500 (附加) ← 一次性,领完就没了
12
- ```
13
-
14
- ---
15
-
16
- ## 为什么选这个
17
-
18
- **积分算得准。** 空号显示 0,不会因为「看起来额度很足」被 magpie 优先派活;
19
- 有余额的号按真实消耗排队。这一栏是 `used` 决定的路由输入,报错一个数字,
20
- 整个路由顺序就跟着错。
21
-
22
- **多领两笔钱。**
23
- - **新手任务 10,000 分** —— 卡片上直接显示「可领 9500 积分(7 项待完成)」,签到后自动到账
24
- - **邀请码 5,000 分** —— 添加新账号时自动带本机老账号的邀请码,**双方各得 5000**
25
-
26
- **不用重新登录。** 凭据格式与旧插件相通,装上就能用,登录态原样带过来。
27
-
28
- **免费模型标得对。** `spark-x2.6` / `spark-x` 是 0 倍率(限时免费),
29
- 插件如实上报 `free`,路由组用「免费优先」时它们会被真正排在前面。
30
-
31
- ---
32
-
33
- ## 模型
34
-
35
- 10 个对话模型,全部实测(非文档抄录):
36
-
37
- | 模型 | 上下文 | 最大输出 | 看图 | 倍率 |
38
- |---|---|---|---|---|
39
- | `DeepSeek-V4.1-Flash` | 1M | 384K | ✓ | x2.0 |
40
- | `MiniMax-M3` | 1M | 512K | ✓ | x4.0 |
41
- | `GLM-5.3-Flash` | 1M | 128K | ✓ | x0.8 |
42
- | `qwen3.8-flash` | 1M | 128K | ✓ | x0.8 |
43
- | `mimo-v2.5` | 1M | 128K | ✓ | x3.3 |
44
- | `qwen-3.8-max` | 1M | 64K | ✗ | x12.0 |
45
- | `Kimi-k2.6` | 256K | 64K | ✓ | x6.5 |
46
- | `spark-x2.6` | 1M | 128K | ✗ | **免费** |
47
- | `spark-x` | 1M | 64K | ✗ | **免费** |
48
- | `auto` | — | — | ✗ | 自动选 |
49
-
50
- - **思考档位** `none / low / medium / high / xhigh`,默认 `low`,在 magpie 里直接选
51
- - **看图** 支持视觉的模型可直接发图;deepseek 系要求特殊格式,插件自动转换
52
- - **工具调用** 全部支持
53
-
54
- ---
55
-
56
- ## 供应商
57
-
58
- ### 登录
59
-
60
- | 方式 | 适用 |
61
- |---|---|
62
- | **桌面端导入**(推荐) | 本机装着 Loomy 客户端并已登录 —— 点一下,不收短信 |
63
- | **手机号 + 短信** | 没装客户端 |
64
- | **粘贴 session** | 想手动导 |
65
-
66
- 会话固定 **14 天**,Loomy 没有续期接口,到期后需重新登录一次。
67
- 插件不会假装能自动续 —— 否则 magpie 会一直以为可用,你反而等不到「请重新登录」的提示。
68
-
69
- ### 三笔积分
70
-
71
- | | 怎么来 | 影响路由吗 |
72
- |---|---|---|
73
- | **今日积分** | 每天 5000,北京时间 0 点重置,用掉不回补 | **是** —— 只有它决定现在能不能用这个号 |
74
- | **永久积分** | 注册 + 新手任务,不会过期 | 否(附加) |
75
- | **新手任务** | 8 项任务合计 10,000 分,一次性 | 否(附加) |
76
-
77
- 为什么这么分:今日池每天清零,用光就是没了;永久积分不过期,不该因为它多就优先烧;
78
- 新手任务领一次就结束,更不能当常规额度。
79
-
80
- ### 签到
81
-
82
- magpie 按北京时间每天调一次,签到同时会刷新当天额度,并**自动领取新手任务里已完成的那几项**。
83
- 结果显示具体数字,不是含糊的成功/失败:
84
-
85
- ```
86
- 今日额度已刷新(3479/5000 积分)· 新手任务 +500 分
87
- ```
88
-
89
- ### 加第二个账号
90
-
91
- Loomy 的邀请机制:**邀请好友双方各得 5000 积分**。
92
-
93
- 添加新账号时,插件会自动从本机已有账号里挑一个邀请码带上去 ——
94
- 用没用过的、还有余量的、用得最少的那个。你只要正常「添加账号」,那 5000 分自己就到手。
95
-
96
- ---
97
-
98
- ## 安装
99
-
100
- 在 magpie 的 **插件 → 发现** 里搜 `Loomy`,点**安装**。
101
-
102
- ⚠ 先卸掉别的 Loomy 插件 —— 两个插件抢同一个 `loomy` 供应商会互相打架。
103
-
104
- 装完 `magpie plugin list` 应看到:
105
-
106
- ```
107
- magpie-plugin-loomy-auth on
108
- loomy (Loomy, 10 models) signed in as 189****8051
109
- ```
110
-
111
- ---
112
-
113
- ## 常见问题
114
-
115
- **积分显示 0,但官方 App 里还有?**
116
- ① 今日额度用光,等北京时间 0 点重置;② 那是**新手任务**,卡片上单独一行「可领 XXXX 积分」,签到后到账。
117
-
118
- **显示「登录已失效」?**
119
- 会话 14 天到期,重新登录一次即可。
120
-
121
- **会花我的积分吗?**
122
- 不会。插件只读余额、签到、领任务奖励。消耗积分的是你通过 magpie 发的对话请求。
123
-
124
- **能看到我的什么?**
125
- Loomy 登录态(存在 magpie 自己的凭据文件里)、积分/模型/任务读数。
126
- 直连 `loomyad.xunfei.cn` 与 `account.xfinfr.com`,不经任何第三方中转。
127
- 邀请码功能读同文件里其它 Loomy 账号的码,仅只读,不外发。
128
-
129
- **装完没反应?**
130
- `magpie plugin list` 确认只有一个 `loomy`。改完配置重启一次 magpie 最稳(登录态不丢)。
131
-
132
- ---
133
-
134
- ## 卸载
135
-
136
- `magpie plugin rm magpie-plugin-loomy-auth`
137
-
138
- 登录态留在 magpie 的凭据里,重装即恢复。
139
-
140
- ---
141
-
142
- ## 许可
143
-
144
- MIT。协议事实、实现判据、测试与本地安装见 [`docs/internals.md`](docs/internals.md)。
1
+ # Loomy for magpie
2
+
3
+ **10 个模型 · 三种登录 · 三笔积分一分不落**
4
+
5
+ 讯飞 Loomy 的编程订阅接进 magpie:对话、看图、工具调用、思考档位;
6
+ 积分按 magpie 的调度规则如实上报,该领的新手任务和邀请奖励自动领。
7
+
8
+ ```
9
+ 今日积分 ████████░░ 4014 / 5000 ← 决定现在能不能用这个号
10
+ 永久积分 5500 (附加) ← 不会过期
11
+ 新手任务 可领 9500 (附加) ← 一次性,领完就没了
12
+ ```
13
+
14
+ ---
15
+
16
+ ## 为什么选这个
17
+
18
+ **积分算得准。** 空号显示 0,不会因为「看起来额度很足」被 magpie 优先派活;
19
+ 有余额的号按真实消耗排队。这一栏是 `used` 决定的路由输入,报错一个数字,
20
+ 整个路由顺序就跟着错。
21
+
22
+ **每天签到,顺手把能领的都领了。**
23
+ - **新手任务 10,000 分** —— 卡片上显示「可领 9500 积分」,签到后自动到账
24
+ - **邀请码 5,000 分** —— 本机有别的账号时自动兑换,**双方各得 5000**(实测)
25
+
26
+ 三件事(刷新额度 / 领任务 / 兑换邀请码)全部幂等,都放在 magpie 每天一次
27
+ 的签到里做完,**你不需要点任何按钮**。
28
+
29
+ **不用重新登录。** 凭据格式与旧插件相通,装上就能用,登录态原样带过来。
30
+
31
+ **免费模型标得对。** `spark-x2.6` / `spark-x` 是 0 倍率(限时免费),
32
+ 插件如实上报 `free`,路由组用「免费优先」时它们会被真正排在前面。
33
+
34
+ ---
35
+
36
+ ## 模型
37
+
38
+ 10 个对话模型,全部实测(非文档抄录):
39
+
40
+ | 模型 | 上下文 | 最大输出 | 看图 | 倍率 |
41
+ |---|---|---|---|---|
42
+ | `DeepSeek-V4.1-Flash` | 1M | 384K | ✓ | x2.0 |
43
+ | `MiniMax-M3` | 1M | 512K | ✓ | x4.0 |
44
+ | `GLM-5.3-Flash` | 1M | 128K | ✓ | x0.8 |
45
+ | `qwen3.8-flash` | 1M | 128K | ✓ | x0.8 |
46
+ | `mimo-v2.5` | 1M | 128K | ✓ | x3.3 |
47
+ | `qwen-3.8-max` | 1M | 64K | ✗ | x12.0 |
48
+ | `Kimi-k2.6` | 256K | 64K | ✓ | x6.5 |
49
+ | `spark-x2.6` | 1M | 128K | ✗ | **免费** |
50
+ | `spark-x` | 1M | 64K | ✗ | **免费** |
51
+ | `auto` | — | — | ✗ | 自动选 |
52
+
53
+ - **思考档位** `none / low / medium / high / xhigh`,默认 `low`,在 magpie 里直接选
54
+ - **看图** 支持视觉的模型可直接发图;deepseek 系要求特殊格式,插件自动转换
55
+ - **工具调用** 全部支持
56
+
57
+ ---
58
+
59
+ ## 供应商
60
+
61
+ ### 登录
62
+
63
+ | 方式 | 适用 |
64
+ |---|---|
65
+ | **桌面端导入**(推荐) | 本机装着 Loomy 客户端并已登录 —— 点一下,不收短信 |
66
+ | **手机号 + 短信** | 没装客户端 |
67
+ | **粘贴 session** | 想手动导 |
68
+
69
+ 会话固定 **14 天**,Loomy 没有续期接口,到期后需重新登录一次。
70
+ 插件不会假装能自动续 —— 否则 magpie 会一直以为可用,你反而等不到「请重新登录」的提示。
71
+
72
+ ### 三笔积分
73
+
74
+ | | 怎么来 | 影响路由吗 |
75
+ |---|---|---|
76
+ | **今日积分** | 每天 5000,北京时间 0 点重置,用掉不回补 | **是** —— 只有它决定现在能不能用这个号 |
77
+ | **永久积分** | 注册 + 新手任务,不会过期 | 否(附加) |
78
+ | **新手任务** | 8 项任务合计 10,000 分,一次性 | 否(附加) |
79
+
80
+ 为什么这么分:今日池每天清零,用光就是没了;永久积分不过期,不该因为它多就优先烧;
81
+ 新手任务领一次就结束,更不能当常规额度。
82
+
83
+ ### 签到
84
+
85
+ magpie 按北京时间每天调一次,签到同时会刷新当天额度,并**自动领取新手任务里已完成的那几项**。
86
+ 结果显示具体数字,不是含糊的成功/失败:
87
+
88
+ ```
89
+ 今日额度已刷新(3479/5000 积分)· 新手任务 +500 分
90
+ ```
91
+
92
+ ### 加第二个账号
93
+
94
+ **不用填邀请码,签到自动完成。**
95
+
96
+ Loomy 的邀请奖励:**邀请好友双方各得 5000 积分**(实测,附流水在 `docs/internals.md`)。
97
+ 每个号能邀请 1000 人,每个号**只能兑换一次**别人的码。
98
+
99
+ 你只要照常把新账号登进 magpie,下一次签到时会自动:查本机其它账号的邀请码 →
100
+ 挑一个 → 兑换。新号 +5000,老号 +5000。
101
+
102
+ ⚠ **不支持互相邀请。** 本号的邀请码一旦被别人用过,就不再兑换别人的码 ——
103
+ 否则 A 邀请 B、B 再邀请 A,两个号各拿两笔,那是刷量。单向成链没问题。
104
+
105
+ ---
106
+
107
+ ## 安装
108
+
109
+ 在 magpie 的 **插件 → 发现** 里搜 `Loomy`,点**安装**。
110
+
111
+ ⚠ 先卸掉别的 Loomy 插件 —— 两个插件抢同一个 `loomy` 供应商会互相打架。
112
+
113
+ 装完 `magpie plugin list` 应看到:
114
+
115
+ ```
116
+ magpie-plugin-loomy-auth on
117
+ loomy (Loomy, 10 models) signed in as 189****8051
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 常见问题
123
+
124
+ **积分显示 0,但官方 App 里还有?**
125
+ ① 今日额度用光,等北京时间 0 点重置;② 那是**新手任务**,卡片上单独一行「可领 XXXX 积分」,签到后到账。
126
+
127
+ **显示「登录已失效」?**
128
+ 会话 14 天到期,重新登录一次即可。
129
+
130
+ **会花我的积分吗?**
131
+ 不会。插件只读余额、签到、领任务奖励。消耗积分的是你通过 magpie 发的对话请求。
132
+
133
+ **能看到我的什么?**
134
+ Loomy 登录态(存在 magpie 自己的凭据文件里)、积分/模型/任务读数。
135
+ 直连 `loomyad.xunfei.cn` 与 `account.xfinfr.com`,不经任何第三方中转。
136
+ 邀请码功能读同文件里其它 Loomy 账号的码,仅只读,不外发。
137
+
138
+ **装完没反应?**
139
+ `magpie plugin list` 确认只有一个 `loomy`。改完配置重启一次 magpie 最稳(登录态不丢)。
140
+
141
+ ---
142
+
143
+ ## 卸载
144
+
145
+ `magpie plugin rm magpie-plugin-loomy-auth`
146
+
147
+ 登录态留在 magpie 的凭据里,重装即恢复。
148
+
149
+ ---
150
+
151
+ ## 许可
152
+
153
+ MIT。协议事实、实现判据、测试与本地安装见 [`docs/internals.md`](docs/internals.md)。
package/docs/internals.md CHANGED
@@ -139,39 +139,65 @@ Loomy 还有第三笔额度 —— **新手任务奖励**,总计 10,000 分,
139
139
  > 实况:`181****0632` 余额 0,但还有 10,000 分新手任务可领。卡片会如实显示,
140
140
  > 不会因为「余额 0」就把这个人判死刑。
141
141
 
142
- ### 邀请码:新账号自动用老账号的邀请资格
142
+ ### 邀请码:签到时自动兑换(不在注册时)
143
143
 
144
- Loomy 的「分享有礼」——**邀请好友双方各得 5000 积分**(客户端常量 `nb = 5e3`;
145
- ⚠ 别和新手任务里 `share_soul` 的 2000 混,那是两笔不同的奖励)。
144
+ **实测结论(2026-10-26,真号跑的)**:邀请双方**各得 5000**,不是 2000。
146
145
 
147
- 每个账号有**专属邀请码**(实测:`189****8051` → `E4DPT6`、`181****00632` → `FDW3CU`,
148
- 上限 1000 次、已用 0),并且**每个账号只能兑换一次**别人的码。
146
+ ```
147
+ 操作:181****0632 兑换 189****8051 的码 E4DPT6(走 bindInviteCode → POST /points/activation)
148
+
149
+ 189****8051 永久积分 14860 → 19860 (+5000) 流水:+5000 邀请好友奖励
150
+ 181****0632 永久积分 10000 → 15000 (+5000) 流水:+5000 填写邀请码奖励
151
+ E4DPT6 usedCount 0 → 1 181 activation false → true
152
+ ```
149
153
 
150
154
  | 端点 | 用途 |
151
155
  |---|---|
152
- | `GET /api/v1/invitation-codes` | 查自己的码(只读) |
153
- | `GET /api/v1/points/activation` | 本账号是否已用过别人的码(只读) |
154
- | `POST /api/v1/points/activation` | 绑定别人的码(一次性写) |
155
- | `POST /api/v1/points/first-login` | **也接受 `{inviteCode, deviceId}`** —— 首次初始化时捎上,不用事后再绑 |
156
-
157
- **这个插件怎么做的**:新账号登录时,自动从**本机其它已登录的 Loomy 账号**里挑一个
158
- 邀请码带上去。
159
-
160
- - 挑码规则:排除自己的、用完的(`usedCount >= maxUses`)、非 `active` 的;
161
- 优先**用得最少**的(把额度摊开);`maxUses <= 0` 视为不限次。
162
- - 先查 `activation`:**已用过就跳过**,不白发一次写请求(每账号只能用一次)。
163
- - 走 `first-login {inviteCode}`(首次初始化顺带绑定);那条没成再退回
164
- `points/activation`。**绑定失败不影响登录** —— 这是加分项。
165
- - 为什么能拿到别的账号的邀请码:`getAuth()` 只在当前账号作用域里可见
166
- (magpie 的 `inScope`),所以直接读 `~/.config/magpie/plugin-auth.json`,
167
- 把 `loomy` / `loomy#xxxxxx` 全捞出来逐个查(只读、不打印 session)。
156
+ | `GET /api/v1/invitation-codes` | 查自己的码(只读)。回 `{inviteCode, usedCount, maxUses, status}` |
157
+ | `GET /api/v1/points/activation` | 本号是否已兑换过;回 `{activated, appliedInvitationCode}` |
158
+ | `POST /api/v1/points/activation` | 绑定别人的码(**每账号一次**) |
159
+ | `POST /api/v1/points/first-login` | **也接受 `{inviteCode, deviceId}`** |
160
+
161
+ #### 为什么放在签到里,而不是登录时
162
+
163
+ 邀请码的有效期是「账号存在期间」,不是「注册那一刻」。放签到里覆盖两种情形:
164
+ 新号第一次签到、老号后来才想用。注册流程里不用塞输入框。
165
+
166
+ `auth.checkin` 现在一次做三件事(全部幂等,重复调用无害):
168
167
 
169
168
  ```
170
- 新账号登录 → 收集本机其它 Loomy 账号的邀请码 → 挑用得最少的
171
- → 查 activation(已用过就跳过)→ first-login {inviteCode}
172
- → 双方各 +5000
169
+ ① POST /points/first-login 刷新每日额度(拿 dailyQuota)
170
+ ② GET|POST /onboarding/tasks* 领新手任务
171
+ ③ GET /points/activation 确认没兑换过
172
+ GET /invitation-codes (本机其它账号) 取候选
173
+ POST /points/activation 兑换选中的码
173
174
  ```
174
175
 
176
+ ②③ 失败都不影响签到结果 —— 它们是顺带做的事,不是前置条件。
177
+
178
+ #### ⚠ 互刷防护(实测踩到过)
179
+
180
+ 第一次实现只排除了「自己的码」,于是发生了:
181
+
182
+ ```
183
+ 189 兑换了 181 的 FDW3CU → +5000(填写邀请码奖励)
184
+ 181 兑换了 189 的 E4DPT6 → +5000(填写邀请码奖励)
185
+ 两个号又各自拿到 邀请好友奖励 +5000
186
+ ⇒ 各得 10000,等于两笔邀请的全部收益
187
+ ```
188
+
189
+ **修法**:本号自己的码 `usedCount > 0`(说明已有人邀请过本号这边的关系)时,
190
+ **不再兑换别人的码**。互刷必然要求双方各用一次,所以这条即可堵住。
191
+ 单向成链仍然允许 —— 实测 181 兑换了 189 的码之后,它自己的 `FDW3CU` 仍是
192
+ `active` / `usedCount 0`,可以继续邀请第三个人。
193
+
194
+ #### 候选从哪来
195
+
196
+ `getAuth()` 只在当前账号作用域可见(magpie 的 `inScope`),拿不到别的账号。
197
+ 所以直接读 `~/.config/magpie/plugin-auth.json`,把 `loomy` / `loomy#xxxxxx`
198
+ 全捞出来逐个查(只读,session 不打印、不外发)。挑码规则:
199
+ 排除自己的、用完的(`usedCount >= maxUses`)、非 `active` 的;优先 `usedCount` 最小的。
200
+
175
201
  ### 一次 `usage()` 的请求数
176
202
 
177
203
  | 情形 | 请求数 |
package/index.mjs CHANGED
@@ -164,6 +164,72 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
164
164
  return out
165
165
  }
166
166
 
167
+ /**
168
+ * 给**本账号**挑一个可用的邀请码(供签到钩子调用)。
169
+ *
170
+ * 返回 `undefined` 表示不该兑换 —— 三种情况:
171
+ * 1. 本号已经兑换过别人的码(`activated: true`)—— 每号只能一次,别白发请求
172
+ * 2. 本机没有**别人**的码(只有自己)—— 不能自己邀请自己
173
+ * 3. 挑不出还有余量的码
174
+ *
175
+ * ⚠ **被邀请者仍可以继续邀请别人**:实测 181 号兑换了 189 的码之后,
176
+ * 它自己的 `FDW3CU` 仍是 `active` / `usedCount 0`。所以这里**不**因为
177
+ * 「本号 activated」就把本号从候选里剔除 —— 它自己那份照常可用。
178
+ * 反过来,**已邀请过我的那个码**要排除:不然 A 邀请 B、B 又邀请 A,
179
+ * 平台侧可能视作互刷。
180
+ */
181
+ const pickInviteFor = async (session) => {
182
+ // 已兑换过就到此为止(每账号一次)
183
+ let applied
184
+ try {
185
+ const st = await fetchActivation(session)
186
+ if (st.activated) return undefined
187
+ applied = st.appliedInvitationCode
188
+ } catch {
189
+ return undefined
190
+ }
191
+
192
+ // 本号自己的码(要从候选里排除)
193
+ let selfCode
194
+ try {
195
+ const mine = await fetchInviteCodes(session)
196
+ selfCode = mine[0]?.code
197
+ } catch {
198
+ selfCode = undefined
199
+ }
200
+
201
+ // ⚠⚠ **互相邀请**要去掉:如果本号的码已经被别人用过(`usedCount > 0`),
202
+ // 那用它的那个人反过来又能拿本号的码 —— 两个号互相邀请、各拿两笔
203
+ // 5000,就是刷。实测踩到过:189 与 181 互相兑换,各得 10000。
204
+ //
205
+ // 判据不靠猜:本机每张码的 `usedCount` 就是「被几个人用过」。
206
+ // 只要本号自己的码 `usedCount > 0`,说明有人邀请过本号这边的关系,
207
+ // 此时**已经兑换过就别再换**(上面 `activated` 已拦住),
208
+ // 而本号自己的码继续留给**新来的号**用 —— 单向,不成环。
209
+ //
210
+ // 更严格的一层:把「已经用掉本号码的那些账号的码」也排除。
211
+ // 但 `usedCount` 只给数量、不给是谁,所以能做到的就是:
212
+ // **本号码一旦被用过,就再也不用别人的码**(互刷必然要求双方各用一次)。
213
+ let selfUsed = 0
214
+ try {
215
+ const mine = await fetchInviteCodes(session)
216
+ selfUsed = mine[0]?.usedCount ?? 0
217
+ } catch {
218
+ selfUsed = 0
219
+ }
220
+ if (selfUsed > 0) {
221
+ log(`本号的邀请码已被 ${selfUsed} 人使用,不再兑换别人的码(避免互刷)`)
222
+ return undefined
223
+ }
224
+
225
+ const candidates = await collectInviteCandidates(session)
226
+ const picked = _pure.pickInviteCode(candidates, selfCode)
227
+ if (!picked) return undefined
228
+ // 再排一层:**本号已经用过的那个码**不能重复用(服务端也会拒,但别白发请求)
229
+ if (applied && picked.code === applied) return undefined
230
+ return picked.code
231
+ }
232
+
167
233
  /**
168
234
  * 新账号登录后的收尾:把「已有账号的邀请码」带上去 + 记住当日上限。
169
235
  *
@@ -174,40 +240,17 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
174
240
  * 邀请码从**其它已登录账号**里挑(`getAuth` 拿不到全局,所以由调用方传 candidates)。
175
241
  * 一个都找不到就静默跳过 —— 这是加分项,不该让登录失败。
176
242
  */
177
- const settleNewAccount = async (session, auth, { inviteCandidates, selfCode } = {}) => {
178
- const patch = {}
179
- let claim
180
-
181
- // 挑一个可用的邀请码(排除自己的、用完的、非 active 的)
182
- const picked = _pure.pickInviteCode(inviteCandidates ?? [], selfCode)
183
- if (picked) {
184
- try {
185
- const state = await fetchActivation(session)
186
- if (state.activated) {
187
- log(`该账号已用过邀请码,跳过(不重复兑换)`)
188
- } else {
189
- // 两条路都能绑:带上 inviteCode 调 first-login(首次初始化),
190
- // 或显式 POST points/activation。先用前者 —— 它顺便把当日额度也初始化了。
191
- claim = await claimDailyQuota(session, { inviteCode: picked.code })
192
- if (claim.ok) {
193
- log(`已绑定邀请码 ${picked.code}(来自已有账号),双方各得 ${_pure.INVITE_REWARD_POINTS} 积分`)
194
- } else {
195
- // first-login 那条没成,退回显式绑定
196
- const bound = await bindInviteCode(session, picked.code)
197
- if (bound.ok) log(`已绑定邀请码 ${picked.code}(activation)`)
198
- else log(`邀请码绑定失败(不影响登录):${bound.message}`, "warn")
199
- claim = claim ?? (await claimDailyQuota(session))
200
- }
201
- }
202
- } catch (e) {
203
- log(`邀请码流程跳过:${e?.message ?? e}`, "warn")
204
- }
205
- }
206
- claim = claim ?? (await claimDailyQuota(session))
207
-
208
- const qp = quotaPatch(claim)
209
- if (Object.keys(qp).length) Object.assign(patch, qp)
210
- return patch
243
+ /**
244
+ * 登录后的收尾:**只初始化当天额度**,顺手记进凭据。
245
+ *
246
+ * ⚠ 邀请码**不在这里兑换**。它是「账号存在期间随时可做一次」的事,
247
+ * 不是「注册那一刻必须做」的事 —— 放进每天一次的签到钩子,既覆盖新号
248
+ * 第一次签到,也覆盖「老号后来才想用」,注册流程里不用塞输入框。
249
+ * 见下面 `auth.checkin` 的第 ③ 步。
250
+ */
251
+ const settleNewAccount = async (session) => {
252
+ const claim = await claimDailyQuota(session)
253
+ return quotaPatch(claim)
211
254
  }
212
255
 
213
256
  const remember = async (provider, auth, patch) => {
@@ -283,8 +326,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
283
326
  (desktop.phone ? _pure.maskPhone(String(desktop.phone)) : `Loomy ${session.slice(-6)}`)
284
327
  // 登录后立刻初始化当天额度(官方客户端同款行为)并把上限记进凭据;
285
328
  // 顺便带上已有账号的邀请码(每个账号只能兑换一次,失败不影响登录)
286
- const inviteCandidates = await collectInviteCandidates(session)
287
- const claim = await settleNewAccount(session, null, { inviteCandidates })
329
+ const patch = await settleNewAccount(session)
288
330
  return {
289
331
  type: "success",
290
332
  access: session,
@@ -293,7 +335,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
293
335
  accountId: label,
294
336
  ...(desktop.userid ? { uid: String(desktop.userid) } : {}),
295
337
  ...(desktop.phone ? { phone: String(desktop.phone) } : {}),
296
- ...quotaPatch(claim),
338
+ ...patch,
297
339
  }
298
340
  },
299
341
  }
@@ -322,8 +364,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
322
364
  instructions: `验证码已发送至 ${_pure.maskPhone(phone)},输入 6 位数字完成登录。`,
323
365
  callback: async (code) => {
324
366
  const { session, userid } = await loginBySmsCode(phone, code, msgid)
325
- const inviteCandidates = await collectInviteCandidates(session)
326
- const claim = await settleNewAccount(session, null, { inviteCandidates })
367
+ const patch = await settleNewAccount(session)
327
368
  return {
328
369
  type: "success",
329
370
  access: session,
@@ -332,7 +373,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
332
373
  accountId: _pure.maskPhone(phone),
333
374
  uid: userid,
334
375
  phone,
335
- ...quotaPatch(claim),
376
+ ...patch,
336
377
  }
337
378
  },
338
379
  }
@@ -365,15 +406,14 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
365
406
  return { type: "failed", error: "session 无效或已过期(拉取模型目录失败)" }
366
407
  }
367
408
  const note = String(inputs?.note ?? "").trim()
368
- const inviteCandidates = await collectInviteCandidates(session)
369
- const claim = await settleNewAccount(session, null, { inviteCandidates })
409
+ const patch = await settleNewAccount(session)
370
410
  return {
371
411
  type: "success",
372
412
  access: session,
373
413
  refresh: "",
374
414
  expires: Date.now() + SESSION_TTL_MS,
375
415
  accountId: note || `Loomy ${session.slice(-6)}`,
376
- ...quotaPatch(claim),
416
+ ...patch,
377
417
  }
378
418
  },
379
419
  },
@@ -452,14 +492,27 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
452
492
  * (返回 `"success"`,magpie 归 `failed` 却保留 message,
453
493
  * 界面于是显示「签到失败 · 每日赠送积分已刷新」)。
454
494
  */
495
+ /**
496
+ * 签到钩子 = **每天把能领的都领一遍**。
497
+ *
498
+ * magpie 按北京时间每账号每天调一次,所以这里是「顺手做」的最佳位置:
499
+ *
500
+ * ① 刷新每日额度(`first-login`,幂等)
501
+ * ② 领新手任务(`onboarding/tasks/complete`,幂等)
502
+ * ③ 兑换邀请码(`points/activation`,**每账号一次**)
503
+ *
504
+ * 三件事全部幂等,重复调用无害 —— 这是把它们放在每天签到里的前提。
505
+ * ⚠ ②③ 失败都**不影响签到结果**:它们是一天里顺带做的事,
506
+ * 不是签到的前置条件。
507
+ */
455
508
  async checkin(getAuth, provider) {
456
509
  const auth = await getAuth()
457
510
  const credential = _pure.credentialOf(auth)
458
511
  if (!credential) return { outcome: "failed", message: "未登录" }
459
512
 
513
+ // ① 每日额度
460
514
  const claim = await claimDailyQuota(credential.session)
461
515
  const result = _pure.checkinOutcome(claim)
462
-
463
516
  if (claim.ok) {
464
517
  // 签到是拿到 `dailyQuota` 的**权威时刻**:记进凭据并清缓存,
465
518
  // 让卡片立刻反映新的上限(而不是等 60 秒 TTL)
@@ -467,24 +520,47 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
467
520
  clearUsageCache()
468
521
  }
469
522
 
470
- // 新手任务跟着签到一起领:它们本就是「登录后的一次性引导奖励」,
471
- // 放在每天一次的签到里做,既不会漏也不会重复(领取是幂等的)。
472
- // ⚠ 失败不影响签到结果 —— 这是**加分项**,不是签到的前置条件。
473
- if (claim.ok) {
474
- try {
475
- const state = await fetchOnboarding(credential.session)
476
- const { claimed, failed } = await claimAllTasks(credential.session, state.tasks)
477
- onboardingCache.set(credential.session, { at: Date.now(), value: null })
478
- if (claimed.length > 0) {
479
- const gained = claimed.reduce((n, x) => n + x.points, 0)
480
- log(`新手任务领到 ${claimed.length} 项(+${gained} 分)${failed.length ? `,${failed.length} 项失败` : ""}`)
481
- result.message += ` · 新手任务 +${gained} 分`
523
+ const extras = []
524
+
525
+ // ② 新手任务
526
+ try {
527
+ const state = await fetchOnboarding(credential.session)
528
+ const { claimed, failed } = await claimAllTasks(credential.session, state.tasks)
529
+ onboardingCache.set(credential.session, { at: Date.now(), value: null })
530
+ if (claimed.length > 0) {
531
+ const gained = claimed.reduce((n, x) => n + x.points, 0)
532
+ log(`新手任务领到 ${claimed.length} 项(+${gained} 分)${failed.length ? `,${failed.length} 项失败` : ""}`)
533
+ extras.push(`新手任务 +${gained} 分`)
534
+ clearUsageCache()
535
+ }
536
+ } catch (e) {
537
+ log(`新手任务领取跳过:${e?.message ?? e}`, "warn")
538
+ }
539
+
540
+ // ③ 邀请码 —— **签到时才兑换,不在登录时**
541
+ //
542
+ // 为什么改到这里:邀请码的有效期是「帐号存在期间」,不是「注册那一刻」。
543
+ // 放在签到里,既覆盖了「新号第一次签到」,也覆盖了「老号后来才决定用」,
544
+ // 不用在注册流程里塞一个输入框。
545
+ try {
546
+ const picked = await pickInviteFor(credential.session)
547
+ if (picked) {
548
+ const r = await bindInviteCode(credential.session, picked)
549
+ if (r.ok && !r.alreadyActivated) {
550
+ log(`已兑换邀请码 ${picked}(邀请人 +${_pure.INVITE_REWARD_POINTS},本号 +${_pure.INVITE_REWARD_POINTS})`)
551
+ extras.push(`邀请码 +${_pure.INVITE_REWARD_POINTS} 分`)
482
552
  clearUsageCache()
553
+ } else if (r.ok && r.alreadyActivated) {
554
+ log(`本号已兑换过邀请码(${picked}),跳过`)
555
+ } else {
556
+ log(`邀请码兑换失败(不影响签到):${r.message}`, "warn")
483
557
  }
484
- } catch (e) {
485
- log(`新手任务领取跳过:${e?.message ?? e}`, "warn")
486
558
  }
559
+ } catch (e) {
560
+ log(`邀请码兑换跳过:${e?.message ?? e}`, "warn")
487
561
  }
562
+
563
+ if (extras.length) result.message += ` · ${extras.join(" · ")}`
488
564
  return result
489
565
  },
490
566
  },
@@ -560,4 +636,6 @@ export const _internal = {
560
636
  bindInviteCode,
561
637
  fetchActivation,
562
638
  readAllLoomyAccounts,
639
+ pickInviteFor: (session) => pickInviteFor(session),
640
+ collectInviteCandidates: (s) => collectInviteCandidates(s),
563
641
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie-plugin-loomy-auth",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Loomy (iFlytek) for magpie: desktop-session import / SMS / session sign-in, live model catalog with reasoning efforts, chat with tools and images, and the two credit pools reported as the host routes them — the daily pool counts, the permanent one is shown aside.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",