dsh-desktop:DeepSeek Harness 的本地优先桌面应用
一个基于 Go + Wails v3 构建的 Windows 桌面应用,把 DeepSeek Harness 的 agent 运行时与 Web UI 封装成开箱即用、完全离线的桌面产品。
项目地址: https://github.com/ydhcxh/dsh-desktop
一、项目定位
DeepSeek Harness 是一个功能完整的 agent 运行时,自带 Web 界面。但它以 Node.js CLI 工具的形式分发,普通用户要使用它,需要先安装 Node、手动拉起服务、记住本地端口、处理日志与进程生命周期------这些工程细节对最终用户来说并不友好。
dsh-desktop 不重新实现 Harness,而是在其之上补齐一个"桌面产品"所需的全部宿主能力:
| 能力 | 说明 |
|---|---|
| 免 CLI 启动 | 双击即可进入 Harness,无需手动敲命令 |
| 完全离线 | 内置 Node 运行时与预装依赖,目标机器零安装 |
| 数据隔离 | 应用专属启动目录,用户数据与安装目录分离 |
| 进程托管 | 统一管理子进程的启动、就绪检查、日志与退出 |
| 版本更新 | 可选的 GitHub Releases 版本检测与升级引导 |
换言之,dsh-desktop 负责"宿主",Harness 负责"智能"。两者各司其职,组合成一个可分发、可维护的桌面应用形态。
二、核心特性
1. 启动即用,无落地页
应用启动后直接导航到 Harness 的 Web UI,不设置多余的中转页面。启动期间仅显示一个简洁的加载页,就绪后自动跳转。
2. 随机端口,避免冲突
每次启动默认监听一个随机的 127.0.0.1 空闲端口,天然规避多实例与端口占用问题;需要固定端口时,可通过命令行参数或环境变量指定。
3. 完全离线的运行时
应用通过内置的 node.exe 直接运行预装在 runtime/ 目录中的 dsh 入口脚本 ,运行期不依赖网络、不走 npx 下载,目标机器无需安装 Node.js。
4. 应用专属启动目录
Harness 的工作目录被指向 %LOCALAPPDATA%\dsh-desktop\launch-root,其 profiles / sessions / plugins 等用户数据与安装目录分离,升级程序不会误删用户数据。
5. 健壮的进程生命周期管理
- 退出时优雅终止 Harness 子进程;
- 通过 Windows Job Object (
KILL_ON_JOB_CLOSE)兜底,即使主进程被强杀,整棵子进程树也会被系统强制回收,杜绝"僵尸进程"。
6. 可选的版本检测
构建时注入 GitHub 仓库后,应用会在启动后、每 6 小时以及手动触发时检查最新 Release,发现新版弹出对话框并一键打开下载页。
三、运行时架构
#mermaid-svg-Bckx6uLboOYlaxgQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bckx6uLboOYlaxgQ .error-icon{fill:#552222;}#mermaid-svg-Bckx6uLboOYlaxgQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bckx6uLboOYlaxgQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bckx6uLboOYlaxgQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bckx6uLboOYlaxgQ .marker.cross{stroke:#333333;}#mermaid-svg-Bckx6uLboOYlaxgQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bckx6uLboOYlaxgQ p{margin:0;}#mermaid-svg-Bckx6uLboOYlaxgQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster-label text{fill:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster-label span{color:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster-label span p{background-color:transparent;}#mermaid-svg-Bckx6uLboOYlaxgQ .label text,#mermaid-svg-Bckx6uLboOYlaxgQ span{fill:#333;color:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ .node rect,#mermaid-svg-Bckx6uLboOYlaxgQ .node circle,#mermaid-svg-Bckx6uLboOYlaxgQ .node ellipse,#mermaid-svg-Bckx6uLboOYlaxgQ .node polygon,#mermaid-svg-Bckx6uLboOYlaxgQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bckx6uLboOYlaxgQ .rough-node .label text,#mermaid-svg-Bckx6uLboOYlaxgQ .node .label text,#mermaid-svg-Bckx6uLboOYlaxgQ .image-shape .label,#mermaid-svg-Bckx6uLboOYlaxgQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bckx6uLboOYlaxgQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bckx6uLboOYlaxgQ .rough-node .label,#mermaid-svg-Bckx6uLboOYlaxgQ .node .label,#mermaid-svg-Bckx6uLboOYlaxgQ .image-shape .label,#mermaid-svg-Bckx6uLboOYlaxgQ .icon-shape .label{text-align:center;}#mermaid-svg-Bckx6uLboOYlaxgQ .node.clickable{cursor:pointer;}#mermaid-svg-Bckx6uLboOYlaxgQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bckx6uLboOYlaxgQ .arrowheadPath{fill:#333333;}#mermaid-svg-Bckx6uLboOYlaxgQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bckx6uLboOYlaxgQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bckx6uLboOYlaxgQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bckx6uLboOYlaxgQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bckx6uLboOYlaxgQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bckx6uLboOYlaxgQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster text{fill:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ .cluster span{color:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bckx6uLboOYlaxgQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bckx6uLboOYlaxgQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bckx6uLboOYlaxgQ .icon-shape,#mermaid-svg-Bckx6uLboOYlaxgQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bckx6uLboOYlaxgQ .icon-shape p,#mermaid-svg-Bckx6uLboOYlaxgQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bckx6uLboOYlaxgQ .icon-shape .label rect,#mermaid-svg-Bckx6uLboOYlaxgQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bckx6uLboOYlaxgQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bckx6uLboOYlaxgQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bckx6uLboOYlaxgQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用数据目录 %LOCALAPPDATA%\\dsh-desktop\\
内置运行时 (随 exe 分发)
dsh-desktop (Go + Wails v3)
http://127.0.0.1:<随机端口>
WebView2 窗口
DeepSeek Harness Web UI
Harness 子进程生命周期
启动 / 就绪检查 / 重启 / 关闭
Windows Job Object
强杀兜底回收进程树
runtime/node/node.exe
runtime/node_modules/@deepseek-ai/dsh/...
launch-root/
profiles / sessions / plugins
logs/dsh-desktop.log
启动流程
- 解析端口 :
--port参数 >DSH_PORT环境变量 > 自动挑选空闲端口。 - 清理残留:先停止上一次启动可能遗留的自身子进程。
- 探测复用:若服务已在运行(如用户手动启动过),直接复用,不再重复拉起。
- 启动子进程 :优先用内置
node.exe运行预置的 dsh 入口(离线模式);未预置时回退到npx在线安装。 - 就绪轮询:以 400ms 间隔轮询 HTTP 端点,直至就绪、子进程退出或超时(180 秒)。
- 导航窗口:就绪后把 WebView2 窗口指向本地地址;失败则展示错误页并附日志路径。
进程回收机制
子进程启动后立即被挂入一个 KILL_ON_JOB_CLOSE 的 Job Object:
- 正常退出路径:调用
taskkill /F /T终止整棵进程树; - 异常退出路径(强杀主进程):Job 句柄随主进程关闭,系统自动终止 Job 内所有进程。
双保险设计保证了无论哪种退出方式,都不会留下孤立的后台服务。
四、技术选型与依赖
| 组件 | 角色 | 版本 |
|---|---|---|
| Go | 宿主程序开发语言 | 1.21+(go.mod 为 1.26.4) |
| Wails v3 | 桌面窗口与 WebView2 封装 | v3.0.0-beta.8 |
golang.org/x/sys |
Windows Job Object / 进程 API | v0.46.0 |
| Node.js | 内置运行时(仅构建时下载) | 24.16.0 |
| DeepSeek Harness | 上游 agent 运行时与 Web UI | 0.1.0-rc.6 |
| WebView2 Runtime | 目标机器运行依赖 | Windows 10/11 已内置 |
值得注意的是,Wails v3 使用纯 Go 加载器加载 WebView2,构建与运行均无需 gcc / CGO,大大降低了 Windows 下的交叉编译门槛。
五、构建与分发
构建命令
powershell
cd dsh-desktop
.\build.ps1 # 安装依赖 + 编译 + 打包 node 与 dsh runtime
.\build.ps1 -Version 1.2.3 # 指定版本号
.\build.ps1 -DshVersion 0.1.0-rc.6 # 指定 dsh 版本
.\build.ps1 -NodeVersion 24.16.0 # 指定内置 Node 版本
.\build.ps1 -UpdateRepo "owner/repo" # 启用 GitHub Releases 版本检测
.\build.ps1 -SkipDsh # 跳过 npm 安装(.dsh-runtime 已存在时)
构建脚本依次完成四件事:
- 安装 dsh 依赖 :
npm install到本地.dsh-runtime; - 编译主程序 :
go build,通过-ldflags注入版本号与更新源,并使用-H=windowsgui -s -w生成无窗口的精简 GUI 二进制; - 镜像运行时 :把
.dsh-runtime\node_modules同步到bin\runtime; - 内置 Node :下载并解压 Node Windows 压缩包,仅保留
node.exe。
构建产物
bin/
├── dsh-desktop.exe # 主程序(约 12 MB)
└── runtime/
├── node/ # 内置 Node 运行时(node.exe,约 92 MB)
└── node_modules/ # 预装的 dsh 依赖(约 250 MB)
分发方式
将 dsh-desktop.exe 与 runtime/ 置于同一目录 ,打包为 zip 或安装器(总计约 350 MB)即可分发。最终用户双击 dsh-desktop.exe 即用,首次进入后在 Settings → Models 配置 DeepSeek API Key,并用 Choose workspace 选择项目目录。
六、配置与数据
端口配置(优先级从高到低)
- 命令行参数:
dsh-desktop.exe --port 3090 - 环境变量:
$env:DSH_PORT="3090" - 默认:随机空闲端口
数据与日志
| 内容 | 路径 |
|---|---|
| 用户数据(profiles / sessions / plugins) | %LOCALAPPDATA%\dsh-desktop\launch-root\ |
| 运行日志 | %LOCALAPPDATA%\dsh-desktop\logs\dsh-desktop.log |
菜单项 Harness → Restart Harness 用于重启 dsh 服务,Harness → Open Log 用记事本打开运行日志。
版本检测
- 更新源:GitHub Releases API(
https://api.github.com/repos/{owner}/{repo}/releases/latest) - 注入方式:构建时
-UpdateRepo "owner/repo",留空则禁用 - 检查节奏:启动后 15~30 秒首次检查,此后每 6 小时一次,另有手动检查入口
- 版本比较:对形如
1.2.3的语义化版本逐段比较,忽略预发布后缀
七、健壮性设计
dsh-desktop 在"把上游 CLI 变成可靠桌面产品"这件事上,有几处值得称道的工程细节:
- 先探测、后启动:启动前先探测目标端口是否已有服务,避免重复拉起;同时先清理可能残留的旧子进程。
- 回退机制 :内置运行时缺失时自动回退到
npx在线安装,开发与分发两种形态都能工作。 - 优雅 + 强制双层回收 :
taskkill正常回收与 Job Object 兜底并存,杜绝进程泄漏。 - 错误可观测:启动失败会展示目标地址与日志路径,日志文件贯穿子进程整个生命周期,排错有据可查。
八、局限与展望
- 平台限制:当前仅面向 Windows(依赖 WebView2 与 Windows Job Object)。
- 上游不稳定 :DeepSeek Harness 处于开发者预览阶段、快速迭代,锁定版本
0.1.0-rc.6,可能出现兼容性破坏的变更。 - 体积偏大:内置 Node 与依赖合计约 350 MB,对轻量分发场景不够友好。
未来的可演进方向包括:跨平台支持(Linux / macOS 对应 API)、增量更新与自更新能力、安装器(MSIX / NSIS)打包、以及更细粒度的端口与进程诊断面板。
九、小结
dsh-desktop 是一个小而完整的工程范例:它没有重复造轮子去实现 agent 能力,而是精准地解决了"如何把 CLI 工具交付为桌面产品"这一实际问题------离线运行时、进程托管、数据隔离、更新引导。通过 Go + Wails v3 的轻量组合,它在约 12 MB 的主程序体积内,为 DeepSeek Harness 提供了稳健、可分发、零门槛的桌面载体。
项目地址:https://github.com/ydhcxh/dsh-desktop
项目许可证:MIT(本项目);DeepSeek Harness 及其依赖遵循各自的上游许可证。