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