dsh-connect-modelscope-token-plan 0.3.0 → 0.4.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.
@@ -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
@@ -625,45 +721,26 @@ function toPiDescriptor(entry, options = {}) {
625
721
  };
626
722
  }
627
723
  /**
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
724
  * 把目录缩到用户允许的模型子集。
650
725
  *
651
726
  * **空清单 = 不过滤**:新装没做过选择,仍应提供全部模型。一旦非空就是严格白
652
727
  * 名单;名单里点名了本目录没有的 id 也无害——它只是匹配不到东西。
653
728
  *
654
- * `HIDE_ALL_MODELS` 哨兵靠「匹配不到任何真实 id」表达「什么都不提供」,因为空
655
- * 清单已经被「不过滤」占用了。这里直接走 {@link resolveAllowedList} 的三态:
656
- * 含哨兵的清单属 `"none"`,过滤结果必然是空(哨兵不是真实 id),与
657
- * `isModelEnabled` 的口径一致(fix D)。
729
+ * 三态判定走 {@link parseAllowedList}(与 `resolveAllowedList` / `isModelEnabled`
730
+ * 同一份解释):含哨兵的清单属 `"none"`,过滤结果必然是空——包括「哨兵混排
731
+ * 真实 id」这种旧实现会漏成「提供那些真实 id」的形状(fix D 的收口:解释只有
732
+ * 一份,`switch` 漏分支是编译错误)。
658
733
  * @param {object[]} entries - 归一目录条目。
659
734
  * @param {string[]} [enabledIds] - 允许清单;空/缺省关闭过滤。
660
735
  * @returns {object[]} 仍被提供的条目,按目录顺序。
661
736
  */
662
737
  function filterByEnabled(entries, enabledIds) {
663
- const list = Array.isArray(enabledIds) ? enabledIds : [];
664
- if (list.length === 0) return Array.isArray(entries) ? entries : [];
665
- const allow = new Set(list.map((id) => str(id, "")));
666
- return (Array.isArray(entries) ? entries : []).filter((entry) => allow.has(str(entry?.id, "")));
738
+ const list = parseAllowedList(enabledIds);
739
+ const all = Array.isArray(entries) ? entries : [];
740
+ if (list.mode === "all") return all;
741
+ if (list.mode === "none") return [];
742
+ const allow = new Set(list.ids);
743
+ return all.filter((entry) => allow.has(str(entry?.id, "")));
667
744
  }
668
745
  /**
669
746
  * 面板用的 roster:每条可寻址 chat 条目一行。
@@ -872,7 +949,7 @@ const inject = ["webServer"];
872
949
  * 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
873
950
  * package.json 一致。
874
951
  */
875
- const PLUGIN_VERSION = "0.3.0";
952
+ const PLUGIN_VERSION = "0.4.0";
876
953
  /**
877
954
  * 默认值。刻意不含额度常数——理由见本文件头。
878
955
  * 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
@@ -920,6 +997,19 @@ const CONFIG_DEFAULTS = Object.freeze({
920
997
  * §坑 5)。宁可多等,不要把一张已经在生成的图扔掉。
921
998
  */
922
999
  drawTimeoutMs: 9e5,
1000
+ /**
1001
+ * 一次出图**工具调用**愿意等的时间(毫秒)。
1002
+ *
1003
+ * 与 `drawTimeoutMs`(总放弃线)是两件事:那个回答「这张图值不值得继续等」,
1004
+ * 这个回答「一轮对话该被占住多久」。两者混为同一个数,就是把上游排队的代价
1005
+ * 转嫁成 agent 的停顿——实测排队 61s–537s,一次调用 await 到底等于让整轮
1006
+ * 对话陪着排队。
1007
+ *
1008
+ * 取 90s:能一次拿完实测最快那一档(61s);更慢的本来就必须分段查(工具返回
1009
+ * `status: pending` + `jobId`,带它再调用即续查),再往上加只是把「等得久」
1010
+ * 换成「卡得久」。
1011
+ */
1012
+ drawWaitMs: 9e4,
923
1013
  /** Host 应答的默认主机名;操作者的列表是「只增不替」。 */
924
1014
  admittedHosts: [
925
1015
  "localhost",
@@ -972,6 +1062,7 @@ function resolveSettings(config) {
972
1062
  drawEnabled: source.drawEnabled === true,
973
1063
  drawModelId: str(source.drawModelId, CONFIG_DEFAULTS.drawModelId),
974
1064
  drawTimeoutMs: clampInt(source.drawTimeoutMs, CONFIG_DEFAULTS.drawTimeoutMs, 3e4, 9e5),
1065
+ drawWaitMs: clampInt(source.drawWaitMs, CONFIG_DEFAULTS.drawWaitMs, 1e4, 9e5),
975
1066
  allowedHosts: resolveAllowedHosts(source)
976
1067
  },
977
1068
  configError: null
@@ -990,6 +1081,7 @@ function resolveSettings(config) {
990
1081
  drawEnabled: false,
991
1082
  drawModelId: CONFIG_DEFAULTS.drawModelId,
992
1083
  drawTimeoutMs: CONFIG_DEFAULTS.drawTimeoutMs,
1084
+ drawWaitMs: CONFIG_DEFAULTS.drawWaitMs,
993
1085
  allowedHosts: new Set(CONFIG_DEFAULTS.admittedHosts)
994
1086
  },
995
1087
  configError: redactSecrets(errMsg(error))
@@ -1056,4 +1148,4 @@ function isAdmitted(request, allowedHosts) {
1056
1148
  }
1057
1149
 
1058
1150
  //#endregion
1059
- export { optional as C, verbatim as D, str as E, obj as S, redactSecrets as T, hasHardQuotaWordingIn as _, resolveSettings as a, degrade as b, LLM_PROVIDER_ID as c, isVisionModel as d, normalizeEntry as f, CODE as g, textToImageModelIds as h, name as i, buildDescriptors as l, rosterWithAvailability as m, inject as n, LLM_API_KEY_NAME as o, resolveAllowedList as p, isAdmitted as r, LLM_DISPLAY_NAME as s, PLUGIN_VERSION as t, imageGenModelIds as u, looksLikeRateLimit as v, pluginError as w, errMsg as x, reclassifyStream as y };
1151
+ export { obj as C, str as D, redactSecrets as E, verbatim as O, errMsg as S, pluginError as T, CODE as _, resolveSettings as a, reclassifyStream as b, LLM_PROVIDER_ID as c, isVisionModel as d, normalizeEntry as f, parseAllowedList as g, enabledCountOf as h, name as i, buildDescriptors as l, textToImageModelIds as m, inject as n, LLM_API_KEY_NAME as o, rosterWithAvailability as p, isAdmitted as r, LLM_DISPLAY_NAME as s, PLUGIN_VERSION as t, imageGenModelIds as u, hasHardQuotaWordingIn as v, optional as w, degrade as x, looksLikeRateLimit as y };