dsh-console-sentinel 0.3.0 → 1.0.0

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,4 +1,441 @@
1
- # Changelog
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2026-11-08) — 大版本:一键清理 + 按软件关闭 + 提权关闭 + 全新面板 UI
4
+
5
+ 首个稳定大版本。把此前只在对话里能用的能力全部搬进设置面板,并围绕「安全地批量关窗」重建了 UI 与保护模型。
6
+
7
+ ### 主要能力
8
+ 1. **一键清理任务栏窗口(预演优先)**:`预演清理` 列出将被关闭的窗口(带图标)与保护跳过项;
9
+ 未预演时 `执行清理` 禁用。执行时普通窗口走 `WM_CLOSE`,管理员窗口走一次 UAC 的提权 `WM_CLOSE`。
10
+ 2. **按软件一键关闭**:任务栏按进程分组,`关掉该软件(N)` 一次关闭该程序全部窗口
11
+ (等同对每个窗口按 Alt+F4,但不用挨个点)。
12
+ 3. **两种视图**:默认「仅图标网格」(30×30,一排排,点图标即关),可切「详细视图」
13
+ (分组卡片 + 白名单勾选 + 每窗口 `关闭`)。
14
+ 4. **空壳控制台**:`隐藏可见空壳`(无损)/ `关闭空壳窗口`(真结束,仅限裸 cmd/powershell/pwsh/python)。
15
+ 5. **提权关闭链路**:临时 `.ps1` + `Start-Process -Verb RunAs` + 结果 JSON 回传;
16
+ 用户点「否」如实报 `uac-denied-or-cancelled`,绝不假装成功。
17
+ 6. **自定义设置页图标**(`settings.section.icon`,id 必须等于 section id,否则宿主回退默认齿轮)。
18
+ 7. **面板可见时自动刷新**(3s),关闭设置界面/切走即时停止。
19
+
20
+ ### 安全(本版本的核心)
21
+ - **宿主进程树硬保护**:自身 + 全部祖先 + 所有 dsh/electron/deepseek 进程及其全部后代;
22
+ - **计算保护**:`mpi|fluent|ansys|^cx\d|solver|cae|launcher` 一律不选(mpiexec 控制台会被误判成普通窗口);
23
+ - **只动裸解释器**;前台窗口与自身进程跳过;白名单永久豁免;
24
+ - 全链路只用 `WM_CLOSE`,**不提供 taskkill /F 强杀**。
25
+
26
+ ### 修复(本轮累计)
27
+ - 关闭空壳时误杀宿主进程树(事故级);
28
+ - 提权关闭四连坑:ESM 模板字面量吞 PS 变量、`-Command` 吞换行、`spawn` 不提权、拿不到提权子进程 stdout;
29
+ - 「取消保护」`var` 闭包导致点哪行都操作最后一行;
30
+ - 关窗后列表不刷新(`loadTaskbar` 丢弃并发请求);
31
+ - 设置页图标不生效(icon id 与 section id 不一致)。
32
+
33
+ ### 文档
34
+ - README(中/英)重写:功能、安全模型、管理员窗口原理、配置表、安装;
35
+ - 新增 `docs/screenshots/` 六张实拍效果图并嵌入 README。
36
+ ## 0.5.6 (2026-11-08) — 去掉面板标题里的盾牌 emoji
37
+
38
+ 用户:「把这个盾牌图标删除」——指的是面板标题「🛡️ 控制台哨兵」前的盾牌 emoji。
39
+ (左侧导航栏的图标已换成自定义终端图标,保留不动。)
40
+
41
+ ### 改动
42
+ - `lib/client.js`:面板标题 `"🛡️ 控制台哨兵"` → `"控制台哨兵"`。
43
+
44
+ ### 生效方式
45
+ 仅改 `lib/client.js` → **Ctrl+Shift+R 硬刷新**。
46
+
47
+ ## 0.5.5 (2026-11-08) — 修复设置页图标仍是默认齿轮(icon id 必须等于 section id)
48
+
49
+ 用户实测:图标没变,仍是齿轮。
50
+
51
+ ### 根因(宿主契约,从 app.asar 里挖出来的原文)
52
+ `@deepseek-ai/dsh-cordis-client-runner/lib/client.js` 里 `settings.section.icon` 槽的 doc 明确写着:
53
+
54
+ > **The registration id must equal the target section's id; unmatched ids never render.**
55
+ > The shell owns the button and label and **keeps its default glyph** when no icon is registered for that id.
56
+
57
+ 0.5.4 我把图标条目 id 写成了 `dsh-console-sentinel-icon`(多了一个后缀),**与分区 id 不匹配 → 永不渲染 → 回退齿轮**。
58
+ 渲染侧也确实是 `renderSlot("settings.section.icon", {...}, { only: row.id, fallback: navIcon(row.id) })`,
59
+ `row.id` 就是分区 id。
60
+
61
+ ### 修法
62
+ 图标注册的 `id` 改为与分区**完全一致**:`"dsh-console-sentinel"`(`order` 同 28)。
63
+
64
+ ### 测试(防止再犯)
65
+ 新增断言:注册后取 `settings.section` 与 `settings.section.icon` 两条记录,断言
66
+ **`icon.cfg.id === section.cfg.id`**;若图标槽没注册也直接失败(`.ok(icon, ...)`)。
67
+ client 6 条、taskbar 8 条全绿。
68
+
69
+ ### 生效方式
70
+ 仅改 `lib/client.js` → **Ctrl+Shift+R 硬刷新**。
71
+
72
+ ## 0.5.4 (2026-11-08) — 自定义设置页图标 + 图标视图设为默认 + 面板可见时自动刷新
73
+
74
+ 用户反馈:①设置页显示的是默认齿轮图标,要换成与插件内容相关的图标 ②已选的图标视图要作为默认
75
+ ③进入插件设置页/点预演后自动刷新,关闭设置界面就停止刷新。
76
+
77
+ ### 改动(仅 `lib/client.js`)
78
+ 1. **注册设置页导航图标**:新增 `SettingsIcon` 组件(终端窗口 + `>_` 提示符,与"控制台哨兵"语义一致),
79
+ 通过 **`settings.section.icon` 槽**注册(`only` 限定本分区)。
80
+ 🔴 根因:宿主 `navIcon(row.id)` 对未知 id 走 **默认齿轮 fallback** —— 不注册该槽就永远显示齿轮。
81
+ 2. **图标网格设为默认视图**:`cache.taskbarView` 默认 `"icon"`;按钮文案相应为「详细视图」。
82
+ 3. **面板可见时才自动刷新**(3s 一次):
83
+ - `panelAlive()` 三重判定:`document.visibilityState==='visible'` + 容器仍在文档中(`document.contains`)
84
+ + `offsetParent` 非空 / 有非零尺寸;
85
+ - 不可见 → `stopAutoRefresh()` 立即清 `setInterval`;
86
+ - 监听 `visibilitychange` / `beforeunload` / `pagehide` 即时停表,重新可见时恢复并立即刷一次;
87
+ - `cache.busy || taskbarLoading` 时跳过该轮,避免打断正在进行的操作。
88
+ 4. 任务栏面板顶部那句长说明压缩成一行小字 + `title` 悬浮提示(含完整规则),释放纵向空间。
89
+
90
+ ### 测试(含一个必须修的坑)
91
+ - 新增「图标网格是默认视图」用例;原详细视图用例改为先 `switchToDetail()`。
92
+ - 🔴 **测试挂起修复**:面板现在会 `setInterval`,而单测用 `new Function` 注入替身——没注入
93
+ `setInterval/clearInterval` 时真实定时器让 **Node 进程永不退出**(实测两次挂死 120s+)。
94
+ 两个测试文件的 `loadBundle` 都改为注入假 interval(只记录不调度)。
95
+
96
+ ### 生效方式
97
+ 仅改 `lib/client.js` → **Ctrl+Shift+R 硬刷新**。
98
+
99
+ ## 0.5.3 (2026-11-08) — 操作区压成一行按钮,说明改悬浮提示
100
+
101
+ 用户反馈:一键清理那块说明文字太多、太占地方,要"一行按钮 + 悬浮提醒"。
102
+
103
+ ### 改动(仅 `lib/client.js`)
104
+ - 删除「🧹 一键清理任务栏窗口」大卡片 + 两行说明文字;
105
+ - 所有动作合并成**一行按钮栏**:`隐藏可见空壳 | 关闭空壳窗口 ┃ 预演清理 | 执行清理`(竖线为视觉分隔);
106
+ 每个按钮的说明改到 `title` **悬浮提示**(鼠标悬停才显示),不占版面。
107
+ - 「预演清理 / 执行清理」保留**预演优先**语义:未预演时「执行清理」仍为禁用态(半透明、不可点)。
108
+ - 预演/执行结果区:**无内容时什么都不渲染**(原来那句提示文字也去掉),有内容才出现,最高 160px 内部滚动。
109
+
110
+ ### 测试
111
+ 6 个测试文件全绿(client / client-taskbar / desktop / logic / tool-output / cascadia)。
112
+
113
+ ### 生效方式
114
+ 仅改 `lib/client.js` → **Ctrl+Shift+R 硬刷新**。
115
+
116
+ ## 0.5.2 (2026-11-08) — 合并刷新按钮 + 状态更紧凑 + 任务栏「仅图标」视图
117
+
118
+ 用户反馈:①顶部「刷新」和「刷新任务栏」像重复 ②顶部状态太占地方,要紧凑 ③任务栏清单想能只显示图标、一排排省空间。
119
+
120
+ ### 改动(仅 `lib/client.js`)
121
+ 1. **合并刷新**:顶部「刷新」改为 `refreshAll()`,一次刷新**状态/计数 + 任务栏窗口列表**;删除「刷新任务栏」按钮。
122
+ 2. **顶部状态更紧凑**:
123
+ - 头部卡片内边距 14→9px;
124
+ - 配置行精简为「扫描 10s · 空闲 60s · 空壳 hide」(去掉"自动动作 none"噪音);
125
+ - 计数从 4 个大卡片改为**一行小圆角药丸**(`控制台 36 · 空壳 11 · 可见空壳 0 · 已隐藏 11`),自适应宽度不再撑满。
126
+ 3. **任务栏「仅图标视图」**:任务栏面板右上加「仅图标视图 / 详细视图」切换按钮。
127
+ - 详细视图(默认):按软件分组的卡片(同前)。
128
+ - **仅图标视图**:把每个窗口压成一个 30×30 图标格,`flex-wrap` **一排排排列**,省空间;
129
+ 悬停显示"进程 — 标题(点击关闭此窗口)";可关闭的可点(管理员走 UAC),白名单/系统保护的置灰不可点。
130
+
131
+ ### 测试
132
+ - 新增「图标视图切换」用例:切到仅图标后无复选框、渲染图标网格、按钮文案翻转。client-taskbar 7 条全绿;其余 5 个测试文件全绿。
133
+
134
+ ### 生效方式
135
+ 仅改 `lib/client.js` → **Ctrl+Shift+R 硬刷新**即可。(若上次 0.5.1 的宿主改动还没生效,则需彻底重启一次。)
136
+
137
+ ## 0.5.1 (2026-11-08) — 关窗即时刷新 + 预演优先的显眼「一键清理」+ 按软件分组美化 + 每窗口独立关闭
138
+
139
+ 用户反馈:①关了窗口列表不立即更新 ②一键清理要有预演且放显眼位置 ③UI 要美观整齐、有逻辑、清晰。
140
+
141
+ ### 关闭方式(澄清 + 保持)
142
+ - 普通窗口关闭 = `WM_CLOSE`,**等同 Alt+F4**(优雅、非强杀)。
143
+ - 管理员窗口关闭 = 同 0.5.0,**提权后仍发 WM_CLOSE**(弹一次 UAC),也是优雅关闭;不再需要先「取消保护」。
144
+ - 只有「强制关闭(管理员)」(taskkill /F) 才是强杀——本版已从面板移除该按钮(日常不需要)。
145
+
146
+ ### 改动
147
+ 1. **关窗后即时更新**(修"关了不刷新"的真因):
148
+ - `loadTaskbar()` 改为**队列式**:加载中再次请求不再被丢弃,完成后再补跑一次。
149
+ - 新增 **`pruneTaskbar()` 乐观删除** + **`afterMutation()` 统一收尾**:关闭动作一返回就先把该窗口/该软件从列表移除并重绘,再后台 `loadStatus()`+`loadTaskbar()` 校准。接入到 关掉该软件 / 单窗口关闭 / 关闭空壳 / 一键清理。
150
+ 2. **任务栏按软件分组**(更整齐、有逻辑):一个程序一张卡——组头 `白名单复选框 + 图标 + 进程(pid) + 徽标(白名单/系统保护/管理员) + [关掉该软件(N)]`,
151
+ 组内每个窗口一行 `• 标题 ······ [关闭]`。
152
+ 3. **每个窗口独立「关闭」按钮**:白名单程序、系统保护程序(宿主/Fluent/MPI)的「关闭」**置灰无效**;管理员窗口点「关闭」走提权优雅关闭(UAC 一次)。
153
+ 4. **一键清理改为显眼卡片 + 预演优先**:任务栏上方独立高亮卡片「🧹 一键清理任务栏窗口」,含 `[预演]` 与 `[执行清理]`;
154
+ **未预演时「执行清理」禁用**,预演结果(图标清单 + 将关闭/保护跳过/管理员窗口数)就在卡片内。工具栏不再堆一堆按钮。
155
+ 5. **简化保护语义**:去掉了「取消保护/恢复保护」按钮——「管理员」只是提示,不再是关闭前置条件;真正挡关闭的只有**白名单**和**系统保护**(宿主/DSH/Fluent/MPI/ansys)。
156
+ 6. 后端:`/window` 支持 `elevated:true` 走 `runElevatedClose`(提权 WM_CLOSE),并按 `process` 名拒绝系统保护程序;`/desktop-cleanup` 响应补 `elevatedCount/elevatedUacCancelled`。
157
+
158
+ ### 测试
159
+ - 重写 `client-taskbar.test.mjs`:分组(5 程序/图标数)、白名单复选框状态与宿主禁用、勾选加白名单、关掉该软件→`/close-process` 正确进程、每窗口「关闭」仅可关程序可用且提权窗发 `elevated:true`、Tab 切换。6 条全绿。
160
+ - client 5 条 + desktop/logic/tool-output/cascadia 全绿。
161
+
162
+ ### 生效方式
163
+ 改动含 `lib/index.js` + `lib/client.js` → **彻底退出 DSH 客户端再启动**。
164
+
165
+ ## 0.5.0 (2026-11-08) — 按软件一键关闭(Alt+F4 批量优雅关闭)+ 一键清理支持提权
166
+
167
+ 用户需求:「加入对某个软件强制关闭的功能,类似对窗口 Alt+F4 但不用挨个点;一键处理能不能用这种方法」。
168
+
169
+ ### 新增
170
+ 1. **「关掉该软件(N)」按钮**(任务栏面板,每个程序只出现一次):
171
+ 点一下 = 对该软件的**全部窗口**逐个发 `WM_CLOSE`(就是"每个窗口按一次 Alt+F4",但一次搞定),
172
+ 优雅关闭而非强杀。提权软件走**一次 UAC** 的提权优雅关闭(不是 taskkill /F)。
173
+ 宿主(DSH/Electron)永远不出现此按钮、服务端也拒绝(`host-protected`)。
174
+ 2. **宿主端 `runElevatedClose(hwnds)`**(`lib/desktop.js`):临时 `.ps1` + `Start-Process -Verb RunAs`
175
+ + 结果文件回传(与 runElevatedKill 同套可靠机制),对被提权的窗口发 `PostMessage(WM_CLOSE)`
176
+ —— 解决"提权窗口收不到非提权 WM_CLOSE(UIPI 丢弃)"的老问题,但是**优雅关闭**不是强杀。
177
+ 3. **新路由 `POST /dsh-console-sentinel/close-process`** `{ process }`:
178
+ `runDesktopProbe` 取该进程全部窗口 → 普通窗口逐个 WM_CLOSE、提权窗口批量提权优雅关闭;
179
+ 返回 `closedCount/total/normal/elevated/uacCancelled`。硬保护:宿主家族、`^dsh|electron|deepseek`。
180
+ 4. **一键清理也支持提权优雅关闭**(回答"一键处理能不能用这种方法"):
181
+ `runDesktopCleanup` 现在把目标拆成普通/提权两组——普通走 WM_CLOSE,提权走一次 UAC 的
182
+ `runElevatedClose`(不再像以前那样对提权窗口静默失败)。
183
+ 5. **Fluent/MPI 硬保护加固**:`selectCleanupTargets` 新增进程名正则兜底,
184
+ `mpi|fluent|ansys|^cx\d|solver|cae|launcher|^dsh|electron|deepseek` 一律不选
185
+ —— 防止"一键清理/一键关闭"误杀正在计算的 Fluent(mpiexec 控制台看起来像普通窗口)。
186
+
187
+ ### 安全性(延续 0.4.4 事故教训)
188
+ - 「关掉该软件」仍受 `hostProcessGuard()` 全树保护;
189
+ - 破坏性动作(close/kill)与"隐藏"分离,且都叠加进程名白名单/黑名单。
190
+
191
+ ### 测试
192
+ - 新增用例:点「关掉该软件」发出 `/close-process` 且带上**正确的进程名**(`iScrRec`)。
193
+ - client-taskbar 7 条 + client 5 条 + desktop/logic 等,6 个文件全绿。
194
+ - `runElevatedClose` 内嵌 PS 脚本实测可编译并把结果 JSON 正确落盘(无效 hwnd → `WasAlive:false`,无动作)。
195
+
196
+ ### 生效方式
197
+ 改动含 `lib/index.js` + `lib/desktop.js`(宿主)+ `lib/client.js` → **彻底退出 DSH 客户端再启动**。
198
+
199
+ ## 0.4.4 (2026-11-08) — 🔴 严重事故修复:关闭空壳把 DSH 客户端一起关掉
200
+
201
+ 用户实测:「关闭空壳窗口」直接把 DSH 客户端也关了。**这是重大安全缺陷,已全面加固。**
202
+
203
+ ### 根因
204
+ `/close-empty` 只按 `row.isEmpty` 过滤就发 `WM_CLOSE`,没有排除 **宿主自身的进程家族**。
205
+ 宿主(Electron / 其下 pwsh、node、conhost)里有的控制台窗口恰好"没有业务子进程",
206
+ 被当成空壳 → `WM_CLOSE` → 触发 `CTRL_CLOSE_EVENT` → 连带把宿主进程树杀掉 → DSH 关闭。
207
+ (实测探针里可见 `pwsh pid=... 祖先链含 DSH NEXT` 这类宿主家族窗口。)
208
+
209
+ **更危险的连带隐患**:`mpiexec`(Fluent 的 MPI)窗口的 `Children` 只有 `conhost`,会被误判"空壳"
210
+ → 一键关闭会**杀死正在计算的 Fluent 求解器**。
211
+
212
+ ### 修法(三重硬保护)
213
+ 1. **`hostProcessGuard()` 宿主全树保护**:算出一个 pid 集合 = 宿主进程自身 + 全部祖先 +
214
+ 所有镜像名像 `dsh/deepseek/electron` 的进程 + **它们的全部后代**。任何批量关闭/隐藏都跳过该集合。
215
+ (对 `process.pid` 所在树做上下遍历 + 按名扩展,覆盖整个 DSH 进程家族,无论插件跑在树的哪个节点。)
216
+ 2. **`source === 'dsh-host'` 一律跳过**(修了之前误写的 `'dshHost'` 字面量 bug)。
217
+ 3. **只关"裸解释器"**:`isSafeClosableShell()` 白名单 = cmd / powershell / pwsh / python,
218
+ 且显式排除 `mpi|fluent|ansys|cx\d|solver|launcher|cae`。`node`(托管服务/中继)、
219
+ `WindowsTerminal`(用户终端)、`mpiexec`(Fluent)全部不在白名单 → 不关。
220
+
221
+ 同样的保护也加到了:
222
+ - `/hide-empty`(隐藏也不再碰宿主/Fluent);
223
+ - `/window`(控制台列表里的**单窗口**隐藏/关闭,对宿主家族返回 `host-protected` 拒绝);
224
+ - `/desktop-cleanup`(一键清理任务栏窗口,把宿主全树 pid 作为 `keepPids` 传入)。
225
+
226
+ ### 实测验证(当前真实窗口)
227
+ - 「关闭空壳」新逻辑**只选 5 个裸 powershell 空壳**;
228
+ - **全部 mpiexec(13+ 个 Fluent MPI)、node 中继、WindowsTerminal、openconsole 均被跳过**;
229
+ - 6 个单元测试全绿。
230
+
231
+ ### 教训(写入 dev-log)
232
+ 🔴 **任何"批量关闭进程/窗口"的动作,必须先排除当前进程的整棵进程树(祖先+后代)**——
233
+ `process.pid` 自身不够,"空壳"启发式会把宿主自己判成空壳。
234
+ 🔴 **破坏性动作(close/kill)绝不能复用"隐藏"的启发式(isEmpty)**——隐藏错了可恢复,关闭错了不可逆。
235
+ 必须叠加**进程名白名单**,宁可少关也不能误杀 Fluent/宿主。
236
+
237
+ ### 生效方式
238
+ 改动含 `lib/index.js`(宿主逻辑)+ `lib/client.js` → **彻底退出 DSH 客户端再启动**。
239
+
240
+ ## 0.4.3 (2026-11-08) — 新增「关闭空壳窗口」按钮 + 改名「一键清理任务栏窗口」
241
+
242
+ 用户反馈:①「没有一键关闭空壳控制窗口按钮」②「一键关闭其他窗口改为 一键清理任务栏窗口」。
243
+
244
+ ### 改动
245
+ 1. **新增「关闭空壳窗口」按钮**(红色,工具栏第二个):真正**结束**空壳控制台进程(`WM_CLOSE`),
246
+ 释放资源,区别于原来的「隐藏可见空壳」(只是隐藏、进程仍在)。
247
+ - 新增宿主路由 `POST /dsh-console-sentinel/close-empty`:对 `ledger.snapshot()` 里所有 `isEmpty`
248
+ 的行发 `runWindowAction(hwnd,'close')`,返回 `closed` 计数,随后 `scan()` 刷新。
249
+ - 客户端 `closeEmpty()`:二次确认后调用,提示「已关闭 N 个空壳窗口」。
250
+ 2. **按钮改名**:「一键关闭其他窗口」→ **「一键清理任务栏窗口」**(确认弹窗文案同步改为"清理任务栏窗口")。
251
+ 3. 工具栏现为:`隐藏可见空壳` / `关闭空壳窗口` / `预演` / `一键清理任务栏窗口` / `刷新任务栏`(一行自动换行)。
252
+
253
+ ### 测试
254
+ - 新增用例:点「关闭空壳窗口」调用 `/close-empty`(而非 `/hide-empty`)。
255
+ - 6 个单元测试文件全绿(client 5 条含新增)。
256
+
257
+ ### 生效方式
258
+ 改动含 `lib/index.js`(新路由)+ `lib/client.js` → **彻底退出 DSH 客户端再启动**。
259
+
260
+ ## 0.4.2 (2026-11-08) — 面板 UI 重做:紧凑 / 不滚太长 / 傻瓜清晰
261
+
262
+ 用户要求:「UI 设计,紧凑简洁、傻瓜清晰明了、页面不要滚动太长」。
263
+
264
+ ### 改版(`lib/client.js` 布局重构,功能与元素 id 全部保留)
265
+ - **单屏三段式**:顶部「标题 + 平台 + 刷新」一行、状态一行 → 细条统计(4 个数字并排,矮)→ **操作按钮全部并到一行**
266
+ (隐藏可见空壳 / 预演 / 一键关闭其他窗口 / 刷新任务栏,自动换行)。
267
+ - **Tab 分页**取代纵向堆叠:`任务栏`(默认)/ `控制台` / `白名单`,一次只显示一个,页面高度大幅缩短。
268
+ 切换是纯 DOM 显隐(`setActiveTab`),不触发重渲染、无抖动。
269
+ - **列表内部滚动限高**:任务栏列表 `max-height:270px`、控制台列表 `220px`,`overflow-y:auto` ——
270
+ 10+ 个窗口也只占固定高度,**整页不再被列表撑长**。
271
+ - 统计数字 20px→16px、卡片内边距收紧、每行留白减少;说明文案压成一行小字放在各 Tab 顶部。
272
+
273
+ ### 测试
274
+ - 新增「tab 切换」用例:默认显示任务栏面板、点击「控制台」Tab 后正确切换显隐。
275
+ - client + client-taskbar 共 **10 条**全绿;desktop/logic/tool-output/cascadia 全绿。
276
+
277
+ ### 生效方式
278
+ 仅改 `lib/client.js`(客户端 bundle)→ **Ctrl+Shift+R 硬刷新**即可(不必整机重启)。
279
+
280
+ ## 0.4.1 (2026-11-08) — 修复「强制关闭(管理员)」根本杀不掉(模块加载/提权/回传三重错)
281
+
282
+ 用户反馈:「还是关不掉啊」。实测复现并定位到 **三个独立缺陷**,全部已修。
283
+
284
+ ### 根因(都有实测证据)
285
+ 1. **多行 PowerShell 经 `-Command` 传参被破坏**(0.4.0 就错的)。
286
+ 0.4.0 用 `spawn('powershell.exe', ['-Command', 多行脚本])`,换行在 shell 层被吃掉、
287
+ 语句粘连 → 存活检测拿不到 PID → **一律报 `already-gone`**。
288
+ 证据:0.4.0 端到端跑 notepad,**337 ms 就"成功"**、`Reason: already-gone`,
289
+ 而 `Get-Process notepad` 显示进程**仍然活着** —— 而且全程没弹 UAC(压根没提权)。
290
+ 2. **`spawn` 根本不触发 UAC**。0.4.0 注释里写的 "`-Verb RunAs`" 只是注释,代码里没有
291
+ `RunAs`,所以子进程继承非提权令牌,`taskkill` 对提权进程只会得到"拒绝访问"。
292
+ 3. **提权子进程的 stdout 拿不到**。即使真提权了,`spawn` 的 stdout 管道也不属于提升后的
293
+ 进程 → 只能靠临时文件回传结果。
294
+
295
+ ### 修法
296
+ - 脚本**写进临时 `.ps1`**(UTF-8 **带 BOM**,否则 PS 5.1 按 GBK 解码中文变乱码),
297
+ 再用 `Start-Process -Verb RunAs -Wait -PassThru` 启动 → **真正弹 UAC**。
298
+ - 结果**通过临时 JSON 文件回传**(`[IO.File]::WriteAllText`,UTF-8 无 BOM),
299
+ 不再依赖 stdout。读不到结果文件 → 明确返回 `uac-denied-or-cancelled` / `no-result-file`,
300
+ **绝不假装成功**。
301
+ - 存活检测从 `Get-Process`(某些会话下对存在进程返回空)换成
302
+ **`tasklist /FI "PID eq N" /FO CSV /NH` + 精确匹配 `"PID"`**。
303
+ - 杀完 `Start-Sleep 250ms` 再复检,只有**确认消失**才报 `killed`,否则 `still-running`。
304
+
305
+ ### 实测(2026-11-08)
306
+ | 用例 | 结果 |
307
+ |---|---|
308
+ | 独立 `.ps1` 跑存活 notepad | `alive-before=True` → `Reason: killed`,进程真消失 |
309
+ | 提权路径杀新建 notepad | `killedCount=1/1`、`SUCCESS: The process with PID 171656 has been terminated`,其他 notepad 不受影响 |
310
+ | 探针识别提权进程 | `Taskmgr(73860)` / `iScrRec(25904)` / `iScrCompressor(115988)` 全部正确标 `[ADMIN]` |
311
+ | 6 个单元测试 | 全绿 |
312
+
313
+ ### 踩坑固化
314
+ - 🔴 **多行 PS 脚本永远不要走 `-Command` 传参**:换行会被 shell 层吞掉。统一「写 `.ps1` 文件 + `-File` 执行」。
315
+ - 🔴 **`spawn` 不会提权**:要 UAC 必须 `Start-Process -Verb RunAs`,且 `spawn` 无法读取提权子进程的 stdout
316
+ → 结果一律走临时文件。
317
+ - 🔴 **PS 5.1 读中文 `.ps1` 必须带 BOM**,否则 GBK 乱码。
318
+ - 🔴 **判断进程存活用 `tasklist` 精确匹配 PID**,不要用 `Get-Process` 的 `$null` 判断。
319
+
320
+ ## 0.4.0 (2026-11-07) — 「强制关闭(管理员)」:突破 UIPI 关掉提权窗口
321
+
322
+ 用户反馈:任务管理器 / iTop Video Compressor / iTop Screen Recorder 三个窗口「怎么都关不掉」。
323
+ 用户明确选择方案 B:「我可以给你权限啊」→ 走 UAC 按需提权,而不是整个宿主以管理员运行。
324
+
325
+ ### 根因(实测,不是插件逻辑错)
326
+ 2026-11-07 用令牌 API 实测三个进程 **全部是管理员提权**,而 DSH 宿主是普通权限:
327
+ `Taskmgr pid=73860`、`iScrCompressor pid=114532`、`iScrRec pid=25904` — 全部 `ElevationType=1`。
328
+ Windows **UIPI(用户界面特权隔离)** 规定低完整性进程不能向高完整性窗口 `PostMessage`,
329
+ 我们的 `WM_CLOSE` 被系统**静默丢弃** → 按钮点了"没反应"。
330
+ 另:Taskmgr / explorer / fluent 还在 `DEFAULT_PROTECTED_PROCESSES` 硬保护名单里,需先「取消保护」。
331
+
332
+ ### 新增
333
+ 1. **提权检测** `lib/desktop.js`:探针 Add-Type `PInvoke.ElevK`(`OpenProcess` + `OpenProcessToken`
334
+ + `GetTokenInformation(TokenElevation)`),每个窗口新增 `Elevated` 字段;探针整体新增 `selfElevated`。
335
+ 2. **按需提权强杀** `runElevatedKill(pids)` / `forceCloseElevated(pids, opts)`:
336
+ 一次批量 `taskkill /F /PID ...`,**运行在提权助手进程里,只弹一次 UAC**;用户点「否」返回
337
+ `all-rejected`/`elevated-helper-failed`,**明确报错而不是假装成功**。绝不回退到非提权 taskkill
338
+ (那只会得到"拒绝访问")。
339
+ 3. **三道安全闸门**(实测全部生效):
340
+ - `dsh-host`:进程名以 `dsh` 开头(DSH NEXT 等)一律拒绝 —— 防止把正在用的宿主杀掉;
341
+ - `self`:拒绝当前进程自身 pid;
342
+ - `not-in-request`:用 `tasklist` 复核每个 pid 的真实进程名,必须落在调用方声明的
343
+ `allowedProcesses` 内,**伪造/过期的 pid 直接失效**。
344
+ 4. **面板** `lib/client.js`:提权行显示「管理员」虚线徽标;对「可关闭 + 未白名单 + 已提权」
345
+ 的行显示 **「强制关闭(管理员)」** 按钮(点击前有确认框说明会弹 UAC、会丢未保存内容)。
346
+ 5. **路由** `GET /dsh-console-sentinel/taskbar-windows` 返回 `selfElevated` 与每行 `elevated`;
347
+ 新增 `POST /dsh-console-sentinel/force-close`。
348
+
349
+ ### 踩坑(已固化)
350
+ - 🔴 **PowerShell 模板字面量陷阱**:ESM 里 `` `...${PID_LIST}...` `` 会被 JS 当变量插值 →
351
+ `ReferenceError: PID_LIST is not defined`(模块直接加载失败)。嵌入 PS 脚本里的占位符**必须写成
352
+ 非 `${}` 形式**,本次改为 `__PID_LIST__` + `.replace()`。
353
+ - 提权助手**必须**真正以 UAC 方式启动,否则继承非提权令牌 → 静默失败 → 又变成"按钮没反应"。
354
+
355
+ ### 测试
356
+ - `tests/client-taskbar.test.mjs` 扩到 5 条:新增「强制关闭(管理员)」只在"可关闭+提权+未白名单"
357
+ 行出现,且点击发送的是**该行 pid**(断言 `pids:[104]` / `allowedProcesses:['iScrRec']`)。
358
+ - 6 个单元测试文件全绿。`forceCloseElevated` 三道闸门实测:宿主→`dsh-host`、自身→`self`、
359
+ 名称不符→`not-in-request`,均 `all-rejected` 且不弹 UAC。
360
+
361
+ ### 生效方式
362
+ 改动含 `lib/index.js`(宿主新路由)+ `lib/client.js` + `lib/package.json` 版本
363
+ → **彻底退出 DSH 客户端再启动**。
364
+
365
+ ## 0.3.2 (2026-11-07) — 修复「取消保护」点了没用(var 闭包 #2)
366
+
367
+ 用户实测反馈:「取消保护取消不掉,重启了(仍不行)」。
368
+
369
+ ### 根因
370
+ `lib/client.js` 任务栏面板里,`取消保护 / 恢复保护` 按钮的 onClick 闭包了函数级 `var proc`。
371
+ `var` 是函数作用域,循环结束时 `proc` 只剩最后一行的进程名 → **无论点哪一行的「取消保护」,
372
+ 实际发送的都是最后一行的进程**。用户点任务管理器,结果把最后一行 `iScrCompressor` 写进了
373
+ `whitelist.unprotect`,Taskmgr / explorer 的保护纹丝不动。文件证据:`~/.dsh/console-sentinel/whitelist.json`
374
+ 的 `unprotect` 里只有 `iScrCompressor`(=最后一行),没有用户真正点的 Taskmgr。
375
+
376
+ ### 修法
377
+ 把 `proc` 作为 IIFE 形参按值绑定(`(function(procName,...){...})(w.process,...)`),每行独立。
378
+ 复选框那条(0.3.1 的闭包 #1)此前已修,本次是同类 bug 的第二处。
379
+
380
+ ### 测试强化
381
+ 原测试只断言 `items.length===1`,恰好放过此 bug。改为 `deepEqual(items,['taskmgr'])`——点第 2 行
382
+ 必须发送 `taskmgr`,而不是最后一行。修复前该断言必红,修复后全绿。
383
+
384
+ ### 教训(写入 dev-log)
385
+ 同一函数内 `var` 声明、又在事件/异步回调里引用的循环变量,一律按值绑定;
386
+ 测试断言必须校验**具体进程名**,不能只校验数量。`node --check` 与人工读码均发现不了此类运行时 bug。
387
+
388
+ ## 0.3.1 (2026-11-07) — 任务栏窗口白名单(图标 + 勾选)· 取消保护 · 预览显示图标
389
+
390
+ 用户需求:「识别任务栏然后从任务栏中勾选出白名单更方便,图标显示」;
391
+ 「预览能不能显示出来要关闭的这些窗口的图标」。
392
+
393
+ ### 新增
394
+ 1. **任务栏窗口白名单面板** `lib/client.js`:新增「任务栏窗口 · 勾选加入白名单」卡片。
395
+ 列出当前任务栏上每个可见应用窗口,逐行显示
396
+ - **真实进程图标**(从 exe 提取的 PNG data URL,18×18);
397
+ - **完整进程名 + pid**(如 `chrome (pid 100)`,可核对到底选的是谁);
398
+ - 窗口标题(截断 90 字);
399
+ - **复选框**:勾选即把该进程名加入豁免白名单(`/whitelist` `{action:add,kind:process}`),
400
+ 取消勾选即移除。受保护进程(explorer/taskmgr/fluent/powershell…)复选框置灰并标「受保护」。
401
+ - 「刷新任务栏」按钮重新枚举。
402
+ 2. **取消保护 / 恢复保护(0.3.1 追加)**:内置保护(`DEFAULT_PROTECTED_PROCESSES`:explorer / taskmgr /
403
+ fluent / launcher1 / powershell 等)不再是死名单。面板每行右侧提供
404
+ - **「取消保护」**按钮:确认后把该进程写入持久 `whitelist.unprotect`(`/whitelist {action:add,kind:unprotect}`),
405
+ 该进程随即变为可被「一键关闭其他窗口」关闭;行内徽标变成「已取消保护」。
406
+ - **「恢复保护」**:从 `unprotect` 移除,重新受保护。
407
+ - **宿主硬保护**:进程名以 `dsh` 开头(如 `DSH NEXT`)的行显示「宿主·不可取消」,按钮不出现,
408
+ 防止把正在使用的 DSH 应用关掉。
409
+ - `/desktop-cleanup` 的实际关闭名单 = `(内置保护 + 配置保护 + 白名单) − unprotect`,
410
+ 与面板显示完全一致(不再只是"看起来取消")。
411
+ 3. **宿主路由** `lib/index.js` 新增 `GET /dsh-console-sentinel/taskbar-windows`:
412
+ 调用 `runDesktopProbe()` 枚举任务栏窗口,附带图标、pid、进程名、标题、是否已白名单、
413
+ `protected / hardProtected / unprotected / hostLocked` 四个保护状态。只读、loopback 校验、优雅降级。
414
+ 4. **图标提取** `lib/desktop.js`:`runDesktopProbe` 的 PowerShell 探针新增
415
+ `Add-Type System.Drawing` + `Get-IconData()`,用 `Icon.ExtractAssociatedIcon(exePath)`
416
+ 转 PNG→base64 data URL,按 exe 路径缓存(同进程多窗口不重复提取);窗口记录新增 `Icon` 字段。
417
+ 受权限限制(如提权的任务管理器)读不到 exe 时返回空串,前端用灰色占位块兜底、不影响布局。
418
+ 5. **清理预览显示图标**:`/desktop-cleanup` 响应新增 `plannedDetailed`(含 icon);
419
+ `renderDesktopResult()` 为每个「将被关闭」的窗口渲染图标 + 文本。
420
+
421
+ ### 修复
422
+ - **var 闭包陷阱 #1(复选框)**(`tests/client-taskbar.test.mjs` 抓到):复选框 change 监听器最初把循环变量
423
+ `cb` 直接闭包进 IIFE,导致所有行都读到「最后一个复选框」的 checked(勾 chrome 却发 remove)。
424
+ 改为把 `box` 作为 IIFE 形参传入。
425
+ - **var 闭包陷阱 #2(取消保护按钮,2026-11-07 用户实测"取消不掉")**:`取消保护` 按钮的 onClick 闭包了
426
+ 函数级 `var proc`,导致**每一行的按钮都发送最后一行进程名**——点任务管理器的「取消保护」实际把最后一行
427
+ `iScrCompressor` 写进了 unprotect,Taskmgr/explorer 纹丝不动。改为把 `proc` 作 IIFE 形参按值绑定。
428
+ **教训**:同一函数里用 `var` 声明、又在异步/事件回调里引用的循环变量,全部要按值绑定;测试断言必须校验
429
+ **具体进程名**(`deepEqual(items,['taskmgr'])`),只断言 `items.length===1` 会放过这个 bug。
430
+ `node --check` 与人工读码都发现不了——两次都是靠 DOM-stub 测试(且第二次靠强化了的断言)才暴露。
431
+
432
+ ### 测试
433
+ - 新增 `tests/client-taskbar.test.mjs`(3 条):图标+复选框逐行渲染 / checked 反映白名单且受保护行置灰 /
434
+ 勾选触发 `/whitelist` add(items=[进程名])。连原 `client.test.mjs` 共 7 条客户端测试全绿;
435
+ `desktop.test.mjs` + `logic.test.mjs` 29 条全绿。
436
+
437
+ ### 生效方式
438
+ - 改动了 `lib/index.js`(宿主)+ `lib/client.js`(客户端 bundle)→ **彻底退出 DSH 客户端再启动**(一次全覆盖)。
2
439
 
3
440
  ## 0.3.0 (2026-11-03) — 设置面板 UI(任务栏窗口清理 / 白名单 / 窗口状态)
4
441