移植自 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 带网关原话 / 正常返回)。
10 KiB
10 KiB
AgentOS 联邦系统 — 状态机架构方案 v3.0
目标:让账号在指定动作中随意切换,不受意外打扰,遇到问题自动回到正轨 基于现有
CommandBus/ORACLE.yaml/guardd框架,不重复造轮子
一、现有系统总览(读代码后的正确理解)
1.1 三台机器的真实角色
┌─ Dashboard Web 服务 ────────────────────┐
│ chengzigedeAir (master) :9988 │ ← 唯一运行 Dashboard 的机器
│ 5kechengdeAir (worker) ❌ 不跑 Web │
│ 7kecheng (worker) ❌ 不跑 Web │
└─────────────────────────────────────────┘
用户在任意电脑打开 http://100.111.43.6:9988 即可操作 Dashboard。
不是"三台都打开本地看板"。
各机器的身份归属(来自 ORACLE.yaml):
| 机器 | 身份 | 所属平台账号 |
|---|---|---|
| chengzigedeAir | phone_15370103682 | douyin_test + xhs_01 |
| douyin_133 / douyin_133_2 / douyin_134 | 各一个抖音 | |
| 5kechengdeAir | douyin_01_camo | douyin_01 + xhs_01 |
| douyin_02_camo | douyin_02 + xhs_02 | |
| douyin_camo01 | douyin_camo01 + xhs_03 | |
| 7kecheng | 13个身份 | 各一个抖音 + 小红书 |
1.2 现有五层架构(不是新设计,是读出已有的)
Layer 5: Dashboard UI (静态前端 SPA)
→ 本地访问 localhost:9988 或远程访问 100.111.43.6:9988
Layer 4: FastAPI 后端路由
→ app.py (plugins/ 插件管理, 数据聚合)
→ routes/ops.py (POST /api/ops/run — 统一执行入口)
→ routes/... (各功能路由)
Layer 3: 命令分发总线 — CommandBus
→ services/command_bus.py
→ CommandBus.dispatch(type, accounts, params)
→ 按 machine 分组 → MachineSession
→ 本机: subprocess.Popen (mc run / mc collect / ...)
→ 远程: SSH → remote_exec → 结果回传
Layer 2: 执行引擎 — mc CLI + 养号脚本
→ mc run → nurture_loop() → run_one_round()
→ 养号: nurture_blueprint.py (Python step list)
→ 登录: sms_login.py / xhs_login.py / douyin_login.py
→ 状态检测: _execute_op() + read_state() + BehaviorConfig
Layer 1: 浏览器管理层 — Camoufox
→ CDPConnector → Camoufox (Firefox)
→ 身份目录: agent-local/tools/matrix/identities/{name}/
→ guardd 守护: 每60s检测孤儿浏览器/磁盘/超时
1.3 命令分发全过程
你在 Dashboard 选账号 → 选蓝图 → 填参数 → 点执行
↓
POST /api/ops/run {
"type": "nurture",
"accounts": ["douyin_01"],
"params": {"blueprint": "douyin_daily", "rounds": 3}
}
↓
CommandBus.dispatch()
→ 从 ORACLE.yaml 查到 douyin_01 属于机器 A
→ 获取/创建 MachineSession(A)
→ preflight(): SSH可达? 活跃命令<3?
→ 本机: Popen("mc run --accounts=douyin_01 --blueprint=douyin_daily --rounds=3")
→ 远程: SSH("mc run ... > /tmp/ops_xxx.log &")
↓
GET /api/ops/status ← 轮询状态
→ 本地: 读 runtime/nurture/results/{run_id}.json
→ 远程: SSH cat 远程的结果文件
↓
执行结果写文件 → Dashboard 显示
这就是现有的框架。我 v1/v2 里写的"新五层模型"是重复造轮子,已废弃。
二、我用状态机要加的东西(不做新框架,只加新模块)
2.1 加什么
| 模块 | 位置 | 说明 | 依赖现有代码 |
|---|---|---|---|
login_state_machine.py |
matrix_modules/account/ |
登录检测 + Cookie恢复 + SMS重登 | 复用 xhs_login.py sms_login.py douyin_login.py |
blueprint_engine.py |
matrix_modules/ |
蓝图执行引擎(注入状态检查钩子) | 复用 nurture_blueprint.py runner.py 的 step 格式 |
cooldown_manager.py |
matrix_modules/nurture/ |
操作冷却管理 | 对接现有 BehaviorConfig |
vision_bridge.py |
✅ 已存在 | oMLX 视觉分析 | 独立模块 |
2.2 不加什么
| ❌ 不加 | 原因 |
|---|---|
| 新的命令分发 | CommandBus + MachineSession 已经完整 |
| 新的路由 API | ops.py 的 POST /api/ops/run 已定义格式 |
| 新的 ORACLE | ORACLE.yaml 已经是宪法 |
| 新的身份管理 | 身份目录 + 账号分配已经在 ORACLE 中 |
| 新的看板 | Dashboard 已有完整前后端 |
2.3 状态机怎么嵌入现有流程
现有流程:
POST /api/ops/run → CommandBus → subprocess(Popen mc run) → 写结果文件
我要加的 (在 mc run 内部加钩子):
mc run → 登录状态机(加在开头) → 蓝图执行 → 每步后检查验证弹窗 → 冷却管理
↑ 复用现有 nurture_blueprint.py
核心改动点:在 nurture_blueprint.py / runner.py 的执行循环里插入三个钩子:
# 现有代码 (runner.py nurture_loop)
for round in range(rounds):
for step in blueprint_steps:
result = await step.fn(page) # ← 现有代码
# 改为:
for round in range(rounds):
# 钩子1: 每轮开始前检测登录(新增)
await login_state_machine.ensure_login(page, account)
for step in blueprint_steps:
# 钩子2: 执行每步后检查验证(新增)
result = await step.fn(page) # ← 现有代码
if result == 'VERIFY':
await verification_handler.handle(page)
continue
# 钩子3: 冷却等待(新增)
await cooldown_manager.wait(step.op)
不改现有代码结构,只插入三个钩子。
三、蓝图:保持现有 JSON 格式 + 参数模板
3.1 当前 JSON 格式(保留不动)
{
"id": "douyin_comment",
"steps": [
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}},
{"step_id": 2, "op": "wait_watch", "args": {}},
{"step_id": 3, "op": "open_comments", "args": {}},
{"step_id": 4, "op": "post_comment", "args": {"text": "@comment_text"}},
{"step_id": 5, "op": "close_comments", "args": {}}
]
}
@url @corpus @keyword 这种参数模板系统很好,保留并在 Dashboard 执行时替换。
3.2 怎么区分评论的两种模式
抖音评论有两种入口(当前代码已有这两种操作):
| op | 场景 | 当前实现位置 |
|---|---|---|
post_comment |
在视频评论区直接发评论 | nurture_blueprint.py:op_comment() |
reply_comment |
回复某条已有评论 | runner.py:reply_comment (蓝图 douyin_reply 中用) |
蓝图选哪个 op 取决于你下发的参数。评论时如果给了 reply_to 就用 reply_comment,否则用 post_comment。
3.3 语料库 + 多账号跟踪评论
语料库存放:
05_tools/07_matrix/corpus/
├── corpus_praise.json → ["好作品!", "太棒了!", ...]
├── corpus_question.json → ["请问怎么做的?", ...]
└── corpus_thread.json → [
{"step1": "第一句: 好作品!", "step2": "第二句回复: 同意!", "step3": "第三句: 关注了"}
]
跟踪评论(A→B→C)通过蓝图序列实现:
# 在 sequence_runner.py 中:
SEQUENCE = [
{"blueprint": "comment", "account": "xhs_01", "params": {"url": "@input_url", "text": "@corpus_thread[0]"}},
{"blueprint": "reply_comment", "account": "douyin_test", "params": {"target_text": "@corpus_thread[0]", "reply": "@corpus_thread[1]"}},
{"blueprint": "reply_comment", "account": "douyin_133", "params": {"target_text": "@corpus_thread[1]", "reply": "@corpus_thread[2]"}},
]
每条语料的 step1/step2/step3 会被自动取用。这个不走新框架,直接走现有的 CommandBus 顺序下发。
四、登录状态机(唯一真正要写的新模块)
4.1 接口
class LoginStateMachine:
async def ensure_login(self, page, account_id: str, platform: str) -> bool:
"""确保登录,返回 True=已登录"""
async def _detect(self, page, platform: str) -> str:
"""检测状态: 'logged_in' / 'not_logged' / 'unknown'"""
# DOM 检测: xhs → .user-avatar douyin → [data-e2e=user-avatar]
async def _recover_cookie(self, page) -> bool:
"""刷新页面让 cookie 生效"""
async def _recover_sms(self, page, phone: str) -> bool:
"""SMS 验证码登录"""
# 复用 sms_login.py 的原子操作
4.2 用法
# 在 runner.py nurture_loop 开头加:
async def nurture_loop(...):
lsm = LoginStateMachine()
ok = await lsm.ensure_login(page, identity_name, platform)
if not ok:
return {"status": "failed", "error": "login_failed"}
# ... 继续现有逻辑
五、执行计划(只做增量,不重构)
| 任务 | 文件 | 工作量 | 前置依赖 |
|---|---|---|---|
| 写 LoginStateMachine | matrix_modules/account/login_state_machine.py |
半天 | 无(复用现有原子操作) |
| 加登录钩子到 nurture_loop | 改 runner.py + nurture_blueprint.py |
1小时 | LoginStateMachine |
| 加验证处理钩子到 op 执行后 | 改 runner.py |
1小时 | 无(复现现有 _handle_verify) |
| 加冷却管理 | matrix_modules/nurture/cooldown_manager.py |
1小时 | 无 |
| 语料库目录 + 蓝图参数扩展 | corpus/*.json + 改 blueprint JSON |
半天 | 无 |
| Dashboard 参数表单对接 | static/index.html (Vite 迁移后) |
跟 Vite 一起 | Vite 迁移完成 |
六、误区总结(防止再跑偏)
| 我原来写错的 | 实际上 | 参考文件 |
|---|---|---|
| "五层新架构" | 已有 CommandBus + MachineSession + ORACLE.yaml |
services/command_bus.py |
| "Dashboard三台都跑" | 只有 chengzigedeAir:9988 | DEPLOYMENT.md |
| "蓝图是新概念" | 已有 14 个 JSON + 参数模板 @url @corpus |
blueprints/*.json |
| "新框架要造" | 只在现有代码插三个钩子 | runner.py nurture_blueprint.py |
| "命令格式要定" | 已有 POST /api/ops/run 格式已定 |
routes/ops.py BUSINESS_ARCHITECTURE_v4.md |