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

15 KiB
Raw Blame History

Matrix 养号系统 — 全面架构文档 v2.2

最后更新:2026-05-10
作者:Claw
核心原则:原子操作 × 状态机 × 唯一特征码 × 蓝图链


0. 总架构哲学

每个操作都是一条因果链:

[唯一前置状态] → [原子操作] → [唯一后置状态]
         ↑                          ↓
   上一个操作的后置           下一个操作的前置

关键规则:

  1. 每个状态必须有唯一的特征码组合(URL + DOM锚点 + video数量 + 其他)
  2. 每个操作执行前必须先验证前置状态
  3. 每个操作执行后必须验证后置状态
  4. 验证失败 → 不执行操作,先恢复到一个已知状态
  5. 所有状态可查询、可追踪、可恢复

1. 完整状态机

1.1 状态定义与特征码

状态ID 特征码 描述
INIT 浏览器刚启动,无页面 初始态
LOGIN URL=douyin.com + sessionid Cookie 存在 + [data-e2e="user-avatar"] 可见 已登录首页
HOME_GRID URL=douyin.com/jingxuan + [data-e2e="alink-item"] >= 3 + video=1(背景) 精选页卡片列表
HOME_RECOMMEND URL=douyin.com/?recommend=1 + [data-e2e="alink-item"] >= 3 推荐页卡片列表
VIDEO_PLAYER URL 含 /video/ 或 modal_id + video>=2 + [data-e2e="video-player-digg"] 存在 视频播放页
COMMENT_PANEL VIDEO_PLAYER + [data-e2e="comment-list"] 可见 + .public-DraftEditor-content 存在 评论区已打开
INPUT_FOCUSED COMMENT_PANEL + document.activeElement 是 .public-DraftEditor-content + 光标闪烁 输入框已聚焦
TEXT_ENTERED INPUT_FOCUSED + .public-DraftEditor-content 的 textContent.length > 0 文字已输入
COMMENT_SENT 评论已发送 + [data-e2e="comment-list"] 内含刚发的文本 评论已发送
SEARCH URL 含 /search/ + [data-e2e="searchbar-input"] 搜索结果页
VERIFY_CODE .second-verify-panel 或 input[placeholder*="验证码"] 可见 验证码弹窗
UNKNOWN 无法匹配任何已知特征码 未知状态

1.2 状态迁移图

INIT → LOGIN → HOME_GRID → VIDEO_PLAYER → COMMENT_PANEL → INPUT_FOCUSED → TEXT_ENTERED → COMMENT_SENT
       ↓            ↓              ↓              ↓               ↓               ↓
     HOME_GRID   SEARCH     ←回首页→        VERIFY_CODE    ←聚焦失败→      发送失败→
       ↓                                                                       ↑
    VIDEO_PLAYER ←—— 点卡片/导航视频URL ————————————————————————————————————————

1.3 状态检测函数

核心检测函数 detect_state(page) 返回当前状态ID:

async def detect_state(page) -> str:
    """返回当前状态ID(唯一可识别)"""
    # 检测优先级:从最具体到最通用
    # 1. 验证码弹窗
    if has_verify_popup: return "VERIFY_CODE"
    # 2. 评论发送成功
    if has_comment_list_with_text: return "COMMENT_SENT"
    # 3. 文字已输入
    if editor_has_text: return "TEXT_ENTERED"
    # 4. 输入框聚焦
    if editor_is_focused: return "INPUT_FOCUSED"
    # 5. 评论区打开
    if comment_panel_visible: return "COMMENT_PANEL"
    # 6. 视频播放
    if video_player_active: return "VIDEO_PLAYER"
    # 7. 搜索结果
    if search_page: return "SEARCH"
    # 8. 首页/推荐
    if home_page: return "HOME_GRID" / "HOME_RECOMMEND"
    # 9. 已登录
    if logged_in: return "LOGIN"
    return "UNKNOWN"

2. 原子操作完整清单

每个原子操作 = (名称, 前置状态, 操作, 后置状态, 超时, 重试策略)

2.1 登录段

原子操作 前置 操作 后置 特征码变化
navigate_login INIT goto douyin.com → 等待页面加载 LOGIN URL→douyin + sessionid存在

2.2 首页段

原子操作 前置 操作 后置 特征码变化
goto_home LOGIN/任意 导航到 douyin.com/?recommend=1 HOME_GRID URL→jingxuan + alink-item≥3
click_video_card HOME_GRID 双击 .discover-video-card-item 或 [data-e2e="alink-item"] VIDEO_PLAYER video≥2 + /video/ 或 player UI
search_keyword HOME_GRID 输入关键词+Enter SEARCH URL→/search/
click_search_result SEARCH 点击第一个结果卡片 VIDEO_PLAYER video≥2
scroll_feed HOME_GRID 鼠标滚轮/键盘下滑 HOME_GRID URL不变,卡片刷新

2.3 视频播放段

原子操作 前置 操作 后置 特征码变化
wait_watch VIDEO_PLAYER 等待N秒 VIDEO_PLAYER video 内容变化(可选)
like_video VIDEO_PLAYER 按Z/点digg按钮 VIDEO_PLAYER digg_state→digged
collect_video VIDEO_PLAYER 点收藏按钮 VIDEO_PLAYER 收藏状态变化
follow_author VIDEO_PLAYER 按G/点关注 VIDEO_PLAYER 关注按钮变化
next_video VIDEO_PLAYER 键盘ArrowDown / 滚轮 VIDEO_PLAYER video.src变化
open_comments VIDEO_PLAYER 按X / 点评论图标 COMMENT_PANEL comment-list可见 + .public-DraftEditor-content存在

2.4 评论段

原子操作 前置 操作 后置 特征码变化
focus_editor COMMENT_PANEL Playwright locator双击 .public-DraftEditor-content INPUT_FOCUSED activeElement=编辑器
type_comment INPUT_FOCUSED page.keyboard.type() 逐字输入 TEXT_ENTERED editor textContent>0
send_comment TEXT_ENTERED 点击发送按钮 / Ctrl+Enter / Alt+Enter COMMENT_SENT / VERIFY_CODE comment-list含文本
close_comments COMMENT_PANEL 按X / Escape VIDEO_PLAYER comment-list消失

2.5 恢复段

原子操作 前置 操作 后置 特征码变化
recover_home 任意 导航到 douyin.com → 等待卡片加载 HOME_GRID URL+卡片
recover_video HOME_GRID/任意 点卡片进入视频 VIDEO_PLAYER video≥2
recover_comments VIDEO_PLAYER 开评论(键盘+DOM双策略) COMMENT_PANEL comment-list可见

3. 完整蓝图链:评论流程

┌─────────────────────────────────────────────────────────┐
│                  蓝图:douyin_comment_send               │
├─────────────────────────────────────────────────────────┤
│ Step 1: goto_home                                      │
│   pre:  INIT or any       post: HOME_GRID              │
│   anchor: URL=jingxuan + alink-item≥3                  │
│   fail: 重试3次,截图                                     │
├─────────────────────────────────────────────────────────┤
│ Step 2: click_video_card                               │
│   pre:  HOME_GRID          post: VIDEO_PLAYER          │
│   anchor: video≥2 + player UI                          │
│   操作: 双击 .discover-video-card-item 或 alink-item     │
│   fail: goto_home → 重试                                  │
├─────────────────────────────────────────────────────────┤
│ Step 3: open_comments                                  │
│   pre:  VIDEO_PLAYER      post: COMMENT_PANEL          │
│   anchor: comment-list可见 + Draft-editor存在             │
│   操作: 键盘X / DOM点评论图标 / 键盘x                      │
│   fail: 恢复VIDEO_PLAYER → 重试                           │
├─────────────────────────────────────────────────────────┤
│ Step 4: focus_editor                                   │
│   pre:  COMMENT_PANEL     post: INPUT_FOCUSED          │
│   anchor: activeElement = .public-DraftEditor-content   │
│   操作: Playwright locator 双击编辑器                      │
│   兜底: DOM focus + click / 坐标(479,687)双击             │
│   fail: 恢复COMMENT_PANEL → 重试                          │
├─────────────────────────────────────────────────────────┤
│ Step 5: type_comment                                   │
│   pre:  INPUT_FOCUSED     post: TEXT_ENTERED           │
│   anchor: editor.textContent.length > 0                │
│   操作: page.keyboard.type(text, delay=40)              │
│   兜底: execCommand('insertText')逐字符注入               │
│   等待: 1.5s 让 React 处理(发送按钮出现)                  │
├─────────────────────────────────────────────────────────┤
│ Step 6: send_comment                                   │
│   pre:  TEXT_ENTERED      post: COMMENT_SENT           │
│   anchor: comment-list含刚发的文本                        │
│   操作: 找发送按钮点击 / Ctrl+Enter / Alt+Enter           │
│   兜底: 3次尝试找按钮 → 键盘兜底                            │
│   验证: 检查 [data-e2e="comment-list"] 内文本             │
│   fail: 检测 VERIFY_CODE                                 │
├─────────────────────────────────────────────────────────┤
│ Step 7: goto_home (下一轮)                              │
│   pre:  COMMENT_SENT      post: HOME_GRID              │
│   操作: 导航到 douyin.com                                 │
└─────────────────────────────────────────────────────────┘

4. 数据表结构

atomic_ops — 原子操作注册表

字段 类型 说明
id INTEGER PK
op_name TEXT UNIQUE 操作名
description TEXT 描述
action_type TEXT click/nav/key/eval/wait
target TEXT 操作目标(选择器/键名/URL)
pre_state TEXT 前置状态ID
post_state TEXT 后置状态ID
timeout INTEGER 超时秒数
retry INTEGER 重试次数
verify_rule TEXT 后置验证规则(JSON)
created_at DATETIME

anchor_rules — 状态特征码规则

字段 类型 说明
id INTEGER PK
state_name TEXT 状态ID
field TEXT 检测字段(url/videoCount/selector等)
operator TEXT eq/neq/gt/gte/lt/lte/includes
value TEXT 期望值
logic_group INTEGER 同一组AND,不同组OR
active BOOLEAN
created_at DATETIME

state_transitions — 合法状态迁移

字段 类型 说明
id INTEGER PK
from_state TEXT 起始状态
to_state TEXT 目标状态
via_op TEXT 通过哪个操作
weight INTEGER 权重(优先级)

action_logs — 操作执行日志

字段 类型 说明
id INTEGER PK
execution_id TEXT 执行批次
identity TEXT 账号ID
op_name TEXT 操作名
pre_state TEXT 执行前状态
post_state TEXT 执行后状态
success BOOLEAN
duration_ms INTEGER
error TEXT
created_at DATETIME

blueprints — 蓝图定义

字段 类型 说明
id TEXT PK 蓝图ID
name TEXT 名称
version TEXT 版本
steps JSON 步骤序列
created_at DATETIME

5. 关键选择器清单

5.1 页面检测

检测项 选择器/代码 说明
视频播放页 document.querySelectorAll('video').length >= 2 + [data-e2e="video-player-digg"] video≥2 + player按钮
评论区打开 [data-e2e="comment-list"] + .public-DraftEditor-content 列表+编辑器都存在
输入框聚焦 document.activeElement === document.querySelector('.public-DraftEditor-content') activeElement是编辑器
输入框有内容 .public-DraftEditor-content.textContent.length > 0 编辑器内有文字
验证码弹窗 .second-verify-panel / input[placeholder*="验证码"]
首页卡片 [data-e2e="alink-item"] / .discover-video-card-item

5.2 操作目标

操作 方式 目标
进入视频 Playwright locator双击 .discover-video-card-item 或 [data-e2e="alink-item"]
打开评论 键盘x / DOM 点 [data-e2e="video-comment-count"] 或 [data-e2e="feed-comment-icon"]
聚焦编辑器 Playwright locator双击 .public-DraftEditor-content
输入文字 page.keyboard.type(text) 已聚焦的activeElement
发送 DOM找按钮点击 按钮含"发送"/上箭头 / Ctrl+Enter / Alt+Enter

6. 已校准配置

参数 窗口 值 校准日期
输入框中心X 702×783 479 2026-05-10 (实测算)
输入框中心Y 702×783 687 2026-05-10 (实测算)
离右边缘 702 223px 2026-05-10
离底边缘 783 96px 2026-05-10
编辑器选择器 - .public-DraftEditor-content 2026-05-10

7. 已知问题与修复记录

问题 状态 根因 修复
a[href*="/video/"] 不存在 ✅ 已修 抖音用JS点击事件,不用a标签 改用 [data-e2e="alink-item"]
_detect_page_state 太严格 ✅ 已修 要求video≥2+playerUI才判player 简化:有video就算player
KeyX 不生效 ✅ 已修 没先点视频区域获取焦点 加 _ensure_video_focused() 再按x
焦点检测在两快键之间 ✅ 已修 page.evaluate 干扰焦点 删除中间的detect调用
评论验证假阳性 ✅ 已修 body.innerText.includes 命中input内容 改查 [data-e2e="comment-list"]
execCommand不触发React ⚠️ 待测 Draft.js拦截DOM修改 改用 page.keyboard.type()
Camoufox profile lock ⚠️ 待修 强杀不释放 .parentlock 每次启动前检测+清理

8. 后续开发规范

  1. 任何修改前先确认状态 — 读当前状态ID,只执行当前状态允许的操作
  2. 一次只改一个原子操作 — 不改整条链,改完就测
  3. 改完更新文档 — 状态机、蓝图、选择器、配置同步更新
  4. 所有坐标必须从配置读 — 不硬编码,ui_layout.py 统一管理
  5. 验证防假阳性 — 检查特定元素,不用 body.innerText.includes()
  6. 双击之间不插操作 — 不调用 page.evaluate 或其他可能干扰焦点的操作