为什么要把 Harness 包成桌面端
DeepSeek Harness 的 Web UI 默认跑在浏览器里,对开发者来说够用,但离"产品化"还差口气。每次开终端、输命令、等端口,流程不够顺畅;而且浏览器标签一多,Harness 很容易淹没在一堆页面里。
用 Tauri 2 把它包成独立 exe,本质上做三件事:自动托管服务生命周期、提供原生窗口体验、让配置外置可定制。这篇文章基于社区已有的开源实现,拆解这套方案的核心机制,如果你也想把类似的 Web 工具桌面化,可以直接参考这个思路。
三大核心机制拆解
自动探测与按需启停
桌面端最大的价值是让"服务有没有启动"这件事对用户透明。实现上,Rust 侧在应用启动时先尝试连接127.0.0.1:3080:
- 探测成功:说明 Harness 服务已在运行,直接加载 Web UI,退出时不碰现有服务
- 探测失败:后台自动拉起服务进程,等就绪后再加载;退出时只停掉由自己启动的服务,避免误伤手动开启的实例
这个设计很克制,没有粗暴地"启动时全拉起来、退出时全杀掉",而是区分了服务来源。对开发者来说,这意味着你可以先用pnpm dsh web跑起服务,再开桌面端,两者能和平共处。
探测逻辑的核心是轮询检查端口连通性,配合超时控制。超时时间、探测地址、启动命令全部外置到配置文件,程序本身不硬编码任何路径。
harness.json 外置配置
配置文件放在 exe 同级目录,典型内容如下:
{ "url": "http://127.0.0.1:3080", "probeAddr": "127.0.0.1:3080", "workingDir": "D:/deepseek_harness/deepseek-harness-master", "command": "node", "args": ["--import", "tsx/esm", "apps/cli/src/bin.ts", "web"], "startupTimeoutSecs": 60 }几个关键设计点:
- 路径用
/或\\都可以,JSON 里反斜杠记得转义 - 打包时通过
bundle.resources把 harness.json 打进安装包,装好后用户直接改安装目录里的文件即可生效,无需重新编译 - 项目根目录的 harness.json 是"源文件",改了要重新
tauri build才会进安装包
找不到配置文件时,程序会回退到内置默认值,保证开箱可用。这种"外置优先、内置兜底"的策略,既方便了终端用户自定义,也不让开发者担心配置丢失导致无法启动。
窗口与视觉定制
默认窗口尺寸 1280×800,支持缩放。需要调整的话,改src-tauri/src/lib.rs里的inner_size和min_inner_size即可。
图标替换也很直接:把src-tauri/icons/下的 png 和 ico 文件换掉,或者用npm run tauri icon 图标.png自动生成全套标准尺寸。应用名、版本号、包标识符则在tauri.conf.json里配置。
Rust 侧的 probe 逻辑实现
虽然具体源码没完全展开,但从工程结构可以推断 probe 的实现脉络。src-tauri/src/lib.rs承担核心 orchestration 职责:读取配置 → 异步探测端口 → 条件启动子进程 → 创建窗口加载 URL。
关键的状态管理在于区分"谁启动的服务"。桌面端需要维护一个标志位,记录当前实例是否"拥有"这个服务进程,这决定了退出时是否要执行清理。Tauri 的命令系统(Command Pattern)让前端可以调用 Rust 函数,但 probe 和启停逻辑更适合放在后台自动完成,避免用户感知到延迟。
超时处理用到了异步 Rust 的tokio::time::timeout,如果startupTimeoutSecs内服务未就绪,应该给用户明确的错误提示,而不是让窗口一直空白加载。
打包产物与平台差异
构建完成后,产物在src-tauri/target/release/bundle/下:
| 格式 | 说明 | 适用场景 |
|---|---|---|
NSIS (*-setup.exe) | 安装向导,支持自定义安装路径 | 推荐分发给终端用户 |
| MSI | Windows Installer 标准格式 | 企业部署、组策略分发 |
首次构建需要下载 Rust 依赖并编译,耗时约 5–15 分钟,取决于网络和机器性能。建议提前配置好 cargo 的 USTC 镜像和 npm 的 npmmirror 镜像,避免卡在下载阶段。
macOS 打包需要额外注意两点:
- 必须在 macOS 本机执行构建,无法跨平台编译
- 需要准备
icon.icns格式的图标,可以用iconutil或在线工具从 png 转换
如果暂时不需要 macOS 版本,把tauri.conf.json里的bundle.targets改为["nsis"]可以缩短构建时间。
Tauri 2 vs Electron:体积与启动速度
把 Web UI 包成桌面端,Electron 是最先想到的选择。但在这个场景下,Tauri 2 有几个明显优势:
体积方面,Tauri 2 的应用体积显著更小。Electron 每个应用都自带 Chromium 和 Node.js,基础体积就过百兆;Tauri 2 用系统 WebView(Windows 上基于 Edge WebView2),Rust 编译后的二进制本身很紧凑,最终安装包可以控制在 10MB 级别。
启动速度,Tauri 2 冷启动更快。Electron 需要初始化完整的 Chromium 进程,而 Tauri 2 的 Rust 主进程轻量得多,窗口可以更快出现。对于"先探测服务、再加载页面"这种有依赖链路的场景,主进程启动快意味着整个流程的感知延迟更低。
内存占用,运行时的差异更明显。Electron 的内存基准线较高,开几个窗口就容易吃满内存;Tauri 2 的 Rust 进程内存占用更可控,长时间运行更稳定。
当然,Electron 的生态系统更成熟,调试工具更丰富。但如果你的需求就是"把 Web UI 装进原生窗口,再加一些系统级能力(文件读写、进程管理)",Tauri 2 的精简架构反而更贴合,不需要为用不到的 Chromium 功能买单。
实际构建中的踩坑记录
Node.js 版本:Harness 本身要求 v22.19+,构建桌面端前务必确认。版本不对会导致服务起不来,但错误提示可能藏在子进程输出里,需要手动在终端跑一遍启动命令才能看到。
路径问题:Windows 上workingDir如果用了未转义的反斜杠,JSON 解析会直接失败。建议统一用正斜杠,或者确保双反斜杠转义。
端口占用:3080 被占用时,桌面端探测会认为"服务已运行",但加载的可能是另一个 Harness 实例或其他应用。目前配置里没有端口冲突的检测逻辑,如果多人共用机器,可能需要手动协调。
开发模式与生产模式的配置差异:npm run tauri dev默认用内置配置,想在调试时测试自定义的 harness.json,需要把文件放到调试 exe 旁边。这个细节文档里没强调,容易让人误以为改了项目根目录的配置就能在 dev 模式生效。
这套方案的边界与适用场景
这个 Tauri 2 封装层最适合两类人:一是想把 Harness 作为内部工具固定下来的技术团队,二是基于 Harness 做二次产品化的开发者。它解决的不是"让 Harness 功能更强",而是"让 Harness 用起来更顺"。
如果你的需求更复杂——比如要离线运行模型、要深度改造 UI、要集成更多原生能力——那可能需要直接改动 Harness 源码,而不是在外面包一层。但对于"Web UI 桌面化"这个经典需求,这套实现已经相当完整,从自动服务管理到跨平台打包都覆盖到了。
目前项目还是社区驱动,v0.1 级别的 Harness 本身也在快速迭代。建议把 harness.json 的自定义配置和 Tauri 的打包流程写成内部文档,方便团队新人上手,也避免每次更新后重新踩一遍环境的坑。