magpie-plugin-loomy-auth 0.1.7 → 0.1.9
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/README.md +154 -144
- package/docs/internals.md +62 -24
- package/index.mjs +118 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,144 +1,154 @@
|
|
|
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 分** ——
|
|
24
|
-
- **邀请码 5,000 分** ——
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
`
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
每条邀请码能邀请 1000 人;**每个号只能被邀请一次**,所以「A 邀 B、B 再邀 A」
|
|
103
|
+
平台是允许的(实测过,服务端不拦)。插件不干预这个 —— 它只守平台自己的那条:
|
|
104
|
+
本号已经被邀请过就不再兑换。
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 安装
|
|
109
|
+
|
|
110
|
+
在 magpie 的 **插件 → 发现** 里搜 `Loomy`,点**安装**。
|
|
111
|
+
|
|
112
|
+
⚠ 先卸掉别的 Loomy 插件 —— 两个插件抢同一个 `loomy` 供应商会互相打架。
|
|
113
|
+
|
|
114
|
+
装完 `magpie plugin list` 应看到:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
magpie-plugin-loomy-auth on
|
|
118
|
+
loomy (Loomy, 10 models) signed in as 189****8051
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 常见问题
|
|
124
|
+
|
|
125
|
+
**积分显示 0,但官方 App 里还有?**
|
|
126
|
+
① 今日额度用光,等北京时间 0 点重置;② 那是**新手任务**,卡片上单独一行「可领 XXXX 积分」,签到后到账。
|
|
127
|
+
|
|
128
|
+
**显示「登录已失效」?**
|
|
129
|
+
会话 14 天到期,重新登录一次即可。
|
|
130
|
+
|
|
131
|
+
**会花我的积分吗?**
|
|
132
|
+
不会。插件只读余额、签到、领任务奖励。消耗积分的是你通过 magpie 发的对话请求。
|
|
133
|
+
|
|
134
|
+
**能看到我的什么?**
|
|
135
|
+
Loomy 登录态(存在 magpie 自己的凭据文件里)、积分/模型/任务读数。
|
|
136
|
+
直连 `loomyad.xunfei.cn` 与 `account.xfinfr.com`,不经任何第三方中转。
|
|
137
|
+
邀请码功能读同文件里其它 Loomy 账号的码,仅只读,不外发。
|
|
138
|
+
|
|
139
|
+
**装完没反应?**
|
|
140
|
+
`magpie plugin list` 确认只有一个 `loomy`。改完配置重启一次 magpie 最稳(登录态不丢)。
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 卸载
|
|
145
|
+
|
|
146
|
+
`magpie plugin rm magpie-plugin-loomy-auth`
|
|
147
|
+
|
|
148
|
+
登录态留在 magpie 的凭据里,重装即恢复。
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 许可
|
|
153
|
+
|
|
154
|
+
MIT。协议事实、实现判据、测试与本地安装见 [`docs/internals.md`](docs/internals.md)。
|
package/docs/internals.md
CHANGED
|
@@ -139,39 +139,77 @@ Loomy 还有第三笔额度 —— **新手任务奖励**,总计 10,000 分,
|
|
|
139
139
|
> 实况:`181****0632` 余额 0,但还有 10,000 分新手任务可领。卡片会如实显示,
|
|
140
140
|
> 不会因为「余额 0」就把这个人判死刑。
|
|
141
141
|
|
|
142
|
-
###
|
|
142
|
+
### 邀请码:签到时自动兑换(不在注册时)
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
⚠ 别和新手任务里 `share_soul` 的 2000 混,那是两笔不同的奖励)。
|
|
144
|
+
**实测结论(2026-10-26,真号跑的)**:邀请双方**各得 5000**,不是 2000。
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
所以判断依据就是 `GET /points/activation` 的 `activated`,跟「谁邀请了谁」无关。
|
|
182
|
+
|
|
183
|
+
**为什么不需要知道对方是谁**:`GET /api/v1/invitation-codes` 只回
|
|
184
|
+
`{inviteCode, usedCount, maxUses, status}` —— 有数量、没有名单。
|
|
185
|
+
客户端里也没有「被邀请的好友」列表接口;探测 `/invitation-codes/invitees`、
|
|
186
|
+
`/invitation-records`、`/points/invitation-records` 全部 404。
|
|
187
|
+
既然拿不到,就不该假装能判断。
|
|
188
|
+
|
|
189
|
+
#### 平台**允许**互刷(实测,不是推测)
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
189 兑换 181 的 FDW3CU → +5000 填写邀请码奖励
|
|
193
|
+
181 兑换 189 的 E4DPT6 → +5000 填写邀请码奖励
|
|
194
|
+
两边各再收 +5000 邀请好友奖励
|
|
195
|
+
⇒ 各得 10000,服务端全程 200,没有任何拦截
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**这是平台的规则漏洞,不是插件的责任。** 本插件只做两件合规的事:
|
|
199
|
+
|
|
200
|
+
1. 本号 `activated: true` 时不再兑换(平台规则);
|
|
201
|
+
2. 把本号自己的码从候选里排除(不能自己邀请自己)。
|
|
202
|
+
|
|
203
|
+
⚠ 曾经加过一条「本号码被用过就不再兑换别人的码」的规则,**已移除** —— 它拦不住互刷
|
|
204
|
+
(先兑换的一方 `usedCount` 还是 0),却会误伤 A→B→C 这种正常长链里的 B。
|
|
205
|
+
|
|
206
|
+
#### 候选从哪来
|
|
207
|
+
|
|
208
|
+
`getAuth()` 只在当前账号作用域可见(magpie 的 `inScope`),拿不到别的账号。
|
|
209
|
+
所以直接读 `~/.config/magpie/plugin-auth.json`,把 `loomy` / `loomy#xxxxxx`
|
|
210
|
+
全捞出来逐个查(只读,session 不打印、不外发)。挑码规则:
|
|
211
|
+
排除自己的、用完的(`usedCount >= maxUses`)、非 `active` 的;优先 `usedCount` 最小的。
|
|
212
|
+
|
|
175
213
|
### 一次 `usage()` 的请求数
|
|
176
214
|
|
|
177
215
|
| 情形 | 请求数 |
|
package/index.mjs
CHANGED
|
@@ -164,6 +164,54 @@ 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
|
+
//
|
|
184
|
+
// 平台规则就这一条(客户端文案:「每个账号仅能兑换一次」「你已经兑换过积分啦」)。
|
|
185
|
+
// 所以不需要知道「谁邀请了谁」—— 那信息接口根本不提供
|
|
186
|
+
// (`invitation-codes` 只回 `usedCount` 这个数量,不说是谁)。
|
|
187
|
+
// `appliedInvitationCode` 只用于日志,不参与判断。
|
|
188
|
+
let applied
|
|
189
|
+
try {
|
|
190
|
+
const st = await fetchActivation(session)
|
|
191
|
+
if (st.activated) return undefined
|
|
192
|
+
applied = st.appliedInvitationCode
|
|
193
|
+
} catch {
|
|
194
|
+
// 读不到状态就别乱兑换 —— 宁可这一天不换
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ② 本号自己的码要从候选里排除(不能自己邀请自己)
|
|
199
|
+
let selfCode
|
|
200
|
+
try {
|
|
201
|
+
const mine = await fetchInviteCodes(session)
|
|
202
|
+
selfCode = mine[0]?.code
|
|
203
|
+
} catch {
|
|
204
|
+
selfCode = undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ③ 从本机**别的账号**的码里挑一个还有余量的
|
|
208
|
+
const candidates = await collectInviteCandidates(session)
|
|
209
|
+
const picked = _pure.pickInviteCode(candidates, selfCode)
|
|
210
|
+
if (!picked) return undefined
|
|
211
|
+
if (applied && picked.code === applied) return undefined // 理论上不可达,防御性保留
|
|
212
|
+
return picked.code
|
|
213
|
+
}
|
|
214
|
+
|
|
167
215
|
/**
|
|
168
216
|
* 新账号登录后的收尾:把「已有账号的邀请码」带上去 + 记住当日上限。
|
|
169
217
|
*
|
|
@@ -174,40 +222,17 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
174
222
|
* 邀请码从**其它已登录账号**里挑(`getAuth` 拿不到全局,所以由调用方传 candidates)。
|
|
175
223
|
* 一个都找不到就静默跳过 —— 这是加分项,不该让登录失败。
|
|
176
224
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
225
|
+
/**
|
|
226
|
+
* 登录后的收尾:**只初始化当天额度**,顺手记进凭据。
|
|
227
|
+
*
|
|
228
|
+
* ⚠ 邀请码**不在这里兑换**。它是「账号存在期间随时可做一次」的事,
|
|
229
|
+
* 不是「注册那一刻必须做」的事 —— 放进每天一次的签到钩子,既覆盖新号
|
|
230
|
+
* 第一次签到,也覆盖「老号后来才想用」,注册流程里不用塞输入框。
|
|
231
|
+
* 见下面 `auth.checkin` 的第 ③ 步。
|
|
232
|
+
*/
|
|
233
|
+
const settleNewAccount = async (session) => {
|
|
234
|
+
const claim = await claimDailyQuota(session)
|
|
235
|
+
return quotaPatch(claim)
|
|
211
236
|
}
|
|
212
237
|
|
|
213
238
|
const remember = async (provider, auth, patch) => {
|
|
@@ -283,8 +308,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
283
308
|
(desktop.phone ? _pure.maskPhone(String(desktop.phone)) : `Loomy ${session.slice(-6)}`)
|
|
284
309
|
// 登录后立刻初始化当天额度(官方客户端同款行为)并把上限记进凭据;
|
|
285
310
|
// 顺便带上已有账号的邀请码(每个账号只能兑换一次,失败不影响登录)
|
|
286
|
-
const
|
|
287
|
-
const claim = await settleNewAccount(session, null, { inviteCandidates })
|
|
311
|
+
const patch = await settleNewAccount(session)
|
|
288
312
|
return {
|
|
289
313
|
type: "success",
|
|
290
314
|
access: session,
|
|
@@ -293,7 +317,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
293
317
|
accountId: label,
|
|
294
318
|
...(desktop.userid ? { uid: String(desktop.userid) } : {}),
|
|
295
319
|
...(desktop.phone ? { phone: String(desktop.phone) } : {}),
|
|
296
|
-
...
|
|
320
|
+
...patch,
|
|
297
321
|
}
|
|
298
322
|
},
|
|
299
323
|
}
|
|
@@ -322,8 +346,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
322
346
|
instructions: `验证码已发送至 ${_pure.maskPhone(phone)},输入 6 位数字完成登录。`,
|
|
323
347
|
callback: async (code) => {
|
|
324
348
|
const { session, userid } = await loginBySmsCode(phone, code, msgid)
|
|
325
|
-
const
|
|
326
|
-
const claim = await settleNewAccount(session, null, { inviteCandidates })
|
|
349
|
+
const patch = await settleNewAccount(session)
|
|
327
350
|
return {
|
|
328
351
|
type: "success",
|
|
329
352
|
access: session,
|
|
@@ -332,7 +355,7 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
332
355
|
accountId: _pure.maskPhone(phone),
|
|
333
356
|
uid: userid,
|
|
334
357
|
phone,
|
|
335
|
-
...
|
|
358
|
+
...patch,
|
|
336
359
|
}
|
|
337
360
|
},
|
|
338
361
|
}
|
|
@@ -365,15 +388,14 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
365
388
|
return { type: "failed", error: "session 无效或已过期(拉取模型目录失败)" }
|
|
366
389
|
}
|
|
367
390
|
const note = String(inputs?.note ?? "").trim()
|
|
368
|
-
const
|
|
369
|
-
const claim = await settleNewAccount(session, null, { inviteCandidates })
|
|
391
|
+
const patch = await settleNewAccount(session)
|
|
370
392
|
return {
|
|
371
393
|
type: "success",
|
|
372
394
|
access: session,
|
|
373
395
|
refresh: "",
|
|
374
396
|
expires: Date.now() + SESSION_TTL_MS,
|
|
375
397
|
accountId: note || `Loomy ${session.slice(-6)}`,
|
|
376
|
-
...
|
|
398
|
+
...patch,
|
|
377
399
|
}
|
|
378
400
|
},
|
|
379
401
|
},
|
|
@@ -452,14 +474,27 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
452
474
|
* (返回 `"success"`,magpie 归 `failed` 却保留 message,
|
|
453
475
|
* 界面于是显示「签到失败 · 每日赠送积分已刷新」)。
|
|
454
476
|
*/
|
|
477
|
+
/**
|
|
478
|
+
* 签到钩子 = **每天把能领的都领一遍**。
|
|
479
|
+
*
|
|
480
|
+
* magpie 按北京时间每账号每天调一次,所以这里是「顺手做」的最佳位置:
|
|
481
|
+
*
|
|
482
|
+
* ① 刷新每日额度(`first-login`,幂等)
|
|
483
|
+
* ② 领新手任务(`onboarding/tasks/complete`,幂等)
|
|
484
|
+
* ③ 兑换邀请码(`points/activation`,**每账号一次**)
|
|
485
|
+
*
|
|
486
|
+
* 三件事全部幂等,重复调用无害 —— 这是把它们放在每天签到里的前提。
|
|
487
|
+
* ⚠ ②③ 失败都**不影响签到结果**:它们是一天里顺带做的事,
|
|
488
|
+
* 不是签到的前置条件。
|
|
489
|
+
*/
|
|
455
490
|
async checkin(getAuth, provider) {
|
|
456
491
|
const auth = await getAuth()
|
|
457
492
|
const credential = _pure.credentialOf(auth)
|
|
458
493
|
if (!credential) return { outcome: "failed", message: "未登录" }
|
|
459
494
|
|
|
495
|
+
// ① 每日额度
|
|
460
496
|
const claim = await claimDailyQuota(credential.session)
|
|
461
497
|
const result = _pure.checkinOutcome(claim)
|
|
462
|
-
|
|
463
498
|
if (claim.ok) {
|
|
464
499
|
// 签到是拿到 `dailyQuota` 的**权威时刻**:记进凭据并清缓存,
|
|
465
500
|
// 让卡片立刻反映新的上限(而不是等 60 秒 TTL)
|
|
@@ -467,24 +502,47 @@ export const LoomyAuthPlugin = async ({ client, directory } = {}) => {
|
|
|
467
502
|
clearUsageCache()
|
|
468
503
|
}
|
|
469
504
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
505
|
+
const extras = []
|
|
506
|
+
|
|
507
|
+
// ② 新手任务
|
|
508
|
+
try {
|
|
509
|
+
const state = await fetchOnboarding(credential.session)
|
|
510
|
+
const { claimed, failed } = await claimAllTasks(credential.session, state.tasks)
|
|
511
|
+
onboardingCache.set(credential.session, { at: Date.now(), value: null })
|
|
512
|
+
if (claimed.length > 0) {
|
|
513
|
+
const gained = claimed.reduce((n, x) => n + x.points, 0)
|
|
514
|
+
log(`新手任务领到 ${claimed.length} 项(+${gained} 分)${failed.length ? `,${failed.length} 项失败` : ""}`)
|
|
515
|
+
extras.push(`新手任务 +${gained} 分`)
|
|
516
|
+
clearUsageCache()
|
|
517
|
+
}
|
|
518
|
+
} catch (e) {
|
|
519
|
+
log(`新手任务领取跳过:${e?.message ?? e}`, "warn")
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ③ 邀请码 —— **签到时才兑换,不在登录时**
|
|
523
|
+
//
|
|
524
|
+
// 为什么改到这里:邀请码的有效期是「帐号存在期间」,不是「注册那一刻」。
|
|
525
|
+
// 放在签到里,既覆盖了「新号第一次签到」,也覆盖了「老号后来才决定用」,
|
|
526
|
+
// 不用在注册流程里塞一个输入框。
|
|
527
|
+
try {
|
|
528
|
+
const picked = await pickInviteFor(credential.session)
|
|
529
|
+
if (picked) {
|
|
530
|
+
const r = await bindInviteCode(credential.session, picked)
|
|
531
|
+
if (r.ok && !r.alreadyActivated) {
|
|
532
|
+
log(`已兑换邀请码 ${picked}(邀请人 +${_pure.INVITE_REWARD_POINTS},本号 +${_pure.INVITE_REWARD_POINTS})`)
|
|
533
|
+
extras.push(`邀请码 +${_pure.INVITE_REWARD_POINTS} 分`)
|
|
482
534
|
clearUsageCache()
|
|
535
|
+
} else if (r.ok && r.alreadyActivated) {
|
|
536
|
+
log(`本号已兑换过邀请码(${picked}),跳过`)
|
|
537
|
+
} else {
|
|
538
|
+
log(`邀请码兑换失败(不影响签到):${r.message}`, "warn")
|
|
483
539
|
}
|
|
484
|
-
} catch (e) {
|
|
485
|
-
log(`新手任务领取跳过:${e?.message ?? e}`, "warn")
|
|
486
540
|
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
log(`邀请码兑换跳过:${e?.message ?? e}`, "warn")
|
|
487
543
|
}
|
|
544
|
+
|
|
545
|
+
if (extras.length) result.message += ` · ${extras.join(" · ")}`
|
|
488
546
|
return result
|
|
489
547
|
},
|
|
490
548
|
},
|
|
@@ -560,4 +618,6 @@ export const _internal = {
|
|
|
560
618
|
bindInviteCode,
|
|
561
619
|
fetchActivation,
|
|
562
620
|
readAllLoomyAccounts,
|
|
621
|
+
pickInviteFor: (session) => pickInviteFor(session),
|
|
622
|
+
collectInviteCandidates: (s) => collectInviteCandidates(s),
|
|
563
623
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-plugin-loomy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|