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,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差异
---
以上是完整的审计和方案。你要我按这个方向执行吗?
@@ -0,0 +1,320 @@
# 原子操作体系开发经验文档 v1.0
> 创建时间: 2026-06-21 10:05
> 时间跨度: 2026-06-20 09:00 ~ 2026-06-21 10:00
> 涉及平台: 抖音(douyin)、小红书(xiaohongshu)
> 文档性质: 经验沉淀 + 框架设计 + 代码对照
---
## 第一章:时间线回顾
### 2026-06-20(Day 1)
| 时间 | 事件 | 关键结论 |
|:-----|:-----|:---------|
| 09:00-12:00 | 登录状态机重构 + Vite前端修复 | 三组件架构(PlatformDetector/RecoveryChain/LoginManager)定型 |
| 14:00-16:20 | 抖音登录 3 种场景调试 | 全通。关键修复: 检测器误判(移除data-e2e anchor)、短信验证码取旧码、登录按钮是"确认登录"不是"登录" |
| 16:30-18:10 | 小红书登录修复 | 全通。关键修复: SmsRecovery调xhs_login、XhsDetector cookie误判、is_logged_in()太松、步骤顺序错误(同意并登录在点登录后)、div.foot-btn选择器、oauth-tip切换 |
| 18:10-20:40 | 养号蓝图v2.0 + 抖音日常45步测试 | 45步39成功。关键修复: open_video双击+验证+焦点、like从DOM改为键盘z、open_video加回首页兜底 |
| 20:40-23:10 | 抖音录制2轮 + 小红书原子操作测试 | 小红书post_comment首次成功(JS找输入框方案)。关键修复: xhs_ops补wait_watch/go_back、冷却前缀匹配 |
### 2026-06-21(Day 2)
| 时间 | 事件 | 关键结论 |
|:-----|:-----|:---------|
| 08:00-09:00 | 双平台修复 | follow键盘f→g、post_comment加send按钮查找+验证、open_video多选择器+状态检测 |
| 09:00-09:27 | 架构反思 | ghai指出新代码全不能用——缺乏前置状态检测和后置验证。Claw从头学习旧代码_retry_enter_video、_check_anchor、CommentStateMachine |
| 09:27-09:34 | 方案重构 | 提出三层状态模型(L1页面/L2区域/L3元素)、增强录制、单步调试、看板流程图 |
| 09:34-09:42 | PageInspector实现 | 创建page_inspector.py,L1/L2/L3三层检测 |
| 09:42-09:46 | 增强录制讨论 | ghai提出: 只按一次·标记状态变化,系统自动推断操作。Claw优化: 系统根据状态差自动识别操作类型 |
| 09:46-09:54 | ghai录制18步 | 首次用新思路录制。发现新问题: 收藏键是C不是无键、弹幕B键、分享V键 |
| 09:54-10:05 | 视觉状态层讨论 | ghai发现: 按钮变色(灰→红→黄)是重要的状态变化,当前录制没有捕获。提出L4视觉状态层 |
---
## 第二章:核心概念与设计原则
### 2.1 原子操作的本质
一个原子操作 = **四个要素**:
```
[前置状态] → [操作执行] → [后置状态] → [验证结果]
```
- **前置状态**必须包含: 页面类型(L1)、鼠标区域(L2)、元素详情(L3)、视觉状态(L4)
- **操作执行**是一个确定的动作: 点击某个元素、按某个键盘键
- **后置状态**与前置状态有明确差异(URL变化、DOM变化、视觉变化)
- **验证结果**是真实可观测的,不是代码返回的布尔值
### 2.2 错误模式总结
以下所有错误都有一个共同根因: **没有做状态检测**
| 错误表现 | 具体操作 | 原因 |
|:---------|:---------|:-----|
| open_video 在首页找不到卡片 | 直接找.discover-video-card-item | 不先检测是否在播放页 |
| like 返回"成功"但页面没反应 | 键盘z在feed流中无效 | 不检测是否在视频详情页 |
| collect 全部失败0.0s | 用[data-e2e]选择器找按钮 | 不检测按钮的视觉/属性状态 |
| post_comment 返回sent但评论没发 | pbcopy+Meta+V+Enter | 不验证comment-list是否出现文字 |
| follow 无反应 | KEYS['follow']='f' | 没有用录制数据验证(实际是g键) |
### 2.3 各种选择器优劣
| 方式 | 可靠性 | 适用场景 |
|:-----|:------|:---------|
| 键盘快捷键(z/c/g/x) | ⭐⭐⭐⭐⭐ | 抖音所有互动操作 |
| data-e2e 锚点 | ⭐⭐⭐ | 旧版抖音 (可能被新版移除) |
| CSS类名 | ⭐⭐ | 不稳定(CSS modules会加hash) |
| 坐标(录制位置) | ⭐⭐⭐⭐ | 固定布局中稳定 |
| 文字匹配 | ⭐⭐⭐ | 关注/发送等有固定文字 |
| 视觉状态(颜色) | ⭐⭐⭐⭐ | 🆕 L4层,按钮状态判断 |
### 2.4 抖音网页版快捷键完整表
```
互动类:
Z 赞/取消赞
C 收藏/取消收藏
G 关注/取消关注
F 进入作者主页
X 打开/关闭评论
V 复制分享口令
P 推荐给朋友
R 不感兴趣
播放类:
B 开始/关闭弹幕
J 清屏
K 自动连播
Y 网页内全屏
H 全屏
L 稍后再看
Space 暂停/播放
↑↓ 上下翻页
←→ 快进快退(长按2倍速)
Shift+/Shift- 音量调大/调小
```
---
## 第三章:统一状态模型(引擎层核心)
### 3.1 四层状态检测
```
L1: 页面类型
├── 判断依据: URL pattern + DOM特征(video数量、card数量、搜索栏、导航栏)
├── 抖音类型: grid(首页)、player_modal(弹窗)、player_full(独立页)、search(搜索)、profile(主页)
└── 小红书类型: grid(explore)、note_detail(笔记详情)、search(搜索结果)、profile(主页)
L2: 鼠标区域
├── 判断依据: 鼠标坐标 / 窗口尺寸 → 百分比区域
├── 区域类型: video_area(中央上)、bottom_bar(底部按钮)、comment_area(评论区)、feed_area(内容区)、nav_area(左侧导航)、sidebar(右侧推荐)
└── 用途: 确定操作上下文(在按钮区点击=互动操作,在feed区点击=选择内容)
L3: 元素详情
├── 检测: 鼠标正指向的元素(elementFromPoint) + 周围200px内的所有可交互元素
├── 属性: tag、class、text、位置、尺寸、visibility
└── 用途: 确定具体操作目标(这个SPAN是like-lottie点赞按钮)
L4: 视觉状态 🆕
├── 检测: computedStyle(color/fill/stroke)、data-state/aria-pressed属性、textContent变化
├── 典型变化:
│ ├── 点赞: 灰色(#999) → 红色(#ff2d55), data-e2e-state从空变digged
│ ├── 收藏: 灰色 → 黄色, 文字从"收藏"变"已收藏"
│ ├── 关注: 文字从"关注"变"已关注", 按钮颜色变红
│ └── 弹幕: aria-pressed属性变化
└── 用途: 验证操作是否真正生效(不是代码返回的假成功)
```
### 3.2 状态转换规则
```
抖音:
grid ──(双击卡片)──> player_modal
grid ──(搜索)──────> search
grid ──(点头像)────> profile
player_* ──(z键)───> player_*(点赞已变)
player_* ──(c键)───> player_*(收藏已变)
player_* ──(g键)───> player_*(关注已变)
player_* ──(go_back)──> grid
search ──(点结果)──> player_*
search ──(go_back)──> grid
profile ──(go_back)──> grid
小红书:
grid ──(点笔记)──> note_detail
note_detail ──(点❤️)──> note_detail(已赞)
note_detail ──(点⭐)──> note_detail(已收藏)
note_detail ──(go_back)──> grid
grid ──(搜索)──> search
search ──(点结果)──> note_detail
```
---
## 第四章:录制系统设计
### 4.1 录制原理
```
用户操作流程:
1. 按 · → 系统捕获状态快照(所有四层数据)
2. 做操作 → 用户自由操作(点击/键盘/停留/滚动)
3. 按 · → 系统捕获状态快照 + 自动对比差异
...重复...
4. 按 Esc → 结束录制
系统自动推断:
- 对比相邻两个状态的差异
- URL变化 → 可能进入新页面
- video数量变化 → 进入/离开播放页
- 按钮颜色变化 → 点赞/收藏/关注操作
- 评论输入框出现/消失 → 打开/关闭评论
```
### 4.2 录制数据格式
```json
{
"step": 2,
"label": "open_video", // 人工标注或系统自动建议
"before": {
"url": "...jingxuan", // L1
"page_type": "grid", // L1
"video_count": 0, // L1
"card_count": 20, // L1
"mouse_x": 262, "mouse_y": 209, // L2
"mouse_region": "video_area", // L2
"element_at_point": { // L3
"tag": "VIDEO", "class": "", ...
},
"visual_states": { // L4
"like_active": false, // 赞为空
"collect_active": false // 收为空
}
},
"action": {
"type": "click",
"x": 262, "y": 209,
"target": "VIDEO",
"click_count": 2 // 双击
},
"after": {
"url": "...modal_id=xxx",
"page_type": "player_modal",
"video_count": 1,
"diff": "从首页进入播放页"
}
}
```
### 4.3 标注系统(人工+自动)
录制后进入标注界面:
- 自动建议: 系统根据状态diff推断操作名称(URL变化→open_video, 按钮变色→like)
- 人工修正: 你可以修改名称、补充备注
- 合并步骤: 连续2-N步可以合并为1个复合原子操作(如: x+打字+发送 = post_comment)
---
## 第五章:当前代码状态
### 5.1 已修复的问题
| 文件 | 问题 | 修复 |
|:-----|:-----|:-----|
| douyin_ops.py | KEYS['follow']='f' → 应该是'g' | ✅ 已修复 |
| douyin_ops.py | collect没有键盘fallback | ⚠️ 加了C键但未测试 |
| douyin_ops.py | open_video选择器单一 | ✅ 加了多选择器+状态检测 |
| douyin_ops.py | post_comment无验证逻辑 | ✅ 加了send按钮搜索+comment-list文本验证 |
| xhs_ops.py | go_back未定义 | ✅ 已添加 |
| xhs_ops.py | wait_watch未定义 | ✅ 已添加 |
| xhs_ops.py | post_comment完全失败 | ✅ JS找输入框方案成功 |
| mc/engine.py | 冷却前缀不匹配(xhs_like不匹配like) | ✅ 已去前缀 |
### 5.2 仍需修复的问题
| 文件 | 问题 | 优先级 |
|:-----|:-----|:------|
| douyin_ops.py | collect 用 C 键(不是DOM选择器) | P0 |
| douyin_ops.py | 所有操作加 L4 视觉验证 | P0 |
| douyin_ops.py | 新增 danmaku(B键), share(V键) | P1 |
| xhs_ops.py | xhs_click_note go_back后feed未刷新 | P1 |
| xhs_ops.py | 小红书没有C/G快捷键,需要DOM点 | P1 |
### 5.3 关键文件清单
| 文件 | 作用 | 状态 |
|:-----|:-----|:----:|
| `scripts/ARCHITECTURE_AUDIT.md` | 新旧代码对比审计 | ✅ 完成 |
| `scripts/page_inspector.py` | 四层状态检测器(新) | ✅ 已创建 |
| `scripts/douyin_ops.py` | 抖音原子操作 | ⚠️ 部分修复 |
| `scripts/ops/xhs_ops.py` | 小红书原子操作 | ⚠️ 部分修复 |
| `scripts/mc/engine.py` | 执行引擎 | ✅ 冷却修复 |
| `scripts/mc/recorder.py` | 录制工具 | 🔵 待增强(L4+自动推断) |
| `blueprints/dy_step_test.json` | 抖音11步测试蓝图 | ✅ |
| `blueprints/xhs_test_all.json` | 小红书17步测试蓝图 | ✅ |
| `blueprints/dy_test_all.json` | 抖音16步全原子测试蓝图 | ✅ |
| `scripts/matrix_modules/nurture/runner.py` | 旧版稳定代码(参考用) | ✅ |
---
## 第六章:开发方法论
### 6.1 禁止事项
1. **不要修改旧代码** — 旧代码是宝贵的参考,即使不完美也证明了可行路径
2. **不要用CSS类名作为主要选择器** — CSS modules会使类名不可靠
3. **不要假设操作成功了** — 必须用L4视觉状态验证
4. **不要把抖音的操作方法套用给小红书** — 两个平台的DOM结构完全不同
5. **不要在没有录制数据支持的情况下添加新操作** — 优先让ghai录一次,从数据出发
### 6.2 高效开发流程
```
1. ghai录制真实操作(5-10分钟)
2. Claw分析录制数据,提取状态diff(2分钟)
3. 一起标注原子操作名称(2分钟)
4. Claw生成代码模板(1分钟)
5. ghai审核确认(1分钟)
6. 单步测试(3秒/步)
7. 全流程验证(1分钟)
总计: ~15分钟完成一个完整原子操作
```
反面对比:
```
旧流程: Claw手写代码 → 猜测选择器 → 跑全蓝图 → 等14分钟 → 失败 → 再猜 → ...
完全不可行
```
### 6.3 测试原则
1. **从首页开始**: goto_home是最基础的原子操作,先确认
2. **逐条路径测试**: 不要混在一起测
3. **三秒原则**: 单步测试结果3秒内必须看到,不等完整蓝图
4. **绿了就固化**: 一个操作测试通过立即标记绿色,不回头动
---
## 第七章:附录 - 按键映射
### 抖音 Keys 正确值
```python
KEYS = {
"like": "z", # ✅ 正确
"collect": "c", # ❌ 之前代码里没有定义
"comment": "x", # ✅ 正确
"follow": "g", # ✅ 修复后正确 (之前错为f)
"danmaku": "b", # 🆕 新增
"share": "v", # 🆕 新增(复制分享口令)
"clear_screen": "j", # 🆕 新增
"prev": "ArrowUp",
"next": "ArrowDown",
"enter": "Enter",
}
```
@@ -0,0 +1,343 @@
# AgentOS 原子操作体系 v2 完整规划
> 状态: 规划中 | 创建: 2026-06-21 09:30
> 目标: 建立系统性、可持续迭代的原子操作开发与测试体系
> 角色: ghai 是项目负责人/决策者,Claw 是技术搭档/建议者
---
## 一、核心理念
### 1.1 我们不是在写代码,是在建立一套"自动化操作知识库"
每个平台的每个操作,都应该像一本字典里的词条:
- 有明确的"前提条件"(在什么页面/状态下才能做)
- 有确定的"执行方式"(用什么方法做)
- 有可验证的"结果"(做了之后页面变成什么样子)
### 1.2 不做"按键精灵",做"智能操作代理"
按键精灵: 盲按 → 不管结果 → 继续按下一个
智能代理: 检测当前状态 → 确认可以操作 → 执行 → 验证结果 → 记录
### 1.3 开发效率 > 代码量
一次录制 = 10个原子操作的新代码,比你手写10个函数快100倍。
但前提是:录制系统能捕获足够的信息(状态+操作+结果),并且你我能高效标注它。
---
## 二、总架构:三层 + 两工具 + 一视图
```
┌─────────────────────────────────────────────────────────┐
│ 看板 (Dashboard) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 操作流程图 │ │ 录制标注界面 │ │ 测试结果面板 │ │
│ │ (你的操作地图)│ │ (你的编辑器) │ │ (你的眼睛) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 工具层 │
│ ┌──────────────────────┐ ┌───────────────────────┐ │
│ │ 增强录制系统 │ │ 单步调试模式 │ │
│ │ (捕获三层状态+操作) │ │ (mc debug --step) │ │
│ │ (支持人工标注命名) │ │ (3秒出结果不等待) │ │
│ └──────────────────────┘ └───────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 引擎层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PageState (统一状态模型) │ │
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │L1: │→│L2: │→│L3: │→│Action│→│验证 │ │ │
│ │ │页面│ │区域│ │元素│ │执行 │ │结果 │ │ │
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ 原子操作库 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 抖音 ops │ │ 小红书ops │ │ 未来平台 │ │
│ │ douyin/ │ │ xhs/ │ │ ... │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 三、PageState 状态模型(引擎层核心)
### 3.1 抖音页面状态定义
| 状态名 | 判断条件 | 可用操作 |
|:------|:---------|:---------|
| `grid` | URL含jingxuan,card_count>0 | scroll_feed, open_video, search, goto_profile |
| `player_modal` | URL含modal_id,video>0 | like, collect, follow, comment, next_video, go_back |
| `player_full` | URL含/video/xxx | like, collect, follow, comment, go_back |
| `search` | URL含/search/ | click_result, scroll, go_back |
| `profile` | URL含/user/ | read_field, go_back |
### 3.2 小红书页面状态定义
| 状态名 | 判断条件 | 可用操作 |
|:------|:---------|:---------|
| `grid` | URL=/explore, note-item>0 | scroll_feed, click_note, search, goto_profile |
| `note_detail` | URL=/explore/xxx | like, collect, comment, post_comment, follow, go_back |
| `search` | URL=/search_result | click_result, go_back |
| `profile` | URL=/user/profile/xxx | read_field, go_back |
### 3.3 状态转换规则
```
grid ──(open_video/click_note)──> player/note_detail
grid ──(search)──────────────────> search
grid ──(goto_profile)────────────> profile
player ──(go_back)───────────────> grid
player ──(next_video)────────────> player (不同视频)
search ──(click_result)──────────> player/note_detail
search ──(go_back)───────────────> grid
profile ──(go_back)──────────────> grid
```
---
## 四、增强录制系统(工具层)
### 4.1 录制数据的完整结构
```json
{
"session": {
"platform": "douyin",
"account": "douyin_test",
"start_time": "2026-06-21T10:00:00",
"window_size": [702, 783]
},
"steps": [
{
"id": 1,
"label": "", // 你来命名(如"点开第一个视频")
"before": {
"url": "...",
"page_type": "grid",
"dom_snapshot": {
"video_count": 0,
"card_count": 20,
"has_search_bar": true,
"has_nav": true,
"author_id": null,
"is_self": false
},
"mouse_position": [434, 190],
"mouse_region": "feed_area",
"nearby_elements": [
{"tag": "A", "class": "cover mask ld", "text": "", "x": 375, "y": 292}
]
},
"action": {
"type": "click",
"x": 375,
"y": 292,
"target_tag": "A",
"target_class": "cover mask ld",
"target_text": "",
"details": "笔记封面点击,可能打开了笔记详情页"
},
"after": {
"url": "https://www.douyin.com/jingxuan?modal_id=xxx",
"page_type": "player_modal",
"dom_snapshot": {
"video_count": 1,
"card_count": 0,
"has_comment_list": true
},
"url_changed": true,
"dom_diff": "video元素从0变为1,进入了播放页"
}
}
]
}
```
### 4.2 录音交互流程
```
1. 你启动录制: mc record --account=douyin_test --platform=douyin
→ 浏览器打开,开始录制
2. 你做操作,按 `·` 标记每一步
→ 系统自动捕获 before/action/after
3. 录制结束按 Esc
→ 系统保存完整录制数据
4. 进入标注界面(Dashboard新页面)
→ 展示每一步的 before/action/after
→ 自动检测操作类型(点击/按键/滚动)
→ 自动建议原子操作名称(like/collect/comment)
→ 你可以修改名称和补充说明
→ 连续2步可以合并为1个原子操作(如:开评论(x) + 输入 + 发送 = post_comment)
5. 标注完成 → 系统自动生成原子操作代码模板
→ 你审核通过 → 代码入库
```
### 4.3 录制界面的设计要点
```
┌──────────────────────────────────────────────┐
│ 录制回放: douyin_test - 2026-06-21 10:00 │
├──────────────────────────────────────────────┤
│ │
│ Step 1: grid → player_modal │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ BEFORE │→│ ACTION │→│ AFTER │ │
│ │ 首页 │ │ 双击卡片 │ │ 播放页 │ │
│ │ card=20│ │ A.cover │ │ video=1 │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ 操作名: [open_video___] ← 你填或系统建议 │
│ 备注: [双击feed卡片进入首个视频___] │
│ │
│ Step 2: 播放页 → 播放页(已点赞) │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ BEFORE │→│ ACTION │→│ AFTER │ │
│ │ 播放页 │ │ 按z键 │ │ 心变红 │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ 操作名: [like_________] ← 系统建议: like │
│ 备注: [键盘z点赞_______________] │
│ │
│ [合并步骤1+2?] [导出为蓝图] [生成代码] │
└──────────────────────────────────────────────┘
```
---
## 五、单步调试模式(工具层)
### 5.1 命令设计
```bash
# 模式1: 启动交互式调试(浏览器保持打开)
mc debug --accounts=douyin_test --platform=douyin
→ 浏览器打开,保持运行
→ 等待命令
# 模式2: 执行单步操作
mc step open_video # 只执行这一个操作,返回结果+消耗时间
mc step like # 继续下一步
mc step collect # 3秒内出结果
# 模式3: 批量单步(自动但不跳过)
mc step open_video like collect follow open_comments post_comment
→ 每步执行后停顿1秒,显示结果,继续下一步
```
### 5.2 交互流程
```
> mc debug --accounts=douyin_test
✅ 浏览器已启动,当前状态: grid (首页)
等待命令...
> open_video
✅ open_video → video_detail (5.2s) 状态: grid → player_modal
验证: video=1 ✅ 可继续下一步
> like
✅ like → 👍 (1.1s) 状态: player_modal
验证: 通过✅
> collect
❌ collect → - (0.0s) 状态: player_modal
元素未找到: [data-e2e="video-player-collect"]
回退: JS搜索也失败,坐标兜底也失败
建议: 你在浏览器里点一下收藏,我看看实际DOM是什么
```
---
## 六、看板集成(展示层)
### 6.1 新增三个页面
| 页面 | 路径 | 功能 |
|:-----|:-----|:------|
| 操作流程图 | `/ops-flow` | SVG流程图,实时显示测试状态 |
| 录制标注 | `/recorder` | 回放录制,标注原子操作,合并步骤 |
| 测试控制台 | `/debug` | 单步测试,查看日志,状态监控 |
### 6.2 操作流程图功能
- 展示所有平台的操作状态机
- 绿色 = 已测试通过
- 橙黄色 = 部分通过/有问题
- 灰色 = 未测试
- 点击任意节点 → 显示该操作的详细信息
- 点击连线 → 显示状态转换条件
- 底部显示当前正在测试的"路径"和进度
---
## 七、实施计划(分阶段)
### Phase 1: 基础设施(1-2天)
| 任务 | 说明 |
|:-----|:------|
| 1.1 实现 PageInspector | L1: 页面类型, L2: 鼠标区域, L3: 元素详情 |
| 1.2 增强 Recorder | 录制时捕获完整三层状态 |
| 1.3 实现 mc debug | 浏览器保持打开,接受后续命令 |
### Phase 2: 标注系统 + 流程图(1-2天)
| 任务 | 说明 |
|:-----|:------|
| 2.1 录制标注页面 | 看板新页面,回放+标注+合并步骤 |
| 2.2 操作流程图页面 | SVG流程图 + 实时状态 |
| 2.3 测试控制台页面 | 单步调试 + 日志查看 |
### Phase 3: 存量修复(1天)
| 任务 | 说明 |
|:-----|:------|
| 3.1 录一次抖音全流程 | 你录制 → 我分析 |
| 3.2 修复 collect + open_comments | 基于录制数据修复 |
| 3.3 录一次小红书全流程 | 你录制 → 我分析 |
| 3.4 修复小红书全部操作 | 基于录制数据修复 |
### Phase 4: 扩展(持续)
| 任务 | 说明 |
|:-----|:------|
| 4.1 新增平台 | 每个平台遵循同样流程:录制 → 标注 → 测试 |
| 4.2 蓝图组合 | 基于稳定的原子操作,组合成养号/评论等蓝图 |
| 4.3 定时任务 | 每日自动化执行 |
---
## 八、当前迭代聚焦
### 本周目标(2026-06-21 ~ 06-27)
```
Week 1: 抖音全流程打通 → 小红书全流程打通 → 看板可查看流程图
```
### 今天目标(2026-06-21)
1. **你先录一次抖音完整流程**(约10步),我在录制过程中捕获完整状态
2. **一起标注这些步骤**,给每个操作命名
3. **基于录制数据修复问题**(collect + open_comments)
4. **完成抖音流程图**,所有路径从灰色变绿色
---
## 九、你需要的(而不是我想要的)
这个计划的核心目标只有一个:**让你能高效地验证想法,而不是把时间花在猜代码上**。
每一步都是:
1. 你操作 → 我记录
2. 你标注 → 我理解
3. 你测试 → 我修复
4. 你确认 → 下一个
你不是在写代码。你是在教会我你的平台操作方式。
我是你的"手"和"眼",你是"大脑"和"决策者"。
@@ -0,0 +1,284 @@
"""
PageInspector — 三层页面状态检测
L1: 页面类型 (grid/player/search/profile) — URL pattern + DOM特征
L2: 鼠标所在区域 (feed/video/comment/bottom_bar) — 坐标区间判断
L3: 鼠标指向元素详情 — tag/class/text/position
基于 ghai 的人类视角判断逻辑:
1. 先看URL + 页面大结构 → 确定在什么页面
2. 再看鼠标周围区域 → 确定操作上下文
3. 最后看鼠标指向的具体元素 → 确定要操作什么
Usage:
inspector = PageInspector(page, platform="douyin")
state = await inspector.inspect()
print(state.page_type) # "player_modal"
print(state.has_video) # True
print(state.nearby_elements) # [element at cursor...]
"""
import asyncio
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class PageState:
"""完整页面状态快照"""
platform: str = ""
url: str = ""
title: str = ""
# L1: 页面类型
page_type: str = "unknown" # grid/player_modal/player_full/search/profile/unknown
page_type_confidence: float = 0.0
# L1: DOM特征
video_count: int = 0
card_count: int = 0
has_search_bar: bool = False
has_nav: bool = False
has_comment_list: bool = False
is_self_page: bool = False # 是否自己的主页
author_id: str = ""
# L2: 鼠标区域
mouse_x: int = 0
mouse_y: int = 0
mouse_region: str = "unknown" # feed_area/video_area/bottom_bar/comment_area/nav_area
# L3: 附近元素
nearby_elements: list = field(default_factory=list) # [{tag, class, text, x, y, ...}]
element_at_point: Optional[dict] = None
# 验证
errors: list = field(default_factory=list)
warnings: list = field(default_factory=list)
def to_dict(self) -> dict:
return {k: v for k, v in self.__dict__.items()}
class PageInspector:
"""三层页面状态检测器"""
def __init__(self, page, platform: str = "douyin"):
self.page = page
self.platform = platform
async def inspect(self, mouse_x: int = 0, mouse_y: int = 0) -> PageState:
"""执行完整三层检测"""
state = PageState(platform=self.platform)
try:
# L1: 页面基本信息
state.url = self.page.url
state.title = await self.page.title()
# L1: DOM特征
dom = await self._capture_dom()
state.video_count = dom["video_count"]
state.card_count = dom["card_count"]
state.has_search_bar = dom["has_search_bar"]
state.has_nav = dom["has_nav"]
state.has_comment_list = dom["has_comment_list"]
state.author_id = dom["author_id"]
state.is_self_page = dom["is_self"]
# L1: 页面类型判定
state.page_type = self._classify_page_type(state)
# L2: 鼠标区域
if mouse_x > 0 and mouse_y > 0:
state.mouse_x = mouse_x
state.mouse_y = mouse_y
size = await self.page.evaluate(
"() => ({ w: window.innerWidth, h: window.innerHeight })"
)
state.mouse_region = self._classify_region(
mouse_x, mouse_y, size["w"], size["h"]
)
# L3: 鼠标处元素
if mouse_x > 0 and mouse_y > 0:
el_info = await self.page.evaluate(
f"""() => {{
const el = document.elementFromPoint({mouse_x}, {mouse_y});
if (!el) return null;
const rect = el.getBoundingClientRect();
return {{
tag: el.tagName,
class: el.className || '',
text: (el.textContent || '').trim().slice(0, 50),
x: Math.round(rect.left), y: Math.round(rect.top),
w: Math.round(rect.width), h: Math.round(rect.height),
visible: el.offsetParent !== null
}};
}}"""
)
state.element_at_point = el_info
# L3: 附近区域元素(鼠标周围200px内的可交互元素)
if mouse_x > 0 and mouse_y > 0:
nearby = await self.page.evaluate(
f"""() => {{
const mx = {mouse_x}, my = {mouse_y};
const els = document.querySelectorAll(
'a, button, span, div, input, textarea, svg')
const results = [];
for (const el of els) {{
if (!el.offsetParent) continue;
const r = el.getBoundingClientRect();
const cx = r.left + r.width/2;
const cy = r.top + r.height/2;
const dist = Math.sqrt((cx-mx)**2 + (cy-my)**2);
if (dist < 200) {{
results.push({{
tag: el.tagName,
class: (el.className || '').slice(0, 40),
text: (el.textContent || '').trim().slice(0, 30),
cx: Math.round(cx), cy: Math.round(cy), dist: Math.round(dist)
}});
}}
}}
return results.sort((a,b) => a.dist - b.dist).slice(0, 10);
}}"""
)
state.nearby_elements = nearby
except Exception as e:
state.errors.append(str(e))
return state
async def _capture_dom(self) -> dict:
"""捕获DOM特征"""
try:
return await self.page.evaluate("""() => {
const u = location.href;
const vc = document.querySelectorAll('video').length;
// cards: 视频卡片/笔记卡片
const cards = document.querySelectorAll(
'.discover-video-card-item, [class*="video-card"], ' +
'section.note-item, a[href*="/explore/"], [class*="note-item"]'
).length;
// 搜索栏
const searchBar = document.querySelector(
'[data-e2e="searchbar-input"], input[placeholder*="搜索"], textarea.textarea'
);
// 导航
const nav = document.querySelector(
'[data-e2e="douyin-navigation"], [class*="nav"], .ulxic5B4'
);
// 评论列表
const commentList = document.querySelector(
'[data-e2e="comment-list"], [class*="comment-list"]'
);
// 作者ID (从URL或个人信息区获取)
let authorId = '';
const userMatch = u.match(/\/user\/(\\d+)/);
if (userMatch) authorId = userMatch[1];
const exploreMatch = u.match(/\/explore\/(\\w+)/);
if (exploreMatch) authorId = exploreMatch[1];
// 是否自己的页面
const isSelf = u.includes('/user/self') ||
!!document.querySelector('[data-e2e="user-info"]');
return {
video_count: vc,
card_count: cards,
has_search_bar: !!searchBar,
has_nav: !!nav,
has_comment_list: !!commentList,
author_id: authorId,
is_self: isSelf
};
}""")
except:
return {"video_count":0, "card_count":0, "has_search_bar":False,
"has_nav":False, "has_comment_list":False, "author_id":"", "is_self":False}
def _classify_page_type(self, state: PageState) -> str:
"""L1: 页面类型判定"""
u = state.url.lower()
# 个人主页
if '/user/' in u:
return 'profile'
# 搜索页
if '/search' in u:
return 'search'
# 播放页: 有video
if state.video_count > 0:
if '/video/' in u and 'modal_id' not in u:
return 'player_full'
if 'modal_id' in u:
return 'player_modal'
return 'player'
# 笔记详情页 (小红书)
if '/explore/' in u and u.count('/') >= 4:
return 'note_detail'
# 首页/grid
if state.card_count > 0 or '/jingxuan' in u or '/explore' in u:
return 'grid'
return 'unknown'
def _classify_region(self, x: int, y: int, w: int, h: int) -> str:
"""L2: 鼠标区域判定(基于百分比位置)"""
x_pct = x / w
y_pct = y / h
# 视频播放区(中央上部区域)
if 0.2 < x_pct < 0.8 and 0.1 < y_pct < 0.5:
return 'video_area'
# 底部互动按钮区
if 0.2 < x_pct < 0.8 and y_pct > 0.8:
return 'bottom_bar'
# 评论区(底部大区域)
if y_pct > 0.7:
return 'comment_area'
# 左侧导航区
if x_pct < 0.2:
return 'nav_area'
# 右侧推荐区
if x_pct > 0.8:
return 'sidebar'
# 内容feed区(中下部)
if 0.2 < x_pct < 0.8 and 0.5 < y_pct < 0.8:
return 'feed_area'
return 'unknown'
async def demo(page):
"""快速演示"""
inspector = PageInspector(page, "douyin")
# 全量检测
state = await inspector.inspect()
print(f"[{state.page_type}] v={state.video_count} c={state.card_count}")
# 带鼠标位置的检测(模拟鼠标在右下角互动区)
state2 = await inspector.inspect(mouse_x=479, mouse_y=687)
print(f"鼠标区域: {state2.mouse_region}")
print(f"指向元素: {state2.element_at_point}")
print(f"附近元素: {len(state2.nearby_elements)}个")
for el in state2.nearby_elements[:3]:
print(f" {el['tag']} dist={el['dist']} {el['class'][:20]}")
return state2
@@ -0,0 +1,202 @@
# 2026-06-20 工作日誌
## 00:00-00:30 — 执行状态显示细腻度升级
### 背景
用户反馈 Dashboard 侧边栏的执行历史区块只显示"进程运行中"/"进程已结束"两种状态,但后端状态机有10种状态。
### 修改内容
- **后端** (`services/command_bus.py`): `CommandStatus` 枚举定义 10 种状态:queued / preflighting / preflight_failed / dispatching / running / completed / failed / timed_out / crashed / cancelled ✅ 已有
- **前端** (`frontend/src/modules/matrix_views.js`): 重写 `loadExecutionHistory()` 函数:
- 新增 `STATUS_MAP` 常量,每种状态映射到独立图标+颜色+中文标签+分组
- 顶部显示各状态的实际数量标签(不合并分组)
- 每台机器显示 ok/active/fail 细分(不再只看完成率)
- 每条执行记录显示:状态标签 | 机器 | 账号 | 消息 | 耗时 | 时间
- 格式化的耗时显示(s/m 自适应)
- Vite build 完成,hash 更新为 index-BhCDf8FF.js
- 无需重启服务(静态文件在磁盘上实时读取)
## 00:30-01:00 — 信息采集全链路断点分析(L1→L5)
### 背景
用户反馈信息采集页面点击「采集选中」后没有获取到内容。
### 分析结论
**断点在 L1(登录检测)**,不是数据回传问题:
- 所有 recent collect log 都显示 `ensure_login` 返回 `"unknown"` → 账号被跳过
- 实际 douyin_134 有 3 个 session cookie(有效登录态)但 DOM 锚点检测不到
- 旧版 `collect_batch_runner.py` 用不同检测逻辑能正确识别,新版 `LoginStateMachine._detect()` 只查 DOM 锚点失败
### L1 修复
1. **抖音 `logged_in` 锚点从 2 个扩展到 9 个**
2. **新增 Cookie 兜底检测** — DOM 检测失败后用 `page.context.cookies()` 检查 sessionid/token
3. **新增页面标题检测** — `"xxx的抖音"` = 已登录,`"记录美好生活"` = 未登录
4. **`_recover_cookie()` 改为导航到 `user/self`** — 不再只刷新首页,强制触发登录态
### L1→L5 测试验证
- L1: ✅ 登录检测通过(dl/fix-recover-cookie -> 进入蓝图执行)
- L2: ✅ 蓝图引擎启动,到达 check_verify_dialog 钩子
- L3: ✅ CommandBus dispatch → status:"accepted", PID=32964
- L4: ✅ API /api/ops/status 正常返回
- L5: ✅ 前端可通过 API 获取(等待下次 build)
- 次要问题:douyin_test 触发 SMS 验证弹窗导致执行卡住(已有钩子但 _recover_sms 阻塞)
### 文档更新
- `FEDERATION_GUIDE.md` 新增「十一、五层执行架构 (L1–L5)」完整文档
- 更新 4.4 登录状态机说明,记录 2026-06-20 修复细节
- 合并 4.4/4.5 多机账号管理部分(之前有格式错误)
## 09:00-16:30 — 登录状态机重构 + 全场景验证通过
### 背景
- 14:00 用户提供阿里百炼 API key,配置 vision_bridge.py 远程视觉支持(DashScope qwen-vl-plus)
- 15:00-16:20 反复调试抖音登录,逐个修复问题
### 修复清单
1. **检测器误判 logged_in** — 移除 `[data-e2e="user-avatar"]` 锚点(视频创作者头像也匹配)
2. **增加文本检测** — `body.innerText` 检查"未登录"和"粉丝/关注"关键词
3. **登录按钮被浮层拦截** — JS fallback `el.click()` 绕过 Playwright 可见性检查
4. **手机号为空** — 添加 `_ensure_phone()` 检查当前值,空则重填
5. **短信验证码取旧码** — 改为直接调用 ApiSMSHandler._fetch_messages() + 过滤 `id > min_id`
6. **登录按钮是"确认登录"不是"登录"** — 多文本匹配
### 验证结果(三种场景全通过)
- **全新登录**: detect→点登录→填手机→获取验证码→登录→douyin_read_profile 9步 ✅
- **短期过期**: detect→JS点登录→JS点一键登录→验证码→确认登录→douyin_read_profile 9步 ✅
- **已登录**: detect→logged_in→直接执行蓝图 ✅
### 代码清理
- `login_state_machine.py` 重构为三组件架构(PlatformDetector / RecoveryChain / LoginManager)
- `SmsRecovery` 限制仅小红书使用(抖音由 DouyinLoginRecovery 处理)
- `cdp_connector.py` close() 三层兜底修复 Camoufox stop() 缺失
- `mc/cli.py` setup_log() 加 logging.basicConfig 修复日志不输出
- 归档 2 个 export_* 蓝图到 _archive/
- `FEDERATION_GUIDE.md` 更新登录状态机章节 + 蓝图清单
## 16:30-18:10 — 小红书登录修复 + 全场景验证通过
### 修复清单
1. SmsRecovery 调用错版本 — 之前调 sms_login(抖音版),改为直接调 xhs_login(小红书6位框版)
2. XhsDetector cookie 误判 — 移除 cookie 直接返回 logged_in
3. xhs_login.is_logged_in() 太松 — 改为严格匹配 + 登录面板检测
4. 步骤顺序错误 — 「协议同意处理」移到点登录后
5. "同意并继续"按钮文字 — 改为「同意并继续」,class 为 div.foot-btn
6. 缺少 oauth-tip 切换 — 填手机号后需点 div.oauth-tip
7. SmsRecovery 二次验证过严 — 移除重复验证,信任 xhs_login
### 验证结果(18:08 全新登录 8/8 成功)
✅ 昵称=橙子哥 | ID=254437248 | 关注=1 | 粉丝=6
### 两种平台验证状态
| 场景 | 抖音 | 小红书 |
|:-----|:----:|:------:|
| 全新登录 | ✅ | ✅ |
| 短期过期 | ✅ | 🔵 |
| 已登录 | ✅ | ✅ |
## 18:10-18:45 — 养号蓝图 v2.0 更新
### 基础修
1. 冷却前缀匹配 — `_get_cooldown()` 去掉 `xhs_`/`dy_`/`dy_` 前缀
2. wait_watch 补到 xhs_ops — 使小红书养号蓝图可用
3. 搜索 @random 支持 — douyin_ops + xhs_ops 均支持随机关键词
### 4个养号蓝图更新
| 蓝图 | 版本 | 步数 | 变化 |
|:-----|:----:|:----:|:-----|
| douyin_daily | v2.0 | 40 | 10个视频,1-2条评论,概率点赞/收藏 |
| douyin_active_v1 | v2.0 | 80 | 2次@random搜索+各6结果,50%评论率 |
| xhs_daily | v2.0 | 49 | 10篇笔记,1-2条评论,参照抖音 |
| xhs_active_v1 | v2.0 | 89 | 2次搜索+各6结果+关注,参照抖音 |
## 19:10-20:40 — 养号蓝图 v2.x 修复
### 抖音日常 v2.1 (最终通)
- 根因: `open_video` 只单点不验证 + 蓝图未进详情页
- 修复: open_video 双击+验证+焦点+回首页兜底
- like/collect 改为键盘 z(非 DOM 点击)
- 结果: 45步 39成功 6失败 ✅
### 修复清单
1. douyin_ops._click_selector — 加 JS fallback(备用)
2. douyin_ops.like — 键盘 z 为主线
3. douyin_ops.open_video — 双击+验证+焦点+回首页
4. douyin_daily蓝图 — 每视频前加 open_video
5. xhs_ops — 补 wait_watch、go_back
6. xhs_ops.xhs_click_note — 回首页兜底、返回 False
7. engine._get_cooldown — 去前缀匹配
### 待测试
- xhs_daily 小红书日常(需用户配合验证码)
## 20:40-21:40 — 抖音版面整理 + 录制验证
### 录制成果(2轮,共48步操作)
- 第1轮: 弹窗模式完整操作(点赞z→评论x→收藏→关注g→下一页→首页→主页)
- 第2轮: 搜索"美食"→结果操作 + 全屏详情页/video/xxx操作
- 发现: 键盘快捷键(z/x/g/ArrowDown) 两种模式通用
- 收藏按钮是SVG图标,非 data-e2e 选择器
### 蓝图整理(9→6个活跃)
| 状态 | 蓝图 |
|:----:|:------|
| ✅ 保留 | douyin_daily / active / comment / reply / search / read_profile |
| 📦 归档 | comment_test / search_browse / collect(_archive/ 保留旧版做参考) |
| ↩️ 不删 | _archive/12个旧蓝图保留不动 |
## 23:50 — 今日复盘与明日任务清单
### 今日底线(已通过验证)
- douyin_read_profile: ✅
- xiaohongshu_read_profile: ✅
- 抖音登录(3场景): ✅
- 小红书登录(全新登录): ✅
- 抖音原子操作全测: 15/16 ✅(仅follow失败)
### 今日未解决的问题
#### 1. post_comment(抖音)— 代码返回成功但实际没发
- 根因: _do_execute 中 `ok = await self.post_comment()` 返回 `'ok'` 字符串,Python 视为 truthy,直接显示 sent
- 实际: 注释输入框 `.public-DraftEditor-content` 点击→打字→Enter,但评论未出现在列表
- 修复方向(已改未测): 加 send 按钮查找(发送/发布类名)+comment-list 文本验证,验证不通过返回 failed
- 旧代码参考: `archive/interactive_test.py` 的 step_send_comment 和 `matrix_modules/nurture/runner.py` 的 _send_comment
#### 2. follow(抖音)— 失败
- 当前实现: `page.keyboard.press("g")` + _click_selector 查找关注按钮
- 录制发现: 用户按 g 键关注/取消关注,键盘方法本身正确
- 可能原因: 视频详情页中已关注作者,g 键变成取消关注
#### 3. xhs_post_comment(小红书)— 完全没发出去
- 根因: `p.content-input` 选择器找不到(CSS modules 添加 hash)
- 录制数据: 正确路径是 → 笔记详情页滑到底部 → 找到 `DIV[class*="inner"]` 点击激活 → `P.content-input` 出现 → 填字 → `BUTTON.btn.submit` 发送
- 但 `p.content-input` 始终匹配不到,尝试了 x 键/JS激活/scrollTo 等5种方式均失败
- 明天方向: 用截图+视觉识别找输入框位置,或改用 Playwright getByPlaceholder
#### 4. xhs_click_note 在 go_back 后失败
- 笔记返回首页后 feed 未渲染完就找笔记,找不到
- 方向: go_back 后加 2-3秒延迟
### 明日任务清单(优先级排序)
```
P0: 修抖音 post_comment 验证(已改代码,明天测试验证)
P0: 修抖音 follow(查 g 键是否被已关注状态干扰)
P0: 修小红书 post_comment(截图视觉分析找输入框)
P1: 修 xhs_click_note 后 feed 刷新延迟
P1: 修 xhs_like 选择器稳定性
P1: 抖音高活跃蓝图 douyin_active 测试
P1: 新建小红书 xhs_comment_on_post / xhs_search 蓝图
P2: 小红书高活跃蓝图 xhs_active 测试
P2: 双平台日常养号自动化定时任务
```
### 关键教训(以后不要犯)
1. 录制数据要直接用(坐标/元素类名),不要加 is_visible() 等额外检查
2. 抖音和小红书的操作不能通用(x 键开评论在抖音通,小红书不通)
3. 代码返回 ✅ 不等于页面真的有反应,每种操作必须加验证
4. post_comment 必须检查 comment-list 中是否出现刚发的文字
5. 跑测试用"原子操作全测"蓝图(每个操作一次),不用养号蓝图(49步浪费时间)
6. CSS modules 类名不可靠,优先用 text/placeholder/坐标
7. 每次测试前先给路径让用户审查,确认再跑
补注: 2026-06-21 08:20 确认小红书 xhs_post_comment 测试通过 ✅
@@ -0,0 +1,48 @@
# 2026-06-21 工作日志
## 今日计划
基于昨日录制数据和旧稳定代码,修复两个平台剩余原子操作
## 08:00-08:20 — 双平台修复+测试
### 修复内容
1. 抖音 follow: KEYS['follow'] 从 'f' 改为 'g'(录制发现用户用g键) + 键盘g兜底 ✅
2. 抖音 post_comment: 加发送按钮查找+comment-list文本验证(参考旧版interactive_test.py) ✅
3. 抖音 open_video: 加feed加载等待 + 多选择器(discover-video-card/href/video/feed-card) ⚠️
4. 小红书 post_comment: 重写为JS找textarea/input/editable + 找发送按钮 ✅
### 测试结果
| 平台 | 步数 | 成功 | 失败 | 耗时 |
|:-----|:----:|:----:|:----:|:----:|
| 抖音 | 16 | 13 | 3 | 4.9min |
| 小红书 | 16 | 12 | 4 | 10.8min |
### 关键进展
- **小红书 post_comment 首次成功发送评论** ✅ "👍评论(测试评论) (5.0s)"
- 抖音 follow 键盘g生效 ✅
- 小红书 like/collect/search 全部正常 ✅
### 剩余问题
1. 抖音 open_video: 当前 douyin `/jingxuan` 页面没有 `.discover-video-card-item`,需录制查看实际DOM
2. 小红书搜索结果页: search后页面布局不同,click_note/like/collect/follow 均失败
3. xhs_like 在非笔记页(搜索页)取31s兜底
### 明日任务
P0: 抖音 open_video 录制→找出正确的选择器
P1: 小红书搜索结果页操作适配
P1: 双平台日常养号蓝图测试
## 09:00-10:05 — 架构反思 + 新方案设计 + 录制验证
### 关键突破
1. 发现collect用的键是C(不是无键),KEYS['follow']是g不是f
2. 拿到完整抖音快捷键表
3. 创建PageInspector(L1/L2/L3) + 四层状态模型
4. ghai录制18步(首次用状态diff思路)
5. 提出L4视觉状态层(按钮变色等)
6. 完整经验文档: EXPLAIN-atomic-ops-20260621.md
### 新核心文件
- page_inspector.py: 四层状态检测器
- PLAN-atomic-ops-v2.md: 完整项目规划
- EXPERIENCE-atomic-ops-20260621.md: 经验文档
@@ -0,0 +1,147 @@
# AgentOS 联邦系统 — 三台机器统一环境
## 机器对照表
| ORACLE 名 | Tailscale 名 | IP | SSH 用户 | Home 目录 |
|:----------|:-------------|:---|:---------|:----------|
| chengzigedeAir | macbook-air | 100.111.43.6 | chengzige | /Users/chengzige |
| 5kechengdeAir | 5macbook-air | 100.72.182.121 | 5kecheng | /Users/5kecheng |
| 7kecheng | 7macbook-air | 100.65.35.28 | 7kecheng | /Users/7kecheng |
## 统一后的 Python 环境
| 项目 | 三台机器一致 ✅ |
|:-----|:---------------|
| 养号用 venv | agent-os (旧名 matrix 已废弃) ✅ |
| Python 版本 | 3.13.12 ✅ |
| Python 签名 | adhoc(无 Team ID)✅ |
| orjson | 3.11.9 ✅ |
| camoufox | 0.4.11 ✅ |
| playwright | 1.58.0 ✅ |
| Git 版本 | 一致 ✅ |
| 蓝图文件 | 14 个(已同步)✅ |
## PYTHON 路径规范(重要)
所有涉及 PYTHON 路径的地方**必须使用 agent-os venv**,禁用了 matrix venv:
- `$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3`
- 旧路径 `envs/matrix/` 已全部替换
- 远程执行用 `$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3`
- remote_exec.py 使用 `$MC_PYTHON` 动态发现变量
## 登录系统关键发现
- 小红书登录弹窗: 无cookie自动弹出, DOM类名 `.login-container` / `.reds-modal-open`
- 手机号输入框: `input[placeholder="输入手机号"]`
- "继续"按钮: 手机号输入框右侧, 文字"继续", 无勾选协议
- 填手机号必须用 Playwright `page.fill()` 或 `page.type()` (+键盘事件), 不能用 JS set value (React不识别)
- 填完后按 Tab 键触发失焦, "继续"按钮才可点击
- 短信验证码: 通过 ApiSMSHandler 轮询 wx.tyhtak.com API
- 抖音登录已有完整 douyin_login.py (含sms_login原子操作)
## 小红书登录方案演进
### v1/v2 (已淘汰) — 鼠标点击协议复选框 ❌
- 找包含"同意"的最小高度可见元素 → `mouse.click(left - 20px, center_y)`
- 问题: 小红书复选框是自定义元素(非原生 `<input type="checkbox">`),left-20px 定位极不可靠
- 多次验证失败,元素位置随弹窗布局变化
### v3 (当前方案, 待验证) — 点"同意并登录"按钮 ✅
- 新发现: 点击「直接登录」按钮会弹出浮窗,浮窗上有「同意并登录」按钮
- 点击「同意并登录」= 协议勾选 + 登录意图一次性完成
- **不再需要**定位复选框或 left-20px hack
- 2026-06-16 晚发现该方案,尚未做 DOM 扫描验证
### 原子操作文件
- `matrix_modules/account/xhs_login.py` — 小红书原子操作(仿 sms_login.py 模式)
- 状态检测: `has_login_panel()`, `has_sms_inputs()`, `is_logged_in()`
- 操作函数: `fill_phone()`, `click_continue()`, `click_agree_and_login()`, `fill_6_digit_code()`, `click_largest_login_btn()`
- 主流程: `xhs_login(page, phone, account_name, log_func, use_v3)`
- `matrix_modules/account/xiaohongshu_login.py` — 顶层入口(调用 xhs_login 原子操作)
## 路径变量
- AGENT_SYNC=$HOME/workbuddy-agent-os/agent-sync
- AGENT_LOCAL=$HOME/workbuddy-agent-os/agent-local
## 远程管理
本机 (chengzigedeAir) 可通过 SSH 控制所有机器:
```bash
ssh [email protected] "命令"
ssh [email protected] "命令"
# 或通过 mc CLI
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts
python3 -m mc remote exec 5kechengdeAir "命令" --via ssh
```
## 版本锁定
依赖版本已锁定在 `agent-sync/requirements.txt`
新机器部署后执行: `pip install -r requirements.txt`
## 代码签名修复
新机器必须执行: `codesign -f -s - $HOME/.workbuddy/binaries/python/versions/3.13.12/bin/python3`
deploy.sh 已自动检测此步骤。
## 部署文档
- `DEPLOY-GUIDE.md` v2.0: 完整部署指南
- `deploy.sh`: 一键部署脚本
- `fleet_sync.sh`: 多机同步
- `fleet_reconcile.sh`: 对账引擎
## 架构关键概念(已修正 v2)
- **身份模型**: 一个手机号=一个身份目录。这个身份下可以存多个平台账号(xhs+douyin), 跨平台只需改 URL, 不用重开浏览器
- **窗口排布**: 本机最多3个浏览器同时运行, 每个窗口 X坐标+150 重叠排布(cdp_connector window_position参数)
- **蓝图定义**: 蓝图是你和我交互的"命名模板+参数", 不是内部执行步骤链。如 daily(时长), comment(url+text)
- **原子操作**: 最小执行单元, 自带状态检测。执行中触发验证弹窗 → 自动组合其他原子操作(如SMS登录)恢复
- **状态机核心**: 每次操作前必检登录态 → 执行 → 后检查是否触发验证 → 验证则自动恢复 → 冷却 → 下一步
## Vite 前端重构 (已完成)
- `static/index.html` 7700行 → Vite + Vanilla JS 模块化架构 ✅
- 已迁移到按视图拆分文件,Vite HMR 热更新 ✅
## 五层执行架构 (L1-L5) — 2026-06-20
所有 Dashboard 操作的执行链路:
```
L5 Dashboard UI → L4 API路由 → L3 CommandBus → L2 mc引擎 → L1 浏览器
```
### 各层核心文件
| 层 | 位置 |
|:---|:-----|
| L5 | `frontend/src/views/matrix-*.js` |
| L4 | `routes/ops.py`, `routes/matrix.py` |
| L3 | `services/command_bus.py` |
| L2 | `scripts/mc/engine.py`, `scripts/mc/run.py` |
| L1 | `scripts/douyin_ops.py`, `scripts/xhs_ops.py` |
### 登录检测 (2026-06-20 重构 v2.0)
- **架构**: PlatformDetector(策略模式) + RecoveryChain(可配置) + LoginStateMachine(编排器)
- **检测**: DouyinDetector 四重检测 — DOM锚点 → 页面文本("未登录"/"粉丝+关注") → 标题 → Cookie(仅日志)
- **恢复链**: DouyinLoginRecovery(抖音专用) → CookieRecovery → SmsRecovery(小红书) → VisualRecovery
- **三种场景全通过**: 已登录 ✅ / 短期过期(一键+SMS) ✅ / 全新登录(手机+SMS) ✅
- **关键**: 登录按钮文字在短期过期时为"确认登录",全新登录时为"登录"
- **参考**: `FEDERATION_GUIDE.md` 4.4 节 + 第十一章完整文档
## 原子操作体系 v2(2026-06-21 建立)
### 核心文档
| 文档 | 路径 | 内容 |
|:-----|:-----|:-----|
| 经验文档 | `.workbuddy/memory/EXPERIENCE-atomic-ops-20260621.md` | 完整时间线、设计原则、选择器优劣、快捷键表、代码状态 |
| 项目规划 | `.workbuddy/memory/PLAN-atomic-ops-v2.md` | 三层架构设计、增强录制、单步调试、看板集成 |
| 架构审计 | `.workbuddy/memory/ARCHITECTURE_AUDIT.md` | 新旧代码对比、根本问题分析、重构方案 |
### 关键技术突破
1. **状态检测模型**: L1页面类型 + L2鼠标区域 + L3元素详情 + L4视觉状态
2. **键盘快捷键优先**: Z赞/C收藏/G关注/X评论/B弹幕/V分享 — DOM只是兜底
3. **录制增强**: 只按一次·标记状态diff, 系统自动推断操作类型
4. **单步调试**: 3秒出结果, 不等完整蓝图
### 各平台操作清单
| 抖音 | 方法 | 小红书 | 方法 |
|:-----|:-----|:------|:-----|
| like | 键盘Z | xhs_like | DOM点击SPAN.like-lottie |
| collect | 键盘C | xhs_collect | DOM点击SVG(like右边70px) |
| follow | 键盘G | xhs_follow | DOM点击SPAN.reds-button |
| open_comments | 键盘X | xhs_comment | 需打开评论区(不能用X) |
| post_comment | X→点输入框→打字→点发送 | xhs_post_comment | JS找输入框+按钮 |
| close_comments | 键盘X | — | go_back() |
| next_video | 键盘↓ | xhs_scroll_feed | 键盘↓ |
| open_video | 双击卡片(键盘→DOM→坐标三层兜底) | xhs_click_note | 点笔记封面 |