dsh-xdpool 2.2.1 → 2.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.
@@ -29446,6 +29446,13 @@ var CHANNEL_PACK_NS = "channel-pack";
29446
29446
  var channelPackSchema = Schema.object({
29447
29447
  accounts: Schema.array(Schema.any()).default([]),
29448
29448
  disabledModels: Schema.dict(Schema.any()).default({}),
29449
+ // ── 本地扩展(dsh-xdpool,2026-10-17)────────────────────────────────
29450
+ // 每 provider 一个「模型 → 上下文预算(正整数)」表:模型原生的上下文窗口
29451
+ // 常常远超会话实际能吃下的量(1M/2M),用户需要能在选择器里按需收窄,
29452
+ // 语义与 WorkBuddy 账号池里的「上下文大小」选择器一致(0/缺键 = 不收窄)。
29453
+ // **必须显式声明**:SettingsStore 的 `scope.replace()` 是整体覆盖,
29454
+ // schema 里没声明的键会被 schemastery 剥掉,写回时就此静默丢失。
29455
+ contextBudgets: Schema.dict(Schema.any()).default({}),
29449
29456
  loomyPermanentLocked: Schema.boolean().default(false),
29450
29457
  gatewayEnabled: Schema.boolean().default(true),
29451
29458
  providerOrder: Schema.array(Schema.string()).default([])
@@ -29489,6 +29496,21 @@ function sanitizeDisabledModels(raw) {
29489
29496
  }
29490
29497
  return result;
29491
29498
  }
29499
+ function sanitizeContextBudgets(raw) {
29500
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
29501
+ const result = {};
29502
+ for (const [provider, value] of Object.entries(raw)) {
29503
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
29504
+ const perProvider = {};
29505
+ for (const [modelId, budget] of Object.entries(value)) {
29506
+ // 只收正整数:0/负数/NaN/字符串一律当作「没设」,与 catalog.ts 的
29507
+ // `budget > 0` 判据对齐,避免同一份状态在两处得到相反的结论。
29508
+ if (Number.isSafeInteger(budget) && budget > 0) perProvider[modelId] = budget;
29509
+ }
29510
+ if (Object.keys(perProvider).length > 0) result[provider] = perProvider;
29511
+ }
29512
+ return result;
29513
+ }
29492
29514
  function sanitizeAccounts(raw) {
29493
29515
  if (!Array.isArray(raw)) return [];
29494
29516
  const accounts = [];
@@ -29519,6 +29541,7 @@ var SettingsStore = class {
29519
29541
  return {
29520
29542
  accounts: sanitizeAccounts(value.accounts),
29521
29543
  disabledModels: sanitizeDisabledModels(value.disabledModels),
29544
+ contextBudgets: sanitizeContextBudgets(value.contextBudgets),
29522
29545
  // 只是**镜像**(权威表在 permanent-locks.json);老文档没这个键 → false。
29523
29546
  loomyPermanentLocked: value.loomyPermanentLocked === true,
29524
29547
  // 老文档没这个键 → 启用,与升级前行为一致。
@@ -29531,6 +29554,9 @@ var SettingsStore = class {
29531
29554
  await this.scope.replace({
29532
29555
  accounts: state.accounts,
29533
29556
  disabledModels: state.disabledModels,
29557
+ // 预算表丢失的后果只是「下次选择器回到原生窗口」,用户重选一次即可
29558
+ // (纯偏好、可逆),故接受老版本重写时丢掉本键。
29559
+ contextBudgets: state.contextBudgets ?? {},
29534
29560
  // 镜像字段由 AccountPool 与独立文档**同源写出**:同机上只认这个字段的
29535
29561
  // 旧版本代码(其它 profile)读它、也会原样写回它,故两边不会脱节。
29536
29562
  loomyPermanentLocked: state.loomyPermanentLocked === true,
@@ -29634,6 +29660,7 @@ var FileStore = class {
29634
29660
  return {
29635
29661
  accounts: sanitizeAccounts(value.accounts),
29636
29662
  disabledModels: sanitizeDisabledModels(value.disabledModels),
29663
+ contextBudgets: sanitizeContextBudgets(value.contextBudgets),
29637
29664
  // 只是镜像(权威表在 permanent-locks.json);老文档没这个键 → false。
29638
29665
  loomyPermanentLocked: value.loomyPermanentLocked === true,
29639
29666
  // 老文档没这个键 → 启用,与升级前行为一致。
@@ -29677,6 +29704,7 @@ var FileStore = class {
29677
29704
  const state = {
29678
29705
  accounts,
29679
29706
  disabledModels: {},
29707
+ contextBudgets: {},
29680
29708
  // 凭据文件里没有任何锁定信息 → 镜像写 false(权威表另有其文档)。
29681
29709
  loomyPermanentLocked: false,
29682
29710
  // 恢复出的文档本来就不含任何开关信息 → 启用(与全新安装一致)。
@@ -29820,6 +29848,14 @@ var AccountPool = class {
29820
29848
  * 本副本为准)。
29821
29849
  */
29822
29850
  modelCache = {};
29851
+ /**
29852
+ * 每 provider 的「模型 → 上下文预算」表的**权威进程内副本**(与
29853
+ * {@link modelCache} 同理:载入一次后即以本副本为准)。
29854
+ *
29855
+ * ⚠️ 缺键语义是**不收窄**(用模型原生窗口)。预算只用于「用户在选择器里
29856
+ * 把窗口收窄到 200K 这类值」,不改变上游真实能力。
29857
+ */
29858
+ contextBudgetCache = {};
29823
29859
  /**
29824
29860
  * 「锁定永久积分」开关表的**权威进程内副本**(与 {@link cache} 同理)。
29825
29861
  *
@@ -29870,6 +29906,7 @@ var AccountPool = class {
29870
29906
  if (state !== void 0) {
29871
29907
  this.cache = state.accounts;
29872
29908
  this.modelCache = state.disabledModels;
29909
+ this.contextBudgetCache = state.contextBudgets ?? {};
29873
29910
  this.gatewayEnabledCache = state.gatewayEnabled !== false;
29874
29911
  this.providerOrderCache = state.providerOrder ?? [];
29875
29912
  }
@@ -29946,6 +29983,7 @@ var AccountPool = class {
29946
29983
  await this.store.save({
29947
29984
  accounts: this.cache,
29948
29985
  disabledModels: this.modelCache,
29986
+ contextBudgets: this.contextBudgetCache,
29949
29987
  ...this.lockFields(),
29950
29988
  // ⚠️ 必须与账号、黑名单取自**同一时刻**的快照(见本方法的注释):
29951
29989
  // 分两次快照会让同一份文档里的字段互相矛盾。
@@ -30069,6 +30107,48 @@ var AccountPool = class {
30069
30107
  else next[provider] = perProvider;
30070
30108
  await this.writeModels(next);
30071
30109
  }
30110
+ /**
30111
+ * 持久化上下文预算表(同时更新进程内权威副本)。
30112
+ *
30113
+ * 与 {@link writeModels} 同款:先改副本再排队落盘,落盘快照里三个字段取自
30114
+ * 同一时刻,不会出现「预算表与黑名单互相矛盾」。
30115
+ */
30116
+ async writeContextBudgets(contextBudgets) {
30117
+ this.contextBudgetCache = contextBudgets;
30118
+ this.loaded = true;
30119
+ if (this.store.kind === "memory") {
30120
+ this.ctx.logger?.warn?.("[channel-pack] \u65E0\u6301\u4E45\u5316\u540E\u7AEF\uff0c\u4E0A\u4E0B\u6587\u9884\u7B97\u53D8\u66F4\u672A\u843D\u76D8");
30121
+ return;
30122
+ }
30123
+ await this.queueStoreSave();
30124
+ }
30125
+ /**
30126
+ * 读取某 provider 的上下文预算表(模型 id → 正整数窗口)。
30127
+ *
30128
+ * 缺 provider 键 / 缺模型键都表示**不收窄**,适配器据此保留原生窗口。
30129
+ */
30130
+ contextBudgetsFor(provider) {
30131
+ this.ensureLoaded();
30132
+ return { ...this.contextBudgetCache[provider] ?? {} };
30133
+ }
30134
+ /**
30135
+ * 设置/清除单个模型的上下文预算。
30136
+ *
30137
+ * ⚠️ 清除时**删除该键**而不是写 0(与 {@link setModelDisabled} 同款约定):
30138
+ * 表里只留真正在收窄的模型,「没设」永远只有一个表示法。
30139
+ *
30140
+ * @param budget - 正整数窗口;非正整数/非整数视为清除
30141
+ */
30142
+ async setModelContextBudget(provider, modelId, budget) {
30143
+ this.ensureLoaded();
30144
+ const next = { ...this.contextBudgetCache };
30145
+ const perProvider = { ...next[provider] ?? {} };
30146
+ if (Number.isSafeInteger(budget) && budget > 0) perProvider[modelId] = budget;
30147
+ else delete perProvider[modelId];
30148
+ if (Object.keys(perProvider).length === 0) delete next[provider];
30149
+ else next[provider] = perProvider;
30150
+ await this.writeContextBudgets(next);
30151
+ }
30072
30152
  /**
30073
30153
  * 批量关闭一批模型(Channel Pack 模型列表的「关闭全部」)。
30074
30154
  *
@@ -31879,6 +31959,63 @@ function withDeadModelPruning(providers, adapter) {
31879
31959
  });
31880
31960
  }
31881
31961
 
31962
+ /**
31963
+ * 包一层「上下文预算收窄」,只在 `resolveModel` 上生效。
31964
+ *
31965
+ * ## 为什么要包在 `resolveModel` 而不是改 `listModels`
31966
+ *
31967
+ * 目录(有哪些模型)与事实(每个模型多大窗口)是两回事:预算只该影响事实,
31968
+ * 不该影响目录 —— 否则用户把预算设小之后,那个模型会从目录里消失,看起来
31969
+ * 像「被下线了」。
31970
+ *
31971
+ * ## 为什么保留一个「未收窄」入口
31972
+ *
31973
+ * 客户端要同时显示「当前窗口」与「原生窗口」才能渲染出预算选择器(两颗
31974
+ * radio)。若这里直接把 `context.contextWindow` 改小,上层就再也拿不到原值
31975
+ * 了,所以 {@link RAW_ADAPTER} 暴露原始适配器,供
31976
+ * `catalogFactsFor` 取未收窄的事实。
31977
+ *
31978
+ * @param provider - provider id(`this.product.id`,不要写死字面量)
31979
+ * @param adapter - 已包过失效模型剔除的适配器
31980
+ * @param pool - AccountPool,用于现读预算表
31981
+ */
31982
+ var RAW_ADAPTER = Symbol("channel-pack:rawAdapter");
31983
+ function withContextBudgets(provider, adapter, pool) {
31984
+ if (typeof adapter.resolveModel !== "function") return adapter;
31985
+ const target = adapter;
31986
+ const read = (modelId) => {
31987
+ try {
31988
+ return pool.contextBudgetsFor(provider)[modelId];
31989
+ } catch {
31990
+ return void 0;
31991
+ }
31992
+ };
31993
+ const wrapped = new Proxy(adapter, {
31994
+ get(obj, prop, receiver) {
31995
+ if (prop === RAW_ADAPTER) return target;
31996
+ if (prop === "resolveModel") {
31997
+ // 现读 `pool.contextBudgetsFor()`(进程内权威副本,`setContextBudget`
31998
+ // 之后立即生效),所以改一次预算下一次调用就是新的,不需要重注册适配器。
31999
+ return async (...args) => {
32000
+ const resolved = await target.resolveModel.apply(obj, args);
32001
+ if (resolved === null || typeof resolved !== "object") return resolved;
32002
+ const native = Number(resolved.context?.contextWindow);
32003
+ const budget = read(resolved.id ?? args[1]);
32004
+ // 只在「真的有原生窗口、且预算比它小」时收窄;预算 ≥ 原生视为
32005
+ // 「用户没真收窄」,保持原生值,避免把 1M 报成 2M 这类反向放大。
32006
+ if (!Number.isSafeInteger(native) || native <= 0) return resolved;
32007
+ if (!Number.isSafeInteger(budget) || budget <= 0 || budget >= native) return resolved;
32008
+ return { ...resolved, context: { ...resolved.context, contextWindow: budget } };
32009
+ };
32010
+ }
32011
+ const value = Reflect.get(obj, prop, receiver);
32012
+ if (typeof value !== "function") return value;
32013
+ return value.bind(obj);
32014
+ }
32015
+ });
32016
+ return wrapped;
32017
+ }
32018
+
31882
32019
  // src/image-pricing.js
31883
32020
  var FALLBACK_IMAGE_PRICING = Object.freeze({
31884
32021
  /** @param {readonly { offloaded?: boolean }[]} images */
@@ -32373,13 +32510,60 @@ var DEFAULT_MODELS = [
32373
32510
  "deepseek-v4-pro",
32374
32511
  "deepseek-v4.1-flash"
32375
32512
  ];
32513
+ /*
32514
+ * Per-model facts for this channel, keyed lowercase because the remote roster
32515
+ * is inconsistent about case (`GLM-5.2` next to `glm-5.3`).
32516
+ *
32517
+ * The gateway's own listing carries ids and names only — `parseModelInfo` keeps
32518
+ * nothing else — so these are the vendor's published figures, cross-checked
32519
+ * against the same model family on the WorkBuddy gateway (they agreed on every
32520
+ * id the two rosters share). Absent means "this channel publishes no figure":
32521
+ * the row then shows no window instead of a plausible-looking guess, because a
32522
+ * wrong 128K reads as fact and is worse than a blank.
32523
+ *
32524
+ * `openpangu-2.0-flash` / `openpangu-2.0-pro` are absent on purpose — Huawei
32525
+ * publishes no window for them, and they are not on any other roster here.
32526
+ * `glm-5.2-sft-harmony` is the ArkTS SFT variant of GLM-5.2: same window, and
32527
+ * text-only (the SFT is tuned for code, not images).
32528
+ */
32376
32529
  var CONTEXT_WINDOWS = /* @__PURE__ */ new Map([
32377
- ["GLM-5.2", 202752],
32530
+ ["glm-5.2", 202752],
32531
+ ["glm-5.2-sft-harmony", 202752],
32532
+ ["glm-5.1", 2e5],
32533
+ ["glm-5.3", 1e6],
32378
32534
  ["glm-5.3-flash", 1048576],
32379
32535
  ["deepseek-v4-flash", 1048576],
32380
32536
  ["deepseek-v4-pro", 1048576],
32381
- ["deepseek-v4.1-flash", 1e6]
32537
+ ["deepseek-v4.1-flash", 1e6],
32538
+ ["kimi-k2.6-vl", 256e3]
32382
32539
  ]);
32540
+ /*
32541
+ * Which ids accept image input on this channel.
32542
+ *
32543
+ * The adapter used to answer `inputModalities: ["text"]` for every model, which
32544
+ * made the account-pool cards label nine of nine as "text only" — including
32545
+ * models the vendor ships as multimodal. Membership mirrors the vendor's own
32546
+ * capability table; the `-vl` id is here for completeness even though
32547
+ * `listAllModels` filters it out of the picker.
32548
+ */
32549
+ var IMAGE_MODELS = /* @__PURE__ */ new Set([
32550
+ "glm-5.2",
32551
+ "glm-5.1",
32552
+ "glm-5.3",
32553
+ "glm-5.3-flash",
32554
+ "deepseek-v4-flash",
32555
+ "deepseek-v4-pro",
32556
+ "deepseek-v4.1-flash",
32557
+ "kimi-k2.6-vl"
32558
+ ]);
32559
+ /** Window for one id, or undefined when this channel publishes none. */
32560
+ function contextWindowFor(model) {
32561
+ return CONTEXT_WINDOWS.get(String(model).toLowerCase());
32562
+ }
32563
+ /** Input modalities for one id; text is the floor every model here meets. */
32564
+ function modalitiesFor(model) {
32565
+ return IMAGE_MODELS.has(String(model).toLowerCase()) ? ["text", "image"] : ["text"];
32566
+ }
32383
32567
  function contentToText(content) {
32384
32568
  if (typeof content === "string") return content;
32385
32569
  if (!Array.isArray(content)) return "";
@@ -32827,14 +33011,23 @@ var CodeArtsAdapter = class extends LlmAdapter {
32827
33011
  const visible = this.listAllModels();
32828
33012
  const disabled = this.options.accountPool?.disabledModelsFor(PROVIDER);
32829
33013
  const listed = disabled === void 0 || disabled.size === 0 ? visible : visible.filter((m) => !disabled.has(m.id));
32830
- return listed.map((m) => ({ provider: PROVIDER, id: m.id, name: m.name, inputModalities: ["text"] }));
33014
+ return listed.map((m) => {
33015
+ const contextWindow = contextWindowFor(m.id);
33016
+ return {
33017
+ provider: PROVIDER,
33018
+ id: m.id,
33019
+ name: m.name,
33020
+ inputModalities: modalitiesFor(m.id),
33021
+ ...contextWindow === void 0 ? {} : { contextWindow }
33022
+ };
33023
+ });
32831
33024
  }
32832
33025
  async resolveModel(provider, model, _signal) {
32833
33026
  await this.ensureRemoteModels();
32834
33027
  const remoteModel = this.remoteModels?.find((m) => m.id === model);
32835
33028
  const name2 = remoteModel?.name ?? model;
32836
- const contextWindow = CONTEXT_WINDOWS.get(model);
32837
- const resolved = { provider, id: model, name: name2 };
33029
+ const contextWindow = contextWindowFor(model);
33030
+ const resolved = { provider, id: model, name: name2, inputModalities: modalitiesFor(model) };
32838
33031
  if (contextWindow !== void 0) resolved.context = { contextWindow };
32839
33032
  resolved.reasoning = {
32840
33033
  efforts: [
@@ -32847,7 +33040,7 @@ var CodeArtsAdapter = class extends LlmAdapter {
32847
33040
  }
32848
33041
  async prepareCall(provider, model, signal) {
32849
33042
  return {
32850
- model: { ...await this.resolveModel(provider, model, signal), inputModalities: ["text"] },
33043
+ model: await this.resolveModel(provider, model, signal),
32851
33044
  stream: (options) => this.stream(options)
32852
33045
  };
32853
33046
  }
@@ -34046,7 +34239,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34046
34239
  name: "GLM-5.1",
34047
34240
  contextWindow: 2e5,
34048
34241
  supportsImages: true,
34049
- reasoningEfforts: ["medium"],
34242
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34243
+ // 200 with a distinct reasoning_content length per level. The single level
34244
+ // this used to declare was a guess, and it hid four working levels from
34245
+ // the picker.
34246
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34050
34247
  maxOutputTokens: 48e3
34051
34248
  },
34052
34249
  {
@@ -34054,7 +34251,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34054
34251
  name: "GLM-5V-Turbo",
34055
34252
  contextWindow: 2e5,
34056
34253
  supportsImages: true,
34057
- reasoningEfforts: ["medium"],
34254
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34255
+ // 200 with a distinct reasoning_content length per level. The single level
34256
+ // this used to declare was a guess, and it hid four working levels from
34257
+ // the picker.
34258
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34058
34259
  maxOutputTokens: 64e3
34059
34260
  },
34060
34261
  {
@@ -34062,7 +34263,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34062
34263
  name: "Kimi-K3-1",
34063
34264
  contextWindow: 1e6,
34064
34265
  supportsImages: true,
34065
- reasoningEfforts: ["medium"],
34266
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34267
+ // 200 with a distinct reasoning_content length per level. The single level
34268
+ // this used to declare was a guess, and it hid four working levels from
34269
+ // the picker.
34270
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34066
34271
  maxOutputTokens: 32e3
34067
34272
  },
34068
34273
  {
@@ -34081,7 +34286,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34081
34286
  name: "Kimi-K2.7",
34082
34287
  contextWindow: 256e3,
34083
34288
  supportsImages: true,
34084
- reasoningEfforts: ["medium"],
34289
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34290
+ // 200 with a distinct reasoning_content length per level. The single level
34291
+ // this used to declare was a guess, and it hid four working levels from
34292
+ // the picker.
34293
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34085
34294
  maxOutputTokens: 32e3
34086
34295
  },
34087
34296
  {
@@ -34089,7 +34298,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34089
34298
  name: "Kimi-K2.6",
34090
34299
  contextWindow: 256e3,
34091
34300
  supportsImages: true,
34092
- reasoningEfforts: ["medium"],
34301
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34302
+ // 200 with a distinct reasoning_content length per level. The single level
34303
+ // this used to declare was a guess, and it hid four working levels from
34304
+ // the picker.
34305
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34093
34306
  maxOutputTokens: 32e3
34094
34307
  },
34095
34308
  {
@@ -34097,7 +34310,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34097
34310
  name: "MiniMax-M3",
34098
34311
  contextWindow: 512e3,
34099
34312
  supportsImages: true,
34100
- reasoningEfforts: ["medium"],
34313
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34314
+ // 200 with a distinct reasoning_content length per level. The single level
34315
+ // this used to declare was a guess, and it hid four working levels from
34316
+ // the picker.
34317
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34101
34318
  maxOutputTokens: 64e3
34102
34319
  }
34103
34320
  ];
@@ -34123,8 +34340,8 @@ var WORKBUDDY_FALLBACK_MODELS = [
34123
34340
  // (`node scripts/dump-max-output.mjs`)。该值就是用户在 IDE 里实际拿到的
34124
34341
  // 单次输出额度,远端不可用时由本表顶替。远端未下发的模型保持 undefined。
34125
34342
  { id: "default-model", name: "Auto", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
34126
- { id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34127
- { id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34343
+ { id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34344
+ { id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34128
34345
  { id: "primary-model", name: "Primary", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["high"], maxOutputTokens: 72e3 },
34129
34346
  { id: "deep-model", name: "Deep", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
34130
34347
  {
@@ -34156,14 +34373,14 @@ var WORKBUDDY_FALLBACK_MODELS = [
34156
34373
  defaultReasoningEffort: "high",
34157
34374
  maxOutputTokens: 64e3
34158
34375
  },
34159
- { id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["high"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
34376
+ { id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
34160
34377
  {
34161
34378
  // 2026-09 补录:新加坡区的同代模型(-sg 后缀),远端下发且实测能看图。
34162
34379
  id: "deepseek-v4.1-flash-sg",
34163
34380
  name: "Deepseek-V4.1-Flash",
34164
34381
  contextWindow: 1e6,
34165
34382
  supportsImages: true,
34166
- reasoningEfforts: ["high"],
34383
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34167
34384
  defaultReasoningEffort: "high",
34168
34385
  maxOutputTokens: 128e3
34169
34386
  },
@@ -34223,7 +34440,11 @@ var WORKBUDDY_FALLBACK_MODELS = [
34223
34440
  },
34224
34441
  // gpt-5.3-codex:远端未下发 maxOutputTokens,故不填(保持 undefined,
34225
34442
  // 交由网关默认),不臆造数值。
34226
- { id: "gpt-5.3-codex", name: "GPT-5.3-Codex", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["medium"] },
34443
+ // gpt-5.3-codex answered 200 at every level, but returned an EMPTY
34444
+ // reasoning_content at all five — the levels are legal yet not observably
34445
+ // distinct. Declaring the ladder states what may be sent; it does not
34446
+ // claim the model thinks.
34447
+ { id: "gpt-5.3-codex", name: "GPT-5.3-Codex", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"] },
34227
34448
  { id: "gemini-3.5-flash", name: "Gemini-3.5-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 65536 },
34228
34449
  {
34229
34450
  id: "glm-5.3",
@@ -34243,7 +34464,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
34243
34464
  defaultReasoningEffort: "high",
34244
34465
  maxOutputTokens: 48e3
34245
34466
  },
34246
- { id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34467
+ { id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34247
34468
  {
34248
34469
  // 2026-09 补录:远端 /v3/config 的 cli agent 白名单里有它,兜底表原先漏了。
34249
34470
  id: "kimi-k2.8-preview",
@@ -34254,7 +34475,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
34254
34475
  defaultReasoningEffort: "high",
34255
34476
  maxOutputTokens: 32e3
34256
34477
  },
34257
- { id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 }
34478
+ { id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 }
34258
34479
  ];
34259
34480
  var WORKBUDDY = {
34260
34481
  id: "workbuddy",
@@ -60236,6 +60457,76 @@ async function fullCatalogIds(ctx, modelAdapters, provider) {
60236
60457
  return { error: `\u8BFB\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1A${reason}` };
60237
60458
  }
60238
60459
  }
60460
+ /**
60461
+ * 逐个模型向适配器问一次「能力事实」(上下文窗口 / 输入模态 / 输出上限 /
60462
+ * 思考档位),并套用用户设定的上下文预算。
60463
+ *
60464
+ * ## 为什么必须走这里,而不是 `llm.listModels()`
60465
+ *
60466
+ * 上游每个适配器的 `listModels()` 首行都是
60467
+ * `if (!await providerCatalogVisible(pool, PROVIDER)) return []` —— 该闸门在
60468
+ * 凭据解析不出来时(历史遗留的已删除 ref、尚未完成登录)返回**空数组**。
60469
+ * 于是 `llm.listModels()` 对 9 个渠道里有 8 个给出空目录,上下文窗口与识图
60470
+ * 能力这类事实自然一条都拿不到。而适配器自己的 `resolveModel()` 读的是**本地
60471
+ * 目录与产品常量**,不经过那道闸门 —— 与 {@link fullCatalogIds} 先问
60472
+ * `listAllModels()` 是同一条思路。
60473
+ *
60474
+ * ## 成本与取舍
60475
+ *
60476
+ * 部分适配器的 `resolveModel()` 会先 `ensureRemoteModels()`(一次网络往返),
60477
+ * 所以并发限在 {@link FACT_LOOKUP_CONCURRENCY};单个模型失败只丢它自己的事实,
60478
+ * 绝不让整条 `model.list` 失败(缺事实只是少显示一行,不该把目录整个打没)。
60479
+ *
60480
+ * @returns modelId → 事实对象
60481
+ */
60482
+ var FACT_LOOKUP_CONCURRENCY = 6;
60483
+ async function catalogFactsFor(modelAdapters, provider, catalog, contextBudgets) {
60484
+ const facts = /* @__PURE__ */ new Map();
60485
+ const registered = modelAdapters?.[provider];
60486
+ if (registered === void 0) return facts;
60487
+ // 刻意问**未包预算**的原始适配器:这里要的是原生窗口,预算由调用方自己套
60488
+ // (见下面的 `capped`),否则「收窄后」再问一次只会拿到被自己改小的值,
60489
+ // `nativeContextWindow` 永远等于当前窗口,客户端就没法渲染选择器了。
60490
+ const adapter = registered[RAW_ADAPTER] ?? registered;
60491
+ if (typeof adapter.resolveModel !== "function") return facts;
60492
+ let cursor = 0;
60493
+ const worker = async () => {
60494
+ while (cursor < catalog.length) {
60495
+ const index = cursor++;
60496
+ const model = catalog[index];
60497
+ if (model === void 0) continue;
60498
+ let resolved;
60499
+ try {
60500
+ resolved = await adapter.resolveModel(provider, model.id);
60501
+ } catch (error) {
60502
+ void error;
60503
+ continue;
60504
+ }
60505
+ if (resolved === null || typeof resolved !== "object") continue;
60506
+ const native = Number(resolved.context?.contextWindow);
60507
+ const budget = contextBudgets?.[model.id];
60508
+ const capped = Number.isSafeInteger(budget) && budget > 0 && budget < native ? budget : void 0;
60509
+ const maxTokens = Number(resolved.defaultMaxTokens);
60510
+ const modalities = Array.isArray(resolved.inputModalities) ? [...resolved.inputModalities] : void 0;
60511
+ const efforts = resolved.reasoning?.efforts;
60512
+ const fact = {};
60513
+ if (Number.isSafeInteger(native) && native > 0) {
60514
+ fact.contextWindow = capped === void 0 ? native : capped;
60515
+ // 只在真的收窄过时才带原生窗口:没收窄时两个值相同,重复传输没有意义,
60516
+ // 而客户端正是靠「有 nativeContextWindow 才显示预算选择器」来决定要不要
60517
+ // 渲染那两颗 radio 的。
60518
+ if (capped !== void 0) fact.nativeContextWindow = native;
60519
+ }
60520
+ if (Number.isSafeInteger(maxTokens) && maxTokens > 0) fact.maxTokens = maxTokens;
60521
+ if (modalities !== void 0 && modalities.length > 0) fact.inputModalities = modalities;
60522
+ if (Array.isArray(efforts) && efforts.length > 0) fact.reasoning = true;
60523
+ if (Object.keys(fact).length > 0) facts.set(model.id, fact);
60524
+ }
60525
+ };
60526
+ const width = Math.min(FACT_LOOKUP_CONCURRENCY, catalog.length);
60527
+ await Promise.all(Array.from({ length: width }, () => worker()));
60528
+ return facts;
60529
+ }
60239
60530
  function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lobsterai, qoder, qoderCn, trae, cline, loomy, raccoon, minimax, zcode, gemini, modelAdapters, config = {}) {
60240
60531
  const connection = ctx.connection ?? ctx.get("connection");
60241
60532
  if (!connection || typeof connection.fetch?.register !== "function") {
@@ -60752,13 +61043,21 @@ function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lob
60752
61043
  userId
60753
61044
  );
60754
61045
  if (existingId.length > 0 && existingId !== id) {
61046
+ // 本地适配(dsh-xdpool):重扫同一账号时用新凭据覆盖已有条目,
61047
+ // 而不是新增一条「重复,已停用」的死条目。覆盖顺序很重要:
61048
+ // 先把已有条目的 credentialRef 指到新凭据,再删掉本次的新条目
61049
+ //(removeAccount 会 unset 其 credentialRef 指向的凭据本体,
61050
+ // 此时新条目仍指着新凭据,所以必须最后删)。若先删新条目,
61051
+ // 新凭据会被 unset,覆盖就变成了把已有条目指向一个已销毁的 ref。
61052
+ await pool.updateAccount(existingId, {
61053
+ credentialRef: refName,
61054
+ nickname: `ZCode ${label}`,
61055
+ enabled: true
61056
+ });
60755
61057
  ctx.logger.warn(
60756
- `[channel-pack] zcode \u8BE5\u8D26\u53F7\uFF08user_id=${userId}\uFF09\u5DF2\u5B58\u5728\u4E8E ${existingId}\uFF0C\u65B0\u6761\u76EE ${id} \u5DF2\u81EA\u52A8\u505C\u7528\uFF08\u4E0D\u53C2\u4E0E\u9009\u53F7\uFF09`
61058
+ `[channel-pack] zcode \u8BE5\u8D26\u53F7\uFF08user_id=${userId}\uFF09\u5DF2\u5B58\u5728\u4E8E ${existingId}\uFF0C\u5DF2\u7528\u65B0\u51ED\u636E\u8986\u76D6\u8BE5\u6761\u76EE\uFF08\u4E0D\u65B0\u589E\u505C\u7528\u6761\u76EE\uFF09`
60757
61059
  );
60758
- await pool.updateAccount(id, {
60759
- nickname: `ZCode ${label}\uFF08\u91CD\u590D\uFF0C\u5DF2\u505C\u7528\uFF09`,
60760
- enabled: false
60761
- });
61060
+ await pool.removeAccount(id);
60762
61061
  return;
60763
61062
  }
60764
61063
  }
@@ -61936,20 +62235,34 @@ function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lob
61936
62235
  // 「设置在某个模型上,选择器里却找不到它」。
61937
62236
  case "model.list": {
61938
62237
  const req = payload;
61939
- const llm = llmServiceOf(ctx);
61940
- if (llm === void 0) {
61941
- return { ok: false, error: { code: "bad-request", message: "llm \u670D\u52A1\u4E0D\u53EF\u7528" } };
61942
- }
61943
- let models;
61944
- try {
61945
- models = await llm.listModels(req.provider);
61946
- } catch (error) {
61947
- const reason = error instanceof Error ? error.message : String(error);
61948
- return { ok: false, error: { code: "bad-request", message: `\u8BFB\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1A${reason}` } };
61949
- }
61950
62238
  const disabledMap = pool.listDisabledModels(req.provider);
61951
62239
  const catalogSource = modelAdapters?.[req.provider];
61952
62240
  const all = catalogSource?.listAllModels();
62241
+ // ── 本地适配(dsh-xdpool,2026-10-17)────────────────────────────
62242
+ // `llm` 只在**没有适配器目录**时才需要:上游把 `llm.listModels()`
62243
+ // 放在最前面当硬门槛,于是宿主未注册 `llm` 服务(或它抛错)时,
62244
+ // 整条 RPC 直接失败 —— 而下面那段目录其实就在本地、根本不需要
62245
+ // 任何服务。表现是账号池页每一张渠道卡都印「该渠道还没有发布
62246
+ // 模型」,把一次宿主侧的服务缺失伪装成了渠道的空目录。
62247
+ //
62248
+ // 有适配器目录时 `llm.listModels()` 的返回值**本来就没被用到**
62249
+ // (见下面的 `all !== void 0` 分支:目录直接取自适配器),所以
62250
+ // 把它降级为「仅无目录时必需」既不改变任何一条成功路径的输出,
62251
+ // 又能让目录照常返回。仅在没有目录时才继续问 `llm`,问不通就
62252
+ // 如实报错。
62253
+ let models;
62254
+ if (all === void 0) {
62255
+ const llm = llmServiceOf(ctx);
62256
+ if (llm === void 0) {
62257
+ return { ok: false, error: { code: "bad-request", message: "llm \u670D\u52A1\u4E0D\u53EF\u7528" } };
62258
+ }
62259
+ try {
62260
+ models = await llm.listModels(req.provider);
62261
+ } catch (error) {
62262
+ const reason = error instanceof Error ? error.message : String(error);
62263
+ return { ok: false, error: { code: "bad-request", message: `\u8BFB\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1A${reason}` } };
62264
+ }
62265
+ }
61953
62266
  let catalog;
61954
62267
  if (all !== void 0) {
61955
62268
  catalog = [...all];
@@ -61973,6 +62286,15 @@ function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lob
61973
62286
  if (!known.has(id)) catalog.push({ id, name: id });
61974
62287
  }
61975
62288
  }
62289
+ // ── 本地扩展(dsh-xdpool,2026-10-17)────────────────────────────
62290
+ // 上游这份 value 只有 `{id,name,disabled,dead,isFree?}`,上下文窗口与
62291
+ // 识图能力一概没有,于是账号池页每一行都只能印一个光秃秃的模型名。
62292
+ // 那些事实其实**就在适配器里**(`contextWindowFor` / `inputModalitiesFor`
62293
+ // / `resolveModel`),只是被 `llm.listModels()` 的凭据闸门挡在了外面,
62294
+ // 所以这里直接向适配器逐个问一次(见 `catalogFactsFor` 的注释)。
62295
+ // 单个模型问不到就只缺它自己的事实,目录照常返回。
62296
+ const contextBudgets = pool.contextBudgetsFor(req.provider);
62297
+ const facts = await catalogFactsFor(modelAdapters, req.provider, catalog, contextBudgets);
61976
62298
  const value = {
61977
62299
  models: catalog.map((model) => ({
61978
62300
  id: model.id,
@@ -61984,7 +62306,10 @@ function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lob
61984
62306
  // ⚠️ 免费标记**照原样透传,缺失就不写**(不编造 `false`):Channel Pack 的
61985
62307
  // 模型列表按「计费/来源」分组,把「适配器没报」当成「按量计费」是
61986
62308
  // 保守归组,但字段本身仍保持「未知」语义(与全仓约定一致)。
61987
- ...model.isFree === void 0 ? {} : { isFree: model.isFree }
62309
+ ...model.isFree === void 0 ? {} : { isFree: model.isFree },
62310
+ // 上下文预算(缺键 = 不收窄)。与 disabled/dead 同款透传约定。
62311
+ ...contextBudgets[model.id] === void 0 ? {} : { contextBudget: contextBudgets[model.id] },
62312
+ ...facts.get(model.id) ?? {}
61988
62313
  }))
61989
62314
  };
61990
62315
  return { ok: true, value };
@@ -62129,6 +62454,32 @@ function registerChannelPackEndpoints(ctx, pool, codearts, buddy, workbuddy, lob
62129
62454
  };
62130
62455
  return { ok: true, value };
62131
62456
  }
62457
+ /**
62458
+ * 设置/清除单个模型的上下文预算(账号池页模型行上的「上下文大小」)。
62459
+ *
62460
+ * 与 `model.setDisabled` 同款约定:不猜默认值、不空转落盘。
62461
+ *
62462
+ * ⚠️ **不广播 `llm/adapters-updated`**:与「打开/关闭模型」不同,预算不
62463
+ * 改变目录本身(模型还是那些模型),而它真正影响的 `resolveModel` 结果
62464
+ * 每次请求都会现读 `pool.contextBudgetsFor()`(见
62465
+ * `withContextBudgets`),所以下一次调用即生效,无需让客户端缓存失效。
62466
+ */
62467
+ case "model.setContextBudget": {
62468
+ const req = payload;
62469
+ if (typeof req.provider !== "string" || req.provider.length === 0 || typeof req.modelId !== "string" || req.modelId.length === 0) {
62470
+ return { ok: false, error: { code: "bad-request", message: "provider \u4E0E modelId \u5FC5\u586B" } };
62471
+ }
62472
+ const budget = req.budget;
62473
+ if (budget !== void 0 && budget !== null && !Number.isSafeInteger(budget)) {
62474
+ return { ok: false, error: { code: "bad-request", message: "budget \u5FC5\u987B\u662F\u6B63\u6574\u6570" } };
62475
+ }
62476
+ await pool.setModelContextBudget(req.provider, req.modelId, budget === null ? void 0 : budget);
62477
+ const value = {
62478
+ provider: req.provider,
62479
+ contextBudgets: pool.contextBudgetsFor(req.provider)
62480
+ };
62481
+ return { ok: true, value };
62482
+ }
62132
62483
  /**
62133
62484
  * 读取多个供应商的汇总状态(Channel Pack 左侧导航分组 + 一键开关据此渲染)。
62134
62485
  *
@@ -67588,6 +67939,147 @@ function apply(ctx, config = {}) {
67588
67939
  const service = new CodeArtsAuth(ctx);
67589
67940
  const pool = new AccountPool(ctx);
67590
67941
  configureDeadModelStore(ctx);
67942
+ // ── 本地适配(dsh-xdpool):扫码渠道**首次**登录成功后默认静音该渠道 ──
67943
+ // 需求(2026-10-16):新扫码的渠道不该立刻把它的全部模型灌进选择器;
67944
+ // 账号先出现在账号池页,模型默认全不勾,用户在账号池页手动勾选启用。
67945
+ // 实现:包装 `pool.addAccount`,当该 provider 此前没有任何账号(即本次是
67946
+ // 该渠道的第一条账号),或该渠道已有账号但黑名单为空(历史上在无钩子的
67947
+ // 旧版本下登录过、从未静音过的渠道——2026-10-16 补)时,在凭据落定后把
67948
+ // 该渠道全部模型写入黑名单。
67949
+ // - 对全部渠道一视同仁(含 buddy / workbuddy——用户预期两条 WorkBuddy 卡
67950
+ // 也从扫码登录页进来,且 WorkBuddy 渠道卡也展示模型勾选);
67951
+ // - 静音动作在微任务里执行(addAccount 是同步追加,不阻塞其返回值);
67952
+ // - 目录缺失时静默放弃(不报错 —— 静音失败只是少一次便利,不该毁掉登录);
67953
+ // - `modelAdapters` 在本函数更下方才组装,这里用 let + 闭包间接引用。
67954
+ // - 用户在账号池页取消勾选后再登录同一渠道:isFirst 为 false,且黑名单
67955
+ // 非空(用户勾选的模型已被移出黑名单),不会重复静音。
67956
+ let channelPackModelAdaptersRef;
67957
+ const rawAddAccount = pool.addAccount.bind(pool);
67958
+ // ── 本地适配(dsh-xdpool):清掉同渠道同昵称的重复账号 ──
67959
+ // 需求(2026-10-16):用户在扫码登录页反复点「添加账号」,上游 `addAccount`
67960
+ // 是纯追加(无任何去重),于是同一渠道攒出多条同号账号,账号池页一张卡里
67961
+ // 显示 3-4 个一模一样的人。判定键取 (provider, nickname):同一渠道里昵称
67962
+ // 相同即视为同一个账号(上游所有渠道在凭据落定后都用 `updateAccount` 把
67963
+ // nickname 改成真实显示名,所以昵称才是稳定身份,id 每次都是新随机值)。
67964
+ //
67965
+ // ⚠️ 保留哪一条不能只看 createdAt newest。反复点「添加账号」时,**新加的
67966
+ // 那条是登录还没走完的半成品**(expiresAt 缺失),而**旧的那条是完整可用
67967
+ // 的旧凭据**(zcode 的凭据甚至不带过期时间)。按 newest 保留会留下半成品、
67968
+ // 删掉能用的 —— 这正是本机 zcode 的实际形态(10-16 15:15 的三条无
67969
+ // expiresAt,16:15 新加的那条也无 expiresAt,而其它渠道都有)。所以判定
67970
+ // 优先级改成「可用性」:
67971
+ // ① expiresAt 为空视为长期凭据、可用;有值时必须晚于「现在」才算可用;
67972
+ // ② 双方都可用 → 留旧的(先登录的那条,凭据久经使用);
67973
+ // 旧的已过期而新的可用 → 换新;其余 → 留旧。
67974
+ const channelAccountIsUsable = (account, now) => {
67975
+ const expiresAt = Number(account.expiresAt);
67976
+ if (!Number.isFinite(expiresAt) || expiresAt <= 0) return true;
67977
+ return expiresAt > now;
67978
+ };
67979
+ const dedupeChannelAccounts = async () => {
67980
+ try {
67981
+ const all = await pool.listAllAccounts();
67982
+ const now = Date.now();
67983
+ const groups = new Map();
67984
+ for (const account of all) {
67985
+ const nickname = typeof account.nickname === "string" ? account.nickname : "";
67986
+ // 昵称为空的条目无从判断是否重复(登录尚未落定的中间态),一律保留。
67987
+ if (account.provider === "" || nickname === "") continue;
67988
+ const key = `${account.provider}\u0000${nickname}`;
67989
+ const kept = groups.get(key);
67990
+ if (kept === undefined) {
67991
+ groups.set(key, account);
67992
+ continue;
67993
+ }
67994
+ const keptUsable = channelAccountIsUsable(kept, now);
67995
+ const accountUsable = channelAccountIsUsable(account, now);
67996
+ const replace = !keptUsable && accountUsable;
67997
+ const drop = replace ? kept : account;
67998
+ if (replace) groups.set(key, account);
67999
+ await pool.removeAccount(drop.id);
68000
+ ctx.logger.info(
68001
+ `[channel-pack] ${drop.provider} 重复账号 ${drop.nickname}(${drop.id}):已清理,保留 ${(replace ? account : kept).id}`
68002
+ );
68003
+ }
68004
+ } catch (error) {
68005
+ ctx.logger.warn(
68006
+ `[channel-pack] 重复账号清理失败(不影响使用):${error instanceof Error ? error.message : String(error)}`
68007
+ );
68008
+ }
68009
+ };
68010
+ // 启动时先扫一遍:用户升级前攒下的重复只能靠这一次清掉。
68011
+ void Promise.resolve().then(() => dedupeChannelAccounts());
68012
+ // ── 本地适配(dsh-xdpool):启动时补静音「黑名单为空但已有账号」的渠道 ──
68013
+ // 需求(2026-10-17):上面的首登静音只在「下一次登录」时才补,而用户是在
68014
+ // 打上补丁之前就登好的那些渠道(codearts / lobsterai / cline / raccoon /
68015
+ // minimax / gemini),黑名单永远是空的,账号池页打开就是「全部已勾」,
68016
+ // 与「让用户自己选」的预期相反。启动这一次把这类渠道补上。
68017
+ //
68018
+ // 例外:buddy / workbuddy 不在其中。这两条 lane 由 dsh-xdpool 自己的账号池
68019
+ // 服务(账号池页顶部的 WorkBuddy 国内版 / 国际版两张卡),它们有一套自己的
68020
+ // 模型选择(写进本插件的配置,不走 pack 的黑名单);在这里把它们静音等于
68021
+ // 用 pack 的黑名单去覆盖另一套开关,会让这两张卡的模型凭空消失。
68022
+ const POOL_OWNED_PROVIDERS = /* @__PURE__ */ new Set([
68023
+ "buddy",
68024
+ "workbuddy"
68025
+ ]);
68026
+ const muteChannelsWithEmptyBlocklist = async () => {
68027
+ try {
68028
+ const all = await pool.listAllAccounts();
68029
+ const providers = new Set(
68030
+ all.map((account) => account.provider).filter((provider) => typeof provider === "string" && provider !== "" && !POOL_OWNED_PROVIDERS.has(provider))
68031
+ );
68032
+ for (const provider of providers) {
68033
+ if (pool.disabledModelsFor(provider).size !== 0) continue;
68034
+ const catalog = channelPackModelAdaptersRef?.[provider]?.listAllModels();
68035
+ if (!Array.isArray(catalog) || catalog.length === 0) continue;
68036
+ await pool.setModelsDisabled(provider, catalog.map((model) => model.id));
68037
+ ctx.logger.info(
68038
+ `[channel-pack] ${provider} 启动补静音(已有账号但黑名单为空):已默认静音 ${catalog.length} 个模型(可在账号池页勾选启用)`
68039
+ );
68040
+ }
68041
+ } catch (error) {
68042
+ ctx.logger.warn(
68043
+ `[channel-pack] 启动补静音失败(不影响使用):${error instanceof Error ? error.message : String(error)}`
68044
+ );
68045
+ }
68046
+ };
68047
+ void Promise.resolve().then(() => muteChannelsWithEmptyBlocklist());
68048
+ pool.addAccount = async (entry) => {
68049
+ const isFirstForProvider = pool.listAccountsByProvider(entry.provider).length === 0;
68050
+ const hadEmptyBlocklist = pool.disabledModelsFor(entry.provider).size === 0;
68051
+ const result = await rawAddAccount(entry);
68052
+ if ((isFirstForProvider || hadEmptyBlocklist) && !POOL_OWNED_PROVIDERS.has(entry.provider)) {
68053
+ void Promise.resolve().then(async () => {
68054
+ try {
68055
+ const adapters = channelPackModelAdaptersRef;
68056
+ const catalog = adapters?.[entry.provider]?.listAllModels();
68057
+ if (!Array.isArray(catalog) || catalog.length === 0) return;
68058
+ await pool.setModelsDisabled(entry.provider, catalog.map((model) => model.id));
68059
+ ctx.logger.info(
68060
+ `[channel-pack] ${entry.provider} ${isFirstForProvider ? "首次登录" : "补静音(黑名单为空)"}:已默认静音 ${catalog.length} 个模型(可在账号池页勾选启用)`
68061
+ );
68062
+ } catch (error) {
68063
+ ctx.logger.warn(
68064
+ `[channel-pack] ${entry.provider} 首次登录默认静音失败(不影响登录):${error instanceof Error ? error.message : String(error)}`
68065
+ );
68066
+ }
68067
+ });
68068
+ }
68069
+ // 登录完成后昵称才变成真实显示名,此时才谈得上判重;微任务里跑,
68070
+ // 不阻塞 `account.create` 的返回值。
68071
+ void Promise.resolve().then(() => dedupeChannelAccounts());
68072
+ return result;
68073
+ };
68074
+ // 判重的真正时机在 `updateAccount`:各渠道的登录结果都是在那儿把 nickname
68075
+ // 从随机 id 改成真实显示名(手机号尾号 / 服务端昵称),重复的两条在这一刻
68076
+ // 才变得可辨。同 provider + 同显示名 = 同一个账号。
68077
+ const rawUpdateAccount = pool.updateAccount.bind(pool);
68078
+ pool.updateAccount = async (id, patch) => {
68079
+ const result = await rawUpdateAccount(id, patch);
68080
+ void Promise.resolve().then(() => dedupeChannelAccounts());
68081
+ return result;
68082
+ };
67591
68083
  const tokenLedgerStore = createTokenLedgerStore(ctx);
67592
68084
  const detachTokenLedger = attachTokenLedgerStore(tokenLedgerStore);
67593
68085
  ctx.effect(() => () => {
@@ -68485,7 +68977,7 @@ function apply(ctx, config = {}) {
68485
68977
  }
68486
68978
  const pruned = (provider, adapter) => {
68487
68979
  try {
68488
- return withDeadModelPruning([provider], adapter);
68980
+ return withContextBudgets(provider, withDeadModelPruning([provider], adapter), pool);
68489
68981
  } catch (error) {
68490
68982
  ctx.logger.warn(`[channel-pack] ${provider} \u7684\u5931\u6548\u6A21\u578B\u5254\u9664\u5305\u88C5\u5931\u8D25\uFF0C\u4F7F\u7528\u539F\u9002\u914D\u5668: ${String(error)}`);
68491
68983
  return adapter;
@@ -68509,6 +69001,7 @@ function apply(ctx, config = {}) {
68509
69001
  zcode: pruned(ZCODE.id, zcodeAdapter),
68510
69002
  ...opencodeAdapter === void 0 ? {} : { opencode: pruned(OPENCODE.id, opencodeAdapter) }
68511
69003
  };
69004
+ channelPackModelAdaptersRef = modelAdapters;
68512
69005
  ctx.effect(() => () => {
68513
69006
  void geminiSigStore.flush().catch(() => {
68514
69007
  });