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 +33 -0
- package/lib/bin.js +77 -19
- package/lib/index.d.ts +37 -4
- package/lib/index.js +101 -37
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2156
|
+
const selection = {
|
|
2099
2157
|
...enabledModelIds === void 0 ? {} : { enabledModelIds },
|
|
2100
2158
|
...imageModelIds === void 0 ? {} : { imageModelIds },
|
|
2101
2159
|
...contextBudgets === void 0 ? {} : { contextBudgets }
|
|
2102
|
-
}
|
|
2103
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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.
|
|
2278
|
-
|
|
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(
|
|
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.
|
|
5
|
+
"version": "0.4.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "aosi526",
|
|
8
8
|
"repository": {
|