- Регистрация
- 24.12.2024
- Сообщения
- 72
- Реакции
- 170
- Баллы
- 33
Мобильная экосистема:
связка ZennoPoster / ZennoDroid с авторским Android-приложением
На примере эмуляторов MEmu и физических устройств
Гибридная автоматизация: когда браузерный скрипт и телефон в руке работают как одна система
связка ZennoPoster / ZennoDroid с авторским Android-приложением
На примере эмуляторов MEmu и физических устройств
Гибридная автоматизация: когда браузерный скрипт и телефон в руке работают как одна система
1. Реалии мобильной автоматизации и гибридный стек
Честный тезис, с которого стоит начать: чистая браузерная автоматизация, которая отлично работает на десктопе, на мобильных платформах упирается в стену системных ограничений уже на втором шаге. Android и Google Play спроектированы так, чтобы автоматизация была *трудной* — не потому что кто-то намеренно вредит автоматизаторам, а потому что те же самые барьеры защищают обычных пользователей от вредоносных приложений. Разберём это честно, без иллюзий «просто накатим и заработает».
1.1. Почему системный прокси нельзя просто «накатить» через GUI на все приложения
На десктопе один системный прокси в настройках Windows разом меняет маршрут трафика для браузера, всех фоновых служб и большинства приложений. На Android всё устроено иначе:- Приложения могут игнорировать системный прокси. Начиная с Android 6+, системная настройка http_proxy гарантированно применяется к трафику через HttpURLConnection/WebView, но многие приложения (в первую очередь нативные Chromium-движки, включая сам Chrome в отдельных сценариях, и приложения с собственным сетевым стеком на OkHttp/Cronet) держат собственные сетевые настройки и системный прокси попросту не учитывают.
- Глобальный http_proxy не поддерживает авторизацию. Поле принимает только ip:port — ни на одной версии Android нет штатного способа передать туда логин и пароль. Это системное ограничение, а не недоработка какого-то конкретного приложения.
- VPN-профиль — не то же самое, что HTTP-прокси. Android действительно поддерживает авторизованные прокси через механизм VpnService, но это требует установки полноценного VPN-приложения на устройство и настройки его как системного VPN — тяжеловесное решение для фермы из десятков эмуляторов.
- У каждого эмулятора и физического устройства — своя изоляция. Прокси, назначенный одному инстансу MEmu, никак не влияет на соседние — что хорошо для параллельной работы, но требует адресного управления каждым устройством по отдельности, а не одной галочкой «включить прокси для всех».
Вывод простой и практичный: единственный способ, который стабильно работает на всех версиях Android без root и без установки дополнительного VPN-клиента — это adb shell settings put global http_proxy ip:port, применяемый индивидуально к каждому устройству через ADB. Для авторизованных прокси эту команду приходится дополнять локальным авторизующим релеем — рабочее решение для этого приведено в разделе 1.4 полностью, без сокращений.
1.2. Почему мобильные капчи требуют гибридного подхода
На десктопе связка «браузер + расширение для решения капч + API-ключ CapMonster» решается одним расширением. У мобильного приложения (или мобильной версии сайта в браузере эмулятора) такой возможности просто нет — установить браузерное расширение в Chrome для Android нельзя, а нативное приложение соцсети тем более не даёт встроить сторонний JS-код в свой рендер капчи.
Из этого следует архитектурное решение, а не временный костыль: распознавание капчи физически не может происходить *на* мобильном устройстве — туда её только показывают. Обработка обязана происходить *снаружи*, там, где есть полноценный доступ к HTTP API сервиса распознавания — то есть на стороне ZennoPoster.
Из этого следует архитектурное решение, а не временный костыль: распознавание капчи физически не может происходить *на* мобильном устройстве — туда её только показывают. Обработка обязана происходить *снаружи*, там, где есть полноценный доступ к HTTP API сервиса распознавания — то есть на стороне ZennoPoster.
1.3. Скрытая деталь, которая на практике решает половину проблем: «версия для ПК»
При разборе реального рабочего макроса для Facebook в связке обнаруживается приём, который стоит вынести отдельным пунктом, потому что он экономит часы отладки: мобильная версия сайта в браузере эмулятора зачастую физически не показывает полноценный интерфейс загрузки контента — форма загрузки видео на m.facebook.com заметно урезана по сравнению с десктопной. Рабочий макрос явно фиксирует это как обязательный шаг:Заметка из реального макроса (Facebook, шаг «ОБЯЗАТЕЛЬНО»):
«ОБЯЗАТЕЛЬНО ВЫБИРАЕМ В БРАУЗЕРЕ ВЕРСИЮ ДЛЯ ПК — она дефолтно будет на том сайте, на котором выбрали»
То есть первый практический шаг сценария — не заход на мобильную версию сайта, а принудительное переключение Chrome в режим «Версия для ПК» (Site settings -> Desktop site), после чего сайт отдаёт полноценную десктопную разметку с рабочей формой загрузки — и уже по ней можно строить надёжные клики. Это ровно тот тип «непубличного» инженерного знания, который отличает рабочий продакшен-скрипт от красивой теоретической схемы: без этого шага загрузка на Facebook с мобильного браузера просто не находит нужные элементы интерфейса.
1.4. Универсальное проксирование через Python + ADB — полный рабочий скрипт
Ниже — законченный, рабочий скрипт без плейсхолдеров и демо-фрагментов. Он читает proxies.txt из своей папки, сканирует все подключённые ADB-устройства (эмуляторы MEmu/LDPlayer и реальные телефоны видны командой одинаково), поддерживает глобальный режим (назначение с ротацией/сдвигом на все устройства сразу) и индивидуальный режим (конкретный прокси из файла — на конкретный Serial ID), а также решает системное ограничение из пункта 1.1 — отсутствие авторизации в global http_proxy — через локальный релей с подстановкой Proxy-Authorization.Формат proxies.txt (одна строка — один прокси, пустые строки и строки с # игнорируются):
185.22.14.10:8000
185.22.14.11:8000:user01:_Pass123
45.155.68.4:3128:user02:_Pass456
Полный код `adb_proxy_manager.py`:
Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
adb_proxy_manager.py
=====================================================================
Универсальное проксирование Android-устройств (эмуляторы MEmu, LDPlayer
и др., а также реальные физические смартфоны) через ADB.
Требования:
pip install --upgrade pip # requests не нужен, используется только stdlib
Формат файла proxies.txt (рядом со скриптом), одна строка — один прокси:
ip:port
ip:port:login:password
Пустые строки и строки, начинающиеся с '#', игнорируются.
РЕЖИМЫ РАБОТЫ
---------------------------------------------------------------------
1) Глобальный (со сдвигом/ротацией на ВСЕ устройства):
python adb_proxy_manager.py --mode global
python adb_proxy_manager.py --mode global --shift 3
2) Индивидуальный (конкретный прокси -> конкретный Serial ID):
python adb_proxy_manager.py --mode individual --serial emulator-5554 --line 2
3) Сброс прокси:
python adb_proxy_manager.py --mode reset --all
python adb_proxy_manager.py --mode reset --serial emulator-5554
4) Просто посмотреть список подключенных устройств и текущий прокси на них:
python adb_proxy_manager.py --mode list
ВАЖНО ПРО АВТОРИЗОВАННЫЕ ПРОКСИ (login:password)
---------------------------------------------------------------------
Стандартная настройка Android `global http_proxy` НЕ умеет передавать
Proxy-Authorization — это ограничение самой ОС, а не этого скрипта: поле
принимает только "ip:port" и не поддерживает логин/пароль ни на одной
версии Android "из коробки". Обходной путь, который использует этот
скрипт: для каждого авторизованного прокси на ПК поднимается локальный
"прозрачный" релей (класс LocalAuthRelay) без авторизации, который сам
подставляет заголовок Proxy-Authorization при обращении к реальному
upstream-прокси. Устройство получает через adb reverse доступ к этому
релею на 127.0.0.1:<порт> и прописывается на НЕГО как на обычный прокси
без пароля — с точки зрения Android это обычный анонимный прокси.
=====================================================================
"""
import argparse
import base64
import select
import socket
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
SCRIPT_DIR = Path(__file__).resolve().parent
PROXIES_FILE = SCRIPT_DIR / "proxies.txt"
LOCAL_RELAY_HOST = "127.0.0.1"
LOCAL_RELAY_PORT_START = 33000 # с этого порта нумеруются локальные релеи
# ======================================================================
# 1) МОДЕЛЬ ПРОКСИ И ЧТЕНИЕ proxies.txt
# ======================================================================
@dataclass
class ProxyEntry:
host: str
port: int
login: Optional[str] = None
password: Optional[str] = None
@property
def needs_auth(self) -> bool:
return bool(self.login and self.password)
def __str__(self) -> str:
auth = f"{self.login}:***@" if self.needs_auth else ""
return f"{auth}{self.host}:{self.port}"
def load_proxies(path: Path = PROXIES_FILE) -> List[ProxyEntry]:
if not path.exists():
raise SystemExit(
f"Файл со списком прокси не найден: {path}\n"
f"Создайте {path.name} рядом со скриптом, формат:\n"
f" ip:port\n ip:port:login:password"
)
proxies: List[ProxyEntry] = []
for lineno, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
line = raw_line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(":")
if len(parts) == 2:
host, port = parts
proxies.append(ProxyEntry(host=host, port=int(port)))
elif len(parts) == 4:
host, port, login, password = parts
proxies.append(ProxyEntry(host=host, port=int(port), login=login, password=password))
else:
print(f" [!] proxies.txt:{lineno}: не распознан формат строки '{raw_line}' — пропускаю.")
if not proxies:
raise SystemExit(f"{path.name} пуст либо не содержит ни одной валидной строки прокси.")
return proxies
# ======================================================================
# 2) РАБОТА С ADB: СПИСОК УСТРОЙСТВ, ЧТЕНИЕ/УСТАНОВКА/СБРОС ПРОКСИ
# ======================================================================
def run_adb(args: List[str], timeout: int = 15) -> subprocess.CompletedProcess:
"""Тонкая обёртка над subprocess.run для вызовов adb с единым таймаутом
и текстовым выводом — используется всеми функциями ниже."""
return subprocess.run(
["adb"] + args,
capture_output=True,
text=True,
timeout=timeout,
)
def list_adb_devices() -> List[str]:
"""Возвращает serial'ы ВСЕХ устройств в состоянии 'device' (т.е. реально
готовых к работе) — как эмуляторов (MEmu, LDPlayer и т.д., видны как
emulator-5554, 127.0.0.1:21503 и т.п.), так и физических телефонов,
подключённых по USB или по Wi-Fi (adb connect). Устройства в статусе
'unauthorized' (не подтверждён запрос отладки на самом телефоне) или
'offline' сознательно исключаются — трогать их прокси бессмысленно и
может привести к зависанию команды."""
result = run_adb(["devices"])
if result.returncode != 0:
raise SystemExit(f"'adb devices' завершился с ошибкой: {result.stderr.strip()}")
serials = []
for line in result.stdout.splitlines()[1:]: # первая строка — заголовок "List of devices attached"
line = line.strip()
if not line:
continue
parts = line.split()
if len(parts) >= 2 and parts[1] == "device":
serials.append(parts[0])
elif len(parts) >= 2 and parts[1] in ("unauthorized", "offline"):
print(f" [!] Устройство {parts[0]} в состоянии '{parts[1]}' — пропускаю.")
return serials
def adb_set_http_proxy(serial: str, ip: str, port: int) -> bool:
"""Устанавливает системный HTTP-прокси устройству через
'settings put global http_proxy ip:port' — работает на всех версиях
Android 5+ и не требует root (settings — обычная системная утилита)."""
value = f"{ip}:{port}"
result = run_adb(["-s", serial, "shell", "settings", "put", "global", "http_proxy", value])
if result.returncode != 0:
print(f" [✗] {serial}: не удалось установить прокси ({result.stderr.strip()})")
return False
print(f" [✓] {serial}: прокси установлен -> {value}")
return True
def adb_reset_http_proxy(serial: str) -> bool:
"""Сбрасывает системный прокси. 'settings delete' — основной способ,
актуальный на Android 9+. Для старых версий (где delete не всегда
подчищает значение) дополнительно подстраховываемся явной записью
':0' — специальное значение, которое сама система трактует как
'прокси отключён'."""
ok = True
result = run_adb(["-s", serial, "shell", "settings", "delete", "global", "http_proxy"])
if result.returncode != 0:
print(f" [!] {serial}: 'settings delete' вернул ошибку ({result.stderr.strip()}), пробую fallback...")
ok = False
fallback = run_adb(["-s", serial, "shell", "settings", "put", "global", "http_proxy", ":0"])
if fallback.returncode != 0:
print(f" [✗] {serial}: не удалось сбросить прокси ни одним из способов.")
return False
print(f" [✓] {serial}: прокси сброшен.")
return True or ok
def adb_get_http_proxy(serial: str) -> str:
result = run_adb(["-s", serial, "shell", "settings", "get", "global", "http_proxy"])
value = result.stdout.strip()
return value if value and value != "null" else "(не задан)"
def adb_reverse_port(serial: str, port: int) -> bool:
"""Пробрасывает TCP-порт устройства на тот же порт хоста: устройство,
обращаясь к 127.0.0.1:<port> у себя, на самом деле попадёт на
127.0.0.1:<port> ПК, где слушает LocalAuthRelay. Именно так устройство
получает доступ к авторизованному прокси, оставаясь настроенным на
'обычный, беспарольный' локальный адрес."""
result = run_adb(["-s", serial, "reverse", f"tcp:{port}", f"tcp:{port}"])
if result.returncode != 0:
print(f" [✗] {serial}: adb reverse tcp:{port} не удался ({result.stderr.strip()})")
return False
return True
# ======================================================================
# 3) ЛОКАЛЬНЫЙ АВТОРИЗУЮЩИЙ РЕЛЕЙ ДЛЯ ПРОКСИ С ЛОГИНОМ/ПАРОЛЕМ
# ======================================================================
class LocalAuthRelay(threading.Thread):
"""Простой потоковый TCP-релей: слушает локальный порт БЕЗ авторизации,
а любое входящее HTTP/HTTPS(CONNECT)-соединение прозрачно перенаправляет
на реальный upstream-прокси, подставляя Proxy-Authorization: Basic ...
Поддерживает как обычные HTTP-запросы, так и CONNECT-туннели (HTTPS)."""
def __init__(self, listen_port: int, upstream: ProxyEntry):
super().__init__(daemon=True)
self.listen_port = listen_port
self.upstream = upstream
self._server_sock: Optional[socket.socket] = None
self._stop_flag = threading.Event()
def _auth_header(self) -> bytes:
token = base64.b64encode(f"{self.upstream.login}:{self.upstream.password}".encode()).decode()
return f"Proxy-Authorization: Basic {token}\r\n".encode()
def run(self) -> None:
self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._server_sock.bind((LOCAL_RELAY_HOST, self.listen_port))
self._server_sock.listen(64)
self._server_sock.settimeout(1.0)
print(f" [релей] слушаю 127.0.0.1:{self.listen_port} -> {self.upstream}")
while not self._stop_flag.is_set():
try:
client_sock, _ = self._server_sock.accept()
except socket.timeout:
continue
except OSError:
break
threading.Thread(target=self._handle_client, args=(client_sock,), daemon=True).start()
def stop(self) -> None:
self._stop_flag.set()
if self._server_sock:
self._server_sock.close()
def _handle_client(self, client_sock: socket.socket) -> None:
try:
head = client_sock.recv(65536)
if not head:
client_sock.close()
return
upstream_sock = socket.create_connection((self.upstream.host, self.upstream.port), timeout=10)
if head.startswith(b"CONNECT "):
# HTTPS-туннель: дописываем заголовок авторизации перед
# пустой строкой, разделяющей заголовки и тело.
header_block, _, rest = head.partition(b"\r\n\r\n")
patched = header_block + b"\r\n" + self._auth_header() + b"\r\n" + rest
upstream_sock.sendall(patched)
else:
# Обычный HTTP-запрос — тоже вставляем Proxy-Authorization
# первой строкой после стартовой строки запроса.
first_line, _, remainder = head.partition(b"\r\n")
patched = first_line + b"\r\n" + self._auth_header() + remainder
upstream_sock.sendall(patched)
self._pump(client_sock, upstream_sock)
except (OSError, socket.timeout) as e:
print(f" [релей:{self.listen_port}] ошибка соединения: {e}")
finally:
client_sock.close()
@staticmethod
def _pump(a: socket.socket, b: socket.socket) -> None:
"""Двунаправленная перекачка байт между клиентом и upstream-прокси
до закрытия любой из сторон — минимальный, но полностью рабочий
TCP-форвардинг на select()."""
sockets = [a, b]
try:
while True:
readable, _, exceptional = select.select(sockets, [], sockets, 60)
if exceptional or not readable:
break
done = False
for s in readable:
other = b if s is a else a
try:
data = s.recv(65536)
except OSError:
done = True
break
if not data:
done = True
break
other.sendall(data)
if done:
break
finally:
a.close()
b.close()
# ======================================================================
# 4) ВЫСОКОУРОВНЕВАЯ ЛОГИКА: НАЗНАЧЕНИЕ ПРОКСИ УСТРОЙСТВУ
# ======================================================================
_active_relays: List[LocalAuthRelay] = []
_next_local_port = LOCAL_RELAY_PORT_START
def assign_proxy_to_device(serial: str, proxy: ProxyEntry) -> bool:
"""Назначает прокси устройству: если прокси анонимный — просто
прописывает ip:port напрямую. Если у прокси есть логин/пароль —
поднимает локальный релей, пробрасывает его на устройство через
adb reverse и прописывает устройству 127.0.0.1:<порт>."""
global _next_local_port
if not proxy.needs_auth:
return adb_set_http_proxy(serial, proxy.host, proxy.port)
local_port = _next_local_port
_next_local_port += 1
relay = LocalAuthRelay(listen_port=local_port, upstream=proxy)
relay.start()
_active_relays.append(relay)
time.sleep(0.3) # даём релею время забиндить порт перед adb reverse
if not adb_reverse_port(serial, local_port):
relay.stop()
return False
return adb_set_http_proxy(serial, LOCAL_RELAY_HOST, local_port)
def cmd_global(devices: List[str], proxies: List[ProxyEntry], shift: int) -> None:
print(f"\nГлобальный режим: {len(devices)} устройств, {len(proxies)} прокси, сдвиг={shift}\n")
for i, serial in enumerate(devices):
proxy = proxies[(i + shift) % len(proxies)]
print(f"[{i + 1}/{len(devices)}] {serial} -> {proxy}")
assign_proxy_to_device(serial, proxy)
def cmd_individual(serial: str, proxies: List[ProxyEntry], line: int) -> None:
if line < 1 or line > len(proxies):
raise SystemExit(f"--line {line} вне диапазона (в proxies.txt валидных строк: {len(proxies)})")
proxy = proxies[line - 1]
print(f"\nИндивидуальный режим: {serial} -> строка {line} ({proxy})\n")
assign_proxy_to_device(serial, proxy)
def cmd_reset(devices: List[str]) -> None:
print(f"\nСброс прокси на {len(devices)} устройствах\n")
for serial in devices:
adb_reset_http_proxy(serial)
def cmd_list(devices: List[str]) -> None:
print(f"\nПодключено устройств: {len(devices)}\n")
for serial in devices:
current = adb_get_http_proxy(serial)
print(f" {serial:<24} текущий прокси: {current}")
# ======================================================================
# 5) ТОЧКА ВХОДА
# ======================================================================
def main() -> None:
parser = argparse.ArgumentParser(description="Управление системным HTTP-прокси Android-устройств через ADB.")
parser.add_argument("--mode", choices=["global", "individual", "reset", "list"], required=True)
parser.add_argument("--shift", type=int, default=0, help="Сдвиг/ротация при назначении в глобальном режиме")
parser.add_argument("--serial", help="Serial ID конкретного устройства (individual / reset --serial)")
parser.add_argument("--line", type=int, help="Номер строки в proxies.txt (начиная с 1) для --mode individual")
parser.add_argument("--all", action="store_true", help="Применить --mode reset ко ВСЕМ подключенным устройствам")
parser.add_argument("--proxies-file", default=str(PROXIES_FILE), help="Свой путь к proxies.txt")
args = parser.parse_args()
devices = list_adb_devices()
if not devices:
raise SystemExit("Не найдено ни одного устройства в состоянии 'device'. Проверьте 'adb devices'.")
if args.mode == "list":
cmd_list(devices)
return
if args.mode == "reset":
if args.serial:
cmd_reset([args.serial])
elif args.all:
cmd_reset(devices)
else:
raise SystemExit("--mode reset требует --serial <id> либо --all")
return
proxies = load_proxies(Path(args.proxies_file))
if args.mode == "global":
cmd_global(devices, proxies, args.shift)
elif args.mode == "individual":
if not args.serial or not args.line:
raise SystemExit("--mode individual требует --serial <id> и --line <N>")
cmd_individual(args.serial, proxies, args.line)
if _active_relays:
print(f"\n{len(_active_relays)} локальных релея(ев) для авторизованных прокси остаются "
f"запущенными в фоне — не закрывайте это окно консоли, пока устройства работают через них.")
try:
while True:
time.sleep(3600)
except KeyboardInterrupt:
print("\nОстанавливаю релеи...")
for relay in _active_relays:
relay.stop()
if __name__ == "__main__":
main()
Примеры запуска:
# Посмотреть подключенные устройства и их текущий прокси
python adb_proxy_manager.py --mode list
# Глобально раздать прокси из файла на все устройства (с ротацией)
python adb_proxy_manager.py --mode global --shift 0
# Назначить конкретной строкой прокси конкретному устройству
python adb_proxy_manager.py --mode individual --serial emulator-5554 --line 3
# Сбросить прокси на всех устройствах перед сменой пула
python adb_proxy_manager.py --mode reset --all
Логика проверки доступности сознательно не строится на ping изнутри Android-шелла — на многих прошивках ICMP из shell заблокирован политиками SELinux, а сама проверка ничего не говорит о работоспособности именно HTTP(S)-прокси. Вместо этого скрипт читает обратно settings get global http_proxy сразу после установки (режим --mode list) — это подтверждает, что значение реально принялось системой, а не было тихо отклонено (так бывает, если формат строки нарушен).
1.5. Гибридное решение капч: ZennoPoster + CapMonster Cloud
Раз распознавание физически должно происходить снаружи устройства, архитектура решения капч выглядит так:- Обнаружение. Мобильное приложение/сценарий в браузере эмулятора при появлении капчи не пытается решить её самостоятельно — оно останавливается и либо делает скриншот проблемного экрана (сохраняя его в общую папку, см. раздел 3), либо, если капча имеет программный API (reCAPTCHA/hCaptcha с site-key в DOM браузерной версии), извлекает site-key и page-url.
- Передача в ZennoPoster. ZennoPoster забирает скриншот или пару site-key/page-url через тот же файловый мост, которым обмениваются все остальные сигналы между модулями (см. раздел 3) — никакого отдельного канала заводить не нужно, это то же самое .txt-триггер с полем type: captcha вместо type: publish.
- Распознавание. ZennoPoster обращается к CapMonster Cloud API (или аналогичному сервису) — создаёт задачу createTask с нужным типом (ImageToTextTask для картиночной капчи, NoCaptchaTaskProxyless/HCaptchaTaskProxyless для программных капч), опрашивает getTaskResult до готовности и получает готовый токен или текст.
- Возврат токена на устройство. Готовый ответ ZennoPoster записывает обратно в общую папку тем же файловым механизмом — приложение, которое всё это время ждало (см. цикл опроса в разделе 3.2), подхватывает токен и либо вставляет его в скрытое поле формы через InputText, либо кликает по загруженной картинке в нужных координатах, если капча была графической.
Ключевое архитектурное следствие: с точки зрения обмена данными капча — это не отдельная подсистема, а ещё один тип сообщения в той же файловой шине, которая уже используется для публикации контента. Не нужно проектировать вторую систему обмена — достаточно расширить схему .txt-файла одним полем type.
2. Приложение Automator: что это, сколько стоит и на каких настройках MEmu собраны макросы
2.1. О приложении
Мобильная сторона связки — не самописный APK «для служебного пользования», а обычное, легально опубликованное в Google Play приложение Automator — Smart Macro Tool (разработчик AI Code Sherlock, пакет com.automator.app). Это визуальный конструктор макросов без необходимости root для базовых сценариев: свыше 400 действий, запись и воспроизведение жестов (тапы, свайпы, drag-and-drop, мультитач, ввод текста), условия/циклы/подпрограммы/try-catch и параллельное выполнение внутри самого макроса, распознавание экрана через OCR и сравнение с картинкой-шаблоном (то самое INPUT_ROOT action: IMAGE, на котором строятся макросы из раздела 3), триггеры по расписанию/геозоне/NFC/уведомлениям, сетевые инструменты (встроенный HTTP-сервер, WebSocket, MQTT, FTP/SFTP — то есть при желании ту же самую файловую шину из раздела 3 можно заменить на HTTP или WebSocket прямо средствами приложения, без стороннего кода), работу с файлами/JSON/regex/SQLite, а также опциональный слой ИИ (облачные провайдеры, свой сервер в локальной сети или полностью офлайн-модели прямо на устройстве).Важно: приложение платное.
Automator распространяется не бесплатно — на момент подготовки статьи цена варьируется примерно от $0.99 до $4.99 в зависимости от гео/страны аккаунта Google Play. После одной покупки приложение можно устанавливать неограниченное количество раз на разные устройства/эмуляторы, привязанные к тому же аккаунту Google — но здесь стоит знать меру: Google Play не относится благосклонно к аккаунтам, которые массово переустанавливают платное приложение на десятки эмуляторов подряд, поэтому для крупных ферм разумно распределять покупки по нескольким аккаунтам, а не заводить один аккаунт-донор на всю ферму.
Официальная страница в Google Play: https://play.google.com/store/apps/details?id=com.automator.app. Минимальные требования — Android 8.0 и новее, включённая служба специальных возможностей (Accessibility — без неё приложение физически не может ни читать экран, ни нажимать за пользователя) и отключённая экономия батареи для самого приложения (иначе система будет останавливать фоновое выполнение по расписанию). Root нужен не для запуска приложения в целом, а точечно — только для части действий (системные настройки, коды клавиш, shell-команды), что как раз и объясняет, почему в конфигурации MEmu из следующего пункта root включён: без него не сработают шаги, которые меняют системные настройки устройства.
2.2. Точные настройки MEmu, под которые собраны прилагаемые шаблоны макросов
Отдельно стоит подчеркнуть то, что легко упустить и на чём стоит заострить внимание: макросы из раздела 3 распознают элементы интерфейса по картинке-шаблону (см. templateBitmapPath/confidenceThreshold в коде шага). Совпадение картинки зависит от фактического разрешения экрана и плотности пикселей — то есть приложенные шаблоны гарантированно совпадут только на эмуляторе, настроенном ровно так же, как тот, на котором они были записаны. Ниже — точные значения из рабочей конфигурации, вкладки «Показать» и «Движок» в системных настройках инстанса MEmu:| Параметр | Значение |
|---|---|
| Вкладка «Показать» — Разрешение | Супер широкоэкранный, 2400 × 1080 (360 dpi) |
| Количество кадров | 60 FPS (режим высокого FPS 90 — выключен) |
| Анти-мерцание / Поворот экрана | Включены |
| Принудительный ландшафтный режим / Фиксированный размер | Выключены |
| Вкладка «Движок» — Производительность | Предустановленные настройки, «Топ ЦПУ:8 ОЗУ:6144 МБ» |
| Режим рендеринга | Vulkan |
| ASTC расшифровывание / ASTC Кэш | Авто / Выключено |
| Root | Включён (обязательно — нужен части шагов макроса и утилите adb shell settings) |
| Оптимизация памяти GPU | Выключена |
Скриншот 1: вкладка «Показать» системных настроек MEmu — разрешение 2400×1080 (360dpi), 60 FPS, анти-мерцание и поворот экрана включены
Скриншот 2: вкладка «Движок» — производительность «Топ ЦПУ:8 ОЗУ:6144 МБ», рендер Vulkan, Root включён
Если у вас другое разрешение/DPI/рендерер:
Приложенные PNG-шаблоны для распознавания кнопок не обязаны совпасть на другом разрешении — интерфейс сдвинется, элементы отмасштабируются иначе. В этом случае самый быстрый путь — либо повторить конфигурацию инстанса один в один по таблице выше, либо просто переснять несколько шаблонов под свой профиль (в конструкторе макросов это пара кликов: выделить область на живом скриншоте устройства). Сама логика макроса (порядок шагов, работа с общей папкой, файлы-сигналы) от разрешения экрана никак не зависит и менять её не нужно — трогать придётся только картинки-шаблоны.
2.3. Бонус для читателей: 15 кодов активации
Автор статьи предоставляет читателям 15 кодов бесплатной активации Automator в Google Play — по принципу «кто успел, тот и получил», каждый код одноразовый:
3. Практический кейс: файловая синхронизация и оркестрация через общую папку MEmu
Самая простая, надёжная и доступная архитектура обмена данными между ZennoPoster и мобильным приложением — не HTTP, не сокет и не ADB push на каждый чих, а обычная расшаренная папка MEmu (Shared Folder), в которой обе стороны читают и пишут простые текстовые файлы-сигналы. Ниже — разбор реального работающего проекта, воспроизведённый из связки ZennoPoster-оркестратора и Android-приложения Automator, установленного из Google Play на MEmu и на физические устройства.Все конкретные числа ниже — пример, а не обязательные значения:
Пять циклов генерации подряд, пауза опроса ровно 60 секунд, именно такие имена площадок и подпапок — это конкретная рабочая конфигурация одного проекта, которую удобно разобрать на реальном примере. Сама архитектура «сигнал → работа → отчёт» не привязана к этим цифрам: количество циклов, интервал опроса, структуру папок и состав площадок можно настроить абсолютно любым удобным способом под свою ферму — меняются только значения переменных и пути, логика связки остаётся той же.
Ещё один принципиальный момент, который стоит проговорить сразу: у одного устройства — один экран и одна активная сессия службы специальных возможностей, поэтому пять макросов (по одному на площадку) физически не могут выполняться параллельно, даже если материалы для всех пяти площадок ZennoPoster разложил по общим папкам одним пакетом. На устройстве они отрабатывают строго последовательно, один за другим: пока приложение публикует ролик в Facebook, оно не может параллельно нажимать что-то в интерфейсе Instagram — это не ограничение архитектуры, а естественное следствие того, что автоматизация управляет реальным экраном.
Скриншот 3: внешний цикл оркестратора: сброс счётчика, чтение списка директорий вывода, Regex для выделения номеров проектов, сверка с уже пройденными номерами (защита от дублей) → пауза → пять очередей площадок → счётчик {sch} растёт на 1 за проход; при {sch}>=5 цикл останавливается уведомлением «Залив готов!»
3.1. Сигнал на старт: ZennoPoster → приложение
Прежде чем что-либо распределять по площадкам, оркестратору нужен сам контент. Цикл начинается с вызова двух внешних программ — clean_project (уборка рабочих папок перед новой партией, тот же принцип, что и clean_project.pyw на стороне публикации) и следом orchestrator.py — это и есть тот самый Python-конвейер генерации контента, разобранный в статье про модульный конвейер: он подбирает тему, пишет сценарий, озвучивает и монтирует готовое видео. Здесь он вызывается как внешняя программа прямо из ZennoPoster, а не запускается отдельно вручную.Коротко о том, что нужно заполнить в проекте генерации перед первым запуском:
orchestrator.py — это конвейер trend_pipeline со своим config/settings.yaml: там задаются ниша и название бренда (niche, brand_name), источники тем и провайдер LLM/TTS вместе с ключами API, число роликов за прогон (флаг --count, тот же счётчик, что и {sch} в цикле оркестратора ZennoPoster — их разумно держать равными), и пути вывода (paths.output_dir), куда лягут output_video, output_shorts, output_metadata, output_thumbnails — те самые очереди, которые оркестратор ZennoPoster дальше вычищает после каждой успешной публикации (см. скриншот ниже). Без заполненных ключей провайдера и корректной ниши в settings.yaml конвейер либо упадёт на первом же шаге, либо начнёт подставлять фикстуры из папки fixtures/ вместо реальной генерации — так что перед первым проходом стоит явно проверить оба файла.
Скриншот 4: начало цикла оркестратора: clean_project → orchestrator.py (вызов Python-конвейера генерации) → последовательная очистка очередей output_video / output_shorts / output_metadata / output_thumbnails → чтение файла metadata и разбор его C#-кодом
Когда видео готово, оркестратор поочерёдно проходит четыре очереди вывода конвейера — output_video, output_shorts, output_metadata, output_thumbnails — читая и вычищая обработанные строки перед тем как взять новую (это то же самое место, где, при второй сверке в конце цикла, будут удалены записи об уже опубликованных материалах). Затем блок metadata считывает готовый JSON с метаданными под все площадки, и единственный кастомный C#-код во всём проекте раскладывает его по переменным для каждой соцсети:
C#:
// Получаем JSON из входной переменной проекта
string jsonString = project.Variables["input_json"].Value;
// Проверяем, заполнена ли переменная
if (string.IsNullOrWhiteSpace(jsonString)) {
project.SendWarningToLog("Сниппет остановлен: входная переменная 'input_json' пуста.", true);
return "error";
}
try {
// Вспомогательный метод для извлечения текстовых полей
string ExtractField(string json, string section, string field) {
string sectionPattern = $"\"{section}\"\\s*:\\s*\\{{([^\\}}]+)\\}}";
var sectionMatch = System.Text.RegularExpressions.Regex.Match(json, sectionPattern, System.Text.RegularExpressions.RegexOptions.Singleline);
if (!sectionMatch.Success) return string.Empty;
string sectionContent = sectionMatch.Groups[1].Value;
string fieldPattern = $"\"{field}\"\\s*:\\s*\"([^\"]*)\"";
var fieldMatch = System.Text.RegularExpressions.Regex.Match(sectionContent, fieldPattern, System.Text.RegularExpressions.RegexOptions.Singleline);
if (fieldMatch.Success) {
// Корректно декодируем экранированные символы (например, перевод строк \n)
return System.Text.RegularExpressions.Regex.Unescape(fieldMatch.Groups[1].Value);
}
return string.Empty;
}
// Вспомогательный метод для извлечения массивов (теги, хэштеги)
string ExtractArray(string json, string section, string field, string separator) {
string sectionPattern = $"\"{section}\"\\s*:\\s*\\{{([^\\}}]+)\\}}";
var sectionMatch = System.Text.RegularExpressions.Regex.Match(json, sectionPattern, System.Text.RegularExpressions.RegexOptions.Singleline);
if (!sectionMatch.Success) return string.Empty;
string sectionContent = sectionMatch.Groups[1].Value;
string arrayPattern = $"\"{field}\"\\s*:\\s*\\[([^\\]]+)\\]";
var arrayMatch = System.Text.RegularExpressions.Regex.Match(sectionContent, arrayPattern, System.Text.RegularExpressions.RegexOptions.Singleline);
if (arrayMatch.Success) {
string arrayContent = arrayMatch.Groups[1].Value;
var items = new System.Collections.Generic.List<string>();
var itemMatches = System.Text.RegularExpressions.Regex.Matches(arrayContent, "\"([^\"]*)\"");
foreach (System.Text.RegularExpressions.Match m in itemMatches) {
items.Add(m.Groups[1].Value);
}
return string.Join(separator, items);
}
return string.Empty;
}
// ==========================================
// 1. YouTube
// ==========================================
project.Variables["youtube_title"].Value = ExtractField(jsonString, "youtube", "title");
project.Variables["youtube_description"].Value = ExtractField(jsonString, "youtube", "description");
project.Variables["youtube_tags"].Value = ExtractArray(jsonString, "youtube", "tags", ", ");
// ==========================================
// 2. Instagram
// ==========================================
project.Variables["instagram_caption"].Value = ExtractField(jsonString, "instagram", "caption");
project.Variables["instagram_hashtags"].Value = ExtractArray(jsonString, "instagram", "hashtags", " ");
// ==========================================
// 3. TikTok
// ==========================================
project.Variables["tiktok_caption"].Value = ExtractField(jsonString, "tiktok", "caption");
project.Variables["tiktok_hashtags"].Value = ExtractArray(jsonString, "tiktok", "hashtags", " ");
// ==========================================
// 4. Facebook
// ==========================================
project.Variables["facebook_text"].Value = ExtractField(jsonString, "facebook", "text");
project.SendInfoToLog("JSON успешно распарсен, все переменные распределены.", true);
return "ok";
}
catch (Exception ex) {
project.SendErrorToLog($"Ошибка при разборе JSON: {ex.Message}", true);
return "error";
}
Разбор построен на регулярных выражениях, а не на полноценном JSON-парсере — это осознанное упрощение: структура входного JSON стабильна (её формирует тот же конвейер, 09_generate_metadata.py из статьи про модульный конвейер, а не сторонний источник), поэтому пара регулярок с секцией и полем внутри неё работает надёжно и не требует подключать внешнюю библиотеку прямо в C#-сниппете ZennoPoster. ExtractField достаёт одно текстовое значение (заголовок, описание, подпись), ExtractArray — список (теги/хэштеги), с разным разделителем на выходе: через запятую для тегов YouTube, через пробел для хэштегов Instagram/TikTok, чтобы сразу получить готовую строку в том виде, в котором её ожидает форма конкретной площадки.
После разбора для каждой площадки в проекте есть готовый набор переменных, и дальше оркестратор просто проходит по пяти одинаковым по структуре, но разным по набору полей цепочкам — по одной на площадку:
Скриншот 5: очереди YOUTUBE MAIN и YOUTUBE SHORT: чтение очередной строки output_video → запись video / titles / tags / descriptions → создание to_work → цикл ожидания ready.txt/lose.txt с паузой 60 c → очистка на устройстве
Для YouTube (основной ролик и Shorts) в очередь идут четыре текстовых поля — video, titles, tags, descriptions — они записываются в отдельные файлы внутри общей папки, а затем создаётся файл-триггер to_work. Интересная деталь именно этого проекта: оба блока, YOUTUBE MAIN и YOUTUBE SHORT, читают очередную готовую запись из одной и той же очереди output_video — то есть автор конвейера сознательно публикует под Shorts тот же ролик, что ушёл в основной YouTube, а не отдельно смонтированную вертикальную версию; при желании это меняется на отдельную очередь output_shorts, которую конвейер тоже формирует.
Скриншот 6: очереди TikTok, Instagram и Facebook: TikTok и Instagram получают video + подпись + tags, Facebook — только video + facebook_caption (тегов у площадки нет), остальная схема (to_work → ожидание ready.txt/lose.txt → очистка) идентична YouTube-очередям
У TikTok и Instagram набор полей короче — video, своя подпись (tiktok_caption / instagram_caption) и tags, без отдельных заголовков и описаний. У Facebook он ещё компактнее: только video и facebook_caption — площадка не разделяет заголовок, описание и теги, у поста есть только текст. Перед записью каждая из этих трёх очередей (в отличие от YouTube-блоков, идущих сразу за разбором метаданных) начинается с Удалить файл + Пауза 0 c — подчистка возможного файла, оставшегося от прошлого неудачного прогона, перед тем как записать свежий.
После записи всех полей и файла-триггера to_work для каждой площадки включается один и тот же паттерн ожидания: блоки lose.txt и проверка на готовность ожидание файла ready.txt, соединённые в цикл с Пауза 60 c — обычные встроенные блоки ZennoPoster «Проверить существование файла» и «Пауза», без единой строчки кода. Если раньше появляется lose.txt — событие уходит в алерт «Ошибка залива в проекте {Variable.default_path_to_ph...}»; если ready.txt — управление уходит в группу «очистка залитого в папке проекта на устройстве», где четыре блока «Удалить директорию» вычищают использованные материалы.
Файл-триггер to_work — это и есть механизм передачи инструкций и метаданных, о котором шла речь в задаче: ZennoPoster не отправляет команду напрямую, а кладёт материалы и текстовые поля в общую папку и ждёт результата. Устройство физически не может быть «дёрнуто» с ПК — обратной связи по сети между Windows-процессом ZennoPoster и приложением на Android нет, поэтому вся синхронизация построена на наблюдаемом состоянии файловой системы, а не на прямых вызовах.
3.2. Опрос и подхват: приложение на устройстве
На стороне Android-приложения (реальный проект «Automator», установленный из Google Play — конструктор макросов с движком пошаговых сценариев, распознаванием элементов экрана по resourceId/тексту/шаблону-картинке и файловыми операциями) логика зеркальна. Разберём реальный рабочий макрос auto_facebook пошагово, вживую, по его собственным JSON-описаниям шагов:
JSON:
// Шаг 1 — READ_FILE_LINES: читаем файл-триггер to_work.txt,
// который положил ZennoPoster, в переменную "prv"
{
"type": "READ_FILE_LINES",
"filePath": "{path_file_to_Work}",
"targetVariableName": "prv",
"skipEmptyLines": true,
"onErrorAction": "ContinueAnyway" // если файла ещё нет — не ошибка, а сигнал "пока нечего делать"
}
// Шаг 2 — FILE_OPERATION (DELETE): сразу удаляем to_work.txt,
// чтобы при следующем опросе не обработать тот же сигнал дважды
{ "type": "FILE_OPERATION", "mode": "DELETE", "filePath": "{path_file_to_Work}" }
// Переменные путей заданы один раз в начале макроса —
// это и есть карта общей папки на конкретном устройстве:
path_file_ready = "/storage/emulated/0/Download/Automator/facebook/ready.txt"
path_file_to_Work = "/storage/emulated/0/Download/Automator/facebook/to_work.txt"
path_folder_video = "/storage/emulated/0/Download/Automator/facebook/video/"
path_folder_text = "/storage/emulated/0/Download/Automator/facebook/text/"
path_file_to_lose = "/storage/emulated/0/Download/Automator/facebook/lose.txt"
Периодичность опроса (в задаче — «раз в минуту») реализована не бесконечным циклом внутри одного макроса, а планировщиком самого приложения: макрос запускается по расписанию раз в 60 секунд, каждый раз быстро проверяет наличие to_work.txt и либо тут же завершается (если сигнала ещё нет — это дёшево по ресурсам и не держит устройство постоянно занятым), либо — если сигнал появился — переходит к полной публикации.
Дальше макрос находит нужное готовое видео и текстовые поля через FILE_BROWSER, открывает facebook.com в Chrome (принудительно версия для ПК, см. пункт 1.3), находит элементы интерфейса по картинке-шаблону с порогом схожести confidenceThreshold: 0.85 (устойчиво к небольшим визуальным отличиям темы/локализации), вводит текст и подтверждает публикацию:
Дальше макрос находит нужное готовое видео и текстовые поля через FILE_BROWSER, открывает facebook.com в Chrome (принудительно версия для ПК, см. пункт 1.3), находит элементы интерфейса по картинке-шаблону с порогом схожести confidenceThreshold: 0.85 (устойчиво к небольшим визуальным отличиям темы/локализации), вводит текст и подтверждает публикацию:
JSON:
// Поиск и клик по элементу через сравнение со скриншотом-эталоном —
// работает даже там, где у элемента нет стабильного resourceId
{
"type": "INPUT_ROOT",
"action": "IMAGE",
"templateBitmapPath": "templates/tpl_1787653479763.png",
"confidenceThreshold": 0.85,
"matchMaxWaitMs": 10000,
"region": { "left": 1954, "top": 202, "right": 2028, "bottom": 274 },
"findQuery": "Новая публикация"
}
// Ввод текста заголовка из переменной, прочитанной из общей папки
{ "type": "INPUT_TEXT", "text": "{str}", "clearFirst": true }
Полную схему очередей всех пяти площадок — с деревом блоков to_work → lose.txt → проверка готовности → очистка на устройстве — мы уже разобрали на реальных скриншотах проекта в разделе 3.1.
3.3. Сигнал о завершении: приложение → ZennoPoster
По завершении публикации макрос пишет один из двух возможных файлов-отчётов — успех или явную ошибку, никогда не оставляя ZennoPoster неопределённо ждать без обратной связи:
JSON:
// УСПЕХ: записываем файл-сигнал готовности
{
"type": "FILE_OPERATION", "mode": "WRITE",
"filePath": "{path_file_ready}",
"content": "ready",
"overwriteIfExists": false // не перетираем, если предыдущий отчёт ещё не забрали
}
// ОШИБКА (например, картинка-шаблон кнопки "Опубликовать" не нашлась
// за отведённые 10 секунд — сработала ветка onFailureStepId):
{
"type": "FILE_OPERATION", "mode": "WRITE",
"filePath": "{path_file_to_lose}",
"content": "Ошибка залива"
}
{ "type": "FLOW_END_BAD" } // явно завершаем сценарий как неуспешный, а не тихо зависаем
Со стороны ZennoPoster это ровно тот цикл опроса, что был показан в разделе 3.1: проверка на готовность ожидание файла ready.txt с паузой 60 секунд между попытками, и параллельная проверка lose.txt — если он появился раньше ready.txt, ZennoPoster не тратит время до истечения полного таймаута, а сразу переходит к обработке ошибки и логированию, что именно пошло не так.
Симметрия file-based протокола в обе стороны — главное, что делает эту архитектуру надёжной: обе стороны никогда не предполагают состояние друг друга, а только читают его из файловой системы. Если процесс ZennoPoster перезапустят посреди ожидания — он просто продолжит опрашивать ready.txt/lose.txt после рестарта, ничего не потеряв, потому что всё состояние живёт не в оперативной памяти, а на диске.
Симметрия file-based протокола в обе стороны — главное, что делает эту архитектуру надёжной: обе стороны никогда не предполагают состояние друг друга, а только читают его из файловой системы. Если процесс ZennoPoster перезапустят посреди ожидания — он просто продолжит опрашивать ready.txt/lose.txt после рестарта, ничего не потеряв, потому что всё состояние живёт не в оперативной памяти, а на диске.
4. Гибкость и альтернативные пути связи
Файловый триггер через .txt — самый простой и надёжный вариант именно потому, что не требует поднимать сервер, открывать порты или разбираться с сетевой доступностью эмулятора. Но важно понимать: сама архитектура «сигнал → работа → отчёт» не привязана к файлам — это универсальный паттерн, который без изменения логики переносится на любой другой транспорт:| Транспорт | Когда предпочтителен | Что меняется в логике |
|---|---|---|
| Общая папка (.txt-файлы) | MEmu/LDPlayer с Shared Folder, один ПК управляет пачкой эмуляторов на себе | Ничего — это базовый вариант из раздела 3 |
| HTTP-сервер на ПК (Flask/FastAPI) | Ферма из физических устройств без общей файловой системы, устройства в другой Wi-Fi сети | Устройство опрашивает GET /task вместо чтения файла, отчитывается POST /result |
| WebSocket | Нужна мгновенная реакция без задержки опроса (polling) — например, быстрая передача токена капчи | ZennoPoster держит соединение и пушит задание сразу по готовности, без ожидания следующего цикла опроса |
| ADB push/pull | Физические устройства без Shared Folder вообще (только USB/Wi-Fi ADB-доступ) | Вместо копирования в сетевую папку — adb push файл /sdcard/.../to_work.txt, вместо чтения ready.txt — adb pull |
| Прямое сокетное соединение | Максимальный контроль и минимальные накладные расходы, разработчик пишет свой сервер и свой клиент под Android | Полностью кастомный протокол — гибкость ценой дополнительной разработки |
Практическая рекомендация: начинать всегда с файлового варианта — он реализуется за час, отлаживается визуально (можно просто открыть папку в проводнике и увидеть, застрял ли процесс на конкретном шаге), и переживает перезапуск любой из сторон без потери состояния. Переходить на HTTP/WebSocket/сокеты имеет смысл только тогда, когда счёт устройств идёт на сотни и накладные расходы на дисковый I/O действительно становятся узким местом — для типичной фермы из нескольких десятков эмуляторов и телефонов файловый мост обгонит все более сложные альтернативы по соотношению «надёжность / время на внедрение».
5. Настройка общих папок и интеграция со ZennoPoster
5.1. Настройка Shared Folder в MEmu
1. Открыть MEmu Multi-Player (или настройки конкретного инстанса) → раздел Shared Folder.2. Указать путь на диске ПК, который станет общей папкой — например, C:\MEmu-shared\facebook\. Рекомендуется завести отдельную подпапку под каждую площадку публикации, зеркально структуре, которая уже видна в проекте («facebook», «instagram», «TikTok», «youtube», «youtube_short»).
3. Внутри Android-инстанса указанный путь ПК становится доступен приложениям по адресу вида /storage/emulated/0/Download/Automator/<площадка>/ — именно этот путь и записан в переменные path_file_ready / path_folder_video макроса.
4. Создать внутри каждой папки-площадки подпапки video/, text/ (или descriptions/, caption/, tags/ — по аналогии со схемой из статьи про модульный конвейер публикации) — именно их сканирует FILE_BROWSER внутри макроса.
5. Для физических устройств без функции Shared Folder эмулятора — та же папка ПК синхронизируется иначе: либо через adb push/adb pull по расписанию (см. раздел 4), либо через любое стороннее приложение синхронизации папок (Syncthing, FolderSync), настроенное на тот же путь /storage/emulated/0/Download/Automator/<площадка>/ на телефоне.
5.2. Итоговая цепочка: создание, чтение и удаление служебных .txt-файлов
Раздел 3 уже разобрал это на реальных скриншотах проекта, здесь — короткое резюме всей цепочки от начала до конца, чтобы держать её перед глазами при настройке своей копии:- ZennoPoster, запись. Готовые текстовые поля (video/titles/tags/descriptions или их сокращённый набор под конкретную площадку) пишутся обычными блоками «Записать в файл» в общую папку конкретной площадки; файл-триггер to_work создаётся строго последним блоком в цепочке — только когда все данные уже на диске.
- Приложение, чтение и удаление триггера. Макрос находит to_work, читает его и сразу удаляет — чтобы при следующем опросе (раз в 60 секунд по расписанию самого приложения) не обработать тот же сигнал повторно.
- Приложение, публикация и отчёт. Найдя нужные файлы через FILE_BROWSER, макрос вводит текст и жмёт публикацию по картинке-шаблону (см. раздел 1.3 про десктопную версию сайта), затем пишет либо ready.txt при успехе, либо lose.txt с текстом ошибки при сбое.
- ZennoPoster, опрос и очистка. Встроенные блоки «Проверить существование файла» + «Пауза 60 c» ждут ready.txt/lose.txt в цикле без ограничения по числу попыток (устройство может публиковать долгое видео дольше одной минуты — жёсткий таймаут по времени легко принял бы медленную, но живую загрузку за зависание). При успехе — группа «Удалить директорию» вычищает использованные материалы на устройстве; при ошибке — уведомление «Ошибка залива в проекте» и текст из lose.txt уходят в лог.
Со стороны Android-приложения (реальный JSON-шаг макроса «Automator») — именно так выглядит чтение и удаление триггера:
JSON:
{
"type": "READ_FILE_LINES",
"filePath": "{path_file_to_Work}",
"targetVariableName": "prv",
"onErrorAction": { "type": "ContinueAnyway" } // файла нет = ещё не наша очередь, не ошибка
},
{
"type": "FILE_OPERATION",
"mode": "DELETE",
"filePath": "{path_file_to_Work}" // удаляем сразу, чтобы не сработать дважды
}
6. Перспективы и масштабирование
Файловая шина сигналов — не временное решение «пока руки не дошли до нормального API», а осознанный выбор, который прямо экономит ресурсы и защищает от каскадных сбоев:- Экономия CPU/RAM хоста. Опрос файла раз в 60 секунд практически не потребляет ресурсов — в отличие от постоянно открытого HTTP-соединения или веб-сокета на каждое из десятков-сотен устройств одновременно.
- Устойчивость к перезапуску любой стороны. Ни ZennoPoster, ни приложение на устройстве не хранят состояние диалога в оперативной памяти — оно целиком на диске, поэтому падение процесса ZennoPoster, перезагрузка эмулятора MEmu или зависание конкретного макроса не роняют всю систему, а самое большее задерживают обработку одной конкретной задачи до следующего цикла опроса.
- Прозрачная отладка. Любой человек, даже без доступа к коду, может открыть общую папку в проводнике и увидеть ровно то же, что видят обе программы: лежит ли ещё to_work.txt, появился ли ready.txt, что написано в lose.txt. Это резко сокращает время диагностики по сравнению с чтением логов сетевого протокола.
- Линейное масштабирование фермы. Поскольку каждое устройство работает со своей собственной подпапкой и не конкурирует за общий ресурс (кроме дисковой полосы, которой много), одна и та же архитектура одинаково хорошо работает и на пяти эмуляторах на одном ПК, и на нескольких сотнях физических смартфонов, объединённых через сеть синхронизации папок — единственное, что растёт линейно, это количество подпапок, а не сложность кода.
- Явное разделение зон ответственности. ZennoPoster отвечает за контент и решения, требующие полноценного браузера и внешних API (генерация, капчи, аналитика); Android-приложение отвечает только за физическое воспроизведение действий на реальном мобильном интерфейсе площадки. Ни один из компонентов не обязан знать внутреннее устройство другого — только формат .txt-файлов, который обе стороны читают одинаково.
Именно эта простота и делает связку ZennoPoster + ZennoDroid + авторское Android-приложение по-настоящему промышленным решением: не потому что в ней использованы сложные технологии, а потому что каждая часть делает ровно одну вещь, хорошо документированную парой текстовых файлов на диске — и от этого систему легко и чинить, и масштабировать.
Вложения
-
Мобильная автоматизация (Zenno + Легальное Android-приложение).rar287,2 KB · Просмотры: 0
-
auto_facebook.automator.png789,1 KB · Просмотры: 2 -
auto_instagram.automator.png882,5 KB · Просмотры: 2 -
auto_tiktok.automator.png997,2 KB · Просмотры: 2 -
auto_youtube.automator.png584,6 KB · Просмотры: 1 -
auto_youtube_shorts.automator.png599 KB · Просмотры: 1
Последнее редактирование модератором:


