我的日常大概是这样的:上午在终端里用dsh run把批量任务丢给 DeepSeek Harness,CLI 一动就不用管;中午想看进度,懒得回去敲命令,直接打开浏览器访问dsh web打印出来的本地地址;下午整理 skill 包和插件时,又切到桌面端图形界面去拖拽编排。三个端来回切换,我最怕的就是每个端各搞一套逻辑、各存一份状态,体验还互相打架。
后来把 DeepSeek Harness 的多端架构拆开看,发现它的设计思路其实很朴素:多端不是把同一个软件做三遍,而是把内核收敛成一套可嵌入的引擎,CLI、Web、桌面端都只是它的一件外套。这篇文章就把这套多端架构的分层逻辑、关键实现,以及我实际踩过的几个坑完整讲一遍。如果你想自己部署一套本地 Agent 工具,或者正在纠结"多端产品怎么避免重复造轮子",这篇应该能给你一个能直接落地的参考。
1. 先说清楚一件事:多端不是做三个软件,而是做一套内核
很多人一听到"多端架构"就以为是要把 UI 层做适配,像移动端那套跨端框架一样,用一套代码编译到三个平台。但 DeepSeek Harness 这类 AI Agent 编排工具的核心价值根本不在界面上,而在 Agent 运行时本身:会话怎么管理、模型怎么调用、工具怎么注册、插件怎么加载、多智能体怎么协作。这些能力如果每个端都重新实现一遍,那才是真正的灾难。
1.1 三个端分别要回答什么产品问题
三个端的目标用户和交互场景其实是错开的,产品诉求完全不同。
| 端 | 核心诉求 | 典型使用方式 |
|---|---|---|
| CLI | 效率、脚本化、自动化 | dsh run批量任务、CI/CD 接入、SSH 远程执行 |
| Web | 随时访问、无安装、轻监控 | 浏览器看日志、查看任务状态、分享面板给同事 |
| 桌面端 | 系统集成、完整 GUI、配置管理 | 管理插件/skill、可视化编排、本地模型端点设置 |
CLI 面向的是"熟手",Web 面向的是"临时看一眼的人",桌面端面向的是"需要把复杂状态可视化的操作者"。三者注定不是同一套交互逻辑,但底层能力完全可以是同一份。
1.2 为什么必须共享一套内核
如果三个端各自实现 Agent 循环,维护成本会随着功能迭代指数级上涨:修一个模型调用 Bug 要改三处,新增一种工具协议要同步三端,插件开发者还得为不同端写不同版本的插件。
DeepSeek Harness 的做法是反过来:交互层随便换,但能力层只保留一份。任务状态放在引擎层,三端都是"接同一个 session";插件体系也是统一的,写一次插件,CLI 能加载、Web 能加载、桌面端也能加载。这样用户从终端切到 Web 面板时,任务进度、日志、中间产物都不丢。这才是多端架构存在的意义。
2. 内核层:Agent 引擎如何做到"可嵌入、可常驻"
拆开 DeepSeek Harness 之后你会发现,它并没有一个叫做"内核"的独立的进程,而是把引擎设计成了两种形态:一种是库模式,被dsh run、dsh board这类一次性命令直接加载,跑完就退出;另一种是服务模式,由dsh web和桌面端的后台进程拉起,常驻监听请求。同一个引擎,两种形态,这是整个多端架构能成立的基础。
2.1 会话与事件流设计
所有端之间共享的最核心抽象是 Session。一个 Session 里包含任务上下文、Agent 消息历史、工具调用记录、中间产物,以及当前状态(排队中、运行中、等待输入、已完成、已失败)。
引擎层会为每个 Session 产生一条统一的事件流,事件类型大致包括:
agent.message:某个 Agent 输出的文本消息tool.call:Agent 准备调用某个工具tool.result:工具执行完毕并返回结果session.status:Session 状态切换artifact.ready:产生了一个可下载的中间产物
CLI 拿到事件流后渲染成终端里的流式文本,Web 端拿到同样的事件流渲染成浏览器的实时日志面板,桌面端拿到事件流渲染成图形化的状态卡片。三端面向的是同一条数据管道,只是呈现方式不同。这也是为什么"从 CLI 发起任务,再到 Web 端附加进去看进度"能够无缝衔接。
2.2 多智能体编排:引擎层最复杂的一块
多智能体是 DeepSeek Harness 这类工具经常被提到的能力。多个 Agent 之间不是简单的前后调用,而是需要在同一个 Session 里共享上下文、互相传递中间结果。
我理解它的编排模型是:一个 Session 里可以有多个 Agent 角色,比如规划器、执行器、审查器。规划器先拆解任务,执行器按步骤干活,审查器对产出做检查。配置方式类似这样:
# dsh.agents.yaml 意图示例 agents: planner: model: deepseek-chat system: 你是规划器,负责把任务拆解为可执行的步骤清单 executor: model: deepseek-reasoner system: 你是执行器,负责按步骤完成代码编写和修改 reviewer: model: deepseek-chat system: 你是审查器,负责检查产出质量并给出结论这些 Agent 的编排图、消息流转、当前执行到哪一步,全部是引擎层的状态,天然可以被三端读取。CLI 里你看到的是文本形式的步骤清单,Web 面板里你看到的是可视化流程,桌面端里你看到的是一张可拖拽的编排画布。底层跑的是同一套东西。
2.3 插件系统如何定义跨端边界
插件是多端架构里最容易翻车的一块。很多人一开始把插件理解成"一段脚本",随便挂个钩子就能跑。但一个跨端插件至少要区分两类能力:一类是运行时能力,比如"我可以执行代码";另一类是 UI 扩展能力,比如"我要在 Web/桌面端渲染一个自定义面板"。
DeepSeek Harness 的插件包结构大致是这样:
@linxin666/dsh-p/ manifest.json cli.js web.js desktop.jsmanifest.json声明插件元数据,cli.js提供命令行端的运行时入口,web.js和desktop.js是可选的 UI 扩展入口。引擎在加载插件时会根据当前运行形态选择对应的 entry,而不是一股脑全部加载。
这也解释了为什么你可能会看到类似 "failed to load plugins web boot: 2 entries did not activate @linxin666/dsh-p" 的报错:某个插件在 CLI 下能用,但它没有声明 Web 端的 entry,或者 Web entry 初始化失败了。引擎会跳过这类插件,不至于让整个服务起不来,但这种半激活状态恰恰是插件跨端兼容性最容易踩的坑。
3. CLI 端:朴素界面背后藏着架构的"参考实现"
CLI 往往是最不起眼的端,却是对架构约束最严格的一个端。因为没有鼠标、没有图形按钮、没有任何样式,所有交互都要靠 stdin/stdout 完成,这倒逼内核接口必须足够干净。如果 CLI 端能流畅完成一次完整的 Agent 任务,说明引擎的能力边界划分是正确的。
3.1 dsh run 到 dsh web:从一次性命令到常驻服务
CLI 的命令体系我实际用下来大致分几类:
dsh run "任务描述":启动一个一次性任务dsh board:列出当前所有 Session,查看状态dsh web:启动本地 Web 服务dsh plugin:插件管理dsh doctor:环境自检
dsh run是库模式的最好例子。它启动引擎、创建 Session、执行任务、把结果输出到终端,然后退出。整个过程不常驻系统,不占资源,适合做自动化脚本。
dsh web则是服务模式的例子。它会拉起一个本地 HTTP 服务,打印出访问地址,然后进程一直挂着。执行dsh run需要的是引擎一次性调用,执行dsh web需要的是可复用的常驻引擎服务。两种形态能在同一个 CLI 里共存,靠的正是"引擎可嵌入"这个设计。
3.2 CLI 端的细节体验与工程取舍
CLI 端的工程细节看起来琐碎,实际影响很大。我举几个例子:
输出双轨制。人类可读的终端渲染和机器可读的 JSON 输出要分开。比如dsh run "修复测试" --json能输出结构化结果,方便接入 CI 或脚本;不带参数的默认输出则是带颜色、带缩进的终端友好格式。这个双轨制如果设计晚了,后面想接外部工具会很痛苦。
中断与恢复。长任务跑了一半,按 Ctrl+C 不能直接把进程杀了,而是应该优雅地给引擎发送一个中断信号,让当前工具调用收尾,然后保存 Session 快照。下次用dsh board --resume <session-id>可以从断点接着跑。Session 快照的序列化格式是跨端复用的关键,这里必须设计成稳定版本化的 schema。
终端兼容性。某些 CI 环境没有 TTY,ANSI 颜色会变成乱码。CLI 要么检测NO_COLOR环境变量,要么提供--plain参数。这类问题在架构文档里根本不值一提,但实际使用频率非常高。
3.3 安装部署与路径规划
很多人装 DeepSeek Harness 时会纠结"装到哪个盘"。其实它跟 Node 工具链类似,安装目录本身不重要,关键是环境变量和全局数据目录的设置。
# 假设你装在 D:/deepseek-harness export DSH_HOME="D:/deepseek-harness" export PATH="$DSH_HOME/bin:$PATH" dsh doctor我建议把安装目录和用户数据目录分开:安装目录只放程序文件,用户数据目录(Session、日志、插件、skill)默认放在用户目录下,也可以通过DSH_DATA_HOME指到别处。这样重装工具不会丢掉你的历史任务和配置。这算是我多端实践里的一个重要教训:数据目录如果不独立,卸载一次全没了。
4. Web 端:本地服务模式下的认证与实时推送
Web 端的架构核心和普通 Web 应用很不一样。它不是部署在远程服务器上,而是由dsh web在你本机拉起一个 HTTP 服务,浏览器访问的是localhost。这意味着 Web 端和 CLI 跑的是同一个引擎、同一份数据目录,不需要任何网络中转。
4.1 认证流程拆解
本地服务有一个很常见的提示:dsh web authentication required; reopen the url printed by dsh web.第一次看到这个提示的人往往会困惑:本地服务为什么还要认证?
原因很简单:dsh web监听的不一定只有 127.0.0.1,有时候为了局域网访问会绑到0.0.0.0,这时候同一局域网的人都能访问你的控制面板;另外浏览器里的恶意网页也可能通过 DNS rebinding 等手段打到 localhost 端口。所以本地 Web 服务必须有一个轻量认证机制。
流程大致是:
dsh web启动时生成一个带一次性 token 的 URL,打印在终端里- 浏览器打开这个 URL,服务端校验一次性 token 后返回一个长效会话 token
- 浏览器把会话 token 存到 localStorage,后续请求通过 Header 或 Cookie 带上
- 服务重启、原 URL 过期之后,浏览器再访问旧地址就会看到 "authentication required",需要重新用终端打印的新 URL 打开
这个"重新打开 URL"的设计比在浏览器里搞登录表单省事得多,因为本地服务根本没有用户体系。它追求的不是身份验证,而是"这台机器上的某个操作者授权了这个浏览器"。
4.2 实时推送选型:SSE 还是 WebSocket
Web 面板要实时显示 Agent 的输出,推送通道必须在 SSE 和 WebSocket 之间做选择。我实际接触过的实现里,WebSocket 是更合适的选择,理由有三:
| 对比维度 | SSE | WebSocket |
|---|---|---|
| 连接方向 | 服务器单向推送 | 双向 |
| 浏览器 POST 请求 | 原生 SSE 无法带 body,复杂查询困难 | 双向自由 |
| 断线重连 | 有内置重连但事件定位弱 | 需要自己实现心跳和续传 |
Agent 任务的交互不是纯日志流,用户可能需要从 Web 面板直接给任务发指令(比如中止、切换 Agent、追加输入),这已经超出 SSE 的能力边界了。WebSocket 配合心跳消息,能做到 30 秒无消息不自动断开,断线后按 Session 版本号续拉增量事件,体验比 SSE 完整得多。
4.3 Web 端能做什么、故意不做什么
Web 端不是把桌面端或 CLI 的所有功能都搬到浏览器里。我理解 DeepSeek Harness 的 Web 端是克制的:重点做任务列表、实时日志、Session 详情、线程重跑,不做复杂的 Agent 编排画布编辑。原因是浏览器端交互能力本来就有限,如果强行做复杂编辑,就要维护两套交互逻辑,架构成本高,收益却不大。
这种"故意不做"和"刻意少做"的边界,在多端架构里和"做了什么"一样重要。少划一条边界,后期多端功能漂移就会加速。
5. 桌面端:把同一套能力翻译成图形界面
桌面端在技术选型上,一般无非是 Electron、Tauri 这两个方向。DeepSeek Harness 这类工具的桌面端,从体感和启动速度上看更像是 Web 技术栈套壳的方案,因为它的前端资源大概率可以被 Web 端复用。但具体用了哪个框架不是关键,关键是桌面端的架构位置。
5.1 桌面端到底封装了什么
桌面端不是独立软件,而是"内核宿主 + 图形前端"。它的核心职责有四块:
- 管理常驻引擎进程:桌面端启动时拉起引擎服务,退出时优雅关闭,前端崩溃时引擎保活继续执行后台任务
- 提供 Agent 编排画布:以图形方式查看多智能体之间的协作关系
- 插件和 Skill 管理:把
@linxin666/dsh-p这类插件的安装、启用、状态查看做成 GUI,替代命令行操作 - 本地模型端点配置:管理本地推理服务的地址、模型名称、上下文长度等参数
这些功能里,插件管理是用户感知最强的。一个插件在 CLI 下只要dsh plugin install一行命令就行,但在桌面端,你会看到插件列表、启停开关、入口类型(cli/web/desktop)、激活状态。前面说的 "2 entries did not activate" 这类问题,在桌面端会被直接可视化地标出来。
5.2 桌面端与 Web 端前端资源的复用
如果 Web 端用的是一套主流的组件框架,桌面端完全可以直接复用大部分前端代码,这是这类架构天然的优势。我建议把"页面组件"和"桌面容器能力"分成两层:组件层做任务台账、日志渲染、状态卡片;容器层做系统托盘、全局快捷键、开机自启、菜单栏。
但要注意一个隐蔽问题:桌面端切到后台或最小化时,如果还在持续渲染高频率的日志流,CPU 会莫名其妙飙升。我踩过这个坑之后的理解是,桌面端必须实现"可见性感知":窗口隐藏时降低日志刷新频率,只保留事件落盘,不做实时渲染。这在多端架构里属于"由端侧负责的优化",引擎不用管。
5.3 桌面端常见问题:登录、回调端口、进程残留
桌面端最常见的问题是 OAuth 回调端口冲突。某些模型的远程鉴权需要拉起浏览器做 OAuth 登录,回调地址默认绑定某个固定端口。如果端口被占用,登录就会一直转圈。遇到这种情况,优先确认是不是本机有另一个服务占用了端口,或者配置里指定的回调端口和实际不一致。
另一个常见问题是进程残留。桌面端已经退出,但后端引擎进程还挂在后台,导致下次启动时端口冲突。处理方式是在桌面端里加一个启动自检:启动时检查数据目录里的锁文件,发现旧的 PID 活着就先发信号让它优雅退出,实在不行再提示用户清理。
6. 多端状态同步与插件跨端加载的细节
多端架构最容易出问题的地方,不在单点功能,而在状态一致性和插件跨端适配。如果三端各自读写一份状态,用户一定会遇到"CLI 跑完的任务在桌面端看不到"这种尴尬。
6.1 状态文件与 SQLite 的选择
Session、日志、产物、配置这些状态需要被三端同时读取。如果全部用纯 JSON 文件存,并发写的问题迟早会爆:CLI 进程正在写 session 文件,桌面端同时打开同一个文件,轻则读到半截数据,重则互相覆盖。
更稳妥的方式是把状态收进一个 SQLite 数据库,开启 WAL 模式,多进程可以同时读、单进程写,写锁粒度小,不会因为一个端在写就把另外两个端卡死。我自己在本地工具开发里的体会是:只要涉及"多进程共享状态"这个场景,尽早换 SQLite 比后续做文件锁迁移的成本低得多。
6.2 插件"装了但没激活"完整排查链路
我在实际使用中遇到过一次典型的插件激活失败,报错是:
harness failed to load plugins web boot: 2 entries did not activate @linxin666/dsh-p这种报错字面上是"Web 引导阶段有两个入口没激活",但它没有告诉你是哪两个入口、为什么没激活。我建议的排查链路是:
- 先跑
dsh plugin list --verbose,看插件整体的激活状态 - 再跑
dsh plugin inspect @linxin666/dsh-p,查看它的 manifest 和各 entry 文件的解析情况 - 检查
web.js/desktop.js这类 UI entry 是否存在,文件里有没有语法错误或 import 了当前环境没有的依赖 - 在设置里关闭插件再重新启用,确认是否是启动时序问题
实际情况里,"web entry 激活失败"最常见的根因是插件作者只测试了 CLI 入口,Web 入口只是留了一个占位文件,但启动时的依赖检查不通过。这类问题不是 DeepSeek Harness 独有的,所有跨端插件系统都会遇到,排查思路完全可以通用。
6.3 环境变量与安装位置细节
关于"装到 D 盘"这类问题,其实只需要把两个关键路径规划清楚。一个是安装目录,放可执行文件;另一个是数据目录,放 Session、日志、插件、skill。
DSH_HOME # 安装根目录,一般不写入数据 DSH_DATA_HOME # 数据目录,默认为 ~/.dsh如果你把两个目录混在一起,升级覆盖安装时就会连带删掉历史任务和插件配置。另一个高频坑是 PATH 没有配好,导致dsh命令找不到,或者找到的是旧版本的dsh。升级之后我一般会先跑一下dsh doctor,它会同时反馈安装路径、数据目录、插件状态和模型端点连通性,能省掉大量无头绪的排查时间。
7. 架构取舍复盘:哪些设计是对的,哪些可以再斟酌
多端架构没有标准答案。DeepSeek Harness 选择"CLI、Web、桌面端全都要",和很多只做 CLI 的 AI 工具形成了鲜明对比。像 Codex CLI、Claude CLI、Trae CLI 这类工具,核心阵地都在终端,界面只是辅助。DeepSeek Harness 把三端都作为一等公民,必然付出更高的前端维护成本,但换来的是更宽的使用场景。
7.1 与其他终端工具的对比
只做 CLI 的工具,交互闭环短、开发成本低,但很多人真的不想打开终端,尤其是配置插件、管理多智能体编排图的时候。DeepSeek Harness 把桌面端和 Web 端补上之后,相当于把同一套 Agent 能力分发到了不同习惯的用户面前。熟悉命令行的继续在终端里高效干活;习惯图形界面的用户可以在桌面端完成同样的事情。
这个取舍的代价也很明显:三个前端共享一套引擎,任何一个端的需求都可能牵动引擎接口的演进。比如为了配合 Web 端的实时推送,引擎层的事件流必须做成可订阅、可重放的形式,这就比单纯面向 CLI 的实现复杂得多。从工程实际看,这类工具更适合"先跑通 CLI,再补 Web,最后上桌面端"的推进路径。
7.2 我的实际使用建议
最后说几个真实使用建议。平时我自己会同时开着三个端,但职责是分开的:CLI 负责发起任务和跑脚本,Web 面板只在需要快速看进度时打开,桌面端则留在配置插件、查看编排图、管理模型端点时才用。不要三个端同时高频操作同一个 Session,虽然架构上支持,但没有必要。
一个小技巧:dsh web启动时如果遇到端口冲突,可以用dsh web --port 0让它自动分配一个空闲端口,终端打印出来的就是实际可用的地址。另外,dsh web打印的一次性 URL 建议直接复制到自己的私有笔记里,因为一旦服务重启,旧的 URL 就失效了,重新在终端翻历史输出比想象中麻烦得多。
多端架构这件事,说到底不是炫技,而是在保证内核能力一致的前提下,让不同习惯的人都能找到顺手的使用方式。能把这一步做稳,工具才谈得上被更多人真正用起来。