iFlow CLI hooks 实战:用 osascript 在 Mac 上实现任务完成通知

发布时间:2026/10/16 3:56:52
iFlow CLI hooks 实战:用 osascript 在 Mac 上实现任务完成通知
写这篇东西的起因很简单我经常用 iFlow CLI 跑一些耗时任务比如批量导出数据、打包压缩、跑回归测试短则三五分钟长则两三小时。终端里盯着吧浪费生命切去干别的吧又总担心“跑完了没有”“是不是已经失败了”。后来我顺手研究了一下 iFlow CLI 的 hooks 机制把任务状态直接变成 Mac 的系统通知彻底治好了这个“等任务焦虑症”。这个玩法不复杂但对所有常年在终端里折腾脚本、又不想把时间浪费在盯屏上的朋友来说值得抄作业。本文围绕 iFlow CLI 实战展开目标只有一个通过 hooks 在 Mac 上实现任务成功、失败、结束时的本地提醒。适合经常用 CLI 跑长任务、希望任务跑完立刻感知到的开发者也适合把 iFlow 流程接入自己现有脚本体系的人参考。我会把配置思路、通知脚本原理、常见坑和扩展玩法都说清楚照着做就能跑起来。1. 项目整体拆解iFlow CLI 的 hooks 到底解决什么问题1.1 iFlow CLI 是什么hooks 在哪个环节生效iFlow CLI 是一款面向流程编排的终端工具核心解决的是“多个命令按步骤串起来、统一管理”的问题。它把任务组织成命令块支持变量、依赖关系、并行执行、错误终止等能力。比起裸写 shell 脚本它的优势是规则清晰、可读性强、便于复用。不过我更喜欢它的 hooks 机制。所谓 hooks可以理解成“挂在任务生命周期节点上的回调脚本”。 iFlow 在跑一个任务时会经过几个固定节点任务开始前pre-run、任务成功后on-success、任务失败后on-error、任务最终结束finally。你可以在这几个节点各挂一段命令或脚本让它们在对应时机自动执行。这样设计带来的直接好处是提醒逻辑不用写进业务命令本身。你不需要在每条命令后面加一行“echo 完成”也不用在命令失败时手动补一句错误处理。hooks 把这些横切关注点独立出来了业务命令保持干净提醒逻辑单独维护换掉任务内容提醒脚本还能继续用。1.2 用 hooks 做提醒的核心优势举个例子。假设我有一条数据导出命令命令本身是python scripts/export_data.py --date 2025-06-01如果没有 hooks我想知道它跑完没跑完只能原地等待、时不时看一眼终端或者在命令后面追加echo 完成。但追加 echo 只能覆盖“正常结束”的情况如果命令中途崩了我也许得等发现没有输出时才意识到出了问题。hooks 则把这两种状态都覆盖了。任务成功时执行成功回调任务失败时执行失败回调无论结果如何都有一个地方能响应通知。而且 iFlow 在回调脚本里注入了任务名、退出状态、开始时间、结束时间等信息我可以直接把这些内容拼进通知里一眼就知道是哪个任务、结果如何、耗时多长。这是手动追加命令很难做到的信息量。所以 iFlow CLI hooks 的组合本质上把“终端消息”升级成了“系统级通知”任务跑完不需要你一直睁眼看着屏幕它直接弹到 macOS 通知中心效果等同于有人在你旁边说一句“活儿干完了”。1.3 Mac 版实现思路的整体流程本次实战涉及四个环节先在 iFlow CLI 里定义一个任务任务内容是一段耗时脚本。在任务的 hooks 节点上绑定通知脚本。通知脚本里使用 macOS 自带的 osascript 指令把消息投递到系统通知中心。用一句话或一段声音提醒代替终端输出形成闭环。整条链路不依赖第三方付费应用全部由系统能力和 iFlow CLI 原生的 hooks 承担。只要你的 Mac 能运行 osascript这台机器天然就是一个“可编程的通知中心”。2. 环境准备与方案选型为什么选 osascript 而不是其他提醒方式2.1 在 Mac 上做本地通知的主要路径对比Mac 上做本地提醒常见的有这几种方法方案依赖可定制程度优缺点osascript 调用系统通知macOS 自带中最轻量无需安装通知中心原生展示终端内播放提示音macOS 自带低适合不靠视觉、只需感知离席状态语音播报 say 命令macOS 自带中可播报完整信息但公共场合有点扰人第三方命令行工具需安装高样式丰富但多一个依赖多有移植成本我最终选 osascript。原因有三第一它随系统自带不需要额外安装任何东西在别人机器上复现配置时少一道安装软件的步骤第二它能直接投递到 macOS 通知中心消息会保留一段时间即使当时没看到之后锁屏状态或通知中心也能翻到第三命令结构简单用 shell 字符串就能拼出通知内容和标题。say 语音播报我也试过适合任务失败时用因为失败往往比成功更需要“强提醒”。声音方案可以叠加使用比如失败时先发通知再播一句“任务失败请检查”成功率更高。不过日常场景下通知中心提醒已经足够。2.2 确认系统环境与基础权限在开始写脚本之前先确认三个基础条件macOS 版本支持 osascript。这是系统自带组件一般情况下直接可用。当前用户对终端有“发送通知”的权限。首次运行 osascript 时系统可能会弹权限确认允许后即可长期使用。iFlow CLI 的 hooks 功能需要在较新版本中开启。建议运行iflow version确认版本号如果版本过旧先升级到最新版本再用。登录 iFlow CLI 后所有任务配置放在项目配置文件iflow.yaml里。 hooks 也是这个文件里定义的一部分字段。我会在下一步把配置完整写出来。2.3 为什么用独立脚本文件而不是直接把长命令塞进配置把通知逻辑写进iflow.yaml的直观做法是直接在 commands 里写一长串 osascript。比如hooks: on-success: commands: - osascript -e display notification done with title task finished这样写的问题很多引号嵌套极其难受消息内容里一旦包含单引号或双引号就会炸后续想加逻辑比如判断退出状态、拼接多个信息更是寸步难行。我在实战中踩过一次后立刻改成“配置里只写脚本路径通知逻辑全部挪进独立脚本文件”。这样配置文件简短清晰通知脚本变成普通 shell 脚本任意扩展都不受影响。所以接下来优先准备好一个目录专门存放 hooks 脚本。我的习惯是放在项目下的.iflow/hooks/目录里也可以在系统配置目录下放一套全局 hooks按需引用。3. 核心细节解析通知脚本的参数传递与安全转义3.1 osascript 通知命令的基本结构macOS 调用通知中心的命令格式如下osascript -e display notification 提醒内容 with title 标题执行后右上角会弹出系统通知。如果想要在通知里加上声音可以在结尾追加sound name Glass例如osascript -e display notification 任务已完成 with title iFlow sound name Glasssound name 必须是系统里存在的提示音名称。为了避免“指定的声音不存在导致整条命令报错”的情况我通常不加声音或者只使用系统默认的几个Glass、Ping、Sosumi这些在多数 macOS 上都有。3.2 hooks 注入的环境变量有哪些iFlow CLI 在触发 hooks 时会向脚本注入几个环境变量。这些信息对组织通知内容非常重要环境变量含义IFLOW_TASK_NAME当前执行的任务名IFLOW_STATUS任务结果状态比如 success / errorIFLOW_START_TIME任务开始时间IFLOW_END_TIME任务结束时间IFLOW_EXIT_CODE任务真实退出码我在通知脚本里主要使用任务名和退出码。比如成功通知会写成“任务 build: ok”失败通知则带上退出码“任务 build: fail code2”一眼就能定位问题。但这里有一个关键注意点不要把环境变量直接拼进 osascript 命令里。具体原因下面单独说。3.3 命令注入与引号转义问题假设你在 shell 里直接写osascript -e display notification \$IFLOW_TASK_NAME 已完成\ with title \iFlow\这在小范围测试时能跑通可一旦任务名或消息内容里出现了引号、美元符号、反引号整条命令都可能被解析错误甚至触发意外的命令执行。比如任务名是text_update消息内容就被拆成了几段osascript 直接报错。更安全的方式是把 osascript 当成一个带参数的命令用位置参数传值不让消息内容直接参与 shell 拼接。在 osascript 脚本内部再用item 1 of argv去取参数。但是 shell 里给 osascript 传参数并不像 C 语言那样天然安全麻烦点在于你要构造出合法的 AppleScript 语法字符串。我最后采用的稳妥做法是用 printf 来生成 AppleScript 转义后的字符串。具体来说写一个函数把传入的文本先做“双引号转义”——把内部的双引号替换成\然后再嵌进 AppleScript 的字符串里。这样任何文本内容都能安全塞进通知notify() { local msg$1 local escaped escaped$(printf %s $msg | sed s//\\/g) osascript -e display notification \$escaped\ with title \iFlow\ /dev/null 21 }这样处理之后无论是普通文字、带标点的文件名还是包含引号的任务描述都能原样送进通知中心不再出现解析错误。3.4 成功与失败脚本的共用逻辑因为成功和失败两个 hook 脚本要做的事情非常相似我会把它们拆成一个小型公共函数再让两个入口脚本分别调用。公共函数负责拼通知消息入口脚本负责判断当前状态并选择合适的提示音。公共脚本notify-helper.sh大致长这样#!/usr/bin/env bash # 公共通知辅助脚本通过 source 方式引入 notify_via_osascript() { local msg$1 local title$2 local escaped escaped$(printf %s $msg | sed s//\\/g) osascript -e display notification \$escaped\ with title \$title\ /dev/null 21 || true } notify_with_sound() { local msg$1 local title$2 local escaped escaped$(printf %s $msg | sed s//\\/g) osascript -e display notification \$escaped\ with title \$title\ sound name \Sosumi\ /dev/null 21 || true }雪上加霜的安全隐患在于如果你在 osascript 字符串里放入含反引号的文本某些 shell 版本会尝试执行命令替换。所以转义时我索性把反引号也一并转掉escaped$(printf %s $msg | sed -e s/\\/\\\\/g -e s//\\/g -e s//\\/g)这条处理规则来自我实际踩坑后的沉淀通知内容你永远不知道用户会填什么多转义一层少一个半夜被叫起来修脚本的风险。4. 实操过程从零到一配置 iFlow 通知提醒4.1 定义演示任务和目录结构我准备了一个模拟项目目录结构如下~/projects/demo-task/ ├── iflow.yaml ├── scripts/ │ └── export_data.py # 模拟耗时任务 └── .iflow/ └── hooks/ ├── notify-helper.sh ├── on-success.sh └── on-error.sh模拟任务是一段 Python 脚本它循环处理数据、休眠一段时间模拟真实耗时import time total 10 for i in range(total): print(fprocessing batch {i 1}/{total}) time.sleep(0.5)在实战场景里这段 Python 脚本可以换成真实的数据导出、资源打包、模型训练、文件转码等任何长耗时代码。提醒机制不会因任务内容而改变。4.2 编写 iFlow 配置文件iflow.yaml的核心内容如下versions: 1 tasks: data_export: command: python scripts/export_data.py hooks: on-success: commands: - .iflow/hooks/on-success.sh on-error: commands: - .iflow/hooks/on-error.sh这里要说明两个细节任务名是data_export在 hooks 脚本里能够通过$IFLOW_TASK_NAME读取到。hooks 命令的相对路径是以项目根目录为基准的所以.iflow/hooks/on-success.sh能正确定位。运行任务用iflow run data_export任务跑完后iFlow 会依据退出码自动触发对应的回调。模拟脚本正常结束时返回 0成功回调被触发一旦脚本抛异常退出失败回调就会接管。4.3 成功通知脚本的完整实现on-success.sh的逻辑是读取 iFlow 传过来的任务名和结束时间拼成一句“任务已完成”的通知文案再调用 osascript 弹出到通知中心。#!/usr/bin/env bash source $(dirname $0)/notify-helper.sh task_name${IFLOW_TASK_NAME:-unknown} end_time${IFLOW_END_TIME:-$(date %Y-%m-%d %H:%M:%S)} message任务 ${task_name} 已完成结束时间 ${end_time} titleiFlow 成功提醒 notify_via_osascript $message $title这里有两点值得展开。一是source的路径不要写死用dirname $0动态获取脚本所在目录这样项目换位置后依然能正常工作二是给变量设置默认值防止旧版本 iFlow 没注入对应变量时脚本报错。4.4 失败通知脚本的完整实现失败通知在内容上要比成功通知更醒目一些。除了任务名我还把退出码带上#!/usr/bin/env bash source $(dirname $0)/notify-helper.sh task_name${IFLOW_TASK_NAME:-unknown} exit_code${IFLOW_EXIT_CODE:-1} message任务 ${task_name} 执行失败退出码 ${exit_code} titleiFlow 失败提醒 notify_with_sound $message $title失败时我加了声音提醒。实际运行中如果你正处于专注模式或者会议室场景声音提醒可能造成打扰。我在脚本里预设一个环境变量开关IFLOW_QUIET1可以临时禁用声音避免不同场景切来切去if [[ $IFLOW_QUIET ! 1 ]]; then notify_with_sound $message $title else notify_via_osascript $message $title fi每次跑高风险任务时我会先决定要不要安静模式再决定是否执行。这个开关看起来简单但在真实使用中非常有用。4.5 给脚本加执行权限并验证写好的脚本必须加上执行权限否则 hooks 会被触发但运行失败通知自然也不会出现。我通常执行chmod x .iflow/hooks/on-success.sh .iflow/hooks/on-error.sh测试时用一个短任务验证即可。把scripts/export_data.py临时改成只 sleep 1 秒然后跑iflow run data_export几秒钟后如果右上角弹出了“任务 data_export 已完成”的通知说明链路已经通了。有一点容易忽略iFlow hooks 的 stdout 会回流到主终端如果你的通知脚本不小心向 stdout 输出了一大段信息终端会瞬间被刷屏。我习惯把 osascript 的标准输出和错误输出都重定向到/dev/null因为通知已经出现在系统界面里了终端里没必要再重复一遍。4.6 定时长任务的完整效果演示前端提醒和任务执行是可以并行的。我实际使用时不会一直守在终端前而是切到编辑器写代码或者查资料。任务结束后右上方弹出通知标题是“iFlow 成功提醒”内容包含任务名、结束时间。通知中心也会保留条目哪怕错过弹窗稍后也能打开通知中心查看历史记录。对于失败任务弹窗会带声音。再加上退出码信息我能快速判断退出码 1大概率是脚本内部业务错误比如数据校验没通过。退出码 2大概率是命令行参数或者文件路径问题。退出码 127大概率是命令找不到环境变量没配置好。这样可以省下重新回放终端日志的时间直接从通知里获得第一层诊断信息。5. 常见问题与排查技巧实录5.1 通知始终不弹先检查这三层如果 hooks 已经配置好脚本也写了但通知不弹我按三层思路排查。第一层任务有没有触发 hook。在iflow.yaml里临时加一条输出调试hooks: on-success: commands: - echo hook triggered - .iflow/hooks/on-success.sh如果终端里连hook triggered都没打出来说明配置格式有问题或者节点名写错了。iFlow 对不同节点名的大小写敏感比如on-success和on_success不是一回事。第二层脚本有没有被正确执行。检查脚本是否有可执行权限检查脚本的第一行解释器路径是否正确。之前我在一个项目里把#!/usr/bin/env bash写成了#!/usr/bin/env sh本身没问题但脚本里使用了 bash 特有的数组语法于是 sh 解释器直接报错通知完全没出来。统一用 bash 最省心。第三层osascript 本身有没有成功运行。单独在终端执行osascript -e display notification test with title test如果这条命令没有弹窗说明是系统权限或者系统级设置的问题。打开“系统设置”找到“通知”确认终端 App 的通知权限是允许状态同时确认当前没有开启“勿扰模式”。很多时候闹钟不响不是代码问题而是系统把通知拦了。5.2 任务名或消息内容带引号导致通知报错这是新手最容易踩的坑。现象是 osascript 报语法错误或者通知里的文字被截断。原因就是我把消息内容直接拼进了 AppleScript 字符串里没有转义。解决方式已经在前文提过核心是给消息内容里的双引号和反引号做转义。还有一点如果消息内容包含多行文本直接塞进 AppleScript 字符串也会出问题稳妥做法是把换行替换成空格message$(echo $message | tr \n )这样通知栏的显示效果更整洁也规避了 AppleScript 对换行符的敏感性问题。5.3 hook 脚本执行了但通知声音没响成功通知我故意不设声音所以无声是正常的。失败通知我会加声音如果没响可能是 sound name 写了一个系统中不存在的名字。系统内置的可用声音可以进/System/Library/Sounds/目录查看确认文件名字后用不带扩展名的名称引用。来自实践的一个细节如果通过 SSH 登录 Mac 执行 iFlowosascript 默认无法弹出 GUI 通知。这是因为 SSH 会话不是 Aqua 会话无法直接调用用户界面服务。如果要让远程机器上的任务提醒也有效需要在 Mac 本机保持一个用户会话然后通过 launchctl 方式在用户域执行 osascript。这个方案复杂度略高我本次实战以本机执行为主远程执行场景留作后续扩展。5.4 常见问题速查表我把实战中遇到的问题整理成一张速查表方便直接对照现象可能原因解决方案通知完全不弹系统通知权限未开启在系统设置里允许终端发送通知通知不弹但终端没有 hook 输出hook 节点未触发检查 iflow.yaml 节点名和格式hook 触发但脚本无效果脚本缺少执行权限chmod x 脚本osascript 报语法错误消息内容包含未转义引号使用 printf sed 转义声音没响sound name 不存在使用内置 Glass / Ping / Sosumi通知内容乱码特殊字符被 shell 解释统一走函数转义禁止直接拼接通知出现在了但点击没啥反应通知没有绑定 URL 动作不需要处理保持纯文本展示这张表其实同样适用于其他 CLI 工具的 hook 机制只要把环境变量名替换成对应工具的名字就行。6. 进阶从“单次提醒”到“批量任务感知”6.1 并联任务完成时的批量通知策略真实使用中我经常一次跑多个任务。如果每个任务成功都弹一条通知通知中心会瞬间刷屏反而失去提醒的意义。我采用的做法是在通知脚本里加一个简单的“延迟聚合”机制。具体原理是通知脚本不直接调用 osascript而是把任务名写进一个临时文件然后使用一个后台进程去延迟 5 秒再统一弹出聚合通知。这个做法的实现不复杂但涉及多进程并发写文件的问题。我是这样处理的aggregate_dir/tmp/iflow-notify mkdir -p $aggregate_dir # 写入当前任务名 echo $task_name $exit_code $aggregate_dir/pending.txt # 如果后台聚合进程已存在就不要再起一个 if [[ ! -f $aggregate_dir/aggregate.lock ]]; then touch $aggregate_dir/aggregate.lock ( sleep 5 # 读取所有待通知的任务名拼接成一条消息 tasks$(cat $aggregate_dir/pending.txt 2/dev/null | tr \n ) notify_via_osascript 完成 ${tasks} iFlow 批量完成 rm -f $aggregate_dir/pending.txt $aggregate_dir/aggregate.lock ) fi这样多个任务在 5 秒内陆续结束时最终只弹出一条加总通知。如果任务本来就在不同时间里分批结束超过 5 秒的间隔就会被拆成多条各取所需。这个设计对大量并行任务非常友好我跑批量测试时再也没被通知轰炸过。6.2 给通知加上操作意图iFlow 的 hooks 通知目前是纯文本展示不能点击后直接跳转到任务日志目录。但我们可以在通知内容里带上日志路径需要时去打开对应目录查看。我在任务里把日志输出到固定位置然后在通知文案里带上这个路径比如message任务 ${task_name} 失败退出码 ${exit_code}日志logs/${task_name}.log实际使用中我觉得这个信息量比退出码本身更有价值。相当于从通知栏直接获得了一个打开日志的线索。想要进一步集成的话也可以让通知脚本在失败时把最近 30 行日志直接拼进消息里但通知栏显示空间有限长日志会截断所以真实场景我一般只放路径。6.3 把提醒脚本扩展到其他事件hooks 不只有成功和失败两个节点。任务开始前也可以用 pre-run 弹一条“任务已启动”的通知。但这个场景对多数人意义不大因为你都手动发起任务了大概率知道任务已经启动。不过有一种情况例外你通过 at、cron 或者别的方式在后台调度 iFlow 任务任务什么时候真正开始你不一定清楚。这时 pre-run 通知就有用了它告诉你调度是否生效、时间点是否正确。我还会在 finally 节点做一件事把任务总耗时打印到一个统计文件里。用IFLOW_END_TIME减去IFLOW_START_TIME得到耗时附加到历史统计表中。这样长期跑批任务之后可以回顾哪些任务变慢了、哪些阶段出现了波动算是 hooks 在提醒之外的额外价值。6.4 提醒脚本的维护小技巧通知脚本虽然简单但维护起来也有讲究。我给我的 hooks 脚本定了三条规则所有脚本全部用 bash 编写不使用 zsh-only 语法。所有通知调用统一经过notify-helper.sh不使用散落的 osascript 语句。脚本里凡涉及中文文本文件统一保存为 UTF-8避免在不同终端环境里出现乱码。特别是 UTF-8 这点在 macOS 上通常问题不大但如果你的 iFlow 任务在 CI 容器里执行、然后通过 SSH 回传结果字符编码风险就会冒出来。宁可实现时多校验一次。7. 写在最后的一点体会这套 iFlow CLI 加 hooks 加 osascript 的组合我实际用了快半个季度。变化是明显的跑长任务时终于可以安心切走不用每隔几分钟就切回终端看一眼。通知弹出来后瞄一眼右上角就知道结果成功则接着干失败则带着退出码去查日志效率提升肉眼可见。如果你也想改造成自己项目里的默认提醒建议第一次落地时先跑一个短任务把链路通起来确认通知能弹之后再逐步接入真实任务。因为真实任务一旦跑崩你同时要面对脚本报错和任务报错两个问题排查成本会翻倍。先把链路调稳是对自己对任务负责。最后分享一个实际使用中颇有好感的小细节通知标题我固定用“iFlow 成功提醒”和“iFlow 失败提醒”但通知内容会根据任务名动态变化。用久之后系统通知中心里会形成一条按时间排序的历史清单每天哪些任务跑过、结果如何一翻便知。某种程度上这比日志文件更直观——因为它是系统主动提醒不是靠自己翻记录。这个小习惯推荐给和我一样会同时兼顾多个终端任务的开发者。