B2BÍøÂçÈí¼þ

 ÕÒ»ØÃÜÂë
 Á¢¼´×¢²á ÉóºËÍøÕ¾ºÅ:QQ:896757558
ËÑË÷
²é¿´: 3|»Ø¸´: 0
´òÓ¡ ÉÏÒ»Ö÷Ìâ ÏÂÒ»Ö÷Ìâ

ʵսʹÓÃ×ÀÃæ¿ØÖÆWebSocket

[¸´ÖÆÁ´½Ó]

1155

Ö÷Ìâ

1160

Ìû×Ó

7897

»ý·Ö

abc

Rank: 9Rank: 9Rank: 9

»ý·Ö
7897
Ìø×ªµ½Ö¸¶¨Â¥²ã
Â¥Ö÷

ʵսʹÓÃ×ÀÃæ¿ØÖÆ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. }
¸´ÖÆ´úÂë


»Ø¸´

ʹÓõÀ¾ß ¾Ù±¨

±¾°æ»ý·Ö¹æÔò

¹Ø±Õ

QQ|»ÓªÏúÈí¼þ×ÛºÏÌÖÂÛ|»ÓªÏúÈí¼þÓÐÎʱشð|»ÓªÏúÈí¼þ½Ì³Ì×¨Çø|»ÓªÏúÈí¼þPOST½Å±¾·ÖÏí|»ÓªÏúÈí¼þÆÕͨ½Å±¾·ÖÏí|»ÓªÏúÈí¼þÈí¼þ×ÊѶ|»ÓªÏúÈí¼þ¾«Æ·Èí¼þ|»ÓªÏúÈí¼þ¸üй«¸æ|ÓªÏúÈí¼þ|B2BÈí¼þ|B2BÍøÂçÈí¼þ ( ¾©ICP±¸09078825ºÅ )±¾ÍøÕ¾¿ª·¢µÄÓªÏúÈí¼þÊÇÒ»¿îеÄÍøÂçÓªÏúÈí¼þ£¬Õâ¿îÓªÏú¿ÉÒÔÈ¥ÍøÕ¾Èí¼þ£¬²©¿ÍÈí¼þ£¬B2BÈí¼þ£¬·ÖÀàÐÅÏ¢Íø·¢Ìù£¬¿ÉÒÔÇÀɳ·¢£¬¿ÉÒÔµ½°Ù¶ÈÎÄ¿âÉÏ´«WORDÎĵµ£¬¿ÉÒÔµ½Ò»Ð©ÊÇÏà²áÍøÕ¾×Ô¶¯ÉÏ´«Í¼Æ¬£¬Õâ¸ö×Ô¶¯·¢ÌûÈí¼þ×Ô´øÔÆÖ©Ö룬¼Ó¿ìÊÕ¼£¬ÓÐ6ÖÖ¶Ô½Ó´òÂë½Ó¿Ú£¬·½±ã£¬Ð§Âʸߣ¬Ëٶȿ죬¶øÇÒ¶ÔÍ϶¯µÄÑéÖ¤ÂëÈ«ÍøµÚÒ»¼Ò¶À¼ÒÖ§³Ö£¬È«²¿Ô­´´¼¼Êõ£¬¶À¼ÒÑз¢£¬Õý°æÔ­´´´ø°æÈ¨Èí¼þ¡£Ñ¡ÔñÍòÄÜÓªÏúÈí¼þ£¬¾ÍÑ¡ÔñÁËÒ»ÖÖ׬ǮµÄЧÂÊ£¬´ÓûÓб»³¬Ô½¹ý£¬Ò»Ö±ÔÚŬÁ¦Ñз¢Ð¼¼Êõ¡£·Å·ÉÃÎÏ룬½â·ÅË«ÊÖ£¬À´µã´´Ò⣬³É¾ÍÄãµÄÃÎÏ룬¾ÍÔÚÍòÄÜÓªÏúÈí¼þ¿ªÊ¼

map2

GMT+8, 2026-8-14 08:00 , Processed in 0.128984 second(s), 36 queries .

¿ìËٻظ´ ·µ»Ø¶¥²¿ ·µ»ØÁбí