ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP+Autojs云控系统源码拆解:从设备接入到任务分发架构

PHP+Autojs云控系统源码拆解:从设备接入到任务分发架构 我在不少开源站和付费群里见过类似“PHP Autojs 云控框架源码”的东西多数是打包好的后台加一个 Autojs 脚本配上简陋的说明文档。但真正拿到手你会发现光有代码根本跑不起来——设备连不上、任务不执行、掉线没人管最后只能丢进收藏夹吃灰。这篇文章我想换个角度不吹“源码有多好”而是把整套云控系统拆开讲清楚PHP 负责什么、Autojs 负责什么、通信怎么设计、有哪些坑。如果你手里恰好有一份源码或者正准备自己写一个安卓云控系统这篇文章可以当一份“阅读理解指南”来用。1. 你手里缺的不是源码是一套链路PHP Autojs 到底在配合什么1.1 云控系统的三个角色和一条消息链云控这个名字听起来很高大上其实就是三个角色在互相配合控制端、服务端和设备端。控制端是你自己可能是电脑上的一个网页后台也可能是直接调接口的脚本服务端是跑在服务器上的 PHP 程序负责接收指令、保管设备状态、下发任务设备端就是装了 Autojs 的安卓手机它周期性地跟服务端说话领取任务并执行。对应的消息链路很简单控制端下发任务 → PHP 服务端把任务存进队列 → 手机端轮询领取 → Autojs 执行并上报结果 → PHP 更新任务状态 → 控制端看到结果这套链路里PHP 不是核心的“遥控器”而是一个永远在线的中转站。手机不开机、不联网PHP 再怎么下发也没用反过来手机一直在但 PHP 把任务状态处理错了手机就会重复执行、漏执行或者卡死。整条链路比代码本身重要得多。1.2 为什么偏偏是 PHP 和 Autojs很多做自动化的人第一反应是 Python Appium或者 Node.js WebSocket。那为什么大量云控源码选的是 PHP Autojs其实就是务实二字。先说 PHP。它最大的优点是部署简单、生态成熟虚拟机可以随便迁移。你找一个最低配的云服务器装个宝塔或 PHP Study几十秒就能跑起一个带 MySQL 的 Web 服务。而 Python 虽然写自动化脚本舒服但要在服务器上常驻跑一个调度进程还得管进程守护、多线程问题对只想快速搭起一套“控制台 设备组”的人来说门槛偏高了。再说 Autojs。它基于安卓的 AccessibilityService 无障碍服务可以用 JavaScript 直接控制界面点击、滑动、读取文本、模拟按键不需要 root。相比 Appium 要启动一个 WebDriver 服务器再连手机Autojs 轻量得多适合跑在低端机或者旧手机上一台手机一个脚本就能长时间待命。所以这个组合解决的问题很纯粹一个低门槛的服务端 一个低门槛的设备端。它不是为了大规模高并发设计的而是为了让你在已有设备资源上快速形成“集中管控”的能力。1.3 这套组合适合的场景与不适合的场景先说适合的自动化回归测试App 每次发版用一批手机跑同样的用例路径回收执行日志。个人或多设备的日常批处理比如每天早上统一刷新签到、统一清理缓存、统一上传截图。团队内部的 RPA 式辅助操作把重复性的界面操作脚本化集中管理、统计成功率。不适合的大规模高并发设备管理千台以上设备同时在线时MySQL 轮询的压力很大。对实时性要求极高的远程控制要求秒级响应就得上 WebSocket 或者 MQTTREST 轮询做不到。涉及账号批量操作、营销群控、外挂辅助的灰产场景技术上可以做但法律风险极高后面我单独聊这块的边界。2. PHP 服务端骨架设备注册、任务分发、结果回收2.1 数据模型的取舍大多数源码的数据表设计大同小异核心就两张表设备表和任务表。设备表至少要包含这些字段字段说明id自增主键device_id设备的唯一标识Autojs 里通常用 AndroidId 或 IMEIstatus设备状态online / offline / busygroup_name设备分组比如“测试一组”“生产一组”last_heartbeat最后一次心跳时间versionAutojs 脚本版本方便后文做灰度升级extra_info设备型号、安卓版本、分辨率等 JSON 扩展信息任务表的核心字段字段说明id自增主键device_id指定哪个设备执行为空表示广播给整个分组task_type任务的类型比如 click、input、upload、screenshottask_body完整可执行的 JavaScript 代码或者是 JSON 指令status任务状态pending / dispatched / running / success / failed / timeoutcreate_time / dispatch_time / finish_time任务在各阶段的时间点result执行结果的 JSON 文本这里有个容易踩坑的设计问题任务表不要存一段特别长的脚本。虽然存得下但在后台任务列表里很难阅读和搜索。更合理的做法是把脚本文件放在服务端的一个目录里任务表只存脚本路径再用 task_body 存临时参数。这样可以分版本管理脚本出问题也能回溯。2.2 三个核心接口的职责边界一个最小可用的 PHP 服务端只需要三个接口接口一设备注册与心跳上报设备启动时调一次注册接口之后每次循环都调这个接口。它太重了我一般拆成两个动作注册接口POST /api/device/register设备首次连接时上报 device_id、设备信息服务端落库。心跳接口POST /api/device/heartbeat设备周期性上报在线状态更新时间戳。拆开的好处是你可以根据心跳时间精确判断设备是否掉线注册逻辑只在首次或换脚本版本时触发。接口二任务领取设备不停地问服务端“有没有活给我”。最简单的实现是// 伪代码框架以ThinkPHP示例 public function poll() { $deviceId $this-request-param(device_id); $device Device::where(device_id, $deviceId) -where(status, online) -find(); if (!$device) { return json([code 403, msg device offline]); } // 领取一个属于自己的待执行任务 $task Task::where(device_id, $deviceId) -where(status, pending) -order(id asc) -find(); if (!$task) { return json([code 0, data []]); } // 原子性抢占任务防止多设备重复领取 $updated Task::where(id, $task[id]) -where(status, pending) -update([ status dispatched, dispatch_time date(Y-m-d H:i:s), ]); if (!$updated) { return json([code 0, data []]); } return json([code 0, data $task]); }关键点在第 22 行的where(status, pending)。为什么这里要带一个状态条件去做 update因为 PHP 最常见的坑就是你查出来时任务还是 pending但上一个执行同样逻辑的并发请求已经把任务抢走了。不加条件直接 update同一任务会被两台设备各拉一份导致重复执行。接口三结果上报设备执行完任务之后把执行日志、结果截图、状态码回传public function report() { $taskId $this-request-param(task_id); $deviceId $this-request-param(device_id); $status $this-request-param(status); // success / failed $result $this-request-param(result); Task::where(id, $taskId) -where(device_id, $deviceId) -update([ status $status, result $result, finish_time date(Y-m-d H:i:s), ]); // 通知设备下线 Device::where(device_id, $deviceId) -update([status online]); }这里注意result 是 JSON不要用单独的 result_code 和 result_message 两个字段。因为任务类型千差万别截图类任务想传 file_url点击类任务想传是否点击成功表单类任务想传表单返回值。一个 JSON 字段最灵活。2.3 任务状态机的设计状态机是这个系统最容易被忽略的部分。如果你的任务流程只是简单的“创建 → 执行 → 完成”看起来够用但一旦加上超时重试、取消任务、批量导入状态就乱套了。我常用的状态流转是这样pending - dispatched - running - success |-- failed |-- timeout dispatched - cancelled控制端取消pending任务在队列里还没人领。dispatched设备已经领走但还没回执开始执行。running设备收到后确认开始跑了。这一步不能省略因为设备可能领到任务后突然被杀死若没有 running 状态服务端永远不知道任务到底跑没跑。timeout从 dispatched 到 running 超过 30 秒或者 running 持续时间超过任务上限就自动判超时。超时判定不能依赖设备端上报。设备端死了没人会来告诉你。服务端要起一个定时任务比如每分钟扫描一次任务表把dispatched状态且超过一定时间未推进的任务改成timeout然后重新生成一个 pending 任务给设备重试或者直接标记失败。你看这样设计之后后台看任务的“半路状态”就靠谱了不会出现设备断网 5 天任务还停在“正在执行”的假象。3. Autojs 端实现从“脚本”到“真正被控的端”3.1 设备接入三部曲Autojs 端接到 PHP 服务端最核心的代码其实只有三块注册、轮询、执行。我来写一个可以跑通的最小案例。第一步设备注册// 注册或者心跳 const CONFIG { server: http://你的服务器地址, token: 自定义鉴权字符串, interval: 10000 // 轮询间隔单位毫秒 }; function register() { const res http.post(CONFIG.server /api/device/register, { device_id: device.getAndroidId(), model: device.model, version: device.release, // AndroidId 在某些定制 ROM 上可能重复可叠加 Build 信息 extra: JSON.stringify({ brand: device.brand, release: device.release, sdk: device.sdkInt }) }); const data res.body.json(); if (data.code ! 0) { console.warn(注册失败 data.msg); return false; } return true; }这里我把注册和心跳合并了实际项目里建议分开心跳频率可以比注册更频繁。第二步轮询主循环function pollTask() { const res http.post(CONFIG.server /api/task/poll, { device_id: device.getAndroidId() }, { headers: { X-Token: CONFIG.token } }); const data res.body.json(); if (data.data data.data.id) { return data.data; } return null; } function mainLoop() { while (true) { if (!device.isScreenOn()) { // 屏幕熄灭时强制点亮避免任务执行时黑屏 device.wakeUp(); } const task pollTask(); if (task) { executeTask(task); } sleep(CONFIG.interval); } }核心是while(true)循环每interval毫秒向服务端拿一次任务。这个轮询模式天然有“断线重连”的能力——就算手机网络闪断下一次轮询恢复了就自己接上了不用任何额外处理。第三步任务执行与结果上报function executeTask(task) { const taskId task.id; // 先把状态改成 running避免服务端误判超时 reportStatus(taskId, running, ); try { // 这里直接用 eval 执行服务端下发的代码 // 一定要确保你的服务端只在你控制之下否则等于远程 RCE const result eval(task.task_body); reportStatus(taskId, success, JSON.stringify({ result: result, time: Date.now() })); } catch (e) { reportStatus(taskId, failed, JSON.stringify({ error: e.toString(), stack: e.stack })); } } function reportStatus(taskId, status, result) { http.post(CONFIG.server /api/task/report, { task_id: taskId, device_id: device.getAndroidId(), status: status, result: result }, { headers: { X-Token: CONFIG.token } }); }eval是最后的手段也是最实用的手段。服务端下发什么手机就执行什么完全动态。但要明确一旦有人能修改你的 PHP 后台或者你在公网上裸放这个接口没做鉴权就等于把手机的控制权交给了别人。所以我在代码里强制加了X-Token头后文也会专门说安全策略。3.2 无障碍服务的坑与唤醒策略Autojs 依赖无障碍服务AccessibilityService来模拟点击这个服务有一个经典问题它在手机重启后默认是关闭的。很多云控源码在手机重启后静默失效就是因为没处理无障碍服务的自动开启。Autojs 提供了auto.waitFor()可以等待无障碍服务启动但并没有办法在脚本里“强制打开”它——这需要用户手动到设置里开启或者用 Autojs Pro 打包应用后在引导页做一次性授权。最稳妥的做法是脚本启动的时候先检查无障碍服务状态没开启就弹提示并跳转到系统设置页。示例auto.waitFor(); if (!isServiceOk()) { app.startActivity({ action: android.settings.ACCESSIBILITY_SETTINGS_PATH }); toast(请打开无障碍服务); auto.waitFor(); exit(); } function isServiceOk() { return auto.getService() ! null; }还有一个在低端机上经常踩的坑屏幕熄灭之后无障碍服务的触摸事件时常会被系统拦截。代码里device.wakeUp()只是点亮屏幕但如果没有解锁某些机型点击还是无效。云控设备在使用期间最实用的方案是先把自动锁屏时间调到最长再配合device.setScreenBrightness(20)降低亮度减少功耗。如果设备必须锁屏那就得在脚本里监听广播锁屏瞬间自动亮屏解锁。3.3 执行任务时如何实现“脚本分组”很多源码里只分成“界面脚本”和“设备端脚本”会用 Autojs 的 plugin 机制做成模块。其实一个最简单的组织方式是让服务端下发任务时带一个task_type字段设备端按类型加载本地对应脚本文件function executeTask(task) { const type task.type; // 本地模块表 const modules { click_flow: () require(./tasks/click_flow.js), upload_file: () require(./tasks/upload_file.js), screenshot: () require(./tasks/screenshot.js), app_launch: () require(./tasks/app_launch.js) }; const runner modules[type]; if (!runner) { throw new Error(未知任务类型 type); } return runner()(task.params); }这样服务器只告诉设备“做什么类型的事”具体逻辑在手机本地维护。好处显而易见不需要每次都给所有手机下发大段代码脚本更新时只要升级部分设备不用全网重启。这也是为什么很多云控框架里都有“脚本版本号”字段比对版本号之后决定要不要让设备拉取新脚本。4. 通信链路选择为什么我推荐“短轮询”而不是 WebSocket4.1 三种方案的对比做云控时你一定会面临通信方案的选择REST 短轮询、WebSocket 长连接、MQTT 消息推送。常见的网上言论会把 WebSocket 吹得很好但结合 PHP Autojs 这个组合我的判断是“看场景”。方案实时性服务端复杂度设备端复杂度我的评价REST 短轮询取决于轮询间隔一般 1~10 秒最低只要有 Web 服务就能跑Autojs 原生支持首选简单可靠WebSocket毫秒级需要常驻进程PHP 需搭配 Workerman/Swoole需要管理连接状态和重连逻辑不优先MQTT毫秒级需要搭建/购买 Broker需要 MQTT 客户端库适合超大规模短轮询最大的优势是无状态。设备离线、网络抖动最多只是丢几次请求轮询机制天然重连。而 WebSocket 一旦断开你得处理心跳保活、断线重连、消息补偿听起来不难实际用 PHP 跑起来之后内存泄漏和连接管理会让你怀疑人生。当然短轮询的缺陷也很明显任务下发有延迟设备多时服务端压力大。我会在下面给出一个务实的优化方案。4.2 心跳与离线判断手表计时不准你就不知道设备是真在线还是假在线。心跳时间戳是判断设备在线与否的唯一依据。服务端用定时任务扫描设备表// 建议每分钟执行一次 $threshold date(Y-m-d H:i:s, time() - 90); Device::where(last_heartbeat, , $threshold) -where(status, online) -update([status offline]);为什么阈值要大于轮询间隔的 3 倍因为轮询间隔本身是 10 秒一次 HTTP 请求在弱网下可能耗时 20 秒甚至上报后服务器才处理。90 秒的阈值可以过滤掉大部分临时抖动又不会让离线判定太迟钝。另外我强烈建议心跳接口返回服务器时间return json([ code 0, server_time time(), script_version 1.2.3, ]);设备端拿本地时间和服务器时间对比能校验自己的系统时间是否偏差过大。安卓设备时间错了会导致所有按时间触发的任务全部乱套这个坑很多源码作者没考虑到。4.3 下发代码的安全边界在 Autojs 里直接eval服务端返回的代码是功能上最灵活的方案但也是风险最高的方案。你必须做到以下几点全站 HTTPS明文 HTTP 下代码可以被中间人篡改等同远程 RCE。接口鉴权不要用简单的 device_id 就算认证。至少加一个 token服务端下发任务时校验 token 和设备状态。限制任务来源PHP 后台创建任务的入口必须登录鉴权不能用公开 URL 直接造任务。执行沙箱Autojs 的 eval 运行在 APP 沙箱内但依然可以访问文件系统、网络等资源。不要把 root 权限给 Autojs 脚本尤其不要配合adb写系统级文件。实际项目中我见过一个事故某源码把 PHP 后台直接绑在公网 IP 的 80 端口上没有任何登录页任何人都能通过POST /api/task/create?device_idxxx给在线手机下发指令。这种框架无论代码多全都是出生就带洞。我建议至少用一层简单的Authorization: Bearertoken后台与后台之间再用独立密钥。5. 落地部署与稳定性优化5.1 服务端部署要点PHP 部分如果你用 ThinkPHP 或 Laravel部署本身没太多说的。但我有几个针对云控场景的小建议PHP 进程不要开太多。绝大部分云控请求是短轮询每个请求执行体量很小用 PHP-FPM 默认配置就行。反而是 MySQL 的连接数容易被打满注意调max_connections。给 MySQL 做读写分离没意义。这个阶段设备量级还没到但一定要建立索引。task表检索条件就是device_id status这个联合索引必须建否则任务一多全表扫描能把数据库拖死。CREATE INDEX idx_device_status ON task(device_id, status);用 Nginx 做访问日志裁剪。轮询接口每分钟会打大量 access_log日志文件几天就能涨到几个 GB。直接在 Nginx 配access_log off或者只记录状态码非 200 的请求可以少很多运维压力。5.2 多设备并发下的性能隐患当设备数量超过 50 台最容易出问题的是“同时轮询”。假设 50 台设备每 10 秒轮询一次平均每秒才 5 个请求问题不大。但如果设备被配置为 1 秒轮询一次50 个请求每秒服务器也能撑住可数据库的即时写入压力会变大。我实测过一种很隐蔽的性能问题所有设备在同一时刻发请求形成“刺猬型”尖峰。原因是每个设备的sleep(interval)都从同一基准时间开始设备只要保持同步请求就会一拨一拨来。解决办法很简单让设备端的启程时间错开// 根据 deviceId 哈希结果错峰 const offset (device.getAndroidId().hashCode() % 100) * 100; sleep(offset);100 毫秒的错峰在人工操作层面完全无感但能让服务端的压力曲线平滑很多。5.3 实测中容易翻车的细节分享几个我自己踩过的相对冷门的坑。第一个坑Autojs 的http模块在部分机型上断流。表现是设备跑了一个小时后轮询请求全部失败但网络本身是正常的。最终排查到是某些 Android 版本对底层 OkHttp 连接池回收不及时导致连接被服务端无声关闭。解决方案是在每次请求前强制设置http.setGlobalConfig({timeout: 10000})并关闭连接复用策略或者在主循环里定期http.clearAllCookies()清理状态。第二个坑MySQL 里的 text 字段存中文导致任务截断。这个不常发生在 PHP 侧而是 Autojs 端上报 result 时用了JSON.stringify({result: result})结果里包含大段的中文日志。如果数据库表是 utf8mb4_general_ci 问题不大但如果你为了兼容老数据用了 utf8 排序规则部分 emoji 或生僻字写入时会报错导致整个接口抛出 500设备端误判为任务失败。尽量所有表统一 utf8mb4。第三个坑设备 ID 用 AndroidId但 AndroidId 在恢复出厂后会变。这在测试环境不明显一旦设备交付给第三方人家一重置手机设备就变成了新设备历史任务全丢了。最好加上品牌、型号组合生成一个自定义 ID双重保险。6. 云控工具的红线这套组合能做什么不能做什么我必须把这条单独拿出来说。云控这个技术名词在公众认知里越来越偏向“群控”“批量操作”所以一套源码拿到手怎么看它的设计意图可以做这些企业内部自动化测试App 发版前的冒烟测试、兼容性测试、UI 回归测试。把测试用例脚本用 Autojs 跑把结果回传到 PHP 后台统一分析非常合适。个人设备的自动化管家定时清缓存、定时检查更新、定时备份相册到 NAS。自己写几个小任务挂在家里旧手机上。团队协作的脚本库把常用自动化能力沉淀成插件模块设备分组管理适合小团队做 RPA 辅助。不要碰这些批量雷同账号的批量操作。无论你是做“引流”“刷量”还是“群控”这都会直接踩到平台方规则和法律风险。绕过实名制、验证码、人机校验去抢购/签到/薅羊毛。任何要求手机 root 并静默安装系统级模块的方案。哪怕是技术上完全可行的东西你也要想清楚运行它的人是你自己还是别人。一旦别人拿到你的后台地址且鉴权又没做好这不是“云控”这是“给他人的送分器”。最后聊点实在的。我曾经把一套单点轮询的云控框架在 100 台测试机上跑了两周发现真正的问题往往不出在 PHP 代码也不出在 Autojs 语法而是出在“设备不守时”这件事上。网络延迟、系统休眠、App 被杀、屏幕锁住任何一环没稳住链路就断了。所以我给你的第一建议不是去改源码而是先在两台手机上把“注册 → 轮询 → 执行 → 上报”跑通再考虑上批量。任何云控系统稳定性的地基都在那台手机的脚本进程到底活多久。
返回列表