← 返回列表
未验证
把 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)的其他插件
扫码进群