自动化
插件
给空间加上你自己的动作:用任何语言写的 HTTPS 服务,或者通过 runner 在你自己机器上跑的命令。带签名的任务、按范围读取输入、结果写成附加信息和新文件。
插件给 Side Space 加动作:质检、自家的转写服务、在自己显卡上跑的超分辨率……插件的代码从不在 Side Space 服务器上运行。它要么是接收签名任务的 HTTPS 服务,要么是 runner:在你自己机器上主动领任务的命令,不需要公网地址。
空间管理员安装后,它的动作以 x-acme.qc.check 这样的名字出现在内置动作旁边:可以手动运行、放进流水线、被 agent 通过 MCP 调用,也一样按内容缓存(只在本空间内复用,插件升版本后重新处理)。
清单
{
"name": "x-acme.qc",
"title": "Acme QC",
"version": "1.2.0",
"endpoint": "https://qc.acme.example/side/jobs",
"timeout_seconds": 1800,
"actions": [
{
"name": "check",
"title": "检查掉帧",
"accepts": ["video/"],
"writes": ["x-acme.qc"],
"params_schema": { "type": "object", "properties": { "strict": { "type": "boolean" } } }
},
{ "name": "report", "title": "生成质检报告", "accepts": ["video/"], "writes": ["x-acme.qc", "files"] }
]
}name以x-开头;动作名是<name>.<action>。accepts是 MIME 前缀列表(空表示任何文件)。发任务之前,参数会按params_schema校验。writes是动作可能写的所有东西:x-…命名空间,声明了的话还有transcript、ai.tags、ai.generation、user和files(新文件,会带derived附加信息)。结果里写了没声明的内容,整次运行失败,什么都不写。version是缓存键的一部分:升版本后会重新处理文件。
sidespace plugin install "Film A" https://qc.acme.example/side/manifest.json # 打印签名密钥,只显示这一次
sidespace plugin list "Film A"
sidespace run "Film A" /Ep01/a.mov x-acme.qc.check --param strict=true
sidespace plugin enable "Film A" plg_… --off
sidespace plugin rm "Film A" plg_…HTTPS 插件
服务器向 endpoint POST 一个任务,签名方式和 Webhook 相同(Side-Signature,用安装时给的密钥):
{
"type": "run",
"run_id": "run_…",
"action": "check",
"attempt": 1,
"space_id": "spc_…",
"node_id": "nod_…",
"version_id": "ver_…",
"path": "/Ep01/a.mov",
"name": "a.mov",
"mime": "video/quicktime",
"size": 734003200,
"params": { "strict": true },
"sidecars": { "media": { "duration_s": 92.4, "width": 3840, "height": 2160, "fps": 24 } },
"input": { "url": "https://blocks.side.example.com/files/ver_…?t=…", "expires_at": "…" },
"callback": { "url": "https://side.example.com/v1/plugin-runs/run_…", "token": "srt_…", "expires_at": "…" }
}input.url 可以按字节范围读取这个版本的内容,有效期覆盖整个超时时间,所以只读需要的那一段就行。60 秒内回答:
| 回答 | 含义 |
|---|---|
200 加结果 {"sidecars": {"x-acme.qc": {…}}, "files": […]} | 完成 |
202 | 在后台处理,之后通过回调完成 |
4xx | 运行失败,显示你给的原因 |
5xx 或超时 | 和普通动作一样重试 |
回答 202 之后,带 Authorization: Bearer <callback.token> 调用回调:
POST {callback.url}/progress,带{"progress": 0.4}POST {callback.url}/upload拿上传票据,再把文件 POST 到网关的/stream-upload,得到 manifestPOST {callback.url}/complete交结果,或者{"error": "…", "retry": true}
超过 timeout_seconds 还没完成,会重新发一次,最多 3 次。
import { PluginRun } from "@sidespace/sdk";
import { verifyWebhookSignature } from "@sidespace/sdk/webhooks";
// 在你的 HTTP 处理函数里,用原始 body:
if (!verifyWebhookSignature(secret, req.headers["side-signature"], rawBody)) return res.writeHead(401).end();
res.writeHead(202).end();
const run = new PluginRun(JSON.parse(rawBody));
await run.progress(0.5);
const manifest = await run.upload(reportBytes);
await run.complete({ sidecars: { "x-acme.qc": { passed: true } }, files: [{ path: "/Ep01/a.qc.pdf", manifest }] });仓库里的 tests/e2e/fakeplugin.py 是一个只用 Python 标准库的完整参考实现,包括同步回答、回答 202 后回调、上传文件和验签。
Runner:在自己的机器上跑
不想部署 HTTPS 服务,或者要用本机的模型和显卡(whisper.cpp、本地视觉模型、调色工具)时,把清单里的 endpoint 换成 "runtime": "runner",然后在那台机器上运行:
sidespace plugin install "Film A" ./local-tools.json # 记下 plg_… 和 whsec_…
sidespace runner --plugin plg_… --secret whsec_… --jobs 2 -- ./qc.sh- runner 向服务器长轮询领任务,所以那台机器不需要公网地址,在 NAT 后面、笔记本上都行。它不在线时任务会排队。
- 每个任务调用一次你的命令。输入文件已经下载好,路径在
SIDE_INPUT;任务 JSON 在SIDE_JOB(stdin 上也有一份);SIDE_ACTION和SIDE_PARAMS是动作名和参数;新文件写到SIDE_OUTPUT目录。 - 在 stdout 打印结果 JSON,runner 会上传其中列出的文件,再完成这次运行:
{ "sidecars": { "x-acme.qc": { "passed": true } }, "files": [{ "path": "/Ep01/qc.pdf", "file": "qc.pdf", "mime": "application/pdf" }] }- stderr 里的
progress 0.4(或progress: 40%)会显示为进度,其余输出原样打印。退出码75表示稍后重试,其他非零退出码让运行失败,原因取 stderr 的最后几行。 timeout_seconds内没有 runner 来领,任务会重新排队,3 次后失败并提示“没有 runner”。plugin list会显示 runner 最后一次来领任务的时间。
其他语言自己实现 runner:带 Authorization: Bearer plg_…:whsec_… 调用 POST /v1/plugin-runs/claim?wait=25,拿到的任务和 HTTPS 插件收到的一样,再用任务里的回调汇报。