1. 这个提醒器到底在解决什么问题
长时间跑代码生成任务的人都有一个共同的痛点:你把一段复杂的重构需求丢给命令行里的 AI 编程助手,它开始吭哧吭哧干活,你盯着屏幕等了半分钟,觉得无聊就切到浏览器看两眼新闻,结果一回头发现它早就跑完了,白白浪费了好几分钟。更糟的情况是,它中途弹出一个确认提示等你输入,而你完全没注意到,任务就卡在那里一动不动。
我平时用 Claude Code 和 Codex 这类命令行 AI 编程工具的频率很高,几乎每天都要跑十几个任务。有些任务几秒钟就结束,有些要跑三五分钟,尤其是涉及大文件重构或者多轮推理的场景。一开始我的做法是傻等,后来变成每隔十几秒切回来瞄一眼,再后来干脆去干别的事,但心里总惦记着“它跑完了没有”,根本没法真正放松。
这个完成提醒器要解决的核心问题就一个:让 AI 编程助手在任务结束时主动通知你,而不是你反复去查看它的状态。听起来简单,但要做到“安心摸鱼”这个程度,背后涉及几个关键的技术决策——怎么检测任务完成、用什么方式发通知、怎么区分“正常完成”和“等待输入”这两种状态。这几个问题不想清楚,做出来的东西要么误报要么漏报,反而更让人焦虑。
适合读这篇内容的人:日常使用命令行 AI 编程工具的开发者、想给自己工作流加自动化提醒的效率爱好者、以及任何对终端任务监控感兴趣的人。不需要很深的编程基础,但至少要能看懂基本的 Shell 命令和 Python 脚本。
2. 整体设计思路与方案选型
2.1 为什么不做成插件而是做成独立监控进程
最开始我考虑过给 Claude Code 写一个插件或者 hook,直接在它的生命周期里挂回调。但实际调研后发现两个问题:一是不同工具的插件机制差异很大,Claude Code 有 hooks 机制,Codex 的扩展方式又不一样,想统一处理很麻烦;二是插件方案和工具本身耦合太紧,工具一升级就可能失效。
所以最终选了一个更“笨”但更稳的方案:独立监控进程。它的工作逻辑很简单——定期检查目标进程是否还在运行、终端输出有没有新变化,一旦判断任务结束就触发通知。这个方案的好处是跟具体工具解耦,不管是 Claude Code、Codex 还是以后换别的工具,只要它是在终端里跑的进程,监控逻辑都能复用。
代价是检测精度不如原生 hook。原生 hook 能精确知道“任务完成”这个事件,而独立监控只能通过间接信号去推断。这就引出了下一个关键决策。
2.2 检测任务完成的三种信号及取舍
判断一个终端里的 AI 任务是否结束,我梳理了三种可用的信号:
| 检测信号 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 进程存活检测 | 检查目标 PID 是否还在 | 实现简单,零误报 | 无法区分“完成”和“等待输入” |
| 输出静默检测 | 监控终端输出,超过 N 秒无新内容则判定结束 | 能覆盖大多数场景 | 阈值难调,长推理会误判 |
| 提示符匹配 | 检测终端是否回到 shell 提示符 | 最准确 | 依赖终端环境,跨平台麻烦 |
我最终采用的是进程存活 + 输出静默的组合方案。逻辑是这样的:先确认目标进程还在,如果进程消失了,那肯定是结束了;如果进程还在但输出已经静默超过设定阈值(我用的默认值是 8 秒),那就判定为“可能完成或等待输入”,触发一个较弱的提醒。
为什么是 8 秒?这是我实测下来的经验值。AI 编程工具在正常推理时,输出间隔一般在 1 到 3 秒之间,偶尔有长推理会到 5 秒左右。设成 8 秒能过滤掉绝大多数正常的推理间隙,又不会让提醒来得太晚。当然这个值可以配置,后面会讲怎么调。
提示:如果你用的模型推理特别慢,或者网络波动大,8 秒可能不够,建议先观察几次正常任务的输出间隔,再定阈值。
2.3 通知方式的选择
通知方式我试过好几种,最后保留了三种,按打扰程度递增:
- 终端标题闪烁:最轻量,不打断当前操作,适合你就在电脑前但没看终端的情况
- 系统桌面通知:中等打扰,会弹一个通知卡片,适合你切到了别的窗口
- 声音提醒:最强打扰,适合你离开工位去倒水的情况
这三种可以组合使用,也可以单独开关。我自己的配置是桌面通知常开,声音只在任务超过 60 秒时才触发——短任务没必要响,长任务才值得提醒。
3. 核心实现细节与实操要点
3.1 进程监控的实现
监控进程的核心是拿到目标进程的 PID,然后定期检查它是否存活。在 Linux 和 macOS 上,用ps -p <PID>就能判断;在 Windows 上要用tasklist配合过滤。为了跨平台,我用 Python 的psutil库来统一处理,它封装了各平台的进程查询接口。
拿到 PID 的方式有两种。一种是你启动任务时手动记录,比如claude-code ... & echo $!,把 PID 存到一个临时文件里。另一种是自动发现,通过进程名或者命令行参数去匹配。我推荐第一种,虽然麻烦一点但更可靠,自动发现容易误匹配到别的进程。
import psutil import time def is_process_alive(pid): try: proc = psutil.Process(pid) return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE except psutil.NoSuchProcess: return False这段代码看起来简单,但有个坑要注意:进程变成僵尸状态(zombie)时,is_running()可能还返回 True,所以必须额外检查status()。我一开始没加这个判断,结果任务明明结束了却一直不触发提醒,排查了半天才发现是僵尸进程的问题。
3.2 输出静默检测的实现
输出静默检测比进程监控复杂一些,因为要实时读取终端的输出。这里有个关键问题:AI 编程工具的输出是写到它自己的 stdout 的,而我们的监控进程是独立的,怎么拿到它的输出?
我的做法是重定向 + 文件监控。启动任务时把它的输出同时写到终端和一个日志文件:
claude-code "重构这个模块" 2>&1 | tee /tmp/ai_task.log然后用监控进程定期检查这个日志文件的最后修改时间。如果超过阈值没有更新,就判定为静默。
import os def get_last_modified(filepath): return os.path.getmtime(filepath) def is_output_silent(filepath, threshold=8): last_mod = get_last_modified(filepath) return (time.time() - last_mod) > threshold用tee的好处是输出既能在终端看到,又能被监控进程读取,两不耽误。这里要注意日志文件的路径要固定,别用随机名,否则监控进程找不到。
注意:有些工具的输出是带缓冲的,可能攒一批才写一次,这会导致静默检测误判。如果遇到这种情况,可以在启动命令前加
stdbuf -oL强制行缓冲。
3.3 通知模块的实现
桌面通知在 macOS 上用osascript,Linux 上用notify-send,Windows 上用win10toast或者 PowerShell 的 BurntToast 模块。我写了一个统一的接口,根据当前系统自动选择:
import platform import subprocess def send_notification(title, message): system = platform.system() if system == "Darwin": subprocess.run(["osascript", "-e", f'display notification "{message}" with title "{title}"']) elif system == "Linux": subprocess.run(["notify-send", title, message]) elif system == "Windows": subprocess.run(["powershell", "-Command", f'New-BurntToastNotification -Text "{title}", "{message}"'])声音提醒更简单,macOS 用afplay播放系统音效,Linux 用aplay,Windows 用winsound。终端标题闪烁则是往终端发送 ANSI 转义序列,这个稍微 tricky 一点,因为要找到正确的终端设备。
3.4 状态区分:完成还是等待输入
这是整个项目里最难的部分。进程还在、输出静默了,可能是任务完成了,也可能是工具在等你确认某个操作。这两种情况需要不同的提醒策略——完成了就通知你收工,等待输入则要提醒你赶紧去处理。
我用的区分方法是检测输出内容的最后几行。AI 编程工具在等待输入时,通常会在最后输出一个提示符或者问句,比如Do you want to proceed? (y/n)或者Continue?。而正常完成时,最后输出的一般是结果总结或者直接回到 shell 提示符。
def check_waiting_for_input(logfile): with open(logfile, 'r') as f: lines = f.readlines() last_lines = ''.join(lines[-5:]).lower() keywords = ['(y/n)', 'continue?', 'proceed?', 'confirm', '请输入', '是否继续'] return any(kw in last_lines for kw in keywords)这个方法不是 100% 准确,因为工具的提示语可能变化,但实测下来覆盖了我常用的几个工具。如果匹配到等待输入的信号,通知内容会变成“任务等待你的输入”,而不是“任务已完成”。
实操心得:关键词列表要定期更新。工具升级后提示语可能变,我一般每隔几周会看一眼最近的日志,把新的提示语加进去。
4. 完整实操流程与配置方法
4.1 环境准备与依赖安装
先把基础环境搭起来。需要 Python 3.8 以上,以及psutil库:
pip install psutilWindows 用户额外装win10toast:
pip install win10toastLinux 用户确认notify-send可用,一般桌面环境都自带,没有的话装libnotify-bin:
sudo apt install libnotify-binmacOS 用户不需要额外装东西,osascript和afplay都是系统自带的。
4.2 启动任务并记录 PID
写一个包装脚本,把启动任务和记录 PID 合并成一步:
#!/bin/bash # run_with_monitor.sh TASK_LOG="/tmp/ai_task_$(date +%s).log" PID_FILE="/tmp/ai_task.pid" # 启动任务,输出同时写终端和日志 "$@" 2>&1 | tee "$TASK_LOG" & TASK_PID=$! # 记录 PID 和日志路径 echo "$TASK_PID" > "$PID_FILE" echo "$TASK_LOG" > /tmp/ai_task_log_path # 启动监控进程 python3 monitor.py "$TASK_PID" "$TASK_LOG" & wait $TASK_PID用的时候这样调用:
./run_with_monitor.sh claude-code "帮我重构 utils 模块"这样任务在跑的同时,监控进程也在后台盯着,任务一结束就发通知。
4.3 监控进程的完整实现
把前面几节的代码整合起来,加上主循环:
import sys import time import psutil import platform import subprocess def is_process_alive(pid): try: proc = psutil.Process(pid) return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE except psutil.NoSuchProcess: return False def is_output_silent(logfile, threshold=8): import os try: last_mod = os.path.getmtime(logfile) return (time.time() - last_mod) > threshold except FileNotFoundError: return False def check_waiting_for_input(logfile): try: with open(logfile, 'r') as f: lines = f.readlines() last_lines = ''.join(lines[-5:]).lower() keywords = ['(y/n)', 'continue?', 'proceed?', 'confirm', '请输入', '是否继续'] return any(kw in last_lines for kw in keywords) except FileNotFoundError: return False def send_notification(title, message): system = platform.system() if system == "Darwin": subprocess.run(["osascript", "-e", f'display notification "{message}" with title "{title}"']) elif system == "Linux": subprocess.run(["notify-send", title, message]) elif system == "Windows": subprocess.run(["powershell", "-Command", f'New-BurntToastNotification -Text "{title}", "{message}"']) def main(): pid = int(sys.argv[1]) logfile = sys.argv[2] notified = False start_time = time.time() while True: if not is_process_alive(pid): if not notified: send_notification("任务完成", "AI 编程任务已结束") notified = True break if is_output_silent(logfile): if not notified: if check_waiting_for_input(logfile): send_notification("等待输入", "任务需要你的确认") else: send_notification("可能完成", "输出已静默,请检查") notified = True else: notified = False time.sleep(2) if __name__ == "__main__": main()这个实现里有个细节:notified标志位用来防止重复通知。输出静默时发一次通知,如果之后又有新输出(说明任务还在跑),就把标志位重置,下次静默时再通知。这样避免了同一个任务反复弹通知。
4.4 参数调优与个性化配置
默认的 8 秒静默阈值不一定适合所有人。调优的方法是:先跑几个典型任务,记录每次输出间隔的最大值,然后把这个值乘以 1.5 到 2 倍作为阈值。
我自己的配置是这样的:
| 参数 | 默认值 | 我的配置 | 说明 |
|---|---|---|---|
| 静默阈值 | 8 秒 | 10 秒 | 我的任务推理时间偏长 |
| 检查间隔 | 2 秒 | 2 秒 | 再短没必要,再长反应慢 |
| 声音提醒阈值 | 无 | 60 秒 | 短任务不响,长任务才响 |
| 等待输入关键词 | 内置列表 | 自定义扩展 | 加了几个我常用工具的提示语 |
声音提醒的阈值逻辑是在监控进程里加一个判断:任务运行时间超过 60 秒才播放声音,否则只发桌面通知。
elapsed = time.time() - start_time if elapsed > 60: play_sound()5. 常见问题与排查技巧实录
5.1 通知不触发或触发太晚
这是最常见的问题,原因通常有三个。第一个是 PID 记录错了,比如用了管道导致$!拿到的是tee的 PID 而不是任务的 PID。解决办法是在启动脚本里仔细确认 PID 来源,必要时用pgrep按进程名反查。
第二个是日志文件路径不对,监控进程读不到文件,getmtime一直返回旧值或者抛异常。排查方法是手动ls -la看一下日志文件是否存在、修改时间是否在更新。
第三个是静默阈值设得太大,任务早就结束了但监控还在等。这个直接调小阈值就行,但要注意别调太小导致误报。
5.2 误报:任务还在跑却提示完成
误报的根源是输出静默检测把正常的推理间隙当成了任务结束。AI 模型在生成长文本时,有时候会思考好几秒才吐下一个 token,如果阈值设成 5 秒就很容易误报。
我的解决办法是双重确认:第一次检测到静默时先不通知,等再过 3 秒重新检查,如果还是静默才发通知。这样能把大部分瞬时静默过滤掉。
if is_output_silent(logfile): time.sleep(3) if is_output_silent(logfile): # 确认静默,发通知代价是通知会晚 3 秒,但比起误报带来的焦虑,这 3 秒完全值得。
5.3 跨平台兼容性问题
Windows 上的坑最多。psutil在 Windows 上查进程状态没问题,但tee命令不存在,要用 PowerShell 的Tee-Object替代。桌面通知也要装额外的模块。我建议 Windows 用户直接用 WSL 跑这套方案,省去很多兼容性麻烦。
macOS 上主要问题是osascript的通知权限,第一次运行会弹权限请求,要点允许。如果没弹或者误点了拒绝,要去系统设置的“通知”里手动给终端或者 Python 开权限。
Linux 上的问题是不同桌面环境通知机制不一样,GNOME 和 KDE 都能用notify-send,但一些轻量级桌面环境可能没装。装个libnotify-bin基本能解决。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 完全不通知 | PID 错误 | 检查 PID 文件内容 | 修正启动脚本的 PID 获取 |
| 通知太晚 | 阈值过大 | 观察输出间隔 | 调小静默阈值 |
| 频繁误报 | 阈值过小 | 看日志确认任务状态 | 调大阈值 + 双重确认 |
| 通知重复弹 | 标志位逻辑错误 | 检查 notified 变量 | 修正重置逻辑 |
| Windows 无通知 | 缺依赖 | 检查 win10toast | 安装依赖或用 WSL |
| 声音不响 | 音频命令缺失 | 手动执行播放命令 | 装对应音频工具 |
实操心得:我建议第一次部署时把检查间隔调到 1 秒、阈值调到 5 秒,这样能快速看到效果,确认整条链路通了之后再调回正常值。先跑通再调优,比一上来就追求完美配置效率高得多。
6. 进阶玩法与扩展思路
6.1 多任务并行监控
同时跑多个 AI 任务时,每个任务都要有独立的 PID 和日志文件。我把监控进程改成了支持多任务的形式,用一个配置文件列出所有正在跑的任务:
{ "tasks": [ {"pid": 12345, "log": "/tmp/task1.log", "name": "重构任务"}, {"pid": 12346, "log": "/tmp/task2.log", "name": "测试生成"} ] }监控进程定期读这个配置,对每个任务独立判断。通知里带上任务名,这样你一眼就知道是哪个任务完成了。
6.2 通知内容带上任务摘要
光说“任务完成”信息量太少,如果能带上任务的关键信息就更好了。我的做法是从日志文件的最后几行提取摘要,比如最后修改的文件名、生成的代码行数等,拼到通知内容里。
def extract_summary(logfile): with open(logfile, 'r') as f: lines = f.readlines() # 取最后 3 行非空内容 content = [l.strip() for l in lines if l.strip()][-3:] return ' | '.join(content)[:100]这样通知就变成了“任务完成:重构了 utils.py,新增 45 行,删除 12 行”,比干巴巴的“完成”有用得多。
6.3 与手机联动
如果你经常离开电脑,桌面通知就看不到了。可以把通知转发到手机上,方法是用一个简单的 HTTP 请求推送到手机的通知服务。具体用哪个服务这里不展开,思路就是监控进程检测到完成后,除了发桌面通知,再发一个网络请求到你的手机。
这个功能我用的不多,因为大部分时候我就在电脑附近,但如果你有远程监控的需求,值得加上。
6.4 统计与回顾
跑了一段时间后,我加了一个简单的统计功能:记录每个任务的开始时间、结束时间、运行时长,存到一个 CSV 文件里。周末回顾一下,能看出哪些任务耗时最长、哪些工具效率最高。这个数据对优化工作流很有参考价值。
import csv from datetime import datetime def log_task_stats(name, start, end): with open('task_stats.csv', 'a', newline='') as f: writer = csv.writer(f) writer.writerow([name, datetime.fromtimestamp(start).isoformat(), datetime.fromtimestamp(end).isoformat(), round(end - start, 1)])这个 CSV 用 Excel 或者 Numbers 打开就能看,不需要额外的可视化工具。
7. 我踩过的几个坑和最终体会
第一个坑是过度依赖进程存活检测。一开始我觉得进程没了就是任务结束了,简单可靠。但实际用下来发现,AI 编程工具经常会有子进程,主进程结束了子进程还在跑,或者反过来。后来改成进程存活和输出静默双重判断才稳定下来。
第二个坑是通知太频繁。刚开始我把静默阈值设成 3 秒,结果任务正常推理时也频繁弹通知,烦得我直接关掉了。阈值这个东西真的不能拍脑袋定,一定要观察实际任务的输出模式再定。
第三个坑是忽略了等待输入的情况。有次我跑一个需要确认的任务,去倒了杯水回来发现它卡在确认提示那里已经五分钟了。从那以后我才加了等待输入的检测逻辑,虽然不完美但至少能提醒我去看一眼。
这套东西我用了几个月,最大的体会是:工具的价值不在于多复杂,而在于能不能让你真正放心地去做别的事。一个简单的完成提醒器,解决的是心理层面的焦虑——你知道任务结束时会有人告诉你,所以你可以安心地去摸鱼、去开会、去干别的。这种“不用惦记”的感觉,比省下来的那几分钟时间值钱得多。
如果你也想搭一套,我的建议是从最简版本开始:先只做进程存活检测加桌面通知,跑通了再逐步加输出静默、等待输入检测、声音提醒这些功能。别一上来就追求大而全,能解决 80% 场景的简单方案,比一个复杂但跑不起来的完美方案有用得多。