移植自 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 带网关原话 / 正常返回)。
8.1 KiB
AgentOS 命令传导架构
版本: 1.0 | 最后更新: 2026-06-28 本文档描述从 L5 前端到 L0 浏览器的命令传导全链路设计与实际实现
一、五层传导架构
L5: 前端视图 (views/*.js)
│ POST /api/ops/run {type, accounts, params}
│
L4: API 路由层 (routes/ops.py)
│ → api_ops_run() → CommandBus.dispatch()
│
L3: 命令分发层 (CommandBus)
│ dispatch():
│ 1. ORACLE 合规检查 (account→machine映射)
│ 2. CMD_REGISTRY 查模板 / 硬编码
│ 3. 按机器分组 + 模板渲染
│ 4. ThreadPoolExecutor 并行分发
│
├──→ ▸ 新路径(优先): /scheduler/submit
│ 发送 {command_line, cmd_type, accounts, priority, interval, params}
│ → guardd Scheduler → PriorityQueue → SlotManager
│ → Executor._build_cmd() 仅加执行环境前缀
│
├──→ ▸ 旧路径(降级): /task (v7)
│ 发送 {cmd: "cd scripts && python -m ..."}
│ → guardd _start_task() → subprocess.Popen
│
└──→ ▸ 兜底路径: subprocess(本地) / SSH nohup(远程)
nurture: nurture_runner.sh / 其他: 直接 Python
L2: 调度执行器 (Scheduler + Executor)
│ Scheduler.run_cycle() 每15秒:
│ → _check_all_active_tasks() 检查3个 slot
│ → _schedule_all_slots() 分配新任务
│ → Executor.execute(task)
│ → _build_cmd(task):
│ ▸ 优先用 command_line (L3已渲染好) → 仅加 cd+python 前缀
│ ▸ 兜底: 自行硬编码构建 (向后兼容)
│ → 子进程执行 → 日志解析 → 进度更新
L1: 平台原子操作 (douyin_ops.py / xhs_ops.py)
│ → Camoufox 浏览器执行
L0: Camoufox 0.4.11 + Playwright 1.58.0
二、核心设计原则
2.1 三层降级设计
不是两条平行的命令生成路径,而是三层降级保护:
| 层级 | 路径 | 命令由谁构建 | 执行管理 | 可用条件 |
|---|---|---|---|---|
| 新路径 ⭐ | /scheduler/submit |
L3渲染cmd_line, L2只加前缀 | guardd调度器(15s+3slot) | guardd v2.3.0+ |
| 旧路径 | /task |
L3渲染完整shell命令 | guardd直接subprocess | guardd可用 |
| 兜底 | subprocess/SSH | L3渲染+执行环境 | 直接执行 | 任何时候 |
正常流中只有单一路径(新路径),每次降级都是因为上层不可用。
2.2 命令生成责任分离
| 层 | 职责 | 产出 |
|---|---|---|
| L3 CommandBus | 操作意图→CLI命令字符串 | mc run --accounts=A --blueprints=B --rounds=3 |
| L2 Executor | 加执行环境前缀 | cd scripts && PYTHONPATH=... python3 -m mc run ... |
| L2 Executor.兜底 | command_line为空时重新构建 | 同上格式的完整命令 |
2.3 任务拆解
interact 类型的多账号任务在 L3 被拆解为 per-account 子任务(command_bus.py:1062-1087行),直接投递到 guardd /scheduler/submit。每条子任务有独立 task_id 和状态追踪。
三、CMD_REGISTRY 注册表
3.1 注册表结构
CMD_REGISTRY = {
"nurture": {
# 无 template — dispatch() 硬编码(因--mix,--interval特殊参数)
"defaults": {"blueprint": "douyin_daily", "rounds": 10},
"auto_blueprint": False,
},
"collect": {
"template": "mc run --accounts={ids} --blueprints={blueprint} --rounds={rounds}",
"defaults": {"rounds": 1},
"auto_blueprint": True,
"blueprint_map": {"douyin": "douyin_read_profile", "xiaohongshu": "xiaohongshu_read_profile"},
},
"login": {
"template": "mc smart-login {ids}",
"single_account": True,
},
"logout": {
"template": "mc run --accounts={ids} --blueprints=douyin_daily --rounds=1",
},
"comment": {
"template": "mc task comment --account={ids} --url={url} --direction={direction}",
"required_params": ["url"],
},
"like": {
"template": "mc run --accounts={ids} --blueprints=douyin_daily --rounds=1",
},
"interact": {
"template": "mc run --accounts={ids} --blueprints={blueprint} --rounds=1 "
"--url={url} --direction={direction} --corpus={corpus}",
"defaults": {"blueprint": "interact_comment", "direction": "", "corpus": ""},
"required_params": ["url"],
},
}
3.2 nurture 的特殊处理
nurture 类型不经过 CMD_REGISTRY 模板渲染,dispatch() 对其做硬编码(command_bus.py:965-978行),原因是:
- 需要按平台分组(douyin/xiaohongshu 走不同的默认蓝图)
- 有固定参数
--mix --interval=45-90 - 支持通过 params 传递自定义
interval、stagger等参数
四、任务生命周期
pending → scheduled → queued → preflight → running → completed
↓ → failed
↓ → cancelled
↓ → skipped
→ paused (可恢复)
→ waiting_dep (依赖等待)
终端状态: completed / failed / cancelled / skipped 活跃状态: running / queued / preflight / paused / waiting_dep
五、调度引擎架构 (guardd)
5.1 组件关系
HTTP Server (:9090)
├── POST /scheduler/submit → api_scheduler_submit()
├── GET /scheduler/tasks → api_scheduler_status()
├── POST /task/{id}/stop → _stop_task()
├── POST /task/pause → scheduler.pause_task()
├── POST /task/resume → scheduler.resume_task()
├── POST /queue/reorder → scheduler.reorder_queue()
├── GET /accounts/status → AccountMonitor
└── GET /accounts/profiles → profiles.json
Scheduler (15秒循环)
├── PriorityQueue (heapq, P0/P1/P2)
├── BrowserSlotManager (3 slot)
├── Executor (子进程+日志解析)
└── TaskStore (内存+SQLite双写)
HeartbeatReporter (每轮上报)
├── slot使用率
├── 账号登录状态
├── 任务统计
└── 浏览器健康
ScheduleBridge (60秒检查)
└── schedule.yaml → guardd scheduler
5.2 优先级策略
| 任务类型 | 优先级 | 说明 |
|---|---|---|
| interact, comment | P0 | 用户主动交互,最高优先 |
| nurture, collect | P1 | 日常养号/采集 |
| scheduled tasks | P1 | 定时任务 |
| logout, login | P2 | 低优先 |
六、已知维护注意事项
6.1 executor._build_cmd() 兜底代码
executor.py:148-189行的 _build_cmd() 有独立于 CMD_REGISTRY 的命令构建逻辑。此代码正常流不会触发(因 command_line 始终存在),仅作为向后兼容的安全网保留。修改 dispatch() 中的命令格式时,应同步检查此处的兜底是否一致。
6.2 comment 的潜在不一致
| 来源 | comment 命令格式 |
|---|---|
| CMD_REGISTRY template | mc task comment --account=... --url=... |
| executor._build_cmd() 兜底 | mc run --accounts=... --blueprints=... --rounds=1 --url=... |
正常流走 CMD_REGISTRY,但如果兜底被触发(command_line 为空),会导致执行完全不同的命令。
6.3 routes/matrix.py 旧路由状态
| 路由 | 状态 | 说明 |
|---|---|---|
/nurture/start |
已废弃·走CommandBus | 保留兼容,内部调 CommandBus |
/nurture/preview |
只读·保留 | 排期预览,无执行操作 |
/task/run |
已废弃·走CommandBus | 保留兼容,内部调 CommandBus |
/batch-run |
已改造·走CommandBus | 2026-06-28 改造完成 |
/blueprints/{name}/execute |
保留 | 蓝图单步执行 |
/accounts/{id}/login |
保留 | 小红书走SMS/抖音走CommandBus |
6.4 命令轮询的迁移
旧版 CommandBus 有 _start_poll_guard() 轮询线程(每15秒检查各机状态)。v4.3.0 后此逻辑已迁移到 guardd 的 Scheduler.run_cycle()(15秒循环),CommandBus 中的 _start_poll_guard() 已空桩(command_bus.py:1264-1266行)。