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 +32 -0
- package/README.en.md +14 -10
- package/README.md +14 -10
- package/lib/bin.d.ts +3 -1
- package/lib/bin.js +103 -69
- package/lib/client.js +185 -64
- package/lib/{host-heartbeat-DQQwiUtm.js → host-heartbeat-sqjxgXkW.js} +988 -910
- package/lib/index.d.ts +133 -28
- package/lib/index.js +197 -97
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
|
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`,
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
33
|
+
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
33
34
|
- **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
|
|
34
35
|
- **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。
|
|
35
|
-
- **安全 loopback shim** ——
|
|
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
|
|
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
|
-
|
|
51
|
+
国内版与国际版各持一套完整的运行时栈——凭据 store、模型 catalog、回环 shim、adapter——按凭据域名(`workbuddy.ai` → 国际版,其余 → 国内版)隔离可见账号,所以**两个区域的账号可以同时在线、同时被不同会话使用**。
|
|
50
52
|
|
|
51
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
31
|
-
" logout remove
|
|
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
|
-
|
|
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
|
|
45
|
-
const
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
103
|
+
}))])),
|
|
104
|
+
fallbackModels: {
|
|
105
|
+
cn: FALLBACK_WORKBUDDY_MODELS.length,
|
|
106
|
+
global: FALLBACK_WORKBUDDY_MODELS_GLOBAL.length
|
|
107
|
+
},
|
|
81
108
|
hints: [
|
|
82
|
-
...
|
|
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
|
|
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
|
-
`
|
|
94
|
-
`
|
|
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
|
|
124
|
+
return anySignedIn && desktopPresent ? 0 : 1;
|
|
101
125
|
}
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
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
|
-
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
credits: (await client.fetchCredits(credential)).total
|
|
151
|
+
};
|
|
124
152
|
} catch (error) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
153
|
+
return {
|
|
154
|
+
...base,
|
|
155
|
+
credits: 0,
|
|
156
|
+
creditsError: safeMessage(error)
|
|
128
157
|
};
|
|
129
158
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
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
|
-
...
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
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) {
|