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