ARTICLE · 1197257
一切皆插件:搭 Agent 从写代码变成装配
DeepSeek Harness(命令行里叫 dsh)2026 年 8 月建仓,TypeScript 写的,MIT 许可。这阵子写它的中文技术号几乎都是同一套:一行命令装起来、配个 API Key、装几个插件、再感叹一句「一切皆插件」。口号谁都会念,底下的问题没人回答。
这篇把「一切皆插件」当成一道待验证的命题,去它自己的仓库和文档里找证据,也找代价。基准是官方仓库 deepseek-ai/deepseek-harness 的 0.2.1-alpha.4,对照前一版 0.2.1-alpha.2,依据是仓库内的 docs/ 中文原文与仓库结构。
一、命题一:一切真的都是插件吗
先去仓库看它把东西怎么切。packages/ 底下按能力分目录,面向用户的能力各占一个包:
core | |
session | |
llm | |
storage | |
sandbox | |
subagent | |
workflow | |
ptc-runtime | |
guard | |
goal | |
skill | |
mcp | |
fs | |
terminal | |
jobs | |
schedule |
再往下还有 web、document、deliverables、browser-use、computer-use 等等。
清单长不等于零件多。判断它有没有真的拆开,要看这些包之间怎么接。把核心包和它们在共享上下文里的落点摆到一起,接法就出来了:

图 1|核心组件图。左边每个包在右边共享上下文 ctx 上有一个对应服务,消费方只认这个键,不认包。
来源:据官方文档 docs/architecture.zh.md 的「核心包」表自绘。
右边那一列才是重点。模型、工具、会话、活跃 agent 注册表、文件系统,全都是 ctx 上的一个键位。这就把「一切皆插件」从口号变成了一个可以检查的问题:能不能只替换其中一格,而不动别的。
回到清单,先看 core。架构文档的原话是:
“ 产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志,以及 agent loop(智能体循环)本身,因此每个都可以从配置替换。
循环确实住在 packages/core 这个目录里,但 core 只是个目录名。同一份文档紧接着还有一句:不存在需要打补丁的特权内核,扩展 dsh 的方式是把插件挂载到其他插件旁边。
模型适配器是插件,这个常见;工具注册表是插件,也不稀奇;但连 Agent 循环本身都是插件,这就是另一回事了。对比第 1 篇里那两家:Qoder CN 的边界在「SDK ↔ 闭源 CLI 子进程」,你改不了循环;Qwen Code 的循环虽然开源可改,但它是 packages/core 里的一段代码,改它意味着你 fork 之后自己维护。dsh 的做法是把这个循环做成了配置项。
命题一成立,而且成立得比 slogan 更彻底。
二、命题二:插件化靠什么撑住
插件架构谁都会画,难的是卸载时不留残余。dsh 底下用的是 Cordis,一份 Koishi 生态里打磨过的插件内核。它的设计约定只有三条,但每条都指向同一个目标:
“ 插件向共享上下文贡献服务、类型化事件和可逆的副作用。
关键词是最后一条,可逆的副作用。提示词片段、工具 schema、适配器、提供方和监听器,全部通过 ctx.effect() 或 ctx.on() 安装,reload 和 teardown 时按注册顺序撤销。插件的卸载在这里不是「尽力清理」,而是框架层面的契约。
上面那句「贡献服务」落到代码里,是一个叫能力接缝(capability seam)的结构。官方把一项可替换能力拆成三个角色:声明接口的 Service Definition、实现它的 Service Provider、使用它的 Consumer。三个角色可以分在三个包里,而且互相不认识:

图 2|能力接缝的三个角色。定义包只放接口,实现包和消费包各占一边,彼此没有依赖。
来源:据官方文档 docs/architecture.zh.md 的「能力 seam」一节自绘。
这张图解释了替换为什么便宜:换提供方,动的只是实现包,消费方一行都不用改。官方在文档里把话说到更满一层:文件系统和进程的提供方共享同一个执行世界,所以把这两者指向远程沙箱,Bash、PTY 和 LSP 会跟着一起搬过去,不需要为某个提供方单独 fork 一份。
再往上是一套三层配置:
上层覆盖下层,你不动源码就能换掉任意一个能力。官方随仓库发的成套组合包有这几套,也能看出它的分层意图:
@deepseek-ai/dsh-base | |
@deepseek-ai/dsh-web-app | |
@deepseek-ai/dsh-headless | |
@deepseek-ai/dsh-sdk-app | |
@deepseek-ai/dsh-sdk-minimal | |
@deepseek-ai/dsh-acp-app |
从命令行到一棵跑起来的插件树,中间隔着一串调用。把这条链路摊开看:

图 3|启动装配链路。从 CLI 启动器一路到运行中的 dsh 实例,中间每一步都是官方某个包里的一个函数或加载器。
来源:据官方文档 docs/architecture.zh.md 的「Profile 与组合包」一节自绘,该节给出每一步对应的声明字段。
链路上真正属于你的只有一层:profile 各层。按官方文档,各层依次应用在一个空条目列表之上,顺序是 profile 里有序列出的组合包、它自己的 cordis.patch.yml、home patch,最后是命令行的 --patch overlay。一条 patch 按 id 定位到某个条目,把它的整个 config 换掉,也可以插入新条目。所以「不碰源码也能换」不是宣传语,它是配置层叠的必然结果。
撑起这套设计的形式化依据,仓库 README 是直接点名的:Cordis 的设计写在论文《A Programming Paradigm for Spatiotemporal Composability》(arXiv 2608.25512)里。也就是说,「一切皆插件」不是 dsh 发明的,它把一个已有论文描述的设计拿来当运行时底座,然后在上面盖了一整栋楼。
三条约定加三层配置还有个直接的副产品:SDK 和 ACP 在这里不是独立命令,而是两个 profile。web、headless、sdk、sdk-minimal、acp 是随发行版交付的模板 profile;desktop 由 Electron 桌面应用初始化并占用保留目录 $DSH_HOME/profiles/desktop,与 npm CLI 共享产品数据,但包、启用选择与锁文件各管各的。这些入口共用同一套启动器,差别只在装了哪些插件;连 SDK 都默认绑定 sdk 这个 profile,官方极简示例用的是 sdk-minimal,Python 的运行时 wheel 里装的也是同一个命令。第 1 篇比较过「SDK 与 daemon 的形态差异」,在这里它只是同一份配置的两个取值。
三、命题三:插件市场从哪来
命题一证明了零件可以换,命题二说明了靠什么撑住。但还有个没人问的问题:这些插件,从哪来?
0.2.1-alpha.4 给出了答案,也是这一版最实质的变化:插件市场。
它的机制朴素得有点反直觉:插件市场就是一个 JSON 索引文件,列出可安装的 DSH 组合包及其标题、图标、作者和版本。索引里每一条指向一个 npm 包,或者一个 Git 仓库里的具体目录。而最关键的半句是:
“ DSH 不内置任何索引源。
没有官方市场,没有默认源。你要用,得先去「插件 → 实验性插件 → 更多」里打开自定义插件市场这个实验开关;打开之后,「已安装」旁边才会出现发现标签页,Creator 模式同时获得一个只读的 plugin_market 工具。关掉开关,两者一起消失。
发布端也一样轻。索引是一份 UTF-8 的 JSON,默认不超过 128 MiB,以 2xx 状态码返回、不经重定向即可,放到任意静态主机上就算发布了,GitLab Pages、GitHub Pages、任何能走 https 的对象存储都行。图标可以内联成 base64,也可以并排放一个 icons/ 目录、用相对路径引用。每个索引源只属于当前 profile,不跨环境串门。
至于内容可信度,官方把话说在了前面:
“ 市场是第三方内容。每个插件的标题、描述、图标、作者和来源都由市场决定;DSH 检查文件格式,但不验证这些声明。
这句话单独看是免责声明,放在这里就是另一回事了:它和后面沙箱「只做到一半就直说」是同一套语气。
而这件事最有说服力的地方,是它在仓库里留下了时间戳。0.2.1-alpha.2 时,这功能还只是一份提案文档,躺在 .agents/notes/proposed/feature/2026-10-02-plugin-market-index-format.md;到 0.2.1-alpha.4,同一份文件已经挪进了 .agents/notes/implemented/feature/。同期新增的包里,boot/plugin-market、client/ui-plugin-market、util/plugin-market-index、experimental/plugin-market-bundle 都和市场有关。
所以「官方做得越少,社区越繁荣」这句话在这里有工程上的落点:主循环是插件,谁都能替换;插件市场不自建索引源,谁都能开一个。框架只负责定义索引格式,然后退到一边。
四、拉近看:扩展点挂在哪
前三节回答的是「拆得开吗」「靠什么撑住」「从哪来」。但还有个更实际的问题:你要往这套东西里加自己的行为,具体加在哪?
官方把一次轮次(turn)拆成零个或多个步骤(step),一个步骤是一次模型请求加上它调用的工具。整个过程不是一串函数调用,而是一条事件链,从轮次打开、组装提示词、发起模型请求、执行工具,一直到轮次关闭:

图 4|轮次与步骤生命周期。持久的回放事实落在 session/event 里,实时控制与状态走 agent/*。
来源:据官方文档 docs/agent-lifecycle.zh.md 自绘。
图里那几个 agent/pre-step、agent/request、llm/stream 和三个 tools/* 事件是 waterfall(瀑布式事件),监听器必须调用 next() 才能把控制权交下去;agent/turn-stopping 则是 serial 事件,没有 next()。所以「改 agent loop」在 dsh 里通常不是改循环的代码,而是把监听器挂到某个事件上。官方文档对这个机制只有一句话:「事件就是扩展点」,而选对事件域是大多数改动的第一个决定。
工具这一侧还有一张更细的图。一次工具调用要依次穿过策略、钩子、审批、沙箱和结果重写,而这些全都不是循环里的分支:

图 5|工具执行流水线。三个 waterfall 各自可以改写一次调用,任何一环抛错都会被归一化成 isError。
来源:据官方文档 docs/tool-execution-pipeline.zh.md 自绘。
按顺序走一遍:tools/pre-execute 瀑布先跑,钩子、权限和沙箱策略在这一步介入;接着是注册的单调守卫,它只能拒绝或弃权,不能改身份;守卫放行之后进入 tools/execute 瀑布,超时、重试、指标这类环绕分发的关注点包在这一层;再往里才是工具本体。工具跑完,tools/post-execute 瀑布可以接受、阻断、替换结果或补充上下文,finalizeContent 收最后一道仅限内容的不变式,tools/result 再把冻结的权威结果同步通知出去。
官方给这张图的说明是:它要展示的是「策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行」。这句话就是「一切皆插件」的工程版本:扩展点被固定成事件,而不是被写进循环里的条件分支。
五、代价一:工程约束变硬了
插件化不是白拿的。要让任意组合都能成立,工程上必须付出代价,而 dsh 把代价明明白白写进了开发文档。
第一条,注册必须是可逆的副作用。任何插件注册东西时,必须同时声明撤销路径,否则重载就会泄漏。
第二条,配置错误必须在加载时报错,不能静默跳过。缺失的 host 值不许提供默认值,注册表或必需 key 缺席就明确失败。这条听起来只是严格,实际是在防一类很难查的 bug:插件树里某处悄悄降级,运行几天之后才以奇怪的方式暴露。
第三条最关键,覆盖率门禁是逐文件 100%:
“ 覆盖率门禁(
pnpm run test:coverage):门禁级运行,对packages/*/*/src按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。
官方在这一条后面还补了一句:
“ 行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。
除此之外,普通包只能登记进 Host 或 Client 其中一个 aggregate,跨边界的标识符要用品牌类型(typert),lint 之外还单有一道 pnpm run doc-typecheck 检查文档代码块。这套门禁指向的是同一个目的:让仓库在大量外部分叉与快速迭代下保持稳定。装配式架构的自由度越大,约束就必须越硬,两者是一笔交易。
六、代价二:文档本身成了基础设施
第二个代价是文档。
docs/subsystems/ 下面按主题分文件,从 core、session、sandbox,到 voice-input、sidebar-right、worktrees,每个主题都配齐三份:英文、中文、翻译元数据。其中几份中文文档体量都大得需要分节跳读:配置目录、持久化目录(另附一份完整的 schema JSON)、模块图、工具目录、能力接缝,各自都是独立成篇的长文。
还有两个细节,比体量更说明问题:
一是 docs/postmortem/,事故复盘目录,逐篇编号归档,从 ACP 的默认导出丢注入,到 JS 表达式把文件系统工具关掉。一个刚开源不久的项目,把踩过的坑归档进文档结构里。
二是文档过期会直接阻断 CI。文档站构建前先跑 doc-sync,它用 TypeScript 解析器从源码里提取声明和对应的 JSDoc,断言文档中的代码块与两者同时匹配;改了一个已记录的类型却忘了同步文档,合并就会被卡住。
站在「搭 Agent」的视角看,这件事的含义是:你装配时依赖的不是 API 文档,而是一份把内部接缝也写出来的说明书。capability-seams.md 讲的就是「这个能力从哪接、换掉它要动什么」。要自己组装一个 Agent,这种文档比示例代码值钱。
七、代价三:自由,但不可回退
第三层代价出现在使用姿势上。dsh 同时提供四种运行模式(官方站点定义):
模式在实现上就是 Agent Preset。dsh-agent-preset 的文档写得很直白:
“ 在普通 Cordis YAML 中定义 Agent 的子插件列表。可同时声明多个 preset,由会话选择。声明会提前加载,修改只影响之后创建的 Agent。
最后半句是这套设计的前提条件。工具集决定了会话历史怎么被解释,所以模式改动的生效边界只能划在「之后创建的 Agent」,不能回头改一个已经在跑的会话。自由是有条件的自由。
沙箱也是同一套语气,有几档就说几档:
read-only | /dev/null 这类必需接收器 |
workspace-write | |
danger-full-access |
而且这三档只管文件系统效果,网络与进程可见性根本不在这套模式的定义范围内。底层用三个平台的原生机制:Linux 走 bwrap / Landlock,macOS 走 Seatbelt,Windows 走 ACL 受限令牌。而这一点它的写法很直接:后端必须上报强制执行完整性,full 表示该模式承诺的文件效果全部管控,partial 表示只管控了一个子集,「因此要求绝对保证的消费方必须拒绝或向上暴露这一区别」。沙箱主动报告自己只做到一半,把要不要接受交给消费方判断。
MCP 那一章同样把边界写清楚:每个服务器一个连接插件,不支持 MCP 提示词模板、人工输入征询、基于任务的执行和资源订阅。「一切皆插件」不等于「一切都能接」。
八、你实际能搭出什么
说回动手。装 Node,一行命令:
npx @deepseek-ai/dsh web默认在 http://127.0.0.1:3080 起 Web UI,启动时会顺手打开浏览器。第一次进去要配模型(DeepSeek 官方 API 或任意 OpenAI 兼容端点),然后选工作区。官方文档里的模型配置页:

图 6|官方文档的模型配置页,第一次进入时要在这里选模型端点。
来源:官方仓库 docs/user/guide/providers-models-page.zh.png。
跑通之后,你手上就有了这些可以组装的东西:
Headless 入口: dsh --profile headless "job"跑完打印最终答案就退出,天生适合 CI 和批处理;Code Mode:让模型写一段 TypeScript,把「五步工具调用」合成一次执行。中间结果只留在程序里,不进上下文; 事件日志:模型看到的一切都进一份只增不改的会话日志,系统提示词、思维链、工具调用与结果、子代理调度、每一次上下文注入都在里面。会话恢复、fork、搜索、回放,操作的是同一条事件流。
据内测社区观察,配套的 dsh-external 组织里已经攒了一批仓库,反馈板上的 issue 也在快速增长(二手来源,未经官方核实)。主仓库自己的 Issues 是关着的,只开了 Discussions。「官方做得越少」这句话,连入口设计都在执行。
九、它站在哪
把第 1 篇的三种形态,连同本篇的 dsh 和下一篇要写的 Pi 摆在一起,位置就清楚了。先看「你拿到什么」:
query() 循环 + 工具层 | |||
qwen serve | |||
| DeepSeek Harness | |||
前三种是「买一个 Agent」,差别在买的深浅;后两种是「自己装一个 Agent」,dsh 和 Pi 是这条路的两端:一个把零件拆到最细、配一整套目录让你自己拼,一个把核削到最小、明确告诉你进阶能力全部自己补。
再看一层:客户端和服务端之间那条线
上面那张表看的是能力面。再往下还有一层更要紧:Agent 跑在哪个进程里,客户端和服务端之间那条线由谁定义。这一层决定了你以后想换部署形态时要付多大代价。
qwen serve | |||
dsh 这一格展开看。Host 与 Client 是两个隔离的 TypeScript aggregate,两边的构建不会进同一个 ts.Program:Host 入口不导入 Client 的 Context 合并,Client 入口也不导入 Host 的服务。跨边界的调用得先用 @Remote 或 @RemoteScope 声明,没标记的方法根本不会出现在生成的 Client 类型里。客户端能调什么,在编译期就被划定了一次。
传输是另一回事。Connection 提供 RPC 载体、请求关联、信任边界和 /api 的 HTTP bridge;流式方法走多路复用的 WebSocket 或者进程内载体。官方在这件事上写得很直白:替换载体,不要求改动 Remote 描述符,也不要求改动客户端的编程接口。也就是说,「Agent 和界面在不在同一台机器上」被做成了一个可以替换的接口,而不是一个架构前提。
第 1 篇那几家交付的是各自定好的那条线,dsh 交付的是一条可以换的线。
十、一个人用,和一个团队用
前面讲的都是机制。最后落回使用场景:同一批零件,一个人用和一个团队用,装配时该选的几乎相反。
/api:HTTP 与多路复用 WebSocket | ||
danger-full-access | read-only 或 workspace-write | |
ctx.approval 逐次放行 | ||
role('secret') 的字段不进响应 | ||
headless |
这两列不是两套代码,是同一批插件加同一个权限选择器的两种取值。官方把沙箱模式(sandbox/mode)和审批策略(approval/policy)这两个相互独立的强制开关捆成具名预设,客户端把它当单个 Permissions 选择器呈现,配置表里存的是「未来会话的默认值」。
「一个团队用」那一列里,真正落到实处的是远程访问控制。@deepseek-ai/dsh-api-remote-access 由业务方注册基于 role 的 policy,用来判定非 owner 的 Peer 能调用哪些 Remote 方法,同时过滤列表结果、给每个 Peer 推一份访问快照。Gateway 每次调用都带着这次调用代表的 Peer;不是 owner 的调用,要先解析它访问的资源、再问一次 ctx.remoteAccess.decide(),被拒的调用连 lookup 都不解析,也不会激活 Agent。它还留了一条边界:不带 @Remote 标注的方法仍然是普通的服务方法,只是调用者仅限于进程内。访问控制约束的是「什么能成为 Remote 方法」,不是「服务能做什么」。
有一件事得说清楚:官方没有写任何面向企业部署的指南,也没有多租户、配额、计费这类章节。这张表是从机制上推出来的:这些接缝摆在代码里,说明「多个客户端共用一个 Host」是被设计过的场景,但「怎么把它变成一套能交给一整个团队的内部平台」,文档里没给答案。这大概也是 dsh 和 Qoder Cloud Agents 那一类产品最本质的差别:治理层由谁提供,一个不提供、留给自己补,一个连同运行时一起交付。
文档没给答案,不等于这个空缺没人填。有个第三方项目 MOVO(himovo/movo),自我定位就一句话:DSH 负责智能体如何运行,MOVO 负责智能体如何进入企业生产环境。它不是 dsh 的一部分,而是在 dsh 的 Runtime、Skill、工具与 MCP 之上,补上面那张表右列缺的那一层。
补出来的东西分两边,正好对上表里的两列。
员工那边是一个工作台。对话、检索企业知识、做多轮研究,回答要留下引用和支撑依据;PDF、DOCX、XLSX、PPTX 这类文档连同里面的图与图表一起解析;报告、文章、演示文稿、表格、PDF 直接生成成交付物。
管理员那边是一套管理面。组织、用户、角色有了归属,模型、知识、Skill、工具集中配置;敏感工具操作要审批,定时任务可编排,执行过程与产物留痕可审计。再加上一层统一网关把工作台、管理后台和几个后端服务串成一个整体,一次部署拿到的是完整产品,不是一堆零件。
还有一点是位置:模型服务自己接,应用数据、企业知识与运行服务都留在部署者自己的环境里,凭证加密存放。dsh 只交付运行时,这就是下游把它接住的样子。它自己划的边界也很清楚:只需要底层 Runtime,dsh 已经够了;要把 Runtime 变成可部署、可管理、能服务真实用户的产品,差的正是这一层。
它这套东西是按能力拆开的 dsh 包组起来的;仓库里的兼容性文档还明令禁止 fork 内核,DSH Core 的 fork 记为 none、状态 prohibited,不许改动 vendored 或已安装的 dsh 源码。命题一和命题二在第三方手上又各自复现了一次。
最后
回到标题那句话。dsh 真正改变的不是技术,是搭 Agent 这件事的重心:以前你要写代码,现在你要做装配决策。装哪套模型适配、留哪几层沙箱、要不要 Code Mode、会话日志存哪里,这些决定以前藏在框架的默认值里,现在被摊到配置文件上当选择题。
上一节讲的是部署形态。回到人本身:它适合已经想清楚自己要什么 Agent 的人,也适合团队里那个必须能回答「我们这套 Agent 到底由哪些零件组成」的人。不适合的也很明确:如果你只是想要一个开箱能用的编码助手,Minimal 模式那两个工具会让你当场失去耐心,Standard 模式下的装配工作量也不划算。
一句话:它把 autonomy 从「能不能改」推进到了「要不要装」,代价是你得先想清楚要装什么。
「Agent 构建」系列第二篇,下一篇见。
参考引用
DeepSeek Harness 官方仓库:https://github.com/deepseek-ai/deepseek-harness Cordis 仓库:https://github.com/cordiverse/cordis 论文《A Programming Paradigm for Spatiotemporal Composability》:https://arxiv.org/abs/2608.25512 MOVO 项目:https://github.com/himovo/movo dsh-external(第三方社区组织,未经官方核实):https://github.com/dsh-external















































































