baa-conductor


commit
9f7e60a
parent
a2d48c4
author
im_wower
date
2026-04-01 23:34:08 +0800 CST
docs: add unified automation task queue mvp plan
1 files changed,  +380, -0
Raw patch view.
  1diff --git a/plans/UNIFIED_AUTOMATION_TASK_QUEUE_MVP.md b/plans/UNIFIED_AUTOMATION_TASK_QUEUE_MVP.md
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..199d70fce4eb5127e98811001301e1ce7dd99699
  4--- /dev/null
  5+++ b/plans/UNIFIED_AUTOMATION_TASK_QUEUE_MVP.md
  6@@ -0,0 +1,380 @@
  7+# 统一自动化任务队列 MVP
  8+
  9+日期:`2026-04-01`
 10+状态:`proposed`
 11+
 12+## 目标
 13+
 14+把当前分散的 `指令即时执行`、`renewal 独立 job`、`proxy_delivery / DOM delivery` 收敛成一条最小主线:
 15+
 16+- `conductor` 只负责接收最终消息、解析指令、入库
 17+- 所有待执行动作统一落到一张 `automation_tasks` 队列表
 18+- 只由一个定时 `dispatcher` 扫描并执行任务
 19+- 节流、抖动、lease、重试、状态推进只保留一套
 20+- 执行层首版只保留 `HTTP proxy executor`
 21+- `DOM adapter` 暂时退出主链,不删除代码;如果 HTTP 长期不稳定,再显式加回 fallback
 22+
 23+## 为什么要这样收敛
 24+
 25+当前实现的问题不是“功能不够”,而是“执行路径过多”:
 26+
 27+- instruction ingest 有现场执行语义
 28+- renewal 有独立 job 与独立 dispatcher
 29+- browser request policy 有一套节流 / jitter
 30+- renewal dispatcher 又有一套 inter-job jitter
 31+- delivery 既有 `proxy_delivery`,又有 DOM `inject / send`
 32+
 33+这会导致:
 34+
 35+- 同一类发送动作有多套节奏控制
 36+- 同一类任务有多条执行路径
 37+- 失败恢复和状态推进分散在多个模块里
 38+
 39+MVP 目标不是一次性做最强,而是先把主链收成“一套任务队列 + 一套 dispatcher + 一套 HTTP executor”。
 40+
 41+## 当前决策
 42+
 43+### 1. conductor 不再现场执行指令
 44+
 45+收到 `browser.final_message` 后,`conductor` 仍然负责:
 46+
 47+- 提取 ` ```baa ` block
 48+- 解析 target / tool / params
 49+- 做消息级去重与权限校验
 50+- 把结果写入本地存储
 51+
 52+但不再在 ingest 链路里直接执行。
 53+
 54+对每个合法 block,只创建任务记录,等待 dispatcher 后台执行。
 55+
 56+### 2. 指令任务与续命任务合并到一张通用任务表
 57+
 58+不再维护两套独立 job 模型。
 59+
 60+统一为:
 61+
 62+- `automation_tasks`
 63+
 64+但保留不同 `kind`:
 65+
 66+- `instruction.execute`
 67+- `instruction.deliver`
 68+- `renewal.deliver`
 69+
 70+统一的是调度层,不是业务语义本身。
 71+
 72+### 3. timed dispatcher 成为唯一执行入口
 73+
 74+首版只保留一个后台 dispatcher:
 75+
 76+- 周期性扫描 `pending` / `retry_wait`
 77+- 应用节流、抖动、并发限制
 78+- 抢 lease
 79+- 调用对应 executor
 80+- 回写任务状态
 81+
 82+不再允许其他路径绕过 dispatcher 直接执行任务。
 83+
 84+### 4. 首版只保留 HTTP proxy executor
 85+
 86+任务实际发送时,先只走:
 87+
 88+- `browser.proxy_delivery`
 89+- Claude / ChatGPT / Gemini 对应页面内 HTTP 代理请求
 90+
 91+首版不走 DOM adapter 主路径。
 92+
 93+DOM 代码先保留,但 dispatcher 不路由到 DOM executor。
 94+
 95+### 5. 结果回投与执行本体拆成两个 task kind
 96+
 97+不要把“执行指令”和“回投执行结果”绑成一个不可分步骤。
 98+
 99+否则如果“执行已成功,但回投失败”,重试时会把指令再执行一遍,可能造成重复副作用。
100+
101+因此:
102+
103+- `instruction.execute` 成功后,写入执行结果
104+- 然后创建 `instruction.deliver`
105+- dispatcher 只重试 `instruction.deliver`
106+- 不会重复重跑已成功的指令执行
107+
108+## 非目标
109+
110+这份 MVP 文档明确不做:
111+
112+- 不做 DOM fallback 主路径
113+- 不做前台 GUI 自动化
114+- 不做多节点编排
115+- 不做任务优先级体系的大规模扩展
116+- 不做“收到消息立即执行”的低延迟优化
117+
118+后续如果需要更低延迟,可以在不改变模型的前提下做“入库即唤醒 dispatcher”,但这不属于首版。
119+
120+## 通用任务模型
121+
122+### 表名
123+
124+- `automation_tasks`
125+
126+### 最小字段
127+
128+```text
129+task_id                 TEXT PRIMARY KEY
130+kind                    TEXT NOT NULL
131+status                  TEXT NOT NULL
132+priority                INTEGER NOT NULL
133+dedupe_key              TEXT
134+
135+payload_json            TEXT NOT NULL
136+result_json             TEXT
137+
138+target_client_id        TEXT
139+target_platform         TEXT
140+target_conversation_id  TEXT
141+
142+execute_after           INTEGER NOT NULL
143+attempt_count           INTEGER NOT NULL
144+max_attempts            INTEGER NOT NULL
145+
146+lease_owner             TEXT
147+lease_until             INTEGER
148+
149+last_error              TEXT
150+
151+created_at              INTEGER NOT NULL
152+updated_at              INTEGER NOT NULL
153+started_at              INTEGER
154+finished_at             INTEGER
155+```
156+
157+### 建议索引
158+
159+```text
160+(status, execute_after)
161+(kind, status, execute_after)
162+(target_client_id, target_platform, status, execute_after)
163+(dedupe_key) UNIQUE WHERE dedupe_key IS NOT NULL
164+```
165+
166+## 任务状态机
167+
168+首版统一状态收敛为:
169+
170+- `pending`
171+- `leased`
172+- `running`
173+- `retry_wait`
174+- `succeeded`
175+- `failed`
176+
177+说明:
178+
179+- `pending`:已入队,等待 dispatcher 抢占
180+- `leased`:dispatcher 抢到 lease,但尚未真正开始执行
181+- `running`:executor 已开始
182+- `retry_wait`:本次失败,但可重试;等待下一次 `execute_after`
183+- `succeeded`:成功完成
184+- `failed`:不可重试或已超过 `max_attempts`
185+
186+不再单独保留 renewal 专属状态机。
187+
188+## 三类 task kind 的语义
189+
190+### `instruction.execute`
191+
192+输入:
193+
194+- 来源消息 ID
195+- 解析后的 target / tool / params
196+- 原始 block 文本
197+- 所属 conversation / platform
198+
199+执行:
200+
201+- 调用现有 instruction execution 栈
202+
203+成功:
204+
205+- 持久化执行结果
206+- 创建后续 `instruction.deliver`
207+- 当前 task 标记 `succeeded`
208+
209+失败:
210+
211+- 可重试错误 -> `retry_wait`
212+- 不可重试错误 -> `failed`
213+
214+### `instruction.deliver`
215+
216+输入:
217+
218+- 已持久化的执行结果
219+- 目标页面路由信息
220+- assistant message id / conversation id / page url 等 delivery 上下文
221+
222+执行:
223+
224+- 只走 `browser.proxy_delivery`
225+
226+成功:
227+
228+- 标记 `succeeded`
229+
230+失败:
231+
232+- 页面暂停、无活动 client、下游超时、HTTP 非 2xx 等 -> `retry_wait`
233+- payload 缺失、route 不可解析、平台不支持等 -> `failed`
234+
235+### `renewal.deliver`
236+
237+输入:
238+
239+- renewal projector 决定要发送的 payload
240+- 对话路由、平台、client、conversation
241+
242+执行:
243+
244+- 同样只走 `browser.proxy_delivery`
245+
246+成功 / 失败语义与 `instruction.deliver` 一致。
247+
248+## dispatcher 合同
249+
250+### 扫描频率
251+
252+首版使用固定 tick:
253+
254+- `2s`
255+
256+原因:
257+
258+- 比当前 renewal 的低频 tick 更适合承接普通指令执行
259+- 仍然保留“天然抖动 / 兜底恢复”的后台模型
260+
261+### 并发模型
262+
263+首版保守限制:
264+
265+- 全局 `max tasks per tick`: `20`
266+- 每个 `(target_client_id, target_platform)` 并发上限:`1`
267+
268+这样可以避免同一浏览器客户端上的同平台页面并发发包。
269+
270+### 发送前抖动
271+
272+首版只保留一套发送前抖动,统一放在 dispatcher:
273+
274+- 每个 outbound task 在真正执行前加 `0ms ~ 800ms` 的有界随机抖动
275+- 分布先用简单均匀分布
276+- 不再保留 renewal dispatcher 自己的独立 jitter 逻辑
277+
278+MVP 阶段不需要 Gaussian;先保证单点、单实现、可观测。
279+
280+### 重试策略
281+
282+首版默认:
283+
284+- `max_attempts = 3`
285+- backoff: `5s` / `15s` / `45s`
286+- 每次重试前再叠加小抖动:`0ms ~ 1000ms`
287+
288+### lease
289+
290+dispatcher 抢任务时必须写 lease:
291+
292+- `lease_owner`
293+- `lease_until`
294+
295+如果进程崩溃,后续 tick 可以回收过期 lease,把任务重新拉回 `pending`。
296+
297+## executor 合同
298+
299+首版只保留一个 executor:
300+
301+- `HTTP proxy executor`
302+
303+它内部仍然可以按平台区分:
304+
305+- Claude completion API
306+- ChatGPT 对应 conversation API
307+- Gemini 对应 proxy API
308+
309+但统一对 dispatcher 暴露一个接口:
310+
311+```text
312+execute(task) -> success | retryable_failure | terminal_failure
313+```
314+
315+## 与现有模块的关系
316+
317+### 保留
318+
319+- 现有 `final_message` 观察与 ingest 主链
320+- 现有 instruction parser / validator / executor 逻辑
321+- 现有 `browser.proxy_delivery` 能力
322+- 现有 DOM adapter 代码(仅保留,不进主链)
323+
324+### 退出主链
325+
326+- ingest 时的现场执行
327+- renewal 独立 dispatcher
328+- DOM `inject_message / send_message` 主路径
329+
330+### 不立即删除
331+
332+- `delivery-adapters.js`
333+
334+原因:
335+
336+- HTTP proxy 如果长期不稳定,还需要低成本恢复 fallback
337+
338+## 建议迁移顺序
339+
340+### Phase 1
341+
342+- 新增 `automation_tasks`
343+- 新增通用 dispatcher
344+- 支持 `instruction.execute`
345+
346+### Phase 2
347+
348+- `instruction.execute` 成功后生成 `instruction.deliver`
349+- 回投只走 `browser.proxy_delivery`
350+
351+### Phase 3
352+
353+- renewal projector 不再写独立 renewal job 表
354+- 改为直接写 `renewal.deliver` 到 `automation_tasks`
355+
356+### Phase 4
357+
358+- 停用旧 renewal dispatcher
359+- 停用 instruction ingest 里的现场执行路径
360+
361+### Phase 5
362+
363+- 观察 HTTP proxy 稳定性
364+- 如果稳定,再考虑正式下线 DOM 主路径
365+- 如果不稳定,再显式恢复 `HTTP primary -> DOM fallback`
366+
367+## MVP 验收标准
368+
369+- 收到 ` ```baa ` 指令后,ingest 只入库,不现场执行
370+- dispatcher 能在固定 tick 内扫描出 `instruction.execute`
371+- 执行成功后生成 `instruction.deliver`
372+- `instruction.deliver` 只走 `browser.proxy_delivery`
373+- renewal 任务也能写入同一张 `automation_tasks`
374+- 同一 `(client, platform)` 不会并发发出多条 outbound 请求
375+- 抖动、重试、lease 都有统一日志可观测
376+
377+## 结论
378+
379+这份 MVP 的核心不是“功能更多”,而是“把分散执行路径收成一条可解释的主线”:
380+
381+- 一张队列表
382+- 一个 dispatcher
383+- 一个主 executor
384+- 多个 task kind
385+
386+先把模型收简单,再根据线上稳定性决定是否把 DOM fallback 加回主链。