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]