execute-py.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /**
  2. * 执行 Python 相关操作模块(通用功能)
  3. * 负责图像匹配、OCR识别等通用功能(通过调用 Python/JS 实现)
  4. * 注意:聊天记录提取等业务相关功能在 read-and-write.js 中
  5. */
  6. import { ipcMain } from 'electron';
  7. import { writeFile, mkdir, rm } from 'fs/promises';
  8. import { join, dirname, isAbsolute } from 'path';
  9. import { fileURLToPath } from 'url';
  10. import { captureScreenshot } from './adb/screenshot.js';
  11. import { getDeviceResolution } from './adb/device-info.js';
  12. import { matchImage } from './func/image-center-location.js';
  13. import { findTextLocation } from './func/string-reg-location.js';
  14. import { ocrFullScreen as ocrFullScreenFromFunc, getLastMessage as getLastMessageFromFunc } from './func/ocr-chat.js';
  15. const __filename = fileURLToPath(import.meta.url);
  16. const __dirname = dirname(__filename);
  17. /**
  18. * 执行图像匹配:截图、调用 Python 脚本、返回坐标
  19. * @param {string} ipPort - 设备 ID/IP:Port
  20. * @param {string} templateImagePath - 模板图片路径
  21. * @returns {Promise<{success: boolean, error?: string, coordinate?: Object, clickPosition?: Object}>}
  22. */
  23. export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
  24. try {
  25. if (!ipPort) {
  26. return { success: false, error: '缺少设备 ID' };
  27. }
  28. if (!templateImagePath) {
  29. return { success: false, error: '缺少模板图片路径' };
  30. }
  31. // 将相对路径转换为绝对路径
  32. let absoluteTemplatePath = templateImagePath;
  33. if (!isAbsolute(templateImagePath)) {
  34. absoluteTemplatePath = join(__dirname, '..', templateImagePath);
  35. }
  36. // 1. 获取设备分辨率
  37. const resolutionResult = await getDeviceResolution(ipPort);
  38. if (!resolutionResult.success) {
  39. return { success: false, error: '获取设备分辨率失败' };
  40. }
  41. const { width, height } = resolutionResult;
  42. // 2. 获取屏幕截图
  43. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  44. if (!screenshotResult.success || !screenshotResult.data) {
  45. return { success: false, error: '获取屏幕截图失败' };
  46. }
  47. // 3. 保存截图到临时文件
  48. const tempDir = join(__dirname, '..');
  49. const screenshotPath = join(tempDir, 'temp_screenshot.png');
  50. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  51. await writeFile(screenshotPath, screenshotBuffer);
  52. // 4. 调用 JS 函数进行图像匹配
  53. const matchResult = await matchImage(screenshotPath, absoluteTemplatePath, width, height);
  54. if (!matchResult.success) {
  55. return { success: false, error: matchResult.error || '图像匹配失败' };
  56. }
  57. // 5. 返回匹配结果
  58. if (matchResult.success && matchResult.x !== undefined) {
  59. const { x, y, width: w, height: h } = matchResult;
  60. // 计算点击位置(中心点)
  61. const clickX = Math.round(x + w / 2);
  62. const clickY = Math.round(y + h / 2);
  63. return {
  64. success: true,
  65. coordinate: { x, y, width: w, height: h },
  66. clickPosition: { x: clickX, y: clickY }
  67. };
  68. } else {
  69. return {
  70. success: false,
  71. error: matchResult.error || '图像匹配失败'
  72. };
  73. }
  74. } catch (error) {
  75. console.error('图像匹配失败:', error);
  76. return { success: false, error: error.message };
  77. }
  78. }
  79. /**
  80. * 图像区域定位:在完整截图中查找区域截图的位置,返回四个顶点坐标
  81. * @param {string} screenshotPath - 完整截图路径
  82. * @param {string} regionPath - 区域截图路径
  83. * @param {string} device - 设备 ID(可选,用于获取分辨率)
  84. * @returns {Promise<{success: boolean, error?: string, corners?: Object}>}
  85. */
  86. export async function matchImageRegionLocation(screenshotPath, regionPath, device = null) {
  87. try {
  88. if (!regionPath) {
  89. return { success: false, error: '缺少区域截图路径' };
  90. }
  91. // 如果 screenshotPath 为 '__AUTO_SCREENSHOT__' 或 null,自动从设备获取截图
  92. let absoluteScreenshotPath = screenshotPath;
  93. if (!screenshotPath || screenshotPath === '__AUTO_SCREENSHOT__' || screenshotPath === null) {
  94. if (!device) {
  95. return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' };
  96. }
  97. // 自动获取设备截图
  98. const resolutionResult = await getDeviceResolution(device);
  99. if (!resolutionResult.success) {
  100. return { success: false, error: '获取设备分辨率失败' };
  101. }
  102. const screenshotResult = await captureScreenshot(device, { format: 'png' });
  103. if (!screenshotResult.success || !screenshotResult.data) {
  104. return { success: false, error: '自动获取设备截图失败' };
  105. }
  106. // 保存截图到临时文件
  107. const tempDir = join(__dirname, '..');
  108. const tempScreenshotPath = join(tempDir, 'temp_screenshot.png');
  109. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  110. await writeFile(tempScreenshotPath, screenshotBuffer);
  111. absoluteScreenshotPath = tempScreenshotPath;
  112. } else {
  113. // 将相对路径转换为绝对路径
  114. if (!isAbsolute(screenshotPath)) {
  115. absoluteScreenshotPath = join(__dirname, '..', screenshotPath);
  116. }
  117. }
  118. let absoluteRegionPath = regionPath;
  119. if (!isAbsolute(regionPath)) {
  120. absoluteRegionPath = join(__dirname, '..', regionPath);
  121. }
  122. // 可选:如果提供了设备ID,获取设备分辨率用于缩放
  123. let width = null;
  124. let height = null;
  125. if (device) {
  126. const resolutionResult = await getDeviceResolution(device);
  127. if (resolutionResult.success) {
  128. width = resolutionResult.width;
  129. height = resolutionResult.height;
  130. }
  131. }
  132. // 调用图像匹配函数
  133. const matchResult = await matchImage(absoluteScreenshotPath, absoluteRegionPath, width, height);
  134. if (!matchResult.success) {
  135. return { success: false, error: matchResult.error || '图像匹配失败' };
  136. }
  137. // 获取匹配结果
  138. const { x, y, width: w, height: h } = matchResult;
  139. // 计算四个顶点坐标
  140. const corners = {
  141. topLeft: { x, y },
  142. topRight: { x: x + w, y },
  143. bottomLeft: { x, y: y + h },
  144. bottomRight: { x: x + w, y: y + h }
  145. };
  146. return {
  147. success: true,
  148. x,
  149. y,
  150. width: w,
  151. height: h,
  152. corners: corners,
  153. similarity: matchResult.similarity
  154. };
  155. } catch (error) {
  156. console.error('图像区域定位失败:', error);
  157. return { success: false, error: error.message };
  158. }
  159. }
  160. /**
  161. * 执行文字识别:截图、调用 Python 脚本、返回坐标
  162. * @param {string} ipPort - 设备 ID/IP:Port
  163. * @param {string} targetText - 目标文字
  164. * @returns {Promise<{success: boolean, error?: string, coordinate?: Object, clickPosition?: Object}>}
  165. */
  166. export async function findTextAndGetCoordinate(ipPort, targetText) {
  167. try {
  168. if (!ipPort) {
  169. return { success: false, error: '缺少设备 ID' };
  170. }
  171. if (!targetText) {
  172. return { success: false, error: '缺少目标文字' };
  173. }
  174. // 1. 获取设备分辨率
  175. const resolutionResult = await getDeviceResolution(ipPort);
  176. if (!resolutionResult.success) {
  177. return { success: false, error: '获取设备分辨率失败' };
  178. }
  179. const { width, height } = resolutionResult;
  180. // 2. 获取屏幕截图
  181. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  182. if (!screenshotResult.success || !screenshotResult.data) {
  183. return { success: false, error: '获取屏幕截图失败' };
  184. }
  185. // 3. 保存截图到临时文件
  186. const tempDir = join(__dirname, '..');
  187. const screenshotPath = join(tempDir, 'temp_screenshot.png');
  188. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  189. await writeFile(screenshotPath, screenshotBuffer);
  190. // 4. 调用 JS 函数进行文字识别
  191. const textResult = await findTextLocation(screenshotPath, targetText, width, height);
  192. if (!textResult.success || !textResult.found) {
  193. return { success: false, error: textResult.error || `未找到文字: ${targetText}` };
  194. }
  195. // 5. 返回识别结果
  196. const { x, y, width: w, height: h } = textResult;
  197. // 计算点击位置(中心点)
  198. const clickX = Math.round(x + w / 2);
  199. const clickY = Math.round(y + h / 2);
  200. return {
  201. success: true,
  202. coordinate: { x, y, width: w, height: h },
  203. clickPosition: { x: clickX, y: clickY }
  204. };
  205. } catch (error) {
  206. console.error('文字识别失败:', error);
  207. // 如果是超时错误,提供更友好的提示
  208. if (error.message && error.message.includes('timeout')) {
  209. return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
  210. }
  211. return { success: false, error: error.message };
  212. }
  213. }
  214. /**
  215. * 全屏OCR识别(通用功能)
  216. * @param {string} ipPort - 设备 ID/IP:Port
  217. * @param {string} folderPath - 工作流文件夹路径(可选,用于保存临时文件)
  218. * @returns {Promise<{success: boolean, error?: string, text?: string}>}
  219. */
  220. export async function ocrFullScreen(ipPort, folderPath = null) {
  221. try {
  222. if (!ipPort) {
  223. return { success: false, error: '缺少设备 ID' };
  224. }
  225. // 1. 获取设备分辨率
  226. const resolutionResult = await getDeviceResolution(ipPort);
  227. if (!resolutionResult.success) {
  228. return { success: false, error: '获取设备分辨率失败' };
  229. }
  230. const { width, height } = resolutionResult;
  231. // 2. 获取屏幕截图
  232. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  233. if (!screenshotResult.success || !screenshotResult.data) {
  234. return { success: false, error: '获取屏幕截图失败' };
  235. }
  236. // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
  237. let screenshotPath;
  238. let tmpDir = null; // 用于跟踪需要删除的临时目录
  239. if (folderPath) {
  240. // 确保 folderPath 是绝对路径
  241. let absoluteFolderPath = folderPath;
  242. if (!isAbsolute(folderPath)) {
  243. // 如果已经是 static/processing/xxx 格式,去掉开头的 static/processing 再拼接
  244. if (folderPath.startsWith('static/processing/')) {
  245. const folderName = folderPath.replace('static/processing/', '');
  246. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
  247. } else if (folderPath.startsWith('static\\processing\\')) {
  248. const folderName = folderPath.replace('static\\processing\\', '');
  249. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
  250. } else {
  251. // 如果只是文件夹名,需要加上 static/processing
  252. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderPath);
  253. }
  254. }
  255. const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
  256. tmpDir = join(absoluteFolderPath, 'tmp', timestamp);
  257. await mkdir(tmpDir, { recursive: true });
  258. screenshotPath = join(tmpDir, 'screenshot_ocr.png');
  259. } else {
  260. const tempDir = join(__dirname, '..');
  261. screenshotPath = join(tempDir, 'temp_screenshot_ocr.png');
  262. }
  263. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  264. await writeFile(screenshotPath, screenshotBuffer);
  265. try {
  266. // 4. 调用 JS 实现进行全屏OCR识别
  267. const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
  268. const result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
  269. if (result.success) {
  270. return {
  271. success: true,
  272. text: result.text || ''
  273. };
  274. } else {
  275. return { success: false, error: result.error || 'OCR识别失败' };
  276. }
  277. } finally {
  278. // 5. 使用完后删除临时目录
  279. if (tmpDir) {
  280. try {
  281. await rm(tmpDir, { recursive: true, force: true });
  282. // 已删除临时目录日志(不显示)
  283. } catch (rmError) {
  284. console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
  285. }
  286. }
  287. }
  288. } catch (error) {
  289. console.error('OCR识别失败:', error);
  290. if (error.message && error.message.includes('timeout')) {
  291. return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
  292. }
  293. return { success: false, error: error.message };
  294. }
  295. }
  296. /**
  297. * OCR识别最后一条消息(兼容旧API)
  298. * @param {string} ipPort - 设备 ID/IP:Port
  299. * @param {string} method - 识别方法 ('full-screen' | 'by-avatar')
  300. * @param {string} avatarPath - 头像路径(by-avatar 时使用)
  301. * @param {string} area - 区域(未使用,保留兼容性)
  302. * @param {string} folderPath - 工作流文件夹路径(可选)
  303. * @returns {Promise<{success: boolean, error?: string, text?: string, position?: Object}>}
  304. */
  305. export async function ocrLastMessage(ipPort, method, avatarPath, area, folderPath = null) {
  306. try {
  307. if (!ipPort) {
  308. return { success: false, error: '缺少设备 ID' };
  309. }
  310. // 1. 获取设备分辨率
  311. const resolutionResult = await getDeviceResolution(ipPort);
  312. if (!resolutionResult.success) {
  313. return { success: false, error: '获取设备分辨率失败' };
  314. }
  315. const { width, height } = resolutionResult;
  316. // 2. 获取屏幕截图
  317. const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
  318. if (!screenshotResult.success || !screenshotResult.data) {
  319. return { success: false, error: '获取屏幕截图失败' };
  320. }
  321. // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
  322. let screenshotPath;
  323. let tmpDir = null; // 用于跟踪需要删除的临时目录
  324. if (folderPath) {
  325. // 确保 folderPath 是绝对路径
  326. let absoluteFolderPath = folderPath;
  327. if (!isAbsolute(folderPath)) {
  328. // 如果已经是 static/processing/xxx 格式,去掉开头的 static/processing 再拼接
  329. if (folderPath.startsWith('static/processing/')) {
  330. const folderName = folderPath.replace('static/processing/', '');
  331. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
  332. } else if (folderPath.startsWith('static\\processing\\')) {
  333. const folderName = folderPath.replace('static\\processing\\', '');
  334. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
  335. } else {
  336. // 如果只是文件夹名,需要加上 static/processing
  337. absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderPath);
  338. }
  339. }
  340. const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
  341. tmpDir = join(absoluteFolderPath, 'tmp', timestamp);
  342. await mkdir(tmpDir, { recursive: true });
  343. screenshotPath = join(tmpDir, 'screenshot_ocr.png');
  344. } else {
  345. const tempDir = join(__dirname, '..');
  346. screenshotPath = join(tempDir, 'temp_screenshot_ocr.png');
  347. }
  348. const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
  349. await writeFile(screenshotPath, screenshotBuffer);
  350. try {
  351. // 4. 调用 JS 实现进行OCR识别
  352. const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
  353. let result;
  354. if (method === 'full-screen') {
  355. // 全屏OCR识别
  356. result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
  357. } else if (method === 'by-avatar' && avatarPath) {
  358. // 通过头像定位最后一条消息
  359. let friendAvatarArg = null;
  360. let myAvatarArg = null;
  361. if (isAbsolute(avatarPath)) {
  362. friendAvatarArg = avatarPath;
  363. myAvatarArg = avatarPath;
  364. } else {
  365. const folderName = avatarPath.split(/[/\\]/)[0];
  366. const avatarName = avatarPath.split(/[/\\]/).slice(1).join('/');
  367. friendAvatarArg = join(__dirname, '..', 'static', 'processing', folderName, avatarName);
  368. myAvatarArg = friendAvatarArg;
  369. }
  370. const normalizedFriendAvatar = friendAvatarArg.replace(/\\/g, '/');
  371. const normalizedMyAvatar = myAvatarArg.replace(/\\/g, '/');
  372. result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
  373. } else {
  374. // 默认使用全屏OCR
  375. result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
  376. }
  377. if (result.success) {
  378. // 返回兼容旧API的格式
  379. return {
  380. success: true,
  381. text: result.text || '',
  382. position: result.position || null
  383. };
  384. } else {
  385. return { success: false, error: result.error || 'OCR识别失败' };
  386. }
  387. } finally {
  388. // 5. 使用完后删除临时目录
  389. if (tmpDir) {
  390. try {
  391. await rm(tmpDir, { recursive: true, force: true });
  392. // 已删除临时目录日志(不显示)
  393. } catch (rmError) {
  394. console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
  395. }
  396. }
  397. }
  398. } catch (error) {
  399. console.error('OCR识别失败:', error);
  400. if (error.message && error.message.includes('timeout')) {
  401. return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
  402. }
  403. return { success: false, error: error.message };
  404. }
  405. }
  406. /**
  407. * 注册 IPC 处理器(Python 执行相关)
  408. */
  409. export function registerIpcHandlers() {
  410. // 图像匹配
  411. ipcMain.handle('match-image-and-get-coordinate', async (event, ipPort, templateImagePath) => {
  412. return await matchImageAndGetCoordinate(ipPort, templateImagePath);
  413. });
  414. // 图像区域定位
  415. ipcMain.handle('match-image-region-location', async (event, screenshotPath, regionPath, device) => {
  416. return await matchImageRegionLocation(screenshotPath, regionPath, device);
  417. });
  418. // 文字识别
  419. ipcMain.handle('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
  420. return await findTextAndGetCoordinate(ipPort, targetText);
  421. });
  422. // OCR识别最后一条消息(兼容旧API)
  423. ipcMain.handle('ocr-last-message', async (event, ipPort, method, avatarPath, area, folderPath) => {
  424. return await ocrLastMessage(ipPort, method, avatarPath, area, folderPath);
  425. });
  426. }