history.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import { ipcMain } from 'electron';
  2. import { readdir, writeFile, readFile } from 'fs/promises';
  3. import { join, dirname, isAbsolute } from 'path';
  4. import { fileURLToPath } from 'url';
  5. import { exec } from 'child_process';
  6. import { promisify } from 'util';
  7. import { captureScreenshot } from './adb/screenshot.js';
  8. import { getDeviceResolution } from './adb/device-info.js';
  9. const execAsync = promisify(exec);
  10. const __filename = fileURLToPath(import.meta.url);
  11. const __dirname = dirname(__filename);
  12. // 获取 static/processing 目录下的所有文件夹
  13. export async function getStaticFolders() {
  14. try {
  15. const staticPath = join(__dirname, '..', 'static', 'processing');
  16. const entries = await readdir(staticPath, { withFileTypes: true });
  17. const folders = [];
  18. for (const entry of entries) {
  19. if (entry.isDirectory()) {
  20. folders.push(entry.name);
  21. }
  22. }
  23. return folders.sort(); // 按名称排序
  24. } catch (error) {
  25. console.error('Failed to read static/processing folders:', error);
  26. return [];
  27. }
  28. }
  29. // 执行图像匹配:截图、调用 Python 脚本、返回坐标
  30. export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
  31. try {
  32. if (!ipPort) {
  33. return { success: false, error: '缺少设备 ID' };
  34. }
  35. if (!templateImagePath) {
  36. return { success: false, error: '缺少模板图片路径' };
  37. }
  38. // 将相对路径转换为绝对路径
  39. let absoluteTemplatePath = templateImagePath;
  40. if (!isAbsolute(templateImagePath)) {
  41. absoluteTemplatePath = join(__dirname, '..', templateImagePath);
  42. }
  43. // 1. 获取设备分辨率
  44. const resolutionResult = await getDeviceResolution(ipPort);
  45. if (!resolutionResult.success) {
  46. return { success: false, error: '获取设备分辨率失败' };
  47. }
  48. const { width, height } = resolutionResult;
  49. // 2. 获取屏幕截图
  50. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  51. if (!screenshotResult.success || !screenshotResult.data) {
  52. return { success: false, error: '获取屏幕截图失败' };
  53. }
  54. // 3. 保存截图到临时文件
  55. const tempDir = join(__dirname, '..');
  56. const screenshotPath = join(tempDir, 'temp_screenshot.png');
  57. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  58. await writeFile(screenshotPath, screenshotBuffer);
  59. // 4. 调用 Python 脚本进行图像匹配
  60. // 使用虚拟环境中的 Python 解释器
  61. const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
  62. const pythonScriptPath = join(__dirname, '..', 'py', 'img-reg.py');
  63. // 确保路径使用正斜杠,避免 Windows 路径问题
  64. const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
  65. const normalizedTemplatePath = absoluteTemplatePath.replace(/\\/g, '/');
  66. // 使用 -u 参数确保输出不被缓冲,并设置 UTF-8 编码
  67. const command = `"${pythonExePath}" -u "${pythonScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width} ${height}`;
  68. const { stdout, stderr } = await execAsync(command, {
  69. timeout: 10000,
  70. maxBuffer: 10 * 1024 * 1024,
  71. cwd: join(__dirname, '..'),
  72. encoding: 'utf8',
  73. env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
  74. });
  75. // 5. 解析 Python 脚本输出
  76. // 输出格式可能是:
  77. // - "找到匹配!坐标: x=100, y=200, 宽度=50, 高度=50"
  78. // - "JSON格式: {"x": 100, "y": 200, "width": 50, "height": 50}"
  79. // - "未找到匹配"
  80. // - "错误: xxx"
  81. // 检查是否有错误或未找到匹配
  82. if (stdout.includes('未找到匹配') || stdout.toLowerCase().includes('错误') || stderr) {
  83. const errorMsg = stdout.includes('未找到匹配')
  84. ? '未找到匹配的图像'
  85. : (stderr || stdout.match(/错误[::]\s*(.+)/)?.[1] || '图像匹配失败');
  86. return { success: false, error: errorMsg };
  87. }
  88. // 尝试从输出中提取 JSON
  89. // 先尝试匹配 JSON 格式的行
  90. let jsonMatch = stdout.match(/JSON格式:\s*(\{[^}]+\})/);
  91. if (!jsonMatch) {
  92. // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
  93. 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*\}/);
  94. }
  95. if (!jsonMatch) {
  96. // 如果还是没有找到,尝试从文本中提取坐标
  97. const coordMatch = stdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
  98. if (coordMatch) {
  99. const x = parseInt(coordMatch[1], 10);
  100. const y = parseInt(coordMatch[2], 10);
  101. const w = parseInt(coordMatch[3], 10);
  102. const h = parseInt(coordMatch[4], 10);
  103. // 计算点击位置(中心点)
  104. const clickX = Math.round(x + w / 2);
  105. const clickY = Math.round(y + h / 2);
  106. return {
  107. success: true,
  108. coordinate: { x, y, width: w, height: h },
  109. clickPosition: { x: clickX, y: clickY }
  110. };
  111. }
  112. // 如果都找不到,返回错误并输出原始输出用于调试
  113. console.error('Python 脚本输出:', cleanStdout);
  114. console.error('Python 脚本错误输出:', cleanStderr);
  115. return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
  116. }
  117. let coordinate;
  118. try {
  119. coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
  120. } catch (parseError) {
  121. console.error('JSON 解析失败:', parseError);
  122. console.error('原始输出:', cleanStdout);
  123. return { success: false, error: `JSON 解析失败: ${parseError.message}` };
  124. }
  125. const { x, y, width: w, height: h } = coordinate;
  126. // 计算点击位置(中心点)
  127. const clickX = Math.round(x + w / 2);
  128. const clickY = Math.round(y + h / 2);
  129. return {
  130. success: true,
  131. coordinate: { x, y, width: w, height: h },
  132. clickPosition: { x: clickX, y: clickY }
  133. };
  134. } catch (error) {
  135. console.error('图像匹配失败:', error);
  136. return { success: false, error: error.message };
  137. }
  138. }
  139. // 执行文字识别:截图、调用 Python 脚本、返回坐标
  140. export async function findTextAndGetCoordinate(ipPort, targetText) {
  141. try {
  142. if (!ipPort) {
  143. return { success: false, error: '缺少设备 ID' };
  144. }
  145. if (!targetText) {
  146. return { success: false, error: '缺少目标文字' };
  147. }
  148. // 1. 获取设备分辨率
  149. const resolutionResult = await getDeviceResolution(ipPort);
  150. if (!resolutionResult.success) {
  151. return { success: false, error: '获取设备分辨率失败' };
  152. }
  153. const { width, height } = resolutionResult;
  154. // 2. 获取屏幕截图
  155. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  156. if (!screenshotResult.success || !screenshotResult.data) {
  157. return { success: false, error: '获取屏幕截图失败' };
  158. }
  159. // 3. 保存截图到临时文件
  160. const tempDir = join(__dirname, '..');
  161. const screenshotPath = join(tempDir, 'temp_screenshot.png');
  162. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  163. await writeFile(screenshotPath, screenshotBuffer);
  164. // 4. 调用 Python 脚本进行文字识别
  165. // 使用虚拟环境中的 Python 解释器
  166. const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
  167. const pythonScriptPath = join(__dirname, '..', 'py', 'string-reg-location.py');
  168. // 转义目标文字中的双引号,避免命令解析错误
  169. const escapedText = targetText.replace(/"/g, '\\"');
  170. const command = `"${pythonExePath}" "${pythonScriptPath}" "${screenshotPath}" "${escapedText}" ${width} ${height}`;
  171. // 设置环境变量,跳过模型源连接检查
  172. const env = {
  173. ...process.env,
  174. DISABLE_MODEL_SOURCE_CHECK: 'True'
  175. };
  176. const { stdout, stderr } = await execAsync(command, {
  177. timeout: 60000, // OCR 首次运行可能需要下载模型,设置60秒超时
  178. maxBuffer: 10 * 1024 * 1024,
  179. cwd: join(__dirname, '..'),
  180. env: env
  181. });
  182. // 5. 解析 Python 脚本输出
  183. // 检查是否有错误或未找到匹配
  184. // 忽略警告信息(DeprecationWarning 等)
  185. const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
  186. const cleanStderr = stderr ? stderr.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '') : '';
  187. if (cleanStdout.includes('未找到匹配的文字') || (cleanStdout.toLowerCase().includes('错误') && !cleanStdout.includes('找到文字')) || (cleanStderr && !cleanStderr.includes('Checking connectivity'))) {
  188. const errorMsg = cleanStdout.includes('未找到匹配的文字')
  189. ? `未找到文字: ${targetText}`
  190. : (cleanStderr || cleanStdout.match(/错误[::]\s*(.+)/)?.[1] || '文字识别失败');
  191. return { success: false, error: errorMsg };
  192. }
  193. // 尝试从输出中提取 JSON(使用清理后的输出)
  194. let jsonMatch = cleanStdout.match(/JSON格式:\s*(\{[^}]+\})/);
  195. if (!jsonMatch) {
  196. // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
  197. 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*\}/);
  198. }
  199. if (!jsonMatch) {
  200. // 如果还是没有找到,尝试从文本中提取坐标
  201. const coordMatch = cleanStdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
  202. if (coordMatch) {
  203. const x = parseInt(coordMatch[1], 10);
  204. const y = parseInt(coordMatch[2], 10);
  205. const w = parseInt(coordMatch[3], 10);
  206. const h = parseInt(coordMatch[4], 10);
  207. // 计算点击位置(中心点)
  208. const clickX = Math.round(x + w / 2);
  209. const clickY = Math.round(y + h / 2);
  210. return {
  211. success: true,
  212. coordinate: { x, y, width: w, height: h },
  213. clickPosition: { x: clickX, y: clickY }
  214. };
  215. }
  216. // 如果都找不到,返回错误并输出原始输出用于调试
  217. console.error('Python 脚本输出:', cleanStdout);
  218. console.error('Python 脚本错误输出:', cleanStderr);
  219. return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
  220. }
  221. let coordinate;
  222. try {
  223. coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
  224. } catch (parseError) {
  225. console.error('JSON 解析失败:', parseError);
  226. console.error('原始输出:', cleanStdout);
  227. return { success: false, error: `JSON 解析失败: ${parseError.message}` };
  228. }
  229. const { x, y, width: w, height: h } = coordinate;
  230. // 计算点击位置(中心点)
  231. const clickX = Math.round(x + w / 2);
  232. const clickY = Math.round(y + h / 2);
  233. return {
  234. success: true,
  235. coordinate: { x, y, width: w, height: h },
  236. clickPosition: { x: clickX, y: clickY }
  237. };
  238. } catch (error) {
  239. console.error('文字识别失败:', error);
  240. // 如果是超时错误,提供更友好的提示
  241. if (error.message && error.message.includes('timeout')) {
  242. return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
  243. }
  244. return { success: false, error: error.message };
  245. }
  246. }
  247. // 读取 processing.json 文件
  248. export async function readProcessingJson(folderName) {
  249. try {
  250. const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'processing.json');
  251. const jsonContent = await readFile(jsonPath, 'utf-8');
  252. // 解析 JSON(处理可能的格式问题,如注释、尾随逗号等)
  253. // 先尝试直接解析
  254. let parsed;
  255. try {
  256. parsed = JSON.parse(jsonContent);
  257. } catch (parseError) {
  258. // 如果直接解析失败,尝试清理注释和尾随逗号(简单处理)
  259. let cleaned = jsonContent
  260. .replace(/\/\/.*$/gm, '') // 移除单行注释
  261. .replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
  262. .replace(/,(\s*[}\]])/g, '$1'); // 移除尾随逗号(在 ] 或 } 之前的逗号)
  263. try {
  264. parsed = JSON.parse(cleaned);
  265. } catch (retryError) {
  266. console.error(`JSON 解析失败 [${folderName}]:`, parseError.message);
  267. console.error('原始内容:', jsonContent.substring(0, 500));
  268. throw new Error(`JSON 格式错误: ${parseError.message}`);
  269. }
  270. }
  271. // 处理不同的 JSON 格式
  272. // 如果直接是数组,包装成对象
  273. if (Array.isArray(parsed)) {
  274. return { actions: parsed };
  275. }
  276. // 如果已经是对象,直接返回
  277. if (parsed && typeof parsed === 'object') {
  278. // 如果已经有 actions 字段,直接返回
  279. if (parsed.actions) {
  280. return parsed;
  281. }
  282. // 如果没有 actions 字段,尝试查找数组字段
  283. for (const key in parsed) {
  284. if (Array.isArray(parsed[key])) {
  285. return { actions: parsed[key], ...parsed };
  286. }
  287. }
  288. }
  289. // 如果解析成功但没有 actions 字段,返回错误信息
  290. if (!parsed || (typeof parsed === 'object' && !parsed.actions && !Array.isArray(parsed))) {
  291. console.error(`processing.json 格式错误 [${folderName}]: 缺少 actions 字段`);
  292. return null;
  293. }
  294. return parsed;
  295. } catch (error) {
  296. console.error(`读取 processing.json 失败 [${folderName}]:`, error.message);
  297. return null;
  298. }
  299. }
  300. // 注册 IPC 处理器
  301. export function registerIpcHandlers() {
  302. ipcMain.handle('get-static-folders', async () => {
  303. return await getStaticFolders();
  304. });
  305. ipcMain.handle('match-image-and-get-coordinate', async (event, ipPort, templateImagePath) => {
  306. return await matchImageAndGetCoordinate(ipPort, templateImagePath);
  307. });
  308. ipcMain.handle('read-processing-json', async (event, folderName) => {
  309. return await readProcessingJson(folderName);
  310. });
  311. ipcMain.handle('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
  312. return await findTextAndGetCoordinate(ipPort, targetText);
  313. });
  314. }