贝塞尔曲线滑动的实际演示
贝塞尔曲线滑动的实际演示// =====================================================================
//例子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 = ,例如
function tapAreaPct(areaPct) {
var w = sw(), h = sh();
var x1 = Math.round(areaPct * w), y1 = Math.round(areaPct * h);
var x2 = Math.round(areaPct * w), y2 = Math.round(areaPct * 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 === "object") ? JSON.stringify(list) : String(list);
} 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) < 0) continue; // 页面里没这个词,跳过
// 通道1:节点(先验明节点真身再点)
var n = null;
try { n = auto.nodeSearch(300).text(POPUP_WORDS).getNode(0); } catch (e) { n = null; }
if (n) {
var txtN = "";
try { txtN = String(n.getText() || ""); } catch (e1) { txtN = ""; }
if (txtN.indexOf(POPUP_WORDS) >= 0) {
var clicked = false;
try { n.clickPoint(); clicked = true; } catch (e2) {
try { n.click(); clicked = true; } catch (e3) {}
}
if (clicked) {
log("关弹窗(节点): " + POPUP_WORDS);
sleep.millisecond(1000);
return true;
}
}
}
// 通道2:OCR
try {
var p = ocr.findTapPoint(POPUP_WORDS);
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);
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();
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();
页:
[1]