AutoJS脚本工程化解析:从无障碍服务到稳定签到任务
2026/9/16 19:38:38 网站建设 项目流程

简介:收集各种薅羊毛脚本.zip 是一份基于 AutoJS 的 Android 自动化脚本合集,面向希望借助工具参与平台优惠活动、减少重复操作的用户。压缩包共包含 79 个文件,以 73 个 JavaScript 脚本为主,涉及签到、做任务、领奖励等常见场景,另附 2 个 Python 辅助脚本、Shell 脚本以及配置和说明文档,整包仅 1.44MB,便于下载和筛选。目前已有 288 人学习下载。脚本集合覆盖多个平台和活动,如主流电商、阅读、视频等,文件名直观,便于按需选用。通过阅读这些代码,可以了解 AutoJS 的触摸模拟、定时任务、网络请求等 API 用法,也能学习如何在脚本中处理异常、维护登录状态。需要留意的是,自动化薅羊毛存在账号风控与合规风险,使用前应仔细阅读平台规则,避免因滥用导致封号。适合有一定 JavaScript 基础、想提升自动化效率或研究脚本编写的开发者参考。

1. 拆开一份几十个 ym_ 脚本的 AutoJS 项目包,先看到什么

这份收集各种薅羊毛脚本.zip解开之后,bienaoccc_hym-main目录里躺着几十个命名整齐的.js文件:ym_jd.jsym_meituan.jsym_elm.jsym_wangyiyanxuan.js,按平台缩写区分的主脚本挤满了第一屏,旁边还跟着sendNotify.pysendNotify.jsql.js这类支撑文件。说句让多数人意外的话:这类脚本包真正容易翻车的地方,从来不是签到逻辑本身,而是脚本开头没等无障碍服务就绪、选择器超时设置不合理、App 弹窗把流程打断之后没有兜底。下面按 AutoJS 运行机制、脚本骨架、工程化改造三条线来拆,适合已经碰过一点 Android 自动化、想把脚本从“能跑”做到“长期不挂”的工程师。

2. AutoJS 运行原理:控件树、选择器与引擎边界

2.1 无障碍服务、脚本引擎和任务触发三层分工

AutoJS 不是把 JavaScript 直接塞进目标 App 进程里,它本身是一个常驻设备的前台与后台混合应用,通过 Android 无障碍服务拿到当前窗口的 View 树。脚本引擎在 AutoJS 进程内解析 JS,把text("签到").click()这类调用翻译成“在 View 树上找到匹配节点,再注入一个点击事件”。整个体系可以拆成三层来理解。

第一层是无障碍服务。系统把当前界面的控件结构序列化成节点树,脚本里所有 find 系列方法都在这棵树上做遍历搜索。凡是uiautomator dump能导出界面结构的应用,AutoJS 的选择器基本就能定位到控件。第二层是 JavaScript 运行时和封装 API,launchAppcurrentPackagedevicefiles这些全局对象由 AutoJS 提供,脚本只负责编排调用顺序。第三层是调度,AutoJS 自带的定时任务、setInterval,以及外部ql.js入口,本质都是在不同时间点拉起同一批业务函数。

这三层决定了脚本的能力边界:它擅长找按钮、填输入框、模拟滑动;它不擅长判断图片内容,也不能脱离无障碍服务工作。排错时可以顺着这条链走——脚本说找不到控件,先去看系统是否关闭了 AutoJS 的无障碍授权,再去看目标页面是否真的渲染出了对应节点。

2.2 选择器 API 的优先级:text、desc、className 怎么取舍

分析这类项目时,第一步总是先搜findOnefindOnce的调用。两者有一个容易被忽略的差别:无参数的findOne()会一直阻塞等待,findOne(3000)才会在超时后返回 null;findOnce()则立即返回,适合做页面状态的瞬时判断。

方式返回类型阻塞行为推荐用途
findOne()UiObject不传毫秒则无限等待主流程里的强依赖控件
findOne(timeout)UiObject/null最多等 timeout 毫秒日常脚本里最常用,必须给超时上限
findOnce()UiObject/null立即返回判断页面是否具备某个特征
find()UiCollection持续监听,可配untilFind()需要批量处理的控件集合

选择器本身的匹配优先级,一般按这个顺序尝试:text精确文本、textContains包含文本、desc语义描述、最后才是className加属性过滤。前两者可读性最好,App 改版时一眼能看出哪一条失效;desc多出现在图片按钮和 WebView 内部控件上;className太宽泛,同一屏里往往命中几十个节点,只适合当兜底。

// 四种定位方式按优先级依次尝试,命中即返回 function pickNode(selectorList, timeout) { for (let i = 0; i < selectorList.length; i++) { let node = selectorList[i].findOnce(timeout); if (node) return node; } return null; } var target = pickNode([ text("立即签到"), textContains("签到"), desc("签到"), className("android.view.View").clickable(true).depth(5) ], 3000);

这段代码给一串选择器排队,先命中就直接返回;timeout是单次findOnce的等待上限,不是整个数组的总上限。写在最前面的text("立即签到")匹配最精确,代价是文案稍微改动就会失效;textContains("签到")用包含关系扩大容错;最后的className组合虽然抗文案变化,却可能命中页面里其他可点击区域,误触风险最高,所以永远放在最后一位。

2.3 坐标点击和控件点击的边界

坐标点击的写法很短,click(540, 1200)一行就够,但它把脚本和屏幕像素绑死。AutoJS 提供setScreenMetrics(1080, 2400)可以按设计分辨率做等比换算,可是 Android 的全面屏手势、底部导航栏高度、系统字体缩放都会造成偏移。控件点击基于 View 树,和屏幕尺寸无关,这是签到脚本普遍选择节点查找而不是坐标的根本原因。

不过控件方式也有明确边界。应用内用 Canvas 自绘按钮,或者页面整体跑在 WebView 里且前端没有暴露可访问性语义时,节点树里找不到可操作对象。这类场景只能退回图片识别或坐标,并且必须接受一个事实:稳定性和适配成本都会高一个量级。判断标准很直接——uiautomator dump导出的 XML 里能看到目标控件,就走节点方案;看不到,才考虑坐标。

3. 从 ZIP 到可运行工程:文件布局、入口脚本与通知链路

3.1 解压后先判断项目结构再动手

拿到 zip 压缩包,很多人第一反应是找密码破解工具,或者直接把ym_jd.js塞进 AutoJS 运行。实际更稳妥的顺序是先验证文件完整性,再观察目录结构。先用解压软件看一下压缩包是否带加密标记、文件列表是否完整,尤其注意README.md.idea这些辅助文件是否齐全——它们往往记录了项目原本的运行方式和 IDE 工程配置。一个缺了.idea但脚本完整的包通常没问题,一个连README.md都没有的包就要多留一个心眼。

解压后重点看这几类文件的分布:

文件角色推断运行时的作用
bienaoccc_hym-main仓库解压出来的根目录所有脚本的统一引用路径
ym_*.js平台签到主脚本AutoJS 直接执行的对象
sendNotify.py通知脚本的 Python 版设备装了 Python 时优先使用
sendNotify.js通知脚本的 JS 版纯 AutoJS 环境也能推送状态
ql.js青龙面板适配入口让服务端任务系统能拉起脚本
.idea/git_toolbox_prj.xmlJetBrains IDE 工程配置说明原项目在桌面 IDE 中维护
ym_lb_elm.shShell 启动脚本通过 adb 或终端触发任务

目录里同名文件出现多份并不奇怪,sendNotify.jssendNotify.py共存,通常是同一份逻辑在不同运行环境下的双实现。AutoJS 的 JavaScript 引擎可以直接跑 JS 版通知模块,设备没装 Python 时也能走完整链路;ym_lb_elm.sh则是给 adb 远程调用准备的入口,里面一般就是一行am start或者调用 AutoJS 主程序的命令。

3.2 ql.js:AutoJS 与服务端调度之间的适配层

青龙面板这类任务系统跑在 Linux 服务端,没法直接调用 AutoJS 的无障碍服务。ql.js在这里起的是协议翻译作用。常见做法有两种:一种是把 AutoJS 脚本改造成可导出模块,青龙通过 HTTP 或 adb 远程触发设备端 AutoJS 主程序执行;另一种是脚本同时维护一条“纯服务端分支”,不依赖 UI,直接调目标平台接口,把整个流程从设备端搬到服务端。第二种更稳定,但需要逆向接口签名,维护成本高得多。zip 里的ym_*.js仍以控件操作为主,说明第一类是主线。

// ql.js 的兼容层写法:由青龙定时调用,再由它串起各平台任务 const taskMap = { jd: require("./ym_jd.js"), meituan: require("./ym_meituan.js"), elm: require("./ym_elm.js"), }; exports.start = function (platform) { if (platform) { let task = taskMap[platform]; if (task && typeof task.run === "function") { return task.run(); } console.log("unsupported platform: " + platform); return; } // 未指定平台时,串行跑一遍主任务 Object.keys(taskMap).forEach((key) => { try { taskMap[key].run(); } catch (e) { console.error(key + " failed: " + e.message); } }); };

这里的关键约束有两个:所有ym_*.js都要导出run()方法,接口统一才好编排;forEach里用try/catch包住每次调用,一个平台挂了不能阻断后续任务。青龙面板只需在 cron 配置里执行node ql.js startnode ql.js start jd,就能选择全量跑还是只跑某平台。

3.3 sendNotify.py:任务状态怎么抵达手机

sendNotify.py的价值在于让脚本结束后的状态可感知。脚本在设备上跑,人不可能一直盯着屏幕,通知才是闭环。Python 版通知脚本一般读取命令行参数,再通过 Bark、Server酱、Telegram Bot 等渠道推送。

import sys import json import requests def send_msg(payload): # payload 里包含 title/content 两个字段 resp = requests.post( "https://api.day.app/YOUR_BARK_KEY", json={ "title": payload.get("title", "auto task"), "body": payload.get("content", ""), }, timeout=10, ) return resp.status_code == 200 if __name__ == "__main__": data = json.loads(sys.argv[1]) ok = send_msg(data) print("push ok" if ok else "push fail") sys.exit(0 if ok else 1)

这个脚本用sys.argv[1]接收 AutoJS 传过来的 JSON 字符串,requests.post调 Bark 的接口完成推送,返回码 0 或 1 让调用方知道推送是否成功。AutoJS 侧调用时要注意 shell 转义,中文和引号最好先encodeURIComponent再拼接,否则 Python 拿到的参数会被截断。

4. 签到脚本的通用骨架:唤醒、定位与异常分支

4.1 一个最小可运行的签到流程

梳理这类项目时会发现,ym_*.js尽管平台不同,主体流程高度一致,差异只在启动方式、签到按钮文案和弹窗特征。把流程提炼出来就是一个固定骨架,以下写法可以套用到大多数签到场景。

auto.waitFor(); setScreenMetrics(1080, 2400); const APP_NAME = "京东"; const FIND_TIMEOUT = 5000; // 亮屏解锁,避免黑屏状态下操作 if (!device.isScreenOn()) { device.wakeUp(); sleep(1200); } // 按应用名拉起目标 App,AutoJS 会做包名匹配 launchApp(APP_NAME); sleep(2000); // 文本和语义描述双通道找签到入口 let node = textContains("签到").findOnce(FIND_TIMEOUT) || descContains("签到").findOnce(FIND_TIMEOUT); if (node) { node.click(); console.log("sign ok"); } else { console.error("sign button not found"); }

代码按顺序做了四件事:等待无障碍服务、唤醒屏幕、启动 App、查找签到控件并点击。device.wakeUp()处理锁屏状态下脚本空跑的问题,因为无障碍事件在息屏时不可靠。textContains("签到").findOnce(FIND_TIMEOUT)给单次查找定下 5 秒上限,||短路保证文本匹配失败才走 desc 通道,不会两个通道各点击一次。

4.2 等页面稳定,而不是盲目叠加固定 sleep

新手容易写出一堆sleep(3000),这在页面加载快时浪费大量时间,在页面加载慢时又起不到保护作用。更可控的写法是轮询等待某个标志性控件出现,超时才放弃。

// 循环轮询直到目标节点出现,或超过最大等待时间 function waitForNode(selector, maxWait) { let waited = 0; while (waited <= maxWait) { let n = selector.findOnce(); if (n) return n; sleep(500); waited += 500; } return null; } let entry = waitForNode(textContains("领取奖励"), 20000); if (!entry) { console.error("timeout waiting for page"); }

waitForNode每 500 毫秒检查一次,和一次性findOne(20000)相比,它可以在等待期间插入状态输出,也方便在条件变化时提前退出。实际使用中,最大等待时间按页面复杂度配置:纯原生页面 5 秒足够,WebView 或启动广告较多的页面再放宽到 20 秒。

4.3 两类高频异常分支:升级弹窗和登录态失效

应用升级引导和登录过期是打断脚本流程最多的两类异常。处理思路分两步:在每步关键操作前检查这两个特征控件,发现后要么点关闭、要么终止任务,不能带着异常状态继续往下走。

function checkBlockers() { // 升级弹窗通常有“稍后”或关闭按钮 let upgrade = textContains("立即升级").findOnce() || textContains("稍后再说").findOnce(); if (upgrade) { textContains("稍后再说").click(); return true; } // 登录态失效时,继续点击只会触发登录页 let login = textContains("立即登录").findOnce() || textContains("登录已过期").findOnce(); if (login) { console.error("login expired, abort task"); return true; } return false; }

这个函数返回“当前页面有没有需要停下来的异常”。上层循环每轮操作后调用一次,返回 true 就重试或退出。注意textContains匹配比较宽泛,如果目标页面本身常有“登录”二字,就要改用“立即登录”“登录已过期”这种更精确的词,避免误伤正常流程。

5. 做长期任务时怎么保持稳定:随机延时、退避重试与串行纪律

5.1 随机间隔参数怎么设计

固定间隔的自动化操作,在风控系统眼里有明显的节奏特征。AutoJS 的random(a, b)用来生成区间随机数,配合sleep就能把操作间隔打散。

function humanPause() { // 800~2500ms 随机停顿,模拟人工操作节奏 let pause = random(800, 2500); console.log("human pause " + pause + "ms"); sleep(pause); }

区间设计有一个常见误区:把范围拉得越大越安全,实际影响是单次任务时长无法预估,长时间无法完成反而显得异常。常用的区间是 800ms 到 3000ms;在关键提交动作之前,再额外加一次 1.5 到 2 秒的停顿,因为人手在关键操作前确实会犹豫片刻。随机延时并不会让你完全免于风控,真正的作用是让行为特征不至于一眼被识别为程序。

5.2 指数退避:失败时不要立刻猛敲

控件找不到后立刻重试三次,每次都间隔 1 秒,这是脚本里最常见的写法。但连续快速重试恰好是自动化脚本最明显的特征之一。合理的做法是按指数增长间隔退避,同时给重试次数封顶。

function runWithRetry(taskFn, maxTimes) { let lastError = null; for (let i = 1; i <= maxTimes; i++) { try { let result = taskFn(); if (result) return result; } catch (e) { lastError = e; console.warn("attempt " + i + " failed: " + e.message); } // 退避间隔按 2 的幂增长,最多停 10 秒 let backoff = Math.min(1000 * Math.pow(2, i - 1), 10000); sleep(backoff); } throw lastError || new Error("retry exhausted"); }

这个函数值得强调的不是退避本身,而是封顶逻辑。Math.min(..., 10000)把单次等待限制在 10 秒以内,避免脚本在无人值守时长时间停在 sleep 里。taskFn()返回 true 才认为成功,否则继续下一轮。指数退避配合 3 次上限,对偶发的页面加载延迟足够宽容,又不会在 App 真正出问题时浪费过多时间。

5.3 串行为主,少开并行

AutoJS 的threads.start()能开多线程,但这不意味任务应该用并发跑。一个 App 只有一个前台界面,两个线程同时找“签到”控件,先到先得,后到的要么找不到节点、要么点错位置。所以多个平台任务之间默认用串行队列。

const tasks = ["./ym_jd.js", "./ym_meituan.js", "./ym_elm.js"]; for (const path of tasks) { const mod = require(path); console.log("start " + path); try { mod.run(); } catch (e) { console.error(path + " aborted: " + e); } humanPause(); } console.log("queue finished");

串行的好处除了避免界面竞争,还让每次任务的结果天然可追踪——上一个任务结束,下一个才启动。如果设备资源充足,可以考虑把不同平台拆到多台设备上并行,每台设备只跑一个任务,既避开单设备并发问题,又能摊平总耗时。

6. 收编进定时任务前的三个工程化细节

6.1 cron 表达式不要整点扎堆

把脚本交给青龙面板或 Linux crontab 之后,执行时间要刻意错峰。多个任务同时启动,即使运行在不同 App 上,也会因为设备同一时间只有一个前台窗口而产生队列等待。cron 里给每个任务分配不同分钟段,比全部安排在 8:00 更合理。

# 京东签到定在 8 点 10 分 10 8 * * * node /data/scripts/ql.js start jd # 美团签到比 jd 晚 15 分钟 25 8 * * * node /data/scripts/ql.js start meituan

错峰设置还要考虑任务执行时长。如果一个任务可能耗时 20 分钟,下一个任务的触发时间至少要比它晚半个小时,否则队列会被上一个任务阻塞。

6.2 账号配置从脚本里拆出去

直接写在ym_*.js里的账号信息,会让脚本没法分享、也没法多账号复用。统一改成从外部 JSON 读取,脚本本身保持无状态。

[ {"platform": "jd", "username": "u1", "password": "p1"}, {"platform": "meituan", "username": "u2", "password": "p2"} ]

对应在 AutoJS 里就是文件系统读取加 JSON 解析,运行时按当前任务名选出对应账号。改配置时只动 JSON,不动代码,出问题也好定位。

6.3 加一个空跑校验入口,用最小成本抓住版本更新问题

最后一个值得落进代码的技巧是 dry-run 模式。脚本通过标记文件触发后,只做控件探测、不做点击,把“选择器还能不能匹配到目标节点”作为输出。App 每次发版改掉按钮文案之后,第一个报警的一定是这个自检项。

// 存在 dry_run 标记文件时,只探测控件不执行点击 if (files.exists("/sdcard/scripts/.dry_run")) { let found = textContains("签到").findOnce(3000); console.log(found ? "check ok" : "check lost"); files.remove("/sdcard/scripts/.dry_run"); exit(); }

这个方式把“失效”和“出错”分开:选择器失效不等于任务失败,但它预示后续正常执行都会失败。定时任务可以先写标记文件、再跑脚本,得到 check lost 告警时人工介入。这个检查逻辑不依赖外部服务,改动量很小,配合青龙面板的 cron 任务,就能在 App 发版后第一时间收到选择器失配的通知,而不是等用户手动打开脚本跑一遍才发现。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询