YYPOST群发软件 发表于 2026-9-11 06:46:24

日志分级落盘配置持久化任务调度


日志分级落盘配置持久化任务调度










// =====================================================================
//例子5:日志分级落盘 + 配置持久化 + 任务调度(真机可直接运行)
//作用:把脚本从"能跑"升级到"能长期跑、出问题能查"——
//      · 日志分级 + 自动分卷,跑一晚上也不会把文件撑爆
//      · 配置写文件,杀掉脚本重开还能接着上次的参数跑
//      · 定时启动、限时结束、轮次休息、优雅停止(随时刹车不丢数据)
//      · 统计上报:成功/失败计数 + 断网缓存补报
//引擎:ES5 / Rhino
// =====================================================================

// ---------------- 一、路径与配置 ----------------
var BASE = "/sdcard/AIWROK/技巧5_任务调度";
var LOG_DIR = BASE + "/日志";
var CONF_FILE = BASE + "/配置.ini";
var QUEUE_FILE = BASE + "/待上报队列.txt";
var STAT_FILE = BASE + "/统计.txt";
var STOP_FILE = "/sdcard/AIWROK/STOP_ALL";   // 建这个文件 = 优雅停止
var PAUSE_FILE = BASE + "/PAUSE";            // 建这个文件 = 暂停

var CFG = {
    // 定时任务:到点才开工(24小时制,如 "09:30");留空 = 立即开始
    startTime: "",
    // 限时:最多跑多少分钟,到点自动收工(防跑一夜)
    maxMinutes: 2,
    // 轮次:总共跑几轮,每轮之间休息多少秒
    rounds: 5,
    restMinSec: 5,
    restMaxSec: 12,
    // 日志分卷:单个日志文件超过多少行就切新文件
    logMaxLines: 200,
    // 上报地址(没有服务器就留空,脚本会自动跳过上报)
    reportUrl: "",
    deviceTag: "",
    // 目标APP:快手(刷视频)
    targetPkg: "com.smile.gifmaker",
    // 短视频翻页参数:必须一次完整快速长滑,分段曲线只会导致画面抖动不翻页
    videoSwipeMinMs: 220,
    videoSwipeMaxMs: 380,
    stayMinMs: 2000,      // 每轮看完视频停留
    stayMaxMs: 5000,
    verifySwipe: true,      // 翻页后截图比对,确认真的换了视频
    finishGoHome: false   // 跑完停在快手,不回桌面
};

function rand(a, b) { return Math.floor(a + Math.random() * (b - a + 1)); }
function rSleep(a, b) { sleep.millisecond(rand(a, b)); }
function dirOf(p) { var i = p.lastIndexOf("/"); return i > 0 ? p.substring(0, i) : p; }

// 返回键多通道:实测部分设备 auto 对象没有 pressBack
function pressBackSafe() {
    try { if (auto.back) { auto.back(); return true; } } catch (e) {}
    try { if (device.pressBack) { device.pressBack(); return true; } } catch (e) {}
    try { if (gesture.back) { gesture.back(); return true; } } catch (e) {}
    try { if (auto.pressBack) { auto.pressBack(); return true; } } catch (e) {}
    return false;
}
function ts() {
    try { return time.now(); } catch (e) {
      var d = new Date();
      return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " " + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
    }
}

// ---------------- 二、日志系统 ----------------
var LOG_BUF = [];
var LOG_LINES = 0;
var LOG_NOW = "";

function ensureDirs() {
    try { file.createFloder(BASE); } catch (e) {}
    try { file.createFloder(LOG_DIR); } catch (e) {}
}

function flushLog(force) {
    if (LOG_BUF.length === 0) return;
    if (!force && LOG_BUF.length < 5) return; // 攒够再写,减少 IO
    try {
      if (!LOG_NOW) LOG_NOW = LOG_DIR + "/run_" + new Date().getTime() + ".log";
      txt.writeTxt(LOG_NOW, LOG_BUF.join("\n") + "\n", true);
      LOG_BUF = [];
    } catch (e) {}
}

// 分级日志:D 调试 / I 信息 / W 警告 / E 错误
function logD(s) { writeLog("D", s); }
function logI(s) { writeLog("I", s); }
function logW(s) { writeLog("W", s); }
function logE(s) { writeLog("E", s); }

function writeLog(level, s) {
    var line = ts() + " [" + level + "] " + s;
    try { printl(line); } catch (e) {}
    LOG_BUF.push(line);
    LOG_LINES++;
    // 分卷:超过上限就换新文件(长期跑的必备)
    if (LOG_LINES >= CFG.logMaxLines) {
      flushLog(true);
      LOG_LINES = 0;
      LOG_NOW = LOG_DIR + "/run_" + new Date().getTime() + ".log";
      logI("日志分卷,新文件: " + LOG_NOW);
    } else {
      flushLog(false);
    }
}

// ---------------- 三、配置持久化 ----------------
// 技巧:config 读写会自动建文件;读的时候一定给默认值,防止首次运行取到空
function saveConfig() {
    try {
      config.setConfig(CONF_FILE, "rounds", "" + CFG.rounds);
      config.setConfig(CONF_FILE, "maxMinutes", "" + CFG.maxMinutes);
      config.setConfig(CONF_FILE, "startTime", CFG.startTime);
      // 注意:targetPkg 不写入配置!否则上次跑的设置页会被记住,
      // 下次把代码改成快手也会被旧配置覆盖回去(已实测踩坑)
      config.setConfig(CONF_FILE, "lastRun", ts());
      config.setConfig(CONF_FILE, "runTimes", "" + (parseInt(getConf("runTimes", "0"), 10) + 1));
      logI("配置已保存: " + CONF_FILE);
    } catch (e) { logW("配置保存失败: " + (e.message || e)); }
}

function getConf(key, defVal) {
    try {
      var v = config.getConfig(CONF_FILE, key, defVal);
      if (v === null || v === undefined || v === "") return defVal;
      return v;
    } catch (e) { return defVal; }
}

function loadConfig() {
    try {
      CFG.rounds = parseInt(getConf("rounds", "" + CFG.rounds), 10);
      CFG.maxMinutes = parseInt(getConf("maxMinutes", "" + CFG.maxMinutes), 10);
      CFG.startTime = getConf("startTime", CFG.startTime);
      // targetPkg 一律以代码里的为准,绝不被旧配置覆盖
      // (历史配置里存过 com.android.settings,导致改成快手后仍打开设置页)
      logI("配置已加载,历史运行次数=" + getConf("runTimes", "0"));
    } catch (e) {}
}

function resetConfig() {
    try { file.delete(CONF_FILE); logI("配置已重置"); } catch (e) {}
}

// ---------------- 四、优雅停止 / 暂停 ----------------
function shouldStop() { try { return file.isExists(STOP_FILE); } catch (e) { return false; } }
function shouldPause() { try { return file.isExists(PAUSE_FILE); } catch (e) { return false; } }

// 暂停时原地打转,删掉 PAUSE 文件就继续(调试脚本神器)
function waitIfPaused() {
    var guard = 0;
    while (shouldPause() && !shouldStop() && guard < 600) {
      if (guard === 0) logW("已暂停(删除 " + PAUSE_FILE + " 继续)");
      sleep.millisecond(1000);
      guard++;
    }
    if (guard > 0) logI("恢复运行");
}

// ---------------- 五、定时任务 ----------------
// 等到 startTime 才开工;返回 false 表示被停止
function waitForStartTime() {
    // 配置文件可能读回空白字符,统一清洗后再判断,避免空值误触发定时等待
    try { CFG.startTime = String(CFG.startTime || "").replace(/\s/g, ""); } catch (e) { CFG.startTime = ""; }
    if (!CFG.startTime) return true;
    logI("定时模式,等待到 " + CFG.startTime + " 开工");
    var guard = 0;
    while (guard < 3600 * 12) { // 最多等 12 小时
      if (shouldStop()) return false;
      var now = "";
      try { now = time.now(); } catch (e) { now = ""; }
      // time.now() 一般形如 2026-09-09 09:30:12,取中间 hh:mm
      var hm = now.substring(11, 16);
      if (now.indexOf(CFG.startTime) >= 0 || hm === CFG.startTime) {
            logI("到点开工");
            return true;
      }
      if (guard % 300 === 0) logD("等待中,当前 " + now);
      sleep.millisecond(1000);
      guard++;
    }
    return true;
}

// ---------------- 六、统计与上报 ----------------
var STAT = { ok: 0, fail: 0, skip: 0, startMs: 0 };

function statAdd(key) {
    if (key === "ok") STAT.ok++;
    else if (key === "fail") STAT.fail++;
    else STAT.skip++;
}

// 上报:有网直接发,没网写队列,等有网再补报(防止数据丢)
function reportLine(obj) {
    if (!CFG.reportUrl) return; // 没配地址就直接跳过
    var body = "";
    try { body = JSON.stringify(obj); } catch (e) { body = "" + obj; }
    try {
      var r = http.json(CFG.reportUrl, body);
      if (r) { logD("上报成功"); return; }
      throw new Error("空响应");
    } catch (e) {
      logW("上报失败,入队缓存: " + (e.message || e));
      try { txt.writeTxt(QUEUE_FILE, body + "\n", true); } catch (e2) {}
    }
}

// 补报队列(联网后调用一次即可)
function flushQueue() {
    if (!CFG.reportUrl) return;
    var content = "";
    try { content = txt.readFileSdcard(QUEUE_FILE); } catch (e) { return; }
    if (!content) return;
    var lines = content.split("\n");
    var left = [];
    for (var i = 0; i < lines.length; i++) {
      var ln = lines;
      if (!ln) continue;
      try {
            http.json(CFG.reportUrl, ln);
            logI("补报成功一条");
      } catch (e) {
            left.push(ln);
      }
    }
    try {
      file.delete(QUEUE_FILE);
      if (left.length > 0) txt.writeTxt(QUEUE_FILE, left.join("\n") + "\n", true);
      logI("补报完成,剩余 " + left.length + " 条");
    } catch (e) {}
}

function saveStat() {
    var s = "开始:" + ts() + " 成功:" + STAT.ok + " 失败:" + STAT.fail +
            " 跳过:" + STAT.skip + " 耗时(分):" +
            Math.round((new Date().getTime() - STAT.startMs) / 60000);
    try { txt.writeTxt(STAT_FILE, s + "\n", true); } catch (e) {}
    logI("统计: " + s);
}

// ---------------- 六点五、快手刷视频相关工具 ----------------
function sw() { try { return screen.getScreenWidth(); } catch (e) { return 1080; } }
function sh() { try { return screen.getScreenHeight(); } catch (e) { return 1920; } }

// 前台包名多通道(该设备 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) {}
    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) {
    try { var v = app.getVerName(pkg); if (v) return true; } catch (e) {}
    try { var n = app.getAppName(pkg); if (n) return true; } catch (e) {}
    return false;
}

// 确保快手在前台,不在就拉起来
var _launched = false;
function ensureForeground() {
    var top = topPkg();
    if (top === CFG.targetPkg) { _launched = true; return true; }
    // 该设备前台API取不到包名时不要每轮重启APP:只保证首轮拉起过即可
    if (!top && _launched) return true;
    logI("拉起快手(当前前台=" + (top || "空") + ")");
    try { app.openApp(CFG.targetPkg); } catch (e) {
      try { app.startApp(CFG.targetPkg); } catch (e2) {}
    }
    sleep.millisecond(4000);
    _launched = true;
    return true;
}

// 短视频翻页:一次完整快速长滑(不能分段,否则只抖不翻)
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 shotFingerprint() {
    try {
      var b = screen.screenShotFull().toBase64();
      if (!b) return "";
      return b.substring(0, 200) + "|" + b.substring(b.length - 200) + "|" + b.length;
    } catch (e) { return ""; }
}

// 返回 true = 确认翻到新视频
function swipeUpWithVerify() {
    var before = shotFingerprint();
    var w = sw(), h = sh();
    var x = rand(Math.round(w * 0.35), Math.round(w * 0.65));
    var y1 = Math.round(h * (0.78 + Math.random() * 0.07));
    var y2 = Math.round(h * (0.12 + Math.random() * 0.08));
    var dur = rand(CFG.videoSwipeMinMs, CFG.videoSwipeMaxMs);
    onceSwipe(x, y1, x + rand(-12, 12), y2, dur);
    logI("翻页上滑 起点y=" + y1 + " 终点y=" + y2 + " 距离" + (y1 - y2) + "px 耗时" + dur + "ms");
    sleep.millisecond(700);
    var after = shotFingerprint();
    if (before && after && before === after) {
      logW("画面无变化,补一次强滑");
      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();
      return after2 && after2 !== before;
    }
    return true;
}

// ---------------- 七、业务任务:快手刷视频一轮 ----------------
function doOneRound(idx) {
    logI("---- 第 " + idx + " 轮开始 ----");

    ensureForeground();

    // 翻页(带校验)
    var ok;
    if (CFG.verifySwipe) ok = swipeUpWithVerify();
    else { swipeUpWithVerify(); ok = true; }

    if (ok) { statAdd("ok"); logI("本轮结果: 翻页成功"); }
    else { statAdd("fail"); logW("本轮结果: 翻页失败"); }

    // 停留观看(分段 sleep,随时能响应停止信号)
    var stay = rand(CFG.stayMinMs, CFG.stayMaxMs);
    logI("停留观看 " + stay + "ms");
    var secs = Math.round(stay / 1000);
    for (var s = 0; s < secs; s++) {
      if (shouldStop()) break;
      sleep.millisecond(1000);
    }

    logI("---- 第 " + idx + " 轮结束 ----");
}

// ---------------- 八、调度主循环 ----------------
function main() {
    ensureDirs();
    logI("===== 例子5 日志配置与任务调度 开始 =====");

    // 1) 读配置(第二次运行会沿用上次的参数)
    loadConfig();

    // 2) 设备标识
    try { CFG.deviceTag = "" + device.getModel(); } catch (e) { CFG.deviceTag = "未知设备"; }
    logI("设备: " + CFG.deviceTag);

    // 3) 目标APP检查 + 启动
    var vName = "", aName = "";
    try { vName = app.getVerName(CFG.targetPkg); } catch (e) {}
    try { aName = app.getAppName(CFG.targetPkg); } catch (e) {}
    logI("目标APP: " + CFG.targetPkg + " 应用名=" + (aName || "取不到") + " 版本=" + (vName || "取不到"));
    if (!isInstalled(CFG.targetPkg)) logW("安装检测未确认(可能误报),仍尝试启动");
    ensureForeground();

    // 4) 定时等待
    if (!waitForStartTime()) { logW("等待期间被停止"); return; }

    STAT.startMs = new Date().getTime();
    var deadline = STAT.startMs + CFG.maxMinutes * 60 * 1000;

    // 4) 主循环:轮次 + 限时 + 停止/暂停
    for (var i = 1; i <= CFG.rounds; i++) {
      if (shouldStop()) { logW("检测到停止文件,提前收工"); break; }
      if (new Date().getTime() > deadline) { logW("到达限时,自动收工"); break; }

      waitIfPaused();
      if (shouldStop()) break;

      doOneRound(i);

      // 每轮上报一次
      reportLine({ device: CFG.deviceTag, round: i, ok: STAT.ok, fail: STAT.fail, time: ts() });
      saveConfig();

      // 轮次间休息(最后一轮不用休)
      if (i < CFG.rounds) {
            var rest = rand(CFG.restMinSec, CFG.restMaxSec);
            logI("休息 " + rest + " 秒");
            for (var s = 0; s < rest; s++) {
                if (shouldStop()) break;
                sleep.millisecond(1000);
            }
      }
    }

    // 5) 收尾:默认停在快手(刷视频不要回桌面)
    if (CFG.finishGoHome) {
      try { auto.home(); } catch (e) {}
      logI("已回桌面");
    } else {
      logI("收尾:停在 " + CFG.targetPkg + " 不退出");
    }

    // 6) 补报 + 统计落盘
    flushQueue();
    saveStat();
    flushLog(true);

    var msg = "调度结束 成功" + STAT.ok + " 失败" + STAT.fail;
    logI("===== " + msg + " =====");
    try { toast.show(msg); } catch (e) {}
}

main();


页: [1]
查看完整版本: 日志分级落盘配置持久化任务调度