- 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.
本文是 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 从零搭建,而是聚焦两个已经落地/正在落地的能力:
- 注册恢复(catch-up):当 iOS 设备重新注册 Live Activity 推送目标后,服务端在受控范围内补发当前运行中的任务快照,避免"注册前已经跑起来的任务在灵动岛上永远不出现"。
- 优先级滚动(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_token(push_+ 43 位)、apns_environment(development/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校验用户为active,canReceiveAppEvent再做事件级授权过滤——补发不会绕过消费者侧的权限检查与重放防护(源码注释明确:"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.json(config.appHome/config.json,见 app-config.ts 的APP_HOME/APP_CONFIG_FILE)。全局配置的读写走readAppConfig/writeAppConfig:读端带进程内缓存,写端使用safeFileStore.updateText实现锁定读-合并-写 + 备份({ backup: true }),并最终chmod 0o600收紧权限。数据库类设置(如live_activity_runs、live_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."
四、发布前验收清单与已知边界
文档列出的发布前置条件:
- 网关审批与部署:
relevance_score契约(含 400 校验、幂等/大小检查、APNs 透传)需先上线; - 补发授权测试:
catch-up的授权/权限/重放防护路径需完整覆盖; - 真机多活动验收:实际设备上多 activity 并存的端到端验收(对应测试 live-activity.test.ts 中"separate activities for separate sessions"等用例);
- 持久化 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(
updateLiveActivityDestination→catchUpLiveActivities(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_runs、live_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.
相关推荐
Ekko Studio 用户级移动推送注册与路由机制详解:从设备注册到 APNs 事件投递
Ekko Studio 用户级移动推送注册与路由机制详解:从设备注册到 APNs 事件投递 本文围绕 Ekko Studio 仓库中的核心设计文档 docs/r
AI 应用人工智能AI Agent本地部署前端后端工作流自动化Kong版本发布:新特性介绍与升级注意事项
Kong版本发布:新特性介绍与升级注意事项 你还在为API网关性能瓶颈、AI服务集成复杂而困扰吗?一文解决Kong 3.8.0 3.9.1版本升级难题 读完本文
API网关后端LLM 网关微服务人工智能FluentAssertions版本发布:新特性介绍与升级注意事项
FluentAssertions版本发布:新特性介绍与升级注意事项 概述 FluentAssertions作为.NET生态系统中广受欢迎的测试断言库,近期发布了
测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考