dsh-connect-workbuddy 3.4.1 → 3.4.3
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 +50 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/WINDOWS.md +6 -0
- package/lib/bin.js +12 -4
- package/lib/client.js +6 -0
- package/lib/{host-heartbeat-DSO-07lc.js → host-heartbeat-D5ArLcFf.js} +245 -6
- package/lib/index.d.ts +18 -6
- package/lib/index.js +53 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.3 (2026-10-03)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **装在非默认目录的 Windows 安装也能找到了:注册表回退(issue #30)。**
|
|
8
|
+
- **现象**:WorkBuddy 桌面端装在 `E:\workbuddy`、`F:\AGENTS\WorkBuddy` 这类非默认位置时,插件报「未登录」——而用户明明已经登录。
|
|
9
|
+
- **根因**:`workbuddyAppExecutableCandidates()` 只探四个**默认布局**路径(`%LOCALAPPDATA%\Programs\WorkBuddy`、`%LOCALAPPDATA%\WorkBuddy`、`%ProgramFiles%\WorkBuddy`、`%ProgramFiles(x86)%\WorkBuddy`)。装在别处时四个候选全落空,于是拿不到加密字段的密钥,**已登录的账号被报成未登录**。README 的答案是手工设 `WORKBUDDY_APP_EXECUTABLE`——而安装器其实早就把答案记在注册表里了。
|
|
10
|
+
- **修法**:四个候选全落空后,在 `win32` 上查询卸载注册表(`HKCU` / `HKLM` / `HKLM\WOW6432Node` 三处),从 `DisplayIcon` 优先、`InstallLocation` 兜底推出可执行文件路径。路径必须 basename 为 `WorkBuddy.exe` 且 `existsSync` 通过才会被采用(该路径最终会传给 `execFile`);注册表只是**回退**,默认安装零开销。`queryRegistry` 作为可注入 seam,测试不依赖主机状态。
|
|
11
|
+
- `doctor` 现在同时报告**来源**(`via env` / `default-layout` / `registry`):只看路径的话,「默认布局命中」和「回退救了你」输出的字符串一模一样,而这正是用户排查时唯一有用的区别。
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- **Windows 路径不再用宿主平台的分隔符构造(非 Windows 上会静默失效)。**
|
|
16
|
+
- **现象**:在 Linux/macOS 上构建/运行时,Windows 专用路径会带上 `/` 分隔符,永远不可能命中真实安装。
|
|
17
|
+
- **根因**:`workbuddyAppExecutableCandidates()` 的四个 Windows 候选、以及注册表回退的 `normalizeRegistryExecutable()` / `registryInstallLocationFromQuery()`,都用了 `path.join` / `path.basename`——它们跟随**运行平台**,不是数据的平台。在 POSIX 宿主上 `basename('E:\workbuddy\WorkBuddy.exe')` 返回整串,于是**每一个合法路径都被 basename 白名单拒绝**,注册表回退等于从未生效。
|
|
18
|
+
- **修法**:凡处理 Windows 数据一律改用 `path.win32.join` / `path.win32.basename`——**分隔符由数据决定,不由宿主决定**。
|
|
19
|
+
- **测试同样是错的**:期望值也用 `join()` 构造,生产与测试一起漂移,所以本地绿、CI(ubuntu)绿,而 Windows 上根本不命中。已改为 `win32.*` 并补一条**形状守卫**(断言每个 Windows 候选都不含 `/`、末段为 `WorkBuddy.exe`),变异验证(把 `win32.join` 换回 `join`)必红。
|
|
20
|
+
- 顺带修正一条与环境耦合的断言:原测试断言 `darwin` 返回 `undefined`,但本机装了 WorkBuddy 时 macOS 分支会**合法**命中,于是「开发者机器红、CI 绿」。该用例真正要断言的是注册表 seam 未被调用,已按此重写。
|
|
21
|
+
|
|
22
|
+
### Fixes
|
|
23
|
+
|
|
24
|
+
- **403「request illegal」(11140)不再伪装成登录故障。**
|
|
25
|
+
|
|
26
|
+
国际网关的真实失败(2026-09-29 抓包):同一凭据下 `GET /v3/config` 返回 200,聊天 `POST /v2/chat/completions` 却被 403 拒绝,响应体逐字为:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"code":11140,"msg":"request illegal","requestId":"3498bf50-98a9-4746-962e-c14016b8c578","displayMsg":{"en":"The content did not pass the safety review. Please adjust and retry.","zh":"内容未通过安全审核,请调整后重试。"},"actions":["SUBMIT_FEEDBACK","COPY_ERROR","EDIT_INPUT"]}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **根因**:`classifyUpstreamError` 只认积分、会话失效、限流三类标记,11140 落进兜底的 `client`;shim 把 `client` 映射成 400,DSH 侧进一步归为 AUTH。用户看到的是「登录坏了」——实际是服务端内容策略拒绝,重新登录、刷新 token 都不会有效。
|
|
33
|
+
- **改动**:
|
|
34
|
+
- `upstream.ts` 新增 `policy_reject` 分类(标记 `request illegal` / `"code":11140`,在通用 4xx 之前判定)与 `parseUpstreamErrorDetail()`(从 JSON 失败体提取 `code` / `requestId` / `displayMsg`,非 JSON 返回 `undefined`,不猜);chat 失败结果附带 `detail`。
|
|
35
|
+
- `shim.ts` 把 `policy_reject` 映射回 403,错误消息从解析字段合成:官方文案(zh 优先,回落 en、`msg`)+ `code 11140` + `requestId` +「重新登录不会解决;可在桌面端用同一账号验证,或切换区域/账号后重试」;不再把整段原始 JSON 嵌进 message。
|
|
36
|
+
- **写回宿主的 `code` 是 `policy_reject`,不是上游的数值码 11140**(2026-10-03 适配 3.4.1 的 `hostErrorCode`)。宿主按稳定的机器可读类别路由,`11140` 不是它认识的类别;而 3.4.1 建立的规则是「只有 429/6004 需要翻译成宿主认得的词」。`policy_reject` 在 403 下本就落在重试集合之外,**不需要**翻译;更要紧的是**不能**被翻译进集合——内容策略拒绝是确定性的,原地重试只会把预算烧在一次必然再被拒的请求上,跨供应商故障转移也治不了策略判定。
|
|
37
|
+
- 探针 `outcomeOfFailure` 在 401/403 一刀切**之前**先判 `policy_reject`,卡片新增「服务端按内容策略拒绝」文案(中英)。探针与真实请求复用同一分类函数,对同一响应的判定不会分歧。
|
|
38
|
+
- **账号池不再因一次策略拒绝把健康账号永久踢出**(适配 3.4.2 时发现的连带问题):`policy-rejected` 是新 outcome,在 `exclusionOf` 里落进了 `default` 分支被标成 `unusable`——而 `unusable` 刻意**不带冷却**,于是账号会被**永久**移出池子,且卡片会显示「不可用」。这是错的:策略拒绝针对的是**那一条请求的内容**,不是这个账号,同一个账号换一条消息照样能服务。现按 `ok` 处理(测量照常记录,卡片能解释发生了什么,但不影响它继续被计费),并补守卫 + 变异验证。
|
|
39
|
+
- **守卫(先红后绿)**:`upstream.spec` / `shim.spec` / `probe.spec` 新增 8 例——分类(完整/最小 403 体 → `policy_reject`)、字段解析(zh 优先、缺 `displayMsg` 回落 `msg`、非 JSON 为 `undefined`)、shim 端到端(重放上面那段真实 403 体,断言 403 + `policy_reject` + **`code` 为 `policy_reject`** + 官方文案 + requestId,而不是 400)、探针(11140 → `policy-rejected`)。7 例先在未修复代码上跑红、实现后转绿;1 例良性对照(良性 403 / HTML 403 仍判 `client` / `credential-rejected`)双向保持绿——防止修复把无辜的 403 一起吞掉。
|
|
40
|
+
|
|
41
|
+
## 3.4.2 (2026-10-03)
|
|
42
|
+
|
|
43
|
+
### Fixes
|
|
44
|
+
|
|
45
|
+
- **换号重试之间补上 2 秒间隔,不再零延迟连发。**
|
|
46
|
+
- **现象**:一次请求撞上限流后,换号循环会**零间隔**地把池里剩下的账号挨个试一遍——实测 4 个账号在 **35 秒内全部失败**(最后两次只隔 1 秒),而它们全部撞的是同一个 429。
|
|
47
|
+
- **根因**:上游的限流(业务码 6004)**按请求体积/频率触发**,零间隔连发等于自己把每个候选都推到同一堵墙上。批量测试早就为此在账号之间等 `POOL_BATCH_GAP_MS`(400ms),**换号循环却什么都没有**——两者的差别没有理由。
|
|
48
|
+
- **修法**:每次换号前等待 `FAILOVER_ACCOUNT_GAP_MS`(2 秒)。比批测的 400ms 更长,因为换号重试是**用户在等的真实请求**,不是探测——它需要给上游的窗口留出喘息,而不只是避免自己踩到限流。
|
|
49
|
+
- **等待是可中断的**:客户端断开时立刻结束等待并退出循环,不会白等 2 秒,也不会拿一个已经作废的 signal 去把剩余账号试完。
|
|
50
|
+
- 顺带修正:等待结束后重新检查一次中断状态——否则客户端已断开时,剩余账号会被逐个发出去(每个都立即因 signal 作废而失败),白白写下一串假的账号失败测量。
|
|
51
|
+
- 测试:新增一条用例钉住「两次尝试之间确实等待了 ≥1.9 秒」,变异验证(把间隔改成 0)必红。
|
|
52
|
+
|
|
3
53
|
## 3.4.1 (2026-10-02)
|
|
4
54
|
|
|
5
55
|
### Fixes
|
|
@@ -50,7 +100,6 @@
|
|
|
50
100
|
|
|
51
101
|
- README 的「账号为什么出池」表按新规则改写:区分「限流/欠费/瞬时故障会自动回池」与「凭据被拒/模型不存在只能手动重测」,并删掉已不存在的「定时器」表述。
|
|
52
102
|
- 清理死代码:`POOL_TICK_MS`、`POOL_MIN_INTERVAL_MINUTES`、`poolDueAt`、`duePoolRegions`、`PoolScheduleInput` / `PoolScheduleResult` 及其单元测试全部删除。它们自 3.2.0 起已无任何生产调用(构建时已被 tree-shake),却仍被测试覆盖——于是「测试全绿」对「账号会不会回来」这个问题什么也没说明。
|
|
53
|
-
|
|
54
103
|
## 3.3.0 (2026-10-01)
|
|
55
104
|
|
|
56
105
|
### Features
|
package/README.en.md
CHANGED
|
@@ -249,9 +249,10 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
249
249
|
- 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.
|
|
250
250
|
- **On Windows, an account change may fail to save while the profile's `cordis.patch.yml` 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.
|
|
251
251
|
- Windows and Linux credential paths are derived from platform conventions. On a real Windows 11 machine, `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` and `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` **did resolve**, but **the remaining candidates and the Linux paths are still not verified on real hardware**; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
|
|
252
|
-
- **Encrypted credentials require the desktop app to be present.** Newer desktop builds (**from 5.6.0, on macOS as well as Windows**) 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.
|
|
252
|
+
- **Encrypted credentials require the desktop app to be present.** Newer desktop builds (**from 5.6.0, on macOS as well as Windows**) 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 (on Windows the install registration is consulted automatically, so a non-default install is found without configuration; set `WORKBUDDY_APP_EXECUTABLE` if that misses too), 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.
|
|
253
253
|
- **The executable is located from the bundle's own declaration, not guessed from the app name.** The WorkBuddy macOS bundles declare `CFBundleExecutable` as `Electron` (not `WorkBuddy`), so the plugin reads each bundle's `Info.plist` to learn the binary's name. Both the domestic `WorkBuddy.app` and the international `WorkBuddy AI.app` are candidates, and an app filed into a subdirectory of an applications folder (e.g. `/Applications/IDE/WorkBuddy.app`) is found by a one-level scan that confirms each candidate's bundle identifier before use — so a different Electron app can never be launched by mistake.
|
|
254
254
|
- **"Signed out" and "your sign-in cannot be read" are reported separately.** When an encrypted file cannot be read, the advice is to install the desktop app or point `WORKBUDDY_APP_EXECUTABLE` at it — **not** to sign in again, which cannot help: the credential is fine, the key is what is missing.
|
|
255
|
+
- **A 403 "request illegal" (code 11140) is a server-side policy refusal, not a sign-in problem.** The chat gateway sometimes refuses a request outright under its content policy; the body reads `{"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"en":"The content did not pass the safety review. Please adjust and retry."}}`. The plugin used to file this under the generic `client` error (HTTP 400), which DSH then displayed as an AUTH failure — it looked like a broken sign-in, but **signing in again cannot fix it**: the credentials are fine; it is this one request that was refused. The refusal now passes through as 403 with the official message, `code 11140`, and the `requestId` (give that to official support). The `error.code` written back to the host is the stable class `policy_reject` rather than the numeric `11140`: the host decides in-place retrial from that class, and a policy refusal is deterministic — retrying only burns the budget, and `11140` is not a word the host knows, and the per-model "test" reports "refused by the server's content policy" instead of "token refused". Two ways out: send the same prompt from the WorkBuddy desktop app with the same account (if the desktop app is refused too, the limit is on the account/content side), or switch region/account and retry.
|
|
255
256
|
|
|
256
257
|
## Disclaimer
|
|
257
258
|
|
package/README.md
CHANGED
|
@@ -259,9 +259,10 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
|
259
259
|
- 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
|
|
260
260
|
- **Windows:profile 的配置文件 `cordis.patch.yml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
|
|
261
261
|
- Windows / Linux 的凭据默认路径按平台约定推导。已在真实 Windows 11 上确认 `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` 命中,**其余候选与 Linux 路径仍未经真机验证**;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
|
|
262
|
-
- **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App
|
|
262
|
+
- **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(**Windows 上会自动回退查安装注册表**,仍找不到时用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
|
|
263
263
|
- **App 可执行文件按 bundle 自身声明定位,不按 App 名猜**:WorkBuddy 的 macOS bundle 里 `CFBundleExecutable` 是 `Electron`(不是 `WorkBuddy`),因此插件读取 bundle 的 `Info.plist` 来决定二进制名。国内版 `WorkBuddy.app` 与国际版 `WorkBuddy AI.app` 都在候选内;App 被归入 `/Applications` 子目录(如 `/Applications/IDE/WorkBuddy.app`)时,插件还会向下扫一层并用 bundle id 确认身份后才使用——**不会**因为同名的其他 Electron 应用而误启动它。
|
|
264
264
|
- **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,而**不是**「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
|
|
265
|
+
- **403「request illegal」(code 11140)是服务端策略拒绝,不是登录问题**:聊天网关有时会按内容策略直接拒绝一次请求,响应体形如 `{"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"zh":"内容未通过安全审核,请调整后重试。"}}`。此前插件把它归为通用 `client` 错误返回 400,DSH 侧又显示成 AUTH 异常——看起来像「登录坏了」,但**重新登录解决不了它**:凭据本身是好的,被拒的是这一次请求。现在这类拒绝按 403 原样透传,消息里带官方文案、`code 11140` 和 `requestId`(联系官方支持时提供)。写回宿主的 `error.code` 是 `policy_reject` 这个稳定类别(而非数值 `11140`):宿主按类别决定要不要原地重试,而策略拒绝是确定性的——重试只会白烧预算,`11140` 也不是宿主认得的词;模型行的「测试」也显示「服务端按内容策略拒绝」而非「token 被拒」。遇到时的两个出口:在 WorkBuddy 桌面端用同一账号发同样的内容验证(桌面端同样被拒,说明限制在账号/内容侧);或切换区域/账号后重试。
|
|
265
266
|
|
|
266
267
|
## 免责声明
|
|
267
268
|
|
package/docs/WINDOWS.md
CHANGED
|
@@ -30,6 +30,12 @@ Windows 在本项目里不是「顺带能跑」的平台,而是**有专属分
|
|
|
30
30
|
|
|
31
31
|
**通用注意**:路径一律经 `path.join` 构造,**不得写死 POSIX 分隔符**。历史上正因此挂过 CI——`2.0.11` 新增的 darwin 候选断言写死了 `/Applications/WorkBuddy.app/...`,生产代码没问题(用 `join`),错的是测试:它断言的是「跑测试的机器是 POSIX」而非「候选列表对不对」,于是 Windows 必挂、macOS 恒绿。现有回归守卫:候选路径不得**混用**分隔符。
|
|
32
32
|
|
|
33
|
+
**更隐蔽的反向形态**:处理 **Windows 专用**数据(注册表值、`%LOCALAPPDATA%` 等 Windows 环境变量)时,`path.join` / `path.basename` **不是**正确答案——它们跟随**运行平台**。在 Linux/macOS 上跑,`basename('E:\workbuddy\WorkBuddy.exe')` 返回整串(`\` 在 POSIX 下不是分隔符),`join('E:\workbuddy', 'x.exe')` 产出 `E:\workbuddy/x.exe`,两者在 Windows 上都不可能命中。正确写法是 `path.win32.join` / `path.win32.basename`:**分隔符由数据决定,而不是由宿主决定**。
|
|
34
|
+
|
|
35
|
+
- 生产代码里曾有两处踩到:`workbuddyAppExecutableCandidates()` 的四个 Windows 候选(`DIRTY` 形态的分隔符 → 非 Windows 宿主上构造出的路径永远不存在,且**它的测试用 `join()` 构造期望值,以同样的方式被改坏,于是双双通过**),以及注册表回退的 `normalizeRegistryExecutable()` / `registryInstallLocationFromQuery()`。
|
|
36
|
+
- 因此**测试的期望值也必须用 `win32.*` 构造**:用 `join()` 拼 Windows 期望值时,生产与测试会一起漂移,CI 在 ubuntu 上照样绿——这正是上一条「断言的是宿主而非逻辑」的同一种错,只是方向相反。
|
|
37
|
+
- 判断标准很简单:**这个字符串里出现 `\` 或盘符了吗?**有 → `win32.*`;是 POSIX 绝对路径或拼接本机路径 → 用 `join`。
|
|
38
|
+
|
|
33
39
|
## 2. 证据分级(写结论时必须用对档位)
|
|
34
40
|
|
|
35
41
|
| 档位 | 含义 | 实例 |
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath,
|
|
2
|
+
import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, J as findWorkbuddyAppExecutableWithSource, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-D5ArLcFf.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -63,7 +63,8 @@ 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
|
|
66
|
+
const discoveredApp = findWorkbuddyAppExecutableWithSource();
|
|
67
|
+
const appExecutable = discoveredApp?.executable;
|
|
67
68
|
const regionLists = await Promise.all(REGIONS.map(async (region) => ({
|
|
68
69
|
region,
|
|
69
70
|
accounts: await makeStore(region).accounts()
|
|
@@ -106,6 +107,13 @@ async function doctor(jsonOutput) {
|
|
|
106
107
|
*/
|
|
107
108
|
atRestDecryption: {
|
|
108
109
|
appExecutable: appExecutable ?? `(not found; set WORKBUDDY_APP_EXECUTABLE)`,
|
|
110
|
+
/**
|
|
111
|
+
* Where the path came from. `registry` means the app is installed outside
|
|
112
|
+
* the default layout and the Windows uninstall registration found it; the
|
|
113
|
+
* absence of this field (older reports) is why the hint below can now name
|
|
114
|
+
* the mechanism instead of telling every user to set the env var.
|
|
115
|
+
*/
|
|
116
|
+
appExecutableSource: discoveredApp?.source,
|
|
109
117
|
available: appExecutable !== void 0
|
|
110
118
|
},
|
|
111
119
|
ownAuthFiles: {
|
|
@@ -137,7 +145,7 @@ async function doctor(jsonOutput) {
|
|
|
137
145
|
hints: [
|
|
138
146
|
...anySignedIn ? [] : ["Sign in once in the WorkBuddy desktop app (either region), then run status again."],
|
|
139
147
|
...desktopPresent ? [] : [`No WorkBuddy desktop auth file at the expected path; set ${WORKBUDDY_AUTH_FILE_ENV} if it lives elsewhere.`],
|
|
140
|
-
...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.`] : [],
|
|
148
|
+
...appExecutable === void 0 ? [`The WorkBuddy desktop app was not found, so encrypted credential fields cannot be opened. On Windows the install registration was already checked, so the app is either not installed or its uninstall entry is missing; set ${WORKBUDDY_APP_EXECUTABLE_ENV} to its executable if it is installed elsewhere.`] : [],
|
|
141
149
|
...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."],
|
|
142
150
|
...piAi.generation === "legacy" && piAi.resolvedLocally ? [`This plugin resolves pi-ai ${piAi.version ?? "(unreadable version)"} (the 0.85 context shape) while DSH 0.2.0+ hosts normalize with 0.87; the adapter bridges that gap, but if tools still misbehave, remove any nested pi-ai copy so the plugin resolves the host's.`] : []
|
|
143
151
|
]
|
|
@@ -147,7 +155,7 @@ async function doctor(jsonOutput) {
|
|
|
147
155
|
`WorkBuddy Connect ${WORKBUDDY_CONNECT_VERSION} on ${process.version}`,
|
|
148
156
|
...piAi.resolvedLocally ? [`pi-ai runtime: ${piAi.generation}${piAi.version === void 0 ? "" : ` ${piAi.version}`} (${piAi.resolvedFrom}) — ${report.piAiRuntime.adapterBehaviour}`] : [`pi-ai runtime: ${piAi.generation}, host-provided (no local copy; DSH 0.2.0+ ships 0.87.1) — passes provider contexts through unchanged`],
|
|
149
157
|
`Desktop auth file: ${report.desktopAuthFile.present ? "present" : "missing"} (${report.desktopAuthFile.path})`,
|
|
150
|
-
`Encrypted-credential support: ${report.atRestDecryption.available ? "available" : "unavailable"} (${report.atRestDecryption.appExecutable})`,
|
|
158
|
+
`Encrypted-credential support: ${report.atRestDecryption.available ? "available" : "unavailable"} (${report.atRestDecryption.appExecutable}${report.atRestDecryption.appExecutableSource === void 0 ? "" : `, via ${report.atRestDecryption.appExecutableSource}`})`,
|
|
151
159
|
`Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : heartbeat !== void 0 ? "stale heartbeat (process exited)" : "not started"}`,
|
|
152
160
|
...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()}`)]),
|
|
153
161
|
`Static fallback models: CN ${report.fallbackModels.cn}, Global ${report.fallbackModels.global}`,
|
package/lib/client.js
CHANGED
|
@@ -2073,6 +2073,10 @@ window.__ModuleLoader__.load({
|
|
|
2073
2073
|
text: t("row.probeCredentialRejected"),
|
|
2074
2074
|
tone: "bad"
|
|
2075
2075
|
};
|
|
2076
|
+
case "policy-rejected": return {
|
|
2077
|
+
text: t("row.probePolicyRejected"),
|
|
2078
|
+
tone: "bad"
|
|
2079
|
+
};
|
|
2076
2080
|
case "not-found": return {
|
|
2077
2081
|
text: t("row.probeNotFound"),
|
|
2078
2082
|
tone: "bad"
|
|
@@ -3024,6 +3028,7 @@ window.__ModuleLoader__.load({
|
|
|
3024
3028
|
"row.probeOutOfCreditUnknown": "✗ Not enough credits · the upstream gave no time for when it frees up",
|
|
3025
3029
|
"row.probeOutOfCreditAt": "✗ Not enough credits · quota refreshes {at}",
|
|
3026
3030
|
"row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
|
|
3031
|
+
"row.probePolicyRejected": "✗ WorkBuddy's server refused this request under its content policy — re-signing in won't help; try the desktop app or another region/account",
|
|
3027
3032
|
"row.probeNotFound": "✗ The upstream does not know this model",
|
|
3028
3033
|
"row.probeUnavailable": "✗ The upstream could not be reached — no usable answer came back",
|
|
3029
3034
|
"row.probeFailed": "✗ Test failed",
|
|
@@ -3200,6 +3205,7 @@ window.__ModuleLoader__.load({
|
|
|
3200
3205
|
"row.probeOutOfCreditUnknown": "✗ 积分不足 · 上游未给出何时恢复",
|
|
3201
3206
|
"row.probeOutOfCreditAt": "✗ 积分不足 · 积分包 {at} 刷新",
|
|
3202
3207
|
"row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
|
|
3208
|
+
"row.probePolicyRejected": "✗ WorkBuddy 服务端按内容策略拒绝了这次请求——重新登录无效;可在桌面端验证或切换区域/账号",
|
|
3203
3209
|
"row.probeNotFound": "✗ 上游不认识这个模型",
|
|
3204
3210
|
"row.probeUnavailable": "✗ 连不上上游——上游没有给出可用答复",
|
|
3205
3211
|
"row.probeFailed": "✗ 测试失败",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createDecipheriv, createHash } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, join, win32 } from "node:path";
|
|
5
5
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
6
6
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
7
7
|
import { execFile, execFileSync } from "node:child_process";
|
|
@@ -202,6 +202,167 @@ function macosBundleExecutable(bundle) {
|
|
|
202
202
|
return join(bundle, "Contents", "MacOS", name);
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
|
+
* The executable path WorkBuddy's own uninstall registration points at, or
|
|
206
|
+
* undefined when the app was never registered.
|
|
207
|
+
*
|
|
208
|
+
* Why this exists: the four directory candidates in
|
|
209
|
+
* {@link workbuddyAppExecutableCandidates} encode the DEFAULT install layout.
|
|
210
|
+
* An app installed anywhere else — a second drive (the observed case was
|
|
211
|
+
* `E:\workbuddy\WorkBuddy.exe`) — is installed and signed in, yet every
|
|
212
|
+
* candidate misses, so the encrypted credential file cannot be opened and a
|
|
213
|
+
* user who IS signed in is reported as signed out. The README's answer was to
|
|
214
|
+
* set `WORKBUDDY_APP_EXECUTABLE` by hand; the installer already recorded the
|
|
215
|
+
* answer, so this reads it instead.
|
|
216
|
+
*
|
|
217
|
+
* The registration is the app's own claim about itself, which is what makes it
|
|
218
|
+
* safe to hand the path to the credential probe: a display name alone would be
|
|
219
|
+
* a guess, whereas the path here was written by the installer that placed the
|
|
220
|
+
* binary. The value is still checked against {@link APP_EXECUTABLE_NAME} and for
|
|
221
|
+
* existence before use, and the callers fall through to the documented hint when
|
|
222
|
+
* it is absent.
|
|
223
|
+
*
|
|
224
|
+
* The read is done through ALL THREE uninstall views, because the hive a
|
|
225
|
+
* registration lands in depends on how the app was installed: per-user
|
|
226
|
+
* (`/currentuser`, the layout WorkBuddy uses) registers under `HKCU`, a
|
|
227
|
+
* machine-wide install under `HKLM`, and a 32-bit machine-wide one under
|
|
228
|
+
* `HKLM\...\WOW6432Node`. A hive that does not exist or is unreadable is the
|
|
229
|
+
* normal case on a machine without the app, not an error: every failure path
|
|
230
|
+
* returns undefined so the caller can try the next candidate.
|
|
231
|
+
*/
|
|
232
|
+
function windowsRegistryAppExecutable(query = queryRegistry) {
|
|
233
|
+
for (const root of [
|
|
234
|
+
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
235
|
+
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
236
|
+
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
237
|
+
]) {
|
|
238
|
+
const listing = query([
|
|
239
|
+
"query",
|
|
240
|
+
root,
|
|
241
|
+
"/s",
|
|
242
|
+
"/f",
|
|
243
|
+
"WorkBuddy",
|
|
244
|
+
"/t",
|
|
245
|
+
"REG_SZ"
|
|
246
|
+
]);
|
|
247
|
+
if (listing === void 0) continue;
|
|
248
|
+
const executable = registryExecutableFromQuery(listing);
|
|
249
|
+
if (executable !== void 0) return executable;
|
|
250
|
+
const fromDirectory = registryInstallLocationFromQuery(listing);
|
|
251
|
+
if (fromDirectory !== void 0) return fromDirectory;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Parse `reg query` output for the value that names the installed executable.
|
|
256
|
+
*
|
|
257
|
+
* `reg query` prints keys as `HKEY_CURRENT_USER\...\Uninstall\<id>` followed by
|
|
258
|
+
* indented ` ValueName REG_SZ Data` lines. Only `DisplayIcon` is
|
|
259
|
+
* trusted here: it is the one value the installer writes pointing AT the
|
|
260
|
+
* binary. The value is normalised by {@link normalizeRegistryExecutable}, which
|
|
261
|
+
* strips the quotes and the icon index and rejects anything that is not the
|
|
262
|
+
* desktop executable.
|
|
263
|
+
*/
|
|
264
|
+
function registryExecutableFromQuery(output) {
|
|
265
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
266
|
+
const match = /^\s*DisplayIcon\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
|
|
267
|
+
if (match === null) continue;
|
|
268
|
+
const executable = normalizeRegistryExecutable(match[1]);
|
|
269
|
+
if (executable !== void 0) return executable;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The `WorkBuddy.exe` under an `InstallLocation`-style registry directory.
|
|
274
|
+
*
|
|
275
|
+
* Kept separate from {@link registryExecutableFromQuery} because the two values
|
|
276
|
+
* mean different things: `DisplayIcon` NAMES a file, whereas the directory has
|
|
277
|
+
* to have {@link APP_EXECUTABLE_NAME} joined onto it, and the result must still
|
|
278
|
+
* be checked for existence by the caller's probe.
|
|
279
|
+
*
|
|
280
|
+
* `win32.join` rather than the host's `join`: the input is a Windows registry
|
|
281
|
+
* value on EVERY machine this function can be called on. `path.join` is the
|
|
282
|
+
* running platform's, so a suite on Linux/macOS would mangle `E:\workbuddy`
|
|
283
|
+
* into `E:\workbuddy/WorkBuddy.exe` and the caller's `existsSync` could never
|
|
284
|
+
* match it. The separator here is fixed by the DATA, not by the host.
|
|
285
|
+
*/
|
|
286
|
+
function registryInstallLocationFromQuery(output) {
|
|
287
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
288
|
+
const match = /^\s*InstallLocation\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
|
|
289
|
+
if (match === null) continue;
|
|
290
|
+
const directory = stripRegistryQuotes(match[1]);
|
|
291
|
+
if (directory === void 0 || directory === "") continue;
|
|
292
|
+
return win32.join(directory, APP_EXECUTABLE_NAME);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* A registry executable value reduced to a usable path, or undefined when it
|
|
297
|
+
* cannot be one.
|
|
298
|
+
*
|
|
299
|
+
* `DisplayIcon` is stored as `"E:\workbuddy\WorkBuddy.exe",0`. The quotes and
|
|
300
|
+
* the icon index are stripped, and a value that names something other than the
|
|
301
|
+
* desktop executable — an `.ico`, an uninstaller, another product's binary — is
|
|
302
|
+
* rejected rather than launched. A bare path without quotes is accepted too,
|
|
303
|
+
* because not every installer writes the indexed form.
|
|
304
|
+
*
|
|
305
|
+
* The basename test uses `win32.basename` for the reason given on
|
|
306
|
+
* {@link registryInstallLocationFromQuery}: the separator belongs to the
|
|
307
|
+
* registry value (always Windows), so the host's `basename` — which does not
|
|
308
|
+
* treat `\` as a separator — would return the whole string and reject every
|
|
309
|
+
* legitimate path.
|
|
310
|
+
*/
|
|
311
|
+
function normalizeRegistryExecutable(value) {
|
|
312
|
+
const unquoted = stripRegistryQuotes(value);
|
|
313
|
+
if (unquoted === void 0 || unquoted === "") return void 0;
|
|
314
|
+
const withoutIndex = unquoted.replace(/,\s*-?\d+\s*$/u, "");
|
|
315
|
+
if (withoutIndex === "") return void 0;
|
|
316
|
+
if (win32.basename(withoutIndex).toLowerCase() !== APP_EXECUTABLE_NAME.toLowerCase()) return void 0;
|
|
317
|
+
return withoutIndex;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The path inside a `DisplayIcon`/`InstallLocation` value, unquoted.
|
|
321
|
+
*
|
|
322
|
+
* Registry data may be written either quoted (`"E:\workbuddy\WorkBuddy.exe",0`)
|
|
323
|
+
* or bare. Only a leading quote is treated as quoting: a bare `C:\dir\a"b.exe`
|
|
324
|
+
* is a real (if unusual) filename and must not be truncated at the quote.
|
|
325
|
+
*/
|
|
326
|
+
function stripRegistryQuotes(value) {
|
|
327
|
+
const trimmed = value.trim();
|
|
328
|
+
if (trimmed === "") return void 0;
|
|
329
|
+
if (!trimmed.startsWith("\"")) return trimmed;
|
|
330
|
+
const closing = trimmed.indexOf("\"", 1);
|
|
331
|
+
if (closing === -1) return void 0;
|
|
332
|
+
return trimmed.slice(1, closing).trim();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Run `reg query` and return its stdout, or undefined when it cannot be run.
|
|
336
|
+
*
|
|
337
|
+
* `windowsHide` is REQUIRED, not cosmetic: the DSH Desktop host is an Electron
|
|
338
|
+
* GUI process with no console, so spawning a console program without it flashes
|
|
339
|
+
* a visible black window on every probe. The same reason is recorded for the
|
|
340
|
+
* heartbeat probe in `host-heartbeat.ts` and in `docs/WINDOWS.md` §1-3.
|
|
341
|
+
*
|
|
342
|
+
* `execFileSync` (not `execFile`) keeps this synchronous so the candidates list
|
|
343
|
+
* stays a pure function of its inputs: it is called during `findWorkbuddyApp`
|
|
344
|
+
* `Executable`, which `readAtRestKey()` and the `doctor` command both treat as
|
|
345
|
+
* a plain lookup. The read is bounded by the registry query's own output and
|
|
346
|
+
* happens at most once per discovery.
|
|
347
|
+
*/
|
|
348
|
+
function queryRegistry(args) {
|
|
349
|
+
try {
|
|
350
|
+
return execFileSync("reg", [...args], REGISTRY_PROBE_OPTIONS);
|
|
351
|
+
} catch {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Options for the registry probe; `windowsHide` is mandatory (see above), and
|
|
357
|
+
* the timeout matches `fetchAtRestKeyPayload()`'s: a hung `reg` must not wedge
|
|
358
|
+
* the synchronous discovery path forever.
|
|
359
|
+
*/
|
|
360
|
+
const REGISTRY_PROBE_OPTIONS = {
|
|
361
|
+
encoding: "utf8",
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
timeout: 1e4
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
205
366
|
* Candidate paths of the WorkBuddy desktop executable, in probe order.
|
|
206
367
|
*
|
|
207
368
|
* The Windows build is the one that encrypts credentials, so Windows leads;
|
|
@@ -224,7 +385,7 @@ function workbuddyAppExecutableCandidates(platform = process.platform, home = ho
|
|
|
224
385
|
const local = env["LOCALAPPDATA"]?.trim();
|
|
225
386
|
const programFiles = env["ProgramFiles"]?.trim();
|
|
226
387
|
const programFilesX86 = env["ProgramFiles(x86)"]?.trim();
|
|
227
|
-
candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
|
|
388
|
+
candidates.push(local === void 0 || local === "" ? void 0 : win32.join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : win32.join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : win32.join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : win32.join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
|
|
228
389
|
} else if (platform === "darwin") for (const name of MACOS_APP_BUNDLE_NAMES) candidates.push(readBundleExecutable(join("/Applications", name)), readBundleExecutable(join(home, "Applications", name)));
|
|
229
390
|
return candidates.filter((candidate) => candidate !== void 0 && candidate !== "");
|
|
230
391
|
}
|
|
@@ -298,13 +459,55 @@ function macosNestedAppBundles(parent) {
|
|
|
298
459
|
return bundles;
|
|
299
460
|
}
|
|
300
461
|
/**
|
|
462
|
+
* Locate the desktop executable AND say how it was found.
|
|
463
|
+
*
|
|
464
|
+
* Kept beside {@link findWorkbuddyAppExecutable} rather than folded into it
|
|
465
|
+
* because the two answer different questions: callers on the credential path
|
|
466
|
+
* only want the path, whereas `doctor` must explain the discovery. Sharing the
|
|
467
|
+
* candidate list keeps them from drifting.
|
|
468
|
+
*/
|
|
469
|
+
function findWorkbuddyAppExecutableWithSource(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
|
|
470
|
+
const fromEnv = env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
|
|
471
|
+
for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) {
|
|
472
|
+
try {
|
|
473
|
+
if (!existsSync(candidate)) continue;
|
|
474
|
+
} catch {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
executable: candidate,
|
|
479
|
+
source: fromEnv !== void 0 && fromEnv !== "" && candidate === fromEnv ? "env" : "default-layout"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (platform === "win32") {
|
|
483
|
+
const fromRegistry = readRegistryAppPath();
|
|
484
|
+
if (fromRegistry !== void 0) try {
|
|
485
|
+
if (existsSync(fromRegistry)) return {
|
|
486
|
+
executable: fromRegistry,
|
|
487
|
+
source: "registry"
|
|
488
|
+
};
|
|
489
|
+
} catch {}
|
|
490
|
+
}
|
|
491
|
+
const executable = findWorkbuddyAppExecutable(platform, home, env, readRegistryAppPath);
|
|
492
|
+
return executable === void 0 ? void 0 : {
|
|
493
|
+
executable,
|
|
494
|
+
source: "nested-bundle"
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
301
498
|
* The first candidate that exists as a file, or undefined when the desktop app
|
|
302
499
|
* is not installed where this platform expects it.
|
|
303
500
|
*/
|
|
304
|
-
function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env) {
|
|
501
|
+
function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
|
|
305
502
|
for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) try {
|
|
306
503
|
if (existsSync(candidate)) return candidate;
|
|
307
504
|
} catch {}
|
|
505
|
+
if (platform === "win32") {
|
|
506
|
+
const fromRegistry = readRegistryAppPath();
|
|
507
|
+
if (fromRegistry !== void 0) try {
|
|
508
|
+
if (existsSync(fromRegistry)) return fromRegistry;
|
|
509
|
+
} catch {}
|
|
510
|
+
}
|
|
308
511
|
if (platform === "darwin") for (const parent of ["/Applications", join(home, "Applications")]) for (const bundle of macosNestedAppBundles(parent)) {
|
|
309
512
|
const executable = macosBundleExecutable(bundle);
|
|
310
513
|
if (executable === void 0) continue;
|
|
@@ -449,12 +652,19 @@ const HARD_CREDIT_MARKERS = [
|
|
|
449
652
|
];
|
|
450
653
|
/** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
|
|
451
654
|
const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
655
|
+
/**
|
|
656
|
+
* Content-policy refusal markers from the chat gateway. The quoted-key form
|
|
657
|
+
* avoids substring hits inside unrelated values (request ids are hex and can
|
|
658
|
+
* contain `11140` by coincidence).
|
|
659
|
+
*/
|
|
660
|
+
const POLICY_REJECT_MARKERS = ["request illegal", "\"code\":11140"];
|
|
452
661
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
453
662
|
function classifyUpstreamError(status, body) {
|
|
454
663
|
if (status === 402) return "hard_credit";
|
|
455
664
|
const lower = body.toLowerCase();
|
|
456
665
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
457
666
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
667
|
+
for (const marker of POLICY_REJECT_MARKERS) if (body.includes(marker)) return "policy_reject";
|
|
458
668
|
if (status === 429) return "soft_rate";
|
|
459
669
|
if (status === 404) return "not_found";
|
|
460
670
|
if (status >= 500) return "server";
|
|
@@ -462,6 +672,34 @@ function classifyUpstreamError(status, body) {
|
|
|
462
672
|
return "client";
|
|
463
673
|
}
|
|
464
674
|
/**
|
|
675
|
+
* Parse the known-good fields out of an upstream failure body. Non-JSON
|
|
676
|
+
* bodies (HTML error pages, empty strings) yield undefined rather than a guess.
|
|
677
|
+
*/
|
|
678
|
+
function parseUpstreamErrorDetail(body) {
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = JSON.parse(body);
|
|
682
|
+
} catch {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
686
|
+
const raw = parsed;
|
|
687
|
+
let displayMsg;
|
|
688
|
+
const display = raw.displayMsg;
|
|
689
|
+
if (typeof display === "string") displayMsg = display;
|
|
690
|
+
else if (typeof display === "object" && display !== null) {
|
|
691
|
+
const localized = display;
|
|
692
|
+
const pick = localized.zh ?? localized.en;
|
|
693
|
+
if (typeof pick === "string") displayMsg = pick;
|
|
694
|
+
}
|
|
695
|
+
if (displayMsg === void 0 && typeof raw.msg === "string") displayMsg = raw.msg;
|
|
696
|
+
const detail = {};
|
|
697
|
+
if (typeof raw.code === "number") detail.upstreamCode = raw.code;
|
|
698
|
+
if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
|
|
699
|
+
if (displayMsg !== void 0) detail.displayMsg = displayMsg;
|
|
700
|
+
return detail;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
465
703
|
* Region for a login domain; an empty domain means CN (matching upstream tooling).
|
|
466
704
|
*
|
|
467
705
|
* The international product is reachable under TWO brand domains: the WorkBuddy
|
|
@@ -944,7 +1182,8 @@ var WorkBuddyUpstreamClient = class {
|
|
|
944
1182
|
ok: false,
|
|
945
1183
|
status: response.status,
|
|
946
1184
|
kind: classifyUpstreamError(response.status, text),
|
|
947
|
-
message: text
|
|
1185
|
+
message: text,
|
|
1186
|
+
detail: parseUpstreamErrorDetail(text)
|
|
948
1187
|
};
|
|
949
1188
|
}
|
|
950
1189
|
/**
|
|
@@ -2552,7 +2791,7 @@ function generationOf(version) {
|
|
|
2552
2791
|
//#endregion
|
|
2553
2792
|
//#region src/version.ts
|
|
2554
2793
|
/** The npm package version this build was produced from. */
|
|
2555
|
-
const WORKBUDDY_CONNECT_VERSION = "3.4.
|
|
2794
|
+
const WORKBUDDY_CONNECT_VERSION = "3.4.3";
|
|
2556
2795
|
//#endregion
|
|
2557
2796
|
//#region src/host-heartbeat.ts
|
|
2558
2797
|
/**
|
|
@@ -2710,4 +2949,4 @@ async function clearHostHeartbeat() {
|
|
|
2710
2949
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
2711
2950
|
}
|
|
2712
2951
|
//#endregion
|
|
2713
|
-
export {
|
|
2952
|
+
export { openEncryptedField as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, findWorkbuddyAppExecutableWithSource as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, macosNestedAppBundles as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, isWorkbuddyBundle as X, isEncryptedFieldWrapper as Y, macosBundleExecutable as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, readAtRestKey as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, workbuddyAppExecutableCandidates as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -435,7 +435,7 @@ declare class WorkBuddyCredentialStore {
|
|
|
435
435
|
/** WorkBuddy region selected by the credential's login domain. */
|
|
436
436
|
type WorkBuddyRegion = 'cn' | 'global';
|
|
437
437
|
/** Upstream failure classes the shim maps onto distinct HTTP answers. */
|
|
438
|
-
type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
|
|
438
|
+
type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'policy_reject' | 'not_found' | 'server' | 'client';
|
|
439
439
|
/** Reasoning capability as the upstream catalog declares it. */
|
|
440
440
|
interface WorkBuddyReasoning {
|
|
441
441
|
supportedEfforts?: readonly string[];
|
|
@@ -532,6 +532,8 @@ type WorkBuddyChatResult = {
|
|
|
532
532
|
status: number;
|
|
533
533
|
kind: UpstreamErrorKind;
|
|
534
534
|
message: string;
|
|
535
|
+
/** Structured fields lifted from a JSON failure body, when there were any. */
|
|
536
|
+
detail?: UpstreamErrorDetail | undefined;
|
|
535
537
|
};
|
|
536
538
|
/**
|
|
537
539
|
* Chat answer that also carries the response headers.
|
|
@@ -554,6 +556,15 @@ interface WorkBuddyProbeAnswer {
|
|
|
554
556
|
}
|
|
555
557
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
556
558
|
declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
|
|
559
|
+
/** Structured fields lifted out of a JSON upstream failure body. */
|
|
560
|
+
interface UpstreamErrorDetail {
|
|
561
|
+
/** Numeric upstream error code, e.g. 11140 for a content-policy refusal. */
|
|
562
|
+
upstreamCode?: number;
|
|
563
|
+
/** Upstream request id — the handle support asks for. */
|
|
564
|
+
requestId?: string;
|
|
565
|
+
/** Best human-facing message: `displayMsg.zh`, falling back to `.en`, then `msg`. */
|
|
566
|
+
displayMsg?: string;
|
|
567
|
+
}
|
|
557
568
|
/**
|
|
558
569
|
* Region for a login domain; an empty domain means CN (matching upstream tooling).
|
|
559
570
|
*
|
|
@@ -1170,7 +1181,7 @@ declare function macosNestedAppBundles(parent: string): string[];
|
|
|
1170
1181
|
* The first candidate that exists as a file, or undefined when the desktop app
|
|
1171
1182
|
* is not installed where this platform expects it.
|
|
1172
1183
|
*/
|
|
1173
|
-
declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
1184
|
+
declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv, readRegistryAppPath?: () => string | undefined): string | undefined;
|
|
1174
1185
|
/**
|
|
1175
1186
|
* Ask the installed desktop app for its key payload by running its own binary
|
|
1176
1187
|
* as plain Node (`ELECTRON_RUN_AS_NODE`) and calling the native binding.
|
|
@@ -1387,7 +1398,7 @@ declare function probeRequestBody(modelId: string): string;
|
|
|
1387
1398
|
* outcome the feature exists for: it means "this model works, but not right
|
|
1388
1399
|
* now". Everything else is a real, actionable failure or a success.
|
|
1389
1400
|
*/
|
|
1390
|
-
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1401
|
+
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1391
1402
|
/**
|
|
1392
1403
|
* One probe result, as the card renders it.
|
|
1393
1404
|
*
|
|
@@ -1455,9 +1466,10 @@ declare function cooldownOf(input: {
|
|
|
1455
1466
|
*
|
|
1456
1467
|
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
1457
1468
|
* probe and a real chat request can never disagree about what the same upstream
|
|
1458
|
-
* answer means. The extra
|
|
1469
|
+
* answer means. The extra splits here are `credential-rejected`, which the
|
|
1459
1470
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
1460
|
-
* different advice (re-auth, not "wait")
|
|
1471
|
+
* different advice (re-auth, not "wait"), and `policy-rejected`, a JSON 403
|
|
1472
|
+
* content-policy refusal where re-auth advice would be actively misleading.
|
|
1461
1473
|
*/
|
|
1462
1474
|
declare function outcomeOfFailure(status: number, body: string): WorkBuddyProbeOutcome;
|
|
1463
1475
|
/**
|
|
@@ -1902,7 +1914,7 @@ type WorkBuddyWebUsage =
|
|
|
1902
1914
|
* here because this module is the node-free bridge and must not import the
|
|
1903
1915
|
* Host-side probe module (which pulls in the credential type).
|
|
1904
1916
|
*/
|
|
1905
|
-
type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1917
|
+
type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1906
1918
|
/** One model's probe result, as the card renders it. */
|
|
1907
1919
|
interface WorkBuddyWebProbeResult {
|
|
1908
1920
|
modelId: string;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as openEncryptedField, A as CREDENTIAL_REJECTED_CODE, B as prepareChatBody, C as defaultDesktopAuthPath, D as parseWorkBuddyAuth, E as legacyWorkbuddyOwnAuthPath, F as declaredTools, G as deriveAtRestKeyId, H as WORKBUDDY_APP_EXECUTABLE_ENV, I as isCredentialRejectedError, K as fetchAtRestKeyPayload, L as parseCreditMultiplier, M as WorkBuddyCredentialRejectedError, N as WorkBuddyUpstreamClient, O as workbuddyAccountId, P as classifyUpstreamError, Q as macosNestedAppBundles, R as parseReasoning, S as defaultDesktopAuthDirs, T as isEncryptedCredentialError, U as clearAtRestKeyCache, V as regionOf, W as deriveAtRestKey, X as isWorkbuddyBundle, Y as isEncryptedFieldWrapper, Z as macosBundleExecutable, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as authFileName, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as readAtRestKey, f as WorkBuddyCatalog, g as WORKBUDDY_AUTH_FILENAME, h as ENCRYPTED_CREDENTIAL_CODE, i as processStartTimeMs, j as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, m as fallbackModelsFor, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as deriveCatalog, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as workbuddyAppExecutableCandidates, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, w as hasEncryptedCredentialFields, x as defaultDesktopAuthCandidates, y as WorkBuddyEncryptedCredentialError, z as parseUpstreamModel } from "./host-heartbeat-D5ArLcFf.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1118,6 +1118,19 @@ function finish(parts, calls) {
|
|
|
1118
1118
|
* @module dsh-connect-workbuddy/shim
|
|
1119
1119
|
*/
|
|
1120
1120
|
const REQUEST_BODY_LIMIT = 67108864;
|
|
1121
|
+
/**
|
|
1122
|
+
* Pause between failover attempts, after one account's request failed.
|
|
1123
|
+
*
|
|
1124
|
+
* The upstream's rate limit (6004) fires on request volume — firing retries at
|
|
1125
|
+
* zero gap makes every candidate hit the same wall, and 4 accounts can all fail
|
|
1126
|
+
* in under a second. The batch test already waits `POOL_BATCH_GAP_MS` (400ms)
|
|
1127
|
+
* between accounts for exactly this reason; the failover loop used to wait
|
|
1128
|
+
* nothing. 2 seconds is longer than the batch's 400 because a failover retry is
|
|
1129
|
+
* a real request the user is waiting on, not a probe — it needs enough room for
|
|
1130
|
+
* the upstream's window to breathe, not just enough to avoid self-inflicted
|
|
1131
|
+
* rate-limiting.
|
|
1132
|
+
*/
|
|
1133
|
+
const FAILOVER_ACCOUNT_GAP_MS = 2e3;
|
|
1121
1134
|
/** Loopback hostnames the shim's own in-process client uses. */
|
|
1122
1135
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
1123
1136
|
"127.0.0.1",
|
|
@@ -1167,6 +1180,7 @@ const KIND_STATUS = {
|
|
|
1167
1180
|
hard_credit: 402,
|
|
1168
1181
|
soft_rate: 429,
|
|
1169
1182
|
session_dead: 401,
|
|
1183
|
+
policy_reject: 403,
|
|
1170
1184
|
not_found: 502,
|
|
1171
1185
|
server: 502,
|
|
1172
1186
|
client: 400
|
|
@@ -1199,6 +1213,16 @@ const KIND_STATUS = {
|
|
|
1199
1213
|
* Every other kind already lands outside the retry set by its status alone
|
|
1200
1214
|
* (`hard_credit` 402 → quota, `session_dead` 401 → auth, 502/400 → other), so
|
|
1201
1215
|
* none of them is listed: a translation nobody needs is a lie waiting to drift.
|
|
1216
|
+
*
|
|
1217
|
+
* `policy_reject` is deliberately NOT listed either, for a sharper reason than
|
|
1218
|
+
* the others: the obvious candidate translation would be the upstream's own
|
|
1219
|
+
* numeric code (11140), but that is not a class the host knows, so it would take
|
|
1220
|
+
* the host's generic path and add nothing over `policy_reject`. It needs no
|
|
1221
|
+
* translation to stay outside the retry set (403), and it must NOT be translated
|
|
1222
|
+
* INTO the set — a content-policy refusal is deterministic, so retrying it in
|
|
1223
|
+
* place would spend the retry budget on a request the server will refuse again,
|
|
1224
|
+
* and cross-provider failover cannot help a policy decision either. The code the
|
|
1225
|
+
* host receives therefore names the class honestly.
|
|
1202
1226
|
*/
|
|
1203
1227
|
const KIND_HOST_CODE = { soft_rate: "quota_exceeded" };
|
|
1204
1228
|
/**
|
|
@@ -1745,6 +1769,16 @@ function createWorkBuddyShim(options) {
|
|
|
1745
1769
|
const next = await failoverAccount(triedAccountIds).catch(() => void 0);
|
|
1746
1770
|
if (next === void 0) break;
|
|
1747
1771
|
if (triedAccountIds.includes(workbuddyAccountId(next))) break;
|
|
1772
|
+
if (controller.signal.aborted) break;
|
|
1773
|
+
await new Promise((resolve) => {
|
|
1774
|
+
if (controller.signal.aborted) return resolve();
|
|
1775
|
+
const timer = setTimeout(resolve, FAILOVER_ACCOUNT_GAP_MS);
|
|
1776
|
+
controller.signal.addEventListener("abort", () => {
|
|
1777
|
+
clearTimeout(timer);
|
|
1778
|
+
resolve();
|
|
1779
|
+
}, { once: true });
|
|
1780
|
+
});
|
|
1781
|
+
if (controller.signal.aborted) break;
|
|
1748
1782
|
logger?.warn(`dsh-connect-workbuddy: retrying chat on another pool account after ${result.kind} (http ${result.status})`);
|
|
1749
1783
|
credential = next;
|
|
1750
1784
|
result = await client.chatStream(credential, prepared, controller.signal);
|
|
@@ -1754,6 +1788,16 @@ function createWorkBuddyShim(options) {
|
|
|
1754
1788
|
if (!result.ok) {
|
|
1755
1789
|
const attempts = triedAccountIds.length;
|
|
1756
1790
|
const note = attempts > 1 ? ` (after trying ${attempts} accounts)` : "";
|
|
1791
|
+
if (result.kind === "policy_reject") {
|
|
1792
|
+
const detail = result.detail;
|
|
1793
|
+
const lead = detail?.displayMsg ?? result.message.slice(0, 200);
|
|
1794
|
+
const meta = ["服务端策略拒绝"];
|
|
1795
|
+
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
1796
|
+
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
1797
|
+
const message = `${lead}(${meta.join(",")})${note}。该请求被 WorkBuddy 服务端策略拒绝,重新登录不会解决;可在 WorkBuddy 桌面端用同一账号验证,或切换区域/账号后重试`;
|
|
1798
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1757
1801
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status})${note}: ${result.message.slice(0, 400)}`, hostErrorCode(result.kind));
|
|
1758
1802
|
return;
|
|
1759
1803
|
}
|
|
@@ -2003,12 +2047,16 @@ function cooldownOf(input) {
|
|
|
2003
2047
|
*
|
|
2004
2048
|
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
2005
2049
|
* probe and a real chat request can never disagree about what the same upstream
|
|
2006
|
-
* answer means. The extra
|
|
2050
|
+
* answer means. The extra splits here are `credential-rejected`, which the
|
|
2007
2051
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
2008
|
-
* different advice (re-auth, not "wait")
|
|
2052
|
+
* different advice (re-auth, not "wait"), and `policy-rejected`, a JSON 403
|
|
2053
|
+
* content-policy refusal where re-auth advice would be actively misleading.
|
|
2009
2054
|
*/
|
|
2010
2055
|
function outcomeOfFailure(status, body) {
|
|
2011
|
-
if (status === 401 || status === 403)
|
|
2056
|
+
if (status === 401 || status === 403) {
|
|
2057
|
+
if (classifyUpstreamError(status, body) === "policy_reject") return "policy-rejected";
|
|
2058
|
+
return "credential-rejected";
|
|
2059
|
+
}
|
|
2012
2060
|
switch (classifyUpstreamError(status, body)) {
|
|
2013
2061
|
case "soft_rate": return "rate-limited";
|
|
2014
2062
|
case "hard_credit": return "out-of-credit";
|
|
@@ -2339,6 +2387,7 @@ function exclusionOf(probe, nowMs) {
|
|
|
2339
2387
|
case "failed": return retryDue(probe, nowMs) ? void 0 : "failed";
|
|
2340
2388
|
case "rate-limited": return retryDue(probe, nowMs) ? void 0 : "rate-limited";
|
|
2341
2389
|
case "out-of-credit": return retryDue(probe, nowMs) ? void 0 : "out-of-credit";
|
|
2390
|
+
case "policy-rejected": return;
|
|
2342
2391
|
default: return "unusable";
|
|
2343
2392
|
}
|
|
2344
2393
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "3.4.
|
|
5
|
+
"version": "3.4.3",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|