const { exec } = require('child_process'); const fs = require('fs'); const path = require('path'); // Lưu ý: Cần cài đặt thư viện fast-xml-parser để phân tích XML dễ dàng hơn: npm install fast-xml-parser const { XMLParser } = require('fast-xml-parser'); // Hàm chạy lệnh shell/adb function runCommand(cmd) { return new Promise((resolve, reject) => { exec(cmd, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve(stdout.trim()); } }); }); } // 1. Chụp màn hình và lấy cấu trúc giao diện XML từ điện thoại async function dumpScreenUI() { try { console.log("Đang quét màn hình điện thoại..."); // Dump giao diện hiện tại thành file XML trên điện thoại await runCommand('adb shell uiautomator dump /sdcard/window_dump.xml'); // Tải file XML đó về thư mục trên Armbian/máy tính const localPath = path.join(__dirname, 'window_dump.xml'); await runCommand(`adb pull /sdcard/window_dump.xml "${localPath}"`); console.log(`Đã tải cấu trúc màn hình về: ${localPath}`); return localPath; } catch (error) { console.error("Lỗi khi dump màn hình:", error.message); throw error; } } // 2. Đọc và phân tích file XML để tìm tọa độ của chữ/nút trên màn hình function findElementCoordinates(xmlFilePath, targetText) { const xmlData = fs.readFileSync(xmlFilePath, 'utf8'); const parser = new XMLParser({ ignoreAttributes: false }); const jsonObj = parser.parse(xmlData); let foundBounds = null; // Hàm đệ quy duyệt qua toàn bộ các node XML để tìm text trùng khớp function traverse(node) { if (!node) return; // Kiểm tra node hiện tại có chứa text mục tiêu không const textAttr = node['@_text'] || ''; const descAttr = node['@_content-desc'] || ''; if (textAttr.toLowerCase().includes(targetText.toLowerCase()) || descAttr.toLowerCase().includes(targetText.toLowerCase())) { foundBounds = node['@_bounds']; return; } // Nếu node có các node con (node có thể là mảng hoặc object đơn lẻ) const children = node.node; if (children) { if (Array.isArray(children)) { for (let child of children) { traverse(child); if (foundBounds) break; } } else { traverse(children); } } } // Bắt đầu duyệt từ node gốc hierarchy if (jsonObj.hierarchy && jsonObj.hierarchy.node) { traverse(jsonObj.hierarchy.node); } if (foundBounds) { // bounds có định dạng: "[x1,y1][x2,y2]" (Ví dụ: "[100,200][300,400]") const matches = foundBounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/); if (matches) { const x1 = parseInt(matches[1]); const y1 = parseInt(matches[2]); const x2 = parseInt(matches[3]); const y2 = parseInt(matches[4]); // Tính tọa độ trung tâm của phần tử để click const centerX = Math.floor((x1 + x2) / 2); const centerY = Math.floor((y1 + y2) / 2); return { x: centerX, y: centerY }; } } return null; } // 3. Điều khiển điện thoại bằng ADB async function clickAt(x, y) { console.log(`Đang click vào tọa độ: X=${x}, Y=${y}`); await runCommand(`adb shell input tap ${x} ${y}`); } async function swipe(x1, y1, x2, y2, duration = 300) { console.log(`Đang cuộn màn hình từ (${x1}, ${y1}) tới (${x2}, ${y2})`); await runCommand(`adb shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`); } async function inputText(text) { console.log(`Đang nhập văn bản: "${text}"`); // Gửi text qua ADB shell (Lưu ý: ADB mặc định không hỗ trợ tiếng Việt có dấu tốt) await runCommand(`adb shell input text "${text}"`); } // 4. Kịch bản chạy mẫu (Ví dụ: Tìm nút "Mua ngay" trên Shopee và click vào) async function startAutomation() { try { // Bước 1: Dump giao diện hiện tại const xmlFile = await dumpScreenUI(); // Bước 2: Tìm tọa độ của nút có chữ "Mua ngay" hoặc "Mua Với Voucher" const coords = findElementCoordinates(xmlFile, "Mua ngay"); if (coords) { console.log(`Đã tìm thấy phần tử tại tọa độ:`, coords); // Bước 3: Click vào phần tử đó await clickAt(coords.x, coords.y); } else { console.log("Không tìm thấy chữ 'Mua ngay' trên màn hình hiện tại."); // Thử cuộn màn hình xuống nếu không thấy await swipe(500, 1500, 500, 500); } } catch (error) { console.error("Lỗi kịch bản:", error); } } // Chạy thử nghiệm startAutomation();