Adds an unsigned, manual-download update flow. The main process fetches the OSS updates/releases.json manifest (DSH_UPDATE_URL overrides), compares the latest version, and on startup and hourly prompts to open the per-platform installer URL; a preload bridge exposes the same check to the SPA's About "check for updates" button, which renders the version, notes, and a download link. scripts/generate-release-json.mjs builds the manifest from the packaged .dmg/.exe. Windows nsis packaging is configured. Co-Authored-By: Claude <noreply@anthropic.com>
6.9 KiB
@deepseek-ai/dsh-desktop
Electron desktop shell for DeepSeek Harness. The Electron main process is a thin
wrapper: it spawns the real dsh CLI running the web profile on loopback (an
OS-assigned port), parses the readiness URL line the profile prints
(dsh web: http://127.0.0.1:<port>), and opens a native window at that address.
The harness — its Cordis plugins, webserver, static frontend dist, and WebSocket
transport — runs exactly as dsh web would, as a separate system-Node process,
so native addons keep their system Node ABI and are never loaded into Electron.
Why a system-Node child
Electron's bundled Node ABI differs from the system Node the harness's native
addons (e.g. node-pty for the terminal capability) are built against. Running
the harness in its own node child keeps those addons on the system Node ABI;
the Electron main and renderer never load them. This is why the desktop is a
launcher, not a bundler: it delegates the whole harness runtime to a child
process and only opens a window at the served URL.
Architecture
Electron main (this package)
│ spawns node <harness>/apps/cli/lib/bin.js --profile web --host 127.0.0.1 --port 0
▼
system-Node child — the dsh harness
│ prints "dsh web: http://127.0.0.1:<port>" on stdout
▼
Electron opens a BrowserWindow at that loopback URL
The main process keeps the window bound to the child: it reads the child's stdout for the readiness line, opens the window once the port is known, and quits when the child exits. A fatal child failure is shown in a native error box.
Feature surface
- Chinese application menu — replaces Electron's English default with
文件/编辑/视图/窗口/帮助 (plus the macOS app menu: 关于/服务/隐藏/退出). Menu
items are Electron roles with localized labels, so accelerators stay the
system defaults (e.g.
Cmd+Rreload,Cmd+Alt+IDevTools). - Branded icon —
build/icon.png(1024px) is used for the window icon and, throughelectron-builder.yml, derived into the per-target.icns/.ico. Replace it with final artwork anytime. - Self-contained harness — the packaged app ships the full harness runtime
under
Contents/Resources/harness(see below), so it runs on any target machine with no external Node or repository install.
Run from a checkout
pnpm install
pnpm run build # builds lib + the apps/web frontend dist
pnpm desktop:dev # opens the desktop window
desktop:dev builds the frontend dist and the dsh CLI, then launches
Electron. In development the harness resolves from the workspace, and you can
override the spawned Node or dsh entry with DSH_NODE / DSH_ENTRY.
Packaging
pnpm --filter @deepseek-ai/dsh-desktop run build:harness # (re)assemble the bundled runtime
pnpm desktop:pack # electron-builder: mac .dmg / win .nsis
Artifacts land in apps/desktop/dist.
The self-contained harness (build/harness)
The harness's pnpm workspace does not cleanly materialize into a portable
node_modules: its packages are linked through per-package symlinks to vendored
sources at the repository root (vendor/, packages/, native/), it has
native addons built for a specific Node ABI, and the web profile serves a
separately built frontend dist. pnpm deploy and electron-builder's dependency
resolution both produce incomplete or broken copies of this runtime.
The only known-good runtime is the repository's own working tree. So
scripts/build-harness.mjs assembles a self-contained copy into
apps/desktop/build/harness, preserving the relative layout the symlinks
depend on:
build/harness/
node_modules/ the full dependency store + link network
vendor/ vendored Cordis packages the symlinks resolve to
packages/ workspace packages the symlinks resolve to
native/ native addon sources (landlock-run)
apps/cli/ the dsh CLI (the harness entry)
apps/web/ the frontend + built dist
bin/node the platform Node binary the child runs on
electron-builder ships this as an extraResource at
Contents/Resources/harness, and src/main.ts spawns
Resources/harness/bin/node + Resources/harness/apps/cli/lib/bin.js with the
harness root as the child working directory. Because every symlink is relative
to that tree, the bundled runtime resolves identically on any machine of the
same OS/arch.
Platform matrix
Each target platform needs its own harness: the bundled bin/node and the
native addons are OS/arch-specific. Regenerate build/harness on each target
platform (or per-target in CI) before packaging that platform. The current
configuration targets macOS arm64 (mac.target: dmg); win.target: nsis
is declared but needs a Windows-built harness.
Signing
No signing is configured (electron-builder.yml has no mac.sign identity).
Artifacts are unsigned: on first open, macOS Gatekeeper may block them (open
via right-click → Open, or add a Developer ID Application certificate to
distribute). Provide a certificate before public distribution.
Size
The bundled harness is multi-GB uncompressed (≈2 GB), dominated by
node_modules; the compressed .dmg is on the order of a few hundred MB to
~1 GB. This is the inherent footprint of shipping the full harness runtime
standalone, and is the accepted trade-off for a zero-external-dependency app.
Updates (manual download)
The app is not code-signed, so updates are a manual-download flow rather
than a silent swap. On startup (and hourly), and from the Settings → About
"check for updates" button (through a preload bridge), the main process fetches
the OSS manifest (updates/releases.json, DSH_UPDATE_URL overrides) and
compares the latest version against app.getVersion(). When a newer release
exists it prompts the user to open the per-platform installer URL.
To publish a release:
pnpm --filter @deepseek-ai/dsh-desktop run build:harness
pnpm desktop:pack # produces .dmg (mac) / .nsis .exe (win)
DSH_RELEASE_NOTES="…" pnpm --filter @deepseek-ai/dsh-desktop run generate-release-json
scripts/generate-release-json.mjs writes dist/releases.json from the built
installers (DSH_UPDATE_BASE defaults to https://deepseek.pinesound.cn/updates/).
Upload releases.json plus the .dmg/.exe to the OSS updates/ folder, and
the deepseek-harness-web download/ page renders the same manifest. Building
the Windows .exe on macOS needs wine (or build on a Windows host).
Notes
asar: falseandnpmRebuild: falseare deliberate (seeelectron-builder.ymlcomments): the harness stays as real on-disk files for the child and its native addons, and native addons are not rebuilt for the Electron ABI because they never load under Electron.- The profile data (
~/.dsh/profiles/web) is created and read on the host the app runs on; it is not part of the packaged artifact.