动作
动作是一段在叠加层中显示的 HTML、CSS 和 JavaScript。每个动作独立运行,不能联网,也拿不到任何密码:需要外部的东西时,它会向 Vovó 请求。
开始
“新建动作”可以新建空白动作,或从示例开始:提醒和聊天、正在播放、倒计时、目标、抽奖、死亡计数等。示例会变成你自己的副本,可以随意修改。
三步走
只用 HTML:一个在事件发生时显示的 <template>,事件字段写在花括号里。
<template data-on="follow" data-seconds="5">
<div class="alert">{user} 关注了频道!</div>
</template>
加一点 JavaScript:vovo.on("raid", (e) => { ... }) 和 vovo.show("name", e)。剩下的交给完整的 API。
触发器
动作只会收到编辑器中触发器列表里的事件。控制板按钮就是 button 触发器。
条件
触发器旁边的滑块图标会打开它的条件:只有全部满足时触发器才会触发。比如 50 人以上的突袭、以 !加入 开头的消息、指定的奖励。
定时器和快捷键
“定时器”触发器会定期触发:它的 every 条件表示间隔多少秒(不设置则每分钟一次)。“快捷键”触发器在按下组合键时触发,即它的 keys 条件,游戏在前台时也有效。
控件
在编辑器的“控件”卡片中,动作声明控制板在它运行时可以更改的内容:开或关、在选项中选择、计数器(可以设目标)或文字。每一项都保存在 vovo.state 中,键名就是对话框显示的那个,每次更改都会以 control 事件送达,无需触发器。“提醒和聊天”“关注目标”和“死亡计数”示例展示了用法。
vovo.on("control", () =>
{
document.getElementById("chat").hidden = !vovo.state.chat;
});
自定义
动作可以让使用者不打开代码就修改它:颜色、文字大小、字体、屏幕的哪一侧、自己的图片或声音。面板名称旁边、或动作列表中该行的调色板图标会打开“自定义”:每项设置一行,显示当前内容、与之不同时的默认值,以及恢复默认的办法。更改会立即出现在显示该动作的场景中,无需重启,每次更改都是一个版本,可以在操作记录中撤销。只能在这台电脑上选择:手机或其他 Vovó 都看不到。动作的包只带默认值,从不带你的选择;取代该动作的包会保留它仍有位置容纳的选择。
动作作者在编辑器的“自定义”卡片中声明它们,或写在 action.json 的 settings 下:每项都有一种类型(颜色、从最小到最大的数字、开或关、选项、文字、图片、声音或视频、字体)、一个名称、一个键和一个默认值。动作在 vovo.settings 中读取它们,某项改变时会收到 settings 事件;它的 CSS 以 var(--vovo-键名) 读取(数字带单位,比如 32px),开关和选项则作为页面的属性,供选择器使用:
.chat
{
color: var(--vovo-color);
font-size: var(--vovo-size);
font-family: var(--vovo-font), sans-serif;
}
html[data-vovo-side="Right"] .chat
{
right: 40px;
}
设置也可以是摄像头:使用动作的人在“自定义”中选择自己电脑上的一个摄像头,动作把它显示在自己放置的位置(vovo.camera.place,需要 camera.show 权限)。动作本身不带摄像头,永远不会知道选的是哪个,也看不到它的画面:vovo.settings 只用 true 或 false 说明是否选择了摄像头。“摄像头”示例就是一个带边框的摄像头。
面板
按钮装不下的东西(抽奖里的名字、带数字的目标、需要确认两次的操作)放进动作的面板:写在清单的 panel 中,显示在侧边栏的“面板”里(只要有动作带面板,窗口右边缘就会有它的图标)。面板是由 Vovó 绘制的一组部件:heading 和 note,按键名引用的 control,button(加上 confirm 会先问“确定?”),最多四个字段的 form,以及从 vovo.state 读取的 list(它的 key,以及其中的 path),每行最多三个按钮。
内容多的动作有两种拆分方式。{ "kind": "panel", "label": "抽奖", "icon": "gift" } 会开始另一个面板:在“面板”中有自己的条目,有自己的名称和图标(Vovó 的图标名称或一个表情符号;没有的话就用该动作第一个控制板按钮的图标)。它后面直到下一个面板的部件都属于它,第一个面板之前的部件会显示在每个面板顶部。在面板内部,tab 会开始一个分区(最多十二个):它后面直到下一个分区的部件只在选中时显示,第一个分区之前的部件在所有分区都显示。这样抽奖就是一个面板,一个分区用来抽奖,一个列出参与者,一个列出中奖者,而不是一长页。没有声明任何 { "kind": "panel" } 的动作只有一个面板,以动作命名。
“面板”的标题显示当前面板;点一下会列出全部,每个都会注明是否有打开的场景在显示它、属于谁,超过八个时还有搜索。分区以标签页形式显示在标题下方:放得下的直接显示,其余收在“更多”里。标题会开启一个分组:后面的部件共用一张卡片,相邻的按钮共用一行,列表单独一张卡片并显示数量,说明文字位于卡片之间。只有一个字段的表单直接在面板里填写,按 Enter 发送,文字控件也一样;字段更多的表单会打开小窗口。带目标(max)的计数器会显示进度。
列表可以有成千上万行。超过八行的列表有自己的搜索框,列表滚动时它一直可见。如果它是所在分区的最后一个部件,就会完整显示;后面还有部件时,只显示前八行和查看全部的入口,所以长列表放在最后,或单独放一个分区。
在面板上的操作会以 panel 事件送达,无需触发器:包括它的 name、表单的 values,以及某一行的 item 和 index。名称属于动作,无论由哪个面板显示。只有当某个打开的场景(OBS 中或预览窗口中)显示该动作时,动作才能收到面板操作:没有的话,面板会提示,按钮变淡等待,“打开预览”会打开场景的预览;控件和列表仍会显示,因为 Vovó 保存着它们。只有操控直播的人能用面板,在这里或在你允许使用面板的设备上,紧急期间不会有任何东西送达动作。
在聊天中发言
有 chat.send 权限时,vovo.chat.send("...") 会以你的身份在直播聊天中发言。Vovó 会控制节奏(30 秒内 20 行),两个显示该动作的页面请求同一句话时只发一次;按两次按钮就是两行。“聊天提醒”示例展示了用法。加上 { as: "persona" },这一行就改为通过角色自己的 Twitch 账号发出(集成,Twitch 下方):角色说的、不属于其回答的话,比如有人替它打出的一句。该账号未连接时,什么也不会写,也不会改用你的身份写。
Vovó 根据动作调用时正在处理的事件来区分不同页面的调用。稍后才发出的调用,例如在 await 之后或在定时器里,要用 { event: e } 说明它在回应哪个事件,e 是你的回调收到的对象:vovo.chat.send("欢迎!", { event: e })。vovo.emit、vovo.pad.play、vovo.webhook.send、vovo.draw、vovo.media.play 和 OBS 的调用也一样。否则两个页面会各发一次,管理员的链接也会在铃铛里等待。
麦克风、屏幕和你的动作
动作可以跟随你麦克风的音量(audio.level)、屏幕的颜色(screen.color),以及你在游戏中的移动(input.move):镜头转了多少、你往哪里走、有没有跳,来源包括鼠标、W、A、S、D 或方向键,以及手柄,这样场景就能随镜头晃动。“屏幕光晕”和“镜头摇摆”示例展示了用法。只有当打开的叠加层显示着等待这些数据的动作时,Vovó 才会读取,期间状态栏中间会显示对应图标,提示中列出相关动作。键盘只读取移动键,作为方向,从不记录按的是哪个键;Vovó 自己的窗口在前台时也不读取,因为你打字会让场景晃动;此时鼠标和手柄仍会让它移动,除非开启了“Vovó 在前台时暂停移动”(设置 › 常规)。声音从不离开电脑,离开的只有音量大小。
剪辑和视频
有 media.play 权限时,vovo.media.play(链接) 会在官方播放器中显示 Twitch 剪辑、Twitch 视频或 YouTube 视频,位置由 vovo.media.place(元素) 决定,带声音。只由操作者触发的动作会立即播放;其他动作可能是聊天里的人触发的,会在铃铛里等待,直到你(或有批准权限的设备)放行。播放器不知道剪辑何时结束:请传入 { seconds: 60 },或调用 vovo.media.stop()。“电视里的剪辑”示例接受管理员发送的 !clip 和链接。
管理员是你托付聊天的人:在“集成”的 Twitch 下,“管理员的链接立即播放”可以让他们中任何一位(或你自己)贴出的链接无需等待直接播出,前提是动作是因为他们的消息才显示这个链接的。默认关闭,需要你手动打开;其他人的链接仍然要等待。
语音通话
有 call 权限时,动作可以让你在直播中和 AI 模型通电话:你对着麦克风说,它大声回答,直播能听到双方。vovo.call.start({ instructions, voice }) 用你的 OpenAI 密钥(集成 › AI)开启通话;instructions 说明模型是谁、怎么说话。通话开启时是静音的,动作可以先让电话响铃;vovo.call.answer() 打开麦克风并让模型先开口,vovo.call.tell() 在通话中告诉它一些事,vovo.call.end() 挂断。call.started、call.answered、call.text(说的内容,转成文字)和 call.ended 会报告进展。
模型说的话会立即播出,事先没人看过,所以通话只能由你亲手操作发起:控制板按钮、快捷键、控件或动作的面板。聊天、奖励和 webhook 都不能发起。OpenAI 从接通起按秒计费,所以每次通话都有上限(最多五分钟)。通话期间,状态栏会显示一个红色电话:按它或紧急按钮即可立即挂断。请戴耳机,否则模型会从你的音箱里听到它自己。
Webhook
有 webhook 权限时,vovo.webhook.send("名称", { ... }) 会请 Vovó 把 JSON 发送到“设置 › Webhook”中设置的 webhook:Discord 频道的、自动化工具的、你网络中某个工具的。地址保存在这台电脑的保管库里,不进入备份和包,动作只知道名称。“在 Discord 上公告”示例展示了用法。
入站 webhook
反过来,互联网上的服务也可以通知 Vovó:在“设置 › Webhook”中创建一个入站 webhook,复制它的地址,粘贴到服务中(捐赠页面、商店、脚本)。每次调用都会以 webhook 事件送达动作,带有 hook(名称)和 data(发送的内容)。使用 webhook 触发器,并设置 hook 等于该名称的条件。每个 webhook 都有自己的地址:只交给调用它的服务。中继会为这台电脑把调用密封保存最多一天,所以 Vovó 关闭时发来的调用会在它打开时送达。送来的是别人的文字:动作只把它当作文字显示。
来自这台电脑上的程序
这台电脑上的程序可以把它知道的事告诉动作:读取游戏日志的脚本、记录比赛比分的工具、宏。动作在 action.json 的触发器中监听一个自己命名的事件(就像在 vovo dev 跟随的文件夹中那样):
"triggers": ["score"]
然后像其他事件一样接收:
vovo.on("score", (e) =>
{
document.getElementById("home").textContent = e.home;
document.getElementById("away").textContent = e.away;
});
程序用 vovo emit 发送,带上名称和字段:
vovo emit score home=2 away=1
数字、true 和 false 原样送达,其他内容都是文字。列表和对象以 JSON 整体发送,程序可以这样调用:
import json, subprocess
subprocess.run(["vovo", "emit", "score", "--fields", json.dumps({"home": 2, "away": 1})])
它会说明哪些动作收到了,如果没有任何动作会收到,就会失败(退出码 1,并说明原因):没有动作把这个名称列为触发器、条件不满足,或没有打开的页面显示它们。它的去向与动作自己的 vovo.emit 相同,紧急期间不发送,10 秒内最多 30 次,操作记录会注明是命令行触发的。仅限操作者的动作收不到它。程序发送的是那个程序的文字:动作只把它当作文字显示。若要假装 API 中的某个事件发生了,比如关注,请使用 vovo simulate。
预览和模拟
编辑器会在你输入时实时显示动作,“模拟”按钮无需等待真实事件即可触发触发器,并像控制板按钮那样移动控件。预览中的任何东西都不会进入直播。代码或预览角落的图标可以让它占满窗口;同一个图标或 Esc 会把它放回原位。
屏幕阅读器
屏幕阅读器无法读取代码编辑器。代码上方的“纯文本”按钮会在原处换成一个文本框,讲述人、NVDA 和 JAWS 可以完整朗读,按行、按词、按字符都可以。当 Windows 报告屏幕阅读器正在运行时,它默认开启;Tab 会跳到下一个控件,所以请用空格缩进。
文件
在编辑器的“文件”标签页中添加图片、声音、视频和字体。动作通过名称使用它们:<img src="logo.png">。若要让自己的声音或视频经过 Web Audio(压缩器、电平表),请在指定名称之前用 crossOrigin = "anonymous" 请求它:动作与一切隔离运行,对它来说连自己的文件也来自外部,否则 Web Audio 只会听到静音。
权限
动作可以向 Vovó 请求的内容在“行为”下:在聊天中发言、调用 webhook、显示共享屏幕、显示摄像头、在 OBS 中显示来源和开启滤镜。关闭后,动作就做不到。
在动作中控制 OBS
有 obs.toggle 权限时,vovo.obs.source("场景", "来源", true) 会在 OBS 场景中显示某个来源(false 隐藏;不写则切换),vovo.obs.filter("来源", "滤镜", true) 对滤镜做同样的事。名称以 OBS 中的为准,不区分大小写和重音符号;OBS 中没有的名称会在控制台中提示,并给出可能想找的那个。动作在 OBS 中能做的仅此而已:它不能停止推流,也读不到推流密钥。
仅限操作者
在“行为”下,“仅限操控直播的人”会让动作只能由控制板、快捷键、定时器和它的控件触发:观众、聊天、奖励、webhook 或其他动作的事件都不能触发。用于观众不该能做的事,比如清空聊天或切换场景。
版本
每次保存都会保留一个版本。“历史”可以恢复其中任意一个。
包
动作列表中的导出图标会把动作连同代码和文件写入一个 .zip,可以交给别人。在“行为”下填写的作者、网站和一句功能说明会一并带上,让导入的人知道它是什么、从哪来。“导入动作”会在保存之前显示包带来了什么、请求了什么,并在编辑器中打开动作:使用前请先读一读。如果包的名称和你已有的某个动作相同,它很可能是那个动作的新版本,所以会提议替换:动作保留它的场景、按钮和状态,原来的版本留在它的历史中。在终端中,vovo export 和 vovo import 也能做到,vovo import --replace 会替换同名的动作。 给别人用的包可以在“行为”下写一个 id:制作者的名称和动作的名称,中间用点隔开(example.raffle)。这样,即使你给动作改了名,它的下一个版本也能找到要更新的动作,而不同制作者的同名动作永远不会互相替换;vovo import --replace 也按它来找。不管有没有 id,导入时都会说明哪个版本替换哪个版本,并在文件是同一版本或更旧版本时指出;你在“自定义”里选的内容,只要新版本还有那项设置,就会保留。 包可以说明它按什么许可证交给别人:在“行为”下写上许可证的名称或你自己的一行话,再加上许可证全文——在那里从文件选取,或在文件夹里作为 LICENSE 放在 action.json 旁边。导入动作的人能看到这一行,并在动作导入之前从导入对话框里阅读全文。Vovó 只说明你写的内容,不约束任何人:不检查密钥、账号或购买记录,出售动作是在 Vovó 之外进行的。 旁边的复制图标会复制动作并在编辑器中打开副本,方便从能用的动作出发做一个变体:代码、文件和触发器会一起复制;状态、场景和按钮不会,快捷键触发器也不带按键组合,以免一个键同时触发两个。
用你自己的编辑器
动作可以用你喜欢的编辑器编写,也可以用你自己的构建流程:把它的各部分按包的结构放在一个文件夹里(action.json、index.html、style.css、script.js 和 files/),然后在终端中对这个文件夹运行 vovo dev。Vovó 会在编辑器中打开同名的动作,配置文件中没有时就从文件夹创建,并把文件夹中保存的每次修改立即显示在预览中;“模拟”照常可用。和在这里输入的修改一样,只有点“保存”后才会进入直播。vovo dev stop,或编辑器顶部的“停止跟随”,会放开这个文件夹。
vovo new <文件夹> 会替你建好这个文件夹,空白的或来自某个示例(vovo new --list 列出示例,--from goal 选用其中一个);vovo check <文件夹> 会一次说出文件夹或包里所有不对的地方,不创建也不打开任何东西:每个问题连同所在的文件,以及 Vovó 会接受却悄悄跳过的内容,比如它用不到的键。Vovó 关闭时这两个命令也能回答。vovo export <动作> <文件夹> 会把你已有的动作写成这样的文件夹。
vovo dev 还会在文件夹中写入 vovo.d.ts,即这个 Vovó 当前的 API;没有 jsconfig.json 时也会写一个,这样 VS Code 之类的编辑器就能补全 vovo.、vovo.on(" 中的事件名称以及每个事件的字段。两者都不属于动作,包也从不带上它们;在版本控制中保留或忽略都可以。如果想让错误的字段显示下划线,请在 jsconfig 的 compilerOptions 中添加 "checkJs": true。
它还会在那里写入 action.schema.json,它同样不属于动作:在 action.json 开头写上 "$schema": "./action.schema.json",编辑器就会补全它的键,按名称提示事件和权限,并给 Vovó 会拒绝的内容画下划线。
预览也可以在你的浏览器中打开,地址就是 vovo dev 打印出的那个,或者点预览角落的箭头:同样的草稿,由与直播中相同的 vovo 运行,每次修改都会重新加载,“模拟”也能作用于它,还能使用浏览器自带的开发者工具(控制台、断点、元素)。
在直播中显示
动作必须在某个场景中,场景必须在 OBS 中。