dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
English | 简体中文
An Electron desktop shell built on the official npm package @deepseek-ai/dsh. Approach A:
the Electron main process spawns the official dsh web as a child process and loads its local
HTTP page into a window, wrapping the official WebUI into a standalone desktop application.
Principle: no imports, no modifications of the official dsh source — consume only the official npm package and follow its
npm updateupgrades.
flowchart TB
subgraph Main["Electron Main Process (our code, zero native modules)"]
Mgr["DshManager<br/>spawn official dsh + port parsing<br/>crash auto-restart"]
Win["BrowserWindow<br/>loads http://127.0.0.1:port"]
Tray["System Tray<br/>show window / restart / quit"]
Menu["Localized Menu Bar<br/>restart / about / quit"]
end
subgraph Child["Official dsh Child Process (system Node)"]
Cli["@deepseek-ai/dsh CLI<br/>dsh web --host 127.0.0.1 --port 0"]
Http["HTTP server + Agent core<br/>+ frontend static assets"]
end
Mgr -- "spawn (system Node, handles native modules / Node version)" --> Cli
Cli -- "parse port from stdout" --> Mgr
Cli --> Http
Mgr -- "port ready" --> Win
Win -- "close → hide to tray" --> Tray
Tray -- "show main window" --> Win
Menu -- "restart dsh service" --> Mgr
spawn.electron-rebuild,
so there is no ABI burden.asarUnpack, and the main process launches the child using an absolute system Node path +
dsh/lib/bin.js (see "How packaged builds locate system Node").| Dependency | Version | Notes |
|---|---|---|
| System Node.js | ^22.19.0 || >=24.0.0 | dsh official requirement, used by the child process |
| npm | bundled with Node | for installing dependencies |
| Electron | ^43.0.0 (dev) | window & display only |
Note: Electron's bundled Node version does not satisfy dsh's requirement — but that is fine, because dsh runs in a separate system Node child process and Electron only displays the page.
Since v0.2.0 the app bundles a slim Node runtime (resources/node-runtime/), so packaged
builds work without the user installing Node. The resolution order:
resources/node-runtime/node.exe
(Windows) or bin/node (macOS)nodePath in config.json (for customization) — e.g. on macOS:
{ "nodePath": "/usr/local/bin/node" }
^22.19 || >=24); outdated versions (e.g. system v18) are skippedThe bundled Node is produced by
scripts/fetch-node.cjs(slimming the official dist innode/intovendor/) and packaged viaextraResourcesinelectron-builder.yml. Dev mode (npm run dev) has no bundled dir and falls back to steps 2/3.
Packaged builds also require dsh and all its dependencies to be unpacked via
asarUnpack(electron-builder.ymlsets**/node_modules/**), because the system/bundled Node cannot read files inside the asar archive.
# Enter the project directory
cd electron-app
# Install dependencies (installs official @deepseek-ai/dsh and Electron)
npm install
# Dev mode: compile TypeScript first, then launch Electron
npm run dev
# Or in two steps
npm run build # compile src -> dist
npm start # launch electron .
On first launch:
spawns the official dsh web --host 127.0.0.1 --port 0;http://127.0.0.1:<port> — you see the official WebUI.The current release is not signed/notarized with an Apple Developer ID, so macOS Gatekeeper may block the first install/launch. Common errors and their fixes:
| Error message | Cause | Fix |
|---|---|---|
| "cannot be opened because the developer cannot be verified" / "Apple cannot check it for malicious software" | Gatekeeper blocks the unsigned app | Right-click the app icon → select "Open" → click "Open" again in the dialog (first time only; double-click works afterwards) |
| "is damaged and can't be opened. You should move it to the Trash" | Download carries a quarantine flag | Run xattr -dr com.apple.quarantine "/Applications/DeepSeek Harness 桌面端.app" in Terminal |
| "cannot be opened because it is from an unidentified developer" | Same as above | System Settings → Privacy & Security → click "Open Anyway" |
Each fix is needed only once; the app itself is unaffected — this only bypasses the Gatekeeper check. If prompts persist, check System Settings → Privacy & Security → Security and make sure "Allow applications from" is not locked to "App Store and identified developers" only.
Environment variable injection is recommended (most secure, no file writes):
export DEEPSEEK_API_KEY="your-key"
npm run dev
Or create a config.json at the project root (gitignored, never commit it):
{
"apiKey": "your-key",
"host": "127.0.0.1",
"port": 0,
"extraArgs": []
}
Precedence: environment variable > config.json > built-in default.
host: listen address, default 127.0.0.1 (loopback only, not exposed).port: 0 lets the OS assign a free port; you may fix one (e.g. 3080).extraArgs: extra CLI arguments passed through to dsh web verbatim.nodePath: absolute path to the system Node.js (required for packaged builds — see
"How packaged builds locate system Node").The desktop app looks up config.json in order; the first found is used, not merged:
| Scenario | Path |
|---|---|
Dev mode (npm run dev) | project root: electron-app/config.json |
| Packaged Windows | %APPDATA%\dsh-desktop\config.json, i.e. C:\Users\<user>\AppData\Roaming\dsh-desktop\config.json |
| Packaged macOS | ~/Library/Application Support/dsh-desktop/config.json |
The directory name comes from Electron's
app.getName()(thenamefield ofpackage.json, i.e.dsh-desktop, when packaged; note it is NOTproductName"DeepSeek Harness 桌面端" — that value only lives in electron-builder.yml and does not reach the packaged app.asar's package.json). A missing file is silently ignored, falling back to defaults/environment variables.
electron-app/
├── package.json # deps & scripts (official @deepseek-ai/dsh, electron-builder)
├── tsconfig.json # TypeScript config (CommonJS output to dist/)
├── electron-builder.yml # packaging config (npmRebuild:false + asarUnpack native modules)
├── .npmrc # CN mirrors (npmmirror + Electron binary mirror)
├── .gitignore
├── README.md # Chinese (primary)
├── README.en.md # English
├── scripts/
│ ├── build-native.cjs # pre-pack: materialize koffi native binary (best-effort)
│ ├── generate-icon.cjs # icon generation: official SVG → build/icon.ico / icon.png (needs sharp)
│ ├── publish-lib.mjs # publish common module: artifact scan, version parse, release notes
│ └── publish-github.mjs # publish script: gh CLI creates GitHub Release & uploads artifacts
└── src/
├── main/ # main process code (Node)
│ ├── index.ts # entry: lifecycle, IPC, menu/tray wiring, error fallback
│ ├── dsh-process.ts # core: DshManager (spawn + port retry + crash auto-restart)
│ ├── window.ts # BrowserWindow, close→tray interception, error page, reload by port
│ ├── menu.ts # localized menu (File/Edit/View/Window/Help + restart/about/quit)
│ ├── tray.ts # system tray (show/restart/quit; persistent entry after close)
│ ├── config.ts # config loading & dsh env assembly
│ └── log.ts # unified logging
└── preload/ # preload script (reserved desktop bridge for Approach A)
└── index.ts # exposes window.dshDesktop (platform/version/open external/retry)
The desktop app is fault-tolerant across "startup" and "survival" to avoid white screens or silent crashes:
| Scenario | Behavior |
|---|---|
| Port conflict retry | If a fixed port is configured and occupied, retry with the next port (up to 10 times). Default --port 0 is OS-assigned, no conflicts. |
| Child crash auto-restart | On abnormal exit, restart with exponential backoff (1s→2s→4s…), up to 5 times; the window seamlessly refreshes to the new port. |
| Load failure fallback | On timeout or did-fail-load (e.g. dsh crash), render a localized error page with a "Reconnect" button that restarts dsh. |
| Startup failure | If the first launch fails with no window, show a dialog and quit; with a window, show the error page instead of a white screen. |
| Close → system tray | Clicking the window X (or Cmd+W) does not quit; the app hides to the system tray. Restore via tray menu "Show main window" / tray double-click. If the tray is unavailable (some Linux desktops), closing quits directly to avoid losing the window. |
| Explicit quit | "Quit" in tray or menu ends the app; on exit the whole process tree is tree-killed so grandchild processes spawned by dsh are cleaned up. |
Restart/retry entries: menu "File → Restart dsh Service", the error page "Reconnect" button, and macOS Dock rebuild. Window restore entries: tray menu/double-click, macOS Dock activation, and second-instance focus.
Production distribution uses electron-builder. The core challenge is shipping native
modules (node-pty / koffi) correctly; our principle is "do not let Electron rebuild, only
unpack":
npmRebuild: false (key)
Native modules are loaded by the dsh child process running on system Node; never let
electron-rebuild compile them against Electron's bundled Node ABI, or the child process
crashes on load. See electron-builder.yml.asarUnpack native directories
Node cannot load .node / .dll / .exe from inside the asar archive, so
node_modules/node-pty/**, node_modules/koffi/**, etc. must be unpacked to the real
filesystem (the current config unpacks all of node_modules/**).build:native materializes the koffi binary
koffi ships no prebuilt binary in its npm package (it downloads at runtime).
scripts/build-native.cjs materializes it into node_modules/koffi/win32_x64/koffi.node
before packaging so the first run does not depend on the network. Failure does not block
packaging (runtime can still self-download).# Unpacked directory for dev/debug (verify structure)
npm run pack
# Windows installer (NSIS .exe)
npm run build:electron:win
# macOS dual-arch (produces x64 + arm64)
npm run build:electron:mac
# Other platforms / custom args: passed through to electron-builder
npm run build:electron -- --linux
build:electronis the main command (build:native→tsc→electron-builder); platform args are passed via--: Windows uses--win, macOS dual-arch uses--mac --x64 --arm64.
Artifacts are output to dist-electron/ (gitignored).
# 1. Build first (produces installer & latest.yml under dist-electron/)
npm run build:electron:win
# 2. Publish (create/update GitHub Release and upload all artifacts)
npm run release:github
gh CLI (winget install --id GitHub.cli && gh auth login).package.json version → v{version} (e.g. v0.1.0).RELEASE_NOTES.md at the project root to customize
(full Markdown becomes the Release Notes).--clobber-overwrites same-named assets..exe / .dmg / .AppImage / .deb / .zip / .yml / .blockmap
under dist-electron/ (excluding builder-* debug files).scripts/generate-icon.cjs into build/icon.ico (multi-size) and build/icon.png —
no manual maintenance..npmrc configures npmmirror and the Electron binary mirror, so Electron
downloads are not affected.npm install downloads Electron and dsh's native
dependencies; using the CN mirror is recommended.@deepseek-ai/dsh is in rc; upgrades may bring breaking changes —
pin the version and regression-test after upgrading (the port-parsing regex depends on its
stdout format).file:// + IPC desktop form. Once an official embed/SDK ships, the transport layer can
switch from HTTP to IPC without touching frontend code.@deepseek-ai/dsh is in rc; upgrades may bring breaking changes —
pin the version and regression-test after upgrading (the port-parsing regex depends on its
stdout format).