首页
/ claude-mem 的 Toast 通知服务:把 Observation 保存事件接入 macOS 原生通知的完整实现路线

claude-mem 的 Toast 通知服务:把 Observation 保存事件接入 macOS 原生通知的完整实现路线

2026-09-04 17:29:37作者:宣海椒Queenly

本篇基于仓库内的 Maestro 实施 playbook Phase-01-Toast-Notification-Service.md 与配套测试 playbook Phase-02-Toast-Notification-Tests.md,完整还原 claude-mem 中“Observation 保存后弹出 macOS 原生通知”这一功能的实现路线:依赖安装、配置开关设计、ToastNotifier 功能模块的规范、在 observation 广播管线中的挂接方式,以及六个单元测试用例的覆盖策略。读完本文,你可以在任何遵循 claude-mem 架构的分支中复现一套“配置受控、平台隔离、永不阻塞主流程”的本地通知模块。

一、功能定位:为什么把通知挂在 observation 保存管线上

claude-mem 的核心工作流是:Agent 会话结束后,worker 解析 transcript、生成 observation(结构化记忆条目)、写入 SQLite,并通过 SSE 把新 observation 广播给前端 viewer。playbook 的目标非常明确——让 observation 落库这个“值得打扰用户”的事件,转化为一条 macOS 原生通知

When saving an observation with CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED=true in ~/.claude-mem/settings.json, a native macOS notification shows the observation's title and subtitle.

这一设计决定了整个实现的四条主线,playbook 的每个 Task 一一对应:

主线 对应 Task 关键产出
依赖引入 安装 node-notifier node-notifier@10.0.1(dependencies)、@types/node-notifier@8.0.5(devDependencies)
配置开关 扩展 SettingsDefaultsManager 新增 CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED,默认 'false'
通知模块 创建 ToastNotifier.ts 单一导出函数 sendObservationToast(title, subtitle)
管线挂接 修改 ResponseProcessor.ts syncAndBroadcastObservations() 循环中调用 toast

playbook 采用“每个 Task 附 Completed: ... 结论”的格式记录执行结果,最后一项确认构建通过:

Completed: Build succeeded, binary compiled (60.5 MB), marketplace synced, worker restarted and healthy

二、配置开关:CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED 的落点

playbook 要求把新开关加入 SettingsDefaultsManager.ts,具体位置有三个:

  1. SettingsDefaults 接口中,放在“Feature Toggles”区段、紧邻 CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED 之后;
  2. DEFAULTS 对象同区段,取值 'false'(所有设置值在 claude-mem 中统一以字符串存储);
  3. 附注释 // macOS toast notifications for saved observations
  4. 同步加入 SettingsRoutessettingKeys 白名单与布尔校验列表,保证 worker 的设置路由能识别并校验它。

对照当前仓库的 SettingsDefaultsManager.ts 可以印证这个“区段”的组织方式:CLAUDE_MEM_WELCOME_HINT_ENABLED(第 170 行)与 CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false'(第 171 行)正是 Feature Toggles 区段的相邻成员,CLAUDE_MEM_TRANSCRIPTS_ENABLED 紧随其后——toast 开关的插入点就在这组开关之间。

理解这个开关为什么能真正生效,需要理解 SettingsDefaultsManager 的三层解析机制(当前源码中全部可见):

  • 文件缺失时自动播种loadFromFile(settingsPath) 发现文件不存在时,会把完整 DEFAULTS 通过 writeJsonFileAtomic 原子写入磁盘(见 SettingsDefaultsManager.tsloadFromFile 的首个分支)。注释特别强调“a fresh settings.json is seeded with EVERY default ... and persisted values then win over DEFAULTS”——这正是 toast 开关需要注册进 DEFAULTS 的原因:未注册的键在播种时不会落盘,用户之后手动写入也会因不在白名单而被丢弃
  • 持久化值优先:加载时以 { ...this.DEFAULTS } 为底,逐键用文件中的值覆盖(if (flatSettings[key] !== undefined)),即用户写的 "CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED": "true" 会覆盖默认 'false'
  • 环境变量最高优先applyEnvOverrides() 遍历 DEFAULTS 的全部键,凡 process.env[key] 有定义即覆盖——所以 CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED=true 既可以写在 ~/.claude-mem/settings.json,也可以直接以环境变量注入,后者优先级更高。

设置文件路径由 paths.ts 统一导出:export const USER_SETTINGS_PATH = join(DATA_DIR, 'settings.json')(第 48 行),即 ~/.claude-mem/settings.json。启用该功能的最小配置为:

{
  "CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED": "true"
}

一个值得注意的事实:当前仓库快照中 SettingsDefaults 接口与 DEFAULTS 对象里已没有 CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED 这一键,package.jsondependencies 中也只有 better-auth 系列,没有 node-notifier。从源码结构看,playbook 所记录的这一功能在当前主线上已被移除或从未并入——本文将其作为“完整的实施档案 + 可复现的参考设计”来讲读,这也是仓库保留 .maestro/playbooks/ 的价值所在。

三、ToastNotifier:按 ObservationBroadcaster 范式编写的功能模块

playbook 对 src/services/worker/agents/ToastNotifier.ts 的规格描述极为具体,核心约束是:仿照 ObservationBroadcaster.ts 的“功能模块、非 class”模式,只导出一个函数

先看模板本体。ObservationBroadcaster.ts 只有两个纯函数 broadcastObservation / broadcastSummary,结构是:空值守卫(if (!worker?.sseBroadcaster) return)→ 业务过滤(shouldEmitProjectRow,内部项目不发 SSE)→ 执行动作。没有状态、没有类实例,天然适合被 ResponseProcessor 这种流程文件以 fire-and-forget 方式调用。

按 playbook 的五条行为规格,sendObservationToast 的参考实现如下(依据 playbook 描述重构,非当前仓库现存代码):

import notifier from 'node-notifier';
import { logger } from '../../../utils/logger.js';
import { SettingsDefaultsManager } from '../../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../../shared/paths.js';

/**
 * ToastNotifier — native macOS notifications for saved observations.
 * Functional module (not a class), mirrors ObservationBroadcaster.ts.
 * Fire-and-forget: never throws, must not crash the observation pipeline.
 */
export function sendObservationToast(
  title: string | null,
  subtitle: string | null
): void {
  try {
    // 1. Platform guard — macOS only
    if (process.platform !== 'darwin') {
      return;
    }

    // 2. Load settings from ~/.claude-mem/settings.json
    const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);

    // 3. Toggle check — handle both string 'true' and boolean true
    const enabled = settings.CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED;
    if (enabled !== 'true' && enabled !== true) {
      return;
    }

    // 4. Native notification (no sound)
    notifier.notify({
      title: title || 'Observation saved',
      message: subtitle || '',
      sound: false
    });
  } catch (error) {
    // 5. Log but NEVER throw
    logger.warn('TOAST', 'failed to send observation toast', {
      error: error instanceof Error ? error.message : String(error)
    });
  }
}

五条规格各自解决一个真实风险,逐条拆解:

  • 平台守卫(process.platform !== 'darwin' 立即返回)node-notifier 在无 GUI 或 Linux/Windows 环境下 notify() 可能回调 error 或产生无意义的终端提示;在入口直接短路,保证非 macOS 机器上零开销、零日志噪音。
  • 设置加载走 loadFromFile 而非环境变量快捷路径:与 worker 其余模块一致,以 USER_SETTINGS_PATH 为唯一配置事实源(环境变量仍可通过 applyEnvOverrides 间接生效)。
  • 布尔双形态兼容:settings.json 中该键以字符串 'true' 存储(SettingsDefaults 接口声明为 string),但用户可能手写成 JSON 布尔 trueenabled !== 'true' && enabled !== true 同时放行两种写法。
  • 兜底文案title || 'Observation saved'message: subtitle || '',保证 observation 的 title/subtitle 任一为 null 时通知仍合法(与 Phase-02 测试用例 4、5 一一对应)。
  • 整体 try/catch + logger.warn('TOAST', ...):这是整个模块最重要的契约——永不抛出。toast 属于“锦上添花”的旁路功能,一次通知失败绝不能拖垮 observation 写入与 SSE 广播主链路。这与 playbook 中“follows the same fire-and-forget pattern as the Chroma sync and folder CLAUDE.md updates”的表述一致。

日志约定也值得注意:logger.warn('TOAST', ...) 使用 claude-mem 统一的 logger(level, source, message, meta?) 格式(logger.ts),TOAST 作为独立 source 便于用 npm run worker:logs(即 worker-logs.cjs)按来源过滤排查通知失败。

四、管线挂接:syncAndBroadcastObservations 中的调用位置

playbook 指定的挂接点在 ResponseProcessor.tssyncAndBroadcastObservations() 函数内(当前源码中定义于第 598 行,由第 554 行 await syncAndBroadcastObservations(...) 调用)。该函数遍历本批 observation,对每条执行 broadcastObservation(worker, {...})(当前源码第 658 行)把新 observation 推给 SSE 广播器。

playbook 要求在此循环内、紧跟现有 broadcastObservation(worker, {...}) 调用之后(约第 247 行,对应 playbook 执行时的行号)追加:

import { sendObservationToast } from './ToastNotifier.js';
// ...
broadcastObservation(worker, { /* ...既有 payload... */ });
sendObservationToast(obs.title, obs.subtitle);   // 新增:fire-and-forget

从源码结构看,这个位置的选择有三个理由:

  1. 语义对齐:SSE 广播发生在 observation 已写入持久层之后,toast 与 SSE 是同一事件的两种“出站通知”(一个给同机的 viewer 前端,一个给操作系统通知中心),放在相邻行保持了“一次落库、两条出站”的清晰时序;
  2. 不 await、不 try:调用方不等待 node-notifier 完成,错误已在 sendObservationToast 内部被吞掉,主循环的异常路径完全不受影响;
  3. 循环内逐条调用:一批会话可能产出多条 observation,每条独立成一条通知(title/subtitle 一一对应),与 SSE 逐条广播的粒度一致。

五、测试策略:Phase-02 的六个用例如何锁定行为契约

Phase-02-Toast-Notification-Tests.md 把 Phase-01 的五条行为规格转化为 tests/services/worker/agents/ToastNotifier.test.ts 的六个可执行用例。其 mock 策略是:

  • vi.mock('node-notifier', ...) 拦截 notify() 并记录调用参数;
  • mock SettingsDefaultsManager.loadFromFile 以精确控制 CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED 的取值(隔离磁盘依赖);
  • 平台伪造使用 vi.spyOn(process, ...)Object.defineProperty(process, 'platform', ...) 并保存/还原,避免污染其他测试。

六个用例与规格条目的映射关系:

用例名 输入条件 断言 锁定的规格
sends notification when enabled on macOS platform=darwin,设置 'true' notify 以匹配的 title/message 被调用 主路径
does not send notification when disabled 设置 'false' notify 未被调用 规格 3(开关)
does not send notification on non-macOS platforms platform=linux,设置 'true' notify 未被调用 规格 1(平台守卫)
handles null title gracefully (null, 'subtitle') 以兜底 title 调用 notify 规格 4
handles null subtitle gracefully ('title', null) 以空 message 调用 notify 规格 4
does not throw when notifier errors notify 抛出 sendObservationToast 不抛出 规格 5(永不崩溃)

Phase-02 还包含两条工程护栏:先跑 npx vitest run tests/services/worker/agents/ToastNotifier.test.ts 单独验证,再跑全量 npx vitest run 确认无回归;并预先声明了“存在 24 个与本功能无关的既有失败”作为验收基线——若新设置键破坏了 SettingsDefaultsManager 相关 fixture,则需给 fixture 补 CLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED: 'false'。这条要求揭示了一个容易踩的坑:SettingsDefaults 注册新键会改变 getAllDefaults() 的输出形状,任何对该形状做深比较的测试 fixture 都会失配

六、构建与验证:npm run build-and-sync

playbook 的收尾 Task 是构建验证,命令链在 package.json 中可查:build-and-sync = build(同步插件清单、构建 hooks、生成 lockfile)+ sync-marketplace + node scripts/restart-marketplace-worker.cjs,即编译、分发与 worker 重启一气呵成。playbook 记录的结果是构建成功、二进制编译完成、marketplace 同步完成、worker 重启后健康。

由于该功能依赖 Bun/Node 混合工具链(engines 要求 node >= 20.12.0bun >= 1.0.0),复现时的完整验证顺序是:npm run build-and-sync(编译与重启)→ npx vitest run(单测与回归)→ 在 macOS 上把 ~/.claude-mem/settings.json 的开关置为 "true",触发一次 observation 保存,确认通知中心出现以 observation title 为标题、subtitle 为正文的静默通知(sound: false)。

七、当前仓库状态核对

为保证事实边界清晰,对照当前仓库快照需要明确三点:

  • src/services/worker/agents/ToastNotifier.tstests/services/worker/agents/ToastNotifier.test.ts 不存在于当前源码树,全仓库源码中也检索不到 node-notifiersendObservationToastCLAUDE_MEM_TOAST_NOTIFICATIONS_ENABLED 的任何引用;
  • SettingsDefaultsManager.tsSettingsDefaults 接口已不含该键(接口当前以 CLAUDE_MEM_SERVER_BETA_* 系列收尾);
  • 因此本文的“参考实现”代码块是依据 playbook 规格逐条重构的示例,而配置系统、广播器范式、ResponseProcessor 挂接点等所有周边事实均以当前仓库文件为准并给出了路径。

八、小结

这套 playbook 展示了一个小型旁路功能的完整工程闭环:依赖声明 → 注册进统一设置系统(播种、持久化、环境变量三层覆盖)→ 以功能模块 + 平台守卫 + 永不抛错实现的旁路模块 → 在既有广播循环中一行挂接 → 用六个 mock 用例锁定全部行为规格 → 构建同步后健康检查。其中“fire-and-forget + 全函数 try/catch + 旁路失败只告警不中断”的模式,是 claude-mem 中 Chroma 同步、folder CLAUDE.md 更新等旁路功能共用的稳定性设计,也是在其 worker 内新增任何“通知类”功能时最值得沿用的契约。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384