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.mdat the main repo root; reference implementations:lib/plugin.js(PluginManager),src/services/plugins.js(assembly),src/routes/plugins.js(HTTP endpoints),plugins/openvideo-plugin-demo1.1.0 (full-capability demo).
New manifest Fields
{
"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.openvideois a semver range for the host (>=,^,1.x,*,||); unsatisfied → load fails with主程序版本不满足: 需要 X,当前 Y.deps.pluginsmaps 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
hookssub-object). Omit them and behavior is identical to v1.
Lifecycle & Hooks
| Stage | When | Notes |
|---|---|---|
| load | on enable | validate deps → inject services → apply(ctx, config); broadcasts plugin:loaded {name,version} |
| unload | on disable | dispose chain (events, cron, static mounts, page unregistration, i18n rollback) → broadcasts plugin:unloaded |
| install | after npm install | runs hooks.install |
| enable / disable | after load / before unload | runs the matching hook |
| uninstall | after disable, before npm uninstall | runs hook; then the plugin's plugin_settings entries are wiped |
| update | after npm update, before reload | runs 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
// 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
| Event | Payload |
|---|---|
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
- Routes: every
ctx.routermethod 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. - Hooks: 15s timeout + full capture, logged only.
- Events:
emitwraps each handler in try/catch; cron errors are logged only.
Frontend Injection Points (v2 additions)
| Point | Declaration / usage |
|---|---|
| Custom pages | ctx.pages.register(...) — full-page HTML at any route, admin-auth optional |
| Static assets | ctx.static(mount, dir) — directory serving at any mount path |
| i18n terms | ctx.i18n.add(locale, dict) — aggregated via GET /api/plugins/i18n |
| Admin / player / login | client.admin / client.player / client.login (v1, unchanged) |
Client scripts are served via GET /api/plugins/client/:scope/:pkg/* (admin scope requires auth).
Related Endpoints
- Admin (unchanged from v1):
GET /api/admin/pluginsplus 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-demo1.1.0 — full demo of pages / static / cron / events / namespaces