- 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.
导读
本文以 docs/notification-content-preview.md 为核心骨架,系统讲解 Hermes Studio(Ekko Studio 桌面与 Web 端)iOS APNs 推送通知中「内容预览」机制的完整设计:环境变量STUDIO_PUSH_CONTENT_PREVIEW的三态语义、基于事件信封显示字段的 40/160 字素(grapheme)截断与 Markdown 清理管线、以及精确锁定终端助手消息(terminal assistant message)的取文策略。读完本文,你将掌握该功能的部署配置方法、隐私取舍原则、底层实现调用链(run-push.ts、notification-preview.ts、chat-completion-text.ts)以及对应的验证测试(run-push-consumer.test.ts)。
一、功能定位:推送通知中的「内容预览」是什么
在 Hermes Studio 的移动端推送链路中,Studio 服务端在检测到聊天完成(chat.run.completed)、失败(chat.run.failed)、审批请求(chat.approval.requested)、澄清请求(chat.clarification.requested)等业务事件后,会通过 App Relay 网关向该用户名下的 iOS 设备(通过 APNs)发送推送。推送负载里除了一条用于点击回跳的ekko_run路由信息外,还有一个notification字段,携带title与body——这正是用户会在系统通知中心、锁屏上看到的内容。
在内容预览机制引入之前,网关侧默认按事件类型选择固定文案,会话标题与生成回复一律不出 Studio。而「内容预览」则允许在满足隐私配置的前提下,把当前会话标题和当前回合的 AI 回复摘要一并发送给网关与 APNs,让用户在锁屏上就能看到大致回复内容。相关背景与设备注册、事件路由的完整机制可进一步阅读 docs/run-push-registration.md。
关键设计原则(原文档核心主张)
- 沿用既有权限路由:接收者是「已认证的 Studio 登录用户」,推送内容预览不改变既有的用户/设备权限判定(
canReceiveAppEvent),预览只是附加在原有消费链路之上的内容增强。 - 只使用事件信封的显示字段:预览数据仅来源于
appEventEnvelope产出的display字段(单聊取content,群聊取preview),绝不使用 prompt、原始错误、工具调用或历史消息作为回退。 - 这不是敏感数据过滤器:格式化规则(截断、去 Markdown)只是让文本可读,并不具备分类、脱敏能力。可见的 AI 回复中仍可能包含私密内容,需要部署者在配置层面自行决策。
二、部署配置:STUDIO_PUSH_CONTENT_PREVIEW 三态语义
原文档给出了该环境变量的完整语义,以下是逐条展开与源码印证。
| 取值 | 行为 | 说明 |
|---|---|---|
| 未设置(unset) | 默认启用预览 | 推送请求携带真实 title/body |
1 | 显式启用预览 | 与默认行为一致,用于部署清单中显式声明 |
0 | 禁用预览 | 发送{ title: '', body: '' },由网关生成通用回退文案 |
在源码 run-push.ts 中,逻辑清晰对应:
// Preview is enabled by default; set 0 to keep notification content private. // Custom content requires a gateway that honors notification title/body. notification: process.env.STUDIO_PUSH_CONTENT_PREVIEW !== '0' ? notificationPreview(...) : { title: '', body: '' },几个必须注意的运维要点:
- 修改后必须重启 Studio:该环境变量在服务进程启动后读取,属于进程级配置,热更新不生效。
- 既有部署的「0」保持退出状态:已经显式配置
0的部署不会因升级而自动开启预览,避免存量隐私策略被悄然改变。 0模式仍走完整推送链路:禁用预览只是让notification为空对象,网关按事件类型显示固定文案(Android 等价文本),点击回跳、设备注册、权限校验等全部照常。1只是部署开关,不是用户级同意控制:原文档明确指出,per-user/per-device 的预览设置、同意/设置 UX 与完整隐私政策尚未实现,切勿把该环境变量当作面向终端用户的授权机制使用。
真实部署中的教训:LPK 遗漏环境变量
原文档记录了 2026-09-19 的真实设备验收结果:第一个 LPK(部署包,对应 PR3106+PR3111)遗漏了STUDIO_PUSH_CONTENT_PREVIEW=1,导致网关正确回退到通用文案;同时,一个未命名的结构化移动端会话需要安全的文本提取而非直接吐原始 JSON。后续补丁为结构化标题增加了回归测试,替换版 LPK 显式开启了经评审的预览模式。这条记录说明:该功能是部署级默认开启的,若你的部署策略要求内容不出站,必须在环境变量中显式写0,而不能依赖默认值之外的空想。
三、预览格式化的实现:display 字段 + 40/160 字素 + Markdown 清理
3.1 数据来源:appEventEnvelope 的 display 字段
预览的输入来自 app-events.ts 的appEventEnvelope(event)。它把内部业务事件转换为对外统一信封:
- 单聊完成事件:
display包含title(会话标题预览)与content(当前回复,来自chatCompletionText); - 群聊
group.message.created:display包含title与preview(群内最终助手回复摘要),以及 agent 列表; - 工作流/其他事件:
display直接透传event.payload.display。
推送消费者在组装负载时(run-push.ts),对单聊完成事件会把chatCompletionText(event)的结果合入content,其余情况直接取envelope.display——这正是原文档所说「Uses only the existing appEventEnvelope display fields (content for chat, preview for group)」。
3.2 格式化管线:notificationPreview
核心实现位于 notification-preview.ts:
return { title: plain(display.title, 40), body: completion ? plain(display.content || display.preview, 160) : '' }要点:
- 标题上限 40 字素,正文上限 160 字素(
completion类事件才有正文;审批/澄清类交互事件body为空); - 截断按grapheme(字素簇)计算而非 UTF-16 码元,使用
Intl.Segmenter切分,保证 emoji、组合字符等不会被拦腰截断;超限时以…收尾; - 基础 Markdown 清理,顺序如下:
- 移除围栏代码块(``` 与 ~~~,含未闭合情形);
- 移除图片语法
alt,链接语法text只保留可见文本; - 移除行首 0~3 个空格后的
#/>标题与引用标记; - 剥离
*、_、反引号、~等强调/行内代码标记; - 将控制字符与空白折叠为单个空格;
- JSON 安全提取:当输入以
[或{开头时尝试JSON.parse,若解析结果是内容块数组(content数组),则拼接其中type === 'text'的text字段;解析失败则原样返回。这正是为「未命名结构化移动端会话」提供的安全文本提取,避免把原始 JSON 直接推给 APNs。
3.3 源码注释明确的两条红线
- 无任意原始错误/命令回退:
notification-preview.ts头注释「Explicit display fields only. Never use prompt, raw error, tool or history fallback.」;对应测试run-push-consumer.test.ts中chat.run.failed携带error: 'SECRET'、审批事件携带command: 'SECRET'时,断言整个请求体不包含SECRET。 - 空当前输出绝不回退旧聊天文本:预览正文为空就发送空,宁可让网关显示通用文案,也不从会话历史里捞旧回复(详见下文第四节)。
四、终端消息选择:为什么 output 不可靠,以及共享选择器的修复
4.1 背景:两次真实设备验收暴露的问题
原文档记载了两次验收发现,直接推动了选择器演进:
- 第一次(2026-09-19):coding-agent 的
run.completed可能携带空output,但精确的助手message_id对应消息其实已持久化;同时未命名结构化会话需要安全文本提取。 - 第二次:非空
output也可能拼接了最终答案之前的中间回复(interim replies),导致 Android 与 iOS 都预览了第一条回复而非最终答案。
4.2 共享选择器:精确终端助手行优先
解决方案是共享取文函数 chat-completion-text.ts,其逻辑:
export function chatCompletionText(event: BusinessEvent): string { if (event.type !== 'chat.run.completed') return '' const sessionId = event.subject.session_id const messageId = Number(event.subject.message_id) const message = sessionId && Number.isSafeInteger(messageId) && messageId > 0 ? getSessionContextMessage(sessionId, messageId) : null if (message?.role === 'assistant') { for (const text of [message.display_content, message.content]) { if (typeof text === 'string' && text.trim()) return text } } return typeof event.payload.output === 'string' ? event.payload.output : '' }选择优先级明确:
- 优先读取完成事件
message_id定位到的精确持久化助手消息,先看display_content再看content; - 仅当该行没有可用助手文本时,才回退到
event.payload.output; - 绝不回退到最新一条或上一条助手消息——保留回合归属(turn attribution)与既有隐私边界(不把历史内容外泄);
- 绝不回退会话历史:即使精确行与 output 都为空,正文就是空字符串。
该函数被两个消费方共享(appEventEnvelope生成display.content、run-push消费者组装推送正文),所以 Android 事件流与 iOS APNs 走的是同一选择器,保证双端行为一致。此修复落在 Studio 的事件/推送消费端,只需更新 Studio,无需原生 App 重新构建。
4.3 测试验证
run-push-consumer.test.ts 中的关键用例:
'uses the exact persisted terminal message when the completion payload output is empty':output: ''+message_id: '42'→ body 为'Persisted current reply';'uses the terminal assistant message ahead of accumulated output on both Android and iOS':output是'First progress reply. '重复 30 次再拼接最终答案,断言两端信封与推送 body 都是'Persisted current reply',且session.preview中的旧回复不参与回退;'never falls back to session history on Android or iOS when the current reply is unavailable':输出为空时display.content与推送 body 均为空串;'defaults to current title and reply when the preview setting is absent':STUDIO_PUSH_CONTENT_PREVIEW未设置时,Markdown 加粗的**Current reply**被清理为纯文本'Current reply'。
五、隐私边界与合规提醒(务必阅读)
原文档以「Privacy change」单独强调,这是部署者必须向用户交代的变更:
- 内容出站:启用预览后,会话标题与有界(bounded)的当前回复摘要会经过推送网关与 APNs,可能按 iOS 锁屏设置显示在锁屏上;
- 这不是脱敏器:格式化规则无法识别、过滤敏感信息。可见 AI 回复仍可能包含私密材料,需要敏感数据合规的部署请使用
0; - 建议:对不得传输内容的部署,明确设置
STUDIO_PUSH_CONTENT_PREVIEW=0并纳入部署清单(LPK/环境配置),避免因默认开启而意外出站。
已知未完成需求(原文档如实列明)
- 网关契约待确认:标题/正文是否被真正采用、空字段回退行为、端到端负载大小强制,需网关侧确认;本仓库不拥有 APNs 最终序列化器。
- Android/群聊全管线检查未完成:Android 端与群聊事件的事件显示格式化仍需独立的全管线校验。
- 同意/设置 UX 未实现:per-user/per-device 预览设置与完整隐私政策尚不存在;部署级环境开关不是用户级同意控制。
- 真实设备验收未完成:iOS 与 Android 的内容/路由验收尚未全部通过。
- Live Activity 不在此范围:其投递、令牌与状态是独立系统,不属于本 PR。
- 原文档同时给出验证状态:加入消费者选择加入集成回归后,16 个聚焦测试通过,harness 与生产构建通过,空当前输出绝不回退旧聊天文本。
六、完整调用链速览与源码索引
业务事件 BusinessEvent(chat.run.completed / group.message.created / ...) └─ run-push.ts 的 createRunPushConsumer(event) ├─ 过滤:replayed/restored/background_snapshot/interrupted/queue_insertion 等不发送 ├─ appEventEnvelope(event) → display 字段(chat 用 content,group 用 preview) │ └─ chatCompletionText(event) → 精确终端助手行(display_content/content → payload.output) ├─ notificationPreview(display, isCompletion) → { title: ≤40 字素, body: ≤160 字素 } + Markdown 清理 + JSON 安全提取 ├─ 按设备组装 /push/v1/send 负载(含 ekko_run 点击回跳路由) └─ POST 到 App Relay 网关(official: config.appRelay.url;Cloudflare: https://cn.ekkostudio.xyz)延伸阅读索引:
- 功能设计文档:docs/notification-content-preview.md
- 推送注册与所有权背景:docs/run-push-registration.md
- 推送消费者实现:run-push.ts
- 预览格式化实现:notification-preview.ts
- 终端消息选择实现:chat-completion-text.ts
- 事件信封实现:app-events.ts
- 聚焦测试:run-push-consumer.test.ts
结语
Hermes Studio 的通知内容预览机制是一个「功能克制、边界清晰」的工程案例:通过单一环境变量三态控制部署级隐私策略,通过事件信封显示字段限定数据来源,通过字素级截断与 Markdown 清理保证可读性,通过精确终端消息选择器解决流式拼接带来的预览错位,并用一组聚焦测试(16 个用例)固化行为。部署者只需记住两件事:默认开启、0关闭且修改后重启;若合规要求内容不出站,请在 LPK 中显式写入STUDIO_PUSH_CONTENT_PREVIEW=0。
- 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.
相关推荐
Open Design:开源AI设计革命,如何用259+技能打造专业级设计原型
Open Design:开源AI设计革命,如何用259+技能打造专业级设计原型 Open Design是一款革命性的开源AI设计工具,它正在重新定义设计工作流程
AI 应用人工智能AI 技能设计系统媒体生成掌握Visual Studio Code通知系统:从消息提示到进度管理的完整指南
掌握Visual Studio Code通知系统:从消息提示到进度管理的完整指南 Visual Studio Code(VS Code)作为一款广受欢迎的代码编
开发工具代码编辑器5个技巧让Mac第三方鼠标超越苹果触控板:Mac Mouse Fix深度解析
5个技巧让Mac第三方鼠标超越苹果触控板:Mac Mouse Fix深度解析 还在为macOS上第三方鼠标的糟糕体验而烦恼吗?滚动生硬、侧键闲置、功能单一——这
桌面应用系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考