关于 background-state
background-state 是为 Even Hub 插件添加状态持久化的 Agent 技能。它识别后台切换时需要保留的可变状态,并插入 SDK 导出与恢复处理代码。
主要功能
- 01识别需要保留的状态
- 02选择合适的快照键
- 03添加 SDK 导出与恢复处理代码
- 04检查可序列化性和恢复回退
适用场景
- 01后台切换后保留计数器
- 02恢复所选项目和活动模式
- 03恢复缓冲区或历史记录
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ even-realities/ everything-evenhub --skill background-state安装后在 Claude Code 中输入/background-state确认已加载。
详细介绍
后台状态
通过分析 Even Hub 插件的代码,并插入来自 @evenrealities/even_hub_sdk 的 setBackgroundState + onBackgroundRestore 调用,为插件添加后台状态持久化。
背景
当手机进入后台时,Even Hub 宿主应用会采用无头 WebView 迁移策略:
inactive—— 宿主通过window.__getStateSnapshot()快照当前 JS 状态paused—— 宿主创建新的HeadlessInAppWebView,加载相同的插件 URL,并调用window.__restoreState(snapshot)重放已保存的状态- 后台 —— 无头 WebView 在不可见的情况下运行,继续向眼镜推送帧
resumed—— 快照会在前台 WebView 唤醒前注入其中,随后销毁无头 WebView
如果插件没有注册任何状态导出器,__getStateSnapshot() 会返回 '{}',无头 WebView 便会从头开始运行,导致每次手机从后台返回时,插件都会重置为初始状态。
使用时机
当插件符合以下情况时,使用此技能:
- 显示随时间递增的计数器、分数、计时器或序列号
- 跟踪选中的项目、当前页面或活动模式
- 维护不应重置的数据缓冲区或历史记录
- 有任何在插件执行期间会发生变化、且返回前台时必须保持一致的变量
API 参考
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
// Register state exporter — called by host before going to background
setBackgroundState('myKey', () => ({ ...myState }))
// Register state restorer — called by host after headless WebView loads
onBackgroundRestore('myKey', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
规则:
- 两次调用中的
key必须是相同的字符串 - 导出器必须返回可由 JSON 序列化的普通对象(不能包含类实例、函数、Map 或 Set)
- 恢复器接收
Record<string, unknown>—— 使用前务必进行类型转换 - 两次调用都必须在模块初始化时运行(顶层或启动函数内,且要在
bridge.onEvenHubEvent之前) - 在任何修改状态变量的代码之前调用
setBackgroundState,以确保初始快照始终有效
工作流
步骤 1 — 阅读源代码
如果 $ARGUMENTS 指向一个文件,则直接读取。如果它指向一个目录,则使用 glob 查找 *.ts 和 *.tsx 文件,并读取最相关的文件(通常是 main.ts、index.ts,或包含 waitForEvenAppBridge 的文件)。
步骤 2 — 确定适合快照的状态
查找符合以下模式的变量:
| 模式 | 示例 | 是否快照? |
|---|---|---|
| 基本类型的计数器或计时器 | let counter = 0 / frameIndex++ | ✅ 是 |
| 选中索引或活动模式 | let selectedIdx = 0 / let mode: 'A'|'B' = 'A' | ✅ 是 |
| 数据缓冲区或历史数组 | const history: string[] = [] | ✅ 是 |
| 启动时设置的配置 | let speed = 1,之后 speed = userChoice | ✅ 是 |
| 派生值/计算值 | const label = items[selectedIdx].name | ❌ 否——恢复后重新派生 |
| WebView 生命周期标志 | let isReady = false | ❌ 否——会重新初始化 |
| Bridge/控制器引用 | let bridge: EvenAppBridge | ❌ 否——不可序列化 |
| 临时 UI 标志 | let isAnimating = false | ❌ 否——属于临时状态 |
关键判断标准:如果变量在进入后台时丢失,会导致插件的行为与从未进入后台时不同,那么该变量就适合进行快照。
步骤 3 — 选择快照键
每个逻辑状态组使用一个键。避免每个变量使用一个键(过于细碎),也避免所有变量共用一个键(过于笼统)。单个插件通常需要 1–2 个键。
示例:
'counter'用于帧计数器'appState'用于主要可变状态对象'selection'用于当前选中的项目索引及相关数据
步骤 4 — 生成并插入代码
找到正确的插入位置——状态变量声明之后、bridge.onEvenHubEvent(...) 之前。插入:
// Background state persistence — survives host background/foreground migration
setBackgroundState('myKey', () => ({ /* snapshot of mutable state */ }))
onBackgroundRestore('myKey', (saved) => {
const s = saved as { /* type annotation */ }
// restore each field with nullish fallback
})
如果尚未添加,也请添加导入语句:
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
步骤 5 — 验证正确性
插入后,请确认:
- 已添加导入语句,且没有重复导入
- 两处调用使用相同的键字符串
- 导出器展开的是快照副本(
{ ...state },而不是实时引用) - 恢复器会重新赋值实时变量(而不只是读取
saved) - 导出对象中没有类实例、日期、映射或集合
- 恢复器中的每个字段都使用
??回退到其当前值
完整示例
之前(后台状态切换时会丢失所有状态):
import { waitForEvenAppBridge } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
之后(后台状态得以保留):
import { waitForEvenAppBridge, setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
// Background state persistence — survives host background/foreground migration
setBackgroundState('appState', () => ({ counter, lastEvent }))
onBackgroundRestore('appState', (saved) => {
const s = saved as { counter: number; lastEvent: string }
counter = s.counter ?? counter
lastEvent = s.lastEvent ?? lastEvent
})
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
常见错误
捕获引用,而不是创建快照
// ❌ Wrong — exporter returns the live object reference; value changes by the time host reads it
setBackgroundState('state', () => myState)
// ✅ Correct — spread creates a snapshot copy at the moment of export
setBackgroundState('state', () => ({ ...myState }))
恢复器读取数据却未重新赋值
// ❌ Wrong — local const, live variable unchanged
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
console.log(s.counter) // myState.counter still 0
})
// ✅ Correct — reassign the live variable
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
快照中包含不可序列化的值
// ❌ Wrong — Map and Date do not survive JSON round-trip
setBackgroundState('state', () => ({
items: new Map([['a', 1]]),
timestamp: new Date(),
}))
// ✅ Correct — convert to plain types
setBackgroundState('state', () => ({
items: Object.fromEntries(itemsMap),
timestamp: Date.now(),
}))
恢复器中缺少空值合并保护
// ❌ Wrong — crashes or resets to 0 if field missing in snapshot
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter
})
// ✅ Correct — keep current value as fallback when field absent
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter ?? counter
})
在条件语句或事件处理程序内部注册
// ❌ Wrong — if this code path isn't hit before background, state is not saved
bridge.onEvenHubEvent(event => {
if (event.sysEvent?.eventType === 4) {
setBackgroundState('state', () => ({ counter })) // too late
}
})
// ✅ Correct — always register at module init time
setBackgroundState('state', () => ({ counter }))
bridge.onEvenHubEvent(event => { /* ... */ })
任务
分析 $ARGUMENTS 中的插件代码。按照上表,找出所有值得快照保存的状态变量。然后在正确的位置插入 setBackgroundState + onBackgroundRestore。请汇总报告:
- 找到的状态变量及其值得快照保存的原因
- 跳过了哪些内容及原因
- 选择的键及插入的确切代码