[{"data":1,"prerenderedAt":1631},["ShallowReactive",2],{"docs-nav:docs_zh":3,"docs-page:\u002Fdocs\u002Freference\u002Fplugins":155,"docs-surround:\u002Fdocs\u002Freference\u002Fplugins":1627},[4],{"title":5,"path":6,"stem":7,"children":8,"page":70},"Docs","\u002Fdocs","docs",[9,71,130],{"title":10,"icon":11,"path":12,"stem":13,"children":14,"page":70},"指南","i-lucide-book-open","\u002Fdocs\u002Fguides","docs\u002F1.guides",[15,20,25,30,35,40,45,50,55,60,65],{"title":16,"path":17,"stem":18,"icon":19},"简介","\u002Fdocs\u002Fguides\u002Fintroduction","docs\u002F1.guides\u002F01.introduction","i-lucide-info",{"title":21,"path":22,"stem":23,"icon":24},"快速上手","\u002Fdocs\u002Fguides\u002Fquick-start","docs\u002F1.guides\u002F02.quick-start","i-lucide-play",{"title":26,"path":27,"stem":28,"icon":29},"把办公室的电脑加进来","\u002Fdocs\u002Fguides\u002Foffice","docs\u002F1.guides\u002F03.office","i-lucide-building-2",{"title":31,"path":32,"stem":33,"icon":34},"在 GPU 租用平台渲染","\u002Fdocs\u002Fguides\u002Fcloud","docs\u002F1.guides\u002F04.cloud","i-lucide-cloud",{"title":36,"path":37,"stem":38,"icon":39},"配置 Cloudflare R2 存储","\u002Fdocs\u002Fguides\u002Fr2","docs\u002F1.guides\u002F05.r2","i-lucide-database",{"title":41,"path":42,"stem":43,"icon":44},"把主机放到 NAS 上","\u002Fdocs\u002Fguides\u002Fnas","docs\u002F1.guides\u002F06.nas","i-lucide-hard-drive",{"title":46,"path":47,"stem":48,"icon":49},"设备不在一处","\u002Fdocs\u002Fguides\u002Fremote","docs\u002F1.guides\u002F07.remote","i-lucide-globe",{"title":51,"path":52,"stem":53,"icon":54},"拿到成片","\u002Fdocs\u002Fguides\u002Ffilm","docs\u002F1.guides\u002F08.film","i-lucide-film",{"title":56,"path":57,"stem":58,"icon":59},"改了文件怎么办","\u002Fdocs\u002Fguides\u002Fupdate","docs\u002F1.guides\u002F09.update","i-lucide-refresh-cw",{"title":61,"path":62,"stem":63,"icon":64},"主机坏了或要换主机","\u002Fdocs\u002Fguides\u002Fhost","docs\u002F1.guides\u002F10.host","i-lucide-server-cog",{"title":66,"path":67,"stem":68,"icon":69},"常见问题","\u002Fdocs\u002Fguides\u002Ffaq","docs\u002F1.guides\u002F11.faq","i-lucide-circle-help",false,{"title":72,"icon":73,"path":74,"stem":75,"children":76,"page":70},"概念","i-lucide-box","\u002Fdocs\u002Fconcepts","docs\u002F2.concepts",[77,82,86,91,96,101,105,110,115,120,125],{"title":78,"path":79,"stem":80,"icon":81},"工作区和主机","\u002Fdocs\u002Fconcepts\u002Fworkspace","docs\u002F2.concepts\u002F01.workspace","i-lucide-network",{"title":83,"path":84,"stem":85,"icon":44},"存储","\u002Fdocs\u002Fconcepts\u002Fstorage","docs\u002F2.concepts\u002F02.storage",{"title":87,"path":88,"stem":89,"icon":90},"资产、解包与按需拉取","\u002Fdocs\u002Fconcepts\u002Fassets","docs\u002F2.concepts\u002F03.assets","i-lucide-images",{"title":92,"path":93,"stem":94,"icon":95},"项目、任务和版本","\u002Fdocs\u002Fconcepts\u002Ftasks","docs\u002F2.concepts\u002F04.tasks","i-lucide-list-checks",{"title":97,"path":98,"stem":99,"icon":100},"镜头和分段","\u002Fdocs\u002Fconcepts\u002Fshots","docs\u002F2.concepts\u002F05.shots","i-lucide-clapperboard",{"title":102,"path":103,"stem":104,"icon":54},"成片","\u002Fdocs\u002Fconcepts\u002Ffilm","docs\u002F2.concepts\u002F06.film",{"title":106,"path":107,"stem":108,"icon":109},"Blender 和 FFmpeg","\u002Fdocs\u002Fconcepts\u002Fruntimes","docs\u002F2.concepts\u002F07.runtimes","i-lucide-package",{"title":111,"path":112,"stem":113,"icon":114},"加入与权限","\u002Fdocs\u002Fconcepts\u002Fjoining","docs\u002F2.concepts\u002F08.joining","i-lucide-key-round",{"title":116,"path":117,"stem":118,"icon":119},"渲染名额","\u002Fdocs\u002Fconcepts\u002Fseats","docs\u002F2.concepts\u002F09.seats","i-lucide-gauge",{"title":121,"path":122,"stem":123,"icon":124},"备份和接管","\u002Fdocs\u002Fconcepts\u002Fbackup","docs\u002F2.concepts\u002F10.backup","i-lucide-life-buoy",{"title":126,"path":127,"stem":128,"icon":129},"插件","\u002Fdocs\u002Fconcepts\u002Fplugins","docs\u002F2.concepts\u002F11.plugins","i-lucide-puzzle",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":70},"参考","i-lucide-library","\u002Fdocs\u002Freference","docs\u002F3.reference",[136,141,146,151],{"title":137,"path":138,"stem":139,"icon":140},"命令行","\u002Fdocs\u002Freference\u002Fcli","docs\u002F3.reference\u002F1.cli","i-lucide-terminal",{"title":142,"path":143,"stem":144,"icon":145},"本机配置","\u002Fdocs\u002Freference\u002Fconfig","docs\u002F3.reference\u002F2.config","i-lucide-sliders",{"title":147,"path":148,"stem":149,"icon":150},"HTTP 接口","\u002Fdocs\u002Freference\u002Fapi","docs\u002F3.reference\u002F3.api","i-lucide-code-xml",{"title":152,"path":153,"stem":154,"icon":129},"插件开发","\u002Fdocs\u002Freference\u002Fplugins","docs\u002F3.reference\u002F4.plugins",{"id":156,"title":152,"body":157,"description":1621,"extension":1622,"meta":1623,"navigation":1624,"path":153,"seo":1625,"stem":154,"__hash__":1626},"docs_zh\u002Fdocs\u002F3.reference\u002F4.plugins.md",{"type":158,"value":159,"toc":1606},"minimark",[160,168,171,175,215,218,233,358,506,510,515,566,569,572,579,645,648,659,661,804,810,814,853,870,921,927,1102,1122,1125,1169,1172,1175,1191,1195,1201,1249,1254,1335,1340,1356,1369,1372,1378,1434,1448,1455,1521,1565,1579,1583,1602],[161,162,163,164,167],"p",{},"插件是装在主机上、给工作区加上 danio 本身不做的事的 npm 包，比如把任务状态发到聊天群，或把成片传到网盘。这一页讲怎么写。怎么安装和管理，见",[165,166,126],"a",{"href":127},"。",[161,169,170],{},"插件用 JavaScript 或 TypeScript 写，由 Bun 运行。danio 会在主机上启动一个 Bun 进程，加载所有启用的插件。",[172,173,174],"h2",{"id":174},"先知道这几件事",[176,177,178,186,192,198,209],"ul",{},[179,180,181,185],"li",{},[182,183,184],"strong",{},"只在主机上运行","。美术电脑和渲染机不装 Bun，也不运行插件。",[179,187,188,191],{},[182,189,190],{},"danio 管生命周期","。安装、启停、更新、设置和日志都在 danio 里，不用自己部署进程。",[179,193,194,197],{},[182,195,196],{},"一个插件出错，只影响它自己","。加载失败只标出这一个，其余照常；Bun 进程退出后 danio 会重启它，间隔从 1 秒起，最长 5 分钟。插件全部崩溃也不影响渲染。",[179,199,200,203,204,208],{},[182,201,202],{},"权限要声明","。插件通过 ",[205,206,207],"code",{},"ctx.api"," 调工作区接口，只有它声明、并经管理员确认的权限。",[179,210,211,214],{},[182,212,213],{},"这不是沙箱","。插件在主机上以 danio 的权限运行，能联网，能读写文件。权限只限制它对工作区的操作。",[172,216,217],{"id":217},"包的结构",[161,219,220,221,224,225,228,229,232],{},"插件是一个 npm 包，也可以是一个本地文件夹或 ",[205,222,223],{},".tgz"," 文件。",[205,226,227],{},"package.json"," 里的 ",[205,230,231],{},"danio"," 字段说明这个插件：",[234,235,240],"pre",{"className":236,"code":237,"language":238,"meta":239,"style":239},"language-json shiki shiki-themes material-theme-palenight","{\n  \"name\": \"@my-studio\u002Fdanio-chat-notify\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"engines\": { \"danio\": \">=1\" },\n  \"danio\": {\n    \"name\": \"聊天群通知\",\n    \"summary\": { \"zh\": \"任务完成和出错时发到聊天群\", \"en\": \"Post task results to a chat group\" },\n    \"permissions\": [],\n    \"settings\": {\n      \"type\": \"object\",\n      \"required\": [\"webhookUrl\"],\n      \"properties\": {\n        \"webhookUrl\": { \"type\": \"string\", \"title\": \"Webhook 地址\", \"secret\": true }\n      }\n    }\n  }\n}\n","json","",[205,241,242,250,256,262,268,274,280,286,292,298,304,310,316,322,328,334,340,346,352],{"__ignoreMap":239},[243,244,247],"span",{"class":245,"line":246},"line",1,[243,248,249],{},"{\n",[243,251,253],{"class":245,"line":252},2,[243,254,255],{},"  \"name\": \"@my-studio\u002Fdanio-chat-notify\",\n",[243,257,259],{"class":245,"line":258},3,[243,260,261],{},"  \"version\": \"1.0.0\",\n",[243,263,265],{"class":245,"line":264},4,[243,266,267],{},"  \"type\": \"module\",\n",[243,269,271],{"class":245,"line":270},5,[243,272,273],{},"  \"main\": \"index.js\",\n",[243,275,277],{"class":245,"line":276},6,[243,278,279],{},"  \"engines\": { \"danio\": \">=1\" },\n",[243,281,283],{"class":245,"line":282},7,[243,284,285],{},"  \"danio\": {\n",[243,287,289],{"class":245,"line":288},8,[243,290,291],{},"    \"name\": \"聊天群通知\",\n",[243,293,295],{"class":245,"line":294},9,[243,296,297],{},"    \"summary\": { \"zh\": \"任务完成和出错时发到聊天群\", \"en\": \"Post task results to a chat group\" },\n",[243,299,301],{"class":245,"line":300},10,[243,302,303],{},"    \"permissions\": [],\n",[243,305,307],{"class":245,"line":306},11,[243,308,309],{},"    \"settings\": {\n",[243,311,313],{"class":245,"line":312},12,[243,314,315],{},"      \"type\": \"object\",\n",[243,317,319],{"class":245,"line":318},13,[243,320,321],{},"      \"required\": [\"webhookUrl\"],\n",[243,323,325],{"class":245,"line":324},14,[243,326,327],{},"      \"properties\": {\n",[243,329,331],{"class":245,"line":330},15,[243,332,333],{},"        \"webhookUrl\": { \"type\": \"string\", \"title\": \"Webhook 地址\", \"secret\": true }\n",[243,335,337],{"class":245,"line":336},16,[243,338,339],{},"      }\n",[243,341,343],{"class":245,"line":342},17,[243,344,345],{},"    }\n",[243,347,349],{"class":245,"line":348},18,[243,350,351],{},"  }\n",[243,353,355],{"class":245,"line":354},19,[243,356,357],{},"}\n",[359,360,361,374],"table",{},[362,363,364],"thead",{},[365,366,367,371],"tr",{},[368,369,370],"th",{},"字段",[368,372,373],{},"说明",[375,376,377,395,426,435,445,459,472,496],"tbody",{},[365,378,379,385],{},[380,381,382],"td",{},[205,383,384],{},"main",[380,386,387,388,391,392],{},"入口文件。没写时用 ",[205,389,390],{},"module","，再没有就是 ",[205,393,394],{},"index.js",[365,396,397,402],{},[380,398,399],{},[205,400,401],{},"engines.danio",[380,403,404,405,408,409,412,413,412,416,412,418,421,422,425],{},"需要的插件接口的主版本，现在是 ",[205,406,407],{},"1","。可以写 ",[205,410,411],{},">=1","、",[205,414,415],{},"^1",[205,417,407],{},[205,419,420],{},"1.x"," 或 ",[205,423,424],{},"*","。不兼容时 danio 不装",[365,427,428,432],{},[380,429,430],{},[205,431,231],{},[380,433,434],{},"必须有，没有这个字段就不是 danio 插件",[365,436,437,442],{},[380,438,439],{},[205,440,441],{},"danio.name",[380,443,444],{},"显示的名称，没写时用包名",[365,446,447,452],{},[380,448,449],{},[205,450,451],{},"danio.icon",[380,453,454,455,458],{},"图标的地址，如 ",[205,456,457],{},"data:"," 地址",[365,460,461,466],{},[380,462,463],{},[205,464,465],{},"danio.summary",[380,467,468,469],{},"一句话说明，按语言写：",[205,470,471],{},"{ \"zh\": \"…\", \"en\": \"…\" }",[365,473,474,479],{},[380,475,476],{},[205,477,478],{},"danio.permissions",[380,480,481,482,485,486,488,489,492,493],{},"需要的工作区权限，如 ",[205,483,484],{},"workspace.view","。不调用 ",[205,487,207],{}," 的插件可以写 ",[205,490,491],{},"[]","。权限的完整列表见",[165,494,147],{"href":495},"\u002Fdocs\u002Freference\u002Fapi#%E4%BB%A4%E7%89%8C%E5%92%8C%E6%9D%83%E9%99%90",[365,497,498,503],{},[380,499,500],{},[205,501,502],{},"danio.settings",[380,504,505],{},"设置的 JSON Schema",[507,508,509],"h3",{"id":509},"设置",[161,511,512,514],{},[205,513,502],{}," 是 JSON Schema，管理员在界面里按它填设置。界面认这些写法：",[176,516,517,544,550,556],{},[179,518,519,522,523,526,527,412,530,412,533,412,536,539,540,543],{},[205,520,521],{},"properties"," 里每一项的 ",[205,524,525],{},"type"," 可以是 ",[205,528,529],{},"string",[205,531,532],{},"number",[205,534,535],{},"integer",[205,537,538],{},"boolean","，写了 ",[205,541,542],{},"enum"," 就是下拉选择。",[179,545,546,549],{},[205,547,548],{},"title"," 是界面上显示的名字，没写时显示键名。",[179,551,552,555],{},[205,553,554],{},"required"," 里的项没填时，插件那一行会提示还没填。",[179,557,558,561,562,565],{},[205,559,560],{},"secret: true"," 的项只写不读：界面上输入框是密码框，保存后只显示“已设置”，留空是不改。插件自己在 ",[205,563,564],{},"ctx.settings"," 里能读到它。",[161,567,568],{},"改了设置，插件会重启。",[172,570,571],{"id":571},"入口",[161,573,574,575,578],{},"入口文件默认导出一个函数，参数是 ",[205,576,577],{},"ctx","，返回插件实现的钩子。函数可以是异步的：",[234,580,584],{"className":581,"code":582,"language":583,"meta":239,"style":239},"language-js shiki shiki-themes material-theme-palenight","export default async (ctx) => {\n  ctx.log('插件已加载')\n\n  return {\n    events: {\n      types: ['job.done', 'job.attention'],\n      handle: async (event) => {\n        \u002F\u002F 处理事件\n      },\n    },\n  }\n}\n","js",[205,585,586,591,596,602,607,612,617,622,627,632,637,641],{"__ignoreMap":239},[243,587,588],{"class":245,"line":246},[243,589,590],{},"export default async (ctx) => {\n",[243,592,593],{"class":245,"line":252},[243,594,595],{},"  ctx.log('插件已加载')\n",[243,597,598],{"class":245,"line":258},[243,599,601],{"emptyLinePlaceholder":600},true,"\n",[243,603,604],{"class":245,"line":264},[243,605,606],{},"  return {\n",[243,608,609],{"class":245,"line":270},[243,610,611],{},"    events: {\n",[243,613,614],{"class":245,"line":276},[243,615,616],{},"      types: ['job.done', 'job.attention'],\n",[243,618,619],{"class":245,"line":282},[243,620,621],{},"      handle: async (event) => {\n",[243,623,624],{"class":245,"line":288},[243,625,626],{},"        \u002F\u002F 处理事件\n",[243,628,629],{"class":245,"line":294},[243,630,631],{},"      },\n",[243,633,634],{"class":245,"line":300},[243,635,636],{},"    },\n",[243,638,639],{"class":245,"line":306},[243,640,351],{},[243,642,643],{"class":245,"line":312},[243,644,357],{},[161,646,647],{},"没导出函数，或函数抛错，这个插件加载失败，界面上显示原因。",[161,649,650,651,654,655,658],{},"现在有两个钩子：",[205,652,653],{},"events"," 订阅事件，",[205,656,657],{},"output"," 接收成片。都可以不写。",[172,660,577],{"id":577},[359,662,663,672],{},[362,664,665],{},[365,666,667,670],{},[368,668,669],{},"成员",[368,671,373],{},[375,673,674,710,723,750,781,791],{},[365,675,676,680],{},[380,677,678],{},[205,679,207],{},[380,681,682,683,412,686,412,689,412,692,412,695,167,698,701,702,705,706,709],{},"工作区接口的客户端：",[205,684,685],{},"get(path)",[205,687,688],{},"post(path, body)",[205,690,691],{},"put(path, body)",[205,693,694],{},"patch(path, body)",[205,696,697],{},"delete(path)",[205,699,700],{},"path"," 必须以 ",[205,703,704],{},"\u002Fapi\u002Fv1\u002F"," 开头，返回解析好的 JSON。操作记在“插件：\u003C名称>”名下，只有管理员确认的权限。出错时抛出带状态码的错误，如 ",[205,707,708],{},"403 …","。每次调用最长 2 分钟",[365,711,712,716],{},[380,713,714],{},[205,715,564],{},[380,717,718,719,722],{},"管理员填的设置，包括 ",[205,720,721],{},"secret"," 的项",[365,724,725,730],{},[380,726,727],{},[205,728,729],{},"ctx.state",[380,731,732,733,412,736,412,739,742,743,746,747],{},"插件自己的小型键值存储：",[205,734,735],{},"get(key)",[205,737,738],{},"set(key, value)",[205,740,741],{},"delete(key)","。值是 JSON，每个不超过 64 KB。存在主机的数据库里，跟着备份走。",[205,744,745],{},"get"," 找不到时返回 ",[205,748,749],{},"null",[365,751,752,757],{},[380,753,754],{},[205,755,756],{},"ctx.log",[380,758,759,760,763,764,412,767,412,770,412,773,776,777,780],{},"写日志：",[205,761,762],{},"ctx.log(…)","，以及 ",[205,765,766],{},"ctx.log.info",[205,768,769],{},"warn",[205,771,772],{},"error",[205,774,775],{},"debug","。插件里的 ",[205,778,779],{},"console.log"," 也记到这里",[365,782,783,788],{},[380,784,785],{},[205,786,787],{},"ctx.notify(title, body)",[380,789,790],{},"在 danio 界面上弹一条提示，标明来自哪个插件",[365,792,793,798],{},[380,794,795],{},[205,796,797],{},"ctx.workspace",[380,799,800,803],{},[205,801,802],{},"{ id, name, addresses }","：工作区 ID、名称，以及主机的地址列表",[161,805,806,807,167],{},"其余的用 Bun 和 npm 本身的能力，比如 ",[205,808,809],{},"fetch",[172,811,813],{"id":812},"events订阅事件","events：订阅事件",[234,815,817],{"className":581,"code":816,"language":583,"meta":239,"style":239},"events: {\n  types: ['job.done', 'shot.failed'],\n  handle: async (event) => {\n    const { type, data } = event\n    \u002F\u002F …\n  },\n}\n",[205,818,819,824,829,834,839,844,849],{"__ignoreMap":239},[243,820,821],{"class":245,"line":246},[243,822,823],{},"events: {\n",[243,825,826],{"class":245,"line":252},[243,827,828],{},"  types: ['job.done', 'shot.failed'],\n",[243,830,831],{"class":245,"line":258},[243,832,833],{},"  handle: async (event) => {\n",[243,835,836],{"class":245,"line":264},[243,837,838],{},"    const { type, data } = event\n",[243,840,841],{"class":245,"line":270},[243,842,843],{},"    \u002F\u002F …\n",[243,845,846],{"class":245,"line":276},[243,847,848],{},"  },\n",[243,850,851],{"class":245,"line":282},[243,852,357],{},[161,854,855,858,859,861,862,865,866,869],{},[205,856,857],{},"types"," 写要订阅的事件类型，",[205,860,424],{}," 是全部。",[205,863,864],{},"handle"," 收到的 ",[205,867,868],{},"event"," 有这几项：",[359,871,872,880],{},[362,873,874],{},[365,875,876,878],{},[368,877,370],{},[368,879,373],{},[375,881,882,892,901,911],{},[365,883,884,889],{},[380,885,886],{},[205,887,888],{},"id",[380,890,891],{},"事件 ID，用来去重：同一个事件可能送达不止一次",[365,893,894,898],{},[380,895,896],{},[205,897,525],{},[380,899,900],{},"事件类型",[365,902,903,908],{},[380,904,905],{},[205,906,907],{},"at",[380,909,910],{},"发生的时间，ISO 8601",[365,912,913,918],{},[380,914,915],{},[205,916,917],{},"data",[380,919,920],{},"事件的数据，见下表",[161,922,923,924,926],{},"事件类型和 ",[205,925,917],{}," 里的字段：",[359,928,929,943],{},[362,930,931],{},[365,932,933,936,939],{},[368,934,935],{},"类型",[368,937,938],{},"什么时候",[368,940,941],{},[205,942,917],{},[375,944,945,966,992,1010,1030,1048,1066,1086],{},[365,946,947,952,955],{},[380,948,949],{},[205,950,951],{},"job.submitted",[380,953,954],{},"提交了新任务",[380,956,957,412,960,412,963],{},[205,958,959],{},"jobId",[205,961,962],{},"version",[205,964,965],{},"by",[365,967,968,973,976],{},[380,969,970],{},[205,971,972],{},"shot.done",[380,974,975],{},"一段渲完",[380,977,978,412,980,412,982,985,986,412,989],{},[205,979,959],{},[205,981,962],{},[205,983,984],{},"shot","（第几段）、",[205,987,988],{},"frames",[205,990,991],{},"device",[365,993,994,999,1002],{},[380,995,996],{},[205,997,998],{},"shot.failed",[380,1000,1001],{},"一段失败",[380,1003,1004,1005,412,1007],{},"同上，加 ",[205,1006,772],{},[205,1008,1009],{},"attempts",[365,1011,1012,1017,1020],{},[380,1013,1014],{},[205,1015,1016],{},"job.attention",[380,1018,1019],{},"任务停下等人处理：连续失败、缺文件、拼接失败",[380,1021,1022,412,1024,412,1026,1029],{},[205,1023,959],{},[205,1025,962],{},[205,1027,1028],{},"reason","（手动暂停时为空）",[365,1031,1032,1037,1040],{},[380,1033,1034],{},[205,1035,1036],{},"output.ready",[380,1038,1039],{},"成片拼好",[380,1041,1042,412,1044,412,1046],{},[205,1043,959],{},[205,1045,962],{},[205,1047,657],{},[365,1049,1050,1055,1061],{},[380,1051,1052],{},[205,1053,1054],{},"job.done",[380,1056,1057,1058,1060],{},"任务完成，在 ",[205,1059,1036],{}," 之后",[380,1062,1063,1064],{},"同 ",[205,1065,1036],{},[365,1067,1068,1073,1076],{},[380,1069,1070],{},[205,1071,1072],{},"device.joined",[380,1074,1075],{},"有设备加入",[380,1077,1078,412,1081,412,1083],{},[205,1079,1080],{},"deviceId",[205,1082,991],{},[205,1084,1085],{},"access",[365,1087,1088,1093,1096],{},[380,1089,1090],{},[205,1091,1092],{},"device.removed",[380,1094,1095],{},"设备被移除",[380,1097,1098,412,1100],{},[205,1099,1080],{},[205,1101,991],{},[161,1103,1104,1105,1107,1108,1110,1111,1114,1115,1118,1119,167],{},"带 ",[205,1106,959],{}," 的事件，",[205,1109,917],{}," 里还有 ",[205,1112,1113],{},"job","（任务名）、",[205,1116,1117],{},"project","（项目名）和 ",[205,1120,1121],{},"projectId",[161,1123,1124],{},"一个例子，任务完成时发到聊天群：",[234,1126,1128],{"className":581,"code":1127,"language":583,"meta":239,"style":239},"handle: async ({ type, data }) => {\n  if (type !== 'job.done') return\n  await fetch(ctx.settings.webhookUrl, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ text: `${data.project} \u002F ${data.job} v${data.version} 渲完了` }),\n  })\n}\n",[205,1129,1130,1135,1140,1145,1150,1155,1160,1165],{"__ignoreMap":239},[243,1131,1132],{"class":245,"line":246},[243,1133,1134],{},"handle: async ({ type, data }) => {\n",[243,1136,1137],{"class":245,"line":252},[243,1138,1139],{},"  if (type !== 'job.done') return\n",[243,1141,1142],{"class":245,"line":258},[243,1143,1144],{},"  await fetch(ctx.settings.webhookUrl, {\n",[243,1146,1147],{"class":245,"line":264},[243,1148,1149],{},"    method: 'POST',\n",[243,1151,1152],{"class":245,"line":270},[243,1153,1154],{},"    headers: { 'Content-Type': 'application\u002Fjson' },\n",[243,1156,1157],{"class":245,"line":276},[243,1158,1159],{},"    body: JSON.stringify({ text: `${data.project} \u002F ${data.job} v${data.version} 渲完了` }),\n",[243,1161,1162],{"class":245,"line":282},[243,1163,1164],{},"  })\n",[243,1166,1167],{"class":245,"line":288},[243,1168,357],{},[507,1170,1171],{"id":1171},"送达",[161,1173,1174],{},"danio 先把事件存进主机的数据库，再送给订阅了这一类的插件，所以至少送达一次：",[176,1176,1177,1182,1185,1188],{},[179,1178,1179,1181],{},[205,1180,864],{}," 抛错或超过 1 分钟没返回，就重试，间隔 2 秒起，每次翻倍，最长 1 小时。",[179,1183,1184],{},"插件进程崩溃或主机重启后，没送达的事件接着送。",[179,1186,1187],{},"插件停用期间的事件留着，再启用时送。删除插件时一起删掉。",[179,1189,1190],{},"超过一天没送达的事件，danio 放弃并记一条日志。",[172,1192,1194],{"id":1193},"output成片投递","output：成片投递",[161,1196,1197,1198,1200],{},"成片拼好后，danio 把它交给每个实现了 ",[205,1199,657],{}," 的插件，由插件传到网盘或别的系统：",[234,1202,1204],{"className":581,"code":1203,"language":583,"meta":239,"style":239},"output: {\n  handle: async ({ job, project, version, film }) => {\n    const res = await fetch(`https:\u002F\u002Fexample.com\u002Fupload\u002F${film.name}`, {\n      method: 'PUT',\n      body: Bun.file(film.path),\n    })\n    return { url: `https:\u002F\u002Fexample.com\u002Ffilms\u002F${film.name}`, note: `已传到 ${project}` }\n  },\n}\n",[205,1205,1206,1211,1216,1221,1226,1231,1236,1241,1245],{"__ignoreMap":239},[243,1207,1208],{"class":245,"line":246},[243,1209,1210],{},"output: {\n",[243,1212,1213],{"class":245,"line":252},[243,1214,1215],{},"  handle: async ({ job, project, version, film }) => {\n",[243,1217,1218],{"class":245,"line":258},[243,1219,1220],{},"    const res = await fetch(`https:\u002F\u002Fexample.com\u002Fupload\u002F${film.name}`, {\n",[243,1222,1223],{"class":245,"line":264},[243,1224,1225],{},"      method: 'PUT',\n",[243,1227,1228],{"class":245,"line":270},[243,1229,1230],{},"      body: Bun.file(film.path),\n",[243,1232,1233],{"class":245,"line":276},[243,1234,1235],{},"    })\n",[243,1237,1238],{"class":245,"line":282},[243,1239,1240],{},"    return { url: `https:\u002F\u002Fexample.com\u002Ffilms\u002F${film.name}`, note: `已传到 ${project}` }\n",[243,1242,1243],{"class":245,"line":288},[243,1244,848],{},[243,1246,1247],{"class":245,"line":294},[243,1248,357],{},[161,1250,1251,1253],{},[205,1252,864],{}," 的参数有：",[359,1255,1256,1264],{},[362,1257,1258],{},[365,1259,1260,1262],{},[368,1261,370],{},[368,1263,373],{},[375,1265,1266,1283,1292,1302,1315,1325],{},[365,1267,1268,1280],{},[380,1269,1270,412,1272,412,1274,412,1276,412,1278],{},[205,1271,959],{},[205,1273,1113],{},[205,1275,1117],{},[205,1277,1121],{},[205,1279,962],{},[380,1281,1282],{},"任务和版本",[365,1284,1285,1289],{},[380,1286,1287],{},[205,1288,657],{},[380,1290,1291],{},"成片在存储里的位置",[365,1293,1294,1299],{},[380,1295,1296],{},[205,1297,1298],{},"film.path",[380,1300,1301],{},"主机上的临时文件路径。成片已经从存储取好，插件不需要存储的凭据",[365,1303,1304,1309],{},[380,1305,1306],{},[205,1307,1308],{},"film.name",[380,1310,1311,1312],{},"文件名，任务名加版本号加扩展名，如 ",[205,1313,1314],{},"sh010-Main_v2.mp4",[365,1316,1317,1322],{},[380,1318,1319],{},[205,1320,1321],{},"film.size",[380,1323,1324],{},"字节数",[365,1326,1327,1332],{},[380,1328,1329],{},[205,1330,1331],{},"film.open()",[380,1333,1334],{},"返回读这个文件的流",[161,1336,1337,1339],{},[205,1338,864],{}," 返回后，临时文件就删掉了。",[161,1341,1342,1343,1346,1347,1350,1351,1355],{},"返回的 ",[205,1344,1345],{},"url"," 和 ",[205,1348,1349],{},"note"," 都可以不写。它们显示在任务详情的 ",[1352,1353,1354],"ui",{},"投递"," 里，每个版本、每个插件一行：投递中、重试中、已投递、失败。",[161,1357,1358,1360,1361,1364,1365,1368],{},[205,1359,864],{}," 抛错时，和事件一样按退避重试，最长一次等 1 小时，单次最长跑 1 小时。一天都没成功就记为失败。失败后点 ",[1352,1362,1363],{},"重新投递","，或调用 ",[205,1366,1367],{},"POST \u002Fapi\u002Fv1\u002Fjobs\u002F{id}\u002Fhandovers\u002F{plugin}\u002Fretry","，重新交给它。",[172,1370,1371],{"id":1371},"本地安装和调试",[161,1373,1374,1375,1377],{},"写插件时不用发布到 npm，在主机上直接装本地文件夹或 ",[205,1376,223],{},"：",[234,1379,1383],{"className":1380,"code":1381,"language":1382,"meta":239,"style":239},"language-bash shiki shiki-themes material-theme-palenight","# 装一个文件夹\ndanio plugin add .\u002Fmy-plugin\n\n# 或先打包\nnpm pack\ndanio plugin add .\u002Fmy-plugin-1.0.0.tgz\n","bash",[205,1384,1385,1391,1406,1410,1415,1423],{"__ignoreMap":239},[243,1386,1387],{"class":245,"line":246},[243,1388,1390],{"class":1389},"saEQR","# 装一个文件夹\n",[243,1392,1393,1396,1400,1403],{"class":245,"line":252},[243,1394,231],{"class":1395},"s5Dmg",[243,1397,1399],{"class":1398},"sfyAc"," plugin",[243,1401,1402],{"class":1398}," add",[243,1404,1405],{"class":1398}," .\u002Fmy-plugin\n",[243,1407,1408],{"class":245,"line":258},[243,1409,601],{"emptyLinePlaceholder":600},[243,1411,1412],{"class":245,"line":264},[243,1413,1414],{"class":1389},"# 或先打包\n",[243,1416,1417,1420],{"class":245,"line":270},[243,1418,1419],{"class":1395},"npm",[243,1421,1422],{"class":1398}," pack\n",[243,1424,1425,1427,1429,1431],{"class":245,"line":276},[243,1426,231],{"class":1395},[243,1428,1399],{"class":1398},[243,1430,1402],{"class":1398},[243,1432,1433],{"class":1398}," .\u002Fmy-plugin-1.0.0.tgz\n",[161,1435,1436,1439,1440,1443,1444,1447],{},[205,1437,1438],{},"danio plugin add"," 先显示插件要的权限，你输入 ",[205,1441,1442],{},"y"," 确认后才启用。加 ",[205,1445,1446],{},"--yes"," 跳过确认。",[161,1449,1450,1451,1454],{},"改了代码，运行 ",[205,1452,1453],{},"danio plugin update","，danio 从原来的来源重装所有插件，设置和权限保留。",[359,1456,1457,1467],{},[362,1458,1459],{},[365,1460,1461,1464],{},[368,1462,1463],{},"命令",[368,1465,1466],{},"做什么",[375,1468,1469,1479,1489,1499,1508],{},[365,1470,1471,1476],{},[380,1472,1473],{},[205,1474,1475],{},"danio plugin [--json]",[380,1477,1478],{},"列出插件：状态、版本、权限、订阅的事件、最近的错误",[365,1480,1481,1486],{},[380,1482,1483],{},[205,1484,1485],{},"danio plugin add \u003Cnpm 包 | 文件夹 | .tgz> [--yes]",[380,1487,1488],{},"安装，确认权限后启用",[365,1490,1491,1496],{},[380,1492,1493],{},[205,1494,1495],{},"danio plugin on|off|rm \u003C插件>",[380,1497,1498],{},"启用、停用、删除",[365,1500,1501,1505],{},[380,1502,1503],{},[205,1504,1453],{},[380,1506,1507],{},"从各自的来源重装全部插件",[365,1509,1510,1515],{},[380,1511,1512],{},[205,1513,1514],{},"danio plugin config \u003C插件> [键=值]…",[380,1516,1517,1518,1520],{},"改设置，不给值时逐项询问。",[205,1519,721],{}," 的项不能写在命令行里",[161,1522,1523,1346,1526,1529,1530,1533,1534,1346,1537,1540,1541,1544,1545,412,1548,412,1551,1554,1555,1557,1558,1561,1562,167],{},[205,1524,1525],{},"on",[205,1527,1528],{},"config"," 的 ",[205,1531,1532],{},"\u003C插件>"," 写 ID、显示名称或包名都行，",[205,1535,1536],{},"off",[205,1538,1539],{},"rm"," 只认 ID。ID 是包名去掉 ",[205,1542,1543],{},"@","，其他不是小写字母、数字、",[205,1546,1547],{},".",[205,1549,1550],{},"_",[205,1552,1553],{},"-"," 的字符换成 ",[205,1556,1553],{},"，如 ",[205,1559,1560],{},"@my-studio\u002Fdanio-chat-notify"," 的 ID 是 ",[205,1563,1564],{},"my-studio-danio-chat-notify",[161,1566,1567,1568,1571,1572,1575,1576,1578],{},"这些命令要在主机上运行。在界面的 ",[1352,1569,1570],{},"插件与集成"," 页，每个插件的菜单里有 ",[1352,1573,1574],{},"日志","，显示插件的日志，包括 ",[205,1577,779],{}," 的输出。",[507,1580,1582],{"id":1581},"bun-从哪来","Bun 从哪来",[161,1584,1585,1586,1589,1590,1593,1594,1597,1598,1601],{},"danio 按顺序找：环境变量 ",[205,1587,1588],{},"DANIO_BUN"," 指定的、PATH 里的、之前下载到数据目录 ",[205,1591,1592],{},"runtimes\u002Fbun\u002F"," 里的。都没有，就从 Bun 的 GitHub 发布页下载最新版，用它的 ",[205,1595,1596],{},"SHASUMS256.txt"," 核对后放进去。插件装在数据目录的 ",[205,1599,1600],{},"data\u002Fplugins\u002F\u003CID>\u002F"," 里。",[1603,1604,1605],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .saEQR, html code.shiki .saEQR{--shiki-default:#676E95;--shiki-default-font-style:italic}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}",{"title":239,"searchDepth":252,"depth":258,"links":1607},[1608,1609,1612,1613,1614,1617,1618],{"id":174,"depth":252,"text":174},{"id":217,"depth":252,"text":217,"children":1610},[1611],{"id":509,"depth":258,"text":509},{"id":571,"depth":252,"text":571},{"id":577,"depth":252,"text":577},{"id":812,"depth":252,"text":813,"children":1615},[1616],{"id":1171,"depth":258,"text":1171},{"id":1193,"depth":252,"text":1194},{"id":1371,"depth":252,"text":1371,"children":1619},[1620],{"id":1581,"depth":258,"text":1582},"怎么写 danio 插件：包的结构、权限和设置、ctx 里有什么、事件和成片投递两个钩子，以及怎么安装调试。","md",{},{"icon":129},{"title":152,"description":1621},"7ZRcfWAzac4g6kgSmxwdsvv0vVF6VOHauU2QynJVK0I",[1628,1630],{"title":147,"path":148,"stem":149,"description":1629,"icon":150,"children":-1},"danio 的 HTTP 接口：地址、令牌与权限、错误格式、事件流、OpenAPI 描述，以及常用接口。",null,1794619223479]