YYPOST群发软件 发表于 2026-9-3 06:39:59

AIWROK手机指挥台停止与开始执行器



AIWROK手机指挥台停止与开始执行器










https://www.yuque.com/aiwork/nba2pr/ssiowosqrnpscnef



/**
* AIWROK 手机执行器(MQTT 群控端 · 增强版)
* ------------------------------------------------------------------
* 配套:指挥台.html(电脑浏览器打开)
*
* 主题约定(ROOM 换成你自己的房间号,避免和别人串台)
*   收:aiwrok/<ROOM>/cmd/<本机机号>   只发给某一台
*   收:aiwrok/<ROOM>/cmd/all            广播,所有机器都执行
*   发:aiwrok/<ROOM>/ret/<本机机号>   心跳 + 执行结果 + 截图
*
* 指令清单(JSON,带 "id" 的回执会原样带回,电脑端用来算耗时)
*   {"cmd":"PING"}                                 测连通
*   {"cmd":"INFO"}                                 完整环境快照
*   {"cmd":"STATUS"}                                 运行状态
*   {"cmd":"SHOT","w":540,"q":60}                  截图回传(可临时改缩放/质量)
*   {"cmd":"CLICK","x":540,"y":960}                  点坐标
*   {"cmd":"SWIPE","x1":540,"y1":1400,"x2":540,"y2":500,"ms":300}   滑动
*   {"cmd":"TEXT","text":"你好"}                      输入(sendString)
*   {"cmd":"CLIP","text":"你好"}                      输入(剪贴板+长按粘贴)
*   {"cmd":"FIND","text":"点赞","tap":1}            按屏幕文字找,找到就点
*   {"cmd":"APP","pkg":"com.tencent.mm"}             打开应用
*   {"cmd":"HOME"} / {"cmd":"BACK"}
*   {"cmd":"WAIT","ms":3000}                         等待
*   {"cmd":"LOOP","act":"SWIPE","count":30,"min":2000,"max":2000}   循环
*   {"cmd":"STOP"}                                 停循环
*   {"cmd":"RAND","on":1,"px":20}                  随机偏移开关(防风控)
*   {"cmd":"TASK","steps":[{"cmd":"APP","pkg":"..."},{"cmd":"WAIT","ms":4000},{"cmd":"SWIPE"},{"cmd":"FIND","text":"点赞"}]}
*                                                    任务编排:按顺序跑一整套
*   {"cmd":"SELFTEST"}                               一键自检:逐项体检报 pass/fail
* ------------------------------------------------------------------
* 日志:每条指令都打 序号/来源/参数/耗时/结果/回传字节,循环和任务每步都打,
*       绝不静默执行。
* ------------------------------------------------------------------
*/

// ==================== 配置:只改这三行 ====================
var ROOM      = "room001";               // 房间号,多台电脑/多套设备用它隔离
var DEV_ID    = "";                        // 机号,留空=自动用 IMEI 后 6 位
var BROKER    = "tcp://broker.emqx.io:1883";
var SHOT_MAXW = 540;      // 回传截图最大宽度,超出等比缩小
var SHOT_QUAL = 60;       // JPEG 质量 1-100,调小更省流量更不易掉线
var VER       = "v6";   // 版本号,日志里能看到跑的是哪版
// ========================================================

var T = { cmdMine: "", cmdAll: "", ret: "" };

// ---------- 日志 ----------
// 重要:printl 只能在脚本主线程调用。Paho 的回调(收消息/掉线)跑在别的
// Java 线程上,在那里直接 printl 会抛异常并打死消息派发线程,表现为
// "只收到第一条指令,之后全部丢失,最后 32000 超时掉线"。
var IN_CALLBACK = false;
var LOGQ = [];

function log(s) {
    if (IN_CALLBACK) {
      if (LOGQ.length < 800) LOGQ.push(s);   // 回调线程:只入队,绝不 printl
    } else {
      try { printl("[执行器] " + s); } catch (e) {}
    }
}
function logk(s) { log(s); }                   // 关键节点,写法统一方便过滤

function flushLog() {
    var n = 0;
    while (LOGQ.length > 0 && n < 40) {
      try { printl("[执行器] " + LOGQ.shift()); } catch (e) { LOGQ.length = 0; break; }
      n++;
    }
}

// ==================== 1. 加载 Paho ====================
try {
    rhino.loadDex("paho-mqtt.dex");
    log(" dex 已加载 paho-mqtt.dex");
} catch (e) {
    log(" loadDex 跳过(可能系统已自动加载插件):" + e.message);
}

try {
    importClass(Packages.org.eclipse.paho.client.mqttv3.MqttClient);
    importClass(Packages.org.eclipse.paho.client.mqttv3.MqttConnectOptions);
    importClass(Packages.org.eclipse.paho.client.mqttv3.MqttCallback);
    importClass(Packages.org.eclipse.paho.client.mqttv3.MqttMessage);
    importClass(Packages.org.eclipse.paho.client.mqttv3.persist.MemoryPersistence);
    log(" Paho 类导入成功");
} catch (e) {
    log(" Paho 类导入失败:" + e.message);
    log("      请把 paho-mqtt.dex 放到本工程的 插件\\ 目录后重跑");
    exit();
}

// ==================== 2. 设备信息 ====================
function deviceInfo() {
    var o = { brand: "?", model: "?", imei: "", w: 0, h: 0, android: "?", sdk: "?" };
    try { o.brand = "" + device.getBrand(); } catch (e) {}
    try { o.model = "" + device.getModel(); }catch (e) {}
    try { o.imei= "" + device.getIMEI(); }   catch (e) {}
    try { o.w = screen.getScreenWidth(); o.h = screen.getScreenHeight(); } catch (e) {}
    try { o.android = "" + java.lang.System.getProperty("os.version"); } catch (e) {}
    try { o.sdk = "" + Packages.android.os.Build.VERSION.SDK_INT; } catch (e) {}
    return o;
}
var INFO = deviceInfo();
if (!DEV_ID) {
    DEV_ID = INFO.imei ? ("" + INFO.imei).slice(-6) : ("dev" + Math.floor(Math.random() * 10000));
}
DEV_ID = "" + DEV_ID;
T.cmdMine = "aiwrok/" + ROOM + "/cmd/" + DEV_ID;
T.cmdAll= "aiwrok/" + ROOM + "/cmd/all";
T.ret   = "aiwrok/" + ROOM + "/ret/" + DEV_ID;

// ==================== 3. 状态 ====================
var client = null, connected = false;
var startAt = (new Date()).getTime();
var nRecv = 0, nSent = 0, nRun = 0, nErr = 0;
var loopOn = false, loopTotal = 0, loopTimes = 0;
var taskOn = false, taskStep = 0, taskTotal = 0;
var CMDQ = [];                              // 回调线程入队,主线程执行
var randOn = 0, randPx = 20;                // 随机偏移(防风控)

// ==================== 4. 回传 ====================
function report(obj, retain) {
    if (!connected) { log("回传丢弃(未连接) type=" + obj.type); return; }
    try {
      obj.dev = DEV_ID; obj.room = ROOM; obj.ver = VER;
      obj.brand = INFO.brand; obj.model = INFO.model;
      obj.screen = INFO.w + "x" + INFO.h;
      obj.ts = (new Date()).getTime();
      var body = JSON.stringify(obj);
      var m = new MqttMessage();
      m.setPayload(new java.lang.String(body).getBytes("UTF-8"));
      m.setQos(retain ? 1 : 0);
      if (retain) m.setRetained(true);
      client.publish(T.ret, m);
      nSent++;
      log("→ 回传 " + T.ret + " type=" + obj.type +
            (obj.cmd ? ("/" + obj.cmd) : "") + " 字节=" + body.length);
    } catch (e) { nErr++; log("回传失败:" + e.message); }
}

// ==================== 5. 基础动作 ====================
function jitter(v) {
    if (!randOn) return v;
    var d = Math.floor(Math.random() * (randPx * 2 + 1)) - randPx;
    var r = v + d;
    if (r < 1) r = 1;
    return r;
}
function doClick(x, y) {
    var jx = jitter(x), jy = jitter(y);
    auto.clickPoint(jx, jy);
    return { x: jx, y: jy, raw: x + "," + y, rand: randOn ? 1 : 0 };
}
function doSwipe(o) {
    var x1 = jitter(o.x1 || Math.floor(INFO.w / 2)),   y1 = jitter(o.y1 || Math.floor(INFO.h * 0.75));
    var x2 = jitter(o.x2 || Math.floor(INFO.w / 2)),   y2 = jitter(o.y2 || Math.floor(INFO.h * 0.25));
    var ms = o.ms || 300;
    auto.swip(x1, y1, x2, y2, ms, 0);
    return { x1: x1, y1: y1, x2: x2, y2: y2, ms: ms, rand: randOn ? 1 : 0 };
}
function doText(t) {
    input.sendString(t);
    return { len: ("" + t).length, way: "sendString" };
}
// 剪贴板输入:sendString 不生效时用它(需输入框已聚焦,再长按选"粘贴")
function doClip(t) {
    var s = "" + t;
    try {
      if (typeof Clipboard !== "undefined" && Clipboard && typeof Clipboard.copy === "function") {
            Clipboard.copy(s);
            return { len: s.length, way: "Clipboard.copy" };
      }
    } catch (e1) { throw new Error("Clipboard.copy 失败:" + e1.message); }
    try {
      if (typeof clipboard !== "undefined" && clipboard && typeof clipboard.copy === "function") {
            clipboard.copy(s);
            return { len: s.length, way: "clipboard.copy" };
      }
    } catch (e2) { throw new Error("clipboard.copy 失败:" + e2.message); }
    throw new Error("本机无剪贴板接口(Clipboard/clipboard 都不可用),请改用 TEXT");
}
function doShot(maxW, qual) {
    var mw = maxW || SHOT_MAXW, qq = qual || SHOT_QUAL;
    var bmp = screen.screenShotFull();
    if (!bmp) throw new Error("截图返回空");
    var b64 = null;
    try {
      var w = bmp.getWidth(), h = bmp.getHeight();
      var target = bmp, from = null;
      try {
            if (w > mw) {
                var sc = mw / w;
                var mx = new Packages.android.graphics.Matrix();
                mx.postScale(sc, sc);
                target = Packages.android.graphics.Bitmap.createBitmap(bmp, 0, 0, w, h, mx, true);
                from = target;
            }
      } catch (eScale) { target = bmp; }
      var bos = new java.io.ByteArrayOutputStream();
      target.compress(Packages.android.graphics.Bitmap.CompressFormat.JPEG, qq, bos);
      b64 = "" + Packages.android.util.Base64.encodeToString(bos.toByteArray(),
                Packages.android.util.Base64.NO_WRAP);
      if (from) { try { from.recycle(); } catch (e2) {} }
    } catch (e) {
      try { b64 = "" + bmp.toBase64(); } catch (e5) { b64 = null; }
    }
    if (!b64 || b64.length < 32) {
      try { bmp.recycle(); } catch (e3) {}
      throw new Error("截图编码失败");
    }
    try { bmp.recycle(); } catch (e4) {}
    var kb = Math.round(b64.length / 1024);
    if (kb > 400) throw new Error("截图仍过大 " + kb + "KB,调小 w 或 q");
    return { img: b64, kb: kb };
}
function doApp(pkg) {
    if (!pkg) throw new Error("缺少 pkg 包名");
    app.openApp(pkg);
    return { pkg: pkg };
}
function doHome() { auto.home(); return {}; }
function doBack() {
    if (typeof auto.back === "function") { auto.back(); return {}; }
    throw new Error("此版本 auto 无 back(),改用 CLICK 点返回键坐标");
}
// 按屏幕文字查找,找到就点中心(依赖 UI 树,部分应用读不到会报未找到)
function doFind(text, tap) {
    var xml = "";
    try { xml = "" + agent.getXml(); }
    catch (e) { throw new Error("读取界面失败:" + e.message); }
    if (!xml || xml.length < 20) throw new Error("界面内容为空");
    var idx = xml.indexOf('text="' + text + '"');
    if (idx < 0) { return { found: 0, text: text }; }
    var seg = xml.substring(idx, idx + 600);
    var bm = seg.match(/bounds="\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]"/);
    if (!bm) return { found: 1, tapped: 0, text: text, note: "找到但无坐标" };
    var cx = Math.round(parseInt(bm, 10) + (parseInt(bm, 10) - parseInt(bm, 10)) / 2);
    var cy = Math.round(parseInt(bm, 10) + (parseInt(bm, 10) - parseInt(bm, 10)) / 2);
    var out = { found: 1, text: text, x: cx, y: cy, bounds: bm };
    if (tap === undefined || tap) { auto.clickPoint(cx, cy); out.tapped = 1; }
    return out;
}

// ==================== 6. 统一执行入口(handle / TASK / SELFTEST 都走这里) ==========
function execCmd(o) {
    var cmd = (o.cmd || "").toUpperCase();
    var res = { cmd: cmd, ok: false };
    var t0 = (new Date()).getTime();

    if (cmd === "PING")       { res.ok = true; res.msg = "pong"; }
    else if (cmd === "INFO"){
      res.ok = true;
      res.env = { brand: INFO.brand, model: INFO.model, imei: INFO.imei,
                  screen: INFO.w + "x" + INFO.h, android: INFO.android, sdk: INFO.sdk,
                  room: ROOM, dev: DEV_ID, ver: VER, broker: BROKER,
                  sub: , pub: T.ret,
                  shot: { maxW: SHOT_MAXW, qual: SHOT_QUAL }, rand: { on: randOn, px: randPx } };
    }
    else if (cmd === "STATUS"){
      res.ok = true;
      res.info = { up: Math.round(((new Date()).getTime() - startAt) / 1000),
                     recv: nRecv, sent: nSent, run: nRun, err: nErr,
                     loop: loopOn, loopTimes: loopTimes, loopTotal: loopTotal,
                     task: taskOn, taskStep: taskStep, taskTotal: taskTotal,
                     rand: randOn, connected: connected };
    }
    else if (cmd === "CLICK") { var px = parseInt(o.x, 10), py = parseInt(o.y, 10);
                              if (isNaN(px) || isNaN(py)) throw new Error("坐标无效 x=" + o.x + " y=" + o.y);
                              res.ok = true; res.data = doClick(px, py); }
    else if (cmd === "SWIPE") { res.ok = true; res.data = doSwipe(o); }
    else if (cmd === "TEXT"){ if (!o.text) throw new Error("缺少 text");
                              res.ok = true; res.data = doText(o.text); }
    else if (cmd === "CLIP"){ if (!o.text) throw new Error("缺少 text");
                              res.ok = true; res.data = doClip(o.text); }
    else if (cmd === "FIND"){ if (!o.text) throw new Error("缺少 text");
                              res.ok = true; res.data = doFind(o.text, o.tap); }
    else if (cmd === "SHOT"){ var r = doShot(o.w, o.q);
                              res.ok = true; res.kb = r.kb; res.img = r.img; }
    else if (cmd === "APP")   { res.ok = true; res.data = doApp(o.pkg); }
    else if (cmd === "HOME"){ res.ok = true; doHome(); res.msg = "已回桌面"; }
    else if (cmd === "BACK"){ res.ok = true; doBack(); res.msg = "已返回"; }
    else if (cmd === "WAIT"){ res.ok = true; res.wait = o.ms || 1000; }
    else if (cmd === "RAND"){ randOn = o.on ? 1 : 0; randPx = o.px || randPx;
                              res.ok = true; res.msg = "随机偏移=" + (randOn ? ("开 " + randPx + "px") : "关"); }
    else if (cmd === "STOP"){ loopOn = false; taskOn = false;
                              res.ok = true; res.msg = "已发停止(循环+任务)"; }
    else { throw new Error("未知指令 " + cmd); }

    res.ms = (new Date()).getTime() - t0;
    return res;
}

// ==================== 7. 任务编排:按顺序跑一组步骤 ====================
function runTask(steps) {
    if (!steps || !steps.length) throw new Error("steps 为空");
    taskOn = true; taskStep = 0; taskTotal = steps.length;
    logk("任务开始 共 " + steps.length + " 步:" + JSON.stringify(steps).slice(0, 200));
    report({ type: "TASK", state: "start", total: steps.length });

    (function next(i) {
      if (!taskOn) {
            logk("任务已停止,完成 " + i + "/" + steps.length + " 步");
            report({ type: "TASK", state: "stop", step: i, total: steps.length });
            return;
      }
      if (i >= steps.length) {
            taskOn = false;
            logk("任务全部完成,共 " + steps.length + " 步");
            report({ type: "TASK", state: "done", step: steps.length, total: steps.length });
            return;
      }
      var s = steps || {};
      taskStep = i + 1;
      var one = { type: "TASK", state: "run", step: i + 1, total: steps.length, cmd: (s.cmd || "").toUpperCase() };
      try {
            var r = execCmd(s);
            one.ok = r.ok; one.ms = r.ms;
            if (r.data) one.data = r.data;
            if (r.img) { report({ type: "SHOT", ok: true, kb: r.kb, img: r.img, step: i + 1 }); one.kb = r.kb; }
            log("任务 第" + (i + 1) + "/" + steps.length + "步 " + one.cmd + " 成功 耗时=" + r.ms + "ms" +
                (r.data ? (" 参数=" + JSON.stringify(r.data)) : ""));
      } catch (e) {
            nErr++;
            one.ok = false; one.err = "" + e.message;
            log("任务 第" + (i + 1) + "/" + steps.length + "步 " + one.cmd + " 失败:" + e.message);
      }
      report(one);
      nRun++;
      var wait = (one.cmd === "WAIT") ? (s.ms || 1000) : 300;
      setTimeout(function () { next(i + 1); }, wait);
    })(0);
}

// ==================== 8. 一键自检:逐项体检 ====================
function runSelfTest() {
    logk("自检开始(7 项)");
    report({ type: "SELFTEST", state: "start" });
    var items = [];
    var plan = [
      { name: "屏幕分辨率", run: function () {
            if (!INFO.w || !INFO.h) throw new Error("读不到宽高");
            return INFO.w + "x" + INFO.h; } },
      { name: "截图能力", run: function () {
            var r = doShot(320, 50); return r.kb + "KB"; } },
      { name: "点击能力", run: function () {
            doClick(Math.floor(INFO.w / 2), Math.floor(INFO.h / 2)); return "已点中心"; } },
      { name: "滑动能力", run: function () {
            doSwipe({}); return "已上滑一次"; } },
      { name: "UI树读取", run: function () {
            var x = "" + agent.getXml();
            if (!x || x.length < 20) throw new Error("UI树为空");
            return x.length + " 字节"; } },
      { name: "MQTT回传", run: function () {
            if (!connected) throw new Error("未连接"); return "在线"; } },
      { name: "随机偏移", run: function () {
            return randOn ? ("开 " + randPx + "px") : "关"; } }
    ];
    (function one(i) {
      if (i >= plan.length) {
            var pass = 0;
            for (var k = 0; k < items.length; k++) { if (items.ok) pass++; }
            logk("自检完成 通过 " + pass + "/" + plan.length);
            report({ type: "SELFTEST", state: "done", pass: pass, total: plan.length, items: items });
            return;
      }
      var p = plan;
      var it = { name: p.name, ok: false };
      try { it.msg = p.run(); it.ok = true; log("自检 [" + (i + 1) + "/" + plan.length + "] " + p.name + " 通过:" + it.msg); }
      catch (e) { it.msg = "" + e.message; log("自检 [" + (i + 1) + "/" + plan.length + "] " + p.name + " 失败:" + it.msg); }
      items.push(it);
      report({ type: "SELFTEST", state: "run", item: p.name, ok: it.ok, msg: it.msg, step: i + 1, total: plan.length });
      setTimeout(function () { one(i + 1); }, 400);
    })(0);
}

// ==================== 9. 循环养机 ====================
function runLoop(o) {
    loopOn = true;
    var act = o.act || "SWIPE";
    var count = o.count || 0;
    var minMs = o.min || 2000, maxMs = o.max || 2000;
    var i = 0;
    loopTotal = count; loopTimes = 0;
    logk("循环开始 动作=" + act + " 次数=" + (count > 0 ? count : "无限") + " 间隔=" + minMs + "~" + maxMs + "ms");
    report({ type: "LOOP", state: "start", act: act, count: count, times: 0 });

    (function step() {
      if (!loopOn) {
            logk("循环已停止,实际执行 " + i + " 次");
            report({ type: "LOOP", state: "stop", times: i });
            return;
      }
      try {
            var r = execCmd({ cmd: act, x1: o.x1, y1: o.y1, x2: o.x2, y2: o.y2, ms: o.ms, x: o.x, y: o.y, text: o.text, pkg: o.pkg });
            nRun++; i++; loopTimes = i;
            log("循环 第" + i + "次/" + (count > 0 ? count : "∞") + " " + act + " 成功 耗时=" + r.ms + "ms" +
                (r.data ? (" " + JSON.stringify(r.data)) : ""));
            if (r.img) report({ type: "SHOT", ok: true, kb: r.kb, img: r.img });
            report({ type: "LOOP", state: "run", act: act, times: i, total: count, ms: r.ms });
      } catch (e) {
            nErr++;
            log("循环 第" + (i + 1) + "次 失败:" + e.message);
            report({ type: "LOOP", state: "err", err: e.message, times: i });
      }
      if (count > 0 && i >= count) {
            loopOn = false;
            logk("循环完成,共 " + i + " 次");
            report({ type: "LOOP", state: "done", times: i });
            return;
      }
      var wait = minMs + Math.floor(Math.random() * Math.max(1, maxMs - minMs));
      log("下一次将在 " + wait + "ms 后(发 STOP 可停)");
      setTimeout(step, wait);
    })();
}

// ==================== 10. 指令分发 ====================
function handle(raw, from) {
    nRecv++;
    var o;
    try { o = JSON.parse(raw); }
    catch (e) { nErr++; log("收到非 JSON,忽略:" + raw.slice(0, 120)); return; }

    var cmd = (o.cmd || "").toUpperCase();
    log("收到 #" + nRecv + " " + cmd + " 来源=" + from + " id=" + (o.id || "-") +
      " 参数=" + raw.slice(0, 200));

    // 三个特殊指令单独走(异步链式执行)
    if (cmd === "LOOP") { runLoop(o); report({ type: "ACK", cmd: cmd, id: o.id || "", ok: true, msg: "循环已启动" }); return; }
    if (cmd === "TASK") {
      try { runTask(o.steps); report({ type: "ACK", cmd: cmd, id: o.id || "", ok: true, msg: "任务已启动 共" + o.steps.length + "步" }); }
      catch (e) { nErr++; report({ type: "ACK", cmd: cmd, id: o.id || "", ok: false, err: e.message }); }
      return;
    }
    if (cmd === "SELFTEST") {
      runSelfTest();
      report({ type: "ACK", cmd: cmd, id: o.id || "", ok: true, msg: "自检已启动" });
      return;
    }

    var res = { type: "ACK", cmd: cmd, id: o.id || "" };
    try {
      var r = execCmd(o);
      res.ok = r.ok; res.ms = r.ms;
      if (r.msg) res.msg = r.msg;
      if (r.data) res.data = r.data;
      if (r.info) res.info = r.info;
      if (r.env) res.env = r.env;
      if (r.wait) res.wait = r.wait;
      if (r.img) { res.kb = r.kb; res.img = r.img; }
      log("执行 " + cmd + " 成功 耗时=" + r.ms + "ms");
    } catch (e) {
      nErr++;
      res.ok = false; res.err = "" + e.message;
      log("执行 " + cmd + " 失败:" + e.message);
    }
    report(res);
}

// ==================== 11. 连接 ====================
function connect() {
    var cid = "aiwrok_" + ROOM + "_" + DEV_ID + "_" + Math.floor(Math.random() * 10000);
    logk(" 连接 " + BROKER + " clientId=" + cid);
    client = new MqttClient(BROKER, cid, new MemoryPersistence());

    var opt = new MqttConnectOptions();
    opt.setCleanSession(true);
    opt.setKeepAliveInterval(45);
    opt.setConnectionTimeout(15);
    opt.setAutomaticReconnect(true);

    client.setCallback(new MqttCallback({
      connectionLost: function (c) {
            IN_CALLBACK = true;
            try { connected = false; if (LOGQ.length < 800) LOGQ.push("掉线:" + c + "(看门狗将自动重连)"); }
            finally { IN_CALLBACK = false; }
      },
      messageArrived: function (topic, msg) {
            IN_CALLBACK = true;                     // 本线程禁止 printl / 禁止做重活
            try {
                var text = "" + new java.lang.String(msg.getPayload(), "UTF-8");
                var from = ("" + topic === T.cmdAll) ? "广播" : "定向";
                if (CMDQ.length < 200) CMDQ.push({ raw: text, from: from });
                if (LOGQ.length < 800) LOGQ.push("收到原始包 主题=" + topic + " 字节=" + text.length);
            } catch (e) {
                if (LOGQ.length < 800) LOGQ.push("收包异常:" + e.message);
            } finally { IN_CALLBACK = false; }
      },
      deliveryComplete: function (t) { }
    }));

    client.connect(opt);
    connected = true;
    client.subscribe(T.cmdMine, 1);
    client.subscribe(T.cmdAll, 1);
    logk(" 已订阅 " + T.cmdMine);
    logk(" 已订阅 " + T.cmdAll);
    report({ type: "ONLINE" }, true);
    flushLog();
}

// ==================== 12. 启动:打印完整环境 ====================
logk("========== 执行器 " + VER + " 启动 ==========");
log("设备品牌 = " + INFO.brand);
log("设备型号 = " + INFO.model);
log("IMEI   = " + (INFO.imei || "(无权限)"));
log("安卓版本 = " + INFO.android + "SDK=" + INFO.sdk);
log("屏幕   = " + INFO.w + "x" + INFO.h);
log("房间号   = " + ROOM + "   机号 = " + DEV_ID);
log("Broker   = " + BROKER);
log("订阅主题 = " + T.cmdMine + " , " + T.cmdAll);
log("回传主题 = " + T.ret);
log("截图参数 = 最大宽 " + SHOT_MAXW + "px, JPEG质量 " + SHOT_QUAL);
log("支持指令 = PING INFO STATUS SHOT CLICK SWIPE TEXT CLIP FIND APP HOME BACK WAIT LOOP TASK SELFTEST STOP RAND");
logk(" 环境读取完成");

try { connect(); }
catch (e) { connected = false; log(" 首次连接失败:" + e.message + ",看门狗持续重试"); }

// ---------- 主线程看门狗:执行队列 + 刷日志 + 重连重订阅 ----------
var tickSec = 0, retryAt = 0;
setInterval(function () {
    tickSec++;
    // 1) 主线程执行排队指令(每轮最多 5 条)
    var k = 0;
    while (CMDQ.length > 0 && k < 5) {
      var job = CMDQ.shift();
      try { handle(job.raw, job.from); }
      catch (e) { nErr++; log("指令执行异常:" + e.message); }
      k++;
    }
    flushLog();                                        // 2) 刷回调日志
    // 3) 连接状态同步
    try {
      var real = client ? client.isConnected() : false;
      if (real && !connected) {
            connected = true;
            client.subscribe(T.cmdMine, 1);
            client.subscribe(T.cmdAll, 1);
            logk("已恢复连接并重新订阅(cleanSession 下必须重订阅)");
            report({ type: "ONLINE" }, true);
      }
      if (!real && client && (tickSec - retryAt) >= 8) {
            connected = false;
            retryAt = tickSec;
            log("第 " + tickSec + " 秒:未连接,主动触发重连…");
            try { client.reconnect(); }
            catch (e) {
                log("reconnect 失败:" + e.message + ",重建连接");
                try { connect(); } catch (e2) { log("重建失败:" + e2.message); }
            }
      }
    } catch (e) { log("看门狗异常:" + e.message); }
}, 1000);

// ---------- 心跳 ----------
var beat = 0;
setInterval(function () {
    beat++;
    if (connected) {
      report({ type: "HEART", up: beat * 20, recv: nRecv, sent: nSent, run: nRun,
               err: nErr, loop: loopOn, task: taskOn, rand: randOn });
    } else {
      log("心跳跳过(当前离线,看门狗重连中)");
    }
}, 20000);

logk(" 已进入待命:每 1 秒取指令/查连接,每 20 秒心跳。停止请点 IDE 停止按钮");


页: [1]
查看完整版本: AIWROK手机指挥台停止与开始执行器