DeepSeek Harness Hub
← 返回列表

macOS 原生多标签窗口Boy-Grid/deepseek-harness-desktop-for-macos

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

把 Web 界面装进原生窗口,多会话并行且实例自动托管

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/8/26 · 已提供中文文档

DeepSeek Harness Web UI 的非官方原生 macOS 应用——多标签页窗口、驻留 Dock 的实例管理,以及可选的多文件夹工作区。

综合分
28.5
GitHub 分
28.5
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add Boy-Grid/deepseek-harness-desktop-for-macos
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

DSH Desktop for macOS

CI
License: MIT
macOS 14+

English

给 DeepSeek Harness 的 Web UI
一个原生 macOS 窗口:双击启动,在一个窗口里并行操作多个会话,实例的生命周期由应用
自己负责。

非官方项目。 独立开源项目,与 DeepSeek 无隶属关系,也未获其背书。
"DeepSeek" 与 "DeepSeek Harness" 是 DeepSeek 的商标。应用图标派生自 DeepSeek
Harness Web UI 的 favicon,出处见 THIRD_PARTY_NOTICES.md。

为什么做这个

在终端里跑 dsh web、把页面留在浏览器标签里当然能用,只是那个标签是三十个里的一个;
服务比窗口活得久,却没人真的在管它;想同时开几个会话,就得开几个浏览器标签,而它们
共用同一份浏览器存储、互相打架。这个应用把 harness 变成一个正常的桌面程序:

- 一个窗口,多个会话。 标题栏里最多 8 个标签,每个是独立的 WebView、有自己的
存储,全部由同一个实例提供服务。
- 实例归应用管。 需要时启动 dsh,退出时停掉——并且只停自己启动的那个
(见 SECURITY.md)。
- 关窗不等于退出。 实例继续跑,再打开是瞬间的事。从 Dock 退出或 ⌘Q 才会把两者
一起收掉。
- 外链外放。 任何不是本机 harness 的地址都交给系统默认浏览器,不会让窗口跳走。
- 可选的多文件夹工作区,通过一个需要你显式选择的第二后端提供。

系统要求

- macOS 14 或更新(每标签的持久化存储用的是这个版本引入的 API)
- Node.js 与 DeepSeek Harness。本应用两者都不捆绑——磁盘映像只有一兆左右。自行安装:
npm install -g @deepseek-ai/dsh

也可以在找不到时让应用代为下载一份(见托管运行时;这是可选的,而且你
自己装的运行时永远优先)。
- 仅多文件夹后端需要:Node.js 22+ 与 pnpm 11+(或 corepack)

安装

从 Releases
下载磁盘映像,打开后把应用拖进「应用程序」。映像带 Developer ID 签名并已公证,打开时
不会被 Gatekeeper 拦。

建议校验一下下载到的东西:

shasum -a 256 -c SHA256SUMS
spctl --assess --type open --context context:primary-signature -v
期望:accepted / source=Notarized Developer ID

从源码构建

git clone https://github.com/Boy-Grid/deepseek-harness-desktop-for-macos.git
cd deepseek-harness-desktop-for-macos
./build.sh --register

它会拷贝资源、编译 agent、签名,并刷新 LaunchServices。默认输出
~/Applications/DSH Desktop.app;--output  可以构建到别处且不注册。

部署目标取自 Info.plist 的 LSMinimumSystemVersion,构建后会校验二进制的 minos
与之一致。不显式传 -target 的话,swiftc 会把构建机的系统版本写进二进制,于是
这个包在任何比构建机更旧的系统上都起不来——而 plist 看起来还很宽容。

默认构建是 ad-hoc 签名,本地跑够用了。

发布

scripts/make-dmg.sh 从源码一路做到「打开时不会被 Gatekeeper 拦」的磁盘映像:通用
二进制(Apple Silicon 与 Intel)、Developer ID 签名、hardened runtime、安全时间戳、
经 Apple 公证,并把票据装订进映像。

./scripts/make-dmg.sh                  # 构建 --release、打包、公证、装订
./scripts/make-dmg.sh --skip-notarize  # 只出签名映像,用于试跑

公证凭据从钥匙串读,所以不会出现在命令行或仓库里的任何文件中。一次性配置:

xcrun notarytool store-credentials "dsh-desktop-notary" \
--key  --key-id  --issuer

公证只认 App Store Connect 的 Team Key;用 Individual Key 会拿到一个不解释原因的
401。

签名在维护者本机做,不在 CI 里。否则 Developer ID 私钥就得放进仓库 secrets——为了一年
省几次命令,把泄露后的影响面扩大得多。发布 workflow 只负责校验 tag 并开一个 draft,
映像在本地上传上去。

托管运行时

什么都不捆绑,也不会背着你下载任何东西。当找不到 Node.js 或 dsh 时,应用会询问是否
代你取一份放进它自己的目录;也可以直接用命令:

"$L" runtime status      # 取过什么,以及实际会用哪个 node/dsh
"$L" runtime install     # 下载 Node.js 与 dsh
"$L" runtime uninstall   # 删掉它们;DSH 数据不动

你自己装的运行时永远优先。 托管副本是兜底,不是接管——只有一个例外值得知道:mfw
后端要求 Node 22,所以如果你自己的 node 更旧,那个后端会改用托管的,而不是直接失败。
最低版本是查找条件而不是查完再检查,正是为了不让一个过旧的 node 遮蔽掉可用的那个。

代价是实测的,不是估的:

| | |
|---|---|
| 下载量 | 约 50 MB(Node 官方 tarball) |
| 安装后磁盘占用 | 约 470 MB——Node 解压后约 100 MB,dsh 带一百多个依赖包 |
| 耗时 | 5–10 分钟,主要花在 npm |

Node 的 tarball 会先与固定在启动器脚本里的校验和比对,通过后才解压。如果校验和跟
下载来自同一台服务器,那只能证明两者一致;把它固定在脚本里,意味着即使镜像(甚至
nodejs.org 本身)被攻破,代码也过不了这一关。不匹配就丢弃下载内容并拒绝解压。

运行时目录以 700 创建,且不在任何 per-port 状态目录之下——一份副本供所有后端与端口
共用。

两个后端

| 后端 | 启动的东西 |
|---|---|
| stock(默认) | 你自己安装的 dsh |
| mfw | dsh-mfw,它准备一棵打了补丁的运行时,让一个工作区可以包含多个分散的文件夹 |

在 DSH Desktop → 偏好设置(⌘,)里选,或者用命令行:

"$L" start --backend mfw     # 首次会准备约 300 MB 运行时(10–30 秒)

⚠️ 多文件夹后端会扩大 Agent 的写面:工作区内的会话可以写入全部成员文件夹,
而不只是它自己的工作目录。这是安全语义的变化,所以它不是默认值,也绝不会被隐式
选中——应用会在首次运行时明确问你一次。

两个后端各有独立的状态目录,因此可以并行跑在不同端口上。它们默认共享 $DSH_HOME,
所以会话与凭据两边都能看到。

版本错开守卫。 dsh-mfw 精确 pin 一个 dsh 基线。当这个基线与你已安装的 dsh 版本
错开时,启动器会拒绝在原版也在用的那个 home 下启动 mfw:更新的 dsh 可能已经把记录写
成了 pin 住的旧基线不认识的格式,而反方向同样是破坏性的——原版第一次重写工作区记录时
会把额外的成员列表剥掉。出路是用一个独立的 home(--dsh-home ,或偏好设置里的
那一项),或者等 dsh-mfw rebase 到新基线。

偏好设置

⌘, 里有下列几项。除端口外,改动都只需重启被托管的实例;端口需要重启应用本身,因为
状态目录、窗口标题和每个标签的地址都由它推导而来。

| 设置 | 说明 |
|---|---|
| 启动哪个 dsh | 原版或多文件夹,附两个项目的链接 |
| DSH home | 会话与凭据的位置,默认 ~/.dsh;也是基线错开时的出路 |
| 绑定地址 | 默认 127.0.0.1。改之前请读让其他设备访问 |
| 受信任主机 | Web 应用 Host 头围栏的额外 authority,逗号分隔 |
| 端口 | 默认 3080;环境变量 DSH_LAUNCHER_PORT 优先于该设置 |
| 下载源 | 代下载运行时时用哪个 npm / Node.js 镜像,见下载源与镜像 |

切换会先停掉正在跑的实例、再启动新的,然后重载全部标签。如果停不掉,会如实报告切换
失败,而不是悄悄什么都没做。

让其他设备访问

绑定到 loopback 之外是一个需要明确做出的选择,而它的后果很容易被低估:DeepSeek
Harness 的 Web 界面没有任何认证。 没有密码,没有令牌。能连上这个端口的人就能直接
驱动 Agent 读写你的文件、执行命令、使用你已经登录好的凭据。

--trusted-host 不改变这一点。它扩展的是 Web 应用用来防御 DNS 重绑定的 Host 头校验,
不是访问控制;把某个 authority 加进去,并不因此授予任何人权限。

所以应用会在开放端口前询问、把后果讲明,并把确认框的默认按钮设为「取消」。无论绑定到
哪里,就绪探测和各个标签始终访问 127.0.0.1——0.0.0.0 指的是要监听哪些接口,而不是
一个可以连接的地址。如果确实要开放,请只在你信任其上每一台设备的网络里这样做;更好的
做法是用 SSH 隧道,或在 dsh 前面放一个带认证的反向代理。

下载源与镜像

给 registry.npmjs.org 和 nodejs.org 慢或不通的网络环境用。只作用于本应用代为下载的
东西——固定版本的 Node.js 和托管的 dsh。在偏好设置里选,或用 --mirror:

"$L" mirrors                              # 列出内置镜像
"$L" --mirror npmmirror runtime install
"$L" --registry https://npm.internal.example/ runtime install

| 名字 | npm registry | Node.js |
|---|---|---|
| npmjs | registry.npmjs.org | nodejs.org/dist |
| npmmirror | registry.npmmirror.com | npmmirror.com/mirrors/node |
| tencent | mirrors.cloud.tencent.com/npm | mirrors.cloud.tencent.com/nodejs-release |
| huawei | repo.huaweicloud.com/repository/npm | repo.huaweicloud.com/nodejs |

--registry 和 --node-mirror 可以只指定其中一半,与 --mirror 同时使用时覆盖预设。

默认什么都不传。 这样 npm 会读你自己的 ~/.npmrc——代理和凭据都在那里;强行指向
registry.npmjs.org 反而会破坏这个选项本来要照顾的那些配置。如果你就是想覆盖一个坏掉的
.npmrc,显式选 npmjs。

两半的性质并不相同,这一点值得讲清楚:

- 换 Node.js 下载源不是信任选择。 期望的 SHA-256 固定写在 launcher 里,镜像给出别的
内容会被拒绝且不解压。上表中的镜像都实测过,其压缩包与 nodejs.org 逐字节一致。
- 换 npm registry 是信任选择。 npm 校验的完整性哈希来自 registry 自身,所以选 registry
等于选择相信谁提供的包内容。因此内置的只是一份短名单,不做猜测。

多文件夹后端不在此列:dsh-mfw 会自己调包管理器准备运行时,跟随你的 npm/pnpm 配置。那条
路径请在 ~/.npmrc 里设置 registry(或 pnpm config set registry …)。

检查更新

DSH Desktop → 检查更新… 会把当前 bundle 与 GitHub 上最新的发布做比较,并可以直接
下载安装。没有任何后台轮询:一次菜单操作对应一次 API 请求,真正开始下载还需要再确认
一次。

下载的内容要替换正在运行的应用,必须依次满足三点:

1. 磁盘映像的 SHA-256 与发布里 SHA256SUMS 的对应行一致;
2. 映像内的应用通过 codesign --verify --strict 与 Gatekeeper 的 spctl --assess,
也就是首次启动时会面对的那道检查;
3. 新版本签名的 Team ID 与被替换的应用一致。

第三点才是关键。与映像一同发布的校验和只能证明两者相符;有效的开发者签名只能证明
「有人」签过名。以已经安装的那个身份为锚,才能让一个被攻破的发布无法换上另一个发布者
的应用。其余细节见 SECURITY.md。

自己构建的 bundle 是 ad-hoc 签名,没有可比对的身份,所以对它直接拒绝原地更新,而不是
退化成一道更弱的检查。重新跑 ./build.sh,或者装一个正式发布版。

替换动作由一个写在 bundle 之外的小脚本完成——bundle 无法在运行中覆盖自己。它等应用退出、
用重命名交换目录、重新打开应用,然后删掉自己;任何一步失败都会保持已安装的版本不变。

这次查询是匿名的,因此会占用 GitHub 每小时 60 次、按 IP 计算的 API 额度。在共享出口
地址后面,这个额度可能已经被别人用掉了;这种情况会如实报成限额问题,而不是网络故障。

标签

+、⌘T 或右键菜单新建;最新的紧邻 +,更早的依次向右。用标签上的 ×、⌘W 或右键菜单
关闭。拖动可以重排。⌘R 重载当前标签,⇧⌘W 关闭窗口。

标签名默认跟随网页标题,harness 会在新建和切换会话时更新它。双击标签(或右键 →
重命名)可以自己命名;手动名优先于自动标题,并且会被记住。

为什么每个标签有自己的持久化存储

harness 的 Web UI 把当前会话记在 localStorage 的 dsh.sessions.current 里,并在页面
加载时读回来,而整个 UI 没有 URL 路由。如果共享一份存储,每个标签都会覆写这同一个键,
于是新开的标签、以及重启之后的所有标签,都会收敛到同一个会话上——而这恰恰是"开多个
标签"想避免的事。

所以每个标签有自己的 WKWebsiteDataStore,按标签的 UUID 标识,落在
~/Library/WebKit/io.github.boy-grid.dsh-desktop/WebsiteDataStore//。会话彼此
独立,页面级设置能活过重启,每个标签回到它自己上次的会话。关闭标签会连它的存储
一起删掉;崩溃遗留的孤立存储在下次启动时清理。

命令行

所有与实例生命周期有关的逻辑都在一个 shell 脚本里,GUI 也是调它。它单独用也有价值——
写脚本、开第二个实例、或者排查一次启动失败。

L="$HOME/Applications/DSH Desktop.app/Contents/MacOS/launcher"

"$L" status     # 在跑还是停了、哪个后端、哪个 home、端口归谁
"$L" start      # 未运行则启动,等到能响应
"$L" stop       # 停掉本启动器启动的那个实例
"$L" restart
"$L" open       # 在默认浏览器里打开
"$L" launch     # start + open
"$L" update check    # 把当前 bundle 与最新发布比较
"$L" update install  # 下载、校验并替换当前 bundle
"$L" mirrors         # 列出内置的 npm / Node.js 镜像
"$L" help

| 参数 | 环境变量 | 默认 |
|---|---|---|
| --port  | DSH_LAUNCHER_PORT | 3080 |
| --host  | DSH_LAUNCHER_HOST | 127.0.0.1(见上文) |
| --mirror  | DSH_LAUNCHER_MIRROR | 无,见下载源与镜像 |
| --registry  | DSH_LAUNCHER_NPM_REGISTRY | 沿用 ~/.npmrc |
| --node-mirror  | DSH_LAUNCHER_NODE_DIST | https://nodejs.org/dist |
| --trusted-host  | DSH_LAUNCHER_TRUSTED_HOSTS(换行分隔) | 无;可重复 |
| --backend  | DSH_LAUNCHER_BACKEND | stock |
| --dsh  | DSH_LAUNCHER_DSH | 自动探测,结果缓存进状态目录 |
| --mfw  | DSH_LAUNCHER_MFW | 自动探测(仅 mfw 后端) |
| --node  | DSH_LAUNCHER_NODE | 自动探测并缓存 |
| --dsh-home  | DSH_LAUNCHER_DSH_HOME | $DSH_HOME,未设则 ~/.dsh |
| --allow-version-skew | DSH_LAUNCHER_ALLOW_VERSION_SKEW=1 | 关 |
| --state  | DSH_LAUNCHER_STATE | ~/Library/Application Support/DSH Desktop |
| --no-browser | DSH_LAUNCHER_NO_BROWSER=1 | 关(只影响 open/launch) |

状态按后端与端口分区——非 stock 后端多一级 ,非默认端口多一级
ports/——所以两个以不同方式启动的实例永远不会共用 pid 文件或日志:

"$L" --port 3099 start                    # 第二个实例
"$L" --port 3099 stop

GUI 是单实例的(LaunchServices 会激活已在运行的应用,而不是再起一个);命令行不受限制。

为什么要单独解析 node。 双击启动的应用只拿到系统 PATH,所以用 nvm、volta 或装在
~/.local 里的 node 是看不见的——而 dsh 的 shim 是 #!/usr/bin/env node,会直接失败。
启动器自己解析 node,并以 node  的方式运行。子进程的 PATH 用的是 node 的
realpath 目录,因为 pnpm 与 corepack 装在真正的二进制旁边,而 ~/.local/bin
这类 symlink 目录通常只有 node、npm、npx——这一点弄错的话,即使 mfw 的运行时已经
准备好了,它也会找不到包管理器。

安全模型

简版:应用记下自己启动的 pid,并在向任何进程发信号之前,先确认端口上的监听者确实属于
那个 pid 的进程树。被别人占着的端口——你在终端里起的 dsh web,或者一个复用了旧 pid
的无关进程——会被拒绝操作,而不是被停掉。细节与漏洞报告流程见 SECURITY.md。

测试

没有测试框架,也没有外部依赖;用替身 dsh 与替身 dsh-mfw,所以两者都没装的机器上
也能跑。

bash tests/run.sh          # 全部
bash tests/run.sh t-04     # 名字含 "t-04" 的

覆盖「只停自己启动的服务」这条承诺、归属判定、后端与路径解析、mfw 的几道守卫、
完整的启动流程,以及绑定地址规则的两侧——脚本的判定与应用的判定必须一致,所以其中
一个用例会拿 Preferences.swift 编出一个探针,检查真正发布的代码而不是它的副本。
在本机跑不了的用例会显式列为 SKIP,不会安静地算作通过。装了 shellcheck 就一并跑。

故障排查

启动失败会在对话框里给出原因,同时写进日志。日志目录在任何环节可能失败之前就已建好,
所以总有地方可查:

cat "$HOME/Library/Application Support/DSH Desktop/logs/web.log"   # 实例
cat "$HOME/Library/Application Support/DSH Desktop/logs/agent.log" # 应用

非默认端口或 mfw 后端要看 mfw/ 与 ports// 下面;"$L" status 会打印它实际
解析出的状态目录。

从头开始

只想让首次运行的询问重新出现、又不想丢掉已下载的运行时,删掉记录答案的那一个键就够了:

defaults delete io.github.boy-grid.dsh-desktop backendChosen && killall cfprefsd

要清掉本应用的全部数据,先退出应用——cfprefsd 会缓存偏好,只删文件会被它写回去:

osascript -e 'quit app "DSH Desktop"'; sleep 2
"$L" stop 2>/dev/null

defaults delete io.github.boy-grid.dsh-desktop
rm -f  "$HOME/Library/Preferences/io.github.boy-grid.dsh-desktop.plist"
killall cfprefsd

状态、日志,以及已下载的托管运行时(约 470 MB)
rm -rf "$HOME/Library/Application Support/DSH Desktop"

每标签的 Web 存储
rm -rf "$HOME/Library/WebKit/io.github.boy-grid.dsh-desktop" \
"$HOME/Library/HTTPStorages/io.github.boy-grid.dsh-desktop" \
"$HOME/Library/Caches/io.github.boy-grid.dsh-desktop"

以上都不会碰 ~/.dsh:会话与凭据属于 dsh,不属于本应用。想一并清掉就单独删它,之后需要
重新登录。

仓库结构

main.swift              入口(顶层语句只能放这里)
LauncherAgent.swift     AppKit GUI:窗口、标题栏标签条、WebView、菜单
Preferences.swift       设置模型、首次运行询问、偏好设置窗口
TabStore.swift          每标签持久化存储:创建、删除、清理孤立项
launcher                实例生命周期:start/stop/status、双后端、状态目录
Info.plist              bundle 元数据;LSMinimumSystemVersion 决定构建目标
icon.icns, make-icon.py 应用图标与合成脚本
build.sh                组装、编译、签名、注册;--release 用于发布
scripts/make-dmg.sh     打包、公证、装订、校验和
tests/                  run.sh、lib/assert.sh、t-*.sh、fixtures/
.github/workflows/      CI 与发布 workflow
OPENSOURCE-PLAN.md      路线图:已实现 / 未实现 / 明确不做(英文)

重新生成图标

图标派生自 harness Web UI 自己的 favicon(出处见
THIRD_PARTY_NOTICES.md):从运行中的实例取回、光栅化,再用
make-icon.py 合成到 macOS 风格的渐变圆角底上(纯 Python 标准库)。

curl -s http://127.0.0.1:3080/favicon.svg -o /tmp/favicon.svg
sed 's/width="50.000000" height="50.000000"/width="1024" height="1024"/' \
/tmp/favicon.svg > /tmp/favicon_1024.svg
sips -s format png /tmp/favicon_1024.svg --out /tmp/logo_1024.png
python3 make-icon.py /tmp/logo_1024.png /tmp/icon_final_1024.png
之后:sips 生成各尺寸 → iconutil -c icns icon.iconset -o icon.icns

许可与致谢

MIT,见 LICENSE。被托管的 DeepSeek Harness 是 DeepSeek 自己的 MIT 项目。
第三方声明与图标出处见 THIRD_PARTY_NOTICES.md;应用内
DSH Desktop → 关于 DSH Desktop 载有同样的声明。

欢迎贡献,见 CONTRIBUTING.md。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

同作者(Boy-Grid)的其他插件

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群