dsh-workbuddy-xdpool 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@
4
4
 
5
5
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
6
6
 
7
+ ## 0.4.1 (2026-09-19)
8
+
9
+ 这次主要修了三个毛病。
10
+
11
+ ### 一、国际版账号终于能被识别了
12
+
13
+ 之前国际版账号一直检测不到。原因是筛选登录文件时只认「点号」拼出来的文件名,而国际版客户端登录后生成的文件是 `workbuddy-desktop-ai.info`(中间是连字符),于是被整个跳过了。
14
+
15
+ 现在改成只看文件后缀,不管文件名长什么样都能收进来。实测国内版 4 个、国际版 1 个账号都能正常识别。
16
+
17
+ ### 二、国内版和国际版不再共用同一份模型列表
18
+
19
+ 之前两个版本共用同一份模型清单,而且这份清单只从国内版账号拉取 —— 换句话说,国际版看到的其实一直是国内版的模型。
20
+
21
+ 现在两个版本各查各的:
22
+
23
+ - 国内版:16 个模型
24
+ - 国际版:21 个模型(之前只有凑数的 10 个,而且内容还是国内版的)
25
+
26
+ 国际版独有的 Auto / Fast / Balanced 这些模型现在也能正常显示。
27
+
28
+ 顺带修了国际版读取模型的方式:它的接口地址和客户端标识跟国内版都不一样,之前照搬国内版的写法,国际版那边会直接报错,所以才一直退回那份凑数的清单。
29
+
30
+ ### 三、模型选择器里同一个供应商不再被拆成两组
31
+
32
+ 之前模型选择器里「WorkBuddy XD Pool(国内版)」「(国际版)」会各出现两次,免费模型和付费模型被分到不同的组,中间还夹着别的插件,看着像重复注册了。
33
+
34
+ 根因是插件把每个模型都上报成「不支持图片」。宿主上如果装了图片增强类插件,它会给这些模型再补一条**名字一模一样**的条目,于是同一个名字就出现了两遍。
35
+
36
+ 现在按上游真实的能力上报,分组就合并回来了。
37
+
38
+ 另外卡片里的「图片输入」勾选也改了语义:勾选只表示**额外**支持图片,不会再反过来把没勾选的模型降级成「不支持」。
39
+
7
40
  ## 0.4.0 (2026-09-16)
8
41
 
9
42
  本次更新来自两条用户反馈与一次上游兼容性对齐:账号现在可以**按顺序逐个用尽**(不再平均分摊),积分包**逐条显示具体到期时刻**,并修掉了一处会让所有接口返回 401 的凭据选择缺陷。
package/lib/bin.js CHANGED
@@ -15,7 +15,18 @@ const CN_BILLING_BASE = "https://www.codebuddy.cn";
15
15
  /** Global base for `workbuddy.ai` logins. */
16
16
  const GLOBAL_BASE = "https://www.workbuddy.ai";
17
17
  /** Client UA the desktop CLI uses. */
18
+ /** Client UA the desktop CLI uses — the CN gateway answers this one. */
18
19
  const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
20
+ /**
21
+ * Desktop app UA. The global gateway serves its product config only to this
22
+ * client channel: the CLI UA gets a truncated roster (or an HTTP 500), which
23
+ * is why the international catalog must be read with the desktop spelling.
24
+ */
25
+ const DESKTOP_UA = "WorkBuddy/5.5.2";
26
+ /** CN model catalog. */
27
+ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
28
+ /** Global product config, which carries the international model roster. */
29
+ const GLOBAL_CONFIG_PATH = "/v3/config";
19
30
  const JSON_TIMEOUT_MS = 3e4;
20
31
  const ERROR_BODY_LIMIT = 4096;
21
32
  /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
@@ -261,6 +272,7 @@ function parseUpstreamModel(value) {
261
272
  const creditMultiplier = parseCreditMultiplier(raw["credits"]);
262
273
  const reasoning = parseReasoning(raw["reasoning"]);
263
274
  const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
275
+ const supportsImages = typeof raw["supportsImages"] === "boolean" ? raw["supportsImages"] : void 0;
264
276
  return {
265
277
  id,
266
278
  name,
@@ -270,7 +282,8 @@ function parseUpstreamModel(value) {
270
282
  ...reasoning === void 0 ? {} : { reasoning },
271
283
  ...descriptionZh === void 0 ? {} : { descriptionZh },
272
284
  ...descriptionEn === void 0 ? {} : { descriptionEn },
273
- ...supportsToolCall === void 0 ? {} : { supportsToolCall }
285
+ ...supportsToolCall === void 0 ? {} : { supportsToolCall },
286
+ ...supportsImages === void 0 ? {} : { supportsImages }
274
287
  };
275
288
  }
276
289
  var WorkBuddyUpstreamClient = class {
@@ -374,16 +387,43 @@ var WorkBuddyUpstreamClient = class {
374
387
  if (typeof data["domain"] === "string" && data["domain"] !== "") outcome.domain = data["domain"];
375
388
  return outcome;
376
389
  }
377
- /** GET the personal model catalog, keeping the `cli` agent's models only. */
390
+ /**
391
+ * Fetch the model catalog, keeping the `cli` agent's models only.
392
+ *
393
+ * The two gateways are read differently, because they answer differently:
394
+ *
395
+ * - **CN** serves the roster at `/v2/enterprises/personal/models` and expects
396
+ * the CLI client spelling.
397
+ * - **Global** serves it as part of the product config at `/v3/config`, and
398
+ * only to the DESKTOP client channel. Asking the global host with the CLI UA
399
+ * yields a truncated roster, and the CN path answers HTTP 500 there — which
400
+ * is what left the international provider on its static fallback.
401
+ *
402
+ * Both documents share the `{ models, agents }` entry shape, so the parsing
403
+ * below is common to the two branches.
404
+ */
378
405
  async fetchModels(credential, signal) {
379
- const response = await this.fetchImpl(`${chatBase(credential)}/console/enterprises/personal/models`, {
380
- headers: {
381
- "Authorization": `Bearer ${credential.accessToken}`,
382
- "Accept": "application/json",
383
- "Origin": originReferer(credential),
384
- "Referer": `${originReferer(credential)}/`,
385
- "User-Agent": CLIENT_UA
386
- },
406
+ const global = regionOf(credential.domain) === "global";
407
+ const url = global ? `${globalBase(credential)}${GLOBAL_CONFIG_PATH}` : `${chatBase(credential)}${MODELS_CATALOG_PATH}`;
408
+ const headers = global ? {
409
+ "Authorization": `Bearer ${credential.accessToken}`,
410
+ "Accept": "application/json",
411
+ ...credential.uid === void 0 || credential.uid === "" ? {} : { "X-User-Id": credential.uid },
412
+ ...credential.domain === "" ? {} : { "X-Domain": credential.domain },
413
+ "X-Product": "SaaS",
414
+ "X-Requested-With": "XMLHttpRequest",
415
+ "Connection": "close",
416
+ "User-Agent": DESKTOP_UA
417
+ } : {
418
+ "Authorization": `Bearer ${credential.accessToken}`,
419
+ "Accept": "application/json",
420
+ "Origin": originReferer(credential),
421
+ "Referer": `${originReferer(credential)}/`,
422
+ "User-Agent": CLIENT_UA
423
+ };
424
+ if (!global && credential.enterpriseId !== void 0 && credential.enterpriseId !== "") headers["X-Enterprise-Id"] = credential.enterpriseId;
425
+ const response = await this.fetchImpl(url, {
426
+ headers,
387
427
  ...signal === void 0 ? {} : { signal }
388
428
  });
389
429
  const envelope = await readEnvelope(response);
@@ -553,7 +593,6 @@ var WorkBuddyUpstreamClient = class {
553
593
  /** Live auth file name the WorkBuddy desktop app writes. */
554
594
  const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
555
595
  /** Snapshot files left behind by previous logins share this prefix. */
556
- const SNAPSHOT_PREFIX = "workbuddy-desktop.";
557
596
  /** Env override for the auth file or its directory. */
558
597
  const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
559
598
  function nonEmptyEnv(value) {
@@ -678,6 +717,17 @@ function accountLabel(credential) {
678
717
  return discriminator === "" ? name : `${name}#${discriminator}`;
679
718
  }
680
719
  /** List the auth files in one directory: the live file plus every snapshot. */
720
+ /**
721
+ * Credential files in one auth directory, freshest first.
722
+ *
723
+ * Every `*.info` file counts, not just the timestamped `workbuddy-desktop.*`
724
+ * snapshots: the international client signs in as `workbuddy-desktop-ai.info`
725
+ * (a hyphen, not a dot), so a prefix test silently dropped every global
726
+ * credential and the global provider then saw an empty pool.
727
+ *
728
+ * Filenames are plain strings, and the ordering here is only a first pass —
729
+ * `isFresher` makes the real call once each file has been parsed.
730
+ */
681
731
  async function authFilesIn(dir) {
682
732
  let entries;
683
733
  try {
@@ -685,10 +735,7 @@ async function authFilesIn(dir) {
685
735
  } catch {
686
736
  return [];
687
737
  }
688
- const files = entries.filter((name) => {
689
- if (name === "workbuddy-desktop.info") return true;
690
- return name.startsWith(SNAPSHOT_PREFIX) && name.endsWith(".info");
691
- });
738
+ const files = entries.filter((name) => name.endsWith(".info"));
692
739
  files.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
693
740
  return files.map((name) => join(dir, name));
694
741
  }
@@ -1038,7 +1085,7 @@ var WorkBuddyCatalog = class {
1038
1085
  const budgets = this.selection.contextBudgets;
1039
1086
  return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
1040
1087
  const next = { ...model };
1041
- if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
1088
+ if (imageSet !== void 0) next.supportsImages = next.supportsImages || imageSet.has(model.id);
1042
1089
  const budget = budgets?.[model.id];
1043
1090
  if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
1044
1091
  return next;
@@ -1086,7 +1133,7 @@ function toModelInfo(model) {
1086
1133
  name: model.name,
1087
1134
  contextWindow: model.contextWindow,
1088
1135
  maxOutputTokens: model.maxTokens,
1089
- supportsImages: model.multimodal ?? false,
1136
+ supportsImages: model.supportsImages ?? false,
1090
1137
  ...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
1091
1138
  ...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
1092
1139
  };
@@ -1116,6 +1163,14 @@ z.object({
1116
1163
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Per-model context-window override, keyed by model id")
1117
1164
  });
1118
1165
  /** Assemble the runtime objects without registering anything. */
1166
+ /**
1167
+ * Assemble the runtime objects without registering anything.
1168
+ *
1169
+ * One catalog per region, mirroring the two shims: the CN and global gateways
1170
+ * do not advertise the same roster, and a shared catalog meant the picker showed
1171
+ * whichever list happened to be fetched first (always the CN one, since the
1172
+ * seeding step read `accounts[0]`).
1173
+ */
1119
1174
  function createCore(logger) {
1120
1175
  const client = new WorkBuddyUpstreamClient();
1121
1176
  return {
@@ -1123,7 +1178,10 @@ function createCore(logger) {
1123
1178
  ...logger === void 0 ? {} : { logger },
1124
1179
  client
1125
1180
  }),
1126
- catalog: new WorkBuddyCatalog(),
1181
+ catalogs: {
1182
+ cn: new WorkBuddyCatalog(),
1183
+ global: new WorkBuddyCatalog()
1184
+ },
1127
1185
  client
1128
1186
  };
1129
1187
  }
@@ -1212,7 +1270,7 @@ async function commandStatus(args) {
1212
1270
  };
1213
1271
  }),
1214
1272
  cooling: accounts.filter((account) => account.cooldownUntilMs > Date.now()).length,
1215
- models: core.catalog.current().map((model) => ({
1273
+ models: core.catalogs.cn.current().map((model) => ({
1216
1274
  id: model.id,
1217
1275
  name: model.name,
1218
1276
  multiplier: model.multiplier
package/lib/index.d.ts CHANGED
@@ -20,7 +20,12 @@ interface WorkBuddyUpstreamModel {
20
20
  contextWindow: number;
21
21
  maxTokens: number;
22
22
  creditMultiplier?: number;
23
- multimodal?: boolean;
23
+ /** Upstream image-input flag. Both gateways spell it `supportsImages`; some entries
24
+ * also carry `disabledMultimodal`, the negative spelling. Reading anything else
25
+ * reported every model as text-only, which made a vision shim register a second
26
+ * route under the same display name and split the model picker group.
27
+ */
28
+ supportsImages?: boolean;
24
29
  reasoning?: {
25
30
  supportedEfforts?: readonly string[];
26
31
  defaultEffort?: string;
@@ -112,7 +117,21 @@ export declare class WorkBuddyUpstreamClient {
112
117
  chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
113
118
  /** POST the token-refresh endpoint; the caller merges the outcome. */
114
119
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
115
- /** GET the personal model catalog, keeping the `cli` agent's models only. */
120
+ /**
121
+ * Fetch the model catalog, keeping the `cli` agent's models only.
122
+ *
123
+ * The two gateways are read differently, because they answer differently:
124
+ *
125
+ * - **CN** serves the roster at `/v2/enterprises/personal/models` and expects
126
+ * the CLI client spelling.
127
+ * - **Global** serves it as part of the product config at `/v3/config`, and
128
+ * only to the DESKTOP client channel. Asking the global host with the CLI UA
129
+ * yields a truncated roster, and the CN path answers HTTP 500 there — which
130
+ * is what left the international provider on its static fallback.
131
+ *
132
+ * Both documents share the `{ models, agents }` entry shape, so the parsing
133
+ * below is common to the two branches.
134
+ */
116
135
  fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
117
136
  /** Read-only credits query, aggregated by package. Does not consume credits. */
118
137
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
@@ -142,6 +161,7 @@ interface TokenRefresher {
142
161
  }
143
162
  /** Live auth file name the WorkBuddy desktop app writes. */
144
163
  export declare const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
164
+ /** Snapshot files left behind by previous logins share this prefix. */
145
165
  /** Env override for the auth file or its directory. */
146
166
  export declare const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
147
167
  /** One parsed WorkBuddy credential. */
@@ -488,6 +508,7 @@ interface WorkBuddyStatus {
488
508
  }
489
509
  interface StatusOptions {
490
510
  pool: WorkBuddyAccountPool;
511
+ /** The catalog to report. Regional callers pass their own region's. */
491
512
  catalog: WorkBuddyCatalog;
492
513
  client: WorkBuddyUpstreamClient;
493
514
  shim?: {
@@ -737,7 +758,8 @@ export declare const Config: z<Config>;
737
758
  /** Everything the CLI needs from a live plugin instance. */
738
759
  export interface WorkBuddyPoolApi {
739
760
  pool: WorkBuddyAccountPool;
740
- catalog: WorkBuddyCatalog;
761
+ /** One catalog per region, matching the two registered providers. */
762
+ catalogs: Readonly<Record<WorkBuddyRegion, WorkBuddyCatalog>>;
741
763
  client: WorkBuddyUpstreamClient;
742
764
  shim: WorkBuddyShim;
743
765
  adapter: WorkBuddyAdapter | undefined;
@@ -750,11 +772,22 @@ export declare function currentApi(): WorkBuddyPoolApi | undefined;
750
772
  /** Test seam: install an API instance without booting cordis. */
751
773
  export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
752
774
  /** Assemble the runtime objects without registering anything. */
775
+ /**
776
+ * Assemble the runtime objects without registering anything.
777
+ *
778
+ * One catalog per region, mirroring the two shims: the CN and global gateways
779
+ * do not advertise the same roster, and a shared catalog meant the picker showed
780
+ * whichever list happened to be fetched first (always the CN one, since the
781
+ * seeding step read `accounts[0]`).
782
+ */
753
783
  export declare function createCore(logger?: {
754
784
  warn(...args: unknown[]): void;
755
785
  }): {
756
786
  pool: WorkBuddyAccountPool;
757
- catalog: WorkBuddyCatalog;
787
+ catalogs: {
788
+ readonly cn: WorkBuddyCatalog;
789
+ readonly global: WorkBuddyCatalog;
790
+ };
758
791
  client: WorkBuddyUpstreamClient;
759
792
  };
760
793
  /**
package/lib/index.js CHANGED
@@ -17,7 +17,18 @@ const CN_BILLING_BASE = "https://www.codebuddy.cn";
17
17
  /** Global base for `workbuddy.ai` logins. */
18
18
  const GLOBAL_BASE = "https://www.workbuddy.ai";
19
19
  /** Client UA the desktop CLI uses. */
20
+ /** Client UA the desktop CLI uses — the CN gateway answers this one. */
20
21
  const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
22
+ /**
23
+ * Desktop app UA. The global gateway serves its product config only to this
24
+ * client channel: the CLI UA gets a truncated roster (or an HTTP 500), which
25
+ * is why the international catalog must be read with the desktop spelling.
26
+ */
27
+ const DESKTOP_UA = "WorkBuddy/5.5.2";
28
+ /** CN model catalog. */
29
+ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
30
+ /** Global product config, which carries the international model roster. */
31
+ const GLOBAL_CONFIG_PATH = "/v3/config";
21
32
  const JSON_TIMEOUT_MS = 3e4;
22
33
  const ERROR_BODY_LIMIT = 4096;
23
34
  /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
@@ -277,6 +288,7 @@ function parseUpstreamModel(value) {
277
288
  const creditMultiplier = parseCreditMultiplier(raw["credits"]);
278
289
  const reasoning = parseReasoning(raw["reasoning"]);
279
290
  const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
291
+ const supportsImages = typeof raw["supportsImages"] === "boolean" ? raw["supportsImages"] : void 0;
280
292
  return {
281
293
  id,
282
294
  name,
@@ -286,7 +298,8 @@ function parseUpstreamModel(value) {
286
298
  ...reasoning === void 0 ? {} : { reasoning },
287
299
  ...descriptionZh === void 0 ? {} : { descriptionZh },
288
300
  ...descriptionEn === void 0 ? {} : { descriptionEn },
289
- ...supportsToolCall === void 0 ? {} : { supportsToolCall }
301
+ ...supportsToolCall === void 0 ? {} : { supportsToolCall },
302
+ ...supportsImages === void 0 ? {} : { supportsImages }
290
303
  };
291
304
  }
292
305
  var WorkBuddyUpstreamClient = class {
@@ -390,16 +403,43 @@ var WorkBuddyUpstreamClient = class {
390
403
  if (typeof data["domain"] === "string" && data["domain"] !== "") outcome.domain = data["domain"];
391
404
  return outcome;
392
405
  }
393
- /** GET the personal model catalog, keeping the `cli` agent's models only. */
406
+ /**
407
+ * Fetch the model catalog, keeping the `cli` agent's models only.
408
+ *
409
+ * The two gateways are read differently, because they answer differently:
410
+ *
411
+ * - **CN** serves the roster at `/v2/enterprises/personal/models` and expects
412
+ * the CLI client spelling.
413
+ * - **Global** serves it as part of the product config at `/v3/config`, and
414
+ * only to the DESKTOP client channel. Asking the global host with the CLI UA
415
+ * yields a truncated roster, and the CN path answers HTTP 500 there — which
416
+ * is what left the international provider on its static fallback.
417
+ *
418
+ * Both documents share the `{ models, agents }` entry shape, so the parsing
419
+ * below is common to the two branches.
420
+ */
394
421
  async fetchModels(credential, signal) {
395
- const response = await this.fetchImpl(`${chatBase(credential)}/console/enterprises/personal/models`, {
396
- headers: {
397
- "Authorization": `Bearer ${credential.accessToken}`,
398
- "Accept": "application/json",
399
- "Origin": originReferer(credential),
400
- "Referer": `${originReferer(credential)}/`,
401
- "User-Agent": CLIENT_UA
402
- },
422
+ const global = regionOf(credential.domain) === "global";
423
+ const url = global ? `${globalBase(credential)}${GLOBAL_CONFIG_PATH}` : `${chatBase(credential)}${MODELS_CATALOG_PATH}`;
424
+ const headers = global ? {
425
+ "Authorization": `Bearer ${credential.accessToken}`,
426
+ "Accept": "application/json",
427
+ ...credential.uid === void 0 || credential.uid === "" ? {} : { "X-User-Id": credential.uid },
428
+ ...credential.domain === "" ? {} : { "X-Domain": credential.domain },
429
+ "X-Product": "SaaS",
430
+ "X-Requested-With": "XMLHttpRequest",
431
+ "Connection": "close",
432
+ "User-Agent": DESKTOP_UA
433
+ } : {
434
+ "Authorization": `Bearer ${credential.accessToken}`,
435
+ "Accept": "application/json",
436
+ "Origin": originReferer(credential),
437
+ "Referer": `${originReferer(credential)}/`,
438
+ "User-Agent": CLIENT_UA
439
+ };
440
+ if (!global && credential.enterpriseId !== void 0 && credential.enterpriseId !== "") headers["X-Enterprise-Id"] = credential.enterpriseId;
441
+ const response = await this.fetchImpl(url, {
442
+ headers,
403
443
  ...signal === void 0 ? {} : { signal }
404
444
  });
405
445
  const envelope = await readEnvelope(response);
@@ -569,7 +609,6 @@ var WorkBuddyUpstreamClient = class {
569
609
  /** Live auth file name the WorkBuddy desktop app writes. */
570
610
  const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
571
611
  /** Snapshot files left behind by previous logins share this prefix. */
572
- const SNAPSHOT_PREFIX = "workbuddy-desktop.";
573
612
  /** Env override for the auth file or its directory. */
574
613
  const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
575
614
  function nonEmptyEnv(value) {
@@ -694,6 +733,17 @@ function accountLabel(credential) {
694
733
  return discriminator === "" ? name : `${name}#${discriminator}`;
695
734
  }
696
735
  /** List the auth files in one directory: the live file plus every snapshot. */
736
+ /**
737
+ * Credential files in one auth directory, freshest first.
738
+ *
739
+ * Every `*.info` file counts, not just the timestamped `workbuddy-desktop.*`
740
+ * snapshots: the international client signs in as `workbuddy-desktop-ai.info`
741
+ * (a hyphen, not a dot), so a prefix test silently dropped every global
742
+ * credential and the global provider then saw an empty pool.
743
+ *
744
+ * Filenames are plain strings, and the ordering here is only a first pass —
745
+ * `isFresher` makes the real call once each file has been parsed.
746
+ */
697
747
  async function authFilesIn(dir) {
698
748
  let entries;
699
749
  try {
@@ -701,10 +751,7 @@ async function authFilesIn(dir) {
701
751
  } catch {
702
752
  return [];
703
753
  }
704
- const files = entries.filter((name) => {
705
- if (name === "workbuddy-desktop.info") return true;
706
- return name.startsWith(SNAPSHOT_PREFIX) && name.endsWith(".info");
707
- });
754
+ const files = entries.filter((name) => name.endsWith(".info"));
708
755
  files.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
709
756
  return files.map((name) => join(dir, name));
710
757
  }
@@ -1054,7 +1101,7 @@ var WorkBuddyCatalog = class {
1054
1101
  const budgets = this.selection.contextBudgets;
1055
1102
  return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
1056
1103
  const next = { ...model };
1057
- if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
1104
+ if (imageSet !== void 0) next.supportsImages = next.supportsImages || imageSet.has(model.id);
1058
1105
  const budget = budgets?.[model.id];
1059
1106
  if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
1060
1107
  return next;
@@ -1102,7 +1149,7 @@ function toModelInfo(model) {
1102
1149
  name: model.name,
1103
1150
  contextWindow: model.contextWindow,
1104
1151
  maxOutputTokens: model.maxTokens,
1105
- supportsImages: model.multimodal ?? false,
1152
+ supportsImages: model.supportsImages ?? false,
1106
1153
  ...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
1107
1154
  ...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
1108
1155
  };
@@ -1825,7 +1872,7 @@ function toWebModel(model, selection) {
1825
1872
  async function poolWebStatus(deps, region = "cn") {
1826
1873
  const accounts = deps.pool.list(region);
1827
1874
  const regions = ["cn", "global"];
1828
- const selection = deps.catalog.currentSelection();
1875
+ const selection = deps.catalogs[region].currentSelection();
1829
1876
  const rows = [];
1830
1877
  const now = Date.now();
1831
1878
  for (const account of accounts) {
@@ -1875,7 +1922,7 @@ async function poolWebStatus(deps, region = "cn") {
1875
1922
  accounts: rows,
1876
1923
  ...firstUsable === void 0 ? {} : { activeAccountId: firstUsable.id },
1877
1924
  cooling,
1878
- models: deps.catalog.current().map((model) => toWebModel({
1925
+ models: deps.catalogs[region].current().map((model) => toWebModel({
1879
1926
  id: model.id,
1880
1927
  name: model.name,
1881
1928
  contextWindow: model.contextWindow,
@@ -2048,6 +2095,14 @@ function setApi(next) {
2048
2095
  api = next;
2049
2096
  }
2050
2097
  /** Assemble the runtime objects without registering anything. */
2098
+ /**
2099
+ * Assemble the runtime objects without registering anything.
2100
+ *
2101
+ * One catalog per region, mirroring the two shims: the CN and global gateways
2102
+ * do not advertise the same roster, and a shared catalog meant the picker showed
2103
+ * whichever list happened to be fetched first (always the CN one, since the
2104
+ * seeding step read `accounts[0]`).
2105
+ */
2051
2106
  function createCore(logger) {
2052
2107
  const client = new WorkBuddyUpstreamClient();
2053
2108
  return {
@@ -2055,7 +2110,10 @@ function createCore(logger) {
2055
2110
  ...logger === void 0 ? {} : { logger },
2056
2111
  client
2057
2112
  }),
2058
- catalog: new WorkBuddyCatalog(),
2113
+ catalogs: {
2114
+ cn: new WorkBuddyCatalog(),
2115
+ global: new WorkBuddyCatalog()
2116
+ },
2059
2117
  client
2060
2118
  };
2061
2119
  }
@@ -2095,12 +2153,13 @@ function apply(ctx, config = {}) {
2095
2153
  ...cooldownMs === void 0 ? {} : { cooldownMs },
2096
2154
  distribution: distribution ?? "priority"
2097
2155
  });
2098
- core.catalog.applySelection({
2156
+ const selection = {
2099
2157
  ...enabledModelIds === void 0 ? {} : { enabledModelIds },
2100
2158
  ...imageModelIds === void 0 ? {} : { imageModelIds },
2101
2159
  ...contextBudgets === void 0 ? {} : { contextBudgets }
2102
- });
2103
- invalidateCatalog();
2160
+ };
2161
+ core.catalogs.cn.applySelection(selection);
2162
+ core.catalogs.global.applySelection(selection);
2104
2163
  };
2105
2164
  const settingsService = ctx.settings;
2106
2165
  if (typeof settingsService.installSection === "function") settingsService.installSection(ctx, WORKBUDDY_POOL_SETTINGS_NS, Config, config, sectionHooks);
@@ -2124,14 +2183,14 @@ function apply(ctx, config = {}) {
2124
2183
  cn: createWorkBuddyShim({
2125
2184
  pool: core.pool,
2126
2185
  client: core.client,
2127
- catalog: core.catalog,
2186
+ catalog: core.catalogs.cn,
2128
2187
  logger: ctx.logger,
2129
2188
  region: "cn"
2130
2189
  }),
2131
2190
  global: createWorkBuddyShim({
2132
2191
  pool: core.pool,
2133
2192
  client: core.client,
2134
- catalog: core.catalog,
2193
+ catalog: core.catalogs.global,
2135
2194
  logger: ctx.logger,
2136
2195
  region: "global"
2137
2196
  })
@@ -2156,7 +2215,7 @@ function apply(ctx, config = {}) {
2156
2215
  });
2157
2216
  ctx.inject(["webServer"], (webCtx) => registerPoolStatusRoute(webCtx, {
2158
2217
  pool: core.pool,
2159
- catalog: core.catalog,
2218
+ catalogs: core.catalogs,
2160
2219
  client: core.client,
2161
2220
  shim: () => shimInfo("cn"),
2162
2221
  saveSelection: (selection) => {
@@ -2179,7 +2238,7 @@ function apply(ctx, config = {}) {
2179
2238
  async status(includeCredits = false) {
2180
2239
  return buildStatus({
2181
2240
  pool: core.pool,
2182
- catalog: core.catalog,
2241
+ catalog: core.catalogs.cn,
2183
2242
  client: core.client,
2184
2243
  shim: shimInfo("cn"),
2185
2244
  includeCredits
@@ -2195,13 +2254,13 @@ function apply(ctx, config = {}) {
2195
2254
  const adaptersByRegion = {
2196
2255
  cn: createWorkBuddyAdapter({
2197
2256
  shim: shims.cn,
2198
- catalog: core.catalog,
2257
+ catalog: core.catalogs.cn,
2199
2258
  providerId: POOL_PROVIDER_BY_REGION.cn,
2200
2259
  displayName: POOL_NAME_BY_REGION.cn
2201
2260
  }),
2202
2261
  global: createWorkBuddyAdapter({
2203
2262
  shim: shims.global,
2204
- catalog: core.catalog,
2263
+ catalog: core.catalogs.global,
2205
2264
  providerId: POOL_PROVIDER_BY_REGION.global,
2206
2265
  displayName: POOL_NAME_BY_REGION.global
2207
2266
  })
@@ -2250,7 +2309,8 @@ function apply(ctx, config = {}) {
2250
2309
  }
2251
2310
  ctx.llm.registerModelDiscovery(WORKBUDDY_POOL_SETTINGS_NS, async (request) => {
2252
2311
  if (request.provider !== "workbuddy-xdpool" && request.provider !== "workbuddy-xdpool-global") return [];
2253
- return core.catalog.visible().map((model) => ({
2312
+ const region = request.provider === "workbuddy-xdpool-global" ? "global" : "cn";
2313
+ return core.catalogs[region].visible().map((model) => ({
2254
2314
  id: model.id,
2255
2315
  name: model.name,
2256
2316
  contextWindow: model.contextWindow,
@@ -2270,16 +2330,20 @@ function apply(ctx, config = {}) {
2270
2330
  ctx.logger.warn("dsh-workbuddy-xdpool: account discovery failed", error);
2271
2331
  });
2272
2332
  (async () => {
2273
- try {
2274
- const credential = (await core.pool.scan())[0]?.credential;
2275
- if (credential === void 0) return;
2333
+ const accounts = await core.pool.scan();
2334
+ for (const region of ["cn", "global"]) try {
2335
+ const credential = accounts.find((account) => regionOf(account.credential.domain) === region)?.credential;
2336
+ if (credential === void 0) {
2337
+ ctx.logger.info?.(`dsh-workbuddy-xdpool: no ${region} account yet; keeping the static ${region} catalog`);
2338
+ continue;
2339
+ }
2276
2340
  const models = await core.client.fetchModels(credential);
2277
- core.catalog.updateFromUpstream(models);
2278
- invalidateCatalog();
2279
- ctx.logger.info?.(`dsh-workbuddy-xdpool: live catalog seeded with ${models.length} model(s)`);
2341
+ core.catalogs[region].updateFromUpstream(models);
2342
+ ctx.logger.info?.(`dsh-workbuddy-xdpool: ${region} catalog seeded with ${models.length} model(s)`);
2280
2343
  } catch (error) {
2281
- ctx.logger.warn("dsh-workbuddy-xdpool: live model catalog unavailable; using static fallback", error);
2344
+ ctx.logger.warn(`dsh-workbuddy-xdpool: ${region} model catalog unavailable; using static fallback`, error);
2282
2345
  }
2346
+ invalidateCatalog();
2283
2347
  })();
2284
2348
  }, (error) => {
2285
2349
  ctx.logger.error("dsh-workbuddy-xdpool: shim failed to listen", error);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-xdpool",
3
3
  "displayName": "DSH WorkBuddy XD Pool",
4
4
  "description": "Merge every locally signed-in WorkBuddy account into DeepSeek Harness as one auto-failing-over model pool (multi-account rotation, live credits, daily check-in and model catalog).",
5
- "version": "0.4.0",
5
+ "version": "0.4.1",
6
6
  "license": "MIT",
7
7
  "author": "aosi526",
8
8
  "repository": {