dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
deepseek harness live view chromium via cdp in sidebar tab
为 dsh-better-sidebar 侧边栏注册一个「CDP 实时视图」标签页(DeepSeek Harness / DSH 插件):把一个外部 Chromium 的画面以 JPEG 像素流实时投进 DSH Web 界面,并在严格受限的命令白名单内回传键鼠操作。目标网页不会作为 iframe 嵌入——Host 进程独占 CDP 连接,浏览器端既拿不到 CDP 地址,也发不出任意 CDP 透传命令。
DSH Web (浏览器) DSH Host 进程 外部 Chromium
┌──────────────────┐ POST /dsh-cdp-live/api/open ┌──────────────┐
│ 侧边栏 Tab │ ────────────────────────────► │ 签发一次性 │
│ (React + Canvas) │ ◄──── ticket (TTL 30s) ────── │ 256-bit ticket│
│ │ │ │ 唯一 CDP 连接
│ 渲染 JPEG 帧 ◄───┼── WS /sidebar/ws/cdp-live ──────┼─ puppeteer ──┼──► :9222
│ │ 帧元信息 JSON + 二进制 JPEG │ -core │ (screencast
└─│──────────────┘ └──────────────┘ + Input)
└ 受限命令白名单(zod strict 判别联合,无任意 CDP 透传)
实时视图
dsh-sidebar-cdp-browser:live),Canvas 渲染远端页面的实时像素流(Page.startScreencast JPEG 帧,等比 contain 适配、按 devicePixelRatio 高清绘制);Target.getTargets 快照为准,1s 轮询刷新);visible gating),重新可见即恢复;受控交互
strict() 判别联合(src/cdp/protocol.ts)——target 生命周期、screencast start/stop、Input.dispatch*、Page.navigate / history、ping,没有任何 {method, params} 形式的任意 CDP 透传;设置面板
Node.js ≥ 20 的 DSH 宿主(Web GUI,web profile);
dsh-better-sidebar ≥ 0.17.1——必选 peer 依赖:本插件的 Tab 经它注册,不安装则 Tab 不会出现(推荐用下面的一句组合命令一起安装);
一个已启动、且能从 DSH Host 进程访问的 Chromium CDP endpoint。示例:
chromium \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port=9222
包内声明了 dsh.bundle.patch(cordis.patch.yml:一条 insert 行,挂载 host 半 entry)。从 npm registry 安装(预构建产物,无需构建许可)——一条命令同时安装框架与本插件:
dsh plugin --profile web add dsh-better-sidebar dsh-sidebar-cdp-browser
从 GitHub 仓库安装(源码——pnpm 会执行 prepare 构建):
dsh plugin --profile web add dsh-better-sidebar github:chendefine/dsh-sidebar-cdp-browser
或经 DSH 插件市场(设置 → DSH插件市场)——给仓库打上 dsh-plugin topic 即被自动收录。
pnpm 可能拦截 better-sidebar 的原生依赖构建(如
node-pty),按提示把具体包名加入 profile 目录下pnpm-workspace.yaml的allowBuilds后重跑即可。
插件加入 profile 层叠后需重启 dsh web 才加载;卸载用 dsh plugin --profile web remove dsh-sidebar-cdp-browser,再重启一次。
CDP 地址在 DSH Web 设置页配置,不需要改 profile 数组:
127.0.0.1:9222;host:port(自动补 http://)或 http://host:port:Host 从 /json/version 发现 webSocketDebuggerUrl;https:// / wss://:同上,TLS 由部署层保证;ws://.../devtools/browser/...:直接连接 browser WebSocket;地址持久化在 better-sidebar 的 prefs 文档(pluginSettings['dsh-sidebar-cdp-browser:live'].endpoint),Host 通过 DSH settings 服务读取并订阅变更。没有 loopback 限制——远程地址直接可用,请自行确保网络可达与访问控制。
同一设置面板的「交互输入」总开关,配合视图标题栏的「键盘鼠标远程控制」复选框构成两级控制门:
复选框状态不持久化:刷新页面或重新打开面板后回到未勾选(默认安全)。任一开关变化都会让实时视图按新模式自动重连,Host 侧按 ticket 模式强制拦截——未授权时即使伪造客户端也发不出任何控制命令。
profile 配置里的可选项(均可省略,括号内为默认值与范围)。四个 frame* 字段同时是 Web UI 设置项的部署默认值——UI 改过的字段以 UI 为准:
- insert:
- id: sidebar-cdp-browser
name: dsh-sidebar-cdp-browser
config:
frameQuality: 60 # JPEG 质量 (20–90)
frameMaxWidth: 1280 # 帧最大宽 (320–3840)
frameMaxHeight: 900 # 帧最大高 (240–2160)
| 字段 | 默认值 | 范围 |
|---|---|---|
ticketTtlMs | 30000 | 5000–120000,一次性 ticket 有效期 |
connectTimeoutMs | 15000 | 1000–120000,CDP 连接超时 |
frameQuality | 60 | 20–90,JPEG 质量 |
frameMaxWidth | 1280 | 320–3840 |
frameMaxHeight | 900 | 240–2160 |
frameEveryNth | 1 | 1–30,每 N 帧取 1 帧 |
bufferedAmountSoftLimit | 524288 | 64KB–16MB,超过开始丢帧 |
bufferedAmountHardLimit | 4194304 | 256KB–64MB,超过断开连接 |
四个帧采集参数(frameQuality / frameEveryNth / frameMaxWidth / frameMaxHeight)除了 loader 配置外,也可以直接在 Web UI 的功能设置弹层里调整:
1012 断开实时视图并自动重连,新参数在重连后的下一次 Page.startScreencast 生效;GET /dsh-cdp-live/api/config 获取当前生效值(同样受 trust fence 保护)。注意:frameMaxWidth / frameMaxHeight 只是投影像素上限(抓帧后等比缩小的上限),不影响浏览器中网页的实际布局宽高——页面始终按远端 Chromium 窗口的真实 viewport 渲染(本插件连接时显式 defaultViewport: null,从不覆写 viewport)。
被拒绝的 WebSocket 升级返回明确的 HTTP 状态,浏览器控制台显示 Unexpected response code: <status>:
| 状态 | 含义 | 处理 |
|---|---|---|
| 401 | ticket 无效或已过期 | 客户端会自动重新调 /open 拿新 ticket;持续出现请检查会话是否仍存活 |
| 403 | trust fence 拒绝(Host 头不在信任列表,或带 cross-site 标记) | 经反向代理访问时确认代理转发了正确的 Host 头 |
| 404 | 路径错误 / 会话不存在 | 确认路由路径与会话 id |
| 1006(无状态码) | 请求在反向代理层就被掐断 | 反代必须转发 WebSocket 升级头(见下) |
| 1011(应用层 close) | CDP 连接 / attach 失败 | 看 DSH Host 日志(见下) |
| 1012(应用层 close) | 设置变更(地址 / 帧参数),属预期 | 客户端自动重连,无需处理 |
| 1013(应用层 close) | 客户端消费过慢,超过硬背压限 | 正常网络恢复后自动重连;频繁出现可调大 bufferedAmount*Limit 或降低质量 |
nginx WebSocket 升级转发示例:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
CDP 连接失败会在 DSH Host 日志里输出 [dsh-sidebar-cdp-browser] CDP session attach failed: ...,浏览器端只看到断连,排障先看 Host 日志。
DSH 插件分 host(node)半与 browser 半,本插件各自职责:
┌──────────────── DSH Host 进程 ────────────────┐ ┌── Chromium ──┐
│ Host half (src/index.ts) │ │ │
│ ├─ POST /dsh-cdp-live/api/open ── 签发 ticket│ │ CDP :9222 │
│ ├─ GET /dsh-cdp-live/api/config── 生效帧参数 │ │ (screencast │
│ ├─ WS /sidebar/ws/cdp-live ── 业务协议 │ │ + Input) │
│ └─ EndpointManager → puppeteer-core ────────────────┤ │
└───────────────────────────────────────────────┘ └──────────────┘
▲ ticket + 二进制帧(JSON 元信息 + JPEG) ┌── 浏览器 ────┐
│ │ Client half │
└────────────────────────────────────────────┤ Canvas 渲染 │
鼠标/键盘/导航等受限命令(zod 白名单) └──────────────┘
src/,inject webServer / sessions / webRuntime):注册 HTTP 与 WebSocket 路由,经 puppeteer-core 维持唯一一条 CDP 连接;订阅 DSH settings 服务读取 Web UI 的地址与帧参数覆盖,设置提交即热切换连接;src/client/,经 dsh-better-sidebar 注册 Tab):React 组件,Canvas 渲染帧,把输入事件翻译成受限命令。POST /dsh-cdp-live/api/open → trust fence + DSH session 校验
→ 签发一次性 256-bit ticket(TTL 默认 30s)
GET /dsh-cdp-live/api/config → trust fence;返回当前生效帧参数(loader 与 UI 覆盖合并)
WS /sidebar/ws/cdp-live?ticket=… → trust fence + ticket 一次性消费
→ 101 后进入版本化协议(v: 1)
Page.startScreencast (JPEG; 质量/尺寸/抽帧参数 = loader 配置 ∪ Web UI 覆盖,逐字段取 UI 优先)
→ Host 收到 Page.screencastFrame 后立即 ACK(与下游消费解耦)
→ LatestFrameQueue 单槽最新帧队列:生产者永不阻塞,新帧顶掉旧帧
→ ws.send(frameMeta JSON) 元信息(sequence / mimeType / byteLength …)
→ ws.send(JPEG bytes) 二进制帧紧随其后
→ Client Canvas 绘制
两级背压基于 ws.bufferedAmount:
1013 断开,防止慢客户端无限积压内存。版本化(v: 1,zod strict() 判别联合,src/cdp/protocol.ts),Client 可发命令枚举如下:
targets.list / targets.create / target.select / target.detach / target.closevisibility(Tab 隐藏 / 恢复的帧流开关)screencast.start / screencast.stop(选项:format / quality / maxWidth / maxHeight / everyNthFrame)input.mouse / input.key / input.textnavigate(仅 http(s) URL)/ history(back / forward / reload)pingServer 下行 ready / response / targets.changed / target.closed / frame / error 同为 strict schema;未知字段一律拒绝。
settings/document-updated → 关旧连接、以 1012 弹开客户端 → 客户端退避重连落到新地址;http(s) 地址经 /json/version 发现 webSocketDebuggerUrl,ws(s) 地址直连,连接超时与重试可调;Target.getTargets 快照为准,1s 轮询;导航后的真实文档标题经同命令补齐)。远端关闭 tab(其他 CDP 操作方关掉正在串流的 tab)是一等生命周期路径(自 v0.1.2 起,由 tests/remote-close.spec.ts 覆盖):registry 标记 target 关闭,所有已连接客户端收到 target.closed;清理链(TargetController.detach → ScreencastController.stop → 帧队列关闭 → 帧泵结束)为尽力而为且绝不 reject —— puppeteer 对已死 session 抛出的 "Session already detached" 被有意吞掉,因为那里一旦冒出未处理的 rejection 会连累宿主进程。同一保证也覆盖"关闭与进行中的 target.select 竞态"、以及端点变更时序(连接代先于被弹开的 socket 完成关闭而被销毁)。
默认开放的 CDP 能力仅包括:target 生命周期、screencast start/stop/ack、Input.dispatch*、Page.navigate / Page.reload / navigation history。
交互命令(键鼠 / 导航 / 标签页管理)受两级门控:「交互输入」设置(总开关)+ 标题栏「键盘鼠标远程控制」复选框(运行时逐视图,默认不勾选)。二者都打开才会以 interactive 模式建立会话;Host 按 ticket 里的 mode 强制拦截一切控制命令。
默认不开放:
Runtime.evaluate;注意两点:
src/
├── index.ts # host 半入口:路由注册、settings 订阅、热切换(inject: webServer, sessions, webRuntime)
├── config.ts # 配置 schema(schemastery + zod 双形态)与 endpoint 归一化(4 测试经 endpoint-config 覆盖)
├── frame-settings.ts # 帧参数单一 spec 表:范围/默认/覆盖读取/合并(host 与 client 共享纯逻辑)(6 测试)
├── trust-fence.ts # Host/cross-site 信任判定(5 测试,经 cdp-security)
├── routes/
│ ├── http.ts # /open + /config 路由 + 一次性 ticket registry
│ └── websocket.ts # WS 升级路由(trust fence + ticket 准入 + 401/403/404 拒绝)
├── cdp/
│ ├── live-session.ts # 会话编排:attach、命令分发、帧泵、1012/1013 生命周期
│ ├── endpoint-manager.ts / browser-connection-manager.ts / puppeteer-adapter.ts # 唯一 CDP 连接(9 测试,经 cdp-manager)
│ ├── screencast-controller.ts / frame-queue.ts # 抓帧与单槽最新帧队列(3 测试)
│ ├── input-controller.ts / target-controller.ts / target-registry.ts
│ ├── lease-manager.ts # target 独占租约
│ └── protocol.ts # 版本化协议 zod schema(4 测试)
└── client/ # better-sidebar Tab(React + Canvas + 输入桥接)
├── index.tsx # browser 半入口:注册 tab + 设置面板(ctx.effect)
├── SidebarCdpBrowser.tsx # 视图组装:settings / socket / target store
├── use-cdp-socket.ts # WS 客户端:ticket 获取、退避重连、二进制帧
├── use-target-store.ts # target 状态
├── LiveCanvas.tsx # Canvas + IME sink + 键盘所有权
├── input-bridge.ts # 鼠标桥接 + 捕获阶段键盘桥接(7+5+3 测试)
├── frame-renderer.ts # ImageBitmap 渲染(contain 适配 + DPR)
├── geometry.ts # contain-fit 坐标映射(3 测试)
├── TargetTabStrip.tsx / BrowserToolbar.tsx / ConnectionToolbar.tsx / StatusOverlay.tsx # Chrome 组件(13 测试)
├── settings.tsx # 功能设置面板:地址 / 交互开关 / 帧参数行(10 测试)
├── i18n.ts # locale 服务挂接:注册 zh/en 词典 + t()(跟随 DSH 语言偏好)+ 断连原因本地化
└── locales.ts # zh/en 词典(键集合强制对齐,5 测试)
tests/ # vitest:94 例 / 18 文件(live-probe 为 opt-in 实机探针;live-wiring 起真实回环 HTTP/WS 服务;remote-close 以真实 WS 复现远端关 tab 生命周期;client-bundle 校验产物身份与纯度)
cordis.patch.yml # bundle 通道的 host 半 insert 行(挂载声明)
tsdown.config.ts # 双 bundle 构建(host ESM + client ModuleLoader 注册格式 + 纯度门 + CSS 内联)
pnpm-workspace.yaml # pnpm ≥ 11 专属设置(allowBuilds / minimumReleaseAgeExclude)
lib/ # 构建产物(lib/index.js Host 半 ESM;lib/client.js client bundle;lib/types 类型声明)
构建产物交付:host 半为普通 ESM bundle(puppeteer-core / ws / zod 等随包分发);browser 半为 window.__ModuleLoader__.load({ id, factory }) 注册格式(官方外部 client 插件交付格式),React / cordis 走 external,并带纯度门——拒绝 Node 内建与 @deepseek-ai/* 值导入,tests/client-bundle.spec.ts 在构建后校验产物身份与浏览器纯度。
git clone https://github.com/chendefine/dsh-sidebar-cdp-browser && cd dsh-sidebar-cdp-browser
pnpm install # 安装依赖(prepare 会先构建一次)
pnpm typecheck # tsc --noEmit
pnpm test # 构建后跑 vitest(82 例)
pnpm build # rm -rf lib && tsc 声明 + tsdown 双 bundle → lib/
需要真实 Chromium 的实机探针:CDP_PROBE=1 pnpm vitest run tests/live-probe.spec.ts(可用 CDP_PROBE_ENDPOINT 指定地址)。
pnpm-workspace.yaml 读取(.npmrc 中的同名键会被静默忽略)。本仓库在其中固定 allowBuilds.node-pty: false(better-sidebar 的可选原生依赖仅类型引用,不运行其构建)与 minimumReleaseAgeExclude: dsh-better-sidebar@0.17.1(发布年龄策略豁免);>=0.17.1。自 0.17 起该包不再对独立 cordis 模块做 Context augmentation——client 半统一从包根导入 Context(import type { Context } from 'dsh-better-sidebar',类型自带 betterSidebar 服务面);TabComponentProps / SidebarSettingsRenderProps 仍从 dsh-better-sidebar/client/service 导入;@deepseek-ai/* 均为可选 peer(dsh-host-webserver / cordis / react / react-dom),运行时由 DSH 宿主解析,不进 bundle;src/frame-settings.ts 不带任何 import,host 与 client 两个 bundle 原样复用同一张 spec 表——loader schema、UI 面板输入、覆盖读取三者的范围 / 默认值永不漂移;client bundle 的纯度门(构建期拒绝 Node 内建与 @deepseek-ai/* 值导入)在 tsdown.config.ts 强制;PROTOCOL_VERSION 常量位于 src/cdp/protocol.ts / src/client/cdp-api.ts 两端,消息形态用 zod strict() 锁死——新增命令 = 新判别成员,永不放松既有字段;dsh web 后生效;client 半改动重建后硬刷新浏览器即可。npm 包名为 dsh-sidebar-cdp-browser(仓库:chendefine/dsh-sidebar-cdp-browser):
# 1. 提升 package.json 版本,并同步所有引用该版本号的文档/注释
# 2. 构建 + 测试,然后发布(prepublishOnly 会再跑一次构建)
pnpm test && pnpm publish --access public
# 3. 打 tag 并推送发布
git tag v<version> && git push origin main --tags
Page.captureScreenshot 兼容回退;MIT(见 LICENSE)。