@channek/sandbox-sdk
v0.1.2
Published
Channek T1 沙箱插件的零依赖 MessagePort SDK
Maintainers
Readme
@channek/sandbox-sdk
Channek T1 插件的零依赖 ESM SDK。插件入口在 sandbox="allow-scripts" 的 opaque-origin
iframe 内运行;所有宿主能力都经一次性移交的 MessagePort 白名单协议调用。
npm i @channek/sandbox-sdk它是构建期依赖,必须打进你的 bundle。 沙箱文档的 CSP 是 script-src plugin://<你的插件 id>,
不含任何外部源——从 CDN 引一个 <script> 会被当场挡掉,装进 node_modules 再由 esbuild /
Vite 打成插件目录内的单文件才跑得起来。
import { createSandboxSdk } from '@channek/sandbox-sdk';
const sdk = createSandboxSdk();
sdk.onInit(async init => {
document.body.textContent = await sdk.readFile();
sdk.setState({ scrollY: 0 });
});不要假设 event.origin 是插件 URL——沙箱 origin 固定为 opaque。视图状态(滚动位置、展开态)
用 setState 交还宿主。
Web Storage
opaque origin 下 localStorage / sessionStorage 读一下就抛 SecurityError,靠它们做持久化的
现成库会当场死。installWebStorage() 用「内存镜像 + 经桥落盘」把标准同步语义补回来:
const sdk = createSandboxSdk();
sdk.onInit(async () => {
await sdk.installWebStorage(); // await 之后再启动业务代码:同步读的前提是镜像已拉完
mountApp();
});之后 localStorage.getItem() / localStorage.foo = 'x' 等标准写法(含裸标识符)都可用。
差异:容量约 448KiB(超限抛 QuotaExceededError 并回滚本次写)、落盘合并且最多滞后数秒、
进程崩溃可能丢最后一批、不触发 storage 事件、作用域按插件而非 origin。
传 { global: false } 可只取返回的 { local, session } 不动全局;
{ scope: 'workspace' } 把 localStorage 切成每频道一份。
IndexedDB 依然没有。要更强的一致性用底层 sdk.storage.get/set(异步、直达 plugin-storage),
更大的量走「T2 侧持有 + rpc 分页取」。
功能区(suite section)最小用例
第三方功能区 = manifest 的 ui.suiteSection 贡献 + 沙箱入口 html(多区插件各区用
decl.entry 指各自入口——入口即身份,init 里没有 sectionId)。数据、导航、剪贴板全部走
suite 桥(宿主 typed 白名单 op);素材图片 / 视频不过桥,用应答里的 channek-media:
capability URL 直接 <img>/<video> 加载。
import {
createSandboxSdk,
suiteContextFromInit,
SuiteBridgeError,
} from '@channek/sandbox-sdk';
const sdk = createSandboxSdk();
sdk.onInit(async init => {
const context = suiteContextFromInit(init);
if (!context) return; // 非 suite 承载面(viewer/panel):suite 桥会被宿主以 not-a-suite 拒
if (context.workspaceId === null) {
document.body.textContent = '当前没有打开的频道';
return;
}
try {
// ≡ 渲染 IPC workspace:assetsOverview 的 DTO(精确形状见下「类型怎么拿」)
const overview = await sdk.suite.assetsOverview();
render(overview);
} catch (error) {
if (error instanceof SuiteBridgeError && error.code === 'response-too-large') {
document.body.textContent = '素材库过大,等待分页支持';
return;
}
throw error;
}
});
// 导航(宿主逐条校验实参,防导航钓鱼):
await sdk.suite.openContent('item-1');
await sdk.suite.openSection('homes');
// 剪贴板(沙箱 iframe 拿不到 navigator.clipboard,必须过桥):
await sdk.suite.clipboardWriteText('复制的文案');要点(第三方作者常踩):
- 类型怎么拿:读 op 应答在线上按
unknown承载;精确 DTO (WorkspaceAssetsOverviewDTO/WorkspaceOperationsSnapshotDTO)从@channek/ipc-contract类型层 import(import type),运行时保持零依赖。 - 错误分支认
SuiteBridgeError.code(unknown-op/not-a-suite/response-too-large/permission-denied/consent-required/timeout/invalid-payload/rate-limited/host-error),别 parse message 文本——文案不冻结, 码才冻结。 - 写 op(
sdk.suite.saveBacklog/sdk.suite.updateField):manifest 要声明workspace:write(缺 →permission-denied);每 (插件, 频道) 首次写会得consent-required,同时宿主弹一次同意确认——把它渲染成「等待用户允许后重试」,别当致命 错误。revision 冲突以人类可读错误回来(host-error),刷新快照(operationsSnapshot) 取新 revision 再重试。 - 剪贴板本地先限流:文本 ≤32KiB、每分钟 ≤10 次(
SUITE_CLIPBOARD_LIMITS),超限 SDK 直接拒、不出桥——宿主那侧的限额是审计面,不是你的节流器。 - 大清单自己虚拟滚动:读应答上限 1MiB(
SUITE_RESPONSE_LIMITS),超限宿主拒并提示 分页;iframe 内的渲染性能是插件职责。
