观看它的构建过程
影片 / 使用 SDK 构建自助服务终端应用
3:07

Claude Code 编写了一个大厅签到自助机,针对 window.screenrig:waitUntilActive()、ready()、kv.get() 和 emitConfirmed()。访客点击即可联系智能体,智能体在欢迎墙上回应。章节和文字记录 →
运行时已在应用中
app pack 和 app upload 接收已构建的静态目录,根目录为 index.html。打包器会注入 _screenrig/runtime.js 并将其脚本标签添加到该文档。运行中的应用无需构建步骤和 npm 依赖即可接收 window.screenrig:无需安装、无需导入、无需配置。
每个不可变的应用发布都在apps.screenrig.ai下独立的隔离域名上运行。应用 id 比发布周期更长,因此某个发布写入的 key/value 数据,下一个发布仍可使用。
window.screenrig 表面
三个只读属性、十个方法、一个包含另外五个方法的 kv 命名空间,以及一个用于显示器、人员到场、硬件按键、光线传感器、网络状态和触觉反馈的 device 命名空间。请先阅读 readyState,并在调用某次激活可能没有的方法前检查 capabilities。
每种方法回答不同的问题,请从您的需求出发:
| 要执行此操作 | 调用 |
|---|---|
| 等待播放器激活此页面 | screenrig.waitUntilActive() |
| 表示应用程序正在运行并显示中 | screenrig.ready() |
| 结束此应用在应用推进页面的轮次 | screenrig.nextPage() |
| 告知播放器此页面暂时没有内容可显示 | screenrig.skip(reason) |
| 告诉智能体发生了什么 | screenrig.emit(code) |
| 确认后端已记录该事件 | screenrig.emitConfirmed(code) |
| 为播放器和运行时留下诊断信息以收集 | screenrig.log(...) |
| 读取实时应用状态 | screenrig.kv.get(), screenrig.kv.list() |
| 写入实时应用状态 | screenrig.kv.set(), screenrig.kv.delete() |
| 实时应用状态变更时更新 | screenrig.kv.onChange() |
| 响应尺寸或功能变化 | screenrig.onResize(), screenrig.onCapabilities() |
| 调暗、熄灭或唤醒显示器 | screenrig.device.display.set() |
| 获知有人走近了屏幕 | screenrig.device.onPresence() |
| 接管硬件音量键 | screenrig.device.onKey() |
| 获知环境光线强度 | screenrig.device.onLight() |
| 获知设备已上线或离线 | screenrig.device.onNetwork() |
| 播放一次触觉轻震 | screenrig.device.vibrate() |
| 应用完成运行时后拆除监听器 | screenrig.destroy() |
screenrig.readyState#
注入的运行时是否附加。每个文档开始为 inert,当播放器握手和上下文到达时变为 active;尚未显示在屏幕上的页面保持 inert。等待 active 使用 screenrig.waitUntilActive();加载时不要将 inert 视为“屏幕外”。在 failed,或 window.screenrig 缺失时,渲染自己的内容并跳过下面的表面,因此相同构建也可在 screenRIG 外运行。处于 inert、ready()、log()、emit()、emitConfirmed()、nextPage()、skip() 以及 kv 读写时会抛出或拒绝,代码为 inert;kv.onChange 可随时注册。
screenrig.context#
此文档运行位置。
| 字段 | 类型 | 备注 |
|---|---|---|
application_id | string | 来自 app upload 的 app_ ID。键值存储属于它。 |
release_id | string | 服务此来源的不可变版本。 |
primitive_id | string | 本页当前填充的原始数据。 |
viewport | { width, height } | 播放器渲染的表面。 |
primitive | { width, height } | 该文档在视口内占据的矩形区域。 |
capabilities | object | 与 screenrig.capabilities 相同的布尔值。 |
screen_id | 字符串,可选 | 运行时已知哪个屏幕正在播放时出现。 |
context 是描述信息,不是请求内容。其中任何内容都不应签名、转发或返回给 screenRIG;下方方法会携带运行时所需内容。
启动参数不属于 context:播放列表可以为应用提供 params(参见iframe 地址和应用参数中的屏幕令牌),播放器会将每一项作为查询参数追加到页面地址中 ticket 之后。请从 location.search 读取,例如 new URLSearchParams(location.search).get("kiosk")。它们是提示而非凭据,任何能打开该页面的人都可以发送任意值,因此应视为不受信任的输入;要了解正在播放的是哪个屏幕,仍应使用 context.screen_id。Qt、Android、Apple 和 Windows 播放器从本地包加载应用,该包没有启动地址,也不向其传递参数。
screenrig.capabilities#
十个布尔值表示哪些受限方法会对该激活执行操作。键名带点,使用括号表示法读取。
| 标记 | 大门 |
|---|---|
page.advance | screenrig.nextPage() |
page.skip | screenrig.skip() |
kv.read | screenrig.kv.get(),screenrig.kv.list() 和 screenrig.kv.onChange() |
kv.write | screenrig.kv.set() 和 screenrig.kv.delete() |
device.display | screenrig.device.display.get(),.set() 和 screenrig.device.onDisplay() |
device.presence | screenrig.device.onPresence() |
device.keys | screenrig.device.onKey() |
device.light | screenrig.device.onLight() |
device.network | screenrig.device.onNetwork() |
device.haptics | screenrig.device.vibrate() |
if (window.screenrig.capabilities["kv.write"]) {
saveButton.hidden = false;
}使用 onCapabilities 订阅,而不是启动时一次性读取标志。
screenrig.waitUntilActive(timeoutMs)#
首先调用,优先于其他方法,并在等待时渲染 UI。当播放器激活此页面时解析,或运行时已 active 时立即解析,且不声明应用已渲染。timeoutMs(默认 10000,最大 300000)后若未解析则以 activation_timeout 拒绝;等待轮次的页面可继续等待。请在其解析后再开始播放声音,因为播放器会在页面显示之前加载应用:浏览器允许自动播放的屏幕会立即开始播放,拒绝自动播放的屏幕会使 play() 以 NotAllowedError 拒绝,因此请捕获该错误并静音播放。
const sr = window.screenrig;
async function start() {
render(); // the UI works with or without screenRIG
if (!sr) return;
for (;;) {
try { await sr.waitUntilActive(60000); break; }
catch (err) { if (err.code !== "activation_timeout") return; }
}
await sr.ready();
// From here: emit, emitConfirmed, log, kv, nextPage.
}
start();在事件处理程序、计时器或 catch 调用 SDK 之前检查 readyState === "active",并排队早于它到达的输入。在 inert 运行时的处理程序抛出 log(),这可能导致启动前未发送任何事件。
screenrig.ready()#
在 waitUntilActive() 解析后且依赖 context、kv 或 nextPage 之前调用,一旦应用具备上屏所需条件。调用时若 inert,则抛出异常。
await window.screenrig.waitUntilActive();
await window.screenrig.ready();screenrig.nextPage()#
仅在此原语为控制器的应用前进页面调用,当应用完成其轮次。一次调用对应一次激活。
window.screenrig.nextPage();该方法用于 advance.mode 为 application 的页面,此原语为控制器:
{
"advance": { "mode": "application", "max_ms": 60000 },
"primitives": [
{
"id": "app",
"primitive": "application",
"release_id": "rel_01EXAMPLERELEASE000000000",
"rect": { "x": 0, "y": 0, "width": 1920, "height": 1080 },
"layer": 0,
"content_fit": "fill",
"controller": true
}
]
}该页面上恰好有一个原语携带 controller: true,且必须是应用原语。页面上的 max_ms 推进对象边界。
screenrig.skip(reason)#
当应用程序暂时没有内容可显示时,请调用它来代替 ready()。屏幕会立即以页面的常规过渡切换到下一页,此次出现不计为播放。这是启动时的决定:请在激活后十秒内、在 ready() 之前调用。
await window.screenrig.waitUntilActive();
const events = await loadToday();
if (events.length === 0) {
await window.screenrig.skip("no events today");
} else {
render(events);
await window.screenrig.ready();
}页面的控制器,或该页面上唯一的应用程序,在没有图片或视频的页面上拥有此能力。没有该能力时方法以 capability_denied 拒绝;已调用 ready() 之后以 skip_after_ready 拒绝;播放器不支持时以 not_supported 拒绝。当播放器保留该页面时(操作员正在保持它、显示器已熄灭、没有其他页面可播放,或其他所有页面也都已跳过)以 forbidden 拒绝。此时请渲染您的后备内容并调用 ready()。全部跳过的图块播放列表会完整显示最后一个,而不是循环切换。可选的 reason(最多 64 个字符)会作为 player.application_skipped 出现在项目的播放器日志中。skip() 随 app pack 和 app upload 注入的运行时提供,因此请重新打包版本以获得该方法。
screenrig.emit(code)#
当智能体应知晓事件发生时使用:页面完成步骤、提交自助终端表单、应用确认运行。这是命名的应用事件,记录在项目事件日志中,智能体通过捆绑 CLI 的 events list 或 events follow 观看。仅代码,无消息体。
log 不是替代品。诊断不是应用事件,读取事件日志的智能体不会看到它。
window.screenrig.emit("menu.rendered");SDK 的 emit() 和 emitConfirmed() 会添加以屏幕为资源的 application.event 记录。使用 --type application.event 读取,可选择添加 --screen-id。返回的 details.primitive_id 和 details.code 标识发出事件的图元和命名操作。--app-id 选择应用资源、发布版本和 K/V 事件,因此不包含这些屏幕事件。
screenrig events list --type application.event --screen-id scr_SCREEN| 参数 | 类型 | 备注 |
|---|---|---|
code | string | 净化为 [A-Za-z0-9._-],最多 64 个字符。 |
screenrig.emitConfirmed(code)#
当应用必须确认事件已到达时使用:自助终端订单已下、表单已发送、访问已结束。发送与 emit 相同的命名应用事件,且仅当播放器确认后端接受事件时,承诺才解决。
事件被拒绝时返回后端问题,10 秒无回复后返回 ack_timeout。超时后勿自动重试:后端可能已接受事件。收据证明事件已入项目事件日志,但不代表已执行。
收据适用于运行应用的每个播放器。
await window.screenrig.emitConfirmed("order.placed");| 参数 | 类型 | 备注 |
|---|---|---|
code | string | 净化为 [A-Za-z0-9._-],最多 64 个字符。 |
screenrig.log({ level, code, message })#
用于诊断:debug、info、warn 和 error 是播放器和运行时在应用显示时收集的文本。注入的运行时日志通过此方法自身记录。日志限制:每客户端 32 行,且每条消息最多 2048 字节。记录调试时需要的信息,而非每帧一行。
当你想用 emit 时,不要使用 log。智能体不将日志行视为应用事件,智能体需要处理的内容应放在 emit 代码中。
window.screenrig.log({
level: "warn",
code: "menu.stale",
message: "Falling back to the cached menu."
});| 字段 | 类型 | 备注 |
|---|---|---|
level | string | debug,info,warn,或 error。 |
code | string | 净化为 [A-Za-z0-9._-],最多 64 个字符。 |
message | string | 最多 2048 字节。 |
screenrig.onResize(handler)#
当布局可能变化时订阅,而非只读取一次 context。处理程序接收 { viewport, primitive },与 context 传递的两个对象相同,调用返回结束订阅的函数。
const stopResize = window.screenrig.onResize(function (size) {
render(size.viewport, size.primitive);
});
stopResize();screenrig.onCapabilities(handler)#
当授予的功能可能变化时订阅,而非启动时一次决定。在处理程序内读取 screenrig.capabilities 当前布尔值,启用或隐藏相关控件;调用返回结束订阅的函数。
const stopCapabilities = window.screenrig.onCapabilities(function () {
saveButton.hidden = !window.screenrig.capabilities["kv.write"];
});screenrig.destroy()#
应用完成运行时拆除监听器。 运行完整周期并允许播放器替换文档的应用无需调用。
screenrig.kv#
应用的键值存储,用于存活超过一次激活的实时应用状态。五种自身方法:get、list、set、delete 和 onChange。详见下文 键值存储。
键值存储
使用存储获取实时应用状态:应用渲染的菜单、操作员今晨更改的文案、上次激活留下的计数器。用 kv.get 和 kv.list 读取,用 kv.set 和 kv.delete 写入,用 kv.onChange 监听所有更改。
商店属于 context.application_id 中的应用 ID,而非发布,因此条目可在下一次 app upload 后存活。值为字节,文本、JSON 和小型二进制均可往返。读取需 kv.read,写入需 kv.write;调用前请检查标志。
商店及其变更推送运行于所有运行应用的播放器:播放器 Web 应用(PWA)、Android、Amazon Signage Stick、macOS、Windows、Linux、Raspberry Pi、LG webOS Signage、BrightSign 和 Samsung Smart Signage(Tizen)。Apple TV、iPhone 和 iPad 不运行应用;参见 平台的原语支持。
screenrig.kv.get(key)#
读取实时应用状态。 等待 Promise。
const entry = await window.screenrig.kv.get("menu");screenrig.kv.list()#
在尚未知晓密钥时读取实时应用状态: 列出应用已存储的密钥。等待 Promise。
const keys = await window.screenrig.kv.list();screenrig.kv.set(key, value, options)#
写入实时应用状态。 value 是字符串或 Uint8Array;options 可选。
await window.screenrig.kv.set("menu", JSON.stringify(menu), {
contentType: "application/json"
});
await window.screenrig.kv.set("badge.png", pngBytes, {
contentType: "image/png"
});| 参数 | 类型 | 必填 | 备注 |
|---|---|---|---|
key | string | 是 | 参见 键。 |
value | 字符串或 Uint8Array | 是 | 待存储的字节。 |
options.contentType | string | 否 | 默认字符串为 text/plain,Uint8Array 的默认值为 application/octet-stream。 |
options.expectedRevision | 非负整数 | 否 | 仅当条目仍处于此版本时写入。 |
options.idempotencyKey | string | 否 | 重试相同写入操作,避免重复应用。 |
screenrig.kv.delete(key, expectedRevision, options)#
写入实时应用状态 通过删除条目。expectedRevision 可选:省略以删除当前条目,或提供以拒绝并发更改。
await window.screenrig.kv.delete("menu");| 参数 | 类型 | 必填 | 备注 |
|---|---|---|---|
key | string | 是 | 参见 键。 |
expectedRevision | 非负整数 | 否 | 你读取时条目的修订版本。 |
options.idempotencyKey | string | 否 | 重试相同删除操作,避免重复应用。 |
screenrig.kv.onChange(handler)#
在实时应用状态变化时更新,而非轮询。处理程序在此应用的任何键变化时运行,无论是谁写的:带有 kv set 的智能体、通过 键值 webhook 的外部系统,或任何屏幕上的此应用。它接收哪个键改变,而非值,因此用 kv.get 读取新值。调用返回结束订阅的函数。
const sr = window.screenrig;
async function showMenu() {
render(await sr.kv.get("menu"));
}
const stopChanges = sr.kv.onChange(function (change) {
if (change.key === "menu" && !change.deleted) showMenu();
});
await sr.waitUntilActive();
await sr.ready();
await showMenu();在首次读取前订阅,以免错过读取时发生的更改;激活前注册的处理程序在页面激活后开始运行。调用 stopChanges() 结束订阅。
更改在几秒内传达到所有播放该应用的屏幕。对同一键的多次快速写入可能合并为携带最新修订的单次更改,因此将更改视为“重新读取此键”,而非写入计数。更改是信号,不是历史:应用启动时读取所需内容,然后跟踪更改。
| 字段 | 类型 | 备注 |
|---|---|---|
key | string | 已更改的键。 |
revision | 整数,可选 | 条目的新修订版。当 deleted 是 true 时缺失。 |
deleted | boolean | true 在密钥被删除时。 |
键列表
密钥为 1 到 256 个 UTF-8 字节。限制为字节数,不是字符数,因此含多字节字符的密钥比长度显示的更快达到限制。密钥不得以 _screenrig/ 开头;该前缀属于注入的运行时。
修订和幂等性
每个条目都有一个修订版本。可选地传递expectedRevision到screenrig.kv.set或screenrig.kv.delete,仅当条目仍处于你读取的修订版本时才写入。过期的修订版本意味着其他人先写入:请重新读取条目,重新应用更改,然后重试。
idempotencyKey 负责另一半。对于在响应丢失后可能重试的写入操作,每次尝试都可以携带相同的密钥,商店只会应用一次。
来自 CLI 的相同商店
代理可以通过捆绑 CLI 的 kv 命令,针对 app upload 中的 app_ ID,从浏览器外部访问这些条目。这是运营者对商店的视图。它不能替代上述五种方法:应用程序通过 screenrig.kv 在浏览器中、屏幕上运行时更改自己的数据。代理写入的更改通过 kv.onChange 传达给运行中的应用程序。
从其他系统推送数据
键值 webhook 是一个 URL,写入此应用的一个键。交给任何能发送 HTTP POST 的工具,如 销售点、电子表格脚本、定时任务或 curl,每次 POST 都替换该键的值。运行该应用的每个屏幕通过 kv.onChange 在几秒内获知,无需新版本发布或智能体介入。
代理使用捆绑 CLI 为密钥创建一个,并提供其 URL 和密钥:
screenrig kv webhook create menu --app-id app_01EXAMPLEAPP00000000000 --description "Point of sale menu feed"响应携带 webhook 的 url 和 secret(srkw_…)。密钥仅在创建 webhook 和密钥轮换时显示,请立即在发送系统中输入。URL 非密钥,密钥才是。
发送方将新值作为原始请求主体发布,秘密放在 Authorization 头中:
curl https://api.screenrig.ai/api/applications/app_01EXAMPLEAPP00000000000/kv-webhooks/kvw_01EXAMPLEWEBHOOK00000/write \
-H "Authorization: Bearer $KV_WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
--data '{"soup":"lentil","price":"6.50"}'仅使用 HTTP Basic 认证的发送方使用 webhook id 作为用户名,secret 作为密码:
curl https://api.screenrig.ai/api/applications/app_01EXAMPLEAPP00000000000/kv-webhooks/kvw_01EXAMPLEWEBHOOK00000/write \
--user "kvw_01EXAMPLEWEBHOOK00000:$KV_WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
--data '{"soup":"lentil","price":"6.50"}'- 主体最大 1 MiB,成为值,
Content-Type为其内容类型(缺头时为application/octet-stream)。值传递至浏览器应用,切勿作为凭证发布。 - 每次发布都是一次类似
kv set的键值写入:会替换值、提升版本号,并像其他写入一样计入存储用量。 - 响应为密钥的新元数据(
key、revision、bytes、sha256、content_type和updated_at),不包含其值。 - 添加一个
Idempotency-Key头(8 到 200 个字符)以安全重试 POST:24 小时内相同密钥只应用一次,且相同密钥但不同主体或内容类型的请求以idempotency_mismatch拒绝。无此头时,每个 POST 都是新写入。 - 一个 webhook 每分钟最多接收 120 次 POST;超过则返回
429和Retry-After。 - 缺失或错误的密钥,或未知的 webhook,响应
401。URL 或查询字符串中的密钥永不被接受。 - 应用最多有 20 个 webhook。
kv webhook rotate-secret颁发新密钥,旧密钥立即停止;kv webhook delete立即停止 URL,但保留密钥值。
卡片同样支持 webhook:每次发送都会生成其数据文档,可原样使用,也可通过映射生成(卡片 webhook)。
设备
运行壁挂面板或自助终端的应用程序可以控制自己所在的屏幕。device 标志会交给控制其页面的框架(页面的控制器,或页面上唯一的应用程序),前提是播放器能在该设备上完成这项工作:Android 播放器授予 device.display、device.keys 和 device.network,在设备有摄像头、且设置设备的人允许播放器使用摄像头时授予 device.presence(Android 设置、应用、screenRIG Player、权限、相机),在设备有光线传感器时授予 device.light,在设备有振动马达时授予 device.haptics。无法控制设备的播放器会让每个 device 标志都为 false。display.get()、display.set() 和 vibrate() 在不提供它们的播放器上以 not_supported 拒绝,没有该标志时以 capability_denied 拒绝。onDisplay、onPresence、onKey、onLight 和 onNetwork 可随时注册,并且与 kv.onChange 一样,没有该标志时永远不会运行。
screenrig.device.display.set(state)#
调暗、熄灭或唤醒显示器。brightness 取值 0 到 1(0 是背光能达到的最低亮度,并非关闭);on: false 会让屏幕变暗,on: true 会让它重新亮起。省略某个成员则保持其原状。屏幕变暗时,触摸仍会传到应用程序,因此一次触摸即可将其唤醒。让屏幕保持熄灭的显示计划优先于应用程序,页面离开屏幕时播放器会恢复显示。
await window.screenrig.device.display.set({ brightness: 0.3 });
await window.screenrig.device.display.set({ on: false });screenrig.device.display.get()#
当前显示器状态,格式为 { on, brightness }。
screenrig.device.onDisplay(handler)#
每当显示器发生变化时,以 { on, brightness } 调用,无论是此应用程序、显示计划还是操作员所做的更改。
screenrig.device.onPresence(handler)#
当设备检测到有人在屏幕前移动时调用,每秒最多一次。播放器读取设备上的摄像头,不保留任何图像:不存储、不记录、也不发送任何图像。只有在注册了处理程序时,摄像头才会运行。
window.screenrig.device.onPresence(function () {
window.screenrig.device.display.set({ on: true, brightness: 1 });
});screenrig.device.onKey(handler)#
每次按下硬件音量键(包括重复按键)时,以 "volume_up" 或 "volume_down" 调用。注册处理程序期间,按键归应用程序所有,设备音量保持不变。
screenrig.device.onLight(handler)#
以环境光照度(单位 lux)调用:先是处理程序注册后的第一个读数,之后每当光线变化至少 10% 或跨越 1 lux 时再调用,每秒最多一次。设备需要具备光线传感器。
window.screenrig.device.onLight(function (lux) {
document.body.classList.toggle("dim-room", lux < 10);
});screenrig.device.onNetwork(handler)#
每当设备获得或失去系统已验证的互联网连接时,以 true 或 false 调用;注册第一个处理程序时,会立即以当前状态调用。
window.screenrig.device.onNetwork(function (online) {
offlineBadge.hidden = online;
});screenrig.device.vibrate(ms)#
播放一次触觉轻震。 ms 是 1 到 200 之间的整数毫秒数,默认为 12。播放器开始振动后,Promise 以无值兑现;对于任何其他 ms,则以 invalid_request 拒绝,且不发送任何内容。
await window.screenrig.device.vibrate();
await window.screenrig.device.vibrate(40);访问屏幕所在网络中的设备
应用程序除了 https:// 地址外,还可以从 http:// 和 ws:// 地址获取数据,因此可以与屏幕所在局域网中的设备或服务器通信,例如位于 http://192.168.1.20 的控制器。Android、Linux 和 Raspberry Pi、Windows 以及 macOS 的播放器均允许这样做。在运行播放器 Web 应用的自助终端浏览器上,由浏览器的所有者通过企业策略允许:参见本地网络上的应用程序。您调用的服务器需针对应用程序的源响应 CORS;在 macOS 上,应用程序的请求携带 Origin: null,因此服务器的 CORS 需要在那里允许该源。
浏览器引擎会在安全页面上阻止 <img src="http://…">,因此请使用 fetch() 和 URL.createObjectURL() 加载来自 http:// 地址的图片。
const response = await fetch("http://192.168.1.20/snapshot.jpg");
image.src = URL.createObjectURL(await response.blob());