信息发布软件,b2b软件,广告发布软件

 找回密码
 立即注册
搜索
查看: 1|回复: 0
打印 上一主题 下一主题

[宣传软件网站动态] 实战使用桌面控制WebSocket

[复制链接]

1955

主题

1965

帖子

1万

积分

积分
11458

资讯缩略图:

资讯发布日期:2026-08-14

资讯发布简介:实战使用桌面控制WebSocket

资讯关键词:实战使用桌面控制WebSocket

资讯所属分类:IT资讯 SEO资讯 

联系:

① 本信息收集于网络,如有不对的地方欢迎联系我纠正!
② 本信息免费收录,不存在价格的问题!
③ 如果您的网站也想这样出现在这里,请您加好友情链接,我当天会审核通过!

④友情链接关键字:软件网站分类目录 网址:http://www.postbbs.com/

资讯详细描述
实战使用桌面控制WebSocket
实战使用桌面控制WebSocket b2b软件

  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. }
复制代码



untoWebSocket调试okhttp版nextnocontent
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

相关导读
群发软件实战使用桌面控制WebSocket
实战使用桌面控制WebSocket
群发软件WebSocket调试okhttp版
WebSocket调试okhttp版
群发软件快手极速版自动刷视频
快手极速版自动刷视频
群发软件极速视频APP刷金币
极速视频APP刷金币
群发软件语音合成核心模块AIWROK 单文件版
语音合成核心模块AIWROK 单文件版
群发软件AIWROK软件自动文字转语音例子
AIWROK软件自动文字转语音例子
群发软件AIWROK adb.exec() 实用工具库 —— 常用功能直接调用版
AIWROK adb.exec() 实用工具库 —— 常用功能直接调用版
群发软件安卓AIWROK获取包名所有方法汇总
安卓AIWROK获取包名所有方法汇总
群发软件AIWROK软件番茄小说自动阅读刷金币示例
AIWROK软件番茄小说自动阅读刷金币示例
信息发布软件AIWROK软件无碍模式一键删除所有APP版示例
AIWROK软件无碍模式一键删除所有APP版示例
信息发布软件AIWROK软件无碍模式auto模块抖音极速版滑动刷视频
AIWROK软件无碍模式auto模块抖音极速版滑动刷视频
信息发布软件快手极速版自动刷视频无障碍模式
快手极速版自动刷视频无障碍模式
信息发布软件AIWROK软件代理模式一键删除桌面所有APP版示例
AIWROK软件代理模式一键删除桌面所有APP版示例
信息发布软件AIWROK软件自动找图利用agent代理模式删除应用版示例
AIWROK软件自动找图利用agent代理模式删除应用版示例
信息发布软件解析Kimi返回的拖动距离示例
解析Kimi返回的拖动距离示例
信息发布软件自动截图并发送给DEEPSEEK识别示例过程
自动截图并发送给DEEPSEEK识别示例过程
信息发布软件AIWROK软件测试文心识别识别验证码示例
AIWROK软件测试文心识别识别验证码示例
信息发布软件元宝截图B站验证码示例测试
元宝截图B站验证码示例测试
信息发布软件示例千问自动采集文章保存记事本
示例千问自动采集文章保存记事本
信息发布软件豆包识别验证码加拖动验证码示例
豆包识别验证码加拖动验证码示例
信息发布软件豆包对话采集示例
豆包对话采集示例
信息发布软件启动自动百度贴吧发图片
启动自动百度贴吧发图片
信息发布软件自动百度贴吧示例
自动百度贴吧示例
信息发布软件代理模式简书自动流程
代理模式简书自动流程
信息发布软件启动极速版任务页看小说赚钱自动阅读
启动极速版任务页看小说赚钱自动阅读
信息发布软件agent代理模式功能小结
agent代理模式功能小结
信息发布软件完整苹果IOS小脚本文本输入与粘贴流程
完整苹果IOS小脚本文本输入与粘贴流程
信息发布软件AIWROK使用activity列表开关与进度条的协同交互
AIWROK使用activity列表开关与进度条的协同交互
群发软件苹果脚本找图后Detect对象各种方法演示
苹果脚本找图后Detect对象各种方法演示
群发软件苹果脚本网络API监控与数据管理工具
苹果脚本网络API监控与数据管理工具
群发软件IOS苹果脚本TXT文件读写H5日志打印完整示例
IOS苹果脚本TXT文件读写H5日志打印完整示例
群发软件AIWROK软件演示多线程创建暂停恢复
AIWROK软件演示多线程创建暂停恢复
群发软件AIWROK软件定时检查页面状态防止卡死技巧
AIWROK软件定时检查页面状态防止卡死技巧
群发软件安卓脚本runTime相关功能示例
安卓脚本runTime相关功能示例
群发软件floatUI实例秒显示秒关闭吐司功能
floatUI实例秒显示秒关闭吐司功能
群发软件苹果脚本HID多行实例粘帖板示例
苹果脚本HID多行实例粘帖板示例
群发软件安卓脚本HID节点查找完整方法示例
HID节点查找完整方法示例
群发软件AIWROK软件框架的抖音自动化营销系统
AIWROK软件框架的抖音自动化营销系统
群发软件赛博朋克任务控制中心H5示例
赛博朋克任务控制中心H5示例
群发软件苹果脚本现代深色UI设计风格
现代深色UI设计风格
群发软件AIWROK软件智能任务监控悬浮窗
AIWROK软件智能任务监控悬浮窗
群发软件AIWROK软件事件监听示例
AIWROK软件事件监听示例
群发软件QZ掘金原生UI界面
QZ掘金原生UI界面
群发软件安卓脚本auto方法完整示例
安卓脚本auto方法完整示例
群发软件安卓脚本暂停弹出确认对话框
安卓脚本暂停弹出确认对话框
群发软件苹果iOS脚本天气信息查询工具
苹果iOS脚本天气信息查询工具
群发软件安卓脚本简化版H5日志输出窗口
安卓脚本简化版H5日志输出窗口
群发软件安卓脚本简化版H5日志输出窗口
安卓脚本简化版H5日志输出窗口
群发软件安卓脚本应用重置、代理模式监控及补丁升级
安卓脚本应用重置、代理模式监控及补丁升级
群发软件安卓脚本里的打开关闭安装以及卡密授权信息
安卓脚本里的打开关闭安装以及卡密授权信息
群发软件AIWROK软件脚本GET下载文件并检测0KB工具
AIWROK软件脚本GET下载文件并检测0KB工具
群发软件安卓脚本HID按键方法完整示例
安卓脚本HID按键方法完整示例
群发软件AIWROK软件键鼠HID滑动所有方法功能演示
AIWROK软件键鼠HID滑动所有方法功能演示
群发软件苹果脚本里HID的5种滑动方法的使用
苹果脚本里HID的5种滑动方法的使用
群发软件苹果脚本示例7种不同的OCR识别和点击方法
苹果脚本示例7种不同的OCR识别和点击方法
群发软件AIWROK软件安卓脚本FTP上传下载例子
AIWROK软件安卓脚本FTP上传下载例子
群发软件AIWROK软件脚本JSON转换示例
AIWROK软件脚本JSON转换示例
群发软件AIWROK软件运算符高级应用实例
AIWROK软件运算符高级应用实例
群发软件JavaScript语法小示例
JavaScript语法小示例
群发软件展示JavaScript各种语句标识符的实际应用
展示JavaScript各种语句标识符的实际应用
群发软件AIWROK条件语句实用示例智能活动推荐系统
AIWROK条件语句实用示例智能活动推荐系统
群发软件AIWROK软件示例数组方法实战应用
AIWROK软件示例数组方法实战应用
群发软件示例JavaScript的 try-catch-finally-throw用法
示例JavaScript的 try-catch-finally-throw用法
群发软件JavaScript 高级错误处理实战示例
JavaScript 高级错误处理实战示例
群发软件安卓脚本AIWROK软件示例JS函数高级用法
安卓脚本AIWROK软件示例JS函数高级用法

QQ|( 京ICP备09078825号 )

本网站信息发布软件,是可以发布论坛,发送信息到各大博客,各大b2b软件自动发布,好不夸张的说:只要手工能发在电脑打开IE能发的网站,用这个宣传软件就可以仿制动作,进行推送发到您想发送的B2B网站或是信息发布平台上,不管是后台,还是前台,都可以进行最方便的广告发布,这个广告发布软件,可以按月购买,还可以试用软件,对网站的验证码也可以完全自动对信息发布,让客户自动找上门,使企业轻松实现b2b发布,这个信息发布软件,均是本站原创正版开发,拥有正版的血统,想要新功能,欢迎提意见给我,一好的分类信息群发软件在手,舍我其谁。QQ896757558

GMT+8, 2026-8-14 08:22 , Processed in 0.279552 second(s), 52 queries .

宣传软件--信息发布软件--b2b软件广告发布软件

快速回复 返回顶部 返回列表