node-api.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. /**
  2. * Node.js 环境下的 API 桥接,替代 window.electronAPI
  3. */
  4. const path = require('path')
  5. const fs = require('fs')
  6. const os = require('os')
  7. const { spawnSync } = require('child_process')
  8. const projectRoot = path.resolve(__dirname, '..', '..')
  9. const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
  10. const imageMatchScriptPath = path.join(projectRoot, 'python', 'scripts', 'image-match.py')
  11. const config = require(path.join(projectRoot, 'configs', 'config.js'))
  12. function runAdb(action, args = [], deviceId = '') {
  13. const result = spawnSync('node', [adbInteractPath, action, ...args, deviceId], {
  14. encoding: 'utf-8',
  15. timeout: 10000
  16. })
  17. return { success: result.status === 0, error: result.stderr }
  18. }
  19. async function sendTap(device, x, y) {
  20. const r = runAdb('tap', [String(x), String(y)], device)
  21. return r
  22. }
  23. async function sendSwipe(device, x1, y1, x2, y2, duration) {
  24. const r = runAdb('swipe-coords', [String(x1), String(y1), String(x2), String(y2), String(duration || 300)], device)
  25. return r
  26. }
  27. async function sendKeyEvent(device, keyCode) {
  28. const r = runAdb('keyevent', [String(keyCode)], device)
  29. return r
  30. }
  31. async function sendText(device, text) {
  32. const r = runAdb('text', [String(text)], device)
  33. return r
  34. }
  35. async function matchImageAndGetCoordinate(device, imagePath) {
  36. const templatePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(projectRoot, imagePath)
  37. if (!fs.existsSync(templatePath)) {
  38. return { success: false, error: `模板图片不存在: ${templatePath}` }
  39. }
  40. const ts = Date.now()
  41. const screenshotPath = path.join(os.tmpdir(), `ef-screenshot-${ts}.png`)
  42. const templateCopyPath = path.join(os.tmpdir(), `ef-template-${ts}.png`)
  43. try {
  44. fs.copyFileSync(templatePath, templateCopyPath)
  45. } catch (e) {
  46. return { success: false, error: `复制模板失败: ${e.message}` }
  47. }
  48. const venvPython = path.join(projectRoot, 'python', 'env', 'Scripts', 'python.exe')
  49. const hasVenv = fs.existsSync(venvPython)
  50. const pythonPath = hasVenv
  51. ? venvPython
  52. : (config.pythonPath?.path ? path.join(config.pythonPath.path, 'python.exe') : 'python')
  53. const adbPathRel = config.adbPath?.path || 'lib/scrcpy-adb/adb.exe'
  54. const adbPath = path.isAbsolute(adbPathRel) ? adbPathRel : path.join(projectRoot, adbPathRel)
  55. const screenshotPathNorm = screenshotPath.replace(/\\/g, '/')
  56. const templateCopyPathNorm = templateCopyPath.replace(/\\/g, '/')
  57. const matchResult = spawnSync(pythonPath, [imageMatchScriptPath, '--adb', adbPath, '--device', device, '--screenshot', screenshotPathNorm, '--template', templateCopyPathNorm], {
  58. encoding: 'utf-8',
  59. timeout: 20000,
  60. env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
  61. cwd: projectRoot
  62. })
  63. try { fs.unlinkSync(screenshotPath) } catch (_) {}
  64. try { fs.unlinkSync(templateCopyPath) } catch (_) {}
  65. if (matchResult.status !== 0) {
  66. const errMsg = (matchResult.stderr || matchResult.stdout || '').trim()
  67. return { success: false, error: errMsg || '图像匹配失败' }
  68. }
  69. let out
  70. try {
  71. out = JSON.parse(matchResult.stdout.trim())
  72. } catch (e) {
  73. return { success: false, error: `解析匹配结果失败: ${matchResult.stdout}` }
  74. }
  75. if (!out.success) {
  76. return { success: false, error: out.error || '未找到匹配' }
  77. }
  78. return {
  79. success: true,
  80. coordinate: { x: out.x, y: out.y, width: out.width, height: out.height },
  81. clickPosition: { x: out.center_x, y: out.center_y }
  82. }
  83. }
  84. async function findTextAndGetCoordinate(device, targetText) {
  85. return { success: false, error: 'findTextAndGetCoordinate 需在主进程实现' }
  86. }
  87. async function appendLog(folderPath, message) {
  88. const logPath = path.join(folderPath, 'log.txt')
  89. fs.appendFileSync(logPath, message + '\n')
  90. return Promise.resolve()
  91. }
  92. function readTextFile(filePath) {
  93. const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
  94. const content = fs.readFileSync(fullPath, 'utf8')
  95. return { success: true, content }
  96. }
  97. function writeTextFile(filePath, content) {
  98. const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
  99. const dir = path.dirname(fullPath)
  100. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
  101. fs.writeFileSync(fullPath, content)
  102. return { success: true }
  103. }
  104. async function stub(name) {
  105. return { success: false, error: `${name} 需在主进程实现` }
  106. }
  107. const nodeApi = {
  108. sendTap,
  109. sendSwipe,
  110. sendKeyEvent,
  111. sendText,
  112. matchImageAndGetCoordinate,
  113. findTextAndGetCoordinate,
  114. appendLog,
  115. readTextFile,
  116. writeTextFile,
  117. saveChatHistory: () => stub('saveChatHistory'),
  118. readChatHistory: () => stub('readChatHistory'),
  119. readAllChatHistory: () => stub('readAllChatHistory'),
  120. saveChatHistorySummary: () => stub('saveChatHistorySummary'),
  121. getChatHistorySummary: () => stub('getChatHistorySummary'),
  122. saveChatHistoryTxt: () => stub('saveChatHistoryTxt'),
  123. extractChatHistory: () => stub('extractChatHistory'),
  124. readLastMessage: () => stub('readLastMessage'),
  125. ocrLastMessage: () => stub('ocrLastMessage'),
  126. getCachedScreenshot: () => stub('getCachedScreenshot'),
  127. captureScreenshot: () => stub('captureScreenshot'),
  128. async readImageFileAsBase64(filePath) {
  129. try {
  130. const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
  131. const buf = fs.readFileSync(fullPath)
  132. const data = buf.toString('base64')
  133. return { success: true, data }
  134. } catch (e) {
  135. return { success: false, error: e.message }
  136. }
  137. },
  138. matchImageRegionLocation: () => stub('matchImageRegionLocation'),
  139. cropAndSaveImage: () => stub('cropAndSaveImage'),
  140. async saveBase64Image(base64, filePath) {
  141. try {
  142. const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
  143. const dir = path.dirname(fullPath)
  144. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
  145. const buf = Buffer.from(base64, 'base64')
  146. fs.writeFileSync(fullPath, buf)
  147. return { success: true }
  148. } catch (e) {
  149. return { success: false, error: e.message }
  150. }
  151. }
  152. }
  153. module.exports = nodeApi