|
|
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[i].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[type]) G.dispatcher[type] = [];
- G.dispatcher[type].push(fn);
- },
- off: function (type, fn) {
- var arr = G.dispatcher[type] || [];
- for (var i = arr.length - 1; i >= 0; i--) if (arr[i] === fn) arr.splice(i, 1);
- },
- fire: function (type, msgObj) {
- var arr = G.dispatcher[type] || [];
- for (var i = 0; i < arr.length; i++) {
- try {
- arr[i](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 ([x1, y1, x2, y2].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[tid] = { thread: th, path: fullPath, startAt: nowSec(), timeoutSec: timeoutSec, cmdId: cmdId };
- // 守护协程:非阻塞链式 setTimeout 轮询(不占用派发线程,避免 AIWROK 杀脚本)
- (function watcherStep() {
- try {
- var rec = G.activeThreads[tid];
- 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[tid]; } 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[k];
- 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[k];
- 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();
复制代码
|
|