dsh-connect-workbuddy 1.4.0 → 2.0.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 CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0 (2026-09-15)
4
+
5
+ ### Breaking Changes
6
+
7
+ - **为什么是 2.0.0**:本版是架构级变更——插件由「单 provider 单活区域」变为「双 provider 并行」,运行形态与外部可见契约均有变化,按语义化版本升主版本号:
8
+ - **运行形态**:插件现在注册两个 provider 路由(`workbuddy` + `workbuddy-global`)并打开**两个**回环端口(原先各一个)。下游脚本若假定「只存在一个 workbuddy provider」,需适配新增的 `workbuddy-global`。
9
+ - **CLI `--json` schema v2**:`doctor` / `status` 的 JSON 输出 `schemaVersion` 1 → 2,新增 `regions.{cn,global}` 分区报告;顶层字段保持 CN 语义向后兼容,但按 `schemaVersion` 判断结构的消费方需注意。
10
+ - **设置 schema 扩展**:新增 `accounts.{cn,global}` 每区域账号选择。旧的单 `accountId` 仍被读取并按其账号实际区域自动归位(软迁移,非硬破坏);`regions` 分槽结构不变。
11
+
12
+ ### Features
13
+
14
+ - **🆕 国内版与国际版拆分为两个并行供应商,可同时使用**——此前一个 provider 一次只能活一个区域(选哪个账号整个 `workbuddy` 就服务于哪个区域,切账号 = 翻转整个运行时目录)。现在两侧是两套完全独立的实例,**互不干扰**:
15
+
16
+ - **双 provider 注册**:国内版保持 `workbuddy`(老 id 不变,存量会话的默认模型与已保存选择全部继续有效),国际版新增 `workbuddy-global`(displayName `WorkBuddy Global`)。**两边模型同时出现在 DSH 模型选择器里**——不同会话 / 子代理可以各选一边,互不干扰。
17
+ - **每个区域一套完整运行时栈**:凭据 store、模型 catalog、回环 shim、adapter 各一份。store 按凭据域名过滤可见账号(`workbuddy.ai` → 国际版,其余 → 国内版),两个区域的账号可同时在线。
18
+ - **插件卡片 tab 化**:设置卡片顶部新增「国内版 / 国际版」tab 栏(带各自登录状态圆点)。每个 tab 有独立的账号选择、积分概览、签到与模型管理;**切 tab 不丢另一侧未保存的草稿**(模型勾选 / 图片开关 / 上下文预算的草稿按区域隔离保存)。4 条卡片路由全部按 `?region=cn|global` 参数化。
19
+ - **「减少刷新变化」**:一个 tab 里切账号、刷新模型、轮询积分,完全不触碰另一边的运行时目录——绑定另一边模型的进行中会话不受任何影响(单 provider 架构做不到这一点)。
20
+ - **账号选择按区域独立**:配置新增 `accounts.{cn,global}`,每个 tab 各选各的账号。旧的单 `accountId` 在启动时按其实际所属区域归位(另一区域保持「跟随 App 当前登录」的默认,绝不静默继承错区域的选中账号)。
21
+ - **凭据刷新副本按区域分文件**:`$DSH_HOME/.workbuddy-auth.cn.json` 与 `.workbuddy-auth.global.json`,双账号同时在线互不覆盖(此前单文件只存一个账号的刷新结果,后写者赢);旧单文件 `.workbuddy-auth.json` 作为迁移来源保留读取(只被其凭据所属的区域采纳),`logout` 清除全部三个副本。
22
+ - **CLI 按区域报告**:`doctor` / `status` 分区域列出账号、登录态与积分(JSON schema 升至 v2,新增 `regions` 字段,顶层字段保持 CN 语义向后兼容);`logout` 清除所有插件自有副本。
23
+
24
+ ### Fixes
25
+
26
+ - adapter 工厂参数化 provider id / displayName(此前 `WORKBUDDY_PROVIDER` 在 `toPiModel`、`createProvider`、profile 等 5 处硬编码);`WorkBuddyCatalog` 构造函数接受区域参数,国际实例从第一刻起就 serve 国际 fallback 目录。
27
+ - `registerModelDiscovery` 单 handler 按 `request.provider` 分流区域;web-status 的模型刷新路由不再先 `resolve()` 凭据推导区域(区域直接来自请求参数)。
28
+
29
+ ### Tests
30
+
31
+ - auth:新增区域化测试 5 例——区域过滤的账号发现、跨区域显式选中不回退、旧单副本只服务其所属区域(迁移)、区域刷新写区域文件不动旧副本、logout 清除全部副本。
32
+ - settings-integration:双 provider 注册断言(`workbuddy` + `workbuddy-global`、两条 configurable provider 目录、各自 fallback 阵容);跨区域污染回归(CN 的图片勾选绝不泄漏进国际 provider 的同名模型;国际 tab 的保存绝不触碰 CN provider)。
33
+ - web-status:region 参数路由测试(`?region=global` 分发到对应 store、缺省回落 `cn`、未知区域 400 且不触碰 store);`workBuddyWebStatus` 断言请求区域驱动全部四个区域化访问器。
34
+
3
35
  ## 1.4.0 (2026-09-11)
4
36
 
5
37
  ### Features
package/README.en.md CHANGED
@@ -17,32 +17,36 @@
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 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.
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. **The CN and international sides are two parallel providers (`workbuddy` / `workbuddy-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management.
21
21
 
22
22
  ## Features
23
23
 
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.
24
+ - **Dual parallel providers (CN + international)** — the CN side registers as the `workbuddy` provider (`GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc.), the international side as `workbuddy-global` (`GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.), and **both rosters appear in the DSH model picker simultaneously**: different sessions can each pick a side without interfering. Model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu.
25
+ - **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account picker, credits overview, and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
25
26
  - **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
- - **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
+ - **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
27
28
  - **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
28
29
  - **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card.
29
- - **Secure loopback shim** — random port + in-process random secret; the real WorkBuddy token is never handed to pi-ai.
30
- - **Command-line diagnostics** — `status` / `doctor` / `logout`, so sign-in, credits, and host health can be checked without a browser.
30
+ - **Secure loopback shim** — one random port + in-process random secret per region; the real WorkBuddy token is never handed to pi-ai.
31
+ - **Command-line diagnostics** — `status` / `doctor` / `logout`, reporting sign-in and credits per region, without a browser.
31
32
 
32
33
  ## How it works
33
34
 
34
35
  ```text
35
- DSH PiAiAdapter
36
- -> secure loopback shim (random port + in-process random secret)
36
+ DSH PiAiAdapter (one stack per provider)
37
+ -> secure loopback shim (one random port + in-process random secret per region)
37
38
  -> WorkBuddyUpstreamClient
38
- -> https://copilot.tencent.com/v2/chat/completions
39
+ -> CN: https://copilot.tencent.com/v2/chat/completions
40
+ -> Global: https://www.workbuddy.ai/v2/chat/completions
39
41
  -> WorkBuddy SSE
40
42
  -> DSH executes local tools and returns their results
41
43
  ```
42
44
 
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.
45
+ The CN and international sides each own a complete runtime stack — credential store, model catalog, loopback shim, adapter — with visibility filtered by credential domain (`workbuddy.ai` → international, anything else → CN), so **both regions' accounts can be signed in and used by different sessions at the same time**.
44
46
 
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.
47
+ 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.
48
+
49
+ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file. Refreshed tokens are kept per region in `$DSH_HOME/.workbuddy-auth.cn.json` and `$DSH_HOME/.workbuddy-auth.global.json` (two simultaneously signed-in accounts never overwrite each other; the legacy single file `.workbuddy-auth.json` is still read as a migration source); the desktop app's file is never written.
46
50
 
47
51
  ## Install
48
52
 
package/README.md CHANGED
@@ -23,32 +23,36 @@
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 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供**只读**的积分概览与可勾选的模型管理界面。
26
+ 一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供**只读**的积分概览与可勾选的模型管理界面。**国内版与国际版是两个并行的供应商(`workbuddy` / `workbuddy-global`),可同时使用**;插件设置卡片以 tab 区分两者,方便统一管理。
27
27
 
28
28
  ## 功能特性
29
29
 
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 自身模型菜单一致。**区域自动路由,无需任何开关**——选中哪个账号就走哪个区域。
30
+ - **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等),**两边模型同时出现在 DSH 模型选择器里**:不同会话可以各选一边,互不干扰。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致。
31
+ - **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
31
32
  - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;图片输入按模型手动勾选(默认不勾选)。
32
- - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可切换账号;Token 不写入 DSH 设置。
33
+ - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
33
34
  - **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
34
35
  - **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。
35
- - **安全 loopback shim** —— 随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。
36
- - **命令行诊断** —— `status` / `doctor` / `logout`,无需浏览器即可确认登录、积分与宿主状态。
36
+ - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。
37
+ - **命令行诊断** —— `status` / `doctor` / `logout`,按区域报告登录与积分,无需浏览器即可确认宿主状态。
37
38
 
38
39
  ## 工作原理
39
40
 
40
41
  ```text
41
- DSH PiAiAdapter
42
- -> 安全 loopback shim(随机端口 + 进程内随机 secret)
42
+ DSH PiAiAdapter(每个 provider 一套)
43
+ -> 安全 loopback shim(每区域一个随机端口 + 进程内随机 secret)
43
44
  -> WorkBuddyUpstreamClient
44
- -> https://copilot.tencent.com/v2/chat/completions
45
+ -> 国内版 https://copilot.tencent.com/v2/chat/completions
46
+ -> 国际版 https://www.workbuddy.ai/v2/chat/completions
45
47
  -> WorkBuddy SSE
46
48
  -> DSH 本地执行工具并回传结果
47
49
  ```
48
50
 
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` → 国际版,其余 → 国内版),且**两个区域各自保存一份目录与勾选**,切换账号互不覆盖。
51
+ 国内版与国际版各持一套完整的运行时栈——凭据 store、模型 catalog、回环 shim、adapter——按凭据域名(`workbuddy.ai` → 国际版,其余 → 国内版)隔离可见账号,所以**两个区域的账号可以同时在线、同时被不同会话使用**。
50
52
 
51
- 凭据读取自 WorkBuddy 桌面 App 自身的 auth 文件(只读);刷新得到的 token 单独存放在 `$DSH_HOME/.workbuddy-auth.json`,桌面端文件永不被写入。
53
+ 模型目录按区域取自各自的正确来源:国内版走 `/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`),均为只读接口。
54
+
55
+ 凭据读取自 WorkBuddy 桌面 App 自身的 auth 文件(只读);刷新得到的 token 按区域存放在 `$DSH_HOME/.workbuddy-auth.cn.json` 与 `$DSH_HOME/.workbuddy-auth.global.json`(双账号同时在线互不覆盖;旧的单文件 `.workbuddy-auth.json` 作为迁移来源保留读取),桌面端文件永不被写入。
52
56
 
53
57
  ## 安装
54
58
 
package/lib/bin.d.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  * 均由该项目设计。
10
10
  * 改动:凭据诊断由单文件扩展为「目录扫描 + 按账号分组」,
11
11
  * doctor 会列出发现的每个账号及其文件来源,便于确认多账号是否可用;
12
- * 另补 desktopAuthDir 字段。
12
+ * 另补 desktopAuthDir 字段。双 provider 化后,doctor/status 按
13
+ * 区域(cn | global)分别报告各自的账号与登录态,logout 清除
14
+ * 所有插件自有凭据副本(两个区域文件 + 旧单文件)。
13
15
  *
14
16
  * @module dsh-connect-workbuddy/bin
15
17
  */
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { T as WorkBuddyUpstreamClient, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-sqjxgXkW.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -13,11 +13,20 @@ import { fileURLToPath } from "node:url";
13
13
  * 均由该项目设计。
14
14
  * 改动:凭据诊断由单文件扩展为「目录扫描 + 按账号分组」,
15
15
  * doctor 会列出发现的每个账号及其文件来源,便于确认多账号是否可用;
16
- * 另补 desktopAuthDir 字段。
16
+ * 另补 desktopAuthDir 字段。双 provider 化后,doctor/status 按
17
+ * 区域(cn | global)分别报告各自的账号与登录态,logout 清除
18
+ * 所有插件自有凭据副本(两个区域文件 + 旧单文件)。
17
19
  *
18
20
  * @module dsh-connect-workbuddy/bin
19
21
  */
20
- const JSON_SCHEMA_VERSION = 1;
22
+ const JSON_SCHEMA_VERSION = 2;
23
+ /** Both regions, in reporting order. */
24
+ const REGIONS = ["cn", "global"];
25
+ /** Region labels for human output. */
26
+ const REGION_LABELS = {
27
+ cn: "CN (domestic)",
28
+ global: "Global"
29
+ };
21
30
  /** Remove token-like strings from an unexpected diagnostic message. */
22
31
  function safeMessage(error) {
23
32
  return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]");
@@ -27,8 +36,8 @@ function printHelp() {
27
36
  "Usage: dsh-connect-workbuddy <doctor|status|logout> [--json]",
28
37
  "",
29
38
  " doctor secret-free sign-in and environment diagnostics",
30
- " status sign-in state, remaining WorkBuddy credit, and host-bundle health",
31
- " logout remove the plugin-owned credential copy (the desktop app keeps its sign-in)",
39
+ " status per-region sign-in state, remaining credit, and host-bundle health",
40
+ " logout remove every plugin-owned credential copy (the desktop app keeps its sign-in)",
32
41
  " --json emit one secret-free JSON document (doctor/status only)",
33
42
  ""
34
43
  ].join("\n"));
@@ -36,29 +45,45 @@ function printHelp() {
36
45
  function printJson(value) {
37
46
  process.stdout.write(`${JSON.stringify(value)}\n`);
38
47
  }
39
- function makeStore() {
48
+ /** One region-scoped credential store (its own plugin-owned copy file). */
49
+ function makeStore(region) {
50
+ const client = new WorkBuddyUpstreamClient();
51
+ return new WorkBuddyCredentialStore({
52
+ region,
53
+ refresh: (credential) => client.refreshToken(credential)
54
+ });
55
+ }
56
+ /** The region-unscoped store (reads every plugin-owned copy). */
57
+ function makeAnyStore() {
40
58
  const client = new WorkBuddyUpstreamClient();
41
59
  return new WorkBuddyCredentialStore({ refresh: (credential) => client.refreshToken(credential) });
42
60
  }
43
61
  async function doctor(jsonOutput) {
44
- const store = makeStore();
45
- const status = await store.status();
46
- const desktopPresent = await store.desktopFilePresent();
62
+ const anyStore = makeAnyStore();
63
+ const desktopPresent = await anyStore.desktopFilePresent();
47
64
  const heartbeat = await readHostHeartbeat();
48
65
  const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
49
- const accounts = await store.accounts();
66
+ const regionLists = await Promise.all(REGIONS.map(async (region) => ({
67
+ region,
68
+ accounts: await makeStore(region).accounts()
69
+ })));
70
+ const anySignedIn = regionLists.some(({ accounts }) => accounts.length > 0);
50
71
  const report = {
51
72
  schemaVersion: JSON_SCHEMA_VERSION,
52
73
  package: "dsh-connect-workbuddy",
53
74
  version: WORKBUDDY_CONNECT_VERSION,
54
75
  node: process.version,
55
76
  desktopAuthFile: {
56
- path: store.desktopAuthPath() ?? "(no platform default; set WORKBUDDY_AUTH_FILE)",
77
+ path: anyStore.desktopAuthPath() ?? "(no platform default; set WORKBUDDY_AUTH_FILE)",
57
78
  dir: defaultDesktopAuthDirs()[0] ?? "(no platform default)",
58
79
  candidates: defaultDesktopAuthCandidates(),
59
80
  present: desktopPresent
60
81
  },
61
- ownAuthFile: workbuddyOwnAuthPath(),
82
+ ownAuthFiles: {
83
+ cn: workbuddyOwnAuthPath("cn"),
84
+ global: workbuddyOwnAuthPath("global"),
85
+ legacy: legacyWorkbuddyOwnAuthPath()
86
+ },
62
87
  hostHeartbeat: {
63
88
  path: workbuddyHostHeartbeatPath(),
64
89
  present: heartbeat !== void 0,
@@ -68,21 +93,22 @@ async function doctor(jsonOutput) {
68
93
  },
69
94
  processAlive: hostAlive
70
95
  },
71
- signIn: status.state,
72
- accounts: accounts.map((account) => ({
96
+ regions: Object.fromEntries(regionLists.map(({ region, accounts }) => [region, accounts.map((account) => ({
73
97
  id: account.id,
74
98
  accountName: account.accountName,
75
99
  domain: account.domain === "" ? void 0 : account.domain,
76
100
  source: account.source,
77
101
  selected: account.selected,
78
102
  tokenExpiresAt: new Date(account.tokenExpiresAtMs).toISOString()
79
- })),
80
- fallbackModels: FALLBACK_WORKBUDDY_MODELS.length,
103
+ }))])),
104
+ fallbackModels: {
105
+ cn: FALLBACK_WORKBUDDY_MODELS.length,
106
+ global: FALLBACK_WORKBUDDY_MODELS_GLOBAL.length
107
+ },
81
108
  hints: [
82
- ...status.state === "signed-in" ? [] : ["Sign in once in the WorkBuddy desktop app, then run status again."],
109
+ ...anySignedIn ? [] : ["Sign in once in the WorkBuddy desktop app (either region), then run status again."],
83
110
  ...desktopPresent ? [] : [`No WorkBuddy desktop auth file at the expected path; set ${WORKBUDDY_AUTH_FILE_ENV} if it lives elsewhere.`],
84
- ...hostAlive ? [] : ["Host bundle not running in this DSH profile (or the process exited). The browser card and provider are unavailable until DSH starts the plugin."],
85
- ...accounts.length > 1 ? [] : ["Only one local WorkBuddy credential was found; account switching needs the app to have left backups of earlier sign-ins."]
111
+ ...hostAlive ? [] : ["Host bundle not running in this DSH profile (or the process exited). The browser card and providers are unavailable until DSH starts the plugin."]
86
112
  ]
87
113
  };
88
114
  if (jsonOutput) printJson(report);
@@ -90,77 +116,85 @@ async function doctor(jsonOutput) {
90
116
  `WorkBuddy Connect ${WORKBUDDY_CONNECT_VERSION} on ${process.version}`,
91
117
  `Desktop auth file: ${report.desktopAuthFile.present ? "present" : "missing"} (${report.desktopAuthFile.path})`,
92
118
  `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : heartbeat !== void 0 ? "stale heartbeat (process exited)" : "not started"}`,
93
- `Sign-in state: ${report.signIn}`,
94
- `Local accounts: ${accounts.length}`,
95
- ...accounts.map((account) => ` - ${account.accountName} (${account.id})${account.selected ? " [selected]" : ""} expires ${new Date(account.tokenExpiresAtMs).toISOString()}`),
96
- `Static fallback models: ${report.fallbackModels}`,
119
+ ...regionLists.flatMap(({ region, accounts }) => [`${REGION_LABELS[region]} accounts: ${accounts.length} (own copy ${workbuddyOwnAuthPath(region)})`, ...accounts.map((account) => ` - ${account.accountName} (${account.id})${account.selected ? " [selected]" : ""} expires ${new Date(account.tokenExpiresAtMs).toISOString()}`)]),
120
+ `Static fallback models: CN ${report.fallbackModels.cn}, Global ${report.fallbackModels.global}`,
97
121
  ...report.hints.map((hint) => `Hint: ${hint}`),
98
122
  ""
99
123
  ].join("\n"));
100
- return status.state === "signed-in" && desktopPresent ? 0 : 1;
124
+ return anySignedIn && desktopPresent ? 0 : 1;
101
125
  }
102
- async function status(jsonOutput) {
103
- const store = makeStore();
126
+ /** One region's sign-in and credit summary. */
127
+ async function regionStatus(region) {
128
+ const store = makeStore(region);
104
129
  const client = new WorkBuddyUpstreamClient();
105
130
  const authStatus = await store.status();
106
- const heartbeat = await readHostHeartbeat();
107
- const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
108
- const hostState = hostAlive ? "running" : heartbeat !== void 0 ? "stale" : "not-started";
109
- if (authStatus.state !== "signed-in") {
110
- if (jsonOutput) printJson({
111
- schemaVersion: JSON_SCHEMA_VERSION,
112
- package: "dsh-connect-workbuddy",
113
- version: WORKBUDDY_CONNECT_VERSION,
114
- status: "signed-out",
115
- hostBundle: hostState
116
- });
117
- else process.stdout.write(`WorkBuddy Connect: signed out\nHost bundle: ${hostState}\n`);
118
- return 1;
119
- }
120
- let credits;
131
+ const accounts = await store.accounts();
132
+ const selected = accounts.find((account) => account.selected);
133
+ const base = {
134
+ region,
135
+ status: authStatus.state,
136
+ ...authStatus.expiresAtMs === void 0 ? {} : { accessTokenExpires: new Date(authStatus.expiresAtMs).toISOString() },
137
+ ...authStatus.nickname === void 0 ? {} : { nickname: authStatus.nickname },
138
+ ...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
139
+ ...selected === void 0 ? {} : {
140
+ accountId: selected.id,
141
+ accountName: selected.accountName
142
+ },
143
+ accountCount: accounts.length
144
+ };
145
+ if (authStatus.state !== "signed-in") return base;
121
146
  try {
122
147
  const credential = await store.resolve();
123
- credits = { total: (await client.fetchCredits(credential)).total };
148
+ return {
149
+ ...base,
150
+ credits: (await client.fetchCredits(credential)).total
151
+ };
124
152
  } catch (error) {
125
- credits = {
126
- total: 0,
127
- error: safeMessage(error)
153
+ return {
154
+ ...base,
155
+ credits: 0,
156
+ creditsError: safeMessage(error)
128
157
  };
129
158
  }
130
- const accounts = await store.accounts();
131
- const selected = accounts.find((account) => account.selected);
132
- const expiresAt = authStatus.expiresAtMs !== void 0 ? new Date(authStatus.expiresAtMs).toISOString() : void 0;
159
+ }
160
+ async function status(jsonOutput) {
161
+ const heartbeat = await readHostHeartbeat();
162
+ const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
163
+ const hostState = hostAlive ? "running" : heartbeat !== void 0 ? "stale" : "not-started";
164
+ const fragments = await Promise.all(REGIONS.map((region) => regionStatus(region)));
165
+ const cn = fragments.find((fragment) => fragment.region === "cn");
133
166
  if (jsonOutput) {
134
167
  printJson({
135
168
  schemaVersion: JSON_SCHEMA_VERSION,
136
169
  package: "dsh-connect-workbuddy",
137
170
  version: WORKBUDDY_CONNECT_VERSION,
138
- status: "signed-in",
139
- ...expiresAt === void 0 ? {} : { accessTokenExpires: expiresAt },
140
- ...authStatus.nickname === void 0 ? {} : { nickname: authStatus.nickname },
141
- ...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
142
- source: authStatus.source,
143
- ...selected === void 0 ? {} : {
144
- accountId: selected.id,
145
- accountName: selected.accountName
146
- },
147
- accountCount: accounts.length,
148
- credits: credits?.total,
149
- ...credits?.error === void 0 ? {} : { creditsError: credits.error },
171
+ status: cn?.status === "signed-in" ? "signed-in" : "signed-out",
172
+ ...cn?.status === "signed-in" ? {
173
+ ...cn.accessTokenExpires === void 0 ? {} : { accessTokenExpires: cn.accessTokenExpires },
174
+ ...cn.nickname === void 0 ? {} : { nickname: cn.nickname },
175
+ ...cn.domain === void 0 ? {} : { domain: cn.domain },
176
+ ...cn.accountId === void 0 ? {} : {
177
+ accountId: cn.accountId,
178
+ accountName: cn.accountName
179
+ }
180
+ } : {},
181
+ regions: Object.fromEntries(fragments.map((fragment) => [fragment.region, fragment])),
150
182
  hostBundle: hostState
151
183
  });
152
- return 0;
184
+ return fragments.some((fragment) => fragment.status === "signed-in") ? 0 : 1;
153
185
  }
154
186
  process.stdout.write([
155
- `WorkBuddy Connect: signed in${authStatus.nickname === void 0 ? "" : ` as ${authStatus.nickname}`}`,
156
- ...expiresAt === void 0 ? [] : [`Access token expires ${expiresAt} (refresh is automatic)`],
157
- `Local accounts: ${accounts.length}${selected === void 0 ? "" : ` (using ${selected.accountName})`}`,
158
- credits?.error === void 0 ? `Remaining credit: ${credits?.total ?? "unknown"}` : `Remaining credit: unavailable (${credits.error})`,
187
+ ...fragments.flatMap((fragment) => [
188
+ `${REGION_LABELS[fragment.region]}: ${fragment.status === "signed-in" ? `signed in${fragment.accountName === void 0 ? "" : ` as ${fragment.accountName}`}` : "signed out"}`,
189
+ ...fragment.status === "signed-in" && fragment.accessTokenExpires !== void 0 ? [` Access token expires ${fragment.accessTokenExpires} (refresh is automatic)`] : [],
190
+ ` Local accounts: ${fragment.accountCount}`,
191
+ ...fragment.creditsError !== void 0 ? [` Remaining credit: unavailable (${fragment.creditsError})`] : fragment.credits !== void 0 ? [` Remaining credit: ${fragment.credits}`] : []
192
+ ]),
159
193
  `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : hostState === "stale" ? "stale heartbeat (DSH process exited)" : "not started in this profile"}`,
160
- "Client card: load failures are logged to the browser console only; the host provider is unaffected.",
194
+ "Client card: load failures are logged to the browser console only; the host providers are unaffected.",
161
195
  ""
162
196
  ].join("\n"));
163
- return 0;
197
+ return fragments.some((fragment) => fragment.status === "signed-in") ? 0 : 1;
164
198
  }
165
199
  /** Execute one boot-free command. */
166
200
  async function run(argv) {
@@ -188,8 +222,8 @@ async function run(argv) {
188
222
  case "doctor": return await doctor(jsonOutput);
189
223
  case "status": return await status(jsonOutput);
190
224
  case "logout":
191
- await makeStore().logout();
192
- process.stdout.write(`WorkBuddy Connect: removed ${workbuddyOwnAuthPath()}; the desktop app's sign-in is untouched\n`);
225
+ await makeAnyStore().logout();
226
+ process.stdout.write(`WorkBuddy Connect: removed the plugin-owned credential copies (${workbuddyOwnAuthPath("cn")}, ${workbuddyOwnAuthPath("global")}, ${legacyWorkbuddyOwnAuthPath()}); the desktop app's sign-in is untouched\n`);
193
227
  return 0;
194
228
  }
195
229
  } catch (error) {