插件开发

Motrix 插件是打包成单个 ES2020 module 的 TypeScript 或 JavaScript。它运行在隔离沙箱里:没有 Node.js API,没有 require,不能直接访问文件或网络。与外界的一切交互都要经过你在 manifest 里声明的 capability,而 Motrix 会在安装前把你声明的内容原原本本展示给用户。所以请按最小权限设计——你多申请一项权限,用户的授权界面上就多一行;过宽的 host pattern 只会让你的插件看起来比实际更可疑。

插件能做什么

插件挂在下载的生命周期上。每个 hook 都会收到一个 context 对象,你可以检查它,并在允许的范围内修改它。

Hook运行时机能做什么
beforeCreate下载创建之前检查 urisheaderssaveDirctx.update({ uris, filename, connections, headers, proxy })
beforeFinalize数据下载完、文件尚未定稿ctx.update({ filePath }) 重命名
afterComplete文件已落盘做副作用:发通知、后处理、交接给其他工具
onError任务失败读取 error.code / error.message,记日志或发通知

除了 hook,插件还可以:

  • 贡献 command——注册可被其他插件或 host 调用的入口,参数与返回值都由 JSON Schema 校验。
  • 携带设置——声明一份 configuration schema,用户直接在 Motrix 界面上该插件的 「设置」 标签页里编辑,你的代码通过 config capability 读取。

每个 hook context 都带 AbortSignal(ctx.signal)——长耗时操作请尊重它——以及一个 metadata 存储,可在同一任务生命周期内的各个 hook 之间传值。

快速开始

pnpm create motrix-plugin my-plugin              # basic-resolver 模板
pnpm create motrix-plugin my-plugin post-action  # 模板作为第二个参数
cd my-plugin && pnpm install
模板初始内容
basic-resolver(默认)一个检查并改写下载的 beforeCreate hook(site-resolver category)
post-action一个下载完成后发桌面通知的 afterComplete hook(post-action category)

脚手架结构:

my-plugin/
├── motrix-plugin.json      # manifest——身份、权限、hook
├── src/index.ts            # 入口文件
├── locales/                # en-US.json、zh-CN.json(UI 字符串)
├── esbuild.config.mjs      # 独立构建配置
├── package.json            # scripts: build / pack / dev
└── tsconfig.json

第一个 hook 已经接好了:

import { hooks, log } from 'motrix:plugin-api'

hooks.beforeCreate(async (ctx) => {
  log.info('resolving', { uri: ctx.uris[0] })
  // rewrite the download before it starts:
  // ctx.update({ filename: 'nicer-name.zip' })
  return ctx
})

脚手架生成的插件 id 初始为 me.<name>。要设置真实的 publisher 前缀,可以直接编辑 motrix-plugin.json 里的 id,或改用支持 flag 的完整 CLI 来脚手架:

pnpm --package=@motrix/plugin-cli dlx motrix-plugin init my-plugin -t post-action -p acme

开发循环

pnpm dev        # 等价于 motrix-plugin dev

dev 会 watch-build src/index.ts(带 inline sourcemap),并启动你本机的 Motrix、直接从工作目录加载插件。改代码,bundle 自动重建;改 motrix-plugin.jsonlocales/,host 会重载插件。Motrix 从标准安装路径自动定位;如果你跑的是别处的构建,用 MOTRIX_BIN=/path/to/motrix 显式指定。

开着 dev 时,你的 log.* 输出会实时进入 Motrix 里该插件的 「日志」 标签页,可按 「日志级别」 过滤。在那里打开 「详细模式」 会在 1 小时内捕获完整 URL 与 header——调试 resolver 时很好用,但它会把真实 URL 写进日志,平时请保持关闭。

发布之前:

pnpm exec motrix-plugin validate   # 用官方 schema 校验 manifest
pnpm run pack                      # 压缩后的 bundle + .moext 包
pnpm exec motrix-plugin lint       # 对打包产物做静态检查

Tip

注意是 pnpm run pack 而不是 pnpm pack——裸的 pnpm pack 会调用 pnpm 内置的 tarball 命令,而不是脚手架里的 script。lint 检查的是打包产物,所以要先 pack。

Manifest

motrix-plugin.json 是插件与 host 之间的契约。一个最小的 resolver:

{
  "manifestVersion": 1,
  "id": "acme.example-resolver",
  "name": "Example Resolver",
  "version": "0.1.0",
  "description": "Rewrites example.com download links",
  "categories": ["site-resolver"],
  "engines": { "motrix": ">=2.0.0 <3.0.0" },
  "main": "dist/plugin.js",
  "permissions": ["http"],
  "hostPermissions": ["https://example.com/*"],
  "activationEvents": ["onTaskType:http"],
  "contributes": {
    "hooks": { "beforeCreate": { "role": "resolve" } }
  },
  "l10n": "locales"
}

字段规则,全部由 motrix-plugin validate 强制:

字段规则
id<publisher>.<name>,小写 a-z0-9-。publisher 前缀 motrixverifiedofficialsystem 为保留字。
versionSemver(1.2.3,允许 prerelease/build 后缀)。
categoriessite-resolverpost-actionthemeproductivityintegration 中选 1–8 个。hook 的 role 与 category 挂钩。
engines.motrix支持的 Motrix 版本 semver range。engines.ffmpeg 可选。
permissions你需要的 capability。自动注入的那些(logi18nconfiglifecyclecommandsappcrypto)不允许列出。最多 32 个。
optionalPermissions缺了也能用的 capability——用户拒绝时安装照样成功;运行时检查 .available
hostPermissions限定 http 能访问哪些地址的 URL match pattern(https://*.example.com/*<all_urls>)——超出范围的请求会以 plugin.http.host_not_permitted 失败。它同时决定按 URL 触发的 activation 何时生效,并会原样展示在授权界面上。最多 64 个。
activationEventshost 何时加载你(onTaskType:httponStartup 等)。必填。
contributes.hooks你实现了哪些生命周期 hook,每个都要带 role
contributes.commands你注册的 command,id 形如 <publisher>.<plugin>.<command>,最多 64 个。标记 public: true 的必须声明 argsSchema + resultSchema(受限的 JSON-Schema 子集:不支持 $ref/oneOf,≤ 8 KiB,深度 ≤ 8)。
contributes.configuration你的设置 schema,同一受限子集。
requestedHeapMB沙箱堆大小,32(默认)到 64。
l10nlocale JSON 文件所在目录。

当多个插件实现同一个 hook 时,role 决定执行顺序:resolveenrichpost-processaudit。其中两个与 category 挂钩——resolve 要求 site-resolverpost-process 要求 post-action。代码里注册但没声明 role 的 hook 按 enrich 执行。(pre-resolve 保留给内置插件。)

UI 字符串放在 locales/<lang>.json,运行时用 i18n.t('key') 读取。pack 会校验 locale 覆盖率——缺 key 在你构建时就失败,而不是留到用户会话里。

Capability 与权限

一切都从 motrix:plugin-api 这个 virtual module import。类型来自 @motrix/plugin-api 包,实现由 host 注入,所以永远不要把它打进 bundle——脚手架的 esbuild 配置已将其标记为 external

以下 capability 始终可用,且不允许出现在 permissions 里:

Namespace提供什么
log结构化日志:trace/debug/info/warn/error/fatal(msg, fields?)
i18nt(key, params?)、当前 language/dir、语言切换事件
config读取 contributes.configuration 的值,并可 onChange 订阅
lifecycleonActivate / onDeactivate,做初始化与清理
commands跨插件的 register(id, handler) / execute(id, args)
appHost 的 versionplatformarchruntime(electron/server)、locale
cryptohashhmacrandomBytesaes(cbc/gcm)

以下 capability 需要申请权限。声明之后,使用前先检查 .available

Namespace权限提供什么
httphttphttp.cookiesrequest/get/post,响应类型化(text/json/bytes),支持 timeout、range、按需开启的 cookie jar——仅限 http(s),且被限制在你声明的 hostPermissions 之内:超出范围的 URL(包括重定向目标)会抛出 plugin.http.host_not_permitted
storagestorage带版本号的 key-value 存储,compareAndSet 保证并发更新安全
fs.taskfs.task.readfs.task.write读取、stat、hash 或重命名当前 hook 正在处理的任务文件
fs.storagefs.storage插件私有的 scratch 目录
notifynotify桌面通知
ffmpegffmpegprobetranscodeextractAudiomergeStreamsgenerateThumbnail,带 progress 流——需要用户系统里装有 FFmpeg,所以请声明为 optional

用户看到的不是你写的权限字符串,Motrix 会在安装界面为每项权限渲染一行大白话:

你声明的用户读到的
http「读取网页」——“打开此插件支持的网页。”
http.cookies「使用 Cookie」——“用于访问已登录的网站。”
fs.task.read「读取任务文件」——“查看下载任务里的文件。”
fs.task.write「修改任务文件」——“可添加或更新任务文件。”
fs.storage「保存插件文件」——“只存此插件自己的文件。”
storage「保存设置」——“记住此插件的选项。”
notify「发送通知」——“显示完成提醒。”
ffmpeg「使用 FFmpeg」——“处理音频或视频。”

Warning

<all_urls>*://*/*http://*/*https://*/* 这几个 pattern 都算广泛访问。只要用了其中任意一个,安装对话框就会多出一块红色的 「广泛主机访问」 提示,告诉用户这个插件“可读取并修改任意 URL 的下载”。请改成逐个列出你真正需要的 host;motrix-plugin validate-host-permissions 能帮你检查 pattern 里的常见错误。

可选 capability 的标准写法是优雅降级:

import { notify } from 'motrix:plugin-api'

if (notify.available) {
  await notify.show({ title: 'Done', body: ctx.filePath })
}

沙箱规则

  • **禁止 top-level 副作用。**在顶层只注册 hook 和 command handler,实际工作放进 handler 里。lint 会把 top-level 的副作用调用判为 error——import 时就开始干活的插件会拖慢每一次 Motrix 启动。
  • **没有 Node.js。**没有 fsnetprocessrequire。上面列出的 capability 就是全部的外界入口。
  • **管好堆内存。**你有 32 MB,经 requestedHeapMB 最多 64。用流代替整块缓冲:fs.task.openReaderhttp 的 range 请求就是为这个准备的。
  • **尊重 ctx.signal。**用户会取消下载;无视 abort signal 的 hook 会把整条流水线扣为人质。
  • **目标 ES2020,单个 ESM 文件。**若使用自己的 bundler,记得保持 motrix:plugin-api external。

SDK 的四个包

用途
create-motrix-pluginpnpm create motrix-plugin 脚手架
@motrix/plugin-climotrix-plugin CLI:initdevvalidatepacklintvalidate-host-permissions
@motrix/plugin-api面向插件的类型 + motrix:plugin-api virtual-module 声明(在你的插件里是 devDependency)
@motrix/plugin-manifest-schemamanifest 的 Zod schema——CLI 与 Motrix host 共同校验所依据的唯一事实来源

正因为 CLI 和 host 用的是同一份 schema,能通过 motrix-plugin validate 的 manifest,Motrix 就一定接受。

分发插件

motrix-plugin pack 产出 dist/<id>-<version>.moext——一个可复现的 zip,内含 motrix-plugin.jsondist/plugin.js、locale 文件,以及存在时的 icon.png / LICENSE / CHANGELOG.md。它同时强制分发上限:bundle ≤ 1 MiB、archive ≤ 5 MiB。

用户在 Motrix 的 「插件」 页点 「添加插件」 来安装这个文件。同一个输入框接受三种形式,并自动识别你给的是哪一种:

形式粘贴什么
GitHubowner/repo[@tag]——Motrix 自己去解析 release 附件
URL指向 .moexthttps 直链
本地文件选择或拖入一个 .moext 文件

接着 Motrix 会根据你包内的 manifest 生成授权界面:插件名与描述、每项权限一行(可选权限是开关,用户可以不打开)、必要时的 「广泛主机访问」 提示,以及折叠在 「高级详情」 里的 host pattern。点 「安装插件」 完成安装。

想让插件出现在应用内商店的 「可安装的插件」 里,需要向公开的插件 registry 提交条目:向 motrixapp/plugin-registry 发一个 PR,新增 plugins/<your.plugin-id>.json,指向一个 https release 附件并附上它的 sha256size。Motrix 会在解包前校验这个哈希,并且会拒绝 manifest 与 registry 条目不一致的包——所以每发一个新版本,都要再提一次改版本号的 PR。

Important

registry 条目里的权限只是给商店列表看的预览,Motrix 真正授予的权限永远来自插件包内的 manifest。从 registry 直接安装目前只在桌面版可用;web 与服务器版本还在开发中,用户请改用 URL 或本地文件安装。

延伸阅读

  • 插件——用户怎么安装、审查、停用和删除插件,以及调试时在哪找每个插件的 「设置」「日志」 标签页。
  • 内置插件——官方插件,可以当作现成的 manifest 范例来读。
  • motrixapp/plugin-sdk——SDK 仓库与完整 README(含中文版),包含完整的 CLI 与 capability 参考。
  • motrixapp/plugin-registry——registry 的 schema、提交规则,以及商店背后的数据。