@fish-under-sea/opencode-meoo-auth 1.0.2 → 1.1.1
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/README.md +30 -3
- package/index.mjs +136 -32
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,12 +18,39 @@ OpenCode & Magpie provider plugin for **Meoo 秒悟**(`meoo.com`)。npm 包
|
|
|
18
18
|
|
|
19
19
|
作者:[@Fish-under-sea](https://github.com/Fish-under-sea)、[Chensy(elysia395)](https://github.com/elysia395)。本包由 [FISH-DOCK-Plugins PR #35](https://github.com/Fish-under-sea/FISH-DOCK-Plugins/pull/35) 的 Magpie 供应商插件落成独立 npm 包;代码源头是 [dsh-meoo-connect](https://github.com/2239058652/dsh-meoo-connect)(DeepSeek Harness 的 dsh 插件):同一套 Meoo 官方「AI 服务」链路(OAuth 登录 → 项目服务 AK → OpenAI 兼容网关),换到 Magpie / OpenCode 的 provider 插件平面上。装好并登录后,Meoo 的模型会出现在 Magpie 的模型选择器里,也作为 OpenCode 的一个 provider。
|
|
20
20
|
|
|
21
|
+
## 模型与上下文
|
|
22
|
+
|
|
23
|
+
网关 `/models` 只回裸 id,不带上下文与输出上限,所以规格由包里那张表给出(数字取自各家官方文档,2026-11 核对):
|
|
24
|
+
|
|
25
|
+
| 模型 | 传参值 | 上下文 | 最大输出 | 图片 |
|
|
26
|
+
| --- | --- | --- | --- | --- |
|
|
27
|
+
| Qwen3.8-Flash | `qwen3.8-flash` | 1M | 131072 | ✓ |
|
|
28
|
+
| Qwen3.8-Max | `qwen3.8-max` | 1M | 131072 | ✓ |
|
|
29
|
+
| Qwen3.7-Max | `qwen3.7-max` | 1M | 131072 | |
|
|
30
|
+
| Qwen3.7-Plus | `qwen3.7-plus` | 1M | 131072 | ✓ |
|
|
31
|
+
| DeepSeek V4.1 Flash | `deepseek-v4.1-flash-1017` | 1M | 384K | ✓ |
|
|
32
|
+
| DeepSeek V4 Pro | `deepseek-v4-pro-0813` | 1M | 384K | |
|
|
33
|
+
| DeepSeek V4 Flash | `deepseek-v4-flash-0731` | 1M | 384K | |
|
|
34
|
+
| GLM-5.2-Fast | `glm-5.2-fast-preview` | 1M | 128K | |
|
|
35
|
+
| GLM-5.2 | `glm-5.2` | 1M | 128K | |
|
|
36
|
+
| Qwen VL(视觉理解) | `qwen3-vl-plus` | 262144 | 32768 | ✓ |
|
|
37
|
+
|
|
38
|
+
两点和别家插件不一样,值得知道:
|
|
39
|
+
|
|
40
|
+
- **`qwen3-vl-plus` 不在网关 `/models` 里**,但 Meoo 文档给了、网关也接(实测可调)。插件拉目录时会用一次 1-token 的探针试它,接了就补进目录,否则不提——不写死一个调不通的模型。
|
|
41
|
+
- **目录里出现表外的新模型**按保守值给(200K 上下文 / 64K 输出、纯文本),不会把 1M 随手许出去;要正式支持就把它加进 `MODEL_SPECS`。
|
|
42
|
+
|
|
43
|
+
## 余额
|
|
44
|
+
|
|
45
|
+
浏览器授权登录后,供应商卡片会带一张积分卡(控制面 `/open/v1/cli-compat/account/points`):
|
|
46
|
+
|
|
21
47
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
GLM-5.2 · GLM-5.2-Fast
|
|
48
|
+
Fishundersea · 58,847 积分 · 2026-11-10 到期
|
|
49
|
+
已用 2,153 积分
|
|
25
50
|
```
|
|
26
51
|
|
|
52
|
+
按 magpie 的**预付费钱包**窗口给出:窗口只有 `balance` + `unit`、不写 `used`/`limit`,所以卡片显示数字而不是百分比条;`runsOutAt` 是最近一批还没用完的积分的到期日。手填项目服务 AK 的登录读不到积分账户,卡片会说明要用浏览器授权。
|
|
53
|
+
|
|
27
54
|
## 思考强度
|
|
28
55
|
|
|
29
56
|
模型默认带三档思考强度,对应会话 UI 里的 **快速 / 适中 / 极致**(`reasoning_effort: low / medium / high`)。网关实测该参数真实生效(low 档思考 tokens 约为 high 档的 60%~70%,`enable_thinking:false` 可完全关闭,但为避免不可控行为未暴露「关闭」档)。不选档位时走上游默认思考。
|
package/index.mjs
CHANGED
|
@@ -44,22 +44,47 @@ const EARLY_MS = 60 * 1000
|
|
|
44
44
|
const LEAD_MS = 5 * 60 * 1000
|
|
45
45
|
const KEEP_RENEWED_MS = 60 * 1000
|
|
46
46
|
const CATALOG_TTL_MS = 10 * 60 * 1000
|
|
47
|
+
// 目录里认不出的新模型先按保守值给,等它也进规格表。
|
|
47
48
|
const CONTEXT_WINDOW = 200_000
|
|
48
49
|
const DEFAULT_MAX_TOKENS = 64_000
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{ id: "
|
|
51
|
+
/**
|
|
52
|
+
* 模型规格表。网关 /models 只回裸 id,不带上下文/输出上限,所以数字得自己带。
|
|
53
|
+
* 取值来自各家官方文档(2026-11 核对):
|
|
54
|
+
* Qwen 3.8/3.7 全家 1M 上下文 / 131072 输出;
|
|
55
|
+
* DeepSeek V4 全家 1M / 384K;
|
|
56
|
+
* GLM-5.2 与 GLM-5.2-Fast 1M / 128K;
|
|
57
|
+
* Qwen VL 262144 / 32768。
|
|
58
|
+
* image 只给官方写明原生多模态的那几个,其余按纯文本,免得让 UI 递图片。
|
|
59
|
+
*/
|
|
60
|
+
const MODEL_SPECS = [
|
|
61
|
+
{ id: "qwen3.8-flash", name: "Qwen3.8-Flash", context: 1_000_000, output: 131_072, image: true },
|
|
62
|
+
{ id: "qwen3.8-max", name: "Qwen3.8-Max", context: 1_000_000, output: 131_072, image: true },
|
|
63
|
+
{ id: "qwen3.7-max", name: "Qwen3.7-Max", context: 1_000_000, output: 131_072 },
|
|
64
|
+
{ id: "qwen3.7-plus", name: "Qwen3.7-Plus", context: 1_000_000, output: 131_072, image: true },
|
|
65
|
+
{ id: "deepseek-v4.1-flash-1017", name: "DeepSeek V4.1 Flash", context: 1_000_000, output: 384_000, image: true },
|
|
66
|
+
{ id: "deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", context: 1_000_000, output: 384_000 },
|
|
67
|
+
{ id: "deepseek-v4-flash-0731", name: "DeepSeek V4 Flash", context: 1_000_000, output: 384_000 },
|
|
68
|
+
{ id: "glm-5.2-fast-preview", name: "GLM-5.2-Fast", context: 1_000_000, output: 128_000 },
|
|
69
|
+
{ id: "glm-5.2", name: "GLM-5.2", context: 1_000_000, output: 128_000 },
|
|
70
|
+
{ id: "qwen3-vl-plus", name: "Qwen VL", context: 262_144, output: 32_768, image: true },
|
|
61
71
|
]
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 文档里写了、网关 /models 却不一定列出来的模型:拉目录时用一次 1-token 的
|
|
75
|
+
* 请求探一下,网关接了才放进目录。
|
|
76
|
+
* qwen3-vl-plus 就是这样:Meoo 文档「视觉理解」一节里的唯一模型,2026-11 实测
|
|
77
|
+
* 网关可调,但 /models 只回 9 个文本 id,不探就永远看不见。
|
|
78
|
+
*/
|
|
79
|
+
const SUPPLEMENT_MODELS = ["qwen3-vl-plus"]
|
|
80
|
+
|
|
81
|
+
const MODEL_BY_ID = new Map(MODEL_SPECS.map((m) => [m.id, m]))
|
|
82
|
+
|
|
83
|
+
/** 目录里出现的未知 id 当纯文本、给保守上限,不把 1M 乱许出去。 */
|
|
84
|
+
function specOf(id) {
|
|
85
|
+
return MODEL_BY_ID.get(id) ?? { id, name: id, context: CONTEXT_WINDOW, output: DEFAULT_MAX_TOKENS }
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
const mainOrigin = () => String(process.env.MEOO_MAIN_SITE_URL || DEFAULT_API_BASE).replace(/\/+$/, "")
|
|
64
89
|
const gatewayBase = () => String(process.env.MEOO_GATEWAY_BASE_URL || DEFAULT_GATEWAY).replace(/\/+$/, "")
|
|
65
90
|
// apiBase 跟着账号记录走(登录用的哪个站点,续期/控制面就打哪个),环境变量只是兜底。
|
|
@@ -73,13 +98,27 @@ function clientIdFor(apiBaseUrl) {
|
|
|
73
98
|
}
|
|
74
99
|
}
|
|
75
100
|
|
|
76
|
-
const labelOf = (id) =>
|
|
101
|
+
const labelOf = (id) => specOf(id).name
|
|
77
102
|
const int = (n, fallback = 0) => {
|
|
78
103
|
const v = Math.trunc(Number(n))
|
|
79
104
|
return Number.isFinite(v) && v >= 0 ? v : fallback
|
|
80
105
|
}
|
|
81
106
|
const vendorError = (json, text) => json?.detail || json?.message || String(text ?? "").slice(0, 160)
|
|
82
107
|
|
|
108
|
+
/** 最近一批还没用完的积分的到期日(YYYY-MM-DD),给余额卡当提示。 */
|
|
109
|
+
function soonestLapse(grants) {
|
|
110
|
+
let best = ""
|
|
111
|
+
for (const g of Array.isArray(grants) ? grants : []) {
|
|
112
|
+
for (const d of g?.grantDetails || []) {
|
|
113
|
+
if (int(d?.remainingAmount) <= 0) continue
|
|
114
|
+
const day = String(d?.expiresDate || "").slice(0, 10)
|
|
115
|
+
if (!day) continue
|
|
116
|
+
if (!best || day < best) best = day
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return best
|
|
120
|
+
}
|
|
121
|
+
|
|
83
122
|
// ---- OAuth(Authorization Code + PKCE,public client)----------------------
|
|
84
123
|
|
|
85
124
|
async function tokenRequest(apiBaseUrl, params) {
|
|
@@ -342,12 +381,36 @@ function parseGatewayModels(payload) {
|
|
|
342
381
|
const id = typeof entry === "string" ? entry.trim() : String(entry?.id ?? "").trim()
|
|
343
382
|
if (!id || seen.has(id)) continue
|
|
344
383
|
seen.add(id)
|
|
345
|
-
const
|
|
346
|
-
out.push({
|
|
384
|
+
const spec = specOf(id)
|
|
385
|
+
out.push({
|
|
386
|
+
id,
|
|
387
|
+
name: (typeof entry === "object" && entry?.name) || spec.name,
|
|
388
|
+
context: int(entry?.context) || spec.context,
|
|
389
|
+
output: int(entry?.max_output || entry?.max_output_tokens || entry?.max_tokens) || spec.output,
|
|
390
|
+
image: !!spec.image,
|
|
391
|
+
})
|
|
347
392
|
}
|
|
348
393
|
return out
|
|
349
394
|
}
|
|
350
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 探一个目录外的模型:1 token 的对话请求,网关接了才算可用。
|
|
398
|
+
* 只用来补 SUPPLEMENT_MODELS,结果随目录一起缓存,10 分钟最多探一次。
|
|
399
|
+
*/
|
|
400
|
+
async function acceptsModel(ak, id) {
|
|
401
|
+
try {
|
|
402
|
+
const res = await fetch(`${gatewayBase()}/chat/completions`, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { authorization: `Bearer ${ak}`, "content-type": "application/json" },
|
|
405
|
+
body: JSON.stringify({ model: id, messages: [{ role: "user", content: "hi" }], max_tokens: 1, stream: false }),
|
|
406
|
+
signal: AbortSignal.timeout(20_000),
|
|
407
|
+
})
|
|
408
|
+
return res.ok
|
|
409
|
+
} catch {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
351
414
|
async function fetchGatewayModels(ak) {
|
|
352
415
|
const base = gatewayBase()
|
|
353
416
|
if (catalogCache.models && catalogCache.origin === base && Date.now() - catalogCache.at < CATALOG_TTL_MS) {
|
|
@@ -360,6 +423,13 @@ async function fetchGatewayModels(ak) {
|
|
|
360
423
|
if (!res.ok) throw new Error(`模型目录 HTTP ${res.status}`)
|
|
361
424
|
const models = parseGatewayModels(await res.json())
|
|
362
425
|
if (!models.length) throw new Error("empty catalog")
|
|
426
|
+
const seen = new Set(models.map((m) => m.id))
|
|
427
|
+
for (const id of SUPPLEMENT_MODELS) {
|
|
428
|
+
if (seen.has(id)) continue
|
|
429
|
+
if (!(await acceptsModel(ak, id))) continue
|
|
430
|
+
const spec = specOf(id)
|
|
431
|
+
models.push({ id, name: spec.name, context: spec.context, output: spec.output, image: !!spec.image })
|
|
432
|
+
}
|
|
363
433
|
catalogCache = { origin: base, models, at: Date.now() }
|
|
364
434
|
return models
|
|
365
435
|
}
|
|
@@ -374,40 +444,47 @@ const REASONING_VARIANTS = {
|
|
|
374
444
|
high: { reasoningEffort: "high" },
|
|
375
445
|
}
|
|
376
446
|
|
|
447
|
+
function configModel(m) {
|
|
448
|
+
const spec = specOf(m.id)
|
|
449
|
+
const context = m.context ?? spec.context
|
|
450
|
+
const output = m.output ?? spec.output
|
|
451
|
+
const image = m.image ?? spec.image ?? false
|
|
452
|
+
return {
|
|
453
|
+
name: m.name ?? spec.name,
|
|
454
|
+
limit: { context, output },
|
|
455
|
+
tool_call: true,
|
|
456
|
+
reasoning: true,
|
|
457
|
+
attachment: image,
|
|
458
|
+
modalities: { input: image ? ["text", "image"] : ["text"], output: ["text"] },
|
|
459
|
+
variants: REASONING_VARIANTS,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
377
463
|
function configModels() {
|
|
378
|
-
return Object.fromEntries(
|
|
379
|
-
FALLBACK_MODELS.map((m) => [
|
|
380
|
-
m.id,
|
|
381
|
-
{
|
|
382
|
-
name: m.name,
|
|
383
|
-
limit: { context: CONTEXT_WINDOW, output: DEFAULT_MAX_TOKENS },
|
|
384
|
-
tool_call: true,
|
|
385
|
-
reasoning: true,
|
|
386
|
-
attachment: false,
|
|
387
|
-
modalities: { input: ["text"], output: ["text"] },
|
|
388
|
-
variants: REASONING_VARIANTS,
|
|
389
|
-
},
|
|
390
|
-
]),
|
|
391
|
-
)
|
|
464
|
+
return Object.fromEntries(MODEL_SPECS.map((m) => [m.id, configModel(m)]))
|
|
392
465
|
}
|
|
393
466
|
|
|
394
467
|
function runtimeModel(provider, m) {
|
|
468
|
+
const spec = specOf(m.id)
|
|
469
|
+
const context = m.context ?? spec.context
|
|
470
|
+
const output = m.output ?? spec.output
|
|
471
|
+
const image = m.image ?? spec.image ?? false
|
|
395
472
|
return {
|
|
396
473
|
id: m.id,
|
|
397
474
|
providerID: provider.id,
|
|
398
|
-
name: m.name,
|
|
475
|
+
name: m.name ?? spec.name,
|
|
399
476
|
api: { id: m.id, url: gatewayBase(), npm: NPM },
|
|
400
477
|
status: "active",
|
|
401
478
|
headers: {},
|
|
402
479
|
options: {},
|
|
403
480
|
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
404
|
-
limit: { context
|
|
481
|
+
limit: { context, output },
|
|
405
482
|
capabilities: {
|
|
406
483
|
temperature: true,
|
|
407
484
|
reasoning: true,
|
|
408
|
-
attachment:
|
|
485
|
+
attachment: image,
|
|
409
486
|
toolcall: true,
|
|
410
|
-
input: { text: true, image
|
|
487
|
+
input: { text: true, image, audio: false, video: false, pdf: false },
|
|
411
488
|
output: { text: true, image: false, audio: false, video: false, pdf: false },
|
|
412
489
|
interleaved: false,
|
|
413
490
|
},
|
|
@@ -740,6 +817,33 @@ export const MeooAuthPlugin = async ({ client }) => {
|
|
|
740
817
|
...(a.serviceAk ? { serviceAk: a.serviceAk, serviceAkProjectId: a.serviceAkProjectId, projectUrlId: a.projectUrlId, apiBase: a.apiBase } : {}),
|
|
741
818
|
}
|
|
742
819
|
},
|
|
820
|
+
// 余额:Meoo 的积分账户(控制面 /account/points)。预付费钱包按 magpie 的
|
|
821
|
+
// balance 窗口显示——这种窗口不写 used/limit,卡片显示数字而不是百分比条;
|
|
822
|
+
// 再补一条「已用」和最近一批到期日,免得只看得到余额看不到花在哪。
|
|
823
|
+
async usage(getAuth) {
|
|
824
|
+
const auth = await getAuth()
|
|
825
|
+
if (!auth) throw new Error("还没有连接 Meoo")
|
|
826
|
+
if (auth.type === "api") {
|
|
827
|
+
throw new Error("手填的项目服务 AK 读不到积分账户;要用余额卡,请用浏览器授权登录一次")
|
|
828
|
+
}
|
|
829
|
+
if (auth.type !== "oauth") throw new Error("登录态类型未知,无法读取积分")
|
|
830
|
+
const { a } = await currentAuth(ctx, auth)
|
|
831
|
+
const data = await controlRequest(ctx, a, "GET", "/open/v1/cli-compat/account/points")
|
|
832
|
+
const acc = data?.account || {}
|
|
833
|
+
const left = int(acc.availableBalance)
|
|
834
|
+
const used = int(acc.totalConsumed)
|
|
835
|
+
const frozen = int(acc.frozenBalance)
|
|
836
|
+
const fmt = (n) => new Intl.NumberFormat("en-US").format(n)
|
|
837
|
+
// 钱包窗口:新版 magpie 认 window.balance(不画百分比条、只显示数字);
|
|
838
|
+
// 旧版会把它当纯文本窗口,所以同时给 display —— 两种宿主都能看到余额。
|
|
839
|
+
const wallet = { name: "积分", balance: left, unit: "积分", display: `${fmt(left)} 积分` }
|
|
840
|
+
const lapse = soonestLapse(data?.activeGrants)
|
|
841
|
+
if (lapse) wallet.runsOutAt = lapse
|
|
842
|
+
const windows = [wallet]
|
|
843
|
+
if (used > 0) windows.push({ name: "已用", display: `${fmt(used)} 积分`, aside: true })
|
|
844
|
+
if (frozen > 0) windows.push({ name: "冻结", display: `${fmt(frozen)} 积分`, aside: true })
|
|
845
|
+
return { user: a.username || "", balance: `${fmt(left)} 积分`, windows }
|
|
846
|
+
},
|
|
743
847
|
async loader(getAuth) {
|
|
744
848
|
const auth = await getAuth()
|
|
745
849
|
if (auth?.type === "api") return auth.key ? { baseURL: gatewayBase(), apiKey: auth.key } : {}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fish-under-sea/opencode-meoo-auth",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Meoo 秒悟 (meoo.com) in OpenCode and Magpie:
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Meoo 秒悟 (meoo.com) in OpenCode and Magpie: PKCE sign-in, reuse of a local dsh-meoo-connect state or a project service AK, a live catalog with each model's real context window, and the account's points balance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./opencode.mjs",
|
|
7
7
|
"exports": {
|