yichael 5 ヶ月 前
コミット
824874ce6d

+ 122 - 0
document/工作流语法.md

@@ -0,0 +1,122 @@
+# 工作流语法
+
+## 基本结构
+
+```json
+{
+  "variables": {},
+  "execute": []
+}
+```
+
+> 注意:为了向后兼容,`excuse` 字段名仍然支持,但建议使用 `execute`。
+
+## 语法分层
+
+- **基础语法**:`schedule`、`if`、`while`
+- **基础 action**:`adb`(通过 method 区分)
+- **扩展标签(Func)**:`src/pages/processing/func/` 目录下的脚本文件名即为标签名
+
+## 定时执行(schedule)
+
+```json
+{
+  "type": "schedule",
+  "condition": {
+    "interval": "1s",
+    "repeat": -1
+  },
+  "interval": []
+}
+```
+
+- `interval`: 执行间隔("1s", "2m", "3h")
+- `repeat`: 重复次数(`-1` 表示无限循环)
+
+## ADB操作(adb)
+
+统一格式:
+```json
+{
+  "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]`: 文字内容 |
+
+## 条件判断(if)
+
+```json
+{
+  "type": "if",
+  "condition": "{变量} == '值'",
+  "ture": [],
+  "false": []
+}
+```
+
+支持操作符:`==` `!=` `>` `<` `>=` `<=`
+
+## 循环(while)
+
+```json
+{
+  "type": "while",
+  "condition": "{变量} > 0",
+  "ture": []
+}
+```
+
+> 注意:`ture` 是 `body` 的别名。
+
+## 内置操作
+
+### 延迟(delay)
+```json
+{ "type": "delay", "value": "2s" }
+```
+
+### 设置变量(set)
+```json
+{ "type": "set", "variable": "name", "value": "value" }
+```
+
+### 随机数(random)
+```json
+{ "type": "random", "variable": "num", "min": 1, "max": 100, "integer": true }
+```
+
+## 变量
+
+- 定义:`"variables": {"name": "value"}`
+- 使用:`"{name}"` 在字段中引用
+- 保存:`"outVars": ["{name}"]` 或 `"variable": "name"`
+
+## 时间格式
+
+- 间隔:`"1s"`, `"2m"`, `"3h"`
+- 日期:`"2026/1/14 01:21"`
+- 时间:`"09:00"`
+
+## 扩展标签
+
+扩展标签由 `src/pages/processing/func/` 目录下的脚本文件决定,每个脚本文件名即为标签名。
+
+常用标签:
+- `ocr-chat-history`: OCR提取聊天记录
+- `read-chat-history`: 读取聊天记录
+- `read-last-message`: 读取最后一条消息
+- `save-new-chat`: 保存对话
+- `image-center-location`: 图像中心点定位
+- `image-region-location`: 图像区域定位(返回四个顶点)

+ 0 - 352
document/工作流语言使用说明.md

@@ -1,352 +0,0 @@
-# 工作流语言使用说明
-
-## 基本结构(JSON)
-
-```json
-{
-  "version": "1.0",
-  "name": "工作流名称",
-  "triggers": [],      // 触发条件(scheduleOnce / schedule)
-  "variables": {},     // 变量定义
-  "actions": []        // 操作列表
-}
-```
-
-## 语法分层(重要)
-
-本项目的工作流语言分为 3 层:
-
-- **基础语法(控制流 / 定时)**:`scheduleOnce`、`schedule`、`for`、`while`、`if`
-- **基础 action(模拟手机操作)**:`press`、`swipe`、`scroll`
-- **扩展标签(Func)**:除基础能力外的所有扩展功能,全部由 `src/pages/processing/func/` 下的脚本文件决定;**每个脚本文件名就是一个标签名**。工作流能用哪些扩展标签,以该目录为准。
-
-## 触发条件(Triggers:scheduleOnce / schedule)
-
-### scheduleOnce(单次执行)
-```json
-{
-  "type": "schedule",
-  "schedule": {
-    "datetime": "2026/1/14 01:21"
-  }
-}
-```
-
-### schedule(每天执行)
-```json
-{
-  "type": "schedule",
-  "schedule": {
-    "time": "09:00",
-    "repeat": "daily"
-  }
-}
-```
-
-### schedule(每周执行)
-```json
-{
-  "type": "schedule",
-  "schedule": {
-    "time": "09:00",
-    "weekdays": ["monday", "wednesday", "friday"]
-  }
-}
-```
-
-### schedule(间隔执行)
-```json
-{
-  "type": "schedule",
-  "schedule": {
-    "interval": "30 minutes"
-  }
-}
-```
-
-## 基础语法(for / while / if)
-
-### if
-```json
-{ "type": "if", "condition": "{count} > 5", "then": [...], "else": [...] }
-```
-
-### for
-```json
-{ "type": "for", "variable": "item", "items": [1,2,3], "body": [...] }
-```
-
-### while
-```json
-{ "type": "while", "condition": "true", "body": [...] }
-```
-
-## 基础 action(press / swipe / scroll)
-
-| 操作 | 说明 | 示例 |
-|------|------|------|
-| `press` | 点击(图片/坐标/文字等) | `{"type":"press","value":"button.png"}` |
-| `swipe` | 滑动 | `{"type":"swipe","value":"down-up"}` |
-| `scroll` | 滚动(小幅度) | `{"type":"scroll","value":"down-up"}` |
-
-> 说明:项目内部为了实现完整自动化,还存在一些内置能力(如 `locate/click/input/ocr/ai-generate/delay/set/random` 等)。但从"语言设计"角度,**低层模拟手机操作统一归为 action(press/swipe/scroll)**,其余更高阶能力推荐以 Func 标签方式抽象出来(见下文)。
-
-## 内置操作(delay / set / random)
-
-### delay(延迟)
-```json
-{ "type": "delay", "value": "2s" }
-{ "type": "delay", "value": "{randomValue}s" }  // 支持变量引用
-```
-- `value`: 延迟时间,支持格式:`"2s"`(秒)、`"5m"`(分钟)、`"2h"`(小时)
-- 支持变量引用,例如:`"{randomValue}s"` 会解析变量值后加上单位
-
-### set(设置变量)
-```json
-{ "type": "set", "variable": "count", "value": "10" }
-{ "type": "set", "variable": "message", "value": "{lastMessage}" }  // 支持变量引用
-```
-- `variable`: 变量名
-- `value`: 变量值,支持变量引用
-
-### random(生成随机数)
-```json
-{ "type": "random", "variable": "randomValue", "min": 1, "max": 100, "integer": true }
-{ "type": "random", "variable": "delayTime", "min": 3.5, "max": 8.5, "integer": false }
-```
-- `variable`: 保存随机数的变量名
-- `min`: 最小值(数字)
-- `max`: 最大值(数字)
-- `integer`: 是否生成整数(默认 `true`)
-  - `true`: 生成整数,范围 `[min, max]`(包含两端)
-  - `false`: 生成浮点数,范围 `[min, max)`(不包含 max)
-
-**示例:随机点赞概率**
-```json
-{
-  "type": "random",
-  "variable": "randomValue",
-  "min": 1,
-  "max": 100,
-  "integer": true,
-  "delay": "0.5s"
-},
-{
-  "type": "if",
-  "condition": "{randomValue} <= 30",
-  "then": [
-    { "type": "click", "method": "image", "target": "点赞按钮.png" }
-  ]
-}
-```
-
-## 定位方式(method)
-
-- `image`: 图像匹配
-- `text`: 文字识别
-- `coordinate`: 坐标定位
-- `by-avatar`: 通过头像定位(OCR专用)
-
-## 扩展标签(Func:以目录为准)
-
-扩展标签统一放在:
-
-- `src/pages/processing/func/`
-
-该目录下目前提供的标签(即“除了基础功能之外还能调用哪些能力”):
-
-- **`extract-chat-history`**:提取消息/会话记录(含发送者),保存到变量
-- **`string-reg-location`**:通过文字匹配获取位置坐标
-- **`save-chat-history`**:把消息/会话记录保存到 `history` 目录
-
-> 注意:以上列表应当与 `src/pages/processing/func/` 目录内容保持一致;后续新增能力只需要新增对应脚本文件即可。
-
-## 扩展标签示例:extract-chat-history
-
-```json
-{
-  "type": "extract-chat-history",
-  "avatar1": "参与者1头像.png",
-  "avatar2": "参与者2头像.png",
-  "variable": "messages",
-  "delay": "2s"
-}
-```
-
-## 变量使用
-
-- 定义:`"variables": {"topic": "美食"}`
-- 使用:`"{topic}"` 在任意字段中引用
-- 保存:`"variable": "result"` 保存操作结果
-
----
-
-## 示例1:小红书定时发文
-
-```json
-{
-  "version": "1.0",
-  "name": "小红书定时发布",
-  "triggers": [
-    {
-      "type": "schedule",
-      "schedule": {
-        "time": "09:00",
-        "weekdays": ["monday", "wednesday", "friday"]
-      }
-    }
-  ],
-  "variables": {
-    "topics": ["美食探店", "旅行攻略", "穿搭分享"]
-  },
-  "actions": [
-    {
-      "type": "locate",
-      "method": "image",
-      "target": "xiaohongshu_icon.png",
-      "variable": "appIcon",
-      "delay": "2s"
-    },
-    {
-      "type": "click",
-      "method": "position",
-      "target": "{appIcon}",
-      "delay": "3s"
-    },
-    {
-      "type": "for",
-      "variable": "topic",
-      "items": "{topics}",
-      "body": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "发布",
-          "delay": "2s"
-        },
-        {
-          "type": "ai-generate",
-          "prompt": "生成一篇关于{topic}的小红书文章,标题+正文500字,包含emoji",
-          "variable": "article",
-          "delay": "15s"
-        },
-        {
-          "type": "input",
-          "method": "locate",
-          "target": "内容输入框",
-          "value": "{article}",
-          "delay": "3s"
-        },
-        {
-          "type": "click",
-          "method": "text",
-          "target": "发布",
-          "delay": "2s"
-        },
-        {
-          "type": "delay",
-          "value": "5s"
-        }
-      ]
-    }
-  ]
-}
-```
-
----
-
-## 示例2:QQ多人聊天自动回复
-
-```json
-{
-  "version": "1.0",
-  "name": "QQ群自动回复",
-  "triggers": [
-    {
-      "type": "schedule",
-      "schedule": {
-        "interval": "30 minutes"
-      }
-    }
-  ],
-  "variables": {
-    "chatTargets": ["工作群", "朋友群", "家人群"],
-    "myAvatar": "my_avatar.png"
-  },
-  "actions": [
-    {
-      "type": "for",
-      "variable": "target",
-      "items": "{chatTargets}",
-      "body": [
-        {
-          "type": "click",
-          "method": "text",
-          "target": "{target}",
-          "delay": "2s"
-        },
-        {
-          "type": "ocr",
-          "method": "by-avatar",
-          "avatar": "{myAvatar}",
-          "variable": "lastMessage",
-          "delay": "3s"
-        },
-        {
-          "type": "if",
-          "condition": "{lastMessage} != '' && {lastMessage} != null",
-          "then": [
-            {
-              "type": "ai-generate",
-              "prompt": "根据以下消息生成友好回复:{lastMessage}",
-              "variable": "reply",
-              "delay": "5s"
-            },
-            {
-              "type": "input",
-              "method": "locate",
-              "target": "输入框",
-              "value": "{reply}",
-              "delay": "1s"
-            },
-            {
-              "type": "click",
-              "method": "text",
-              "target": "发送",
-              "delay": "2s"
-            }
-          ]
-        },
-        {
-          "type": "click",
-          "method": "text",
-          "target": "返回",
-          "delay": "1s"
-        }
-      ]
-    }
-  ]
-}
-```
-
----
-
-## 常用字段说明
-
-- `type`: 操作类型(必需)
-- `method`: 实现方式(可选)
-- `target`: 操作目标
-- `value`: 操作的值
-- `variable`: 保存结果到变量
-- `condition`: 执行条件
-- `delay`: 延迟时间(如 "2s", "1m")
-- `body`: 子操作列表(用于循环、条件)
-- `then`/`else`: 条件分支
-- `items`: 循环的数组
-
-## 时间格式
-
-- 日期时间:`"2026/1/14 01:21"`
-- 时间:`"09:00"`
-- 星期:`["monday", "wednesday", "friday"]` 或 `["周一", "周三", "周五"]`
-- 间隔:`"30 minutes"`, `"2 hours"`

+ 366 - 0
main-js/execute-py.js

@@ -0,0 +1,366 @@
+/**
+ * 执行 Python 相关操作模块(通用功能)
+ * 负责图像匹配、OCR识别等通用功能(通过调用 Python/JS 实现)
+ * 注意:聊天记录提取等业务相关功能在 read-and-write.js 中
+ */
+
+import { ipcMain } from 'electron';
+import { writeFile, mkdir, rm } 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 { 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';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 执行图像匹配:截图、调用 Python 脚本、返回坐标
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} templateImagePath - 模板图片路径
+ * @returns {Promise<{success: boolean, error?: string, coordinate?: Object, clickPosition?: Object}>}
+ */
+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 };
+  }
+}
+
+/**
+ * 图像区域定位:在完整截图中查找区域截图的位置,返回四个顶点坐标
+ * @param {string} screenshotPath - 完整截图路径
+ * @param {string} regionPath - 区域截图路径
+ * @param {string} device - 设备 ID(可选,用于获取分辨率)
+ * @returns {Promise<{success: boolean, error?: string, corners?: Object}>}
+ */
+export async function matchImageRegionLocation(screenshotPath, regionPath, device = null) {
+  try {
+    if (!regionPath) {
+      return { success: false, error: '缺少区域截图路径' };
+    }
+
+    // 如果 screenshotPath 为 '__AUTO_SCREENSHOT__' 或 null,自动从设备获取截图
+    let absoluteScreenshotPath = screenshotPath;
+    if (!screenshotPath || screenshotPath === '__AUTO_SCREENSHOT__' || screenshotPath === null) {
+      if (!device) {
+        return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' };
+      }
+      
+      // 自动获取设备截图
+      const resolutionResult = await getDeviceResolution(device);
+      if (!resolutionResult.success) {
+        return { success: false, error: '获取设备分辨率失败' };
+      }
+
+      const screenshotResult = await captureScreenshot(device, { format: 'png' });
+      if (!screenshotResult.success || !screenshotResult.data) {
+        return { success: false, error: '自动获取设备截图失败' };
+      }
+
+      // 保存截图到临时文件
+      const tempDir = join(__dirname, '..');
+      const tempScreenshotPath = join(tempDir, 'temp_screenshot.png');
+      const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
+      await writeFile(tempScreenshotPath, screenshotBuffer);
+      
+      absoluteScreenshotPath = tempScreenshotPath;
+    } else {
+      // 将相对路径转换为绝对路径
+      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 脚本、返回坐标
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} targetText - 目标文字
+ * @returns {Promise<{success: boolean, error?: string, coordinate?: Object, clickPosition?: Object}>}
+ */
+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识别(通用功能)
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} folderPath - 工作流文件夹路径(可选,用于保存临时文件)
+ * @returns {Promise<{success: boolean, error?: string, text?: string}>}
+ */
+export async function ocrFullScreen(ipPort, 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, '/');
+      const result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
+      
+      if (result.success) {
+        return {
+          success: true,
+          text: result.text || ''
+        };
+      } 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 };
+  }
+}
+
+/**
+ * 注册 IPC 处理器(Python 执行相关)
+ */
+export function registerIpcHandlers() {
+  // 图像匹配
+  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('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
+    return await findTextAndGetCoordinate(ipPort, targetText);
+  });
+
+  // 全屏OCR识别(兼容旧API:ocr-last-message)
+  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' };
+  });
+}

+ 0 - 314
main-js/func/adb-extract-chat-history.js

@@ -1,314 +0,0 @@
-/**
- * 使用 ADB XML 解析方式提取聊天记录
- * 通过 uiautomator dump 获取 UI 层次结构 XML,然后解析提取文本
- */
-
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import { join, dirname, isAbsolute } from 'path';
-import { fileURLToPath } from 'url';
-import { getCachedAdbPath } from '../config.js';
-
-const execAsync = promisify(exec);
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-/**
- * 使用 ADB uiautomator dump 获取屏幕文本
- * @param {string} ipPort - 设备 ID/IP:Port
- * @param {string} method - 提取方法('full-screen' 或 'last-message')
- * @param {string} folderPath - 工作流文件夹路径(可选,如果提供则保存 XML 到 tmp 目录)
- * @returns {Promise<{success: boolean, text?: string, error?: string}>}
- */
-export async function extractChatHistoryByAdbXml(ipPort, method = 'full-screen', folderPath = null) {
-  try {
-    if (!ipPort) {
-      return { success: false, error: '缺少设备 ID' };
-    }
-
-    const adbPath = getCachedAdbPath();
-
-    // 使用 uiautomator dump 获取 UI 层次结构 XML
-    // 先 dump 到设备文件,然后读取内容
-    let xmlContent = '';
-    
-    try {
-      // 执行 uiautomator dump 到设备文件
-      // 尝试多种方式获取 UI dump
-      console.log('[ADB XML] 尝试方法1: uiautomator dump 到文件');
-      const dumpCommand = `${adbPath} -s ${ipPort} shell uiautomator dump /sdcard/ui_dump.xml`;
-      const { stdout: dumpStdout, stderr: dumpStderr } = await execAsync(dumpCommand, {
-        timeout: 10000,
-        maxBuffer: 1024 * 1024,
-        encoding: 'utf8'
-      });
-      
-      // 检查 dump 是否成功(通常输出 "UI hierchary dumped to: /sdcard/ui_dump.xml")
-      console.log('[ADB XML] dump 命令输出:', dumpStdout);
-      if (dumpStderr) {
-        console.log('[ADB XML] dump 命令错误输出:', dumpStderr);
-      }
-      
-      // 等待一小段时间,确保文件写入完成
-      await new Promise(resolve => setTimeout(resolve, 500));
-      
-      // 读取 XML 文件内容
-      const catCommand = `${adbPath} -s ${ipPort} shell cat /sdcard/ui_dump.xml`;
-      const { stdout } = await execAsync(catCommand, {
-        timeout: 10000,
-        maxBuffer: 10 * 1024 * 1024,
-        encoding: 'utf8'
-      });
-      
-      console.log('[ADB XML] 读取到的原始内容长度:', stdout.length);
-      console.log('[ADB XML] 读取到的原始内容(前500字符):', stdout.substring(0, 500));
-      
-      // 清理可能的提示信息(uiautomator dump 可能会在 XML 前输出提示)
-      // 查找 XML 开始标记
-      const xmlStartIndex = stdout.indexOf('<?xml');
-      if (xmlStartIndex > 0) {
-        xmlContent = stdout.substring(xmlStartIndex);
-      } else if (stdout.includes('<hierarchy')) {
-        // 如果没有 XML 声明,查找 hierarchy 标签
-        const hierarchyStartIndex = stdout.indexOf('<hierarchy');
-        xmlContent = stdout.substring(hierarchyStartIndex);
-      } else {
-        xmlContent = stdout;
-      }
-      
-      // 清理设备上的临时文件
-      try {
-        await execAsync(`${adbPath} -s ${ipPort} shell rm /sdcard/ui_dump.xml`, {
-          timeout: 3000
-        });
-      } catch (rmError) {
-        // 忽略删除失败
-      }
-    } catch (error) {
-      console.error('[ADB XML] 获取 UI dump 失败:', error);
-      return { success: false, error: `获取 UI dump 失败: ${error.message}` };
-    }
-
-    if (!xmlContent || !xmlContent.trim()) {
-      console.error('[ADB XML] UI dump 内容为空');
-      return { success: false, error: 'UI dump 内容为空' };
-    }
-
-    // 调试:输出 XML 内容的前1000字符和总长度
-    console.log('[ADB XML] UI dump 内容长度:', xmlContent.length);
-    console.log('[ADB XML] UI dump 内容(前1000字符):', xmlContent.substring(0, 1000));
-    
-    // 检查是否包含 XML 结构
-    if (!xmlContent.includes('<hierarchy') && !xmlContent.includes('<?xml')) {
-      console.error('[ADB XML] UI dump 内容不包含有效的 XML 结构');
-      console.error('[ADB XML] 实际内容:', xmlContent.substring(0, 500));
-      return { success: false, error: 'UI dump 内容格式不正确,未找到 XML 结构' };
-    }
-    
-    // 检查 XML 是否为空(只有空的 hierarchy 节点)
-    const nodeCount = (xmlContent.match(/<node/g) || []).length;
-    console.log('[ADB XML] XML 中的 node 节点数量:', nodeCount);
-    
-    if (nodeCount <= 1) {
-      console.warn('[ADB XML] 警告:XML 中几乎没有节点,可能是微信使用了自定义视图,uiautomator dump 无法获取聊天内容');
-      console.warn('[ADB XML] 建议:微信聊天内容可能使用 Canvas 或自定义 View 绘制,uiautomator dump 无法捕获这些内容');
-      console.warn('[ADB XML] 解决方案:1. 检查是否需要开启辅助功能权限 2. 考虑使用 OCR 方式 3. 尝试其他 ADB 命令');
-      
-      // 尝试使用 dumpsys 获取窗口信息(作为备选方案)
-      try {
-        console.log('[ADB XML] 尝试方法2: dumpsys window 获取窗口信息');
-        const dumpsysCommand = `${adbPath} -s ${ipPort} shell dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp'`;
-        const { stdout: dumpsysStdout } = await execAsync(dumpsysCommand, {
-          timeout: 5000,
-          maxBuffer: 1024 * 1024,
-          encoding: 'utf8'
-        });
-        console.log('[ADB XML] dumpsys window 输出:', dumpsysStdout);
-      } catch (dumpsysError) {
-        console.warn('[ADB XML] dumpsys 命令执行失败:', dumpsysError.message);
-      }
-    }
-
-    // 如果提供了 folderPath,保存 XML 到 tmp 目录
-    if (folderPath) {
-      try {
-        const { mkdir, writeFile } = await import('fs/promises');
-        
-        // 确保 folderPath 是绝对路径
-        let absoluteFolderPath = folderPath;
-        if (!isAbsolute(folderPath)) {
-          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 {
-            absoluteFolderPath = join(__dirname, '..', '..', 'static', 'processing', folderPath);
-          }
-        }
-        
-        // 创建 tmp 目录
-        const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
-        const tmpDir = join(absoluteFolderPath, 'tmp', timestamp);
-        await mkdir(tmpDir, { recursive: true });
-        
-        // 保存 XML 文件
-        const xmlPath = join(tmpDir, 'ui_dump.xml');
-        await writeFile(xmlPath, xmlContent, 'utf-8');
-        console.log(`[ADB XML] XML 已保存到: ${xmlPath}`);
-      } catch (saveError) {
-        console.warn('[ADB XML] 保存 XML 到 tmp 目录失败:', saveError);
-        // 保存失败不影响主流程,继续执行
-      }
-    }
-
-    // 解析 XML,提取所有 text 属性
-    const texts = extractTextsFromXml(xmlContent);
-
-    console.log('[ADB XML] 提取到的文本数量:', texts.length);
-    if (texts.length > 0) {
-      console.log('[ADB XML] 提取到的文本(前5条):', texts.slice(0, 5));
-    }
-
-    if (texts.length === 0) {
-      // 调试:尝试直接匹配所有 text 属性(不过滤)
-      const allTexts = extractTextsFromXml(xmlContent, true);
-      console.log('[ADB XML] 未过滤的文本数量:', allTexts.length);
-      if (allTexts.length > 0) {
-        console.log('[ADB XML] 未过滤的文本(前5条):', allTexts.slice(0, 5));
-      }
-      
-      // 如果 XML 中几乎没有节点,说明 uiautomator dump 无法获取微信的聊天内容
-      const nodeCount = (xmlContent.match(/<node/g) || []).length;
-      if (nodeCount <= 1) {
-        return { 
-          success: false, 
-          error: '微信聊天内容使用自定义视图(Canvas/自定义View),uiautomator dump 无法获取。建议:1. 检查是否需要开启辅助功能权限 2. 考虑使用 OCR 方式提取文本' 
-        };
-      }
-      
-      return { success: false, error: '未能从 UI dump 中提取到文本' };
-    }
-
-    // 根据 method 返回结果
-    if (method === 'last-message') {
-      // 返回最后一条消息(通常是最后出现的文本)
-      return {
-        success: true,
-        text: texts[texts.length - 1] || ''
-      };
-    } else {
-      // 返回所有文本(用换行符连接)
-      return {
-        success: true,
-        text: texts.join('\n')
-      };
-    }
-  } catch (error) {
-    console.error('ADB XML 解析失败:', error);
-    return { success: false, error: error.message || 'ADB XML 解析失败' };
-  }
-}
-
-/**
- * 从 XML 内容中提取所有文本
- * 使用正则表达式提取 text 属性(简单但有效)
- * @param {string} xmlContent - XML 内容
- * @param {boolean} skipFilter - 是否跳过过滤(用于调试)
- * @returns {string[]} 提取的文本数组
- */
-function extractTextsFromXml(xmlContent, skipFilter = false) {
-  const texts = [];
-  
-  // 方法1: 使用正则表达式匹配 text="..." 属性(支持转义字符)
-  const textRegex1 = /text\s*=\s*"((?:[^"\\]|\\.)*)"|text\s*=\s*'((?:[^'\\]|\\.)*)'/g;
-  let match;
-  
-  while ((match = textRegex1.exec(xmlContent)) !== null) {
-    let text = match[1] || match[2] || '';
-    
-    // 处理转义字符
-    if (text) {
-      text = text
-        .replace(/\\"/g, '"')
-        .replace(/\\'/g, "'")
-        .replace(/\\\\/g, '\\');
-    }
-    
-    if (text && text.trim()) {
-      const trimmedText = text.trim();
-      if (skipFilter || !isUIControlText(trimmedText)) {
-        texts.push(trimmedText);
-      }
-    }
-  }
-  
-  // 方法2: 如果方法1没有匹配到任何内容,尝试更简单的正则表达式
-  if (texts.length === 0) {
-    console.log('[ADB XML] 方法1未匹配到文本,尝试方法2(简单正则)');
-    const textRegex2 = /text="([^"]*)"|text='([^']*)'/g;
-    let match2;
-    
-    while ((match2 = textRegex2.exec(xmlContent)) !== null) {
-      const text = match2[1] || match2[2] || '';
-      if (text && text.trim()) {
-        const trimmedText = text.trim();
-        if (skipFilter || !isUIControlText(trimmedText)) {
-          texts.push(trimmedText);
-        }
-      }
-    }
-  }
-  
-  // 方法3: 如果仍然没有匹配到,尝试匹配 content-desc 属性(某些应用使用 content-desc 而不是 text)
-  if (texts.length === 0) {
-    console.log('[ADB XML] 方法2未匹配到文本,尝试方法3(content-desc)');
-    const contentDescRegex = /content-desc="([^"]*)"|content-desc='([^']*)'/g;
-    let match3;
-    
-    while ((match3 = contentDescRegex.exec(xmlContent)) !== null) {
-      const text = match3[1] || match3[2] || '';
-      if (text && text.trim()) {
-        const trimmedText = text.trim();
-        if (skipFilter || !isUIControlText(trimmedText)) {
-          texts.push(trimmedText);
-        }
-      }
-    }
-  }
-  
-  // 调试:输出匹配结果
-  if (texts.length > 0) {
-    console.log(`[ADB XML] 成功提取 ${texts.length} 条文本`);
-  } else {
-    // 输出 XML 片段,帮助调试
-    const sampleXml = xmlContent.substring(0, 2000);
-    console.log('[ADB XML] 未能匹配到任何文本,XML 片段:', sampleXml);
-  }
-  
-  return texts;
-}
-
-/**
- * 判断是否为 UI 控件文本(按钮、标签等)
- * 可以根据实际需求调整过滤规则
- * @param {string} text - 文本内容
- * @returns {boolean} 是否为 UI 控件文本
- */
-function isUIControlText(text) {
-  // 暂时不过滤任何文本,保留所有文本
-  // 如果后续需要过滤,可以在这里添加逻辑
-  return false;
-  
-  // 以下是之前的过滤逻辑(已禁用)
-  // const uiControlKeywords = [
-  //   '发送', '返回', '确定', '取消', '搜索', '设置',
-  //   '更多', '分享', '复制', '删除', '编辑',
-  //   'SEND', 'BACK', 'OK', 'CANCEL', 'SEARCH', 'SETTINGS'
-  // ];
-  // if (text.length <= 3 && uiControlKeywords.includes(text)) {
-  //   return true;
-  // }
-  // return false;
-}

+ 2 - 2
main-js/func/img-reg.js → main-js/func/image-center-location.js

@@ -1,6 +1,6 @@
 /**
- * 图像匹配功能(Node.js 实现)
- * 识别模板图片是否在截图中的位置
+ * 图像中心点定位功能(Node.js 实现)
+ * 在截图中查找模板图片的位置,返回中心点坐标
  * 
  * 直接调用 Python 的 OpenCV,通过内联 Python 代码实现
  */

+ 0 - 0
main-js/func/extract-chat-history.js → main-js/func/ocr-chat-history.js


+ 240 - 2
main-js/history.js

@@ -6,9 +6,9 @@ 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/img-reg.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/extract-chat-history.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);
@@ -107,6 +107,71 @@ export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
   }
 }
 
+// 图像区域定位:在完整截图中查找区域截图的位置,返回四个顶点坐标
+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 {
@@ -546,6 +611,10 @@ export function registerIpcHandlers() {
     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);
   });
@@ -817,6 +886,175 @@ export function registerIpcHandlers() {
       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 目录

+ 611 - 0
main-js/read-and-write.js

@@ -0,0 +1,611 @@
+/**
+ * 文件读写操作模块
+ * 负责文件系统的读写操作,包括聊天记录的保存和读取
+ */
+
+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 { 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';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 获取最新的聊天记录文件
+ * @param {string} absoluteWorkflowPath - 工作流文件夹的绝对路径
+ * @returns {Promise<Object|null>} 最新的文件信息,如果不存在则返回 null
+ */
+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;
+  }
+}
+
+/**
+ * 将相对路径转换为绝对路径
+ * @param {string} workflowFolderPath - 工作流文件夹路径(相对或绝对)
+ * @returns {string} 绝对路径
+ */
+function getAbsoluteWorkflowPath(workflowFolderPath) {
+  if (isAbsolute(workflowFolderPath)) {
+    return workflowFolderPath;
+  }
+  
+  if (workflowFolderPath.startsWith('static/processing/')) {
+    const folderName = workflowFolderPath.replace('static/processing/', '');
+    return join(__dirname, '..', 'static', 'processing', folderName);
+  } else if (workflowFolderPath.startsWith('static\\processing\\')) {
+    const folderName = workflowFolderPath.replace('static\\processing\\', '');
+    return join(__dirname, '..', 'static', 'processing', folderName);
+  } else {
+    return join(__dirname, '..', 'static', 'processing', workflowFolderPath);
+  }
+}
+
+/**
+ * 注册文件读写相关的 IPC handlers
+ */
+export function registerIpcHandlers() {
+  // 确保目录存在
+  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 };
+    }
+  });
+
+  // 保存聊天记录到 history 文件夹
+  ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => {
+    try {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(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 {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(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 {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(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 {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(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 {
+      const absoluteWorkflowPath = getAbsoluteWorkflowPath(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-chat-history', async (event, workflowFolderPath) => {
+    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 };
+    }
+  });
+
+  // 提取聊天记录(通过OCR)
+  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);
+  });
+
+  // OCR 聊天记录(向后兼容,重定向到 extract-chat-history)
+  ipcMain.handle('ocr-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => {
+    return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath);
+  });
+}
+
+/**
+ * 提取聊天记录(通过OCR)
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} friendAvatarPath - 好友头像路径
+ * @param {string} myAvatarPath - 我的头像路径
+ * @param {string} workflowFolderPath - 工作流文件夹路径(可选)
+ * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
+ */
+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) {
+      // 确保 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 };
+  }
+}
+
+/**
+ * 获取最后一条消息(带发送者信息)
+ * @param {string} ipPort - 设备 ID/IP:Port
+ * @param {string} friendAvatarPath - 好友头像路径
+ * @param {string} myAvatarPath - 我的头像路径
+ * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>}
+ */
+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 };
+  }
+}
+

+ 213 - 0
main-js/workflow.js

@@ -0,0 +1,213 @@
+/**
+ * 工作流管理模块
+ * 负责工作流的保存、删除、读取等操作
+ */
+
+import { ipcMain } from 'electron';
+import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 获取 static/processing 目录下的所有文件夹
+ * @returns {Promise<Array<{name: string, createdAt: Date}>>}
+ */
+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 [];
+  }
+}
+
+/**
+ * 读取 processing.json 文件
+ * @param {string} folderName - 工作流文件夹名称
+ * @returns {Promise<Object|null>} 解析后的 JSON 对象,失败返回 null
+ */
+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;
+  }
+}
+
+/**
+ * 保存工作流到 static/processing 目录
+ * @param {Object} workflowJson - 工作流 JSON 对象
+ * @param {Array} imagesData - 图片数据数组(包含 base64 和 name)
+ * @returns {Promise<{success: boolean, error?: string, folderName?: string, path?: string}>}
+ */
+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 };
+  }
+}
+
+/**
+ * 删除工作流文件夹
+ * @param {string} folderName - 工作流文件夹名称
+ * @returns {Promise<{success: boolean, error?: string, folderName?: string}>}
+ */
+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 };
+  }
+}
+
+/**
+ * 注册工作流管理相关的 IPC handlers
+ */
+export function registerIpcHandlers() {
+  // 获取工作流文件夹列表
+  ipcMain.handle('get-static-folders', async () => {
+    return await getStaticFolders();
+  });
+
+  // 读取 processing.json 文件
+  ipcMain.handle('read-processing-json', async (event, folderName) => {
+    return await readProcessingJson(folderName);
+  });
+
+  // 保存工作流
+  ipcMain.handle('save-workflow', async (event, workflowJson, imagesData) => {
+    return await saveWorkflow(workflowJson, imagesData);
+  });
+
+  // 删除工作流
+  ipcMain.handle('delete-workflow', async (event, folderName) => {
+    return await deleteWorkflow(folderName);
+  });
+}

+ 6 - 2
main.js

@@ -9,7 +9,9 @@ import { registerIpcHandlers as registerScreenshotHandlers } from './main-js/adb
 import { registerIpcHandlers as registerTouchEventHandlers } from './main-js/adb/touch-event.js';
 import { registerIpcHandlers as registerInputHandlers } from './main-js/adb/input.js';
 import { registerIpcHandlers as registerScrollHandlers } from './main-js/adb/scroll.js';
-import { registerIpcHandlers as registerHistoryHandlers } from './main-js/history.js';
+import { registerIpcHandlers as registerReadWriteHandlers } from './main-js/read-and-write.js';
+import { registerIpcHandlers as registerExecutePyHandlers } from './main-js/execute-py.js';
+import { registerIpcHandlers as registerWorkflowHandlers } from './main-js/workflow.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -29,7 +31,9 @@ app.whenReady().then(() => {
   registerTouchEventHandlers();
   registerInputHandlers();
   registerScrollHandlers();
-  registerHistoryHandlers();
+  registerReadWriteHandlers();
+  registerExecutePyHandlers();
+  registerWorkflowHandlers();
 
   app.on('activate', () => {
     if (BrowserWindow.getAllWindows().length === 0) {

+ 12 - 0
preload.cjs

@@ -24,6 +24,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
   getStaticFolders: () => ipcRenderer.invoke('get-static-folders'),
   // 图像匹配并获取坐标
   matchImageAndGetCoordinate: (ipPort, templateImagePath) => ipcRenderer.invoke('match-image-and-get-coordinate', ipPort, templateImagePath),
+  // 图像区域定位:在完整截图中查找区域截图位置,返回四个顶点坐标
+  matchImageRegionLocation: (screenshotPath, regionPath, device) => ipcRenderer.invoke('match-image-region-location', screenshotPath, regionPath, device),
   // 读取 processing.json 文件
   readProcessingJson: (folderName) => ipcRenderer.invoke('read-processing-json', folderName),
   // 文字识别并获取坐标
@@ -32,6 +34,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
   ocrLastMessage: (ipPort, method, avatarPath, area, folderPath) => ipcRenderer.invoke('ocr-last-message', ipPort, method, avatarPath, area, folderPath),
   // 提取聊天记录
   extractChatHistory: (ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => ipcRenderer.invoke('extract-chat-history', ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath),
+  // OCR 聊天记录(向后兼容,重定向到 extract-chat-history)
+  ocrChatHistory: (ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => ipcRenderer.invoke('ocr-chat-history', ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath),
   // 获取最后一条消息(带发送者信息)
   getLastChatMessage: (ipPort, friendAvatarPath, myAvatarPath) => ipcRenderer.invoke('get-last-chat-message', ipPort, friendAvatarPath, myAvatarPath),
   // 保存工作流(支持图片)
@@ -50,5 +54,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
   saveChatHistorySummary: (workflowFolderPath, summary) => ipcRenderer.invoke('save-chat-history-summary', workflowFolderPath, summary),
   // 获取聊天记录总结
   getChatHistorySummary: (workflowFolderPath) => ipcRenderer.invoke('get-chat-history-summary', workflowFolderPath),
+  // 读取最新聊天记录的所有消息
+  readLatestChatHistory: (workflowFolderPath) => ipcRenderer.invoke('read-latest-chat-history', workflowFolderPath),
+  // 读取最新聊天记录的最后一条消息
+  readLastMessage: (workflowFolderPath) => ipcRenderer.invoke('read-last-message', workflowFolderPath),
+  // 读取聊天记录(合并所有历史文件)
+  readChatHistory: (workflowFolderPath) => ipcRenderer.invoke('read-chat-history', workflowFolderPath),
+  // 向后兼容
+  readAllChatHistory: (workflowFolderPath) => ipcRenderer.invoke('read-chat-history', workflowFolderPath),
 });
 

+ 1 - 1
src/pages/Chat/Input/Input.js

@@ -132,7 +132,7 @@ const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手
 在生成工作流时:
 - 逻辑结构尽量用 for/while/if + triggers(schedules) 表达
 - 具体手机操作尽量落到 press/swipe/scroll
-- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:extract-chat-history、string-reg-location、save-chat-history
+- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:ocr-chat-history、string-reg-location、save-new-chat
 
 (实现层说明:项目内部为兼容与落地,仍存在 locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。)
 

+ 1 - 1
src/pages/Chat/Input/input-hooks.jsx

@@ -132,7 +132,7 @@ const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手
 在生成工作流时:
 - 逻辑结构尽量�?for/while/if + triggers(schedules) 表达
 - 具体手机操作尽量落到 press/swipe/scroll
-- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:extract-chat-history、string-reg-location、save-chat-history�?
+- 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:ocr-chat-history、string-reg-location、save-new-chat�?
 
 (实现层说明:项目内部为兼容与落地,仍存�?locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。)
 

+ 0 - 158
src/pages/Processing/Func/adb-extract-chat-history.js

@@ -1,158 +0,0 @@
-/**
- * Func 标签:adb-extract-chat-history
- * 
- * 使用 ADB uiautomator dump 获取 UI 层次结构 XML,然后解析 XML 提取聊天记录文本
- * 相比 OCR 方式,这种方法更快、更准确,且无需 OCR 库
- * 
- * 注意:由于 ADB 命令需要在主进程中执行,此文件提供渲染进程接口
- * 实际实现逻辑需要在主进程中添加(main-js/history.js 和 main-js/func/adb-extract-chat-history.js)
- */
-
-export const tagName = 'adb-extract-chat-history';
-
-export const schema = {
-  description: '使用 ADB XML 解析方式提取屏幕上的聊天记录文本(替代 OCR 方式)',
-  inputs: {
-    device: '设备 ID/IP:Port',
-    method: '提取方法(full-screen:全屏提取,last-message:仅最后一条消息)',
-    variable: '输出变量名(保存提取的文本)',
-  },
-  outputs: {
-    variable: '提取的聊天文本(字符串)',
-  },
-};
-
-/**
- * 使用 ADB XML 解析方式获取屏幕文本
- * 
- * 实现原理:
- * 1. 使用 `adb shell uiautomator dump` 获取当前屏幕的 UI 层次结构 XML
- * 2. 解析 XML,提取所有节点的 text 属性
- * 3. 过滤并合并文本内容
- * 4. 返回提取的文本
- * 
- * 优势:
- * - 速度更快(无需 OCR 识别,通常 < 1 秒 vs OCR 2-5 秒)
- * - 准确度更高(直接获取 UI 文本属性,无识别错误)
- * - 无需 OCR 库(纯 Node.js 实现)
- * - 资源消耗更低(无需截图和 OCR 计算)
- * 
- * 挑战:
- * - 微信 UI 结构可能比较复杂,需要找到正确的节点
- * - 可能需要过滤 UI 控件文本(按钮、标签等)
- * - 不同版本微信的 UI 结构可能不同
- * 
- * @param {Object} params - 参数对象
- * @param {string} params.device - 设备 ID/IP:Port
- * @param {string} params.method - 提取方法('full-screen' 或 'last-message')
- * @returns {Promise<{success: boolean, text?: string, error?: string}>}
- */
-export async function executeAdbExtractChatHistory({ device, method = 'full-screen' }) {
-  try {
-    // 检查 electronAPI 是否可用
-    if (!window.electronAPI) {
-      return { 
-        success: false, 
-        error: 'electronAPI 不可用' 
-      };
-    }
-
-    // 注意:需要在主进程中添加 'adb-extract-chat-history' IPC handler
-    // 实际实现应该在 main-js/func/adb-extract-chat-history.js 中
-    // 这里提供接口调用示例
-    
-    // 如果主进程已经实现了 IPC handler,可以这样调用:
-    // const result = await window.electronAPI.adbExtractChatHistory(device, method);
-    
-    // 目前返回提示信息,需要实现主进程逻辑
-    return {
-      success: false,
-      error: 'ADB XML 解析功能需要在主进程中实现。请实现 main-js/func/adb-extract-chat-history.js 和 main-js/history.js 中的相关代码。'
-    };
-  } catch (error) {
-    console.error('执行 adb-extract-chat-history 失败:', error);
-    return { 
-      success: false, 
-      error: error.message || 'ADB XML 解析失败' 
-    };
-  }
-}
-
-/**
- * 实现方案说明(需要在主进程中实现):
- * 
- * 1. 在 main-js/func/adb-extract-chat-history.js 中实现核心逻辑:
- *    - 使用 `adb shell uiautomator dump /sdcard/ui_dump.xml` 获取 UI XML
- *    - 使用 `adb shell cat /sdcard/ui_dump.xml` 读取 XML 内容(或直接输出到 stdout)
- *    - 解析 XML,提取所有节点的 text 属性
- *    - 过滤空文本和 UI 控件文本
- *    - 返回提取的文本
- * 
- * 2. 在 main-js/history.js 中添加 IPC handler:
- *    - 添加 `adbExtractChatHistory` 函数
- *    - 在 `registerIpcHandlers` 中注册 'adb-extract-chat-history' handler
- * 
- * 3. 在 preload.cjs 中添加 IPC 接口:
- *    - 添加 `adbExtractChatHistory: (ipPort, method) => ipcRenderer.invoke('adb-extract-chat-history', ipPort, method)`
- * 
- * 4. XML 解析可以使用:
- *    - Node.js 内置方式(正则表达式,适用于简单 XML)
- *    - 或者安装 XML 解析库(如 fast-xml-parser、xmldom)
- * 
- * 5. 示例实现代码(main-js/func/adb-extract-chat-history.js):
- * 
- *    import { exec } from 'child_process';
- *    import { promisify } from 'util';
- *    import { getCachedAdbPath } from '../config.js';
- *    
- *    const execAsync = promisify(exec);
- *    
- *    export async function extractChatHistoryByAdbXml(ipPort, method = 'full-screen') {
- *      try {
- *        const adbPath = getCachedAdbPath();
- *        
- *        // 1. 获取 UI dump(可以直接输出到 stdout,避免 pull)
- *        const dumpCommand = `${adbPath} -s ${ipPort} shell uiautomator dump /dev/tty`;
- *        // 或者:${adbPath} -s ${ipPort} shell uiautomator dump /sdcard/ui_dump.xml
- *        // 然后:${adbPath} -s ${ipPort} shell cat /sdcard/ui_dump.xml
- *        
- *        const { stdout } = await execAsync(dumpCommand, {
- *          timeout: 10000,
- *          maxBuffer: 10 * 1024 * 1024,
- *          encoding: 'utf8'
- *        });
- *        
- *        // 2. 解析 XML(使用正则表达式提取 text 属性,或使用 XML 解析库)
- *        const texts = [];
- *        const textRegex = /text="([^"]*)"/g;
- *        let match;
- *        while ((match = textRegex.exec(stdout)) !== null) {
- *          const text = match[1];
- *          if (text && text.trim() && !isUIControlText(text)) {
- *            texts.push(text.trim());
- *          }
- *        }
- *        
- *        // 3. 根据 method 返回结果
- *        if (method === 'last-message') {
- *          return {
- *            success: true,
- *            text: texts[texts.length - 1] || ''
- *          };
- *        } else {
- *          return {
- *            success: true,
- *            text: texts.join('\n')
- *          };
- *        }
- *      } catch (error) {
- *        return { success: false, error: error.message };
- *      }
- *    }
- *    
- *    function isUIControlText(text) {
- *      // 过滤 UI 控件文本(按钮、标签等)
- *      const uiControlKeywords = ['发送', '返回', '确定', '取消', '搜索', '设置'];
- *      return uiControlKeywords.some(keyword => text === keyword);
- *    }
- */

+ 33 - 1
src/pages/Processing/Func/chat-history.js → src/pages/Processing/Func/chat/chat-history.js

@@ -1,6 +1,6 @@
 /**
  * 聊天历史记录管理模块
- * 负责保存聊天记录、生成AI总结、读取历史总结
+ * 负责保存聊天记录、生成AI总结、读取历史总结、读取所有聊天记录
  */
 
 /**
@@ -158,3 +158,35 @@ function parseChatHistoryText(chatHistoryText) {
 
   return messages;
 }
+
+/**
+ * 读取所有聊天记录(合并所有历史文件)
+ * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
+ * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
+ */
+export async function readAllChatHistory(folderPath) {
+  try {
+    // 优先使用新的 API,如果没有则使用旧的(向后兼容)
+    const api = window.electronAPI?.readChatHistory || window.electronAPI?.readAllChatHistory;
+    if (!api) {
+      return { success: false, error: '读取聊天记录 API 不可用' };
+    }
+
+    const result = await api(folderPath);
+
+    if (!result.success) {
+      return { success: false, error: result.error };
+    }
+
+    console.log(`已读取聊天记录,共 ${result.fileCount || 0} 个文件,${result.totalMessages || 0} 条消息`);
+    return { 
+      success: true, 
+      messages: result.messages || [],
+      fileCount: result.fileCount || 0,
+      totalMessages: result.totalMessages || 0
+    };
+  } catch (error) {
+    console.error('读取聊天记录失败:', error);
+    return { success: false, error: error.message || '读取聊天记录失败' };
+  }
+}

+ 6 - 6
src/pages/Processing/Func/extract-chat-history.js → src/pages/Processing/Func/chat/ocr-chat-history.js

@@ -1,13 +1,13 @@
 /**
- * Func 标签:extract-chat-history
+ * Func 标签:ocr-chat-history
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
  *
- * 运行时真实执行逻辑由 ActionParser + electronAPI.extractChatHistory + main-js/func/extract-chat-history.js 实现。
+ * 运行时真实执行逻辑由 ActionParser + electronAPI.extractChatHistory + main-js/func/ocr-chat-history.js 实现。
  */
 
-export const tagName = 'extract-chat-history';
+export const tagName = 'ocr-chat-history';
 
 export const schema = {
   description: '从屏幕截图中提取会话/消息记录(包含发送者),并格式化保存到变量,供后续 AI 或逻辑判断使用。',
@@ -23,7 +23,7 @@ export const schema = {
 };
 
 /**
- * 执行 extract-chat-history 功能
+ * 执行 ocr-chat-history 功能
  * 这个函数会被 ActionParser 调用
  * 
  * @param {Object} params - 参数对象
@@ -33,7 +33,7 @@ export const schema = {
  * @param {string} params.folderPath - 工作流文件夹路径
  * @returns {Promise<{success: boolean, messagesText?: string, error?: string}>}
  */
-export async function executeExtractChatHistory({ device, avatar1, avatar2, folderPath }) {
+export async function executeOcrChatHistory({ device, avatar1, avatar2, folderPath }) {
   try {
     if (!window.electronAPI || !window.electronAPI.extractChatHistory) {
       return { 
@@ -73,7 +73,7 @@ export async function executeExtractChatHistory({ device, avatar1, avatar2, fold
       messages: result.messages || []
     };
   } catch (error) {
-    console.error('执行 extract-chat-history 失败:', error);
+    console.error('执行 ocr-chat-history 失败:', error);
     return { 
       success: false, 
       error: error.message || '提取聊天记录失败' 

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

@@ -0,0 +1,51 @@
+/**
+ * 读取聊天记录
+ * 从工作流的 history 文件夹中读取所有聊天记录文件,合并所有消息
+ */
+
+export const tagName = 'read-chat-history';
+
+export const schema = {
+  description: '读取聊天记录的所有消息(合并所有历史文件)。',
+  inputs: {
+    variable: '输出变量名(保存聊天记录数组)',
+  },
+  outputs: {
+    variable: '包含所有消息的数组,每个消息包含 sender 和 text 字段',
+  },
+};
+
+/**
+ * 执行读取聊天记录
+ * @param {Object} params - 参数对象
+ * @param {string} params.folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
+ * @param {string} params.variable - 输出变量名
+ * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
+ */
+export async function executeReadChatHistory({ folderPath, variable }) {
+  try {
+    if (!variable) {
+      return { success: false, error: 'read-chat-history 缺少 variable 参数' };
+    }
+
+    // 导入 readAllChatHistory 函数
+    const { readAllChatHistory } = await import('./chat-history.js');
+
+    const result = await readAllChatHistory(folderPath);
+
+    if (!result.success) {
+      return { success: false, error: result.error };
+    }
+
+    // 返回消息数组
+    return { 
+      success: true, 
+      messages: result.messages || [],
+      fileCount: result.fileCount || 0,
+      totalMessages: result.totalMessages || 0
+    };
+  } catch (error) {
+    console.error('执行 read-chat-history 失败:', error);
+    return { success: false, error: error.message || '读取聊天记录失败' };
+  }
+}

+ 110 - 0
src/pages/Processing/Func/chat/read-last-message.js

@@ -0,0 +1,110 @@
+/**
+ * 读取最后一条消息
+ * 支持两种模式:
+ * 1. 从变量读取(如果提供了 inputData)- 从 chatHistory 文本或消息数组中提取最后一条消息
+ * 2. 从 history 文件夹读取(如果没有提供 inputData)- 从最新的聊天记录文件中读取
+ */
+
+export const tagName = 'read-last-message';
+
+export const schema = {
+  description: '读取最后一条消息,包括消息内容和发送者角色。支持从变量或 history 文件夹读取。',
+  inputs: {
+    inputData: '输入数据(可选)- 可以是聊天记录文本或消息数组,如果不提供则从 history 文件夹读取',
+    textVariable: '输出变量名(保存消息文本)',
+    senderVariable: '输出变量名(保存发送者角色:friend 或 me)',
+  },
+  outputs: {
+    textVariable: '最后一条消息的文本内容',
+    senderVariable: '最后一条消息的发送者角色(friend 或 me)',
+  },
+};
+
+/**
+ * 解析聊天记录文本为结构化数据
+ * @param {string} chatHistoryText - 聊天记录文本(格式:好友: xxx\n我: xxx)
+ * @returns {Array<{sender: string, text: string}>}
+ */
+function parseChatHistoryText(chatHistoryText) {
+  const messages = [];
+  const lines = chatHistoryText.split('\n').filter(line => line.trim());
+
+  for (const line of lines) {
+    // 匹配格式:对方: xxx、好友: xxx 或 我: xxx
+    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 });
+    }
+  }
+
+  return messages;
+}
+
+/**
+ * 执行读取最后一条消息
+ * @param {Object} params - 参数对象
+ * @param {string} params.folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
+ * @param {string|Array} params.inputData - 输入数据(可选)- 聊天记录文本或消息数组
+ * @param {string} params.textVariable - 输出变量名(保存消息文本)
+ * @param {string} params.senderVariable - 输出变量名(保存发送者角色)
+ * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>}
+ */
+export async function executeReadLastMessage({ folderPath, inputData, textVariable, senderVariable }) {
+  try {
+    if (!textVariable && !senderVariable) {
+      return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' };
+    }
+
+    let messages = [];
+    let lastMessage = null;
+
+    // 如果提供了 inputData,从变量读取
+    if (inputData !== undefined && inputData !== null) {
+      if (typeof inputData === 'string') {
+        // 如果是字符串,解析为消息数组
+        messages = parseChatHistoryText(inputData);
+      } else if (Array.isArray(inputData)) {
+        // 如果已经是数组,直接使用
+        messages = inputData;
+      } else {
+        return { success: false, error: 'inputData 必须是字符串或消息数组' };
+      }
+
+      if (messages.length === 0) {
+        return { success: false, error: '输入数据中没有消息' };
+      }
+
+      // 获取最后一条消息
+      lastMessage = messages[messages.length - 1];
+    } else {
+      // 如果没有提供 inputData,从 history 文件夹读取
+      if (!window.electronAPI || !window.electronAPI.readLastMessage) {
+        return { success: false, error: '读取最后一条消息 API 不可用' };
+      }
+
+      const result = await window.electronAPI.readLastMessage(folderPath);
+
+      if (!result.success) {
+        return { success: false, error: `读取最后一条消息失败: ${result.error}` };
+      }
+
+      lastMessage = {
+        text: result.text || '',
+        sender: result.sender || ''
+      };
+    }
+
+    // 返回最后一条消息的文本和发送者
+    return { 
+      success: true, 
+      text: lastMessage.text || '', 
+      sender: lastMessage.sender || '' 
+    };
+  } catch (error) {
+    console.error('执行 read-last-message 失败:', error);
+    return { success: false, error: error.message || '读取最后一条消息失败' };
+  }
+}

+ 3 - 3
src/pages/Processing/Func/save-chat-history.js → src/pages/Processing/Func/chat/save-new-chat.js

@@ -1,5 +1,5 @@
 /**
- * Func 标签:save-chat-history
+ * Func 标签:save-new-chat
  *
  * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
@@ -8,12 +8,12 @@
  * 当前项目里对应能力主要由 chat-history.js + IPC(save-chat-history)实现承载。
  */
 
-export const tagName = 'save-chat-history';
+export const tagName = 'save-new-chat';
 
 export const schema = {
   description: '将消息记录/聊天记录保存到工作流目录的 history 文件夹,按时间戳落盘(JSON)。',
   inputs: {
-    variable: '要保存的变量名(一般是 extract-chat-history 的输出变量)',
+    variable: '要保存的变量名(一般是 ocr-chat-history 的输出变量)',
   },
   outputs: {
     filePath: '保存后的文件路径(可选)',

+ 77 - 0
src/pages/Processing/Func/image-center-location.js

@@ -0,0 +1,77 @@
+/**
+ * Func 标签:image-center-location
+ * 
+ * 图像匹配功能:识别模板图片是否在截图中的位置,返回中心点坐标
+ * 注意:实际的 OpenCV 处理需要在 Node.js 主进程中实现
+ */
+
+export const tagName = 'image-center-location';
+
+export const schema = {
+  description: '在屏幕截图中查找模板图片的位置并返回中心点坐标(可用于定位/点击)。',
+  inputs: {
+    template: '模板图片路径(相对于工作流目录)',
+    variable: '输出变量名(保存中心点坐标)',
+  },
+  outputs: {
+    variable: '中心点坐标 {x, y}',
+  },
+};
+
+/**
+ * 执行 image-center-location 功能
+ * 这个函数会被 ActionParser 调用
+ * 
+ * @param {Object} params - 参数对象
+ * @param {string} params.device - 设备 ID/IP:Port(必需,用于获取截图)
+ * @param {string} params.template - 模板图片路径
+ * @param {string} params.folderPath - 工作流文件夹路径
+ * @returns {Promise<{success: boolean, center?: Object, error?: string}>}
+ */
+export async function executeImageCenterLocation({ device, template, folderPath }) {
+  try {
+    if (!window.electronAPI || !window.electronAPI.matchImageAndGetCoordinate) {
+      return { 
+        success: false, 
+        error: 'matchImageAndGetCoordinate API 不可用' 
+      };
+    }
+
+    if (!device) {
+      return { 
+        success: false, 
+        error: '缺少设备 ID,无法自动获取截图' 
+      };
+    }
+
+    // 构建模板图片完整路径(如果路径不是绝对路径,则相对于工作流目录)
+    const templatePath = template.startsWith('/') || template.includes(':') 
+      ? template 
+      : `${folderPath}/${template}`;
+
+    // 调用主进程的图像匹配函数(会自动获取设备截图)
+    const result = await window.electronAPI.matchImageAndGetCoordinate(
+      device,
+      templatePath
+    );
+
+    if (!result.success) {
+      return { success: false, error: result.error };
+    }
+
+    // 返回中心点坐标
+    const center = result.clickPosition || { x: result.coordinate.x + result.coordinate.width / 2, y: result.coordinate.y + result.coordinate.height / 2 };
+
+    return {
+      success: true,
+      center: center,
+      coordinate: result.coordinate // 同时返回完整坐标信息
+    };
+  } catch (error) {
+    console.error('执行 image-center-location 失败:', error);
+    return { 
+      success: false, 
+      error: error.message || '图像中心点定位失败' 
+    };
+  }
+}

+ 93 - 0
src/pages/Processing/Func/image-region-location.js

@@ -0,0 +1,93 @@
+/**
+ * Func 标签:image-region-location
+ *
+ * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
+ * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
+ *
+ * 语义:通过图像匹配找到区域截图在完整截图中的位置,返回区域的四个顶点坐标。
+ * 当前项目里对应能力主要由 electronAPI.matchImageRegionLocation + main-js/func/image-center-location.js 实现承载。
+ */
+
+export const tagName = 'image-region-location';
+
+export const schema = {
+  description: '在完整截图中查找区域截图的位置,返回区域的四个顶点坐标(左上、右上、左下、右下)。',
+  inputs: {
+    screenshot: '完整截图路径(相对于工作流目录)',
+    region: '区域截图路径(相对于工作流目录)',
+    variable: '输出变量名(保存四个顶点坐标)',
+  },
+  outputs: {
+    variable: '四个顶点坐标对象 {topLeft: {x, y}, topRight: {x, y}, bottomLeft: {x, y}, bottomRight: {x, y}}',
+  },
+};
+
+/**
+ * 执行 image-region-location 功能
+ * 这个函数会被 ActionParser 调用
+ * 
+ * @param {Object} params - 参数对象
+ * @param {string} params.device - 设备 ID/IP:Port(可选,用于获取分辨率)
+ * @param {string} params.screenshot - 完整截图路径
+ * @param {string} params.region - 区域截图路径
+ * @param {string} params.folderPath - 工作流文件夹路径
+ * @returns {Promise<{success: boolean, corners?: Object, error?: string}>}
+ */
+export async function executeImageRegionLocation({ device, screenshot, region, folderPath }) {
+  try {
+    if (!window.electronAPI || !window.electronAPI.matchImageRegionLocation) {
+      return { 
+        success: false, 
+        error: 'matchImageRegionLocation API 不可用' 
+      };
+    }
+
+    // 如果 screenshot 为 null,使用特殊标记让主进程自动获取截图
+    let screenshotPath = screenshot;
+    if (screenshot === null) {
+      screenshotPath = '__AUTO_SCREENSHOT__';
+    } else if (screenshot) {
+      // 构建完整路径(如果路径不是绝对路径,则相对于工作流目录)
+      screenshotPath = screenshot.startsWith('/') || screenshot.includes(':') 
+        ? screenshot 
+        : `${folderPath}/${screenshot}`;
+    }
+    
+    const regionPath = region.startsWith('/') || region.includes(':') 
+      ? region 
+      : `${folderPath}/${region}`;
+
+    // 调用主进程的图像区域定位函数
+    // 如果 screenshotPath 是 '__AUTO_SCREENSHOT__',主进程会自动获取截图
+    const result = await window.electronAPI.matchImageRegionLocation(
+      screenshotPath,
+      regionPath,
+      device // 可选,用于获取设备分辨率进行缩放或自动获取截图
+    );
+
+    if (!result.success) {
+      return { success: false, error: result.error };
+    }
+
+    // 计算四个顶点坐标
+    const { x, y, width, height } = result;
+    const corners = {
+      topLeft: { x, y },
+      topRight: { x: x + width, y },
+      bottomLeft: { x, y: y + height },
+      bottomRight: { x: x + width, y: y + height }
+    };
+
+    return {
+      success: true,
+      corners: corners,
+      bounds: { x, y, width, height } // 同时返回边界框信息
+    };
+  } catch (error) {
+    console.error('执行 image-region-location 失败:', error);
+    return { 
+      success: false, 
+      error: error.message || '图像区域定位失败' 
+    };
+  }
+}

+ 0 - 19
src/pages/Processing/Func/img-reg.js

@@ -1,19 +0,0 @@
-/**
- * Func 标签:img-reg
- * 
- * 图像匹配功能:识别模板图片是否在截图中的位置
- * 注意:实际的 OpenCV 处理需要在 Node.js 主进程中实现
- */
-
-export const tagName = 'img-reg';
-
-export const schema = {
-  description: '在屏幕截图中查找模板图片的位置并返回坐标(可用于定位/点击)。',
-  inputs: {
-    template: '模板图片路径(相对于工作流目录)',
-    variable: '输出变量名(保存坐标)',
-  },
-  outputs: {
-    variable: '坐标({x, y, width, height})',
-  },
-};

ファイルの差分が大きいため隠しています
+ 919 - 49
src/pages/Processing/action-parser.js


+ 3 - 0
static/processing/微信聊天自动发送工作流/discription.md

@@ -0,0 +1,3 @@
+背景:微信和一个好友的对话输入框,并已点击了输入框召唤出了键盘。
+
+业务需求:要求调用 AI 自动和用户聊天。

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

@@ -1,88 +1,100 @@
 {
-	"version": "1.0",
-	"name": "微信聊天自动发送工作流",
-	"triggers": [],
 	"variables": {
-		"lastMessage": "",
+		"chatHistory": "",
 		"currentMessage": "",
-		"lastSender": "",
-		"reply": "",
-		"sendPos": "",
-		"chatHistory": ""
+		"chat-history":"",
+		"lastMessage": "",
+		"lastRole": "",
+		"aiReply": "",
+		"aiCallBack":0,
+		"sendBtnPos": null,
 	},
-	"actions": [
+	"execute": [
 		{
-			"type": "while",
-			"condition": "true",
-			"body": [
+			"type": "schedule",
+			"condition": 
+			{
+				"interval": "1s",
+				"repeat": -1
+			},
+			"interval": 
+			[
+				{
+					"type": "ocr-chat-history",
+					"inVars": ["好友头像.png","我的头像.png"],
+					"outVars": ["{chatHistory}"],
+
+				},
+				{
+					"type": "read-last-message",
+					"inVars": ["{chatHistory}"],
+					"outVars": ["{currentMessage}"],
+				},
+				{
+					"type": "read-chat-history",
+					"inVars": [],
+					"outVars": ["{chat-history}"],
+				},
 				{
-					"type": "extract-chat-history",
-					"avatar1": "好友头像.png",
-					"avatar2": "我的头像.png",
-					"variable": "chatHistory",
-					"lastMessageVariable": "currentMessage",
-					"senderVariable": "lastSender",
-					"delay": "1s"
+					"type": "read-last-message",
+					"inputVars": ["{chat-history}"],
+					"outputVars": ["{lastMessage}","{lastRole}"],
 				},
 				{
 					"type": "if",
 					"condition": "{currentMessage} != {lastMessage}",
-					"then": [
+					"ture": 
+					[
 						{
-							"type": "save-chat-history",
-							"variable": "chatHistory",
-							"delay": "0.5s"
+							"type": "save-new-chat",
+							"inVars": ["{chatHistory}"],
+							"outVars": [],
 						},
 						{
 							"type": "if",
-							"condition": "{lastSender} == \"friend\"",
-							"then": [
+							"condition": "{lastRole} == \"friend\"",
+							"ture": 
+							[
 								{
 									"type": "ai-generate",
 									"prompt": "你是一个刚认识女方不久的男生,正在和女方通过微信聊天。你的目标是:通过自然、有趣的对话让女方对你产生好感,建立良好的第一印象。\n\n女方最新发来的消息:\"{currentMessage}\"\n\n请根据以上信息,代入我的角色,生成一条自然、有趣的回复。要求:\n1. 完全代入刚认识女方不久的男生角色,用轻松、有趣、略带暧昧的口吻回复;\n2. 回复要自然流畅,像真实聊天一样,不要过于刻意或油腻;\n3. 可以适当展现幽默感、关心或好奇心,让对话更有趣;\n4. 如果识别的内容不清楚或包含无关文字,请根据核心意思理解并回复;\n5. 回复长度控制在20字以内;\n6. 目标是让女方对你产生好感,但不要过于直接或冒进。\n\n请严格按照以下JSON格式返回,不要包含任何其他文字:\n{\n  \"reply\": \"你的回复内容\"\n}",
-									"variable": "reply",
-									"delay": "5s"
+									"inVars": ["{currentMessage}","{aiCallBack}"],
+									"outVars": ["{aiReply}"]	
 								},
 								{
-									"type": "input",
-									"target": "输入框",
-									"value": "{reply}",
-									"delay": "0.5s"
+									"type": "while",
+									"condition": "1 == aiCallBack",
+									"ture": 
+									[
+										
+									]
+								},
+								{
+									"type": "adb",
+									"method": "input",
+									"inVars": ["{aiReply}"]
 								},
 								{
 									"type": "if",
-									"condition": "{sendPos} == \"\" || {sendPos} == undefined",
-									"then": [
+									"condition": "{sendBtnPos} == null",
+									"ture": [
 										{
-											"type": "locate",
-											"method": "image",
-											"target": "微信聊天界面的发送按钮定位图.png",
-											"variable": "sendPos",
-											"delay": "0.5s"
+											"type": "image-center-location",
+											"inVars": ["微信聊天界面的发送按钮定位图.png"],
+											"outVars": ["{sendBtnPos}"]
 										}
 									]
 								},
 								{
-									"type": "click",
-									"method": "position",
-									"target": "{sendPos}",
-									"delay": "0.5s"
-								}
+									"type": "adb",
+									"method": "click",
+									"inVars": ["{sendBtnPos}"]
+								}		
 							]
-						},
-						{
-							"type": "set",
-							"variable": "lastMessage",
-							"value": "{currentMessage}",
-							"delay": "0.5s"
 						}
 					]
-				},
-				{
-					"type": "delay",
-					"value": "5s"
 				}
-			]
-		}
+			]	
+		},		
 	]
 }

BIN
static/processing/微信聊天自动发送工作流/好友头像.png


BIN
temp_screenshot.png


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません