移植自 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 带网关原话 / 正常返回)。
13 KiB
13 KiB
抖音 Web 端任务接管 — 完整方案文档
版本: 3.1 | 日期: 2026-04-27 | 平台: 抖音网页版 前置条件: Chrome + CDP 端口 9222 + Patchright 阶段A状态: ✅ 完成(原子操作库V2 + 账号切换器 + 指纹注入 + 双账号并行)
一、架构总览
自然语言指令 / 蓝图 JSON
↓
蓝图引擎 (task_engine.py)
↓
原子操作库 (douyin_ops.py)
↓
账号切换器 (switch_account.py) ← V3 新增
├── 方案A: Chrome Profile 切换 + 指纹注入
└── 方案B: Cookie 注入(实验性)
↓
CDP 连接器 (cdp_connector.py)
↓
Chrome (端口 9222/9223/...) → 抖音网页
↓
代理 IP (静态住宅 IP) ← V3 新增
核心设计原则
- 原子化:每个操作是最小不可分割单元,含前置检查+执行+后置验证
- 固定起点:所有蓝图从推荐页 (
goto_home) 出发,确保状态一致 - 选择器优先:
data-e2e> XPath > 键盘 > 鼠标坐标 - 频率安全:内置操作频率上限(点赞<20/h,评论<5/h)
- 验证码降级:评论触发验证码 → 取消 → 记录 → 继续
- 账号隔离:每个账号独立 Profile + 独立端口 + 独立 IP(V3 新增)
- 指纹稳定:同一账号永远同一指纹模板,不跨进程漂移(V3 新增)
二、原子操作清单
| 原子操作 | 方法名 | 选择器 | 键盘备选 | 后置验证 |
|---|---|---|---|---|
| 回推荐页 | goto_home() |
[data-e2e="douyin-navigation"] a:has-text("推荐") |
- | video.duration > 0 |
| 导航到URL | goto_url(url) |
- | - | URL 变化 |
| 后退 | go_back() |
- | - | URL 变化 |
| 点赞 | like() |
[data-e2e="video-player-digg"] |
Z | data-e2e-state 变化 |
| 收藏 | collect() |
[data-e2e="video-player-collect"] |
- | - |
| 关注 | follow() |
[data-e2e="feed-follow-icon"] |
F | - |
| 下翻视频 | next_video() |
[data-e2e="video-switch-next-arrow"] |
↓ | modal_id 变化 |
| 上翻视频 | prev_video() |
[data-e2e="video-switch-prev-arrow"] |
↑ | modal_id 变化 |
| 播放/暂停 | toggle_play() |
- | 空格 | video.paused 变化 |
| 搜索 | search(keyword) |
[data-e2e="searchbar-input"] |
- | URL 含 search |
| 点击搜索结果 | click_search_result(idx) |
.search-result-card |
- | URL 含 modal_id |
| 打开评论 | open_comments() |
[data-e2e="feed-comment-icon"] |
X | comment_list 出现 |
| 关闭评论 | close_comments() |
- | X | comment_list 消失 |
| 发评论 | post_comment(text) |
.public-DraftEditor-content |
Enter | verify_panel? |
| 取消验证码 | cancel_verify() |
.uc-ui-verify_sms-verify_button.second |
- | 面板消失 |
| 观看等待 | wait_watch(secs) |
- | - | 时间到 |
三、键盘快捷键
| 键 | 功能 | 可靠性 | 建议 |
|---|---|---|---|
| Z | 点赞 | ❌ 焦点问题 | 用点击选择器 |
| X | 评论 | ✅ 基本可靠 | 推荐 |
| F | 关注/进直播间 | ✅ | 推荐 |
| B | 弹幕开关 | ✅ | |
| 空格 | 播放/暂停 | ⚠️ 需焦点 | 点击视频先 |
| ↑ | 上一个视频 | ⚠️ 需焦点 | 用点击箭头 |
| ↓ | 下一个视频 | ⚠️ 需焦点 | 用点击箭头 |
| Enter | 搜索确认/评论发送 | ✅ |
四、蓝图清单
| 蓝图ID | 名称 | 步骤数 | 状态 |
|---|---|---|---|
douyin_browse_v2 |
日常浏览V2 | 11 | ✅ active |
douyin_search_browse |
搜索浏览 | 7 | ✅ active |
douyin_comment_interact |
评论互动 | 8 | ✅ active |
douyin_browse |
日常浏览V1 | 8 | ⛔ deprecated |
五、稳定链路设计
5.1 固定起点流程
所有蓝图 → 第1步 goto_home() → 等视频加载 → 后续操作
这确保每次执行从同一状态出发,不依赖上次操作的页面残留状态。
5.2 错误恢复策略
| 错误类型 | 恢复动作 |
|---|---|
| 元素未找到 | 等待1s重试 → 刷新页面重试 → 放弃当前步骤继续 |
| 验证码弹出 | cancel_verify() → 记录 → 继续 |
| 页面空白/加载超时 | goto_home() 重新开始 |
| CDP 断连 | 报错退出,调度器30分钟后重试 |
| 连续3步失败 | 熔断:暂停当前蓝图,跳到下一个 |
5.3 操作频率控制
RATE_LIMITS = {
"likes_per_hour": 20,
"comments_per_hour": 5,
"follows_per_hour": 10,
"collects_per_hour": 15,
"searches_per_hour": 10,
}
超过频率的操作自动跳过,不报错。
六、选择器维护策略
6.1 三级定位
| 优先级 | 方式 | 覆盖率 | 失败处理 |
|---|---|---|---|
| L1 | data-e2e 选择器 |
95% | 降级到 L2 |
| L2 | XPath / class 选择器 | 4% | 降级到 L3 |
| L3 | MLX 视觉定位 | 1% | 人工介入 |
6.2 变更检测
每次操作记录 locator_used,如果 L1 选择器连续失败 3 次:
- 自动写入
ui_changes表 - 标记受影响蓝图状态为
broken - 通知人工扫描页面更新选择器
七、账号切换 + 反检测方案
7.1 当前两套逻辑
| 方案 | 逻辑 | 优点 | 缺点 |
|---|---|---|---|
| A: Chrome Profile 切换 | 每个账号独立 --user-data-dir,切换 = 重启 Chrome |
Cookie 自然持久化,最简单 | 切换慢(需关闭重开Chrome),同时间只能1个账号 |
| B: Cookie 注入 | 1个浏览器实例,通过 CDP 注入不同 Cookie | 切换快(秒级),可同时多标签 | Cookie 格式复杂,部分平台有额外校验,反检测弱 |
7.2 推荐方案:A 为主,B 备用
现阶段(2-5个账号):方案 A 足够
- 每个账号一个 Chrome Profile 目录
- 切换 = 关 Chrome → 重新
launch_chrome.sh <账号ID> <端口> - 间隔 10-30 分钟自然切换
扩展阶段(5+账号):方案 B 补充
- 提取 Cookie → JSON 存储 → 注入到新上下文
- 需要
cdp_connector.py增加inject_cookies()方法
7.3 反检测清单
| 检测维度 | 当前措施 | 下一步 |
|---|---|---|
| 浏览器指纹 | 真实 Chrome(指纹原生) | 阶段B:Camoufox/Patchright 容器 |
| navigator.webdriver | Patchright 自动处理 | ✅ 已覆盖 |
| IP地址 | 同一IP(风险中等) | 静态住宅IP(每账号独立) |
| 行为模式 | 随机延迟+频率控制 | 增加鼠标轨迹仿真 |
| Cookie/Session | Chrome Profile 自然持久 | ✅ 已覆盖 |
| 操作时间 | 随机间隔 2-15s | 增加活跃时段控制(8:00-23:00) |
| 账号关联 | 同一设备不同Profile | 独立代理IP + 不同的浏览习惯 |
7.4 账号切换操作流程
1. 结束当前账号蓝图执行
2. 记录执行日志到 operation_logs
3. 关闭 Chrome(kill进程或关闭窗口)
4. 等待 30-120 秒(模拟人工间隔)
5. 启动新账号 Chrome:
bash ~/matrix/scripts/launch_chrome.sh <新账号ID> <新端口>
6. 等待 Chrome 就绪
7. 验证 Cookie 是否有效(check_login)
8. 开始新账号蓝图执行
7.5 下一步实施优先级
| 优先级 | 任务 | 预计时间 | 状态 |
|---|---|---|---|
| P0 | 账号切换脚本 switch_account.py |
2h | ✅ 已完成 |
| P0 | 指纹注入 + 双账号并行验证 | 2h | ✅ 已完成 |
| P1 | 鼠标轨迹仿真(贝塞尔曲线) | 4h | 待开发 |
| P1 | 语料库填充(评论/关键词) | 1h | 待开发 |
| P2 | Cookie 注入方案 B 完善 | 4h | 骨架已有 |
| P2 | 静态住宅IP接入 | 按服务商 | 待购买 |
| P3 | 阶段B:Docker + Camoufox | 2-3天 | 待规划 |
八、账号切换系统(V3 新增)
8.1 两种切换方案
| 方案 | 机制 | 切换速度 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| A: Profile 切换 | 关闭 Chrome → 启动新 Profile → 注入指纹 | ~15秒 | ⭐⭐⭐⭐⭐ | 日常养号 |
| B: Cookie 注入 | 清除 Cookie → 注入新 Cookie → 刷新验证 | ~5秒 | ⭐⭐⭐ | 快速切换(实验性) |
8.2 switch_account.py 用法
# 列出所有账号
python scripts/switch_account.py --list
# 查看当前状态
python scripts/switch_account.py --status
# 方案A: 切换到 douyin_01
python scripts/switch_account.py --method profile --target douyin_01 --port 9222
# 方案B: Cookie 注入切换
python scripts/switch_account.py --method cookie --target douyin_02 --port 9222
# 导出当前 Cookie(给方案B备用)
python scripts/switch_account.py --target douyin_01 --export-cookies
8.3 切换流程(方案A)
1. 关闭当前 Chrome(SIGTERM → 等待 → SIGKILL)
2. 启动新 Chrome(目标 Profile + CDP 端口 + 代理)
3. 注入浏览器指纹:
- 视口覆盖(桌面端 702x783,保持 Cookie 兼容)
- 时区覆盖(Asia/Shanghai)
- 语言覆盖(zh-CN)
- WebGL Vendor/Renderer 注入(覆盖 GPU 指纹)
- navigator.webdriver 隐藏
- navigator.plugins/languages 伪装
- App 跳转拦截(CDP Fetch)
4. 验证登录状态
5. 更新数据库
8.4 指纹模板映射
每个账号 ID 稳定映射到一个指纹模板(同一账号永远同一指纹):
| 账号 | 指纹模板 | 视口 | 设备 |
|---|---|---|---|
| douyin_01 | fp_iphone14pro | 393x852 | iPhone 14 Pro |
| douyin_02 | fp_samsung24 | 412x915 | Samsung S24 |
| xhs_01 | fp_iphone15pro | 393x852 | iPhone 15 Pro |
| zhihu_01 | fp_huawei_p60 | 393x873 | Huawei P60 |
⚠️ 当前养号场景使用桌面端视口(702x783),移动端视口会破坏 Cookie 登录态。 移动端伪装仅在特殊需要时通过 cdp_connector 单独开启。
8.5 双账号同时运行
每个账号使用不同的 CDP 端口和 Profile 目录:
# 终端1: 启动账号1(端口 9222)
python scripts/switch_account.py --method profile --target douyin_01 --port 9222
# 终端2: 启动账号2(端口 9223)
python scripts/switch_account.py --method profile --target douyin_02 --port 9223
九、反检测策略(V3 新增)
9.1 当前已实现的反检测
| 维度 | 措施 | 实现方式 | 状态 |
|---|---|---|---|
| navigator.webdriver | 隐藏自动化标记 | init_script undefined |
✅ |
| HeadlessChrome | 移除 Headless 标记 | init_script | ✅ |
| WebGL 指纹 | 覆盖 GPU 信息 | init_script | ✅ |
| App 跳转 | 拦截 deeplink | CDP Fetch | ✅ |
| 时区/语言 | 统一为中国 | CDP Emulation | ✅ |
| 浏览器 | 真实 Chrome | 非 Headless | ✅ |
| plugins | 伪装非空 | init_script | ✅ |
| Patchright | 自动处理自动化检测 | 框架级 | ✅ |
9.2 待实现的反检测
| 维度 | 方案 | 优先级 |
|---|---|---|
| 鼠标轨迹 | 贝塞尔曲线仿真(避免完美直线) | P1 |
| 输入节奏 | press_sequentially + 随机延迟 | P1(已部分实现) |
| Canvas 指纹 | 噪声注入 | P2 |
| AudioContext 指纹 | 噪声注入 | P2 |
| 字体指纹 | 覆盖字体列表 | P3 |
| IP 地址 | 静态住宅代理 | P1(见 IP 方案) |
9.3 鼠标轨迹仿真(下一步)
当前点击是"瞬移"到目标位置,这不符合人类行为。下一步实现:
# 贝塞尔曲线鼠标移动
async def human_move_mouse(page, target_x, target_y, steps=20):
"""模拟人类鼠标移动(贝塞尔曲线 + 加减速)"""
current = await page.evaluate('() => ({x: window._mx || 0, y: window._my || 0})')
start_x, start_y = current['x'], current['y']
# 随机控制点(贝塞尔曲线)
ctrl_x = start_x + (target_x - start_x) * 0.5 + random.uniform(-50, 50)
ctrl_y = start_y + (target_y - start_y) * 0.5 + random.uniform(-50, 50)
for i in range(steps):
t = i / steps
# ease-in-out 加速曲线
t = t * t * (3 - 2 * t)
x = (1-t)**2 * start_x + 2*(1-t)*t * ctrl_x + t**2 * target_x
y = (1-t)**2 * start_y + 2*(1-t)*t * ctrl_y + t**2 * target_y
await page.mouse.move(x, y)
await asyncio.sleep(random.uniform(0.01, 0.03))
十、IP 切换方案(V3 新增)
详见
docs/IP_SWITCH_GUIDE.md
10.1 核心结论
- 养号最佳选择:静态住宅 IP(固定 IP + 真实住宅 + 无限流量)
- 推荐服务商:巨量HTTP(¥3-6/个/月)、IPFoxy(¥5-8/个/月)
- 最低成本方案:家里 WiFi + 手机热点(¥0,2-3 个账号)
- 不推荐:数据中心 IP(易识别)、免费代理(不稳定)
10.2 推荐方案(5-10 个账号)
买 2-3 个中国静态住宅 IP → 每个 IP 绑 2-3 个账号
成本:¥30-50/月
10.3 技术集成
Chrome 启动时注入代理:
"$CHROME" \
--proxy-server="http://user:[email protected]:8080" \
--remote-debugging-port="$DEBUG_PORT" \
--user-data-dir="$PROFILE_DIR"
账号配置增加代理字段:
accounts:
- id: douyin_01
proxy: "http://user:pass@cn-proxy1:8080"
- id: douyin_02
proxy: "socks5://user:pass@cn-proxy2:1080"