本系列记录 DeepSeek Harness 的本地部署与周边工具折腾。从最基础的安装配置,到把它做成 Windows 后台服务、桌面端应用,一步步把 AI Agent 变成日常真正在用的东西。
前两篇里,我把 DeepSeek Harness(后面简称 DSH)部署到了本地,又用 NSSM 把它注册成了 Windows 后台服务。如果有些小伙伴这从这篇开始看的,可以点击下方两篇文章链接,
这样确实不用每次开终端了,但用起来还有个不大不小的问题:每次都得打开浏览器、找到书签、点进去。
时间长了就有点烦。DSH 对我来说已经不是一个"偶尔用一下的网站",而是每天都要开的工作台。既然是工作台,它就应该像其他软件一样——桌面上有个图标,双击就打开。
所以就动手做了这个:DSH Desktop,把 DSH 的 Web 界面封装成一个真正的 Windows 桌面应用。
做完之后我把代码开源了,仓库在 GitHub 上。这篇文章把整个过程记下来,包括为什么这么设计、踩过哪些坑,下面就是我的开源项目地址,记得帮我点个小星星哟,最好 fork 到本地,我好每次更新的时候,可以第一时间更新到。
dsh-desktop
666su • Updated Sep 27, 2026
一、先说结论:它长什么样
一句话概括:双击直接进 DSH 界面,点 × 不退出而是缩到托盘,后端继续跑。
它不是一个"启动页"或者"按钮页"——很多同类小工具做出来是这样:打开先看到一个窗口,上面几个按钮,点一下才用浏览器打开目标网站。那种东西其实没什么意义,因为最终还是回到浏览器。
这个应用打开就是 DSH 本体。

还有个细节我挺满意:点右上角的 × 不会退出程序,而是缩到系统托盘继续运行。
因为 DSH 的后端是要一直跑着的,如果点 × 就把整个进程干掉,那下次打开又得等它重新起来。所以这里的 × 相当于"最小化到托盘"。
二、为什么不用 Electron
做桌面端,第一反应肯定是 Electron。但这次我特意没用,原因是太重了。
Electron 的方案是自带一整套 Chromium,编译出来就是一百多兆,运行时还会拉起六到八个进程。对于一个"只是想把网页装进窗口"的需求来说,这个代价有点大。
Windows 自己其实就有 WebView2 运行时,是系统级的共享组件,Win11 直接自带。用它来做外壳,程序只需要一个 exe。
方案 | 进程数 | 体积 |
Electron | 6–8 个 + 自带约 150 MB Chromium | 最重 |
本项目(WebView2) | 1 个 exe + 5–6 个子进程 | exe 约 400 KB |
最后编译出来的 exe 是 405 KB 左右,加上三个必需的 DLL 也就一兆出头。这个体积差距还是很直观的。

顺便澄清一个特别常见的误解:WebView2 不是 Edge 浏览器。它是 Windows 的独立共享运行时,进程名叫 msedgewebview2.exe,运行时不会启动 msedge.exe。
这个误解挺普遍的,我一开始也以为用了 WebView2 就等于在后台开了个 Edge。
三、核心功能
除了前面说的双击进入和托盘常驻,还有几个我觉得必要的:
单实例。重复双击 exe 不会开出第二个窗口,而是把已经在托盘里的那个唤到前台。Windows 上做单实例的常规做法是命名互斥量(Mutex)加一个命名事件,前者判断有没有实例,后者负责"喂,出来一下"。
`--hidden` 启动参数。加上这个参数启动时直接躺进托盘,不弹窗。配合开机自启特别合适——开机之后它安安静静待在那儿,你想用的时候点一下托盘图标就行。
托盘菜单。左键点图标恢复窗口,右键出菜单。菜单里除了显示主窗口和退出,还能配置两个自定义入口:一个是"打开 web 页面",一个是"打开监控平台"。这两个地址都由配置文件给出,留空就不显示这一项。

这里说一下域名的问题。因为我的 DSH 是部署在自己的域名上的(现在换成了桌面端,但我还保留了从托盘直接打开线上地址的入口),所以配置文件里可以填任意地址:
开源的时候我把这两个默认值清空了。原因很简单:这是我自己内网的地址,写进开源仓库既没意义也不合适。模板里留空,使用者填自己的就行。

dsh-desktop
666su • Updated Sep 27, 2026
四、自愈机制
这部分是我花时间最多的地方,也是整个项目里最有价值的一块。
起因是一个很烦的现象:桌面端放一会儿就黑屏,点按钮没反应。
一开始我以为是 WebView2 的问题,排查了半天才找到根因——DSH 每次重启都会换一个新的访问 token。
DSH 的 Web 界面在启动时会打印一条带 token 的 URL:
这个 token 每次进程启动都不一样(我实测连续启动八次,八个不同的值)。而桌面端里那套前端还拿着旧的 token,于是认证失败,页面就成了一个点不动的空壳。
知道了原因,解决方案就很直接了:盯着 DSH 的日志文件,发现 token 变了就带着新 token 自动重载。
围绕这个问题,我最后做了四层防护:
机制 | 触发条件 | 行为 |
token 跟随 | 日志里的 token 与当前页面不同 | 自动带新 token 重载 |
崩溃重建 | WebView2 渲染进程崩溃或浏览器进程退出 | 自动重载,必要时重建内核 |
UI 看门狗 | UI 线程连续无响应 | 进程级重启自身 |
孤儿清理 | 启动时发现有宿主已死的 WebView2 进程 | 结束它们,避免抢占用户数据目录 |
最后一条"孤儿清理"值得单独说一下。WebView2 会用一个用户数据目录,如果上一轮崩溃留下了没退干净的 msedgewebview2.exe 进程,它们会一直占着这个目录,导致新实例起不来。所以启动时先扫一遍、把宿主已经死掉的那些清理掉。
另外还有个小的体验优化:如果导航本身就失败了(比如 DSH 还没启动完),6 秒后会自动重试一次。
桌面端的日志文件,能看到 core ready 和 nav done 这些记录,这里有两种方法,
法一:
快捷打开方式:Win+R 粘贴下面这行回车

法二:
直接在 C 盘里找,类似我的路径
打开以后就是这样,通过这里面来查自己的部署的有什么问题。

五、免登录是怎么做到的
既然能读到日志里的 token URL,那就顺手把免登录也做了。
原理是这样的:打开带 token 的那条 URL,DSH 会种下一个持久签名 cookie,签名密钥存在本地的 .credentials.yaml 里。这个 cookie 是跨重启有效的。
所以只要配置文件里的 logFile 指向 DSH 日志,应用启动时就会自动取最后一条 token URL 去加载,完全不用手动登录。
不过这里有个小坑:读日志的时候必须用共享读写的模式打开,因为 DSH 还在往这个文件里写东西,用独占模式会直接失败。
还有个限制:cookie 虽然跨重启有效,但换一台机器或者换一个目录就失效了——因为签名密钥跟着走。
六、构建:不用装 .NET SDK
这个项目在构建上的目标是:一台什么都没装的 Windows 也能编译。
常见的做法是装 Visual Studio 或者 .NET SDK,但那太重了。Windows 其实自带 .NET Framework 4.x,里面就包含命令行编译器:
不过它是 C# 5 编译器,版本比较老,所以写代码的时候要避开这些语法:
- 字符串插值
$"..."(C# 6 才有)
nameof、?.这类空引用操作符(C# 6)
- 元组、模式匹配(C# 7 以后)
好消息是
async/await、var、lambda 这些在 C# 5 里就有了,日常写完全够用。代价是没有 MSBuild 也没有 NuGet 客户端,所以 WebView2 的 SDK 得手动下载。好在 nuget.org 提供了直链,.nupkg 本质上就是个 zip,解开之后只需要里面三个文件。
整个构建就是一条命令:
脚本会自动下载 SDK(如果本地没有)、解压出需要的 DLL,然后调用系统自带的编译器完成构建。
可以在 powershell 里面进行运行,powershell 不需要每行运行,是按照顺序运行的,所以直接点击复制就好了。

七、踩坑记录
这个项目踩的坑还挺多的,我把值得记的整理出来,详细的都写在仓库的 docs/BUILD.md 里。
坑一:路径里有空格,编译参数会被拆错
这个是花了最久才定位的。如果项目路径里有空格(比如放在
D:\My Projects\ 下面),编译就会报:原因是 PowerShell 5.1 在把参数传给原生程序时,不会给含空格的元素加引号,所以编译器收到的是被空格切碎的两段。
解法不是加转义,而是避免拼绝对路径:先把源文件复制到输出目录,切换过去之后全部用相对文件名编译。
坑二:含中文的 PowerShell 脚本必须存成带 BOM 的 UTF-8
这个坑特别隐蔽。PowerShell 5.1 读取 .ps1 文件时,默认是按系统 ANSI 代码页解码的(中文系统就是 GBK),而不是 UTF-8。
如果脚本是无 BOM 的 UTF-8 且含中文,中文就会被解成乱码,而乱码字节可能吃掉字符串的结束引号,于是报出一堆莫名其妙的错误:
明明报错在
< 上,根因其实是几行前的中文。坑三:原生程序写 stderr 会终止脚本
脚本开头写了
$ErrorActionPreference = 'Stop' 之后,只要原生程序往 stderr 写任何东西,PowerShell 就会把它包装成错误并终止脚本——即使那条信息完全无害、命令也执行成功了。Chrome 无头模式就特别爱写这种信息。而且
2>&1 | Out-Null 挡不住这个行为。解法是调用原生程序前后临时把错误偏好放宽,真正的失败靠退出码或者产物是否存在来判断。
坑四:Windows 图标缓存会骗你
症状很有意思:任务栏的图标是新的,桌面的图标还是旧的。
原因是任务栏直接读 exe 里内嵌的资源,而桌面快捷方式的图标被系统缓存了,并且缓存是按快捷方式路径键控的——就算删掉重建一个同名的 .lnk 也没用。
解法是两个一起上:换一个快捷方式文件名,并且让快捷方式引用独立的 .ico 文件而不是 exe 内嵌图标。
坑五:隐藏过早会让 WebView2 初始化中止
做「启动即进托盘」的时候,如果窗口还没准备好就调用隐藏,WebView2 的初始化会直接抛异常中止。
解法是先设置成最小化避免闪窗,等内核真正就绪之后再去隐藏。
坑六:8.3 短路径会改变进程名
用短路径启动 exe 时,进程名会从 DeepSeekHarness 变成
DEEPSE~1。如果程序里有按进程名做匹配的逻辑,这里就会对不上。正确的做法是按命令行匹配,并且大小写不敏感。八、已知限制
有几点要说清楚,免得用起来有落差。
exe 不等于单进程。DSH 的界面是 HTML/CSS/JS,任何 exe 想显示它都需要一个浏览器引擎。这个项目复用系统的 WebView2,比 Electron 轻很多,但仍然会有五到六个子进程。这是浏览器渲染架构决定的,绕不开。
token 跟随依赖日志。如果 DSH 是前台启动、URL 只打在了控制台上而没写进日志文件,那日志里的 token 就是旧的,这时会自动退回 cookie 认证。logFile 留空则完全不启用 token 跟随,DSH 重启后需要手动点一下刷新。
不接管服务。这个应用只是"一层壳",DSH 本身还是由你自己的服务或者进程提供。所以它不会和你现有的部署方式冲突——这也是我特意这么设计的。

九、和官方桌面版的区别
后来我才发现,DSH 官方仓库里其实已经有一个桌面端了(在
apps/desktop),是 Electron 做的,自带独立的 Python / Node.js / pnpm 发行版,默认端口 19387。那还需要这个项目吗?我觉得看需求:
- 想要开箱即用、功能完整、官方维护的,用官方的。
- 已经在跑 DSH(比如像我一样注册成了 Windows 服务),只想要一个轻量窗口的,用这个。
两者的定位不一样:官方版是"自带一套完整环境",这个是"附着你已有的环境做一层壳"。
十、小结
这个项目从头到尾其实就是解决一个很小的体验问题:DSH 值得有一个桌面图标。
但做下来发现真正费时间的不是"把网页装进窗口"这件事本身,而是让它在长期运行中不出毛病——token 会变、进程会崩、UI 会卡死、上一轮的残留进程会捣乱。这些才是日常使用中真正会遇到的。
对我来说,最有价值的收获是搞清楚了 token 那个问题。之前一直以为是 WebView2 不稳定,其实根因在 DSH 每次重启都换 token,而桌面端的前端并不知道这件事。找到根因之后,解决起来反而很快。
现在这套用下来体验挺稳定的:开机静默进托盘,随手点一下就进工作台,放一整天也不会黑屏。
代码已经开源在 GitHub 上了,用的是 MIT 协议。
如果你也在用 DSH,而且觉得每次开浏览器有点麻烦,可以试试。有问题的欢迎在评论区讨论,或者直接去仓库提 issue。
如果有问题的,可以评论区交流,也可以在 关于我 里面找到我,私信我都是可以的。
之前文章链接
