dsh-workbuddy-connect 0.7.6 → 0.7.8

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/README.en.md CHANGED
@@ -13,7 +13,7 @@ Brings the models available in your signed-in WorkBuddy / WorkBuddy AI desktop a
13
13
  ```text
14
14
  Please install and verify the DSH (DeepSeek Harness) plugin dsh-workbuddy-connect for me (project: https://github.com/corrinehu/dsh-workbuddy-connect).
15
15
  Start by reading the project README and confirming which interface I actually use (Web / Desktop / TUI) and its DSH core version — for the Desktop app, the bundled core, never the standalone CLI's version.
16
- Then install from the official npm package per the version mapping (0.2.0-rc.2 -> 0.7.6, rc.1 -> 0.7.0, 0.1.5-0.1.7 -> 0.6.5), preferring a locally supported non-market channel; on some Desktop distributions (the official Electron app in particular) the profile is app-managed and rules vary by distribution — check mine first, then explore its compliant install route, but never bypass its validation.
16
+ Then install from the official npm package per the version mapping (0.2.1-rc.2 -> 0.7.8, 0.2.1-rc.1 -> 0.7.7, 0.2.0-rc.2 -> 0.7.6, rc.1 -> 0.7.0, 0.1.5-0.1.7 -> 0.6.5), preferring a locally supported non-market channel; on some Desktop distributions (the official Electron app in particular) the profile is app-managed and rules vary by distribution — check mine first, then explore its compliant install route, but never bypass its validation.
17
17
  Ask for my consent and make backups before changing any local files or configuration.
18
18
  After installing, verify the WorkBuddy group appears in the model picker and walk me through one short conversation.
19
19
  If the Desktop install still fails generation validation (e.g. the @types/retry false positive), report it honestly, reference upstream dataelement/dsh-desktop#688, and suggest switching to the standalone Web / TUI — do not modify dependency packages or bypass the check.
@@ -23,7 +23,9 @@ Core-to-plugin quick reference (full table in "Compatibility and release history
23
23
 
24
24
  | DSH core | Recommended plugin |
25
25
  |---|---|
26
- | 0.2.0-rc.2 | 0.7.6 (npm latest) |
26
+ | 0.2.1-rc.2 | 0.7.8 (npm latest) |
27
+ | 0.2.1-rc.1 | 0.7.7 |
28
+ | 0.2.0-rc.2 | 0.7.6 |
27
29
  | 0.2.0-rc.1 | 0.7.0 |
28
30
  | 0.1.5 – 0.1.7 | 0.6.5 |
29
31
 
@@ -31,8 +33,8 @@ Core-to-plugin quick reference (full table in "Compatibility and release history
31
33
  <summary>Manual install (command line, Web / TUI only)</summary>
32
34
 
33
35
  ```sh
34
- # Web (pin per your actual core — example: 0.2.0-rc.2 -> 0.7.6; or install from source via github:corrinehu/dsh-workbuddy-connect)
35
- dsh plugin --profile web add dsh-workbuddy-connect@0.7.6
36
+ # Web (pin per your actual core — example: 0.2.1-rc.2 -> 0.7.8; or install from source via github:corrinehu/dsh-workbuddy-connect)
37
+ dsh plugin --profile web add dsh-workbuddy-connect@0.7.8
36
38
  dsh web
37
39
 
38
40
  # TUI (example for a 0.1.x core; that profile needs pnpm 11, e.g. npx pnpm@11)
@@ -58,11 +60,11 @@ dsh --profile dsh-tui
58
60
 
59
61
  ![WorkBuddy AI models in the DSH model picker](assets/5.png)
60
62
 
61
- - **Image input**: most models accept images pasted or dropped straight into the conversation (GLM-5.3-Flash, GLM-5.2, the DeepSeek-V4 series, and more); the few text-only models (e.g. GLM-5.1) clearly say so.
63
+ - **Image input**: most models accept images pasted or dropped straight into the conversation (GLM-5.3-Flash, the DeepSeek-V4 series, and more); the few text-only models (e.g. GLM-5.1 / GLM-5.2 on the China endpoint) clearly say so.
62
64
 
63
65
  - **Reasoning levels**: levels explicitly declared by WorkBuddy appear directly — for example, GLM-5.3 and GLM-5.3-Flash offer low / high / max. For models without declared levels, Web and Desktop offer a manual check in the model picker (it sends a few requests and may consume credit); models without a check result or selectable levels keep using WorkBuddy's default.
64
66
 
65
- - **Balance at a glance**: with a WorkBuddy / WorkBuddy AI model selected, the composer shows the account's remaining credit next to the model selector (e.g. `WB.3,401 credits`, `WB AI.3,401 credits` on the international app). Credit is not polled automatically (every status read is a real billing query): one read on mount, then refresh on demand with the ↻ beside it. During session switching, the controls may briefly hide until the model selection is ready, then return automatically. The refresh button remains available even when the model has no reasoning-level detection entry or the balance cannot be read.
67
+ - **Balance at a glance**: with a WorkBuddy / WorkBuddy AI model selected, the composer shows the account's remaining credit next to the model selector (e.g. `WB.3,401 credits`, `WB AI.3,401 credits` on the international app). Credit is not polled automatically (every status read is a real billing query): one read on mount, then refresh on demand with the ↻ before the balance. During session switching, the controls may briefly hide until the model selection is ready, then return automatically. The refresh button remains available even when the model has no reasoning-level detection entry or the balance cannot be read.
66
68
 
67
69
  - **Status and detection**: Settings → Plugins → the matching card shows the account, token validity, remaining credit, and model offers; it also refreshes the model list manually, shows whether the current list came from the upstream or the built-in fallback, and runs manual reasoning-level detection for eligible models. On DSH 0.1.5 the entry lives under Settings → Plugins; on 0.1.6+ it moved to the left-sidebar Plugins panel (workbuddy-connect → View) — and the Settings → Models page shows no WorkBuddy entries on either generation, by design.
68
70
 
@@ -126,7 +128,9 @@ The plugin version must match the DSH core — a mismatched combination fails to
126
128
 
127
129
  | DSH core | Recommended plugin | Notes |
128
130
  |---|---|---|
129
- | `0.2.0-rc.2` | `0.7.6` (npm latest) | Current line; fixes enterprise catalog gaps and international chat requests blocked by the upstream channel check. Avoid 0.7.2 with its registry regression. |
131
+ | `0.2.1-rc.2` | `0.7.8` | Targets this core only; `0.2.0-rc.2` and `0.2.1-rc.2` differ. Check the core bundled in your Desktop app. |
132
+ | `0.2.1-rc.1` | `0.7.7` | The core moved to `@earendil-works/pi-ai` 1.3.0, so the plugin bundles 1.3.0 too — a leftover 0.87.1 overrides entry must be removed (see the pi-ai note below). |
133
+ | `0.2.0-rc.2` | `0.7.6` | Previous core line; fixes enterprise catalog gaps and international chat requests blocked by the upstream channel check. Avoid 0.7.2 with its registry regression. |
130
134
  | `0.2.0-rc.1` | `0.7.0` | 0.7.1 drops rc.1 support; stay on this version. |
131
135
  | `0.1.5` – `0.1.7` | `0.6.5` | Final release for the old cores; 0.7.0 targets 0.2.0 only. |
132
136
  | `0.1.2-rc.1` | `0.3.1` | Older line, historical maintenance only. |
@@ -134,6 +138,8 @@ The plugin version must match the DSH core — a mismatched combination fails to
134
138
 
135
139
  Release history (full records at [releases](https://github.com/corrinehu/dsh-workbuddy-connect/releases)):
136
140
 
141
+ - **0.7.8**: adapts to DSH 0.2.1-rc.2; bundles the official atomic-write helper instead of relying on host module exposure; refreshes the China catalog snapshot and distinguishes Hy3 / Hy3-X; improves probe result labels and fixes the credit-refresh placement and tooltip. Thanks to @chenpiyanghappy for PR [#156](https://github.com/corrinehu/dsh-workbuddy-connect/pull/156).
142
+ - **0.7.7**: adapts to the DSH 0.2.1-rc.1 core: peers narrow to 0.2.1-rc.1 and the plugin-bundled `@earendil-works/pi-ai` moves to 1.3.0 with it (a profile-level leftover 0.87.1 overrides entry must be removed, or the plugin fails to load).
137
143
  - **0.7.6**: fixes enterprise accounts missing `glm-5.3-flashx` from the model catalog ([#123](https://github.com/corrinehu/dsh-workbuddy-connect/issues/123)), and fixes international chat requests being blocked by the upstream channel check ([#139](https://github.com/corrinehu/dsh-workbuddy-connect/issues/139) / [#142](https://github.com/corrinehu/dsh-workbuddy-connect/issues/142)).
138
144
  - **0.7.5**: restores composer controls after session switching ([#125](https://github.com/corrinehu/dsh-workbuddy-connect/issues/125) / [#132](https://github.com/corrinehu/dsh-workbuddy-connect/issues/132)), fixes WSL helper environment forwarding ([#133](https://github.com/corrinehu/dsh-workbuddy-connect/issues/133)), and adds community projects to both READMEs.
139
145
  - **0.7.4**: Windows diagnostics and stability improvements: accurate classification of oversized registry-subprocess output, cross-platform test-assumption fixes ([#62](https://github.com/corrinehu/dsh-workbuddy-connect/issues/62)), and a reworked install section.
@@ -143,9 +149,9 @@ Release history (full records at [releases](https://github.com/corrinehu/dsh-wor
143
149
  - **0.7.0**: adapts to DSH 0.2.0's settings-service rework and drops 0.1.x support.
144
150
  - **0.6.5**: final release of the 0.1.x line.
145
151
 
146
- Pinned-version install example: `dsh plugin --profile web add dsh-workbuddy-connect@0.7.6`.
152
+ Pinned-version install example: `dsh plugin --profile web add dsh-workbuddy-connect@0.7.8`.
147
153
 
148
- **Do not upgrade the profile-level `@earendil-works/pi-ai` to 1.x**: this plugin and the `0.2.0-rc.2` generation of the DSH core both require `^0.87.1` (bundled by the plugin since 0.7.2), and third-party plugin managers' "updatable" prompts do not check peer constraints — do not act on them ([#86](https://github.com/corrinehu/dsh-workbuddy-connect/issues/86)); likewise, do not blindly remove a leftover `overrides: {'@earendil-works/pi-ai': '0.87.1'}` until you have confirmed nothing else depends on it.
154
+ **pi-ai versions are per core line and must not be mixed**: the 0.2.1-rc.1 / rc.2 core lines (plugin 0.7.7 / 0.7.8 respectively) moved to `@earendil-works/pi-ai` 1.3.0 (bundled by the plugin), so a profile-level leftover `overrides: {'@earendil-works/pi-ai': '0.87.1'}` **must be removed** or the plugin fails to load; the 0.2.0-rc.2 core line (plugin 0.7.6) still requires `^0.87.1` — on that line, **do not** upgrade the profile-level pi-ai to 1.x, and do not act on third-party plugin managers' "updatable" prompts, which do not check peer constraints ([#86](https://github.com/corrinehu/dsh-workbuddy-connect/issues/86)).
149
155
 
150
156
  ## Disclaimer
151
157
 
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  ```text
14
14
  请帮我安装并验证 DSH(DeepSeek Harness)插件 dsh-workbuddy-connect(项目地址:https://github.com/corrinehu/dsh-workbuddy-connect):
15
15
  先阅读项目 README,确认我实际使用的界面(Web / Desktop / TUI)与 DSH 内核版本,桌面版以 App 内置核心为准,不要用独立 CLI 的版本代替;
16
- 再按版本对应关系从 npm 正式包安装(0.2.0-rc.2→0.7.6、rc.1→0.7.0、0.1.5–0.1.7→0.6.5),优先走本机受支持的非插件市场渠道;部分 Desktop 发行版(尤其官方 Electron 版)的 profile 由 App 管理,各发行版规则未必相同,先确认我所用的发行版,再探查其合规的安装方式,但不得绕过其校验;
16
+ 再按版本对应关系从 npm 正式包安装(0.2.1-rc.2→0.7.8、0.2.1-rc.1→0.7.7、0.2.0-rc.2→0.7.6、rc.1→0.7.0、0.1.5–0.1.7→0.6.5),优先走本机受支持的非插件市场渠道;部分 Desktop 发行版(尤其官方 Electron 版)的 profile 由 App 管理,各发行版规则未必相同,先确认我所用的发行版,再探查其合规的安装方式,但不得绕过其校验;
17
17
  任何本地文件或配置改动,先征得我同意并做好备份;
18
18
  装完后验证模型选择器里出现 WorkBuddy 分组,并陪我完成一次简短对话;
19
19
  若 Desktop 安装仍因 generation 校验失败(如 @types/retry 误报),请如实报告、参照上游 dataelement/dsh-desktop#688,并建议我改用独立的 Web / TUI,不要修改依赖包或绕过校验。
@@ -23,7 +23,9 @@
23
23
 
24
24
  | DSH 内核 | 推荐插件版 |
25
25
  |---|---|
26
- | 0.2.0-rc.2 | 0.7.6(npm latest) |
26
+ | 0.2.1-rc.2 | 0.7.8(npm latest) |
27
+ | 0.2.1-rc.1 | 0.7.7 |
28
+ | 0.2.0-rc.2 | 0.7.6 |
27
29
  | 0.2.0-rc.1 | 0.7.0 |
28
30
  | 0.1.5 – 0.1.7 | 0.6.5 |
29
31
 
@@ -31,8 +33,8 @@
31
33
  <summary>手动安装(命令行,仅 Web / TUI)</summary>
32
34
 
33
35
  ```sh
34
- # Web(按实际内核固定版本,示例为 0.2.0-rc.2 → 0.7.6;也可用 github:corrinehu/dsh-workbuddy-connect 从源码安装)
35
- dsh plugin --profile web add dsh-workbuddy-connect@0.7.6
36
+ # Web(按实际内核固定版本,示例为 0.2.1-rc.2 → 0.7.8;也可用 github:corrinehu/dsh-workbuddy-connect 从源码安装)
37
+ dsh plugin --profile web add dsh-workbuddy-connect@0.7.8
36
38
  dsh web
37
39
 
38
40
  # TUI(示例为 0.1.x 内核;该 profile 需用 pnpm 11 安装,如 npx pnpm@11)
@@ -58,11 +60,11 @@ dsh --profile dsh-tui
58
60
 
59
61
  ![WorkBuddy AI 模型出现在 DSH 模型选择器中](assets/5.png)
60
62
 
61
- - **图片输入**:大部分模型支持在对话里直接粘贴或拖入图片(GLM-5.3-Flash、GLM-5.2、DeepSeek-V4 系列等);少数纯文字模型(如 GLM-5.1)会明确提示不支持。
63
+ - **图片输入**:大部分模型支持在对话里直接粘贴或拖入图片(GLM-5.3-Flash、DeepSeek-V4 系列等);少数纯文字模型(如 GLM-5.1、GLM-5.2)会明确提示不支持。
62
64
 
63
65
  - **推理档位**:WorkBuddy 明确声明的档位会直接显示,例如 GLM-5.3 和 GLM-5.3-Flash 可选 low / high / max。未声明档位的模型可在 Web 和 Desktop 的模型选择器中手动检测(发送少量请求,可能消耗积分);未检测或没有可用档位的模型使用 WorkBuddy 默认档位。
64
66
 
65
- - **余额速览**:选中 WorkBuddy / WorkBuddy AI 模型时,输入框底部在模型选择器旁显示当前账号的剩余积分(如 `WB.3,401(积分)`、国际版 `WB AI.3,401(积分)`)。积分不自动轮询(每次读取都是真实计费查询),挂载时读取一次后可点旁边的 ↻ 手动刷新。切换会话时组件可能短暂隐藏,模型选择就绪后自动恢复;即使模型没有推理档位检测入口或余额读取失败,刷新按钮仍保留。
67
+ - **余额速览**:选中 WorkBuddy / WorkBuddy AI 模型时,输入框底部在模型选择器旁显示当前账号的剩余积分(如 `WB.3,401(积分)`、国际版 `WB AI.3,401(积分)`)。积分不自动轮询(每次读取都是真实计费查询),挂载时读取一次后可点积分文字前方的 ↻ 手动刷新。切换会话时组件可能短暂隐藏,模型选择就绪后自动恢复;即使模型没有推理档位检测入口或余额读取失败,刷新按钮仍保留。
66
68
 
67
69
  - **信息查看与检测**:设置 → 插件 → 对应卡片可查看账号、令牌有效期、剩余积分和模型优惠,也可手动刷新模型列表并查看当前列表来自上游还是内置兜底,或对可检测模型手动检测推理档位。DSH 0.1.5 的入口在「设置 → 插件」,0.1.6+ 移到主界面左侧栏「插件」面板(workbuddy-connect → 查看);「设置 → 模型」页两代都不显示 WorkBuddy 条目,属有意设计。
68
70
 
@@ -127,7 +129,9 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
127
129
 
128
130
  | DSH 内核 | 推荐插件版 | 注意事项 |
129
131
  |---|---|---|
130
- | `0.2.0-rc.2` | `0.7.6`(npm latest) | 当前最新线;修复企业账号模型目录缺项与国际版对话被渠道校验拦截,勿停留在有注册表回归的 0.7.2。 |
132
+ | `0.2.1-rc.2` | `0.7.8` | 仅适配这一内核;`0.2.0-rc.2` 与 `0.2.1-rc.2` 不同,请核对 App 内置版本。 |
133
+ | `0.2.1-rc.1` | `0.7.7` | 内核升级到 `@earendil-works/pi-ai` 1.3.0,插件同步自带 1.3.0,历史遗留的 0.87.1 overrides 必须移除(见下方 pi-ai 说明)。 |
134
+ | `0.2.0-rc.2` | `0.7.6` | 上一代内核线;修复企业账号模型目录缺项与国际版对话被渠道校验拦截,勿停留在有注册表回归的 0.7.2。 |
131
135
  | `0.2.0-rc.1` | `0.7.0` | 0.7.1 起不再支持 rc.1,请停留本版。 |
132
136
  | `0.1.5` – `0.1.7` | `0.6.5` | 旧内核最终版;0.7.0 起仅面向 0.2.0。 |
133
137
  | `0.1.2-rc.1` | `0.3.1` | 更早旧线,仅历史维护。 |
@@ -135,6 +139,8 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
135
139
 
136
140
  历次更新(完整记录见 [releases](https://github.com/corrinehu/dsh-workbuddy-connect/releases)):
137
141
 
142
+ - **0.7.8**:适配 DSH 0.2.1-rc.2;将官方原子写入工具打包到插件,避免依赖宿主暴露该模块;更新国内模型目录快照,区分 Hy3 / Hy3-X;优化推理检测结果展示,调整积分刷新按钮位置并修正悬停提示。感谢 @chenpiyanghappy 的 PR [#156](https://github.com/corrinehu/dsh-workbuddy-connect/pull/156)。
143
+ - **0.7.7**:适配 DSH 0.2.1-rc.1 内核:peer 收窄到 0.2.1-rc.1,插件自带的 `@earendil-works/pi-ai` 随内核升级到 1.3.0(profile 级历史遗留的 0.87.1 overrides 必须移除,否则插件无法加载);修复国际版与国内版推理档位检测停留「检测未完成」——上游把档位拒绝码改拼为 `invalid_parameter_value`([#143](https://github.com/corrinehu/dsh-workbuddy-connect/issues/143) / [#148](https://github.com/corrinehu/dsh-workbuddy-connect/issues/148),PR [#145](https://github.com/corrinehu/dsh-workbuddy-connect/pull/145));修复中文 Windows 控制台下注册表缺键诊断因 GBK 编码被误判(zh-CN 实机验证)。
138
144
  - **0.7.6**:修复企业账号模型目录缺少 `glm-5.3-flashx`([#123](https://github.com/corrinehu/dsh-workbuddy-connect/issues/123)),修复国际版对话请求被上游渠道校验拦截([#139](https://github.com/corrinehu/dsh-workbuddy-connect/issues/139) / [#142](https://github.com/corrinehu/dsh-workbuddy-connect/issues/142))。
139
145
  - **0.7.5**:修复切换会话后输入框余额与刷新按钮消失([#125](https://github.com/corrinehu/dsh-workbuddy-connect/issues/125) / [#132](https://github.com/corrinehu/dsh-workbuddy-connect/issues/132)),修复 WSL 调用 Windows 凭据解密程序的环境转发([#133](https://github.com/corrinehu/dsh-workbuddy-connect/issues/133)),README 收录社区项目([#130](https://github.com/corrinehu/dsh-workbuddy-connect/issues/130))。
140
146
  - **0.7.4**:Windows 诊断与稳定性改进:注册表子进程输出超限准确分类、跨平台测试假设修正([#62](https://github.com/corrinehu/dsh-workbuddy-connect/issues/62)),安装说明重排。
@@ -144,9 +150,9 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
144
150
  - **0.7.0**:适配 DSH 0.2.0 的设置服务改造,不再支持 0.1.x。
145
151
  - **0.6.5**:0.1.x 线最终版本。
146
152
 
147
- 固定版本安装示例:`dsh plugin --profile web add dsh-workbuddy-connect@0.7.6`。
153
+ 固定版本安装示例:`dsh plugin --profile web add dsh-workbuddy-connect@0.7.8`。
148
154
 
149
- **不要把 profile 级的 `@earendil-works/pi-ai` 升到 1.x**:本插件与 `0.2.0-rc.2` 这一代内核都要求 `^0.87.1`(0.7.2 起由插件自带),第三方插件管理器的「可更新」提示不校验 peer 约束、不能照做([#86](https://github.com/corrinehu/dsh-workbuddy-connect/issues/86));为此历史遗留的 `overrides: {'@earendil-works/pi-ai': '0.87.1'}` 在确认其他插件不依赖前也不要盲目移除。
155
+ **pi-ai 版本按内核线区分,不能混**:0.2.1-rc.1 / rc.2 内核线(分别对应插件 0.7.7 / 0.7.8)已升级到 `@earendil-works/pi-ai` 1.3.0(由插件自带),profile 级历史遗留的 `overrides: {'@earendil-works/pi-ai': '0.87.1'}` **必须移除**,否则插件会因加载到旧一代 pi-ai 而启动失败;0.2.0-rc.2 内核线(插件 0.7.6)仍要求 `^0.87.1`,在这条线上**不要**把 profile 级的 pi-ai 升到 1.x,第三方插件管理器的「可更新」提示不校验 peer 约束、不能照做([#86](https://github.com/corrinehu/dsh-workbuddy-connect/issues/86))。
150
156
 
151
157
  ## 免责声明
152
158
 
@@ -0,0 +1,24 @@
1
+ @deepseek-ai/dsh-atomic-write 0.2.1-rc.2
2
+ Bundled from https://github.com/deepseek-ai/deepseek-harness/tree/main/packages/util/atomic-write
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 DeepSeek
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as WORKBUDDY_VARIANTS, O as WorkBuddyUpstreamClient, Q as resolveAppVersion, S as CN_VARIANT, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as variantFor } from "./host-heartbeat-vU86u9aU.js";
2
+ import { C as WORKBUDDY_VARIANTS, O as WorkBuddyUpstreamClient, Q as resolveAppVersion, S as CN_VARIANT, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as variantFor } from "./host-heartbeat-sjotAEdb.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -456,6 +456,37 @@ window.__ModuleLoader__.load({
456
456
  color: "var(--dsw-alias-state-success-primary, #22a06b)"
457
457
  };
458
458
  /**
459
+ * Chip for a probe that came back `non-validating`.
460
+ *
461
+ * Amber rather than the success green: the upstream accepted a sentinel effort,
462
+ * which tells us it does not check the parameter — not that any level works.
463
+ * The state is an observation, and it used to share the green chip with the
464
+ * verified levels, which read as "these are the supported levels".
465
+ */
466
+ const probeChipNotValidatingStyle = {
467
+ padding: "1px 8px",
468
+ borderRadius: 999,
469
+ fontSize: 11,
470
+ lineHeight: "18px",
471
+ background: "var(--dsw-alias-state-warning-subtle, rgba(214, 142, 23, 0.14))",
472
+ color: "var(--dsw-alias-state-warning-primary, #b26a00)"
473
+ };
474
+ /** Chip for a probe that never produced an answer (cancelled or failed). */
475
+ const probeChipUnknownStyle = {
476
+ padding: "1px 8px",
477
+ borderRadius: 999,
478
+ fontSize: 11,
479
+ lineHeight: "18px",
480
+ background: "var(--dsw-alias-fill-tertiary, rgba(127, 127, 127, 0.12))",
481
+ color: "var(--dsw-alias-label-tertiary, #8a8a8a)"
482
+ };
483
+ /** Pick the chip style that matches what the observation actually says. */
484
+ function probeChipStyleFor(result) {
485
+ if (result.validation === "validating" && result.efforts.length > 0) return modelBadgeChipStyle;
486
+ if (result.validation === "non-validating") return probeChipNotValidatingStyle;
487
+ return probeChipUnknownStyle;
488
+ }
489
+ /**
459
490
  * Localize an upstream promotional badge label, with an unknown-badge fallback.
460
491
  *
461
492
  * The CN catalog spells badges in Chinese (`限时免费`, `夜间折扣`); the
@@ -467,9 +498,32 @@ window.__ModuleLoader__.load({
467
498
  function modelBadgeLabel(badge, t) {
468
499
  if (badge === "限时免费") return t("badgeLimitedFree");
469
500
  if (badge === "夜间折扣") return t("badgeNightDiscount");
501
+ if (badge === "夜间免费") return t("badgeNightFree");
502
+ if (badge === "独家限免") return t("badgeExclusiveFree");
470
503
  if (badge === "Free now") return t("badgeFreeNow");
471
504
  return badge;
472
505
  }
506
+ /**
507
+ * Upstream badge labels that already say "you pay nothing", in either language.
508
+ *
509
+ * The catalog reports both `free: true` and a promotional badge for the same
510
+ * row (`hy3` carries `限时免费`, `step-5-preview` carries `独家限免`). Rendering
511
+ * the generic free chip on top of those printed the same fact twice and, worse,
512
+ * pushed the second chip past the row's horizontal budget — it collapsed into a
513
+ * sliver instead of wrapping. One chip per row is enough: a badge that already
514
+ * states the offer is preferred over the bare "Free".
515
+ */
516
+ const FREE_STATEMENT_BADGES = /* @__PURE__ */ new Set([
517
+ "限时免费",
518
+ "夜间免费",
519
+ "独家限免",
520
+ "Free now"
521
+ ]);
522
+ /** Whether a row still needs the generic free chip next to its badges. */
523
+ function needsFreeChip(free, badges) {
524
+ if (free !== true) return false;
525
+ return !(badges ?? []).some((badge) => FREE_STATEMENT_BADGES.has(badge));
526
+ }
473
527
  const progressTrackStyle = {
474
528
  height: 8,
475
529
  overflow: "hidden",
@@ -718,7 +772,7 @@ window.__ModuleLoader__.load({
718
772
  children: [model.badges?.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
719
773
  style: modelBadgeChipStyle,
720
774
  children: modelBadgeLabel(badge, t)
721
- }, badge)), model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
775
+ }, badge)), needsFreeChip(model.free, model.badges) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
722
776
  style: modelBadgeChipStyle,
723
777
  children: t("freeModel")
724
778
  }) : null]
@@ -822,7 +876,7 @@ window.__ModuleLoader__.load({
822
876
  style: modelBadgeChipStyle,
823
877
  children: modelBadgeLabel(badge, t)
824
878
  }, badge)),
825
- model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
879
+ needsFreeChip(model.free, model.badges) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
826
880
  style: modelBadgeChipStyle,
827
881
  children: t("freeModel")
828
882
  }) : null
@@ -923,7 +977,9 @@ window.__ModuleLoader__.load({
923
977
  style: quotaGroupStyle,
924
978
  children: probe.candidates.map((id) => {
925
979
  const result = probe.results.find((entry) => entry.id === id);
926
- const name = models?.find((model) => model.id === id)?.name ?? result?.name ?? id;
980
+ const row = models?.find((model) => model.id === id);
981
+ const name = row?.name ?? result?.name ?? id;
982
+ const declared = (row?.declaredEfforts?.length ?? 0) > 0 ? row.declaredEfforts.join(" / ") : row?.defaultEffort;
927
983
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
928
984
  style: modelOfferStyle,
929
985
  children: [
@@ -932,8 +988,8 @@ window.__ModuleLoader__.load({
932
988
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
933
989
  style: probeRowEndStyle,
934
990
  children: [result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
935
- style: modelBadgeChipStyle,
936
- children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
991
+ style: probeChipStyleFor(result),
992
+ children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : result.validation === "non-validating" ? t("probeResultNotValidating") + (declared === void 0 ? "" : t("probeDeclaredSuffix", { levels: declared })) : t("probeResultUnknown")
937
993
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
938
994
  type: "button",
939
995
  style: buttonStyle$2,
@@ -1560,7 +1616,7 @@ window.__ModuleLoader__.load({
1560
1616
  lineHeight: "16px",
1561
1617
  color: "var(--dsw-alias-label-tertiary)",
1562
1618
  whiteSpace: "nowrap",
1563
- padding: "0 4px"
1619
+ padding: "0 4px 0 0"
1564
1620
  };
1565
1621
  /**
1566
1622
  * The badge's ↻ (#99): the only re-read after the mount fetch. An icon-only
@@ -1890,17 +1946,7 @@ window.__ModuleLoader__.load({
1890
1946
  const showTooltip = tooltipVisible && !confirming && note === void 0;
1891
1947
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1892
1948
  style: wrapperStyle,
1893
- onMouseEnter: () => {
1894
- setTooltipVisible(true);
1895
- },
1896
- onMouseLeave: () => {
1897
- setTooltipVisible(false);
1898
- },
1899
1949
  children: [
1900
- credit === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1901
- style: creditStyle,
1902
- children: credit
1903
- }),
1904
1950
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1905
1951
  type: "button",
1906
1952
  style: {
@@ -1929,6 +1975,10 @@ window.__ModuleLoader__.load({
1929
1975
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 12a8 8 0 1 1-2.34-5.66" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 4v4h-4" })]
1930
1976
  })
1931
1977
  }),
1978
+ credit === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1979
+ style: creditStyle,
1980
+ children: credit
1981
+ }),
1932
1982
  visible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1933
1983
  type: "button",
1934
1984
  "aria-label": text,
@@ -1939,6 +1989,12 @@ window.__ModuleLoader__.load({
1939
1989
  onClick: () => {
1940
1990
  setConfirming(true);
1941
1991
  },
1992
+ onMouseEnter: () => {
1993
+ setTooltipVisible(true);
1994
+ },
1995
+ onMouseLeave: () => {
1996
+ setTooltipVisible(false);
1997
+ },
1942
1998
  onFocus: () => {
1943
1999
  setTooltipVisible(true);
1944
2000
  },
@@ -2772,7 +2828,7 @@ window.__ModuleLoader__.load({
2772
2828
  };
2773
2829
  //#endregion
2774
2830
  //#region src/version.ts
2775
- const WORKBUDDY_CONNECT_VERSION = "0.7.6";
2831
+ const WORKBUDDY_CONNECT_VERSION = "0.7.8";
2776
2832
  //#endregion
2777
2833
  //#region src/client/WorkBuddyConfigPage.tsx
2778
2834
  /**
@@ -2815,15 +2871,15 @@ window.__ModuleLoader__.load({
2815
2871
  //#region src/client/locales.ts
2816
2872
  /** Plugin-card copy registered under the settings.workbuddy locale namespace. */
2817
2873
  const en = {
2818
- title: "DSH WorkBuddy Connect",
2819
- intro: "Use the models in the WorkBuddy desktop app directly in DSH — zero configuration, ready out of the box.",
2820
- titleAI: "DSH WorkBuddy AI Connect",
2874
+ title: "DSH WorkBuddy Connect (China)",
2875
+ intro: "Use the models in the WorkBuddy desktop app (China edition) directly in DSH — zero configuration, ready out of the box.",
2876
+ titleAI: "DSH WorkBuddy AI Connect (International)",
2821
2877
  introAI: "Use the models in the WorkBuddy AI international desktop app directly in DSH — zero configuration, ready out of the box.",
2822
2878
  expand: "Expand",
2823
2879
  collapse: "Collapse",
2824
2880
  loading: "Loading account…",
2825
2881
  signedOut: "Not signed in",
2826
- signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
2882
+ signedOutHint: "Sign in once in the WorkBuddy desktop app (China edition); this plugin follows that sign-in automatically.",
2827
2883
  signedOutHintAI: "Sign in once in the WorkBuddy AI desktop app; this plugin follows that sign-in automatically.",
2828
2884
  signedInAs: "Signed in as {nickname}",
2829
2885
  accessTokenExpires: "Access token expires {time} (refresh is automatic)",
@@ -2873,17 +2929,19 @@ window.__ModuleLoader__.load({
2873
2929
  badgeLimitedFree: "Limited-time free",
2874
2930
  badgeNightDiscount: "Night discount",
2875
2931
  badgeFreeNow: "Free now",
2932
+ badgeExclusiveFree: "Exclusive free",
2933
+ badgeNightFree: "Free at night",
2876
2934
  rate: "{rate} credits per message",
2877
2935
  rateUnknown: "Price unavailable — refresh to update",
2878
2936
  probeLabel: "Reasoning levels",
2879
2937
  probeTooltipIdle: "Detect the reasoning levels {model} accepts",
2880
2938
  probeTooltipVerified: "Accepted levels: {levels} · click to detect again",
2881
- probeTooltipNotValidating: "This model does not check the effort parameter",
2939
+ probeTooltipNotValidating: "This model accepts any effort value, so the probe cannot tell the levels apart · click to re-probe",
2882
2940
  probeTooltipRetry: "Detection did not complete · click to retry",
2883
2941
  probeBubbleBody: "Send test requests to confirm the available reasoning levels. May consume a small amount of credit.",
2884
2942
  probeConfirmAction: "Confirm",
2885
2943
  probeNoteVerified: "Detected: {levels}",
2886
- probeNoteNotValidating: "This model does not check the effort parameter",
2944
+ probeNoteNotValidating: "Could not probe the levels: the upstream does not validate the effort parameter — that is not the same as no level being supported",
2887
2945
  probeNoteUnknown: "Detection did not complete",
2888
2946
  probeNoteDismiss: "Got it",
2889
2947
  probeHeading: "Reasoning effort detection",
@@ -2899,7 +2957,8 @@ window.__ModuleLoader__.load({
2899
2957
  probeConfirmBody: "Send test requests to {model} to confirm its available reasoning levels. May consume a small amount of credit.",
2900
2958
  cancel: "Cancel",
2901
2959
  probeResultVerified: "Verified levels: {levels}",
2902
- probeResultNotValidating: "This model does not check the effort parameter",
2960
+ probeResultNotValidating: "Upstream ignores the effort parameter",
2961
+ probeDeclaredSuffix: " (catalogue declares {levels})",
2903
2962
  probeResultUnknown: "Detection did not complete",
2904
2963
  probeResultAt: "Detected {time}",
2905
2964
  probeResultEmpty: "No detectable models right now.",
@@ -2939,15 +2998,15 @@ window.__ModuleLoader__.load({
2939
2998
  agentUpgradePrompt: "Please open {repository}, check its latest version, and install or update the plugin \"dsh-workbuddy-connect\" in my current DSH profile following the project README (the install command differs by profile). After upgrading, fully quit and restart DSH."
2940
2999
  };
2941
3000
  const zh = {
2942
- title: "DSH WorkBuddy Connect",
2943
- intro: "在 DSH 中直接使用 WorkBuddy 桌面 App 包含的模型,开箱即用,无需额外配置。",
2944
- titleAI: "DSH WorkBuddy AI Connect",
3001
+ title: "DSH WorkBuddy Connect(国内版)",
3002
+ intro: "在 DSH 中直接使用 WorkBuddy 国内版桌面 App 包含的模型,开箱即用,无需额外配置。",
3003
+ titleAI: "DSH WorkBuddy AI Connect(国际版)",
2945
3004
  introAI: "在 DSH 中直接使用 WorkBuddy AI 国际版桌面 App 包含的模型,开箱即用,无需额外配置。",
2946
3005
  expand: "展开",
2947
3006
  collapse: "收起",
2948
3007
  loading: "正在读取账号…",
2949
3008
  signedOut: "未登录",
2950
- signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
3009
+ signedOutHint: "在 WorkBuddy 国内版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
2951
3010
  signedOutHintAI: "在 WorkBuddy AI 国际版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
2952
3011
  signedInAs: "已登录:{nickname}",
2953
3012
  accessTokenExpires: "访问令牌 {time} 过期(自动续期)",
@@ -2997,17 +3056,19 @@ window.__ModuleLoader__.load({
2997
3056
  badgeLimitedFree: "限时免费",
2998
3057
  badgeNightDiscount: "夜间折扣",
2999
3058
  badgeFreeNow: "限时免费",
3059
+ badgeExclusiveFree: "独家限免",
3060
+ badgeNightFree: "夜间免费",
3000
3061
  rate: "{rate} 积分/次",
3001
3062
  rateUnknown: "价格未知 — 刷新后更新",
3002
3063
  probeLabel: "推理等级",
3003
3064
  probeTooltipIdle: "检测 {model} 可用的推理档位",
3004
3065
  probeTooltipVerified: "已接受:{levels} · 点击可重新检测",
3005
- probeTooltipNotValidating: "该模型不校验该参数",
3066
+ probeTooltipNotValidating: "该模型接受任意档位参数,探测无法区分可用档位 · 点击可重新检测",
3006
3067
  probeTooltipRetry: "检测未完成 · 点击重试",
3007
3068
  probeBubbleBody: "发送探测请求以确认可用推理档位。可能消耗少量积分。",
3008
3069
  probeConfirmAction: "确认检测",
3009
3070
  probeNoteVerified: "已检测:{levels}",
3010
- probeNoteNotValidating: "该模型不校验该参数",
3071
+ probeNoteNotValidating: "无法探测档位:上游不校验该参数 —— 这不等于该档位不被支持",
3011
3072
  probeNoteUnknown: "检测未完成",
3012
3073
  probeNoteDismiss: "知道了",
3013
3074
  probeHeading: "推理档位检测",
@@ -3023,7 +3084,8 @@ window.__ModuleLoader__.load({
3023
3084
  probeConfirmBody: "向 {model} 发送探测请求,以确认可用推理档位。可能消耗少量积分。",
3024
3085
  cancel: "取消",
3025
3086
  probeResultVerified: "已验证接受的档位:{levels}",
3026
- probeResultNotValidating: "该模型不校验该参数",
3087
+ probeResultNotValidating: "上游不校验档位参数",
3088
+ probeDeclaredSuffix: "(目录声明 {levels})",
3027
3089
  probeResultUnknown: "检测未完成",
3028
3090
  probeResultAt: "检测于 {time}",
3029
3091
  probeResultEmpty: "当前没有可检测的模型。",
@@ -1,11 +1,226 @@
1
- import { readFile, rm, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { homedir, release } from "node:os";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
5
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
4
  import { createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
5
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { execFile, execFileSync } from "node:child_process";
8
8
  import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
9
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-atomic-write@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js
10
+ /**
11
+ * Zero-dependency atomic file replacement and writer coordination.
12
+ * `writeFileAtomic` writes a random-suffix sibling with exclusive create and
13
+ * the caller's permission bits, then renames it over the target, so readers
14
+ * observe either the old or the new complete content and a replaced file ends
15
+ * up with exactly the stated mode. `withFileLock` serializes cross-process
16
+ * writers of one file through a `wx`-created `<file>.lock` sibling, so a
17
+ * read-modify-write cycle can never resurrect a state another writer just
18
+ * replaced; readers stay lock-free because the rename commit is atomic. A lock
19
+ * whose recorded holder process no longer exists is taken over.
20
+ * @module @deepseek-ai/dsh-atomic-write
21
+ */
22
+ const WINDOWS_TRANSIENT_RENAME_ERRORS = /* @__PURE__ */ new Set([
23
+ "EACCES",
24
+ "EBUSY",
25
+ "EPERM"
26
+ ]);
27
+ const WINDOWS_RENAME_RETRY_INITIAL_MS = 20;
28
+ const WINDOWS_RENAME_RETRY_MAX_MS = 200;
29
+ const WINDOWS_RENAME_RETRY_LIMIT = 8;
30
+ /** Whether Windows reported temporary interference with an atomic replacement. */
31
+ function isTransientWindowsRenameError(error) {
32
+ if (process.platform !== "win32") return false;
33
+ return WINDOWS_TRANSIENT_RENAME_ERRORS.has(error?.code ?? "");
34
+ }
35
+ /** Replace the target after bounded retries for transient Windows interference. */
36
+ async function renameAtomicTemp(temp, filename) {
37
+ let delay = WINDOWS_RENAME_RETRY_INITIAL_MS;
38
+ for (let retries = 0;; retries += 1) {
39
+ try {
40
+ await rename(temp, filename);
41
+ return;
42
+ } catch (error) {
43
+ if (!isTransientWindowsRenameError(error)) throw error;
44
+ if (retries >= WINDOWS_RENAME_RETRY_LIMIT) throw error;
45
+ }
46
+ await new Promise((resolve) => setTimeout(resolve, delay));
47
+ delay = Math.min(delay * 2, WINDOWS_RENAME_RETRY_MAX_MS);
48
+ }
49
+ }
50
+ /**
51
+ * Replace `filename` with `content` in one atomic step, creating parent
52
+ * directories. The content is first written to a random-suffix sibling opened
53
+ * with exclusive create (`wx`): the open refuses to follow a symlink planted
54
+ * at the temp path, and the fresh inode carries `options.mode` through the
55
+ * rename, so replacing a wider-permission file narrows it without a chmod
56
+ * race. The rename also replaces a symlinked target itself instead of writing
57
+ * through to its referent, and the same-directory sibling keeps the rename on
58
+ * one filesystem. Windows replacement retries transient `EACCES`, `EBUSY`,
59
+ * and `EPERM` failures for a bounded interval while the complete temp file
60
+ * remains the rename source. On any remaining failure the temp file is
61
+ * removed and the failure rethrown. Crash durability (fsync) is out of scope.
62
+ * @param filename - final path receiving the content.
63
+ * @param content - complete next file content.
64
+ * @param options - permission bits for the replacement inode.
65
+ */
66
+ async function writeFileAtomic(filename, content, options) {
67
+ await mkdir(dirname(filename), {
68
+ recursive: true,
69
+ ...options.dirMode === void 0 ? {} : { mode: options.dirMode }
70
+ });
71
+ const temp = `${filename}.${randomBytes(6).toString("hex")}.tmp`;
72
+ try {
73
+ await writeFile(temp, content, {
74
+ mode: options.mode,
75
+ flag: "wx"
76
+ });
77
+ await renameAtomicTemp(temp, filename);
78
+ } catch (error) {
79
+ await rm(temp, { force: true });
80
+ throw error;
81
+ }
82
+ }
83
+ /** Whether an exclusive create found an existing lock. */
84
+ async function isLockContention(error, lockPath) {
85
+ const code = error?.code;
86
+ if (code === "EEXIST") return true;
87
+ if (code !== "EPERM") return false;
88
+ try {
89
+ await lstat(lockPath);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ /** Whether the holder a `<pid>\n` record names is proven gone: a signal probe finds no such process. */
96
+ function holderExited(record) {
97
+ if (!/^\d+\n$/.test(record)) return false;
98
+ const pid = Number(record.trim());
99
+ if (pid === 0 || pid > 2147483647) return false;
100
+ if (pid === process.pid) return false;
101
+ try {
102
+ process.kill(pid, 0);
103
+ return false;
104
+ } catch (error) {
105
+ return error.code === "ESRCH";
106
+ }
107
+ }
108
+ /** The lock file's content, or undefined when it cannot be read. */
109
+ async function readLockRecord(lockPath) {
110
+ try {
111
+ return await readFile(lockPath, "utf8");
112
+ } catch (error) {
113
+ return;
114
+ }
115
+ }
116
+ /**
117
+ * Remove the lock when its recorded holder exited. Contenders that read the
118
+ * same record serialize on a claim file named after it. Under the claim, the
119
+ * claimant re-reads the lock and probes its PID again, and removes it only
120
+ * when it still holds that record and that PID is still gone: the record's
121
+ * holder can no longer release it, and no other contender can remove it
122
+ * without the claim, so a removal never deletes a lock another contender
123
+ * acquired after the dead holder's, including one whose holder reused the PID.
124
+ * @returns Whether this call removed the dead holder's lock.
125
+ */
126
+ async function takeOverExitedLock(lockPath) {
127
+ const record = await readLockRecord(lockPath);
128
+ if (record === void 0 || !holderExited(record)) return false;
129
+ const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
130
+ try {
131
+ await writeFile(claim, `${process.pid}\n`, {
132
+ mode: 384,
133
+ flag: "wx"
134
+ });
135
+ } catch (error) {
136
+ const code = error.code;
137
+ if (code === "EEXIST" || code === "EPERM") return false;
138
+ throw error;
139
+ }
140
+ try {
141
+ if (await readLockRecord(lockPath) !== record || !holderExited(record)) return false;
142
+ try {
143
+ await rm(lockPath, { force: true });
144
+ } catch (error) {
145
+ return false;
146
+ }
147
+ return true;
148
+ } finally {
149
+ await rm(claim, { force: true }).catch((error) => {});
150
+ }
151
+ }
152
+ /**
153
+ * Retry cadence for a contended lock. These stay robustness invariants of the
154
+ * cross-process write protocol rather than deployment tunables: they govern how
155
+ * often a contender asks, which no caller has a reason to vary.
156
+ */
157
+ const LOCK_RETRY_INITIAL_MS = 20;
158
+ const LOCK_RETRY_MAX_MS = 200;
159
+ /**
160
+ * How long a contender waits when the caller states no limit — sized for the
161
+ * render-and-rename cycle every call site had when this package was written.
162
+ * Expiry fails the contender rather than guessing whether the existing lock
163
+ * still has an owner. How long is *worth* waiting is a property of the
164
+ * operation the lock holder runs, which is why {@link FileLockOptions.waitMs}
165
+ * exists; the value here is the floor for an operation that does file work
166
+ * alone.
167
+ */
168
+ const DEFAULT_LOCK_WAIT_MS = 2e3;
169
+ /**
170
+ * Hold the cross-process writer lock for `filename` around one operation. The
171
+ * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
172
+ * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
173
+ * only writers contend. `EEXIST` is contention directly; an `EPERM` is
174
+ * contention only when a fresh `lstat` confirms the lock path exists, covering
175
+ * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
176
+ * because the holder can release before the probe; a repeated unconfirmed
177
+ * permission error is rethrown. The lock records its holder's PID. A contender
178
+ * removes the lock and retries at once when no process with that PID exists
179
+ * (`ESRCH`); any other lock, including one whose holder exists under another
180
+ * user (`EPERM`) or whose record is incomplete, is waited for. Contention backs
181
+ * off exponentially and times out after the deadline. A holder whose PID a
182
+ * live process reused keeps its lock until an operator removes it. Takeover
183
+ * proves only that the recorded process exited: an operation that starts other
184
+ * writers must stop them with it or leave its successor a way to find them.
185
+ * PIDs are compared on the contender's host, so writers on other hosts or in
186
+ * other PID namespaces sharing the file are unsupported and could both hold
187
+ * the lock. The parent directory must exist.
188
+ * @param filename - the file whose writers this lock serializes.
189
+ * @param operation - the read-render-commit cycle to run while holding the lock.
190
+ * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
191
+ * @returns the operation's result; the lock releases on both outcomes.
192
+ */
193
+ async function withFileLock(filename, operation, options) {
194
+ const lockPath = `${filename}.lock`;
195
+ const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
196
+ let delay = LOCK_RETRY_INITIAL_MS;
197
+ let retriedUnconfirmedPermissionError = false;
198
+ for (;;) {
199
+ options?.signal?.throwIfAborted();
200
+ try {
201
+ await writeFile(lockPath, `${process.pid}\n`, {
202
+ mode: 384,
203
+ flag: "wx"
204
+ });
205
+ break;
206
+ } catch (error) {
207
+ if (!await isLockContention(error, lockPath)) {
208
+ if (process.platform !== "win32" || error?.code !== "EPERM" || retriedUnconfirmedPermissionError) throw error;
209
+ retriedUnconfirmedPermissionError = true;
210
+ } else if (await takeOverExitedLock(lockPath)) continue;
211
+ }
212
+ if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
213
+ await setTimeout$1(delay, void 0, { signal: options?.signal });
214
+ delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
215
+ }
216
+ try {
217
+ options?.signal?.throwIfAborted();
218
+ return await operation();
219
+ } finally {
220
+ await rm(lockPath, { force: true });
221
+ }
222
+ }
223
+ //#endregion
9
224
  //#region src/app-version.ts
10
225
  /**
11
226
  * The international desktop app's version, used as the `/v3/config` UA.
@@ -396,22 +611,37 @@ function randomSentinel() {
396
611
  * - `model_param_invalid` — the global endpoint, measured 2026-10-01. A
397
612
  * non-canonical value is answered `400` / `11133` with this code, which names
398
613
  * no field (`extError.param` is empty), instead of one that names the effort.
399
- * It is generic enough to be readable here only because the baseline step has
400
- * already proved the *same* request without `reasoning_effort` succeeds,
401
- * leaving the sentinel as the only difference between the two attempts. A
402
- * sibling code in the same `11133` envelope that names another parameter
403
- * (`integer_below_min_value`, the `max_tokens` floor) and a body carrying no
404
- * `extError` at all (`11102`, unknown model) are therefore not mistaken for
405
- * it, and neither is a level-sweep answer. It is *not* added to `cn`: that
406
- * endpoint was measured answering the specific code, and reading a generic
407
- * code there would widen attribution beyond what was observed.
614
+ * - `invalid_parameter_value` — both endpoints. Measured on the global
615
+ * endpoint 2026-10-16 (issue #143); observed on the China endpoint the same
616
+ * day (`deepseek-v4-pro`, PR #145 discussion) and reproduced there
617
+ * 2026-10-17 on WorkBuddy CN 5.8.2. The *same* `400` / `11133` envelope as
618
+ * `model_param_invalid`, with `extError.code` respelled and the `msg`
619
+ * unchanged; a non-canonical value is again what triggers it and the
620
+ * baseline still answers 200. Kept alongside the old spellings rather than
621
+ * replacing them: the rename was observed on the gateways, not promised for
622
+ * every model behind them.
623
+ *
624
+ * The generic codes name no effort field. They are readable here only
625
+ * because the baseline step has already proved the *same* request without
626
+ * `reasoning_effort` succeeds, leaving the sentinel as the only difference
627
+ * between the two attempts — an isolation that holds per endpoint, which is
628
+ * why both regions read the respelled spelling once each was observed on it.
629
+ * A sibling code in the same `11133` envelope that names another parameter
630
+ * (`integer_below_min_value`, the `max_tokens` floor) and a body carrying no
631
+ * `extError` at all (`11102`, unknown model) are therefore not mistaken for
632
+ * them, and neither is a level-sweep answer.
408
633
  *
409
- * Neither is treated as a permanent protocol promise: anything unrecognized
410
- * still degrades to `unknown` rather than to a capability conclusion.
634
+ * None of these is treated as a permanent protocol promise: anything
635
+ * unrecognized still degrades to `unknown` rather than to a capability
636
+ * conclusion.
411
637
  */
412
638
  const INVALID_EFFORT_CODES = {
413
- cn: /* @__PURE__ */ new Set(["invalid_reasoning_effort"]),
414
- global: /* @__PURE__ */ new Set(["invalid_reasoning_effort", "model_param_invalid"])
639
+ cn: /* @__PURE__ */ new Set(["invalid_reasoning_effort", "invalid_parameter_value"]),
640
+ global: /* @__PURE__ */ new Set([
641
+ "invalid_reasoning_effort",
642
+ "model_param_invalid",
643
+ "invalid_parameter_value"
644
+ ])
415
645
  };
416
646
  /** Whether an attempt is an attributable rejection of the effort value. */
417
647
  function isEffortRejection(attempt, region) {
@@ -1504,7 +1734,41 @@ function parseModelCatalog(data, international = false, promoBadges) {
1504
1734
  }
1505
1735
  const models = cliIds.map((id) => byId.get(id)).filter((model) => model !== void 0);
1506
1736
  if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
1507
- return models;
1737
+ return disambiguateDisplayNames(models);
1738
+ }
1739
+ /**
1740
+ * Make display names unique within one catalog.
1741
+ *
1742
+ * The CN document names both `hy3` and `hy3-x` "Hy3" — two neighbouring rows
1743
+ * whose rates differ (`x0.00` free versus `x0.05`), so the picker and the probe
1744
+ * card render two identical rows and the user cannot tell which one they are
1745
+ * paying for. The static fallback spells the second one "Hy3-X", but the live
1746
+ * fetch overwrote that correction: as soon as the roster gained the id, the
1747
+ * collision came back through the parse path.
1748
+ *
1749
+ * The suffix comes from the id itself — whatever follows the shared name stem
1750
+ * — so `hy3-x` reads "Hy3-X" again. Ids are untouched: they are the wire
1751
+ * contract the request path and the shim route on. Only exact (case-
1752
+ * insensitive) collisions are rewritten; a distinct upstream name such as
1753
+ * `MiniMax-M3` for `minimax-m3-pay` keeps the upstream's own wording.
1754
+ */
1755
+ function disambiguateDisplayNames(models) {
1756
+ const seen = /* @__PURE__ */ new Set();
1757
+ return models.map((model) => {
1758
+ const key = model.name.trim().toLowerCase();
1759
+ if (!seen.has(key)) {
1760
+ seen.add(key);
1761
+ return model;
1762
+ }
1763
+ const tail = model.id.toLowerCase().startsWith(key) ? model.id.slice(key.length) : "";
1764
+ if (tail === "") return model;
1765
+ const name = `${model.name}${tail.toUpperCase()}`;
1766
+ seen.add(name.toLowerCase());
1767
+ return {
1768
+ ...model,
1769
+ name
1770
+ };
1771
+ });
1508
1772
  }
1509
1773
  /** Extract the promotions covering `model` from the `modelPromotions` array. */
1510
1774
  function parsePromotions(value, model) {
@@ -2410,7 +2674,7 @@ function workBuddyWindowsDiscoveryTools() {
2410
2674
  if (settled) return;
2411
2675
  settled = true;
2412
2676
  if (error !== null && error !== void 0) {
2413
- if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr.toString("utf8"))) {
2677
+ if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissingFromBytes(stderr)) {
2414
2678
  resolve(Buffer.alloc(0));
2415
2679
  return;
2416
2680
  }
@@ -2829,6 +3093,24 @@ function windowsRegistryKeyMissing(stderr) {
2829
3093
  return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
2830
3094
  }
2831
3095
  /**
3096
+ * Reg.exe speaks the console OEM code page, not UTF-8: on a zh-CN install the
3097
+ * missing-key diagnostic arrives as cp936 bytes, which UTF-8 decoding turns
3098
+ * into mojibake no diagnostic regex can match — the query then reads as
3099
+ * "incomplete" on every absent root. Extension of #88's byte-level principle:
3100
+ * the bytes decide, so decode as UTF-8 first and then as GBK. ASCII — every
3101
+ * English diagnostic — is identical under both, so a match is unambiguous and
3102
+ * recognition only ever widens; a genuine failure (access denied, unknown
3103
+ * diagnostics) still stays incomplete in either decode.
3104
+ */
3105
+ function windowsRegistryKeyMissingFromBytes(stderr) {
3106
+ if (windowsRegistryKeyMissing(stderr.toString("utf8"))) return true;
3107
+ try {
3108
+ return windowsRegistryKeyMissing(new TextDecoder("gbk").decode(stderr));
3109
+ } catch {
3110
+ return false;
3111
+ }
3112
+ }
3113
+ /**
2832
3114
  * Parse the value columns emitted by `reg query ... /s`.
2833
3115
  *
2834
3116
  * Entries are filtered by the product's DisplayName pattern *before* the
@@ -3403,12 +3685,12 @@ var WorkBuddyCredentialStore = class {
3403
3685
  */
3404
3686
  /**
3405
3687
  * Static CLI models observed on the CN endpoint (re-verified against the live
3406
- * `/v3/config` document 2026-09-23, including the thinking-effort and billing
3688
+ * `/v3/config` document 2026-10-17, including the thinking-effort and billing
3407
3689
  * metadata). The upstream refresh replaces this list at startup; it exists so
3408
3690
  * the provider registers with a usable catalog even while the first fetch is
3409
3691
  * in flight or offline.
3410
3692
  *
3411
- * The list tracks the `cli` agent's model roster exactly — the 16 models it
3693
+ * The list tracks the `cli` agent's model roster exactly — the 19 models it
3412
3694
  * offered that day. The roster churns quickly (`auto`, `kimi-k3-1`,
3413
3695
  * `minimax-m3` each appeared or vanished within days, and a competing patch's
3414
3696
  * 2026-09-22 snapshot named three ids that were gone a day later), so this
@@ -3419,6 +3701,11 @@ var WorkBuddyCredentialStore = class {
3419
3701
  * document, and the `free` flag follows the normalized `x0.00` credits
3420
3702
  * marker.
3421
3703
  *
3704
+ * Image capability is also verbatim, and it moved since the previous capture:
3705
+ * `glm-5.2` and `glm-5.1` now report `supportsImages: false`. Claiming it for
3706
+ * them admitted an attachment the upstream then rejected after the message was
3707
+ * durable, so this table follows the document rather than the old snapshot.
3708
+ *
3422
3709
  * Deliberately NOT baked in: promotional badges. `限时免费` and friends are
3423
3710
  * dynamic console-side promotions with no reliable validity window, so a
3424
3711
  * static table would keep them alive long after the offers end. The live
@@ -3430,7 +3717,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3430
3717
  {
3431
3718
  id: "hy4-preview",
3432
3719
  name: "Hy4 preview",
3433
- contextWindow: 1e6,
3720
+ contextWindow: 96e4,
3434
3721
  maxTokens: 64e3,
3435
3722
  supportsImages: true,
3436
3723
  reasoning: {
@@ -3479,6 +3766,52 @@ const FALLBACK_WORKBUDDY_MODELS = [
3479
3766
  free: false
3480
3767
  }
3481
3768
  },
3769
+ {
3770
+ id: "step-5-preview",
3771
+ name: "Step-5-Preview",
3772
+ contextWindow: 1e6,
3773
+ maxTokens: 64e3,
3774
+ supportsImages: true,
3775
+ reasoning: {
3776
+ supports: true,
3777
+ onlyReasoning: true,
3778
+ supportedEfforts: [
3779
+ "low",
3780
+ "medium",
3781
+ "high"
3782
+ ],
3783
+ defaultEffort: "high",
3784
+ canDisableThinking: false
3785
+ },
3786
+ billing: {
3787
+ credits: "x0.00 credits",
3788
+ free: true
3789
+ }
3790
+ },
3791
+ {
3792
+ id: "space-bunny",
3793
+ name: "Space-Bunny",
3794
+ contextWindow: 1e6,
3795
+ maxTokens: 128e3,
3796
+ supportsImages: true,
3797
+ reasoning: {
3798
+ supports: true,
3799
+ onlyReasoning: true,
3800
+ supportedEfforts: [
3801
+ "low",
3802
+ "medium",
3803
+ "high",
3804
+ "xhigh",
3805
+ "max"
3806
+ ],
3807
+ defaultEffort: "max",
3808
+ canDisableThinking: false
3809
+ },
3810
+ billing: {
3811
+ credits: "x0.08 credits",
3812
+ free: false
3813
+ }
3814
+ },
3482
3815
  {
3483
3816
  id: "deepseek-v4.1-flash",
3484
3817
  name: "Deepseek-V4.1-Flash",
@@ -3488,11 +3821,16 @@ const FALLBACK_WORKBUDDY_MODELS = [
3488
3821
  reasoning: {
3489
3822
  supports: true,
3490
3823
  onlyReasoning: true,
3824
+ supportedEfforts: [
3825
+ "low",
3826
+ "high",
3827
+ "max"
3828
+ ],
3491
3829
  defaultEffort: "high",
3492
- canDisableThinking: false
3830
+ canDisableThinking: true
3493
3831
  },
3494
3832
  billing: {
3495
- credits: "x0.03 credits",
3833
+ credits: "x0.11 credits",
3496
3834
  free: false
3497
3835
  }
3498
3836
  },
@@ -3505,7 +3843,12 @@ const FALLBACK_WORKBUDDY_MODELS = [
3505
3843
  reasoning: {
3506
3844
  supports: true,
3507
3845
  onlyReasoning: true,
3508
- defaultEffort: "medium",
3846
+ supportedEfforts: [
3847
+ "low",
3848
+ "high",
3849
+ "max"
3850
+ ],
3851
+ defaultEffort: "max",
3509
3852
  canDisableThinking: false
3510
3853
  },
3511
3854
  billing: {
@@ -3528,19 +3871,41 @@ const FALLBACK_WORKBUDDY_MODELS = [
3528
3871
  "max"
3529
3872
  ],
3530
3873
  defaultEffort: "high",
3531
- canDisableThinking: true
3874
+ canDisableThinking: false
3532
3875
  },
3533
3876
  billing: {
3534
3877
  credits: "x0.06 credits",
3535
3878
  free: false
3536
3879
  }
3537
3880
  },
3881
+ {
3882
+ id: "glm-5.3-flashx",
3883
+ name: "GLM-5.3-FlashX",
3884
+ contextWindow: 1e6,
3885
+ maxTokens: 131072,
3886
+ supportsImages: true,
3887
+ reasoning: {
3888
+ supports: true,
3889
+ onlyReasoning: true,
3890
+ supportedEfforts: [
3891
+ "low",
3892
+ "high",
3893
+ "max"
3894
+ ],
3895
+ defaultEffort: "high",
3896
+ canDisableThinking: false
3897
+ },
3898
+ billing: {
3899
+ credits: "x0.14 credits",
3900
+ free: false
3901
+ }
3902
+ },
3538
3903
  {
3539
3904
  id: "glm-5.2",
3540
3905
  name: "GLM-5.2",
3541
3906
  contextWindow: 1e6,
3542
3907
  maxTokens: 64e3,
3543
- supportsImages: true,
3908
+ supportsImages: false,
3544
3909
  reasoning: {
3545
3910
  supports: true,
3546
3911
  onlyReasoning: true,
@@ -3557,7 +3922,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3557
3922
  name: "GLM-5.1",
3558
3923
  contextWindow: 2e5,
3559
3924
  maxTokens: 48e3,
3560
- supportsImages: true,
3925
+ supportsImages: false,
3561
3926
  reasoning: {
3562
3927
  supports: true,
3563
3928
  onlyReasoning: true,
@@ -3587,10 +3952,10 @@ const FALLBACK_WORKBUDDY_MODELS = [
3587
3952
  }
3588
3953
  },
3589
3954
  {
3590
- id: "minimax-m3",
3955
+ id: "minimax-m3-pay",
3591
3956
  name: "MiniMax-M3",
3592
3957
  contextWindow: 512e3,
3593
- maxTokens: 64e3,
3958
+ maxTokens: 128e3,
3594
3959
  supportsImages: true,
3595
3960
  reasoning: {
3596
3961
  supports: true,
@@ -3616,12 +3981,12 @@ const FALLBACK_WORKBUDDY_MODELS = [
3616
3981
  canDisableThinking: false
3617
3982
  },
3618
3983
  billing: {
3619
- credits: "x0.19 credits",
3984
+ credits: "x0.26 credits",
3620
3985
  free: false
3621
3986
  }
3622
3987
  },
3623
3988
  {
3624
- id: "kimi-k3-1",
3989
+ id: "kimi-k3-2",
3625
3990
  name: "Kimi-K3",
3626
3991
  contextWindow: 1e6,
3627
3992
  maxTokens: 32e3,
@@ -3633,7 +3998,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3633
3998
  canDisableThinking: false
3634
3999
  },
3635
4000
  billing: {
3636
- credits: "x1.62 credits",
4001
+ credits: "x1.62",
3637
4002
  free: false
3638
4003
  }
3639
4004
  },
@@ -4177,7 +4542,7 @@ var WorkBuddyCatalog = class {
4177
4542
  };
4178
4543
  //#endregion
4179
4544
  //#region src/version.ts
4180
- const WORKBUDDY_CONNECT_VERSION = "0.7.6";
4545
+ const WORKBUDDY_CONNECT_VERSION = "0.7.8";
4181
4546
  //#endregion
4182
4547
  //#region src/host-heartbeat.ts
4183
4548
  /**
package/lib/index.d.ts CHANGED
@@ -1129,12 +1129,12 @@ declare class WorkBuddyCredentialStore {
1129
1129
  type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
1130
1130
  /**
1131
1131
  * Static CLI models observed on the CN endpoint (re-verified against the live
1132
- * `/v3/config` document 2026-09-23, including the thinking-effort and billing
1132
+ * `/v3/config` document 2026-10-17, including the thinking-effort and billing
1133
1133
  * metadata). The upstream refresh replaces this list at startup; it exists so
1134
1134
  * the provider registers with a usable catalog even while the first fetch is
1135
1135
  * in flight or offline.
1136
1136
  *
1137
- * The list tracks the `cli` agent's model roster exactly — the 16 models it
1137
+ * The list tracks the `cli` agent's model roster exactly — the 19 models it
1138
1138
  * offered that day. The roster churns quickly (`auto`, `kimi-k3-1`,
1139
1139
  * `minimax-m3` each appeared or vanished within days, and a competing patch's
1140
1140
  * 2026-09-22 snapshot named three ids that were gone a day later), so this
@@ -1145,6 +1145,11 @@ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
1145
1145
  * document, and the `free` flag follows the normalized `x0.00` credits
1146
1146
  * marker.
1147
1147
  *
1148
+ * Image capability is also verbatim, and it moved since the previous capture:
1149
+ * `glm-5.2` and `glm-5.1` now report `supportsImages: false`. Claiming it for
1150
+ * them admitted an attachment the upstream then rejected after the message was
1151
+ * durable, so this table follows the document rather than the old snapshot.
1152
+ *
1148
1153
  * Deliberately NOT baked in: promotional badges. `限时免费` and friends are
1149
1154
  * dynamic console-side promotions with no reliable validity window, so a
1150
1155
  * static table would keep them alive long after the offers end. The live
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as validAppVersion, A as extractDisplayErrorMessage, B as randomSentinel, C as WORKBUDDY_VARIANTS, D as WORKBUDDY_UPDATE_PATH, F as prepareCnChatBody, G as readCliVersion, H as FALLBACK_CN_APP_VERSION, I as prepareInternationalChatBody, J as WORKBUDDY_APP_VERSION_FILENAME, K as resolveChatIdentity, L as regionOf, M as normalizeCredits, N as parseModelCatalog, O as WorkBuddyUpstreamClient, P as prepareChatBody, Q as resolveAppVersion, R as PROBE_EFFORT_CANDIDATES, S as CN_VARIANT, U as chatUserAgent, V as CN_APP_VERSION_FILENAME, W as fallbackChatIdentity, X as installedAppVersion, Y as appUserAgent, Z as readBundleVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as modelWithCurrentPromotion, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as validCliVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as variantFor, x as AI_VARIANT, y as workbuddyOwnAuthPath, z as probeModel } from "./host-heartbeat-vU86u9aU.js";
1
+ import { $ as validAppVersion, A as extractDisplayErrorMessage, B as randomSentinel, C as WORKBUDDY_VARIANTS, D as WORKBUDDY_UPDATE_PATH, F as prepareCnChatBody, G as readCliVersion, H as FALLBACK_CN_APP_VERSION, I as prepareInternationalChatBody, J as WORKBUDDY_APP_VERSION_FILENAME, K as resolveChatIdentity, L as regionOf, M as normalizeCredits, N as parseModelCatalog, O as WorkBuddyUpstreamClient, P as prepareChatBody, Q as resolveAppVersion, R as PROBE_EFFORT_CANDIDATES, S as CN_VARIANT, U as chatUserAgent, V as CN_APP_VERSION_FILENAME, W as fallbackChatIdentity, X as installedAppVersion, Y as appUserAgent, Z as readBundleVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as modelWithCurrentPromotion, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as validCliVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as variantFor, x as AI_VARIANT, y as workbuddyOwnAuthPath, z as probeModel } from "./host-heartbeat-sjotAEdb.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
5
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
8
  import { createProvider } from "@earendil-works/pi-ai";
9
9
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -1159,7 +1159,7 @@ var WorkBuddyProbeService = class {
1159
1159
  //#region src/pi-ai-guard.ts
1160
1160
  /**
1161
1161
  * Confirm the pi-ai copy this plugin actually loaded is the generation the DSH
1162
- * 0.2.0-rc.2 host was built against.
1162
+ * 0.2.1-rc.1 host was built against.
1163
1163
  *
1164
1164
  * #92's failure mode: a pnpm profile with `nodeLinker: hoisted` and
1165
1165
  * `autoInstallPeers: false` installs no peer for us, so a co-installed
@@ -1178,7 +1178,7 @@ var WorkBuddyProbeService = class {
1178
1178
  * @module dsh-workbuddy-connect/pi-ai-guard
1179
1179
  */
1180
1180
  /** The pi-ai generation this plugin is built and tested against. */
1181
- const REQUIRED_PI_AI_VERSION = "0.87.1";
1181
+ const REQUIRED_PI_AI_VERSION = "1.3.0";
1182
1182
  /**
1183
1183
  * Read the version of the pi-ai copy Node resolves for *this* module.
1184
1184
  *
@@ -1225,7 +1225,7 @@ function resolvedPiAiVersion() {
1225
1225
  */
1226
1226
  function piAiVersionWarning(resolved) {
1227
1227
  const version = arguments.length === 0 ? resolvedPiAiVersion() : resolved;
1228
- if (version === void 0 || version === "0.87.1") return void 0;
1228
+ if (version === void 0 || version === "1.3.0") return void 0;
1229
1229
  return `loaded @earendil-works/pi-ai ${version}, but this plugin is built for ${REQUIRED_PI_AI_VERSION}. The wrong copy is being resolved — check the profile's pi-ai install (a pnpm profile with nodeLinker "hoisted" and autoInstallPeers false can expose another package's older copy). Reinstalling the plugin so it installs its own ${REQUIRED_PI_AI_VERSION} copy resolves it`;
1230
1230
  }
1231
1231
  //#endregion
@@ -1282,6 +1282,8 @@ async function workBuddyWebStatus(deps) {
1282
1282
  ...model.billing?.badges !== void 0 && model.billing.badges.length > 0 ? { badges: model.billing.badges } : {},
1283
1283
  ...rate === void 0 ? {} : { credits: rate },
1284
1284
  ...model.billing?.rateUnknown === true ? { rateUnknown: true } : {},
1285
+ ...(model.reasoning?.supportedEfforts?.length ?? 0) > 0 ? { declaredEfforts: model.reasoning.supportedEfforts } : {},
1286
+ ...model.reasoning?.defaultEffort !== void 0 ? { defaultEffort: model.reasoning.defaultEffort } : {},
1285
1287
  ...typeof model.contextWindow === "number" && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {},
1286
1288
  ...typeof defaultContextWindow === "number" && defaultContextWindow > 0 && defaultContextWindow < model.contextWindow ? { defaultContextWindow } : {},
1287
1289
  ...maxContextWindow === void 0 || maxContextWindow <= defaultContextWindow ? {} : { maxContextWindow },
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-connect",
3
3
  "displayName": "DSH WorkBuddy Connect",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.7.6",
5
+ "version": "0.7.8",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",
@@ -41,7 +41,8 @@
41
41
  "cordis.patch.yml",
42
42
  "README.md",
43
43
  "README.en.md",
44
- "LICENSE"
44
+ "LICENSE",
45
+ "THIRD_PARTY_NOTICES.txt"
45
46
  ],
46
47
  "license": "MIT",
47
48
  "repository": {
@@ -67,42 +68,41 @@
67
68
  }
68
69
  },
69
70
  "dependencies": {
70
- "@earendil-works/pi-ai": "0.87.1"
71
+ "@earendil-works/pi-ai": "1.3.0"
71
72
  },
72
73
  "peerDependencies": {
73
- "@deepseek-ai/cordis": "^4.0.2",
74
- "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
75
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
76
- "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
78
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
79
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.2",
80
- "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
81
- "@deepseek-ai/schemastery": "^3.18.2",
74
+ "@deepseek-ai/cordis": "^4.0.5",
75
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
76
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
77
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
78
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
79
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.2",
80
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
81
+ "@deepseek-ai/schemastery": "^3.18.5",
82
82
  "react": "^18.2.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@deepseek-ai/cordis": "^4.0.4",
86
- "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
87
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
88
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
89
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
90
- "@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.2",
91
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
92
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.0-rc.2",
93
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
94
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
95
- "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
96
- "@deepseek-ai/dsh-authorization": "0.2.0-rc.2",
97
- "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
98
- "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
99
- "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
100
- "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
101
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
102
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.2",
103
- "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
104
- "@deepseek-ai/schemastery": "^3.18.2",
105
- "@earendil-works/pi-ai": "0.87.1",
85
+ "@deepseek-ai/cordis": "^4.0.5",
86
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.2",
87
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
88
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.2",
91
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
92
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.2",
93
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
94
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
95
+ "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
96
+ "@deepseek-ai/dsh-authorization": "0.2.1-rc.2",
97
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
98
+ "@deepseek-ai/dsh-launch-environment": "0.2.1-rc.2",
99
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
100
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
101
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
102
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.2",
103
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
104
+ "@deepseek-ai/schemastery": "^3.18.5",
105
+ "@earendil-works/pi-ai": "1.3.0",
106
106
  "@types/node": "^22.20.0",
107
107
  "@types/react": "~18.3.1",
108
108
  "@types/react-test-renderer": "^18.3.1",