dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
A community DSH plugin that opens a Sidebar file in the system default browser instead of the in-app preview.
Two entry points ship in one plugin:
| Seat | Where it shows | What it needs from the Host |
|---|---|---|
sidebar.right.tab.document.unpreviewable | "Cannot preview this file" empty state | the file's Host path (absolutePath) |
sidebar.right.tab.menu.item | Right-click menu of a Sidebar tab chip | only the tab's contentId (dsh-resource://file/session/…) |
There is deliberately no sidebar.right.tab.document.actions seat. When the
preview renders a file, the header already carries the preview's own controls, and a
second "open in browser" button next to them earns nothing; the tab-chip menu is one
right-click away and works for every tab. That menu seat is also what makes the
plugin useful for a tab whose preview fails: the built-in preview only renders
the document seats once its Host metadata read succeeds, so a tab showing an error
(for example HTTP 405 from a third-party previewer) has no absolutePath. The tab
menu still has the tab's resource address, which is enough to resolve, verify, and
open the file.
The package ships prebuilt artifacts (dist/), so installing needs no build step
and no runtime dependency.
From npm — published as
dsh-plugin-open-in-browser:
dsh-plugin-open-in-browser.The same target works from the plugin tooling:
plugin_manager action=install_bundle target=dsh-plugin-open-in-browser
From a local checkout or a GitHub clone (the clone already contains dist/):
plugin_manager action=install_bundle target=<absolute path to the checkout>
Requirements: the DSH desktop app (the Host must be able to launch a native application); Node ≥ 20 only when building from source. A remote/SSH deployment has no desktop browser to launch, so the control reports that instead of failing silently.
After any install, update, or rebuild, fully quit and reopen DSH: the client bundle is cached by the module loader.
The built-in preview renders HTML inside a sandboxed iframe (srcDoc/blob). Pages
that depend on fetch, modules, absolute paths, service workers, or a real origin
show up blank — and files the preview cannot render at all show an empty state. The
fastest escape hatch is the OS: hand the file to the default browser, which resolves
relative assets and origin-dependent APIs normally.
The browser half posts to one exact Host route registered by this plugin's Host half:
POST /dsh-plugin-open-in-browser/open
content-type: application/json
{ "path": "C:\\Users\\me\\project\\preview.html" }
{ "address": "dsh-resource://file/session/<sessionId>/pages/preview.html" }
address is the form a tab menu can produce: the Host resolves the Session's
workspace root (sessions.get(sessionId).header.cwd), joins the relative path,
then verifies the result (below). The reply is
{ ok: true, mode: "browser" | "association", path }; failures are
{ ok: false, code } with a 4xx/5xx status.
The Host half then resolves the user's actual default browser instead of relying on the OS file association:
HKCU\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https|http\UserChoice
→ ProgId → HKCR\<ProgId>\shell\open\command → executable;explorer.exe <file-uri> on Windows, open on
macOS, xdg-open on Linux).This matters for images, PDFs, and other previewable files: the shipped
session.openWorkspacePath Remote follows the OS association (a .png opens the
Photos app on Windows), while this plugin promises a browser. If the Host route is
unavailable (older Host half, deployment without it), the browser half degrades to
that Remote so the control still does something useful.
The Remote request schema is
{ path, action?: 'reveal', application?: string }. There is nointentfield — omittingaction/applicationis "open with the default application".
open-in-appDSH already ships @deepseek-ai/dsh-host-open-in-app +
@deepseek-ai/dsh-client-ui-open-in-app, which register the document-preview seats
(…document.actions and …document.unpreviewable) with an "Open in app" menu
(editors, Git GUIs, terminals). This plugin is not a replacement: it is a
single-purpose shortcut for "just open it in the browser", including files that
plugin's own preview cannot render, and it adds the tab-chip menu.
src/security.ts path-safety layer: absolute-path check, traversal/UNC/device
rejection, extension whitelist, workspace containment, symlink
escape detection via an injected realpath, and the
dsh-resource://file/… address parser
src/browser.ts Host-side default-browser resolver: registry UserChoice → ProgId →
command, well-known install paths, detached launch with a
watch window
src/index.ts Host half: registers the one route, resolves address → workspace
path, validates, launches
src/client.ts browser half: two slot registrations + route/Remote adapter
locale/en.json meta.title / meta.description
locale/zh.json same in Chinese
cordis.patch.yml Loader insert row
scripts/*.mjs tsc-driven builds (no bundler); the client build wraps the
compiled module in the DSH module-loader factory
tests/*.test.mjs node:test suites: safety layer, browser resolver, Host route
contract, and the built client bundle
src/main/, src/preload/, src/renderer/?Those are the layers of an Electron application. A DSH plugin is a Cordis
bundle, not an Electron app: it never reaches the Electron main process and DSH
exposes no preload/contextBridge bridge to plugins. The equivalent split is:
| Electron app layer | This plugin |
|---|---|
| main process (Node) | src/index.ts + src/browser.ts — the Host half, running inside DSH's Host process |
| preload / contextBridge | the Host route POST /dsh-plugin-open-in-browser/open (with DSH's session.openWorkspacePath Remote as the fallback) |
| renderer injection | src/client.ts — the browser artifact loaded through window.__ModuleLoader__ |
security.ts is shared by both halves on purpose: the browser half validates
before calling, and the Host validates again before launching. Splitting it into
a "main" copy would let the two gates drift apart.
Requires Node ≥ 20 (verified on Node 24) and no runtime dependency.
npm install # typescript + @types/node only
npm run build # dist/index.js + dist/browser.js (host) + dist/client.js (browser)
npm run typecheck # host and client configs
npm test # builds, then runs all four node:test suites
npm run build is the only step DSH needs: the package's files and exports
point at dist/.
Build and unit-test
npm run build
npm test
Expected: dist/index.js, dist/browser.js, and dist/client.js exist; all
node:test cases pass (currently 78). The suites cover the pure safety layer and
address grammar, the Windows registry/path resolution and launch-plan ordering
(with injected process facts), the HTTP contract of the Host route (405/415/403/
400 paths and the Origin fence), and the built client bundle executed in a bare VM
(loader id, two registrations, route-first behavior with Remote fallback, and the
refusal paths).
Install into the current profile (from an absolute package directory):
plugin_manager action=install_bundle target=E:\Ai\DSHprojects\plug-in\open-in-browser
A profile that already had this package installed must be restarted before the new artifact is loaded. When the profile links the workspace directory (as the local development setup does), rebuilding is enough; only the restart is required.
Smoke-test in the GUI
.html
page, a .png) → the preview header carries no extra browser button; the
file is meant to be read in place.HTTP 405) → the same right-click item resolves the tab's resource address and
opens the file.canOpenWorkspacePath() false and no Host route) the
document controls stay hidden and the menu item reports that the Host route is
unavailable..., a
non-whitelisted extension, a foreign Origin, or a path the Host cannot map is
reported.Uninstall
plugin_manager action=set_plugin target=dsh-plugin-open-in-browser enabled=false
Origin check (dsh-app://app or the serving Host; absent Origin
is a local client such as curl), and a bounded JSON body (16 KiB,
content-type: application/json required). It can only ever open a file path
that passes validation, never accept a command../.., UNC or device-namespace paths, control characters, or reserved
device names, and its extension must be on the browser-safe whitelist.header.cwd, then
passed through the same gate and an injected realpath; a symlink that leaves
the workspace is rejected (symlink-escape), as is an unknown Session
(session-unavailable) rather than guessing a root.sidebar.right.tab.files.actions, a toolbar that receives the displayed
workspace directory), and the shipped Files tree has no context menu of its own.
Adding one would require shipping a replacement file tree (what
dsh-better-sidebar does).absolutePath); a tab whose preview fails is covered by the tab-chip menu seat.sidebar.right.tab.document.actions in src/client.ts.DEFAULT_ALLOWED_EXTENSIONS if you want
more types opened externally.The plugin adds a process-launch capability, so it ships with a written threat model: the single Host route, the fences around it, the path gate, and what is explicitly out of scope. See docs/SECURITY.md.
To report a vulnerability, use GitHub's private advisory form (Security tab) rather than a public issue.
Repository: https://github.com/haoqiya138-source/dsh-plugin-open-in-browser.
The package is published to npm as
dsh-plugin-open-in-browser.
0.2.0 (2026-10-21) was the first public release.
Release checklist (Trusted Publishing is configured, see below):
npm test (rebuilds dist/ and runs all 78 cases) and npm run typecheck.npm version <patch|minor|major> — bumps package.json and package-lock.json,
commits, and creates the v<version> tag.git push origin main --follow-tags — the tag starts
.github/workflows/release.yml, which runs the typecheck and tests, publishes to
npm with provenance through Trusted Publishing (OIDC), and creates the GitHub
release. No local npm login is needed.The first public release (0.2.0) was published by hand with npm's interactive 2FA
flow: npm publish --access public prints an
https://www.npmjs.com/auth/cli/... URL that must be approved in a browser. That
path still works as a fallback, but an access token alone returns EOTP for
accounts in the auth-and-writes 2FA mode, so CI does not use tokens.
On npmjs.com the package has a Trusted publisher: GitHub Actions, repository
haoqiya138-source/dsh-plugin-open-in-browser, workflow release.yml, environment
empty, permissions npm publish / npm stage publish / npm dist-tag.
release.yml authenticates with the runner's OIDC token (id-token: write) and
keeps --provenance; no NPM_TOKEN secret is stored in the repository. The
v0.2.1 release is the verification publish npm requires after a publisher is
configured.
CI (.github/workflows/ci.yml) runs the typecheck and all 78 test cases on Linux
and Windows for Node 20 and 22. The release workflow rebuilds dist/ before
publishing, so the tarball always matches the tagged source.