Files
MediaCrawler/mac-agent-os-main/archive_docs/STATE_MACHINE_ARCHITECTURE.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

267 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 的执行循环里插入三个钩子**:
```python
# 现有代码 (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 格式(保留不动)
```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)通过蓝图序列实现**:
```python
# 在 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 接口
```python
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 用法
```python
# 在 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` |