这套脚本的出发点其实没那么宏大,就是为了应付打卡。
说直白一点,我本来就是个经常迟到的人。考勤弹性还算大,不太会因为晚几分钟就出什么事,但飞书里的记录不会陪你演戏:迟到就是迟到,缺卡就是缺卡,一直挂在那里,看久了还是挺难看的。
而且我不只是会迟到,还有概率忘记打卡。有时候人已经到公司了,一坐下就开始回消息、开会、处理事情,等想起来的时候,考勤记录已经不好看了。
所以我想得很直接:既然这件事本身没有价值,又每天都要重复,那就把它自动化掉。
问题在于,飞书考勤不是网页,也没有现成 API 可以调。你没法像调一个 HTTP 接口那样直接“提交打卡”,必须真的有个东西去打开 App、进入考勤页面、看当前状态、再决定要不要点按钮。
我手边刚好有一台长期插在 Mac 上的备用 Android 测试机,AutoGLM 也刚开源不久。它比较适合这种场景:给它一句自然语言指令,它会自己截图、理解界面、点按钮。我需要补上的,不是“怎么识别页面”,而是外围那一圈更脏的工程活:
- 什么时候跑
- 今天到底是不是工作日
- 不同时段怎么随机出一个像人类的时间
- 手机怎么唤醒、解锁、启动飞书
- 如果今天已经打过卡了,怎么确保别再点第二次
- 跑完之后怎么留痕,方便我事后核对
换句话说,AutoGLM 只负责最后那一下“在飞书里完成打卡”,剩下的调度、状态和设备控制,都是我自己补的。
整体架构
整条链路其实很朴素:
Mac 上的 launchd 拉起 Python 调度器,调度器每天重新生成当天的随机打卡时间;到点之后,通过 ADB 把 USB 连着的 Android 手机唤醒、解锁、拉起飞书,再把操作指令交给 AutoGLM;执行完以后保存截图、更新状态文件,最后把手机和电脑屏幕都收回去。
所以这不是一个“AI 自己在后台神奇地完成一切”的系统,本质上还是传统自动化,只不过把最难写死规则的那段 UI 操作,交给了 Agent。
这套脚本每天是怎么跑的
我现在的配置是上午和晚上各打一轮,时间都不是固定点,而是在窗口里随机:
- 上午:
10:19到10:27 - 晚上:
20:00到20:07
调度器会在每天 00:01 先把当天任务重新排一遍。这样第二天的执行时间会重新随机,不会每天都卡在同一分钟。
真正触发时,流程大概是这样:
-
先判断今天是不是工作日
这一步不是简单看周几。我接了timor.tech的节假日 API,优先按官方调休数据判断;如果接口挂了,再回退到本地的周一到周五逻辑。 -
再看这个时段今天是不是已经打过了
状态会写到logs/checkin_status.json。如果当天这个时段已经成功过,后面就什么都不做,直接退出,避免重复打卡。 -
准备手机环境
通过 ADB 唤醒屏幕、输入锁屏密码、确认设备连通;然后把飞书进程先干掉,再从首页重新拉起,尽量避免 App 留在一个奇怪页面。 -
把最后的页面操作交给 AutoGLM
指令里会明确告诉它:去哪里找打卡入口,看到“上班打卡”或“下班打卡”时才点击;如果页面已经显示“已打卡 XX:XX”,就直接退出;如果看到“缺卡”或者其他异常状态,也不要乱点,更不要申请补卡。 -
收尾
无论成功与否,都会尽量截图留档;然后写日志、更新状态文件,最后把手机熄屏,Mac 这边也让显示器休眠。
整个流程里,最像“AI”的只有中间那一步;前后基本都是普通脚本在兜底。
几个地方如果不处理好,会很容易翻车
1. 工作日判断不能只看周一到周五
这是最开始就踩到的坑。
如果只是简单判断星期几,那碰上调休一定会出问题。周六可能要补班,某个工作日也可能因为节假日安排变成休息日。对“自动打卡”这种东西来说,时间判断错一次,就不是小问题。
所以我把国务院节假日数据接进来了,按年缓存到本地,缓存有效期是 30 天。这样平时不会天天请求接口,接口偶尔挂了也还能用旧缓存兜住。
2. 时间必须随机,不然太假
固定在同一分同一秒执行,日志看起来就很不像人。
所以我给上午和晚上各配了一个很窄的时间窗口,每天凌晨重新抽一次。它不会偏离太远,但也不会每天都一模一样。这个改动很小,效果却很直接,日志读起来顺眼得多。
3. 幂等性一定要做
Agent 不是每次都稳定。
有时候是 ADB 没连上,有时候是飞书页面加载慢了,有时候是 AutoGLM 没找到入口就超时。如果失败后没有状态控制,脚本一重跑,就可能在已经打过卡的情况下再点一次。
所以我这里的原则很死板:只要当天这个时段已经记录为成功,就绝不再执行。
状态文件大概长这样:
{
"2026-04-15": {
"morning": {
"success": true,
"message": "morning 打卡流程结束",
"timestamp": "2026-04-15 10:24:37"
}
}
}
这种本地 JSON 很土,但对这种单机脚本来说完全够用,而且排查起来非常直接。
4. 给 AutoGLM 的指令不能只写“帮我打卡”
这一点也很关键。
如果你只丢一句“帮我打卡”,Agent 会自己理解,但它的理解不一定和你要的一样。比如页面已经显示“已打卡”,它可能还会继续尝试操作;再比如看到“缺卡”,它可能会顺手去点补卡相关入口。
所以我的做法不是写一句目标,而是把边界写清楚:
只有看到蓝色打卡按钮时才点击;如果已经显示已打卡,就退出;如果是缺卡或其他异常状态,不要处理,直接结束任务。
这种约束写清楚以后,稳定性提升很明显。对 Agent 来说,“不要做什么”往往和“要做什么”一样重要。
5. 截图和日志不能省
自动化一旦出问题,最怕的是你根本不知道它停在哪一步。
所以每次执行后我都会尽量保留截图,日志里也会把调度、ADB 状态、当前包名、Agent 执行结果这些关键信息打出来。这样哪天记录不对,我至少能很快判断:
- 是不是 USB 线松了
- 是不是手机没解锁成功
- 是不是飞书没拉起来
- 还是 Agent 在页面里迷路了
真正排查时,截图比一句“失败了”有用得多。
这三个月跑下来怎么样
从日志看,主要的失败原因其实很集中:
- ADB 断连,比如 USB 接触不稳
- 飞书页面没到预期状态,导致 Agent 超时
这两类问题都不算“逻辑写错”,更像是外部环境不稳定。脚本本身做的事情,主要就是在这些不稳定外面再包一层保护:工作日判断、随机时间、幂等、截图、日志、失败后下次继续跑。
跑了三个月,整体成功率大概在 95% 左右。对一个靠 ADB + 手机 + Agent 串起来的小工具来说,我已经挺满意了。
更重要的是,这件事从“我每天都得记着”变成了“它自己跑,偶尔我看一眼日志”。考勤记录没那么难看了,我也不用每天早上给自己加一道没意义的提醒。
如果你也碰到类似场景:某个手机 App 里有一段固定流程,没有公开 API,但你又不想为这件事手搓一整套 UIAutomator 规则,那 AutoGLM 这种 Phone Agent 方案确实值得试一试。
它不神奇,也不是什么银弹。真正让这件事能长期跑起来的,还是外围那些看起来不酷、但必须做扎实的工程细节。