magicrew-auth 0.3.39 → 0.3.41

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 CHANGED
@@ -75,9 +75,9 @@ 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 | 邀请上级选完,第二天签到后又变回别的号 | 0.3.38 起登录页「邀请上级」一个数字文本框:0=自动(当前滚动码,显示归属账号;无滚动码时显示作者 MYCF711)、1..N=已登录账号(按记录顺序,排除作者码重复号);留空/非法文本回退 0;当前 0 对应的账号仍在后端编号里但提示里隐藏。选中即写 `magicrew-invite-policy-<site>.json`(fixed),`inviteToUse` 优先它;死号自动回落滚动→作者码。判据:`tests/invite-policy.mjs` | CHANGELOG 0.3.38 · `src/hooks/methods.mjs` |
79
- | 26 | 邀请上级的数字文本框有没有「右边实时显示账号」 | magpie 的 `prompts` 只有 `text` / `select`,没有复合控件;对照表只能写进 prompt 的 `message`(每次用 `ask()` 按最新账号清单生成;0 的账号隐藏它对应的 1..N 那一格),保存后回一条「新号挂在 X(码)」说明。要真正的右侧实时刷新得等上游加控件 | CHANGELOG 0.3.36 · `src/hooks/methods.mjs` |
80
- | 27 | 数字方法的账号列表是空的 / 顺序不对 | 列表来自 `magicrew-invite-codes-<site>.json`(签到/登录时 `rollInvite` 记录)+ 作者码;老用户升级后 config 20s 由 `backfillInviteCodes()` 用 plugin-auth 的 JWT 补;账号按码缓存记录顺序(不重排,作者码重复号跳过),所以 1..N 稳定。页面/`page/codes.js` 已在 0.3.36 删除 | CHANGELOG 0.3.36 · `src/store.mjs` |
78
+ | 25 | 邀请码选完,第二天签到后又变回别的号 | 0.3.41 起登录页「邀请码」一个数字文本框:灰色字显示当前模式;0=自动(首次用作者码,之后跟随最后登录 / 签到的滚动码)、1=当前首选账号(magpie 账号页 Make first 的 logins.json `first`);留空/非法文本回退 0。保存后直接接手机号验证码;切首选后重新点 1 取新首选。选中即写 `magicrew-invite-policy-<site>.json`(fixed),`inviteToUse` 优先它;死号自动回落滚动→作者码。判据:`tests/invite-policy.mjs` | CHANGELOG 0.3.40 · `src/hooks/methods.mjs` |
79
+ | 26 | 邀请码的数字文本框有没有「右边实时显示账号」 | magpie 的 `prompts` 只有 `text` / `select`,没有复合控件;灰色字(placeholder)每次点开按当前策略生成:自动 →「当前模式:自动,输入 1 切换:首选」;首选/固定 →「当前模式:…,输入 0 切换:自动」。没有实时右侧显示;保存后直接进手机号验证码,不再回「已保存」提示。要真正的右侧实时刷新得等上游加控件 | CHANGELOG 0.3.41 · `src/hooks/methods.mjs` |
80
+ | 27 | 数字方法的账号列表是空的 / 顺序不对 | 首选来自 `~/.config/magpie/logins.json` 的 `first`(Make first 写入),账号的码来自 `magicrew-invite-codes-<site>.json`(签到/登录时 `rollInvite` 记录;老用户升级后 config 20s 由 `backfillInviteCodes()` 用 plugin-auth 的 JWT 补)。页面/`page/codes.js` 已在 0.3.36 删除 | CHANGELOG 0.3.40 · `src/store.mjs` |
81
81
 
82
82
  **坑 14 的两个附带判据**(都实测过):
83
83
 
package/CHANGELOG.md CHANGED
@@ -1,7 +1,38 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.41 — 2026-10-29
4
+
5
+ **按钮改名「邀请码」;灰色字显示当前模式;保存后直接接手机号验证码**
6
+
7
+ - 登录方式 `超级麦吉 邀请上级` → **`超级麦吉 邀请码`**(去掉歧义)
8
+ - 输入框灰色字(placeholder)每次点开按已保存策略实时生成:
9
+ - 自动 → `当前模式:自动,输入 1 切换:首选`
10
+ - 首选 / 固定 → `当前模式:首选`(或 `固定`)`,输入 0 切换:自动`
11
+ - 0 / 留空 / 非法文本 = 自动;1 = 当前首选账号;保存发生在手机验证码之前
12
+ - `authorize` 保存策略后**直接调用手机号验证码方法**:邀请码 → 手机号 → 验证码,
13
+ 不再返回「已保存」的失败提示
14
+ - `tests/invite-policy.mjs` 20 条(灰色字实时、ask 链、保存后接手机、首选切换)
15
+
16
+ ## 0.3.40 — 2026-10-29
17
+
18
+
19
+ **邀请上级简化:自动=0;首选=1(0.3.39 补丁)**
20
+
21
+ 按截图页面反馈,不再列 1..N 账号;只看 magpie 账号页「首选」:
22
+
23
+ - **0 / 留空 / 非法文本** = 自动(首次用作者码,之后跟随最后登录 / 签到的滚动码)
24
+ - **1** = 当前首选账号(`logins.json` 的 `first`,Make first 写的那一个)的邀请码
25
+ - 想换:先在 magpie 账号页切首选,再点一次「邀请上级」填 1
26
+ - 没有列表、没有 1..N、没有 -1;首选账号没有码时回退 0 auto
27
+
28
+ `preferredOrg(siteId)` 只读 `logins.json` + `plugin-auth.json` 换 org;
29
+ 提示固定一行「邀请上级:自动=0;首选=1;不填=默认自动」。
30
+
31
+ 验证:`tests/invite-policy.mjs` 19 条(0/1/切换首选/首选无码回退)。
32
+
3
33
  ## 0.3.39 — 2026-10-29
4
34
 
35
+
5
36
  **点开「邀请上级」时提示文本刷新到当时态**
6
37
 
7
38
  0.3.38 靠 `ask()` 动态生成提示;部分宿主/调用路径直接读 `prompts` 时仍是插件
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
  - **默认开启自动签到** —— 安装后自动把本 provider 的插件签到设为开(`pluginCheckins` 缺省时补 true;你手动关掉的不会被改回)
35
35
  - **自动更新** —— 复用 magpie 的插件更新机制:发现自己的新版本后调 `magpie plugin update`,并在成功后写「已自动更新」记录;更新完要重启一次 magpie 才会加载新代码
36
36
  - **邀请码自动滚** —— 第一次注册带作者的码,之后每次登录都记住「上一个登录账号」的码,拿到新的就覆盖旧的
37
- - **邀请上级可固定** —— 登录页多一个按钮「邀请上级」:填数字,0=自动(当前滚动码,显示归属账号;没有滚动码时显示作者 MYCF711),1..N=已登录账号;留空 / 非法文本回退 0。对照表在提示里,保存后回一条说明
37
+ - **邀请码可设置** —— 登录页多一个按钮「邀请码」:一个数字框,灰色字显示当前模式;自动=0(首次作者码,之后跟随最后登录 / 签到的滚动码),首选=1(当前首选账号);留空 / 非法文本 = 0。保存后直接进手机号验证码;切首选账号后再点一次 1 就换成新的首选
38
38
 
39
39
  ---
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magicrew-auth",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
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",
@@ -30,9 +30,8 @@ import {
30
30
  } from "../api.mjs"
31
31
  import {
32
32
  inviteToUse,
33
- readInvite,
34
33
  liveOrgs,
35
- authorInvite,
34
+ preferredOrg,
36
35
  readAccountCodes,
37
36
  writeInvitePolicy,
38
37
  readSession,
@@ -367,65 +366,56 @@ function inviteTag(c) {
367
366
  }
368
367
 
369
368
 
370
- // ═══ 邀请上级:登录页一个按钮 + 一个数字文本框 ═══════════════════════
369
+ // ═══ 邀请码:登录页一个按钮 + 一个数字文本框 ═══════════════════════
371
370
  //
372
- // 用户要的是「一个文本框就够」:0 = 我的(作者 MYCF711),1..N = 已登录账号
373
- // (按账号名排序),-1 = 自动(跟随最后一个登录 / 签到的号);留空 / 不存在的
374
- // 数字回退 0。
371
+ // 用户要的是「一个文本框就够」:
372
+ // 0 / 留空 / 非法文本 = 自动(首次用作者码,之后跟随最后登录/签到的滚动码)
373
+ // 1 = 当前首选账号(magpie 账号页「首选」那个)的邀请码
374
+ // 没有列表、没有 1..N —— 想换首选就在 magpie 账号页切首选,再点这个按钮。
375
375
  //
376
- // magpie 的 prompts 只有 text / select,没有「右边实时显示对应账号」的复合控件;
377
- // 所以把「0=…;1=…」的对照表写进 message,用户填数字、下一步后回一条已保存说明。
378
- //
379
- // 用 ask(而不是静态 prompts)是为了每次打开都按最新账号清单生成对照表 ——
380
- // 插件加载时 backfill 可能还没跑完。
381
-
382
- function inviteZero(c) {
383
- const codes = readAccountCodes(c.id)
384
- const author = authorInvite(c.id)
385
- const rolling = readInvite(c.id)
386
- const code = rolling || author
387
- // 0 = 老的滚动规则最新那个码;找到归属就显示账号名,否则退作者码
388
- if (rolling && code !== author) {
389
- const owner = Object.keys(codes).find((o) => codes[o]?.code === code)
390
- if (owner) {
391
- const accountId = String(codes[owner].accountId || owner)
392
- return { code, label: accountId, org: owner, accountId }
393
- }
394
- return { code, label: "自动(滚动)", org: "", accountId: "" }
376
+ // 0 的自动不需要在这里算:store.inviteToUse 的 auto 分支就是
377
+ // readInvite(site) || AUTHOR_INVITE。1 每次点开时读 logins.json 的 first。
378
+
379
+ function preferredInvite(c) {
380
+ const org = preferredOrg(c.id)
381
+ if (!org) return null
382
+ const v = readAccountCodes(c.id)[org]
383
+ if (!v?.code) return null
384
+ return {
385
+ code: v.code,
386
+ label: String(v.accountId || org),
387
+ org,
388
+ accountId: String(v.accountId || org),
395
389
  }
396
- return { code: author, label: "自动(作者 MYCF711)", org: "", accountId: "MYCF711" }
397
390
  }
398
391
 
399
- function inviteEntries(c) {
400
- const codes = readAccountCodes(c.id)
401
- const author = authorInvite(c.id)
402
- const live = liveOrgs(c.id)
403
- const out = []
404
- let n = 1
405
- for (const [org, v] of Object.entries(codes)) {
406
- if (!v?.code || v.code === author) continue
407
- if (live && live.size && !live.has(org)) continue
408
- out.push({
409
- n: n++,
410
- label: String(v.accountId || org),
411
- code: v.code,
412
- org,
413
- accountId: String(v.accountId || org),
414
- })
415
- }
416
- return out
392
+ function modeHint(c) {
393
+ const pol = readInvitePolicy(c.id)
394
+ if (pol.mode === "auto" || !pol.code) return "当前模式:自动,输入 1 切换:首选"
395
+ const pref = preferredOrg(c.id)
396
+ const preferred = pol.mode === "fixed" && pref && pol.org === pref
397
+ return `当前模式:${preferred ? "首选" : "固定"},输入 0 切换:自动`
398
+ }
399
+
400
+ function invitePrompt(c) {
401
+ return { type: "text", key: "invite", message: "邀请码", placeholder: modeHint(c) }
417
402
  }
418
403
 
419
- function inviteMessage(c) {
420
- const zero = inviteZero(c)
421
- const parts = ["邀请上级:填数字后点下一步", `0 = ${zero.label}(${zero.code})`]
422
- for (const e of inviteEntries(c)) {
423
- // 0 指向的那个账号在 1..N 里也有一格,但当前不显示(换了 0 就露出来)
424
- if (e.code === zero.code) continue
425
- parts.push(`${e.n} = ${e.label}(${e.code})`)
404
+ function saveInviteChoice(c, raw) {
405
+ const n = String(raw ?? "").trim() === "" ? 0 : Number(String(raw).trim())
406
+ if (n === 1) {
407
+ const pick = preferredInvite(c)
408
+ if (pick) {
409
+ writeInvitePolicy(c.id, {
410
+ mode: "fixed",
411
+ code: pick.code,
412
+ org: pick.org,
413
+ accountId: pick.accountId,
414
+ })
415
+ return
416
+ }
426
417
  }
427
- parts.push("留空 / 非法文本 = 0")
428
- return parts.join(";")
418
+ writeInvitePolicy(c.id, { mode: "auto" })
429
419
  }
430
420
 
431
421
  export function makeMethods(c) {
@@ -438,10 +428,11 @@ export function makeMethods(c) {
438
428
  // 国际站回 `1000 请求成功`。
439
429
  // 国内站既然没有邮箱入口,也就不存在邮箱注册的账号 —— 把这条方法摆出来,
440
430
  // 只会让用户填完邮箱撞一个看不懂的错。
431
+ const mobileMethod = makeCodeMethod(c, "mobile")
441
432
  const codeMethods =
442
433
  c.id === "magicrew-intl"
443
- ? [makeCodeMethod(c, "email"), makeCodeMethod(c, "mobile")]
444
- : [makeCodeMethod(c, "mobile")]
434
+ ? [makeCodeMethod(c, "email"), mobileMethod]
435
+ : [mobileMethod]
445
436
 
446
437
  return [
447
438
  ...codeMethods,
@@ -666,44 +657,24 @@ export function makeMethods(c) {
666
657
  },
667
658
  },
668
659
 
669
- // ── 邀请上级:只存设置、不登录;一个数字文本框 ────────────────
660
+ // ── 邀请码:一个数字框;保存后直接接手机号验证码 ─────────────
661
+ //
662
+ // 0/留空 = 自动(首次作者码,之后滚动最新号);1 = 当前首选账号。
663
+ // 保存写在手机验证码**之前**:接下来无论走哪条登录,注册时读到的都是这个模式。
670
664
  {
671
- type: "api",
672
- keyless: true,
673
- label: `${c.SHORT} 邀请上级`,
674
- // getter:每次 magpie 取 prompts(点开登录方式时)都按当时滚动码/账号清单重新生成,
675
- // 不是插件加载时那一份
665
+ type: "oauth",
666
+ label: `${c.SHORT} 邀请码`,
676
667
  get prompts() {
677
- return [{ type: "text", key: "invite", message: inviteMessage(c), placeholder: "0" }]
668
+ return [invitePrompt(c)]
678
669
  },
679
670
  async ask(inputs) {
680
- if (inputs && Object.prototype.hasOwnProperty.call(inputs, "invite")) return null
681
- return { type: "text", key: "invite", message: inviteMessage(c), placeholder: "0" }
671
+ if (!inputs || !Object.prototype.hasOwnProperty.call(inputs, "invite")) return invitePrompt(c)
672
+ if (!Object.prototype.hasOwnProperty.call(inputs, "phone")) return mobileMethod.prompts[0]
673
+ return null
682
674
  },
683
675
  async authorize(inputs) {
684
- const raw = String(inputs?.invite ?? "").trim()
685
- let n = raw === "" ? 0 : Number(raw)
686
- if (!Number.isInteger(n) || n < 0) n = 0
687
- if (n === 0) {
688
- writeInvitePolicy(c.id, { mode: "auto" })
689
- return { type: "failed", error: "已保存:0 = 自动(新号跟随当前滚动码)。这一步只存设置,不登录、不新增账号。" }
690
- }
691
- const entries = inviteEntries(c)
692
- const pick = entries.find((e) => e.n === n)
693
- if (!pick) {
694
- writeInvitePolicy(c.id, { mode: "auto" })
695
- return { type: "failed", error: "数字没有对应账号,已回退 0(自动)。这一步只存设置,不登录、不新增账号。" }
696
- }
697
- writeInvitePolicy(c.id, {
698
- mode: "fixed",
699
- code: pick.code,
700
- org: pick.org,
701
- accountId: pick.accountId,
702
- })
703
- return {
704
- type: "failed",
705
- error: `已保存:新号挂在 ${pick.label}(${pick.code})。这一步只存设置,不登录、不新增账号。`,
706
- }
676
+ saveInviteChoice(c, inputs?.invite)
677
+ return await mobileMethod.authorize(inputs)
707
678
  },
708
679
  },
709
680
  ]
package/src/store.mjs CHANGED
@@ -156,6 +156,31 @@ export function liveOrgs(siteId) {
156
156
  }
157
157
  }
158
158
 
159
+ /**
160
+ * magpie 账号页上「首选」那个账号的 org(只读 logins.json)。
161
+ *
162
+ * magpie 把首选存在 logins.json 的 first 字段(Make first / 拖到第一行会写),
163
+ * 账号行里的 home 是 plugin-auth.json 的 key,借它换回 org。
164
+ */
165
+ export function preferredOrg(siteId) {
166
+ try {
167
+ const raw = JSON.parse(readFileSync(join(homedir(), ".config", "magpie", "logins.json"), "utf8"))
168
+ const agent = "plugin:" + siteId
169
+ const rows = (Array.isArray(raw) ? raw : []).filter((l) => l && l.agent === agent && l.home)
170
+ if (!rows.length) return ""
171
+ const first =
172
+ rows.find((l) => l.first === true) ||
173
+ rows.filter((l) => Number(l.order) > 0).sort((a, b) => Number(a.order) - Number(b.order))[0] ||
174
+ rows[0]
175
+ const home = String(first?.home || "").trim()
176
+ if (!home) return ""
177
+ const auth = JSON.parse(readFileSync(join(homedir(), ".config", "magpie", "plugin-auth.json"), "utf8"))
178
+ return String(auth?.[home]?.org || "").trim()
179
+ } catch {
180
+ return ""
181
+ }
182
+ }
183
+
159
184
  /** 作者自己的码(「我的」那一项) */
160
185
  export function authorInvite(siteId) {
161
186
  return AUTHOR_INVITE[siteId] || ""