magicrew-auth 0.3.31 → 0.3.32
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 +1 -0
- package/CHANGELOG.md +46 -0
- package/README.md +1 -0
- package/index.mjs +22 -2
- package/package.json +1 -1
- package/src/hooks/methods.mjs +208 -10
- package/src/store.mjs +109 -4
package/AGENTS.md
CHANGED
|
@@ -75,6 +75,7 @@ curl -s "https://usemagpie.ai/api/latest?channel=stable" | grep -o '"version":"[
|
|
|
75
75
|
| 22 | 同一账号有的请求 200、有的 403 `token无效`(`retryable=false`),换个时间又好了 | key 有两条签发路径时 store 与 magpie 各存一把,**静态挑一把必有一半踩空**。0.3.29 起 `keyCandidates()` 把 store 两槽 + `auth.access` 逐把打 chat,命中的写回 store;全被拒才交 402 让 magpie 换号。判据:`GET /v1/models` 对死 key **放行**,逐把探活必须用 `/v1/chat/completions` | CHANGELOG 0.3.29 · `tests/key-failover.mjs` |
|
|
76
76
|
| 23 | 插件列表里别人有「已自动更新」徽章,magicrew 没有 | magpie 只自动更新包名以 `@magpie-community/` 开头的包(`internal/plugin/updates.go` 的 `Official()` 硬编码前缀);第三方 npm / git 源永远只提示。本插件用 `src/auto-maintenance.mjs` 复刻:读 `plugin-updates.json` 的 `waiting` → 调 `magpie plugin update` → 成功后补 `updated[]`(徽章读它)。判据:`plugin-updates.json` 的 waiting 有 `package:"Magicrew", git:true`;`findMagpieExe()` 能解析到 magpie 可执行文件。⚠ 更新后要重启 magpie 才加载新代码 | CHANGELOG 0.3.30 · `tests/auto-maintenance.mjs` |
|
|
77
77
|
| 24 | 新装插件后没有任何签到记录,`plugin-checkin.json` 里没有 magicrew | magpie 的 `PluginCheckinOn()`:`pluginCheckins` 没有显式值时默认 **false**(只有 WorkBuddy / Trae / MiniMax / Qoder 有内置开关)。插件只能在加载时把 `settings.json` 的 `pluginCheckins[provider]` 补成 true —— **只在键不存在时补**,用户显式 false 不覆盖。判据:`settings.json` 的 `pluginCheckins` 有 `magicrew` / `magicrew-intl` 键 | CHANGELOG 0.3.30 · `src/auto-maintenance.mjs` |
|
|
78
|
+
| 25 | 邀请上级选了某个账号,第二天签到后又变回别的号 | 旧版 `rollInvite` 每天覆盖滚动文件。0.3.32 起策略在**锁定开关打开时**优先(provider 设置「锁定邀请上级」,默认开);锁定关掉则回落到滚动。判据:`~/.cache/magpie/magicrew-invite-policy-<site>.json` 是 fixed/custom,且开关开着;`tests/invite-policy.mjs` 锁死 | CHANGELOG 0.3.32 · `src/store.mjs` |
|
|
78
79
|
|
|
79
80
|
**坑 14 的两个附带判据**(都实测过):
|
|
80
81
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.3.32 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
**邀请上级可固定 / 自定义(PR #13,作者 @baijiu666)**
|
|
6
|
+
|
|
7
|
+
### 你看到的
|
|
8
|
+
|
|
9
|
+
登录方式列表里多了两个入口:
|
|
10
|
+
|
|
11
|
+
- **`超级麦吉 邀请上级`** —— 展开的按钮列表,第一项是「我的(作者 MYCF711)」,
|
|
12
|
+
接着是「自动(跟随最后一个登录 / 签到的号)」,然后是每个已登录账号
|
|
13
|
+
(手机号/邮箱打码 + 自己的邀请码)
|
|
14
|
+
- **`超级麦吉 邀请上级:自定义码`** —— 文本框,IV 开头 12 位;留空不改
|
|
15
|
+
|
|
16
|
+
另外 provider 设置里多了一个开关:
|
|
17
|
+
|
|
18
|
+
- **`锁定邀请上级(签到不再覆盖)`**,默认开。打开时新号固定挂在选定的码下;
|
|
19
|
+
关掉就回到「跟随最后一个登录/签到的账号」。
|
|
20
|
+
|
|
21
|
+
两个入口都只落盘 + 回一条「已保存」,不登录、不新增账号
|
|
22
|
+
(`authorize` 返回 `{ type: "failed" }` 是 magpie 唯一不建号的出口)。
|
|
23
|
+
|
|
24
|
+
### 为什么不是「一个文本框 + 展开按钮」
|
|
25
|
+
|
|
26
|
+
magpie 的 `prompts` 只有两种控件:`select`(一排按钮)和 `text`(输入框),
|
|
27
|
+
没有把两者合成一个控件的口子;要真正做组合控件只能写插件页面/面板,而页面
|
|
28
|
+
**读不到账号邀请码**(`PageReads` 只给 `usage:summary` / `providers:list`,
|
|
29
|
+
不含账号)。所以用两个单步入口,最稳。
|
|
30
|
+
|
|
31
|
+
### 实现
|
|
32
|
+
|
|
33
|
+
- `src/store.mjs`:`readInvitePolicy` / `writeInvitePolicy` /
|
|
34
|
+
`magicrew-invite-codes-<site>.json`;`inviteToUse(siteId, locked)` 在锁定时
|
|
35
|
+
优先策略,否则回落到滚动文件;`authorInvite()` 给「我的」。
|
|
36
|
+
- `src/hooks/methods.mjs`:两个 `api + keyless` 入口;`inviteChoices()` 生成
|
|
37
|
+
作者 → 自动 → 各账号的列表;`accountLabel()` 只认本站点的 provider key。
|
|
38
|
+
- `index.mjs`:`auth.options` 声明 `inviteLock`,用 `input.magpie.options()` 读
|
|
39
|
+
(不是 `option()` —— 后者对未知键也返回 false,加载早期会把默认 true 误判成关)。
|
|
40
|
+
|
|
41
|
+
### 合入时改的
|
|
42
|
+
|
|
43
|
+
- PR 原列表第一项是「保持当前」;改成「我的(作者 MYCF711)」在第一位,并补上作者码。
|
|
44
|
+
- `accountLabel` 原来用 `startsWith(siteId)`,`magicrew` 会把 `magicrew-intl#…`
|
|
45
|
+
也算进来;改成 `k === siteId || k.startsWith(siteId + "#")`。
|
|
46
|
+
- 增加「锁定邀请上级」开关(PR 只有 fixed/custom 就默认覆盖,没有显式锁)。
|
|
47
|
+
- 补 `tests/invite-policy.mjs` 17 条断言。
|
|
48
|
+
|
|
3
49
|
## 0.3.31 — 2026-10-29
|
|
4
50
|
|
|
5
51
|
**自动更新加全局锁(0.3.30 补丁)**
|
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- **默认开启自动签到** —— 安装后自动把本 provider 的插件签到设为开(`pluginCheckins` 缺省时补 true;你手动关掉的不会被改回)
|
|
35
35
|
- **自动更新** —— 复用 magpie 的插件更新机制:发现自己的新版本后调 `magpie plugin update`,并在成功后写「已自动更新」记录;更新完要重启一次 magpie 才会加载新代码
|
|
36
36
|
- **邀请码自动滚** —— 第一次注册带作者的码,之后每次登录都记住「上一个登录账号」的码,拿到新的就覆盖旧的
|
|
37
|
+
- **邀请上级可固定** —— 登录列表里的「邀请上级」展开所有已登录账号(第一项是「我的(作者 MYCF711)」),或「自定义码」填任意 IV 码;provider 设置里的「锁定邀请上级」开关决定是否让签到覆盖
|
|
37
38
|
|
|
38
39
|
---
|
|
39
40
|
|
package/index.mjs
CHANGED
|
@@ -41,7 +41,17 @@ import { makeHeaders } from "./src/hooks/headers.mjs"
|
|
|
41
41
|
|
|
42
42
|
/** 一个站点进,一个完整插件出 */
|
|
43
43
|
function buildPlugin(c) {
|
|
44
|
-
return async () => {
|
|
44
|
+
return async (input) => {
|
|
45
|
+
// magpie 的 optionValues():用户在 provider 设置里改的开关,实时读。
|
|
46
|
+
// ⚠ 用 options(provider) 而不是 option(provider,key):后者对「还不知道的
|
|
47
|
+
// 选项」也返回 false,插件加载早期会把默认 true 误判成关闭。没有该键
|
|
48
|
+
// (老 magpie / 测试)时按默认 true。
|
|
49
|
+
const getLock = () => {
|
|
50
|
+
const vals = input?.magpie?.options?.(c.PROVIDER)
|
|
51
|
+
if (vals && Object.prototype.hasOwnProperty.call(vals, "inviteLock")) return vals.inviteLock !== false
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
// 卡片标题与文案不在这里做 —— 那是 registry 条目的职责。
|
|
46
56
|
// 插件不写 magpie 自己的缓存目录:那份文件属于 magpie,插件只读不写。
|
|
47
57
|
return {
|
|
@@ -53,7 +63,17 @@ function buildPlugin(c) {
|
|
|
53
63
|
maxConcurrency: 4,
|
|
54
64
|
refreshLead: REFRESH_LEAD_MS,
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
options: [
|
|
67
|
+
{
|
|
68
|
+
key: "inviteLock",
|
|
69
|
+
label: "锁定邀请上级(签到不再覆盖)",
|
|
70
|
+
description:
|
|
71
|
+
"打开:新号固定挂在选定的邀请码下;关闭:跟随最后一个登录 / 签到的账号(会漂移)。",
|
|
72
|
+
default: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
methods: makeMethods(c, getLock),
|
|
57
77
|
refresh: makeRefresh(c),
|
|
58
78
|
loader: makeLoader(c),
|
|
59
79
|
usage: makeUsage(c),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magicrew-auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.32",
|
|
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",
|
package/src/hooks/methods.mjs
CHANGED
|
@@ -28,7 +28,22 @@ import {
|
|
|
28
28
|
fetchWho,
|
|
29
29
|
fetchOwnInviteCode,
|
|
30
30
|
} from "../api.mjs"
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
inviteToUse,
|
|
33
|
+
authorInvite,
|
|
34
|
+
readSession,
|
|
35
|
+
writeSession,
|
|
36
|
+
noteLoginFailure,
|
|
37
|
+
noteOrg,
|
|
38
|
+
readKnownOrgs,
|
|
39
|
+
rollInvite,
|
|
40
|
+
readInvitePolicy,
|
|
41
|
+
writeInvitePolicy,
|
|
42
|
+
readAccountCodes,
|
|
43
|
+
} from "../store.mjs"
|
|
44
|
+
import { readFileSync } from "node:fs"
|
|
45
|
+
import { homedir } from "node:os"
|
|
46
|
+
import { join } from "node:path"
|
|
32
47
|
import { readLocalCreds } from "../local-browser.mjs"
|
|
33
48
|
import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
|
|
34
49
|
import { readVisionModel } from "../vision.mjs"
|
|
@@ -144,7 +159,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
|
|
|
144
159
|
* @param {object} c 站点闭包(id / SHORT / web / host)
|
|
145
160
|
* @param {"mobile"|"email"} kind
|
|
146
161
|
*/
|
|
147
|
-
function makeCodeMethod(c, kind) {
|
|
162
|
+
function makeCodeMethod(c, kind, getLock = () => true) {
|
|
148
163
|
const isEmail = kind === "email"
|
|
149
164
|
const what = isEmail ? "邮箱" : "手机号"
|
|
150
165
|
/** 官方登录页上那个 tab 的名字 —— 回退到浏览器时要说清楚点哪个 */
|
|
@@ -168,7 +183,7 @@ function makeCodeMethod(c, kind) {
|
|
|
168
183
|
|
|
169
184
|
return {
|
|
170
185
|
type: "oauth",
|
|
171
|
-
label: `${c.SHORT} ${what}
|
|
186
|
+
label: `${c.SHORT} ${what}验证码${inviteTag(c, getLock)}`,
|
|
172
187
|
prompts: [
|
|
173
188
|
isEmail
|
|
174
189
|
? {
|
|
@@ -199,7 +214,7 @@ function makeCodeMethod(c, kind) {
|
|
|
199
214
|
// ⚠ 无头这条路**没有**「打开带邀请码的页面」这一步,所以邀请码只能靠
|
|
200
215
|
// `redirect` 传 —— 网页表单的初始值里 redirect 就是 window.location.href,
|
|
201
216
|
// 服务端从它上面取 `?invitation=`。少了它,新注册的号就没有邀请关系。
|
|
202
|
-
const inviteCode = inviteToUse(c.id)
|
|
217
|
+
const inviteCode = inviteToUse(c.id, getLock())
|
|
203
218
|
const loginUrl = inviteCode
|
|
204
219
|
? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
|
|
205
220
|
: `${c.web}/login`
|
|
@@ -330,7 +345,120 @@ function makeCodeMethod(c, kind) {
|
|
|
330
345
|
}
|
|
331
346
|
}
|
|
332
347
|
|
|
333
|
-
|
|
348
|
+
// ═══ 邀请上级:选择 / 自定义 ═════════════════════════════════════════
|
|
349
|
+
//
|
|
350
|
+
// 默认行为(auto)是「用最后一个登录 / 签到的号的码」,但它会漂移 ——
|
|
351
|
+
// 每天 00:00 签到最后跑的那个号会把文件覆盖掉。这里让用户注册时就能定死。
|
|
352
|
+
//
|
|
353
|
+
// 界面(magpie 的 prompts 只有 select / text 两种控件,没有复合控件):
|
|
354
|
+
// · select = 展开的按钮列表:我的(作者 MYCF711)→ 自动 → 已登录账号
|
|
355
|
+
// · text = 自定义码
|
|
356
|
+
// · 锁定 = provider 的 auth.options 开关(真正的开关,magpie 设置页可见)
|
|
357
|
+
//
|
|
358
|
+
// 为什么不做「一个文本框 + 展开按钮」:prompts 没有把两者合成一个控件的口子;
|
|
359
|
+
// 硬拼要写插件页面/面板,而页面读不到账号邀请码(PageReads 只给
|
|
360
|
+
// usage:summary / providers:list,不含账号)。两个单步入口最稳。
|
|
361
|
+
//
|
|
362
|
+
// 选项靠 `magicrew-invite-codes-<site>.json` 填 —— 那是签到 / 登录时
|
|
363
|
+
// `rollInvite` 顺手记下来的(每个账号每天都会跑一次签到,所以不会过期)。
|
|
364
|
+
// 这里**不能**发请求:prompts 的 options 是同步生成的。
|
|
365
|
+
|
|
366
|
+
/** org → 便于辨认的名字(手机号打码 / 邮箱),从 magpie 自己的账号文件只读 */
|
|
367
|
+
function accountLabel(siteId, org) {
|
|
368
|
+
try {
|
|
369
|
+
const raw = JSON.parse(readFileSync(join(homedir(), ".config", "magpie", "plugin-auth.json"), "utf8"))
|
|
370
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
371
|
+
// 只认这个站点自己的 provider key(magicrew 不能匹配到 magicrew-intl)
|
|
372
|
+
if (k !== siteId && !String(k).startsWith(siteId + "#")) continue
|
|
373
|
+
if (String(v?.org || "") !== org) continue
|
|
374
|
+
if (v?.accountId) return String(v.accountId)
|
|
375
|
+
}
|
|
376
|
+
} catch {}
|
|
377
|
+
return org
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function inviteChoices(c, getLock = () => true) {
|
|
381
|
+
const pol = readInvitePolicy(c.id)
|
|
382
|
+
const codes = readAccountCodes(c.id)
|
|
383
|
+
const author = authorInvite(c.id)
|
|
384
|
+
const locked = getLock() !== false
|
|
385
|
+
const opts = []
|
|
386
|
+
if (author) {
|
|
387
|
+
opts.push({
|
|
388
|
+
label: "我的(作者 MYCF711):" + author,
|
|
389
|
+
value: "__author__",
|
|
390
|
+
hint: locked && pol.mode === "fixed" && pol.code === author ? "当前" : "作者码",
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
opts.push({
|
|
394
|
+
label: "自动(跟随最后一个登录 / 签到的号)",
|
|
395
|
+
value: "__auto__",
|
|
396
|
+
hint: pol.mode === "auto" ? "当前" : "会漂移",
|
|
397
|
+
})
|
|
398
|
+
for (const [org, v] of Object.entries(codes)) {
|
|
399
|
+
if (!v?.code || v.code === author) continue
|
|
400
|
+
opts.push({
|
|
401
|
+
label: accountLabel(c.id, org) + "(" + v.code + ")",
|
|
402
|
+
value: v.code,
|
|
403
|
+
hint: pol.mode === "fixed" && pol.code === v.code ? "当前固定" : org,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
return opts
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** 两个「只设置」入口共用的收尾 —— 把落盘结果回成一条可读的说明 */
|
|
410
|
+
function savedPolicy(c, getLock = () => true) {
|
|
411
|
+
return {
|
|
412
|
+
type: "failed",
|
|
413
|
+
error: `已保存:新号默认挂在 ${describePolicy(readInvitePolicy(c.id), getLock())}。这一步只存设置,不登录、不新增账号。`,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 面板标题后缀 —— 让「当前挂在谁名下」在选登录方式那一屏就能看见。
|
|
419
|
+
* (那张面板只能显示 methods 的 label,插件没有别的可写位置。)
|
|
420
|
+
*
|
|
421
|
+
* ⚠ 值在插件加载时算一次;改完设置要重启 magpie 才会刷新标题。
|
|
422
|
+
* 设置本身是立即生效的,标题只是个提示。
|
|
423
|
+
*/
|
|
424
|
+
function inviteTag(c, getLock = () => true) {
|
|
425
|
+
if (getLock() === false) return ""
|
|
426
|
+
const pol = readInvitePolicy(c.id)
|
|
427
|
+
if (pol.mode === "fixed" && pol.code) return `(上级 ${pol.accountId || pol.code})`
|
|
428
|
+
if (pol.mode === "custom" && pol.code) return `(上级 ${pol.code})`
|
|
429
|
+
return ""
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function describePolicy(pol, locked = true) {
|
|
433
|
+
if (!locked) return "自动(锁定已关闭)"
|
|
434
|
+
if (pol.mode === "fixed" && pol.code) return (pol.accountId || pol.org || "某个号") + "(" + pol.code + ")"
|
|
435
|
+
if (pol.mode === "custom" && pol.code) return "自定义码 " + pol.code
|
|
436
|
+
return "自动(跟随最后一个登录 / 签到的号)"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const INVITE_MESSAGE = "新号挂在谁的邀请码下?(决定拉新奖励进哪个账号)"
|
|
440
|
+
const CUSTOM_KEY = "inviteCode"
|
|
441
|
+
|
|
442
|
+
/** 这次注册真正要带上的码 */
|
|
443
|
+
|
|
444
|
+
/** 选择落盘 —— 只在 authorize 里、真正要开登录页之前调 */
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 动态提问:宿主支持 `ask`(magpie 内嵌契约里的 nextPrompt 优先用它),
|
|
448
|
+
* 所以下拉框每次都是最新的;静态 `prompts` 只作兜底。
|
|
449
|
+
*
|
|
450
|
+
* ⚠ 每次登录都会问 —— 但不能问第二次「自定义码」(选了才问),
|
|
451
|
+
* 而且上次的选择会作为默认带 hint,直接回车就用它。
|
|
452
|
+
*
|
|
453
|
+
* 契约限制(`extract-contract.py` 提取的原文):那个「用哪种方式登录」面板
|
|
454
|
+
* 只能列 `methods`,而 methods 的 type 只有 `oauth` / `api` 两种 ——
|
|
455
|
+
* **两种都会真的建一个账号**,没有「纯设置项」这种入口。
|
|
456
|
+
* 所以入口只能挂在登录流程里,做不到面板上单独一格。
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
/** 静态兜底:宿主不认 `ask` 时用 */
|
|
460
|
+
|
|
461
|
+
export function makeMethods(c, getLock = () => true) {
|
|
334
462
|
// 顺序 = magpie 登录方式列表里的顺序。
|
|
335
463
|
//
|
|
336
464
|
// ⚠ **邮箱验证码只有国际站有**,国内站不给。实测两件事:
|
|
@@ -342,18 +470,18 @@ export function makeMethods(c) {
|
|
|
342
470
|
// 只会让用户填完邮箱撞一个看不懂的错。
|
|
343
471
|
const codeMethods =
|
|
344
472
|
c.id === "magicrew-intl"
|
|
345
|
-
? [makeCodeMethod(c, "email"), makeCodeMethod(c, "mobile")]
|
|
346
|
-
: [makeCodeMethod(c, "mobile")]
|
|
473
|
+
? [makeCodeMethod(c, "email", getLock), makeCodeMethod(c, "mobile", getLock)]
|
|
474
|
+
: [makeCodeMethod(c, "mobile", getLock)]
|
|
347
475
|
|
|
348
476
|
return [
|
|
349
477
|
...codeMethods,
|
|
350
478
|
|
|
351
479
|
{
|
|
352
480
|
type: "oauth",
|
|
353
|
-
label: `${c.SHORT}
|
|
354
|
-
async authorize() {
|
|
481
|
+
label: `${c.SHORT} 浏览器登录${inviteTag(c, getLock)}`,
|
|
482
|
+
async authorize(inputs) {
|
|
355
483
|
// 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份
|
|
356
|
-
const inviteCode = inviteToUse(c.id)
|
|
484
|
+
const inviteCode = inviteToUse(c.id, getLock())
|
|
357
485
|
const url = inviteCode
|
|
358
486
|
? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
|
|
359
487
|
: `${c.web}/login`
|
|
@@ -567,5 +695,75 @@ export function makeMethods(c) {
|
|
|
567
695
|
}
|
|
568
696
|
},
|
|
569
697
|
},
|
|
698
|
+
// ── ④⑤ 邀请上级(只设置,不登录)───────────────────────────────
|
|
699
|
+
//
|
|
700
|
+
// 契约限制(extract-contract.py 原文 + 2026-10-29 实测):
|
|
701
|
+
// · 这一屏只渲染 `methods`,type 只有 `oauth` / `api`
|
|
702
|
+
// · `save()` 对两者都会写一个账号;唯一不建号的出口是让 authorize
|
|
703
|
+
// 返回 `{ type: "failed" }`(所以这两个入口都只落盘 + 回「已保存」)
|
|
704
|
+
// · prompts 的控件只有 select(按钮列表)和 text(输入框)
|
|
705
|
+
//
|
|
706
|
+
// 拆成两个单步入口:一个展开账号列表(第一项就是「我的(作者 MYCF711)」),
|
|
707
|
+
// 一个填自定义码。锁定用 auth.options 的开关(provider 设置页里的真开关)。
|
|
708
|
+
{
|
|
709
|
+
type: "api",
|
|
710
|
+
keyless: true,
|
|
711
|
+
label: `${c.SHORT} 邀请上级${inviteTag(c, getLock)}`,
|
|
712
|
+
prompts: [
|
|
713
|
+
{ type: "select", key: "invite", message: INVITE_MESSAGE, options: inviteChoices(c, getLock) },
|
|
714
|
+
],
|
|
715
|
+
async authorize(inputs) {
|
|
716
|
+
const pick = String(inputs?.invite || "")
|
|
717
|
+
if (!pick) return { type: "failed", error: "没有选择 —— 保持当前设置" }
|
|
718
|
+
if (pick === "__author__") {
|
|
719
|
+
writeInvitePolicy(c.id, {
|
|
720
|
+
mode: "fixed",
|
|
721
|
+
code: authorInvite(c.id),
|
|
722
|
+
org: "author",
|
|
723
|
+
accountId: "MYCF711",
|
|
724
|
+
})
|
|
725
|
+
} else if (pick === "__auto__") {
|
|
726
|
+
// 选了自动 → 回到默认的滚动机制
|
|
727
|
+
writeInvitePolicy(c.id, { mode: "auto" })
|
|
728
|
+
} else {
|
|
729
|
+
const codes = readAccountCodes(c.id)
|
|
730
|
+
const org = Object.keys(codes).find((o) => codes[o]?.code === pick) || ""
|
|
731
|
+
writeInvitePolicy(c.id, {
|
|
732
|
+
mode: "fixed",
|
|
733
|
+
code: pick,
|
|
734
|
+
org,
|
|
735
|
+
accountId: org ? accountLabel(c.id, org) : "",
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
return savedPolicy(c, getLock)
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
{
|
|
743
|
+
type: "api",
|
|
744
|
+
keyless: true,
|
|
745
|
+
label: `${c.SHORT} 邀请上级:自定义码`,
|
|
746
|
+
prompts: [
|
|
747
|
+
{
|
|
748
|
+
type: "text",
|
|
749
|
+
key: CUSTOM_KEY,
|
|
750
|
+
message: "自定义邀请码(IV 开头 12 位;留空 = 保持当前)",
|
|
751
|
+
placeholder: "IVXXXXXXXXXX",
|
|
752
|
+
validate: (v) => {
|
|
753
|
+
const s = String(v || "").trim()
|
|
754
|
+
if (!s) return undefined
|
|
755
|
+
return /^IV[A-Z0-9]{10}$/i.test(s) ? undefined : "邀请码是 IV 开头的 12 位字母数字"
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
async authorize(inputs) {
|
|
760
|
+
const code = String(inputs?.[CUSTOM_KEY] || "")
|
|
761
|
+
.trim()
|
|
762
|
+
.toUpperCase()
|
|
763
|
+
if (!code) return { type: "failed", error: "没有填码 —— 保持当前设置" }
|
|
764
|
+
writeInvitePolicy(c.id, { mode: "custom", code })
|
|
765
|
+
return savedPolicy(c, getLock)
|
|
766
|
+
},
|
|
767
|
+
},
|
|
570
768
|
]
|
|
571
769
|
}
|
package/src/store.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// ⚠ 这不是「状态管理」,是缓存:删掉全部,插件照样能跑(只是要重新登录一次)。
|
|
9
9
|
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs"
|
|
11
11
|
import { homedir } from "node:os"
|
|
12
12
|
import { join, dirname } from "node:path"
|
|
13
13
|
import { fetchOwnInviteCode } from "./api.mjs"
|
|
@@ -113,11 +113,113 @@ function writeInvite(siteId, code) {
|
|
|
113
113
|
return c
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
function inviteToUse(siteId) {
|
|
116
|
+
/** 这次登录该带的邀请码:锁定 → 策略 → 记录 → 作者码 */
|
|
117
|
+
function inviteToUse(siteId, locked = true) {
|
|
118
|
+
if (locked) {
|
|
119
|
+
const pol = readInvitePolicy(siteId)
|
|
120
|
+
if ((pol.mode === "fixed" || pol.mode === "custom") && pol.code) return pol.code
|
|
121
|
+
}
|
|
118
122
|
return readInvite(siteId) || AUTHOR_INVITE[siteId] || ""
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/** 作者自己的码(「我的」那一项) */
|
|
126
|
+
export function authorInvite(siteId) {
|
|
127
|
+
return AUTHOR_INVITE[siteId] || ""
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ═══ 邀请上级策略 ═══════════════════════════════════════════════════
|
|
131
|
+
//
|
|
132
|
+
// 三种模式,登录时选,落盘后一直生效:
|
|
133
|
+
//
|
|
134
|
+
// auto (默认)用「最后一个登录 / 签到的号」的码 —— 老行为,会漂移
|
|
135
|
+
// fixed 固定挂到某个已登录账号(存 org + code)
|
|
136
|
+
// custom 任意码,用户自己填
|
|
137
|
+
//
|
|
138
|
+
// 为什么要这个:`rollInvite` 会在每次登录、每天签到把
|
|
139
|
+
// magicrew-invite-<site>.json 覆盖成最后那个号的码。奖励虽然不出圈,
|
|
140
|
+
// 但「新号挂谁」就变成随机的了 —— 用户要的是可预期。
|
|
141
|
+
//
|
|
142
|
+
// ⚠ fixed 存的是**解析好的码**,不是「登录时再去问那个号要」:
|
|
143
|
+
// 要问就得多一条带那个账号 jwt 的请求,而签到 / 登录路径上都没有它。
|
|
144
|
+
// 代价是对方换码后要重新选一次(码实测长期不变)。
|
|
145
|
+
|
|
146
|
+
function invitePolicyPath(siteId) {
|
|
147
|
+
return join(cacheDir(), `magicrew-invite-policy-${siteId}.json`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function readInvitePolicy(siteId) {
|
|
151
|
+
try {
|
|
152
|
+
const v = JSON.parse(readFileSync(invitePolicyPath(siteId), "utf8"))
|
|
153
|
+
const mode = ["auto", "fixed", "custom"].includes(v?.mode) ? v.mode : "auto"
|
|
154
|
+
return {
|
|
155
|
+
mode,
|
|
156
|
+
code: String(v?.code || "").trim(),
|
|
157
|
+
org: String(v?.org || "").trim(),
|
|
158
|
+
accountId: String(v?.accountId || "").trim(),
|
|
159
|
+
at: String(v?.at || ""),
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
return { mode: "auto", code: "", org: "", accountId: "", at: "" }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function writeInvitePolicy(siteId, policy) {
|
|
167
|
+
try {
|
|
168
|
+
const f = invitePolicyPath(siteId)
|
|
169
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
170
|
+
const mode = ["auto", "fixed", "custom"].includes(policy?.mode) ? policy.mode : "auto"
|
|
171
|
+
writeFileSync(
|
|
172
|
+
f,
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
mode,
|
|
175
|
+
code: String(policy?.code || "").trim(),
|
|
176
|
+
org: String(policy?.org || "").trim(),
|
|
177
|
+
accountId: String(policy?.accountId || "").trim(),
|
|
178
|
+
at: new Date().toISOString(),
|
|
179
|
+
}),
|
|
180
|
+
)
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 清掉策略 —— 下次登录会重新问一次(「改回去」的唯一入口) */
|
|
185
|
+
export function clearInvitePolicy(siteId) {
|
|
186
|
+
try {
|
|
187
|
+
unlinkSync(invitePolicyPath(siteId))
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ═══ 见过哪些账号的码 ═══════════════════════════════════════════════
|
|
192
|
+
//
|
|
193
|
+
// `rollInvite` 每天签到会给**每个**账号跑一次,顺手把它的码记下来 ——
|
|
194
|
+
// 于是登录时那个「挂在谁名下」的下拉框不用发网络请求就能填满。
|
|
195
|
+
// (prompts 的 options 是同步生成的,等不了 API。)
|
|
196
|
+
|
|
197
|
+
function inviteCodesPath(siteId) {
|
|
198
|
+
return join(cacheDir(), `magicrew-invite-codes-${siteId}.json`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function readAccountCodes(siteId) {
|
|
202
|
+
try {
|
|
203
|
+
const v = JSON.parse(readFileSync(inviteCodesPath(siteId), "utf8"))
|
|
204
|
+
return v && typeof v === "object" ? v : {}
|
|
205
|
+
} catch {
|
|
206
|
+
return {}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function noteAccountCode(siteId, org, code, accountId) {
|
|
211
|
+
const o = String(org || "").trim()
|
|
212
|
+
const cd = String(code || "").trim()
|
|
213
|
+
if (!o || !cd) return
|
|
214
|
+
try {
|
|
215
|
+
const all = readAccountCodes(siteId)
|
|
216
|
+
all[o] = { code: cd, accountId: String(accountId || ""), at: new Date().toISOString() }
|
|
217
|
+
const f = inviteCodesPath(siteId)
|
|
218
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
219
|
+
writeFileSync(f, JSON.stringify(all))
|
|
220
|
+
} catch {}
|
|
221
|
+
}
|
|
222
|
+
|
|
121
223
|
/** 滚动请求最多等这么久 —— 登录流程不能被它拖死 */
|
|
122
224
|
const INVITE_TIMEOUT_MS = 6000
|
|
123
225
|
|
|
@@ -137,7 +239,10 @@ async function rollInvite(siteId, jwt, org, host) {
|
|
|
137
239
|
fetchOwnInviteCode(jwt, org, host),
|
|
138
240
|
new Promise((_, reject) => setTimeout(() => reject(new Error("超时")), INVITE_TIMEOUT_MS)),
|
|
139
241
|
])
|
|
140
|
-
|
|
242
|
+
const code = writeInvite(siteId, own.code)
|
|
243
|
+
// 顺手记下「哪个账号的码是多少」—— 登录时的下拉框靠它填,不用发请求
|
|
244
|
+
noteAccountCode(siteId, org, code)
|
|
245
|
+
return code
|
|
141
246
|
} catch (e) {
|
|
142
247
|
if (process.env.MAGICREW_DEBUG) {
|
|
143
248
|
console.error(`[magicrew] 滚动邀请码失败(${siteId}):${e?.message ?? e}`)
|