missed-task-guard —— 定时任务错过执行后的自动补跑
用途与适用场景
WorkBuddy 定时任务由本机执行:电脑关机、休眠或客户端退出时,任务直接跳过且无原生 catch-up、无开机触发。本技能提供一个平台无关的守护层:系统恢复可用(登录/唤醒/定点)后自动校验当天任务是否已完成,未完成即补跑一次,并保证一天内最多补跑一次。
- 安装后无需任何人工操作:登录电脑即自动检查。
- 任务由注册表
references/tasks.json驱动,可注册任意本机定时任务(不限 WorkBuddy)。 - 内置实例:
buddy-daily:WorkBuddy「Buddy 每日任务(签到 + 派喵 + 收奖励)」buddy-claim:派喵后 +4h5m 到点领取旅行奖励checkin:WorkBuddy 签到助手(与 buddy-daily 并存双保险)
机制三要素(核心设计)
1. 校验时机(何时检查)
多个独立触发源,任一命中即校验一次;guard 内部再做"到点判断"(未到计划时间绝不提前跑):
| 触发源 | 时机 | 作用 |
|---|---|---|
| WorkBuddy 自动化(开机/登录即补) | 每次进入系统后 | 主补跑时机:电脑一恢复可用就校验当天漏掉的任务,含已过点的领奖 |
Windows 计划任务 ONLOGON |
每次登录后 45 秒 | 同上;本机 schtasks 受限时不可用(见"schtasks 可用性"一节) |
Windows 计划任务 DAILY(多个兜底时刻,带 StartWhenAvailable) |
每个任务声明的 catchup_daily 时刻(缺省 13:00) |
电脑在线但客户端没跑 / 主任务失败时兜底;睡眠错过也能在唤醒时补 |
| WorkBuddy AI 自动化(保留原任务) | 按各自 rrule | 正常主跑;成功后调用 mark-done 落完成标记 |
| 固定时点兜底自动化 | 16:10 / 20:10 等 | 一次性任务被错过时补上 |
> 为什么必须有"开机即补":领奖这类任务有固定触发时刻(如 16:07),若该时刻电脑处于
> 关机/休眠,触发器不会排队等待,任务直接丢失、当天再无第二次机会。开机即补把"电脑恢复
> 可用的那一刻"变成一次兜底校验,只要当天 23:59 前开过机就能补上。
> 日志实证:2026-09-19 13:07 关机 → 20:50 开机,16:07 的领奖与 16:10 的兜底双双落空。
> 每日兜底触发器不是写死的单一 13:00,而是从 references/tasks.json 各任务的
> catchup_daily 自动汇总去重(最多 3 个)。因为像 buddy-claim 这种"到点更晚"的任务
> (16:10 / 16:30),只靠 13:00 定点是永远补不到的。
只补当天:跨午夜登录不补昨天(buddy 类按服务端自然日计,补昨天=语义错)。
2. 重复触发的防护(一天内最多补跑一次)
四层防御:
1. 状态文件 done 标记(主闸门):状态目录下 state_YYYY-MM-DD.json 中 done=true 后,当日任何触发源一律零执行退出。
2. 完成白名单 done_actions:见下文——防止把"未到点"误判成"已完成"而挡死后续触发。
3. 任务脚本幂等:buddy_daily.py 服务端状态权威,即便极端竞态重复执行也只得 skip_*,无副作用。
4. 锁文件互斥 + 失败限次:并发实例互斥(O_EXCL 锁,陈旧 300s 可抢占);status=error 计数 attempts,达 attempts_max 当日放弃;识别到登录态/token/401 错误 → token_error=true 当日停试(同日重试无意义,token 由客户端刷新)。
2.1 完成白名单 done_actions
问题:旧逻辑"退出码 0 且 status≠error 即视为完成",对「到点才做」的任务是错的。
buddy-claim(派喵后 +4h5m 到点领奖)在 13:00 兜底触发时必然"未到点",脚本会
正常返回 skip_not_due(退出码 0)。若照旧写 done=true,后续 16:00 之后的真正领奖
触发就全被 already_done 挡死了——任务永远收不到奖励。
解法:在 tasks.json 里给该任务声明白名单,只有命中白名单的 action 才算完成:
"buddy-claim": {
"scheduled_time": "16:05",
"command": ["{PY}", "{HOME}/.workbuddy/scripts/buddy_daily.py", "--claim-due"],
"catchup_daily": "16:30",
"done_actions": ["claimed", "skip_no_pending"]
}
| 任务输出的 action | 在白名单内? | guard 判定 | 是否写 done | 是否计失败 |
|---|---|---|---|---|
claimed(领到了) |
✅ | ran_ok |
✅ 是 | 否 |
skip_no_pending(无待领/已领过) |
✅ | ran_skip |
✅ 是 | 否 |
skip_not_due(未到点) |
❌ | pending_retry |
❌ 否 | ❌ 否 |
skip_not_arrived(服务端还没返回) |
❌ | pending_retry |
❌ 否 | ❌ 否 |
> 不配 done_actions 的任务(如 buddy-daily、checkin)保持旧行为:退出码 0 且
> status≠error 即完成(skip_* 一视同仁)。向后兼容,无需改动既有任务。
3. 状态记录与判断逻辑
- 状态文件
~/.workbuddy/state/missed-task-guard/state_YYYY-MM-DD.json(自然日滚动,自动保留 14 天):
{"date": "2026-09-04", "tasks": {"buddy-daily": {
"done": true, "done_at": "13:01:37", "done_source": "automation",
"attempts": 1, "last_attempt_at": "13:01:37",
"last_action": "clicked+departed", "last_error": "", "token_error": false}}}
- 判断流程(
run):到点?→ 读今日状态 →done则退出 → 执行任务命令(timeout)→ - 退出码 0 且 status≠error 且(无白名单 或 action ∈ 白名单)→ 写
done=true - 退出码 0 但 action 不在白名单 →
pending_retry(不写 done、不计失败、不计 attempts) - 否则 → attempts+1、记 last_error,达上限/遇 token 错误当日停试。
- 损坏容错:状态文件损坏自动归档
*.corrupt-<ts>并重建,不中断补跑。
依赖与环境自检(何时检查 / 如何路由 / 何时降级)
本技能是依赖型技能,进入正常调度前先跑一次只读检查:
python scripts/check_environment.py
何时检查:首次部署后、换机后、出现 failed / token_error_stop 时、改动 references/tasks.json 之后。
如何路由:
| 输出状态 | 处理 |
|---|---|
ready |
直接进入调度,不展示任何安装说明 |
partial |
正常调度,仅说明受影响功能与降级范围(典型:schtasks 受限时原生触发器不可用) |
needs_setup |
只展示缺失项(如任务脚本未部署)的配置步骤,完成后重检 |
unavailable |
展示故障原因与恢复步骤,按降级矩阵执行 |
何时降级:核心输入(注册表、解释器)缺失时停止调度;仅触发器或日志不可用时继续调度并注明降级范围。
机器可读的依赖与降级矩阵见 skill-dependencies.json,逐步配置见 @references/setup-guide.md。
运行期产物位置:状态、锁、日志与任务 XML 全部写在技能包之外
(~/.workbuddy/state/missed-task-guard/,可用 WORKBUDDY_GUARD_STATE_DIR 覆盖),
包内 scripts/ 只保留可执行 .py,保证技能包可被静态扫描、可干净打包。
使用方法
脚本:scripts/guard.py(纯 Python 标准库,零第三方依赖,pythonw 静默运行)
> ⚠️ 全部是子命令形式,不加前导 --。写成 --run 之类会被 argparse 判为未知参数而必然失败。
python guard.py install # 注册 Windows 计划任务(ONLOGON 45s + 各任务兜底时刻)
python guard.py uninstall # 卸载计划任务(恢复到未装状态)
python guard.py run # 到点校验并补跑(schtasks 自动调用,一般无需手动)
python guard.py run <task> # 只处理指定任务
python guard.py check # 只读检查:输出各任务今日判定,不执行任何命令
python guard.py status # 状态摘要(今日状态文件 + schtasks 注册情况)
python guard.py mark-done <task> [--source automation] # 主任务成功后落完成标记
python guard.py force-run <task> # 无视 done/上限强制补跑一次(排障用)
python guard.py clear-today # 清空今日状态(测试/维护)
python guard.py version # 版本号
输出为单行 JSON(run 的动作字段):ran_ok(真实补跑成功)/ ran_skip(执行了但任务本就 skip,也写 done)/ pending_retry(执行了但未到点/未就绪,不写 done、不计失败)/ already_done(当日已完成,零执行)/ not_due(未到点)/ failed / token_error_stop / attempts_exhausted。
⚠️ 本机 schtasks 可用性(重要前置检查)
guard 的 ONLOGON / DAILY 触发器依赖 Windows 计划任务(schtasks)。但实测本机:
schtasks /Create返回PermissionError(13, '拒绝访问。');schtasks /Query同样"拒绝访问";- WorkBuddy 的安全策略还会把
schtasks.exe列入 Program Blacklist,代理侧无法直接调用。
结论:schtasks 链路在本机当前不可用,guard 的 OS 级触发器实际处于"已实现但未生效"状态。
此时 guard 仅被动接收主任务的 mark-done,无法自行补跑。
替代路径:改用 WorkBuddy 自动化驱动 guard.py run(或直接跑任务脚本)——
自动化是本机可靠运转的调度层。推荐组合:
| 层 | 实现 | 作用 |
|---|---|---|
| 主(精确) | 12:00 主任务派喵后,读 buddy_daily.py --print-claim-due 的 claim_due_iso,用 automation_update 建/更新一条一次性自动化,定在该时刻跑 --claim-due |
精确到分钟的"+派喵 4h5m" |
| 补(开机即补) | 一条每日自动化,跑 guard.py run,负责在电脑恢复可用后立即校验 |
覆盖固定触发点落在关机/休眠时段的情况 |
| 兜底(固定) | 每日固定时点自动化(16:10 与 20:10 各一条)跑 guard.py run |
一次性任务被错过时补上,并复用 guard 的防重与白名单 |
| 收口 | 次日 12:00 主任务"先收后派" | 极端情况(跨午夜)最终收口 |
开机即补的搭建要点:建一条 recurring 自动化(FREQ=DAILY;BYHOUR=<较早时刻>;BYMINUTE=0),
prompt 里让它执行 guard.py run。电脑长时间离线后重新上线,该自动化会随客户端启动被触发,
guard 随即校验当天所有已过点的未完成任务并补跑;已完成的任务由状态文件挡住,不会重复执行。
> 平台无"开机触发"原生类型,故以"每天一次 + 客户端启动即补"的方式近似实现;
> 自动化因离线被跳过时,客户端重新可用后会补上当天这次执行。
> ⚠️ 平台 RRULE 的 BYHOUR 只接受单个 0~23 的整数,不支持逗号多值
> (FREQ=DAILY;BYHOUR=16,20 会被拒)。需要多个时点就建多条自动化,一条一个时刻。
> 若日后解除 schtasks 限制,guard.py install 即可恢复原生 ONLOGON + 定点补跑,无需改代码。
> 排查前先跑 python guard.py status 看 schtasks_registered:为 false/null 即说明触发器未生效。
搭建步骤(新机器/新任务)
- 把
scripts/与references/一并放到技能目录,确认references/tasks.json里命令用{PY}等占位符(guard 运行期替换为实际路径)。 - 首次自检:
python guard.py check(应输出各任务 verdict)。 - 实测:
python guard.py run(首跑落 done)→ 再run应already_done。 - 注册开机触发:
python guard.py install;核对schtasks /Query /TN WorkBuddyMissedGuard /V /FO LIST。
> 若报"拒绝访问",见上一节,改用 WorkBuddy 自动化驱动,不要反复重试。 - (可选)把主自动化 prompt 末尾加一句"成功后执行
python guard.py mark-done <task> --source automation",避免定点守护冗余执行。
注册新任务(通用框架)
编辑 references/tasks.json 追加条目:
"my-task": {
"note": "说明",
"scheduled_time": "09:00", // 任务原计划时间,HH:MM
"command": ["{PY}", "{HOME}/.workbuddy/scripts/脚本.py"], // 执行命令(用 {PY}/{HOME} 等占位符,勿写本机绝对路径)
"logon": true, // 声明式:期望登录后补跑
"catchup_daily": "13:00", // 声明式:期望的每日兜底时刻(会汇总进触发器)
"timeout_seconds": 180, "attempts_max": 3,
"done_on_skip": true, // 退出码0即视为完成(幂等任务恒 true)
"done_actions": ["claimed"] // 可选:完成白名单,仅这些 action 才算完成
}
> 约束:
> - 注册的任务命令必须幂等或可重复执行无副作用;若任务输出非标准 JSON,guard 仅按退出码判定。
> - 命令一律用占位符 {PY}/{HOME}/{SKILL_DIR}/{SCRIPTS_DIR}/{STATE_DIR} 书写,保证跨机器可移植。
> - tasks.json 顶层只能是 {任务id: 配置},不要加 _comment/_meta 之类的键——会被当成任务执行。
> - done_actions 只对单动作/可枚举动作的任务有意义;配置后 guard 优先取输出里的顶层
> action 字段,没有再合并 checkin.action + travel.action。
日志与排错
日志:~/.workbuddy/state/missed-task-guard/guard.log(格式 时间 | 级别 | 消息)。任务自身日志不受影响。
| 现象 | 原因与处理 |
|---|---|
not_due |
开机早于计划时间,属正常(等主任务或定点触发) |
already_done |
当天已完成,属正常防重 |
pending_retry |
已执行但未到点/未就绪(如 skip_not_due、skip_not_arrived),属正常;不写 done、不计失败,等下次触发 |
token_error_stop |
登录态失效(如客户端长期未开)。打开 WorkBuddy 刷新登录态后 force-run |
attempts_exhausted |
当天失败 3 次放弃。查 guard.log 与任务日志定位原因,修复后 force-run |
| 计划任务没触发 | python guard.py status 看 schtasks_registered;本机若受限(拒绝访问)则改用 WorkBuddy 自动化驱动 |
| 任务明明到了时间却一直不 done | 检查 done_actions 白名单是否与脚本实际输出的 action 对得上(guard.py check 会打印白名单与 last_action) |
| 固定触发点全部落空、状态文件当天再无写入 | 触发时刻电脑处于关机/休眠。查系统事件日志 Kernel-General(ID 12/13) 与 Kernel-Power(ID 42/107) 核对关机时段;补齐"开机即补"自动化 |
| 整天不开机 | OS 级无解(任何本地方案皆然);需要 7×24 请用 WorkBuddy 云上模式 |
判断任务是否"错过"而非"失败":看状态文件 state_YYYY-MM-DD.json 的修改时间。
若该时间早于预期触发时刻,说明触发器根本没有执行(错过);若晚于,说明执行了但结果异常(失败)。
安全约束
- 严禁输出/落盘/回显任何 token、Cookie、登录态内容(guard 不触碰登录态文件,由注册的任务脚本自行处理)。
- 命令一律绝对路径 + 参数列表方式执行(subprocess 无 shell,防注入)。
- 只读登录态/状态文件;状态目录(
~/.workbuddy/state/missed-task-guard/)仅含任务完成标记与日志,不含任何凭据。
相关文件
- 完整机制说明(时序图、故障矩阵):
references/mechanism.md - 接口/命令不做任何网络调用,纯本地调度(注册的任务脚本自管网络)。
- 配套技能
workbuddy-buddy-daily—— 被守护的任务本体(签到 + 派喵 + 到点领奖)。