Files
dsh-float-window/README.md
T

214 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 截屏里彻底看不到这个窗 |
| 底部聊天框 | 直接发消息进当前会话(浮窗 POST 给插件,插件调宿主网关:0.1.7 走 `ctx.sessionController.prompt`,0.1.0 走 `ctx.apiProxy.sessions.prompt`,与浏览器 UI 同一条路) |
| 计费/状态条 | 轮数、步数、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` 专门干这个)。
8. **cordis:`ctx.set` 之前必须 `ctx.provide`**。漏了会抛 `cannot set property "floatWindow" without provide`,后果不是"少个功能"而是**整个插件树加载失败、DSH 进程直接退出**。本轮就是在隔离实例里实测复现后修掉的。
9. **PowerShell 事件处理器里参数不能叫 `$args`**:它是自动变量,会把整个事件参数数组占住,于是 `$e.TryGetWebMessageAsString()` 变成"数组没有这个方法"——表现为**页面所有按钮(含右下角缩放、置顶、锁定、全屏、设置)全部失效**。正确写法是 `param($s, $e)`。
10. **profile 的 `package.json` 不能带 BOM**:Node 读 JSON 会报 `SyntaxError: Unexpected token '锘?`。用 `[IO.File]::WriteAllText(..., UTF8Encoding($false))` 写。
11. **宿主里可能返回 Promise 的 API,必须 `.catch()`**:`ctx.get('llm').resolveModelInfo()` 在没注册 provider 时会抛错,而且它返回 Promise——只 `try/catch` 同步部分会留下 **unhandled rejection,直接把 DSH 进程 `fatal load failure` 干掉**(我在隔离实例里真把 DSH 搞崩过一次,1 秒一次的 SSE tick 是触发器)。
12. **会话事件载荷在 `event.data` 下**,不在顶层:`user/message` → `data.content[{type:'text',text}]`、`assistant/chunk` → `data.chunk{type,text,reason}`、`session/title` → `data.title`、`tool/call` → `data.name/args`。另外要过滤 `data.source.kind === 'plugin'` 的消息(系统提示快照),否则镜像里会混入一大段内部注入文本。
13. **宿主 RPC 的信封是 `{ rpcId, payload }`**(0.1.0 的 `ctx.apiProxy.sessions.*`):把字段平铺在顶层会得到 `Cannot read properties of undefined (reading 'sessionId')`。
14. **0.1.7 起宿主面换成了 `ctx.sessionController`**(`ctx.apiProxy` 不再存在),并且 Remote 签名是 `(request, signal)`——**漏了 signal 会报 `Cannot read properties of undefined (reading 'throwIfAborted')`**;`create` 还必须带一个合法的 `agentPreset`(否则 `Unknown agent preset: xxx`)。插件对两套 API 都自动适配(见 `hostCall`)。
## 版本支持
本插件在两种真实宿主上都实测跑通过(同一份代码,运行时自适应):
| 宿主 | 来源 | 会话 API | 结果 |
|---|---|---|---|
| dsh **0.1.0-rc.6** | npx 缓存(`dsh web`) | `ctx.apiProxy.sessions.*`(`{rpcId,payload}` 信封) | 插件加载、工具注册、路由、镜像、发消息全部通过 |
| dsh **0.1.7-rc.2** | `DSH Desktop.exe` 内置 | `ctx.sessionController.*`(`(request, signal)`) | 同上全部通过(`settings` 服务在 0.1.7 存在,设置入口走 profile 配置模型) |
## 怎么验证(可复现)
不需要动你正在用的 DSH:
```powershell
# 1) 造一个隔离的 DSH_HOME,profile 只装 base + web-app + 本插件
$t = '.runtime\testhome'
New-Item -ItemType Directory -Force -Path "$t\profiles\web\node_modules" | Out-Null
# package.json(bundles 只留三个) + cordis.yml/cordis.patch.yml 见 README 上文
cmd /c mklink /J "$t\profiles\web\node_modules\dsh-float-window" "$PWD"
# 2) 起第二个实例(8099),观察插件是否被加载
$env:DSH_HOME = (Resolve-Path $t)
node <dsh bin.js> web --port 8099
# -> 出现 endpoint.json 且无 "plugin tree failed to load" 即通过
# 3) 直接打插件路由
curl http://127.0.0.1:8099/dsh-float-window/health
curl http://127.0.0.1:8099/dsh-float-window/state
curl -N http://127.0.0.1:8099/dsh-float-window/events # SSE,每秒一帧
# 4) 让浮窗连到这个测试实例(看视觉效果时再加 -NoExclude)
shell\run-float.cmd -DshHome $t -NoExclude
# 标题栏应变成绿点 + 127.0.0.1:8099
# 5) 造一个真实会话并灌一条消息,验证镜像与指标(不需要浏览器)
curl -X POST http://127.0.0.1:8099/dsh-float-window/session -H "content-type: application/json" -d "{\"action\":\"create\",\"cwd\":\"D:/project\"}"
curl -X POST http://127.0.0.1:8099/dsh-float-window/chat -H "content-type: application/json" -d "{\"text\":\"你好\"}"
curl http://127.0.0.1:8099/dsh-float-window/state
# -> messages 里出现 user 行;metrics 里 turns/steps/tokens 有数(没有 API key 时
# assistant 行会是一条 ⚠️ 报错,这正是 LLM 分支被正确映射的证据)
```
`tools/smoke-host.mjs` 则是更轻的一层:假 ctx 直接调 `apply()`,验证路由/SSE/工具注册不抛异常。
## 配置
持久化在 `%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 是否遵守需要单独实测)。
* 从浮窗**新建会话**还没打通:`session.create` 在宿主里要工作区上下文,直接从插件调会被拒(`/session` 路由已留好,等接上 workspace)。
* 背景效果里的“半透明”依赖整窗 alpha,WebView2 子窗口在这种模式下的观感不如毛玻璃,建议优先用亚克力/云母。
* 还没做托盘图标与全局热键。
## 排障:插件自检文件
插件每次启动会写 `%DSH_HOME%\float-window\plugin-status.json`:
```json
{ "version": "0.1.0", "tool": true, "settings": false,
"notes": ["06:24:39 tool float_window registered", "..."],
"eventTypes": { "user/message": 3, "assistant/message": 2, "tool/call": 1 },
"lastRow": { "type": "assistant/message", "row": { "role": "assistant", "text": "…" } } }
```
* `tool` —— Agent 工具是否注册成功(`false` 说明 tools 服务不在);
* `settings` —— 设置命名空间是否注册成功(`link:` 安装时可能拿不到 schemastery,属已知降级);
* `notes` —— 启动自检日志(服务探测、endpoint 路径、错误);
* `eventTypes` / `lastRow` —— 会话事件直方图与最近一次映射结果,用于核对不同 DSH 版本的事件字段名。
## License
MIT