移植自 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
🦀 身份工厂:反检测 + 持久化标准化方案
基于 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()
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
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
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):
- id: douyin_04
phone: '18550099083'
enabled: true
- id: xhs_04
phone: '18550099083' # 同手机号
enabled: true
MatrixManager 的 list_accounts() 方法会自动为同手机号的 xhs 账号分配与 douyin 相同的 identity_dir。
验证方法:
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
# 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 反检测浏览器评测,Camoufox 在以下维度表现:
| 指标 | Camoufox | Chrome stealth | Chrome 原生 |
|---|---|---|---|
| Cloudflare 通过率 | 88.58% | 11.21% (patchright) | ~50% |
| Canvas 指纹伪装 | ✅ C++ 级 | ⚠️ JS 级 | ❌ |
| WebGL 伪装 | ✅ | ⚠️ | ❌ |
| 字体伪装 | ✅ | ⚠️ | ❌ |
| 多身份隔离 | ✅ 身份级 | ⚠️ Profile 级 | Profile 级 |
Camoufox + 持久化上下文 + BrowserForge 固化指纹 + 固定代理 = 当前最优方案。