English | 中文
Electron desktop shell for DeepSeek Harness. It boots the published dsh web
backend on a loopback port chosen by the OS and loads the Web UI in a window,
so the full agent runtime and UI run locally with no protocol changes.
Electron main process (CommonJS)
├─ BrowserWindow opens immediately on the local boot page src/boot.html
│ (no backend awaited first)
├─ spawn → node lib/bin.js web --port 0 (staged registry backend under the
│ bundled standalone Node; or the source CLI of $DSH_SOURCE_REPO
│ via tsx)
│ └─ reads "dsh web: http://127.0.0.1:<port>" from stdout
├─ health-poll GET / until 200
└─ on success loadURL(http://127.0.0.1:<port>/) ← same-origin HTTP + WS
Boot progress streams to the boot page over IPC. Any failed step (missing Node, backend exit, health-check timeout) shows the error, the backend's stderr tail, and a retry button in the window instead of quitting; a backend crash while the UI is up returns to the boot page the same way.
The Web UI is served by its own backend and is same-origin with it, so the desktop shell reuses the entire HTTP/WebSocket transport and trust perimeter.
- Building/running from source: Node.js >= 22.19 and
pnpm install. - End users need nothing else: the installer bundles a pinned standalone Node
runtime at
resources/node(staged by the backend build from official nodejs.org archives, checksum-verified), and the backend, thedshCLI shims, and the Electron shell all run it. The backend cannot use Electron's embedded Node — itsnode-addon-require-builtinaddon (HMR) needs V8 embedder slots Electron does not provide.
The bundled runtime is only a convenience, not a lock: set
DSH_NODE=/absolute/path/to/node to force a specific Node binary (it wins
over the bundled one), or delete resources/node to fall back to node from
PATH. On macOS GUI launches, the fallback chain also probes the standard
Homebrew/MacPorts node locations and the login shell.
pnpm run backend # stage the win32-x64 registry backend into dist-desktop/backend
pnpm run backend:mac # stage the darwin-arm64 closure (Apple Silicon dev)
pnpm start # = electron . — dev launches the staged registry backendWindows x64 is the pnpm run backend default. For another dev target pass the
flags explicitly, e.g. pnpm backend -- --platform darwin --arch x64.
Set DSH_SOURCE_REPO to a dsh checkout to develop against harness source
instead: the shell launches that checkout's apps/cli/src/bin.ts via tsx (the
checkout needs its dependencies installed). The backend inherits the process
environment, so provide your API key the same way as dsh web — either export
DEEPSEEK_API_KEY or place it in the backend cwd's .env.
pnpm dist # win32 x64 (default): registry backend closure + NSIS installer
pnpm dist -- --arch arm64 # win32 arm64, cross-built on any hostArtifacts land in dist-desktop/release/win32/<arch>/: the installer is named
DeepSeek Harness-<version>-<arch>-setup.exe next to a win-unpacked/ layout.
macOS packages must be built on macOS (DMG creation uses hdiutil):
pnpm dist:mac # darwin arm64 (Apple Silicon)
pnpm dist:mac:x64 # darwin x64 (Intel)
# or explicitly:
pnpm dist -- --platform darwin --arch arm64Artifacts land in dist-desktop/release/darwin/<arch>/:
DeepSeek Harness-<version>-<arch>.dmg and .zip next to the packaged .app
directory. No signing identity is required for a local build; set
CSC_LINK/CSC_KEY_PASSWORD to sign a distributable build, then notarize the
DMG before sharing it outside your machine.
The backend closure (scripts/build-desktop-backend.mjs --platform win32|darwin --arch x64|arm64) materializes the committed registry manifest
(backend/: @deepseek-ai/dsh pinned to an exact version plus its lockfile)
with npm ci --omit=dev --ignore-scripts --os=<platform> --cpu=<arch>,
promotes the registry CLI package to the staging root (lib/bin.js with
node_modules beside it), prunes runtime-dead files and node-pty's foreign
platform prebuilds/, and asserts the target platform's addons staged before
packaging. Install-time --os/--cpu filtering stages only the target's
platform-optional leaves (@img/sharp-<platform>-<arch>,
@koromix/koffi-<platform>-<arch>). --ignore-scripts is required: koffi's
cnoke postinstall builds from source when allowed and fails on a cross-arch
host, while the prebuilt leaves make every install script in the closure
unnecessary. Cross-platform/arch builds skip the spawn-and-poll self-check
(target addons cannot load under the host node); run each closure's self-check
on matching native hardware. The same script stages the bundled Node runtime
(NODE_VERSION pin in scripts/build-desktop-backend.mjs): it downloads the
official archive from nodejs.org — DSH_NODE_DIST_MIRROR overrides the base
URL — verifies it against the published SHASUMS256.txt, caches it under
dist-desktop/cache/, and ships just the node binary (no npm/corepack) as
resources/node. The self-check runs the closure under that exact runtime
with a throwaway DSH_HOME, mirroring a fresh user's first launch. Upgrading
the backend is deliberate: bump the pin in backend/package.json, regenerate
the lockfile, ship a new installer. Upgrading the bundled Node is the same
ritual on NODE_VERSION.
Packaged builds check the release feed once per launch — on Windows through
electron-updater (full in-app flow), on macOS through the built-in feed
check that links to the browser download page (auto-update there needs code
signing + notarization first). A failed check (offline, unreachable feed,
malformed yml) is fully silent and never affects boot.
On Windows the flow is consent-driven end to end: a dialog announces the new version, the user clicks download and watches the progress bar (closing the dialog cancels the download), and only the「立即重启更新」button installs — via a silent reinstall that relaunches the app. If a downloaded update is left uninstalled, quitting asks once whether to install it on the way out; nothing is ever installed without an explicit yes. 「暂不」at the prompt silences that version until the next one.
- The feed defaults to this repo's GitHub Releases (the
publishblock inelectron-builder.ymlships asresources/app-update.yml). Publish a release withGH_TOKEN=<token> node scripts/release-desktop.mjs, which uploads the installer, its.blockmap, andlatest.ymltogether and verifies the feed's url lines name exactly the staged files. Artifacts are hyphen-joined (DeepSeek-Harness-<version>-<arch>-setup.exe) so the file name, the feed, and the GitHub asset name are identical. DSH_UPDATE_FEEDoverrides the feed: a full feed URL, or a directory base (the per-platform filename is appended); set it tooff/false/0to disable checking. Dev runs skip unlessDSH_UPDATE_DEV=1(the in-app download flow needs a packaged app, so dev Windows only exercises the browser-link flow).- The ignored version lives in
userData/update-state.json.
The desktop installers enable three dsh plugins by default:
@liustack/modlensdsh-usage-ledgerdsh-git-tree
They are packaged as dependencies of the backend closure under resources/backend. On first launch the Electron main process reads backend/desktop-plugins.json and appends the listed bundles to $DSH_HOME/profiles/web, so end users do not need pnpm or a manual dsh plugin add.
Keep these three places in sync when updating plugins: plugins/ tarballs, backend/package.json, and backend/desktop-plugins.json.
- The backend is the published
@deepseek-ai/dshfrom npm; a released version's bump commit may not be on the deepseek-harness GitHub master yet. Debug a packaged regression by unpacking the installed tarballs underdist-desktop/backend/node_modulesand diffing against a dsh checkout. - Dev (source via
DSH_SOURCE_REPO) and packaged (registry) backends share~/.dsh(DSH_HOME). Session formats are not compatible across pre-release versions; pointDSH_HOMEat a throwaway directory when smoke-testing an installer on a dev machine. - MVP: local run only. Linux installers, app icons, and code signing / notarization are out of scope for now (next steps).
- Closing the window kills the backend process tree. On Unix the backend runs
in its own process group; on Windows the tree is killed via
taskkill /T.