Back to home

lijian-ui

dsh-desktop

基于官方 Deepseek Harness 打包的桌面客户端,方便直接安装使用

Stars
3
Language
TypeScript
Created
Aug 14, 2026
Updated
Aug 17, 2026

Introduction

dsh-desktop(DeepSeek Harness 桌面端)

简体中文 | English

基于官方 npm 包 @deepseek-ai/dsh 的 Electron 桌面壳。方案 A 的核心思路: Electron 主进程通过子进程启动官方 dsh web,再把其本地 HTTP 页面加载进窗口, 从而把官方 WebUI 包装成一个独立桌面应用。

原则:不引用、不改动官方 dsh 任何源码,仅消费官方 npm 包,跟随其 npm update 升级。


架构概览

flowchart TB
    subgraph Main["Electron 主进程(我们的代码,零原生模块)"]
        Mgr["DshManager<br/>spawn 官方 dsh + 端口解析<br/>崩溃自动重启"]
        Win["BrowserWindow<br/>加载 http://127.0.0.1:port"]
        Tray["系统托盘<br/>显示窗口 / 重启 / 退出"]
        Menu["中文菜单栏<br/>重启 / 关于 / 退出"]
    end

    subgraph Child["官方 dsh 子进程(系统 Node 运行)"]
        Cli["@deepseek-ai/dsh CLI<br/>dsh web --host 127.0.0.1 --port 0"]
        Http["HTTP 服务 + Agent 核心<br/>+ 前端静态资源"]
    end

    Mgr -- "spawn(系统 Node,承担原生模块 / Node 版本)" --> Cli
    Cli -- "stdout 解析端口" --> Mgr
    Cli --> Http
    Mgr -- "端口就绪" --> Win
    Win -- "关闭 → 隐藏到托盘" --> Tray
    Tray -- "显示主窗口" --> Win
    Menu -- "重启 dsh 服务" --> Mgr
  • 官方主包只暴露 CLI,没有可 import 的运行时 API,因此采用进程外 spawn
  • 原生模块(node-pty / koffi)与 Node 版本要求全部由系统 Node 跑的官方子进程承担, Electron 侧无需 electron-rebuild,无 ABI 负担。
  • 打包版:dsh 及其全部依赖由 asarUnpack 解包到真实文件系统,主进程用 「系统 Node 绝对路径 + dsh/lib/bin.js」启动子进程(见「打包版如何找到系统 Node」)。
  • 后续若官方发布 embed/SDK,可演进到方案 B(file:// + IPC 桥接),前端代码无需改动。

环境要求

依赖版本要求说明
系统 Node.js^22.19.0 || >=24.0.0dsh 官方的硬性要求,由子进程使用
npm随 Node 自带用于安装依赖
Electron^43.0.0(开发依赖)仅负责窗口与显示

注意:Electron 内置的 Node 版本不满足 dsh 要求,但无所谓—— dsh 跑在独立的系统 Node 子进程里,Electron 只负责显示页面。

打包版如何找到 Node 运行时

v0.2.0 起内置精简 Node 运行时resources/node-runtime/),打包版无需用户预装 Node, 开箱即用。桌面端按以下顺序解析 Node 可执行文件:

  1. 内置 Node 运行时(打包版自带,优先)——resources/node-runtime/node.exe(Win)或 bin/node(macOS)
  2. config.json 显式配置 nodePath(自定义时用)——例如 macOS:
    { "nodePath": "/usr/local/bin/node" }
    
  3. 自动探测(nvm → Homebrew → 官方路径),且每个候选都会做版本校验 (dsh 要求 ^22.19 || >=24),不满足的旧版(如系统 v18)自动跳过

内置 Node 由 scripts/fetch-node.cjsnode/ 官方发行包精简生成到 vendor/, 打包时经 electron-builder.ymlextraResources 打进安装包。 开发模式(npm run dev)无内置目录,走 2/3 步探测。

若探测失败且未配置,启动会报错提示。打包版同时要求 dsh 及其全部依赖 被 asarUnpack 解包(electron-builder.yml 已配置 **/node_modules/**), 因为系统 Node 无法读取 asar 压缩包内的文件。


安装与运行

# 进入项目目录
cd electron-app

# 安装依赖(会同时装上官方 @deepseek-ai/dsh 与 Electron)
npm install

# 开发模式:先编译 TypeScript,再启动 Electron
npm run dev

# 或分两步
npm run build   # 编译 src -> dist
npm start       # 启动 electron .

首次启动会:

  1. 主进程 spawn 官方 dsh web --host 127.0.0.1 --port 0
  2. 从子进程 stdout 解析实际端口(系统自动分配,避免冲突);
  3. 创建窗口加载 http://127.0.0.1:<port>,看到的就是官方 WebUI。

macOS 未签名应用安装说明

当前版本未做 Apple 开发者签名与公证,首次安装/启动时 macOS Gatekeeper 会拦截,常见报错与解决方法如下:

报错提示原因解决方法
「无法验证开发者」/「Apple 无法检查其是否包含恶意软件」Gatekeeper 拦截未签名应用右键应用图标 → 选择「打开」→ 弹窗中再点「打开」(仅首次,之后可直接双击)
「已损坏,无法打开。您应该将它移到废纸篓」下载文件被加了隔离标记(quarantine)终端执行 xattr -dr com.apple.quarantine "/Applications/DeepSeek Harness 桌面端.app"
「打不开,因为来自身份不明的开发者」同上系统设置 → 隐私与安全性 → 点击「仍要打开」

以上均只需在首次处理一次;应用本体功能不受影响,仅跳过 Gatekeeper 检查。 说明:未签名应用在部分系统设置下可能仍提示,此时请检查「系统设置 → 隐私与安全性 → 安全性」, 确认「允许从以下位置下载的应用」中选择了「App Store 和被认可的开发者」之外的选项。


配置

API Key

推荐用环境变量注入(最安全,不写入文件):

export DEEPSEEK_API_KEY="你的密钥"
npm run dev

或在项目根创建 config.json(已被 .gitignore 忽略,切勿提交):

{
  "apiKey": "你的密钥",
  "host": "127.0.0.1",
  "port": 0,
  "extraArgs": []
}

配置优先级:环境变量 > config.json > 内置默认

其他参数

  • host:监听地址,默认 127.0.0.1(仅本机,不暴露网络)。
  • port:传 0 让系统分配空闲端口;也可固定(如 3080)。
  • extraArgs:需要原样透传给 dsh web 的额外命令行参数数组。
  • nodePath:系统 Node.js 绝对路径(打包版必须,见下文「打包版如何找到系统 Node」)。

config.json 的读取位置

桌面端按顺序查找 config.json找到即用、不合并

场景路径
开发模式(npm run dev项目根:electron-app/config.json
打包版 Windows%APPDATA%\dsh-desktop\config.json,即 C:\Users\<用户名>\AppData\Roaming\dsh-desktop\config.json
打包版 macOS~/Library/Application Support/dsh-desktop/config.json

目录名取 Electron 的 app.getName()(打包后为 package.jsonname 字段,即 dsh-desktop; 注意不是 productName「DeepSeek Harness 桌面端」——该值只配在 electron-builder.yml, 不会进入打包后 app.asar 的 package.json),文件不存在时忽略,全部走默认值/环境变量。


目录结构

electron-app/
├── package.json          # 依赖与脚本(含官方 @deepseek-ai/dsh、electron-builder)
├── tsconfig.json         # TypeScript 配置(CommonJS 输出到 dist/)
├── electron-builder.yml  # 打包配置(重点:npmRebuild:false + asarUnpack 原生模块)
├── .npmrc                # 国内镜像源(npmmirror + Electron 二进制镜像)
├── .gitignore
├── README.md
├── scripts/
│   ├── build-native.cjs    # 打包前置:物化 koffi 原生二进制(best-effort)
│   ├── generate-icon.cjs   # 图标生成:官方 SVG → build/icon.ico / icon.png(依赖 sharp)
│   ├── publish-lib.mjs     # 发布公共模块:产物扫描、版本解析、发布说明加载
│   └── publish-github.mjs  # 发版脚本:gh CLI 创建 GitHub Release 并上传产物
└── src/
    ├── main/             # 主进程代码(Node)
    │   ├── index.ts          # 入口:生命周期、IPC、菜单/托盘串联、错误兜底
    │   ├── dsh-process.ts    # 核心:DshManager(spawn + 端口冲突重试 + 崩溃自动重启)
    │   ├── window.ts         # 创建 BrowserWindow、关闭→托盘拦截、加载失败兜底错误页、按端口重加载
    │   ├── menu.ts           # 中文应用菜单(文件/编辑/视图/窗口/帮助 + 重启/关于/退出)
    │   ├── tray.ts           # 系统托盘(显示窗口/重启 dsh/退出,关闭窗口后常驻入口)
    │   ├── config.ts         # 配置读取与 dsh 环境变量组装
    │   └── log.ts            # 统一日志工具
    └── preload/          # 预加载脚本(方案 A 预留桌面集成接口)
        └── index.ts          # 暴露 window.dshDesktop(平台/版本/打开外部链接/重试)

健壮性设计

桌面端在「启动」与「存活」两个维度做了容错,避免白屏或静默崩溃:

场景行为
端口冲突重试若显式配置了固定端口且该端口被占用,自动顺延端口重试(最多 10 次)后再失败。默认 --port 0 由系统分配,不会冲突。
子进程崩溃自动重启dsh 子进程运行期间异常退出时,按指数退避(1s→2s→4s…)自动重启,最多 5 次;重启成功后窗口无缝刷新到新端口。
加载失败兜底页面加载超时或 did-fail-load(如 dsh 崩溃)时,渲染中文错误页,提供「重新连接」按钮,点击即重启 dsh。
启动彻底失败首次启动即失败且无窗口时,弹窗提示后退出;有窗口时展示错误页,而非白屏。
关闭 → 系统托盘点窗口右上角 X(或 Cmd+W)不退出应用,隐藏到系统托盘常驻;托盘菜单「显示主窗口」/双击托盘图标恢复。托盘不可用(个别 Linux 桌面)时关闭窗口直接退出,避免窗口丢失。
主动退出托盘或菜单「退出」显式结束应用;退出时按进程树 tree-kill,dsh 拉起的孙进程一并清理。

重试/重启入口有三处:菜单「文件 → 重启 dsh 服务」、错误页「重新连接」按钮、macOS Dock 重建。 窗口恢复入口有三处:托盘菜单/双击、macOS Dock 激活、二次启动实例聚焦。


打包与原生模块分发

生产分发使用 electron-builder。核心难点是原生模块(node-pty / koffi)随包正确分发, 我们的处理原则是「不让 Electron 重编译、只负责解包」:

  1. npmRebuild: false(关键) 原生模块由系统 Node 运行的 dsh 子进程加载,绝不能让 electron-rebuild 把它们 编译成 Electron 内置 Node 的 ABI,否则子进程一加载就崩。配置见 electron-builder.yml
  2. asarUnpack 解包原生目录 Node 无法从 asar 压缩包内加载 .node / .dll / .exe,必须把 node_modules/node-pty/**node_modules/koffi/** 等解包到真实文件系统。
  3. build:native 物化 koffi 二进制 koffi 不在 npm 包内附带预编译二进制(运行时自下载到临时目录)。scripts/build-native.cjs 在打包前主动把它物化进 node_modules/koffi/win32_x64/koffi.node,随包分发, 避免打包后首次运行依赖联网。该步骤失败不阻断打包(运行时仍可自下载)。

打包命令

# 开发调试用的免安装目录(验证打包结构)
npm run pack

# Windows 安装包(NSIS .exe)
npm run build:electron:win

# macOS 双架构(同时产出 x64 + arm64)
npm run build:electron:mac

# 其他平台 / 自定义参数:透传给 electron-builder
npm run build:electron -- --linux

build:electron 是主命令(build:nativetscelectron-builder), 平台参数通过 -- 透传:Windows 用 --win,macOS 双架构用 --mac --x64 --arm64

产物输出到 dist-electron/(已在 .gitignore 忽略)。

发布到 GitHub Release

# 1. 先打包(产出 dist-electron/ 下的安装包 + latest.yml / latest-mac.yml 更新元数据)
npm run build:electron:win

# 2. 发布(创建/更新 GitHub Release 并上传全部产物)
npm run release:github
  • 前置:安装并登录 gh CLI(winget install --id GitHub.cli && gh auth login)。
  • tag:自动取 package.jsonversion,生成 v{version}(如 v0.2.0)。
  • 发布说明:默认文案;在项目根创建 RELEASE_NOTES.md 即可自定义(Markdown 全文作为 Release Notes)。
  • 重复发布同版本:脚本检测到已存在的 release 时会更新说明并 --clobber 覆盖同名附件。
  • 产物范围dist-electron/ 顶层所有 .exe / .dmg / .AppImage / .deb / .zip / .yml / .blockmap(排除 builder-* 调试文件)。

自动更新

v0.2.0 起内置 electron-updater,走 GitHub Release 通道:

  • 打包时(publish 已配置 GitHub provider)生成 latest.yml(Windows)/ latest-mac.yml(macOS)
  • 客户端启动 60 秒后自动检查,发现新版本自动下载,下载完弹窗「立即重启 / 稍后」
  • 帮助菜单「检查更新」可手动触发;每小时定时检查
  • 发布时务必保证 latest*.yml + .zip(macOS 更新用 zip,不是 dmg)+ .blockmap 都上传, 否则对应平台无法增量更新(release:github 的产物范围已覆盖)

注意事项

  • 自定义图标:官方 Harness 黑色鲸鱼图标已由 scripts/generate-icon.cjs 自动生成到 build/icon.ico(多尺寸)与 build/icon.png,无需手工维护。
  • 国内镜像.npmrc 已配置 npmmirror 与 Electron 二进制镜像,Electron 下载不受影响。
  • 首次打包耗时npm install 会下载 Electron 与官方 dsh 的原生依赖,建议使用国内源。
  • dev-preview 风险@deepseek-ai/dsh 处于 rc 阶段,版本升级可能带来破坏性变更, 建议锁定版本并在升级后回归测试(端口解析正则依赖其 stdout 格式)。

已知限制与后续演进

  • 此方案不是终态:官方 webserver 注释已预留 file:// + IPC 的桌面形态。 待官方发布正式的 embed/SDK 后,可将传输层从 HTTP 替换为 IPC,前端代码无需改动。
  • 端口:当前通过 loopback HTTP 通信,本机任意进程都能访问该端口。方案 B 可消除此面。
  • dev-preview 风险@deepseek-ai/dsh 处于 rc 阶段,版本升级可能带来破坏性变更, 建议锁定版本并在升级后回归测试(端口解析正则依赖其 stdout 格式)。