运行多部手机

给手机命名,用 Python、Node.js 或 shell 并行运行它们,更换移动 IP 地址,并让一整架手机连续多天保持连接。

本页为译文。如与英文版不一致,以英文版为准。 English

一个 droidline serve 能驱动的手机数量,只受你的电脑和网络承载能力的限制。每部手机都有自己的命令队列:发给同一部手机的命令按顺序执行,不同手机之间同时执行。

给每部手机起名字

每部手机都有一个随机 ID,例如 k7d2q9xa。在配对时给它起一个好记的名字,或者之后再重命名:

droidline pair 482913 --name shelf-01
droidline rename k7d2q9xa shelf-02
droidline devices

名称可以使用字母、数字、点、连字符和下划线。在手机上贴一张写着同样名字的标签,能省去很多猜测。

并行运行

向服务器询问哪些手机在线,然后为每部手机分配各自的线程、任务或后台作业:

from concurrent.futures import ThreadPoolExecutor
from droidline import Droidline

dl = Droidline()

def morning(name: str) -> str:
    d = dl.device(name)
    d.wake()
    d.launch("com.example.app")
    d.touch("text", "Check in", timeout=20)
    return d.get_text("id", "status")

names = [p["name"] for p in dl.devices() if p["online"]]
with ThreadPoolExecutor(len(names) or 1) as pool:
    for name, status in zip(names, pool.map(morning, names)):
        print(name, status)
import { Droidline } from "droidline";

const dl = new Droidline();
const phones = (await dl.devices()).filter((p) => p.online);

const results = await Promise.allSettled(phones.map(async (p) => {
  const d = dl.device(p.name);
  await d.wake();
  await d.launch("com.example.app");
  await d.touch("text", "Check in", { timeout: 20 });
  return d.getText("id", "status");
}));

results.forEach((r, i) => console.log(phones[i].name, r.status === "fulfilled" ? r.value : r.reason.message));
for name in $(droidline --json devices | jq -r '.value[] | select(.online) | .name'); do
  (
    droidline -d "$name" wake
    droidline -d "$name" launch com.example.app
    droidline -d "$name" touch text "Check in" --timeout 20
    echo "$name: $(droidline -d "$name" get_text id status)"
  ) &
done
wait

一部手机失败不会让其他手机停下:在 Python 中,异常会在你取到那部手机的结果时从 pool.map 中抛出;在 Node.js 中,allSettled 会记录它;在 shell 中,每部手机都在各自的子 shell 中运行。

让每部手机使用各自的网络

借助按应用代理,每部手机可以让某个应用通过不同的上游联网。先保存一次上游,然后按名称分配:

droidline proxy add kr1 socks5://user:pass@203.0.113.10:1080
droidline proxy add kr2 socks5://user:pass@203.0.113.11:1080
plan = {"shelf-01": "@kr1", "shelf-02": "@kr2"}
for name, upstream in plan.items():
    dl.device(name).proxy(upstream, app="com.android.chrome")
const plan = { "shelf-01": "@kr1", "shelf-02": "@kr2" };
for (const [name, upstream] of Object.entries(plan)) {
  await dl.device(name).proxy(upstream, { app: "com.android.chrome" });
}
droidline -d shelf-01 proxy @kr1 --app com.android.chrome
droidline -d shelf-02 proxy @kr2 --app com.android.chrome

新的移动 IP 地址

使用移动数据时,开关一次飞行模式通常能从运营商处获得新地址。手机离线期间无法接收命令,所以要把开和关两个操作放进同一个 batch 发送,由手机自行完成:

d.batch([("airplane", True), ("sleep", 3000), ("airplane", False)], wait=True)
await d.batch([["airplane", true], ["sleep", 3000], ["airplane", false]], { wait: true });
droidline -d shelf-01 batch '[["airplane",true],["sleep",3000],["airplane",false]]' --wait

不传 wait 时,调用会立即返回 {"accepted": true},结果稍后作为 result 事件到达。

屏幕熄灭时

手机屏幕熄灭时,点击和滑动会失败。每项任务开始前调用 wake();它会点亮屏幕,并解除没有 PIN 的锁屏。对于无人值守的手机,请在手机设置中移除 PIN、图案或密码锁,因为 Droidline 无法解锁它们。

连续多天保持连接

  • 把 Droidline 的电池优化设为不受限制,并在手机品牌额外添加的后台限制中允许它(故障排查列出了常见的位置)。
  • 配对之后,手机开机时应用会自动启动。
  • 重新连接后,手机会重发电脑漏收的回复,重复的命令不会执行两次。参见概念。
  • droidline devices 显示每部手机的线路和缺少的权限。devices() 中的 last_seen 告诉你离线手机最后一次连接的时间。
  • 让手机一直充电。USB 充电器就够了,不需要数据连接。

监控整架手机

订阅 device 事件,就能在手机掉线或重新上线的那一刻得知:

from droidline import Droidline

dl = Droidline()
dl.subscribe(["device"])
dl.on("device", lambda e: print(e["name"], e["state"], e.get("route")))
input("Watching. Press Enter to stop.\n")
droidline --json devices | jq -r '.value[] | "\(.name)\t\(if .online then "online" else "offline" end)\t\(.route)"'

更新

应用每天检查一次 GitHub Releases,有新版本时显示一条通知。把更新直接安装在旧版应用上,配对会保留。