dsh-connect-workbuddy 2.0.6 → 2.0.10
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 +140 -0
- package/README.en.md +6 -1
- package/README.md +6 -1
- package/lib/bin.js +15 -2
- package/lib/client.js +416 -66
- package/lib/{host-heartbeat--iKDuwr5.js → host-heartbeat-DPbL9oi2.js} +539 -31
- package/lib/index.d.ts +438 -10
- package/lib/index.js +262 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.10 (2026-09-26)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **未登录卡片「重复提示」**(用户实拍反馈)。上一版把探测过的路径列出来之后,同一条信息在屏幕上出现了四遍:`resolve()` 抛出的错误本身已经**枚举了所有候选路径**(`expected <Local>\workbuddy-desktop.info or <Roaming>\... or WORKBUDDY_AUTH_FILE`),下面折叠区又把同样的路径列了一遍(还带各自原因);两处提示语(`row.signedOutHint` 与 `row.searchedHint`)又各自说了一遍「没有找到登录信息」。现在分工明确:**段落说结论,列表给细节**——只要列表会渲染,段落就不再复述 `resolve()` 的路径枚举,退回简短提示;`row.searchedHint` 也删掉了开头的「未找到登录信息」(列表标题「已检查的路径」已经承担)。
|
|
8
|
+
|
|
9
|
+
- **列表缺席时才显示原始错误**:`searched` 为空(旧 Host、或探测抛错)时,`message` 不是重复而是唯一的信息来源,此时照常显示。被丢弃的只有「或刷新已有会话」——在 `searched` 出现的那条分支上它恰好是废话:那条分支**只在账号数为 0 时**成立,根本没有会话可刷新。
|
|
10
|
+
- **`selectionLost` 优先级最高**:那种情况下 token 是健康的,正确动作是重选账号,任何路径列表都不能替代这条建议。
|
|
11
|
+
- **判定规则下沉到可测模块**:新增 `signedOutNotice()` / `signedOutText()`(`src/client/searched-paths.ts`,无浏览器依赖)。卡片是 `.tsx`,本项目测试环境是纯 Node、没有 jsdom/react-dom,规则留在组件里就永远测不到——而「有没有重复」正是必须守住的东西。
|
|
12
|
+
|
|
13
|
+
- **`已检查的路径 (N)` 的 N 少数了一条**(同一张截图暴露的真实缺陷,经实机复现确认)。`diagnose()` 里有一句 `if ('credential' in probe) continue`:只要文件**能解析成凭据**就跳过,**不套用 `readAll()` 的区域过滤**。于是「一份有效的、属于另一个区域的登录」同时被两边排除——`readAll()` 按区域把它滤掉(所以它不是账号),`diagnose()` 因为它解析成功而跳过(所以它不是失败)。结果是:**它不在任何列表里**,卡片如实报告「检查了 1 条」,而同屏的报错信息却列了 2 条路径。用户那份**真正可用的登录**,正是被隐掉的那条。
|
|
14
|
+
|
|
15
|
+
- **新增 `wrong-region` 原因**(第五档):这**不是「没有」而是「找到了」**——凭据真实、可读、只是属于另一个标签页。因此它归入「值得先看」一组,直接展开,绝不收进折起的「不存在」里;它同时是整件事最省事的解释(切个标签页就好,不用重新登录)。
|
|
16
|
+
- **段落直接给出修法**:存在 `wrong-region` 时,未登录文案换成 `row.signedOutWrongRegion`(「找到了你的登录信息,但它属于另一个地区…切换标签页即可,不需要重新登录」),而不是让用户自己从折叠列表里推断。
|
|
17
|
+
|
|
18
|
+
- **`missing` 条目的消息把路径又印了一遍**(实机验证时发现,同一类问题的更隐蔽一处)。Node 的 ENOENT 文案形如 `ENOENT: no such file or directory, open '<path>'`,而卡片本来就用自己的 `<code>` 行显示该路径,于是同一串字符**上下两行完全相同**。现在 `missing` 不再携带消息;真正的读取错误(`EISDIR` / `EACCES`)仍然保留——它们的文案说的是原因本身不包含的信息,且**不含路径**,这一点有测试钉住。
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
测试总数 286 → 300(新增 14 例)。
|
|
23
|
+
|
|
24
|
+
- `tests/searched-paths.spec.ts`(14 → 23 例):新增 `signedOutNotice` / `signedOutText` 专项——**有列表时不再复述路径**(直接对渲染出的字符串断言,且断言其中不含 `workbuddy-desktop.info` / `WORKBUDDY_AUTH_FILE`)、列表缺席时仍显示原始错误、`wrong-region` 抢先给出切换建议、`selectionLost` 在任何组合下都最高优先。另外把散在各处的四档原因清单收敛为 `ALL_REASONS`,并新增「**每一档原因都必须被分组,新原因不许落进 `missing`**」——这正是本次踩到的坑:新原因默认归入 `missing` 会被**藏进折起区**,与「发现」所需的可见性正好相反。
|
|
25
|
+
- `tests/diagnose.spec.ts`(10 → 14 例):**跨区域的桌面凭据必须上报为 `wrong-region`**(直接钉住上面那个 `(1)` 的根因)、**它报告的路径条数必须与它给出的路径数一致**(总数不许少报)、`missing` 不带消息、`unreadable` 保留消息且不含路径。
|
|
26
|
+
- `tests/web-status.spec.ts`(+1 例):`wrong-region` 原样穿过到浏览器——这条链路任何一环丢掉它,用户的登录就既不可见、计数也短一位。
|
|
27
|
+
|
|
28
|
+
**变异验证(实测)**:本轮共 8 处定向改写,全部被测试咬住(0 逃脱)——恢复 `diagnose()` 的静默跳过、段落重新复述 `resolve()` 消息、去掉 `wrong-region` 头部结论、把 `selectionLost` 降级、把 `wrong-region` 归入 `missing`、原因标签表回落到 `invalid`、提示语重新加上「未找到登录信息」、恢复 ENOENT 消息。
|
|
29
|
+
|
|
30
|
+
### Docs
|
|
31
|
+
|
|
32
|
+
- `docs/DESIGN.md` §3.4:记录「段落说结论、列表给细节」的分工,以及 `wrong-region` 为什么是**发现**而不是**缺席**。
|
|
33
|
+
|
|
34
|
+
## 2.0.9 (2026-09-25)
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
|
|
38
|
+
- **未登录时列出「探测过哪些路径、各自为什么没成」**(对齐 trae 的未登录体验)。此前卡片只会说一句「在桌面 App 里登录一次」,用户无法判断插件到底找过哪里——凭据存在别处、或桌面 App 装在非默认位置时,这句话既不解释现状也不给出下一步。现在未登录态的卡片带一个折叠区「已检查的路径 (N)」,逐条列出**路径 + 来源 + 原因**;标签页的改动只落在**真正什么都没找到**的那条分支上。
|
|
39
|
+
|
|
40
|
+
- **四档原因,而不是笼统的「读不到」**:`missing`(不存在)/ `unreadable`(在但读不了)/ `invalid`(读到了但没有可用 token)/ `encrypted`(读到了、字段是加密的、拿不到密钥)。探测在 `probeAuthFile()` 里一次完成,`readAuthFile()` 退化为只要凭据的薄包装,既有调用方一行未改。
|
|
41
|
+
- **`encrypted` 是 workbuddy 特有的、也是本版最重要的新增档位**:Windows 新版桌面端把 token 字段加密,读它需要桌面 App 自身的原生绑定(`WORKBUDDY_APP_EXECUTABLE` 可指定)。此时用户**多半已经登录了**,卡片若沿用「重新登录一次」就是在指一条无效的路。因此这类探测结果会额外顶出一条提示,明说「需要桌面 App 在场,单纯重新登录解决不了」。把它并入 `invalid` 会让这句提示永远显示不出来——这正是变异测试专门盯住的一条。
|
|
42
|
+
- **同一档内再分「值得先看 / 只是不存在」**:普通机器上多数候选路径都是 `missing`(App 只写一个文件,平台却有好几个候选目录),全部平铺会把真正解释失败的那一条淹掉。因此 `encrypted`/`invalid`/`unreadable` 直接展开,`missing` 收在第二层折叠后;而当**全都是** `missing` 时它就是答案本身,直接展开,不需要再点一次。trae 定义了 `row.searchedMore` 文案却从未接上,这里真正实现。
|
|
43
|
+
- **只在「一无所获」时列出**:`selectionLost`(有本地登录、只是保存的账号 id 失配)这条分支不列路径——那些 token 是好的,正确动作是重新选账号,一列失败路径只会把话带偏。这一条有独立测试。
|
|
44
|
+
- **诊断本身永不成为失败源**:`diagnose()` 会真的读盘,因此卡片侧对「没有 `diagnose()` 的旧 Host」「探测抛错」「结果为空」三种情况都退回到原来那句提示,不把整个卡片拖成错误态;空结果也不下发空数组(否则卡片会渲染出一个「已检查的路径 (0)」的空块)。
|
|
45
|
+
- **消息先脱敏再下发**:探测信息来自文件系统与桌面 App 的子进程,两者都可能回显 token 片段。新增的路径列表不能变成新的泄露面,因此沿用 `safeMessage()` 过滤后再跨到浏览器。
|
|
46
|
+
- **插件自己的副本不作为「已检查路径」上报**:它直到首次刷新才存在,报出来只会给每台机器稳定加两条噪声。
|
|
47
|
+
|
|
48
|
+
### Tests
|
|
49
|
+
|
|
50
|
+
测试总数 255 → 286(新增 31 例):
|
|
51
|
+
|
|
52
|
+
- `tests/diagnose.spec.ts`(新增 10 例):`diagnose()` 的四档原因逐一钉住——全空机器上**仍报告探测路径**、可读文件**不出现在报告里**、非法 JSON 与「有 JSON 无 token」都归 `invalid`、加密且无密钥归 `encrypted`、密钥查找抛错也归 `encrypted`、**拿到了密钥却打不开归 `invalid`**(这是「App 在、文件坏了」,建议与「装 App」不同)、**健康文件不因旁边有个坏文件而被误报**、报告里不含 token 片段、跨区域不串号。加密夹具用与桌面端相同的 AES-256-GCM 编解码器现场封制(复用 `at-rest.spec.ts` 的做法),因此 `encrypted` 是对着一个真实信封证明的,而不是一个现实中不可能出现的假标记。
|
|
53
|
+
- `tests/web-status.spec.ts`(新增 7 例):路径列表下发、`encrypted` 原因原样穿过到浏览器、空结果**不下发空数组**、无 `diagnose()` 的旧 store 优雅降级、探测抛错不影响状态路由、`selectionLost` 时不列路径、消息里的 token 形状内容被脱敏而路径本身保留。
|
|
54
|
+
- `tests/searched-paths.spec.ts`(新增 14 例):展示规则(`missing` 何时隐藏/何时展开、分组内保持 Host 探测顺序、四档原因各自映射到不同文案键、来源标签、总数口径),以及**两套语言里四档原因的文案互不相同**——文案合并会让 `encrypted` 在用户眼里退回成「文件不存在」,而其余测试全绿。
|
|
55
|
+
|
|
56
|
+
**变异验证(实测)**:共 14 处定向改写,全部被测试咬住(0 逃脱)——`encrypted` 并入 `invalid`、`missing` 误报为 `unreadable`、把插件副本的空缺也上报、无密钥的加密文件报成 `invalid`、空结果下发空数组、消息不脱敏、`selectionLost` 时也列路径、`missingOpen` 恒为 `false`、`encrypted` 移出「值得先看」集合、原因标签丢掉来源、`encrypted` 映射到别的文案键、总数只数可见组、加密提示丢掉 `WORKBUDDY_APP_EXECUTABLE`、以及四档文案合并成一条。
|
|
57
|
+
|
|
58
|
+
### Docs
|
|
59
|
+
|
|
60
|
+
- `README.md` / `README.en.md`:功能特性新增「未登录时列出探测过的路径与原因」一条。
|
|
61
|
+
- `docs/DESIGN.md` §3.4:记录未登录诊断的四档原因划分,以及「`encrypted` 必须独立成档」的理由。
|
|
62
|
+
|
|
63
|
+
## 2.0.8 (2026-09-24)
|
|
64
|
+
|
|
65
|
+
### Features
|
|
66
|
+
|
|
67
|
+
- **可以单独关掉任一版本的供应商**(对齐 trae 的 issue #11)。每个 tab 右侧新增一个勾选框(默认都勾选),**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**,而不是只藏起 tab。
|
|
68
|
+
|
|
69
|
+
- **关闭动作落到注册层,不是 UI 层**:`AdapterRegistrationHandle.replace([])` 抽掉该区域的路由,`DirectoryRegistrationHandle.replace([])` 摘掉它在「设置 → 模型」页的目录条目。已验证 `listProviders()` 在该区域关闭后不再含它——这正是 DSH 构造模型选择器的取数来源,所以分组是**真的消失**,不是被藏起来。注册仍是「先正常注册、再 replace([])」两步在同一同步段内完成:空数组的**首次**注册会被 DSH 拒绝(`INVALID_ADAPTER`),而 `replace([])` 明确合法。
|
|
70
|
+
- **配置**:`regions.<cn|global>.enabled`,**opt-out**——只有显式 `false` 才关闭。**开关出现前的配置、以及区域拆分前的扁平旧字段一律视为开启**,老用户零感知。
|
|
71
|
+
- **状态不丢**:开关只改该区域 `enabled` 一个字段,该槽位的目录 / 勾选 / 图片开关 / 上下文预算,以及另一个区域的整个槽位,全部原样带过。关掉再打开能完整恢复。
|
|
72
|
+
- **保存模型不会偷偷重开**:卡片保存目录时会**回写 `enabled`**——那次写入替换整个槽位,漏掉它就会「保存一次模型列表 = 悄悄重新打开已关闭的供应商」。
|
|
73
|
+
- **已关闭的区域不再做无意义的启动请求**:跳过它的启动目录刷新与 `registerModelDiscovery` 兜底,顺带消掉「无账号区域每次启动报一次错」的失败面。
|
|
74
|
+
- **已知代价**:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`(`no adapter registered for provider "..."`)。这是「彻底移除」的必然结果,卡片在关闭态的 tab 上给出明确提示文案,不静默失败。
|
|
75
|
+
- **与 issue #12 的「无账号自动隐藏」是两个独立维度**:`WorkBuddyCatalog` 现在同时持有 `usable`(自动:本机有无该区域账号)与 `enabled`(用户显式开关),`current()` 在**两者都成立**时才返回模型。只有用户重新勾选、且该区域确有账号时,模型才会回来。
|
|
76
|
+
|
|
77
|
+
### Tests
|
|
78
|
+
|
|
79
|
+
测试总数 236 → 255(新增 19 例):
|
|
80
|
+
|
|
81
|
+
- `tests/region.spec.ts`(新增 11 例):`regionEnabledOf` 的 opt-out 缺省、**同时接受「整个 settings 段」与「`regions` 子对象」两种入参**(这正是 trae 上真实发生过的 bug:卡片传错一半 → 判定恒为 `true` → 勾选「点了没反应」)、`nextRegionEnabled` 只改目标字段且不碰另一区域、以及 Host 侧 `regionEnabled` 对扁平旧配置判定为开启。
|
|
82
|
+
- `tests/catalog.spec.ts`(新增 4 例):`setRegionEnabled` / `isRegionEnabled`、关闭后 `current()` 为空、重新开启恢复、以及 **`usable` 与 `enabled` 两个闸门互不干扰**(只开开关不足以让无账号区域复活)。
|
|
83
|
+
- `tests/settings-integration.spec.ts`(新增 4 例,走真实宿主装配):关闭国际版后 `listProviders()` 不含 `workbuddy-global`、目录条目同步消失、**国内版的目录条目必须仍在**(守护「目录是一次性注册持有两个条目」这一约束)、另一侧不受影响、重新开启恢复、两个都关仍可恢复、旧配置两边都开。
|
|
84
|
+
- `tests/web-status.spec.ts`:`enabled` 在 signed-in / signed-out 两个分支都下发。
|
|
85
|
+
|
|
86
|
+
**变异验证(实测)**:把「撤掉路由」改成「照常注册」(即只隐藏 tab 的假实现),3 条集成测试立刻失败;把目录 `replace` 改回**按区域分别调用**,`registers both regional providers` 与 `both switched off can still be restored` 两条失败,失败现场收到的正是 `[{workbuddy-global}]`——国内版条目被覆盖掉了,与 trae 记录的这个 bug 完全一致。测试确实咬得住。
|
|
87
|
+
|
|
88
|
+
### Docs
|
|
89
|
+
|
|
90
|
+
- `README.md` / `README.en.md`:「功能特性」新增「可单独关闭任一版本供应商」一条,并在双供应商那条说明关闭后的效果与代价。
|
|
91
|
+
- `docs/DESIGN.md`:说明区域开关为何必须落在注册层(`replace([])`),以及它与 issue #12「无账号隐藏」的维度差异。
|
|
92
|
+
|
|
93
|
+
## 2.0.7 (2026-09-23)
|
|
94
|
+
|
|
95
|
+
### Features
|
|
96
|
+
|
|
97
|
+
- **凭据被上游拒绝时,给出「可执行的」建议,而不是笼统地让你重新登录**。一个 401 背后可能是两件完全不同的事,而说错那一句比不说更糟:**选中的账号**已作废、但本机另一个登录明明好用(此时该「切换账号」,重新登录毫无作用——凭据从来不是问题,作废的是那条**选择**);或者本机所有登录都已失效(此时才该「重新登录」)。
|
|
98
|
+
|
|
99
|
+
- **先分类,再给建议**:新增 `WorkBuddyCredentialRejectedError`(由 `CREDENTIAL_REJECTED_CODE` 标记而非 `instanceof` 识别,跨打包边界仍可靠),网关的 HTML 401/403 与 JSON 形态的 401/403 都归入此类;503 之类的**瞬时故障不会被误判**成凭据问题——那需要重试,不是重新登录。
|
|
100
|
+
- **确实去探测,而不是猜**:被拒后,插件用 `credentialFor()` 取出**其他**账号的凭据(**不触碰当前选择**),向计费网关的廉价只读接口发一次真实请求,验证它是否仍然可用。有一个能用 → 卡片直说「本机账号『X』实测可用,切换即可」并给**一键切换**按钮;一个都不能用 → 如实说「还有其他账号但都没通过校验,可切换试试」;**本机再无其他账号** → 才提示重新登录。
|
|
101
|
+
- **`reloginRequired` 只在「本机没有别的账号可切换」时为真**,绝不作为「探测没验证出东西」的兜底——那正是本次要消除的误导。三种情形互斥,各有独立测试。
|
|
102
|
+
- **代价只在失败路径上**:健康账号一次探测都不发。判定按「区域 + 账号 id + 该凭据的 `lastRefreshAtMs`」缓存 60 秒(卡片会轮询),**重新登录会改变签发时间从而自动失效旧判定**,无需显式清理。
|
|
103
|
+
- **绝不自动切换**:账单落到哪个账号是用户的选择,探测只回答「切过去是否有用」,切换动作留给用户点。`credentialFor()` 独立于当前选择,正是为了在探测期间不把计费悄悄改道。
|
|
104
|
+
- **卡片的泛化提示不再与之打架**:原先「出现错误 → 重新登录 APP」在凭据被拒时会让位给上面这条具体建议;`creditsError` 仍原样保留在下方作为技术细节。
|
|
105
|
+
|
|
106
|
+
### Fixes
|
|
107
|
+
|
|
108
|
+
- **新版桌面端(Windows 先行)加密了 auth 文件的 token 字段,插件不再把「已登录」误判为「未登录」**。WorkBuddy 桌面端某个 Windows 构建起,`auth.accessToken` / `auth.refreshToken` 不再是纯字符串,而是写成 `{"$wbEncrypted":1,"envelope":"…"}` —— 一个 AES-256-GCM 信封。此前的解析器只接受字符串,遇到对象即返回「无 token」,于是**文件完全健康、账号却显示未登录**,或退回到某个早就作废的旧备份凭据上、由上游回一个 HTML 401。用户侧的「重新登录桌面端 + 重新选账号」当然修不好它:问题不在凭据,而在读不出来。
|
|
109
|
+
|
|
110
|
+
- **加密格式**:`envelope` 是 base64 的 JSON(`suite` / `keyId` / `nonce` / `authTag` / `ciphertext`),suite 1 = AES-256-GCM,12 字节 nonce、16 字节 tag。AAD 不是空串,而是一段**长度前缀转录**(域分隔符 `WB-AAD\0` + 版本 + 格式 ID `WBEV1` + scheme `sym-v1` + suite + keyId + framing 码 + 两个可选字段的缺省标记)。转录把「这段密文是为哪种 framing 封的」也绑进认证,因此把整文件信封当字段信封打开会认证失败,而不是解出半截东西。
|
|
111
|
+
- **密钥来源(本版本最关键的决定)**:字段密钥是 App 的**构建期常量**,编译在其 Electron 原生模块(`electron_browser_workbuddy_storage`)里,由 `loggerGet()` 返回载荷;App 自己对载荷里的 base64 **字符串**做 SHA-256 得到 32 字节密钥,`keyId` 是该密钥 SHA-256 的前 16 个十六进制字符。**插件因此不内置任何密钥副本**,而是在遇到加密文档时用 `ELECTRON_RUN_AS_NODE` 运行**本机已安装的那个 App**、调它自己的同一个绑定,取回同一份载荷后就地派生。也就是「向写下这个文件的那个构建本身询问」——App 升级换了密钥,插件无需跟着发版。
|
|
112
|
+
- **只在真的需要时才付代价**:先按纯字符串解析,只有文档确实带加密字段时才去问 App 要密钥,所以 macOS 与旧版 Windows(以及插件自有副本)这条常用路径**一次子进程都不会启动**。密钥只在进程内存缓存,**不落盘、不进日志**;派生结果先按 `keyId` 校验,不匹配即报「不属于这把密钥」。
|
|
113
|
+
- **fail-closed**:信封畸形、密钥不匹配、GCM tag 校验失败,一律当作「这份文档读不出来」(返回未登录),**绝不退化成截断的、空的、或半解密的 token**——那会让计费落到错误的地方。
|
|
114
|
+
- **身份与时间字段原样保留**:加密路径只替换被加密的那些字段,`uin` / `uid` / `domain` / `expiresAt` / `lastRefreshTime` 的读取与纯文本路径完全一致,多账号去重与排序不受影响(`uin` 是明文,账号 id 保持稳定,无迁移问题)。
|
|
115
|
+
- **账号名也一并解密**:这些构建**同样加密了 `account.nickname`**。只解 token 的读法会让 token 工作正常、账号名却读不出来而回退成一串 uin 数字(`330105772757`),看起来像「插件不知道这是谁」——实测确认。现在昵称走同一条解密路径,卡片与 `doctor` 显示 `老丁`、`LaoDing` 这样的名字。昵称是**可选**字段:信封打不开(密钥轮换、信封损坏)只表示「名字未知」,**不会**像 token 那样让整份凭据作废;token 依旧 fail-closed。`account.phoneNumber` 在这些构建里同样加密,**故意不读**——卡片的隐私契约只允许昵称、到期时间与积分,手机号不在其列。
|
|
116
|
+
- **uin 不再显示,也不再见诸接口**:账号名是**显示值,不是标识符**。原先的名字兜底链是 `nickname ?? uin ?? uid`,昵称一旦缺席就把一串数字当名字显示;现在 store 的名字只取昵称,**没有就返回空串**,由卡片渲染本地化的「未命名账号」占位(`doctor` 对应打印 `(unnamed)`),标识符不再冒充名字。同时把 `uin` 从下发给浏览器的账号行与登录文档里**整个移除**——卡片从未渲染过它,继续传只是在无功能收益地暴露账号标识;`uin` 仍留在凭据模型内部(账号 id 由它派生,去重与排序都靠它)。
|
|
117
|
+
- **门控随之放宽**:`hasEncryptedTokenFields` → `hasEncryptedCredentialFields`,除两个 token 外还检查 `nickname` / `uin` / `uid` / `enterpriseId` / `domain`。否则一个「token 明文、昵称加密」的构建会读不到密钥,账号名又静默退回数字。
|
|
118
|
+
- **`doctor` 新增一行**:报告「加密凭据支持」是否可用及其解析到的 App 可执行文件路径;App 装在非常规位置时可用新增的 `WORKBUDDY_APP_EXECUTABLE` 指定。该项**不读取也不输出任何密钥材料**。
|
|
119
|
+
|
|
120
|
+
**核查**:以本机真机取证——加密文件解出的 access token 长度 1462、`refreshToken` 1102,两者都是标准 JWT 形状;用解出的 token 调上游积分接口得 **HTTP 200**(`code: 0`,真实积分数据),模型目录接口同样 200。集成后 `store.accounts()` 正确列出 2 个账号(加密的那个被标为 selected),`store.resolve()` 返回可用凭据,`status` 子命令打印出真实余额。
|
|
121
|
+
|
|
122
|
+
### Tests
|
|
123
|
+
|
|
124
|
+
测试总数 179 → 236(新增 57 例,新文件 `tests/at-rest.spec.ts` 与 `tests/credential-recovery.spec.ts`):
|
|
125
|
+
|
|
126
|
+
- 密钥派生:断言哈希的是载荷里的 base64 **字符串**而非解码后的字节(两者结果不同,写错即挂)、`keyId` 规则、以及畸形载荷必须抛错而不是派生出一把假密钥。
|
|
127
|
+
- 信封开启:本文件用与服务端**同一算法独立封出**的密文往返,覆盖同一密钥下的多个字段;另含「换一把密钥必须拒绝」「篡改密文必须失败(不得返回部分明文)」「整文件 framing 的信封必须认证失败」「畸形信封逐个字段报错」。
|
|
128
|
+
- 文档解析:纯字符串文档不需要密钥即可读;**看起来像 base64 的普通 token 不得被当成密文**;加密文档在没有密钥时返回 undefined(正是原缺陷的形状);给对密钥后 token 与身份/时间字段全部正确;access 加密而 refresh 明文、以及只有 refresh 加密的混合形状;**给错密钥必须 fail-closed**;空 token 规则保持不变。
|
|
129
|
+
- 账号名:加密昵称必须还原成真名而不是退回 uin(并按卡片同一 fallback 链断言);明文昵称照常原样读;**打不开的昵称只算「未知」、不得让整份凭据作废**(与 token 的 fail-closed 相反);`phoneNumber` 即使加密且可解也**不得**被读入凭据对象(隐私契约)。
|
|
130
|
+
- 门控:加密昵称/uin/uid/enterpriseId/domain 各自单独出现时都要被识别为「需要密钥」,全明文的文档必须为 false。
|
|
131
|
+
- 可执行文件探测:环境变量优先、空/空白覆盖值被丢弃、darwin 给出 bundle 路径、linux 不给候选、路径都不存在时返回 undefined。
|
|
132
|
+
- 恢复建议(`tests/credential-recovery.spec.ts`):探测命中/被拒/账号已消失三种结果;TTL 内不重复探测;**重新登录改变签发时间后旧判定自动失效**(同一账号从不可用翻回可用);判定按区域与账号互不串味;**「本机没有其他账号」才 `reloginRequired`**、有其他账号时即使一个都没验证通过也**绝不**要求重新登录(误导守卫)、且永不把「本机一个账号都没有」误报成需要重新登录之外的结论。
|
|
133
|
+
- 拒绝分类(`tests/web-status.spec.ts`):HTML 401/403 与 JSON 401 都标记 `credentialRejected` 并附建议;**503 之类的瞬时故障不得被当成凭据问题**;探测只针对**被拒账号之外**的账号;探测确认可用时给出 `usableAccount` 且不要求重新登录。
|
|
134
|
+
- 账号名与标识符(`tests/auth.spec.ts` / `tests/web-status.spec.ts`):有昵称时用昵称;**昵称缺席时 `accountName` 必须是空串,绝不含 uin/uid**(并断言账号仍完全可用、id 仍正常);空字符串昵称与缺失同义;`uin` 仍留在凭据模型里供 id 派生使用;**下发给浏览器的文档与账号行都不得出现 `uin` 字段,也不得出现 uin/uid 的数字**。
|
|
135
|
+
|
|
136
|
+
每条守卫都用「删掉对应实现即失败」验证过:把密钥派生改回哈希解码字节、或让解析器忽略 `keyId` 校验、或去掉 `hasEncryptedCredentialFields` 的门控、或让 `optionalCredentialField` 的失败向上抛、或把 `reloginRequired` 改成「没验证出来就算 true」、或把名字兜底链改回 `nickname ?? uin`,都会挂掉对应用例。
|
|
137
|
+
|
|
138
|
+
### Docs
|
|
139
|
+
|
|
140
|
+
- `README.md` / `README.en.md`:新增「加密凭据字段」说明(为什么要向本机 App 现取密钥、密钥不落盘、纯文本路径不启子进程、账号名也来自加密字段、`WORKBUDDY_APP_EXECUTABLE`),在「已知限制」补一条「加密凭据依赖桌面 App 在场」,并在「功能特性」补一条「凭据失效时给出可执行的建议」。
|
|
141
|
+
- `docs/DESIGN.md`:§5.3 源文件标注表新增 `src/at-rest.ts` 一行(原创,无参考实现);§6 风险与边界新增第 5 条「字段加密随版本轮换」,说明为何不得把密钥内置进插件。
|
|
142
|
+
|
|
3
143
|
## 2.0.6 (2026-09-22)
|
|
4
144
|
|
|
5
145
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -23,10 +23,12 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
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.). **A region's models appear in the DSH model picker when that region has an account**: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. **A region with no local sign-in lists no models** — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card and account picker are how you sign in. Model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu.
|
|
25
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.
|
|
26
|
+
- **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
|
|
26
27
|
- **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).
|
|
27
28
|
- **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.
|
|
29
|
+
- **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
|
|
28
30
|
- **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
|
|
29
|
-
- **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card.
|
|
31
|
+
- **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card. **When nothing is found, the card lists which paths were probed and why each one failed**: five distinct reasons (absent / unreadable / no usable token / encrypted with no key available / belongs to the other region), so the easiest case to misdiagnose — signed in, but the desktop app is not present — says the app must be there instead of telling you to sign in again. The **belongs to the other region** reason is different in kind: that sign-in is real and usable, it is simply filed under the other tab, so the card states the fix outright — switch tabs, no need to sign in again. The list says it once: the paragraph states the conclusion, the collapsed list supplies the per-path detail, and the two never repeat each other.
|
|
30
32
|
- **Secure loopback shim** — one random port + in-process random secret per region; the real WorkBuddy token is never handed to pi-ai.
|
|
31
33
|
- **Command-line diagnostics** — `status` / `doctor` / `logout`, reporting sign-in and credits per region, without a browser.
|
|
32
34
|
|
|
@@ -48,6 +50,8 @@ The model catalog comes from each region's correct source: CN reads `/v2/enterpr
|
|
|
48
50
|
|
|
49
51
|
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.
|
|
50
52
|
|
|
53
|
+
**Encrypted credential fields**: newer desktop builds (Windows first) replace the auth file's `accessToken` / `refreshToken` strings with an AES-256-GCM envelope (`{"$wbEncrypted":1,"envelope":"…"}`), and the account's **display name (`nickname`) is encrypted the same way** — left unopened it degrades to a bare uin number. The envelope is sealed with a field key **compiled into the app itself**, not a user secret — so the plugin ships no key copy. When it meets an encrypted document it runs the installed app with `ELECTRON_RUN_AS_NODE` and calls that app's own native binding (`electron_browser_workbuddy_storage.loggerGet()`) to fetch the same payload and derive the key locally: it asks the very build that wrote the file. The key is cached in process memory only — never on disk, never in logs. Plain-string documents (macOS, older Windows builds) take the original path and never spawn a child process. `account.phoneNumber` is encrypted there too, and the plugin deliberately **neither reads nor displays it**. If the app lives somewhere unusual, point `WORKBUDDY_APP_EXECUTABLE` at its executable.
|
|
54
|
+
|
|
51
55
|
## Install
|
|
52
56
|
|
|
53
57
|
Prerequisite: the WorkBuddy desktop app is installed and signed in (the plugin reuses the app's sign-in state).
|
|
@@ -119,6 +123,7 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
119
123
|
- Users upgrading from before 2.0.0 may still carry the old top-level `accountId` (the migration source). It applies only to its own region and **stops applying once that region is explicitly cleared (`accounts.<region> = ""`)** — otherwise the account you cleared would be silently restored at the next startup. The card shows whether a region is "using the saved account" or "following the app's current sign-in": the restored default is usually the very account you just cleared, so without that line there is no way to tell a successful clear from a no-op.
|
|
120
124
|
- **On Windows, an account change may fail to save while `settings.yaml` is held open.** DSH updates that file by writing a temp file and replacing the target; an antivirus scanner, a sync client (OneDrive), or an open editor can lock it briefly, and once the replacement retries are exhausted the failed write does **not** make `set()` reject. The plugin therefore reads the value back after writing: it shows "cleared" **only once the write is confirmed on disk**, and on failure it reports the error and keeps your previous choice (never a false confirmation that the old selection silently contradicts). Close whatever holds the file and retry.
|
|
121
125
|
- Windows and Linux credential paths are derived from platform conventions and are not verified on real hardware; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
|
|
126
|
+
- **Encrypted credentials require the desktop app to be present.** Newer desktop builds encrypt the token fields in the auth file, and the plugin needs that app's own native binding to obtain the field key. If the app is not installed, was uninstalled, or lives outside the probed paths (set `WORKBUDDY_APP_EXECUTABLE`), an encrypted file cannot be read — the account then shows as signed out, and the plugin **never** degrades into emitting a truncated token. `doctor` reports whether this capability is available. The key is a build-time constant that may rotate with app releases; the plugin fetches it from the local app each run and caches nothing on disk, so an app upgrade needs no plugin upgrade.
|
|
122
127
|
|
|
123
128
|
## Disclaimer
|
|
124
129
|
|
package/README.md
CHANGED
|
@@ -29,10 +29,12 @@
|
|
|
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 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片与账号选择器正是登录的入口。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致。
|
|
31
31
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
32
|
+
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
32
33
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;图片输入按模型手动勾选(默认不勾选)。
|
|
33
34
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
35
|
+
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
34
36
|
- **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
|
|
35
|
-
- **凭据路径多候选** —— macOS / Windows / Linux
|
|
37
|
+
- **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。**未登录时卡片会列出「探测过哪些路径、各自为什么没成」**:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。**属于另一个地区**那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。
|
|
36
38
|
- **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。
|
|
37
39
|
- **命令行诊断** —— `status` / `doctor` / `logout`,按区域报告登录与积分,无需浏览器即可确认宿主状态。
|
|
38
40
|
|
|
@@ -54,6 +56,8 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
54
56
|
|
|
55
57
|
凭据读取自 WorkBuddy 桌面 App 自身的 auth 文件(只读);刷新得到的 token 按区域存放在 `$DSH_HOME/.workbuddy-auth.cn.json` 与 `$DSH_HOME/.workbuddy-auth.global.json`(双账号同时在线互不覆盖;旧的单文件 `.workbuddy-auth.json` 作为迁移来源保留读取),桌面端文件永不被写入。
|
|
56
58
|
|
|
59
|
+
**加密凭据字段**:新版桌面端(Windows 先行)把 auth 文件里的 `accessToken` / `refreshToken` 从纯字符串改为 `{"$wbEncrypted":1,"envelope":"…"}` 的 AES-256-GCM 信封,**账号昵称 `nickname` 同样被加密**(不解密就会退回成一串 uin 数字)。信封用的是 App **自身构建期内置**的字段密钥,并非用户密钥,因此插件不内置任何密钥副本,而是在需要时用 `ELECTRON_RUN_AS_NODE` 调用已安装 App 的原生绑定(`electron_browser_workbuddy_storage.loggerGet()`)取回同一份载荷并就地派生密钥——也就是「向写下这个文件的那个构建本身询问」。密钥只在本进程内存中缓存,不落盘、不进日志;纯字符串文件(macOS 与旧版 Windows)走原路径,一次子进程都不会启动。其中的 `account.phoneNumber` 虽同样加密,插件**不读取也不显示**。若 App 安装在非常规位置,用 `WORKBUDDY_APP_EXECUTABLE` 指定其可执行文件。
|
|
60
|
+
|
|
57
61
|
## 安装
|
|
58
62
|
|
|
59
63
|
前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态)。
|
|
@@ -127,6 +131,7 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
|
127
131
|
- 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
|
|
128
132
|
- **Windows:设置文件 `settings.yaml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
|
|
129
133
|
- Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
|
|
134
|
+
- **加密凭据依赖桌面 App 在场**:新版桌面端加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
|
|
130
135
|
|
|
131
136
|
## 免责声明
|
|
132
137
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { I as WORKBUDDY_APP_EXECUTABLE_ENV, O as WorkBuddyUpstreamClient, S as legacyWorkbuddyOwnAuthPath, T as workbuddyOwnAuthPath, V as findWorkbuddyAppExecutable, 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, y as defaultDesktopAuthDirs } from "./host-heartbeat-DPbL9oi2.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -63,6 +63,7 @@ async function doctor(jsonOutput) {
|
|
|
63
63
|
const desktopPresent = await anyStore.desktopFilePresent();
|
|
64
64
|
const heartbeat = await readHostHeartbeat();
|
|
65
65
|
const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
|
|
66
|
+
const appExecutable = findWorkbuddyAppExecutable();
|
|
66
67
|
const regionLists = await Promise.all(REGIONS.map(async (region) => ({
|
|
67
68
|
region,
|
|
68
69
|
accounts: await makeStore(region).accounts()
|
|
@@ -79,6 +80,16 @@ async function doctor(jsonOutput) {
|
|
|
79
80
|
candidates: defaultDesktopAuthCandidates(),
|
|
80
81
|
present: desktopPresent
|
|
81
82
|
},
|
|
83
|
+
/**
|
|
84
|
+
* Whether the encrypted-credential path is available. The desktop app
|
|
85
|
+
* encrypts token fields on Windows builds; opening them needs that same
|
|
86
|
+
* app, so an install location is exactly what this records. No key
|
|
87
|
+
* material is read or reported here.
|
|
88
|
+
*/
|
|
89
|
+
atRestDecryption: {
|
|
90
|
+
appExecutable: appExecutable ?? `(not found; set WORKBUDDY_APP_EXECUTABLE)`,
|
|
91
|
+
available: appExecutable !== void 0
|
|
92
|
+
},
|
|
82
93
|
ownAuthFiles: {
|
|
83
94
|
cn: workbuddyOwnAuthPath("cn"),
|
|
84
95
|
global: workbuddyOwnAuthPath("global"),
|
|
@@ -108,6 +119,7 @@ async function doctor(jsonOutput) {
|
|
|
108
119
|
hints: [
|
|
109
120
|
...anySignedIn ? [] : ["Sign in once in the WorkBuddy desktop app (either region), then run status again."],
|
|
110
121
|
...desktopPresent ? [] : [`No WorkBuddy desktop auth file at the expected path; set ${WORKBUDDY_AUTH_FILE_ENV} if it lives elsewhere.`],
|
|
122
|
+
...appExecutable === void 0 ? [`The WorkBuddy desktop app was not found, so encrypted credential fields cannot be opened; set ${WORKBUDDY_APP_EXECUTABLE_ENV} to its executable if it is installed elsewhere.`] : [],
|
|
111
123
|
...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."]
|
|
112
124
|
]
|
|
113
125
|
};
|
|
@@ -115,8 +127,9 @@ async function doctor(jsonOutput) {
|
|
|
115
127
|
else process.stdout.write([
|
|
116
128
|
`WorkBuddy Connect ${WORKBUDDY_CONNECT_VERSION} on ${process.version}`,
|
|
117
129
|
`Desktop auth file: ${report.desktopAuthFile.present ? "present" : "missing"} (${report.desktopAuthFile.path})`,
|
|
130
|
+
`Encrypted-credential support: ${report.atRestDecryption.available ? "available" : "unavailable"} (${report.atRestDecryption.appExecutable})`,
|
|
118
131
|
`Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : heartbeat !== void 0 ? "stale heartbeat (process exited)" : "not started"}`,
|
|
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()}`)]),
|
|
132
|
+
...regionLists.flatMap(({ region, accounts }) => [`${REGION_LABELS[region]} accounts: ${accounts.length} (own copy ${workbuddyOwnAuthPath(region)})`, ...accounts.map((account) => ` - ${account.accountName === "" ? "(unnamed)" : account.accountName} (${account.id})${account.selected ? " [selected]" : ""} expires ${new Date(account.tokenExpiresAtMs).toISOString()}`)]),
|
|
120
133
|
`Static fallback models: CN ${report.fallbackModels.cn}, Global ${report.fallbackModels.global}`,
|
|
121
134
|
...report.hints.map((hint) => `Hint: ${hint}`),
|
|
122
135
|
""
|