| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- import { ipcMain } from 'electron';
- import { readdir, writeFile, readFile } from 'fs/promises';
- import { join, dirname, isAbsolute } from 'path';
- import { fileURLToPath } from 'url';
- import { exec } from 'child_process';
- import { promisify } from 'util';
- import { captureScreenshot } from './adb/screenshot.js';
- import { getDeviceResolution } from './adb/device-info.js';
- const execAsync = promisify(exec);
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- // 获取 static/processing 目录下的所有文件夹
- export async function getStaticFolders() {
- try {
- const staticPath = join(__dirname, '..', 'static', 'processing');
- const entries = await readdir(staticPath, { withFileTypes: true });
-
- const folders = [];
- for (const entry of entries) {
- if (entry.isDirectory()) {
- folders.push(entry.name);
- }
- }
-
- return folders.sort(); // 按名称排序
- } catch (error) {
- console.error('Failed to read static/processing folders:', error);
- return [];
- }
- }
- // 执行图像匹配:截图、调用 Python 脚本、返回坐标
- export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
- try {
- if (!ipPort) {
- return { success: false, error: '缺少设备 ID' };
- }
- if (!templateImagePath) {
- return { success: false, error: '缺少模板图片路径' };
- }
- // 将相对路径转换为绝对路径
- let absoluteTemplatePath = templateImagePath;
- if (!isAbsolute(templateImagePath)) {
- absoluteTemplatePath = join(__dirname, '..', templateImagePath);
- }
- // 1. 获取设备分辨率
- const resolutionResult = await getDeviceResolution(ipPort);
- if (!resolutionResult.success) {
- return { success: false, error: '获取设备分辨率失败' };
- }
- const { width, height } = resolutionResult;
- // 2. 获取屏幕截图
- const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
- if (!screenshotResult.success || !screenshotResult.data) {
- return { success: false, error: '获取屏幕截图失败' };
- }
- // 3. 保存截图到临时文件
- const tempDir = join(__dirname, '..');
- const screenshotPath = join(tempDir, 'temp_screenshot.png');
- const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
- await writeFile(screenshotPath, screenshotBuffer);
- // 4. 调用 Python 脚本进行图像匹配
- // 使用虚拟环境中的 Python 解释器
- const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
- const pythonScriptPath = join(__dirname, '..', 'py', 'img-reg.py');
-
- // 确保路径使用正斜杠,避免 Windows 路径问题
- const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
- const normalizedTemplatePath = absoluteTemplatePath.replace(/\\/g, '/');
-
- // 使用 -u 参数确保输出不被缓冲,并设置 UTF-8 编码
- const command = `"${pythonExePath}" -u "${pythonScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width} ${height}`;
-
- const { stdout, stderr } = await execAsync(command, {
- timeout: 10000,
- maxBuffer: 10 * 1024 * 1024,
- cwd: join(__dirname, '..'),
- encoding: 'utf8',
- env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
- });
- // 5. 解析 Python 脚本输出
- // 输出格式可能是:
- // - "找到匹配!坐标: x=100, y=200, 宽度=50, 高度=50"
- // - "JSON格式: {"x": 100, "y": 200, "width": 50, "height": 50}"
- // - "未找到匹配"
- // - "错误: xxx"
-
- // 检查是否有错误或未找到匹配
- if (stdout.includes('未找到匹配') || stdout.toLowerCase().includes('错误') || stderr) {
- const errorMsg = stdout.includes('未找到匹配')
- ? '未找到匹配的图像'
- : (stderr || stdout.match(/错误[::]\s*(.+)/)?.[1] || '图像匹配失败');
- return { success: false, error: errorMsg };
- }
- // 尝试从输出中提取 JSON
- // 先尝试匹配 JSON 格式的行
- let jsonMatch = stdout.match(/JSON格式:\s*(\{[^}]+\})/);
- if (!jsonMatch) {
- // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
- jsonMatch = stdout.match(/\{\s*"x"\s*:\s*\d+\s*,\s*"y"\s*:\s*\d+\s*,\s*"width"\s*:\s*\d+\s*,\s*"height"\s*:\s*\d+\s*\}/);
- }
-
- if (!jsonMatch) {
- // 如果还是没有找到,尝试从文本中提取坐标
- const coordMatch = stdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
- if (coordMatch) {
- const x = parseInt(coordMatch[1], 10);
- const y = parseInt(coordMatch[2], 10);
- const w = parseInt(coordMatch[3], 10);
- const h = parseInt(coordMatch[4], 10);
-
- // 计算点击位置(中心点)
- const clickX = Math.round(x + w / 2);
- const clickY = Math.round(y + h / 2);
- return {
- success: true,
- coordinate: { x, y, width: w, height: h },
- clickPosition: { x: clickX, y: clickY }
- };
- }
-
- // 如果都找不到,返回错误并输出原始输出用于调试
- console.error('Python 脚本输出:', cleanStdout);
- console.error('Python 脚本错误输出:', cleanStderr);
- return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
- }
- let coordinate;
- try {
- coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
- } catch (parseError) {
- console.error('JSON 解析失败:', parseError);
- console.error('原始输出:', cleanStdout);
- return { success: false, error: `JSON 解析失败: ${parseError.message}` };
- }
-
- const { x, y, width: w, height: h } = coordinate;
- // 计算点击位置(中心点)
- const clickX = Math.round(x + w / 2);
- const clickY = Math.round(y + h / 2);
- return {
- success: true,
- coordinate: { x, y, width: w, height: h },
- clickPosition: { x: clickX, y: clickY }
- };
- } catch (error) {
- console.error('图像匹配失败:', error);
- return { success: false, error: error.message };
- }
- }
- // 执行文字识别:截图、调用 Python 脚本、返回坐标
- export async function findTextAndGetCoordinate(ipPort, targetText) {
- try {
- if (!ipPort) {
- return { success: false, error: '缺少设备 ID' };
- }
- if (!targetText) {
- return { success: false, error: '缺少目标文字' };
- }
- // 1. 获取设备分辨率
- const resolutionResult = await getDeviceResolution(ipPort);
- if (!resolutionResult.success) {
- return { success: false, error: '获取设备分辨率失败' };
- }
- const { width, height } = resolutionResult;
- // 2. 获取屏幕截图
- const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
- if (!screenshotResult.success || !screenshotResult.data) {
- return { success: false, error: '获取屏幕截图失败' };
- }
- // 3. 保存截图到临时文件
- const tempDir = join(__dirname, '..');
- const screenshotPath = join(tempDir, 'temp_screenshot.png');
- const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
- await writeFile(screenshotPath, screenshotBuffer);
- // 4. 调用 Python 脚本进行文字识别
- // 使用虚拟环境中的 Python 解释器
- const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
- const pythonScriptPath = join(__dirname, '..', 'py', 'string-reg-location.py');
- // 转义目标文字中的双引号,避免命令解析错误
- const escapedText = targetText.replace(/"/g, '\\"');
- const command = `"${pythonExePath}" "${pythonScriptPath}" "${screenshotPath}" "${escapedText}" ${width} ${height}`;
-
- // 设置环境变量,跳过模型源连接检查
- const env = {
- ...process.env,
- DISABLE_MODEL_SOURCE_CHECK: 'True'
- };
-
- const { stdout, stderr } = await execAsync(command, {
- timeout: 60000, // OCR 首次运行可能需要下载模型,设置60秒超时
- maxBuffer: 10 * 1024 * 1024,
- cwd: join(__dirname, '..'),
- env: env
- });
- // 5. 解析 Python 脚本输出
- // 检查是否有错误或未找到匹配
- // 忽略警告信息(DeprecationWarning 等)
- const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
- const cleanStderr = stderr ? stderr.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '') : '';
-
- if (cleanStdout.includes('未找到匹配的文字') || (cleanStdout.toLowerCase().includes('错误') && !cleanStdout.includes('找到文字')) || (cleanStderr && !cleanStderr.includes('Checking connectivity'))) {
- const errorMsg = cleanStdout.includes('未找到匹配的文字')
- ? `未找到文字: ${targetText}`
- : (cleanStderr || cleanStdout.match(/错误[::]\s*(.+)/)?.[1] || '文字识别失败');
- return { success: false, error: errorMsg };
- }
- // 尝试从输出中提取 JSON(使用清理后的输出)
- let jsonMatch = cleanStdout.match(/JSON格式:\s*(\{[^}]+\})/);
- if (!jsonMatch) {
- // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
- jsonMatch = cleanStdout.match(/\{\s*"x"\s*:\s*\d+\s*,\s*"y"\s*:\s*\d+\s*,\s*"width"\s*:\s*\d+\s*,\s*"height"\s*:\s*\d+\s*\}/);
- }
-
- if (!jsonMatch) {
- // 如果还是没有找到,尝试从文本中提取坐标
- const coordMatch = cleanStdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
- if (coordMatch) {
- const x = parseInt(coordMatch[1], 10);
- const y = parseInt(coordMatch[2], 10);
- const w = parseInt(coordMatch[3], 10);
- const h = parseInt(coordMatch[4], 10);
-
- // 计算点击位置(中心点)
- const clickX = Math.round(x + w / 2);
- const clickY = Math.round(y + h / 2);
- return {
- success: true,
- coordinate: { x, y, width: w, height: h },
- clickPosition: { x: clickX, y: clickY }
- };
- }
-
- // 如果都找不到,返回错误并输出原始输出用于调试
- console.error('Python 脚本输出:', cleanStdout);
- console.error('Python 脚本错误输出:', cleanStderr);
- return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
- }
- let coordinate;
- try {
- coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
- } catch (parseError) {
- console.error('JSON 解析失败:', parseError);
- console.error('原始输出:', cleanStdout);
- return { success: false, error: `JSON 解析失败: ${parseError.message}` };
- }
-
- const { x, y, width: w, height: h } = coordinate;
- // 计算点击位置(中心点)
- const clickX = Math.round(x + w / 2);
- const clickY = Math.round(y + h / 2);
- return {
- success: true,
- coordinate: { x, y, width: w, height: h },
- clickPosition: { x: clickX, y: clickY }
- };
- } catch (error) {
- console.error('文字识别失败:', error);
- // 如果是超时错误,提供更友好的提示
- if (error.message && error.message.includes('timeout')) {
- return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
- }
- return { success: false, error: error.message };
- }
- }
- // 读取 processing.json 文件
- export async function readProcessingJson(folderName) {
- try {
- const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'processing.json');
- const jsonContent = await readFile(jsonPath, 'utf-8');
-
- // 解析 JSON(处理可能的格式问题,如注释、尾随逗号等)
- // 先尝试直接解析
- let parsed;
- try {
- parsed = JSON.parse(jsonContent);
- } catch (parseError) {
- // 如果直接解析失败,尝试清理注释和尾随逗号(简单处理)
- let cleaned = jsonContent
- .replace(/\/\/.*$/gm, '') // 移除单行注释
- .replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
- .replace(/,(\s*[}\]])/g, '$1'); // 移除尾随逗号(在 ] 或 } 之前的逗号)
-
- try {
- parsed = JSON.parse(cleaned);
- } catch (retryError) {
- console.error(`JSON 解析失败 [${folderName}]:`, parseError.message);
- console.error('原始内容:', jsonContent.substring(0, 500));
- throw new Error(`JSON 格式错误: ${parseError.message}`);
- }
- }
- // 处理不同的 JSON 格式
- // 如果直接是数组,包装成对象
- if (Array.isArray(parsed)) {
- return { actions: parsed };
- }
-
- // 如果已经是对象,直接返回
- if (parsed && typeof parsed === 'object') {
- // 如果已经有 actions 字段,直接返回
- if (parsed.actions) {
- return parsed;
- }
- // 如果没有 actions 字段,尝试查找数组字段
- for (const key in parsed) {
- if (Array.isArray(parsed[key])) {
- return { actions: parsed[key], ...parsed };
- }
- }
- }
- // 如果解析成功但没有 actions 字段,返回错误信息
- if (!parsed || (typeof parsed === 'object' && !parsed.actions && !Array.isArray(parsed))) {
- console.error(`processing.json 格式错误 [${folderName}]: 缺少 actions 字段`);
- return null;
- }
-
- return parsed;
- } catch (error) {
- console.error(`读取 processing.json 失败 [${folderName}]:`, error.message);
- return null;
- }
- }
- // 注册 IPC 处理器
- export function registerIpcHandlers() {
- ipcMain.handle('get-static-folders', async () => {
- return await getStaticFolders();
- });
- ipcMain.handle('match-image-and-get-coordinate', async (event, ipPort, templateImagePath) => {
- return await matchImageAndGetCoordinate(ipPort, templateImagePath);
- });
- ipcMain.handle('read-processing-json', async (event, folderName) => {
- return await readProcessingJson(folderName);
- });
- ipcMain.handle('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
- return await findTextAndGetCoordinate(ipPort, targetText);
- });
- }
|