mcpField
B
background-state

background-state

github.com/even-realities开发79136 次安装1 周前更新
代码仓库

关于 background-state

background-state 是为 Even Hub 插件添加状态持久化的 Agent 技能。它识别后台切换时需要保留的可变状态,并插入 SDK 导出与恢复处理代码。

主要功能

  1. 01识别需要保留的状态
  2. 02选择合适的快照键
  3. 03添加 SDK 导出与恢复处理代码
  4. 04检查可序列化性和恢复回退

适用场景

  1. 01后台切换后保留计数器
  2. 02恢复所选项目和活动模式
  3. 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 迁移策略:

  1. inactive —— 宿主通过 window.__getStateSnapshot() 快照当前 JS 状态
  2. paused —— 宿主创建新的 HeadlessInAppWebView,加载相同的插件 URL,并调用 window.__restoreState(snapshot) 重放已保存的状态
  3. 后台 —— 无头 WebView 在不可见的情况下运行,继续向眼镜推送帧
  4. 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。请汇总报告:

  1. 找到的状态变量及其值得快照保存的原因
  2. 跳过了哪些内容及原因
  3. 选择的键及插入的确切代码

评论

支持 Markdown