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,212 @@
# Douyin 全模拟运营系统架构 v1.0
> 最后更新: 2026-05-15
> 基于 AgentOS 多机协同体系
---
## 设计原则
1. **代码走同步,数据留本机** — 脚本在 agent-sync (Gitee),工作区在 agent-local (不参与同步)
2. **四层隔离** — 管理层/流水线层/操作层/数据层,每层独立演进
3. **每账号独立工作区** — 身份、素材、文案、发布记录各自独立
4. **原子操作可组合** — 所有操作遵循 `前置锚点 → 执行 → 后置锚点` 模式
5. **与现有养号系统共存** — 新架构不破坏 `matrix.py` / `nurture_blueprint.py`
---
## 目录结构
```
~/workbuddy-agent-os/
├── agent-sync/ ← Gitee 同步(代码/技能)
│ └── 05_tools/
│ └── 07_matrix/
│ ├── ARCHITECTURE.md ← 本文档
│ ├── scripts/
│ │ ├── matrix.py ← 统一 CLI
│ │ ├── atom_ops.py ← 原子操作库
│ │ ├── nurture_blueprint.py ← 养号蓝图
│ │ ├── douyin_ops.py ← 抖音专用操作
│ │ ├── matrix_modules/ ← 子模块
│ │ ├── pipelines/ ← [新] 流水线脚本
│ │ │ ├── __init__.py
│ │ │ ├── content_publish.py ← 内容发布流水线(待建)
│ │ │ ├── ecom_pipeline.py ← 电商流水线(待建)
│ │ │ └── live_pipeline.py ← 直播流水线(待建)
│ │ └── ops/ ← 平台操作
│ └── blueprints/
│
├── agent-local/ ← 本机数据(不参与同步)
│ └── tools/
│ └── matrix/
│ ├── config/
│ │ └── accounts.yaml ← 账号注册信息
│ ├── identities/ ← 浏览器指纹+登录态
│ │ ├── douyin_01_camo/
│ │ ├── douyin_02_camo/
│ │ └── douyin_camo01/
│ └── accounts/ ← [新] 每账号工作区
│ ├── douyin_01/
│ │ ├── content/ ← 视频素材
│ │ │ ├── raw/ ← 原始素材
│ │ │ ├── edited/ ← 剪辑成品
│ │ │ └── thumbnails/ ← 封面图
│ │ ├── scripts/ ← 脚本文案
│ │ │ ├── drafts/ ← 草稿
│ │ │ └── posted/ ← 已发布
│ │ ├── publish/ ← 发布记录
│ │ │ ├── queue/ ← 待发布队列
│ │ │ └── published/ ← 已发布记录
│ │ └── backup/
│ │ └── cookies/ ← 登录态备份
│ ├── douyin_02/ ← 同上
│ └── douyin_camo01/ ← 同上
```
---
## 四层架构
### Layer 1: 数据层(本机,不同步)
每个账号独立文件夹,包含四类数据:
| 子目录 | 用途 | 数据来源 |
|:-------|:-----|:---------|
| `content/` | 视频素材(raw/edited/thumbnails) | 外部导入 / 剪辑 |
| `scripts/` | 脚本文案(drafts/posted) | AI 生成 / 手动编写 |
| `publish/` | 发布队列 + 历史记录 | 系统自动写入 |
| `backup/` | 登录态备份 | 系统自动备份 |
### Layer 2: 操作层(代码,Gitee 同步)
原子操作(`atom_ops.py` 和 `douyin_ops.py`):
| 操作 | 状态 | 说明 |
|:-----|:----:|:-----|
| browse | ✅ | 浏览视频流 |
| like / collect | ✅ | 点赞/收藏 |
| comment path a | ✅ | 弹窗覆盖层评论(KeyX+Enter) |
| comment path b | ✅ | 全屏视频页评论(scroll+click+Enter) |
| search | ✅ | 搜索并打开 |
| follow | ✅ | 关注 |
| **publish_video** | ⏳ 待建 | 上传视频+文案+封面 |
| **live_sim** | ⏳ 待建 | 直播推流模拟 |
| **product_select** | ⏳ 待建 | 选品/带货 |
| **dm_send** | ⏳ 待建 | 私信 |
### Layer 3: 流水线层(代码,Gitee 同步)
| 流水线 | 状态 | 操作链 |
|:-------|:----:|:-------|
| 养号流水线 (nurture) | ✅ | browse → interact → comment → loop |
| 内容发布流水线 | ⏳ 待建 | 读取队列 → 登录 → 上传 → 发布 → 记录 |
| 电商流水线 | ⏳ 待建 | 选品 → 添加到橱窗 → 推广 |
| 直播流水线 | ⏳ 待建 | OBS推流 → 直播间互动 → 带货 |
### Layer 4: 管理层(规划中)
- 内容排期日历
- 多账号任务调度
- 发布成功率 KPI
---
## 两条评论路径对比
| 维度 | Path A: 弹窗覆盖层 | Path B: 全屏视频页 |
|:-----|:------------------|:------------------|
| 入口 | 精选页→点卡片→弹窗播放器 | 直接打开 `/video/xxx` URL |
| 评论区状态 | 隐藏 (hasCL=false) → KeyX 打开 | 页面上方直接可见 |
| 激活方式 | 坐标 (479,687) 双击 | scrollIntoView + click 输入容器 |
| 输入方式 | pbcopy + Meta+V (共享) | pbcopy + Meta+V (共享) |
| 发送方式 | Enter | Enter |
| 适用场景 | 养号流程中的日常操作 | 收到指定链接后自动评论 |
Route 规则:
```
if URL 包含 /video/ → Path B
else → 检查 hasCL
if hasCL=true → 直接聚焦
if hasCL=false → KeyX 打开 → Path A
```
---
## 并行执行架构(v4.3 新增)
`mc run` 使用 `mc/engine.py` 的 `BatchEngine` 执行批量任务。v4.3 改为并行模式:
### 设计要点
1. **单账号全轮复用浏览器** — 每个账号创建一个 Camoufox 连接,跑完全部轮次再关闭
2. **多账号 `asyncio.gather`** — 所有账号的 `_run_account_all_rounds()` 同时启动
3. **浏览器启停优化** — N 个账号 × R 轮 → 启停 N 次(而非 N×R 次)
### 新旧对比
```
旧版 (v1.1):
轮1: A→B→C (串行) → 间隔 → 轮2: A→B→C → 间隔 → 轮3: A→B→C
# 每步开闭一次浏览器,后一个等前一个完成
# 3账号×3轮 ≈ 500-900s/批次
新版 (v1.2):
A的全部3轮 ──┐
B的全部3轮 ──┼── asyncio.gather ── 同时完成
C的全部3轮 ──┘
# 每账号一个持久浏览器,互不等待
# 3账号×3轮 ≈ 150-350s/批次(约节省 60%)
```
### 代码入口
| 方法 | 职责 |
|:-----|:-----|
| `BatchEngine.run()` | prepare 所有账号 → gather 子任务 → 汇总 |
| `_run_account_all_rounds()` | 每账号:resolve → cookie 检查 → 启动浏览器 → 循环轮次 → 关闭 |
| `run_single(conn=...)` | 单轮单账号:导航到首页 → 执行蓝图步骤 → 返回报告 |
| `_pick_blueprint(round_idx)` | 混合随机/顺序选择本轮蓝图 |
### 共享连接模式
`run_single()` 新增 `conn` 参数:
- `conn=None` → 自行创建并关闭浏览器(独立使用场景)
- `conn=CDPConnector` → 复用已有连接(`_run_account_all_rounds` 内部调用)
### 注意事项
- 并行启动多个 Camoufox 实例时,不要同时调用 `connect()` 中的 `pkill -f camoufox`
(目前 connect() 中始终执行,但因同步调用在 async 之前完成,3 个并行的 connect() 的
pkill 都在任何 Camoufox 启动前执行完毕,因此不会跨进程误杀)
- `BrowserManager.prepare()` 在 `run()` 中统一调用,不在子任务中重复执行
2026-05-15 修改:窗口位置固定从 `accounts.yaml` 读取,不再保存回写。
Camoufox 启动偏移会导致保存的坐标偏离预期(如 0→652, 400→2169)。
| 账号 | 固定位置 |
|:-----|:--------:|
| douyin_01 | (0, 0) |
| douyin_02 | (400, 0) |
| douyin_camo01 | (750, 0) |
---
## 评论故障修复记录(2026-05-15)
### 问题
养号流程中评论发送失败,日志显示 `页面出现评论: ❌`
### 根因
1. **输入方式错误**: `keyboard.type()` 被 Draft.js 忽略,execCommand fallback 不触发 React 状态
2. **发送键错误**: Alt+Enter 不再被新版抖音支持
### 修复
| 文件 | 行 | 改前 | 改后 |
|:-----|:---|:-----|:-----|
| `runner.py` | Step 2 输入 | keyboard.type → execCommand | **pbcopy + Meta+V** |
| `runner.py` | Step 3 发送 | Ctrl+Enter → Alt+Enter | **Enter** |
| `runner.py` | 坐标保存 | 每次结束回写 accounts.yaml | **删除,固定读配置** |
| `nurture_blueprint.py` | op_comment 发送 | Alt+Enter (osascript) | **keyboard.press Enter** |