Skip to content

Plugin Contract v2 ​

Applies to OpenVideoAPI 26.10.0+. v2 is fully backward compatible — every v1 field, method, event and frontend injection point is preserved; v2 is purely additive. The authoritative contract is PLUGIN-CONTRACT.md at the main repo root; reference implementations: lib/plugin.js (PluginManager), src/services/plugins.js (assembly), src/routes/plugins.js (HTTP endpoints), plugins/openvideo-plugin-demo 1.1.0 (full-capability demo).

New manifest Fields ​

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 (optional): deps.openvideo is a semver range for the host (>=, ^, 1.x, *, ||); unsatisfied → load fails with 主程序版本不满足: 需要 X,当前 Y. deps.plugins maps plugin names to ranges; missing → 依赖插件未安装, version mismatch → 依赖插件版本不满足; an installed-but-disabled dependency is enabled recursively.
  • hooks (optional): lifecycle hooks declared as exported function names (or a hooks sub-object). Omit them and behavior is identical to v1.

Lifecycle & Hooks ​

StageWhenNotes
loadon enablevalidate deps → inject services → apply(ctx, config); broadcasts plugin:loaded {name,version}
unloadon disabledispose chain (events, cron, static mounts, page unregistration, i18n rollback) → broadcasts plugin:unloaded
installafter npm installruns hooks.install
enable / disableafter load / before unloadruns the matching hook
uninstallafter disable, before npm uninstallruns hook; then the plugin's plugin_settings entries are wiped
updateafter npm update, before reloadruns hooks.update

Hook rules: the module is re-required per run; hooks receive a one-shot ctx (resources registered during a hook auto-expire); sync/async errors and the 15s timeout are logged only — management operations are never interrupted.

New ctx Capabilities ​

js
// HTTP client extensions (default timeout 15s)
await ctx.http.get(url, opts);            // raw Response
await ctx.http.text(url, opts);           // 2xx→text, else null
await ctx.http.request(url, { method, headers, body, timeout }); // full control

// Static asset dir (traversal-safe; auto-unmounted on dispose)
ctx.static('/plugins/xxx-assets', 'lib/client/assets');

// Custom pages (auth:true → 302 to admin path when unauthenticated; returns unregister fn)
ctx.pages.register({ route: '/plugin/xxx/hello', file: 'lib/client/page.html', title: 'Hello', auth: false });

// Cron (min interval 500ms; errors logged only; auto-cleaned on dispose)
const cancel = ctx.cron.every(5000, fn);
const cancel2 = ctx.cron.at(Date.now() + 60000, fn);

// Plugin-private persistent settings (isolated kv; ≤100KB per value, ≤200 keys; wiped on uninstall)
await ctx.settings.set('key', value);

// Structured logs (ring buffer; readable via GET /api/admin/plugins/logs)
ctx.logs.info('message', detail);

// i18n terms (public aggregate endpoint GET /api/plugins/i18n?locale=en[&plugin=xxx])
ctx.i18n.add('en', { 'xxx.hello': 'Hello' });
ctx.i18n.t('xxx.hello', 'zh');

// Event bus enhancements
const off = ctx.bus.on('ev', fn);          // same as ctx.on
ctx.bus.once('ev', fn); ctx.bus.off('ev', fn);
ctx.bus.emitTo('openvideo-plugin-otp', 'ev', payload); // targeted delivery
ctx.bus.events();

// Storage namespace (actual table xxx__kv, ≤48 chars)
const ns = ctx.model.namespace('xxx');
ns.define('kv', { primary: 'id', fields: { ... } });

The only behavioral difference: ctx.on now returns a cancel function (v1 returned the original handler; official plugins never relied on it).

Events Broadcast by the Host ​

EventPayload
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—

Any handler error is caught and logged; the main flow is never affected.

Three-Layer Error Isolation ​

  1. Routes: every ctx.router method wraps each handler — stale routes from hot-reloaded instances auto-neutralize (next()); errors are logged and returned as {code:1,msg:'插件路由异常: …'}, never thrown back into the host process.
  2. Hooks: 15s timeout + full capture, logged only.
  3. Events: emit wraps each handler in try/catch; cron errors are logged only.

Frontend Injection Points (v2 additions) ​

PointDeclaration / usage
Custom pagesctx.pages.register(...) — full-page HTML at any route, admin-auth optional
Static assetsctx.static(mount, dir) — directory serving at any mount path
i18n termsctx.i18n.add(locale, dict) — aggregated via GET /api/plugins/i18n
Admin / player / loginclient.admin / client.player / client.login (v1, unchanged)

Client scripts are served via GET /api/plugins/client/:scope/:pkg/* (admin scope requires auth).

  • Admin (unchanged from v1): GET /api/admin/plugins plus install / toggle / config / uninstall / update / market / logs.
  • Public, new in v2: GET /api/plugins/pages, GET /api/plugins/i18n?locale=&plugin=.

Install & Hot Reload ​

Install via npm (POST /api/admin/plugins/install) or drop a folder into plugins/ for auto-discovery (disabled by default); state lives in data/plugins.json; OPENVIDEO_PLUGIN_DIR / OPENVIDEO_DATA_DIR override the directories. With OPENVIDEO_DEV=1 a watcher monitors the plugin dir (400ms debounce, falling back to 1s mtime polling) — editing an enabled local plugin's source auto-runs disable→enable.

Compatibility ​

v1 plugins (demo 1.0.x, otp 1.0.x, ip-ban 1.0.x, embed-subtitle 1.0.5) run unmodified under v2. deps and hooks are optional declarations.

Next Steps ​

  • ctx API Reference — full v1 + v2 API
  • Plugin Guide — write your first plugin
  • Main-repo plugins/openvideo-plugin-demo 1.1.0 — full demo of pages / static / cron / events / namespaces

MIT License · Made with ♥ by yangyang8002