magicrew-auth 0.3.31 → 0.3.33

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.
@@ -0,0 +1,53 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>超级麦吉设置</title>
6
+ <script src="_magpie/bridge.js"></script>
7
+ <style>
8
+ :root { color-scheme: light dark; }
9
+ body {
10
+ margin: 0; padding: 16px 18px 28px;
11
+ font: 13px/1.5 -apple-system, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
12
+ background: var(--magpie-bg, #1b1b1d); color: var(--magpie-fg, #e6e6e6);
13
+ }
14
+ h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
15
+ .sub { opacity: .6; margin: 0 0 16px; font-size: 12px; }
16
+ section { border: 1px solid rgba(128,128,128,.25); border-radius: 10px; padding: 12px 14px; margin-bottom: 14px; }
17
+ h2 { font-size: 13px; font-weight: 600; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
18
+ .now { font-size: 12px; opacity: .8; margin-bottom: 10px; }
19
+ .now b { font-weight: 600; opacity: 1; }
20
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
21
+ button.chip {
22
+ font: inherit; padding: 5px 10px; border-radius: 999px; cursor: pointer;
23
+ border: 1px solid rgba(128,128,128,.35); background: transparent; color: inherit;
24
+ }
25
+ button.chip:hover { background: rgba(128,128,128,.16); }
26
+ button.chip[aria-pressed="true"] { border-color: var(--magpie-accent, #6b8afd); background: rgba(107,138,253,.18); }
27
+ .custom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
28
+ input[type="text"] {
29
+ font: inherit; padding: 6px 9px; border-radius: 8px; min-width: 190px;
30
+ border: 1px solid rgba(128,128,128,.35); background: rgba(128,128,128,.08); color: inherit;
31
+ }
32
+ button.save {
33
+ font: inherit; padding: 6px 12px; border-radius: 8px; cursor: pointer;
34
+ border: 1px solid var(--magpie-accent, #6b8afd); background: rgba(107,138,253,.22); color: inherit;
35
+ }
36
+ .hint { font-size: 12px; opacity: .55; margin-top: 8px; }
37
+ .lock { display: flex; align-items: center; gap: 6px; font-size: 12px; margin-top: 10px; opacity: .85; }
38
+ #status { position: sticky; bottom: 0; font-size: 12px; padding: 8px 0 0; min-height: 18px; }
39
+ #status.ok { color: #5ec27d; }
40
+ #status.bad { color: #f2777a; }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <h1>超级麦吉设置</h1>
45
+ <p class="sub">新账号的邀请上级在这里设置。改完立即生效,不需要重新登录。</p>
46
+
47
+ <div id="app"></div>
48
+ <div id="status"></div>
49
+
50
+ <script src="codes.js"></script>
51
+ <script src="app.js"></script>
52
+ </body>
53
+ </html>
@@ -15,10 +15,10 @@
15
15
  // ⚠ 独立 profile 意味着用户要在这个窗口里登一次(没有他日常浏览器的 cookie)。
16
16
  // **每次登录一个一次性 profile,用完删** —— 为什么不能复用,见下面 newProfileDir()。
17
17
 
18
- import { spawn } from "node:child_process"
18
+ import { spawn, execFileSync } from "node:child_process"
19
19
  import { existsSync, readFileSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"
20
20
  import { homedir } from "node:os"
21
- import { join } from "node:path"
21
+ import { join, basename } from "node:path"
22
22
 
23
23
  const BASE = () => join(homedir(), ".cache", "magpie")
24
24
 
@@ -264,7 +264,27 @@ async function waitPort(dir, timeoutMs = 8_000, aborted = null) {
264
264
  * @param {{candidates?: {name:string,path:string}[]}} [opts] 测试注入用
265
265
  * @returns {Promise<{port:number, dir:string, child:import("node:child_process").ChildProcess, name:string, exe:string}>}
266
266
  */
267
+ /** 一次失败后就别再拉:冷却期内直接走手动登录,免得每次点都弹一堆窗口 */
268
+ let cdpDownUntil = 0
269
+ const CDP_COOLDOWN_MS = 30 * 60_000
270
+
271
+ /**
272
+ * 拉起一次登录窗口。
273
+ *
274
+ * ⚠ 2026-10-29 实测(用户机器,Chrome 155 + 已有一个日常 Chrome 在跑):
275
+ * 按原来那套参数 spawn 出去,浏览器把页面**转交给已运行实例**后自己 exit 0,
276
+ * 既不写 DevToolsActivePort,也不关它开出来的窗口 —— 而原代码会
277
+ * 「把所有候选浏览器都试一遍」,于是点一次「邮箱验证码登录」就弹好几个窗口。
278
+ *
279
+ * 参数集参照 GoogleChrome/chrome-launcher 的 DEFAULT_FLAGS(官方给自动化工具用的那套),
280
+ * 把扩展 / 组件更新 / 同步 / 首次运行向导 / 后台网络全关掉,
281
+ * 再补 `--remote-allow-origins=*`(新版 Chrome 连 CDP 的 WebSocket 要它)。
282
+ */
267
283
  export async function launchLoginBrowser(url, { candidates } = {}) {
284
+ // 冷却期内不再 spawn —— 本机接管不了就是接管不了,反复试只会反复弹窗
285
+ if (Date.now() < cdpDownUntil) {
286
+ throw new Error("本机浏览器不接受自动接管(刚才失败过,30 分钟内不再尝试),直接用手动登录")
287
+ }
268
288
  const list = (candidates ?? browserCandidates()).filter((c) => {
269
289
  try {
270
290
  return c?.path && existsSync(c.path)
@@ -279,21 +299,20 @@ export async function launchLoginBrowser(url, { candidates } = {}) {
279
299
  // 先扫掉上一次登录留下的临时 profile(含历史复用目录)—— 只做一次
280
300
  sweepProfiles()
281
301
 
282
- const failures = []
283
- for (const cand of list) {
284
- let session = null
285
- try {
286
- session = await launchOne(cand, url)
287
- return session
288
- } catch (e) {
289
- failures.push(`${cand.name}:${String(e?.message ?? e).slice(0, 160)}`)
290
- // launchOne 自己已经收拾过失败的进程和目录,这里只管继续试下一个
291
- }
302
+ // ⚠ 只试**第一个**能用的浏览器。
303
+ // 每个候选失败都会留下一个收不回来的窗口(浏览器把自己 exit 掉、窗口归已运行实例),
304
+ // 试 N 个就弹 N 个 —— 这正是「一下子弹出很多个浏览器」的来源。
305
+ // 第一个不行就交给调用方走手动登录,别再用窗口堆失败。
306
+ const cand = list[0]
307
+ try {
308
+ return await launchOne(cand, url)
309
+ } catch (e) {
310
+ // 记下这次失败:30 分钟内不再自动拉浏览器(想提前恢复就重启 magpie)
311
+ cdpDownUntil = Date.now() + CDP_COOLDOWN_MS
312
+ throw new Error(
313
+ `没能自己接管浏览器(${cand.name}:${String(e?.message ?? e).slice(0, 200)})`,
314
+ )
292
315
  }
293
-
294
- throw new Error(
295
- `试过 ${list.length} 个浏览器都没能打开调试通道:\n` + failures.map((f) => ` · ${f}`).join("\n"),
296
- )
297
316
  }
298
317
 
299
318
  /**
@@ -323,10 +342,28 @@ async function launchOne(cand, url) {
323
342
  [
324
343
  `--user-data-dir=${dir}`,
325
344
  "--remote-debugging-port=0",
345
+ // 新版 Chrome 连 CDP 的 WebSocket 要放行来源,否则 /json/* 能取到、WebSocket 连不上
346
+ "--remote-allow-origins=*",
326
347
  "--no-first-run",
327
348
  "--no-default-browser-check",
328
- "--disable-features=Translate,MediaRouter",
349
+ // ═══ 以下照抄 GoogleChrome/chrome-launcher 的 DEFAULT_FLAGS(只留跨平台、有意义的)═══
350
+ "--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4",
351
+ "--disable-extensions",
352
+ "--disable-component-extensions-with-background-pages",
329
353
  "--disable-background-networking",
354
+ "--disable-component-update",
355
+ "--disable-client-side-phishing-detection",
356
+ "--disable-sync",
357
+ "--metrics-recording-only",
358
+ "--disable-default-apps",
359
+ "--mute-audio",
360
+ "--disable-backgrounding-occluded-windows",
361
+ "--disable-renderer-backgrounding",
362
+ "--disable-background-timer-throttling",
363
+ "--disable-hang-monitor",
364
+ "--disable-prompt-on-repost",
365
+ "--disable-domain-reliability",
366
+ "--password-store=basic",
330
367
  url,
331
368
  ],
332
369
  // 某些国产浏览器不接受部分参数会直接退出,stdio 必须 ignore,
@@ -334,7 +371,7 @@ async function launchOne(cand, url) {
334
371
  { stdio: "ignore", detached: false, windowsHide: false },
335
372
  )
336
373
  } catch (e) {
337
- await cleanupFailed({ dir })
374
+ await cleanupFailed({ dir, exe: cand.path })
338
375
  throw new Error(`进程起不来:${e?.message ?? e}`)
339
376
  }
340
377
 
@@ -347,7 +384,7 @@ async function launchOne(cand, url) {
347
384
 
348
385
  const port = await waitPort(dir, 8_000, () => exited !== null)
349
386
  if (!port) {
350
- await cleanupFailed({ dir, child })
387
+ await cleanupFailed({ dir, child, exe: cand.path })
351
388
  throw new Error(
352
389
  exited !== null
353
390
  ? `进程启动后退出了(code ${exited}),没写出调试端口`
@@ -358,10 +395,50 @@ async function launchOne(cand, url) {
358
395
  }
359
396
 
360
397
  /** 失败收尾:杀进程 + 删目录,都不抛错 */
361
- async function cleanupFailed({ dir, child }) {
398
+ /**
399
+ * Windows:按 `--user-data-dir` 精确杀进程。
400
+ *
401
+ * ⚠ 为什么不能只 kill(child):实测浏览器会「父进程 exit 0、真正的窗口挂在另一批
402
+ * 子进程上」,child 早就没了,kill 落空 —— 于是每失败一次就多一个关不掉的窗口
403
+ * (用户看到的就是「一下子弹出很多个浏览器」)。
404
+ * profile 目录是一次性的、带随机后缀,按它匹配进程命令行既精确又不会误伤日常浏览器。
405
+ */
406
+ function killByProfileDir(dir, exePath) {
407
+ if (process.platform !== "win32" || !dir) return 0
408
+ const name = basename(String(exePath || "chrome.exe")).replace(/'/g, "''")
409
+ const pattern = String(dir).replace(/'/g, "''")
362
410
  try {
363
- child?.kill()
364
- } catch {}
411
+ execFileSync(
412
+ "powershell",
413
+ [
414
+ "-NoProfile",
415
+ "-NonInteractive",
416
+ "-Command",
417
+ `$p = Get-CimInstance Win32_Process -Filter "Name='${name}'" | Where-Object { $_.CommandLine -like '*${pattern}*' }; ` +
418
+ `$p | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; $p.Count`,
419
+ ],
420
+ { stdio: "ignore", timeout: 10_000 },
421
+ )
422
+ return 1
423
+ } catch {
424
+ return 0
425
+ }
426
+ }
427
+
428
+ async function cleanupFailed({ dir, child, exe }) {
429
+ // 先按 profile 目录收(这一批才是真正开着窗口的进程),再补一刀 child 树
430
+ killByProfileDir(dir, exe)
431
+ try {
432
+ if (child?.pid && process.platform === "win32") {
433
+ execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" })
434
+ } else {
435
+ child?.kill()
436
+ }
437
+ } catch {
438
+ try {
439
+ child?.kill()
440
+ } catch {}
441
+ }
365
442
  if (!dir) return
366
443
  try {
367
444
  rmSync(dir, { recursive: true, force: true, maxRetries: 2, retryDelay: 150 })
@@ -14,7 +14,7 @@
14
14
  // ⚠ 不要在这里读流、不要手拼 x-api-key ——
15
15
  // 流由 magpie 处理,头由 SDK 包自动加。见 README「最小交付面」。
16
16
 
17
- import { ensureKey, accountOf, keyCandidates, adoptKey } from "../keys.mjs"
17
+ import { ensureKey, accountOf, keyCandidates, adoptKey, renewViaStore } from "../keys.mjs"
18
18
 
19
19
  /**
20
20
  * 上游「这个账号不行」的两种形态(2026-10-29 实测):
@@ -484,6 +484,28 @@ export async function rewriteCreditRefusal(res) {
484
484
  })
485
485
  }
486
486
 
487
+ /**
488
+ * 上游(Swow/PHP)对请求体有 ~8MB 的硬上限(实测 6MB 过、8MB 拒),
489
+ * 超了直接回 413 的 HTML 页 —— magpie 只能显示成难懂的
490
+ * 「a web page, not an API reply」。翻译成人话,并说明换账号没用。
491
+ */
492
+ async function rewriteTooLarge(res, SHORT) {
493
+ if (!res || res.status !== 413) return res
494
+ const headers = new Headers(res.headers)
495
+ headers.delete("content-length")
496
+ headers.delete("content-encoding")
497
+ headers.set("content-type", "application/json")
498
+ const body = JSON.stringify({
499
+ error: {
500
+ message:
501
+ `${SHORT}: 请求体超过上游上限(约 8 MB)—— 会话上下文过大或含大截图。` +
502
+ `压缩上下文(如 /compact)或新开会话后重试;换账号无效。`,
503
+ type: "payload_too_large",
504
+ },
505
+ })
506
+ return new Response(body, { status: 413, statusText: "Payload Too Large", headers })
507
+ }
508
+
487
509
  export function makeLoader(c) {
488
510
  /** 已经写回 store 的 key —— 避免每个请求都去读一次 store */
489
511
  const adopted = new Set()
@@ -554,7 +576,7 @@ export function makeLoader(c) {
554
576
  adopted.add(cand.apiKey)
555
577
  adoptKey(c, fresh, cand.apiKey, cand.expiresAt)
556
578
  }
557
- return rewriteCreditRefusal(res)
579
+ return rewriteTooLarge(await rewriteCreditRefusal(res), c.SHORT)
558
580
  }
559
581
  // 这把被拒:响应不再使用,显式取消 body —— 否则逐把试时
560
582
  // 每条死连接的响应体都挂着,等 GC 才释放。
@@ -563,6 +585,22 @@ export function makeLoader(c) {
563
585
  } catch {}
564
586
  }
565
587
 
588
+ // 全被拒 —— 很可能是 key 被别处顶掉(同一账号在别处又登了一次,服务端换发)。
589
+ // 手里有 refresh_token 就续一把、再给一次机会:这是唯一能从 403 自愈的路,
590
+ // 否则这把 key 会一直「没过期但已失效」,直到 30 分钟 rest 结束还是它。
591
+ try {
592
+ if (accountOf(fresh) !== "default") {
593
+ const renewed = await renewViaStore(c, fresh)
594
+ if (renewed?.apiKey) {
595
+ const res = await sendWith(renewed.apiKey)
596
+ if (!(await isKeyRejected(res))) {
597
+ adoptKey(c, fresh, renewed.apiKey, renewed.expiresAt)
598
+ return rewriteTooLarge(await rewriteCreditRefusal(res), c.SHORT)
599
+ }
600
+ }
601
+ }
602
+ } catch {}
603
+
566
604
  // 这个账号的 key 全被拒。上游给的是 403「token无效」,magpie 会判成
567
605
  // 「不可重试」直接抛给客户端(ZCode 显示 auth_failed / token无效)。
568
606
  // 改写成 402:magpie 的 failure(402) 是 failCredit → 把这个账号 rest 掉、
@@ -28,7 +28,17 @@ import {
28
28
  fetchWho,
29
29
  fetchOwnInviteCode,
30
30
  } from "../api.mjs"
31
- import { inviteToUse, readSession, writeSession, noteLoginFailure, noteOrg, readKnownOrgs, rollInvite } from "../store.mjs"
31
+ import {
32
+ inviteToUse,
33
+ liveOrgs,
34
+ readSession,
35
+ writeSession,
36
+ noteLoginFailure,
37
+ noteOrg,
38
+ readKnownOrgs,
39
+ rollInvite,
40
+ readInvitePolicy,
41
+ } from "../store.mjs"
32
42
  import { readLocalCreds } from "../local-browser.mjs"
33
43
  import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
34
44
  import { readVisionModel } from "../vision.mjs"
@@ -144,7 +154,44 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
144
154
  * @param {object} c 站点闭包(id / SHORT / web / host)
145
155
  * @param {"mobile"|"email"} kind
146
156
  */
147
- function makeCodeMethod(c, kind) {
157
+ /** org 已不在 magpie 的账号文件里(= 账号被删);文件读不到时按「不知道」处理 */
158
+ function orgDead(c, org) {
159
+ if (!org) return false
160
+ const live = liveOrgs(c.id)
161
+ return Boolean(live && live.size && !live.has(org))
162
+ }
163
+
164
+ /**
165
+ * 页面 options 里的邀请设置(inviteBySite[siteId]),带死号校验。
166
+ * · auto → 显式覆盖文件策略,走滚动 / 作者码
167
+ * · fixed 指向死号 → 降级成 auto(页面已提示,登录侧自动回退)
168
+ * · fixed/custom 活 → 用它
169
+ * 没设置(null)才回落文件策略。
170
+ */
171
+ function optionInvite(c, getOpts) {
172
+ try {
173
+ const per = getOpts?.()?.inviteBySite?.[c.id]
174
+ if (!per || typeof per !== "object") return null
175
+ if (per.mode === "auto") return { mode: "auto", code: "", org: "", accountId: "" }
176
+ if ((per.mode === "fixed" || per.mode === "custom") && String(per.code || "").trim()) {
177
+ if (per.mode === "fixed" && orgDead(c, per.org)) {
178
+ return { mode: "auto", code: "", org: "", accountId: "" }
179
+ }
180
+ return per
181
+ }
182
+ } catch {}
183
+ return null
184
+ }
185
+
186
+ export function inviteFromOptions(c, getOpts, getLock = () => true) {
187
+ const per = optionInvite(c, getOpts)
188
+ if (!per) return null
189
+ // 页面上的「锁定」关掉 → 页面选中的码也不作数,回到滚动 / 作者码
190
+ if (getLock() === false || per.mode === "auto") return inviteToUse(c.id, false)
191
+ return per.code
192
+ }
193
+
194
+ function makeCodeMethod(c, kind, getLock = () => true, getOpts = () => ({})) {
148
195
  const isEmail = kind === "email"
149
196
  const what = isEmail ? "邮箱" : "手机号"
150
197
  /** 官方登录页上那个 tab 的名字 —— 回退到浏览器时要说清楚点哪个 */
@@ -168,7 +215,7 @@ function makeCodeMethod(c, kind) {
168
215
 
169
216
  return {
170
217
  type: "oauth",
171
- label: `${c.SHORT} ${what}验证码`,
218
+ label: `${c.SHORT} ${what}验证码${inviteTag(c, getLock, getOpts)}`,
172
219
  prompts: [
173
220
  isEmail
174
221
  ? {
@@ -199,7 +246,7 @@ function makeCodeMethod(c, kind) {
199
246
  // ⚠ 无头这条路**没有**「打开带邀请码的页面」这一步,所以邀请码只能靠
200
247
  // `redirect` 传 —— 网页表单的初始值里 redirect 就是 window.location.href,
201
248
  // 服务端从它上面取 `?invitation=`。少了它,新注册的号就没有邀请关系。
202
- const inviteCode = inviteToUse(c.id)
249
+ const inviteCode = inviteFromOptions(c, getOpts, getLock) ?? inviteToUse(c.id, getLock())
203
250
  const loginUrl = inviteCode
204
251
  ? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
205
252
  : `${c.web}/login`
@@ -330,7 +377,47 @@ function makeCodeMethod(c, kind) {
330
377
  }
331
378
  }
332
379
 
333
- export function makeMethods(c) {
380
+ // ═══ 邀请上级:选择 / 自定义 ═════════════════════════════════════════
381
+ //
382
+ // 默认行为(auto)是「用最后一个登录 / 签到的号的码」,但它会漂移 ——
383
+ // 每天 00:00 签到最后跑的那个号会把文件覆盖掉。这里让用户注册时就能定死。
384
+ //
385
+ // 界面(magpie 的 prompts 只有 select / text 两种控件,没有复合控件):
386
+ // · select = 展开的按钮列表:我的(作者 MYCF711)→ 自动 → 已登录账号
387
+ // · text = 自定义码
388
+ // · 锁定 = provider 的 auth.options 开关(真正的开关,magpie 设置页可见)
389
+ //
390
+ // 为什么不做「一个文本框 + 展开按钮」:prompts 没有把两者合成一个控件的口子;
391
+ // 硬拼要写插件页面/面板,而页面读不到账号邀请码(PageReads 只给
392
+ // usage:summary / providers:list,不含账号)。两个单步入口最稳。
393
+ //
394
+ // 选项靠 `magicrew-invite-codes-<site>.json` 填 —— 那是签到 / 登录时
395
+ // `rollInvite` 顺手记下来的(每个账号每天都会跑一次签到,所以不会过期)。
396
+ // 这里**不能**发请求:prompts 的 options 是同步生成的。
397
+
398
+ /**
399
+ * 面板标题后缀 —— 让「当前挂在谁名下」在选登录方式那一屏就能看见。
400
+ * (那张面板只能显示 methods 的 label,插件没有别的可写位置。)
401
+ *
402
+ * ⚠ 值在插件加载时算一次;改完设置要重启 magpie 才会刷新标题。
403
+ * 设置本身是立即生效的,标题只是个提示。
404
+ */
405
+ function inviteTag(c, getLock = () => true, getOpts = () => ({})) {
406
+ if (getLock() === false) return ""
407
+ // 页面里选过就按页面的走 —— 与登录时真正用的是同一个来源(含死号校验)
408
+ const per = optionInvite(c, getOpts)
409
+ if (per) {
410
+ if (per.mode === "auto") return ""
411
+ return "(上级 " + (per.accountId || per.org || per.code) + ")"
412
+ }
413
+ const pol = readInvitePolicy(c.id)
414
+ if ((pol.mode === "fixed" || pol.mode === "custom") && pol.code && !orgDead(c, pol.org)) {
415
+ return `(上级 ${pol.accountId || pol.code})`
416
+ }
417
+ return ""
418
+ }
419
+
420
+ export function makeMethods(c, getLock = () => true, getOpts = () => ({})) {
334
421
  // 顺序 = magpie 登录方式列表里的顺序。
335
422
  //
336
423
  // ⚠ **邮箱验证码只有国际站有**,国内站不给。实测两件事:
@@ -342,18 +429,18 @@ export function makeMethods(c) {
342
429
  // 只会让用户填完邮箱撞一个看不懂的错。
343
430
  const codeMethods =
344
431
  c.id === "magicrew-intl"
345
- ? [makeCodeMethod(c, "email"), makeCodeMethod(c, "mobile")]
346
- : [makeCodeMethod(c, "mobile")]
432
+ ? [makeCodeMethod(c, "email", getLock, getOpts), makeCodeMethod(c, "mobile", getLock, getOpts)]
433
+ : [makeCodeMethod(c, "mobile", getLock, getOpts)]
347
434
 
348
435
  return [
349
436
  ...codeMethods,
350
437
 
351
438
  {
352
439
  type: "oauth",
353
- label: `${c.SHORT} 浏览器登录`,
354
- async authorize() {
440
+ label: `${c.SHORT} 浏览器登录${inviteTag(c, getLock, getOpts)}`,
441
+ async authorize(inputs) {
355
442
  // 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份
356
- const inviteCode = inviteToUse(c.id)
443
+ const inviteCode = inviteFromOptions(c, getOpts, getLock) ?? inviteToUse(c.id, getLock())
357
444
  const url = inviteCode
358
445
  ? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
359
446
  : `${c.web}/login`
@@ -26,6 +26,12 @@
26
26
 
27
27
  import { fetchPoints, SITES } from "../api.mjs"
28
28
  import { ensureCheckinDefault } from "../auto-maintenance.mjs"
29
+ import { syncInvitePage } from "../store.mjs"
30
+
31
+ // 页面清单的节流心跳:magpie 会定期轮 usage,借这一分钟一次的时机
32
+ // 把「有哪些账号、各自的码」同步给插件页面 —— 账号增删后页面自己跟上,
33
+ // 不用等下次签到 / 登录。
34
+ let lastPageSync = 0
29
35
 
30
36
  // 每日登录积分:国内 +200/天、国际 +50/天(实测,见 docs/internals.md)。
31
37
  // 今日批次的分母,只用于展示「今日还剩 / 今日额度」。
@@ -42,6 +48,12 @@ export function makeUsage(c) {
42
48
  ensureCheckinDefault(c.PROVIDER)
43
49
  } catch {}
44
50
  }
51
+ if (Date.now() - lastPageSync > 60_000) {
52
+ lastPageSync = Date.now()
53
+ try {
54
+ syncInvitePage()
55
+ } catch {}
56
+ }
45
57
  const auth = await getAuth()
46
58
  if (!auth) return { error: "未登录", windows: [] }
47
59
  try {