npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mbws/plugin-sdk

v0.3.19

Published

Moye 插件开发 SDK——panel/viewer 与宿主的类型定义与运行时端口适配

Readme

@mbws/plugin-sdk

MBWS 数据加工插件协议 SDK——为插件面板提供 createMbws() 与宿主完成握手,返回带 data / runtime / storage / ui / host / caps(授予时)/ panel / contract 命名空间的客户端对象。 通常你不需要手动安装本包:用 pnpm create @mbws/plugin 脚手架生成的工程已自带依赖; 手动集成时再 pnpm add @mbws/plugin-sdk。

安装

pnpm add @mbws/plugin-sdk

要求 Node ≥18(或现代浏览器环境);包内零运行时依赖(类型与运行时均为自带实现)。

最小面板示例

import { createMbws } from "@mbws/plugin-sdk";

const mbws = await createMbws();

// 读全部输入(键集见关联任务的契约 inputs;媒体字段值是引用句柄 MbwsRef,非文件名)
const inputs = await mbws.data.inputs();

// 面板内容变化后按内容自适应高度,宿主会同步调整 iframe 高度
mbws.panel.resize(document.body.scrollHeight);

// 计算完成,写回输出(outputs 的 key 集须与契约 outputs 对齐,越界会被拒)
await mbws.data.apply({ result: transform(value) });

注意 createMbws() 全局只能调用一次(与 VS Code acquireVsCodeApi 同心智),二次调用 会抛 MbwsInvokeError(E_PROTOCOL_VIOLATION)。

API 速查

握手成功后返回 MbwsClient。除 panel / contract 恒存在外,其余命名空间按 manifest 权限授予情况存在——未声明/未授予的命名空间在对象上为 undefined,使用前 建议判空。

| 命名空间.方法 | 签名 | 用途 | | --- | --- | --- | | data.inputs | inputs(): Promise<Record<string, unknown>> | 全部输入 slot 值(媒体字段是引用句柄,非展示名) | | data.outputs | outputs(): Promise<Partial<Record<string, unknown>>> | 上次产出反演(增量编辑;缺键 = 从头开始) | | data.blob | blob(key: string): Promise<{ url: string; mime?: string }> | 拿文件类输入的签名访问 URL | | data.apply | apply(outputs: Record<string, unknown>): Promise<void> | 草稿写回(经宿主 write gate 校验;幂等、不关面板) | | data.commit | commit(outputs: Record<string, unknown>): Promise<void> | 立即落库(同一 write gate;宿主直接 upsert + 立即反显,不等用户提交表单) | | runtime.progress | progress(ratio?: number, message?: string): Promise<void> | 上报进度(0~1)与状态文案 | | runtime.log | log(...args: unknown[]): Promise<void> | 写运行日志(宿主侧可查看) | | runtime.signal | signal(): AbortSignal | 取消信号——用户取消任务时 abort | | storage.get | get(key: string): Promise<unknown> | 读插件专属配额 KV | | storage.set | set(key: string, value: unknown): Promise<void> | 写插件专属配额 KV(配额超限抛错) | | ui.confirm | confirm(opts: { title?: string; message: string }): Promise<boolean> | 宿主渲染的确认弹窗 | | ui.toast | toast(message: string, opts?: { type?: "info" \| "success" \| "error" }): Promise<void> | 宿主渲染的轻提示 | | panel.resize | resize(height: number): void | 上报面板期望高度 | | panel.autoResize | autoResize(target?: HTMLElement \| (() => HTMLElement)): () => void | 高度自适应样板收敛:ResizeObserver 盯目标(缺省 document.body)变化即上报 + 订阅即报一次;返回停止函数(0.3.10+) | | panel.onBeforeClose | onBeforeClose(cb: () => boolean \| { dirty: boolean; message?: string } \| Promise<同左> \| undefined): void | 注册关闭前询问回调(promise 语义,见下;对象形态可自定义确认文案) | | panel.replyBeforeClose | replyBeforeClose(dirty: boolean, message?: string): void | 应答关闭询问(dirty=true 宿主先弹确认,message 自定义文案);旧式回调用,promise 写法不必调 | | panel.close | close(): Promise<void> | 插件主动请求关闭面板(fire-and-forget,宿主仍走一次关闭询问) | | host.capabilities | capabilities(): Promise<{ sdk: string; methods: string[] }> | 一次性宿主能力面探测(0.3.10+;集中降级,替代散落 typeof 探测) | | host.can | can(method: string): Promise<boolean> | 探测某 invoke 方法是否可用(capabilities 的 memo 包装;0.3.11+) | | viewer.inputs | inputs(): Promise<Partial<Record<string, unknown>>> | viewer 容器内拉该记录输入 slot 值集(原始数据对照;0.3.15+) | | viewer.outputs | outputs(): Promise<Partial<Record<string, unknown>>> | viewer 容器内拉本插件产出 slot 值集(0.3.15+) | | viewer.onChange | onChange(cb: (outputs: Partial<Record<string, unknown>>) => void): () => void | 订阅运行期产出变更(推送新 outputs 对象;订阅不进 methods,探测走 typeof;0.3.15+) | | caps.exec.spawn | spawn(req: { attachment; args; timeoutMs?; progress? }): Promise<ExecJob> | 派发附件执行(调度权归宿主);progress: { totalSeconds } 声明编码进度分母,宿主按 out_time_us 行折算自持反显(见下) | | caps.exec.attachJob | attachJob(jobId: string): Promise<ExecJob \| null> | 重挂后台 job 续看进度(null = 已结束/不存在,走 jobResult 兜底;0.3.10+) | | contract.get | get(opts?: FetchContractOptions): Promise<PluginContract> | 拉关联任务的最新数据契约 | | contract.allows | allows(outputKey: string, fieldPath: string): Promise<boolean> | 写回前探问:字段是否在契约 outputs schema 允许范围(内部缓存契约) |

contract.get 不经宿主端口,直接走 HTTP 按插件编号拉取:返回的 PluginContract 含 inputs / outputs(key → JSON Schema,data.inputs / data.apply 的对齐基准)、title、status、updatedAt 等。FetchContractOptions 可传 pluginCode(缺省从工程 manifest.json 读)、baseUrl、devKey、signal。

关闭权协议

弹窗外壳(chrome)永远归宿主:X / Esc / 遮罩 / panel.close() 四条关闭路径同源, 都走一次「关闭询问」——宿主发 panel:before-close,插件以 promise 语义应答:

mbws.panel.onBeforeClose(async () => {
  // 感知关闭意图:暂停任务 / 保存草稿 / 弹自己的确认 UI
  return dirtyRef.current; // true = 有未保存(宿主弹「确认放弃」),false = 放行
});

// 进阶:dirty 时自定义确认弹窗文案(≤200 字符;旧宿主忽略该字段走默认文案)
mbws.panel.onBeforeClose(async () => ({
  dirty: encoding,
  message: "当前段落将丢失,已完成段落已保存",
}));
  • 返回 boolean / Promise<boolean> 时 SDK 自动应答;返回 undefined 保持旧式 (稍后显式调 replyBeforeClose);
  • 宿主等待 PANEL_BEFORE_CLOSE_TIMEOUT_MS(500ms,SDK 导出常量)超时按放行关, 防插件锁死面板;
  • 插件无硬否决:dirty 应答后用户确认放弃仍会关(关闭权最终在用户);
  • panel.close() 供面板跑完收尾(加工+写回+上传全部完成)后自行请求关闭, 宿主收到后同样先走一次关闭询问再关;
  • data.apply 是纯写回(幂等、可多次增量编辑),不会自动关面板;面板关闭也不 终止宿主侧任务与上传队列。

caps 产物登记(reserve → register 两段式)

长任务(视频编码等)跑几分钟才发现目标文件夹已有同名文件太浪费——覆盖预检放在 spawn 之前:

// ① spawn 前:预占产物名(冲突缺省 auto-rename,返回最终可用名)+ 占位符
const { filename, placeholder } = await mbws.caps.exec.reserveOutput({
  filename: "cut1_JJD巨人S4.mp4",
  dir: outputDir, // 缺省 = job 沙箱 out/;绝对路径须在本会话 pick 范围内
});
// 用返回的 filename 组装 spawn 的输出路径(冲突时已是 `名 (1).ext`)

// ①' 占位符先行(0.3.10+):预占即发——编码开始前就把字段值 commit 成占位符,
//     字段从点击瞬间有值、面板中途关闭零丢失(旧宿主不返回 placeholder,探测降级)
await mbws.data.apply({ segments: [{ clip: placeholder }] });

// ② 产出后:登记(与 ① 返回同一占位值——同一路径主键)
const registered = await mbws.caps.exec.registerOutput({
  jobId,
  dirname: outputDir, // 所在文件夹(绝对 = pick 范围内 reference;相对 = 沙箱内 move)
  filename, // 纯文件名,完整路径由宿主 join(dirname, filename) 拼出
});
// registered.placeholder === placeholder(面板关闭后宿主对账自动转正,见下)
  • 占位符先行协议(0.3.10):reserveOutput 返回 placeholder,编码完成前 mediaUrl(placeholder) 抛 E_NOT_READY(data 带当前 status)——据此渲染「编码中」 占位卡;编码完成后宿主自动转正并可预览/上传。字段值全程不变(占位符稳定);
  • 面板中途关闭:宿主对账三触发源(registerOutput / job 终态 / 兜底扫描)任一到达 即转正 reserved 条目并广播 onUpload 事件——重开面板 caps.uploads.list() 自动跟上;

编码进度声明化(progress)+ 任务调度权契约

const job = await mbws.caps.exec.spawn({
  attachment: "ffmpeg",
  // 宿主不注入参数:进度行需插件自己在 args 里开(-progress pipe:1 -nostats)
  args: ["-progress", "pipe:1", "-nostats", "-i", "inputs/0.mp4", /* … */],
  progress: { totalSeconds: 125 }, // 声明分母(预期产出总时长,秒)
});
  • 声明后宿主自行解析 stdout 的标准进度行(out_time_us=)折算 ratio 并持有: job → reserved 上传条目 → 上传中心「编码中 42%」全链宿主自持——面板关闭后百分比 照常推进,编码完成自动转正上传。转码/压图等任何给得出「分母 + 标准进度行」的 工具即复用;上传条目经 caps.uploads.list() 可读 encodingProgress(dev-mock 不产生)。
  • 面板开着时插件用同一条 onLine 流画自己的进度条,与宿主互不干扰;不声明 progress 的 spawn 完全不受影响(非强制协议)。
  • 任务调度权归宿主:spawn 是「派发任务」而非「请求执行」——队列的执行方式 (并发池 / 优先级 / 暂停恢复)由宿主决定,插件无感;宿主未来引入并发调度不需要 任何插件改代码。宿主保证的不变量:行流与 exit 事件的顺序性(attachJob 回放/续流) 与 jobResult 兜底语义。
  • reserveOutput 的 onConflict: "reject" 改为返回 { filename, conflict: true } 由插件自行处置;预占会在上传中心立一条 reserved 条目(编码中可见),register 到达转正;
  • 同内容重复(宿主按 size + 头尾指纹判重)登记时宿主会向用户弹确认——跳过则 register 返回既有占位值,插件无感知;
  • 旧版 path 参数(0.3.8 及以前)已被明确拒绝(E_API_CHANGED),按 dirname + filename 重构建。

viewer 反显数据面

viewer.html(只读反显,manifest components.viewer 声明)由宿主挂载进字段反显位。 宿主 viewer 容器与 panel 同构:mbws:init + MessageChannel 端口握手——createMbws() 在 viewer 内立即拿到真客户端,数据由插件主动拉取(0.3.15 起与 panel 的 data 面 对齐但只读):

import { createMbws } from "@mbws/plugin-sdk";

const mbws = await createMbws();
// 产出为主(Partial——顶层键可能未产出,用前判断有无);原始数据对照按需再取
const outputs = await mbws.viewer.outputs();
const inputs = await mbws.viewer.inputs();

if (outputs.b !== undefined) {
  // 判断后拿到精确类型(如 string),直接用不需要再 cast
}

// 运行期产出变更(宿主发 mbws:event "viewer",payload = 新 outputs 对象):订阅制
const stop = mbws.viewer.onChange((next) => render(next));

Partial 判断约定:viewer.inputs() / viewer.outputs() 整体 Partial——顶层键 可能缺席(未产出/未采集),判断留在第一层(TS 强制「有/没有」),深层结构由插件自管; 漏判断强行使用会运行时抛 TypeError——建议模板同款 ErrorBoundary 兜底反显 JSON, 不会整面白屏。

探测口径:invoke 方法(进 host.capabilities().methods 的,如 viewer.outputs) 统一用 await mbws.host.can("viewer.outputs");订阅型成员(不进 methods 的,如 onChange)用 typeof mbws.viewer?.onChange === "function"——两类口径不要混用。 viewer 容器 grants 还含 caps(有桥时)——caps.exec.mediaUrl(local:…) 可直接播 未上传完的本地文件(编码中/上传中也可播)。宿主加载失败/超时会回退内置反显组件, 插件无需处理降级。

dev 夹具(0.3.16+):dev 直开 viewer.html 时 viewer.outputs() 读工程内 samples/outputs.json(内容原样返回,写什么得什么);面板 data.apply / data.commit 在 dev 下会写回该文件(deep-merge 落盘)——不开宿主页即可端到端联调反显样式。 脚手架(create-plugin 0.1.10 起)按契约 outputs 键集生成占位。

运行形态

插件代码零环境分支——createMbws() 内部自动分辨:

  • 生产(宿主 sandbox iframe 内):等待宿主经 window.postMessage 递来的 MessagePort,完成 init 握手后按授权构造命名空间。此后所有调用都走端口消息。
  • 开发(本地 pnpm dev / 顶窗口 / 端口等待超时):自动降级为 dev mock—— data.inputs / viewer.inputs 读工程内 samples/inputs.json、viewer.outputs / data.outputs 读 samples/outputs.json(均原样返回,缺省回落 per-key 合并); data.apply / data.commit 在控制台打印的同时写回 samples/outputs.json; 其余命名空间基本 no-op。contract.get 仍真实走 HTTP 拉线上契约。

握手等待端口默认 3000ms(createMbws({ timeoutMs }) 可调),单次调用默认 30s 超时 (invokeTimeoutMs 可调)。

dev 面模拟:MBWS_DEV_HOST_FACE=<ver> 环境变量让 dev mock 按 METHOD_INTRODUCED_AT 版本表摘掉新方法(对象属性真删,typeof 探测与 host.capabilities() 一致生效)——降级分支在浏览器 dev 即可测试,如 MBWS_DEV_HOST_FACE=0.3.9 pnpm dev。

dev 故障模拟:MBWS_DEV_SIMULATE=<token>[,<token>...] 往 happy path 里注入指定 故障,插件的降级/重试分支浏览器 dev 即可覆盖(语义与宿主对齐):

| token | 注入的故障 | | --- | --- | | reserve-conflict | reserveOutput 同名再占视为冲突:auto-rename 按 名 (1).ext 改名返回;onConflict: "reject" 返回 conflict: true 不预占 | | upload-fail | 模拟上传时序终态从 done 改 failed(error 标注模拟来源) | | attachment-fail | ensureAttachment 抛 E_ATTACHMENT_DOWNLOAD_FAILED + failed 事件 | | spawn-fail | spawn 发一行 stderr 后按 exit 1 失败收尾(不打 dev 执行端点) |

如 MBWS_DEV_SIMULATE=upload-fail,reserve-conflict pnpm dev。

状态词表(uploads × attachments 对照)

两套状态机字面相近、语义不同,对照如下(插件 UI 按它选文案/图标):

caps.uploads(产物上传,UploadStatusPayload.status;事件 CapsUploadEvent.phase 同词)

| status | 含义 | 触发时机 | | --- | --- | --- | | reserved | 已预占未落盘(编码中,mediaUrl 抛 E_NOT_READY) | reserveOutput 即立;上传中心可见 | | registered | 已登记待入队 | registerOutput(或 reserved 自动转正) | | pending | 已入队待传 | 上传队列调度中 | | uploading | 上传中(progress/speedBytesPerSec/etaSec 事件派生) | 字节推进时 | | done | 终态·已上传(objectKey 下发) | 上传完成 | | failed | 终态·失败(error 下发) | 上传失败 / 任务失败无产出 | | missing | 终态·文件缺失(预占超时无产出等) | 兜底回收 |

caps.exec 附件(处理引擎下载,AttachmentState.status;事件 CapsAttachmentEvent.phase)

| phase | 含义 | 触发时机 | | --- | --- | --- | | missing | 尚未下载(快照起点,非错误) | 首屏快照 | | downloading | 下载中(ratio/receivedBytes/totalBytes) | 首次使用附件 | | verifying | 校验 hash | 下载完成后 | | ready | 终态·就绪(同 hash 只下一次,后续零下载) | 校验通过 | | failed | 终态·失败(message 下发) | 下载/校验失败 |

错误处理

调用被拒时抛 MbwsInvokeError(Error 子类),error.code 为以下错误码之一:

| 错误码 | 含义 | | --- | --- | | E_INVALID_ARGS | 调用参数不符合方法签名 | | E_API_CHANGED | 调用使用了已废弃的旧方法签名(data 带 method/oldFields/newFields/since——按新字段重构建,不是修值) | | E_NOT_READY | 引用的内容未就绪(data 带 { status };典型:mediaUrl 对编码中的产物) | | E_UNKNOWN_METHOD | 调用了命名空间下不存在的方法 | | E_PERMISSION_DENIED | manifest 未声明该权限,或宿主未授予 | | E_QUOTA_EXCEEDED | 配额超限(AI 次数/费用、指令预算、wall-clock 超时等) | | E_WRITE_GATE_REJECTED | apply 写回被拒(key 越界 / outputSchema 校验失败) | | E_CAPABILITY_UNAVAILABLE | 依赖的能力包未安装或版本不满足 | | E_ATTACHMENT_DOWNLOAD_FAILED | 附件下载/校验/解包失败 | | E_ATTACHMENT_NOT_READY | spawn 时附件未就绪(data 带当前 AttachmentState) | | E_JOB_FAILED | 附件子进程非零退出(data 带 exitCode / stderrTail) | | E_TIMEOUT | 调用超时 | | E_CANCELLED | 任务被取消后的在途调用收尾 | | E_PROTOCOL_VIOLATION | 消息违反协议(含 createMbws() 二次调用) |

import { createMbws, MbwsInvokeError } from "@mbws/plugin-sdk";

const mbws = await createMbws();
try {
  await mbws.data.apply({ result: 1 });
} catch (error) {
  if (error instanceof MbwsInvokeError && error.code === "E_WRITE_GATE_REJECTED") {
    // error.data.issues: [{ field, reason, allowedFields?, hint }]——结构化定位,
    // 如 { field: "outputs.segments/0/sourceRanges", reason: "additional",
    //       allowedFields: ["clip", "duration"], hint: "..." }
  }
}

提交与上架流程

工程内不带文档(pnpm create @mbws/plugin 生成的骨架保持最小),流程统一查本包页:

  1. 认领任务的认领页打开「git 凭证」弹窗,拿 remote 地址 + username/token;
  2. 本地 git init && git add -A && git commit -m init, git remote add origin <remote 地址> && git push -u origin HEAD:main(push 只上传源码);
  3. 开发完成在认领页点「提交审查」——锚定那一刻的 main HEAD 进代码审查;
  4. 审查通过平台自动构建,构建成功进样例页试用验收;驳回改完再 push + 提交审查。

另请参阅

  • 工程脚手架:@mbws/create-plugin (pnpm create @mbws/plugin <目录> 一条命令生成带本 SDK 的完整插件工程)