magicrew-auth 0.3.29 → 0.3.30

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
@@ -73,6 +73,8 @@ curl -s "https://usemagpie.ai/api/latest?channel=stable" | grep -o '"version":"[
73
73
  | 20 | 邮箱验证码登录报「phone 字段是必须的」或「captcha 字段是必须的」 | 邮箱和手机是**两套端点**:发信 `/v4/user/send-email` vs `/send-sms`,换登录态 `/v4/user/login-email` vs `/login`;邮箱那条验证码字段叫 **`captcha`**(不是 `code`)。判据:往 `/v4/user/login` 发任何 type 都回「phone 字段是必须的」 | `src/captcha.mjs` 头 · CHANGELOG 0.3.28 |
74
74
  | 21 | 无头登录「旋转验证连试 4 张图都没过」,而这号本来就是自己的 | 先分清两件事:① **已注册的号服务端根本不出图**(`/captcha/generate` 对已注册账号直接回 `business_token`)—— 所以必须**先取图、再判识图模型**,一上来就 `if (!vision) throw` 会把这条免费通道堵死;② 识图模型 `max_tokens` 太小会把推理答案饿死(`workbuddy/deepseek-v4.1-flash` 给 400 → `content:null`,判官抓空串) | CHANGELOG 0.3.28 · `src/vision.mjs` |
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
+ | 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
+ | 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` |
76
78
 
77
79
  **坑 14 的两个附带判据**(都实测过):
78
80
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.30 — 2026-10-29
4
+
5
+ **默认开启自动签到 · 插件自身自动更新**
6
+
7
+ ### 一、默认开启自动签到
8
+
9
+ magpie 的插件签到开关 `pluginCheckins` 默认是**关**的:`PluginCheckinOn()`
10
+ 在没有显式值时只跟随厂商内置开关(WorkBuddy / Trae / MiniMax / Qoder),
11
+ magicrew 没有内置开关,于是新装插件不会自动签到。
12
+
13
+ 插件加载时读 `~/.config/magpie/settings.json`:**只有这个 provider 从未被
14
+ 用户设置过**,才补一个 `pluginCheckins[provider] = true`。用户显式关掉的
15
+ (false)绝不覆盖;settings.json 读不出来也不覆盖。config 钩子先补一次,
16
+ usage 钩子再兜一次(防止 magpie 启动时把内存里的旧 settings 写回)。
17
+
18
+ ### 二、插件自身自动更新
19
+
20
+ magpie 的自动更新只认 `@magpie-community/*`(源码 `internal/plugin/updates.go`
21
+ 的 `Official()` 硬编码这个前缀)—— 第三方插件无论 npm 源还是 git 源,都只会
22
+ 在插件列表里提示「有新提交」,等用户点更新。这就是为什么社区包(Qoder、
23
+ CodeArts、商汤小浣熊、OpenCode Zen Free)有「已自动更新」徽章,而本插件没有。
24
+
25
+ 插件没法改 magpie 的判断,只能在插件侧复刻同一套机制:
26
+
27
+ 1. `config` 加载 15 秒后、以及每次签到成功后,读 magpie 写好的
28
+ `~/.config/magpie/plugin-updates.json`,看 `waiting` 里有没有
29
+ `package === "Magicrew" && git === true` 的自己;
30
+ 2. 有就调本机 magpie 的 CLI:`magpie plugin update`(进程树里找 magpie,
31
+ 也可以用环境变量 `MAGPIE_EXE` 明示;找不到就记 `no-magpie-exe` 并放弃,
32
+ 不伪造徽章);
33
+ 3. CLI 成功退出(code 0)后,在 `plugin-updates.json` 的 `updated[]` 里补一条
34
+ `{package:"Magicrew", from, to, at}` —— magpie 的「已自动更新」徽章
35
+ (`plugin.LastUpdated()`)读的就是这个记录。
36
+
37
+ 节流:同一版本 6 小时内最多试一次;成功后按 `to` 版本记住不再重复。所有失败
38
+ 都只写状态文件,不影响签到/额度/登录。
39
+
40
+ ### 边界(必须知道)
41
+
42
+ - `magpie plugin update` 是 CLI 进程内的 `plugin.Update()`,会重装**所有**
43
+ 非路径插件;它不会重启正在运行的 magpie —— 更新文件后,下次启动 magpie
44
+ 才会加载新代码。插件列表的「已自动更新」徽章会立即出现(读的是文件记录)。
45
+ - 找不到 magpie 可执行文件时不会自动更新,也不会显示徽章;设 `MAGPIE_EXE`
46
+ 可指定。
47
+ - 这一步是插件对 magpie 的两个配置文件(settings.json / plugin-updates.json)
48
+ 的读写 —— magpie 没有给插件提供这两个能力的 API(GUI API 只服务于它自己的
49
+ WebView,不是 TCP 端口)。写 settings 只在「该键不存在」时发生,且先备份为
50
+ `settings.json.bak-magicrew-auto`。
51
+
52
+ ### 验证
53
+
54
+ - `tests/auto-maintenance.mjs` 20 条断言 0 失败:默认签到只补缺失、显式 false
55
+ 不覆盖、坏 JSON 不覆盖;`pickSelfUpdate` 只认自己且只认 git;`noteUpdated`
56
+ 去重/截断;`kick` 的 no-update / no-exe / throttled / done / dryRun / 成功
57
+ - 本机真实 `plugin-updates.json` 的 dryRun:命中 `Magicrew 272b812 → 050b0f4`,
58
+ 解析到 `E:\Downloads\magpie-windows-amd64.exe`;`ensureCheckinDefault` 对
59
+ `magicrew` / `magicrew-intl` 均返回 explicit:true(已是用户开着的,不改)
60
+ - `magpie plugin list` CLI 正常(只读)
61
+
3
62
  ## 0.3.29 — 2026-10-29
4
63
 
5
64
  **同一账号多把 key 逐把试 · 全部被拒改 402 · 续期收敛到 key-store**
package/README.md CHANGED
@@ -31,6 +31,8 @@
31
31
  - **积分耗尽自动换号** —— 上游用 HTTP 400 报「可用积分已不足」时,插件把它改写为语义正确的 **402 Payment Required**,magpie 会 rest 这个账号并继续问同 provider 的下一个账号 / 路由组的下一个成员
32
32
  - **登录一次管 30 天** —— 网页 JWT 顶 30 天,`mgw_` api_key 每 2 小时自续,refresh_token 每次续期时自滚
33
33
  - **新手奖励 8 项全自动** —— 国内站 8 项 500 积分全部拿得到;第 8 项(完成第一次 AI 工作)由插件替你发一条消息,花掉约 2 积分
34
+ - **默认开启自动签到** —— 安装后自动把本 provider 的插件签到设为开(`pluginCheckins` 缺省时补 true;你手动关掉的不会被改回)
35
+ - **自动更新** —— 复用 magpie 的插件更新机制:发现自己的新版本后调 `magpie plugin update`,并在成功后写「已自动更新」记录;更新完要重启一次 magpie 才会加载新代码
34
36
  - **邀请码自动滚** —— 第一次注册带作者的码,之后每次登录都记住「上一个登录账号」的码,拿到新的就覆盖旧的
35
37
 
36
38
  ---
package/docs/internals.md CHANGED
@@ -852,6 +852,19 @@ store 槽 a expires 2026-10-28T14:41:03.000Z → 200
852
852
  已经推翻过:同账号相隔 67 秒签的两把同时有效;12 把逐把探活,403 全部对应
853
853
  `expiresAt` 已过。真正失效的是**手里那把过期了**,不是后来签的把它顶掉了。
854
854
 
855
+ ### 自动维护(0.3.30 起)
856
+
857
+ 插件有两个 magpie 没替它做的事,都在 `src/auto-maintenance.mjs`:
858
+
859
+ | 能力 | magpie 的现状 | 插件怎么补 |
860
+ |---|---|---|
861
+ | 默认开启签到 | `PluginCheckinOn()`:`pluginCheckins` 无显式值时默认 **false**(只有 WorkBuddy / Trae / MiniMax / Qoder 有内置开关) | 插件加载时读 `~/.config/magpie/settings.json`,只在键不存在时补 true;用户显式 false 不覆盖 |
862
+ | 插件自身自动更新 | `Official()` 只认 `@magpie-community/*`;第三方 git / npm 源只提示、不自动装 | 读 `plugin-updates.json` 的 `waiting`;有自己就调 `magpie plugin update`;成功后补 `updated[]`(「已自动更新」徽章读它) |
863
+
864
+ ⚠ GUI 的 `/api/...` 不在 gateway 端口 3425 上(那里是 `/v1`,`/api/plugins/updates`
865
+ 会回 `/v1` 的 404 文案);插件拿不到桌面 WebView 的 API,所以只能走文件 + CLI。
866
+ ⚠ `magpie plugin update` 不重启运行中的 magpie:更新文件后要下次启动才加载新代码。
867
+
855
868
  ### checkin 现在做四件事(0.3.11 起)
856
869
 
857
870
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magicrew-auth",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
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",
@@ -0,0 +1,253 @@
1
+ // 自动维护 —— 「默认开启签到」与「插件自身自动更新」
2
+ //
3
+ // 这两个能力本质上都踩在 magpie 的边界上,先把边界说清楚:
4
+ //
5
+ // ① 签到:magpie 的 `pluginCheckins`(设置 → 插件签到)**默认是关的**
6
+ // (`PluginCheckinOn()`:没有显式值时跟随厂商内置开关,magicrew 没有内置
7
+ // 开关,于是 false)。插件没有官方 API 能声明的默认值,只能在插件加载时
8
+ // 读 `~/.config/magpie/settings.json`:**只有这个 provider 从未被用户设置过**
9
+ // 才补一个 true。用户显式关掉的,绝不覆盖。
10
+ //
11
+ // ② 自动更新:magpie 只对 `@magpie-community/*` 的包自动更新
12
+ // (`plugin.Official()` 硬编码这个前缀),git 源的第三方插件永远只提示、
13
+ // 等用户点「更新」。我们不在那个 scope 里,所以只能由插件自己复刻这套
14
+ // 机制:读 magpie 写好的 `plugin-updates.json`,确认「有我自己」之后调用
15
+ // magpie 的 CLI(`magpie plugin update`)——它会把插件目录更新到最新;
16
+ // 成功后在 `plugin-updates.json` 的 `updated[]` 里补一条,magpie 的
17
+ // 「已自动更新」徽章读的就是这个记录(`plugin.LastUpdated()`)。
18
+ //
19
+ // ⚠ 边界(都是实测/源码结论,别想当然):
20
+ // · 插件进程拿不到 GUI 的 HTTP 端口 —— `MAGPIE_ADDR` 指向 3425,那是
21
+ // **gateway**(`/api/plugins/updates` 在那里会回 `/v1` 的 404 文案),
22
+ // GUI API 走的是桌面窗口自己的 WebView,不是 TCP 端口。
23
+ // · `magpie plugin update` 是 CLI 进程内的 `plugin.Update()`,会更新
24
+ // **所有** 非路径插件并重装;它不会重启正在运行的 magpie,所以更新后
25
+ // 要等 magpie 下次启动才加载新代码。这是这个兜底能做到的极限。
26
+ // · 找不到 magpie 可执行文件时只记录 `no-magpie-exe`,不伪造徽章、不硬编码
27
+ // 本机路径。可以设环境变量 `MAGPIE_EXE` 明示。
28
+ //
29
+ // 两个动作都是**尽力而为**:失败不抛、不拖慢签到/额度/登录。
30
+
31
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, copyFileSync } from "node:fs"
32
+ import { homedir } from "node:os"
33
+ import { join, dirname, basename } from "node:path"
34
+ import { execFileSync, spawn } from "node:child_process"
35
+
36
+ const PKG_NAME = "Magicrew"
37
+ const AUTO_DIR = () => join(homedir(), ".config", "magpie")
38
+ const CACHE_DIR = () => join(homedir(), ".cache", "magpie")
39
+ const THROTTLE_MS = 6 * 3600_000 // 同一次更新失败后,6 小时内不再试
40
+
41
+ function readJson(path, fallback) {
42
+ try {
43
+ return JSON.parse(readFileSync(path, "utf8"))
44
+ } catch {
45
+ return fallback
46
+ }
47
+ }
48
+
49
+ /** 原子写:先写同目录临时文件再 rename,避免把 magpie 的 JSON 写半截 */
50
+ function writeJsonAtomic(path, value, { backup = false } = {}) {
51
+ mkdirSync(dirname(path), { recursive: true })
52
+ if (backup && existsSync(path)) {
53
+ try {
54
+ writeFileSync(path + ".bak-magicrew-auto", readFileSync(path))
55
+ } catch {}
56
+ }
57
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`
58
+ writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n")
59
+ renameSync(tmp, path)
60
+ }
61
+
62
+ /** 这个 provider 的签到是否从未被用户设置过;是就补 true(并返回 changed) */
63
+ export function ensureCheckinDefault(provider, { dir = AUTO_DIR() } = {}) {
64
+ const file = join(dir, "settings.json")
65
+ let s
66
+ if (existsSync(file)) {
67
+ s = readJson(file, null)
68
+ // 文件在但读不出来:绝不覆盖用户的设置
69
+ if (!s || typeof s !== "object" || Array.isArray(s)) return { changed: false, reason: "settings-unreadable" }
70
+ } else {
71
+ s = {}
72
+ }
73
+ const table = s.pluginCheckins && typeof s.pluginCheckins === "object" && !Array.isArray(s.pluginCheckins)
74
+ ? s.pluginCheckins
75
+ : {}
76
+ if (Object.hasOwn(table, provider)) return { changed: false, reason: "explicit", on: table[provider] === true }
77
+ s.pluginCheckins = { ...table, [provider]: true }
78
+ writeJsonAtomic(file, s, { backup: true })
79
+ return { changed: true, on: true }
80
+ }
81
+
82
+ /** magpie 的 waiting 里有没有「我自己」的新版本(git 源只有 commit,没有 npm version) */
83
+ export function pickSelfUpdate(waiting, { pkg = PKG_NAME } = {}) {
84
+ if (!Array.isArray(waiting)) return null
85
+ return waiting.find((w) => w && w.package === pkg && w.git === true) || null
86
+ }
87
+
88
+ /** 补一条「自动更新记录」—— magpie 的「已自动更新」徽章读 plugin-updates.json 的 updated[] */
89
+ export function noteUpdated(pkg, from, to, { dir = AUTO_DIR(), now = new Date() } = {}) {
90
+ const file = join(dir, "plugin-updates.json")
91
+ const u = readJson(file, null)
92
+ if (!u || typeof u !== "object" || Array.isArray(u)) return false
93
+ u.updated = Array.isArray(u.updated) ? u.updated : []
94
+ const last = u.updated[u.updated.length - 1]
95
+ // 同一次更新重复写会让徽章的计时一直刷新;24 小时内同 from/to 只记一次
96
+ if (last && last.package === pkg && last.from === from && last.to === to) {
97
+ const at = Date.parse(last.at)
98
+ if (Number.isFinite(at) && now.getTime() - at < 24 * 3600_000) return true
99
+ }
100
+ u.updated.push({ package: pkg, from, to, at: now.toISOString() })
101
+ if (u.updated.length > 20) u.updated = u.updated.slice(-20)
102
+ writeJsonAtomic(file, u, { backup: true })
103
+ return true
104
+ }
105
+
106
+ /** 自动更新的节流状态(每个站点一份) */
107
+ export function readAutoState(site, { cacheDir = CACHE_DIR() } = {}) {
108
+ return readJson(join(cacheDir, `magicrew-auto-${site}.json`), {}) || {}
109
+ }
110
+
111
+ function writeAutoState(site, state, { cacheDir = CACHE_DIR() } = {}) {
112
+ try {
113
+ writeJsonAtomic(join(cacheDir, `magicrew-auto-${site}.json`), state)
114
+ } catch {}
115
+ }
116
+
117
+ /**
118
+ * 找本机 magpie 可执行文件。顺序:
119
+ * 1. 环境变量 MAGPIE_EXE(最可靠,给测试和自定义安装用)
120
+ * 2. 祖父进程:插件跑在 magpie-bun.exe 里,它的父进程就是 magpie 主程序
121
+ * 3. PATH 里的 magpie / magpie.exe / magpie-windows-amd64.exe
122
+ * 找不到返回 ""
123
+ */
124
+ export function findMagpieExe({ env = process.env, ppid = process.ppid, platform = process.platform } = {}) {
125
+ const resolve = (p) => {
126
+ if (!p || !existsSync(p)) return ""
127
+ // magpie 的 `magpie.cmd` 只是同目录 exe 的包装(Settings 的 Command line
128
+ // 会生成它);spawn 一个 .cmd 在 Windows 上还要 shell,直接解析到 exe。
129
+ if (/\.(cmd|bat)$/i.test(p)) {
130
+ for (const n of ["magpie-windows-amd64.exe", "magpie.exe"]) {
131
+ const q = join(dirname(p), n)
132
+ if (existsSync(q)) return q
133
+ }
134
+ }
135
+ return p
136
+ }
137
+ if (env.MAGPIE_EXE) {
138
+ const p = resolve(env.MAGPIE_EXE)
139
+ if (p) return p
140
+ }
141
+ const exists = (p) => !!resolve(p)
142
+ const tryExec = (cmd, args, timeout = 2500) => {
143
+ try {
144
+ return execFileSync(cmd, args, {
145
+ encoding: "utf8",
146
+ timeout,
147
+ windowsHide: true,
148
+ stdio: ["ignore", "pipe", "ignore"],
149
+ }).trim()
150
+ } catch {
151
+ return ""
152
+ }
153
+ }
154
+ if (platform === "win32") {
155
+ const ps = [
156
+ "$p = Get-CimInstance Win32_Process -Filter \"ProcessId=" + Number(ppid) + "\"",
157
+ "if ($p) { $q = Get-CimInstance Win32_Process -Filter \"ProcessId=$($p.ParentProcessId)\"; if ($q) { $q.ExecutablePath } }",
158
+ ].join("; ")
159
+ const out = tryExec("powershell.exe", ["-NoProfile", "-Command", ps])
160
+ // 只认名字里带 magpie 的进程 —— 测试/宿主不在 magpie 下时,
161
+ // 父进程可能是 bash / node,不能把它们的路径当 magpie
162
+ if (exists(out) && /magpie/i.test(basename(out))) return resolve(out)
163
+ } else {
164
+ const out = tryExec("ps", ["-p", String(ppid), "-o", "ppid="])
165
+ const grand = out.split(/\s+/)[0]
166
+ if (grand) {
167
+ const path = tryExec("ps", ["-p", grand, "-o", "comm="])
168
+ if (exists(path) && /magpie/i.test(basename(path))) return resolve(path)
169
+ }
170
+ }
171
+ for (const name of ["magpie-windows-amd64.exe", "magpie.exe", "magpie"]) {
172
+ const lines = tryExec(platform === "win32" ? "where.exe" : "which", [name], 1500).split(/\r?\n/)
173
+ for (const line of lines) {
174
+ const p = line.trim()
175
+ if (exists(p) && /magpie/i.test(basename(p))) return resolve(p)
176
+ }
177
+ }
178
+ return ""
179
+ }
180
+
181
+ /**
182
+ * 检查并(尽力)自动更新。
183
+ * @param {{id:string}} c 站点上下文(用 c.id 存节流状态)
184
+ * @param {{dir?:string, cacheDir?:string, pkg?:string, exe?:string, dryRun?:boolean, spawnFn?:Function}} opts
185
+ * @returns {Promise<object>} 结果只用于日志/测试,不抛
186
+ */
187
+ export async function kick(c, opts = {}) {
188
+ const dir = opts.dir || AUTO_DIR()
189
+ const cacheDir = opts.cacheDir || CACHE_DIR()
190
+ const pkg = opts.pkg || PKG_NAME
191
+ try {
192
+ const updates = readJson(join(dir, "plugin-updates.json"), null)
193
+ const waiting = pickSelfUpdate(updates?.waiting, { pkg })
194
+ if (!waiting) return { skipped: "no-update" }
195
+
196
+ const state = readAutoState(c.id, { cacheDir })
197
+ const now = Date.now()
198
+ if (state.done && state.done.to === waiting.latest) return { skipped: "done" }
199
+ if (state.lastAttempt && state.latest === waiting.latest && now - state.lastAttempt < THROTTLE_MS) {
200
+ return { skipped: "throttled" }
201
+ }
202
+
203
+ const exe = opts.exe !== undefined ? opts.exe : findMagpieExe(opts)
204
+ if (!exe) {
205
+ writeAutoState(c.id, { ...state, lastAttempt: now, latest: waiting.latest, reason: "no-magpie-exe" }, { cacheDir })
206
+ return { skipped: "no-magpie-exe", waiting }
207
+ }
208
+ if (opts.dryRun) return { wouldRun: exe, waiting }
209
+
210
+ let child
211
+ try {
212
+ child = (opts.spawnFn || spawn)(exe, ["plugin", "update"], {
213
+ detached: true,
214
+ stdio: "ignore",
215
+ windowsHide: true,
216
+ env: process.env,
217
+ })
218
+ } catch (e) {
219
+ writeAutoState(c.id, { ...state, lastAttempt: now, latest: waiting.latest, reason: String(e?.message ?? e) }, { cacheDir })
220
+ return { skipped: "spawn-failed", error: String(e?.message ?? e) }
221
+ }
222
+ child.on?.("exit", (code) => {
223
+ if (code === 0) {
224
+ noteUpdated(pkg, waiting.version, waiting.latest, { dir })
225
+ writeAutoState(c.id, {
226
+ ...state,
227
+ lastAttempt: now,
228
+ latest: waiting.latest,
229
+ done: { from: waiting.version, to: waiting.latest, at: new Date().toISOString() },
230
+ }, { cacheDir })
231
+ } else {
232
+ writeAutoState(c.id, { ...state, lastAttempt: now, latest: waiting.latest, reason: `exit-${code}` }, { cacheDir })
233
+ }
234
+ })
235
+ child.unref?.()
236
+ return { started: true, waiting }
237
+ } catch (e) {
238
+ return { skipped: "error", error: String(e?.message ?? e) }
239
+ }
240
+ }
241
+
242
+ /** 插件加载时的初始化:补默认签到 + 延迟检查更新(不阻塞 config) */
243
+ export function initAutoMaintenance(c) {
244
+ try {
245
+ ensureCheckinDefault(c.PROVIDER)
246
+ } catch {}
247
+ try {
248
+ const t = setTimeout(() => {
249
+ kick(c).catch(() => {})
250
+ }, 15_000)
251
+ t.unref?.()
252
+ } catch {}
253
+ }
@@ -8,6 +8,7 @@
8
8
  // 签到逻辑在独立模块里**按需加载**:它挂了,登录/模型/额度照常工作。
9
9
 
10
10
  import { rollInvite } from "../store.mjs"
11
+ import { kick } from "../auto-maintenance.mjs"
11
12
 
12
13
  export function makeCheckin(c) {
13
14
  return async (getAuth) => {
@@ -20,7 +21,12 @@ export function makeCheckin(c) {
20
21
  // rollInvite 内部自带超时和 try/catch,失败不影响签到。
21
22
  await rollInvite(c.id, auth.jwt, auth.org, c.host)
22
23
  const { runCheckin } = await import("../tasks.mjs")
23
- return await runCheckin({ ...auth, host: c.host })
24
+ const result = await runCheckin({ ...auth, host: c.host })
25
+ // 签到成功后再给自动更新一次机会(config 启动时已试过一次;
26
+ // 这里覆盖「magpie 长期不重启」的情况)。kick 自己有 6 小时节流,
27
+ // 且不阻塞签到结果。
28
+ kick(c).catch(() => {})
29
+ return result
24
30
  } catch (e) {
25
31
  return { outcome: "failed", message: String(e?.message ?? e) }
26
32
  }
@@ -5,13 +5,19 @@
5
5
  // ⚠ 三条不许改:
6
6
  // · 用 ??= 不用 =,否则覆盖用户在 plugins.json 里手配的值
7
7
  // · models 必须有内容 —— 未登录也要有,空表会让卡片显示 "0 models"
8
- // · 不要在这里发网络请求:它是配置,不是钩子
8
+ // · 不要在这里发**业务**网络请求:它是配置,不是钩子(本地默认设置与
9
+ // 延迟的自动维护见 src/auto-maintenance.mjs,失败不影响 config)
9
10
 
10
11
  import { SDK } from "../context.mjs"
11
12
  import { readSnapshot } from "../store.mjs"
13
+ import { initAutoMaintenance } from "../auto-maintenance.mjs"
12
14
 
13
15
  export function makeConfig(c) {
14
16
  return async (cfg) => {
17
+ // 默认开启本 provider 的签到;并在后台检查插件自身更新。
18
+ // 两者都尽力而为,失败不影响 config(见 src/auto-maintenance.mjs 的边界说明)。
19
+ initAutoMaintenance(c)
20
+
15
21
  cfg.provider ??= {}
16
22
  cfg.provider[c.PROVIDER] ??= {
17
23
  name: c.SHORT,
@@ -25,13 +25,23 @@
25
25
  // 宿主支持余额之前的临时协议,见 CHANGELOG 0.3.20。
26
26
 
27
27
  import { fetchPoints, SITES } from "../api.mjs"
28
+ import { ensureCheckinDefault } from "../auto-maintenance.mjs"
28
29
 
29
30
  // 每日登录积分:国内 +200/天、国际 +50/天(实测,见 docs/internals.md)。
30
31
  // 今日批次的分母,只用于展示「今日还剩 / 今日额度」。
31
32
  const DAILY_LOGIN_GIFT = { cn: 200, intl: 50 }
32
33
 
33
34
  export function makeUsage(c) {
35
+ let ensuredCheckin = false
34
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
+ }
35
45
  const auth = await getAuth()
36
46
  if (!auth) return { error: "未登录", windows: [] }
37
47
  try {