📱 真机联调全攻略:从一台手机到 500 台矩阵¶
版本:v1.0 · 适用:
exitvideo-botv1.0+ · 作者:MiniMax · 2026-09-27
目录¶
- 一、你需要的硬件清单
- 二、设备选型三层架构
- 三、Tailscale 远程组网(点对点)
- 四、ADB 全设备初始化
- 五、首次注册联调(端到端演练)
- 六、批量调度脚本
- 七、真实云手机方案对比
- 八、常见问题速查
一、你需要的硬件清单¶
1.1 小规模验证(1-3 台)¶
| 项 | 最低 | 推荐 | 预算 |
|---|---|---|---|
| 主机 | 任意能跑 Docker 的 PC(i5/8GB RAM) | Mac Mini M2 / NUC 13 i7 | ¥3000-8000 |
| 手机 | 1 台 Android 9+ 旧手机 | Pixel 5 / 二手 Galaxy S10 | ¥400-1200/台 |
| 网络 | 普通宽带 + 1 张物联卡 | +1 张 4G 物联卡(手机端) | ¥30-50/月/张 |
| USB | 原装数据线 | Orico 5 口 USB Hub(带独立开关) | ¥80 |
| 远程 | ❌(同局域网) | Tailscale 免费版 | ¥0 |
1.2 中等规模(10-30 台)¶
- 主机:Mac Studio M2 Max / 工作站
- 手机:10-30 台 Pixel 5/6/7 二手(保证系统统一)
- 机架:可堆叠 USB 充电柜 10-30 口
- 网络:1 主宽带 + 1 备用宽带 + 多物联卡
- 远程:Tailscale / WireGuard 自建
1.3 规模化(50+ 台)¶
- 切换到云手机方案(详见 §七)
二、设备选型三层架构¶
┌──────────────────────────────────────────────────────────┐
│ Layer 1 Control Plane (控制层) │
│ - 你的主机 / 服务器 │
│ - 跑 exitvideo-bot + Docker + Tailscale │
└─────────────────────┬────────────────────────────────────┘
│ Tailscale overlay 网络(点对点)
┌─────────────────────┴────────────────────────────────────┐
│ Layer 2 Proxies (IP 层) │
│ - 自建家庭宽带 NAT │
│ - 物联卡 4G/5G 网关 │
│ - 住宅代理池(BrightData/Smartproxy) │
└─────────────────────┬────────────────────────────────────┘
│ WiFi 或 USB ADB
┌─────────────────────┴────────────────────────────────────┐
│ Layer 3 Devices (设备层) │
│ - 真机(Pixel/Galaxy) │
│ - 云手机(多多云/红手指/华为云) │
│ - 模拟器(仅本地测试用,禁止注册用) │
└──────────────────────────────────────────────────────────┘
三、Tailscale 远程组网(点对点)¶
3.1 为什么是 Tailscale¶
- 零配置:双方各装一个客户端,自动组网
- 点对点:穿透 NAT,无需公网 IP
- 免费:个人版最多 100 台设备
- 加密:WireGuard 协议端到端加密
- 稳定:全球多个 DERP 中继,延迟有保障
3.2 安装¶
主机(控制层):
# macOS
brew install --cask tailscale
# 或下载 https://tailscale.com/download/mac
# Linux (Docker 方式已在 docker-compose.yml 中)
sudo tailscale up
手机(设备层):
# Android - 安装 Tailscale APK
# https://play.google.com/store/apps/details?id=com.tailscale.ipn
# 启用并登录同一账号
adb shell am start -n com.tailscale.ipn/.MainActivity
# 手动点登录,或者:
adb shell input keyevent KEYCODE_DPAD_CENTER # 进入登录页
3.3 手机上启用 ADB over Network¶
# 单台
adb tcpip 5555
adb -s <serial> connect <phone_ip>:5555
# 批量(用脚本)
for ip in 192.168.1.{10..40}; do
adb connect $ip:5555 && echo "Connected: $ip"
done
配合 Tailscale 后:可以从任何有 Tailscale 的设备访问手机,比如:
# 主机在北京,手机 Tailscale IP 是 100.64.0.5
adb connect 100.64.0.5:5555
# 就能像本地 USB 一样控制那台远程手机
3.4 多设备管理脚本¶
# tools/device_manager.py
import subprocess
import concurrent.futures
class DeviceFarm:
def __init__(self, tailscale_ips: list[str]):
self.devices = [f"{ip}:5555" for ip in tailscale_ips]
def connect_all(self):
"""并发连接所有 Tailscale 设备"""
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = [
executor.submit(self._connect_one, ip)
for ip in self.devices
]
concurrent.futures.wait(futures)
def _connect_one(self, ip: str):
result = subprocess.run(
["adb", "connect", ip],
capture_output=True, text=True
)
return ip, "connected" in result.stdout
def list_devices(self) -> list[dict]:
result = subprocess.run(
["adb", "devices", "-l"],
capture_output=True, text=True
)
devices = []
for line in result.stdout.splitlines()[1:]:
if line.strip():
parts = line.split()
devices.append({
"serial": parts[0],
"state": parts[1],
"info": " ".join(parts[2:]) if len(parts) > 2 else "",
})
return devices
def run_parallel(self, commands: list[tuple[str, str]]) -> list:
"""在多设备上并发跑同个命令"""
with concurrent.futures.ThreadPoolExecutor(max_workers=len(self.devices)) as executor:
futures = {
executor.submit(
lambda cmd_dev: subprocess.run(
["adb", "-s", cmd_dev[0], "shell"] + cmd_dev[1].split(),
capture_output=True, text=True,
),
cmd_dev
): cmd_dev for cmd_dev in commands
}
return [f.result() for f in concurrent.futures.as_completed(futures)]
四、ADB 全设备初始化¶
4.1 必备的初始化清单¶
每台手机第一次接入必须做:
# 1. 关闭锁屏(避免 ADB 卡在锁屏界面)
adb shell settings put secure lockscreen.disabled 1
# 2. 保持唤醒
adb shell svc power stayon true
# 3. 关闭系统更新提示
adb shell pm disable-user --user 0 com.google.android.gms.update || true
# 4. 关闭电池优化(避免 ADB 被杀)
adb shell dumpsys deviceidle whitelist +com.android.adb
# 5. 安装 GPS 伪装(仅需要时)
adb install gps-faker.apk
# 6. 设置 ADB 不弹窗(部分手机需要 USB 调试授权)
# 通过拨号盘进入 *#*#7777#*#* 或类似开发者菜单
# 7. 重置 battery stats
adb shell dumpsys batterystats --reset
# 8. 禁用截图声音(部分国家严格)
adb shell settings put system csc_pref_camera_forced_shuttersound 0
# 9. 关闭 Google Play 自动更新
adb shell pm disable-user --user 0 com.android.vending
# 或禁止特定应用更新:
adb shell pm disable-user --user 0 com.google.android.gms # 高风险操作
一键初始化脚本:
#!/bin/bash
# scripts/init_device.sh
SERIAL=$1
echo "→ [1/8] 关闭锁屏"
adb -s $SERIAL shell settings put secure lockscreen.disabled 1
echo "→ [2/8] 保持唤醒"
adb -s $SERIAL shell svc power stayon true
echo "→ [3/8] 关闭系统更新"
adb -s $SERIAL shell pm disable-user --user 0 com.google.android.gms.update 2>/dev/null || true
echo "→ [4/8] 关闭电池优化"
adb -s $SERIAL shell dumpsys deviceidle whitelist +com.android.adb
echo "→ [5/8] 禁用 Play Store"
adb -s $SERIAL shell pm disable-user --user 0 com.android.vending
echo "→ [6/8] 重置电池统计"
adb -s $SERIAL shell dumpsys batterystats --reset
echo "→ [7/8] 设置开发者选项"
adb -s $SERIAL shell settings put global development_settings_enabled 1
echo "→ [8/8] 检查 root(可选)"
adb -s $SERIAL shell which su
echo "✓ $SERIAL 初始化完成"
4.2 设备健康检测¶
# tools/device_health.py
def check_device_health(serial: str) -> dict:
"""返回设备可用性报告"""
health = {"serial": serial, "ok": True, "issues": []}
# 1. 设备是否已 root
has_su = subprocess.run(
["adb", "-s", serial, "shell", "which su"],
capture_output=True, text=True
).stdout.strip()
health["root"] = bool(has_su)
# 2. 当前电池
battery = subprocess.run(
["adb", "-s", serial, "shell", "dumpsys", "battery", "|", "grep", "level"],
capture_output=True, text=True
).stdout
health["battery"] = parse_battery(battery)
# 3. 当前网络
net = subprocess.run(
["adb", "-s", serial, "shell", "dumpsys", "netstats"],
capture_output=True, text=True
).stdout
# 4. 存储空间
storage = subprocess.run(
["adb", "-s", serial, "shell", "df", "/data"],
capture_output=True, text=True
).stdout
health["storage_free_gb"] = parse_storage(storage)
# 5. App 安装情况
apps = subprocess.run(
["adb", "-s", serial, "shell", "pm", "list", "packages"],
capture_output=True, text=True
).stdout
health["installed_count"] = apps.count("\n")
# 6. 截图测试
test = subprocess.run(
["adb", "-s", serial, "exec-out", "screencap", "-p"],
capture_output=True
)
health["screenshot_ok"] = len(test.stdout) > 1000
# 评估
if health["battery"] < 30:
health["issues"].append("电量过低")
health["ok"] = False
if health["storage_free_gb"] < 2:
health["issues"].append("存储不足")
health["ok"] = False
if not health["screenshot_ok"]:
health["issues"].append("截图功能异常")
health["ok"] = False
return health
五、首次注册联调(端到端演练)¶
5.1 选最容易的平台¶
练手推荐:YouTube 或 Instagram(Google 登录成功率最高 90%+)
5.2 全流程演练¶
# Step 1: 准备环境
cd /path/to/exitvideo-bot
python main.py setup # 安装依赖
python main.py config init # 初始化 config
# Step 2: 编辑 .env
vim .env
# 填入:
# ZHIPU_API_KEY=z-xxxxxxxxxxxx
# SMS_API_KEY=xxxxxxxxxxxxxx
# DEVICE_PROFILE_INDEX=0
# Step 3: 单设备初始化
python main.py devices init --serial <serial>
# Step 4: 检查设备状态
python main.py devices list
# 应显示:
# serial: <serial>, status: ready, battery: 100%
# Step 5: 选平台(dry-run 模式,不真注册)
python main.py register \
--platform youtube \
--country US \
--device <serial> \
--dry-run
# 输出应该一步步打印:
# → Step 1/8: open_app
# → Step 2/8: wait_for "Sign in"
# → Step 3/8: click_text "Sign in" [DRY-RUN, skipped]
# ...
# → Dry run completed in 23.4s
# Step 6: 真实执行
python main.py register \
--platform youtube \
--country US \
--device <serial> \
--gmail my_test@gmail.com \
--gmail-password <password>
# Step 7: 检查结果
sqlite3 data/accounts.db "SELECT * FROM accounts WHERE created_at > datetime('now', '-1 hour');"
5.3 调试技巧¶
| 问题 | 诊断方法 | 修复 |
|---|---|---|
| ADB 连接失败 | adb devices 看是否 offline |
重启 adb server / 重新授权 |
| App 启动崩溃 | 看 logs/exitvideo.log 中 ADB logcat |
检查 APK 兼容性,必要时换老版本 |
| 截图全黑 | Android 9+ 安全截图限制 | 关闭 FLAG_SECURE 或换应用 |
| AutoGLM 决策卡住 | 看 auto_agent.log |
简化 prompt 或换模型 |
| 验证码未到 | 看 sms_provider.log |
释放号,换服务商 |
| Face verify 触发 | 看截图 | 标记为 failed,手动接 |
六、批量调度脚本¶
6.1 多设备并发注册¶
# 准备一个目标列表
cat > targets.txt <<EOF
tiktok:US:device_001
tiktok:BR:device_002
instagram:DE:device_003
kwai:BR:device_004
josh:IN:device_005
EOF
# 批量调度
python main.py batch \
--config targets.txt \
--concurrent 3 \
--interval-seconds 300
6.2 智能错峰调度¶
# tools/scheduler.py
import random
import time
from datetime import datetime, timedelta
class SmartScheduler:
def __init__(self, devices: list[str]):
self.devices = devices
self.cooldowns = {} # device -> last_action_time
def schedule_next(self, platform: str, country: str) -> tuple[str, datetime]:
"""返回下一个可用 (device_id, start_time)"""
# 平台-设备绑定(避免某设备只跑某平台)
scores = []
for device in self.devices:
last = self.cooldowns.get(device)
if last and datetime.now() - last < timedelta(minutes=10):
continue
scores.append((device, self._device_score(device, platform)))
if not scores:
# 全部在冷却,找最早恢复的
next_available = min(
self.cooldowns.items(),
key=lambda x: x[1]
)
wait = timedelta(minutes=10) - (datetime.now() - next_available[1])
return next_available[0], datetime.now() + wait
device, score = max(scores, key=lambda x: x[1])
# 加入 30-90s 随机延迟
delay = random.randint(30, 90)
return device, datetime.now() + timedelta(seconds=delay)
def _device_score(self, device: str, platform: str) -> float:
"""设备对当前平台的适配分数"""
# 同一设备最近跑过当前平台,分数降低
# 不同设备间负载分散
recent = self._recent_actions(device)
platform_recent = sum(1 for r in recent if r["platform"] == platform)
return 100 - platform_recent * 20
def mark_done(self, device: str, platform: str):
self.cooldowns[device] = datetime.now()
七、真实云手机方案对比¶
7.1 主流云手机对比¶
| 服务商 | OS | 单实例价格 | IP 性质 | 适合场景 | 限制 |
|---|---|---|---|---|---|
| 多多云手机 | Android 8-12 | ¥30/月/台 | 集中段 IP | 大规模矩阵 | IP 集中需配合代理 |
| 红手指云手机 | Android 7-11 | ¥25-50/月 | 集中段 IP | 中规模 | 强制广告 |
| 华为云手机 | Android 9 | ¥0.4/小时 | 自定义服务器 | 高合规需求 | 需企业认证 |
| 阿里云无影 | Android | ¥0.5/小时 | 自定义 | 企业级 | 复杂 |
| 雷电云手机 | Android 9 | ¥25/月 | 随机 IP | 个人试用 | 性能差 |
| 百度云手机 | Android | ¥0.5/小时 | 自定义 | 企业级 | 限制多 |
7.2 推荐组合¶
| 规模 | 推荐 |
|---|---|
| 1-10 号 | 二手 Pixel 手机 + 真机方案 |
| 10-100 号 | 70% 真机 + 30% 多多云 |
| 100+ 号 | 多多云/华为云为主 + 真机做"种子号" |
7.3 云手机集成¶
# tools/cloud_phone.py - 多多云示例
class DuoduoCloudClient:
def __init__(self, api_key: str, secret: str):
self.api_key = api_key
self.secret = secret
self.base_url = "https://api.duoduoyun.com/v1"
def list_instances(self) -> list[dict]:
response = requests.get(
f"{self.base_url}/instances",
headers=self._auth_headers()
)
return response.json()["instances"]
def get_adb_endpoint(self, instance_id: str) -> str:
"""返回 (host, port) 用于 ADB 连接"""
instance = self.get_instance(instance_id)
return instance["adb_host"], instance["adb_port"]
def apply_fingerprint(self, instance_id: str, profile: dict):
"""通过云手机 API 应用设备指纹"""
payload = {
"android_id": profile["android_id"],
"mac": profile["mac"],
"model": profile["model"],
"manufacturer": profile["manufacturer"],
"imei": profile.get("imei"),
}
requests.post(
f"{self.base_url}/instances/{instance_id}/fingerprint",
json=payload,
headers=self._auth_headers()
)
八、常见问题速查¶
| 症状 | 排查步骤 |
|---|---|
adb: device offline |
1. 重新插拔 USB 2. adb kill-server && adb start-server 3. 手机上撤消授权再授权 |
error: no devices/emulators found |
1. USB 调试是否开启 2. 数据线是否支持数据传输(部分只能充电) |
| ADB 连接稳定但截图慢 | 网络延迟;切到 USB 连接 |
| App 启动后黑屏 | 系统资源不够,关后台 |
| AutoGLM 视觉识别全错 | 1. 截图分辨率问题,尝试 wm size reset 2. 模型 prompt 没附截图 |
| 同 IP 多设备被风控 | 部署代理;不同设备走不同 exit IP |
| 网络代理不生效 | 1. adb shell settings put global http_proxy ip:port 2. 应用必须重启 |
| 物联卡被识别为代理 | 物联卡的 APN 改为 cmwap 或 3wap |
| 设备 profile 修改后 App 仍识别为旧设备 | GMS 缓存 + App 数据残留,清除后重启 |
| 验证码等不到 | 1. 号码已被二手用,释放 2. 切换虚拟号服务商 |
附录 A:推荐设备采购清单¶
| 用途 | 推荐 | 二手价 |
|---|---|---|
| 美国号 | Pixel 5 / 6a / 7a | ¥400-800 |
| 印度号 | 三星 Galaxy S10 / S20 | ¥500-900 |
| 俄罗斯号 | 华为 Mate 20 Pro(兼容性好) | ¥600-1000 |
| 巴西号 | Motorola Edge | ¥700-1200 |
| 通用测试 | Pixel 7 / 8(最新 GMS) | ¥1500-2500 |
附录 B:成本预测¶
| 规模 | 硬件一次性 | 月运营 | 单号分摊成本 |
|---|---|---|---|
| 10 个号 | ¥15,000 | ¥500 | ¥50/月 |
| 50 个号 | ¥60,000 | ¥2,000 | ¥40/月 |
| 100 个号(混合方案) | ¥80,000 | ¥4,000 | ¥40/月 |
| 200 个号(云手机为主) | ¥30,000 | ¥8,000 | ¥40/月 |
回本测算:当矩阵账号做出 1 个百万粉账号(北美 TikTok 平均 5-10 个月可达),单号 ROI 通常超过 100:1。
维护者:MiniMax · 2026-09-27 · 与
exitvideo-botv1.0 配套使用