Ekko Studio 苹果 Live Activity 注册恢复与优先级滚动发布:实现原理、配置开关与升级注意事项
2026/9/23 14:08:25 网站建设 项目流程
  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

本文是 Ekko Studio(本地优先的多 Agent 聊天、编码与可视化工作流工作台)中苹果 iOS Live Activity(灵动岛/锁屏实时活动)推送能力在「注册恢复(registration recovery / catch-up)」与「优先级滚动(priority rollout / relevance)」两个方向的实现与运维指南。内容以仓库 docs/live-activity/recovery-and-priority.md 为核心骨架,并结合服务端源码与测试用例展开。读完本文,你将掌握:Live Activity 注册补发机制的边界条件、防抖与限流策略、30 分钟启动预算抑制规则,以及liveActivityRelevanceEnabled配置开关的语义、网关契约与升级注意事项。

一、文档定位与核心结论速览

recovery-and-priority.md是一份高度凝练的设计决策与发布约束文档,它不讲解 Live Activity 从零搭建,而是聚焦两个已经落地/正在落地的能力:

  1. 注册恢复(catch-up):当 iOS 设备重新注册 Live Activity 推送目标后,服务端在受控范围内补发当前运行中的任务快照,避免"注册前已经跑起来的任务在灵动岛上永远不出现"。
  2. 优先级滚动(priority):通过给 APNs 推送附加relevance-score字段,让 iOS 在灵动岛/锁屏上对实时活动做排序提示;该能力默认开启,但允许通过持久化配置显式关闭。

文档给出的核心约束可以概括为 8 条(下文逐一展开):

  • 补发范围:仅限普通活跃聊天任务(含 coding agent),排除群聊(group)与工作流(workflow)绑定
  • 补发对象:先授权调用方,最多选择一个"最近更新的已授权任务",且只针对该任务注册的 connection 投递;
  • 快照要求:运行态快照必须匹配当前回合(turn)且不能处于 aborting 状态;
  • 状态机:已有活跃 activity 走更新,终态记录绝不因注册而被"复活"
  • 并发与节流:在途重复补发被合并(coalesced),注册触发的补发 2 分钟内节流,目的地在有记录的 start 后 30 分钟内抑制补发启动;
  • 全局影响:普通 live start 不受本变更的全局预算限制
  • 优先级开关:liveActivityRelevanceEnabled缺省或为true时启用;config.json中显式false关闭传输,且升级后保留;
  • 免责边界:以上防护不揭示也不重置 Apple 的预算,也不保证创建成功;relevance-score只是排序提示,不是显示顺序保证,更不增加推送预算。

二、注册恢复(Catch-up):受控补发而非无脑重放

2.1 触发点:注册即补发

恢复流程由 live-activity-registration.ts 的updateLiveActivityDestination驱动:当移动端完成一次合法的 Live Activity 目标注册(updateLiveActivityDestination(token, value))并落库后,代码会立即异步触发:

saveLiveActivityDestination({ ... }) void catchUpLiveActivities(connection.id).catch(() => { console.warn('[live-activity] catchup_failed') })

也就是说,每次成功的注册/重注册都会尝试一次补发。注册入口本身还会校验appearance(仅light/dark)、locale(白名单zh|zh-TW|en|ja|ko|fr|es|de|pt|ru|ar)、push_tokenpush_+ 43 位)、apns_environmentdevelopment/production)等字段,非法注册直接 400,从源头避免脏数据进入补发流程。

2.2 补发实现:单连接、单快照、立即中止

核心实现在 live-activity-catchup.ts 的catchUpLiveActivities(connectionId)

export async function catchUpLiveActivities(connectionId: number): Promise<void> { if (!deliver || pending.has(connectionId)) return if (Date.now() - (lastAttempt.get(connectionId) || 0) < 120_000) return const device = listLiveActivityDestinations().find(row => row.connection_id === connectionId && row.enabled) const user = device && findUserById(device.user_id) if (!user || user.status !== 'active') return lastAttempt.set(connectionId, Date.now()) ... const snapshots = getChatRunServer()?.getLiveActivityPlans?.() || [] for (const entry of snapshots) { const plan = taskPlanWebhookSnapshot(entry.snapshot) if (!plan || plan.execution_state !== 'running') continue const event: BusinessEvent = { ... } if (!canReceiveAppEvent(user, event)) continue await deliver(event, connectionId) break // At most one latest authorized task; do not burst push-to-start on reconnect. } }

结合文档约束可以还原完整语义:

  • 仅活跃普通聊天任务:只处理execution_state === 'running'的快照(running即未 aborting,满足"不处于 aborting"的约束);快照经taskPlanWebhookSnapshot转成chat.plan.updated业务事件,天然只覆盖 chat 类任务。
  • 授权先行findUserById校验用户为activecanReceiveAppEvent再做事件级授权过滤——补发不会绕过消费者侧的权限检查与重放防护(源码注释明确:"Snapshot events bypass neither consumer permission checks nor replay protection")。
  • 单任务投递:循环中break只投递第一个匹配的活跃快照,配合deliver(event, connectionId)只针对该连接,实现"至多选择一个最近更新的已授权任务"。
  • 不复活终态:投递目标deliver即主消费者consume(event, heartbeat=false, targetConnectionId),消费者内对targetConnectionId !== undefined的分支会执行:
    const stored = getLiveActivityRun(key) if (stored?.terminal) return // registration never revives a terminal activity

    已终态的 Live Activity 记录直接跳过,因此注册永远不会复活一张已经结束的卡片

  • 去重与串行:同一个 activity key 的投递经由serialized队列串行执行;latest/polling机制保证最新快照优先,重复的在途补发天然被合并。

2.3 防抖与预算抑制

文档中的三条时间约束都能在代码中找到对应实现:

约束实现位置语义
注册触发补发 2 分钟节流lastAttemptMap,< 120_000直接返回同一 connection 两次补发至少间隔 2 分钟
在途重复补发合并pendingSet同一 connection 的补发并发去重(coalesced)
记录 start 后 30 分钟抑制补发启动主消费者Date.now() - getLiveActivityLastStart(...) < 30 * 60_000见下文 2.4

lastAttempt还做了容量保护(超过 1000 个条目时淘汰最旧),防止长运行实例内存无限增长。

2.4 30 分钟启动抑制的真实含义

在 live-activity.ts 的补发分支(targetConnectionId !== undefined)中:

if (targetConnectionId !== undefined && Date.now() - getLiveActivityLastStart(device.destination_id) < 30 * 60_000) return await dispatch(event, device, registration, key, 'start')

getLiveActivityLastStart读取的是 live-activity-runtime-store.ts 中live_activity_start_budget表(destination_id主键 +last_start_at),该表在每次成功start时由recordLiveActivityStart(device.destination_id, Date.now())写入。

含义:若该 destination 在最近 30 分钟内已经成功启动过 Live Activity,则注册补发不会再次发起 start——因为卡片很可能仍然活跃在锁屏/灵动岛上,重复 start 只会白白消耗推送预算。注意这是"有记录的 start"(成功收到网关 2xx 后才记录),未记录的成功不构成抑制条件。同时,文档明确:正常 live start 不受本变更全局预算限制——该 30 分钟窗口只作用于注册补发路径,不影响任务自然流转中的 start/update/end。

2.5 消费者侧对补发的最终校验

即使补发事件进入主消费者,consume仍会二次确认:

if (targetConnectionId !== undefined) { if (!active(event)) return const stored = getLiveActivityRun(key) if (stored?.terminal) return }

其中active(event)要求runKind(event) === 'chat'(排除 group/workflow 绑定)且isLiveActivityRunActive(...) === true——运行态快照必须与当前回合匹配且未 aborting。这一层"消费者 ownership/connection 检查重复"正是文档所述"Consumer ownership/connection checks are repeated"的体现:设备连接有效性(validConnection:connection_id/user_id/device_code/token_hash 全匹配、未吊销、未过期)与运行态在补发时都会被重新验证。

三、优先级滚动(Priority / Relevance):默认开启的排名提示

3.1 配置开关语义

优先级传输由持久化配置liveActivityRelevanceEnabled控制,定义于 app-config.ts 的AppConfig接口:

export interface AppConfig { // Defaults on for supported gateways. Explicit false opts out for old gateways; persisted in appHome/config.json. liveActivityRelevanceEnabled?: boolean ... }

语义:

  • 缺省或true:启用优先级传输(default-on);
  • 显式false:禁用传输,并跨升级保留(persisted)。

该配置位于 Studio 持久化用户数据目录的config.jsonconfig.appHome/config.json,见 app-config.ts 的APP_HOME/APP_CONFIG_FILE)。全局配置的读写走readAppConfig/writeAppConfig:读端带进程内缓存,写端使用safeFileStore.updateText实现锁定读-合并-写 + 备份{ backup: true }),并最终chmod 0o600收紧权限。数据库类设置(如live_activity_runslive_activity_start_budget表)则落在持久化 appHome 下,与配置文件同源同目录。

3.2 默认开启的前提:网关必须支持 relevance_score

文档强调:默认开启要求网关支持顶层relevance_score字段;使用旧版/自托管网关的部署必须先在升级前显式设置false,否则可能出现网关不识别字段的情况。这是 default-on 策略对部署方的硬性前置条件。

3.3 网关契约(maintainer 已确认)

文档给出已部署提交43c870356b4aebb57a57b8e01f65b31d36e8fb79的网关契约要点:

  • relevance_score:可选的有界有限数字,范围0..9007199254740991(即Number.MAX_SAFE_INTEGER);
  • 生命周期:start/update/end三个动作都会携带/处理;
  • 持久化:网关持久化该值并透传给 APNs 的aps["relevance-score"]
  • 幂等与大小校验:relevance_score参与网关的幂等键与消息大小检查;
  • 非法值:返回400 invalid_relevance_score
  • 取值约定:Studio 使用业务 Unix 秒;心跳(heartbeat)与补发(catch-up)保留原始业务时间;end动作发送0

这些约定与源码一一对应(live-activity.ts):

if ((await readAppConfig()).liveActivityRelevanceEnabled !== false) { const businessAt = event.chat?.task_plan?.updated_at ?? Date.parse(event.occurred_at) const score = action === 'end' ? 0 : businessAt / 1000 if (Number.isFinite(score) && score >= 0 && score <= Number.MAX_SAFE_INTEGER) body.relevance_score = score }
  • 业务时间而非派发时间score = task_plan.updated_at / 1000(业务 Unix 秒);注释明确"Business event time, not dispatch/heartbeat time: heartbeats cannot steal priority"——心跳(2 分钟刷新)沿用原值,不会因为刷新而"偷"到更高的优先级。
  • end 归零action === 'end' ? 0
  • 范围防护Number.isFinite+0 <= score <= MAX_SAFE_INTEGER,与网关的 400 边界完全一致。
  • 显式 false 兼容旧网关!== false的判断意味着只有显式false才整体移除relevance_score字段。

测试 tests/server/live-activity.test.ts 覆盖了完整语义:

  • sends business priority by default when the setting is absent——配置缺省时 start 携带relevance_score = Date.now()/1000
  • preserves explicit opt-out for old gateways on start update and end——显式false时 start/update/end 三动作均无relevance_score字段;
  • gates priority and preserves business priority across heartbeat——心跳沿用task_plan.updated_at/1000,end 发送0,且 update/end 与 start 的值语义正确。

3.4 定位:排名提示而非保证

文档与代码都反复强调边界:relevance-score只是给 iOS 的排序提示(ranking hint),不保证显示顺序,也不代表 push-to-start 预算增加;注册恢复与优先级机制都不揭示也不重置 Apple 的 Live Activity 预算(live_activity_start_budget只是 Studio 自身的启动记录,而非 Apple 配额),更不保证卡片一定创建成功。文档明确:"No claim of immediate display or iOS forced ordering."

四、发布前验收清单与已知边界

文档列出的发布前置条件:

  1. 网关审批与部署relevance_score契约(含 400 校验、幂等/大小检查、APNs 透传)需先上线;
  2. 补发授权测试catch-up的授权/权限/重放防护路径需完整覆盖;
  3. 真机多活动验收:实际设备上多 activity 并存的端到端验收(对应测试 live-activity.test.ts 中"separate activities for separate sessions"等用例);
  4. 持久化 appHome 挂载与重启检查config.json与数据库在挂载/重启后保持一致。

已知边界(务必向用户/测试人员明确):

  • 前台重复注册仍可能返回 accepted,但Apple 预算耗尽时不创建卡片
  • 不做"立即显示"或"iOS 强制排序"的承诺;
  • 正常升级保留已配置的 user-data 挂载;卸载/清除数据或挂载连续性受损时不保留(配置随数据目录走,liveActivityRelevanceEnabled: false也不例外)。

五、配置实操示例

5.1 查看当前配置

# appHome 指向持久化用户数据目录(由 config.appHome 决定) cat "$APP_HOME/config.json"

5.2 显式关闭优先级传输(旧网关/自托管网关升级前)

编辑持久化用户数据目录下的config.json(路径为config.appHome/config.json,即 app-config.ts 中join(APP_HOME, 'config.json')),加入:

{ "liveActivityRelevanceEnabled": false }

保存后可通过writeAppConfig(读-合并-写 + 备份 +0o600权限)路径更新;该值显式保留,升级后不丢失。恢复默认行为只需删除该键或置为true

5.3 行为验证(对应测试用例)

期望行为测试用例(tests/server/live-activity.test.ts)
缺省即发优先级sends business priority by default when the setting is absent
显式关闭后三动作均不带字段preserves explicit opt-out for old gateways on start update and end
心跳不窃取优先级、end 归零gates priority and preserves business priority across heartbeat
注册不复活终态活动does not start cards for terminal-only or unknown-total work、补发分支stored?.terminal判断

六、关键源码速查

  • 注册入口与补发触发:live-activity-registration.ts(updateLiveActivityDestinationcatchUpLiveActivities(connection.id)
  • 补发实现:live-activity-catchup.ts(授权校验、单任务 break、lastAttempt/pending节流)
  • 主消费者与优先级传输:live-activity.ts(consume补发分支、30 分钟抑制、relevance_score计算、serialized串行与polling回执轮询)
  • 运行态/启动预算存储:live-activity-runtime-store.ts(live_activity_runslive_activity_start_budget表)
  • 全局配置:app-config.ts(liveActivityRelevanceEnabled、锁定读-合并-写与备份)
  • 测试覆盖:tests/server/live-activity.test.ts

以上实现共同保证:注册恢复永远在"授权 → 校验 → 去重 → 节流 → 预算抑制"的护栏内运行,优先级永远是可关闭的提示而非承诺——这正是本方案可以放心默认开启、安全滚动的设计根基。

  • AI 应用
  • 人工智能
  • AI Agent
  • 本地部署
  • 前端
  • 后端
  • 工作流自动化

【免费下载链接】ekko-studio

Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.

项目地址:https://gitcode.com/gh_mirrors/he/ekko-studio
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询