| name | dsh-bundle-and-profile |
| description | 把 dsh 插件做成可安装/可发布的包时使用——package.json 的 dsh.bundle / dsh.profile / dsh.client 字段、cordis.patch.yml 写法、patch 层的应用顺序与按 id 覆盖、dsh plugin add(tgz / git / npm)、profile 目录结构、从 GitHub 安装的 prepare + allowBuilds 坑、files 白名单与 publint、发布与版本同步。也用于排查"插件装了但没生效"。 |
打包、安装与 patch 层
先运行 python3 tools/check-harness-drift.py。目标版本的真源是
vendor/deepseek-harness/docs/user/develop/basic/publish.zh.md(架构侧是
docs/architecture.zh.md,实现侧是 packages/boot/app-boot/README.md)。包字段以目标
插件实际安装版本和上游同版本 manifest 为准。
两个概念别混
都由一份 package.json 描述,但 dsh 键下的 manifest 种类不同:
- 组合包(bundle) —— 附带一个配置层的 npm 包。声明
dsh.bundle,回答
"这个包贡献什么?"。这是你写并分发的东西。
- profile ——
$DSH_HOME/profiles/<name> 下描述一份可启动组合的目录。声明
dsh.profile,回答"这套配置由哪些组合包按什么顺序组成?"。这是用户
dsh --profile <name> 启动的东西,由 dsh plugin 维护,不手写。
没有东西同时是两者。
组合包骨架
my-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # profile 列出本包时应用的那一层
└── lib/index.js # patch 行引用的插件模块
{
"name": "@you/dsh-my-plugin",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"files": ["lib/*.js", "lib/types/**/*.d.ts", "cordis.patch.yml", "README.md", "LICENSE"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
cordis.patch.yml 是一个 patch 条目的 YAML 数组。按包名引用(不是相对源码
路径),这样 Node 才能解析到已安装的代码:
- insert:
- id: my-plugin
name: '@you/dsh-my-plugin'
没有 dsh.bundle 声明的包仍能装,但只作为普通依赖:dsh plugin 会警告,且不
激活任何层。给插件 import 的库就该用这种形态。
层序("装了但没生效"先查这里)
在空条目列表之上按顺序叠:
- profile 的
dsh.profile.bundles 里每个组合包的 patch,按列表顺序
(@deepseek-ai/dsh-base 总是第一个)
- profile 自己的
cordis.patch.yml
- home 级
$DSH_HOME/cordis.patch.yml(各 profile 共享的机器本地偏好)
- 每个
--patch <path> overlay,按 argv 顺序
后应用的层按行胜出,且 patch 替换目标行的整个 config 值,不做深度合并。
两个推论:
- 你可以按
id 覆盖前面层的行,但必须重述那一行需要的每一个键,不能只写
改动的那个。
- 用户能在自己 profile 的
cordis.patch.yml 里覆盖你的行而不改你的包。所以只把
用户大概率会保留的值写成 patch 默认值,其余交给 schema。
内置组合包名始终从 dsh 安装目录解析;pnpm 只管树外的包。
!!js 表达式由 cordis-plugin-include 解析:Loader 在该行声明的注入激活后,基于
插件上下文插值 config,disabled 字段基于 loader 上下文插值。
启动组合与用户 patch HMR
锁定上游的 app-boot 在 boot 时读取 dsh.profile.bundles,同时用
watchUserPatches 监视 profile/home 的用户 patch。有效 patch 更新会按完整层序重新组合;
读取、解析或 Loader 候选失败时保留上一棵好树并发出失败事件。bundle 清单不是这个
watcher 的输入,所以增删 bundle 后要按目标版本行为重启验证。
不要把“配置行能 HMR”推成“新包一定能无重启安装”:模块解析、Client graph 和生产/
开发 HMR 还各有边界。标准安装仍走 dsh plugin add,然后用 --dump-config 加一次真实
boot 验证;实验性的 link: + patch 回路只用于已审阅的测试环境。
安装
dsh plugin --profile <name> <args...> 在 profile 目录内转发给 pnpm,所有
pnpm 子命令都能用。
dsh plugin --profile web add ./my-plugin-0.1.0.tgz
dsh plugin --profile web add @you/dsh-my-plugin
dsh plugin --profile web add github:you/my-plugin#<sha>
dsh plugin --profile web remove @you/dsh-my-plugin
首次使用会初始化 profile(@deepseek-ai/dsh-base 作为第一个组合包),并因为包
声明了 dsh.bundle 而把它追加进 dsh.profile.bundles。
先验证层,再启动:
dsh --profile web --dump-config
开发期最快回路:不装包
- insert:
- id: hello
name: '/abs/path/to/my-plugin/src/my-plugin.ts'
config:
greeting: 'Hi there'
dsh web --patch ./scratch/cordis.yml
patch 文件只贡献配置,不改变 loader 解析模块路径时用的 profile 目录——所以
这里的路径必须绝对。
从 GitHub 装:构建脚本这道坎
git 安装拉的是源码,不是构建产物,没有任何环节会跑你的 build。TypeScript
包到手时没有 lib/,加载直接失败。两边各要做一件事:
- 作者提供
prepare 脚本(pnpm 在 git 安装后跑它),且必须自包含:不能
假设旁边有 monorepo checkout。
- 用户授权构建:pnpm ≥10 默认拒绝跑 git 依赖的
prepare,第一次 add 会
失败;把 pnpm 打印的确切包键写进该 profile 的 pnpm-workspace.yaml:
allowBuilds:
'@you/dsh-my-plugin': true
如实看待这项授权:等于允许该包的代码在安装时于本机执行,且不在任何 agent
沙箱内。只对源码可信的包授权,并锁 commit(#<sha>),让后续推送无法悄悄改变
实际运行的内容。
想让用户免掉这一步,就分发构建产物:发 npm(pnpm publish 时构建好 lib/),
或 pnpm pack 交 tarball。
客户端半(浏览器插件)
要往 Web UI 贡献界面,在 package.json 声明 dsh.client 并从
exports["./client"] 导出构建好的 bundle:
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"client": {
"platform": "web",
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-sidebar"
]
}
}
宿主的 ctx.clientModules 扫描声明了 dsh.client 的包,组出
window.__DSH_BOOT__ 图,并在 /plugins/<id>/client.js 提供 bundle。
immediately: true 标记第一阶段预取。开发环境的 Client HMR 由独立插件驱动,生产
graph 不包含 HMR 行;详见
vendor/deepseek-harness/docs/subsystems/client-modules.zh.md。不能把开发期热更新能力
当成生产保证。
浏览器半和 Node 半之间要通信,走目标版本公开的 Connection / Remote 契约;只想同主机
可达时使用同版本文档声明的 loopback authority,不从旧插件示例猜当前 API。
发布
python3 ../tools/check-plugin.py .
pnpm pack
pnpm publish
files 白名单要与真实入口一致:包含 manifest/exports 指向的 JS、声明、
cordis.patch.yml 和必要运行资产。默认不发 src、声明映射或无消费方的中间产物;
是否发布 JS sourcemap 是项目自己的可审计策略,不是所有外部插件的统一上游规则。
用 pnpm pack 检查 tarball,再用 publint 验证入口。peerDependencies 声明对 dsh 的
兼容范围,并在开发依赖里安装同一目标版本供编译和测试。
发布后记得显式升级并验证目标 profile 里的版本——profile 的 dependencies 是独立
解析的,不会因为本地仓库或文档更新就自动变成新包。
排查"装了但没生效"
dsh --profile web --dump-config 里有你的层吗?
- 没有 → 包缺
dsh.bundle,或没进 dsh.profile.bundles
- 层在,但行被覆盖了?—— 后面的层按
id 胜出,检查 profile 和 home 的 patch
- 行在,但插件没加载?——
inject 的服务在这个 profile 里不存在(比如 headless
没有 UI 服务),或 apply 抛异常进了 FAILED
- 装的不是你以为的那份代码?—— 查 profile 的
pnpm-lock.yaml:link: 才是本地
源码,版本号是 registry tarball
- 客户端半没出现?—— 行若在 bundles 清单里,插件集合变更要重启;行若在
profile/home patch 层则热生效,浏览器 F5 即可。确认
exports["./client"] 指向的
bundle 真的构建出来了(未构建的行会返回大声的 404)