dsh-connect-workbuddy 1.3.0 → 1.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.
- package/CHANGELOG.md +40 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/bin.js +1 -1
- package/lib/client.js +15 -6
- package/lib/{host-heartbeat-UlZjd09_.js → host-heartbeat-DQQwiUtm.js} +254 -24
- package/lib/index.d.ts +82 -15
- package/lib/index.js +80 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2026-09-11)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **🆕 正式支持国际版 WorkBuddy AI(www.workbuddy.ai)**——国内版与国际版账号在本插件中获得完全对等的支持,**零配置、零开关,全自动**:
|
|
8
|
+
|
|
9
|
+
- **怎么用**:在插件卡片的账号列表里选中国际版账号(凭据文件 `workbuddy-desktop-ai.info`,Gmail 等邮箱登录的那个)即等于切到国际版;选回手机号账号即回到国内版。区域判定完全跟随凭据文件自带的 `domain` 字段(国际版为 `www.workbuddy.ai`,国内版为 `www.codebuddy.cn` / `www.workbuddy.cn`),无需任何手动设置。
|
|
10
|
+
- **国际账号可用的完整功能**(全部经真实国际账号端到端实测):
|
|
11
|
+
- **模型接入**:**20 个模型**进入 DSH 模型选择器,含国际版独有阵容——GPT-6-Astra、GPT-5.6-Sol / Terra / Luna、GPT-5.5 / 5.4 / 5.3-Codex、Gemini-3.5-Flash、免费的 `deepseek-v4.1-flash` / Hy3 / Hy4 preview 等;与国内版共有的 GLM-5.3 / 5.2、Kimi-K3 / K2.6 也正常列出;
|
|
12
|
+
- **积分倍率**:20/20 模型全部带倍率展示(GPT-6-Astra x6.67、GPT-5.6-Sol x3.47、Auto x0.79、`deepseek-v4.1-flash` / Hy3 x0.00 免费等),与 WorkBuddy AI 官方客户端展示的是同一个 `credits` 字段(已在其 App bundle 中确认渲染数据源一致);
|
|
13
|
+
- **积分概览**:按套餐展示国际账号的剩余积分与到期时间(一次性 Bonus Pack 与月度 Free Plan Subscription 均正确区分),查询走国际网关 `https://www.workbuddy.ai`;
|
|
14
|
+
- **每日签到**:签到状态与领取端点在国际网关实测可用;活动未开启时按钮自动禁用(409 守卫),开启后即可直接领取;
|
|
15
|
+
- **对话路由**:chat 请求自动走国际网关,wire 协议与国内版同构(业务码 11128 等语义一致,`prepareChatBody` 的改写逻辑两侧通用)。
|
|
16
|
+
- **核心修复:按渠道读取模型目录**。原先国际账号读 `/console/enterprises/personal/models`(国内路径,国际网关返回 HTTP 500)。改用两区域各自的正确来源后,国际账号首次拿到**完整 20 个模型**:
|
|
17
|
+
- **国内**:`copilot.tencent.com/v2/enterprises/personal/models`(保持原样,29 个模型中取 `cli` agent 的 15 个);
|
|
18
|
+
- **国际**:`www.workbuddy.ai/v3/config` —— 关键在于**必须用桌面端 User-Agent**:配置服务按客户端渠道返回不同产品配置,`CLI/… CodeBuddy/…` 只给 35 个模型并**缺失 `deepseek-v4.1-flash`、`gpt-6-astra`**(尽管两者都能正常对话),而桌面渠道给出账号真实的 20 个 chat 模型。版本号无关(`WorkBuddy/5.5.2`、`WorkBuddy/1.0.0`、裸 `WorkBuddy` 结果一致),是 `WorkBuddy` 这个产品标识选择渠道。国内网关不需要这个切换:其桌面配置的 `cli` agent 为 0 个模型。
|
|
19
|
+
- 国际账号此前**只剩 `hy3`、`hy4-preview`** 两个可用模型(`deepseek-v4.1-flash` 被静默丢弃、`gpt-6-astra` 不可见),现在 20 个全部就位,与 WorkBuddy AI 客户端的模型菜单一致。
|
|
20
|
+
|
|
21
|
+
### Fixes
|
|
22
|
+
|
|
23
|
+
- **目录与勾选按区域隔离(国内 / 国际各一套)**:此前 `lastCatalog` 与 `enabledModelIds` 是全局单槽。国内账号下勾选的模型(含 `deepseek-v4.1-flash`、`hy3-x`)在切到国际账号后,会与**国际目录**求交集——凡是国际目录里没有的 id 都被静默丢弃:国际账号实际只剩 `hy3`、`hy4-preview` 两个模型可用(`src/index.ts` 启动时 `deriveCatalog(国际目录, 国内勾选)`)。
|
|
24
|
+
- 现改为 `regions.cn` / `regions.global` 两套独立槽位(目录、勾选、图片开关、上下文预算各一份),互不干扰;切换账号不再覆盖另一区域的配置。
|
|
25
|
+
- 新增按区域区分的静态 fallback 目录:国际账号在首次拉取前不再被塞入国内模型清单(`FALLBACK_WORKBUDDY_MODELS_GLOBAL`,2026-09-11 从国际网关桌面渠道配置实测捕获 20 个模型及其倍率,含免费的 `deepseek-v4.1-flash`)。
|
|
26
|
+
- 旧的扁平字段保留为**国内区域的迁移来源**(旧配置一律来自国内端点),国际区域绝不继承——这正是丢勾选的根源。
|
|
27
|
+
- 卡片保存改为写入当前账号所属区域的槽位;usage 文档新增 `region` 字段供卡片定位。
|
|
28
|
+
- 三个探测脚本(`probe-models` / `probe-credits` / `probe-account-switch`)同步改用 `/v2/...` 路径:此前它们对国际账号同样会 500,导致诊断输出误导。
|
|
29
|
+
- 新增只读评估脚本 `scripts/probe-global-eval.mjs`:对比国际网关 `/v2/...`(500)与 `/v3/config`(200)两条取目录路径,并验证积分与签到端点——本轮的渠道发现即由它得出。
|
|
30
|
+
|
|
31
|
+
### Tests
|
|
32
|
+
|
|
33
|
+
- 新增 `fetchModels` 回归测试:stub 上游后断言 CN 凭据(`codebuddy.cn` / `workbuddy.cn`)拼出 `copilot.tencent.com/v2/enterprises/personal/models`、国际凭据(`workbuddy.ai`)拼出 `www.workbuddy.ai/v3/config`,并断言国际请求携带**桌面端 User-Agent**(`WorkBuddy/5.5.2`,CLI UA 会拿到缺模型的 35 个)、CN 请求**不得**携带桌面端 UA(国内桌面配置的 `cli` agent 为空)。把「按渠道取目录」钉死,防止日后退回会丢模型的取法。
|
|
34
|
+
- `regionOf` 测试补上 `www.workbuddy.cn`(国内版新 domain 形态,实测本机存在)。
|
|
35
|
+
- 新增区域隔离测试:`regionStateOf` 断言旧扁平字段只被读作国内状态、国际区域绝不继承、显式槽位优先;`fallbackModelsFor` 断言国内清单不得泄漏进国际(国内无 `gpt-*`/`gemini-*`,且 `default-model` 仅属国际);web-status 断言文档携带的 `region` 与凭据域一致、且四个区域化访问器收到的都是该区域。
|
|
36
|
+
- **每日签到(领取)路由的守卫逻辑首次获得直接 handler 测试**(实测两区域端点行为后补齐):
|
|
37
|
+
- 活动未开启(国际账号实测形态,`active:false`)→ 409 且**绝不调用**上游领取;
|
|
38
|
+
- 今日已签到 → 200 `alreadyCheckedIn` 且不调用上游领取(幂等保护);
|
|
39
|
+
- 活动开启且未签到 → 恰好领取一次、领取后重读状态并返回刷新后的连签天数;
|
|
40
|
+
- 非回环 Origin → 403、非 POST → 405,两道安全守卫均在触碰上游前拒绝。
|
|
41
|
+
- 实测依据(全部经插件真实代码路径、零状态变更验证):CN 网关领取端点对已签到账号返回业务拒绝「今天已签到,请明天再来」;国际网关同路径存在,对未开启活动返回「签到活动未开启或已过期」(均为 HTTP 400 业务码而非 404,证明端点存在、鉴权与协议一致)。国际版 App bundle 中确认签到端点与插件同路径,官方 App 对签到走无前缀形态、`get-user-resource` 才需 `/v2` 前缀——插件统一 `/v2` 形态在两侧网关均实测可用。
|
|
42
|
+
|
|
3
43
|
## 1.3.0 (2026-09-10)
|
|
4
44
|
|
|
5
45
|
### Breaking Changes
|
package/README.en.md
CHANGED
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
[English](README.en.md) | 中文
|
|
19
19
|
|
|
20
|
-
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy
|
|
20
|
+
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with a read-only credits overview and selectable model management.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
|
-
- **WorkBuddy model provider** — registers locally signed-in WorkBuddy models as the `workbuddy` provider
|
|
24
|
+
- **WorkBuddy model provider (CN + international)** — registers locally signed-in WorkBuddy models as the `workbuddy` provider: CN accounts expose `GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc., while international accounts expose `GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.; model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu. **Region routing is fully automatic — no toggle anywhere**: whichever account you select is the region you use.
|
|
25
25
|
- **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; image input is opted in per model manually (off by default).
|
|
26
26
|
- **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch between them. Tokens are never written to DSH settings.
|
|
27
27
|
- **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
|
|
@@ -40,7 +40,7 @@ DSH PiAiAdapter
|
|
|
40
40
|
-> DSH executes local tools and returns their results
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The model catalog comes from `/
|
|
43
|
+
The model catalog comes from each region's correct source: CN reads `/v2/enterprises/personal/models`, while international reads `www.workbuddy.ai/v3/config` — the config service serves a different roster per client channel, and only the desktop user agent yields the account's real 20-model list (including the free `deepseek-v4.1-flash`). The credits overview comes from `https://www.codebuddy.cn/v2/billing/meter/get-user-resource` (international accounts auto-route to `https://www.workbuddy.ai`); all are read-only. No manual region toggle: routing follows the `domain` field inside the credential file (`workbuddy.ai` → international, anything else → CN), and **each region keeps its own directory and selection**, so switching accounts never overwrites the other region's picks.
|
|
44
44
|
|
|
45
45
|
Credentials are read (read-only) from the WorkBuddy desktop app's own auth file. Refreshed tokens are kept separately in `$DSH_HOME/.workbuddy-auth.json`; the desktop app's file is never written.
|
|
46
46
|
|
package/README.md
CHANGED
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
<a href="https://dshfind.com/plugins/dingminhua/dsh-connect-workbuddy"><img src="https://dshfind.com/api/badge/dingminhua/dsh-connect-workbuddy" alt="dshfind plugin"></a>
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy
|
|
26
|
+
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供**只读**的积分概览与可勾选的模型管理界面。
|
|
27
27
|
|
|
28
28
|
## 功能特性
|
|
29
29
|
|
|
30
|
-
- **WorkBuddy
|
|
30
|
+
- **WorkBuddy 模型接入(国内版 + 国际版)** —— 把本机登录的 WorkBuddy 模型注册为 DSH 的 `workbuddy` provider:国内账号出现 `GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等,国际账号出现 `GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等;模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致。**区域自动路由,无需任何开关**——选中哪个账号就走哪个区域。
|
|
31
31
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;图片输入按模型手动勾选(默认不勾选)。
|
|
32
32
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可切换账号;Token 不写入 DSH 设置。
|
|
33
33
|
- **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
|
|
@@ -46,7 +46,7 @@ DSH PiAiAdapter
|
|
|
46
46
|
-> DSH 本地执行工具并回传结果
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
模型目录按区域取自各自的正确来源:国内版走 `/v2/enterprises/personal/models`,国际版走 `www.workbuddy.ai/v3/config`(配置服务按客户端渠道返回不同清单,国际版必须用桌面端 User-Agent 才能取到账号真实的 20 个模型,含免费的 `deepseek-v4.1-flash`)。积分概览走 `https://www.codebuddy.cn/v2/billing/meter/get-user-resource`(国际版账号自动路由到 `https://www.workbuddy.ai`),均为只读接口。区域无需手动切换:插件按凭据文件里的 `domain` 字段自动路由(`workbuddy.ai` → 国际版,其余 → 国内版),且**两个区域各自保存一份目录与勾选**,切换账号互不覆盖。
|
|
50
50
|
|
|
51
51
|
凭据读取自 WorkBuddy 桌面 App 自身的 auth 文件(只读);刷新得到的 token 单独存放在 `$DSH_HOME/.workbuddy-auth.json`,桌面端文件永不被写入。
|
|
52
52
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as workbuddyOwnAuthPath, C as WorkBuddyCredentialStore, E as defaultDesktopAuthDirs, S as WORKBUDDY_AUTH_FILE_ENV, T as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, l as WorkBuddyUpstreamClient, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive } from "./host-heartbeat-DQQwiUtm.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
package/lib/client.js
CHANGED
|
@@ -391,8 +391,10 @@ window.__ModuleLoader__.load({
|
|
|
391
391
|
const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
|
|
392
392
|
const activeImageIds = draftImageIds ?? savedImageIds;
|
|
393
393
|
const configured = settingsScope?.getSnapshot().value;
|
|
394
|
-
const
|
|
395
|
-
const
|
|
394
|
+
const configuredRegion = status.status === "signed-in" ? configured?.regions?.[status.region] : void 0;
|
|
395
|
+
const legacyContextBudgets = status.status === "signed-in" && status.region === "cn" ? configured?.contextBudgets : void 0;
|
|
396
|
+
const savedContextBudgetsSource = configuredRegion?.contextBudgets ?? legacyContextBudgets;
|
|
397
|
+
const activeContextBudgets = draftContextBudgets ?? (typeof savedContextBudgetsSource === "object" && savedContextBudgetsSource !== null ? savedContextBudgetsSource : {});
|
|
396
398
|
const dirty = draftModels !== void 0 || draftEnabledIds !== void 0 || draftImageIds !== void 0 || draftContextBudgets !== void 0;
|
|
397
399
|
const toggleModel = (modelId) => {
|
|
398
400
|
const next = new Set(activeEnabledIds);
|
|
@@ -421,13 +423,20 @@ window.__ModuleLoader__.load({
|
|
|
421
423
|
};
|
|
422
424
|
const saveModels = async () => {
|
|
423
425
|
if (settingsScope === void 0) return;
|
|
426
|
+
if (status.status !== "signed-in") return;
|
|
424
427
|
setSaving(true);
|
|
425
428
|
setSaveError(void 0);
|
|
426
429
|
try {
|
|
427
|
-
|
|
428
|
-
await settingsScope.set("
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
const configuredRegions = configured?.regions;
|
|
431
|
+
await settingsScope.set("regions", {
|
|
432
|
+
...typeof configuredRegions === "object" && configuredRegions !== null ? configuredRegions : {},
|
|
433
|
+
[status.region]: {
|
|
434
|
+
lastCatalog: visibleModels.map(toPersistedWorkBuddyModel),
|
|
435
|
+
enabledModelIds: [...activeEnabledIds],
|
|
436
|
+
imageModelIds: [...activeImageIds],
|
|
437
|
+
contextBudgets: activeContextBudgets
|
|
438
|
+
}
|
|
439
|
+
});
|
|
431
440
|
discardModels();
|
|
432
441
|
await refreshUsage();
|
|
433
442
|
} catch (error) {
|
|
@@ -518,6 +518,164 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
518
518
|
maxTokens: 5e4
|
|
519
519
|
}
|
|
520
520
|
];
|
|
521
|
+
/**
|
|
522
|
+
* Static CLI models captured from the INTERNATIONAL gateway's desktop-channel
|
|
523
|
+
* product config (`www.workbuddy.ai/v3/config`, 2026-09-11). The two regions
|
|
524
|
+
* expose different rosters — the CN list has no `gpt-*`/`gemini-*` entries —
|
|
525
|
+
* so a global account must never be seeded with the CN list. Like the CN
|
|
526
|
+
* fallback this is replaced by the live refresh; it only keeps the provider
|
|
527
|
+
* usable before the first fetch lands. Order and rates mirror the upstream.
|
|
528
|
+
*/
|
|
529
|
+
const FALLBACK_WORKBUDDY_MODELS_GLOBAL = [
|
|
530
|
+
{
|
|
531
|
+
id: "default-model",
|
|
532
|
+
name: "Auto",
|
|
533
|
+
contextWindow: 176e3,
|
|
534
|
+
maxTokens: 24e3,
|
|
535
|
+
creditMultiplier: .79
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: "fast-model",
|
|
539
|
+
name: "Fast",
|
|
540
|
+
contextWindow: 2e5,
|
|
541
|
+
maxTokens: 32e3,
|
|
542
|
+
creditMultiplier: .34
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "balanced-model",
|
|
546
|
+
name: "Balanced",
|
|
547
|
+
contextWindow: 256e3,
|
|
548
|
+
maxTokens: 32e3,
|
|
549
|
+
creditMultiplier: .59
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "primary-model",
|
|
553
|
+
name: "Primary",
|
|
554
|
+
contextWindow: 272e3,
|
|
555
|
+
maxTokens: 72e3,
|
|
556
|
+
creditMultiplier: 3.31
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: "deep-model",
|
|
560
|
+
name: "Deep",
|
|
561
|
+
contextWindow: 176e3,
|
|
562
|
+
maxTokens: 24e3,
|
|
563
|
+
creditMultiplier: 3.33
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: "deepseek-v4.1-flash",
|
|
567
|
+
name: "Deepseek-V4.1-Flash",
|
|
568
|
+
contextWindow: 1e6,
|
|
569
|
+
maxTokens: 128e3,
|
|
570
|
+
creditMultiplier: 0
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: "gpt-6-astra",
|
|
574
|
+
name: "GPT-6-Astra",
|
|
575
|
+
contextWindow: 1e6,
|
|
576
|
+
maxTokens: 128e3,
|
|
577
|
+
creditMultiplier: 6.67
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: "hy4-preview",
|
|
581
|
+
name: "Hy4 preview",
|
|
582
|
+
contextWindow: 1e6,
|
|
583
|
+
maxTokens: 64e3,
|
|
584
|
+
creditMultiplier: 0
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: "hy3",
|
|
588
|
+
name: "Hy3",
|
|
589
|
+
contextWindow: 192e3,
|
|
590
|
+
maxTokens: 64e3,
|
|
591
|
+
creditMultiplier: 0
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "gpt-5.6-sol",
|
|
595
|
+
name: "GPT-5.6-Sol",
|
|
596
|
+
contextWindow: 1e6,
|
|
597
|
+
maxTokens: 128e3,
|
|
598
|
+
creditMultiplier: 3.47
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
id: "gpt-5.6-terra",
|
|
602
|
+
name: "GPT-5.6-Terra",
|
|
603
|
+
contextWindow: 1e6,
|
|
604
|
+
maxTokens: 128e3,
|
|
605
|
+
creditMultiplier: 1.39
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "gpt-5.6-luna",
|
|
609
|
+
name: "GPT-5.6-Luna",
|
|
610
|
+
contextWindow: 1e6,
|
|
611
|
+
maxTokens: 128e3,
|
|
612
|
+
creditMultiplier: .14
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "gpt-5.5",
|
|
616
|
+
name: "GPT-5.5",
|
|
617
|
+
contextWindow: 1e6,
|
|
618
|
+
maxTokens: 128e3,
|
|
619
|
+
creditMultiplier: 3.31
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "gpt-5.4",
|
|
623
|
+
name: "GPT-5.4",
|
|
624
|
+
contextWindow: 272e3,
|
|
625
|
+
maxTokens: 72e3,
|
|
626
|
+
creditMultiplier: 1.65
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: "gpt-5.3-codex",
|
|
630
|
+
name: "GPT-5.3-Codex",
|
|
631
|
+
contextWindow: 272e3,
|
|
632
|
+
maxTokens: 72e3,
|
|
633
|
+
creditMultiplier: 1.25
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: "gemini-3.5-flash",
|
|
637
|
+
name: "Gemini-3.5-Flash",
|
|
638
|
+
contextWindow: 1e6,
|
|
639
|
+
maxTokens: 65536,
|
|
640
|
+
creditMultiplier: .99
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: "glm-5.3",
|
|
644
|
+
name: "GLM-5.3",
|
|
645
|
+
contextWindow: 1e6,
|
|
646
|
+
maxTokens: 48e3,
|
|
647
|
+
creditMultiplier: .79
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
id: "glm-5.2",
|
|
651
|
+
name: "GLM-5.2",
|
|
652
|
+
contextWindow: 1e6,
|
|
653
|
+
maxTokens: 48e3,
|
|
654
|
+
creditMultiplier: .79
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
id: "kimi-k3",
|
|
658
|
+
name: "Kimi-K3",
|
|
659
|
+
contextWindow: 1e6,
|
|
660
|
+
maxTokens: 32e3,
|
|
661
|
+
creditMultiplier: 1.62
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: "kimi-k2.6",
|
|
665
|
+
name: "Kimi-K2.6",
|
|
666
|
+
contextWindow: 256e3,
|
|
667
|
+
maxTokens: 32e3,
|
|
668
|
+
creditMultiplier: .52
|
|
669
|
+
}
|
|
670
|
+
];
|
|
671
|
+
/**
|
|
672
|
+
* Static fallback directory for a region. Each region keeps its own model
|
|
673
|
+
* slot in settings; the fallback must match the region so an account never
|
|
674
|
+
* shows the other region's roster.
|
|
675
|
+
*/
|
|
676
|
+
function fallbackModelsFor(region) {
|
|
677
|
+
return region === "global" ? FALLBACK_WORKBUDDY_MODELS_GLOBAL : FALLBACK_WORKBUDDY_MODELS;
|
|
678
|
+
}
|
|
521
679
|
/** Apply the saved local DSH budget; models above 200K default to 200K. */
|
|
522
680
|
function applyContextBudgets(catalog, budgets = {}) {
|
|
523
681
|
return catalog.map((model) => ({
|
|
@@ -546,7 +704,41 @@ var WorkBuddyCatalog = class {
|
|
|
546
704
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
547
705
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
548
706
|
const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
707
|
+
/**
|
|
708
|
+
* Model-catalog path used by the CN region (and the global fallback). The CN
|
|
709
|
+
* gateway answers it with the same bytes as its legacy
|
|
710
|
+
* `/console/enterprises/personal/models` alias. See {@link GLOBAL_CONFIG_PATH}
|
|
711
|
+
* for why the international region reads a different document.
|
|
712
|
+
*/
|
|
713
|
+
const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
|
|
714
|
+
/**
|
|
715
|
+
* Remote product-config path on the global gateway. This is the document the
|
|
716
|
+
* desktop channel receives; it is the only source that lists the account's
|
|
717
|
+
* full international chat roster (see {@link DESKTOP_UA}).
|
|
718
|
+
*/
|
|
719
|
+
const GLOBAL_CONFIG_PATH = "/v3/config";
|
|
549
720
|
const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
|
|
721
|
+
/**
|
|
722
|
+
* User agent of the WorkBuddy desktop app.
|
|
723
|
+
*
|
|
724
|
+
* The config service serves a DIFFERENT product configuration per client
|
|
725
|
+
* channel, selected by this product token — the version suffix is ignored
|
|
726
|
+
* (`WorkBuddy/5.5.2`, `WorkBuddy/1.0.0` and a bare `WorkBuddy` answer
|
|
727
|
+
* identically). On the INTERNATIONAL gateway the split decides which models
|
|
728
|
+
* exist at all:
|
|
729
|
+
*
|
|
730
|
+
* - CLI channel (`CLI/… CodeBuddy/…`) → 35 models that OMIT
|
|
731
|
+
* `deepseek-v4.1-flash` and `gpt-6-astra`, even though both are perfectly
|
|
732
|
+
* chat-usable (verified: `deepseek-v4.1-flash` streams HTTP 200 and is
|
|
733
|
+
* billed `x0.00`);
|
|
734
|
+
* - desktop channel → the account's real 20-model chat roster including both.
|
|
735
|
+
*
|
|
736
|
+
* The plugin emulates the CLI channel for CHAT but reads the desktop channel's
|
|
737
|
+
* configuration to learn the account's actual model list. The CN gateway needs
|
|
738
|
+
* no such switch: its desktop config carries no `cli` agent roster at all, so
|
|
739
|
+
* CN keeps reading the shared `/v2/enterprises/personal/models` path.
|
|
740
|
+
*/
|
|
741
|
+
const DESKTOP_UA = "WorkBuddy/5.5.2";
|
|
550
742
|
const JSON_TIMEOUT_MS = 3e4;
|
|
551
743
|
const ERROR_BODY_LIMIT = 4096;
|
|
552
744
|
/** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
|
|
@@ -775,6 +967,34 @@ function parseUpstreamModel(value) {
|
|
|
775
967
|
};
|
|
776
968
|
}
|
|
777
969
|
/**
|
|
970
|
+
* Select the chat-capable models from a catalog-shaped document: parse every
|
|
971
|
+
* entry, then keep the `cli` agent's roster in its declared order.
|
|
972
|
+
*
|
|
973
|
+
* Both the CN personal-models document and the global `/v3/config` document
|
|
974
|
+
* carry `models` plus an `agents` roster with the same entry shape, so one
|
|
975
|
+
* selector serves them. Without a usable `cli` roster the whole parsed catalog
|
|
976
|
+
* is exposed rather than nothing: the roster is an upstream detail that may
|
|
977
|
+
* change, and an empty answer would silently disarm the provider.
|
|
978
|
+
*/
|
|
979
|
+
function selectCliModels(rawModels, agents) {
|
|
980
|
+
const byId = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const model of Array.isArray(rawModels) ? rawModels : []) {
|
|
982
|
+
const parsed = parseUpstreamModel(model);
|
|
983
|
+
if (parsed !== void 0) byId.set(parsed.id, parsed);
|
|
984
|
+
}
|
|
985
|
+
let cliIds;
|
|
986
|
+
for (const agent of Array.isArray(agents) ? agents : []) if (typeof agent === "object" && agent !== null) {
|
|
987
|
+
const wrapped = agent;
|
|
988
|
+
if (wrapped["name"] === "cli" && Array.isArray(wrapped["models"])) {
|
|
989
|
+
cliIds = wrapped["models"].filter((id) => typeof id === "string");
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const models = (cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()]).map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
994
|
+
if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
|
|
995
|
+
return models;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
778
998
|
* Upstream HTTP client. One instance serves the whole plugin; requests take
|
|
779
999
|
* the credential explicitly so token refreshes apply on the next call.
|
|
780
1000
|
*/
|
|
@@ -831,11 +1051,38 @@ var WorkBuddyUpstreamClient = class {
|
|
|
831
1051
|
return outcome;
|
|
832
1052
|
}
|
|
833
1053
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
1054
|
+
* Read the model directory for the credential's region.
|
|
1055
|
+
*
|
|
1056
|
+
* The two regions expose their chat roster through different documents:
|
|
1057
|
+
* CN answers `/v2/enterprises/personal/models`, while the global gateway's
|
|
1058
|
+
* personal-models path returns HTTP 500 and the CLI channel's `/v3/config`
|
|
1059
|
+
* omits chat-usable models — so global reads `/v3/config` as the desktop
|
|
1060
|
+
* channel (see {@link DESKTOP_UA}). Both documents share the entry shape, so
|
|
1061
|
+
* one parser serves them. No user-side toggle is involved: the region comes
|
|
1062
|
+
* from the credential's `domain`.
|
|
836
1063
|
*/
|
|
837
1064
|
async fetchModels(credential, signal) {
|
|
838
|
-
const
|
|
1065
|
+
const timeout = signal ?? AbortSignal.timeout(JSON_TIMEOUT_MS);
|
|
1066
|
+
if (regionOf(credential.domain) === "global") {
|
|
1067
|
+
const response = await fetch(`${GLOBAL_BASE}${GLOBAL_CONFIG_PATH}`, {
|
|
1068
|
+
headers: {
|
|
1069
|
+
"Authorization": `Bearer ${credential.accessToken}`,
|
|
1070
|
+
"Accept": "application/json",
|
|
1071
|
+
...credential.uid === "" ? {} : { "X-User-Id": credential.uid },
|
|
1072
|
+
...credential.domain === "" ? {} : { "X-Domain": credential.domain },
|
|
1073
|
+
"X-Product": "SaaS",
|
|
1074
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
1075
|
+
"Connection": "close",
|
|
1076
|
+
"User-Agent": DESKTOP_UA
|
|
1077
|
+
},
|
|
1078
|
+
signal: timeout
|
|
1079
|
+
});
|
|
1080
|
+
const envelope = await readEnvelope(response);
|
|
1081
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
1082
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1083
|
+
return selectCliModels(data["models"], data["agents"]);
|
|
1084
|
+
}
|
|
1085
|
+
const response = await fetch(`${chatBase(credential)}${MODELS_CATALOG_PATH}`, {
|
|
839
1086
|
headers: {
|
|
840
1087
|
"Authorization": `Bearer ${credential.accessToken}`,
|
|
841
1088
|
"Accept": "application/json",
|
|
@@ -843,29 +1090,12 @@ var WorkBuddyUpstreamClient = class {
|
|
|
843
1090
|
"Referer": `${originReferer(credential)}/`,
|
|
844
1091
|
"User-Agent": CLIENT_UA
|
|
845
1092
|
},
|
|
846
|
-
signal:
|
|
1093
|
+
signal: timeout
|
|
847
1094
|
});
|
|
848
1095
|
const envelope = await readEnvelope(response);
|
|
849
1096
|
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
850
1097
|
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
851
|
-
|
|
852
|
-
const agents = Array.isArray(data["agents"]) ? data["agents"] : [];
|
|
853
|
-
let cliIds;
|
|
854
|
-
for (const agent of agents) if (typeof agent === "object" && agent !== null) {
|
|
855
|
-
const wrapped = agent;
|
|
856
|
-
if (wrapped["name"] === "cli" && Array.isArray(wrapped["models"])) {
|
|
857
|
-
cliIds = wrapped["models"].filter((id) => typeof id === "string");
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
const byId = /* @__PURE__ */ new Map();
|
|
862
|
-
for (const model of rawModels) {
|
|
863
|
-
const parsed = parseUpstreamModel(model);
|
|
864
|
-
if (parsed !== void 0) byId.set(parsed.id, parsed);
|
|
865
|
-
}
|
|
866
|
-
const models = (cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()]).map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
867
|
-
if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
|
|
868
|
-
return models;
|
|
1098
|
+
return selectCliModels(data["models"], data["agents"]);
|
|
869
1099
|
}
|
|
870
1100
|
/** Query today's check-in status without changing account state. */
|
|
871
1101
|
async fetchCheckinStatus(credential) {
|
|
@@ -996,7 +1226,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
996
1226
|
//#endregion
|
|
997
1227
|
//#region src/version.ts
|
|
998
1228
|
/** The npm package version this build was produced from. */
|
|
999
|
-
const WORKBUDDY_CONNECT_VERSION = "1.
|
|
1229
|
+
const WORKBUDDY_CONNECT_VERSION = "1.4.0";
|
|
1000
1230
|
//#endregion
|
|
1001
1231
|
//#region src/host-heartbeat.ts
|
|
1002
1232
|
/**
|
|
@@ -1102,4 +1332,4 @@ async function clearHostHeartbeat() {
|
|
|
1102
1332
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
1103
1333
|
}
|
|
1104
1334
|
//#endregion
|
|
1105
|
-
export {
|
|
1335
|
+
export { workbuddyOwnAuthPath as A, WorkBuddyCredentialStore as C, defaultDesktopAuthPath as D, defaultDesktopAuthDirs as E, parseWorkBuddyAuth as O, WORKBUDDY_AUTH_FILE_ENV as S, defaultDesktopAuthCandidates as T, FALLBACK_WORKBUDDY_MODELS_GLOBAL as _, readHostHeartbeat as a, fallbackModelsFor as b, WORKBUDDY_CONNECT_VERSION as c, parseCreditMultiplier as d, parseReasoning as f, FALLBACK_WORKBUDDY_MODELS as g, regionOf as h, processStartTimeMs as i, workbuddyAccountId as k, WorkBuddyUpstreamClient as l, prepareChatBody as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, parseUpstreamModel as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, classifyUpstreamError as u, WorkBuddyCatalog as v, authFileName as w, WORKBUDDY_AUTH_FILENAME as x, deriveCatalog as y };
|
package/lib/index.d.ts
CHANGED
|
@@ -294,8 +294,15 @@ declare class WorkBuddyUpstreamClient {
|
|
|
294
294
|
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
295
295
|
refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
|
|
296
296
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
297
|
+
* Read the model directory for the credential's region.
|
|
298
|
+
*
|
|
299
|
+
* The two regions expose their chat roster through different documents:
|
|
300
|
+
* CN answers `/v2/enterprises/personal/models`, while the global gateway's
|
|
301
|
+
* personal-models path returns HTTP 500 and the CLI channel's `/v3/config`
|
|
302
|
+
* omits chat-usable models — so global reads `/v3/config` as the desktop
|
|
303
|
+
* channel (see {@link DESKTOP_UA}). Both documents share the entry shape, so
|
|
304
|
+
* one parser serves them. No user-side toggle is involved: the region comes
|
|
305
|
+
* from the credential's `domain`.
|
|
299
306
|
*/
|
|
300
307
|
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
301
308
|
/** Query today's check-in status without changing account state. */
|
|
@@ -320,6 +327,21 @@ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
|
|
|
320
327
|
* with a usable catalog even while the first fetch is in flight or offline.
|
|
321
328
|
*/
|
|
322
329
|
declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
|
|
330
|
+
/**
|
|
331
|
+
* Static CLI models captured from the INTERNATIONAL gateway's desktop-channel
|
|
332
|
+
* product config (`www.workbuddy.ai/v3/config`, 2026-09-11). The two regions
|
|
333
|
+
* expose different rosters — the CN list has no `gpt-*`/`gemini-*` entries —
|
|
334
|
+
* so a global account must never be seeded with the CN list. Like the CN
|
|
335
|
+
* fallback this is replaced by the live refresh; it only keeps the provider
|
|
336
|
+
* usable before the first fetch lands. Order and rates mirror the upstream.
|
|
337
|
+
*/
|
|
338
|
+
declare const FALLBACK_WORKBUDDY_MODELS_GLOBAL: readonly WorkBuddyModelInfo[];
|
|
339
|
+
/**
|
|
340
|
+
* Static fallback directory for a region. Each region keeps its own model
|
|
341
|
+
* slot in settings; the fallback must match the region so an account never
|
|
342
|
+
* shows the other region's roster.
|
|
343
|
+
*/
|
|
344
|
+
declare function fallbackModelsFor(region: 'cn' | 'global'): readonly WorkBuddyModelInfo[];
|
|
323
345
|
/**
|
|
324
346
|
* Derive the runtime catalog from the last-refreshed directory plus the
|
|
325
347
|
* user's selection. This is the single source of truth for what DSH exposes,
|
|
@@ -579,6 +601,12 @@ interface WorkBuddyWebAccount {
|
|
|
579
601
|
selected: boolean;
|
|
580
602
|
}
|
|
581
603
|
type WorkBuddyWebPackage = WorkBuddyWebCreditPackage;
|
|
604
|
+
/**
|
|
605
|
+
* Region of the signed-in credential: the CN app (`codebuddy.cn` /
|
|
606
|
+
* `workbuddy.cn`) or the international WorkBuddy AI app (`workbuddy.ai`).
|
|
607
|
+
* The card uses this to read and write the matching per-region model slot.
|
|
608
|
+
*/
|
|
609
|
+
type WorkBuddyWebRegion = 'cn' | 'global';
|
|
582
610
|
/** The JSON document the plugin card renders. */
|
|
583
611
|
type WorkBuddyWebUsage = {
|
|
584
612
|
status: 'signed-out';
|
|
@@ -590,6 +618,8 @@ type WorkBuddyWebUsage = {
|
|
|
590
618
|
accountName: string;
|
|
591
619
|
uin?: string;
|
|
592
620
|
domain?: string;
|
|
621
|
+
/** Which per-region model directory and selection this account owns. */
|
|
622
|
+
region: WorkBuddyWebRegion;
|
|
593
623
|
source?: 'desktop' | 'dsh';
|
|
594
624
|
tokenExpiresAtMs: number;
|
|
595
625
|
accounts: readonly WorkBuddyWebAccount[];
|
|
@@ -610,14 +640,19 @@ type WorkBuddyWebUsage = {
|
|
|
610
640
|
interface WorkBuddyStatusRouteOptions {
|
|
611
641
|
store: WorkBuddyCredentialStore;
|
|
612
642
|
client: Pick<WorkBuddyUpstreamClient, 'fetchCredits' | 'fetchCheckinStatus' | 'claimDailyCheckin'>;
|
|
613
|
-
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
643
|
+
/**
|
|
644
|
+
* The selected region's last-refreshed model directory (unfiltered) for card
|
|
645
|
+
* display. Region-scoped because the CN and international apps expose
|
|
646
|
+
* different rosters; showing one region's directory on the other account is
|
|
647
|
+
* the bug this parameter exists to prevent.
|
|
648
|
+
*/
|
|
649
|
+
displayModels(region: WorkBuddyRegion): readonly WorkBuddyModelInfo[];
|
|
650
|
+
/** The selected region's selection, stored as model ids. */
|
|
651
|
+
enabledModelIds(region: WorkBuddyRegion): readonly string[];
|
|
652
|
+
/** Model ids the user opted into image input, for the selected region. */
|
|
653
|
+
imageModelIds(region: WorkBuddyRegion): readonly string[];
|
|
654
|
+
/** Saved local DSH context budgets by model id, for the selected region. */
|
|
655
|
+
contextBudgets(region: WorkBuddyRegion): Readonly<Record<string, number | undefined>>;
|
|
621
656
|
/** Re-read the live catalog from the upstream. */
|
|
622
657
|
discoverModels?(signal?: AbortSignal): Promise<readonly WorkBuddyModelInfo[]>;
|
|
623
658
|
}
|
|
@@ -641,22 +676,54 @@ declare const name = "dsh-connect-workbuddy";
|
|
|
641
676
|
declare const inject: string[];
|
|
642
677
|
/** Settings namespace for the plugin configuration card. */
|
|
643
678
|
declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
|
|
679
|
+
/** One region's model directory and the user's selection within it. */
|
|
680
|
+
interface WorkBuddyRegionState {
|
|
681
|
+
/** The last-refreshed directory for this region; what the card displays. */
|
|
682
|
+
lastCatalog?: WorkBuddyModelInfo[];
|
|
683
|
+
/** The user's selection in this region, as model ids. */
|
|
684
|
+
enabledModelIds?: string[];
|
|
685
|
+
/** Model ids the user explicitly opted into image input. */
|
|
686
|
+
imageModelIds?: string[];
|
|
687
|
+
/** Local DSH context budget per model in this region. */
|
|
688
|
+
contextBudgets?: Record<string, WorkBuddyContextBudget>;
|
|
689
|
+
}
|
|
644
690
|
/** Plugin configuration. */
|
|
645
691
|
interface Config {
|
|
646
692
|
/** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
|
|
647
693
|
authFile?: string;
|
|
648
694
|
/** Stable local account selector; tokens remain outside settings. */
|
|
649
695
|
accountId?: string;
|
|
650
|
-
/**
|
|
696
|
+
/**
|
|
697
|
+
* Per-region model state, keyed `cn` | `global`. The CN app and the
|
|
698
|
+
* international WorkBuddy AI app expose different rosters, so each keeps its
|
|
699
|
+
* own directory and selection and switching accounts never drops the other
|
|
700
|
+
* region's picks.
|
|
701
|
+
*/
|
|
702
|
+
regions?: Partial<Record<WorkBuddyRegion, WorkBuddyRegionState>>;
|
|
703
|
+
/**
|
|
704
|
+
* @deprecated Legacy single-slot fields from before the region split. They
|
|
705
|
+
* predate international support and are read as the CN region's state when
|
|
706
|
+
* `regions.cn` is absent; new writes go to `regions`.
|
|
707
|
+
*/
|
|
651
708
|
lastCatalog?: WorkBuddyModelInfo[];
|
|
652
|
-
/**
|
|
709
|
+
/** @deprecated See {@link Config.lastCatalog}. */
|
|
653
710
|
enabledModelIds?: string[];
|
|
654
|
-
/**
|
|
711
|
+
/** @deprecated See {@link Config.lastCatalog}. */
|
|
655
712
|
imageModelIds?: string[];
|
|
656
|
-
/**
|
|
713
|
+
/** @deprecated See {@link Config.lastCatalog}. */
|
|
657
714
|
contextBudgets?: Record<string, WorkBuddyContextBudget>;
|
|
658
715
|
}
|
|
659
716
|
declare const Config: z<Config>;
|
|
717
|
+
/**
|
|
718
|
+
* One region's saved model state. A config written before the region split has
|
|
719
|
+
* only the flat fields: those were always captured from the CN endpoint (the
|
|
720
|
+
* plugin had no international support), so they are read as the CN state and
|
|
721
|
+
* only when no explicit CN slot exists. The global region never inherits them —
|
|
722
|
+
* that inheritance is exactly the bug where a stale CN directory was
|
|
723
|
+
* intersected with the international catalog and silently dropped the user's
|
|
724
|
+
* picks.
|
|
725
|
+
*/
|
|
726
|
+
declare function regionStateOf(config: Config, region: WorkBuddyRegion): WorkBuddyRegionState;
|
|
660
727
|
/**
|
|
661
728
|
* Start the loopback endpoint, register the `workbuddy` provider, and
|
|
662
729
|
* refresh the model catalog from the upstream once credentials allow it.
|
|
@@ -665,4 +732,4 @@ declare const Config: z<Config>;
|
|
|
665
732
|
*/
|
|
666
733
|
declare function apply(ctx: Context, config: Config): void;
|
|
667
734
|
//#endregion
|
|
668
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyReasoning, type WorkBuddyRefreshOutcome, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
735
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyReasoning, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionStateOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as workbuddyOwnAuthPath, C as WorkBuddyCredentialStore, D as defaultDesktopAuthPath, E as defaultDesktopAuthDirs, O as parseWorkBuddyAuth, S as WORKBUDDY_AUTH_FILE_ENV, T as defaultDesktopAuthCandidates, _ as FALLBACK_WORKBUDDY_MODELS_GLOBAL, a as readHostHeartbeat, b as fallbackModelsFor, c as WORKBUDDY_CONNECT_VERSION, d as parseCreditMultiplier, f as parseReasoning, g as FALLBACK_WORKBUDDY_MODELS, h as regionOf, i as processStartTimeMs, k as workbuddyAccountId, l as WorkBuddyUpstreamClient, m as prepareChatBody, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as parseUpstreamModel, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as WorkBuddyCatalog, w as authFileName, x as WORKBUDDY_AUTH_FILENAME, y as deriveCatalog } from "./host-heartbeat-DQQwiUtm.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -530,17 +530,20 @@ async function workBuddyWebStatus(deps) {
|
|
|
530
530
|
message: safeMessage(error)
|
|
531
531
|
};
|
|
532
532
|
}
|
|
533
|
+
const selected = accounts.find((account) => account.selected);
|
|
534
|
+
const region = regionOf(credential.domain);
|
|
533
535
|
const account = {
|
|
534
|
-
accountId:
|
|
536
|
+
accountId: selected?.id ?? "",
|
|
535
537
|
accountName: credential.nickname ?? credential.uin ?? credential.uid,
|
|
536
538
|
...credential.uin === void 0 ? {} : { uin: credential.uin },
|
|
537
539
|
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
540
|
+
region,
|
|
538
541
|
source: credential.source,
|
|
539
542
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
540
543
|
accounts: accounts.map(toWebAccount),
|
|
541
|
-
models: deps.displayModels().map((model) => toWebModel(model, deps.contextBudgets())),
|
|
542
|
-
enabledModelIds: [...deps.enabledModelIds()],
|
|
543
|
-
imageModelIds: [...deps.imageModelIds()]
|
|
544
|
+
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
545
|
+
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
546
|
+
imageModelIds: [...deps.imageModelIds(region)]
|
|
544
547
|
};
|
|
545
548
|
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
546
549
|
return {
|
|
@@ -621,7 +624,8 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
621
624
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
622
625
|
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
623
626
|
try {
|
|
624
|
-
|
|
627
|
+
const region = regionOf((await deps.store.resolve()).domain);
|
|
628
|
+
json(res, 200, { models: (await deps.discoverModels()).map((model) => toWebModel(model, deps.contextBudgets(region))) });
|
|
625
629
|
} catch (error) {
|
|
626
630
|
json(res, 500, { error: safeMessage(error) });
|
|
627
631
|
}
|
|
@@ -649,15 +653,42 @@ const modelConfig = z.object({
|
|
|
649
653
|
contextWindow: z.number().step(1).min(1),
|
|
650
654
|
maxTokens: z.number().step(1).min(1)
|
|
651
655
|
});
|
|
656
|
+
const regionStateConfig = z.object({
|
|
657
|
+
lastCatalog: z.array(modelConfig).default([]),
|
|
658
|
+
enabledModelIds: z.array(z.string()).default([]),
|
|
659
|
+
imageModelIds: z.array(z.string()).default([]),
|
|
660
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({})
|
|
661
|
+
});
|
|
652
662
|
const Config = z.object({
|
|
653
663
|
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)"),
|
|
654
664
|
accountId: z.string().description("Selected local WorkBuddy account id (never a token)"),
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
665
|
+
regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | global"),
|
|
666
|
+
lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
|
|
667
|
+
enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
|
|
668
|
+
imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
|
|
669
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets")
|
|
659
670
|
});
|
|
660
671
|
/**
|
|
672
|
+
* One region's saved model state. A config written before the region split has
|
|
673
|
+
* only the flat fields: those were always captured from the CN endpoint (the
|
|
674
|
+
* plugin had no international support), so they are read as the CN state and
|
|
675
|
+
* only when no explicit CN slot exists. The global region never inherits them —
|
|
676
|
+
* that inheritance is exactly the bug where a stale CN directory was
|
|
677
|
+
* intersected with the international catalog and silently dropped the user's
|
|
678
|
+
* picks.
|
|
679
|
+
*/
|
|
680
|
+
function regionStateOf(config, region) {
|
|
681
|
+
const stored = config.regions?.[region];
|
|
682
|
+
if (stored !== void 0) return stored;
|
|
683
|
+
if (region !== "cn") return {};
|
|
684
|
+
return {
|
|
685
|
+
...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
|
|
686
|
+
...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
|
|
687
|
+
...config.imageModelIds === void 0 ? {} : { imageModelIds: config.imageModelIds },
|
|
688
|
+
...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
661
692
|
* Start the loopback endpoint, register the `workbuddy` provider, and
|
|
662
693
|
* refresh the model catalog from the upstream once credentials allow it.
|
|
663
694
|
* The static fallback catalog serves from the first moment, so an offline
|
|
@@ -677,27 +708,45 @@ function apply(ctx, config) {
|
|
|
677
708
|
catalog,
|
|
678
709
|
logger: ctx.logger
|
|
679
710
|
});
|
|
680
|
-
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
681
|
-
const imageSet = (value) => new Set(value.imageModelIds ?? []);
|
|
682
711
|
const withImageSelection = (models, images) => models.map((model) => ({
|
|
683
712
|
...model,
|
|
684
713
|
...images.has(model.id) ? { multimodal: true } : { multimodal: false }
|
|
685
714
|
}));
|
|
686
|
-
const configuredModels = (value
|
|
687
|
-
|
|
715
|
+
const configuredModels = (value, region) => {
|
|
716
|
+
const state = regionStateOf(value, region);
|
|
717
|
+
return withImageSelection(deriveCatalog(state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region), new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
|
|
718
|
+
};
|
|
719
|
+
const displayModels = (value, region) => {
|
|
720
|
+
const state = regionStateOf(value, region);
|
|
721
|
+
return state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region);
|
|
722
|
+
};
|
|
688
723
|
let current = () => config;
|
|
689
724
|
let invalidateCatalog = () => {};
|
|
725
|
+
/**
|
|
726
|
+
* Region of the selected account, tracked so a settings change can recompute
|
|
727
|
+
* the runtime catalog without re-resolving the credential synchronously.
|
|
728
|
+
* Every path that reads the credential (startup seed, discovery, card route)
|
|
729
|
+
* refreshes it, so it converges on the real region.
|
|
730
|
+
*/
|
|
731
|
+
let currentRegion = "cn";
|
|
732
|
+
const regionOfCredential = async () => {
|
|
733
|
+
try {
|
|
734
|
+
currentRegion = regionOf((await store.resolve()).domain);
|
|
735
|
+
} catch {}
|
|
736
|
+
return currentRegion;
|
|
737
|
+
};
|
|
690
738
|
const discoverModels = async (signal) => {
|
|
691
739
|
const credential = await store.resolve();
|
|
740
|
+
currentRegion = regionOf(credential.domain);
|
|
692
741
|
return client.fetchModels(credential, signal);
|
|
693
742
|
};
|
|
694
743
|
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
695
744
|
store,
|
|
696
745
|
client,
|
|
697
|
-
displayModels: () => displayModels(current()),
|
|
698
|
-
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
699
|
-
imageModelIds: () => current().imageModelIds ?? [],
|
|
700
|
-
contextBudgets: () => current().contextBudgets ?? {},
|
|
746
|
+
displayModels: (region) => displayModels(current(), region),
|
|
747
|
+
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
748
|
+
imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
|
|
749
|
+
contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
|
|
701
750
|
discoverModels
|
|
702
751
|
}));
|
|
703
752
|
ctx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
|
|
@@ -708,8 +757,12 @@ function apply(ctx, config) {
|
|
|
708
757
|
const next = current();
|
|
709
758
|
store.setDesktopPath(next.authFile);
|
|
710
759
|
store.selectAccount(next.accountId);
|
|
711
|
-
catalog.set(configuredModels(next));
|
|
760
|
+
catalog.set(configuredModels(next, currentRegion));
|
|
712
761
|
invalidateCatalog();
|
|
762
|
+
regionOfCredential().then((region) => {
|
|
763
|
+
catalog.set(configuredModels(current(), region));
|
|
764
|
+
invalidateCatalog();
|
|
765
|
+
});
|
|
713
766
|
}
|
|
714
767
|
});
|
|
715
768
|
let stopped = false;
|
|
@@ -760,7 +813,10 @@ function apply(ctx, config) {
|
|
|
760
813
|
}
|
|
761
814
|
ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
|
|
762
815
|
if (request.provider !== "workbuddy") return [];
|
|
763
|
-
|
|
816
|
+
const discovered = await discoverModels(signal);
|
|
817
|
+
const region = currentRegion;
|
|
818
|
+
const state = regionStateOf(current(), region);
|
|
819
|
+
return withImageSelection(deriveCatalog(discovered, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
|
|
764
820
|
id: model.id,
|
|
765
821
|
name: workBuddyDisplayName(model),
|
|
766
822
|
contextWindow: model.contextWindow,
|
|
@@ -778,9 +834,11 @@ function apply(ctx, config) {
|
|
|
778
834
|
try {
|
|
779
835
|
const credential = await store.resolve();
|
|
780
836
|
if (stopped) return;
|
|
837
|
+
currentRegion = regionOf(credential.domain);
|
|
781
838
|
const models = await client.fetchModels(credential);
|
|
782
839
|
if (stopped) return;
|
|
783
|
-
|
|
840
|
+
const state = regionStateOf(current(), currentRegion);
|
|
841
|
+
catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
|
|
784
842
|
invalidate?.();
|
|
785
843
|
} catch (error) {
|
|
786
844
|
ctx.logger.warn("dsh-connect-workbuddy: dynamic model catalog unavailable; serving the static fallback list", error);
|
|
@@ -791,4 +849,4 @@ function apply(ctx, config) {
|
|
|
791
849
|
});
|
|
792
850
|
}
|
|
793
851
|
//#endregion
|
|
794
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
852
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionStateOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.4.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|