移植自 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 带网关原话 / 正常返回)。
1448 lines
58 KiB
Markdown
1448 lines
58 KiB
Markdown
# 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 任务分类(按意图)
|
||
|
||
```json
|
||
{
|
||
"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 账号状态跟踪
|
||
|
||
每个账号在执行过程中会维护状态,任务完成后回写:
|
||
|
||
```json
|
||
{
|
||
"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** 统一管理本机浏览器槽位:
|
||
|
||
```python
|
||
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 心跳中的任务状态(升级后)
|
||
|
||
```json
|
||
{
|
||
"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 上报完整状态
|
||
|
||
每个浏览器槽位的状态包含:
|
||
|
||
```python
|
||
@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 的浏览器健康检查
|
||
|
||
```python
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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 新增能力
|
||
|
||
```python
|
||
# 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 瘦身后的职责
|
||
|
||
```python
|
||
# 瘦身后的 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)
|
||
|
||
```python
|
||
#!/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 中的实现
|
||
|
||
```python
|
||
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分钟没收到则主动查一次上游。
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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 任务参数字段)
|
||
|
||
互动任务复合任务的完整参数:
|
||
|
||
```json
|
||
{
|
||
"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** | 任务超时熔断 + 自动恢复 | 中 | ❌ |
|