一个"只承载官方 DSH Web UI"的悬浮窗壳子:Electron 无边框置顶窗口(一窗一框),渲染层 React + shadcn/ui + Tailwind CSS;内容区 iframe 内嵌官方 DeepSeek Harness Web UI,通过 ?focus=<data-slot 路径> 聚焦到任意插槽——消息流、输入区、会话列表、详情面板,或任意第三方插件注册的插槽。
多窗口同连一个 dsh 实例,经官方 events.mux 广播实现数据同步显示:在一个窗口选择会话、输入内容、发送消息,其他窗口同步呈现。目标端口未连接 dsh 时,内容区透明透出 Windows 桌面(v0.2.0)。
只兼容官方 DSH Web UI:第三方 UI 必须基于官方插槽系统(
@deepseek-ai/dsh-client-ui-slots)扩展,随官方界面照常渲染;不兼容任意第三方页面——内容源白名单在构造层拒绝一切非官方形态的 URL。
| 能力 | 说明 |
|---|---|
| 🪟 悬浮窗 | 无边框、置顶(alwaysOnTop)、标题栏拖拽、边缘缩放、最小化/还原、关闭 |
| 🎯 插槽聚焦 | ?focus=<data-slot 路径> 只显示目标插槽;预置 6 项 + focus-plugin 动态插槽清单 + 自定义路径输入 |
| 🔒 内容源白名单 | iframe src 唯一经 lib/frameUrl.ts 构造——只允许「目标 origin 根路径 + focus 参数」,第三方页面一律拒绝 |
| 🔗 目标连接管理 | 默认 http://127.0.0.1:3081,UI 可改并记忆(localStorage),storage 事件跨窗口同步 |
| 📡 多窗口数据同步 | 所有窗口同连一个 dsh 实例,共享服务端会话状态 + events.mux 广播,一个窗口操作、全部窗口同步显示 |
| 🧩 第三方 UI 兼容 | 基于官方插槽注册的 DSH UI 插件在聚焦视图内照常渲染 |
| 👻 空端口透明显示 | 目标 dsh 未启动/中断/地址非法时内容区透明透出桌面(标题栏常驻、内容区点击穿透);主进程低频探活 + 点击窗口快速自检自动回连(v0.2.0) |
- 只兼容官方 DSH Web UI(deepseek-ai/deepseek-harness 的 web 客户端,含
@deepseek-ai/dsh-client-app-shell装配与@deepseek-ai/dsh-client-ui-slots插槽注册表) - 聚焦效果依赖目标 dsh 已挂载
@dsh-focus/focus-plugin(?focus=的 CSS 聚焦由官方页面内的插件实现,壳子只构造 URL);未挂载时?focus=无聚焦效果,页面完整正常显示 - 版本基线:
@deepseek-ai/dsh@0.1.0-rc.6;壳子只依赖 URL 参数协议与data-slot语义,不锚定官方编译产物类名 - 目标 dsh 默认回环地址(
127.0.0.1);跨机器部署需在 dsh 侧配置trustedHosts
┌──────────────────────── Electron 主进程 ────────────────────────┐
│ window-manager:无边框置顶悬浮窗创建/最小化/关闭/多窗口 │
│ connection-manager:低频探活 + focus 快速自检(connection:state)│
│ ipc:window:minimize/close/create + set-target/set-click-through│
└───────────────▲───────────────────────────────┬─────────────────┘
IPC invoke(preload contextBridge) │ ?slot= URL 参数
┌───────────────┴───────────────────────────────▼─────────────────┐
│ 渲染层(React + shadcn/ui) │
│ TitleBar / SlotPicker / SettingsDialog / DSHFrame │
│ lib/frameUrl:白名单 src 唯一构造入口 │
│ lib/connectionState:连接态纯判定与推送校验 │
│ DSHFrame:iframe 承载 + postMessage 接收(source/origin/schema 三重校验) │
│ useClickThrough:空端口内容区点击穿透(标题栏命中间键) │
│ store/config:目标地址 localStorage + 跨窗口 storage 同步 │
└───────────────┬─────────────────────────────────────────────────┘
│ iframe src = <target>/?focus=<slot>
┌───────────────▼─────────────────────────────────────────────────┐
│ 官方 DSH Web UI(同源 iframe,数据完全一致) │
│ 插槽树 sidebar | conversation | details + 第三方注册插槽 │
└─────────────────────────────────────────────────────────────────┘
- 数据同步:所有窗口连同一个 dsh 进程,会话状态只存在 host 侧一份;每次变更经
/api/events.mux、/api/events.host下行 WebSocket 广播给所有连接的窗口;同源浏览器状态(localStorage,如会话选择)跨窗口共享。?focus=只是纯 CSS 显示裁剪,数据流不变——"显示不同插槽,数据完全同步"。 - 白名单边界:壳子只约束 iframe 的初始 src(导航层);iframe 文档内部的
/api、WebSocket 请求同源于 DSH host,不受壳子约束。 - 空端口透明:主进程
connection-manager低频 GET 目标 origin 根路径判 up/down 并广播;渲染层在 down/加载失败时卸载 iframe(避免 Chromium 错误页白块破坏透明)并让内容区透明 +setIgnoreMouseEvents穿透,标题栏常驻可拖拽/设置;窗口 focus 触发一次快速自检。
前置条件:
- 已启动一个 dsh 实例(默认
http://127.0.0.1:3081) - 目标实例已挂载
@dsh-focus/focus-plugin(聚焦效果依赖;未挂载也可运行,页面正常显示)
npm install # 安装依赖
npm run dev # 启动悬浮窗(默认聚焦 conversation.session 消息流)生产构建与打包:
npm run build # electron-vite 构建(out/)
npm run package # electron-builder 打包(dist/:portable + NSIS 安装包)Windows 注意:若 shell 环境注入了
ELECTRON_RUN_AS_NODE=1,先执行Remove-Item Env:ELECTRON_RUN_AS_NODE再npm run dev(否则 Electron 会被当纯 Node 运行)。
| 操作 | 说明 |
|---|---|
| 拖动标题栏 | 移动悬浮窗 |
| 拖窗口边缘/角落 | 缩放 |
| 最小化 / 关闭 | 窗口控制按钮(右上) |
| + 新建悬浮框 | 打开第二个悬浮窗(一窗一框,可再选插槽) |
| 插槽下拉 | focus-plugin 动态发现 [data-slot] 清单(经 postMessage 三重校验后展示);未装/枚举失败时回落预置 6 项:消息流 / 输入区 / 对话区 / 会话列表 / 详情面板 / 消息全宽 |
| 自定义路径… | 输入任意 data-slot 路径(含第三方插件注册的插槽);未知路径按官方 no-op 语义完整显示 |
| 设置按钮 | 修改目标 DSH 地址(默认 http://127.0.0.1:3081,白名单校验,非法地址拒绝并提示) |
| 空端口(未连接 dsh) | 内容区透明透出桌面、点击穿透到桌面;标题栏常驻可拖拽/设置。启动 dsh 后自动回连,或点标题栏快速自检 |
配置持久化于渲染层 localStorage(key dsh-shell.target),同源多窗口自动同步。
npm run typecheck # tsc --noEmit(main/preload/renderer 双 tsconfig)
npm run test # vitest 单测(白名单/插槽/slot 消息 schema/连接态/配置持久化,44 用例)
npm run dev # electron-vite dev(渲染层 HMR + 主进程热重载)结构(electron-vite 三段构建):
src/
├── shared/ipc-channels.ts # IPC 契约(通道名 + WindowApi 类型)
├── main/ # 主进程:窗口管理 / IPC / 连接探活 / 生命周期
│ └── connection-manager.ts # 低频探活 + focus 快速自检 + 状态广播
├── preload/ # contextBridge 暴露最小 window.api
└── renderer/src/
├── components/ # 壳子 UI(含 shadcn ui 原语)+ useClickThrough 点击穿透 hook
├── lib/ # 业务纯函数:frameUrl 白名单 / slots 插槽 / slotDiscovery 消息校验 / connectionState 连接态
└── store/ # 数据存取:config / ipc / windowParams
- 窗口位置/大小不记忆、无托盘常驻、无一键拉起 dsh、每窗口共享同一目标地址——均为 v1 边界,演进项见
.intentflow/floating-frame/later-on.md;空端口透明态的演进项(选中标识、探测升级/api、慢自检退避等)见.intentflow/empty-port/later-on.md - 连接判定 = 主进程低频探活(GET 目标 origin 根路径,任意 HTTP 响应即 up)+ iframe
onLoad;跨源限制下无法读取官方页面内部状态 - 空端口透明基于 Windows 窗口级
transparent,静止态无碍;拖拽/缩放仍有已知闪烁/性能瑕疵(有内容时不受影响)
- 新增空端口透明显示:目标 dsh 未启动/中断/地址非法时内容区透明透出桌面,标题栏常驻、内容区点击穿透
- 主进程低频探活(
connection-manager)+ 窗口 focus 快速自检自动回连 - 连接态纯判定与推送校验(
lib/connectionState.ts)+ 单测
- 首个版本:Electron 无边框置顶悬浮窗壳子 + 官方 DSH iframe 承载 + 插槽聚焦 + 多窗口
dsh-focus— 本壳子的聚焦引擎:?focus=<插槽路径>纯 CSS 聚焦插件(需挂载到目标 dsh 实例)- deepseek-ai/deepseek-harness — 官方 DeepSeek Harness(本壳子唯一兼容的 Web UI)
MIT