feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 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:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,357 @@
# 🦀 身份工厂:反检测 + 持久化标准化方案
> 基于 Camoufox 的「反检测伪装」+「持久化上下文」构建的多账号身份管理系统
> 版本: v1.0 | 日期: 2026-05-03
---
## 一、核心认知纠偏
### 1.1 反检测 ≠ 反持久化
| 概念 | 定义 | 关系 |
|---|---|---|
| **反检测 (Anti-Detection)** | 让网站无法识别你是自动化工具/不同身份 | Camoufox 的**设计目标** |
| **反持久化 (Anti-Persistence)** | 每次关闭后清空所有状态 | Camoufox 的**默认行为**(不是设计目标) |
| **持久化 (Persistence)** | 保存并复用登录态、浏览数据 | Camoufox **完全支持**,只是需要正确配置 |
**关键结论**:Camoufox 的默认行为(临时 profile + 随机指纹)是为了开发测试方便,**不代表它不支持持久化**。其 Playwright API 天然支持 `launch_persistent_context` + `user_data_dir`。
### 1.2 Camoufox 的真正优势
| 维度 | Chrome Profiles | Camoufox 持久化 |
|---|---|---|
| 指纹隔离 | ❌ 同一台机器的真实指纹 | ✅ 每身份独立伪造指纹 |
| 反检测深度 | ❌ JS 层修补(易被检测) | ✅ C++ 源码级修改(底层注入噪声) |
| 内核优势 | ❌ Chrome 被重点监控 | ✅ Firefox 冷门,避开重点关照 |
| Cloudflare 通过率 | ~50% | **88.58%**(2026 评测) |
| 身份隔离 | ❌ 数据隔离 ≠ 身份隔离 | ✅ 真正的"不同人用不同电脑" |
| 持久化能力 | ✅ user-data-dir 原生支持 | ✅ persistent_context + user_data_dir |
### 1.3 当前代码的三个致命缺陷
#### 缺陷一:没有使用持久化上下文
**位置**:`cdp_connector.py:69-104` — `_launch_camoufox()`
```python
cf = AsyncCamoufox(**kwargs)
self._camoufox_browser = await cf.start()
```
`start()` → `AsyncNewBrowser(..., persistent_context=False)` → `playwright.firefox.launch()`(临时 profile)
**影响**:每次启动生成 `/tmp/playwright_xxx/` 临时目录,所有登录态丢失。
#### 缺陷二:没有固化指纹
**位置**:`cdp_connector.py:73-85`
```python
kwargs = {
'os': 'windows', # 只约束了范围(Windows 池)
'humanize': 1.5,
# ← 没有 fingerprint 参数
}
```
**链路**:`launch_options()` → `if fingerprint is None` → `generate_fingerprint(random)` → 每次指纹不同
**影响**:抖音服务端检测到「同一 session_id 对应不同设备指纹」→ 强制短信验证。
#### 缺陷三:Cookie 注入是错误做法
**位置**:`yanghao_runner.py:149-164`
```python
cookie_file = ... / f"{acct_id}_cookies.json"
cookies = json.loads(cookie_file.read_text())
# 注入 Cookie
for c in cookies:
await conn.context.add_cookies([{...}])
```
**问题**:抖音风控的校验逻辑是「session_id + 设备指纹」双重校验。Cookie 注入只传了 session_id,但当前设备的随机指纹与当初登录时的指纹不匹配 → 风控拦截 → 强制短信验证。
---
## 二、正确架构:身份工厂(Identity Factory)
### 2.1 核心理念
把「反检测伪装」和「持久化登录」变成标准化工程组件:
```
┌──────────────────────────────────────────────────────────┐
│ 身份工厂 (Identity Factory) │
├───────────────┬────────────────┬─────────────────────────┤
│ create_identity │ login_identity │ run_task │
│ (BrowserForge │ (手动登录一次 │ (以登录态自动执行) │
│ 生成固化指纹) │ state 保存) │ │
├───────────────┴────────────────┴─────────────────────────┤
│ identities/ │
│ ├── douyin_01/ ← 第一个身份 │
│ │ ├── config.yaml ← 固化指纹 + 代理配置 │
│ │ └── user_data/ ← Firefox 持久化 Profile │
│ ├── douyin_02/ ← 第二个身份 │
│ │ ├── config.yaml │
│ │ └── user_data/ │
│ └── ... │
└──────────────────────────────────────────────────────────┘
```
### 2.2 核心规则:一手机号 = 一身份
**同手机号的抖音和小红书账号必须共用同一个 identity_dir。**
这是经过实践验证的结论:
| ❌ 错误做法 | ✅ 正确做法 |
|:-----------|:-----------|
| douyin_01 → `identities/douyin_01_camo` | douyin_01 → `identities/douyin_01_camo` |
| xhs_01 → `identities/xhs_01`(新建身份) | xhs_01 → `identities/douyin_01_camo`(**复用**) |
| 结果:两个浏览器指纹,两个登录态 | 结果:一个浏览器指纹,一份登录态,抖音和小红书 cookie 共存 |
**为什么必须相同?**
- 一个手机号绑定一个实际的"人",反检测的本质是让平台认为你是"同一个人在不同网站活动"
- 抖音和小红书分别存储 session cookie,互不冲突
- 只需用 `CookieGuard` 保护对方平台的 cookie(删除时别误删)
**代码中如何配置(accounts.override.yaml)**:
```yaml
- id: douyin_04
phone: '18550099083'
enabled: true
- id: xhs_04
phone: '18550099083' # 同手机号
enabled: true
```
`MatrixManager` 的 `list_accounts()` 方法会自动为同手机号的 xhs 账号分配与 douyin 相同的 identity_dir。
**验证方法**:
```bash
cd scripts && python -c "
from mc.engine import resolve_account
a = resolve_account('xhs_04')
print(a.get('identity_hint')) # 输出: douyin_04(与 douyin_04 相同)
"
```
### 2.2 目录结构
```
agent-local/tools/matrix/
├── identities/ # 所有身份集中存放
│ ├── douyin_01/ # 抖音主号
│ │ ├── config.yaml # 指纹 + 代理配置(可版本控制)
│ │ └── user_data/ # 持久化 Profile(不提交 Git)
│ ├── douyin_02/ # 抖音副号
│ │ ├── config.yaml
│ │ └── user_data/
│ └── douyin_camo01/ # 第三个号(Chrome 或 Camoufox)
│ ├── config.yaml
│ └── user_data/
├── scripts/ # 已有脚本,需改造
│ ├── cdp_connector.py # → 新增: launch_camoufox_persistent()
│ ├── yanghao_runner.py # → 新增: --identity 模式
│ ├── camoufox_manager.py # → 改造: 支持 persistent_context
│ └── ... (新建)
│ ├── create_identity.py # 生成新身份(指纹固化)
│ ├── login_identity.py # 首次手动登录
│ └── run_task.py # 以已登录身份执行任务
└── config/
└── accounts.yaml # → 重构: 指向 identities/ 目录
```
### 2.3 配置文件格式 `config.yaml`
```yaml
# identities/douyin_01/config.yaml
identity:
name: "douyin_01"
platform: "douyin"
proxy: null # 可选:固定代理
notes: "抖音主号"
# 由 BrowserForge 生成并固化的指纹
fingerprint:
navigator:
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"
platform: "Win32"
hardwareConcurrency: 8
deviceMemory: 8
language: "zh-CN"
screen:
width: 1920
height: 1080
availWidth: 1920
availHeight: 1040
colorDepth: 24
webgl:
vendor: "Google Inc. (Intel)"
renderer: "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BC4) Direct3D11 vs_5_0 ps_5_0)"
# ... 完整指纹由 BrowserForge 生成
```
### 2.4 关键 API:持久化上下文
```
AsyncCamoufox(persistent_context=True, user_data_dir=path, fingerprint=fixed_fp)
↓
AsyncNewBrowser(..., persistent_context=True)
↓
playwright.firefox.launch_persistent_context(user_data_dir=path, ...)
✅ 所有状态保存到 path 下
✅ 下次启动直接复用
```
对应 Chrome 的方案:
```
Google Chrome --user-data-dir=path --remote-debugging-port=port
↓
CDP connect
✅ 所有状态保存在 path 下
✅ 浏览器不退出,状态持久
```
**两者本质相同**,差异在于:
- Chrome:浏览器进程常驻 + CDP 连接
- Camoufox:`launch_persistent_context` 管理生命周期
---
## 三、标准化流程
### 3.1 创建身份 → `create_identity`
```
BrowserForge.generate()
↓
生成完整设备指纹(固定)
↓
写入 identities/{name}/config.yaml
↓
✅ 身份创建完成
```
### 3.2 首次登录 → `login_identity`
```
启动 Camoufox(headless=False, persistent_context=True, user_data_dir=path, fingerprint=fixed)
↓
打开目标网站(抖音首页)
↓
弹窗等待 → 用户手动登录
↓
用户按 Enter 确认
↓
所有登录态保存到 user_data/
↓
✅ 身份激活完成
```
**这是唯一一次手动操作**。之后所有任务复用这个 state。
### 3.3 日常任务 → `run_task`
```
启动 Camoufox(headless=True, persistent_context=True, user_data_dir=path, fingerprint=fixed)
↓
直接以已登录状态访问抖音
↓
执行蓝图操作(浏览/点赞/收藏等)
↓
保持浏览器不退出,继续下一轮
↓
✅ 循环执行
```
### 3.4 多账号并行
```
for identity in [douyin_01, douyin_02, douyin_camo01]:
创建身份目录
首次登录(手动)
# 日常运行
asyncio.gather(
run_loop("douyin_01"),
run_loop("douyin_02"),
run_loop("douyin_camo01"),
)
```
---
## 四、与现有代码的关系
### 4.1 保留的部分
| 文件 | 用途 | 保留 |
|---|---|---|
| `blueprints/` | 蓝图定义 | ✅ 不变 |
| `douyin_ops.py` | 原子操作 | ✅ 不变 |
| `atom_ops.py` | 原子操作 | ✅ 不变 |
| `auth_manager.py` | 登录状态检测 | ✅ 改进 |
| `task_engine.py` | 蓝图执行引擎 | ✅ 改进加载方式 |
| `anti_detection.py` | 反检测 | ✅ 改进 |
| `accounts.yaml` | 账号配置 | 🛠 重构格式 |
### 4.2 需要改造的文件
| 文件 | 改造内容 | 优先级 |
|---|---|---|
| `cdp_connector.py` | 新增 `_launch_camoufox_persistent()`,支持 `persistent_context` + `user_data_dir` + 固化指纹 | **P0** |
| `yanghao_runner.py` | 新增 `--identity` 模式,通过身份目录启动 | **P0** |
| `camoufox_manager.py` | 改造 `--launch` 使用持久化上下文 | **P1** |
| `accounts.yaml` | 新增 `identity_dir` 字段指向 identities/ | P1 |
| `config_template/` | 更新模板 | P2 |
### 4.3 新增文件
| 文件 | 功能 | 优先级 |
|---|---|---|
| `scripts/create_identity.py` | 用 BrowserForge 生成指纹并创建身份目录 | **P0** |
| `scripts/login_identity.py` | 标准化首次登录流程 | **P0** |
| `scripts/run_task.py` | 以已登录身份执行自动化任务 | P1 |
| `docs/IDENTITY_FACTORY.md` | 本文档 | ✅ 已完成 |
---
## 五、关键注意事项
### 5.1 user_data 不可共享
- 每个身份独立的 `user_data/` 目录
- 禁止混用,否则身份关联
- 禁止提交到 Git(包含登录态)
### 5.2 指纹固化后不可变
- 一旦身份创建完成,`config.yaml` 中的指纹不可修改
- 修改指纹 = 换了一台电脑 → 掉登录
- 如果需要新指纹 → 创建新身份,重新登录
### 5.3 代理一致性
- 一个身份最好绑定固定代理
- 代理变更会触发 IP 地理位置突变 → 风控
- 在 `config.yaml` 的 `identity.proxy` 中指定
### 5.4 浏览器常驻模式
- 登录后浏览器不退出(当前循环模式正确)
- `persistent_context` 确保状态即时刷新
- 避免频繁启停(减少指纹比对次数)
### 5.5 检测结论
根据 [2026 反检测浏览器评测](https://www.browserling.com/blog/2026-antidetect-browsers-review),Camoufox 在以下维度表现:
| 指标 | Camoufox | Chrome stealth | Chrome 原生 |
|---|---|---|---|
| Cloudflare 通过率 | **88.58%** | 11.21% (patchright) | ~50% |
| Canvas 指纹伪装 | ✅ C++ 级 | ⚠️ JS 级 | ❌ |
| WebGL 伪装 | ✅ | ⚠️ | ❌ |
| 字体伪装 | ✅ | ⚠️ | ❌ |
| 多身份隔离 | ✅ 身份级 | ⚠️ Profile 级 | Profile 级 |
Camoufox + 持久化上下文 + BrowserForge 固化指纹 + 固定代理 = 当前最优方案。