插件开发
怎么写 danio 插件:包的结构、权限和设置、ctx 里有什么、事件和成片投递两个钩子,以及怎么安装调试。
插件是装在主机上、给工作区加上 danio 本身不做的事的 npm 包,比如把任务状态发到聊天群,或把成片传到网盘。这一页讲怎么写。怎么安装和管理,见插件。
插件用 JavaScript 或 TypeScript 写,由 Bun 运行。danio 会在主机上启动一个 Bun 进程,加载所有启用的插件。
先知道这几件事
- 只在主机上运行。美术电脑和渲染机不装 Bun,也不运行插件。
- danio 管生命周期。安装、启停、更新、设置和日志都在 danio 里,不用自己部署进程。
- 一个插件出错,只影响它自己。加载失败只标出这一个,其余照常;Bun 进程退出后 danio 会重启它,间隔从 1 秒起,最长 5 分钟。插件全部崩溃也不影响渲染。
- 权限要声明。插件通过
ctx.api调工作区接口,只有它声明、并经管理员确认的权限。 - 这不是沙箱。插件在主机上以 danio 的权限运行,能联网,能读写文件。权限只限制它对工作区的操作。
包的结构
插件是一个 npm 包,也可以是一个本地文件夹或 .tgz 文件。package.json 里的 danio 字段说明这个插件:
{
"name": "@my-studio/danio-chat-notify",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"engines": { "danio": ">=1" },
"danio": {
"name": "聊天群通知",
"summary": { "zh": "任务完成和出错时发到聊天群", "en": "Post task results to a chat group" },
"permissions": [],
"settings": {
"type": "object",
"required": ["webhookUrl"],
"properties": {
"webhookUrl": { "type": "string", "title": "Webhook 地址", "secret": true }
}
}
}
}
| 字段 | 说明 |
|---|---|
main | 入口文件。没写时用 module,再没有就是 index.js |
engines.danio | 需要的插件接口的主版本,现在是 1。可以写 >=1、^1、1、1.x 或 *。不兼容时 danio 不装 |
danio | 必须有,没有这个字段就不是 danio 插件 |
danio.name | 显示的名称,没写时用包名 |
danio.icon | 图标的地址,如 data: 地址 |
danio.summary | 一句话说明,按语言写:{ "zh": "…", "en": "…" } |
danio.permissions | 需要的工作区权限,如 workspace.view。不调用 ctx.api 的插件可以写 []。权限的完整列表见HTTP 接口 |
danio.settings | 设置的 JSON Schema |
设置
danio.settings 是 JSON Schema,管理员在界面里按它填设置。界面认这些写法:
properties里每一项的type可以是string、number、integer、boolean,写了enum就是下拉选择。title是界面上显示的名字,没写时显示键名。required里的项没填时,插件那一行会提示还没填。secret: true的项只写不读:界面上输入框是密码框,保存后只显示“已设置”,留空是不改。插件自己在ctx.settings里能读到它。
改了设置,插件会重启。
入口
入口文件默认导出一个函数,参数是 ctx,返回插件实现的钩子。函数可以是异步的:
export default async (ctx) => {
ctx.log('插件已加载')
return {
events: {
types: ['job.done', 'job.attention'],
handle: async (event) => {
// 处理事件
},
},
}
}
没导出函数,或函数抛错,这个插件加载失败,界面上显示原因。
现在有两个钩子:events 订阅事件,output 接收成片。都可以不写。
ctx
| 成员 | 说明 |
|---|---|
ctx.api | 工作区接口的客户端:get(path)、post(path, body)、put(path, body)、patch(path, body)、delete(path)。path 必须以 /api/v1/ 开头,返回解析好的 JSON。操作记在“插件:<名称>”名下,只有管理员确认的权限。出错时抛出带状态码的错误,如 403 …。每次调用最长 2 分钟 |
ctx.settings | 管理员填的设置,包括 secret 的项 |
ctx.state | 插件自己的小型键值存储:get(key)、set(key, value)、delete(key)。值是 JSON,每个不超过 64 KB。存在主机的数据库里,跟着备份走。get 找不到时返回 null |
ctx.log | 写日志:ctx.log(…),以及 ctx.log.info、warn、error、debug。插件里的 console.log 也记到这里 |
ctx.notify(title, body) | 在 danio 界面上弹一条提示,标明来自哪个插件 |
ctx.workspace | { id, name, addresses }:工作区 ID、名称,以及主机的地址列表 |
其余的用 Bun 和 npm 本身的能力,比如 fetch。
events:订阅事件
events: {
types: ['job.done', 'shot.failed'],
handle: async (event) => {
const { type, data } = event
// …
},
}
types 写要订阅的事件类型,* 是全部。handle 收到的 event 有这几项:
| 字段 | 说明 |
|---|---|
id | 事件 ID,用来去重:同一个事件可能送达不止一次 |
type | 事件类型 |
at | 发生的时间,ISO 8601 |
data | 事件的数据,见下表 |
事件类型和 data 里的字段:
| 类型 | 什么时候 | data |
|---|---|---|
job.submitted | 提交了新任务 | jobId、version、by |
shot.done | 一段渲完 | jobId、version、shot(第几段)、frames、device |
shot.failed | 一段失败 | 同上,加 error、attempts |
job.attention | 任务停下等人处理:连续失败、缺文件、拼接失败 | jobId、version、reason(手动暂停时为空) |
output.ready | 成片拼好 | jobId、version、output |
job.done | 任务完成,在 output.ready 之后 | 同 output.ready |
device.joined | 有设备加入 | deviceId、device、access |
device.removed | 设备被移除 | deviceId、device |
带 jobId 的事件,data 里还有 job(任务名)、project(项目名)和 projectId。
一个例子,任务完成时发到聊天群:
handle: async ({ type, data }) => {
if (type !== 'job.done') return
await fetch(ctx.settings.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `${data.project} / ${data.job} v${data.version} 渲完了` }),
})
}
送达
danio 先把事件存进主机的数据库,再送给订阅了这一类的插件,所以至少送达一次:
handle抛错或超过 1 分钟没返回,就重试,间隔 2 秒起,每次翻倍,最长 1 小时。- 插件进程崩溃或主机重启后,没送达的事件接着送。
- 插件停用期间的事件留着,再启用时送。删除插件时一起删掉。
- 超过一天没送达的事件,danio 放弃并记一条日志。
output:成片投递
成片拼好后,danio 把它交给每个实现了 output 的插件,由插件传到网盘或别的系统:
output: {
handle: async ({ job, project, version, film }) => {
const res = await fetch(`https://example.com/upload/${film.name}`, {
method: 'PUT',
body: Bun.file(film.path),
})
return { url: `https://example.com/films/${film.name}`, note: `已传到 ${project}` }
},
}
handle 的参数有:
| 字段 | 说明 |
|---|---|
jobId、job、project、projectId、version | 任务和版本 |
output | 成片在存储里的位置 |
film.path | 主机上的临时文件路径。成片已经从存储取好,插件不需要存储的凭据 |
film.name | 文件名,任务名加版本号加扩展名,如 sh010-Main_v2.mp4 |
film.size | 字节数 |
film.open() | 返回读这个文件的流 |
handle 返回后,临时文件就删掉了。
返回的 url 和 note 都可以不写。它们显示在任务详情的 投递 里,每个版本、每个插件一行:投递中、重试中、已投递、失败。
handle 抛错时,和事件一样按退避重试,最长一次等 1 小时,单次最长跑 1 小时。一天都没成功就记为失败。失败后点 重新投递,或调用 POST /api/v1/jobs/{id}/handovers/{plugin}/retry,重新交给它。
本地安装和调试
写插件时不用发布到 npm,在主机上直接装本地文件夹或 .tgz:
# 装一个文件夹
danio plugin add ./my-plugin
# 或先打包
npm pack
danio plugin add ./my-plugin-1.0.0.tgz
danio plugin add 先显示插件要的权限,你输入 y 确认后才启用。加 --yes 跳过确认。
改了代码,运行 danio plugin update,danio 从原来的来源重装所有插件,设置和权限保留。
| 命令 | 做什么 |
|---|---|
danio plugin [--json] | 列出插件:状态、版本、权限、订阅的事件、最近的错误 |
danio plugin add <npm 包 | 文件夹 | .tgz> [--yes] | 安装,确认权限后启用 |
danio plugin on|off|rm <插件> | 启用、停用、删除 |
danio plugin update | 从各自的来源重装全部插件 |
danio plugin config <插件> [键=值]… | 改设置,不给值时逐项询问。secret 的项不能写在命令行里 |
on 和 config 的 <插件> 写 ID、显示名称或包名都行,off 和 rm 只认 ID。ID 是包名去掉 @,其他不是小写字母、数字、.、_、- 的字符换成 -,如 @my-studio/danio-chat-notify 的 ID 是 my-studio-danio-chat-notify。
这些命令要在主机上运行。在界面的 插件与集成 页,每个插件的菜单里有 日志,显示插件的日志,包括 console.log 的输出。
Bun 从哪来
danio 按顺序找:环境变量 DANIO_BUN 指定的、PATH 里的、之前下载到数据目录 runtimes/bun/ 里的。都没有,就从 Bun 的 GitHub 发布页下载最新版,用它的 SHASUMS256.txt 核对后放进去。插件装在数据目录的 data/plugins/<ID>/ 里。