@dp-bohrium/widget-sdk

中文文档
0.10.0发布于 2026/11/08MITTypeScriptES Module零依赖近 30 天下载 3342

Bohrium Widget QQ SDK: 12. Widget Bundle QX主机 出道桥,XX。

中文使用文档
文档目录13 节
  1. 环境要求
  2. 安装
  3. 校验工程
  4. 快速开始
  5. 渲染失败与本地诊断
  6. 从 Manifest 到插件运行
  7. 统一插件入口
  8. 包入口
  9. 能力
  10. 读取和保存当前文件
  11. 测试
  12. 限制
  13. 许可证

@dp-bohrium/widget-sdk

Widget 开发者 SDK,也是创建、构建、校验、开发和打包 Widget 的统一安装入口。普通作者只实现 activate(context) 和可选的 deactivate();SDK 自带的 bohr-widget 会生成 Widget Host 入口, 负责向 Host 宣告 Bundle、接收转移过来的 MessagePort,并暴露共享的 生命周期与能力协议。作者无需直接处理 window.postMessage,也无需手动启动插件。

使用 Agent 开发时,先读 llms.txt 获取文档索引,再读 AGENTS.md 了解安装、运行时调用和错误恢复规则。

激活事件目录、context.events 与低层 topic 的边界以本包的 AGENTS.md、本 README 和本包公开的类型与 Schema 为准;协议实现保持同一份事实源。

环境要求

项要求
Node.js≥ 20(执行 bohr-widget 与构建时;Widget 产物运行在浏览器 iframe 内)
源码格式纯 ESM,必须经打包器处理后交付
运行环境必须由 Bohrium Host 加载,脱离 Host 无法完成握手
产物格式Host iframe 为 sandbox="allow-scripts" 且无 allow-same-origin 时,打成 classic script / IIFE(隔离源下 type="module" 需要 CORS)

安装

安装 SDK 即可获得 Widget API、协议类型、JSON Schema、测试工具和 bohr-widget 命令。

npm install @dp-bohrium/widget-sdk
pnpm add @dp-bohrium/widget-sdk
yarn add @dp-bohrium/widget-sdk

新建工程只需调用 SDK 自带的命令,生成结果也只直接声明精确 SDK 版本:

pnpm dlx @dp-bohrium/widget-sdk create my-widget --template panel
cd my-widget
pnpm install
pnpm exec bohr-widget build .
pnpm exec bohr-widget check .

常用作者类型从本包根入口导入;完整协议从 @dp-bohrium/widget-sdk/contracts 导入, 编辑器 Schema 使用 @dp-bohrium/widget-sdk/schemas/*.json,协议测试语料使用 @dp-bohrium/widget-sdk/conformance/*。升级已有工程时,参照 版本兼容。

在自己的 package.json 里钉死精确 SemVer,不要用 ^ 或裸 latest。SDK 运行时会从已安装 包自身读取版本,插件源码和 Manifest 不需要再次填写 SDK 版本。

校验工程

使用 SDK 自带的命令检查 Widget 工程:

pnpm exec bohr-widget build .
pnpm exec bohr-widget check .
pnpm exec bohr-widget pack . --out widget.zip

命令复用同一份协议 Schema 校验 Manifest 与样例,并检查入口、资源和打包要求。 校验实现供内部工具使用,不提供独立公开子入口,普通 SDK 导入不加载校验器或 AJV。 校验通过只表示本地工程符合检查要求,Host 仍负责实际权限、网络策略与运行隔离。

快速开始

import type { WidgetContext } from "@dp-bohrium/widget-sdk";

export function activate(context: WidgetContext) {
  context.render((request, renderContext) => {
    renderContribution(request.contribution, request.content, renderContext.phase);
  });
  context.events.on("tool-call-result", () => {
    void context.ui.present("floating");
  });
}

CLI 生成的 src/widget-host.ts 会调用平台入口 runWidgetHost(widget)。 这借鉴 VS Code 的激活入口。最终模型按 Binding 建立一个 Extension Runtime,activate(context) 在 Runtime 内只执行一次;工具、文件或面板目标是挂载到 Runtime 的 Surface Instance,拥有 独立的快照和释放句柄,不会再次触发 activate。开发者实现 activate(context),可选实现 deactivate();Widget Host/SDK 负责加载、等待初始化、报告启动失败和执行清理。普通插件 代码不需要调用 ready(),也不应该写 plugin.ready().catch(() => {})。whenReady 只保留 给测试和高级宿主编排;whenDisposed 可用于等待已完成激活插件的异步 deactivate()。 停用不会等待一个永不结束的 activate();若激活在停用后迟到完成,SDK 会在已 abort 的生命周期上补偿释放并调用一次 deactivate(),但这不再延长 whenDisposed。

这是 SDK 的目标生命周期契约;当前 Widget Host 的动态加载仍由 Surface-bound Adapter 接线, Binding 级 Runtime Loader 尚未开放。插件可以按 Runtime/Surface 分层设计,但不能把当前版本 当成“无界面必然已启动”的生产保证。

context.signal 会在 Host 停用插件或 Bridge 断开时 abort。context.render 和 context.events 会自动登记到当前插件生命周期;subscriptions 只用于 第三方计时器、观察器或其它需要在停用时释放的资源。

三条时序约定:

  • ready() 只在 activate(context) 返回(含其 Promise settle)之后才发给 Host, handshakeTimeoutMs 同时约束这一步;
  • 初始化失败时,SDK 先报结构化错误,再调用已登记的插件资源清理;成功激活后的释放会 最多调用一次 deactivate(),已完成激活的异步清理可通过 whenDisposed 等待;
  • context.render 首帧和更新都会收到完整快照,通过 renderContext.phase 区分。

渲染失败与本地诊断

runWidgetHost 会隔离 context.render 处理器的同步异常和异步拒绝,并在 Guest console 记录原始异常。已有 onError 和 events.onError 也会收到本地结构化诊断:首帧使用 RENDER_FAILED/render,更新使用 UPDATE_FAILED/update,correlationId 对应失败快照的 requestId,retryable 为 false。结构化消息不包含渲染负载或原始异常。

这类通知本身不会让 SDK 终止 Bridge、销毁界面或自动重试;其他处理器和后续快照仍可继续, 插件应保留草稿并提供可恢复的提示。观察者显式调用 dispose() 时,SDK 仍尊重停用意图。 SDK 不回滚处理器失败前已执行的 DOM 修改;界面和草稿的恢复仍由插件拥有。 观察者自身的同步异常、异步拒绝和 console 失败不会阻断渲染,也不会递归触发 onError; 停用后迟到的渲染拒绝会被消费,但不再产生本地诊断。

这些是 Guest 本地出口,SDK 不通过 Bridge 把它们发送给 Host。 旧 Host 会把 Widget 的协议 error 当作致命失败, 不能用它承载可恢复的渲染诊断;Host 的 ready 或 render-sent 也不能证明 Guest DOM 渲染成功。

从 Manifest 到插件运行

Manifest 是插件的静态声明,SDK 代码是隔离 iframe 内的运行时实现,Host 负责二者之间的生命周期编排。一个插件可以这样声明:

{
  "$schema": "@dp-bohrium/widget-sdk/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"] }
  }
}

需要接管完整的会话右侧区域时,在同一个 Panel contribution 上声明结构性放置意图,并同时申请匹配的 ui.region capability:

{
  "contributes": {
    "widget": {
      "type": "panel",
      "id": "better-sidebar",
      "placement": { "kind": "region", "region": "session.rightRail", "mode": "replace" }
    }
  },
  "capabilities": {
    "region": { "regions": ["session.rightRail"], "mode": "replace" }
  }
}

placement 只表达贡献点的结构性意图;区域租约、确认、几何、焦点、原生面板恢复和失败回退仍由 Host 负责。没有匹配 capability 时,SDK 编译阶段直接拒绝清单。

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 的编辑器提示来自本包 /schemas 的 JSON Schema 和公开 TypeScript 类型, 不依赖某个 defineManifest 工厂。构建工具调用 compileWidgetPluginManifest,自动补齐 schemaVersion、hostApiVersion、SDK 版本和 宿主加载字段;它会直接读取 Manifest 内的 samples,只有构建器从别处生成样例时 才需要传入第二个参数。createWidgetClient 是协议级基础实现,不属于 Widget 作者 API;当前根入口只提供统一 Widget API,协议级代码应从 ./runtime 显式引入。

受信任的内部 Host/协议测试仍可直接构造低层 Tool Result RenderRequest,但该能力不属于外部作者清单或公共示例。

SDK 发布包会生成 dist/index.d.ts 与各子入口声明,并通过 package.json 的 types/exports 暴露。插件安装 @dp-bohrium/widget-sdk 后,编辑器会直接获得 WidgetContext、事件名、能力请求、渲染请求和生命周期返回值的类型提示;Manifest 编辑器提示由本包的 /schemas 和声明文件提供,Schema 在构建时从协议唯一源生成。

包入口

入口内容用途
.runWidgetHost、compileWidgetPluginManifest 和常用作者类型生成的生产 Bundle
./contracts完整协议类型、事件目录与判定函数高级协议集成;同名协议类型显式从此处获取
./conformance/*同源协议校验语料Host/控制面验证
./schemas/*.json作者清单、宿主清单、运行配置与消息 Schema编辑器提示和工具校验
./testingMemoryTransport、MockWidgetHost单元测试
./runtime低层 WidgetClient、transport 与 bootstrap 常量协议级集成
./package.json已安装包的元数据SDK 和构建工具自动读取版本,作者无需复制版本字符串
bohr-widgetNode.js 命令入口创建、构建、校验、开发和打包工程;不进入浏览器 Bundle

根入口只导出插件运行时和 Manifest 编译所需的少量值,其余均为类型。低层 MessagePortTransport 与 bootstrap 消息不属于普通 Widget API。

runWidgetHost 是生成的 widget-host.ts 使用的 Widget Host 启动边界; 插件作者只维护 widget.ts 中的 activate/deactivate。

能力

公开能力要在 Manifest 的 capabilities 里声明,并由 Host 实际 grant; service.invoke 需要由 Host binding 显式授权。缺任一侧,调用在 Widget 本地即被拒绝。 所有能力调用都必须在 ready() 之后。

下面列出的能力是当前 SDK 支持的能力。grant 名与 Manifest 申请键不一定同名 —— ui.presentation 对应的作者侧申请键是 capabilities.presentation(编译后的 宿主格式字段才叫 requestedCapabilities.presentation)。

grant 名Manifest 申请键SDK 调用
files.list / files.metadatafilescontext.capabilities.request({ operation, input })
files.readfiles.readcontext.files.read({ file, offset, length, encoding, expectedRevision? })
files.writefiles.writecontext.files.write({ file, content, encoding, expectedRevision, mutationId })
files.downloadfiles.downloadcontext.files.download({ file, suggestedName })
events.readevents(需列出 topics)低层分页订阅;context.events.on(…) 是 Host 主动推送,不需要该 grant
ui.statestate(read / write)context.ui.state.read(key) / write(key, value)
actions.invokeactionscontext.capabilities.request({ operation: "actions.invoke", input })
network.fetchnetworkcontext.network.fetch(…)
service.invoke由 Host binding 显式授权context.services.invoke({ service, action, payload })
ui.presentationpresentationcontext.ui.present(…) / changeSurface / close
ui.regionregion(固定申请 session.rightRail)context.ui.region.acquire() / release()

除 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);
}

跨能力约定:

  • Host 拒绝以 { ok: false, reason, message, retryable } 正常兑现;本地状态、输入、客户端超时、 中止、dispose 和畸形响应可 reject。files.write 发出后的结果不确定时兑现为 write-outcome-unknown,处理方式见下文。
  • 中止与超时统一用 { signal, timeoutMs },复用既有 MessageChannel 能力生命周期。
  • Widget 永远拿不到 Host 凭据,也不应直接调用浏览器 fetch:是否弹窗、记住 Origin 授权、跟随重定向还是拒绝,全部由 Host 决定。
  • context.services 仅在 Host binding 授予 service.invoke 后可用。可调用的服务、操作、请求/响应 大小和有效期由 Host 的授权策略决定;未获授权时调用会被拒绝。认证状态由宿主侧管理,SDK 只收发 业务 payload,不会把 Cookie、Bearer Token 或其他凭据放入 iframe。
  • context.files.download 只提交文件引用和建议文件名;Host 在用户确认后经宿主 Download Adapter 惰性打开内容源。Widget 不会收到源 URL、保存路径或凭据。
  • context.ui.state 只保存按 Widget + Session 隔离的 JSON UI 状态;Host 按 key、单值大小、总字节数和调用 频率限流。它不能保存任务执行、凭据、文件内容或 Workspace 权限状态。
  • 外部插件的工具结果场景通过 tool-call-result 事件作为可选运行时信号;内部 Host/协议测试仍可使用低层 Tool Result RenderRequest 或展示槽位。插件应把结果转换成自己的 Panel 数据,再通过 context.render 更新面板。
  • 呈现只是请求。Host 可能降级到别的表面,成功结果里的 surface 才是真实表面; Widget 不能指定目标 Panel、身份、运行时 generation、坐标或层级。
  • 高层插件 API 使用 context.events.on(handler) 接收 Host 从当前 Runtime 主动推送的 聚合事件,只按 type 分发,不依赖 Manifest Topic 或 events.read grant。需要直接使用有界 分页读取时,从 @dp-bohrium/widget-sdk/runtime 使用 subscribeEvents / onTool。
  • 低层 subscribeEvents / onTool 的所有订阅共享同一实例的 events 读预算,并各自遵守 topic 最小轮询间隔 —— 订阅越多,单个 topic 刷新越慢。游标只在 onBatch 成功后 提交,重新订阅需显式传回 events.cursor。工具 Hook 观察的是已经发生的服务端 事实,不是执行前拦截器。

读取和保存当前文件

作者 Manifest 申请 "capabilities": { "files": { "read": true, "write": true } },Host 仍需独立 授权两个能力。files.write 对 Host 签发引用指向的已有文件作整文件条件替换:在 file-preview 中引用来自 request.content.file;在获得写 grant 的 Panel 中引用来自 data.root 或 files.list 的结果。不能传路径、URL、自造的文件 id 或指定另存位置。Host 校验引用、作用域、 写权限和实际字节预算,成功回执表示执行端已确认持久化。SDK 不负责 ASE、晶格或文件格式转换, 插件应使用自己的序列化器生成最终字节。

FileReadInput / FileReadResult、FileWriteInput / FileWriteResult、FileRevision、 FileMutationId 与两个 *CapabilityResult 类型均可从 SDK 根入口导入。revision 是文件内容的 不透明版本,与渲染快照的 RenderRequest.revision 无关。下面的 file 来自当前文件预览:

const loaded = await context.files.read({
  file, offset: 0, length: 64_000, encoding: "utf8",
  ...(file.revision ? { expectedRevision: file.revision } : {}),
});
if (!loaded.ok) return showReadFailure(loaded.reason);
if (!loaded.data.revision) return showReadOnlyFile(loaded.data.content);
if (loaded.data.eof === false || loaded.data.truncated === true) {
  return showFileNeedsChunkedReading();
}

const expectedRevision = loaded.data.revision;
const content = serializeEditedModel(loaded.data.content);
// 一次逻辑保存只生成一次;核对或重试同一内容时复用。
const mutationId = crypto.randomUUID();
const saved = await context.files.write({
  file, content, encoding: "utf8", expectedRevision, mutationId,
}, { signal: saveController.signal, timeoutMs: 15_000 });
if (saved.ok) {
  markSaved(saved.data.file, saved.data.revision);
} else {
  keepUnsavedChanges(saved.reason, mutationId);
}

read 兼容旧 Host 的 { content, encoding } 结果;offset、bytesRead、nextOffset、eof、 truncated 和 revision 均为可选扩展。SDK 原样返回结果,只验证已提供字段,不补造游标或版本。 没有 revision 时内容可展示,但不能据此保存;带 expectedRevision 的读取必须返回有效且匹配的 版本,不能降级为无版本读取。Host 必须从同次读取的内容快照签发版本,不能另用 metadata/stat 补取。

有游标的 read 按原始字节计数:使用 nextOffset 继续;若首块带版本,后续块携带对应 expectedRevision,防止混合版本。utf8 字节区间可能切断多字节字符,不能用重新编码后的文本长度推断 bytesRead。 二进制文件或需完整解码的分块文本使用 base64,先将每块解码为字节并拼接,再解码文本。 base64 使用标准、带正确 padding 的编码;空文件和空字符串替换均合法。 上面的整文件保存示例以 Host 返回完整内容为前提;缺少完整性标记时,插件需依据 Host 的读取约定 确认内容完整,不能把一个未完成的分块直接替换为整个文件。

write 必填读取时的 expectedRevision 和一次逻辑保存的 mutationId(1–128 个字符,只含 字母、数字、_ . : - 且以字母或数字开头)。revision-conflict 表示当前文件已变化,应重新读取 并由用户合并;相同 mutationId 携带不同内容会返回 mutation-conflict。保存成功后更新本地文件引用 和基础版本;未修改内容的保存可以返回相同版本。 游标元信息不是保存的前提;只有读取内容所绑定的有效版本才能用作 expectedRevision。

SDK 不自动重试保存。调用前已中止、客户端未就绪或输入非法会 reject,且不发送请求;缺少 grant 返回 permission-denied。发出后超时、中止、断线或畸形回执都不能证明未写入,返回 { ok: false, reason: "write-outcome-unknown", retryable: false }。此时保留未保存内容、原基础版本 和 mutationId,通过 Host 核对文件版本或用同一 mutationId、同一内容确认结果;不要直接生成新 id 重试,也不要提前标记已保存。新的编辑内容属于新的逻辑保存,应使用新的 mutationId。

测试

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 判定。
  • invokeAction 不自动重试,回调完成也不代表工具执行完成。
  • subscribeEvents 的 events.read 是有界分页读取,不是 websocket;实时推送走 onEvent。当前版本没有单请求取消的独立协议消息:unsubscribe 会停止后续轮询并取消 本地等待,已发出的服务端读取可能完成,但结果被忽略。
  • Host 发来畸形 Bridge 信封或载荷时,transport 报结构化 MESSAGE_INVALID 并关闭 连接,不会让 Widget 悬等。

许可证

MIT。

版本与发布记录

@dp-bohrium/widget-sdk 共发布 18 个版本,首次发布于 2026/09/19;过去 12 个月发布了 18 个版本。

版本发布日期距今
0.10.0latest2026/11/085 天前
0.9.02026/11/076 天前
0.8.02026/11/049 天前
0.7.02026/11/0211 天前
0.6.02026/10/3113 天前
0.5.02026/10/2915 天前

其他发布通道:widget-alpha → 0.0.2。

关于 @dp-bohrium/widget-sdk 的常见问题

@dp-bohrium/widget-sdk 是什么?

Bohrium Widget QQ SDK: 12. Widget Bundle QX主机 出道桥,XX。

如何安装 @dp-bohrium/widget-sdk?

在项目目录运行 npm install @dp-bohrium/widget-sdk,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 bohr-widget,可以用 npx bohr-widget 直接运行。

@dp-bohrium/widget-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dp-bohrium/widget-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@dp-bohrium/widget-sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dp-bohrium/widget-sdk 可以商用吗?

@dp-bohrium/widget-sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dp-bohrium/widget-sdk 的下载量有多少?

根据 npm 官方下载统计,@dp-bohrium/widget-sdk 在 2026/10/13 – 2026/11/11 这 30 天内被下载 3,342 次,日均约 111 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dp-bohrium/widget-sdk 还在维护吗?

最新版本 0.10.0 发布于 2026/11/08(5 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/09/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。