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,21 @@
|
||||
profiles/
|
||||
|
||||
# Build / Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Local config (per-machine, not synced)
|
||||
local.yaml
|
||||
# Temp artifacts
|
||||
*.png
|
||||
temp_*.json
|
||||
diagnose_ops.py
|
||||
step_by_step_*.py
|
||||
praise_all.sh
|
||||
@@ -0,0 +1 @@
|
||||
2026-04-30T11:39:39.643502+08:00
|
||||
@@ -0,0 +1 @@
|
||||
2026-04-30T11:39:39.643640+08:00
|
||||
@@ -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** |
|
||||
@@ -0,0 +1,120 @@
|
||||
# Matrix 系统文档索引
|
||||
|
||||
> 版本: v5.3 | 最后更新: 2026-06-13
|
||||
> 新机器部署请先看本文档,按序号阅读
|
||||
|
||||
---
|
||||
|
||||
## 一、新机器部署(必读)
|
||||
|
||||
| # | 文档 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | `init_matrix.sh` | **一键部署脚本** — 检测环境→修复→启动 Dashboard |
|
||||
| 2 | `config_template/accounts.yaml` | 账号配置模板(含身份共享规范) |
|
||||
| 3 | `config_template/profiles.json` | 人设数据模板 |
|
||||
| 4 | `config_template/schedule.yaml` | 定时任务配置模板 |
|
||||
| 5 | `config_template/ai.yaml` | AI 评论生成配置模板 |
|
||||
| 6 | `start_dashboard.sh` | Dashboard 启停脚本 |
|
||||
| 7 | `fix_dashboard_launchd.sh` | launchd plist 修复脚本 |
|
||||
|
||||
## 二、核心文档
|
||||
|
||||
| # | 文档 | 说明 |
|
||||
|---|------|------|
|
||||
| 8 | `README.md` | **项目总览** — 功能、命令、架构 |
|
||||
| 9 | `TOOL.md` | **工具说明** — 版本、前置依赖、文件结构 |
|
||||
| 10 | `MODULE.md` | **模块架构** — 系统层级、文件依赖 |
|
||||
| 11 | `docs/MC_COMMAND_REFERENCE.md` | **命令参考手册** — 所有 CLI 命令详解 |
|
||||
| 12 | `docs/MATRIX_V5_GUIDE.md` | **v5 使用指南** — 完整操作指南 |
|
||||
| 13 | `docs/CORPUS_V2_PLAN.md` | 语料库架构升级方案 v2.0 |
|
||||
|
||||
## 三、配置说明
|
||||
|
||||
| # | 文件 | 说明 |
|
||||
|---|------|------|
|
||||
| 14 | `accounts_registry.yaml` | 账号注册表(联邦同步用) |
|
||||
| 15 | `config/accounts.yaml`(agent-local) | 本地账号配置 |
|
||||
| 16 | `config/sms.yaml` | 短信 API 配置 |
|
||||
| 17 | `config/ai.yaml` | AI 评论生成配置 |
|
||||
| 18 | `config/schedule.yaml` | 定时任务配置 |
|
||||
| 19 | `corpus/douyin.yaml` | 抖音语料库(33条) |
|
||||
| 20 | `corpus/xiaohongshu.yaml` | 小红书语料库(31条) |
|
||||
|
||||
## 四、蓝图清单
|
||||
|
||||
| # | 蓝图 | 平台 | 步骤 | 用途 |
|
||||
|---|------|------|------|------|
|
||||
| 21 | `douyin_daily` | 🎵 | 23步 | 日常养号(随机浏览+点赞+收藏+评论) |
|
||||
| 22 | `douyin_active_v1` | 🎵 | 27步 | 高活跃养号 |
|
||||
| 23 | `douyin_comment` | 🎵 | 5步 | 定向评论(给链接→打开→评论) |
|
||||
| 24 | `douyin_search` | 🎵 | 14步 | 搜索浏览(搜关键词→随机浏览+互动) |
|
||||
| 25 | `douyin_collect` | 🎵 | 5步 | 信息采集(搜博主→采集主页信息) |
|
||||
| 26 | `douyin_reply` | 🎵 | 5步 | 作者回复(打开自己视频→读评论→回复) |
|
||||
| 27 | `douyin_read_profile` | 🎵 | 9步 | 读取抖音主页信息 |
|
||||
| 28 | `xhs_daily` | 📕 | 17步 | 小红书日常养号 |
|
||||
| 29 | `xhs_active_v1` | 📕 | 26步 | 小红书高活跃养号 |
|
||||
| 30 | `xiaohongshu_read_profile` | 📕 | 8步 | 读取小红书主页信息 |
|
||||
|
||||
## 五、原子操作
|
||||
|
||||
| # | 操作 | 说明 |
|
||||
|---|------|------|
|
||||
| 31 | `goto_home` | 导航到首页 |
|
||||
| 32 | `wait_watch` | 等待观看(随机5-12秒) |
|
||||
| 33 | `like` | 点赞 |
|
||||
| 34 | `collect` | 收藏 |
|
||||
| 35 | `post_comment` | 发表评论(含@corpus占位符替换) |
|
||||
| 36 | `open_video` | 打开视频(双击卡片) |
|
||||
| 37 | `next_video` | 切换到下一个视频 |
|
||||
| 38 | `search` | 搜索关键词 |
|
||||
| 39 | `scroll_feed` | 滚动信息流 |
|
||||
| 40 | `goto_profile` | 进入个人主页(采集全部字段) |
|
||||
| 41 | `read_my_comments` | 读当前视频评论区 |
|
||||
| 42 | `reply_comment` | 回复评论 |
|
||||
| 43 | `set_account_id` | 设置账号上下文(人设/语料匹配用) |
|
||||
|
||||
## 六、旧文档(已归档,仅供参考)
|
||||
|
||||
```
|
||||
docs/ARCHITECTURE_FULL.md
|
||||
docs/BASE_CONVENTIONS.md
|
||||
docs/REFACTOR-PLAN.md
|
||||
docs/REPAIR_PLAN.md
|
||||
docs/SYSTEM_ARCHITECTURE.md
|
||||
docs/PROJECT_OVERVIEW.md
|
||||
docs/ANTI-DETECTION-PLAN.md
|
||||
(以上为 v4.x 旧文档,内容已过时)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新机器部署步骤
|
||||
|
||||
```bash
|
||||
# 1. 拉取代码
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git pull
|
||||
|
||||
# 2. 运行初始化脚本(自动检测环境)
|
||||
bash 05_tools/07_matrix/init_matrix.sh
|
||||
|
||||
# 3. 重启 Dashboard
|
||||
launchctl kickstart -k gui/$(id -u)/com.agentos.dashboard
|
||||
|
||||
# 4. 浏览器打开 http://localhost:9988
|
||||
# 左侧「矩阵系统」→「身份与账号」→ 确认账号列表
|
||||
```
|
||||
|
||||
如果 Dashboard 无法启动,单独修复:
|
||||
```bash
|
||||
bash 05_tools/07_matrix/fix_dashboard_launchd.sh
|
||||
```
|
||||
|
||||
## 关键变更说明(v5.0 → v5.3)
|
||||
|
||||
- **身份共享**:同手机号共用同一浏览器指纹(identity_dir=phone_手机号)
|
||||
- **引擎重构**:按身份分组→同浏览器多平台→不同组错峰启动
|
||||
- **@corpus 恢复**:蓝图中的 @corpus 占位符自动替换为真实评论
|
||||
- **蓝图平台标记**:所有蓝图名称带平台前缀(抖音-日常养号/小红书-日常养号)
|
||||
- **清除登录**:每个平台可单独清除登录状态(不影响同身份其他平台)
|
||||
- **批量执行**:账号不默认勾选、蓝图显示全部带平台标签
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: matrix
|
||||
version: "2.1.0"
|
||||
type: tool
|
||||
description: 多平台社交账号矩阵养号工具
|
||||
author: agent-os
|
||||
last_updated: 2026-05-01
|
||||
---
|
||||
|
||||
# Matrix 养号工具模块
|
||||
|
||||
## 模块信息
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| 名称 | matrix |
|
||||
| 版本 | 2.1.0 |
|
||||
| 类型 | tool |
|
||||
| 路径 | `05_tools/07_matrix/` |
|
||||
| 本地数据 | `agent-local/tools/matrix/` |
|
||||
| 依赖 | patchright, camoufox, PyYAML |
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
05_tools/07_matrix/ ← 同步目录(Git + 坚果云)
|
||||
├── MODULE.md ← 本文件(模块描述)
|
||||
├── install.sh ← 新机部署脚本
|
||||
├── requirements.txt ← Python 依赖
|
||||
├── local.yaml.template ← 本地配置模板
|
||||
├── config_template/ ← 账号配置模板
|
||||
├── scripts/ ← 核心代码
|
||||
├── blueprints/ ← 蓝图文件
|
||||
└── docs/ ← 标准化文档
|
||||
|
||||
agent-local/tools/matrix/ ← 本地数据(每机独立,不同步)
|
||||
├── config/accounts.yaml ← 本机账号配置
|
||||
├── data/cookies/ ← 登录 Cookie
|
||||
├── data/matrix.db ← 执行记录
|
||||
├── profiles/ ← Chrome Profile(需重新登录)
|
||||
├── logs/ ← 运行日志
|
||||
└── screenshots/ ← 截图
|
||||
```
|
||||
|
||||
## 部署方式
|
||||
|
||||
```bash
|
||||
# 本模块的部署由 agentos upgrade 统一管理
|
||||
# 如单独执行:
|
||||
bash install.sh # 建目录 + 装依赖 + 生成 local.yaml
|
||||
pip install -r requirements.txt # 安装 Python 包
|
||||
python -m patchright install chromium # 安装 Playwright Chromium
|
||||
pip install camoufox && python -m camoufox fetch # 安装 Camoufox
|
||||
```
|
||||
|
||||
## 升级历史
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| 2.1.0 | 2026-05-01 | 原子化登录模块 auth_manager.py + 稳定性修复 |
|
||||
| 2.0.0 | 2026-04-30 | Camoufox 集成 + 蓝图引擎升级 |
|
||||
| 1.0.0 | 2026-04-27 | 初始版本 |
|
||||
@@ -0,0 +1,222 @@
|
||||
# Matrix 矩阵养号系统
|
||||
|
||||
> **版本**: v5.2 | **环境**: macOS (Apple Silicon) | **引擎**: Camoufox (Firefox 内核)
|
||||
> **平台**: 抖音 ✅ | 小红书 ✅ | 知乎 🔄
|
||||
> **最后更新**: 2026-06-13
|
||||
|
||||
---
|
||||
|
||||
## 一句话
|
||||
|
||||
Matrix 是一套社交账号养号自动化系统。通过 Camoufox 控制浏览器,自动完成日常养号任务(浏览、点赞、评论、搜索),模拟真实用户行为。
|
||||
|
||||
代码在 `agent-sync`(Gitee + GitHub 双同步),数据在 `agent-local`(本机,不同步)。
|
||||
|
||||
---
|
||||
|
||||
## 快速上手
|
||||
|
||||
```bash
|
||||
# 1. 进入目录
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 2. 查看所有命令
|
||||
./mc --help
|
||||
|
||||
# 3. 日常养号(抖音)
|
||||
./mc run --accounts douyin_test --blueprints douyin_daily --rounds 3
|
||||
|
||||
# 4. 日常养号(小红书)
|
||||
./mc run --accounts xhs_01 --blueprints xhs_daily --rounds 2
|
||||
|
||||
# 5. 定向评论(给链接+方向)
|
||||
./mc task comment --url https://v.douyin.com/xxx --direction 称赞
|
||||
|
||||
# 6. 搜索浏览
|
||||
./mc task search --keyword 美食探店 --rounds 5
|
||||
|
||||
# 7. 启动 Dashboard
|
||||
open http://localhost:9988
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard 导航
|
||||
|
||||
| 菜单 | 功能 |
|
||||
|------|------|
|
||||
| **首页** | 运营数据看板(本机账号/已登录/已采集/联邦账号) |
|
||||
| **身份与账号** | 短信接收、注册新账号、账号卡片(四维状态+人设)、删除/登录/采集 |
|
||||
| **命令与任务** | TAB1: 批量执行 / TAB2: 定向评论 / TAB3: 定时任务 |
|
||||
| **账号总览** | 多机摘要 + 全账号表格 |
|
||||
| **蓝图编排** | 管理所有蓝图 |
|
||||
| **原子操作** | 原子操作库 |
|
||||
| **录制管理** | 录制/分析/导出操作 |
|
||||
| **⚙️ 矩阵设置** | TAB1: 导入导出 / TAB2: 备份恢复 / TAB3: 语料库 |
|
||||
|
||||
启动 Dashboard:
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/10_dashboard
|
||||
nohup ~/.workbuddy/binaries/python/envs/agent-os/bin/uvicorn app:app --host 0.0.0.0 --port 9988 &
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 日常养号
|
||||
|
||||
5 个核心蓝图:
|
||||
|
||||
| 蓝图 | 用途 | 执行 |
|
||||
|------|------|------|
|
||||
| `douyin_daily` | 抖音日常养号(浏览+点赞+收藏+评论) | `mc run --accounts A --blueprints douyin_daily --rounds 3` |
|
||||
| `xhs_daily` | 小红书日常养号 | `mc run --accounts A --blueprints xhs_daily --rounds 3` |
|
||||
| `douyin_comment` | 定向评论(打开链接→评论) | `mc task comment --url URL --direction 称赞` |
|
||||
| `douyin_search` | 搜索浏览(搜关键词→随机看视频) | `mc task search --keyword K --rounds 5` |
|
||||
| `douyin_collect` | 信息采集(搜博主→采主页信息) | `mc task collect --keyword 博主名` |
|
||||
| `douyin_reply` | 作者回复(看评论→回复) | `mc task reply --account douyin_test` |
|
||||
|
||||
### 智能任务(mc task)
|
||||
|
||||
```bash
|
||||
# 定向评论(自动识别平台、选账号、生成评论)
|
||||
mc task comment --url https://v.douyin.com/xxx --direction 称赞
|
||||
|
||||
# 搜索浏览
|
||||
mc task search --keyword 美食探店 --rounds 5
|
||||
|
||||
# 信息采集
|
||||
mc task collect --keyword 张三说科技
|
||||
|
||||
# 作者回复
|
||||
mc task reply --account douyin_test
|
||||
|
||||
# 参数说明
|
||||
mc task --list
|
||||
```
|
||||
|
||||
### 账号管理
|
||||
|
||||
每个账号四维状态一目了然:
|
||||
```
|
||||
🎵 小美养生茶 douyin_test
|
||||
📱 15370103682
|
||||
✅ 身份 ✅ 登录 ✅ 昵称 ✅ 同步
|
||||
🎯 美食·养生·旅行 | 💬 称赞/提问 ← 人设
|
||||
[🔑 登录] [👤 采集] [🗑 删除]
|
||||
```
|
||||
|
||||
- **状态**:身份 ✅/❌ | 登录 ✅/❌ | 昵称 ✅/❌ | 同步 ✅/❌
|
||||
- **操作**:🔑登录(打开浏览器扫码) | 👤采集(获取昵称/粉丝数) | 🗑删除
|
||||
|
||||
### 注册新账号
|
||||
|
||||
```bash
|
||||
# 方式一:Dashboard 身份与账号 → 注册新账号
|
||||
# 方式二:CLI
|
||||
mc account register --platform douyin --phone 138xxxx --name 备注名
|
||||
```
|
||||
|
||||
- 同手机号注册第二个平台时,自动复用同一份浏览器指纹
|
||||
- 自动创建身份目录 → 打开浏览器 → 扫码登录
|
||||
|
||||
### 定时任务
|
||||
|
||||
```bash
|
||||
# 添加定时任务
|
||||
mc schedule add --id morning --account douyin_test --blueprint douyin_daily --time 09:00 --rounds 3
|
||||
|
||||
# 列出定时任务
|
||||
mc schedule list
|
||||
|
||||
# 启动调度器(保持终端运行)
|
||||
mc schedule start
|
||||
```
|
||||
|
||||
### 账号人设
|
||||
|
||||
每个账号可配置人设,养号时自动按人设选语料方向和搜索关键词:
|
||||
|
||||
```json
|
||||
{
|
||||
"nickname": "小美养生茶",
|
||||
"persona": {
|
||||
"interests": ["美食", "养生", "旅行"],
|
||||
"comment_style": ["称赞", "提问"],
|
||||
"search_keywords": ["美食探店", "养生茶", "周末去哪"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置位置:`agent-local/tools/matrix/data/profiles.json`
|
||||
|
||||
---
|
||||
|
||||
## 新机部署
|
||||
|
||||
```bash
|
||||
# 1. 拉取代码
|
||||
cd ~
|
||||
git clone [email protected]:neowoodland-art/mac-agent-os.git workbuddy-agent-os/agent-sync
|
||||
cd workbuddy-agent-os/agent-sync
|
||||
|
||||
# 2. 运行初始化
|
||||
bash 00_bootstrap/init.sh
|
||||
|
||||
# 3. 安装 Matrix 依赖
|
||||
cd 05_tools/07_matrix
|
||||
bash install.sh
|
||||
|
||||
# 4. 同步本机账号
|
||||
# 从 accounts_registry.yaml 自动同步 assigned_machine 为本机的账号
|
||||
|
||||
# 5. 启动 Dashboard
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/10_dashboard
|
||||
nohup ~/.workbuddy/binaries/python/envs/agent-os/bin/uvicorn app:app --host 0.0.0.0 --port 9988 &
|
||||
|
||||
# 6. 第一次使用需扫码登录
|
||||
./mc account login douyin_test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI 命令参考
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `mc run --accounts A,B --blueprints X,Y --rounds N` | 批量执行养号任务 |
|
||||
| `mc task comment --url U --direction D` | 智能定向评论 |
|
||||
| `mc task search --keyword K` | 搜索浏览 |
|
||||
| `mc task collect --keyword K` | 信息采集 |
|
||||
| `mc task reply --account A` | 作者回复 |
|
||||
| `mc account list` | 列出所有账号 |
|
||||
| `mc account login <name>` | 扫码登录 |
|
||||
| `mc schedule list` | 列出定时任务 |
|
||||
| `mc schedule add --id X --account Y --blueprint Z --time HH:MM` | 添加定时任务 |
|
||||
| `mc schedule start` | 启动定时调度器 |
|
||||
| `mc record start --account A` | 录制原子操作 |
|
||||
| `mc record list/analyze/export` | 录制管理 |
|
||||
| `mc corpus list` | 查看语料库 |
|
||||
|
||||
---
|
||||
|
||||
## 数据存储
|
||||
|
||||
```
|
||||
~/workbuddy-agent-os/
|
||||
├── agent-sync/05_tools/07_matrix/ ← 代码(Git 同步)
|
||||
│ ├── blueprints/ ← 任务蓝图(JSON)
|
||||
│ ├── scripts/mc/ ← 核心引擎
|
||||
│ ├── corpus/ ← 评论语料库
|
||||
│ └── config/ ← 配置(定时任务、AI 等)
|
||||
│
|
||||
└── agent-local/tools/matrix/ ← 数据(本机,不同步)
|
||||
├── config/accounts.yaml ← 本机账号配置
|
||||
├── identities/ ← 浏览器身份目录
|
||||
│ ├── douyin_test/ ← 账号A(含 cookies、指纹)
|
||||
│ └── phone_138xxxx/ ← 同手机号共享身份(新注册)
|
||||
├── data/profiles.json ← 昵称/人设数据
|
||||
├── recordings/ ← 录制包
|
||||
└── logs/ ← 运行日志
|
||||
```
|
||||
@@ -0,0 +1,278 @@
|
||||
# TOOL.md — 矩阵养号系统 Matrix
|
||||
|
||||
> **工具版本**: v4.1
|
||||
> **接入日期**: 2026-04-29
|
||||
> **维护者**: ghai
|
||||
> **路径方案**: local.yaml + local_paths.py(无软链接,多机安全)
|
||||
> **更新日期**: 2026-05-03
|
||||
> **平台支持**: 抖音 ✅ | 小红书 🔄 | 知乎 🔄 | 快手 📋
|
||||
|
||||
---
|
||||
|
||||
## 一句话说明
|
||||
|
||||
通过 Chrome CDP 和 Camoufox(Firefox 内核)程序化控制浏览器,自动完成多平台社交账号的日常养号任务(浏览、点赞、评论、收藏等),模拟真实用户行为。
|
||||
|
||||
---
|
||||
|
||||
## 架构概览(v4.1)
|
||||
|
||||
```
|
||||
matrix.py ← 统一 CLI 入口(account/nurture/config/status 四域)
|
||||
│
|
||||
├── account/ ← 账号管理域
|
||||
│ ├── create_identity.py ← 身份工厂(BrowserForge 固化指纹)
|
||||
│ ├── login_identity.py ← 首次登录(持久化保存登录态)
|
||||
│ ├── sms/ ← 短信验证码接口(预留)
|
||||
│ └── captcha/ ← 图形验证码接口(预留)
|
||||
│
|
||||
├── nurture/ ← 养号执行域
|
||||
│ ├── runner.py ← 常驻循环引擎(Chrome CDP + Camoufox 双引擎)
|
||||
│ └── behavior.py ← 行为模拟参数化(13项可配置)
|
||||
│
|
||||
├── engine/ ← 浏览器引擎层
|
||||
│ └── cdp_connector.py ← Chrome CDP / Camoufox 持久化连接
|
||||
│
|
||||
├── ops/ ← 操作层(多平台)
|
||||
│ └── douyin/
|
||||
│ ├── browse.py ← 浏览类操作
|
||||
│ └── interact.py ← 交互类操作
|
||||
│
|
||||
└── config/ ← 配置管理
|
||||
└── accounts.yaml ← 账号配置(含 identity_dir)
|
||||
```
|
||||
└── screenshots/ # 截图快照
|
||||
|
||||
⭐ 路径解析方式 (v4.0):
|
||||
local_paths.py → 读取 local.yaml → 获取 data_root
|
||||
→ config_path("x") → data_root/config/x
|
||||
→ data_path("x") → data_root/data/x
|
||||
→ logs_path("x") → data_root/logs/x
|
||||
→ profiles_path("x") → data_root/profiles/x
|
||||
→ screenshots_path("x")→ data_root/screenshots/x
|
||||
→ code_dir() → 05_tools/07_matrix/(代码目录)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速使用
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 启动抖音账号(Chrome CDP)
|
||||
python scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
|
||||
# 执行日常浏览蓝图
|
||||
python scripts/task_engine.py --blueprint douyin_browse_v2 --account douyin_01
|
||||
|
||||
# 定时调度
|
||||
python scripts/task_scheduler.py
|
||||
|
||||
# 查看账号状态
|
||||
python scripts/switch_account.py --status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境依赖
|
||||
|
||||
| 依赖 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Python | 3.10+ | 使用 agent-os venv |
|
||||
| Google Chrome | 最新版 | CDP 直连 |
|
||||
| Camoufox | 最新版 | Firefox 内核,反检测 |
|
||||
| playwright | ≥1.40 | 浏览器自动化 |
|
||||
|
||||
完整依赖见 `requirements.txt`,安装:
|
||||
```bash
|
||||
pip install -r ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新机恢复
|
||||
|
||||
```bash
|
||||
# 一键恢复(建立本地目录骨架 + 生成 local.yaml + 安装依赖)
|
||||
bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/install.sh
|
||||
|
||||
# 然后将账号配置复制/重新填写
|
||||
cp ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/config_template/accounts.yaml \
|
||||
~/workbuddy-agent-os/agent-local/tools/matrix/config/accounts.yaml
|
||||
# 编辑 accounts.yaml 填入本机账号信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 统一 CLI 使用(v4.1 推荐)
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts
|
||||
|
||||
# 账号管理
|
||||
python matrix.py account list # 列出所有账号
|
||||
python matrix.py account create <name> # 创建新身份
|
||||
python matrix.py account login <name> # 首次登录
|
||||
python matrix.py account status [name] # 查看状态
|
||||
|
||||
# 养号执行
|
||||
python matrix.py nurture run -a <name> -r 10 # 单账号循环
|
||||
python matrix.py nurture run -a a -a b -r 10 # 多账号并发
|
||||
python matrix.py nurture run -a <name> -r 10 --daemon # 保持连接不退出
|
||||
|
||||
# 配置管理
|
||||
python matrix.py config show # 查看配置
|
||||
python matrix.py config blueprint list # 查看蓝图
|
||||
|
||||
# 状态监控
|
||||
python matrix.py status all # 全局状态
|
||||
python matrix.py status browsers # 浏览器状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心机制
|
||||
|
||||
### 双引擎架构
|
||||
|
||||
| 引擎 | 适用账号 | 连接方式 | 窗口控制 | 视口控制 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| Chrome CDP | douyin_01/02 | CDP WebSocket | --window-size 参数 | UA + viewport + touchEmulation 三合一 |
|
||||
| Camoufox 持久化 | douyin_camo01 | launch_persistent_context | window.open(features) | 启动前 config 注入 DOM 属性 |
|
||||
|
||||
---
|
||||
|
||||
### Chrome 反检测方案(三合一)
|
||||
|
||||
Chrome CDP 模式下,抖音识别移动端需要**三个设置缺一不可**:
|
||||
|
||||
```python
|
||||
# 1) UA 覆盖 — 告诉抖音是安卓平板
|
||||
await cdp.send("Network.setUserAgentOverride", {"userAgent": ANDROID_TABLET_UA})
|
||||
|
||||
# 2) 视口尺寸 — 告诉抖音是小屏设备
|
||||
await page.set_viewport_size({"width": 702, "height": 783})
|
||||
|
||||
# 3) 触摸模拟 — 告诉抖音是触摸设备(关键!缺这个就是桌面版)
|
||||
await cdp.send("Emulation.setTouchEmulationEnabled", {
|
||||
"enabled": True, "maxTouchPoints": 5, "configuration": "mobile"
|
||||
})
|
||||
```
|
||||
|
||||
⚠️ 注意:`Emulation.setDeviceMetricsOverride` 在 **Chrome 148** 上 `width/height` 参数失效,不要使用。
|
||||
|
||||
### Camoufox 窗口/视口控制方案(v4.1 修复)
|
||||
|
||||
**问题**:Playwright 的 Firefox `launch_persistent_context` 模式完全接管了窗口管理和视口设置,
|
||||
`--width/--height` 参数、`window.resizeTo`、`page.set_viewport_size` 均无效。
|
||||
|
||||
**解决方案(双层控制)**:
|
||||
|
||||
1. **物理窗口尺寸** — 启动后用 `window.open(url, name, 'width=702,height=783')` 创建新窗口。
|
||||
Firefox 的 `window.open` 特性参数字段是原生支持的,Playwright 无法覆盖。
|
||||
|
||||
2. **页面视口/屏幕尺寸** — 通过 Camoufox 的 `config` 参数在启动前注入 DOM 属性覆盖:
|
||||
```python
|
||||
config={
|
||||
"window.innerWidth": 702,
|
||||
"window.innerHeight": 783,
|
||||
"window.outerWidth": 702,
|
||||
"window.outerHeight": 783,
|
||||
"screen.width": 702,
|
||||
"screen.height": 783,
|
||||
"screen.availWidth": 702,
|
||||
"screen.availHeight": 783,
|
||||
}
|
||||
```
|
||||
Camoufox 的扩展会在每个页面加载前读取 config 并覆盖 DOM 属性,
|
||||
效果等同 Playwright 的 `set_viewport_size` 但不受其限制。
|
||||
|
||||
3. **窗口尺寸配置** — 写入 `identities/{name}/config.yaml`:
|
||||
```yaml
|
||||
window: [702, 783]
|
||||
```
|
||||
|
||||
### 行为模拟参数化
|
||||
|
||||
行为参数可在 `identities/{name}/config.yaml` 中覆盖默认值:
|
||||
|
||||
```yaml
|
||||
behavior:
|
||||
base_delay: 1.5 # 操作间基础间隔(秒)
|
||||
delay_variance: 0.8 # 随机波动范围
|
||||
attention:
|
||||
distraction_chance: 0.05 # "分心"概率
|
||||
watch_duration: [4, 12] # 观看视频时长(秒)
|
||||
round_break:
|
||||
min_break: 5 # 轮间最短休息(秒)
|
||||
max_break: 20 # 轮间最长休息(秒)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口预留
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `account/sms/base.py` | 抽象基类 | 短信验证码处理(默认手动输入) |
|
||||
| `account/captcha/base.py` | 抽象基类 | 图形验证码处理(默认手动处理) |
|
||||
| `ops/xiaohongshu/` | 目录预留 | 小红书操作集 |
|
||||
| `ops/kuaishou/` | 目录预留 | 快手操作集 |
|
||||
| `ops/bilibili/` | 目录预留 | B站操作集 |
|
||||
|
||||
---
|
||||
|
||||
## 环境依赖
|
||||
|
||||
| 依赖 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Python | 3.10+ | 使用 agent-os venv |
|
||||
| Google Chrome | 最新版 | CDP 直连 |
|
||||
| Camoufox | ≥0.4.11 | Firefox 内核,反检测 |
|
||||
| playwright | ≥1.40 | 浏览器自动化 |
|
||||
| browserforge | 最新 | BrowserForge 指纹生成 |
|
||||
| camoufox | ≥0.4 | Firefox 反检测浏览器 |
|
||||
|
||||
完整依赖见 `requirements.txt`,安装:
|
||||
```bash
|
||||
pip install -r ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/requirements.txt
|
||||
```
|
||||
|
||||
## 多机同步说明 (v4.0)
|
||||
|
||||
本工具不再使用软链接,改为 `local.yaml` + `local_paths.py` 方案:
|
||||
|
||||
- `local.yaml` — 记录本机数据目录路径,每台机器独立生成
|
||||
- `local.yaml` 不参与坚果云同步(请加入排除列表)
|
||||
- 所有 Python 脚本通过 `scripts/local_paths.py` 读取路径
|
||||
- 任何脚本启动时若 `local.yaml` 不存在,会报错并提示运行 `install.sh`
|
||||
|
||||
`local.yaml` 格式(自动生成,无需手动编辑):
|
||||
```yaml
|
||||
matrix:
|
||||
local_data_root: /Users/xxx/workbuddy-agent-os/agent-local/tools/matrix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前开发状态
|
||||
|
||||
| 阶段 | 内容 | 状态 |
|
||||
|------|------|------|
|
||||
| A | Chrome CDP + 抖音原子操作(18个)+ 蓝图引擎 | ✅ 已完成 |
|
||||
| B | Camoufox 集成 + 多浏览器内核 | 🔄 进行中 |
|
||||
| C | 鼠标轨迹仿真 + 语料库完善 | 📋 规划中 |
|
||||
| D | 小红书/知乎完整支持 | 📋 规划中 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `local.yaml` 每台机器独立配置,**不纳入坚果云同步**(加入排除列表)
|
||||
- `profiles/` 目录含 Chrome 用户数据(~100MB),存于 `agent-local/` 本地
|
||||
- `config/accounts.yaml` 含账号标识信息,存于 `agent-local/` 本地
|
||||
- 换机后 profiles 需重新登录各平台账号
|
||||
- Camoufox 需单独安装(见 docs/CAMOUFOX_LOGIN_MANAGEMENT.md)
|
||||
- 多机同步只需每台机器运行一次 `install.sh`
|
||||
- 避免使用软链接指向 agent-local,统一通过 `local_paths.py` 管理路径
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# agentos — AgentOS 联邦智能体统一命令入口
|
||||
# 安装: chmod +x agentos && sudo ln -s $(pwd)/agentos /usr/local/bin/agentos
|
||||
# 或: alias agentos='$(pwd)/agentos'
|
||||
|
||||
_SRC="${BASH_SOURCE[0]}"
|
||||
while [ -L "$_SRC" ]; do _SRC="$(readlink "$_SRC")"; done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$_SRC")" && pwd)"
|
||||
|
||||
PYTHON=""
|
||||
if [ -n "$AGENTOS_PYTHON" ] && [ -x "$AGENTOS_PYTHON" ]; then
|
||||
PYTHON="$AGENTOS_PYTHON"
|
||||
elif [ -f "$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3" ]; then
|
||||
PYTHON="$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
elif command -v python3 &>/dev/null; then
|
||||
PYTHON="python3"
|
||||
else
|
||||
echo "找不到 Python 3,请设置 AGENTOS_PYTHON 环境变量" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$SCRIPT_DIR/scripts"
|
||||
exec "$PYTHON" -m agentos "$@"
|
||||
@@ -0,0 +1,701 @@
|
||||
"""
|
||||
corpus.py — 评论语料库管理
|
||||
|
||||
双平台隔离: 抖音/小红书 各自独立语料
|
||||
每个平台下分多个分类,含权重、启用状态
|
||||
|
||||
文件结构:
|
||||
corpus/
|
||||
├── douyin.yaml ← 抖音语料
|
||||
└── xiaohongshu.yaml ← 小红书语料
|
||||
|
||||
YAML 格式:
|
||||
version: "1.0"
|
||||
categories:
|
||||
赞美:
|
||||
weight: 30
|
||||
enabled: true
|
||||
label: "赞美评论"
|
||||
comments:
|
||||
- "讲得太好了,受益匪浅!"
|
||||
- "这个观点很新颖,学习了"
|
||||
搞笑:
|
||||
weight: 20
|
||||
enabled: true
|
||||
label: "搞笑评论"
|
||||
comments:
|
||||
- "笑死我了哈哈哈哈哈"
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
TOOL_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
CORPUS_DIR = TOOL_DIR / "corpus"
|
||||
CONFIG_DIR = TOOL_DIR / "config"
|
||||
|
||||
# 项目根目录(agent-sync/05_tools/.. 的上一层,即 workbuddy-agent-os)
|
||||
PROJECT_ROOT = TOOL_DIR.parent.parent.parent
|
||||
PROFILES_PATH = PROJECT_ROOT / "agent-local/tools/matrix/data/profiles.json"
|
||||
|
||||
# ── 关键词 → 方向 映射 ──────────────────────────────────────────
|
||||
KEYWORD_CATEGORY_MAP = [
|
||||
(["科技", "数码", "手机"], ["称赞", "提问"]),
|
||||
(["美食", "做饭", "菜"], ["称赞", "共鸣"]),
|
||||
(["旅游", "风景", "旅行"], ["称赞", "提问"]),
|
||||
(["情感", "生活", "感悟"], ["共鸣", "安慰"]),
|
||||
(["知识", "科普", "教育"], ["提问", "补充"]),
|
||||
]
|
||||
|
||||
# 方向名称 → 实际分类名称(兼容两个平台已有的分类)
|
||||
DIRECTION_TO_CATEGORY = {
|
||||
"称赞": "赞美",
|
||||
"正面": "赞美",
|
||||
"提问": "提问",
|
||||
"共鸣": "感慨",
|
||||
"安慰": "感慨",
|
||||
"补充": "客观",
|
||||
}
|
||||
|
||||
# 默认方向标签(用于 fallback)
|
||||
ALL_DIRECTIONS = list(DIRECTION_TO_CATEGORY.keys())
|
||||
CORPUS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 默认跨平台通用分类
|
||||
DEFAULT_DOUYIN_COMMENTS = {
|
||||
"赞美": {
|
||||
"weight": 30, "enabled": True, "label": "赞美评论",
|
||||
"comments": [
|
||||
"讲得太好了,受益匪浅!",
|
||||
"这个观点很新颖,学习了",
|
||||
"说得真是振聋发聩",
|
||||
"干货满满,感谢分享",
|
||||
"条理清晰,逻辑严密",
|
||||
"每一句话都说到了心坎里",
|
||||
],
|
||||
"templates": ["关于{keyword}的观点很到位", "感谢分享{keyword}的心得", "说得太好了,{keyword}这部分启发很大"],
|
||||
},
|
||||
"搞笑": {
|
||||
"weight": 20, "enabled": True, "label": "搞笑评论",
|
||||
"comments": ["哈哈哈哈哈笑死", "有才,这评论比视频还精彩", "笑不活了家人们", "你要把我笑死"],
|
||||
"templates": ["{keyword}这个梗笑死我了", "哈哈哈哈{keyword}太有才了"],
|
||||
},
|
||||
"客观": {
|
||||
"weight": 15, "enabled": True, "label": "客观评价",
|
||||
"comments": ["比较客观", "抛开立场看,说得有道理", "中肯的评价", "理性分析,值得一读"],
|
||||
"templates": ["关于{keyword}的分析很客观", "理性的声音,{keyword}这部分说得好"],
|
||||
},
|
||||
"提问": {
|
||||
"weight": 10, "enabled": True, "label": "提问互动",
|
||||
"comments": ["请问这是在哪里?", "能分享一下具体方法吗?", "新手想问一下怎么入门", "这个需要什么工具?"],
|
||||
"templates": ["{keyword}具体怎么操作?", "能详细讲讲{keyword}吗?"],
|
||||
},
|
||||
"感慨": {
|
||||
"weight": 10, "enabled": True, "label": "生活感慨",
|
||||
"comments": ["生活不易,且行且珍惜", "每个人都有自己的故事", "看得我热泪盈眶", "这就是生活啊"],
|
||||
"templates": ["看到{keyword}想起了自己", "{keyword}真的太真实了"],
|
||||
},
|
||||
}
|
||||
|
||||
# 小红书专属分类
|
||||
DEFAULT_XHS_COMMENTS = {
|
||||
"穿搭": {
|
||||
"weight": 25, "enabled": True, "label": "穿搭评论",
|
||||
"comments": ["这套搭配好好看!", "求链接!", "色系搭配太舒服了", "超有气质", "这个风格很适合你"],
|
||||
"templates": ["{keyword}这套太好看了", "求{keyword}的链接"],
|
||||
},
|
||||
"美妆": {
|
||||
"weight": 20, "enabled": True, "label": "美妆评论",
|
||||
"comments": ["这个妆面好干净", "求粉底色号", "口红颜色太美了", "教程很详细,收藏了"],
|
||||
"templates": ["{keyword}这个妆容好干净", "{keyword}是什么色号"],
|
||||
},
|
||||
"种草": {
|
||||
"weight": 20, "enabled": True, "label": "种草评论",
|
||||
"comments": ["种草了!", "我也买了同款,真的不错", "已加购物车", "求推荐更多好物"],
|
||||
"templates": ["被{keyword}种草了", "{keyword}已加入清单"],
|
||||
},
|
||||
"美食": {
|
||||
"weight": 15, "enabled": True, "label": "美食评论",
|
||||
"comments": ["看着好好吃", "求地址", "收藏了,周末去试试", "这个做法很简单,明天就试"],
|
||||
"templates": ["{keyword}看着好好吃", "求{keyword}的店名"],
|
||||
},
|
||||
"家居": {
|
||||
"weight": 10, "enabled": True, "label": "家居评论",
|
||||
"comments": ["布置得好温馨", "求家具链接", "这个风格好喜欢", "家的感觉真好"],
|
||||
"templates": ["{keyword}的布置好温馨", "求{keyword}的链接"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# AI 评论生成器
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class AIGenerator:
|
||||
"""AI 评论生成器(OpenAI 兼容 API)
|
||||
|
||||
使用简单的 HTTP 请求,不依赖 openai 库。
|
||||
没有配置 API key 时跳过,不影响现有功能。
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str = None, base_url: str = None, model: str = None):
|
||||
config = self._load_config()
|
||||
|
||||
raw_key = api_key or config.get("api_key", "") or os.environ.get("OPENAI_API_KEY", "")
|
||||
self.api_key = raw_key.strip() if raw_key else ""
|
||||
|
||||
self.base_url = (base_url or config.get("base_url") or "https://api.openai.com/v1").rstrip("/")
|
||||
self.model = model or config.get("model") or "gpt-4o-mini"
|
||||
self.temperature = config.get("temperature", 0.8)
|
||||
|
||||
# ── 配置加载 ────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _load_config() -> dict:
|
||||
"""从 config/ai.yaml 加载配置"""
|
||||
path = CONFIG_DIR / "ai.yaml"
|
||||
if path.exists():
|
||||
try:
|
||||
raw = yaml.safe_load(path.read_text())
|
||||
return (raw or {}).get("ai", {})
|
||||
except Exception as exc:
|
||||
log.warning(" ⚠️ ai.yaml 解析失败: %s", exc)
|
||||
return {}
|
||||
|
||||
# ── 公共 API ────────────────────────────────────────────
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""是否有 API key 可用"""
|
||||
return bool(self.api_key)
|
||||
|
||||
async def generate_comment(
|
||||
self,
|
||||
video_title: str,
|
||||
video_desc: str = "",
|
||||
direction: str = "称赞",
|
||||
persona: dict = None,
|
||||
) -> Optional[str]:
|
||||
"""根据视频标题和方向生成评论
|
||||
|
||||
Args:
|
||||
video_title: 视频标题
|
||||
video_desc: 视频描述(可选)
|
||||
direction: 评论方向(称赞/提问/共鸣/补充/搞笑)
|
||||
persona: 账号人设字典(可选)
|
||||
|
||||
Returns:
|
||||
生成的评论文本,失败返回 None
|
||||
"""
|
||||
if not self.available:
|
||||
log.debug(" ⏭️ AI 生成跳过(未配置 API key)")
|
||||
return None
|
||||
|
||||
prompt = self._build_prompt(video_title, video_desc, direction, persona)
|
||||
text = await self._call_api(prompt)
|
||||
return text
|
||||
|
||||
# ── 内部方法 ────────────────────────────────────────────
|
||||
|
||||
def _build_prompt(self, video_title: str, video_desc: str, direction: str, persona: dict = None) -> str:
|
||||
"""构建 prompt"""
|
||||
style_hint = f"(风格要求:{direction})"
|
||||
|
||||
persona_hint = ""
|
||||
if persona:
|
||||
parts = []
|
||||
if persona.get("age_group"):
|
||||
parts.append(f"年龄层: {persona['age_group']}")
|
||||
if persona.get("gender"):
|
||||
parts.append(f"性别: {persona['gender']}")
|
||||
if persona.get("interests"):
|
||||
parts.append(f"兴趣: {'、'.join(persona['interests'])}")
|
||||
if persona.get("comment_style"):
|
||||
parts.append(f"评论风格: {'、'.join(persona['comment_style'])}")
|
||||
if parts:
|
||||
persona_hint = f"\n账号人设: {'; '.join(parts)}"
|
||||
|
||||
desc_part = f"\n视频描述: {video_desc}" if video_desc else ""
|
||||
|
||||
return (
|
||||
f"你是一个短视频平台的活跃用户。请根据以下视频信息写一条 {direction} 方向的评论{style_hint}。"
|
||||
f"{persona_hint}"
|
||||
f"\n视频标题: {video_title}"
|
||||
f"{desc_part}"
|
||||
"\n\n要求:"
|
||||
"\n- 语言自然,像真实用户的评论"
|
||||
"\n- 长度 10~50 字"
|
||||
"\n- 不要用 emoji"
|
||||
"\n- 直接输出评论内容,不要加引号或前缀"
|
||||
)
|
||||
|
||||
async def _call_api(self, prompt: str) -> Optional[str]:
|
||||
"""调用 OpenAI 兼容 API 生成文本"""
|
||||
import httpx
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
resp = await client.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": 150,
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
log.warning(" ⚠️ AI API 返回 %s: %s", resp.status_code, resp.text[:200])
|
||||
return None
|
||||
data = resp.json()
|
||||
choices = data.get("choices", [])
|
||||
if not choices:
|
||||
return None
|
||||
text = choices[0].get("message", {}).get("content", "").strip()
|
||||
return text if text else None
|
||||
except Exception as exc:
|
||||
log.warning(" ⚠️ AI API 调用失败: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 语料库管理器
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class CorpusManager:
|
||||
"""语料库管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
self._personas = {} # account_id → persona dict
|
||||
self._ai_gen = None # AIGenerator 懒实例化
|
||||
self._ensure_files()
|
||||
|
||||
# ── 语料文件管理 ────────────────────────────────────────
|
||||
|
||||
def _ensure_files(self):
|
||||
"""确保语料库 YAML 文件存在"""
|
||||
for platform, data in [("douyin", DEFAULT_DOUYIN_COMMENTS), ("xiaohongshu", DEFAULT_XHS_COMMENTS)]:
|
||||
path = CORPUS_DIR / f"{platform}.yaml"
|
||||
if not path.exists():
|
||||
content = {"version": "1.0", "categories": data}
|
||||
path.write_text(yaml.dump(content, default_flow_style=False, allow_unicode=True, sort_keys=False))
|
||||
log.info(f" 📝 创建语料文件: {path.name} ({sum(1 for c in data.values() for _ in c.get('comments',[]))} 条)")
|
||||
|
||||
def _load(self, platform: str) -> dict:
|
||||
"""加载指定平台的语料"""
|
||||
if platform not in self._cache:
|
||||
path = CORPUS_DIR / f"{platform}.yaml"
|
||||
if path.exists():
|
||||
self._cache[platform] = yaml.safe_load(path.read_text()) or {}
|
||||
else:
|
||||
self._cache[platform] = {"version": "1.0", "categories": {}}
|
||||
return self._cache[platform]
|
||||
|
||||
# ── 人设管理 ──────────────────────────────────────────
|
||||
|
||||
def load_personas(self) -> dict:
|
||||
"""从 profiles.json 加载所有人设,返回 account_id → persona dict"""
|
||||
log.info(f" 📂 加载人设数据: {PROFILES_PATH}")
|
||||
try:
|
||||
if PROFILES_PATH.exists():
|
||||
raw = json.loads(PROFILES_PATH.read_text())
|
||||
self._personas = {}
|
||||
for account_id, profile in raw.items():
|
||||
persona = profile.get("persona")
|
||||
if persona:
|
||||
self._personas[account_id] = persona
|
||||
log.info(f" ✅ 加载 {len(self._personas)} 个人设")
|
||||
else:
|
||||
log.warning(" ⚠️ 人设文件不存在: %s", PROFILES_PATH)
|
||||
self._personas = {}
|
||||
except Exception as exc:
|
||||
log.warning(" ⚠️ 加载人设失败: %s", exc)
|
||||
self._personas = {}
|
||||
return self._personas
|
||||
|
||||
def get_persona(self, account_id: str) -> Optional[dict]:
|
||||
"""获取指定账号的人设
|
||||
|
||||
Args:
|
||||
account_id: 账号 ID
|
||||
|
||||
Returns:
|
||||
人设字典,或 None
|
||||
"""
|
||||
if not self._personas:
|
||||
self.load_personas()
|
||||
persona = self._personas.get(account_id)
|
||||
if persona is None:
|
||||
log.debug(f" ℹ️ 账号 %s 无人设", account_id)
|
||||
return persona
|
||||
|
||||
def get_search_keywords(self, account_id: str) -> list:
|
||||
"""获取账号人设中的搜索关键词
|
||||
|
||||
Args:
|
||||
account_id: 账号 ID
|
||||
|
||||
Returns:
|
||||
关键词列表(无人设时返回空列表)
|
||||
"""
|
||||
persona = self.get_persona(account_id)
|
||||
if persona:
|
||||
return persona.get("search_keywords", [])
|
||||
return []
|
||||
|
||||
# ── v2 格式支持 ────────────────────────────────────────
|
||||
|
||||
def list_personas(self) -> list:
|
||||
"""列出所有身份(v2格式)"""
|
||||
result = []
|
||||
for platform in ["douyin", "xiaohongshu"]:
|
||||
data = self._load(platform)
|
||||
for pid, info in data.get("personas", {}).items():
|
||||
result.append({
|
||||
"platform": platform,
|
||||
"id": pid,
|
||||
"name": info.get("name", pid),
|
||||
"tags": info.get("tags", []),
|
||||
"style": info.get("style", ""),
|
||||
})
|
||||
return result
|
||||
|
||||
def list_scenes(self, persona_id: str = None) -> list:
|
||||
"""列出所有场景(v2格式)"""
|
||||
scenes_set = {}
|
||||
for platform in ["douyin", "xiaohongshu"]:
|
||||
data = self._load(platform)
|
||||
for pid, pinfo in data.get("personas", {}).items():
|
||||
if persona_id and pid != persona_id:
|
||||
continue
|
||||
for scene_id, sinfo in data.get("scenes", {}).items():
|
||||
key = f"{pid}.{scene_id}"
|
||||
if key not in scenes_set:
|
||||
scenes_set[key] = {
|
||||
"persona": pid,
|
||||
"id": scene_id,
|
||||
"label": sinfo.get("label", scene_id),
|
||||
"rounds": sinfo.get("rounds", 1),
|
||||
}
|
||||
return list(scenes_set.values())
|
||||
|
||||
def get_comment_for_scene(self, persona: str, scene: str,
|
||||
keyword: str = "", round_num: int = 1) -> Optional[str]:
|
||||
"""根据身份和场景获取评论(v2格式)
|
||||
|
||||
Args:
|
||||
persona: 身份ID(如 health_lover)
|
||||
scene: 场景ID(如 first_comment)
|
||||
keyword: 关键词
|
||||
round_num: 多轮对话第几轮
|
||||
|
||||
Returns:
|
||||
评论文本,或 None
|
||||
"""
|
||||
for platform in ["douyin", "xiaohongshu"]:
|
||||
data = self._load(platform)
|
||||
content = data.get("content", {})
|
||||
# 尝试精确匹配 persona.scene
|
||||
key = f"{persona}.{scene}"
|
||||
if key in content:
|
||||
items = content[key]
|
||||
if isinstance(items, list):
|
||||
return random.choice(items).replace("{keyword}", keyword) if items else None
|
||||
# 多轮格式
|
||||
round_key = f"round_{round_num}"
|
||||
if isinstance(items, dict) and round_key in items:
|
||||
round_items = items[round_key]
|
||||
if round_items:
|
||||
return random.choice(round_items).replace("{keyword}", keyword)
|
||||
# 尝试匹配 scene 但不限定 persona
|
||||
for ck, cv in content.items():
|
||||
if ck.endswith(f".{scene}"):
|
||||
if isinstance(cv, list) and cv:
|
||||
return random.choice(cv).replace("{keyword}", keyword)
|
||||
return None
|
||||
|
||||
# ── 语料查询 ──────────────────────────────────────────
|
||||
|
||||
def list_categories(self) -> list:
|
||||
"""列出所有平台的分类"""
|
||||
result = []
|
||||
for platform in ["douyin", "xiaohongshu"]:
|
||||
data = self._load(platform)
|
||||
for name, info in data.get("categories", {}).items():
|
||||
count = len(info.get("comments", [])) + len(info.get("templates", []))
|
||||
result.append({
|
||||
"platform": platform,
|
||||
"name": name,
|
||||
"weight": info.get("weight", 0),
|
||||
"enabled": info.get("enabled", True),
|
||||
"count": count,
|
||||
"label": info.get("label", name),
|
||||
})
|
||||
return result
|
||||
|
||||
def get_comments(self, categories: List[str] = None, platform: str = None, count: int = 20) -> List[str]:
|
||||
"""获取评论列表
|
||||
|
||||
Args:
|
||||
categories: 指定分类(None = 所有启用分类)
|
||||
platform: 指定平台(None = 所有平台)
|
||||
count: 最多返回数量
|
||||
|
||||
Returns:
|
||||
评论文本列表
|
||||
"""
|
||||
results = []
|
||||
platforms = [platform] if platform else ["douyin", "xiaohongshu"]
|
||||
|
||||
for p in platforms:
|
||||
data = self._load(p)
|
||||
for name, info in data.get("categories", {}).items():
|
||||
# 筛选分类
|
||||
if categories and name not in categories:
|
||||
continue
|
||||
if not info.get("enabled", True):
|
||||
continue
|
||||
if info.get("weight", 0) <= 0:
|
||||
continue
|
||||
|
||||
# 取评论
|
||||
comments = info.get("comments", [])
|
||||
templates = info.get("templates", [])
|
||||
combined = comments + templates
|
||||
# 按权重比例取
|
||||
take = max(1, int(count * info.get("weight", 10) / 100))
|
||||
results.extend(random.sample(combined, min(take, len(combined))))
|
||||
|
||||
random.shuffle(results)
|
||||
return results[:count]
|
||||
|
||||
def add_comment(self, category: str, text: str, platform: str = "douyin"):
|
||||
"""添加一条评论到指定平台指定分类"""
|
||||
data = self._load(platform)
|
||||
cats = data.get("categories", {})
|
||||
if category not in cats:
|
||||
cats[category] = {"weight": 10, "enabled": True, "label": category, "comments": [], "templates": []}
|
||||
if "comments" not in cats[category]:
|
||||
cats[category]["comments"] = []
|
||||
cats[category]["comments"].append(text)
|
||||
data["categories"] = cats
|
||||
|
||||
path = CORPUS_DIR / f"{platform}.yaml"
|
||||
path.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False))
|
||||
self._cache.pop(platform, None) # 清除缓存
|
||||
log.info(f" ✅ 已添加评论到 [{platform}/{category}]: {text}")
|
||||
|
||||
def delete_comment(self, category: str, index: int, platform: str = "douyin"):
|
||||
"""删除指定索引的评论"""
|
||||
data = self._load(platform)
|
||||
cats = data.get("categories", {}).get(category, {})
|
||||
comments = cats.get("comments", [])
|
||||
if 0 <= index < len(comments):
|
||||
removed = comments.pop(index)
|
||||
cats["comments"] = comments
|
||||
path = CORPUS_DIR / f"{platform}.yaml"
|
||||
path.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False))
|
||||
self._cache.pop(platform, None)
|
||||
log.info(f" 🗑 已删除评论 [{platform}/{category}]: {removed}")
|
||||
|
||||
def update_category(self, platform: str, category: str, **kwargs):
|
||||
"""更新分类设置(weight/enabled/label 等)"""
|
||||
data = self._load(platform)
|
||||
cats = data.get("categories", {})
|
||||
if category not in cats:
|
||||
cats[category] = {"weight": 10, "enabled": True, "label": category, "comments": [], "templates": []}
|
||||
for k, v in kwargs.items():
|
||||
if k in ("weight", "enabled", "label"):
|
||||
cats[category][k] = v
|
||||
data["categories"] = cats
|
||||
path = CORPUS_DIR / f"{platform}.yaml"
|
||||
path.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False))
|
||||
self._cache.pop(platform, None)
|
||||
log.info(f" ✅ 已更新分类 [{platform}/{category}]: {kwargs}")
|
||||
|
||||
# ── 账号上下文 ──────────────────────────────────────────
|
||||
|
||||
def set_account_id(self, account_id: str):
|
||||
"""设置当前账号 ID,方便后续跟踪上下文"""
|
||||
self._account_id = account_id
|
||||
log.info(f" 👤 设置账号: {account_id}")
|
||||
|
||||
# ── AI 生成器 ──────────────────────────────────────────
|
||||
|
||||
def _get_ai_generator(self) -> AIGenerator:
|
||||
"""懒获取 AI 生成器实例"""
|
||||
if self._ai_gen is None:
|
||||
self._ai_gen = AIGenerator()
|
||||
return self._ai_gen
|
||||
|
||||
# ── 视频标题匹配 → 评论 ──────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _extract_first_keyword(video_title: str) -> Optional[str]:
|
||||
"""从视频标题中提取第一个匹配的关键词"""
|
||||
for keywords, _ in KEYWORD_CATEGORY_MAP:
|
||||
for kw in keywords:
|
||||
if kw in video_title:
|
||||
return kw
|
||||
# fallback: 取标题第一个有意义的词
|
||||
words = re.findall(r'[\w\u4e00-\u9fff]{2,}', video_title)
|
||||
return words[0] if words else ""
|
||||
|
||||
@staticmethod
|
||||
def _match_keywords(video_title: str) -> list:
|
||||
"""根据视频标题匹配关键词,返回匹配到的方向列表"""
|
||||
matched_directions = []
|
||||
for keywords, directions in KEYWORD_CATEGORY_MAP:
|
||||
for kw in keywords:
|
||||
if kw in video_title:
|
||||
matched_directions.extend(directions)
|
||||
break # 一个关键词组只匹配一次
|
||||
return matched_directions
|
||||
|
||||
def _get_comment_from_categories(self, category_names: list, platform: str = None,
|
||||
allow_templates: bool = True) -> Optional[str]:
|
||||
"""从指定的分类名列表中随机取一条评论(遍历所有平台)
|
||||
Args:
|
||||
allow_templates: 是否允许使用含 {keyword} 的模板
|
||||
"""
|
||||
platforms = [platform] if platform else ["douyin", "xiaohongshu"]
|
||||
candidates = []
|
||||
|
||||
for p in platforms:
|
||||
data = self._load(p)
|
||||
for cat_name, info in data.get("categories", {}).items():
|
||||
if cat_name not in category_names:
|
||||
continue
|
||||
if not info.get("enabled", True):
|
||||
continue
|
||||
if allow_templates:
|
||||
candidates.extend(info.get("comments", []) + info.get("templates", []))
|
||||
else:
|
||||
candidates.extend(info.get("comments", []))
|
||||
|
||||
if candidates:
|
||||
return random.choice(candidates)
|
||||
return None
|
||||
|
||||
def _get_random_comment(self, platform: str = None) -> Optional[str]:
|
||||
"""从所有启用分类中随机取一条评论"""
|
||||
platforms = [platform] if platform else ["douyin", "xiaohongshu"]
|
||||
all_comments = []
|
||||
|
||||
for p in platforms:
|
||||
data = self._load(p)
|
||||
for name, info in data.get("categories", {}).items():
|
||||
if not info.get("enabled", True):
|
||||
continue
|
||||
if info.get("weight", 0) <= 0:
|
||||
continue
|
||||
comments = info.get("comments", []) # 随机兜底不包含 {keyword} 模板
|
||||
all_comments.extend(comments)
|
||||
|
||||
if all_comments:
|
||||
return random.choice(all_comments)
|
||||
return None
|
||||
|
||||
def get_comment_for_video(
|
||||
self,
|
||||
video_title: str,
|
||||
direction: str = None,
|
||||
account_id: str = None,
|
||||
) -> Optional[str]:
|
||||
"""根据视频标题(和可选方向/账号人设)从语料库中匹配一条评论
|
||||
|
||||
Args:
|
||||
video_title: 视频标题文本
|
||||
direction: 可选,指定方向,如 "称赞"/"提问"/"共鸣"/"补充"
|
||||
account_id: 可选,账号 ID,用于根据人设的 comment_style 优先选择方向
|
||||
|
||||
Returns:
|
||||
匹配到的评论文本,或 None
|
||||
"""
|
||||
# 0) 有人设 → 优先用人设的 comment_style 作为方向
|
||||
persona = None
|
||||
if account_id:
|
||||
persona = self.get_persona(account_id)
|
||||
if persona and not direction:
|
||||
# 如果未指定 direction,用人设的第一个 comment_style
|
||||
styles = persona.get("comment_style", [])
|
||||
if styles:
|
||||
# 先用 comment_style 去匹配,最后 fallback 到随机
|
||||
pass # 后面有具体逻辑
|
||||
|
||||
# 1) 从标题匹配关键词,得到方向列表
|
||||
matched_directions = self._match_keywords(video_title)
|
||||
|
||||
# 2) 如果用户指定了 direction,优先使用
|
||||
if direction and direction in DIRECTION_TO_CATEGORY:
|
||||
if direction in matched_directions:
|
||||
matched_directions.remove(direction)
|
||||
matched_directions = [direction] + matched_directions
|
||||
elif direction:
|
||||
pass
|
||||
|
||||
# 2.5) 如果有人设且未指定 direction,把人设的 comment_style 优先排列
|
||||
if persona and not direction:
|
||||
styles = persona.get("comment_style", [])
|
||||
# 把人设风格中在 matched_directions 里的提到最前面
|
||||
for style in reversed(styles):
|
||||
if style in matched_directions:
|
||||
matched_directions.remove(style)
|
||||
matched_directions = [style] + matched_directions
|
||||
elif style in DIRECTION_TO_CATEGORY:
|
||||
# 人设风格虽未匹配到关键词,也尝试作为候选方向
|
||||
matched_directions = [style] + matched_directions
|
||||
|
||||
# 3) 有匹配的方向 → 尝试依次取对应分类的评论
|
||||
if matched_directions:
|
||||
for d in matched_directions:
|
||||
cat = DIRECTION_TO_CATEGORY.get(d)
|
||||
if not cat:
|
||||
continue
|
||||
kw = self._extract_first_keyword(video_title)
|
||||
# 有关键词 → 可用模板(含 {keyword} 替换)
|
||||
if kw:
|
||||
comment = self._get_comment_from_categories([cat], allow_templates=True)
|
||||
if comment:
|
||||
return comment.replace("{keyword}", kw)
|
||||
# 无关键词 → 只用成品评论(不含模板)
|
||||
comment = self._get_comment_from_categories([cat], allow_templates=False)
|
||||
if comment:
|
||||
return comment
|
||||
|
||||
# 4) 没有语料匹配 → fallback: 尝试 AI 生成
|
||||
ai = self._get_ai_generator()
|
||||
if ai.available:
|
||||
try:
|
||||
# 同步上下文执行异步 AI 调用
|
||||
comment = asyncio.run(
|
||||
ai.generate_comment(
|
||||
video_title=video_title,
|
||||
direction=direction or (persona.get("comment_style", [""])[0] if persona else "称赞"),
|
||||
persona=persona,
|
||||
)
|
||||
)
|
||||
if comment:
|
||||
log.info(" 🤖 AI 生成评论: %s", comment[:40])
|
||||
return comment
|
||||
except Exception as exc:
|
||||
log.warning(" ⚠️ AI 生成失败: %s", exc)
|
||||
|
||||
# 5) 最后兜底 → 随机返回一条
|
||||
comment = self._get_random_comment()
|
||||
if comment and "{keyword}" in comment:
|
||||
kw = self._extract_first_keyword(video_title)
|
||||
if kw:
|
||||
comment = comment.replace("{keyword}", kw)
|
||||
return comment
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "daily_comment",
|
||||
"name": "日常-评论激活",
|
||||
"description": "日常评论:打开视频 → 观看 → 评论 → 关闭。每天2-3次保持账号活跃,弹验证自动处理",
|
||||
"version": "1.1",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "scroll_feed", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_video", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "like", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 6, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 7, "op": "post_comment", "args": {"text": "@corpus"}, "pre": {}, "post": {}},
|
||||
{"step_id": 8, "op": "auto_verify", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 9, "op": "close_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 10, "op": "scroll_feed", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 11, "op": "open_video", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 13, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 14, "op": "post_comment", "args": {"text": "@corpus"}, "pre": {}, "post": {}},
|
||||
{"step_id": 15, "op": "auto_verify", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 16, "op": "close_comments", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"daily_limit": 3,
|
||||
"min_interval_minutes": 30,
|
||||
"args_schema": {
|
||||
"corpus": {"type": "string", "desc": "语料分类"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "douyin_comment",
|
||||
"name": "抖音-定向评论",
|
||||
"description": "给视频链接 → 打开 → 观看 → 评论",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_url",
|
||||
"args": {
|
||||
"url": "@url"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "open_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "post_comment",
|
||||
"args": {
|
||||
"text": "@comment_text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "close_comments",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "douyin_reply",
|
||||
"name": "抖音-作者回复",
|
||||
"description": "打开自己视频 → 读评论 → 回复评论",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "goto_profile",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "read_my_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "reply_comment",
|
||||
"args": {
|
||||
"text": "@reply_text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "go_back",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
"""
|
||||
engine.py — mc run 执行引擎(v5.3 — 身份共享 + 多平台同浏览器)
|
||||
|
||||
核心逻辑:
|
||||
1. 按 identity_dir 分组账号 → 同身份共用一个浏览器
|
||||
2. 每个浏览器内先做完抖音,再做小红书
|
||||
3. 不同身份组之间错峰启动(防假死)
|
||||
4. 每个账号在其平台上顺序执行蓝图
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
||||
TOOL_DIR = SCRIPTS_DIR.parent
|
||||
from matrix_mgmt import AGENT_SYNC, AGENT_LOCAL
|
||||
|
||||
IDENTITIES_ROOT = AGENT_LOCAL / "tools" / "matrix" / "identities"
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 工具函数
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
def resolve_account(account_id: str) -> dict:
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if a["id"] == account_id:
|
||||
return a
|
||||
raise ValueError(f"账号不存在: {account_id}")
|
||||
|
||||
|
||||
def resolve_blueprint(name: str) -> Optional[dict]:
|
||||
bp_dir = TOOL_DIR / "blueprints"
|
||||
for f in sorted(bp_dir.glob("*.json")):
|
||||
bp = json.loads(f.read_text())
|
||||
if name in (bp.get("id", ""), bp.get("name", ""), f.stem):
|
||||
return bp
|
||||
if name in f.stem or f.stem in name:
|
||||
return json.loads(f.read_text())
|
||||
return None
|
||||
|
||||
|
||||
def check_cookie(identity_hint: str) -> str:
|
||||
"""检查身份目录 cookie 状态 → 'ok'|'no_cookie'|'expired'|'no_identity'"""
|
||||
if not identity_hint:
|
||||
return "no_identity"
|
||||
ident_dir = IDENTITIES_ROOT / identity_hint
|
||||
if not ident_dir.exists():
|
||||
return "no_identity"
|
||||
ck = ident_dir / "user_data" / "cookies.sqlite"
|
||||
if not ck.exists() or ck.stat().st_size < 100:
|
||||
return "no_cookie"
|
||||
try:
|
||||
import sqlite3
|
||||
conn_ck = sqlite3.connect(str(ck), timeout=2)
|
||||
cnt = conn_ck.execute("SELECT count(*) FROM moz_cookies WHERE name LIKE '%session%'").fetchone()[0]
|
||||
conn_ck.close()
|
||||
return "ok" if cnt > 0 else "expired"
|
||||
except:
|
||||
return "error"
|
||||
|
||||
|
||||
def group_accounts_by_identity(account_ids: List[str]) -> dict:
|
||||
"""按 identity_dir 分组账号,同目录的放到一个组里"""
|
||||
groups = {}
|
||||
for aid in account_ids:
|
||||
try:
|
||||
info = resolve_account(aid)
|
||||
except ValueError:
|
||||
continue
|
||||
ident = info.get("identity_dir", aid).replace("identities/", "")
|
||||
groups.setdefault(ident, []).append(info)
|
||||
return groups
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 执行报告
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
class StepResult:
|
||||
def __init__(self, op: str, step_id: int, success: bool = True,
|
||||
duration: float = 0, error: str = ""):
|
||||
self.op = op; self.step_id = step_id; self.success = success
|
||||
self.duration = duration; self.error = error
|
||||
|
||||
def __str__(self):
|
||||
icon = "✅" if self.success else "❌"
|
||||
return f"{icon} [{self.step_id:2d}] {self.op:20s} ({self.duration:.1f}s) {self.error}"
|
||||
|
||||
|
||||
class AccountRunReport:
|
||||
def __init__(self, account: str, blueprint: str, round_idx: int):
|
||||
self.account = account; self.blueprint = blueprint; self.round = round_idx
|
||||
self.steps: List[StepResult] = []; self.start = time.time()
|
||||
self.success = 0; self.failed = 0; self.skipped = False
|
||||
|
||||
@property
|
||||
def duration(self) -> float: return time.time() - self.start
|
||||
|
||||
def add_step(self, result: StepResult):
|
||||
self.steps.append(result)
|
||||
if result.success: self.success += 1
|
||||
else: self.failed += 1
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"account": self.account, "blueprint": self.blueprint,
|
||||
"round": self.round, "success": self.success, "failed": self.failed,
|
||||
"skipped": self.skipped, "duration": round(self.duration, 1),
|
||||
"steps": [{"op": s.op, "step_id": s.step_id, "success": s.success,
|
||||
"duration": round(s.duration, 1), "error": s.error} for s in self.steps]}
|
||||
|
||||
|
||||
class BatchReport:
|
||||
def __init__(self):
|
||||
self.accounts: List[str] = []; self.blueprints: List[str] = []
|
||||
self.rounds_total = 0; self.mix_mode = False
|
||||
self.start = time.time(); self.account_reports: List[AccountRunReport] = []
|
||||
self.error: Optional[str] = None
|
||||
|
||||
@property
|
||||
def duration(self) -> float: return time.time() - self.start
|
||||
@property
|
||||
def success(self) -> int: return sum(r.success for r in self.account_reports)
|
||||
@property
|
||||
def failed(self) -> int: return sum(r.failed for r in self.account_reports)
|
||||
@property
|
||||
def skipped(self) -> int: return sum(1 for r in self.account_reports if r.skipped)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"accounts": self.accounts, "blueprints": self.blueprints,
|
||||
"rounds": self.rounds_total, "mix_mode": self.mix_mode,
|
||||
"duration": round(self.duration, 1),
|
||||
"total_steps": self.success + self.failed,
|
||||
"success": self.success, "failed": self.failed,
|
||||
"skipped": self.skipped,
|
||||
"account_reports": [r.to_dict() for r in self.account_reports],
|
||||
"error": self.error}
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 核心引擎
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
# 平台→默认蓝图映射(当不传蓝图时自动匹配)
|
||||
PLATFORM_BLUEPRINTS = {
|
||||
"douyin": "douyin_read_profile",
|
||||
"xiaohongshu": "xiaohongshu_read_profile",
|
||||
}
|
||||
PLATFORM_DAILY_BLUEPRINTS = {
|
||||
"douyin": "douyin_daily",
|
||||
"xiaohongshu": "xhs_daily",
|
||||
}
|
||||
|
||||
|
||||
class BatchEngine:
|
||||
def __init__(self, accounts: List[str], blueprints: List[str] = None,
|
||||
rounds: int = 10, mix: bool = False,
|
||||
corpus: List[str] = None,
|
||||
stagger: str = "15-30",
|
||||
keep_open: bool = False,
|
||||
max_browsers: int = 3,
|
||||
run_id: str = "",
|
||||
task_params: dict = None):
|
||||
self.accounts = accounts
|
||||
self.blueprints = blueprints or []
|
||||
self.rounds_total = rounds
|
||||
self.mix = mix
|
||||
self.corpus = corpus or []
|
||||
self.stagger = stagger
|
||||
self.keep_open = keep_open
|
||||
self.max_browsers = max_browsers
|
||||
self.run_id = run_id
|
||||
self.task_params = task_params or {}
|
||||
|
||||
def _mark_banned(self, account_id: str, platform: str):
|
||||
"""将账号标记为封号状态,写入 profiles.json"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
home = Path.home()
|
||||
profiles_path = home / "workbuddy-agent-os" / "agent-local" / "tools" / "matrix" / "data" / "profiles.json"
|
||||
if profiles_path.exists():
|
||||
import json
|
||||
all_p = json.loads(profiles_path.read_text())
|
||||
else:
|
||||
all_p = {}
|
||||
all_p[account_id] = {
|
||||
"nickname": all_p.get(account_id, {}).get("nickname", "?"),
|
||||
"fans": "0", "following": "0", "likes": "0", "posts": "0", "bio": "",
|
||||
"status": "banned",
|
||||
"platform": platform,
|
||||
"updated": __import__("time").strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
profiles_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
profiles_path.write_text(json.dumps(all_p, ensure_ascii=False, indent=2))
|
||||
log.warning(f" 🚫 [{account_id}] 已写入封号标记到 profiles.json")
|
||||
except Exception as e:
|
||||
log.warning(f" ❌ [{account_id}] 写入封号标记失败: {e}")
|
||||
|
||||
def _pick_blueprint(self, round_idx: int) -> str:
|
||||
if not self.blueprints:
|
||||
return "" # 由 _run_acct_on_conn 按平台自动匹配
|
||||
if self.mix:
|
||||
return random.choice(self.blueprints)
|
||||
return self.blueprints[(round_idx - 1) % len(self.blueprints)]
|
||||
|
||||
def _resolve_args(self, sargs: dict, account_id: str, platform: str) -> dict:
|
||||
"""解析蓝图步骤中的占位符,替换为实际内容"""
|
||||
import copy
|
||||
resolved = copy.deepcopy(sargs)
|
||||
for k, v in resolved.items():
|
||||
if not isinstance(v, str):
|
||||
continue
|
||||
if "@corpus" in v:
|
||||
from mc.corpus import CorpusManager
|
||||
cm = CorpusManager()
|
||||
direction = resolved.get("direction", "") or self.task_params.get("direction", "")
|
||||
text = cm.get_comment_for_video(
|
||||
video_title=self.task_params.get("keyword", ""),
|
||||
direction=direction,
|
||||
account_id=account_id,
|
||||
)
|
||||
resolved[k] = v.replace("@corpus", text)
|
||||
if "@keyword" in v:
|
||||
kw = self.task_params.get("keyword", "")
|
||||
resolved[k] = v.replace("@keyword", kw)
|
||||
if "@direction" in v:
|
||||
dr = self.task_params.get("direction", "")
|
||||
resolved[k] = v.replace("@direction", dr)
|
||||
if "@url" in v:
|
||||
url = self.task_params.get("url", "")
|
||||
resolved[k] = v.replace("@url", url)
|
||||
if "@comment_text" in v:
|
||||
ct = self.task_params.get("comment_text", "")
|
||||
if ct:
|
||||
resolved[k] = v.replace("@comment_text", ct)
|
||||
if "@reply_text" in v:
|
||||
rt = self.task_params.get("reply_text", "")
|
||||
if rt:
|
||||
resolved[k] = v.replace("@reply_text", rt)
|
||||
if "@my_code" in v:
|
||||
mc = self.task_params.get("my_code", "")
|
||||
if mc:
|
||||
resolved[k] = v.replace("@my_code", mc)
|
||||
if "@target_code" in v:
|
||||
tc = self.task_params.get("target_code", "")
|
||||
if tc:
|
||||
resolved[k] = v.replace("@target_code", tc)
|
||||
if "@scene" in v:
|
||||
scene = self.task_params.get("scene", "first_comment")
|
||||
from mc.corpus import CorpusManager
|
||||
cm = CorpusManager()
|
||||
text = cm.get_comment_for_scene(
|
||||
persona=self.task_params.get("persona", ""),
|
||||
scene=scene,
|
||||
keyword=self.task_params.get("keyword", ""),
|
||||
round_num=self.task_params.get("round_num", 1),
|
||||
)
|
||||
if text:
|
||||
resolved[k] = v.replace("@scene", text)
|
||||
return resolved
|
||||
|
||||
async def _run_acct_on_conn(self, account_info: dict, blueprint_name: str,
|
||||
round_idx: int, conn) -> AccountRunReport:
|
||||
"""在已有浏览器连接上执行单个账号的单个轮次"""
|
||||
account_id = account_info["id"]
|
||||
platform = account_info.get("platform", "douyin")
|
||||
|
||||
# 没传蓝图时按平台自动匹配(支持不同平台账号同一次批量执行)
|
||||
if not blueprint_name:
|
||||
blueprint_name = PLATFORM_BLUEPRINTS.get(platform, "douyin_read_profile")
|
||||
report = AccountRunReport(account_id, blueprint_name, round_idx)
|
||||
|
||||
bp = resolve_blueprint(blueprint_name)
|
||||
if not bp:
|
||||
report.skipped = True
|
||||
return report
|
||||
steps = bp.get("steps", [])
|
||||
log.info(f" 📱 {account_id} ({platform}) → {blueprint_name} ({len(steps)}步)")
|
||||
|
||||
# 导航到该平台的首页
|
||||
home_url = "https://www.xiaohongshu.com/explore" if platform == "xiaohongshu" else "https://www.douyin.com/"
|
||||
try:
|
||||
await conn.page.goto(home_url, timeout=30000, wait_until="domcontentloaded")
|
||||
await asyncio.sleep(5)
|
||||
except:
|
||||
await conn.page.goto(home_url, timeout=60000, wait_until="load")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# ── 钩子1: 登录状态检测(执行前确保登录)──
|
||||
from matrix_modules.account.login_state_machine import LoginStateMachine
|
||||
lsm = LoginStateMachine()
|
||||
login_ok = await lsm.ensure_login(conn.page, account_id, platform)
|
||||
if not login_ok:
|
||||
# 检测是否被封号
|
||||
if hasattr(lsm, 'last_status') and lsm.last_status == 'banned':
|
||||
log.warning(f" 🚫 [{account_id}] 账号被封禁,写入标记")
|
||||
self._mark_banned(account_id, platform)
|
||||
else:
|
||||
log.warning(f" ❌ [{account_id}] 登录检测不通过,跳过本轮")
|
||||
report.skipped = True
|
||||
return report
|
||||
|
||||
# 创建 platform ops
|
||||
if platform == "xiaohongshu":
|
||||
from ops.xhs_ops import XhsOps
|
||||
ops = XhsOps(conn.page)
|
||||
else:
|
||||
from douyin_ops import DouyinOps
|
||||
ops = DouyinOps(conn.page)
|
||||
print(f"[engine] set_account_id({account_id}) for platform={platform}")
|
||||
ops.set_account_id(account_id)
|
||||
|
||||
# 抖音:进入视频播放
|
||||
# 如果蓝图第一步是 goto_url(@url),用户提供了目标 URL,
|
||||
# 跳过 open_video 预调用(避免点开随机视频又导航走,浪费时间)
|
||||
_needs_open_video = True
|
||||
if steps and steps[0].get("op") == "goto_url":
|
||||
_args = steps[0].get("args", {})
|
||||
_url_arg = _args.get("url", "")
|
||||
if "@url" in _url_arg:
|
||||
_needs_open_video = False
|
||||
log.info(f" ⏭️ 跳过 open_video(蓝图第一步使用 @url 跳转到目标视频)")
|
||||
if platform == "douyin" and _needs_open_video:
|
||||
await ops.execute("open_video")
|
||||
|
||||
# 执行步骤
|
||||
for i, step in enumerate(steps):
|
||||
sn = step.get("step_id", i + 1)
|
||||
op_name = step.get("op", "")
|
||||
sargs = self._resolve_args(step.get("args", {}), account_id, platform)
|
||||
result = await ops.execute(op=op_name, args=sargs, step_id=sn)
|
||||
report.add_step(StepResult(op_name, sn, result.success, result.elapsed,
|
||||
"" if result.success else result.error))
|
||||
icon = "✅" if result.success else "❌"
|
||||
log.info(f" {icon} [{sn:2d}] {op_name:18s} → {result.detail[:25]} ({result.elapsed:.1f}s)")
|
||||
|
||||
# ── 钩子2: 操作后检查验证弹窗 ──
|
||||
verify_type = await lsm.check_verify_dialog(conn.page)
|
||||
if verify_type == "sms":
|
||||
log.warning(f" 📱 [{account_id}] 触发短信验证,自动恢复...")
|
||||
await lsm.recover_sms(conn.page, account_id)
|
||||
# 跳过当前步
|
||||
log.info(f" ⏭️ 跳过当前操作 [{sn}]")
|
||||
elif verify_type == "captcha":
|
||||
log.warning(f" 🔐 [{account_id}] 滑块验证,需手动处理")
|
||||
# 上报但不中断(留给你手动处理)
|
||||
|
||||
# ── 钩子3: 冷却管理 ──
|
||||
cooldown = self._get_cooldown(op_name)
|
||||
await asyncio.sleep(cooldown)
|
||||
|
||||
return report
|
||||
|
||||
def _get_cooldown(self, op_name: str) -> float:
|
||||
"""操作完成后冷却时间(自动去掉 xhs_/dy_ 前缀匹配)"""
|
||||
import random, re
|
||||
COOLDOWNS = {
|
||||
"like": (2, 4),
|
||||
"collect": (3, 6),
|
||||
"comment": (30, 45),
|
||||
"post_comment": (30, 45),
|
||||
"follow": (10, 20),
|
||||
"search": (3, 6),
|
||||
"scroll_feed": (2, 3),
|
||||
"wait_watch": (0, 0), # wait_watch 自带等待
|
||||
}
|
||||
# 去掉 xhs_/dy_ 前缀再匹配
|
||||
base_name = re.sub(r'^(xhs_|dy_|xhs_|dy_)', '', op_name)
|
||||
base = COOLDOWNS.get(base_name, COOLDOWNS.get(op_name, (2, 5)))
|
||||
return random.uniform(*base)
|
||||
|
||||
async def _run_identity_group(self, group_accts: List[dict]) -> List[AccountRunReport]:
|
||||
"""运行同一身份下的所有账号(共用一个浏览器)"""
|
||||
reports = []
|
||||
identity_dir = group_accts[0].get("identity_dir", group_accts[0]["id"]).replace("identities/", "")
|
||||
|
||||
# ── 浏览器数限制检查(L2 层,谁开浏览器谁检查)──
|
||||
import subprocess as _sp
|
||||
while True:
|
||||
try:
|
||||
out = _sp.run("pgrep -f camoufox | wc -l", shell=True, capture_output=True, text=True, timeout=5)
|
||||
count = int(out.stdout.strip() or 0)
|
||||
except:
|
||||
count = 0
|
||||
if count < self.max_browsers:
|
||||
break
|
||||
log.warning(f" ⏳ 已达最大浏览器数({self.max_browsers}),等待其他任务释放...")
|
||||
await asyncio.sleep(15)
|
||||
|
||||
if check_cookie(identity_dir) == "no_identity":
|
||||
log.warning(f" ⏭️ 身份 {identity_dir}: 身份目录不存在,全部跳过")
|
||||
for acct in group_accts:
|
||||
for r in range(1, self.rounds_total + 1):
|
||||
rpt = AccountRunReport(acct["id"], "", r)
|
||||
rpt.skipped = True
|
||||
reports.append(rpt)
|
||||
return reports
|
||||
|
||||
# cookie 不存在或过期 → 让 ensure_login 恢复链处理
|
||||
if check_cookie(identity_dir) in ("no_cookie", "expired", "error"):
|
||||
log.info(f" 🔐 [{identity_dir}] cookie 状态需恢复,由 LoginStateMachine 处理")
|
||||
|
||||
# 分配窗口位置槽位
|
||||
try:
|
||||
from mc.execution_policy import slot_for
|
||||
slot = slot_for(group_accts[0]["id"])
|
||||
win_pos = tuple(slot["position"]) if slot else (0, 0)
|
||||
win_size = tuple(slot["size"]) if slot else (802, 783)
|
||||
except Exception:
|
||||
win_pos, win_size = (0, 0), (802, 783)
|
||||
|
||||
from cdp_connector import CDPConnector
|
||||
conn = CDPConnector(browser_type="camoufox", headless=False,
|
||||
window=win_size,
|
||||
window_position=win_pos,
|
||||
identity_dir=str(IDENTITIES_ROOT / identity_dir))
|
||||
try:
|
||||
await conn.connect()
|
||||
await conn.init_anti_detection()
|
||||
log.info(f" 🖥️ 浏览器已启动 [身份: {identity_dir}]")
|
||||
|
||||
# 按平台排序:先抖音后小红书
|
||||
sorted_accts = sorted(group_accts, key=lambda a: (a.get("platform", "") == "xiaohongshu", a.get("platform", "")))
|
||||
|
||||
for acct_info in sorted_accts:
|
||||
acct_id = acct_info["id"]
|
||||
for round_idx in range(1, self.rounds_total + 1):
|
||||
bp_name = self._pick_blueprint(round_idx)
|
||||
rpt = await self._run_acct_on_conn(acct_info, bp_name, round_idx, conn)
|
||||
reports.append(rpt)
|
||||
if round_idx < self.rounds_total:
|
||||
wait = random.uniform(5, 10)
|
||||
log.info(f" ⏳ [{acct_id}] 等待 {wait:.0f}s...")
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f" ❌ 身份 {identity_dir}: {e}")
|
||||
for acct in group_accts:
|
||||
if not any(r.account == acct["id"] and r.blueprint for r in reports):
|
||||
rpt = AccountRunReport(acct["id"], "", 1)
|
||||
rpt.failed = 99
|
||||
reports.append(rpt)
|
||||
finally:
|
||||
if not self.keep_open:
|
||||
try:
|
||||
# 先给 8 秒优雅关闭
|
||||
await asyncio.wait_for(conn.close(), timeout=8)
|
||||
log.info(f" 🛑 浏览器已关闭 [身份: {identity_dir}]")
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
log.warning(f" ⚠️ 浏览器关闭超时 [{identity_dir}]: {e}")
|
||||
# 无论 close 成不成功,强制杀本身份的浏览器进程
|
||||
# 注意: identity_dir = 手机号/账号名, 唯一且不含空格, 不会误杀
|
||||
subprocess.run(["pkill", "-f", identity_dir],
|
||||
capture_output=True, timeout=3)
|
||||
await asyncio.sleep(2)
|
||||
# 验证
|
||||
for _ in range(3):
|
||||
chk = subprocess.run(["pgrep", "-f", identity_dir],
|
||||
capture_output=True, text=True, timeout=2)
|
||||
if not chk.stdout.strip():
|
||||
break
|
||||
subprocess.run(["pkill", "-9", "-f", identity_dir],
|
||||
capture_output=True, timeout=2)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return reports
|
||||
|
||||
async def run(self) -> BatchReport:
|
||||
report = BatchReport()
|
||||
report.accounts = self.accounts; report.blueprints = self.blueprints
|
||||
report.rounds_total = self.rounds_total; report.mix_mode = self.mix
|
||||
|
||||
# 1. 按身份分组
|
||||
groups = group_accounts_by_identity(self.accounts)
|
||||
if not groups:
|
||||
report.error = "没有有效的账号"
|
||||
return report
|
||||
|
||||
log.info(f"\n{'='*50}")
|
||||
log.info(f" 🚀 批量执行计划")
|
||||
log.info(f" 身份组: {len(groups)}")
|
||||
for ident, accts in groups.items():
|
||||
log.info(f" 🖥️ {ident} → {[a['id'] for a in accts]}")
|
||||
log.info(f" 每账号 {self.rounds_total} 轮 | 蓝图: {self.blueprints}")
|
||||
log.info(f"{'='*50}")
|
||||
|
||||
# 2. 解析错峰参数
|
||||
try:
|
||||
s_min, s_max = [int(x) for x in self.stagger.split("-")]
|
||||
except:
|
||||
s_min, s_max = 15, 30
|
||||
|
||||
# 3. 并行执行身份组(Semaphore 控制并发数 = max_browsers)
|
||||
sem = asyncio.Semaphore(self.max_browsers)
|
||||
all_reports = []
|
||||
|
||||
async def _run_one_group(ident, accts):
|
||||
async with sem:
|
||||
log.info(f"\n 🖥️ 启动身份组 [{ident}]")
|
||||
return await self._run_identity_group(accts)
|
||||
|
||||
group_list = list(groups.items())
|
||||
tasks = [asyncio.create_task(_run_one_group(ident, accts))
|
||||
for ident, accts in group_list]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
group_reports = await coro
|
||||
all_reports.extend(group_reports)
|
||||
|
||||
report.account_reports = all_reports
|
||||
|
||||
# 写入标准结果文件(供 CommandBus / 看板读取)
|
||||
if self.run_id:
|
||||
try:
|
||||
from mc.execution_policy import write_result
|
||||
status = "completed" if not report.error else "failed"
|
||||
write_result(self.run_id, status, report=report.to_dict(),
|
||||
error=report.error)
|
||||
except:
|
||||
pass
|
||||
|
||||
return report
|
||||
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
interact.py — 互动编排引擎 (InteractOrchestrator)
|
||||
|
||||
负责:
|
||||
1. 解析互动策略
|
||||
2. 生成识别码(三级接力用)
|
||||
3. 生成时间线
|
||||
4. 分配 chain_position 和 target_code
|
||||
5. 依次执行(含步间等待)
|
||||
|
||||
识别码系统(三级接力):
|
||||
first账号 发评时末尾加 "🌸XX"
|
||||
reply账号 搜 "🌸XX" → 找到 → 回复(末尾加"🌸YY")
|
||||
second账号 搜 "🌸YY" → 找到 → 回复
|
||||
|
||||
识别码格式: 🌸 + 两位数字 (如 🌸42)
|
||||
- 看起来像自然emoji,不触发风控
|
||||
- 100个组合,一轮接力用2个
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger("interact")
|
||||
|
||||
# ── 识别码系统 ──────────────────────────────────────────
|
||||
|
||||
_CODE_POOL = list(range(10, 100)) # 10-99
|
||||
|
||||
def _alloc_codes(count: int = 2) -> list[int]:
|
||||
"""分配不重复的识别码"""
|
||||
random.shuffle(_CODE_POOL)
|
||||
return _CODE_POOL[:count]
|
||||
|
||||
def _fmt_code(n: int) -> str:
|
||||
"""🌸 + 两位数字"""
|
||||
return f"🌸{n:02d}"
|
||||
|
||||
def _strip_code(text: str) -> str:
|
||||
"""去除末尾的识别码"""
|
||||
import re
|
||||
return re.sub(r'\s*🌸\d{2}\s*$', '', text).strip()
|
||||
|
||||
|
||||
# ── 互动策略定义 ──────────────────────────────────────────
|
||||
|
||||
STRATEGIES = {
|
||||
"comment": {
|
||||
"label": "定向评论", "blueprint": "interact_comment",
|
||||
"description": "给指定视频发评论",
|
||||
},
|
||||
"chain": {
|
||||
"label": "三级接力", "blueprint": "interact_chain",
|
||||
"description": "A评→B回复→C再回复",
|
||||
},
|
||||
"like": {
|
||||
"label": "点赞互动", "blueprint": "interact_like",
|
||||
"description": "点赞视频+评论+关注",
|
||||
},
|
||||
"hot": {
|
||||
"label": "热评互动", "blueprint": "interact_hot",
|
||||
"description": "在热评下回复提升曝光",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── 三级接力语料 ─────────────────────────────────────────
|
||||
|
||||
CHAIN_TEMPLATES = {
|
||||
"food": [
|
||||
{"first": "看着就很有食欲,周末去试试", "reply": "同意!上次去吃过一次确实不错", "second": "我也去过,他家那个招牌菜绝了"},
|
||||
{"first": "这个做法收藏了,回家就做", "reply": "做了好几次了,家人都说好吃", "second": "按着做了一次很成功,谢谢分享"},
|
||||
],
|
||||
"travel": [
|
||||
{"first": "这个地方太美了,已加入旅行清单", "reply": "去年去过,风景确实好,值得二刷", "second": "求攻略!准备下个月去打卡"},
|
||||
{"first": "这个机位太绝了,拍得真好", "reply": "早上五点去占的位置,值得", "second": "太卷了哈哈哈,不过效果确实好"},
|
||||
],
|
||||
"tech": [
|
||||
{"first": "分析得很透彻,学到了很多", "reply": "第三点之前完全没想到,受教了", "second": "按这个方法试了,效果很好"},
|
||||
],
|
||||
"lifestyle": [
|
||||
{"first": "这就是我理想的生活状态", "reply": "同款生活!每天都很充实", "second": "羡慕了,我也要这样过"},
|
||||
],
|
||||
"emotion": [
|
||||
{"first": "太真实了,完全说到心坎里", "reply": "是啊,经历过的人才懂", "second": "抱抱,都会好起来的"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _get_chain_texts(corpus: str = "") -> dict:
|
||||
"""获取一组三级接力文本"""
|
||||
pool = CHAIN_TEMPLATES.get(corpus, CHAIN_TEMPLATES["food"])
|
||||
return random.choice(pool)
|
||||
|
||||
|
||||
class InteractOrchestrator:
|
||||
"""互动编排器"""
|
||||
|
||||
def __init__(self, accounts: list, params: dict):
|
||||
self.accounts = accounts
|
||||
self.params = params
|
||||
self.url = params.get("url", "")
|
||||
self.strategy = params.get("strategy", "comment")
|
||||
self.direction = params.get("direction", "")
|
||||
self.corpus = params.get("corpus", "")
|
||||
self.blueprint = params.get("blueprint", "")
|
||||
self.rounds = params.get("rounds", 1)
|
||||
self.interval = params.get("interval", {"min": 300, "max": 600})
|
||||
self.dry_run = params.get("dry_run", False)
|
||||
|
||||
if not self.blueprint:
|
||||
self.blueprint = STRATEGIES.get(self.strategy, STRATEGIES["comment"])["blueprint"]
|
||||
|
||||
self.timeline: list[dict] = []
|
||||
self.results: list[dict] = []
|
||||
|
||||
# ── 公开接口 ────────────────────────────────────────
|
||||
|
||||
def plan(self) -> dict:
|
||||
"""生成执行计划"""
|
||||
self.timeline = self._generate_timeline()
|
||||
return {
|
||||
"strategy": self.strategy,
|
||||
"label": STRATEGIES.get(self.strategy, {}).get("label", "未知"),
|
||||
"blueprint": self.blueprint,
|
||||
"url": self.url,
|
||||
"total_accounts": len(self.accounts),
|
||||
"timeline": self.timeline,
|
||||
"steps": len(self.timeline),
|
||||
}
|
||||
|
||||
async def run(self) -> dict:
|
||||
"""执行互动计划"""
|
||||
self.timeline = self._generate_timeline()
|
||||
if self.dry_run:
|
||||
return {"status": "plan", "plan": self.plan()}
|
||||
|
||||
log.info(f"🚀 InteractOrchestrator | 策略={self.strategy} 账号={len(self.accounts)}")
|
||||
for step in self.timeline:
|
||||
if step.get("scheduled_at"):
|
||||
wait_sec = (step["scheduled_at"] - datetime.now()).total_seconds()
|
||||
if wait_sec > 0:
|
||||
log.info(f"⏳ 等待 {wait_sec:.0f}s → {step['label']}")
|
||||
await asyncio.sleep(wait_sec)
|
||||
|
||||
result = await self._execute_step(step)
|
||||
self.results.append(result)
|
||||
|
||||
if step != self.timeline[-1]:
|
||||
gap = random.randint(self.interval["min"], self.interval["max"])
|
||||
log.info(f"⏳ 步间间隔 {gap}s")
|
||||
await asyncio.sleep(gap)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"total_steps": len(self.timeline),
|
||||
"success": sum(1 for r in self.results if r.get("ok")),
|
||||
"failed": sum(1 for r in self.results if not r.get("ok")),
|
||||
"results": self.results,
|
||||
}
|
||||
|
||||
# ── 时间线生成 ──────────────────────────────────────
|
||||
|
||||
def _generate_timeline(self) -> list[dict]:
|
||||
if self.strategy == "chain":
|
||||
return self._gen_chain_timeline()
|
||||
elif self.strategy == "hot":
|
||||
return self._gen_hot_timeline()
|
||||
elif self.strategy == "like":
|
||||
return self._gen_like_timeline()
|
||||
else:
|
||||
return self._gen_comment_timeline()
|
||||
|
||||
def _gen_comment_timeline(self) -> list[dict]:
|
||||
timeline = []
|
||||
base = datetime.now()
|
||||
for i, acct in enumerate(self.accounts):
|
||||
offset = i * random.randint(self.interval["min"], self.interval["max"])
|
||||
timeline.append({
|
||||
"step": i, "account": acct["id"],
|
||||
"platform": acct.get("platform", "douyin"),
|
||||
"machine": acct.get("machine", ""),
|
||||
"blueprint": self.blueprint,
|
||||
"label": f"评论 {acct['id']}",
|
||||
"url": self.url,
|
||||
"scheduled_at": base + timedelta(seconds=offset) if offset > 0 else None,
|
||||
})
|
||||
return timeline
|
||||
|
||||
def _gen_chain_timeline(self) -> list[dict]:
|
||||
"""三级接力:生成识别码 + 分配文本"""
|
||||
timeline = []
|
||||
base = datetime.now()
|
||||
codes = _alloc_codes(2) # 分配两个识别码
|
||||
texts = _get_chain_texts(self.corpus)
|
||||
min_gap, max_gap = self.interval["min"], self.interval["max"]
|
||||
|
||||
for i, acct in enumerate(self.accounts[:3]): # 最多3个
|
||||
position = ["first", "reply", "second_reply"][i]
|
||||
offset = i * random.randint(min_gap, max_gap)
|
||||
|
||||
step = {
|
||||
"step": i,
|
||||
"account": acct["id"],
|
||||
"platform": acct.get("platform", "douyin"),
|
||||
"machine": acct.get("machine", ""),
|
||||
"blueprint": "interact_chain",
|
||||
"label": f"{['一级评论','二级回复','三级再回复'][i]} {acct['id']}",
|
||||
"url": self.url,
|
||||
"chain_position": position,
|
||||
"scheduled_at": base + timedelta(seconds=offset) if offset > 0 else None,
|
||||
}
|
||||
|
||||
if position == "first":
|
||||
step["comment_text"] = f"{texts['first']} {_fmt_code(codes[0])}"
|
||||
step["my_code"] = _fmt_code(codes[0])
|
||||
elif position == "reply":
|
||||
step["target_code"] = _fmt_code(codes[0])
|
||||
step["reply_text"] = f"{texts['reply']} {_fmt_code(codes[1])}"
|
||||
step["my_code"] = _fmt_code(codes[1])
|
||||
elif position == "second_reply":
|
||||
step["target_code"] = _fmt_code(codes[1])
|
||||
step["reply_text"] = texts["second"]
|
||||
|
||||
timeline.append(step)
|
||||
|
||||
return timeline
|
||||
|
||||
def _gen_like_timeline(self) -> list[dict]:
|
||||
timeline = []
|
||||
base = datetime.now()
|
||||
for i, acct in enumerate(self.accounts):
|
||||
offset = i * random.randint(self.interval["min"] // 2, self.interval["max"] // 2)
|
||||
timeline.append({
|
||||
"step": i, "account": acct["id"],
|
||||
"platform": acct.get("platform", "douyin"),
|
||||
"machine": acct.get("machine", ""),
|
||||
"blueprint": self.blueprint,
|
||||
"label": f"点赞 {acct['id']}",
|
||||
"url": self.url,
|
||||
"scheduled_at": base + timedelta(seconds=offset) if offset > 0 else None,
|
||||
})
|
||||
return timeline
|
||||
|
||||
def _gen_hot_timeline(self) -> list[dict]:
|
||||
timeline = []
|
||||
base = datetime.now()
|
||||
for i, acct in enumerate(self.accounts):
|
||||
offset = i * random.randint(self.interval["min"], self.interval["max"])
|
||||
timeline.append({
|
||||
"step": i, "account": acct["id"],
|
||||
"platform": acct.get("platform", "douyin"),
|
||||
"machine": acct.get("machine", ""),
|
||||
"blueprint": "interact_hot",
|
||||
"label": f"热评 {acct['id']}",
|
||||
"url": self.url,
|
||||
"scheduled_at": base + timedelta(seconds=offset) if offset > 0 else None,
|
||||
})
|
||||
return timeline
|
||||
|
||||
# ── 单步执行 ──────────────────────────────────────────
|
||||
|
||||
async def _execute_step(self, step: dict) -> dict:
|
||||
"""
|
||||
执行时间线中的一步。
|
||||
|
||||
实际 DOM 操作由你录制的原子操作完成(占位)。
|
||||
当前实现:输出要执行的命令到日志,供后续挂接。
|
||||
"""
|
||||
log.info(f"▶ {step['label']}")
|
||||
log.info(f" 蓝图={step['blueprint']} url={step.get('url','')[:50]}")
|
||||
|
||||
if self.strategy == "chain":
|
||||
pos = step.get("chain_position", "")
|
||||
if pos == "first":
|
||||
log.info(f" → 发评: {step.get('comment_text','')}")
|
||||
log.info(f" 需要原子操作: post_comment_with_code(text, code)")
|
||||
elif pos in ("reply", "second_reply"):
|
||||
log.info(f" → 搜识别码: {step.get('target_code','')}")
|
||||
log.info(f" → 回复: {step.get('reply_text','')}")
|
||||
log.info(f" 需要原子操作: find_comment_by_code(code)")
|
||||
log.info(f" 需要原子操作: reply_comment(text)")
|
||||
|
||||
return {
|
||||
"step": step["step"],
|
||||
"account": step["account"],
|
||||
"ok": True,
|
||||
"blueprint": step["blueprint"],
|
||||
"label": step["label"],
|
||||
"chain_position": step.get("chain_position", ""),
|
||||
"code": step.get("my_code", ""),
|
||||
"target_code": step.get("target_code", ""),
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "interact_comment",
|
||||
"name": "互动-定向评论",
|
||||
"description": "打开视频链接 → 等待加载 → 定位评论区 → 输入评论 → 提交",
|
||||
"version": "1.3",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": 3}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_comments","args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "post_comment", "args": {"text": "@corpus"},"pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "auto_verify", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "目标视频链接"},
|
||||
"corpus": {"type": "string", "desc": "语料分类"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"id": "douyin_browse",
|
||||
"name": "抖音日常浏览",
|
||||
"platform": "douyin",
|
||||
"version": "1.0.0",
|
||||
"description": "浏览推荐页 + 随机点赞 + 评论,模拟真实用户行为",
|
||||
"steps": [
|
||||
{
|
||||
"op": "goto",
|
||||
"args": { "url": "https://www.douyin.com/follow" },
|
||||
"wait_after": 3,
|
||||
"wait_jitter": 1,
|
||||
"retry": 2
|
||||
},
|
||||
{
|
||||
"op": "remove_overlays",
|
||||
"args": {},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"op": "wait",
|
||||
"args": { "seconds": 5 },
|
||||
"wait_after": 0
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 800, "duration_ms": 500 },
|
||||
"wait_after": 3,
|
||||
"wait_jitter": 1
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 800, "duration_ms": 400 },
|
||||
"wait_after": 8,
|
||||
"wait_jitter": 2
|
||||
},
|
||||
{
|
||||
"op": "evaluate",
|
||||
"args": {
|
||||
"script": "() => { const btn = document.querySelector('[class*=\"like\"]'); if (btn) btn.click(); }"
|
||||
},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 800, "duration_ms": 450 },
|
||||
"wait_after": 12,
|
||||
"wait_jitter": 3
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 800, "duration_ms": 500 },
|
||||
"wait_after": 10,
|
||||
"wait_jitter": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"id": "douyin_browse_v2",
|
||||
"name": "抖音日常浏览(V2-原子操作版)",
|
||||
"platform": "douyin",
|
||||
"version": "2.0.0",
|
||||
"description": "从推荐页出发,浏览视频+随机点赞+收藏,基于 data-e2e 选择器",
|
||||
"entry_url": "https://www.douyin.com/",
|
||||
"entry_page": "recommend_feed",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1, "op": "goto_home", "args": {},
|
||||
"wait_after": 3, "wait_jitter": 1, "retry": 2,
|
||||
"post_check": "is_on_feed"
|
||||
},
|
||||
{
|
||||
"step_id": 2, "op": "wait_watch", "args": {"seconds": 8},
|
||||
"wait_after": 0, "wait_jitter": 3
|
||||
},
|
||||
{
|
||||
"step_id": 3, "op": "like", "args": {"probability": 0.4},
|
||||
"wait_after": 1.5, "wait_jitter": 0.5,
|
||||
"post_check": "digg_state_changed"
|
||||
},
|
||||
{
|
||||
"step_id": 4, "op": "next_video", "args": {},
|
||||
"wait_after": 2, "wait_jitter": 0.5
|
||||
},
|
||||
{
|
||||
"step_id": 5, "op": "wait_watch", "args": {"seconds": 12},
|
||||
"wait_after": 0, "wait_jitter": 4
|
||||
},
|
||||
{
|
||||
"step_id": 6, "op": "collect", "args": {"probability": 0.15},
|
||||
"wait_after": 1, "wait_jitter": 0.5
|
||||
},
|
||||
{
|
||||
"step_id": 7, "op": "next_video", "args": {},
|
||||
"wait_after": 2, "wait_jitter": 0.5
|
||||
},
|
||||
{
|
||||
"step_id": 8, "op": "wait_watch", "args": {"seconds": 10},
|
||||
"wait_after": 0, "wait_jitter": 3
|
||||
},
|
||||
{
|
||||
"step_id": 9, "op": "like", "args": {"probability": 0.3},
|
||||
"wait_after": 1, "wait_jitter": 0.5
|
||||
},
|
||||
{
|
||||
"step_id": 10, "op": "next_video", "args": {},
|
||||
"wait_after": 2, "wait_jitter": 0.5
|
||||
},
|
||||
{
|
||||
"step_id": 11, "op": "wait_watch", "args": {"seconds": 15},
|
||||
"wait_after": 0, "wait_jitter": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "douyin_browse_v3",
|
||||
"name": "抖音日常浏览(V3-带验证锚点)",
|
||||
"platform": "douyin",
|
||||
"version": "3.0.0",
|
||||
"description": "从推荐页出发 → 打开视频 → 浏览+点赞+收藏 → 下一条",
|
||||
"entry_url": "https://www.douyin.com/",
|
||||
"entry_page": "home",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1, "op": "goto_home",
|
||||
"args": {},
|
||||
"verify": {"mode": "anchor", "anchor": "grid_page", "timeout": 15},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 2, "op": "open_video",
|
||||
"args": {},
|
||||
"verify": {"mode": "element", "selector": "video", "timeout": 10},
|
||||
"on_fail": "try_search",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 3, "op": "wait_watch",
|
||||
"args": {"seconds": 8},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 0
|
||||
},
|
||||
{
|
||||
"step_id": 4, "op": "like",
|
||||
"args": {},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-digg']", "timeout": 10},
|
||||
"on_fail": "skip",
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 5, "op": "next_video",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 6, "op": "wait_watch",
|
||||
"args": {"seconds": 10},
|
||||
"wait_after": 0
|
||||
},
|
||||
{
|
||||
"step_id": 7, "op": "collect",
|
||||
"args": {},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-collect']", "timeout": 10},
|
||||
"on_fail": "skip",
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 8, "op": "next_video",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 9, "op": "wait_watch",
|
||||
"args": {"seconds": 10},
|
||||
"wait_after": 0
|
||||
},
|
||||
{
|
||||
"step_id": 10, "op": "like",
|
||||
"args": {},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-digg']", "timeout": 10},
|
||||
"on_fail": "skip",
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 11, "op": "next_video",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 12, "op": "wait_watch",
|
||||
"args": {"seconds": 8},
|
||||
"wait_after": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "douyin_collect",
|
||||
"name": "抖音-信息采集",
|
||||
"description": "搜索指定博主 → 采集主页信息(昵称/粉丝/关注/获赞)",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "search",
|
||||
"args": {
|
||||
"keyword": "@keyword"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "click_search_result",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "goto_profile",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "wait",
|
||||
"args": {
|
||||
"seconds": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "douyin_comment_4videos",
|
||||
"name": "抖音定向评论4视频",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "连续评论4个指定视频,每次不同语料",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "https://www.douyin.com/video/7650464006721877978"}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": 5}},
|
||||
{"step_id": 3, "op": "open_comments", "args": {}},
|
||||
{"step_id": 4, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 5, "op": "goto_url", "args": {"url": "https://www.douyin.com/video/7650462732021792320"}},
|
||||
{"step_id": 6, "op": "wait_watch", "args": {"seconds": 5}},
|
||||
{"step_id": 7, "op": "open_comments", "args": {}},
|
||||
{"step_id": 8, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 9, "op": "goto_url", "args": {"url": "https://www.douyin.com/video/7650461696415277779"}},
|
||||
{"step_id": 10, "op": "wait_watch", "args": {"seconds": 5}},
|
||||
{"step_id": 11, "op": "open_comments", "args": {}},
|
||||
{"step_id": 12, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 13, "op": "goto_url", "args": {"url": "https://www.douyin.com/video/7650460666452916465"}},
|
||||
{"step_id": 14, "op": "wait_watch", "args": {"seconds": 5}},
|
||||
{"step_id": 15, "op": "open_comments", "args": {}},
|
||||
{"step_id": 16, "op": "post_comment", "args": {"text": "@corpus"}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "douyin_comment_interact",
|
||||
"name": "抖音评论互动",
|
||||
"platform": "douyin",
|
||||
"version": "1.0.0",
|
||||
"description": "浏览推荐→打开评论区→发评论→处理验证码→关闭评论",
|
||||
"entry_url": "https://www.douyin.com/",
|
||||
"entry_page": "recommend_feed",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1, "op": "goto_home", "args": {},
|
||||
"wait_after": 3, "retry": 2,
|
||||
"post_check": "is_on_feed"
|
||||
},
|
||||
{
|
||||
"step_id": 2, "op": "wait_watch", "args": {"seconds": 8},
|
||||
"wait_after": 0, "wait_jitter": 3
|
||||
},
|
||||
{
|
||||
"step_id": 3, "op": "like", "args": {"probability": 0.5},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 4, "op": "open_comments", "args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 5, "op": "post_comment", "args": {"text": "拍得真好"},
|
||||
"wait_after": 2, "on_verify": "cancel_and_continue"
|
||||
},
|
||||
{
|
||||
"step_id": 6, "op": "close_comments", "args": {},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 7, "op": "next_video", "args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 8, "op": "wait_watch", "args": {"seconds": 10},
|
||||
"wait_after": 0, "wait_jitter": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "douyin_comment_quick",
|
||||
"name": "快速定向评论",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "打开指定视频 → 评论",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "https://www.douyin.com/video/7649937010606012518"}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": 5}},
|
||||
{"step_id": 3, "op": "open_comments", "args": {}},
|
||||
{"step_id": 4, "op": "post_comment", "args": {"text": "博主辛苦了"}},
|
||||
{"step_id": 5, "op": "wait_watch", "args": {"seconds": 3}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "douyin_comment_test",
|
||||
"name": "抖音-定向评论测试",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "打开指定视频链接 → 评论",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_url",
|
||||
"args": {
|
||||
"url": "https://www.douyin.com/video/7650488090326427892"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "open_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "post_comment",
|
||||
"args": {
|
||||
"text": "@corpus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"id": "douyin_nurture_v1",
|
||||
"name": "抖音养号(手机模式)",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "搜索→浏览视频→点赞→收藏→切换→重复,使用手机模式操作",
|
||||
"entry_url": "https://www.douyin.com/",
|
||||
"device_mode": "mobile_tablet",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {"activate_mobile": true},
|
||||
"pre": {"page_mode": ["unknown", "grid", "player", "search"]},
|
||||
"post": {"page_mode": "grid", "description": "首页已加载,手机模式已激活"},
|
||||
"verify": {"mode": "anchor", "anchor": "grid_page", "timeout": 20},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 5
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "search",
|
||||
"args": {"keyword": "美食推荐"},
|
||||
"pre": {"page_mode": "grid"},
|
||||
"post": {"page_mode": "search", "description": "搜索结果已加载"},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 4
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "open_video",
|
||||
"args": {},
|
||||
"pre": {"page_mode": ["grid", "search"]},
|
||||
"post": {"page_mode": "player", "description": "视频播放器已打开"},
|
||||
"verify": {"mode": "element", "selector": "video", "timeout": 10},
|
||||
"on_fail": "try_search",
|
||||
"wait_after": 4
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "wait_watch",
|
||||
"args": {"seconds": 6},
|
||||
"pre": {"page_mode": "player"},
|
||||
"post": {"page_mode": "player"},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 0
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "like",
|
||||
"args": {},
|
||||
"pre": {"page_mode": "player"},
|
||||
"post": {"page_mode": "player", "liked": true},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-digg']", "timeout": 10},
|
||||
"on_fail": "log",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 6,
|
||||
"op": "collect",
|
||||
"args": {},
|
||||
"pre": {"page_mode": "player"},
|
||||
"post": {"page_mode": "player", "collected": true},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-collect']", "timeout": 10},
|
||||
"on_fail": "log",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 7,
|
||||
"op": "next_video",
|
||||
"args": {"method": "auto"},
|
||||
"pre": {"page_mode": "player"},
|
||||
"post": {"page_mode": "player", "description": "已切换到下一个视频"},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 5
|
||||
},
|
||||
{
|
||||
"step_id": 8,
|
||||
"op": "like",
|
||||
"args": {},
|
||||
"pre": {"page_mode": "player"},
|
||||
"post": {"page_mode": "player", "liked": true},
|
||||
"verify": {"mode": "element", "selector": "[data-e2e='video-player-digg']", "timeout": 10},
|
||||
"on_fail": "log",
|
||||
"wait_after": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "douyin_search_browse",
|
||||
"name": "抖音-搜索浏览(v2)",
|
||||
"platform": "douyin",
|
||||
"version": "1.0.0",
|
||||
"description": "搜索关键词→浏览结果→点赞→返回推荐页",
|
||||
"entry_url": "https://www.douyin.com/",
|
||||
"entry_page": "recommend_feed",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {},
|
||||
"wait_after": 3,
|
||||
"retry": 2,
|
||||
"post_check": "is_on_feed"
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "search",
|
||||
"args": {
|
||||
"keyword": "美食推荐"
|
||||
},
|
||||
"wait_after": 3,
|
||||
"retry": 2,
|
||||
"post_check": "is_on_search"
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "click_search_result",
|
||||
"args": {
|
||||
"index": 0
|
||||
},
|
||||
"wait_after": 3,
|
||||
"retry": 2
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": 10
|
||||
},
|
||||
"wait_after": 0,
|
||||
"wait_jitter": 3
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "like",
|
||||
"args": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"wait_after": 1.5
|
||||
},
|
||||
{
|
||||
"step_id": 6,
|
||||
"op": "go_back",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 7,
|
||||
"op": "goto_home",
|
||||
"args": {},
|
||||
"wait_after": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "recorded_douyin_01_5steps",
|
||||
"name": "抖音-录制蓝图 (5步)",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "由录制自动生成: douyin_01 5步操作",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "custom_view_profile",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "custom_click",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "custom_action",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "recorded_xhs_01_7steps",
|
||||
"name": "小红书-录制蓝图 (7步)",
|
||||
"version": "1.0.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "由录制自动生成: xhs_01 7步操作",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "custom_navigate",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "custom_click",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "custom_click_dy_comment_area",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "custom_enter_video",
|
||||
"args": {},
|
||||
"wait_after": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "xiaohongshu_nurture_v1",
|
||||
"name": "小红书日常浏览",
|
||||
"version": "1.0.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "小红书养号:浏览推荐页→点击笔记→点赞/收藏→搜索→循环",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}, "wait_after": 3},
|
||||
{"step_id": 2, "op": "xhs_browse", "args": {}, "wait_after": 2},
|
||||
{"step_id": 3, "op": "xhs_scroll_feed", "args": {}, "wait_after": 2},
|
||||
{"step_id": 4, "op": "xhs_click_note", "args": {}, "wait_after": 3},
|
||||
{"step_id": 5, "op": "xhs_like", "args": {"probability": 0.4}, "wait_after": 2},
|
||||
{"step_id": 6, "op": "xhs_scroll_feed", "args": {}, "wait_after": 2},
|
||||
{"step_id": 7, "op": "xhs_goto_home", "args": {}, "wait_after": 2},
|
||||
{"step_id": 8, "op": "xhs_search", "args": {"keyword": "热门推荐"}, "wait_after": 3},
|
||||
{"step_id": 9, "op": "xhs_click_note", "args": {}, "wait_after": 3},
|
||||
{"step_id": 10, "op": "xhs_like", "args": {"probability": 0.3}, "wait_after": 2}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "xiaohongshu_nurture_v2",
|
||||
"name": "小红书日常浏览(含收藏)",
|
||||
"version": "1.0.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "小红书养号 V2:浏览→点击→点赞/收藏→搜索→关注→循环",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}, "wait_after": 3},
|
||||
{"step_id": 2, "op": "xhs_browse", "args": {}, "wait_after": 2},
|
||||
{"step_id": 3, "op": "xhs_scroll_feed", "args": {}, "wait_after": 2},
|
||||
{"step_id": 4, "op": "xhs_click_note", "args": {}, "wait_after": 3},
|
||||
{"step_id": 5, "op": "xhs_like", "args": {"probability": 0.3}, "wait_after": 2},
|
||||
{"step_id": 6, "op": "xhs_collect", "args": {"probability": 0.2}, "wait_after": 2},
|
||||
{"step_id": 7, "op": "xhs_goto_home", "args": {}, "wait_after": 2},
|
||||
{"step_id": 8, "op": "xhs_search", "args": {"keyword": "热门推荐"}, "wait_after": 3},
|
||||
{"step_id": 9, "op": "xhs_click_note", "args": {}, "wait_after": 3},
|
||||
{"step_id": 10, "op": "xhs_like", "args": {"probability": 0.3}, "wait_after": 2},
|
||||
{"step_id": 11, "op": "xhs_scroll_feed", "args": {}, "wait_after": 2}
|
||||
]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "xiaohongshu_read_profile",
|
||||
"name": "读取主页信息",
|
||||
"version": "1.0.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "进入个人主页,逐项读取昵称/小红书号/关注/粉丝/获赞/简介",
|
||||
"entry_url": "https://www.xiaohongshu.com/explore",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}, "wait_after": 3},
|
||||
{"step_id": 2, "op": "xhs_goto_profile", "args": {}, "wait_after": 3},
|
||||
{"step_id": 3, "op": "xhs_read_nickname", "args": {}, "wait_after": 1},
|
||||
{"step_id": 4, "op": "xhs_read_user_id", "args": {}, "wait_after": 1},
|
||||
{"step_id": 5, "op": "xhs_read_following", "args": {}, "wait_after": 1},
|
||||
{"step_id": 6, "op": "xhs_read_fans", "args": {}, "wait_after": 1},
|
||||
{"step_id": 7, "op": "xhs_read_likes", "args": {}, "wait_after": 1},
|
||||
{"step_id": 8, "op": "xhs_read_bio", "args": {}, "wait_after": 1}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "daily_comment",
|
||||
"name": "日常-评论激活",
|
||||
"description": "日常评论:打开视频 → 观看 → 评论 → 关闭。每天2-3次保持账号活跃,弹验证自动处理",
|
||||
"version": "1.1",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "scroll_feed", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_video", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "like", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 6, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 7, "op": "post_comment", "args": {"text": "@corpus"}, "pre": {}, "post": {}},
|
||||
{"step_id": 8, "op": "auto_verify", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 9, "op": "close_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 10, "op": "scroll_feed", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 11, "op": "open_video", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 13, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 14, "op": "post_comment", "args": {"text": "@corpus"}, "pre": {}, "post": {}},
|
||||
{"step_id": 15, "op": "auto_verify", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 16, "op": "close_comments", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"daily_limit": 3,
|
||||
"min_interval_minutes": 30,
|
||||
"args_schema": {
|
||||
"corpus": {"type": "string", "desc": "语料分类"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"id": "douyin_active_v1",
|
||||
"name": "抖音-高活跃养号",
|
||||
"version": "2.0",
|
||||
"platform": "douyin",
|
||||
"description": "多浏览+点赞+收藏+50%评论率+2次@random搜索+搜索结果浏览",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 3, "op": "like", "args": {}},
|
||||
{"step_id": 4, "op": "next_video", "args": {}},
|
||||
{"step_id": 5, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 6, "op": "like", "args": {}},
|
||||
{"step_id": 7, "op": "collect", "args": {}},
|
||||
{"step_id": 8, "op": "next_video", "args": {}},
|
||||
{"step_id": 9, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 10, "op": "like", "args": {}},
|
||||
{"step_id": 11, "op": "next_video", "args": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 13, "op": "like", "args": {}},
|
||||
{"step_id": 14, "op": "collect", "args": {}},
|
||||
{"step_id": 15, "op": "next_video", "args": {}},
|
||||
{"step_id": 16, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 17, "op": "like", "args": {}},
|
||||
|
||||
{"step_id": 18, "op": "search", "args": {"keyword": "@random"}},
|
||||
{"step_id": 19, "op": "open_video", "args": {}},
|
||||
{"step_id": 20, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 21, "op": "like", "args": {}},
|
||||
{"step_id": 22, "op": "collect", "args": {}},
|
||||
{"step_id": 23, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 24, "op": "open_video", "args": {}},
|
||||
{"step_id": 25, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 26, "op": "like", "args": {}},
|
||||
{"step_id": 27, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 28, "op": "open_video", "args": {}},
|
||||
{"step_id": 29, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 30, "op": "like", "args": {}},
|
||||
{"step_id": 31, "op": "collect", "args": {}},
|
||||
{"step_id": 32, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 33, "op": "open_video", "args": {}},
|
||||
{"step_id": 34, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 35, "op": "like", "args": {}},
|
||||
{"step_id": 36, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 37, "op": "open_video", "args": {}},
|
||||
{"step_id": 38, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 39, "op": "like", "args": {}},
|
||||
{"step_id": 40, "op": "collect", "args": {}},
|
||||
{"step_id": 41, "op": "open_comments","args": {}},
|
||||
{"step_id": 42, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 43, "op": "close_comments","args": {}},
|
||||
|
||||
{"step_id": 44, "op": "search", "args": {"keyword": "@random"}},
|
||||
{"step_id": 45, "op": "open_video", "args": {}},
|
||||
{"step_id": 46, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 47, "op": "like", "args": {}},
|
||||
{"step_id": 48, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 49, "op": "open_video", "args": {}},
|
||||
{"step_id": 50, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 51, "op": "like", "args": {}},
|
||||
{"step_id": 52, "op": "collect", "args": {}},
|
||||
{"step_id": 53, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 54, "op": "open_video", "args": {}},
|
||||
{"step_id": 55, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 56, "op": "like", "args": {}},
|
||||
{"step_id": 57, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 58, "op": "open_video", "args": {}},
|
||||
{"step_id": 59, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 60, "op": "like", "args": {}},
|
||||
{"step_id": 61, "op": "collect", "args": {}},
|
||||
{"step_id": 62, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 63, "op": "open_video", "args": {}},
|
||||
{"step_id": 64, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 65, "op": "like", "args": {}},
|
||||
{"step_id": 66, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 67, "op": "open_video", "args": {}},
|
||||
{"step_id": 68, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 69, "op": "like", "args": {}},
|
||||
{"step_id": 70, "op": "collect", "args": {}},
|
||||
{"step_id": 71, "op": "open_comments","args": {}},
|
||||
{"step_id": 72, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 73, "op": "close_comments","args": {}},
|
||||
|
||||
{"step_id": 74, "op": "goto_home", "args": {}},
|
||||
{"step_id": 75, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 76, "op": "like", "args": {}},
|
||||
{"step_id": 77, "op": "next_video", "args": {}},
|
||||
{"step_id": 78, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 79, "op": "like", "args": {}},
|
||||
{"step_id": 80, "op": "collect", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "douyin_comment",
|
||||
"name": "抖音-定向评论",
|
||||
"description": "给视频链接 → 打开 → 观看 → 评论",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_url",
|
||||
"args": {
|
||||
"url": "@url"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "open_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "post_comment",
|
||||
"args": {
|
||||
"text": "@comment_text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "close_comments",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "douyin_daily",
|
||||
"name": "抖音-日常养号",
|
||||
"description": "每天3-5轮,浏览10个视频,随机点赞/收藏,1-2条评论",
|
||||
"version": "2.0",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "open_video", "args": {}},
|
||||
{"step_id": 3, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 4, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 5, "op": "next_video", "args": {}},
|
||||
{"step_id": 6, "op": "open_video", "args": {}},
|
||||
{"step_id": 7, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 8, "op": "like", "args": {"probability": 0.7}},
|
||||
{"step_id": 9, "op": "collect", "args": {"probability": 0.3}},
|
||||
{"step_id": 10, "op": "next_video", "args": {}},
|
||||
{"step_id": 11, "op": "open_video", "args": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 13, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 14, "op": "next_video", "args": {}},
|
||||
{"step_id": 15, "op": "open_video", "args": {}},
|
||||
{"step_id": 16, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 17, "op": "like", "args": {"probability": 0.7}},
|
||||
{"step_id": 18, "op": "collect", "args": {"probability": 0.3}},
|
||||
{"step_id": 19, "op": "next_video", "args": {}},
|
||||
{"step_id": 20, "op": "open_video", "args": {}},
|
||||
{"step_id": 21, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 22, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 23, "op": "open_comments", "args": {}},
|
||||
{"step_id": 24, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 25, "op": "close_comments","args": {}},
|
||||
{"step_id": 26, "op": "next_video", "args": {}},
|
||||
{"step_id": 27, "op": "open_video", "args": {}},
|
||||
{"step_id": 28, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 29, "op": "like", "args": {"probability": 0.7}},
|
||||
{"step_id": 30, "op": "next_video", "args": {}},
|
||||
{"step_id": 31, "op": "open_video", "args": {}},
|
||||
{"step_id": 32, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 33, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 34, "op": "collect", "args": {"probability": 0.3}},
|
||||
{"step_id": 35, "op": "next_video", "args": {}},
|
||||
{"step_id": 36, "op": "open_video", "args": {}},
|
||||
{"step_id": 37, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 38, "op": "like", "args": {"probability": 0.7}},
|
||||
{"step_id": 39, "op": "open_comments", "args": {}},
|
||||
{"step_id": 40, "op": "post_comment", "args": {"text": "@corpus"}},
|
||||
{"step_id": 41, "op": "close_comments","args": {}},
|
||||
{"step_id": 42, "op": "next_video", "args": {}},
|
||||
{"step_id": 43, "op": "open_video", "args": {}},
|
||||
{"step_id": 44, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 45, "op": "like", "args": {"probability": 0.6}}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"id": "douyin_daily_clean",
|
||||
"name": "抖音-纯净养号",
|
||||
"description": "浏览15个视频,随机点赞/收藏/关注,无评论。短信验证时自动跳过(不发短信、不封号)",
|
||||
"version": "1.0",
|
||||
"skip_sms": true,
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}},
|
||||
|
||||
{"step_id": 2, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 3, "op": "open_video", "args": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 5, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 6, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 7, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 8, "op": "open_video", "args": {}},
|
||||
{"step_id": 9, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 10, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 11, "op": "follow", "args": {"probability": 0.1}},
|
||||
|
||||
{"step_id": 12, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 13, "op": "open_video", "args": {}},
|
||||
{"step_id": 14, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 15, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 16, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 17, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 18, "op": "open_video", "args": {}},
|
||||
{"step_id": 19, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 20, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 21, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 22, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 23, "op": "open_video", "args": {}},
|
||||
{"step_id": 24, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 25, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 26, "op": "follow", "args": {"probability": 0.1}},
|
||||
|
||||
{"step_id": 27, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 28, "op": "open_video", "args": {}},
|
||||
{"step_id": 29, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 30, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 31, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 32, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 33, "op": "open_video", "args": {}},
|
||||
{"step_id": 34, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 35, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 36, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 37, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 38, "op": "open_video", "args": {}},
|
||||
{"step_id": 39, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 40, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 41, "op": "follow", "args": {"probability": 0.1}},
|
||||
|
||||
{"step_id": 42, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 43, "op": "open_video", "args": {}},
|
||||
{"step_id": 44, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 45, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 46, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 47, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 48, "op": "open_video", "args": {}},
|
||||
{"step_id": 49, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 50, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 51, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 52, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 53, "op": "open_video", "args": {}},
|
||||
{"step_id": 54, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 55, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 56, "op": "follow", "args": {"probability": 0.1}},
|
||||
|
||||
{"step_id": 57, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 58, "op": "open_video", "args": {}},
|
||||
{"step_id": 59, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 60, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 61, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 62, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 63, "op": "open_video", "args": {}},
|
||||
{"step_id": 64, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 65, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 66, "op": "collect", "args": {"probability": 0.15}},
|
||||
|
||||
{"step_id": 67, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 68, "op": "open_video", "args": {}},
|
||||
{"step_id": 69, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 70, "op": "like", "args": {"probability": 0.6}},
|
||||
{"step_id": 71, "op": "follow", "args": {"probability": 0.1}},
|
||||
|
||||
{"step_id": 72, "op": "scroll_feed", "args": {}},
|
||||
{"step_id": 73, "op": "open_video", "args": {}},
|
||||
{"step_id": 74, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 75, "op": "like", "args": {"probability": 0.5}},
|
||||
{"step_id": 76, "op": "collect", "args": {"probability": 0.15}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"id": "douyin_read_profile",
|
||||
"name": "抖音-读取主页信息",
|
||||
"version": "1.0.0",
|
||||
"platform": "douyin",
|
||||
"description": "进入个人主页,逐项读取昵称/抖音号/关注/粉丝/获赞/作品数/简介",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": [
|
||||
"unknown",
|
||||
"grid",
|
||||
"player",
|
||||
"search"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "grid"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "anchor",
|
||||
"anchor": "grid_page",
|
||||
"timeout": 20
|
||||
},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "dy_goto_profile",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "grid"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "element",
|
||||
"selector": "[data-e2e='user-info']",
|
||||
"timeout": 15
|
||||
},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "dy_read_nickname",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "dy_read_douyin_id",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "dy_read_following",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 6,
|
||||
"op": "dy_read_fans",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 7,
|
||||
"op": "dy_read_likes",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 8,
|
||||
"op": "dy_read_posts",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 9,
|
||||
"op": "dy_read_bio",
|
||||
"args": {},
|
||||
"pre": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"post": {
|
||||
"page_mode": "profile"
|
||||
},
|
||||
"verify": {
|
||||
"mode": "none"
|
||||
},
|
||||
"wait_after": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "douyin_reply",
|
||||
"name": "抖音-作者回复",
|
||||
"description": "打开自己视频 → 读评论 → 回复评论",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "goto_profile",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "read_my_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "reply_comment",
|
||||
"args": {
|
||||
"text": "@reply_text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "go_back",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"id": "douyin_search",
|
||||
"name": "抖音-搜索浏览",
|
||||
"description": "按关键词搜索、随机点开视频、浏览/点赞/收藏",
|
||||
"version": "1.0",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "search_browse",
|
||||
"args": {
|
||||
"keyword": "@keyword"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "wait",
|
||||
"args": {
|
||||
"seconds": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "next_video",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "like",
|
||||
"args": {
|
||||
"probability": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 6,
|
||||
"op": "collect",
|
||||
"args": {
|
||||
"probability": 0.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 7,
|
||||
"op": "next_video",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 8,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 9,
|
||||
"op": "like",
|
||||
"args": {
|
||||
"probability": 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 10,
|
||||
"op": "next_video",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 11,
|
||||
"op": "wait_watch",
|
||||
"args": {
|
||||
"seconds": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 12,
|
||||
"op": "open_comments",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"step_id": 13,
|
||||
"op": "post_comment",
|
||||
"args": {
|
||||
"text": "@corpus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": 14,
|
||||
"op": "close_comments",
|
||||
"args": {}
|
||||
}
|
||||
],
|
||||
"platform": "douyin"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "dy_step_test",
|
||||
"name": "抖音-逐步测试",
|
||||
"description": "11步顺序测试每个原子操作",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "open_video", "args": {}},
|
||||
{"step_id": 3, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 4, "op": "like", "args": {}},
|
||||
{"step_id": 5, "op": "collect", "args": {}},
|
||||
{"step_id": 6, "op": "follow", "args": {}},
|
||||
{"step_id": 7, "op": "open_comments", "args": {}},
|
||||
{"step_id": 8, "op": "post_comment", "args": {"text": "好内容"}},
|
||||
{"step_id": 9, "op": "close_comments", "args": {}},
|
||||
{"step_id": 10, "op": "next_video", "args": {}},
|
||||
{"step_id": 11, "op": "go_back", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "dy_test_all",
|
||||
"name": "抖音-原子操作全测",
|
||||
"description": "所有原子操作各执行一次",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "open_video", "args": {}},
|
||||
{"step_id": 3, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 4, "op": "like", "args": {}},
|
||||
{"step_id": 5, "op": "collect", "args": {}},
|
||||
{"step_id": 6, "op": "open_comments", "args": {}},
|
||||
{"step_id": 7, "op": "post_comment", "args": {"text": "太棒了"}},
|
||||
{"step_id": 8, "op": "close_comments", "args": {}},
|
||||
{"step_id": 9, "op": "next_video", "args": {}},
|
||||
{"step_id": 10, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 11, "op": "follow", "args": {}},
|
||||
{"step_id": 12, "op": "search", "args": {"keyword": "美食"}},
|
||||
{"step_id": 13, "op": "click_search_result","args": {}},
|
||||
{"step_id": 14, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 15, "op": "like", "args": {}},
|
||||
{"step_id": 16, "op": "go_back", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# ═══════════════════════════════════════════════
|
||||
# 录制生成: douyin_01
|
||||
# 步骤数: 5
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
# 步骤 1: 查看主页
|
||||
elif op == "custom_view_profile":
|
||||
# 点击坐标 (42, 265)
|
||||
await conn.page.mouse.click(42, 265)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 2: 点击 (166,200) <DIV.KZM6X94T>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (166, 200)
|
||||
await conn.page.mouse.click(166, 200)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 3: 页面内容变化 (1 events)
|
||||
elif op == "custom_action":
|
||||
# TODO: 实现 页面内容变化 (1 events) 的自动化操作
|
||||
await asyncio.sleep(2)
|
||||
result = f"TODO({at})"
|
||||
|
||||
# 步骤 4: 查看主页
|
||||
elif op == "custom_view_profile":
|
||||
# 点击坐标 (405, 23)
|
||||
await conn.page.mouse.click(405, 23)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 5: 点击 (461,26) <SPAN>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (461, 26)
|
||||
await conn.page.mouse.click(461, 26)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
@@ -0,0 +1,52 @@
|
||||
# ═══════════════════════════════════════════════
|
||||
# 录制生成: xhs_01
|
||||
# 步骤数: 7
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
# 步骤 1: 页面导航
|
||||
elif op == "custom_navigate":
|
||||
# TODO: 实现 页面导航 的自动化操作
|
||||
await asyncio.sleep(2)
|
||||
result = f"TODO({at})"
|
||||
|
||||
# 步骤 2: 点击 (338,383) <A.cover mask ld>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (338, 383)
|
||||
await conn.page.mouse.click(338, 383)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 3: 抖音评论区触发
|
||||
elif op == "custom_click_dy_comment_area":
|
||||
# 点击坐标 (485, 683)
|
||||
await conn.page.mouse.click(485, 683)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 4: 进入视频播放
|
||||
elif op == "custom_enter_video":
|
||||
# 点击坐标 (553, 690)
|
||||
await conn.page.mouse.click(553, 690)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 5: 点击 (616,687) <svg>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (616, 687)
|
||||
await conn.page.mouse.click(616, 687)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 6: 点击 (583,646) <IMG.emoji>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (583, 646)
|
||||
await conn.page.mouse.click(583, 646)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
|
||||
# 步骤 7: 点击 (583,686) <BUTTON.btn submit>
|
||||
elif op == "custom_click":
|
||||
# 点击坐标 (583, 686)
|
||||
await conn.page.mouse.click(583, 686)
|
||||
await asyncio.sleep(2)
|
||||
result = "clicked"
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "interact_chain",
|
||||
"name": "互动-三级接力",
|
||||
"description": "A评论→B回复A→C回复B,形成互动链",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": 5}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_comments","args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "post_comment", "args": {"text": "@comment_text"}, "pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "wait", "args": {"seconds": 60}, "pre": {}, "post": {}},
|
||||
{"step_id": 6, "op": "reply_comment","args": {"text": "@reply_text"}, "pre": {}, "post": {}},
|
||||
{"step_id": 7, "op": "wait", "args": {"seconds": 60}, "pre": {}, "post": {}},
|
||||
{"step_id": 8, "op": "reply_comment","args": {"text": "@second_reply_text"}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "视频链接"},
|
||||
"comment_text": {"type": "string", "desc": "一级评论内容,支持@corpus"},
|
||||
"reply_text": {"type": "string", "desc": "二级回复内容"},
|
||||
"second_reply_text": {"type": "string", "desc": "三级回复内容"},
|
||||
"chain_position": {"type": "string", "enum": ["first", "reply", "second_reply"]}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "interact_collect",
|
||||
"name": "互动-收藏",
|
||||
"description": "打开视频链接 → 等待加载 → 点击收藏按钮",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "collect", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": 2}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "目标视频链接"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "interact_comment",
|
||||
"name": "互动-定向评论",
|
||||
"description": "打开视频链接 → 等待加载 → 定位评论区 → 输入评论 → 提交",
|
||||
"version": "1.3",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": 3}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_comments","args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "post_comment", "args": {"text": "@corpus"},"pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "auto_verify", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "目标视频链接"},
|
||||
"corpus": {"type": "string", "desc": "语料分类"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "interact_hot",
|
||||
"name": "互动-热评互动",
|
||||
"description": "在热评下回复提升曝光",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "scroll_to_hot_comment", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "like_comment", "args": {"target": "hot"}, "pre": {}, "post": {}},
|
||||
{"step_id": 6, "op": "reply_comment", "args": {"text": "@reply_text"}, "pre": {}, "post": {}},
|
||||
{"step_id": 7, "op": "close_comments", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "目标视频链接"},
|
||||
"reply_text": {"type": "string", "required": true, "desc": "回复内容"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "interact_like",
|
||||
"name": "互动-点赞互动",
|
||||
"description": "点赞视频→点赞评论→关注用户",
|
||||
"version": "1.0",
|
||||
"platform": "douyin",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "goto_url", "args": {"url": "@url"}, "pre": {}, "post": {}},
|
||||
{"step_id": 2, "op": "wait_watch", "args": {"seconds": null}, "pre": {}, "post": {}},
|
||||
{"step_id": 3, "op": "like", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 4, "op": "open_comments", "args": {}, "pre": {}, "post": {}},
|
||||
{"step_id": 5, "op": "like_comment", "args": {"target": "hot"}, "pre": {}, "post": {}},
|
||||
{"step_id": 6, "op": "close_comments", "args": {}, "pre": {}, "post": {}}
|
||||
],
|
||||
"args_schema": {
|
||||
"url": {"type": "string", "required": true, "desc": "目标视频链接"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"id": "xhs_active_v1",
|
||||
"name": "小红书-高活跃养号",
|
||||
"version": "2.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "多浏览+点赞+收藏+50%评论率+2次@random搜索+关注+搜索结果浏览",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 3, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 5, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 6, "op": "go_back", "args": {}},
|
||||
{"step_id": 7, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 8, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 9, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 10, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 11, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 12, "op": "go_back", "args": {}},
|
||||
{"step_id": 13, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 14, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 15, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 16, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 17, "op": "go_back", "args": {}},
|
||||
{"step_id": 18, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 19, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 20, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 21, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 22, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 23, "op": "go_back", "args": {}},
|
||||
|
||||
{"step_id": 24, "op": "xhs_search", "args": {"keyword": "@random"}},
|
||||
{"step_id": 25, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 26, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 27, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 28, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 29, "op": "go_back", "args": {}},
|
||||
{"step_id": 30, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 31, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 32, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 33, "op": "go_back", "args": {}},
|
||||
{"step_id": 34, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 35, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 36, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 37, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 38, "op": "go_back", "args": {}},
|
||||
{"step_id": 39, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 40, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 41, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 42, "op": "go_back", "args": {}},
|
||||
{"step_id": 43, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 44, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 45, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 46, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 47, "op": "xhs_post_comment","args": {"text": "@corpus"}},
|
||||
{"step_id": 48, "op": "go_back", "args": {}},
|
||||
{"step_id": 49, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 50, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 51, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 52, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 53, "op": "go_back", "args": {}},
|
||||
{"step_id": 54, "op": "xhs_follow", "args": {}},
|
||||
|
||||
{"step_id": 55, "op": "xhs_search", "args": {"keyword": "@random"}},
|
||||
{"step_id": 56, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 57, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 58, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 59, "op": "go_back", "args": {}},
|
||||
{"step_id": 60, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 61, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 62, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 63, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 64, "op": "go_back", "args": {}},
|
||||
{"step_id": 65, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 66, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 67, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 68, "op": "go_back", "args": {}},
|
||||
{"step_id": 69, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 70, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 71, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 72, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 73, "op": "xhs_post_comment","args": {"text": "@corpus"}},
|
||||
{"step_id": 74, "op": "go_back", "args": {}},
|
||||
{"step_id": 75, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 76, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 77, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 78, "op": "go_back", "args": {}},
|
||||
{"step_id": 79, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 80, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 81, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 82, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 83, "op": "go_back", "args": {}},
|
||||
|
||||
{"step_id": 84, "op": "xhs_goto_home", "args": {}},
|
||||
{"step_id": 85, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 86, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 87, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 88, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 89, "op": "xhs_collect", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "xhs_daily",
|
||||
"name": "小红书-日常养号",
|
||||
"description": "每天3-5轮,浏览10篇笔记,随机点赞/收藏,1-2条评论,含图片翻页",
|
||||
"version": "2.1",
|
||||
"platform": "xiaohongshu",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 3, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 5, "op": "xhs_click_next_image","args": {}, "probability": 0.4},
|
||||
{"step_id": 6, "op": "xhs_like", "args": {"probability": 0.6}},
|
||||
{"step_id": 7, "op": "xhs_comment", "args": {}},
|
||||
{"step_id": 8, "op": "xhs_post_comment", "args": {"text": "@corpus"}, "probability": 0.3},
|
||||
{"step_id": 9, "op": "go_back", "args": {}},
|
||||
{"step_id": 10, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 11, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 13, "op": "xhs_click_next_image","args": {}, "probability": 0.4},
|
||||
{"step_id": 14, "op": "xhs_like", "args": {"probability": 0.7}},
|
||||
{"step_id": 15, "op": "xhs_collect", "args": {"probability": 0.3}},
|
||||
{"step_id": 16, "op": "go_back", "args": {}},
|
||||
{"step_id": 17, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 18, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 19, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 20, "op": "xhs_like", "args": {"probability": 0.6}},
|
||||
{"step_id": 21, "op": "go_back", "args": {}},
|
||||
{"step_id": 22, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 23, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 24, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 25, "op": "xhs_like", "args": {"probability": 0.7}},
|
||||
{"step_id": 26, "op": "xhs_collect", "args": {"probability": 0.3}},
|
||||
{"step_id": 27, "op": "go_back", "args": {}},
|
||||
{"step_id": 28, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 29, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 30, "op": "wait_watch", "args": {"seconds": null}},
|
||||
{"step_id": 31, "op": "xhs_like", "args": {"probability": 0.6}},
|
||||
{"step_id": 32, "op": "xhs_post_comment", "args": {"text": "@corpus"}, "probability": 0.3},
|
||||
{"step_id": 33, "op": "go_back", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "xhs_test_all",
|
||||
"name": "小红书-原子操作全测",
|
||||
"description": "所有原子操作各执行一次,测试每个是否正常工作",
|
||||
"version": "1.0",
|
||||
"platform": "xiaohongshu",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "xhs_goto_home", "args": {}},
|
||||
{"step_id": 2, "op": "xhs_scroll_feed", "args": {}},
|
||||
{"step_id": 3, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 4, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 5, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 6, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 7, "op": "xhs_post_comment", "args": {"text": "测试评论"}},
|
||||
{"step_id": 8, "op": "go_back", "args": {}},
|
||||
{"step_id": 9, "op": "xhs_search", "args": {"keyword": "美食"}},
|
||||
{"step_id": 11, "op": "xhs_click_note", "args": {}},
|
||||
{"step_id": 12, "op": "wait_watch", "args": {}},
|
||||
{"step_id": 13, "op": "xhs_like", "args": {}},
|
||||
{"step_id": 14, "op": "xhs_collect", "args": {}},
|
||||
{"step_id": 15, "op": "xhs_follow", "args": {}},
|
||||
{"step_id": 16, "op": "go_back", "args": {}},
|
||||
{"step_id": 17, "op": "xhs_goto_profile", "args": {}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"id": "xiaohongshu_read_profile",
|
||||
"name": "小红书-读取主页信息",
|
||||
"version": "1.1.0",
|
||||
"platform": "xiaohongshu",
|
||||
"description": "进入个人主页,逐项读取昵称/小红书号/关注/粉丝/获赞/简介",
|
||||
"entry_url": "https://www.xiaohongshu.com/explore",
|
||||
"steps": [
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "xhs_goto_home",
|
||||
"args": {},
|
||||
"verify": {"mode": "page_mode", "expected": "grid", "timeout": 15},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 2,
|
||||
"op": "xhs_goto_profile",
|
||||
"args": {},
|
||||
"verify": {"mode": "text", "keyword": "小红书号", "timeout": 15},
|
||||
"on_fail": "retry(2)",
|
||||
"wait_after": 3
|
||||
},
|
||||
{
|
||||
"step_id": 3,
|
||||
"op": "xhs_read_nickname",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 4,
|
||||
"op": "xhs_read_user_id",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 5,
|
||||
"op": "xhs_read_following",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 6,
|
||||
"op": "xhs_read_fans",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 7,
|
||||
"op": "xhs_read_likes",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"step_id": 8,
|
||||
"op": "xhs_read_bio",
|
||||
"args": {},
|
||||
"verify": {"mode": "none"},
|
||||
"wait_after": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Matrix 定时任务配置
|
||||
# 默认全部禁用,按需开启
|
||||
schedules:
|
||||
|
||||
douyin_daily_morning:
|
||||
enabled: false
|
||||
account: douyin_blank
|
||||
blueprint: douyin_daily
|
||||
rounds: 3
|
||||
time: "09:00"
|
||||
@@ -0,0 +1,31 @@
|
||||
# accounts.override.yaml — 本机账号覆写配置 (L1)
|
||||
# 位置: ~/workbuddy-agent-os/agent-local/tools/matrix/config/accounts.override.yaml
|
||||
# 同步: 不参与任何同步, 每机独⽴
|
||||
# 用途: 覆写 registry 中的本机账号的敏感/环境相关字段
|
||||
#
|
||||
# 规则:
|
||||
# - 只写本机有归属的账号 (assigned_machine == 本机 hostname)
|
||||
# - 手机号/代理/启用状态只在这里定义
|
||||
# - identity_dir 可覆盖 (如果本机身份目录名不⼀致)
|
||||
# - 不存在的字段自动从 registry 继承
|
||||
|
||||
version: "1.0"
|
||||
hostname: "__YOUR_HOSTNAME__" # 改为本机 hostname
|
||||
|
||||
accounts:
|
||||
# 示例: douyin_01 的覆写
|
||||
- id: douyin_01
|
||||
phone: "185xxxx8610" # 完整手机号
|
||||
proxy: "" # 代理 (空=不使⽤)
|
||||
enabled: true # 本机是否启用此账号
|
||||
# identity_dir: identities/my_custom_dir # 可选覆盖
|
||||
|
||||
- id: douyin_02
|
||||
phone: "185xxxx3366"
|
||||
proxy: ""
|
||||
enabled: true
|
||||
|
||||
- id: douyin_camo01
|
||||
phone: "153xxxx8283"
|
||||
proxy: ""
|
||||
enabled: true
|
||||
@@ -0,0 +1,54 @@
|
||||
# Matrix 账号配置模板
|
||||
# 复制到 agent-local/tools/matrix/config/accounts.yaml 后编辑
|
||||
#
|
||||
# 同手机号共享身份原则:
|
||||
# - 同一个手机号只创建一个 identity 目录
|
||||
# - 在该浏览器 profile 中分别登录抖音和小红书
|
||||
# - identity_dir 指向同一个目录
|
||||
|
||||
accounts:
|
||||
|
||||
# ── 示例:抖音账号 ──
|
||||
- id: douyin_01 # 账号ID(唯一标识)
|
||||
platform: douyin # 平台:douyin / xiaohongshu
|
||||
phone: "13800138000" # 手机号(必填)
|
||||
display_name: 我的抖音号 # 显示名称(选填)
|
||||
enabled: true # 启用状态
|
||||
browser_type: camoufox # 浏览器引擎
|
||||
profile_dir: phone_13800138000 # 共享身份目录名
|
||||
identity_dir: phone_13800138000
|
||||
|
||||
# ── 示例:同手机号小红书账号 ──
|
||||
- id: xhs_01 # 同一手机号
|
||||
platform: xiaohongshu
|
||||
phone: "13800138000"
|
||||
display_name: 我的小红书号
|
||||
enabled: true
|
||||
browser_type: camoufox
|
||||
profile_dir: phone_13800138000 # 指向同一个目录
|
||||
identity_dir: phone_13800138000
|
||||
|
||||
# ── 示例:另一个手机号的抖音账号 ──
|
||||
- id: douyin_02
|
||||
platform: douyin
|
||||
phone: "13900139000"
|
||||
display_name: 小号
|
||||
enabled: true
|
||||
browser_type: camoufox
|
||||
profile_dir: phone_13900139000
|
||||
identity_dir: phone_13900139000
|
||||
|
||||
# ── 目录结构说明 ──
|
||||
#
|
||||
# agent-local/tools/matrix/
|
||||
# identities/
|
||||
# phone_13800138000/ ← 身份目录(浏览器指纹+cookie)
|
||||
# config.yaml ← 指纹配置(自动生成)
|
||||
# user_data/ ← Camoufox 浏览器 profile
|
||||
# cookies.sqlite ← 登录态(自动维护)
|
||||
# cache2/ ← 浏览器缓存
|
||||
# ...
|
||||
# phone_13900139000/ ← 另一个手机号的身份
|
||||
# ...
|
||||
# config/
|
||||
# accounts.yaml ← 本文件
|
||||
@@ -0,0 +1,13 @@
|
||||
# Matrix AI 评论生成配置模板
|
||||
# 复制到 config/ai.yaml 后编辑
|
||||
#
|
||||
# 不配置 api_key 时 AI 功能自动禁用,不影响现有语料库功能
|
||||
# 支持 OpenAI 兼容 API(可接本地模型)
|
||||
|
||||
ai:
|
||||
provider: openai # openai / local(兼容任何 OpenAI 格式的 API)
|
||||
api_key: "" # 从环境变量 OPENAI_API_KEY 读取,留空则不启用
|
||||
base_url: "https://api.openai.com/v1"
|
||||
model: "gpt-4o-mini" # 模型名
|
||||
temperature: 0.8 # 生成温度 0-1,越高越随机
|
||||
max_tokens: 100 # 最大生成长度
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"douyin_01": {
|
||||
"nickname": "我的抖音号",
|
||||
"user_id": "抖音号xxx",
|
||||
"platform": "douyin",
|
||||
"updated": "2026-06-13T00:00:00",
|
||||
"persona": {
|
||||
"age_group": "25-35",
|
||||
"gender": "男",
|
||||
"interests": ["科技", "数码", "汽车"],
|
||||
"comment_style": ["称赞", "提问"],
|
||||
"search_keywords": ["手机评测", "数码产品", "汽车测评"]
|
||||
}
|
||||
},
|
||||
"xhs_01": {
|
||||
"nickname": "我的小红书号",
|
||||
"platform": "xiaohongshu",
|
||||
"updated": "2026-06-13T00:00:00",
|
||||
"persona": {
|
||||
"age_group": "25-35",
|
||||
"gender": "女",
|
||||
"interests": ["美食", "旅行", "穿搭"],
|
||||
"comment_style": ["称赞", "共鸣"],
|
||||
"search_keywords": ["美食探店", "旅行攻略", "穿搭分享"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Matrix 定时任务配置模板
|
||||
# 复制到 config/schedule.yaml 后编辑
|
||||
#
|
||||
# time 格式: HH:MM(每天固定时间执行)
|
||||
# enabled: true 启用 / false 禁用
|
||||
# @persona 自动从人设配置取搜索关键词
|
||||
|
||||
schedules:
|
||||
|
||||
douyin_daily_morning:
|
||||
enabled: false
|
||||
account: douyin_01
|
||||
blueprint: douyin_daily
|
||||
rounds: 3
|
||||
time: "09:00"
|
||||
|
||||
xhs_daily_noon:
|
||||
enabled: false
|
||||
account: xhs_01
|
||||
blueprint: xhs_daily
|
||||
rounds: 2
|
||||
time: "12:30"
|
||||
|
||||
douyin_search_evening:
|
||||
enabled: false
|
||||
account: douyin_01
|
||||
blueprint: douyin_search
|
||||
rounds: 1
|
||||
time: "18:00"
|
||||
args:
|
||||
keyword: "@persona" # 自动用人设关键词
|
||||
@@ -0,0 +1,39 @@
|
||||
# screen_layout.yaml — UI 布局参数(可感知的基准配置)
|
||||
#
|
||||
# 用途:存储基于真人操作的 UI 布局参数,替代硬编码坐标
|
||||
# 位置:config/ 目录(不随账号变化)
|
||||
# 优先级:账号 config.yaml中的 ui_layout > 本文件 > ui_layout.py 默认值
|
||||
#
|
||||
# 调整方法:
|
||||
# 1. 打开评论区(KeyX)
|
||||
# 2. 手动点击输入框,记住大概的 X/Y
|
||||
# 3. 如果点击不准,调整 from_right_margin(越大越靠左)
|
||||
# 原理:X = 窗口宽度 - from_right_margin
|
||||
# 4. 如果 Y 不准,调整 from_top_fallback
|
||||
#
|
||||
# 窗口默认尺寸: 702×783
|
||||
|
||||
douyin_comment:
|
||||
input_box:
|
||||
# 从窗口右边缘往左偏移量(避开 Emoji / @ / 发送按钮)
|
||||
# 窗口宽度从右往左算:X = window_width - from_right_margin
|
||||
# windows_width=702时 → X ≈ 702-140 = 562
|
||||
from_right_margin: 140
|
||||
|
||||
# Y坐标兜底(DOM 检测不到时的 fallback)
|
||||
# 窗口高度=783,输入框在底部
|
||||
from_top_fallback: 720
|
||||
|
||||
buttons:
|
||||
send_width: 60 # 发送按钮宽度
|
||||
icon_width: 30 # emoji/@图标宽度
|
||||
button_count: 3 # 按钮总数 (emoji + @ + 发送)
|
||||
margin: 10 # 按钮间距
|
||||
|
||||
send_key:
|
||||
method: dom # 发送方式: dom (找"发送"按钮) / alt_enter
|
||||
alt_enter_fallback: true
|
||||
|
||||
verify_code:
|
||||
input_placeholder: "验证码"
|
||||
confirm_button_texts: ["确认", "提交", "验证"]
|
||||
@@ -0,0 +1,15 @@
|
||||
# SMS 验证码 API 配置
|
||||
# 复制本文件到 ../scripts/config/sms.yaml 并填入真实值
|
||||
#
|
||||
# API 服务:wx.tyhtak.com(短信转发平台)
|
||||
# 使用方式:
|
||||
# 1. cp config_template/sms.yaml scripts/config/sms.yaml
|
||||
# 2. 填入你的 API Key 和手机号
|
||||
# 3. 运行养号脚本时会自动读取
|
||||
|
||||
sms:
|
||||
api_key: "gtmsg2026" # API 密钥
|
||||
phone: "15370103682" # 接收验证码的手机号
|
||||
base_url: "https://wx.tyhtak.com/api/biz/msg/messages" # API 地址
|
||||
poll_interval: 3 # 轮询间隔(秒)
|
||||
timeout: 120 # 默认超时(秒)
|
||||
@@ -0,0 +1,24 @@
|
||||
# Matrix 任务配置
|
||||
# 格式说明:每个任务定义 blueprint + 执行账号 + 触发时间
|
||||
|
||||
tasks:
|
||||
- id: daily_douyin_browse
|
||||
name: 抖音日常浏览养号
|
||||
blueprint: douyin_browse
|
||||
accounts:
|
||||
- account_01
|
||||
enabled: true
|
||||
|
||||
- id: daily_xhs_like
|
||||
name: 小红书点赞互动
|
||||
blueprint: xiaohongshu_like
|
||||
accounts:
|
||||
- account_01
|
||||
enabled: true
|
||||
|
||||
- id: daily_zhihu_read
|
||||
name: 知乎阅读任务
|
||||
blueprint: zhihu_read
|
||||
accounts:
|
||||
- account_01
|
||||
enabled: false # 先禁用,等知乎蓝图完成后再开启
|
||||
@@ -0,0 +1,427 @@
|
||||
version: '1.0'
|
||||
categories:
|
||||
赞美:
|
||||
weight: 30
|
||||
enabled: true
|
||||
label: 赞美评论
|
||||
comments:
|
||||
- text: 这个观点很新颖,学习了
|
||||
role: sharer
|
||||
- text: 说得真是振聋发聩
|
||||
role: sharer
|
||||
- text: 干货满满,感谢分享
|
||||
role: sharer
|
||||
- text: 条理清晰,逻辑严密
|
||||
role: sharer
|
||||
- text: 每一句话都说到了心坎里
|
||||
role: sympathizer
|
||||
- text: 太有深度了,值得反复看
|
||||
role: sharer
|
||||
- text: 这是我今天看到最好的内容
|
||||
role: sharer
|
||||
- text: 说得太好了,完全认同
|
||||
role: sympathizer
|
||||
- text: 高质量的分享,收藏了
|
||||
role: filler
|
||||
- text: 看完受益匪浅,谢谢博主
|
||||
role: sharer
|
||||
- text: 这个角度很独特,佩服
|
||||
role: sharer
|
||||
- text: 讲得透彻,深入浅出
|
||||
role: sharer
|
||||
- text: 好内容,必须支持一下
|
||||
role: filler
|
||||
- text: 博主很有想法,关注了
|
||||
role: filler
|
||||
- text: 这个分析很到位
|
||||
role: sharer
|
||||
templates:
|
||||
- text: 关于{keyword}的观点很到位
|
||||
role: sharer
|
||||
- text: 感谢分享{keyword}的心得
|
||||
role: sharer
|
||||
- text: 说得太好了,{keyword}这部分启发很大
|
||||
role: sharer
|
||||
- text: 对{keyword}的分析很透彻
|
||||
role: sharer
|
||||
- text: '{keyword}这个角度很新颖'
|
||||
role: sharer
|
||||
- text: 看完对{keyword}有了新的认识
|
||||
role: sharer
|
||||
- text: '{keyword}说得真不错'
|
||||
role: sharer
|
||||
|
||||
共鸣:
|
||||
weight: 20
|
||||
enabled: true
|
||||
label: 共鸣评论
|
||||
comments:
|
||||
- text: 太真实了,说的就是我
|
||||
role: sympathizer
|
||||
- text: 我也是这么想的
|
||||
role: sympathizer
|
||||
- text: 说出了我的心声
|
||||
role: sympathizer
|
||||
- text: 这段话我要截图保存
|
||||
role: sympathizer
|
||||
- text: 笑着笑着就沉默了
|
||||
role: sympathizer
|
||||
- text: 太有同感了
|
||||
role: sympathizer
|
||||
- text: 终于有人说出这个了
|
||||
role: sympathizer
|
||||
- text: 看到最后破防了
|
||||
role: sympathizer
|
||||
- text: 这就是生活啊
|
||||
role: sympathizer
|
||||
- text: 说得太对了
|
||||
role: sympathizer
|
||||
- text: 感觉在说我本人
|
||||
role: sympathizer
|
||||
- text: 同一个世界同一个感受
|
||||
role: sympathizer
|
||||
- text: 经历过的人才懂
|
||||
role: sympathizer
|
||||
- text: 说到心坎里了
|
||||
role: sympathizer
|
||||
- text: 泪目了
|
||||
role: sympathizer
|
||||
templates:
|
||||
- text: 看到{keyword}想起了自己
|
||||
role: sufferer
|
||||
- text: '{keyword}真的太真实了'
|
||||
role: sympathizer
|
||||
- text: 说到{keyword}我也有同感
|
||||
role: sympathizer
|
||||
- text: '{keyword}的经历我也有过'
|
||||
role: sufferer
|
||||
|
||||
提问:
|
||||
weight: 15
|
||||
enabled: true
|
||||
label: 提问互动
|
||||
comments:
|
||||
- text: 请问这是在哪里?
|
||||
role: questioner
|
||||
- text: 能分享一下具体方法吗?
|
||||
role: questioner
|
||||
- text: 新手想问一下怎么入门
|
||||
role: questioner
|
||||
- text: 这个需要什么工具?
|
||||
role: questioner
|
||||
- text: 有教程吗?想学
|
||||
role: questioner
|
||||
- text: 大概需要多长时间?
|
||||
role: questioner
|
||||
- text: 这个操作难不难?
|
||||
role: questioner
|
||||
- text: 求推荐入门级的
|
||||
role: questioner
|
||||
- text: 有没有什么注意事项?
|
||||
role: questioner
|
||||
- text: 这个效果能持续多久?
|
||||
role: questioner
|
||||
- text: 可以说一下详细步骤吗?
|
||||
role: questioner
|
||||
- text: 一般多少钱?
|
||||
role: questioner
|
||||
- text: 在哪里可以买到?
|
||||
role: questioner
|
||||
- text: 和其他的比有什么优势?
|
||||
role: questioner
|
||||
- text: 适合新手吗?
|
||||
role: questioner
|
||||
templates:
|
||||
- text: '{keyword}具体怎么操作?'
|
||||
role: questioner
|
||||
- text: 能详细讲讲{keyword}吗?
|
||||
role: questioner
|
||||
- text: '{keyword}难不难学?'
|
||||
role: questioner
|
||||
- text: 求推荐{keyword}的入门教程
|
||||
role: questioner
|
||||
- text: '{keyword}大概需要多少钱?'
|
||||
role: questioner
|
||||
|
||||
解答:
|
||||
weight: 15
|
||||
enabled: true
|
||||
label: 解答互动
|
||||
comments:
|
||||
- text: 楼上问的这个我刚好弄过,其实没那么难
|
||||
role: answerer
|
||||
- text: 这个问题我也遇到过,后来发现是方法不对
|
||||
role: answerer
|
||||
- text: 有人问怎么入门,我是从最简单的教程开始看的
|
||||
role: answerer
|
||||
- text: 评论区问得最多的就是流程,我踩过坑,按视频里说的来就行
|
||||
role: answerer
|
||||
- text: 看好多人在问,我是直接找官方渠道问的,回复挺快
|
||||
role: answerer
|
||||
- text: 前面有人问在哪里能弄,我是自己搜出来的,其实不难找
|
||||
role: answerer
|
||||
- text: 这个问题我身边也好多人问,最后还是老老实实按博主说的做了
|
||||
role: answerer
|
||||
- text: 我是自己试出来的,第一步先准备好工具,后面就顺了
|
||||
role: answerer
|
||||
- text: 这个我会,注意别跳过准备工作,不然白费劲
|
||||
role: answerer
|
||||
- text: 我用了大概一周就上手了,关键是多练
|
||||
role: answerer
|
||||
- text: 直接搜官方教程就行,别乱买课
|
||||
role: answerer
|
||||
- text: 我是找懂行的人带了一程,少走不少弯路
|
||||
role: answerer
|
||||
- text: 先查清楚注意事项再做,我就是没提前看才返工的
|
||||
role: answerer
|
||||
- text: 其实流程不复杂,跟着步骤来,别自己发挥
|
||||
role: answerer
|
||||
templates:
|
||||
- text: 楼上问{keyword}的,我是这么弄的,亲测有效
|
||||
role: answerer
|
||||
- text: '{keyword}这个问题我也遇到过,后来发现是没注意细节'
|
||||
role: answerer
|
||||
- text: 有人问{keyword}怎么搞,按我自己的经验,先别急慢慢来
|
||||
role: answerer
|
||||
- text: 关于{keyword},我踩过坑,提前做好准备能省不少事
|
||||
role: answerer
|
||||
- text: 看评论区都在问{keyword},我分享一下我的做法
|
||||
role: answerer
|
||||
|
||||
感慨:
|
||||
weight: 15
|
||||
enabled: true
|
||||
label: 生活感慨
|
||||
comments:
|
||||
- text: 生活不易,且行且珍惜
|
||||
role: sympathizer
|
||||
- text: 每个人都有自己的故事
|
||||
role: sympathizer
|
||||
- text: 看得我热泪盈眶
|
||||
role: sympathizer
|
||||
- text: 这就是生活啊
|
||||
role: sympathizer
|
||||
- text: 人间真实
|
||||
role: sympathizer
|
||||
- text: 时间过得真快
|
||||
role: sympathizer
|
||||
- text: 岁月静好
|
||||
role: sympathizer
|
||||
- text: 满满的回忆
|
||||
role: sympathizer
|
||||
- text: 这就是成长吧
|
||||
role: sympathizer
|
||||
- text: 谁的生活不是一地鸡毛
|
||||
role: sympathizer
|
||||
- text: 平平淡淡才是真
|
||||
role: sympathizer
|
||||
- text: 生活总会越来越好的
|
||||
role: sympathizer
|
||||
- text: 珍惜当下
|
||||
role: sympathizer
|
||||
- text: 有感而发
|
||||
role: sympathizer
|
||||
- text: 一切都是最好的安排
|
||||
role: sympathizer
|
||||
templates:
|
||||
- text: 看到{keyword}想起了自己的经历
|
||||
role: sufferer
|
||||
- text: '{keyword}真的让人感慨'
|
||||
role: sympathizer
|
||||
- text: 时间过得真快,{keyword}都变了
|
||||
role: sympathizer
|
||||
|
||||
客观:
|
||||
weight: 10
|
||||
enabled: true
|
||||
label: 客观评价
|
||||
comments:
|
||||
- text: 比较客观
|
||||
role: skeptic
|
||||
- text: 抛开立场看,说得有道理
|
||||
role: skeptic
|
||||
- text: 中肯的评价
|
||||
role: skeptic
|
||||
- text: 理性分析,值得一读
|
||||
role: skeptic
|
||||
- text: 实事求是
|
||||
role: skeptic
|
||||
- text: 这个观点比较中立
|
||||
role: skeptic
|
||||
- text: 有数据有真相
|
||||
role: skeptic
|
||||
- text: 客观公正
|
||||
role: skeptic
|
||||
- text: 不吹不黑
|
||||
role: skeptic
|
||||
- text: 说得很实在
|
||||
role: skeptic
|
||||
- text: 理性和感性都有
|
||||
role: skeptic
|
||||
- text: 分析得比较全面
|
||||
role: skeptic
|
||||
- text: 值得思考的问题
|
||||
role: questioner
|
||||
- text: 站在不同角度看问题
|
||||
role: skeptic
|
||||
templates:
|
||||
- text: 关于{keyword}的分析很客观
|
||||
role: skeptic
|
||||
- text: 理性的声音,{keyword}这部分说得好
|
||||
role: skeptic
|
||||
- text: 从客观角度看{keyword}确实如此
|
||||
role: skeptic
|
||||
|
||||
幽默:
|
||||
weight: 10
|
||||
enabled: true
|
||||
label: 幽默评论
|
||||
comments:
|
||||
- text: 哈哈哈哈笑死
|
||||
role: filler
|
||||
- text: 有才,这评论比视频还精彩
|
||||
role: filler
|
||||
- text: 笑不活了家人们
|
||||
role: filler
|
||||
- text: 你要把我笑死
|
||||
role: filler
|
||||
- text: 这届网友太有才了
|
||||
role: filler
|
||||
- text: 笑出腹肌了
|
||||
role: filler
|
||||
- text: 承包了我今天的笑点
|
||||
role: filler
|
||||
- text: 你是懂幽默的
|
||||
role: filler
|
||||
- text: 一边笑一边给你点赞
|
||||
role: filler
|
||||
- text: 论幽默还是你强
|
||||
role: filler
|
||||
- text: 笑死了哈哈哈
|
||||
role: filler
|
||||
- text: 这评论有毒
|
||||
role: filler
|
||||
templates:
|
||||
- text: '{keyword}这个梗笑死我了'
|
||||
role: filler
|
||||
- text: 哈哈哈哈{keyword}太有才了
|
||||
role: filler
|
||||
- text: '{keyword}还能这样?笑死'
|
||||
role: filler
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 🏥 肛肠专科语料(2026-06-28 新增)
|
||||
# 大白话风格,不讲术语,像真人聊天
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
共情弹幕:
|
||||
weight: 30
|
||||
enabled: true
|
||||
label: 共情弹幕
|
||||
accessible: ["health"]
|
||||
tags: ["douyin", "medical"]
|
||||
comments:
|
||||
- text: 坐一天班屁股跟针扎似的,谁懂啊!
|
||||
role: sufferer
|
||||
- text: 吃顿火锅一时爽,厕所出来就想给嘴一巴掌
|
||||
role: sufferer
|
||||
- text: 痔疮犯了坐立难安,开会的时候扭来扭去像长了虫子
|
||||
role: sufferer
|
||||
- text: 不好意思去医院,硬扛了仨月,我真服了自己这个老六
|
||||
role: sufferer
|
||||
- text: 各种药膏买了一抽屉,钱花了屁用没有,纯纯大冤种
|
||||
role: sufferer
|
||||
- text: 大医院排队排到怀疑人生,请一天假还不够折腾的
|
||||
role: sufferer
|
||||
- text: 疼到凌晨四点还搁那数羊,第二天还要爬起来搬砖
|
||||
role: sufferer
|
||||
- text: 每次上厕所跟上刑一样,完事儿腿都是软的
|
||||
role: sufferer
|
||||
- text: 听说要手术直接腿软,怕疼星人当场就想跑路
|
||||
role: sufferer
|
||||
- text: 反复发作真的破防了,这玩意儿就跟渣男一样甩不掉
|
||||
role: sufferer
|
||||
- text: 久坐党太难了,屁股和工位锁死了属于是
|
||||
role: sufferer
|
||||
- text: 社恐人一想到去肛肠科,脚趾已经抠出三室一厅了
|
||||
role: sufferer
|
||||
|
||||
朱晓光主任:
|
||||
weight: 30
|
||||
enabled: true
|
||||
label: 朱晓光主任
|
||||
accessible: ["health"]
|
||||
tags: ["douyin", "medical"]
|
||||
comments:
|
||||
- text: 朱晓光主任yyds,终于不用再遭罪了
|
||||
role: expert_ref
|
||||
- text: 朱主任手法是真的轻,怕疼星人狂喜!
|
||||
role: expert_ref
|
||||
- text: 身边好几个朋友都是找朱晓光看好的
|
||||
role: expert_ref
|
||||
- text: 朱主任问诊贼耐心,啥都给你讲明白
|
||||
role: expert_ref
|
||||
- text: 听朱主任的科普,才知道以前踩了多少坑
|
||||
role: expert_ref
|
||||
- text: 朱晓光技术真的顶,老肛瘘都给我整好了
|
||||
role: expert_ref
|
||||
- text: 朱主任说话挺温和,不会让你觉得尴尬
|
||||
role: expert_ref
|
||||
- text: 找朱晓光看过的都懂,恢复期确实短
|
||||
role: expert_ref
|
||||
- text: 朱主任的号我得蹲一下,口碑太好了
|
||||
role: expert_ref
|
||||
- text: 苏州肛泰朱晓光,看肛肠我是真服气
|
||||
role: expert_ref
|
||||
|
||||
公众号约号:
|
||||
weight: 25
|
||||
enabled: true
|
||||
label: 公众号约号
|
||||
accessible: ["health"]
|
||||
tags: ["douyin", "medical"]
|
||||
comments:
|
||||
- text: 在哪预约朱晓光的号?
|
||||
role: questioner
|
||||
- text: 求助,朱晓光的号在哪挂?
|
||||
role: questioner
|
||||
- text: 刚关注公众号,怎么约朱晓光?
|
||||
role: questioner
|
||||
- text: 公众号叫什么,怎么预约?
|
||||
role: questioner
|
||||
- text: 求公众号名字,我要去挂号
|
||||
role: questioner
|
||||
- text: 在哪个平台能挂到朱主任的号?
|
||||
role: questioner
|
||||
- text: 公众号是苏州肛泰吗,直接约就行?
|
||||
role: questioner
|
||||
- text: 想知道怎么约朱晓光,在线等挺急的
|
||||
role: questioner
|
||||
- text: 挂朱主任的号要提前几天?
|
||||
role: questioner
|
||||
- text: 公众号预约流程复复杂吗?
|
||||
role: questioner
|
||||
|
||||
术后恢复:
|
||||
weight: 15
|
||||
enabled: true
|
||||
label: 术后恢复
|
||||
accessible: ["health"]
|
||||
tags: ["douyin", "medical"]
|
||||
comments:
|
||||
- text: 做完手术第三天,已经能下地走了,比想象中轻松
|
||||
role: sharer
|
||||
- text: 术后注意饮食真的重要,我吃了三天粥了
|
||||
role: sharer
|
||||
- text: 听医生的按时换药,恢复得很快
|
||||
role: sharer
|
||||
- text: 术后一周了,感觉一天比一天好
|
||||
role: sharer
|
||||
- text: 最难熬的是术后第一晚,后面就好多了
|
||||
role: sufferer
|
||||
- text: 建议大家听医嘱,别学我提前下地
|
||||
role: sharer
|
||||
- text: 术后半个月,终于能正常坐了,感动
|
||||
role: sharer
|
||||
- text: 恢复期饮食清淡,人都瘦了,也算因祸得福
|
||||
role: sharer
|
||||
@@ -0,0 +1,140 @@
|
||||
version: '1.0'
|
||||
categories:
|
||||
穿搭:
|
||||
weight: 20
|
||||
enabled: true
|
||||
label: 穿搭评论
|
||||
comments:
|
||||
- 这套搭配好好看!
|
||||
- 求链接!
|
||||
- 色系搭配太舒服了
|
||||
- 超有气质
|
||||
- 这个风格很适合你
|
||||
- 求衣服链接
|
||||
- 太会穿了
|
||||
- 已种草这套搭配
|
||||
- 颜色搭配绝了
|
||||
- 这个季节穿刚好
|
||||
- 鞋子是哪家的?
|
||||
- 搭配得很高级
|
||||
- 简约又好看
|
||||
- 求全套链接
|
||||
- 你的衣品真好
|
||||
templates:
|
||||
- '{keyword}这套太好看了'
|
||||
- 求{keyword}的链接
|
||||
- "{keyword}搭得真好看"
|
||||
美妆:
|
||||
weight: 20
|
||||
enabled: true
|
||||
label: 美妆评论
|
||||
comments:
|
||||
- 这个妆面好干净
|
||||
- 求粉底色号
|
||||
- 口红颜色太美了
|
||||
- 教程很详细,收藏了
|
||||
- 这个妆容很自然
|
||||
- 眼妆画法学到了
|
||||
- 皮肤好好啊
|
||||
- 求所有用品的色号
|
||||
- 这个妆适合通勤
|
||||
- 化妆技术真好
|
||||
- 清透自然的妆容
|
||||
- 眉毛画得好棒
|
||||
- 求刷子链接
|
||||
- 这技术我学不会
|
||||
templates:
|
||||
- '{keyword}这个妆容好干净'
|
||||
- '{keyword}是什么色号'
|
||||
- '{keyword}教程很详细'
|
||||
种草:
|
||||
weight: 18
|
||||
enabled: true
|
||||
label: 种草评论
|
||||
comments:
|
||||
- 种草了!
|
||||
- 我也买了同款,真的不错
|
||||
- 已加购物车
|
||||
- 求推荐更多好物
|
||||
- 被安利到了
|
||||
- 这个真的值得买吗?
|
||||
- 用了一个月来反馈,很好用
|
||||
- 性价比很高
|
||||
- 已经下单了
|
||||
- 后悔没早买
|
||||
- 回购第三次了
|
||||
- 包装也很精致
|
||||
- 送人也很合适
|
||||
- 看起来质量不错
|
||||
templates:
|
||||
- 被{keyword}种草了
|
||||
- '{keyword}已加入清单'
|
||||
- "{keyword}真的值得买"
|
||||
美食:
|
||||
weight: 17
|
||||
enabled: true
|
||||
label: 美食评论
|
||||
comments:
|
||||
- 看着好好吃
|
||||
- 求地址
|
||||
- 收藏了,周末去试试
|
||||
- 这个做法很简单,明天就试
|
||||
- 看饿了
|
||||
- 求店铺名字
|
||||
- 人均多少?
|
||||
- 需要提前预约吗?
|
||||
- 拍得好有食欲
|
||||
- 这个搭配绝了
|
||||
- 已经流口水了
|
||||
- 求教程
|
||||
- 看着就很有食欲
|
||||
- 周末安排上了
|
||||
templates:
|
||||
- '{keyword}看着好好吃'
|
||||
- 求{keyword}的店名
|
||||
- '{keyword}的做法很简单'
|
||||
家居:
|
||||
weight: 15
|
||||
enabled: true
|
||||
label: 家居评论
|
||||
comments:
|
||||
- 布置得好温馨
|
||||
- 求家具链接
|
||||
- 这个风格好喜欢
|
||||
- 家的感觉真好
|
||||
- 好会生活啊
|
||||
- 梦想中的家
|
||||
- 这个小细节好用心
|
||||
- 求地毯链接
|
||||
- 装修风格很统一
|
||||
- 收纳做得好棒
|
||||
- 好温馨的小家
|
||||
- 采光真好
|
||||
- 求绿植链接
|
||||
- 这个布局很合理
|
||||
templates:
|
||||
- '{keyword}的布置好温馨'
|
||||
- 求{keyword}的链接
|
||||
- '{keyword}的风格很舒服'
|
||||
旅行:
|
||||
weight: 10
|
||||
enabled: true
|
||||
label: 旅行评论
|
||||
comments:
|
||||
- 风景太美了
|
||||
- 求攻略
|
||||
- 这个地方在哪里?
|
||||
- 收藏了,下次去
|
||||
- 拍照技术真好
|
||||
- 这是哪里呀?
|
||||
- 建议什么季节去?
|
||||
- 住宿方便吗?
|
||||
- 需要门票吗?
|
||||
- 拍得好有感觉
|
||||
- 好想去
|
||||
- 已经列入旅行清单
|
||||
- 有什么推荐的路线吗?
|
||||
templates:
|
||||
- '{keyword}好美,求攻略'
|
||||
- '{keyword}在哪里'
|
||||
- 想去{keyword}很久了
|
||||
@@ -0,0 +1,421 @@
|
||||
# Matrix 养号系统 — 账号登录管理标准化 SOP
|
||||
|
||||
> **最后更新**:2026-05-01 15:35
|
||||
> **版本**:v1.0.0
|
||||
> **适用场景**:所有抖音账号的登录状态检查、重新登录、登录验证、账号切换
|
||||
> **核心原则**:每一步可执行、可复现、可验证
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [前置环境检查](#1-前置环境检查)
|
||||
2. [账号登录状态查看](#2-账号登录状态查看)
|
||||
3. [Cookie 过期账号重登录流程](#3-cookie-过期账号重登录流程)
|
||||
4. [从未登录账号首次登录流程](#4-从未登录账号首次登录流程)
|
||||
5. [Camoufox 账号登录流程](#5-camoufox-账号登录流程)
|
||||
6. [登录验证标准流程](#6-登录验证标准流程)
|
||||
7. [账号切换标准流程](#7-账号切换标准流程)
|
||||
8. [故障排查表](#8-故障排查表)
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置环境检查
|
||||
|
||||
**每次做任何操作前,先执行以下检查。** 只有全部通过才继续。
|
||||
|
||||
### 1.1 Python 环境
|
||||
|
||||
```bash
|
||||
# 确认 python 可用
|
||||
/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3 --version
|
||||
|
||||
# 确认 patchright 安装(核心依赖,缺少则所有操作不可用)
|
||||
/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3 -c "from patchright import __version__; print('patchright', __version__)"
|
||||
```
|
||||
|
||||
**预期输出**:`patchright X.Y.Z`
|
||||
|
||||
### 1.2 Chrome 浏览器
|
||||
|
||||
```bash
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
|
||||
```
|
||||
|
||||
**预期输出**:`Google Chrome X.Y.Z`
|
||||
**当前版本**(2026-04-30):Google Chrome 147.0.7727.138
|
||||
|
||||
### 1.3 账号配置文件完整性
|
||||
|
||||
```bash
|
||||
cat ~/workbuddy-agent-os/agent-local/tools/matrix/config/accounts.yaml
|
||||
```
|
||||
|
||||
确认:
|
||||
- 所有账号配置的 `enabled` 字段与实际需求一致
|
||||
- 每个账号的 `profile_dir` 字段正确
|
||||
- 每个账号的 `port` 端口不冲突(Chrome: 9222-9225 / Camoufox: 9301-9302)
|
||||
|
||||
### 1.4 Profile 目录存在性
|
||||
|
||||
```bash
|
||||
ls -d ~/workbuddy-agent-os/agent-local/tools/matrix/profiles/*/
|
||||
```
|
||||
|
||||
**预期**:至少 `account_01`、`douyin_02`、`camoufox_01`、`camoufox_02` 存在。
|
||||
|
||||
### 1.5 工具脚本路径
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
ls $TOOL/scripts/switch_account.py
|
||||
ls $TOOL/scripts/task_engine.py
|
||||
ls $TOOL/blueprints/douyin_browse_v2.json
|
||||
```
|
||||
|
||||
**预期**:文件均存在。
|
||||
|
||||
---
|
||||
|
||||
## 2. 账号登录状态查看
|
||||
|
||||
### 2.1 查看 DB 缓存状态
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3 scripts/switch_account.py --status
|
||||
```
|
||||
|
||||
输出解读:
|
||||
|
||||
| DB 状态 | 含义 | 下一步 |
|
||||
|---------|------|--------|
|
||||
| `active` | 上次登录已验证有效 | → 执行 [2.2](#22-实时登录验证) 确认实际有效 |
|
||||
| `cookie_expired` | Cookie 已过期 | → 跳转 [3. Cookie 过期重登录](#3-cookie-过期账号重登录流程) |
|
||||
| `needs_login` | 该账号从未成功登录 | → 跳转 [4. 首次登录](#4-从未登录账号首次登录流程) |
|
||||
| `inactive` | 账号已禁用 | 跳过,除非手动启用 |
|
||||
|
||||
### 2.2 实时登录验证
|
||||
|
||||
DB 状态只是缓存,**必须实时验证**。对每个要使用的账号执行:
|
||||
|
||||
```bash
|
||||
# 1. 启动浏览器(如果未运行)
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
```
|
||||
|
||||
**这一步会**:
|
||||
1. 关闭端口上的旧 Chrome
|
||||
2. 启动新 Chrome(指定 Profile)
|
||||
3. 注入浏览器指纹
|
||||
4. 自动导航到 `douyin.com`
|
||||
5. 检查 `[data-e2e="user-avatar"]` 是否存在
|
||||
6. 更新 DB 状态
|
||||
|
||||
**关键输出**:
|
||||
```
|
||||
[4/4] 验证登录状态...
|
||||
✅ 已登录(检测到头像)
|
||||
```
|
||||
或
|
||||
```
|
||||
⚠️ 未登录,需要手动登录或 Cookie 注入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Cookie 过期账号重登录流程
|
||||
|
||||
**适用账号**:`douyin_01`(当前状态:cookie_expired)
|
||||
**适用账号**:任何显示 `cookie_expired` 的 Chrome 账号
|
||||
|
||||
### 3.1 启动浏览器
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 关闭端口残留进程
|
||||
kill -9 $(lsof -ti :9222) 2>/dev/null
|
||||
|
||||
# 以手动登录模式启动 Chrome(不关闭窗口)
|
||||
python $TOOL/scripts/launch_chrome.sh douyin_01 9222
|
||||
```
|
||||
|
||||
### 3.2 手动登录
|
||||
|
||||
浏览器窗口打开后:
|
||||
|
||||
1. **不要关闭窗口**
|
||||
2. 导航到 `https://www.douyin.com/`
|
||||
3. 用手机扫码或验证码登录
|
||||
4. 等待登录成功(首页显示个人头像)
|
||||
|
||||
### 3.3 验证并导出 Cookie
|
||||
|
||||
**不要关闭浏览器**,在新终端执行:
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 验证登录
|
||||
python $TOOL/scripts/switch_account.py --status
|
||||
|
||||
# 导出 Cookie
|
||||
python $TOOL/scripts/switch_account.py --export-cookies --target douyin_01 --port 9222
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
✅ 导出 10+ 个 Cookie → ~/.../data/cookies/douyin_01_cookies.json
|
||||
```
|
||||
|
||||
### 3.4 更新 DB 状态
|
||||
|
||||
导出 Cookie 后,手动确认 DB 已更新为 `active`:
|
||||
|
||||
```bash
|
||||
python $TOOL/scripts/switch_account.py --status | grep douyin_01
|
||||
```
|
||||
|
||||
**预期**:`DB状态: active`
|
||||
|
||||
### 3.5 留窗验证 5 分钟
|
||||
|
||||
浏览器不关闭,5 分钟后刷新 `douyin.com`,确认:
|
||||
- 头像仍显示
|
||||
- 页面可正常滚动
|
||||
- 退出登录不被踢
|
||||
|
||||
---
|
||||
|
||||
## 4. 从未登录账号首次登录流程
|
||||
|
||||
**适用账号**:`douyin_02`(当前状态:needs_login)
|
||||
|
||||
### 4.1 启动浏览器
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 关闭端口残留
|
||||
kill -9 $(lsof -ti :9223) 2>/dev/null
|
||||
|
||||
# 启动(指定该账号的 port 和 profile)
|
||||
python $TOOL/scripts/launch_chrome.sh douyin_02 9223
|
||||
```
|
||||
|
||||
### 4.2 手动登录
|
||||
|
||||
1. 浏览器窗口打开后,导航到 `https://www.douyin.com/`
|
||||
2. 使用账号 `18500003366` 手机验证码登录
|
||||
3. 注意:**抖音可能要求短信验证**,需要手机验证码
|
||||
|
||||
### 4.3 验证并导出 Cookie
|
||||
|
||||
同 [3.3](#33-验证并导出-cookie),将 `douyin_01` 替换为 `douyin_02`,`9222` 替换为 `9223`。
|
||||
|
||||
### 4.4 首次登录特别验证
|
||||
|
||||
由于是首次登录(needs_login),额外验证:
|
||||
1. 确认 Profile 目录已生成登录数据
|
||||
2. 关闭浏览器,重新用 switch_account.py 启动
|
||||
3. 确认自动登录成功
|
||||
|
||||
```bash
|
||||
# 关闭浏览器后,重新用标准化流程启动
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_02 --port 9223
|
||||
```
|
||||
|
||||
**预期**:
|
||||
```
|
||||
[4/4] 验证登录状态...
|
||||
✅ 已登录(检测到头像)
|
||||
✅ 切换完成: douyin_02
|
||||
CDP: http://localhost:9223
|
||||
登录: ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Camoufox 账号登录流程
|
||||
|
||||
**适用账号**:`douyin_camo01`、`douyin_camo02`
|
||||
|
||||
### 5.1 检查 Camoufox 环境
|
||||
|
||||
```bash
|
||||
# 检查二进制
|
||||
ls -la ~/Library/Caches/camoufox/Camoufox.app/Contents/MacOS/camoufox
|
||||
|
||||
# 检查 Python 包
|
||||
/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3 -c "import camoufox; print(camoufox.__version__)"
|
||||
```
|
||||
|
||||
### 5.2 启动 Camoufox 浏览器
|
||||
|
||||
```bash
|
||||
# 注意:Camoufox 启动方式不同于 Chrome
|
||||
# 使用 camoufox_manager 或直接运行的 camoufox_server
|
||||
/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/python3 \
|
||||
~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/camoufox_server.py \
|
||||
--account douyin_camo01
|
||||
```
|
||||
|
||||
### 5.3 手动登录
|
||||
|
||||
同 [3.2](#32-手动登录),导航到 `douyin.com` 并登录。
|
||||
|
||||
### 5.4 验证
|
||||
|
||||
Camoufox 是 Firefox 内核,验证方式:
|
||||
|
||||
```bash
|
||||
# 检查 Cookie 文件是否存在
|
||||
ls -la ~/workbuddy-agent-os/agent-local/tools/matrix/data/cookies/*camoufox*
|
||||
```
|
||||
|
||||
### 5.5 注意事项
|
||||
|
||||
- Camoufox 当前的 DB 状态显示 `active` 但浏览器未运行,这是**缓存数据失准**
|
||||
- Camoufox 登录后需在浏览器打开状态下验证
|
||||
- Camoufox 与 Chrome 的 Cookie 不互通
|
||||
|
||||
---
|
||||
|
||||
## 6. 登录验证标准流程
|
||||
|
||||
无论哪种方式登录后,三步验证:
|
||||
|
||||
### 6.1 Cookie 验证
|
||||
|
||||
```python
|
||||
# 从 DB 确认状态
|
||||
python scripts/switch_account.py --status | grep <account_id>
|
||||
```
|
||||
|
||||
### 6.2 功能验证
|
||||
|
||||
执行蓝图前 2 步,确认操作正常:
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 只测试 goto_home + wait_watch(不涉及点赞等)
|
||||
python $TOOL/scripts/task_engine.py \
|
||||
--blueprint douyin_browse_v2 \
|
||||
--account douyin_01 \
|
||||
--steps 2
|
||||
```
|
||||
|
||||
### 6.3 持久性验证
|
||||
|
||||
**关键检验**:关闭浏览器后重新启动,确认能自动登录。
|
||||
|
||||
```bash
|
||||
# 关闭浏览器
|
||||
kill -9 $(lsof -ti :9222) 2>/dev/null
|
||||
sleep 2
|
||||
|
||||
# 重新启动
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 账号切换标准流程
|
||||
|
||||
### 7.1 Profile 切换(推荐,稳定)
|
||||
|
||||
```bash
|
||||
# 从 douyin_01 切换到 douyin_02
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_02 --port 9223
|
||||
```
|
||||
|
||||
**流程**:关闭旧 Chrome → 启动新 Profile → 注入指纹 → 验证登录
|
||||
|
||||
### 7.2 Cookie 注入切换(实验性,快速)
|
||||
|
||||
```bash
|
||||
# 前提:Cookie 文件已导出且未过期
|
||||
python $TOOL/scripts/switch_account.py --method cookie --target douyin_01 --port 9222
|
||||
```
|
||||
|
||||
**注意**:Cookie 切换要求浏览器已运行。如果 Cookie 过期,自动恢复原 Cookie。
|
||||
|
||||
---
|
||||
|
||||
## 8. 故障排查表
|
||||
|
||||
### 8.1 DB 状态与实际情况对应指南
|
||||
|
||||
| DB 状态 | 实际可能性 | 操作 |
|
||||
|---------|-----------|------|
|
||||
| `active` | ✅ Cookie 有效,浏览器可运行 | 直接使用 |
|
||||
| `active` | ❌ Cookie 已过期,缓存未更新 | 执行 [2.2 实时验证](#22-实时登录验证) |
|
||||
| `cookie_expired` | ✅ 确实过期 | 执行 [3. 重登录](#3-cookie-过期账号重登录流程) |
|
||||
| `needs_login` | ✅ 从未登录过 | 执行 [4. 首次登录](#4-从未登录账号首次登录流程) |
|
||||
| `needs_login` | ⚠️ 可能已经手动登录过但 DB 未更新 | 执行 [2.2 实时验证](#22-实时登录验证) |
|
||||
| 端口无进程 | — | 执行 [2.2 实时验证](#22-实时登录验证) (自动启动浏览器) |
|
||||
|
||||
### 8.2 常见错误处理
|
||||
|
||||
| 错误 | 根因 | 修复 |
|
||||
|------|------|------|
|
||||
| `patchright 未安装` | Python venv 缺少包 | `/Users/chengzige/.workbuddy/binaries/python/envs/agent-os/bin/pip install patchright` |
|
||||
| `DouyinOps.like() got an unexpected keyword argument 'probability'` | like()/collect() API 参数不兼容 | 更新蓝图去掉 probability 参数,或用旧版 API |
|
||||
| `连接失败 / 端口无响应` | Chrome 未启动或端口错误 | 先检查端口占用、再启动 |
|
||||
| `视频加载超时` | 网络问题或页面元素变更 | 增加等待时间或检查 douyin.com 是否可访问 |
|
||||
| `SEARCH_KEYWORD 未替换` | 蓝图模板变量未配置 | 在 accounts.yaml 中为账号配置 search_keywords 字段 |
|
||||
|
||||
### 8.3 当前系统状态基线(2026-05-01)
|
||||
|
||||
| 账号 | Profile | 端口 | DB 状态 | 浏览器 | 上次活跃 |
|
||||
|------|---------|------|---------|--------|---------|
|
||||
| douyin_01 | account_01 | 9222 | cookie_expired | ⏹ 未运行 | 2026-04-30 14:50 |
|
||||
| douyin_02 | douyin_02 | 9223 | needs_login | ⏹ 未运行 | 2026-04-30 09:02 |
|
||||
| douyin_camo01 | camoufox_01 | 9301 | active(缓存) | ⏹ 未运行 | None |
|
||||
| douyin_camo02 | camoufox_02 | 9302 | active(缓存) | ⏹ 未运行 | None |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:标准化检查清单
|
||||
|
||||
每次操作前后,对照此清单确认:
|
||||
|
||||
- [ ] 前置环境检查通过(patchright、Chrome、配置)
|
||||
- [ ] 账号当前 DB 状态已记录
|
||||
- [ ] 浏览器进程状态已记录(运行/未运行)
|
||||
- [ ] 操作前备份了当前状态
|
||||
- [ ] 每一步的标准命令已执行
|
||||
- [ ] 操作后状态已重新验证
|
||||
- [ ] DB 状态已更新到最新
|
||||
- [ ] 工作日志已记录(`└ 04_memory/logs/`)
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:可用命令速查
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 查看状态
|
||||
python $TOOL/scripts/switch_account.py --list # 列出所有账号
|
||||
python $TOOL/scripts/switch_account.py --status # 查看实时状态
|
||||
|
||||
# 启动 + 验证
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
|
||||
# Cookie 管理
|
||||
python $TOOL/scripts/switch_account.py --export-cookies --target douyin_01 --port 9222
|
||||
python $TOOL/scripts/switch_account.py --method cookie --target douyin_01 --port 9222
|
||||
|
||||
# 执行蓝图
|
||||
python $TOOL/scripts/task_engine.py --blueprint douyin_browse_v2 --account douyin_01
|
||||
|
||||
# 全面测试
|
||||
python $TOOL/scripts/full_test.py --skip-launch --account-only douyin_01
|
||||
|
||||
# 手动启动浏览器
|
||||
bash $TOOL/scripts/launch_chrome.sh douyin_01 9222
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# AgentOS Matrix 反检测技术方案 v1.0.0
|
||||
|
||||
> 最后更新:2026-05-01
|
||||
> 基于旧版完整手册 + 当前系统现状分析
|
||||
|
||||
## 一、当前系统缺失的反检测措施
|
||||
|
||||
| 层级 | 缺失项 | 风险 | 旧版实施方案 |
|
||||
|------|--------|------|-------------|
|
||||
| **浏览器层** | CDP调试标记 (`--remote-debugging-port`) | 🔴 高 | 已修复:改 Playwright 原生启动 |
|
||||
| **浏览器层** | `navigator.webdriver` 未覆写 | 🔴 高 | CDP注入 `Object.defineProperty(navigator, 'webdriver', {get:()=>false})` |
|
||||
| **浏览器层** | `navigator.platform` 与 UA 不一致 | 🟠 中 | 覆写为 `Linux armv8l` (Android) 或 `iPad` |
|
||||
| **浏览器层** | `navigator.standalone` 未覆写 | 🟠 中 | 强制返回 `false` |
|
||||
| **浏览器层** | 触摸事件模拟 | 🟠 中 | `Emulation.setTouchEmulationOverride` |
|
||||
| **行为层** | 无随机动作延迟 | 🔴 高 | `action_delay: [200, 5000]ms` 随机延迟 |
|
||||
| **行为层** | 无行为画像配置 | 🟠 中 | 每小时点赞上限、评论上限、每日总量 |
|
||||
| **行为层** | 无观看时长随机化 | 🟠 中 | `view_duration: [5000, 60000]ms` |
|
||||
| **行为层** | 无打字速度模拟 | 🟠 中 | `typing_speed: [50, 200]ms/char` |
|
||||
| **网络层** | 无独立IP代理 | 🔴 高 | 静态住宅IP + socks5 代理 |
|
||||
| **网络层** | 无账号熔断机制 | 🟠 中 | 连续失败3次自动冷却30分钟 |
|
||||
| **DOM层** | App跳转拦截未完整 | 🟠 中 | 已实现 Fetch 拦截 |
|
||||
| **DOM层** | 弹窗清理选择器不全 | 🟠 中 | 需要扩展覆盖更多弹窗类型 |
|
||||
| **操作层** | 搜索后跳转到 `so.douyin.com` | 🟠 中 | 已修复:用首页搜索栏 |
|
||||
|
||||
## 二、浏览器启动配置(已修复)
|
||||
|
||||
```python
|
||||
# ✅ 正确:不使用 --remote-debugging-port
|
||||
# ✅ 正确:在 launch_persistent_context 中设置平板参数
|
||||
ctx = await pw.chromium.launch_persistent_context(
|
||||
user_data_dir=profile,
|
||||
channel="chrome",
|
||||
headless=False,
|
||||
viewport={"width": 702, "height": 783},
|
||||
device_scale_factor=2,
|
||||
is_mobile=True,
|
||||
has_touch=True,
|
||||
user_agent="Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X)...",
|
||||
locale="zh-CN",
|
||||
args=["--no-first-run", "--no-default-browser-check"],
|
||||
)
|
||||
```
|
||||
|
||||
## 三、需要新增的反检测措施
|
||||
|
||||
### 3.1 浏览器指纹覆写
|
||||
|
||||
```python
|
||||
async def apply_fingerprint_protection(page):
|
||||
"""覆写浏览器指纹特征"""
|
||||
await page.evaluate("""() => {
|
||||
// 隐藏自动化标记
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
||||
|
||||
// 平台一致性
|
||||
Object.defineProperty(navigator, 'platform', { get: () => 'iPad' });
|
||||
|
||||
// 隐藏PWA能力
|
||||
Object.defineProperty(navigator, 'standalone', { get: () => false });
|
||||
|
||||
// 修改插件检测
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => [1, 2, 3, 4, 5]
|
||||
});
|
||||
|
||||
// 修改语言
|
||||
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] });
|
||||
}""")
|
||||
```
|
||||
|
||||
### 3.2 拟人化行为参数
|
||||
|
||||
```python
|
||||
BEHAVIOR_PROFILE = {
|
||||
"douyin_01": {
|
||||
"action_delay": [0.5, 3.0], # 动作间隔秒
|
||||
"view_duration": [5, 60], # 浏览停留秒
|
||||
"typing_speed": [50, 200], # 打字 ms/字符
|
||||
"max_likes_per_hour": 15,
|
||||
"max_comments_per_hour": 3,
|
||||
"max_collects_per_hour": 5,
|
||||
"active_hours": [8, 23],
|
||||
"max_daily_actions": 150,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 账号熔断器
|
||||
|
||||
```python
|
||||
class CircuitBreaker:
|
||||
def __init__(self, account_id):
|
||||
self.failures = 0
|
||||
self.max_failures = 3
|
||||
self.cooldown = 1800 # 30分钟
|
||||
```
|
||||
|
||||
### 3.4 IP代理配置
|
||||
|
||||
需要为每个账号配置独立静态住宅 IP(当前未配置)。
|
||||
|
||||
## 四、实施优先级
|
||||
|
||||
| 优先级 | 措施 | 预估工时 |
|
||||
|--------|------|---------|
|
||||
| P0 | 浏览器指纹覆写 (webdriver/platform/standalone) | 30分钟 |
|
||||
| P0 | 随机动作延迟 (action_delay) | 30分钟 |
|
||||
| P1 | 行为画像配置 (每小时上限/每日上限) | 30分钟 |
|
||||
| P1 | 弹窗清理扩展 | 15分钟 |
|
||||
| P1 | 账号熔断器 | 30分钟 |
|
||||
| P2 | IP代理接入 | 需要外部采购 |
|
||||
| P2 | 多浏览器内核轮换 | 后续阶段 |
|
||||
|
||||
## 五、与旧版对比总结
|
||||
|
||||
旧版完整方案包含:隐身容器 (Colima+Docker) → 反检测浏览器 (Camoufox/Patchright) → 移动端伪装 → 拟人化行为 → IP代理 → 熔断机制
|
||||
|
||||
当前系统只实现了基础的浏览器控制和页面交互,**反检测层几乎全部缺失**。
|
||||
需要在后续迭代中逐步补齐。
|
||||
@@ -0,0 +1,76 @@
|
||||
# Matrix 基础操作约定 v1.0
|
||||
|
||||
> **最后更新**:2026-05-10
|
||||
> **适用范围**:所有原子操作
|
||||
|
||||
---
|
||||
|
||||
## 约定 1:窗口焦点(每次操作前必做)
|
||||
|
||||
**规则**:所有键盘操作、鼠标操作、页面交互前,必须先确保浏览器窗口在最前端。
|
||||
|
||||
**实现**:每个原子操作的 pre-check 阶段,先执行 `_activate_window()`。
|
||||
|
||||
```python
|
||||
async def _activate_window():
|
||||
"""AppleScript 将 Camoufox 窗口置顶"""
|
||||
script = 'tell application "System Events" to set frontmost of every process whose name contains "camoufox" to true'
|
||||
subprocess.run(['osascript', '-e', script], capture_output=True, timeout=3)
|
||||
```
|
||||
|
||||
这个调用放在每个原子操作执行函数的最前面。
|
||||
|
||||
---
|
||||
|
||||
## 约定 2:双击模式
|
||||
|
||||
**规则**:所有"点击进入"类操作使用双击模式。
|
||||
- 第1次点击:获取焦点 / 激活窗口 / 使元素可交互
|
||||
- 等待 1 秒
|
||||
- 第2次点击:真正的操作
|
||||
|
||||
---
|
||||
|
||||
## 约定 3:优雅关闭浏览器
|
||||
|
||||
**规则**:每次脚本启动前:
|
||||
1. 检测是否有运行的 Camoufox 进程
|
||||
2. 如有 → `pkill -f camoufox` → 等待 3 秒
|
||||
3. 删除 `.parentlock` 锁文件
|
||||
4. 再启动
|
||||
|
||||
**实现**:在 `CDPConnector.connect()` 中自动执行。
|
||||
|
||||
---
|
||||
|
||||
## 约定 4:错误报告格式
|
||||
|
||||
当任何一个原子操作失败时,**必须**按以下格式报告,不自行修复:
|
||||
|
||||
```
|
||||
❌ 操作失败报告
|
||||
═══════════════════
|
||||
当前状态: [状态码]
|
||||
当前页面: [URL]
|
||||
当前页面标题: [title]
|
||||
失败操作: [操作名]
|
||||
失败阶段: [pre/execute/post]
|
||||
失败原因: [具体错误信息/不符合的特征码]
|
||||
预期后置状态: [状态码]
|
||||
实际检测状态: [状态码]
|
||||
环境: 窗口[W×H] 位置([X],[Y]) 激活态[Y/N]
|
||||
═══════════════════
|
||||
等待指示...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 约定 5:交互测试流程
|
||||
|
||||
每个原子操作测试流程:
|
||||
1. 我启动浏览器到起始状态
|
||||
2. 我报告当前状态的特征码,你确认
|
||||
3. 你指示 "可以执行"
|
||||
4. 我执行该原子操作
|
||||
5. 我报告结果(成功/失败+特征码变化)
|
||||
6. 你指示下一步
|
||||
@@ -0,0 +1,111 @@
|
||||
# Blueprint 蓝图设计规范 v1.0.0
|
||||
|
||||
> 最后更新:2026-05-01
|
||||
> 设计目标:可组合、可验证、可追踪的脚本化操作流
|
||||
|
||||
## 一、设计原则
|
||||
|
||||
每个蓝图由若干 **原子操作步骤** 组成,每个步骤必须记录:
|
||||
|
||||
```
|
||||
前置条件 (pre-condition) → 执行操作 → 后置条件 (post-condition)
|
||||
↑ ↓
|
||||
状态验证锚点 状态验证锚点
|
||||
```
|
||||
|
||||
## 二、步骤格式
|
||||
|
||||
```json
|
||||
{
|
||||
"step_id": 1,
|
||||
"op": "goto_home",
|
||||
"args": {},
|
||||
|
||||
"pre": {
|
||||
"page_mode": ["unknown", "grid", "player", "search"],
|
||||
"login_required": false,
|
||||
"check": []
|
||||
},
|
||||
|
||||
"post": {
|
||||
"page_mode": "grid",
|
||||
"anchors": ["alink-item"],
|
||||
"video_found": false,
|
||||
"description": "到达首页卡片列表"
|
||||
},
|
||||
|
||||
"verify": {"mode": "anchor", "anchor": "grid_page", "timeout": 15},
|
||||
"on_fail": "retry(2)"
|
||||
}
|
||||
```
|
||||
|
||||
## 三、预定义页面模式
|
||||
|
||||
| 模式 | 锚点元素 | 描述 |
|
||||
|------|---------|------|
|
||||
| `unknown` | 无 | 未知/空白页 |
|
||||
| `grid` | `[data-e2e="alink-item"]` | 首页卡片列表 |
|
||||
| `player` | `[data-e2e="video-player-digg"]` | 视频播放器(带点赞/收藏按钮) |
|
||||
| `search` | `[data-e2e="searchbar-input"]` | 搜索页 |
|
||||
| `login_popup` | `[class*="login"]` 覆盖层 | 登录弹窗 |
|
||||
|
||||
## 四、预定义的原子操作
|
||||
|
||||
| 操作 | 前置条件 | 后置条件 | 说明 |
|
||||
|------|---------|---------|------|
|
||||
| `goto_home` | 任意 | `grid` | 导航到首页,激活手机模式 |
|
||||
| `search(keyword)` | `grid` | `search` | 输入关键词搜索 |
|
||||
| `open_video` | `grid` / `search` | `player` | 打开第一个找到的视频 |
|
||||
| `like` | `player` | `player`(已赞) | 点赞当前视频 |
|
||||
| `collect` | `player` | `player`(已收藏) | 收藏当前视频 |
|
||||
| `next_video` | `player` | `player`(新视频) | 切换下一条(手机模式:全局流) |
|
||||
| `comment(text)` | `player` + 登录 | `player`(已评论) | 输入评论并发送 |
|
||||
| `go_back` | `player` | `grid` / `search` | 返回上一页 |
|
||||
| `wait_watch(seconds)` | `player` | `player` | 观看一段时间 |
|
||||
| `scroll_feed` | `grid` | `grid` | 首页滚动 |
|
||||
|
||||
## 五、完整流程示例:douyin_nurture_v1
|
||||
|
||||
```
|
||||
start: unknown
|
||||
│
|
||||
├── goto_home (pre:unknown → post:grid)
|
||||
│ └── 激活手机模式:设UA→导航→刷新→等渲染
|
||||
│
|
||||
├── search(关键词) (pre:grid → post:search)
|
||||
│
|
||||
├── open_video (pre:search → post:player)
|
||||
│
|
||||
├── like (pre:player → post:player)
|
||||
├── wait_watch(8s) (pre:player → post:player)
|
||||
│
|
||||
├── next_video (pre:player → post:player) ← 手机模式全局流
|
||||
│
|
||||
├── like (pre:player → post:player)
|
||||
├── collect (pre:player → post:player)
|
||||
│
|
||||
├── go_back (pre:player → post:search)
|
||||
│
|
||||
└── open_video (pre:search → post:player)
|
||||
├── like
|
||||
├── collect
|
||||
└── next_video
|
||||
```
|
||||
|
||||
## 六、状态持久化
|
||||
|
||||
每次养号运行后,生成 `.state.json` 记录:
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "20260501_093000",
|
||||
"account": "douyin_01",
|
||||
"blueprint": "douyin_nurture_v1",
|
||||
"steps": [
|
||||
{"step": 1, "op": "goto_home", "pre": "unknown", "post": "grid", "success": true}
|
||||
],
|
||||
"login_method": "profile",
|
||||
"device_mode": "mobile_tablet",
|
||||
"duration_sec": 76
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,294 @@
|
||||
# Camoufox 登录管理方案
|
||||
|
||||
## 背景
|
||||
当前矩阵养号系统基于 Chrome + Profile 切换(方案A)与 Cookie 注入(方案B)。为提升反检测能力、支持多浏览器内核,计划引入 **Camoufox**(Firefox 内核反检测浏览器)作为新的登录管理方式。
|
||||
|
||||
## 目标
|
||||
1. 提供 Camoufox 的安装、配置指南
|
||||
2. 扩展账号配置,支持 `browser_type` 字段(`chrome` / `camoufox`)
|
||||
3. 实现 Camoufox 实例的启动、指纹注入、CDP 连接
|
||||
4. 提供登录状态验证与 Cookie 管理
|
||||
5. 演示如何添加两个新账号并尝试 Camoufox 登录
|
||||
|
||||
## 1. 安装与配置
|
||||
|
||||
### 1.1 安装 Python 包
|
||||
```bash
|
||||
# 已安装的 camoufox 包可能需要更新
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 -m pip install -U camoufox[geoip]
|
||||
```
|
||||
|
||||
### 1.2 下载 Camoufox 浏览器
|
||||
```bash
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 -m camoufox fetch
|
||||
```
|
||||
这将把浏览器可执行文件下载到 `~/Library/Caches/camoufox/`。
|
||||
|
||||
### 1.3 验证安装
|
||||
```bash
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 -m camoufox path
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 -c "import camoufox; print(camoufox.__version__)"
|
||||
```
|
||||
|
||||
## 2. 账号配置扩展
|
||||
|
||||
### 2.1 修改 `config/accounts.yaml`
|
||||
在现有配置基础上增加 `browser_type` 字段,并为 Camoufox 账号指定独立端口(建议从 9300 开始,避免与 Chrome 端口冲突)。
|
||||
|
||||
```yaml
|
||||
# 新增全局配置块
|
||||
camoufox:
|
||||
# Camoufox 可执行文件路径(自动检测)
|
||||
executable: ~/Library/Caches/camoufox/camoufox
|
||||
# 默认视口(Camoufox 使用 screen 参数)
|
||||
screen:
|
||||
width: 702
|
||||
height: 783
|
||||
# 默认地理设置
|
||||
geo:
|
||||
timezone: Asia/Shanghai
|
||||
locale: zh-CN
|
||||
|
||||
# 视口配置(Chrome 用)
|
||||
viewport:
|
||||
width: 702
|
||||
height: 783
|
||||
mobile: false
|
||||
|
||||
accounts:
|
||||
# ── 原有 Chrome 账号 ──
|
||||
- id: douyin_01
|
||||
platform: douyin
|
||||
phone: "18513308610"
|
||||
port: 9222
|
||||
browser_type: chrome
|
||||
profile_dir: account_01
|
||||
enabled: true
|
||||
notes: 抖音主号(Chrome)
|
||||
|
||||
# ── 新增 Camoufox 账号(两个) ──
|
||||
- id: douyin_camo01
|
||||
platform: douyin
|
||||
phone: "" # 待填写
|
||||
port: 9301
|
||||
browser_type: camoufox
|
||||
profile_dir: camoufox_01 # 对应 ~/matrix/profiles/camoufox_01
|
||||
proxy: socks5://127.0.0.1:7890 # 可选,推荐使用静态住宅IP
|
||||
screen: # 覆盖全局 screen 设置
|
||||
width: 702
|
||||
height: 783
|
||||
geo:
|
||||
timezone: Asia/Shanghai
|
||||
locale: zh-CN
|
||||
enabled: true
|
||||
notes: Camoufox 测试账号1
|
||||
|
||||
- id: douyin_camo02
|
||||
platform: douyin
|
||||
phone: ""
|
||||
port: 9302
|
||||
browser_type: camoufox
|
||||
profile_dir: camoufox_02
|
||||
proxy: null
|
||||
screen:
|
||||
width: 702
|
||||
height: 783
|
||||
geo:
|
||||
timezone: Asia/Shanghai
|
||||
locale: zh-CN
|
||||
enabled: true
|
||||
notes: Camoufox 测试账号2
|
||||
```
|
||||
|
||||
### 2.2 创建 Profile 目录
|
||||
```bash
|
||||
mkdir -p ~/matrix/profiles/camoufox_01
|
||||
mkdir -p ~/matrix/profiles/camoufox_02
|
||||
```
|
||||
|
||||
## 3. Camoufox 管理器脚本
|
||||
|
||||
创建 `~/matrix/scripts/camoufox_manager.py`,提供以下功能:
|
||||
|
||||
- 启动/停止 Camoufox 实例(指定端口、Profile、代理)
|
||||
- 通过 CDP 连接已启动的实例
|
||||
- 注入指纹(Camoufox 内置,无需额外注入)
|
||||
- 验证登录状态
|
||||
- 导出/导入 Cookie
|
||||
|
||||
### 3.1 脚本骨架
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Camoufox 管理器 — 启动、连接、登录验证
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from camoufox import AsyncCamoufox
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
CONFIG_PATH = PROJECT_DIR / "config" / "accounts.yaml"
|
||||
PROFILES_DIR = PROJECT_DIR / "profiles"
|
||||
COOKIE_STORAGE = PROJECT_DIR / "data" / "cookies"
|
||||
|
||||
def load_account_config(account_id: str) -> dict:
|
||||
"""从 accounts.yaml 加载指定账号配置"""
|
||||
# 实现略,参考 switch_account.py 的 get_account
|
||||
pass
|
||||
|
||||
async def launch_camoufox(account: dict) -> bool:
|
||||
"""启动 Camoufox 实例"""
|
||||
profile_dir = PROFILES_DIR / account.get('profile_dir', account['id'])
|
||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 从配置中读取 screen 和 geo
|
||||
screen = account.get('screen', {'width': 702, 'height': 783})
|
||||
geo = account.get('geo', {'timezone': 'Asia/Shanghai', 'locale': 'zh-CN'})
|
||||
proxy = account.get('proxy') # 如 "socks5://127.0.0.1:7890"
|
||||
|
||||
print(f"🚀 启动 Camoufox(账号: {account['id']},端口: {account['port']})")
|
||||
try:
|
||||
async with AsyncCamoufox(
|
||||
headless=False,
|
||||
proxy=proxy,
|
||||
user_data_dir=str(profile_dir),
|
||||
screen=screen,
|
||||
geo=geo,
|
||||
# 以下参数为 Camoufox 特有
|
||||
cdp_port=account['port'], # 指定 CDP 端口
|
||||
disable_automation=True, # 隐藏自动化标记
|
||||
disable_webdriver=True,
|
||||
disable_canvas_noise=False, # 启用 Canvas 噪声
|
||||
disable_font_fingerprinting=False, # 启用字体指纹伪造
|
||||
disable_webgl_fingerprinting=False,
|
||||
disable_audio_fingerprinting=False,
|
||||
) as browser:
|
||||
# 保持浏览器运行(实际会阻塞,需要多进程)
|
||||
# 这里我们只启动,然后通过 CDP 连接
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"❌ Camoufox 启动失败: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def connect_via_cdp(port: int):
|
||||
"""通过 CDP 连接已启动的 Camoufox 实例"""
|
||||
import urllib.request
|
||||
import json
|
||||
from patchright.async_api import async_playwright
|
||||
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with opener.open(f"http://localhost:{port}/json/version", timeout=5) as r:
|
||||
info = json.loads(r.read())
|
||||
ws_url = info["webSocketDebuggerUrl"]
|
||||
|
||||
pw = await async_playwright().start()
|
||||
try:
|
||||
# 注意:Camoufox 是 Firefox 内核,需使用 p.firefox 连接
|
||||
browser = await pw.firefox.connect_over_cdp(ws_url)
|
||||
context = browser.contexts[0]
|
||||
page = context.pages[0] if context.pages else await context.new_page()
|
||||
return page
|
||||
finally:
|
||||
await pw.stop()
|
||||
|
||||
async def verify_login(page) -> bool:
|
||||
"""验证抖音登录状态"""
|
||||
await page.goto('https://www.douyin.com/', wait_until='domcontentloaded', timeout=15000)
|
||||
await asyncio.sleep(3)
|
||||
avatar = page.locator("[data-e2e='user-avatar']")
|
||||
logged_in = await avatar.count() > 0
|
||||
return logged_in
|
||||
|
||||
async def export_cookies(account_id: str, port: int):
|
||||
"""导出 Cookie 到文件"""
|
||||
page = await connect_via_cdp(port)
|
||||
cookies = await page.context.cookies()
|
||||
cookie_file = COOKIE_STORAGE / f"{account_id}_camoufox_cookies.json"
|
||||
cookie_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(cookie_file, 'w') as f:
|
||||
json.dump(cookies, f, indent=2)
|
||||
print(f"✅ 导出 {len(cookies)} 个 Cookie → {cookie_file}")
|
||||
|
||||
async def main():
|
||||
"""CLI 入口"""
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Camoufox 管理器')
|
||||
parser.add_argument('--launch', type=str, help='启动指定账号')
|
||||
parser.add_argument('--connect', type=int, help='连接指定端口')
|
||||
parser.add_argument('--export', type=str, help='导出 Cookie(账号ID)')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.launch:
|
||||
account = load_account_config(args.launch)
|
||||
await launch_camoufox(account)
|
||||
elif args.connect:
|
||||
page = await connect_via_cdp(args.connect)
|
||||
print(f"✅ 已连接页面: {page.url}")
|
||||
elif args.export:
|
||||
account = load_account_config(args.export)
|
||||
await export_cookies(args.export, account['port'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 3.2 集成到现有切换器
|
||||
修改 `switch_account.py`,增加 `--browser` 参数,支持 `chrome`(默认)和 `camoufox`。或者,直接创建一个新的入口脚本 `switch_browser.py`,统一管理两种浏览器。
|
||||
|
||||
## 4. 登录流程
|
||||
|
||||
### 4.1 首次登录
|
||||
1. 启动 Camoufox 账号:
|
||||
```bash
|
||||
python scripts/camoufox_manager.py --launch douyin_camo01
|
||||
```
|
||||
2. 手动登录抖音(扫码/密码)。
|
||||
3. 登录后导出 Cookie:
|
||||
```bash
|
||||
python scripts/camoufox_manager.py --export douyin_camo01
|
||||
```
|
||||
4. 关闭浏览器(Ctrl+C),后续可通过 CDP 连接使用 Cookie 恢复登录。
|
||||
|
||||
### 4.2 后续切换
|
||||
使用 Cookie 注入方式快速切换(类似方案B):
|
||||
- 读取该账号的 Cookie 文件
|
||||
- 通过 CDP 注入到已启动的 Camoufox 实例
|
||||
- 刷新页面验证登录
|
||||
|
||||
## 5. 踩坑经验(预判)
|
||||
|
||||
1. **CDP 端口冲突**:Camoufox 默认 CDP 端口可能与 Chrome 冲突,建议使用 9300+ 端口范围。
|
||||
2. **Firefox CDP 协议差异**:Playwright 连接 Firefox CDP 时,部分 API 与 Chromium 不同,需测试兼容性。
|
||||
3. **代理配置**:Camoufox 的 `proxy` 参数格式需为 `socks5://ip:port` 或 `http://ip:port`。
|
||||
4. **Profile 目录权限**:确保 `~/matrix/profiles/` 目录 Camoufox 可写。
|
||||
5. **浏览器下载失败**:网络问题可能导致 `camoufox fetch` 失败,可手动下载并放置到缓存目录。
|
||||
|
||||
## 6. 后续优化方向
|
||||
|
||||
1. **Docker 容器化**:将 Camoufox 打包为 Docker 镜像,实现更彻底的隔离。
|
||||
2. **多账号并行**:使用多个 Camoufox 容器同时运行多个账号。
|
||||
3. **指纹池**:为每个账号配置不同的指纹模板(屏幕尺寸、时区、语言等)。
|
||||
4. **自动化登录**:结合短信验证码平台实现全自动登录。
|
||||
|
||||
## 7. 快速开始(两个新账号)
|
||||
|
||||
1. **编辑配置**:在 `accounts.yaml` 中添加 `douyin_camo01` 和 `douyin_camo02`(如上所示)。
|
||||
2. **创建目录**:执行 `mkdir -p ~/matrix/profiles/camoufox_{01,02}`。
|
||||
3. **启动第一个账号**:
|
||||
```bash
|
||||
cd ~/matrix
|
||||
python scripts/camoufox_manager.py --launch douyin_camo01
|
||||
```
|
||||
4. **手动登录**,然后导出 Cookie。
|
||||
5. **关闭浏览器**,测试 Cookie 注入恢复登录。
|
||||
6. **重复**步骤3-5 用于第二个账号。
|
||||
|
||||
---
|
||||
|
||||
**下一步**:确认方案后,可开始实施脚本编写与配置更新。
|
||||
@@ -0,0 +1,100 @@
|
||||
# 主页信息采集规则 (Collect Batch Runner)
|
||||
|
||||
> 适用于所有机器,联邦统一规则。
|
||||
> 脚本路径: `~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/collect_batch_runner.py`
|
||||
|
||||
---
|
||||
|
||||
## 核心规则
|
||||
|
||||
### 1. 最大并发浏览器数: 3
|
||||
|
||||
同一时间最多打开 3 个 Camoufox (Firefox) 浏览器窗口。超过会卡顿。
|
||||
|
||||
### 2. 启动间隔: 15 秒 + 窗口错位
|
||||
|
||||
同一批次内的 3 个身份,每个间隔 15 秒启动浏览器。避免同时加载导致系统卡顿。
|
||||
同时窗口 X 轴位置错开 150px,不会互相遮挡:
|
||||
|
||||
```
|
||||
身份0 → 位置 (0, 30)
|
||||
身份1 → 位置 (150, 30)
|
||||
身份2 → 位置 (300, 30)
|
||||
```
|
||||
|
||||
```
|
||||
时间线:
|
||||
0s 身份 A 启动浏览器 → 抖音 → 小红书 → 关闭
|
||||
15s 身份 B 启动浏览器 → 抖音 → 小红书 → 关闭
|
||||
30s 身份 C 启动浏览器 → 抖音 → 小红书 → 关闭
|
||||
```
|
||||
|
||||
### 3. 同身份同浏览器
|
||||
|
||||
同一个手机号身份下的两个平台(抖音 + 小红书)使用**同一个浏览器窗口**。浏览器打开后先导航到抖音读取主页,再导航到小红书读取主页,最后关闭。不换浏览器。
|
||||
|
||||
```
|
||||
身份(苏州胃肠小曹) → 打开 1 个浏览器:
|
||||
1. 导航到 douyin.com → 读取主页信息
|
||||
2. 导航到 xiaohongshu.com → 读取主页信息
|
||||
3. 关闭浏览器
|
||||
```
|
||||
|
||||
### 4. 分批执行
|
||||
|
||||
11 个身份分 4 批:
|
||||
|
||||
| 批次 | 身份数 | 预计耗时 |
|
||||
|------|--------|----------|
|
||||
| 第1批 | 3 个 | ~2 分钟 |
|
||||
| 第2批 | 3 个 | ~2 分钟 |
|
||||
| 第3批 | 3 个 | ~2 分钟 |
|
||||
| 第4批 | 2 个 | ~1.5 分钟 |
|
||||
|
||||
总计约 8-10 分钟(含浏览器启动、页面加载、间隔时间)。
|
||||
|
||||
---
|
||||
|
||||
## 数据输出
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `data/homepage_info.json` | 最终结果,含 `collected_at` 时间戳 |
|
||||
| `data/collect_progress.json` | 实时进度(供 Dashboard 轮询) |
|
||||
| `screenshots/homepage/` | 每个身份的抖音/小红书截屏 |
|
||||
|
||||
### 判断数据新旧
|
||||
|
||||
`homepage_info.json` 顶部的 `collected_at` 字段记录了本次采集时间:
|
||||
|
||||
```json
|
||||
{
|
||||
"collected_at": "2026-06-14T15:18:01",
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
Dashboard 中会根据这个时间显示相对时间("刚刚"、"3小时前"、"1天前"),
|
||||
绿色 = 刚刚采集 / 黄色 = 超过1天。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. **oMLX**(如需向量库更新):localhost:8000 运行中
|
||||
2. **账号已登录**:Camoufox 持久化 profile 中有 session cookie
|
||||
3. **Python 环境**:matrix venv(含 chromadb、numpy、playwright)
|
||||
|
||||
---
|
||||
|
||||
## 手动执行
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts
|
||||
|
||||
# 全部采集(分批并行)
|
||||
python collect_batch_runner.py
|
||||
|
||||
# 采集单个身份(顺序执行)
|
||||
python collect_homepage_info.py --single 手机号
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# 采集与展示分离方案
|
||||
|
||||
## 当前问题
|
||||
|
||||
身份与账号页面的每个平台行都有 👤 采集按钮,点击后打开浏览器实际采集。但:
|
||||
- 采集和展示耦合在一起,用户分不清是"重新读取文件"还是"重新打开浏览器"
|
||||
- 每次想看最新状态都得重新采集(打开浏览器耗时几十秒)
|
||||
- 展示数据已经在 profiles.json 中,但缺少"仅刷新展示"的入口
|
||||
|
||||
## 当前数据流
|
||||
|
||||
```
|
||||
采集(打开浏览器)
|
||||
│
|
||||
douyin_ops 执行
|
||||
│
|
||||
▼
|
||||
profiles.json
|
||||
│
|
||||
▼
|
||||
api_sms_accounts 读取
|
||||
│
|
||||
▼
|
||||
loadSmsAccounts 展示
|
||||
```
|
||||
|
||||
## 目标:采集与展示分离
|
||||
|
||||
```
|
||||
命令与任务 身份与账号
|
||||
───────────────────── ─────────────────────
|
||||
信息采集 TAB 卡片展示
|
||||
├─ 选账号(单/多/全部) ├─ 状态:登录/昵称/同步 ✅❌
|
||||
├─ 执行采集(开浏览器) ├─ 昵称名称
|
||||
└─ 查看采集状态 └─ [🔄 刷新展示] 按钮
|
||||
│
|
||||
▼
|
||||
只重新调 API,不打开浏览器
|
||||
从 profiles.json 读最新数据
|
||||
```
|
||||
|
||||
## 改动点
|
||||
|
||||
### 1. 身份与账号页面
|
||||
- 去掉每行平台的 👤 采集按钮(移到命令与任务)
|
||||
- 卡片底部保留「👤 全部采集」(批量操作)
|
||||
- 增加「🔄 刷新展示」按钮(只重调 API,不打开浏览器)
|
||||
- 增加「🔄 刷新状态」按钮在页面右上角(已有)
|
||||
|
||||
### 2. 命令与任务页面
|
||||
- 新增「信息采集」TAB(第四个 TAB)
|
||||
- 内容:账号下拉选择 + 执行采集 + 采集状态日志
|
||||
|
||||
### 3. 后端 API
|
||||
- 现有 `POST /api/matrix/accounts/{id}/collect-profile` — 保留(采集用)
|
||||
- 现有 `GET /api/matrix/sms/accounts` — 保留(展示用)
|
||||
- 不需要新增 API
|
||||
|
||||
## 好处
|
||||
- 展示秒级刷新(只读文件,不开浏览器)
|
||||
- 采集集中管理(在命令与任务统一操作)
|
||||
- 用户不混淆"刷新展示"和"重新采集"
|
||||
@@ -0,0 +1,288 @@
|
||||
# 评论发送 — 原子操作详细规格书 v1.0
|
||||
|
||||
> 版本:1.0 | 最后更新:2026-05-10
|
||||
> 状态:已通过全自动测试验证
|
||||
> 适用窗口:702×783 | 窗口位置:(652, 0)
|
||||
|
||||
---
|
||||
|
||||
## 0. 前置条件(所有操作前必检)
|
||||
|
||||
| 检查项 | 方式 | 失败处理 |
|
||||
|--------|------|---------|
|
||||
| Camoufox 进程不冲突 | `pkill -f camoufox` + 删 `.parentlock` | 重试 |
|
||||
| 窗口在最前端 | AppleScript `set frontmost` | 重试3次 |
|
||||
| 窗口大小正确 | `window.innerWidth===702 && window.innerHeight===783` | 调整后再继续 |
|
||||
| 页面已完全加载 | `document.readyState === 'complete'` | 等待3秒重试 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 原子操作:enter_video(精选页 → 视频播放页)
|
||||
|
||||
### 前置状态:H_JINGXUAN
|
||||
```
|
||||
特征码:
|
||||
- URL: douyin.com/jingxuan
|
||||
- cards: document.querySelectorAll('.discover-video-card-item').length >= 3
|
||||
- vc: document.querySelectorAll('video').length === 1
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
```
|
||||
1. _activate_window() # 窗口置前
|
||||
2. card = locator('.discover-video-card-item').first
|
||||
3. card.click(force=True) # 第1次点卡片 → 触发预览播放
|
||||
4. sleep(1.5) # 等预览播放出现
|
||||
5. vid = locator('video').first
|
||||
6. vid.click() # 第2次点 video → 进入弹窗播放器
|
||||
7. sleep(1)
|
||||
8. vid.click() # 第3次点 video → 确保播放
|
||||
9. sleep(4) # 等播放器完全加载
|
||||
```
|
||||
|
||||
### 后置状态:P_FULL
|
||||
```
|
||||
特征码:
|
||||
- vc: >= 2 (弹窗播放器有多个 video)
|
||||
- URL: douyin.com/jingxuan?modal_id=xxx (弹窗模式)
|
||||
- 或 URL: douyin.com/video/xxx (全屏模式)
|
||||
```
|
||||
|
||||
### 失败恢复
|
||||
```
|
||||
重试3次 → 每次回到 H_JINGXUAN (goto douyin.com) → 滚动画廊选不同卡片
|
||||
3次全失败 → 截图 + 报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 原子操作:open_comments(视频播放页 → 评论区打开)
|
||||
|
||||
### 前置状态:P_FULL
|
||||
```
|
||||
特征码: vc>=2 + (URL含/video/或modal_id)
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
```
|
||||
1. 前置锚点检测: [data-e2e="comment-list"] 是否存在?
|
||||
→ 已存在:跳过(评论区已开)
|
||||
→ 不存在:继续
|
||||
|
||||
2. _activate_window()
|
||||
3. vid = locator('video').first
|
||||
4. box = vid.bounding_box()
|
||||
5. mouse.click(box.center) # 点视频中心获得焦点
|
||||
6. sleep(0.5)
|
||||
7. keyboard.press('x') # x 键开评论
|
||||
8. sleep(2)
|
||||
9. 验证: [data-e2e="comment-list"] 是否存在
|
||||
10. 如果不存在 → DOM 点评论图标兜底
|
||||
```
|
||||
|
||||
### 后置状态:C_PANEL
|
||||
```
|
||||
特征码:
|
||||
- [data-e2e="comment-list"] 可见
|
||||
- 输入框尚未加载(懒加载)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2b. 原子操作:close_comments(评论区 → 关闭)
|
||||
|
||||
### 前置状态:C_PANEL
|
||||
```
|
||||
特征码: [data-e2e="comment-list"] 可见
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
```
|
||||
1. keyboard.press('Escape') 或 keyboard.press('x')
|
||||
2. sleep(1)
|
||||
3. 验证: [data-e2e="comment-list"] 不可见
|
||||
```
|
||||
|
||||
### 后置状态:P_FULL
|
||||
|
||||
---
|
||||
|
||||
## 3. 原子操作:focus_editor(评论区 → 输入框聚焦)
|
||||
|
||||
### 前置状态:C_PANEL
|
||||
```
|
||||
特征码: [data-e2e="comment-list"] 可见
|
||||
```
|
||||
|
||||
### 操作步骤(核心——模拟真人鼠标轨迹)
|
||||
```
|
||||
1. _activate_window()
|
||||
2. 目标坐标: (479, 687) — 窗口 702×783
|
||||
|
||||
3. 鼠标轨迹模拟(8步缓慢移动,总耗时约1.3s):
|
||||
for step in 0..7:
|
||||
px = 10 + (479-10) * (step+1) / 8
|
||||
py = 10 + (687-10) * (step+1) / 8
|
||||
mouse.move(px, py)
|
||||
sleep(0.1)
|
||||
|
||||
4. sleep(0.5) # 停顿让 hover 触发
|
||||
|
||||
5. mouse.click(479, 687) # 第1次单击 → 加载编辑器 + 获得焦点
|
||||
6. sleep(1) # 等待编辑器渲染
|
||||
|
||||
7. 验证: activeElement 是否是 .public-DraftEditor-content
|
||||
8. 如果没激活 → mouse.click(479, 687) 第2次 → sleep(1) → 再验证
|
||||
|
||||
9. 仍未激活 → 多点尝试:
|
||||
[(350,747), (300,747), (400,747), (250,747)]
|
||||
每个位置: mouse.move → sleep(0.3) → click → sleep(0.5) → click → sleep(0.5) → 验证
|
||||
```
|
||||
|
||||
### 后置状态:INPUT_FOCUSED
|
||||
```
|
||||
特征码:
|
||||
- document.activeElement === .public-DraftEditor-content
|
||||
- activeElement.isContentEditable === true
|
||||
- 编辑器存在(懒加载完成)
|
||||
```
|
||||
|
||||
### 关键参数
|
||||
```
|
||||
编辑器选择器: .public-DraftEditor-content
|
||||
元素类名: notranslate public-DraftEditor-content
|
||||
角色: combobox
|
||||
内容可编辑: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 原子操作:type_comment(输入框 → 文字输入)
|
||||
|
||||
### 前置状态:INPUT_FOCUSED
|
||||
```
|
||||
特征码: activeElement 是 Draft.js 编辑器
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
```
|
||||
1. osascript -e 'set the clipboard to "评论内容"' # 设置剪贴板
|
||||
2. sleep(0.3)
|
||||
3. keyboard.press('Meta+v') # Playwright Cmd+V 粘贴
|
||||
4. sleep(2) # 等待 Draft.js 处理
|
||||
5. 验证: .public-DraftEditor-content.textContent.length > 0
|
||||
```
|
||||
|
||||
### 后置状态:TEXT_ENTERED
|
||||
```
|
||||
特征码:
|
||||
- .public-DraftEditor-content.textContent.trim().length > 0
|
||||
- 内容为刚输入的文本
|
||||
```
|
||||
|
||||
### 注意
|
||||
```
|
||||
不能使用:
|
||||
- keyboard.type() → Draft.js 不处理 CDP 键盘事件
|
||||
- execCommand('insertText') → Draft.js 覆盖 DOM 更改
|
||||
- pyautogui.write() → 中文 IME 问题
|
||||
|
||||
只能用: 剪贴板粘贴(系统级复制 + 浏览器内 Cmd+V)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 原子操作:send_comment(文字已输入 → 发送)
|
||||
|
||||
### 前置状态:TEXT_ENTERED
|
||||
```
|
||||
特征码: editor 有内容
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
```
|
||||
方案A(优先): 找发送按钮
|
||||
1. 查询 button 含 text"发送"/class含"send"/"submit"/"arrow"
|
||||
2. 找到后 click
|
||||
3. 验证
|
||||
|
||||
方案B: Alt+Enter(系统级)
|
||||
1. osascript key code 36 using option down
|
||||
2. sleep(3)
|
||||
3. 验证验证码弹窗
|
||||
|
||||
方案C: Ctrl+Enter(兜底)
|
||||
1. keyboard.press('Control+Enter')
|
||||
2. sleep(1)
|
||||
3. keyboard.press('Alt+Enter')
|
||||
4. 验证
|
||||
```
|
||||
|
||||
### 后置状态:COMMENT_SENT 或 VERIFY_CODE
|
||||
```
|
||||
COMMENT_SENT:
|
||||
- [data-e2e="comment-list"] 含刚发的文本内容
|
||||
|
||||
VERIFY_CODE:
|
||||
- input[placeholder*="验证码"] 可见
|
||||
- 或 .second-verify-panel 可见
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 异常处理:验证码弹窗
|
||||
|
||||
### 触发条件
|
||||
```
|
||||
send_comment 后检测到:
|
||||
- input[placeholder*="验证码"]
|
||||
- .second-verify-panel
|
||||
```
|
||||
|
||||
### 自动处理
|
||||
```
|
||||
1. ApiSMSHandler 启动轮询(每3秒)
|
||||
- API: wx.tyhtak.com/api/biz/msg/messages
|
||||
- api_key: gtmsg2026
|
||||
- phone: 15370103682
|
||||
2. 提取最新短信中的验证码(4-6位数字)
|
||||
3. 回填到 input[placeholder*="验证码"]
|
||||
4. 点击确认按钮
|
||||
5. 超时120秒 → 手动输入兜底
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 完整蓝图链(执行顺序)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 蓝图:douyin_comment_send │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ ① launch_browser → 状态: H_JINGXUAN │
|
||||
│ ② enter_video → 状态: P_FULL │
|
||||
│ ③ open_comments → 状态: C_PANEL │
|
||||
│ ④ focus_editor → 状态: INPUT_FOCUSED │
|
||||
│ ⑤ type_comment → 状态: TEXT_ENTERED │
|
||||
│ ⑥ send_comment → 状态: COMMENT_SENT │
|
||||
│ ⑦ 如果是 VERIFY_CODE → 自动获取+回填验证码 │
|
||||
│ ⑧ goto_home → 回到 H_JINGXUAN(下一轮) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 调试信息记录格式
|
||||
|
||||
每个原子操作执行时必须记录:
|
||||
```
|
||||
[步骤名] 前置状态: X → 执行中 → 后置状态: Y | 耗时: Ns | 结果: OK/FAIL
|
||||
```
|
||||
|
||||
失败时额外记录:
|
||||
```
|
||||
当前URL: xxx
|
||||
当前vc: N
|
||||
activeElement: tag/class
|
||||
错误信息: xxx
|
||||
```
|
||||
@@ -0,0 +1,235 @@
|
||||
# 语料库架构升级方案 v2.0
|
||||
|
||||
> 作者: ghai | 日期: 2026-06-13 | 状态: 方案阶段
|
||||
|
||||
---
|
||||
|
||||
## 一、当前架构(v1.x)的问题
|
||||
|
||||
```
|
||||
语料库 = 分类 → 评论列表
|
||||
│
|
||||
├─ 赞美: ["讲得太好了", "学到了"]
|
||||
├─ 搞笑: ["哈哈哈哈哈"]
|
||||
└─ 提问: ["请问这是哪里?"]
|
||||
```
|
||||
|
||||
**局限:**
|
||||
1. 只有**态度语气**维度(赞美/搞笑/客观),没有内容维度(科技/美食/养生)
|
||||
2. 没有**人设/身份**维度(以什么"人"在说话)
|
||||
3. 没有**多轮交互**(第一次说啥、第二次说啥)
|
||||
4. 没有**故事线/IP**绑定(围绕某个话题展开)
|
||||
5. 无法表达复杂评论(带逻辑的、有结构的)
|
||||
|
||||
---
|
||||
|
||||
## 二、目标架构(v2.0)
|
||||
|
||||
### 三维语料模型
|
||||
|
||||
```
|
||||
语料 = 身份(Persona) × 场景(Scene) × 内容(Content)
|
||||
|
||||
身份(Persona): 谁在说话
|
||||
├─ 养生爱好者 → "小美养生茶" 这个号的人设
|
||||
├─ 科技极客 → 说话带专业术语
|
||||
├─ 美食探店达人 → 语气活泼,爱用表情
|
||||
└─ 情感博主 → 感性、共情
|
||||
|
||||
场景(Scene): 在什么情境下说话
|
||||
├─ 初次评论 → 第一印象
|
||||
├─ 跟帖回复 → 对别人评论的回应
|
||||
├─ 作者回复 → 博主回复粉丝
|
||||
└─ 故事线对话 → 多轮剧情互动
|
||||
|
||||
内容(Content): 具体说什么
|
||||
├─ 模板句 → "这个{keyword}太棒了"
|
||||
├─ 故事片段 → "我曾经也遇到过..."
|
||||
├─ 知识补充 → "其实{keyword}的原理是..."
|
||||
└─ IP台词 → 某个IP的经典语录
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
|
||||
```yaml
|
||||
# corpus/douyin.yaml v2.0
|
||||
version: "2.0"
|
||||
|
||||
# ── 身份配置 ──
|
||||
personas:
|
||||
health_lover:
|
||||
name: "养生爱好者"
|
||||
tags: ["养生", "健康", "中医"]
|
||||
style: "温和、专业"
|
||||
rules:
|
||||
- "不提西医西药"
|
||||
- "多用'调理'代替'治疗'"
|
||||
|
||||
tech_geek:
|
||||
name: "科技极客"
|
||||
tags: ["数码", "AI", "编程"]
|
||||
style: "理性、数据驱动"
|
||||
|
||||
# ── 场景配置 ──
|
||||
scenes:
|
||||
first_comment: # 首次评论
|
||||
label: "首次评论"
|
||||
rounds: 1 # 1轮
|
||||
rules:
|
||||
- "不要提到自己"
|
||||
- "与视频内容相关"
|
||||
|
||||
follow_up: # 跟帖回复
|
||||
label: "跟帖回复"
|
||||
rounds: 2 # 最多2轮
|
||||
rules:
|
||||
- "先认同对方观点"
|
||||
- "再补充自己的看法"
|
||||
|
||||
story_arc: # 故事线
|
||||
label: "故事线对话"
|
||||
rounds: 3-5 # 3-5轮
|
||||
rules:
|
||||
- "每轮推进剧情"
|
||||
- "保持角色一致性"
|
||||
|
||||
author_reply: # 作者回复粉丝
|
||||
label: "作者回复"
|
||||
rules:
|
||||
- "先感谢评论"
|
||||
- "针对评论内容回应"
|
||||
|
||||
# ── 内容库 ──
|
||||
content:
|
||||
health_lover:
|
||||
first_comment:
|
||||
- "这个养生方法很实用,我也在坚持"
|
||||
- "中医讲究辨证论治,不能一概而论"
|
||||
- "食疗比药疗更温和,推荐大家试试"
|
||||
- "{keyword}这个方法确实有效,亲测"
|
||||
|
||||
follow_up:
|
||||
round_1:
|
||||
- "确实如此,{keyword}我也是这么认为的"
|
||||
- "同意,不过还要注意个体差异"
|
||||
- "你说得对,我再补充一点..."
|
||||
round_2:
|
||||
- "对,我之前也遇到过同样的情况"
|
||||
- "是的,后来我换了一种方法就好了"
|
||||
|
||||
author_reply:
|
||||
- "谢谢你的评论,{keyword}确实是个好问题"
|
||||
- "感谢支持,我会继续分享更多{keyword}的内容"
|
||||
|
||||
tech_geek:
|
||||
first_comment:
|
||||
- "这个技术方案在性能上还有优化空间"
|
||||
- "从架构角度看,{keyword}这个方案更合理"
|
||||
- "数据支撑不够,建议补充A/B测试结果"
|
||||
|
||||
# ── IP/故事线 ──
|
||||
ip_stories:
|
||||
douyin_hot_series:
|
||||
name: "抖音热评系列"
|
||||
scenes:
|
||||
intro:
|
||||
- "最近{keyword}这个事我也关注了"
|
||||
- "看了好几遍,说说我的看法"
|
||||
develop:
|
||||
- "其实这个事情要从几个方面看..."
|
||||
- "很多人忽略了最关键的一点..."
|
||||
climax:
|
||||
- "最让我震撼的是..."
|
||||
- "这个案例告诉我们..."
|
||||
ending:
|
||||
- "总的来说,{keyword}给了我们很多启示"
|
||||
- "希望以后能看到更多这样的内容"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、执行链路
|
||||
|
||||
```
|
||||
用户输入:评论指令
|
||||
│
|
||||
├─ 1. 解析上下文
|
||||
│ ├─ 当前账号 → 获取 Persona(养生爱好者/科技极客)
|
||||
│ ├─ 当前场景 → 首次评论/跟帖回复/作者回复
|
||||
│ └─ 视频标题 → 提取关键词
|
||||
│
|
||||
├─ 2. 选择语料
|
||||
│ ├─ content.{persona}.{scene} → 匹配模板
|
||||
│ ├─ 关键词替换 {keyword}
|
||||
│ ├─ 多轮计数(round_1/round_2)
|
||||
│ └─ fallback → AI 生成
|
||||
│
|
||||
├─ 3. 发送评论
|
||||
│ └─ pbcopy + Meta+V + Enter
|
||||
│
|
||||
└─ 4. 记录上下文
|
||||
├─ 已发送评论内容
|
||||
├─ 当前轮次(用于多轮)
|
||||
└─ 下次回复策略
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Dashboard 语料管理
|
||||
|
||||
矩阵设置 → 语料库 页面升级:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 📚 语料库管理 │
|
||||
│ │
|
||||
│ [🎵 抖音] [📕 小红书] │
|
||||
│ │
|
||||
│ ┌─ TAB: 按身份 ───────────────────────┐ │
|
||||
│ │ 🧑 养生爱好者 🧑 科技极客 │ │
|
||||
│ │ 🧑 美食探店 🧑 情感博主 │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ TAB: 按场景 ───────────────────────┐ │
|
||||
│ │ 💬 首次评论 💬 跟帖回复 │ │
|
||||
│ │ 💬 作者回复 💬 故事线 │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ TAB: 按IP/故事线 ──────────────────┐ │
|
||||
│ │ 📖 抖音热评系列 📖 美食探店系列 │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 内容编辑区(按选中的身份+场景显示): │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ ✏️ 评论模板1 [编辑] [删除] │ │
|
||||
│ │ ✏️ 这个{keyword}方法确实有效 │ │
|
||||
│ │ ✏️ 评论模板2 [编辑] [删除] │ │
|
||||
│ │ ✏️ 中医讲究辨证论治 │ │
|
||||
│ │ [+ 新增评论] │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施路线
|
||||
|
||||
| 阶段 | 内容 | 工作量 |
|
||||
|------|------|--------|
|
||||
| Phase 1 | 语料库 YAML 格式升级(v1→v2) | 2天 |
|
||||
| Phase 2 | CorpusManager 升级(支持身份/场景/多轮) | 2天 |
|
||||
| Phase 3 | Dashboard 语料管理页面升级(三维管理) | 3天 |
|
||||
| Phase 4 | 执行链路接入(engine.py 上下文传递) | 1天 |
|
||||
| Phase 5 | 语料内容填充(用户工作) | 持续 |
|
||||
|
||||
**当前可以先开始的:** Phase 1(格式设计)+ 少量示例数据,验证可行性后再继续。
|
||||
|
||||
---
|
||||
|
||||
## 六、与当前系统的兼容
|
||||
|
||||
- 旧版 YAML 格式(v1)自动兼容,可正常读取
|
||||
- 新版格式新增字段,不破坏现有功能
|
||||
- `@corpus` 占位符继续保持可用
|
||||
- 新增 `@persona`、`@scene` 占位符支持
|
||||
- CorpusManager.get_comment_for_video() 参数不变,内部升级
|
||||
@@ -0,0 +1,96 @@
|
||||
# AgentOS 联邦系统 — 开发规则 v1.0
|
||||
|
||||
> 所有新功能/修复必须遵守以下规则,防止代码混乱。
|
||||
|
||||
---
|
||||
|
||||
## 一、代码分层与职责
|
||||
|
||||
| 层 | 文件 | 职责 | 谁写 |
|
||||
|:---|:-----|:------|:------|
|
||||
| **蓝图 JSON** | `blueprints/*.json` | **你和我交互的命名模板**,只定义参数,不写逻辑 | AI 写参数定义,你用看板填参数 |
|
||||
| **原子操作** | `douyin_ops.py` / `ops/xhs_ops.py` / `matrix_modules/account/*.py` | 被 `mc run` 引擎调用的最小操作单元 | AI 写,封装到已有类里 |
|
||||
| **引擎** | `mc/engine.py` / `mc/task.py` | 执行蓝图 + 状态机钩子(登录检测/验证处理/冷却) | AI |
|
||||
| **独立工具** | `create_identity.py` / `login_identity.py` / `publish_video.py` | CLI 可直接调的系统维护脚本 | AI |
|
||||
| **一次脚本** | `/tmp/*.py` | 临时探索/诊断,用完即删 | AI |
|
||||
| **看板视图** | `frontend/src/views/*.js` | Dashboard 前端视图 | AI |
|
||||
| **API 路由** | `routes/*.py` | 看板调用的后端接口 | AI |
|
||||
|
||||
---
|
||||
|
||||
## 二、三原则(必须遵守)
|
||||
|
||||
### 原则 1:功能加到已有类里,不建新文件
|
||||
|
||||
- 抖音的新操作 → 加 `DouyinOps` 类(`douyin_ops.py`)
|
||||
- 小红书的新操作 → 加 `XhsOps` 类(`ops/xhs_ops.py`)
|
||||
- SMS 登录 → 已有 `sms_login.py`,改它
|
||||
- 登录状态机 → 已有 `login_state_machine.py`
|
||||
|
||||
**禁止**:为每个小功能创建独立脚本。除非它是一个**完整的、可独立 CLI 调用的工具**(如 `create_identity.py`)。
|
||||
|
||||
### 原则 2:蓝图只定义参数,不写执行逻辑
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "douyin_daily",
|
||||
"name": "抖音-日常养号",
|
||||
"params": { "duration": {"type": "int", "default": 600} },
|
||||
"steps": [{"step_id": 1, "op": "goto_home", "args": {}}]
|
||||
}
|
||||
```
|
||||
|
||||
- 蓝图文件只描述「有什么参数」「分几步」
|
||||
- 真正的执行逻辑在 `mc/engine.py` + 原子操作里
|
||||
|
||||
**禁止**:在蓝图 JSON 里写条件/循环/异常处理。
|
||||
|
||||
### 原则 3:`/tmp/*.py` 用完即删
|
||||
|
||||
- `/tmp/douyin_login_explore.py` — 用完即删
|
||||
- `/tmp/test_xxx.py` — 用完即删
|
||||
|
||||
**禁止**:把 `/tmp/` 脚本作为正式功能提交到 Git。
|
||||
|
||||
---
|
||||
|
||||
## 三、新增功能的标准化流程
|
||||
|
||||
```
|
||||
你提需求 → 我评估归属层 → 写入对应文件 → 你测试 → 我修正
|
||||
|
||||
例: 抖音SMS登录
|
||||
你: "douyin_133 登录过期了, 需要SMS重新登录"
|
||||
我: 归属 → DouyinOps 类
|
||||
→ douyin_ops.py 加 sms_login() 方法
|
||||
→ 调已有 sms_login.py 的 SMS 轮询逻辑
|
||||
→ 蓝图的 needs_login 字段自动触发
|
||||
→ 不建新文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、什么算"独立工具"
|
||||
|
||||
只有满足以下**全部**条件的才可以建独立脚本:
|
||||
|
||||
1. ✅ 可从 CLI 直接调用(有 `main()` + `argparse`)
|
||||
2. ✅ 不依赖 `mc run` 引擎
|
||||
3. ✅ 功能完整闭环(如 `create_identity.py` 创建身份→生成目录→写配置)
|
||||
4. ❌ 如果只是被引擎或原子操作调用的功能,必须写在对应类里
|
||||
|
||||
---
|
||||
|
||||
## 五、历史遗留处理
|
||||
|
||||
| 文件 | 状态 | 原因 |
|
||||
|:-----|:------|:------|
|
||||
| `collect_batch_runner.py` | ✅ 已归档 | 功能已被蓝图 `douyin_read_profile` 替代 |
|
||||
| `collect_homepage_info.py` | ✅ 已归档 | 同上 |
|
||||
| `matrix.py` | ✅ 已归档 | 功能被 `mc` CLI 替代 |
|
||||
| `ops/douyin/` | ✅ 已归档 | 功能被 `douyin_ops.py` 替代 |
|
||||
| `atom_ops.py` | ✅ 已归档 | 废弃 |
|
||||
| `yanghao_runner.py` | ✅ 已归档 | 废弃 |
|
||||
| `step_by_step_v*.py` | ✅ 已归档 | 废弃 |
|
||||
|
||||
**新功能必须直接写入活跃文件,不建新独立脚本。**
|
||||
@@ -0,0 +1,285 @@
|
||||
# 抖音 Web 端选择器手册
|
||||
|
||||
> 扫描时间:2026-04-27 | 浏览器窗口:702×681 | CDP 端口:9222
|
||||
> 连接方式:Patchright `connect_over_cdp` | 代理需绕过:`no_proxy=localhost,127.0.0.1`
|
||||
|
||||
---
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
| 键 | 功能 | 备注 |
|
||||
|----|------|------|
|
||||
| **Z** | 点赞 | 需英文输入法 |
|
||||
| **X** | 打开/关闭评论 | |
|
||||
| **F** | 关注作者 / 进直播间 | |
|
||||
| **B** | 弹幕开关 | |
|
||||
| **空格** | 播放/暂停 | |
|
||||
| **↑** | 上一个视频 | |
|
||||
| **↓** | 下一个视频 | |
|
||||
| **Enter** | 搜索确认 / 评论发送 | |
|
||||
|
||||
---
|
||||
|
||||
## 全局元素
|
||||
|
||||
| 元素 | 选择器 | 备注 |
|
||||
|------|--------|------|
|
||||
| 搜索框 | `[data-e2e="searchbar-input"]` | type=input |
|
||||
| 搜索按钮 | `[data-e2e="searchbar-button"]` | |
|
||||
| 右上角头像 | `[data-e2e="live-avatar"]` | hover 弹出菜单 |
|
||||
| 左侧导航 | `[data-e2e="douyin-navigation"]` | |
|
||||
|
||||
### 左侧导航栏
|
||||
|
||||
| 项目 | 大致位置(y) |
|
||||
|------|-----------|
|
||||
| 精选 | 64 |
|
||||
| 推荐 | 124 |
|
||||
| 搜索 | 184 |
|
||||
| 关注 | 244 |
|
||||
| 朋友 | 304 |
|
||||
| 我的 | 364 |
|
||||
| 直播 | 441 |
|
||||
| 放映厅 | 501 |
|
||||
| 短剧 | 561 |
|
||||
|
||||
---
|
||||
|
||||
## 视频播放页(推荐/关注)
|
||||
|
||||
### 互动按钮
|
||||
|
||||
| 操作 | 键盘 | 选择器 | 状态判断 |
|
||||
|------|------|--------|---------|
|
||||
| 点赞 | **Z** | `[data-e2e="video-player-digg"]` | `data-e2e-state`: `video-player-no-digged`(未赞) / `video-player-digged`(已赞) |
|
||||
| 评论(打开) | **X** | `[data-e2e="feed-comment-icon"]` | |
|
||||
| 收藏 | - | `[data-e2e="video-player-collect"]` | |
|
||||
| 分享 | - | `[data-e2e="video-player-share"]` | |
|
||||
| 关注/进直播 | **F** | `[data-e2e="feed-follow-icon"]` | |
|
||||
| 更多 | - | `[data-e2e="video-play-more"]` | |
|
||||
|
||||
### 翻页
|
||||
|
||||
| 操作 | 键盘 | 选择器 |
|
||||
|------|------|--------|
|
||||
| 上翻视频 | **↑** | `[data-e2e="video-switch-prev-arrow"]` |
|
||||
| 下翻视频 | **↓** | `[data-e2e="video-switch-next-arrow"]` |
|
||||
|
||||
### 播放控制
|
||||
|
||||
| 操作 | 键盘 | 备注 |
|
||||
|------|------|------|
|
||||
| 播放/暂停 | **空格** | |
|
||||
| 弹幕开关 | **B** | |
|
||||
|
||||
---
|
||||
|
||||
## 评论区
|
||||
|
||||
> 按 X 或点击评论图标后,右侧弹出评论面板,视频从 570px 缩窄到 234px
|
||||
|
||||
| 元素 | 选择器 | 备注 |
|
||||
|------|--------|------|
|
||||
| 评论列表 | `[data-e2e="comment-list"]` | |
|
||||
| 评论输入框 | `.public-DraftEditor-content` | Draft.js 富文本编辑器,contentEditable |
|
||||
| 输入容器(聚焦态) | `.comment-input-container-focus` | 激活后添加此 class |
|
||||
| 发送按钮 | `.commentInput-right-ct .WFB7wUOX` | 向上箭头图标 |
|
||||
| 表情按钮 | `.commentInput-right-ct .BVMl8WNl` 第2个 | |
|
||||
| 图片上传 | `.commentInput-right-ct .BVMl8WNl` 第1个 | |
|
||||
| @提及 | `.commentInput-right-ct .BVMl8WNl` 第3个 | |
|
||||
| 评论更多 | `[data-e2e="video-comment-more"]` | |
|
||||
| 弹幕输入框 | `input[placeholder="发一条弹幕吧"]` | 非评论框,注意区分 |
|
||||
|
||||
### 评论操作流程
|
||||
|
||||
```
|
||||
1. 按 X 打开评论面板
|
||||
2. 点击 .public-DraftEditor-content 激活输入
|
||||
3. 输入文字(type_into Draft.js 编辑器)
|
||||
4. Enter 发送 / 点击发送按钮
|
||||
5. 可能触发验证码弹窗
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 搜索结果页
|
||||
|
||||
**URL 格式**:`https://www.douyin.com/root/search/{关键词}?type=general`
|
||||
|
||||
### Tab 分类栏
|
||||
|
||||
| Tab | 选择器 | 备注 |
|
||||
|-----|--------|------|
|
||||
| 综合(激活) | `.NiqGqBbw` | 不同 class 表示激活态 |
|
||||
| 视频/用户/直播 | `.t3OsOj2N` | |
|
||||
|
||||
### 子分类筛选
|
||||
|
||||
| 选择器 | 示例 |
|
||||
|--------|------|
|
||||
| `.neT9xRMd.vZeqhI2r` | 直播间、胃肠镜、中医院… |
|
||||
| 激活态额外 class | `.Bp4xnVjA` |
|
||||
|
||||
### 显示模式
|
||||
|
||||
| 模式 | 选择器 |
|
||||
|------|--------|
|
||||
| 多列 | `.cSG1ckL8` |
|
||||
| 单列 | `.cSG1ckL8.ltQEgur_` |
|
||||
|
||||
### 结果卡片
|
||||
|
||||
| 元素 | 选择器 | 备注 |
|
||||
|------|--------|------|
|
||||
| 卡片容器 | `.search-result-card` | 整卡可点击(cursor:pointer),无 `<a>` 标签 |
|
||||
| 视频封面 | `.videoImage` | 219×292 |
|
||||
| 标题 | `.wSEoiOKC` | |
|
||||
| 作者 | `.YebDknhI` | @username |
|
||||
| 点赞数 | `.LUd60iMQ` | |
|
||||
| 布局 | 3列网格 | x=31, x=276, x=521 |
|
||||
|
||||
### 点击第 N 个搜索结果
|
||||
|
||||
```python
|
||||
cards = page.query_selector_all('.search-result-card')
|
||||
cards[0].click() # 第1个
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关注页
|
||||
|
||||
**URL**:`https://www.douyin.com/follow`
|
||||
|
||||
### 左侧关注列表
|
||||
|
||||
| 元素 | 选择器 | 备注 |
|
||||
|------|--------|------|
|
||||
| 列表头 | `.mZ1rdREf` | "列表综合排序" |
|
||||
| 我的关注 | `.IMOte1q1.mZ1rdREf` | "我的关注(N)" |
|
||||
| 关注用户项 | `.ILK2RAD5` | 点击切换到该用户视频 |
|
||||
|
||||
### 右侧视频流
|
||||
|
||||
与推荐页结构相同,互动按钮选择器通用。
|
||||
|
||||
---
|
||||
|
||||
## 个人主页
|
||||
|
||||
**URL**:`https://www.douyin.com/user/self?showTab=post`
|
||||
|
||||
### 用户信息
|
||||
|
||||
| 元素 | data-e2e |
|
||||
|------|----------|
|
||||
| 用户详情 | `user-detail` |
|
||||
| 用户信息 | `user-info` |
|
||||
| 关注数 | `user-info-follow` |
|
||||
| 粉丝数 | `user-info-fans` |
|
||||
| 获赞数 | `user-info-like` |
|
||||
|
||||
### Tab 栏
|
||||
|
||||
| Tab | 位置(y) |
|
||||
|-----|---------|
|
||||
| 作品 | 221 |
|
||||
| 推荐 | 221 |
|
||||
| 喜欢 | 221 |
|
||||
| 收藏 | 221 |
|
||||
| 观看历史 | 221 |
|
||||
| 稍后再看 | 221 |
|
||||
| 我的预约 | 221 |
|
||||
| AI 笔记 | 221 |
|
||||
|
||||
### 作品列表
|
||||
|
||||
| 元素 | data-e2e |
|
||||
|------|----------|
|
||||
| 作品列表 | `user-post-list` |
|
||||
| 滚动列表 | `scroll-list` |
|
||||
|
||||
---
|
||||
|
||||
## 头像悬停菜单
|
||||
|
||||
**触发**:hover `[data-e2e="live-avatar"]`
|
||||
|
||||
**面板**:`.userMenuPanelShadowAnimation`(x≈360, 宽328)
|
||||
|
||||
| 菜单项 | 选择器 | 链接 |
|
||||
|--------|--------|------|
|
||||
| 用户名 | `.uz1VJwFY.e6huIECy` | → 个人主页 |
|
||||
| 我的喜欢 | `.uz1VJwFY.espXX7re` 第1个 | → 喜欢 |
|
||||
| 我的收藏 | `.uz1VJwFY.espXX7re` 第2个 | → 收藏 |
|
||||
| 观看历史 | `.uz1VJwFY.espXX7re` 第3个 | → 历史 |
|
||||
| 稍后再看 | `.uz1VJwFY.espXX7re` 第4个 | → 稍后再看 |
|
||||
| 我的作品 | `.uz1VJwFY.espXX7re` 第5个 | → 作品 |
|
||||
| 我的订单 | `.uz1VJwFY.VSHeqy7Q` | 无链接 |
|
||||
|
||||
---
|
||||
|
||||
## 验证码弹窗
|
||||
|
||||
> 评论后可能触发短信验证码
|
||||
|
||||
| 元素 | 选择器 | 备注 |
|
||||
|------|--------|------|
|
||||
| 遮罩 | `.second-verify-mask` | 全屏遮罩 |
|
||||
| 面板 | `.second-verify-panel` | |
|
||||
| 标题 | `.uc-ui-verify-new_header-title` | "接收短信验证码" |
|
||||
| 关闭 | `.uc-ui-verify-new_header-close` | |
|
||||
| 提示 | `.uc-ui-verify_sms-verify_content_desc` | 手机号脱敏 |
|
||||
| 输入框 | `.uc-ui-verify_sms-verify_input` | |
|
||||
| 获取验证码 | 面板内按钮 | |
|
||||
| 验证按钮 | `.uc-ui-verify_sms-verify_button.primary` | 非 disabled 时可点 |
|
||||
| 取消按钮 | `.uc-ui-verify_sms-verify_button.second` | |
|
||||
|
||||
---
|
||||
|
||||
## 技术备忘
|
||||
|
||||
### CDP 连接
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ['no_proxy'] = 'localhost,127.0.0.1'
|
||||
os.environ['NO_PROXY'] = 'localhost,127.0.0.1'
|
||||
|
||||
from patchright.async_api import async_playwright
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.connect_over_cdp('http://localhost:9222')
|
||||
context = browser.contexts[0]
|
||||
page = context.pages[0]
|
||||
```
|
||||
|
||||
### Draft.js 输入
|
||||
|
||||
评论框是 Draft.js 富文本编辑器,不能用普通 `fill()`,需用 `type_into()` 或 `press_sequentially()`:
|
||||
|
||||
```python
|
||||
editor = page.locator('.public-DraftEditor-content')
|
||||
await editor.click()
|
||||
await editor.press_sequentially('评论内容')
|
||||
```
|
||||
|
||||
### 起始页固定
|
||||
|
||||
每次操作从固定 URL 开始:`https://www.douyin.com/?recommend=1`
|
||||
|
||||
### SVG className
|
||||
|
||||
SVG 元素的 `className` 是 `SVGAnimatedString` 对象,非字符串。JS 中需用 `el.className.baseVal`。
|
||||
|
||||
### 点赞状态判断
|
||||
|
||||
```python
|
||||
digg = page.locator('[data-e2e="video-player-digg"]')
|
||||
state = await digg.get_attribute('data-e2e-state')
|
||||
# "video-player-no-digged" = 未点赞
|
||||
# "video-player-digged" = 已点赞
|
||||
```
|
||||
|
||||
### 搜索卡片无 `<a>` 标签
|
||||
|
||||
搜索结果卡片通过 JS 事件跳转,需用 `card.click()` 而非导航链接。
|
||||
@@ -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 固化指纹 + 固定代理 = 当前最优方案。
|
||||
@@ -0,0 +1,420 @@
|
||||
# 本地化适配说明
|
||||
|
||||
**基于当前环境的落地调整方案**
|
||||
**当前机器**:macOS 26.4,Apple M1,8GB RAM,约 164GB 可用
|
||||
**文档日期**:2026-04-27
|
||||
|
||||
---
|
||||
|
||||
## 一、环境现状扫描结果
|
||||
|
||||
| 检查项 | 手册要求 | 当前状态 | 差距 |
|
||||
|:---|:---|:---|:---|
|
||||
| **Homebrew** | 用于安装 Colima/Docker | ❌ 未安装 | 需先装 Homebrew |
|
||||
| **Colima** | 容器运行时 | ❌ 未安装 | 依赖 Homebrew |
|
||||
| **Docker** | 容器管理 | ❌ 未安装 | 依赖 Homebrew |
|
||||
| **Python** | 3.10+(脚本运行) | ✅ 3.13.12(managed) | 已满足 |
|
||||
| **Playwright** | 登录脚本(过渡期) | ✅ 已安装 | 已满足 |
|
||||
| **Camoufox** | 主力反检测浏览器 | ❌ 未安装 | pip install |
|
||||
| **Patchright** | 备用反检测浏览器 | ❌ 未安装 | pip install |
|
||||
| **MLX** | 视觉定位后备 | ❌ 未安装 | pip install |
|
||||
| **Chrome** | 真实浏览器(CDP 直连) | ❌ 未安装 | 需手动安装 |
|
||||
| **代理IP** | 每账号独立静态住宅IP | ❌ 未配置 | 需向服务商购买 |
|
||||
| **账号** | 5-6 个养号账号 | ⚠️ 仅确认1个手机号 | 需补充 |
|
||||
|
||||
---
|
||||
|
||||
## 二、关键技术路径调整
|
||||
|
||||
### 2.1 Docker 方案的现实问题
|
||||
|
||||
手册的 Docker + Colima 方案在当前机器上有以下制约:
|
||||
|
||||
| 问题 | 说明 | 影响 |
|
||||
|:---|:---|:---|
|
||||
| **无 Homebrew** | Colima/Docker 需要 Homebrew 安装 | 安装路径变长 |
|
||||
| **8GB RAM** | 5个 Docker 容器 + 宿主系统,8GB 会吃紧 | 同时运行容器数有上限 |
|
||||
| **Camoufox ARM 适配** | Camoufox 的 ARM64 Docker 镜像需要编译,社区还不稳定 | 有编译失败风险 |
|
||||
| **无 X Display** | macOS Docker 容器跑有头浏览器需要 XQuartz,配置麻烦 | 增加配置难度 |
|
||||
|
||||
### 2.2 推荐:分两阶段走
|
||||
|
||||
```
|
||||
阶段 A(现在,1-2周):CDP 直连本机真实 Chrome
|
||||
↓ 先跑通核心逻辑(登录→养号→蓝图)
|
||||
阶段 B(稳定后):切换 Colima + Camoufox/Patchright 容器
|
||||
↓ 上完整反检测方案,扩规模
|
||||
```
|
||||
|
||||
**阶段 A 的优点**:
|
||||
- 立刻可以开始,不依赖 Docker/Brew
|
||||
- 真实 Chrome 无指纹问题
|
||||
- 人工登录流程最自然(你直接在 Chrome 里操作)
|
||||
|
||||
**阶段 A 的限制**:
|
||||
- Chrome 指纹是真实的(反检测弱),适合初期少量账号
|
||||
- 每次只能跑 1 个 Chrome 实例(不支持多账号并发)
|
||||
|
||||
---
|
||||
|
||||
## 三、阶段 A:CDP 直连 Chrome 方案(当前可立即执行)
|
||||
|
||||
### 3.1 整体架构
|
||||
|
||||
```
|
||||
你的 Mac(本机)
|
||||
│
|
||||
├── Chrome 实例 1(账号A)
|
||||
│ ├── 调试端口 9222
|
||||
│ └── 独立 User Data Dir:~/matrix/profiles/account_01/
|
||||
│
|
||||
├── Chrome 实例 2(账号B)
|
||||
│ ├── 调试端口 9223
|
||||
│ └── 独立 User Data Dir:~/matrix/profiles/account_02/
|
||||
│
|
||||
└── Python 调度器
|
||||
└── 通过 CDP connect_over_cdp() 连接各 Chrome 实例
|
||||
```
|
||||
|
||||
### 3.2 安装步骤(顺序执行)
|
||||
|
||||
```bash
|
||||
# Step 1: 安装 Chrome
|
||||
# 从 https://www.google.com/chrome/ 下载,安装到 /Applications/
|
||||
|
||||
# Step 2: 安装 Python 依赖
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 \
|
||||
-m pip install patchright camoufox aiohttp aiosqlite
|
||||
|
||||
# Step 3: 安装 Patchright 浏览器核心
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 \
|
||||
-m patchright install chromium
|
||||
|
||||
# Step 4: 验证
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 \
|
||||
-c "from patchright.sync_api import sync_playwright; print('OK')"
|
||||
```
|
||||
|
||||
### 3.3 启动 Chrome 的标准脚本
|
||||
|
||||
创建 `~/matrix/scripts/launch_chrome.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 用法: ./launch_chrome.sh <账号ID> <调试端口>
|
||||
# 示例: ./launch_chrome.sh account_01 9222
|
||||
|
||||
ACCOUNT_ID=${1:-account_01}
|
||||
DEBUG_PORT=${2:-9222}
|
||||
PROFILE_DIR="$HOME/matrix/profiles/$ACCOUNT_ID"
|
||||
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
|
||||
mkdir -p "$PROFILE_DIR"
|
||||
|
||||
"$CHROME" \
|
||||
--remote-debugging-port="$DEBUG_PORT" \
|
||||
--user-data-dir="$PROFILE_DIR" \
|
||||
--window-size=390,844 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--disable-extensions \
|
||||
2>/dev/null &
|
||||
|
||||
echo "Chrome 已启动,账号: $ACCOUNT_ID,调试端口: $DEBUG_PORT"
|
||||
echo "验证: curl http://localhost:$DEBUG_PORT/json/version"
|
||||
```
|
||||
|
||||
### 3.4 核心 CDP 连接器
|
||||
|
||||
文件:`~/matrix/scripts/cdp_connector.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CDP 连接器 - 连接本机 Chrome 实例
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from patchright.async_api import async_playwright
|
||||
|
||||
class CDPConnector:
|
||||
def __init__(self, port: int = 9222):
|
||||
self.port = port
|
||||
self.browser = None
|
||||
self.context = None
|
||||
self.page = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接到已运行的 Chrome"""
|
||||
p = await async_playwright().start()
|
||||
self.browser = await p.chromium.connect_over_cdp(
|
||||
f"http://localhost:{self.port}"
|
||||
)
|
||||
self.context = self.browser.contexts[0]
|
||||
|
||||
if self.context.pages:
|
||||
self.page = self.context.pages[0]
|
||||
else:
|
||||
self.page = await self.context.new_page()
|
||||
|
||||
print(f"已连接 Chrome(端口 {self.port}),页面数:{len(self.context.pages)}")
|
||||
return self.page
|
||||
|
||||
async def new_page(self):
|
||||
"""新建标签页"""
|
||||
self.page = await self.context.new_page()
|
||||
return self.page
|
||||
|
||||
async def goto(self, url: str, wait_until: str = "networkidle"):
|
||||
"""导航到 URL"""
|
||||
await self.page.goto(url, wait_until=wait_until, timeout=30000)
|
||||
return self.page
|
||||
|
||||
async def setup_anti_jump(self):
|
||||
"""设置 App 跳转拦截"""
|
||||
cdp = await self.context.new_cdp_session(self.page)
|
||||
|
||||
blocked_schemes = [
|
||||
"xhdsdiscover://*", "snssdk1128://*", "snssdk1233://*",
|
||||
"kuaishou://*", "zhihu://*", "weixin://*",
|
||||
"alipays://*", "taobao://*", "intent://*",
|
||||
]
|
||||
|
||||
await cdp.send("Fetch.enable", {
|
||||
"patterns": [
|
||||
{"urlPattern": s, "requestStage": "Request"}
|
||||
for s in blocked_schemes
|
||||
]
|
||||
})
|
||||
|
||||
async def handle_paused(event):
|
||||
try:
|
||||
await cdp.send("Fetch.failRequest", {
|
||||
"requestId": event["requestId"],
|
||||
"errorReason": "Aborted"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
cdp.on("Fetch.requestPaused", handle_paused)
|
||||
return cdp
|
||||
|
||||
async def setup_mobile_viewport(self, cdp_session):
|
||||
"""设置移动端视口(iPhone 15 Pro)"""
|
||||
await cdp_session.send("Emulation.setDeviceMetricsOverride", {
|
||||
"width": 390, "height": 844,
|
||||
"deviceScaleFactor": 3, "mobile": True,
|
||||
"screenOrientation": {"type": "portrait", "angle": 0}
|
||||
})
|
||||
await cdp_session.send("Emulation.setTouchEmulationOverride", {
|
||||
"enabled": True, "maxTouchPoints": 5
|
||||
})
|
||||
|
||||
async def remove_overlays(self):
|
||||
"""清理 App 跳转弹窗"""
|
||||
script = """
|
||||
() => {
|
||||
const selectors = [
|
||||
'[class*="download"]','[class*="open-app"]','[class*="app-guide"]',
|
||||
'.open-in-app','.app-launch-mask','.download-tip','.bottom-bar',
|
||||
'.download-banner','.open-app-btn','.app-download-tip',
|
||||
'.open-app-layer','.download-guide-mask',
|
||||
'.open-in-app-bar','.app-open-button',
|
||||
'#app-launch-dialog','#open-app-modal',
|
||||
];
|
||||
let count = 0;
|
||||
selectors.forEach(s => {
|
||||
document.querySelectorAll(s).forEach(el => { el.remove(); count++; });
|
||||
});
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.overflowY = 'auto';
|
||||
return count;
|
||||
}
|
||||
"""
|
||||
removed = await self.page.evaluate(script)
|
||||
if removed > 0:
|
||||
print(f" 已清理 {removed} 个弹窗/遮罩")
|
||||
return removed
|
||||
|
||||
|
||||
# 使用示例
|
||||
async def main():
|
||||
conn = CDPConnector(port=9222)
|
||||
page = await conn.connect()
|
||||
cdp = await conn.setup_anti_jump()
|
||||
await conn.setup_mobile_viewport(cdp)
|
||||
|
||||
await page.goto("https://www.douyin.com")
|
||||
await asyncio.sleep(2)
|
||||
await conn.remove_overlays()
|
||||
|
||||
print("当前URL:", page.url)
|
||||
print("页面标题:", await page.title())
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 3.5 登录流程(阶段 A)
|
||||
|
||||
```
|
||||
1. 运行 launch_chrome.sh account_01 9222
|
||||
2. Chrome 弹出(390×844 手机比例)
|
||||
3. 你在 Chrome 里手动导航到各平台,手机号登录
|
||||
4. 登录成功后,Cookie 自动保存在 ~/matrix/profiles/account_01/
|
||||
5. 关闭 Chrome 窗口
|
||||
6. 下次用同一个 profile_dir 启动,Cookie 自动复用
|
||||
```
|
||||
|
||||
### 3.6 阶段 A 的内存估算
|
||||
|
||||
| 组件 | 内存占用 |
|
||||
|:---|:---|
|
||||
| macOS 系统 | ~3GB |
|
||||
| Chrome 实例 × 2(同时跑) | ~1.5GB × 2 = 3GB |
|
||||
| Python 调度器 | ~200MB |
|
||||
| **总计** | **~6.2GB(8GB 机器刚好可以跑 2 个实例)** |
|
||||
|
||||
**结论**:阶段 A 最多同时运行 2 个账号,轮换操作其他账号。
|
||||
|
||||
---
|
||||
|
||||
## 四、阶段 B:Colima + 容器方案(扩展期)
|
||||
|
||||
### 4.1 安装 Homebrew(先决条件)
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# Apple Silicon 需要添加到 PATH
|
||||
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
```
|
||||
|
||||
### 4.2 安装 Colima + Docker
|
||||
|
||||
```bash
|
||||
brew install colima docker docker-compose
|
||||
|
||||
# 启动(给足资源,8GB 机器建议 6GB 给 Colima)
|
||||
colima start --cpu 3 --memory 6 --disk 50 --arch aarch64 --vm-type vz
|
||||
|
||||
# 验证
|
||||
docker ps
|
||||
```
|
||||
|
||||
### 4.3 8GB 机器的容器资源分配建议
|
||||
|
||||
| 资源 | Colima VM | 宿主系统(macOS) |
|
||||
|:---|:---|:---|
|
||||
| CPU | 3核 | 1核(低负载) |
|
||||
| 内存 | 6GB | 2GB(保底) |
|
||||
| 磁盘 | 50GB | 剩余 |
|
||||
|
||||
**每容器内存限制**(避免 OOM):
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml 加上限制
|
||||
services:
|
||||
account_01:
|
||||
mem_limit: 1.5g
|
||||
memswap_limit: 2g
|
||||
```
|
||||
|
||||
**8GB 机器最多稳定运行 3 个容器**(3 × 1.5GB = 4.5GB,留 1.5GB 给系统开销)
|
||||
|
||||
### 4.4 Camoufox ARM64 适配
|
||||
|
||||
Camoufox 官方 Docker 镜像以 x86 为主,ARM 需要特殊处理:
|
||||
|
||||
```dockerfile
|
||||
# 使用官方提供的 ARM 兼容镜像(如有),或者用 x86 + Rosetta 模拟
|
||||
FROM --platform=linux/amd64 ubuntu:22.04
|
||||
```
|
||||
|
||||
在 colima 启动时加 Rosetta:
|
||||
```bash
|
||||
colima start --cpu 3 --memory 6 --disk 50 --arch x86_64 --vm-type vz --vz-rosetta
|
||||
```
|
||||
|
||||
> **注意**:Rosetta 模拟会有约 20-30% 的性能损耗,但换来更好的兼容性。
|
||||
|
||||
---
|
||||
|
||||
## 五、现在可以立即做的事(今天)
|
||||
|
||||
按优先级排序:
|
||||
|
||||
### P0:安装 Chrome(5 分钟)
|
||||
从 [google.com/chrome](https://www.google.com/chrome/) 下载,安装到 `/Applications/`
|
||||
|
||||
### P1:测试 CDP 连接(30 分钟)
|
||||
|
||||
```bash
|
||||
# 启动 Chrome(带调试端口)
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
||||
--remote-debugging-port=9222 \
|
||||
--user-data-dir="$HOME/matrix/profiles/test" \
|
||||
--window-size=390,844 &
|
||||
|
||||
# 验证 CDP 可用
|
||||
curl http://localhost:9222/json/version
|
||||
|
||||
# 运行连接测试
|
||||
/Users/7kecheng/.workbuddy/binaries/python/versions/3.13.12/bin/python3 \
|
||||
~/matrix/scripts/cdp_connector.py
|
||||
```
|
||||
|
||||
### P2:登录第一个账号(1 小时)
|
||||
|
||||
```bash
|
||||
# 启动账号 account_01 的 Chrome
|
||||
bash ~/matrix/scripts/launch_chrome.sh account_01 9222
|
||||
|
||||
# 在弹出的 Chrome 里手动登录抖音(手机号 18513308610)
|
||||
# 成功后关闭 Chrome
|
||||
|
||||
# 重新启动,验证 Cookie 是否持久化
|
||||
bash ~/matrix/scripts/launch_chrome.sh account_01 9222
|
||||
# 直接打开 https://www.douyin.com,看是否还在登录态
|
||||
```
|
||||
|
||||
### P3:安装 Homebrew(背景任务,不紧急)
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、与手册原文的差异对照表
|
||||
|
||||
| 手册原文 | 本地适配版本 | 原因 |
|
||||
|:---|:---|:---|
|
||||
| Docker + Colima(全程) | 阶段 A 用原生 Chrome + CDP | 优先跑通核心逻辑,降低初期复杂度 |
|
||||
| Camoufox 容器 | 阶段 A 用真实 Chrome(指纹原生) | ARM Docker 兼容性风险 |
|
||||
| 5容器并发 | 阶段 A 最多 2 个账号并发 | 8GB 内存限制 |
|
||||
| 视口 390×844(容器内) | `--window-size=390,844` 启动参数 | Chrome 直接支持 |
|
||||
| MLX 本地视觉(同步部署) | 阶段 B 再集成 | 优先走通主流程,MLX 是兜底方案 |
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单
|
||||
|
||||
```
|
||||
~/matrix/
|
||||
├── docs/
|
||||
│ ├── IMPLEMENTATION_GUIDE.md ✅ 已创建(完整落地手册)
|
||||
│ └── LOCAL_ADAPTATION.md ✅ 本文件
|
||||
├── scripts/
|
||||
│ ├── launch_chrome.sh ← 待创建(Chrome 启动脚本)
|
||||
│ ├── cdp_connector.py ← 待创建(CDP 连接器)
|
||||
│ ├── blueprint_engine.py ← 待创建(蓝图回放引擎)
|
||||
│ ├── anti_jump.py ← 待创建(防跳转模块)
|
||||
│ └── scheduler.py ← 待创建(总调度器)
|
||||
├── profiles/ ✅ 已创建(账号 Cookie 目录)
|
||||
├── data/matrix.db ← 待创建(首次运行自动建)
|
||||
├── blueprints/ ✅ 已创建
|
||||
├── corpus/ ✅ 已创建
|
||||
└── docker-compose.yml ← 阶段 B 再写
|
||||
```
|
||||
@@ -0,0 +1,221 @@
|
||||
# Matrix 养号系统 · 操作手册
|
||||
|
||||
> 本手册定义所有任务的标准化创建流程。
|
||||
|
||||
---
|
||||
|
||||
## 一、文件结构
|
||||
|
||||
```
|
||||
~/matrix/
|
||||
├── config/
|
||||
│ ├── accounts.yaml ← 账号配置(手机/端口/Profile路径)
|
||||
│ └── tasks.yaml ← 任务调度配置(蓝图+账号+开关)
|
||||
├── blueprints/ ← 所有蓝图放这里
|
||||
│ ├── douyin_browse.json
|
||||
│ ├── xiaohongshu_like.json
|
||||
│ └── zhihu_read.json
|
||||
├── scripts/
|
||||
│ ├── launch_chrome.sh ← 启动 Chrome 调试模式
|
||||
│ ├── cdp_connector.py ← CDP 连接器(连接 Chrome + 反检测)
|
||||
│ ├── task_engine.py ← 蓝图执行引擎
|
||||
│ ├── task_scheduler.py ← 定时调度器
|
||||
│ └── init_db.py ← 数据库初始化
|
||||
├── data/
|
||||
│ └── matrix.db ← SQLite 数据库
|
||||
└── profiles/
|
||||
└── account_01/ ← 账号 Cookie(自动保存)
|
||||
└── Default/ ← Chrome Profile 目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、日常操作(每日标准流程)
|
||||
|
||||
### 1. 启动 Chrome 调试模式
|
||||
```bash
|
||||
bash ~/matrix/scripts/launch_chrome.sh account_01 9222
|
||||
```
|
||||
|
||||
### 2. 手动登录(如需新账号)
|
||||
在 Chrome 里登录平台 → Cookie 自动保存到 `~/matrix/profiles/<账号>/`
|
||||
|
||||
### 3. 执行单个蓝图
|
||||
```bash
|
||||
python3 ~/matrix/scripts/task_engine.py <蓝图名> --account <账号ID> --port <端口>
|
||||
|
||||
# 示例:执行抖音浏览蓝图
|
||||
python3 ~/matrix/scripts/task_engine.py douyin_browse --account account_01
|
||||
|
||||
# 示例:执行小红书点赞蓝图
|
||||
python3 ~/matrix/scripts/task_engine.py xiaohongshu_like --account account_01
|
||||
```
|
||||
|
||||
### 4. 预览蓝图(不连接浏览器)
|
||||
```bash
|
||||
python3 ~/matrix/scripts/task_engine.py <蓝图名> --dry-run
|
||||
```
|
||||
|
||||
### 5. 定时自动跑
|
||||
```bash
|
||||
# 前台运行
|
||||
python3 ~/matrix/scripts/task_scheduler.py
|
||||
|
||||
# 或用 cron/launchd 定时触发
|
||||
python3 ~/matrix/scripts/task_scheduler.py --once
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、标准化:新增一个完整任务
|
||||
|
||||
假设要新增「小红书点赞养号」任务,需要修改/新增以下 3 个文件:
|
||||
|
||||
### Step 1 → 新增蓝图文件
|
||||
|
||||
**文件**:`~/matrix/blueprints/xiaohongshu_like.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "xiaohongshu_like",
|
||||
"name": "小红书点赞互动",
|
||||
"platform": "xiaohongshu",
|
||||
"version": "1.0.0",
|
||||
"description": "浏览发现页,随机点赞收藏评论",
|
||||
"steps": [
|
||||
{
|
||||
"op": "goto",
|
||||
"args": { "url": "https://www.xiaohongshu.com" },
|
||||
"wait_after": 3,
|
||||
"retry": 2
|
||||
},
|
||||
{
|
||||
"op": "remove_overlays",
|
||||
"args": {},
|
||||
"wait_after": 1
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 600, "duration_ms": 500 },
|
||||
"wait_after": 5,
|
||||
"wait_jitter": 1
|
||||
},
|
||||
{
|
||||
"op": "evaluate",
|
||||
"args": {
|
||||
"script": "() => { const btn = document.querySelector('[class*=like]'); if(btn) btn.click(); }"
|
||||
},
|
||||
"wait_after": 2
|
||||
},
|
||||
{
|
||||
"op": "swipe_up",
|
||||
"args": { "distance": 600, "duration_ms": 450 },
|
||||
"wait_after": 10,
|
||||
"wait_jitter": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 → 注册到任务配置
|
||||
|
||||
**文件**:`~/matrix/config/tasks.yaml`
|
||||
|
||||
```yaml
|
||||
tasks:
|
||||
- id: daily_xhs_like
|
||||
name: 小红书点赞互动
|
||||
blueprint: xiaohongshu_like # ← 对应 blueprints/*.json 的文件名
|
||||
accounts:
|
||||
- account_01
|
||||
enabled: true # true=启用,false=禁用
|
||||
```
|
||||
|
||||
### Step 3 → 如果是新平台,添加账号配置
|
||||
|
||||
**文件**:`~/matrix/config/accounts.yaml`
|
||||
|
||||
```yaml
|
||||
accounts:
|
||||
- id: account_02
|
||||
platform: xiaohongshu
|
||||
phone: "13800001111"
|
||||
port: 9223 # 每个账号用不同端口
|
||||
profile_dir: profiles/xhs_02
|
||||
enabled: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、蓝图操作参考
|
||||
|
||||
### 支持的操作(op 字段)
|
||||
|
||||
| op | 说明 | 关键参数 |
|
||||
|---|---|---|
|
||||
| `goto` | 导航到 URL | `url` |
|
||||
| `wait` | 等待秒数 | `seconds` |
|
||||
| `swipe_up` | 向上滑动 | `distance`(px), `duration_ms` |
|
||||
| `swipe_down` | 向下滑动 | `distance`(px), `duration_ms` |
|
||||
| `touch_tap` | 触摸点击坐标 | `x`, `y` |
|
||||
| `evaluate` | 执行 JS | `script`(返回 `()=>{...}` 函数) |
|
||||
| `click` | 点击元素 | `selector` |
|
||||
| `fill` | 填写输入框 | `selector`, `value` |
|
||||
| `press` | 按键 | `selector`, `key` |
|
||||
| `screenshot` | 截图 | `path`(可选) |
|
||||
| `remove_overlays` | 清理 App 弹窗 | 无参数 |
|
||||
| `wait_for` | 等待元素出现 | `selector`, `timeout` |
|
||||
|
||||
### 步骤级配置
|
||||
|
||||
每个步骤可额外设置:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "goto",
|
||||
"args": { "url": "https://..." },
|
||||
"wait_after": 3, // 步骤执行后等待 3 秒
|
||||
"wait_jitter": 0.5, // 等待时间随机抖动 ±0.5 秒
|
||||
"retry": 2 // 失败重试 2 次
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、进阶配置
|
||||
|
||||
### 多账号并发
|
||||
|
||||
每个账号用独立 Chrome 调试端口,示例:
|
||||
|
||||
```bash
|
||||
# 账号1 → 端口9222
|
||||
bash ~/matrix/scripts/launch_chrome.sh account_01 9222
|
||||
|
||||
# 账号2 → 端口9223
|
||||
bash ~/matrix/scripts/launch_chrome.sh account_02 9223
|
||||
```
|
||||
|
||||
### 熔断机制(自动)
|
||||
|
||||
连续失败 3 次自动暂停 30 分钟,人工介入后自动恢复。
|
||||
|
||||
### 数据库执行记录
|
||||
|
||||
所有执行自动写入 `~/matrix/data/matrix.db`,可用 sqlite3 查看:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/matrix/data/matrix.db "SELECT * FROM executions ORDER BY created_at DESC LIMIT 10;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、故障排查
|
||||
|
||||
| 症状 | 原因 | 处理 |
|
||||
|---|---|---|
|
||||
| `HTTP Error 502` | urllib 走了代理 | 检查 `cdp_connector.py` 是否用 `ProxyHandler({})` |
|
||||
| `Emulation.setTouchEmulationOverride` 报错 | Chrome 147 移除了此 API | 升级 cdp_connector.py |
|
||||
| `webSocketDebuggerUrl` 获取失败 | Chrome 未启动调试模式 | 先运行 `launch_chrome.sh` |
|
||||
| 步骤执行正常但页面空白 | 窗口尺寸太小 | 修改 `launch_chrome.sh` 的 `--window-size` |
|
||||
| Cookie 丢失 | Profile 路径不一致 | 确认 `accounts.yaml` 中 `profile_dir` 正确 |
|
||||
@@ -0,0 +1,323 @@
|
||||
# Matrix 矩阵养号系统 v5.0 使用文档
|
||||
|
||||
> 最后更新: 2026-06-11
|
||||
> 适用版本: v5.0(PlatformOps 架构)
|
||||
> 多机同步: Git + 坚果云
|
||||
|
||||
---
|
||||
|
||||
## 一、总体架构
|
||||
|
||||
```
|
||||
mc(统一 CLI 入口)
|
||||
│
|
||||
├─ mc run → mc/engine.py(纯调度层,~300行)
|
||||
│ └─ PlatformOps.execute()
|
||||
│ ├── DouyinOps(抖音,20+ 操作,platform_ops 接口)
|
||||
│ └── XhsOps(小红书,16 操作,ops/xhs_ops.py)
|
||||
│
|
||||
├─ mc account → matrix_mgmt.py(账号管理)
|
||||
├─ mc blueprint → matrix_mgmt.py(蓝图管理)
|
||||
├─ mc corpus → mc/corpus.py(语料库)
|
||||
└─ mc status → cli.py(状态查询)
|
||||
```
|
||||
|
||||
### 代码 vs 数据
|
||||
|
||||
| | 路径 | 共享方式 |
|
||||
|---|---|---|
|
||||
| 代码/蓝图 | `agent-sync/05_tools/07_matrix/` | Git 同步 |
|
||||
| 账号配置 | `agent-local/tools/matrix/config/accounts.yaml` | 本机独立 |
|
||||
| 浏览器指纹 | `agent-local/tools/matrix/identities/{name}/` | 本机独立 |
|
||||
| 登录态 | `agent-local/tools/matrix/identities/{name}/user_data/` | 本机独立 |
|
||||
| 联邦状态 | `cross_machine/data/matrix/{uid}.json` | Git 同步 |
|
||||
|
||||
---
|
||||
|
||||
## 二、新机部署
|
||||
|
||||
```bash
|
||||
# 1. 拉取代码
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git pull
|
||||
|
||||
# 2. 运行安装脚本(建目录 + 装依赖 + 配置)
|
||||
bash 05_tools/07_matrix/install.sh
|
||||
|
||||
# 3. 确认 Python 环境
|
||||
# mc 会自动检测 Python,或设置环境变量:
|
||||
export AGENTOS_PYTHON=~/.workbuddy/binaries/python/envs/agent-os/bin/python3
|
||||
|
||||
# 4. 检查可用账号
|
||||
cd 05_tools/07_matrix && ./mc account list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、账号管理
|
||||
|
||||
### 新建账号身份
|
||||
|
||||
```bash
|
||||
# 创建新身份(生成 BrowserForge 指纹)
|
||||
python scripts/create_identity.py --name douyin_new --platform douyin
|
||||
|
||||
# 首次登录(浏览器打开,扫码/短信登录)
|
||||
./mc account login douyin_new
|
||||
```
|
||||
|
||||
### 同步已有账号(跨机迁移)
|
||||
|
||||
```bash
|
||||
# 旧机器导出
|
||||
./mc account export # → 生成压缩包
|
||||
|
||||
# 新机器导入
|
||||
./mc account import <path_to_export.tar.gz>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、cli 命令参考
|
||||
|
||||
### 养号执行
|
||||
|
||||
```bash
|
||||
# 单账号单轮(测试用)
|
||||
./mc run --accounts douyin_test --blueprints douyin_active_v1 --rounds 1
|
||||
|
||||
# 多账号并行
|
||||
./mc run --accounts douyin_test,douyin_133 --blueprints douyin_active_v1 --rounds 3
|
||||
|
||||
# 混合多蓝图(每轮随机选)
|
||||
./mc run --accounts douyin_test --blueprints douyin_active_v1,douyin_browse_v2 --rounds 3 --mix
|
||||
|
||||
# 指定小红书
|
||||
./mc run --accounts xhs_01 --blueprints xhs_active_v1 --rounds 2
|
||||
|
||||
# 完整参数
|
||||
./mc run --accounts A,B --blueprints X,Y --rounds 3 --mix --interval 45-90 --engine auto
|
||||
```
|
||||
|
||||
### 账号管理
|
||||
|
||||
```bash
|
||||
./mc account list # 列表(含登录态)
|
||||
./mc account login <name> # 首次登录
|
||||
./mc account status [name] # 查看状态
|
||||
./mc account export # 导出
|
||||
./mc account import <path> # 导入
|
||||
```
|
||||
|
||||
### 蓝图管理
|
||||
|
||||
```bash
|
||||
./mc blueprint list # 列表
|
||||
./mc blueprint show <name> # 详情
|
||||
```
|
||||
|
||||
### 状态查询
|
||||
|
||||
```bash
|
||||
./mc status all # 全局状态
|
||||
./mc status accounts # 账号列表
|
||||
./mc status browsers # 浏览器进程
|
||||
```
|
||||
|
||||
### 旧版兼容
|
||||
|
||||
旧 `matrix.py` 仍可用但标记 deprecated。对照表:
|
||||
|
||||
| 旧命令(matrix.py) | 新命令(mc) |
|
||||
|---|---|
|
||||
| `python matrix.py account list` | `./mc account list` |
|
||||
| `python matrix.py account login X` | `./mc account login X` |
|
||||
| `python matrix.py nurture run -a X -r 10` | `./mc run --accounts X --rounds 10 ...` |
|
||||
| `python matrix.py status all` | `./mc status all` |
|
||||
| `python matrix.py config blueprint list` | `./mc blueprint list` |
|
||||
|
||||
---
|
||||
|
||||
## 五、蓝图说明
|
||||
|
||||
### 抖音蓝图
|
||||
|
||||
| 蓝图 | 步骤 | 说明 |
|
||||
|------|------|------|
|
||||
| `douyin_active_v1` | 27步 | 浏览×6 + 点赞×4 + 评论×2 + 收藏×1 + 搜索×2 |
|
||||
| `douyin_browse_v2` | 11步 | 轻量浏览+随机点赞 |
|
||||
| `douyin_nurture_v1` | 8步 | 搜索→浏览→点赞→收藏→切换 |
|
||||
| `douyin_search_browse` | 7步 | 搜索→浏览→点赞 |
|
||||
| `douyin_comment_interact` | 8步 | 浏览→打开评论区→发评论 |
|
||||
|
||||
### 小红书蓝图
|
||||
|
||||
| 蓝图 | 步骤 | 说明 |
|
||||
|------|------|------|
|
||||
| `xhs_active_v1` | 26步 | 浏览×5 + 点赞×5 + 搜索×2 + 收藏×2 + 评论×1 + 关注×1 |
|
||||
| `xiaohongshu_nurture_v2` | 11步 | 浏览→点击→点赞→收藏→搜索→关注 |
|
||||
|
||||
### 蓝图格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "蓝图唯一ID",
|
||||
"name": "显示名称",
|
||||
"platform": "douyin 或 xiaohongshu",
|
||||
"description": "说明",
|
||||
"steps": [
|
||||
{"step_id": 1, "op": "操作名", "args": {}},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**抖音可用操作**: goto_home, like, collect, follow, open_comments, close_comments, post_comment, next_video, prev_video, search, wait_watch, scroll_feed, open_video, wait, goto_url, go_back
|
||||
|
||||
**小红书可用操作**: xhs_goto_home, xhs_browse, xhs_scroll_feed, xhs_click_note, xhs_like, xhs_collect, xhs_comment, xhs_follow, xhs_search, xhs_goto_profile, xhs_read_nickname, xhs_read_user_id, xhs_read_following, xhs_read_fans, xhs_read_likes, xhs_read_bio
|
||||
|
||||
---
|
||||
|
||||
## 六、自动化任务
|
||||
|
||||
### 每日养号
|
||||
|
||||
```bash
|
||||
# 完整执行
|
||||
bash scripts/nurture_daily.sh
|
||||
|
||||
# 仅查看命令(不执行)
|
||||
bash scripts/nurture_daily.sh --dry
|
||||
|
||||
# 仅跑第1组
|
||||
bash scripts/nurture_daily.sh --group 1
|
||||
```
|
||||
|
||||
### 定时执行(cron)
|
||||
|
||||
```cron
|
||||
# 每天 9:00 和 21:00 执行
|
||||
0 9,21 * * * cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix && bash scripts/nurture_daily.sh >> /tmp/nurture_daily.log 2>&1
|
||||
```
|
||||
|
||||
### 定时调度器(旧版兼容)
|
||||
|
||||
```bash
|
||||
python scripts/task_scheduler.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、多账号并行
|
||||
|
||||
`mc run` 原生支持多账号并行。每个账号启动独立 Camoufox 浏览器,窗口自动错位:
|
||||
|
||||
| 账号索引 | 窗口位置 | 屏幕区域 |
|
||||
|----------|---------|---------|
|
||||
| 0 | (30, 30) | 左上 |
|
||||
| 1 | (400, 30) | 中上 |
|
||||
| 2 | (800, 30) | 右上 |
|
||||
| 3 | (30, 450) | 左下 |
|
||||
| 4 | (400, 450) | 中下 |
|
||||
| 5 | (800, 870) | 右下 |
|
||||
|
||||
```bash
|
||||
# 3 个抖音同时跑
|
||||
./mc run --accounts douyin_test,douyin_133,douyin_133_2 --blueprints douyin_active_v1 --rounds 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、跨机器同步流程
|
||||
|
||||
```bash
|
||||
# 主机器修改后
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git add 05_tools/07_matrix/
|
||||
git commit -m "Matrix v5.0 更新"
|
||||
git push
|
||||
|
||||
# 其他机器
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git pull
|
||||
bash 05_tools/07_matrix/install.sh # 如需要重建本地目录
|
||||
```
|
||||
|
||||
**不同步的内容**(各机器独立):
|
||||
- `agent-local/tools/matrix/config/accounts.yaml` — 账号配置
|
||||
- `agent-local/tools/matrix/identities/` — 浏览器指纹 + 登录态
|
||||
- `agent-local/tools/matrix/logs/` — 运行日志
|
||||
|
||||
---
|
||||
|
||||
## 九、v5.0 变更说明
|
||||
|
||||
### 新增
|
||||
|
||||
- `mc` 统一 CLI(替代 `matrix.py`)
|
||||
- `ops/_base.py` — PlatformOps 基类 + OpResult 结构化结果
|
||||
- `ops/xhs_ops.py` — 小红书 16 个原子操作
|
||||
- `mc/runlog.py` — 结构化运行日志(JSONL)
|
||||
- `mc/engine.py` — 重构引擎(~300行,原 775行)
|
||||
- `blueprints/douyin_active_v1.json` — 抖音高活跃蓝图
|
||||
- `blueprints/xhs_active_v1.json` — 小红书高活跃蓝图
|
||||
- `docs/MC_COMMAND_REFERENCE.md` — 命令参考手册
|
||||
|
||||
### 改进
|
||||
|
||||
- `douyin_ops.py` 实现 PlatformOps 接口,每个操作返回 OpResult
|
||||
- `_click_selector` 增加 JS evaluate fallback(穿透 Shadow DOM)
|
||||
- `goto_home` 增加 commit 模式 + 强制继续(不因 video 未就绪阻塞)
|
||||
- 进入视频模式:从卡片 `data-aweme-id` 提取视频 ID 直接导航(/video/{id})
|
||||
- 视频页双击 video 获取焦点(原代码约定)
|
||||
- post_comment 用 pbcopy + Meta+V(Draft.js 兼容)
|
||||
- 所有 `force=True` 跳过 Playwright 可点击性检查
|
||||
- 硬编码路径修复(Python、用户名)
|
||||
- `.gitignore` 排除临时蓝图和一次性脚本
|
||||
|
||||
### 废弃
|
||||
|
||||
- `matrix.py` — 标记 deprecated(用 mc 替代)
|
||||
- `yanghao_runner.py` — 功能合并到 mc
|
||||
- `task_engine.py` — 功能合并到 mc/engine.py
|
||||
- `camoufox_manager.py` / `camoufox_server.py` — 被 cdp_connector.py 取代
|
||||
- `browser_manager.py` / `browser_keepalive.py` — mc/browser.py 取代
|
||||
- 25+ 调试脚本 — 移入 `_archive/`
|
||||
|
||||
---
|
||||
|
||||
## 十、故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| 页面加载超时 | Camoufox 下 douyin.com 使用 `commit` 模式 | 自动 fallback,强制继续 |
|
||||
| like/collect 失败 | Shadow DOM 影响 Playwright locator | 自动 fallback 到 JS evaluate |
|
||||
| post_comment 失败 | 评论区无标准输入框 DOM | 不依赖选择器,pbcopy + Meta+V 硬发 |
|
||||
| 多账号窗口重叠 | 未设置 window_position | 自动按索引分配错位 |
|
||||
| "代理"行消失 | 账号 config.yaml 中 proxy 被注释 | 取消注释恢复代理 |
|
||||
|
||||
### 调试命令
|
||||
|
||||
```bash
|
||||
# 查看运行日志
|
||||
cat /tmp/matrix_step_douyin_test.log
|
||||
|
||||
# Peekaboo 截图观察
|
||||
peekaboo image
|
||||
|
||||
# 原子操作诊断
|
||||
python scripts/diagnose_ops.py
|
||||
```
|
||||
|
||||
### 版本信息
|
||||
|
||||
当前所有文档统一版本:v5.0.0。文件清单:
|
||||
|
||||
- `MODULE.md` — v5.0.0
|
||||
- `TOOL.md` — v5.0
|
||||
- `mc/__main__.py` — v1.0.0(mc CLI 版本)
|
||||
- `docs/MC_COMMAND_REFERENCE.md` — v5.0
|
||||
- `blueprints/*.json` — 每个蓝图独立版本
|
||||
- `install.sh` — v4.0(未变)
|
||||
@@ -0,0 +1,224 @@
|
||||
# mc 命令参考手册 — Matrix v5.2
|
||||
|
||||
> 统一 CLI 入口,替代旧版 matrix.py。
|
||||
> 智能体通过 mc 命令调用 Matrix,不需要直接操作 Python 脚本。
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
chmod +x mc
|
||||
# 可选:ln -s $(pwd)/mc /usr/local/bin/mc
|
||||
```
|
||||
|
||||
Python 路径自动检测(AGENTOS_PYTHON > agent-os venv > 系统 python3),换机无需修改。
|
||||
|
||||
---
|
||||
|
||||
## 命令一览
|
||||
|
||||
### 录制系统(v5.1 新增)
|
||||
|
||||
```bash
|
||||
# 开始录制
|
||||
mc record start --account douyin_test
|
||||
# → 启动 Camoufox,你在浏览器操作,关键节点按数字键 1-8 标记
|
||||
# 按 0 结束录制
|
||||
|
||||
# 查看录制包
|
||||
mc record list
|
||||
|
||||
# 分析录制包
|
||||
mc record analyze <录制包路径>
|
||||
|
||||
# 导出为蓝图+代码
|
||||
mc record export <录制包路径>
|
||||
|
||||
# 删除录制包
|
||||
mc record delete <录制包路径>
|
||||
```
|
||||
|
||||
### 定向评论(v5.1 新增)
|
||||
|
||||
```bash
|
||||
# 创建一个仅包含 "打开视频→评论" 的蓝图,然后执行:
|
||||
mc run --accounts douyin_test --blueprints douyin_comment_test --rounds 1 --keep
|
||||
|
||||
# 或手动创建自定义蓝图:
|
||||
# 1. 在 blueprints/ 下创建 JSON 文件
|
||||
# 2. 包含 goto_url → wait_watch → open_comments → post_comment
|
||||
# 3. 用 mc run 执行
|
||||
```
|
||||
|
||||
### 养号执行
|
||||
|
||||
```bash
|
||||
# 批量养号(推荐)
|
||||
mc run --accounts douyin_01,douyin_02 --blueprints nurture_v1 --rounds 3 --mix --interval 45-90
|
||||
|
||||
# 指定语料库
|
||||
mc run --accounts douyin_01 --blueprints nurture_v1 --rounds 3 --corpus daily_hot
|
||||
|
||||
# 查看帮助
|
||||
mc run --help
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `--accounts` | 逗号分隔的账号列表 | 必填 |
|
||||
| `--blueprints` | 逗号分隔的蓝图列表 | 必填 |
|
||||
| `--rounds` | 每账号执行轮数 | 10 |
|
||||
| `--mix` | 混合随机模式(每轮随机选蓝图) | 顺序模式 |
|
||||
| `--interval` | 轮间间隔(秒),支持范围 `45-90` | 30-60 |
|
||||
| `--corpus` | 逗号分隔的语料库分类 | 无 |
|
||||
| `--daemon` | 后台运行模式 | 否 |
|
||||
| `--engine` | 引擎(auto/camoufox) | auto |
|
||||
| `--keep` | 执行完后保留浏览器(需手动关) | 否 |
|
||||
| `--json` | 输出 JSON 格式结果 | 否 |
|
||||
|
||||
### 账号管理
|
||||
|
||||
```bash
|
||||
mc account list # 列出所有账号及状态
|
||||
mc account login <name> # 首次手动登录(打开浏览器等用户操作)
|
||||
mc account status [name] # 查看账号登录状态
|
||||
mc account export # 导出账号配置
|
||||
mc account import <path> # 导入账号配置
|
||||
```
|
||||
|
||||
### 注册新账号(Dashboard 推荐)
|
||||
|
||||
在 Dashboard 上完成:
|
||||
1. 启动 Dashboard → 左侧「矩阵」→「短信与管理」
|
||||
2. 底部「注册新账号」表单
|
||||
3. 选平台、填手机号、填备注名 → 点确认
|
||||
4. 浏览器自动打开 → 扫码登录 → 后续可采集昵称
|
||||
5. 登录后点「采集昵称」获取昵称/粉丝数
|
||||
|
||||
也可以通过 CLI:
|
||||
```bash
|
||||
python scripts/matrix.py account create --platform douyin --phone 138xxxx
|
||||
```
|
||||
|
||||
### 短信 API(Dashboard 推荐)
|
||||
|
||||
Dashboard 短信页面:
|
||||
1. 左侧「矩阵」→「短信与管理」
|
||||
2. 下拉选账号 → 点「查短信」
|
||||
3. 系统自动解析抖音/小红书验证码
|
||||
|
||||
API:
|
||||
```bash
|
||||
# 查短信
|
||||
GET /api/matrix/sms/test/{phone_or_account}
|
||||
|
||||
# 查看所有账号手机号
|
||||
GET /api/matrix/sms/accounts
|
||||
```
|
||||
|
||||
### Dashboard 启动
|
||||
|
||||
```bash
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/10_dashboard
|
||||
nohup ~/.workbuddy/binaries/python/envs/agent-os/bin/uvicorn app:app --host 0.0.0.0 --port 9988 &
|
||||
# 浏览器打开 http://localhost:9988
|
||||
# 左侧「矩阵」:短信 / 录制 / 账号管理
|
||||
```
|
||||
|
||||
### 新机部署说明
|
||||
|
||||
新机部署后同步账号:
|
||||
```bash
|
||||
bash ~/workbuddy-agent-os/agent-sync/00_bootstrap/init.sh
|
||||
# 自动拉取仓库 → 安装依赖 → 同步本机账号
|
||||
```
|
||||
|
||||
`accounts_registry.yaml` 中 `assigned_machine` 标记为当前机器 UID 的账号会自动同步到本机。
|
||||
|
||||
### 蓝图管理
|
||||
|
||||
```bash
|
||||
mc blueprint list # 列出所有蓝图
|
||||
mc blueprint show <name> # 查看蓝图详情
|
||||
```
|
||||
|
||||
### 语料库
|
||||
|
||||
```bash
|
||||
mc corpus list # 查看语料分类
|
||||
mc corpus add <平台> "语料" # 添加语料
|
||||
```
|
||||
|
||||
### 状态查询
|
||||
|
||||
```bash
|
||||
mc status all # 全局状态(浏览器+账号)
|
||||
mc status accounts # 账号登录状态
|
||||
mc status browsers # 浏览器运行状态
|
||||
```
|
||||
|
||||
### 代理管理
|
||||
|
||||
```bash
|
||||
mc proxy list # 代理列表
|
||||
mc proxy test <name> # 测试代理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动化任务
|
||||
|
||||
### 每日养号(定时执行)
|
||||
|
||||
```bash
|
||||
# 完整执行
|
||||
bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/nurture_daily.sh
|
||||
|
||||
# 仅查看命令(不执行)
|
||||
bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/nurture_daily.sh --dry
|
||||
|
||||
# 仅跑第1组
|
||||
bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/nurture_daily.sh --group 1
|
||||
```
|
||||
|
||||
日志:`/tmp/nurture_daily_YYYYMMDD.log`
|
||||
|
||||
### 结构化日志
|
||||
|
||||
每次 mc run 执行后自动写入:
|
||||
|
||||
`agent-local/tools/matrix/logs/run_{date}_{account}.jsonl`
|
||||
|
||||
查询:
|
||||
```python
|
||||
from mc.runlog import get_today_summary
|
||||
print(get_today_summary())
|
||||
# → {"date": "20260609", "accounts": {"douyin_01": {"runs": 3, "last_success": 8, ...}}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与旧版 matrix.py 对照
|
||||
|
||||
| 旧命令 | 新命令 |
|
||||
|--------|--------|
|
||||
| `python matrix.py account list` | `mc account list` |
|
||||
| `python matrix.py account login X` | `mc account login X` |
|
||||
| `python matrix.py nurture run -a X -r 10` | `mc run --accounts X --rounds 10 ...` |
|
||||
| `python matrix.py status all` | `mc status all` |
|
||||
| `python matrix.py config blueprint list` | `mc blueprint list` |
|
||||
|
||||
---
|
||||
|
||||
## 智能体调用约定
|
||||
|
||||
智能体通过 WorkBuddy 技能 `matrix` 触发。SKILL.md 定义了触发词和对应的 mc 命令。
|
||||
|
||||
当用户说"养号"、"执行蓝图"、"账号状态"时,智能体:
|
||||
1. 加载 `02_skills/matrix/SKILL.md`
|
||||
2. 根据触发词匹配对应的 mc 命令
|
||||
3. 执行命令并汇报结果
|
||||
|
||||
**不需要读源码。不需要知道 engine.py 内部细节。**
|
||||
@@ -0,0 +1,57 @@
|
||||
# SMS 验证码 API 使用说明
|
||||
|
||||
## 服务地址
|
||||
`https://wx.tyhtak.com`
|
||||
|
||||
## API 接口
|
||||
```
|
||||
GET /api/biz/msg/messages
|
||||
?api_key=gtmsg2026
|
||||
&receiver_phone={phone}
|
||||
&page=1&per_page=20
|
||||
```
|
||||
|
||||
## 配置方式
|
||||
|
||||
### 1. 全局默认手机号(scripts/config/sms.yaml)
|
||||
```yaml
|
||||
sms:
|
||||
api_key: "gtmsg2026"
|
||||
phone: "15370103682" # 默认手机号
|
||||
base_url: "https://wx.tyhtak.com/api/biz/msg/messages"
|
||||
```
|
||||
|
||||
### 2. 账号级手机号(accounts.yaml)
|
||||
```yaml
|
||||
accounts:
|
||||
- id: douyin_01
|
||||
phone: "18513308610" # 每个账号绑定自己的手机号
|
||||
- id: douyin_02
|
||||
phone: "18500003366"
|
||||
- id: douyin_camo01
|
||||
phone: "15370103682"
|
||||
```
|
||||
|
||||
### 3. 运行时优先级
|
||||
`sms_login(account_name='douyin_02')` → 查 accounts.yaml → 拿到 phone=18500003366 → 调用 API 时用 `receiver_phone=18500003366`
|
||||
|
||||
### 4. 三个账号的 API 支持情况
|
||||
所有账号共用同一个 API 平台(wx.tyhtak.com),只需在 API URL 中替换 `receiver_phone` 参数即可查询对应手机号的短信。
|
||||
|
||||
| 账号 | 手机号 | API receiver_phone |
|
||||
|------|--------|-------------------|
|
||||
| douyin_01 | 18513308610 | 替换即可 |
|
||||
| douyin_02 | 18500003366 | 替换即可 |
|
||||
| douyin_camo01 | 15370103682 | 替换即可 |
|
||||
|
||||
## 使用方式
|
||||
```python
|
||||
# 自动模式:从 accounts.yaml 查手机号
|
||||
from matrix_modules.account.sms_login import sms_login
|
||||
ok = await sms_login(page, account_name='douyin_02')
|
||||
|
||||
# 手动模式:指定手机号
|
||||
from matrix_modules.account.sms import ApiSMSHandler
|
||||
handler = ApiSMSHandler(phone='18500003366')
|
||||
code = await handler.wait('抖音登录', timeout=60)
|
||||
```
|
||||
@@ -0,0 +1,252 @@
|
||||
# Matrix 统— 多机同步标准化指南
|
||||
|
||||
> **版本**: v1.0.0
|
||||
> **最后更新**: 2026-05-01
|
||||
> **适用场景**: 本机测试完成后,将修改同步到其他电脑
|
||||
|
||||
---
|
||||
|
||||
## 1. 同步架构总览
|
||||
|
||||
```
|
||||
主电脑(Redmi-12C)
|
||||
│
|
||||
├── agent-sync/ ←────────── 坚果云 / Gitee ──────────→ agent-sync/(其他电脑)
|
||||
│ ├── 02_skills/matrix/ 代码 + 文档 │
|
||||
│ ├── 05_tools/07_matrix/ │
|
||||
│ └── ... │
|
||||
│ │
|
||||
└── agent-local/ ←────────── 不同步(每机独立) ────→ agent-local/(其他电脑)
|
||||
└── tools/matrix/ │
|
||||
├── config/accounts.yaml (手动配置) │
|
||||
├── profiles/ (手动登录) │
|
||||
├── data/cookies/ (重新导出) │
|
||||
└── data/matrix.db (重新初始化) │
|
||||
```
|
||||
|
||||
### 同步内容清单
|
||||
|
||||
| 类别 | 内容 | 同步方式 | 是否同步 |
|
||||
|------|------|---------|---------|
|
||||
| **代码脚本** | scripts/*.py, *.sh | Git + 坚果云 | ✅ |
|
||||
| **蓝图文件** | blueprints/*.json | Git + 坚果云 | ✅ |
|
||||
| **标准化文档** | docs/*.md | Git + 坚果云 | ✅ |
|
||||
| **技能定义** | 02_skills/matrix/ | Git + 坚果云 | ✅ |
|
||||
| **安装脚本** | install.sh | Git + 坚果云 | ✅ |
|
||||
| **依赖清单** | requirements.txt | Git + 坚果云 | ✅ |
|
||||
| **配置模板** | config_template/ | Git + 坚果云 | ✅ |
|
||||
| **local.yaml** | 本机路径配置 | 每机独立生成 | ❌ |
|
||||
| **accounts.yaml** | 手机号/端口 | 每机手动配置 | ❌ |
|
||||
| **Chrome Profile** | 浏览器登录数据 | 每机重新登录 | ❌ |
|
||||
| **Cookie 文件** | data/cookies/ | 每机重新导出 | ❌ |
|
||||
| **matrix.db** | 执行记录/DB | 每机重新初始化 | ❌ |
|
||||
| **Python venv** | 安装的包 | 每机重新安装 | ❌ |
|
||||
| **Playwright Chromium** | 浏览器二进制 | 每机重新安装 | ❌ |
|
||||
| **Camoufox** | Firefox 浏览器 | 每机重新安装 | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 2. 标准化同步流程
|
||||
|
||||
### 2.1 在主电脑上提交修改
|
||||
|
||||
```bash
|
||||
# 1. 确认所有修改已保存
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
|
||||
# 2. 查看修改文件
|
||||
git status
|
||||
# 预期看到:
|
||||
# modified: 02_skills/matrix/SKILL.md
|
||||
# modified: 05_tools/07_matrix/scripts/switch_account.py
|
||||
# modified: 05_tools/07_matrix/scripts/cdp_connector.py
|
||||
# new file: 05_tools/07_matrix/scripts/auth_manager.py
|
||||
# modified: 05_tools/07_matrix/docs/FULL_TEST_REPORT.md
|
||||
# new file: 05_tools/07_matrix/docs/SYSTEM_ARCHITECTURE.md
|
||||
# new file: 05_tools/07_matrix/docs/SYNC_GUIDE.md
|
||||
|
||||
# 3. 提交并推送
|
||||
git add -A
|
||||
git commit -m "Matrix 系统修复: 原子化登录模块 + 稳定性修复"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
> **注意**: 坚果云会同步 `agent-sync/` 目录下的文件修改,但建议以 Git 为主要版本控制手段,坚果云仅作为文件快速分发。两台电脑都需要安装坚果云并登录同一账号。
|
||||
|
||||
### 2.2 在其他电脑上拉取更新
|
||||
|
||||
```bash
|
||||
# 方式A: 通过 Git(推荐,版本可控)
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git pull origin main
|
||||
|
||||
# 方式B: 通过坚果云(自动同步)
|
||||
# 确保两台电脑登录同一坚果云账号,agent-sync/ 目录会自动同步
|
||||
```
|
||||
|
||||
### 2.3 在新电脑上运行安装脚本
|
||||
|
||||
```bash
|
||||
# 1. 运行一键安装(建立目录骨架 + 安装依赖 + 生成配置)
|
||||
bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/install.sh
|
||||
|
||||
# 2. 安装 Camoufox(额外的 Firefox 浏览器)
|
||||
~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/install_camoufox.sh
|
||||
# 或手动:
|
||||
# pip install camoufox
|
||||
# python -m camoufox fetch
|
||||
```
|
||||
|
||||
### 2.4 配置本地账号
|
||||
|
||||
```bash
|
||||
# 编辑本机 accounts.yaml(手机号/端口/备注按本机设置)
|
||||
vim ~/workbuddy-agent-os/agent-local/tools/matrix/config/accounts.yaml
|
||||
|
||||
# 必须修改的字段:
|
||||
# - phone: 本机对应的手机号
|
||||
# - port: 确保端口不冲突(Chrome: 9222+, Camoufox: 9301+)
|
||||
```
|
||||
|
||||
### 2.5 初始化数据库(可选)
|
||||
|
||||
```bash
|
||||
# 如果新机没有 matrix.db,初始化一个
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/scripts/init_db.py
|
||||
```
|
||||
|
||||
### 2.6 重新登录各平台
|
||||
|
||||
```bash
|
||||
# 每个账号需要重新手动登录一次
|
||||
# Chrome 方案:
|
||||
cd ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
python scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
# → 浏览器打开 douyin.com,扫码登录
|
||||
# → 登录后自动检测 Cookie → 自动导出到 data/cookies/
|
||||
|
||||
# Camoufox 方案:
|
||||
python scripts/yanghao_runner.py --account douyin_camo01 --blueprint douyin_browse_v2 --browser camoufox
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 本地依赖完整清单
|
||||
|
||||
以下是在新电脑上需要重新部署的所有本地依赖:
|
||||
|
||||
### 3.1 Python 包
|
||||
|
||||
```bash
|
||||
# 使用 agent-os 的 Python 环境
|
||||
PY=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/install.sh 自动处理
|
||||
|
||||
# 或手动安装:
|
||||
pip install -r ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/requirements.txt
|
||||
# 包含: patchright, camoufox, PyYAML
|
||||
```
|
||||
|
||||
### 3.2 Playwright 浏览器
|
||||
|
||||
```bash
|
||||
# Chromium(Chrome 方案需要)
|
||||
python -m patchright install chromium
|
||||
|
||||
# 注意: 不需要 firefox(Camoufox 自带 Firefox)
|
||||
```
|
||||
|
||||
### 3.3 Camoufox 浏览器
|
||||
|
||||
```bash
|
||||
# Camoufox 方案需要
|
||||
pip install camoufox
|
||||
python -m camoufox fetch
|
||||
# 下载到: ~/Library/Caches/camoufox/Camoufox.app/
|
||||
```
|
||||
|
||||
### 3.4 本地目录结构
|
||||
|
||||
```
|
||||
agent-local/tools/matrix/
|
||||
├── config/
|
||||
│ └── accounts.yaml # 手动配置(每机不同)
|
||||
├── data/
|
||||
│ ├── cookies/ # 登录后自动导出
|
||||
│ ├── camoufox_pids/ # 运行时自动创建
|
||||
│ └── matrix.db # init_db.py 初始化
|
||||
├── profiles/
|
||||
│ ├── account_01/ # 登录后自动生成
|
||||
│ ├── douyin_02/
|
||||
│ ├── camoufox_01/
|
||||
│ └── camoufox_02/
|
||||
├── logs/ # 运行时日志
|
||||
└── screenshots/ # 可选
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 版本对照表
|
||||
|
||||
每次修改后,在主电脑上记录修改范围,方便其他电脑核对自己是否拉取了最新代码。
|
||||
|
||||
### v2.1.0 (2026-05-01) 修改清单
|
||||
|
||||
| 文件 | 修改类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| `scripts/auth_manager.py` | **新增** | 原子化登录模块(Cookie 检测/导出/注入) |
|
||||
| `scripts/switch_account.py` | 修改 | 登录检测改为 auth_manager,自动导出 Cookie |
|
||||
| `scripts/cdp_connector.py` | 修改 | 添加 os=windows / 中文字体 / 委托 auth_manager |
|
||||
| `02_skills/matrix/SKILL.md` | 修改 | 更新正确入口、新增原子模块说明 |
|
||||
| `docs/FULL_TEST_REPORT.md` | 修改 | 新增修复记录、已验证功能 |
|
||||
| `docs/SYSTEM_ARCHITECTURE.md` | **新增** | 全链路系统架构文档 |
|
||||
| `docs/ACCOUNT_LOGIN_SOP.md` | **新增** | 账号登录标准化操作流程 |
|
||||
| `docs/SYNC_GUIDE.md` | **新增** | 多机同步指南(本文) |
|
||||
| `blueprints/douyin_browse_v3.json` | DB注册 | 已在主电脑注册到 task_blueprints |
|
||||
| `blueprints/douyin_nurture_v1.json` | DB注册 | 同上 |
|
||||
| `profiles/douyin_01/` | 删除 | 冗余目录(11MB) |
|
||||
|
||||
**新机注意事项**:
|
||||
- DB 注册的蓝图(v3/nurture)需在新机重新运行 `init_db.py` 或在首次使用时自动注册
|
||||
- 删除冗余目录是主电脑操作,新机没有这个目录,无需处理
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题
|
||||
|
||||
### Q: 为什么不能直接复制 Chrome Profile?
|
||||
Chrome Profile 包含设备绑定的加密数据(Cookie 用本机密钥加密),复制到其他电脑会失效。必须在新机重新扫码登录。
|
||||
|
||||
### Q: 为什么 accounts.yaml 不能同步?
|
||||
每个账号绑定的手机号是固定的,但不同电脑上 Chrome Profile 目录路径和端口分配可能不同(例如一台电脑 9222 已占用,需要用 9224)。
|
||||
|
||||
### Q: 主电脑上的修改如何确保不丢失?
|
||||
```bash
|
||||
# 养成好习惯:每次修改后
|
||||
cd ~/workbuddy-agent-os/agent-sync
|
||||
git add -A
|
||||
git commit -m "描述修改内容"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Q: 新电脑需要安装什么额外软件?
|
||||
- 坚果云(同步 agent-sync/ 文件)
|
||||
- Git(拉取代码)
|
||||
- Chrome 浏览器(养号工具使用)
|
||||
- agent-os 的 Python 环境(install.sh 自动处理)
|
||||
|
||||
---
|
||||
|
||||
## 6. 快速检查清单
|
||||
|
||||
新机部署完成后,逐项确认:
|
||||
|
||||
- [ ] Git pull 或坚果云同步完成
|
||||
- [ ] install.sh 运行成功
|
||||
- [ ] Camoufox 安装完成(如需要)
|
||||
- [ ] accounts.yaml 已配置本机账号
|
||||
- [ ] matrix.db 已初始化
|
||||
- [ ] Chrome Profile 已重新登录
|
||||
- [ ] Cookie 已自动导出
|
||||
- [ ] `python scripts/switch_account.py --status` 显示 active
|
||||
- [ ] `python scripts/auth_manager.py 9222` 显示 sessionid ✅
|
||||
@@ -0,0 +1,181 @@
|
||||
# Matrix 养号系统 — 系统架构设计
|
||||
|
||||
> **版本**:v2.1.0
|
||||
> **最后更新**:2026-05-01
|
||||
> **设计原则**:原子化可复用 → 固化不反复 → 全链路串接
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 用户触发(对话/定时) │
|
||||
└─────────────────┬───────────────────────────┬───────────────┘
|
||||
│ │
|
||||
┌───────▼───────┐ ┌─────────▼────────┐
|
||||
│ yanghao_runner.py │ │ task_scheduler.py │
|
||||
│ (手动/交互入口) │ │ (定时任务入口) │
|
||||
└───────┬───────┘ └─────────┬────────┘
|
||||
│ │
|
||||
└──────────┬────────────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ CDPConnector │
|
||||
│ (浏览器连接层) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
┌───────▼──────┐ ┌────▼────┐ ┌───────▼──────┐
|
||||
│ Chrome CDP │ │Camoufox │ │ auth_manager │
|
||||
│ (端口 9222+) │ │原生 API │ │ (登录管理) │
|
||||
└───────┬──────┘ └────┬────┘ └───────┬──────┘
|
||||
│ │ │
|
||||
└──────┬───────┘ │
|
||||
│ │
|
||||
┌──────▼──────┐ ┌─────────▼────────┐
|
||||
│ DouyinOps │ │ Cookie 文件 │
|
||||
│ (原子操作库) │ │ (持久化存储) │
|
||||
└──────┬──────┘ └─────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ 蓝图引擎 │
|
||||
│ task_engine │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 原子化模块清单
|
||||
|
||||
| 模块 | 文件 | 职责 | 依赖 |
|
||||
|------|------|------|------|
|
||||
| **auth_manager** | `scripts/auth_manager.py` | 登录检测、Cookie 导出/注入、等待登录 | 无(纯函数 + Playwright API) |
|
||||
| **cdp_connector** | `scripts/cdp_connector.py` | 浏览器连接(Chrome CDP / Camoufox 原生) | auth_manager(登录检测) |
|
||||
| **douyin_ops** | `scripts/douyin_ops.py` | 抖音原子操作(18个:like/collect/comment/scroll等) | Playwright Page |
|
||||
| **local_paths** | `scripts/local_paths.py` | 统一路径管理 | 无 |
|
||||
| **page_state** | `scripts/page_state.py` | 页面模式检测、状态建议 | Playwright Page |
|
||||
| **task_engine** | `scripts/task_engine.py` | 蓝图加载与逐步执行 | douyin_ops |
|
||||
| **yanghao_runner** | `scripts/yanghao_runner.py` | 完整养号流程入口(交互/参数双模式) | cdp_connector + douyin_ops |
|
||||
| **switch_account** | `scripts/switch_account.py` | 账号切换、状态查看 | auth_manager |
|
||||
| **full_test** | `scripts/full_test.py` | 全面测试 + 报告生成 | task_engine + auth_manager |
|
||||
|
||||
---
|
||||
|
||||
## 3. 登录管理(auth_manager 原子操作)
|
||||
|
||||
### 3.1 检测流程
|
||||
|
||||
```
|
||||
get_login_status(context, page)
|
||||
│
|
||||
├── Cookie 检测(主方案)
|
||||
│ ├── 遍历所有 Cookie
|
||||
│ ├── 检查 sessionid / sid_guard 是否存在
|
||||
│ └── 返回 cookie_ok + cookie_count + session_id
|
||||
│
|
||||
└── DOM 检测(备选,仅移动端视口有效)
|
||||
├── 检查 [data-e2e='user-avatar']
|
||||
├── 检查 [data-e2e='user-detail']
|
||||
└── 返回 dom_ok
|
||||
|
||||
└── 综合判断
|
||||
├── cookie_ok + dom_ok → logged_in=True, method="both"
|
||||
├── cookie_ok only → logged_in=True, method="cookie"
|
||||
├── dom_ok only → logged_in=True, method="dom"
|
||||
└── 均无 → logged_in=False
|
||||
```
|
||||
|
||||
### 3.2 可用 API
|
||||
|
||||
| 函数 | 用途 | 返回 |
|
||||
|------|------|------|
|
||||
| `check_login_by_cookie(context)` | Cookie 检测 | bool |
|
||||
| `check_login_by_dom(page, platform)` | DOM 检测 | bool |
|
||||
| `get_login_status(context, page)` | 多维综合检测 | dict |
|
||||
| `export_cookies(context, path)` | 导出 Cookie | int(数量) |
|
||||
| `inject_cookies(context, path)` | 注入 Cookie | int(数量) |
|
||||
| `wait_for_login(context, page)` | 等待手动登录 | dict |
|
||||
| `get_session_id(cookies)` | 提取 sessionid | str/None |
|
||||
|
||||
### 3.3 持久化策略
|
||||
|
||||
```
|
||||
Chrome 方案:Profile 目录持久化(浏览器关闭重启后 Cookie 自动保留)
|
||||
↓
|
||||
首次切换/登录后 → auth_manager.export_cookies() → data/cookies/{id}_cookies.json
|
||||
↓
|
||||
Camoufox 启动时 → auth_manager.inject_cookies() → 从 Cookie 文件恢复登录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 浏览器方案对比
|
||||
|
||||
| 维度 | Chrome CDP | Camoufox (Firefox) |
|
||||
|------|-----------|-------------------|
|
||||
| 启动入口 | `switch_account.py --method profile` | `CDPConnector(browser_type="camoufox")` |
|
||||
| 登录方式 | Chrome Profile 持久化(自动) | Cookie 注入(从文件) |
|
||||
| 登录检测 | auth_manager Cookie 检测 | auth_manager Cookie 检测 |
|
||||
| 操作系统指纹 | 实际 macOS(CDP 覆盖 UA) | 固定 Windows(`os='windows'`) |
|
||||
| 中文字体 | 系统自带 | 需指定 fonts 参数 |
|
||||
| 反检测 | CDP 指纹注入 + JS 注入 | Camoufox 内置 + humanize |
|
||||
| 适用场景 | 主力养号(doyin_01/02) | 辅助养号(douyin_camo01/02) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 蓝图执行链路
|
||||
|
||||
```
|
||||
yanghao_runner.py / task_engine.py
|
||||
│
|
||||
├── 1. CDPConnector.connect() → 启动/连接浏览器
|
||||
├── 2. conn.init_anti_detection() → 指纹注入
|
||||
├── 3. auth_manager.get_login_status() → 验证登录
|
||||
├── 4. 加载 blueprint JSON → 读取步骤
|
||||
├── 5. DouyinOps(page) → 绑定操作对象
|
||||
├── 6. 逐步骤执行:
|
||||
│ ├── goto_home()
|
||||
│ ├── wait_watch(seconds)
|
||||
│ ├── like(probability)
|
||||
│ ├── collect(probability)
|
||||
│ ├── next_video()
|
||||
│ ├── search(keyword)
|
||||
│ ├── post_comment(text)
|
||||
│ └── ...
|
||||
└── 7. 记录执行结果 → matrix.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 已知问题跟踪
|
||||
|
||||
| ID | 状态 | 问题 | 涉及文件 | 修复日期 |
|
||||
|----|------|------|---------|---------|
|
||||
| P0 | ✅ 已修复 | DOM 登录检测桌面端误报 | auth_manager.py + switch_account.py | 2026-05-01 |
|
||||
| P1 | ✅ 已修复 | Camoufox 显示 macOS | cdp_connector.py | 2026-05-01 |
|
||||
| P1 | ✅ 已修复 | Camoufox 中文乱码 | cdp_connector.py | 2026-05-01 |
|
||||
| P2 | ✅ 已修复 | douyin_browse_v3/nurture_v1 未注册 DB | DB task_blueprints | 2026-05-01 |
|
||||
| P3 | ✅ 已修复 | 冗余 profiles/douyin_01 目录(11MB) | 文件系统 | 2026-05-01 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 快速操作指南
|
||||
|
||||
```bash
|
||||
TOOL=~/workbuddy-agent-os/agent-sync/05_tools/07_matrix
|
||||
|
||||
# 查看账号状态
|
||||
python $TOOL/scripts/switch_account.py --status
|
||||
python $TOOL/scripts/auth_manager.py 9222 # 独立登录检测
|
||||
|
||||
# 启动 Chrome + 切换账号 + 自动导出 Cookie
|
||||
python $TOOL/scripts/switch_account.py --method profile --target douyin_01 --port 9222
|
||||
|
||||
# 启动 Camoufox + Cookie 注入 + 执行蓝图(正确方式)
|
||||
python $TOOL/scripts/yanghao_runner.py --account douyin_camo01 --blueprint douyin_browse_v2 --browser camoufox
|
||||
|
||||
# 完整测试
|
||||
python $TOOL/scripts/full_test.py --account-only douyin_01
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# 修复 Dashboard launchd 配置
|
||||
# 自动检测当前机器的 Python 路径并更新 plist
|
||||
# 用法: bash fix_dashboard_launchd.sh
|
||||
|
||||
PLIST="$HOME/Library/LaunchAgents/com.agentos.dashboard.plist"
|
||||
|
||||
# 检测 Python
|
||||
PYTHON=""
|
||||
if [ -f "$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3" ]; then
|
||||
PYTHON="$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
elif [ -f "$HOME/.workbuddy/binaries/python/envs/dashboard/bin/python" ]; then
|
||||
PYTHON="$HOME/.workbuddy/binaries/python/envs/dashboard/bin/python"
|
||||
elif command -v python3 &>/dev/null; then
|
||||
PYTHON="$(which python3)"
|
||||
else
|
||||
echo "❌ 找不到 Python 3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 检测到 Python: $PYTHON"
|
||||
|
||||
# 停止旧服务
|
||||
echo "🛑 停止旧服务..."
|
||||
launchctl bootout gui/$(id -u)/com.agentos.dashboard 2>/dev/null || true
|
||||
|
||||
# 生成新的 plist
|
||||
DASHBOARD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../10_dashboard"
|
||||
|
||||
cat > "$PLIST" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.agentos.dashboard</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$PYTHON</string>
|
||||
<string>-m</string>
|
||||
<string>uvicorn</string>
|
||||
<string>app:app</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--port</string>
|
||||
<string>9988</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$DASHBOARD_DIR</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/dashboard_launchd.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/dashboard_launchd_err.log</string>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "✅ plist 已更新: $PLIST"
|
||||
|
||||
# 启动服务
|
||||
echo "🚀 启动服务..."
|
||||
launchctl bootstrap gui/$(id -u) "$PLIST" 2>/dev/null || \
|
||||
launchctl kickstart -k gui/$(id -u)/com.agentos.dashboard 2>/dev/null || true
|
||||
|
||||
echo "✅ Dashboard 已启动"
|
||||
echo " 访问: http://localhost:9988"
|
||||
echo " 日志: /tmp/dashboard_launchd.log"
|
||||
@@ -0,0 +1,496 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================
|
||||
# Matrix 环境检测 & 初始化脚本 v5.2
|
||||
#
|
||||
# 流程:
|
||||
# 1. 检测环境 → 输出报告
|
||||
# 2. 用户确认 → 执行修复
|
||||
# 3. 验证结果
|
||||
#
|
||||
# 用法:
|
||||
# bash init_matrix.sh # 完整流程
|
||||
# bash init_matrix.sh --check # 仅检测,不执行
|
||||
# bash init_matrix.sh --apply # 跳过检测,直接执行
|
||||
# ==============================================================
|
||||
|
||||
set -e
|
||||
MODE="${1:-full}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
AGENT_SYNC_DIR="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || echo "$HOME/workbuddy-agent-os/agent-sync")"
|
||||
AGENT_LOCAL_DIR="$HOME/workbuddy-agent-os/agent-local"
|
||||
MATRIX_CODE="$AGENT_SYNC_DIR/05_tools/07_matrix"
|
||||
DASHBOARD_DIR="$AGENT_SYNC_DIR/05_tools/10_dashboard"
|
||||
PLIST="$HOME/Library/LaunchAgents/com.agentos.dashboard.plist"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 检测函数
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
check_python() {
|
||||
local candidates=(
|
||||
"$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
"$HOME/.workbuddy/binaries/python/envs/dashboard/bin/python"
|
||||
"$(which python3 2>/dev/null || echo "")"
|
||||
)
|
||||
for p in "${candidates[@]}"; do
|
||||
if [ -n "$p" ] && [ -x "$p" ]; then
|
||||
echo "$p"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
check_module() {
|
||||
local module="$1"
|
||||
local py="$2"
|
||||
"$py" -c "import $module" 2>/dev/null && return 0 || return 1
|
||||
}
|
||||
|
||||
check_file() {
|
||||
[ -f "$1" ] && return 0 || return 1
|
||||
}
|
||||
|
||||
check_dir() {
|
||||
[ -d "$1" ] && return 0 || return 1
|
||||
}
|
||||
|
||||
check_launchd() {
|
||||
if [ -f "$PLIST" ]; then
|
||||
# 检查 plist 中的 Python 路径是否和当前环境匹配
|
||||
local plist_py=$(grep -A1 'ProgramArguments' "$PLIST" 2>/dev/null | grep -v "ProgramArguments\|--\|array\|string" | head -1 | sed 's/.*<string>//;s/<\/string>.*//')
|
||||
if [ -n "$plist_py" ] && [ -x "$plist_py" ]; then
|
||||
if launchctl list | grep -q "com.agentos.dashboard" 2>/dev/null; then
|
||||
echo "running"
|
||||
else
|
||||
echo "stopped"
|
||||
fi
|
||||
else
|
||||
echo "broken_path"
|
||||
fi
|
||||
else
|
||||
echo "missing"
|
||||
fi
|
||||
}
|
||||
|
||||
check_git() {
|
||||
if [ -d "$AGENT_SYNC_DIR/.git" ]; then
|
||||
cd "$AGENT_SYNC_DIR"
|
||||
local behind=$(git rev-list HEAD..@{u} 2>/dev/null | wc -l)
|
||||
if [ "$behind" -gt 0 ]; then
|
||||
echo "behind($behind)"
|
||||
else
|
||||
echo "uptodate"
|
||||
fi
|
||||
else
|
||||
echo "norepo"
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 阶段1:检测环境
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════╗"
|
||||
echo "║ Matrix 环境检测 v5.2 ║"
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
PYTHON=$(check_python)
|
||||
ISSUES=0
|
||||
FIXES=""
|
||||
|
||||
echo "📋 ─── 1. 基础环境 ───"
|
||||
echo -n " Python 3: "
|
||||
if [ -n "$PYTHON" ]; then
|
||||
echo "✅ $($PYTHON --version 2>&1)"
|
||||
else
|
||||
echo "❌ 未找到"
|
||||
ISSUES=$((ISSUES+1))
|
||||
FIXES="$FIXES\n - 安装 Python 3"
|
||||
fi
|
||||
|
||||
echo -n " 代码仓库: "
|
||||
GIT_STATUS=$(check_git)
|
||||
case "$GIT_STATUS" in
|
||||
uptodate) echo "✅ 已是最新" ;;
|
||||
behind*) echo "⚠️ 落后远程 $(echo $GIT_STATUS | grep -oP '\d+') 个提交"
|
||||
FIXES="$FIXES\n - git pull 拉取更新" ;;
|
||||
norepo) echo "❌ 未克隆"
|
||||
ISSUES=$((ISSUES+1))
|
||||
FIXES="$FIXES\n - git clone 克隆仓库" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "📦 ─── 2. Python 依赖 ───"
|
||||
DEPS=("httpx" "yaml" "fastapi" "uvicorn" "playwright")
|
||||
DEP_NAMES=("httpx" "pyyaml" "fastapi" "uvicorn" "playwright")
|
||||
MISSING_DEPS=()
|
||||
for i in "${!DEPS[@]}"; do
|
||||
echo -n " ${DEP_NAMES[$i]}: "
|
||||
if check_module "${DEPS[$i]}" "$PYTHON"; then
|
||||
echo "✅"
|
||||
else
|
||||
echo "❌"
|
||||
MISSING_DEPS+=("${DEP_NAMES[$i]}")
|
||||
fi
|
||||
done
|
||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||
ISSUES=$((ISSUES+1))
|
||||
FIXES="$FIXES\n - pip install ${MISSING_DEPS[*]}"
|
||||
fi
|
||||
|
||||
echo -n " Camoufox: "
|
||||
if check_module "camoufox" "$PYTHON"; then
|
||||
echo "✅"
|
||||
else
|
||||
echo "❌"
|
||||
FIXES="$FIXES\n - pip install camoufox && python -m camoufox fetch"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📁 ─── 3. 目录结构 ───"
|
||||
for d in \
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/config" \
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/identities" \
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/data" \
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/recordings"; do
|
||||
name=$(basename "$(dirname "$d")")/$(basename "$d")
|
||||
echo -n " agent-local/$name: "
|
||||
if check_dir "$d"; then echo "✅"; else echo "❌"; FIXES="$FIXES\n - mkdir -p $d"; fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "⚙️ ─── 4. 配置文件 ───"
|
||||
CONFIGS=(
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/config/accounts.yaml:账号配置"
|
||||
"$MATRIX_CODE/config/sms.yaml:短信API"
|
||||
"$MATRIX_CODE/config/ai.yaml:AI配置"
|
||||
"$MATRIX_CODE/config/schedule.yaml:定时任务"
|
||||
"$AGENT_LOCAL_DIR/tools/matrix/data/profiles.json:人设数据"
|
||||
)
|
||||
for entry in "${CONFIGS[@]}"; do
|
||||
path="${entry%%:*}"
|
||||
label="${entry##*:}"
|
||||
echo -n " $label: "
|
||||
if check_file "$path"; then
|
||||
echo "✅"
|
||||
else
|
||||
echo "❌(将从模板创建)"
|
||||
FIXES="$FIXES\n - cp config_template/xxx $path"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🔧 ─── 5. 系统服务 ───"
|
||||
echo -n " mc 命令: "
|
||||
if command -v mc &>/dev/null; then
|
||||
local_mc=$(which mc)
|
||||
repo_mc="$MATRIX_CODE/mc"
|
||||
if [ "$local_mc" = "$repo_mc" ] || [ "$(readlink "$local_mc" 2>/dev/null)" = "$repo_mc" ]; then
|
||||
echo "✅ $local_mc"
|
||||
else
|
||||
echo "⚠️ $local_mc(指向其他位置)"
|
||||
FIXES="$FIXES\n - ln -sf $repo_mc /usr/local/bin/mc"
|
||||
fi
|
||||
else
|
||||
echo "❌"
|
||||
FIXES="$FIXES\n - ln -sf $MATRIX_CODE/mc /usr/local/bin/mc"
|
||||
fi
|
||||
|
||||
echo -n " mc 包装器路径: "
|
||||
if grep -q "/Users/5kecheng\|/Users/chengzige" "$MATRIX_CODE/mc" 2>/dev/null; then
|
||||
echo "❌ 含硬编码路径"
|
||||
FIXES="$FIXES\n - git checkout -- 05_tools/07_matrix/mc(恢复自动检测版本)"
|
||||
else
|
||||
echo "✅ 自动检测"
|
||||
fi
|
||||
|
||||
echo -n " Dashboard 服务: "
|
||||
L_STATUS=$(check_launchd)
|
||||
case "$L_STATUS" in
|
||||
running) echo "✅ 运行中" ;;
|
||||
stopped) echo "⚠️ 已停止"
|
||||
FIXES="$FIXES\n - launchctl kickstart com.agentos.dashboard" ;;
|
||||
broken_path)
|
||||
broken_py=$(grep -A1 'ProgramArguments' "$PLIST" 2>/dev/null | grep -v "ProgramArguments\|--\|array\|string" | head -1 | sed 's/.*<string>//;s/<\/string>.*//')
|
||||
echo "❌ Python 路径错误: $broken_py"
|
||||
FIXES="$FIXES\n - 运行本脚本自动修复 plist" ;;
|
||||
missing) echo "❌ 未配置"
|
||||
FIXES="$FIXES\n - 运行本脚本自动创建 plist" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "📊 ─── 6. 蓝图 & 语料库 ───"
|
||||
BP_COUNT=$(ls "$MATRIX_CODE/blueprints/"*.json 2>/dev/null | wc -l)
|
||||
echo -n " 蓝图文件: "
|
||||
if [ "$BP_COUNT" -gt 0 ]; then echo "✅ $BP_COUNT 个"; else echo "❌"; fi
|
||||
|
||||
echo -n " 语料库(抖音): "
|
||||
if [ -f "$MATRIX_CODE/corpus/douyin.yaml" ]; then echo "✅"; else echo "❌"; fi
|
||||
|
||||
echo -n " 语料库(小红书): "
|
||||
if [ -f "$MATRIX_CODE/corpus/xiaohongshu.yaml" ]; then echo "✅"; else echo "❌"; fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 7. 身份共享检测
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo "📊 ─── 7. 身份共享检查 ───"
|
||||
IDENTITY_BASE="$AGENT_LOCAL/tools/matrix/identities"
|
||||
ACCOUNTS_YAML="$AGENT_LOCAL/tools/matrix/config/accounts.yaml"
|
||||
|
||||
IDENTITY_ISSUES=0
|
||||
if [ -f "$ACCOUNTS_YAML" ]; then
|
||||
# 用 Python 检测身份共享状态
|
||||
python3 << 'PYEOF' 2>/dev/null || true
|
||||
import yaml, os, shutil
|
||||
from pathlib import Path
|
||||
|
||||
HOME = Path.home()
|
||||
ACCOUNTS = HOME / "workbuddy-agent-os/agent-local/tools/matrix/config/accounts.yaml"
|
||||
IDENTITIES = HOME / "workbuddy-agent-os/agent-local/tools/matrix/identities"
|
||||
|
||||
if not ACCOUNTS.exists():
|
||||
print("UNCONFIGURED")
|
||||
exit(0)
|
||||
|
||||
data = yaml.safe_load(ACCOUNTS.read_text())
|
||||
accts = data.get("accounts", []) if isinstance(data, dict) else data if isinstance(data, list) else []
|
||||
|
||||
# 按手机号分组
|
||||
phones = {}
|
||||
for a in accts:
|
||||
phone = a.get("phone", "")
|
||||
if phone:
|
||||
phones.setdefault(phone, []).append(a)
|
||||
|
||||
shared = 0
|
||||
not_shared = 0
|
||||
fixes = []
|
||||
|
||||
for phone, group in phones.items():
|
||||
if len(group) < 2:
|
||||
continue
|
||||
dirs = set(a.get("identity_dir", a["id"]) for a in group)
|
||||
if len(dirs) == 1:
|
||||
shared += 1
|
||||
else:
|
||||
not_shared += 1
|
||||
# 找有 user_data 的作为源
|
||||
source = None
|
||||
for a in group:
|
||||
d = a.get("identity_dir", a["id"]).replace("identities/", "")
|
||||
ud = IDENTITIES / d / "user_data"
|
||||
if ud.exists() and any(ud.iterdir()):
|
||||
source = d
|
||||
break
|
||||
shared_dir = f"phone_{phone}"
|
||||
fixes.append(f"PHONE:{phone}|SRC:{source or 'none'}|DIR:{shared_dir}")
|
||||
|
||||
print(f"SHARED:{shared}")
|
||||
print(f"NOT_SHARED:{not_shared}")
|
||||
for f in fixes:
|
||||
print(f"FIX:{f}")
|
||||
PYEOF
|
||||
else:
|
||||
echo " accounts.yaml 不存在,跳过"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 检测汇总
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════╗"
|
||||
if [ "$ISSUES" -eq 0 ] && [ -z "$FIXES" ]; then
|
||||
echo "║ ✅ 环境检查通过,无需修复 ║"
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "║ ⚠️ 发现以下问题: ║"
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo -e "$FIXES" | while read line; do
|
||||
[ -n "$line" ] && echo " $line"
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ "$MODE" = "--check" ]; then
|
||||
echo "🔍 检测模式,未执行任何操作。"
|
||||
echo " 执行 bash init_matrix.sh 开始修复。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 阶段2:用户确认
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "即将执行以下操作:"
|
||||
echo ""
|
||||
[ -n "$(echo -e "$FIXES" | grep "git pull\|git clone")" ] && echo " • 拉取/克隆代码仓库"
|
||||
[ -n "$(echo -e "$FIXES" | grep "pip install")" ] && echo " • 安装 Python 依赖"
|
||||
[ -n "$(echo -e "$FIXES" | grep "mkdir")" ] && echo " • 创建目录结构"
|
||||
[ -n "$(echo -e "$FIXES" | grep "cp config_template")" ] && echo " • 从模板创建配置文件"
|
||||
[ -n "$(echo -e "$FIXES" | grep "ln -sf")" ] && echo " • 安装 mc 命令"
|
||||
[ -n "$(echo -e "$FIXES" | grep "plist\|launchctl")" ] && echo " • 修复 Dashboard 启动配置"
|
||||
[ -n "$(echo -e "$FIXES" | grep "git checkout.*mc")" ] && echo " • 修复 mc 包装器硬编码路径"
|
||||
echo ""
|
||||
echo -n "确认执行?(y/N): "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ] && [ "$confirm" != "yes" ]; then
|
||||
echo "已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 阶段3:执行修复
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo "🛠 开始修复..."
|
||||
|
||||
# 1. 拉取代码
|
||||
if [ -d "$AGENT_SYNC_DIR/.git" ]; then
|
||||
echo " 📦 git pull..."
|
||||
cd "$AGENT_SYNC_DIR" && git pull 2>/dev/null || true
|
||||
else
|
||||
echo " 📦 git clone..."
|
||||
mkdir -p "$HOME/workbuddy-agent-os"
|
||||
cd "$HOME/workbuddy-agent-os"
|
||||
git clone [email protected]:neowoodland-art/mac-agent-os.git agent-sync 2>/dev/null || \
|
||||
git clone [email protected]:babycalf/mac-agent-os.git agent-sync 2>/dev/null || true
|
||||
AGENT_SYNC_DIR="$HOME/workbuddy-agent-os/agent-sync"
|
||||
MATRIX_CODE="$AGENT_SYNC_DIR/05_tools/07_matrix"
|
||||
DASHBOARD_DIR="$AGENT_SYNC_DIR/05_tools/10_dashboard"
|
||||
fi
|
||||
|
||||
# 2. 创建目录
|
||||
echo " 📁 创建目录..."
|
||||
mkdir -p "$AGENT_LOCAL_DIR/tools/matrix/config"
|
||||
mkdir -p "$AGENT_LOCAL_DIR/tools/matrix/identities"
|
||||
mkdir -p "$AGENT_LOCAL_DIR/tools/matrix/data"
|
||||
mkdir -p "$AGENT_LOCAL_DIR/tools/matrix/recordings"
|
||||
mkdir -p "$AGENT_LOCAL_DIR/tools/matrix/logs"
|
||||
|
||||
# 3. 安装依赖
|
||||
echo " 📦 安装 Python 依赖..."
|
||||
$PYTHON -m pip install -q --upgrade pip 2>/dev/null || true
|
||||
for dep in httpx pyyaml requests fastapi uvicorn playwright; do
|
||||
$PYTHON -m pip install -q "$dep" 2>/dev/null || true
|
||||
done
|
||||
if [ -f "$MATRIX_CODE/requirements.txt" ]; then
|
||||
$PYTHON -m pip install -q -r "$MATRIX_CODE/requirements.txt" 2>/dev/null || true
|
||||
fi
|
||||
if ! check_module "camoufox" "$PYTHON"; then
|
||||
echo " 📥 安装 Camoufox..."
|
||||
$PYTHON -m pip install -q camoufox 2>/dev/null || true
|
||||
$PYTHON -m camoufox fetch 2>&1 | tail -1 || true
|
||||
fi
|
||||
|
||||
# 4. 创建配置
|
||||
echo " ⚙️ 创建配置文件..."
|
||||
[ ! -f "$AGENT_LOCAL_DIR/tools/matrix/config/accounts.yaml" ] && \
|
||||
cp "$MATRIX_CODE/config_template/accounts.yaml" "$AGENT_LOCAL_DIR/tools/matrix/config/accounts.yaml" && \
|
||||
echo " ✅ accounts.yaml"
|
||||
[ ! -f "$MATRIX_CODE/config/sms.yaml" ] && \
|
||||
cp "$MATRIX_CODE/config_template/sms.yaml" "$MATRIX_CODE/config/sms.yaml" 2>/dev/null && \
|
||||
echo " ✅ sms.yaml"
|
||||
[ ! -f "$MATRIX_CODE/config/ai.yaml" ] && \
|
||||
cp "$MATRIX_CODE/config_template/ai.yaml" "$MATRIX_CODE/config/ai.yaml" 2>/dev/null && \
|
||||
echo " ✅ ai.yaml"
|
||||
[ ! -f "$MATRIX_CODE/config/schedule.yaml" ] && \
|
||||
cp "$MATRIX_CODE/config_template/schedule.yaml" "$MATRIX_CODE/config/schedule.yaml" 2>/dev/null && \
|
||||
echo " ✅ schedule.yaml"
|
||||
[ ! -f "$AGENT_LOCAL_DIR/tools/matrix/data/profiles.json" ] && \
|
||||
cp "$MATRIX_CODE/config_template/profiles.json" "$AGENT_LOCAL_DIR/tools/matrix/data/profiles.json" 2>/dev/null && \
|
||||
echo " ✅ profiles.json"
|
||||
|
||||
# 5. 修复 mc 包装器
|
||||
if grep -q "/Users/5kecheng\|/Users/chengzige" "$MATRIX_CODE/mc" 2>/dev/null; then
|
||||
echo " 🔧 修复 mc 包装器..."
|
||||
cd "$AGENT_SYNC_DIR" && git checkout -- 05_tools/07_matrix/mc
|
||||
fi
|
||||
if ! command -v mc &>/dev/null; then
|
||||
sudo ln -sf "$MATRIX_CODE/mc" /usr/local/bin/mc 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 6. 修复 launchd
|
||||
echo " 🚀 修复 Dashboard 启动配置..."
|
||||
launchctl bootout gui/$(id -u)/com.agentos.dashboard 2>/dev/null || true
|
||||
cat > "$PLIST" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.agentos.dashboard</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$PYTHON</string>
|
||||
<string>-m</string>
|
||||
<string>uvicorn</string>
|
||||
<string>app:app</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--port</string>
|
||||
<string>9988</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$DASHBOARD_DIR</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/dashboard_launchd.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/dashboard_launchd_err.log</string>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
launchctl bootstrap gui/$(id -u) "$PLIST" 2>/dev/null || \
|
||||
launchctl kickstart -k gui/$(id -u)/com.agentos.dashboard 2>/dev/null || true
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 阶段4:验证
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
# 清除 Python 缓存(防止旧 .pyc 影响)
|
||||
echo " 🧹 清除 Python 缓存..."
|
||||
find "$AGENT_SYNC_DIR" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$AGENT_SYNC_DIR" -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
echo "🔍 验证..."
|
||||
sleep 4
|
||||
|
||||
echo -n " Dashboard: "
|
||||
if curl -s http://localhost:9988/api/matrix/recordings/status >/dev/null 2>&1; then
|
||||
echo "✅ http://localhost:9988"
|
||||
else
|
||||
echo "⚠️ 检查日志: tail -20 /tmp/dashboard_launchd.log"
|
||||
fi
|
||||
|
||||
echo -n " mc task: "
|
||||
if $PYTHON -c "from mc.task import Task" 2>/dev/null; then echo "✅"; else echo "⚠️"; fi
|
||||
|
||||
echo -n " 定时调度: "
|
||||
if $PYTHON -c "from mc.scheduler import load_schedules" 2>/dev/null; then echo "✅"; else echo "⚠️"; fi
|
||||
|
||||
echo -n " 人设系统: "
|
||||
if $PYTHON -c "from mc.corpus import CorpusManager; CorpusManager()" 2>/dev/null; then echo "✅"; else echo "⚠️"; fi
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ 初始化完成 ║"
|
||||
echo "║ Dashboard: http://localhost:9988 ║"
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Matrix 矩阵养号系统 — 新机一键恢复脚本(v4.0 配置文件版)
|
||||
#
|
||||
# 用法:
|
||||
# bash ~/workbuddy-agent-os/agent-sync/05_tools/07_matrix/install.sh
|
||||
#
|
||||
# 功能:
|
||||
# 1. 在 agent-local/tools/matrix/ 建立本机数据目录骨架
|
||||
# 2. 生成本机 local.yaml 路径配置文件(替代软链接,解决多机同步问题)
|
||||
# 3. 清理旧版软链接残留(如有)
|
||||
# 4. 安装 Python 依赖
|
||||
# 5. 提示初始化账号配置
|
||||
#
|
||||
# 设计原则 (v4.0):
|
||||
# - local.yaml 不参与坚果云同步,每台机器独立生成
|
||||
# - Python 脚本统一通过 scripts/local_paths.py 读取路径,不再依赖软链接
|
||||
# - 多机同步只需跑一次 install.sh,互不干扰
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
TOOL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOCAL_DIR="$HOME/workbuddy-agent-os/agent-local/tools/matrix"
|
||||
PYTHON="${WORKBUDDY_PYTHON:-$(which python3)}"
|
||||
|
||||
echo "=========================================="
|
||||
echo " Matrix 矩阵养号系统 — 新机恢复 (v4.0)"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "代码目录: $TOOL_DIR"
|
||||
echo "本地数据: $LOCAL_DIR"
|
||||
echo "Python: $PYTHON"
|
||||
echo ""
|
||||
|
||||
# ── Step 1: 建立本机数据目录骨架 ────────────────────────────
|
||||
echo "▶ [1/5] 建立本地数据目录..."
|
||||
mkdir -p "$LOCAL_DIR/config"
|
||||
mkdir -p "$LOCAL_DIR/data/cookies"
|
||||
mkdir -p "$LOCAL_DIR/data/camoufox_pids"
|
||||
mkdir -p "$LOCAL_DIR/profiles"
|
||||
mkdir -p "$LOCAL_DIR/logs"
|
||||
mkdir -p "$LOCAL_DIR/screenshots"
|
||||
echo " ⚠️ 注意: profiles/ 含 Chrome 登录数据,换机后需重新登录各平台账号"
|
||||
echo " ✅ $LOCAL_DIR"
|
||||
|
||||
# ── Step 2: 生成 local.yaml 配置文件 ─────────────────────────
|
||||
echo "▶ [2/5] 生成 local.yaml 路径配置..."
|
||||
CONF_FILE="$TOOL_DIR/local.yaml"
|
||||
if [ -f "$CONF_FILE" ]; then
|
||||
CURRENT_ROOT=$(grep "local_data_root:" "$CONF_FILE" 2>/dev/null | sed 's/.*local_data_root: *//' | tr -d "\"'" | xargs)
|
||||
if [ "$CURRENT_ROOT" = "$LOCAL_DIR" ]; then
|
||||
echo " ✅ local.yaml 已存在且路径正确,跳过"
|
||||
else
|
||||
echo " ⚠️ local.yaml 存在但路径不一致: $CURRENT_ROOT"
|
||||
echo " → 更新为当前路径: $LOCAL_DIR"
|
||||
cat > "$CONF_FILE" << EOF
|
||||
# Matrix 本机数据目录配置
|
||||
# 此文件由 install.sh 生成,每台机器独立,不参与坚果云同步
|
||||
#
|
||||
# local_data_root: 本机 agent-local/tools/matrix 目录的绝对路径
|
||||
|
||||
matrix:
|
||||
local_data_root: $LOCAL_DIR
|
||||
EOF
|
||||
echo " ✅ local.yaml 已更新"
|
||||
fi
|
||||
else
|
||||
cat > "$CONF_FILE" << EOF
|
||||
# Matrix 本机数据目录配置
|
||||
# 此文件由 install.sh 生成,每台机器独立,不参与坚果云同步
|
||||
#
|
||||
# local_data_root: 本机 agent-local/tools/matrix 目录的绝对路径
|
||||
|
||||
matrix:
|
||||
local_data_root: $LOCAL_DIR
|
||||
EOF
|
||||
echo " ✅ local.yaml 已生成: $CONF_FILE"
|
||||
fi
|
||||
|
||||
# ── Step 3: 清理旧版软链接 ───────────────────────────────────
|
||||
echo "▶ [3/5] 检查旧版软链接残留..."
|
||||
CLEANUP_NEEDED=0
|
||||
for name in config data logs screenshots profiles; do
|
||||
LINK="$TOOL_DIR/$name"
|
||||
if [ -L "$LINK" ]; then
|
||||
TARGET=$(readlink "$LINK")
|
||||
echo " 🧹 发现旧版软链接: $name → $TARGET"
|
||||
rm "$LINK"
|
||||
echo " 已删除(路径已由 local.yaml 管理)"
|
||||
CLEANUP_NEEDED=1
|
||||
elif [ -d "$LINK" ]; then
|
||||
# 可能是坚果云展开的真实目录(含旧数据)
|
||||
echo " ⚠️ $LINK 是真实目录(非软链接)"
|
||||
echo " 可能是坚果云同步时展开的旧软链接数据"
|
||||
echo " 建议检查内容后手动处理或删除"
|
||||
fi
|
||||
done
|
||||
if [ "$CLEANUP_NEEDED" -eq 0 ]; then
|
||||
echo " ✅ 无旧版软链接残留"
|
||||
fi
|
||||
|
||||
# ── Step 4: 安装 Python 依赖 ────────────────────────────────
|
||||
echo "▶ [4/5] 安装 Python 依赖..."
|
||||
if $PYTHON -m pip install -r "$TOOL_DIR/requirements.txt" -q; then
|
||||
echo " ✅ 依赖安装完成"
|
||||
# 安装 patchright 浏览器二进制
|
||||
$PYTHON -m patchright install chromium 2>/dev/null && echo " ✅ Chromium (patchright) 已安装" || echo " ⚠️ Chromium 安装跳过(可稍后手动执行: python -m patchright install chromium)"
|
||||
else
|
||||
echo " ❌ 依赖安装失败,请检查 Python 环境"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Step 5: 配置模板初始化 (统一跨机模板) ──────────────────
|
||||
echo "▶ [5/5] 初始化配置模板..."
|
||||
CONFIG_DIR="$LOCAL_DIR/config"
|
||||
TEMPLATE_DIR="$TOOL_DIR/config_template"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# 从模板复制所有配置(不覆盖已有文件)
|
||||
for tmpl in "$TEMPLATE_DIR"/*; do
|
||||
fname=$(basename "$tmpl")
|
||||
target="$CONFIG_DIR/$fname"
|
||||
if [ ! -f "$target" ]; then
|
||||
cp "$tmpl" "$target"
|
||||
echo " 📋 已安装: $fname"
|
||||
else
|
||||
echo " ✅ 已存在: $fname"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 初始化数据库 ────────────────────────────────────────────
|
||||
DB_FILE="$LOCAL_DIR/data/matrix.db"
|
||||
if [ ! -f "$DB_FILE" ]; then
|
||||
echo ""
|
||||
echo "▶ 初始化数据库..."
|
||||
cd "$TOOL_DIR" && $PYTHON scripts/init_db.py && echo " ✅ 数据库已初始化" || echo " ⚠️ 数据库初始化失败,可稍后手动执行: python scripts/init_db.py"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " ✅ 安装完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "使用方法:"
|
||||
echo " cd $TOOL_DIR"
|
||||
echo " python scripts/switch_account.py --method profile --target douyin_01 --port 9222"
|
||||
echo " python scripts/task_engine.py --blueprint douyin_browse_v2 --account douyin_01"
|
||||
echo ""
|
||||
echo "账号配置文件:"
|
||||
echo " $CONFIG_FILE"
|
||||
echo ""
|
||||
echo "❗ 坚果云同步提示:"
|
||||
echo " 请将 local.yaml 加入坚果云排除列表,避免被同步到其他机器"
|
||||
echo ""
|
||||
exit 0
|
||||
@@ -0,0 +1,8 @@
|
||||
# Matrix 本机数据目录配置
|
||||
# 此文件由 install.sh 生成,每台机器独立,不参与坚果云同步
|
||||
#
|
||||
# local_data_root: 本机 agent-local/tools/matrix 目录的绝对路径
|
||||
# 安装脚本会自动填入正确路径,无需手动编辑
|
||||
|
||||
matrix:
|
||||
local_data_root: {{MATRIX_LOCAL_DATA_ROOT}}
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# mc — Matrix Console 快捷命令
|
||||
# 统一 CLI 入口: 使用 00_setup/agentos 作为 agentos 包路径,调用 matrix 子命令
|
||||
# 安装: chmod +x mc && sudo ln -s $(pwd)/mc /usr/local/bin/mc
|
||||
|
||||
_SRC="${BASH_SOURCE[0]}"
|
||||
while [ -L "$_SRC" ]; do _SRC="$(readlink "$_SRC")"; done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$_SRC")" && pwd)"
|
||||
|
||||
PYTHON=""
|
||||
if [ -n "$AGENTOS_PYTHON" ] && [ -x "$AGENTOS_PYTHON" ]; then
|
||||
PYTHON="$AGENTOS_PYTHON"
|
||||
elif [ -f "$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3" ]; then
|
||||
PYTHON="$HOME/.workbuddy/binaries/python/envs/agent-os/bin/python3"
|
||||
elif command -v python3 &>/dev/null; then
|
||||
PYTHON="python3"
|
||||
else
|
||||
echo "找不到 Python 3,请设置 AGENTOS_PYTHON 环境变量" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 使用统一 CLI: 00_setup/agentos 作为包路径
|
||||
AGENTOS_PKG="$SCRIPT_DIR/../../00_setup/agentos"
|
||||
if [ -d "$AGENTOS_PKG" ]; then
|
||||
cd "$SCRIPT_DIR/../../00_setup"
|
||||
exec "$PYTHON" -m agentos matrix "$@"
|
||||
else
|
||||
# 降级: 使用 scripts/ 下的 agentos
|
||||
cd "$SCRIPT_DIR/scripts"
|
||||
exec "$PYTHON" -m agentos matrix "$@"
|
||||
fi
|
||||
@@ -0,0 +1,40 @@
|
||||
"""MC 平台插件 — 自动发现与注册"""
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
_registry = {}
|
||||
|
||||
|
||||
def discover_platforms():
|
||||
"""自动扫描 platforms/ 目录下的所有插件"""
|
||||
global _registry
|
||||
_registry = {}
|
||||
here = Path(__file__).parent
|
||||
for entry in here.iterdir():
|
||||
if not entry.is_dir() or entry.name.startswith("__"):
|
||||
continue
|
||||
init_file = entry / "__init__.py"
|
||||
if not init_file.exists():
|
||||
continue
|
||||
try:
|
||||
mod = importlib.import_module(f"platforms.{entry.name}")
|
||||
if hasattr(mod, "register_platform"):
|
||||
name, instance = mod.register_platform()
|
||||
_registry[name] = instance
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 插件 {entry.name} 加载失败: {e}")
|
||||
return _registry
|
||||
|
||||
|
||||
def get_platform(name):
|
||||
if not _registry:
|
||||
discover_platforms()
|
||||
return _registry.get(name)
|
||||
|
||||
|
||||
def list_platforms():
|
||||
if not _registry:
|
||||
discover_platforms()
|
||||
return dict(_registry)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""MC 平台插件基类"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BasePlatform:
|
||||
"""平台插件基类
|
||||
|
||||
所有平台插件继承此类, 实现标准接口。
|
||||
未实现的接口返回 {"status": "error", "message": "暂不支持"}。
|
||||
"""
|
||||
|
||||
name: str = "" # 平台标识: 'douyin'
|
||||
display_name: str = "" # 显示名: '抖音'
|
||||
version: str = "1.0.0"
|
||||
description: str = ""
|
||||
|
||||
# ── 登录 ──
|
||||
def login(self, account_name: str, headless: bool = False) -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
def logout(self, account_name: str) -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
def status(self, account_name: str) -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
# ── 采集 ──
|
||||
def collect(self, account_name: str) -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
# ── 发布 ──
|
||||
def publish(self, account_name: str, file_path: str = "",
|
||||
title: str = "", desc: str = "") -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
# ── 养号 ──
|
||||
def nurture(self, account_name: str, blueprint: str = "daily") -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
# ── 互动 ──
|
||||
def interact(self, account_name: str, action: str = "like",
|
||||
target: str = "") -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
# ── 搜索 ──
|
||||
def search(self, keyword: str, limit: int = 10) -> dict:
|
||||
return {"status": "error", "message": "暂不支持"}
|
||||
|
||||
|
||||
def not_implemented(msg="暂不支持"):
|
||||
"""返回标准未实现响应"""
|
||||
return {"status": "error", "message": msg}
|
||||
@@ -0,0 +1,19 @@
|
||||
# 抖音平台插件
|
||||
|
||||
## 能力
|
||||
|
||||
| 接口 | 状态 | 说明 |
|
||||
|------|:----:|------|
|
||||
| login | ✅ | 打开浏览器扫码登录 |
|
||||
| collect | ✅ | 采集抖音主页信息 |
|
||||
| status | ✅ | 检查登录状态 |
|
||||
| publish | 📋 | 计划中 (集成 social-auto-upload) |
|
||||
| nurture | ✅ | 执行养号蓝图 |
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
mc douyin collect --account 手机号
|
||||
mc douyin login --account 手机号
|
||||
mc douyin status --account 手机号
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
"""抖音平台插件"""
|
||||
|
||||
from .plugin import DouyinPlatform
|
||||
|
||||
|
||||
def register_platform():
|
||||
return "douyin", DouyinPlatform()
|
||||
@@ -0,0 +1,103 @@
|
||||
"""抖音插件实现 — 当前包装现有脚本
|
||||
|
||||
⚠️ DEPRECATED: 本插件层的功能已被 CommandBus + mc 引擎取代。
|
||||
所有执行操作请走:
|
||||
- 看板: POST /api/ops/run {type:'collect'|'nurture'|'login'|'comment', accounts, params}
|
||||
- CLI: mc run / mc collect / mc smart-login
|
||||
platforms/ 层将在后续清理中移除。
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from platforms.base import BasePlatform
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent / "scripts"
|
||||
AGENT_LOCAL = Path.home() / "workbuddy-agent-os" / "agent-local"
|
||||
|
||||
|
||||
class DouyinPlatform(BasePlatform):
|
||||
name = "douyin"
|
||||
display_name = "抖音"
|
||||
version = "1.0.0"
|
||||
description = "抖音账号管理: 登录/采集/养号"
|
||||
|
||||
def _resolve_account(self, account_name: str) -> str:
|
||||
"""通过手机号或账号ID找到 identity_dir"""
|
||||
try:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if a.get("phone") == account_name or a["id"] == account_name:
|
||||
if a.get("platform") == "douyin":
|
||||
return a.get("identity_dir", a["id"]).replace("identities/", "")
|
||||
except: pass
|
||||
return account_name
|
||||
|
||||
def collect(self, account_name: str) -> dict:
|
||||
"""采集主页信息(建议通过 mc run --blueprints=douyin_read_profile 执行)"""
|
||||
# 旧路径:引用已归档的 collect_homepage_info.py(不存在)
|
||||
# 改为通过 CommandBus 分发 mc run 采集蓝图
|
||||
try:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR.parent.parent / "10_dashboard"))
|
||||
from services.command_bus import CommandBus
|
||||
result = CommandBus.dispatch("collect", [account_name], {"rounds": 1})
|
||||
status = "ok" if result.get("status") in ("accepted", "completed") else "error"
|
||||
return {"status": status, "message": f"已通过 CommandBus 分发: {result.get('status', '?')}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"CommandBus 分发失败: {e}"}
|
||||
|
||||
def login(self, account_name: str, headless: bool = False) -> dict:
|
||||
"""打开浏览器登录"""
|
||||
login_script = SCRIPTS_DIR / "login_identity.py"
|
||||
identity = self._resolve_account(account_name)
|
||||
cmd = [sys.executable, str(login_script), identity, "--platform", "douyin"]
|
||||
try:
|
||||
subprocess.Popen(cmd, cwd=str(SCRIPTS_DIR))
|
||||
return {"status": "ok", "message": f"浏览器已打开, 请扫码登录 {account_name}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def status(self, account_name: str) -> dict:
|
||||
"""检查登录状态"""
|
||||
try:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
from auth_manager import check_login_by_cookie_sync, get_session_id
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if (a.get("phone") == account_name or a["id"] == account_name) and a.get("platform") == "douyin":
|
||||
# 检查 Cookie
|
||||
cookies_path = AGENT_LOCAL / "identities" / (a.get("identity_dir", a["id"]).replace("identities/", "")) / "user_data" / "cookies.sqlite"
|
||||
if cookies_path.exists():
|
||||
return {"status": "logged_in", "account": a["id"]}
|
||||
return {"status": "no_cookie", "account": a["id"]}
|
||||
return {"status": "not_found"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def publish(self, account_name: str, file_path: str = "", title: str = "", desc: str = "") -> dict:
|
||||
"""发布视频到抖音"""
|
||||
identity = self._resolve_account(account_name)
|
||||
publish_script = SCRIPTS_DIR / "publish_video.py"
|
||||
if not Path(file_path).exists():
|
||||
return {"status": "error", "message": f"文件不存在: {file_path}"}
|
||||
cmd = [sys.executable, str(publish_script), "douyin",
|
||||
"--account", identity, "--file", str(Path(file_path).resolve())]
|
||||
if title:
|
||||
cmd += ["--title", title]
|
||||
if desc:
|
||||
cmd += ["--desc", desc]
|
||||
try:
|
||||
p = subprocess.run(cmd, cwd=str(SCRIPTS_DIR), capture_output=True, text=True, timeout=300)
|
||||
return {"status": "ok" if p.returncode == 0 else "error", "output": p.stdout[-500:]}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"status": "error", "message": "发布超时 (300s)"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
Douyin = DouyinPlatform
|
||||
@@ -0,0 +1,18 @@
|
||||
# 小红书平台插件
|
||||
|
||||
## 能力
|
||||
|
||||
| 接口 | 状态 | 说明 |
|
||||
|------|:----:|------|
|
||||
| login | ✅ | 打开浏览器扫码登录 |
|
||||
| collect | ✅ | 采集小红书主页信息 |
|
||||
| status | ✅ | 检查登录状态 |
|
||||
| publish | 📋 | 计划中 (集成 social-auto-upload) |
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
mc xiaohongshu collect --account 手机号
|
||||
mc xiaohongshu login --account 手机号
|
||||
mc xiaohongshu status --account 手机号
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
"""小红书平台插件"""
|
||||
|
||||
from .plugin import XiaohongshuPlatform
|
||||
|
||||
|
||||
def register_platform():
|
||||
return "xiaohongshu", XiaohongshuPlatform()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""小红书插件实现
|
||||
|
||||
⚠️ DEPRECATED: 本插件层的功能已被 CommandBus + mc 引擎取代。
|
||||
所有执行操作请走:
|
||||
- 看板: POST /api/ops/run {type:'collect'|'nurture'|'login', accounts, params}
|
||||
- CLI: mc run / mc collect / mc smart-login
|
||||
platforms/ 层将在后续清理中移除。
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from platforms.base import BasePlatform
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent / "scripts"
|
||||
AGENT_LOCAL = Path.home() / "workbuddy-agent-os" / "agent-local"
|
||||
|
||||
|
||||
class XiaohongshuPlatform(BasePlatform):
|
||||
name = "xiaohongshu"
|
||||
display_name = "小红书"
|
||||
version = "1.0.0"
|
||||
description = "小红书账号管理: 登录/采集"
|
||||
|
||||
def _resolve_account(self, account_name: str) -> str:
|
||||
try:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if a.get("phone") == account_name or a["id"] == account_name:
|
||||
if a.get("platform") == "xiaohongshu":
|
||||
return a.get("identity_dir", a["id"]).replace("identities/", "")
|
||||
except: pass
|
||||
return account_name
|
||||
|
||||
def collect(self, account_name: str) -> dict:
|
||||
"""采集主页信息(建议通过 mc run --blueprints=xiaohongshu_read_profile 执行)"""
|
||||
# 旧路径:引用已归档的 collect_homepage_info.py(不存在)
|
||||
# 改为通过 CommandBus 分发 mc run 采集蓝图
|
||||
try:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR.parent.parent / "10_dashboard"))
|
||||
from services.command_bus import CommandBus
|
||||
result = CommandBus.dispatch("collect", [account_name], {"rounds": 1})
|
||||
status = "ok" if result.get("status") in ("accepted", "completed") else "error"
|
||||
return {"status": status, "message": f"已通过 CommandBus 分发: {result.get('status', '?')}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"CommandBus 分发失败: {e}"}
|
||||
|
||||
def login(self, account_name: str, headless: bool = False) -> dict:
|
||||
login_script = SCRIPTS_DIR / "login_identity.py"
|
||||
identity = self._resolve_account(account_name)
|
||||
cmd = [sys.executable, str(login_script), identity, "--platform", "xiaohongshu"]
|
||||
try:
|
||||
subprocess.Popen(cmd, cwd=str(SCRIPTS_DIR))
|
||||
return {"status": "ok", "message": f"浏览器已打开, 请扫码登录 {account_name}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def status(self, account_name: str) -> dict:
|
||||
try:
|
||||
from matrix_mgmt import MatrixManager
|
||||
mgr = MatrixManager()
|
||||
for a in mgr.list_accounts():
|
||||
if (a.get("phone") == account_name or a["id"] == account_name) and a.get("platform") == "xiaohongshu":
|
||||
cookies_path = AGENT_LOCAL / "identities" / (a.get("identity_dir", a["id"]).replace("identities/", "")) / "user_data" / "cookies.sqlite"
|
||||
if cookies_path.exists():
|
||||
return {"status": "logged_in", "account": a["id"]}
|
||||
return {"status": "no_cookie", "account": a["id"]}
|
||||
return {"status": "not_found"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
def publish(self, account_name: str, file_path: str = "", title: str = "", desc: str = "") -> dict:
|
||||
"""发布笔记到小红书"""
|
||||
identity = self._resolve_account(account_name)
|
||||
publish_script = SCRIPTS_DIR / "publish_video.py"
|
||||
if not Path(file_path).exists():
|
||||
return {"status": "error", "message": f"文件不存在: {file_path}"}
|
||||
cmd = [sys.executable, str(publish_script), "xiaohongshu",
|
||||
"--account", identity, "--file", str(Path(file_path).resolve())]
|
||||
if title:
|
||||
cmd += ["--title", title]
|
||||
if desc:
|
||||
cmd += ["--desc", desc]
|
||||
try:
|
||||
p = subprocess.run(cmd, cwd=str(SCRIPTS_DIR), capture_output=True, text=True, timeout=300)
|
||||
return {"status": "ok" if p.returncode == 0 else "error", "output": p.stdout[-500:]}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"status": "error", "message": "发布超时 (300s)"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
Xiaohongshu = XiaohongshuPlatform
|
||||
@@ -0,0 +1,9 @@
|
||||
# 矩阵养号系统 Python 依赖
|
||||
# 安装: pip install -r requirements.txt
|
||||
|
||||
# 浏览器自动化
|
||||
patchright>=1.40.0 # Playwright 反检测 fork(CDP + 指纹注入)
|
||||
camoufox>=0.4.0 # Firefox 内核浏览器(Camoufox)
|
||||
|
||||
# 配置解析
|
||||
PyYAML>=6.0
|
||||
@@ -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,23 @@
|
||||
"""
|
||||
AgentOS — 联邦智能体统一命令入口
|
||||
|
||||
命名空间:
|
||||
agentos matrix 社交矩阵(抖音/小红书运营)
|
||||
agentos ave 视频工厂(视频制作与编辑)
|
||||
agentos crawl 内容采集(互联网内容抓取)
|
||||
agentos fleet 联邦管理(多机协同)
|
||||
agentos serve 服务管理(MCP/Dashboard/调度)
|
||||
|
||||
用法:
|
||||
agentos matrix run --accounts A,B --blueprints X,Y
|
||||
agentos fleet sync
|
||||
agentos serve mcp
|
||||
agentos --help
|
||||
|
||||
设计原则:
|
||||
- 每个领域是一个插件,放在 plugins/ 目录下
|
||||
- 插件自动发现,加领域 = 加文件
|
||||
- 所有操作可远程执行 (--on 指定目标机器)
|
||||
- 所有操作支持 --json 输出
|
||||
"""
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""agentos — python -m agentos 入口"""
|
||||
from agentos.cli import main
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user