packages/dsh-remote-web-ui
zhu1090093659/dsh-web手机/PC 远程操控 dsh web 工作区:扫码配对、令牌门控通道、SSE 实时同步,提供移动端与完整桌面 GUI 两种远程形态。
DeepSeek Harness 独立远程 Web UI:适配手机视口、软键盘与触控热区,同时保持原生桌面端界面不变。
English | 中文
GitHub Release tarballs ship prebuilt lib/ files and do not run a consumer-side
build lifecycle. Source checkouts use pnpm run build.
Mobile & Cross-Device Remote Web Control for DeepSeek Harness (DSH)
Instant QR Pairing · Cross-Device PC Access · Mobile Touch Gestures · Cloudflare Tunnels · Secure Device Session
Seamless remote access for DeepSeek Harness (DSH) Web GUI and desktop client sharing the exact same interface: pair an iPhone, Android smartphone, tablet, or another computer from the QR code next to settings. Both ends run the authentic official Web GUI — phones receive an injected portrait touch layer while remote computers enjoy the complete desktop workbench. Access is guarded by expiring pairing tokens and revocable sessions. Built-in LAN binding and optional Cloudflare quick tunnels enable effortless external access with persistent hostnames.
This is an unofficial community plugin derived from dsh-web v0.4.5. It is structured as a cordis bundle: the host half manages pairing tokens, device sessions, /api/pair routes, the gated /remote channel, and LAN binding; the browser half renders the remote access trigger, QR pairing modal, live device roster, settings card, and mobile portrait touch adaptation. It is not affiliated with or endorsed by DeepSeek.
/pair-accept → /pair-app): the plugin itself serves the official shell, so the phone needs no harness browser-auth cookie at all. A reopen service worker (https origins) then owns later navigations to / from history, bookmarks, or tab restore, so the phone reopens straight into the app instead of a 401 dead end (see Security model)./remote channel; unpaired PCs see a guided blocking page (with a manual pair-token input) and no workspace data behind it./remote channel cuts them off on their next request. Pairing is this plugin's access control for the /remote channel; direct /api on a LAN-exposed bind is governed by the harness fence + browser auth (see Security model). Loopback (127.0.0.1) keeps using /api directly, and so does a page an application on this machine delivered — the desktop shell's dsh-app://app/ or a file: page — because a pairing link is reachable only over the network. A paired device is a full-control credential (see Security model).cordis.patch.yml that pins the webserver bind to 0.0.0.0 (on) or 127.0.0.1 (off) — no --host command-line dance; an explicit --host/--port flag still wins. It maintains the matching host firewall rule (Windows Defender via netsh; Linux firewalld/ufw/iptables; other platforms report the firewall as unmanaged) and shows the live bind, the reachable LAN URLs, and the firewall state./api posture probe reports any host whose /api fence the SDK leaves open, and the auto-tunnel state appears on the panel while the tunnel is starting.dsh-update, the same sidebar foot seat), so turning remote access off — or disabling this plugin — never removes the update trigger.The official desktop layout already auto-collapses the sidebar below 1024px. On top of that, while the viewport is portrait + coarse pointer + narrower than 1100px the plugin injects:
[class$="_composerSeat"]) so the selectors survive official rebuilds that only change hashes: 44px touch targets on the collapsed rail, 16px inputs (prevents iOS focus zoom), safe-area padding for the composer, compact type for the message list and sidebar, a column layout for the settings modal, and a full-width PlanReview card;ctx.layout.toggleSidebar()), hidden while the sidebar is expanded;data-dsh-plugin), so ownership stays with the declaring plugin and class churn cannot resurrect them. These are render suppressions; the client bundles still load. Activation also closes the details panel through the official ctx.layout.closeDetails().sessionStorage.dsh-remote-force-desktop = 1 disables the whole layer; touch-phone portrait and constrained landscape layouts are adapted, while the native Desktop app and wide desktop-browser layout stay on the official presentation.The paired remote desktop also runs in host mode: on this harness line the "configuration plane is local" behavior is a client-side branch on connection.isLoopback, and the channel boot script installs the transport hook (__DSH_TRANSPORT__.ownsHost = true) before any boot entry. Host mode is server-granted: the hook is installed only when the device-gated app landing (/pair-app) published the grant marker ahead of the boot patch, so a shell served to an unpaired browser — a fence-open deployment — never poses as the machine owner. Settings, credentials, agent presets, and deliverables therefore work on the phone exactly as on the desktop — every call still rides the gated /remote channel. Three control planes stay physically local: /api/pair/*, /api/update/*, and /api/plugin-manager/*.
dsh CLI supports profiles (dsh --profile, dsh plugin) — the profile/bundle mechanism this package rides on.dsh web start) or start with dsh web --host 0.0.0.0. With the default 127.0.0.1 bind the panel shows an explicit explanation instead of a dead QR code — unless a public base URL is configured (see "Remote access over the internet" below), which makes the QR reachable from anywhere without rebinding. The panel's mint/stop endpoints are loopback-only by design: a desktop browser opened at the LAN URL sees a "配对面板仅限本机使用" banner.autoTunnel), the cloudflared platform binary ships with the package (its postinstall downloads it; a runtime download covers installers that skip postinstall scripts). No user-side tooling, account, or domain is needed.tunnelToken): the public hostname never changes, so a paired phone keeps its bookmark and its pairing cookie across restarts and never re-pairs.Install the prebuilt release artifact. The package is not yet advertised as an npm release, and installing a GitHub source archive directly is unsupported because consumers must receive the prebuilt lib/ directory:
# Download the stable artifact and its checksum from the matching GitHub Release,
# verify SHA256SUMS.txt, then install the local artifact.
curl -fL -o dsh-community-web-ui.tgz \
https://github.com/sunrulin123/dsh-community-web-ui/releases/download/v0.1.0/dsh-community-web-ui.tgz
dsh plugin --profile web add ./dsh-community-web-ui.tgz
# Source checkout (development only): build before linking it.
git clone https://github.com/sunrulin123/dsh-community-web-ui.git
cd dsh-community-web-ui
pnpm install --frozen-lockfile
pnpm run build
dsh plugin --profile web add link:$(pwd)
Restart the profile (dsh web), then open the phone icon in the sidebar foot. The plugin's cordis.patch.yml inserts the single plugin row that mounts both halves.
The release tarball already contains
lib/and does not run a build lifecycle during consumer installation. A source checkout intentionally has nopreparescript; runpnpm run buildbefore using thelink:form.
dsh web start.dsh web, click the phone icon, and the panel mints a fresh time-limited QR./pair-accept → /pair-app). The clean paired-shell URL remains /pair-app, so refreshing does not fall through to the host's / authentication gate. On a phone, the touch adaptation layer is already active — same live state with a responsive layout. Later reopens from history or a bookmark go straight back into the app (https origins; see Security model)./remote channel; unpaired PCs see the guided blocking page and no data. One active token pairs one device; mint a fresh QR for the next device.requirePairingForLan), the public base URL, the auto tunnel, the stable-hostname relay, and the named-tunnel token in the same settings card.A tunnel process restart — most often a mobile network dropping and reconnecting — used to drop the public host from the pairing fence the moment the tunnel left running, so the address still printed in the QR code answered 403, including on the deliberately public /api/pair/status (issue #1547). A named tunnel's fixed hostname and a registered relay origin never change, so both now stay trusted throughout the reconnect; a quick tunnel's previous host stays trusted for a 60 s grace window, because the edge may still be delivering a connection the phone already opened, and is dropped only if the tunnel has not come back by then.
Set 自动公网隧道 (autoTunnel) in the settings card. The plugin runs its own Cloudflare quick tunnel (cloudflared ships with the package), feeds the minted public URL into the QR base and the pairing fence dynamically, and keeps the posture probe informed — a phone anywhere can pair at any time. The manual public address and the named-tunnel token below are ignored while this is on. The minted hostname is ephemeral and changes on every dsh web restart — the stable-hostname relay below keeps the phone's origin fixed across those restarts.
While the quick tunnel runs, the plugin additionally registers its current tunnel URL with the dsh-market relay registry and rebuilds the QR on a fixed origin — https://<id>.dsh-market.com — minted once per profile and stored under $DSH_HOME/remote-web-ui-registry/. The hostname never changes, so the phone's bookmark and its pairing cookie keep working across dsh web restarts with no Cloudflare account, no dashboard, and no domain of your own.
The relay above already fixes the phone's origin on a shared dsh-market subdomain. A Cloudflare named tunnel is the alternative for deployments that want a hostname on their own domain: its public hostname is permanent, so the phone's bookmark and its pairing cookie keep working across dsh web restarts — pair once, never again (until the session idles past idleExpireMs).
dsh.example.com — to http://127.0.0.1:3080 (the port dsh web listens on).The plugin then runs cloudflared tunnel run --token with the same lifecycle management as the quick tunnel: the binary ships with the package, unexpected exits restart with backoff, a running tunnel's public URL is probed every 60 s and two consecutive failures mint a fresh tunnel through that same restart path, and the posture probe audits the /api fence for the fixed host. The token is stored as a settings secret and read back redacted. The auto quick tunnel takes precedence while it is on.
A running tunnel is not a terminal state: a connector that lost its Cloudflare edge registration keeps its process and its metrics port alive while the minted hostname stops resolving, so the plugin used to report running indefinitely and the relay kept forwarding the phone to the dead address (Cloudflare 530 / Error 1016) instead of the relay's offline page (issue #1723). The readiness watchdog now treats a public URL that stops answering — DNS failure, refused connection, or a Cloudflare 5xx such as 530 — as a dead tunnel after two consecutive probes, and the ordinary backoff restart mints a new URL and re-registers the relay. An unauthenticated 401/403 answer still counts as alive, because that is a healthy round trip to a harness that has not been paired yet.
Expose the local port with any tunnel and set 公网地址 (publicBaseUrl), e.g.:
# 1. Expose the local port (whatever dsh web is listening on):
cloudflared tunnel --url http://127.0.0.1:3080
# prints something like: https://xxxx-xxxx-xxxx.trycloudflare.com
# 2. Start dsh web as usual. Do not add --trusted-host for the tunnel
# domain unless you intentionally want the SDK to trust that host for
# /api. Keep the LAN bind off if only tunnel access is wanted.
Then set the printed URL as 公网地址. The QR link is built from it, and the plugin's pairing fence accepts the tunneled authority. The posture probe keeps auditing which /api authorities the SDK fence leaves open.
Work from this standalone repository (no sibling checkout needed):
cd dsh-community-web-ui
export NPM_TOKEN='<token>' # only if private @deepseek-ai auth is still required
pnpm install
pnpm run build
pnpm test
pnpm run typecheck
The peer APIs come from the official NPM SDK: every @deepseek-ai/* package used here is declared in devDependencies (0.2.0-rc.2 cohort), and TypeScript/Vitest resolve types straight from node_modules — no DSH source checkout is required. Release artifacts contain prebuilt lib/; a Git source checkout must run the explicit build command above.
pnpm run typecheck
pnpm test
pnpm run build
Pinned to the 0.2.0-rc.2 line; the seams this build relies on:
sidebar.footer.action foot seat (the 0.1.7 shell composition): the sidebar declares and renders the seat the remote entry occupies.ctx.layout.toggleSidebar() (packages/client/ui-layout): the whale button expands the collapsed sidebar through the official panel-action face.ctx.connection.authenticatedUrl() (packages/client/connection): the sanctioned launch-token seam the proxy redeems once for its inner credential (src/inner-auth.ts), so re-issued /api calls satisfy the harness browser-auth check.__DSH_FILE_UPLOAD__ (file-upload client hooks): the optional pre-Cordis transport the upload service reads once at construction. The remote boot patch publishes it so background uploads stay on the rewritten main-thread fetch instead of a Web Worker that escapes the channel (issue #1580).__DSH_TRANSPORT__.ownsHost (client-connection transport hooks): the host-mode flip for paired remote desktops. It is installed only when the device-gated app landing published the __DSH_REMOTE_HOST_GRANT__ marker ahead of the boot patch, so the flip is server-granted rather than asserted from the origin. There is no host-side per-method privilege pin on this line — the configuration plane branches on connection.isLoopback in the client — and no api/gate waterfall either (the gate listener stays mounted for deployments that gain the seam; pairing enforcement lives on this plugin's own /remote channel).webStartup-dependent !!js expressions reliably — the LAN bind block therefore materializes static values and the plugin re-asserts it every boot.The fence helpers (isTrustedApiRequest / isLoopbackHostname) are reimplemented locally in src/gate.ts / src/routes.ts: the connection plugin stopped exporting them, so the pairing routes carry their own copy scoped to the literals the QR links advertise.
The unit/component specs cover the route family, the gate, the channel, the lan-bind block, and the adaptation layer, but the pairing loop involves a real browser on a non-loopback origin. Repeat this after any change to the wire contract or the connection loop:
DSH_HOME=/tmp/dsh-qa dsh --profile web --no-open --port 3191 with the LAN bind toggle on (or a profile whose bind block pins 0.0.0.0).http://127.0.0.1:3191) in a browser: the phone icon sits in the sidebar foot; the panel mints a QR whose link is <lan-url>/pair-accept?pair=<token>./pair-accept → /pair-app?grant=<one-time> → /pair-app sets the device cookie, spends the grant, serves the patched official shell, and boots the UI — document.body.classList carries dsh-remote-portrait, the adaptation stylesheet and the whale button exist, and __DSH_TRANSPORT__.ownsHost is true. The settings surface renders host data (host mode), not a memory mirror. In an https deployment the shell also registers the reopen service worker (/pair-app.sw.js); reloading /pair-app in that tab boots the app instead of falling through to the harness / authentication gate. Legacy / bookmarks are still recovered by the worker.unpaired (the fence page offers the manual pair-token input).The public path is the same round trip through a tunnel (see "Remote access over the internet"): loopback mint → device opens the public QR URL → accept → official UI. Only publicBaseUrl (plugin config) names the tunneled host; --trusted-host is not part of this pairing flow. The desktop panel still opens at http://127.0.0.1.
/remote channel, unconditionally: every request must carry a live paired-device cookie, enforced before any bytes are forwarded. A missing or revoked session receives HTTP 403 with a JSON rejection carrying error.code: "unpaired"; the browser's EventSource API exposes only the stream failure, not that response body. This gate does not follow requirePairingForLan: the channel re-issues every call with the process's own browser-auth credential, so admitting an unpaired caller would hand out that authority. The LAN policy governs the plain /api surface only and can never widen the channel (issue #1665).host:port a browser visited) and has no loopback exemption, so a proxied re-issue to 127.0.0.1 cannot reuse a device's cookie. The plugin therefore redeems its own launch token once — the same exchange a first browser visit performs — and attaches that cookie to re-issued requests. The credential is only ever exercised behind the pairing gate above; 停止/取消配对 immediately stop exercising it./api. Nothing in the official runtime emits the api/gate seam (verified against the 0.2.0-rc.2 packages installed by this repository), so a dir