# 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 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:`,镜像区开始滚动会话内容。 ## 命令行 ```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 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