- shell: WinForms + WebView2 原生壳(无边框/置顶/固定位置/锁定尺寸/全屏/自适应缩放/截图排除) - shell/ui.html: 自适应界面(工具栏、会话镜像区、底部聊天框、轮/步/tok/s/MTOK/缓存命中/上下文状态条、外观设置) - lib/host.js: DSH 插件 Host 半(endpoint 写入、SSE 会话与指标快照、float_window Agent 工具) - tools: WebView2 SDK 拉取、BOM 修复、Host 冒烟测试 - README: 架构、实测结论(WDA_EXCLUDEFROMCAPTURE、玻璃模式、DPI、WebView2 加载)
147 lines
8.9 KiB
Markdown
147 lines
8.9 KiB
Markdown
# dsh-float-window
|
||
|
||
> DeepSeek Harness(DSH)悬浮窗:**始终置顶的会话镜像小窗**。把主界面切到后台,Agent 照样在跑,你在前面用一个小窗盯着它——而且 **Agent 抓屏时这个浮窗自动隐身**,完全不影响它的屏幕操控。
|
||
|
||
仅支持 Windows。
|
||
|
||
---
|
||
|
||
## 它长什么样
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ ● DSH 悬浮窗 未连接 📌 🔒 ⬆ ⛶ ⚙ ✕ │ ← 拖动这里移动窗口
|
||
├──────────────────────────────────────────┤
|
||
│ │
|
||
│ 会话镜像区(消息流 / 工具调用 / 状态) │
|
||
│ │
|
||
├──────────────────────────────────────────┤
|
||
│ [ 给会话发消息 …………………… ] ↑ │ ← 底部聊天框
|
||
├──────────────────────────────────────────┤
|
||
│ 轮 12 · 步 34 · 18.2 tok/s · 0.43 M tok │ ← 计费/状态条
|
||
│ 缓存 62% · 上下文 ▓▓▓▓░░░░ 41% │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
## 功能
|
||
|
||
| 需求 | 实现 |
|
||
|---|---|
|
||
| 悬浮窗模式(不全屏打开) | 独立原生顶层窗口,无边框,默认贴在屏幕右侧 |
|
||
| 始终置顶 | `TopMost`,标题栏 ⬆ 一键开关 |
|
||
| 固定位置 | 标题栏 📌(图钉)锁定/解锁拖动 |
|
||
| 锁定尺寸 | 标题栏 🔒(小锁)锁定/解锁缩放 |
|
||
| 自适应调整 | 布局随窗口尺寸重排;拖动**任意边缘和四角**都能缩放(系统级 WM_NCHITTEST) |
|
||
| 全屏切换 | 标题栏 ⛶ 或 `F11`,返回时还原原来的位置与尺寸 |
|
||
| 抓屏隐身 | `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)`,Agent 截屏里彻底看不到这个窗 |
|
||
| 底部聊天框 | 与当前会话互通(随插件启用) |
|
||
| 计费/状态条 | 轮数、步数、tok/s、累计 MTOK、缓存命中率、上下文用量进度条 |
|
||
| 外观自定义 | 背景效果:不透明 / 毛玻璃(亚克力)/ 云母 / 模糊 / 半透明;不透明度、界面缩放 |
|
||
| 按钮触发 | 运行 `shell\run-float.cmd`,或 DSH 里点插件入口 |
|
||
| Agent 自动打开 | 注册 `float_window` 工具,模型可以自己 `open / close / pin / fullscreen / hide` |
|
||
|
||
## 快速开始
|
||
|
||
```powershell
|
||
git clone <repo> dsh-float-window
|
||
cd dsh-float-window
|
||
|
||
# 1) 拉 WebView2 SDK(首次一次即可)
|
||
powershell -ExecutionPolicy Bypass -File tools\setup-webview2.ps1
|
||
|
||
# 2) 直接跑浮窗(不依赖 DSH)
|
||
shell\run-float.cmd
|
||
|
||
# 3) 装进 DSH(获得会话镜像 + Agent 触发能力),然后重启 DSH
|
||
dsh plugin --profile desktop add link:.
|
||
```
|
||
|
||
重启后浮窗会自动连上宿主,标题栏的“未连接”变成 `127.0.0.1:<port>`,镜像区开始滚动会话内容。
|
||
|
||
## 命令行
|
||
|
||
```powershell
|
||
shell\run-float.cmd # 打开(已开着则前置)
|
||
shell\run-float.cmd -NoExclude # 调试:允许被截图(否则你截不到它)
|
||
shell\run-float.cmd -Backdrop acrylic -Opacity 0.8 # 毛玻璃 + 80% 霜化
|
||
shell\run-float.cmd -Action pin # 给正在运行的实例下发动作
|
||
```
|
||
|
||
`-Action` 支持 `pin` `sizeLock` `topmost` `fullscreen` `hide` `show` `close`;
|
||
已运行的实例每 700ms 读一次 `%DSH_HOME%\float-window\command.json`,所以插件也能这样遥控它。
|
||
|
||
## 架构
|
||
|
||
```
|
||
┌──────────────────────────────┐ ┌───────────────────────────────┐
|
||
│ shell/float-window.ps1 │ │ lib/host.js (DSH 插件) │
|
||
│ WinForms + WebView2 原生壳 │ HTTP │ · 写 endpoint.json 给壳 │
|
||
│ · 置顶 / 锁定 / 全屏 / 缩放 │◀──────▶│ · SSE /events 会话+指标快照 │
|
||
│ · 截图排除 0x11 │ SSE │ · /cmd 控制通道 │
|
||
│ · 位置尺寸记忆 / 单实例 │ │ · float_window 工具(Agent) │
|
||
└──────────────┬───────────────┘ └───────────────────────────────┘
|
||
│ postMessage 桥 ▲
|
||
┌──────────────▼───────────────┐ │ session/event
|
||
│ shell/ui.html (全部界面) │ ┌─────────┴─────────────────────┐
|
||
│ 工具栏 / 镜像区 / 聊天框 / 状态条 │ │ DSH 宿主:sessions / tokenMeter │
|
||
└──────────────────────────────┘ └───────────────────────────────┘
|
||
```
|
||
|
||
* **界面完全在 `shell/ui.html`**:原生壳只负责窗口行为,页面通过 WebView2 的 `postMessage` 桥发 `drag / resize / topmost / posLock / sizeLock / fullscreen / opacity / scale / backdrop` 等命令。
|
||
* **数据来自插件**:壳把 `endpoint.json` 里的地址交给页面,页面用 `EventSource` 订阅 `/dsh-float-window/events`,拿到 `{messages, metrics}` 快照。
|
||
* **插件是"探测式"的**:`ctx.get('sessions' | 'tokenMeter' | 'sessionProjection' | 'tools')` 全部 try/catch,缺哪个服务就少几个数字,绝不让插件把 DSH 拖垮。
|
||
|
||
## 已实测的关键结论
|
||
|
||
这台机器(Windows 11 / 150% 缩放 / WebView2 154)上的实测记录:
|
||
|
||
1. **截图排除确实可行**:GDI 抓屏下
|
||
* `WDA_NONE` → 抓到窗口本身(对照组)
|
||
* `WDA_MONITOR (0x1)` → 抓到一个**纯黑方块**(窗口还在,只是变黑,观感很差)
|
||
* `WDA_EXCLUDEFROMCAPTURE (0x11)` → 窗口**完全消失**,抓到的是它背后的画面
|
||
* 只对本进程自己的顶层窗口生效(跨进程调用返回 False),所以浮窗必须是我们自己起的进程。
|
||
2. **毛玻璃模式顺带也是"抓屏隐身"的**:`DWMWA_SYSTEMBACKDROP_TYPE` + `DwmExtendFrameIntoClientArea` 的窗口,GDI 抓屏同样拿不到它(抓到的是底下内容)。
|
||
3. **WebView2 SDK 用 `Assembly.LoadFrom` 会报 0x80131515**(本机 PowerShell 5.1),改用 `[Reflection.Assembly]::Load(bytes)` 加载托管 DLL,再把原生 `WebView2Loader.dll` 放进 `PATH` 就好。
|
||
4. **初始化不能阻塞 UI 线程**:`EnsureCoreWebView2Async(...).GetAwaiter().GetResult()` 会死锁,必须用 `CoreWebView2InitializationCompleted` 事件。
|
||
5. **`CoreWebView2Environment.CreateAsync` 的参数顺序是 `(browserExecutableFolder, userDataFolder, options)`**,传错会得到 "Couldn't find a compatible Webview2 Runtime"。
|
||
6. **高 DPI**:WinForms 会把逻辑尺寸按缩放放大,位置要按真实 `Width/Height` 夹回屏幕内。
|
||
7. `.ps1` 里出现中文时**必须存成 UTF-8 with BOM**,否则 PowerShell 5.1 按 GBK 读会把脚本解析坏(`tools/fix-bom.ps1` 专门干这个)。
|
||
|
||
## 配置
|
||
|
||
持久化在 `%DSH_HOME%\float-window\window.json`(不可写时自动降到 `%LOCALAPPDATA%\dsh-float-window` 或仓库内 `.runtime`):
|
||
|
||
```json
|
||
{ "x": 1895, "y": 36, "w": 420, "h": 720, "topmost": true,
|
||
"posLock": false, "sizeLock": false, "opacity": 0.92,
|
||
"exclude": true, "scale": 1.0, "backdrop": "opaque" }
|
||
```
|
||
|
||
日志:`%DSH_HOME%\float-window\float-window.log`(每次启动都会记,排障先看它)。
|
||
|
||
## 目录
|
||
|
||
```
|
||
dsh-float-window/
|
||
├─ package.json DSH bundle 插件元数据(dsh.bundle.patch)
|
||
├─ cordis.patch.yml 挂载声明
|
||
├─ lib/host.js 插件 Host 半(路由 + SSE + float_window 工具)
|
||
├─ shell/float-window.ps1 原生壳(窗口行为 + 互操作)
|
||
├─ shell/ui.html 浮窗界面(自适应、工具栏、聊天框、状态条、设置)
|
||
├─ shell/run-float.cmd 启动器(用户双击 / 插件拉起)
|
||
├─ tools/setup-webview2.ps1 拉取 WebView2 SDK
|
||
├─ tools/fix-bom.ps1 给所有 .ps1 补 BOM(中文脚本必备)
|
||
└─ tools/smoke-host.mjs 插件 Host 半的假 ctx 冒烟测试
|
||
```
|
||
|
||
## 已知限制 / 路线图
|
||
|
||
* 仅 Windows(macOS 的排除要换 `NSWindow.sharingType`,且 ScreenCaptureKit 是否遵守需要单独实测)。
|
||
* 聊天框要真正把消息发进会话,还需要接宿主侧“发送到会话”的服务(当前只做只读镜像)。
|
||
* 背景效果里的“半透明”依赖整窗 alpha,WebView2 子窗口在这种模式下的观感不如毛玻璃,建议优先用亚克力/云母。
|
||
* 还没做托盘图标与全局热键。
|
||
|
||
## License
|
||
|
||
MIT
|