第 10 课 · 记忆冷却
变量随运行而死。有些事实不该如此:"这个我已经收过了"必须活到下一次运行。
问题
一个按计划每几分钟跑一次的流程,收集的奖励却每个周期只刷新一次。一个周期收两次,第二次轻则白干,重则有害。
流程需要在每次运行开头问:最近的某次运行是不是已经干过这事了?——而答案必须活在任何单次运行之外。
你将学到
- Runtime Memory(运行时记忆):带名称、字段、作用域(scope)和 TTL 的条目。
- Memory Match(记忆匹配):拿候选者对照每条已记住的条目。
- 冷却守卫模式:先匹配,再记住,再动手。
- 为什么 TTL 胜过手动清理。
开始之前
- 完成第 09 课。OCR 开关开着(我们要读周期计时)。
流程

徽标按阅读顺序给节点编号,与动手构建步骤对应。顶部的 Memory Match 才是重点——它决定下面的活儿到底跑不跑,于是冷却逻辑集中在一个节点里,而不是散落在整个流程中。
动手构建
- 新建流程;声明
cycle_minutes(number?)。 - Read Text
1. Read the cycle timer对着 Next Cycle 读数,类型 Time,分钟 →cycle_minutes。 - Memory Match
2. Already collected this cycle?:- Memory:
pa_collected_cycles - Expression:
{{item.cycle}} == {{cycle_minutes}}
- Memory:
- Runtime Memory
3. Remember this cycle (10 min TTL):- Operation remember,memory
pa_collected_cycles,scope persist - 一个字段:
cycle={{cycle_minutes}} - TTL 600 秒
- Operation remember,memory
- Click 点 Collect,三个 Stop 如图(
skipped是 success——拒绝重复劳动是正确行为)。 - 连线:先匹配,再 记住,再动手。 Save,Auto Layout。
两个记忆节点如何配合
Runtime Memory 写;Memory Match 读。 一条条目是个微型记录——这里是 {cycle: 20}——存在某个记忆名称之下、某个 scope 里、带一个寿命:
| 部件 | 此处 | 缘由 |
|---|---|---|
名称 pa_collected_cycles | 两个节点共用的标签 | 任一侧打错字 = 守卫永不命中 |
字段 cycle | 标识"这项工作"的东西 | 匹配时写作 {{item.cycle}} |
| Scope persist | 活过运行结束和应用重启 | 正是重点——run 作用域立刻就忘 |
| TTL 600 s | 条目自我删除 | 冷却自动到期:没有清理流程,没有发霉的清单 |
Memory Match 对每条存活条目求值一次表达式,受测条目以 {{item.…}} 暴露。任一条目满足 → matched。过期条目干脆消失——TTL 就是把"永远记住"变成"恰好记住一个周期"的东西。
顺序就是一切:匹配 → 记住 → 动手。 若在匹配之前记住,每次运行都会命中自己刚写的新条目——流程永远跳过。若在记住之前动手,两步之间一次崩溃就收两次。安全顺序是先问、再占名、最后干活。
这里对分钟做等值比较就够了。真实的 OCR 坐标会漂移一两个像素——这正是 Memory Match 收整条表达式的原因:abs({{current_x}} - {{item.x}}) <= 1 匹配的是邻近,不是精确。毕业设计的守卫用的正是这一招的简化版。
运行
- 第一次:计时读到
04:20→ 无条目命中 → 记住{cycle: 20}→ Collect →success · Collected this cycle。 - 立刻再跑:同样的读数 → matched →
success · Already collected this cycle。没有发生任何点击。 - 等十分钟(或改计时:
dashboard.html?timer=07:15)再跑:旧条目已过期(或不匹配)→ 再次收集。
第 2 次运行就是本课的课义:一次不同的运行、几分钟之后,知道第 1 次运行做了什么。
最佳实践
- 先匹配,再记住,最后动手。
- TTL = 真实冷却时长,不是"图安全设长点"——太长会跳过正当的工作。
- 单机守卫用
persist。 只有所有实例必须尊重同一清单时才用shared——那时它们互相覆盖对方的占名是设计使然。 - 记忆名称要具体——
pa_collected_cycles,不是data。 - 跳过就是成功。 按成功来连线。
常见错误
| 错误 | 后果 | 修复 |
|---|---|---|
| Remember 放在 Match 之前 | 命中自己的条目;永远跳过 | 先匹配 |
| 两个节点的记忆名不一致 | 守卫永不命中 | 两侧同一字符串 |
字段名 ≠ {{item.…}} 引用 | 永不命中 | cycle ↔ {{item.cycle}} |
| 不设 TTL | 条目堆积;冷却早过仍在跳过 | 永远设 TTL |
Scope 用 run | 运行一结束就忘——跨运行守卫失效 | persist |
故障排查
从不跳过,哪怕背靠背运行。
先查两个节点的记忆名称,再查字段/item 名,再确认写入的 scope 不是 run。
永远在跳过。 节点顺序——remember 排在了 match 上游。重连。
应用重启后意外地跳过。
那是 persist 在尽职:TTL 是绝对时间,跨重启存续。若你要的是"仅本次启动",session 作用域才是工具。
小结
- Runtime Memory + Memory Match = 活得比运行更久、寿命自动到期的事实。
- 守卫模式是一个顺序:匹配 → 记住 → 动手。
- Scope 决定谁共享记忆;TTL 决定它约束多久。
下一课
单项技能你已集齐:操作、验证、读取、计数、重试、恢复、记忆。第 11 课 · 用子流程搭建是流程开始互相调用的地方——你的课程真正变成一座库。
另见:Runtime Memory · Memory Match · 变量与记忆