一个挂在电脑上的自动打卡调度器:到点自动点亮手机、打开钉钉、点下打卡、截图留证,再把结果推到企业微信或邮箱。
它不是我从零写的,而是在一个优秀的开源项目上做的二次开发。这篇博客记录我改了什么、踩了哪些坑,以及最后怎么让它在 Windows / 统信 UOS / 通用 Linux / macOS 上都能装。
一、缘起:一个每天 2 次的重复动作
事情很简单:每天早上 8:30、下午 17:45,我要在手机上打卡。次数不多,但每天都得记着——开会开到一半想起来、出差在高铁上想起来,都是麻烦事。
我想要的不是"提醒我去打卡",而是这件事根本不需要我参与:电脑一直开着,到点它自己去操作手机,打完卡把截图发通知给我,我只需要偶尔瞄一眼结果。
这类工具的核心只有一件事:通过 ADB 驱动 Android 手机完成一串固定动作。难点不在"点一下",而在"每天稳定地、不出岔子地点这一下"。
搜了一圈,找到了一个很合我胃口的开源项目——TaskDispatcher,作者 AndroidCoderPeng,Qt 5 + C++ 写的跨平台桌面端,已经把整套骨架搭好了:
- 三层任务调度:预设时间点 → 随机偏移 → 节假日跳过;
- 完整 ADB 交互链:亮屏、滑动解锁、打开 App、截屏、压缩、杀进程、息屏;
- 双通知通道:QQ 邮箱 SMTP/SSL + 企业微信 Webhook 机器人;
- 无线 ADB:TCP/IP 连手机,5 秒轮询连接状态;
- 节假日数据:自动同步中国法定节假日(含调休补班),多 CDN 故障转移、按年缓存。
这基本上就是我想要的全部能力。所以与其重写,不如在它基础上做重构和打磨——这也就是后来被我改名为「TD-打卡」的东西。
本文结构:先讲我改了哪些工程上的东西(二~四节),再讲最有技术含量的跨平台打包(第五节),最后是踩坑清单(第六节)和致谢。
二、技术栈与整体架构
先说清楚底座,因为后面很多坑都和它有关。
| 项目 | 选型 |
|---|---|
| 语言 | C++17 |
| UI 框架 | Qt 5.15(Widgets) |
| 构建 | qmake(.pro) |
| 数据库 | SQLite(任务持久化) |
| 配置 | JSON(task_config.json) |
| 设备控制 | ADB 37.0.1(随包分发,无需自装) |
| 通知 | Qt Network(HTTP/HTTPS) |
架构上是典型的分层 + 单例:
DispatcherApplication (QApplication)
└── MainWindow
├── TaskExecutor ← 三层调度引擎
│ └── TaskStore ← SQLite 任务存储
├── ProcessExecutor ← ADB 命令执行器(亮屏/解锁/截屏/打开/杀进程/息屏)
├── MailSender ← QQ 邮箱 SMTP/SSL
├── WxMessageSender ← 企业微信 Webhook
├── ChinaHolidayManager ← 节假日同步与查询
├── ImageProcessor ← PNG 阶梯压缩(1920→1600→…→800,压到 2MB 内)
├── ConfigStore ← JSON 配置(线程安全单例)
├── ToastWidget ← 非阻塞提示
└── TaskItemWidget ← 任务列表项
执行流程(这才是这个软件的灵魂):
任务触发
→ 亮屏 adb shell input keyevent KEYCODE_WAKEUP
→ 取分辨率 adb shell wm size
→ 滑动解锁 (底部 4/5 → 顶部 1/5)
→ 等待 3 秒稳定
→ 打开 App adb shell monkey
→ 等待 N 秒(可配置,默认 30)
→ 截屏 adb exec-out screencap -p
→ 阶梯压缩 PNG(目标 ≤ 2MB)
→ 发送通知(企微图片 / 邮件附件)
→ 关闭 App adb shell am force-stop
→ 有下一个任务就继续调度,否则等次日重置
一句话概括:用 ADB 把"人手点一下"这件事拆成了 8 条命令,再用一个调度器把它按时间串起来。
三、一次重构:把「能用」变成「好用、好分发」
原版功能很完整,但它是为作者自己用写的。我的需求多了一条:要能安全地发给同事用。于是有了下面这些改动。
3.1 配置脱敏:导出配置不能带走凭据
分发场景是这样的:我把配置导出成一个 JSON,连同程序文件夹一起发给你,你导入后就能复用我的全部设置(打卡时间、APP 包名、行为开关、通知模板……)。
问题来了——task_config.json 里有两个字段是凭据:
| 字段 | 含义 |
|---|---|
emailConfig.authCode |
邮箱 SMTP 授权码 |
wxConfig.wxKey |
企业微信机器人 WebHook Key |
如果原样导出,拿到文件的人就能冒用我的通知通道(往我的企微群里发消息、用我的邮箱发信)。这不是小事。
我把它抽成了一个独立、可测试的规则模块 ConfigMasking.hpp:
namespace ConfigMasking {
inline constexpr const char *kMaskedValue = "******MASKED******";
inline bool isSensitiveField(const QString &cfgKey, const QString &field) {
return (cfgKey == QLatin1String("emailConfig") &&
field == QLatin1String("authCode")) ||
(cfgKey == QLatin1String("wxConfig") &&
field == QLatin1String("wxKey"));
}
// 导出:敏感字段 → 占位符
inline int maskSensitive(QJsonObject &obj, const QString &cfgKey);
// 导入:占位符 → 保留本机已填的值;本机也没有就删掉,让用户自己填
inline int unmaskWithLocal(QJsonObject &obj, const QString &cfgKey,
const QJsonObject &local);
}
这里有个容易翻车的细节:ConfigStore::save() 是整键覆盖语义。如果导入时看到占位符就直接写入,会把对方机器上原本填好的授权码覆盖成 ******MASKED******——对方第二天就收不到通知了,而且完全不知道为什么。
所以导入方向必须是 unmaskWithLocal():占位符 → 还原成本机原值;本机也没填过 → 删掉该字段,让用户自己补。
这套逻辑由 test_config_masking.cpp 单测覆盖「A 机导出 → B 机导入」全闭环,18 条断言。
3.2 通知模板:自动分流 markdown / HTML / 纯文本
企微和邮件对"富文本"的要求完全不同:企微机器人吃 markdown(### 标题 + <font color="info">),邮件吃 HTML。
如果让用户自己选"我这条模板是 markdown 还是 HTML",体验很糟。所以我做成了发送时自动判断:
| 模板里出现 | 走哪条通道 |
|---|---|
# / ** / <font> / [文字](链接) |
企微 markdown |
<div / <html |
HTML 邮件 |
| 以上都没有 | 纯文本 |
可用占位符:{title} {task_name} {time} {date} {status} {device} {app} {remark},两套默认模板都支持预览和恢复默认。
3.3 统一设置中心:所有要填的东西集中在一处
原版的配置散在各菜单里。改版后收到一个「统一设置中心」对话框,左侧 6 页导航,每页顶部带一行灰色说明文字:
| 页面 | 填什么 |
|---|---|
| 目标 APP | 打卡 APP 包名(默认钉钉;可"从手机导入"或快捷选择) |
| 设备连接 | 手机 WiFi 调试地址、连接状态、自动保活 |
| 打卡任务 | 一天打几次、每次几点;支持导入导出 |
| 通知设置 | 企微 Key / 邮箱授权码 + 两套模板 |
| 行为开关 | 跳过节假日、随机延时、循环执行、暗色主题等 9 个 |
| 在线配置 | 在线配置服务器地址 |
配合一个「配置管理」对话框(16 个配置项的中文名 + 当前值 + 说明全列出),把"翻菜单找一个开关"变成了"打开一个页面就全看到"。
3.4 在线配置:不发版也能改行为
软件「关于」页可以连一个自建的配置服务(一个 PHP 接口),用来在线下发配置项——通知模板、是否跳过节假日、随机延时策略、保活策略等等。
这样以后调整默认行为不需要重新打包发版,改服务器上的 JSON 即可。接口同时提供人可读版本(?format=html),方便在浏览器里直接看当前下发的内容。
四、其他几处改动
| # | 改动 | 原因 |
|---|---|---|
| 1 | 软件名 / exe 名 / 窗口标题 → 「TD-打卡」 | 区分于原版,避免和上游混淆 |
| 2 | 移除手机实时画面 | 删掉 VideoDecoder / MirrorWindow / ScrcpyBridge / WebMirrorServer / InputInjector 五个模块及全部调用点,明显减小体积与依赖 |
| 3 | QQ 风格美化 | 亮/暗双 QSS 重写:#F2F3F5 底、白卡片、圆角、主色 QQ 蓝 #12B7F5 |
| 4 | 关于页重构 | 改为展示在线配置服务器 + 版本信息 |
| 5 | 字体处理 | Windows 内嵌微软雅黑;Linux/macOS 改用系统 CJK 字体(PingFang SC / Noto Sans CJK SC / 文泉驿),规避字体版权 |
五、重点:一次提交,产出四个平台的安装包
这是我在这件事上花时间最多的部分,也最有意思。
5.1 为什么不能在 Windows 上直接交叉编译
我的开发机是 Windows。Linux 和 macOS 的产物没法在 Windows 上编出来:
- Qt 的 qmake 是平台绑定工具,MinGW 编不出 Mach-O 和 ELF;
- 交叉编译 Qt 桌面应用需要目标平台完整的 Qt 库和工具链,配置成本远高于收益;
- 本机没有 Docker / WSL / 虚拟机可用。
所以路线是:用 GitHub Actions 云端 CI(免费额度足够),在真实的目标系统上编译、打包、验证。
三个 job:
| Job | Runner | 产物 |
|---|---|---|
| Linux | ubuntu-24.04 宿主 + debian:10 容器 |
.deb + .AppImage |
| macOS Intel | macos-15-intel |
.dmg(x86_64) |
| macOS ARM | macos-15 |
.dmg(arm64) |
5.2 坑一:Linux 二进制只能「向下兼容」,所以必须在 Debian 10 里编
统信 UOS 20 / Deepin 20 基于 Debian 10,glibc 2.28。而 Linux 二进制的兼容规则是:高版本 glibc 编出来的程序,装不进低版本系统。
如果我在 Ubuntu 22.04(glibc 2.35)上编,产出的包装到 UOS 上会直接报:
version `GLIBC_2.35' not found
解法:在 debian:10 容器里编译。容器内 gcc 决定的 glibc 版本是 2.28,产物就能一路往上兼容 Ubuntu 18.04+ / UOS 20 / Deepin 20。
顺便踩了个小坑:Debian 10(buster)已经进 archive,
deb.debian.org的源会 404。要切到archive.debian.org并加Acquire::Check-Valid-Until=false。
5.3 坑二:Qt 版本地板 —— 源码要求的 API 比 Debian 10 自带的还新
Debian 10 自带的 Qt 是 5.11.3,但源码里用到了两个更新的 API:
// Qt 5.14+ 才有
str.split(",", Qt::SkipEmptyParts);
// Qt 5.15+ 才有
request.setTransferTimeout(5000);
7 处 + 15 处,直接用 buster 的 Qt 编必挂。
两条路:改源码降级 API(要动 20 多处、且改变行为),或者用新版 Qt。我选后者:在宿主上用 aqtinstall 拉 Qt 5.15.2(和 Windows 版同版本),bind-mount 进容器。
好处是源码零改动,而且跨端行为一致(同一个 Qt 版本)。Qt 官方 Linux 预编译包是 RHEL7 构建的(要求 glibc 2.17),在 Debian 10 容器里能正常跑,产物 glibc 需求仍由容器内 gcc 决定(2.28),不影响兼容性。
5.4 坑三:linuxdeployqt 已停止维护,且会「静默失败」
Linux 打包 AppImage 的常规做法是 linuxdeployqt。但它 2019 年后基本没人维护,最要命的是:找不到 Qt 插件时它可能不报错,只是打包个"空壳"出来——你拿到几百 MB 的 AppImage,拷到别人机器上一点,报"找不到平台插件 xcb"。
我把它换成了一个确定性的部署函数,逻辑完全自己控制:
qt_libs="$(qmake -query QT_INSTALL_LIBS)"
qt_plugins="$(qmake -query QT_INSTALL_PLUGINS)"
# 关键:用 cp -a 而不是 cp -L
# .so.5 是符号链接,指向 .so.5.15.2 —— 必须保留链接本身,
# 用 -L 解开会让目标机找不到 soname
for so in "$qt_libs"/lib*.so.*; do
cp -af "$so" "$appdir/usr/lib/"
done
# 硬校验:这三个 soname 缺一个,包在目标机必崩
for so in libQt5Core.so.5 libQt5Gui.so.5 libQt5Widgets.so.5; do
[ -e "$appdir/usr/lib/$so" ] || die "致命:$so 缺失"
done
# 平台插件是 AppImage 的命门,缺了直接失败,绝不产出坏包
[ -f "$appdir/usr/plugins/platforms/libqxcb.so" ] \
|| die "platforms/libqxcb.so 没部署成功"
核心原则:宁可构建失败,也不要产出一个"看起来成功但打不开"的包。
5.5 坑四:macdeployqt 没有 -no-translations 这个参数
这个坑最典型,也是我最该记下来的。
macOS 的 dmg 一开始两个 job 都显示 success,我差点就信了。直到我加了硬校验(必须存在 Contents/Frameworks/QtCore.framework)才发现:
dmg 只有 18 MB —— 里面只有主程序,一个 Qt 框架都没有。
没有框架的 dmg,拷到任何没装 Qt5 的 Mac 上必然打不开。之前的"成功"是假的,因为我写了 || true 把失败吞掉了。
翻原始日志,真凶在这一行(缩进的,我第一遍排查时被自己的关键字过滤滤掉了):
ERROR: Unknown argument "-no-translations"
我把 windeployqt(Windows 版) 的参数抄给了 macdeployqt(macOS 版)。
windeployqt有--no-translations;macdeployqt没有这个参数,它总是部署翻译。
传了未知参数,它立刻退出,什么都不部署。去掉之后:
| dmg 体积 | 结果 | |
|---|---|---|
| 修复前 | 18.3 MB | 空壳,打不开 |
| 修复后 | 37 ~ 39 MB | 框架 + 插件齐全 ✅ |
顺手加了第二条硬校验:Contents/PlugIns/platforms/libqcocoa.dylib 必须存在。框架在但缺 cocoa 插件时,目标机会报"找不到 Qt 平台插件 cocoa"——这比缺框架更隐蔽。
教训:排查构建问题时,先看原始日志,别先过滤。 我用关键字过滤日志,恰好把唯一的那行错误滤掉了,白白多烧了一轮 CI。
5.6 在 Windows 上怎么验证 dmg 真的有框架
这是最后一道关。macOS 的 dmg 是 UDZO(zlib 压缩的 UDIF 镜像)。我的 Windows 机器上:
- 没有
hdiutil(macOS 专属); - 没有能读 dmg 的 7-Zip;
py7zr不支持 dmg。
但我又不想仅凭"体积 ≥ 28MB"这种启发式就下结论——那正是当初被骗的原因。于是我写了个纯 Python 的 UDZO 解包器:
- 读文件末尾 512 字节的
koly尾块,取XMLOffset / XMLLength; - 解析出 blkx plist(注意:顶层 key 是
resource-fork,不是blkx); - 遍历
mish分块描述符(起始偏移 204),按压缩类型逐块解压 (0x00000001= 原样,0x80000005= raw deflate); - 把镜像摊开后,直接在字节流里搜关键字。
结果(只解出 1.99 MB 就命中了):
libqcocoa.dylib ← macOS 平台插件 ✅
QtCore.framework ← Qt 核心框架 ✅
libqjpeg.dylib ← JPEG 图像格式 ✅
libqmacstyle.dylib ← 原生风格插件 ✅
这是硬证据,比"体积猜"可靠得多。
5.7 CI 还要顺手做真机级验证
Linux job 在打完包之后,会真的把 deb 装一遍:
apt install -y ./td-daka_1.0.0_amd64.deb
xvfb-run -a td-daka # 无显示环境用虚拟屏启动
# 确认能起来、确认日志文件写出来了
能打包 ≠ 能运行。这一步能在 CI 里直接暴露"缺依赖""缺插件""路径不可写"这类问题。
另外,非 Windows 平台的坑有个共同特点:代码写在 #ifdef Q_OS_WIN 的 #else 分支里时,Windows 编译根本不会碰它,所以错误只会在 CI 上第一次暴露。这次就抓到两个:
QFontDatabase::families()在 Qt5 里是非静态成员(Qt6 才变静态),写成QFontDatabase::families()报 "call to non-static member function without an object argument";ConfigStore/Logger在原版里把配置和日志写到程序所在目录——Windows 便携版没问题,但 macOS 是写入 .app 包内,会破坏代码签名。所以非 macOS 才走"便携模式",macOS 一律用系统用户目录。
六、踩坑清单(可复用的经验)
| 坑 | 现象 | 正解 |
|---|---|---|
| glibc 向下兼容 | UOS 上报 GLIBC_2.35 not found |
在最低目标系统(Debian 10)的容器里编 |
| Debian 10 源 404 | apt 全挂 |
切 archive.debian.org + 关掉 Valid-Until 检查 |
| Qt 版本地板 | Qt::SkipEmptyParts 编不过 |
容器里额外装 Qt 5.15.2,源码零改动 |
| linuxdeployqt 静默失败 | 打了空壳 AppImage | 自己确定性部署 + 硬校验平台插件 |
| macdeployqt 未知参数 | dmg 只有 18MB,秒退 | 删掉只属于 windeployqt 的 -no-translations |
cp -L 解开符号链接 |
目标机找不到 libQt5Core.so.5 |
用 cp -a 保留 .so.N 链 |
|| true 吞掉失败 |
假成功 | 关键步骤硬失败,不产出坏包 |
脚本里的 $0 层级 |
qmake 找不到 .pro |
BASH_SOURCE[0] 上溯正确层级 + 断言校验 |
| 日志写到 stdout | VAR="$(func)" 把日志当返回值 |
诊断输出一律走 stderr |
| 下载只判"文件存在" | 半包 zip 被当成成功 | 体积比对 + zipfile.testzip() CRC 校验 |
docker run bash -c '…' |
unexpected EOF |
内层脚本别出现裸单引号 |
七、最终成果
四个平台、一次提交、云端产出:
| 平台 | 产物 | 体积 |
|---|---|---|
| 统信 UOS 20 / Deepin 20 / Ubuntu 18.04+ | td-daka_1.0.0_amd64.deb |
37.43 MB |
| 其他 Linux(免安装) | TD-打卡-1.0.0-x86_64.AppImage |
49.98 MB |
| macOS Intel | TD-打卡-1.0.0.dmg |
39.00 MB |
| macOS Apple Silicon | TD-打卡-1.0.0-arm64.dmg |
37.29 MB |
每个都逐项校验过:deb 是 288 个条目 + 22 个依赖声明 + platforms/libqxcb.so 在位;AppImage ELF 头确认 x86_64;两个 dmg 解包后能搜到 Qt 框架和 cocoa 插件。
唯一的遗憾是 macOS 没有做代码签名和公证(个人开发者证书每年几百元),所以首次打开会被 Gatekeeper 拦一下,右键 →「打开」即可。
📥 下载(本站「下载专区」)
Windows 版已经上架本站下载专区,点下面的链接直接下载使用:
- 下载专区总入口:https://jz.j520m.site/downloads.php — 打开后往下翻,找到「钉钉打卡」那张卡片即可
- Windows 版①:https://jz.j520m.site/s.php?c=7CAY4D
- Windows 版②:https://jz.j520m.site/s.php?c=Y7SrHq
下载专区里每个版本首次下载还能领积分(积分可在会员中心抽奖、兑换)。下载专区目前上架的是 Windows 两个版本。
八、致谢:感谢开源作者
这个项目里,真正难的部分不是我写的。
「用 ADB 驱动手机完成一串固定动作 + 三层调度 + 双通道通知 + 节假日识别」这一整套骨架,是 AndroidCoderPeng 一行一行写出来并且开源出来的。它把"从零开始"变成了"在此基础上打磨"。我做的更多是:
- 把配置收拢成统一入口,把凭据脱敏做成可测试的模块;
- 重写视觉、精简体量;
- 以及最花时间的——让它能跨四个平台打包分发。
没有 TaskDispatcher,就不会有 TD-打卡。
在此郑重致谢:
TaskDispatcher · 作者 AndroidCoderPeng 项目地址:https://github.com/AndroidCoderPeng/TaskDispatcher 邮箱:290677893@qq.com
感谢你把这样一个完整、扎实、真正能用的工具开源出来。它的三层调度设计和完整的 ADB 交互链,是这个项目的地基,也是我学到最多的地方。
如果你也正好有自动打卡这类需求,强烈建议去原仓库点一个 Star——开源作者最需要的就是这个。
另外也感谢 Qt 社区、Android platform-tools 团队,以及 chinese-days 节假日数据包的维护者,这些基础设施让个人开发者能做出真正可用的桌面工具。
本项目仅供学习和个人使用。请遵守你所在单位关于考勤的规定。

留言 0