价格文档账号

插件开发

怎么写 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>/ 里。