我用 Tauri 给命令行工具 dsh 做了个桌面客户端,顺手聊聊踩过的坑

0 阅读4分钟

起因:一个需要天天开终端的工具

最近在用 DeepSeek Harnessdsh),它自带一个 dsh web 命令,能把命令行的能力用浏览器 UI 跑起来。功能是挺好用的,但每次用都得:

  1. 打开终端
  2. 记住并敲 dsh web 这条命令
  3. 等它打印出一个 localhost:xxxx 的端口
  4. 手动复制到浏览器打开
  5. 关闭的时候还得回终端 Ctrl+C,一不小心忘了关,端口和进程就一直占着

说实话,作为一个"应用",这套体验对非常规用户不太友好。于是我干脆用 Tauri 把这套流程封装成了一个原生桌面客户端 —— dsh desktop,双击图标就能用,托盘常驻,进程自动回收。

官网:dsh-desktop.cc.cd/ | 仓库:github.com/MochiNek0/d…

thumbnail.png

这货是干嘛的

一句话:启动即自动拉起本地 dsh web 服务,并把它内嵌到一个原生窗口里,用户全程不需要碰终端。

  • 自动检测系统里的 Node.js 和 dsh 环境,没有的话自动引导装好,不需要管理员权限
  • 动态端口分配,不会跟你手动在终端里开的 dsh web 打架,两边互不干扰
  • 会话记录、凭证、配置全部复用 CLI 全局的 $DSH_HOME(默认 ~/.dsh),桌面端和命令行是同一份数据,不会出现"终端里登录了桌面端还要再登一次"的割裂感
  • 内置一个可视化插件面板,装卸 dsh 插件不用再记 pnpm 命令;同时也留了一个"打开终端"的入口,环境变量都配置好了,方便喜欢敲命令的人
  • 菜单里还可以直接查看当前运行环境、一键原地更新 dsh 本体,服务出问题时也能主动点「重启 dsh」,不用干等它自己崩溃恢复
  • 支持 Windows / macOS / Linux,托盘图标、系统主题跟随、中英文双语、开机自启都安排上了

技术上做了哪些事

1. 子进程托管与生命周期管理

核心难点不是"启动一个子进程",而是保证子进程的生命周期严格跟着主进程走。用 Tauri 的 Rust 后端去 spawn dsh web,需要处理几件事:

  • 应用正常退出、被系统强杀、崩溃等各种退出路径下,都要把子进程一起清理掉,不能留下孤儿进程占用端口。Windows 上用 Job Object 兜底强杀场景,Unix 上则是进程组 + 信号
  • dsh web 自己意外退出了(比如环境变量被改坏、端口被占用),前端要能感知到并给用户一个"重启"按钮,而不是直接卡死在白屏;同时菜单里也留了一个主动重启入口,不用等它先崩溃
  • 全程只允许单实例运行,二次启动直接聚焦到已存在的窗口,避免同一个 $DSH_HOME 被两个进程同时读写

2. 动态端口 + 内嵌 WebView 的组合

dsh web 本身支持随机端口启动(--port 0),桌面端拿到端口号之后再把 Tauri 的 WebView 导航过去,中间有个短暂的"加载中"状态(dist/index.html 专门做了这个等待页和报错兜底页),避免用户盯着一个空白窗口。

3. 系统通知的桥接

dsh 页面本身会调用浏览器的 Notification API 来提醒"某个任务跑完了",但这个 API 在 WebView 里其实是个空壳,没有权限弹窗也没有真正的系统通知渲染。所以做法是往每个加载的文档里注入一个 shim,把 window.Notification 整个替换掉,拦截页面发出的通知请求,转成 Rust 侧的真实系统通知——而且当窗口本身正处于前台可见状态时会自动静音,避免重复打扰。这样任何用标准 API 写通知的插件,不需要知道桌面端的存在就能被覆盖到。

4. 环境探测与免管理员权限安装

跨平台最麻烦的部分其实是"怎么在用户机器上找到/装好 Node.js 和 dsh",还得保证不需要 sudo/管理员权限,不能对系统全局 PATH 造成污染。这部分逻辑单独拆了一批 scripts/ 脚本来做依赖初始化和运行时打包,构建产物直接输出到 src-tauri/target/release/bundle/

5. 自动更新

桌面端接入了 Tauri 的自更新机制,Windows/macOS 都能弹窗检查更新一键安装,下载进度会实时显示在标题栏;Linux 目前只支持 AppImage 格式的自更新(.deb 用户还是得手动重装)。这套跟"更新 dsh 本体"是两条独立的更新链路:一个更新的是这个壳应用自己,一个更新的是里面跑的 CLI 内核。

踩过的坑(简单列几个)

  • Linux 上的 AppImage 权限问题:不同发行版对 AppImage 的默认执行权限处理不一样,Debian 系验证下来是稳的,其它发行版还在收集反馈。
  • macOS 的 Gatekeeper 隔离标记:没签名分发的应用首次打开会被拦截,目前的临时方案是在 README 里放了 xattr -dr com.apple.quarantine 的命令,长期还是得考虑签名证书。
  • 端口冲突:一开始用固定端口,结果只要用户在终端里手动开过一次 dsh web,桌面端就直接起不来。后来改成动态端口分配,两边共存互不干扰。
  • Tauri 默认接管了 WebView2 的拖放:导致页面里靠 HTML5 拖拽排序的功能(比如插件设置卡片)在桌面端完全拖不动,浏览器里却一切正常。后来发现是 Tauri 把系统级文件拖放事件抢先处理了,关掉这个接管就好——应用本身也用不到文件拖放,没有任何损失。

写在最后

这是个纯粹因为"自己用着不爽所以动手改"的第三方项目,跟 DeepSeek 官方没有从属关系。如果你也在用 dsh,或者对 Tauri 做本地服务托管型客户端感兴趣,欢迎来看看代码,提 Issue 或者 PR 都欢迎:

技术栈:Tauri v2 + Rust + Node.js,MIT 协议开源。