image-area-cropping.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * Func 标签:image-area-cropping
  3. *
  4. * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  5. * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
  6. *
  7. * 语义:根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。
  8. */
  9. const electronAPI = require('../node-api.js')
  10. const tagName = 'image-area-cropping'
  11. const schema = {
  12. description: '根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。',
  13. inputs: {
  14. area: '区域坐标(JSON字符串格式,包含 topLeft 和 bottomRight,或包含 x, y, width, height)',
  15. savePath: '保存路径(相对于工作流目录或绝对路径)',
  16. },
  17. outputs: {
  18. result: '保存结果(成功返回 "1",失败返回 "0")',
  19. },
  20. };
  21. /**
  22. * 执行 image-area-cropping 功能
  23. * 这个函数会被 ActionParser 调用
  24. *
  25. * @param {Object} params - 参数对象
  26. * @param {string} params.area - 区域坐标(JSON字符串或对象,格式:{topLeft: {x, y}, bottomRight: {x, y}} 或 {x, y, width, height})
  27. * @param {string} params.savePath - 保存路径
  28. * @param {string} params.folderPath - 工作流文件夹路径
  29. * @param {string} params.device - 设备 ID/IP:Port(可选,用于获取最新截图)
  30. * @returns {Promise<{success: boolean, error?: string}>}
  31. */
  32. async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
  33. try {
  34. // 解析区域坐标
  35. let areaObj = area;
  36. if (typeof area === 'string') {
  37. try {
  38. areaObj = JSON.parse(area);
  39. } catch (e) {
  40. return {
  41. success: false,
  42. error: `区域坐标格式错误,无法解析JSON: ${e.message}`
  43. };
  44. }
  45. }
  46. if (!areaObj || typeof areaObj !== 'object') {
  47. return {
  48. success: false,
  49. error: '区域坐标必须是对象格式'
  50. };
  51. }
  52. // 提取坐标信息(支持多种格式)
  53. let x, y, width, height;
  54. if (areaObj.topLeft && areaObj.bottomRight) {
  55. // 格式1:{topLeft: {x, y}, bottomRight: {x, y}}
  56. x = parseInt(areaObj.topLeft.x);
  57. y = parseInt(areaObj.topLeft.y);
  58. width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
  59. height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
  60. } else if (areaObj.topLeft && areaObj.topRight && areaObj.bottomLeft && areaObj.bottomRight) {
  61. // 格式1.5:{topLeft, topRight, bottomLeft, bottomRight} - 使用 topLeft 和 bottomRight
  62. x = parseInt(areaObj.topLeft.x);
  63. y = parseInt(areaObj.topLeft.y);
  64. width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
  65. height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
  66. } else if (areaObj.x !== undefined && areaObj.y !== undefined && areaObj.width !== undefined && areaObj.height !== undefined) {
  67. // 格式2:{x, y, width, height}
  68. x = parseInt(areaObj.x);
  69. y = parseInt(areaObj.y);
  70. width = parseInt(areaObj.width);
  71. height = parseInt(areaObj.height);
  72. } else {
  73. return {
  74. success: false,
  75. error: '区域坐标格式不正确,需要包含 topLeft/bottomRight 或 x/y/width/height'
  76. };
  77. }
  78. // 验证坐标有效性
  79. if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
  80. return {
  81. success: false,
  82. error: `区域坐标无效: x=${x}, y=${y}, width=${width}, height=${height}`
  83. };
  84. }
  85. // 先通过 ADB 截图当前手机屏幕并保存到 history 文件夹
  86. // 参考 screenshot.js 的实现方式
  87. let imageBase64 = null; // 声明 imageBase64 变量
  88. let screenshotPath;
  89. const fileExtension = 'jpg'
  90. if (folderPath.includes(':')) {
  91. // 绝对路径
  92. screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
  93. } else {
  94. // 相对路径,构建相对于项目根目录的路径
  95. screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
  96. }
  97. if (device && electronAPI.getCachedScreenshot) {
  98. const screenshotResult = await electronAPI.getCachedScreenshot(device)
  99. if (screenshotResult && screenshotResult.success && screenshotResult.data) {
  100. imageBase64 = screenshotResult.data
  101. }
  102. }
  103. if (!imageBase64 && electronAPI.captureScreenshot) {
  104. const screenshotResult = await electronAPI.captureScreenshot(device)
  105. if (screenshotResult && screenshotResult.success && screenshotResult.data) {
  106. imageBase64 = screenshotResult.data
  107. }
  108. }
  109. if (imageBase64 && electronAPI.saveBase64Image) {
  110. await electronAPI.saveBase64Image(imageBase64, screenshotPath)
  111. }
  112. // 处理保存路径(如果是相对路径,相对于工作流目录)
  113. let absoluteSavePath = savePath;
  114. if (!savePath.includes(':')) {
  115. // 相对路径,相对于工作流目录
  116. if (folderPath.includes(':')) {
  117. absoluteSavePath = `${folderPath}/${savePath}`;
  118. } else {
  119. absoluteSavePath = `${folderPath}/${savePath}`;
  120. }
  121. }
  122. if (!imageBase64 && electronAPI.readImageFileAsBase64) {
  123. const fileContent = await electronAPI.readImageFileAsBase64(screenshotPath)
  124. if (!fileContent || !fileContent.success) {
  125. return { success: false, error: `无法读取截图文件: ${fileContent?.error || '未知错误'}` }
  126. }
  127. imageBase64 = fileContent.data
  128. }
  129. // 验证 base64 数据是否有效(至少应该是几百字节)
  130. if (!imageBase64 || imageBase64.length < 100) {
  131. return {
  132. success: false,
  133. error: `截图数据无效,base64长度: ${imageBase64?.length || 0},应该是至少几百字节。可能是截图失败或读取失败`
  134. };
  135. }
  136. return { success: false, error: 'image-area-cropping 需使用 sharp/jimp 等 Node 图片库实现' }
  137. } catch (error) {
  138. return {
  139. success: false,
  140. error: error.message || '裁剪图片失败'
  141. };
  142. }
  143. }
  144. module.exports = { tagName, schema, executeImageAreaCropping }