dsh-mini
Windows only (needs the Edge WebView2 Runtime): pop the current session into an always-on-top WebView2 mini window with a self-drawn title bar, free resizing and a jump back to the main window; adds full-screen and region screenshots that paste straight into the composer, a "ask about selection" pil
Install
dsh plugin --profile web add dsh-mini
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-mini


仓库 https://github.com/xiaoxin3309/dsh-mini | npm 包名 dsh-mini
把当前会话一键弹出成一个独立置顶小窗,小窗里有整屏截图和框选截图两个键(截图时小窗自己不进入画面,图片直接插进输入框);在任意程序里选中文字,还会就地弹出「问大肥鱼」小药丸,点一下把小窗叫出来并引用那段话。
主窗 DSH DSH 小窗(WebView2 原生窗口,置顶贴右)
┌──────────────────────────────┐ ┌───────────────┐
│ 会话标题 ▯ ← 弹出 │ ───────▶ │ DSH 小窗 ✕📌↔📷│
│ │ │ 会话标题 ☰ ⊕ │
│ 对话 … │ │ 对话 … │
│ �� │ │
│ [+] … [发送] │ │ [+] … [发送] │
└──────────────────────────────┘ └───────────────┘
无边框自绘标题条、可拖拽、可缩放、置顶
它怎么工作
三个部分:
1. 宿主半身(lib/index.js,跑在 DSH 的 Node 进程里):注册 /dsh-mini/* 路由,
负责拼地址(webApp.getAdvertisedUrl() + connection.authenticatedUrl())、起窗口、转发截图。
2. 浏览器半身(lib/client.js,跑在页面里):主窗只注册一个「弹出此窗口」按钮;
小窗里由原生壳的标题条负责交互,所以不再重复渲染按钮。
另外把宿主给的 session 参数/converted cookie 变成 uiWorkspace.openSession(sessionId),让小窗直接落在当前会话上。
3. 原生窗口壳(app/HudWindow.cs → app/bin/DSH小窗.exe):WinForms + WebView2,
自绘标题条(✕ 关闭、📌 置顶、↔ 回主窗、📷 截图),无边框、可按边缘拖拽缩放、系统托盘。
为什么不直接 window.open
DSH 桌面主窗的 setWindowOpenHandler 对 http/https 一律 shell.openExternal 并 deny,
拿不到第二个 BrowserWindow;宿主进程又是 Electron main 的子进程,没有 Electron API。
所以窗口壳只能自己造:WebView2 复用系统自带的 Chromium,不必打包一整个 Electron。
401 与 token
小窗是独立配置档,没有主窗 cookie,裸地址会被回 dsh web authentication required。
所以 /open 用 connection.authenticatedUrl() 取进程 token;小窗打开后 GUI 会 302 到干净 URL
(query 全丢),因此壳在导航前写入两个 cookie:dsh_hud=1(小窗标志)和
dsh_hud_session=<会话 id>(要落哪个会话),页面侧据此恢复。
截图(两个键)
两个键都在原生壳内完成,不再调 PowerShell,小窗自己不会出现在画面里:隐藏自己 → 抓屏 →
PNG → base64 → postMessage 给页面 → 走 Lexical 粘贴事件塞进输入框。
键 · 行为
📷 截整屏 · 一次截下整个虚拟桌面,直接进输入框
⧉ 框选 · 冻结当前桌面 → 全屏遮罩里移动鼠标即自动吸附到光标下那个窗口(带窗口标题和尺寸标签),单击就截这个窗口;拖拽则自由框选一块矩形;Esc / 右键取消
框选的实现要点(app/RegionCapture.cs):
- 遮罩自己盖住整屏,所以不能用
WindowFromPoint(它只会返回遮罩自己)。正确顺序是:
先把屏幕冻结成位图,再在遮罩出现之前用 EnumWindows 快照窗口列表(顶层在前,
跳过自身进程 / WS_EX_TOOLWINDOW / 空标题 / DWMWA_CLOAKED 幽灵窗 / 任务栏与桌面),
然后按 z 序取「第一个包含光标的矩形」——等于遮罩不存在时 WindowFromPoint 的答案。
- 窗口矩形用
DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)取,去掉 DWM 给最大化窗口
加的那圈隐形边框,截出来的图和眼睛看到的一致。
- 单位必须统一到物理像素:本进程是 per-monitor v2,而
SystemInformation.VirtualScreen
走 GetSystemMetrics,会按线程的 DPI 上下文作答,在 150% 缩放下返回逻辑尺寸
(1707x1067 而不是 2560x1600),遮罩就只盖住屏幕左上一块。所以虚拟屏改用
EnumDisplayMonitors + GetMonitorInfo 的 rcMonitor 并集(永远是物理像素),
遮罩窗口再用 SetWindowPos 按物理坐标钉住;选区、候选窗口、光标一律存物理坐标,
只在绘制时用 PointToClient 换算一次。
配色与缩放(v0.1.x 两处关键修法)
- 边框跟着页面主题走:导航完成后(400ms / 1800ms 各一次)读出
getComputedStyle(document.body).backgroundColor 和 color,解析成 RGB 后
ApplyChrome(bg, fg):窗体底色 = 页面底色、标题条 = 底色混 4% 前景色、图标色 = 前景色混底色、
再按亮度决定要不要 DWMWA_USE_IMMERSIVE_DARK_MODE。深色主题下自动变深,浅色下自动变浅,
不会出现「浅色页面套黑框」。
- 不要用 CSS
body{zoom}缩小页面:它只缩「已用高度」,视口没变,右/底会留空白带。
正确做法是 WebView2 原生 web.ZoomFactor = 0.9——布局视口会变成 460/0.9≈511 CSS px,
页面四周都填满。
界面上的东西
位置 · 主窗 · 小窗标题条(从左到右)
按钮 · 会话标题栏右侧 ▯ 弹出此窗口 · ✕ 关闭、📌 置顶、↔ 回主窗、📷 截整屏、⧉ 框选
小窗打开的是当前会话;再点一次「弹出此窗口」不会开第二个窗口,只是把已有窗口提到前面
(带会话参数时才重新导航)。
系统要求
跑起来需要的东西,除了第一行都必须由机器自备(dsh-mini 只带插件代码 + app/bin 里那 4 个文件):
依赖 · 谁提供 · 说明
node ≥ 20 + DSH ≥ 0.2.0-rc.1 · DSH 本体 · dsh.compatibility 声明的范围
Windows 10 1903+ / 11,x64 · 系统 · 原生壳按 /platform:x64 编译,WebView2Loader.dll 只有 win-x64;package.json 有 "os": ["win32"]
.NET Framework 4.x(4.7.2 以上) · 系统自带 · 原生壳是 WinForms + csc 编的 .NET Framework exe;UIAutomationClient/UIAutomationTypes/WindowsBase 都用 GAC 里的系统版本,不用另外装
Microsoft Edge WebView2 Runtime · 需要自备 · 小窗就是 WebView2 窗口,没有它开不出画面。Win11 通常自带;Win10 多数机器随 Edge 已装。缺的话原生壳会弹窗给出官方下载地址(Evergreen 包),装完重开小窗即可。包内不含 runtime(几百 MB,且应走系统更新)
Windows PowerShell 5.1 · 系统自带 · 只在浏览器回退模式(backend:"edge")和 scripts/native-status.ps1 查进程时用;原生模式下截图/划词全是 C#
剪贴板、全局热键 · 用户权限 · 划词读选区用的 UIA/Ctrl+C 走当前用户桌面;管理员窗口(UIPI)里的选区读不到,这是 Windows 的限制
运行时数据在 <插件目录>\app\bin\data\(config.ini、window.txt、hud.log、request.txt、
WebView2 用户数据),和 exe 放在一起、按用户隔离。之所以不再放 %LOCALAPPDATA%\dsh-hud\:
宿主进程树下派生的子进程可能带着受限令牌,连用户目录都写不了(0x80070005),
而 exe 自己的目录一定可写;数据一旦落在用户目录,小窗连日志都写不出来。
config.ini 首次运行时会从旧位置自动搬过来,老用户不用手工迁移。
安装
A. 从 npm 装(别人用这个)
cd ~/.dsh/profiles/desktop npm install dsh-mini
然后在同一个 package.json 的 dsh.profile.bundles 末尾加一项 "dsh-mini",重启 DSH(或 plugin_manager set_bundle 关一次再开一次)。
装完不需要额外配置:插件会在 DSH 启动 2.5 秒后自己拉起常驻服务(划词药丸 + 全局热键 + 托盘),小窗由标题栏的「弹出此窗口」按需打开。前提是机器上已有 WebView2 Runtime(见上面「系统要求」)。
exe 会被自动复制到工作区之外:第一次用到时,插件把 app/bin/ 里的 exe 与三个 dll
拷到%LOCALAPPDATA%\dsh-mini\bin\,之后一切都从那里跑(数据目录是同级bin\data\)。
原因见下面「为什么不能就地运行」——DSH 的工作区沙箱会按镜像路径限制位于工作区里的可执行文件,
就地跑会出现「写不了 AppData、读不到鼠标、WebView2 拒绝访问」这一串怪现象。
B. 源码 link(改代码时用)
改代码不必每次 npm pack,把 profile 里的依赖指向你本地这份仓库即可:
// ~/.dsh/profiles/desktop/package.json
"dependencies": { "dsh-mini": "link:<本仓库在你机器上的绝对路径>" },
"dsh": { "profile": { "bundles": [ ..., "dsh-mini" ] } }
把 <本仓库在你机器上的绝对路径> 换成你 clone 下来的那个目录(例如 C:/code/dsh-mini),
然后 npm install 让 npm 建好 node_modules/dsh-mini 这层链接;之后改代码不需要重新安装。
宿主半身(lib/index.js)的改动需要 plugin_manager set_bundle 关一次再开一次(或重启 DSH);
浏览器半身(lib/client.js)的改动需要刷新页面(F5)——bundle 的 rev 由内容算出,刷新即取新版。
配置
cordis.patch.yml:
键 · 默认 · 说明
titlePrefix · DSH 小窗 · 窗口标题(也是浏览器回退模式下找窗口的依据)
width / height · 460 / 560 · 小窗初始尺寸(逻辑像素),超出工作区自动夹紧
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::20,最后更新 2026-11-04