YYPOST群发软件 发表于 2026-8-14 06:52:04

实战使用桌面控制WebSocket


实战使用桌面控制WebSocket




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

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

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

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

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

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

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

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

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

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

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

    // 通用
    MAX_RETRY: 999,
    RETRY_BASE_MS: 2000,
    RETRY_MAX_MS: 30000,
    DEBUG_LOG: true,             // 本脚本为演示,默认开日志
    SHOW_LOG_WINDOW: false,

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

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

if (CONFIG.SHOW_LOG_WINDOW) {
    try { logWindow.show(); } catch (e) {}
    try { logWindow.setNoClickModel(); } catch (e) {}
}

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

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

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

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

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

    // 6. 自适应心跳
    heartbeatTimer: null,
    currentHeartbeatMs: 25000,
    networkQuality: "good", // good / fair / poor
    lastPongAt: 0,
    pingRtt: 0,

    // 7. 背压
    lastSendAt: 0,
    sendBurstCount: 0,

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

// ============================================================
//消息构建 & 发送(含背压控制)
// ============================================================
function buildMsg(type, data, opts) {
    G.seq++;
    var msg = {
      type: type,
      seq: G.seq,
      ts: new Date().getTime(),
      device: { brand: "", model: "" },
      data: data || {}
    };
    try { msg.device.brand = device.getBrand(); } catch (e) {}
    try { msg.device.model = device.getModel(); } catch (e) {}
    if (opts) {
      if (opts.req_id) msg.req_id = opts.req_id;
      if (opts.ack_for) msg.ack_for = opts.ack_for;
      if (opts.channel) msg.channel = opts.channel;
      if (opts.no_ack) msg.no_ack = true;// 标记不需要 ACK
    }
    return msg;
}

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

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

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

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

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

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

// ============================================================
//模式 1:ACK 确认 + 超时重传
//
//发送消息后,等对端回 ACK(seq)。
//超时未收到 → 重传(最多 ACK_MAX_RETRY 次)。
//超过最大重传次数 → 通知上层失败。
// ============================================================
function startAckTimer(msg) {
    var entry = {
      raw: JSON.stringify(msg),
      retryCount: 0,
      sentAt: new Date().getTime(),
      timerId: null
    };
    G.pendingAcks = entry;

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

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

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

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

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

function handleAck(seq) {
    var entry = G.pendingAcks;
    if (!entry) {
      LOG("DEBUG", "收到重复 ACK seq=" + seq);
      return;
    }
    if (entry.timerId) {
      try { runTime.stopTimeout(entry.timerId); } catch (e) {}
    }
    var rtt = new Date().getTime() - entry.sentAt;
    LOG("DEBUG", "ACK 确认 seq=" + seq + " RTT=" + rtt + "ms");
    delete G.pendingAcks;
}

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

function sendRequest(type, data, onOk, onTimeout) {
    _reqCounter++;
    var reqId = "req_" + new Date().getTime() + "_" + _reqCounter;
    var entry = {
      reqId: reqId,
      onOk: onOk,
      onTimeout: onTimeout,
      timerId: null
    };
    G.pendingRequests = entry;

    // 超时计时
    entry.timerId = runTime.setTimeout(function () {
      if (G.pendingRequests) {
            delete G.pendingRequests;
            LOG("WARN", "请求超时 type=" + type + " reqId=" + reqId);
            if (onTimeout) onTimeout("timeout");
      }
    }, CONFIG.REQ_TIMEOUT_MS);

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

function handleResponse(resp) {
    var reqId = resp.req_id;
    var entry = G.pendingRequests;
    if (!entry) {
      LOG("DEBUG", "收到过期响应 reqId=" + reqId);
      return;
    }
    if (entry.timerId) {
      try { runTime.stopTimeout(entry.timerId); } catch (e) {}
    }
    delete G.pendingRequests;
    if (entry.onOk) entry.onOk(resp.data || resp);
    LOG("DEBUG", "响应收到 reqId=" + reqId);
}

// ============================================================
//模式 3:消息序列号 + 断线补偿
//
//每条收到的消息都带 seq。
//重连后发送 RESYNC(lastSeq),请求服务端补发 lastSeq 之后的消息。
// ============================================================
function handleIncomingSeq(seq) {
    if (seq > 0) {
      if (seq <= G.lastReceivedSeq) {
            LOG("WARN", "重复消息 seq=" + seq + " (last=" + G.lastReceivedSeq + "),丢弃");
            return false; // 重复消息
      }
      if (G.lastReceivedSeq > 0 && seq > G.lastReceivedSeq + 1) {
            var gap = seq - G.lastReceivedSeq - 1;
            LOG("WARN", "消息间隙 seq=" + seq + " 缺少 " + gap + " 条");
            // 可选:请求补发
            // sendMsg("RESYNC", { from: G.lastReceivedSeq + 1, to: seq - 1 });
      }
      G.lastReceivedSeq = seq;
    }
    return true;
}

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

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

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

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

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

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

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

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

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

                var b64 = "";
                if (typeof img.toBase64 === "function") {
                  b64 = "" + img.toBase64();
                } else if (typeof img.toBase64Jpg === "function") {
                  b64 = "" + img.toBase64Jpg();
                } else {
                  throw new Error("截图对象没有 toBase64 方法");
                }

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

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

      // 3) 最后兜底:尝试 image.* 系列
      if (typeof image !== "undefined" && image) {
            var tryList = ["fastCapture", "screenshot", "captureScreen", "capture"];
            for (var ti = 0; ti < tryList.length; ti++) {
                var fnName = tryList;
                try {
                  if (typeof image === "function") {
                        var img3 = image();
                        if (img3) {
                            var w3 = 0, h3 = 0;
                            try { w3 = img3.getWidth(); } catch (ew3) {}
                            try { h3 = img3.getHeight(); } catch (eh3) {}
                            var b64_3 = "" + img3.toBase64();
                            try { img3.recycle(); } catch (er) {}
                            var sizeKB3 = Math.floor((b64_3.length * 0.75) / 1024);
                            LOG("INFO", "截图 OK (image." + fnName + ") size=" + sizeKB3 + "KB");
                            return { base64: b64_3, size_kb: sizeKB3, width: w3, height: h3 };
                        }
                  }
                } catch (e3) { errs.push(fnName + ":" + e3.message); }
            }
      }

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

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

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

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

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

      // 保留原始 Base64 和参数,用于 CHUNK_RESEND 重传
      G.lastSentChunks = {
            b64: b64,
            chunkSize: chunkSize,
            width: shot.width,
            height: shot.height,
            sizeKB: sizeKB,
            totalChunks: totalChunks,
            ts: new Date().getTime()
      };
      // 清理过期记录,避免内存无限增长
      var _keepMs = CONFIG.CHUNK_RETAIN_LAST_MS || 60000;
      var _now = new Date().getTime();
      var _keys = [];
      for (var _k in G.lastSentChunks) if (G.lastSentChunks.hasOwnProperty(_k)) _keys.push(_k);
      for (var _ki = 0; _ki < _keys.length; _ki++) {
            if (_now - (G.lastSentChunks].ts || 0) > _keepMs) delete G.lastSentChunks];
      }

      sendMsg("CHUNK_START", {
            transfer_id: transferId,
            total_chunks: totalChunks,
            mime: "jpeg",
            width: shot.width,
            height: shot.height,
            size_kb: sizeKB
      });

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

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

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

(function _polyfill_helpers() {
    try {
    function _has(obj, name) { return !!(obj && typeof obj === "function"); }

    // 尝试在全局作用域赋值(安全方式,使用全局对象属性)
    function _tryAssignGlobal(name, fn) {
      try {
            var g = (typeof window !== "undefined") ? window :
                  (typeof globalThis !== "undefined") ? globalThis :
                  (typeof runtime !== "undefined") ? runtime : this;
            g = fn;
            return true;
      } catch (e) {
            return false;
      }
    }

    // 尝试在全局作用域读取(安全方式)
    function _tryGetGlobal(name) {
      try {
            var g = (typeof window !== "undefined") ? window :
                  (typeof globalThis !== "undefined") ? globalThis :
                  (typeof runtime !== "undefined") ? runtime : this;
            return g;
      } catch (e) { return undefined; }
    }

    // 通用:auto API 直接调用兜底
    var _autoClick = function (x, y) {
      if (_has(auto, "clickPoint")) return auto.clickPoint(x, y);
      if (_has(auto, "clickPercent")) {
            var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
            return auto.clickPercent(x / sw, y / sh);
      }
      throw new Error("no_click_api");
    };
    var _autoSwipe = function (x1, y1, x2, y2, dur) {
      if (_has(auto, "swip")) return auto.swip(x1, y1, x2, y2, dur, 0);
      throw new Error("no_swipe_api");
    };
    var _autoInput = function (text) {
      if (_has(auto, "input")) return auto.input(text);
      if (_has(auto, "sendText")) return auto.sendText(text);
      if (_has(auto, "inputText")) return auto.inputText(text);
      throw new Error("no_input_api");
    };
    var _autoPress = function (fnName) {
      if (_has(auto, fnName)) return auto();
      throw new Error("auto." + fnName + "_not_found");
    };
    var _keyPress = function (keyName, keyCode) {
      if (_has(device, keyName)) return device();
      if (_has(device, "pressKey")) return device.pressKey(keyCode);
      if (_has(device, "press")) return device.press(keyCode);
      try {
            var KeyEvent = android.view.KeyEvent;
            if (_has(runtime, "postEvent")) return runtime.postEvent(KeyEvent.ACTION_DOWN, keyCode);
      } catch (e) {}
      throw new Error("no_" + keyName + "_api");
    };

    // click
    PhoneCtrl.click = function (x, y) {
      x = parseInt(x || 0); y = parseInt(y || 0);
      try { return _autoClick(x, y); } catch (e1) {}
      try { if (_has(gesture, "click")) return gesture.click(x, y); } catch (e2) {}
      try { if (_has(device, "click")) return device.click(x, y); } catch (e3) {}
      throw new Error("click_failed");
    };
    _tryAssignGlobal("click", PhoneCtrl.click);

    // swipe
    PhoneCtrl.swipe = function (x1, y1, x2, y2, duration) {
      x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
      x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
      duration = parseInt(duration || 400);
      try { return _autoSwipe(x1, y1, x2, y2, duration); } catch (e1) {}
      try {
            if (_has(auto, "dispatchGesture")) {
                gesture.setDurTime(duration);
                gesture.addPoint(x1, y1);
                gesture.addPoint(x2, y2);
                return auto.dispatchGesture();
            }
      } catch (e2) {}
      try { if (_has(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, duration); } catch (e3) {}
      try { if (_has(device, "swipe")) return device.swipe(x1, y1, x2, y2, duration); } catch (e4) {}
      throw new Error("swipe_failed");
    };
    _tryAssignGlobal("swipe", PhoneCtrl.swipe);

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

    // home / back / menu / power / search / camera
    PhoneCtrl.home = function () {
      try { return _autoPress("home"); } catch (e) {}
      return _keyPress("home", 3);
    };
    PhoneCtrl.back = function () {
      try { return _autoPress("back"); } catch (e) {}
      return _keyPress("back", 4);
    };
    PhoneCtrl.menu = function () {
      try { return _autoPress("menu"); } catch (e) {}
      return _keyPress("menu", 82);
    };
    PhoneCtrl.power = function () {
      try { return _autoPress("power"); } catch (e) {}
      return _keyPress("power", 26);
    };
    PhoneCtrl.search = function () {
      try { return _autoPress("search"); } catch (e) {}
      return _keyPress("search", 84);
    };
    PhoneCtrl.camera = function () {
      try { return _autoPress("camera"); } catch (e) {}
      return _keyPress("camera", 27);
    };

    // recentApps
    PhoneCtrl.recentApps = function () {
      try { return _autoPress("recents"); } catch (e1) {}
      try { return _autoPress("recentApps"); } catch (e2) {}
      try {
            var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
            return PhoneCtrl.swipe(Math.floor(sw/2), sh - 20, Math.floor(sw/2), Math.floor(sh/2), 300);
      } catch (e3) {}
      return _keyPress("recentApps", 187);
    };

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

    // openApp
    PhoneCtrl.openApp = function (pkg) {
      pkg = String(pkg || "");
      try {
            if (typeof app !== "undefined" && app) {
                if (_has(app, "openApp")) return app.openApp(pkg);
                if (_has(app, "openAppv2")) return app.openAppv2(pkg);
                if (_has(app, "openAppSeting")) return app.openAppSeting(pkg);
                if (_has(app, "launch")) return app.launch(pkg);
                if (_has(app, "open")) return app.open(pkg);
            }
      } catch (e1) {}
      try {
            if (typeof device !== "undefined" && device) {
                if (_has(device, "startApp")) return device.startApp(pkg);
                if (_has(device, "launchApp")) return device.launchApp(pkg);
                if (_has(device, "openApp")) return device.openApp(pkg);
            }
      } catch (e2) {}
      try {
            var ctxt = context || runtime.getContext();
            var Intent = android.content.Intent;
            var pm = ctxt.getPackageManager();
            var launchIntent = pm.getLaunchIntentForPackage(pkg);
            if (launchIntent) {
                launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                ctxt.startActivity(launchIntent);
                return true;
            }
            throw new Error("no_launch_intent");
      } catch (e3) { throw new Error("openApp_fail: " + e3.message); }
    };

    // openAppName
    PhoneCtrl.openAppName = function (name) {
      name = String(name || "");
      try {
            if (typeof app !== "undefined" && app) {
                if (_has(app, "openApp")) return app.openApp(name);
                if (_has(app, "openAppv2")) return app.openAppv2(name);
                if (_has(app, "launchByName")) return app.launchByName(name);
                if (_has(app, "openByName")) return app.openByName(name);
            }
      } catch (e1) {}
      try {
            if (typeof device !== "undefined" && device) {
                if (_has(device, "openAppByName")) return device.openAppByName(name);
                if (_has(device, "launchByName")) return device.launchByName(name);
            }
      } catch (e2) {}
      try {
            var ctxt = context || runtime.getContext();
            var pm = ctxt.getPackageManager();
            var apps = pm.getInstalledApplications(0);
            var target = name.toLowerCase();
            var iter = apps.iterator();
            while (iter.hasNext()) {
                var ai = iter.next();
                var label = String(pm.getApplicationLabel(ai) || "").toLowerCase();
                var pkg = String(ai.packageName || "").toLowerCase();
                if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
                  return PhoneCtrl.openApp(ai.packageName);
                }
            }
            throw new Error("pkg_not_found_for_name_" + name);
      } catch (e3) { throw new Error("openAppName_fail: " + e3.message); }
    };

    // killApp
    PhoneCtrl.killApp = function (pkg) {
      pkg = String(pkg || "");
      try {
            if (typeof app !== "undefined" && app) {
                if (_has(app, "forceStop")) return app.forceStop(pkg);
                if (_has(app, "kill")) return app.kill(pkg);
                if (_has(app, "close")) return app.close(pkg);
            }
      } catch (e1) {}
      try {
            if (typeof device !== "undefined" && device) {
                if (_has(device, "forceStopApp")) return device.forceStopApp(pkg);
                if (_has(device, "killApp")) return device.killApp(pkg);
                if (_has(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
            }
      } catch (e2) {}
      try {
            var ctxt = context || runtime.getContext();
            var actMan = ctxt.getSystemService(android.content.Context.ACTIVITY_SERVICE);
            actMan.killBackgroundProcesses(pkg);
            return true;
      } catch (e3) { throw new Error("killApp_fail: " + e3.message); }
    };

    // 全局变量赋值(尽力而为,失败不影响 PhoneCtrl 上的函数)
    _tryAssignGlobal("home", PhoneCtrl.home);
    _tryAssignGlobal("back", PhoneCtrl.back);
    _tryAssignGlobal("menu", PhoneCtrl.menu);
    _tryAssignGlobal("power", PhoneCtrl.power);
    _tryAssignGlobal("search", PhoneCtrl.search);
    _tryAssignGlobal("camera", PhoneCtrl.camera);
    _tryAssignGlobal("recentApps", PhoneCtrl.recentApps);
    _tryAssignGlobal("expandNotificationBar", PhoneCtrl.expandNotificationBar);
    _tryAssignGlobal("openApp", PhoneCtrl.openApp);
    _tryAssignGlobal("openAppName", PhoneCtrl.openAppName);
    _tryAssignGlobal("killApp", PhoneCtrl.killApp);

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

/**
* 接收端:组装分片(如果手机也需要接收大文件的话)
*/
function handleChunkStart(data) {
    G.chunkTransfers = {
      chunks: {},
      totalChunks: data.total_chunks,
      received: 0,
      meta: data
    };
    LOG("DEBUG", "分片接收开始 transferId=" + data.transfer_id + " 总片数=" + data.totalChunks);
}

function handleChunkData(data) {
    var transfer = G.chunkTransfers;
    if (!transfer) {
      LOG("WARN", "未知分片传输 transferId=" + data.transfer_id);
      return;
    }
    transfer.chunks = data.data_base64;
    transfer.received++;

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

function handleChunkEnd(data) {
    var transfer = G.chunkTransfers;
    if (!transfer) return;

    if (transfer.received < transfer.totalChunks) {
      LOG("WARN", "分片不完整 " + transfer.received + "/" + transfer.totalChunks);
      // 可请求重传缺失的片
      var missing = [];
      for (var i = 0; i < transfer.totalChunks; i++) {
            if (!transfer.chunks) missing.push(i);
      }
      sendMsg("CHUNK_RESEND", {
            transfer_id: data.transfer_id,
            missing_indices: missing
      });
      return;
    }

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

    // 这里可以解码 Base64 → Bitmap → 保存文件等
    delete G.chunkTransfers;
}

/**
* 处理对端发来的 CHUNK_RESEND(缺失分片重传请求)
* 从 G.lastSentChunks 中取出该 transferId 的原始 b64,按索引重新发送对应片
*/
function handleChunkResend(data) {
    var transferId = data.transfer_id;
    var missing = data.missing_indices || [];
    var cached = G.lastSentChunks;
    if (!cached) {
      LOG("WARN", "收到 CHUNK_RESEND 但本地已无缓存 transferId=" + transferId);
      return;
    }
    LOG("INFO", "收到 CHUNK_RESEND transferId=" + transferId + " 缺失片数=" + missing.length);
    var b64 = cached.b64;
    var chunkSize = cached.chunkSize;
    var reSentCount = 0;
    for (var mi = 0; mi < missing.length; mi++) {
      var idx = parseInt(missing);
      if (isNaN(idx) || idx < 0) continue;
      var start = idx * chunkSize;
      if (start >= b64.length) continue;
      var end = Math.min(start + chunkSize, b64.length);
      var chunk = b64.substring(start, end);
      sendMsg("CHUNK_DATA", {
            transfer_id: transferId,
            chunk_index: idx,
            data_base64: chunk
      });
      reSentCount++;
      // 每 20 片让 1ms
      if (reSentCount % 20 === 0) { try { sleep.millisecond(1); } catch (e) {} }
    }
    LOG("INFO", "CHUNK_RESEND 已重传 " + reSentCount + " 片 transferId=" + transferId);
}

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

    if (rtt === 0 || rtt < 100) {
      G.networkQuality = "good";
      G.currentHeartbeatMs = CONFIG.HEARTBEAT_MIN_MS;
    } else if (rtt < 500) {
      G.networkQuality = "fair";
      G.currentHeartbeatMs = Math.floor((CONFIG.HEARTBEAT_MIN_MS + CONFIG.HEARTBEAT_MAX_MS) / 2);
    } else {
      G.networkQuality = "poor";
      G.currentHeartbeatMs = CONFIG.HEARTBEAT_MAX_MS;
    }

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

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

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

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

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

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

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

function stopHeartbeat() {
    if (G.heartbeatTimer) {
      try { runTime.stopTimeout(G.heartbeatTimer); } catch (e) {}
      G.heartbeatTimer = null;
    }
}

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

function stopNetworkCheck() {
    if (networkCheckTimer) {
      try { runTime.stopTimeout(networkCheckTimer); } catch (e) {}
      networkCheckTimer = null;
    }
}

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

// ============================================================
//模式 8:频道订阅模式
//
//客户端可以动态订阅/取消订阅事件频道。
//服务端只推送已订阅频道的消息。
//
//subscribe("cmd")   → 服务端开始推送 cmd 频道消息
//unsubscribe("cmd")   → 服务端停止推送
// ============================================================
function subscribe(channel) {
    G.subscribedChannels = true;
    sendMsg("SUBSCRIBE", { channel: channel });
    LOG("INFO", "订阅频道 " + channel);
}

function unsubscribe(channel) {
    delete G.subscribedChannels;
    sendMsg("UNSUBSCRIBE", { channel: channel });
    LOG("INFO", "取消订阅 " + channel);
}

function autoSubscribeChannels() {
    for (var i = 0; i < CONFIG.AUTO_SUBSCRIBE_CHANNELS.length; i++) {
      subscribe(CONFIG.AUTO_SUBSCRIBE_CHANNELS);
    }
}

// ============================================================
//服务器消息处理
// ============================================================
function handleServerMessage(raw) {
    // AIWROK 中回调参数可能是 bytes / java object / string,先统一转字符串
    if (raw === null || raw === undefined) return;
    if (typeof raw !== "string") {
      try {
            if (typeof raw === "object" && typeof raw.toString === "function") {
                var s = raw.toString();
                // Java 字节数组或其他对象
                if (s && (s.indexOf("[B@") === 0 || s.indexOf("[object") === 0)) {
                  // 尝试通过其他方式解码
                  try { s = String(new java.lang.String(raw, "UTF-8")); } catch (e) {}
                }
                raw = s;
            } else {
                raw = String(raw);
            }
      } catch (e) {
            LOG("DEBUG", "无法转换消息类型: " + (typeof raw));
            return;
      }
    }
    if (!raw || typeof raw !== "string") return;

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

    // 兼容旧版文本协议
    if (raw.indexOf("WELCOME|") === 0) {
      LOG("INFO", "服务器欢迎 " + raw.split("|"));
      return;
    }
    if (raw === "PONG" || raw.indexOf("ECHO|") === 0) {
      return;
    }

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

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

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

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

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

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

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

            // 模式 5:分片接收
            case "CHUNK_START":
                handleChunkStart(obj.data);
                break;
            case "CHUNK_DATA":
                handleChunkData(obj.data);
                break;
            case "CHUNK_END":
                handleChunkEnd(obj.data);
                break;

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

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

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

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

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

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

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

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

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

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

      // ===== 触控 =====
      function _doClick(x, y) {
            x = parseInt(x || 0); y = parseInt(y || 0);
            if (_HAS(auto, "clickPoint")) return auto.clickPoint(x, y);
            if (_HAS(auto, "clickPercent")) {
                var sw = screen.getScreenWidth(), sh = screen.getScreenHeight();
                return auto.clickPercent(x / sw, y / sh);
            }
            if (_HAS(gesture, "click")) return gesture.click(x, y);
            if (_HAS(device, "click")) return device.click(x, y);
            throw new Error("click_api_not_found");
      }
      function _doSwipe(x1, y1, x2, y2, d) {
            x1 = parseInt(x1 || 0); y1 = parseInt(y1 || 0);
            x2 = parseInt(x2 || 0); y2 = parseInt(y2 || 0);
            d = parseInt(d || 400);
            if (_HAS(auto, "swip")) return auto.swip(x1, y1, x2, y2, d, 0);
            if (_HAS(auto, "dispatchGesture") && _HAS(gesture, "setDurTime")) {
                gesture.setDurTime(d);
                gesture.addPoint(x1, y1);
                gesture.addPoint(x2, y2);
                return auto.dispatchGesture();
            }
            if (_HAS(gesture, "swipe")) return gesture.swipe(x1, y1, x2, y2, d);
            if (_HAS(device, "swipe")) return device.swipe(x1, y1, x2, y2, d);
            throw new Error("swipe_api_not_found");
      }
      function _doInput(t) {
            t = String(t == null ? "" : t);
            if (_HAS(auto, "input")) return auto.input(t);
            if (_HAS(auto, "sendText")) return auto.sendText(t);
            if (_HAS(auto, "inputText")) return auto.inputText(t);
            if (_HAS(device, "input")) return device.input(t);
            throw new Error("input_api_not_found");
      }

      // ===== 按键 =====
      function _doKey(fnName, keyCode, keyName) {
            if (_HAS(auto, fnName)) return auto();
            if (_HAS(device, keyName)) return device();
            if (_HAS(device, "pressKey")) return device.pressKey(keyCode);
            if (_HAS(device, "press")) return device.press(keyCode);
            if (_HAS(runtime, "postEvent")) {
                var KEv = android.view.KeyEvent;
                return runtime.postEvent(KEv.ACTION_DOWN, keyCode);
            }
            throw new Error("key_" + fnName + "_not_found");
      }

      // ===== 应用管理 =====
      function _openAppDirect(pkg) {
            pkg = String(pkg || "");
            if (_HAS(app, "openApp")) return app.openApp(pkg);
            if (_HAS(app, "openAppv2")) return app.openAppv2(pkg);
            if (_HAS(app, "openAppSeting")) return app.openAppSeting(pkg);
            if (_HAS(app, "launch")) return app.launch(pkg);
            if (_HAS(app, "open")) return app.open(pkg);
            if (_HAS(device, "startApp")) return device.startApp(pkg);
            if (_HAS(device, "launchApp")) return device.launchApp(pkg);
            if (_HAS(device, "openApp")) return device.openApp(pkg);
            var ctxt = context || runtime.getContext();
            var Intent = android.content.Intent;
            var pm = ctxt.getPackageManager();
            var li = pm.getLaunchIntentForPackage(pkg);
            if (li) { li.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ctxt.startActivity(li); return true; }
            throw new Error("openApp_intent_fail");
      }
      function _openByNameDirect(name) {
            name = String(name || "");
            if (_HAS(app, "openApp")) { try { return app.openApp(name); } catch (e1) {} }
            if (_HAS(app, "openAppv2")) { try { return app.openAppv2(name); } catch (e2) {} }
            if (_HAS(app, "launchByName")) { try { return app.launchByName(name); } catch (e3) {} }
            if (_HAS(app, "openByName")) { try { return app.openByName(name); } catch (e4) {} }
            if (_HAS(device, "openAppByName")) { try { return device.openAppByName(name); } catch (e5) {} }
            if (_HAS(device, "launchByName")) { try { return device.launchByName(name); } catch (e6) {} }
            // 模糊匹配
            var ctxt2 = context || runtime.getContext();
            var pm2 = ctxt2.getPackageManager();
            var apps = pm2.getInstalledApplications(0);
            var target = name.toLowerCase();
            var iter = apps.iterator();
            while (iter.hasNext()) {
                var ai = iter.next();
                var label = String(pm2.getApplicationLabel(ai) || "").toLowerCase();
                var pkg = String(ai.packageName || "").toLowerCase();
                if (label === target || pkg === target || label.indexOf(target) !== -1 || pkg.indexOf(target) !== -1) {
                  return _openAppDirect(ai.packageName);
                }
            }
            throw new Error("app_name_not_found:" + name);
      }
      function _killAppDirect(pkg) {
            pkg = String(pkg || "");
            if (_HAS(app, "forceStop")) return app.forceStop(pkg);
            if (_HAS(app, "kill")) return app.kill(pkg);
            if (_HAS(app, "close")) return app.close(pkg);
            if (_HAS(device, "forceStopApp")) return device.forceStopApp(pkg);
            if (_HAS(device, "killApp")) return device.killApp(pkg);
            if (_HAS(device, "killBackgroundProcesses")) return device.killBackgroundProcesses(pkg);
            var ctxt3 = context || runtime.getContext();
            var am = ctxt3.getSystemService(android.content.Context.ACTIVITY_SERVICE);
            am.killBackgroundProcesses(pkg);
            return true;
      }

      switch (cmd) {
            case "SCREENSHOT":
                var shotRes = sendScreenshotChunked(params.id || ("shot_" + new Date().getTime()));
                if (shotRes && shotRes.ok) {
                  result.data = shotRes;
                } else {
                  result.ok = false;
                  result.error = (shotRes && shotRes.error_msg) ? shotRes.error_msg : "screenshot_failed";
                  result.data = shotRes || {};
                }
                break;

            case "CLICK":
                if (params.x != null && params.y != null) {
                  var cx = parseInt(params.x);
                  var cy = parseInt(params.y);
                  try { _doClick(cx, cy); result.data = { x: cx, y: cy }; }
                  catch (e) { result.ok = false; result.error = e.message; }
                } else {
                  result.ok = false; result.error = "missing_xy";
                }
                break;

            case "SWIPE":
                if (params.x1 != null && params.y1 != null && params.x2 != null && params.y2 != null) {
                  var x1 = parseInt(params.x1); var y1 = parseInt(params.y1);
                  var x2 = parseInt(params.x2); var y2 = parseInt(params.y2);
                  var dur = parseInt(params.duration_ms || 400);
                  try { _doSwipe(x1, y1, x2, y2, dur); result.data = { x1: x1, y1: y1, x2: x2, y2: y2, duration_ms: dur }; }
                  catch (e) { result.ok = false; result.error = e.message; }
                } else {
                  result.ok = false; result.error = "missing_points";
                }
                break;

            case "INPUT_TEXT":
                if (params.text != null) {
                  var txt = String(params.text);
                  try { _doInput(txt); result.data = { text: txt }; }
                  catch (e) { result.ok = false; result.error = e.message; }
                } else {
                  result.ok = false; result.error = "missing_text";
                }
                break;

            case "KEYCODE":
                if (params.code) {
                  var code = String(params.code).toUpperCase();
                  try {
                        switch (code) {
                            case "HOME":      _doKey("home", 3, "home"); break;
                            case "BACK":      _doKey("back", 4, "back"); break;
                            case "RECENT":
                            case "APP_SWITCH":
                            case "TASK":
                              try { _doKey("recents", 187, "recentApps"); } catch (eA) { _doKey("recentApps", 187, "recentApps"); }
                              break;
                            case "NOTIFICATION":
                            case "STATUS_BAR":
                              try { _doKey("statusBar", 83, "statusBar"); } catch (eB) { _doKey("notification", 83, "notification"); }
                              break;
                            case "POWER":   _doKey("power", 26, "power"); break;
                            case "VOL_UP":
                              try { device.setMusicVolume(device.getMusicVolume() + 1); } catch (e) { throw new Error("vol_up_failed"); }
                              break;
                            case "VOL_DOWN":
                              try { device.setMusicVolume(Math.max(0, device.getMusicVolume() - 1)); } catch (e) { throw new Error("vol_down_failed"); }
                              break;
                            case "MENU":      _doKey("menu", 82, "menu"); break;
                            case "SEARCH":    _doKey("search", 84, "search"); break;
                            case "CAMERA":    _doKey("camera", 27, "camera"); break;
                            case "ENTER":   try { _doInput("\n"); } catch (e) {} break;
                            case "DEL":
                            case "BACKSPACE":
                              try { var et = device.getFocusEditText(); if (et) { et.setText(""); } } catch (e) { throw new Error("del_failed"); }
                              break;
                            default:
                              throw new Error("unknown_keycode_" + code);
                        }
                        result.data = { code: code };
                  } catch (e) {
                        result.ok = false; result.error = e.message;
                  }
                } else {
                  result.ok = false; result.error = "missing_code";
                }
                break;

            case "OPEN_APP":
                try {
                  if (params.pkg) {
                        _openAppDirect(String(params.pkg));
                        result.data = { opened: "pkg:" + params.pkg };
                  } else if (params.name) {
                        _openByNameDirect(String(params.name));
                        result.data = { opened: "name:" + params.name };
                  } else if (params.package_name) {
                        _openAppDirect(String(params.package_name));
                        result.data = { opened: "pkg:" + params.package_name };
                  } else {
                        result.ok = false; result.error = "missing_pkg_or_name";
                  }
                } catch (e) {
                  result.ok = false; result.error = e.message;
                }
                break;

            case "KILL_APP":
                try {
                  var pkg = params.pkg || params.package_name;
                  if (pkg) {
                        _killAppDirect(String(pkg));
                        result.data = { killed: pkg };
                  } else {
                        result.ok = false; result.error = "missing_pkg";
                  }
                } catch (e) {
                  result.ok = false; result.error = e.message;
                }
                break;

            case "REPORT_STATUS":
            case "GET_STATUS":
                result.data = collectStatusFull();
                break;

            case "RUN_SCRIPT":
                if (params.code != null) {
                  try {
                        var codeStr = String(params.code);
                        var r = (new Function(codeStr))();
                        result.data = { returned: (r === undefined ? null : (typeof r === "object" ? r : String(r))) };
                  } catch (e) {
                        result.ok = false; result.error = e.message;
                  }
                } else {
                  result.ok = false; result.error = "missing_code";
                }
                break;

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

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

            case "ECHO":
                result.data = params;
                break;

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

            default:
                result.ok = false;
                result.error = "unknown_cmd_" + cmd;
      }
    } catch (e) {
      result.ok = false;
      result.error = e.message;
    }

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

function collectStatus() {
    // 兼容旧调用方:字段名全部对齐"新规范",避免返回老名字 battery/charging
    var s = {
      battery_level: 0,
      battery_charging: false,
      network_type: "unknown",
      storage_free_mb: 0
    };
    try { s.battery_level = device.getBatteryLevel(); } catch (e) {}
    try { s.battery_charging = device.isCharging(); } catch (e) {}
    try { s.network_type = device.getNetworkType(); } catch (e) {}
    // 旧字段(保留防止老代码依赖)
    try { s.battery = s.battery_level; } catch (e) {}
    try { s.charging = s.battery_charging; } catch (e) {}
    try { s.network = s.network_type; } catch (e) {}
    return s;
}

// 完整的状态上报(所有情况下都保证字段名以"新规范"优先出现,不会有 null)
function collectStatusFull() {
    var s = {
      battery_level: 0,
      battery_charging: false,
      network_type: "unknown",
      signal_level: 0,
      storage_total_mb: 0,
      storage_free_mb: 0,
      ram_free_mb: 0,
      screen_on: true,
      brightness: 0,
      volume: 0,
      sdk: 0,
      brand: "",
      model: "",
      debug: ""
    };
    var errors = [];
    var gotInfo = false;
    try {
      var info = device.getInfo && device.getInfo();
      if (info && typeof info === "object") {
            for (var k in info) if (info.hasOwnProperty(k)) {
                try { s = info; } catch (e) {}
            }
            gotInfo = true;
      }
    } catch (e) { errors.push("getInfo:" + e.message); }

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

    // 关键:老字段名 → 新字段名 反向同步(getInfo 里可能只有老名字,而单独API又抛异常失败)
    try {
      if ((s.battery_level === 0 || s.battery_level == null) && s.battery && typeof s.battery === "number") {
            s.battery_level = s.battery;
      }
    } catch (e) {}
    try {
      if (s.battery_charging == null && s.charging != null) {
            s.battery_charging = !!s.charging;
      }
    } catch (e) {}
    try {
      if ((!s.network_type || s.network_type === "unknown") && s.network) {
            s.network_type = String(s.network);
      }
    } catch (e) {}
    try {
      if (s.signal_level === 0 && s.signal != null && typeof s.signal === "number") {
            s.signal_level = s.signal;
      }
    } catch (e) {}
    try {
      if (s.storage_free_mb === 0 && s.storage_free != null && typeof s.storage_free === "number") {
            s.storage_free_mb = s.storage_free;
      }
    } catch (e) {}
    try {
      if (s.ram_free_mb === 0 && s.ram_free != null && typeof s.ram_free === "number") {
            s.ram_free_mb = s.ram_free;
      }
    } catch (e) {}

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

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

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

// ============================================================
//连接 & 重连
// ============================================================
function doConnect() {
    G.connected = false;
    var ws = new websocket();
    G.ws = ws;

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

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

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

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

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

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

                // 发送注册(携带设备信息给控制面板展示品牌/型号)
                var brand = ""; var model = "";
                try { brand = device.getBrand(); } catch (e) {}
                try { model = device.getModel(); } catch (e) {}
                sendMsg("REGISTER", {
                  socket_id: G.socketID,
                  version: "2.0",
                  features: ["ack", "req_resp", "resync", "offline_queue",
                               "chunked", "adaptive_heartbeat", "backpressure", "channels"]
                });
                // 上线后立刻报一次状态,让控制面板能看到
                try {
                  var s = collectStatusFull();
                  if (s) sendMsg("STATUS", s, { no_ack: true });
                } catch (e) {}
            },
            function (msg) { // onmessage
                try { handleServerMessage(msg); }
                catch (e) { LOG("ERROR", "消息处理异常: " + e.message); }
            },
            function (err) { // onerror
                LOG("WARN", "连接失败");
                G.connected = false;
                stopHeartbeat();
                stopNetworkCheck();
                clearPendingTimers();
            },
            function () { // onclose
                G.connected = false;
                stopHeartbeat();
                stopNetworkCheck();
                clearPendingTimers();
            }
      );
    } catch (e) {
      LOG("ERROR", "event注册失败: " + e.message);
      return false;
    }

    var url = "ws://" + CONFIG.SERVER_IP + ":" + CONFIG.SERVER_PORT + CONFIG.WS_PATH;
    try {
      ws.connet(url);
      return true;
    } catch (e) {
      LOG("ERROR", "connet异常: " + e.message);
      return false;
    }
}

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

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

    while (G.running) {
      G.retryCount++;
      doConnect();

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

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

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

      if (!G.running) break;

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

    // 清理
    stopHeartbeat();
    stopNetworkCheck();
    try { G.ws.close(); } catch (e) {}
    LOG("INFO", "脚本结束");
    exit();
}

// 启动
try {
    main();
} catch (e) {
    LOG("FATAL", e.message + "\n" + (e.stack || ""));
    try { G.ws.close(); } catch (e2) {}
    exit();
}


页: [1]
查看完整版本: 实战使用桌面控制WebSocket