dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
An in-app browser pane for DeepSeek Harness — real Chrome over CDP, plus rendered previews of local files, markdown and PDFs.
⚠️ Unofficial community project
Not affiliated with, endorsed by, or maintained by DeepSeek AI. This is a third-party plugin for their open-source harness. For the official project see deepseek-ai/deepseek-harness. Please do not report issues with this repo to DeepSeek.
A browser pane inside the harness, beside the chat. A real Chrome the human can watch and type into, plus rendered previews of local files — and three tools that let the model drive it and read what is on screen.
The pane is a real layout column: the conversation gives up space rather than being covered. Measured 907px → 447px when it opens, and back on close.
Requires dsh 0.2.0-rc.1 or newer. Web UI (profile
web) and DeepSeek's desktop app (profiledesktop): both supported. In the desktop app the pane leases a real<webview>through the app's own browser bridge, so every site loads (no framing limits). Verified on 0.2.0-rc.1: CNN opened by the agent, page text back toread_previewin about 1.5 s. (The desktop app has no public download yet; it is built from DeepSeek's source.)Using dsh 0.1.x? Use the
dsh-0.1tag of this repo.
dsh 0.2 ships browsing of its own. Pick what you actually need:
| you want | use |
|---|---|
| A browser for yourself in the desktop app | DeepSeek's built-in sidebar browser (ui-sidebar-browser): a full <webview> with native history in the desktop app. In the web UI it is off by default and falls back to an iframe, with the framing limits described below. |
| To read a local document yourself | the built-in document preview sidebar (ui-sidebar-documentpreview): Markdown, code, HTML, PDF, images, Office files. |
| The agent to open a page and read what is on screen | this repo: open_preview / read_preview / close_preview. The built-in sidebars register no model tools; they are for you, not the agent. |
| A full browser-automation agent (click, fill, multi-step) | DeepSeek's experimental browser-use providers (chrome-devtools-mcp, playwright-mcp, stagehand). Off by default, explicitly mounted, no support promise. |
So on 0.2 this repo's job is narrower than it was: agent tools, plus a pane the agent drives, in the web UI or the desktop app. The optional desktop shell described below predates the official desktop app; if you run DeepSeek's desktop app, you already have a real embedded browser and do not need the shell.
Web pages open in a real Chrome, driven over the Chrome DevTools Protocol with its own persistent profile. You can log in. You can type. Sites that refuse to be framed work, because nothing is being framed.
Local files are rendered, not linked:
| kind | shown as |
|---|---|
.md | formatted document — headings, tables, code, task lists, quotes |
.html | the real page, with its relative images and styles resolving |
| images | framed on the app background |
| page images, rendered server-side — no browser PDF viewer needed | |
| code / text / config | syntax-framed with language and line count |
Clicking a link or a file path in the chat opens it in the pane.
Saving a file to your own machine. A ↓ button appears in the toolbar
whenever the pane is showing a file. It serves the real bytes with
Content-Disposition: attachment rather than the rendered page you are looking
at. This matters most when the harness is on another machine — the file
lives over there, and until now the pane only ever showed you a picture of it.
The button is deliberately absent for web pages: "download" is not a meaningful action on a live page, and a button that does nothing is worse than no button.
open_preview(url, label?) — a web page or a local file. Accepts bare
domains (cnn.com), http(s) URLs, localhost:PORT, and file paths
(C:\path\file.md, /abs/path, file: URLs).read_preview(start?, count?) — what is on screen, as text. Pages through
with start=end when end < total_chars.close_preview(url?) — one tab, or the whole pane.read_preview answers from the right source for each kind:
| showing | read from |
|---|---|
| web page | the live DOM over CDP — document.body.innerText |
| the document's text layer (PyMuPDF), not the page images | |
| local file | the rendered page |
This is the central design decision, and it was reached by measurement rather than preference. An iframe cannot do this job, for three independent reasons:
x.com sends X-Frame-Options: SAMEORIGIN
plus a CSP frame-ancestors 'self'; reddit.com sends SAMEORIGIN. The
browser obeys, and the pane renders blank.SameSite=Lax, which the browser withholds inside a
cross-site iframe. Even a framable site renders signed out, with a valid
session sitting right there unused.Only (3) is fixable in userland, and fixing it would not have helped.
So cdpbrowser.js launches Chrome with a dedicated profile, attaches to a page
target over one WebSocket, and the pane mirrors it while forwarding clicks,
scroll and keystrokes back as CDP input events. No dependency is needed — CDP is
JSON over a socket, and Node has fetch and WebSocket built in.
On cookies and safety. A real browser is safer than the proxy it replaced, not riskier: cookies stay in Chrome's own encrypted store with per-origin isolation and SameSite enforced by the browser. Nothing is copied into a jar of this project's invention. The profile is dedicated, so the user's daily browser is untouched (Chrome cannot open one profile twice) and a login here persists.
The pane shows JPEG frames of a real browser. It is not an embedded Chromium surface — a page cannot host one, and the harness UI is a page. If you want a true embedded surface, this is not that, and no amount of tuning makes it that.
What it is: a responsive viewport onto a real, logged-in browser, where the agent reads the actual DOM rather than anything mirrored.
If you want the real thing, there is an optional desktop shell (for web-UI
users; DeepSeek's desktop app now provides its own embedded browser). A ~130-line
Electron wrapper loads this same UI in a window where <webview> exists, and the
plugin feature-detects it: the pane becomes an actual embedded browsing context
with native input and scrolling, and no second Chrome is launched at all. It is
strictly additive — in a plain browser tab nothing changes, and deleting the
shell folder reverts you with nothing to undo.
See docs/DESKTOP-SHELL.md; the source is in
shell/.
Page.startScreencast emits a frame only when the page repaints; the host
forwards each over Server-Sent Events. Measured ~22fps, first frame 59ms —
against ~2fps and up to 450ms of stale pixels for the polling version this
replaced.
Two findings made that possible, both non-obvious:
Chrome does not paint a window it believes nobody can see. The window is launched off-screen so it stays off the desktop — and that alone produced zero screencast frames, which is what forced screenshot polling and made the pane feel like a remote desktop. Four flags fix it, and the window stays hidden:
--disable-backgrounding-occluded-windows
--disable-renderer-backgrounding
--disable-background-timer-throttling
--disable-features=CalculateNativeWinOcclusion
Measured: 0 frames → 176.
Uncapped, Chrome pushes ~100fps (~4.5 MB/s at 46KB a frame) — far more than a reading pane needs, and enough to thrash the client. The host coalesces to ~25fps, holding it near 1 MB/s.
dsh 0.2.0-rc.1+. npm's latest tag still points at 0.1.x at the time of
writing, so install the 0.2 line explicitly: npm i -g @deepseek-ai/dsh@next^22.19.0 || >=24.0.0 (needs global fetch and WebSocket)dsh plugin shells out to itpip install pymupdf) for PDF rendering.
Without it, PDFs fall back to the browser's own viewer.1. Install the plugin's dependency (@deepseek-ai/dsh-tools, pinned to the
dsh 0.2 line):
cd /absolute/path/to/DeepSeek-Harness-Browser/plugin
pnpm install --ignore-scripts
2. Install it into the profile:
dsh plugin --profile web add link:/absolute/path/to/DeepSeek-Harness-Browser/plugin
For DeepSeek's desktop app use --profile desktop instead (or both). Quit the
desktop app fully (tray icon → Quit) and reopen it after installing: it loads
plugins only at launch, and reloading its window does not.
3. Add the row. On 0.2 all configuration lives in
$DSH_HOME/profiles/<profile>/cordis.patch.yml (default $DSH_HOME is ~/.dsh;
settings.yaml is gone). Append the - insert: row from
examples/cordis.patch.yml to that file. Do not
replace the file: it holds the rest of your configuration.
4. Restart: stop dsh web and start it again, then open a new session.
Plugins resolve from the profile directory. Putting one in the dsh install's
node_modules crashes every profile on boot with ERR_MODULE_NOT_FOUND.
One failing client plugin blocks the whole 0.2 UI. This plugin has a
browser half (client.js). If it fails to load, 0.2 does not skip it: the page
stops at "Failed to load plugins" and nothing renders. If that happens, remove
the row, restart, and check the browser devtools console for the error.
dsh host process the web GUI at :3080
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ index.js │ │ client.js │
│ · open/read/close_preview │ │ · the pane, in a real │
│ · /api/preview/* bridge │◀──────▶│ layout column │
│ │ │ · <img> of pushed frames │
│ cdpbrowser.js ──CDP──▶ Chrome│ │ · forwards clicks + keys │
│ filerender.js md/html/img │ │ · renders local files in an │
│ pdfrender.js PDF→img+text │ │ <iframe> │
│ webproxy.js fallback │ └──────────────────────────────┘
└──────────────────────────────┘
index.js is the host half; client.js is served to the page because
package.json declares dsh.client.platform: "web". One plugin entry, two
halves.
Recorded because none of them are guessable.
A drain-on-read command queue breaks with two panes open. Commands were
spliced off a queue, so whichever pane polled first swallowed them and every
other pane missed the navigation entirely — which looks exactly like
"open_preview is broken". Commands now carry a sequence number and are
retained briefly; each pane tracks the last it saw.
A prefix route ending in / never matches. dsh's matcher tests
pathname === prefix || pathname.startsWith(prefix + '/'), so a registered
prefix of /api/preview/asset/ is looking for a double slash. Register it
without the trailing slash.
file: URLs cannot be framed inside an http page. Chrome refuses, and the
frame renders nothing. Local files must be served by the host — which also
makes them same-origin, and therefore readable.
A local HTML file needs a <base> tag or its relative <img src> resolve
against the harness origin and 404 — the page renders with every image blank.
Serve assets as raw bytes. Running an image through the renderer wraps it in
a styled HTML page; feeding that to an <img> tag shows nothing.
Published dsh sub-packages can be a different generation.
@deepseek-ai/dsh-tools on npm's latest tag was still 0.0.1-rc.1 when the
harness was at 0.1.0-rc.6, and dsh-web-search-exa@0.0.1-rc.1 imports a
package that was never published — installing it takes the whole profile down.
plugin/package.json pins ^0.2.0-rc.1; bump that range when dsh moves to a
new minor, and re-run pnpm install in plugin/ after upgrading dsh.
dsh 0.2 dropped current from the session list. The pane only acts on
commands addressed to the Session on screen. 0.1's useSessions snapshot had
current; 0.2's has only {ids, byId, phase, projectionsBySession}, and the
on-screen Session is the byId row with retainedBy.mainView > 0. Reading
s.current on 0.2 yields nothing, so every agent command looked like another
Session's and the pane silently opened nothing. onScreenSession() handles both.
The desktop app refuses any <webview> it did not lease. Its main process
cancels will-attach-webview unless the guest carries a lease from
globalThis.dshDesktop.browser.acquire() (the bridge its own sidebar browser
uses), so a plain element never loads and read_preview waits forever. The pane
leases a guest, attaches it with the returned partition and
src="about:blank#<lease>", loads the real URL on the first dom-ready, and
releases the lease on close. Logins in these guests last until the app quits.
Debug the desktop app from outside. Launch it with
--remote-debugging-port=<port> and attach puppeteer-core with
connect({ browserURL }). The pane publishes window.__dshPreview = { sessionId, shell, desktopLease }. Do not reload the app window: it hangs
on "Loading plugins…" because its boot data only arrives at launch. Restart the app.
Do not let a read fall back to a previous page. An earlier version, on finding the active tab not yet rendered, returned the last tab that had rendered. A tab labelled Google returned CNN's text and the agent reported it as current. Labelling that answer with a warning was the wrong fix; not producing it is the right one. If a page has not rendered, the answer is "still loading".
The pane can hold real logged-in sessions. It is driven by an agent. If that agent runs without approval prompts, it can navigate, click and type in a browser holding the user's accounts. Tell it not to sign in or out, post, purchase or send anything unasked, and never to solve a CAPTCHA — and understand that this is guidance, not enforcement.
Proxied pages are sandboxed without allow-same-origin. Everything the pane
shows is served from the harness's own origin, so allow-scripts +
allow-same-origin together would let an arbitrary site's JavaScript reach
parent and read the app around it. Local files are unsandboxed (own content,
and required for the PDF viewer); external pages are given an opaque origin.
The asset route refuses to escape its directory. The token names a directory
and a ../ in the relative part is rejected — otherwise page content could
drive an arbitrary file read.
MIT — see LICENSE.