你有没有遇到过这种情况:电脑上编译任务跑完没提示,下载任务结束没提醒,想给自己定时发一条系统消息却不知道怎么操作。macOS 的通知中心并不只是 App 的专属通道,你自己也能随时发送一条“mac自定义系统消息”,让它在指定时间、指定场景下自己弹出来。这事听起来像开发者的专属技能,实际上一条命令就能搞定,普通用户也能玩得很溜。
这篇文章我会把从入门到进阶的玩法都过一遍,包括最基础的 osascript 命令、如何带声音和图标、如何定时自动触发,以及怎么从 Python、本地网页、快捷指令里“借力”推送系统通知。适合想把 Mac 用得更有章法、又不想装一堆乱七八糟软件的人。你在终端里复制粘贴就能跑通,不需要懂编程基础。
1. 一条自定义系统消息能替你盯住什么事
1.1 日常提醒:从喝水到抢课都能用
很多人装了一堆提醒 App,最后发现最可靠的还是系统自带的通知中心。原因很简单:它不会因为 App 没打开就罢工,也不会因为后台被清理就漏掉消息。但系统通知默认是 App 发给你的,你没法直接“命令”它弹一条“该喝水了”。
自定义系统消息做的事情,就是绕开 App,直接调用 macOS 的通知中心接口。你可以让它每天早上 9 点提醒你打卡,下午 3 点提醒开会,每工作 45 分钟提醒站起来活动一下,甚至在你盯着屏幕太久时弹一条“看看远处”。
我自己的习惯是配合番茄工作法,每 25 分钟弹一次“休息一下”,声音用比较柔和的 Purr。这个提醒不需要任何第三方工具,比那些付费的专注 App 更直接,也不会有乱七八糟的弹窗广告。
1.2 脚本跑完自动告诉你
如果你平时会在终端里跑编译、打包、跑测试、下载大文件,那你一定经历过反复切回终端看进度的过程。其实完全可以让命令跑完后自动弹一条系统消息。
比如你执行一个耗时的 npm run build,跑完后终端可以自动调用通知接口,弹出一条“构建完成”的消息。这样你可以放心切到浏览器刷页面,听到提示音再回来干活。
更实用的是把这类通知封装成一个终端函数,任何命令都可以套进去。后面我会在第 4 章给你完整的写法,你只需要复制粘贴到 ~/.zshrc 里,就能获得一个 notify_after 命令,专门用来“盯着”长任务。
1.3 跨设备联动:手机一句话,电脑弹通知
这招稍微进阶一点,但很实用。借助快捷指令和 iCloud 同步,你可以在 iPhone 上创建一个快捷指令,运行后把“提醒”内容同步到 Mac 上,让 Mac 立刻弹出一条系统消息。
具体思路是:Mac 端设置一个“接收快捷指令”的自动化,iOS 端发送文本给它,Mac 收到后调用 osascript 显示通知。这样一来,你躺在床上用手机就能让客厅的 Mac 弹一条“记得明天带钥匙”的消息。
如果你有自建的推送服务,比如 Bark 这类开源工具,也可以把手机消息直接转发到 Mac,原理更简单,但需要一台能接收推送的设备。这里的核心思想是一致的:系统消息只是展示层,触发方式可以来自任何地方。
1.4 服务器监控和定时任务汇总
稍微懂点脚本的人,还可以把系统消息变成个人监控中心。比如服务器 CPU 负载超过阈值、日志里出现某个关键字、每日备份结果,都可以在脚本里加上通知逻辑,让结果主动弹出来。
我之前用一套简单的 cron 脚本检查本地磁盘剩余空间,低于 20% 就弹一条警告。后来加了电池健康度提醒、外接显示器状态提醒,全都是靠同一种系统消息机制。它的价值不在于某一次弹窗,而在于让你把“主动查询”变成“被动接收”,减少了很多不必要的焦虑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先学会最基础的指令:osascript 发通知的完整玩法
2.1 display notification 的语法
macOS 自带 AppleScript 解释器,可以通过 osascript 命令直接调用通知中心接口。最基础的一条命令长这样:
bash复制osascript -e 'display notification "这是一条自定义消息" with title "我的通知"'
这条命令的意思很直白:让系统弹一条通知,正文是“这是一条自定义消息”,标题是“我的通知”。你可以把它理解成“给通知中心打个电话,叫它帮你贴一张便签”。
它还支持三个常用参数:
with title:通知的标题subtitle:副标题,显示在标题和正文之间sound name:提示音名称,比如"Glass"、"Ping"
组合起来就是:
bash复制osascript -e 'display notification "时间到了" subtitle "该休息一下" with title "提醒" sound name "Purr"'
执行之后,屏幕右上角会弹出横幅,通知中心里也会保留记录。这个命令几乎兼容所有新版本的 macOS,你不用担心系统版本太老跑不起来。
2.2 在 Shell 脚本里传变量
实际使用中,你往往要在通知里带上动态内容,比如“下载完成,耗时 12 分 30 秒”。这时需要把变量拼进命令里,但要注意引号的嵌套问题。
推荐用双引号包整个 osascript 参数,内部变量用反斜杠转义:
bash复制msg="构建完成,耗时 2 分 15 秒"
osascript -e "display notification \"$msg\" with title \"任务结束\""
如果消息里可能包含双引号,最好先做一层替换:
bash复制msg=${msg//\"/\'}
这个细节容易踩坑,我一开始直接在变量里放中文和引号,结果命令断掉,后来统一做了转义处理才好。经验就是:能不用单引号嵌变量就不用,能用 printf 构造消息就不用字符串硬拼。
2.3 从 App 内“借用”通知能力:快捷指令里的“显示通知”
如果你完全不想碰终端,也可以使用 macOS 自带的“快捷指令” App。新建一个快捷指令,添加“显示通知”操作,填入标题和正文,然后运行,效果和 osascript 一样。
最方便的是,快捷指令可以被添加到菜单栏、触控栏,甚至通过“嘿 Siri”触发。你还能把它设置成个人自动化的一部分,比如连接到某个 Wi-Fi 时自动弹一条“已连接公司网络”。这个功能对不熟悉命令行的人非常友好。
不过快捷指令的通知自定义程度没有命令行高,比如有些声音参数就不是所有版本都支持。我的建议是:日常简单提醒用快捷指令,脚本集成和定时任务用命令行,两者互补。
3. 给通知加上声音、图标和点击动作,体验才完整
3.1 声音参数和可用声音列表
默认通知没有声音,很多时候会被忽略。加上声音参数后,效果立刻不一样:
bash复制osascript -e 'display notification "打包完成" with title "构建通知" sound name "Glass"'
macOS 系统自带的声音存放在 /System/Library/Sounds 目录,你可以直接在 Finder 里打开试听。常用且比较耐听的声音有这些:
| 声音名 | 风格 |
|---|---|
| Basso | 低沉短促 |
| Blow | 短促气流声 |
| Bottle | 空瓶声 |
| Frog | 蛙鸣 |
| Funk | 电子音 |
| Glass | 清脆玻璃声 |
| Hero | 英雄式音效 |
| Morse | 摩斯电码感 |
| Ping | 清爽提示音 |
| Pop | 软木塞声 |
| Purr | 猫呼噜声 |
| Sosumi | macOS 经典提示音 |
| Submarine | 潜艇声 |
| Tink | 清脆小铃铛 |
注意声音名是大小写敏感的,写成 "glass" 不会报错,但也不会响。建议先用 Ping 或 Glass 做测试,确认能听到声音再换成喜欢的。
3.2 想点击通知后打开链接?用 terminal-notifier
osascript 的通知本身不支持“点击后打开某个 URL”或“激活某个 App”。如果你需要这个能力,可以用 terminal-notifier,一个老牌开源命令行工具。
安装方式:
bash复制brew install terminal-notifier
然后发送带链接的通知:
bash复制terminal-notifier -title "日报提醒" -message "点我打开日报页面" -open "https://example.com"
它还有一些实用参数:
-activate com.apple.Safari:点击后激活某个 App-sender com.apple.Safari:让通知显示成来自 Safari-appIcon:指定自定义图标路径
这个工具对开发者特别实用,比如监控脚本发现服务挂了,点击通知直接打开管理页面。稳定性方面也还不错,我用了一年多没出过问题。
3.3 自定义图标和 App 名称显示
osascript 的通知默认显示“脚本编辑器”作为来源,看起来不够“正经”。用 terminal-notifier 可以指定发送方 App 和图标:
bash复制terminal-notifier -title "服务器告警" -message "CPU 负载过高" -sender com.apple.Terminal -appIcon ~/icon.png
这样通知横幅上会显示 Terminal 的图标,视觉上更自然。如果你希望显示成某个特定业务 App,也可以把 -sender 换成对应的 Bundle ID。
懒人方案是用快捷指令的“显示通知”,它默认使用快捷指令的图标,也能在快捷指令里更换成任意图标。两种方式各有优势,看你更习惯哪套流程。
3.4 一个按钮通知怎么实现:AppleScript 的 display alert
有时候你需要的不是横幅通知,而是一个带按钮的弹窗,比如“下载完成,点击确定继续下一步”。这就要用到 display alert:
bash复制osascript -e 'display alert "确认" message "要继续执行后续任务吗?" buttons {"取消", "继续"} default button "继续"'
这个弹窗是模态的,会阻塞脚本直到你点了按钮。它适合用在关键操作节点,防止你错过重要选择。和通知中心消息相比,它更“强制”,不会因为勿扰模式被吞掉。
需要注意的是,display alert 的按钮文案不要写太长,否则弹窗会被拉得很宽。如果想获取用户点击了哪个按钮,可以用返回值做判断,不过这需要稍复杂一点的 AppleScript 脚本,普通场景用不太到。
4. 让消息自己发出来:launchd 定时任务与快捷键触发
4.1 用 launchd 做一个每天定时提醒
手动执行命令只是第一步,真正让系统消息发挥价值的是自动触发。macOS 原生推荐用 launchd 做定时任务,它比 cron 更贴近系统底层,能管理用户会话、环境变量,也更稳定。
举个例子:每天下午 15:00 弹一条“记得整理今日待办”。先写一个 plist 配置文件,放在 ~/Library/LaunchAgents/ 下:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.dailyreminder</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>-e</string>
<string>display notification "记得整理今日待办" with title "每日提醒" sound name "Ping"</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>15</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>
然后加载它:
bash复制launchctl load ~/Library/LaunchAgents/com.example.dailyreminder.plist
以后每天 15:00 系统就会自动弹通知,不需要你操心。如果哪天不想要了,执行 launchctl unload 即可。
这里有个关键点:ProgramArguments 里的命令尽量用绝对路径,比如 /usr/bin/osascript,不要写裸命令 osascript。因为 launchd 环境里不一定有你终端里的 PATH,裸命令很容易静默失败。
4.2 快捷键触发一条通知
定时提醒之外,快捷键触发也很常用。比如你正在浏览器里写东西,突然想给自己记一个临时提醒,按一下组合键就能弹出来,比切到日历 App 快得多。
方式一:用“快捷指令” App 创建一个“显示通知”的快捷指令,然后在“系统设置 - 键盘 - 键盘快捷键 - 服务”里给它绑定快捷键。你可以自定义,比如 Option+Command+N,按一下就会弹通知。
方式二:用“自动操作”创建“服务”,接收“没有输入”,运行 Shell 脚本,再绑定快捷键。这个方法稍旧,但依然有效。很多人习惯用 Option+Command+某键 刷新页面,自然也能用同样思路触发通知。
我个人更推荐快捷指令方式,设置界面更直观,而且在较新系统上对快捷键的支持也更完整。
4.3 命令跑完后自动提示:给你的终端加一个包装函数
这是程序员最常用的玩法。在 ~/.zshrc 或 ~/.bashrc 里加一个函数:
bash复制function notify_after() {
local start_time=$(date +%s)
"$@"
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
local msg="命令执行完成,耗时 ${duration} 秒,退出码 ${exit_code}"
osascript -e "display notification \"$msg\" with title \"终端任务\" sound name \"Glass\""
return $exit_code
}
保存后执行 source ~/.zshrc,然后你就有了一个 notify_after 命令:
bash复制notify_after npm run build
任务跑完,不管成功失败,都会弹一条带耗时的通知。这个函数比网上很多版本多了“耗时”和“退出码”信息,排查问题时很有用。
我还在这个函数基础上加了“仅在耗时超过 10 秒时才通知”的逻辑,避免短命令频繁打扰。判断方法很简单:在函数里比较 duration 是否大于 10,小于就直接不调用 osascript。这样能把系统消息用在真正值得通知的事情上。
5. 从 Python、网页和快捷指令“借力”推送系统通知
5.1 Python 脚本调用系统通知
很多人的脚本是 Python 写的,比如爬虫、数据分析、定时任务。要在 Python 里发系统通知,不需要额外安装第三方库,直接用标准库 subprocess 调用 osascript 就行:
python复制import subprocess
def notify(title, message):
script = f'display notification "{message}" with title "{title}" sound name "Ping"'
subprocess.run(["osascript", "-e", script])
notify("爬虫完成", "今日数据已抓取,共 1200 条")
这段代码在只要你装好 Python 就能跑,不需要 pip install 任何东西。如果你一直在用 Homebrew 或官方包安装 Python,那么“mac安装python”之后的第一件趣事就可以从这条通知开始。
如果你不想手动处理字符串转义,也可以用 pync 这个第三方库,它封装了通知逻辑,支持更丰富的参数:
bash复制pip install pync
python复制from pync import Notifier
Notifier.notify('任务完成', title='Python通知')
但说实话,subprocess 方案已经足够,还少一个依赖。我自己的 Python 脚本里就一直用标准库版本,省心。
5.2 本地网页触发通知:把“网页上的按钮”变成“系统消息”
很多人搜过“html页面,页面上获取本地网卡的信息,mac地址,ip地址等”这样的关键词,想在浏览器里获取本机网卡信息。这里先说结论:浏览器出于安全沙箱限制,不能直接读取本机 MAC 地址和 IP 地址,更不能直接调用 osascript。正确做法是起一个本地 HTTP 服务,让网页通过接口去拿数据,再由服务端负责弹通知。
下面是一个极简的 Python 服务,在终端跑起来后,浏览器访问特定地址就能触发系统通知:
python复制from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import socket
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/notify":
subprocess.run(["osascript", "-e", 'display notification "本地网页触发了系统消息" with title "网页通知" sound name "Glass"'])
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
elif self.path == "/network":
ip = socket.gethostbyname(socket.gethostname())
mac = "请通过 ifconfig 命令查看,或调用系统接口获取"
body = f'{{"ip": "{ip}", "mac": "{mac}"}}'.encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8787), Handler).serve_forever()
启动后在浏览器打开 http://127.0.0.1:8787/notify,Mac 就会弹出一条来自“网页通知”的系统消息。你在自己写的本地 HTML 页面里放一个按钮,用 fetch 请求这个地址,就实现了“点网页按钮,系统弹出原生通知”。
获取网卡信息时,最稳妥的方式是在服务端用 ifconfig 或标准库读取,再返回给网页。网页只负责展示和触发动作,这样既安全又可靠。千万别想着用浏览器里的各种 hack 去直接读 MAC 地址,新版本浏览器基本都堵死了。
5.3 快捷指令进阶:通知内容从哪来
快捷指令的“显示通知”可以插入很多变量,你可以在通知里带上当前时间、剪贴板内容、天气信息等。比如做一个“早安”快捷指令:
- 添加“获取当前日期”操作,格式设为“今天,M月d日”
- 添加“显示通知”,标题填“早安”,正文插入日期信息
- 运行它,就会弹出“早安,今天是 5月17日”
进阶一点,你还可以让快捷指令接收“快捷指令输入”,这样其他 App 可以把文本传给它,由它来展示系统消息。比如从一个 Markdown 编辑器选中一段文字,分享到快捷指令,Mac 就弹出那段文字的通知。
这个思路的价值在于:系统消息不只是“自己写死的一段话”,它可以变成整个工作流里的一个出口,任何信息都能通过它呈现。
6. 通知不弹、声音不响、权限丢失:我把这些坑都替你踩了一遍
6.1 第一次发通知没有反应:检查通知权限
最常遇到的问题就是命令执行了,但屏幕右上角什么也没弹出来。先别怀疑命令写错,大概率是通知权限没开。
macOS 会把“谁发的通知”当作一个来源。如果你在终端里执行命令,通知来源可能是“终端”或“脚本编辑器”;如果你用的是快捷指令,来源就是“快捷指令”。每个来源都要在“系统设置 - 通知”里单独允许。
建议你执行一次命令后,立刻打开“系统设置 - 通知”,找到对应 App,确认“允许通知”是打开的,横幅样式也选对了。我第一次用 osascript 时,通知权限默认是开启的,但换到 Python 通过 subprocess 调用后,发现来源变成了 Python 解释器,需要重新授权一次。
6.2 勿扰模式会吞掉横幅
macOS 的“专注模式”开启后,通知会进入通知中心,但不显示横幅,也不响声音。如果你调了半天代码都没看到通知,检查一下菜单栏的月亮图标是否亮着。
解决方式有两种:一是调试时直接关闭专注模式;二是在“系统设置 - 通知 - 专注模式”里,把当前来源 App 设为“允许通知”。这样即使开着专注模式,也能正常弹出你的自定义消息。
这里有个小经验:如果你只是想在屏幕右上角看到一条不打扰自己的通知,开着勿扰模式反而更合适,因为通知会静默进入通知中心。想让它响铃时,才需要确认专注模式是关闭状态。
6.3 声音参数写错了
声音不响,不一定是被静音,也可能是指定了不存在的声音名。osascript 对声音名是大小写敏感的,而且不会给任何报错提示。
排查方法很简单,打开 /System/Library/Sounds 目录,对照文件名输入。比如系统里是 Glass.aiff,你就要写 "Glass",而不是 "glass" 或 "GLASS"。
另外,有些第三方 App 会往用户声音目录放自定义提示音,这些也能用,但需要放在 ~/Library/Sounds 下,并且声音格式要正确。普通场景用系统自带的声音就够了,别在这上面花太多时间。
6.4 launchd 任务里 osascript 静默失败
这是我看过最多人翻车的地方。plist 里明明写了对,加载也成功,但到时间就是不弹通知。常见原因有三个:
第一,PATH 问题。launchd 环境里没有你终端里那套 PATH,osascript 这种系统命令建议直接写绝对路径 /usr/bin/osascript,其他命令同理。第二,脚本路径里的空格。比如你写 /Users/My Name/script.sh,plist 的 array 格式里必须分成多个 <string>,不能整个塞进一个参数。第三,脚本执行后立即退出,但通知还没弹出来。可以在脚本末尾加一行 sleep 1,保证通知有足够时间被系统接收。
排查时可以先把 launchctl list | grep com.example 看看任务是否在运行,再看有没有报错日志。更直接的方式是把输出重定向到文件:
xml复制<key>StandardOutPath</key>
<string>/tmp/my_notify.log</string>
<key>StandardErrorPath</key>
<string>/tmp/my_notify_error.log</string>
这样任何错误都能在日志里看到。
6.5 远程 SSH 会话里发不出通知
这坑只影响有远程登录需求的人。如果你用 SSH 登录到 Mac,在远程终端里执行 osascript,经常会发现命令执行成功,但桌面就是不弹通知。
原因在于 launchd 的任务运行在后台会话里,没有权限访问当前用户的图形界面服务。解决方法是借助 launchctl asuser 把命令放到用户会话域里:
bash复制launchctl asuser $(id -u) osascript -e 'display notification "远程任务完成" with title "SSH通知"'
这样能把命令绑定到当前用户的 Aqua 会话中,通知才能正常弹出。顺便提醒一句,如果通知内容是敏感信息,不要轻易通过远程会话推送,注意使用环境的安全。
6.6 多条通知被通知中心折叠
如果你在循环脚本里连续发通知,比如每 5 秒发一条,会发现它们可能在通知中心被折叠成一堆,而不是一条条展示。这其实是 macOS 通知中心故意做的聚合优化,避免同一来源的消息把屏幕刷爆。
想避免折叠,可以在消息内容里加上序号或时间戳,因为内容不同时系统更倾向于分别展示。但也不要无脑刷屏,连续几十条通知会让人崩溃。我的做法是:循环脚本里只在状态变化时通知一次,比如“失败了一次”“恢复成功了”,而不是每条记录都弹。
如果只是调试,可以用 display alert 弹窗,它会强制逐条展示,但也会阻塞脚本,看你实际需求来选。
最后分享一个小经验:我最常用的组合是“终端函数 + launchd 定时任务 + 带声音的通知”。每天下班前,系统自动弹一条“记得提交日报”,配一声清脆的 Glass 音,既不容易忽略,又不会像闹钟那么烦躁。如果你也经常在 Mac 上跑长任务,建议从第 2 章的那条 osascript 命令开始,粘贴进终端试一次,然后顺手加进自己的脚本里。试过之后你会发现,这大概是成本最低、确定性最高的系统消息方案。
