Przeglądaj źródła

聊天功能完美

yichael 5 miesięcy temu
rodzic
commit
583ff96241
100 zmienionych plików z 1601 dodań i 298 usunięć
  1. 45 3
      BAT-TOOL/0-ENABLE-TCPIP.BAT
  2. 303 0
      document/工作流语言使用说明.md
  3. 1 1
      index.html
  4. 422 0
      main-js/func/extract-chat-history.js
  5. 213 0
      main-js/func/img-reg.js
  6. 39 0
      main-js/func/string-reg-location.js
  7. 546 160
      main-js/history.js
  8. 2 2
      main-js/security.js
  9. 8 8
      node_modules/.vite/deps/_metadata.json
  10. 22 0
      preload.cjs
  11. BIN
      py/extract-chat-history.py
  12. BIN
      py/img-reg.py
  13. BIN
      py/string-reg-location.py
  14. 0 124
      py/test-to-img.py
  15. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-312.pyc
  16. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/Image.cpython-312.pyc
  17. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-312.pyc
  18. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-312.pyc
  19. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-312.pyc
  20. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-312.pyc
  21. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/ImageText.cpython-312.pyc
  22. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-312.pyc
  23. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-312.pyc
  24. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-312.pyc
  25. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-312.pyc
  26. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-312.pyc
  27. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/_util.cpython-312.pyc
  28. BIN
      py/venv/Lib/site-packages/PIL/__pycache__/_version.cpython-312.pyc
  29. BIN
      py/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc
  30. BIN
      py/venv/Lib/site-packages/cv2/__pycache__/__init__.cpython-312.pyc
  31. BIN
      py/venv/Lib/site-packages/cv2/__pycache__/load_config_py3.cpython-312.pyc
  32. BIN
      py/venv/Lib/site-packages/cv2/__pycache__/version.cpython-312.pyc
  33. BIN
      py/venv/Lib/site-packages/cv2/data/__pycache__/__init__.cpython-312.pyc
  34. BIN
      py/venv/Lib/site-packages/cv2/gapi/__pycache__/__init__.cpython-312.pyc
  35. BIN
      py/venv/Lib/site-packages/cv2/mat_wrapper/__pycache__/__init__.cpython-312.pyc
  36. BIN
      py/venv/Lib/site-packages/cv2/misc/__pycache__/__init__.cpython-312.pyc
  37. BIN
      py/venv/Lib/site-packages/cv2/misc/__pycache__/version.cpython-312.pyc
  38. BIN
      py/venv/Lib/site-packages/cv2/typing/__pycache__/__init__.cpython-312.pyc
  39. BIN
      py/venv/Lib/site-packages/cv2/utils/__pycache__/__init__.cpython-312.pyc
  40. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/__config__.cpython-312.pyc
  41. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/__init__.cpython-312.pyc
  42. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/_array_api_info.cpython-312.pyc
  43. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/_distributor_init.cpython-312.pyc
  44. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/_expired_attrs_2_0.cpython-312.pyc
  45. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/_globals.cpython-312.pyc
  46. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/_pytesttester.cpython-312.pyc
  47. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/dtypes.cpython-312.pyc
  48. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/exceptions.cpython-312.pyc
  49. BIN
      py/venv/Lib/site-packages/numpy/__pycache__/version.cpython-312.pyc
  50. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/__init__.cpython-312.pyc
  51. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_add_newdocs.cpython-312.pyc
  52. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_add_newdocs_scalars.cpython-312.pyc
  53. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_asarray.cpython-312.pyc
  54. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_dtype.cpython-312.pyc
  55. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_dtype_ctypes.cpython-312.pyc
  56. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_exceptions.cpython-312.pyc
  57. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_internal.cpython-312.pyc
  58. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_machar.cpython-312.pyc
  59. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_methods.cpython-312.pyc
  60. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_string_helpers.cpython-312.pyc
  61. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_type_aliases.cpython-312.pyc
  62. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/_ufunc_config.cpython-312.pyc
  63. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/arrayprint.cpython-312.pyc
  64. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/einsumfunc.cpython-312.pyc
  65. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/fromnumeric.cpython-312.pyc
  66. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/function_base.cpython-312.pyc
  67. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/getlimits.cpython-312.pyc
  68. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/memmap.cpython-312.pyc
  69. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/multiarray.cpython-312.pyc
  70. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/numeric.cpython-312.pyc
  71. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/numerictypes.cpython-312.pyc
  72. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/overrides.cpython-312.pyc
  73. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/printoptions.cpython-312.pyc
  74. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/records.cpython-312.pyc
  75. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/shape_base.cpython-312.pyc
  76. BIN
      py/venv/Lib/site-packages/numpy/_core/__pycache__/umath.cpython-312.pyc
  77. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/__init__.cpython-312.pyc
  78. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_array_like.cpython-312.pyc
  79. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_char_codes.cpython-312.pyc
  80. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_dtype_like.cpython-312.pyc
  81. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nbit.cpython-312.pyc
  82. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nbit_base.cpython-312.pyc
  83. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nested_sequence.cpython-312.pyc
  84. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_scalars.cpython-312.pyc
  85. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_shape.cpython-312.pyc
  86. BIN
      py/venv/Lib/site-packages/numpy/_typing/__pycache__/_ufunc.cpython-312.pyc
  87. BIN
      py/venv/Lib/site-packages/numpy/_utils/__pycache__/__init__.cpython-312.pyc
  88. BIN
      py/venv/Lib/site-packages/numpy/_utils/__pycache__/_convertions.cpython-312.pyc
  89. BIN
      py/venv/Lib/site-packages/numpy/_utils/__pycache__/_inspect.cpython-312.pyc
  90. BIN
      py/venv/Lib/site-packages/numpy/core/__pycache__/__init__.cpython-312.pyc
  91. BIN
      py/venv/Lib/site-packages/numpy/core/__pycache__/_utils.cpython-312.pyc
  92. BIN
      py/venv/Lib/site-packages/numpy/core/__pycache__/multiarray.cpython-312.pyc
  93. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/__init__.cpython-312.pyc
  94. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_array_utils_impl.cpython-312.pyc
  95. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_arraypad_impl.cpython-312.pyc
  96. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_arraysetops_impl.cpython-312.pyc
  97. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_arrayterator_impl.cpython-312.pyc
  98. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_datasource.cpython-312.pyc
  99. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_function_base_impl.cpython-312.pyc
  100. BIN
      py/venv/Lib/site-packages/numpy/lib/__pycache__/_histograms_impl.cpython-312.pyc

+ 45 - 3
BAT-TOOL/0-ENABLE-TCPIP.BAT

@@ -12,18 +12,60 @@ set "SCRIPT_DIR=%~dp0"
 set "PROJECT_ROOT=%SCRIPT_DIR%.."
 
 REM Read ADB path from adb-path-config.js using PowerShell
-echo [0/2] Reading ADB path from adb-path-config.js...
+echo [0/3] Reading ADB path from adb-path-config.js...
 for /f "delims=" %%i in ('powershell -Command "$content = Get-Content '%PROJECT_ROOT%\adb-path-config.js' -Raw; if ($content -match '\"adb-path\"\s*:\s*''([^'']+)''') { $adbPath = $matches[1]; $fullPath = $adbPath.TrimEnd([char]92) + [char]92 + 'adb.exe'; if (Test-Path $fullPath) { Write-Output $fullPath } else { Write-Output 'adb' } } else { Write-Output 'adb' }"') do set "ADB_PATH=%%i"
 
 if "%ADB_PATH%"=="" set "ADB_PATH=adb"
 echo Using ADB: %ADB_PATH%
 echo.
 
-echo [1/2] List connected devices...
+REM Check if port 5037 is in use and kill the process if needed
+echo [1/3] Checking ADB daemon port (5037)...
+set "PORT_FOUND=0"
+set "KILLED_PID="
+for /f "tokens=5" %%a in ('netstat -ano 2^>nul ^| findstr :5037') do (
+    if !PORT_FOUND! equ 0 (
+        set "PID=%%a"
+        if not "!PID!"=="" (
+            set "PORT_FOUND=1"
+            set "KILLED_PID=!PID!"
+            echo Port 5037 is in use by process ID: !PID!
+            echo Attempting to kill process...
+            taskkill /F /PID !PID! 2>nul
+            if !errorlevel! neq 0 (
+                echo Warning: Failed to kill process !PID!, may need administrator privileges.
+            ) else (
+                echo Process !PID! killed successfully.
+            )
+            goto :port_killed
+        )
+    )
+)
+:port_killed
+
+REM Kill all ADB processes regardless of port check
+echo Killing all ADB processes...
+taskkill /F /IM adb.exe 2>nul
+if !errorlevel! neq 0 (
+    echo Warning: Failed to kill ADB processes, may need administrator privileges.
+    echo Please run this script as Administrator.
+) else (
+    echo All ADB processes terminated.
+)
+
+REM Wait a bit for processes to fully terminate
+if !PORT_FOUND! equ 1 (
+    timeout /t 2 /nobreak >nul
+) else (
+    timeout /t 1 /nobreak >nul
+)
+echo.
+
+echo [2/3] List connected devices...
 "%ADB_PATH%" devices
 echo.
 
-echo [2/2] Enable TCP/IP mode...
+echo [3/3] Enable TCP/IP mode...
 "%ADB_PATH%" tcpip 5555
 echo.
 

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

@@ -0,0 +1,303 @@
+# 工作流语言使用说明
+
+## 基本结构(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` 等)。但从“语言设计”角度,**低层模拟手机操作统一归为 action(press/swipe/scroll)**,其余更高阶能力推荐以 Func 标签方式抽象出来(见下文)。
+
+## 定位方式(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"`

+ 1 - 1
index.html

@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:5173 http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:5173 http://localhost:* ws://localhost:5173 ws://localhost:*; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:5173 http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:5173 http://localhost:* ws://localhost:5173 ws://localhost:* https://ai-anim.com; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;">
     <title>Auto Android Controller</title>
 </head>
 <body>

+ 422 - 0
main-js/func/extract-chat-history.js

@@ -0,0 +1,422 @@
+/**
+ * 提取聊天记录功能(Node.js 实现)
+ * 从屏幕截图中提取完整的聊天记录,包括发送者信息
+ * 
+ * 直接调用 Python 的 OnnxOCR 和 OpenCV,通过内联 Python 代码实现
+ */
+
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { join, isAbsolute } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import { readFile, writeFile } from 'fs/promises';
+
+const execAsync = promisify(exec);
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 安全读取包含 Unicode 字符的图片
+ */
+function readImageSafe(imagePath) {
+  return `
+import cv2
+import numpy as np
+import os
+
+def read_image_safe(image_path):
+    abs_path = os.path.abspath(str(image_path))
+    try:
+        with open(abs_path, 'rb') as f:
+            image_data = f.read()
+        img_array = np.frombuffer(image_data, np.uint8)
+        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
+        if img is None:
+            raise ValueError(f"cv2.imdecode 无法解码图片: {abs_path}")
+        return img
+    except FileNotFoundError:
+        raise FileNotFoundError(f"图片文件不存在: {abs_path}")
+    except Exception as e:
+        raise Exception(f"读取图片失败: {abs_path}, 错误: {str(e)}")
+`;
+}
+
+/**
+ * 提取聊天记录
+ * @param {string} screenshotPath - 截图路径
+ * @param {string} friendAvatarPath - 好友头像路径(可选)
+ * @param {string} myAvatarPath - 我的头像路径(可选)
+ * @param {number} deviceWidth - 设备宽度(可选)
+ * @param {number} deviceHeight - 设备高度(可选)
+ * @param {string} workflowFolder - 工作流文件夹路径(可选)
+ * @returns {Promise<{success: boolean, messages?: Array, messagesText?: string, error?: string}>}
+ */
+export async function extractChatHistory(screenshotPath, friendAvatarPath, myAvatarPath, deviceWidth, deviceHeight, workflowFolder) {
+  try {
+    const pythonExePath = join(__dirname, '..', '..', 'py', 'venv', 'Scripts', 'python.exe');
+    const onnxocrPath = join(__dirname, '..', '..', 'py', 'OnnxOCR');
+    
+    // 构建内联 Python 脚本
+    const pythonCode = `
+import sys
+import os
+import cv2
+import numpy as np
+import json
+from pathlib import Path
+
+# 添加 OnnxOCR 路径
+sys.path.insert(0, r"${onnxocrPath.replace(/\\/g, '/')}")
+
+from onnxocr.onnx_paddleocr import ONNXPaddleOcr
+
+# 设置环境变量
+os.environ['DISABLE_MODEL_SOURCE_CHECK'] = 'True'
+
+${readImageSafe()}
+
+def find_avatar_positions(screenshot_path, friend_avatar_path, my_avatar_path):
+    """在截图中查找头像位置"""
+    screenshot = read_image_safe(screenshot_path)
+    result = {'friend': [], 'my': []}
+    
+    if friend_avatar_path and friend_avatar_path != 'None':
+        try:
+            friend_avatar = read_image_safe(friend_avatar_path)
+            result_friend = cv2.matchTemplate(screenshot, friend_avatar, cv2.TM_CCOEFF_NORMED)
+            locations_friend = np.where(result_friend >= 0.8)
+            for pt in zip(*locations_friend[::-1]):
+                result['friend'].append([int(pt[0]), int(pt[1])])
+        except Exception as e:
+            print(f"查找好友头像失败: {e}", file=sys.stderr)
+    
+    if my_avatar_path and my_avatar_path != 'None':
+        try:
+            my_avatar = read_image_safe(my_avatar_path)
+            result_my = cv2.matchTemplate(screenshot, my_avatar, cv2.TM_CCOEFF_NORMED)
+            locations_my = np.where(result_my >= 0.8)
+            for pt in zip(*locations_my[::-1]):
+                result['my'].append([int(pt[0]), int(pt[1])])
+        except Exception as e:
+            print(f"查找我的头像失败: {e}", file=sys.stderr)
+    
+    return result
+
+def extract_chat_history(screenshot_path, friend_avatar_path, my_avatar_path, device_width, device_height, workflow_folder):
+    """提取完整的聊天记录"""
+    try:
+        screenshot = read_image_safe(screenshot_path)
+        if screenshot is None:
+            return {'success': False, 'error': '无法读取截图文件'}
+        
+        # 查找头像位置
+        avatar_positions = find_avatar_positions(screenshot_path, friend_avatar_path, my_avatar_path)
+        
+        # 获取 OCR 实例
+        ocr = ONNXPaddleOcr(use_angle_cls=True, use_gpu=False)
+        
+        # 执行全屏 OCR
+        ocr_result = ocr.ocr(screenshot)
+        
+        if not ocr_result or not ocr_result[0]:
+            return {'success': False, 'error': 'OCR 识别失败'}
+        
+        # 解析 OCR 结果,按 y 坐标分组消息
+        messages = []
+        friend_positions = avatar_positions.get('friend', [])
+        my_positions = avatar_positions.get('my', [])
+        
+        # 简单的消息分组逻辑:根据 y 坐标和头像位置判断发送者
+        for line in ocr_result[0]:
+            if not line:
+                continue
+            
+            box = line[0]
+            text = line[1][0] if len(line) > 1 and line[1] else ''
+            confidence = line[1][1] if len(line) > 1 and len(line[1]) > 1 else 0.0
+            
+            if not text or confidence < 0.5:
+                continue
+            
+            # 计算消息框的中心 y 坐标
+            y_center = sum([point[1] for point in box]) / len(box)
+            
+            # 判断发送者(简单的距离判断)
+            sender = 'unknown'
+            min_friend_dist = float('inf')
+            min_my_dist = float('inf')
+            
+            for fx, fy in friend_positions:
+                dist = abs(y_center - (fy + 20))  # 假设头像高度约 40px
+                if dist < min_friend_dist:
+                    min_friend_dist = dist
+            
+            for mx, my in my_positions:
+                dist = abs(y_center - (my + 20))
+                if dist < min_my_dist:
+                    min_my_dist = dist
+            
+            if min_friend_dist < 50 and min_friend_dist < min_my_dist:
+                sender = 'friend'
+            elif min_my_dist < 50 and min_my_dist < min_friend_dist:
+                sender = 'me'
+            
+            messages.append({
+                'text': text,
+                'sender': sender,
+                'y': int(y_center),
+                'confidence': float(confidence)
+            })
+        
+        # 按 y 坐标排序(从上到下)
+        messages.sort(key=lambda m: m['y'])
+        
+        # 格式化消息文本
+        messages_text = '\\n'.join([f"{'对方' if m['sender'] == 'friend' else '我' if m['sender'] == 'me' else '未知'}: {m['text']}" for m in messages])
+        
+        result = {
+            'success': True,
+            'messages': messages,
+            'messagesText': messages_text,
+            'count': len(messages)
+        }
+        
+        return result
+        
+    except Exception as e:
+        return {'success': False, 'error': f'提取聊天记录失败: {str(e)}'}
+
+# 主逻辑
+if __name__ == '__main__':
+    import sys
+    screenshot_path = sys.argv[1]
+    friend_avatar_path = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] != 'None' else None
+    my_avatar_path = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != 'None' else None
+    device_width = int(sys.argv[4]) if len(sys.argv) > 4 and sys.argv[4] else 1080
+    device_height = int(sys.argv[5]) if len(sys.argv) > 5 and sys.argv[5] else 2400
+    workflow_folder = sys.argv[6] if len(sys.argv) > 6 and sys.argv[6] != 'None' else None
+    
+    result = extract_chat_history(screenshot_path, friend_avatar_path, my_avatar_path, device_width, device_height, workflow_folder)
+    print(json.dumps(result, ensure_ascii=False))
+`;
+
+    // 将 Python 代码写入临时文件
+    const tempScriptPath = join(__dirname, '..', '..', 'temp_extract_chat_history.py');
+    await writeFile(tempScriptPath, pythonCode, 'utf8');
+
+    // 构建命令
+    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
+    let friendAvatarArg = 'None';
+    if (friendAvatarPath) {
+      friendAvatarArg = isAbsolute(friendAvatarPath) 
+        ? friendAvatarPath.replace(/\\/g, '/')
+        : join(__dirname, '..', '..', 'static', 'processing', friendAvatarPath).replace(/\\/g, '/');
+    }
+    
+    let myAvatarArg = 'None';
+    if (myAvatarPath) {
+      myAvatarArg = isAbsolute(myAvatarPath)
+        ? myAvatarPath.replace(/\\/g, '/')
+        : join(__dirname, '..', '..', 'static', 'processing', myAvatarPath).replace(/\\/g, '/');
+    }
+    
+    let workflowFolderArg = 'None';
+    if (workflowFolder) {
+      workflowFolderArg = isAbsolute(workflowFolder)
+        ? workflowFolder.replace(/\\/g, '/')
+        : join(__dirname, '..', '..', 'static', 'processing', workflowFolder).replace(/\\/g, '/');
+    }
+
+    const command = `"${pythonExePath}" "${tempScriptPath}" "${normalizedScreenshotPath}" "${friendAvatarArg}" "${myAvatarArg}" ${deviceWidth || 1080} ${deviceHeight || 2400} "${workflowFolderArg}"`;
+
+    const env = {
+      ...process.env,
+      DISABLE_MODEL_SOURCE_CHECK: 'True'
+    };
+
+    const { stdout, stderr } = await execAsync(command, {
+      timeout: 60000,
+      maxBuffer: 10 * 1024 * 1024,
+      cwd: join(__dirname, '..', '..'),
+      encoding: 'utf8',
+      env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
+    });
+
+    // 打印 Python 脚本的 stderr 输出
+    if (stderr && stderr.trim()) {
+      try {
+        const decodedStderr = Buffer.from(stderr, 'utf8').toString('utf8');
+        console.log(decodedStderr.trim());
+      } catch (e) {
+        console.log(stderr.trim());
+      }
+    }
+
+    // 清理临时文件
+    try {
+      await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
+    } catch (e) {
+      // 忽略删除失败
+    }
+
+    // 解析输出
+    const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
+    
+    try {
+      const result = JSON.parse(cleanStdout.trim());
+      return result;
+    } catch (parseError) {
+      console.error('聊天记录解析失败:', parseError);
+      console.error('原始输出:', cleanStdout);
+      return { success: false, error: `解析聊天记录失败: ${parseError.message}` };
+    }
+  } catch (error) {
+    console.error('提取聊天记录失败:', error);
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
+    }
+    return { success: false, error: error.message };
+  }
+}
+
+/**
+ * 获取最后一条消息
+ */
+export async function getLastMessage(screenshotPath, friendAvatarPath, myAvatarPath, deviceWidth, deviceHeight) {
+  try {
+    // 先提取完整聊天记录
+    const result = await extractChatHistory(screenshotPath, friendAvatarPath, myAvatarPath, deviceWidth, deviceHeight, null);
+    
+    if (!result.success || !result.messages || result.messages.length === 0) {
+      return { success: false, error: '未找到消息' };
+    }
+    
+    // 获取最后一条消息(y 坐标最大的)
+    const lastMessage = result.messages.reduce((max, msg) => msg.y > max.y ? msg : max, result.messages[0]);
+    
+    return {
+      success: true,
+      text: lastMessage.text,
+      sender: lastMessage.sender,
+      position: { y: lastMessage.y }
+    };
+  } catch (error) {
+    return { success: false, error: error.message };
+  }
+}
+
+/**
+ * 全屏 OCR 识别
+ */
+export async function ocrFullScreen(screenshotPath, deviceWidth, deviceHeight) {
+  try {
+    const pythonExePath = join(__dirname, '..', '..', 'py', 'venv', 'Scripts', 'python.exe');
+    const onnxocrPath = join(__dirname, '..', '..', 'py', 'OnnxOCR');
+    
+    const pythonCode = `
+import sys
+import os
+import cv2
+import numpy as np
+import json
+
+# 添加 OnnxOCR 路径
+sys.path.insert(0, r"${onnxocrPath.replace(/\\/g, '/')}")
+
+from onnxocr.onnx_paddleocr import ONNXPaddleOcr
+
+# 设置环境变量
+os.environ['DISABLE_MODEL_SOURCE_CHECK'] = 'True'
+
+def read_image_safe(image_path):
+    abs_path = os.path.abspath(str(image_path))
+    try:
+        with open(abs_path, 'rb') as f:
+            image_data = f.read()
+        img_array = np.frombuffer(image_data, np.uint8)
+        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
+        if img is None:
+            raise ValueError(f"cv2.imdecode 无法解码图片: {abs_path}")
+        return img
+    except FileNotFoundError:
+        raise FileNotFoundError(f"图片文件不存在: {abs_path}")
+    except Exception as e:
+        raise Exception(f"读取图片失败: {abs_path}, 错误: {str(e)}")
+
+# 主逻辑
+if __name__ == '__main__':
+    screenshot_path = sys.argv[1]
+    
+    try:
+        screenshot = read_image_safe(screenshot_path)
+        if screenshot is None:
+            print(json.dumps({'success': False, 'error': '无法读取截图文件'}, ensure_ascii=False))
+            sys.exit(1)
+        
+        ocr = ONNXPaddleOcr(use_angle_cls=True, use_gpu=False)
+        ocr_result = ocr.ocr(screenshot)
+        
+        if not ocr_result or not ocr_result[0]:
+            print(json.dumps({'success': False, 'error': 'OCR 识别失败'}, ensure_ascii=False))
+            sys.exit(1)
+        
+        # 提取所有文本
+        texts = []
+        for line in ocr_result[0]:
+            if line and len(line) > 1:
+                text = line[1][0] if isinstance(line[1], (list, tuple)) else str(line[1])
+                if text:
+                    texts.append(text)
+        
+        full_text = '\\n'.join(texts)
+        
+        print(json.dumps({
+            'success': True,
+            'text': full_text,
+            'position': None
+        }, ensure_ascii=False))
+    except Exception as e:
+        print(json.dumps({'success': False, 'error': f'OCR 识别失败: {str(e)}'}, ensure_ascii=False))
+        sys.exit(1)
+`;
+
+    // 将 Python 代码写入临时文件
+    const tempScriptPath = join(__dirname, '..', '..', 'temp_ocr_full_screen.py');
+    await writeFile(tempScriptPath, pythonCode, 'utf8');
+
+    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
+    const command = `"${pythonExePath}" "${tempScriptPath}" "${normalizedScreenshotPath}"`;
+
+    const env = {
+      ...process.env,
+      DISABLE_MODEL_SOURCE_CHECK: 'True'
+    };
+
+    const { stdout, stderr } = await execAsync(command, {
+      timeout: 60000,
+      maxBuffer: 10 * 1024 * 1024,
+      cwd: join(__dirname, '..', '..'),
+      encoding: 'utf8',
+      env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
+    });
+
+    // 清理临时文件
+    try {
+      await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
+    } catch (e) {
+      // 忽略删除失败
+    }
+
+    const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
+
+    try {
+      const result = JSON.parse(cleanStdout.trim());
+      return result;
+    } catch (parseError) {
+      return { success: false, error: `解析失败: ${parseError.message}` };
+    }
+  } catch (error) {
+    return { success: false, error: error.message };
+  }
+}

+ 213 - 0
main-js/func/img-reg.js

@@ -0,0 +1,213 @@
+/**
+ * 图像匹配功能(Node.js 实现)
+ * 识别模板图片是否在截图中的位置
+ * 
+ * 直接调用 Python 的 OpenCV,通过内联 Python 代码实现
+ */
+
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { join, isAbsolute } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import { writeFile } from 'fs/promises';
+
+const execAsync = promisify(exec);
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 匹配图像
+ * @param {string} screenshotPath - 截图路径
+ * @param {string} templatePath - 模板图片路径
+ * @param {number} width - 设备宽度(可选)
+ * @param {number} height - 设备高度(可选)
+ * @returns {Promise<{success: boolean, x?: number, y?: number, width?: number, height?: number, error?: string}>}
+ */
+export async function matchImage(screenshotPath, templatePath, width, height) {
+  try {
+    const pythonExePath = join(__dirname, '..', '..', 'py', 'venv', 'Scripts', 'python.exe');
+    
+    // 构建内联 Python 脚本
+    const pythonCode = `
+import sys
+import os
+import cv2
+import numpy as np
+import json
+from pathlib import Path
+
+def read_image_safe(image_path):
+    """安全读取包含 Unicode 字符的图片"""
+    abs_path = os.path.abspath(str(image_path))
+    try:
+        with open(abs_path, 'rb') as f:
+            image_data = f.read()
+        img_array = np.frombuffer(image_data, np.uint8)
+        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
+        if img is None:
+            raise ValueError(f"cv2.imdecode 无法解码图片: {abs_path}")
+        return img
+    except FileNotFoundError:
+        raise FileNotFoundError(f"图片文件不存在: {abs_path}")
+    except Exception as e:
+        raise Exception(f"读取图片失败: {abs_path}, 错误: {str(e)}")
+
+def match_image(image1_path, image2_path, image1_size=None):
+    """
+    在 image1 中查找 image2 的位置
+    
+    Args:
+        image1_path: 截图路径(大图)
+        image2_path: 模板图片路径(小图)
+        image1_size: image1 的目标尺寸 (width, height),可选
+    
+    Returns:
+        如果找到,返回 (x, y, width, height),否则返回 None
+    """
+    img1_path = Path(image1_path)
+    img2_path = Path(image2_path)
+    
+    if not img1_path.exists():
+        raise FileNotFoundError(f"图片1不存在: {image1_path}")
+    if not img2_path.exists():
+        raise FileNotFoundError(f"图片2不存在: {image2_path}")
+    
+    # 安全读取图片(支持 Unicode 路径)
+    img1 = read_image_safe(img1_path)
+    img2 = read_image_safe(img2_path)
+    
+    # 如果指定了 image1 的目标尺寸,先调整大小
+    if image1_size is not None:
+        target_width, target_height = image1_size
+        img1 = cv2.resize(img1, (target_width, target_height))
+    
+    # 转换为灰度图以提高匹配速度
+    img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
+    img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
+    
+    # 获取模板尺寸
+    template_height, template_width = img2_gray.shape
+    
+    # 如果模板比截图大,无法匹配
+    if template_height > img1_gray.shape[0] or template_width > img1_gray.shape[1]:
+        return None
+    
+    # 执行模板匹配
+    # cv2.TM_CCOEFF_NORMED 方法返回 0-1 的值,1 表示完全匹配
+    result = cv2.matchTemplate(img1_gray, img2_gray, cv2.TM_CCOEFF_NORMED)
+    
+    # 找到最佳匹配位置
+    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
+    
+    # 设置阈值(0.8 表示 80% 相似度)
+    threshold = 0.8
+    
+    if max_val >= threshold:
+        # 找到匹配位置
+        top_left = max_loc
+        x, y = top_left
+        
+        # 返回坐标和尺寸 (x, y, width, height)
+        return (x, y, template_width, template_height)
+    else:
+        # 未找到匹配
+        return None
+
+# 主逻辑
+if __name__ == '__main__':
+    image1_path = sys.argv[1]
+    image2_path = sys.argv[2]
+    image1_width = int(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3] else None
+    image1_height = int(sys.argv[4]) if len(sys.argv) > 4 and sys.argv[4] else None
+    
+    image1_size = None
+    if image1_width and image1_height:
+        image1_size = (image1_width, image1_height)
+    
+    try:
+        result = match_image(image1_path, image2_path, image1_size)
+        
+        if result:
+            x, y, w, h = result
+            output = {
+                'success': True,
+                'x': int(x),
+                'y': int(y),
+                'width': int(w),
+                'height': int(h)
+            }
+        else:
+            output = {
+                'success': False,
+                'error': '未找到匹配的图像'
+            }
+        
+        print(json.dumps(output, ensure_ascii=False))
+    except Exception as e:
+        output = {
+            'success': False,
+            'error': f'图像匹配失败: {str(e)}'
+        }
+        print(json.dumps(output, ensure_ascii=False))
+        sys.exit(1)
+`;
+
+    // 将 Python 代码写入临时文件
+    const tempScriptPath = join(__dirname, '..', '..', 'temp_img_reg.py');
+    await writeFile(tempScriptPath, pythonCode, 'utf8');
+
+    // 构建命令
+    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
+    const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
+    const command = `"${pythonExePath}" "${tempScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width || ''} ${height || ''}`;
+
+    const env = {
+      ...process.env,
+      DISABLE_MODEL_SOURCE_CHECK: 'True'
+    };
+
+    const { stdout, stderr } = await execAsync(command, {
+      timeout: 30000,
+      maxBuffer: 10 * 1024 * 1024,
+      cwd: join(__dirname, '..', '..'),
+      encoding: 'utf8',
+      env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
+    });
+
+    // 打印 Python 脚本的 stderr 输出(如果有)
+    if (stderr && stderr.trim()) {
+      try {
+        const decodedStderr = Buffer.from(stderr, 'utf8').toString('utf8');
+        console.log(decodedStderr.trim());
+      } catch (e) {
+        console.log(stderr.trim());
+      }
+    }
+
+    // 清理临时文件
+    try {
+      await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
+    } catch (e) {
+      // 忽略删除失败
+    }
+
+    // 解析输出
+    const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
+    
+    try {
+      const result = JSON.parse(cleanStdout.trim());
+      return result;
+    } catch (parseError) {
+      console.error('图像匹配结果解析失败:', parseError);
+      console.error('原始输出:', cleanStdout);
+      return { success: false, error: `解析图像匹配结果失败: ${parseError.message}` };
+    }
+  } catch (error) {
+    console.error('图像匹配失败:', error);
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: '图像匹配超时,请检查网络连接或稍后重试' };
+    }
+    return { success: false, error: error.message };
+  }
+}

+ 39 - 0
main-js/func/string-reg-location.js

@@ -0,0 +1,39 @@
+/**
+ * 文字识别和定位功能(Node.js 实现)
+ * 在截图中查找指定文字,并返回文字在截图中的坐标
+ * 
+ * 注意:Python 脚本已被删除,此功能需要重新实现
+ * 未来可以迁移到 Node.js 的 OCR 库(如 tesseract.js)
+ */
+
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { join } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const execAsync = promisify(exec);
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * 查找文字位置
+ * @param {string} screenshotPath - 截图路径
+ * @param {string} targetText - 目标文字
+ * @param {number} deviceWidth - 设备宽度(可选)
+ * @param {number} deviceHeight - 设备高度(可选)
+ * @returns {Promise<{success: boolean, found?: boolean, x?: number, y?: number, width?: number, height?: number, error?: string}>}
+ */
+export async function findTextLocation(screenshotPath, targetText, deviceWidth, deviceHeight) {
+  try {
+    // Python 脚本已被删除,此功能需要重新实现
+    // TODO: 使用 Node.js 的 OCR 库(如 tesseract.js)重新实现
+    return { 
+      success: false, 
+      found: false,
+      error: 'string-reg-location.py 已被删除,此功能需要重新实现。请使用 Node.js 的 OCR 库(如 tesseract.js)。' 
+    };
+  } catch (error) {
+    return { success: false, found: false, error: error.message };
+  }
+}

+ 546 - 160
main-js/history.js

@@ -1,11 +1,14 @@
 import { ipcMain } from 'electron';
-import { readdir, writeFile, readFile } from 'fs/promises';
+import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
 import { join, dirname, isAbsolute } from 'path';
 import { fileURLToPath } from 'url';
 import { exec } from 'child_process';
 import { promisify } from 'util';
 import { captureScreenshot } from './adb/screenshot.js';
 import { getDeviceResolution } from './adb/device-info.js';
+import { matchImage } from './func/img-reg.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';
 
 const execAsync = promisify(exec);
 const __filename = fileURLToPath(import.meta.url);
@@ -20,11 +23,17 @@ export async function getStaticFolders() {
     const folders = [];
     for (const entry of entries) {
       if (entry.isDirectory()) {
-        folders.push(entry.name);
+        const folderPath = join(staticPath, entry.name);
+        const stats = await stat(folderPath);
+        folders.push({
+          name: entry.name,
+          createdAt: stats.birthtime || stats.mtime, // 使用创建时间,如果没有则使用修改时间
+        });
       }
     }
     
-    return folders.sort(); // 按名称排序
+    // 按创建时间排序,最新的在前
+    return folders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
   } catch (error) {
     console.error('Failed to read static/processing folders:', error);
     return [];
@@ -66,86 +75,77 @@ export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
     const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
     await writeFile(screenshotPath, screenshotBuffer);
 
-    // 4. 调用 Python 脚本进行图像匹配
-    // 使用虚拟环境中的 Python 解释器
-    const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
-    const pythonScriptPath = join(__dirname, '..', 'py', 'img-reg.py');
+    // 4. 调用 JS 函数进行图像匹配
+    const matchResult = await matchImage(screenshotPath, absoluteTemplatePath, width, height);
     
-    // 确保路径使用正斜杠,避免 Windows 路径问题
-    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
-    const normalizedTemplatePath = absoluteTemplatePath.replace(/\\/g, '/');
-    
-    // 使用 -u 参数确保输出不被缓冲,并设置 UTF-8 编码
-    const command = `"${pythonExePath}" -u "${pythonScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width} ${height}`;
-    
-    const { stdout, stderr } = await execAsync(command, {
-      timeout: 10000,
-      maxBuffer: 10 * 1024 * 1024,
-      cwd: join(__dirname, '..'),
-      encoding: 'utf8',
-      env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
-    });
-
-    // 5. 解析 Python 脚本输出
-    // 输出格式可能是: 
-    // - "找到匹配!坐标: x=100, y=200, 宽度=50, 高度=50"
-    // - "JSON格式: {"x": 100, "y": 200, "width": 50, "height": 50}"
-    // - "未找到匹配"
-    // - "错误: xxx"
-    
-    // 检查是否有错误或未找到匹配
-    if (stdout.includes('未找到匹配') || stdout.toLowerCase().includes('错误') || stderr) {
-      const errorMsg = stdout.includes('未找到匹配') 
-        ? '未找到匹配的图像' 
-        : (stderr || stdout.match(/错误[::]\s*(.+)/)?.[1] || '图像匹配失败');
-      return { success: false, error: errorMsg };
+    if (!matchResult.success) {
+      return { success: false, error: matchResult.error || '图像匹配失败' };
     }
 
-    // 尝试从输出中提取 JSON
-    // 先尝试匹配 JSON 格式的行
-    let jsonMatch = stdout.match(/JSON格式:\s*(\{[^}]+\})/);
-    if (!jsonMatch) {
-      // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
-      jsonMatch = stdout.match(/\{\s*"x"\s*:\s*\d+\s*,\s*"y"\s*:\s*\d+\s*,\s*"width"\s*:\s*\d+\s*,\s*"height"\s*:\s*\d+\s*\}/);
-    }
-    
-    if (!jsonMatch) {
-      // 如果还是没有找到,尝试从文本中提取坐标
-      const coordMatch = stdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
-      if (coordMatch) {
-        const x = parseInt(coordMatch[1], 10);
-        const y = parseInt(coordMatch[2], 10);
-        const w = parseInt(coordMatch[3], 10);
-        const h = parseInt(coordMatch[4], 10);
-        
-        // 计算点击位置(中心点)
-        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 }
-        };
-      }
+    // 5. 返回匹配结果
+    if (matchResult.success && matchResult.x !== undefined) {
+      const { x, y, width: w, height: h } = matchResult;
       
-      // 如果都找不到,返回错误并输出原始输出用于调试
-      console.error('Python 脚本输出:', cleanStdout);
-      console.error('Python 脚本错误输出:', cleanStderr);
-      return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
+      // 计算点击位置(中心点)
+      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 };
+  }
+}
 
-    let coordinate;
-    try {
-      coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
-    } catch (parseError) {
-      console.error('JSON 解析失败:', parseError);
-      console.error('原始输出:', cleanStdout);
-      return { success: false, error: `JSON 解析失败: ${parseError.message}` };
+// 执行文字识别:截图、调用 Python 脚本、返回坐标
+export async function findTextAndGetCoordinate(ipPort, targetText) {
+  try {
+    if (!ipPort) {
+      return { success: false, error: '缺少设备 ID' };
     }
+    if (!targetText) {
+      return { success: false, error: '缺少目标文字' };
+    }
+
+    // 1. 获取设备分辨率
+    const resolutionResult = await getDeviceResolution(ipPort);
+    if (!resolutionResult.success) {
+      return { success: false, error: '获取设备分辨率失败' };
+    }
+    const { width, height } = resolutionResult;
+
+    // 2. 获取屏幕截图
+    const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
+    if (!screenshotResult.success || !screenshotResult.data) {
+      return { success: false, error: '获取屏幕截图失败' };
+    }
+
+    // 3. 保存截图到临时文件
+    const tempDir = join(__dirname, '..');
+    const screenshotPath = join(tempDir, 'temp_screenshot.png');
+    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
+    await writeFile(screenshotPath, screenshotBuffer);
+
+    // 4. 调用 JS 函数进行文字识别
+    const textResult = await findTextLocation(screenshotPath, targetText, width, height);
     
-    const { x, y, width: w, height: h } = coordinate;
+    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);
@@ -156,20 +156,21 @@ export async function matchImageAndGetCoordinate(ipPort, templateImagePath) {
       clickPosition: { x: clickX, y: clickY }
     };
   } catch (error) {
-    console.error('图像匹配失败:', error);
+    console.error('文字识别失败:', error);
+    // 如果是超时错误,提供更友好的提示
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
+    }
     return { success: false, error: error.message };
   }
 }
 
-// 执行文字识别:截图、调用 Python 脚本、返回坐标
-export async function findTextAndGetCoordinate(ipPort, targetText) {
+// OCR识别最后一条消息(兼容旧API)
+export async function ocrLastMessage(ipPort, method, avatarPath, area, folderPath = null) {
   try {
     if (!ipPort) {
       return { success: false, error: '缺少设备 ID' };
     }
-    if (!targetText) {
-      return { success: false, error: '缺少目标文字' };
-    }
 
     // 1. 获取设备分辨率
     const resolutionResult = await getDeviceResolution(ipPort);
@@ -184,104 +185,266 @@ export async function findTextAndGetCoordinate(ipPort, targetText) {
       return { success: false, error: '获取屏幕截图失败' };
     }
 
-    // 3. 保存截图到临时文件
-    const tempDir = join(__dirname, '..');
-    const screenshotPath = join(tempDir, 'temp_screenshot.png');
+    // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
+    let screenshotPath;
+    if (folderPath) {
+      const { mkdir } = await import('fs/promises');
+      // 确保 folderPath 是绝对路径
+      let absoluteFolderPath = folderPath;
+      if (!isAbsolute(folderPath)) {
+        // 如果已经是 static/processing/xxx 格式,去掉开头的 static/processing 再拼接
+        if (folderPath.startsWith('static/processing/')) {
+          const folderName = folderPath.replace('static/processing/', '');
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (folderPath.startsWith('static\\processing\\')) {
+          const folderName = folderPath.replace('static\\processing\\', '');
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          // 如果只是文件夹名,需要加上 static/processing
+          absoluteFolderPath = join(__dirname, '..', 'static', 'processing', folderPath);
+        }
+      }
+      
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
+      const 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);
 
-    // 4. 调用 Python 脚本进行文字识别
-    // 使用虚拟环境中的 Python 解释器
-    const pythonExePath = join(__dirname, '..', 'py', 'venv', 'Scripts', 'python.exe');
-    const pythonScriptPath = join(__dirname, '..', 'py', 'string-reg-location.py');
-    // 转义目标文字中的双引号,避免命令解析错误
-    const escapedText = targetText.replace(/"/g, '\\"');
-    const command = `"${pythonExePath}" "${pythonScriptPath}" "${screenshotPath}" "${escapedText}" ${width} ${height}`;
-    
-    // 设置环境变量,跳过模型源连接检查
-    const env = {
-      ...process.env,
-      DISABLE_MODEL_SOURCE_CHECK: 'True'
-    };
+    // 4. 调用 JS 实现进行OCR识别
+    const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
+    let result;
     
-    const { stdout, stderr } = await execAsync(command, {
-      timeout: 60000, // OCR 首次运行可能需要下载模型,设置60秒超时
-      maxBuffer: 10 * 1024 * 1024,
-      cwd: join(__dirname, '..'),
-      env: env
-    });
-
-    // 5. 解析 Python 脚本输出
-    // 检查是否有错误或未找到匹配
-    // 忽略警告信息(DeprecationWarning 等)
-    const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
-    const cleanStderr = stderr ? stderr.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '') : '';
+    if (method === 'full-screen') {
+      // 全屏OCR识别
+      result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
+    } else if (method === 'by-avatar' && avatarPath) {
+      // 通过头像定位最后一条消息
+      let friendAvatarArg = null;
+      let myAvatarArg = null;
+      
+      if (isAbsolute(avatarPath)) {
+        friendAvatarArg = avatarPath;
+        myAvatarArg = avatarPath;
+      } else {
+        const folderName = avatarPath.split(/[/\\]/)[0];
+        const avatarName = avatarPath.split(/[/\\]/).slice(1).join('/');
+        friendAvatarArg = join(__dirname, '..', 'static', 'processing', folderName, avatarName);
+        myAvatarArg = friendAvatarArg;
+      }
+      
+      const normalizedFriendAvatar = friendAvatarArg.replace(/\\/g, '/');
+      const normalizedMyAvatar = myAvatarArg.replace(/\\/g, '/');
+      result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
+    } else {
+      // 默认使用全屏OCR
+      result = await ocrFullScreenFromFunc(normalizedScreenshotPath, width, height);
+    }
     
-    if (cleanStdout.includes('未找到匹配的文字') || (cleanStdout.toLowerCase().includes('错误') && !cleanStdout.includes('找到文字')) || (cleanStderr && !cleanStderr.includes('Checking connectivity'))) {
-      const errorMsg = cleanStdout.includes('未找到匹配的文字') 
-        ? `未找到文字: ${targetText}` 
-        : (cleanStderr || cleanStdout.match(/错误[::]\s*(.+)/)?.[1] || '文字识别失败');
-      return { success: false, error: errorMsg };
+    if (result.success) {
+      // 返回兼容旧API的格式
+      return {
+        success: true,
+        text: result.text || '',
+        position: result.position || null
+      };
+    } else {
+      return { success: false, error: result.error || 'OCR识别失败' };
     }
+  } catch (error) {
+    console.error('OCR识别失败:', error);
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: 'OCR识别超时,请检查网络连接或稍后重试' };
+    }
+    return { success: false, error: error.message };
+  }
+}
 
-    // 尝试从输出中提取 JSON(使用清理后的输出)
-    let jsonMatch = cleanStdout.match(/JSON格式:\s*(\{[^}]+\})/);
-    if (!jsonMatch) {
-      // 如果没有找到 JSON 格式,尝试直接匹配 JSON 对象
-      jsonMatch = cleanStdout.match(/\{\s*"x"\s*:\s*\d+\s*,\s*"y"\s*:\s*\d+\s*,\s*"width"\s*:\s*\d+\s*,\s*"height"\s*:\s*\d+\s*\}/);
+// 提取聊天记录
+export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath = null) {
+  try {
+    if (!ipPort) {
+      return { success: false, error: '缺少设备 ID' };
     }
-    
-    if (!jsonMatch) {
-      // 如果还是没有找到,尝试从文本中提取坐标
-      const coordMatch = cleanStdout.match(/坐标:\s*x=(\d+),\s*y=(\d+),\s*宽度=(\d+),\s*高度=(\d+)/);
-      if (coordMatch) {
-        const x = parseInt(coordMatch[1], 10);
-        const y = parseInt(coordMatch[2], 10);
-        const w = parseInt(coordMatch[3], 10);
-        const h = parseInt(coordMatch[4], 10);
-        
-        // 计算点击位置(中心点)
-        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 }
-        };
+
+    // 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;
+    if (workflowFolderPath) {
+      const { mkdir } = await import('fs/promises');
+      // 确保 workflowFolderPath 是绝对路径
+      let absoluteWorkflowPath = workflowFolderPath;
+      if (!isAbsolute(workflowFolderPath)) {
+        // 如果已经是 static/processing/xxx 格式,直接拼接(去掉开头的 static/processing)
+        if (workflowFolderPath.startsWith('static/processing/')) {
+          const folderName = workflowFolderPath.replace('static/processing/', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
+          const folderName = workflowFolderPath.replace('static\\processing\\', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          // 如果只是文件夹名,需要加上 static/processing
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
+        }
       }
       
-      // 如果都找不到,返回错误并输出原始输出用于调试
-      console.error('Python 脚本输出:', cleanStdout);
-      console.error('Python 脚本错误输出:', cleanStderr);
-      return { success: false, error: `无法解析 Python 脚本输出。输出: ${cleanStdout.substring(0, 200)}` };
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
+      const 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');
     }
-
-    let coordinate;
+    const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
+    await writeFile(screenshotPath, screenshotBuffer);
+    
+    // 验证文件是否成功写入
     try {
-      coordinate = JSON.parse(jsonMatch[1] || jsonMatch[0]);
-    } catch (parseError) {
-      console.error('JSON 解析失败:', parseError);
-      console.error('原始输出:', cleanStdout);
-      return { success: false, error: `JSON 解析失败: ${parseError.message}` };
+      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}` };
+    }
+
+    // 4. 调用 JS 函数提取聊天记录
+    // 转换头像路径为绝对路径
+    let friendAvatarArg = null;
+    if (friendAvatarPath) {
+      if (isAbsolute(friendAvatarPath)) {
+        friendAvatarArg = friendAvatarPath;
+      } else {
+        friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath);
+      }
     }
     
-    const { x, y, width: w, height: h } = coordinate;
+    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;
+  } catch (error) {
+    console.error('提取聊天记录失败:', error);
+    if (error.message && error.message.includes('timeout')) {
+      return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
+    }
+    return { success: false, error: error.message };
+  }
+}
 
-    // 计算点击位置(中心点)
-    const clickX = Math.round(x + w / 2);
-    const clickY = Math.round(y + h / 2);
+// 获取最后一条消息(带发送者信息)
+export async function getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath) {
+  try {
+    if (!ipPort) {
+      return { success: false, error: '缺少设备 ID' };
+    }
 
-    return {
-      success: true,
-      coordinate: { x, y, width: w, height: h },
-      clickPosition: { x: clickX, y: clickY }
-    };
+    // 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);
-    // 如果是超时错误,提供更友好的提示
+    console.error('获取最后一条消息失败:', error);
     if (error.message && error.message.includes('timeout')) {
-      return { success: false, error: '文字识别超时,请检查网络连接或稍后重试' };
+      return { success: false, error: '获取最后一条消息超时,请检查网络连接或稍后重试' };
     }
     return { success: false, error: error.message };
   }
@@ -364,4 +527,227 @@ export function registerIpcHandlers() {
   ipcMain.handle('find-text-and-get-coordinate', async (event, ipPort, targetText) => {
     return await findTextAndGetCoordinate(ipPort, targetText);
   });
+
+  ipcMain.handle('ocr-last-message', async (event, ipPort, method, avatarPath, area, folderPath) => {
+    return await ocrLastMessage(ipPort, method, avatarPath, area, folderPath);
+  });
+
+  ipcMain.handle('extract-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => {
+    return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath);
+  });
+
+  ipcMain.handle('get-last-chat-message', async (event, ipPort, friendAvatarPath, myAvatarPath) => {
+    return await getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath);
+  });
+
+  ipcMain.handle('save-workflow', async (event, workflowJson, imagesData) => {
+    return await saveWorkflow(workflowJson, imagesData);
+  });
+
+  ipcMain.handle('delete-workflow', async (event, folderName) => {
+    return await deleteWorkflow(folderName);
+  });
+
+  ipcMain.handle('ensure-directory', async (event, dirPath) => {
+    try {
+      await mkdir(dirPath, { recursive: true });
+      return { success: true };
+    } catch (error) {
+      return { success: false, error: error.message };
+    }
+  });
+
+  ipcMain.handle('write-text-file', async (event, filePath, content) => {
+    try {
+      await writeFile(filePath, content, 'utf-8');
+      return { success: true };
+    } catch (error) {
+      return { success: false, error: error.message };
+    }
+  });
+
+  ipcMain.handle('read-text-file', async (event, filePath) => {
+    try {
+      const content = await readFile(filePath, 'utf-8');
+      return { success: true, content };
+    } catch (error) {
+      return { success: false, error: error.message };
+    }
+  });
+
+  // 保存聊天记录到 history 文件夹
+  ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => {
+    try {
+      // 将相对路径转换为绝对路径
+      let absoluteWorkflowPath = workflowFolderPath;
+      if (!isAbsolute(workflowFolderPath)) {
+        if (workflowFolderPath.startsWith('static/processing/')) {
+          const folderName = workflowFolderPath.replace('static/processing/', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
+          const folderName = workflowFolderPath.replace('static\\processing\\', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
+        }
+      }
+
+      const historyDir = join(absoluteWorkflowPath, 'history');
+      await mkdir(historyDir, { recursive: true });
+
+      // 生成文件名(使用时间戳)
+      const 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}`);
+      return { success: true, filePath };
+    } catch (error) {
+      console.error('保存聊天记录失败:', error);
+      return { success: false, error: error.message };
+    }
+  });
+
+  // 保存聊天记录总结
+  ipcMain.handle('save-chat-history-summary', async (event, workflowFolderPath, summary) => {
+    try {
+      // 将相对路径转换为绝对路径
+      let absoluteWorkflowPath = workflowFolderPath;
+      if (!isAbsolute(workflowFolderPath)) {
+        if (workflowFolderPath.startsWith('static/processing/')) {
+          const folderName = workflowFolderPath.replace('static/processing/', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
+          const folderName = workflowFolderPath.replace('static\\processing\\', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
+        }
+      }
+
+      const historyDir = join(absoluteWorkflowPath, 'history');
+      await mkdir(historyDir, { recursive: true });
+
+      const summaryFilePath = join(historyDir, 'summary.txt');
+      await writeFile(summaryFilePath, summary, 'utf-8');
+
+      console.log(`聊天记录总结已保存到: ${summaryFilePath}`);
+      return { success: true };
+    } catch (error) {
+      console.error('保存聊天记录总结失败:', error);
+      return { success: false, error: error.message };
+    }
+  });
+
+  // 读取聊天记录总结
+  ipcMain.handle('get-chat-history-summary', async (event, workflowFolderPath) => {
+    try {
+      // 将相对路径转换为绝对路径
+      let absoluteWorkflowPath = workflowFolderPath;
+      if (!isAbsolute(workflowFolderPath)) {
+        if (workflowFolderPath.startsWith('static/processing/')) {
+          const folderName = workflowFolderPath.replace('static/processing/', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else if (workflowFolderPath.startsWith('static\\processing\\')) {
+          const folderName = workflowFolderPath.replace('static\\processing\\', '');
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+        } else {
+          absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
+        }
+      }
+
+      const summaryFilePath = join(absoluteWorkflowPath, 'history', 'summary.txt');
+
+      // 检查文件是否存在
+      try {
+        const { access, constants } = await import('fs/promises');
+        await access(summaryFilePath, constants.F_OK);
+        const summary = await readFile(summaryFilePath, 'utf-8');
+        return { success: true, summary: summary.trim() };
+      } catch (error) {
+        // 文件不存在,返回空字符串
+        return { success: true, summary: '' };
+      }
+    } catch (error) {
+      console.error('读取聊天记录总结失败:', error);
+      return { success: false, error: error.message };
+    }
+  });
+}
+
+// 保存工作流到 static/processing 目录
+export async function saveWorkflow(workflowJson, imagesData = []) {
+  try {
+    // 支持新旧格式
+    const hasActions = Array.isArray(workflowJson.actions) || Array.isArray(workflowJson);
+    if (!workflowJson || typeof workflowJson !== 'object' || !hasActions) {
+      return { success: false, error: '工作流格式错误:缺少 actions 数组' };
+    }
+
+    // 生成文件夹名称(使用时间戳)
+    const now = new Date();
+    const timestamp = now.getFullYear() + 
+                     String(now.getMonth() + 1).padStart(2, '0') + 
+                     String(now.getDate()).padStart(2, '0') + '_' +
+                     String(now.getHours()).padStart(2, '0') + 
+                     String(now.getMinutes()).padStart(2, '0') + 
+                     String(now.getSeconds()).padStart(2, '0');
+    const folderName = workflowJson.name || `工作流_${timestamp}`;
+
+    // 创建工作流文件夹
+    const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+    await mkdir(workflowPath, { recursive: true });
+
+    // 保存 processing.json
+    const jsonPath = join(workflowPath, 'processing.json');
+    const jsonContent = JSON.stringify(workflowJson, null, '\t');
+    await writeFile(jsonPath, jsonContent, 'utf-8');
+
+    // 保存图片
+    if (imagesData && Array.isArray(imagesData) && imagesData.length > 0) {
+      for (const imageData of imagesData) {
+        if (imageData.base64 && imageData.name) {
+          try {
+            // 将base64转换为Buffer
+            const imageBuffer = Buffer.from(imageData.base64, 'base64');
+            const imagePath = join(workflowPath, imageData.name);
+            await writeFile(imagePath, imageBuffer);
+            console.log(`图片已保存: ${imageData.name}`);
+          } catch (imageError) {
+            console.error(`保存图片失败 ${imageData.name}:`, imageError);
+          }
+        }
+      }
+    }
+
+    console.log(`工作流已保存: ${folderName}`);
+    return { success: true, folderName, path: workflowPath };
+  } catch (error) {
+    console.error('保存工作流失败:', error);
+    return { success: false, error: error.message };
+  }
+}
+
+// 删除工作流文件夹
+export async function deleteWorkflow(folderName) {
+  try {
+    if (!folderName || typeof folderName !== 'string') {
+      return { success: false, error: '文件夹名称无效' };
+    }
+
+    // 构建文件夹路径
+    const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
+
+    // 删除整个文件夹(包括所有内容)
+    await rm(workflowPath, { recursive: true, force: true });
+
+    console.log(`工作流已删除: ${folderName}`);
+    return { success: true, folderName };
+  } catch (error) {
+    console.error('删除工作流失败:', error);
+    return { success: false, error: error.message };
+  }
 }

+ 2 - 2
main-js/security.js

@@ -4,8 +4,8 @@ import { session } from 'electron';
 export function setContentSecurityPolicy(isDev) {
   session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
     const csp = isDev
-      ? "default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;"
-      : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;";
+      ? "default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:* https://ai-anim.com; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;"
+      : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://ai-anim.com; img-src 'self' data: https:; font-src 'self' data:;";
     
     const responseHeaders = Object.assign({}, details.responseHeaders);
     responseHeaders['Content-Security-Policy'] = [csp];

+ 8 - 8
node_modules/.vite/deps/_metadata.json

@@ -1,37 +1,37 @@
 {
-  "hash": "ff97c773",
-  "configHash": "e49b0d65",
+  "hash": "29f0465f",
+  "configHash": "cf599be1",
   "lockfileHash": "0bc5dcf5",
-  "browserHash": "97550751",
+  "browserHash": "6dc5b351",
   "optimized": {
     "react": {
       "src": "../../react/index.js",
       "file": "react.js",
-      "fileHash": "218bf1d5",
+      "fileHash": "3a9d2efb",
       "needsInterop": true
     },
     "react-dom": {
       "src": "../../react-dom/index.js",
       "file": "react-dom.js",
-      "fileHash": "b355103a",
+      "fileHash": "a6195955",
       "needsInterop": true
     },
     "react/jsx-dev-runtime": {
       "src": "../../react/jsx-dev-runtime.js",
       "file": "react_jsx-dev-runtime.js",
-      "fileHash": "cef655ef",
+      "fileHash": "1a627eef",
       "needsInterop": true
     },
     "react/jsx-runtime": {
       "src": "../../react/jsx-runtime.js",
       "file": "react_jsx-runtime.js",
-      "fileHash": "26e53c00",
+      "fileHash": "c6ef57ca",
       "needsInterop": true
     },
     "react-dom/client": {
       "src": "../../react-dom/client.js",
       "file": "react-dom_client.js",
-      "fileHash": "2b421505",
+      "fileHash": "8aa33148",
       "needsInterop": true
     }
   },

+ 22 - 0
preload.cjs

@@ -28,5 +28,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
   readProcessingJson: (folderName) => ipcRenderer.invoke('read-processing-json', folderName),
   // 文字识别并获取坐标
   findTextAndGetCoordinate: (ipPort, targetText) => ipcRenderer.invoke('find-text-and-get-coordinate', ipPort, targetText),
+  // OCR识别最后一条消息(兼容旧API)
+  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),
+  // 获取最后一条消息(带发送者信息)
+  getLastChatMessage: (ipPort, friendAvatarPath, myAvatarPath) => ipcRenderer.invoke('get-last-chat-message', ipPort, friendAvatarPath, myAvatarPath),
+  // 保存工作流(支持图片)
+  saveWorkflow: (workflowJson, imagesData) => ipcRenderer.invoke('save-workflow', workflowJson, imagesData),
+  // 删除工作流
+  deleteWorkflow: (folderName) => ipcRenderer.invoke('delete-workflow', folderName),
+  // 确保目录存在
+  ensureDirectory: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath),
+  // 写入文本文件
+  writeTextFile: (filePath, content) => ipcRenderer.invoke('write-text-file', filePath, content),
+  // 读取文本文件
+  readTextFile: (filePath) => ipcRenderer.invoke('read-text-file', filePath),
+  // 保存聊天记录
+  saveChatHistory: (workflowFolderPath, historyData) => ipcRenderer.invoke('save-chat-history', workflowFolderPath, historyData),
+  // 保存聊天记录总结
+  saveChatHistorySummary: (workflowFolderPath, summary) => ipcRenderer.invoke('save-chat-history-summary', workflowFolderPath, summary),
+  // 获取聊天记录总结
+  getChatHistorySummary: (workflowFolderPath) => ipcRenderer.invoke('get-chat-history-summary', workflowFolderPath),
 });
 

BIN
py/extract-chat-history.py


BIN
py/img-reg.py


BIN
py/string-reg-location.py


+ 0 - 124
py/test-to-img.py

@@ -1,124 +0,0 @@
-"""
-文字转图片模块
-功能:将文字转换为图片,用于图像匹配
-"""
-
-import sys
-import os
-from PIL import Image, ImageDraw, ImageFont
-from pathlib import Path
-import tempfile
-
-
-def text_to_image(
-    text: str,
-    output_path: str = None,
-    font_size: int = 50,
-    font_path: str = None,
-    text_color: tuple = (0, 0, 0),  # 黑色
-    bg_color: tuple = (255, 255, 255),  # 白色背景
-    padding: int = 10
-) -> str:
-    """
-    将文字转换为图片
-    
-    Args:
-        text: 要转换的文字
-        output_path: 输出图片路径,如果为 None 则使用临时文件
-        font_size: 字体大小(像素)
-        font_path: 字体文件路径,如果为 None 则使用系统默认字体
-        text_color: 文字颜色 RGB 元组,默认黑色
-        bg_color: 背景颜色 RGB 元组,默认白色
-        padding: 图片内边距(像素)
-    
-    Returns:
-        生成的图片文件路径
-    """
-    if not text or not text.strip():
-        raise ValueError("文字内容不能为空")
-    
-    # 尝试加载字体
-    try:
-        if font_path and os.path.exists(font_path):
-            font = ImageFont.truetype(font_path, font_size)
-        else:
-            # Windows 系统默认字体
-            if sys.platform == 'win32':
-                # 尝试使用微软雅黑
-                font_paths = [
-                    'C:/Windows/Fonts/msyh.ttc',  # 微软雅黑
-                    'C:/Windows/Fonts/simhei.ttf',  # 黑体
-                    'C:/Windows/Fonts/simsun.ttc',  # 宋体
-                ]
-                font = None
-                for fp in font_paths:
-                    if os.path.exists(fp):
-                        try:
-                            font = ImageFont.truetype(fp, font_size)
-                            break
-                        except:
-                            continue
-                if font is None:
-                    font = ImageFont.load_default()
-            else:
-                font = ImageFont.load_default()
-    except Exception as e:
-        print(f"警告: 无法加载指定字体,使用默认字体: {e}")
-        font = ImageFont.load_default()
-    
-    # 创建临时画布来测量文字尺寸
-    temp_img = Image.new('RGB', (1, 1))
-    temp_draw = ImageDraw.Draw(temp_img)
-    
-    # 获取文字边界框
-    bbox = temp_draw.textbbox((0, 0), text, font=font)
-    text_width = bbox[2] - bbox[0]
-    text_height = bbox[3] - bbox[1]
-    
-    # 计算图片尺寸(添加 padding)
-    img_width = text_width + padding * 2
-    img_height = text_height + padding * 2
-    
-    # 创建图片
-    img = Image.new('RGB', (img_width, img_height), bg_color)
-    draw = ImageDraw.Draw(img)
-    
-    # 绘制文字(居中)
-    x = padding
-    y = padding
-    draw.text((x, y), text, font=font, fill=text_color)
-    
-    # 保存图片
-    if output_path is None:
-        # 使用临时文件
-        temp_dir = tempfile.gettempdir()
-        output_path = os.path.join(temp_dir, f'temp_text_{os.getpid()}_{hash(text)}.png')
-    
-    # 确保输出目录存在
-    output_dir = os.path.dirname(output_path)
-    if output_dir and not os.path.exists(output_dir):
-        os.makedirs(output_dir, exist_ok=True)
-    
-    img.save(output_path, 'PNG')
-    return output_path
-
-
-if __name__ == "__main__":
-    # 测试示例
-    if len(sys.argv) < 2:
-        print("用法: python test-to-img.py <文字内容> [输出路径] [字体大小] [字体路径]")
-        print("示例: python test-to-img.py \"测试文字\" output.png 50")
-        sys.exit(1)
-    
-    text = sys.argv[1]
-    output_path = sys.argv[2] if len(sys.argv) > 2 else None
-    font_size = int(sys.argv[3]) if len(sys.argv) > 3 else 50
-    font_path = sys.argv[4] if len(sys.argv) > 4 else None
-    
-    try:
-        result_path = text_to_image(text, output_path, font_size, font_path)
-        print(f"成功生成图片: {result_path}")
-        print(f"JSON格式: {{\"path\": \"{result_path}\"}}")
-    except Exception as e:
-        print(f"错误: {e}")
-        sys.exit(1)

BIN
py/venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/Image.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/ImageText.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/_util.cpython-312.pyc


BIN
py/venv/Lib/site-packages/PIL/__pycache__/_version.cpython-312.pyc


BIN
py/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/__pycache__/load_config_py3.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/__pycache__/version.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/data/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/gapi/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/mat_wrapper/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/misc/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/misc/__pycache__/version.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/typing/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/cv2/utils/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/__config__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/_array_api_info.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/_distributor_init.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/_expired_attrs_2_0.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/_globals.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/_pytesttester.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/dtypes.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/exceptions.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/__pycache__/version.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_add_newdocs.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_add_newdocs_scalars.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_asarray.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_dtype.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_dtype_ctypes.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_exceptions.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_internal.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_machar.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_methods.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_string_helpers.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_type_aliases.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/_ufunc_config.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/arrayprint.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/einsumfunc.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/fromnumeric.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/function_base.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/getlimits.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/memmap.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/multiarray.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/numeric.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/numerictypes.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/overrides.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/printoptions.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/records.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/shape_base.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_core/__pycache__/umath.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_array_like.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_char_codes.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_dtype_like.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nbit.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nbit_base.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_nested_sequence.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_scalars.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_shape.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_typing/__pycache__/_ufunc.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_utils/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_utils/__pycache__/_convertions.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/_utils/__pycache__/_inspect.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/core/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/core/__pycache__/_utils.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/core/__pycache__/multiarray.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/__init__.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_array_utils_impl.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_arraypad_impl.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_arraysetops_impl.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_arrayterator_impl.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_datasource.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_function_base_impl.cpython-312.pyc


BIN
py/venv/Lib/site-packages/numpy/lib/__pycache__/_histograms_impl.cpython-312.pyc


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików