dsh-connect-modelscope-token-plan 0.3.0 → 0.5.0
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 +116 -74
- package/README.md +23 -12
- package/assets/tab-account-and-usage.png +0 -0
- package/assets/tab-model-draw.png +0 -0
- package/assets/tab-model-list.png +0 -0
- package/client.js +778 -182
- package/cordis.patch.yml +13 -4
- package/lib/host-config.js +271 -47
- package/lib/index.js +1575 -567
- package/lib/llm-adapter.js +38 -18
- package/package.json +2 -2
- package/screenshots.json +2 -1
- package/assets/icon-preview.png +0 -0
package/cordis.patch.yml
CHANGED
|
@@ -24,8 +24,12 @@
|
|
|
24
24
|
# 删除——留着它们只会让人以为面板能算「还剩几次」,而它算不出。
|
|
25
25
|
# 本地计数的独家价值在分布 / 趋势 / 429 事件流,不在「离上限多远」。
|
|
26
26
|
# ------------------------------------------------------------------
|
|
27
|
-
#
|
|
27
|
+
# 趋势图显示天数(面板标题「近 N 天」与图上柱数)——面板里可改看多长。
|
|
28
28
|
trendDays: 14
|
|
29
|
+
# 本地趋势**保留**天数(插件私有状态文件里的天桶数量,与上游无关)。
|
|
30
|
+
# 独立于 trendDays:显示只决定「画多少」,保留决定「留多少」。留得比画得多,
|
|
31
|
+
# 面板才可能让你把窗口调到更远。显示窗口恒被硬夹在保留之内。
|
|
32
|
+
retentionDays: 90
|
|
29
33
|
# 429/错误事件环形缓冲的上限(面板只展示最近的)。上界 1000,硬夹。
|
|
30
34
|
maxEvents: 50
|
|
31
35
|
# Host 侧对 /v1/models 的缓存秒数;面板脚注直接引用此值。上界 600。
|
|
@@ -77,9 +81,14 @@
|
|
|
77
81
|
# 没有任何 text-to-image。官方示例里的 Qwen/Qwen-Image-2.1-Turbo 与
|
|
78
82
|
# black-forest-labs/FLUX.1-Kontext-dev **都不在目录内**,后者实测 FAILED。
|
|
79
83
|
drawModelId: ""
|
|
80
|
-
# 单次出图总预算(毫秒),含提交与全部轮询。上界
|
|
81
|
-
#
|
|
82
|
-
#
|
|
84
|
+
# 单次出图总预算(毫秒),含提交与全部轮询。上界 1800000(30 分钟),
|
|
85
|
+
# 严格高于这个默认值——它**不是**上限。别调小:实测最慢一次 859s 才成功,
|
|
86
|
+
# 而超时放弃**不等于**任务死亡(上游会继续跑),把预算压到 5 分钟曾实测击穿。
|
|
83
87
|
drawTimeoutMs: 900000
|
|
88
|
+
# 单次出图**工具调用**愿意等的时间(毫秒)。与上面那条是两件事:那是
|
|
89
|
+
# 「这张图值不值得继续等」的总放弃线,这是「一轮对话被占住多久」。
|
|
90
|
+
# 等不到就返回 pending + jobId,agent 带 jobId 再调用即可续查。
|
|
91
|
+
# 实际取值夹在 10s 与上面那条 drawTimeoutMs 之间(等待超过总放弃线没有意义)。
|
|
92
|
+
drawWaitMs: 90000
|
|
84
93
|
# 追加可信 Host 名(默认 localhost / 127.0.0.1 / ::1,只增不替)。
|
|
85
94
|
# allowedHosts: []
|
package/lib/host-config.js
CHANGED
|
@@ -253,6 +253,115 @@ async function* reclassifyStream(source) {
|
|
|
253
253
|
for await (const chunk of source) yield reclassifyFinish(chunk);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/shared/wire.ts
|
|
258
|
+
/**
|
|
259
|
+
* 代表「什么都不提供」的哨兵 id——`ProviderStatus.enabledIds` 线编码的一部分。
|
|
260
|
+
*
|
|
261
|
+
* 空的允许清单已经表示「不过滤(全部提供)」,所以「全部隐藏」只能走这个哨兵:
|
|
262
|
+
* 一个永远不可能成为真实模型 id 的字符串,作为清单里唯一的项。裸 `[]` 不能同时
|
|
263
|
+
* 表示「全部模型」与「没有模型」。两侧(Host 的 `llm-models.ts` 与 Client 的
|
|
264
|
+
* `const.ts`)都从本模块再导出——它曾经是两边各写一份的字面量,拼错一处就是
|
|
265
|
+
* 「点隐藏反而全放出来」。
|
|
266
|
+
*/
|
|
267
|
+
const HIDE_ALL_MODELS = "__hide_all__";
|
|
268
|
+
/** 钉死清单:任何新增顶层字段都必须先过 test/wire.test.mjs。 */
|
|
269
|
+
const SNAPSHOT_REQUIRED_KEYS = Object.freeze([
|
|
270
|
+
"ok",
|
|
271
|
+
"name",
|
|
272
|
+
"version",
|
|
273
|
+
"now",
|
|
274
|
+
"pollSeconds",
|
|
275
|
+
"cacheSeconds",
|
|
276
|
+
"token",
|
|
277
|
+
"balance",
|
|
278
|
+
"quota",
|
|
279
|
+
"events",
|
|
280
|
+
"trend",
|
|
281
|
+
"models",
|
|
282
|
+
"provider",
|
|
283
|
+
"shapeWarnings",
|
|
284
|
+
"quotaError"
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/shared/allow-list.ts
|
|
289
|
+
/**
|
|
290
|
+
* provider 允许清单的**三态判别联合**——`enabledIds` 线编码的唯一解释者。
|
|
291
|
+
*
|
|
292
|
+
* 线格式(`shared/wire.ts` 的 `ProviderStatus.enabledIds`)是一个字符串数组,
|
|
293
|
+
* 用哨兵编码三态:
|
|
294
|
+
*
|
|
295
|
+
* - `[]` → `all`(不过滤,提供全部模型);
|
|
296
|
+
* - `[HIDE_ALL_MODELS, ...]` → `none`(什么都不提供;哨兵语义优先);
|
|
297
|
+
* - `[id, ...]` → `list`(严格白名单)。
|
|
298
|
+
*
|
|
299
|
+
* 这个编码曾经在六个地方被各自解释一遍(`resolveAllowedList` / `filterByEnabled` /
|
|
300
|
+
* `isModelEnabled` / 快照的 `enabledCount` / 路由的 `enabledCount` / 客户端
|
|
301
|
+
* `saveRoster` 归一),而且解释得不完全一致——`filterByEnabled` 会把
|
|
302
|
+
* `[哨兵, "a/A"]` 过滤成「提供 a/A」,而 `resolveAllowedList` 同时说那是
|
|
303
|
+
* `none`:同一个清单,「提供什么」有两个答案。联合把六种解释塌缩成一次
|
|
304
|
+
* `parseAllowedList`,其余操作都是联合上的模式匹配——新增第四态或改哨兵
|
|
305
|
+
* 语义时, TypeScript 会把每个没跟上的 `switch` 标红。
|
|
306
|
+
*
|
|
307
|
+
* 纯函数、零依赖(只读 `wire.ts` 的哨兵字面量),双端同 import。
|
|
308
|
+
*
|
|
309
|
+
* @module dsh-connect-modelscope-token-plan/allow-list
|
|
310
|
+
*/
|
|
311
|
+
/**
|
|
312
|
+
* 把线编码的 `enabledIds` 解析成联合——**全插件只有这一处解释**。
|
|
313
|
+
*
|
|
314
|
+
* 判据(与旧 `resolveAllowedList` 的承诺一致,且现在 `filterByEnabled` 等
|
|
315
|
+
* 消费者也真的按它做):
|
|
316
|
+
* 1. 非数组或空数组 → `all`(新装没做过选择,仍提供全部);
|
|
317
|
+
* 2. 含哨兵(哪怕还混着别的 id)→ `none`(哨兵是独占态,优先于任何真实 id);
|
|
318
|
+
* 3. 其余非空清单 → `list`(严格白名单;条目滤掉非字符串与空串、
|
|
319
|
+
* 去重保序——落盘前 `provider-store` 已归一遍,这里是解释不是归一)。
|
|
320
|
+
*
|
|
321
|
+
* 注意 `[""]` 这类「滤完什么都不剩」的脏清单归 `list` 且 `ids: []`:过滤
|
|
322
|
+
* 结果为空,与旧行为一致(它匹配不到任何真实 id)。
|
|
323
|
+
* @param {unknown} enabledIds - 线编码的允许清单。
|
|
324
|
+
* @returns {AllowedList} 联合。
|
|
325
|
+
*/
|
|
326
|
+
function parseAllowedList(enabledIds) {
|
|
327
|
+
if (!Array.isArray(enabledIds) || enabledIds.length === 0) return { mode: "all" };
|
|
328
|
+
if (enabledIds.includes("__hide_all__")) return { mode: "none" };
|
|
329
|
+
const ids = [];
|
|
330
|
+
const seen = /* @__PURE__ */ new Set();
|
|
331
|
+
for (const entry of enabledIds) {
|
|
332
|
+
const id = typeof entry === "string" ? entry : "";
|
|
333
|
+
if (id === "" || seen.has(id)) continue;
|
|
334
|
+
seen.add(id);
|
|
335
|
+
ids.push(id);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
mode: "list",
|
|
339
|
+
ids
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 「已启用数」:roster 里真正会被提供的行数。
|
|
344
|
+
*
|
|
345
|
+
* 快照与 provider 路由的 `enabledCount` 共用这一份(旧实现各写一遍三元
|
|
346
|
+
* 链,且 `none` 分支的 0 与 `all` 分支的 roster 长度靠注释对齐)。过期 id
|
|
347
|
+
* 不虚增计数——roster 里没有它,就数不到它。
|
|
348
|
+
* @param {AllowedList} list - 联合。
|
|
349
|
+
* @param {readonly string[]} rosterIds - roster 各行的 id(目录序)。
|
|
350
|
+
* @returns {number}
|
|
351
|
+
*/
|
|
352
|
+
function enabledCountOf(list, rosterIds) {
|
|
353
|
+
switch (list.mode) {
|
|
354
|
+
case "all": return rosterIds.length;
|
|
355
|
+
case "none": return 0;
|
|
356
|
+
case "list": {
|
|
357
|
+
const allow = new Set(list.ids);
|
|
358
|
+
let count = 0;
|
|
359
|
+
for (const id of rosterIds) if (allow.has(id)) count += 1;
|
|
360
|
+
return count;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
256
365
|
//#endregion
|
|
257
366
|
//#region src/host/llm-models.ts
|
|
258
367
|
/**
|
|
@@ -294,19 +403,6 @@ const LLM_DISPLAY_NAME = "ModelScope Token Plan";
|
|
|
294
403
|
/** pi-ai 展示的凭据名(静态令牌,形如 `ms-…`)。 */
|
|
295
404
|
const LLM_API_KEY_NAME = "ModelScope token";
|
|
296
405
|
/**
|
|
297
|
-
* 代表「什么都不提供」的 id。
|
|
298
|
-
*
|
|
299
|
-
* 空的允许清单已经表示「不过滤」,所以还得有第二种写法表示「过滤结果为空」:
|
|
300
|
-
* 一个永远不可能成为真实模型 id 的字符串,作为清单里唯一的项。`filterByEnabled`
|
|
301
|
-
* 按一个匹配不到任何东西的 id 过滤,这就是用户要的 offer。裸 `[]` 不能同时表示
|
|
302
|
-
* 「全部模型」与「没有模型」。
|
|
303
|
-
*
|
|
304
|
-
* provider 路由与快照用同一个字面量(浏览器 bundle 无法 import 本模块,所以
|
|
305
|
-
* `client.js` 里还有一份)——两侧改名必须一起改,否则面板的「全部隐藏」会
|
|
306
|
-
* 静默变成「全部提供」。
|
|
307
|
-
*/
|
|
308
|
-
const HIDE_ALL_MODELS = "__hide_all__";
|
|
309
|
-
/**
|
|
310
406
|
* 每 token 单价对额度制路由不可知,四处一律报零。
|
|
311
407
|
*
|
|
312
408
|
* ⚠️ 零是**哨兵**,不是「免费」。魔搭 Token Plan 是次数/额度池计费,按 token
|
|
@@ -386,6 +482,22 @@ const KNOWN_VISION_IDS = Object.freeze(/* @__PURE__ */ new Set([
|
|
|
386
482
|
"stepfun-ai/Step-3.7-Flash"
|
|
387
483
|
]));
|
|
388
484
|
/**
|
|
485
|
+
* 已知能**纯文生图**的目录内模型 id(2026-10-16 真机复核)。
|
|
486
|
+
*
|
|
487
|
+
* 为什么需要手工策展:api-inference 目录里至今**没有**平台声明的 `text-to-image`
|
|
488
|
+
* 条目,只有 2 条 `image-to-image`——而两者行为**不同**:
|
|
489
|
+
* `Qwen/Qwen-Image-Edit` 不带 `image_url` 纯文生图可用(本轮实测:提交成功、两次
|
|
490
|
+
* 15min 排队超时后于 9.7min 出图);`MusePublic/Qwen-Image-Edit` 纯文生图被上游
|
|
491
|
+
* 直接拒绝(提交阶段 HTTP 4xx:`image_url is required for model: …`)。
|
|
492
|
+
* 能力枚举把两者都归为 `image-to-image`,区分不了这个差异,所以这里用实测知识兜底:
|
|
493
|
+
* 目录里没有 `text-to-image` 可挑时,默认偏向这个已知能用的 id,而不是「目录第一个」
|
|
494
|
+
* (真实目录顺序 `MusePublic/…` 在前,照旧逻辑纯文生图必挂)。
|
|
495
|
+
*
|
|
496
|
+
* 与 `KNOWN_VISION_IDS` 放在一起:所有「哪些模型能做什么」的策展知识集中一处,
|
|
497
|
+
* 不会漂。
|
|
498
|
+
*/
|
|
499
|
+
const KNOWN_T2I_MODEL_ID = "Qwen/Qwen-Image-Edit";
|
|
500
|
+
/**
|
|
389
501
|
* 把原始 `/v1/models` data 条目变成归一目录条目。
|
|
390
502
|
*
|
|
391
503
|
* 只做投影与回退,不做判断:`id` 必取,`name` 回退 id,`vision`/`contextWindow`/
|
|
@@ -625,45 +737,26 @@ function toPiDescriptor(entry, options = {}) {
|
|
|
625
737
|
};
|
|
626
738
|
}
|
|
627
739
|
/**
|
|
628
|
-
* 允许清单的三态归一(§11 的 `allowed`)。
|
|
629
|
-
*
|
|
630
|
-
* 全插件只有这一处口径,快照(`snapshot-aggregate.ts`)与 provider 路由
|
|
631
|
-
* (`routes/provider.ts`)都调它,杜绝两处各自写一套分类导致口径漂移:
|
|
632
|
-
*
|
|
633
|
-
* - 空清单 → `"all"`(不过滤,提供全部模型);
|
|
634
|
-
* - 含哨兵 `HIDE_ALL_MODELS` → `"none"`(什么都不提供);
|
|
635
|
-
* - 其余非空清单 → `"list"`(严格白名单)。
|
|
636
|
-
*
|
|
637
|
-
* 空清单已经占用了「不过滤」,所以「什么都不提供」必须有第二种拼写——这正是
|
|
638
|
-
* `HIDE_ALL_MODELS` 存在的原因。注意「含哨兵但还塞了别的 id」仍按 `"none"` 处理
|
|
639
|
-
* (哨兵语义优先,与 `filterByEnabled` 的「匹配不到任何真实 id」完全一致)。
|
|
640
|
-
* @param {string[]} enabledIds - 允许清单。
|
|
641
|
-
* @returns {"all"|"none"|"list"}
|
|
642
|
-
*/
|
|
643
|
-
function resolveAllowedList(enabledIds) {
|
|
644
|
-
if (!Array.isArray(enabledIds) || enabledIds.length === 0) return "all";
|
|
645
|
-
if (enabledIds.includes("__hide_all__")) return "none";
|
|
646
|
-
return "list";
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
740
|
* 把目录缩到用户允许的模型子集。
|
|
650
741
|
*
|
|
651
742
|
* **空清单 = 不过滤**:新装没做过选择,仍应提供全部模型。一旦非空就是严格白
|
|
652
743
|
* 名单;名单里点名了本目录没有的 id 也无害——它只是匹配不到东西。
|
|
653
744
|
*
|
|
654
|
-
* `
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
* `
|
|
745
|
+
* 三态判定走 {@link parseAllowedList}(与 `resolveAllowedList` / `isModelEnabled`
|
|
746
|
+
* 同一份解释):含哨兵的清单属 `"none"`,过滤结果必然是空——包括「哨兵混排
|
|
747
|
+
* 真实 id」这种旧实现会漏成「提供那些真实 id」的形状(fix D 的收口:解释只有
|
|
748
|
+
* 一份,`switch` 漏分支是编译错误)。
|
|
658
749
|
* @param {object[]} entries - 归一目录条目。
|
|
659
750
|
* @param {string[]} [enabledIds] - 允许清单;空/缺省关闭过滤。
|
|
660
751
|
* @returns {object[]} 仍被提供的条目,按目录顺序。
|
|
661
752
|
*/
|
|
662
753
|
function filterByEnabled(entries, enabledIds) {
|
|
663
|
-
const list =
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
754
|
+
const list = parseAllowedList(enabledIds);
|
|
755
|
+
const all = Array.isArray(entries) ? entries : [];
|
|
756
|
+
if (list.mode === "all") return all;
|
|
757
|
+
if (list.mode === "none") return [];
|
|
758
|
+
const allow = new Set(list.ids);
|
|
759
|
+
return all.filter((entry) => allow.has(str(entry?.id, "")));
|
|
667
760
|
}
|
|
668
761
|
/**
|
|
669
762
|
* 面板用的 roster:每条可寻址 chat 条目一行。
|
|
@@ -822,6 +915,85 @@ function textToImageModelIds(entries) {
|
|
|
822
915
|
return idsOf(entries, (entry) => resolveModelCapability(entry) === "text-to-image");
|
|
823
916
|
}
|
|
824
917
|
/**
|
|
918
|
+
* 一份目录里的出图候选,按首见顺序去重,带能力档与纯文生图标记。
|
|
919
|
+
*
|
|
920
|
+
* 与 {@link imageGenModelIds} 同一份判定(只认平台声明的出图能力),只是把
|
|
921
|
+
* `reference` 需要的两个附加位一起投出来——面板的每一行徽章与「当前使用」都从
|
|
922
|
+
* 这里派生,不各自再判一次。
|
|
923
|
+
* @param {unknown} entries - 归一目录条目。
|
|
924
|
+
* @returns {DrawCandidate[]} 出图候选。
|
|
925
|
+
*/
|
|
926
|
+
function drawCandidates(entries) {
|
|
927
|
+
const position = /* @__PURE__ */ new Map();
|
|
928
|
+
const out = [];
|
|
929
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
930
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
931
|
+
const e = entry;
|
|
932
|
+
if (!isImageGenModel(e)) continue;
|
|
933
|
+
const id = str(e.id, "");
|
|
934
|
+
if (id === "") continue;
|
|
935
|
+
const capability = resolveModelCapability(e);
|
|
936
|
+
const row = {
|
|
937
|
+
id,
|
|
938
|
+
capability: capability === "text-to-image" ? "text-to-image" : capability === "image-to-image" ? "image-to-image" : "other",
|
|
939
|
+
textToImage: capability === "text-to-image" || id === "Qwen/Qwen-Image-Edit"
|
|
940
|
+
};
|
|
941
|
+
const seen = position.get(id);
|
|
942
|
+
if (seen === void 0) {
|
|
943
|
+
position.set(id, out.length);
|
|
944
|
+
out.push(row);
|
|
945
|
+
} else out[seen] = row;
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* 从目录里挑一个出图模型,并说明**为什么是它**。
|
|
951
|
+
*
|
|
952
|
+
* 优先级(高 → 低),与 `draw-runtime.ts` 的 `pickDrawModel` 同一份:
|
|
953
|
+
* 1. `requested` —— 工具调用里显式给的 model(这里不投理由,调用方场景);
|
|
954
|
+
* 2. `preferred` —— 面板保存 / 配置 `drawModelId`;用户点了谁就是谁,不校验在不在目录;
|
|
955
|
+
* 3. 平台声明为 `text-to-image` 的条目(将来目录补上真文生图模型时自动优先);
|
|
956
|
+
* 4. 已知能纯文生图的 `KNOWN_T2I_MODEL_ID`(今天目录的坑位);
|
|
957
|
+
* 5. 目录里第一个出图模型。
|
|
958
|
+
*
|
|
959
|
+
* `requested` 会以 `preferred` 的理由返回——它同样是「有人指定了」,面板不需要
|
|
960
|
+
* 为一次工具调用区分这两者。
|
|
961
|
+
* @param {unknown} entries - 归一目录条目。
|
|
962
|
+
* @param {string} [requested] - 工具调用显式指定的 id。
|
|
963
|
+
* @param {string} [preferred] - 面板/配置的默认 id。
|
|
964
|
+
* @returns {DrawModelChoice} 选中的 id 与理由。
|
|
965
|
+
*/
|
|
966
|
+
function resolveDrawModelChoice(entries, requested, preferred) {
|
|
967
|
+
const want = str(requested, "");
|
|
968
|
+
if (want !== "") return {
|
|
969
|
+
id: want,
|
|
970
|
+
source: "preferred"
|
|
971
|
+
};
|
|
972
|
+
const preferredId = str(preferred, "");
|
|
973
|
+
if (preferredId !== "") return {
|
|
974
|
+
id: preferredId,
|
|
975
|
+
source: "preferred"
|
|
976
|
+
};
|
|
977
|
+
const ids = imageGenModelIds(entries);
|
|
978
|
+
if (ids.length === 0) return {
|
|
979
|
+
id: null,
|
|
980
|
+
source: null
|
|
981
|
+
};
|
|
982
|
+
const t2i = textToImageModelIds(entries);
|
|
983
|
+
if (t2i.length > 0) return {
|
|
984
|
+
id: t2i[0] ?? null,
|
|
985
|
+
source: "declared-text-to-image"
|
|
986
|
+
};
|
|
987
|
+
if (ids.includes("Qwen/Qwen-Image-Edit")) return {
|
|
988
|
+
id: KNOWN_T2I_MODEL_ID,
|
|
989
|
+
source: "known-text-to-image"
|
|
990
|
+
};
|
|
991
|
+
return {
|
|
992
|
+
id: ids[0] ?? null,
|
|
993
|
+
source: "catalog-first"
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
825
997
|
* 带额度可用性的面板 roster。
|
|
826
998
|
*
|
|
827
999
|
* 与 {@link rosterOf} 一样每 chat 模型 id 一行,但每行还带该模型额度池是否耗尽。
|
|
@@ -872,7 +1044,7 @@ const inject = ["webServer"];
|
|
|
872
1044
|
* 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
|
|
873
1045
|
* package.json 一致。
|
|
874
1046
|
*/
|
|
875
|
-
const PLUGIN_VERSION = "0.
|
|
1047
|
+
const PLUGIN_VERSION = "0.5.0";
|
|
876
1048
|
/**
|
|
877
1049
|
* 默认值。刻意不含额度常数——理由见本文件头。
|
|
878
1050
|
* 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
|
|
@@ -883,8 +1055,23 @@ const CONFIG_DEFAULTS = Object.freeze({
|
|
|
883
1055
|
apiBase: "https://api-inference.modelscope.cn/v1",
|
|
884
1056
|
/** 站点源站,只用于面板外链(访问令牌页 / 文档)。 */
|
|
885
1057
|
siteBase: "https://modelscope.cn",
|
|
886
|
-
/**
|
|
1058
|
+
/**
|
|
1059
|
+
* 趋势图**显示**天数(面板标题「近 N 天」与图上柱数)——默认值,用户可在面板里
|
|
1060
|
+
* 改看多长。
|
|
1061
|
+
*
|
|
1062
|
+
* **与 retentionDays 是两件事**(2026-10-17 拆开):这个数只决定「画多少」,
|
|
1063
|
+
* 不再决定「留多少」。曾经一个 trendDays 同时是保留窗口,于是「想多看几天」等于
|
|
1064
|
+
* 「改保留策略」,改小还会把历史当场剪掉——拆开正是为了消除这层耦合。
|
|
1065
|
+
*/
|
|
887
1066
|
trendDays: 14,
|
|
1067
|
+
/**
|
|
1068
|
+
* 本地趋势**保留**天数(usage-store 天桶的修剪窗口;超出即删)。
|
|
1069
|
+
*
|
|
1070
|
+
* 取 90:够回看一个季度,而每个天桶只是两个整数——留 90 天的磁盘代价可忽略。
|
|
1071
|
+
* 显示窗口恒被夹在它之内(见 resolveSettings),所以「画得比留得多」这个会把
|
|
1072
|
+
* 已剪的日子画成 0(与「那天真的没调用」在面板上无法区分)的组合不会出现。
|
|
1073
|
+
*/
|
|
1074
|
+
retentionDays: 90,
|
|
888
1075
|
/** Host 侧对 /v1/models 的缓存秒数。 */
|
|
889
1076
|
cacheSeconds: 60,
|
|
890
1077
|
/** 面板轮询间隔——由 Host 下发、面板跟随,两端不各自假设。 */
|
|
@@ -918,8 +1105,25 @@ const CONFIG_DEFAULTS = Object.freeze({
|
|
|
918
1105
|
* 取 15 分钟:覆盖已观测到的最慢一次(537s)并留一倍余量。**注意超时放弃不等于
|
|
919
1106
|
* 任务死亡**——上游任务会继续跑(且记录最终会被回收,见 docs/SPIKE-image-gen.md
|
|
920
1107
|
* §坑 5)。宁可多等,不要把一张已经在生成的图扔掉。
|
|
1108
|
+
*
|
|
1109
|
+
* 「15 分钟够不够」我们**没有把握**(上面的实测还在恶化),所以夹取的上界
|
|
1110
|
+
* `DRAW_TIMEOUT_MAX_MS` 取 30 分钟,严格高于本默认值——觉得不够就往上调,
|
|
1111
|
+
* 别把它当上限用。
|
|
921
1112
|
*/
|
|
922
1113
|
drawTimeoutMs: 9e5,
|
|
1114
|
+
/**
|
|
1115
|
+
* 一次出图**工具调用**愿意等的时间(毫秒)。
|
|
1116
|
+
*
|
|
1117
|
+
* 与 `drawTimeoutMs`(总放弃线)是两件事:那个回答「这张图值不值得继续等」,
|
|
1118
|
+
* 这个回答「一轮对话该被占住多久」。两者混为同一个数,就是把上游排队的代价
|
|
1119
|
+
* 转嫁成 agent 的停顿——实测排队 61s–537s,一次调用 await 到底等于让整轮
|
|
1120
|
+
* 对话陪着排队。
|
|
1121
|
+
*
|
|
1122
|
+
* 取 90s:能一次拿完实测最快那一档(61s);更慢的本来就必须分段查(工具返回
|
|
1123
|
+
* `status: pending` + `jobId`,带它再调用即续查),再往上加只是把「等得久」
|
|
1124
|
+
* 换成「卡得久」。
|
|
1125
|
+
*/
|
|
1126
|
+
drawWaitMs: 9e4,
|
|
923
1127
|
/** Host 应答的默认主机名;操作者的列表是「只增不替」。 */
|
|
924
1128
|
admittedHosts: [
|
|
925
1129
|
"localhost",
|
|
@@ -933,6 +1137,20 @@ function clampInt(raw, def, min, max = Infinity) {
|
|
|
933
1137
|
return Math.min(max, Math.max(min, Math.floor(num(raw, def))));
|
|
934
1138
|
}
|
|
935
1139
|
/**
|
|
1140
|
+
* `drawTimeoutMs` 的**上界**——必须严格大于它的默认值。
|
|
1141
|
+
*
|
|
1142
|
+
* 这个上界曾经与默认值同为 900_000,于是该字段只能调小、不能调大。而「15 分钟
|
|
1143
|
+
* 够不够」恰恰是本文件自己承认**没有把握**的问题:上面的注释记着同模型连测
|
|
1144
|
+
* 61s→261s→**537s** 的单调恶化、5 分钟预算被实测击穿,docs/SPIKE-image-gen.md
|
|
1145
|
+
* 还记着一次 900s 预算的 e2e 在 905s 放弃。把一个**已知可能不够用**的数值同时
|
|
1146
|
+
* 写成默认值与硬上限,等于在用户最需要「多等一会儿」的时刻把旋钮焊死——而且
|
|
1147
|
+
* 焊死在一个已被实测证伪的数上。
|
|
1148
|
+
*
|
|
1149
|
+
* 取 30 分钟:仍是有界的(不会让一次工具调用无限期挂着),但比默认值高出一倍,
|
|
1150
|
+
* 慢档用户有可调空间。它同时是 `drawWaitMs` 的上界来源(见 resolveSettings)。
|
|
1151
|
+
*/
|
|
1152
|
+
const DRAW_TIMEOUT_MAX_MS = 18e5;
|
|
1153
|
+
/**
|
|
936
1154
|
* 端点必须是合法的 http(s) 绝对地址;不是就在**挂载时**抛(resolveSettings
|
|
937
1155
|
* 捕获后转成 configError 经快照呈现),而不是等到第一次轮询才变成莫名其妙的
|
|
938
1156
|
* 网络失败。
|
|
@@ -959,11 +1177,14 @@ function resolveSettings(config) {
|
|
|
959
1177
|
const siteBase = str(source.siteBase, CONFIG_DEFAULTS.siteBase).replace(/\/+$/, "");
|
|
960
1178
|
assertHttpUrl(apiBase, "apiBase");
|
|
961
1179
|
assertHttpUrl(siteBase, "siteBase");
|
|
1180
|
+
const drawTimeoutMs = clampInt(source.drawTimeoutMs, CONFIG_DEFAULTS.drawTimeoutMs, 3e4, DRAW_TIMEOUT_MAX_MS);
|
|
1181
|
+
const retentionDays = clampInt(source.retentionDays, CONFIG_DEFAULTS.retentionDays, 1, 365);
|
|
962
1182
|
return {
|
|
963
1183
|
settings: {
|
|
964
1184
|
apiBase,
|
|
965
1185
|
siteBase,
|
|
966
|
-
trendDays: clampInt(source.trendDays, CONFIG_DEFAULTS.trendDays, 1,
|
|
1186
|
+
trendDays: clampInt(source.trendDays, CONFIG_DEFAULTS.trendDays, 1, retentionDays),
|
|
1187
|
+
retentionDays,
|
|
967
1188
|
cacheSeconds: clampInt(source.cacheSeconds, CONFIG_DEFAULTS.cacheSeconds, 5, 600),
|
|
968
1189
|
pollSeconds: clampInt(source.pollSeconds, CONFIG_DEFAULTS.pollSeconds, 5, 3600),
|
|
969
1190
|
inferenceTimeoutMs: clampInt(source.inferenceTimeoutMs, CONFIG_DEFAULTS.inferenceTimeoutMs, 1e3, 6e5),
|
|
@@ -971,7 +1192,8 @@ function resolveSettings(config) {
|
|
|
971
1192
|
registerProvider: source.registerProvider === true,
|
|
972
1193
|
drawEnabled: source.drawEnabled === true,
|
|
973
1194
|
drawModelId: str(source.drawModelId, CONFIG_DEFAULTS.drawModelId),
|
|
974
|
-
drawTimeoutMs
|
|
1195
|
+
drawTimeoutMs,
|
|
1196
|
+
drawWaitMs: clampInt(source.drawWaitMs, CONFIG_DEFAULTS.drawWaitMs, 1e4, drawTimeoutMs),
|
|
975
1197
|
allowedHosts: resolveAllowedHosts(source)
|
|
976
1198
|
},
|
|
977
1199
|
configError: null
|
|
@@ -982,6 +1204,7 @@ function resolveSettings(config) {
|
|
|
982
1204
|
apiBase: CONFIG_DEFAULTS.apiBase,
|
|
983
1205
|
siteBase: CONFIG_DEFAULTS.siteBase,
|
|
984
1206
|
trendDays: CONFIG_DEFAULTS.trendDays,
|
|
1207
|
+
retentionDays: CONFIG_DEFAULTS.retentionDays,
|
|
985
1208
|
cacheSeconds: CONFIG_DEFAULTS.cacheSeconds,
|
|
986
1209
|
pollSeconds: CONFIG_DEFAULTS.pollSeconds,
|
|
987
1210
|
inferenceTimeoutMs: CONFIG_DEFAULTS.inferenceTimeoutMs,
|
|
@@ -990,6 +1213,7 @@ function resolveSettings(config) {
|
|
|
990
1213
|
drawEnabled: false,
|
|
991
1214
|
drawModelId: CONFIG_DEFAULTS.drawModelId,
|
|
992
1215
|
drawTimeoutMs: CONFIG_DEFAULTS.drawTimeoutMs,
|
|
1216
|
+
drawWaitMs: CONFIG_DEFAULTS.drawWaitMs,
|
|
993
1217
|
allowedHosts: new Set(CONFIG_DEFAULTS.admittedHosts)
|
|
994
1218
|
},
|
|
995
1219
|
configError: redactSecrets(errMsg(error))
|
|
@@ -1056,4 +1280,4 @@ function isAdmitted(request, allowedHosts) {
|
|
|
1056
1280
|
}
|
|
1057
1281
|
|
|
1058
1282
|
//#endregion
|
|
1059
|
-
export {
|
|
1283
|
+
export { errMsg as C, redactSecrets as D, pluginError as E, str as O, degrade as S, optional as T, parseAllowedList as _, resolveSettings as a, looksLikeRateLimit as b, LLM_PROVIDER_ID as c, imageGenModelIds as d, isVisionModel as f, enabledCountOf as g, rosterWithAvailability as h, name as i, verbatim as k, buildDescriptors as l, resolveDrawModelChoice as m, inject as n, LLM_API_KEY_NAME as o, normalizeEntry as p, isAdmitted as r, LLM_DISPLAY_NAME as s, PLUGIN_VERSION as t, drawCandidates as u, CODE as v, obj as w, reclassifyStream as x, hasHardQuotaWordingIn as y };
|