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

402

主题

193

回帖

2507

积分

管理员

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

QQ截图20260824132624.png


QQ截图20260824132557.png


QQ截图20260824132547.png


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



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

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

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

  55. // ---------- 工具函数 ----------
  56. function nowSec() {
  57.     return Math.floor((new Date()).getTime() / 1000);
  58. }
  59. function nowMs() {
  60.     return (new Date()).getTime();
  61. }
  62. function jitter(ms) {
  63.     return Math.floor(ms * (0.85 + Math.random() * 0.3));
  64. }
  65. function readTextFile(path) {
  66.     try {
  67.         var br = new java.io.BufferedReader(
  68.             new java.io.InputStreamReader(new java.io.FileInputStream(path), "utf-8"));
  69.         var sb = new java.lang.StringBuilder();
  70.         var line;
  71.         while ((line = br.readLine()) != null) sb.append(line).append("\n");
  72.         br.close();
  73.         return "" + sb.toString();
  74.     } catch (e) { return null; }
  75. }
  76. function deleteFileOrDir(p) {
  77.     try {
  78.         var f = new java.io.File(p);
  79.         if (!f.exists()) return true;
  80.         if (f.isDirectory()) {
  81.             var list = f.listFiles();
  82.             for (var i = 0; i < list.length; i++) deleteFileOrDir(list[i].getAbsolutePath());
  83.         }
  84.         return f["delete"]();
  85.     } catch (e) { return false; }
  86. }
  87. function humanSize(bytes) {
  88.     if (bytes < 1024) return bytes + "B";
  89.     if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "KB";
  90.     return (bytes / 1024 / 1024).toFixed(2) + "MB";
  91. }

  92. // ---------- 本地缓存封装 ----------
  93. var Cache = {
  94.     BASE: "/sdcard/AIWROK/wscache",
  95.     ensure: function () {
  96.         try {
  97.             var f = new java.io.File(this.BASE);
  98.             if (!f.exists()) f.mkdirs();
  99.         } catch (e) {}
  100.     },
  101.     read: function (key) {
  102.         this.ensure();
  103.         try {
  104.             var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
  105.             return JSON.parse(readTextFile(p));
  106.         } catch (e) { return null; }
  107.     },
  108.     write: function (key, val) {
  109.         this.ensure();
  110.         try {
  111.             var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
  112.             var fw = new java.io.FileWriter(p);
  113.             fw.write(JSON.stringify(val));
  114.             fw.close();
  115.             return true;
  116.         } catch (e) { return false; }
  117.     },
  118.     clear: function (key) {
  119.         try {
  120.             var p = this.BASE + "/" + encodeURIComponent(key) + ".json";
  121.             var f = new java.io.File(p);
  122.             if (f.exists()) f["delete"]();
  123.         } catch (e) {}
  124.     },
  125.     clearAll: function () {
  126.         return deleteFileOrDir(this.BASE);
  127.     }
  128. };

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

  131. // ---------- 统一 CMD_RESP ----------
  132. function cmdResp(cmdName, cmdId, ok, msg, data) {
  133.     var obj = {
  134.         type: "CMD_RESP",
  135.         cmd: cmdName,
  136.         cmdId: cmdId || ("cid_" + nowMs() + "_" + Math.floor(Math.random() * 1000)),
  137.         ok: !!ok,
  138.         msg: msg || (ok ? "ok" : "failed"),
  139.         ts: nowSec()
  140.     };
  141.     if (typeof data !== "undefined") obj.data = data;
  142.     if (G.clientId) obj.client_id = G.clientId;
  143.     WSC.send(obj);
  144.     return obj;
  145. }

  146. // ---------- 消息派发器 ----------
  147. var WSC = {
  148.     on: function (type, fn) {
  149.         if (!G.dispatcher[type]) G.dispatcher[type] = [];
  150.         G.dispatcher[type].push(fn);
  151.     },
  152.     off: function (type, fn) {
  153.         var arr = G.dispatcher[type] || [];
  154.         for (var i = arr.length - 1; i >= 0; i--) if (arr[i] === fn) arr.splice(i, 1);
  155.     },
  156.     fire: function (type, msgObj) {
  157.         var arr = G.dispatcher[type] || [];
  158.         for (var i = 0; i < arr.length; i++) {
  159.             try {
  160.                 arr[i](msgObj);
  161.             } catch (e) {
  162.                 printl("派发[" + type + "]异常: " + (e.message || e));
  163.             }
  164.         }
  165.     },
  166.     send: function (obj) {
  167.         if (!G.ws) return false;
  168.         if (G.clientId && obj.clientId == null) obj.client_id = G.clientId;
  169.         if (obj.timestamp == null) obj.timestamp = nowSec();
  170.         if (obj.platform == null) obj.platform = CFG.platform;
  171.         var txt = JSON.stringify(obj);
  172.         try {
  173.             G.ws.send(txt);
  174.             if (CFG.debug) {
  175.                 var head = (obj.type || "?") + " ";
  176.                 // 截图回包 base64 太长,只打摘要
  177.                 if (obj.type === "CMD_RESP" && obj.data && obj.data.base64) {
  178.                     printl("SEND -> " + head + "[" + obj.cmd + "] base64_len=" + obj.data.base64.length + " (" + obj.data.w + "x" + obj.data.h + " q=" + obj.data.quality + ")");
  179.                 } else {
  180.                     printl("SEND -> " + head + txt.slice(0, 160));
  181.                 }
  182.             }
  183.             return true;
  184.         } catch (e) {
  185.             printl("发送失败: " + (e.message || e));
  186.             return false;
  187.         }
  188.     },
  189.     stop: function () {
  190.         G.running = false;
  191.         G.isAllowReConnect = false;
  192.         if (G.heartbeatTimer) {
  193.             try { clearTimeout(G.heartbeatTimer); } catch (e) {}
  194.             G.heartbeatTimer = null;
  195.         }
  196.         try { if (G.ws) G.ws.close(); } catch (e) {}
  197.     }
  198. };

  199. // ---------- 心跳(安全版:链式 setTimeout 全包 try-catch,避免 AIWROK 未捕获异常杀脚本)----------
  200. function startHeartbeat() {
  201.     if (G.heartbeatTimer) {
  202.         try { clearTimeout(G.heartbeatTimer); } catch (e) {}
  203.     }
  204.     (function tick() {
  205.         try {
  206.             if (!G.running) return;
  207.             var imei = "";
  208.             try { if (typeof device !== "undefined") imei = device.getIMEI() || ""; } catch (e) {}
  209.             WSC.send({
  210.                 type: "CLIENT_HEARTBEAT",
  211.                 imei: imei,
  212.                 platform: CFG.platform,
  213.                 app: CFG.appName,
  214.                 runtimeSec: nowSec() - G.startTime,
  215.                 stopAll: !!G.stopAll
  216.             });
  217.         } catch (eOuter) {
  218.             try { printl("心跳异常(已兜住): " + (eOuter.message || eOuter)); } catch (eL) {}
  219.         }
  220.         try {
  221.             G.heartbeatTimer = setTimeout(tick, G.heartbeatIntervalMs);
  222.         } catch (eTimer) {
  223.             try { printl("心跳下一次调度失败: " + (eTimer.message || eTimer)); } catch (eL) {}
  224.         }
  225.     })();
  226. }

  227. // ---------- 设备信息采集(连接时用,也被 CMD_STATUS 复用)----------
  228. function collectDeviceInfo() {
  229.     var info = {
  230.         platform: CFG.platform,
  231.         app: CFG.appName,
  232.         imei: "",
  233.         android: "",
  234.         model: "",
  235.         brand: "",
  236.         w: 1080,
  237.         h: 2400,
  238.         dpi: 0,
  239.         frontPkg: "",
  240.         internalMB: 0,
  241.         sdMB: 0,
  242.         clientId: G.clientId,
  243.         runtimeSec: nowSec() - G.startTime,
  244.         stopAll: !!G.stopAll,
  245.         activeScriptCount: 0
  246.     };
  247.     try { if (typeof device !== "undefined") info.imei = device.getIMEI() || ""; } catch (e) {}
  248.     try { info.android = java.lang.System.getProperty("os.version") + ""; } catch (e) {}
  249.     try { info.model = android.os.Build.MODEL + ""; } catch (e) {}
  250.     try { info.brand = android.os.Build.BRAND + ""; } catch (e) {}
  251.     try {
  252.         if (typeof screen !== "undefined") {
  253.             info.w = screen.getScreenWidth();
  254.             info.h = screen.getScreenHeight();
  255.             try { info.dpi = screen.getDensityDpi(); } catch (e2) {}
  256.         }
  257.     } catch (e) {}
  258.     try {
  259.         var am = context.getSystemService(android.content.Context.ACTIVITY_SERVICE);
  260.         info.frontPkg = am.getRunningTasks(1).get(0).topActivity.getPackageName() + "";
  261.     } catch (e) {}
  262.     try {
  263.         var stat = new android.os.StatFs(android.os.Environment.getDataDirectory().getAbsolutePath());
  264.         info.internalMB = Math.floor((stat.getAvailableBytes() * 1.0) / 1024 / 1024);
  265.     } catch (e) {}
  266.     try {
  267.         var sd = android.os.Environment.getExternalStorageDirectory();
  268.         var s2 = new android.os.StatFs(sd.getAbsolutePath());
  269.         info.sdMB = Math.floor((s2.getAvailableBytes() * 1.0) / 1024 / 1024);
  270.     } catch (e) {}
  271.     var cnt = 0;
  272.     try { for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) cnt++; } catch (e) {}
  273.     info.activeScriptCount = cnt;
  274.     return info;
  275. }

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

  281.     var info = collectDeviceInfo();
  282.     WSC.send({
  283.         type: "CLIENT_REGISTER",
  284.         platform: info.platform,
  285.         imei: info.imei,
  286.         app: info.app,
  287.         lastClientId: G.clientId,
  288.         screen: info.w + "x" + info.h,
  289.         dpi: info.dpi,
  290.         android: info.android,
  291.         model: info.model,
  292.         brand: info.brand,
  293.         internalMB: info.internalMB,
  294.         sdMB: info.sdMB
  295.     });
  296.     startHeartbeat();
  297.     WSC.fire("_connected", info);
  298. }

  299. // ---------- 事件回调:onTextMessage ----------
  300. function wsOnTextMessage(msg) {
  301.     var s = "" + msg;
  302.     if (CFG.debug) printl("RECV <- " + s.slice(0, 200));
  303.     var obj = null;
  304.     try { obj = JSON.parse(s); } catch (e) {
  305.         printl("非JSON消息,忽略: " + s.slice(0, 120));
  306.         WSC.fire("_text", { raw: s });
  307.         return;
  308.     }
  309.     if (obj && obj.type === "SERVER_INIT") {
  310.         if (obj.client_id) {
  311.             G.clientId = obj.client_id;
  312.             Cache.write("wsc_client_id", { id: G.clientId });
  313.             printl("服务端分配 client_id: " + G.clientId + "(已本地保存)");
  314.         }
  315.         if (obj.msg) printl("服务端消息: " + obj.msg);
  316.     }
  317.     if (obj && obj.type) {
  318.         WSC.fire(obj.type, obj);
  319.         WSC.fire("_any", obj);
  320.     }
  321. }

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

  327. // ---------- 事件回调:onDisconnected ----------
  328. function wsOnDisconnected() {
  329.     printl("✗ 连接已断开");
  330.     if (G.heartbeatTimer) {
  331.         try { clearTimeout(G.heartbeatTimer); } catch (e) {}
  332.         G.heartbeatTimer = null;
  333.     }
  334.     scheduleReconnect();
  335. }

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

  343.     var ws = null;
  344.     try { ws = new websocket(); }
  345.     catch (e) {
  346.         printl("new websocket() 失败: " + (e.message || e));
  347.         scheduleReconnect();
  348.         return;
  349.     }

  350.     G.ws = ws;

  351.     // 挂事件:AIWROK 新版 ws.event() 优先,失败兜底属性赋值
  352.     var eventOk = false;
  353.     try {
  354.         ws.event(wsOnConnected, wsOnTextMessage, wsOnConnectError, wsOnDisconnected);
  355.         eventOk = true;
  356.         if (CFG.debug) printl("ws.event() 挂事件成功");
  357.     } catch (e1) {
  358.         if (CFG.debug) printl("ws.event() 失败: " + (e1.message || e1) + " — 尝试属性赋值");
  359.     }
  360.     if (!eventOk) {
  361.         try {
  362.             ws.onConnected = wsOnConnected;
  363.             ws.onTextMessage = wsOnTextMessage;
  364.             ws.onConnectError = wsOnConnectError;
  365.             ws.onDisconnected = wsOnDisconnected;
  366.             if (CFG.debug) printl("属性赋值挂事件成功");
  367.         } catch (e2) {
  368.             printl("两种事件挂法都失败: " + (e2.message || e2));
  369.         }
  370.     }

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

  385. // ---------- 指数退避 + 抖动重连(安全版:setTimeout 最外层包 try-catch)----------
  386. function scheduleReconnect() {
  387.     try {
  388.         if (!G.running) return;
  389.         if (!G.isAllowReConnect) return;
  390.         G.reConnectTimes += 1;
  391.         if (!G.isInfiniteReConnect && G.reConnectTimes > G.reConnectMaxTimes) {
  392.             printl("⚠️  超过最大重试次数(" + G.reConnectMaxTimes + "),停止重连");
  393.             return;
  394.         }
  395.         G.connectDelayMs = Math.min(G.connectDelayMs * G.multiplier, G.maxConnectDelayMs);
  396.         var waitMs = jitter(G.connectDelayMs);
  397.         printl("下次重连等待 " + (waitMs / 1000).toFixed(2) + "s  (封顶" + (G.maxConnectDelayMs / 1000) + "s)");
  398.         setTimeout(function () {
  399.             try { connectWebSocket(); } catch (e) {
  400.                 printl("重连回调异常(已兜住): " + (e.message || e));
  401.                 scheduleReconnect();
  402.             }
  403.         }, waitMs);
  404.     } catch (eOuter) {
  405.         printl("scheduleReconnect 外层异常: " + (eOuter.message || eOuter));
  406.     }
  407. }

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

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

  423.         // 1) fastCapture 截图
  424.         var bmp = null;
  425.         try {
  426.             if (typeof image !== "undefined") {
  427.                 bmp = image.fastCapture();
  428.             } else {
  429.                 cmdResp("CMD_SCREENSHOT", cmdId, false, "image 对象不可用");
  430.                 return;
  431.             }
  432.         } catch (e) {
  433.             cmdResp("CMD_SCREENSHOT", cmdId, false, "fastCapture 异常: " + (e.message || e));
  434.             return;
  435.         }
  436.         if (!bmp) { cmdResp("CMD_SCREENSHOT", cmdId, false, "截图返回空"); return; }

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

  439.         // 2) 超宽就等比缩放
  440.         if (w > maxW) {
  441.             var ratio = maxW * 1.0 / w;
  442.             var newH = Math.floor(h * ratio);
  443.             try {
  444.                 var scaled = android.graphics.Bitmap.createScaledBitmap(bmp, maxW, newH, true);
  445.                 try { bmp.recycle(); } catch (e) {}
  446.                 bmp = scaled;
  447.                 w = maxW;
  448.                 h = newH;
  449.             } catch (e2) {}
  450.         }

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

  458.         cmdResp("CMD_SCREENSHOT", cmdId, true, "ok", {
  459.             base64: "" + b64,
  460.             w: w,
  461.             h: h,
  462.             quality: quality,
  463.             sizeKb: Math.floor(bytes.length / 1024),
  464.             costMs: nowMs() - t0
  465.         });
  466.     } catch (eBig) {
  467.         cmdResp("CMD_SCREENSHOT", cmdId, false, "截图总异常: " + (eBig.message || eBig));
  468.     }
  469. });

  470. // -------- CMD_CLICK:点击 {x, y} -------
  471. WSC.on("CMD_CLICK", function (o) {
  472.     var cmdId = o.cmdId;
  473.     var x = (o && o.data && o.data.x != null) ? parseInt(o.data.x, 10) : NaN;
  474.     var y = (o && o.data && o.data.y != null) ? parseInt(o.data.y, 10) : NaN;
  475.     if (isNaN(x) || isNaN(y) || x < 0 || y < 0) {
  476.         cmdResp("CMD_CLICK", cmdId, false, "缺少有效 x/y 坐标");
  477.         return;
  478.     }
  479.     try {
  480.         if (typeof action !== "undefined") {
  481.             action.click(x, y);
  482.             cmdResp("CMD_CLICK", cmdId, true, "ok", { x: x, y: y });
  483.         } else {
  484.             cmdResp("CMD_CLICK", cmdId, false, "action 对象不可用");
  485.         }
  486.     } catch (e) {
  487.         cmdResp("CMD_CLICK", cmdId, false, "点击异常: " + (e.message || e));
  488.     }
  489. });

  490. // -------- CMD_SWIPE:滑屏 {x1,y1,x2,y2,duration} -------
  491. WSC.on("CMD_SWIPE", function (o) {
  492.     var cmdId = o.cmdId;
  493.     var d = o && o.data ? o.data : {};
  494.     var x1 = parseInt(d.x1, 10), y1 = parseInt(d.y1, 10);
  495.     var x2 = parseInt(d.x2, 10), y2 = parseInt(d.y2, 10);
  496.     var dur = d.duration ? parseInt(d.duration, 10) : 300;
  497.     if ([x1, y1, x2, y2].some(function (n) { return isNaN(n) || n < 0; })) {
  498.         cmdResp("CMD_SWIPE", cmdId, false, "坐标缺失");
  499.         return;
  500.     }
  501.     try {
  502.         if (typeof action !== "undefined") {
  503.             action.swipe(x1, y1, x2, y2, dur);
  504.             cmdResp("CMD_SWIPE", cmdId, true, "ok", { x1: x1, y1: y1, x2: x2, y2: y2, duration: dur });
  505.         } else {
  506.             cmdResp("CMD_SWIPE", cmdId, false, "action 对象不可用");
  507.         }
  508.     } catch (e) {
  509.         cmdResp("CMD_SWIPE", cmdId, false, "滑屏异常: " + (e.message || e));
  510.     }
  511. });

  512. // -------- CMD_KEY:按键 -------
  513. // data.key 支持: "BACK" / "HOME" / "RECENT" / "POWER" / "NOTIFICATION" / 数字 keyCode
  514. WSC.on("CMD_KEY", function (o) {
  515.     var cmdId = o.cmdId;
  516.     var key = (o && o.data && o.data.key != null) ? ("" + o.data.key) : "";
  517.     if (!key) { cmdResp("CMD_KEY", cmdId, false, "缺少 key"); return; }
  518.     try {
  519.         // 方式1:action 快捷方法
  520.         if (typeof action !== "undefined") {
  521.             var k = key.toUpperCase();
  522.             if (k === "BACK") { action.back(); cmdResp("CMD_KEY", cmdId, true, "BACK"); return; }
  523.             if (k === "HOME") { action.home(); cmdResp("CMD_KEY", cmdId, true, "HOME"); return; }
  524.             if (k === "RECENT" || k === "RECENTS" || k === "APP_SWITCH") {
  525.                 action.recents(); cmdResp("CMD_KEY", cmdId, true, "RECENT"); return;
  526.             }
  527.         }
  528.         // 方式2:systempress 数字 keyCode
  529.         var code = parseInt(key, 10);
  530.         if (!isNaN(code) && code > 0) {
  531.             if (typeof systempress !== "undefined") {
  532.                 systempress(code);
  533.                 cmdResp("CMD_KEY", cmdId, true, "keyCode " + code);
  534.                 return;
  535.             }
  536.         }
  537.         // 方式3:shell input keyevent(兜底)
  538.         var shellKey = null;
  539.         var k2 = key.toUpperCase();
  540.         if (k2 === "BACK") shellKey = "4";
  541.         else if (k2 === "HOME") shellKey = "3";
  542.         else if (k2 === "RECENT" || k2 === "RECENTS") shellKey = "187";
  543.         else if (k2 === "POWER") shellKey = "26";
  544.         else if (k2 === "NOTIFICATION") shellKey = "83";
  545.         else if (!isNaN(code) && code > 0) shellKey = "" + code;
  546.         if (shellKey) {
  547.             try {
  548.                 java.lang.Runtime.getRuntime().exec(["su", "-c", "input keyevent " + shellKey]).waitFor();
  549.                 cmdResp("CMD_KEY", cmdId, true, "shell key " + key);
  550.                 return;
  551.             } catch (eS) {}
  552.             try {
  553.                 java.lang.Runtime.getRuntime().exec(["input", "keyevent", shellKey]).waitFor();
  554.                 cmdResp("CMD_KEY", cmdId, true, "shell key(no su) " + key);
  555.                 return;
  556.             } catch (eS2) {}
  557.         }
  558.         cmdResp("CMD_KEY", cmdId, false, "不支持的 key: " + key);
  559.     } catch (e) {
  560.         cmdResp("CMD_KEY", cmdId, false, "按键异常: " + (e.message || e));
  561.     }
  562. });

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

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

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

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

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

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

  607.                 if (!shouldStop) {
  608.                     setTimeout(watcherStep, 1000);
  609.                     return;
  610.                 }

  611.                 // 超时或 STOP_ALL -> 停线程
  612.                 if (stillAlive && timeIsUp) {
  613.                     try { if (th && th.stop) th.stop(); } catch (eStop) {}
  614.                 }
  615.                 var cost = nowSec() - rec.startAt;
  616.                 try { delete G.activeThreads[tid]; } catch (eDel) {}
  617.                 WSC.send({
  618.                     type: "CLIENT_SCRIPT_RESULT",
  619.                     cmdId: cmdId,
  620.                     threadId: tid,
  621.                     path: fullPath,
  622.                     timeout: timeIsUp && stillAlive,
  623.                     runtimeSec: cost,
  624.                     stopAll: !!G.stopAll,
  625.                     ts: nowSec()
  626.                 });
  627.             } catch (eOuter) {
  628.                 printl("CMD_RUN_SCRIPT 守护协程异常(已兜住): " + (eOuter.message || eOuter));
  629.             }
  630.         })();

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

  636. // -------- CMD_STATUS:查状态 -------
  637. WSC.on("CMD_STATUS", function (o) {
  638.     try {
  639.         var info = collectDeviceInfo();
  640.         // 列活跃线程
  641.         var threads = [];
  642.         try {
  643.             for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) {
  644.                 var t = G.activeThreads[k];
  645.                 threads.push({
  646.                     id: k,
  647.                     path: t.path,
  648.                     runSec: nowSec() - t.startAt,
  649.                     timeoutSec: t.timeoutSec
  650.                 });
  651.             }
  652.         } catch (e) {}
  653.         info.threads = threads;
  654.         cmdResp("CMD_STATUS", o && o.cmdId, true, "ok", info);
  655.     } catch (e) {
  656.         cmdResp("CMD_STATUS", o && o.cmdId, false, "状态异常: " + (e.message || e));
  657.     }
  658. });

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

  671. // -------- CMD_CLEAR_CACHE:清除本地缓存 -------
  672. WSC.on("CMD_CLEAR_CACHE", function (o) {
  673.     var cmdId = o && o.cmdId;
  674.     try {
  675.         Cache.clearAll();
  676.         // 顺便删 STOP_ALL 标志
  677.         try {
  678.             var f2 = new java.io.File(G.STOP_FLAG);
  679.             if (f2.exists()) f2["delete"]();
  680.         } catch (e2) {}
  681.         G.stopAll = false;
  682.         cmdResp("CMD_CLEAR_CACHE", cmdId, true, "ok");
  683.     } catch (e) {
  684.         cmdResp("CMD_CLEAR_CACHE", cmdId, false, "清除异常: " + (e.message || e));
  685.     }
  686. });

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

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

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

  714.     // 2) 停止所有 CMD_RUN_SCRIPT 启动的活跃线程
  715.     var stoppedCnt = 0;
  716.     try {
  717.         for (var k in G.activeThreads) if (G.activeThreads.hasOwnProperty(k)) {
  718.             try {
  719.                 var tObj = G.activeThreads[k];
  720.                 if (tObj && tObj.thread && tObj.thread.stop) tObj.thread.stop();
  721.                 stoppedCnt++;
  722.             } catch (eSt) {}
  723.         }
  724.     } catch (e) {}
  725.     try { G.activeThreads = {}; } catch (e) {}

  726.     // 3) 立即发 CLIENT_STOPPING
  727.     try {
  728.         WSC.send({
  729.             type: "CLIENT_STOPPING",
  730.             reason: "MANAGE_CMD_STOP_ALL",
  731.             stoppedThreads: stoppedCnt,
  732.             runtimeSec: nowSec() - G.startTime,
  733.             ts: nowSec()
  734.         });
  735.     } catch (eS) {}

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

  747. // -------- SERVER_PING:心跳回包 -------
  748. WSC.on("SERVER_PING", function (o) {
  749.     WSC.send({
  750.         type: "CLIENT_PONG",
  751.         echo: o,
  752.         runtimeSec: nowSec() - G.startTime,
  753.         ts: nowSec()
  754.     });
  755. });

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

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

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

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

  824. main();
复制代码



举报 回复