magicrew-auth 0.0.0-stage → 0.3.6
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/CHANGELOG.md +176 -0
- package/LICENSE +21 -0
- package/NOTICE.md +33 -0
- package/README.md +200 -3
- package/docs/internals.md +802 -0
- package/docs/npm-publish.md +77 -0
- package/index.mjs +99 -0
- package/package.json +52 -4
- package/src/api.mjs +409 -0
- package/src/browser-login.mjs +408 -0
- package/src/clipboard.mjs +108 -0
- package/src/context.mjs +60 -0
- package/src/hooks/checkin.mjs +21 -0
- package/src/hooks/config.mjs +25 -0
- package/src/hooks/headers.mjs +14 -0
- package/src/hooks/loader.mjs +93 -0
- package/src/hooks/methods.mjs +191 -0
- package/src/hooks/models.mjs +37 -0
- package/src/hooks/refresh.mjs +29 -0
- package/src/hooks/usage.mjs +40 -0
- package/src/icons.mjs +24 -0
- package/src/key-store.mjs +188 -0
- package/src/keys.mjs +109 -0
- package/src/model-context.mjs +100 -0
- package/src/self-register.mjs +234 -0
- package/src/store.mjs +183 -0
- package/src/tasks.mjs +119 -0
package/src/store.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// 落盘 —— 这个插件自己需要记住的那几样东西
|
|
2
|
+
//
|
|
3
|
+
// 三份文件,各自一个用途,互不依赖:
|
|
4
|
+
// 模型快照 magicrew-models-<site>.json 未登录时卡片显示什么
|
|
5
|
+
// 邀请码 magicrew-invite-<site>.json 下一次注册带谁的码(滚动的)
|
|
6
|
+
// 登录态 magicrew-session-<site>.json 「自己的邀请码」要用它去换
|
|
7
|
+
//
|
|
8
|
+
// ⚠ 这不是「状态管理」,是缓存:删掉全部,插件照样能跑(只是要重新登录一次)。
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
11
|
+
import { homedir } from "node:os"
|
|
12
|
+
import { join, dirname } from "node:path"
|
|
13
|
+
|
|
14
|
+
function cacheDir() {
|
|
15
|
+
return join(homedir(), ".cache", "magpie")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ═══ 模型:不写死,按账号实时拉 ═══════════════════════════════════════
|
|
19
|
+
//
|
|
20
|
+
// 权威来源只有一条:
|
|
21
|
+
//
|
|
22
|
+
// GET <host>/api/v1/flows/models (要网页 JWT + organization-code)
|
|
23
|
+
// → { data: { models: [{ value, label, vision, tags, icon, configs }] } }
|
|
24
|
+
//
|
|
25
|
+
// 实测(2026-10-27):国内 27 个、国际 49 个。两站列表**不一样**,而且会
|
|
26
|
+
// 随订阅档位和平台方开关变 —— 所以这个包里没有一张模型清单要维护。
|
|
27
|
+
//
|
|
28
|
+
// ⚠ 别改成读 /v1/models:那个国内 44、国际 65,混着 VIP 锁定、生图、
|
|
29
|
+
// embedding,塞进 magpie 用户会选到根本调不动的模型。
|
|
30
|
+
//
|
|
31
|
+
// ⚠ 上下文窗口:flows/models 和 /v1/models **都不返回** context_length
|
|
32
|
+
// (/v1/models 只有 id / object / created_at / owner_by)。所以一律按
|
|
33
|
+
// 256K 保守填 —— 填大了 agent 要等上游报错才知道越界,填小了自己压得早。
|
|
34
|
+
//
|
|
35
|
+
// 未登录时 config() 用**上次登录成功时落盘的快照**,没有快照就是空表。
|
|
36
|
+
// 空表让卡片显示 0 models,但那是诚实的:这会儿确实不知道账号能选什么。
|
|
37
|
+
|
|
38
|
+
/** 快照落在 magpie 自己的缓存目录,按站点分开 */
|
|
39
|
+
function snapshotPath(siteId) {
|
|
40
|
+
return join(cacheDir(), `magicrew-models-${siteId}.json`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSnapshot(siteId) {
|
|
44
|
+
try {
|
|
45
|
+
const v = JSON.parse(readFileSync(snapshotPath(siteId), "utf8"))
|
|
46
|
+
return v && typeof v === "object" && Object.keys(v).length ? v : null
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeSnapshot(siteId, models) {
|
|
53
|
+
try {
|
|
54
|
+
const f = snapshotPath(siteId)
|
|
55
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
56
|
+
writeFileSync(f, JSON.stringify(models))
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══ 邀请码:滚动的,就一条规则 ═══════════════════════════════════════
|
|
61
|
+
//
|
|
62
|
+
// ① 固定位置有记录 → 用记录里那个
|
|
63
|
+
// ② 没有记录 → 用作者码
|
|
64
|
+
// ③ 登录成功后 → 查这个账号自己的邀请码 → 覆盖写回
|
|
65
|
+
//
|
|
66
|
+
// 固定位置:~/.cache/magpie/magicrew-invite-<siteId>.json { code, at }
|
|
67
|
+
//
|
|
68
|
+
// 效果:第一次用作者的,之后每次都用「上一个登录成功的账号」的码,一路滚下去。
|
|
69
|
+
//
|
|
70
|
+
// ⚠ ③ 不是每次都能成。/api/v1/invitations/info 只认带 device_id 的 JWT ——
|
|
71
|
+
// 同一个账号、同一时刻签的两把,实测差别就在这一个字段:
|
|
72
|
+
// 桌面客户端 438 字符 cus.device_id = 64 位 hex → 200 拿到 invite_code
|
|
73
|
+
// 网页版 350 字符 cus.device_id = "" → 2155 用户不存在
|
|
74
|
+
// 密码登录走 API、会带 device 指纹,签出来的是前者,滚得动;
|
|
75
|
+
// 浏览器登录(剪贴板)拿到的就是网页那把,滚不动 —— 那就保留旧值,
|
|
76
|
+
// 不报错、不挡登录。
|
|
77
|
+
//
|
|
78
|
+
// 奖励口径(上游前端 constants,2026-10-27):
|
|
79
|
+
// 每次注册 每次订阅 邀请注册封顶 总额封顶
|
|
80
|
+
// 国内 1,000 3,000 60,000 300,000
|
|
81
|
+
// 国际 200 500 10,000 30,000
|
|
82
|
+
const AUTHOR_INVITE = {
|
|
83
|
+
// 国内站(2026-10-27 用作者账号 orgcode12345 从 /api/v1/invitations/info 复核)
|
|
84
|
+
magicrew: "IVC8BAWYXRY1",
|
|
85
|
+
// 国际站
|
|
86
|
+
"magicrew-intl": "IV75F0ZKVKXG",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function invitePath(siteId) {
|
|
90
|
+
return join(cacheDir(), `magicrew-invite-${siteId}.json`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 固定位置里存的邀请码;没有就是空串 */
|
|
94
|
+
function readInvite(siteId) {
|
|
95
|
+
try {
|
|
96
|
+
const v = JSON.parse(readFileSync(invitePath(siteId), "utf8"))
|
|
97
|
+
return typeof v?.code === "string" ? v.code.trim() : ""
|
|
98
|
+
} catch {
|
|
99
|
+
return ""
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 覆盖写 —— 拿到新的就盖掉旧的 */
|
|
104
|
+
function writeInvite(siteId, code) {
|
|
105
|
+
const c = String(code || "").trim()
|
|
106
|
+
if (!c) return ""
|
|
107
|
+
try {
|
|
108
|
+
const f = invitePath(siteId)
|
|
109
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
110
|
+
writeFileSync(f, JSON.stringify({ code: c, at: new Date().toISOString() }))
|
|
111
|
+
} catch {}
|
|
112
|
+
return c
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 这次登录该带的邀请码:记录里的,没有就作者码 */
|
|
116
|
+
function inviteToUse(siteId) {
|
|
117
|
+
return readInvite(siteId) || AUTHOR_INVITE[siteId] || ""
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 登录成功后把账号自己的邀请码滚上去;拿不到就安静跳过 */
|
|
121
|
+
async function rollInvite(siteId, jwt, org, host) {
|
|
122
|
+
try {
|
|
123
|
+
const own = await fetchOwnInviteCode(jwt, org, host)
|
|
124
|
+
return writeInvite(siteId, own.code)
|
|
125
|
+
} catch {
|
|
126
|
+
return ""
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 上次登录成功时落盘的 { jwt, org },只给「自己的邀请码」用 */
|
|
131
|
+
function sessionPath(siteId) {
|
|
132
|
+
return join(cacheDir(), `magicrew-session-${siteId}.json`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readSession(siteId) {
|
|
136
|
+
try {
|
|
137
|
+
const v = JSON.parse(readFileSync(sessionPath(siteId), "utf8"))
|
|
138
|
+
return v?.jwt ? v : null
|
|
139
|
+
} catch {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 登录链路的失败原因落盘,排障用;不影响任何正常流程 */
|
|
145
|
+
function loginLogPath(siteId) {
|
|
146
|
+
return join(cacheDir(), `magicrew-login-${siteId}.log`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function noteLoginFailure(siteId, msg) {
|
|
150
|
+
try {
|
|
151
|
+
const f = loginLogPath(siteId)
|
|
152
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
153
|
+
writeFileSync(f, `[${new Date().toISOString()}] ${msg}
|
|
154
|
+
`, { flag: "a" })
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeSession(siteId, jwt, org) {
|
|
159
|
+
try {
|
|
160
|
+
const f = sessionPath(siteId)
|
|
161
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
162
|
+
writeFileSync(f, JSON.stringify({ jwt, org }))
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
AUTHOR_INVITE,
|
|
169
|
+
cacheDir,
|
|
170
|
+
rollInvite,
|
|
171
|
+
invitePath,
|
|
172
|
+
inviteToUse,
|
|
173
|
+
loginLogPath,
|
|
174
|
+
noteLoginFailure,
|
|
175
|
+
readInvite,
|
|
176
|
+
readSession,
|
|
177
|
+
readSnapshot,
|
|
178
|
+
sessionPath,
|
|
179
|
+
snapshotPath,
|
|
180
|
+
writeInvite,
|
|
181
|
+
writeSession,
|
|
182
|
+
writeSnapshot,
|
|
183
|
+
}
|
package/src/tasks.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// 签到 = 自动做完新手福利
|
|
2
|
+
//
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
// 结论先说:不用浏览器
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
6
|
+
//
|
|
7
|
+
// 一开始从主 chunk 里只挖到埋点事件名(`onboarding_guide_step_click`),
|
|
8
|
+
// 所以以为任务要靠前端导览判定。翻到 `onboarding-CZBI0pQt.js` 才发现
|
|
9
|
+
// 真正的接口只有两行:
|
|
10
|
+
//
|
|
11
|
+
// GET /api/v1/onboarding/tasks
|
|
12
|
+
// PUT /api/v1/onboarding/tasks/{code}/completion
|
|
13
|
+
//
|
|
14
|
+
// 8 项任务里 7 项 PUT 一下就完成、积分当场到账。只有 `first_ai_work`
|
|
15
|
+
// 回 9303「该新人引导任务不支持手动完成」—— 那项得真的跑一次 AI 任务。
|
|
16
|
+
//
|
|
17
|
+
// 实测(2026-10-27):1240 → 1540,+300,六项全部当场入账。
|
|
18
|
+
//
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
20
|
+
//
|
|
21
|
+
// 所以 magpie 的 auth.checkin 钩子做的事:
|
|
22
|
+
// ① 拉任务列表
|
|
23
|
+
// ② 把 PENDING 的挨个 PUT(跳过不支持手动的)
|
|
24
|
+
// ③ 报积分变化
|
|
25
|
+
//
|
|
26
|
+
// 每天跑一次,第一次就把 300 分拿完;之后每次都是 done。
|
|
27
|
+
|
|
28
|
+
import { fetchPoints, fetchOnboardingTasks, completeOnboardingTask } from "./api.mjs"
|
|
29
|
+
|
|
30
|
+
/** 已知不支持手动完成的任务,别再白试 */
|
|
31
|
+
const NOT_MANUALLY_COMPLETABLE = new Set(["first_ai_work"])
|
|
32
|
+
|
|
33
|
+
/** 9303 = 该新人引导任务不支持手动完成 */
|
|
34
|
+
const CODE_NOT_SUPPORTED = 9303
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ jwt: string, org: string, host: string }} auth
|
|
38
|
+
* host 由调用方的插件闭包给(每个供应商绑定一个站点,不在 auth 里存站点代号)
|
|
39
|
+
*/
|
|
40
|
+
export async function runCheckin(auth) {
|
|
41
|
+
const host = auth.host
|
|
42
|
+
const h = { jwt: auth.jwt, org: auth.org, host }
|
|
43
|
+
|
|
44
|
+
let before = null
|
|
45
|
+
try {
|
|
46
|
+
before = (await fetchPoints(h.jwt, h.org, h.host)).total
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { outcome: "failed", message: `读不到积分:${e?.message ?? e}` }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let tasks = []
|
|
52
|
+
try {
|
|
53
|
+
tasks = await fetchOnboardingTasks(h.jwt, h.org, h.host)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { outcome: "failed", message: `读不到新手任务:${e?.message ?? e}` }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const done = tasks.filter((t) => t.status === "COMPLETED" || t.rewardStatus === "GRANTED")
|
|
59
|
+
const pending = tasks.filter((t) => t.status !== "COMPLETED" && t.rewardStatus !== "GRANTED")
|
|
60
|
+
|
|
61
|
+
if (!pending.length) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: "done",
|
|
64
|
+
message: `新手福利 ${tasks.length} 项已全部完成(当前积分 ${before})`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 挨个完成
|
|
69
|
+
const claimed = []
|
|
70
|
+
const skipped = []
|
|
71
|
+
const failed = []
|
|
72
|
+
|
|
73
|
+
for (const t of pending) {
|
|
74
|
+
if (NOT_MANUALLY_COMPLETABLE.has(t.code)) {
|
|
75
|
+
skipped.push(`${t.title}(${t.points} 分,要真跑一次 AI 任务)`)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const r = await completeOnboardingTask(h.jwt, h.org, t.code, h.host)
|
|
80
|
+
claimed.push({ code: t.code, title: t.title, points: r.points || t.points })
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e?.code === CODE_NOT_SUPPORTED) {
|
|
83
|
+
skipped.push(`${t.title}(${t.points} 分,不支持手动完成)`)
|
|
84
|
+
} else {
|
|
85
|
+
failed.push(`${t.code}: ${e?.message ?? e}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 服务端限流留点余地
|
|
89
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let after = before
|
|
93
|
+
try {
|
|
94
|
+
after = (await fetchPoints(h.jwt, h.org, h.host)).total
|
|
95
|
+
} catch {}
|
|
96
|
+
|
|
97
|
+
const gained = after - before
|
|
98
|
+
const parts = []
|
|
99
|
+
if (gained > 0) parts.push(`积分 +${gained}(${before} → ${after})`)
|
|
100
|
+
else parts.push(`积分 ${after}(本次无变动)`)
|
|
101
|
+
if (claimed.length) parts.push(`完成 ${claimed.length} 项:${claimed.map((c) => c.title).join("、")}`)
|
|
102
|
+
if (skipped.length) parts.push(`需手动:${skipped.join("、")}`)
|
|
103
|
+
if (failed.length) parts.push(`失败 ${failed.length} 项:${failed.join(";")}`)
|
|
104
|
+
|
|
105
|
+
if (claimed.length) {
|
|
106
|
+
return {
|
|
107
|
+
outcome: "claimed",
|
|
108
|
+
credit: gained > 0 ? gained : undefined,
|
|
109
|
+
message: parts.join("。"),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
outcome: failed.length ? "failed" : "done",
|
|
115
|
+
message: parts.join("。"),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { NOT_MANUALLY_COMPLETABLE }
|