YYPOST群发软件 发表于 2026-9-10 06:37:58

贝塞尔曲线滑动的实际演示

贝塞尔曲线滑动的实际演示








// =====================================================================
//例子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]
查看完整版本: 贝塞尔曲线滑动的实际演示