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

移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,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
}
]
}
@@ -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
}
]
}
@@ -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}
]
}
@@ -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}}
+31
View File
@@ -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