yichael 4 месяцев назад
Родитель
Сommit
6dd4356481
32 измененных файлов с 2251 добавлено и 3253 удалено
  1. 130 0
      BAT-TOOL/SwitchDefaultKeyboard.bat
  2. 22 8
      document/工作流语法.md
  3. 0 3
      main-js/adb/input.js
  4. 124 9
      main-js/execute-py.js
  5. 57 14
      main-js/func/ocr-chat.js
  6. 0 1132
      main-js/history.js
  7. 187 55
      main-js/read-and-write.js
  8. 1 1
      main-js/workflow.js
  9. 2 0
      preload.cjs
  10. 1 1
      py/OnnxOCR
  11. 5 604
      src/pages/Chat/Input/Input.js
  12. 407 0
      src/pages/Chat/Input/generate-processing.js
  13. 0 1156
      src/pages/Chat/Input/input-hooks.jsx
  14. 160 0
      src/pages/Chat/Input/resource-require.js
  15. 99 8
      src/pages/Processing/Func/chat/chat-history.js
  16. 29 21
      src/pages/Processing/Func/chat/ocr-chat.js
  17. 2 2
      src/pages/Processing/Func/chat/read-chat-history.js
  18. 20 3
      src/pages/Processing/Func/chat/read-last-message.js
  19. 1 1
      src/pages/Processing/Func/chat/save-new-chat.js
  20. 4 3
      src/pages/Processing/Func/image-center-location.js
  21. 4 3
      src/pages/Processing/Func/image-region-location.js
  22. 403 117
      src/pages/Processing/action-parser.js
  23. 0 34
      static/processing/微信聊天自动发送工作流/history/chat_2026-01-14T22-23-31.json
  24. 0 54
      static/processing/微信聊天自动发送工作流/history/chat_2026-01-15T07-15-56.json
  25. 431 0
      static/processing/微信聊天自动发送工作流/log.txt
  26. 81 24
      static/processing/微信聊天自动发送工作流/processing.json
  27. 0 0
      static/processing/微信聊天自动发送工作流/resources/好友头像.png
  28. 0 0
      static/processing/微信聊天自动发送工作流/resources/微信聊天界面的发送按钮定位图.png
  29. 0 0
      static/processing/微信聊天自动发送工作流/resources/微信聊天界面的输入框定位图.png
  30. 0 0
      static/processing/微信聊天自动发送工作流/resources/我的头像.png
  31. 81 0
      static/processing/微信聊天自动发送工作流/工作流思维导图.md
  32. BIN
      temp_screenshot.png

+ 130 - 0
BAT-TOOL/SwitchDefaultKeyboard.bat

@@ -0,0 +1,130 @@
+@echo off
+chcp 65001 >nul
+setlocal enabledelayedexpansion
+
+echo ========================================
+echo Switch to Sogou Input Method
+echo ========================================
+echo.
+
+REM Get the directory where this batch file is located
+set "SCRIPT_DIR=%~dp0"
+set "PROJECT_ROOT=%SCRIPT_DIR%.."
+
+REM Read ADB path from adb-path-config.js using PowerShell
+echo [0/5] Reading ADB path from adb-path-config.js...
+for /f "delims=" %%i in ('powershell -Command "$content = Get-Content '%PROJECT_ROOT%\adb-path-config.js' -Raw; if ($content -match '\"adb-path\"\s*:\s*''([^'']+)''') { $adbPath = $matches[1]; $fullPath = $adbPath.TrimEnd([char]92) + [char]92 + 'adb.exe'; if (Test-Path $fullPath) { Write-Output $fullPath } else { Write-Output 'adb' } } else { Write-Output 'adb' }"') do set "ADB_PATH=%%i"
+
+if "%ADB_PATH%"=="" set "ADB_PATH=adb"
+echo Using ADB: %ADB_PATH%
+echo.
+
+REM Set fixed device address
+set "DEVICE_ADDRESS=192.168.2.5:5555"
+set "USE_DEVICE_OPTION=-s !DEVICE_ADDRESS!"
+echo Using device: !DEVICE_ADDRESS!
+
+echo.
+
+echo [1/5] Check if ADB is available...
+"%ADB_PATH%" version >nul 2>&1
+if errorlevel 1 (
+    echo Error: ADB not found at: %ADB_PATH%
+    echo Please check adb-path-config.js and ensure ADB path is correct
+    pause
+    exit /b 1
+)
+echo ADB is available
+echo.
+
+echo [2/5] Check connected devices...
+echo.
+"%ADB_PATH%" devices
+echo.
+REM Try to connect if not already connected
+echo Checking if device !DEVICE_ADDRESS! is connected...
+    "%ADB_PATH%" !USE_DEVICE_OPTION! get-state >nul 2>&1
+    if errorlevel 1 (
+        echo Device not connected, attempting to connect...
+        "%ADB_PATH%" connect !DEVICE_ADDRESS! >nul 2>&1
+        timeout /t 2 /nobreak >nul
+        "%ADB_PATH%" !USE_DEVICE_OPTION! get-state >nul 2>&1
+        if errorlevel 1 (
+            echo Error: Cannot connect to device !DEVICE_ADDRESS!
+            echo.
+            echo Please ensure:
+            echo   1. Device is on the same network
+            echo   2. Run 0-TEST-CONNECT.BAT to test connection first
+            pause
+            exit /b 1
+        )
+        echo Device connected successfully
+    ) else (
+        echo Device already connected
+    )
+    echo.
+echo.
+
+echo [3/5] Check current default input method...
+echo.
+for /f "delims=" %%i in ('"%ADB_PATH%" !USE_DEVICE_OPTION! shell settings get secure default_input_method') do set "CURRENT_IME=%%i"
+set "CURRENT_IME=!CURRENT_IME: =!"
+echo Current default IME: !CURRENT_IME!
+echo.
+
+REM Target input method: Sogou Input Method
+set "TARGET_IME=com.sohu.inputmethod.sogou/.SogouIME"
+
+REM Check if already using target input method
+if "!CURRENT_IME!"=="!TARGET_IME!" (
+    echo Sogou Input Method is already the default input method.
+    echo No action needed.
+    pause
+    exit /b 0
+)
+
+echo [4/5] List available input methods...
+echo.
+"%ADB_PATH%" !USE_DEVICE_OPTION! shell ime list -s
+echo.
+
+echo [5/5] Switch to Sogou Input Method...
+echo Setting !TARGET_IME! as default input method...
+"%ADB_PATH%" !USE_DEVICE_OPTION! shell ime enable !TARGET_IME!
+if errorlevel 1 (
+    echo Warning: Failed to enable Sogou Input Method
+    echo   Make sure Sogou Input Method is installed on your device
+    echo.
+) else (
+    echo Sogou Input Method enabled successfully
+    echo.
+)
+
+"%ADB_PATH%" !USE_DEVICE_OPTION! shell ime set !TARGET_IME!
+if errorlevel 1 (
+    echo Error: Failed to set default input method
+    pause
+    exit /b 1
+)
+echo Default input method set successfully
+echo.
+
+echo Verifying current default input method...
+for /f "delims=" %%i in ('"%ADB_PATH%" !USE_DEVICE_OPTION! shell settings get secure default_input_method') do set "NEW_IME=%%i"
+set "NEW_IME=!NEW_IME: =!"
+echo Current default IME: !NEW_IME!
+echo.
+
+if "!NEW_IME!"=="!TARGET_IME!" (
+    echo ========================================
+    echo Success: Sogou Input Method is now default
+    echo ========================================
+) else (
+    echo ========================================
+    echo Warning: Failed to set Sogou Input Method as default
+    echo Expected: !TARGET_IME!
+    echo Current: !NEW_IME!
+    echo ========================================
+)
+echo.
+pause

+ 22 - 8
document/工作流语法.md

@@ -48,7 +48,7 @@
 | method | 说明 | inVars |
 |--------|------|--------|
 | `input` | 输入文本 | `[0]`: 文本内容 |
-| `click` | 点击 | `[0]`: 位置坐标 `{x, y}` |
+| `click` | 点击 | `[0]`: 位置坐标(字符串格式:`"{\"x\":123,\"y\":456}"` 或 `"123,456"`) |
 | `locate` | 定位 | `[0]`: 图片/文字,`outVars[0]`: 保存位置 |
 | `swipe` | 滑动 | `[0]`: 方向(up-down/down-up/left-right/right-left) |
 | `scroll` | 滚动 | `[0]`: 方向(up/down) |
@@ -89,9 +89,22 @@
 
 ### 设置变量(set)
 ```json
-{ "type": "set", "variable": "name", "value": "value" }
+{ "type": "set", "variable": "{name}", "value": "value" }
 ```
 
+### 打印信息(echo)
+```json
+{ "type": "echo", "value": "当前消息: {lastMessage}" }
+```
+
+或使用 `inVars`:
+```json
+{ "type": "echo", "inVars": ["{lastMessage}", "{lastRole}"] }
+```
+
+- `value`: 直接文本,支持 `{variable}` 格式的变量替换
+- `inVars`: 变量名数组,输出所有变量的值
+
 ### 随机数(random)
 ```json
 { "type": "random", "variable": "num", "min": 1, "max": 100, "integer": true }
@@ -101,7 +114,8 @@
 
 - 定义:`"variables": {"name": "value"}`
 - 使用:`"{name}"` 在字段中引用
-- 保存:`"outVars": ["{name}"]` 或 `"variable": "name"`
+- 保存:`"outVars": ["{name}"]` 或 `"variable": "{name}"`
+- 类型:变量值只能是 `number` 或 `string` 类型(`null`、`true`、`false` 等会被自动转换)
 
 ## 时间格式
 
@@ -114,9 +128,9 @@
 扩展标签由 `src/pages/processing/func/` 目录下的脚本文件决定,每个脚本文件名即为标签名。
 
 常用标签:
-- `ocr-chat-history`: OCR提取聊天记录
-- `read-chat-history`: 读取聊天记录
-- `read-last-message`: 读取最后一条消息
-- `save-new-chat`: 保存对话
-- `image-center-location`: 图像中心点定位
+- `ocr-chat`: OCR识别对话内容(输出JSON字符串格式)
+- `read-chat-history`: 读取聊天记录(输出JSON字符串格式)
+- `read-last-message`: 读取最后一条消息(输出文本和发送者角色)
+- `save-new-chat`: 保存对话(自动去重和文件大小管理)
+- `image-center-location`: 图像中心点定位(输出JSON字符串格式:`{"x":123,"y":456}`)
 - `image-region-location`: 图像区域定位(返回四个顶点)

+ 0 - 3
main-js/adb/input.js

@@ -370,7 +370,6 @@ export async function sendText(ipPort, text) {
           maxBuffer: 1024 * 1024
         });
         
-        console.log('使用 input text 成功输入文本');
         return { success: true };
       } catch (error) {
         console.warn('input text 方法失败,尝试 ADBKeyBoard:', error.message);
@@ -405,7 +404,6 @@ export async function sendText(ipPort, text) {
         await sendTextViaADBKeyBoard(adbPath, ipPort, text);
       }
       
-      console.log('使用 ADBKeyBoard 成功输入文本');
       return { success: true };
     } catch (adbKeyboardError) {
       console.warn('ADBKeyBoard 方法失败,尝试自动安装:', adbKeyboardError.message);
@@ -522,7 +520,6 @@ export async function sendText(ipPort, text) {
         await performPaste(adbPath, ipPort);
       }
       
-      console.log('使用剪贴板方式成功输入文本');
       return { success: true };
     } catch (clipboardError) {
       // 所有方法都失败

+ 124 - 9
main-js/execute-py.js

@@ -12,7 +12,7 @@ import { captureScreenshot } from './adb/screenshot.js';
 import { getDeviceResolution } from './adb/device-info.js';
 import { matchImage } from './func/image-center-location.js';
 import { findTextLocation } from './func/string-reg-location.js';
-import { ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat-history.js';
+import { ocrFullScreen as ocrFullScreenFromFunc, getLastMessage as getLastMessageFromFunc } from './func/ocr-chat.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
@@ -320,7 +320,127 @@ export async function ocrFullScreen(ipPort, folderPath = null) {
       if (tmpDir) {
         try {
           await rm(tmpDir, { recursive: true, force: true });
-          console.log(`已删除临时目录: ${tmpDir}`);
+          // 已删除临时目录日志(不显示)
+        } catch (rmError) {
+          console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
+        }
+      }
+    }
+  } catch (error) {
+    console.error('OCR识别失败:', error);
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
+    }
+    return { success: false, error: error.message };
+  }
+}
+
+/**
+ * OCR识别最后一条消息(兼容旧API)
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} method - 识别方法 ('full-screen' | 'by-avatar')
+ * @param {string} avatarPath - 头像路径(by-avatar 时使用)
+ * @param {string} area - 区域(未使用,保留兼容性)
+ * @param {string} folderPath - 工作流文件夹路径(可选)
+ * @returns {Promise<{success: boolean, error?: string, text?: string, position?: Object}>}
+ */
+export async function ocrLastMessage(ipPort, method, avatarPath, area, folderPath = null) {
+  try {
+    if (!ipPort) {
+      return { success: false, error: '缺少设备 ID' };
+    }
+
+    // 1. 获取设备分辨率
+    const resolutionResult = await getDeviceResolution(ipPort);
+    if (!resolutionResult.success) {
+      return { success: false, error: '获取设备分辨率失败' };
+    }
+    const { width, height } = resolutionResult;
+
+    // 2. 获取屏幕截图
+    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
+    if (!screenshotResult.success || !screenshotResult.data) {
+      return { success: false, error: '获取屏幕截图失败' };
+    }
+
+    // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
+    let screenshotPath;
+    let tmpDir = null; // 用于跟踪需要删除的临时目录
+    if (folderPath) {
+      // 确保 folderPath 是绝对路径
+      let absoluteFolderPath = folderPath;
+      if (!isAbsolute(folderPath)) {
+        // 如果已经是 static/processing/xxx 格式,去掉开头的 static/processing 再拼接
+        if (folderPath.startsWith('static/processing/')) {
+          const folderName = folderPath.replace('static/processing/', '');
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (folderPath.startsWith('static\\processing\\')) {
+          const folderName = folderPath.replace('static\\processing\\', '');
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          // 如果只是文件夹名,需要加上 static/processing
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderPath);
+        }
+      }
+      
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
+      tmpDir = join(absoluteFolderPath, 'tmp', timestamp);
+      await mkdir(tmpDir, { recursive: true });
+      screenshotPath = join(tmpDir, 'screenshot_ocr.png');
+    } else {
+      const tempDir = join(__dirname, '..');
+      screenshotPath = join(tempDir, 'temp_screenshot_ocr.png');
+    }
+    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
+    await writeFile(screenshotPath, screenshotBuffer);
+
+    try {
+      // 4. 调用 JS 实现进行OCR识别
+      const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
+      let result;
+      
+      if (method === 'full-screen') {
+        // 全屏OCR识别
+        result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
+      } else if (method === 'by-avatar' && avatarPath) {
+        // 通过头像定位最后一条消息
+        let friendAvatarArg = null;
+        let myAvatarArg = null;
+        
+        if (isAbsolute(avatarPath)) {
+          friendAvatarArg = avatarPath;
+          myAvatarArg = avatarPath;
+        } else {
+          const folderName = avatarPath.split(/[/\\]/)[0];
+          const avatarName = avatarPath.split(/[/\\]/).slice(1).join('/');
+          friendAvatarArg = join(__dirname, '..', 'static', 'processing', folderName, avatarName);
+          myAvatarArg = friendAvatarArg;
+        }
+        
+        const normalizedFriendAvatar = friendAvatarArg.replace(/\\/g, '/');
+        const normalizedMyAvatar = myAvatarArg.replace(/\\/g, '/');
+        result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
+      } else {
+        // 默认使用全屏OCR
+        result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
+      }
+      
+      if (result.success) {
+        // 返回兼容旧API的格式
+        return {
+          success: true,
+          text: result.text || '',
+          position: result.position || null
+        };
+      } else {
+        return { success: false, error: result.error || 'OCR识别失败' };
+      }
+    } finally {
+      // 5. 使用完后删除临时目录
+      if (tmpDir) {
+        try {
+          await rm(tmpDir, { recursive: true, force: true });
+          // 已删除临时目录日志(不显示)
         } catch (rmError) {
           console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
         }
@@ -354,13 +474,8 @@ export function registerIpcHandlers() {
     return await findTextAndGetCoordinate(ipPort, targetText);
   });
 
-  // 全屏OCR识别(兼容旧API:ocr-last-message
+  // OCR识别最后一条消息(兼容旧API)
   ipcMain.handle('ocr-last-message', async (event, ipPort, method, avatarPath, area, folderPath) => {
-    // 兼容旧API,如果 method 是 'full-screen',调用新的 ocrFullScreen
-    if (method === 'full-screen') {
-      return await ocrFullScreen(ipPort, folderPath);
-    }
-    // 其他情况返回错误,建议使用新的 API
-    return { success: false, error: '请使用 extract-chat-history 或 get-last-chat-message API' };
+    return await ocrLastMessage(ipPort, method, avatarPath, area, folderPath);
   });
 }

+ 57 - 14
main-js/func/ocr-chat-history.js → main-js/func/ocr-chat.js

@@ -1,8 +1,12 @@
 /**
- * 提取聊天记录功能(Node.js 实现)
- * 从屏幕截图中提取完整的聊天记录,包括发送者信息
+ * OCR 聊天记录提取功能(Node.js 实现)
  * 
- * 直接调用 Python 的 OnnxOCR 和 OpenCV,通过内联 Python 代码实现
+ * 功能:根据屏幕截图提取聊天记录和聊天角色,并输出为 JSON 格式
+ * - 使用 OCR 识别聊天内容
+ * - 根据头像位置或消息位置识别发送者角色(friend/me)
+ * - 返回 JSON 格式的消息数组,每条消息包含 sender 和 text 字段
+ * 
+ * 实现方式:直接调用 Python 的 OnnxOCR 和 OpenCV,通过内联 Python 代码实现
  */
 
 import { exec } from 'child_process';
@@ -43,14 +47,19 @@ def read_image_safe(image_path):
 }
 
 /**
- * 提取聊天记录
- * @param {string} screenshotPath - 截图路径
- * @param {string} friendAvatarPath - 好友头像路径(可选)
- * @param {string} myAvatarPath - 我的头像路径(可选)
- * @param {number} deviceWidth - 设备宽度(可选)
- * @param {number} deviceHeight - 设备高度(可选)
+ * 根据屏幕截图提取聊天记录和聊天角色
+ * 
+ * @param {string} screenshotPath - 截图路径(屏幕截图文件)
+ * @param {string} friendAvatarPath - 好友头像路径(可选,用于识别发送者角色)
+ * @param {string} myAvatarPath - 我的头像路径(可选,用于识别发送者角色)
+ * @param {number} deviceWidth - 设备宽度(可选,用于位置判断)
+ * @param {number} deviceHeight - 设备高度(可选,用于位置判断)
  * @param {string} workflowFolder - 工作流文件夹路径(可选)
- * @returns {Promise<{success: boolean, messages?: Array, messagesText?: string, error?: string}>}
+ * @returns {Promise<{success: boolean, messages?: Array<{sender: 'friend'|'me'|'unknown', text: string}>, messagesText?: string, error?: string}>}
+ * 
+ * messages: JSON 格式的消息数组,每条消息包含:
+ *   - sender: 发送者角色('friend' 表示好友,'me' 表示自己,'unknown' 表示无法识别)
+ *   - text: 消息文本内容
  */
 export async function extractChatHistory(screenshotPath, friendAvatarPath, myAvatarPath, deviceWidth, deviceHeight, workflowFolder) {
   try {
@@ -139,14 +148,16 @@ def extract_chat_history(screenshot_path, friend_avatar_path, my_avatar_path, de
             if not text or confidence < 0.5:
                 continue
             
-            # 计算消息框的中心 y 坐标
+            # 计算消息框的中心坐标
+            x_center = sum([point[0] for point in box]) / len(box)
             y_center = sum([point[1] for point in box]) / len(box)
             
-            # 判断发送者(简单的距离判断)
+            # 判断发送者(优先使用头像距离判断,如果失败则使用 x 坐标判断)
             sender = 'unknown'
             min_friend_dist = float('inf')
             min_my_dist = float('inf')
             
+            # 方法1: 使用头像位置距离判断(阈值100像素)
             for fx, fy in friend_positions:
                 dist = abs(y_center - (fy + 20))  # 假设头像高度约 40px
                 if dist < min_friend_dist:
@@ -157,10 +168,42 @@ def extract_chat_history(screenshot_path, friend_avatar_path, my_avatar_path, de
                 if dist < min_my_dist:
                     min_my_dist = dist
             
-            if min_friend_dist < 50 and min_friend_dist < min_my_dist:
+            # 优先使用距离判断(阈值100像素)
+            if min_friend_dist < 100 and min_friend_dist < min_my_dist:
                 sender = 'friend'
-            elif min_my_dist < 50 and min_my_dist < min_friend_dist:
+            elif min_my_dist < 100 and min_my_dist < min_friend_dist:
                 sender = 'me'
+            else:
+                # 距离判断失败,使用备选方法
+                screen_center_x = device_width / 2
+                # 如果两个头像都没找到,直接使用 x 坐标判断
+                if not friend_positions and not my_positions:
+                    if x_center < screen_center_x:
+                        sender = 'friend'  # 左侧通常是好友
+                    else:
+                        sender = 'me'  # 右侧通常是"我"
+                # 如果只找到好友头像,放宽阈值到150像素
+                elif friend_positions and not my_positions:
+                    if min_friend_dist < 150:
+                        sender = 'friend'
+                    elif x_center < screen_center_x:
+                        sender = 'friend'
+                    else:
+                        sender = 'me'
+                # 如果只找到我的头像,放宽阈值到150像素
+                elif my_positions and not friend_positions:
+                    if min_my_dist < 150:
+                        sender = 'me'
+                    elif x_center >= screen_center_x:
+                        sender = 'me'
+                    else:
+                        sender = 'friend'
+                # 如果两个头像都找到了但距离判断失败,使用 x 坐标判断
+                else:
+                    if x_center < screen_center_x:
+                        sender = 'friend'
+                    else:
+                        sender = 'me'
             
             messages.append({
                 'text': text,

+ 0 - 1132
main-js/history.js

@@ -1,1132 +0,0 @@
-import { ipcMain } from 'electron';
-import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
-import { join, dirname, isAbsolute } from 'path';
-import { fileURLToPath } from 'url';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import { captureScreenshot } from './adb/screenshot.js';
-import { getDeviceResolution } from './adb/device-info.js';
-import { matchImage } from './func/image-center-location.js';
-import { findTextLocation } from './func/string-reg-location.js';
-import { extractChatHistory as extractChatHistoryFromFunc, getLastMessage as getLastMessageFromFunc, ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat-history.js';
-
-const execAsync = promisify(exec);
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-// 获取 static/processing 目录下的所有文件夹
-export async function getStaticFolders() {
-  try {
-    const staticPath = join(__dirname, '..', 'static', 'processing');
-    const entries = await readdir(staticPath, { withFileTypes: true });
-    
-    const folders = [];
-    for (const entry of entries) {
-      if (entry.isDirectory()) {
-        const folderPath = join(staticPath, entry.name);
-        const stats = await stat(folderPath);
-        folders.push({
-          name: entry.name,
-          createdAt: stats.birthtime || stats.mtime, // 使用创建时间,如果没有则使用修改时间
-        });
-      }
-    }
-    
-    // 按创建时间排序,最新的在前
-    return folders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
-  } catch (error) {
-    console.error('Failed to read static/processing folders:', error);
-    return [];
-  }
-}
-
-// 执行图像匹配:截图、调用 Python 脚本、返回坐标
-export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-    if (!templateImagePath) {
-      return { success: false, error: '缺少模板图片路径' };
-    }
-
-    // 将相对路径转换为绝对路径
-    let absoluteTemplatePath = templateImagePath;
-    if (!isAbsolute(templateImagePath)) {
-      absoluteTemplatePath = join(__dirname, '..', templateImagePath);
-    }
-
-    // 1. 获取设备分辨率
-    const resolutionResult = await getDeviceResolution(ipPort);
-    if (!resolutionResult.success) {
-      return { success: false, error: '获取设备分辨率失败' };
-    }
-    const { width, height } = resolutionResult;
-
-    // 2. 获取屏幕截图
-    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
-    if (!screenshotResult.success || !screenshotResult.data) {
-      return { success: false, error: '获取屏幕截图失败' };
-    }
-
-    // 3. 保存截图到临时文件
-    const tempDir = join(__dirname, '..');
-    const screenshotPath = join(tempDir, 'temp_screenshot.png');
-    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
-    await writeFile(screenshotPath, screenshotBuffer);
-
-    // 4. 调用 JS 函数进行图像匹配
-    const matchResult = await matchImage(screenshotPath, absoluteTemplatePath, width, height);
-    
-    if (!matchResult.success) {
-      return { success: false, error: matchResult.error || '图像匹配失败' };
-    }
-
-    // 5. 返回匹配结果
-    if (matchResult.success && matchResult.x !== undefined) {
-      const { x, y, width: w, height: h } = matchResult;
-      
-      // 计算点击位置(中心点)
-      const clickX = Math.round(x + w / 2);
-      const clickY = Math.round(y + h / 2);
-
-      return {
-        success: true,
-        coordinate: { x, y, width: w, height: h },
-        clickPosition: { x: clickX, y: clickY }
-      };
-    } else {
-      return { 
-        success: false, 
-        error: matchResult.error || '图像匹配失败' 
-      };
-    }
-  } catch (error) {
-    console.error('图像匹配失败:', error);
-    return { success: false, error: error.message };
-  }
-}
-
-// 图像区域定位:在完整截图中查找区域截图的位置,返回四个顶点坐标
-export async function matchImageRegionLocation(screenshotPath, regionPath, device = null) {
-  try {
-    if (!screenshotPath) {
-      return { success: false, error: '缺少完整截图路径' };
-    }
-    if (!regionPath) {
-      return { success: false, error: '缺少区域截图路径' };
-    }
-
-    // 将相对路径转换为绝对路径
-    let absoluteScreenshotPath = screenshotPath;
-    if (!isAbsolute(screenshotPath)) {
-      absoluteScreenshotPath = join(__dirname, '..', screenshotPath);
-    }
-
-    let absoluteRegionPath = regionPath;
-    if (!isAbsolute(regionPath)) {
-      absoluteRegionPath = join(__dirname, '..', regionPath);
-    }
-
-    // 可选:如果提供了设备ID,获取设备分辨率用于缩放
-    let width = null;
-    let height = null;
-    if (device) {
-      const resolutionResult = await getDeviceResolution(device);
-      if (resolutionResult.success) {
-        width = resolutionResult.width;
-        height = resolutionResult.height;
-      }
-    }
-
-    // 调用图像匹配函数
-    const matchResult = await matchImage(absoluteScreenshotPath, absoluteRegionPath, width, height);
-    
-    if (!matchResult.success) {
-      return { success: false, error: matchResult.error || '图像匹配失败' };
-    }
-
-    // 获取匹配结果
-    const { x, y, width: w, height: h } = matchResult;
-    
-    // 计算四个顶点坐标
-    const corners = {
-      topLeft: { x, y },
-      topRight: { x: x + w, y },
-      bottomLeft: { x, y: y + h },
-      bottomRight: { x: x + w, y: y + h }
-    };
-
-    return {
-      success: true,
-      x,
-      y,
-      width: w,
-      height: h,
-      corners: corners,
-      similarity: matchResult.similarity
-    };
-  } catch (error) {
-    console.error('图像区域定位失败:', error);
-    return { success: false, error: error.message };
-  }
-}
-
-// 执行文字识别:截图、调用 Python 脚本、返回坐标
-export async function findTextAndGetCoordinate(ipPort, targetText) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-    if (!targetText) {
-      return { success: false, error: '缺少目标文字' };
-    }
-
-    // 1. 获取设备分辨率
-    const resolutionResult = await getDeviceResolution(ipPort);
-    if (!resolutionResult.success) {
-      return { success: false, error: '获取设备分辨率失败' };
-    }
-    const { width, height } = resolutionResult;
-
-    // 2. 获取屏幕截图
-    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
-    if (!screenshotResult.success || !screenshotResult.data) {
-      return { success: false, error: '获取屏幕截图失败' };
-    }
-
-    // 3. 保存截图到临时文件
-    const tempDir = join(__dirname, '..');
-    const screenshotPath = join(tempDir, 'temp_screenshot.png');
-    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
-    await writeFile(screenshotPath, screenshotBuffer);
-
-    // 4. 调用 JS 函数进行文字识别
-    const textResult = await findTextLocation(screenshotPath, targetText, width, height);
-    
-    if (!textResult.success || !textResult.found) {
-      return { success: false, error: textResult.error || `未找到文字: ${targetText}` };
-    }
-
-    // 5. 返回识别结果
-    const { x, y, width: w, height: h } = textResult;
-    
-    // 计算点击位置(中心点)
-    const clickX = Math.round(x + w / 2);
-    const clickY = Math.round(y + h / 2);
-
-    return {
-      success: true,
-      coordinate: { x, y, width: w, height: h },
-      clickPosition: { x: clickX, y: clickY }
-    };
-  } catch (error) {
-    console.error('文字识别失败:', error);
-    // 如果是超时错误,提供更友好的提示
-    if (error.message && error.message.includes('timeout')) {
-      return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
-    }
-    return { success: false, error: error.message };
-  }
-}
-
-// OCR识别最后一条消息(兼容旧API)
-export async function ocrLastMessage(ipPort, method, avatarPath, area, folderPath = null) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-
-    // 1. 获取设备分辨率
-    const resolutionResult = await getDeviceResolution(ipPort);
-    if (!resolutionResult.success) {
-      return { success: false, error: '获取设备分辨率失败' };
-    }
-    const { width, height } = resolutionResult;
-
-    // 2. 获取屏幕截图
-    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
-    if (!screenshotResult.success || !screenshotResult.data) {
-      return { success: false, error: '获取屏幕截图失败' };
-    }
-
-    // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
-    let screenshotPath;
-    let tmpDir = null; // 用于跟踪需要删除的临时目录
-    if (folderPath) {
-      const { mkdir } = await import('fs/promises');
-      // 确保 folderPath 是绝对路径
-      let absoluteFolderPath = folderPath;
-      if (!isAbsolute(folderPath)) {
-        // 如果已经是 static/processing/xxx 格式,去掉开头的 static/processing 再拼接
-        if (folderPath.startsWith('static/processing/')) {
-          const folderName = folderPath.replace('static/processing/', '');
-          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (folderPath.startsWith('static\\processing\\')) {
-          const folderName = folderPath.replace('static\\processing\\', '');
-          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          // 如果只是文件夹名,需要加上 static/processing
-          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderPath);
-        }
-      }
-      
-      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
-      tmpDir = join(absoluteFolderPath, 'tmp', timestamp);
-      await mkdir(tmpDir, { recursive: true });
-      screenshotPath = join(tmpDir, 'screenshot_ocr.png');
-    } else {
-      const tempDir = join(__dirname, '..');
-      screenshotPath = join(tempDir, 'temp_screenshot_ocr.png');
-    }
-    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
-    await writeFile(screenshotPath, screenshotBuffer);
-
-    try {
-      // 4. 调用 JS 实现进行OCR识别
-      const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
-      let result;
-      
-      if (method === 'full-screen') {
-        // 全屏OCR识别
-        result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
-      } else if (method === 'by-avatar' && avatarPath) {
-        // 通过头像定位最后一条消息
-        let friendAvatarArg = null;
-        let myAvatarArg = null;
-        
-        if (isAbsolute(avatarPath)) {
-          friendAvatarArg = avatarPath;
-          myAvatarArg = avatarPath;
-        } else {
-          const folderName = avatarPath.split(/[/\\]/)[0];
-          const avatarName = avatarPath.split(/[/\\]/).slice(1).join('/');
-          friendAvatarArg = join(__dirname, '..', 'static', 'processing', folderName, avatarName);
-          myAvatarArg = friendAvatarArg;
-        }
-        
-        const normalizedFriendAvatar = friendAvatarArg.replace(/\\/g, '/');
-        const normalizedMyAvatar = myAvatarArg.replace(/\\/g, '/');
-        result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
-      } else {
-        // 默认使用全屏OCR
-        result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
-      }
-      
-      if (result.success) {
-        // 返回兼容旧API的格式
-        return {
-          success: true,
-          text: result.text || '',
-          position: result.position || null
-        };
-      } else {
-        return { success: false, error: result.error || 'OCR识别失败' };
-      }
-    } finally {
-      // 5. 使用完后删除临时目录
-      if (tmpDir) {
-        try {
-          await rm(tmpDir, { recursive: true, force: true });
-          console.log(`已删除临时目录: ${tmpDir}`);
-        } catch (rmError) {
-          console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
-        }
-      }
-    }
-  } catch (error) {
-    console.error('OCR识别失败:', error);
-    if (error.message && error.message.includes('timeout')) {
-      return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
-    }
-    return { success: false, error: error.message };
-  }
-}
-
-// 提取聊天记录
-export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath = null) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-
-    // 1. 获取设备分辨率
-    const resolutionResult = await getDeviceResolution(ipPort);
-    if (!resolutionResult.success) {
-      return { success: false, error: '获取设备分辨率失败' };
-    }
-    const { width, height } = resolutionResult;
-
-    // 2. 获取屏幕截图
-    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
-    if (!screenshotResult.success || !screenshotResult.data) {
-      return { success: false, error: '获取屏幕截图失败' };
-    }
-
-    // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
-    let screenshotPath;
-    let tmpDir = null; // 用于跟踪需要删除的临时目录
-    if (workflowFolderPath) {
-      const { mkdir } = await import('fs/promises');
-      // 确保 workflowFolderPath 是绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        // 如果已经是 static/processing/xxx 格式,直接拼接(去掉开头的 static/processing)
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          // 如果只是文件夹名,需要加上 static/processing
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-      
-      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
-      tmpDir = join(absoluteWorkflowPath, 'tmp', timestamp);
-      await mkdir(tmpDir, { recursive: true });
-      screenshotPath = join(tmpDir, 'screenshot.png');
-    } else {
-      const tempDir = join(__dirname, '..');
-      screenshotPath = join(tempDir, 'temp_screenshot_chat.png');
-    }
-    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
-    await writeFile(screenshotPath, screenshotBuffer);
-    
-    // 验证文件是否成功写入
-    try {
-      const { access, constants } = await import('fs/promises');
-      await access(screenshotPath, constants.F_OK);
-      console.log(`截图已保存到: ${screenshotPath}`);
-    } catch (err) {
-      console.error(`截图文件写入验证失败: ${screenshotPath}`, err);
-      return { success: false, error: `截图文件写入失败: ${err.message}` };
-    }
-
-    try {
-      // 4. 调用 JS 函数提取聊天记录
-      // 转换头像路径为绝对路径
-      let friendAvatarArg = null;
-      if (friendAvatarPath) {
-        if (isAbsolute(friendAvatarPath)) {
-          friendAvatarArg = friendAvatarPath;
-        } else {
-          friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath);
-        }
-      }
-      
-      let myAvatarArg = null;
-      if (myAvatarPath) {
-        if (isAbsolute(myAvatarPath)) {
-          myAvatarArg = myAvatarPath;
-        } else {
-          myAvatarArg = join(__dirname, '..', 'static', 'processing', myAvatarPath);
-        }
-      }
-      
-      // 如果提供了工作流文件夹路径,转换为绝对路径
-      let workflowFolderArg = null;
-      if (workflowFolderPath) {
-        if (isAbsolute(workflowFolderPath)) {
-          workflowFolderArg = workflowFolderPath;
-        } else {
-          if (workflowFolderPath.startsWith('static/processing/')) {
-            const folderName = workflowFolderPath.replace('static/processing/', '');
-            workflowFolderArg = join(__dirname, '..', 'static', 'processing', folderName);
-          } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-            const folderName = workflowFolderPath.replace('static\\processing\\', '');
-            workflowFolderArg = join(__dirname, '..', 'static', 'processing', folderName);
-          } else {
-            workflowFolderArg = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-          }
-        }
-      }
-      
-      const result = await extractChatHistoryFromFunc(screenshotPath, friendAvatarArg, myAvatarArg, width, height, workflowFolderArg);
-      return result;
-    } finally {
-      // 5. 使用完后删除临时目录
-      if (tmpDir) {
-        try {
-          await rm(tmpDir, { recursive: true, force: true });
-          console.log(`已删除临时目录: ${tmpDir}`);
-        } catch (rmError) {
-          console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
-        }
-      }
-    }
-  } catch (error) {
-    console.error('提取聊天记录失败:', error);
-    if (error.message && error.message.includes('timeout')) {
-      return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
-    }
-    return { success: false, error: error.message };
-  }
-}
-
-// 获取最后一条消息(带发送者信息)
-export async function getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-
-    // 1. 获取设备分辨率
-    const resolutionResult = await getDeviceResolution(ipPort);
-    if (!resolutionResult.success) {
-      return { success: false, error: '获取设备分辨率失败' };
-    }
-    const { width, height } = resolutionResult;
-
-    // 2. 获取屏幕截图
-    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
-    if (!screenshotResult.success || !screenshotResult.data) {
-      return { success: false, error: '获取屏幕截图失败' };
-    }
-
-    // 3. 保存截图到临时文件
-    const tempDir = join(__dirname, '..');
-    const screenshotPath = join(tempDir, 'temp_screenshot_chat.png');
-    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
-    await writeFile(screenshotPath, screenshotBuffer);
-
-    // 4. 调用 JS 实现获取最后一条消息
-    // 转换头像路径为绝对路径
-    let friendAvatarArg = null;
-    if (friendAvatarPath) {
-      if (isAbsolute(friendAvatarPath)) {
-        friendAvatarArg = friendAvatarPath;
-      } else {
-        friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath);
-      }
-    }
-    
-    let myAvatarArg = null;
-    if (myAvatarPath) {
-      if (isAbsolute(myAvatarPath)) {
-        myAvatarArg = myAvatarPath;
-      } else {
-        myAvatarArg = join(__dirname, '..', 'static', 'processing', myAvatarPath);
-      }
-    }
-    
-    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
-    const normalizedFriendAvatar = friendAvatarArg ? friendAvatarArg.replace(/\\/g, '/') : null;
-    const normalizedMyAvatar = myAvatarArg ? myAvatarArg.replace(/\\/g, '/') : null;
-    
-    const result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
-    
-    if (result.success) {
-      // 确保正确显示UTF-8编码的中文
-      const displayText = result.text || '';
-      try {
-        const textStr = Buffer.isBuffer(displayText) 
-          ? displayText.toString('utf8') 
-          : String(displayText);
-        console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, textStr);
-      } catch (e) {
-        console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, displayText);
-      }
-    }
-    
-    return result;
-  } catch (error) {
-    console.error('获取最后一条消息失败:', error);
-    if (error.message && error.message.includes('timeout')) {
-      return { success: false, error: '获取最后一条消息超时,请检查网络连接或稍后重试' };
-    }
-    return { success: false, error: error.message };
-  }
-}
-
-// 读取 processing.json 文件
-export async function readProcessingJson(folderName) {
-  try {
-    const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'processing.json');
-    const jsonContent = await readFile(jsonPath, 'utf-8');
-    
-    // 解析 JSON(处理可能的格式问题,如注释、尾随逗号等)
-    // 先尝试直接解析
-    let parsed;
-    try {
-      parsed = JSON.parse(jsonContent);
-    } catch (parseError) {
-      // 如果直接解析失败,尝试清理注释和尾随逗号(简单处理)
-      let cleaned = jsonContent
-        .replace(/\/\/.*$/gm, '') // 移除单行注释
-        .replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
-        .replace(/,(\s*[}\]])/g, '$1'); // 移除尾随逗号(在 ] 或 } 之前的逗号)
-      
-      try {
-        parsed = JSON.parse(cleaned);
-      } catch (retryError) {
-        console.error(`JSON 解析失败 [${folderName}]:`, parseError.message);
-        console.error('原始内容:', jsonContent.substring(0, 500));
-        throw new Error(`JSON 格式错误: ${parseError.message}`);
-      }
-    }
-
-    // 处理不同的 JSON 格式
-    // 如果直接是数组,包装成对象
-    if (Array.isArray(parsed)) {
-      return { actions: parsed };
-    }
-    
-    // 如果已经是对象,直接返回
-    if (parsed && typeof parsed === 'object') {
-      // 如果已经有 actions 字段,直接返回
-      if (parsed.actions) {
-        return parsed;
-      }
-      // 如果没有 actions 字段,尝试查找数组字段
-      for (const key in parsed) {
-        if (Array.isArray(parsed[key])) {
-          return { actions: parsed[key], ...parsed };
-        }
-      }
-    }
-
-    // 如果解析成功但没有 actions 字段,返回错误信息
-    if (!parsed || (typeof parsed === 'object' && !parsed.actions && !Array.isArray(parsed))) {
-      console.error(`processing.json 格式错误 [${folderName}]: 缺少 actions 字段`);
-      return null;
-    }
-    
-    return parsed;
-  } catch (error) {
-    console.error(`读取 processing.json 失败 [${folderName}]:`, error.message);
-    return null;
-  }
-}
-
-// 注册 IPC 处理器
-export function registerIpcHandlers() {
-  ipcMain.handle('get-static-folders', async () => {
-    return await getStaticFolders();
-  });
-
-  ipcMain.handle('match-image-and-get-coordinate', async (event, ipPort, templateImagePath) => {
-    return await matchImageAndGetCoordinate(ipPort, templateImagePath);
-  });
-
-  ipcMain.handle('match-image-region-location', async (event, screenshotPath, regionPath, device) => {
-    return await matchImageRegionLocation(screenshotPath, regionPath, device);
-  });
-
-  ipcMain.handle('read-processing-json', async (event, folderName) => {
-    return await readProcessingJson(folderName);
-  });
-
-  ipcMain.handle('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
-    return await findTextAndGetCoordinate(ipPort, targetText);
-  });
-
-  ipcMain.handle('ocr-last-message', async (event, ipPort, method, avatarPath, area, folderPath) => {
-    return await ocrLastMessage(ipPort, method, avatarPath, area, folderPath);
-  });
-
-  ipcMain.handle('extract-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => {
-    return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath);
-  });
-
-  ipcMain.handle('get-last-chat-message', async (event, ipPort, friendAvatarPath, myAvatarPath) => {
-    return await getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath);
-  });
-
-  ipcMain.handle('save-workflow', async (event, workflowJson, imagesData) => {
-    return await saveWorkflow(workflowJson, imagesData);
-  });
-
-  ipcMain.handle('delete-workflow', async (event, folderName) => {
-    return await deleteWorkflow(folderName);
-  });
-
-  ipcMain.handle('ensure-directory', async (event, dirPath) => {
-    try {
-      await mkdir(dirPath, { recursive: true });
-      return { success: true };
-    } catch (error) {
-      return { success: false, error: error.message };
-    }
-  });
-
-  ipcMain.handle('write-text-file', async (event, filePath, content) => {
-    try {
-      await writeFile(filePath, content, 'utf-8');
-      return { success: true };
-    } catch (error) {
-      return { success: false, error: error.message };
-    }
-  });
-
-  ipcMain.handle('read-text-file', async (event, filePath) => {
-    try {
-      const content = await readFile(filePath, 'utf-8');
-      return { success: true, content };
-    } catch (error) {
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 获取最新的聊天记录文件
-  async function getLatestHistoryFile(absoluteWorkflowPath) {
-    try {
-      const historyDir = join(absoluteWorkflowPath, 'history');
-      const files = await readdir(historyDir, { withFileTypes: true });
-      
-      // 过滤出 JSON 文件(chat_*.json)
-      const jsonFiles = files
-        .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
-        .map(file => ({
-          name: file.name,
-          path: join(historyDir, file.name)
-        }));
-
-      if (jsonFiles.length === 0) {
-        return null;
-      }
-
-      // 获取所有文件的统计信息(修改时间)
-      const filesWithStats = await Promise.all(
-        jsonFiles.map(async (file) => {
-          const stats = await stat(file.path);
-          return {
-            ...file,
-            mtime: stats.mtime
-          };
-        })
-      );
-
-      // 按修改时间排序(最新的在前)
-      filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
-
-      // 返回最新的文件
-      return filesWithStats[0];
-    } catch (error) {
-      // 如果目录不存在或读取失败,返回 null
-      return null;
-    }
-  }
-
-  // 保存聊天记录到 history 文件夹
-  ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      const historyDir = join(absoluteWorkflowPath, 'history');
-      await mkdir(historyDir, { recursive: true });
-
-      // 获取最新的历史文件
-      const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
-      
-      // 如果存在最新文件,对比内容
-      if (latestFile) {
-        try {
-          const latestContent = await readFile(latestFile.path, 'utf-8');
-          const latestData = JSON.parse(latestContent);
-          
-          // 对比消息数量:如果新的消息数量没有增加,不保存
-          if (latestData.messages && Array.isArray(latestData.messages)) {
-            const latestMessageCount = latestData.messages.length;
-            const newMessageCount = historyData.messages ? historyData.messages.length : 0;
-            
-            if (newMessageCount <= latestMessageCount) {
-              console.log(`聊天记录未增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),跳过保存`);
-              return { success: true, skipped: true, reason: 'no_new_messages' };
-            }
-            
-            console.log(`聊天记录已增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),将保存`);
-          }
-        } catch (compareError) {
-          console.warn('对比历史文件失败,继续保存:', compareError);
-        }
-      }
-
-      // 生成文件名(使用时间戳)
-      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
-      const fileName = `chat_${timestamp}.json`;
-      const filePath = join(historyDir, fileName);
-
-      // 保存到文件
-      await writeFile(filePath, JSON.stringify(historyData, null, 2), 'utf-8');
-
-      console.log(`聊天记录已保存到: ${filePath}`);
-
-      // 限制 history 文件夹中最多保存 10 个文件,超过则删除最早的文件
-      try {
-        const files = await readdir(historyDir, { withFileTypes: true });
-        // 过滤出 JSON 文件(chat_*.json)
-        const jsonFiles = files
-          .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
-          .map(file => ({
-            name: file.name,
-            path: join(historyDir, file.name)
-          }));
-
-        // 如果文件数量超过 10 个,删除最早的文件
-        if (jsonFiles.length > 10) {
-          // 获取所有文件的统计信息(创建时间或修改时间)
-          const filesWithStats = await Promise.all(
-            jsonFiles.map(async (file) => {
-              const stats = await stat(file.path);
-              return {
-                ...file,
-                birthtime: stats.birthtime || stats.mtime, // 使用创建时间,如果没有则使用修改时间
-                mtime: stats.mtime
-              };
-            })
-          );
-
-          // 按时间排序(最早的在前)
-          filesWithStats.sort((a, b) => {
-            const timeA = a.birthtime.getTime();
-            const timeB = b.birthtime.getTime();
-            return timeA - timeB;
-          });
-
-          // 删除最早的文件,直到只剩下 10 个
-          const filesToDelete = filesWithStats.slice(0, filesWithStats.length - 10);
-          for (const file of filesToDelete) {
-            try {
-              await rm(file.path, { force: true });
-              console.log(`已删除最早的聊天记录文件: ${file.name}`);
-            } catch (deleteError) {
-              console.warn(`删除文件失败: ${file.name}`, deleteError);
-            }
-          }
-        }
-      } catch (cleanupError) {
-        // 清理失败不影响保存操作
-        console.warn('清理旧聊天记录文件时出错:', cleanupError);
-      }
-
-      return { success: true, filePath };
-    } catch (error) {
-      console.error('保存聊天记录失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 保存聊天记录总结
-  ipcMain.handle('save-chat-history-summary', async (event, workflowFolderPath, summary) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      const historyDir = join(absoluteWorkflowPath, 'history');
-      await mkdir(historyDir, { recursive: true });
-
-      const summaryFilePath = join(historyDir, 'summary.txt');
-      await writeFile(summaryFilePath, summary, 'utf-8');
-
-      console.log(`聊天记录总结已保存到: ${summaryFilePath}`);
-      return { success: true };
-    } catch (error) {
-      console.error('保存聊天记录总结失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 读取聊天记录总结
-  ipcMain.handle('get-chat-history-summary', async (event, workflowFolderPath) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      const summaryFilePath = join(absoluteWorkflowPath, 'history', 'summary.txt');
-
-      // 检查文件是否存在
-      try {
-        const { access, constants } = await import('fs/promises');
-        await access(summaryFilePath, constants.F_OK);
-        const summary = await readFile(summaryFilePath, 'utf-8');
-        return { success: true, summary: summary.trim() };
-      } catch (error) {
-        // 文件不存在,返回空字符串
-        return { success: true, summary: '' };
-      }
-    } catch (error) {
-      console.error('读取聊天记录总结失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 读取最新聊天记录的所有消息
-  ipcMain.handle('read-latest-chat-history', async (event, workflowFolderPath) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      // 获取最新的聊天记录文件
-      const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
-      
-      if (!latestFile) {
-        return { success: false, error: '未找到聊天记录文件' };
-      }
-
-      // 读取文件内容
-      const fileContent = await readFile(latestFile.path, 'utf-8');
-      const historyData = JSON.parse(fileContent);
-
-      // 返回消息数组
-      return { 
-        success: true, 
-        messages: historyData.messages || [] 
-      };
-    } catch (error) {
-      console.error('读取最新聊天记录失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 读取最新聊天记录的最后一条消息
-  ipcMain.handle('read-last-message', async (event, workflowFolderPath) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      // 获取最新的聊天记录文件
-      const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
-      
-      if (!latestFile) {
-        return { success: false, error: '未找到聊天记录文件' };
-      }
-
-      // 读取文件内容
-      const fileContent = await readFile(latestFile.path, 'utf-8');
-      const historyData = JSON.parse(fileContent);
-
-      // 获取最后一条消息
-      const messages = historyData.messages || [];
-      if (messages.length === 0) {
-        return { success: false, error: '聊天记录为空' };
-      }
-
-      const lastMessage = messages[messages.length - 1];
-
-      // 返回最后一条消息的文本和发送者
-      return { 
-        success: true, 
-        text: lastMessage.text || '', 
-        sender: lastMessage.sender || '' 
-      };
-    } catch (error) {
-      console.error('读取最后一条消息失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-
-  // 读取所有聊天记录(合并所有历史文件)
-  ipcMain.handle('read-all-chat-history', async (event, workflowFolderPath) => {
-    try {
-      // 将相对路径转换为绝对路径
-      let absoluteWorkflowPath = workflowFolderPath;
-      if (!isAbsolute(workflowFolderPath)) {
-        if (workflowFolderPath.startsWith('static/processing/')) {
-          const folderName = workflowFolderPath.replace('static/processing/', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
-          const folderName = workflowFolderPath.replace('static\\processing\\', '');
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-        } else {
-          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
-        }
-      }
-
-      const historyDir = join(absoluteWorkflowPath, 'history');
-      
-      // 检查目录是否存在
-      try {
-        const files = await readdir(historyDir, { withFileTypes: true });
-        
-        // 过滤出 JSON 文件(chat_*.json)
-        const jsonFiles = files
-          .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
-          .map(file => ({
-            name: file.name,
-            path: join(historyDir, file.name)
-          }));
-
-        if (jsonFiles.length === 0) {
-          return { success: true, messages: [] }; // 没有文件时返回空数组
-        }
-
-        // 获取所有文件的统计信息(修改时间),用于排序
-        const filesWithStats = await Promise.all(
-          jsonFiles.map(async (file) => {
-            const stats = await stat(file.path);
-            return {
-              ...file,
-              mtime: stats.mtime
-            };
-          })
-        );
-
-        // 按修改时间排序(最早的在前,保持时间顺序)
-        filesWithStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
-
-        // 读取所有文件并合并消息
-        const allMessages = [];
-        for (const file of filesWithStats) {
-          try {
-            const fileContent = await readFile(file.path, 'utf-8');
-            const historyData = JSON.parse(fileContent);
-            const messages = historyData.messages || [];
-            allMessages.push(...messages);
-          } catch (error) {
-            console.warn(`读取聊天记录文件失败: ${file.name}`, error);
-            // 继续处理其他文件
-          }
-        }
-
-        return { 
-          success: true, 
-          messages: allMessages,
-          fileCount: filesWithStats.length,
-          totalMessages: allMessages.length
-        };
-      } catch (error) {
-        // 目录不存在或读取失败
-        if (error.code === 'ENOENT') {
-          return { success: true, messages: [] }; // 目录不存在时返回空数组
-        }
-        throw error;
-      }
-    } catch (error) {
-      console.error('读取所有聊天记录失败:', error);
-      return { success: false, error: error.message };
-    }
-  });
-}
-
-// 保存工作流到 static/processing 目录
-export async function saveWorkflow(workflowJson, imagesData = []) {
-  try {
-    // 支持新旧格式
-    const hasActions = Array.isArray(workflowJson.actions) || Array.isArray(workflowJson);
-    if (!workflowJson || typeof workflowJson !== 'object' || !hasActions) {
-      return { success: false, error: '工作流格式错误:缺少 actions 数组' };
-    }
-
-    // 生成文件夹名称(使用时间戳)
-    const now = new Date();
-    const timestamp = now.getFullYear() + 
-                     String(now.getMonth() + 1).padStart(2, '0') + 
-                     String(now.getDate()).padStart(2, '0') + '_' +
-                     String(now.getHours()).padStart(2, '0') + 
-                     String(now.getMinutes()).padStart(2, '0') + 
-                     String(now.getSeconds()).padStart(2, '0');
-    const folderName = workflowJson.name || `工作流_${timestamp}`;
-
-    // 创建工作流文件夹
-    const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-    await mkdir(workflowPath, { recursive: true });
-
-    // 保存 processing.json
-    const jsonPath = join(workflowPath, 'processing.json');
-    const jsonContent = JSON.stringify(workflowJson, null, '\t');
-    await writeFile(jsonPath, jsonContent, 'utf-8');
-
-    // 保存图片
-    if (imagesData && Array.isArray(imagesData) && imagesData.length > 0) {
-      for (const imageData of imagesData) {
-        if (imageData.base64 && imageData.name) {
-          try {
-            // 将base64转换为Buffer
-            const imageBuffer = Buffer.from(imageData.base64, 'base64');
-            const imagePath = join(workflowPath, imageData.name);
-            await writeFile(imagePath, imageBuffer);
-            console.log(`图片已保存: ${imageData.name}`);
-          } catch (imageError) {
-            console.error(`保存图片失败 ${imageData.name}:`, imageError);
-          }
-        }
-      }
-    }
-
-    console.log(`工作流已保存: ${folderName}`);
-    return { success: true, folderName, path: workflowPath };
-  } catch (error) {
-    console.error('保存工作流失败:', error);
-    return { success: false, error: error.message };
-  }
-}
-
-// 删除工作流文件夹
-export async function deleteWorkflow(folderName) {
-  try {
-    if (!folderName || typeof folderName !== 'string') {
-      return { success: false, error: '文件夹名称无效' };
-    }
-
-    // 构建文件夹路径
-    const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
-
-    // 删除整个文件夹(包括所有内容)
-    await rm(workflowPath, { recursive: true, force: true });
-
-    console.log(`工作流已删除: ${folderName}`);
-    return { success: true, folderName };
-  } catch (error) {
-    console.error('删除工作流失败:', error);
-    return { success: false, error: error.message };
-  }
-}

+ 187 - 55
main-js/read-and-write.js

@@ -4,12 +4,12 @@
  */
 
 import { ipcMain } from 'electron';
-import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
+import { readdir, writeFile, readFile, mkdir, rm, stat, appendFile } from 'fs/promises';
 import { join, dirname, isAbsolute } from 'path';
 import { fileURLToPath } from 'url';
 import { captureScreenshot } from './adb/screenshot.js';
 import { getDeviceResolution } from './adb/device-info.js';
-import { extractChatHistory as extractChatHistoryFromFunc, getLastMessage as getLastMessageFromFunc, ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat-history.js';
+import { extractChatHistory as extractChatHistoryFromFunc, getLastMessage as getLastMessageFromFunc, ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
@@ -113,6 +113,33 @@ export function registerIpcHandlers() {
     }
   });
 
+  // 追加日志到文件
+  ipcMain.handle('append-log', async (event, workflowFolderPath, logMessage) => {
+    try {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
+      const logFilePath = join(absoluteWorkflowPath, 'log.txt');
+      
+      // 添加时间戳(精确到毫秒)
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, '0');
+      const day = String(now.getDate()).padStart(2, '0');
+      const hour = String(now.getHours()).padStart(2, '0');
+      const minute = String(now.getMinutes()).padStart(2, '0');
+      const second = String(now.getSeconds()).padStart(2, '0');
+      const ms = String(now.getMilliseconds()).padStart(3, '0');
+      const timestamp = `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`;
+      const logLine = `[${timestamp}] ${logMessage}\n`;
+      
+      await appendFile(logFilePath, logLine, 'utf-8');
+      return { success: true };
+    } catch (error) {
+      // 日志写入失败不应该影响主流程,只记录错误
+      console.error('追加日志失败:', error);
+      return { success: false, error: error.message };
+    }
+  });
+
   // 保存聊天记录到 history 文件夹
   ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => {
     try {
@@ -123,40 +150,60 @@ export function registerIpcHandlers() {
       // 获取最新的历史文件
       const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
       
-      // 如果存在最新文件,对比内容
+      let targetFilePath = null;
+      let mergedMessages = [];
+      
+      // 如果存在最新文件,检查是否可以追加
       if (latestFile) {
         try {
-          const latestContent = await readFile(latestFile.path, 'utf-8');
-          const latestData = JSON.parse(latestContent);
+          const latestStats = await stat(latestFile.path);
+          const latestFileSize = latestStats.size; // 文件大小(字节)
           
-          // 对比消息数量:如果新的消息数量没有增加,不保存
-          if (latestData.messages && Array.isArray(latestData.messages)) {
-            const latestMessageCount = latestData.messages.length;
-            const newMessageCount = historyData.messages ? historyData.messages.length : 0;
+          // 检查文件大小是否超过1MB(1MB = 1024 * 1024 字节)
+          if (latestFileSize < 1024 * 1024) {
+            // 文件大小未超过1MB,可以追加
+            const latestContent = await readFile(latestFile.path, 'utf-8');
+            const latestData = JSON.parse(latestContent);
+            const latestMessages = latestData.messages || [];
+            const newMessages = historyData.messages || [];
             
-            if (newMessageCount <= latestMessageCount) {
-              console.log(`聊天记录未增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),跳过保存`);
-              return { success: true, skipped: true, reason: 'no_new_messages' };
-            }
+            // 简单合并消息(不去重,去重逻辑在 saveChatHistory 中处理)
+            mergedMessages = [...latestMessages, ...newMessages];
+            targetFilePath = latestFile.path;
             
-            console.log(`聊天记录已增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),将保存`);
+          } else {
+            // 文件大小超过1MB,创建新文件
+            mergedMessages = historyData.messages || [];
           }
         } catch (compareError) {
-          console.warn('对比历史文件失败,继续保存:', compareError);
+          console.warn('读取最新文件失败,将创建新文件:', compareError);
+          mergedMessages = historyData.messages || [];
         }
+      } else {
+        // 不存在历史文件,直接使用新消息
+        mergedMessages = historyData.messages || [];
       }
 
-      // 生成文件名(使用时间戳)
-      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
-      const fileName = `chat_${timestamp}.json`;
-      const filePath = join(historyDir, fileName);
+      // 如果还没有确定目标文件路径,创建新文件
+      if (!targetFilePath) {
+        const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+        const fileName = `chat_${timestamp}.json`;
+        targetFilePath = join(historyDir, fileName);
+      }
+
+      // 构建保存的数据结构
+      const now = new Date();
+      const finalData = {
+        timestamp: now.toISOString(),
+        messageCount: mergedMessages.length,
+        messages: mergedMessages
+      };
 
       // 保存到文件
-      await writeFile(filePath, JSON.stringify(historyData, null, 2), 'utf-8');
+      await writeFile(targetFilePath, JSON.stringify(finalData, null, 2), 'utf-8');
 
-      console.log(`聊天记录已保存到: ${filePath}`);
 
-      // 限制 history 文件夹中最多保存 10 个文件,超过则删除最早的文件
+      // 限制 history 文件夹总大小不超过10MB,超过则删除最早的文件
       try {
         const files = await readdir(historyDir, { withFileTypes: true });
         // 过滤出 JSON 文件(chat_*.json)
@@ -167,44 +214,59 @@ export function registerIpcHandlers() {
             path: join(historyDir, file.name)
           }));
 
-        // 如果文件数量超过 10 个,删除最早的文件
-        if (jsonFiles.length > 10) {
-          // 获取所有文件的统计信息(创建时间或修改时间)
-          const filesWithStats = await Promise.all(
-            jsonFiles.map(async (file) => {
-              const stats = await stat(file.path);
-              return {
-                ...file,
-                birthtime: stats.birthtime || stats.mtime,
-                mtime: stats.mtime
-              };
-            })
-          );
-
-          // 按时间排序(最早的在前)
-          filesWithStats.sort((a, b) => {
-            const timeA = a.birthtime.getTime();
-            const timeB = b.birthtime.getTime();
-            return timeA - timeB;
-          });
-
-          // 删除最早的文件,直到只剩下 10 个
-          const filesToDelete = filesWithStats.slice(0, filesWithStats.length - 10);
-          for (const file of filesToDelete) {
-            try {
-              await rm(file.path, { force: true });
-              console.log(`已删除最早的聊天记录文件: ${file.name}`);
-            } catch (deleteError) {
-              console.warn(`删除文件失败: ${file.name}`, deleteError);
-            }
+        // 获取所有文件的大小和统计信息
+        const filesWithStats = await Promise.all(
+          jsonFiles.map(async (file) => {
+            const stats = await stat(file.path);
+            return {
+              ...file,
+              size: stats.size,
+              birthtime: stats.birthtime || stats.mtime,
+              mtime: stats.mtime
+            };
+          })
+        );
+
+        // 计算总大小
+        let totalSize = filesWithStats.reduce((sum, file) => sum + file.size, 0);
+        const maxSize = 10 * 1024 * 1024; // 10MB
+
+        // 按时间排序(最早的在前)
+        filesWithStats.sort((a, b) => {
+          const timeA = a.birthtime.getTime();
+          const timeB = b.birthtime.getTime();
+          return timeA - timeB;
+        });
+
+        // 如果总大小超过10MB,删除最早的文件直到总大小小于10MB
+        const filesToDelete = [];
+        for (const file of filesWithStats) {
+          if (totalSize <= maxSize) {
+            break;
+          }
+          filesToDelete.push(file);
+          totalSize -= file.size;
+        }
+
+        // 删除文件
+        for (const file of filesToDelete) {
+          try {
+            await rm(file.path, { force: true });
+            // 已删除临时文件日志(不显示)
+          } catch (deleteError) {
+            console.warn(`删除文件失败: ${file.name}`, deleteError);
           }
         }
+
+        if (filesToDelete.length > 0) {
+          console.log(`已清理 ${filesToDelete.length} 个文件,当前总大小: ${((totalSize) / 1024 / 1024).toFixed(2)}MB`);
+        }
       } catch (cleanupError) {
         // 清理失败不影响保存操作
         console.warn('清理旧聊天记录文件时出错:', cleanupError);
       }
 
-      return { success: true, filePath };
+      return { success: true, filePath: targetFilePath };
     } catch (error) {
       console.error('保存聊天记录失败:', error);
       return { success: false, error: error.message };
@@ -314,6 +376,76 @@ export function registerIpcHandlers() {
     }
   });
 
+  // 读取所有聊天记录(合并所有历史文件)- 向后兼容
+  ipcMain.handle('read-all-chat-history', async (event, workflowFolderPath) => {
+    // 向后兼容,重定向到 read-chat-history(使用相同的实现)
+    try {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
+      const historyDir = join(absoluteWorkflowPath, 'history');
+      
+      // 检查目录是否存在
+      try {
+        const files = await readdir(historyDir, { withFileTypes: true });
+        
+        // 过滤出 JSON 文件(chat_*.json)
+        const jsonFiles = files
+          .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
+          .map(file => ({
+            name: file.name,
+            path: join(historyDir, file.name)
+          }));
+
+        if (jsonFiles.length === 0) {
+          return { success: true, messages: [] }; // 没有文件时返回空数组
+        }
+
+        // 获取所有文件的统计信息(修改时间),用于排序
+        const filesWithStats = await Promise.all(
+          jsonFiles.map(async (file) => {
+            const stats = await stat(file.path);
+            return {
+              ...file,
+              mtime: stats.mtime
+            };
+          })
+        );
+
+        // 按修改时间排序(最早的在前,保持时间顺序)
+        filesWithStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
+
+        // 读取所有文件并合并消息
+        const allMessages = [];
+        for (const file of filesWithStats) {
+          try {
+            const fileContent = await readFile(file.path, 'utf-8');
+            const historyData = JSON.parse(fileContent);
+            const messages = historyData.messages || [];
+            allMessages.push(...messages);
+          } catch (error) {
+            console.warn(`读取聊天记录文件失败: ${file.name}`, error);
+            // 继续处理其他文件
+          }
+        }
+
+        return { 
+          success: true, 
+          messages: allMessages,
+          fileCount: filesWithStats.length,
+          totalMessages: allMessages.length
+        };
+      } catch (error) {
+        // 目录不存在或读取失败
+        if (error.code === 'ENOENT') {
+          return { success: true, messages: [] }; // 目录不存在时返回空数组
+        }
+        throw error;
+      }
+    } catch (error) {
+      console.error('读取所有聊天记录失败:', error);
+      return { success: false, error: error.message };
+    }
+  });
+
   // 读取聊天记录(合并所有历史文件)
   ipcMain.handle('read-chat-history', async (event, workflowFolderPath) => {
     try {
@@ -461,7 +593,7 @@ export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath,
     try {
       const { access, constants } = await import('fs/promises');
       await access(screenshotPath, constants.F_OK);
-      console.log(`截图已保存到: ${screenshotPath}`);
+      // 截图已保存日志(不显示)
     } catch (err) {
       console.error(`截图文件写入验证失败: ${screenshotPath}`, err);
       return { success: false, error: `截图文件写入失败: ${err.message}` };
@@ -513,7 +645,7 @@ export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath,
       if (tmpDir) {
         try {
           await rm(tmpDir, { recursive: true, force: true });
-          console.log(`已删除临时目录: ${tmpDir}`);
+          // 已删除临时目录日志(不显示)
         } catch (rmError) {
           console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
         }

+ 1 - 1
main-js/workflow.js

@@ -146,7 +146,7 @@ export async function saveWorkflow(workflowJson, imagesData = []) {
             const imageBuffer = Buffer.from(imageData.base64, 'base64');
             const imagePath = join(workflowPath, imageData.name);
             await writeFile(imagePath, imageBuffer);
-            console.log(`图片已保存: ${imageData.name}`);
+            // 图片已保存日志(不显示)
           } catch (imageError) {
             console.error(`保存图片失败 ${imageData.name}:`, imageError);
           }

+ 2 - 0
preload.cjs

@@ -48,6 +48,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
   writeTextFile: (filePath, content) => ipcRenderer.invoke('write-text-file', filePath, content),
   // 读取文本文件
   readTextFile: (filePath) => ipcRenderer.invoke('read-text-file', filePath),
+  // 追加日志到文件
+  appendLog: (workflowFolderPath, logMessage) => ipcRenderer.invoke('append-log', workflowFolderPath, logMessage),
   // 保存聊天记录
   saveChatHistory: (workflowFolderPath, historyData) => ipcRenderer.invoke('save-chat-history', workflowFolderPath, historyData),
   // 保存聊天记录总结

+ 1 - 1
py/OnnxOCR

@@ -1 +1 @@
-Subproject commit 0274df695d9a7f694ffa4649ac70093522010b7a
+Subproject commit ade2ead0c5830ceb30666fc33be8e0f9eb8b97ed

+ 5 - 604
src/pages/Chat/Input/Input.js

@@ -1,4 +1,6 @@
 import { useState, useRef } from 'react';
+import { analyzeRequirements } from './resource-require.js';
+import { generateProcessing, extractWorkflowJsonFromText, updateImagePathsInWorkflow } from './generate-processing.js';
 
 // useInput - 管理输入组件的状态和逻辑
 export function useInput(onSendMessage, onLoadingChange) {
@@ -117,278 +119,7 @@ export function useInput(onSendMessage, onLoadingChange) {
   };
 }
 
-// InputLogic - 处理消息发送到GPT API
-
-// 第一阶段提示词:分析需求,返回需要的信息
-const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手。你需要像聊天一样与用户对话。
-
-如果用户想要创建自动化工作流,你需要分析需求并返回需要的信息。请用自然、友好的语言与用户交流,保持对话的连贯性和友好性。
-
-语法分层(重要):
-- 基础语法(控制流/定时):scheduleOnce、schedule、for、while、if
-- 基础 action(模拟手机操作):press、swipe、scroll
-- 扩展标签(Func):除基础能力外的所有扩展功能,全部由 src/pages/processing/func/ 目录下的脚本文件决定;每个脚本文件名就是一个"标签"。
-
-在生成工作流时:
-- 逻辑结构尽量用 for/while/if + triggers(schedules) 表达
-- 具体手机操作尽量落到 press/swipe/scroll
-- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:ocr-chat-history、string-reg-location、save-new-chat)
-
-(实现层说明:项目内部为兼容与落地,仍存在 locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。)
-
-9. if - 条件判断
-   - condition: 条件表达式(如 "{count} > 5")
-   - then: 条件为真时执行的操作数组
-   - else: 条件为假时执行的操作数组(可选)
-
-10. for - 循环
-    - variable: 循环变量名
-    - items: 数组(可以是变量 {arrayVariable})
-    - body: 循环体操作数组
-
-11. delay - 延迟
-    - value: 延迟时间(如 "2s", "1m", "30 minutes")
-
-12. set - 设置变量
-    - variable: 变量名
-    - value: 变量值
-
-13. scroll/swipe - 滚动/滑动
-    - value: 方向("up-down", "down-up", "left-right", "right-left")
-
-定时任务配置:
-- 单次执行: {"datetime": "2026/1/14 01:21"}
-- 每天执行: {"time": "09:00", "repeat": "daily"}
-- 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
-- 间隔执行: {"interval": "30 minutes"}
-
-重要规则:
-- 变量使用 {variableName} 格式引用
-- 根据用户描述合理设置delay延迟时间
-- 如果用户提到循环、条件判断,使用for/if操作
-- 如果用户提到定时执行,添加triggers配置
-- 如果用户提到AI生成内容,使用ai-generate操作
-- 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
-- 图片文件名按顺序命名为"1.png"、"2.png"等
-- **特别重要**:如果工作流中需要输入文字(使用 input 或 ai-generate 操作),且这些文字需要根据场景背景和用户角色来生成,你必须询问用户以下信息:
-  - 场景的背景:这个工作流在什么场景下使用?(例如:微信聊天、客服回复、评论互动等)
-  - 用户的角色:用户在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
-  - 这些信息对于生成合适的文字内容非常重要,请务必在询问需求时主动询问
-
-请分析用户的需求,根据上述操作类型,判断需要哪些图片和文字参考:
-- 图片(needsImages):用于 locate/click 的 image 方法,或 ocr 的 by-avatar 方法
-- 文字参考(needsTexts):用于 locate/click 的 text 方法
-
-如果用户想要创建自动化工作流:
-1. 先友好地确认理解用户的需求
-2. 然后返回JSON格式的需求信息(不包含在自然语言回复中,仅作为JSON返回)
-
-需求分析的JSON格式:
-{
-  "needsImages": 2,  // 需要多少张图片(用于图像匹配定位)
-  "needsTexts": 1,   // 需要多少个文字参考(用于文字识别定位)
-  "imageDescriptions": ["登录按钮", "用户名输入框"],  // 每张图片的描述
-  "textDescriptions": ["登录按钮文字"]  // 每个文字参考的描述
-}
-
-返回格式:先自然语言回复,然后单独一行JSON。
-
-**特别提醒**:如果工作流涉及输入文字(如自动回复、自动评论、自动发送消息等),请务必询问用户场景背景和角色信息。
-
-例如(不涉及文字输入的场景):
-我理解了,您想创建一个登录流程的自动化工作流。为了生成准确的工作流配置,我需要您提供以下信息:
-
-📋 需要的信息:
-🖼️ 需要 2 张图片:登录按钮截图、用户名输入框截图
-📝 需要 1 个文字参考:登录按钮上的文字
-
-\`\`\`json
-{
-  "needsImages": 2,
-  "needsTexts": 1,
-  "imageDescriptions": ["登录按钮", "用户名输入框"],
-  "textDescriptions": ["登录按钮文字"]
-}
-\`\`\`
-
-例如(涉及文字输入的场景):
-我理解了,您想创建一个自动聊天回复的自动化工作流。为了生成准确且符合场景的文字内容,我需要了解以下信息:
-
-📋 需要的信息:
-🖼️ 需要 2 张图片:好友头像截图、我的头像截图
-📝 需要 1 个文字参考:输入框位置
-💬 **场景信息**(重要):
-   - 场景背景:这个聊天是在什么场景下进行的?(例如:微信聊天、QQ群聊、客服对话等)
-   - 您的角色:您在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
-
-\`\`\`json
-{
-  "needsImages": 2,
-  "needsTexts": 1,
-  "imageDescriptions": ["好友头像", "我的头像"],
-  "textDescriptions": ["输入框位置"]
-}
-\`\`\`
-
-如果用户的话题与工作流无关,请友好地自然回复,不需要返回JSON。如果用户是在询问如何使用、或者其他一般性问题,请用自然语言友好地回答。`;
-
-// 第二阶段提示词:生成工作流
-const SYSTEM_PROMPT = `你是一个友好的自动化工作流生成助手。用户已经提供了所需的图片和文字参考,现在你需要生成JSON格式的工作流配置文件。
-
-请用自然、友好的语言与用户交流,保持对话的连贯性。在生成工作流时,请先简短地确认一下,然后生成JSON格式的工作流配置。
-
-工作流JSON格式(新版本):
-{
-  "version": "1.0",
-  "name": "工作流名称",
-  "triggers": [
-    {
-      "type": "schedule",
-      "schedule": {
-        "time": "09:00",
-        "weekdays": ["monday", "wednesday", "friday"]
-      }
-    }
-  ],
-  "variables": {
-    "topic": "美食"
-  },
-  "actions": [
-    {
-      "type": "locate",
-      "method": "image",
-      "target": "button.png",
-      "variable": "buttonPos",
-      "delay": "1s"
-    },
-    {
-      "type": "click",
-      "method": "position",
-      "target": "{buttonPos}",
-      "delay": "2s"
-    },
-    {
-      "type": "input",
-      "target": "输入框",
-      "value": "文字内容",
-      "delay": "1s"
-    },
-    {
-      "type": "ai-generate",
-      "prompt": "生成关于{topic}的文章",
-      "variable": "article",
-      "delay": "10s"
-    },
-    {
-      "type": "if",
-      "condition": "{count} > 5",
-      "then": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "确定"
-        }
-      ]
-    },
-    {
-      "type": "for",
-      "variable": "item",
-      "items": ["选项1", "选项2"],
-      "body": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "{item}"
-        }
-      ]
-    }
-  ]
-}
-
-核心操作类型:
-1. locate - 定位元素
-   - method: "image"(图像匹配)| "text"(文字识别)| "coordinate"(坐标)
-   - target: 目标内容
-   - variable: 保存位置到变量
-
-2. click - 点击
-   - method: "position" | "image" | "text" | "coordinate"
-   - target: 目标(可以是变量 {variable})
-
-3. input - 输入文字
-   - target: 输入框位置(文字或变量)
-   - value: 要输入的文本(可以是变量 {variable})
-
-4. ai-generate - AI生成内容
-   - prompt: 提示词(支持变量 {variable})
-   - variable: 保存结果到变量
-
-5. ocr - 文字识别
-   - method: "by-avatar" | "by-position" | "full-screen"
-   - avatar: 头像图片(by-avatar时)
-   - variable: 保存识别结果
-
-6. extract-messages - 提取消息记录
-   - avatar1: 第一个参与者的头像/标识图片路径
-   - avatar2: 第二个参与者的头像/标识图片路径
-   - variable: 保存消息记录到变量(格式:对方: 消息\n我: 消息)
-
-7. save-messages - 保存消息记录
-   - variable: 包含消息记录的变量名(保存到 history 文件夹)
-
-8. generate-summary - 生成消息总结
-   - variable: 包含消息记录的变量名
-   - summaryVariable: 保存总结结果的变量名
-   - model: AI模型名称(可选)
-
-9. if - 条件判断
-   - condition: 条件表达式(如 "{count} > 5")
-   - then: 条件为真时执行的操作数组
-   - else: 条件为假时执行的操作数组(可选)
-
-10. for - 循环
-    - variable: 循环变量名
-    - items: 数组(可以是变量 {arrayVariable})
-    - body: 循环体操作数组
-
-11. delay - 延迟
-    - value: 延迟时间(如 "2s", "1m", "30 minutes")
-
-12. set - 设置变量
-    - variable: 变量名
-    - value: 变量值
-
-13. scroll/swipe - 滚动/滑动
-    - value: 方向("up-down", "down-up", "left-right", "right-left")
-
-定时任务配置:
-- 单次执行: {"datetime": "2026/1/14 01:21"}
-- 每天执行: {"time": "09:00", "repeat": "daily"}
-- 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
-- 间隔执行: {"interval": "30 minutes"}
-
-重要规则:
-- 使用新格式(type字段),但保持向后兼容旧格式
-- 变量使用 {variableName} 格式引用
-- 根据用户描述合理设置delay延迟时间
-- 如果用户提到循环、条件判断,使用for/if操作
-- 如果用户提到定时执行,添加triggers配置
-- 如果用户提到AI生成内容,使用ai-generate操作
-- 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
-- 在ai-generate的prompt中可以使用总结变量引用历史消息记录的总结
-- 图片文件名按顺序命名为"1.png"、"2.png"等
-
-返回格式:
-1. 如果用户已经提供了所有需要的图片和文字参考,且明确要求生成工作流:
-   - 先用简短的自然语言确认(如"好的,我现在为您生成工作流配置...")
-   - 然后单独一行返回JSON格式的工作流配置(可以用代码块包裹)
-   
-2. 如果用户只是在询问问题或者还没准备好生成工作流:
-   - 用自然语言友好地回答用户的问题
-   - 不需要返回JSON
-
-请保持对话的自然和友好,不要生硬地只返回JSON。`;
-
+// InputLogic - 处理消息发送和图片管理
 export function InputLogic(onSendMessage, onLoadingChange) {
   // 存储上传的图片
   const [uploadedImages, setUploadedImages] = useState([]);
@@ -447,6 +178,7 @@ export function InputLogic(onSendMessage, onLoadingChange) {
       return null;
     }
   };
+
   // 存储工作流需求信息(需要多少图片和文字)
   const [workflowRequirements, setWorkflowRequirements] = useState(null);
   // 存储用户输入的文字参考(用于文字识别)
@@ -495,206 +227,6 @@ export function InputLogic(onSendMessage, onLoadingChange) {
     }
   };
 
-  // 根据用户描述给图片命名(使用AI)
-  const nameImagesWithAI = async (userPrompt, images) => {
-    if (!images || images.length === 0) return images;
-
-    try {
-      // 构建提示词,让AI根据用户描述给图片命名
-      const imagePrompt = `用户上传了 ${images.length} 张图片,用于工作流自动化。请根据用户的描述,为每张图片生成一个简洁的描述性文件名(只返回JSON数组,不要其他文字)。
-
-用户描述:${userPrompt}
-
-图片信息:
-${images.map((img, index) => `图片${index + 1}: 原始文件名 ${img.originalName}`).join('\n')}
-
-请返回JSON数组,格式如下:
-["登录按钮.png", "用户名输入框.png", "密码输入框.png"]
-
-要求:
-1. 文件名要简洁,能清楚描述图片内容
-2. 使用中文或英文都可以
-3. 必须包含文件扩展名(.png)
-4. 文件名要符合工作流中使用的命名规范`;
-
-      const response = await fetch('https://ai-anim.com/api/text2textByModel', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          prompt: imagePrompt,
-          modelName: 'gpt-5-nano-ca'
-        })
-      });
-
-      if (!response.ok) {
-        throw new Error('AI命名失败');
-      }
-
-      const data = await response.json();
-      const responseText = data.data?.output_text || data.text || data.content || '';
-      
-      // 解析AI返回的文件名数组
-      let imageNames = [];
-      try {
-        // 尝试直接解析JSON
-        imageNames = JSON.parse(responseText.trim());
-      } catch (e) {
-        // 尝试从代码块中提取
-        const match = responseText.match(/\[[\s\S]*?\]/);
-        if (match) {
-          imageNames = JSON.parse(match[0]);
-        } else {
-          // 如果解析失败,使用默认命名
-          imageNames = images.map((img, index) => `${index + 1}.png`);
-        }
-      }
-
-      // 更新图片信息,添加AI生成的文件名
-      return images.map((img, index) => ({
-        ...img,
-        aiName: imageNames[index] || `${index + 1}.png`
-      }));
-    } catch (error) {
-      console.error('AI命名图片失败:', error);
-      // 如果AI命名失败,使用默认命名
-      return images.map((img, index) => ({
-        ...img,
-        aiName: `${index + 1}.png`
-      }));
-    }
-  };
-
-  const sendToGPT = async (userPrompt, images = []) => {
-    // 构建图片信息提示(如果图片已经命名)
-    let imageInfoPrompt = '';
-    if (images.length > 0) {
-      imageInfoPrompt = `\n\n用户上传了以下图片,请在生成工作流时使用这些图片文件名:\n${images.map((img, index) => `${index + 1}. ${img.aiName || img.tempName} - ${img.originalName}`).join('\n')}\n\n在生成工作流时,如果操作需要点击图片,请使用上述图片文件名(如 "登录按钮.png")。`;
-    }
-
-    // 构建完整的提示词:系统提示词 + 图片信息 + 用户输入
-    const fullPrompt = `${SYSTEM_PROMPT}${imageInfoPrompt}\n\n用户需求:${userPrompt}`;
-      
-      const requestBody = {
-        input: fullPrompt
-      };
-      
-      console.log('Sending request to Doubao API:', requestBody);
-
-      const response = await fetch('https://ai-anim.com/api/doubaoText2text', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify(requestBody),
-      });
-
-      console.log('Response status:', response.status, response.statusText);
-
-      if (!response.ok) {
-        let errorText;
-        try {
-          errorText = await response.text();
-          // 尝试解析为 JSON
-          try {
-            const errorJson = JSON.parse(errorText);
-            console.error('API Error Response (JSON):', errorJson);
-            // 如果 JSON 中有 error 字段,使用它
-            if (errorJson.error) {
-              throw new Error(`服务器错误: ${errorJson.error}`);
-            }
-            if (errorJson.message) {
-              throw new Error(`服务器错误: ${errorJson.message}`);
-            }
-          } catch (parseError) {
-            // 不是 JSON,使用原始文本
-            console.error('API Error Response (Text):', errorText);
-          }
-        } catch (readError) {
-          errorText = `无法读取错误响应: ${readError.message}`;
-        }
-        throw new Error(`HTTP ${response.status}: ${errorText}`);
-      }
-
-      const data = await response.json();
-      console.log('API Response:', data);
-    
-      return data;
-  };
-
-  // 分析需求,返回需要的信息
-  const analyzeRequirements = async (userPrompt) => {
-    const response = await fetch('https://ai-anim.com/api/text2textByModel', {
-      method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({
-        prompt: `${REQUIREMENTS_PROMPT}\n\n用户需求:${userPrompt}`,
-        modelName: 'gpt-5-nano-ca'
-      })
-    });
-
-    if (!response.ok) {
-      throw new Error('分析需求失败');
-    }
-
-    const data = await response.json();
-    const responseText = data.data?.output_text || data.text || data.content || '';
-    
-    // 提取自然语言回复(去除JSON部分)
-    let naturalLanguageReply = responseText.trim();
-    
-    // 尝试从回复中提取JSON(可能在代码块中,也可能单独一行)
-    let requirements = null;
-    
-    // 方法1: 尝试从代码块中提取JSON
-    const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
-    if (codeBlockMatch) {
-      try {
-        requirements = JSON.parse(codeBlockMatch[1]);
-        // 移除代码块部分,保留自然语言
-        naturalLanguageReply = responseText.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '').trim();
-      } catch (e) {
-        // 解析失败,继续尝试其他方法
-      }
-    }
-    
-    // 方法2: 尝试匹配JSON对象
-    if (!requirements) {
-      const jsonMatch = responseText.match(/\{[\s\S]*"needsImages"[\s\S]*\}/);
-      if (jsonMatch) {
-        try {
-          requirements = JSON.parse(jsonMatch[0]);
-          // 移除JSON部分,保留自然语言
-          naturalLanguageReply = responseText.replace(/\{[\s\S]*"needsImages"[\s\S]*\}/, '').trim();
-        } catch (e) {
-          // 解析失败
-        }
-      }
-    }
-    
-    // 方法3: 尝试直接解析整个文本
-    if (!requirements) {
-      try {
-        requirements = JSON.parse(responseText.trim());
-      } catch (e) {
-        // 不是纯JSON
-      }
-    }
-    
-    // 如果找到JSON,返回JSON和自然语言回复
-    if (requirements && typeof requirements === 'object') {
-      // 将自然语言回复保存到requirements中,以便后续显示
-      if (naturalLanguageReply && naturalLanguageReply !== responseText.trim()) {
-        requirements.naturalLanguageReply = naturalLanguageReply;
-      }
-      return requirements;
-    }
-    
-    // 如果没有找到JSON,可能是用户的问题与工作流无关,返回错误信息
-    return { 
-      error: naturalLanguageReply || '请描述你的工作流需求,例如:生成一个登录流程,先点击登录按钮,然后输入用户名和密码'
-    };
-  };
-
   // 解析用户输入的文字参考(按行分割)
   const parseReferenceTexts = (text) => {
     // 如果已经有工作流需求,提取文字参考
@@ -886,7 +418,7 @@ ${images.map((img, index) => `图片${index + 1}: 原始文件名 ${img.original
       }
 
       // 发送到GPT并获取回复(传递已命名的图片信息和文字参考)
-      const response = await sendToGPT(text + textReferencesPrompt + imageDescriptionsPrompt, namedImages);
+      const response = await generateProcessing(text + textReferencesPrompt + imageDescriptionsPrompt, namedImages);
       
       // 根据实际 API 响应结构解析文本
       // API 返回: { success: true, data: { output_text: "..." } }
@@ -1023,134 +555,3 @@ ${images.map((img, index) => `图片${index + 1}: 原始文件名 ${img.original
     setReferenceTexts,
   };
 }
-
-// 更新工作流JSON中的图片路径
-function updateImagePathsInWorkflow(workflowJson, imageNameMap) {
-  // 深拷贝,避免修改原始对象
-  const updated = JSON.parse(JSON.stringify(workflowJson));
-  
-  // 递归更新actions中的图片路径
-  function updateActions(actions) {
-    if (!Array.isArray(actions)) return;
-    
-    for (const action of actions) {
-      // locate 和 click 操作可能使用图片
-      if ((action.type === 'locate' || action.type === 'click') && action.method === 'image' && action.target) {
-        const target = action.target;
-        // 检查是否是映射表中的文件名
-        if (imageNameMap[target]) {
-          action.target = imageNameMap[target];
-        } else {
-          // 尝试匹配 "1.png", "2.png" 等格式
-          const match = target.match(/^(\d+)\.png$/i);
-          if (match) {
-            const index = parseInt(match[1]) - 1;
-            if (imageNameMap[`${index + 1}.png`]) {
-              action.target = imageNameMap[`${index + 1}.png`];
-            }
-          }
-        }
-      }
-      
-      // ocr 操作可能使用头像图片
-      if (action.type === 'ocr' && action.method === 'by-avatar' && action.avatar) {
-        const avatar = action.avatar;
-        if (imageNameMap[avatar]) {
-          action.avatar = imageNameMap[avatar];
-        } else {
-          const match = avatar.match(/^(\d+)\.png$/i);
-          if (match) {
-            const index = parseInt(match[1]) - 1;
-            if (imageNameMap[`${index + 1}.png`]) {
-              action.avatar = imageNameMap[`${index + 1}.png`];
-            }
-          }
-        }
-      }
-      
-      // 递归处理嵌套的 actions(if、for、while)
-      if (action.then && Array.isArray(action.then)) {
-        updateActions(action.then);
-      }
-      if (action.else && Array.isArray(action.else)) {
-        updateActions(action.else);
-      }
-      if (action.body && Array.isArray(action.body)) {
-        updateActions(action.body);
-      }
-    }
-  }
-  
-  if (updated.actions && Array.isArray(updated.actions)) {
-    updateActions(updated.actions);
-  }
-  
-  return updated;
-}
-
-// 从文本中提取JSON格式的工作流(支持新旧格式)
-function extractWorkflowJsonFromText(text) {
-    if (!text) return null;
-    
-    // 尝试直接解析整个文本
-    try {
-      const parsed = JSON.parse(text.trim());
-      // 新格式:包含 actions 字段
-      if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-        return parsed;
-      }
-      // 旧格式:直接是数组
-      if (Array.isArray(parsed) && parsed.length > 0) {
-        return { actions: parsed };
-      }
-    } catch (e) {
-      // 不是纯JSON,继续尝试提取
-    }
-    
-    // 尝试从代码块中提取JSON
-    const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
-    if (codeBlockMatch) {
-      try {
-        const parsed = JSON.parse(codeBlockMatch[1]);
-        if (parsed && typeof parsed === 'object') {
-          // 新格式:包含 actions
-          if (Array.isArray(parsed.actions)) {
-          return parsed;
-          }
-          // 旧格式:直接是数组
-          if (Array.isArray(parsed)) {
-            return { actions: parsed };
-          }
-        }
-      } catch (e) {
-        // 解析失败
-      }
-    }
-    
-    // 尝试查找JSON对象(查找包含"actions"的对象,支持多行)
-    const jsonMatch = text.match(/\{[\s\S]*?"actions"[\s\S]*?\}/);
-    if (jsonMatch) {
-      try {
-        const parsed = JSON.parse(jsonMatch[0]);
-        if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-          return parsed;
-        }
-      } catch (e) {
-        // 解析失败,尝试更宽松的匹配
-        try {
-          // 尝试匹配整个JSON对象(包括嵌套结构)
-          const fullJsonMatch = text.match(/\{(?:[^{}]|(?:\{[^{}]*\}))*\}/);
-          if (fullJsonMatch) {
-            const parsed = JSON.parse(fullJsonMatch[0]);
-            if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-              return parsed;
-            }
-          }
-        } catch (e2) {
-        // 解析失败
-        }
-      }
-    }
-    
-    return null;
-}

+ 407 - 0
src/pages/Chat/Input/generate-processing.js

@@ -0,0 +1,407 @@
+// 第二阶段提示词:生成工作流
+const SYSTEM_PROMPT = `你是一个友好的自动化工作流生成助手。用户已经提供了所需的图片和文字参考,现在你需要生成JSON格式的工作流配置文件。
+
+请用自然、友好的语言与用户交流,保持对话的连贯性。在生成工作流时,请先简短地确认一下,然后生成JSON格式的工作流配置。
+
+工作流基本结构:
+{
+  "variables": {
+    "variableName": "initialValue"
+  },
+  "execute": [
+    // 操作数组
+  ]
+}
+
+基础语法:
+
+1. 定时执行(schedule):
+{
+  "type": "schedule",
+  "condition": {
+    "interval": "1s",
+    "repeat": -1
+  },
+  "interval": [
+    // 要执行的操作数组
+  ]
+}
+- interval: 执行间隔("1s", "2m", "3h")
+- repeat: 重复次数(-1 表示无限循环)
+
+2. ADB操作(adb):
+统一格式,通过 method 区分:
+{
+  "type": "adb",
+  "method": "input|click|locate|swipe|scroll|press|string-press",
+  "inVars": [],
+  "outVars": []
+}
+
+| method | 说明 | inVars |
+|--------|------|--------|
+| input | 输入文本 | [0]: 文本内容 |
+| click | 点击 | [0]: 位置坐标 {x, y} |
+| locate | 定位 | [0]: 图片/文字,outVars[0]: 保存位置 |
+| swipe | 滑动 | [0]: 方向(up-down/down-up/left-right/right-left) |
+| scroll | 滚动 | [0]: 方向(up/down) |
+| press | 图像匹配并点击 | [0]: 图片路径 |
+| string-press | 文字识别并点击 | [0]: 文字内容 |
+
+3. 条件判断(if):
+{
+  "type": "if",
+  "condition": "{变量} == '值'",
+  "ture": [],
+  "false": []
+}
+- 支持操作符:== != > < >= <=
+- 注意:使用 ture 而不是 then
+
+4. 循环(while):
+{
+  "type": "while",
+  "condition": "{变量} > 0",
+  "ture": []
+}
+- 注意:使用 ture 作为 body 的别名
+
+5. 内置操作:
+
+延迟(delay):
+{ "type": "delay", "value": "2s" }
+
+设置变量(set):
+{ "type": "set", "variable": "name", "value": "value" }
+
+随机数(random):
+{ "type": "random", "variable": "num", "min": 1, "max": 100, "integer": true }
+
+6. 扩展标签(Func):
+由 src/pages/processing/func/ 目录下的脚本文件决定,常用标签:
+
+- ocr-chat: OCR识别对话内容(输出JSON字符串格式)
+  - inVars: [头像1路径, 头像2路径]
+  - outVars: [聊天记录变量(JSON字符串)]
+  
+- read-chat-history: 读取聊天记录
+  - inVars: []
+  - outVars: [消息数组变量]
+  
+- read-last-message: 读取最后一条消息
+  - inVars: [聊天记录变量(可选)]
+  - outVars: [消息文本变量, 发送者变量]
+  
+- save-new-chat: 保存对话
+  - inVars: [聊天记录变量]
+  - outVars: []
+  
+- image-center-location: 图像中心点定位
+  - inVars: [模板图片路径]
+  - outVars: [位置坐标变量 {x, y}]
+  
+- image-region-location: 图像区域定位(返回四个顶点)
+  - inVars: [截图路径(可选), 区域图片路径]
+  - outVars: [corners变量]
+  
+- ai-generate: AI生成内容
+  - inVars: [提示词中的变量]
+  - outVars: [结果变量]
+
+重要规则:
+- 使用 execute 作为顶级数组字段(不再使用 actions 或 triggers)
+- 变量使用 {variableName} 格式引用
+- 图片路径直接写文件名(不需要 resources/ 前缀,代码会自动处理)
+- 所有操作都支持 inVars 和 outVars(可以为空数组)
+- if 和 while 使用 ture 而不是 then 或 body
+- 图片文件名按顺序命名为"1.png"、"2.png"等
+
+返回格式:
+1. 如果用户已经提供了所有需要的图片和文字参考,且明确要求生成工作流:
+   - 先用简短的自然语言确认(如"好的,我现在为您生成工作流配置...")
+   - 然后单独一行返回JSON格式的工作流配置(可以用代码块包裹)
+   
+2. 如果用户只是在询问问题或者还没准备好生成工作流:
+   - 用自然语言友好地回答用户的问题
+   - 不需要返回JSON
+
+请保持对话的自然和友好,不要生硬地只返回JSON。`;
+
+/**
+ * 调用 AI 生成工作流配置
+ * @param {string} userPrompt - 用户输入的需求描述
+ * @param {Array} images - 图片数组,包含 aiName, tempName, originalName 等
+ * @returns {Promise<Object>} - 返回 API 响应数据
+ */
+export async function generateProcessing(userPrompt, images = []) {
+  // 构建图片信息提示(如果图片已经命名)
+  let imageInfoPrompt = '';
+  if (images.length > 0) {
+    imageInfoPrompt = `\n\n用户上传了以下图片,请在生成工作流时使用这些图片文件名:\n${images.map((img, index) => `${index + 1}. ${img.aiName || img.tempName} - ${img.originalName}`).join('\n')}\n\n在生成工作流时,如果操作需要点击图片,请使用上述图片文件名(如 "登录按钮.png")。`;
+  }
+
+  // 构建完整的提示词:系统提示词 + 图片信息 + 用户输入
+  const fullPrompt = `${SYSTEM_PROMPT}${imageInfoPrompt}\n\n用户需求:${userPrompt}`;
+    
+  const requestBody = {
+    input: fullPrompt
+  };
+    
+  console.log('Sending request to Doubao API:', requestBody);
+
+  const response = await fetch('https://ai-anim.com/api/doubaoText2text', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify(requestBody),
+  });
+
+  console.log('Response status:', response.status, response.statusText);
+
+  if (!response.ok) {
+    let errorText;
+    try {
+      errorText = await response.text();
+      // 尝试解析为 JSON
+      try {
+        const errorJson = JSON.parse(errorText);
+        console.error('API Error Response (JSON):', errorJson);
+        // 如果 JSON 中有 error 字段,使用它
+        if (errorJson.error) {
+          throw new Error(`服务器错误: ${errorJson.error}`);
+        }
+        if (errorJson.message) {
+          throw new Error(`服务器错误: ${errorJson.message}`);
+        }
+      } catch (parseError) {
+        // 不是 JSON,使用原始文本
+        console.error('API Error Response (Text):', errorText);
+      }
+    } catch (readError) {
+      errorText = `无法读取错误响应: ${readError.message}`;
+    }
+    throw new Error(`HTTP ${response.status}: ${errorText}`);
+  }
+
+  const data = await response.json();
+  console.log('API Response:', data);
+  
+  return data;
+}
+
+/**
+ * 从文本中提取JSON格式的工作流(支持新旧格式)
+ * @param {string} text - 包含工作流JSON的文本
+ * @returns {Object|null} - 提取的工作流JSON对象,如果未找到则返回null
+ */
+export function extractWorkflowJsonFromText(text) {
+  if (!text) return null;
+  
+  // 尝试直接解析整个文本
+  try {
+    const parsed = JSON.parse(text.trim());
+    // 新格式:包含 execute 字段
+    if (parsed && typeof parsed === 'object' && Array.isArray(parsed.execute)) {
+      return parsed;
+    }
+    // 向后兼容:包含 actions 字段
+    if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
+      return parsed;
+    }
+    // 向后兼容:直接是数组
+    if (Array.isArray(parsed) && parsed.length > 0) {
+      return { execute: parsed };
+    }
+  } catch (e) {
+    // 不是纯JSON,继续尝试提取
+  }
+  
+  // 尝试从代码块中提取JSON
+  const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
+  if (codeBlockMatch) {
+    try {
+      const parsed = JSON.parse(codeBlockMatch[1]);
+      if (parsed && typeof parsed === 'object') {
+        // 新格式:包含 execute
+        if (Array.isArray(parsed.execute)) {
+          return parsed;
+        }
+        // 向后兼容:包含 actions
+        if (Array.isArray(parsed.actions)) {
+          return parsed;
+        }
+        // 向后兼容:直接是数组
+        if (Array.isArray(parsed)) {
+          return { execute: parsed };
+        }
+      }
+    } catch (e) {
+      // 解析失败
+    }
+  }
+  
+  // 尝试查找JSON对象(优先查找包含"execute"的对象,其次查找"actions")
+  const executeMatch = text.match(/\{[\s\S]*?"execute"[\s\S]*?\}/);
+  if (executeMatch) {
+    try {
+      const parsed = JSON.parse(executeMatch[0]);
+      if (parsed && typeof parsed === 'object' && Array.isArray(parsed.execute)) {
+        return parsed;
+      }
+    } catch (e) {
+      // 解析失败,尝试更宽松的匹配
+    }
+  }
+  
+  // 向后兼容:查找包含"actions"的对象
+  const actionsMatch = text.match(/\{[\s\S]*?"actions"[\s\S]*?\}/);
+  if (actionsMatch) {
+    try {
+      const parsed = JSON.parse(actionsMatch[0]);
+      if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
+        return parsed;
+      }
+    } catch (e) {
+      // 解析失败
+    }
+  }
+  
+  return null;
+}
+
+/**
+ * 更新工作流JSON中的图片路径
+ * @param {Object} workflowJson - 工作流JSON对象
+ * @param {Object} imageNameMap - 图片文件名映射表
+ * @returns {Object} - 更新后的工作流JSON对象
+ */
+export function updateImagePathsInWorkflow(workflowJson, imageNameMap) {
+  // 深拷贝,避免修改原始对象
+  const updated = JSON.parse(JSON.stringify(workflowJson));
+  
+  // 递归更新actions中的图片路径
+  function updateActions(actions) {
+    if (!Array.isArray(actions)) return;
+    
+    for (const action of actions) {
+      // adb 的 locate 和 click 操作可能使用图片
+      if (action.type === 'adb' && (action.method === 'locate' || action.method === 'click' || action.method === 'press') && action.inVars) {
+        // 更新 inVars 中的图片路径
+        for (let i = 0; i < action.inVars.length; i++) {
+          const varValue = action.inVars[i];
+          // 检查是否是图片文件名
+          if (typeof varValue === 'string' && (varValue.endsWith('.png') || varValue.endsWith('.jpg') || varValue.endsWith('.jpeg'))) {
+            if (imageNameMap[varValue]) {
+              action.inVars[i] = imageNameMap[varValue];
+            } else {
+              // 尝试匹配 "1.png", "2.png" 等格式
+              const match = varValue.match(/^(\d+)\.(png|jpg|jpeg)$/i);
+              if (match) {
+                const index = parseInt(match[1]) - 1;
+                const ext = match[2];
+                if (imageNameMap[`${index + 1}.${ext}`]) {
+                  action.inVars[i] = imageNameMap[`${index + 1}.${ext}`];
+                }
+              }
+            }
+          }
+        }
+      }
+      
+      // ocr-chat 使用头像图片
+      if ((action.type === 'ocr-chat' || action.type === 'ocr-chat-history') && action.inVars) {
+        for (let i = 0; i < action.inVars.length; i++) {
+          const varValue = action.inVars[i];
+          if (typeof varValue === 'string' && (varValue.endsWith('.png') || varValue.endsWith('.jpg') || varValue.endsWith('.jpeg'))) {
+            if (imageNameMap[varValue]) {
+              action.inVars[i] = imageNameMap[varValue];
+            } else {
+              const match = varValue.match(/^(\d+)\.(png|jpg|jpeg)$/i);
+              if (match) {
+                const index = parseInt(match[1]) - 1;
+                const ext = match[2];
+                if (imageNameMap[`${index + 1}.${ext}`]) {
+                  action.inVars[i] = imageNameMap[`${index + 1}.${ext}`];
+                }
+              }
+            }
+          }
+        }
+      }
+      
+      // image-center-location 和 image-region-location 使用图片
+      if ((action.type === 'image-center-location' || action.type === 'image-region-location') && action.inVars) {
+        for (let i = 0; i < action.inVars.length; i++) {
+          const varValue = action.inVars[i];
+          if (typeof varValue === 'string' && (varValue.endsWith('.png') || varValue.endsWith('.jpg') || varValue.endsWith('.jpeg'))) {
+            if (imageNameMap[varValue]) {
+              action.inVars[i] = imageNameMap[varValue];
+            } else {
+              const match = varValue.match(/^(\d+)\.(png|jpg|jpeg)$/i);
+              if (match) {
+                const index = parseInt(match[1]) - 1;
+                const ext = match[2];
+                if (imageNameMap[`${index + 1}.${ext}`]) {
+                  action.inVars[i] = imageNameMap[`${index + 1}.${ext}`];
+                }
+              }
+            }
+          }
+        }
+      }
+      
+      // 向后兼容:旧格式的 locate 和 click
+      if ((action.type === 'locate' || action.type === 'click') && action.method === 'image' && action.target) {
+        const target = action.target;
+        if (imageNameMap[target]) {
+          action.target = imageNameMap[target];
+        } else {
+          const match = target.match(/^(\d+)\.(png|jpg|jpeg)$/i);
+          if (match) {
+            const index = parseInt(match[1]) - 1;
+            const ext = match[2];
+            if (imageNameMap[`${index + 1}.${ext}`]) {
+              action.target = imageNameMap[`${index + 1}.${ext}`];
+            }
+          }
+        }
+      }
+      
+      // 递归处理嵌套的操作(if、while、schedule)
+      if (action.ture && Array.isArray(action.ture)) {
+        updateActions(action.ture);
+      }
+      // 向后兼容:then
+      if (action.then && Array.isArray(action.then)) {
+        updateActions(action.then);
+      }
+      if (action.false && Array.isArray(action.false)) {
+        updateActions(action.false);
+      }
+      // 向后兼容:else
+      if (action.else && Array.isArray(action.else)) {
+        updateActions(action.else);
+      }
+      // 向后兼容:body
+      if (action.body && Array.isArray(action.body)) {
+        updateActions(action.body);
+      }
+      // schedule 的 interval
+      if (action.type === 'schedule' && action.interval && Array.isArray(action.interval)) {
+        updateActions(action.interval);
+      }
+    }
+  }
+  
+  // 更新 execute 数组(新格式)
+  if (updated.execute && Array.isArray(updated.execute)) {
+    updateActions(updated.execute);
+  }
+  // 向后兼容:actions 数组
+  if (updated.actions && Array.isArray(updated.actions)) {
+    updateActions(updated.actions);
+  }
+  
+  return updated;
+}

+ 0 - 1156
src/pages/Chat/Input/input-hooks.jsx

@@ -1,1156 +0,0 @@
-import { useState, useRef } from 'react';
-
-// useInput - 管理输入组件的状态和逻辑
-export function useInput(onSendMessage, onLoadingChange) {
-  const { 
-    onSend, 
-    onUpload, 
-    uploadedImages, 
-    removeImage,
-    workflowRequirements,
-  } = InputLogic(onSendMessage, onLoadingChange);
-  const [message, setMessage] = useState('');
-  const [isLoading, setIsLoading] = useState(false);
-  const fileInputRef = useRef(null);
-  const textInputRef = useRef(null);
-
-  const handleSend = async () => {
-    const trimmed = message.trim();
-    // 允许在有图片时发送(即使没有文字),或者有文字时发�?
-    if ((!trimmed && uploadedImages.length === 0) || isLoading) return;
-    
-    // 先清空输入框(在发送前清空,提供更好的用户体验�?
-    setMessage('');
-    
-    setIsLoading(true);
-    try {
-      await onSend?.(trimmed || ''); // 如果没有文字,传递空字符�?
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  const handleContainerClick = (e) => {
-    // 如果点击的是容器本身(不是按钮、输入框或其他交互元素),则聚焦输入�?
-    if (e.target === e.currentTarget) {
-      textInputRef.current?.focus();
-    }
-  };
-
-  // 有文字或上传了图片就可以发�?
-  const isSendDisabled = (!message.trim() && uploadedImages.length === 0) || isLoading;
-
-  const handleUploadClick = () => {
-    fileInputRef.current?.click();
-  };
-
-  const handleFileChange = async (e) => {
-    const files = e.target.files;
-    if (!files || files.length === 0) return;
-
-    for (const file of files) {
-      if (file.type.startsWith('image/')) {
-        await onUpload?.(file);
-      }
-    }
-
-    // 清空input,允许重复选择同一文件
-    e.target.value = '';
-  };
-
-  const handleSendKeyDown = (e) => {
-    if ((e.key === 'Enter' || e.key === ' ') && !isSendDisabled) {
-      e.preventDefault();
-      handleSend();
-    }
-  };
-
-  const handleTextareaKeyDown = (e) => {
-    // Enter 发送(不按 Ctrl�?
-    if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
-      e.preventDefault();
-      handleSend();
-      return;
-    }
-    // 其他所有按键都允许默认行为(包括Backspace、Delete等)
-    // 不要阻止任何其他按键的默认行�?
-  };
-
-  const handleUploadKeyDown = (e) => {
-    if (e.key === 'Enter' || e.key === ' ') {
-      e.preventDefault();
-      handleUploadClick();
-    }
-  };
-
-  const getSendButtonClassName = () => {
-    return `send-btn ${isSendDisabled ? 'disabled' : ''}`;
-  };
-
-  const getSendButtonOnClick = () => {
-    return !isSendDisabled ? handleSend : undefined;
-  };
-
-  const getSendButtonTabIndex = () => {
-    return isSendDisabled ? -1 : 0;
-  };
-
-  return {
-    message,
-    setMessage,
-    isLoading,
-    fileInputRef,
-    textInputRef,
-    handleSend,
-    handleContainerClick,
-    handleUploadClick,
-    handleFileChange,
-    handleSendKeyDown,
-    handleTextareaKeyDown,
-    handleUploadKeyDown,
-    isSendDisabled,
-    getSendButtonClassName,
-    getSendButtonOnClick,
-    getSendButtonTabIndex,
-    uploadedImages,
-    removeImage,
-  };
-}
-
-// InputLogic - 处理消息发送到GPT API
-
-// 第一阶段提示词:分析需求,返回需要的信息
-const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手。你需要像聊天一样与用户对话�?
-
-如果用户想要创建自动化工作流,你需要分析需求并返回需要的信息。请用自然、友好的语言与用户交流,保持对话的连贯性和友好性�?
-
-语法分层(重要)�?
-- 基础语法(控制流/定时):scheduleOnce、schedule、for、while、if
-- 基础 action(模拟手机操作):press、swipe、scroll
-- 扩展标签(Func):除基础能力外的所有扩展功能,全部�?src/pages/processing/Func/ 目录下的脚本文件决定;每个脚本文件名就是一个“标签”�?
-
-在生成工作流时:
-- 逻辑结构尽量�?for/while/if + triggers(schedules) 表达
-- 具体手机操作尽量落到 press/swipe/scroll
-- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:ocr-chat-history、string-reg-location、save-new-chat�?
-
-(实现层说明:项目内部为兼容与落地,仍存�?locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。)
-
-9. if - 条件判断
-   - condition: 条件表达式(�?"{count} > 5"�?
-   - then: 条件为真时执行的操作数组
-   - else: 条件为假时执行的操作数组(可选)
-
-10. for - 循环
-    - variable: 循环变量�?
-    - items: 数组(可以是变量 {arrayVariable}�?
-    - body: 循环体操作数�?
-
-11. delay - 延迟
-    - value: 延迟时间(如 "2s", "1m", "30 minutes"�?
-
-12. set - 设置变量
-    - variable: 变量�?
-    - value: 变量�?
-
-13. scroll/swipe - 滚动/滑动
-    - value: 方向�?up-down", "down-up", "left-right", "right-left"�?
-
-定时任务配置�?
-- 单次执行: {"datetime": "2026/1/14 01:21"}
-- 每天执行: {"time": "09:00", "repeat": "daily"}
-- 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
-- 间隔执行: {"interval": "30 minutes"}
-
-重要规则�?
-- 变量使用 {variableName} 格式引用
-- 根据用户描述合理设置delay延迟时间
-- 如果用户提到循环、条件判断,使用for/if操作
-- 如果用户提到定时执行,添加triggers配置
-- 如果用户提到AI生成内容,使用ai-generate操作
-- 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
-- 图片文件名按顺序命名�?1.png"�?2.png"�?
-- **特别重要**:如果工作流中需要输入文字(使用 input �?ai-generate 操作),且这些文字需要根据场景背景和用户角色来生成,你必须询问用户以下信息:
-  - 场景的背景:这个工作流在什么场景下使用?(例如:微信聊天、客服回复、评论互动等�?
-  - 用户的角色:用户在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等�?
-  - 这些信息对于生成合适的文字内容非常重要,请务必在询问需求时主动询问
-
-请分析用户的需求,根据上述操作类型,判断需要哪些图片和文字参考:
-- 图片(needsImages):用于 locate/click �?image 方法,或 ocr �?by-avatar 方法
-- 文字参考(needsTexts):用于 locate/click �?text 方法
-
-如果用户想要创建自动化工作流�?
-1. 先友好地确认理解用户的需�?
-2. 然后返回JSON格式的需求信息(不包含在自然语言回复中,仅作为JSON返回�?
-
-需求分析的JSON格式�?
-{
-  "needsImages": 2,  // 需要多少张图片(用于图像匹配定位)
-  "needsTexts": 1,   // 需要多少个文字参考(用于文字识别定位�?
-  "imageDescriptions": ["登录按钮", "用户名输入框"],  // 每张图片的描�?
-  "textDescriptions": ["登录按钮文字"]  // 每个文字参考的描述
-}
-
-返回格式:先自然语言回复,然后单独一行JSON�?
-
-**特别提醒**:如果工作流涉及输入文字(如自动回复、自动评论、自动发送消息等),请务必询问用户场景背景和角色信息�?
-
-例如(不涉及文字输入的场景)�?
-我理解了,您想创建一个登录流程的自动化工作流。为了生成准确的工作流配置,我需要您提供以下信息�?
-
-📋 需要的信息�?
-🖼�?需�?2 张图片:登录按钮截图、用户名输入框截�?
-📝 需�?1 个文字参考:登录按钮上的文字
-
-\`\`\`json
-{
-  "needsImages": 2,
-  "needsTexts": 1,
-  "imageDescriptions": ["登录按钮", "用户名输入框"],
-  "textDescriptions": ["登录按钮文字"]
-}
-\`\`\`
-
-例如(涉及文字输入的场景):
-我理解了,您想创建一个自动聊天回复的自动化工作流。为了生成准确且符合场景的文字内容,我需要了解以下信息:
-
-📋 需要的信息�?
-🖼�?需�?2 张图片:好友头像截图、我的头像截�?
-📝 需�?1 个文字参考:输入框位�?
-💬 **场景信息**(重要)�?
-   - 场景背景:这个聊天是在什么场景下进行的?(例如:微信聊天、QQ群聊、客服对话等�?
-   - 您的角色:您在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等�?
-
-\`\`\`json
-{
-  "needsImages": 2,
-  "needsTexts": 1,
-  "imageDescriptions": ["好友头像", "我的头像"],
-  "textDescriptions": ["输入框位�?]
-}
-\`\`\`
-
-如果用户的话题与工作流无关,请友好地自然回复,不需要返回JSON。如果用户是在询问如何使用、或者其他一般性问题,请用自然语言友好地回答。`;
-
-// 第二阶段提示词:生成工作�?
-const SYSTEM_PROMPT = `你是一个友好的自动化工作流生成助手。用户已经提供了所需的图片和文字参考,现在你需要生成JSON格式的工作流配置文件�?
-
-请用自然、友好的语言与用户交流,保持对话的连贯性。在生成工作流时,请先简短地确认一下,然后生成JSON格式的工作流配置�?
-
-工作流JSON格式(新版本):
-{
-  "version": "1.0",
-  "name": "工作流名�?,
-  "triggers": [
-    {
-      "type": "schedule",
-      "schedule": {
-        "time": "09:00",
-        "weekdays": ["monday", "wednesday", "friday"]
-      }
-    }
-  ],
-  "variables": {
-    "topic": "美食"
-  },
-  "actions": [
-    {
-      "type": "locate",
-      "method": "image",
-      "target": "button.png",
-      "variable": "buttonPos",
-      "delay": "1s"
-    },
-    {
-      "type": "click",
-      "method": "position",
-      "target": "{buttonPos}",
-      "delay": "2s"
-    },
-    {
-      "type": "input",
-      "target": "输入�?,
-      "value": "文字内容",
-      "delay": "1s"
-    },
-    {
-      "type": "ai-generate",
-      "prompt": "生成关于{topic}的文�?,
-      "variable": "article",
-      "delay": "10s"
-    },
-    {
-      "type": "if",
-      "condition": "{count} > 5",
-      "then": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "确定"
-        }
-      ]
-    },
-    {
-      "type": "for",
-      "variable": "item",
-      "items": ["选项1", "选项2"],
-      "body": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "{item}"
-        }
-      ]
-    }
-  ]
-}
-
-核心操作类型�?
-1. locate - 定位元素
-   - method: "image"(图像匹配)| "text"(文字识别)| "coordinate"(坐标)
-   - target: 目标内容
-   - variable: 保存位置到变�?
-
-2. click - 点击
-   - method: "position" | "image" | "text" | "coordinate"
-   - target: 目标(可以是变量 {variable}�?
-
-3. input - 输入文字
-   - target: 输入框位置(文字或变量)
-   - value: 要输入的文本(可以是变量 {variable}�?
-
-4. ai-generate - AI生成内容
-   - prompt: 提示词(支持变量 {variable}�?
-   - variable: 保存结果到变�?
-
-5. ocr - 文字识别
-   - method: "by-avatar" | "by-position" | "full-screen"
-   - avatar: 头像图片(by-avatar时)
-   - variable: 保存识别结果
-
-6. extract-messages - 提取消息记录
-   - avatar1: 第一个参与者的头像/标识图片路径
-   - avatar2: 第二个参与者的头像/标识图片路径
-   - variable: 保存消息记录到变量(格式:对�? 消息\n�? 消息�?
-
-7. save-messages - 保存消息记录
-   - variable: 包含消息记录的变量名(保存到 history 文件夹)
-
-8. generate-summary - 生成消息总结
-   - variable: 包含消息记录的变量名
-   - summaryVariable: 保存总结结果的变量名
-   - model: AI模型名称(可选)
-
-9. if - 条件判断
-   - condition: 条件表达式(�?"{count} > 5"�?
-   - then: 条件为真时执行的操作数组
-   - else: 条件为假时执行的操作数组(可选)
-
-10. for - 循环
-    - variable: 循环变量�?
-    - items: 数组(可以是变量 {arrayVariable}�?
-    - body: 循环体操作数�?
-
-11. delay - 延迟
-    - value: 延迟时间(如 "2s", "1m", "30 minutes"�?
-
-12. set - 设置变量
-    - variable: 变量�?
-    - value: 变量�?
-
-13. scroll/swipe - 滚动/滑动
-    - value: 方向�?up-down", "down-up", "left-right", "right-left"�?
-
-定时任务配置�?
-- 单次执行: {"datetime": "2026/1/14 01:21"}
-- 每天执行: {"time": "09:00", "repeat": "daily"}
-- 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
-- 间隔执行: {"interval": "30 minutes"}
-
-重要规则�?
-- 使用新格式(type字段),但保持向后兼容旧格式
-- 变量使用 {variableName} 格式引用
-- 根据用户描述合理设置delay延迟时间
-- 如果用户提到循环、条件判断,使用for/if操作
-- 如果用户提到定时执行,添加triggers配置
-- 如果用户提到AI生成内容,使用ai-generate操作
-- 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
-- 在ai-generate的prompt中可以使用总结变量引用历史消息记录的总结
-- 图片文件名按顺序命名�?1.png"�?2.png"�?
-
-返回格式�?
-1. 如果用户已经提供了所有需要的图片和文字参考,且明确要求生成工作流�?
-   - 先用简短的自然语言确认(如"好的,我现在为您生成工作流配�?.."�?
-   - 然后单独一行返回JSON格式的工作流配置(可以用代码块包裹)
-   
-2. 如果用户只是在询问问题或者还没准备好生成工作流:
-   - 用自然语言友好地回答用户的问题
-   - 不需要返回JSON
-
-请保持对话的自然和友好,不要生硬地只返回JSON。`;
-
-export function InputLogic(onSendMessage, onLoadingChange) {
-  // 存储上传的图�?
-  const [uploadedImages, setUploadedImages] = useState([]);
-  
-  // img2text API可用性标记(使用useRef避免重复输出警告�?
-  const img2textApiAvailableRef = useRef(true);
-  
-  // 调用img2text API获取图片描述
-  const img2text = async (imageBase64, prompt = '请描述这张图片的内容') => {
-    // 如果已经知道API不可用,直接返回null,不尝试调用
-    if (!img2textApiAvailableRef.current) {
-      return null;
-    }
-    
-    try {
-      // 确保base64数据是完整的data URL格式
-      let imageUrl = imageBase64;
-      if (!imageBase64.startsWith('data:')) {
-        // 如果没有前缀,假设是PNG格式的base64数据
-        imageUrl = `data:image/png;base64,${imageBase64}`;
-      }
-
-      // 使用AbortController设置超时,快速失�?
-      const controller = new AbortController();
-      const timeoutId = setTimeout(() => controller.abort(), 1000); // 1秒超�?
-
-      const response = await fetch('https://ai-anim.com/api/img2text', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({
-          prompt: prompt,
-          imageUrl: imageUrl
-        }),
-        signal: controller.signal,
-      });
-
-      clearTimeout(timeoutId);
-
-      if (!response.ok) {
-        const errorText = await response.text();
-        throw new Error(`img2text API error: ${response.status} ${response.statusText} - ${errorText}`);
-      }
-
-      const data = await response.json();
-      // 支持多种可能的返回格�?
-      return data.text || data.description || data.result || data.output_text || data.content || '';
-    } catch (error) {
-      // img2text API是可选的,失败不影响主要功能
-      // 标记API不可用,之后直接返回null,不再尝�?
-      if (img2textApiAvailableRef.current) {
-        img2textApiAvailableRef.current = false;
-      }
-      // 静默返回null,不输出任何错误信息(因为这是可选功能)
-      return null;
-    }
-  };
-  // 存储工作流需求信息(需要多少图片和文字�?
-  const [workflowRequirements, setWorkflowRequirements] = useState(null);
-  // 存储用户输入的文字参考(用于文字识别�?
-  const [referenceTexts, setReferenceTexts] = useState([]);
-  // 存储AI生成的工作流JSON(不显示在聊天框�?
-  const [workflowJson, setWorkflowJson] = useState(null);
-
-  // 删除图片
-  const removeImage = (index) => {
-    setUploadedImages(prev => prev.filter((_, i) => i !== index));
-  };
-
-  // 上传图片并命�?
-  const onUpload = async (file) => {
-    try {
-      // 读取图片为base64
-      const reader = new FileReader();
-      const base64 = await new Promise((resolve, reject) => {
-        reader.onload = () => {
-          const result = reader.result;
-          // 提取base64数据部分(去掉data:image/...;base64,前缀�?
-          const base64Data = result.split(',')[1];
-          resolve(base64Data);
-        };
-        reader.onerror = reject;
-        reader.readAsDataURL(file);
-      });
-
-      // 生成临时文件名(使用时间戳)
-      const timestamp = Date.now();
-      const tempFileName = `temp_${timestamp}.${file.name.split('.').pop()}`;
-
-      // 存储图片信息
-      const imageInfo = {
-        file: file,
-        base64: base64,
-        tempName: tempFileName,
-        originalName: file.name,
-        timestamp: timestamp,
-      };
-
-      setUploadedImages(prev => [...prev, imageInfo]);
-      console.log('图片已上�?', imageInfo.originalName);
-    } catch (error) {
-      console.error('上传图片失败:', error);
-    }
-  };
-
-  // 根据用户描述给图片命名(使用AI�?
-  const nameImagesWithAI = async (userPrompt, images) => {
-    if (!images || images.length === 0) return images;
-
-    try {
-      // 构建提示词,让AI根据用户描述给图片命�?
-      const imagePrompt = `用户上传�?${images.length} 张图片,用于工作流自动化。请根据用户的描述,为每张图片生成一个简洁的描述性文件名(只返回JSON数组,不要其他文字)�?
-
-用户描述�?{userPrompt}
-
-图片信息�?
-${images.map((img, index) => `图片${index + 1}: 原始文件�?${img.originalName}`).join('\n')}
-
-请返回JSON数组,格式如下:
-["登录按钮.png", "用户名输入框.png", "密码输入�?png"]
-
-要求�?
-1. 文件名要简洁,能清楚描述图片内�?
-2. 使用中文或英文都可以
-3. 必须包含文件扩展名(.png�?
-4. 文件名要符合工作流中使用的命名规范`;
-
-      const response = await fetch('https://ai-anim.com/api/text2textByModel', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          prompt: imagePrompt,
-          modelName: 'gpt-5-nano-ca'
-        })
-      });
-
-      if (!response.ok) {
-        throw new Error('AI命名失败');
-      }
-
-      const data = await response.json();
-      const responseText = data.data?.output_text || data.text || data.content || '';
-      
-      // 解析AI返回的文件名数组
-      let imageNames = [];
-      try {
-        // 尝试直接解析JSON
-        imageNames = JSON.parse(responseText.trim());
-      } catch (e) {
-        // 尝试从代码块中提�?
-        const match = responseText.match(/\[[\s\S]*?\]/);
-        if (match) {
-          imageNames = JSON.parse(match[0]);
-        } else {
-          // 如果解析失败,使用默认命�?
-          imageNames = images.map((img, index) => `${index + 1}.png`);
-        }
-      }
-
-      // 更新图片信息,添加AI生成的文件名
-      return images.map((img, index) => ({
-        ...img,
-        aiName: imageNames[index] || `${index + 1}.png`
-      }));
-    } catch (error) {
-      console.error('AI命名图片失败:', error);
-      // 如果AI命名失败,使用默认命�?
-      return images.map((img, index) => ({
-        ...img,
-        aiName: `${index + 1}.png`
-      }));
-    }
-  };
-
-  const sendToGPT = async (userPrompt, images = []) => {
-    // 构建图片信息提示(如果图片已经命名)
-    let imageInfoPrompt = '';
-    if (images.length > 0) {
-      imageInfoPrompt = `\n\n用户上传了以下图片,请在生成工作流时使用这些图片文件名:\n${images.map((img, index) => `${index + 1}. ${img.aiName || img.tempName} - ${img.originalName}`).join('\n')}\n\n在生成工作流时,如果操作需要点击图片,请使用上述图片文件名(如 "登录按钮.png")。`;
-    }
-
-    // 构建完整的提示词:系统提示词 + 图片信息 + 用户输入
-    const fullPrompt = `${SYSTEM_PROMPT}${imageInfoPrompt}\n\n用户需求:${userPrompt}`;
-      
-      const requestBody = {
-        input: fullPrompt
-      };
-
-      console.log('Sending request to Doubao API:', requestBody);
-
-      const response = await fetch('https://ai-anim.com/api/doubaoText2text', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify(requestBody),
-      });
-
-      console.log('Response status:', response.status, response.statusText);
-
-      if (!response.ok) {
-        let errorText;
-        try {
-          errorText = await response.text();
-          // 尝试解析�?JSON
-          try {
-            const errorJson = JSON.parse(errorText);
-            console.error('API Error Response (JSON):', errorJson);
-            // 如果 JSON 中有 error 字段,使用它
-            if (errorJson.error) {
-              throw new Error(`服务器错�? ${errorJson.error}`);
-            }
-            if (errorJson.message) {
-              throw new Error(`服务器错�? ${errorJson.message}`);
-            }
-          } catch (parseError) {
-            // 不是 JSON,使用原始文�?
-            console.error('API Error Response (Text):', errorText);
-          }
-        } catch (readError) {
-          errorText = `无法读取错误响应: ${readError.message}`;
-        }
-        throw new Error(`HTTP ${response.status}: ${errorText}`);
-      }
-
-      const data = await response.json();
-      console.log('API Response:', data);
-    
-      return data;
-  };
-
-  // 分析需求,返回需要的信息
-  const analyzeRequirements = async (userPrompt) => {
-    const response = await fetch('https://ai-anim.com/api/text2textByModel', {
-      method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({
-        prompt: `${REQUIREMENTS_PROMPT}\n\n用户需求:${userPrompt}`,
-        modelName: 'gpt-5-nano-ca'
-      })
-    });
-
-    if (!response.ok) {
-      throw new Error('分析需求失败');
-    }
-
-    const data = await response.json();
-    const responseText = data.data?.output_text || data.text || data.content || '';
-    
-    // 提取自然语言回复(去除JSON部分�?
-    let naturalLanguageReply = responseText.trim();
-    
-    // 尝试从回复中提取JSON(可能在代码块中,也可能单独一行)
-    let requirements = null;
-    
-    // 方法1: 尝试从代码块中提取JSON
-    const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
-    if (codeBlockMatch) {
-      try {
-        requirements = JSON.parse(codeBlockMatch[1]);
-        // 移除代码块部分,保留自然语言
-        naturalLanguageReply = responseText.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '').trim();
-      } catch (e) {
-        // 解析失败,继续尝试其他方�?
-      }
-    }
-    
-    // 方法2: 尝试匹配JSON对象
-    if (!requirements) {
-      const jsonMatch = responseText.match(/\{[\s\S]*"needsImages"[\s\S]*\}/);
-      if (jsonMatch) {
-        try {
-          requirements = JSON.parse(jsonMatch[0]);
-          // 移除JSON部分,保留自然语言
-          naturalLanguageReply = responseText.replace(/\{[\s\S]*"needsImages"[\s\S]*\}/, '').trim();
-        } catch (e) {
-          // 解析失败
-        }
-      }
-    }
-    
-    // 方法3: 尝试直接解析整个文本
-    if (!requirements) {
-      try {
-        requirements = JSON.parse(responseText.trim());
-      } catch (e) {
-        // 不是纯JSON
-      }
-    }
-    
-    // 如果找到JSON,返回JSON和自然语言回复
-    if (requirements && typeof requirements === 'object') {
-      // 将自然语言回复保存到requirements中,以便后续显示
-      if (naturalLanguageReply && naturalLanguageReply !== responseText.trim()) {
-        requirements.naturalLanguageReply = naturalLanguageReply;
-      }
-      return requirements;
-    }
-    
-    // 如果没有找到JSON,可能是用户的问题与工作流无关,返回错误信息
-    return { 
-      error: naturalLanguageReply || '请描述你的工作流需求,例如:生成一个登录流程,先点击登录按钮,然后输入用户名和密码'
-    };
-  };
-
-  // 解析用户输入的文字参考(按行分割�?
-  const parseReferenceTexts = (text) => {
-    // 如果已经有工作流需求,提取文字参�?
-    if (workflowRequirements && workflowRequirements.needsTexts > 0) {
-      // 尝试从文本中提取,可能是用换行、逗号或分号分�?
-      const lines = text.split(/[\n,�?;]/).map(t => t.trim()).filter(t => t);
-      return lines.slice(0, workflowRequirements.needsTexts);
-    }
-    return [];
-  };
-
-  const onSend = async (text) => {
-    if (!text.trim() && uploadedImages.length === 0) return;
-
-    // 保存上传的图片信息(用于后续使用和显示)
-    const currentImages = [...uploadedImages];
-    
-    // 添加用户消息(包含图片信息,包含base64数据以便在聊天界面显示)
-    const userMessage = {
-      role: 'user',
-      content: text,
-      images: currentImages.map(img => ({
-        name: img.originalName,
-        tempName: img.tempName,
-        base64: img.base64, // 包含base64数据,用于在聊天界面显示
-        originalName: img.originalName
-      })),
-      timestamp: new Date(),
-    };
-    onSendMessage?.(userMessage);
-    
-    // 发送消息后立即清空上传的图片,让输入框的提示方块消�?
-    setUploadedImages([]);
-
-    // 开始加�?
-    onLoadingChange?.(true);
-
-    try {
-      // 第一阶段:如果没有需求信息,先分析需�?
-      if (!workflowRequirements) {
-        // 如果有参考图,先调用img2text获取图片描述
-        let imageDescriptionsPrompt = '';
-        if (currentImages.length > 0) {
-          const imageDescriptions = [];
-          for (const img of currentImages) {
-            const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字等信息');
-            if (description) {
-              imageDescriptions.push(description);
-            }
-          }
-          
-          if (imageDescriptions.length > 0) {
-            imageDescriptionsPrompt = `\n\n用户提供了以下参考图片及其描述:\n${imageDescriptions.map((desc, index) => `图片${index + 1}�?{desc}`).join('\n')}\n\n请根据这些图片描述,分析用户需要哪些图片和文字参考来生成工作流。`;
-          }
-        }
-        
-        const requirements = await analyzeRequirements(text + imageDescriptionsPrompt);
-        
-        if (requirements.error) {
-          // 如果返回错误信息,直接显示(可能是自然语言回复�?
-          const errorMessage = {
-            role: 'assistant',
-            content: requirements.error,
-            timestamp: new Date(),
-          };
-          onSendMessage?.(errorMessage);
-          return;
-        }
-
-        // 保存需求信�?
-        setWorkflowRequirements(requirements);
-        
-        // 显示需求信息给用户(优先显示AI的自然语言回复,然后显示需求列表)
-        const needsImages = requirements.needsImages || 0;
-        const needsTexts = requirements.needsTexts || 0;
-        
-        let requirementMessage = '';
-        
-        // 如果有自然语言回复,先显示
-        if (requirements.naturalLanguageReply) {
-          requirementMessage = requirements.naturalLanguageReply + '\n\n';
-        }
-        
-        // 然后显示需求信�?
-        if (needsImages > 0 || needsTexts > 0) {
-          requirementMessage += '📋 根据您的需求,我需要以下信息:\n\n';
-          
-          if (needsImages > 0) {
-            requirementMessage += `🖼�?需�?${needsImages} 张图片:\n`;
-            (requirements.imageDescriptions || []).forEach((desc, index) => {
-              requirementMessage += `${index + 1}. ${desc}\n`;
-            });
-            requirementMessage += '\n';
-          }
-          
-          if (needsTexts > 0) {
-            requirementMessage += `📝 需�?${needsTexts} 个文字参考:\n`;
-            (requirements.textDescriptions || []).forEach((desc, index) => {
-              requirementMessage += `${index + 1}. ${desc}\n`;
-            });
-            requirementMessage += '\n';
-          }
-          
-          requirementMessage += '璇锋寜椤哄簭涓婁紶鍥剧墖鍜岃緭鍏ユ枃瀛楀弬鑰冿紝鐒跺悗鍐嶆鍙戦€佹秷鎭€?;
-        }
-        
-        const reqMessage = {
-          role: 'assistant',
-          content: requirementMessage || '濂界殑锛屾垜鐞嗚В浜嗘偍鐨勯渶姹傘€?,
-          timestamp: new Date(),
-        };
-        onSendMessage?.(reqMessage);
-        return;
-      }
-
-      // 第二阶段:验证用户提供的信息是否完整
-      const needsImages = workflowRequirements.needsImages || 0;
-      const needsTexts = workflowRequirements.needsTexts || 0;
-      const providedImages = currentImages.length;
-      const parsedTexts = parseReferenceTexts(text);
-      const providedTexts = parsedTexts.length;
-
-      if (providedImages < needsImages || providedTexts < needsTexts) {
-        // 信息不完整,提示用户
-        let missingMessage = '鉂?淇℃伅涓嶅畬鏁达紝璇锋彁渚涳細\n\n';
-        if (providedImages < needsImages) {
-          missingMessage += 馃柤锔?杩橀渶瑕?\ 寮犲浘鐗嘰n;
-        }
-        if (providedTexts < needsTexts) {
-          missingMessage += 馃摑 杩橀渶瑕?\ 涓枃瀛楀弬鑰僜n;
-        }
-        missingMessage += '\n璇锋寜椤哄簭涓婁紶鍥剧墖鍜岃緭鍏ユ枃瀛楀弬鑰冿紝鐒跺悗鍐嶆鍙戦€佹秷鎭€?;
-        
-        const missingMsg = {
-          role: 'assistant',
-          content: missingMessage,
-          timestamp: new Date(),
-        };
-        onSendMessage?.(missingMsg);
-        return;
-      }
-
-      // 第三阶段:信息完整,生成工作�?
-      let namedImages = [];
-      if (currentImages.length > 0) {
-        try {
-          // 使用需求描述给图片命名
-          const imageDescriptions = workflowRequirements.imageDescriptions || [];
-          namedImages = currentImages.map((img, index) => ({
-            ...img,
-            aiName: imageDescriptions[index] 
-              ? `${imageDescriptions[index]}.png` 
-              : `${index + 1}.png`
-          }));
-        } catch (nameError) {
-          console.error('图片命名失败,使用默认命�?', nameError);
-          namedImages = currentImages.map((img, index) => ({
-            ...img,
-            aiName: `${index + 1}.png`
-          }));
-        }
-      }
-
-      // 构建包含文字参考的提示�?
-      let textReferencesPrompt = '';
-      if (parsedTexts.length > 0) {
-        textReferencesPrompt = `\n\n用户提供的文字参考:\n${parsedTexts.map((t, i) => `${i + 1}. ${t}`).join('\n')}\n\n在生成工作流时,如果操作需要定位文字,请使用上述文字参考。`;
-        // 保存文字参考到state
-        setReferenceTexts(parsedTexts);
-      }
-
-      // 如果有参考图,调用img2text获取图片描述并加入到prompt�?
-      let imageDescriptionsPrompt = '';
-      if (currentImages.length > 0) {
-        const imageDescriptions = [];
-        for (const img of currentImages) {
-          const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字、布局等信息,这些信息将用于生成自动化工作�?);
-          if (description) {
-            imageDescriptions.push(description);
-          }
-        }
-        
-        if (imageDescriptions.length > 0) {
-          imageDescriptionsPrompt = `\n\n参考图片描述:\n${imageDescriptions.map((desc, index) => {
-            const imgName = namedImages[index]?.aiName || `${index + 1}.png`;
-            return `图片 ${imgName}�?{desc}`;
-          }).join('\n')}\n\n请根据这些图片描述,在生成工作流时合理使用对应的图片文件名。`;
-        }
-      }
-
-      // 发送到GPT并获取回复(传递已命名的图片信息和文字参考)
-      const response = await sendToGPT(text + textReferencesPrompt + imageDescriptionsPrompt, namedImages);
-      
-      // 根据实际 API 响应结构解析文本
-      // API 返回: { success: true, data: { output_text: "..." } }
-      const responseText = response.data?.output_text || 
-                          response.text || 
-                          response.content || 
-                          response.message || 
-                          response.data?.text ||
-                          response.data?.content ||
-                          response.result ||
-                          (typeof response === 'string' ? response : JSON.stringify(response));
-      
-      // 检测是否是JSON格式的工作流
-      const detectedWorkflowJson = extractWorkflowJsonFromText(responseText);
-      
-      if (detectedWorkflowJson) {
-        // 如果检测到工作流JSON,保存到内存中,不显示在聊天�?
-        setWorkflowJson(detectedWorkflowJson);
-        
-        // 准备图片数据(base64和文件名�?
-        const imagesData = namedImages.map(img => ({
-          name: img.aiName || img.tempName,
-          base64: img.base64,
-          originalName: img.originalName
-        }));
-
-        // 建立图片文件名映射:将JSON中可能使用的简单文件名(如 "1.png")映射到实际保存的文件名
-        // 这样即使AI使用�?"1.png"�?2.png" 等简单名称,也能正确映射到实际的文件�?
-        const imageNameMap = {};
-        namedImages.forEach((img, index) => {
-          const actualName = img.aiName || img.tempName;
-          // 映射 "1.png", "2.png" �?
-          imageNameMap[`${index + 1}.png`] = actualName;
-          // 也映射原始名称(如果有)
-          if (img.originalName) {
-            imageNameMap[img.originalName] = actualName;
-          }
-        });
-
-        // 在保存前,更新工作流JSON中的图片文件�?
-        const updatedWorkflowJson = updateImagePathsInWorkflow(detectedWorkflowJson, imageNameMap);
-
-        // 直接保存工作流(不显示JSON内容�?
-        try {
-          if (window.electronAPI && window.electronAPI.saveWorkflow) {
-            const saveResult = await window.electronAPI.saveWorkflow(updatedWorkflowJson, imagesData);
-            if (saveResult.success) {
-              // 保存成功,清空所有状态(图片已经在发送时清空�?
-              setWorkflowRequirements(null);
-              setReferenceTexts([]);
-              setWorkflowJson(null);
-              
-              // 显示成功消息(不包含JSON内容�?
-              const successMessage = {
-                role: 'assistant',
-                content: `�?工作流已成功生成并保存!\n\n文件夹名称:${saveResult.folderName}`,
-                timestamp: new Date(),
-              };
-              onSendMessage?.(successMessage);
-              
-              // 触发工作流列表刷新事�?
-              window.dispatchEvent(new CustomEvent('workflow-saved'));
-              return;
-            } else {
-              // 保存失败,显示错�?
-              console.error('保存工作流失�?', saveResult.error);
-              const errorMessage = {
-                role: 'assistant',
-                content: `�?保存工作流失败:${saveResult.error}`,
-                timestamp: new Date(),
-                error: true,
-              };
-              onSendMessage?.(errorMessage);
-              return;
-            }
-          }
-        } catch (saveError) {
-          console.error('保存工作流时出错:', saveError);
-          const errorMessage = {
-            role: 'assistant',
-            content: `�?保存工作流时出错�?{saveError.message}`,
-            timestamp: new Date(),
-            error: true,
-          };
-          onSendMessage?.(errorMessage);
-          return;
-        }
-      }
-      
-      // 如果没有检测到工作流JSON,显示原始回�?
-      const gptMessage = {
-        role: 'assistant',
-        content: responseText || '抱歉,我无法理解您的请求�?,
-        timestamp: new Date(),
-      };
-      onSendMessage?.(gptMessage);
-    } catch (error) {
-      // 添加错误消息,显示更详细的错误信�?
-      let errorContent = '抱歉,发送消息时出现错误�?;
-      
-      if (error.message) {
-        if (error.message.includes('服务器错�?)) {
-          errorContent = error.message;
-        } else if (error.message.includes('HTTP 500')) {
-          errorContent = '服务器内部错误,请检�?API 端点是否正确,或联系服务器管理员�?;
-        } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
-          errorContent = '网络连接失败,请检查网络连接或服务器是否可访问�?;
-        } else {
-          errorContent = `错误�?{error.message}`;
-        }
-      }
-      
-      const errorMessage = {
-        role: 'assistant',
-        content: errorContent,
-        timestamp: new Date(),
-        error: true,
-      };
-      onSendMessage?.(errorMessage);
-    } finally {
-      // 结束加载
-      onLoadingChange?.(false);
-    }
-  };
-
-  return {
-    onSend,
-    onUpload,
-    uploadedImages,
-    removeImage,
-    workflowRequirements,
-    setWorkflowRequirements,
-    referenceTexts,
-    setReferenceTexts,
-  };
-}
-
-// 更新工作流JSON中的图片路径
-function updateImagePathsInWorkflow(workflowJson, imageNameMap) {
-  // 深拷贝,避免修改原始对象
-  const updated = JSON.parse(JSON.stringify(workflowJson));
-  
-  // 递归更新actions中的图片路径
-  function updateActions(actions) {
-    if (!Array.isArray(actions)) return;
-    
-    for (const action of actions) {
-      // locate �?click 操作可能使用图片
-      if ((action.type === 'locate' || action.type === 'click') && action.method === 'image' && action.target) {
-        const target = action.target;
-        // 检查是否是映射表中的文件名
-        if (imageNameMap[target]) {
-          action.target = imageNameMap[target];
-        } else {
-          // 尝试匹配 "1.png", "2.png" 等格�?
-          const match = target.match(/^(\d+)\.png$/i);
-          if (match) {
-            const index = parseInt(match[1]) - 1;
-            if (imageNameMap[`${index + 1}.png`]) {
-              action.target = imageNameMap[`${index + 1}.png`];
-            }
-          }
-        }
-      }
-      
-      // ocr 操作可能使用头像图片
-      if (action.type === 'ocr' && action.method === 'by-avatar' && action.avatar) {
-        const avatar = action.avatar;
-        if (imageNameMap[avatar]) {
-          action.avatar = imageNameMap[avatar];
-        } else {
-          const match = avatar.match(/^(\d+)\.png$/i);
-          if (match) {
-            const index = parseInt(match[1]) - 1;
-            if (imageNameMap[`${index + 1}.png`]) {
-              action.avatar = imageNameMap[`${index + 1}.png`];
-            }
-          }
-        }
-      }
-      
-      // 递归处理嵌套�?actions(if、for、while�?
-      if (action.then && Array.isArray(action.then)) {
-        updateActions(action.then);
-      }
-      if (action.else && Array.isArray(action.else)) {
-        updateActions(action.else);
-      }
-      if (action.body && Array.isArray(action.body)) {
-        updateActions(action.body);
-      }
-    }
-  }
-  
-  if (updated.actions && Array.isArray(updated.actions)) {
-    updateActions(updated.actions);
-  }
-  
-  return updated;
-}
-
-// 从文本中提取JSON格式的工作流(支持新旧格式)
-function extractWorkflowJsonFromText(text) {
-    if (!text) return null;
-    
-    // 尝试直接解析整个文本
-    try {
-      const parsed = JSON.parse(text.trim());
-      // 新格式:包含 actions 字段
-      if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-        return parsed;
-      }
-      // 旧格式:直接是数�?
-      if (Array.isArray(parsed) && parsed.length > 0) {
-        return { actions: parsed };
-      }
-    } catch (e) {
-      // 不是纯JSON,继续尝试提�?
-    }
-    
-    // 尝试从代码块中提取JSON
-    const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
-    if (codeBlockMatch) {
-      try {
-        const parsed = JSON.parse(codeBlockMatch[1]);
-        if (parsed && typeof parsed === 'object') {
-          // 新格式:包含 actions
-          if (Array.isArray(parsed.actions)) {
-          return parsed;
-          }
-          // 旧格式:直接是数�?
-          if (Array.isArray(parsed)) {
-            return { actions: parsed };
-          }
-        }
-      } catch (e) {
-        // 解析失败
-      }
-    }
-    
-    // 尝试查找JSON对象(查找包�?actions"的对象,支持多行�?
-    const jsonMatch = text.match(/\{[\s\S]*?"actions"[\s\S]*?\}/);
-    if (jsonMatch) {
-      try {
-        const parsed = JSON.parse(jsonMatch[0]);
-        if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-          return parsed;
-        }
-      } catch (e) {
-        // 解析失败,尝试更宽松的匹�?
-        try {
-          // 尝试匹配整个JSON对象(包括嵌套结构)
-          const fullJsonMatch = text.match(/\{(?:[^{}]|(?:\{[^{}]*\}))*\}/);
-          if (fullJsonMatch) {
-            const parsed = JSON.parse(fullJsonMatch[0]);
-            if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
-              return parsed;
-            }
-          }
-        } catch (e2) {
-        // 解析失败
-        }
-      }
-    }
-    
-    return null;
-}

+ 160 - 0
src/pages/Chat/Input/resource-require.js

@@ -0,0 +1,160 @@
+// 第一阶段提示词:分析需求,返回需要的信息
+const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手。你需要像聊天一样与用户对话。
+
+如果用户想要创建自动化工作流,你需要分析需求并返回需要的信息。请用自然、友好的语言与用户交流,保持对话的连贯性和友好性。
+
+工作流语法分层(重要):
+- 基础语法(控制流/定时):schedule、if、while
+- 基础 action:adb(通过 method 区分:input、click、locate、swipe、scroll、press、string-press)
+- 扩展标签(Func):由 src/pages/processing/func/ 目录下的脚本文件决定,每个脚本文件名即为标签名
+  - 常用标签:ocr-chat、read-chat-history、read-last-message、save-new-chat、image-center-location、image-region-location、ai-generate
+
+工作流基本结构:
+{
+  "variables": {},
+  "execute": []
+}
+
+在生成工作流时:
+- 逻辑结构使用 schedule、if、while 表达
+- 手机操作统一使用 adb 类型,通过 method 区分
+- 当需求超出基础 action,使用对应的 Func 标签(如:ocr-chat、image-center-location)
+
+重要规则:
+- 变量使用 {variableName} 格式引用
+- 图片路径直接写文件名(不需要 resources/ 前缀,代码会自动处理)
+- 如果工作流中需要输入文字(使用 adb 的 input 方法或 ai-generate 操作),且这些文字需要根据场景背景和用户角色来生成,你必须询问用户以下信息:
+  - 场景的背景:这个工作流在什么场景下使用?(例如:微信聊天、客服回复、评论互动等)
+  - 用户的角色:用户在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
+  - 这些信息对于生成合适的文字内容非常重要,请务必在询问需求时主动询问
+
+请分析用户的需求,根据上述操作类型,判断需要哪些图片和文字参考:
+- 图片(needsImages):用于 adb 的 locate/click 方法的 image 方式,或 ocr-chat 的头像
+- 文字参考(needsTexts):用于 adb 的 locate/click 方法的 text 方式
+
+如果用户想要创建自动化工作流:
+1. 先友好地确认理解用户的需求
+2. 然后返回JSON格式的需求信息(不包含在自然语言回复中,仅作为JSON返回)
+
+需求分析的JSON格式:
+{
+  "needsImages": 2,  // 需要多少张图片(用于图像匹配定位)
+  "needsTexts": 1,   // 需要多少个文字参考(用于文字识别定位)
+  "imageDescriptions": ["登录按钮", "用户名输入框"],  // 每张图片的描述
+  "textDescriptions": ["登录按钮文字"]  // 每个文字参考的描述
+}
+
+返回格式:先自然语言回复,然后单独一行JSON。
+
+**特别提醒**:如果工作流涉及输入文字(如自动回复、自动评论、自动发送消息等),请务必询问用户场景背景和角色信息。
+
+例如(不涉及文字输入的场景):
+我理解了,您想创建一个登录流程的自动化工作流。为了生成准确的工作流配置,我需要您提供以下信息:
+
+需要的信息:
+需要 2 张图片:登录按钮截图、用户名输入框截图
+需要 1 个文字参考:登录按钮上的文字
+
+{
+  "needsImages": 2,
+  "needsTexts": 1,
+  "imageDescriptions": ["登录按钮", "用户名输入框"],
+  "textDescriptions": ["登录按钮文字"]
+}
+
+例如(涉及文字输入的场景):
+我理解了,您想创建一个自动聊天回复的自动化工作流。为了生成准确且符合场景的文字内容,我需要了解以下信息:
+
+需要的信息:
+需要 2 张图片:好友头像截图、我的头像截图
+需要 1 个文字参考:输入框位置
+**场景信息**(重要):
+   - 场景背景:这个聊天是在什么场景下进行的?(例如:微信聊天、QQ群聊、客服对话等)
+   - 您的角色:您在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
+
+{
+  "needsImages": 2,
+  "needsTexts": 1,
+  "imageDescriptions": ["好友头像", "我的头像"],
+  "textDescriptions": ["输入框位置"]
+}
+
+如果用户的话题与工作流无关,请友好地自然回复,不需要返回JSON。如果用户是在询问如何使用、或者其他一般性问题,请用自然语言友好地回答。`;
+
+/**
+ * 分析用户需求,返回需要准备的素材信息
+ * @param {string} userPrompt - 用户输入的需求描述
+ * @returns {Promise<Object>} - 返回需求信息对象,包含 needsImages, needsTexts, imageDescriptions, textDescriptions 等
+ */
+export async function analyzeRequirements(userPrompt) {
+  const response = await fetch('https://ai-anim.com/api/text2textByModel', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      prompt: `${REQUIREMENTS_PROMPT}\n\n用户需求:${userPrompt}`,
+      modelName: 'gpt-5-nano-ca'
+    })
+  });
+
+  if (!response.ok) {
+    throw new Error('分析需求失败');
+  }
+
+  const data = await response.json();
+  const responseText = data.data?.output_text || data.text || data.content || '';
+  
+  // 提取自然语言回复(去除JSON部分)
+  let naturalLanguageReply = responseText.trim();
+  
+  // 尝试从回复中提取JSON(可能在代码块中,也可能单独一行)
+  let requirements = null;
+  
+  // 方法1: 尝试从代码块中提取JSON
+  const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
+  if (codeBlockMatch) {
+    try {
+      requirements = JSON.parse(codeBlockMatch[1]);
+      // 移除代码块部分,保留自然语言
+      naturalLanguageReply = responseText.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '').trim();
+    } catch (e) {
+      // 解析失败,继续尝试其他方法
+    }
+  }
+  
+  // 方法2: 尝试匹配JSON对象
+  if (!requirements) {
+    const jsonMatch = responseText.match(/\{[\s\S]*"needsImages"[\s\S]*\}/);
+    if (jsonMatch) {
+      try {
+        requirements = JSON.parse(jsonMatch[0]);
+        // 移除JSON部分,保留自然语言
+        naturalLanguageReply = responseText.replace(/\{[\s\S]*"needsImages"[\s\S]*\}/, '').trim();
+      } catch (e) {
+        // 解析失败
+      }
+    }
+  }
+  
+  // 方法3: 尝试直接解析整个文本
+  if (!requirements) {
+    try {
+      requirements = JSON.parse(responseText.trim());
+    } catch (e) {
+      // 不是纯JSON
+    }
+  }
+  
+  // 如果找到JSON,返回JSON和自然语言回复
+  if (requirements && typeof requirements === 'object') {
+    // 将自然语言回复保存到requirements中,以便后续显示
+    if (naturalLanguageReply && naturalLanguageReply !== responseText.trim()) {
+      requirements.naturalLanguageReply = naturalLanguageReply;
+    }
+    return requirements;
+  }
+  
+  // 如果没有找到JSON,可能是用户的问题与工作流无关,返回错误信息
+  return { 
+    error: naturalLanguageReply || '请描述你的工作流需求,例如:生成一个登录流程,先点击登录按钮,然后输入用户名和密码'
+  };
+}

+ 99 - 8
src/pages/Processing/Func/chat/chat-history.js

@@ -5,13 +5,13 @@
 
 /**
  * 保存聊天记录到 history 文件夹
- * @param {string} chatHistory - 聊天记录文本(格式:好友: xxx\n我: xxx
+ * @param {string|Array} chatHistory - 聊天记录(可以是JSON字符串、文本格式或消息数组
  * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  * @returns {Promise<{success: boolean, error?: string, filePath?: string}>}
  */
 export async function saveChatHistory(chatHistory, folderPath) {
   try {
-    if (!chatHistory || !chatHistory.trim()) {
+    if (!chatHistory) {
       return { success: false, error: '聊天记录为空' };
     }
 
@@ -19,25 +19,116 @@ export async function saveChatHistory(chatHistory, folderPath) {
       return { success: false, error: '保存聊天记录 API 不可用' };
     }
 
-    // 解析聊天记录文本,转换为结构化数据
-    const messages = parseChatHistoryText(chatHistory);
+    // 解析聊天记录,转换为消息数组
+    let newMessages = [];
+    
+    if (Array.isArray(chatHistory)) {
+      // 如果已经是数组,直接使用
+      newMessages = chatHistory;
+    } else if (typeof chatHistory === 'string') {
+      // 如果是字符串,尝试解析
+      try {
+        // 先尝试作为JSON字符串解析
+        const parsed = JSON.parse(chatHistory);
+        if (Array.isArray(parsed)) {
+          newMessages = parsed;
+        } else {
+          // 如果不是数组,按文本格式解析
+          newMessages = parseChatHistoryText(chatHistory);
+        }
+      } catch (e) {
+        // JSON解析失败,按文本格式解析
+        newMessages = parseChatHistoryText(chatHistory);
+      }
+    }
+
+    if (newMessages.length === 0) {
+      return { success: false, error: '聊天记录解析后为空' };
+    }
+
+    // 读取历史聊天记录,进行去重
+    let historyMessages = [];
+    try {
+      if (window.electronAPI && window.electronAPI.readChatHistory) {
+        const historyResult = await window.electronAPI.readChatHistory(folderPath);
+        if (historyResult.success && historyResult.messages && Array.isArray(historyResult.messages)) {
+          historyMessages = historyResult.messages;
+        }
+      }
+    } catch (error) {
+      console.warn('读取历史聊天记录失败,将保存所有新消息:', error);
+    }
+
+    // 对历史消息构建查找集合(用于去重)
+    const historyMessageMap = new Map();
+    historyMessages.forEach((msg, index) => {
+      const normalizedText = (msg.text || '').trim();
+      const key = `${msg.sender || ''}:${normalizedText}`;
+      // 如果同一个 key 已存在,保留索引较小的(更早的消息)
+      if (!historyMessageMap.has(key)) {
+        historyMessageMap.set(key, {
+          index,
+          sender: msg.sender,
+          text: normalizedText
+        });
+      }
+    });
+
+    // 找出真正新的消息(不在历史记录中的)
+    const uniqueNewMessages = [];
+    const duplicateMessages = [];
+    
+    for (const newMsg of newMessages) {
+      const normalizedText = (newMsg.text || '').trim();
+      const key = `${newMsg.sender || ''}:${normalizedText}`;
+      
+      // 检查是否已存在于历史记录中
+      if (!historyMessageMap.has(key)) {
+        // 这是新消息
+        uniqueNewMessages.push(newMsg);
+      } else {
+        // 消息已存在,记录为重复消息
+        const existingMsg = historyMessageMap.get(key);
+        duplicateMessages.push({
+          sender: existingMsg.sender,
+          text: normalizedText.substring(0, 50) + (normalizedText.length > 50 ? '...' : '')
+        });
+      }
+    }
+    
+    if (uniqueNewMessages.length === 0) {
+      console.log(`没有新消息(所有 ${newMessages.length} 条消息都与历史记录重合),跳过保存`);
+      if (duplicateMessages.length > 0) {
+        console.log(`重复消息示例: ${duplicateMessages[0].sender}: ${duplicateMessages[0].text}`);
+      }
+      return { success: true, skipped: true, reason: 'no_new_messages' };
+    }
+
+    // 合并消息:历史消息 + 新的唯一消息
+    const mergedMessages = [...historyMessages, ...uniqueNewMessages];
+    
+    console.log(`将保存 ${uniqueNewMessages.length} 条新消息(已剔除 ${duplicateMessages.length} 条重复消息)`);
+    if (duplicateMessages.length > 0 && duplicateMessages.length <= 5) {
+      console.log(`重复消息: ${duplicateMessages.map(m => `${m.sender}: ${m.text}`).join('; ')}`);
+    } else if (duplicateMessages.length > 5) {
+      console.log(`重复消息示例: ${duplicateMessages.slice(0, 3).map(m => `${m.sender}: ${m.text}`).join('; ')} ...`);
+    }
 
     // 构建保存的数据结构
     const now = new Date();
     const historyData = {
       timestamp: now.toISOString(),
-      messageCount: messages.length,
-      messages: messages
+      messageCount: mergedMessages.length,
+      messages: mergedMessages
     };
 
-    // 通过 IPC 调用主进程保存聊天记录
+    // 通过 IPC 调用主进程保存聊天记录(追加模式)
     const result = await window.electronAPI.saveChatHistory(folderPath, historyData);
     
     if (!result.success) {
       return { success: false, error: result.error };
     }
 
-    console.log(`聊天记录已保存到: ${result.filePath}`);
     return { success: true, filePath: result.filePath };
   } catch (error) {
     console.error('保存聊天记录失败:', error);

+ 29 - 21
src/pages/Processing/Func/chat/ocr-chat-history.js → src/pages/Processing/Func/chat/ocr-chat.js

@@ -1,29 +1,29 @@
 /**
- * Func 标签:ocr-chat-history
+ * Func 标签:ocr-chat
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
  *
- * 运行时真实执行逻辑由 ActionParser + electronAPI.extractChatHistory + main-js/func/ocr-chat-history.js 实现。
+ * 运行时真实执行逻辑由 ActionParser + electronAPI.extractChatHistory + main-js/func/ocr-chat.js 实现。
  */
 
-export const tagName = 'ocr-chat-history';
+export const tagName = 'ocr-chat';
 
 export const schema = {
-  description: '从屏幕截图中提取会话/消息记录(包含发送者),并格式化保存到变量,供后续 AI 或逻辑判断使用。',
+  description: '根据图片识别对话内容,区分好友和自己的对话,输出JSON字符串格式的消息记录。',
   inputs: {
     // 按用户要求:抽象为 avatar1/avatar2,不绑定 friend/me 语义
     avatar1: '参与者1的头像/标识图片路径(相对于工作流目录)',
     avatar2: '参与者2的头像/标识图片路径(相对于工作流目录)',
-    variable: '输出变量名(保存格式化后的消息文本)',
+    variable: '输出变量名(保存JSON字符串格式的消息记录)',
   },
   outputs: {
-    variable: '消息记录文本(例如:对方: xxx\\n我: yyy)',
+    variable: 'JSON字符串格式的消息记录(数组,每个元素包含 sender 和 text 字段)',
   },
 };
 
 /**
- * 执行 ocr-chat-history 功能
+ * 执行 ocr-chat 功能
  * 这个函数会被 ActionParser 调用
  * 
  * @param {Object} params - 参数对象
@@ -31,9 +31,9 @@ export const schema = {
  * @param {string} params.avatar1 - 参与者1的头像路径
  * @param {string} params.avatar2 - 参与者2的头像路径
  * @param {string} params.folderPath - 工作流文件夹路径
- * @returns {Promise<{success: boolean, messagesText?: string, error?: string}>}
+ * @returns {Promise<{success: boolean, messagesJson?: string, messages?: Array, error?: string}>}
  */
-export async function executeOcrChatHistory({ device, avatar1, avatar2, folderPath }) {
+export async function executeOcrChat({ device, avatar1, avatar2, folderPath }) {
   try {
     if (!window.electronAPI || !window.electronAPI.extractChatHistory) {
       return { 
@@ -54,26 +54,34 @@ export async function executeOcrChatHistory({ device, avatar1, avatar2, folderPa
       return { success: false, error: result.error };
     }
 
-    // 格式化消息文本
-    let messagesText = '';
+    // 获取消息数组
+    let messages = [];
     if (result.messages && Array.isArray(result.messages)) {
-      messagesText = result.messages
-        .map(msg => {
-          const sender = msg.sender === 'friend' ? '对方' : msg.sender === 'me' ? '我' : '未知';
-          return `${sender}: ${msg.text || ''}`;
-        })
-        .join('\n');
+      messages = result.messages;
     } else if (result.messagesText) {
-      messagesText = result.messagesText;
+      // 如果只有文本,尝试解析(向后兼容)
+      const lines = result.messagesText.split('\n').filter(line => line.trim());
+      for (const line of lines) {
+        const match = line.match(/^(对方|好友|我):\s*(.+)$/);
+        if (match) {
+          const senderLabel = match[1];
+          const sender = (senderLabel === '对方' || senderLabel === '好友') ? 'friend' : 'me';
+          const text = match[2].trim();
+          messages.push({ sender, text });
+        }
+      }
     }
 
+    // 将消息数组转换为JSON字符串
+    const messagesJson = JSON.stringify(messages);
+
     return {
       success: true,
-      messagesText: messagesText,
-      messages: result.messages || []
+      messagesJson: messagesJson,
+      messages: messages
     };
   } catch (error) {
-    console.error('执行 ocr-chat-history 失败:', error);
+    console.error('执行 ocr-chat 失败:', error);
     return { 
       success: false, 
       error: error.message || '提取聊天记录失败' 

+ 2 - 2
src/pages/Processing/Func/chat/read-chat-history.js

@@ -8,10 +8,10 @@ export const tagName = 'read-chat-history';
 export const schema = {
   description: '读取聊天记录的所有消息(合并所有历史文件)。',
   inputs: {
-    variable: '输出变量名(保存聊天记录数组)',
+    variable: '输出变量名(保存聊天记录 JSON 字符串)',
   },
   outputs: {
-    variable: '包含所有消息的数组,每个消息包含 sender 和 text 字段',
+    variable: 'JSON字符串格式的消息记录(数组,每个元素包含 sender 和 text 字段)',
   },
 };
 

+ 20 - 3
src/pages/Processing/Func/chat/read-last-message.js

@@ -64,8 +64,20 @@ export async function executeReadLastMessage({ folderPath, inputData, textVariab
     // 如果提供了 inputData,从变量读取
     if (inputData !== undefined && inputData !== null) {
       if (typeof inputData === 'string') {
-        // 如果是字符串,解析为消息数组
-        messages = parseChatHistoryText(inputData);
+        // 如果是字符串,先尝试作为JSON字符串解析
+        try {
+          const parsed = JSON.parse(inputData);
+          if (Array.isArray(parsed)) {
+            // 是JSON数组格式
+            messages = parsed;
+          } else {
+            // JSON解析成功但不是数组,按文本格式解析
+            messages = parseChatHistoryText(inputData);
+          }
+        } catch (e) {
+          // JSON解析失败,按文本格式解析(例如:"对方: xxx\n我: yyy")
+          messages = parseChatHistoryText(inputData);
+        }
       } else if (Array.isArray(inputData)) {
         // 如果已经是数组,直接使用
         messages = inputData;
@@ -74,7 +86,12 @@ export async function executeReadLastMessage({ folderPath, inputData, textVariab
       }
 
       if (messages.length === 0) {
-        return { success: false, error: '输入数据中没有消息' };
+        // 如果数组为空,返回空值而不是报错(允许第一次运行或历史记录为空的情况)
+        return { 
+          success: true, 
+          text: '', 
+          sender: '' 
+        };
       }
 
       // 获取最后一条消息

+ 1 - 1
src/pages/Processing/Func/chat/save-new-chat.js

@@ -13,7 +13,7 @@ export const tagName = 'save-new-chat';
 export const schema = {
   description: '将消息记录/聊天记录保存到工作流目录的 history 文件夹,按时间戳落盘(JSON)。',
   inputs: {
-    variable: '要保存的变量名(一般是 ocr-chat-history 的输出变量)',
+    variable: '要保存的变量名(一般是 ocr-chat 的输出变量)',
   },
   outputs: {
     filePath: '保存后的文件路径(可选)',

+ 4 - 3
src/pages/Processing/Func/image-center-location.js

@@ -14,7 +14,7 @@ export const schema = {
     variable: '输出变量名(保存中心点坐标)',
   },
   outputs: {
-    variable: '中心点坐标 {x, y}',
+    variable: '中心点坐标(JSON 字符串格式,如:{"x":123,"y":456})',
   },
 };
 
@@ -44,10 +44,11 @@ export async function executeImageCenterLocation({ device, template, folderPath
       };
     }
 
-    // 构建模板图片完整路径(如果路径不是绝对路径,则相对于工作流目录)
+    // 构建模板图片完整路径(如果路径不是绝对路径,则相对于工作流目录的 resources 文件夹)
+    // resources 作为根目录
     const templatePath = template.startsWith('/') || template.includes(':') 
       ? template 
-      : `${folderPath}/${template}`;
+      : `${folderPath}/resources/${template}`;
 
     // 调用主进程的图像匹配函数(会自动获取设备截图)
     const result = await window.electronAPI.matchImageAndGetCoordinate(

+ 4 - 3
src/pages/Processing/Func/image-region-location.js

@@ -47,15 +47,16 @@ export async function executeImageRegionLocation({ device, screenshot, region, f
     if (screenshot === null) {
       screenshotPath = '__AUTO_SCREENSHOT__';
     } else if (screenshot) {
-      // 构建完整路径(如果路径不是绝对路径,则相对于工作流目录)
+      // 构建完整路径(如果路径不是绝对路径,则相对于工作流目录的 resources 文件夹)
+      // resources 作为根目录
       screenshotPath = screenshot.startsWith('/') || screenshot.includes(':') 
         ? screenshot 
-        : `${folderPath}/${screenshot}`;
+        : `${folderPath}/resources/${screenshot}`;
     }
     
     const regionPath = region.startsWith('/') || region.includes(':') 
       ? region 
-      : `${folderPath}/${region}`;
+      : `${folderPath}/resources/${region}`;
 
     // 调用主进程的图像区域定位函数
     // 如果 screenshotPath 是 '__AUTO_SCREENSHOT__',主进程会自动获取截图

+ 403 - 117
src/pages/Processing/action-parser.js

@@ -6,13 +6,62 @@ const DEFAULT_SCROLL_DISTANCE = 100; // 默认每次滚动距离(像素)
 // 变量上下文(用于存储变量值)
 let variableContext = {};
 
+// 变量上下文是否已初始化(防止循环中重置变量)
+let variableContextInitialized = false;
+
 // 全局步骤计数器(用于连续的步骤编号)
 let globalStepCounter = 0;
 
+// 当前工作流文件夹路径(用于日志记录)
+let currentWorkflowFolderPath = null;
+
+/**
+ * 记录日志(同时输出到 console 和文件)
+ * @param {string} message - 日志消息
+ * @param {string} folderPath - 工作流文件夹路径(可选,如果不提供则使用 currentWorkflowFolderPath)
+ */
+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 => {
+        // 静默失败,不影响主流程
+        console.error('写入日志失败:', err);
+      });
+    }
+  } catch (error) {
+    // 静默失败,不影响主流程
+  }
+}
+
+// 打印所有 outVars 的值
+async function logOutVars(action, variableContext, folderPath = null) {
+  if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) {
+    return;
+  }
+  
+  const outVarsInfo = action.outVars.map((varName, index) => {
+    const varNameClean = extractVarName(varName);
+    const value = variableContext[varNameClean];
+    // 如果值太长,截断显示(超过100字符)
+    let displayValue = value;
+    if (typeof value === 'string' && value.length > 100) {
+      displayValue = value.substring(0, 100) + '...';
+    }
+    return `${varNameClean}: ${JSON.stringify(displayValue)}`;
+  });
+  
+  const logMsg = `输出变量: ${outVarsInfo.join(', ')}`;
+  await logMessage(logMsg, folderPath);
+}
+
 // 导入聊天历史记录管理模块
 import { saveChatHistory, generateHistorySummary, getHistorySummary } from './func/chat/chat-history.js';
-// 导入 ocr-chat-history 执行函数
-import { executeOcrChatHistory } from './func/chat/ocr-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 执行函数
@@ -125,6 +174,59 @@ function extractVarName(varName) {
   return varName;
 }
 
+/**
+ * 替换字符串中的变量(支持 {{variable}} 和 {variable} 格式)
+ * @param {string} str - 原始字符串
+ * @param {Object} context - 变量上下文
+ * @returns {string} 替换后的字符串
+ */
+function replaceVariablesInString(str, context = variableContext) {
+  if (typeof str !== 'string') {
+    return str;
+  }
+  
+  let result = str;
+  
+  // 优先替换 {{variable}} 格式的变量(双花括号)
+  const doubleBracePattern = /\{\{(\w+)\}\}/g;
+  result = result.replace(doubleBracePattern, (match, varName) => {
+    const varValue = context[varName];
+    if (varValue === undefined || varValue === null) {
+      return match;
+    }
+    // 如果是数组或对象,转换为 JSON 字符串
+    if (Array.isArray(varValue) || typeof varValue === 'object') {
+      try {
+        return JSON.stringify(varValue);
+      } catch (e) {
+        return String(varValue);
+      }
+    }
+    return String(varValue);
+  });
+  
+  // 也支持 {variable} 格式的变量(单花括号,向后兼容)
+  // 注意:使用负向前瞻确保不会匹配已经是 {{}} 格式的开始部分
+  const varPattern = /\{(\w+)\}(?!\})/g;
+  result = result.replace(varPattern, (match, varName) => {
+    const varValue = context[varName];
+    if (varValue === undefined || varValue === null) {
+      return match;
+    }
+    // 如果是数组或对象,转换为 JSON 字符串
+    if (Array.isArray(varValue) || typeof varValue === 'object') {
+      try {
+        return JSON.stringify(varValue);
+      } catch (e) {
+        return String(varValue);
+      }
+    }
+    return String(varValue);
+  });
+  
+  return result;
+}
+
 /**
  * 解析变量值(支持 {variable} 格式)
  * @param {any} value - 原始值
@@ -166,10 +268,31 @@ function evaluateCondition(condition, context = variableContext) {
       const value = context[varName];
       if (value === undefined) return 'undefined';
       if (typeof value === 'string') {
+        // 尝试判断是否是 JSON 字符串
+        try {
+          const parsed = JSON.parse(value);
+          if (Array.isArray(parsed)) {
+            // 如果是 JSON 数组字符串,转换为 JSON 字符串用于比较(保持原格式)
+            const escaped = value.replace(/"/g, '\\"');
+            return `"${escaped}"`;
+          }
+        } catch (e) {
+          // 不是 JSON,按普通字符串处理
+        }
         // 转义字符串中的引号
         const escaped = value.replace(/"/g, '\\"');
         return `"${escaped}"`;
       }
+      if (Array.isArray(value)) {
+        // 数组转换为 JSON 字符串(与 chat-history 和 currentMessage 格式一致)
+        try {
+          const jsonStr = JSON.stringify(value);
+          const escaped = jsonStr.replace(/"/g, '\\"');
+          return `"${escaped}"`;
+        } catch (e) {
+          return `"[]"`;
+        }
+      }
       if (typeof value === 'number') return value;
       if (typeof value === 'boolean') return value;
       // 其他类型转为字符串
@@ -179,7 +302,6 @@ function evaluateCondition(condition, context = variableContext) {
     // 手动解析简单的条件表达式(不使用eval)
     // 支持: ==, !=, >, <, >=, <=, &&, ||
     const result = parseConditionExpression(expr);
-    console.log(`条件评估: "${condition}" -> "${expr}" = ${result}`);
     return result;
   } catch (error) {
     console.error('条件评估失败:', error, condition);
@@ -212,7 +334,21 @@ function parseConditionExpression(expr) {
   // 处理比较运算符
   const operators = [
     { op: '!=', fn: (a, b) => a != b },
-    { op: '==', fn: (a, b) => a == b },
+    { op: '==', fn: (a, b) => {
+      // 特殊处理:如果一边是空字符串,另一边是 JSON 数组字符串,判断数组是否为空
+      if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
+        const jsonStr = a === '' ? b : a;
+        try {
+          const parsed = JSON.parse(jsonStr);
+          if (Array.isArray(parsed)) {
+            return parsed.length === 0; // 空数组 == 空字符串
+          }
+        } catch (e) {
+          // 不是 JSON,按普通比较
+        }
+      }
+      return a == b;
+    }},
     { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
     { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
     { op: '>', fn: (a, b) => Number(a) > Number(b) },
@@ -234,8 +370,23 @@ function parseConditionExpression(expr) {
   const value = parseValue(expr);
   if (typeof value === 'boolean') return value;
   if (typeof value === 'string') {
-    // 空字符串为false,非空为true
-    return value !== '' && value !== 'undefined';
+    // 空字符串为false
+    if (value === '' || value === 'undefined') return false;
+    // 尝试解析 JSON 字符串,如果是空数组 "[]",返回 false
+    try {
+      const parsed = JSON.parse(value);
+      if (Array.isArray(parsed)) {
+        return parsed.length > 0;
+      }
+      // 如果是空对象 "{}",返回 false
+      if (typeof parsed === 'object' && Object.keys(parsed).length === 0) {
+        return false;
+      }
+    } catch (e) {
+      // 不是 JSON,按普通字符串处理
+    }
+    // 非空字符串为true
+    return true;
   }
   // 数字:0为false,非0为true
   if (typeof value === 'number') return value !== 0;
@@ -284,8 +435,50 @@ export function parseWorkflow(workflow) {
     return null;
   }
 
-  // 初始化变量上下文
-  variableContext = workflow.variables ? { ...workflow.variables } : {};
+  // 初始化变量上下文(仅在未初始化时初始化,避免循环中重置)
+  if (!variableContextInitialized) {
+    variableContext = workflow.variables ? {} : {};
+    if (workflow.variables) {
+      // 转换变量类型:只允许 number 或 string
+      for (const key in workflow.variables) {
+        const value = workflow.variables[key];
+        if (value === null || value === undefined) {
+          variableContext[key] = ''; // null/undefined 转换为空字符串
+        } else if (typeof value === 'boolean') {
+          variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
+        } else if (typeof value === 'number') {
+          variableContext[key] = value; // number 保持不变
+        } else if (typeof value === 'string') {
+          variableContext[key] = value; // string 保持不变
+        } else {
+          variableContext[key] = String(value); // 其他类型转换为字符串
+        }
+      }
+    }
+    variableContextInitialized = true;
+  } else {
+    // 如果已初始化,只更新新增的变量(保留已有变量值)
+    if (workflow.variables) {
+      for (const key in workflow.variables) {
+        // 如果变量已存在且不为空,则保留原值;否则使用新值
+        if (variableContext[key] === undefined || variableContext[key] === null || variableContext[key] === '') {
+          const value = workflow.variables[key];
+          // 转换变量类型:只允许 number 或 string
+          if (value === null || value === undefined) {
+            variableContext[key] = ''; // null/undefined 转换为空字符串
+          } else if (typeof value === 'boolean') {
+            variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
+          } else if (typeof value === 'number') {
+            variableContext[key] = value; // number 保持不变
+          } else if (typeof value === 'string') {
+            variableContext[key] = value; // string 保持不变
+          } else {
+            variableContext[key] = String(value); // 其他类型转换为字符串
+          }
+        }
+      }
+    }
+  }
   
   // 解析 actions(支持新格式和旧格式)
   let actions = [];
@@ -418,7 +611,8 @@ function parseNewFormatAction(action) {
       parsed.avatar = resolveValue(action.avatar);
       break;
     case 'extract-messages':
-    case 'ocr-chat-history':
+    case 'ocr-chat':
+    case 'ocr-chat-history': // 向后兼容
     case 'extract-chat-history': // 向后兼容
       // 支持新的 inVars/outVars 格式(都可以为空)
       if (action.inVars && Array.isArray(action.inVars)) {
@@ -514,6 +708,18 @@ function parseNewFormatAction(action) {
       parsed.variable = action.variable;
       parsed.value = resolveValue(action.value);
       break;
+    case 'echo':
+      // echo 操作:支持 inVars 或 value 字段
+      if (action.inVars && Array.isArray(action.inVars)) {
+        parsed.inVars = action.inVars.map(v => extractVarName(v));
+      } else {
+        parsed.inVars = [];
+      }
+      // 支持 value 字段作为直接输出内容
+      if (action.value) {
+        parsed.value = action.value;
+      }
+      break;
     case 'read-last-message':
       // 支持新的 inVars/outVars 格式,也支持 inputVars/outputVars(大小写不同,都可以为空)
       const inputVars = action.inVars || action.inputVars || [];
@@ -717,6 +923,7 @@ function getActionName(action) {
     'extract-messages': '提取消息记录',
     'save-messages': '保存消息记录',
     'generate-summary': '生成总结',
+    'ocr-chat': 'OCR识别对话',
     // 向后兼容
     'ocr-chat-history': 'OCR提取消息记录',
     'extract-chat-history': '提取消息记录', // 向后兼容
@@ -733,7 +940,8 @@ function getActionName(action) {
     'for': '循环',
     'while': '循环',
     'delay': '延迟',
-    'set': '设置变量'
+    'set': '设置变量',
+    'echo': '打印信息'
   };
   
   const typeName = typeNames[action.type] || action.type;
@@ -873,7 +1081,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `输入失败: ${textResult.error}` };
             }
 
-            console.log(`成功输入文本: ${inputValue}`);
             return { success: true };
           }
 
@@ -893,12 +1100,26 @@ export async function executeAction(action, device, folderPath, resolution) {
 
             // 如果 position 是字符串,尝试解析为坐标对象
             if (typeof position === 'string') {
+              if (position === '') {
+                return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' };
+              }
               try {
+                // 尝试解析 JSON 字符串(例如:{"x":123,"y":456})
                 position = JSON.parse(position);
               } catch (e) {
-                // 如果不是 JSON,可能是变量名,尝试从变量上下文读取
-                const posVar = extractVarName(position);
-                position = variableContext[posVar] || position;
+                // 如果不是 JSON,可能是 "x,y" 格式,尝试解析
+                const parts = position.split(',');
+                if (parts.length === 2) {
+                  const x = parseFloat(parts[0].trim());
+                  const y = parseFloat(parts[1].trim());
+                  if (!isNaN(x) && !isNaN(y)) {
+                    position = { x: Math.round(x), y: Math.round(y) };
+                  } else {
+                    return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
+                  }
+                } else {
+                  return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
+                }
               }
             }
 
@@ -927,7 +1148,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
 
-            console.log(`成功点击坐标: (${position.x}, ${position.y})`);
             return { success: true };
           }
 
@@ -950,9 +1170,10 @@ export async function executeAction(action, device, folderPath, resolution) {
                 return { success: false, error: 'locate 操作(image)缺少图片路径' };
               }
 
+              // resources 作为根目录
               const fullPath = imagePath.startsWith('/') || imagePath.includes(':')
                 ? imagePath
-                : `${folderPath}/${imagePath}`;
+                : `${folderPath}/resources/${imagePath}`;
 
               if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
                 return { success: false, error: '图像匹配 API 不可用' };
@@ -1005,10 +1226,9 @@ export async function executeAction(action, device, folderPath, resolution) {
             if (outVars.length > 0) {
               const outputVar = extractVarName(outVars[0]);
               variableContext[outputVar] = position;
-              console.log(`定位结果已保存到变量 ${outputVar}:`, position);
+              await logOutVars(action, variableContext, folderPath);
             } else if (action.variable) {
               variableContext[action.variable] = position;
-              console.log(`定位结果已保存到变量 ${action.variable}:`, position);
             }
 
             return { success: true, result: position };
@@ -1061,7 +1281,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `滑动失败: ${swipeResult.error}` };
             }
 
-            console.log(`成功滑动: ${direction} (${x1},${y1}) -> (${x2},${y2})`);
             return { success: true };
           }
 
@@ -1096,7 +1315,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `滚动失败: ${scrollResult.error}` };
             }
 
-            console.log(`成功滚动: ${direction}`);
             return { success: true };
           }
 
@@ -1139,7 +1357,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
 
-            console.log(`成功点击图片坐标: (${x}, ${y})`);
             return { success: true };
           }
 
@@ -1178,7 +1395,6 @@ export async function executeAction(action, device, folderPath, resolution) {
               return { success: false, error: `点击失败: ${tapResult.error}` };
             }
 
-            console.log(`成功点击文字 "${targetText}" 的坐标: (${x}, ${y})`);
             return { success: true };
           }
 
@@ -1276,13 +1492,13 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
         }
 
-        console.log(`成功点击坐标: (${position.x}, ${position.y})`);
         return { success: true };
       }
 
       case 'press': {
         // 向后兼容:图像匹配并点击
-        const imagePath = `${folderPath}/${action.value}`;
+        // resources 作为根目录
+        const imagePath = `${folderPath}/resources/${action.value}`;
         
         if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
           return { success: false, error: '图像匹配 API 不可用' };
@@ -1307,20 +1523,15 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
         }
 
-        console.log(`成功点击坐标: (${x}, ${y})`);
         return { success: true };
       }
 
       case 'input': {
         // 输入文本
         const inputStartTime = Date.now();
-        console.log(`[input] 开始输入操作: ${new Date(inputStartTime).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 })}`);
         
         // 先解析value(可能在运行时变量才被赋值,需要重新解析)
-        let parseStartTime = Date.now();
         let inputValue = resolveValue(action.value);
-        let parseEndTime = Date.now();
-        console.log(`[input] 解析变量耗时: ${parseEndTime - parseStartTime}ms`);
         
         // 如果value为空或undefined,且target存在,使用target(用于向后兼容,target可能也是定位文字)
         if (!inputValue && action.target) {
@@ -1349,9 +1560,6 @@ export async function executeAction(action, device, folderPath, resolution) {
 
         // 如果设置了clear,先清空输入框(通过发送退格键)
         if (action.clear) {
-          const clearStartTime = Date.now();
-          console.log(`[input] 开始清空输入框: ${new Date(clearStartTime).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 })}`);
-          
           // 发送退格键清空输入框(假设最多200个字符)
           // 使用Android的KEYCODE_DEL,值为67
           for (let i = 0; i < 200; i++) {
@@ -1364,37 +1572,23 @@ export async function executeAction(action, device, folderPath, resolution) {
           }
           // 等待清空完成
           await new Promise(resolve => setTimeout(resolve, 200));
-          
-          const clearEndTime = Date.now();
-          console.log(`[input] 清空输入框完成,耗时: ${clearEndTime - clearStartTime}ms`);
         }
 
-        const sendTextStartTime = Date.now();
-        console.log(`[input] 开始发送文本: ${new Date(sendTextStartTime).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 })}`);
-        console.log(`[input] 待输入文本内容: ${inputValue}`);
-        
         const textResult = await window.electronAPI.sendText(device, inputValue);
         
-        const sendTextEndTime = Date.now();
-        console.log(`[input] 发送文本完成,耗时: ${sendTextEndTime - sendTextStartTime}ms`);
-        console.log(`[input] sendText 返回结果:`, textResult);
-        
         if (!textResult.success) {
           console.error(`[input] 输入失败: ${textResult.error}`);
           return { success: false, error: `输入失败: ${textResult.error}` };
         }
 
-        const inputEndTime = Date.now();
-        console.log(`[input] 输入操作总耗时: ${inputEndTime - inputStartTime}ms`);
-
         // 确保正确显示UTF-8编码的中文
         try {
           const displayValue = Buffer.isBuffer(inputValue) 
             ? inputValue.toString('utf8') 
             : String(inputValue);
-          console.log('成功输入文本:', displayValue);
+          // 输入成功,不打印日志
         } catch (e) {
-          console.log('成功输入文本:', inputValue);
+          // 输入成功,不打印日志
         }
         return { success: true };
       }
@@ -1438,10 +1632,10 @@ export async function executeAction(action, device, folderPath, resolution) {
             const textStr = Buffer.isBuffer(displayText) 
               ? displayText.toString('utf8') 
               : String(displayText);
-            console.log(`OCR识别结果已保存到变量 ${action.variable}:`, textStr);
+            // OCR识别结果已保存到变量
           } catch (e) {
             // 如果转换失败,直接输出
-            console.log(`OCR识别结果已保存到变量 ${action.variable}:`, displayText);
+            // OCR识别结果已保存到变量
           }
         }
 
@@ -1453,7 +1647,8 @@ export async function executeAction(action, device, folderPath, resolution) {
       }
 
       case 'extract-messages':
-      case 'ocr-chat-history':
+      case 'ocr-chat':
+      case 'ocr-chat-history': // 向后兼容
       case 'extract-chat-history': { // 向后兼容
         // 提取消息记录
         // 获取头像路径(支持新的 inVars 格式,也支持旧参数 avatar1/avatar2 和 friendAvatar/myAvatar)
@@ -1475,19 +1670,21 @@ export async function executeAction(action, device, folderPath, resolution) {
         if (avatar1Name) {
           const avatar1Resolved = resolveValue(avatar1Name);
           if (avatar1Resolved) {
-            avatar1Path = `${folderName}/${avatar1Resolved}`;
+            // resources 作为根目录
+            avatar1Path = `${folderName}/resources/${avatar1Resolved}`;
           }
         }
 
         if (avatar2Name) {
           const avatar2Resolved = resolveValue(avatar2Name);
           if (avatar2Resolved) {
-            avatar2Path = `${folderName}/${avatar2Resolved}`;
+            // resources 作为根目录
+            avatar2Path = `${folderName}/resources/${avatar2Resolved}`;
           }
         }
 
         // 调用 Func 目录下的执行函数
-        const chatResult = await executeOcrChatHistory({
+        const chatResult = await executeOcrChat({
           device,
           avatar1: avatar1Path,
           avatar2: avatar2Path,
@@ -1498,19 +1695,26 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `提取消息记录失败: ${chatResult.error}` };
         }
 
-        // 保存消息记录到变量
-        if (action.variable) {
-          // 使用格式化后的消息文本
-          const messagesText = chatResult.messagesText || '';
-          variableContext[action.variable] = messagesText;
+        // 保存消息记录到变量(支持新的 outVars 格式)
+        let outputVarName = null;
+        if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
+          outputVarName = extractVarName(action.outVars[0]);
+        } else if (action.variable) {
+          outputVarName = extractVarName(action.variable);
+        }
+
+        if (outputVarName) {
+          // 使用JSON字符串格式的消息记录
+          const messagesJson = chatResult.messagesJson || JSON.stringify(chatResult.messages || []);
+          variableContext[outputVarName] = messagesJson;
           const messageCount = chatResult.messages ? chatResult.messages.length : 0;
-          console.log(`消息记录已保存到变量 ${action.variable},共 ${messageCount} 条消息`);
+          await logOutVars(action, variableContext, folderPath);
         }
 
         return { 
           success: true, 
           messages: chatResult.messages || [],
-          messagesText: chatResult.messagesText || '',
+          messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
           lastMessage: chatResult.messages && chatResult.messages.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null
         };
       }
@@ -1550,12 +1754,8 @@ export async function executeAction(action, device, folderPath, resolution) {
           prompt = prompt.replace(/{historySummary}/g, historySummary);
         }
         
-        // 替换 prompt 中所有剩余的变量占位符
-        const varPlaceholderRegex = /\{(\w+)\}/g;
-        prompt = prompt.replace(varPlaceholderRegex, (match, varName) => {
-          const varValue = variableContext[varName];
-          return varValue !== undefined && varValue !== null ? String(varValue) : match;
-        });
+        // 替换 prompt 中所有剩余的变量占位符(支持 {{variable}} 和 {variable} 格式)
+        prompt = replaceVariablesInString(prompt, variableContext);
         
         try {
           // 调用AI API(使用现有的GPT API)
@@ -1627,29 +1827,41 @@ export async function executeAction(action, device, folderPath, resolution) {
           }
           
           // 保存到变量(支持 outVars 格式)
-          let outputVarName = null;
-          if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-            outputVarName = extractVarName(action.outVars[0]);
+          // outVars[0] 是 AI 生成的结果,outVars[1](如果有)是 aiCallBack 变量
+          if (action.outVars && Array.isArray(action.outVars)) {
+            // 第一个输出变量保存 AI 生成结果
+            if (action.outVars.length > 0) {
+              const outputVarName = extractVarName(action.outVars[0]);
+              if (outputVarName) {
+                variableContext[outputVarName] = result;
+              }
+            }
+            // 第二个输出变量(如果有)设置为 1 表示 AI 生成完成(aiCallBack)
+            if (action.outVars.length > 1) {
+              const callbackVarName = extractVarName(action.outVars[1]);
+              if (callbackVarName) {
+                variableContext[callbackVarName] = 1;
+              }
+            }
+            await logOutVars(action, variableContext, folderPath);
           } else if (action.variable) {
-            outputVarName = extractVarName(action.variable);
-          }
-          
-          if (outputVarName) {
-            variableContext[outputVarName] = result;
-            console.log(`AI生成结果已保存到变量 ${outputVarName}: ${result}`);
+            // 向后兼容:使用旧的 variable 字段
+            const outputVarName = extractVarName(action.variable);
+            if (outputVarName) {
+              variableContext[outputVarName] = result;
+            }
           }
           
-          // 处理 aiCallBack 变量(如果提供了)
-          if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 1) {
-            const callbackVarName = extractVarName(action.inVars[1]);
-            if (callbackVarName) {
-              // 将 aiCallBack 设置为 1 表示 AI 生成完成
-              variableContext[callbackVarName] = 1;
-              console.log(`AI回调变量 ${callbackVarName} 已设置为 1`);
+          // 向后兼容:如果 aiCallBack 在 inVars 中提供,也设置它(但优先使用 outVars
+          if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length <= 1) {
+            if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 1) {
+              const callbackVarName = extractVarName(action.inVars[1]);
+              if (callbackVarName) {
+                variableContext[callbackVarName] = 1;
+              }
             }
           }
 
-          console.log(`AI生成成功: ${result}`);
           return { success: true, result };
         } catch (error) {
           return { success: false, error: `AI生成失败: ${error.message}` };
@@ -1691,8 +1903,8 @@ export async function executeAction(action, device, folderPath, resolution) {
             variableContext[outputVarName] = result.filePath;
           }
         }
+        await logOutVars(action, variableContext, folderPath);
 
-        console.log(`消息记录已保存到 history 文件夹`);
         return { success: true };
       }
 
@@ -1718,7 +1930,7 @@ export async function executeAction(action, device, folderPath, resolution) {
         // 保存总结到变量
         if (action.summaryVariable) {
           variableContext[action.summaryVariable] = result.summary;
-          console.log(`消息记录总结已保存到变量 ${action.summaryVariable}`);
+          // 消息记录总结已保存到变量
         }
 
         return { success: true, summary: result.summary };
@@ -1727,9 +1939,42 @@ export async function executeAction(action, device, folderPath, resolution) {
       case 'set': {
         // 设置变量
         if (action.variable) {
-          variableContext[action.variable] = action.value;
-          console.log(`设置变量 ${action.variable} = ${action.value}`);
+          const varName = extractVarName(action.variable);
+          variableContext[varName] = action.value;
+          // 设置变量
+        }
+        return { success: true };
+      }
+
+      case 'echo': {
+        // 打印信息到 console.log
+        // 支持 inVars 或 value 字段
+        let message = '';
+        
+        if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
+          // 从 inVars 中读取变量值
+          const messages = action.inVars.map(varWithBraces => {
+            const varName = extractVarName(varWithBraces);
+            const varValue = variableContext[varName];
+            // 如果变量包含 {variable} 格式,尝试解析为变量名
+            if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
+              return varValue !== undefined ? String(varValue) : varWithBraces;
+            }
+            // 如果不是变量格式,直接使用原值
+            return varWithBraces;
+          });
+          message = messages.join(' ');
+        } else if (action.value) {
+          // 使用 value 字段,支持变量替换(支持 {{variable}} 和 {variable} 格式)
+          message = replaceVariablesInString(action.value, variableContext);
+        } else {
+          // 如果没有提供任何内容,输出空字符串
+          message = '';
         }
+        
+        // 输出到 console.log
+        console.log(message);
+        
         return { success: true };
       }
 
@@ -1809,7 +2054,8 @@ export async function executeAction(action, device, folderPath, resolution) {
         
         if (outputVarName) {
           variableContext[outputVarName] = result.corners;
-          console.log(`图像区域定位结果已保存到变量 ${outputVarName}:`, result.corners);
+          // 图像区域定位结果已保存到变量
+          await logOutVars(action, variableContext, folderPath);
         }
 
         return { success: true, result: result.corners };
@@ -1863,9 +2109,14 @@ export async function executeAction(action, device, folderPath, resolution) {
         }
         
         if (outputVarName) {
-          // 保存中心点坐标
-          variableContext[outputVarName] = result.center;
-          console.log(`图像中心点定位结果已保存到变量 ${outputVarName}:`, result.center);
+          // 保存中心点坐标为字符串(JSON 格式):只允许 number 或 string
+          if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) {
+            variableContext[outputVarName] = JSON.stringify({ x: result.center.x, y: result.center.y });
+          } else {
+            variableContext[outputVarName] = '';
+          }
+          // 图像中心点定位结果已保存到变量
+          await logOutVars(action, variableContext, folderPath);
         }
 
         return { success: true, result: result.center };
@@ -1899,7 +2150,7 @@ export async function executeAction(action, device, folderPath, resolution) {
         let inputData = null;
         if (inputVar && variableContext[inputVar] !== undefined) {
           inputData = variableContext[inputVar];
-          console.log(`从变量 ${inputVar} 读取数据,类型: ${typeof inputData}, 是否为数组: ${Array.isArray(inputData)}`);
+          // 从变量读取数据
         }
 
         const result = await executeReadLastMessage({
@@ -1916,12 +2167,13 @@ export async function executeAction(action, device, folderPath, resolution) {
         // 保存消息文本和发送者到变量
         if (textVar) {
           variableContext[textVar] = result.text;
-          console.log(`最后一条消息文本已保存到变量 ${textVar}: ${result.text}`);
+          // 最后一条消息文本已保存到变量
         }
         if (senderVar) {
           variableContext[senderVar] = result.sender;
-          console.log(`最后一条消息发送者已保存到变量 ${senderVar}: ${result.sender}`);
+          // 最后一条消息发送者已保存到变量
         }
+        await logOutVars(action, variableContext, folderPath);
 
         return { success: true, text: result.text, sender: result.sender };
       }
@@ -1949,12 +2201,15 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: result.error };
         }
 
-        // 保存消息数组到变量
-        variableContext[varName] = result.messages;
-        console.log(`聊天记录已读取并保存到变量 ${varName},共 ${result.fileCount || 0} 个文件,${result.totalMessages || 0} 条消息`);
+        // 保存消息数组为 JSON 字符串到变量(保持与 ocr-chat 输出格式一致)
+        const messagesJson = JSON.stringify(result.messages || []);
+        variableContext[varName] = messagesJson;
+        // 聊天记录已读取并保存到变量
+        await logOutVars(action, variableContext, folderPath);
         return { 
           success: true, 
           messages: result.messages,
+          messagesJson: messagesJson,
           fileCount: result.fileCount || 0,
           totalMessages: result.totalMessages || 0
         };
@@ -1987,7 +2242,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `滑动失败: ${swipeResult.error}` };
         }
 
-        console.log(`成功滑动: ${action.value} (${x1},${y1}) -> (${x2},${y2})`);
+        // 成功滑动
         return { success: true };
       }
 
@@ -2016,7 +2271,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `点击失败: ${tapResult.error}` };
         }
 
-        console.log(`成功点击文字 "${action.value}" 的坐标: (${x}, ${y})`);
+        // 成功点击文字
         return { success: true };
       }
 
@@ -2039,7 +2294,7 @@ export async function executeAction(action, device, folderPath, resolution) {
           return { success: false, error: `滚动失败: ${scrollResult.error}` };
         }
 
-        console.log(`成功滚动: ${action.value}`);
+        // 成功滚动
         return { success: true };
       }
 
@@ -2074,9 +2329,15 @@ export async function executeActionSequence(
   shouldStop = null,
   depth = 0
 ) {
-  // 如果是顶层(depth === 0),重置全局步骤计数器
+  // 如果是顶层(depth === 0),重置全局步骤计数器和变量初始化标志
   if (depth === 0) {
     globalStepCounter = 0;
+    // 重置变量初始化标志,允许在新工作流开始时重新初始化
+    variableContextInitialized = false;
+    // 保存当前工作流文件夹路径,用于日志记录
+    currentWorkflowFolderPath = folderPath;
+    // 记录工作流开始执行
+    await logMessage('==================== 工作流开始执行 ====================', folderPath);
   }
   
   let completedSteps = 0;
@@ -2085,7 +2346,7 @@ export async function executeActionSequence(
   for (let i = 0; i < actions.length; i++) {
     // 检查是否应该停止
     if (shouldStop && shouldStop()) {
-      console.log(`${stepPrefix}执行被停止`);
+      // 执行被停止
       return { success: false, error: '执行被停止', completedSteps };
     }
 
@@ -2115,7 +2376,7 @@ export async function executeActionSequence(
         
         // 如果不是第一次迭代,等待间隔时间
         if (iteration > 1 && intervalMs > 0) {
-          console.log(`${stepPrefix}等待 ${intervalMs}ms 后执行第 ${iteration} 次循环...`);
+          await logMessage(`${stepPrefix}等待 ${intervalMs}ms 后执行第 ${iteration} 次循环...`, folderPath);
           let remainingTime = intervalMs;
           const countdownInterval = 100;
           
@@ -2251,7 +2512,7 @@ export async function executeActionSequence(
     
     if (waitTime > 0) {
       const waitSeconds = Math.round(waitTime / 1000);
-      console.log(`${stepPrefix}步骤 ${i + 1}/${actions.length} 等待 ${waitSeconds} 秒后执行...`);
+      await logMessage(`${stepPrefix}步骤 ${i + 1}/${actions.length} 等待 ${waitSeconds} 秒后执行...`, folderPath);
       
       // 在等待期间也更新倒计时
       let remainingTime = waitTime;
@@ -2277,7 +2538,7 @@ export async function executeActionSequence(
     for (let t = 0; t < times; t++) {
       // 检查是否应该停止
       if (shouldStop && shouldStop()) {
-        console.log(`${stepPrefix}执行被停止`);
+        await logMessage(`${stepPrefix}执行被停止`, folderPath);
         return { success: false, error: '执行被停止', completedSteps };
       }
 
@@ -2293,26 +2554,50 @@ export async function executeActionSequence(
       
       // 记录步骤开始时间
       const stepStartTime = Date.now();
+      const startTimeStr = new Date(stepStartTime).toLocaleString('zh-CN', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: false
+      });
       
-      // 打印步骤开始执行
-      if (times > 1) {
-        console.log(`${stepPrefix}步骤 ${currentStepNumber} 开始执行: ${action.type} (第 ${t + 1}/${times} 次)`);
-      } else {
-        console.log(`${stepPrefix}步骤 ${currentStepNumber} 开始执行: ${action.type}`);
-      }
+      // 获取操作类型名称
+      const typeName = getActionName(action);
+      
+      // 打印步骤开始执行(包含系统时间)
+      await logMessage(`开始执行:${typeName} [系统时间: ${startTimeStr}]`, folderPath);
 
       // 执行操作
       const result = await executeAction(action, device, folderPath, resolution);
 
       // 记录步骤结束时间
       const stepEndTime = Date.now();
-      const stepDuration = stepEndTime - stepStartTime;
+      const endTimeStr = new Date(stepEndTime).toLocaleString('zh-CN', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: false
+      });
+      const stepDuration = (stepEndTime - stepStartTime) / 1000; // 转换为秒
       
-      // 打印步骤执行完成
-      console.log(`${stepPrefix}步骤 ${currentStepNumber} 执行完成: ${action.type}, 耗时: ${stepDuration}ms (${(stepDuration / 1000).toFixed(2)}秒)`);
+      // 打印步骤执行完成(包含系统时间和执行时长)
+      // 如果时长超过 0.1 秒,才打印结束日志
+      if (stepDuration > 0.1) {
+        const endMessage = `结束执行:${typeName} 时长:${stepDuration.toFixed(2)}秒 [系统时间: ${endTimeStr}]`;
+        await logMessage(endMessage, folderPath);
+      }
 
       if (!result.success) {
-        console.error(`${stepPrefix}步骤 ${currentStepNumber} 执行失败:`, result.error);
+        // 失败时,如果时长超过 0.1 秒才打印结束日志
+        if (stepDuration > 0.1) {
+          await logMessage(`结束执行:${typeName} 时长:${stepDuration.toFixed(2)}秒`, folderPath);
+        }
         return { success: false, error: result.error, completedSteps: i };
       }
 
@@ -2354,7 +2639,8 @@ export async function executeActionSequence(
   }
 
   if (depth === 0) {
-  console.log('所有操作执行完成');
+    await logMessage('所有操作执行完成', folderPath);
+    await logMessage('==================== 工作流执行完成 ====================', folderPath);
   }
   return { success: true, completedSteps };
 }

+ 0 - 34
static/processing/微信聊天自动发送工作流/history/chat_2026-01-14T22-23-31.json

@@ -1,34 +0,0 @@
-{
-  "timestamp": "2026-01-14T22:23:31.772Z",
-  "messageCount": 7,
-  "messages": [
-    {
-      "sender": "me",
-      "text": "你好"
-    },
-    {
-      "sender": "friend",
-      "text": "你好"
-    },
-    {
-      "sender": "me",
-      "text": "在干嘛"
-    },
-    {
-      "sender": "friend",
-      "text": "在吃饭"
-    },
-    {
-      "sender": "friend",
-      "text": "你吃了吗"
-    },
-    {
-      "sender": "me",
-      "text": "刚回家准备吃"
-    },
-    {
-      "sender": "friend",
-      "text": "准备吃啥"
-    }
-  ]
-}

+ 0 - 54
static/processing/微信聊天自动发送工作流/history/chat_2026-01-15T07-15-56.json

@@ -1,54 +0,0 @@
-{
-  "timestamp": "2026-01-15T07:15:56.486Z",
-  "messageCount": 12,
-  "messages": [
-    {
-      "sender": "friend",
-      "text": "C"
-    },
-    {
-      "sender": "friend",
-      "text": "¥15.00"
-    },
-    {
-      "sender": "me",
-      "text": "你好"
-    },
-    {
-      "sender": "friend",
-      "text": "你好"
-    },
-    {
-      "sender": "me",
-      "text": "在干嘛"
-    },
-    {
-      "sender": "me",
-      "text": "你好"
-    },
-    {
-      "sender": "friend",
-      "text": "你好"
-    },
-    {
-      "sender": "me",
-      "text": "在干嘛"
-    },
-    {
-      "sender": "friend",
-      "text": "在吃饭"
-    },
-    {
-      "sender": "friend",
-      "text": "你吃了吗"
-    },
-    {
-      "sender": "me",
-      "text": "刚回家准备吃"
-    },
-    {
-      "sender": "friend",
-      "text": "准备吃啥"
-    }
-  ]
-}

+ 431 - 0
static/processing/微信聊天自动发送工作流/log.txt

@@ -0,0 +1,431 @@
+[2026-01-16 07:07:13.733] ==================== 工作流开始执行 ====================
+[2026-01-16 07:07:13.734] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:07:13]
+[2026-01-16 07:07:19.203] 输出变量: currentMessage: "[{\"text\":\"5GA\",\"sender\":\"me\",\"y\":58,\"confidence\":0.7834983468055725},{\"text\":\"2\",\"sender\":\"me\",\"y\":6..."
+[2026-01-16 07:07:19.204] 结束执行:OCR识别对话:  时长:5.47秒 [系统时间: 2026/01/16 07:07:19]
+[2026-01-16 07:07:20.274] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:07:20]
+[2026-01-16 07:07:21.383] 开始执行:AI生成:  [系统时间: 2026/01/16 07:07:21]
+[2026-01-16 07:07:37.687] 输出变量: relationBg: "这段对话看起来你们是通过 Benny 这位共同朋友认识的熟人关系。对方自称是 Benny 的朋友,主动找你聊关于游戏开发的事,想和你一起学习,并询问你是否有编程基础,表明希望与你成为学习或合作的伙伴。...", aiCallBack: 1
+[2026-01-16 07:07:37.688] 结束执行:AI生成:  时长:16.30秒 [系统时间: 2026/01/16 07:07:37]
+[2026-01-16 07:07:38.767] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:07:38]
+[2026-01-16 07:07:38.768] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:07:38]
+[2026-01-16 07:07:38.769] 输出变量: lastMessage: "U", lastRole: "friend"
+[2026-01-16 07:07:39.849] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:07:39]
+[2026-01-16 07:07:39.850] 输出变量: lastHistoryMessage: "三", lastHistoryRole: "me"
+[2026-01-16 07:07:40.936] 开始执行:保存对话:  [系统时间: 2026/01/16 07:07:40]
+[2026-01-16 07:07:42.080] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:07:42]
+[2026-01-16 07:07:43.192] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:07:43]
+[2026-01-16 07:07:43.193] 输出变量: lastHistoryMessage: "U", lastHistoryRole: "friend"
+[2026-01-16 07:07:44.269] 开始执行:AI生成:  [系统时间: 2026/01/16 07:07:44]
+[2026-01-16 07:08:15.026] 输出变量: aiReply: "下面给你几种不同风格的回复模板,你可以直接使用或稍作调整后发送对方。核心是表明你也对学习有热情、想了解对方的基础与偏好,并提出一个初步的学习方向和下一步的沟通计划。\n\n版本 A:简短友好\n- 嗨,感谢...", aiCallBack: 1
+[2026-01-16 07:08:15.027] 结束执行:AI生成:  时长:30.76秒 [系统时间: 2026/01/16 07:08:15]
+[2026-01-16 07:08:16.110] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:08:16]
+[2026-01-16 07:08:17.194] 开始执行:ADB操作:  [系统时间: 2026/01/16 07:08:17]
+[2026-01-16 07:40:51.457] ==================== 工作流开始执行 ====================
+[2026-01-16 07:40:51.458] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:40:51]
+[2026-01-16 07:40:57.541] 输出变量: currentMessage: "[{\"text\":\"5G\",\"sender\":\"me\",\"y\":58,\"confidence\":0.9917705059051514},{\"text\":\"淘\",\"sender\":\"friend\",\"y..."
+[2026-01-16 07:40:57.542] 结束执行:OCR识别对话:  时长:6.09秒 [系统时间: 2026/01/16 07:40:57]
+[2026-01-16 07:40:58.620] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:40:58]
+[2026-01-16 07:40:59.703] 开始执行:AI生成:  [系统时间: 2026/01/16 07:40:59]
+[2026-01-16 07:41:16.259] 输出变量: relationBg: "他们是通过共同朋友 Benny 认识的熟人,关系介于普通朋友和潜在合作伙伴之间。对方一开始就提到你可能从事游戏开发,想请你帮忙学习和入门,显然是出于交流学习和可能的合作意向;而你自我介绍就是 Benn...", aiCallBack: 1
+[2026-01-16 07:41:16.259] 结束执行:AI生成:  时长:16.56秒 [系统时间: 2026/01/16 07:41:16]
+[2026-01-16 07:41:17.327] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:41:17]
+[2026-01-16 07:41:17.327] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:41:17]
+[2026-01-16 07:41:17.328] 输出变量: lastMessage: "△", lastRole: "friend"
+[2026-01-16 07:41:18.416] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:41:18]
+[2026-01-16 07:41:18.416] 输出变量: lastHistoryMessage: "U", lastHistoryRole: "friend"
+[2026-01-16 07:41:19.517] 开始执行:保存对话:  [系统时间: 2026/01/16 07:41:19]
+[2026-01-16 07:41:20.619] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:41:20]
+[2026-01-16 07:41:21.695] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:41:21]
+[2026-01-16 07:41:21.695] 输出变量: lastHistoryMessage: "我是benny朋友", lastHistoryRole: "friend"
+[2026-01-16 07:41:22.767] 开始执行:AI生成:  [系统时间: 2026/01/16 07:41:22]
+[2026-01-16 07:41:42.835] 输出变量: aiReply: "很高兴认识你!我最近也在研究游戏开发,愿意把我的编程基础和学习体会和你分享,也挺期待和你一起探讨方向。你想从哪块入门?咱们可以找个时间聊聊,顺便看看有没有可以合作的小点子。对了,听说你爱调侃,气氛这么...", aiCallBack: 1
+[2026-01-16 07:41:42.835] 结束执行:AI生成:  时长:20.07秒 [系统时间: 2026/01/16 07:41:42]
+[2026-01-16 07:41:43.899] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:41:43]
+[2026-01-16 07:41:44.990] 开始执行:ADB操作:  [系统时间: 2026/01/16 07:41:44]
+[2026-01-16 07:41:47.261] 结束执行:ADB操作:  时长:2.27秒 [系统时间: 2026/01/16 07:41:47]
+[2026-01-16 07:41:48.368] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 07:41:48]
+[2026-01-16 07:41:49.519] 结束执行:图像中心点定位:  时长:1.16秒 [系统时间: 2026/01/16 07:41:49]
+[2026-01-16 07:41:49.520] 开始执行:ADB操作:  [系统时间: 2026/01/16 07:41:49]
+[2026-01-16 07:41:49.650] 结束执行:ADB操作:  时长:0.13秒 [系统时间: 2026/01/16 07:41:49]
+[2026-01-16 07:41:49.651] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 07:41:50.731] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:41:50]
+[2026-01-16 07:41:54.786] 输出变量: currentMessage: "[{\"text\":\"07:41\",\"sender\":\"friend\",\"y\":61,\"confidence\":0.996913731098175},{\"text\":\"淘\",\"sender\":\"frie..."
+[2026-01-16 07:41:54.787] 结束执行:OCR识别对话:  时长:4.06秒 [系统时间: 2026/01/16 07:41:54]
+[2026-01-16 07:41:55.850] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:41:55]
+[2026-01-16 07:41:56.980] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:41:56]
+[2026-01-16 07:41:56.982] 输出变量: lastMessage: "△", lastRole: "friend"
+[2026-01-16 07:41:58.121] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:41:58]
+[2026-01-16 07:41:58.121] 输出变量: lastHistoryMessage: "我是benny朋友", lastHistoryRole: "friend"
+[2026-01-16 07:41:59.187] 开始执行:保存对话:  [系统时间: 2026/01/16 07:41:59]
+[2026-01-16 07:42:00.327] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:42:00]
+[2026-01-16 07:42:01.421] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:01]
+[2026-01-16 07:42:01.422] 输出变量: lastHistoryMessage: "07:41", lastHistoryRole: "friend"
+[2026-01-16 07:42:02.530] 开始执行:AI生成:  [系统时间: 2026/01/16 07:42:02]
+[2026-01-16 07:42:15.952] 输出变量: aiReply: "太好了,听起来很合拍。我也是 Benny 的朋友,对游戏开发也挺有热情,看到你愿意一起学习,心情很棒。咱们就把这当成一起学习和探讨的机会吧,你最近的编程基础和想要入门的方向是?", aiCallBack: 1
+[2026-01-16 07:42:15.953] 结束执行:AI生成:  时长:13.43秒 [系统时间: 2026/01/16 07:42:15]
+[2026-01-16 07:42:18.865] ==================== 工作流开始执行 ====================
+[2026-01-16 07:42:18.866] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:42:18]
+[2026-01-16 07:42:23.516] 结束执行:OCR识别对话:  时长:4.65秒 [系统时间: 2026/01/16 07:42:23]
+[2026-01-16 07:42:23.516] 输出变量: currentMessage: "[{\"text\":\"淘\",\"sender\":\"friend\",\"y\":60,\"confidence\":0.9976772665977478},{\"text\":\".205\",\"sender\":\"me\",..."
+[2026-01-16 07:42:24.615] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:42:24]
+[2026-01-16 07:42:25.761] 开始执行:AI生成:  [系统时间: 2026/01/16 07:42:25]
+[2026-01-16 07:42:35.389] 输出变量: relationBg: "你们其实是通过共同朋友 Benny 认识的。对方自称是 Benny 的朋友,主动联系你,想让你一起学做游戏开发,问你是否有编程基础,表述中带着学习和合作的意愿,同时夹杂着一些玩笑甚至暧昧的语气,显然还...", aiCallBack: 1
+[2026-01-16 07:42:35.390] 结束执行:AI生成:  时长:9.63秒 [系统时间: 2026/01/16 07:42:35]
+[2026-01-16 07:42:36.477] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:42:36]
+[2026-01-16 07:42:36.477] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:36]
+[2026-01-16 07:42:36.478] 输出变量: lastMessage: "三", lastRole: "me"
+[2026-01-16 07:42:37.552] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:37]
+[2026-01-16 07:42:37.553] 输出变量: lastHistoryMessage: "07:41", lastHistoryRole: "friend"
+[2026-01-16 07:42:38.628] 开始执行:保存对话:  [系统时间: 2026/01/16 07:42:38]
+[2026-01-16 07:42:39.733] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:42:39]
+[2026-01-16 07:42:40.832] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:40]
+[2026-01-16 07:42:40.834] 输出变量: lastHistoryMessage: "换行", lastHistoryRole: "me"
+[2026-01-16 07:42:41.922] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 07:42:43.015] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:42:43]
+[2026-01-16 07:42:48.630] 输出变量: currentMessage: "[{\"text\":\"a 3\",\"sender\":\"me\",\"y\":60,\"confidence\":0.5683026909828186},{\"text\":\"淘\",\"sender\":\"friend\",\"..."
+[2026-01-16 07:42:48.631] 结束执行:OCR识别对话:  时长:5.62秒 [系统时间: 2026/01/16 07:42:48]
+[2026-01-16 07:42:49.705] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:42:49]
+[2026-01-16 07:42:50.786] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:50]
+[2026-01-16 07:42:50.787] 输出变量: lastMessage: "三", lastRole: "me"
+[2026-01-16 07:42:51.860] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:51]
+[2026-01-16 07:42:51.861] 输出变量: lastHistoryMessage: "换行", lastHistoryRole: "me"
+[2026-01-16 07:42:52.923] 开始执行:保存对话:  [系统时间: 2026/01/16 07:42:52]
+[2026-01-16 07:42:54.040] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:42:54]
+[2026-01-16 07:42:55.124] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:42:55]
+[2026-01-16 07:42:55.126] 输出变量: lastHistoryMessage: "41", lastHistoryRole: "me"
+[2026-01-16 07:42:56.209] 等待 1000ms 后执行第 3 次循环...
+[2026-01-16 07:42:57.294] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:42:57]
+[2026-01-16 07:43:01.850] 输出变量: currentMessage: "[{\"text\":\"淘\",\"sender\":\"friend\",\"y\":60,\"confidence\":0.9976772665977478},{\"text\":\"07:42\",\"sender\":\"fri..."
+[2026-01-16 07:43:01.851] 结束执行:OCR识别对话:  时长:4.56秒 [系统时间: 2026/01/16 07:43:01]
+[2026-01-16 07:43:02.940] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:43:02]
+[2026-01-16 07:43:04.024] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:43:04]
+[2026-01-16 07:43:04.026] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 07:43:05.100] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:43:05]
+[2026-01-16 07:43:05.102] 输出变量: lastHistoryMessage: "41", lastHistoryRole: "me"
+[2026-01-16 07:43:06.172] 开始执行:保存对话:  [系统时间: 2026/01/16 07:43:06]
+[2026-01-16 07:43:07.276] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:43:07]
+[2026-01-16 07:43:08.367] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:43:08]
+[2026-01-16 07:43:08.369] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 07:43:09.451] 开始执行:AI生成:  [系统时间: 2026/01/16 07:43:09]
+[2026-01-16 07:43:23.781] 输出变量: aiReply: "嘿,原来你是 Benny 的朋友,认识你很高兴。我确实有编程基础,也对游戏开发很感兴趣,愿意一起学习和探索。你想从哪个方向开始?我们找个时间聊聊,看能不能一起做个小项目先试试。", aiCallBack: 1
+[2026-01-16 07:43:23.782] 结束执行:AI生成:  时长:14.33秒 [系统时间: 2026/01/16 07:43:23]
+[2026-01-16 07:43:55.882] ==================== 工作流开始执行 ====================
+[2026-01-16 07:43:55.884] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:43:55]
+[2026-01-16 07:43:59.932] 输出变量: currentMessage: "[{\"text\":\"淘\",\"sender\":\"friend\",\"y\":59,\"confidence\":0.9988546371459961},{\"text\":\"78 2\",\"sender\":\"me\",..."
+[2026-01-16 07:43:59.933] 结束执行:OCR识别对话:  时长:4.05秒 [系统时间: 2026/01/16 07:43:59]
+[2026-01-16 07:44:01.010] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:44:01]
+[2026-01-16 07:44:02.147] 开始执行:AI生成:  [系统时间: 2026/01/16 07:44:02]
+[2026-01-16 07:44:18.139] 输出变量: relationBg: "从聊天记录看,你和对方自称“Benny的朋友”来往,像是通过共同认识的中介进行初步接触。对话中的你表示对游戏有强烈兴趣、想学习并做游戏开发;对方则在不断引导你了解自己的编程基础、并提出与“学做开发/一...", aiCallBack: 1
+[2026-01-16 07:44:18.140] 结束执行:AI生成:  时长:16.00秒 [系统时间: 2026/01/16 07:44:18]
+[2026-01-16 07:44:21.456] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:44:21]
+[2026-01-16 07:44:21.456] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:44:21]
+[2026-01-16 07:44:21.457] 输出变量: lastMessage: "△", lastRole: "friend"
+[2026-01-16 07:44:22.578] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:44:22]
+[2026-01-16 07:44:22.578] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 07:44:23.651] 开始执行:保存对话:  [系统时间: 2026/01/16 07:44:23]
+[2026-01-16 07:44:24.776] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 07:44:24]
+[2026-01-16 07:44:25.910] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 07:44:25]
+[2026-01-16 07:44:25.914] 输出变量: lastHistoryMessage: "07:43", lastHistoryRole: "friend"
+[2026-01-16 07:44:27.000] 开始执行:AI生成:  [系统时间: 2026/01/16 07:44:26]
+[2026-01-16 07:44:38.420] 输出变量: aiReply: "嗨,很高兴通过 Benny 认识你。我对游戏开发充满热情,正在系统学习编程基础,期待与你一起学习并参与开发。谢谢你愿意提供辅导/合作的机会,这正是我想走的路径。我愿意按你们的节奏推进,也想先明确一个小...", aiCallBack: 1
+[2026-01-16 07:44:38.421] 结束执行:AI生成:  时长:11.42秒 [系统时间: 2026/01/16 07:44:38]
+[2026-01-16 07:44:39.494] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 07:44:39]
+[2026-01-16 07:44:40.549] 开始执行:ADB操作:  [系统时间: 2026/01/16 07:44:40]
+[2026-01-16 07:44:42.839] 结束执行:ADB操作:  时长:2.28秒 [系统时间: 2026/01/16 07:44:42]
+[2026-01-16 07:44:43.919] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 07:44:43]
+[2026-01-16 07:44:45.028] 结束执行:图像中心点定位:  时长:1.11秒 [系统时间: 2026/01/16 07:44:45]
+[2026-01-16 07:44:45.029] 开始执行:ADB操作:  [系统时间: 2026/01/16 07:44:45]
+[2026-01-16 07:44:45.175] 结束执行:ADB操作:  时长:0.14秒 [系统时间: 2026/01/16 07:44:45]
+[2026-01-16 07:44:45.176] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 07:44:46.233] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 07:44:46]
+[2026-01-16 07:44:51.070] 输出变量: currentMessage: "[{\"text\":\"07:44淘\",\"sender\":\"friend\",\"y\":60,\"confidence\":0.9948108196258545},{\"text\":\"3\",\"sender\":\"me..."
+[2026-01-16 07:44:51.070] 结束执行:OCR识别对话:  时长:4.84秒 [系统时间: 2026/01/16 07:44:51]
+[2026-01-16 08:05:31.162] ==================== 工作流开始执行 ====================
+[2026-01-16 08:05:31.163] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:05:31]
+[2026-01-16 08:05:32.223] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:05:32]
+[2026-01-16 08:05:33.266] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:05:33]
+[2026-01-16 08:05:38.003] 输出变量: currentMessage: "[{\"text\":\"淘\",\"sender\":\"friend\",\"y\":59,\"confidence\":0.9994462132453918},{\"text\":\"08:05\",\"sender\":\"fri..."
+[2026-01-16 08:05:38.004] 结束执行:OCR识别对话:  时长:4.74秒 [系统时间: 2026/01/16 08:05:38]
+[2026-01-16 08:05:39.074] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:05:39]
+[2026-01-16 08:05:40.209] 开始执行:AI生成:  [系统时间: 2026/01/16 08:05:40]
+[2026-01-16 08:05:56.753] 输出变量: relationBg: "你和这位聊天对象是 Benny 的朋友,正在聊如何学习游戏开发和编程基础,寻求对方的指导与帮助。", aiCallBack: 1
+[2026-01-16 08:05:56.754] 结束执行:AI生成:  时长:16.54秒 [系统时间: 2026/01/16 08:05:56]
+[2026-01-16 08:05:57.835] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:05:57]
+[2026-01-16 08:05:57.837] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:05:57]
+[2026-01-16 08:05:57.839] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:05:58.903] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:05:58]
+[2026-01-16 08:05:58.908] 输出变量: lastHistoryMessage: "07:43", lastHistoryRole: "friend"
+[2026-01-16 08:05:59.977] 开始执行:AI生成:  [系统时间: 2026/01/16 08:05:59]
+[2026-01-16 08:06:09.559] 输出变量: aiReply: "太好了,Benny 介绍你来找我。你现在的基础是啥,想学游戏开发的哪块,我们聊聊就好。", aiCallBack: 1
+[2026-01-16 08:06:09.560] 结束执行:AI生成:  时长:9.58秒 [系统时间: 2026/01/16 08:06:09]
+[2026-01-16 08:06:10.644] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:06:10]
+[2026-01-16 08:06:11.698] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:06:11]
+[2026-01-16 08:06:13.258] 结束执行:ADB操作:  时长:1.56秒 [系统时间: 2026/01/16 08:06:13]
+[2026-01-16 08:06:14.333] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 08:06:14]
+[2026-01-16 08:06:15.405] 结束执行:图像中心点定位:  时长:1.07秒 [系统时间: 2026/01/16 08:06:15]
+[2026-01-16 08:06:15.405] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:06:15]
+[2026-01-16 08:06:15.538] 结束执行:ADB操作:  时长:0.13秒 [系统时间: 2026/01/16 08:06:15]
+[2026-01-16 08:06:15.540] 开始执行:保存对话:  [系统时间: 2026/01/16 08:06:15]
+[2026-01-16 08:06:15.716] 结束执行:保存对话:  时长:0.18秒 [系统时间: 2026/01/16 08:06:15]
+[2026-01-16 08:06:15.717] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 08:06:16.806] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:06:16]
+[2026-01-16 08:06:17.880] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:06:17]
+[2026-01-16 08:06:18.963] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:06:18]
+[2026-01-16 08:06:23.204] 输出变量: currentMessage: "[{\"text\":\"1\",\"sender\":\"me\",\"y\":60,\"confidence\":0.720310389995575},{\"text\":\"08:06淘\",\"sender\":\"friend\"..."
+[2026-01-16 08:06:23.205] 结束执行:OCR识别对话:  时长:4.24秒 [系统时间: 2026/01/16 08:06:23]
+[2026-01-16 08:06:24.286] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:06:24]
+[2026-01-16 08:06:25.448] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:06:25]
+[2026-01-16 08:06:25.448] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:06:26.509] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:06:26]
+[2026-01-16 08:06:26.516] 输出变量: lastHistoryMessage: "08:05", lastHistoryRole: "friend"
+[2026-01-16 08:06:27.603] 开始执行:AI生成:  [系统时间: 2026/01/16 08:06:27]
+[2026-01-16 08:06:33.783] 输出变量: aiReply: "嗨,最近和 Benny 一起聊着想系统学习游戏开发和编程基础,想请教你在入门阶段最值得关注的方向和资源,方便给我点推荐吗?", aiCallBack: 1
+[2026-01-16 08:06:33.784] 结束执行:AI生成:  时长:6.18秒 [系统时间: 2026/01/16 08:06:33]
+[2026-01-16 08:06:34.876] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:06:34]
+[2026-01-16 08:06:35.947] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:06:35]
+[2026-01-16 08:06:37.513] 结束执行:ADB操作:  时长:1.57秒 [系统时间: 2026/01/16 08:06:37]
+[2026-01-16 08:06:38.582] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:06:38]
+[2026-01-16 08:06:38.744] 结束执行:ADB操作:  时长:0.16秒 [系统时间: 2026/01/16 08:06:38]
+[2026-01-16 08:06:38.745] 开始执行:保存对话:  [系统时间: 2026/01/16 08:06:38]
+[2026-01-16 08:06:39.034] 结束执行:保存对话:  时长:0.29秒 [系统时间: 2026/01/16 08:06:39]
+[2026-01-16 08:06:39.034] 等待 1000ms 后执行第 3 次循环...
+[2026-01-16 08:06:40.110] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:06:40]
+[2026-01-16 08:06:41.191] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:06:41]
+[2026-01-16 08:06:42.278] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:06:42]
+[2026-01-16 08:06:46.874] 输出变量: currentMessage: "[{\"text\":\"08:06淘\",\"sender\":\"friend\",\"y\":60,\"confidence\":0.9900429248809814},{\"text\":\"1\",\"sender\":\"me..."
+[2026-01-16 08:06:46.875] 结束执行:OCR识别对话:  时长:4.60秒 [系统时间: 2026/01/16 08:06:46]
+[2026-01-16 08:06:47.948] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:06:47]
+[2026-01-16 08:06:48.240] 结束执行:读取聊天记录:  时长:0.29秒 [系统时间: 2026/01/16 08:06:48]
+[2026-01-16 08:06:49.311] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:06:49]
+[2026-01-16 08:06:49.312] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:06:50.387] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:06:50]
+[2026-01-16 08:06:50.406] 输出变量: lastHistoryMessage: "換行", lastHistoryRole: "me"
+[2026-01-16 08:06:51.514] 开始执行:AI生成:  [系统时间: 2026/01/16 08:06:51]
+[2026-01-16 08:07:03.785] 输出变量: aiReply: "太好了,和你一起聊聊学习路线肯定更有趣,你现在的基础大概在哪儿,想专注哪类游戏开发?", aiCallBack: 1
+[2026-01-16 08:07:03.785] 结束执行:AI生成:  时长:12.27秒 [系统时间: 2026/01/16 08:07:03]
+[2026-01-16 08:07:04.862] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:07:04]
+[2026-01-16 08:07:05.926] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:07:05]
+[2026-01-16 08:07:07.499] 结束执行:ADB操作:  时长:1.56秒 [系统时间: 2026/01/16 08:07:07]
+[2026-01-16 08:07:08.567] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:07:08]
+[2026-01-16 08:07:08.708] 结束执行:ADB操作:  时长:0.14秒 [系统时间: 2026/01/16 08:07:08]
+[2026-01-16 08:07:08.709] 开始执行:保存对话:  [系统时间: 2026/01/16 08:07:08]
+[2026-01-16 08:07:09.261] 结束执行:保存对话:  时长:0.55秒 [系统时间: 2026/01/16 08:07:09]
+[2026-01-16 08:07:09.262] 等待 1000ms 后执行第 4 次循环...
+[2026-01-16 08:07:10.355] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:07:10]
+[2026-01-16 08:07:11.415] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:07:11]
+[2026-01-16 08:07:12.501] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:07:12]
+[2026-01-16 08:07:20.733] 输出变量: currentMessage: "[{\"text\":\"26\",\"sender\":\"me\",\"y\":61,\"confidence\":0.931991457939148},{\"text\":\"08:07\",\"sender\":\"friend\"..."
+[2026-01-16 08:07:20.736] 结束执行:OCR识别对话:  时长:8.24秒 [系统时间: 2026/01/16 08:07:20]
+[2026-01-16 08:14:50.507] ==================== 工作流开始执行 ====================
+[2026-01-16 08:14:50.512] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:14:50]
+[2026-01-16 08:14:51.630] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:14:51]
+[2026-01-16 08:14:52.683] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:14:52]
+[2026-01-16 08:15:07.136] 输出变量: currentMessage: "[{\"text\":\"17 54\",\"sender\":\"me\",\"y\":57,\"confidence\":0.5367075800895691},{\"text\":\"08:14\",\"sender\":\"fri..."
+[2026-01-16 08:15:07.136] 结束执行:OCR识别对话:  时长:14.45秒 [系统时间: 2026/01/16 08:15:07]
+[2026-01-16 08:15:08.195] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:15:08]
+[2026-01-16 08:15:09.283] 开始执行:AI生成:  [系统时间: 2026/01/16 08:15:09]
+[2026-01-16 08:15:24.799] 输出变量: relationBg: "由于你提供的聊天记录是空的([]),我无法基于它来推断你和好友的关系背景。请把聊天记录粘贴过来,或给出一些线索(比如你们的互动频率、常谈话题、彼此称呼、是否共同工作等),我就能给出一个准确的一句话描述...", aiCallBack: 1
+[2026-01-16 08:15:24.800] 结束执行:AI生成:  时长:15.52秒 [系统时间: 2026/01/16 08:15:24]
+[2026-01-16 08:15:25.894] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:15:25]
+[2026-01-16 08:15:25.895] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:15:25]
+[2026-01-16 08:15:25.896] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:15:26.970] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:15:26]
+[2026-01-16 08:15:26.971] 输出变量: lastHistoryMessage: "", lastHistoryRole: ""
+[2026-01-16 08:15:28.070] 开始执行:AI生成:  [系统时间: 2026/01/16 08:15:28]
+[2026-01-16 08:15:49.530] 输出变量: aiReply: "请把聊天记录发过来,或给我几个线索(互动频率、常聊话题、彼此称呼、是否共同工作等),我就能给出最准确的一句话描述。", aiCallBack: 1
+[2026-01-16 08:15:49.531] 结束执行:AI生成:  时长:21.46秒 [系统时间: 2026/01/16 08:15:49]
+[2026-01-16 08:15:50.622] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:15:50]
+[2026-01-16 08:15:51.715] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:15:51]
+[2026-01-16 08:15:53.219] 结束执行:ADB操作:  时长:1.50秒 [系统时间: 2026/01/16 08:15:53]
+[2026-01-16 08:15:54.290] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 08:15:54]
+[2026-01-16 08:15:55.355] 结束执行:图像中心点定位:  时长:1.07秒 [系统时间: 2026/01/16 08:15:55]
+[2026-01-16 08:15:55.356] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:15:55]
+[2026-01-16 08:15:55.483] 结束执行:ADB操作:  时长:0.13秒 [系统时间: 2026/01/16 08:15:55]
+[2026-01-16 08:15:55.484] 开始执行:保存对话:  [系统时间: 2026/01/16 08:15:55]
+[2026-01-16 08:15:55.511] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 08:15:56.613] 开始执行:打印信息: =============第 {turn} 轮============= [系统时间: 2026/01/16 08:15:56]
+[2026-01-16 08:15:57.711] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:15:57]
+[2026-01-16 08:15:58.808] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:15:58]
+[2026-01-16 08:16:03.927] 输出变量: currentMessage: "[{\"text\":\"08:15\",\"sender\":\"friend\",\"y\":61,\"confidence\":0.993904709815979},{\"text\":\"源24\",\"sender\":\"me..."
+[2026-01-16 08:16:03.928] 结束执行:OCR识别对话:  时长:5.12秒 [系统时间: 2026/01/16 08:16:03]
+[2026-01-16 08:16:05.046] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:16:05]
+[2026-01-16 08:16:06.165] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:16:06]
+[2026-01-16 08:16:06.165] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:16:07.226] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:16:07]
+[2026-01-16 08:16:07.227] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 08:16:08.309] 所有操作执行完成
+[2026-01-16 08:16:08.309] ==================== 工作流执行完成 ====================
+[2026-01-16 08:23:16.418] ==================== 工作流开始执行 ====================
+[2026-01-16 08:23:16.424] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:23:16]
+[2026-01-16 08:23:17.500] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:23:17]
+[2026-01-16 08:23:18.563] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:23:18]
+[2026-01-16 08:23:30.895] 输出变量: currentMessage: "[{\"text\":\"08:23\",\"sender\":\"friend\",\"y\":61,\"confidence\":0.9963470697402954},{\"text\":\"淘\",\"sender\":\"fri..."
+[2026-01-16 08:23:30.896] 结束执行:OCR识别对话:  时长:12.33秒 [系统时间: 2026/01/16 08:23:30]
+[2026-01-16 08:23:31.975] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:23:31]
+[2026-01-16 08:23:33.071] 开始执行:AI生成:  [系统时间: 2026/01/16 08:23:33]
+[2026-01-16 08:23:39.888] 输出变量: relationBg: "我们是通过共同朋友Benny认识的,对方愿意从零基础带我入门游戏开发,充当我的导师。", aiCallBack: 1
+[2026-01-16 08:23:39.888] 结束执行:AI生成:  时长:6.82秒 [系统时间: 2026/01/16 08:23:39]
+[2026-01-16 08:23:40.971] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:23:40]
+[2026-01-16 08:23:40.972] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:23:40]
+[2026-01-16 08:23:40.972] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:23:42.051] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:23:42]
+[2026-01-16 08:23:42.051] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 08:23:43.124] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 08:23:44.178] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:23:44]
+[2026-01-16 08:23:45.257] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:23:45]
+[2026-01-16 08:23:46.325] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:23:46]
+[2026-01-16 08:23:53.003] 输出变量: currentMessage: "[{\"text\":\" \",\"sender\":\"me\",\"y\":61,\"confidence\":0.5889561772346497},{\"text\":\"08:23\",\"sender\":\"friend\"..."
+[2026-01-16 08:23:53.004] 结束执行:OCR识别对话:  时长:6.68秒 [系统时间: 2026/01/16 08:23:53]
+[2026-01-16 08:23:54.094] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:23:54]
+[2026-01-16 08:23:55.160] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:23:55]
+[2026-01-16 08:23:55.160] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:23:56.247] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:23:56]
+[2026-01-16 08:23:56.247] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 08:23:57.328] 所有操作执行完成
+[2026-01-16 08:23:57.328] ==================== 工作流执行完成 ====================
+[2026-01-16 08:40:54.699] ==================== 工作流开始执行 ====================
+[2026-01-16 08:40:54.705] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:40:54]
+[2026-01-16 08:40:55.825] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:40:55]
+[2026-01-16 08:40:56.918] 开始执行:打印信息: set [系统时间: 2026/01/16 08:40:56]
+[2026-01-16 08:40:58.026] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:40:58]
+[2026-01-16 08:41:08.203] 输出变量: currentMessage: "[{\"text\":\"『淘\",\"sender\":\"friend\",\"y\":60,\"confidence\":0.7994641065597534},{\"text\":\"08:40\",\"sender\":\"fr..."
+[2026-01-16 08:41:08.204] 结束执行:OCR识别对话:  时长:10.16秒 [系统时间: 2026/01/16 08:41:08]
+[2026-01-16 08:41:09.376] 开始执行:打印信息: ocr-chat [系统时间: 2026/01/16 08:41:09]
+[2026-01-16 08:41:10.514] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:41:10]
+[2026-01-16 08:41:10.738] 结束执行:读取聊天记录:  时长:0.24秒 [系统时间: 2026/01/16 08:41:10]
+[2026-01-16 08:41:11.801] 开始执行:打印信息: read-chat-history [系统时间: 2026/01/16 08:41:11]
+[2026-01-16 08:41:12.904] 开始执行:打印信息: if [系统时间: 2026/01/16 08:41:12]
+[2026-01-16 08:41:13.978] 开始执行:AI生成:  [系统时间: 2026/01/16 08:41:13]
+[2026-01-16 08:41:23.085] 输出变量: relationBg: "通过 Benny 的介绍认识,这位朋友扮演你在零基础入门游戏开发过程中的导师/辅导者角色。", aiCallBack: 1
+[2026-01-16 08:41:23.086] 结束执行:AI生成:  时长:9.11秒 [系统时间: 2026/01/16 08:41:23]
+[2026-01-16 08:41:24.185] 开始执行:打印信息: ai-generate [系统时间: 2026/01/16 08:41:24]
+[2026-01-16 08:41:25.267] 开始执行:打印信息: while [系统时间: 2026/01/16 08:41:25]
+[2026-01-16 08:41:26.349] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:41:26]
+[2026-01-16 08:41:27.518] 开始执行:打印信息: set [系统时间: 2026/01/16 08:41:27]
+[2026-01-16 08:41:27.519] 开始执行:打印信息: if [系统时间: 2026/01/16 08:41:27]
+[2026-01-16 08:41:28.593] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:41:28]
+[2026-01-16 08:41:28.593] 输出变量: lastMessage: "△", lastRole: "friend"
+[2026-01-16 08:41:29.701] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:41:29]
+[2026-01-16 08:41:30.781] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:41:30]
+[2026-01-16 08:41:30.782] 输出变量: lastHistoryMessage: "-", lastHistoryRole: "friend"
+[2026-01-16 08:41:31.885] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:41:31]
+[2026-01-16 08:41:32.972] 开始执行:AI生成:  [系统时间: 2026/01/16 08:41:32]
+[2026-01-16 08:41:40.373] 输出变量: aiReply: "嗨,感谢 Benny 的介绍!很高兴认识你,期待在你的指导下开始零基础的游戏开发,请问你哪天方便聊聊?", aiCallBack: 1
+[2026-01-16 08:41:40.374] 结束执行:AI生成:  时长:7.41秒 [系统时间: 2026/01/16 08:41:40]
+[2026-01-16 08:41:41.448] 开始执行:打印信息: ai-generate [系统时间: 2026/01/16 08:41:41]
+[2026-01-16 08:41:42.517] 开始执行:打印信息: while [系统时间: 2026/01/16 08:41:42]
+[2026-01-16 08:41:43.651] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:41:43]
+[2026-01-16 08:41:44.718] 开始执行:打印信息: set [系统时间: 2026/01/16 08:41:44]
+[2026-01-16 08:41:45.801] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:41:45]
+[2026-01-16 08:41:47.491] 结束执行:ADB操作:  时长:1.69秒 [系统时间: 2026/01/16 08:41:47]
+[2026-01-16 08:41:48.567] 开始执行:打印信息: adb input [系统时间: 2026/01/16 08:41:48]
+[2026-01-16 08:41:49.648] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 08:41:49]
+[2026-01-16 08:41:51.232] 结束执行:图像中心点定位:  时长:1.58秒 [系统时间: 2026/01/16 08:41:51]
+[2026-01-16 08:41:52.319] 开始执行:打印信息: image-center-location [系统时间: 2026/01/16 08:41:52]
+[2026-01-16 08:41:52.320] 开始执行:打印信息: if [系统时间: 2026/01/16 08:41:52]
+[2026-01-16 08:41:53.441] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:41:53]
+[2026-01-16 08:41:53.650] 结束执行:ADB操作:  时长:0.21秒 [系统时间: 2026/01/16 08:41:53]
+[2026-01-16 08:41:54.748] 开始执行:打印信息: adb click [系统时间: 2026/01/16 08:41:54]
+[2026-01-16 08:41:54.749] 开始执行:保存对话:  [系统时间: 2026/01/16 08:41:54]
+[2026-01-16 08:41:55.892] 开始执行:打印信息: save-new-chat [系统时间: 2026/01/16 08:41:55]
+[2026-01-16 08:41:55.894] 开始执行:打印信息: if [系统时间: 2026/01/16 08:41:55]
+[2026-01-16 08:41:55.895] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 08:41:57.018] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:41:57]
+[2026-01-16 08:41:58.101] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:41:58]
+[2026-01-16 08:41:59.185] 开始执行:打印信息: set [系统时间: 2026/01/16 08:41:59]
+[2026-01-16 08:42:00.251] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:42:00]
+[2026-01-16 08:42:12.711] 输出变量: currentMessage: "[{\"text\":\"08:410\",\"sender\":\"friend\",\"y\":61,\"confidence\":0.9358155131340027},{\"text\":\"淘\",\"sender\":\"fr..."
+[2026-01-16 08:42:12.713] 结束执行:OCR识别对话:  时长:12.46秒 [系统时间: 2026/01/16 08:42:12]
+[2026-01-16 08:42:13.788] 开始执行:打印信息: ocr-chat [系统时间: 2026/01/16 08:42:13]
+[2026-01-16 08:42:14.886] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:42:14]
+[2026-01-16 08:42:15.968] 开始执行:打印信息: read-chat-history [系统时间: 2026/01/16 08:42:15]
+[2026-01-16 08:42:17.035] 开始执行:打印信息: if [系统时间: 2026/01/16 08:42:17]
+[2026-01-16 08:42:18.102] 开始执行:打印信息: if [系统时间: 2026/01/16 08:42:18]
+[2026-01-16 08:42:19.188] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:42:19]
+[2026-01-16 08:42:19.189] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:42:20.301] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:42:20]
+[2026-01-16 08:42:21.385] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:42:21]
+[2026-01-16 08:42:21.385] 输出变量: lastHistoryMessage: "08:06", lastHistoryRole: "me"
+[2026-01-16 08:42:22.437] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:42:22]
+[2026-01-16 08:42:30.881] ==================== 工作流开始执行 ====================
+[2026-01-16 08:42:30.887] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:42:30]
+[2026-01-16 08:42:31.940] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:42:31]
+[2026-01-16 08:42:33.054] 开始执行:打印信息: set [系统时间: 2026/01/16 08:42:33]
+[2026-01-16 08:42:34.134] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:42:34]
+[2026-01-16 08:42:43.718] 输出变量: currentMessage: "[{\"text\":\"08:42淘\",\"sender\":\"friend\",\"y\":61,\"confidence\":0.9943902492523193},{\"text\":\"17\",\"sender\":\"m..."
+[2026-01-16 08:42:43.720] 结束执行:OCR识别对话:  时长:9.59秒 [系统时间: 2026/01/16 08:42:43]
+[2026-01-16 08:42:44.835] 开始执行:打印信息: ocr-chat [系统时间: 2026/01/16 08:42:44]
+[2026-01-16 08:42:45.919] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:42:45]
+[2026-01-16 08:42:47.049] 开始执行:打印信息: read-chat-history [系统时间: 2026/01/16 08:42:47]
+[2026-01-16 08:42:48.163] 开始执行:打印信息: if [系统时间: 2026/01/16 08:42:48]
+[2026-01-16 08:42:49.249] 开始执行:AI生成:  [系统时间: 2026/01/16 08:42:49]
+[2026-01-16 08:42:56.604] 输出变量: relationBg: "你们是通过共同朋友 Benny 认识的,好友是一名游戏开发者/导师,正在帮助你这位初学者从零基础开始学习游戏开发。", aiCallBack: 1
+[2026-01-16 08:42:56.606] 结束执行:AI生成:  时长:7.36秒 [系统时间: 2026/01/16 08:42:56]
+[2026-01-16 08:42:57.703] 开始执行:打印信息: ai-generate [系统时间: 2026/01/16 08:42:57]
+[2026-01-16 08:42:58.772] 开始执行:打印信息: while [系统时间: 2026/01/16 08:42:58]
+[2026-01-16 08:42:59.868] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:42:59]
+[2026-01-16 08:43:00.969] 开始执行:打印信息: set [系统时间: 2026/01/16 08:43:00]
+[2026-01-16 08:43:00.969] 开始执行:打印信息: if [系统时间: 2026/01/16 08:43:00]
+[2026-01-16 08:43:02.051] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:43:02.051] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:43:02]
+[2026-01-16 08:43:03.116] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:43:03]
+[2026-01-16 08:43:04.212] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:43:04]
+[2026-01-16 08:43:04.213] 输出变量: lastHistoryMessage: "08:06", lastHistoryRole: "me"
+[2026-01-16 08:43:05.328] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:43:05]
+[2026-01-16 08:43:06.424] 开始执行:AI生成:  [系统时间: 2026/01/16 08:43:06]
+[2026-01-16 08:43:19.619] 输出变量: aiReply: "嗨,我是小明,和 Benny 通过朋友认识的。很高兴能得到您这位导师的帮助,准备从零基础开始学习游戏开发。方便给我安排一次第一次沟通的时间吗?谢谢!", aiCallBack: 1
+[2026-01-16 08:43:19.620] 结束执行:AI生成:  时长:13.20秒 [系统时间: 2026/01/16 08:43:19]
+[2026-01-16 08:43:20.694] 开始执行:打印信息: ai-generate [系统时间: 2026/01/16 08:43:20]
+[2026-01-16 08:43:21.761] 开始执行:打印信息: while [系统时间: 2026/01/16 08:43:21]
+[2026-01-16 08:43:22.834] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:43:22]
+[2026-01-16 08:43:23.917] 开始执行:打印信息: set [系统时间: 2026/01/16 08:43:23]
+[2026-01-16 08:43:24.991] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:43:24]
+[2026-01-16 08:43:26.774] 结束执行:ADB操作:  时长:1.79秒 [系统时间: 2026/01/16 08:43:26]
+[2026-01-16 08:43:27.881] 开始执行:打印信息: adb input [系统时间: 2026/01/16 08:43:27]
+[2026-01-16 08:43:28.966] 开始执行:图像中心点定位:  [系统时间: 2026/01/16 08:43:28]
+[2026-01-16 08:43:30.852] 结束执行:图像中心点定位:  时长:1.89秒 [系统时间: 2026/01/16 08:43:30]
+[2026-01-16 08:43:31.956] 开始执行:打印信息: image-center-location [系统时间: 2026/01/16 08:43:31]
+[2026-01-16 08:43:31.962] 开始执行:打印信息: if [系统时间: 2026/01/16 08:43:31]
+[2026-01-16 08:43:33.036] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:43:33]
+[2026-01-16 08:43:33.250] 结束执行:ADB操作:  时长:0.21秒 [系统时间: 2026/01/16 08:43:33]
+[2026-01-16 08:43:34.350] 开始执行:打印信息: adb click [系统时间: 2026/01/16 08:43:34]
+[2026-01-16 08:43:34.351] 开始执行:保存对话:  [系统时间: 2026/01/16 08:43:34]
+[2026-01-16 08:43:35.469] 开始执行:打印信息: save-new-chat [系统时间: 2026/01/16 08:43:35]
+[2026-01-16 08:43:35.469] 开始执行:打印信息: if [系统时间: 2026/01/16 08:43:35]
+[2026-01-16 08:43:35.469] 等待 1000ms 后执行第 2 次循环...
+[2026-01-16 08:43:36.574] 开始执行:打印信息: =============第 {{turn}} 轮============= [系统时间: 2026/01/16 08:43:36]
+[2026-01-16 08:43:37.652] 开始执行:设置变量: {turn} [系统时间: 2026/01/16 08:43:37]
+[2026-01-16 08:43:38.738] 开始执行:打印信息: set [系统时间: 2026/01/16 08:43:38]
+[2026-01-16 08:43:39.841] 开始执行:OCR识别对话:  [系统时间: 2026/01/16 08:43:39]
+[2026-01-16 08:43:53.297] 输出变量: currentMessage: "[{\"text\":\"70/59\",\"sender\":\"me\",\"y\":58,\"confidence\":0.5863862633705139},{\"text\":\"781 9\",\"sender\":\"me\"..."
+[2026-01-16 08:43:53.298] 结束执行:OCR识别对话:  时长:13.46秒 [系统时间: 2026/01/16 08:43:53]
+[2026-01-16 08:43:54.342] 开始执行:打印信息: ocr-chat [系统时间: 2026/01/16 08:43:54]
+[2026-01-16 08:43:55.450] 开始执行:读取聊天记录:  [系统时间: 2026/01/16 08:43:55]
+[2026-01-16 08:43:56.512] 开始执行:打印信息: read-chat-history [系统时间: 2026/01/16 08:43:56]
+[2026-01-16 08:43:57.611] 开始执行:打印信息: if [系统时间: 2026/01/16 08:43:57]
+[2026-01-16 08:43:58.679] 开始执行:打印信息: if [系统时间: 2026/01/16 08:43:58]
+[2026-01-16 08:43:59.754] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:43:59]
+[2026-01-16 08:43:59.754] 输出变量: lastMessage: "-", lastRole: "friend"
+[2026-01-16 08:44:00.804] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:44:00]
+[2026-01-16 08:44:01.884] 开始执行:读取最后一条消息:  [系统时间: 2026/01/16 08:44:01]
+[2026-01-16 08:44:01.884] 输出变量: lastHistoryMessage: "换行", lastHistoryRole: "me"
+[2026-01-16 08:44:02.955] 开始执行:打印信息: read-last-message [系统时间: 2026/01/16 08:44:02]
+[2026-01-16 08:44:04.023] 开始执行:AI生成:  [系统时间: 2026/01/16 08:44:04]
+[2026-01-16 08:44:13.952] 输出变量: aiReply: "嗨,我是通过 Benny 认识你的,真的很感谢你愿意从零基础带我进入游戏开发。我现在想了解接下来怎么开始,方便告诉我可行的入门步骤吗?也可以先找个时间聊聊。", aiCallBack: 1
+[2026-01-16 08:44:13.955] 结束执行:AI生成:  时长:9.93秒 [系统时间: 2026/01/16 08:44:13]
+[2026-01-16 08:44:14.986] 开始执行:打印信息: ai-generate [系统时间: 2026/01/16 08:44:14]
+[2026-01-16 08:44:16.065] 开始执行:打印信息: while [系统时间: 2026/01/16 08:44:16]
+[2026-01-16 08:44:17.102] 开始执行:设置变量: {aiCallBack} [系统时间: 2026/01/16 08:44:17]
+[2026-01-16 08:44:18.149] 开始执行:打印信息: set [系统时间: 2026/01/16 08:44:18]
+[2026-01-16 08:44:19.225] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:44:19]
+[2026-01-16 08:44:20.995] 结束执行:ADB操作:  时长:1.77秒 [系统时间: 2026/01/16 08:44:20]
+[2026-01-16 08:44:22.071] 开始执行:打印信息: adb input [系统时间: 2026/01/16 08:44:22]
+[2026-01-16 08:44:23.147] 开始执行:打印信息: if [系统时间: 2026/01/16 08:44:23]
+[2026-01-16 08:44:24.220] 开始执行:ADB操作:  [系统时间: 2026/01/16 08:44:24]
+[2026-01-16 08:44:24.378] 结束执行:ADB操作:  时长:0.16秒 [系统时间: 2026/01/16 08:44:24]
+[2026-01-16 08:44:25.468] 开始执行:打印信息: adb click [系统时间: 2026/01/16 08:44:25]
+[2026-01-16 08:44:25.468] 开始执行:保存对话:  [系统时间: 2026/01/16 08:44:25]
+[2026-01-16 08:44:26.623] 开始执行:打印信息: save-new-chat [系统时间: 2026/01/16 08:44:26]
+[2026-01-16 08:44:26.623] 开始执行:打印信息: if [系统时间: 2026/01/16 08:44:26]
+[2026-01-16 08:44:26.623] 所有操作执行完成
+[2026-01-16 08:44:26.623] ==================== 工作流执行完成 ====================

+ 81 - 24
static/processing/微信聊天自动发送工作流/processing.json

@@ -1,13 +1,17 @@
 {
 	"variables": {
+		"turn": 0,
+		"relationBg": "",
 		"chatHistory": "",
 		"currentMessage": "",
 		"chat-history":"",
+		"lastHistoryMessage": "",
 		"lastMessage": "",
 		"lastRole": "",
+		"lastHistoryRole": "",
 		"aiReply": "",
 		"aiCallBack":0,
-		"sendBtnPos": null,
+		"sendBtnPos": ""
 	},
 	"execute": [
 		{
@@ -15,41 +19,83 @@
 			"condition": 
 			{
 				"interval": "1s",
-				"repeat": -1
+				"repeat": 2
 			},
 			"interval": 
 			[
 				{
-					"type": "ocr-chat-history",
-					"inVars": ["好友头像.png","我的头像.png"],
-					"outVars": ["{chatHistory}"],
-
+					"type": "echo",
+					"value": "=============第 {{turn}} 轮============="
 				},
 				{
-					"type": "read-last-message",
-					"inVars": ["{chatHistory}"],
-					"outVars": ["{currentMessage}"],
+					"type": "set",
+					"variable": "{turn}",
+					"value": "{turn} + 1"
+				},
+				{
+					"type": "ocr-chat",
+					"inVars": ["好友头像.png","我的头像.png"],
+					"outVars": ["{currentMessage}"]
 				},
 				{
 					"type": "read-chat-history",
 					"inVars": [],
-					"outVars": ["{chat-history}"],
+					"outVars": ["{chat-history}"]
 				},
 				{
-					"type": "read-last-message",
-					"inputVars": ["{chat-history}"],
-					"outputVars": ["{lastMessage}","{lastRole}"],
+					"type": "if",
+					"condition": "{chat-history}==\"\"",
+					"ture": 
+					[
+						{
+							"type": "save-new-chat",
+							"inVars": ["{currentMessage}"],
+							"outVars": []
+						}
+					]
 				},
 				{
 					"type": "if",
-					"condition": "{currentMessage} != {lastMessage}",
+					"condition": "{relationBg}==\"\"",
 					"ture": 
 					[
 						{
-							"type": "save-new-chat",
-							"inVars": ["{chatHistory}"],
-							"outVars": [],
+							"type": "ai-generate",
+							"prompt": "根据聊天记录:{{chat-history}},推理出我与聊天好友的关系背景,用一句话描述出来",
+							"inVars": ["{chat-history}"],
+							"outVars": ["{relationBg}","{aiCallBack}"]	
+						},
+						{
+							"type": "while",
+							"condition": "1 == aiCallBack",
+							"ture": 
+							[
+								
+							]
 						},
+						{
+							"type": "set",
+							"variable": "{aiCallBack}",
+							"value": "0"
+						}
+					]
+				},
+				{
+					"type": "read-last-message",
+					"inVars": ["{currentMessage}"],
+					"outVars": ["{lastMessage}","{lastRole}"]
+				},
+				{
+					"type": "read-last-message",
+					"inVars": ["{chat-history}"],
+					"outVars": ["{lastHistoryMessage}","{lastHistoryRole}"]
+				},
+				{
+					"type": "if",
+					"condition": "{lastMessage} != {lastHistoryMessage}",
+					"ture": 
+					[
+						
 						{
 							"type": "if",
 							"condition": "{lastRole} == \"friend\"",
@@ -57,9 +103,9 @@
 							[
 								{
 									"type": "ai-generate",
-									"prompt": "你是一个刚认识女方不久的男生,正在和女方通过微信聊天。你的目标是:通过自然、有趣的对话让女方对你产生好感,建立良好的第一印象。\n\n女方最新发来的消息:\"{currentMessage}\"\n\n请根据以上信息,代入我的角色,生成一条自然、有趣的回复。要求:\n1. 完全代入刚认识女方不久的男生角色,用轻松、有趣、略带暧昧的口吻回复;\n2. 回复要自然流畅,像真实聊天一样,不要过于刻意或油腻;\n3. 可以适当展现幽默感、关心或好奇心,让对话更有趣;\n4. 如果识别的内容不清楚或包含无关文字,请根据核心意思理解并回复;\n5. 回复长度控制在20字以内;\n6. 目标是让女方对你产生好感,但不要过于直接或冒进。\n\n请严格按照以下JSON格式返回,不要包含任何其他文字:\n{\n  \"reply\": \"你的回复内容\"\n}",
-									"inVars": ["{currentMessage}","{aiCallBack}"],
-									"outVars": ["{aiReply}"]	
+									"prompt": "根据我们背景关系描述:{{relationBg}},以及我们的聊天记录:{{lastMessage}},帮我做出适合回复,不要给建议,直接给一条你认为最好的回复信息,回复信息字数要符合微信聊天习惯",
+									"inVars": ["{relationBg}","{lastMessage}"],
+									"outVars": ["{aiReply}","{aiCallBack}"]	
 								},
 								{
 									"type": "while",
@@ -69,6 +115,11 @@
 										
 									]
 								},
+								{
+									"type": "set",
+									"variable": "{aiCallBack}",
+									"value": "0"
+								},
 								{
 									"type": "adb",
 									"method": "input",
@@ -76,8 +127,9 @@
 								},
 								{
 									"type": "if",
-									"condition": "{sendBtnPos} == null",
-									"ture": [
+									"condition": "{sendBtnPos} == \"\"",
+									"ture": 
+									[
 										{
 											"type": "image-center-location",
 											"inVars": ["微信聊天界面的发送按钮定位图.png"],
@@ -90,11 +142,16 @@
 									"method": "click",
 									"inVars": ["{sendBtnPos}"]
 								}		
-							]
+							]	
+						},
+						{
+							"type": "save-new-chat",
+							"inVars": ["{currentMessage}"],
+							"outVars": []
 						}
 					]
 				}
 			]	
-		},		
+		}		
 	]
 }

+ 0 - 0
static/processing/微信聊天自动发送工作流/我的头像.png → static/processing/微信聊天自动发送工作流/resources/好友头像.png


+ 0 - 0
static/processing/微信聊天自动发送工作流/微信聊天界面的发送按钮定位图.png → static/processing/微信聊天自动发送工作流/resources/微信聊天界面的发送按钮定位图.png


+ 0 - 0
static/processing/微信聊天自动发送工作流/微信聊天界面的输入框定位图.png → static/processing/微信聊天自动发送工作流/resources/微信聊天界面的输入框定位图.png


+ 0 - 0
static/processing/微信聊天自动发送工作流/好友头像.png → static/processing/微信聊天自动发送工作流/resources/我的头像.png


+ 81 - 0
static/processing/微信聊天自动发送工作流/工作流思维导图.md

@@ -0,0 +1,81 @@
+# 微信聊天自动发送工作流 - 思维导图
+
+## 🔄 主流程
+
+```
+schedule (每1秒执行一次,共2次)
+│
+├─→ 1. set: turn = turn + 1
+│
+├─→ 2. ocr-chat: OCR识别当前屏幕聊天内容
+│   ├─→ 输入: 好友头像.png, 我的头像.png
+│   └─→ 输出: {currentMessage} (JSON字符串)
+│
+├─→ 3. read-chat-history: 读取历史聊天记录
+│   ├─→ 输入: 无
+│   └─→ 输出: {chat-history} (JSON字符串)
+│
+├─→ 4. if: 判断 {chat-history} 是否为空
+│   │  条件: {chat-history} == ""
+│   │
+│   └─→ [条件为真] → save-new-chat: 保存新聊天记录
+│       └─→ 输入: {currentMessage}
+│
+├─→ 5. if: 判断 {relationBg} 是否为空
+│   │  条件: {relationBg} == ""
+│   │
+│   └─→ [条件为真] → 生成关系背景
+│       ├─→ ai-generate: 根据聊天记录生成关系背景描述
+│       │   ├─→ 提示词: "根据聊天记录:{{chat-history}},推理出我与聊天好友的关系背景,用一句话描述出来"
+│       │   ├─→ 输入: {chat-history}
+│       │   └─→ 输出: {relationBg}, {aiCallBack}
+│       │
+│       ├─→ while: 等待AI回调完成
+│       │   └─→ 条件: 1 == aiCallBack
+│       │
+│       └─→ set: aiCallBack = 0 (重置回调标志)
+│
+├─→ 6. read-last-message: 读取当前消息的最后一条
+│   ├─→ 输入: {currentMessage}
+│   └─→ 输出: {lastMessage}, {lastRole}
+│
+├─→ 7. read-last-message: 读取历史消息的最后一条
+│   ├─→ 输入: {chat-history}
+│   └─→ 输出: {lastHistoryMessage}, {lastHistoryRole}
+│
+└─→ 8. if: 判断是否有新消息
+    │  条件: {lastMessage} != {lastHistoryMessage}
+    │
+    └─→ [条件为真] → 处理新消息
+        │
+        ├─→ if: 判断最后一条消息是否来自好友
+        │   │  条件: {lastRole} == "friend"
+        │   │
+        │   └─→ [条件为真] → 自动回复流程
+        │       │
+        │       ├─→ ai-generate: 生成回复内容
+        │       │   ├─→ 提示词: "根据我们背景关系描述:{{relationBg}},以及我们的聊天记录:{{lastMessage}},帮我做出适合回复..."
+        │       │   ├─→ 输入: {relationBg}, {lastMessage}
+        │       │   └─→ 输出: {aiReply}, {aiCallBack}
+        │       │
+        │       ├─→ while: 等待AI回调完成
+        │       │   └─→ 条件: 1 == aiCallBack
+        │       │
+        │       ├─→ set: aiCallBack = 0 (重置回调标志)
+        │       │
+        │       ├─→ adb input: 输入AI生成的回复
+        │       │   └─→ 输入: {aiReply}
+        │       │
+        │       ├─→ if: 判断发送按钮位置是否已定位
+        │       │   │  条件: {sendBtnPos} == ""
+        │       │   │
+        │       │   └─→ [条件为真] → image-center-location: 定位发送按钮
+        │       │       ├─→ 输入: 微信聊天界面的发送按钮定位图.png
+        │       │       └─→ 输出: {sendBtnPos}
+        │       │
+        │       └─→ adb click: 点击发送按钮
+        │           └─→ 输入: {sendBtnPos}
+        │
+        └─→ save-new-chat: 保存新聊天记录
+            └─→ 输入: {currentMessage}
+```

BIN
temp_screenshot.png