2 · 核心概念
这一章只讲六个必须理解的概念:插件、装配 id 与访问路径、三处逐字一致、lib/ 构建产物、
profile 与 patch 覆盖层、包根形态。理解完,你就能看懂任何一份 cordis.patch.yml。
插件:DSH 的部署与运行单元#
DSH 是全插件(all-plugin)架构:你不改宿主,只写插件、声明要用哪些服务,然后在 apply() 里消费它们。
一个插件就是一行 Cordis patch——装进宿主后,宿主启动时会逐行加载。
- insert:
- id: kth-bus-my-plugin
name: kth-bus-my-plugin插件有两种形态,先想清楚你要哪种:
| 形态 | 你要提供 | 适用 |
|---|---|---|
| Host-only | package.json + cordis.patch.yml + index.js(+ 构建脚本) | 后台服务、HTTP API、工具、调用 LLM/Agent、定时任务。KTH 业务插件多是这种,页面由服务端返回自包含 HTML。 |
| Host + Web Client | 再加客户端入口(dsh.client + ./client) | 要往宿主 Web UI 里加页面 / 面板 / 节点。 |
装配 id ≠ 访问路径#
这是最容易踩的坑:patch 行的 id 只是装配标识,网页 URL 由插件自己注册。
| 装配 id | 访问路径 | |
|---|---|---|
| 来自 | 插件库 cordis.patch.yml 的行 id | 插件在 ctx.webServer.register({ path }) 里自注册 |
| 例子 | kth-bus-yyjz-agents | /bus-yyjz-agents/ |
| 用途 | 装配、依赖、装卸的标识 | 浏览器里访问的 URL |
| 关系 | 通常等于包名 | 与 id 无关,可任意取名 |
访问的是 http://localhost:3080/bus-yyjz-agents/,不是 /kth-bus-yyjz-agents/。
要确认某个插件注册了什么前缀,看它的 src/index.ts 里的 path。
三处逐字一致#
加载器要求「注册 id 等于包名」,所以下面三处必须逐字一致(第三处仅 Host + Web Client 形态需要):
| 位置 | 值示例 | 说明 |
|---|---|---|
package.json 的 name | kth-bus-my-plugin | 包名 |
cordis.patch.yml 的行 name | kth-bus-my-plugin | 装配引用 |
前端 window.__ModuleLoader__.load({ id }) | kth-bus-my-plugin | 客户端注册 id |
构建产物 lib/:宿主加载它,不加载 src/#
你写的是 src/,宿主读的是 lib/。所以改完源码必须重新构建,并重启宿主,
否则你会对着旧行为排查半天。各插件构建通常用 esbuild 把 TS 打成自包含 ESM。
# 在插件自己的包根里(不是库根、不是别处)
npm install
npm run build # 产出 lib/index.js
node --check lib/index.js打成自包含产物(不 import 宿主包 @deepseek-ai/dsh-* / @deepseek-ai/cordis),运行时用 ctx.get() 取服务、自持最小类型;lib/ 不入库。
profile 与 patch 覆盖层#
- profile 是宿主的组合清单(客户基座里装了哪些插件)。
--patch覆盖层只做insert:把本地插件按序追加进来,不改动 profile,也不卸载官方插件。- 行
name相对本文件解析:写../../Infrastructure/...这类相对路径,换机器不用改绝对路径。 - 生效时机:bundle 变更下次启动生效——改完 patch 要重启宿主。
- insert:
- id: kth-infra-configmanagement
name: ../../Infrastructure/infra-configmanagement/content/lib/index.js
- id: infra-database-postgresql
name: ../../Infrastructure/infra-database-postgresql/content/lib/index.js
- id: kth-bus-yyjz-agents
name: ../../Infrastructure/bus-yyjz-agents/lib/index.js启动时用 --patch 指定覆盖层(它是启动器参数,必须写在 --no-open 之前):
$env:DSH_HOME = "$HOME\kth\.dsh-home"
cd $HOME\kth\kth
pnpm dsh web --patch "$HOME\kth\kth-deploys\local-dev\dev-plugins.patch.yml" --no-openexport DSH_HOME="$HOME/kth/.dsh-home"
cd ~/kth/kth
pnpm dsh web --patch ~/kth/kth-deploys/local-dev/dev-plugins.patch.yml --no-open用 dsh --profile <name> --dump-config 看最终组合,确认你的行生效、且没被更高优先级层覆盖。
包根两种形态与菜单内嵌#
加载器找包根时:优先 <插件>/content/,否则用 <插件>/ 本身——两种形态都要兼容。
bus-image-detection/ bus-yyjz-agents/
├── content/ ← 优先 ├── package.json ← 直接用包根
│ ├── package.json ├── src/index.ts
│ └── lib/index.js └── lib/index.js
└── (其余源码) (无 content/ 目录)
package.json 里的 dsh.embed 是给 kth-base-permission 用的菜单项,
声明「这个页面可以作为菜单内嵌」。内嵌时宿主用 ?embed=1 隐藏自带顶栏。
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"embed": [{ "label": "我的页面", "path": "/my-plugin/" }]
}一次请求 → 插件 → DSH seam#
GET /bus-yyjz-agents/api/xxx
│
▼
DSH 宿主 · webServer seam
│ 按前缀路由匹配
▼
kth-bus-yyjz-agents 插件(自注册前缀 /bus-yyjz-agents/)
│ apply() 里按需组合上下文
├──▶ ctx.auth 会话校验(登录守卫)
├──▶ ctx.infraConfig 读台账(台账 > 代码缺省)
├──▶ ctx.agents 跑智能体 / 会话
├──▶ ctx.llm 流式模型调用
├──▶ ctx.tools 注册 / 调用工具
└──▶ ctx.skills 技能(<DSH_HOME>/skills)
│
▼
返回 JSON,或自包含 HTML 页面