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 带网关原话 / 正常返回)。
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# AgentOS 原子操作架构审计与重构方案
|
||||
|
||||
## 一、旧代码设计原则(提炼自旧版 runner.py / interact.py)
|
||||
|
||||
### 1.1 每步操作 = 状态机三要素
|
||||
|
||||
```
|
||||
[前置状态检测] → [操作执行] → [后置状态验证]
|
||||
↓ ↓ ↓
|
||||
当前在哪个页面? 执行操作 操作是否真的生效了?
|
||||
```
|
||||
|
||||
**旧代码实现**(`_ensure_player_state`、`CommentStateMachine`):
|
||||
```python
|
||||
# 前置检测
|
||||
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`)
|
||||
|
||||
旧代码用**行为链**而非固定步骤模板:
|
||||
```python
|
||||
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 新原子操作模板
|
||||
|
||||
```python
|
||||
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 统一状态检测函数
|
||||
|
||||
```python
|
||||
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小时)
|
||||
1. 实现 `detect_page_state()` — 统一状态检测
|
||||
2. 实现 `verify_*()` 系列 — 每个操作的后置验证函数
|
||||
3. 实现 `ensure_page_state(target)` — 如果不在目标状态则导航过去
|
||||
|
||||
### Phase 2:重写原子操作(2-3小时)
|
||||
4. 按模板重写 `douyin_ops.py` 中所有操作
|
||||
5. 按模板重写 `ops/xhs_ops.py` 中所有操作
|
||||
6. 移除所有无验证的假成功返回
|
||||
|
||||
### Phase 3:测试验证(1-2小时)
|
||||
7. 逐步测试每个操作(用户交互模式)
|
||||
8. 修复各平台的特定选择器/DOM差异
|
||||
|
||||
---
|
||||
|
||||
以上是完整的审计和方案。你要我按这个方向执行吗?
|
||||
Reference in New Issue
Block a user