magicrew-auth 0.3.7 → 0.3.13
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 +367 -0
- package/README.md +208 -208
- package/docs/incident-2026-10-28-/347/254/254/344/272/214/344/270/252/350/264/246/345/217/267/344/270/262/345/217/267.md +68 -0
- package/docs/internals.md +82 -11
- package/index.mjs +3 -22
- package/package.json +1 -5
- package/src/api.mjs +506 -409
- package/src/browser-login.mjs +408 -408
- package/src/chat-task.mjs +335 -0
- package/src/clipboard.mjs +106 -106
- package/src/context.mjs +78 -60
- package/src/hooks/checkin.mjs +7 -0
- package/src/hooks/loader.mjs +93 -93
- package/src/hooks/methods.mjs +259 -241
- package/src/hooks/models.mjs +34 -2
- package/src/icons.mjs +24 -24
- package/src/inspect.mjs +191 -0
- package/src/keys.mjs +12 -1
- package/src/model-context.mjs +100 -100
- package/src/store.mjs +342 -220
- package/src/tasks.mjs +257 -62
- package/src/self-register.mjs +0 -234
package/src/hooks/methods.mjs
CHANGED
|
@@ -1,241 +1,259 @@
|
|
|
1
|
-
// ② auth.methods[] —— 登录
|
|
2
|
-
//
|
|
3
|
-
// 两条路,按可靠性排:
|
|
4
|
-
//
|
|
5
|
-
// ① 浏览器登录(默认,零输入):插件自己拉起一个能被 CDP 看进去的浏览器,
|
|
6
|
-
// 用户在官方页里用任何方式登,登录成功那一刻插件把 localStorage 读走。
|
|
7
|
-
// 拉不起来就降级成「magpie 开系统浏览器 + 用户粘一行 Console」。
|
|
8
|
-
// ② 登录态(JWT + 组织码):手填两个字段,兜底用。
|
|
9
|
-
//
|
|
10
|
-
// ⚠ 两条铁律:
|
|
11
|
-
// · authorize() 必须立刻返回 —— 真正等用户的是 callback(),死等会和 magpie 互锁
|
|
12
|
-
// · opened: true 的语义是「我自己已经开过页面了,你别再开」;
|
|
13
|
-
// 只返回 url 让它去开的,**不许**传 opened
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
issueGatewayKey,
|
|
17
|
-
fetchPoints,
|
|
18
|
-
fetchWho,
|
|
19
|
-
fetchOwnInviteCode,
|
|
20
|
-
} from "../api.mjs"
|
|
21
|
-
import { inviteToUse, readSession, writeSession, noteLoginFailure, noteOrg, readKnownOrgs, rollInvite } from "../store.mjs"
|
|
22
|
-
import { readLocalCreds } from "../local-browser.mjs"
|
|
23
|
-
import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1
|
+
// ② auth.methods[] —— 登录
|
|
2
|
+
//
|
|
3
|
+
// 两条路,按可靠性排:
|
|
4
|
+
//
|
|
5
|
+
// ① 浏览器登录(默认,零输入):插件自己拉起一个能被 CDP 看进去的浏览器,
|
|
6
|
+
// 用户在官方页里用任何方式登,登录成功那一刻插件把 localStorage 读走。
|
|
7
|
+
// 拉不起来就降级成「magpie 开系统浏览器 + 用户粘一行 Console」。
|
|
8
|
+
// ② 登录态(JWT + 组织码):手填两个字段,兜底用。
|
|
9
|
+
//
|
|
10
|
+
// ⚠ 两条铁律:
|
|
11
|
+
// · authorize() 必须立刻返回 —— 真正等用户的是 callback(),死等会和 magpie 互锁
|
|
12
|
+
// · opened: true 的语义是「我自己已经开过页面了,你别再开」;
|
|
13
|
+
// 只返回 url 让它去开的,**不许**传 opened
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
issueGatewayKey,
|
|
17
|
+
fetchPoints,
|
|
18
|
+
fetchWho,
|
|
19
|
+
fetchOwnInviteCode,
|
|
20
|
+
} from "../api.mjs"
|
|
21
|
+
import { inviteToUse, readSession, writeSession, noteLoginFailure, noteOrg, readKnownOrgs, rollInvite } from "../store.mjs"
|
|
22
|
+
import { readLocalCreds } from "../local-browser.mjs"
|
|
23
|
+
import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 账号刚加进来(登录成功)→ 一次性把「首次 AI 工作 / 首个对话」补掉。
|
|
27
|
+
*
|
|
28
|
+
* 不 await、不抛:建项目 + 发消息 + 兜底等结算最坏一分钟,登录必须立刻返回;
|
|
29
|
+
* 失败也不该让登录报错。真正的兜底是签到 —— 两边共用同一道闸门
|
|
30
|
+
* (`isAutoChatSettled`:每账号一次,永久),所以重复登录、刷新页面都不会重发。
|
|
31
|
+
*/
|
|
32
|
+
function kickoffFirstTask(c, jwt, org) {
|
|
33
|
+
import("../tasks.mjs")
|
|
34
|
+
.then((m) => m.ensureFirstTaskOnce({ jwt, org, host: c.host }))
|
|
35
|
+
.catch(() => {})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeMethods(c) {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
type: "oauth",
|
|
42
|
+
label: `${c.SHORT} 浏览器登录`,
|
|
43
|
+
async authorize() {
|
|
44
|
+
// 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份
|
|
45
|
+
const inviteCode = inviteToUse(c.id)
|
|
46
|
+
const url = inviteCode
|
|
47
|
+
? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
|
|
48
|
+
: `${c.web}/login`
|
|
49
|
+
const hostFragment = new URL(c.web).hostname
|
|
50
|
+
|
|
51
|
+
// ⓿ 本机浏览器里已经有这个站点的登录态 → 直接接上,不开窗口。
|
|
52
|
+
//
|
|
53
|
+
// 这是「有的人网页登录了、插件里却没多出账号」的正解:用户平时那个浏览器
|
|
54
|
+
// 登录完,凭据就已经落在它的 localStorage 里了 —— 读文件就能拿到,
|
|
55
|
+
// 不用再让他在这个窗口里重登一次。
|
|
56
|
+
//
|
|
57
|
+
// ⚠ 只在这个账号**还没加过**时自动接:否则用户点登录是想加新账号,
|
|
58
|
+
// 自动接上的会是同一个,magpie 侧(同 accountId + 同 uid)只会替换旧账号。
|
|
59
|
+
const local = readLocalCreds(hostFragment)
|
|
60
|
+
if (local && !readKnownOrgs(c.id).includes(local.org)) {
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
opened: true, // 凭据来自本机浏览器,我们没开页面 —— 别让 magpie 再开一个
|
|
64
|
+
instructions: `已从本机 ${local.browser} 读到 ${c.SHORT} 的登录态,正在验证…`,
|
|
65
|
+
method: "auto",
|
|
66
|
+
async callback() {
|
|
67
|
+
let r
|
|
68
|
+
try {
|
|
69
|
+
// 请求放在 callback 里:authorize 必须立刻返回,而且没被调用时不该打网络
|
|
70
|
+
await fetchPoints(local.jwt, local.org, c.host)
|
|
71
|
+
r = await issueGatewayKey(local.jwt, local.org, c.host)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// 本机那把失效了:记进「加过」名单,下次点登录就走窗口
|
|
74
|
+
noteOrg(c.id, local.org)
|
|
75
|
+
return {
|
|
76
|
+
type: "failed",
|
|
77
|
+
error: `本机 ${local.browser} 里的登录态已失效(${String(e?.message ?? e)})。再点一次登录会打开窗口;或者先去那个浏览器里重新登录。`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
writeSession(c.id, local.jwt, local.org)
|
|
81
|
+
noteOrg(c.id, local.org)
|
|
82
|
+
// 等它落盘再返回:这条以前是 .catch(()=>{}) 发射后不管,
|
|
83
|
+
// 一旦 host 进程在 callback 之后被回收,滚动就永远不生效
|
|
84
|
+
await rollInvite(c.id, local.jwt, local.org, c.host)
|
|
85
|
+
kickoffFirstTask(c, local.jwt, local.org)
|
|
86
|
+
const who = await fetchWho(local.jwt, local.org, c.host).catch(() => null)
|
|
87
|
+
return {
|
|
88
|
+
type: "success",
|
|
89
|
+
access: r.apiKey,
|
|
90
|
+
refresh: r.refreshToken,
|
|
91
|
+
expires: Date.now() + 2 * 3600_000,
|
|
92
|
+
accountId: who?.name || local.org,
|
|
93
|
+
uid: local.org,
|
|
94
|
+
jwt: local.jwt,
|
|
95
|
+
org: local.org,
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 浏览器登录是**可选能力**,按需加载:这个模块用 child_process /
|
|
102
|
+
// WebSocket / 端口探测,环境依赖最重。静态 import 一旦失败,
|
|
103
|
+
// 整个插件都加载不起来 —— 连「登录态」那条备选也没了。
|
|
104
|
+
let browser = null
|
|
105
|
+
let why = ""
|
|
106
|
+
try {
|
|
107
|
+
browser = await import("../browser-login.mjs")
|
|
108
|
+
} catch (e) {
|
|
109
|
+
why = `浏览器登录模块加载失败:${String(e?.message ?? e).slice(0, 200)}`
|
|
110
|
+
noteLoginFailure(c.id, why)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ⚠ 启动失败的原因必须留下 —— 静默降级会让用户以为「本机没装 Chrome」
|
|
114
|
+
const session = browser
|
|
115
|
+
? await browser.launchLoginBrowser(url).catch((e) => {
|
|
116
|
+
why = String(e?.message ?? e).slice(0, 300)
|
|
117
|
+
noteLoginFailure(c.id, `launch: ${why}`)
|
|
118
|
+
return null
|
|
119
|
+
})
|
|
120
|
+
: null
|
|
121
|
+
|
|
122
|
+
// 降级通道要用的「打开前剪贴板」(不 await:起 powershell 要几百毫秒)
|
|
123
|
+
const skipPromise = session ? null : clipboardText()
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
url,
|
|
127
|
+
...(session ? { opened: true } : {}),
|
|
128
|
+
instructions: session
|
|
129
|
+
? "浏览器已打开官方登录页 —— 用你平时的方式登进去,这个窗口会自动继续,不用回来点任何东西。"
|
|
130
|
+
: `没能自己拉起浏览器${why ? `(${why})` : "(本机没找到 Chrome / Edge)"},已改用系统浏览器打开登录页。\n登录完成后,在页面里按 F12 → Console 粘这一行:\n${CLIP_HINT}`,
|
|
131
|
+
method: "auto",
|
|
132
|
+
async callback() {
|
|
133
|
+
let got = null
|
|
134
|
+
|
|
135
|
+
// ① 首选:从我们自己拉起来的浏览器里读
|
|
136
|
+
if (session && browser) {
|
|
137
|
+
got = await browser.waitForCreds(session, hostFragment).catch(() => null)
|
|
138
|
+
// 拿到就关窗:先 Browser.close 让它把 cookie 刷到磁盘,
|
|
139
|
+
// 关不掉再 taskkill 整棵进程树
|
|
140
|
+
await browser.closeSession(session).catch(() => {})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ② 降级:读剪贴板
|
|
144
|
+
if (!got && skipPromise) {
|
|
145
|
+
const skip = (await skipPromise).trim()
|
|
146
|
+
got = await pollClipboard({ skip })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!got) {
|
|
150
|
+
return {
|
|
151
|
+
type: "failed",
|
|
152
|
+
error: session
|
|
153
|
+
? "浏览器开着了,但 5 分钟内没在里面读到登录状态 —— 是还没登完,还是登的不是这个站点?"
|
|
154
|
+
: "5 分钟内没拿到登录状态。可以改用「登录态(JWT + 组织码)」那条备选。",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
if (!got.org) throw new Error("只拿到 JWT,没有组织码")
|
|
160
|
+
// 真的验一次:读积分能过,说明 JWT + 组织码都对
|
|
161
|
+
await fetchPoints(got.jwt, got.org, c.host)
|
|
162
|
+
const key = await issueGatewayKey(got.jwt, got.org, c.host)
|
|
163
|
+
writeSession(c.id, got.jwt, got.org)
|
|
164
|
+
noteOrg(c.id, got.org)
|
|
165
|
+
// 滚邀请码:把这个账号自己的码存成下一次用的(等落盘再返回)
|
|
166
|
+
await rollInvite(c.id, got.jwt, got.org, c.host)
|
|
167
|
+
kickoffFirstTask(c, got.jwt, got.org)
|
|
168
|
+
return {
|
|
169
|
+
type: "success",
|
|
170
|
+
access: key.apiKey,
|
|
171
|
+
refresh: key.refreshToken,
|
|
172
|
+
expires: Date.now() + 2 * 3600_000,
|
|
173
|
+
// 卡片显示能认的名字(手机号 / 邮箱 / 昵称),不是随机组织码
|
|
174
|
+
accountId: got.name || got.org,
|
|
175
|
+
uid: got.org,
|
|
176
|
+
jwt: got.jwt,
|
|
177
|
+
org: got.org,
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return { type: "failed", error: String(e?.message ?? e) }
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
// 兜底:直接从浏览器复制登录态。
|
|
188
|
+
//
|
|
189
|
+
// 两样都在 DevTools → Application → Local Storage → <站点域名>:
|
|
190
|
+
// MAGIC:user:authorization → JSON,取 value = JWT
|
|
191
|
+
// MAGIC:user:organizationCode → JSON,取 value = 组织码
|
|
192
|
+
//
|
|
193
|
+
// ⚠ 组织码**不能从 JWT 推**:JWT 里只有 cus.tenant_id(18 位数字),
|
|
194
|
+
// 组织码是另一套短串。服务端按组织码查组织:
|
|
195
|
+
// 缺了 → {"code":2182,"message":"用户所在组织不存在"}
|
|
196
|
+
// 错了 → {"code":3035,"message":"登录失败"}
|
|
197
|
+
type: "oauth",
|
|
198
|
+
label: `${c.SHORT} 登录态(JWT + 组织码)`,
|
|
199
|
+
prompts: [
|
|
200
|
+
{
|
|
201
|
+
type: "text",
|
|
202
|
+
key: "jwt",
|
|
203
|
+
message: "JWT(MAGIC:user:authorization 的 value)",
|
|
204
|
+
placeholder: "eyJhbGciOi...",
|
|
205
|
+
validate: (v) => (String(v || "").trim().startsWith("eyJ") ? undefined : "不是 JWT,要以 eyJ 开头"),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
key: "org",
|
|
210
|
+
message: "组织码(MAGIC:user:organizationCode 的 value)",
|
|
211
|
+
placeholder: "orgcode67890",
|
|
212
|
+
validate: (v) => (/^[a-z0-9]{6,32}$/i.test(String(v || "").trim()) ? undefined : "12 位左右的字母数字"),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
async authorize(inputs) {
|
|
216
|
+
const jwt = String(inputs?.jwt || "").trim().replace(/^Bearer\s+/i, "")
|
|
217
|
+
const org = String(inputs?.org || "").trim()
|
|
218
|
+
if (!jwt.startsWith("eyJ")) return { type: "failed", error: "不是 JWT" }
|
|
219
|
+
if (!org) return { type: "failed", error: "没填组织码" }
|
|
220
|
+
|
|
221
|
+
const result = (async () => {
|
|
222
|
+
await fetchPoints(jwt, org, c.host)
|
|
223
|
+
return issueGatewayKey(jwt, org, c.host)
|
|
224
|
+
})()
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
url: `${c.web}/login`,
|
|
228
|
+
instructions: "正在验证登录态…",
|
|
229
|
+
method: "auto",
|
|
230
|
+
opened: false,
|
|
231
|
+
async callback() {
|
|
232
|
+
let r
|
|
233
|
+
try {
|
|
234
|
+
r = await result
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return { type: "failed", error: String(e?.message ?? e) }
|
|
237
|
+
}
|
|
238
|
+
writeSession(c.id, jwt, org)
|
|
239
|
+
noteOrg(c.id, org)
|
|
240
|
+
await rollInvite(c.id, jwt, org, c.host)
|
|
241
|
+
kickoffFirstTask(c, jwt, org)
|
|
242
|
+
// 卡片上显示手机号/邮箱/昵称,取不到就退回组织码
|
|
243
|
+
const who = await fetchWho(jwt, org, c.host).catch(() => null)
|
|
244
|
+
return {
|
|
245
|
+
type: "success",
|
|
246
|
+
access: r.apiKey,
|
|
247
|
+
refresh: r.refreshToken,
|
|
248
|
+
expires: Date.now() + 2 * 3600_000,
|
|
249
|
+
accountId: who?.name || org,
|
|
250
|
+
uid: org,
|
|
251
|
+
jwt,
|
|
252
|
+
org,
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
}
|
package/src/hooks/models.mjs
CHANGED
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 登录态失效 → 抛带 signIn: "expired" 的错误。
|
|
11
11
|
|
|
12
|
-
import { fetchModels } from "../api.mjs"
|
|
12
|
+
import { fetchModels, fetchModelPoints } from "../api.mjs"
|
|
13
13
|
import { modelEntry } from "../context.mjs"
|
|
14
14
|
import { readSnapshot, writeSnapshot } from "../store.mjs"
|
|
15
15
|
|
|
16
|
+
// 倍率系数:input_points / 2000 与网站实测值吻合
|
|
17
|
+
// Qwen3.5 Flash 20 → 0.01 (网站 0.01) ✓
|
|
18
|
+
// Qwen3.8 Flash 100 → 0.05 (网站 0.05) ✓
|
|
19
|
+
// Qwen3.7 Plus 200 → 0.10 (网站 0.10) ✓
|
|
20
|
+
// Qwen3.8 Max 1200 → 0.60 (网站 0.56)
|
|
21
|
+
const MULT_BASE = 2000
|
|
22
|
+
|
|
16
23
|
export function makeModels(c) {
|
|
17
24
|
return async (provider, { auth } = {}) => {
|
|
18
25
|
const snapshot = readSnapshot(c.id)
|
|
@@ -22,8 +29,32 @@ export function makeModels(c) {
|
|
|
22
29
|
const live = await fetchModels(auth.jwt, auth.org, c.host)
|
|
23
30
|
if (!live.length) return snapshot ?? {}
|
|
24
31
|
|
|
32
|
+
// 计费信息拿不到就退回原名,不挡列表
|
|
33
|
+
const points = await fetchModelPoints(auth.jwt, auth.org, live.map((m) => m.id), c.host).catch(() => ({}))
|
|
34
|
+
const pointsOk = Object.keys(points).length > 0
|
|
35
|
+
|
|
25
36
|
const out = {}
|
|
26
|
-
for (const m of live)
|
|
37
|
+
for (const m of live) {
|
|
38
|
+
// VIP / 锁定的模型不列 —— 选中会报错,插件注释里那条铁律
|
|
39
|
+
// (语言模型这批实测 tags 全是 `Magic`,没有 VIP;VIP 只出现在
|
|
40
|
+
// 网站的「生图模型 / 视频模型」里,那两个接口不在本插件范围内)
|
|
41
|
+
const locked = (m.tags || []).some((t) => /vip|lock|pay|premium/i.test(String(t?.value ?? "")))
|
|
42
|
+
if (locked) continue
|
|
43
|
+
|
|
44
|
+
const p = points[m.id] || {}
|
|
45
|
+
const hasPoints = p.inputPoints != null && Number(p.inputPoints) > 0
|
|
46
|
+
// 计费接口整体成功、但这条没价格 → 不列。
|
|
47
|
+
// 不填 rate 就不会画倍率胶囊(正确),但也别让它出现在列表里 ——
|
|
48
|
+
// 用户选了不知道花多少。接口整体失败时不过滤,免得列表空掉。
|
|
49
|
+
if (pointsOk && !hasPoints) continue
|
|
50
|
+
|
|
51
|
+
// name 保持干净,倍率走 magpie 自己的 `rate` 字段 → 渲染成灰色胶囊。
|
|
52
|
+
// (之前试过拼进 name、也试过塞进 cost,都不对:抓 codearts 插件的
|
|
53
|
+
// modelOf 才看到宿主认的是 rate / free 这两个字段)
|
|
54
|
+
out[m.id] = modelEntry(c, m.id, m.name, m.vision, {
|
|
55
|
+
rate: hasPoints ? Number(p.inputPoints) / MULT_BASE : 0,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
27
58
|
writeSnapshot(c.id, out)
|
|
28
59
|
return out
|
|
29
60
|
} catch (e) {
|
|
@@ -35,3 +66,4 @@ export function makeModels(c) {
|
|
|
35
66
|
}
|
|
36
67
|
}
|
|
37
68
|
}
|
|
69
|
+
|