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 / 插件,管理项目与子智能体。

DSH 运行时

目录 src/runtime/,把库里登记的资源配置成 ctx.llm / ctx.tools / MCP / 技能,再用 ctx.agents 跑会话。

三者共用同一个 领域库(kth_yyjz_agents_* 表):后台写入资源与配置,运行时读出来装配成 DSH 能力,前台消费生成结果。分工与理由见模板的 docs/plan/2026-10-09-runtime-mapping.md。

目录结构#

textbus-yyjz-agents/ 源码树
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 行内。

三处逐字一致#

Infrastructure/AGENTS.md 的原话

「package.json 的 name、cordis.patch.yml 的行 name、 客户端 window.__ModuleLoader__.load({ id }) 三处逐字一致(加载器要求注册 id 等于包名)。」

位置本模板的值 / 说明
package.json 的 namekth-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 ≠ 访问路径

装配 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 分发。

tssrc/index.ts(节选)
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 的池注册表,插件不自己建池。

sqlsrc/db/schema.ts(形态示意)
-- 建表幂等;重复执行安全
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;
列规约取值
主键 / 领域 idvarchar(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 }) 在白名单为空时会抛错,所以「零工具」的子智能体要退化为不收敛,而不是让生成失败。

从模板改造成自己的插件#

  1. 复制并改目录名:bus-yyjz-agents → bus-<你的业务>;package.json 的 name 改成 kth-bus-<你的业务>。
  2. 同步装配标识:cordis.patch.yml 行的 id / name 与包名一致(三处逐字一致铁律)。
  3. 改前缀与应用名:src/config.ts 的 BASE(如 /bus-<你的业务>)、CONFIG_APP、DB_APP、KEYS 前缀、DEFAULT_ROLE_ADMIN。
  4. 改表前缀:src/db/schema.ts 的 TABLES 与 DDL 表名换成 kth_<id>_agents_*;src/db/store.ts 的自检 like 模式同步。
  5. 改前端:web/、web/admin/ 的页面标题与接口前缀;package.json 的 dsh.embed[].path 与 label。
  6. 重建并验证:pnpm install + pnpm run build,重启宿主后访问 /bus-<你的业务>/。

构建与本地联调#

bash在插件目录内
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 会优先使用。

对外接口与 SSE

业务接口挂在 /bus-yyjz-agents/api/v1/*;生成类接口返回 SSE,事件为 event: delta | done | error。 响应头显式带 x-accel-buffering: no,避免 nginx / 代理缓冲把长文本「卡住不出字」(见 src/http.ts 的 openSse)。

下一步

要往插件里加「模型 / 工具 / 技能 / MCP / 子智能体 / 工作流」等能力前, 先看 8 · DSH 运行时能力 的能力映射总表——确认底座已有对应 seam,再动手,不要另造一层。