dsh-connect-workbuddy 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,140 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.2 (2026-09-29)
4
+
5
+ ### ⚠️ 兼容性修复:peer 上界 `<0.2.0` 会在 0.2.0 落地时把整个组合包静默判死
6
+
7
+ 本版**无功能改动**,只修一处**声明**——但这是一处会在下一个 DSH 版本落地那一刻生效、且**用户侧完全无提示**的缺陷。
8
+
9
+ #### 缺陷
10
+
11
+ 10 条 `dsh-*` peer 此前写作 `>=0.1.7-rc.1 <0.2.0`。这个上界在写下的当时是对的,但它有一个**只在下一个版本线落地时才显形**的形状:
12
+
13
+ - `0.2.0-rc.1 < 0.2.0` 成立 → 整条 0.2.0 **预发布**线被放行(所以 0.2.0-rc.1 上一切正常)
14
+ - `0.2.0 < 0.2.0` 不成立 → 正式版一发布,**10 条 peer 全部失败**
15
+
16
+ DSH 的随包门禁 `evaluatePluginCompatibility`(`packages/boot/app-boot/src/plugin-compatibility.ts`)逐条判定这 10 条;**只要一条不满足,整个组合包就被 `loadProfileDirectory` 跳过**,只写进 `skippedBundles` 并由 `reportSkippedBundles` 打到 stderr。它不是崩溃,是**静默消失**:provider 不注册、卡片不出现、模型列表清空,页面上没有任何报错。
17
+
18
+ - **取证方式**:把 0.2.0-rc.1 tag 里的 `plugin-compatibility.ts` **原样取出**,喂进本插件真实的 `package.json`。`0.2.0` / `0.2.1` / `0.3.0-rc.1` 全部 `DENIED(10/10 peers fail)`,而 `0.2.0-rc.1` / `0.2.0-rc.2` 通过——即「预发布能用、正式版不能用」的悬崖。
19
+ - **这条路径并非假想**:同机实测,profile 里 `dsh-rewind-plugin`(`^0.1.7-rc.2`)、`dsh-free-search`(`^0.1.7-rc.1`)、`dsh-better-reasoning-effort`、`@changfenhuang/dsh-genui` 四个组合包**已经**因同样的原因没有出现在实时组合里——`listConfigs` 共 203 条,其中无一条来自它们;同 profile 内 peer 全通过的包则全部在场。
20
+
21
+ #### 修法:上界改为 `<0.3.0-0`
22
+
23
+ **为什么带 `-0`**:DSH 至今 **23 个 tag 全部是预发布**(`-alpha.N` / `-rc.N`),从未发过 GA。因此:
24
+
25
+ - `<0.3.0` 只挡 `0.3.0` GA——而 GA 永远不来——上界**永不生效**,同时静默放行 `0.3.0-alpha.1` / `0.3.0-rc.1` 整条从未验证过的线。**这正是今天这个缺陷的形状:看起来是围栏,实际不挡任何东西。**
26
+ - `<0.3.0-0` 显式挡掉 `0.3.0-0`(含)以上的一切预发布与 GA,围栏真的会响。
27
+
28
+ `-0` 是 semver 里「该版本线的最小可能预发布」,所以 `<0.3.0-0` 意为「0.3.0 线一个都不放」。同作者同架构的 `dsh-connect-trae` 2.3.0 对同一处缺陷的处置与此一致。
29
+
30
+ | 宿主 | 改前 | 改后 |
31
+ | --- | --- | --- |
32
+ | `0.1.7-rc.1` / `0.1.7-rc.2` | admitted | admitted(**不回归**) |
33
+ | `0.2.0-rc.1` / `0.2.0-rc.9` / **`0.2.0`** / `0.2.1` / `0.2.99` | rc 通过、**GA 被拒** | admitted |
34
+ | `0.3.0-0` / `0.3.0-alpha.1` / `0.3.0-rc.1` / `0.3.0` | — | **DENIED**(下一线仍未验证,符合原意) |
35
+
36
+ `0.1.7-alpha.1/alpha.2` 改前改后都是 DENIED(`>=0.1.7-rc.1` 下界所限),属既有行为,不是本次引入的回归。
37
+
38
+ #### 顺带移除一处过期声明:`@deepseek-ai/dsh-client-runtime`
39
+
40
+ 该包停在 `0.1.1-rc.2`、不在任何受支持主机的捆绑集内,且 **源码零引用**(`src/` 里只有两处注释提到它;构建产物 `lib/client.js` 的 `require` 只有 `react` / `react/jsx-runtime`)。它此前作为「旧主机行的类型来源」被保留,但那条线已随 2.1.0 移除。现连同 **devDependency、pnpm override、tsdown 的 `neverBundle` external** 一并删除——保留一个零引用的 override 只会让清单与 lockfile 继续声称一份并不存在的依赖。
41
+
42
+ #### Tests
43
+
44
+ 新增 `tests/dsh-line.spec.ts`(9 例):守卫上下界口径、**上界真的挡得住**、下界不放松、两个客户端包不掉队、过期 devDependency 不回流。
45
+
46
+ - **关键设计**:边界断言**对声明本身求值**,而不是对测试里另写一遍的字面量求值——否则清单与断言各自漂移也能全绿,那正是本套件要防的「看起来是围栏、实际不挡」缺陷换了一层。为此自带一个小而完整的 semver 序比较(本仓库无 `semver` 依赖),并附一条**比较器自检**用例:它错了,整套守卫就是装饰品。
47
+ - **变异验证**(5 处,逐一实测变红):改回 `<0.2.0` → 3 例红;`<0.3.0-0` 改成 `<0.3.0` → 3 例红;下界抬到 `>=0.2.0-rc.1` → 3 例红;**只让 10 条里的一条漂移** → 4 例红;把 `dsh-client-runtime` 塞回 devDependencies → 2 例红。对照组(未变异)9 例全绿。
48
+
49
+ #### Docs
50
+
51
+ - `README.md` / `README.en.md`:「版本要求」一节补上**当前验证窗口**(`0.1.7-rc.1` ~ `0.2.x`)与新增的「受支持的宿主范围」表(含 0.3.0 线被明确拒绝),并就地说明 `<0.3.0-0` 与 `<0.3.0` 的区别。两语一致。
52
+ - README 里指向 `tests/` 的引用写成代码体而非 Markdown 链接:`tests/` 不在 npm `files` 白名单内,写成链接在 npm 上会是死链——与 2.1.1 处理 `docs/WINDOWS.md` 时同一考量。
53
+
54
+ #### 复核结论:对 0.2.0-rc.1 零代码改动
55
+
56
+ 除上述声明外,**代码一行未改**。逐包核对 0.2.0-rc.1 tag,本插件触碰的 12 个内核包其 `src/` 树与 0.1.7-rc.2 **逐字节相同**(`git rev-parse <tag>:<path>/src` 树哈希相等):
57
+
58
+ `dsh-llm`、`dsh-llm-pi-ai`、`dsh-settings`、`dsh-host-webserver`、`dsh-atomic-write`、`dsh-home-paths`、`dsh-attachment`、`dsh-client-ui-settings`、`dsh-client-ui-slots`、`dsh-client-ui-renderer`、`dsh-client-ui-settings-plugins`、`dsh-client-locale` —— 仅 `package.json` 的 `version` 字段由 `0.1.7-rc.2` 变为 `0.2.0-rc.1`。
59
+
60
+ 运行时契约同步核对:`plugins.bundle.config` / `plugins.row.config` 两个槽位仍在且分发点未变(`renderSlot(..., { entryKey })` 三行未改动);客户端 `configForms` 服务与 `PluginConfigViewProps` 契约未变;主题令牌只增未减(插件使用的 `--dsw-alias-state-error-primary` / `-warn-primary` 均在)。实机状态:0.2.0-rc.1 宿主上 `/plugins/dsh-connect-workbuddy/usage` 返回 **200** 真实数据,两个槽位均由 `dsh-connect-workbuddy-client` 占用且 `active: true`,宿主 Config 条目 `include:dsh-connect-workbuddy` 状态为 `schema`。
61
+
62
+ ## 2.1.1 (2026-09-26)
63
+
64
+ ### Features
65
+
66
+ - **平台支持口径明确化:Windows 是一等目标平台。** 此前 README 只在**散落的五处**顺带提到 Windows(凭据路径、加密字段、`settings.yaml` 占用、路径未验证、CLI),**没有任何一处说「支持 Windows」**——读者要自己把五处拼起来才知道这是受支持平台。本次把它写成明示口径,并把「以后每次改动都要考虑 Windows」落到仓库里,而不是留在会话或记忆里。
67
+
68
+ - **`README.md` / `README.en.md` 新增「平台支持 / Platform support」一节**:一张三平台对照表(凭据默认目录、桌面 App 可执行文件的定位方式、心跳时间戳来源、设置写入的瞬时占用、CI 覆盖),加 Windows 上需要知道的三个具体点。表内每格都是**实现事实**(对应代码位置与测试已登记在 `docs/WINDOWS.md`),不是「尚未支持」的委婉说法。
69
+ - **新增 `docs/WINDOWS.md`(平台约束登记处)**:登记 Windows 专属分支(初版四处,本次追加 §1-5 后为五处;改错会怎样)、**证据分级 A/B/C/D**(真机取证 / CI 实测 / 约定推导 / 未验证,写结论时措辞必须与档位相称)、Windows 专有故障历史(2.0.6 / 2.0.11 / 2.0.12 / issue #11 #13 #15)、改动检查清单、尚未做的事。该文件随 npm 包分发(`files` 白名单),否则 README 里指向它的链接在 npm 上会是死链。
70
+ - **`RELEASING.md` 新增 §2.5「平台核对」**:每次发布前过一遍 `docs/WINDOWS.md` §4 的检查清单,并核对 CI 矩阵仍含 `windows-latest`、两条 README 的平台节与登记处三者一致。这是唯一**每次发布都必然被读到**的位置,因此把口径挂在这里。
71
+ - **`.gitattributes`(新增)**:仓库此前没有换行符约定,而文本文件在索引里都是 LF。Git for Windows 默认 `core.autocrlf=true` 会在检出时转成 CRLF,于是同一份测试在 Windows 与 POSIX 上读到的字节不同——本仓库的测试会直接读源码文本,这类断言必须跨平台稳定。现显式声明 `* text=auto eol=lf` 与各文本类型、并把图片等标为 `binary`。
72
+ - **为什么值得写进文件而不是只嘱咐一句**:这四个要素(CI 矩阵的 Windows 项、登记文件、两条 README 的平台节、发布清单)**每一个都是一行之差就会静静消失的东西**,而消失时不会有任何东西报错——CI 会继续全绿,构建会继续成功,README 会继续正常渲染。据此加了 `tests/platform-standing.spec.ts` 作为守卫。
73
+ - **`docs/WINDOWS.md` §7「在 Windows 真机上验证」**:一份可照做的操作手册——先跑 `doctor --json` 一次拿全四项证据(附字段→分支对照表),再逐项验 §1 的各分支,重点是最值得实测的 §1-4(设置写入的瞬时占用,附可直接粘贴的 PowerShell 锁文件脚本),最后给出回报时应附的信息,以便把 §2 里那些 C 档结论升为 A 档。本次已在真机执行过其中大部分,结果记录在 §7.0。
74
+ - **顺带查实并改正一处事实错误:被写入的是 profile 配置,不是 `settings.yaml`。** 撰写 §7.3 的锁文件脚本时才发现,0.1.7 线上配置文档是 profile 目录下的 **`cordis.patch.yml`**(`config-editor` 的 `documentPath`;易失字段也经同一条 `edit()` 落在那里),而 `settings.yaml` 在该线**只用于一次性导入旧版本配置**,导入后即改名为 `settings.yaml.imported`。插件此前在**代码注释、README(两语)、以及一条面向用户的报错文案**里都写着 `settings.yaml`——在「Windows 上文件被占用」这个头号限制的场景里,这等于把用户指向一个不存在的文件。现已全部改正;面向用户的文案改为「当前 profile 的配置文件」并指向 `docs/WINDOWS.md`(不写死具体文件名,避免随 DSH 版本再次过期)。
75
+
76
+ ### Bug Fixes
77
+
78
+ #### Windows 宿主内每次进程存活判定都闪一个黑色控制台窗口
79
+
80
+ `src/host-heartbeat.ts` 的 `processStartTimeMs()` 用 `execFileSync('powershell', …, { encoding: 'utf8' })` 启动子进程,**没有设 `windowsHide`**——而同一仓库的 `src/at-rest.ts` 在取 at-rest 密钥时设了。两处不一致,缺的那一处就是缺陷。
81
+
82
+ - **为什么在 macOS 与终端里永远看不到**:Windows 只在**父进程没有控制台**时才给控制台程序子进程新建窗口。终端里父进程有控制台,子进程只是继承,现象根本不存在;`windowsHide` 在 POSIX 上又是无操作。所以这个缺陷对本地开发与现有测试**完全不可见**。
83
+ - **而真实宿主正是会触发的形态**:`DSH NEXT.exe` 是 Electron GUI 进程,实测 `MainWindowHandle = 0`、无控制台。于是每次 `isHeartbeatProcessAlive()`(`status` / `doctor` 都会走到)都会在用户屏幕上新建并显示一个控制台窗口——即「黑框一闪」。
84
+ - **真机取证**(Windows 11 22621,用 WMI 服务 `Win32_Process.Create` 造出与 GUI 宿主同为「无控制台」的父进程,再由子进程用 `kernel32!GetConsoleWindow` + `user32!IsWindowVisible` 自报它实际拥有的控制台):
85
+
86
+ | 父进程形态 | 不设 `windowsHide` | 设 `windowsHide: true` |
87
+ | --- | --- | --- |
88
+ | 终端(有控制台) | 继承,无新窗口 | 继承,无新窗口 |
89
+ | WMI 服务创建(无控制台,等价于 GUI 宿主) | **`NEW-console hwnd=1573500 visible=True`** | `no-console` |
90
+
91
+ - **同时排除一条错误判据**:不能用 `Get-Process powershell | MainWindowHandle` 观察。控制台窗口归 `conhost.exe` 所有,控制台程序自身的 `MainWindowHandle` 恒为 0——即使窗口正在屏幕上,该探针也报 0(本次实测中它先给出了假阴性)。
92
+ - **修法**:抽出 `PROCESS_PROBE_OPTIONS`(`{ encoding: 'utf8', windowsHide: true }`)与 `processStartProbe(pid, platform)`。后者的 `platform` 可注入,与 `defaultDesktopAuthDirs()` / `workbuddyAppExecutableCandidates()` 同一套缝——使平台分支与启动选项**在任意主机上都可断言**,这是此类「Windows 专有、macOS 上不可能复现」缺陷唯一能被回归拦住的形态。
93
+ - **守卫与变异验证**:`tests/host-heartbeat.spec.ts` 断言选项值与平台分支;`tests/platform-standing.spec.ts` 新增用例要求**两个** spawn 点都带 `windowsHide: true`。去掉该选项 → 对应用例变红(已实测)。`docs/WINDOWS.md` 登记为 §1-5 分支并附 A 档取证。
94
+
95
+ #### `docs/WINDOWS.md` 里 issue #13 的「重试预算(200+600ms)」是错的
96
+
97
+ 该数字没有任何实现支撑。核对上游 `@deepseek-ai/dsh-atomic-write`(rc.1 与 rc.2 常量相同)后,真实预算是**延迟序列 20+40+80+160+200×4 ≈ 1100ms**(`initial 20ms` / `max 200ms` / `limit 8`)——**不存在 600ms 这一项**。
98
+
99
+ - **真机实测**(同一台 Windows 11,逐一量持有期与结果):持有 0 / 100 / 300 / 600 / 1000ms 后释放 → 写入**全部成功**(端到端 3 / 174 / 339 / 760 / 1165ms);持有 60s(超出预算)→ `EPERM`,且目标文件**保持原内容**、不产生半写文件(已验证 `onDisk` 仍为原值)。
100
+ - 现已按实测改写 `docs/WINDOWS.md` §3。这条属于**文档里的数字与实现脱节**:它不影响行为,但会误导后来者按错误的预算去调参或归因。
101
+
102
+ #### `dsh.client.inject` 仍依赖 `dsh-client-ui-primitives`
103
+
104
+ `ddd11ed`(上游复审那次提交)第 6 条把卡片折叠箭头从宿主图标 `IconChevronDownOutline14` 改为**纯 CSS caret**,并删掉了该包在 `src/client/` 的最后一处导入——但 `package.json` 的 `dsh.client.inject` 里的 `@deepseek-ai/dsh-client-ui-primitives` 留了下来。声明比代码活得久。
105
+
106
+ - **为什么不是无害的多余声明**:`inject` 不是「仅供参考的清单」,但**它的作用范围比初看要窄**——两条机制各自加了一道限制,本条据实说明:
107
+ 1. **它是加载/预取元数据**。内核在 `arriveGraphRow` 里沿它走 `arriveDependency`(`packages/client/modules/src/client/system.ts:268`),把该包列为「本行物化前必须先到」;`prune` 也把它当保留根(同文件 `:448`)。内核自己的注释把语义定得很明确:「`inject` names package rows whose factories must arrive before this row materializes」(`manifest.ts:47`),而 `ui-workspace` 的注释补了另一半:**「`dsh.client.inject` edges are informational (loading/prefetch metadata, never apply sequencing)」**(`packages/client/ui-workspace/src/client/index.ts:90`)。**不是 apply 顺序约束**。
108
+ 2. **目标是 graph row 才成边**。`arriveGraphRow` 取依赖时带 `if (dependency !== undefined)` 守卫(`system.ts:269`),而 `graphRows` 只装入 `dsh.client` 声明过的包(`system.ts:133` ← `index.ts:837`)。`ui-primitives` **没有** `dsh.client` 声明,它是平台 seed 词(`packages/client/web/src/seed.ts:36`、`platform.ts:12`)——**因此它永远不在模块图里,这一条 inject 条目实际不产生任何加载边**。
109
+ - **那么它为什么仍该删**:它是**过期声明**,不是运行时炸弹。真实成本是「声明与代码不一致」本身——保留一个零引用条目,等于向未来的维护者与工具谎称本插件依赖该包;一旦哪天该包被移出平台 seed 表(它并非不可能:该包在本区间改动 75 个文件、约 1500 行,且是唯一删除了导出 `OnboardingSurface` 的一个),这条就会从「无效」变成「生效的隐式依赖」,而那时没人记得它是怎么来的。清掉它,是让声明回到只描述真实依赖的状态。
110
+ - **修法**:从 `inject` 删除该条目(`package.json` 的 `peerDependencies` / `devDependencies` / `tsdown.config.ts` 的 externals 保留——那是类型检查与构建所需,与运行时装配无关)。
111
+ - 另:`src/client/WorkBuddyCard.tsx` 的文件头注释至今仍写着「`IconChevronDownOutline14` 的使用…来自该项目」,而该图标导入早在 `ddd11ed` 就已删除(改成纯 CSS caret)。本次一并更正该注释——它就是这条过期声明当初的同类残留。
112
+
113
+ #### 卡片主题令牌名写错:`state-warning-primary` 在两条 0.1.7 线上都不存在
114
+
115
+ 加密凭据原因的着色用了 `--dsw-alias-state-warning-primary`,而宿主真实令牌是**缩写形** `--dsw-alias-state-warn-primary`(`packages/client/ui-theme/src/styles/design-platform.css`,在 rc.1 与 rc.2 上**都**只有缩写形)。
116
+
117
+ - **为什么静默**:该声明带十六进制兜底色 `#e0a13a`。CSS 自定义属性名不存在时 `var()` 走兜底——规则从不空白,于是**兜底值在每一次渲染都生效**,宿主令牌从未被用上,颜色与设计系统彻底脱钩。写错一个词,不会有任何报错、告警或视觉空白来提示。
118
+ - **修法**:令牌改为 `--dsw-alias-state-warn-primary`,兜底值改为该令牌的真实解析值 `#f59e0b`(`--dsw-static-amber-500`)。
119
+ - **实际可见变化**:加密凭据那条原因的着色由 `#e0a13a` 变为 `#f59e0b`(宿主设计系统的 amber)。据实说明一点:`--dsw-static-amber-500` 在 light 与 dark 两处定义**同为 `rgb(245,158,11)`**,即该令牌是**主题不变量**——所以这次修复不是「颜色开始跟随主题」,而是「颜色回到宿主令牌这条正确的取值路径上」:取值此后由宿主决定,且兜底值与令牌一致,即便令牌缺失也不会再显示成一个无关的颜色。
120
+ - 这不是 0.1.7-rc.2 引入的问题,是**既有缺陷**;rc.2 只是提供了发现它的时机。内核自己的决策记录点名过同一处混淆(`.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md:71`)。
121
+
122
+ ### Tests
123
+
124
+ - 新增 `tests/client-host-contract.spec.ts`(6 例):上述两处都是**声明性**缺陷,因此测试读文件(`package.json` 与 `src/client/styles.ts`)而非导入它们——也让本套件不必拖入浏览器专属的 DSH 客户端包。用例覆盖:`inject` 不得出现客户端半边零引用的包;不得依赖 `ui-primitives`;四条真正参与组合的客户端包同时在册(防反向丢漏);不得使用 `state-warning-primary` 长形;`state-*` 令牌必须落在宿主定义集内;兜底值必须是宿主 amber。
125
+ - **变异验证**:把 `ui-primitives` 塞回 `inject` → **2 例变红**;把令牌改回 `state-warning-primary`/`#e0a13a` → **3 例变红**。两处缺陷各自单独还原都会被抓住。
126
+ - 记录一条本次核实到的**反直觉事实**,避免后人写下错误的断言:`inject` **不是** value imports 的镜像。内核自身两个方向的反例都在——`ui-settings-plugins` 用裸 `import type {}` 做模块增强却照样 inject 那两个包(`src/client/index.ts:11,15`);`ui-theme` 实际导入并使用 `ui-primitives` 的**组件**(`AppearanceRow.tsx:11`)却**不** inject 它。故本套件只断言「inject 里不得有零引用项」这一单向不变量,不碰反向。
127
+
128
+ - 新增 `tests/platform-standing.spec.ts`:把「**Windows 是一等目标平台**」这条长期口径固化成可执行的守卫(见上方 Features 的说明)。守卫两头:**政策面**(CI 矩阵仍含 `windows-latest`、登记文件与两条 README 的平台节仍在且互相指得通、`RELEASING.md` 的平台核对仍在、`DESIGN.md` 仍指向登记处),以及**代码面**(`docs/WINDOWS.md` §1 登记的 `win32` 分支仍存在、生产源码不得把平台路径写成 POSIX 字面量)。**变异验证**:CI 矩阵去掉 `windows-latest` → 1 例变红;README 把「一等目标平台」改软 → 1 例变红;`auth.ts` 的 `win32` 分支改名 → 1 例变红。本次追加 1 例(要求两个 spawn 点都带 `windowsHide: true`),并把心跳分支的断言改指向新缝 `processStartProbe(pid, platform)`——**变异验证**:去掉 `windowsHide` → 该例变红。
129
+ - 另新增 2 例(同文件)守卫本次顺带查实的事实:被写入的配置文档是 profile 的 `cordis.patch.yml`,不是 `settings.yaml`。**变异验证**:把用户文案改回 `settings.yaml` → 1 例变红;把 `docs/WINDOWS.md` §7.3 里「不是 `settings.yaml`」的澄清改掉 → 1 例变红。
130
+
131
+ ### Docs
132
+
133
+ - **修正两处已过期的「推理强度两态」表述**——文档还停在「未修复」,而代码早在 **2.0.10**(`4c39645`,issue #7)就已修复。纯文档纠正,**无代码改动**。
134
+ - `docs/DESIGN.md`:「推理强度两态(已知问题)」改为「已修复」,并补上实际实现(`parseReasoning` 两态解析、`singularEffortLadder` 折叠为全阶梯 `low/medium/high/xhigh/max`、单数值折入 `defaultEffort`、`canDisableThinking` 视为 `true`)与两网关实测依据;同节字段表里 `reasoning.effort` 的「固定推理档位」改为「折叠为全阶梯可选档位,该值作默认档」。
135
+ - `docs/reasoning-investigation.md`:状态由「调查完成,**未修复**」改为「已修复(issue #7,随 2.0.10)」,并**就地标注**其余过期处——第三节(根因)标为历史代码形态;第四节的推断「不发 `reasoning_effort` 时上游按默认 `effort` 运行、这些模型一直在思考」被修复时的实测**推翻**(实测为**零思考**,即默认思考关闭);第七节「待决策」标为已决策(采纳「结合实测」一路);第八节补「落地情况」列,如实区分已落地与未采纳(`supportsReasoning` / `onlyReasoning` 至今确实未解析,属拟议增强而非缺陷);第九节改标各文件实际改动状态。
136
+ - 保留原文与就地注记,不删史——记录当时的推理路径,避免后人重复踩。
137
+
3
138
  ## 2.1.0 (2026-09-25)
4
139
 
5
140
  ### ⚠️ 版本基线:仅支持 DSH 0.1.7-rc.1 及以上
package/README.en.md CHANGED
@@ -54,7 +54,22 @@ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file.
54
54
 
55
55
  ## Install
56
56
 
57
- > ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer.** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15.
57
+ > ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer (current window: `0.1.7-rc.1` – `0.2.x`).** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is **`<0.3.0-0`**: the **whole 0.2.0 line, GA included, is verified**; the 0.3.0 line is explicitly refused. See [Supported host range](#supported-host-range) below.
58
+
59
+ ### Supported host range
60
+
61
+ DSH's shipped gate (`evaluatePluginCompatibility` in `packages/boot/app-boot/src/plugin-compatibility.ts`) checks every declared `dsh-*` peer range; **one unsatisfied peer skips the entire bundle** (it lands in `skippedBundles` and is only printed to stderr) — no provider is registered, no card appears, the model list goes empty, and the page shows no error at all.
62
+
63
+ | Host version | Verdict |
64
+ | --- | --- |
65
+ | `0.1.7-rc.1` – `0.1.7-rc.2` | ✅ supported |
66
+ | `0.2.0-rc.1` – `0.2.x` (`0.2.0` GA included) | ✅ supported (verified from 2.1.2) |
67
+ | `0.3.0-0` and above | ❌ explicitly refused (that line is unverified) |
68
+ | `0.1.6` and earlier | ❌ explicitly refused (0.1.5 users should stay on v2.0.15) |
69
+
70
+ > The upper bound is written `<0.3.0-0`, not `<0.3.0`: **every DSH tag to date is a prerelease** (`-alpha.N` / `-rc.N`), so `<0.3.0` alone would admit the entire unverified `0.3.0-alpha.1` / `0.3.0-rc.1` line, leaving the guardrail inert — exactly the defect shape 2.1.2 fixes. `tests/dsh-line.spec.ts` guards this contract.
71
+
72
+ > 🪟 **Platform support: Windows, macOS, and Linux are supported side by side.** **Windows is a first-class target**, not an afterthought: CI runs the full suite on `windows-latest`, and four areas carry Windows-specific branches with their own tests — credential paths, process heartbeat, desktop-app executable lookup, and settings writes. **When fixing a defect that only reproduces on Windows, the Windows reporter's field evidence and an encryption vector captured from a real Windows install are what the fix is judged against** (see the 2.0.6 / 2.0.11 / 2.0.12 sections of [CHANGELOG](CHANGELOG.md)). See the [Platform support](#platform-support) section for the per-platform differences and known limitations.
58
73
 
59
74
  Prerequisite: the WorkBuddy desktop app is installed and signed in (the plugin reuses the app's sign-in state).
60
75
 
@@ -77,6 +92,25 @@ dsh plugin --profile web add dsh-connect-workbuddy # Web
77
92
  dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
78
93
  ```
79
94
 
95
+ ## Platform support
96
+
97
+ All three platforms are supported targets; the differences below are **implementation facts**, not polite ways of saying "not yet supported". To change this section, change [`docs/WINDOWS.md`](docs/WINDOWS.md) too — that file is where platform-related constraints are registered.
98
+
99
+ | Aspect | Windows | macOS | Linux |
100
+ | --- | --- | --- | --- |
101
+ | Default credential directory | `%LOCALAPPDATA%` / `%APPDATA%` first, falling back to `<home>\AppData` when unset (so a redirected profile still resolves) | `~/Library/Application Support/CodeBuddyExtension/Data/Public/auth` | `$XDG_CONFIG_HOME` first, falling back to `~/.config` |
102
+ | Desktop-app executable | `%LOCALAPPDATA%\Programs\WorkBuddy`, `%LOCALAPPDATA%\WorkBuddy`, and both `Program Files` (located as `WorkBuddy.exe`) | Read from the bundle's `Info.plist` (`CFBundleExecutable`) — the real value is **`Electron`**, never guessed from the app name; covers the international `WorkBuddy AI.app` and apps filed under a subdirectory of `/Applications` | Not applicable (no corresponding app for encrypted credentials on Linux; point `WORKBUDDY_AUTH_FILE` at the credential) |
103
+ | Process-heartbeat timestamp source | PowerShell `Get-Process` `StartTime` (POSIX has no `ps -lstart` equivalent) | `ps -o lstart=` | `ps -o lstart=` |
104
+ | Child-process window | **`windowsHide` is required**: the host itself has no console (it is a GUI process), so without it every probe flashes a visible black window on screen | Not applicable (the option only takes effect on Windows) | Not applicable |
105
+ | Transient settings-file contention | **Does hit it**: an antivirus scanner / OneDrive / an editor locking the profile's `cordis.patch.yml` briefly makes the rename replace fail, and `@deepseek-ai/dsh-atomic-write`'s retries are enabled **only on `win32`** | Does not: POSIX rename replaces outright, so the same code never enters the retry branch | Same as macOS |
106
+ | CI coverage | `windows-latest`, full suite | The developer's own Mac | `ubuntu-latest`, full suite |
107
+
108
+ **Three Windows-specific things worth knowing**:
109
+
110
+ 1. **An account change may fail to save** (the profile's `cordis.patch.yml` held open). The plugin **reads the value back after writing**: it reports "cleared" only once the write is confirmed on disk, and otherwise reports the failure and keeps your previous choice. Close whatever holds the file and retry. See [Known limitations](#known-limitations).
111
+ 2. **Credential paths are partly verified on real Windows hardware.** The candidate list is derived from platform conventions and pinned by unit tests (including both the unset and the blank-string fallback for `LOCALAPPDATA` / `APPDATA`). On a real Windows 11 machine, `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` and `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` **did resolve**, and the encrypted credential's `keyId` matched the registered vector verbatim. **The remaining candidates are still not verified per-item** — if your credential or app lives elsewhere, point `WORKBUDDY_AUTH_FILE` / `WORKBUDDY_APP_EXECUTABLE` at it and please report what you see.
112
+ 3. **Encrypted credentials (from 5.6.0) require the desktop app to be present**, on Windows exactly as on macOS; if the app lives in a custom directory (e.g. `E:\WorkBuddy\WorkBuddy.exe`), point `WORKBUDDY_APP_EXECUTABLE` at it.
113
+
80
114
  ## Command line
81
115
 
82
116
  ```sh
@@ -123,8 +157,8 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
123
157
  - **Account switching** relies on the historical auth files the WorkBuddy desktop app leaves behind (the app's own backups), not an official multi-account API. Following the app's current sign-in remains the default; switching is an explicit opt-in, and historical credentials can be invalidated by the app's cleanup or sign-out.
124
158
  - If an explicitly selected account disappears locally (the app replaced its login or cleaned up its backups), the plugin does **not** silently switch to another account — that would bill a different account. The card says the saved account no longer exists; pick an account again, or use "Follow the app's sign-in" to clear the saved choice. **Signing in again in the desktop app will not fix this**: the credentials are fine, it is the saved account id that no longer matches.
125
159
  - 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.
126
- - **On Windows, an account change may fail to save while `settings.yaml` is held open.** DSH updates that file by writing a temp file and replacing the target; an antivirus scanner, a sync client (OneDrive), or an open editor can lock it briefly, and once the replacement retries are exhausted the failed write does **not** make `set()` reject. The plugin therefore reads the value back after writing: it shows "cleared" **only once the write is confirmed on disk**, and on failure it reports the error and keeps your previous choice (never a false confirmation that the old selection silently contradicts). Close whatever holds the file and retry.
127
- - Windows and Linux credential paths are derived from platform conventions and are not verified on real hardware; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
160
+ - **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.
161
+ - 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.
128
162
  - **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.
129
163
  - **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.
130
164
  - **"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.
package/README.md CHANGED
@@ -60,7 +60,22 @@ DSH PiAiAdapter(每个 provider 一套)
60
60
 
61
61
  ## 安装
62
62
 
63
- > ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上。** 自 v2.1.0 起本插件只支持 DSH 0.1.7-rc.1 及以上的宿主(旧版宿主无法通过依赖解析安装本版);0.1.5 及更早的宿主请停留在 v2.0.15。
63
+ > ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上(当前窗口:`0.1.7-rc.1` ~ `0.2.x`)。** 自 v2.1.0 起本插件只支持 DSH 0.1.7-rc.1 及以上的宿主(旧版宿主无法通过依赖解析安装本版);0.1.5 及更早的宿主请停留在 v2.0.15。自 v2.1.2 起上界放宽为 **`<0.3.0-0`**——**0.2.0 全线(含正式版)已实测支持**,0.3.0 线则被明确拒绝。详见下方[受支持的宿主范围](#受支持的宿主范围)。
64
+
65
+ ### 受支持的宿主范围
66
+
67
+ DSH 的随包门禁(`packages/boot/app-boot/src/plugin-compatibility.ts` 的 `evaluatePluginCompatibility`)会逐条核对本插件声明的 `dsh-*` peer 范围;**只要有一条不满足,整个组合包会被跳过**(进入 `skippedBundles`,仅打印到 stderr)——provider 不注册、卡片不出现、模型列表清空,而页面上没有任何报错。
68
+
69
+ | 宿主版本 | 判定 |
70
+ | --- | --- |
71
+ | `0.1.7-rc.1` ~ `0.1.7-rc.2` | ✅ 支持 |
72
+ | `0.2.0-rc.1` ~ `0.2.x`(含 `0.2.0` 正式版) | ✅ 支持(2.1.2 起实测) |
73
+ | `0.3.0-0` 及以上 | ❌ 明确拒绝(该线尚未验证) |
74
+ | `0.1.6` 及更早 | ❌ 明确拒绝(0.1.5 线请停留在 v2.0.15) |
75
+
76
+ > 上界写作 `<0.3.0-0` 而非 `<0.3.0`:DSH 至今**全部 tag 都是预发布**(`-alpha.N` / `-rc.N`),只写 `<0.3.0` 会放行整条 `0.3.0-alpha.1` / `0.3.0-rc.1` 这条从未验证过的线,围栏形同虚设——这正是 2.1.2 修掉的缺陷形状。该口径由 `tests/dsh-line.spec.ts` 守卫。
77
+
78
+ > 🪟 **平台支持:Windows、macOS、Linux 三者并列支持。** **Windows 是一等目标平台**,不是「顺带能跑」:CI 在 `windows-latest` 上跑完整套件,凭据路径、进程心跳、App 可执行文件定位、设置写入四处都有 Windows 专属分支与测试,并且**修复只对 Windows 生效的缺陷时,以 Windows 用户回报的现场与真机取证的加密向量为准**(见 [CHANGELOG](CHANGELOG.md) 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方[平台支持](#平台支持)一节了解各平台的具体差异与已知限制。
64
79
 
65
80
  前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态)。
66
81
 
@@ -85,6 +100,25 @@ dsh plugin --profile web add dsh-connect-workbuddy # Web
85
100
  dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
86
101
  ```
87
102
 
103
+ ## 平台支持
104
+
105
+ 三个平台都是受支持目标;下列差异是**实现事实**,不是「尚未支持」的委婉说法。要改这一节,请同时改 [`docs/WINDOWS.md`](docs/WINDOWS.md)——那份文件是平台相关约束的登记处。
106
+
107
+ | 方面 | Windows | macOS | Linux |
108
+ | --- | --- | --- | --- |
109
+ | 凭据默认目录 | `%LOCALAPPDATA%` / `%APPDATA%` 优先,未设时回落 `<home>\AppData`(重定向配置文件仍可解析) | `~/Library/Application Support/CodeBuddyExtension/Data/Public/auth` | `$XDG_CONFIG_HOME` 优先,未设时回落 `~/.config` |
110
+ | 桌面 App 可执行文件 | `%LOCALAPPDATA%\Programs\WorkBuddy`、`%LOCALAPPDATA%\WorkBuddy`、两个 `Program Files`(按 `WorkBuddy.exe` 定位) | 读 bundle 的 `Info.plist` 取 `CFBundleExecutable`(真实值 **`Electron`**,不是按 App 名猜);含国际版 `WorkBuddy AI.app` 与 `/Applications` 下的子目录 | 不适用(加密凭据场景下 Linux 无对应 App;用 `WORKBUDDY_AUTH_FILE` 指定凭据) |
111
+ | 进程心跳的时间戳来源 | PowerShell `Get-Process` 的 `StartTime`(POSIX 无 `ps -lstart` 等价物) | `ps -o lstart=` | `ps -o lstart=` |
112
+ | 子进程的窗口 | **必须 `windowsHide`**:宿主本身没有控制台(GUI 进程),漏设会让每次探测都在屏幕上闪一个可见的黑框 | 不适用(该选项仅 Windows 生效) | 不适用 |
113
+ | 设置写入的瞬时占用 | **会踩到**:profile 的配置文件 `cordis.patch.yml` 被杀毒软件 / OneDrive / 编辑器短暂锁定会拒绝 rename 覆盖,`@deepseek-ai/dsh-atomic-write` 的重试**只在 `win32` 生效** | 不踩:POSIX 的 rename 直接替换,同一段代码不进重试分支 | 同 macOS |
114
+ | CI 覆盖 | `windows-latest` 跑完整套件 | 开发者本机(macOS) | `ubuntu-latest` 跑完整套件 |
115
+
116
+ **Windows 上需要知道的三个具体点**:
117
+
118
+ 1. **账号改动可能保存失败**(profile 的配置文件 `cordis.patch.yml` 被占用)。插件会在写入后**回读校验**:确认落盘才显示「已清除」,失败则明确报错并保留原选择。遇到提示时关闭占用该文件的程序后重试。详见[已知限制](#已知限制)。
119
+ 2. **凭据路径已部分经真机验证**。候选列表按平台约定推导并有单元测试钉住(含 `LOCALAPPDATA` / `APPDATA` 缺省与空白串两种回落)。已在真实 Windows 11 上确认:`%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` **命中**,加密凭据的 `keyId` 与登记向量逐字一致。**其余候选仍未经真机逐项验证**——若你的凭据或 App 不在默认位置,用 `WORKBUDDY_AUTH_FILE` / `WORKBUDDY_APP_EXECUTABLE` 指定,并欢迎回报现场。
120
+ 3. **加密凭据(5.6.0 起)需要桌面 App 在场**,Windows 与 macOS 同样如此;App 装在自定义目录(如 `E:\WorkBuddy\WorkBuddy.exe`)时用 `WORKBUDDY_APP_EXECUTABLE` 指定。
121
+
88
122
  ## 命令行
89
123
 
90
124
  ```sh
@@ -131,8 +165,8 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
131
165
  - **账号切换**基于 WorkBuddy 桌面端留下的历史 auth 文件(App 自身的备份产物),并非官方多账号 API。默认行为仍是跟随 App 当前登录;切换账号是显式选项,且历史凭据可能因 App 清理或退出登录而失效。
132
166
  - 显式选择的账号若在本机消失(App 更换登录或清理备份文件),插件**不会**静默改选其他账号——那样会让账单落到另一个账号上。卡片会明确说明「保存的账号已不存在」,此时重新选择一个账号,或点「跟随 App 当前登录」清除该选择即可恢复。**重新登录桌面端 App 无法修复这种状态**:凭据本身是好的,失效的只是保存下来的账号标识。
133
167
  - 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
134
- - **Windows:设置文件 `settings.yaml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
135
- - Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
168
+ - **Windows:profile 的配置文件 `cordis.patch.yml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
169
+ - Windows / Linux 的凭据默认路径按平台约定推导。已在真实 Windows 11 上确认 `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` 命中,**其余候选与 Linux 路径仍未经真机验证**;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
136
170
  - **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
137
171
  - **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 应用而误启动它。
138
172
  - **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,而**不是**「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
@@ -0,0 +1,226 @@
1
+ # Windows 支持登记与约束(Windows Support Register)
2
+
3
+ > 本文件是 `dsh-connect-workbuddy` **平台相关约束的登记处**。README 的「平台支持」一节是面向使用者的摘要,本文件是面向维护者的完整登记。
4
+ >
5
+ > **维护约定:任何改动 Windows 行为的提交,必须同时更新本文件;README 的「平台支持」表若随之变化,两份 README(`README.md` / `README.en.md`)必须一起改,且与本文件保持一致。** 这是本项目的长期口径:**Windows 是一等目标平台,每次改动的默认考量面包含 Windows。**
6
+
7
+ ## 0. 为什么需要这份文件
8
+
9
+ Windows 在本项目里不是「顺带能跑」的平台,而是**有专属分支、专属故障、且开发机无法复现**的平台。最后一点是关键:
10
+
11
+ > **macOS 开发机上跑不出来的 Windows 缺陷,不是「没复现」,而是「不可能复现」。**
12
+ >
13
+ > 依据(提交 `1b8bc7f` 的记录):`@deepseek-ai/dsh-atomic-write` 的 rename 重试被 `process.platform !== 'win32'` 直接短路——只有 Windows 会拒绝 rename 覆盖另一个进程打开着的文件,POSIX 的 rename 无论文件是否被打开都直接替换。同一段代码在 macOS/Linux 上**根本不进重试分支**。实测对照:macOS 上把该配置文件持续持有 0/300/800/1500/3000/6000ms,写入全部在 1–2ms 内成功。
14
+ >
15
+ > 因此**「在 Mac 上试过了,没问题」不足以判定 Windows 行为**。这类结论必须来自:CI 的 windows-latest、Windows 用户回报的现场、或真机取证的向量。
16
+
17
+ ## 1. 五个 Windows 专属分支
18
+
19
+ 改动这五处中的任何一处,都要考虑 Windows。
20
+
21
+ | # | 位置 | Windows 行为 | 若改错会怎样 |
22
+ | --- | --- | --- | --- |
23
+ | 1 | `src/auth.ts` `defaultDesktopAuthDirs()` | `%LOCALAPPDATA%` / `%APPDATA%` **优先**,未设或为空白串时回落 `<home>\AppData`(`Local` / `Roaming` 两个候选) | 重定向配置文件(OneDrive 文件夹备份、企业策略)的机器读不到凭据 → 显示未登录 |
24
+ | 2 | `src/at-rest.ts` `workbuddyAppExecutableCandidates()` | 按 `WorkBuddy.exe` 探四个位置:`%LOCALAPPDATA%\Programs\WorkBuddy`、`%LOCALAPPDATA%\WorkBuddy`、`%ProgramFiles%\WorkBuddy`、`%ProgramFiles(x86)%\WorkBuddy` | 装在自定义目录(如同源 issue 里的 `E:\WorkBuddy\WorkBuddy.exe`)的机器取不到字段密钥 → 加密凭据读不出 |
25
+ | 3 | `src/host-heartbeat.ts` `processStartTimeMs()` → `processStartProbe()` | 走 PowerShell `Get-Process` 的 `StartTime`,以 UTC ISO 8601 输出供 `Date.parse` 解析 | PID 复用判定失准 → 心跳误判宿主存活 |
26
+ | 4 | 设置写入回读校验(`src/client/account-selection.ts`) | 写入后**回读校验**:profile 的 `cordis.patch.yml` 被占用会让 rename 覆盖失败,而 `set()` 的 promise 仍 resolve | 用户看到假确认(显示「已清除」但没落盘)→ issue #11 |
27
+ | 5 | 子进程启动选项:`src/host-heartbeat.ts` `PROCESS_PROBE_OPTIONS` 与 `src/at-rest.ts` `fetchAtRestKeyPayload()` | 一律带 `windowsHide: true`(POSIX 忽略该选项) | **宿主本身没有控制台**(Electron GUI 进程,实测 `MainWindowHandle = 0`),Windows 会给子进程**新建一个可见的控制台窗口** → 每次进程存活判定都在屏幕上闪一次黑框 |
28
+
29
+ **第 5 条为什么单列**:它不是路径或逻辑错误,而是**只在「父进程无控制台」时才出现**的一类缺陷——而这恰是 GUI 宿主的常态、终端里的例外。在终端手工跑插件时父进程有控制台,子进程只是继承,**永远看不到这个现象**;它因此逃过了所有本地验证,直到在真机上以「父进程无控制台」的形态复现(§2 A 档)。
30
+
31
+ **通用注意**:路径一律经 `path.join` 构造,**不得写死 POSIX 分隔符**。历史上正因此挂过 CI——`2.0.11` 新增的 darwin 候选断言写死了 `/Applications/WorkBuddy.app/...`,生产代码没问题(用 `join`),错的是测试:它断言的是「跑测试的机器是 POSIX」而非「候选列表对不对」,于是 Windows 必挂、macOS 恒绿。现有回归守卫:候选路径不得**混用**分隔符。
32
+
33
+ ## 2. 证据分级(写结论时必须用对档位)
34
+
35
+ | 档位 | 含义 | 实例 |
36
+ | --- | --- | --- |
37
+ | **A 真机取证** | 在真实 Windows 上取得,可作为最终判据 | ① `tests/at-rest.spec.ts` 里固定向量 `keyId=9127dea1b44020a7`,**采自真实 Windows 安装**(2026-09-26 在本机复核:`workbuddy-desktop.info` 的 `nickname` / `phoneNumber` / `accessToken` 信封全部使用该 keyId);② issue #13 报告的 `E:\WorkBuddy\WorkBuddy.exe`;③ **§1-5 的控制台闪窗**(见下方专门说明);④ §1-1 凭据目录与 §1-2 可执行文件候选在真机上命中(`doctor` 全绿,见 §7 记录) |
38
+ | **B CI 实测** | `windows-latest` 上跑过,覆盖跨平台正确性 | 套件全绿;`tests/auth.spec.ts` 的 `win32` 分支断言(注入 `platform`/`home`/`env`,**任何主机上都能跑**) |
39
+ | **C 约定推导** | 按平台约定写出 + 单元测试钉住,**未在真机逐项验证** | 四个凭据/可执行文件候选路径中**未在本机命中的那些**(见 §7 记录:本机只命中了 `%LOCALAPPDATA%\Programs\WorkBuddy` 与 `%LOCALAPPDATA%\...\auth`,其余候选仍属 C 档) |
40
+ | **D 未验证** | 无任何上述支撑 | —— |
41
+
42
+ ### A 档取证:§1-5 的控制台闪窗(2026-09-26,Windows 11 22621)
43
+
44
+ **缺陷**:`src/host-heartbeat.ts` 的 `processStartTimeMs()` 以 `execFileSync('powershell', …, { encoding: 'utf8' })` 启动子进程,**未设 `windowsHide`**;同仓库的 `src/at-rest.ts` 却设了。两处不一致。
45
+
46
+ **为什么在 macOS / 终端里永远看不到**:Windows 只在**父进程没有控制台**时才给控制台程序子进程新建窗口。终端里父进程有控制台,子进程只是继承——现象不存在。而真实宿主 `DSH NEXT.exe` 是 Electron GUI 进程,实测 `MainWindowHandle = 0`、无控制台,正是会触发的形态。
47
+
48
+ **复现与判据**(两台父进程形态对照,子进程自报它实际拥有的控制台):
49
+
50
+ | 父进程形态 | 不设 `windowsHide` | 设 `windowsHide: true` |
51
+ | --- | --- | --- |
52
+ | 终端(有控制台) | 继承,无新窗口 | 继承,无新窗口 |
53
+ | **WMI 服务创建(无控制台,等价于 GUI 宿主)** | **`NEW-console hwnd=1573500 visible=True`** | `no-console` |
54
+
55
+ WMI 服务(`Win32_Process.Create`)创建的进程与 Electron 宿主同为「无控制台」形态,因此用它复现不依赖 GUI 交互。子进程用 `kernel32!GetConsoleWindow` + `user32!IsWindowVisible` 自报:`visible=True` 即用户屏幕上真实可见的黑框。
56
+
57
+ **同时排除的一条错误判据**(避免后人重走):不能用 `Get-Process powershell | MainWindowHandle` 观察。控制台窗口归 `conhost.exe` 所有,控制台程序自身的 `MainWindowHandle` 是 0,即使窗口正在屏幕上,**该探针也报 0**。
58
+
59
+ **修法与守卫**:抽出 `PROCESS_PROBE_OPTIONS`(含 `windowsHide: true`)与 `processStartProbe(pid, platform)`,使平台分支与启动选项可在**任意主机**上断言;守卫见 `tests/platform-standing.spec.ts`(两个 spawn 点都必须带 `windowsHide: true`)与 `tests/host-heartbeat.spec.ts`(选项值与平台分支)。**变异验证**:去掉 `windowsHide` → 对应用例变红。
60
+
61
+ **README 与 CHANGELOG 里的措辞必须与档位相称**:C 档不得写成「已验证」,A 档才可以写「真机实测」。凭据默认路径**现已部分升为 A 档**(本机命中的那两条,见 §7 记录),未命中的候选仍为 C 档——这正是 README「已知限制」里那句「未经真机验证;必要时用 `WORKBUDDY_AUTH_FILE` 指定实际位置」仍然保留的原因。
62
+
63
+ ## 3. 已知的 Windows 专有故障(历史,均已修复)
64
+
65
+ | 版本 | 现象 | 根因 |
66
+ | --- | --- | --- |
67
+ | 2.0.6 | CI 在 `windows-latest` 上红 2 例 | 测试读取本机真实登录;`host-heartbeat` 测试在 Windows 上超时(PowerShell 冷启动慢) |
68
+ | 2.0.11 | CI 在 `windows-latest` 上红 2 例 | 新增的 darwin 候选断言写死 POSIX 分隔符(**生产代码无辜,错在测试**) |
69
+ | 2.0.12 | Windows 上供应商开关会静默复原 | 落盘校验漏了一处(与 #11 同源) |
70
+ | issue #11 | Windows 上账号清除「点了没反应」 | profile 配置被占用 → 写入被静默丢弃,而 `set()` 不报错 |
71
+ | issue #13 | Windows 上账号切换被判死 | profile 配置瞬时占用超出了重试预算的全部持有期 |
72
+ | issue #15 | Windows 上加密凭据读不出 | 探测路径缺 `%LOCALAPPDATA%\WorkBuddy`(用户级安装) |
73
+ | 2.1.0(本次) | 宿主内每次进程存活判定都闪一次黑色控制台窗口 | 心跳探测未设 `windowsHide`,而宿主是无控制台的 GUI 进程(§1-5、§2 A 档取证) |
74
+
75
+ **共同形态**:Windows 的故障几乎都是**静默**的(不报错、只是没生效),且**只在 Windows 上出现**。因此 Windows 相关的修复必须配「可被变异咬住」的测试,否则回归无人拦。
76
+
77
+ **关于 issue #13 重试预算的更正**:本文件此前写作「重试预算(200+600ms)」。实测并核对上游实现(`@deepseek-ai/dsh-atomic-write`,rc.1 与 rc.2 常量相同:initial 20ms、max 200ms、limit 8)后,真实预算是**延迟序列 20+40+80+160+200×4 ≈ 1100ms**(实测端到端 1149–1165ms,含 `rename` 调用开销);**不存在 600ms 这一项**。真机实测的持有期-结果对照:持有 0/100/300/600/1000ms 后释放 → 全部成功;持有 60s(超出预算)→ `EPERM`,且目标文件**保持原内容不变**(不产生半写文件)。
78
+
79
+ ## 4. 改动检查清单(改任何平台相关代码前过一遍)
80
+
81
+ - [ ] 路径是否全部经 `path.join`?有无写死 `/` 或 `\`?
82
+ - [ ] 新增的环境变量是否处理「未设置」**与**「空白串」两种回落?(Windows 上企业策略可能设成空串)
83
+ - [ ] 若涉及文件写入:Windows 上可能被占用,是否需要回读校验?
84
+ - [ ] 若走外部命令:Windows 上有无等价物?(`ps -o lstart=` → PowerShell;`which` → `where`)
85
+ - [ ] **若 spawn 子进程:是否设了 `windowsHide: true`?** 宿主无控制台,漏设即闪黑框(§1-5)。注意**在终端里验证不出来**——那种形态下子进程只是继承控制台
86
+ - [ ] 新增测试是否**在任意主机上都能跑**(注入 `platform`/`home`/`env`),而不是断言「跑测试的机器是什么平台」?
87
+ - [ ] 结论措辞是否与§2 的证据档位相称?(C 档不要写成「已验证」)
88
+ - [ ] 本文件与两份 README 是否同步?
89
+
90
+ ## 5. 相关位置索引
91
+
92
+ | 内容 | 位置 |
93
+ | --- | --- |
94
+ | CI 矩阵(含 `windows-latest`) | [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) |
95
+ | 凭据目录推导与测试 | `src/auth.ts` `defaultDesktopAuthDirs()`;`tests/auth.spec.ts` |
96
+ | 可执行文件候选与测试 | `src/at-rest.ts` `workbuddyAppExecutableCandidates()`;`tests/at-rest.spec.ts` |
97
+ | 心跳时间戳与测试 | `src/host-heartbeat.ts` `processStartTimeMs()` / `processStartProbe()`;`tests/host-heartbeat.spec.ts` |
98
+ | 子进程启动选项(`windowsHide`)与测试 | `src/host-heartbeat.ts` `PROCESS_PROBE_OPTIONS`、`src/at-rest.ts` `fetchAtRestKeyPayload()`;`tests/platform-standing.spec.ts`、`tests/host-heartbeat.spec.ts` |
99
+ | 写入回读校验与测试 | `src/client/account-selection.ts`;`tests/client-account-selection.spec.ts` |
100
+ | 设置隔离(供测试) | `vitest.config.ts`(`LOCALAPPDATA` / `APPDATA` / `HOME` / `USERPROFILE` / `XDG_CONFIG_HOME`) |
101
+
102
+ ## 6. 尚未做的(如实登记)
103
+
104
+ - **凭据默认路径只在真机上命中了本机形态**(部分 A 档、部分 C 档)。2026-09-26 在真实 Windows 11 上复核:`%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 命中且 `%APPDATA%` 下不存在(`doctor` 的 `desktopAuthFile.present: true`、`candidates` 两条按实际环境变量拼接);可执行文件候选只命中了 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe`。**其余候选(`%LOCALAPPDATA%\WorkBuddy`、两个 `Program Files`、`%APPDATA%` 凭据目录)仍属 C 档**,只有单元测试钉住。若你的机器是别的形态(尤其 `E:\WorkBuddy\...` 这类自定义目录),欢迎回报现场。
105
+ - **issue #13 的 Windows 专用复现探针不在 `main` 上**。它作为 `scripts/probe-issue13-windows.mjs` 加在提交 `1b8bc7f`,该提交位于分支 `fix/0.1.7-forward-compat`(**非 HEAD 祖先**)。若今后需要在真机上量化重试预算是否够用,从该分支取用,或按需要重新引入。预算的真实数值已更正如 §3 末。
106
+ - **无 Windows 真机 CI 之外的手工验收**:桌面 App 的登录态复用、卡片在 Windows 上的实际渲染,目前只有用户回报,没有维护者一侧的系统性验收。下面的 §7 就是为补这一项写的。
107
+
108
+ ## 7. 在 Windows 真机上验证(操作手册)
109
+
110
+ **为什么需要手工验证**:CI 的 `windows-latest` 能覆盖跨平台正确性(B 档),但覆盖不到「真实 WorkBuddy 桌面 App 在场」这件事——CI 机器上没有登录着的 App、没有加密凭据、没有杀毒软件锁文件。§1 的五处分支里,1、2、4、5 都需要真机才能验到 A 档(第 5 条尤其:CI 的无头 Windows 上父进程本来就没有控制台,但 CI 不会去断言「屏幕上没多出窗口」)。
111
+
112
+ ### 7.0 本仓库已完成的真机取证记录(2026-09-26)
113
+
114
+ 一次已执行的现场记录,供后人对照,不必重复:
115
+
116
+ | 项 | 结果 | 结论 |
117
+ | --- | --- | --- |
118
+ | `doctor --json` | `desktopAuthFile.present: true`;`atRestDecryption.available: true`;`hostHeartbeat.processAlive: true`;`regions.cn` 2 个账号 | §1-1 / §1-2 / §1-3 在本机形态下**真机通过** |
119
+ | 凭据加密 | `workbuddy-desktop.info` 的 `nickname` / `phoneNumber` / `accessToken` 均为 `$wbEncrypted` 信封,`keyId` = `9127dea1b44020a7` | 与固定向量**逐字一致** |
120
+ | App 可执行文件 | 命中 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe`;`ELECTRON_RUN_AS_NODE` 通路返回 `v22.21.1` | §1-2 的探测与取密钥通路**真机通过** |
121
+ | 心跳时间戳 | PowerShell 输出 `2026-09-26T04:03:00.9358345Z`(7 位小数),`Date.parse` → `1790395380935` 有限值 | §1-3 的输出格式与解析**咬合** |
122
+ | PID 存活探测 | `process.kill(pid, 0)`:本进程未抛错;不存在的 PID → `ESRCH` | 存活判定可用 |
123
+ | §1-5 控制台闪窗 | 无控制台父进程下:不设 `windowsHide` → `NEW-console visible=True`;设了 → `no-console` | **缺陷已复现并修复** |
124
+ | 构建与套件 | `typecheck` / 368 例测试 / `build` 全绿;`npm pack --dry-run` 18 个文件 | 本机为 Windows,故 CI 双平台中的 Windows 一侧在真机复算 |
125
+
126
+ 仍未在本机验证的:§7.3 的**锁文件场景**(需要杀毒软件 / 同步盘 / 编辑器同时在场,无法在无干扰环境里制造);以及自定义安装目录形态。
127
+
128
+ ### 7.1 先跑 `doctor`(一次拿全四项证据)
129
+
130
+ ```powershell
131
+ dsh plugin --profile desktop exec dsh-connect-workbuddy doctor --json
132
+ ```
133
+
134
+ JSON 里有四个字段直接对应 §1 的分支,对照下表读:
135
+
136
+ | 字段 | 期望 | 若不符 → 指向 |
137
+ | --- | --- | --- |
138
+ | `desktopAuthFile.present` | `true` | §1-1 凭据目录分支。同时看 `desktopAuthFile.dir` 与 `candidates`:`dir` 是**实际选中**的目录,`candidates` 是**探测过**的全部候选。若 `present:false` 而目录里确有文件,多为文件名或目录名与预期不符 |
139
+ | `atRestDecryption.available` | `true`(凭据已加密时必须有) | §1-2 可执行文件候选。`appExecutable` 为 `(not found; set WORKBUDDY_APP_EXECUTABLE)` 说明四个候选都没命中 |
140
+ | `hostHeartbeat.processAlive` | `true`(宿主在跑时) | §1-3 心跳分支;`hostHeartbeat.registeredAt` / `pid` 用于比对 |
141
+ | `regions.cn[].source` / `regions.global[].source` | 至少一个区域有账号 | 凭据读取整体失败 |
142
+
143
+ **若 `dsh` 不在 PATH**(桌面版可能不带 CLI):可直接执行包内的 CLI,效果等价:
144
+
145
+ ```powershell
146
+ node <插件目录>\lib\bin.js doctor --json
147
+ ```
148
+
149
+ **手工确认凭据目录**(用于判定「探测结果 vs 实际位置」):
150
+
151
+ ```powershell
152
+ dir "$env:LOCALAPPDATA\CodeBuddyExtension\Data\Public\auth"
153
+ dir "$env:APPDATA\CodeBuddyExtension\Data\Public\auth"
154
+ ```
155
+
156
+ **手工确认 App 位置**(判定四个候选为何全空):
157
+
158
+ ```powershell
159
+ Get-Process *orkbuddy* -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path
160
+ ```
161
+
162
+ 自定义安装目录(如 `E:\WorkBuddy\WorkBuddy.exe`)四个候选都探不到——这是**已知的**,设 `WORKBUDDY_APP_EXECUTABLE` 指向该 exe 即可:
163
+
164
+ ```powershell
165
+ [Environment]::SetEnvironmentVariable('WORKBUDDY_APP_EXECUTABLE','E:\WorkBuddy\WorkBuddy.exe','User')
166
+ # 重启 DSH,并让跑命令的终端也继承(新开的终端会自动继承用户级变量)
167
+ ```
168
+
169
+ ### 7.2 验 §1-1:重定向配置文件(仅当你的 `%LOCALAPPDATA%` 被重定向时需要)
170
+
171
+ 企业策略或 OneDrive 文件夹备份会把 `%LOCALAPPDATA%` 指到别处。代码的意图是「env 优先,未设才回落 `<home>\AppData`」。验证:
172
+
173
+ ```powershell
174
+ echo $env:LOCALAPPDATA
175
+ echo $env:APPDATA
176
+ ```
177
+
178
+ 两者都非空时,`doctor` 的 `desktopAuthFile.candidates` 应当**由它们的实际值拼接**,而不是 `C:\Users\<你>\AppData\Local`。若你的环境恰好是「变量为空」的形态(少见),候选才应回落到 home 推导值。
179
+
180
+ ### 7.3 验 §1-4:设置写入的瞬时占用(**这是最值得实测的一项**)
181
+
182
+ **为什么最值得**:这个故障是 Windows 独有的,且在 macOS 上**不可能复现**(`atomic-write` 的 rename 重试被 `process.platform !== 'win32'` 短路)。它也是唯一会**静默丢数据**的一项。
183
+
184
+ 步骤:
185
+
186
+ 1. 打开 DSH 插件卡片,展开 WorkBuddy 设置区。
187
+ 2. 用一个能持有文件句柄的工具锁住**当前 profile 的配置文件**(PowerShell 的 `[IO.File]::Open` 最直接)。
188
+
189
+ **注意文件名**:0.1.7 线上配置文档是 profile 目录下的 **`cordis.patch.yml`**(`config-editor` 的 `documentPath`;易失字段也经同一条 `edit()` 落在这里)。**不是 `settings.yaml`**——那个文件在 0.1.7 上只用于一次性导入旧版本配置,导入后即改名为 `settings.yaml.imported`。锁错文件会验不出任何东西。
190
+
191
+ ```powershell
192
+ # 另开一个 PowerShell 窗口,按住 profile 配置文件不放(按任意键释放)
193
+ $dsh = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
194
+ # 用你实际使用的 profile 名替换 desktop(web / dsh-tui 同理)
195
+ $p = Join-Path $dsh 'profiles\desktop\cordis.patch.yml'
196
+ if (-not (Test-Path $p)) {
197
+ Write-Host "找不到 $p —— 先列出实际存在的 profile:"
198
+ Get-ChildItem (Join-Path $dsh 'profiles') -Directory | Select-Object -ExpandProperty FullName
199
+ return
200
+ }
201
+ $fs = [IO.File]::Open($p,'Open','Read','None')
202
+ Write-Host "holding $p — 现在去卡片里改账号,然后按任意键释放"
203
+ $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
204
+ $fs.Close()
205
+ ```
206
+
207
+ 3. 在持有期间,于卡片里**切换账号**或**清除账号**。
208
+ 4. **期望**:出现明确错误提示(写入未落盘),且**原来的选择仍然生效**——绝不能显示「已清除」或新账号却实际没保存。
209
+ 5. 释放句柄后重试,应当成功落盘。
210
+
211
+ **只读判定**:重新打开卡片或重跑 `doctor`,看 `regions.<区域>[].selected` 是否与你刚做的选择一致。**不一致却没有任何错误提示**,就是本项失败——请把场景回报(这正是 issue #11 的形态)。
212
+
213
+ ### 7.4 验 §1-2 / §1-3 的边界
214
+
215
+ - **加密凭据**(§1-2):若 `desktopAuthFile.present:true` 而账号数为 0,且 `atRestDecryption.available:false`,说明凭据是加密的但取不到密钥。设好 `WORKBUDDY_APP_EXECUTABLE` 后重跑 `doctor`,`available` 应变为 `true`,账号应出现。
216
+ - **心跳**(§1-3):PowerShell 冷启动较慢(CI 上曾因此超时)。若 `hostHeartbeat.processAlive:false` 而宿主进程确实在跑,请在报告中附上 `Get-Process -Id <doctor 报的 pid> | Select-Object StartTime` 的输出——那是 PID 复用判定所依赖的值。
217
+
218
+ ### 7.5 回报时请附(便于把结论升到 A 档)
219
+
220
+ ```powershell
221
+ dsh plugin --profile desktop exec dsh-connect-workbuddy doctor --json
222
+ node -v
223
+ $PSVersionTable.PSVersion
224
+ ```
225
+
226
+ 外加:你的 Windows 版本(`winver`)、WorkBuddy 桌面 App 版本、`%LOCALAPPDATA%` 与 `%APPDATA%` 的实际值、以及 §7.3 的结果。**附上这些,§2 里那些 C 档结论就能升为 A 档**;`docs/WINDOWS.md` 与 README 的措辞也随之更新。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-CH6_J1Ee.js";
2
+ import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-DLT0FZbA.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
@@ -314,7 +314,11 @@ window.__ModuleLoader__.load({
314
314
  * Write one region's account slot, then confirm the value actually landed.
315
315
  *
316
316
  * `settingsScope.set()` resolving is NOT proof that anything was stored. The
317
- * Host's document is `settings.yaml`, replaced by writing a temp file and
317
+ * Host's document is the active profile's patch file (`cordis.patch.yml`, the
318
+ * `config-editor`'s `documentPath`; volatile fields land there too, through
319
+ * the same `edit()`. `settings.yaml` is NOT it on the 0.1.7 line — that file
320
+ * is imported once from earlier releases and renamed to
321
+ * `settings.yaml.imported`), replaced by writing a temp file and
318
322
  * renaming it over the target; on Windows an antivirus scanner, a sync client
319
323
  * (OneDrive, Dropbox), or an open editor can hold the file briefly.
320
324
  * `@deepseek-ai/dsh-atomic-write` retries `EPERM` / `EBUSY` / `EACCES` a
@@ -484,7 +488,7 @@ window.__ModuleLoader__.load({
484
488
  .dsm-workbuddy-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
485
489
  .dsm-workbuddy-searched-list code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#b8b8b8);word-break:break-all}
486
490
  .dsm-workbuddy-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
487
- .dsm-workbuddy-searched-reason-encrypted{color:var(--dsw-alias-state-warning-primary,#e0a13a)}
491
+ .dsm-workbuddy-searched-reason-encrypted{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
488
492
  .dsm-workbuddy-searched-reason-wrong-region{color:var(--dsw-alias-brand-primary,#5686fe)}
489
493
  .dsm-workbuddy-searched-more{margin-top:6px;padding:0;border:0;background:none;color:var(--dsw-alias-brand-primary,#5686fe);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}
490
494
  .dsm-workbuddy-searched-more:hover{text-decoration:underline}
@@ -676,11 +680,13 @@ window.__ModuleLoader__.load({
676
680
  * — 卡片的整体结构(折叠外壳 / 账号状态行 / 账号下拉 / 积分区 / 模型表 /
677
681
  * 操作按钮行)、模块加载时注入一次 `<style>` 的写法、
678
682
  * 草稿态(draftModels/draftEnabledIds)与 dirty 标记的保存流程、
679
- * 60 秒轮询与 AbortController 清理、以及
680
- * `IconChevronDownOutline14` 的使用,均来自该项目的 TraeUsageCard。
683
+ * 60 秒轮询与 AbortController 清理,均来自该项目的 TraeUsageCard。
681
684
  * 折叠卡片外壳与 `settings.plugin.item` 槽位形态来自
682
685
  * dingminhua/dsh-subagent-default-model(MIT)。
683
686
  * 改动:
687
+ * 0. 折叠箭头改用纯 CSS caret(理由见 styles.ts):两版宿主的图标命名族
688
+ * 不同,静态导入必挂一边。此前沿用的 `IconChevronDownOutline14` 已
689
+ * 随该改动移除,故不再计入上方参考项。
684
690
  * 1. 积分区改为「合计 + 按套餐名聚合的进度条」,因为实测单个账号下
685
691
  * 同名套餐可达 19 个,逐条渲染会淹没卡片(原项目按上游条目直出);
686
692
  * 2. 模型行补上 WorkBuddy 上游给出的积分倍率、多模态与推理档位;
@@ -959,9 +965,9 @@ window.__ModuleLoader__.load({
959
965
  * write made there does NOT update the browser settings mirror. Reading the
960
966
  * mirror therefore left the checkbox stuck ON after a successful disable —
961
967
  * "不能正确取消国际版/国内版" — even though `enabled: false` was already in
962
- * settings.yaml. The Host sends the committed value it derives from its own
963
- * config (`status.enabled`, see `deps.regionEnabled`), so the card renders
964
- * that.
968
+ * the profile configuration. The Host sends the committed value it derives
969
+ * from its own config (`status.enabled`, see `deps.regionEnabled`), so the
970
+ * card renders that.
965
971
  *
966
972
  * The mirror stays as the FALLBACK: a host that predates the field, or a
967
973
  * status that has not loaded yet, still renders from the stored document
@@ -1666,7 +1672,7 @@ window.__ModuleLoader__.load({
1666
1672
  "row.accountsScanning": "Detecting…",
1667
1673
  "row.accountNoneInEffect": "No account in effect",
1668
1674
  "row.accountsSavedChoice": "Using the account saved for this region.",
1669
- "row.accountsWriteFailed": "The account change was not saved: {message} — the settings file could not be updated (on Windows it may be locked by an antivirus scanner or a sync client). Your previous choice is still in effect; close whatever holds settings.yaml and try again.",
1675
+ "row.accountsWriteFailed": "The account change was not saved: {message} — the active profile's configuration file could not be updated (on Windows it may be locked by an antivirus scanner or a sync client). Your previous choice is still in effect; close whatever holds that file and try again (see docs/WINDOWS.md in the plugin repo for the exact path).",
1670
1676
  "row.selectionLostHint": "The saved account no longer exists locally.",
1671
1677
  "row.selectionLostMessage": "The account saved for this region is no longer among the local sign-ins (WorkBuddy replaced its login or cleaned up backups). Your other sign-ins are fine — pick a current one from the list above. Signing in again in the desktop app will not fix this.",
1672
1678
  "row.modelsTitle": "Models",
@@ -1741,7 +1747,7 @@ window.__ModuleLoader__.load({
1741
1747
  "row.accountsScanning": "正在检测…",
1742
1748
  "row.accountNoneInEffect": "当前没有生效的账号",
1743
1749
  "row.accountsSavedChoice": "正在使用该区域保存的账号。",
1744
- "row.accountsWriteFailed": "账号改动未保存:{message} —— 设置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用 settings.yaml 的程序再重试。",
1750
+ "row.accountsWriteFailed": "账号改动未保存:{message} —— 当前 profile 的配置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用该文件的程序再重试(文件的确切路径见插件仓库的 docs/WINDOWS.md)。",
1745
1751
  "row.selectionLostHint": "已保存的账号在本机已不存在。",
1746
1752
  "row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
1747
1753
  "row.modelsTitle": "模型",
@@ -2208,7 +2208,7 @@ var WorkBuddyCatalog = class {
2208
2208
  //#endregion
2209
2209
  //#region src/version.ts
2210
2210
  /** The npm package version this build was produced from. */
2211
- const WORKBUDDY_CONNECT_VERSION = "2.1.0";
2211
+ const WORKBUDDY_CONNECT_VERSION = "2.1.2";
2212
2212
  //#endregion
2213
2213
  //#region src/host-heartbeat.ts
2214
2214
  /**
@@ -2233,26 +2233,78 @@ function workbuddyHostHeartbeatPath() {
2233
2233
  return join(resolveDshHome(), WORKBUDDY_HOST_HEARTBEAT_FILENAME);
2234
2234
  }
2235
2235
  /**
2236
- * Process start time in epoch milliseconds; undefined when unavailable.
2236
+ * Spawn options every external probe in this module is run with.
2237
2237
  *
2238
- * POSIX reads `ps -o lstart=`; Windows has no such command, so the creation
2239
- * time is taken from PowerShell's `Get-Process` StartTime, emitted as UTC ISO
2240
- * 8601 so `Date.parse` understands it without locale assumptions. Absent or
2241
- * unqueryable processes (other users' processes) degrade to undefined.
2238
+ * `windowsHide` is REQUIRED here, not cosmetic. Windows hands a console-program
2239
+ * child a brand-new console window whenever its PARENT has none — and the
2240
+ * parent frequently has none: the DSH Desktop host is an Electron GUI process
2241
+ * (`MainWindowHandle = 0`, verified on the live host), as are Task Scheduler
2242
+ * jobs and CI services. That window is created VISIBLE, so every start-time
2243
+ * probe would flash a black box on the user's screen.
2244
+ *
2245
+ * Evidence (real Windows 11, parent created console-less through the WMI
2246
+ * service — the same condition as the Electron host; the child reports the
2247
+ * console it actually owns):
2248
+ *
2249
+ * - without `windowsHide`: `NEW-console hwnd=1573500 visible=True`
2250
+ * - with `windowsHide: true`: `no-console`
2251
+ *
2252
+ * Invoked from a terminal the parent HAS a console and the child simply
2253
+ * inherits it (no new window), which is why this never showed up in local
2254
+ * runs — the same blind spot `docs/WINDOWS.md` §0 describes. The sibling call
2255
+ * site `src/at-rest.ts` (the at-rest key fetch) already sets this option; this
2256
+ * module did not, and that inconsistency is what the test pins.
2257
+ *
2258
+ * On POSIX the option is ignored.
2242
2259
  */
2243
- function processStartTimeMs(pid) {
2244
- try {
2245
- const output = process.platform === "win32" ? execFileSync("powershell", [
2260
+ const PROCESS_PROBE_OPTIONS = {
2261
+ encoding: "utf8",
2262
+ windowsHide: true
2263
+ };
2264
+ /**
2265
+ * The command one start-time probe runs, per platform.
2266
+ *
2267
+ * Split out as a value so the spawn can be asserted WITHOUT a Windows machine:
2268
+ * `tests/host-heartbeat.spec.ts` checks the platform branch and the options
2269
+ * object directly, which is the only way a Windows-only spawn defect here can
2270
+ * be caught on the macOS development machine (§0: not "unreproduced" but
2271
+ * "impossible to reproduce").
2272
+ *
2273
+ * `platform` is injectable for the same reason, in the same spirit as
2274
+ * `defaultDesktopAuthDirs()` and `workbuddyAppExecutableCandidates()`.
2275
+ */
2276
+ function processStartProbe(pid, platform = process.platform) {
2277
+ if (platform === "win32") return {
2278
+ file: "powershell",
2279
+ args: [
2246
2280
  "-NoProfile",
2247
2281
  "-NonInteractive",
2248
2282
  "-Command",
2249
2283
  `(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).StartTime.ToUniversalTime().ToString('o')`
2250
- ], { encoding: "utf8" }) : execFileSync("ps", [
2284
+ ]
2285
+ };
2286
+ return {
2287
+ file: "ps",
2288
+ args: [
2251
2289
  "-o",
2252
2290
  "lstart=",
2253
2291
  "-p",
2254
2292
  String(pid)
2255
- ], { encoding: "utf8" });
2293
+ ]
2294
+ };
2295
+ }
2296
+ /**
2297
+ * Process start time in epoch milliseconds; undefined when unavailable.
2298
+ *
2299
+ * POSIX reads `ps -o lstart=`; Windows has no such command, so the creation
2300
+ * time is taken from PowerShell's `Get-Process` StartTime, emitted as UTC ISO
2301
+ * 8601 so `Date.parse` understands it without locale assumptions. Absent or
2302
+ * unqueryable processes (other users' processes) degrade to undefined.
2303
+ */
2304
+ function processStartTimeMs(pid) {
2305
+ try {
2306
+ const probe = processStartProbe(pid);
2307
+ const output = execFileSync(probe.file, probe.args, PROCESS_PROBE_OPTIONS);
2256
2308
  const parsed = Date.parse(output.trim());
2257
2309
  return Number.isFinite(parsed) ? parsed : void 0;
2258
2310
  } catch {
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-CH6_J1Ee.js";
1
+ import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-DLT0FZbA.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createProvider } from "@earendil-works/pi-ai";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
- "description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
5
- "version": "2.1.0",
4
+ "description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
+ "version": "2.1.2",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -46,6 +46,7 @@
46
46
  "README.en.md",
47
47
  "CHANGELOG.md",
48
48
  "THIRD_PARTY_NOTICES.md",
49
+ "docs/WINDOWS.md",
49
50
  "LICENSE"
50
51
  ],
51
52
  "scripts": {
@@ -64,7 +65,6 @@
64
65
  "@deepseek-ai/dsh-client-ui-settings",
65
66
  "@deepseek-ai/dsh-client-ui-slots",
66
67
  "@deepseek-ai/dsh-client-ui-settings-plugins",
67
- "@deepseek-ai/dsh-client-ui-primitives",
68
68
  "@deepseek-ai/dsh-client-locale"
69
69
  ],
70
70
  "platform": "web"
@@ -80,36 +80,35 @@
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@deepseek-ai/cordis": ">=4.0.1-rc.1 <5.0.0 || >=4.0.2 <5.0.0",
83
- "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0",
84
- "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0",
85
- "@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0",
86
- "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0",
87
- "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0",
88
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0",
89
- "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0",
90
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0",
91
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0",
92
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0",
83
+ "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.3.0-0",
84
+ "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.3.0-0",
85
+ "@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.3.0-0",
86
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.3.0-0",
87
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
88
+ "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
89
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
90
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.3.0-0",
91
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.3.0-0",
92
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.3.0-0",
93
93
  "@deepseek-ai/schemastery": ">=3.18.1-rc.1 <4.0.0 || >=3.18.2 <4.0.0",
94
94
  "@earendil-works/pi-ai": ">=0.85.0 <0.86.0",
95
95
  "react": "^18.2.0"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || >=4.0.2 <5.0.0",
99
- "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0",
100
- "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0",
101
- "@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0",
102
- "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0",
103
- "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0",
104
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0",
105
- "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0",
106
- "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0",
107
- "@deepseek-ai/dsh-client-runtime": ">=0.1.1-rc.1 <0.2.0",
108
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0",
109
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0",
110
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0",
111
- "@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.2.0",
112
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0",
99
+ "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.3.0-0",
100
+ "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.3.0-0",
101
+ "@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.3.0-0",
102
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.3.0-0",
103
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
104
+ "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
105
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
106
+ "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.3.0-0",
107
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.3.0-0",
108
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.3.0-0",
109
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.3.0-0",
110
+ "@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.3.0-0",
111
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.3.0-0",
113
112
  "@deepseek-ai/schemastery": ">=3.18.1 <4.0.0 || >=3.18.2 <4.0.0",
114
113
  "@earendil-works/pi-ai": ">=0.85.0 <0.86.0",
115
114
  "react": "^18.3.1",