dsh-web
zhu1090093659
DeepSeek Harness (DSH) Web 插件聚合生态 · 万物皆插件,通过创意工坊分发||DeepSeek Harness (DSH) Web Plugin Aggregation Ecosystem · Everything is a plugin, distributed via the Creative Workshop
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:chendefine/dsh-sidebar-cdp-browser
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
A dsh-better-sidebar sidebar tab for DeepSeek Harness (DSH) that live-projects an external Chromium into the DSH Web GUI as a JPEG pixel stream, and sends back keyboard/mouse actions through a strictly allow-listed command protocol. The target page is never embedded as an iframe — the host process owns the only CDP connection, so the browser side never learns the CDP address and can issue no arbitrary CDP passthrough.


DSH Web (browser) DSH host process external Chromium
┌──────────────────┐ POST /dsh-cdp-live/api/open ┌──────────────┐
│ sidebar tab │ ────────────────────────────► │ one-time │
│ (React + Canvas) │ ◄──── ticket (TTL 30s) ────── │ 256-bit ticket│
│ │ │ │ the only CDP
│ renders JPEG ◄───┼── WS /sidebar/ws/cdp-live ──────┼─ puppeteer ──┼──► :9222
│ │ frame meta JSON + JPEG bytes │ -core │ (screencast
└─│──────────────┘ └──────────────┘ + Input)
└ allow-listed commands (zod strict discriminated union; no CDP passthrough)
The live view
dsh-sidebar-cdp-browser:live) in the better-sidebar sidebar, rendering the remote page as a live pixel stream on a canvas (Page.startScreencast JPEG frames; contain-fit scaling, devicePixelRatio-aware drawing);Target.getTargets snapshot, polled at 1s);visible gating) and resumes on return;Controlled interaction
strict() discriminated union (src/cdp/protocol.ts) — target lifecycle, screencast start/stop, Input.dispatch*, Page.navigate / history, ping — with no {method, params}-shaped CDP passthrough of any kind;The settings panel
A DSH host (Web GUI, web profile) on Node.js ≥ 20;
dsh-better-sidebar ≥ 0.17.1 — a required peer: this plugin's tab is registered through it and will not appear without it (install both with the one-liner below);
A started Chromium CDP endpoint reachable from the DSH host process, e.g.:
chromium \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port=9222
The package ships a dsh.bundle.patch (cordis.patch.yml: one insert row mounting the host-half entry). From the npm registry (prebuilt — no build permission needed) — one command installs the framework and this plugin together:
dsh plugin --profile web add dsh-better-sidebar dsh-sidebar-cdp-browser
From the GitHub repository (source — pnpm runs the prepare build):
dsh plugin --profile web add dsh-better-sidebar github:chendefine/dsh-sidebar-cdp-browser
Or through the DSH plugin marketplace (设置 → DSH插件市场) — tag the repo with the dsh-plugin topic and it is indexed automatically.
pnpm may block native-dependency builds of better-sidebar (e.g.
node-pty); add the named package toallowBuildsin the profile'spnpm-workspace.yamlas prompted and re-run.
After the plugin joins the profile layer stack, restart dsh web for it to load; uninstall with dsh plugin --profile web remove dsh-sidebar-cdp-browser and restart again.
The address is configured in the DSH web settings page, not in the profile array:
127.0.0.1:9222;host:port (auto-prefixed with http://) or http://host:port: the host discovers the webSocketDebuggerUrl via /json/version;https:// / wss://: same, TLS is the deployment's concern;ws://.../devtools/browser/...: connect to the browser WebSocket directly;The address persists in better-sidebar's prefs document (pluginSettings['dsh-sidebar-cdp-browser:live'].endpoint), read and subscribed through the DSH settings service. There is no loopback restriction — remote addresses work as-is; make sure of reachability and access control yourself.
The panel's "interactive input" master switch combines with the view header's "keyboard & mouse control" checkbox into a two-level gate:
The checkbox does not persist: a page refresh or panel reopen resets it unchecked (safe by default). Any switch change reconnects the view in the new mode, and the host enforces the ticket's mode — an unauthorized, even forged, client cannot emit control commands.
Optional fields in the profile config (all omittable; parentheses hold defaults and ranges). The four frame* fields double as the deployment defaults of the web-UI settings — a field changed in the UI wins:
- insert:
- id: sidebar-cdp-browser
name: dsh-sidebar-cdp-browser
config:
frameQuality: 60 # JPEG quality (20–90)
frameMaxWidth: 1280 # max frame width (320–3840)
frameMaxHeight: 900 # max frame height (240–2160)
| Field | Default | Range |
|---|---|---|
ticketTtlMs |
30000 | 5000–120000, one-time ticket TTL |
connectTimeoutMs |
15000 | 1000–120000, CDP connect timeout |
frameQuality |
60 | 20–90, JPEG quality |
frameMaxWidth |
1280 | 320–3840 |
frameMaxHeight |
900 | 240–2160 |
frameEveryNth |
1 | 1–30, keep 1 of every N frames |
bufferedAmountSoftLimit |
524288 | 64KB–16MB, frame dropping starts |
bufferedAmountHardLimit |
4194304 | 256KB–64MB, connection cut |
The four frame-capture parameters (frameQuality / frameEveryNth / frameMaxWidth / frameMaxHeight) can also be adjusted directly in the web-UI gear popup, besides the loader config:
1012 and they reconnect automatically; the new parameters apply at the next Page.startScreencast after the reconnect;GET /dsh-cdp-live/api/config (equally trust-fenced).Note: frameMaxWidth / frameMaxHeight are only projection caps (post-capture downscale bounds); they never affect the page's real layout size — pages always render at the remote Chromium window's true viewport (the plugin connects with an explicit defaultViewport: null and never overrides the viewport).
Refused WebSocket upgrades return explicit HTTP statuses; the browser console shows Unexpected response code: <status>:
| Status | Meaning | Fix |
|---|---|---|
| 401 | ticket invalid or expired | The client re-runs /open for a fresh ticket automatically; if persistent, check the session is still alive |
| 403 | trust fence refusal (Host header not trusted, or cross-site marker) | Behind a reverse proxy, verify the proxy forwards the correct Host header |
| 404 | wrong path / session not found | Check the route path and session id |
| 1006 (no status) | the request died at the reverse-proxy tier | The proxy must forward WebSocket upgrade headers (below) |
| 1011 (app close) | CDP connection / attach failure | Check the DSH host log (below) |
| 1012 (app close) | settings change (address / frame params) — expected | The client reconnects automatically |
| 1013 (app close) | slow client exceeded the hard backpressure limit | Auto-reconnects once the network recovers; raise bufferedAmount*Limit or lower quality if frequent |
nginx WebSocket forwarding reference:
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 connection failures log [dsh-sidebar-cdp-browser] CDP session attach failed: ... in the DSH host log — the browser only sees the disconnect, so start debugging there.
A DSH plugin has a host (node) half and a browser half; this plugin's split:
┌──────────────── DSH host process ─────────────┐ ┌── Chromium ──┐
│ host half (src/index.ts) │ │ │
│ ├─ POST /dsh-cdp-live/api/open ── issue ticket│ │ CDP :9222 │
│ ├─ GET /dsh-cdp-live/api/config── frame params│ │ (screencast │
│ ├─ WS /sidebar/ws/cdp-live ── the protocol│ │ + Input) │
│ └─ EndpointManager → puppeteer-core ────────────────┤ │
└───────────────────────────────────────────────┘ └──────────────┘
▲ ticket + binary frames (JSON meta + JPEG) ┌── browser ───┐
│ │ client half │
└────────────────────────────────────────────┤ canvas render │
mouse/keyboard/navigation — allow-listed (zod) commands ┘
src/, injects webServer / sessions / webRuntime): registers the HTTP and WebSocket routes, keeps the single CDP connection alive through puppeteer-core, subscribes to the DSH settings service for the UI's address and frame overrides, and hot-swaps the connection on settings commits;src/client/, the tab registered via dsh-better-sidebar): React components, canvas rendering, input events translated into the restricted commands.POST /dsh-cdp-live/api/open → trust fence + DSH session check
→ one-time 256-bit ticket (TTL 30s by default)
GET /dsh-cdp-live/api/config → trust fence; effective frame params (loader ∪ UI overrides)
WS /sidebar/ws/cdp-live?ticket=… → trust fence + one-time ticket consumption
→ 101, then the versioned protocol (v: 1)
Page.startScreencast (JPEG; quality/size/sampling = loader config ∪ web-UI overrides, UI per-field wins)
→ the host ACKs each Page.screencastFrame immediately (decoupled from downstream)
→ LatestFrameQueue single-slot latest-frame queue: producers never block, new frames evict old
→ ws.send(frameMeta JSON) metadata (sequence / mimeType / byteLength …)
→ ws.send(JPEG bytes) the binary frame right behind
→ client canvas draw
Two backpressure tiers keyed off ws.bufferedAmount:
1013 so a slow client can't pile up memory without bound.Versioned (v: 1, a zod strict() discriminated union, src/cdp/protocol.ts); the client's commands:
targets.list / targets.create / target.select / target.detach / target.closevisibility (frame-stream toggle for tab hide/restore)screencast.start / screencast.stop (options: format / quality / maxWidth / maxHeight / everyNthFrame)input.mouse / input.key / input.textnavigate (http(s) URLs only) / history (back / forward / reload)pingThe server's ready / response / targets.changed / target.closed / frame / error messages are equally strict schemas; unknown fields are always rejected.
settings/document-updated → drops the old connection, bounces clients with 1012 → the clients' backoff loop lands on the new address; http(s) addresses discover the webSocketDebuggerUrl via /json/version, ws(s) addresses connect directly; timeout and retries are tunable;Target.getTargets snapshot polled at 1s; the real document title after navigation is fetched through the same command).Remote tab closes (another CDP operator closing the streamed tab) are a first-class lifecycle path (since v0.1.2, covered by tests/remote-close.spec.ts): the registry marks the target closed and every attached client receives target.closed; the teardown chain (TargetController.detach → ScreencastController.stop → frame-queue close → pump end) is best-effort and never rejects — puppeteer's "Session already detached" on an already-dead session is swallowed by design, because an unhandled rejection there would take the host process down. The same guarantee covers a close racing an in-flight target.select, and the endpoint-change order (connection generation disposed before the bounced sockets finish closing).
The CDP capabilities open by default: target lifecycle, screencast start/stop/ack, Input.dispatch*, Page.navigate / Page.reload / navigation history.
Interactive commands (keyboard/mouse, navigation, tab management) sit behind the two-level gate: the "interactive input" setting (master) + the header's "keyboard & mouse control" checkbox (per view at runtime, unchecked by default). Only with both open is a session established in interactive mode; the host enforces the ticket's mode and blocks every control command otherwise.
Deliberately not open:
Runtime.evaluate;Two notes:
src/
├── index.ts # host-half entry: route registration, settings subscription, hot swap (inject: webServer, sessions, webRuntime)
├── config.ts # config schema (schemastery + zod twins) & endpoint normalization (covered via endpoint-config, 4 tests)
├── frame-settings.ts # the single frame-spec table: ranges/defaults/override reads/merge (pure, shared host+client) (6 tests)
├── trust-fence.ts # host/cross-site trust decision (5 tests via cdp-security)
├── routes/
│ ├── http.ts # /open + /config routes + the one-time ticket registry
│ └── websocket.ts # the WS upgrade route (trust fence + ticket admission + 401/403/404 refusals)
├── cdp/
│ ├── live-session.ts # session orchestration: attach, command dispatch, frame pump, 1012/1013 lifecycle
│ ├── endpoint-manager.ts / browser-connection-manager.ts / puppeteer-adapter.ts # the single CDP connection (9 tests via cdp-manager)
│ ├── screencast-controller.ts / frame-queue.ts # capture & the single-slot latest-frame queue (3 tests)
│ ├── input-controller.ts / target-controller.ts / target-registry.ts
│ ├── lease-manager.ts # exclusive per-target lease
│ └── protocol.ts # the versioned zod protocol (4 tests)
└── client/ # the better-sidebar tab (React + canvas + input bridges)
├── index.tsx # browser-half entry: register tab + settings panel (ctx.effect)
├── SidebarCdpBrowser.tsx # view assembly: settings / socket / target store
├── use-cdp-socket.ts # WS client: ticket fetch, backoff reconnect, binary frames
├── use-target-store.ts # target state
├── LiveCanvas.tsx # canvas + IME sink + keyboard ownership
├── input-bridge.ts # mouse bridge + capture-phase keyboard bridge (7+5+3 tests)
├── frame-renderer.ts # ImageBitmap rendering (contain fit + DPR)
├── geometry.ts # contain-fit coordinate mapping (3 tests)
├── TargetTabStrip.tsx / BrowserToolbar.tsx / ConnectionToolbar.tsx / StatusOverlay.tsx # the chrome components (13 tests)
├── settings.tsx # the gear settings panel: address / interactive switch / frame rows (10 tests)
├── i18n.ts # locale wiring: register zh/en dicts + t() (follows the DSH preference) + close-reason localization
└── locales.ts # zh/en dictionaries (key sets enforced equal, 5 tests)
tests/ # vitest: 94 cases / 18 files (live-probe is the opt-in real-Chromium probe; live-wiring spins real loopback HTTP/WS servers; remote-close replays the remotely-closed-tab lifecycle over real WS sockets; client-bundle verifies the built artifact's identity & purity)
cordis.patch.yml # the bundle channel's host-half insert row (mount declaration)
tsdown.config.ts # dual-bundle build (host ESM + client ModuleLoader format + purity gate + CSS inlining)
pnpm-workspace.yaml # pnpm ≥ 11-only settings (allowBuilds / minimumReleaseAgeExclude)
lib/ # build outputs (lib/index.js host ESM; lib/client.js client bundle; lib/types declarations)
Build outputs: the host half is a plain ESM bundle (puppeteer-core / ws / zod ship with the package); the browser half is a window.__ModuleLoader__.load({ id, factory }) registration bundle (the official external client-plugin delivery format) with React / cordis external and a purity gate that rejects Node builtins and @deepseek-ai/* value imports — tests/client-bundle.spec.ts verifies the built artifact's identity and browser purity after every build.
git clone https://github.com/chendefine/dsh-sidebar-cdp-browser && cd dsh-sidebar-cdp-browser
pnpm install # install (prepare builds once)
pnpm typecheck # tsc --noEmit
pnpm test # build + vitest run (82 cases)
pnpm build # rm -rf lib && tsc declarations + tsdown dual bundle → lib/
The real-Chromium probe: CDP_PROBE=1 pnpm vitest run tests/live-probe.spec.ts (override the address with CDP_PROBE_ENDPOINT).
pnpm-workspace.yaml (same-named .npmrc keys are silently ignored). This repo pins allowBuilds.node-pty: false there (better-sidebar's optional native dep is types-only; its build never runs) and minimumReleaseAgeExclude: dsh-better-sidebar@0.17.1 (a release-age policy exemption);>=0.17.1. Since 0.17 the package no longer augments the standalone cordis module's Context — the client half imports Context from the package root (import type { Context } from 'dsh-better-sidebar'; the type already carries the betterSidebar service face); TabComponentProps / SidebarSettingsRenderProps still come from dsh-better-sidebar/client/service;are all optional peers** (dsh-host-webserver/cordis/react/react-dom`), resolved by the DSH host at runtime and never bundled;src/frame-settings.ts carries zero imports and is reused verbatim by both bundles as the single spec table — loader schema, UI panel inputs and override reads can never drift apart on ranges/defaults; the client bundle's purity gate (build-time rejection of Node builtins and @deepseek-ai/* value imports) is enforced in tsdown.config.ts;PROTOCOL_VERSION constant lives on both ends (src/cdp/protocol.ts / src/client/cdp-api.ts) and message shapes are locked with zod strict() — adding a command means adding a union member, never loosening an existing field;dsh web restart; client-half changes need a rebuild plus a browser hard refresh.The npm package is dsh-sidebar-cdp-browser (repo: chendefine/dsh-sidebar-cdp-browser):
# 1. bump package.json's version, syncing every doc/comment that cites it
# 2. build + test, then publish (prepublishOnly re-runs the build)
pnpm test && pnpm publish --access public
# 3. tag & push the release
git tag v<version> && git push origin main --tags
Page.captureScreenshot compatibility fallback;MIT (see LICENSE).
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: dsh-better-sidebar。