Browse Source

执行processing ok

yichael 2 weeks ago
parent
commit
14053ebb04
36 changed files with 757 additions and 300 deletions
  1. 14 2
      electron/main.js
  2. 4 2
      electron/preload.js
  3. 34 1
      nodejs/adb/adb-interact.js
  4. 59 0
      nodejs/adb/adb-screencap.js
  5. 19 15
      nodejs/ef-compiler/Func/chat/chat-history.js
  6. 9 5
      nodejs/ef-compiler/Func/chat/ocr-chat.js
  7. 9 5
      nodejs/ef-compiler/Func/chat/read-last-message.js
  8. 5 3
      nodejs/ef-compiler/Func/chat/smart-chat-append.js
  9. 26 138
      nodejs/ef-compiler/Func/image-area-cropping.js
  10. 9 5
      nodejs/ef-compiler/Func/image-center-location.js
  11. 8 5
      nodejs/ef-compiler/Func/image-region-location.js
  12. 9 5
      nodejs/ef-compiler/Func/read-txt.js
  13. 9 5
      nodejs/ef-compiler/Func/save-txt.js
  14. 5 3
      nodejs/ef-compiler/Func/string-reg-location.js
  15. 71 76
      nodejs/ef-compiler/ef-compiler.js
  16. 180 0
      nodejs/ef-compiler/node-api.js
  17. 47 10
      nodejs/run-process.js
  18. 1 0
      python/environment.txt
  19. BIN
      python/opencv/cv2/__pycache__/__init__.cpython-314.pyc
  20. BIN
      python/opencv/numpy/__pycache__/__config__.cpython-314.pyc
  21. BIN
      python/opencv/numpy/__pycache__/__init__.cpython-314.pyc
  22. BIN
      python/opencv/numpy/__pycache__/_distributor_init.cpython-314.pyc
  23. BIN
      python/opencv/numpy/__pycache__/_expired_attrs_2_0.cpython-314.pyc
  24. BIN
      python/opencv/numpy/__pycache__/_globals.cpython-314.pyc
  25. BIN
      python/opencv/numpy/__pycache__/version.cpython-314.pyc
  26. BIN
      python/opencv/numpy/_core/__pycache__/__init__.cpython-314.pyc
  27. BIN
      python/opencv/numpy/_core/__pycache__/multiarray.cpython-314.pyc
  28. BIN
      python/opencv/numpy/_core/__pycache__/overrides.cpython-314.pyc
  29. BIN
      python/opencv/numpy/_utils/__pycache__/__init__.cpython-314.pyc
  30. BIN
      python/opencv/numpy/_utils/__pycache__/_convertions.cpython-314.pyc
  31. BIN
      python/opencv/numpy/_utils/__pycache__/_inspect.cpython-314.pyc
  32. 167 0
      python/scripts/image-match.py
  33. 10 4
      src/page/device/connect-item/connect-item.jsx
  34. 11 7
      src/page/device/device.js
  35. 8 1
      src/page/device/device.jsx
  36. 43 8
      src/page/process/process-item/process-item.js

+ 14 - 2
electron/main.js

@@ -128,7 +128,8 @@ ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) =>
       stderr += data.toString()
     })
     
-    const timeoutId = setTimeout(() => {
+    const isRunProcess = scriptName === 'run-process'
+    const timeoutId = isRunProcess ? null : setTimeout(() => {
       if (!resolved) {
         finish({
           success: false,
@@ -141,7 +142,7 @@ ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) =>
     }, 5000)
 
     nodeProcess.on('close', (code) => {
-      clearTimeout(timeoutId)
+      if (timeoutId) clearTimeout(timeoutId)
       runningProcesses.delete(processKey)
       const exitCode = (code !== null && code !== undefined) ? code : 1
       finish({
@@ -163,6 +164,17 @@ ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) =>
   })
 })
 
+/** 停止指定的 Node.js 脚本进程 */
+ipcMain.handle('kill-nodejs-script', async (event, scriptName, ...parameters) => {
+  const processKey = `${scriptName}-${parameters.join('-')}`
+  if (runningProcesses.has(processKey)) {
+    runningProcesses.get(processKey).kill()
+    runningProcesses.delete(processKey)
+    return { killed: true }
+  }
+  return { killed: false }
+})
+
 // Execute Python script
 ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => {
   return new Promise((resolve, reject) => {

+ 4 - 2
electron/preload.js

@@ -1,8 +1,10 @@
 const { contextBridge, ipcRenderer } = require('electron')
 
 contextBridge.exposeInMainWorld('electronAPI', {
-  runNodejsScript: (scriptName, ...parameters) => 
+  runNodejsScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('run-nodejs-script', scriptName, ...parameters),
-  runPythonScript: (scriptName, ...parameters) => 
+  killNodejsScript: (scriptName, ...parameters) =>
+    ipcRenderer.invoke('kill-nodejs-script', scriptName, ...parameters),
+  runPythonScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('run-python-script', scriptName, ...parameters)
 })

+ 34 - 1
nodejs/adb/adb-interact.js

@@ -85,7 +85,40 @@ switch (action) {
     execSync(swipeCommand, { encoding: 'utf-8', timeout: 2000 })
     process.exit(0)
   }
-  
+
+  case 'swipe-coords': {
+    const x1 = process.argv[3]
+    const y1 = process.argv[4]
+    const x2 = process.argv[5]
+    const y2 = process.argv[6]
+    const duration = process.argv[7] || 300
+    const deviceId = process.argv[8] || ''
+    if (!x1 || !y1 || !x2 || !y2) process.exit(1)
+    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
+    const cmd = `"${adbPath}" ${deviceFlag}shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`
+    execSync(cmd, { encoding: 'utf-8', timeout: 2000 })
+    process.exit(0)
+  }
+
+  case 'keyevent': {
+    const keyCode = process.argv[3]
+    const deviceId = process.argv[4] || ''
+    if (!keyCode) process.exit(1)
+    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
+    execSync(`"${adbPath}" ${deviceFlag}shell input keyevent ${keyCode}`, { encoding: 'utf-8', timeout: 1000 })
+    process.exit(0)
+  }
+
+  case 'text': {
+    const text = process.argv[3]
+    const deviceId = process.argv[4] || ''
+    if (text === undefined) process.exit(1)
+    const deviceFlag = deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
+    const escaped = String(text).replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')
+    execSync(`"${adbPath}" ${deviceFlag}shell input text "${escaped}"`, { encoding: 'utf-8', timeout: 5000 })
+    process.exit(0)
+  }
+
   default:
     process.exit(1)
 }

+ 59 - 0
nodejs/adb/adb-screencap.js

@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+/**
+ * ADB 截图:将设备屏幕捕获到本地文件
+ * 用法: node adb-screencap.js <deviceId> <outputPath>
+ * 示例: node adb-screencap.js 192.168.2.5:5555 C:\temp\screen.png
+ */
+
+const { spawnSync } = require('child_process')
+const path = require('path')
+const fs = require('fs')
+
+const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
+const projectRoot = path.resolve(__dirname, '..', '..')
+const adbPath = config.adbPath?.path
+  ? path.resolve(projectRoot, config.adbPath.path)
+  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+
+const deviceId = process.argv[2]
+const outputPath = process.argv[3]
+
+if (!deviceId || !outputPath) {
+  process.stderr.write('Usage: node adb-screencap.js <deviceId> <outputPath>\n')
+  process.exit(1)
+}
+
+const deviceFlag = deviceId && deviceId.includes(':') ? ['-s', deviceId] : []
+const args = [...deviceFlag, 'exec-out', 'screencap', '-p']
+
+const result = spawnSync(adbPath, args, {
+  encoding: null,
+  timeout: 10000,
+  maxBuffer: 16 * 1024 * 1024
+})
+
+if (result.error) {
+  process.stderr.write(`screencap error: ${result.error.message}\n`)
+  process.exit(1)
+}
+
+if (result.status !== 0) {
+  process.stderr.write((result.stderr || result.stdout || Buffer.from('')).toString('utf8'))
+  process.exit(1)
+}
+
+let data = result.stdout
+if (!data || data.length === 0) {
+  process.stderr.write('screencap returned empty data\n')
+  process.exit(1)
+}
+
+// 注意:不要对 PNG 做 \r\n 替换,会破坏 IDAT 压缩块导致 OpenCV/PIL 无法解析
+
+const dir = path.dirname(outputPath)
+if (!fs.existsSync(dir)) {
+  fs.mkdirSync(dir, { recursive: true })
+}
+fs.writeFileSync(outputPath, data)
+process.stdout.write(outputPath)
+process.exit(0)

+ 19 - 15
nodejs/ef-compiler/Func/chat/chat-history.js

@@ -3,19 +3,21 @@
  * 负责保存聊天记录、生成AI总结、读取历史总结、读取所有聊天记录
  */
 
+const electronAPI = require('../../node-api.js')
+
 /**
  * 保存聊天记录到 history 文件夹
  * @param {string|Array} chatHistory - 聊天记录(可以是JSON字符串、文本格式或消息数组)
  * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  * @returns {Promise<{success: boolean, error?: string, filePath?: string}>}
  */
-export async function saveChatHistory(chatHistory, folderPath) {
+async function saveChatHistory(chatHistory, folderPath) {
   try {
     if (!chatHistory) {
       return { success: false, error: '聊天记录为空' };
     }
 
-    if (!window.electronAPI || !window.electronAPI.saveChatHistory) {
+    if (!electronAPI.saveChatHistory) {
       return { success: false, error: '保存聊天记录 API 不可用' };
     }
 
@@ -55,8 +57,8 @@ export async function saveChatHistory(chatHistory, folderPath) {
     // 读取历史聊天记录,进行去重
     let historyMessages = [];
     try {
-      if (window.electronAPI && window.electronAPI.readChatHistory) {
-        const historyResult = await window.electronAPI.readChatHistory(folderPath);
+      if (electronAPI.readChatHistory) {
+        const historyResult = await electronAPI.readChatHistory(folderPath);
         if (historyResult.success && historyResult.messages && Array.isArray(historyResult.messages)) {
           historyMessages = historyResult.messages;
         }
@@ -130,8 +132,8 @@ export async function saveChatHistory(chatHistory, folderPath) {
 
     // 通过 IPC 调用主进程保存聊天记录到 chat-history.txt(新格式)
     // 使用新的 API 保存为 chat-history.txt 格式
-    if (window.electronAPI && window.electronAPI.saveChatHistoryTxt) {
-      const result = await window.electronAPI.saveChatHistoryTxt(folderPath, uniqueNewMessages);
+    if (electronAPI.saveChatHistoryTxt) {
+      const result = await electronAPI.saveChatHistoryTxt(folderPath, uniqueNewMessages);
       
       if (!result.success) {
         return { success: false, error: result.error };
@@ -141,7 +143,7 @@ export async function saveChatHistory(chatHistory, folderPath) {
     }
     
     // 向后兼容:如果没有新API,使用旧API
-    const result = await window.electronAPI.saveChatHistory(folderPath, historyData);
+    const result = await electronAPI.saveChatHistory(folderPath, historyData);
     
     if (!result.success) {
       return { success: false, error: result.error };
@@ -160,7 +162,7 @@ export async function saveChatHistory(chatHistory, folderPath) {
  * @param {string} modelName - AI模型名称(可选,默认 'gpt-5-nano-ca')
  * @returns {Promise<{success: boolean, error?: string, summary?: string}>}
  */
-export async function generateHistorySummary(chatHistory, folderPath, modelName = 'gpt-5-nano-ca') {
+async function generateHistorySummary(chatHistory, folderPath, modelName = 'gpt-5-nano-ca') {
   try {
     if (!chatHistory || !chatHistory.trim()) {
       return { success: false, error: '聊天记录为空' };
@@ -200,11 +202,11 @@ ${chatHistory}${summaryContext}
     }
 
     // 通过 IPC 调用主进程保存总结
-    if (!window.electronAPI || !window.electronAPI.saveChatHistorySummary) {
+    if (!electronAPI.saveChatHistorySummary) {
       return { success: false, error: '保存聊天记录总结 API 不可用' };
     }
 
-    const saveResult = await window.electronAPI.saveChatHistorySummary(folderPath, summary.trim());
+    const saveResult = await electronAPI.saveChatHistorySummary(folderPath, summary.trim());
     
     if (!saveResult.success) {
       return { success: false, error: saveResult.error };
@@ -222,13 +224,13 @@ ${chatHistory}${summaryContext}
  * @param {string} folderPath - 工作流文件夹路径
  * @returns {Promise<string>} 返回总结文本,如果不存在则返回空字符串
  */
-export async function getHistorySummary(folderPath) {
+async function getHistorySummary(folderPath) {
   try {
-    if (!window.electronAPI || !window.electronAPI.getChatHistorySummary) {
+    if (!electronAPI.getChatHistorySummary) {
       return '';
     }
 
-    const result = await window.electronAPI.getChatHistorySummary(folderPath);
+    const result = await electronAPI.getChatHistorySummary(folderPath);
     
     if (!result.success) {
       // 文件不存在是正常情况,返回空字符串
@@ -309,10 +311,10 @@ function parseChatHistoryText(chatHistoryText) {
  * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
  */
-export async function readAllChatHistory(folderPath) {
+async function readAllChatHistory(folderPath) {
   try {
     // 优先使用新的 API,如果没有则使用旧的(向后兼容)
-    const api = window.electronAPI?.readChatHistory || window.electronAPI?.readAllChatHistory;
+    const api = electronAPI.readChatHistory || electronAPI.readAllChatHistory;
     if (!api) {
       return { success: false, error: '读取聊天记录 API 不可用' };
     }
@@ -334,3 +336,5 @@ export async function readAllChatHistory(folderPath) {
     return { success: false, error: error.message || '读取聊天记录失败' };
   }
 }
+
+module.exports = { saveChatHistory, generateHistorySummary, getHistorySummary, readAllChatHistory }

+ 9 - 5
nodejs/ef-compiler/Func/chat/ocr-chat.js

@@ -7,9 +7,11 @@
  * 运行时真实执行逻辑由 ActionParser + electronAPI.extractChatHistory + main-js/func/ocr-chat.js 实现。
  */
 
-export const tagName = 'ocr-chat';
+const electronAPI = require('../../node-api.js')
 
-export const schema = {
+const tagName = 'ocr-chat'
+
+const schema = {
   description: '根据图片识别对话内容,区分好友和自己的对话,输出JSON字符串格式的消息记录。',
   inputs: {
     // 按用户要求:抽象为 avatar1/avatar2,不绑定 friend/me 语义
@@ -36,9 +38,9 @@ export const schema = {
  * @param {string} params.myRgb - 我的对话框RGB颜色(格式:"(r,g,b)")
  * @returns {Promise<{success: boolean, messagesJson?: string, messages?: Array, error?: string}>}
  */
-export async function executeOcrChat({ device, avatar1, avatar2, folderPath, region = null, friendRgb = null, myRgb = null }) {
+async function executeOcrChat({ device, avatar1, avatar2, folderPath, region = null, friendRgb = null, myRgb = null }) {
   try {
-    if (!window.electronAPI || !window.electronAPI.extractChatHistory) {
+    if (!electronAPI.extractChatHistory) {
       return { 
         success: false, 
         error: 'extractChatHistory API 不可用' 
@@ -46,7 +48,7 @@ export async function executeOcrChat({ device, avatar1, avatar2, folderPath, reg
     }
 
     // 调用主进程的 extractChatHistory 函数
-    const result = await window.electronAPI.extractChatHistory(
+    const result = await electronAPI.extractChatHistory(
       device,
       avatar1,
       avatar2,
@@ -141,3 +143,5 @@ export async function executeOcrChat({ device, avatar1, avatar2, folderPath, reg
     };
   }
 }
+
+module.exports = { tagName, schema, executeOcrChat }

+ 9 - 5
nodejs/ef-compiler/Func/chat/read-last-message.js

@@ -5,9 +5,11 @@
  * 2. 从 history 文件夹读取(如果没有提供 inputData)- 从最新的聊天记录文件中读取
  */
 
-export const tagName = 'read-last-message';
+const electronAPI = require('../../node-api.js')
 
-export const schema = {
+const tagName = 'read-last-message'
+
+const schema = {
   description: '读取最后一条消息,包括消息内容和发送者角色。支持从变量或 history 文件夹读取。',
   inputs: {
     inputData: '输入数据(可选)- 可以是聊天记录文本或消息数组,如果不提供则从 history 文件夹读取',
@@ -92,7 +94,7 @@ function parseChatHistoryText(chatHistoryText) {
  * @param {string} params.senderVariable - 输出变量名(保存发送者角色)
  * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>}
  */
-export async function executeReadLastMessage({ folderPath, inputData, textVariable, senderVariable }) {
+async function executeReadLastMessage({ folderPath, inputData, textVariable, senderVariable }) {
   try {
     if (!textVariable && !senderVariable) {
       return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' };
@@ -272,11 +274,11 @@ export async function executeReadLastMessage({ folderPath, inputData, textVariab
       const sender = lastMessage.sender || lastMessage.role || '';
     } else {
       // 如果没有提供 inputData,从 history 文件夹读取
-      if (!window.electronAPI || !window.electronAPI.readLastMessage) {
+      if (!electronAPI.readLastMessage) {
         return { success: false, error: '读取最后一条消息 API 不可用' };
       }
 
-      const result = await window.electronAPI.readLastMessage(folderPath);
+      const result = await electronAPI.readLastMessage(folderPath);
 
       if (!result.success) {
         return { success: false, error: `读取最后一条消息失败: ${result.error}` };
@@ -298,3 +300,5 @@ export async function executeReadLastMessage({ folderPath, inputData, textVariab
     return { success: false, error: error.message || '读取最后一条消息失败' };
   }
 }
+
+module.exports = { tagName, schema, executeReadLastMessage }

+ 5 - 3
nodejs/ef-compiler/Func/chat/smart-chat-append.js

@@ -8,9 +8,9 @@
  * 输入和输出都是 JSON 字符串格式(参考 chat-history.txt 格式)。
  */
 
-export const tagName = 'smart-chat-append';
+const tagName = 'smart-chat-append'
 
-export const schema = {
+const schema = {
   description: '智能合并历史聊天记录和当前聊天记录,自动检测并去除连续重合部分后返回新的聊天记录字符串(JSON格式)。',
   inputs: {
     history: '历史聊天记录的 JSON 字符串',
@@ -122,7 +122,7 @@ function findOverlapLength(historyEntries, currentEntries) {
  * @param {string} params.current - 当前聊天记录的 JSON 字符串
  * @returns {Promise<{success: boolean, error?: string, result?: string}>}
  */
-export async function executeSmartChatAppend({ history, current }) {
+async function executeSmartChatAppend({ history, current }) {
   try {
     // 如果历史记录为空字符串,直接返回当前记录
     if (!history || history.trim() === '') {
@@ -182,3 +182,5 @@ export async function executeSmartChatAppend({ history, current }) {
     };
   }
 }
+
+module.exports = { tagName, schema, executeSmartChatAppend }

+ 26 - 138
nodejs/ef-compiler/Func/image-area-cropping.js

@@ -7,11 +7,11 @@
  * 语义:根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。
  */
 
-import ScrcpyConfig from '../../screenshot/scrcpy-config.js';
+const electronAPI = require('../node-api.js')
 
-export const tagName = 'image-area-cropping';
+const tagName = 'image-area-cropping'
 
-export const schema = {
+const schema = {
   description: '根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。',
   inputs: {
     area: '区域坐标(JSON字符串格式,包含 topLeft 和 bottomRight,或包含 x, y, width, height)',
@@ -33,15 +33,8 @@ export const schema = {
  * @param {string} params.device - 设备 ID/IP:Port(可选,用于获取最新截图)
  * @returns {Promise<{success: boolean, error?: string}>}
  */
-export async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
+async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
   try {
-    if (!window.electronAPI || !window.electronAPI.cropAndSaveImage) {
-      return { 
-        success: false, 
-        error: 'cropAndSaveImage API 不可用' 
-      };
-    }
-
     // 解析区域坐标
     let areaObj = area;
     if (typeof area === 'string') {
@@ -102,9 +95,7 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
     // 参考 screenshot.js 的实现方式
     let imageBase64 = null; // 声明 imageBase64 变量
     let screenshotPath;
-    // 根据配置确定文件扩展名
-    const screencapFormat = ScrcpyConfig['screencap-format'] || 'jpg';
-    const fileExtension = screencapFormat === 'jpeg' || screencapFormat === 'jpg' ? 'jpg' : 'png';
+    const fileExtension = 'jpg'
     
     if (folderPath.includes(':')) {
       // 绝对路径
@@ -114,42 +105,21 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
     }
     
-    // 如果有设备ID,先尝试从缓存获取截图,如果没有再调用 ADB 截图
-    if (device && window.electronAPI) {
-      try {
-        // 优先从主进程缓存获取截图(避免并发冲突)
-        let screenshotResult = null;
-        if (window.electronAPI.getCachedScreenshot) {
-          screenshotResult = await window.electronAPI.getCachedScreenshot(device);
-          if (screenshotResult && screenshotResult.success && screenshotResult.data) {
-            imageBase64 = screenshotResult.data;
-          }
-        }
-        
-        // 如果缓存不可用,调用 ADB 截图(参考 screenshot.js 的实现方式)
-        if (!imageBase64 && window.electronAPI.captureScreenshot) {
-          screenshotResult = await window.electronAPI.captureScreenshot(device, {
-            format: ScrcpyConfig['screencap-format'],
-            quality: ScrcpyConfig['screencap-quality'],
-            scale: ScrcpyConfig['screencap-scale']
-          });
-          
-          if (screenshotResult && screenshotResult.success && screenshotResult.data) {
-            imageBase64 = screenshotResult.data;
-          }
-        }
-        
-        // 如果有截图数据,保存到文件
-        if (imageBase64) {
-          await window.electronAPI.saveBase64Image(
-            imageBase64,
-            screenshotPath
-          );
-        }
-      } catch (error) {
-        // 截屏循环异常(参考 screenshot.js 的错误处理)
+    if (device && electronAPI.getCachedScreenshot) {
+      const screenshotResult = await electronAPI.getCachedScreenshot(device)
+      if (screenshotResult && screenshotResult.success && screenshotResult.data) {
+        imageBase64 = screenshotResult.data
+      }
+    }
+    if (!imageBase64 && electronAPI.captureScreenshot) {
+      const screenshotResult = await electronAPI.captureScreenshot(device)
+      if (screenshotResult && screenshotResult.success && screenshotResult.data) {
+        imageBase64 = screenshotResult.data
       }
     }
+    if (imageBase64 && electronAPI.saveBase64Image) {
+      await electronAPI.saveBase64Image(imageBase64, screenshotPath)
+    }
 
     // 处理保存路径(如果是相对路径,相对于工作流目录)
     let absoluteSavePath = savePath;
@@ -162,32 +132,12 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       }
     }
 
-    // 如果还没有通过ADB获取base64数据,则从文件读取
-    if (!imageBase64) {
-      try {
-        // 使用 Electron API 读取文件
-        if (window.electronAPI && window.electronAPI.readImageFileAsBase64) {
-          // 读取文件为 base64
-          const fileContent = await window.electronAPI.readImageFileAsBase64(screenshotPath);
-          if (!fileContent || !fileContent.success) {
-            return {
-              success: false,
-              error: `无法读取截图文件: ${fileContent?.error || '未知错误'}`
-            };
-          }
-          imageBase64 = fileContent.data;
-        } else {
-          return {
-            success: false,
-            error: 'readImageFileAsBase64 API 不可用'
-          };
-        }
-      } catch (error) {
-        return {
-          success: false,
-          error: `读取截图文件失败: ${error.message}`
-        };
+    if (!imageBase64 && electronAPI.readImageFileAsBase64) {
+      const fileContent = await electronAPI.readImageFileAsBase64(screenshotPath)
+      if (!fileContent || !fileContent.success) {
+        return { success: false, error: `无法读取截图文件: ${fileContent?.error || '未知错误'}` }
       }
+      imageBase64 = fileContent.data
     }
     
     // 验证 base64 数据是否有效(至少应该是几百字节)
@@ -198,71 +148,7 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
       };
     }
 
-    // 使用 Canvas API 裁剪图片
-    try {
-      // 创建 Image 对象
-      const img = new Image();
-      // 先尝试 JPEG,如果失败再尝试 PNG
-      let imageMimeType = 'image/jpeg';
-      let dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
-      
-      await new Promise((resolve, reject) => {
-        img.onload = () => {
-          resolve();
-        };
-        img.onerror = (error) => {
-          // 尝试 PNG 格式
-          imageMimeType = 'image/png';
-          dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
-          img.src = dataUrl;
-        };
-        img.src = dataUrl;
-      }).catch((error) => {
-        throw new Error(`无法加载图片数据,请检查截图文件是否有效`);
-      });
-
-      // 验证坐标是否在图片范围内
-      if (x < 0 || y < 0 || x + width > img.width || y + height > img.height) {
-        return {
-          success: false,
-          error: `裁剪区域超出图片范围。图片尺寸: ${img.width}x${img.height}, 裁剪区域: x=${x}, y=${y}, width=${width}, height=${height}`
-        };
-      }
-
-      // 创建 Canvas 并裁剪
-      const canvas = document.createElement('canvas');
-      canvas.width = width;
-      canvas.height = height;
-      const ctx = canvas.getContext('2d');
-      
-      // 绘制裁剪后的区域
-      ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
-      
-      // 转换为 base64(PNG 格式)
-      const croppedBase64 = canvas.toDataURL('image/png').split(',')[1]; // 去掉 data:image/png;base64, 前缀
-
-      // 调用主进程保存 base64 图片
-      const result = await window.electronAPI.saveBase64Image(
-        croppedBase64,
-        absoluteSavePath
-      );
-
-      if (!result.success) {
-        return {
-          success: false,
-          error: result.error || '保存图片失败'
-        };
-      }
-
-      return {
-        success: true
-      };
-    } catch (error) {
-      return {
-        success: false,
-        error: `Canvas裁剪失败: ${error.message}`
-      };
-    }
+    return { success: false, error: 'image-area-cropping 需使用 sharp/jimp 等 Node 图片库实现' }
   } catch (error) {
     return { 
       success: false, 
@@ -270,3 +156,5 @@ export async function executeImageAreaCropping({ area, savePath, folderPath, dev
     };
   }
 }
+
+module.exports = { tagName, schema, executeImageAreaCropping }

+ 9 - 5
nodejs/ef-compiler/Func/image-center-location.js

@@ -5,9 +5,11 @@
  * 注意:实际的 OpenCV 处理需要在 Node.js 主进程中实现
  */
 
-export const tagName = 'image-center-location';
+const electronAPI = require('../node-api.js')
 
-export const schema = {
+const tagName = 'image-center-location'
+
+const schema = {
   description: '在屏幕截图中查找模板图片的位置并返回中心点坐标(可用于定位/点击)。',
   inputs: {
     template: '模板图片路径(相对于工作流目录)',
@@ -28,9 +30,9 @@ export const schema = {
  * @param {string} params.folderPath - 工作流文件夹路径
  * @returns {Promise<{success: boolean, center?: Object, error?: string}>}
  */
-export async function executeImageCenterLocation({ device, template, folderPath }) {
+async function executeImageCenterLocation({ device, template, folderPath }) {
   try {
-    if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+    if (!electronAPI.matchImageAndGetCoordinate) {
       return { 
         success: false, 
         error: 'matchImageAndGetCoordinate API 不可用' 
@@ -51,7 +53,7 @@ export async function executeImageCenterLocation({ device, template, folderPath
       : `${folderPath}/resources/${template}`;
 
     // 调用主进程的图像匹配函数(会自动获取设备截图)
-    const result = await window.electronAPI.matchImageAndGetCoordinate(
+    const result = await electronAPI.matchImageAndGetCoordinate(
       device,
       templatePath
     );
@@ -75,3 +77,5 @@ export async function executeImageCenterLocation({ device, template, folderPath
     };
   }
 }
+
+module.exports = { tagName, schema, executeImageCenterLocation }

+ 8 - 5
nodejs/ef-compiler/Func/image-region-location.js

@@ -8,10 +8,11 @@
  * 当前项目里对应能力主要由 electronAPI.matchImageRegionLocation + main-js/func/image-center-location.js 实现承载。
  */
 
+const electronAPI = require('../node-api.js')
 
-export const tagName = 'image-region-location';
+const tagName = 'image-region-location'
 
-export const schema = {
+const schema = {
   description: '在完整截图中查找区域截图的位置,返回区域的四个顶点坐标(左上、右上、左下、右下)。',
   inputs: {
     screenshot: '完整截图路径(相对于工作流目录)',
@@ -34,9 +35,9 @@ export const schema = {
  * @param {string} params.folderPath - 工作流文件夹路径
  * @returns {Promise<{success: boolean, corners?: Object, error?: string}>}
  */
-export async function executeImageRegionLocation({ device, screenshot, region, folderPath }) {
+async function executeImageRegionLocation({ device, screenshot, region, folderPath }) {
   try {
-    if (!window.electronAPI || !window.electronAPI.matchImageRegionLocation) {
+    if (!electronAPI.matchImageRegionLocation) {
       return { 
         success: false, 
         error: 'matchImageRegionLocation API 不可用' 
@@ -62,7 +63,7 @@ export async function executeImageRegionLocation({ device, screenshot, region, f
 
     // 调用主进程的图像区域定位函数
     // 如果 screenshotPath 是 '__AUTO_SCREENSHOT__',主进程会自动获取截图
-    const result = await window.electronAPI.matchImageRegionLocation(
+    const result = await electronAPI.matchImageRegionLocation(
       screenshotPath,
       regionPath,
       device // 可选,用于获取设备分辨率进行缩放或自动获取截图
@@ -93,3 +94,5 @@ export async function executeImageRegionLocation({ device, screenshot, region, f
     };
   }
 }
+
+module.exports = { tagName, schema, executeImageRegionLocation }

+ 9 - 5
nodejs/ef-compiler/Func/read-txt.js

@@ -3,9 +3,11 @@
  * 支持从项目根目录读取文本文件内容
  */
 
-export const tagName = 'read-txt';
+const electronAPI = require('../node-api.js')
 
-export const schema = {
+const tagName = 'read-txt'
+
+const schema = {
   description: '读取根目录下的文本文件内容。',
   inputs: {
     filePath: '文件路径(相对于项目根目录,如 "config.txt" 或 "data/input.txt")',
@@ -23,13 +25,13 @@ export const schema = {
  * @param {string} params.folderPath - 工作流文件夹路径(用于构建绝对路径)
  * @returns {Promise<{success: boolean, error?: string, content?: string}>}
  */
-export async function executeReadTxt({ filePath, folderPath }) {
+async function executeReadTxt({ filePath, folderPath }) {
   try {
     if (!filePath) {
       return { success: false, error: 'read-txt 缺少 filePath 参数' };
     }
 
-    if (!window.electronAPI || !window.electronAPI.readTextFile) {
+    if (!electronAPI.readTextFile) {
       return { success: false, error: '读取文本文件 API 不可用' };
     }
 
@@ -64,7 +66,7 @@ export async function executeReadTxt({ filePath, folderPath }) {
     // 调用主进程的 readTextFile API
     // 主进程会将相对路径解析为相对于项目根目录的绝对路径
     // 如果文件不存在,主进程会返回空字符串
-    const result = await window.electronAPI.readTextFile(absoluteFilePath);
+    const result = electronAPI.readTextFile(absoluteFilePath);
 
     // 即使文件不存在,也返回成功(内容为空字符串)
     if (!result.success) {
@@ -80,3 +82,5 @@ export async function executeReadTxt({ filePath, folderPath }) {
     return { success: false, error: error.message || '读取文本文件失败' };
   }
 }
+
+module.exports = { tagName, schema, executeReadTxt }

+ 9 - 5
nodejs/ef-compiler/Func/save-txt.js

@@ -3,9 +3,11 @@
  * 支持将字符串内容保存到根目录下的文本文件
  */
 
-export const tagName = 'save-txt';
+const electronAPI = require('../node-api.js')
 
-export const schema = {
+const tagName = 'save-txt'
+
+const schema = {
   description: '将字符串内容保存到根目录下的文本文件。',
   inputs: {
     filePath: '文件路径(相对于项目根目录,如 "output.txt" 或 "data/output.txt")',
@@ -24,7 +26,7 @@ export const schema = {
  * @param {string} params.folderPath - 工作流文件夹路径(用于构建绝对路径)
  * @returns {Promise<{success: boolean, error?: string}>}
  */
-export async function executeSaveTxt({ filePath, content, folderPath }) {
+async function executeSaveTxt({ filePath, content, folderPath }) {
   try {
     if (!filePath) {
       return { success: false, error: 'save-txt 缺少 filePath 参数' };
@@ -34,7 +36,7 @@ export async function executeSaveTxt({ filePath, content, folderPath }) {
       return { success: false, error: 'save-txt 缺少 content 参数' };
     }
 
-    if (!window.electronAPI || !window.electronAPI.writeTextFile) {
+    if (!electronAPI.writeTextFile) {
       return { success: false, error: '写入文本文件 API 不可用' };
     }
 
@@ -67,7 +69,7 @@ export async function executeSaveTxt({ filePath, content, folderPath }) {
 
     // 调用主进程的 writeTextFile API
     // 主进程会将相对路径解析为相对于项目根目录的绝对路径
-    const result = await window.electronAPI.writeTextFile(absoluteFilePath, contentString);
+    const result = electronAPI.writeTextFile(absoluteFilePath, contentString);
 
     if (!result.success) {
       return { success: false, error: `保存文件失败: ${result.error}` };
@@ -78,3 +80,5 @@ export async function executeSaveTxt({ filePath, content, folderPath }) {
     return { success: false, error: error.message || '保存文本文件失败' };
   }
 }
+
+module.exports = { tagName, schema, executeSaveTxt }

+ 5 - 3
nodejs/ef-compiler/Func/string-reg-location.js

@@ -8,9 +8,9 @@
  * 当前项目里对应能力主要由 electronAPI.findTextAndGetCoordinate / ActionParser 的 string-press 等实现承载。
  */
 
-export const tagName = 'string-reg-location';
+const tagName = 'string-reg-location'
 
-export const schema = {
+const schema = {
   description: '在屏幕截图中查找目标文字并返回位置坐标(可用于 click/press)。',
   inputs: {
     text: '要匹配的目标文字/正则(建议让用户提供稳定的关键字)',
@@ -19,5 +19,7 @@ export const schema = {
   outputs: {
     variable: '坐标(x,y 或 position 对象)',
   },
-};
+}
+
+module.exports = { tagName, schema }
 

+ 71 - 76
nodejs/ef-compiler/ef-compiler.js

@@ -25,9 +25,8 @@ async function logMessage(message, folderPath = null) {
   // 注意:不输出到 console,只写入日志文件
   try {
     const targetFolderPath = folderPath || currentWorkflowFolderPath;
-    if (targetFolderPath && window.electronAPI && window.electronAPI.appendLog) {
-      // 异步写入,不等待完成
-      window.electronAPI.appendLog(targetFolderPath, message).catch(err => {
+    if (targetFolderPath && electronAPI.appendLog) {
+      electronAPI.appendLog(targetFolderPath, message).catch(err => {
         // 静默失败,不影响主流程
       });
     }
@@ -58,24 +57,18 @@ async function logOutVars(action, variableContext, folderPath = null) {
   // await logMessage(logMsg, folderPath);
 }
 
-// 导入聊天历史记录管理模块
-import { generateHistorySummary, getHistorySummary } from './func/chat/chat-history.js';
-// 导入 ocr-chat 执行函数
-import { executeOcrChat } from './func/chat/ocr-chat.js';
-// 导入 image-region-location 执行函数
-import { executeImageRegionLocation } from './func/image-region-location.js';
-// 导入 image-center-location 执行函数
-import { executeImageCenterLocation } from './func/image-center-location.js';
-// 导入 image-area-cropping 执行函数
-import { executeImageAreaCropping } from './func/image-area-cropping.js';
-// 导入 read-last-message 执行函数
-import { executeReadLastMessage } from './func/chat/read-last-message.js';
-// 导入 read-txt 执行函数
-import { executeReadTxt } from './func/read-txt.js';
-// 导入 save-txt 执行函数
-import { executeSaveTxt } from './func/save-txt.js';
-// 导入 smart-chat-append 执行函数
-import { executeSmartChatAppend } from './func/chat/smart-chat-append.js';
+const path = require('path')
+const funcDir = path.join(__dirname, 'Func')
+const { generateHistorySummary, getHistorySummary } = require(path.join(funcDir, 'chat', 'chat-history.js'))
+const { executeOcrChat } = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
+const { executeImageRegionLocation } = require(path.join(funcDir, 'image-region-location.js'))
+const { executeImageCenterLocation } = require(path.join(funcDir, 'image-center-location.js'))
+const { executeImageAreaCropping } = require(path.join(funcDir, 'image-area-cropping.js'))
+const { executeReadLastMessage } = require(path.join(funcDir, 'chat', 'read-last-message.js'))
+const { executeReadTxt } = require(path.join(funcDir, 'read-txt.js'))
+const { executeSaveTxt } = require(path.join(funcDir, 'save-txt.js'))
+const { executeSmartChatAppend } = require(path.join(funcDir, 'chat', 'smart-chat-append.js'))
+const electronAPI = require('./node-api.js')
 
 /**
  * 解析时间字符串(格式:2026/1/13 02:09)
@@ -713,7 +706,7 @@ function parseValue(str) {
  * @param {Object} workflow - 工作流配置对象
  * @returns {Object} 解析后的工作流
  */
-export function parseWorkflow(workflow) {
+function parseWorkflow(workflow) {
   if (!workflow || typeof workflow !== 'object') {
     return null;
   }
@@ -805,7 +798,7 @@ export function parseWorkflow(workflow) {
  * @param {Array} actions - 操作数组
  * @returns {Array} 解析后的操作列表
  */
-export function parseActions(actions) {
+function parseActions(actions) {
   if (!Array.isArray(actions)) {
     return [];
   }
@@ -1294,7 +1287,7 @@ function getActionName(action) {
  * @param {number} height - 设备高度
  * @returns {Object} 包含起始和结束坐标的对象 {x1, y1, x2, y2}
  */
-export function calculateSwipeCoordinates(direction, width, height) {
+function calculateSwipeCoordinates(direction, width, height) {
   // 滑动距离为屏幕的 70%,起始和结束位置各留 15% 的边距
   const margin = 0.15;
   const swipeDistance = 0.7;
@@ -1346,7 +1339,7 @@ export function calculateSwipeCoordinates(direction, width, height) {
  * @param {Object} resolution - 设备分辨率 {width, height}
  * @returns {Promise<Object>} 执行结果 {success, error?, result?}
  */
-export async function executeAction(action, device, folderPath, resolution) {
+async function executeAction(action, device, folderPath, resolution) {
   try {
     // 检查条件
     if (action.condition && !evaluateCondition(action.condition)) {
@@ -1383,18 +1376,18 @@ export async function executeAction(action, device, folderPath, resolution) {
             // 如果设置了clear,先清空输入框
             if (action.clear) {
               for (let i = 0; i < 200; i++) {
-                const clearResult = await window.electronAPI.sendKeyEvent(device, '67');
+                const clearResult = await electronAPI.sendKeyEvent(device, '67');
                 if (!clearResult.success) break;
                 await new Promise(resolve => setTimeout(resolve, 10));
               }
               await new Promise(resolve => setTimeout(resolve, 200));
             }
 
-            if (!window.electronAPI || !window.electronAPI.sendText) {
+            if (!electronAPI || !electronAPI.sendText) {
               return { success: false, error: '输入 API 不可用' };
             }
 
-            const textResult = await window.electronAPI.sendText(device, String(inputValue));
+            const textResult = await electronAPI.sendText(device, String(inputValue));
             if (!textResult.success) {
               return { success: false, error: `输入失败: ${textResult.error}` };
             }
@@ -1457,11 +1450,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' };
             }
 
-            if (!window.electronAPI || !window.electronAPI.sendTap) {
+            if (!electronAPI || !electronAPI.sendTap) {
               return { success: false, error: '点击 API 不可用' };
             }
 
-            const tapResult = await window.electronAPI.sendTap(device, position.x, position.y);
+            const tapResult = await electronAPI.sendTap(device, position.x, position.y);
             if (!tapResult.success) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
@@ -1493,11 +1486,11 @@ export async function executeAction(action, device, folderPath, resolution) {
                 ? imagePath
                 : `${folderPath}/resources/${imagePath}`;
 
-              if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+              if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
                 return { success: false, error: '图像匹配 API 不可用' };
               }
 
-              const matchResult = await window.electronAPI.matchImageAndGetCoordinate(device, fullPath);
+              const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
               if (!matchResult.success) {
                 return { success: false, error: `图像匹配失败: ${matchResult.error}` };
               }
@@ -1515,11 +1508,11 @@ export async function executeAction(action, device, folderPath, resolution) {
                 return { success: false, error: 'locate 操作(text)缺少文字内容' };
               }
 
-              if (!window.electronAPI || !window.electronAPI.findTextAndGetCoordinate) {
+              if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
                 return { success: false, error: '文字识别 API 不可用' };
               }
 
-              const matchResult = await window.electronAPI.findTextAndGetCoordinate(device, targetText);
+              const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
               if (!matchResult.success) {
                 return { success: false, error: `文字识别失败: ${matchResult.error}` };
               }
@@ -1590,11 +1583,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               y2 = coords.y2;
             }
 
-            if (!window.electronAPI || !window.electronAPI.sendSwipe) {
+            if (!electronAPI || !electronAPI.sendSwipe) {
               return { success: false, error: '滑动 API 不可用' };
             }
 
-            const swipeResult = await window.electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
+            const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
             if (!swipeResult.success) {
               return { success: false, error: `滑动失败: ${swipeResult.error}` };
             }
@@ -1616,11 +1609,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: 'scroll 操作缺少方向参数' };
             }
 
-            if (!window.electronAPI || !window.electronAPI.sendScroll) {
+            if (!electronAPI || !electronAPI.sendScroll) {
               return { success: false, error: '滚动 API 不可用' };
             }
 
-            const scrollResult = await window.electronAPI.sendScroll(
+            const scrollResult = await electronAPI.sendScroll(
               device,
               direction,
               resolution.width,
@@ -1655,11 +1648,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               keyCode = '4';
             }
 
-            if (!window.electronAPI || !window.electronAPI.sendSystemKey) {
+            if (!electronAPI || !electronAPI.sendSystemKey) {
               return { success: false, error: '系统按键 API 不可用' };
             }
 
-            const keyResult = await window.electronAPI.sendSystemKey(device, String(keyCode));
+            const keyResult = await electronAPI.sendSystemKey(device, String(keyCode));
             if (!keyResult.success) {
               return { success: false, error: `按键失败: ${keyResult.error}` };
             }
@@ -1685,11 +1678,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               ? imagePath
               : `${folderPath}/${imagePath}`;
 
-            if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+            if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
               return { success: false, error: '图像匹配 API 不可用' };
             }
 
-            const matchResult = await window.electronAPI.matchImageAndGetCoordinate(device, fullPath);
+            const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
             if (!matchResult.success) {
               return { success: false, error: `图像匹配失败: ${matchResult.error}` };
             }
@@ -1697,11 +1690,11 @@ export async function executeAction(action, device, folderPath, resolution) {
             const { clickPosition } = matchResult;
             const { x, y } = clickPosition;
 
-            if (!window.electronAPI || !window.electronAPI.sendTap) {
+            if (!electronAPI || !electronAPI.sendTap) {
               return { success: false, error: '点击 API 不可用' };
             }
 
-            const tapResult = await window.electronAPI.sendTap(device, x, y);
+            const tapResult = await electronAPI.sendTap(device, x, y);
             if (!tapResult.success) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
@@ -1723,11 +1716,11 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: 'string-press 操作缺少文字内容' };
             }
 
-            if (!window.electronAPI || !window.electronAPI.findTextAndGetCoordinate) {
+            if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
               return { success: false, error: '文字识别 API 不可用' };
             }
 
-            const matchResult = await window.electronAPI.findTextAndGetCoordinate(device, targetText);
+            const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
             if (!matchResult.success) {
               return { success: false, error: `文字识别失败: ${matchResult.error}` };
             }
@@ -1735,11 +1728,11 @@ export async function executeAction(action, device, folderPath, resolution) {
             const { clickPosition } = matchResult;
             const { x, y } = clickPosition;
 
-            if (!window.electronAPI || !window.electronAPI.sendTap) {
+            if (!electronAPI || !electronAPI.sendTap) {
               return { success: false, error: '点击 API 不可用' };
             }
 
-            const tapResult = await window.electronAPI.sendTap(device, x, y);
+            const tapResult = await electronAPI.sendTap(device, x, y);
             if (!tapResult.success) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
@@ -1762,21 +1755,21 @@ export async function executeAction(action, device, folderPath, resolution) {
             ? action.target 
             : `${folderPath}/${action.target}`;
           
-          if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+          if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
             return { success: false, error: '图像匹配 API 不可用' };
           }
 
-          const matchResult = await window.electronAPI.matchImageAndGetCoordinate(device, imagePath);
+          const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
           if (!matchResult.success) {
             return { success: false, error: `图像匹配失败: ${matchResult.error}` };
           }
           position = matchResult.clickPosition;
         } else if (method === 'text') {
-          if (!window.electronAPI || !window.electronAPI.findTextAndGetCoordinate) {
+          if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
             return { success: false, error: '文字识别 API 不可用' };
           }
 
-          const matchResult = await window.electronAPI.findTextAndGetCoordinate(device, action.target);
+          const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
           if (!matchResult.success) {
             return { success: false, error: `文字识别失败: ${matchResult.error}` };
           }
@@ -1807,21 +1800,21 @@ export async function executeAction(action, device, folderPath, resolution) {
             ? action.target
             : `${folderPath}/${action.target}`;
           
-          if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+          if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
             return { success: false, error: '图像匹配 API 不可用' };
           }
 
-          const matchResult = await window.electronAPI.matchImageAndGetCoordinate(device, imagePath);
+          const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
           if (!matchResult.success) {
             return { success: false, error: `图像匹配失败: ${matchResult.error}` };
           }
           position = matchResult.clickPosition;
         } else if (method === 'text') {
-          if (!window.electronAPI || !window.electronAPI.findTextAndGetCoordinate) {
+          if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
             return { success: false, error: '文字识别 API 不可用' };
           }
 
-          const matchResult = await window.electronAPI.findTextAndGetCoordinate(device, action.target);
+          const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
           if (!matchResult.success) {
             return { success: false, error: `文字识别失败: ${matchResult.error}` };
           }
@@ -1832,11 +1825,11 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: '无法获取点击位置' };
         }
 
-        if (!window.electronAPI || !window.electronAPI.sendTap) {
+        if (!electronAPI || !electronAPI.sendTap) {
           return { success: false, error: '点击 API 不可用' };
         }
 
-        const tapResult = await window.electronAPI.sendTap(device, position.x, position.y);
+        const tapResult = await electronAPI.sendTap(device, position.x, position.y);
         if (!tapResult.success) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
         }
@@ -1849,11 +1842,11 @@ export async function executeAction(action, device, folderPath, resolution) {
         // resources 作为根目录
         const imagePath = `${folderPath}/resources/${action.value}`;
         
-        if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+        if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
           return { success: false, error: '图像匹配 API 不可用' };
         }
 
-        const matchResult = await window.electronAPI.matchImageAndGetCoordinate(device, imagePath);
+        const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
         
         if (!matchResult.success) {
           return { success: false, error: `图像匹配失败: ${matchResult.error}` };
@@ -1862,11 +1855,11 @@ export async function executeAction(action, device, folderPath, resolution) {
         const { clickPosition } = matchResult;
         const { x, y } = clickPosition;
 
-        if (!window.electronAPI || !window.electronAPI.sendTap) {
+        if (!electronAPI || !electronAPI.sendTap) {
           return { success: false, error: '点击 API 不可用' };
         }
 
-        const tapResult = await window.electronAPI.sendTap(device, x, y);
+        const tapResult = await electronAPI.sendTap(device, x, y);
         
         if (!tapResult.success) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
@@ -1903,7 +1896,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           // 暂时直接使用 sendText
         }
 
-        if (!window.electronAPI || !window.electronAPI.sendText) {
+        if (!electronAPI || !electronAPI.sendText) {
           return { success: false, error: '输入 API 不可用' };
         }
 
@@ -1912,7 +1905,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           // 发送退格键清空输入框(假设最多200个字符)
           // 使用Android的KEYCODE_DEL,值为67
           for (let i = 0; i < 200; i++) {
-            const clearResult = await window.electronAPI.sendKeyEvent(device, '67');
+            const clearResult = await electronAPI.sendKeyEvent(device, '67');
             if (!clearResult.success) {
               break;
             }
@@ -1922,7 +1915,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           await new Promise(resolve => setTimeout(resolve, 200));
         }
 
-        const textResult = await window.electronAPI.sendText(device, inputValue);
+        const textResult = await electronAPI.sendText(device, inputValue);
         
         if (!textResult.success) {
           return { success: false, error: `输入失败: ${textResult.error}` };
@@ -1942,7 +1935,7 @@ export async function executeAction(action, device, folderPath, resolution) {
 
       case 'ocr': {
         // OCR识别
-        if (!window.electronAPI || !window.electronAPI.ocrLastMessage) {
+        if (!electronAPI || !electronAPI.ocrLastMessage) {
           return { success: false, error: 'OCR API 不可用' };
         }
 
@@ -1963,7 +1956,7 @@ export async function executeAction(action, device, folderPath, resolution) {
         }
 
         // 调用OCR API,传递工作流文件夹路径
-        const ocrResult = await window.electronAPI.ocrLastMessage(device, method, avatarPath, area, folderPath);
+        const ocrResult = await electronAPI.ocrLastMessage(device, method, avatarPath, area, folderPath);
         
         if (!ocrResult.success) {
           return { success: false, error: `OCR识别失败: ${ocrResult.error}` };
@@ -3050,7 +3043,7 @@ export async function executeAction(action, device, folderPath, resolution) {
 
       case 'swipe': {
         // 滑动操作
-        if (!window.electronAPI || !window.electronAPI.sendSwipe) {
+        if (!electronAPI || !electronAPI.sendSwipe) {
           return { success: false, error: '滑动 API 不可用' };
         }
 
@@ -3060,7 +3053,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           resolution.height
         );
 
-        const swipeResult = await window.electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
+        const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
         
         if (!swipeResult.success) {
           return { success: false, error: `滑动失败: ${swipeResult.error}` };
@@ -3072,11 +3065,11 @@ export async function executeAction(action, device, folderPath, resolution) {
 
       case 'string-press': {
         // 向后兼容:文字识别并点击
-        if (!window.electronAPI || !window.electronAPI.findTextAndGetCoordinate) {
+        if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
           return { success: false, error: '文字识别 API 不可用' };
         }
 
-        const matchResult = await window.electronAPI.findTextAndGetCoordinate(device, action.value);
+        const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.value);
         
         if (!matchResult.success) {
           return { success: false, error: `文字识别失败: ${matchResult.error}` };
@@ -3085,11 +3078,11 @@ export async function executeAction(action, device, folderPath, resolution) {
         const { clickPosition } = matchResult;
         const { x, y } = clickPosition;
 
-        if (!window.electronAPI || !window.electronAPI.sendTap) {
+        if (!electronAPI || !electronAPI.sendTap) {
           return { success: false, error: '点击 API 不可用' };
         }
 
-        const tapResult = await window.electronAPI.sendTap(device, x, y);
+        const tapResult = await electronAPI.sendTap(device, x, y);
         
         if (!tapResult.success) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
@@ -3101,11 +3094,11 @@ export async function executeAction(action, device, folderPath, resolution) {
 
       case 'scroll': {
         // 滚动操作(小幅度滚动)
-        if (!window.electronAPI || !window.electronAPI.sendScroll) {
+        if (!electronAPI || !electronAPI.sendScroll) {
           return { success: false, error: '滚动 API 不可用' };
         }
 
-        const scrollResult = await window.electronAPI.sendScroll(
+        const scrollResult = await electronAPI.sendScroll(
           device,
           action.value,
           resolution.width,
@@ -3155,7 +3148,7 @@ export async function executeAction(action, device, folderPath, resolution) {
  * @param {number} depth - 嵌套深度(用于递归)
  * @returns {Promise<Object>} 执行结果 {success, error?, completedSteps}
  */
-export async function executeActionSequence(
+async function executeActionSequence(
   actions,
   device,
   folderPath,
@@ -3484,3 +3477,5 @@ export async function executeActionSequence(
   }
   return { success: true, completedSteps };
 }
+
+module.exports = { parseWorkflow, parseActions, calculateSwipeCoordinates, executeAction, executeActionSequence }

+ 180 - 0
nodejs/ef-compiler/node-api.js

@@ -0,0 +1,180 @@
+/**
+ * Node.js 环境下的 API 桥接,替代 window.electronAPI
+ */
+
+const path = require('path')
+const fs = require('fs')
+const os = require('os')
+const { spawnSync } = require('child_process')
+
+const projectRoot = path.resolve(__dirname, '..', '..')
+const adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
+const imageMatchScriptPath = path.join(projectRoot, 'python', 'scripts', 'image-match.py')
+
+const config = require(path.join(projectRoot, 'configs', 'config.js'))
+
+function runAdb(action, args = [], deviceId = '') {
+  const result = spawnSync('node', [adbInteractPath, action, ...args, deviceId], {
+    encoding: 'utf-8',
+    timeout: 10000
+  })
+  return { success: result.status === 0, error: result.stderr }
+}
+
+async function sendTap(device, x, y) {
+  const r = runAdb('tap', [String(x), String(y)], device)
+  return r
+}
+
+async function sendSwipe(device, x1, y1, x2, y2, duration) {
+  const r = runAdb('swipe-coords', [String(x1), String(y1), String(x2), String(y2), String(duration || 300)], device)
+  return r
+}
+
+async function sendKeyEvent(device, keyCode) {
+  const r = runAdb('keyevent', [String(keyCode)], device)
+  return r
+}
+
+async function sendText(device, text) {
+  const r = runAdb('text', [String(text)], device)
+  return r
+}
+
+async function matchImageAndGetCoordinate(device, imagePath) {
+  const templatePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(projectRoot, imagePath)
+  if (!fs.existsSync(templatePath)) {
+    return { success: false, error: `模板图片不存在: ${templatePath}` }
+  }
+
+  const ts = Date.now()
+  const screenshotPath = path.join(os.tmpdir(), `ef-screenshot-${ts}.png`)
+  const templateCopyPath = path.join(os.tmpdir(), `ef-template-${ts}.png`)
+
+  try {
+    fs.copyFileSync(templatePath, templateCopyPath)
+  } catch (e) {
+    return { success: false, error: `复制模板失败: ${e.message}` }
+  }
+
+  const venvPython = path.join(projectRoot, 'python', 'env', 'Scripts', 'python.exe')
+  const hasVenv = fs.existsSync(venvPython)
+  const pythonPath = hasVenv
+    ? venvPython
+    : (config.pythonPath?.path ? path.join(config.pythonPath.path, 'python.exe') : 'python')
+
+  const adbPathRel = config.adbPath?.path || 'lib/scrcpy-adb/adb.exe'
+  const adbPath = path.isAbsolute(adbPathRel) ? adbPathRel : path.join(projectRoot, adbPathRel)
+
+  const screenshotPathNorm = screenshotPath.replace(/\\/g, '/')
+  const templateCopyPathNorm = templateCopyPath.replace(/\\/g, '/')
+
+  const matchResult = spawnSync(pythonPath, [imageMatchScriptPath, '--adb', adbPath, '--device', device, '--screenshot', screenshotPathNorm, '--template', templateCopyPathNorm], {
+    encoding: 'utf-8',
+    timeout: 20000,
+    env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
+    cwd: projectRoot
+  })
+
+  try { fs.unlinkSync(screenshotPath) } catch (_) {}
+  try { fs.unlinkSync(templateCopyPath) } catch (_) {}
+
+  if (matchResult.status !== 0) {
+    const errMsg = (matchResult.stderr || matchResult.stdout || '').trim()
+    return { success: false, error: errMsg || '图像匹配失败' }
+  }
+
+  let out
+  try {
+    out = JSON.parse(matchResult.stdout.trim())
+  } catch (e) {
+    return { success: false, error: `解析匹配结果失败: ${matchResult.stdout}` }
+  }
+
+  if (!out.success) {
+    return { success: false, error: out.error || '未找到匹配' }
+  }
+
+  return {
+    success: true,
+    coordinate: { x: out.x, y: out.y, width: out.width, height: out.height },
+    clickPosition: { x: out.center_x, y: out.center_y }
+  }
+}
+
+async function findTextAndGetCoordinate(device, targetText) {
+  return { success: false, error: 'findTextAndGetCoordinate 需在主进程实现' }
+}
+
+async function appendLog(folderPath, message) {
+  const logPath = path.join(folderPath, 'log.txt')
+  fs.appendFileSync(logPath, message + '\n')
+  return Promise.resolve()
+}
+
+function readTextFile(filePath) {
+  const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
+  const content = fs.readFileSync(fullPath, 'utf8')
+  return { success: true, content }
+}
+
+function writeTextFile(filePath, content) {
+  const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
+  const dir = path.dirname(fullPath)
+  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
+  fs.writeFileSync(fullPath, content)
+  return { success: true }
+}
+
+async function stub(name) {
+  return { success: false, error: `${name} 需在主进程实现` }
+}
+
+const nodeApi = {
+  sendTap,
+  sendSwipe,
+  sendKeyEvent,
+  sendText,
+  matchImageAndGetCoordinate,
+  findTextAndGetCoordinate,
+  appendLog,
+  readTextFile,
+  writeTextFile,
+  saveChatHistory: () => stub('saveChatHistory'),
+  readChatHistory: () => stub('readChatHistory'),
+  readAllChatHistory: () => stub('readAllChatHistory'),
+  saveChatHistorySummary: () => stub('saveChatHistorySummary'),
+  getChatHistorySummary: () => stub('getChatHistorySummary'),
+  saveChatHistoryTxt: () => stub('saveChatHistoryTxt'),
+  extractChatHistory: () => stub('extractChatHistory'),
+  readLastMessage: () => stub('readLastMessage'),
+  ocrLastMessage: () => stub('ocrLastMessage'),
+  getCachedScreenshot: () => stub('getCachedScreenshot'),
+  captureScreenshot: () => stub('captureScreenshot'),
+  async readImageFileAsBase64(filePath) {
+    try {
+      const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
+      const buf = fs.readFileSync(fullPath)
+      const data = buf.toString('base64')
+      return { success: true, data }
+    } catch (e) {
+      return { success: false, error: e.message }
+    }
+  },
+  matchImageRegionLocation: () => stub('matchImageRegionLocation'),
+  cropAndSaveImage: () => stub('cropAndSaveImage'),
+  async saveBase64Image(base64, filePath) {
+    try {
+      const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath)
+      const dir = path.dirname(fullPath)
+      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
+      const buf = Buffer.from(base64, 'base64')
+      fs.writeFileSync(fullPath, buf)
+      return { success: true }
+    } catch (e) {
+      return { success: false, error: e.message }
+    }
+  }
+}
+
+module.exports = nodeApi

+ 47 - 10
nodejs/run-process.js

@@ -1,16 +1,53 @@
+#!/usr/bin/env node
+/**
+ * run-process.js
+ * 接收两个参数:ip 数组 (JSON)、脚本名
+ * 异步根据每个 ip 执行脚本
+ *
+ * 调用示例:node run-process.js '["192.168.2.5","192.168.2.6"]' 'RedNoteAIThumbsUp'
+ */
+
 const path = require('path')
 const fs = require('fs')
-import adbConnect from './adb/adb-connect.js'
-import efCompile from './ef-compile.js'
+const { parseWorkflow, executeActionSequence } = require('./ef-compiler/ef-compiler.js')
+
+const ipListJson = process.argv[2]
+const scriptName = process.argv[3]
 
-let ipList = []
+const ipList = JSON.parse(ipListJson)
+let shouldStop = false
 
-for (let ip of ipList) {
-    let result = await adbConnect(ip, '5555')
+const folderPath = path.join(path.resolve(__dirname, '..', 'static'), 'process', scriptName)
+const { actions } = parseWorkflow(JSON.parse(fs.readFileSync(path.join(folderPath, 'process.json'), 'utf8')))
+const resolution = { width: 1080, height: 1920 }
 
-    if (result.exitCode === 0) {
-        console.log(`${ip} 连接成功`)
-    } else {
-        console.log(`${ip} 连接失败`)
+/** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
+async function start() {
+  let failedIp = null
+  const runOne = async (ip) => {
+    if (shouldStop) return { ip, success: false, stopped: true }
+    const result = await executeActionSequence(actions, `${ip}:5555`, folderPath, resolution, 1000, null, () => shouldStop)
+    if (!result.success) {
+      if (!failedIp) { failedIp = ip; shouldStop = true }
     }
-}
+    return { ip, success: result.success }
+  }
+
+  const results = await Promise.all(ipList.map(ip => runOne(ip)))
+
+  const output = failedIp
+    ? { success: false, failedIp, results }
+    : { success: true, results }
+  process.stdout.write(JSON.stringify(output) + '\n')
+  process.exit(failedIp ? 1 : 0)
+}
+
+/** 停止执行(SIGTERM/SIGINT 时调用) */
+function stop() {
+  shouldStop = true
+}
+
+process.on('SIGTERM', () => { stop(); process.exit(130) })
+process.on('SIGINT', () => { stop(); process.exit(130) })
+
+start()

+ 1 - 0
python/environment.txt

@@ -7,6 +7,7 @@ opencv-contrib-python==4.13.0.90
 opencv-python==4.13.0.90
 opencv-python-headless==4.13.0.90
 packaging==26.0
+pillow==12.1.1
 protobuf==6.33.4
 pyclipper==1.4.0
 pyreadline3==3.5.4

BIN
python/opencv/cv2/__pycache__/__init__.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/__config__.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/__init__.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/_distributor_init.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/_expired_attrs_2_0.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/_globals.cpython-314.pyc


BIN
python/opencv/numpy/__pycache__/version.cpython-314.pyc


BIN
python/opencv/numpy/_core/__pycache__/__init__.cpython-314.pyc


BIN
python/opencv/numpy/_core/__pycache__/multiarray.cpython-314.pyc


BIN
python/opencv/numpy/_core/__pycache__/overrides.cpython-314.pyc


BIN
python/opencv/numpy/_utils/__pycache__/__init__.cpython-314.pyc


BIN
python/opencv/numpy/_utils/__pycache__/_convertions.cpython-314.pyc


BIN
python/opencv/numpy/_utils/__pycache__/_inspect.cpython-314.pyc


+ 167 - 0
python/scripts/image-match.py

@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+模板匹配:在截图中查找模板图片的位置
+用法1: python image-match.py <screenshot_path> <template_path> [threshold]
+用法2: python image-match.py --adb <adb_path> --device <device_id> --screenshot <out_path> --template <template_path> [--threshold 0.8]
+      用法2 会在 Python 内执行 adb 截图,避免 Node 处理二进制数据导致的兼容性问题
+输出: JSON 到 stdout
+"""
+
+import sys
+import os
+import json
+import subprocess
+
+try:
+    import cv2
+    import numpy as np
+except ImportError as e:
+    print(json.dumps({"success": False, "error": f"OpenCV 导入失败: {e}。请安装: pip install opencv-python numpy"}))
+    sys.exit(1)
+
+try:
+    from PIL import Image as PILImage
+    HAS_PIL = True
+except ImportError:
+    HAS_PIL = False
+
+def run_adb_screencap(adb_path, device, output_path):
+    """在 Python 内执行 adb 截图,直接处理二进制流"""
+    # Windows 下子进程需要可执行路径,正斜杠也可用
+    args = [adb_path.replace('/', os.sep), '-s', device, 'exec-out', 'screencap', '-p']
+    try:
+        result = subprocess.run(args, capture_output=True, timeout=15)
+        if result.returncode != 0:
+            return False, (result.stderr or result.stdout or b'').decode('utf-8', errors='replace')
+        data = result.stdout
+        if not data or len(data) < 100:
+            return False, "截图数据为空"
+        # 注意:不要对 PNG 数据做 \r\n 替换,会破坏 IDAT 压缩块导致无法解析
+        out_dir = os.path.dirname(output_path)
+        if out_dir:
+            os.makedirs(out_dir, exist_ok=True)
+        with open(output_path, 'wb') as f:
+            f.write(data)
+        return True, output_path
+    except subprocess.TimeoutExpired:
+        return False, "截图超时"
+    except Exception as e:
+        return False, str(e)
+
+def load_image(path):
+    """从文件路径加载图片,兼容 OpenCV 无法直接读取的 PNG(如部分 Android 截图)"""
+    if not os.path.exists(path):
+        return None
+    with open(path, 'rb') as f:
+        data = np.frombuffer(f.read(), dtype=np.uint8)
+    img = cv2.imdecode(data, cv2.IMREAD_COLOR)
+    if img is not None:
+        return img
+    img = cv2.imread(path)
+    if img is not None:
+        return img
+    if HAS_PIL:
+        try:
+            pil_img = PILImage.open(path).convert('RGB')
+            img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
+            return img
+        except Exception:
+            pass
+    return None
+
+def main():
+    screenshot_path = None
+    template_path = None
+    threshold = 0.8
+    adb_path = None
+    device = None
+
+    if len(sys.argv) >= 2 and sys.argv[1] == '--adb':
+        # 用法2:--adb --device --screenshot --template
+        i = 1
+        while i < len(sys.argv):
+            if sys.argv[i] == '--adb' and i + 1 < len(sys.argv):
+                adb_path = sys.argv[i + 1]
+                i += 2
+            elif sys.argv[i] == '--device' and i + 1 < len(sys.argv):
+                device = sys.argv[i + 1]
+                i += 2
+            elif sys.argv[i] == '--screenshot' and i + 1 < len(sys.argv):
+                screenshot_path = sys.argv[i + 1]
+                i += 2
+            elif sys.argv[i] == '--template' and i + 1 < len(sys.argv):
+                template_path = sys.argv[i + 1]
+                i += 2
+            elif sys.argv[i] == '--threshold' and i + 1 < len(sys.argv):
+                threshold = float(sys.argv[i + 1])
+                i += 2
+            else:
+                i += 1
+        if adb_path and device and screenshot_path and template_path:
+            ok, msg = run_adb_screencap(adb_path, device, screenshot_path)
+            if not ok:
+                print(json.dumps({"success": False, "error": f"截图失败: {msg}"}))
+                sys.exit(1)
+        else:
+            print(json.dumps({"success": False, "error": "缺少 --adb/--device/--screenshot/--template 参数"}))
+            sys.exit(1)
+    else:
+        # 用法1:位置参数
+        if len(sys.argv) < 3:
+            print(json.dumps({"success": False, "error": "用法: image-match.py <screenshot_path> <template_path> [threshold]"}))
+            sys.exit(1)
+        screenshot_path = sys.argv[1]
+        template_path = sys.argv[2]
+        threshold = float(sys.argv[3]) if len(sys.argv) > 3 else 0.8
+
+    if not os.path.exists(screenshot_path):
+        print(json.dumps({"success": False, "error": f"截图文件不存在: {screenshot_path}"}))
+        sys.exit(1)
+
+    if not os.path.exists(template_path):
+        print(json.dumps({"success": False, "error": f"模板文件不存在: {template_path}"}))
+        sys.exit(1)
+
+    screenshot = load_image(screenshot_path)
+    template = load_image(template_path)
+
+    if screenshot is None:
+        print(json.dumps({"success": False, "error": "无法读取截图(文件损坏或格式不支持)"}))
+        sys.exit(1)
+
+    if template is None:
+        print(json.dumps({"success": False, "error": f"无法读取模板: {template_path}"}))
+        sys.exit(1)
+
+    t_h, t_w = template.shape[:2]
+    if t_h > screenshot.shape[0] or t_w > screenshot.shape[1]:
+        print(json.dumps({"success": False, "error": "模板尺寸大于截图"}))
+        sys.exit(1)
+
+    # 使用 TM_CCOEFF_NORMED 进行模板匹配
+    result = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
+    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
+
+    if max_val < threshold:
+        print(json.dumps({"success": False, "error": f"未找到匹配 (相似度 {max_val:.3f} < {threshold})"}))
+        sys.exit(1)
+
+    x, y = int(max_loc[0]), int(max_loc[1])
+    center_x = x + t_w // 2
+    center_y = y + t_h // 2
+
+    output = {
+        "success": True,
+        "x": x,
+        "y": y,
+        "width": t_w,
+        "height": t_h,
+        "center_x": center_x,
+        "center_y": center_y
+    }
+    print(json.dumps(output))
+    sys.exit(0)
+
+if __name__ == "__main__":
+    main()

+ 10 - 4
src/page/device/connect-item/connect-item.jsx

@@ -2,8 +2,8 @@ import React, { useState, useRef, useEffect } from 'react'
 import './connect-item.scss'
 import { ConnectItemClass } from './connect-item.js'
 
-// 连接状态: grey 灰色(未连接) | red 红色(错误) | green 绿色(已连接)
-function ConnectItem({ ipAddress, isConnected=false, selected=false, onSelect, onRemove, onConnect, onPreview }) {
+// 执行状态灯:grey 未执行/已停止 | green 执行中 | red 执行失败;无 executionStatus 时用 connectionStatus
+function ConnectItem({ ipAddress, isConnected=false, selected=false, executionStatus = null, onSelect, onRemove, onConnect, onPreview }) {
   const connectItemClassRef = useRef(null)
   const [connectionStatus, setConnectionStatus] = useState('grey')  // 默认灰色,由 connect-item.js 切换
   const [isPreviewing, setIsPreviewing] = useState(false)
@@ -63,8 +63,14 @@ function ConnectItem({ ipAddress, isConnected=false, selected=false, onSelect, o
           </div>
 
           <div
-            className={`status-indicator status-indicator--${connectionStatus}`}
-            title={connectionStatus === 'green' ? '已连接' : connectionStatus === 'red' ? '连接异常' : '未连接'}
+            className={`status-indicator status-indicator--${
+              executionStatus?.failedIps?.includes(ipAddress) ? 'red' :
+              executionStatus?.running && executionStatus.executingIps?.includes(ipAddress) ? 'green' : 'grey'
+            }`}
+            title={
+              executionStatus?.failedIps?.includes(ipAddress) ? '执行失败' :
+              executionStatus?.running && executionStatus.executingIps?.includes(ipAddress) ? '执行中' : '未执行'
+            }
           />
         </div>
       </div>

+ 11 - 7
src/page/device/device.js

@@ -4,13 +4,17 @@ import alertView from '../public/alert-view/alert-view.js'
 
 // 模块级 store,供 ProcessItemClass 等普通类读取
 let _selectedDevices = []
-export function getSelectedDevices() {
-  console.log('[store] getSelectedDevices', _selectedDevices.length, _selectedDevices)
-  return _selectedDevices
-}
-export function setSelectedDevicesStore(devices) {
-  console.log('[store] setSelectedDevicesStore', devices?.length, devices)
-  _selectedDevices = devices || []
+export function getSelectedDevices() { return _selectedDevices }
+export function setSelectedDevicesStore(devices) { _selectedDevices = devices || [] }
+
+// 脚本执行状态,供 ConnectItem 显示灯色:grey 未执行/已停止 | green 执行中 | red 执行失败
+let _executionStatus = { running: false, executingIps: [], failedIps: [] }
+let _setExecutionStatusCallback = null
+export function getExecutionStatus() { return _executionStatus }
+export function setExecutionStatusCallback(cb) { _setExecutionStatusCallback = cb }
+export function setExecutionStatus(running, executingIps = [], failedIps = []) {
+  _executionStatus = { running, executingIps: executingIps || [], failedIps: failedIps || [] }
+  _setExecutionStatusCallback?.({ ..._executionStatus })
 }
 
 // 设备管理类,所有方法都可以通过 this. 访问属性

+ 8 - 1
src/page/device/device.jsx

@@ -3,17 +3,23 @@ import './device.scss'
 import UpdateBtn from './update-btn/update-btn.jsx'
 import ConnectItem from './connect-item/connect-item.jsx'
 
-import { DeviceClass, setSelectedDevicesStore } from './device.js'
+import { DeviceClass, setSelectedDevicesStore, setExecutionStatusCallback, getExecutionStatus } from './device.js'
 
 function Device({ show }) {
   const [deviceList, setDeviceList] = useState([])
   const deviceClass = useRef(null)
   const [inputValue, setInputValue] = useState('192.168.')
   const [selectedDevices, setSelectedDevices] = useState([])  // 数组,记录所有选中的 device IP
+  const [executionStatus, setExecutionStatus] = useState(getExecutionStatus())
   if (!show) {
     return null
   }
 
+  useEffect(() => {
+    setExecutionStatusCallback((status) => setExecutionStatus(status))
+    return () => setExecutionStatusCallback(null)
+  }, [])
+
   useEffect(() => {
     if (!deviceClass.current) {
       deviceClass.current = new DeviceClass()
@@ -54,6 +60,7 @@ function Device({ show }) {
               key={index} 
               ipAddress={deviceList[index]} 
               selected={selectedDevices.includes(deviceList[index])}
+              executionStatus={executionStatus}
               onSelect={(ip) => setSelectedDevices(prev => prev.includes(ip) ? prev.filter(x => x !== ip) : [...prev, ip])}
               onRemove={(ip) => deviceClass.current?.onRemoveDevice(ip)}
             />

+ 43 - 8
src/page/process/process-item/process-item.js

@@ -1,22 +1,57 @@
-import { getSelectedDevices } from '../../device/device.js'
+import { getSelectedDevices, setExecutionStatus } from '../../device/device.js'
+import hintView from '../../public/hint-view/hint-view.js'
 
-class ProcessItemClass {
-  constructor() {
+/** 解析 run-process 输出,返回失败设备 IP 列表 */
+function parseRunProcessResult(stdout) {
+  const json = JSON.parse(stdout.trim())
+  return json.failedIp ? [json.failedIp] : []
+}
+
+/** run-process 完成时更新状态并提示 */
+function onRunComplete(setIsRunning, res) {
+  setIsRunning(false)
+  const failedIps = parseRunProcessResult(res.stdout)
+  if (failedIps.length) {
+    hintView.setContent(`设备 ${failedIps[0]} 执行失败`)
+    hintView.show()
   }
+  setExecutionStatus(false, [], failedIps)
+}
+
+/** run-process 异常时恢复状态 */
+function onRunError(setIsRunning) {
+  setIsRunning(false)
+  setExecutionStatus(false, [], [])
+}
+
+class ProcessItemClass {
+  constructor() {}
 
-  async init(processInfo, setIsRunning) {
+  init(processInfo, setIsRunning) {
     this.processInfo = processInfo
     this.setIsRunning = setIsRunning
   }
 
-  async start() {
-    if (this.setIsRunning) this.setIsRunning(true)
+  start() {
     const selectedDevices = getSelectedDevices()
-    console.log('选中的设备:', selectedDevices)
+    if (!selectedDevices?.length) {
+      hintView.setContent('请先在设备列表中勾选要执行的设备')
+      hintView.show()
+      return
+    }
+    this.setIsRunning(true)
+    this._lastRunParams = [JSON.stringify(selectedDevices), this.processInfo.name]
+    setExecutionStatus(true, selectedDevices, [])
+    window.electronAPI.runNodejsScript('run-process', ...this._lastRunParams)
+      .then((res) => onRunComplete(this.setIsRunning, res))
+      .catch(() => onRunError(this.setIsRunning))
   }
 
   stop() {
-    if (this.setIsRunning) this.setIsRunning(false)
+    this.setIsRunning(false)
+    setExecutionStatus(false, [], [])
+    const params = this._lastRunParams || []
+    window.electronAPI.killNodejsScript('run-process', ...params).catch(() => {})
   }
 
   delete() {