deepseekHarness桌面端不开端口:Electron 自定义协议拆解

DSH 桌面端不开端口:Electron 自定义协议拆解

关键词:DeepSeek Harness、DSH 桌面端、Electron、自定义协议、本地 Agent Runtime


目录


一、9 月 14 日:一个没有公告的目录

2026 年 9 月 14 日前后,有人发现 DeepSeek 官方 deepseek-ai/deepseek-harness 仓库的 apps 目录下多了一个 desktop。点进去是一套基于 Electron 的桌面应用,包名 @deepseek-ai/dsh-desktop,版本号已经到 0.1.5-rc.2,签名、公证、自动更新这套发布流程也一并写在了仓库里。

这件事有两个反常的地方。第一,DeepSeek 没有为它发过任何公告,代码是静默合进 master 分支的。第二,GitHub Releases 里没有安装包,想用只能自己拉源码构建。

对开发者来说,值得关心的不是「DeepSeek 也做了个桌面壳」。社区里早就有好几个第三方桌面客户端,其中 star 数最高的一个在 2.4 万左右,装完就能用。真正值得拆的是另一件事:官方这套桌面端没有走社区方案的老路------它不起本地 HTTP 服务。

(图一:官方 DSH 桌面端从渲染进程到 dsh Runtime 的完整链路,全程没有监听端口)

要理解这个差异的分量,得先知道社区方案是怎么做的。多数 Web 产品转桌面端,最省事的路径是:Electron 启动时在本地起一个 HTTP 服务,然后 BrowserWindow 直接打开 http://127.0.0.1:某个端口。DSH 自己的 Web 端就是这么跑的------npx @deepseek-ai/dsh web 默认在 127.0.0.1:3080 起服务。这条路简单、复用率高,但它把「本地端口」这个东西留在了桌面上。

官方桌面端把这条链路整个换掉了。

二、先分清三个「dsh-desktop」

这是目前最容易搞混的地方,先花一分钟理清。

项目 维护方 性质 大致 star 数
@deepseek-ai/dsh-desktop DeepSeek 官方 仓库内 apps/desktop 目录,无独立仓库 ---(随主仓库)
anywhere-labs/dsh-desktop 社区 TypeScript,MIT,桌面工作区 + 插件市场 约 2.4 万
dataelement/dsh-desktop 社区 TypeScript,MIT,把 Harness 封装为安装包 约 4.7 千

三个项目名字几乎一样,但不是一回事。前两个的区别尤其要紧:一个是官方仓库里的子目录,一个是社区独立仓库。有些插件导航站和资讯页把社区版本标注成「官方桌面端」,那是错的,照着装会装到一个第三方壳。

判断方法很简单:看 npm 包作用域。 官方的包名带 @deepseek-ai/ 作用域,社区项目是 GitHub 用户名的裸仓库。另外,官方版本到目前为止没有对外分发任何安装包,凡是提供 DMG / EXE 直接下载的,都是社区项目。

顺带交代一下背景:DeepSeek Harness(命令行名 dsh)是 DeepSeek 在 2026 年 8 月 13 日开源的 Agent 运行时,核心主张是「一切皆插件」------模型、工具、沙箱、会话存储、UI,乃至 agent loop 本身都可以被插件替换。底层基于 Cordis。仓库建仓三周后 star 数突破 20 万(不同数据源在 20.4 万到 22 万之间浮动),这个增速在开发者工具里算得上罕见。

也正因为这个背景,官方这次的做法才值得单独写一篇。插件化的运行时意味着「跑在哪」这件事本身也应该是一个可替换的插件------插件负责能力,宿主负责边界。过去几个月里,这个宿主是终端和浏览器;现在多了第三个选项,而且这个选项是由官方自己定义边界的。

所以当你看到「官方桌面端」这四个字时,真正的问题是:在一个已经很热的插件化运行时上,官方为什么还要自己做一个壳,而且做得跟社区的不一样?

三、链路拆解:从 fetch 到分帧字节流

在展开细节之前,先给一张全景。官方桌面端的一次普通对话请求,要穿过五个层次:渲染进程发起 fetch,Electron 主进程的协议处理器接住它,通过带版本的二进制帧管道交给一个独立的 Node.js 子进程,最后落到 dsh Runtime 和它挂载的 Cordis 插件树上。整条链路里没有任何一环需要网络套接字。

拆开看,每个层次都有一个明确的设计意图。

3.1 dsh-app:// 同时管静态资源和接口

Electron 允许应用注册自定义 URI 协议,官方桌面端注册的是 dsh-app://,并且把它拆成了两个 host:

  • dsh-app://shell:提供启动页和插件管理器窗口,这是 Electron 自己拥有的页面;
  • dsh-app://app:提供与后端版本匹配的 Web 资源,同时转发 API 请求与流式响应。

这个设计的好处是渲染进程看到的仍然是熟悉的 fetch 接口,前端代码基本不用改------Web 端那套 UI 可以直接复用。但底下已经没有 HTTP 端口这回事了,连带消失的还有 CORS 配置、Host 头校验、以及「端口被别的进程占了怎么办」这类问题。

(图二:Web 端、社区桌面壳、官方桌面端在通信方式、运行时来源与更新单元上的差异)

这里有个容易被忽略的工程收益:端口争用是本地 Agent 工具的常见故障源。 固定端口(比如 3080)一旦被占用,轻则启动失败,重则连到一个完全不相干的服务上。改随机端口能缓解,但随之而来的是进程间要交换端口号、要做健康检查、要处理端口复用。用自定义协议,这类问题从架构上就不存在了。

顺带说一句 Electron 这边的实现约束。自定义协议默认是不能当 HTTP 用的:不注册成 standard scheme,相对路径解析不了,localStorageindexedDB 这些存储 API 也不可用。想让 dsh-app:// 表现得像 https:// 一样,注册时要显式开特权:

javascript 复制代码
protocol.registerSchemesAsPrivileged([
  {
    scheme: 'dsh-app',
    privileges: {
      standard: true,        // 支持相对/绝对 URL 解析
      secure: true,          // 被视为安全上下文
      supportFetchAPI: true, // 允许 fetch / XHR 访问
      corsEnabled: true,     // 显式启用 CORS 校验
      stream: true,          // 支持流式响应
    },
  },
]);

最后那个 corsEnabled: true 不是可选项,后面第六节会展开讲为什么。

3.2 真正的 dsh 不跑在 Electron 的 Node 里

这是整个设计里我最认可的一个决策。

直觉上,Electron 自带 Node.js,直接把 dsh 跑在里面最省事。官方没有这么做,而是让真正的 dsh Runtime 跑在一个独立的上游 Node.js 子进程里。原因有两个,都很实在:

  1. Electron 内置的 Node 带自己的补丁、ABI、fuse 开关和生命周期约束,跟标准 Node 不完全等价,插件生态里那些原生模块未必能正常加载;
  2. 用户系统里装的 Node 版本不可控------有人是 18,有人是 22,还有人用 nvm 切来切去,插件依赖树在不同版本上行为不一致。

于是官方做了一个明显的取舍:把固定版本的 Node.js、pnpm 和完整的 dsh 生产依赖树一起打进应用。 安装体积会变大,但换来了「无论在谁的机器上,跑的都是同一个运行时」。对插件化架构来说,这个交换是划算的------插件作者不用再猜用户的 Node 版本。

3.3 数据面和控制面走两条路

主进程和子进程之间怎么通信?官方做了分层:

  • 数据面fetch 请求与流式响应通过带版本的二进制帧传输,走两根管道,单个数据帧上限 64 KiB,实现了背压与取消;
  • 控制面 :Node IPC 只处理 readyfatalshutdown 这类生命周期消息,不承载大块请求与响应数据。

把这两件事分开是个专业做法。控制面保持简单,出了问题好排查;数据面专门为流式传输设计------Agent 的输出是持续不断的 token 流,用 IPC 消息逐条转发会产生大量序列化开销,用带背压的分帧字节管道就顺畅得多。

「背压」这个机制在 Agent 场景里不是锦上添花。模型输出 token 的速度通常快于 UI 渲染和落盘的速度,如果管道不做背压,内存里就会堆积大量还没消费的帧,长时间会话下内存占用会一路涨上去。64 KiB 的帧上限配合背压,等于给这条管道设了一个固定的缓冲区上限。取消则是另一回事:用户在 Agent 跑到一半时点停止,需要管道能立刻丢弃后续帧,而不是等子进程把整段输出写完。

「带版本」这个细节也值得注意:主进程和子进程如果版本不匹配,帧格式解析会直接失败,而不是给出一堆诡异的半截数据。对自带固定版本 Node 和 dsh 的桌面端来说,这个检查其实很少会触发,但它的存在本身就是一种态度------宁可启动失败,也不要带着版本错配继续跑。

3.4 渲染进程被压到多窄

桌面壳最容易出事的地方,是给渲染进程的权限给多了。官方的 BrowserWindow 配置是标准的安全基线:

javascript 复制代码
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,   // 页面拿不到 Node API
    contextIsolation: true,   // 预加载脚本与页面隔离
    sandbox: true,            // 渲染进程沙箱化
    webSecurity: true,        // 保留同源策略
  },
});

再往下还有一层收窄:普通的 dsh 页面甚至只拿到一个桌面协议版本标记,插件管理与启动恢复这类能力只暴露给 Electron 自己拥有的 Shell 页面。也就是说,虽然复用了 Web 端那套 UI,但没有把整个 Electron 主进程的能力一股脑塞给网页。

这一点对插件化架构尤其关键。dsh 允许安装第三方插件,而插件是有 UI 的。如果渲染进程能拿到文件系统、Shell 或原始 Electron IPC,那「装个插件」和「在本机执行任意代码」就没有区别了。

四、三种运行形态,差在哪

把 Web 端、社区桌面壳、官方桌面端放在一起看,差异主要不在功能,而在宿主边界

先看 Web 端为什么必须开端口。这不是设计失误,而是浏览器的运行模型决定的------npx @deepseek-ai/dsh web 起的是一个标准的 HTTP 服务,浏览器必须通过 URL 访问它,而 URL 访问必然要落到某个「IP + 端口」上。选择只有两个:绑 127.0.0.1(只对本机可见)或绑 0.0.0.0(对所有网卡可见)。前者安全,但也就此把「本地端口」这个东西留在了系统里。社区桌面壳沿用了这条路,只是把命令行隐藏起来、把端口改成随机分配,本质没变。

对比项 Web 端 社区桌面壳 官方桌面端
启动方式 npx @deepseek-ai/dsh web 双击安装 Electron 应用 / 源码启动
通信方式 本地 HTTP,默认 127.0.0.1:3080 本地 HTTP + 随机回环端口 dsh-app:// + 分帧字节管道
是否监听端口
运行时来源 当前系统安装的 Node 与 dsh 依赖本机 dsh 自带固定版本 Node、pnpm、dsh 与生产依赖
Profile Web Profile 各自实现 独占 $DSH_HOME/profiles/desktop
单实例保护 视实现而定 有单实例锁
更新单元 npm / CLI 版本 壳与后端分开更新 壳 + dsh + Node + pnpm + 依赖树整体签名发布
「在本地应用中打开」 可用 视实现而定 当前禁用

最后一行值得单独说。桌面端把 Web 的「Open in...」功能关掉了,因为对应的 Host 插件需要 HTTP 路由,而桌面端不提供 web server。这是「不开端口」这个决策直接导致的功能倒退------不是遗漏,是取舍。

表格之外还有一条边界容易被漏掉,就是数据与运行时的切分方式 。桌面端和 CLI 会共享 $DSH_HOME 下受支持的产品数据------会话、设置、凭据、工作区都是通用的,你在命令行里跑出来的对话,桌面端能直接打开。但它们不共享可执行包、插件激活状态、lockfile 和 node_modules

为什么要切得这么干净?因为插件是有原生依赖的。CLI 用的是系统 Node,桌面端用的是自带的固定版本 Node,两者的 ABI 不同,同一份编译产物不能通用。强行共用只会得到一堆难以复现的加载错误。桌面端还加了单实例锁,防止两个桌面进程同时修改同一套插件环境。一句话概括这个设计原则:共享用户数据,隔离运行时依赖。

另外,官方对更新单元的定义很严:Electron Shell、Web Client、Backend、Plugin Graph 被视为一个完整的 Release Identity。 即便某次更新没改一行 Electron 代码,只要 dsh 升级了,就算一次 Desktop Release。好处是永远不会出现「壳是新版、后端是旧版」的错配;代价是更新频率被后端的迭代节奏绑架了。

(图三:从 8 月 28 日第一批 Electron 打包代码入库,到 0.1.5-rc.2 的 Developer Preview 现状与缺口)

五、从源码跑一遍,以及它的坑

既然没有安装包,想试就只能自己构建。前置条件是 Node.js 22.19 或更高版本------这一步本身就劝退了一批人:如果你机器上跑的是 Node 18 或 20,得先升级,而升级 Node 又可能影响到别的项目。

bash 复制代码
# 1) 装 pnpm(仓库指定了版本)
npm install -g pnpm@11.7.0

# 2) 拉仓库并装依赖,仓库较大,这一步会比较慢
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install

# 3) 启动桌面端开发模式
pnpm run dev:desktop

第三步会自动编译项目并下载 Electron,成功后弹出一个窗口,界面与 Web 端基本一致,在设置里填好 API Key 就能用。

几个实际会卡住的地方:

  • dev:desktop 不等于最终用户环境。 官方 README 明确建议用 unpacked application 来验证内置 Node、内置 pnpm、内置 dsh 资源、插件安装与修复路径。也就是说,开发模式跑通了不代表打包后的行为一致。
  • 正式打包只支持三个目标:macOS arm64、macOS x64、Windows x64。Linux 不在官方发布目标里。macOS 走签名 + 公证流程,Windows 需要 EV 签名。
  • 插件不是想装就能装。 如果插件依赖需要执行生命周期脚本(比如要编译原生模块),必须通过桌面端审核过的 allowBuilds 策略,否则安装会被拒。
  • 已知问题:有开发者在 9 月 14 日的编译实测中反馈,应用内无法粘贴内容,API Key 只能手输。这类问题在早期预览版本里很常见,但足以说明现在还不是日常使用的状态。

想验证「不开端口」是不是真的,跑一条命令就知道了:

bash 复制代码
# Windows:列出所有处于 LISTENING 的回环地址端口
netstat -ano | findstr LISTENING | findstr 127.0.0.1

# macOS / Linux
lsof -nP -iTCP -sTCP:LISTEN | grep 127.0.0.1

Web 端启动后,你能在列表里看到 3080;官方桌面端启动后,这个列表里不会多出任何一项。

六、不开端口不等于安全,几个待验证的点

先澄清一个流传很广的误判。很多人看到「本地起了个 HTTP 服务」,第一反应是担心公司内网或公共 WiFi 下的其他设备能扫到这个端口。这个担心基本是打偏的:绑定 127.0.0.1 的服务走的是回环接口,数据包不经过物理网卡,同网段的机器在链路层就连不上。 真正危险的是把服务绑到 0.0.0.0------那才是向所有网卡广播。

本地 HTTP 服务的真实攻击面在别处:浏览器里任意页面都可以向 http://127.0.0.1:3080 发起请求。 只要你在浏览器里打开了一个恶意页面,它就能用 JS 探测本机端口、调用本地接口,配合 DNS rebinding 还能绕过同源策略的部分限制。这类攻击不需要同网段,也不需要你装任何东西------你只是开着服务在上网。从这条路径看,改用自定义协议确实是把攻击面从「任何一个网页都能碰」收窄到了「只有应用自己能碰」。

但自定义协议本身也不是免死金牌,有两个坑要提醒。

第一是 CORS 配置。 2026 年 8 月 5 日公开的 CVE-2026-70604(GHSA-v3j7-r9gq-3gjw,CVSS 7.4)就是冲着这个来的:Electron 里注册自定义协议时如果开了 supportFetchAPI: true 却没设 corsEnabled: true,该协议就不受 CORS 约束,远程 origin 加载的页面可以跨域 fetch 并读取完整响应体。受影响的 Electron 版本已在 39.8.10、40.9.3、41.4.0、42.0.0 修复。规避方式也很直接------需要强制 CORS 的协议显式设 corsEnabled: true,并在协议处理器里校验请求的 Origin

这个漏洞值得多说一句,因为它揭示了一个反直觉的地方:Electron 在这个问题上的默认行为是宽松的,不是收紧的。 注册协议的开发者如果只记得开 supportFetchAPIfetch 能用,很容易漏掉 corsEnabled,结果就是自己造出了一个比 HTTP 更危险的通道------毕竟 HTTP 服务的同源策略是默认生效的,而自定义协议在配置不全时会默认放行。从时间线上看,CVE 在 8 月 5 日公开,桌面端第一批 Electron 代码 8 月 28 日才入库,官方大概率用的是已修复版本,但如果你自己在做类似的东西,这个坑要主动避开。

第二是路径穿越。 自定义协议的 handler 拿到的是不可信的 URL 路径,如果写法是先拼接再归一化,就会被 ../../ 打穿。Electron 官方文档里给的示例恰好就踩了这个坑,照抄很危险。错误与正确的写法差别只在一行顺序:

javascript 复制代码
// 有风险:先拼接再 normalize,../ 已经被拼进去了
const filePath = path.normalize(`${__dirname}/${url.slice('app://'.length)}`);

// 正确:先用 URL 解析,检查是否以 / 开头,再单独 normalize
const reqUrl = new URL(req.url);
if (!reqUrl.pathname.startsWith('/')) return next({ mimeType: null, data: null });
let reqPath = path.normalize(reqUrl.pathname);
const relativePath = path.relative(__dirname, path.join(__dirname, reqPath));
const isSafe = relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);

所以回到那个问题:不开端口到底换来了什么?换来的是攻击面从「浏览器里任何一个网页都能碰一下本机端口」,收窄到了「只有这个应用自己能走这条通道」。这个收益是真实的,但它成立的前提是协议本身配置正确------否则只是把风险从一处挪到了另一处,还挪得更隐蔽。

除此之外,还有几个需要继续观察的点:

  1. 体积代价。自带 Node + pnpm + 完整依赖树,安装包会明显大于社区方案。对本地 Agent 工具来说这可能是合理的,但要有预期。
  2. 更新频率被绑定。前面说过,dsh 一升级就触发一次 Desktop Release。后端现在每周都有发布,桌面端能不能跟上这个节奏,是后续要观察的。
  3. Linux 缺位。官方发布目标只有 macOS 和 Windows 的 x64 / arm64,Linux 用户目前只能靠社区方案。
  4. 功能倒退 。「Open in...」被禁用,插件安装受 allowBuilds 审核约束,这些都是「不开端口」换来的代价,短期内不会补上。
  5. 状态仍是 Developer Preview。官方明确提示会有破坏性兼容变更,GitHub Release 里现在也没有现成安装资产。
  6. 社区壳仍是日常首选。至少在安装包正式分发之前,想要开箱即用,社区项目体验更成熟。

七、现在值不值得折腾

我的判断很直接:如果你只是想用 DeepSeek Harness,现在装官方桌面端不划算。 为了一个与 Web 端几乎一致的界面,去处理编译、签名、运行时依赖和一堆早期 Bug,投入产出比不高。社区方案或者直接在终端跑 npx @deepseek-ai/dsh web 更省事。

但如果你是下面这几类人,这份代码值得花两小时读一遍:

  • 正在做 Coding Agent 的桌面客户端,纠结要不要起本地服务;
  • 在研究 Electron 怎么安全地承载本地 Agent Runtime;
  • 被「端口冲突 + 版本错配 + 插件隔离 + 自动更新」这四件事反复折磨过。

回到最开始那个问题------官方为什么要在社区已经做得很热的时候自己下场?答案不在界面上,而在边界上:它把 Harness 从「一个需要命令行启动的开源框架」,往前推成了「一个有完整宿主边界的本地 Agent 平台」。 可安装、可隔离、可恢复、可更新,这四件事做完了,桌面端才有资格谈更多原生能力。

(图四:官方把 Shell、Web Client、Backend、Plugin Graph 锁成同一个 Release Identity)

按官方仓库里已经写好的签名与更新流程看,后续大概率会通过 download.deepseek.com 正式分发安装包,届时才是普通用户该装的时候。在那之前,源码已经足够说明它想干什么了。


#DeepSeekHarness #DSH桌面端 #Electron #自定义协议 #Agent运行时 #本地部署

相关推荐
懂压力传感器的涌客1 小时前
从具身智能说起:柔性薄膜压力传感器在机器人触觉系统的选型与信号链设计
人工智能·机器人·压力传感器·源头工厂·fsr压力传感器
Zzj_tju1 小时前
CLIP 图文对齐:先核对正样本,再相信检索分数
人工智能·深度学习·语言模型·自然语言处理
ChenDestiny1 小时前
AI Agent学习(5.6):RAG进阶(一)从Naive到Agentic的演进地图
人工智能
千里码aicood1 小时前
基于深度学习的驾驶员分心驾驶行为识别研究
人工智能·深度学习
大模型真好玩1 小时前
DeepSeek Harness 入门很简单(四)——DeepSeek Harness接入插件
人工智能·agent·deepseek
程序员cxuan2 小时前
GPT-6 Astra 的提示词泄露了,里面居然藏着个保安?
人工智能·后端·程序员
悬木2 小时前
从单体到 AI 搜索:一个电商搜索系统的进化
人工智能
码海无涯回头无岸2 小时前
多轮对话:messages是Agent的记忆
人工智能
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(69):STITCH——用上下文意图解决“语义相关但情境错误”的记忆检索
论文阅读·人工智能·学习·开源·github