插件架构

September 10, 2026 · View on GitHub

本文帮你理解插件的模块划分、配置所有权,以及新增一个业务连接器需要改动的位置。

插件按信任边界与变更边界拆分:新增业务工具不应该触碰 OAuth 交换、Runtime 身份或 Gateway 策略代码。

Runtime 工具插件

index.ts
  -> config.ts             部署方拥有的连接与授权输入
  -> connector-settings.ts 租户设置命名空间:结构、限制、安装与实时读取
  -> settings.ts           Host 平面的设置命名空间拥有者
  -> policy.ts             工具 Scope 与写标记的单一事实来源
  -> catalog.ts            businessTool() 工厂,把策略接进 DSH 工具
  -> business-client.ts    Runtime 租约与 OAuth Token Exchange
  -> customer-tools.ts     CRM 示例目录条目
  -> lease.ts              本地租约解析与有效性判断

index.ts 是 Agent 平面的组合根。它一次性解析部署输入、读取 Host 拥有的实时设置、安装 唯一的执行前守卫,并注册选定的目录条目。其中不包含任何业务 Schema 或传输实现。

settings.ts 由 Host Profile 在任何会话存在之前挂载,因此原生设置页不依赖用户是否已经 启动业务 Agent Preset。

policy.ts 是工具 Scope 与写标记的唯一声明处。执行前守卫和 Token Exchange 的 Scope 都 从它派生,因此不可能出现「检查一个 Scope、申请另一个 Scope」的情况。

catalog.ts 把一个声明式条目(参数与输出 Schema、展示信息、请求构建器)变成注册好的 DSH 工具,并在同一处完成换票 Scope、写操作总开关、幂等键、超时与结果渲染的接线。

customer-tools.ts 是刻意保留的示例领域代码。新的集成应该在它旁边定义自己的目录文件, 并复用配置、策略、租约与委托客户端各层。模型参数只能承载业务输入,身份与凭据始终来自 受信任的运行时上下文。

Runtime Gateway

packages/runtime-gateway/src/gateway-policy.ts     RPC 允许/拒绝边界
packages/runtime-gateway/src/runtime-identity.ts   稳定的用户键、租户键与重启指纹
packages/runtime-gateway/src/runtime-manager.ts    进程生命周期、就绪等待与租约续期
packages/runtime-gateway/src/runtime-provision.ts  每个 Subject 的文件布局与子进程环境
packages/runtime-gateway/src/types.ts              宿主集成契约

Gateway 接受的是已验证的 GatewayPrincipal;它不实现登录,也不实现业务对象授权。 Runtime 的重启指纹包含进程启动时捕获的全部策略事实。租户设置在同租户内共享,而进程 状态、DSH_HOME、工作区、租约与会话始终按 Subject 隔离。

浏览器插件

packages/dsh-preferences/src/client.ts          浏览器组合根:主题、话术接收、设置卡片
packages/dsh-preferences/src/composer-bridge.ts 外部话术协议与暂存状态(纯逻辑)
packages/dsh-preferences/src/settings-card.ts   租户策略配置卡片
packages/dsh-preferences/src/theme.ts           浏览器本地主题偏好

composer-bridge.ts 不认识 window、cordis 和 DSH 服务:它只做校验、草稿合并和一条 暂存。client.ts 负责同源与嵌入方校验、解析当前会话的输入机,并在会话或对话界面就绪 时冲刷暂存。写入使用 DSH 原生输入机的唯一草稿写入路径,因此发送始终是用户动作——嵌入 页面能填写输入框,不能让智能体开口。

产物预览

packages/dsh-deliverables/src/contract.ts   路由形状与类型判定(两个平面共享)
packages/dsh-deliverables/src/index.ts      Host 平面:工作区收敛后按只读流服务文件
packages/dsh-deliverables/src/client.ts     浏览器平面:接管 workspaces.openPath
packages/dsh-deliverables/src/client/       预览渲染、选中态与工作区相对路径换算

它只做一件事:改变「打开文件」在托管部署里的去向。DSH 的产物徽章、正文里的文件提及 和工具行共用 workspaces.openPath,而那个方法把路径交给 Host 的桌面打开器——托管服务器 上没有桌面。接管这一个方法,三个界面一起修好;落在当前会话工作区之外的路径原样交回原 实现,插件卸载时恢复。

contract.ts 是两个平面唯一共享的模块,路由路径式而非查询式:产物 HTML 用相对路径 引用配图,查询式路由会让每一处相对引用落到本路由不提供的地址上。index.ts 的 confineToWorkspace 在 realpath 之后比较,因此工作区内一条指向外面的符号链接不构成 逃逸——这是本包唯一的安全断言。

嵌入方 chrome

packages/dsh-embed-chrome/src/contract.ts    路由、消息信封与校验(两个平面共享)
packages/dsh-embed-chrome/src/index.ts       Host 平面:只回答"哪个来源可以说话"
packages/dsh-embed-chrome/src/client/link.ts 三条信任规则,不认识 React 也不认识 cordis
packages/dsh-embed-chrome/src/client.ts      浏览器平面:品牌槽、hero 槽、工作区切换

Host 半边只做信任决定,因为它是唯一有配置的一半:hostOrigin 由运维写死。chrome 本身从不经过它——由嵌入页面在运行时经 postMessage 给出,页面改主意不需要重启 Runtime。

页面在另一个来源上(Runtime 走网关,产品在部署方域名),所以「同源」不能当判据。三条 规则合起来才成立:来源由 Host 指定、发送方必须是自己的父窗口、只往那个来源发而绝不用 '*'。第二条单独看容易漏——只验来源的话,同一来源上的另一个窗口也能给这个 Runtime 穿衣服并递上工作区名单。

switch 是请求而非命令:换工作区意味着换一个 Runtime、换一张令牌,只有页面能重新签发。

部署方品牌

packages/dsh-brand/src/contract.ts  配置形状、槽位判定与校验(两个平面共享)
packages/dsh-brand/src/index.ts     Host 平面:只做校验,让写错的 Profile 在加载时报错
packages/dsh-brand/src/client.ts    浏览器平面:给了什么才占哪个槽位

上一节的包解决嵌入时的品牌,这个包解决独立打开时的:品牌来自 Profile, 部署时定死,不需要页面参与。两者填同一批槽位,不同时启用。

这不是把 DSH 撬开——DSH 自己的品牌就是一个可替换插件(ui-brand-official), 在非官方构建上它什么都不注册,槽位空着等人来填。

没东西可画就不注册是本包的整个形状。sidebar.brand.mark 被渲染在折叠按钮内部当 静息状态,面板图标只在悬停时出现;占下它却渲染 null,展开态看着对,折叠态留下一个 不悬停就看不见的控件——而且槽位的 fallback 只在无人注册时生效,占着它连 DSH 自己的 兜底也一起顶掉。这个错误 dsh-embed-chrome 犯过一次。

校验用普通代码而非 schemastery:本包链接进 Profile,身边没有 node_modules, 一个 z.object() 就足以让 Runtime 以 ERR_MODULE_NOT_FOUND 起不来。

配置所有权

配置拥有者运行期变更
Broker / 业务 API 地址、Audience、租约路径部署运维重启
OAuth Scope 与可见工具业务策略控制面策略指纹变化触发 Runtime 重启
请求超时、写操作总开关、最短变更原因平台管理员通过 DSH Settings 实时生效
主题等界面偏好当前用户浏览器本地

只有第三行注册在 dshserver-integration 设置命名空间下。浏览器插件通过 DSH 原生的 settings.plugin.item 槽位贡献配置卡片。Gateway 的配置类 API 要求 assistant:platform:write;普通用户既不会装载平台设置界面,也无法通过隐藏路由修改租户 行为。

外部话术接收刻意没有配置项:浏览器插件拿不到 Profile 配置,而「谁可以往输入框里写字」是 安全边界,不能由一份可被误设的来源白名单决定。它由页面自身的事实回答:同源,且发送方 是嵌入当前页面的窗口。跨源集成通过统一域名或反向代理满足这一条。

完整参数表见配置参考。

扩展检查表

新增一个业务连接器时逐项完成:

  1. 在 policy.ts 的策略表中加入工具名称及其 Scope / 写标记。
  2. 用 businessTool() 声明目录条目:严格的参数与输出 Schema、展示标题,以及把校验后的 参数映射为一次 Resource Server 调用的请求构建器。
  3. 在 index.ts 的注册循环中加入新目录,并在 Agent Preset 的 exposedTools 中开放。
  4. 绝不直接读取业务数据库;请求构建器是唯一的传输面。
  5. 覆盖允许执行、缺少 Scope、对象级拒绝、超时和写操作幂等五种行为。

配套的接入步骤见集成指南 · 步骤 6。