DSH 实时预览标签页:把任意 dev server 嵌入 iframe,透明代理 SPA 路由。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:RaulLazaro/dsh-preview-plugin
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
该插件的 README 只有英文版本。
Live preview tab for DeepSeek Harness — embed any dev server in an iframe with transparent SPA proxying.
Features
- Path-based SPA proxy:
/preview/:port/*rewrites all routes so the embedded app works as if served from the DSH host - Editable URL bar: type a port, a path, a proxy path or a dev-server URL to move the preview wherever you need; the bar follows links clicked inside the page
- Multi-app monorepo support: Configure backend ports to proxy API calls through the same iframe (e.g. Astro frontend + Payload CMS backend)
- Smart routing: a deep link the dev server answers with
404is served the app shell so the client-side router can boot; assets, API calls and every path the dev server does serve keep their real answer - Fragment-link safe:
#sectionlinks jump inside the page instead of reloading the site root, and anchors rendered after load are still reached - Root-relative resources work:
/assets/photo.png,srcsetandposterare rewritten too, so images and lazy-loaded stylesheets load instead of 404ing against the DSH host - Absolute URL rewriting: Intercepts
fetch()andXMLHttpRequest.open()to rewrite both relative and absolute URLs (e.g.http://localhost:3001/api/...→/preview/3001/api/...) - Real API traffic: Method, body, cookies and
Set-Cookieare forwarded, so the embedded app can log in, submit forms and usePUT/PATCH/DELETE— not just serve pages - Navigation stays inside the preview: forms,
history.pushState,window.openand links that open a new tab keep the proxy prefix; an app that assignslocation.href(which no script can intercept) is brought back instead of leaving you on the DSH app - Port config survives restarts: what you set is written to
$DSH_HOME/preview-plugin.jsonand restored when DSH starts again - Agent tool:
set_preview_porttool lets the agent configure ports programmatically when starting dev servers - Global or per-session ports: Set ports globally or per conversation session via the API
- Auto-sync: The preview tab polls the host for agent-set port changes
- Sandboxed iframe:
allow-scripts allow-same-origin allow-forms allow-popupsfor secure embedding
Installation
1. Add as a dependency
In your DSH web profile's package.json:
{
"dependencies": {
"dsh-preview-plugin": "^1.3.2"
}
}
Installing from a local checkout works the same way:
{
"dependencies": {
"dsh-preview-plugin": "file:~/workspace/dsh-preview-plugin"
}
}
2. Add to bundles
In your profile's package.json, add dsh-preview-plugin to the dsh.profile.bundles array:
{
"dsh": {
"profile": {
"bundles": [
"...",
"dsh-preview-plugin"
]
}
}
}
3. Restart DSH
systemctl restart dsh
Usage
Basic (single server)
- Start your dev server on the VPS (e.g.
npm run dev -- --host 0.0.0.0 --port 3000) - Open the Preview tab in DSH Web GUI
- Type the port (e.g.
3000) in the URL field and click Go
The field then switches to the page being shown (e.g. /preview/3000/laptops?page=2#specs),
so you can edit it at any time and press Enter to load something else.
Navigating
The URL field accepts all of these:
| You type | It loads |
|---|---|
4321 |
port 4321, root page |
4321/laptops |
port 4321, /laptops |
/laptops?page=2 |
that path on the port already in use |
laptops |
same as /laptops |
?page=2 / #specs |
the current page with that query/fragment |
/preview/4321/laptops |
a proxy path, as-is |
http://localhost:4321/laptops |
the dev-server URL |
Non-local URLs are rejected (the proxy only reaches 127.0.0.1). ↻ reloads the
page currently shown, ↗ opens it in a browser tab of its own.
Multi-app monorepo
Start both servers:
# Backend (e.g. Payload CMS) cd apps/backend && pnpm dev # port 3001 # Frontend (e.g. Astro) cd apps/web && pnpm dev # port 4321Open the Preview tab
Type
4321in URL and3001in Backend (comma-separated for multiple)Click Go — API calls to
http://localhost:3001/api/...are automatically proxied through/preview/3001/api/...
Agent tool
The agent can configure ports programmatically using the set_preview_port tool:
set_preview_port(port: 4321, backendPorts: [3001])
This updates the interceptor script and client UI automatically.
API
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/preview-port |
Get the current global port and backend ports |
GET |
/api/preview-port?sessionId=<id> |
Get ports for a session (falls back to global) |
POST |
/api/preview-port |
Set global ports ({ "port": 4321, "backendPorts": [3001] }) |
POST |
/api/preview-port?sessionId=<id> |
Set ports for a session |
How It Works
- The host registers three routes on the DSH web server:
/api/preview-port— port management/preview/:port/*— transparent proxy
- Every request is proxied to
127.0.0.1:<port>with its method, headers and body; the port must be a real TCP port (1–65535) and a request body larger than the cap is refused with413instead of being buffered - SPA fallback: when a top-level HTML navigation (an
Acceptthat includestext/html, a path without a file extension) comes back as404, the proxy answers with the dev server's root HTML so the client-side router handles the route. Missing assets, API404s and everything the dev server does serve are passed through untouched - For HTML responses, it injects:
<base href="/preview/:port/">for relative URL resolution- An interceptor script that:
- Handles fragment links (
#section) itself: with a<base>tag they would otherwise resolve to/preview/:port/#section, a different path, and reload the site root instead of jumping inside the page. Targets that appear later (SPA sections) are retried for a few seconds. - Rewrites
fetch()andXMLHttpRequest.open()URLs (relative, absolute frontend, and absolute backend) - Rewrites root-relative resource URLs (
src,srcset,poster,data-src, and resource<link href>) on nodes added by JS — a<base>tag only affects truly relative URLs, so"/assets/x.png"would otherwise be fetched from the DSH host
- Handles fragment links (
- Resource URLs in the initial HTML are rewritten server-side, before the page is sent
- Requests keep their method, headers and body, and
Set-Cookiecomes back, so sessions and writes work inside the preview - For non-HTML responses, it streams the response directly
- The whole exchange is bounded by a stall timeout: if the upstream goes
silent for longer than the limit (15 s by default,
DSH_PREVIEW_TIMEOUT_MS), the request fails with502— or, once bytes are on the wire, the response is torn down instead of hanging
Persistence
The preview config (frontend port + backend ports) is stored in
${DSH_HOME:-~/.dsh}/preview-plugin.json, so it survives a DSH restart and is shared by
every session. Deleting the file simply resets it; a corrupt file is ignored.
State and cleanup
Besides the persisted global config, the host keeps in-memory state: one entry per session (keyed by session id, holding that session's frontend and backend ports) and a frontend-port → backend-ports lookup the proxy reads. Two rules keep both bounded on a long-lived box:
- Purge on session close. The plugin listens for DSH's
session/disposedand drops everything the session contributed — its entry, plus a port mapping that only that session justified. - Hard cap with LRU eviction. At most
DSH_PREVIEW_MAX_SESSIONSsessions are tracked (default 64; values below 1 fall back to the default). When the cap is exceeded, the least recently written session is evicted first — but never the active session: the one whose preview tab last polledGET /api/preview-port?sessionId=…, i.e. the session on screen. This is the safety net for sessions whosesession/disposednever arrives.
A port mapping survives only while something still points at it: the global config for its own port, or at least one live session. Changing the global port or moving a session to another port releases the mapping it replaced, and a surviving claimant's mapping is re-derived instead of deleted.
Known limit
location.href = "/…", location.assign() and location.replace() cannot be
intercepted: browsers install them as non-configurable own properties of the location
object ([LegacyUnforgeable]). The preview tab notices the escape and reloads that path
under the proxy, but an app that wants a clean URL should build it from its own base
(e.g. Astro's import.meta.env.BASE_URL) or use a relative path.
Testing
pnpm install
pnpm test
node --test runs eight suites:
| Suite | What it covers |
|---|---|
test/client-helpers.test.mjs |
URL/port parsing (resolveTarget) and proxy-path helpers |
test/preview-view.test.mjs |
The Preview tab component with real React and happy-dom |
test/preview-proxy.test.mjs |
The proxy end-to-end against a fixture site with Playwright (skipped when Chromium is unavailable) |
test/proxy-security.test.mjs |
Same-origin-only CORS, port range, request body caps |
test/proxy-timeout.test.mjs |
Stall timeout over the whole exchange, safe teardown of a half-sent response |
test/spa-fallback.test.mjs |
404 → app shell for HTML navigations, and everything that must keep its real 404 |
test/host-state.test.mjs |
The port config surviving a restart, read from $DSH_HOME/preview-plugin.json |
test/session-state.test.mjs |
Purge on session/disposed, port-mapping ownership, LRU cap and the active-session exemption |
Security Notes
- The proxy only targets
127.0.0.1— no external SSRF possible - Port validation enforces a real TCP port (1–65535) at every entry point: the
proxy, the WebSocket upgrade, the port API and the
set_preview_porttool - CORS is same-origin only:
access-control-allow-originis echoed only when the request'sOriginmatches the DSHHost, and a cross-origin preflight is refused with403. With the previous wildcard, any web site the user visits could enumerate every loopback port behind DSH and read what those ports serve - Request bodies are capped — 8 MiB for the proxy (
DSH_PREVIEW_MAX_BODY_BYTES) and 64 KiB for the port API — and a larger body is answered with413and a closed connection instead of being buffered - The upstream exchange is bounded by a stall timeout (15 s by default,
DSH_PREVIEW_TIMEOUT_MS), refreshed on every chunk - Redirects are never followed by the proxy (
redirect: 'manual'): the upstream'sLocationis handed back to the app as-is - The iframe is sandboxed with
allow-scripts allow-same-origin allow-forms allow-popups - Backend ports are validated and stored per frontend port
License
MIT
链接
同类插件
zhu1090093659/dsh-web#packages/dsh-task-board★ 8978
侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
zhu1090093659/dsh-web#packages/dsh-web-all★ 8978
DSH Web UI 插件与皮肤合集:任务看板、git 图、右侧面板、远程移动端 UI、桌宠、实时 token 统计与皮肤中心。
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 5292
右下角常驻的小鲸鱼挂件:余额、今日已用与每轮对话消耗(含峰谷价),余额预警与今日预算的泡泡内容都可编辑;泡泡点击序列模块化自定义,支持并列加权 A/B、随机台词与随机图片;内置 30+ 厂商模板(OpenAI / OpenRouter / Kimi / 硅基流动 / 方舟 / 智谱 / MiniMax 等),按模型查余额与订阅额度;另有任务结束音效、导入音频、自定义角色与资源管理。数据全在本机,无遥测。
ccch1mneyyy/dsh-TUI★ 4819
Claude Code 风格全屏终端 UI:像素鲸鱼顶栏、实时工作状态行、思考流式展开。
omdsh-dev/DSH-better-sidebar★ 4290
侧边栏完整工作台:内置文件渲染编辑、终端、Git 与子代理,支持三方插件注册新 Tab。
Devin-AXIS/deepseek-design#deepseek-idesign★ 1478
可视化设计工作室,支持网站、App 原型、海报、信息卡、报告和杂志的模板创建、元素编辑、选区级 AI 草稿衔接与导出。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。