☰
DeepSeek Harness 的 bundle/profile/patch 到底是什么?cordis.patch.yml 底层实现拆解
2026/10/28 1:52:43 网站建设 项目流程

CFzjy

AI Engineer


▍深耕领域AI Agent 架构设计Harness 插件开发Context Engineering多智能体协同 & RAG
▍技术底座PythonTypeScript·Cordis 插件MCP & Skills·FastAPIDocker
▍工程能力专注 Agent 运行时与 Harness 插件开发、长上下文与流式交互优化、LLM 全链路工程化落地

「 让 AI 交互更智能,让技术落地更高效 」|欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能!

DeepSeek Harness 的 bundle / profile / patch 到底是什么?cordis.patch.yml 底层实现拆解

📚《DeepSeek Harness 插件架构》连载专栏 · 第 05 篇

前四篇讲的是"插件怎么运行",这一篇讲"插件怎么组装成应用"。DSH 的安装与装配机制绕不开三个词——bundle(组合包)、profile(配置档)、patch(补丁层)——它们频繁出现在官方文档、插件市场页面和报错信息里,但很多教程只给命令不给原理,导致两个最常见的困惑:“为什么我装了插件它不生效?”“为什么我的配置被覆盖了?”

这一篇把三个概念拆到底层:bundle 是"我贡献什么"的答案,profile 是"这套应用由什么组成"的答案,patch 是两者之间的粘合层。读完你会理解 DSH 配置系统的层序模型(四层逐层覆盖)、覆盖的"整行替换"语义(不是深度合并!)、以及从 GitHub 装插件的构建坑。本文事实全部来自官方文档《打包与安装插件》。

痛点一:装了不生效。插件装进 profile 了,但启动后毫无反应——因为 patch 层的顺序和覆盖语义没搞懂。

痛点二:配置被神秘覆盖。你在 profile 里改的配置不生效,或者你发布给用户的默认配置被用户悄悄改掉了——层序规则没吃透。

痛点三:分发就是玄学。npm 发布、GitHub 直装、tarball 交付三种方式各有什么坑,全凭试错。

读完你会拿到:bundle/profile 两份 manifest 的完整结构、四层层序模型与覆盖语义、从打包到安装的全流程、三种分发方式的选型与避坑。想追完整系列(下一篇:0 基础学习路线图与概念百科),可以先关注我的 CSDN 主页。

文章目录

    • DeepSeek Harness 的 bundle / profile / patch 到底是什么?cordis.patch.yml 底层实现拆解
      • 一、为什么需要"层"?(原理拆解篇)
        • 1.1 从一个直觉误区说起:配置不是一棵树,是一叠纸
        • 1.2 三个概念的官方定义
      • 二、基础篇:两份 manifest 与一份 patch(官方结构全拆)
        • 2.1 组合包:三个文件的完整结构
        • 2.2 profile:两个文件的组合描述
        • 2.3 层序模型:四层纸的叠放顺序
      • 三、进阶篇:覆盖语义——"整行替换"不是深度合并
        • 3.1 官方定义与两个推论
        • 3.2 一段可运行的层组合模拟器
        • 3.3 表层组合包持有命令行:应用自己的 flag
      • 四、高阶:三种分发方式的选型与避坑
        • 4.1 从 GitHub 安装:prepare 脚本这道坎
        • 4.2 分发方式选型表
        • 4.3 版本兼容与 engines 声明
      • 五、实战避坑指南:高频问题与一键解决速查表
      • 结语

一、为什么需要"层"?(原理拆解篇)

1.1 从一个直觉误区说起:配置不是一棵树,是一叠纸

传统的应用配置模型是"一棵树":一个 config.json,字段层层嵌套,程序读它。DSH 的配置模型是"一叠纸":每一份 patch 是一张纸,纸上写满"插件行";多张纸按顺序叠起来,后放的纸盖住先放的纸。官方文档把这叫"层"(layer),把最终生效的配置叫"空根之上逐层组合"的结果。

为什么选"一叠纸"而不是"一棵树"?因为插件化生态的配置有三个刚性需求,树模型都满足不了:第一,多来源——插件作者想给默认配置、用户想改、机器想做本地偏好、命令行想做临时覆盖,四个来源要同时存在;第二,可覆盖——用户改作者的默认配置时,不能要求用户 fork 作者的包;第三,可追踪——出问题时能回答"这行配置到底是谁写的"。纸叠模型天然满足:每个来源一层纸,覆盖关系肉眼可见,--dump-config一打印,每层的来源都标得清清楚楚。

1.2 三个概念的官方定义

官方文档《打包与安装插件》给了最精确的定义,值得原文照抄:

“组合包(bundle)是附带一个配置层的 npm 包。它的 manifest 声明dsh.bundle,回答的是’这个包贡献什么?':一个插入或覆盖插件行的 patch 文件。”

“profile 是位于$DSH_HOME/profiles/<name>下、描述一份可启动组合的目录。它的 manifest 声明dsh.profile,回答的是’这套配置由哪些组合包按什么顺序组成?'。”

还有一句一锤定音的区分:“组合包是你编写并分发的东西;profile 是用户用dsh --profile <name>启动的东西。没有东西同时是两者。”记住这句话,后面所有概念都不会混。

二、基础篇:两份 manifest 与一份 patch(官方结构全拆)

2.1 组合包:三个文件的完整结构

官方文档给出的组合包最小结构:

hello-plugin/ ├── package.json # declares dsh.bundle ├── cordis.patch.yml # the layer applied when a profile lists this bundle └── index.js # plugin modules the patch rows reference

package.json的关键在dsh键:

{"name":"dsh-hello-plugin","version":"0.1.0","type":"module","main":"index.js","files":["index.js","cordis.patch.yml"],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}

cordis.patch.yml的内容与你开发期用--patch传的 overlay 完全相同——一个 patch 条目的 YAML 数组;唯一的区别是插件行按包名引用,而不是按相对源码路径,这样 Node 的模块解析才能找到已安装的代码:

-insert:-id:helloname:dsh-hello-plugin

官方还给了两个容易忽略的细节:patch 可以是有序文件列表(["./base.patch.yml", "./web.patch.yml"]),launcher 按序把它们作为同一层应用;没有dsh.bundle声明的包也能安装,但只作普通依赖——dsh plugin会打警告、不激活任何层,这种格式专供"给其他插件包 import 的库"使用。

2.2 profile:两个文件的组合描述

profile 目录同样极简:

  • package.json— 树外插件依赖(由 pnpm 管理)+dsh.profilemanifest(含有序bundles列表)
  • cordis.patch.yml—用户自己的 patch 层,在每个组合包层之后应用

官方强调 profile manifest 从不需要手写:dsh plugin add会自动初始化(base 作为第一个 bundle)并维护列表。装完 hello-plugin 后的 profile manifest 长这样:

{"name":"dsh-profile-demo","private":true,"dependencies":{"dsh-hello-plugin":"link:/path/to/hello-plugin"},"dsh":{"profile":{"bundles":["@deepseek-ai/dsh-base","dsh-hello-plugin"]}}}

注意bundles是有序列表——顺序就是层序(下一节细讲)。以及官方对依赖的三个分类值得记住:需要与宿主共享实例的包,同时声明 peerDependencies 与 devDependencies(peer 用安装中的副本、dev 供类型检查);需要独立版本的第三方依赖放 dependencies。

2.3 层序模型:四层纸的叠放顺序

官方文档给出的生效配置组装顺序(从底到顶):

第 1 层:各组合包的 patch
按 profile.bundles 列表顺序
dsh-base 在前,后装的在后

第 2 层:profile 自己的 cordis.patch.yml
(用户的组合层配置)

第 3 层:$DSH_HOME/cordis.patch.yml
(机器本地偏好,各 profile 共享)

第 4 层:--patch overlay
按 argv 顺序(临时覆盖)

生效配置 = 空根之上逐层组合

四层各管一摊:组合包层是"出厂配置"(作者给的默认);profile 层是"这套应用怎么组合"(用户的项目级选择);home 层是"这台机器怎么调"(跨 profile 的本地偏好);overlay 层是"这次启动怎么试"(临时实验)。层序即权限序:越晚的层越权威。

三、进阶篇:覆盖语义——"整行替换"不是深度合并

3.1 官方定义与两个推论

官方文档对覆盖语义有一句必须刻进脑子的定义:“后应用的层按行胜出,且 patch 会替换目标行的整个config值,而不是深度合并各键。”这句话拆成两个推论,每个都直接决定你的 patch 怎么写:

推论一:覆盖别人的行,必须重述每一个键。你的 bundle 想覆盖dsh-base里某个插件的超时配置,不能只写timeoutMs: 60000——那样整行 config 只剩这一个键,其他键全丢。必须把目标行需要的全部键重述一遍(官方举的例子是dsh-web-app覆盖dsh-base的行就是这么干的)。这是"整行替换"语义最容易踩的坑:少写一个键,不是"用默认值",而是"丢了这个键"。

推论二:用户永远能覆盖你,无需改你的包。用户在自己 profile 的cordis.patch.yml里按 id 覆盖你的行就行。官方由此给 bundle 作者一条设计建议:“优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。”翻译:别在 patch 里写死"你必须用我的配置",把可选项留给用户层——你的包提供合理默认,用户的层拥有最终决定权。

3.2 一段可运行的层组合模拟器

层序与覆盖语义写成可运行代码(教学等价实现):

"""Cordis patch 层组合模拟器:四层层序 + "整行替换"覆盖语义(非深度合并)。"""from__future__importannotationsfromcopyimportdeepcopydefapply_layer(rows:dict[str,dict],layer:list[tuple[str,dict]])->dict[str,dict]:"""应用一层 patch:按 id 定位行,整行替换 config(不深度合并)。"""result=deepcopy(rows)forrow_id,configinlayer:ifrow_idinresult:print(f" [覆盖] id={row_id}整行替换 config={config}")else:print(f" [新增] id={row_id}config={config}")result[row_id]=deepcopy(config)returnresultif__name__=="__main__":# 第 1 层:组合包层(作者的出厂配置)rows:dict[str,dict]={}rows=apply_layer(rows,[("bash",{"timeoutMs":5000,"sandbox":True,"shell":"bash"}),("model",{"provider":"deepseek","temperature":0.7}),])print(" 第 1 层结果:",rows)# 第 2 层:profile 层,用户只改超时 —— 注意"整行替换"的坑print("第 2 层(用户只写 timeoutMs):")rows=apply_layer(rows,[("bash",{"timeoutMs":60000})])print(" 结果:",rows["bash"],"← sandbox 和 shell 丢了!")# 正确姿势:重述全部键print("修正(重述全部键):")rows=apply_layer(rows,[("bash",{"timeoutMs":60000,"sandbox":True,"shell":"bash"})])print(" 结果:",rows["bash"])# 第 3 层:home 层机器偏好,第 4 层:--patch 临时覆盖print("第 3+4 层(按序胜出):")rows=apply_layer(rows,[("model",{"provider":"deepseek","temperature":0.0})])rows=apply_layer(rows,[("model",{"provider":"ollama","temperature":0.0})])print(" 最终:",rows["model"],"← 第 4 层胜出")

跑一遍你会亲眼看到"整行替换"的坑:用户层只写timeoutMs,sandbox和shell两个键直接消失——这就是生产环境里"改了个超时,沙箱没了"这类事故的完整机制。以及最后两行:层序即权限序,最晚的--patch临时覆盖胜出。

3.3 表层组合包持有命令行:应用自己的 flag

官方还给了 bundle 的一个高级能力——表层组合包可以定义自己的命令行。挂载一个注入cmdlineArgs的启动插件,用parseCmdline解析自己的 flag,再通过!!js在配置里引用:

-id:my-appname:'@example/my-app'inject:[myAppStartup]config:port:!!jsctx.myAppStartup.port??8080

两个要点:!!js让配置值在加载时求值(官方第 05 章配置教程讲过,仅 config 与 disabled 字段有效),这里用它读命令行结果、部署取值做回退;遇到--help时提供方不发布服务,这些行不激活——"–help 不启动应用"这种细节都由同一套声明机制自然实现,不需要特判代码。

四、高阶:三种分发方式的选型与避坑

4.1 从 GitHub 安装:prepare 脚本这道坎

官方文档专门花了一节讲这个坑,因为它几乎人人会踩:“git 安装拉取的是源码,不是构建产物:没有任何环节运行你的build脚本,因此 TypeScript 包到手时没有lib/输出,加载会失败。”解法是双方各做一件事:

作者提供prepare脚本——pnpm 在 git 安装后运行它,从源码构建发布入口;脚本必须自包含(不能假设旁边有 monorepo checkout)。用户为构建授权——pnpm ≥10 默认拒绝运行 git 依赖的 prepare 脚本,第一次add会失败,要按 dsh 提示把包键加进 profile 的pnpm-workspace.yaml:

allowBuilds:dsh-hello-plugin:true

官方随之给了一句安全警告,值得原文记住:“请把这项授权视为允许该包的代码在安装时于你的机器上执行,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(github:you/hello-plugin#<sha>),让后续推送无法悄悄改变实际运行的内容。”——供应链安全不是口号,是安装命令里的日常。

4.2 分发方式选型表
分发方式用户操作构建授权适用场景
npm 发布dsh plugin add your-package不需要正式发布(第 13 篇全链路)
tarball 交付dsh plugin add ./pkg.tgz不需要内部分发、离线环境
GitHub 直装dsh plugin add github:you/pkg#<sha>需要 allowBuilds尝鲜、私有仓库分发

官方的一句话建议:“如果不想让用户做这项授权,就改为分发构建产物。”npm 发布或 tarball 都是预构建交付,用户零授权。生产分发首选 npm/tarball,git 直装留给开发期和私有场景。

4.3 版本兼容与 engines 声明

官方还给了版本兼容的标准姿势:用engines.dshSemVer 范围声明组合包支持的 DSH 版本(如"engines": { "dsh": ">=0.2.0 <0.3.0" }),安装和 profile 启动都会拒绝不满足范围的包;没有engines.dsh时,框架检查其@deepseek-ai/dsh*peer 依赖范围。这一条看似琐碎,实则决定你的插件生态口碑:DSH 迭代快,声明兼容范围 = 用户升级 DSH 时你的插件不会静默坏掉,而是在安装/启动期被明确拒绝、有据可查。

五、实战避坑指南:高频问题与一键解决速查表

序号遇到的报错或现象背后的真正原因一句话解决办法
1装了插件不生效包没声明 dsh.bundle,只作普通依赖安装package.json 加dsh.bundle.patch声明
2覆盖别人配置后其余键消失patch 是整行替换,不是深度合并重述目标行全部键,不能只写改动项
3改超时后沙箱功能失效同上——config 只剩 timeoutMs 一个键同上;发布前用 --dump-config 核对
4git 安装 TypeScript 包后加载失败git 拉的是源码,没有 build 产物作者加 prepare 脚本;用户配 allowBuilds 并锁 commit
5用户改的配置被你的包"改回去"你的 bundle 层序在用户层之后(层序反了)记住四层层序:出厂配置给默认,用户层天然覆盖你
6升级 DSH 后插件静默行为异常没声明 engines.dsh 兼容范围package.json 加 engines.dsh SemVer 范围
7–dump-config 看不懂层来源不了解层的概念与打印格式对照 2.3 四层层序图逐段核对

这张表的核心心法和前几篇一脉相承:DSH 的一切行为都有明确语义,坑全在"凭直觉"里。层序、整行替换、构建授权——每一条都写在官方文档里,只是大多数人没读到这一页。

结语

本文把 DSH 的装配机制拆成了三个词的完整闭环:bundle 是"我贡献什么"(npm 包 + dsh.bundle.patch 声明 + patch 文件);profile 是"这套应用由什么组成"(目录 + dsh.profile.bundles 有序列表 + 用户层);patch 是两者间的粘合层(YAML 行数组,按层序逐层组合)。四层层序(组合包 → profile → home → overlay)决定"谁的配置说了算",整行替换语义决定"覆盖时怎么写不会丢键"。再加上三种分发方式的选型与供应链安全纪律,你就拥有了从"写插件"到"发插件"的完整装配知识。

一张图总结全文:

知识要点文字回顾:① bundle 回答"贡献什么"、profile 回答"由什么组成"、没有东西同时是两者;② 层序即权限序:组合包层 < profile 层 < home 层 < overlay 层,后层按行胜出;③ patch 覆盖是整行替换不是深度合并——重述全部键;④ 用户层永远能覆盖你的默认,所以默认值要选"大概率被保留"的;⑤ git 直装拉源码不构建:作者 prepare + 用户 allowBuilds + 锁 commit;⑥ 生产分发走 npm/tarball,用户零授权。

下一篇连载预告:《DeepSeek Harness 学习路线图:0 基础到发布第一个插件,附 20 个核心概念速查表》——原理篇收官后的学习地图,关注 CSDN 主页 不错过更新。

你装 DSH 插件时踩过"装了不生效"的坑吗?最后发现是什么原因?评论区聊聊,我挨个回。本文事实基于 DeepSeek Harness 官方文档《打包与安装插件》(2026-09 版本),有出入评论区提醒我。

参考资料(正文数据来源):DeepSeek Harness 官方文档《打包与安装插件》、CLI 行为参考、Cordis 教程《配置》一章。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询