YYPOST群发软件 发表于 7 天前

WebSocket带截图/点击/滑屏/按键/跑脚本/状态查询

WebSocket带截图/点击/滑屏/按键/跑脚本/状态查询


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










// =====================================================================
//AIWROK WebSocket 客户端(实用版 — 带截图/点击/滑屏/按键/跑脚本/状态查询)
//
//服务端可下发的指令(type):
//    CMD_SCREENSHOT         截图(base64 返回,可调 quality 0.3~0.95)
//    CMD_CLICK                点击 {x, y}
//    CMD_SWIPE                滑屏 {x1,y1,x2,y2,duration}
//    CMD_KEY                  按键 {key: "BACK"|"HOME"|"RECENT"|"POWER"|"NOTIFICATION"|数字keyCode}
//    CMD_RUN_SCRIPT         远程启动脚本 {path: "/sdcard/AIWROK/xxx.js", timeoutSec, args:{...}}
//    CMD_STATUS               查设备状态(包名/内存/运行时长/分辨率)
//    CMD_TOAST                弹提示 {msg}
//    CMD_CLEAR_CACHE          清除本地缓存
//    MANAGE_TASK_NEW          下发任务(兼容原格式,自动转成 CMD_RUN_SCRIPT 或回执)
//    MANAGE_CMD_STOP_ALL      停止一切(写标志 + 设 stopAll + 回 CLIENT_STOPPING + 关 WS)
//    SERVER_PING            心跳回包
//
//客户端上报:
//    CLIENT_REGISTER / CLIENT_HEARTBEAT / CLIENT_TASK_ACK
//    CMD_RESP(每条 CMD_* 指令执行完都回,格式统一,含 ok/msg/data/ts/cmdId)
//    CLIENT_STOPPING(停止前最后一条广播给服务端)
//
//用法:CFG.serverUrl 改成 ws://ip:port/ws,直接跑
// =====================================================================

// ---------- 全局变量(必须放在文件顶部,避免 Rhino ES5 ReferenceError)----------
var G = {
    ws: null,
    clientId: null,
    isAllowReConnect: true,
    isInfiniteReConnect: true,
    reConnectMaxTimes: 10,
    reConnectTimes: 0,
    connectDelayMs: 3000,
    multiplier: 2,
    maxConnectDelayMs: 30000,
    heartbeatIntervalMs: 50000,
    heartbeatTimer: null,
    lastTs: 0,
    dispatcher: {},
    running: true,
    stopAll: false,            // STOP_ALL 全局标志(业务脚本都可以读这个退出)
    startTime: nowSec(),       // 启动时间戳
    activeThreads: {},         // CMD_RUN_SCRIPT 启动的线程 id -> thread 对象
    STOP_FLAG: "/sdcard/AIWROK/STOP_ALL"
};

var CFG = {
    serverUrl: "ws://192.168.10.103:32101/ws", // 主群控服务端
    platform:"AIWROK",
    appName:   "AIWROK_WebSocket客户端",
    debug:   true,
    // 截图默认参数(可以被 CMD_SCREENSHOT.data 覆盖)
    defaultQuality: 0.55,
    maxWidth:   1080,          // 超过就等比缩放,减少带宽
    stopReceiverIntervalMs: 1500
};

// ---------- 工具函数 ----------
function nowSec() {
    return Math.floor((new Date()).getTime() / 1000);
}
function nowMs() {
    return (new Date()).getTime();
}
function jitter(ms) {
    return Math.floor(ms * (0.85 + Math.random() * 0.3));
}
function readTextFile(path) {
    try {
      var br = new java.io.BufferedReader(
            new java.io.InputStreamReader(new java.io.FileInputStream(path), "utf-8"));
      var sb = new java.lang.StringBuilder();
      var line;
      while ((line = br.readLine()) != null) sb.append(line).append("\n");
      br.close();
      return "" + sb.toString();
    } catch (e) { return null; }
}
function deleteFileOrDir(p) {
    try {
      var f = new java.io.File(p);
      if (!f.exists()) return true;
      if (f.isDirectory()) {
            var list = f.listFiles();
            for (var i = 0; i < list.length; i++) deleteFileOrDir(list.getAbsolutePath());
      }
      return f["delete"]();
    } catch (e) { return false; }
}
function humanSize(bytes) {
    if (bytes < 1024) return bytes + "B";
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "KB";
    return (bytes / 1024 / 1024).toFixed(2) + "MB";
}

// ---------- 本地缓存封装 ----------
var Cache = {
    BASE: "/sdcard/AIWROK/wscache",
    ensure: function () {
      try {
            var f = new java.io.File(this.BASE);
            if (!f.exists()) f.mkdirs();
      } catch (e) {}
    },
    read: function (key) {
      this.ensure();
      try {
            var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
            return JSON.parse(readTextFile(p));
      } catch (e) { return null; }
    },
    write: function (key, val) {
      this.ensure();
      try {
            var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
            var fw = new java.io.FileWriter(p);
            fw.write(JSON.stringify(val));
            fw.close();
            return true;
      } catch (e) { return false; }
    },
    clear: function (key) {
      try {
            var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
            var f = new java.io.File(p);
            if (f.exists()) f["delete"]();
      } catch (e) {}
    },
    clearAll: function () {
      return deleteFileOrDir(this.BASE);
    }
};

// 尝试恢复 client_id
G.clientId = (Cache.read("wsc_client_id") || {}).id || null;

// ---------- 统一 CMD_RESP ----------
function cmdResp(cmdName, cmdId, ok, msg, data) {
    var obj = {
      type: "CMD_RESP",
      cmd: cmdName,
      cmdId: cmdId || ("cid_" + nowMs() + "_" + Math.floor(Math.random() * 1000)),
      ok: !!ok,
      msg: msg || (ok ? "ok" : "failed"),
      ts: nowSec()
    };
    if (typeof data !== "undefined") obj.data = data;
    if (G.clientId) obj.client_id = G.clientId;
    WSC.send(obj);
    return obj;
}

// ---------- 消息派发器 ----------
var WSC = {
    on: function (type, fn) {
      if (!G.dispatcher) G.dispatcher = [];
      G.dispatcher.push(fn);
    },
    off: function (type, fn) {
      var arr = G.dispatcher || [];
      for (var i = arr.length - 1; i >= 0; i--) if (arr === fn) arr.splice(i, 1);
    },
    fire: function (type, msgObj) {
      var arr = G.dispatcher || [];
      for (var i = 0; i < arr.length; i++) {
            try {
                arr(msgObj);
            } catch (e) {
                printl("派发[" + type + "]异常: " + (e.message || e));
            }
      }
    },
    send: function (obj) {
      if (!G.ws) return false;
      if (G.clientId && obj.clientId == null) obj.client_id = G.clientId;
      if (obj.timestamp == null) obj.timestamp = nowSec();
      if (obj.platform == null) obj.platform = CFG.platform;
      var txt = JSON.stringify(obj);
      try {
            G.ws.send(txt);
            if (CFG.debug) {
                var head = (obj.type || "?") + " ";
                // 截图回包 base64 太长,只打摘要
                if (obj.type === "CMD_RESP" && obj.data && obj.data.base64) {
                  printl("SEND -> " + head + "[" + obj.cmd + "] base64_len=" + obj.data.base64.length + " (" + obj.data.w + "x" + obj.data.h + " q=" + obj.data.quality + ")");
                } else {
                  printl("SEND -> " + head + txt.slice(0, 160));
                }
            }
            return true;
      } catch (e) {
            printl("发送失败: " + (e.message || e));
            return false;
      }
    },
    stop: function () {
      G.running = false;
      G.isAllowReConnect = false;
      if (G.heartbeatTimer) {
            try { clearTimeout(G.heartbeatTimer); } catch (e) {}
            G.heartbeatTimer = null;
      }
      try { if (G.ws) G.ws.close(); } catch (e) {}
    }
};

// ---------- 心跳(安全版:链式 setTimeout 全包 try-catch,避免 AIWROK 未捕获异常杀脚本)----------
function startHeartbeat() {
    if (G.heartbeatTimer) {
      try { clearTimeout(G.heartbeatTimer); } catch (e) {}
    }
    (function tick() {
      try {
            if (!G.running) return;
            var imei = "";
            try { if (typeof device !== "undefined") imei = device.getIMEI() || ""; } catch (e) {}
            WSC.send({
                type: "CLIENT_HEARTBEAT",
                imei: imei,
                platform: CFG.platform,
                app: CFG.appName,
                runtimeSec: nowSec() - G.startTime,
                stopAll: !!G.stopAll
            });
      } catch (eOuter) {
            try { printl("心跳异常(已兜住): " + (eOuter.message || eOuter)); } catch (eL) {}
      }
      try {
            G.heartbeatTimer = setTimeout(tick, G.heartbeatIntervalMs);
      } catch (eTimer) {
            try { printl("心跳下一次调度失败: " + (eTimer.message || eTimer)); } catch (eL) {}
      }
    })();
}

// ---------- 设备信息采集(连接时用,也被 CMD_STATUS 复用)----------
function collectDeviceInfo() {
    var info = {
      platform: CFG.platform,
      app: CFG.appName,
      imei: "",
      android: "",
      model: "",
      brand: "",
      w: 1080,
      h: 2400,
      dpi: 0,
      frontPkg: "",
      internalMB: 0,
      sdMB: 0,
      clientId: G.clientId,
      runtimeSec: nowSec() - G.startTime,
      stopAll: !!G.stopAll,
      activeScriptCount: 0
    };
    try { if (typeof device !== "undefined") info.imei = device.getIMEI() || ""; } catch (e) {}
    try { info.android = java.lang.System.getProperty("os.version") + ""; } catch (e) {}
    try { info.model = android.os.Build.MODEL + ""; } catch (e) {}
    try { info.brand = android.os.Build.BRAND + ""; } catch (e) {}
    try {
      if (typeof screen !== "undefined") {
            info.w = screen.getScreenWidth();
            info.h = screen.getScreenHeight();
            try { info.dpi = screen.getDensityDpi(); } catch (e2) {}
      }
    } catch (e) {}
    try {
      var am = context.getSystemService(android.content.Context.ACTIVITY_SERVICE);
      info.frontPkg = am.getRunningTasks(1).get(0).topActivity.getPackageName() + "";
    } catch (e) {}
    try {
      var stat = new android.os.StatFs(android.os.Environment.getDataDirectory().getAbsolutePath());
      info.internalMB = Math.floor((stat.getAvailableBytes() * 1.0) / 1024 / 1024);
    } catch (e) {}
    try {
      var sd = android.os.Environment.getExternalStorageDirectory();
      var s2 = new android.os.StatFs(sd.getAbsolutePath());
      info.sdMB = Math.floor((s2.getAvailableBytes() * 1.0) / 1024 / 1024);
    } catch (e) {}
    var cnt = 0;
    try { for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) cnt++; } catch (e) {}
    info.activeScriptCount = cnt;
    return info;
}

// ---------- 事件回调:onConnected ----------
function wsOnConnected() {
    G.reConnectTimes = 0;
    G.connectDelayMs = 3000;
    printl("✓ 连接成功 — 握手完成");

    var info = collectDeviceInfo();
    WSC.send({
      type: "CLIENT_REGISTER",
      platform: info.platform,
      imei: info.imei,
      app: info.app,
      lastClientId: G.clientId,
      screen: info.w + "x" + info.h,
      dpi: info.dpi,
      android: info.android,
      model: info.model,
      brand: info.brand,
      internalMB: info.internalMB,
      sdMB: info.sdMB
    });
    startHeartbeat();
    WSC.fire("_connected", info);
}

// ---------- 事件回调:onTextMessage ----------
function wsOnTextMessage(msg) {
    var s = "" + msg;
    if (CFG.debug) printl("RECV <- " + s.slice(0, 200));
    var obj = null;
    try { obj = JSON.parse(s); } catch (e) {
      printl("非JSON消息,忽略: " + s.slice(0, 120));
      WSC.fire("_text", { raw: s });
      return;
    }
    if (obj && obj.type === "SERVER_INIT") {
      if (obj.client_id) {
            G.clientId = obj.client_id;
            Cache.write("wsc_client_id", { id: G.clientId });
            printl("服务端分配 client_id: " + G.clientId + "(已本地保存)");
      }
      if (obj.msg) printl("服务端消息: " + obj.msg);
    }
    if (obj && obj.type) {
      WSC.fire(obj.type, obj);
      WSC.fire("_any", obj);
    }
}

// ---------- 事件回调:onConnectError ----------
function wsOnConnectError() {
    printl("✗ 连接失败(通常服务端未开启或端口不通)");
    scheduleReconnect();
}

// ---------- 事件回调:onDisconnected ----------
function wsOnDisconnected() {
    printl("✗ 连接已断开");
    if (G.heartbeatTimer) {
      try { clearTimeout(G.heartbeatTimer); } catch (e) {}
      G.heartbeatTimer = null;
    }
    scheduleReconnect();
}

// ---------- 连接 + 事件重挂 ----------
function connectWebSocket() {
    if (G.ws) {
      try { G.ws.close(); } catch (e) {}
      G.ws = null;
    }
    printl("开始连接: " + CFG.serverUrl + "(重试计数 " + G.reConnectTimes + "/" + G.reConnectMaxTimes + ")");

    var ws = null;
    try { ws = new websocket(); }
    catch (e) {
      printl("new websocket() 失败: " + (e.message || e));
      scheduleReconnect();
      return;
    }

    G.ws = ws;

    // 挂事件:AIWROK 新版 ws.event() 优先,失败兜底属性赋值
    var eventOk = false;
    try {
      ws.event(wsOnConnected, wsOnTextMessage, wsOnConnectError, wsOnDisconnected);
      eventOk = true;
      if (CFG.debug) printl("ws.event() 挂事件成功");
    } catch (e1) {
      if (CFG.debug) printl("ws.event() 失败: " + (e1.message || e1) + " — 尝试属性赋值");
    }
    if (!eventOk) {
      try {
            ws.onConnected = wsOnConnected;
            ws.onTextMessage = wsOnTextMessage;
            ws.onConnectError = wsOnConnectError;
            ws.onDisconnected = wsOnDisconnected;
            if (CFG.debug) printl("属性赋值挂事件成功");
      } catch (e2) {
            printl("两种事件挂法都失败: " + (e2.message || e2));
      }
    }

    // 发起连接:AIWROK connet(双n) 优先,失败回退 connect
    try {
      var ret = null;
      try { ret = ws.connet(CFG.serverUrl); }
      catch (eA) {
            printl("connet() 异常: " + (eA.message || eA) + " — 回退 connect()");
            ret = ws.connect(CFG.serverUrl);
      }
      if (CFG.debug) printl("连接发起返回: " + (ret == null ? "null(表示已发起)" : ret));
    } catch (eB) {
      printl("两种连接方式都失败: " + (eB.message || eB));
      scheduleReconnect();
    }
}

// ---------- 指数退避 + 抖动重连(安全版:setTimeout 最外层包 try-catch)----------
function scheduleReconnect() {
    try {
      if (!G.running) return;
      if (!G.isAllowReConnect) return;
      G.reConnectTimes += 1;
      if (!G.isInfiniteReConnect && G.reConnectTimes > G.reConnectMaxTimes) {
            printl("⚠️超过最大重试次数(" + G.reConnectMaxTimes + "),停止重连");
            return;
      }
      G.connectDelayMs = Math.min(G.connectDelayMs * G.multiplier, G.maxConnectDelayMs);
      var waitMs = jitter(G.connectDelayMs);
      printl("下次重连等待 " + (waitMs / 1000).toFixed(2) + "s(封顶" + (G.maxConnectDelayMs / 1000) + "s)");
      setTimeout(function () {
            try { connectWebSocket(); } catch (e) {
                printl("重连回调异常(已兜住): " + (e.message || e));
                scheduleReconnect();
            }
      }, waitMs);
    } catch (eOuter) {
      printl("scheduleReconnect 外层异常: " + (eOuter.message || eOuter));
    }
}

// =====================================================================
//业务指令实现(真正能用的功能,不是空壳)
// =====================================================================

// -------- CMD_SCREENSHOT:截图,base64 返回 -------
// 支持: o.data = { quality:0.3~0.95, maxWidth:1080, full:true/false }
WSC.on("CMD_SCREENSHOT", function (o) {
    var t0 = nowMs();
    var cmdId = o.cmdId;
    try {
      var quality = CFG.defaultQuality;
      if (o && o.data && o.data.quality && o.data.quality > 0 && o.data.quality <= 1) {
            quality = o.data.quality;
      }
      var maxW = CFG.maxWidth;
      if (o && o.data && o.data.maxWidth && o.data.maxWidth > 0) maxW = o.data.maxWidth;

      // 1) fastCapture 截图
      var bmp = null;
      try {
            if (typeof image !== "undefined") {
                bmp = image.fastCapture();
            } else {
                cmdResp("CMD_SCREENSHOT", cmdId, false, "image 对象不可用");
                return;
            }
      } catch (e) {
            cmdResp("CMD_SCREENSHOT", cmdId, false, "fastCapture 异常: " + (e.message || e));
            return;
      }
      if (!bmp) { cmdResp("CMD_SCREENSHOT", cmdId, false, "截图返回空"); return; }

      var w = bmp.getWidth();
      var h = bmp.getHeight();

      // 2) 超宽就等比缩放
      if (w > maxW) {
            var ratio = maxW * 1.0 / w;
            var newH = Math.floor(h * ratio);
            try {
                var scaled = android.graphics.Bitmap.createScaledBitmap(bmp, maxW, newH, true);
                try { bmp.recycle(); } catch (e) {}
                bmp = scaled;
                w = maxW;
                h = newH;
            } catch (e2) {}
      }

      // 3) JPEG 压缩转 base64
      var baos = new java.io.ByteArrayOutputStream();
      bmp.compress(android.graphics.Bitmap.CompressFormat.JPEG, Math.floor(quality * 100), baos);
      try { bmp.recycle(); } catch (e) {}
      var bytes = baos.toByteArray();
      var b64 = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP);
      try { baos.close(); } catch (e) {}

      cmdResp("CMD_SCREENSHOT", cmdId, true, "ok", {
            base64: "" + b64,
            w: w,
            h: h,
            quality: quality,
            sizeKb: Math.floor(bytes.length / 1024),
            costMs: nowMs() - t0
      });
    } catch (eBig) {
      cmdResp("CMD_SCREENSHOT", cmdId, false, "截图总异常: " + (eBig.message || eBig));
    }
});

// -------- CMD_CLICK:点击 {x, y} -------
WSC.on("CMD_CLICK", function (o) {
    var cmdId = o.cmdId;
    var x = (o && o.data && o.data.x != null) ? parseInt(o.data.x, 10) : NaN;
    var y = (o && o.data && o.data.y != null) ? parseInt(o.data.y, 10) : NaN;
    if (isNaN(x) || isNaN(y) || x < 0 || y < 0) {
      cmdResp("CMD_CLICK", cmdId, false, "缺少有效 x/y 坐标");
      return;
    }
    try {
      if (typeof action !== "undefined") {
            action.click(x, y);
            cmdResp("CMD_CLICK", cmdId, true, "ok", { x: x, y: y });
      } else {
            cmdResp("CMD_CLICK", cmdId, false, "action 对象不可用");
      }
    } catch (e) {
      cmdResp("CMD_CLICK", cmdId, false, "点击异常: " + (e.message || e));
    }
});

// -------- CMD_SWIPE:滑屏 {x1,y1,x2,y2,duration} -------
WSC.on("CMD_SWIPE", function (o) {
    var cmdId = o.cmdId;
    var d = o && o.data ? o.data : {};
    var x1 = parseInt(d.x1, 10), y1 = parseInt(d.y1, 10);
    var x2 = parseInt(d.x2, 10), y2 = parseInt(d.y2, 10);
    var dur = d.duration ? parseInt(d.duration, 10) : 300;
    if (.some(function (n) { return isNaN(n) || n < 0; })) {
      cmdResp("CMD_SWIPE", cmdId, false, "坐标缺失");
      return;
    }
    try {
      if (typeof action !== "undefined") {
            action.swipe(x1, y1, x2, y2, dur);
            cmdResp("CMD_SWIPE", cmdId, true, "ok", { x1: x1, y1: y1, x2: x2, y2: y2, duration: dur });
      } else {
            cmdResp("CMD_SWIPE", cmdId, false, "action 对象不可用");
      }
    } catch (e) {
      cmdResp("CMD_SWIPE", cmdId, false, "滑屏异常: " + (e.message || e));
    }
});

// -------- CMD_KEY:按键 -------
// data.key 支持: "BACK" / "HOME" / "RECENT" / "POWER" / "NOTIFICATION" / 数字 keyCode
WSC.on("CMD_KEY", function (o) {
    var cmdId = o.cmdId;
    var key = (o && o.data && o.data.key != null) ? ("" + o.data.key) : "";
    if (!key) { cmdResp("CMD_KEY", cmdId, false, "缺少 key"); return; }
    try {
      // 方式1:action 快捷方法
      if (typeof action !== "undefined") {
            var k = key.toUpperCase();
            if (k === "BACK") { action.back(); cmdResp("CMD_KEY", cmdId, true, "BACK"); return; }
            if (k === "HOME") { action.home(); cmdResp("CMD_KEY", cmdId, true, "HOME"); return; }
            if (k === "RECENT" || k === "RECENTS" || k === "APP_SWITCH") {
                action.recents(); cmdResp("CMD_KEY", cmdId, true, "RECENT"); return;
            }
      }
      // 方式2:systempress 数字 keyCode
      var code = parseInt(key, 10);
      if (!isNaN(code) && code > 0) {
            if (typeof systempress !== "undefined") {
                systempress(code);
                cmdResp("CMD_KEY", cmdId, true, "keyCode " + code);
                return;
            }
      }
      // 方式3:shell input keyevent(兜底)
      var shellKey = null;
      var k2 = key.toUpperCase();
      if (k2 === "BACK") shellKey = "4";
      else if (k2 === "HOME") shellKey = "3";
      else if (k2 === "RECENT" || k2 === "RECENTS") shellKey = "187";
      else if (k2 === "POWER") shellKey = "26";
      else if (k2 === "NOTIFICATION") shellKey = "83";
      else if (!isNaN(code) && code > 0) shellKey = "" + code;
      if (shellKey) {
            try {
                java.lang.Runtime.getRuntime().exec(["su", "-c", "input keyevent " + shellKey]).waitFor();
                cmdResp("CMD_KEY", cmdId, true, "shell key " + key);
                return;
            } catch (eS) {}
            try {
                java.lang.Runtime.getRuntime().exec(["input", "keyevent", shellKey]).waitFor();
                cmdResp("CMD_KEY", cmdId, true, "shell key(no su) " + key);
                return;
            } catch (eS2) {}
      }
      cmdResp("CMD_KEY", cmdId, false, "不支持的 key: " + key);
    } catch (e) {
      cmdResp("CMD_KEY", cmdId, false, "按键异常: " + (e.message || e));
    }
});

// -------- CMD_RUN_SCRIPT:远程跑脚本 -------
// data = { path, timeoutSec, args }
// 返回 { threadId, pid, ok },后续可查线程状态
WSC.on("CMD_RUN_SCRIPT", function (o) {
    var cmdId = o.cmdId;
    var d = o && o.data ? o.data : {};
    var path = (d.path || "").trim();
    var timeoutSec = d.timeoutSec ? parseInt(d.timeoutSec, 10) : 600;
    if (!path) { cmdResp("CMD_RUN_SCRIPT", cmdId, false, "缺少脚本 path"); return; }

    // 路径支持两种:绝对路径,或相对于 /sdcard/AIWROK/
    var fullPath = path;
    if (!new java.io.File(path).isAbsolute()) {
      fullPath = "/sdcard/AIWROK/" + path;
    }
    if (!new java.io.File(fullPath).exists()) {
      cmdResp("CMD_RUN_SCRIPT", cmdId, false, "脚本不存在: " + fullPath);
      return;
    }

    try {
      // AIWROK 约束:用 thread.runJsFile() 加载外部脚本
      if (typeof thread === "undefined") {
            cmdResp("CMD_RUN_SCRIPT", cmdId, false, "thread 对象不可用");
            return;
      }
      var tid = "T" + nowMs() + "_" + Math.floor(Math.random() * 10000);
      // 构造成员:把 client_id、CMD_RUN_SCRIPT 的 args、STOP_ALL 标志路径放进脚本共享变量
      try { scriptShared = { CMD_ARGS: d.args || {}, STOP_FLAG: G.STOP_FLAG, CLIENT_ID: G.clientId, CMD_ID: cmdId }; } catch (eSh) {}

      var th = thread.runJsFile(fullPath);
      G.activeThreads = { thread: th, path: fullPath, startAt: nowSec(), timeoutSec: timeoutSec, cmdId: cmdId };

      // 守护协程:非阻塞链式 setTimeout 轮询(不占用派发线程,避免 AIWROK 杀脚本)
      (function watcherStep() {
            try {
                var rec = G.activeThreads;
                if (!rec) return; // 已经被 STOP_ALL 清理掉了
                var stillAlive = true;
                try {
                  // isAlive 在部分 AIWROK 版本里是字段不是方法,两种都兜住
                  if (th && typeof th.isAlive === "function") stillAlive = th.isAlive();
                  else if (th && typeof th.isAlive === "boolean") stillAlive = th.isAlive;
                  else stillAlive = true;
                } catch (eChk) { stillAlive = false; }

                var runSec = nowSec() - rec.startAt;
                var timeIsUp = runSec >= (rec.timeoutSec + 1);
                var shouldStop = !stillAlive || timeIsUp || G.stopAll;

                if (!shouldStop) {
                  setTimeout(watcherStep, 1000);
                  return;
                }

                // 超时或 STOP_ALL -> 停线程
                if (stillAlive && timeIsUp) {
                  try { if (th && th.stop) th.stop(); } catch (eStop) {}
                }
                var cost = nowSec() - rec.startAt;
                try { delete G.activeThreads; } catch (eDel) {}
                WSC.send({
                  type: "CLIENT_SCRIPT_RESULT",
                  cmdId: cmdId,
                  threadId: tid,
                  path: fullPath,
                  timeout: timeIsUp && stillAlive,
                  runtimeSec: cost,
                  stopAll: !!G.stopAll,
                  ts: nowSec()
                });
            } catch (eOuter) {
                printl("CMD_RUN_SCRIPT 守护协程异常(已兜住): " + (eOuter.message || eOuter));
            }
      })();

      cmdResp("CMD_RUN_SCRIPT", cmdId, true, "已启动", { threadId: tid, path: fullPath, timeoutSec: timeoutSec });
    } catch (e) {
      cmdResp("CMD_RUN_SCRIPT", cmdId, false, "启动异常: " + (e.message || e));
    }
});

// -------- CMD_STATUS:查状态 -------
WSC.on("CMD_STATUS", function (o) {
    try {
      var info = collectDeviceInfo();
      // 列活跃线程
      var threads = [];
      try {
            for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) {
                var t = G.activeThreads;
                threads.push({
                  id: k,
                  path: t.path,
                  runSec: nowSec() - t.startAt,
                  timeoutSec: t.timeoutSec
                });
            }
      } catch (e) {}
      info.threads = threads;
      cmdResp("CMD_STATUS", o && o.cmdId, true, "ok", info);
    } catch (e) {
      cmdResp("CMD_STATUS", o && o.cmdId, false, "状态异常: " + (e.message || e));
    }
});

// -------- CMD_TOAST:弹个消息 -------
WSC.on("CMD_TOAST", function (o) {
    var cmdId = o && o.cmdId;
    var msg = (o && o.data && o.data.msg != null) ? ("" + o.data.msg) : "";
    if (!msg) { cmdResp("CMD_TOAST", cmdId, false, "缺少 msg"); return; }
    try {
      if (typeof toast !== "undefined") { toast(msg); }
      cmdResp("CMD_TOAST", cmdId, true, "ok", { msg: msg });
    } catch (e) {
      cmdResp("CMD_TOAST", cmdId, false, "toast 异常: " + (e.message || e));
    }
});

// -------- CMD_CLEAR_CACHE:清除本地缓存 -------
WSC.on("CMD_CLEAR_CACHE", function (o) {
    var cmdId = o && o.cmdId;
    try {
      Cache.clearAll();
      // 顺便删 STOP_ALL 标志
      try {
            var f2 = new java.io.File(G.STOP_FLAG);
            if (f2.exists()) f2["delete"]();
      } catch (e2) {}
      G.stopAll = false;
      cmdResp("CMD_CLEAR_CACHE", cmdId, true, "ok");
    } catch (e) {
      cmdResp("CMD_CLEAR_CACHE", cmdId, false, "清除异常: " + (e.message || e));
    }
});

// -------- MANAGE_TASK_NEW:兼容原任务下发格式 -------
// 如果 data.action === "run_script" 就自动走 CMD_RUN_SCRIPT 逻辑;否则打印并回 ACK
WSC.on("MANAGE_TASK_NEW", function (o) {
    printl("[任务] 收到: " + JSON.stringify(o.data || o).slice(0, 160));
    try {
      var d = o.data || o;
      if (d && d.action === "run_script" && d.path) {
            WSC.fire("CMD_RUN_SCRIPT", { type: "CMD_RUN_SCRIPT", cmdId: (o.taskId || ("task_" + nowMs())), data: d });
            WSC.send({ type: "CLIENT_TASK_ACK", taskId: o.taskId || (d.taskId || ""), accepted: true, mode: "auto_run_script", data: d });
      } else {
            WSC.send({ type: "CLIENT_TASK_ACK", taskId: o.taskId || (d && d.taskId || ""), accepted: true });
      }
    } catch (e) {
      WSC.send({ type: "CLIENT_TASK_ACK", taskId: o.taskId || "", accepted: false, error: (e.message || e) });
    }
});

// -------- MANAGE_CMD_STOP_ALL:真停止逻辑(不是只写个文件就完事)-------
// 动作:1) 设标志 + 写文件 2) 停止所有活跃线程 3) 心跳停 4) 上报 CLIENT_STOPPING 5) 2 秒后 WSC.stop()
WSC.on("MANAGE_CMD_STOP_ALL", function (o) {
    printl("[指令] STOP_ALL 广播,开始完整清理");
    try { G.stopAll = true; } catch (e) {}
    try { G.isAllowReConnect = false; } catch (e) {}

    // 1) 写标志文件(供其他脚本读)
    try {
      var f = new java.io.File(G.STOP_FLAG);
      if (!f.exists()) f.createNewFile();
    } catch (e) {}

    // 2) 停止所有 CMD_RUN_SCRIPT 启动的活跃线程
    var stoppedCnt = 0;
    try {
      for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) {
            try {
                var tObj = G.activeThreads;
                if (tObj && tObj.thread && tObj.thread.stop) tObj.thread.stop();
                stoppedCnt++;
            } catch (eSt) {}
      }
    } catch (e) {}
    try { G.activeThreads = {}; } catch (e) {}

    // 3) 立即发 CLIENT_STOPPING
    try {
      WSC.send({
            type: "CLIENT_STOPPING",
            reason: "MANAGE_CMD_STOP_ALL",
            stoppedThreads: stoppedCnt,
            runtimeSec: nowSec() - G.startTime,
            ts: nowSec()
      });
    } catch (eS) {}

    // 4) 2 秒后彻底关 WS 与保活(链式 setTimeout 全部兜 try-catch,避免 AIWROK 判"异常未捕获"直接杀)
    try {
      setTimeout(function () {
            try {
                if (G.heartbeatTimer) { clearTimeout(G.heartbeatTimer); G.heartbeatTimer = null; }
            } catch (e1) {}
            try { WSC.stop(); } catch (e2) {}
            try { printl("[指令] STOP_ALL 清理完成,已断开 WS"); } catch (e3) {}
      }, 2000);
    } catch (eOuter) {}
});

// -------- SERVER_PING:心跳回包 -------
WSC.on("SERVER_PING", function (o) {
    WSC.send({
      type: "CLIENT_PONG",
      echo: o,
      runtimeSec: nowSec() - G.startTime,
      ts: nowSec()
    });
});

// =====================================================================
//远程停止接收专用线程(硬约束要求)
//单独轮询 STOP_ALL 标志文件;一旦存在就触发完整停止逻辑
// =====================================================================
function startStopReceiver() {
    (function loop() {
      try {
            if (!G.running) return;
            var f = new java.io.File(G.STOP_FLAG);
            if (f.exists() && !G.stopAll) {
                printl("[接收器] 检测到 STOP_ALL 标志文件,触发停止");
                try { f["delete"](); } catch (eDel) {}
                // 模拟收到一条 MANAGE_CMD_STOP_ALL 消息,走同一套清理逻辑
                WSC.fire("MANAGE_CMD_STOP_ALL", { type: "MANAGE_CMD_STOP_ALL", from: "local_flag" });
            }
      } catch (eOuter) {
            try { printl("[接收器] 轮询异常(已兜住): " + (eOuter.message || eOuter)); } catch (eL) {}
      }
      try { setTimeout(loop, CFG.stopReceiverIntervalMs); }
      catch (eTimer) {
            // runTime.setTimeout 兜底(仅当 setTimeout 不存在时用)
            try { runTime.setTimeout(loop, CFG.stopReceiverIntervalMs); } catch (eRT) {}
      }
    })();
}

// ---------- 启动入口(全局 try-catch + setTimeout 不阻塞,杜绝 AIWROK 随便『正在停止』)----------
function main() {
    try {
      // 启动先清一次 STOP_ALL 标志,避免上一次的残余
      try {
            var _f = new java.io.File(G.STOP_FLAG);
            if (_f.exists()) _f["delete"]();
      } catch (e) {}
      G.stopAll = false;

      printl("==========================================");
      printl("AIWROK WebSocket 客户端 启动");
      printl("平台: " + CFG.platform + "   App: " + CFG.appName);
      printl("服务端: " + CFG.serverUrl);
      printl("恢复 client_id: " + (G.clientId || "新注册"));
      printl("可用指令: CMD_SCREENSHOT / CMD_CLICK / CMD_SWIPE / CMD_KEY");
      printl("         CMD_RUN_SCRIPT / CMD_STATUS / CMD_TOAST / CMD_CLEAR_CACHE");
      printl("==========================================");
      connectWebSocket();
      startStopReceiver();

      // 主线程保活(链式 setTimeout,非阻塞,全包 try-catch)
      var keepAliveTick = 0;
      (function keepAlive() {
            try {
                if (!G.running) { printl("保活线程退出"); return; }
                keepAliveTick++;
                if (keepAliveTick % 20 === 0) {
                  printl("[保活] client_id=" + (G.clientId || "N/A") + " 重试=" + G.reConnectTimes + " 运行=" + (nowSec() - G.startTime) + "s stopAll=" + G.stopAll);
                }
            } catch (eInner) {
                try { printl("[保活] 一次循环异常(已兜住): " + (eInner.message || eInner)); } catch (eL) {}
            }
            try { setTimeout(keepAlive, 1000); }
            catch (eTimer) {
                try { runTime.setTimeout(keepAlive, 1000); } catch (eRT) {}
            }
      })();
    } catch (eTop) {
      // 任何 main 里未捕获异常都兜住,不让 AIWROK 直接给『正在停止』
      try { printl("⚠️main() 顶层异常(已兜住): " + (eTop.message || eTop)); } catch (eL) {}
      // 尝试继续重连
      try { setTimeout(function () { try { connectWebSocket(); } catch (e) {} }, 5000); } catch (e) {}
    }
}

main();


页: [1]
查看完整版本: WebSocket带截图/点击/滑屏/按键/跑脚本/状态查询