移植自 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 带网关原话 / 正常返回)。
5.9 KiB
5.9 KiB
AgentOS 原子操作架构审计与重构方案
一、旧代码设计原则(提炼自旧版 runner.py / interact.py)
1.1 每步操作 = 状态机三要素
[前置状态检测] → [操作执行] → [后置状态验证]
↓ ↓ ↓
当前在哪个页面? 执行操作 操作是否真的生效了?
旧代码实现(_ensure_player_state、CommentStateMachine):
# 前置检测
state = await _detect_page_state(page)
# 只有确认在播放页才执行操作
if state not in ("player", "search_player"):
recovered = await _ensure_player_state(...)
# 执行
await page.keyboard.press("KeyZ")
# 后置验证
assert await _check_anchor(page, 'video_page')
1.2 锚点系统(_check_anchor)
| 锚点 | 检测方式 | 作用 |
|---|---|---|
video_page |
video 元素 > 0 |
确认在视频播放页 |
home_page |
.discover-video-card-item > 0 |
确认在首页feed |
video_playing |
!video.paused && readyState > 0 |
确认视频在播放 |
has_videos |
a[href*="/video/"] > 0 |
页面有视频链接 |
1.3 评论区状态机(CommentStateMachine)
closed → panel_open → input_focused → text_entered → sent
↓ ↓ ↓
键盘x打开 检测activeElement 检测textContent
验证has_panel 验证isEditable 验证has_text
核心:每一步都调用 detect() 验证状态是否真的变化了。
1.4 行为链(CHAINS)
旧代码用行为链而非固定步骤模板:
CHAINS = [
[('open_comment', 'KeyX'),
('type_comment', 'input'), # 调用 _send_comment 状态机
('wait_10s', 'wait'),
('scroll_comment', 'scroll')],
]
每个链中的操作都是键盘快捷键优先(KeyZ/KeyX/KeyG),DOM 兜底。
二、当前新代码的问题审计
| 问题 | 表现 | 根因 |
|---|---|---|
| 无前置状态检测 | open_video 在首页找不到卡片→硬等30s→失败 | 不先检测页面状态就直接找元素 |
| 无后置验证 | post_comment 返回"sent"但评论没发出去 | 不检查评论区是否出现了刚发的文字 |
| 无锚点系统 | 所有操作都用 CSS 类名硬匹配(CSS modules 不可靠) | 没有用 video/card 等特征判断 |
| 状态机缺失 | 点赞/收藏/评论等操作的执行结果和页面实际脱节 | 没有 detect() 验证状态变化 |
| DOM 优先于键盘 | 所有操作先找 DOM 元素,找不到才 fallback | 旧代码用键盘快捷键为主,更可靠 |
| 无回退策略 | 一个方法失败就放弃或硬等30s | 旧代码有3层回退(键盘→DOM→坐标) |
三、重构方案
3.1 新原子操作模板
async def atomic_like(page) -> OpResult:
"""点赞操作(前后状态检测模板)"""
# 1. 前置状态检测
state = await detect_page_state(page)
if state not in ('player', 'search_player'):
return OpResult('like', False, f'不在播放页(state={state})')
# 2. 操作执行(键盘优先)
await page.keyboard.press('z')
await asyncio.sleep(1)
# 3. 后置验证(检测按钮状态变化)
ok = await verify_like(page) # 检测 data-e2e-state 或 toast
if ok:
return OpResult('like', True, '👍')
# 4. 回退:DOM 点击
btn = page.locator('[data-e2e="video-player-digg"]')
if await btn.count() > 0:
await btn.click()
await asyncio.sleep(1)
ok = await verify_like(page)
return OpResult('like', ok, '👍' if ok else '-')
3.2 统一状态检测函数
async def detect_page_state(page) -> str:
"""检测当前页面状态"""
url = page.url
video_count = await page.evaluate("document.querySelectorAll('video').length")
cards = await page.evaluate(
"document.querySelectorAll('.discover-video-card-item, [data-e2e=\"alink-item\"]').length"
)
if video_count > 0 and '/video/' in url:
return 'player_full' # B模式:独立播放页
if video_count > 0 and 'modal_id' in url:
return 'player_modal' # A模式:弹窗播放
if video_count > 0:
return 'player_unknown' # 有视频,模式待定
if '/search/' in url:
return 'search'
if '/user/' in url:
return 'profile'
if cards > 0:
return 'grid' # 首页feed
return 'unknown'
3.3 每个操作绑定状态依赖
| 操作 | 前置状态要求 | 后置验证方式 |
|---|---|---|
goto_home |
(任意) | state → 'grid' |
open_video |
'grid' | state → 'player_*' |
like |
'player_*' | 按钮state变化或toast出现 |
collect |
'player_*' | 按钮state变化或toast出现 |
follow |
'player_*' | 按钮文字从"关注"变"已关注" |
open_comments |
'player_*' | state → 'comment_panel_open' |
post_comment |
'comment_panel_open' | comment_list中出现刚发的文字 |
close_comments |
'comment_panel_open' | state回到'player_*' |
next_video |
'player_*' | modal_id或video内容变化 |
search |
(任意) | state → 'search' |
go_back |
(任意) | URL或state变化 |
四、实施计划
Phase 1:基础设施(1-2小时)
- 实现
detect_page_state()— 统一状态检测 - 实现
verify_*()系列 — 每个操作的后置验证函数 - 实现
ensure_page_state(target)— 如果不在目标状态则导航过去
Phase 2:重写原子操作(2-3小时)
- 按模板重写
douyin_ops.py中所有操作 - 按模板重写
ops/xhs_ops.py中所有操作 - 移除所有无验证的假成功返回
Phase 3:测试验证(1-2小时)
- 逐步测试每个操作(用户交互模式)
- 修复各平台的特定选择器/DOM差异
以上是完整的审计和方案。你要我按这个方向执行吗?