自动发帖软件

标题: 实战使用桌面控制WebSocket [打印本页]

作者: 发帖软件    时间: 3 小时前
标题: 实战使用桌面控制WebSocket
实战使用桌面控制WebSocket
实战使用桌面控制WebSocket 群发软件发帖工具

  1. /**
  2. * WebSocket 高级技巧合集 - AIWROK 版
  3. *
  4. *       ws-multi-pool 连接池 / JetSocket / Binary Transfer Protocol /
  5. *       Android WebSocket 实战 / 大文件分片上传)
  6. *
  7. * 包含 8 大特殊模式:
  8. *   1. ACK 确认 + 超时重传 —— 消息不丢(仿 TCP 可靠投递)
  9. *   2. 请求-响应配对 —— 发指令等回应(仿 HTTP 请求模式,带超时)
  10. *   3. 消息序列号 + 断线补偿 —— 重连后补发缺失消息
  11. *   4. 离线消息队列 —— 断线缓存,重连后自动 flush
  12. *   5. 分片传输大截图 —— 64KB 分块,避免大帧阻塞
  13. *   6. 自适应心跳 —— 弱网拉长心跳间隔,省电防断
  14. *   7. 背压控制 —— 发送速率限制,防止淹没对端
  15. *   8. 频道订阅模式 —— 动态订阅/取消订阅事件流
  16. *
  17. * AIWROK 原生 WebSocket API:
  18. *   setSocketID(String) / event(onopen,onmsg,onerr,onclose) / connet(String) / send(Object) / close()
  19. *   定时器:runTime.setTimeout(fn, ms) / runTime.stopTimeout(id)
  20. */

  21. // ============================================================
  22. //  配置
  23. // ============================================================
  24. var CONFIG = {
  25.     // WebSocket 服务器的局域网 IP 地址,需改为你 PC 的实际 IP
  26.     // 查看方式:Windows 下运行 ipconfig → 找 IPv4 地址(通常是 192.168.x.x)
  27.     // ⚠️ 手机和 PC 必须连接到同一个 WiFi 网段
  28.     SERVER_IP: "192.168.10.104",
  29.     // WebSocket 服务器监听的端口号,需与服务器端保持一致
  30.     // 本项目配套的 WebSocket测试服务器.py 默认使用 8886
  31.     SERVER_PORT: "8886",
  32.     // WebSocket 连接的 URL 路径(根路径为 "/")
  33.     // 如果服务器在特定路径上提供服务(如 "/ws"、"/api/v1/socket"),修改此处
  34.     // 最终连接地址格式: ws://{SERVER_IP}:{SERVER_PORT}{WS_PATH}
  35.     WS_PATH: "/",

  36.     // 1. ACK 确认
  37.     ACK_TIMEOUT_MS: 5000,        // 等待 ACK 超时
  38.     ACK_MAX_RETRY: 3,            // 最多重传 3 次

  39.     // 2. 请求-响应配对
  40.     REQ_TIMEOUT_MS: 10000,       // 请求超时 10 秒

  41.     // 3. 序列号
  42.     RESEND_ON_RECONNECT: true,  // 重连后是否请求补发缺失消息

  43.     // 4. 离线队列
  44.     OFFLINE_QUEUE_MAX: 1000,     // 离线队列最大缓存条数(分片传输时需要较大缓冲)

  45.     // 5. 分片传输
  46.     CHUNK_SIZE: 65536,           // 64KB 每片(RFC 标准分片大小)
  47.     LARGE_IMG_THRESHOLD_KB: 100, // 超过 100KB 的截图走分片
  48.     CHUNK_RETAIN_LAST_MS: 60000, // 保留最近一次分片原始数据用于重传,默认 60 秒

  49.     // 6. 自适应心跳
  50.     HEARTBEAT_MIN_MS: 15000,     // 网络好时 15 秒
  51.     HEARTBEAT_MAX_MS: 60000,     // 网络差时 60 秒
  52.     HEARTBEAT_TIMEOUT_MS: 15000,
  53.     NETWORK_CHECK_INTERVAL_MS: 30000, // 每 30 秒评估一次网络质量

  54.     // 7. 背压
  55.     SEND_RATE_LIMIT_MS: 5,       // 两次发送之间最少间隔 5ms(≈200条/秒上限,分片不阻塞)
  56.     SEND_BURST_MAX: 50,          // 允许突发 50 条(一次 23 片不被背压限流)

  57.     // 8. 截图参数(降低大尺寸手机的带宽消耗)
  58.     SCREENSHOT_JPEG_QUALITY: 80, // 0-100,越高质量越好体积越大
  59.     SCREENSHOT_MAX_WIDTH: 0,     // 最大宽度(像素),0=不缩放;建议低端机或省流量设 1080

  60.     // 9. 频道订阅
  61.     AUTO_SUBSCRIBE_CHANNELS: ["system", "cmd", "screenshot"],

  62.     // 通用
  63.     MAX_RETRY: 999,
  64.     RETRY_BASE_MS: 2000,
  65.     RETRY_MAX_MS: 30000,
  66.     DEBUG_LOG: true,             // 本脚本为演示,默认开日志
  67.     SHOW_LOG_WINDOW: false,

  68.     // ============================================================
  69.     //  降级模式(默认开启,避免 AIWROK 环境 ACK 强制 + 心跳过严导致的混乱)
  70.     //    RELAXED_MODE = true  → 所有消息默认 no_ack、ACK 重传不报错、心跳超时只警告不主动断
  71.     //    RELAXED_MODE = false → 完全启用 TCP 式可靠投递(需要服务器完全严格匹配协议)
  72.     // ============================================================
  73.     RELAXED_MODE: true,
  74.     RELAXED_ACK_REQUIRED: false,   // false = 不强制要求 ACK,超时直接丢弃,不打印 ERROR
  75.     RELAXED_HEARTBEAT_SOFT_FAIL: true  // true = 心跳超时仅降级网络质量,不主动 close
  76. };

  77. // ============================================================
  78. //  日志
  79. // ============================================================
  80. function LOG(tag, msg) {
  81.     if (!CONFIG.DEBUG_LOG && (tag === "DEBUG" || tag === "RECV" || tag === "SEND")) return;
  82.     try { printl("[" + tag + "] " + msg); } catch (e) {}
  83. }

  84. if (CONFIG.SHOW_LOG_WINDOW) {
  85.     try { logWindow.show(); } catch (e) {}
  86.     try { logWindow.setNoClickModel(); } catch (e) {}
  87. }

  88. // ============================================================
  89. //  全局状态
  90. // ============================================================
  91. var G = {
  92.     ws: null,
  93.     socketID: "phone_" + new Date().getTime(),
  94.     connected: false,
  95.     retryCount: 0,
  96.     running: true,
  97.     seq: 0,               // 发送序列号
  98.     lastReceivedSeq: 0,   // 收到服务端消息的最后序列号

  99.     // 1. ACK 确认
  100.     pendingAcks: {},       // seq -> { msg, retryCount, timerId, sentAt }

  101.     // 2. 请求-响应配对
  102.     pendingRequests: {},  // reqId -> { resolve, reject, timerId }

  103.     // 4. 离线队列
  104.     offlineQueue: [],

  105.     // 5. 分片传输
  106.     chunkTransfers: {},    // transferId -> { chunks:[], totalChunks, received, ts }
  107.     lastSentChunks: {},    // transferId -> { b64, chunkSize, width, height, ts } (发送端保留,用于 CHUNK_RESEND 重传)

  108.     // 6. 自适应心跳
  109.     heartbeatTimer: null,
  110.     currentHeartbeatMs: 25000,
  111.     networkQuality: "good", // good / fair / poor
  112.     lastPongAt: 0,
  113.     pingRtt: 0,

  114.     // 7. 背压
  115.     lastSendAt: 0,
  116.     sendBurstCount: 0,

  117.     // 8. 频道订阅
  118.     subscribedChannels: {}
  119. };

  120. // ============================================================
  121. //  消息构建 & 发送(含背压控制)
  122. // ============================================================
  123. function buildMsg(type, data, opts) {
  124.     G.seq++;
  125.     var msg = {
  126.         type: type,
  127.         seq: G.seq,
  128.         ts: new Date().getTime(),
  129.         device: { brand: "", model: "" },
  130.         data: data || {}
  131.     };
  132.     try { msg.device.brand = device.getBrand(); } catch (e) {}
  133.     try { msg.device.model = device.getModel(); } catch (e) {}
  134.     if (opts) {
  135.         if (opts.req_id) msg.req_id = opts.req_id;
  136.         if (opts.ack_for) msg.ack_for = opts.ack_for;
  137.         if (opts.channel) msg.channel = opts.channel;
  138.         if (opts.no_ack) msg.no_ack = true;  // 标记不需要 ACK
  139.     }
  140.     return msg;
  141. }

  142. /**
  143. * 发送消息(模式 7:背压控制)
  144. * 两次发送之间至少间隔 SEND_RATE_LIMIT_MS,但允许突发 SEND_BURST_MAX 条
  145. */
  146. function sendMsg(type, data, opts) {
  147.     // 降级模式:默认所有消息都无需 ACK,除非显式要求
  148.     if (CONFIG.RELAXED_MODE && !opts) opts = {};
  149.     if (CONFIG.RELAXED_MODE && opts && !opts.__force_ack) {
  150.         if (type !== "REGISTER" && type !== "SUBSCRIBE" && type !== "UNSUBSCRIBE") {
  151.             opts.no_ack = true;
  152.         }
  153.         // 对于 SUBSCRIBE / REGISTER:降级模式下也走 no_ack,减少混乱
  154.         if (!CONFIG.RELAXED_ACK_REQUIRED) {
  155.             opts.no_ack = true;
  156.         }
  157.     }

  158.     if (!G.ws || !G.connected) {
  159.         // 模式 4:离线队列 —— 断线时缓存
  160.         enqueueOffline(type, data, opts);
  161.         return -1;
  162.     }

  163.     // 分片消息(CHUNK_START/CHUNK_DATA/CHUNK_END)与 RESPONSE/STATUS:绕过背压
  164.     //   避免截图分片被限流后入队导致丢片
  165.     var bypassBackpressure = (
  166.         type === "CHUNK_START" || type === "CHUNK_DATA" || type === "CHUNK_END" ||
  167.         type === "RESPONSE" || type === "STATUS" || type === "SCREENSHOT_DATA" ||
  168.         type === "SCREENSHOT_FAIL" || type === "ACK" || type === "PONG"
  169.     );
  170.     if (!bypassBackpressure) {
  171.         // 背压控制(仅对控制类消息生效)
  172.         var now = new Date().getTime();
  173.         var elapsed = now - G.lastSendAt;
  174.         if (G.sendBurstCount >= CONFIG.SEND_BURST_MAX && elapsed < CONFIG.SEND_RATE_LIMIT_MS) {
  175.             // 超过突发上限且间隔不够 → 入队等待
  176.             LOG("WARN", "背压限制,消息入队等待 type=" + type);
  177.             enqueueOffline(type, data, opts);
  178.             return -1;
  179.         }
  180.         if (elapsed >= CONFIG.SEND_RATE_LIMIT_MS) {
  181.             G.sendBurstCount = 0; // 间隔够了,重置突发计数
  182.         }
  183.         G.sendBurstCount++;
  184.         G.lastSendAt = now;
  185.     }

  186.     var msg = buildMsg(type, data, opts);
  187.     var raw = JSON.stringify(msg);

  188.     try {
  189.         G.ws.send(raw);
  190.         LOG("SEND", type + " seq=" + msg.seq);

  191.         // 模式 1:需要 ACK 的消息 → 启动超时重传
  192.         if (!opts || !opts.no_ack) {
  193.             startAckTimer(msg);
  194.         }
  195.         return msg.seq;
  196.     } catch (e) {
  197.         LOG("ERROR", "send异常: " + e.message);
  198.         enqueueOffline(type, data, opts);
  199.         return -1;
  200.     }
  201. }

  202. // ============================================================
  203. //  模式 1:ACK 确认 + 超时重传
  204. //
  205. //  发送消息后,等对端回 ACK(seq)。
  206. //  超时未收到 → 重传(最多 ACK_MAX_RETRY 次)。
  207. //  超过最大重传次数 → 通知上层失败。
  208. // ============================================================
  209. function startAckTimer(msg) {
  210.     var entry = {
  211.         raw: JSON.stringify(msg),
  212.         retryCount: 0,
  213.         sentAt: new Date().getTime(),
  214.         timerId: null
  215.     };
  216.     G.pendingAcks[msg.seq] = entry;

  217.     entry.timerId = runTime.setTimeout(function () {
  218.         onAckTimeout(msg.seq);
  219.     }, CONFIG.ACK_TIMEOUT_MS);
  220. }

  221. function onAckTimeout(seq) {
  222.     var entry = G.pendingAcks[seq];
  223.     if (!entry) return; // 已经收到 ACK 了

  224.     entry.retryCount++;
  225.     if (entry.retryCount > CONFIG.ACK_MAX_RETRY) {
  226.         if (CONFIG.RELAXED_MODE) {
  227.             LOG("DEBUG", "ACK 未确认 seq=" + seq + "(降级模式自动忽略)");
  228.         } else {
  229.             LOG("ERROR", "ACK 重传超限 seq=" + seq + " 放弃");
  230.         }
  231.         delete G.pendingAcks[seq];
  232.         // 通知上层(可选:触发回调)
  233.         return;
  234.     }

  235.     if (CONFIG.RELAXED_MODE) {
  236.         LOG("DEBUG", "ACK 等待 seq=" + seq + " 第 " + entry.retryCount + " 次(降级模式不重传,仅等待)");
  237.         // 降级模式:不重传,继续等一次,下一次超时就 discard
  238.         entry.timerId = runTime.setTimeout(function () {
  239.             LOG("DEBUG", "ACK 最终未返回 seq=" + seq + "(降级模式丢弃)");
  240.             delete G.pendingAcks[seq];
  241.         }, CONFIG.ACK_TIMEOUT_MS);
  242.         return;
  243.     }

  244.     LOG("WARN", "ACK 超时 seq=" + seq + " 第 " + entry.retryCount + " 次重传");
  245.     if (G.ws && G.connected) {
  246.         try { G.ws.send(entry.raw); } catch (e) {}
  247.         // 重新启动超时计时
  248.         entry.timerId = runTime.setTimeout(function () {
  249.             onAckTimeout(seq);
  250.         }, CONFIG.ACK_TIMEOUT_MS);
  251.     } else {
  252.         // 连接断了,放回离线队列
  253.         delete G.pendingAcks[seq];
  254.         // 离线队列会在重连后 flush
  255.     }
  256. }

  257. function handleAck(seq) {
  258.     var entry = G.pendingAcks[seq];
  259.     if (!entry) {
  260.         LOG("DEBUG", "收到重复 ACK seq=" + seq);
  261.         return;
  262.     }
  263.     if (entry.timerId) {
  264.         try { runTime.stopTimeout(entry.timerId); } catch (e) {}
  265.     }
  266.     var rtt = new Date().getTime() - entry.sentAt;
  267.     LOG("DEBUG", "ACK 确认 seq=" + seq + " RTT=" + rtt + "ms");
  268.     delete G.pendingAcks[seq];
  269. }

  270. // ============================================================
  271. //  模式 2:请求-响应配对(仿 HTTP 请求模式)
  272. //
  273. //  sendRequest("GET_STATUS", { detailed: true })
  274. //    → 等服务端回 { type: "RESPONSE", req_id: "xxx", data: {...} }
  275. //    → 超时则 reject
  276. //
  277. //  AIWROK 不支持 Promise,这里用回调方式:
  278. //    sendRequest(type, data, onOk, onTimeout)
  279. // ============================================================
  280. var _reqCounter = 0;

  281. function sendRequest(type, data, onOk, onTimeout) {
  282.     _reqCounter++;
  283.     var reqId = "req_" + new Date().getTime() + "_" + _reqCounter;
  284.     var entry = {
  285.         reqId: reqId,
  286.         onOk: onOk,
  287.         onTimeout: onTimeout,
  288.         timerId: null
  289.     };
  290.     G.pendingRequests[reqId] = entry;

  291.     // 超时计时
  292.     entry.timerId = runTime.setTimeout(function () {
  293.         if (G.pendingRequests[reqId]) {
  294.             delete G.pendingRequests[reqId];
  295.             LOG("WARN", "请求超时 type=" + type + " reqId=" + reqId);
  296.             if (onTimeout) onTimeout("timeout");
  297.         }
  298.     }, CONFIG.REQ_TIMEOUT_MS);

  299.     sendMsg(type, data, { req_id: reqId });
  300.     LOG("DEBUG", "请求发出 type=" + type + " reqId=" + reqId);
  301.     return reqId;
  302. }

  303. function handleResponse(resp) {
  304.     var reqId = resp.req_id;
  305.     var entry = G.pendingRequests[reqId];
  306.     if (!entry) {
  307.         LOG("DEBUG", "收到过期响应 reqId=" + reqId);
  308.         return;
  309.     }
  310.     if (entry.timerId) {
  311.         try { runTime.stopTimeout(entry.timerId); } catch (e) {}
  312.     }
  313.     delete G.pendingRequests[reqId];
  314.     if (entry.onOk) entry.onOk(resp.data || resp);
  315.     LOG("DEBUG", "响应收到 reqId=" + reqId);
  316. }

  317. // ============================================================
  318. //  模式 3:消息序列号 + 断线补偿
  319. //
  320. //  每条收到的消息都带 seq。
  321. //  重连后发送 RESYNC(lastSeq),请求服务端补发 lastSeq 之后的消息。
  322. // ============================================================
  323. function handleIncomingSeq(seq) {
  324.     if (seq > 0) {
  325.         if (seq <= G.lastReceivedSeq) {
  326.             LOG("WARN", "重复消息 seq=" + seq + " (last=" + G.lastReceivedSeq + "),丢弃");
  327.             return false; // 重复消息
  328.         }
  329.         if (G.lastReceivedSeq > 0 && seq > G.lastReceivedSeq + 1) {
  330.             var gap = seq - G.lastReceivedSeq - 1;
  331.             LOG("WARN", "消息间隙 seq=" + seq + " 缺少 " + gap + " 条");
  332.             // 可选:请求补发
  333.             // sendMsg("RESYNC", { from: G.lastReceivedSeq + 1, to: seq - 1 });
  334.         }
  335.         G.lastReceivedSeq = seq;
  336.     }
  337.     return true;
  338. }

  339. function requestResyncOnReconnect() {
  340.     if (!CONFIG.RESEND_ON_RECONNECT) return;
  341.     if (G.lastReceivedSeq > 0) {
  342.         LOG("INFO", "重连后请求补发 seq>" + G.lastReceivedSeq);
  343.         sendMsg("RESYNC", { last_seq: G.lastReceivedSeq });
  344.     }
  345. }

  346. // ============================================================
  347. //  模式 4:离线消息队列
  348. //
  349. //  断线时所有发送的消息缓存到队列。
  350. //  重连后自动 flush(先进先出)。
  351. // ============================================================
  352. function enqueueOffline(type, data, opts) {
  353.     if (G.offlineQueue.length >= CONFIG.OFFLINE_QUEUE_MAX) {
  354.         G.offlineQueue.shift(); // 丢最老的
  355.         LOG("WARN", "离线队列已满,丢弃最旧消息");
  356.     }
  357.     G.offlineQueue.push({ type: type, data: data, opts: opts });
  358.     LOG("DEBUG", "入离线队列 type=" + type + " 队列长度=" + G.offlineQueue.length);
  359. }

  360. function flushOfflineQueue() {
  361.     if (G.offlineQueue.length === 0) return;
  362.     LOG("INFO", "开始 flush 离线队列 " + G.offlineQueue.length + " 条");
  363.     var count = 0;
  364.     while (G.offlineQueue.length > 0 && G.connected) {
  365.         var item = G.offlineQueue.shift();
  366.         // 直接发送,不走 sendMsg 的离线判断
  367.         var msg = buildMsg(item.type, item.data, item.opts);
  368.         try {
  369.             G.ws.send(JSON.stringify(msg));
  370.             if (!item.opts || !item.opts.no_ack) startAckTimer(msg);
  371.             count++;
  372.         } catch (e) {
  373.             // 发送失败,放回队列头部
  374.             G.offlineQueue.unshift(item);
  375.             break;
  376.         }
  377.         // 背压:每条之间稍微停顿
  378.         // AIWROK 中 sleep.millisecond 会阻塞,这里用极短间隔
  379.     }
  380.     LOG("INFO", "flush 完成 " + count + " 条");
  381. }

  382. // ============================================================
  383. // ============================================================
  384. //  模式 5:分片传输大截图
  385. //
  386. //  超过 LARGE_IMG_THRESHOLD_KB 的截图,分 CHUNK_SIZE 大小的块发送。
  387. //  协议:
  388. //    1. send { type: "CHUNK_START", transfer_id, total_chunks, mime, width, height }
  389. //    2. send { type: "CHUNK_DATA", transfer_id, chunk_index, data_base64 }
  390. //    3. send { type: "CHUNK_END", transfer_id }
  391. //
  392. //  本函数对截图 API 做 4 级 fallback,失败时尽量给出原因而不是 #0
  393. // ============================================================
  394. function takeScreenshotRobust() {
  395.     /**
  396.      * AIWROK 正确截图 API:优先 screen.screenShot(w,h,quality) 可缩放+控质量
  397.      * 失败时回退 screen.screenShotFull() → .toBase64()
  398.      * 返回 { base64, size_kb, width, height }
  399.      * 失败时返回 null
  400.      */
  401.     try {
  402.         var errs = [];
  403.         // 读取配置
  404.         var maxW = parseInt(CONFIG.SCREENSHOT_MAX_WIDTH) || 0;
  405.         var quality = parseInt(CONFIG.SCREENSHOT_JPEG_QUALITY);
  406.         if (!quality || quality < 10 || quality > 100) quality = 80;

  407.         // 0) 检查截图权限
  408.         try {
  409.             if (typeof permissionManger !== "undefined" && permissionManger) {
  410.                 var hasPerm = permissionManger.checkScreenShotPremission();
  411.                 if (hasPerm === false) {
  412.                     LOG("ERROR", "截图权限未开启!请在 AIWROK 权限管理中开启「截图/投屏」权限");
  413.                     return null;
  414.                 }
  415.             }
  416.         } catch (ePerm) { errs.push("perm_check:" + ePerm.message); }

  417.         // 计算目标尺寸(等比缩放)
  418.         var realW = 0, realH = 0;
  419.         try { realW = screen.getScreenWidth(); } catch (ew) {}
  420.         try { realH = screen.getScreenHeight(); } catch (eh) {}
  421.         var targetW = realW, targetH = realH;
  422.         if (maxW > 0 && realW > 0 && realH > 0 && realW > maxW) {
  423.             targetW = maxW;
  424.             targetH = Math.floor(realH * (maxW / realW));
  425.         }

  426.         // 1) 优先 screen.screenShot(w, h, quality):可同时控制分辨率和 JPEG 质量(减少带宽)
  427.         if (typeof screen !== "undefined" && screen && typeof screen.screenShot === "function") {
  428.             try {
  429.                 if (!targetW || !targetH) { targetW = realW || 1080; targetH = realH || 1920; }
  430.                 var imgA = screen.screenShot(targetW, targetH, quality);
  431.                 if (!imgA) throw new Error("screenShot 返回 null");
  432.                 var b64A = "";
  433.                 if (typeof imgA.toBase64 === "function") {
  434.                     b64A = "" + imgA.toBase64();
  435.                 } else if (typeof imgA.toBase64Jpg === "function") {
  436.                     b64A = "" + imgA.toBase64Jpg();
  437.                 } else {
  438.                     throw new Error("screenShot 返回对象无 toBase64 方法");
  439.                 }
  440.                 try { imgA.recycle(); } catch (er) {}
  441.                 if (!b64A || b64A.length < 10) throw new Error("Base64 为空 len=" + b64A.length);
  442.                 var sizeKBA = Math.floor((b64A.length * 0.75) / 1024);
  443.                 LOG("INFO", "截图 OK (screenShot scaled) size=" + sizeKBA + "KB " + targetW + "x" + targetH + " q=" + quality);
  444.                 return { base64: b64A, size_kb: sizeKBA, width: targetW, height: targetH };
  445.             } catch (eA) {
  446.                 errs.push("screenShot(" + targetW + "," + targetH + "," + quality + "):" + (eA.message || eA));
  447.             }
  448.         }

  449.         // 2) 回退 screen.screenShotFull()(AIWROK 主要截图 API)
  450.         if (typeof screen !== "undefined" && screen && typeof screen.screenShotFull === "function") {
  451.             try {
  452.                 var img = screen.screenShotFull();
  453.                 if (!img) throw new Error("screenShotFull 返回 null");

  454.                 var w = realW, h = realH;
  455.                 if (!w || !h) {
  456.                     try { w = img.getWidth(); } catch (ew2) {}
  457.                     try { h = img.getHeight(); } catch (eh2) {}
  458.                 }

  459.                 var b64 = "";
  460.                 if (typeof img.toBase64 === "function") {
  461.                     b64 = "" + img.toBase64();
  462.                 } else if (typeof img.toBase64Jpg === "function") {
  463.                     b64 = "" + img.toBase64Jpg();
  464.                 } else {
  465.                     throw new Error("截图对象没有 toBase64 方法");
  466.                 }

  467.                 try { img.recycle(); } catch (er) {}
  468.                 if (!b64 || b64.length < 10) throw new Error("Base64 为空或过短 len=" + (b64 ? b64.length : 0));

  469.                 var sizeKB = Math.floor((b64.length * 0.75) / 1024);
  470.                 LOG("INFO", "截图 OK size=" + sizeKB + "KB width=" + w + " height=" + h);
  471.                 return { base64: b64, size_kb: sizeKB, width: w, height: h };
  472.             } catch (e1) {
  473.                 errs.push("screenShotFull:" + (e1.message || e1));
  474.             }
  475.         } else {
  476.             errs.push("screenShotFull_not_found");
  477.         }

  478.         // 3) 最后兜底:尝试 image.* 系列
  479.         if (typeof image !== "undefined" && image) {
  480.             var tryList = ["fastCapture", "screenshot", "captureScreen", "capture"];
  481.             for (var ti = 0; ti < tryList.length; ti++) {
  482.                 var fnName = tryList[ti];
  483.                 try {
  484.                     if (typeof image[fnName] === "function") {
  485.                         var img3 = image[fnName]();
  486.                         if (img3) {
  487.                             var w3 = 0, h3 = 0;
  488.                             try { w3 = img3.getWidth(); } catch (ew3) {}
  489.                             try { h3 = img3.getHeight(); } catch (eh3) {}
  490.                             var b64_3 = "" + img3.toBase64();
  491.                             try { img3.recycle(); } catch (er) {}
  492.                             var sizeKB3 = Math.floor((b64_3.length * 0.75) / 1024);
  493.                             LOG("INFO", "截图 OK (image." + fnName + ") size=" + sizeKB3 + "KB");
  494.                             return { base64: b64_3, size_kb: sizeKB3, width: w3, height: h3 };
  495.                         }
  496.                     }
  497.                 } catch (e3) { errs.push(fnName + ":" + e3.message); }
  498.             }
  499.         }

  500.         LOG("ERROR", "所有截图 API 均失败: " + errs.join(" | "));
  501.         LOG("ERROR", "常见原因:(1) AIWROK 未开启「投屏/截图」权限;(2) 应用未启动前台服务;(3) Android 10+ 需要 MediaProjection 授权");
  502.         return null;
  503.     } catch (eOuter) {
  504.         try { LOG("ERROR", "截图最外层异常: " + (eOuter.message || String(eOuter))); } catch (eLog) {}
  505.         return null;
  506.     }
  507. }

  508. function sendScreenshotChunked(shotID) {
  509.     // ⚠️ 最外层 try-catch:防止 java/Android 底层异常直接变成【错误】#0
  510.     try {
  511.         var shot = takeScreenshotRobust();
  512.         if (!shot) {
  513.             // 把失败原因回传给控制面板(便于显示)
  514.             try { sendMsg("SCREENSHOT_FAIL", { id: shotID || "", reason: "capture_failed" }, { no_ack: true }); } catch (eSend) {}
  515.             return { ok: false, reason: "capture_failed", error_msg: "所有截图API均失败,请检查AIWROK投屏权限" };
  516.         }
  517.         var sizeKB = shot.size_kb;
  518.         var b64 = shot.base64;

  519.         if (sizeKB < CONFIG.LARGE_IMG_THRESHOLD_KB) {
  520.             // 小图直接发
  521.             sendMsg("SCREENSHOT", {
  522.                 id: shotID, format: "jpeg",
  523.                 width: shot.width, height: shot.height,
  524.                 size_kb: sizeKB, image_base64: b64
  525.             }, { no_ack: true });
  526.             return { ok: true, size_kb: sizeKB, width: shot.width, height: shot.height, chunked: false };
  527.         }

  528.         // 大图分片
  529.         var transferId = "tf_" + new Date().getTime();
  530.         var chunkSize = CONFIG.CHUNK_SIZE;
  531.         var totalChunks = Math.ceil(b64.length / chunkSize);

  532.         LOG("INFO", "分片传输 transferId=" + transferId + " 总片数=" + totalChunks + " size=" + sizeKB + "KB");

  533.         // 保留原始 Base64 和参数,用于 CHUNK_RESEND 重传
  534.         G.lastSentChunks[transferId] = {
  535.             b64: b64,
  536.             chunkSize: chunkSize,
  537.             width: shot.width,
  538.             height: shot.height,
  539.             sizeKB: sizeKB,
  540.             totalChunks: totalChunks,
  541.             ts: new Date().getTime()
  542.         };
  543.         // 清理过期记录,避免内存无限增长
  544.         var _keepMs = CONFIG.CHUNK_RETAIN_LAST_MS || 60000;
  545.         var _now = new Date().getTime();
  546.         var _keys = [];
  547.         for (var _k in G.lastSentChunks) if (G.lastSentChunks.hasOwnProperty(_k)) _keys.push(_k);
  548.         for (var _ki = 0; _ki < _keys.length; _ki++) {
  549.             if (_now - (G.lastSentChunks[_keys[_ki]].ts || 0) > _keepMs) delete G.lastSentChunks[_keys[_ki]];
  550.         }

  551.         sendMsg("CHUNK_START", {
  552.             transfer_id: transferId,
  553.             total_chunks: totalChunks,
  554.             mime: "jpeg",
  555.             width: shot.width,
  556.             height: shot.height,
  557.             size_kb: sizeKB
  558.         });

  559.         for (var i = 0; i < totalChunks; i++) {
  560.             var start = i * chunkSize;
  561.             var end = Math.min(start + chunkSize, b64.length);
  562.             var chunk = b64.substring(start, end);
  563.             sendMsg("CHUNK_DATA", {
  564.                 transfer_id: transferId,
  565.                 chunk_index: i,
  566.                 data_base64: chunk
  567.             });
  568.             // 分片之间 1ms 让步即可,不再 5 片睡 10ms(绕过背压后不需要额外节流)
  569.             if ((i + 1) % 20 === 0) {
  570.                 try { sleep.millisecond(1); } catch (e) {}
  571.             }
  572.         }

  573.         sendMsg("CHUNK_END", { transfer_id: transferId });
  574.         LOG("INFO", "分片传输完成 transferId=" + transferId);
  575.         return { ok: true, size_kb: sizeKB, width: shot.width, height: shot.height, chunked: true, transfer_id: transferId, total_chunks: totalChunks };
  576.     } catch (eOuter) {
  577.         try { LOG("ERROR", "sendScreenshotChunked 最外层异常: " + (eOuter.message || String(eOuter))); } catch (eLog) {}
  578.         try { sendMsg("SCREENSHOT_FAIL", { id: shotID || "", reason: "exception", error_msg: (eOuter.message || String(eOuter)) }, { no_ack: true }); } catch (eSend) {}
  579.         return { ok: false, reason: "exception", error_msg: (eOuter.message || String(eOuter)) };
  580.     }
  581. }

  582. // ============================================================
  583. //  辅助函数 Polyfill(AIWROK 环境全局函数缺失兜底)
  584. //  ⚠️ 关键策略:所有函数同时挂载到全局变量 + PhoneCtrl 对象
  585. //     即使变量名与 AIWROK 引擎保留名冲突导致赋值失败,
  586. //     PhoneCtrl 对象上的函数仍然可用,handleRemoteCommand 会优先使用
  587. // ============================================================
  588. var PhoneCtrl = PhoneCtrl || {};

  589. (function _polyfill_helpers() {
  590.     try {
  591.     function _has(obj, name) { return !!(obj && typeof obj[name] === "function"); }

  592.     // 尝试在全局作用域赋值(安全方式,使用全局对象属性)
  593.     function _tryAssignGlobal(name, fn) {
  594.         try {
  595.             var g = (typeof window !== "undefined") ? window :
  596.                     (typeof globalThis !== "undefined") ? globalThis :
  597.                     (typeof runtime !== "undefined") ? runtime : this;
  598.             g[name] = fn;
  599.             return true;
  600.         } catch (e) {
  601.             return false;
  602.         }
  603.     }

  604.     // 尝试在全局作用域读取(安全方式)
  605.     function _tryGetGlobal(name) {
  606.         try {
  607.             var g = (typeof window !== "undefined") ? window :
  608.                     (typeof globalThis !== "undefined") ? globalThis :
  609.                     (typeof runtime !== "undefined") ? runtime : this;
  610.             return g[name];
  611.         } catch (e) { return undefined; }
  612.     }

  613.     // 通用:auto API 直接调用兜底
  614.     var _autoClick = function (x, y) {
  615.         if (_has(auto, "clickPoint")) return auto.clickPoint(x, y);
  616.         if (_has(auto, "clickPercent")) {
  617.             var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
  618.             return auto.clickPercent(x / sw, y / sh);
  619.         }
  620.         throw new Error("no_click_api");
  621.     };
  622.     var _autoSwipe = function (x1, y1, x2, y2, dur) {
  623.         if (_has(auto, "swip")) return auto.swip(x1, y1, x2, y2, dur, 0);
  624.         throw new Error("no_swipe_api");
  625.     };
  626.     var _autoInput = function (text) {
  627.         if (_has(auto, "input")) return auto.input(text);
  628.         if (_has(auto, "sendText")) return auto.sendText(text);
  629.         if (_has(auto, "inputText")) return auto.inputText(text);
  630.         throw new Error("no_input_api");
  631.     };
  632.     var _autoPress = function (fnName) {
  633.         if (_has(auto, fnName)) return auto[fnName]();
  634.         throw new Error("auto." + fnName + "_not_found");
  635.     };
  636.     var _keyPress = function (keyName, keyCode) {
  637.         if (_has(device, keyName)) return device[keyName]();
  638.         if (_has(device, "pressKey")) return device.pressKey(keyCode);
  639.         if (_has(device, "press")) return device.press(keyCode);
  640.         try {
  641.             var KeyEvent = android.view.KeyEvent;
  642.             if (_has(runtime, "postEvent")) return runtime.postEvent(KeyEvent.ACTION_DOWN, keyCode);
  643.         } catch (e) {}
  644.         throw new Error("no_" + keyName + "_api");
  645.     };

  646.     // click
  647.     PhoneCtrl.click = function (x, y) {
  648.         x = parseInt(x || 0); y = parseInt(y || 0);
  649.         try { return _autoClick(x, y); } catch (e1) {}
  650.         try { if (_has(gesture, "click")) return gesture.click(x, y); } catch (e2) {}
  651.         try { if (_has(device, "click")) return device.click(x, y); } catch (e3) {}
  652.         throw new Error("click_failed");
  653.     };
  654.     _tryAssignGlobal("click", PhoneCtrl.click);

  655.     // swipe
  656.     PhoneCtrl.swipe = function (x1, y1, x2, y2, duration) {
  657.         x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
  658.         x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
  659.         duration = parseInt(duration || 400);
  660.         try { return _autoSwipe(x1, y1, x2, y2, duration); } catch (e1) {}
  661.         try {
  662.             if (_has(auto, "dispatchGesture")) {
  663.                 gesture.setDurTime(duration);
  664.                 gesture.addPoint(x1, y1);
  665.                 gesture.addPoint(x2, y2);
  666.                 return auto.dispatchGesture([gesture]);
  667.             }
  668.         } catch (e2) {}
  669.         try { if (_has(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, duration); } catch (e3) {}
  670.         try { if (_has(device, "swipe")) return device.swipe(x1, y1, x2, y2, duration); } catch (e4) {}
  671.         throw new Error("swipe_failed");
  672.     };
  673.     _tryAssignGlobal("swipe", PhoneCtrl.swipe);

  674.     // input
  675.     PhoneCtrl.input = function (text) {
  676.         text = String(text == null ? "" : text);
  677.         try { return _autoInput(text); } catch (e1) {}
  678.         try { if (_has(device, "input")) return device.input(text); } catch (e2) {}
  679.         throw new Error("input_failed");
  680.     };
  681.     _tryAssignGlobal("input", PhoneCtrl.input);

  682.     // home / back / menu / power / search / camera
  683.     PhoneCtrl.home = function () {
  684.         try { return _autoPress("home"); } catch (e) {}
  685.         return _keyPress("home", 3);
  686.     };
  687.     PhoneCtrl.back = function () {
  688.         try { return _autoPress("back"); } catch (e) {}
  689.         return _keyPress("back", 4);
  690.     };
  691.     PhoneCtrl.menu = function () {
  692.         try { return _autoPress("menu"); } catch (e) {}
  693.         return _keyPress("menu", 82);
  694.     };
  695.     PhoneCtrl.power = function () {
  696.         try { return _autoPress("power"); } catch (e) {}
  697.         return _keyPress("power", 26);
  698.     };
  699.     PhoneCtrl.search = function () {
  700.         try { return _autoPress("search"); } catch (e) {}
  701.         return _keyPress("search", 84);
  702.     };
  703.     PhoneCtrl.camera = function () {
  704.         try { return _autoPress("camera"); } catch (e) {}
  705.         return _keyPress("camera", 27);
  706.     };

  707.     // recentApps
  708.     PhoneCtrl.recentApps = function () {
  709.         try { return _autoPress("recents"); } catch (e1) {}
  710.         try { return _autoPress("recentApps"); } catch (e2) {}
  711.         try {
  712.             var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
  713.             return PhoneCtrl.swipe(Math.floor(sw/2), sh - 20, Math.floor(sw/2), Math.floor(sh/2), 300);
  714.         } catch (e3) {}
  715.         return _keyPress("recentApps", 187);
  716.     };

  717.     // expandNotificationBar
  718.     PhoneCtrl.expandNotificationBar = function () {
  719.         try { return _autoPress("statusBar"); } catch (e1) {}
  720.         try { return _autoPress("notification"); } catch (e2) {}
  721.         try {
  722.             var sw = screen.getScreenWidth();
  723.             return PhoneCtrl.swipe(Math.floor(sw/2), 10, Math.floor(sw/2), 400, 300);
  724.         } catch (e3) { throw new Error("no_noti_api"); }
  725.     };

  726.     // openApp
  727.     PhoneCtrl.openApp = function (pkg) {
  728.         pkg = String(pkg || "");
  729.         try {
  730.             if (typeof app !== "undefined" && app) {
  731.                 if (_has(app, "openApp")) return app.openApp(pkg);
  732.                 if (_has(app, "openAppv2")) return app.openAppv2(pkg);
  733.                 if (_has(app, "openAppSeting")) return app.openAppSeting(pkg);
  734.                 if (_has(app, "launch")) return app.launch(pkg);
  735.                 if (_has(app, "open")) return app.open(pkg);
  736.             }
  737.         } catch (e1) {}
  738.         try {
  739.             if (typeof device !== "undefined" && device) {
  740.                 if (_has(device, "startApp")) return device.startApp(pkg);
  741.                 if (_has(device, "launchApp")) return device.launchApp(pkg);
  742.                 if (_has(device, "openApp")) return device.openApp(pkg);
  743.             }
  744.         } catch (e2) {}
  745.         try {
  746.             var ctxt = context || runtime.getContext();
  747.             var Intent = android.content.Intent;
  748.             var pm = ctxt.getPackageManager();
  749.             var launchIntent = pm.getLaunchIntentForPackage(pkg);
  750.             if (launchIntent) {
  751.                 launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  752.                 ctxt.startActivity(launchIntent);
  753.                 return true;
  754.             }
  755.             throw new Error("no_launch_intent");
  756.         } catch (e3) { throw new Error("openApp_fail: " + e3.message); }
  757.     };

  758.     // openAppName
  759.     PhoneCtrl.openAppName = function (name) {
  760.         name = String(name || "");
  761.         try {
  762.             if (typeof app !== "undefined" && app) {
  763.                 if (_has(app, "openApp")) return app.openApp(name);
  764.                 if (_has(app, "openAppv2")) return app.openAppv2(name);
  765.                 if (_has(app, "launchByName")) return app.launchByName(name);
  766.                 if (_has(app, "openByName")) return app.openByName(name);
  767.             }
  768.         } catch (e1) {}
  769.         try {
  770.             if (typeof device !== "undefined" && device) {
  771.                 if (_has(device, "openAppByName")) return device.openAppByName(name);
  772.                 if (_has(device, "launchByName")) return device.launchByName(name);
  773.             }
  774.         } catch (e2) {}
  775.         try {
  776.             var ctxt = context || runtime.getContext();
  777.             var pm = ctxt.getPackageManager();
  778.             var apps = pm.getInstalledApplications(0);
  779.             var target = name.toLowerCase();
  780.             var iter = apps.iterator();
  781.             while (iter.hasNext()) {
  782.                 var ai = iter.next();
  783.                 var label = String(pm.getApplicationLabel(ai) || "").toLowerCase();
  784.                 var pkg = String(ai.packageName || "").toLowerCase();
  785.                 if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
  786.                     return PhoneCtrl.openApp(ai.packageName);
  787.                 }
  788.             }
  789.             throw new Error("pkg_not_found_for_name_" + name);
  790.         } catch (e3) { throw new Error("openAppName_fail: " + e3.message); }
  791.     };

  792.     // killApp
  793.     PhoneCtrl.killApp = function (pkg) {
  794.         pkg = String(pkg || "");
  795.         try {
  796.             if (typeof app !== "undefined" && app) {
  797.                 if (_has(app, "forceStop")) return app.forceStop(pkg);
  798.                 if (_has(app, "kill")) return app.kill(pkg);
  799.                 if (_has(app, "close")) return app.close(pkg);
  800.             }
  801.         } catch (e1) {}
  802.         try {
  803.             if (typeof device !== "undefined" && device) {
  804.                 if (_has(device, "forceStopApp")) return device.forceStopApp(pkg);
  805.                 if (_has(device, "killApp")) return device.killApp(pkg);
  806.                 if (_has(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
  807.             }
  808.         } catch (e2) {}
  809.         try {
  810.             var ctxt = context || runtime.getContext();
  811.             var actMan = ctxt.getSystemService(android.content.Context.ACTIVITY_SERVICE);
  812.             actMan.killBackgroundProcesses(pkg);
  813.             return true;
  814.         } catch (e3) { throw new Error("killApp_fail: " + e3.message); }
  815.     };

  816.     // 全局变量赋值(尽力而为,失败不影响 PhoneCtrl 上的函数)
  817.     _tryAssignGlobal("home", PhoneCtrl.home);
  818.     _tryAssignGlobal("back", PhoneCtrl.back);
  819.     _tryAssignGlobal("menu", PhoneCtrl.menu);
  820.     _tryAssignGlobal("power", PhoneCtrl.power);
  821.     _tryAssignGlobal("search", PhoneCtrl.search);
  822.     _tryAssignGlobal("camera", PhoneCtrl.camera);
  823.     _tryAssignGlobal("recentApps", PhoneCtrl.recentApps);
  824.     _tryAssignGlobal("expandNotificationBar", PhoneCtrl.expandNotificationBar);
  825.     _tryAssignGlobal("openApp", PhoneCtrl.openApp);
  826.     _tryAssignGlobal("openAppName", PhoneCtrl.openAppName);
  827.     _tryAssignGlobal("killApp", PhoneCtrl.killApp);

  828.     LOG("INFO", "Polyfill 初始化完成 PhoneCtrl.click/swipe/input/home/back 等已就绪");
  829.     } catch (ePolyOuter) {
  830.         try { LOG("WARN", "polyfill 异常: " + (ePolyOuter.message || String(ePolyOuter))); } catch (eLog) {}
  831.     }
  832. })();

  833. /**
  834. * 接收端:组装分片(如果手机也需要接收大文件的话)
  835. */
  836. function handleChunkStart(data) {
  837.     G.chunkTransfers[data.transfer_id] = {
  838.         chunks: {},
  839.         totalChunks: data.total_chunks,
  840.         received: 0,
  841.         meta: data
  842.     };
  843.     LOG("DEBUG", "分片接收开始 transferId=" + data.transfer_id + " 总片数=" + data.totalChunks);
  844. }

  845. function handleChunkData(data) {
  846.     var transfer = G.chunkTransfers[data.transfer_id];
  847.     if (!transfer) {
  848.         LOG("WARN", "未知分片传输 transferId=" + data.transfer_id);
  849.         return;
  850.     }
  851.     transfer.chunks[data.chunk_index] = data.data_base64;
  852.     transfer.received++;

  853.     // 进度
  854.     var pct = Math.floor(transfer.received / transfer.totalChunks * 100);
  855.     if (transfer.received % 10 === 0 || transfer.received === transfer.totalChunks) {
  856.         LOG("DEBUG", "分片接收进度 " + pct + "% (" + transfer.received + "/" + transfer.totalChunks + ")");
  857.     }
  858. }

  859. function handleChunkEnd(data) {
  860.     var transfer = G.chunkTransfers[data.transfer_id];
  861.     if (!transfer) return;

  862.     if (transfer.received < transfer.totalChunks) {
  863.         LOG("WARN", "分片不完整 " + transfer.received + "/" + transfer.totalChunks);
  864.         // 可请求重传缺失的片
  865.         var missing = [];
  866.         for (var i = 0; i < transfer.totalChunks; i++) {
  867.             if (!transfer.chunks[i]) missing.push(i);
  868.         }
  869.         sendMsg("CHUNK_RESEND", {
  870.             transfer_id: data.transfer_id,
  871.             missing_indices: missing
  872.         });
  873.         return;
  874.     }

  875.     // 组装
  876.     var fullB64 = "";
  877.     for (var i = 0; i < transfer.totalChunks; i++) {
  878.         fullB64 += transfer.chunks[i];
  879.     }
  880.     LOG("INFO", "分片组装完成 transferId=" + data.transfer_id + " 总大小=" + Math.floor(fullB64.length / 1024) + "KB");

  881.     // 这里可以解码 Base64 → Bitmap → 保存文件等
  882.     delete G.chunkTransfers[data.transfer_id];
  883. }

  884. /**
  885. * 处理对端发来的 CHUNK_RESEND(缺失分片重传请求)
  886. * 从 G.lastSentChunks 中取出该 transferId 的原始 b64,按索引重新发送对应片
  887. */
  888. function handleChunkResend(data) {
  889.     var transferId = data.transfer_id;
  890.     var missing = data.missing_indices || [];
  891.     var cached = G.lastSentChunks[transferId];
  892.     if (!cached) {
  893.         LOG("WARN", "收到 CHUNK_RESEND 但本地已无缓存 transferId=" + transferId);
  894.         return;
  895.     }
  896.     LOG("INFO", "收到 CHUNK_RESEND transferId=" + transferId + " 缺失片数=" + missing.length);
  897.     var b64 = cached.b64;
  898.     var chunkSize = cached.chunkSize;
  899.     var reSentCount = 0;
  900.     for (var mi = 0; mi < missing.length; mi++) {
  901.         var idx = parseInt(missing[mi]);
  902.         if (isNaN(idx) || idx < 0) continue;
  903.         var start = idx * chunkSize;
  904.         if (start >= b64.length) continue;
  905.         var end = Math.min(start + chunkSize, b64.length);
  906.         var chunk = b64.substring(start, end);
  907.         sendMsg("CHUNK_DATA", {
  908.             transfer_id: transferId,
  909.             chunk_index: idx,
  910.             data_base64: chunk
  911.         });
  912.         reSentCount++;
  913.         // 每 20 片让 1ms
  914.         if (reSentCount % 20 === 0) { try { sleep.millisecond(1); } catch (e) {} }
  915.     }
  916.     LOG("INFO", "CHUNK_RESEND 已重传 " + reSentCount + " 片 transferId=" + transferId);
  917. }

  918. // ============================================================
  919. //  模式 6:自适应心跳
  920. //
  921. //  根据网络质量动态调整心跳间隔:
  922. //    good → 15s   fair → 30s   poor → 60s
  923. //  网络质量评估依据:PING RTT + 最近是否断线频繁
  924. // ============================================================
  925. function assessNetworkQuality() {
  926.     var rtt = G.pingRtt || 0;
  927.     var oldQ = G.networkQuality;

  928.     if (rtt === 0 || rtt < 100) {
  929.         G.networkQuality = "good";
  930.         G.currentHeartbeatMs = CONFIG.HEARTBEAT_MIN_MS;
  931.     } else if (rtt < 500) {
  932.         G.networkQuality = "fair";
  933.         G.currentHeartbeatMs = Math.floor((CONFIG.HEARTBEAT_MIN_MS + CONFIG.HEARTBEAT_MAX_MS) / 2);
  934.     } else {
  935.         G.networkQuality = "poor";
  936.         G.currentHeartbeatMs = CONFIG.HEARTBEAT_MAX_MS;
  937.     }

  938.     if (oldQ !== G.networkQuality) {
  939.         LOG("INFO", "网络质量变化 " + oldQ + " → " + G.networkQuality + " (RTT=" + rtt + "ms) 心跳=" + (G.currentHeartbeatMs / 1000) + "s");
  940.         // 重启心跳
  941.         startHeartbeat();
  942.     }
  943. }

  944. function startHeartbeat() {
  945.     stopHeartbeat();
  946.     G.lastPongAt = new Date().getTime();

  947.     function tick() {
  948.         if (!G.connected) return;

  949.         var now = new Date().getTime();
  950.         var timeoutThresh = G.currentHeartbeatMs + CONFIG.HEARTBEAT_TIMEOUT_MS;
  951.         var sincePong = now - G.lastPongAt;
  952.         if (sincePong > timeoutThresh) {
  953.             if (CONFIG.RELAXED_MODE && CONFIG.RELAXED_HEARTBEAT_SOFT_FAIL) {
  954.                 // 降级模式:不主动断开,只降级网络质量 + 发警告
  955.                 if (G.networkQuality === "good") { G.networkQuality = "fair"; G.currentHeartbeatMs = Math.floor((CONFIG.HEARTBEAT_MIN_MS + CONFIG.HEARTBEAT_MAX_MS) / 2); }
  956.                 else if (G.networkQuality === "fair") { G.networkQuality = "poor"; G.currentHeartbeatMs = CONFIG.HEARTBEAT_MAX_MS; }
  957.                 LOG("WARN", "心跳未收到 PONG 已 " + (sincePong / 1000).toFixed(0) + "s,降级网络质量为 " + G.networkQuality + "(降级模式不断线)");
  958.                 G.lastPongAt = now; // 重置计时,避免反复告警风暴
  959.             } else {
  960.                 LOG("WARN", "心跳超时 主动断开");
  961.                 try { G.ws.close(); } catch (e) {}
  962.                 return;
  963.             }
  964.         }

  965.         var pingTs = new Date().getTime();
  966.         sendMsg("PING", { t: pingTs }, { no_ack: true });

  967.         // 下一次延迟 = 当前自适应心跳间隔
  968.         G.heartbeatTimer = runTime.setTimeout(tick, G.currentHeartbeatMs);
  969.     }

  970.     // 注意:AIWROK 中 onopen 回调执行顺序可能延迟,这里也设置 lastPongAt
  971.     G.lastPongAt = new Date().getTime();
  972.     G.heartbeatTimer = runTime.setTimeout(tick, G.currentHeartbeatMs);
  973.     LOG("DEBUG", "心跳已启动 间隔=" + (G.currentHeartbeatMs / 1000) + "s 质量=" + G.networkQuality + (CONFIG.RELAXED_MODE ? " 降级模式=开" : ""));
  974. }

  975. function stopHeartbeat() {
  976.     if (G.heartbeatTimer) {
  977.         try { runTime.stopTimeout(G.heartbeatTimer); } catch (e) {}
  978.         G.heartbeatTimer = null;
  979.     }
  980. }

  981. // 定时评估网络质量
  982. var networkCheckTimer = null;
  983. function startNetworkCheck() {
  984.     stopNetworkCheck();
  985.     networkCheckTimer = runTime.setTimeout(function check() {
  986.         assessNetworkQuality();
  987.         // 每 30 秒同步一次最新设备状态给控制面板(电量/存储/网络等实时更新)
  988.         if (G.connected) {
  989.             try {
  990.                 var s = collectStatusFull();
  991.                 if (s) sendMsg("STATUS", s, { no_ack: true });
  992.             } catch (eStatus) {
  993.                 try { LOG("DEBUG", "周期状态上报失败: " + eStatus.message); } catch (eLog) {}
  994.             }
  995.         }
  996.         networkCheckTimer = runTime.setTimeout(check, CONFIG.NETWORK_CHECK_INTERVAL_MS);
  997.     }, CONFIG.NETWORK_CHECK_INTERVAL_MS);
  998. }

  999. function stopNetworkCheck() {
  1000.     if (networkCheckTimer) {
  1001.         try { runTime.stopTimeout(networkCheckTimer); } catch (e) {}
  1002.         networkCheckTimer = null;
  1003.     }
  1004. }

  1005. // ============================================================
  1006. //  模式 7:背压控制
  1007. //  已在 sendMsg() 中实现,核心逻辑:
  1008. //    - 两次发送至少间隔 SEND_RATE_LIMIT_MS
  1009. //    - 允许突发 SEND_BURST_MAX 条
  1010. //    - 超限则入离线队列等待
  1011. // ============================================================

  1012. // ============================================================
  1013. //  模式 8:频道订阅模式
  1014. //
  1015. //  客户端可以动态订阅/取消订阅事件频道。
  1016. //  服务端只推送已订阅频道的消息。
  1017. //
  1018. //  subscribe("cmd")     → 服务端开始推送 cmd 频道消息
  1019. //  unsubscribe("cmd")   → 服务端停止推送
  1020. // ============================================================
  1021. function subscribe(channel) {
  1022.     G.subscribedChannels[channel] = true;
  1023.     sendMsg("SUBSCRIBE", { channel: channel });
  1024.     LOG("INFO", "订阅频道 " + channel);
  1025. }

  1026. function unsubscribe(channel) {
  1027.     delete G.subscribedChannels[channel];
  1028.     sendMsg("UNSUBSCRIBE", { channel: channel });
  1029.     LOG("INFO", "取消订阅 " + channel);
  1030. }

  1031. function autoSubscribeChannels() {
  1032.     for (var i = 0; i < CONFIG.AUTO_SUBSCRIBE_CHANNELS.length; i++) {
  1033.         subscribe(CONFIG.AUTO_SUBSCRIBE_CHANNELS[i]);
  1034.     }
  1035. }

  1036. // ============================================================
  1037. //  服务器消息处理
  1038. // ============================================================
  1039. function handleServerMessage(raw) {
  1040.     // AIWROK 中回调参数可能是 bytes / java object / string,先统一转字符串
  1041.     if (raw === null || raw === undefined) return;
  1042.     if (typeof raw !== "string") {
  1043.         try {
  1044.             if (typeof raw === "object" && typeof raw.toString === "function") {
  1045.                 var s = raw.toString();
  1046.                 // Java 字节数组或其他对象
  1047.                 if (s && (s.indexOf("[B@") === 0 || s.indexOf("[object") === 0)) {
  1048.                     // 尝试通过其他方式解码
  1049.                     try { s = String(new java.lang.String(raw, "UTF-8")); } catch (e) {}
  1050.                 }
  1051.                 raw = s;
  1052.             } else {
  1053.                 raw = String(raw);
  1054.             }
  1055.         } catch (e) {
  1056.             LOG("DEBUG", "无法转换消息类型: " + (typeof raw));
  1057.             return;
  1058.         }
  1059.     }
  1060.     if (!raw || typeof raw !== "string") return;

  1061.     // 只要收到服务器任何有效消息,就刷新 lastPongAt(比等 PONG 更可靠)
  1062.     var nowTs = new Date().getTime();
  1063.     G.lastPongAt = nowTs;

  1064.     // 兼容旧版文本协议
  1065.     if (raw.indexOf("WELCOME|") === 0) {
  1066.         LOG("INFO", "服务器欢迎 " + raw.split("|")[1]);
  1067.         return;
  1068.     }
  1069.     if (raw === "PONG" || raw.indexOf("ECHO|") === 0) {
  1070.         return;
  1071.     }

  1072.     // JSON 协议
  1073.     var obj = null;
  1074.     try { obj = JSON.parse(raw); } catch (e) {
  1075.         LOG("DEBUG", "非JSON消息: " + raw.substring(0, 60));
  1076.         return;
  1077.     }
  1078.     if (!obj || typeof obj !== "object") return;

  1079.     // 模式 1:处理 ACK
  1080.     if (obj.type === "ACK" && obj.ack_for) {
  1081.         handleAck(obj.ack_for);
  1082.         return;
  1083.     }

  1084.     // 模式 2:处理响应
  1085.     if (obj.type === "RESPONSE" && obj.req_id) {
  1086.         handleResponse(obj);
  1087.         return;
  1088.     }

  1089.     // 模式 3:序列号检查
  1090.     if (obj.seq && !handleIncomingSeq(obj.seq)) {
  1091.         return; // 重复消息,丢弃
  1092.     }

  1093.     // 按类型分发(外层 try-catch 防止 #0 崩溃)
  1094.     try {
  1095.         switch (obj.type) {
  1096.             case "PONG":
  1097.                 var pingSentAt = (obj.data && obj.data.t) ? obj.data.t : (obj.ack_for ? obj.ack_for : nowTs);
  1098.                 G.pingRtt = (pingSentAt && pingSentAt < nowTs) ? (nowTs - pingSentAt) : 0;
  1099.                 LOG("DEBUG", "PONG RTT=" + G.pingRtt + "ms");
  1100.                 break;

  1101.             case "PING":
  1102.                 sendMsg("PONG", { echo_ts: obj.ts || 0, t: new Date().getTime() }, { no_ack: true });
  1103.                 break;

  1104.             case "CMD":
  1105.                 handleRemoteCommand(obj.cmd, obj.params, obj.req_id);
  1106.                 break;

  1107.             // 模式 5:分片接收
  1108.             case "CHUNK_START":
  1109.                 handleChunkStart(obj.data);
  1110.                 break;
  1111.             case "CHUNK_DATA":
  1112.                 handleChunkData(obj.data);
  1113.                 break;
  1114.             case "CHUNK_END":
  1115.                 handleChunkEnd(obj.data);
  1116.                 break;

  1117.             // 模式 5 补充:服务端/控制面板请求重传缺失分片
  1118.             case "CHUNK_RESEND":
  1119.                 handleChunkResend(obj.data || {});
  1120.                 break;

  1121.             // 频道消息
  1122.             case "CHANNEL":
  1123.                 LOG("INFO", "频道消息 ch=" + (obj.data.channel || "?") + " content=" + JSON.stringify(obj.data.payload || "").substring(0, 100));
  1124.                 break;

  1125.             case "RESYNC":
  1126.                 // 服务端补发的消息
  1127.                 LOG("INFO", "收到补发消息 seq=" + obj.seq);
  1128.                 break;

  1129.             case "RESPONSE":
  1130.                 // 服务器转发的其他客户端的响应(忽略,我们已在 handleResponse 处理)
  1131.                 LOG("DEBUG", "收到 RESPONSE req_id=" + (obj.req_id || "?"));
  1132.                 break;

  1133.             case "WELCOME":
  1134.                 LOG("INFO", "服务器欢迎 " + (obj.data ? obj.data.client_id : "?"));
  1135.                 break;

  1136.             default:
  1137.                 LOG("DEBUG", "未知消息 type=" + obj.type);
  1138.         }
  1139.     } catch (eSwitch) {
  1140.         LOG("WARN", "消息分发异常 type=" + obj.type + " err=" + (eSwitch.message || String(eSwitch)));
  1141.     }

  1142.     // 模式 1:收到消息后自动回 ACK(除非标记 no_ack)
  1143.     if (obj.seq && !obj.no_ack) {
  1144.         sendMsg("ACK", { ack_for: obj.seq }, { no_ack: true });
  1145.     }
  1146. }

  1147. // 远程指令(对齐 WebSocket手机端常用实例.js 的 9 类 CMD,使控制面板能直接操作手机)
  1148. function handleRemoteCommand(cmd, params, reqId) {
  1149.     params = params || {};
  1150.     LOG("CMD", cmd + " params=" + JSON.stringify(params).substring(0, 200));

  1151.     var result = { ok: true, data: null, error: "" };

  1152.     try {
  1153.         // 终极策略:直接使用 auto/device/app 对象的原生 API
  1154.         //   彻底绕开 click/swipe 等 AIWROK 引擎保留名冲突
  1155.         function _HAS(obj, name) { return !!(obj && typeof obj[name] === "function"); }

  1156.         // ===== 触控 =====
  1157.         function _doClick(x, y) {
  1158.             x = parseInt(x || 0); y = parseInt(y || 0);
  1159.             if (_HAS(auto, "clickPoint")) return auto.clickPoint(x, y);
  1160.             if (_HAS(auto, "clickPercent")) {
  1161.                 var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
  1162.                 return auto.clickPercent(x / sw, y / sh);
  1163.             }
  1164.             if (_HAS(gesture, "click")) return gesture.click(x, y);
  1165.             if (_HAS(device, "click")) return device.click(x, y);
  1166.             throw new Error("click_api_not_found");
  1167.         }
  1168.         function _doSwipe(x1, y1, x2, y2, d) {
  1169.             x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
  1170.             x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
  1171.             d = parseInt(d || 400);
  1172.             if (_HAS(auto, "swip")) return auto.swip(x1, y1, x2, y2, d, 0);
  1173.             if (_HAS(auto, "dispatchGesture") && _HAS(gesture, "setDurTime")) {
  1174.                 gesture.setDurTime(d);
  1175.                 gesture.addPoint(x1, y1);
  1176.                 gesture.addPoint(x2, y2);
  1177.                 return auto.dispatchGesture([gesture]);
  1178.             }
  1179.             if (_HAS(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, d);
  1180.             if (_HAS(device, "swipe")) return device.swipe(x1, y1, x2, y2, d);
  1181.             throw new Error("swipe_api_not_found");
  1182.         }
  1183.         function _doInput(t) {
  1184.             t = String(t == null ? "" : t);
  1185.             if (_HAS(auto, "input")) return auto.input(t);
  1186.             if (_HAS(auto, "sendText")) return auto.sendText(t);
  1187.             if (_HAS(auto, "inputText")) return auto.inputText(t);
  1188.             if (_HAS(device, "input")) return device.input(t);
  1189.             throw new Error("input_api_not_found");
  1190.         }

  1191.         // ===== 按键 =====
  1192.         function _doKey(fnName, keyCode, keyName) {
  1193.             if (_HAS(auto, fnName)) return auto[fnName]();
  1194.             if (_HAS(device, keyName)) return device[keyName]();
  1195.             if (_HAS(device, "pressKey")) return device.pressKey(keyCode);
  1196.             if (_HAS(device, "press")) return device.press(keyCode);
  1197.             if (_HAS(runtime, "postEvent")) {
  1198.                 var KEv = android.view.KeyEvent;
  1199.                 return runtime.postEvent(KEv.ACTION_DOWN, keyCode);
  1200.             }
  1201.             throw new Error("key_" + fnName + "_not_found");
  1202.         }

  1203.         // ===== 应用管理 =====
  1204.         function _openAppDirect(pkg) {
  1205.             pkg = String(pkg || "");
  1206.             if (_HAS(app, "openApp")) return app.openApp(pkg);
  1207.             if (_HAS(app, "openAppv2")) return app.openAppv2(pkg);
  1208.             if (_HAS(app, "openAppSeting")) return app.openAppSeting(pkg);
  1209.             if (_HAS(app, "launch")) return app.launch(pkg);
  1210.             if (_HAS(app, "open")) return app.open(pkg);
  1211.             if (_HAS(device, "startApp")) return device.startApp(pkg);
  1212.             if (_HAS(device, "launchApp")) return device.launchApp(pkg);
  1213.             if (_HAS(device, "openApp")) return device.openApp(pkg);
  1214.             var ctxt = context || runtime.getContext();
  1215.             var Intent = android.content.Intent;
  1216.             var pm = ctxt.getPackageManager();
  1217.             var li = pm.getLaunchIntentForPackage(pkg);
  1218.             if (li) { li.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ctxt.startActivity(li); return true; }
  1219.             throw new Error("openApp_intent_fail");
  1220.         }
  1221.         function _openByNameDirect(name) {
  1222.             name = String(name || "");
  1223.             if (_HAS(app, "openApp")) { try { return app.openApp(name); } catch (e1) {} }
  1224.             if (_HAS(app, "openAppv2")) { try { return app.openAppv2(name); } catch (e2) {} }
  1225.             if (_HAS(app, "launchByName")) { try { return app.launchByName(name); } catch (e3) {} }
  1226.             if (_HAS(app, "openByName")) { try { return app.openByName(name); } catch (e4) {} }
  1227.             if (_HAS(device, "openAppByName")) { try { return device.openAppByName(name); } catch (e5) {} }
  1228.             if (_HAS(device, "launchByName")) { try { return device.launchByName(name); } catch (e6) {} }
  1229.             // 模糊匹配
  1230.             var ctxt2 = context || runtime.getContext();
  1231.             var pm2 = ctxt2.getPackageManager();
  1232.             var apps = pm2.getInstalledApplications(0);
  1233.             var target = name.toLowerCase();
  1234.             var iter = apps.iterator();
  1235.             while (iter.hasNext()) {
  1236.                 var ai = iter.next();
  1237.                 var label = String(pm2.getApplicationLabel(ai) || "").toLowerCase();
  1238.                 var pkg = String(ai.packageName || "").toLowerCase();
  1239.                 if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
  1240.                     return _openAppDirect(ai.packageName);
  1241.                 }
  1242.             }
  1243.             throw new Error("app_name_not_found:" + name);
  1244.         }
  1245.         function _killAppDirect(pkg) {
  1246.             pkg = String(pkg || "");
  1247.             if (_HAS(app, "forceStop")) return app.forceStop(pkg);
  1248.             if (_HAS(app, "kill")) return app.kill(pkg);
  1249.             if (_HAS(app, "close")) return app.close(pkg);
  1250.             if (_HAS(device, "forceStopApp")) return device.forceStopApp(pkg);
  1251.             if (_HAS(device, "killApp")) return device.killApp(pkg);
  1252.             if (_HAS(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
  1253.             var ctxt3 = context || runtime.getContext();
  1254.             var am = ctxt3.getSystemService(android.content.Context.ACTIVITY_SERVICE);
  1255.             am.killBackgroundProcesses(pkg);
  1256.             return true;
  1257.         }

  1258.         switch (cmd) {
  1259.             case "SCREENSHOT":
  1260.                 var shotRes = sendScreenshotChunked(params.id || ("shot_" + new Date().getTime()));
  1261.                 if (shotRes && shotRes.ok) {
  1262.                     result.data = shotRes;
  1263.                 } else {
  1264.                     result.ok = false;
  1265.                     result.error = (shotRes && shotRes.error_msg) ? shotRes.error_msg : "screenshot_failed";
  1266.                     result.data = shotRes || {};
  1267.                 }
  1268.                 break;

  1269.             case "CLICK":
  1270.                 if (params.x != null && params.y != null) {
  1271.                     var cx = parseInt(params.x);
  1272.                     var cy = parseInt(params.y);
  1273.                     try { _doClick(cx, cy); result.data = { x: cx, y: cy }; }
  1274.                     catch (e) { result.ok = false; result.error = e.message; }
  1275.                 } else {
  1276.                     result.ok = false; result.error = "missing_xy";
  1277.                 }
  1278.                 break;

  1279.             case "SWIPE":
  1280.                 if (params.x1 != null && params.y1 != null && params.x2 != null && params.y2 != null) {
  1281.                     var x1 = parseInt(params.x1); var y1 = parseInt(params.y1);
  1282.                     var x2 = parseInt(params.x2); var y2 = parseInt(params.y2);
  1283.                     var dur = parseInt(params.duration_ms || 400);
  1284.                     try { _doSwipe(x1, y1, x2, y2, dur); result.data = { x1: x1, y1: y1, x2: x2, y2: y2, duration_ms: dur }; }
  1285.                     catch (e) { result.ok = false; result.error = e.message; }
  1286.                 } else {
  1287.                     result.ok = false; result.error = "missing_points";
  1288.                 }
  1289.                 break;

  1290.             case "INPUT_TEXT":
  1291.                 if (params.text != null) {
  1292.                     var txt = String(params.text);
  1293.                     try { _doInput(txt); result.data = { text: txt }; }
  1294.                     catch (e) { result.ok = false; result.error = e.message; }
  1295.                 } else {
  1296.                     result.ok = false; result.error = "missing_text";
  1297.                 }
  1298.                 break;

  1299.             case "KEYCODE":
  1300.                 if (params.code) {
  1301.                     var code = String(params.code).toUpperCase();
  1302.                     try {
  1303.                         switch (code) {
  1304.                             case "HOME":      _doKey("home", 3, "home"); break;
  1305.                             case "BACK":      _doKey("back", 4, "back"); break;
  1306.                             case "RECENT":
  1307.                             case "APP_SWITCH":
  1308.                             case "TASK":
  1309.                                 try { _doKey("recents", 187, "recentApps"); } catch (eA) { _doKey("recentApps", 187, "recentApps"); }
  1310.                                 break;
  1311.                             case "NOTIFICATION":
  1312.                             case "STATUS_BAR":
  1313.                                 try { _doKey("statusBar", 83, "statusBar"); } catch (eB) { _doKey("notification", 83, "notification"); }
  1314.                                 break;
  1315.                             case "POWER":     _doKey("power", 26, "power"); break;
  1316.                             case "VOL_UP":
  1317.                                 try { device.setMusicVolume(device.getMusicVolume() + 1); } catch (e) { throw new Error("vol_up_failed"); }
  1318.                                 break;
  1319.                             case "VOL_DOWN":
  1320.                                 try { device.setMusicVolume(Math.max(0, device.getMusicVolume() - 1)); } catch (e) { throw new Error("vol_down_failed"); }
  1321.                                 break;
  1322.                             case "MENU":      _doKey("menu", 82, "menu"); break;
  1323.                             case "SEARCH":    _doKey("search", 84, "search"); break;
  1324.                             case "CAMERA":    _doKey("camera", 27, "camera"); break;
  1325.                             case "ENTER":     try { _doInput("\n"); } catch (e) {} break;
  1326.                             case "DEL":
  1327.                             case "BACKSPACE":
  1328.                                 try { var et = device.getFocusEditText(); if (et) { et.setText(""); } } catch (e) { throw new Error("del_failed"); }
  1329.                                 break;
  1330.                             default:
  1331.                                 throw new Error("unknown_keycode_" + code);
  1332.                         }
  1333.                         result.data = { code: code };
  1334.                     } catch (e) {
  1335.                         result.ok = false; result.error = e.message;
  1336.                     }
  1337.                 } else {
  1338.                     result.ok = false; result.error = "missing_code";
  1339.                 }
  1340.                 break;

  1341.             case "OPEN_APP":
  1342.                 try {
  1343.                     if (params.pkg) {
  1344.                         _openAppDirect(String(params.pkg));
  1345.                         result.data = { opened: "pkg:" + params.pkg };
  1346.                     } else if (params.name) {
  1347.                         _openByNameDirect(String(params.name));
  1348.                         result.data = { opened: "name:" + params.name };
  1349.                     } else if (params.package_name) {
  1350.                         _openAppDirect(String(params.package_name));
  1351.                         result.data = { opened: "pkg:" + params.package_name };
  1352.                     } else {
  1353.                         result.ok = false; result.error = "missing_pkg_or_name";
  1354.                     }
  1355.                 } catch (e) {
  1356.                     result.ok = false; result.error = e.message;
  1357.                 }
  1358.                 break;

  1359.             case "KILL_APP":
  1360.                 try {
  1361.                     var pkg = params.pkg || params.package_name;
  1362.                     if (pkg) {
  1363.                         _killAppDirect(String(pkg));
  1364.                         result.data = { killed: pkg };
  1365.                     } else {
  1366.                         result.ok = false; result.error = "missing_pkg";
  1367.                     }
  1368.                 } catch (e) {
  1369.                     result.ok = false; result.error = e.message;
  1370.                 }
  1371.                 break;

  1372.             case "REPORT_STATUS":
  1373.             case "GET_STATUS":
  1374.                 result.data = collectStatusFull();
  1375.                 break;

  1376.             case "RUN_SCRIPT":
  1377.                 if (params.code != null) {
  1378.                     try {
  1379.                         var codeStr = String(params.code);
  1380.                         var r = (new Function(codeStr))();
  1381.                         result.data = { returned: (r === undefined ? null : (typeof r === "object" ? r : String(r))) };
  1382.                     } catch (e) {
  1383.                         result.ok = false; result.error = e.message;
  1384.                     }
  1385.                 } else {
  1386.                     result.ok = false; result.error = "missing_code";
  1387.                 }
  1388.                 break;

  1389.             case "SUBSCRIBE":
  1390.                 if (params.channel) subscribe(params.channel);
  1391.                 result.data = "subscribed";
  1392.                 break;

  1393.             case "UNSUBSCRIBE":
  1394.                 if (params.channel) unsubscribe(params.channel);
  1395.                 result.data = "unsubscribed";
  1396.                 break;

  1397.             case "ECHO":
  1398.                 result.data = params;
  1399.                 break;

  1400.             case "QUIT":
  1401.                 result.data = "bye";
  1402.                 sendMsg("RESPONSE", result, { req_id: reqId, no_ack: true });
  1403.                 G.running = false;
  1404.                 try { G.ws.close(); } catch (e) {}
  1405.                 return;

  1406.             default:
  1407.                 result.ok = false;
  1408.                 result.error = "unknown_cmd_" + cmd;
  1409.         }
  1410.     } catch (e) {
  1411.         result.ok = false;
  1412.         result.error = e.message;
  1413.     }

  1414.     // 以 RESPONSE 形式回复(模式 2:请求-响应配对)
  1415.     // ⚠️ 必须通过 opts.req_id 把 req_id 放到消息顶层,服务器 obj.get("req_id") 才能取到
  1416.     //    不要把 req_id 包在第二个参数 data 里,否则 buildMsg 会变成 msg.data.req_id
  1417.     if (reqId) {
  1418.         sendMsg("RESPONSE", result, { req_id: reqId, no_ack: true });
  1419.     }
  1420. }

  1421. function collectStatus() {
  1422.     // 兼容旧调用方:字段名全部对齐"新规范",避免返回老名字 battery/charging
  1423.     var s = {
  1424.         battery_level: 0,
  1425.         battery_charging: false,
  1426.         network_type: "unknown",
  1427.         storage_free_mb: 0
  1428.     };
  1429.     try { s.battery_level = device.getBatteryLevel(); } catch (e) {}
  1430.     try { s.battery_charging = device.isCharging(); } catch (e) {}
  1431.     try { s.network_type = device.getNetworkType(); } catch (e) {}
  1432.     // 旧字段(保留防止老代码依赖)
  1433.     try { s.battery = s.battery_level; } catch (e) {}
  1434.     try { s.charging = s.battery_charging; } catch (e) {}
  1435.     try { s.network = s.network_type; } catch (e) {}
  1436.     return s;
  1437. }

  1438. // 完整的状态上报(所有情况下都保证字段名以"新规范"优先出现,不会有 null)
  1439. function collectStatusFull() {
  1440.     var s = {
  1441.         battery_level: 0,
  1442.         battery_charging: false,
  1443.         network_type: "unknown",
  1444.         signal_level: 0,
  1445.         storage_total_mb: 0,
  1446.         storage_free_mb: 0,
  1447.         ram_free_mb: 0,
  1448.         screen_on: true,
  1449.         brightness: 0,
  1450.         volume: 0,
  1451.         sdk: 0,
  1452.         brand: "",
  1453.         model: "",
  1454.         debug: ""
  1455.     };
  1456.     var errors = [];
  1457.     var gotInfo = false;
  1458.     try {
  1459.         var info = device.getInfo && device.getInfo();
  1460.         if (info && typeof info === "object") {
  1461.             for (var k in info) if (info.hasOwnProperty(k)) {
  1462.                 try { s[k] = info[k]; } catch (e) {}
  1463.             }
  1464.             gotInfo = true;
  1465.         }
  1466.     } catch (e) { errors.push("getInfo:" + e.message); }

  1467.     // 逐字段兜底(getInfo 里的字段名可能五花八门,所以每个都重新单独读一次覆盖)
  1468.     try { s.battery_level = device.getBatteryLevel(); } catch (e) { errors.push("battery:" + e.message); }
  1469.     try { s.battery_charging = device.isCharging(); } catch (e) { errors.push("charging:" + e.message); }
  1470.     try { s.network_type = device.getNetworkType(); } catch (e) { errors.push("net:" + e.message); }
  1471.     try { s.signal_level = device.getSignalLevel(); } catch (e) { errors.push("signal:" + e.message); }
  1472.     try { s.storage_total_mb = device.getTotalInternalMemorySize() / 1048576; } catch (e) { errors.push("stotal:" + e.message); }
  1473.     try { s.storage_free_mb = device.getAvailableInternalMemorySize() / 1048576; } catch (e) { errors.push("sfree:" + e.message); }
  1474.     try { s.ram_free_mb = device.getAvailableMemorySize() / 1048576; } catch (e) { errors.push("ram:" + e.message); }
  1475.     try { s.screen_on = device.isScreenOn(); } catch (e) { errors.push("screen:" + e.message); }
  1476.     try { s.brightness = device.getScreenBrightness(); } catch (e) { errors.push("bright:" + e.message); }
  1477.     try { s.volume = device.getMusicVolume(); } catch (e) { errors.push("vol:" + e.message); }
  1478.     try { s.sdk = device.getSdkVersion(); } catch (e) { errors.push("sdk:" + e.message); }
  1479.     try { s.brand = device.getBrand(); } catch (e) { errors.push("brand:" + e.message); }
  1480.     try { s.model = device.getModel(); } catch (e) { errors.push("model:" + e.message); }

  1481.     // 关键:老字段名 → 新字段名 反向同步(getInfo 里可能只有老名字,而单独API又抛异常失败)
  1482.     try {
  1483.         if ((s.battery_level === 0 || s.battery_level == null) && s.battery && typeof s.battery === "number") {
  1484.             s.battery_level = s.battery;
  1485.         }
  1486.     } catch (e) {}
  1487.     try {
  1488.         if (s.battery_charging == null && s.charging != null) {
  1489.             s.battery_charging = !!s.charging;
  1490.         }
  1491.     } catch (e) {}
  1492.     try {
  1493.         if ((!s.network_type || s.network_type === "unknown") && s.network) {
  1494.             s.network_type = String(s.network);
  1495.         }
  1496.     } catch (e) {}
  1497.     try {
  1498.         if (s.signal_level === 0 && s.signal != null && typeof s.signal === "number") {
  1499.             s.signal_level = s.signal;
  1500.         }
  1501.     } catch (e) {}
  1502.     try {
  1503.         if (s.storage_free_mb === 0 && s.storage_free != null && typeof s.storage_free === "number") {
  1504.             s.storage_free_mb = s.storage_free;
  1505.         }
  1506.     } catch (e) {}
  1507.     try {
  1508.         if (s.ram_free_mb === 0 && s.ram_free != null && typeof s.ram_free === "number") {
  1509.             s.ram_free_mb = s.ram_free;
  1510.         }
  1511.     } catch (e) {}

  1512.     // 同时保证"老字段名"也存在(兼容旧的上层逻辑 / 旧控制面板)
  1513.     try { if (s.battery == null) s.battery = s.battery_level; } catch (e) {}
  1514.     try { if (s.charging == null) s.charging = s.battery_charging; } catch (e) {}
  1515.     try { if (!s.network) s.network = s.network_type; } catch (e) {}
  1516.     if (errors.length) s.debug = errors.join("; ");
  1517.     s._info_ok = gotInfo;
  1518.     return s;
  1519. }

  1520. function clearPendingTimers() {
  1521.     // 清理所有 ACK 等待中的定时器(断线后不应继续重传,等重连后由离线队列重发)
  1522.     var keys = [];
  1523.     for (var k in G.pendingAcks) if (G.pendingAcks.hasOwnProperty(k)) keys.push(k);
  1524.     for (var i = 0; i < keys.length; i++) {
  1525.         var entry = G.pendingAcks[keys[i]];
  1526.         if (entry && entry.timerId) {
  1527.             try { runTime.stopTimeout(entry.timerId); } catch (e) {}
  1528.         }
  1529.     }
  1530.     G.pendingAcks = {};

  1531.     // 清理所有请求等待中的定时器
  1532.     var rkeys = [];
  1533.     for (var rk in G.pendingRequests) if (G.pendingRequests.hasOwnProperty(rk)) rkeys.push(rk);
  1534.     for (var j = 0; j < rkeys.length; j++) {
  1535.         var rentry = G.pendingRequests[rkeys[j]];
  1536.         if (rentry && rentry.timerId) {
  1537.             try { runTime.stopTimeout(rentry.timerId); } catch (e) {}
  1538.             // 不删除 entry,让 flushOfflineQueue 可以把请求重新发出
  1539.         }
  1540.     }
  1541. }

  1542. // ============================================================
  1543. //  连接 & 重连
  1544. // ============================================================
  1545. function doConnect() {
  1546.     G.connected = false;
  1547.     var ws = new websocket();
  1548.     G.ws = ws;

  1549.     try { ws.setSocketID(G.socketID); } catch (e) {}

  1550.     try {
  1551.         ws.event(
  1552.             function () { // onopen
  1553.                 G.connected = true;
  1554.                 G.retryCount = 0;
  1555.                 LOG("OK", "连接成功");

  1556.                 // 模式 3:重连后请求补发
  1557.                 requestResyncOnReconnect();

  1558.                 // 模式 4:flush 离线队列
  1559.                 flushOfflineQueue();

  1560.                 // 模式 6:启动自适应心跳
  1561.                 startHeartbeat();
  1562.                 startNetworkCheck();

  1563.                 // 模式 8:自动订阅频道
  1564.                 autoSubscribeChannels();

  1565.                 // 发送注册(携带设备信息给控制面板展示品牌/型号)
  1566.                 var brand = ""; var model = "";
  1567.                 try { brand = device.getBrand(); } catch (e) {}
  1568.                 try { model = device.getModel(); } catch (e) {}
  1569.                 sendMsg("REGISTER", {
  1570.                     socket_id: G.socketID,
  1571.                     version: "2.0",
  1572.                     features: ["ack", "req_resp", "resync", "offline_queue",
  1573.                                "chunked", "adaptive_heartbeat", "backpressure", "channels"]
  1574.                 });
  1575.                 // 上线后立刻报一次状态,让控制面板能看到
  1576.                 try {
  1577.                     var s = collectStatusFull();
  1578.                     if (s) sendMsg("STATUS", s, { no_ack: true });
  1579.                 } catch (e) {}
  1580.             },
  1581.             function (msg) { // onmessage
  1582.                 try { handleServerMessage(msg); }
  1583.                 catch (e) { LOG("ERROR", "消息处理异常: " + e.message); }
  1584.             },
  1585.             function (err) { // onerror
  1586.                 LOG("WARN", "连接失败");
  1587.                 G.connected = false;
  1588.                 stopHeartbeat();
  1589.                 stopNetworkCheck();
  1590.                 clearPendingTimers();
  1591.             },
  1592.             function () { // onclose
  1593.                 G.connected = false;
  1594.                 stopHeartbeat();
  1595.                 stopNetworkCheck();
  1596.                 clearPendingTimers();
  1597.             }
  1598.         );
  1599.     } catch (e) {
  1600.         LOG("ERROR", "event注册失败: " + e.message);
  1601.         return false;
  1602.     }

  1603.     var url = "ws://" + CONFIG.SERVER_IP + ":" + CONFIG.SERVER_PORT + CONFIG.WS_PATH;
  1604.     try {
  1605.         ws.connet(url);
  1606.         return true;
  1607.     } catch (e) {
  1608.         LOG("ERROR", "connet异常: " + e.message);
  1609.         return false;
  1610.     }
  1611. }

  1612. function computeRetryMs(n) {
  1613.     var ms = CONFIG.RETRY_BASE_MS * Math.pow(2, n);
  1614.     return Math.min(ms, CONFIG.RETRY_MAX_MS) + Math.floor(Math.random() * 1000);
  1615. }

  1616. // ============================================================
  1617. //  主循环
  1618. // ============================================================
  1619. function main() {
  1620.     LOG("INFO", "=== WebSocket 高级技巧合集 v2.0 启动 ===");
  1621.     LOG("INFO", "8大模式: ACK重传 / 请求响应 / 序列号补偿 / 离线队列 / 分片传输 / 自适应心跳 / 背压 / 频道订阅");

  1622.     while (G.running) {
  1623.         G.retryCount++;
  1624.         doConnect();

  1625.         var wait = 0;
  1626.         while (!G.connected && wait < 150) {
  1627.             sleep.millisecond(100);
  1628.             wait++;
  1629.         }

  1630.         if (!G.connected) {
  1631.             var ms = computeRetryMs(G.retryCount - 1);
  1632.             LOG("WARN", "连接失败 " + (ms / 1000).toFixed(1) + "s 后重试");
  1633.             try { G.ws.close(); } catch (e) {}
  1634.             sleep.millisecond(ms);
  1635.             continue;
  1636.         }

  1637.         while (G.running && G.connected) {
  1638.             sleep.millisecond(500);
  1639.         }

  1640.         if (!G.running) break;

  1641.         var ms2 = computeRetryMs(G.retryCount - 1);
  1642.         LOG("WARN", "断线 " + (ms2 / 1000).toFixed(1) + "s 后重连");
  1643.         try { G.ws.close(); } catch (e) {}
  1644.         sleep.millisecond(ms2);
  1645.     }

  1646.     // 清理
  1647.     stopHeartbeat();
  1648.     stopNetworkCheck();
  1649.     try { G.ws.close(); } catch (e) {}
  1650.     LOG("INFO", "脚本结束");
  1651.     exit();
  1652. }

  1653. // 启动
  1654. try {
  1655.     main();
  1656. } catch (e) {
  1657.     LOG("FATAL", e.message + "\n" + (e.stack || ""));
  1658.     try { G.ws.close(); } catch (e2) {}
  1659.     exit();
  1660. }
复制代码







欢迎光临 自动发帖软件 (http://www.fatiegongju.com/) Powered by Discuz! X3.2