type
Post
status
Published
date
Sep 27, 2026
slug
summary
把随手记进滴答清单的待办,变成自动开 DSH 会话执行的任务。
tags
Write
AI Note
DSH 自动化实践
category
DSH
icon
password
url
我记待办的方式很随意:想到了就丢一条进滴答清单,不排期,也不分级。这种习惯决定了「按固定时刻拉任务去执行」这个思路在一开始就走不通——我根本不是那种会提前把一天排好的人,固定时刻拉起程序,多半只会对着空清单发呆。于是真正要解决的问题变成了另一个:怎么让随手写下的东西,在它自己该跑的时候跑起来,而且跑完我还知道结果。
判据不能只有一条:拉取窗口与执行截止要分开
第一版上线后最典型的现象是「拉到了,但没执行」。清单里明明有今天该做的事,派发器却像没看见。原因不复杂:最早的判断条件只看任务有没有到期,而界面里被标成「今天做」的那类任务,往往只有「开始时间」已经到、截止时间还没到。界面认为它今天该做,程序认为它还轮不上,两边对同一件事的理解不一致。
修它的过程反而带来一个更有价值的拆分:把「能不能看见」和「能不能跑」分成两件事。看见,按时间窗口判断,只要进入今天的行动窗口就纳入;能不能自动跑,按截止时间判断,没到期的一律只列出、不启动。拉取的判据要宽,执行的判据要严,这两个条件之后会被反复调整,混在一个判断里就会改一次错一次。
第二个更蠢的坑是来源问题:任务来源清单是单一指定的,我把任务随手建在了另一个清单,然后疑惑它为什么不执行。这不是程序的错,是我把「我放任务的地方」和「机器投放产出的地方」当成了同一个地方。
用户的入口,机器的出口
想明白上面那件事之后,清单被明确分成两个:一个专门用来接收任务的清单,是我写的地方,也是唯一的任务来源;另一个专门用来放机器生成的待办,是它的唯一投放目标。
这个分工解决的其实不是整洁问题,而是抢跑。如果机器写待办的地方和它监视执行的地方是同一个,那它每记一条待办就等于给自己递了一份工单——我还没来得及看一眼,它就已经开了一次会话把这件事跑掉了。人机共用一块白板,必然互相踩脚,只能物理分开。
还有一个细节值得记:清单在配置里同时用名称和标识符指定。听上去冗余,但接口按名称匹配时,很容易命中同名清单或者已经被归档、界面上根本看不见的那一个,任务就会掉进一个你永远找不到的角落。
结果回执不能交给执行者自己汇报
每一条任务会开一个独立的无界面会话去跑,串行执行,跑完回写并勾掉。第一版只做对了一半:派发的时候会通知我,跑完却没人说话。
我当时的设计是让执行者自己汇报——在提示词里写好「做完记得发一条结果」。这等于把整条链路的可靠性押在一句提醒上。执行会话是独立进程,它既不认识派发器,也没有理由去调用一个它不需要的工具。只要回执依赖执行者记得,它就一定会漏。
后来改成由编排层收口:执行结束后,由派发逻辑自己推一条「成功或失败 + 标题 + 一句话结果」,多条任务再补一条批次汇总。这里有个便宜的技巧——无界面会话本来就会把最终答复写到标准输出,那正好就是一个现成的摘要接口,剥掉控制字符、丢掉结尾的完成标记、截断到两百字左右即可,不必去解析会话日志。另外,这条回执必须是显式选择开启的,否则一次冒烟测试就会真的把通知发出去。
高峰排队:默认关闭,且关闭时行为与旧版完全一致
有个改动是我一直想加又不敢加的:机器执行消耗额度,而额度在不同时段有贵贱之分。想法很简单——高峰期不跑,把任务排队等到空闲再跑。
真正影响可维护性的不是排队逻辑本身,而是判定点放在哪里。我把它放在启动执行者之前:拉取、过滤、串行执行这三步一行不改。这样关闭这个开关时,行为与旧版逐字节一致。这一点是敢把它默认关掉的前提,也让「开」和「关」两种情况都容易验证。
实现上有两个小坑值得留给后来人。一是时间格式化:用 24 小时制但参数写错时,午夜会返回 24 而不是 0,跨天判断直接错位。二是跨午夜的时间窗口,别写成一串或判断去比较,展开成一张按分钟索引的查找表更省心,边界情况自然被覆盖。
还有一个容易忽略的细节:临近高峰前的尾部余量。如果只判断「当前不在高峰」,一条任务完全可能在高峰前十分钟启动,跑到一半掉进贵时段。留一段余量,让它整段都待在空闲区间里,才符合本意。
拿不准的地方,用证据代替猜测
我担心过一个具体的问题:执行器是不是按父任务归组、只认顶层条目,那样挂在父任务下的子任务就永远轮不到单独跑。
结论是不会,但要靠证据而不是靠猜。一是代码里从拉取到过滤的全过程,没有任何一处判断父子关系,只判断任务状态和时间窗口;二是数据接口返回的任务列表本身就是平铺的,子任务作为独立条目出现,不是藏在父条目的子字段里。我用真实的父子任务对跑了一遍确认,两者都在结果里。
想清楚之后这件事反而变简单了:执行侧根本不看层级,那么层级就是纯粹给人看的组织手段,不必为了执行再维护一份平级副本。
推而广之,「它到底在不在跑」这个问题也不能只靠一个字段来回答。「上次拉取时间」这类信息只能证明程序还活着,不能证明它看见了该看见的东西。我最后用的是三层核对。
第一层是这一轮看见什么的快照——每次拉取都把当时的任务清单落成一个文件,出问题时先看它,能立刻区分「没拉到」和「拉到了但没执行」。第二层是事件时间戳,把「最近一次拉取的时刻」「这一轮的数量」和执行侧记录的尝试记录放在一起看。第三层才是端到端真跑:真的建一条一次性任务,真的触发派发,确认执行者走到正常结束、回读状态确实被勾掉。
还有两个非典型的误判点,都很容易被当成 bug。改了宿主侧代码不等于运行实例生效:界面插件的宿主路由只在服务启动时注册一次,不会热加载;而浏览器端资源是按需从磁盘读取的,刷新一下就是新的。「宿主路由报 404 但状态接口返回 200」正是这个组合,不是故障。另一个是服务端列表接口有缓存滞后:删掉一条任务后,它可能连续几次仍然出现在返回里,而插件自己的列表已经看不到它了。
可迁移的方法
把这几轮返工抽出来,能留下的其实是几条与具体产品无关的判断。
编排层出回执,执行层只管干活。 执行者顺手写出的最终答复就是现成的摘要接口,值得刻意维持这个约定——让「谁负责汇报」这件事有唯一答案。
拉取与执行的判据要分开,判定点尽量靠前。 判据会被反复调整,靠前的判定点改动成本最低,也最容易做到「关掉开关就等于没加过」。
能用黑名单就别列白名单。 凡是「大部分时间可用、少数时间不可用」的约束,列举不可用集合更小,也更天然地覆盖了周末、跨夜这些边界。我列高峰而不是列空闲,就是这个道理。
跑起来就烧钱的开关默认关,且关闭时与旧版行为完全一致。 它既是回退安全网,也让验证这件事变得廉价。
同一份数据被两个方向消费时,把读写位置物理分开,比在一段代码里加判断便宜得多。清理人的意图,比清理代码划算。
最后是边界,这部分比任何机制都要紧:超时保住的是宿主进程,不是任务本身——被强制结束的执行者,中途产出不会进回执。更实际的是权限边界:执行会话只有固定的写入范围,工作区之外的地方它够不到,而无人值守的会话没有审批通道,需要越界的操作会被直接拒绝。这类任务不要派给自动执行,交回有人看着的交互会话。 能自动跑的,是那种目标明确、影响局限在工作区内、失败也不会造成外部后果的事;其余的,宁可让它等人。