你还在用浏览器使用 DeepSeek Harness 吗?为你的 DSH 打开一个客户端吧

DeepSeek Harness 的 Web 入口已经很好用:

sh 复制代码
pnpm dsh web

但当我真的把它放进日常开发工作流后,问题开始变得具体:浏览器标签页太多,DSH 容易淹没在文档、后台和搜索结果之间;每次启动后还要从浏览器里找回那个页面;想把它当成长期驻留的 AI 编程工作台时,它又缺少一个明确、独立的窗口。

我想要的不是重新做一套 DeepSeek Harness,更不是把 Web UI 复制一份再维护一个桌面分支。我只想让已经配置好的 DSH 多一种打开方式:浏览器适合快速访问,Electron 窗口适合持续工作。

这就是 dsh-web-desktop 的出发点。它为现有 Web profile 提供 Electron 窗口,不复制 Web 应用,不修改 DeepSeek Harness 核心,也不新建第二份插件和配置清单。

目标:增加入口,不复制产品

最直观的方案是做一套独立桌面客户端:单独维护 Electron、会话、插件、设置和模型配置。

这个方案的问题是,它会很快与 Web 版本分叉:用户在 Web profile 安装的品牌插件、工具插件、模型配置或会话数据,桌面端都需要再次安装、再次同步或再次迁移。

dsh-web-desktop 选择另一条路:

text 复制代码
pnpm dsh web
  └─ 浏览器打开同一份 Web profile

pnpm dsh plugin --profile web exec dsh-web-desktop
  └─ Electron 打开同一份 Web profile

两种入口最终运行的是同一份 $DSH_HOME/profiles/web:同一份 package.json、同一组 bundle、同一份 cordis.patch.yml、同一批已安装插件和同一份会话/模型状态。

它带来的优势

1. 多一种使用方式

浏览器入口仍然保留,适合临时访问、远程转发或快速调试。

Electron 入口则提供一个独立窗口,适合把 DeepSeek Harness 当作日常 AI 编程工作台使用。它不会额外打开默认浏览器,也不会要求用户记住 overlay 文件的位置。

2. Web 与客户端保持一致

客户端不是另一份 UI,也不是另一套 profile。用户在 Web profile 中做过的操作会自然生效:

  • 安装 dsh-client-ui-brand 等界面插件;
  • 调整 cordis.patch.yml
  • 配置模型、工具、技能和工作区;
  • 保存会话和相关状态。

因此,浏览器和 Electron 的区别只在于"窗口容器",而不是产品能力或用户数据。

3. 不固定绑定某个 DeepSeek Harness 版本

桌面插件不内置一份固定版本的 DSH Runtime。启动器优先使用当前项目的 pnpm dsh;在仓库外使用时,也可以用明确的 DSH_BIN 或 PATH 中的 dsh

这意味着升级 DeepSeek Harness 后,客户端继续复用新的 Web 前端、Web Host、插件系统和 profile 组合,不需要重新维护一套桌面业务代码。

前提也很明确:dsh-web-desktop 与正在使用的 DeepSeek Harness 版本需要保持兼容。它是插件化扩展,而不是冻结运行时的安装包。

4. 符合 DeepSeek Harness 的插件模型

DeepSeek Harness 强调"万物皆插件"。这个项目遵循同一套规则:

  • package.json 声明 dsh.bundle
  • cordis.patch.yml 向 Web profile 插入一个普通插件行;
  • 默认配置不启动 Electron;
  • 启动器通过 DSH 原生的 --patch 为当前调用启用 Electron;
  • 插件使用 ctx.effect() 管理 Electron 子进程生命周期;
  • 不修改 agent loop、Web UI 或 DSH CLI 核心代码。

安装完成后,DSH 会把 bundle 加入 profile 的 bundle 列表;普通 pnpm dsh web 不受影响。

安装与启动

发布版安装到现有 Web profile:

sh 复制代码
pnpm dsh plugin --profile web add dsh-web-desktop

随后通过 profile 内的 pnpm 环境启动:

sh 复制代码
pnpm dsh plugin --profile web exec dsh-web-desktop

需要把 Web 参数继续传给桌面启动器时,使用 --

sh 复制代码
pnpm dsh plugin --profile web exec dsh-web-desktop -- --port 0

这里的 --port 0 让操作系统分配一个临时端口,适合本机已有 Web 实例占用默认端口时调试。

核心实现

一:默认休眠的 bundle 行

插件本身是一个标准 DSH bundle。它在 Web profile 中注册 web-desktop 行,但默认不启动 Electron:

yaml 复制代码
- insert:
    - id: web-desktop
      name: 'dsh-web-desktop'
      config:
        enabled: false
        width: 1280
        height: 800
        minWidth: 900
        minHeight: 640

因此,安装插件不会改变原有 pnpm dsh web 的行为。

二:启动器自动启用桌面模式

启动器定位自己包内的 overlay,并将它与 Web profile 原有参数一起传给 DSH:

ts 复制代码
export function desktopWebArgs(args: readonly string[]): string[] {
  const overlay = fileURLToPath(
    new URL('../desktop.cordis.patch.yml', import.meta.url),
  )
  return ['--profile', 'web', '--patch', overlay, '--no-open', ...args]
}

overlay 只覆盖同一个行的配置:

yaml 复制代码
- id: web-desktop
  config:
    enabled: true
    width: 1280
    height: 800
    minWidth: 900
    minHeight: 640

--no-open 是 Web profile 已有的参数,含义是"不要调用系统默认浏览器"。这样 Web Host 仍然正常启动,但 UI 由 Electron 承载。

三:开发时自动选择当前仓库的 pnpm dsh

裸调用 dsh 容易误命中机器上的全局旧版本。启动器会读取 pnpm 提供的 INIT_CWD,若当前项目存在 dsh script,就从这个项目启动:

ts 复制代码
const localProject = process.env.INIT_CWD

const child = hasDshScript(localProject)
  ? spawn(process.env.npm_execpath ?? 'pnpm', ['dsh', ...desktopWebArgs(args)], {
      cwd: localProject,
      stdio: 'inherit',
    })
  : spawn('dsh', desktopWebArgs(args), { stdio: 'inherit' })

这使本地开发自然使用当前 checkout 的源码入口:

sh 复制代码
pnpm dsh plugin --profile web exec dsh-web-desktop

不需要额外安装全局 dsh,也避免桌面插件意外启动了另一套过期 Runtime。

四:等待 Web Host 结算后再启动 Electron

Electron 不应在 Web Host、API 路由和客户端插件图尚未准备好时抢先加载页面。插件等待 Loader 结算,并确认 webServer 仍可用后才创建子进程:

ts 复制代码
const settled = ctx.get('loader')?.await()

if (settled === undefined) {
  launch()
} else {
  void settled.then(() => {
    if (ctx.get('webServer') !== undefined) launch()
  }, () => {})
}

随后它把本机回环地址和窗口尺寸传给 Electron:

ts 复制代码
child = spawn(electronBin, [mainPath], {
  env: {
    ...process.env,
    DSH_WEB_DESKTOP_WINDOW: encodeWindowOptions({
      url: `http://127.0.0.1:${String(ctx.webServer.port)}`,
      width: config.width,
      height: config.height,
      minWidth: config.minWidth,
      minHeight: config.minHeight,
    }),
  },
  stdio: 'inherit',
})

Electron 端关闭 Node 注入、启用 context isolation,并限制窗口跳转到当前 loopback origin,避免把桌面窗口变成任意网页容器。

关于 Electron 首次下载

Electron 的平台二进制通常在首次安装或首次运行时下载。下载成功后会被本地依赖与缓存复用,后续启动不需要重复下载;升级 Electron、清理依赖或换机器时才可能再次下载。

发布到 npm 的 dsh-web-desktop 已包含 TypeScript 构建产物,因此普通用户使用 dsh plugin add 安装发布版后不需要手动执行 pnpm build。本地 link: 开发时,修改源码后才需要在插件目录执行构建。

结语

dsh-web-desktop 不是要替代 DeepSeek Harness Web,也不是维护另一份桌面产品。它只是把已有的 Web profile 放进一个更适合桌面工作的窗口里。

这种实现让"客户端化"保持了插件系统的核心价值:组合而不是复制、升级而不是分叉、复用而不是重写。

项目地址:

github.com/ningbonb/ds...

npm 包:

www.npmjs.com/package/dsh...

相关推荐
张忠琳2 小时前
【deepseek-harness】Cordis 时空可组合性编程范式 — 三段式精读笔记(五)
ai·agent·deepseek·harness·cordis·dsh
ovO3 小时前
DeepSeek Harness 源码解读(四):一次 Turn 为什么会跑多个 Step
开源·agent·deepseek
JaydenAI4 小时前
[DeepSeek Harness插件内核-06]Context全面解析[自由扩展篇]
ai·agent·deepseek·harness·cordis
ovO5 小时前
DeepSeek Harness 源码解读(三):七个核心服务怎样拼成一次 Agent 运行
开源·agent·deepseek
梅雅达编程笔记5 小时前
实战:用Harness做一个自动化日报Agent
typescript·实战教程·deepseek·harness·自动化agent
ovO5 小时前
DeepSeek Harness 源码解读(二):沿着 ctx.llm 看懂 Cordis 与“一切皆插件”
开源·deepseek
GoCoding6 小时前
DeepSeek Harness 插件
agent·deepseek
GoCodingInMyWay7 小时前
DeepSeek Harness 插件
agent·deepseek·harness
狂师8 小时前
整理了一份 DeepSeek Harness 必备插件清单!
人工智能·开源·deepseek