日志分级落盘配置持久化任务调度
日志分级落盘配置持久化任务调度
// =====================================================================
//例子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]