Skip to content

插件契约 v2 ​

适用版本:OpenVideoAPI 26.10.0+。v2 完全向后兼容——v1 的字段、方法、事件、前端注入点原样保留,v2 全部为增量。 权威契约为主仓库根目录的 PLUGIN-CONTRACT.md;参考实现:lib/plugin.js(PluginManager)、src/services/plugins.js(装配)、src/routes/plugins.js(HTTP 端点)、plugins/openvideo-plugin-demo 1.1.0(全能力演示)。

manifest 新增字段 ​

json
{
  "openvideoPlugin": {
    "deps": { "openvideo": ">=26.0.0", "plugins": { "openvideo-plugin-otp": "^1.0.0" } },
    "hooks": { "install": "onInstall", "enable": "onEnable", "disable": "onDisable", "uninstall": "onUninstall", "update": "onUpdate" }
  }
}
  • deps(可选):deps.openvideo 为主程序版本范围(semver:>=、^、1.x、*、|| 多选);不满足时加载报 主程序版本不满足: 需要 X,当前 Y。deps.plugins 为插件依赖表,未安装报 依赖插件未安装、版本不符报 依赖插件版本不满足;依赖存在但未启用时自动递归启用。
  • hooks(可选):声明生命周期钩子,值为主模块导出的函数名(也可直接挂 hooks 子对象)。不写即无钩子,与 v1 完全一致。

生命周期与钩子 ​

阶段时机说明
load启用时校验 deps → 注入服务 → apply(ctx, config);成功后广播 plugin:loaded {name,version}
unload停用时dispose 链(事件、cron、静态挂载、页面注销、i18n 回滚)→ 广播 plugin:unloaded
installnpm 安装完成后运行 hooks.install
enable / disableload 成功后 / unload 前运行对应钩子
uninstall停用后、npm 卸载前运行钩子;随后清空该插件在 plugin_settings 的设置
updatenpm 更新后、重载前运行 hooks.update

钩子执行规则:每次重新 require 插件模块;收到一次性 ctx(注册的资源随钩子结束自动失效);同步/异步异常与 15 秒超时都只记日志,绝不中断管理操作。

ctx 新增能力 ​

js
// HTTP 客户端扩展(默认超时 15s)
await ctx.http.get(url, opts);            // 原始 Response
await ctx.http.text(url, opts);           // 2xx→文本,否则 null
await ctx.http.request(url, { method, headers, body, timeout }); // 完整控制

// 静态资源目录(防目录穿越,dispose 自动卸载)
ctx.static('/plugins/xxx-assets', 'lib/client/assets');

// 自定义页面(auth:true 未鉴权时 302 到后台路径;返回注销函数)
ctx.pages.register({ route: '/plugin/xxx/hello', file: 'lib/client/page.html', title: 'Hello', auth: false });

// 定时任务(间隔下限 500ms;异常只记日志;dispose 自动清理)
const cancel = ctx.cron.every(5000, fn);
const cancel2 = ctx.cron.at(Date.now() + 60000, fn);

// 插件私有持久化设置(kv 隔离,单值 ≤100KB、≤200 键,卸载自动清空)
await ctx.settings.set('key', value);

// 结构化日志(环形缓冲,GET /api/admin/plugins/logs 可查)
ctx.logs.info('message', detail);

// 词条注入(公共聚合端点 GET /api/plugins/i18n?locale=zh[&plugin=xxx])
ctx.i18n.add('zh', { 'xxx.hello': '你好' });
ctx.i18n.t('xxx.hello', 'en');

// 事件总线增强
const off = ctx.bus.on('ev', fn);          // 同 ctx.on
ctx.bus.once('ev', fn); ctx.bus.off('ev', fn);
ctx.bus.emitTo('openvideo-plugin-otp', 'ev', payload); // 定向投递
ctx.bus.events();

// 存储命名空间(实际表名 xxx__kv,≤48 字符)
const ns = ctx.model.namespace('xxx');
ns.define('kv', { primary: 'id', fields: { ... } });

唯一行为差异:ctx.on 现在返回取消函数(v1 返回原 handler;官方插件未依赖该返回值)。

主程序广播的事件 ​

事件载荷
danmu:send{vid,text,color,type,time,author}
video:created{vid,url,source}(source = `map
video:saved / video:deleted{vid,url,source:'admin'} / {vid}
admin:login-ok / admin:login-fail{username,ip} / {username,ip,reason}
plugin:loaded / plugin:unloaded{name,version}
ready / before:restart—

任何 handler 异常都被捕获并记日志,不影响主流程。

三层错误隔离 ​

  1. 路由层:ctx.router 所有方法自动包裹每个 handler——旧实例路由热重载后自动失效(next());异常记日志并返回 {code:1,msg:'插件路由异常: …'},绝不抛回主进程。
  2. 钩子层:15s 超时 + 全捕获,只记日志。
  3. 事件层:emit 逐 handler try/catch;cron 异常只记日志。

前端注入点(v2 增补) ​

注入点声明 / 用法
自定义页面ctx.pages.register(...) — 任意路由整页 HTML,可要求管理员鉴权
静态资源ctx.static(mount, dir) — 任意挂载路径的目录服务
i18n 词条ctx.i18n.add(locale, dict) — GET /api/plugins/i18n 聚合拉取
后台 / 播放器 / 登录页client.admin / client.player / client.login(v1 保留)

客户端脚本经 GET /api/plugins/client/:scope/:pkg/* 下发(admin scope 需鉴权)。

相关端点 ​

  • 管理端(v1 不变):GET /api/admin/plugins 及 install / toggle / config / uninstall / update / market / logs。
  • 公共端 v2 新增:GET /api/plugins/pages、GET /api/plugins/i18n?locale=&plugin=。

安装与热重载 ​

npm 安装(POST /api/admin/plugins/install)或本地目录放入 plugins/ 自动发现(默认停用);状态存 data/plugins.json;OPENVIDEO_PLUGIN_DIR / OPENVIDEO_DATA_DIR 可覆盖目录。OPENVIDEO_DEV=1 时 watcher 监听插件目录(400ms 防抖,不可用时降级 1s mtime 轮询),已启用插件的本地源码变更自动 disable→enable。

兼容性 ​

v1 插件(demo 1.0.x、otp 1.0.x、ip-ban 1.0.x、embed-subtitle 1.0.5)在 v2 下无需修改即可运行。deps 与 hooks 均为可选声明。

下一步 ​

  • ctx API 参考 — v1 + v2 完整 API
  • 插件开发指南 — 从零写一个插件
  • 主仓库 plugins/openvideo-plugin-demo 1.1.0 — 自定义页面 / 静态资源 / cron / 事件 / 命名空间全演示

MIT License · Made with ♥ by yangyang8002