dsh-connect-workbuddy 2.0.15 → 2.1.1

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,119 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1 (2026-09-26)
4
+
5
+ ### Features
6
+
7
+ - **平台支持口径明确化:Windows 是一等目标平台。** 此前 README 只在**散落的五处**顺带提到 Windows(凭据路径、加密字段、`settings.yaml` 占用、路径未验证、CLI),**没有任何一处说「支持 Windows」**——读者要自己把五处拼起来才知道这是受支持平台。本次把它写成明示口径,并把「以后每次改动都要考虑 Windows」落到仓库里,而不是留在会话或记忆里。
8
+
9
+ - **`README.md` / `README.en.md` 新增「平台支持 / Platform support」一节**:一张三平台对照表(凭据默认目录、桌面 App 可执行文件的定位方式、心跳时间戳来源、设置写入的瞬时占用、CI 覆盖),加 Windows 上需要知道的三个具体点。表内每格都是**实现事实**(对应代码位置与测试已登记在 `docs/WINDOWS.md`),不是「尚未支持」的委婉说法。
10
+ - **新增 `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 上会是死链。
11
+ - **`RELEASING.md` 新增 §2.5「平台核对」**:每次发布前过一遍 `docs/WINDOWS.md` §4 的检查清单,并核对 CI 矩阵仍含 `windows-latest`、两条 README 的平台节与登记处三者一致。这是唯一**每次发布都必然被读到**的位置,因此把口径挂在这里。
12
+ - **`.gitattributes`(新增)**:仓库此前没有换行符约定,而文本文件在索引里都是 LF。Git for Windows 默认 `core.autocrlf=true` 会在检出时转成 CRLF,于是同一份测试在 Windows 与 POSIX 上读到的字节不同——本仓库的测试会直接读源码文本,这类断言必须跨平台稳定。现显式声明 `* text=auto eol=lf` 与各文本类型、并把图片等标为 `binary`。
13
+ - **为什么值得写进文件而不是只嘱咐一句**:这四个要素(CI 矩阵的 Windows 项、登记文件、两条 README 的平台节、发布清单)**每一个都是一行之差就会静静消失的东西**,而消失时不会有任何东西报错——CI 会继续全绿,构建会继续成功,README 会继续正常渲染。据此加了 `tests/platform-standing.spec.ts` 作为守卫。
14
+ - **`docs/WINDOWS.md` §7「在 Windows 真机上验证」**:一份可照做的操作手册——先跑 `doctor --json` 一次拿全四项证据(附字段→分支对照表),再逐项验 §1 的各分支,重点是最值得实测的 §1-4(设置写入的瞬时占用,附可直接粘贴的 PowerShell 锁文件脚本),最后给出回报时应附的信息,以便把 §2 里那些 C 档结论升为 A 档。本次已在真机执行过其中大部分,结果记录在 §7.0。
15
+ - **顺带查实并改正一处事实错误:被写入的是 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 版本再次过期)。
16
+
17
+ ### Bug Fixes
18
+
19
+ #### Windows 宿主内每次进程存活判定都闪一个黑色控制台窗口
20
+
21
+ `src/host-heartbeat.ts` 的 `processStartTimeMs()` 用 `execFileSync('powershell', …, { encoding: 'utf8' })` 启动子进程,**没有设 `windowsHide`**——而同一仓库的 `src/at-rest.ts` 在取 at-rest 密钥时设了。两处不一致,缺的那一处就是缺陷。
22
+
23
+ - **为什么在 macOS 与终端里永远看不到**:Windows 只在**父进程没有控制台**时才给控制台程序子进程新建窗口。终端里父进程有控制台,子进程只是继承,现象根本不存在;`windowsHide` 在 POSIX 上又是无操作。所以这个缺陷对本地开发与现有测试**完全不可见**。
24
+ - **而真实宿主正是会触发的形态**:`DSH NEXT.exe` 是 Electron GUI 进程,实测 `MainWindowHandle = 0`、无控制台。于是每次 `isHeartbeatProcessAlive()`(`status` / `doctor` 都会走到)都会在用户屏幕上新建并显示一个控制台窗口——即「黑框一闪」。
25
+ - **真机取证**(Windows 11 22621,用 WMI 服务 `Win32_Process.Create` 造出与 GUI 宿主同为「无控制台」的父进程,再由子进程用 `kernel32!GetConsoleWindow` + `user32!IsWindowVisible` 自报它实际拥有的控制台):
26
+
27
+ | 父进程形态 | 不设 `windowsHide` | 设 `windowsHide: true` |
28
+ | --- | --- | --- |
29
+ | 终端(有控制台) | 继承,无新窗口 | 继承,无新窗口 |
30
+ | WMI 服务创建(无控制台,等价于 GUI 宿主) | **`NEW-console hwnd=1573500 visible=True`** | `no-console` |
31
+
32
+ - **同时排除一条错误判据**:不能用 `Get-Process powershell | MainWindowHandle` 观察。控制台窗口归 `conhost.exe` 所有,控制台程序自身的 `MainWindowHandle` 恒为 0——即使窗口正在屏幕上,该探针也报 0(本次实测中它先给出了假阴性)。
33
+ - **修法**:抽出 `PROCESS_PROBE_OPTIONS`(`{ encoding: 'utf8', windowsHide: true }`)与 `processStartProbe(pid, platform)`。后者的 `platform` 可注入,与 `defaultDesktopAuthDirs()` / `workbuddyAppExecutableCandidates()` 同一套缝——使平台分支与启动选项**在任意主机上都可断言**,这是此类「Windows 专有、macOS 上不可能复现」缺陷唯一能被回归拦住的形态。
34
+ - **守卫与变异验证**:`tests/host-heartbeat.spec.ts` 断言选项值与平台分支;`tests/platform-standing.spec.ts` 新增用例要求**两个** spawn 点都带 `windowsHide: true`。去掉该选项 → 对应用例变红(已实测)。`docs/WINDOWS.md` 登记为 §1-5 分支并附 A 档取证。
35
+
36
+ #### `docs/WINDOWS.md` 里 issue #13 的「重试预算(200+600ms)」是错的
37
+
38
+ 该数字没有任何实现支撑。核对上游 `@deepseek-ai/dsh-atomic-write`(rc.1 与 rc.2 常量相同)后,真实预算是**延迟序列 20+40+80+160+200×4 ≈ 1100ms**(`initial 20ms` / `max 200ms` / `limit 8`)——**不存在 600ms 这一项**。
39
+
40
+ - **真机实测**(同一台 Windows 11,逐一量持有期与结果):持有 0 / 100 / 300 / 600 / 1000ms 后释放 → 写入**全部成功**(端到端 3 / 174 / 339 / 760 / 1165ms);持有 60s(超出预算)→ `EPERM`,且目标文件**保持原内容**、不产生半写文件(已验证 `onDisk` 仍为原值)。
41
+ - 现已按实测改写 `docs/WINDOWS.md` §3。这条属于**文档里的数字与实现脱节**:它不影响行为,但会误导后来者按错误的预算去调参或归因。
42
+
43
+ #### `dsh.client.inject` 仍依赖 `dsh-client-ui-primitives`
44
+
45
+ `ddd11ed`(上游复审那次提交)第 6 条把卡片折叠箭头从宿主图标 `IconChevronDownOutline14` 改为**纯 CSS caret**,并删掉了该包在 `src/client/` 的最后一处导入——但 `package.json` 的 `dsh.client.inject` 里的 `@deepseek-ai/dsh-client-ui-primitives` 留了下来。声明比代码活得久。
46
+
47
+ - **为什么不是无害的多余声明**:`inject` 不是「仅供参考的清单」,但**它的作用范围比初看要窄**——两条机制各自加了一道限制,本条据实说明:
48
+ 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 顺序约束**。
49
+ 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 条目实际不产生任何加载边**。
50
+ - **那么它为什么仍该删**:它是**过期声明**,不是运行时炸弹。真实成本是「声明与代码不一致」本身——保留一个零引用条目,等于向未来的维护者与工具谎称本插件依赖该包;一旦哪天该包被移出平台 seed 表(它并非不可能:该包在本区间改动 75 个文件、约 1500 行,且是唯一删除了导出 `OnboardingSurface` 的一个),这条就会从「无效」变成「生效的隐式依赖」,而那时没人记得它是怎么来的。清掉它,是让声明回到只描述真实依赖的状态。
51
+ - **修法**:从 `inject` 删除该条目(`package.json` 的 `peerDependencies` / `devDependencies` / `tsdown.config.ts` 的 externals 保留——那是类型检查与构建所需,与运行时装配无关)。
52
+ - 另:`src/client/WorkBuddyCard.tsx` 的文件头注释至今仍写着「`IconChevronDownOutline14` 的使用…来自该项目」,而该图标导入早在 `ddd11ed` 就已删除(改成纯 CSS caret)。本次一并更正该注释——它就是这条过期声明当初的同类残留。
53
+
54
+ #### 卡片主题令牌名写错:`state-warning-primary` 在两条 0.1.7 线上都不存在
55
+
56
+ 加密凭据原因的着色用了 `--dsw-alias-state-warning-primary`,而宿主真实令牌是**缩写形** `--dsw-alias-state-warn-primary`(`packages/client/ui-theme/src/styles/design-platform.css`,在 rc.1 与 rc.2 上**都**只有缩写形)。
57
+
58
+ - **为什么静默**:该声明带十六进制兜底色 `#e0a13a`。CSS 自定义属性名不存在时 `var()` 走兜底——规则从不空白,于是**兜底值在每一次渲染都生效**,宿主令牌从未被用上,颜色与设计系统彻底脱钩。写错一个词,不会有任何报错、告警或视觉空白来提示。
59
+ - **修法**:令牌改为 `--dsw-alias-state-warn-primary`,兜底值改为该令牌的真实解析值 `#f59e0b`(`--dsw-static-amber-500`)。
60
+ - **实际可见变化**:加密凭据那条原因的着色由 `#e0a13a` 变为 `#f59e0b`(宿主设计系统的 amber)。据实说明一点:`--dsw-static-amber-500` 在 light 与 dark 两处定义**同为 `rgb(245,158,11)`**,即该令牌是**主题不变量**——所以这次修复不是「颜色开始跟随主题」,而是「颜色回到宿主令牌这条正确的取值路径上」:取值此后由宿主决定,且兜底值与令牌一致,即便令牌缺失也不会再显示成一个无关的颜色。
61
+ - 这不是 0.1.7-rc.2 引入的问题,是**既有缺陷**;rc.2 只是提供了发现它的时机。内核自己的决策记录点名过同一处混淆(`.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md:71`)。
62
+
63
+ ### Tests
64
+
65
+ - 新增 `tests/client-host-contract.spec.ts`(6 例):上述两处都是**声明性**缺陷,因此测试读文件(`package.json` 与 `src/client/styles.ts`)而非导入它们——也让本套件不必拖入浏览器专属的 DSH 客户端包。用例覆盖:`inject` 不得出现客户端半边零引用的包;不得依赖 `ui-primitives`;四条真正参与组合的客户端包同时在册(防反向丢漏);不得使用 `state-warning-primary` 长形;`state-*` 令牌必须落在宿主定义集内;兜底值必须是宿主 amber。
66
+ - **变异验证**:把 `ui-primitives` 塞回 `inject` → **2 例变红**;把令牌改回 `state-warning-primary`/`#e0a13a` → **3 例变红**。两处缺陷各自单独还原都会被抓住。
67
+ - 记录一条本次核实到的**反直觉事实**,避免后人写下错误的断言:`inject` **不是** value imports 的镜像。内核自身两个方向的反例都在——`ui-settings-plugins` 用裸 `import type {}` 做模块增强却照样 inject 那两个包(`src/client/index.ts:11,15`);`ui-theme` 实际导入并使用 `ui-primitives` 的**组件**(`AppearanceRow.tsx:11`)却**不** inject 它。故本套件只断言「inject 里不得有零引用项」这一单向不变量,不碰反向。
68
+
69
+ - 新增 `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` → 该例变红。
70
+ - 另新增 2 例(同文件)守卫本次顺带查实的事实:被写入的配置文档是 profile 的 `cordis.patch.yml`,不是 `settings.yaml`。**变异验证**:把用户文案改回 `settings.yaml` → 1 例变红;把 `docs/WINDOWS.md` §7.3 里「不是 `settings.yaml`」的澄清改掉 → 1 例变红。
71
+
72
+ ### Docs
73
+
74
+ - **修正两处已过期的「推理强度两态」表述**——文档还停在「未修复」,而代码早在 **2.0.10**(`4c39645`,issue #7)就已修复。纯文档纠正,**无代码改动**。
75
+ - `docs/DESIGN.md`:「推理强度两态(已知问题)」改为「已修复」,并补上实际实现(`parseReasoning` 两态解析、`singularEffortLadder` 折叠为全阶梯 `low/medium/high/xhigh/max`、单数值折入 `defaultEffort`、`canDisableThinking` 视为 `true`)与两网关实测依据;同节字段表里 `reasoning.effort` 的「固定推理档位」改为「折叠为全阶梯可选档位,该值作默认档」。
76
+ - `docs/reasoning-investigation.md`:状态由「调查完成,**未修复**」改为「已修复(issue #7,随 2.0.10)」,并**就地标注**其余过期处——第三节(根因)标为历史代码形态;第四节的推断「不发 `reasoning_effort` 时上游按默认 `effort` 运行、这些模型一直在思考」被修复时的实测**推翻**(实测为**零思考**,即默认思考关闭);第七节「待决策」标为已决策(采纳「结合实测」一路);第八节补「落地情况」列,如实区分已落地与未采纳(`supportsReasoning` / `onlyReasoning` 至今确实未解析,属拟议增强而非缺陷);第九节改标各文件实际改动状态。
77
+ - 保留原文与就地注记,不删史——记录当时的推理路径,避免后人重复踩。
78
+
79
+ ## 2.1.0 (2026-09-25)
80
+
81
+ ### ⚠️ 版本基线:仅支持 DSH 0.1.7-rc.1 及以上
82
+
83
+ **自本版起,本插件只支持 DeepSeek Harness 0.1.7-rc.1 及以上的宿主**(`@deepseek-ai/dsh-*` peer 依赖的最低版本已从 `>=0.1.5-rc.1` 提高到 `>=0.1.7-rc.1`,旧版宿主将无法通过依赖解析安装本版)。
84
+
85
+ - **0.1.5 双线分支已移除**:宿主的 `installSection` 回落、客户端的 `settingsScope` 分支、`settings.plugin.item` 槽位注册已全部删除——`configure({auto}, owner)` 与 `configForms` 是唯一路径。
86
+ - 如果你仍在 0.1.5 宿主上,请停留在 **2.0.15**(最后一个支持 0.1.5 的 GA 版本;2.0.16 / 2.0.17 的缺陷修复已并入本版)。
87
+
88
+ ### Bug Fixes
89
+
90
+ #### provider 被宿主判定为「未配置」(DSH 0.1.7-rc.1)
91
+
92
+ 0.1.7 把设置模型整体重建(`SettingsProvider` → `SettingsForms`)后,**命名空间不再由插件自选**:宿主 `describe()` 以 **`ns: entry.options.id`**(Loader 条目 id)为键,而宿主按**精确匹配**查表 —— `namespaces.get(entry.settingsNs)`。我们此前硬编码 `settingsNs: 'workbuddy'`,而实测条目 id 是 **`include:dsh-connect-workbuddy`**,于是查表落空、provider 读作「未配置」,**不报错、只是静默失效**。
93
+
94
+ - **修法**:新增 `settingsNamespaceOf(ctx)`,采用一线插件的权威范式
95
+ `const settingsNs = ctx.fiber.entry?.options.id ?? NS`(见 `dsh-llm-pi-ai`),并把全部 6 处用法改为解析值。`ctx.fiber.entry` 由 Loader 注入(非 Cordis 公共类型),因此保留探测 + 回落,并在无 Loader 条目时回落到声明常量。
96
+ - **常量移入 `src/status-paths.ts`**:浏览器侧也需要同一个回落值,而它不能 import Node-only 的宿主入口。该模块本就是 host↔client 的无 node 依赖桥梁。`src/index.ts` 继续导出,**公共 API 不变**。
97
+ - **回归测试**(`tests/settings-integration.spec.ts`,+4 例):采纳 Loader 条目 id、无条目/空 id/非字符串时回落、以及**目录实际宣告的就是解析值**(断言目录条目而非常量,否则恒过)。**变异验证**:忽略 entry id 恒用常量 → **1 例立即变红**。
98
+
99
+ #### 插件管理页 / 市场页的 logo 显示不对
100
+
101
+ 0.1.7 的插件管理页从插件包的 `package.json` 的 `icon` 字段读图标(`dsh-client-ui-plugin-manager` 渲染 `row.meta?.icon`,plugin-manager 契约含 `icon: z.string().optional()`),我们没有声明该字段 → 宿主无图可用。
102
+
103
+ - **修法**(照同级 `dsh-ldvh` 的做法):新增 `icons/` 目录(64px + 128px 真实 PNG,其中 64px 与既有卡片内嵌 base64 逐字节一致——同族 LD logo),`package.json` 声明 `"icon": "icons/dsh-connect-workbuddy-128.png"`,并把 `icons` 加入 `files` 白名单随包分发。
104
+ - **验证**:`npm pack` 确认两个 PNG 进包(15 → 17 个文件);desktop profile 的 link 安装下,`node_modules/dsh-connect-workbuddy/package.json` 已含该字段。
105
+
106
+ ### Compatibility(对运行中 0.1.7-rc.1 宿主实测,非推测)
107
+
108
+ - **可用接缝**:宿主已识别本插件 Config(`status: "schema"`、`limitations: []`);`authFile` / `accounts` / `regions` 的 `"x-cordis": {"volatile": true}` 齐备;客户端卡片在 `plugins.bundle.config` 与 `plugins.row.config` **均已注册且 active**;`settings.configure/describe/mutate` 三个方法签名逐字匹配;`loader/volatile-update` 事件仍在。
109
+ - **确认不受影响的破坏性变更**:`settings.plugin.item` 槽位删除(我们各自独立 try/catch,不牵连其他槽位)、`settingsScope` 客户端服务删除(改用 `configForms`)、**`dsh-client-ui-primitives` 图标命名族改名**(本插件不从该包取任何具名图标,折叠箭头早已改为纯 CSS)、`agent/session-start` 删除(未监听)、会话格式 V3→V4(不读写会话文件)。
110
+ - 核对明细见 `.dsh-npm-cache/compat-017-audit.md`。
111
+ - **测试基座改为手动 `apply(ctx, config)`**:`ctx.plugin(WorkBuddy, config)` 会让 Cordis 校验出 frozen 代理对象,测试无法再就地写入;改为直接调用 `apply()` 让 config 由测试与所选 settings 服务共享同一引用(模拟 0.1.7 Loader 的就地提交语义),`loader/volatile-update` 事件用正确的单参形式触发。
112
+
113
+ ### Tests
114
+
115
+ - 349 → 348(删 2 例 0.1.5 形态用例:`installSection` 引用清理、0.1.5 回落;加 1 例 0.1.7 `configure` 唯一路径;命名空间 +4、其余不变)。
116
+
3
117
  ## 2.0.15 (2026-09-24)
4
118
 
5
119
  ### Bug Fixes
package/README.en.md CHANGED
@@ -54,6 +54,10 @@ 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.
58
+
59
+ > 🪟 **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.
60
+
57
61
  Prerequisite: the WorkBuddy desktop app is installed and signed in (the plugin reuses the app's sign-in state).
58
62
 
59
63
  ```sh
@@ -75,6 +79,25 @@ dsh plugin --profile web add dsh-connect-workbuddy # Web
75
79
  dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
76
80
  ```
77
81
 
82
+ ## Platform support
83
+
84
+ 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.
85
+
86
+ | Aspect | Windows | macOS | Linux |
87
+ | --- | --- | --- | --- |
88
+ | 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` |
89
+ | 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) |
90
+ | Process-heartbeat timestamp source | PowerShell `Get-Process` `StartTime` (POSIX has no `ps -lstart` equivalent) | `ps -o lstart=` | `ps -o lstart=` |
91
+ | 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 |
92
+ | 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 |
93
+ | CI coverage | `windows-latest`, full suite | The developer's own Mac | `ubuntu-latest`, full suite |
94
+
95
+ **Three Windows-specific things worth knowing**:
96
+
97
+ 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).
98
+ 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.
99
+ 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.
100
+
78
101
  ## Command line
79
102
 
80
103
  ```sh
@@ -121,8 +144,8 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
121
144
  - **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.
122
145
  - 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.
123
146
  - 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.
124
- - **On Windows, an account change may fail to save while `settings.yaml` is held open.** DSH updates that file by writing a temp file and replacing the target; an antivirus scanner, a sync client (OneDrive), or an open editor can lock it briefly, and once the replacement retries are exhausted the failed write does **not** make `set()` reject. The plugin therefore reads the value back after writing: it shows "cleared" **only once the write is confirmed on disk**, and on failure it reports the error and keeps your previous choice (never a false confirmation that the old selection silently contradicts). Close whatever holds the file and retry.
125
- - Windows and Linux credential paths are derived from platform conventions and are not verified on real hardware; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
147
+ - **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.
148
+ - 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.
126
149
  - **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.
127
150
  - **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.
128
151
  - **"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,6 +60,10 @@ 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。
64
+
65
+ > 🪟 **平台支持:Windows、macOS、Linux 三者并列支持。** **Windows 是一等目标平台**,不是「顺带能跑」:CI 在 `windows-latest` 上跑完整套件,凭据路径、进程心跳、App 可执行文件定位、设置写入四处都有 Windows 专属分支与测试,并且**修复只对 Windows 生效的缺陷时,以 Windows 用户回报的现场与真机取证的加密向量为准**(见 [CHANGELOG](CHANGELOG.md) 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方[平台支持](#平台支持)一节了解各平台的具体差异与已知限制。
66
+
63
67
  前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态)。
64
68
 
65
69
  推荐使用 DSH 插件命令安装 npm 已发布版本:
@@ -83,6 +87,25 @@ dsh plugin --profile web add dsh-connect-workbuddy # Web
83
87
  dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
84
88
  ```
85
89
 
90
+ ## 平台支持
91
+
92
+ 三个平台都是受支持目标;下列差异是**实现事实**,不是「尚未支持」的委婉说法。要改这一节,请同时改 [`docs/WINDOWS.md`](docs/WINDOWS.md)——那份文件是平台相关约束的登记处。
93
+
94
+ | 方面 | Windows | macOS | Linux |
95
+ | --- | --- | --- | --- |
96
+ | 凭据默认目录 | `%LOCALAPPDATA%` / `%APPDATA%` 优先,未设时回落 `<home>\AppData`(重定向配置文件仍可解析) | `~/Library/Application Support/CodeBuddyExtension/Data/Public/auth` | `$XDG_CONFIG_HOME` 优先,未设时回落 `~/.config` |
97
+ | 桌面 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` 指定凭据) |
98
+ | 进程心跳的时间戳来源 | PowerShell `Get-Process` 的 `StartTime`(POSIX 无 `ps -lstart` 等价物) | `ps -o lstart=` | `ps -o lstart=` |
99
+ | 子进程的窗口 | **必须 `windowsHide`**:宿主本身没有控制台(GUI 进程),漏设会让每次探测都在屏幕上闪一个可见的黑框 | 不适用(该选项仅 Windows 生效) | 不适用 |
100
+ | 设置写入的瞬时占用 | **会踩到**:profile 的配置文件 `cordis.patch.yml` 被杀毒软件 / OneDrive / 编辑器短暂锁定会拒绝 rename 覆盖,`@deepseek-ai/dsh-atomic-write` 的重试**只在 `win32` 生效** | 不踩:POSIX 的 rename 直接替换,同一段代码不进重试分支 | 同 macOS |
101
+ | CI 覆盖 | `windows-latest` 跑完整套件 | 开发者本机(macOS) | `ubuntu-latest` 跑完整套件 |
102
+
103
+ **Windows 上需要知道的三个具体点**:
104
+
105
+ 1. **账号改动可能保存失败**(profile 的配置文件 `cordis.patch.yml` 被占用)。插件会在写入后**回读校验**:确认落盘才显示「已清除」,失败则明确报错并保留原选择。遇到提示时关闭占用该文件的程序后重试。详见[已知限制](#已知限制)。
106
+ 2. **凭据路径已部分经真机验证**。候选列表按平台约定推导并有单元测试钉住(含 `LOCALAPPDATA` / `APPDATA` 缺省与空白串两种回落)。已在真实 Windows 11 上确认:`%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` **命中**,加密凭据的 `keyId` 与登记向量逐字一致。**其余候选仍未经真机逐项验证**——若你的凭据或 App 不在默认位置,用 `WORKBUDDY_AUTH_FILE` / `WORKBUDDY_APP_EXECUTABLE` 指定,并欢迎回报现场。
107
+ 3. **加密凭据(5.6.0 起)需要桌面 App 在场**,Windows 与 macOS 同样如此;App 装在自定义目录(如 `E:\WorkBuddy\WorkBuddy.exe`)时用 `WORKBUDDY_APP_EXECUTABLE` 指定。
108
+
86
109
  ## 命令行
87
110
 
88
111
  ```sh
@@ -129,8 +152,8 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
129
152
  - **账号切换**基于 WorkBuddy 桌面端留下的历史 auth 文件(App 自身的备份产物),并非官方多账号 API。默认行为仍是跟随 App 当前登录;切换账号是显式选项,且历史凭据可能因 App 清理或退出登录而失效。
130
153
  - 显式选择的账号若在本机消失(App 更换登录或清理备份文件),插件**不会**静默改选其他账号——那样会让账单落到另一个账号上。卡片会明确说明「保存的账号已不存在」,此时重新选择一个账号,或点「跟随 App 当前登录」清除该选择即可恢复。**重新登录桌面端 App 无法修复这种状态**:凭据本身是好的,失效的只是保存下来的账号标识。
131
154
  - 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
132
- - **Windows:设置文件 `settings.yaml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
133
- - Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
155
+ - **Windows:profile 的配置文件 `cordis.patch.yml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
156
+ - Windows / Linux 的凭据默认路径按平台约定推导。已在真实 Windows 11 上确认 `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` 命中,**其余候选与 Linux 路径仍未经真机验证**;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
134
157
  - **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
135
158
  - **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 应用而误启动它。
136
159
  - **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 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-bvp_AX4w.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-BotcgEag.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
@@ -159,6 +159,20 @@ window.__ModuleLoader__.load({
159
159
  for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
160
160
  return out;
161
161
  }
162
+ /**
163
+ * The plugin's DECLARED settings namespace — the fallback name, not the value
164
+ * the host necessarily serves.
165
+ *
166
+ * On 0.1.7 the settings service keys every form by the plugin's Loader entry id
167
+ * (`describe()` returns `ns: entry.options.id`), and the harness resolves a
168
+ * provider's namespace by EXACT match. The entry id is chosen by the profile
169
+ * patch, so a plugin cannot know it in advance — the live Desktop host mounts
170
+ * this one as `include:dsh-connect-workbuddy`.
171
+ *
172
+ * Lives here, not in the Host entry, because the browser half needs the same
173
+ * fallback and must not import Node-only host code.
174
+ */
175
+ const WORKBUDDY_SETTINGS_NS = "workbuddy";
162
176
  //#endregion
163
177
  //#region src/client/account-selection.ts
164
178
  /**
@@ -300,7 +314,11 @@ window.__ModuleLoader__.load({
300
314
  * Write one region's account slot, then confirm the value actually landed.
301
315
  *
302
316
  * `settingsScope.set()` resolving is NOT proof that anything was stored. The
303
- * 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
304
322
  * renaming it over the target; on Windows an antivirus scanner, a sync client
305
323
  * (OneDrive, Dropbox), or an open editor can hold the file briefly.
306
324
  * `@deepseek-ai/dsh-atomic-write` retries `EPERM` / `EBUSY` / `EACCES` a
@@ -470,7 +488,7 @@ window.__ModuleLoader__.load({
470
488
  .dsm-workbuddy-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
471
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}
472
490
  .dsm-workbuddy-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
473
- .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)}
474
492
  .dsm-workbuddy-searched-reason-wrong-region{color:var(--dsw-alias-brand-primary,#5686fe)}
475
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}
476
494
  .dsm-workbuddy-searched-more:hover{text-decoration:underline}
@@ -662,11 +680,13 @@ window.__ModuleLoader__.load({
662
680
  * — 卡片的整体结构(折叠外壳 / 账号状态行 / 账号下拉 / 积分区 / 模型表 /
663
681
  * 操作按钮行)、模块加载时注入一次 `<style>` 的写法、
664
682
  * 草稿态(draftModels/draftEnabledIds)与 dirty 标记的保存流程、
665
- * 60 秒轮询与 AbortController 清理、以及
666
- * `IconChevronDownOutline14` 的使用,均来自该项目的 TraeUsageCard。
683
+ * 60 秒轮询与 AbortController 清理,均来自该项目的 TraeUsageCard。
667
684
  * 折叠卡片外壳与 `settings.plugin.item` 槽位形态来自
668
685
  * dingminhua/dsh-subagent-default-model(MIT)。
669
686
  * 改动:
687
+ * 0. 折叠箭头改用纯 CSS caret(理由见 styles.ts):两版宿主的图标命名族
688
+ * 不同,静态导入必挂一边。此前沿用的 `IconChevronDownOutline14` 已
689
+ * 随该改动移除,故不再计入上方参考项。
670
690
  * 1. 积分区改为「合计 + 按套餐名聚合的进度条」,因为实测单个账号下
671
691
  * 同名套餐可达 19 个,逐条渲染会淹没卡片(原项目按上游条目直出);
672
692
  * 2. 模型行补上 WorkBuddy 上游给出的积分倍率、多模态与推理档位;
@@ -945,9 +965,9 @@ window.__ModuleLoader__.load({
945
965
  * write made there does NOT update the browser settings mirror. Reading the
946
966
  * mirror therefore left the checkbox stuck ON after a successful disable —
947
967
  * "不能正确取消国际版/国内版" — even though `enabled: false` was already in
948
- * settings.yaml. The Host sends the committed value it derives from its own
949
- * config (`status.enabled`, see `deps.regionEnabled`), so the card renders
950
- * 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.
951
971
  *
952
972
  * The mirror stays as the FALLBACK: a host that predates the field, or a
953
973
  * status that has not loaded yet, still renders from the stored document
@@ -1652,7 +1672,7 @@ window.__ModuleLoader__.load({
1652
1672
  "row.accountsScanning": "Detecting…",
1653
1673
  "row.accountNoneInEffect": "No account in effect",
1654
1674
  "row.accountsSavedChoice": "Using the account saved for this region.",
1655
- "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).",
1656
1676
  "row.selectionLostHint": "The saved account no longer exists locally.",
1657
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.",
1658
1678
  "row.modelsTitle": "Models",
@@ -1727,7 +1747,7 @@ window.__ModuleLoader__.load({
1727
1747
  "row.accountsScanning": "正在检测…",
1728
1748
  "row.accountNoneInEffect": "当前没有生效的账号",
1729
1749
  "row.accountsSavedChoice": "正在使用该区域保存的账号。",
1730
- "row.accountsWriteFailed": "账号改动未保存:{message} —— 设置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用 settings.yaml 的程序再重试。",
1750
+ "row.accountsWriteFailed": "账号改动未保存:{message} —— 当前 profile 的配置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用该文件的程序再重试(文件的确切路径见插件仓库的 docs/WINDOWS.md)。",
1731
1751
  "row.selectionLostHint": "已保存的账号在本机已不存在。",
1732
1752
  "row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
1733
1753
  "row.modelsTitle": "模型",
@@ -1751,6 +1771,29 @@ window.__ModuleLoader__.load({
1751
1771
  };
1752
1772
  //#endregion
1753
1773
  //#region src/client/index.tsx
1774
+ /**
1775
+ * Browser half: WorkBuddy credits and model management inside Plugin
1776
+ * configuration.
1777
+ *
1778
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
1779
+ * — 浏览器插件的注册形态(`slots` / `locale` / `settingsScope` 三项注入、
1780
+ * `LocaleNamespaceMap` 的模块增强、`settings.plugin.item` 槽位与
1781
+ * `key` / `priority` 的 rc.7 写法、以及整个 apply 体包 try/catch
1782
+ * 以便槽位 API 变更时降级为 console.error 而不触发
1783
+ * "Failed to load plugins" 红色横幅)来自该项目,
1784
+ * 其亦注明沿用 corrinehu/dsh-workbuddy-connect 的同一模式。
1785
+ * 改动:无实质改动,仅改为本插件的命名空间与组件名。
1786
+ *
1787
+ * NOTE: the try/catch boundary of this function is mirrored (duplicated) in
1788
+ * `tests/client-fallback.spec.ts`, because the real client entry imports
1789
+ * browser-only DSH packages that cannot load in the Node test environment.
1790
+ * That test therefore does not import this function — it replicates its
1791
+ * shape. If you change the guarded body or the `console.error` message here,
1792
+ * update the mirrored `apply()` in that spec too, or the fallback test will
1793
+ * silently diverge from this real implementation.
1794
+ *
1795
+ * @module dsh-connect-workbuddy/client
1796
+ */
1754
1797
  /** Stable browser-plugin name. */
1755
1798
  const name = "dsh-connect-workbuddy-client";
1756
1799
  /**
@@ -1776,15 +1819,14 @@ window.__ModuleLoader__.load({
1776
1819
  const softGet = (name) => ctx.get(name);
1777
1820
  let settingsScope;
1778
1821
  const forms = softGet("configForms");
1779
- const legacy = softGet("settingsScope");
1780
1822
  if (forms !== void 0) {
1781
- let ns = "workbuddy";
1823
+ let ns = WORKBUDDY_SETTINGS_NS;
1782
1824
  try {
1783
1825
  const served = (forms.describe().getSnapshot().view?.namespaces ?? []).find((entry) => entry.ns === "workbuddy" || /workbuddy/i.test(entry.ns));
1784
1826
  if (served !== void 0) ns = served.ns;
1785
1827
  } catch {}
1786
1828
  settingsScope = forms.get(ns);
1787
- } else if (legacy !== void 0) settingsScope = legacy.bind({ namespace: "workbuddy" });
1829
+ }
1788
1830
  const registerCard = (slotName, key) => {
1789
1831
  try {
1790
1832
  ctx.slots.inject(slotName, () => ctx.slots.register({
@@ -1802,7 +1844,6 @@ window.__ModuleLoader__.load({
1802
1844
  };
1803
1845
  registerCard("plugins.bundle.config", "dsh-connect-workbuddy");
1804
1846
  registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy");
1805
- registerCard("settings.plugin.item", "workbuddy");
1806
1847
  } catch (error) {
1807
1848
  console.error("[dsh-connect-workbuddy] client card failed to load (host provider unaffected):", error);
1808
1849
  }
@@ -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.0.15";
2211
+ const WORKBUDDY_CONNECT_VERSION = "2.1.1";
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.d.ts CHANGED
@@ -1390,6 +1390,20 @@ type WorkBuddyWebUsage =
1390
1390
  * value is never touched.
1391
1391
  */
1392
1392
  declare function unwrapVolatileDeep<T>(value: T): T;
1393
+ /**
1394
+ * The plugin's DECLARED settings namespace — the fallback name, not the value
1395
+ * the host necessarily serves.
1396
+ *
1397
+ * On 0.1.7 the settings service keys every form by the plugin's Loader entry id
1398
+ * (`describe()` returns `ns: entry.options.id`), and the harness resolves a
1399
+ * provider's namespace by EXACT match. The entry id is chosen by the profile
1400
+ * patch, so a plugin cannot know it in advance — the live Desktop host mounts
1401
+ * this one as `include:dsh-connect-workbuddy`.
1402
+ *
1403
+ * Lives here, not in the Host entry, because the browser half needs the same
1404
+ * fallback and must not import Node-only host code.
1405
+ */
1406
+ declare const WORKBUDDY_SETTINGS_NS: "workbuddy";
1393
1407
  //#endregion
1394
1408
  //#region src/web-status.d.ts
1395
1409
  /** Constructor dependencies. */
@@ -1458,8 +1472,30 @@ declare function registerWorkBuddyStatusRoute(ctx: Context, deps: WorkBuddyStatu
1458
1472
  declare const name = "dsh-connect-workbuddy";
1459
1473
  /** The model registry required before the provider can register. */
1460
1474
  declare const inject: string[];
1461
- /** Settings namespace for the plugin configuration card. */
1462
- declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
1475
+ /**
1476
+ * Settings namespace for the plugin configuration card.
1477
+ *
1478
+ * (历史注记:0.1.5 线上插件自选命名空间并经 `installSection` 注册;自
1479
+ * 2.1.0 起只支持 0.1.7 线,该路径已移除。) 0.1.7 的 settings 服务自行推导
1480
+ * 命名空间 —— `describe()` 返回 `ns: entry.options.id`,即 Loader 条目 id
1481
+ * —— 因此插件不再能自选。此常量仅作为「宿主不暴露条目 id」时的回落值;
1482
+ * 实际生效值来自 {@link settingsNamespaceOf}。
1483
+ *
1484
+ * 这个区分不是装饰性的。宿主按**精确匹配**查表
1485
+ * (模型设置页的 `namespaces.get(entry.settingsNs)`),所以宣告
1486
+ * `workbuddy` 而宿主实际服务 `include:dsh-connect-workbuddy` 时,provider
1487
+ * 会被判为「未配置」:配置入口与模型发现双双静默失效。
1488
+ */
1489
+ /**
1490
+ * The namespace the HOST actually serves this plugin under.
1491
+ *
1492
+ * `ctx.fiber.entry` is added by the Loader, not by Cordis itself, so it is not
1493
+ * in Cordis's public types and is absent on hosts that mount a plugin without a
1494
+ * Loader entry (a test harness, or `ctx.plugin()` called directly). Hence the
1495
+ * probe plus the documented fallback, mirroring the first-party plugins:
1496
+ * `const settingsNs = ctx.fiber.entry?.options.id ?? NS`.
1497
+ */
1498
+ declare function settingsNamespaceOf(ctx: unknown): SettingsNamespace;
1463
1499
  /** One region's model directory and the user's selection within it. */
1464
1500
  interface WorkBuddyRegionState {
1465
1501
  /**
@@ -1624,4 +1660,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
1624
1660
  */
1625
1661
  declare function apply(ctx: Context, config: Config): void;
1626
1662
  //#endregion
1627
- export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1663
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
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-bvp_AX4w.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-BotcgEag.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";
@@ -656,6 +656,20 @@ function unwrapVolatileDeep(value) {
656
656
  for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
657
657
  return out;
658
658
  }
659
+ /**
660
+ * The plugin's DECLARED settings namespace — the fallback name, not the value
661
+ * the host necessarily serves.
662
+ *
663
+ * On 0.1.7 the settings service keys every form by the plugin's Loader entry id
664
+ * (`describe()` returns `ns: entry.options.id`), and the harness resolves a
665
+ * provider's namespace by EXACT match. The entry id is chosen by the profile
666
+ * patch, so a plugin cannot know it in advance — the live Desktop host mounts
667
+ * this one as `include:dsh-connect-workbuddy`.
668
+ *
669
+ * Lives here, not in the Host entry, because the browser half needs the same
670
+ * fallback and must not import Node-only host code.
671
+ */
672
+ const WORKBUDDY_SETTINGS_NS = "workbuddy";
659
673
  //#endregion
660
674
  //#region src/web-status.ts
661
675
  /** Redact token-like content before it crosses to the browser. */
@@ -997,8 +1011,33 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
997
1011
  const name = "dsh-connect-workbuddy";
998
1012
  /** The model registry required before the provider can register. */
999
1013
  const inject = ["llm", "settings"];
1000
- /** Settings namespace for the plugin configuration card. */
1001
- const WORKBUDDY_SETTINGS_NS = "workbuddy";
1014
+ /**
1015
+ * Settings namespace for the plugin configuration card.
1016
+ *
1017
+ * (历史注记:0.1.5 线上插件自选命名空间并经 `installSection` 注册;自
1018
+ * 2.1.0 起只支持 0.1.7 线,该路径已移除。) 0.1.7 的 settings 服务自行推导
1019
+ * 命名空间 —— `describe()` 返回 `ns: entry.options.id`,即 Loader 条目 id
1020
+ * —— 因此插件不再能自选。此常量仅作为「宿主不暴露条目 id」时的回落值;
1021
+ * 实际生效值来自 {@link settingsNamespaceOf}。
1022
+ *
1023
+ * 这个区分不是装饰性的。宿主按**精确匹配**查表
1024
+ * (模型设置页的 `namespaces.get(entry.settingsNs)`),所以宣告
1025
+ * `workbuddy` 而宿主实际服务 `include:dsh-connect-workbuddy` 时,provider
1026
+ * 会被判为「未配置」:配置入口与模型发现双双静默失效。
1027
+ */
1028
+ /**
1029
+ * The namespace the HOST actually serves this plugin under.
1030
+ *
1031
+ * `ctx.fiber.entry` is added by the Loader, not by Cordis itself, so it is not
1032
+ * in Cordis's public types and is absent on hosts that mount a plugin without a
1033
+ * Loader entry (a test harness, or `ctx.plugin()` called directly). Hence the
1034
+ * probe plus the documented fallback, mirroring the first-party plugins:
1035
+ * `const settingsNs = ctx.fiber.entry?.options.id ?? NS`.
1036
+ */
1037
+ function settingsNamespaceOf(ctx) {
1038
+ const id = ctx?.fiber?.entry?.options?.id;
1039
+ return typeof id === "string" && id !== "" ? id : WORKBUDDY_SETTINGS_NS;
1040
+ }
1002
1041
  const modelConfig = z.object({
1003
1042
  id: z.string().required(),
1004
1043
  name: z.string().required(),
@@ -1171,6 +1210,7 @@ async function legacyAttributionRegion(value, accountsFor) {
1171
1210
  */
1172
1211
  function apply(ctx, config) {
1173
1212
  const client = new WorkBuddyUpstreamClient();
1213
+ const settingsNs = settingsNamespaceOf(ctx);
1174
1214
  const stacks = {};
1175
1215
  for (const region of REGION_KEYS) {
1176
1216
  const authFile = unwrapVolatile(config.authFile);
@@ -1288,7 +1328,7 @@ function apply(ctx, config) {
1288
1328
  registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
1289
1329
  provider: WORKBUDDY_PROVIDERS[region],
1290
1330
  displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
1291
- settingsNs: WORKBUDDY_SETTINGS_NS,
1331
+ settingsNs,
1292
1332
  settingsPath: [],
1293
1333
  declared: false
1294
1334
  })));
@@ -1309,21 +1349,7 @@ function apply(ctx, config) {
1309
1349
  }));
1310
1350
  ctx.inject(["settings"], (sctx) => {
1311
1351
  const settings = sctx.settings;
1312
- if (typeof settings.configure === "function") {
1313
- settings.configure({ auto: true }, ctx.fiber);
1314
- return;
1315
- }
1316
- if (typeof settings.installSection === "function") {
1317
- const entry = unwrapVolatileDeep(config);
1318
- settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, entry, {
1319
- setSource(source) {
1320
- current = source;
1321
- },
1322
- onChange() {
1323
- applySelection(current());
1324
- }
1325
- });
1326
- }
1352
+ ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
1327
1353
  });
1328
1354
  ctx.on("loader/volatile-update", () => {
1329
1355
  applySelection(current());
@@ -1367,13 +1393,13 @@ function apply(ctx, config) {
1367
1393
  releaseDirectory = registration.cn.directory = ctx.llm.registerConfigurableProviders([{
1368
1394
  provider: WORKBUDDY_PROVIDER,
1369
1395
  displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.cn,
1370
- settingsNs: WORKBUDDY_SETTINGS_NS,
1396
+ settingsNs,
1371
1397
  settingsPath: [],
1372
1398
  declared: false
1373
1399
  }, {
1374
1400
  provider: WORKBUDDY_GLOBAL_PROVIDER,
1375
1401
  displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.global,
1376
- settingsNs: WORKBUDDY_SETTINGS_NS,
1402
+ settingsNs,
1377
1403
  settingsPath: [],
1378
1404
  declared: false
1379
1405
  }]);
@@ -1396,7 +1422,7 @@ function apply(ctx, config) {
1396
1422
  releaseDirectory?.();
1397
1423
  }
1398
1424
  syncRegionRegistration(current());
1399
- ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
1425
+ ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => {
1400
1426
  const region = regionOfProvider(request.provider ?? "");
1401
1427
  if (region === void 0) return [];
1402
1428
  if (!regionEnabled(current(), region)) return [];
@@ -1437,4 +1463,4 @@ function apply(ctx, config) {
1437
1463
  });
1438
1464
  }
1439
1465
  //#endregion
1440
- export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1466
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
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.0.15",
4
+ "description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
+ "version": "2.1.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -23,6 +23,7 @@
23
23
  "bin": {
24
24
  "dsh-connect-workbuddy": "lib/bin.js"
25
25
  },
26
+ "icon": "icons/dsh-connect-workbuddy-128.png",
26
27
  "exports": {
27
28
  ".": {
28
29
  "types": "./lib/index.d.ts",
@@ -37,6 +38,7 @@
37
38
  },
38
39
  "files": [
39
40
  "lib",
41
+ "icons",
40
42
  "docs/assets",
41
43
  "screenshots.json",
42
44
  "cordis.patch.yml",
@@ -44,6 +46,7 @@
44
46
  "README.en.md",
45
47
  "CHANGELOG.md",
46
48
  "THIRD_PARTY_NOTICES.md",
49
+ "docs/WINDOWS.md",
47
50
  "LICENSE"
48
51
  ],
49
52
  "scripts": {
@@ -62,7 +65,6 @@
62
65
  "@deepseek-ai/dsh-client-ui-settings",
63
66
  "@deepseek-ai/dsh-client-ui-slots",
64
67
  "@deepseek-ai/dsh-client-ui-settings-plugins",
65
- "@deepseek-ai/dsh-client-ui-primitives",
66
68
  "@deepseek-ai/dsh-client-locale"
67
69
  ],
68
70
  "platform": "web"
@@ -78,36 +80,36 @@
78
80
  },
79
81
  "peerDependencies": {
80
82
  "@deepseek-ai/cordis": ">=4.0.1-rc.1 <5.0.0 || >=4.0.2 <5.0.0",
81
- "@deepseek-ai/dsh-atomic-write": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
82
- "@deepseek-ai/dsh-attachment": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
83
- "@deepseek-ai/dsh-home-paths": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
84
- "@deepseek-ai/dsh-host-webserver": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
85
- "@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
86
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
87
- "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
88
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
89
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
90
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.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",
91
93
  "@deepseek-ai/schemastery": ">=3.18.1-rc.1 <4.0.0 || >=3.18.2 <4.0.0",
92
94
  "@earendil-works/pi-ai": ">=0.85.0 <0.86.0",
93
95
  "react": "^18.2.0"
94
96
  },
95
97
  "devDependencies": {
96
98
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || >=4.0.2 <5.0.0",
97
- "@deepseek-ai/dsh-atomic-write": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
98
- "@deepseek-ai/dsh-attachment": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
99
- "@deepseek-ai/dsh-home-paths": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
100
- "@deepseek-ai/dsh-host-webserver": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
101
- "@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
102
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
103
- "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
104
- "@deepseek-ai/dsh-client-locale": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.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",
105
107
  "@deepseek-ai/dsh-client-runtime": ">=0.1.1-rc.1 <0.2.0",
106
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
107
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
108
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
109
- "@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-rc.1 <0.2.0",
110
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.1 <0.2.0 || >=0.1.2-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",
111
113
  "@deepseek-ai/schemastery": ">=3.18.1 <4.0.0 || >=3.18.2 <4.0.0",
112
114
  "@earendil-works/pi-ai": ">=0.85.0 <0.86.0",
113
115
  "react": "^18.3.1",