magicrew-auth 0.3.8 → 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 +254 -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 +43 -1
- package/index.mjs +3 -22
- package/package.json +1 -5
- package/src/api.mjs +57 -0
- 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/loader.mjs +93 -93
- package/src/hooks/methods.mjs +16 -0
- package/src/hooks/models.mjs +34 -2
- package/src/icons.mjs +24 -24
- package/src/inspect.mjs +191 -0
- package/src/model-context.mjs +100 -100
- package/src/store.mjs +103 -0
- package/src/tasks.mjs +182 -5
- package/src/self-register.mjs +0 -234
package/src/context.mjs
CHANGED
|
@@ -1,60 +1,78 @@
|
|
|
1
|
-
// 站点上下文 —— 每个供应商拿到的那一份「我是谁」
|
|
2
|
-
//
|
|
3
|
-
// 一个包两个供应商,所以这里不是常量,是**按站点生成**的:
|
|
4
|
-
//
|
|
5
|
-
// siteCtx(SITES.cn) → magicrew 超级麦吉
|
|
6
|
-
// siteCtx(SITES.intl) → magicrew-intl MagiCrew
|
|
7
|
-
//
|
|
8
|
-
// 所有小插头(hooks/*)拿到的都是这个对象,它们不自己去认站点。
|
|
9
|
-
|
|
10
|
-
import { SITES } from "./api.mjs"
|
|
11
|
-
import { ICON_BY_SITE } from "./icons.mjs"
|
|
12
|
-
import { contextOf } from "./model-context.mjs"
|
|
13
|
-
|
|
14
|
-
export { SITES }
|
|
15
|
-
|
|
16
|
-
/** 决定 API 形状,别乱选(见 src/api.mjs 的 base 与 hooks/models 的 api 字段) */
|
|
17
|
-
export const SDK = "@ai-sdk/openai-compatible"
|
|
18
|
-
|
|
19
|
-
/** api_key 只活 2 小时时,提前多久换 */
|
|
20
|
-
export const REFRESH_LEAD_MS = 10 * 60_000
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 站点 → 上下文。
|
|
24
|
-
*
|
|
25
|
-
* 除了 SITES 里那些(id / host / web / needsOrgOnGateway),再补三个派生值:
|
|
26
|
-
* SHORT 卡片上显示的短名
|
|
27
|
-
* BASE 模型网关 <host>/v1
|
|
28
|
-
* ICON 这个供应商自己的图标(插件卡片的图标另外在 package.json.magpie.icon)
|
|
29
|
-
*/
|
|
30
|
-
export function siteCtx(SITE) {
|
|
31
|
-
return {
|
|
32
|
-
...SITE,
|
|
33
|
-
PROVIDER: SITE.id,
|
|
34
|
-
SHORT: SITE.id === "magicrew" ? "超级麦吉" : "MagiCrew",
|
|
35
|
-
BASE: `${SITE.host}/v1`,
|
|
36
|
-
ICON: SITE.id === "magicrew" ? ICON_BY_SITE.cn : ICON_BY_SITE.intl,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
// 站点上下文 —— 每个供应商拿到的那一份「我是谁」
|
|
2
|
+
//
|
|
3
|
+
// 一个包两个供应商,所以这里不是常量,是**按站点生成**的:
|
|
4
|
+
//
|
|
5
|
+
// siteCtx(SITES.cn) → magicrew 超级麦吉
|
|
6
|
+
// siteCtx(SITES.intl) → magicrew-intl MagiCrew
|
|
7
|
+
//
|
|
8
|
+
// 所有小插头(hooks/*)拿到的都是这个对象,它们不自己去认站点。
|
|
9
|
+
|
|
10
|
+
import { SITES } from "./api.mjs"
|
|
11
|
+
import { ICON_BY_SITE } from "./icons.mjs"
|
|
12
|
+
import { contextOf } from "./model-context.mjs"
|
|
13
|
+
|
|
14
|
+
export { SITES }
|
|
15
|
+
|
|
16
|
+
/** 决定 API 形状,别乱选(见 src/api.mjs 的 base 与 hooks/models 的 api 字段) */
|
|
17
|
+
export const SDK = "@ai-sdk/openai-compatible"
|
|
18
|
+
|
|
19
|
+
/** api_key 只活 2 小时时,提前多久换 */
|
|
20
|
+
export const REFRESH_LEAD_MS = 10 * 60_000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 站点 → 上下文。
|
|
24
|
+
*
|
|
25
|
+
* 除了 SITES 里那些(id / host / web / needsOrgOnGateway),再补三个派生值:
|
|
26
|
+
* SHORT 卡片上显示的短名
|
|
27
|
+
* BASE 模型网关 <host>/v1
|
|
28
|
+
* ICON 这个供应商自己的图标(插件卡片的图标另外在 package.json.magpie.icon)
|
|
29
|
+
*/
|
|
30
|
+
export function siteCtx(SITE) {
|
|
31
|
+
return {
|
|
32
|
+
...SITE,
|
|
33
|
+
PROVIDER: SITE.id,
|
|
34
|
+
SHORT: SITE.id === "magicrew" ? "超级麦吉" : "MagiCrew",
|
|
35
|
+
BASE: `${SITE.host}/v1`,
|
|
36
|
+
ICON: SITE.id === "magicrew" ? ICON_BY_SITE.cn : ICON_BY_SITE.intl,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 把一条 flows/models 变成一个 magpie 模型条目。
|
|
42
|
+
*
|
|
43
|
+
* `opts.rate` / `opts.free` 是 **magpie 自己的字段**(从 codearts 插件里挖到的,
|
|
44
|
+
* packages/codearts/index.mjs 的 modelOf):
|
|
45
|
+
*
|
|
46
|
+
* free boolean → 名字后面画一个绿色的「免费」胶囊
|
|
47
|
+
* rate number → 画一个灰色的倍率胶囊(0.09× 这种,就是网站上的「消耗系数」)
|
|
48
|
+
*
|
|
49
|
+
* 关键规则(codearts 原注释):**free 的时候不给 rate** ——
|
|
50
|
+
* 免费模型不该再标一个倍率,两个胶囊并排没意义。
|
|
51
|
+
* 没有 rate 就什么都不画,正好符合「没有倍率就不显示」。
|
|
52
|
+
*
|
|
53
|
+
* ⚠ 别再用 name 拼倍率了:那样筛选框会连倍率一起匹配,
|
|
54
|
+
* 而且和真正的胶囊样式差得远。
|
|
55
|
+
*/
|
|
56
|
+
export function modelEntry(c, id, name, vision, opts = {}) {
|
|
57
|
+
const { rate = 0, free = false } = opts
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
name,
|
|
61
|
+
api: { id, url: c.BASE, npm: SDK },
|
|
62
|
+
status: "active",
|
|
63
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
64
|
+
// 上下文窗口向 magpie 的模型库查 —— 两个模型接口都不返回它,
|
|
65
|
+
// 早前一律填 256K,结果 agent 拿 256K 判断"放不下",而实际是 1M
|
|
66
|
+
limit: { context: contextOf(id), output: 64_000 },
|
|
67
|
+
capabilities: {
|
|
68
|
+
temperature: true,
|
|
69
|
+
reasoning: true,
|
|
70
|
+
toolcall: true,
|
|
71
|
+
attachment: !!vision,
|
|
72
|
+
input: { text: true, image: !!vision },
|
|
73
|
+
output: { text: true },
|
|
74
|
+
},
|
|
75
|
+
...(free ? { free: true } : {}),
|
|
76
|
+
...(!free && Number(rate) > 0 ? { rate: Number(rate) } : {}),
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/hooks/loader.mjs
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
// ④ auth.loader(getAuth) —— 请求接管
|
|
2
|
-
//
|
|
3
|
-
// 返回 baseURL / apiKey / headers / fetch(a,b),magpie 拿它发真实请求。
|
|
4
|
-
// 这是唯一能改请求体的地方。本项目用它干两件事:
|
|
5
|
-
//
|
|
6
|
-
// · 国际站网关强制要 organization-code(国内站不要),不带就是
|
|
7
|
-
// HTTP 500 "Internal Server Error."(22 字节纯文本,不是 JSON)
|
|
8
|
-
// · 推理模型低于 512 的 max_tokens 会被 reasoning 吃光,content 返回空串
|
|
9
|
-
//
|
|
10
|
-
// ⚠ 不要在这里读流、不要重试换号、不要手拼 x-api-key ——
|
|
11
|
-
// 前两件 magpie 做了,第三件看 SDK 包自动加。见 README「最小交付面」。
|
|
12
|
-
|
|
13
|
-
import { ensureKey, accountOf } from "../keys.mjs"
|
|
14
|
-
|
|
15
|
-
export function makeLoader(c) {
|
|
16
|
-
return async (getAuth) => {
|
|
17
|
-
const auth = await getAuth()
|
|
18
|
-
if (!auth) return {}
|
|
19
|
-
return {
|
|
20
|
-
baseURL: c.BASE,
|
|
21
|
-
apiKey: auth.access,
|
|
22
|
-
headers: { "user-agent": "MagicrewClient/1.0" },
|
|
23
|
-
async fetch(input, init) {
|
|
24
|
-
const fresh = await getAuth()
|
|
25
|
-
if (!fresh) throw new Error(`${c.SHORT} 未登录`)
|
|
26
|
-
|
|
27
|
-
// key 只有一个来源(src/keys.mjs)—— 服务端签一把顶一把,多入口必互相吊销
|
|
28
|
-
const key = await ensureKey(c, getAuth)
|
|
29
|
-
if (!key) throw new Error(`${c.SHORT} 未登录(拿不到可用的 key)`)
|
|
30
|
-
const account = accountOf(fresh)
|
|
31
|
-
|
|
32
|
-
const req = input instanceof Request ? input : null
|
|
33
|
-
const url =
|
|
34
|
-
typeof input === "string"
|
|
35
|
-
? input
|
|
36
|
-
: input instanceof URL
|
|
37
|
-
? input.toString()
|
|
38
|
-
: (req?.url ?? "")
|
|
39
|
-
|
|
40
|
-
const incoming = new Headers(init?.headers ?? req?.headers ?? {})
|
|
41
|
-
const headers = {
|
|
42
|
-
authorization: `Bearer ${key.apiKey}`,
|
|
43
|
-
"content-type": "application/json",
|
|
44
|
-
}
|
|
45
|
-
if (c.needsOrgOnGateway && fresh.org) headers["organization-code"] = fresh.org
|
|
46
|
-
|
|
47
|
-
for (const [k, v] of incoming) {
|
|
48
|
-
if (["authorization", "host", "content-length", "user-agent"].includes(k.toLowerCase())) continue
|
|
49
|
-
headers[k] = v
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let body = init?.body
|
|
53
|
-
if (body === undefined && req) body = await req.text()
|
|
54
|
-
|
|
55
|
-
// 改写两处请求体,都为实测出来的:
|
|
56
|
-
//
|
|
57
|
-
// ① max_tokens 抬到 512
|
|
58
|
-
// 实测 glm-5.3-flash:64 → finish_reason:"length" 且 content:""
|
|
59
|
-
// 512 → 正常出话(reasoning 208 / total 233)
|
|
60
|
-
//
|
|
61
|
-
// ② 流式请求补 stream_options.include_usage
|
|
62
|
-
// magicrew 在 stream:true 下**默认不返回 usage**;补上之后,[DONE]
|
|
63
|
-
// 之前会多一条 {"choices":[],"usage":{...}} 的收尾 chunk。
|
|
64
|
-
// magpie 新版靠这条收尾 chunk 判定「流正常结束了」——缺了它就报
|
|
65
|
-
// 「Stream ended without finish_reason」然后重试(实测 2026-10-27)。
|
|
66
|
-
// 控制台里看到的就是:模型请求重试 (3/5) · Stream ended without finish_reason
|
|
67
|
-
if (typeof body === "string" && body.includes('"stream"')) {
|
|
68
|
-
try {
|
|
69
|
-
const j = JSON.parse(body)
|
|
70
|
-
if (Number(j.max_tokens) > 0 && Number(j.max_tokens) < 512) j.max_tokens = 512
|
|
71
|
-
if (j.stream === true && !j.stream_options) {
|
|
72
|
-
j.stream_options = { include_usage: true }
|
|
73
|
-
}
|
|
74
|
-
body = JSON.stringify(j)
|
|
75
|
-
} catch {}
|
|
76
|
-
} else if (typeof body === "string" && body.includes('"max_tokens"')) {
|
|
77
|
-
try {
|
|
78
|
-
const j = JSON.parse(body)
|
|
79
|
-
if (Number(j.max_tokens) > 0 && Number(j.max_tokens) < 512) j.max_tokens = 512
|
|
80
|
-
body = JSON.stringify(j)
|
|
81
|
-
} catch {}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return fetch(url, {
|
|
85
|
-
method: init?.method ?? "POST",
|
|
86
|
-
headers,
|
|
87
|
-
body,
|
|
88
|
-
signal: init?.signal ?? req?.signal ?? AbortSignal.timeout(300_000),
|
|
89
|
-
})
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
// ④ auth.loader(getAuth) —— 请求接管
|
|
2
|
+
//
|
|
3
|
+
// 返回 baseURL / apiKey / headers / fetch(a,b),magpie 拿它发真实请求。
|
|
4
|
+
// 这是唯一能改请求体的地方。本项目用它干两件事:
|
|
5
|
+
//
|
|
6
|
+
// · 国际站网关强制要 organization-code(国内站不要),不带就是
|
|
7
|
+
// HTTP 500 "Internal Server Error."(22 字节纯文本,不是 JSON)
|
|
8
|
+
// · 推理模型低于 512 的 max_tokens 会被 reasoning 吃光,content 返回空串
|
|
9
|
+
//
|
|
10
|
+
// ⚠ 不要在这里读流、不要重试换号、不要手拼 x-api-key ——
|
|
11
|
+
// 前两件 magpie 做了,第三件看 SDK 包自动加。见 README「最小交付面」。
|
|
12
|
+
|
|
13
|
+
import { ensureKey, accountOf } from "../keys.mjs"
|
|
14
|
+
|
|
15
|
+
export function makeLoader(c) {
|
|
16
|
+
return async (getAuth) => {
|
|
17
|
+
const auth = await getAuth()
|
|
18
|
+
if (!auth) return {}
|
|
19
|
+
return {
|
|
20
|
+
baseURL: c.BASE,
|
|
21
|
+
apiKey: auth.access,
|
|
22
|
+
headers: { "user-agent": "MagicrewClient/1.0" },
|
|
23
|
+
async fetch(input, init) {
|
|
24
|
+
const fresh = await getAuth()
|
|
25
|
+
if (!fresh) throw new Error(`${c.SHORT} 未登录`)
|
|
26
|
+
|
|
27
|
+
// key 只有一个来源(src/keys.mjs)—— 服务端签一把顶一把,多入口必互相吊销
|
|
28
|
+
const key = await ensureKey(c, getAuth)
|
|
29
|
+
if (!key) throw new Error(`${c.SHORT} 未登录(拿不到可用的 key)`)
|
|
30
|
+
const account = accountOf(fresh)
|
|
31
|
+
|
|
32
|
+
const req = input instanceof Request ? input : null
|
|
33
|
+
const url =
|
|
34
|
+
typeof input === "string"
|
|
35
|
+
? input
|
|
36
|
+
: input instanceof URL
|
|
37
|
+
? input.toString()
|
|
38
|
+
: (req?.url ?? "")
|
|
39
|
+
|
|
40
|
+
const incoming = new Headers(init?.headers ?? req?.headers ?? {})
|
|
41
|
+
const headers = {
|
|
42
|
+
authorization: `Bearer ${key.apiKey}`,
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
}
|
|
45
|
+
if (c.needsOrgOnGateway && fresh.org) headers["organization-code"] = fresh.org
|
|
46
|
+
|
|
47
|
+
for (const [k, v] of incoming) {
|
|
48
|
+
if (["authorization", "host", "content-length", "user-agent"].includes(k.toLowerCase())) continue
|
|
49
|
+
headers[k] = v
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let body = init?.body
|
|
53
|
+
if (body === undefined && req) body = await req.text()
|
|
54
|
+
|
|
55
|
+
// 改写两处请求体,都为实测出来的:
|
|
56
|
+
//
|
|
57
|
+
// ① max_tokens 抬到 512
|
|
58
|
+
// 实测 glm-5.3-flash:64 → finish_reason:"length" 且 content:""
|
|
59
|
+
// 512 → 正常出话(reasoning 208 / total 233)
|
|
60
|
+
//
|
|
61
|
+
// ② 流式请求补 stream_options.include_usage
|
|
62
|
+
// magicrew 在 stream:true 下**默认不返回 usage**;补上之后,[DONE]
|
|
63
|
+
// 之前会多一条 {"choices":[],"usage":{...}} 的收尾 chunk。
|
|
64
|
+
// magpie 新版靠这条收尾 chunk 判定「流正常结束了」——缺了它就报
|
|
65
|
+
// 「Stream ended without finish_reason」然后重试(实测 2026-10-27)。
|
|
66
|
+
// 控制台里看到的就是:模型请求重试 (3/5) · Stream ended without finish_reason
|
|
67
|
+
if (typeof body === "string" && body.includes('"stream"')) {
|
|
68
|
+
try {
|
|
69
|
+
const j = JSON.parse(body)
|
|
70
|
+
if (Number(j.max_tokens) > 0 && Number(j.max_tokens) < 512) j.max_tokens = 512
|
|
71
|
+
if (j.stream === true && !j.stream_options) {
|
|
72
|
+
j.stream_options = { include_usage: true }
|
|
73
|
+
}
|
|
74
|
+
body = JSON.stringify(j)
|
|
75
|
+
} catch {}
|
|
76
|
+
} else if (typeof body === "string" && body.includes('"max_tokens"')) {
|
|
77
|
+
try {
|
|
78
|
+
const j = JSON.parse(body)
|
|
79
|
+
if (Number(j.max_tokens) > 0 && Number(j.max_tokens) < 512) j.max_tokens = 512
|
|
80
|
+
body = JSON.stringify(j)
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return fetch(url, {
|
|
85
|
+
method: init?.method ?? "POST",
|
|
86
|
+
headers,
|
|
87
|
+
body,
|
|
88
|
+
signal: init?.signal ?? req?.signal ?? AbortSignal.timeout(300_000),
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/hooks/methods.mjs
CHANGED
|
@@ -22,6 +22,19 @@ import { inviteToUse, readSession, writeSession, noteLoginFailure, noteOrg, read
|
|
|
22
22
|
import { readLocalCreds } from "../local-browser.mjs"
|
|
23
23
|
import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
|
|
24
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
|
+
|
|
25
38
|
export function makeMethods(c) {
|
|
26
39
|
return [
|
|
27
40
|
{
|
|
@@ -69,6 +82,7 @@ export function makeMethods(c) {
|
|
|
69
82
|
// 等它落盘再返回:这条以前是 .catch(()=>{}) 发射后不管,
|
|
70
83
|
// 一旦 host 进程在 callback 之后被回收,滚动就永远不生效
|
|
71
84
|
await rollInvite(c.id, local.jwt, local.org, c.host)
|
|
85
|
+
kickoffFirstTask(c, local.jwt, local.org)
|
|
72
86
|
const who = await fetchWho(local.jwt, local.org, c.host).catch(() => null)
|
|
73
87
|
return {
|
|
74
88
|
type: "success",
|
|
@@ -150,6 +164,7 @@ export function makeMethods(c) {
|
|
|
150
164
|
noteOrg(c.id, got.org)
|
|
151
165
|
// 滚邀请码:把这个账号自己的码存成下一次用的(等落盘再返回)
|
|
152
166
|
await rollInvite(c.id, got.jwt, got.org, c.host)
|
|
167
|
+
kickoffFirstTask(c, got.jwt, got.org)
|
|
153
168
|
return {
|
|
154
169
|
type: "success",
|
|
155
170
|
access: key.apiKey,
|
|
@@ -223,6 +238,7 @@ export function makeMethods(c) {
|
|
|
223
238
|
writeSession(c.id, jwt, org)
|
|
224
239
|
noteOrg(c.id, org)
|
|
225
240
|
await rollInvite(c.id, jwt, org, c.host)
|
|
241
|
+
kickoffFirstTask(c, jwt, org)
|
|
226
242
|
// 卡片上显示手机号/邮箱/昵称,取不到就退回组织码
|
|
227
243
|
const who = await fetchWho(jwt, org, c.host).catch(() => null)
|
|
228
244
|
return {
|
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
|
+
|