乐于分享
好东西不私藏

把命令行 AI 工具变成"双击即用"的桌面 App:DeepSeek Harness 桌面化全记录

把命令行 AI 工具变成"双击即用"的桌面 App:DeepSeek Harness 桌面化全记录

从一个"不想每次敲命令"的小需求开始,一路做到 MIT 开源 + GitHub Release。本文完整记录了这个 Windows 桌面应用的诞生过程、架构设计,以及三个真正有意思的技术问题。

一、为什么要做它

DeepSeek Harness 是一个很能打的 AI 助手工作台(会话持久化、子代理、目标循环、插件体系……),但它的标准启动方式是:

cmd> npx @deepseek-ai/dsh web然后自己打开浏览器访问 http://127.0.0.1:3080

对习惯图形界面的用户来说,这已经是一道门槛。于是我给自己定了个目标:把它封装成一个 Windows 桌面应用,双击图标,其余全部自动发生。

最终交付的体验:

  • 点任务栏的小鲸鱼图标 → 服务器无窗口后台启动 → 独立应用窗口自动弹出

  • 窗口最小化后再点图标 → 直接还原到前台继续用(不会开第二个窗口)

  • 关闭窗口 → 后台服务进程整树停止,零残留

  • 任务栏自始至终只有一个图标,没有黑窗口、没有浏览器标签栏

二、迭代之路:每个版本都在解决上一个版本的痛点

这个项目是需求一层层推出来的,复盘一下很有代表性:

版本	方案	暴露出的新问题v1	.cmd 脚本 + 快捷方式	黑窗口、要手动开浏览器v2	csc 编译的 exe 启动器	系统 URL 关联打开会静默失败,浏览器不弹v3	显式启动 Edge + --app 独立窗口	无标签栏了,但任务栏多出一个 Edge 图标v4	WebView2 原生窗口(终局)	—

关键转折在 v3 → v4:Edge 的 --app 窗口属于 Edge 的进程,所以任务栏必然出现独立图标。换成 WebView2(Edge 内核嵌入我们自己的 exe 窗口)后,窗口归属自己的进程,Windows 自动把任务栏按钮归并到固定的鲸鱼图标上。一句话总结:窗口是谁的进程,图标就归谁。

三、架构:四层设计

系统采用四层架构:

  • 呈现层:WebView2(Chromium 内核)渲染本地服务页面 127.0.0.1:3080,独立窗口呈现,无标签栏,使用鲸鱼图标。

  • 服务层:负责 dsh 服务进程的生命周期管理,无窗口后台启动 npx dsh web,关闭窗口时以 taskkill /T /F 整树停止,且仅终止自身启动的实例。

  • 单实例层:命名 Mutex 互斥锁配合 EnumWindows 窗口枚举,重复点击图标即窗口还原置顶。

  • 自愈层:就绪探测、启动监视、自动重载三级机制,用于应对启动时序竞态。

进程所有权语义值得一提:应用只在"服务器是我启动的"时才在关窗时停止它;如果启动时发现端口已被占用(比如用户还开着旧实例),就只开窗口、不接管、更不误杀。

四、三个真正有意思的技术问题

1. "Failed to load plugins":一个隐蔽的启动竞态

最早一版 WebView2 上线后,首次启动时常看到满屏报错:38 个客户端插件 pending,全部在等 connection / remote 服务。

排查发现根因很"巧合":dsh 客户端的启动是一次性的——如果页面在服务器完全初始化之前加载,连接握手失败,就会停在错误页且永远不会自动恢复(源码注释原文:"failures stay here")。而我的应用偏偏在端口刚通的瞬间就导航了。

解法是三层防线:

  1. 就绪探测:TCP 端口通了不够,GET / 必须返回 200 才导航;

  2. 启动监视:每 4 秒检查页面启动屏状态,发现失败屏 → 自动 Reload()(最多 8 次);卡加载屏超 60 秒也重载;

  3. 兜底:8 次失败后弹窗提示并退回 Edge 独立窗口。

两个细节:

  1. 检测用 CSS 类选择器(_failedTitle_*)而不是文本匹配——因为用户可能把报错文字粘贴进聊天记录,文本匹配会假阳性;

  2. 类名带编译期哈希、随前端版本变化,于是应用启动时从服务器自己的样式表实时提取(遍历 index.html 引用的每个 CSS 逐个查找,内置哈希兜底)——dsh 前端随便升级都免维护。

// 每 4 秒执行一次(类名来自运行时提取,兜底值为编译期常量)var script = "(function(){if(document.querySelector('." + bootFailedClass +"'))return 'failed';if(document.querySelector('." + bootContainerClass +"'))return 'loading';return 'app';})()";

2. 单实例 + 窗口还原,而不是"再开一个"

用户的核心诉求之一是:最小化之后,点图标应该像真 App 一样把窗口拉回来。实现:

命名 Mutex 保证全局只有一个实例;

第二个实例启动时,EnumWindows 按进程名过滤(并跳过 ConsoleWindowClass 排除控制台干扰),找到既有窗口后 ShowWindow(SW_RESTORE) + SetForegroundWindow。

3. 从 SVG 到 ICO:零依赖的图标光栅化管线

官方 favicon 是一个 50×50 的纯 SVG 路径。要把它变成 exe 图标(多尺寸 ICO),而机器上没有任何图像库,于是手写了一整条管线:

SVG 路径解析 → 自适应 De Casteljau 贝塞尔细分 → 扫描线非零环绕填充 → 4× 超采样抗锯齿 → 手写 PNG 编码器(zlib + CRC32)→ ICO 容器打包(256/128/64/48/32/24/16 七档)。

验证方式也很硬核:用合成图形做像素级测试(正方形覆盖 1.0、环形 0.64、同向嵌套 1.0 全部精确命中)、跨尺寸面积积分一致、从编译后的 exe 提取图标逐像素比对 1024/1024 = 100%。

五、工程化:从"能用"到"可开源"

一个能对外发布的项目,和"自己机器上能跑"的差别就在这些事上:

  1.  MIT 许可证(没有许可证的公开代码不叫开源)+ NOTICE 第三方声明(鲸鱼图标商标归属 DeepSeek、WebView2 组件归属微软)

  2.  中英双语文档:功能 / 构建 / 使用 / 给他人使用 / 实现要点 / FAQ

  3.  零依赖构建链:Windows 自带的 csc.exe(.NET Framework 4.8,系统预装)+ 纯 Node 脚本——不用装任何 SDK,一条 build.cmd 出产物

  4.  GitHub Release:v1.0.1 附带成品 zip,下载解压即用

  5.  可移植:无写死路径,服务器工作目录默认 exe 所在文件夹(DSH_WORKDIR 可覆盖),日志统一在 %LOCALAPPDATA%\DeepSeekHarness\logs\

六、局限与未来计划

诚实交代边界:

  1. 仅 Windows,依赖 WebView2 Runtime(Win11 自带)与 Node.js;

  2. 是 dsh 的"壳",使用者需要自己的 DeepSeek API Key;

  3. 启动屏检测耦合前端 DOM/CSS——好在有运行时提取 + 兜底哈希 + 重载 + Edge 兜底四层保险。

七、写在最后

这个项目最让我满意的一点:它很小(核心源码一个文件约 450 行),但每个环节都被真实需求推到"必须认真做"的地步——启动竞态、进程归属、图标渲染、开源合规,没有一个是炫技,全是踩坑踩出来的。

开源地址:https://github.com/xu15259756825-bit/deepseek-harness-app 

成品下载:https://github.com/xu15259756825-bit/deepseek-harness-app/releases

欢迎 Star、Issue 和 PR。

数城智安安全实验室