移植自 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 带网关原话 / 正常返回)。
15 KiB
15 KiB
AgentOS 任务编排与调度架构 v1.0
版本: 1.0 | 日期: 2026-07-04 | 基于全部现有代码审计 + 需求讨论 前置阅读:
PLANS/SCHEDULER_ARCHITECTURE_v3.md(调度引擎原始设计) 本文件替代: 旧版调度设计中的 Phase 2/3 部分,整合为新架构
一、设计目标
| 目标 | 说明 |
|---|---|
| 评论与视频内容关联 | 评论不再随机,根据视频标题/标签匹配行业语料 |
| 账号行业隔离 | 大健康账号发健康评论,通用账号发通用评论,互不串 |
| 三优先级调度 | P0优先/P1日常/P2劣后,插队不中断浏览器 |
| 账号互斥 | 同一账号不能同时出现在两个浏览器 |
| 中央分析+远程执行 | Master统一做视频分析/分类/选评论,Worker只执行 |
二、最小任务单元
2.1 定义
最小任务单元 = {account_id, operation_type, params, machine, priority}
一个任务单元 = 一个账号 × 一次浏览器操作的组合,不可再拆。
2.2 任务类型
| operation_type | 说明 | 默认优先级 | 来源 |
|---|---|---|---|
smart_comment |
智能评论(带视频分析+行业匹配) | P0 | Dashboard 提交 |
comment |
定向评论(已有模式) | P0 | Dashboard / CLI |
nurture |
日常养号 | P1 | 定时任务 / 手动 |
collect |
信息采集 | P2 | 手动 / 定时 |
like |
点赞互动 | P1 | 蓝图 |
login |
登录(特殊,不走调度器) | - | 单独处理 |
2.3 任务状态
PENDING → QUEUED → RUNNING → COMPLETED
↓ ↓
FAILED CANCELLED
三、三层编排体系
3.1 系统分层
用户提交(Dashboard / API)
│
▼
┌── L3: Master 中央编排 ──────────────────────────────┐
│ (chengzigedeAir) │
│ │
│ ① 接收任务(url列表 + 账号列表 + 操作类型) │
│ │
│ ② 视频分析(本机浏览器提取信息) │
│ ├─ 标题 / 描述 / 标签 │
│ ├─ 关键词匹配 → 分类(医疗健康 / 通用) │
│ └─ 从对应语料池选评论 │
│ │
│ ③ 拆解为最小单元 │
│ {douyin_133, comment, url, "医生讲得很清楚"} │
│ │
│ ④ 按机器打包 │
│ 7kecheng → [5个单元] │
│ 5kechengdeAir → [3个单元] │
│ │
│ ⑤ POST /scheduler/submit → 各机 guardd │
└────────────────────────────┬──────────────────────────┘
│ Tailscale
▼
┌── L2: 各机 guardd 调度器 ──────────────────────────┐
│ (7kecheng / 5kechengdeAir) │
│ │
│ 收到大包 → 展开为独立任务 │
│ │
│ 三队列管理: │
│ P0 优先: 智能评论、定向操作(当前任务结束后插队) │
│ P1 日常: 养号、点赞(FIFO 正常排队) │
│ P2 劣后: 采集、同步(slot 空闲才执行) │
│ │
│ 账号互斥守卫 │
│ │
│ 3 Slot 流水线 │
│ slot0: [P1→P1→P1] │
│ slot1: [P1→P0→P1] ← P0插队 │
│ slot2: [P2→空闲→P2] ← P2填空 │
└──────────────────────────────────────────────────────┘
3.2 各程序职责
| 节点 | 程序 | 职责 |
|---|---|---|
| Master | CommandBus | 接收请求→拆解→按机器分组→分发 |
| Master | VideoAnalyzer 🆕 | 视频URL分析、行业分类、选评论 |
| Master | Dashboard API | 前端界面、健康监控 |
| Worker | guardd HTTP Server | 接收任务、返回状态 |
| Worker | Scheduler 🆕改 | 三队列管理、3 slot流水线、P0抢插、账号互斥 |
| Worker | Executor | 子进程执行(mc run / mc task comment) |
| Worker | SlotManager | 浏览器槽位管理、进程监控 |
| Worker | HeartbeatReporter | 每15秒上报slot/队列/账号状态到Dashboard |
四、三队列调度算法
4.1 队列结构
class Scheduler:
# 三个独立优先队列
queue_priority = PriorityQueue() # P0: 智能评论/定向操作
queue_normal = PriorityQueue() # P1: 养号/点赞/日常
queue_filler = PriorityQueue() # P2: 采集/同步/劣后
# 3 条流水线
active_tasks: dict[int, dict] # {slot_id: task}
# 账号互斥表
account_slots: dict[str, int] # {account_id: slot_id}
# 被 P0 打断的 P1 任务暂存
paused_tasks: dict[int, dict] # {slot_id: task}
4.2 调度循环(每15秒)
run_cycle():
step 1: 检查所有 slot
任务完成 → 释放 slot + 从 account_slots 中移除账号
step 2: 检查暂停队列
有被 P0 打断的 P1 任务 → 放回日常队列恢复
step 3: 分配空闲 slot
for slot_id in [0, 1, 2]:
if slot 正在运行: continue
# 取当前活跃账号列表
busy = set(account_slots.values())
# 按优先级取任务(避开 busy 账号)
task = pop_available(P0优先, P1日常, P2劣后)
if task:
account_slots[task.account] = slot_id
start_execution(slot_id, task)
4.3 P0 抢占逻辑
收到 P0 任务 → 检查目标账号:
├─ 空闲 → 直接分配给空闲 slot
└─ 忙碌 → 等到当前任务结束后插入:
slot0: [P1养号_01, ▶️P0评论, P1养号_02, P1养号_03]
↑ P1完成后立即执行P0
P0完成后恢复P1队列
不中断正在执行的浏览器(不强行 kill 进程)
4.4 P2 劣后逻辑
P2 只在 slot 空闲且 P0/P1 都为空时才执行
P2 可被任何 P0/P1 抢占:
slot2: [▶️P2采集] → 收到P1养号 → P2回队列 → [▶️P1养号] → P1完成 → [▶️P2采集]
五、账号互斥守卫(三层检查)
5.1 检查层级
| 层级 | 位置 | 检查内容 | 阻止时机 |
|---|---|---|---|
| 第1层 | submit_task() |
该账号在 task_store 中是否有 running/queued 状态 | 入队时拒绝 |
| 第2层 | _schedule_all_slots() |
该账号在 account_slots 表中是否存在 |
分配时跳过 |
| 第3层 | slot_manager.find_account() |
该账号是否已打开浏览器 | 执行前拦截 |
5.2 账号状态表
account_slots = {
"douyin_133": 0, # slot 0 正在运行
"douyin_134": 1, # slot 1 正在运行
# "douyin_135" → 不在表中 = 空闲
}
任务完成自动移除,异常崩溃时 scheduler 清理。
六、视频分析引擎(VideoAnalyzer)
6.1 模块说明
新建文件: agent-sync/05_tools/10_dashboard/services/video_analyzer.py
运行位置: 仅 Master(本机),Worker 不需要
使用技术: Camoufox 浏览器(已有)+ 关键词匹配(已有)
6.2 核心逻辑
class VideoAnalyzer:
"""视频分析器:提取信息 → 行业分类 → 选评论"""
# 行业关键词映射
INDUSTRY_TAGS = {
"medical": ["医生","医院","药","健康","养生","中医","体检",
"症状","治疗","康复","营养","饮食","锻炼","专家"],
"finance": ["股票","基金","理财","投资","经济","A股","财经"],
"tech": ["手机","数码","电脑","科技","AI","人工智能","评测"],
"food": ["美食","做饭","菜谱","餐厅","好吃","探店"],
}
async def analyze(self, url: str) -> VideoContext:
"""分析单个视频URL
1. Camoufox 浏览器打开 URL
2. 提取: 标题(document.title) / meta描述 / 标签
3. 读取评论区热门评论(可选)
4. 返回 VideoContext
"""
def classify(self, context: VideoContext) -> str:
"""分类行业"""
for ind, tags in INDUSTRY_TAGS.items():
if any(tag in context.title for tag in tags):
return ind
return "general" # 兜底
def pick_comment(self, industry: str, account_industry: str, direction: str) -> str:
"""选评论
账号行业匹配视频行业 → 行业语料
不匹配 → 万能语料
"""
6.3 与现有能力的关系
| 现有能力 | 是否使用 | 说明 |
|---|---|---|
| Camoufox + Playwright | ✅ 直接使用 | 已有,打开视频页取信息 |
| douyin_ops.goto_url() | ✅ 直接使用 | 已有,导航到指定URL |
| CorpusManager | ✅ 改造使用 | 加行业过滤 + 万能池 |
| Scrapling / Crawl4AI | ❌ 不使用 | 抓不了抖音动态内容 |
| ChromaDB (agent-local/vector_db/) | ⚠️ 可选 | Phase 2 加向量分类时可复用 |
七、语料库结构
7.1 两层语料池
# corpus/douyin.yaml
categories:
# ── 万能池(所有账号可用,不匹配行业时兜底)──
万能称赞:
accessible: "*"
comments:
- 博主说得很对
- 讲得不错,学到了
- 分析得很到位
万能提问:
accessible: "*"
comments:
- 请问这个怎么学?
- 大概需要多长时间?
万能共鸣:
accessible: "*"
comments:
- 太真实了
- 说出了我的心声
# ── 行业池(仅对应行业账号可用)──
大健康称赞:
accessible: ["health"]
match_tags: ["医生","医院","药","健康","养生","中医","体检","症状"]
comments:
- 医生讲得很清楚,通俗易懂
- 这个方子很实用,收藏了
- 讲得很专业,学到了
大健康提问:
accessible: ["health"]
comments:
- 这个症状一般怎么处理?
- 平时饮食有什么需要注意的?
7.2 账号行业标记
// profiles.json
{
"douyin_133": {"nickname":"苏州胃肠体检敏敏", "industry":"health"},
"douyin_test": {"nickname":"小美养生茶", "industry":"health"},
"douyin_134": {"nickname":"在苏州呀", "industry":"general"},
"xhs_01": {"nickname":"...", "industry":"general"}
}
两种行业:health(大健康)、general(通用)。现阶段统一。
八、完整数据流示例
场景:提交 20 个账号评论 1 个视频
Step 1: Master 收到请求
POST /api/ops/run {type:"smart_comment", urls:["https://..."], accounts:[...]}
↓
Step 2: VideoAnalyzer 分析视频
本机 Camoufox 打开视频URL
→ 标题: "胃镜检查到底疼不疼?医生告诉你真相"
→ 关键词匹配: "医生"、"胃" → industry: medical
→ 选评论: "医生讲得很清楚,通俗易懂"
↓
Step 3: 拆解最小单元
20个账号 × 1个视频 = 20个任务单元
每个单元 = {account_id, comment:"医生讲得很清楚", url, priority:P0}
↓
Step 4: 按机器打包
查 ORACLE 账号→机器分配表:
7kecheng → 12个账号 → 12个单元
5kechengdeAir → 8个账号 → 8个单元
↓
Step 5: 分发到各机 guardd
POST http://100.65.35.28:9090/scheduler/submit (12个P0任务)
POST http://100.72.182.121:9090/scheduler/submit (8个P0任务)
↓
Step 6: 各机 scheduler 处理
7kecheng:
slot0: [P1养号, P1养号, P1养号] (正在执行)
slot1: [P1养号, P1养号, P1养号]
slot2: [P2采集, P2采集]
插入 P0 评论包:
slot0: [P1养号, ▶️P0评论×4, 恢复P1养号]
slot1: [P1养号, ▶️P0评论×4, 恢复P1养号]
slot2: [▶️P0评论×4, P2采集, P2采集]
↓
Step 7: 各账号打开视频 → 贴预选评论 → 验证
(Worker 只做这一步,不做任何分析)
九、要改的文件清单
Phase 1:队列改造(核心)
| 文件 | 改动 | 工作量 |
|---|---|---|
guardd/modules/scheduler.py |
三队列分立 + P0抢插 + P2填空 + 账号互斥表 | 大(~200行) |
guardd/modules/priority_queue.py |
现有类不需要大改,加pop_if()方法 |
小(~20行) |
Phase 2:语料重组
| 文件 | 改动 | 工作量 |
|---|---|---|
corpus/douyin.yaml |
万能池 + 行业池 分层结构 | 中(重组) |
scripts/mc/corpus.py |
get_comment_for_video() 加行业过滤 + 万能兜底 |
中(~50行) |
profiles.json |
各账号加 industry 字段 |
手动 |
Phase 3:视频分析 + 智能评论
| 文件 | 改动 | 工作量 |
|---|---|---|
新建 services/video_analyzer.py |
视频分析+行业分类+选评论 | 中(~150行) |
services/command_bus.py |
CMD_REGISTRY 加 smart_comment 类型 |
小(~20行) |
routes/ops.py |
加智能评论端点 | 小(~10行) |
Phase 4:指挥台展示(可选)
| 文件 | 改动 | 工作量 |
|---|---|---|
frontend/views/ops-command.js |
显示三队列 + P0/P1/P2 过滤 | 中(~80行) |
十、回退方案
| 标签 | commit | 说明 |
|---|---|---|
pre-task-architecture |
7d2b5b14d |
改架构前的完整备份 |
stable-ba34e3d56 |
ba34e3d56 |
再往前的稳定版 |
pre-fix-backup |
略 | 更早备份 |
# 回退命令
git checkout pre-task-architecture
git push origin --force HEAD:main
十一、暂不处理
| 事项 | 原因 |
|---|---|
| 三级接力评论 | 需求暂缓 |
| 向量模型分类 (ChromaDB) | Phase 2 可选,关键词匹配先行 |
| 视频内容深度分析(字幕/画面) | 复杂度高,当前只分析标题+描述 |
| AI 生成评论(大模型) | 需配置 API key,后续可加 |
| 评论风格跟随热门评论 ("跟风") | 后续可加 |