Files
MediaCrawler/mac-agent-os-main/PLANS/SCHEDULER_ARCHITECTURE_v3.md
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

58 KiB
Raw Blame History

AgentOS 任务编排与调度系统 — 全面规划 v3.0

日期: 2026-06-27 | 版本: 4.3.0 规划 | 第2版重写 基于: 用户需求深度讨论 + 现有架构审计 核心原则: 不新增臃肿层级,能合并的合并到现有组件


一、需求全景图

1.1 任务的两类本质

经过讨论,所有操作不按"动作类型"分类,而按意图分类:

维度 日常养号(scheduled) 特殊交互(priority)
触发方式 定时/循环自动触发 用户主动提交
操作内容 蓝图中编排的随机动作 指定具体目标(URL/直播间)
是否指定链接 ❌ 不指定,随机推荐流 ✅ 指定具体视频/博主/直播间
执行时间 按日程表串行执行 随时插入,可设置间隔
优先级 低(可被抢占) 高(抢占低优任务)

关键结论:同一个"点赞"动作,既可以是养号里的随机点赞(走蓝图),也可以是特殊交互里的指定点赞(走定向任务)。按意图分类而非按动作分类。

1.2 任务的粒度拆解

用户提交: "20个账号评论这个视频"
  ↓
调度层拆解为 20 个独立子任务:
  ├─ 账号A 在 13:00 评论
  ├─ 账号B 在 13:05 评论  (间隔5分钟)
  ├─ 账号C 在 13:12 评论  (随机间隔7分钟)
  ├─ 账号D 在 13:15 评论  (随机间隔3分钟)
  └─ ...
  ↓
每个子任务 = 一条独立 Task,有自己的状态追踪

间隔参数:用户可设置 {min: 5, max: 15} 分钟,调度器在每个子任务执行完毕后随机等待。

1.3 任务依赖链

三级接力评论:
  账号A 评论           → 完成后产出 comment_id
     ↓ 依赖 A 成功
  账号B 回复 A         → 需要 A 的 comment_id
     ↓ 依赖 B 成功
  账号C 回复 B         → 需要 B 的 comment_id

直播间关注:
  所有账号先进直播间     → 等待全部进入
     ↓ 全部就绪
  同时发起关注           → 关注主播
     ↓ 关注完成
  等待10分钟            → 观看时长
     ↓ 时间到
  退出直播间

1.4 已有功能不需要改的

现有模块 原因
douyin_ops.py 原子操作 底层能力不变,只管"怎么点"不管"什么时候点"
engine.py 批量执行 单次批量的执行引擎逻辑不变
nurture_runner.sh 养号包装器不变,调度层直接调它
blueprints/*.json 蓝图文件不变,调度层读取蓝图参数
corpus.py 语料库 评论内容生成逻辑不变

二、调度层放在哪?— 方案对比

方案A:完全新增 scheduler/ 模块(v3.0初版方案)

scheduler/  ← 全新模块
  ├── task.py
  ├── task_store.py
  ├── scheduler.py
  └── ...
  • ❌ 新增一层,系统更臃肿
  • ❌ 需要额外进程/线程
  • ❌ 和 guardd 职责重叠

方案B:合并到 guardd(推荐方案)

guardd  ← 现有守护进程,扩展
  ├── HTTP Server (已有)        ← 扩展任务管理API
  ├── 300s周期循环 (已有)        ← 缩短到15-30s
  ├── 心跳上报 (已有)            ← 增加详细任务状态
  ├── 任务调度引擎 [新增]        ← 优先级队列+时间线
  └── 浏览器槽位管理 [新增]      ← 接管mc的浏览器限制

原因:

  • guardd 已经在每台机器上运行
  • guardd 已经有 HTTP API(/task 接受任务)
  • guardd 已经有周期性循环(用于心跳)
  • guardd 已经有跨机通信(心跳上报到 Dashboard)
  • 不需要额外进程,全部在 guardd 内部完成

方案C:合并到 CommandBus

CommandBus  ← 扩展为带调度功能
  • ❌ CommandBus 运行在 Dashboard 进程内,Dashboard 挂了调度就停了
  • ❌ CommandBus 主要职责是分发,不是调度
  • ❌ 不适合做本机队列管理

结论:采用方案B,调度功能合并到 guardd


三、新架构(轻量级)

3.1 系统分层

┌─────────────────────────────────────────────────────┐
│ L5 看板(Dashboard)                                  │
│  ├─ 联邦指挥台 (matrix-command.js) ← 重建为任务管理  │
│  │   显示: 每台机器的实时队列 + 时间线 + 执行状态     │
│  │   操作: 提交任务 / 拖拽排序 / 暂停/恢复/取消      │
│  │   告警: 封号/登录失败/任务失败 → 高亮提示         │
│  └─ 操作视图 (matrix-interact.js) ← 提交交互任务     │
│      POST /api/ops/run {type, accounts, params}      │
├─────────────────────────────────────────────────────┤
│ L4 API 路由 (routes/ops.py)                          │
│  统一入口: POST /api/ops/run                         │
│  新增:     Task 管理路由(查看列表/取消/重排)       │
├─────────────────────────────────────────────────────┤
│ L3 命令分发 (CommandBus)                              │
│  职责缩小: 仅做按机器分组 + 模板渲染 + 投递到 guardd │
│  不再管理队列 — 队列管理下沉到各机 guardd            │
│  不再管理轮询 — 轮询下沉到各机 guardd                │
├─────────────────────────────────────────────────────┤
│ L2 本机调度 (guardd) ← 核心增强                       │
│  ├─ HTTP Server: /task (接收任务)                    │
│  │               /tasks (查询队列)                    │
│  │               /task/{id}/cancel (取消)             │
│  │               /task/{id}/pause (暂停)              │
│  │               /task/{id}/resume (恢复)             │
│  ├─ TaskScheduler: 优先级队列 + 浏览器槽位管理        │
│  ├─ Heartbeat: 每15秒上报详细状态到 Dashboard         │
│  └─ Executor: 调 mc run / mc interact / 等            │
├─────────────────────────────────────────────────────┤
│ L1 mc 引擎                                            │
│  engine.py → 单次执行,被 guardd 调用                 │
│  浏览器数量控制 → 上移至 guardd 的槽位管理            │
├─────────────────────────────────────────────────────┤
│ L0 浏览器层 (Camoufox)                                │
└─────────────────────────────────────────────────────┘

3.2 新架构的轻量说明

组件 变化 原因
guardd 增强,非新增 合并调度器 + 心跳增强 + 槽位管理
CommandBus 瘦身 去掉 poll/queue 逻辑,只做分发+模板渲染
Dashboard 重建联邦指挥台 可视化任务管理(不是新增组件)
scheduler/ 不新增目录 代码放在 guardd 模块内

四、任务模型

4.1 任务分类(按意图)

{
  "task_type": "scheduled",
  // 或 "priority"

  "task_id": "nurture_douyin_daily_20260627",
  "accounts": ["douyin_01", "douyin_02"],
  "machine": "chengzigedeAir",
  
  // 调度参数
  "schedule": {
    "type": "cron",          // cron / interval / dependency / now
    "cron": "0 8 * * *",     // 每天8点
    "interval_sec": null,     // 间隔秒数(优先级任务用)
    "interval_random": null,  // {min: 300, max: 900} 随机间隔
  },

  // 依赖(三级接力用)
  "depends_on": [],
  "interval_after_dep": 0,

  // 执行参数
  "blueprint": "douyin_daily",
  "rounds": 3,
  "params": {},               // 特殊链接等

  // 状态
  "status": "pending",
  "progress": {"current": 0, "total": 3},
  "result": null,
  "error": null
}

4.2 任务状态流转

PENDING ──→ QUEUED ──→ RUNNING ──→ COMPLETED
  │           │           │
  │           │      [高优抢占]
  │           │           │
  │           │      ┌────▼────┐
  │           │      │ PAUSED  │──→ [恢复] → RUNNING
  │           │      └─────────┘
  │           │
  │      [用户取消] → CANCELLED
  │
  [依赖未满足] → WAITING_DEP → [满足] → QUEUED
  
  RUNNING → FAILED → [重试] → QUEUED
                    → [重试耗尽] → FAILED
                                  → [依赖任务] → SKIPPED

4.3 账号状态跟踪

每个账号在执行过程中会维护状态,任务完成后回写:

{
  "account_id": "douyin_136",
  "machine": "7kecheng",
  "status": "active",          // active / login_expired / banned / sms_verify
  "last_task": "interact_001",
  "last_error": null,
  "browser_slot": 2,            // 占用的浏览器槽位
  "tasks_today": 5,
  "failed_today": 0
}

当 guardd 检测到账号状态变化(登录过期/封号)→ Dashboard 收到心跳告警 → 联邦指挥台高亮显示。


五、任务拆解与插入逻辑

5.1 优先级任务的拆解

用户提交: "20个账号评论视频V,间隔5-15分钟"
  ↓
CommandBus 收到请求:
  ├─ 1. 查 ORACLE.yaml 把20个账号按机器分组
  │     机器A: 5个账号
  │     机器B: 8个账号
  │     机器C: 7个账号
  ├─ 2. 对每台机器生成一条"编排任务"
  └─ 3. 发送给各机 guardd
      ↓
各机 guardd 收到编排任务:
  ├─ TaskScheduler.decompose()
  │   ├─ 拆成5条独立子任务(每个账号一条)
  │   ├─ 计算时间线(每条间隔随机5-15分钟)
  │   └─ 写入本机 task_store
  └─ 返回编排计划给 Dashboard

5.2 优先级插入 vs 日常养号

当前机器队列:
  [scheduled] 养号 douyin_01-05 (P1) → 正在执行
  
插入 priority 任务:
  ├─ 方式A: 等待当前这一轮执行完 → 插入 P0
  │   养号 douyin_01-05 第3轮结束
  │   → [priority] 账号A 评论视频V (P0)
  │   → 间隔8分钟
  │   → [priority] 账号B 评论视频V (P0)
  │   → 间隔5分钟
  │   → ...全部 P0 子任务完成
  │   → [scheduled] 恢复养号 douyin_01-05 第4轮
  
  ├─ 方式B: 强行暂停当前养号
  │   养号进程收到 SIGSTOP 或 guardd stop API
  │   → 插入 P0
  │   → P0 完成
  │   → 恢复养号进程
  │   (注: 需要浏览器进程支持暂停,技术复杂)
  
  推荐用方式A: 等当前这一轮完再接 P0
  (Camoufox 浏览器不能随意中断)

5.3 浏览器槽位管理

当前:mc/engine.py 用 --max-browsers=3 控制并发浏览器数。

升级后:由 guardd 统一管理本机浏览器槽位:

class BrowserSlotManager:
    """浏览器槽位管理器 — 在 guardd 内部
    槽位 = Camoufox 浏览器实例,identity_dir 是唯一ID
    槽位编号不重要(用户可能拖拽窗口),核心是 browser_id

    关键规则:
      1. 一个账号同一时间只能在一个浏览器上运行
      2. 槽位只是计数器,实际以 browser_id 为准
      3. 释放槽位 = 关闭浏览器进程,不是简单标记空闲
    """
    def __init__(self, max_slots=3):
        self.max_slots = max_slots
        self.slots = [None] * max_slots

    def acquire(self, account_id, identity_dir):
        for s in self.slots:
            if s and s["account_id"] == account_id:
                raise Exception(f"账号 {account_id} 已在运行")
        for i in range(self.max_slots):
            if self.slots[i] is None:
                info = {"slot_id": i, "account_id": account_id, "browser_id": identity_dir, "pid": None}
                self.slots[i] = info
                return info
        return None

    def release(self, browser_id):
        for i in range(self.max_slots):
            if self.slots[i] and self.slots[i]["browser_id"] == browser_id:
                self.slots[i] = None
                return True
        return False

    def get_usage(self):
        slots_info = []
        for i, s in enumerate(self.slots):
            if s: slots_info.append({"slot_id": i, "account_id": s["account_id"], "browser_id": s["browser_id"]})
        return {"max": self.max_slots, "used": sum(1 for s in self.slots if s), "slots": slots_info}

    def find_account(self, account_id):
        for s in self.slots:
            if s and s["account_id"] == account_id:
                return s
        return None

### 6.1 当前问题

| 问题 | 表现 |
|:-----|:------|
| 信息不全 | 只显示"运行中",不知道每台机器的具体任务 |
| 交互不准 | 状态刷新滞后,看不到队列里有什么 |
| 无手动干预 | 不能暂停/取消/重排任务 |
| 无告警 | 封号/登录失败没有醒目提示 |

### 6.2 新指挥台设计

┌─────────────────────────────────────────────────────────────┐ │ 🚀 联邦指挥台 │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 📡 三机状态总览 │ │ │ │ 🟢 chengzigedeAir (3/3槽位) 运行中: 养号 | 队列: 2 │ │ │ │ 🟡 5kechengdeAir (2/3槽位) 运行中: 评论 | 队列: 3 │ │ │ │ 🔴 7kecheng (0/3槽位) offline | 心跳:-- │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 📋 chengzigedeAir 任务时间线 [今天 08:00 ~ 23:00] │ │ │ │ │ │ │ │ 08:00 ── [🔄] 养号 douyin_01-05 ─── 第3轮 [▓▓▓▓░░] │ │ │ │ 11:00 ── [⏸] 养号 xhs_01-03 ─── 等待中 │ │ │ │ 13:00 ── [🔴] 定向评论 视频V ─── 子任务1/5 │ │ │ │ 13:08 ── [⏳] 定向评论 视频V ─── 子任务2/5 │ │ │ │ 13:15 ── [⏳] 定向评论 视频V ─── 子任务3/5 │ │ │ │ ... │ │ │ │ [拖拽调整顺序] [暂停] [取消] [插入新任务] │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ ⚠️ 告警中心 │ │ │ │ ⚠️ douyin_136 @ 7kecheng — 登录过期 (10分钟前) │ │ │ │ ❌ xhs_07 @ 5kechengdeAir — 封号 (2小时前) │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘


### 6.3 联邦指挥台能力清单

| 能力 | 说明 |
|:-----|:------|
| **状态总览** | 三台机器在线/离线、槽位使用率、当前任务、队列长度 |
| **时间线视图** | 每台机器今天已执行和待执行的任务时间线 |
| **进度条** | 当前运行的每个任务完成百分比 |
| **手动调度** | 拖拽调整待执行任务顺序 |
| **暂停/恢复/取消** | 对正在执行或排队中的任务操作 |
| **插入新任务** | 直接在当前队列中插入 priority 任务 |
| **告警中心** | 封号、登录失败、任务失败的高亮提示 |
| **历史追踪** | 已完成任务的执行结果(成功/失败/耗时)|

### 6.4 任务卡片的优先级标识

每个任务在时间线上显示时,带优先级标签:

[P0🔴 优先] 定向评论 视频V 13:00 账号: douyin_136 | 子任务 2/5 [P1🟢 日常] 养号 douyin_01-05 08:00 蓝图: douyin_daily | 第3/10轮 [P2⚪ 闲时] 采集个人信息 03:00 蓝图: douyin_read_profile | 全部账号


在指挥台上方提供筛选开关: [☑ 优先任务] [☑ 日常任务] [☐ 闲时任务],默认显示 P0+P1。

### 6.5 账号冲突检测机制

当 guardd 收到新任务时:

接收 priority 任务: 账号A 评论视频V ├─ BrowserSlotManager.find_account("账号A") │ ├─ 返回 None → 空闲 → 正常分配槽位 │ └─ 返回 slot_info → 账号A正在运行 → 等待完成 ├─ 检查账号状态: active→执行 / banned→告警跳过 / login_expired→插入登录恢复 └─ 检查浏览器进程: pid存活→复用 / 不存在→启动新浏览器


冲突检测确保: 同账号不分配到两个浏览器实例 → 避免指纹冲突和封号风险



### 6.6 任务执行状态实时追踪(深潜分析)

#### 当前问题:状态可见性存在断层

| 层级 | 当前有什么 | 缺什么 | 原因 |
|:-----|:----------|:-------|:------|
| **mc engine** | 每步执行后 log 输出到 stdout,含 account_id + step_name + 耗时 | 没有标准化的进度事件,只有文本日志 | engine.py 用 `log.info()` 而非结构化事件 |
| **nurture_runner.sh** | 执行完毕后写一份 JSON 结果文件到 `runtime/nurture/results/` | 执行过程中没有中间状态写入 | 只在 `exit` 时写结果 |
| **guardd 心跳** | `current_task` 字段来自 `last_run.json` | 没有 per-account 粒度,没有当前步骤 | `last_run.json` 只在任务切换时更新 |
| **Dashboard** | `CommandBus.get_status()` 返回命令级状态 | 看不到每个账号的进度,看不到当前步骤 | 状态聚合只有命令级,没有 account 级 |

#### 改造后:三层实时状态 pipeline

mc engine (每步执行后) │ emit_progress(account_id, step_id, step_name, success, detail) ▼ executor.py (guardd 模块) │ write to task_store: task.progress.current_step += 1 │ update slot_manager: slot[0].current_step = "post_comment" ▼ guardd 心跳 (每15秒) │ collect task_store + slot_manager → 结构化状态 ▼ Dashboard (每15秒拉取) │ 聚合显示各机实时状态


#### executor.py 的进度回传协议

```python
# executor.py — 执行 mc run 时实时解析输出
class Executor:
    async def execute(self, task: Task):
        proc = await asyncio.create_subprocess_exec(
            "python3", "-m", "mc", "run",
            f"--accounts={task.accounts[0]}",
            f"--blueprints={task.blueprint}",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
        )
        
        # 实时解析 stdout,每行都尝试提取进度信息
        async for line in proc.stdout:
            text = line.decode().strip()
            
            # 提取账号信息
            if "📱" in text:
                # "📱 douyin_136 (douyin) → interact_comment (5步)"
                account = self._extract_account(text)
                blueprint = self._extract_blueprint(text)
                self._update_task_progress(task.task_id, account, blueprint)
            
            # 提取步骤结果
            elif "✅" in text or "❌" in text:
                # "✅ [ 4] post_comment       → ok (12.5s)"
                step = self._extract_step(text)
                self._update_step_progress(task.task_id, step)
            
            # 提取完成状态
            elif "📊" in text:
                # "📊 执行完成: 成功 5, 失败 0, 耗时 125s"
                self._finalize_task(task.task_id, text)
        
        # 保存完整日志
        self.task_store.save_log(task.task_id, log_text)

guardd 心跳中的任务状态(升级后)

{
  "hostname": "chengzigedeAir",
  "status": "online",
  "tasks": {
    "active": {
      "task_id": "nurture_20260627_001",
      "type": "scheduled",
      "accounts_running": [
        {
          "account_id": "douyin_136",
          "slot_id": 1,
          "blueprint": "interact_comment",
          "current_step": "post_comment",
          "step_index": 4,
          "total_steps": 5,
          "elapsed_sec": 82,
          "steps_success": 3,
          "steps_failed": 0,
          "status": "running"
        },
        {
          "account_id": "douyin_137",
          "slot_id": 2,
          "blueprint": "douyin_daily",
          "current_step": "like",
          "step_index": 3,
          "total_steps": 7,
          "elapsed_sec": 145,
          "steps_success": 2,
          "steps_failed": 0,
          "status": "running"
        }
      ],
      "queued_accounts": ["douyin_138", "douyin_139"],
      "alerts": []
    },
    "queued": [{"task_id": "interact_002", "type": "priority", "estimated_start": "13:00"}],
    "completed_today": 3,
    "failed_today": 0
  },
  "slots": {
    "max": 3,
    "used": 2,
    "list": [
      {"slot_id": 0, "account_id": "douyin_136", "identity_dir": "phone_15370103682", "pid": 82345, "current_step": "post_comment", "elapsed_sec": 82},
      {"slot_id": 1, "account_id": "douyin_137", "identity_dir": "phone_15370103683", "pid": 82346, "current_step": "like", "elapsed_sec": 145},
      {"slot_id": 2, "account_id": null, "identity_dir": null, "pid": null, "current_step": null}
    ]
  }
}

指挥台上的显示效果

每条运行中的任务展开后:

┌─────────────────────────────────────────────────────────────┐
│ 🔴 [P0 优先] 定向评论              13:00:23  运行 1分22秒  │
│                                                              │
│  ├─ 🎯 视频: https://www.douyin.com/video/xxx               │
│  ├─ 📋 蓝图: interact_comment (5步)                        │
│  │                                                          │
│  ├─ 🟢 槽位1: douyin_136 (phone_15370103682)               │
│  │   ├─ 当前: [4/5] post_comment ████████░░ 80%            │
│  │   └─ 已完: ✅goto_url ✅wait_watch ✅open_comments       │
│  │                                                          │
│  ├─ ⏳ 排队: [douyin_138] [douyin_139] [douyin_140]        │
│  │                                                          │
│  └─ 📊 统计: 成功 3 | 失败 0 | 总耗时 82秒                 │
└─────────────────────────────────────────────────────────────┘

6.7 浏览器账号/蓝图/脚本运行状态(深潜分析)

当前问题:浏览器层面完全黑盒

问题 影响
不知道每个浏览器在跑哪个账号 无法判断"douyin_136 是否在线"
不知道每个浏览器用哪个身份目录 身份目录 = 指纹,出问题无法定位
不知道浏览器进程是否存活 进程挂了槽位还在占用,资源浪费
不知道浏览器内存/CPU消耗 无法判断"是否需要减少并发浏览器数"
没有浏览器健康度评分 频繁崩溃的浏览器需要重建身份目录

改造后:BrowserSlotManager 上报完整状态

每个浏览器槽位的状态包含:

@dataclass
class SlotStatus:
    """单个浏览器槽位的完整状态"""
    slot_id: int
    account_id: Optional[str]           # 当前运行的账号
    identity_dir: Optional[str]          # 身份目录名(=指纹ID)
    pid: Optional[int]                   # 浏览器进程PID
    blueprint: Optional[str]             # 当前执行的蓝图
    current_step: Optional[str]          # 当前原子操作
    step_index: int = 0                  # 步骤序号
    total_steps: int = 0                 # 总步骤数
    elapsed_sec: int = 0                 # 已运行秒数
    started_at: Optional[float] = None   # 开始时间戳
    cpu_percent: float = 0.0             # 进程CPU占用
    memory_mb: float = 0.0              # 进程内存占用
    health: str = "healthy"              # healthy / warning / crashed
    last_heartbeat: Optional[float] = None  # 上次活跃时间

SlotManager 的浏览器健康检查

class BrowserSlotManager:
    def check_health(self):
        """每轮 cycle 检查所有浏览器进程健康状态"""
        for slot in self.slots:
            if not slot or not slot["pid"]:
                continue
            
            # 检查进程是否存在
            try:
                os.kill(slot["pid"], 0)  # 发空信号测存活
            except OSError:
                slot["health"] = "crashed"
                slot["account_id"] = None
                self._report_crash(slot)
                continue
            
            # 检查进程 CPU/内存
            try:
                r = subprocess.run(
                    ["ps", "-p", str(slot["pid"]), "-o", "%cpu=,%mem=,rss="],
                    capture_output=True, text=True, timeout=3
                )
                parts = r.stdout.strip().split()
                if len(parts) >= 3:
                    slot["cpu_percent"] = float(parts[0])
                    slot["memory_mb"] = float(parts[2]) / 1024
                    slot["health"] = "healthy" if float(parts[0]) < 80 else "warning"
            except:
                pass

指挥台上的浏览器状态面板

┌─────────────────────────────────────────────────────────────┐
│  🌐 chengzigedeAir — 浏览器状态             3槽/3最大      │
│                                                              │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                     │
│  │ 🟢 槽位1 │  │ 🟢 槽位2 │  │ ⚪ 槽位3 │                     │
│  │         │  │         │  │         │                     │
│  │ 账号:   │  │ 账号:   │  │ 空闲    │                     │
│  │ douyin  │  │ douyin  │  │         │                     │
│  │ _136    │  │ _137    │  │         │                     │
│  │         │  │         │  │         │                     │
│  │ 📋      │  │ 📋      │  │         │                     │
│  │ interact│  │ douyin  │  │         │                     │
│  │ _comment│  │ _daily  │  │         │                     │
│  │         │  │         │  │         │                     │
│  │ ⏱ 1分22 │  │ ⏱ 2分25 │  │         │                     │
│  │ 🔄 4/5步│  │ 🔄 3/7步│  │         │                     │
│  │ 💾 220MB│  │ 💾 185MB│  │         │                     │
│  └─────────┘  └─────────┘  └─────────┘                     │
│                                                              │
│  📊 总内存: 620MB/16GB | CPU: 23% | 浏览器进程: 2/3         │
│  ⚠️ 告警: douyin_136 登录已过期(需重新登录)               │
└─────────────────────────────────────────────────────────────┘

账号健康度追踪

每执行完一个任务,更新账号状态到 task_store:

{
  "account_id": "douyin_136",
  "last_seen": "2026-06-27T13:05:23Z",
  "last_machine": "chengzigedeAir",
  "last_blueprint": "interact_comment",
  "last_result": "completed",
  "health": "login_expired",
  "health_detail": "登录态过期,本次执行已自动重登成功",
  "tasks_today": 5,
  "failed_today": 0,
  "banned": false
}

当 health 为 banned 或 login_expired 时,指挥台告警中心高亮显示。调度器在分配任务时跳过健康度异常的账号,避免无效执行。

框架评估:是否合适?

当前文档的调度层设计基本覆盖了需求,但以下三点需要加强:

缺失项 补充建议 优先级
executor 实时进度解析 文档只说了 executor "调 mc run",没说"实时解析 stdout 提取进度"。需要在 executor.py 中增加 stdout 逐行解析逻辑 P0 — 否则指挥台看不到实时进度
浏览器进程健康巡检 slot_manager 的 health check 需要增加 CPU/内存采集和 crash 检测 P0 — 否则浏览器进程挂了都不知道
账号健康度持久化 每次任务完成后更新账号状态到 task_store,供调度器决策"这个账号还适不适合分配任务" P1 — 否则会一直给封号账号发任务

七、恢复 guardd 的增强方案

7.1 guardd 新增能力

# guardd.py 原有 + 新增模块

class GuarddServer:
    """guardd 主进程 — 每个机器一个实例"""
    
    def __init__(self):
        # 原有
        self.http_server = HTTPServer(('0.0.0.0', 9090), GuarddHTTPHandler)
        self.hostname = resolve_hostname()
        
        # 新增
        self.task_store = TaskStore()              # SQLite 持久化
        self.slot_manager = BrowserSlotManager(max_slots=3)
        self.priority_queue = PriorityQueue()
        self.active_task: Optional[Task] = None
        self.heartbeat_interval = 15               # 15秒心跳
        
    def run_cycle(self):
        """主循环 — 每 15 秒执行一次"""
        while True:
            # 1. 检查当前任务状态
            self._check_active_task()
            
            # 2. 队列调度:从队列取出下一个可执行任务
            self._schedule_next()
            
            # 3. 上报心跳到 Dashboard
            self._send_heartbeat()
            
            # 4. 清理过期数据
            self._cleanup()
            
            time.sleep(self.heartbeat_interval)

7.2 与 Dashboard 的通信

guardd → Dashboard (POST /api/push/heartbeat):
  每15秒上报一次:
  {
    "hostname": "chengzigedeAir",
    "status": "online",
    "slots": {"max": 3, "used": 2, "list": ["douyin_01", "douyin_02"]},
    "active_task": {"id": "nurture_001", "type": "scheduled", "blueprint": "douyin_daily", 
                    "progress": "60%", "elapsed_sec": 7200},
    "queued": [{"id": "nurture_002", "type": "scheduled", "estimated_start": "11:00"},
               {"id": "interact_001", "type": "priority", "estimated_start": "13:00"}],
    "alerts": [{"account": "douyin_136", "type": "login_expired", "time": "09:30"}],
    "system": {"cpu": 23, "mem": 62}
  }


Dashboard → guardd (POST /task):
  下发新任务:
  {
    "task_id": "interact_20260627_001",
    "type": "priority",
    "accounts": ["douyin_136"],
    "params": {"url": "https://www.douyin.com/video/xxx", "text": "@corpus"},
    "blueprint": "interact_comment",
    "schedule": {"type": "interval", "interval_sec": 600, "interval_random": {"min": 300, "max": 900}}
  }

7.3 CommandBus 瘦身后的职责

# 瘦身后的 CommandBus — 只做三件事:
class CommandBus:
    def dispatch(self, cmd_type, accounts, params):
        """1. 按机器分组 + 模板渲染 + 投递"""
        machine_groups = self._group_by_machine(accounts)
        for machine, accts in machine_groups:
            task = self._render_task(cmd_type, accts, params)
            self._send_to_guardd(machine, task)
    
    def _send_to_guardd(self, machine, task):
        """2. 投递到目标机器的 guardd"""
        if machine == HOSTNAME:
            # 本机:直接调 guardd 内部 API
            local_guardd.submit_task(task)
        else:
            # 远程:HTTP 调 guardd
            guardd_api(f"http://{ip}:9090/task", "POST", task)
    
    def get_global_queue(self):
        """3. 聚合各机队列信息(通过心跳/API)"""
        for machine in ALL_MACHINES:
            status = self._query_guardd(machine)
            # 聚合为全局视图

八、执行流程完整示例

8.1 日常养号的一天

08:00  ── guardd 读取 ORACLE.yaml 的 schedules 节
     │   发现 08:00 douyin_daily → 生成 Task
     │   TaskScheduler 检查槽位: 3个空 → 开始执行
     │   在 3 个浏览器上同时跑 3 个账号的养号
     │   每跑完一个账号 → 释放槽位 → 启动下一个账号
     ├── 第1轮 douyin_01,02,03 并行
     ├── 第2轮 douyin_04,05,06 并行
     └── ...
11:00  ── 所有 douyin 账号跑完 → 释放槽位 → 等待下个定时任务
     │   心跳上报: 队列为空, 3个槽位空闲
     │   
13:00  ── 用户在 Dashboard 提交定向评论
     │   20个账号, 间隔5-15分钟, 视频V
     │   CommandBus 按机器分组后投递给各机 guardd
     │   guardd 拆解为子任务, 排入优先级队列
     ├── chengzigedeAir: 5个子任务, P0 插入队列头
     ├── 5kechengdeAir:  8个子任务, P0 插入队列头
     └── 7kecheng:       7个子任务, P0 插入队列头
     │   
13:05  ── chengzigedeAir: 当前养号第3轮结束
     │   检测到 P0 任务 → 暂停后续养号 → 开始执行评论
     │   账号A → 浏览器槽位1 → post_comment → 完成
     │   等待8分钟 → 账号B → 浏览器槽位1 → post_comment → 完成
     │   ...全部5个完成 → 恢复养号队列

8.2 三级接力的编排

用户提交: 三级接力, 账号A→B→C, 视频V

CommandBus.dispatch("interact", [A,B,C], {url:V, strategy:"chain"})
  │
  ├─ 按机器分组: A在机器1, B在机器2, C在机器3
  ├─ 生成3条 Task:
  │   Task-A: machine=1, chain_position="first",  schedule_type="now"
  │   Task-B: machine=2, chain_position="reply",   depends_on=["Task-A"]
  │   Task-C: machine=3, chain_position="second",  depends_on=["Task-B"]
  └─ 发送到各机 guardd
      │
      ├─ 机器1 guardd: Task-A → 立即执行 → post_comment → 完成
      │   → 把 result.comment_id 写入 task_store
      │   → Dashboard 心跳收到: Task-A COMPLETED
      │
      ├─ 机器2 guardd: Task-B 状态 WAITING_DEP
      │   → 每15秒检查 Task-A 状态
      │   → 发现 Task-A completed → 等待300秒 → 执行
      │   → 读取 Task-A.result.comment_id
      │   → reply_to_comment(comment_id) → 完成
      │   → Task-B COMPLETED
      │
      └─ 机器3 guardd: Task-C 状态 WAITING_DEP
          → 每15秒检查 Task-B 状态
          → 发现 Task-B completed → 等待300秒 → 执行
          → 读取 Task-B.result.comment_id
          → reply_to_comment(comment_id) → 完成
          → Task-C COMPLETED

Dashboard 实时显示三级进度:
  [✅] 13:00 账号A 评论成功
  [⏳] 13:05 账号B 回复A...
  [📅] 13:10 账号C 回复B (等待中)

九、与现有代码的合并策略

9.1 代码放在哪里

05_tools/00_setup/guardd/
├── guardd.py                  ← 主文件,约1400行 → 预计增加到2000行
├── modules/
│   ├── __init__.py
│   ├── heartbeat.py           ← [new] 心跳上报逻辑(从主文件抽出)
│   ├── task_store.py          ← [new] SQLite任务持久化
│   ├── priority_queue.py      ← [new] 优先级队列
│   ├── slot_manager.py        ← [new] 浏览器槽位管理
│   ├── scheduler.py           ← [new] 调度引擎
│   └── executor.py            ← [new] 任务执行器(调mc)
├── launch.sh
├── install_guardd.sh
└── com.agentos.guardd.plist

guardd.py 主文件保持不变,新增功能以模块方式导入。

9.2 不需要动的文件

文件 原因
douyin_ops.py 原子操作层,只管"怎么点"
xhs_ops.py 同上
engine.py 单次执行引擎,被 guardd Executor 调用
nurture_runner.sh 包装器,被 guardd Executor 调用
blueprints/*.json 蓝图定义不变
corpus.py 语料库不变
login_state_machine.py 登录检测逻辑不变
command_bus.py 瘦身(去掉poll/queue),保留模板渲染+分发

9.3 需要改的文件

文件 改动
guardd.py + 5个模块导入,+ 调度循环,+ 槽位管理
command_bus.py 去掉 poll 守卫线程、CMD_POLL_STRATEGY,dispatch 改为投递到 guardd
routes/ops.py + 任务管理路由
frontend/src/views/matrix-command.js 重建为联邦指挥台
frontend/src/views/matrix-interact.js 提交交互任务(已有框架需完善)

十、实施路线图

Phase 1 — guardd 调度引擎(本周)

# 任务 工作量
1.1 TaskStore (SQLite) 持久化 小
1.2 优先级队列 小
1.3 浏览器槽位管理 小
1.4 调度主循环集成到 guardd 中
1.5 guardd HTTP API 扩展 (/tasks, /task/cancel) 中
1.6 心跳增强(携带详细任务状态) 中

Phase 2 — 联邦指挥台(下周)

# 任务 工作量
2.1 Dashboard 任务状态聚合(从各机心跳读取) 中
2.2 联邦指挥台前端骨架(三机总览+时间线) 大
2.3 任务手动调度(暂停/恢复/取消/重排) 中
2.4 告警中心(封号/登录失败高亮) 中
2.5 CommandBus 瘦身(去掉poll/queue逻辑) 小

Phase 3 — 高级编排(下月)

# 任务 工作量
3.1 任务拆解(一台机器收到群组任务后拆成子任务) 中
3.2 依赖链支持(waitting_dep 状态+跨机查询) 中
3.3 三级接力全流程 大
3.4 任务失败自动重试+链式跳过 中
3.5 ORACLE.yaml 定时任务自动导入 小


十二、架构审计与改进建议(2026-06-27 评审结果)

12.1 需要你决策的议题

决策A:任务拆解位置 — ✅ 已决策

结论:选 方案A — CommandBus 拆解。

CommandBus 负责"模板渲染+按机器分组+拆解"三位一体,master 有全局子任务视图。 指挥台可直接展示各机每条子任务的进度,而不是"1条任务 20/20进度"。

决策B:Dashboard 读路径是否直连 guardd — ✅ 已决策

结论:选 方案C — 读写分离。

  • 写路径(提交任务):Dashboard → CommandBus(做机器分组+模板渲染+拆解)→ guardd
  • 读路径(查看队列):Dashboard → 直连各机 guardd HTTP API
  • 取消/暂停操作:Dashboard → 直连对应机器 guardd

CommandBus 宕机不影响看队列,但提交任务必须经过它。

12.2 我直接确认的决策

以下是我的判断,不需要你决策:

决策 结论 理由
guardd 拆分方式 拆成 modules/ 多文件 主文件1400行已到极限,新增调度逻辑后预计超2500行,必须拆分
跨机依赖通信 事件推送而非轮询 O(n²) 轮询在>10个任务时代价不可接受
guardd 单点故障 三层防护: launchd+恢复+超时兜底 guardd 崩溃不丢失任务,1秒内重生
任务存储 内存 dict + SQLite 双写 内存保性能,SQLite 保持久化
浏览器孤儿清理 guardd 启动时全量扫描 实现简单,成本低
ORACLE 同步 guardd 启动+每6小时 不是实时系统,6小时间隔足够
心跳间隔 15秒 与调度轮询周期一致,不要额外线程

12.3 guardd 拆分方案(详细讨论)

当前问题

guardd.py 目前 1386 行,HTTP 处理器、9个模块循环、心跳上报、文件同步全部在一个文件里。加上调度引擎后预计突破 2500 行,必须拆分。

拆分方案

05_tools/00_setup/guardd/
├── guardd.py                     ← 主入口(~200行),只做:
│                                     1. import 所有模块
│                                     2. 启动 HTTP Server
│                                     3. 启动主循环 run_cycle()
│
├── modules/
│   ├── __init__.py
│   ├── http_handler.py           ← [抽出] HTTP请求处理(约300行)
│   │   ├── GuarddHTTPHandler
│   │   ├── GET /health, /tasks, /task/{id}
│   │   └── POST /task, /task/{id}/cancel
│   │
│   ├── heartbeat.py              ← [抽出] 心跳上报(约200行)
│   │   ├── collect_system_stats()
│   │   ├── collect_task_stats()
│   │   └── send_heartbeat()
│   │
│   ├── task_store.py             ← [新增] 任务持久化(约200行)
│   │   ├── SQLite 存储
│   │   ├── 内存缓存(dict) + 定时写回
│   │   └── 启动时恢复未完成任务
│   │
│   ├── priority_queue.py         ← [新增] 优先级队列(约150行)
│   │   ├── heapq 实现
│   │   ├── push / pop_ready / peek
│   │   └── cancel / reorder
│   │
│   ├── slot_manager.py           ← [新增] 浏览器槽位(约150行)
│   │   ├── acquire / release / find_account
│   │   ├── 启动时扫描孤儿浏览器
│   │   └── 槽位状态上报
│   │
│   ├── scheduler.py              ← [新增] 调度引擎(约300行)
│   │   ├── run_cycle() 主循环
│   │   ├── _check_active_task()
│   │   ├── _schedule_next()
│   │   ├── _decompose_task()
│   │   └── _notify_dependents()
│   │
│   ├── executor.py               ← [新增] 任务执行器(约200行)
│   │   ├── 调 mc run / mc interact
│   │   ├── 捕获输出 + 解析结果
│   │   └── 超时控制
│   │
│   └── oracle_sync.py            ← [新增] ORACLE同步(约100行)
│       ├── 启动时导入 schedules
│       └── 每6小时增量同步
│
├── launch.sh                     ← 不变
├── install_guardd.sh              ← 不变
└── com.agentos.guardd.plist       ← 不变

拆分后主文件 (guardd.py)

#!/usr/bin/env python3
from modules.http_handler import GuarddHTTPHandler, start_http_server
from modules.heartbeat import HeartbeatReporter
from modules.task_store import TaskStore
from modules.priority_queue import PriorityQueue
from modules.slot_manager import BrowserSlotManager
from modules.scheduler import Scheduler
from modules.executor import Executor
from modules.oracle_sync import OracleSync

class GuarddServer:
    def __init__(self):
        self.task_store = TaskStore()
        self.slot_manager = BrowserSlotManager(max_slots=3)
        self.priority_queue = PriorityQueue()
        self.executor = Executor(self.task_store, self.slot_manager)
        self.oracle_sync = OracleSync(self.task_store)
        self.scheduler = Scheduler(
            self.task_store, self.priority_queue,
            self.slot_manager, self.executor
        )
        self.heartbeat = HeartbeatReporter(self.task_store, self.slot_manager, self.scheduler)

    def run(self):
        start_http_server(self)
        self.oracle_sync.sync()
        self.slot_manager.cleanup_orphans()
        self.scheduler.run_cycle()

主文件 ~150 行,只做组装。每个模块职责单一。

12.4 CommandBus 读写分离方案(详细讨论)

当前问题

CommandBus 目前既负责写(分发任务)也负责读(poll 状态、聚合队列)。导致:

  • 页面刷新要等 CommandBus 响应
  • CommandBus 挂了 Dashboard 什么都看不到
  • 代码耦合

读写分离方案

写路径(提交任务):
  前端 → POST /api/ops/run → CommandBus.dispatch()
    ├─ 按机器分组 + 模板渲染 + 任务拆解
    └─ 投递到各机 guardd

读路径(查看状态):
  前端 → GET /api/ops/queue?machine=all
    └→ 直接调各机 guardd HTTP API:
        ├─ chengzigedeAir:  GET http://127.0.0.1:9090/tasks
        ├─ 5kechengdeAir:   GET http://100.72.182.121:9090/tasks
        └─ 7kecheng:        GET http://100.65.35.28:9090/tasks
        └→ 聚合为全局视图

取消/暂停:
  前端 → POST /api/ops/cancel
    └→ 直接调对应机器 guardd: POST guardd_ip:9090/task/{id}/cancel

方案优势

对比项 现在(全走CommandBus) 读写分离后
CommandBus 宕机影响 提交任务❌ + 看队列❌ 只能提交任务❌,看队列✅
前端刷新速度 要等 CommandBus 聚合 直接查各机 guardd,快
代码复杂度 dispatch + poll + queue 混在一起 dispatch 只写,UI 只读

任务拆解在 CommandBus 中的实现

class CommandBus:
    def dispatch(self, cmd_type, accounts, params):
        machine_groups = self._group_by_machine(accounts)
        tasks = []
        for machine, accts in machine_groups.items():
            if cmd_type == "interact" and len(accts) > 1:
                # 交互任务:每个账号拆一条独立 Task
                for acct in accts:
                    task = self._render_single_task(cmd_type, [acct], params, machine)
                    task.decomposed_from = f"group_{int(time.time())}"
                    tasks.append(task)
            else:
                # 普通任务:合并成一条 Task
                task = self._render_task(cmd_type, accts, params, machine)
                tasks.append(task)
        for task in tasks:
            self._send_to_guardd(task.machine, task)
        return {"plan_id": "xxx", "count": len(tasks)}

12.5 其他重要审计建议

1. guardd HTTP 安全 — ✅ 已决策

HTTPServer 绑定到 127.0.0.1:9090,加简单 API token 认证。 各机 guardd 之间通信时携带 token,Dashboard 查询各机时也携带 token。 token 存放在各机 agent-local/identity/secrets/guardd_token,安装时自动生成。

2. 跨机依赖事件推送 — ✅ 已决策

不用轮询。每台机器的 guardd 维护一个"我依赖谁"的反向索引。 Task-A 完成后,机器1 guardd 查索引发现机器2的 Task-B 依赖它,直接 HTTP POST 通知机器2。 机器2 guardd 收到通知后将 Task-B 从 WAITING_DEP 移入 QUEUED。 超时兜底:通知丢失时,15分钟没收到则主动查一次上游。

def _notify_dependents(self, completed_task):
    for dep_task_id in self.task_store.find_dependents(completed_task.task_id):
        dep = self.task_store.get(dep_task_id)
        if dep and dep.machine != self.hostname:
            guardd_api(f"http://{dep.machine_ip}:9090/task/{dep_task_id}/dep_ready", "POST")

3. 任务超时熔断

每个 Task 增加 max_execution_sec 硬限制,超时自动标记 FAILED:

def _check_active_task(self):
    if self.active_task and self.active_task.status == "running":
        elapsed = time.time() - self.active_task.started_at
        max_time = self.active_task.params.get("max_execution_sec", 3600)
        if elapsed > max_time:
            self.active_task.status = "failed"
            self.active_task.error = f"超时 ({elapsed:.0f}s)"

4. 安全:防止重复提交

同一 task_id 重复提交时,guardd 应返回"已存在"而非重新执行。

5. guardd 单点故障防护 — ✅ 已决策

guardd 崩溃不会丢失任务,通过三层防护:

第一层:launchd 自动重启(已有) com.agentos.guardd.plist 已配置 KeepAlive=true + RunAtLoad=true。 guardd 进程崩溃后 launchd 在 1 秒内自动拉起新进程。

第二层:启动时恢复未完成任务(新增) guardd 启动时执行以下恢复流程:

guardd.startup_recovery()
  ├─ 1. 扫描孤儿浏览器进程
  │     ps aux | grep camoufox → 遍历 browser_pids/ 目录
  │     有进程但无对应槽位记录 → 标记为孤儿
  │     有关联槽位但进程不存在 → 清理槽位记录
  │
  ├─ 2. 从 task_store 加载未完成任务
  │     SELECT * FROM tasks WHERE status IN ('running', 'queued', 'waiting_dep')
  │     加载到内存队列,重置状态为 queued(重启后需要重新检查前置条件)
  │
  ├─ 3. 恢复依赖索引
  │     重建 WAITING_DEP 任务的依赖图
  │     检查依赖项是否已完成 → 已完成则入 queued 队列
  │     未完成则保留 WAITING_DEP 状态
  │
  └─ 4. 上报恢复结果到 Dashboard
      通过心跳上报恢复的任务数量

第三层:超时兜底(新增) 如果在心跳周期内(15秒)没有收到某台机器的心跳,Dashboard 标记该机器为 offline。 之前下发给该机器的任务状态变为 unknown,等机器恢复后重新同步。

12.6 调整后的实施路线图

Phase 任务 工作量
1a guardd modules/ 目录拆分 小
1b TaskStore 内存+SQLite + PriorityQueue 中
1c 启动时孤儿浏览器清理 小
1d BrowserSlotManager + 心跳增强 中
1e 调度主循环 + Executor 中
2a CommandBus 读写分离 + 任务拆解 中
2b 指挥台前端(读视图) 大
2c 跨机依赖事件推送 中
3a 指挥台操作功能(取消/暂停/重排) 大
3b ORACLE 定时任务同步 小
3c 三级接力全流程 大
3d 告警中心 中
3e 任务超时熔断 + 自动恢复 中

决策1: 调度放 guardd,不新增层级

  • guardd 已经是每台机器上的常驻进程
  • 合并比新建更轻量,减少进程数

决策2: 按意图分类,不按动作分类

  • scheduled = 定时养号(蓝图驱动,随机动作)
  • priority = 特殊交互(用户指定目标,可设置间隔)
  • 同一个原子操作可以在两类任务中出现

决策3: 浏览器槽位上移

  • 从 mc/engine.py 移到 guardd/slot_manager.py
  • 让调度层能感知和控制浏览器资源

决策4: 非抢占式插入

  • 等待当前养号轮次完成后再插入 P0 任务
  • 不强制中断 Camoufox 浏览器

决策5: CommandBus 瘦身

  • 去掉 poll 守卫线程
  • 去掉 CMD_POLL_STRATEGY
  • 只保留:按机器分组 + 模板渲染 + 投递到 guardd

十一、深化讨论与勘误(2026-06-28)

本节是对 §1-§10 的补充和修正,基于 2026-06-28 深度讨论后的结论。

11.1 任务拆解位置(修正 §12.1 决策A)

原结论(§12.1):CommandBus 拆解。

修正结论:CommandBus 按机器分组 + 模板渲染 → 投递复合任务到各机 guardd → guardd 本地拆解为最小单元。

CommandBus.dispatch()
  ├─ 1. 按 ORACLE 分组(账号→机器映射)
  ├─ 2. 模板渲染(生成完整命令字符串)
  ├─ 3. 对每台机器生成一条**复合任务**
  │    { accounts: [A,B,C], cmd_type, params, priority, interval }
  └─ 4. POST /scheduler/submit → 各机 guardd
       ↓
guardd scheduler.submit_task()
  ├─ 1. 收到复合任务
  ├─ 2. 按账号拆成 N 个最小单元
  │    task_unit = { account_id, operation, machine, priority, interval }
  ├─ 3. 每个单元独立入队(带优先级+间隔参数)
  └─ 4. 分配器按 slot 空闲情况逐个分配执行

进度回传:
  guardd 心跳(每15秒)→ 上报每个 slot 的当前进度
  → Dashboard 聚合显示各机每个账号的细粒度状态

理由:

  • 减少 HTTP 请求(1 个复合任务 = 1 次,不是 N 次)
  • guardd 本地知道 slot 实时状态(Dashboard 不知道)
  • Heartbeat 回传细粒度进度,master 可见

11.2 实施进度审计(对照 §9 和 §12.6)

§9 Phase 1 — guardd 调度引擎:

任务 状态 说明
TaskStore (SQLite) ✅ 已实现 guardd/modules/task_store.py
优先级队列 ✅ 已实现 guardd/modules/priority_queue.py
浏览器槽位管理 ✅ 已实现 guardd/modules/slot_manager.py (3 slots)
调度主循环 ⚠️ 部分实现 scheduler.py 存在但串行(1 active_task),需改为并行
HTTP API 扩展 ⚠️ 部分实现 有 /scheduler/tasks /scheduler/submit /task/{id}/stop,缺 /task/{id}/pause /task/{id}/resume
心跳增强 ❌ 未实现 当前心跳没有 per-account 粒度

§9 Phase 2 — 联邦指挥台:

任务 状态 说明
Dashboard 任务状态聚合 ⚠️ 部分实现 ops-command.js 已读取 /ops/queue 但字段路径有误(已修复)
指挥台前端骨架 ⚠️ 部分实现 三机总览已有,9 轨道 slot 视图未实现
任务手动调度 ❌ 未实现 拖拽排序/暂停/恢复未实现
告警中心 ❌ 未实现 告警面板存在但功能不完整

§9 Phase 3 — 高级编排: 均未开始。

11.3 P0/P1/P2 优先级体系(补充 §6.4)

§6.4 已有优先级标签设计,本节补充队列调度规则:

等级 标识 队列 间隔参数 说明
P0 🔴 优先 高优队列 interval=N秒(0=无间隔) 定向评论、指定点赞。可插队
P1 🟢 日常 正常队列 无 定时任务、每日养号。正常排队
P2 ⚪ 闲时 低优队列 无 批量采集、数据同步。有空位才执行

分配器优先级逻辑:

1. 检查 P0 队列 → 有任务且符合间隔条件 → 分配
2. P0 队列为空 → 检查 P1 队列 → 分配  
3. P1 队列为空 → 检查 P2 队列 → 分配
4. 所有队列为空 → slot 空闲等待

11.4 间隔执行(B 类任务补充 §5.1)

§5.1 已有间隔设计,本节补充 3 slot 交错执行细节:

复合任务 { 20 accounts, interval=30-90, priority=P0 }
  ↓ guardd 拆成 20 个单元,全部入 P0 队列(每个带 interval 参数)
  
分配器逻辑:
  for each slot in [slot1, slot2, slot3]:
    if slot.free:
      task = P0_queue.pop()
      if task.interval > 0:
        last = task_group.last_completed_time
        if now - last < task.interval:
          continue  # 间隔未到,跳过,等下次循环
      slot.assign(task)
  
  3 slot 交错流出:
    slot1: 账号A(完成30s→)账号D(完成45s→)账号G
    slot2: 账号B(完成60s→)账号E(完成35s→)账号H  
    slot3: 账号C(完成50s→)账号F(完成40s→)账号I

11.5 评论互动参数(补充 §5.1 任务参数字段)

互动任务复合任务的完整参数:

{
  "task_id": "interact_20260628_001",
  "type": "interact",
  "priority": "P0",
  "accounts": ["douyin_136", "douyin_137"],
  "interval": "30-90",
  "params": {
    "url": "https://www.douyin.com/video/xxx",
    "direction": "称赞",
    "blueprint": "interact_comment",
    "corpus": "L2"
  }
}

11.6 联邦指挥台展示(引用 §6.2-§6.5,本节仅补充重点)

§6.2-§6.5 已有详细设计,本节补充展示中缺少的信息:

每个 slot 卡片当前只显示账号ID,需补充:

  • 手机号(从 ORACLE 或 profiles.json 查)
  • 昵称(从 profiles.json 查)
  • 当前操作 URL(互动任务用)
  • 间隔倒计时(B 类任务显示下一账号剩余时间)
  • 进度百分比(当前步数/总步数)

11.7 浏览器控制权(修正 §12.3 决策3 表述)

§12.3 决策3 说"浏览器槽位上移",造成误解。实际结论:

  • mc engine 自己控制 max_browsers=3 安全上限(不下放)
  • guardd scheduler 通过 slot_manager.get_usage() 感知剩余空位
  • 不需要 scheduler 下发放"开几个浏览器"的指令
  • engine 管上限,scheduler 尊重上限

§12.3 原表述"上移"应理解为"新增 guardd 层的 slot 感知",而非"从 engine 夺权"。

11.8 §9 和 §12.6 改造顺序的合并

以 §12.6 的详细版本为准,§9 的简述版标记为参考。更新如下:

Phase 任务 工作量 状态
1a guardd modules/ 目录拆分 小 ✅ 已完成
1b TaskStore + PriorityQueue 中 ✅ 已完成
1c 启动时孤儿浏览器清理 小 ✅ 已完成
1d BrowserSlotManager + 心跳增强 中 ⚠️ slot 完成,心跳未增强
1e 调度主循环去串行化 + Executor 中 ⚠️ 需改并行
2a CommandBus 读写分离 + 复合任务投递 中 ❌
2b 指挥台前端(9 轨道 slot 视图) 大 ❌
2c P0/P1/P2 优先级 + 间隔执行 中 ❌
3a 指挥台操作功能(取消/暂停/重排) 大 ❌
3b ORACLE 定时任务同步 小 ❌
3c 告警中心 中 ❌
3d 任务超时熔断 + 自动恢复 中 ❌