magicrew-auth 0.3.35 → 0.3.37
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 +3 -3
- package/CHANGELOG.md +1703 -1669
- package/README.md +307 -307
- package/index.mjs +4 -32
- package/package.json +2 -10
- package/src/hooks/methods.mjs +675 -658
- package/src/hooks/usage.mjs +152 -164
- package/src/store.mjs +1 -106
- package/page/app.js +0 -252
- package/page/index.html +0 -53
package/src/hooks/usage.mjs
CHANGED
|
@@ -1,164 +1,152 @@
|
|
|
1
|
-
// ⑤ auth.usage(getAuth) —— 额度
|
|
2
|
-
//
|
|
3
|
-
// 超级麦吉的积分是「预付钱包」:`/api/v1/quota/points/total` 只回当前余额
|
|
4
|
-
// `total_points` 和一批会过期的明细(`expiring_quota_details`),没有总量、
|
|
5
|
-
// 没有已消耗;积分流水接口恒 500。钱包没有分母,不能编造 used/limit 百分比。
|
|
6
|
-
//
|
|
7
|
-
// magpie 0.1.1790(issue #3950)起,窗口可以直接报余额:
|
|
8
|
-
//
|
|
9
|
-
// windows: [{
|
|
10
|
-
// name: "积分余额",
|
|
11
|
-
// balance: 6291, // 还剩多少,单位见 unit
|
|
12
|
-
// unit: "积分",
|
|
13
|
-
// // 可选:runsOutAt / runsOut / display / model / models / notModels
|
|
14
|
-
// }]
|
|
15
|
-
//
|
|
16
|
-
// 不写 used / limit / aside —— 宿主把余额当读数额度,不画百分比;同 provider、
|
|
17
|
-
// 同单位的账号按余额从多到少排,余额 0 视为用完。`runsOutAt` 取最早过期的
|
|
18
|
-
// 一批,让 smart 路由知道谁的积分先失效。
|
|
19
|
-
//
|
|
20
|
-
// 钱包只有一个总数,但过期批次有先后:今日 23:59 到期的每日积分、其他限时
|
|
21
|
-
// 批次、永久积分要看得见。它们在余额窗口之后作为 `aside` 文本行显示(只展示,
|
|
22
|
-
// 不进 Allowance.Left —— 否则最小的那一条会把总余额压低)。
|
|
23
|
-
//
|
|
24
|
-
// 不要再把「今日 / 限时 / 永久」做成带 used/limit 的假百分比窗口;旧写法是
|
|
25
|
-
// 宿主支持余额之前的临时协议,见 CHANGELOG 0.3.20。
|
|
26
|
-
|
|
27
|
-
import { fetchPoints, SITES } from "../api.mjs"
|
|
28
|
-
import { ensureCheckinDefault } from "../auto-maintenance.mjs"
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/** 北京时间的日期键 `YYYY-MM-DD`(服务端的时间都是 UTC+8) */
|
|
154
|
-
function beijingDay(ms) {
|
|
155
|
-
if (ms === null) return ""
|
|
156
|
-
return new Date(ms + 8 * 3600e3).toISOString().slice(0, 10)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function beijingDate(ms) {
|
|
160
|
-
return beijingDay(ms)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const sum = (list) => list.reduce((n, b) => n + Number(b?.points ?? 0), 0)
|
|
164
|
-
const fmt = (n) => Number(n).toLocaleString("zh-CN")
|
|
1
|
+
// ⑤ auth.usage(getAuth) —— 额度
|
|
2
|
+
//
|
|
3
|
+
// 超级麦吉的积分是「预付钱包」:`/api/v1/quota/points/total` 只回当前余额
|
|
4
|
+
// `total_points` 和一批会过期的明细(`expiring_quota_details`),没有总量、
|
|
5
|
+
// 没有已消耗;积分流水接口恒 500。钱包没有分母,不能编造 used/limit 百分比。
|
|
6
|
+
//
|
|
7
|
+
// magpie 0.1.1790(issue #3950)起,窗口可以直接报余额:
|
|
8
|
+
//
|
|
9
|
+
// windows: [{
|
|
10
|
+
// name: "积分余额",
|
|
11
|
+
// balance: 6291, // 还剩多少,单位见 unit
|
|
12
|
+
// unit: "积分",
|
|
13
|
+
// // 可选:runsOutAt / runsOut / display / model / models / notModels
|
|
14
|
+
// }]
|
|
15
|
+
//
|
|
16
|
+
// 不写 used / limit / aside —— 宿主把余额当读数额度,不画百分比;同 provider、
|
|
17
|
+
// 同单位的账号按余额从多到少排,余额 0 视为用完。`runsOutAt` 取最早过期的
|
|
18
|
+
// 一批,让 smart 路由知道谁的积分先失效。
|
|
19
|
+
//
|
|
20
|
+
// 钱包只有一个总数,但过期批次有先后:今日 23:59 到期的每日积分、其他限时
|
|
21
|
+
// 批次、永久积分要看得见。它们在余额窗口之后作为 `aside` 文本行显示(只展示,
|
|
22
|
+
// 不进 Allowance.Left —— 否则最小的那一条会把总余额压低)。
|
|
23
|
+
//
|
|
24
|
+
// 不要再把「今日 / 限时 / 永久」做成带 used/limit 的假百分比窗口;旧写法是
|
|
25
|
+
// 宿主支持余额之前的临时协议,见 CHANGELOG 0.3.20。
|
|
26
|
+
|
|
27
|
+
import { fetchPoints, SITES } from "../api.mjs"
|
|
28
|
+
import { ensureCheckinDefault } from "../auto-maintenance.mjs"
|
|
29
|
+
|
|
30
|
+
// 每日登录积分:国内 +200/天、国际 +50/天(实测,见 docs/internals.md)。
|
|
31
|
+
// 今日批次的分母,只用于展示「今日还剩 / 今日额度」。
|
|
32
|
+
const DAILY_LOGIN_GIFT = { cn: 200, intl: 50 }
|
|
33
|
+
|
|
34
|
+
export function makeUsage(c) {
|
|
35
|
+
let ensuredCheckin = false
|
|
36
|
+
return async (getAuth) => {
|
|
37
|
+
// config 已经补过默认签到;这里再兜一次 —— magpie 启动时可能先
|
|
38
|
+
// Save 了内存里的旧 settings,把 config 那次覆盖掉。只补「从未设置过」的。
|
|
39
|
+
if (!ensuredCheckin) {
|
|
40
|
+
ensuredCheckin = true
|
|
41
|
+
try {
|
|
42
|
+
ensureCheckinDefault(c.PROVIDER)
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
const auth = await getAuth()
|
|
46
|
+
if (!auth) return { error: "未登录", windows: [] }
|
|
47
|
+
try {
|
|
48
|
+
const p = await fetchPoints(auth.jwt, auth.org, c.host)
|
|
49
|
+
const total = Number(p?.total ?? 0)
|
|
50
|
+
if (!Number.isFinite(total)) return { error: "积分接口没有返回可用余额", windows: [] }
|
|
51
|
+
const batches = Array.isArray(p?.expiring) ? p.expiring : []
|
|
52
|
+
const windows = [{
|
|
53
|
+
name: "积分余额",
|
|
54
|
+
balance: total,
|
|
55
|
+
unit: "积分",
|
|
56
|
+
display: total > 0 ? `${fmt(total)} 积分` : "已耗尽",
|
|
57
|
+
...(total > 0 ? runsOutOf(batches) : {}),
|
|
58
|
+
}]
|
|
59
|
+
|
|
60
|
+
if (total > 0) {
|
|
61
|
+
const today = beijingDay(Date.now())
|
|
62
|
+
const todayBatches = batches.filter((b) => b.points > 0 && beijingDay(parseServerTime(b.expiresAt)) === today)
|
|
63
|
+
const todayLeft = sum(todayBatches)
|
|
64
|
+
const timedLeft = sum(batches)
|
|
65
|
+
const timedRest = Math.max(0, timedLeft - todayLeft)
|
|
66
|
+
const permanent = Math.max(0, total - timedLeft)
|
|
67
|
+
const dailyCap = DAILY_LOGIN_GIFT[c.host === SITES.cn.host ? "cn" : "intl"]
|
|
68
|
+
|
|
69
|
+
// 只展示的批次行:没有 used/limit/balance,宿主把它们当 aside 文本,
|
|
70
|
+
// 不参与 Allowance.Left;路由仍只看上面的总余额。
|
|
71
|
+
if (todayLeft > 0) {
|
|
72
|
+
windows.push({
|
|
73
|
+
name: "今日积分",
|
|
74
|
+
display: `${fmt(todayLeft)} / ${fmt(dailyCap)} 积分 · 今日 23:59 到期`,
|
|
75
|
+
unit: "积分",
|
|
76
|
+
aside: true,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
if (timedRest > 0) {
|
|
80
|
+
const next = pickEarliest(batches.filter((b) => !todayBatches.includes(b)))
|
|
81
|
+
const when = next ? ` · 最早 ${beijingDate(next.ms)} 到期` : ""
|
|
82
|
+
windows.push({
|
|
83
|
+
name: "限时积分",
|
|
84
|
+
display: `${fmt(timedRest)} 积分${when}`,
|
|
85
|
+
unit: "积分",
|
|
86
|
+
aside: true,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
if (permanent > 0) {
|
|
90
|
+
windows.push({
|
|
91
|
+
name: "永久积分",
|
|
92
|
+
display: `${fmt(permanent)} 积分`,
|
|
93
|
+
unit: "积分",
|
|
94
|
+
aside: true,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
plan: total > 0 ? "积分制" : "已耗尽",
|
|
101
|
+
balance: `${fmt(total)} 积分`,
|
|
102
|
+
windows,
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { error: String(e?.message ?? e), windows: [] }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 最早失效的那批 → magpie 的 `runsOutAt` / `runsOut`。 */
|
|
111
|
+
function runsOutOf(expiring) {
|
|
112
|
+
const next = pickEarliest(expiring)
|
|
113
|
+
if (!next) return {}
|
|
114
|
+
return {
|
|
115
|
+
runsOutAt: new Date(next.ms).toISOString(),
|
|
116
|
+
runsOut: next.points,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pickEarliest(expiring) {
|
|
121
|
+
const list = Array.isArray(expiring) ? expiring : []
|
|
122
|
+
let best = null
|
|
123
|
+
for (const e of list) {
|
|
124
|
+
const pts = Number(e?.points ?? 0)
|
|
125
|
+
const ms = parseServerTime(e?.expiresAt)
|
|
126
|
+
if (!(pts > 0) || ms === null) continue
|
|
127
|
+
if (!best || ms < best.ms) best = { points: pts, ms }
|
|
128
|
+
}
|
|
129
|
+
return best
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `"2026-10-28 23:59:59"` → 毫秒;解析不了返回 null */
|
|
133
|
+
function parseServerTime(s) {
|
|
134
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(String(s ?? ""))
|
|
135
|
+
if (!m) return null
|
|
136
|
+
const [, y, mo, d, h, mi, se] = m
|
|
137
|
+
const ms = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(se)).getTime()
|
|
138
|
+
return Number.isFinite(ms) ? ms : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 北京时间的日期键 `YYYY-MM-DD`(服务端的时间都是 UTC+8) */
|
|
142
|
+
function beijingDay(ms) {
|
|
143
|
+
if (ms === null) return ""
|
|
144
|
+
return new Date(ms + 8 * 3600e3).toISOString().slice(0, 10)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function beijingDate(ms) {
|
|
148
|
+
return beijingDay(ms)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sum = (list) => list.reduce((n, b) => n + Number(b?.points ?? 0), 0)
|
|
152
|
+
const fmt = (n) => Number(n).toLocaleString("zh-CN")
|
package/src/store.mjs
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
//
|
|
8
8
|
// ⚠ 这不是「状态管理」,是缓存:删掉全部,插件照样能跑(只是要重新登录一次)。
|
|
9
9
|
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, unlinkSync
|
|
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
|
-
import { fileURLToPath } from "node:url"
|
|
14
13
|
import { fetchOwnInviteCode, SITES } from "./api.mjs"
|
|
15
14
|
|
|
16
15
|
function cacheDir() {
|
|
@@ -215,7 +214,6 @@ export function writeInvitePolicy(siteId, policy) {
|
|
|
215
214
|
at: new Date().toISOString(),
|
|
216
215
|
}),
|
|
217
216
|
)
|
|
218
|
-
syncInvitePage() // 页面上的「当前」跟着刷新
|
|
219
217
|
} catch {}
|
|
220
218
|
}
|
|
221
219
|
|
|
@@ -255,7 +253,6 @@ export function noteAccountCode(siteId, org, code, accountId) {
|
|
|
255
253
|
const f = inviteCodesPath(siteId)
|
|
256
254
|
mkdirSync(dirname(f), { recursive: true })
|
|
257
255
|
writeFileSync(f, JSON.stringify(all))
|
|
258
|
-
syncInvitePage() // 页面清单跟着刷新
|
|
259
256
|
} catch {}
|
|
260
257
|
}
|
|
261
258
|
|
|
@@ -296,108 +293,6 @@ export function backfillInviteCodes() {
|
|
|
296
293
|
return backfillPromise
|
|
297
294
|
}
|
|
298
295
|
|
|
299
|
-
/**
|
|
300
|
-
* 把「有哪些账号、各自的邀请码、当前策略」写进 page/codes.js。
|
|
301
|
-
*
|
|
302
|
-
* 插件页面是沙箱:没有 fetch、读不到磁盘,也读不到账号(magpie 只给它 usage / providers)。
|
|
303
|
-
* 所以清单靠这里预先生成一份静态脚本,页面 <script src="codes.js"> 直接读 —— 与
|
|
304
|
-
* agent-purge 的 gen-personas.mjs 是同一个套路。
|
|
305
|
-
*/
|
|
306
|
-
/**
|
|
307
|
-
* org → 便于辨认的名字(打码手机号 / 邮箱),从 magpie 自己的账号文件**只读**。
|
|
308
|
-
* 页面要显示「178****697(IV…)」,而 rollInvite 只拿得到 org,所以在这里补。
|
|
309
|
-
*/
|
|
310
|
-
function readAccountLabels() {
|
|
311
|
-
const out = {}
|
|
312
|
-
try {
|
|
313
|
-
const raw = JSON.parse(readFileSync(join(homedir(), ".config", "magpie", "plugin-auth.json"), "utf8"))
|
|
314
|
-
for (const v of Object.values(raw || {})) {
|
|
315
|
-
const org = String(v?.org || "")
|
|
316
|
-
const id = String(v?.accountId || "")
|
|
317
|
-
if (org && id && !out[org]) out[org] = id
|
|
318
|
-
}
|
|
319
|
-
} catch {}
|
|
320
|
-
return out
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* 删除账号后的确定性清理:码缓存去掉死号;滚动码若属死号一并清掉
|
|
325
|
-
* (不清掉的话,缓存一清 inviteToUse 就查不到归属,反判成「归属不明」保留)。
|
|
326
|
-
*
|
|
327
|
-
* ⚠ 新号竞态保护(2026-10-29 实测复现):注册登录时 rollInvite 先记码,
|
|
328
|
-
* magpie 把新号写进 plugin-auth.json 在**之后** —— 此刻 liveOrgs 看不到新号,
|
|
329
|
-
* 直接清理会把刚记的码当死号删掉,页面要等下次登录/签到才有它。
|
|
330
|
-
* 所以 **10 分钟内记录的条目不参与清理**;这窗口内 inviteToUse 的死号门
|
|
331
|
-
* (owner 死→回落)仍然兜底。
|
|
332
|
-
* 账号文件读不到(liveOrgs === null)时不动,避免误清。
|
|
333
|
-
*/
|
|
334
|
-
const FRESH_ACCOUNT_MS = 10 * 60_000
|
|
335
|
-
|
|
336
|
-
export function pruneDeadAccounts(siteId) {
|
|
337
|
-
const live = liveOrgs(siteId)
|
|
338
|
-
if (!live || !live.size) return null
|
|
339
|
-
const codes = readAccountCodes(siteId)
|
|
340
|
-
const fresh = (v) => {
|
|
341
|
-
try {
|
|
342
|
-
return Date.now() - Date.parse(v?.at || "") < FRESH_ACCOUNT_MS
|
|
343
|
-
} catch {
|
|
344
|
-
return false
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const dead = Object.keys(codes).filter((org) => !live.has(org) && !fresh(codes[org]))
|
|
348
|
-
if (!dead.length) return { dead: [], rollingCleared: false }
|
|
349
|
-
let rollingCleared = false
|
|
350
|
-
const rollingNow = readInvite(siteId)
|
|
351
|
-
if (rollingNow && dead.some((org) => codes[org]?.code === rollingNow)) {
|
|
352
|
-
try {
|
|
353
|
-
unlinkSync(invitePath(siteId))
|
|
354
|
-
rollingCleared = true
|
|
355
|
-
} catch {}
|
|
356
|
-
}
|
|
357
|
-
const kept = { ...codes }
|
|
358
|
-
for (const org of dead) delete kept[org]
|
|
359
|
-
try {
|
|
360
|
-
const f = inviteCodesPath(siteId)
|
|
361
|
-
mkdirSync(dirname(f), { recursive: true })
|
|
362
|
-
writeFileSync(f, JSON.stringify(kept))
|
|
363
|
-
} catch {}
|
|
364
|
-
return { dead, rollingCleared }
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export function syncInvitePage({ root: target } = {}) {
|
|
368
|
-
try {
|
|
369
|
-
const root = target || dirname(dirname(fileURLToPath(import.meta.url)))
|
|
370
|
-
// 仓库里的测试/开发不生成(否则 page/codes.js 会脏工作区);发布包装有 .git 才写。
|
|
371
|
-
// 显式传 root(测试)时不受这个保护限制。
|
|
372
|
-
if (!target && (process.env.MAGICREW_NO_PAGE_SYNC === "1" || existsSync(join(root, ".git")))) return
|
|
373
|
-
const labels = readAccountLabels()
|
|
374
|
-
const sites = {}
|
|
375
|
-
for (const site of ["magicrew", "magicrew-intl"]) {
|
|
376
|
-
const live = liveOrgs(site)
|
|
377
|
-
pruneDeadAccounts(site)
|
|
378
|
-
let codes = readAccountCodes(site)
|
|
379
|
-
for (const [org, v] of Object.entries(codes)) {
|
|
380
|
-
if (v && !v.accountId && labels[org]) v.accountId = labels[org]
|
|
381
|
-
}
|
|
382
|
-
const pol = readInvitePolicy(site)
|
|
383
|
-
const rolling = readInvite(site)
|
|
384
|
-
const ownerOf = (code) => Object.keys(codes).find((o) => codes[o]?.code === code) || ""
|
|
385
|
-
sites[site] = {
|
|
386
|
-
codes,
|
|
387
|
-
policy: pol,
|
|
388
|
-
// 当前选择指向已删除的账号 —— 页面据此提示,登录侧已自动回退
|
|
389
|
-
policyStale: Boolean(live && live.size && pol.mode === "fixed" && pol.org && !live.has(pol.org)),
|
|
390
|
-
rolling,
|
|
391
|
-
rollingStale: Boolean(live && live.size && rolling && ownerOf(rolling) && !live.has(ownerOf(rolling))),
|
|
392
|
-
author: AUTHOR_INVITE[site] || "",
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const f = join(root, "page", "codes.js")
|
|
396
|
-
mkdirSync(dirname(f), { recursive: true })
|
|
397
|
-
writeFileSync(f, "window.MAGICREW_CODES = " + JSON.stringify({ at: new Date().toISOString(), sites }) + ";\n")
|
|
398
|
-
} catch {}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
296
|
/** 滚动请求最多等这么久 —— 登录流程不能被它拖死 */
|
|
402
297
|
const INVITE_TIMEOUT_MS = 6000
|
|
403
298
|
|