给 Electron Agent 加后台任务看门狗时,我会先把心跳和恢复计划拆开
最近在做一个 Electron 桌面端 Agent 工具时,我遇到一个很隐蔽的问题:面板显示任务还在运行,用户也能点取消和查看日志,但真正跑自动化脚本的后台进程已经卡住。它没有崩溃,也没有抛错,只是很久不再回传进度。用户看到的是一个永远转圈的任务,开发侧看到的是一段很难复现的空白。

原创示意图:把 UI 请求、主进程监督、后台 worker、心跳、超时检测、恢复账本、重启闸门和人工复核拆成可追踪链路。 来源:Codex image generation
问题背景
Electron 的 utilityProcess 文档说明,它可以创建启用 Node.js 和 MessagePort 的子进程,适合把较重任务从主进程隔离出去;实例提供 spawn、message、exit 事件,以及 postMessage 和 kill 控制能力。Node.js 的 child_process 文档也强调,异步创建子进程不会阻塞事件循环,fork 出来的 Node 进程有独立内存和自己的 V8 实例。对桌面端 Agent 来说,这类隔离很关键,因为截图、文件扫描、RAG 索引刷新和批量命令执行都不该拖住窗口响应。
踩坑和关键难点
第一个坑是把进度事件当心跳。进度是业务状态,可能一分钟都没有新变化;心跳是运行状态,必须按固定间隔证明 worker 还在响应。第二个坑是主进程直接重启 worker。桌面端会遇到系统睡眠、网络切换、电池降频和用户主动取消,所有异常都自动重启,可能重复执行有副作用的工具。第三个坑是恢复缺少证据。只知道任务断了还不够,还要知道最后一次 checkpoint、当前输入、已完成副作用和是否允许自动续跑。
解决思路
我把后台任务拆成五层。UI 只提交 taskId、参数摘要和取消意图。main process supervisor 负责创建 utility worker,并维护 workerId、pid、startedAt、lastHeartbeatAt。heartbeat monitor 每隔几秒发送 ping,worker 回 pong 时带上当前 step、checkpointId 和资源状态。timeout detector 只判断运行健康度,不直接改业务结果。recovery ledger 保存启动、心跳、超时、取消、退出、重启和人工复核记录。
这样拆完后,任务结果来自业务事件,任务健康度来自心跳,恢复动作来自 ledger。主进程看到心跳超时后,先冻结 UI 上的任务状态,再读取 recovery policy:纯读取任务可以从 checkpoint 自动续跑;写文件、发请求、发布内容这类有副作用的任务进入 manual review,等用户确认后再继续。
关键步骤
第一步,给每个任务生成稳定的 runId 和 workerId。runId 跟业务任务走,workerId 跟一次进程实例走。worker 重启后,runId 不变,workerId 变化,排查时能分清任务生命周期和进程生命周期。
第二步,心跳包只放健康信息。不要把大日志、完整结果或模型输出塞进 pong。它只需要返回 step、checkpointId、queueDepth、memoryHint 和一个递增的 heartbeatSeq。
第三步,退出事件要和超时事件合并判定。exit 能说明进程结束,心跳超时能说明进程可能卡死,系统 suspend 和 resume 则会解释一段时间内的心跳空洞。恢复策略要同时看这三类信号,避免误判用户合盖睡眠后的正常恢复。
第四步,取消走 AbortController 语义。UI 点取消后,主进程先把 run 标为 cancelling,再向 worker 发 abort,超过宽限时间才 kill。这样能给 worker 清理临时文件、关闭流、写入最后 checkpoint 的机会。
可复用经验
后台任务看门狗的重点是让桌面端 Agent 能解释自己的状态。心跳证明进程活着,checkpoint 证明任务走到哪一步,recovery ledger 证明系统做过什么决定。只要这三件事稳定,RAG 增量索引、批量文件整理、自动化脚本执行和 Electron 内的长任务调试都能复用同一套模式。
我现在会把验收清单写成四问:卡住时 UI 能不能在一个超时时间内转入可解释状态,重启时是否保留 runId 和 checkpoint,有副作用的任务是否进入人工复核,系统睡眠恢复后是否避免误报失败。四问都能通过,再去追求更细的性能指标会更稳。