🎉 小程序已上线!点击查看详情

我把一款开源打卡工具「重做」了一遍:从重构到四平台安装包

我把一款开源打卡工具「重做」了一遍:从重构到四平台安装包

一个挂在电脑上的自动打卡调度器:到点自动点亮手机、打开钉钉、点下打卡、截图留证,再把结果推到企业微信或邮箱。

它不是我从零写的,而是在一个优秀的开源项目上做的二次开发。这篇博客记录我改了什么、踩了哪些坑,以及最后怎么让它在 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 解包器:

  1. 读文件末尾 512 字节的 koly 尾块,取 XMLOffset / XMLLength;
  2. 解析出 blkx plist(注意:顶层 key 是 resource-fork,不是 blkx);
  3. 遍历 mish 分块描述符(起始偏移 204),按压缩类型逐块解压 (0x00000001 = 原样,0x80000005 = raw deflate);
  4. 把镜像摊开后,直接在字节流里搜关键字。

结果(只解出 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 版已经上架本站下载专区,点下面的链接直接下载使用:

下载专区里每个版本首次下载还能领积分(积分可在会员中心抽奖、兑换)。下载专区目前上架的是 Windows 两个版本。

八、致谢:感谢开源作者

这个项目里,真正难的部分不是我写的。

「用 ADB 驱动手机完成一串固定动作 + 三层调度 + 双通道通知 + 节假日识别」这一整套骨架,是 AndroidCoderPeng 一行一行写出来并且开源出来的。它把"从零开始"变成了"在此基础上打磨"。我做的更多是:

  • 把配置收拢成统一入口,把凭据脱敏做成可测试的模块;
  • 重写视觉、精简体量;
  • 以及最花时间的——让它能跨四个平台打包分发。

没有 TaskDispatcher,就不会有 TD-打卡。

在此郑重致谢:

TaskDispatcher · 作者 AndroidCoderPeng 项目地址:https://github.com/AndroidCoderPeng/TaskDispatcher 邮箱:290677893@qq.com

感谢你把这样一个完整、扎实、真正能用的工具开源出来。它的三层调度设计和完整的 ADB 交互链,是这个项目的地基,也是我学到最多的地方。

如果你也正好有自动打卡这类需求,强烈建议去原仓库点一个 Star——开源作者最需要的就是这个。

另外也感谢 Qt 社区、Android platform-tools 团队,以及 chinese-days 节假日数据包的维护者,这些基础设施让个人开发者能做出真正可用的桌面工具。


本项目仅供学习和个人使用。请遵守你所在单位关于考勤的规定。

留言 0

本站已设置「登录后才能留言」 去登录
还没有留言,来做第一个吧
留言需通过图形验证码校验;首次留言、含链接或含图片的留言会先进入审核,通过后才会显示。
Tuzhi记账本
全面守护您的上网安全,请在电脑端下载