7 · 业务插件开发(模板)
面向要开发自己业务插件的开发者:以 bus-yyjz-agents(包名 kth-bus-yyjz-agents,
永盈子智能体平台)为模板,讲清「前端业务应用 + 后台管理配置 + DSH 运行时」三件套怎么组织,
以及如何照抄改造成你自己的插件。
请先按 4 · 快速开始 把模板跑起来。本页所有路径与字段都取自该仓库真实源码,
不是示意。贯穿模板的一句话口径:运行时不另造一套,一切落到 DSH 原生能力——
子智能体走 ctx.agents、模型走 ctx.llm、工具走 ctx.tools、
MCP 走 @deepseek-ai/dsh-mcp-client、技能走 dsh-skill-filesystem。
三件套:一个插件,两个前台,一套运行时#
目录 web/,零构建依赖静态站点,由插件自己托管在 /<前缀>/,可被权限菜单以 iframe 内嵌。
目录 web/admin/,即「资源注册台」:注册模型 / 工具 / 技能 / MCP / 插件,管理项目与子智能体。
目录 src/runtime/,把库里登记的资源配置成 ctx.llm / ctx.tools / MCP / 技能,再用 ctx.agents 跑会话。
三者共用同一个 领域库(kth_yyjz_agents_* 表):后台写入资源与配置,运行时读出来装配成 DSH 能力,前台消费生成结果。分工与理由见模板的 docs/plan/2026-10-09-runtime-mapping.md。
目录结构#
bus-yyjz-agents/
├── package.json # 包名 + dsh.bundle.patch + dsh.embed + scripts
├── cordis.patch.yml # 装配行:宿主按它把插件注入 profile
├── build.mjs # esbuild 打包 + 拷两个前端产物到 lib/web
├── src/
│ ├── index.ts # 插件入口:取服务 + 注册路由 + 装配运行时
│ ├── config.ts # 台账应用名 / 库应用名 / 前缀 / 键名与解析
│ ├── http.ts # 极简 HTTP 类型与工具(readJson / sendJson / openSse)
│ ├── contract.ts # 依赖服务的本地契约副本
│ ├── roles.ts # KTH 角色 code → 业务前端角色 映射
│ ├── assets.ts # 静态站点托管(web 与 web/admin)
│ ├── db/
│ │ ├── schema.ts # 逻辑表名 → 实际表名 + 幂等 DDL
│ │ └── store.ts # 建表 + 启动自检
│ ├── domain/ # 领域层持久化(单向依赖的底)
│ │ ├── shared.ts # id / 时间戳 / JSON 文本 工具
│ │ ├── models.ts tools.ts skills.ts connectors.ts plugins.ts
│ │ ├── platform.ts # 项目 / 子智能体 / 每子智能体配置
│ │ └── business.ts # 公司 / 会话 / 消息 / 三场景报告
│ ├── runtime/ # 把库里的资源翻译成 DSH 原生能力
│ │ ├── model-adapter.ts model-registry.ts
│ │ ├── tool-registry.ts mcp-registry.ts
│ │ ├── skill-store.ts
│ │ └── agent-setup.ts resolve.ts run.ts sse.ts
│ ├── api/ # 接口分发
│ │ ├── core.ts # 状态 / 登录 / 鉴权守卫
│ │ ├── admin.ts # 资源注册台 + 子智能体平台(仅管理员)
│ │ └── business.ts # 业务接口(含 SSE)
│ └── security/crypto.ts
├── web/ # 业务前台(零构建静态站点:index.html + app.js + styles.css)
├── web/admin/ # 配置后台(零构建静态站点)
├── tools/ # register-ts.mjs / selfcheck.mjs / ts-hooks.mjs
└── docs/plan/ # 决策与理由:数据模型 / 运行时映射 / 配置与资源| 路径 | 职责(一句话) |
|---|---|
| package.json | 包名(name)与装配入口:dsh.bundle.patch 指向 cordis.patch.yml;dsh.embed 声明可被权限菜单内嵌的地址。 |
| cordis.patch.yml | 装配行:宿主按它的 insert 条目把插件注入 profile。 |
| build.mjs | 用 esbuild 把 src/index.ts 打成 lib/index.js,并把 web/、web/admin/ 拷进 lib/web。 |
| src/index.ts | 插件入口 apply(ctx):取依赖服务、注册 /<前缀>/ 路由、装配运行时注册表与启动自检。 |
| src/config.ts | 台账应用名 / 库应用名 / 路由前缀 / 台账键名,以及配置解析(台账 > 代码缺省)。 |
| src/http.ts | 极简 HTTP 类型与工具:readJson / sendJson / sendText / openSse。 |
| src/contract.ts | 依赖服务(infraConfig / infraDatabase / auth / 可选的 llm / agents)的本地契约副本。 |
| src/assets.ts | 托管两个 SPA 的静态产物,带 SPA 回退;刻意不设 X-Frame-Options,否则菜单内嵌会白屏。 |
| src/db/schema.ts | 逻辑表名 → 实际表名映射 + 幂等 DDL。 |
| src/db/store.ts | 幂等建表与启动自检(回查 information_schema)。 |
| src/domain/* | 领域层持久化:资源库(模型 / 工具 / 技能 / 连接器 / 插件)+ 子智能体平台 + 业务。 |
| src/runtime/* | 运行时装配:把库里的资源翻译成 ctx.llm / ctx.tools / MCP / 技能,并用 ctx.agents 跑会话。 |
| src/api/* | 接口分发:core(状态 / 登录)、admin(管理)、business(业务 / SSE)。 |
| web/ | 业务前台静态站点,挂 /<前缀>/。 |
| web/admin/ | 配置后台静态站点,挂 /<前缀>/admin/。 |
依赖方向是单行道:db → domain → runtime/api。领域层不反向 import 运行时或接口层,单文件控制在 500 行内。
三处逐字一致#
「package.json 的 name、cordis.patch.yml 的行 name、
客户端 window.__ModuleLoader__.load({ id }) 三处逐字一致(加载器要求注册 id 等于包名)。」
| 位置 | 本模板的值 / 说明 |
|---|---|
package.json 的 name | kth-bus-yyjz-agents |
cordis.patch.yml 行的 name(与 id) | kth-bus-yyjz-agents |
客户端 loader 的 id | 本模板是 Host-only 单包(无客户端 bundle),故无此项;只在 package.json 声明 dsh.bundle.patch。带整页客户端的插件(如 kth-base-permission)才需要第三处。 |
装配 id(patch 行的 id,通常等于包名 kth-bus-yyjz-agents)只是宿主里的标识;
用户访问的 URL 是插件自己注册的 /bus-yyjz-agents/。两者别混。命名层级铁律见
Infrastructure/AGENTS.md:目录前缀 = 层级 = 包名前缀(bus-* → kth-bus-*)。
路由注册:页面路径由插件自己定#
插件把 webServer 作为硬依赖注入,用它的 register({ kind: 'prefix', path, handler }) 注册一个前缀路由,
之后页面、静态资源与 API 都由这个 handler 分发。
export const name = 'kth-bus-yyjz-agents'
/* cordis 插件取未声明的服务会直接抛错,且发生在宿主启动阶段 —— 四个服务本插件都必需 */
export const inject = ['webServer', 'infraConfig', 'auth', 'infraDatabase']
export function apply(ctx: any): void {
const server = ctx?.webServer // 宿主注入的 webServer
const BASE = '/bus-yyjz-agents' // 前端与接口的公共前缀(src/config.ts)
const handler = async (req, res) => { /* 分发 /api/* 与静态资源 */ }
const dispose = server.register({ kind: 'prefix', path: BASE, handler })
ctx.effect(() => () => dispose?.(), 'kth-bus-yyjz-agents: routes')
}路由前缀是「包名去掉产品前缀 kth-」:包名 kth-bus-yyjz-agents → 前缀 /bus-yyjz-agents/。
前缀常量集中在 src/config.ts,改一处即可(见「改造」一节)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /bus-yyjz-agents/ | 业务前台 SPA |
| GET | /bus-yyjz-agents/admin/ | 配置后台 SPA |
| GET | /bus-yyjz-agents/api/status | 插件自述(配置 / 数据层 / 运行时 / 前端是否就位) |
| POST | /bus-yyjz-agents/api/auth/login · me · logout | 会话,走 ctx.auth |
| ANY | /bus-yyjz-agents/api/admin/* | 资源注册台 + 子智能体平台(仅管理员) |
| ANY | /bus-yyjz-agents/api/v1/* | 业务:项目 / 公司 / 会话 / 消息 / 三场景报告 |
领域表:前缀、幂等 DDL、启动自检#
表名统一前缀,通用形态是 kth_<id>_agents_*;本模板 <id> 取 yyjz,即
kth_yyjz_agents_*。表落在共享库(台账应用 basePermission 登记的库),
与 kth_base_*、kth_auth_* 同库不同表;连接走 ctx.infraDatabase 的池注册表,插件不自己建池。
-- 建表幂等;重复执行安全
create table if not exists kth_yyjz_agents_model (
id varchar(40) primary key,
name varchar(128) not null,
base_url varchar(512) not null,
api_key text not null,
enabled boolean not null default true,
created_at bigint not null,
updated_at bigint not null
);
-- 加列也幂等(建表语句不会补列,只能 alter)
alter table kth_yyjz_agents_model add column if not exists max_tokens bigint null;| 列规约 | 取值 |
|---|---|
| 主键 / 领域 id | varchar(40),由代码生成(randomUUID()),不用库函数 |
| 时间戳 | created_at / updated_at 用 bigint(毫秒,代码 Date.now()) |
| 长文本 | text |
| JSON | 存 text,读写经 domain/shared.ts 的 jsonArray / jsonObject / jsonText |
| 外键 | 显式声明,on delete cascade |
| 禁用(跨库兼容) | UUID / JSONB / TIMESTAMPTZ / gen_random_uuid() / now() |
插件启动后惰性建表,再回查 information_schema.tables(like 'kth\_yyjz\_agents\_%')核对该建的表是否都在;
缺表只记日志告警、不阻塞宿主装配——请求路径会把错误回报给调用方。
运行时装配:库里的资源 → DSH 原生能力#
后台页面登记的资源不是「自建引擎」,而是运行时翻译成 DSH 的 seam。每条映射都有对应实现文件:
| 库里的资源 | 落到 DSH 的哪 | 实现文件 |
|---|---|---|
| 模型(页面注册) | ctx.llm.registerAdapter 动态 provider 路由 yyjz-<modelId> | runtime/model-registry.ts · model-adapter.ts |
| 工具(页面注册) | ctx.tools.register 动态工具 | runtime/tool-registry.ts |
| MCP(页面注册) | 每服务器一行 @deepseek-ai/dsh-mcp-client(经 Loader 具名装载) | runtime/mcp-registry.ts |
| 技能(页面上传) | 写入 <DSH_HOME>/skills/<name>/,交 dsh-skill-filesystem 发现 | runtime/skill-store.ts |
| 提示词 | agent 作用域 ctx.systemPrompt.section() | runtime/agent-setup.ts |
| 会话 / 持久化 | ctx.agents.create + setup;以会话日志为准 | runtime/run.ts |
① 底座核心的 agent 工厂不会自动挂 preset,不调 agentPresets.mount(agentCtx) 就没有任何工具;
② tools.restrict({ allow }) 在白名单为空时会抛错,所以「零工具」的子智能体要退化为不收敛,而不是让生成失败。
从模板改造成自己的插件#
- 复制并改目录名:
bus-yyjz-agents→bus-<你的业务>;package.json的name改成kth-bus-<你的业务>。 - 同步装配标识:
cordis.patch.yml行的id/name与包名一致(三处逐字一致铁律)。 - 改前缀与应用名:
src/config.ts的BASE(如/bus-<你的业务>)、CONFIG_APP、DB_APP、KEYS前缀、DEFAULT_ROLE_ADMIN。 - 改表前缀:
src/db/schema.ts的TABLES与 DDL 表名换成kth_<id>_agents_*;src/db/store.ts的自检like模式同步。 - 改前端:
web/、web/admin/的页面标题与接口前缀;package.json的dsh.embed[].path与label。 - 重建并验证:
pnpm install+pnpm run build,重启宿主后访问/bus-<你的业务>/。
构建与本地联调#
pnpm install
pnpm run build # → lib/index.js + lib/web/**(含 admin)
pnpm run check # node --check lib/index.js
# 离线自检(无第三方依赖,验证动态模型 adapter 的分片翻译与路由/配置解析)
node --import ./tools/register-ts.mjs tools/selfcheck.mjs宿主通过 --patch 覆盖层加载插件(--patch 是启动器参数,必须写在 --no-open 之前),
加载的是各插件的 lib/,参见 4 · 快速开始。
宿主加载的是 lib/ 而不是 src/。改完源码只重启宿主是没用的——必须先 pnpm run build,再重启宿主。
前端是零构建静态站点,若改用 Vite,把产物放到 web/dist 与 web/admin/dist,build.mjs 会优先使用。
业务接口挂在 /bus-yyjz-agents/api/v1/*;生成类接口返回 SSE,事件为 event: delta | done | error。
响应头显式带 x-accel-buffering: no,避免 nginx / 代理缓冲把长文本「卡住不出字」(见 src/http.ts 的 openSse)。
要往插件里加「模型 / 工具 / 技能 / MCP / 子智能体 / 工作流」等能力前, 先看 8 · DSH 运行时能力 的能力映射总表——确认底座已有对应 seam,再动手,不要另造一层。