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.
@@ -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
- export function makeMethods(c) {
26
- return [
27
- {
28
- type: "oauth",
29
- label: `${c.SHORT} 浏览器登录`,
30
- async authorize() {
31
- // 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份
32
- const inviteCode = inviteToUse(c.id)
33
- const url = inviteCode
34
- ? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
35
- : `${c.web}/login`
36
- const hostFragment = new URL(c.web).hostname
37
-
38
- // ⓿ 本机浏览器里已经有这个站点的登录态 → 直接接上,不开窗口。
39
- //
40
- // 这是「有的人网页登录了、插件里却没多出账号」的正解:用户平时那个浏览器
41
- // 登录完,凭据就已经落在它的 localStorage 里了 —— 读文件就能拿到,
42
- // 不用再让他在这个窗口里重登一次。
43
- //
44
- // ⚠ 只在这个账号**还没加过**时自动接:否则用户点登录是想加新账号,
45
- // 自动接上的会是同一个,magpie 侧(同 accountId + 同 uid)只会替换旧账号。
46
- const local = readLocalCreds(hostFragment)
47
- if (local && !readKnownOrgs(c.id).includes(local.org)) {
48
- return {
49
- url,
50
- opened: true, // 凭据来自本机浏览器,我们没开页面 —— 别让 magpie 再开一个
51
- instructions: `已从本机 ${local.browser} 读到 ${c.SHORT} 的登录态,正在验证…`,
52
- method: "auto",
53
- async callback() {
54
- let r
55
- try {
56
- // 请求放在 callback 里:authorize 必须立刻返回,而且没被调用时不该打网络
57
- await fetchPoints(local.jwt, local.org, c.host)
58
- r = await issueGatewayKey(local.jwt, local.org, c.host)
59
- } catch (e) {
60
- // 本机那把失效了:记进「加过」名单,下次点登录就走窗口
61
- noteOrg(c.id, local.org)
62
- return {
63
- type: "failed",
64
- error: `本机 ${local.browser} 里的登录态已失效(${String(e?.message ?? e)})。再点一次登录会打开窗口;或者先去那个浏览器里重新登录。`,
65
- }
66
- }
67
- writeSession(c.id, local.jwt, local.org)
68
- noteOrg(c.id, local.org)
69
- rollInvite(c.id, local.jwt, local.org, c.host).catch(() => {})
70
- const who = await fetchWho(local.jwt, local.org, c.host).catch(() => null)
71
- return {
72
- type: "success",
73
- access: r.apiKey,
74
- refresh: r.refreshToken,
75
- expires: Date.now() + 2 * 3600_000,
76
- accountId: who?.name || local.org,
77
- uid: local.org,
78
- jwt: local.jwt,
79
- org: local.org,
80
- }
81
- },
82
- }
83
- }
84
-
85
- // 浏览器登录是**可选能力**,按需加载:这个模块用 child_process /
86
- // WebSocket / 端口探测,环境依赖最重。静态 import 一旦失败,
87
- // 整个插件都加载不起来 —— 连「登录态」那条备选也没了。
88
- let browser = null
89
- let why = ""
90
- try {
91
- browser = await import("../browser-login.mjs")
92
- } catch (e) {
93
- why = `浏览器登录模块加载失败:${String(e?.message ?? e).slice(0, 200)}`
94
- noteLoginFailure(c.id, why)
95
- }
96
-
97
- // ⚠ 启动失败的原因必须留下 —— 静默降级会让用户以为「本机没装 Chrome」
98
- const session = browser
99
- ? await browser.launchLoginBrowser(url).catch((e) => {
100
- why = String(e?.message ?? e).slice(0, 300)
101
- noteLoginFailure(c.id, `launch: ${why}`)
102
- return null
103
- })
104
- : null
105
-
106
- // 降级通道要用的「打开前剪贴板」(不 await:起 powershell 要几百毫秒)
107
- const skipPromise = session ? null : clipboardText()
108
-
109
- return {
110
- url,
111
- ...(session ? { opened: true } : {}),
112
- instructions: session
113
- ? "浏览器已打开官方登录页 —— 用你平时的方式登进去,这个窗口会自动继续,不用回来点任何东西。"
114
- : `没能自己拉起浏览器${why ? `(${why})` : "(本机没找到 Chrome / Edge)"},已改用系统浏览器打开登录页。\n登录完成后,在页面里按 F12 → Console 粘这一行:\n${CLIP_HINT}`,
115
- method: "auto",
116
- async callback() {
117
- let got = null
118
-
119
- // ① 首选:从我们自己拉起来的浏览器里读
120
- if (session && browser) {
121
- got = await browser.waitForCreds(session, hostFragment).catch(() => null)
122
- // 拿到就关窗:先 Browser.close 让它把 cookie 刷到磁盘,
123
- // 关不掉再 taskkill 整棵进程树
124
- await browser.closeSession(session).catch(() => {})
125
- }
126
-
127
- // ② 降级:读剪贴板
128
- if (!got && skipPromise) {
129
- const skip = (await skipPromise).trim()
130
- got = await pollClipboard({ skip })
131
- }
132
-
133
- if (!got) {
134
- return {
135
- type: "failed",
136
- error: session
137
- ? "浏览器开着了,但 5 分钟内没在里面读到登录状态 —— 是还没登完,还是登的不是这个站点?"
138
- : "5 分钟内没拿到登录状态。可以改用「登录态(JWT + 组织码)」那条备选。",
139
- }
140
- }
141
-
142
- try {
143
- if (!got.org) throw new Error("只拿到 JWT,没有组织码")
144
- // 真的验一次:读积分能过,说明 JWT + 组织码都对
145
- await fetchPoints(got.jwt, got.org, c.host)
146
- const key = await issueGatewayKey(got.jwt, got.org, c.host)
147
- writeSession(c.id, got.jwt, got.org)
148
- noteOrg(c.id, got.org)
149
- // 滚邀请码:把这个账号自己的码存成下一次用的
150
- rollInvite(c.id, got.jwt, got.org, c.host).catch(() => {})
151
- return {
152
- type: "success",
153
- access: key.apiKey,
154
- refresh: key.refreshToken,
155
- expires: Date.now() + 2 * 3600_000,
156
- // 卡片显示能认的名字(手机号 / 邮箱 / 昵称),不是随机组织码
157
- accountId: got.name || got.org,
158
- uid: got.org,
159
- jwt: got.jwt,
160
- org: got.org,
161
- }
162
- } catch (e) {
163
- return { type: "failed", error: String(e?.message ?? e) }
164
- }
165
- },
166
- }
167
- },
168
- },
169
- {
170
- // 兜底:直接从浏览器复制登录态。
171
- //
172
- // 两样都在 DevTools → Application → Local Storage → <站点域名>:
173
- // MAGIC:user:authorization → JSON,取 value = JWT
174
- // MAGIC:user:organizationCode → JSON,取 value = 组织码
175
- //
176
- // ⚠ 组织码**不能从 JWT 推**:JWT 里只有 cus.tenant_id(18 位数字),
177
- // 组织码是另一套短串。服务端按组织码查组织:
178
- // 缺了 → {"code":2182,"message":"用户所在组织不存在"}
179
- // 错了 → {"code":3035,"message":"登录失败"}
180
- type: "oauth",
181
- label: `${c.SHORT} 登录态(JWT + 组织码)`,
182
- prompts: [
183
- {
184
- type: "text",
185
- key: "jwt",
186
- message: "JWT(MAGIC:user:authorization 的 value)",
187
- placeholder: "eyJhbGciOi...",
188
- validate: (v) => (String(v || "").trim().startsWith("eyJ") ? undefined : "不是 JWT,要以 eyJ 开头"),
189
- },
190
- {
191
- type: "text",
192
- key: "org",
193
- message: "组织码(MAGIC:user:organizationCode 的 value)",
194
- placeholder: "orgcode67890",
195
- validate: (v) => (/^[a-z0-9]{6,32}$/i.test(String(v || "").trim()) ? undefined : "12 位左右的字母数字"),
196
- },
197
- ],
198
- async authorize(inputs) {
199
- const jwt = String(inputs?.jwt || "").trim().replace(/^Bearer\s+/i, "")
200
- const org = String(inputs?.org || "").trim()
201
- if (!jwt.startsWith("eyJ")) return { type: "failed", error: "不是 JWT" }
202
- if (!org) return { type: "failed", error: "没填组织码" }
203
-
204
- const result = (async () => {
205
- await fetchPoints(jwt, org, c.host)
206
- return issueGatewayKey(jwt, org, c.host)
207
- })()
208
-
209
- return {
210
- url: `${c.web}/login`,
211
- instructions: "正在验证登录态…",
212
- method: "auto",
213
- opened: false,
214
- async callback() {
215
- let r
216
- try {
217
- r = await result
218
- } catch (e) {
219
- return { type: "failed", error: String(e?.message ?? e) }
220
- }
221
- writeSession(c.id, jwt, org)
222
- noteOrg(c.id, org)
223
- rollInvite(c.id, jwt, org, c.host).catch(() => {})
224
- // 卡片上显示手机号/邮箱/昵称,取不到就退回组织码
225
- const who = await fetchWho(jwt, org, c.host).catch(() => null)
226
- return {
227
- type: "success",
228
- access: r.apiKey,
229
- refresh: r.refreshToken,
230
- expires: Date.now() + 2 * 3600_000,
231
- accountId: who?.name || org,
232
- uid: org,
233
- jwt,
234
- org,
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
+ }
@@ -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) out[m.id] = modelEntry(c, m.id, m.name, m.vision)
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
+