@dp-bohrium/widget-sdk
v0.1.0
Published
Bohrium Widget 开发者 SDK:在 Widget Bundle 内与 Host 建立 Bridge,并暴露生命周期与能力协议。
Downloads
7,365
Readme
@dp-bohrium/widget-sdk
Widget 开发者 SDK。普通作者只实现 activate(context) 和可选的 deactivate();CLI
会生成 Widget Host 入口,负责向 Host 宣告 Bundle、接收转移过来的 MessagePort,并暴露共享的
生命周期与能力协议。作者无需直接处理 window.postMessage,也无需手动启动插件。
激活事件目录、context.events 与低层 topic 的边界见
Widget 激活事件与事件查询。
环境要求
| 项 | 要求 |
| --- | --- |
| Node.js | ≥ 20(仅构建期;产物运行在浏览器 iframe 内) |
| 源码格式 | 纯 ESM,必须经打包器处理后交付 |
| 运行环境 | 必须由 Bohrium Host 加载,脱离 Host 无法完成握手 |
| 产物格式 | Host iframe 为 sandbox="allow-scripts" 且无 allow-same-origin 时,打成 classic script / IIFE(隔离源下 type="module" 需要 CORS) |
安装
写 Widget 装本包。Host 与控制面只消费协议类型,装
@dp-bohrium/widget-contracts
即可。
npm install @dp-bohrium/widget-sdk
pnpm add @dp-bohrium/widget-sdk
yarn add @dp-bohrium/widget-sdk需要在 TypeScript 中直接声明或在编辑器中解析 Manifest Schema 时,再把
@dp-bohrium/widget-contracts 作为开发依赖安装;它是独立的类型/Schema 包,不需要通过
defineManifest() 工厂获得提示。
在自己的 package.json 里钉死精确 SemVer,不要用 ^ 或裸 latest。SDK 运行时会从已安装
包自身读取版本,插件源码和 Manifest 不需要再次填写 SDK 版本。
快速开始
import type { WidgetContext } from "@dp-bohrium/widget-sdk";
export function activate(context: WidgetContext) {
context.render((request, renderContext) => {
renderSlot(request.slot, request.content, renderContext.phase);
});
context.events.on("tool-call-result", (event) => showToolResult(event.payload));
}CLI 生成的 src/widget-host.ts 会调用平台入口 runWidgetHost(widget)。
这与 VS Code 的激活入口一致:开发者实现 activate(context),可选实现 deactivate();
Widget Host/SDK 负责加载、等待初始化、报告启动失败和执行清理。普通插件代码不需要调用
ready(),也不应该写 plugin.ready().catch(() => {})。whenReady 只保留给测试和高级宿主编排。
context.signal 会在 Host 停用插件或 Bridge 断开时 abort。context.render 和 context.events
会自动登记到当前插件生命周期;subscriptions 只用于
第三方计时器、观察器或其它需要在停用时释放的资源。
三条时序约定:
ready()只在activate(context)返回(含其 Promise settle)之后才发给 Host,handshakeTimeoutMs同时约束这一步;- 初始化失败时,SDK 先报结构化错误,再调用插件资源清理;
context.render首帧和更新都会收到完整快照,通过renderContext.phase区分。
从 Manifest 到插件运行
Manifest 是插件的静态声明,SDK 代码是隔离 iframe 内的运行时实现,Host 负责二者之间的生命周期编排。一个插件可以这样声明:
{
"$schema": "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
"name": "research",
"version": "1.0.0",
"entry": "src/widget.ts",
"displayName": "研究面板",
"description": "展示研究工具的结构化结果",
"activationEvents": ["session.start", "tool-call-result:ScholarSearch", "ui-tool-opened"],
"contributes": {
"widget": { "type": "panel", "id": "research", "surfaces": ["workspace", "floating"] }
},
"capabilities": {
"files": { "read": true },
"events": { "topics": ["tool-call-result", "workspace-file-opened"] }
}
}activationEvents 只决定 Host 何时创建插件运行时;它不授予事件读取权限,也不负责打开思考过程或某个面板。插件激活后通过 context.events 和 context.render 注册监听与渲染器。Host 会对每个事件和能力请求做白名单投影与授权,插件收到的事件可能是有限回放,也可能是实时事件。
实现建议:事件处理器保持幂等,按 toolCallId 或 requestId 去重;不要从 iframe URL、DOM 或宿主绝对路径推断内部数据;当能力未授予时显示结构化空态并继续保留可渲染的主界面。声明但没有实际使用的能力会被 Host 记录为诊断信息,未声明能力的调用会被拒绝并结束当前 Bridge 会话。
统一插件入口
所有插件类型都使用同一个 activate(context) 约定。面板、工具结果和文件预览由
Manifest 的 contributes.widget 决定;运行时代码只认识统一的 context.events、
context.render、context.capabilities 和 context.ui。首帧和后续快照通过
renderContext.phase 区分,不需要维护两套回调。
Manifest 的编辑器提示来自 @dp-bohrium/widget-contracts 发布的 JSON Schema 和
TypeScript 类型,不依赖某个 defineManifest 工厂。构建工具调用
compileWidgetPluginManifest,自动补齐 schemaVersion、hostApiVersion、SDK 版本和
宿主加载字段;它会直接读取 Manifest 内的 samples,只有构建器从别处生成样例时
才需要传入第二个参数。createWidgetClient 是协议级基础实现,不属于 Widget 作者 API;当前根入口只提供统一 Widget API,协议级代码应从
./runtime 显式引入。
SDK 发布包会生成 dist/index.d.ts 与对应的 declaration map,并通过 package.json 的
types/exports 暴露。插件安装 @dp-bohrium/widget-sdk 后,编辑器会直接获得
WidgetContext、事件名、能力请求、渲染请求和生命周期返回值的类型提示;Manifest
编辑器提示由 @dp-bohrium/widget-contracts 的 JSON Schema 和声明文件提供。
包入口
| 入口 | 内容 | 用途 |
| --- | --- | --- |
| . | runWidgetHost、compileWidgetPluginManifest 和全部公开类型 | 生成的生产 Bundle |
| ./testing | MemoryTransport、MockWidgetHost | 单元测试 |
| ./runtime | 低层 WidgetClient、transport 与 bootstrap 常量 | 协议级集成 |
| ./package.json | 已安装包的元数据 | SDK 和构建工具自动读取版本,作者无需复制版本字符串 |
根入口只导出插件运行时和 Manifest 编译所需的少量值,其余均为类型。低层
MessagePortTransport 与 bootstrap 消息不属于普通 Widget API。
runWidgetHost 是生成的 widget-host.ts 使用的 Widget Host 启动边界;
插件作者只维护 widget.ts 中的 activate/deactivate。
能力
每项能力都要在 Manifest 的 capabilities 里声明,并由 Host 实际 grant;
缺任一侧,调用在 Widget 本地即被拒绝。所有能力调用都必须在 ready() 之后。
下面七项是当前的全部能力。grant 名与 Manifest 申请键不一定同名 —— ui.presentation
对应的作者侧申请键是 capabilities.presentation(编译后的 宿主格式字段才叫
requestedCapabilities.presentation)。
| grant 名 | Manifest 申请键 | SDK 调用 |
| --- | --- | --- |
| files.list / files.metadata / files.read | files | context.capabilities.request({ operation, input }) |
| files.download | files.download | context.files.download({ file, suggestedName }) |
| events.read | events(需列出 topics) | 低层分页订阅;context.events.on(…) 是 Host 主动推送,不需要该 grant |
| tasks.watch | tasks.watch | context.tasks.watch({ taskId, cursor, limit }) |
| ui.state | state(read / write) | context.ui.state.read(key) / write(key, value) |
| actions.invoke | actions | context.capabilities.request({ operation: "actions.invoke", input }) |
| network.fetch | network | context.network.fetch(…) |
| ui.presentation | presentation | context.ui.present(…) / changeSurface / close |
除 Host 主动推送事件外,其余能力共用同一套调用范式,掌握下面这一个就会调其余能力:
const controller = new AbortController();
const result = await context.network.fetch(
{ url: "https://api.example.org/papers", method: "GET", auth: "optional" },
{ signal: controller.signal, timeoutMs: 15_000 },
);
if (result.ok) {
consume(result.data.status, result.data.headers, result.data.body);
} else {
// 策略拒绝、用户拒绝、未授权都走这里,不是 throw。
showNetworkFailure(result.reason, result.retryable);
}跨能力约定:
- 失败以
{ ok: false, reason, message, retryable }正常兑现;只有客户端超时、中止、 dispose 和畸形响应才 reject。 - 中止与超时统一用
{ signal, timeoutMs },复用既有 MessageChannel 能力生命周期。 - Widget 永远拿不到 Host 凭据,也不应直接调用浏览器
fetch:是否弹窗、记住 Origin 授权、跟随重定向还是拒绝,全部由 Host 决定。 context.files.download只提交文件引用和建议文件名;Host 在用户确认后经宿主 Download Adapter 惰性打开内容源。Widget 不会收到源 URL、保存路径或凭据。context.tasks.watch只读取一次受限任务快照和事件页;调用方用返回的 cursor 继续轮询。 它不创建、调度或取消任务,服务端负责任务授权、脱敏和终态保留。context.ui.state只保存按 Widget + Session 隔离的 JSON UI 状态;Host 按 key、单值大小、总字节数和调用 频率限流。它不能保存任务执行、凭据、文件内容或 Workspace 权限状态。- Tool Result 的
content.envelope是可选的结构化投影,包含受约束的状态、数据、指标、产物引用、 provenance、warning 和 missing;产物只携带 opaque id 与展示元数据,不携带下载 URL 或本机路径。 - 呈现只是请求。Host 可能降级到别的表面,成功结果里的
surface才是真实表面; Widget 不能指定目标 Panel、身份、运行时 generation、坐标或层级。 - 高层插件 API 使用
context.events.on(handler)接收 Host 从当前 Runtime 主动推送的 聚合事件,只按type分发,不依赖 Manifest Topic 或events.readgrant。需要直接使用有界 分页读取时,从@dp-bohrium/widget-sdk/runtime使用subscribeEvents/onTool。 - 低层
subscribeEvents/onTool的所有订阅共享同一实例的 events 读预算,并各自遵守 topic 最小轮询间隔 —— 订阅越多,单个 topic 刷新越慢。游标只在onBatch成功后 提交,重新订阅需显式传回events.cursor。工具 Hook 观察的是已经发生的服务端 事实,不是执行前拦截器。
测试
import { MockWidgetHost } from "@dp-bohrium/widget-sdk/testing";MockWidgetHost 负责 init/ready 时序、首帧 render 与 update 选择、resize 事件、
能力 adapter 响应与终态 dispose;capabilityResult 给出确定性的默认允许/拒绝,
同时提供 handleCapability 时后者优先。host.capabilityRequests 返回有序的防御性
拷贝,断言不会改动 Mock 状态。
它使用与生产 Host 相同的公开 contracts,但不等同于生产 iframe、CSP、授权或资源 隔离保证。
限制
- SDK 不做授权决策,能力是否可用最终由 Host 判定。
tasks.watch是有界观察协议,不是调度器、执行器或取消 API;任务状态和事件由服务端投影。invokeAction不自动重试,回调完成也不代表工具执行完成。subscribeEvents的events.read是有界分页读取,不是 websocket;实时推送走onEvent。当前版本没有单请求取消的独立协议消息:unsubscribe会停止后续轮询并取消 本地等待,已发出的服务端读取可能完成,但结果被忽略。- Host 发来畸形 Bridge 信封或载荷时,transport 报结构化
MESSAGE_INVALID并关闭 连接,不会让 Widget 悬等。 - Schema 与类型校验以
@dp-bohrium/widget-contracts为准。
许可证
MIT。
