起因:一个需要天天开终端的工具
最近在用 DeepSeek Harness(dsh),它自带一个 dsh web 命令,能把命令行的能力用浏览器 UI 跑起来。功能是挺好用的,但每次用都得:
- 打开终端
- 记住并敲
dsh web这条命令 - 等它打印出一个
localhost:xxxx的端口 - 手动复制到浏览器打开
- 关闭的时候还得回终端
Ctrl+C,一不小心忘了关,端口和进程就一直占着
说实话,作为一个"应用",这套体验对非常规用户不太友好。于是我干脆用 Tauri 把这套流程封装成了一个原生桌面客户端 —— dsh desktop,双击图标就能用,托盘常驻,进程自动回收。
这货是干嘛的
一句话:启动即自动拉起本地 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 都欢迎:
- GitHub:github.com/MochiNek0/d…
- 官网下载:dsh-desktop.cc.cd/
技术栈:Tauri v2 + Rust + Node.js,MIT 协议开源。