 | |  |  | 实战使用桌面控制WebSocket
- /**
- * WebSocket 高级技巧合集 - AIWROK 版
- *
- * ws-multi-pool 连接池 / JetSocket / Binary Transfer Protocol /
- * Android WebSocket 实战 / 大文件分片上传)
- *
- * 包含 8 大特殊模式:
- * 1. ACK 确认 + 超时重传 —— 消息不丢(仿 TCP 可靠投递)
- * 2. 请求-响应配对 —— 发指令等回应(仿 HTTP 请求模式,带超时)
- * 3. 消息序列号 + 断线补偿 —— 重连后补发缺失消息
- * 4. 离线消息队列 —— 断线缓存,重连后自动 flush
- * 5. 分片传输大截图 —— 64KB 分块,避免大帧阻塞
- * 6. 自适应心跳 —— 弱网拉长心跳间隔,省电防断
- * 7. 背压控制 —— 发送速率限制,防止淹没对端
- * 8. 频道订阅模式 —— 动态订阅/取消订阅事件流
- *
- * AIWROK 原生 WebSocket API:
- * setSocketID(String) / event(onopen,onmsg,onerr,onclose) / connet(String) / send(Object) / close()
- * 定时器:runTime.setTimeout(fn, ms) / runTime.stopTimeout(id)
- */
- // ============================================================
- // 配置
- // ============================================================
- var CONFIG = {
- // WebSocket 服务器的局域网 IP 地址,需改为你 PC 的实际 IP
- // 查看方式:Windows 下运行 ipconfig → 找 IPv4 地址(通常是 192.168.x.x)
- // ⚠️ 手机和 PC 必须连接到同一个 WiFi 网段
- SERVER_IP: "192.168.10.104",
- // WebSocket 服务器监听的端口号,需与服务器端保持一致
- // 本项目配套的 WebSocket测试服务器.py 默认使用 8886
- SERVER_PORT: "8886",
- // WebSocket 连接的 URL 路径(根路径为 "/")
- // 如果服务器在特定路径上提供服务(如 "/ws"、"/api/v1/socket"),修改此处
- // 最终连接地址格式: ws://{SERVER_IP}:{SERVER_PORT}{WS_PATH}
- WS_PATH: "/",
- // 1. ACK 确认
- ACK_TIMEOUT_MS: 5000, // 等待 ACK 超时
- ACK_MAX_RETRY: 3, // 最多重传 3 次
- // 2. 请求-响应配对
- REQ_TIMEOUT_MS: 10000, // 请求超时 10 秒
- // 3. 序列号
- RESEND_ON_RECONNECT: true, // 重连后是否请求补发缺失消息
- // 4. 离线队列
- OFFLINE_QUEUE_MAX: 1000, // 离线队列最大缓存条数(分片传输时需要较大缓冲)
- // 5. 分片传输
- CHUNK_SIZE: 65536, // 64KB 每片(RFC 标准分片大小)
- LARGE_IMG_THRESHOLD_KB: 100, // 超过 100KB 的截图走分片
- CHUNK_RETAIN_LAST_MS: 60000, // 保留最近一次分片原始数据用于重传,默认 60 秒
- // 6. 自适应心跳
- HEARTBEAT_MIN_MS: 15000, // 网络好时 15 秒
- HEARTBEAT_MAX_MS: 60000, // 网络差时 60 秒
- HEARTBEAT_TIMEOUT_MS: 15000,
- NETWORK_CHECK_INTERVAL_MS: 30000, // 每 30 秒评估一次网络质量
- // 7. 背压
- SEND_RATE_LIMIT_MS: 5, // 两次发送之间最少间隔 5ms(≈200条/秒上限,分片不阻塞)
- SEND_BURST_MAX: 50, // 允许突发 50 条(一次 23 片不被背压限流)
- // 8. 截图参数(降低大尺寸手机的带宽消耗)
- SCREENSHOT_JPEG_QUALITY: 80, // 0-100,越高质量越好体积越大
- SCREENSHOT_MAX_WIDTH: 0, // 最大宽度(像素),0=不缩放;建议低端机或省流量设 1080
- // 9. 频道订阅
- AUTO_SUBSCRIBE_CHANNELS: ["system", "cmd", "screenshot"],
- // 通用
- MAX_RETRY: 999,
- RETRY_BASE_MS: 2000,
- RETRY_MAX_MS: 30000,
- DEBUG_LOG: true, // 本脚本为演示,默认开日志
- SHOW_LOG_WINDOW: false,
- // ============================================================
- // 降级模式(默认开启,避免 AIWROK 环境 ACK 强制 + 心跳过严导致的混乱)
- // RELAXED_MODE = true → 所有消息默认 no_ack、ACK 重传不报错、心跳超时只警告不主动断
- // RELAXED_MODE = false → 完全启用 TCP 式可靠投递(需要服务器完全严格匹配协议)
- // ============================================================
- RELAXED_MODE: true,
- RELAXED_ACK_REQUIRED: false, // false = 不强制要求 ACK,超时直接丢弃,不打印 ERROR
- RELAXED_HEARTBEAT_SOFT_FAIL: true // true = 心跳超时仅降级网络质量,不主动 close
- };
- // ============================================================
- // 日志
- // ============================================================
- function LOG(tag, msg) {
- if (!CONFIG.DEBUG_LOG && (tag === "DEBUG" || tag === "RECV" || tag === "SEND")) return;
- try { printl("[" + tag + "] " + msg); } catch (e) {}
- }
- if (CONFIG.SHOW_LOG_WINDOW) {
- try { logWindow.show(); } catch (e) {}
- try { logWindow.setNoClickModel(); } catch (e) {}
- }
- // ============================================================
- // 全局状态
- // ============================================================
- var G = {
- ws: null,
- socketID: "phone_" + new Date().getTime(),
- connected: false,
- retryCount: 0,
- running: true,
- seq: 0, // 发送序列号
- lastReceivedSeq: 0, // 收到服务端消息的最后序列号
- // 1. ACK 确认
- pendingAcks: {}, // seq -> { msg, retryCount, timerId, sentAt }
- // 2. 请求-响应配对
- pendingRequests: {}, // reqId -> { resolve, reject, timerId }
- // 4. 离线队列
- offlineQueue: [],
- // 5. 分片传输
- chunkTransfers: {}, // transferId -> { chunks:[], totalChunks, received, ts }
- lastSentChunks: {}, // transferId -> { b64, chunkSize, width, height, ts } (发送端保留,用于 CHUNK_RESEND 重传)
- // 6. 自适应心跳
- heartbeatTimer: null,
- currentHeartbeatMs: 25000,
- networkQuality: "good", // good / fair / poor
- lastPongAt: 0,
- pingRtt: 0,
- // 7. 背压
- lastSendAt: 0,
- sendBurstCount: 0,
- // 8. 频道订阅
- subscribedChannels: {}
- };
- // ============================================================
- // 消息构建 & 发送(含背压控制)
- // ============================================================
- function buildMsg(type, data, opts) {
- G.seq++;
- var msg = {
- type: type,
- seq: G.seq,
- ts: new Date().getTime(),
- device: { brand: "", model: "" },
- data: data || {}
- };
- try { msg.device.brand = device.getBrand(); } catch (e) {}
- try { msg.device.model = device.getModel(); } catch (e) {}
- if (opts) {
- if (opts.req_id) msg.req_id = opts.req_id;
- if (opts.ack_for) msg.ack_for = opts.ack_for;
- if (opts.channel) msg.channel = opts.channel;
- if (opts.no_ack) msg.no_ack = true; // 标记不需要 ACK
- }
- return msg;
- }
- /**
- * 发送消息(模式 7:背压控制)
- * 两次发送之间至少间隔 SEND_RATE_LIMIT_MS,但允许突发 SEND_BURST_MAX 条
- */
- function sendMsg(type, data, opts) {
- // 降级模式:默认所有消息都无需 ACK,除非显式要求
- if (CONFIG.RELAXED_MODE && !opts) opts = {};
- if (CONFIG.RELAXED_MODE && opts && !opts.__force_ack) {
- if (type !== "REGISTER" && type !== "SUBSCRIBE" && type !== "UNSUBSCRIBE") {
- opts.no_ack = true;
- }
- // 对于 SUBSCRIBE / REGISTER:降级模式下也走 no_ack,减少混乱
- if (!CONFIG.RELAXED_ACK_REQUIRED) {
- opts.no_ack = true;
- }
- }
- if (!G.ws || !G.connected) {
- // 模式 4:离线队列 —— 断线时缓存
- enqueueOffline(type, data, opts);
- return -1;
- }
- // 分片消息(CHUNK_START/CHUNK_DATA/CHUNK_END)与 RESPONSE/STATUS:绕过背压
- // 避免截图分片被限流后入队导致丢片
- var bypassBackpressure = (
- type === "CHUNK_START" || type === "CHUNK_DATA" || type === "CHUNK_END" ||
- type === "RESPONSE" || type === "STATUS" || type === "SCREENSHOT_DATA" ||
- type === "SCREENSHOT_FAIL" || type === "ACK" || type === "PONG"
- );
- if (!bypassBackpressure) {
- // 背压控制(仅对控制类消息生效)
- var now = new Date().getTime();
- var elapsed = now - G.lastSendAt;
- if (G.sendBurstCount >= CONFIG.SEND_BURST_MAX && elapsed < CONFIG.SEND_RATE_LIMIT_MS) {
- // 超过突发上限且间隔不够 → 入队等待
- LOG("WARN", "背压限制,消息入队等待 type=" + type);
- enqueueOffline(type, data, opts);
- return -1;
- }
- if (elapsed >= CONFIG.SEND_RATE_LIMIT_MS) {
- G.sendBurstCount = 0; // 间隔够了,重置突发计数
- }
- G.sendBurstCount++;
- G.lastSendAt = now;
- }
- var msg = buildMsg(type, data, opts);
- var raw = JSON.stringify(msg);
- try {
- G.ws.send(raw);
- LOG("SEND", type + " seq=" + msg.seq);
- // 模式 1:需要 ACK 的消息 → 启动超时重传
- if (!opts || !opts.no_ack) {
- startAckTimer(msg);
- }
- return msg.seq;
- } catch (e) {
- LOG("ERROR", "send异常: " + e.message);
- enqueueOffline(type, data, opts);
- return -1;
- }
- }
- // ============================================================
- // 模式 1:ACK 确认 + 超时重传
- //
- // 发送消息后,等对端回 ACK(seq)。
- // 超时未收到 → 重传(最多 ACK_MAX_RETRY 次)。
- // 超过最大重传次数 → 通知上层失败。
- // ============================================================
- function startAckTimer(msg) {
- var entry = {
- raw: JSON.stringify(msg),
- retryCount: 0,
- sentAt: new Date().getTime(),
- timerId: null
- };
- G.pendingAcks[msg.seq] = entry;
- entry.timerId = runTime.setTimeout(function () {
- onAckTimeout(msg.seq);
- }, CONFIG.ACK_TIMEOUT_MS);
- }
- function onAckTimeout(seq) {
- var entry = G.pendingAcks[seq];
- if (!entry) return; // 已经收到 ACK 了
- entry.retryCount++;
- if (entry.retryCount > CONFIG.ACK_MAX_RETRY) {
- if (CONFIG.RELAXED_MODE) {
- LOG("DEBUG", "ACK 未确认 seq=" + seq + "(降级模式自动忽略)");
- } else {
- LOG("ERROR", "ACK 重传超限 seq=" + seq + " 放弃");
- }
- delete G.pendingAcks[seq];
- // 通知上层(可选:触发回调)
- return;
- }
- if (CONFIG.RELAXED_MODE) {
- LOG("DEBUG", "ACK 等待 seq=" + seq + " 第 " + entry.retryCount + " 次(降级模式不重传,仅等待)");
- // 降级模式:不重传,继续等一次,下一次超时就 discard
- entry.timerId = runTime.setTimeout(function () {
- LOG("DEBUG", "ACK 最终未返回 seq=" + seq + "(降级模式丢弃)");
- delete G.pendingAcks[seq];
- }, CONFIG.ACK_TIMEOUT_MS);
- return;
- }
- LOG("WARN", "ACK 超时 seq=" + seq + " 第 " + entry.retryCount + " 次重传");
- if (G.ws && G.connected) {
- try { G.ws.send(entry.raw); } catch (e) {}
- // 重新启动超时计时
- entry.timerId = runTime.setTimeout(function () {
- onAckTimeout(seq);
- }, CONFIG.ACK_TIMEOUT_MS);
- } else {
- // 连接断了,放回离线队列
- delete G.pendingAcks[seq];
- // 离线队列会在重连后 flush
- }
- }
- function handleAck(seq) {
- var entry = G.pendingAcks[seq];
- if (!entry) {
- LOG("DEBUG", "收到重复 ACK seq=" + seq);
- return;
- }
- if (entry.timerId) {
- try { runTime.stopTimeout(entry.timerId); } catch (e) {}
- }
- var rtt = new Date().getTime() - entry.sentAt;
- LOG("DEBUG", "ACK 确认 seq=" + seq + " RTT=" + rtt + "ms");
- delete G.pendingAcks[seq];
- }
- // ============================================================
- // 模式 2:请求-响应配对(仿 HTTP 请求模式)
- //
- // sendRequest("GET_STATUS", { detailed: true })
- // → 等服务端回 { type: "RESPONSE", req_id: "xxx", data: {...} }
- // → 超时则 reject
- //
- // AIWROK 不支持 Promise,这里用回调方式:
- // sendRequest(type, data, onOk, onTimeout)
- // ============================================================
- var _reqCounter = 0;
- function sendRequest(type, data, onOk, onTimeout) {
- _reqCounter++;
- var reqId = "req_" + new Date().getTime() + "_" + _reqCounter;
- var entry = {
- reqId: reqId,
- onOk: onOk,
- onTimeout: onTimeout,
- timerId: null
- };
- G.pendingRequests[reqId] = entry;
- // 超时计时
- entry.timerId = runTime.setTimeout(function () {
- if (G.pendingRequests[reqId]) {
- delete G.pendingRequests[reqId];
- LOG("WARN", "请求超时 type=" + type + " reqId=" + reqId);
- if (onTimeout) onTimeout("timeout");
- }
- }, CONFIG.REQ_TIMEOUT_MS);
- sendMsg(type, data, { req_id: reqId });
- LOG("DEBUG", "请求发出 type=" + type + " reqId=" + reqId);
- return reqId;
- }
- function handleResponse(resp) {
- var reqId = resp.req_id;
- var entry = G.pendingRequests[reqId];
- if (!entry) {
- LOG("DEBUG", "收到过期响应 reqId=" + reqId);
- return;
- }
- if (entry.timerId) {
- try { runTime.stopTimeout(entry.timerId); } catch (e) {}
- }
- delete G.pendingRequests[reqId];
- if (entry.onOk) entry.onOk(resp.data || resp);
- LOG("DEBUG", "响应收到 reqId=" + reqId);
- }
- // ============================================================
- // 模式 3:消息序列号 + 断线补偿
- //
- // 每条收到的消息都带 seq。
- // 重连后发送 RESYNC(lastSeq),请求服务端补发 lastSeq 之后的消息。
- // ============================================================
- function handleIncomingSeq(seq) {
- if (seq > 0) {
- if (seq <= G.lastReceivedSeq) {
- LOG("WARN", "重复消息 seq=" + seq + " (last=" + G.lastReceivedSeq + "),丢弃");
- return false; // 重复消息
- }
- if (G.lastReceivedSeq > 0 && seq > G.lastReceivedSeq + 1) {
- var gap = seq - G.lastReceivedSeq - 1;
- LOG("WARN", "消息间隙 seq=" + seq + " 缺少 " + gap + " 条");
- // 可选:请求补发
- // sendMsg("RESYNC", { from: G.lastReceivedSeq + 1, to: seq - 1 });
- }
- G.lastReceivedSeq = seq;
- }
- return true;
- }
- function requestResyncOnReconnect() {
- if (!CONFIG.RESEND_ON_RECONNECT) return;
- if (G.lastReceivedSeq > 0) {
- LOG("INFO", "重连后请求补发 seq>" + G.lastReceivedSeq);
- sendMsg("RESYNC", { last_seq: G.lastReceivedSeq });
- }
- }
- // ============================================================
- // 模式 4:离线消息队列
- //
- // 断线时所有发送的消息缓存到队列。
- // 重连后自动 flush(先进先出)。
- // ============================================================
- function enqueueOffline(type, data, opts) {
- if (G.offlineQueue.length >= CONFIG.OFFLINE_QUEUE_MAX) {
- G.offlineQueue.shift(); // 丢最老的
- LOG("WARN", "离线队列已满,丢弃最旧消息");
- }
- G.offlineQueue.push({ type: type, data: data, opts: opts });
- LOG("DEBUG", "入离线队列 type=" + type + " 队列长度=" + G.offlineQueue.length);
- }
- function flushOfflineQueue() {
- if (G.offlineQueue.length === 0) return;
- LOG("INFO", "开始 flush 离线队列 " + G.offlineQueue.length + " 条");
- var count = 0;
- while (G.offlineQueue.length > 0 && G.connected) {
- var item = G.offlineQueue.shift();
- // 直接发送,不走 sendMsg 的离线判断
- var msg = buildMsg(item.type, item.data, item.opts);
- try {
- G.ws.send(JSON.stringify(msg));
- if (!item.opts || !item.opts.no_ack) startAckTimer(msg);
- count++;
- } catch (e) {
- // 发送失败,放回队列头部
- G.offlineQueue.unshift(item);
- break;
- }
- // 背压:每条之间稍微停顿
- // AIWROK 中 sleep.millisecond 会阻塞,这里用极短间隔
- }
- LOG("INFO", "flush 完成 " + count + " 条");
- }
- // ============================================================
- // ============================================================
- // 模式 5:分片传输大截图
- //
- // 超过 LARGE_IMG_THRESHOLD_KB 的截图,分 CHUNK_SIZE 大小的块发送。
- // 协议:
- // 1. send { type: "CHUNK_START", transfer_id, total_chunks, mime, width, height }
- // 2. send { type: "CHUNK_DATA", transfer_id, chunk_index, data_base64 }
- // 3. send { type: "CHUNK_END", transfer_id }
- //
- // 本函数对截图 API 做 4 级 fallback,失败时尽量给出原因而不是 #0
- // ============================================================
- function takeScreenshotRobust() {
- /**
- * AIWROK 正确截图 API:优先 screen.screenShot(w,h,quality) 可缩放+控质量
- * 失败时回退 screen.screenShotFull() → .toBase64()
- * 返回 { base64, size_kb, width, height }
- * 失败时返回 null
- */
- try {
- var errs = [];
- // 读取配置
- var maxW = parseInt(CONFIG.SCREENSHOT_MAX_WIDTH) || 0;
- var quality = parseInt(CONFIG.SCREENSHOT_JPEG_QUALITY);
- if (!quality || quality < 10 || quality > 100) quality = 80;
- // 0) 检查截图权限
- try {
- if (typeof permissionManger !== "undefined" && permissionManger) {
- var hasPerm = permissionManger.checkScreenShotPremission();
- if (hasPerm === false) {
- LOG("ERROR", "截图权限未开启!请在 AIWROK 权限管理中开启「截图/投屏」权限");
- return null;
- }
- }
- } catch (ePerm) { errs.push("perm_check:" + ePerm.message); }
- // 计算目标尺寸(等比缩放)
- var realW = 0, realH = 0;
- try { realW = screen.getScreenWidth(); } catch (ew) {}
- try { realH = screen.getScreenHeight(); } catch (eh) {}
- var targetW = realW, targetH = realH;
- if (maxW > 0 && realW > 0 && realH > 0 && realW > maxW) {
- targetW = maxW;
- targetH = Math.floor(realH * (maxW / realW));
- }
- // 1) 优先 screen.screenShot(w, h, quality):可同时控制分辨率和 JPEG 质量(减少带宽)
- if (typeof screen !== "undefined" && screen && typeof screen.screenShot === "function") {
- try {
- if (!targetW || !targetH) { targetW = realW || 1080; targetH = realH || 1920; }
- var imgA = screen.screenShot(targetW, targetH, quality);
- if (!imgA) throw new Error("screenShot 返回 null");
- var b64A = "";
- if (typeof imgA.toBase64 === "function") {
- b64A = "" + imgA.toBase64();
- } else if (typeof imgA.toBase64Jpg === "function") {
- b64A = "" + imgA.toBase64Jpg();
- } else {
- throw new Error("screenShot 返回对象无 toBase64 方法");
- }
- try { imgA.recycle(); } catch (er) {}
- if (!b64A || b64A.length < 10) throw new Error("Base64 为空 len=" + b64A.length);
- var sizeKBA = Math.floor((b64A.length * 0.75) / 1024);
- LOG("INFO", "截图 OK (screenShot scaled) size=" + sizeKBA + "KB " + targetW + "x" + targetH + " q=" + quality);
- return { base64: b64A, size_kb: sizeKBA, width: targetW, height: targetH };
- } catch (eA) {
- errs.push("screenShot(" + targetW + "," + targetH + "," + quality + "):" + (eA.message || eA));
- }
- }
- // 2) 回退 screen.screenShotFull()(AIWROK 主要截图 API)
- if (typeof screen !== "undefined" && screen && typeof screen.screenShotFull === "function") {
- try {
- var img = screen.screenShotFull();
- if (!img) throw new Error("screenShotFull 返回 null");
- var w = realW, h = realH;
- if (!w || !h) {
- try { w = img.getWidth(); } catch (ew2) {}
- try { h = img.getHeight(); } catch (eh2) {}
- }
- var b64 = "";
- if (typeof img.toBase64 === "function") {
- b64 = "" + img.toBase64();
- } else if (typeof img.toBase64Jpg === "function") {
- b64 = "" + img.toBase64Jpg();
- } else {
- throw new Error("截图对象没有 toBase64 方法");
- }
- try { img.recycle(); } catch (er) {}
- if (!b64 || b64.length < 10) throw new Error("Base64 为空或过短 len=" + (b64 ? b64.length : 0));
- var sizeKB = Math.floor((b64.length * 0.75) / 1024);
- LOG("INFO", "截图 OK size=" + sizeKB + "KB width=" + w + " height=" + h);
- return { base64: b64, size_kb: sizeKB, width: w, height: h };
- } catch (e1) {
- errs.push("screenShotFull:" + (e1.message || e1));
- }
- } else {
- errs.push("screenShotFull_not_found");
- }
- // 3) 最后兜底:尝试 image.* 系列
- if (typeof image !== "undefined" && image) {
- var tryList = ["fastCapture", "screenshot", "captureScreen", "capture"];
- for (var ti = 0; ti < tryList.length; ti++) {
- var fnName = tryList[ti];
- try {
- if (typeof image[fnName] === "function") {
- var img3 = image[fnName]();
- if (img3) {
- var w3 = 0, h3 = 0;
- try { w3 = img3.getWidth(); } catch (ew3) {}
- try { h3 = img3.getHeight(); } catch (eh3) {}
- var b64_3 = "" + img3.toBase64();
- try { img3.recycle(); } catch (er) {}
- var sizeKB3 = Math.floor((b64_3.length * 0.75) / 1024);
- LOG("INFO", "截图 OK (image." + fnName + ") size=" + sizeKB3 + "KB");
- return { base64: b64_3, size_kb: sizeKB3, width: w3, height: h3 };
- }
- }
- } catch (e3) { errs.push(fnName + ":" + e3.message); }
- }
- }
- LOG("ERROR", "所有截图 API 均失败: " + errs.join(" | "));
- LOG("ERROR", "常见原因:(1) AIWROK 未开启「投屏/截图」权限;(2) 应用未启动前台服务;(3) Android 10+ 需要 MediaProjection 授权");
- return null;
- } catch (eOuter) {
- try { LOG("ERROR", "截图最外层异常: " + (eOuter.message || String(eOuter))); } catch (eLog) {}
- return null;
- }
- }
- function sendScreenshotChunked(shotID) {
- // ⚠️ 最外层 try-catch:防止 java/Android 底层异常直接变成【错误】#0
- try {
- var shot = takeScreenshotRobust();
- if (!shot) {
- // 把失败原因回传给控制面板(便于显示)
- try { sendMsg("SCREENSHOT_FAIL", { id: shotID || "", reason: "capture_failed" }, { no_ack: true }); } catch (eSend) {}
- return { ok: false, reason: "capture_failed", error_msg: "所有截图API均失败,请检查AIWROK投屏权限" };
- }
- var sizeKB = shot.size_kb;
- var b64 = shot.base64;
- if (sizeKB < CONFIG.LARGE_IMG_THRESHOLD_KB) {
- // 小图直接发
- sendMsg("SCREENSHOT", {
- id: shotID, format: "jpeg",
- width: shot.width, height: shot.height,
- size_kb: sizeKB, image_base64: b64
- }, { no_ack: true });
- return { ok: true, size_kb: sizeKB, width: shot.width, height: shot.height, chunked: false };
- }
- // 大图分片
- var transferId = "tf_" + new Date().getTime();
- var chunkSize = CONFIG.CHUNK_SIZE;
- var totalChunks = Math.ceil(b64.length / chunkSize);
- LOG("INFO", "分片传输 transferId=" + transferId + " 总片数=" + totalChunks + " size=" + sizeKB + "KB");
- // 保留原始 Base64 和参数,用于 CHUNK_RESEND 重传
- G.lastSentChunks[transferId] = {
- b64: b64,
- chunkSize: chunkSize,
- width: shot.width,
- height: shot.height,
- sizeKB: sizeKB,
- totalChunks: totalChunks,
- ts: new Date().getTime()
- };
- // 清理过期记录,避免内存无限增长
- var _keepMs = CONFIG.CHUNK_RETAIN_LAST_MS || 60000;
- var _now = new Date().getTime();
- var _keys = [];
- for (var _k in G.lastSentChunks) if (G.lastSentChunks.hasOwnProperty(_k)) _keys.push(_k);
- for (var _ki = 0; _ki < _keys.length; _ki++) {
- if (_now - (G.lastSentChunks[_keys[_ki]].ts || 0) > _keepMs) delete G.lastSentChunks[_keys[_ki]];
- }
- sendMsg("CHUNK_START", {
- transfer_id: transferId,
- total_chunks: totalChunks,
- mime: "jpeg",
- width: shot.width,
- height: shot.height,
- size_kb: sizeKB
- });
- for (var i = 0; i < totalChunks; i++) {
- var start = i * chunkSize;
- var end = Math.min(start + chunkSize, b64.length);
- var chunk = b64.substring(start, end);
- sendMsg("CHUNK_DATA", {
- transfer_id: transferId,
- chunk_index: i,
- data_base64: chunk
- });
- // 分片之间 1ms 让步即可,不再 5 片睡 10ms(绕过背压后不需要额外节流)
- if ((i + 1) % 20 === 0) {
- try { sleep.millisecond(1); } catch (e) {}
- }
- }
- sendMsg("CHUNK_END", { transfer_id: transferId });
- LOG("INFO", "分片传输完成 transferId=" + transferId);
- return { ok: true, size_kb: sizeKB, width: shot.width, height: shot.height, chunked: true, transfer_id: transferId, total_chunks: totalChunks };
- } catch (eOuter) {
- try { LOG("ERROR", "sendScreenshotChunked 最外层异常: " + (eOuter.message || String(eOuter))); } catch (eLog) {}
- try { sendMsg("SCREENSHOT_FAIL", { id: shotID || "", reason: "exception", error_msg: (eOuter.message || String(eOuter)) }, { no_ack: true }); } catch (eSend) {}
- return { ok: false, reason: "exception", error_msg: (eOuter.message || String(eOuter)) };
- }
- }
- // ============================================================
- // 辅助函数 Polyfill(AIWROK 环境全局函数缺失兜底)
- // ⚠️ 关键策略:所有函数同时挂载到全局变量 + PhoneCtrl 对象
- // 即使变量名与 AIWROK 引擎保留名冲突导致赋值失败,
- // PhoneCtrl 对象上的函数仍然可用,handleRemoteCommand 会优先使用
- // ============================================================
- var PhoneCtrl = PhoneCtrl || {};
- (function _polyfill_helpers() {
- try {
- function _has(obj, name) { return !!(obj && typeof obj[name] === "function"); }
- // 尝试在全局作用域赋值(安全方式,使用全局对象属性)
- function _tryAssignGlobal(name, fn) {
- try {
- var g = (typeof window !== "undefined") ? window :
- (typeof globalThis !== "undefined") ? globalThis :
- (typeof runtime !== "undefined") ? runtime : this;
- g[name] = fn;
- return true;
- } catch (e) {
- return false;
- }
- }
- // 尝试在全局作用域读取(安全方式)
- function _tryGetGlobal(name) {
- try {
- var g = (typeof window !== "undefined") ? window :
- (typeof globalThis !== "undefined") ? globalThis :
- (typeof runtime !== "undefined") ? runtime : this;
- return g[name];
- } catch (e) { return undefined; }
- }
- // 通用:auto API 直接调用兜底
- var _autoClick = function (x, y) {
- if (_has(auto, "clickPoint")) return auto.clickPoint(x, y);
- if (_has(auto, "clickPercent")) {
- var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
- return auto.clickPercent(x / sw, y / sh);
- }
- throw new Error("no_click_api");
- };
- var _autoSwipe = function (x1, y1, x2, y2, dur) {
- if (_has(auto, "swip")) return auto.swip(x1, y1, x2, y2, dur, 0);
- throw new Error("no_swipe_api");
- };
- var _autoInput = function (text) {
- if (_has(auto, "input")) return auto.input(text);
- if (_has(auto, "sendText")) return auto.sendText(text);
- if (_has(auto, "inputText")) return auto.inputText(text);
- throw new Error("no_input_api");
- };
- var _autoPress = function (fnName) {
- if (_has(auto, fnName)) return auto[fnName]();
- throw new Error("auto." + fnName + "_not_found");
- };
- var _keyPress = function (keyName, keyCode) {
- if (_has(device, keyName)) return device[keyName]();
- if (_has(device, "pressKey")) return device.pressKey(keyCode);
- if (_has(device, "press")) return device.press(keyCode);
- try {
- var KeyEvent = android.view.KeyEvent;
- if (_has(runtime, "postEvent")) return runtime.postEvent(KeyEvent.ACTION_DOWN, keyCode);
- } catch (e) {}
- throw new Error("no_" + keyName + "_api");
- };
- // click
- PhoneCtrl.click = function (x, y) {
- x = parseInt(x || 0); y = parseInt(y || 0);
- try { return _autoClick(x, y); } catch (e1) {}
- try { if (_has(gesture, "click")) return gesture.click(x, y); } catch (e2) {}
- try { if (_has(device, "click")) return device.click(x, y); } catch (e3) {}
- throw new Error("click_failed");
- };
- _tryAssignGlobal("click", PhoneCtrl.click);
- // swipe
- PhoneCtrl.swipe = function (x1, y1, x2, y2, duration) {
- x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
- x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
- duration = parseInt(duration || 400);
- try { return _autoSwipe(x1, y1, x2, y2, duration); } catch (e1) {}
- try {
- if (_has(auto, "dispatchGesture")) {
- gesture.setDurTime(duration);
- gesture.addPoint(x1, y1);
- gesture.addPoint(x2, y2);
- return auto.dispatchGesture([gesture]);
- }
- } catch (e2) {}
- try { if (_has(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, duration); } catch (e3) {}
- try { if (_has(device, "swipe")) return device.swipe(x1, y1, x2, y2, duration); } catch (e4) {}
- throw new Error("swipe_failed");
- };
- _tryAssignGlobal("swipe", PhoneCtrl.swipe);
- // input
- PhoneCtrl.input = function (text) {
- text = String(text == null ? "" : text);
- try { return _autoInput(text); } catch (e1) {}
- try { if (_has(device, "input")) return device.input(text); } catch (e2) {}
- throw new Error("input_failed");
- };
- _tryAssignGlobal("input", PhoneCtrl.input);
- // home / back / menu / power / search / camera
- PhoneCtrl.home = function () {
- try { return _autoPress("home"); } catch (e) {}
- return _keyPress("home", 3);
- };
- PhoneCtrl.back = function () {
- try { return _autoPress("back"); } catch (e) {}
- return _keyPress("back", 4);
- };
- PhoneCtrl.menu = function () {
- try { return _autoPress("menu"); } catch (e) {}
- return _keyPress("menu", 82);
- };
- PhoneCtrl.power = function () {
- try { return _autoPress("power"); } catch (e) {}
- return _keyPress("power", 26);
- };
- PhoneCtrl.search = function () {
- try { return _autoPress("search"); } catch (e) {}
- return _keyPress("search", 84);
- };
- PhoneCtrl.camera = function () {
- try { return _autoPress("camera"); } catch (e) {}
- return _keyPress("camera", 27);
- };
- // recentApps
- PhoneCtrl.recentApps = function () {
- try { return _autoPress("recents"); } catch (e1) {}
- try { return _autoPress("recentApps"); } catch (e2) {}
- try {
- var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
- return PhoneCtrl.swipe(Math.floor(sw/2), sh - 20, Math.floor(sw/2), Math.floor(sh/2), 300);
- } catch (e3) {}
- return _keyPress("recentApps", 187);
- };
- // expandNotificationBar
- PhoneCtrl.expandNotificationBar = function () {
- try { return _autoPress("statusBar"); } catch (e1) {}
- try { return _autoPress("notification"); } catch (e2) {}
- try {
- var sw = screen.getScreenWidth();
- return PhoneCtrl.swipe(Math.floor(sw/2), 10, Math.floor(sw/2), 400, 300);
- } catch (e3) { throw new Error("no_noti_api"); }
- };
- // openApp
- PhoneCtrl.openApp = function (pkg) {
- pkg = String(pkg || "");
- try {
- if (typeof app !== "undefined" && app) {
- if (_has(app, "openApp")) return app.openApp(pkg);
- if (_has(app, "openAppv2")) return app.openAppv2(pkg);
- if (_has(app, "openAppSeting")) return app.openAppSeting(pkg);
- if (_has(app, "launch")) return app.launch(pkg);
- if (_has(app, "open")) return app.open(pkg);
- }
- } catch (e1) {}
- try {
- if (typeof device !== "undefined" && device) {
- if (_has(device, "startApp")) return device.startApp(pkg);
- if (_has(device, "launchApp")) return device.launchApp(pkg);
- if (_has(device, "openApp")) return device.openApp(pkg);
- }
- } catch (e2) {}
- try {
- var ctxt = context || runtime.getContext();
- var Intent = android.content.Intent;
- var pm = ctxt.getPackageManager();
- var launchIntent = pm.getLaunchIntentForPackage(pkg);
- if (launchIntent) {
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- ctxt.startActivity(launchIntent);
- return true;
- }
- throw new Error("no_launch_intent");
- } catch (e3) { throw new Error("openApp_fail: " + e3.message); }
- };
- // openAppName
- PhoneCtrl.openAppName = function (name) {
- name = String(name || "");
- try {
- if (typeof app !== "undefined" && app) {
- if (_has(app, "openApp")) return app.openApp(name);
- if (_has(app, "openAppv2")) return app.openAppv2(name);
- if (_has(app, "launchByName")) return app.launchByName(name);
- if (_has(app, "openByName")) return app.openByName(name);
- }
- } catch (e1) {}
- try {
- if (typeof device !== "undefined" && device) {
- if (_has(device, "openAppByName")) return device.openAppByName(name);
- if (_has(device, "launchByName")) return device.launchByName(name);
- }
- } catch (e2) {}
- try {
- var ctxt = context || runtime.getContext();
- var pm = ctxt.getPackageManager();
- var apps = pm.getInstalledApplications(0);
- var target = name.toLowerCase();
- var iter = apps.iterator();
- while (iter.hasNext()) {
- var ai = iter.next();
- var label = String(pm.getApplicationLabel(ai) || "").toLowerCase();
- var pkg = String(ai.packageName || "").toLowerCase();
- if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
- return PhoneCtrl.openApp(ai.packageName);
- }
- }
- throw new Error("pkg_not_found_for_name_" + name);
- } catch (e3) { throw new Error("openAppName_fail: " + e3.message); }
- };
- // killApp
- PhoneCtrl.killApp = function (pkg) {
- pkg = String(pkg || "");
- try {
- if (typeof app !== "undefined" && app) {
- if (_has(app, "forceStop")) return app.forceStop(pkg);
- if (_has(app, "kill")) return app.kill(pkg);
- if (_has(app, "close")) return app.close(pkg);
- }
- } catch (e1) {}
- try {
- if (typeof device !== "undefined" && device) {
- if (_has(device, "forceStopApp")) return device.forceStopApp(pkg);
- if (_has(device, "killApp")) return device.killApp(pkg);
- if (_has(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
- }
- } catch (e2) {}
- try {
- var ctxt = context || runtime.getContext();
- var actMan = ctxt.getSystemService(android.content.Context.ACTIVITY_SERVICE);
- actMan.killBackgroundProcesses(pkg);
- return true;
- } catch (e3) { throw new Error("killApp_fail: " + e3.message); }
- };
- // 全局变量赋值(尽力而为,失败不影响 PhoneCtrl 上的函数)
- _tryAssignGlobal("home", PhoneCtrl.home);
- _tryAssignGlobal("back", PhoneCtrl.back);
- _tryAssignGlobal("menu", PhoneCtrl.menu);
- _tryAssignGlobal("power", PhoneCtrl.power);
- _tryAssignGlobal("search", PhoneCtrl.search);
- _tryAssignGlobal("camera", PhoneCtrl.camera);
- _tryAssignGlobal("recentApps", PhoneCtrl.recentApps);
- _tryAssignGlobal("expandNotificationBar", PhoneCtrl.expandNotificationBar);
- _tryAssignGlobal("openApp", PhoneCtrl.openApp);
- _tryAssignGlobal("openAppName", PhoneCtrl.openAppName);
- _tryAssignGlobal("killApp", PhoneCtrl.killApp);
- LOG("INFO", "Polyfill 初始化完成 PhoneCtrl.click/swipe/input/home/back 等已就绪");
- } catch (ePolyOuter) {
- try { LOG("WARN", "polyfill 异常: " + (ePolyOuter.message || String(ePolyOuter))); } catch (eLog) {}
- }
- })();
- /**
- * 接收端:组装分片(如果手机也需要接收大文件的话)
- */
- function handleChunkStart(data) {
- G.chunkTransfers[data.transfer_id] = {
- chunks: {},
- totalChunks: data.total_chunks,
- received: 0,
- meta: data
- };
- LOG("DEBUG", "分片接收开始 transferId=" + data.transfer_id + " 总片数=" + data.totalChunks);
- }
- function handleChunkData(data) {
- var transfer = G.chunkTransfers[data.transfer_id];
- if (!transfer) {
- LOG("WARN", "未知分片传输 transferId=" + data.transfer_id);
- return;
- }
- transfer.chunks[data.chunk_index] = data.data_base64;
- transfer.received++;
- // 进度
- var pct = Math.floor(transfer.received / transfer.totalChunks * 100);
- if (transfer.received % 10 === 0 || transfer.received === transfer.totalChunks) {
- LOG("DEBUG", "分片接收进度 " + pct + "% (" + transfer.received + "/" + transfer.totalChunks + ")");
- }
- }
- function handleChunkEnd(data) {
- var transfer = G.chunkTransfers[data.transfer_id];
- if (!transfer) return;
- if (transfer.received < transfer.totalChunks) {
- LOG("WARN", "分片不完整 " + transfer.received + "/" + transfer.totalChunks);
- // 可请求重传缺失的片
- var missing = [];
- for (var i = 0; i < transfer.totalChunks; i++) {
- if (!transfer.chunks[i]) missing.push(i);
- }
- sendMsg("CHUNK_RESEND", {
- transfer_id: data.transfer_id,
- missing_indices: missing
- });
- return;
- }
- // 组装
- var fullB64 = "";
- for (var i = 0; i < transfer.totalChunks; i++) {
- fullB64 += transfer.chunks[i];
- }
- LOG("INFO", "分片组装完成 transferId=" + data.transfer_id + " 总大小=" + Math.floor(fullB64.length / 1024) + "KB");
- // 这里可以解码 Base64 → Bitmap → 保存文件等
- delete G.chunkTransfers[data.transfer_id];
- }
- /**
- * 处理对端发来的 CHUNK_RESEND(缺失分片重传请求)
- * 从 G.lastSentChunks 中取出该 transferId 的原始 b64,按索引重新发送对应片
- */
- function handleChunkResend(data) {
- var transferId = data.transfer_id;
- var missing = data.missing_indices || [];
- var cached = G.lastSentChunks[transferId];
- if (!cached) {
- LOG("WARN", "收到 CHUNK_RESEND 但本地已无缓存 transferId=" + transferId);
- return;
- }
- LOG("INFO", "收到 CHUNK_RESEND transferId=" + transferId + " 缺失片数=" + missing.length);
- var b64 = cached.b64;
- var chunkSize = cached.chunkSize;
- var reSentCount = 0;
- for (var mi = 0; mi < missing.length; mi++) {
- var idx = parseInt(missing[mi]);
- if (isNaN(idx) || idx < 0) continue;
- var start = idx * chunkSize;
- if (start >= b64.length) continue;
- var end = Math.min(start + chunkSize, b64.length);
- var chunk = b64.substring(start, end);
- sendMsg("CHUNK_DATA", {
- transfer_id: transferId,
- chunk_index: idx,
- data_base64: chunk
- });
- reSentCount++;
- // 每 20 片让 1ms
- if (reSentCount % 20 === 0) { try { sleep.millisecond(1); } catch (e) {} }
- }
- LOG("INFO", "CHUNK_RESEND 已重传 " + reSentCount + " 片 transferId=" + transferId);
- }
- // ============================================================
- // 模式 6:自适应心跳
- //
- // 根据网络质量动态调整心跳间隔:
- // good → 15s fair → 30s poor → 60s
- // 网络质量评估依据:PING RTT + 最近是否断线频繁
- // ============================================================
- function assessNetworkQuality() {
- var rtt = G.pingRtt || 0;
- var oldQ = G.networkQuality;
- if (rtt === 0 || rtt < 100) {
- G.networkQuality = "good";
- G.currentHeartbeatMs = CONFIG.HEARTBEAT_MIN_MS;
- } else if (rtt < 500) {
- G.networkQuality = "fair";
- G.currentHeartbeatMs = Math.floor((CONFIG.HEARTBEAT_MIN_MS + CONFIG.HEARTBEAT_MAX_MS) / 2);
- } else {
- G.networkQuality = "poor";
- G.currentHeartbeatMs = CONFIG.HEARTBEAT_MAX_MS;
- }
- if (oldQ !== G.networkQuality) {
- LOG("INFO", "网络质量变化 " + oldQ + " → " + G.networkQuality + " (RTT=" + rtt + "ms) 心跳=" + (G.currentHeartbeatMs / 1000) + "s");
- // 重启心跳
- startHeartbeat();
- }
- }
- function startHeartbeat() {
- stopHeartbeat();
- G.lastPongAt = new Date().getTime();
- function tick() {
- if (!G.connected) return;
- var now = new Date().getTime();
- var timeoutThresh = G.currentHeartbeatMs + CONFIG.HEARTBEAT_TIMEOUT_MS;
- var sincePong = now - G.lastPongAt;
- if (sincePong > timeoutThresh) {
- if (CONFIG.RELAXED_MODE && CONFIG.RELAXED_HEARTBEAT_SOFT_FAIL) {
- // 降级模式:不主动断开,只降级网络质量 + 发警告
- if (G.networkQuality === "good") { G.networkQuality = "fair"; G.currentHeartbeatMs = Math.floor((CONFIG.HEARTBEAT_MIN_MS + CONFIG.HEARTBEAT_MAX_MS) / 2); }
- else if (G.networkQuality === "fair") { G.networkQuality = "poor"; G.currentHeartbeatMs = CONFIG.HEARTBEAT_MAX_MS; }
- LOG("WARN", "心跳未收到 PONG 已 " + (sincePong / 1000).toFixed(0) + "s,降级网络质量为 " + G.networkQuality + "(降级模式不断线)");
- G.lastPongAt = now; // 重置计时,避免反复告警风暴
- } else {
- LOG("WARN", "心跳超时 主动断开");
- try { G.ws.close(); } catch (e) {}
- return;
- }
- }
- var pingTs = new Date().getTime();
- sendMsg("PING", { t: pingTs }, { no_ack: true });
- // 下一次延迟 = 当前自适应心跳间隔
- G.heartbeatTimer = runTime.setTimeout(tick, G.currentHeartbeatMs);
- }
- // 注意:AIWROK 中 onopen 回调执行顺序可能延迟,这里也设置 lastPongAt
- G.lastPongAt = new Date().getTime();
- G.heartbeatTimer = runTime.setTimeout(tick, G.currentHeartbeatMs);
- LOG("DEBUG", "心跳已启动 间隔=" + (G.currentHeartbeatMs / 1000) + "s 质量=" + G.networkQuality + (CONFIG.RELAXED_MODE ? " 降级模式=开" : ""));
- }
- function stopHeartbeat() {
- if (G.heartbeatTimer) {
- try { runTime.stopTimeout(G.heartbeatTimer); } catch (e) {}
- G.heartbeatTimer = null;
- }
- }
- // 定时评估网络质量
- var networkCheckTimer = null;
- function startNetworkCheck() {
- stopNetworkCheck();
- networkCheckTimer = runTime.setTimeout(function check() {
- assessNetworkQuality();
- // 每 30 秒同步一次最新设备状态给控制面板(电量/存储/网络等实时更新)
- if (G.connected) {
- try {
- var s = collectStatusFull();
- if (s) sendMsg("STATUS", s, { no_ack: true });
- } catch (eStatus) {
- try { LOG("DEBUG", "周期状态上报失败: " + eStatus.message); } catch (eLog) {}
- }
- }
- networkCheckTimer = runTime.setTimeout(check, CONFIG.NETWORK_CHECK_INTERVAL_MS);
- }, CONFIG.NETWORK_CHECK_INTERVAL_MS);
- }
- function stopNetworkCheck() {
- if (networkCheckTimer) {
- try { runTime.stopTimeout(networkCheckTimer); } catch (e) {}
- networkCheckTimer = null;
- }
- }
- // ============================================================
- // 模式 7:背压控制
- // 已在 sendMsg() 中实现,核心逻辑:
- // - 两次发送至少间隔 SEND_RATE_LIMIT_MS
- // - 允许突发 SEND_BURST_MAX 条
- // - 超限则入离线队列等待
- // ============================================================
- // ============================================================
- // 模式 8:频道订阅模式
- //
- // 客户端可以动态订阅/取消订阅事件频道。
- // 服务端只推送已订阅频道的消息。
- //
- // subscribe("cmd") → 服务端开始推送 cmd 频道消息
- // unsubscribe("cmd") → 服务端停止推送
- // ============================================================
- function subscribe(channel) {
- G.subscribedChannels[channel] = true;
- sendMsg("SUBSCRIBE", { channel: channel });
- LOG("INFO", "订阅频道 " + channel);
- }
- function unsubscribe(channel) {
- delete G.subscribedChannels[channel];
- sendMsg("UNSUBSCRIBE", { channel: channel });
- LOG("INFO", "取消订阅 " + channel);
- }
- function autoSubscribeChannels() {
- for (var i = 0; i < CONFIG.AUTO_SUBSCRIBE_CHANNELS.length; i++) {
- subscribe(CONFIG.AUTO_SUBSCRIBE_CHANNELS[i]);
- }
- }
- // ============================================================
- // 服务器消息处理
- // ============================================================
- function handleServerMessage(raw) {
- // AIWROK 中回调参数可能是 bytes / java object / string,先统一转字符串
- if (raw === null || raw === undefined) return;
- if (typeof raw !== "string") {
- try {
- if (typeof raw === "object" && typeof raw.toString === "function") {
- var s = raw.toString();
- // Java 字节数组或其他对象
- if (s && (s.indexOf("[B@") === 0 || s.indexOf("[object") === 0)) {
- // 尝试通过其他方式解码
- try { s = String(new java.lang.String(raw, "UTF-8")); } catch (e) {}
- }
- raw = s;
- } else {
- raw = String(raw);
- }
- } catch (e) {
- LOG("DEBUG", "无法转换消息类型: " + (typeof raw));
- return;
- }
- }
- if (!raw || typeof raw !== "string") return;
- // 只要收到服务器任何有效消息,就刷新 lastPongAt(比等 PONG 更可靠)
- var nowTs = new Date().getTime();
- G.lastPongAt = nowTs;
- // 兼容旧版文本协议
- if (raw.indexOf("WELCOME|") === 0) {
- LOG("INFO", "服务器欢迎 " + raw.split("|")[1]);
- return;
- }
- if (raw === "PONG" || raw.indexOf("ECHO|") === 0) {
- return;
- }
- // JSON 协议
- var obj = null;
- try { obj = JSON.parse(raw); } catch (e) {
- LOG("DEBUG", "非JSON消息: " + raw.substring(0, 60));
- return;
- }
- if (!obj || typeof obj !== "object") return;
- // 模式 1:处理 ACK
- if (obj.type === "ACK" && obj.ack_for) {
- handleAck(obj.ack_for);
- return;
- }
- // 模式 2:处理响应
- if (obj.type === "RESPONSE" && obj.req_id) {
- handleResponse(obj);
- return;
- }
- // 模式 3:序列号检查
- if (obj.seq && !handleIncomingSeq(obj.seq)) {
- return; // 重复消息,丢弃
- }
- // 按类型分发(外层 try-catch 防止 #0 崩溃)
- try {
- switch (obj.type) {
- case "PONG":
- var pingSentAt = (obj.data && obj.data.t) ? obj.data.t : (obj.ack_for ? obj.ack_for : nowTs);
- G.pingRtt = (pingSentAt && pingSentAt < nowTs) ? (nowTs - pingSentAt) : 0;
- LOG("DEBUG", "PONG RTT=" + G.pingRtt + "ms");
- break;
- case "PING":
- sendMsg("PONG", { echo_ts: obj.ts || 0, t: new Date().getTime() }, { no_ack: true });
- break;
- case "CMD":
- handleRemoteCommand(obj.cmd, obj.params, obj.req_id);
- break;
- // 模式 5:分片接收
- case "CHUNK_START":
- handleChunkStart(obj.data);
- break;
- case "CHUNK_DATA":
- handleChunkData(obj.data);
- break;
- case "CHUNK_END":
- handleChunkEnd(obj.data);
- break;
- // 模式 5 补充:服务端/控制面板请求重传缺失分片
- case "CHUNK_RESEND":
- handleChunkResend(obj.data || {});
- break;
- // 频道消息
- case "CHANNEL":
- LOG("INFO", "频道消息 ch=" + (obj.data.channel || "?") + " content=" + JSON.stringify(obj.data.payload || "").substring(0, 100));
- break;
- case "RESYNC":
- // 服务端补发的消息
- LOG("INFO", "收到补发消息 seq=" + obj.seq);
- break;
- case "RESPONSE":
- // 服务器转发的其他客户端的响应(忽略,我们已在 handleResponse 处理)
- LOG("DEBUG", "收到 RESPONSE req_id=" + (obj.req_id || "?"));
- break;
- case "WELCOME":
- LOG("INFO", "服务器欢迎 " + (obj.data ? obj.data.client_id : "?"));
- break;
- default:
- LOG("DEBUG", "未知消息 type=" + obj.type);
- }
- } catch (eSwitch) {
- LOG("WARN", "消息分发异常 type=" + obj.type + " err=" + (eSwitch.message || String(eSwitch)));
- }
- // 模式 1:收到消息后自动回 ACK(除非标记 no_ack)
- if (obj.seq && !obj.no_ack) {
- sendMsg("ACK", { ack_for: obj.seq }, { no_ack: true });
- }
- }
- // 远程指令(对齐 WebSocket手机端常用实例.js 的 9 类 CMD,使控制面板能直接操作手机)
- function handleRemoteCommand(cmd, params, reqId) {
- params = params || {};
- LOG("CMD", cmd + " params=" + JSON.stringify(params).substring(0, 200));
- var result = { ok: true, data: null, error: "" };
- try {
- // 终极策略:直接使用 auto/device/app 对象的原生 API
- // 彻底绕开 click/swipe 等 AIWROK 引擎保留名冲突
- function _HAS(obj, name) { return !!(obj && typeof obj[name] === "function"); }
- // ===== 触控 =====
- function _doClick(x, y) {
- x = parseInt(x || 0); y = parseInt(y || 0);
- if (_HAS(auto, "clickPoint")) return auto.clickPoint(x, y);
- if (_HAS(auto, "clickPercent")) {
- var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
- return auto.clickPercent(x / sw, y / sh);
- }
- if (_HAS(gesture, "click")) return gesture.click(x, y);
- if (_HAS(device, "click")) return device.click(x, y);
- throw new Error("click_api_not_found");
- }
- function _doSwipe(x1, y1, x2, y2, d) {
- x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
- x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
- d = parseInt(d || 400);
- if (_HAS(auto, "swip")) return auto.swip(x1, y1, x2, y2, d, 0);
- if (_HAS(auto, "dispatchGesture") && _HAS(gesture, "setDurTime")) {
- gesture.setDurTime(d);
- gesture.addPoint(x1, y1);
- gesture.addPoint(x2, y2);
- return auto.dispatchGesture([gesture]);
- }
- if (_HAS(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, d);
- if (_HAS(device, "swipe")) return device.swipe(x1, y1, x2, y2, d);
- throw new Error("swipe_api_not_found");
- }
- function _doInput(t) {
- t = String(t == null ? "" : t);
- if (_HAS(auto, "input")) return auto.input(t);
- if (_HAS(auto, "sendText")) return auto.sendText(t);
- if (_HAS(auto, "inputText")) return auto.inputText(t);
- if (_HAS(device, "input")) return device.input(t);
- throw new Error("input_api_not_found");
- }
- // ===== 按键 =====
- function _doKey(fnName, keyCode, keyName) {
- if (_HAS(auto, fnName)) return auto[fnName]();
- if (_HAS(device, keyName)) return device[keyName]();
- if (_HAS(device, "pressKey")) return device.pressKey(keyCode);
- if (_HAS(device, "press")) return device.press(keyCode);
- if (_HAS(runtime, "postEvent")) {
- var KEv = android.view.KeyEvent;
- return runtime.postEvent(KEv.ACTION_DOWN, keyCode);
- }
- throw new Error("key_" + fnName + "_not_found");
- }
- // ===== 应用管理 =====
- function _openAppDirect(pkg) {
- pkg = String(pkg || "");
- if (_HAS(app, "openApp")) return app.openApp(pkg);
- if (_HAS(app, "openAppv2")) return app.openAppv2(pkg);
- if (_HAS(app, "openAppSeting")) return app.openAppSeting(pkg);
- if (_HAS(app, "launch")) return app.launch(pkg);
- if (_HAS(app, "open")) return app.open(pkg);
- if (_HAS(device, "startApp")) return device.startApp(pkg);
- if (_HAS(device, "launchApp")) return device.launchApp(pkg);
- if (_HAS(device, "openApp")) return device.openApp(pkg);
- var ctxt = context || runtime.getContext();
- var Intent = android.content.Intent;
- var pm = ctxt.getPackageManager();
- var li = pm.getLaunchIntentForPackage(pkg);
- if (li) { li.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ctxt.startActivity(li); return true; }
- throw new Error("openApp_intent_fail");
- }
- function _openByNameDirect(name) {
- name = String(name || "");
- if (_HAS(app, "openApp")) { try { return app.openApp(name); } catch (e1) {} }
- if (_HAS(app, "openAppv2")) { try { return app.openAppv2(name); } catch (e2) {} }
- if (_HAS(app, "launchByName")) { try { return app.launchByName(name); } catch (e3) {} }
- if (_HAS(app, "openByName")) { try { return app.openByName(name); } catch (e4) {} }
- if (_HAS(device, "openAppByName")) { try { return device.openAppByName(name); } catch (e5) {} }
- if (_HAS(device, "launchByName")) { try { return device.launchByName(name); } catch (e6) {} }
- // 模糊匹配
- var ctxt2 = context || runtime.getContext();
- var pm2 = ctxt2.getPackageManager();
- var apps = pm2.getInstalledApplications(0);
- var target = name.toLowerCase();
- var iter = apps.iterator();
- while (iter.hasNext()) {
- var ai = iter.next();
- var label = String(pm2.getApplicationLabel(ai) || "").toLowerCase();
- var pkg = String(ai.packageName || "").toLowerCase();
- if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
- return _openAppDirect(ai.packageName);
- }
- }
- throw new Error("app_name_not_found:" + name);
- }
- function _killAppDirect(pkg) {
- pkg = String(pkg || "");
- if (_HAS(app, "forceStop")) return app.forceStop(pkg);
- if (_HAS(app, "kill")) return app.kill(pkg);
- if (_HAS(app, "close")) return app.close(pkg);
- if (_HAS(device, "forceStopApp")) return device.forceStopApp(pkg);
- if (_HAS(device, "killApp")) return device.killApp(pkg);
- if (_HAS(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
- var ctxt3 = context || runtime.getContext();
- var am = ctxt3.getSystemService(android.content.Context.ACTIVITY_SERVICE);
- am.killBackgroundProcesses(pkg);
- return true;
- }
- switch (cmd) {
- case "SCREENSHOT":
- var shotRes = sendScreenshotChunked(params.id || ("shot_" + new Date().getTime()));
- if (shotRes && shotRes.ok) {
- result.data = shotRes;
- } else {
- result.ok = false;
- result.error = (shotRes && shotRes.error_msg) ? shotRes.error_msg : "screenshot_failed";
- result.data = shotRes || {};
- }
- break;
- case "CLICK":
- if (params.x != null && params.y != null) {
- var cx = parseInt(params.x);
- var cy = parseInt(params.y);
- try { _doClick(cx, cy); result.data = { x: cx, y: cy }; }
- catch (e) { result.ok = false; result.error = e.message; }
- } else {
- result.ok = false; result.error = "missing_xy";
- }
- break;
- case "SWIPE":
- if (params.x1 != null && params.y1 != null && params.x2 != null && params.y2 != null) {
- var x1 = parseInt(params.x1); var y1 = parseInt(params.y1);
- var x2 = parseInt(params.x2); var y2 = parseInt(params.y2);
- var dur = parseInt(params.duration_ms || 400);
- try { _doSwipe(x1, y1, x2, y2, dur); result.data = { x1: x1, y1: y1, x2: x2, y2: y2, duration_ms: dur }; }
- catch (e) { result.ok = false; result.error = e.message; }
- } else {
- result.ok = false; result.error = "missing_points";
- }
- break;
- case "INPUT_TEXT":
- if (params.text != null) {
- var txt = String(params.text);
- try { _doInput(txt); result.data = { text: txt }; }
- catch (e) { result.ok = false; result.error = e.message; }
- } else {
- result.ok = false; result.error = "missing_text";
- }
- break;
- case "KEYCODE":
- if (params.code) {
- var code = String(params.code).toUpperCase();
- try {
- switch (code) {
- case "HOME": _doKey("home", 3, "home"); break;
- case "BACK": _doKey("back", 4, "back"); break;
- case "RECENT":
- case "APP_SWITCH":
- case "TASK":
- try { _doKey("recents", 187, "recentApps"); } catch (eA) { _doKey("recentApps", 187, "recentApps"); }
- break;
- case "NOTIFICATION":
- case "STATUS_BAR":
- try { _doKey("statusBar", 83, "statusBar"); } catch (eB) { _doKey("notification", 83, "notification"); }
- break;
- case "POWER": _doKey("power", 26, "power"); break;
- case "VOL_UP":
- try { device.setMusicVolume(device.getMusicVolume() + 1); } catch (e) { throw new Error("vol_up_failed"); }
- break;
- case "VOL_DOWN":
- try { device.setMusicVolume(Math.max(0, device.getMusicVolume() - 1)); } catch (e) { throw new Error("vol_down_failed"); }
- break;
- case "MENU": _doKey("menu", 82, "menu"); break;
- case "SEARCH": _doKey("search", 84, "search"); break;
- case "CAMERA": _doKey("camera", 27, "camera"); break;
- case "ENTER": try { _doInput("\n"); } catch (e) {} break;
- case "DEL":
- case "BACKSPACE":
- try { var et = device.getFocusEditText(); if (et) { et.setText(""); } } catch (e) { throw new Error("del_failed"); }
- break;
- default:
- throw new Error("unknown_keycode_" + code);
- }
- result.data = { code: code };
- } catch (e) {
- result.ok = false; result.error = e.message;
- }
- } else {
- result.ok = false; result.error = "missing_code";
- }
- break;
- case "OPEN_APP":
- try {
- if (params.pkg) {
- _openAppDirect(String(params.pkg));
- result.data = { opened: "pkg:" + params.pkg };
- } else if (params.name) {
- _openByNameDirect(String(params.name));
- result.data = { opened: "name:" + params.name };
- } else if (params.package_name) {
- _openAppDirect(String(params.package_name));
- result.data = { opened: "pkg:" + params.package_name };
- } else {
- result.ok = false; result.error = "missing_pkg_or_name";
- }
- } catch (e) {
- result.ok = false; result.error = e.message;
- }
- break;
- case "KILL_APP":
- try {
- var pkg = params.pkg || params.package_name;
- if (pkg) {
- _killAppDirect(String(pkg));
- result.data = { killed: pkg };
- } else {
- result.ok = false; result.error = "missing_pkg";
- }
- } catch (e) {
- result.ok = false; result.error = e.message;
- }
- break;
- case "REPORT_STATUS":
- case "GET_STATUS":
- result.data = collectStatusFull();
- break;
- case "RUN_SCRIPT":
- if (params.code != null) {
- try {
- var codeStr = String(params.code);
- var r = (new Function(codeStr))();
- result.data = { returned: (r === undefined ? null : (typeof r === "object" ? r : String(r))) };
- } catch (e) {
- result.ok = false; result.error = e.message;
- }
- } else {
- result.ok = false; result.error = "missing_code";
- }
- break;
- case "SUBSCRIBE":
- if (params.channel) subscribe(params.channel);
- result.data = "subscribed";
- break;
- case "UNSUBSCRIBE":
- if (params.channel) unsubscribe(params.channel);
- result.data = "unsubscribed";
- break;
- case "ECHO":
- result.data = params;
- break;
- case "QUIT":
- result.data = "bye";
- sendMsg("RESPONSE", result, { req_id: reqId, no_ack: true });
- G.running = false;
- try { G.ws.close(); } catch (e) {}
- return;
- default:
- result.ok = false;
- result.error = "unknown_cmd_" + cmd;
- }
- } catch (e) {
- result.ok = false;
- result.error = e.message;
- }
- // 以 RESPONSE 形式回复(模式 2:请求-响应配对)
- // ⚠️ 必须通过 opts.req_id 把 req_id 放到消息顶层,服务器 obj.get("req_id") 才能取到
- // 不要把 req_id 包在第二个参数 data 里,否则 buildMsg 会变成 msg.data.req_id
- if (reqId) {
- sendMsg("RESPONSE", result, { req_id: reqId, no_ack: true });
- }
- }
- function collectStatus() {
- // 兼容旧调用方:字段名全部对齐"新规范",避免返回老名字 battery/charging
- var s = {
- battery_level: 0,
- battery_charging: false,
- network_type: "unknown",
- storage_free_mb: 0
- };
- try { s.battery_level = device.getBatteryLevel(); } catch (e) {}
- try { s.battery_charging = device.isCharging(); } catch (e) {}
- try { s.network_type = device.getNetworkType(); } catch (e) {}
- // 旧字段(保留防止老代码依赖)
- try { s.battery = s.battery_level; } catch (e) {}
- try { s.charging = s.battery_charging; } catch (e) {}
- try { s.network = s.network_type; } catch (e) {}
- return s;
- }
- // 完整的状态上报(所有情况下都保证字段名以"新规范"优先出现,不会有 null)
- function collectStatusFull() {
- var s = {
- battery_level: 0,
- battery_charging: false,
- network_type: "unknown",
- signal_level: 0,
- storage_total_mb: 0,
- storage_free_mb: 0,
- ram_free_mb: 0,
- screen_on: true,
- brightness: 0,
- volume: 0,
- sdk: 0,
- brand: "",
- model: "",
- debug: ""
- };
- var errors = [];
- var gotInfo = false;
- try {
- var info = device.getInfo && device.getInfo();
- if (info && typeof info === "object") {
- for (var k in info) if (info.hasOwnProperty(k)) {
- try { s[k] = info[k]; } catch (e) {}
- }
- gotInfo = true;
- }
- } catch (e) { errors.push("getInfo:" + e.message); }
- // 逐字段兜底(getInfo 里的字段名可能五花八门,所以每个都重新单独读一次覆盖)
- try { s.battery_level = device.getBatteryLevel(); } catch (e) { errors.push("battery:" + e.message); }
- try { s.battery_charging = device.isCharging(); } catch (e) { errors.push("charging:" + e.message); }
- try { s.network_type = device.getNetworkType(); } catch (e) { errors.push("net:" + e.message); }
- try { s.signal_level = device.getSignalLevel(); } catch (e) { errors.push("signal:" + e.message); }
- try { s.storage_total_mb = device.getTotalInternalMemorySize() / 1048576; } catch (e) { errors.push("stotal:" + e.message); }
- try { s.storage_free_mb = device.getAvailableInternalMemorySize() / 1048576; } catch (e) { errors.push("sfree:" + e.message); }
- try { s.ram_free_mb = device.getAvailableMemorySize() / 1048576; } catch (e) { errors.push("ram:" + e.message); }
- try { s.screen_on = device.isScreenOn(); } catch (e) { errors.push("screen:" + e.message); }
- try { s.brightness = device.getScreenBrightness(); } catch (e) { errors.push("bright:" + e.message); }
- try { s.volume = device.getMusicVolume(); } catch (e) { errors.push("vol:" + e.message); }
- try { s.sdk = device.getSdkVersion(); } catch (e) { errors.push("sdk:" + e.message); }
- try { s.brand = device.getBrand(); } catch (e) { errors.push("brand:" + e.message); }
- try { s.model = device.getModel(); } catch (e) { errors.push("model:" + e.message); }
- // 关键:老字段名 → 新字段名 反向同步(getInfo 里可能只有老名字,而单独API又抛异常失败)
- try {
- if ((s.battery_level === 0 || s.battery_level == null) && s.battery && typeof s.battery === "number") {
- s.battery_level = s.battery;
- }
- } catch (e) {}
- try {
- if (s.battery_charging == null && s.charging != null) {
- s.battery_charging = !!s.charging;
- }
- } catch (e) {}
- try {
- if ((!s.network_type || s.network_type === "unknown") && s.network) {
- s.network_type = String(s.network);
- }
- } catch (e) {}
- try {
- if (s.signal_level === 0 && s.signal != null && typeof s.signal === "number") {
- s.signal_level = s.signal;
- }
- } catch (e) {}
- try {
- if (s.storage_free_mb === 0 && s.storage_free != null && typeof s.storage_free === "number") {
- s.storage_free_mb = s.storage_free;
- }
- } catch (e) {}
- try {
- if (s.ram_free_mb === 0 && s.ram_free != null && typeof s.ram_free === "number") {
- s.ram_free_mb = s.ram_free;
- }
- } catch (e) {}
- // 同时保证"老字段名"也存在(兼容旧的上层逻辑 / 旧控制面板)
- try { if (s.battery == null) s.battery = s.battery_level; } catch (e) {}
- try { if (s.charging == null) s.charging = s.battery_charging; } catch (e) {}
- try { if (!s.network) s.network = s.network_type; } catch (e) {}
- if (errors.length) s.debug = errors.join("; ");
- s._info_ok = gotInfo;
- return s;
- }
- function clearPendingTimers() {
- // 清理所有 ACK 等待中的定时器(断线后不应继续重传,等重连后由离线队列重发)
- var keys = [];
- for (var k in G.pendingAcks) if (G.pendingAcks.hasOwnProperty(k)) keys.push(k);
- for (var i = 0; i < keys.length; i++) {
- var entry = G.pendingAcks[keys[i]];
- if (entry && entry.timerId) {
- try { runTime.stopTimeout(entry.timerId); } catch (e) {}
- }
- }
- G.pendingAcks = {};
- // 清理所有请求等待中的定时器
- var rkeys = [];
- for (var rk in G.pendingRequests) if (G.pendingRequests.hasOwnProperty(rk)) rkeys.push(rk);
- for (var j = 0; j < rkeys.length; j++) {
- var rentry = G.pendingRequests[rkeys[j]];
- if (rentry && rentry.timerId) {
- try { runTime.stopTimeout(rentry.timerId); } catch (e) {}
- // 不删除 entry,让 flushOfflineQueue 可以把请求重新发出
- }
- }
- }
- // ============================================================
- // 连接 & 重连
- // ============================================================
- function doConnect() {
- G.connected = false;
- var ws = new websocket();
- G.ws = ws;
- try { ws.setSocketID(G.socketID); } catch (e) {}
- try {
- ws.event(
- function () { // onopen
- G.connected = true;
- G.retryCount = 0;
- LOG("OK", "连接成功");
- // 模式 3:重连后请求补发
- requestResyncOnReconnect();
- // 模式 4:flush 离线队列
- flushOfflineQueue();
- // 模式 6:启动自适应心跳
- startHeartbeat();
- startNetworkCheck();
- // 模式 8:自动订阅频道
- autoSubscribeChannels();
- // 发送注册(携带设备信息给控制面板展示品牌/型号)
- var brand = ""; var model = "";
- try { brand = device.getBrand(); } catch (e) {}
- try { model = device.getModel(); } catch (e) {}
- sendMsg("REGISTER", {
- socket_id: G.socketID,
- version: "2.0",
- features: ["ack", "req_resp", "resync", "offline_queue",
- "chunked", "adaptive_heartbeat", "backpressure", "channels"]
- });
- // 上线后立刻报一次状态,让控制面板能看到
- try {
- var s = collectStatusFull();
- if (s) sendMsg("STATUS", s, { no_ack: true });
- } catch (e) {}
- },
- function (msg) { // onmessage
- try { handleServerMessage(msg); }
- catch (e) { LOG("ERROR", "消息处理异常: " + e.message); }
- },
- function (err) { // onerror
- LOG("WARN", "连接失败");
- G.connected = false;
- stopHeartbeat();
- stopNetworkCheck();
- clearPendingTimers();
- },
- function () { // onclose
- G.connected = false;
- stopHeartbeat();
- stopNetworkCheck();
- clearPendingTimers();
- }
- );
- } catch (e) {
- LOG("ERROR", "event注册失败: " + e.message);
- return false;
- }
- var url = "ws://" + CONFIG.SERVER_IP + ":" + CONFIG.SERVER_PORT + CONFIG.WS_PATH;
- try {
- ws.connet(url);
- return true;
- } catch (e) {
- LOG("ERROR", "connet异常: " + e.message);
- return false;
- }
- }
- function computeRetryMs(n) {
- var ms = CONFIG.RETRY_BASE_MS * Math.pow(2, n);
- return Math.min(ms, CONFIG.RETRY_MAX_MS) + Math.floor(Math.random() * 1000);
- }
- // ============================================================
- // 主循环
- // ============================================================
- function main() {
- LOG("INFO", "=== WebSocket 高级技巧合集 v2.0 启动 ===");
- LOG("INFO", "8大模式: ACK重传 / 请求响应 / 序列号补偿 / 离线队列 / 分片传输 / 自适应心跳 / 背压 / 频道订阅");
- while (G.running) {
- G.retryCount++;
- doConnect();
- var wait = 0;
- while (!G.connected && wait < 150) {
- sleep.millisecond(100);
- wait++;
- }
- if (!G.connected) {
- var ms = computeRetryMs(G.retryCount - 1);
- LOG("WARN", "连接失败 " + (ms / 1000).toFixed(1) + "s 后重试");
- try { G.ws.close(); } catch (e) {}
- sleep.millisecond(ms);
- continue;
- }
- while (G.running && G.connected) {
- sleep.millisecond(500);
- }
- if (!G.running) break;
- var ms2 = computeRetryMs(G.retryCount - 1);
- LOG("WARN", "断线 " + (ms2 / 1000).toFixed(1) + "s 后重连");
- try { G.ws.close(); } catch (e) {}
- sleep.millisecond(ms2);
- }
- // 清理
- stopHeartbeat();
- stopNetworkCheck();
- try { G.ws.close(); } catch (e) {}
- LOG("INFO", "脚本结束");
- exit();
- }
- // 启动
- try {
- main();
- } catch (e) {
- LOG("FATAL", e.message + "\n" + (e.stack || ""));
- try { G.ws.close(); } catch (e2) {}
- exit();
- }
复制代码
| |  | |  |
|