dsh-mini 0.0.0-stage → 0.1.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/README.md +230 -2
- package/app/HudWindow.cs +1278 -0
- package/app/RegionCapture.cs +552 -0
- package/app/SelectionWatcher.cs +850 -0
- package/app/Service.cs +412 -0
- package/app/app.manifest +22 -0
- package/app/bin/DSH/345/260/217/347/252/227.exe +0 -0
- package/app/bin/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/bin/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/bin/WebView2Loader.dll +0 -0
- package/app/build-app.ps1 +112 -0
- package/app/dev.ps1 +63 -0
- package/app/hud-page.g.cs +164 -0
- package/app/hud-page.js +157 -0
- package/app/probe/clear-composer.js +8 -0
- package/app/probe/dom-probe.js +40 -0
- package/app/probe/drag.ps1 +35 -0
- package/app/probe/dump-composer.js +5 -0
- package/app/probe/frame-probe.js +21 -0
- package/app/probe/selection-sample.txt +4 -0
- package/app/probe/top-probe.js +39 -0
- package/app/vendor/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/vendor/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/vendor/WebView2Loader.dll +0 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +622 -0
- package/lib/index.js +521 -0
- package/package.json +49 -4
- package/scripts/hud-shot.ps1 +168 -0
- package/scripts/native-status.ps1 +63 -0
- package/scripts/win-hud.ps1 +264 -0
- package//345/274/200/345/217/221/346/226/271/346/241/210-WebView2/345/216/237/347/224/237/345/260/217/347/252/227.md +216 -0
package/README.md
CHANGED
|
@@ -1,3 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-mini
|
|
2
|
+
|
|
3
|
+
把当前会话一键弹出成一个**独立置顶小窗**,小窗里有整屏截图和框选截图两个键(截图时小窗自己不进入画面,图片直接插进输入框);在任意程序里选中文字,还会就地弹出「问大肥鱼」小药丸,点一下把小窗叫出来并引用那段话。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
主窗 DSH DSH 小窗(WebView2 原生窗口,置顶贴右)
|
|
7
|
+
┌──────────────────────────────┐ ┌───────────────┐
|
|
8
|
+
│ 会话标题 ▯ ← 弹出 │ ───────▶ │ DSH 小窗 ✕📌↔📷│
|
|
9
|
+
│ │ │ 会话标题 ☰ ⊕ │
|
|
10
|
+
│ 对话 … │ │ 对话 … │
|
|
11
|
+
│ �� │ │
|
|
12
|
+
│ [+] … [发送] │ │ [+] … [发送] │
|
|
13
|
+
└──────────────────────────────┘ └───────────────┘
|
|
14
|
+
无边框自绘标题条、可拖拽、可缩放、置顶
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 它怎么工作
|
|
18
|
+
|
|
19
|
+
三个部分:
|
|
20
|
+
|
|
21
|
+
1. **宿主半身**(`lib/index.js`,跑在 DSH 的 Node 进程里):注册 `/dsh-hud/*` 路由,
|
|
22
|
+
负责拼地址(`webApp.getAdvertisedUrl()` + `connection.authenticatedUrl()`)、起窗口、转发截图。
|
|
23
|
+
2. **浏览器半身**(`lib/client.js`,跑在页面里):主窗只注册一个「弹出此窗口」按钮;
|
|
24
|
+
小窗里由原生壳的标题条负责交互,所以不再重复渲染按钮。
|
|
25
|
+
另外把宿主给的 `session` 参数/converted cookie 变成 `uiWorkspace.openSession(sessionId)`,让小窗直接落在当前会话上。
|
|
26
|
+
3. **原生窗口壳**(`app/HudWindow.cs` → `app/bin/DSH小窗.exe`):WinForms + WebView2,
|
|
27
|
+
自绘标题条(`✕` 关闭、`📌` 置顶、`↔` 回主窗、`📷` 截图),无边框、可按边缘拖拽缩放、系统托盘。
|
|
28
|
+
|
|
29
|
+
### 为什么不直接 `window.open`
|
|
30
|
+
|
|
31
|
+
DSH 桌面主窗的 `setWindowOpenHandler` 对 http/https 一律 `shell.openExternal` 并 `deny`,
|
|
32
|
+
拿不到第二个 `BrowserWindow`;宿主进程又是 Electron main 的子进程,没有 Electron API。
|
|
33
|
+
所以窗口壳只能自己造:WebView2 复用系统自带的 Chromium,不必打包一整个 Electron。
|
|
34
|
+
|
|
35
|
+
### 401 与 token
|
|
36
|
+
|
|
37
|
+
小窗是**独立配置档**,没有主窗 cookie,裸地址会被回 `dsh web authentication required`。
|
|
38
|
+
所以 `/open` 用 `connection.authenticatedUrl()` 取进程 token;小窗打开后 GUI 会 302 到干净 URL
|
|
39
|
+
(query 全丢),因此壳在导航前写入两个 cookie:`dsh_hud=1`(小窗标志)和
|
|
40
|
+
`dsh_hud_session=<会话 id>`(要落哪个会话),页面侧据此恢复。
|
|
41
|
+
|
|
42
|
+
### 截图(两个键)
|
|
43
|
+
|
|
44
|
+
两个键都在原生壳内完成,不再调 PowerShell,小窗自己不会出现在画面里:隐藏自己 → 抓屏 →
|
|
45
|
+
PNG → base64 → `postMessage` 给页面 → 走 Lexical 粘贴事件塞进输入框。
|
|
46
|
+
|
|
47
|
+
| 键 | 行为 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `📷` 截整屏 | 一次截下整个虚拟桌面,直接进输入框 |
|
|
50
|
+
| `⧉` 框选 | 冻结当前桌面 → 全屏遮罩里**移动鼠标即自动吸附到光标下那个窗口**(带窗口标题和尺寸标签),**单击**就截这个窗口;**拖拽**则自由框选一块矩形;`Esc` / 右键取消 |
|
|
51
|
+
|
|
52
|
+
框选的实现要点(`app/RegionCapture.cs`):
|
|
53
|
+
|
|
54
|
+
- 遮罩自己盖住整屏,所以**不能用 `WindowFromPoint`**(它只会返回遮罩自己)。正确顺序是:
|
|
55
|
+
先把屏幕冻结成位图,再在遮罩出现**之前**用 `EnumWindows` 快照窗口列表(顶层在前,
|
|
56
|
+
跳过自身进程 / `WS_EX_TOOLWINDOW` / 空标题 / `DWMWA_CLOAKED` 幽灵窗 / 任务栏与桌面),
|
|
57
|
+
然后按 z 序取「第一个包含光标的矩形」——等于遮罩不存在时 `WindowFromPoint` 的答案。
|
|
58
|
+
- 窗口矩形用 `DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)` 取,去掉 DWM 给最大化窗口
|
|
59
|
+
加的那圈隐形边框,截出来的图和眼睛看到的一致。
|
|
60
|
+
- **单位必须统一到物理像素**:本进程是 per-monitor v2,而 `SystemInformation.VirtualScreen`
|
|
61
|
+
走 `GetSystemMetrics`,会按**线程**的 DPI 上下文作答,在 150% 缩放下返回逻辑尺寸
|
|
62
|
+
(1707x1067 而不是 2560x1600),遮罩就只盖住屏幕左上一块。所以虚拟屏改用
|
|
63
|
+
`EnumDisplayMonitors` + `GetMonitorInfo` 的 `rcMonitor` 并集(永远是物理像素),
|
|
64
|
+
遮罩窗口再用 `SetWindowPos` 按物理坐标钉住;选区、候选窗口、光标一律存物理坐标,
|
|
65
|
+
只在绘制时用 `PointToClient` 换算一次。
|
|
66
|
+
|
|
67
|
+
### 配色与缩放(v0.1.x 两处关键修法)
|
|
68
|
+
|
|
69
|
+
- **边框跟着页面主题走**:导航完成后(400ms / 1800ms 各一次)读出
|
|
70
|
+
`getComputedStyle(document.body).backgroundColor` 和 `color`,解析成 RGB 后
|
|
71
|
+
`ApplyChrome(bg, fg)`:窗体底色 = 页面底色、标题条 = 底色混 4% 前景色、图标色 = 前景色混底色、
|
|
72
|
+
再按亮度决定要不要 `DWMWA_USE_IMMERSIVE_DARK_MODE`。深色主题下自动变深,浅色下自动变浅,
|
|
73
|
+
不会出现「浅色页面套黑框」。
|
|
74
|
+
- **不要用 CSS `body{zoom}` 缩小页面**:它只缩「已用高度」,视口没变,右/底会留空白带。
|
|
75
|
+
正确做法是 WebView2 原生 `web.ZoomFactor = 0.9`——布局视口会变成 `460/0.9≈511` CSS px,
|
|
76
|
+
页面四周都填满。
|
|
77
|
+
|
|
78
|
+
## 界面上的东西
|
|
79
|
+
|
|
80
|
+
| 位置 | 主窗 | 小窗标题条(从左到右) |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| 按钮 | 会话标题栏右侧 `▯` 弹出此窗口 | `✕` 关闭、`📌` 置顶、`↔` 回主窗、`📷` 截整屏、`⧉` 框选 |
|
|
83
|
+
|
|
84
|
+
小窗打开的是**当前会话**;再点一次「弹出此窗口」不会开第二个窗口,只是把已有窗口提到前面
|
|
85
|
+
(带会话参数时才重新导航)。
|
|
86
|
+
|
|
87
|
+
## 安装(本机已做)
|
|
88
|
+
|
|
89
|
+
profile 走 `link:` 依赖,和 `tju-plugin` 一样:
|
|
90
|
+
|
|
91
|
+
```jsonc
|
|
92
|
+
// ~/.dsh/profiles/desktop/package.json
|
|
93
|
+
"dependencies": { "dsh-mini": "link:C:/Users/xiaoxin3309/Documents/deepseek-harness/default-workspace/dsh-hud-window" },
|
|
94
|
+
"dsh": { "profile": { "bundles": [ ..., "dsh-mini" ] } }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`node_modules/dsh-mini` 是指向本目录的 junction,改代码不需要重新安装。
|
|
98
|
+
宿主半身(`lib/index.js`)的改动需要 `plugin_manager set_bundle` 关一次再开一次(或重启 DSH);
|
|
99
|
+
浏览器半身(`lib/client.js`)的改动需要**刷新页面**(`F5`)——bundle 的 `rev` 由内容算出,刷新即取新版。
|
|
100
|
+
|
|
101
|
+
## 配置
|
|
102
|
+
|
|
103
|
+
`cordis.patch.yml`:
|
|
104
|
+
|
|
105
|
+
| 键 | 默认 | 说明 |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `titlePrefix` | `DSH 小窗` | 窗口标题(也是浏览器回退模式下找窗口的依据) |
|
|
108
|
+
| `width` / `height` | 460 / 560 | 小窗初始尺寸(逻辑像素),超出工作区自动夹紧 |
|
|
109
|
+
| `browserPath` | 空 | 只在没有 `app/bin/DSH小窗.exe` 的回退模式下使用 |
|
|
110
|
+
|
|
111
|
+
## 路由
|
|
112
|
+
|
|
113
|
+
| 路由 | 方法 | 作用 |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `/dsh-hud/open` | POST | `{sessionId?}` → 起小窗(`backend:"native"`)或复用已有窗口 |
|
|
116
|
+
| `/dsh-hud/close` | POST | 关掉小窗 |
|
|
117
|
+
| `/dsh-hud/focus` | POST | 重新置顶 |
|
|
118
|
+
| `/dsh-hud/shot` | POST | 截全屏(不含小窗),回 dataURL |
|
|
119
|
+
| `/dsh-hud/state` | GET | 小窗是否在跑 |
|
|
120
|
+
| `/dsh-hud/trace` | GET | 诊断信标:把 `step/detail` 追加到 `%LOCALAPPDATA%\dsh-hud\trace.log` |
|
|
121
|
+
|
|
122
|
+
## 构建原生壳
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
cd app
|
|
126
|
+
./build-app.ps1 # 从 %TEMP%\wv2 或 NuGet 取 WebView2 依赖 → csc 编译 → 产物在 app/bin/
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
只用到 .NET Framework 自带的 `csc.exe`(C# 5 语法,别写 `$"..."`、`?.`、`nameof`)。
|
|
130
|
+
**编译前先关掉正在跑的小窗**,否则 `csc` 会报 `CS0016 未能写入输出文件 … 另一个程序正在使用此文件`。
|
|
131
|
+
|
|
132
|
+
## 调试
|
|
133
|
+
|
|
134
|
+
* `%LOCALAPPDATA%\dsh-hud\hud.log`:壳自己的日志(导航、eval 结果、异常)。
|
|
135
|
+
* `%LOCALAPPDATA%\dsh-hud\trace.log`:页面侧信标(`apply` / `click` / `opened` / `open-failed`)。
|
|
136
|
+
页面没有 console 通道,这是唯一能从外部看到页面行为的办法。
|
|
137
|
+
* `app/dev.ps1 -Build -Url <带 token 的地址> [-Eval probe.js]`:杀旧进程 → 构建 → 启动 → 跑一段
|
|
138
|
+
JS 到页面里 → 打印 `hud.log`。`app/probe/*.js` 是现成的探测脚本。
|
|
139
|
+
* 页面里跑 eval 的办法:把 JS 写进 `%LOCALAPPDATA%\dsh-hud\eval.js`,壳 700ms 内会执行并把结果写进 `hud.log`。
|
|
140
|
+
|
|
141
|
+
## 划词问大肥鱼
|
|
142
|
+
|
|
143
|
+
在**任何程序**里用鼠标选中一段文字,松开后光标右下方会弹出蓝色小药丸「问大肥鱼」;点它,
|
|
144
|
+
小窗自动弹到最前,选中的内容以引用块(每行 `> ` 前缀)插进输入框,直接接着提问即可。
|
|
145
|
+
|
|
146
|
+
* 实现在 `app/SelectionWatcher.cs`:全局鼠标钩子(90ms 轮询)→ 松开时若拖动距离 ≥6 逻辑像素、
|
|
147
|
+
且不在小窗/药丸自己身上 → 后台线程等 130ms(双击选词等 320ms)→ 校验仍在前台 → 取选区。
|
|
148
|
+
* Windows 没有「给我当前选区」的 API,所以取选区是:备份剪贴板 → 发一次 `Ctrl+C` →
|
|
149
|
+
轮询 `GetClipboardSequenceNumber()` 变化(最多 900ms)→ 读文本 → **还原剪贴板**。
|
|
150
|
+
用序列号而不是「文本变了没」,重复选同一段也能识别。
|
|
151
|
+
* 三处保护:终端窗口(console/cascadia/mintty/putty)**不发** `Ctrl+C`(会变 SIGINT);
|
|
152
|
+
资源管理器/桌面/任务栏(Progman/WorkerW/CabinetWClass…)直接跳过(否则会真的复制文件、
|
|
153
|
+
污染 `Win+V` 剪贴板历史);用户已切窗就不发。
|
|
154
|
+
* 药丸 9 秒无操作自动消失;托盘菜单「划词问大肥鱼」可随时关掉,状态存在
|
|
155
|
+
`%LOCALAPPDATA%\dsh-hud\ask.txt`。
|
|
156
|
+
* 因此**标题条 ✕ 只是收进托盘**(不是退出)——watcher 活在这个进程里,退出了就没法划词。
|
|
157
|
+
彻底退出用托盘菜单「退出」。
|
|
158
|
+
* 诊断:`hud.log` 里 `[pill]` 开头的行会写每次 `kick / skip / read N chars`;
|
|
159
|
+
往 `%LOCALAPPDATA%\dsh-hud\seltest.txt` 写任意内容可触发一次「只读不弹」的自检。
|
|
160
|
+
|
|
161
|
+
## 常驻划词服务、全局快捷键与设置页
|
|
162
|
+
|
|
163
|
+
小窗关着也要能划词,所以插件会拉起一个常驻进程(同一个 exe 的 `--service` 模式):
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
DSH小窗.exe --service --url <tokenized> # Mutex Local\DshHudService,单实例
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
* 托盘图标「DSH 划词服务」:双击开小窗;菜单可开关划词、退出。
|
|
170
|
+
* 划词命中后药丸出现在**选中内容最后一个元素的右下方**(`x = sel.Right + 10`、`y = sel.Bottom + 8`)—— 落在最后一行下面、右边,不会盖住你刚划出来的字。右边顶到屏幕边缘就整体左移到选区右端对齐(仍留在下方);下方也放不下时翻到最后一行的上方;最后统一 clamp 到 `GetMonitorInfo().rcWork`,**任何情况下都不会跑出屏幕**。
|
|
171
|
+
* 关掉小窗(✕)只是收进托盘 —— 服务死在同一个进程里,退出就再没有划词。
|
|
172
|
+
|
|
173
|
+
### 全局快捷键
|
|
174
|
+
|
|
175
|
+
服务用 `RegisterHotKey` 注册两个组合,触发后写 `cmd.txt` 并唤起小窗执行:
|
|
176
|
+
|
|
177
|
+
| 默认 | 动作 |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `Ctrl+Shift+1` | 整屏截图(`DoScreenshot`) |
|
|
180
|
+
| `Ctrl+Shift+2` | 框选截图(`DoRegionCapture`) |
|
|
181
|
+
|
|
182
|
+
> 默认值刻意避开 `Ctrl+Alt+1/2`:这台机器上 `Ctrl+Alt+1`、`Ctrl+Alt+2`、`Ctrl+Alt+A/X`
|
|
183
|
+
> 都被别的程序占了(`RegisterHotKey` 直接返回 false,日志里是 `hotkey full busy: …`)。
|
|
184
|
+
> 想换组合在设置页里改,用 `Ctrl/Alt/Shift/Win` + 字母/数字/F1–F24/PrintScreen 等写法。
|
|
185
|
+
|
|
186
|
+
### 设置页
|
|
187
|
+
|
|
188
|
+
主窗「设置 → DSH 小窗」:
|
|
189
|
+
|
|
190
|
+
* 截图自动复制到剪贴板(默认开,整屏/框选都复制);
|
|
191
|
+
* 全屏截图快捷键、框选截图快捷键(失焦即保存)。
|
|
192
|
+
|
|
193
|
+
配置落在 `%LOCALAPPDATA%\dsh-hud\config.ini`(`key=value`,C# 壳、宿主插件、客户端三边都读),
|
|
194
|
+
服务每 2.5 秒看一次文件时间戳,改完约 3 秒内生效并重新注册热键。
|
|
195
|
+
|
|
196
|
+
## 打包与分发
|
|
197
|
+
|
|
198
|
+
```powershell
|
|
199
|
+
cd app; ./build-app.ps1 # 先把窗口壳编出来(app/bin/*.exe)
|
|
200
|
+
cd ..; npm pack # → dsh-mini-0.1.0.tgz(约 600 KB)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
* 包里**已经带上** `app/bin/` 的 `DSH小窗.exe` 和三个 WebView2 dll,所以装完不用再编译。
|
|
204
|
+
* 装到另一个 profile / 另一台机器:
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
// ~/.dsh/profiles/<profile>/package.json
|
|
208
|
+
"dependencies": { "dsh-mini": "file:D:/path/to/dsh-mini-0.1.0.tgz" },
|
|
209
|
+
"dsh": { "profile": { "bundles": [ … , "dsh-mini" ] } }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
然后重启 DSH(宿主插件只在启动时加载;客户端 bundle 刷新页面即可)。
|
|
213
|
+
* `app/build-app.ps1` 自己会解决 WebView2 依赖:先找 `app/vendor/`,再找 `%TEMP%\wv2`,
|
|
214
|
+
都没有就从 NuGet 下 `Microsoft.Web.WebView2 1.0.4314.35` 并解压 —— 空机器上也能直接编(实测 28 秒)。
|
|
215
|
+
* exe 文件名是用码点拼的(`'DSH' + [char]0x5C0F + [char]0x7A97`),故意不写中文字面量:
|
|
216
|
+
这个脚本一旦丢掉 BOM,powershell 5.1 会按 GBK 解析,字面量就会变成 `DSH灏忕獥.exe`(踩过两次)。
|
|
217
|
+
|
|
218
|
+
## 已知边界
|
|
219
|
+
|
|
220
|
+
* 仅 Windows;依赖系统 WebView2 Runtime(Win10/11 一般自带)。
|
|
221
|
+
* 小窗是**独立的浏览器上下文**:主窗的输入框草稿、滚动位置不会带过去,但会话内容通过宿主读得到。
|
|
222
|
+
* 截图只有「整屏」和「框选/吸附窗口」两种,没有滚动长截图;框选吸附依赖窗口标题非空,
|
|
223
|
+
无标题窗口(部分全屏游戏、自绘界面)会退化成「该点所在的整块屏幕」。
|
|
224
|
+
(滚动长截图评估过:要拼接滚动帧 + 去重 + 处理懒加载,工作量明显大于本轮其它几项,先没做。)
|
|
225
|
+
* 划词优先走 UI Automation `TextPattern.GetSelection()`(无侵入、能拿到选区矩形),
|
|
226
|
+
只有 UIA 读不到时才回退「发 Ctrl+C 读剪贴板」;终端(console/cascadia/mintty/putty)
|
|
227
|
+
和桌面/资源管理器窗口直接跳过,避免 Ctrl+C 变成 SIGINT 或真的复制文件。
|
|
228
|
+
* 主题色与小窗边框:导航完成后同步两次,另外每 6 秒复查一次页面背景色;
|
|
229
|
+
跨显示器拖到不同 DPI 的屏会收到 `WM_DPICHANGED` 并重算标题条/图标/最小尺寸。
|
|
230
|
+
* 标题条图标是 `Segoe MDL2 Assets` 字体,字号偏小(E8BB 关闭键取 8.5f 才和其它图标一样大)。
|
|
2
231
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|