 | |  |  | 贝塞尔曲线滑动的实际演示
- // =====================================================================
- // 例子1:贝塞尔曲线滑动的实际演示(真机可直接运行)
- // 作用:把"机器味"很重的直上直下滑动,改成真人手感的曲线滑动,
- // 并加入随机延迟、随机偏移、随机回滑、随机停留等防检测细节。
- //
- // ★ 曲线 vs 单次直滑,用错就翻不动页:
- // · 列表页(设置/图文/小说/商品列表)→ 用曲线,效果好,像人慢慢滚
- // · 短视频信息流(快手/抖音) → 必须用【一次完整的快速长滑】,
- // 曲线会被拆成十几个小手势,画面只抖不翻页(已实测踩坑)
- // 本文件两套都实现了,改 CFG.feedType 即可切换:list / video
- // 引擎:ES5 / Rhino(不要用 let const 箭头函数 模板字符串)
- // =====================================================================
- // ---------------- 运行统计(结束时汇总打印)----------------
- var STAT = { ok: 0, retry: 0, back: 0, stay: 0, popup: 0, startMs: 0 };
- // ---------------- 一、配置区(改这里就行)----------------
- var CFG = {
- // 就用快手,不折腾
- targetPkg: "com.smile.gifmaker",
- loopCount: 8, // 演示滑动多少轮
- swipeMinMs: 420, // 列表页单次滑动最快耗时
- swipeMaxMs: 900, // 列表页单次滑动最慢耗时
- stayMinMs: 1200, // 每屏停留最短
- stayMaxMs: 3500, // 每屏停留最长
- // feedType:list =列表页(设置/图文),用贝塞尔曲线滑动;
- // video=短视频信息流(快手/抖音),用一次完整的快速长滑翻页。
- feedType: "video",
- videoSwipeMinMs: 220, // 短视频单次翻页最快(ms):太快不翻,太慢像拖拽
- videoSwipeMaxMs: 380, // 短视频单次翻页最慢(ms)
- verifySwipe: true, // 翻页后校验画面是否变化,没变就补一次强滑
- curveEnable: true, // 列表模式下的曲线滑动开关
- curveSteps: 14, // 曲线拆成多少小段(越多越圆滑,越慢)
- offsetPx: 18, // 点击时的随机抖动半径(像素)
- backSwipeRate: 25, // 百分之多少的概率"往回滑一下"(真人常有的动作)
- microPauseRate: 35, // 百分之多少的概率中途"手指停一下"
- clearPopupEnable: true, // 刷的过程中顺带关弹窗(青少年模式/升级/权限)
- tapBackEnable: false, // 视频流不按返回(会退出快手)
- randomTapEnable: false, // 视频流不乱点(会误触点赞/评论/进主页)
- finishGoHome: false, // 跑完停在快手,不回桌面
- logToFile: true, // 是否把动作写日志文件
- logPath: "/sdcard/AIWROK/技巧1_曲线滑动日志.txt"
- };
- // ---------------- 二、基础工具 ----------------
- var LOG = [];
- function log(s) {
- var line = "[" + nowStr() + "] " + s;
- try { printl(line); } catch (e) {}
- LOG.push(line);
- if (CFG.logToFile && LOG.length >= 10) flushLog();
- }
- function flushLog() {
- try {
- var txtAll = LOG.join("\n") + "\n";
- LOG = [];
- file.createFloder(parentOf(CFG.logPath));
- txt.writeTxt(CFG.logPath, txtAll, true); // 第三个参数 true = 追加
- } catch (e) {}
- }
- function parentOf(p) {
- var i = p.lastIndexOf("/");
- return i > 0 ? p.substring(0, i) : p;
- }
- function nowStr() {
- try { return time.now(); } catch (e) {}
- var d = new Date();
- return d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
- }
- function rand(min, max) {
- return Math.floor(min + Math.random() * (max - min + 1));
- }
- function rSleep(min, max) {
- sleep.millisecond(rand(min, max));
- }
- // 屏幕宽高(带兜底,某些机型取不到就当 1080x2400)
- function sw() {
- try { return screen.getScreenWidth(); } catch (e) {}
- try { return device.getScreenWidth(); } catch (e) {}
- return 1080;
- }
- function sh() {
- try { return screen.getScreenHeight(); } catch (e) {}
- try { return device.getScreenHeight(); } catch (e) {}
- return 2400;
- }
- // 安全执行:任何一步炸了都不影响主流程(真机必备)
- function safe(fn, tag) {
- try { return fn(); } catch (e) {
- log("!! " + (tag || "未知步骤") + " 异常: " + (e.message || e));
- return null;
- }
- }
- // ---------------- 三、技巧1:贝塞尔曲线滑动 ----------------
- // 直上直下的 swipe 是最容易被识别的特征,用二次贝塞尔让轨迹带弧度
- function bezierSwipe(x1, y1, x2, y2, durMs, steps) {
- steps = steps || CFG.curveSteps;
- // 控制点:取中点 + 随机横向偏移,让每一刀的弧度都不一样
- var cx = (x1 + x2) / 2 + rand(-60, 60);
- var cy = (y1 + y2) / 2;
- var prevX = x1, prevY = y1;
- var segMs = Math.max(16, Math.round(durMs / steps));
- for (var i = 1; i <= steps; i++) {
- var t = i / steps;
- var mt = 1 - t;
- // 二次贝塞尔公式
- var bx = mt * mt * x1 + 2 * mt * t * cx + t * t * x2;
- var by = mt * mt * y1 + 2 * mt * t * cy + t * t * y2;
- // 每一小段都加一点手抖
- bx += rand(-2, 2);
- by += rand(-2, 2);
- try {
- auto.swip(prevX, prevY, bx, by, segMs, 0);
- } catch (e) {
- try { auto.swipe(prevX, prevY, bx, by, segMs); } catch (e2) {}
- }
- prevX = bx; prevY = by;
- // 技巧:中途随机"手指顿一下",更像真人在看内容
- if (CFG.microPauseRate > 0 && rand(1, 100) <= CFG.microPauseRate && i < steps - 1) {
- sleep.millisecond(rand(40, 160));
- }
- }
- }
- function lineSwipe(x1, y1, x2, y2, durMs) {
- try { auto.swip(x1, y1, x2, y2, durMs, 0); }
- catch (e) { try { auto.swipe(x1, y1, x2, y2, durMs); } catch (e2) {} }
- }
- // !!!关键:短视频翻页必须用【一次完整的快速长滑】!!!
- // 拆成十几段小滑动(曲线),快手会把每段当成独立的微小手势 → 画面抖动但不翻页
- function onceSwipe(x1, y1, x2, y2, durMs) {
- try { auto.swip(x1, y1, x2, y2, durMs, 0); }
- catch (e) { try { auto.swipe(x1, y1, x2, y2, durMs); } catch (e2) {} }
- }
- // 向上翻一屏(信息流最常用)
- function swipeUpFeed() {
- var w = sw(), h = sh();
- var x1 = rand(Math.round(w * 0.35), Math.round(w * 0.65));
- if (CFG.feedType === "video") {
- // 短视频模式:起点靠下、终点靠上、距离长、速度快,一次搞定
- var vy1 = Math.round(h * (0.78 + Math.random() * 0.07)); // 0.78~0.85
- var vy2 = Math.round(h * (0.12 + Math.random() * 0.08)); // 0.12~0.20
- var vdur = rand(CFG.videoSwipeMinMs, CFG.videoSwipeMaxMs); // 220~380ms
- onceSwipe(x1, vy1, x1 + rand(-12, 12), vy2, vdur);
- log("翻页上滑(单次) 起点y=" + vy1 + " 终点y=" + vy2 + " 距离" + (vy1 - vy2) + "px x=" + x1 + " 耗时" + vdur + "ms");
- return true;
- }
- // 列表模式:曲线滑动(设置页/图文列表这类才用得上)
- var y1 = rand(Math.round(h * 0.72), Math.round(h * 0.85));
- var x2 = x1 + rand(-70, 70);
- var y2 = rand(Math.round(h * 0.18), Math.round(h * 0.35));
- var dur = rand(CFG.swipeMinMs, CFG.swipeMaxMs);
- if (CFG.curveEnable) bezierSwipe(x1, y1, x2, y2, dur, CFG.curveSteps);
- else lineSwipe(x1, y1, x2, y2, dur);
- log("上滑(列表) (" + Math.round(x1) + "," + y1 + ")->(" + Math.round(x2) + "," + y2 + ") " + dur + "ms");
- return true;
- }
- // 向下回滑(真人看完会往回翻,视频流里 = 翻回上一个视频)
- function swipeDownFeed() {
- var w = sw(), h = sh();
- var x1 = rand(Math.round(w * 0.35), Math.round(w * 0.65));
- if (CFG.feedType === "video") {
- var vy1 = Math.round(h * (0.15 + Math.random() * 0.08));
- var vy2 = Math.round(h * (0.80 + Math.random() * 0.06));
- var vdur = rand(CFG.videoSwipeMinMs, CFG.videoSwipeMaxMs);
- onceSwipe(x1, vy1, x1 + rand(-12, 12), vy2, vdur);
- STAT.back++;
- log("回滑(单次) 上一个视频 (" + x1 + "," + vy1 + ")->(" + vy2 + ") " + vdur + "ms [累计回滑" + STAT.back + "次]");
- return true;
- }
- var y1 = rand(Math.round(h * 0.22), Math.round(h * 0.35));
- var x2 = x1 + rand(-50, 50);
- var y2 = rand(Math.round(h * 0.70), Math.round(h * 0.82));
- var dur = rand(CFG.swipeMinMs, CFG.swipeMaxMs);
- if (CFG.curveEnable) bezierSwipe(x1, y1, x2, y2, dur, CFG.curveSteps);
- else lineSwipe(x1, y1, x2, y2, dur);
- log("回滑一下(拟人)");
- return true;
- }
- // 慢慢"看内容"的小幅滑动(阅读类任务用)
- function slowReadSwipe() {
- if (CFG.feedType === "video") {
- // 视频流没有"慢速阅读",改成老老实实看一会儿(停留就是最好的拟人)
- var s = rand(1500, 3500);
- STAT.stay++;
- log("停留观看 " + s + "ms [累计停留" + STAT.stay + "次]");
- sleep.millisecond(s);
- return;
- }
- var w = sw(), h = sh();
- var x = rand(Math.round(w * 0.4), Math.round(w * 0.6));
- var y1 = Math.round(h * 0.78);
- var y2 = Math.round(h * 0.48);
- if (CFG.curveEnable) bezierSwipe(x, y1, x, y2, rand(1200, 2000), 20);
- else lineSwipe(x, y1, x, y2, 1500);
- log("慢速阅读滑动");
- }
- // ---------------- 翻页校验:滑了没换视频,就补一次更强的滑 ----------------
- function shotFingerprint() {
- try {
- var b = screen.screenShotFull().toBase64();
- if (!b) return "";
- // 取头尾各 200 字符做指纹,够用且快
- return b.substring(0, 200) + "|" + b.substring(b.length - 200) + "|" + b.length;
- } catch (e) { return ""; }
- }
- // 返回 true = 确认翻页成功
- function swipeUpWithVerify() {
- var before = shotFingerprint();
- swipeUpFeed();
- sleep.millisecond(700); // 等视频切换动画
- var after = shotFingerprint();
- if (before && after && before === after) {
- STAT.retry++;
- log("× 画面无变化(没翻页),补一次强滑 0.88h→0.08h 250ms");
- var w = sw(), h = sh();
- onceSwipe(Math.round(w / 2), Math.round(h * 0.88), Math.round(w / 2), Math.round(h * 0.08), 250);
- sleep.millisecond(900);
- var after2 = shotFingerprint();
- if (after2 && after2 !== before) {
- STAT.ok++;
- log("√ 补滑成功,已切到新视频");
- return true;
- }
- log("× 补滑后仍未变化,可能被弹窗/直播页挡住");
- return false;
- }
- STAT.ok++;
- log("√ 翻页成功,画面已变化(本轮第 " + STAT.ok + " 次有效翻页)");
- return true;
- }
- // ---------------- 四、技巧2:随机偏移点击 ----------------
- // 永远点同一个像素 = 一眼假的机器人,点的时候给个抖动
- function tapRandom(cx, cy, offset) {
- offset = (offset === undefined) ? CFG.offsetPx : offset;
- var x = cx + rand(-offset, offset);
- var y = cy + rand(-offset, offset);
- if (x < 0) x = 0; if (y < 0) y = 0;
- if (x > sw()) x = sw(); if (y > sh()) y = sh();
- try { auto.clickPoint(x, y); }
- catch (e) { try { device.clickPoint(x, y); } catch (e2) {} }
- log("点击 (" + x + "," + y + ") 抖动±" + offset);
- return { x: x, y: y };
- }
- // 在矩形区域内随机点(百分比写法,不挑分辨率)
- // areaPct = [x1%, y1%, x2%, y2%],例如 [0.1,0.2,0.9,0.6]
- function tapAreaPct(areaPct) {
- var w = sw(), h = sh();
- var x1 = Math.round(areaPct[0] * w), y1 = Math.round(areaPct[1] * h);
- var x2 = Math.round(areaPct[2] * w), y2 = Math.round(areaPct[3] * h);
- // 抖动上限 25px:区域越大不代表要抖越狠,抖太大会误触到旁边的按钮(点赞/评论/头像)
- var off = Math.min(25, Math.max(8, (x2 - x1) / 4));
- return tapRandom(Math.round((x1 + x2) / 2), Math.round((y1 + y2) / 2), off);
- }
- // ---------------- 五、技巧3:长按 + 轻点 ----------------
- function longTap(x, y, ms) {
- ms = ms || 800;
- try { auto.longClickPoint(x, y, ms); }
- catch (e) {
- // 没有 longClick 接口时,用"按下-停留-抬起"手势兜底
- try {
- auto.swip(x, y, x + rand(-2, 2), y + rand(-2, 2), ms, 0);
- } catch (e2) {}
- }
- log("长按 (" + x + "," + y + ") " + ms + "ms");
- }
- // ---------------- 六、前台判断(多通道,实测有的设备 getTopPackName 返回空)----------------
- function topPkg() {
- try { var t1 = app.getTopPackName(); if (t1) return t1; } catch (e) {}
- try { var t2 = app.getTopPackNameV2(); if (t2) return t2; } catch (e) {}
- try { var t3 = auto.getTopPackName(); if (t3) return t3; } catch (e) {}
- // 兜底:从当前 Activity 里截包名(形如 com.smile.gifmaker/xxx.Activity)
- var act = "";
- try { act = "" + app.getCurrentActivity(); } catch (e) {
- try { act = "" + app.getAgentCurrenAcitvity(); } catch (e2) { act = ""; }
- }
- if (act && act.indexOf("/") > 0) return act.substring(0, act.indexOf("/"));
- return act || "";
- }
- function isInstalled(pkg) {
- // 方式1(最可靠):能查到版本号/应用名 = 装了
- try { var v = app.getVerName(pkg); if (v) return true; } catch (e) {}
- try { var n = app.getAppName(pkg); if (n) return true; } catch (e) {}
- // 方式2:getAllApp 返回格式不定(字符串或对象),统一序列化后匹配
- try {
- var list = app.getAllApp();
- for (var i = 0; i < list.length; i++) {
- var s = "";
- try {
- s = (typeof list[i] === "object") ? JSON.stringify(list[i]) : String(list[i]);
- } catch (e1) { s = ""; }
- if (s.indexOf(pkg) >= 0) return true;
- }
- } catch (e) {}
- return false; // 注意:检测API不全时可能误报,openTarget 里不做硬回退
- }
- // 启动后/刷的过程中的弹窗兜底(快手常见:青少年模式、升级、权限)
- // 只放"点了绝对安全"的词,不放"关闭/确定"防止误点业务按钮
- var POPUP_WORDS = ["我知道了", "以后再说", "下次再说", "暂不更新", "暂不升级",
- "跳过", "稍后再说", "拒绝", "不允许"];
- // 取整页文本(一次调用拿到全部文字,避免对词库逐个 nodeSearch 盲扫刷警告)
- function pageText() {
- var t = "";
- try { t = String(auto.getXml() || ""); } catch (e) { t = ""; }
- if (!t || t.length < 20) {
- try { t = String(ocr.getAllString() || ""); } catch (e) { t = ""; }
- }
- return t;
- }
- function killPopupOnce() {
- if (!CFG.clearPopupEnable) return false;
- // 关键:先确认页面上真的有弹窗词,再去查找节点。
- // 否则找不到元素时引擎会打印"getNode错误元素0未找到"警告,刷满日志。
- var page = pageText();
- if (!page) return false; // 取不到页面信息就放弃本轮杀弹窗,不硬扫
- for (var i = 0; i < POPUP_WORDS.length; i++) {
- if (page.indexOf(POPUP_WORDS[i]) < 0) continue; // 页面里没这个词,跳过
- // 通道1:节点(先验明节点真身再点)
- var n = null;
- try { n = auto.nodeSearch(300).text(POPUP_WORDS[i]).getNode(0); } catch (e) { n = null; }
- if (n) {
- var txtN = "";
- try { txtN = String(n.getText() || ""); } catch (e1) { txtN = ""; }
- if (txtN.indexOf(POPUP_WORDS[i]) >= 0) {
- var clicked = false;
- try { n.clickPoint(); clicked = true; } catch (e2) {
- try { n.click(); clicked = true; } catch (e3) {}
- }
- if (clicked) {
- log("关弹窗(节点): " + POPUP_WORDS[i]);
- sleep.millisecond(1000);
- return true;
- }
- }
- }
- // 通道2:OCR
- try {
- var p = ocr.findTapPoint(POPUP_WORDS[i]);
- if (p && typeof p.x !== "undefined" && p.x > 0) {
- auto.clickPoint(p.x + rand(-2, 2), p.y + rand(-2, 2));
- log("关弹窗(OCR): " + POPUP_WORDS[i]);
- sleep.millisecond(1000);
- return true;
- }
- } catch (e) {}
- }
- return false;
- }
- // ---------------- 七、启动目标APP ----------------
- function openTarget() {
- // 打印检测结果,方便核对(应用名/版本能取到就是装了)
- var vName = "", aName = "";
- try { vName = app.getVerName(CFG.targetPkg); } catch (e) {}
- try { aName = app.getAppName(CFG.targetPkg); } catch (e) {}
- log("安装检测: " + CFG.targetPkg + " 应用名=" + (aName || "取不到") + " 版本=" + (vName || "取不到"));
- if (!isInstalled(CFG.targetPkg)) {
- // 只提示,不回退——安装检测API在部分设备上不全,误报率高于漏报率
- log("注意:安装检测未确认(可能误报),仍直接尝试启动");
- }
- log("启动 " + CFG.targetPkg);
- try { app.openApp(CFG.targetPkg); } catch (e) { try { app.startApp(CFG.targetPkg); } catch (e2) {} }
- rSleep(4500, 6000);
- var top = "";
- for (var i = 0; i < 12; i++) {
- killPopupOnce(); // 首启的青少年模式/权限/升级弹窗
- top = topPkg();
- if (top === CFG.targetPkg) { log("已在前台: " + top); return true; }
- sleep.millisecond(800);
- }
- // 部分设备的前台API一律返回空(非脚本问题),此时再补一次启动兜底,然后照常往下走
- if (!top) {
- log("提示:本机前台API取不到包名(属设备兼容问题,不影响滑动)");
- try { app.openApp(CFG.targetPkg); } catch (e) { try { app.startApp(CFG.targetPkg); } catch (e2) {} }
- sleep.millisecond(3000);
- log("已补启动一次,继续滑动");
- } else {
- log("当前前台=" + top + ",目标=" + CFG.targetPkg + "(继续滑动,不影响)");
- }
- return false;
- }
- // ---------------- 七、主流程 ----------------
- function main() {
- log("===== 例子1 贝塞尔曲线滑动演示 开始(模式:" + CFG.feedType + ")=====");
- STAT.startMs = new Date().getTime();
- var w = sw(), h = sh();
- log("分辨率: " + w + " x " + h);
- // 开场信息:设备、电量、模式、本轮参数
- try { log("设备: " + device.getModel() + " / 系统 " + device.getVersion()); } catch (e) {}
- try { log("电量: " + device.getBatteryLevel() + "% 充电中: " + device.isCharging()); } catch (e) {}
- log("运行参数: 轮数=" + CFG.loopCount + " 模式=" + CFG.feedType +
- " 翻页耗时=" + CFG.videoSwipeMinMs + "~" + CFG.videoSwipeMaxMs + "ms" +
- " 停留=" + CFG.stayMinMs + "~" + CFG.stayMaxMs + "ms" +
- " 回滑概率=" + CFG.backSwipeRate + "%");
- openTarget();
- for (var i = 1; i <= CFG.loopCount; i++) {
- var roundStart = new Date().getTime();
- log("---- 第 " + i + " / " + CFG.loopCount + " 轮(进度 " +
- Math.round(i * 100 / CFG.loopCount) + "%)----");
- // 0) 每轮先顺手清一下弹窗(青少年模式/升级随时会蹦)
- if (killPopupOnce()) {
- STAT.popup++;
- log("本轮清理了弹窗 [累计弹窗" + STAT.popup + "次]");
- }
- // 1) 主行动:上滑翻页(带校验,没翻成功就补一次)
- if (CFG.verifySwipe) swipeUpWithVerify();
- else swipeUpFeed();
- // 2) 拟人停留:随机看一会儿
- var st = rand(CFG.stayMinMs, CFG.stayMaxMs);
- log("停留 " + st + "ms 观看");
- sleep.millisecond(st);
- // 3) 概率性回滑:真人不是单向下拉的
- if (rand(1, 100) <= CFG.backSwipeRate) {
- rSleep(400, 1200);
- swipeDownFeed();
- rSleep(600, 1500);
- }
- // 4) 概率性慢速阅读:偶尔停下来认真看一条
- if (rand(1, 100) <= 30) {
- slowReadSwipe();
- rSleep(1000, 2500);
- }
- // 5) 概率性随机轻点空白区:模拟误触/看图片(视频流默认关闭)
- if (CFG.randomTapEnable && rand(1, 100) <= 20) {
- tapAreaPct([0.15, 0.25, 0.85, 0.55]);
- rSleep(800, 1800);
- if (CFG.tapBackEnable) {
- try { auto.pressBack(); } catch (e) { try { device.pressBack(); } catch (e2) {} }
- }
- rSleep(600, 1200);
- }
- log("本轮耗时 " + (new Date().getTime() - roundStart) + "ms");
- // 每轮汇报一次电量,长时间跑能看出掉电速度
- if (i % 3 === 0) {
- try { log("当前电量: " + device.getBatteryLevel() + "%"); } catch (e) {}
- }
- }
- // 收尾:默认停在原地(刷视频就是让它一直待在APP里)
- if (CFG.finishGoHome) {
- safe(function () { auto.home(); }, "回桌面");
- log("已回桌面");
- } else {
- log("收尾:停在 " + CFG.targetPkg + " 不退出");
- }
- // 结束汇总报告
- var totalMs = new Date().getTime() - STAT.startMs;
- log("================ 运行汇总 ================");
- log("总耗时: " + Math.round(totalMs / 1000) + " 秒(平均单轮 " +
- Math.round(totalMs / CFG.loopCount) + "ms)");
- log("有效翻页: " + STAT.ok + " 次 补滑: " + STAT.retry + " 次");
- log("回滑: " + STAT.back + " 次 停留观看: " + STAT.stay + " 次 清理弹窗: " + STAT.popup + " 次");
- if (CFG.loopCount > 0) {
- log("翻页成功率: " + Math.round(STAT.ok * 100 / CFG.loopCount) + "%");
- }
- log("==========================================");
- flushLog();
- log("===== 例子1 结束,日志: " + CFG.logPath + " =====");
- try { toast.show("演示完成 翻页" + STAT.ok + "次"); } catch (e) {}
- }
- main();
复制代码
| |  | |  |
|