 | |  |  | 日志分级落盘配置持久化任务调度
- // =====================================================================
- // 例子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[i];
- 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();
复制代码
| |  | |  |
|