/** * 文件读写操作模块 * 负责文件系统的读写操作,包括聊天记录的保存和读取 */ import { ipcMain } from 'electron'; import { readdir, writeFile, readFile, mkdir, rm, stat, appendFile } from 'fs/promises'; import { join, dirname, isAbsolute } from 'path'; /** * 递归计算目录大小 * @param {string} dirPath - 目录路径 * @returns {Promise} 目录总大小(字节) */ async function calculateDirSize(dirPath) { let totalSize = 0; try { const entries = await readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = join(dirPath, entry.name); try { if (entry.isFile()) { const stats = await stat(entryPath); totalSize += stats.size; } else if (entry.isDirectory()) { totalSize += await calculateDirSize(entryPath); } } catch (e) { // 忽略无法访问的文件/目录 } } } catch (e) { // 忽略无法读取的目录 } return totalSize; } import { fileURLToPath } from 'url'; import { captureScreenshot } from './adb/screenshot.js'; import { getDeviceResolution } from './adb/device-info.js'; import { extractChatHistory as extractChatHistoryFromFunc, getLastMessage as getLastMessageFromFunc, ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * 获取最新的聊天记录文件 * @param {string} absoluteWorkflowPath - 工作流文件夹的绝对路径 * @returns {Promise} 最新的文件信息,如果不存在则返回 null */ async function getLatestHistoryFile(absoluteWorkflowPath) { try { const historyDir = join(absoluteWorkflowPath, 'history'); const files = await readdir(historyDir, { withFileTypes: true }); // 过滤出 JSON 文件(chat_*.json) const jsonFiles = files .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json')) .map(file => ({ name: file.name, path: join(historyDir, file.name) })); if (jsonFiles.length === 0) { return null; } // 获取所有文件的统计信息(修改时间) const filesWithStats = await Promise.all( jsonFiles.map(async (file) => { const stats = await stat(file.path); return { ...file, mtime: stats.mtime }; }) ); // 按修改时间排序(最新的在前) filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // 返回最新的文件 return filesWithStats[0]; } catch (error) { // 如果目录不存在或读取失败,返回 null return null; } } /** * 将相对路径转换为绝对路径 * @param {string} workflowFolderPath - 工作流文件夹路径(相对或绝对) * @returns {string} 绝对路径 */ function getAbsoluteWorkflowPath(workflowFolderPath) { if (isAbsolute(workflowFolderPath)) { return workflowFolderPath; } if (workflowFolderPath.startsWith('static/processing/')) { const folderName = workflowFolderPath.replace('static/processing/', ''); return join(__dirname, '..', 'static', 'processing', folderName); } else if (workflowFolderPath.startsWith('static\\processing\\')) { const folderName = workflowFolderPath.replace('static\\processing\\', ''); return join(__dirname, '..', 'static', 'processing', folderName); } else { return join(__dirname, '..', 'static', 'processing', workflowFolderPath); } } /** * 注册文件读写相关的 IPC handlers */ export function registerIpcHandlers() { // 确保目录存在 ipcMain.handle('ensure-directory', async (event, dirPath) => { try { await mkdir(dirPath, { recursive: true }); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }); // 写入文本文件 ipcMain.handle('write-text-file', async (event, filePath, content) => { try { // 如果 filePath 是绝对路径,直接使用 // 否则,相对于项目根目录(__dirname 的父目录) let absoluteFilePath = filePath; if (!isAbsolute(filePath)) { // 相对路径,相对于项目根目录 absoluteFilePath = join(__dirname, '..', filePath); } // 确保目录存在 const dir = dirname(absoluteFilePath); await mkdir(dir, { recursive: true }); await writeFile(absoluteFilePath, content, 'utf-8'); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }); // 读取文本文件 ipcMain.handle('read-text-file', async (event, filePath) => { try { // 如果 filePath 是绝对路径,直接使用 // 否则,相对于项目根目录(__dirname 的父目录) let absoluteFilePath = filePath; if (!isAbsolute(filePath)) { // 相对路径,相对于项目根目录 absoluteFilePath = join(__dirname, '..', filePath); } const content = await readFile(absoluteFilePath, 'utf-8'); return { success: true, content }; } catch (error) { // 如果文件不存在,返回空字符串而不是错误 if (error.code === 'ENOENT') { return { success: true, content: '' }; } return { success: false, error: error.message }; } }); // 读取图片文件为 base64 ipcMain.handle('read-image-file-as-base64', async (event, filePath) => { try { // 如果 filePath 是绝对路径,直接使用 // 否则,相对于项目根目录 let absoluteFilePath = filePath; if (!isAbsolute(filePath)) { absoluteFilePath = join(__dirname, '..', filePath); } // 读取文件为 Buffer const fileBuffer = await readFile(absoluteFilePath); // 转换为 base64 const base64 = fileBuffer.toString('base64'); return { success: true, data: base64 }; } catch (error) { return { success: false, error: error.message }; } }); // 保存 base64 图片到文件 ipcMain.handle('save-base64-image', async (event, base64Data, savePath) => { try { // 处理保存路径 let absoluteSavePath = savePath; if (!isAbsolute(savePath)) { // 如果是相对路径,相对于项目根目录 if (savePath.startsWith('static/processing/')) { absoluteSavePath = join(__dirname, '..', savePath); } else { absoluteSavePath = join(__dirname, '..', savePath); } } // 确保目录存在 const dir = dirname(absoluteSavePath); await mkdir(dir, { recursive: true }); // 将 base64 转换为 Buffer 并保存 const imageBuffer = Buffer.from(base64Data, 'base64'); await writeFile(absoluteSavePath, imageBuffer); // 验证文件是否成功写入(检查文件大小) const { access, constants } = await import('fs/promises'); await access(absoluteSavePath, constants.F_OK); const fileStats = await stat(absoluteSavePath); if (fileStats.size === 0) { return { success: false, error: '文件保存失败:文件大小为0' }; } if (fileStats.size < 100) { // JPEG/PNG 文件至少应该有几百字节 return { success: false, error: `文件保存失败:文件大小异常(${fileStats.size} 字节)` }; } return { success: true, fileSize: fileStats.size }; } catch (error) { return { success: false, error: error.message }; } }); // 追加日志到文件 ipcMain.handle('append-log', async (event, workflowFolderPath, logMessage) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const logFilePath = join(absoluteWorkflowPath, 'log.txt'); // 添加时间戳(精确到毫秒) const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); const second = String(now.getSeconds()).padStart(2, '0'); const ms = String(now.getMilliseconds()).padStart(3, '0'); const timestamp = `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`; const logLine = `[${timestamp}] ${logMessage}\n`; await appendFile(logFilePath, logLine, 'utf-8'); return { success: true }; } catch (error) { // 日志写入失败不应该影响主流程 return { success: false, error: error.message }; } }); // 保存聊天记录到 chat-history.txt(追加模式,所有记录保存在一个文件) // 格式:{"data":"时间戳","friend":"消息","me":"消息"},允许重复键(虽然JSON不支持,但保存为文本格式) ipcMain.handle('save-chat-history-txt', async (event, workflowFolderPath, messages) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const historyDir = join(absoluteWorkflowPath, 'history'); await mkdir(historyDir, { recursive: true }); const chatHistoryPath = join(historyDir, 'chat-history.txt'); // 读取现有文件(如果存在) // 由于JSON不支持重复键,我们需要从文本解析每一行 let existingEntries = []; try { const existingContent = await readFile(chatHistoryPath, 'utf-8'); if (existingContent.trim()) { // 使用正则表达式解析每一行:\t"key":"value", const lines = existingContent.split('\n'); for (const line of lines) { // 匹配格式:\t"key":"value", 或 \t"key":"value" const match = line.match(/^\s*"([^"]+)":\s*"([^"]*)"\s*,?\s*$/); if (match) { existingEntries.push({ type: match[1], value: match[2] }); } } } } catch (e) { // 文件不存在或格式错误,使用空数组 existingEntries = []; } // 获取当前时间戳(格式:昨天 HH:MM 或 今天 HH:MM) const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); let timeLabel = ''; const hour = now.getHours(); const minute = now.getMinutes(); if (now >= today) { timeLabel = `今天 ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; } else if (now >= yesterday) { timeLabel = `昨天 ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; } else { timeLabel = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; } // 合并现有数据和新消息 const mergedEntries = [...existingEntries]; // 添加时间戳 mergedEntries.push({ type: 'data', value: timeLabel }); // 添加新消息 for (const msg of messages) { const sender = msg.sender || msg.role || ''; const text = (msg.text || msg.message || '').trim(); if (text && (sender === 'friend' || sender === 'me')) { mergedEntries.push({ type: sender, value: text }); } } // 格式化为用户要求的格式(对象格式,允许重复键) // 由于JSON不支持重复键,我们保存为格式化的文本,模拟对象格式 const formattedLines = []; formattedLines.push('{'); for (let i = 0; i < mergedEntries.length; i++) { const item = mergedEntries[i]; const key = JSON.stringify(item.type); const value = JSON.stringify(item.value); const comma = i < mergedEntries.length - 1 ? ',' : ''; formattedLines.push(`\t${key}:${value}${comma}`); } formattedLines.push('}'); // 保存为格式化的文本 await writeFile(chatHistoryPath, formattedLines.join('\n'), 'utf-8'); return { success: true, filePath: chatHistoryPath }; } catch (error) { return { success: false, error: error.message }; } }); // 保存聊天记录到 history 文件夹(旧格式,保持向后兼容) ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const historyDir = join(absoluteWorkflowPath, 'history'); await mkdir(historyDir, { recursive: true }); // 获取最新的历史文件 const latestFile = await getLatestHistoryFile(absoluteWorkflowPath); let targetFilePath = null; let mergedMessages = []; // 如果存在最新文件,检查是否可以追加 if (latestFile) { try { const latestStats = await stat(latestFile.path); const latestFileSize = latestStats.size; // 文件大小(字节) // 检查文件大小是否超过1MB(1MB = 1024 * 1024 字节) if (latestFileSize < 1024 * 1024) { // 文件大小未超过1MB,可以追加 const latestContent = await readFile(latestFile.path, 'utf-8'); const latestData = JSON.parse(latestContent); const latestMessages = latestData.messages || []; const newMessages = historyData.messages || []; // 简单合并消息(不去重,去重逻辑在 saveChatHistory 中处理) mergedMessages = [...latestMessages, ...newMessages]; targetFilePath = latestFile.path; } else { // 文件大小超过1MB,创建新文件 mergedMessages = historyData.messages || []; } } catch (compareError) { // 读取最新文件失败,将创建新文件 mergedMessages = historyData.messages || []; } } else { // 不存在历史文件,直接使用新消息 mergedMessages = historyData.messages || []; } // 如果还没有确定目标文件路径,创建新文件 if (!targetFilePath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); const fileName = `chat_${timestamp}.json`; targetFilePath = join(historyDir, fileName); } // 构建保存的数据结构 const now = new Date(); const finalData = { timestamp: now.toISOString(), messageCount: mergedMessages.length, messages: mergedMessages }; // 保存到文件 await writeFile(targetFilePath, JSON.stringify(finalData, null, 2), 'utf-8'); // 限制 history 文件夹总大小不超过10MB,超过则删除最早的文件 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) })); // 获取所有文件的大小和统计信息 const filesWithStats = await Promise.all( jsonFiles.map(async (file) => { const stats = await stat(file.path); return { ...file, size: stats.size, birthtime: stats.birthtime || stats.mtime, mtime: stats.mtime }; }) ); // 计算总大小 let totalSize = filesWithStats.reduce((sum, file) => sum + file.size, 0); const maxSize = 10 * 1024 * 1024; // 10MB // 按时间排序(最早的在前) filesWithStats.sort((a, b) => { const timeA = a.birthtime.getTime(); const timeB = b.birthtime.getTime(); return timeA - timeB; }); // 如果总大小超过10MB,删除最早的文件直到总大小小于10MB const filesToDelete = []; for (const file of filesWithStats) { if (totalSize <= maxSize) { break; } filesToDelete.push(file); totalSize -= file.size; } // 删除文件 for (const file of filesToDelete) { try { await rm(file.path, { force: true }); } catch (deleteError) { // 删除文件失败,忽略错误 } } } catch (cleanupError) { // 清理失败不影响保存操作 } return { success: true, filePath: targetFilePath }; } catch (error) { return { success: false, error: error.message }; } }); // 保存聊天记录总结 ipcMain.handle('save-chat-history-summary', async (event, workflowFolderPath, summary) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const historyDir = join(absoluteWorkflowPath, 'history'); await mkdir(historyDir, { recursive: true }); const summaryFilePath = join(historyDir, 'summary.txt'); await writeFile(summaryFilePath, summary, 'utf-8'); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }); // 读取聊天记录总结 ipcMain.handle('get-chat-history-summary', async (event, workflowFolderPath) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const summaryFilePath = join(absoluteWorkflowPath, 'history', 'summary.txt'); // 检查文件是否存在 try { const { access, constants } = await import('fs/promises'); await access(summaryFilePath, constants.F_OK); const summary = await readFile(summaryFilePath, 'utf-8'); return { success: true, summary: summary.trim() }; } catch (error) { // 文件不存在,返回空字符串 return { success: true, summary: '' }; } } catch (error) { return { success: false, error: error.message }; } }); // 读取最新聊天记录的所有消息 ipcMain.handle('read-latest-chat-history', async (event, workflowFolderPath) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); // 获取最新的聊天记录文件 const latestFile = await getLatestHistoryFile(absoluteWorkflowPath); if (!latestFile) { return { success: false, error: '未找到聊天记录文件' }; } // 读取文件内容 const fileContent = await readFile(latestFile.path, 'utf-8'); const historyData = JSON.parse(fileContent); // 返回消息数组 return { success: true, messages: historyData.messages || [] }; } catch (error) { return { success: false, error: error.message }; } }); // 读取最新聊天记录的最后一条消息 ipcMain.handle('read-last-message', async (event, workflowFolderPath) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); // 获取最新的聊天记录文件 const latestFile = await getLatestHistoryFile(absoluteWorkflowPath); if (!latestFile) { return { success: false, error: '未找到聊天记录文件' }; } // 读取文件内容 const fileContent = await readFile(latestFile.path, 'utf-8'); const historyData = JSON.parse(fileContent); // 获取最后一条消息 const messages = historyData.messages || []; if (messages.length === 0) { return { success: false, error: '聊天记录为空' }; } const lastMessage = messages[messages.length - 1]; // 返回最后一条消息的文本和发送者 return { success: true, text: lastMessage.text || '', sender: lastMessage.sender || '' }; } catch (error) { return { success: false, error: error.message }; } }); // 读取所有聊天记录(合并所有历史文件)- 向后兼容 ipcMain.handle('read-all-chat-history', async (event, workflowFolderPath) => { // 向后兼容,重定向到 read-chat-history(使用相同的实现) try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const historyDir = join(absoluteWorkflowPath, 'history'); // 检查目录是否存在 try { const files = await readdir(historyDir, { withFileTypes: true }); // 过滤出 JSON 文件(chat_*.json) const jsonFiles = files .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json')) .map(file => ({ name: file.name, path: join(historyDir, file.name) })); if (jsonFiles.length === 0) { return { success: true, messages: [] }; // 没有文件时返回空数组 } // 获取所有文件的统计信息(修改时间),用于排序 const filesWithStats = await Promise.all( jsonFiles.map(async (file) => { const stats = await stat(file.path); return { ...file, mtime: stats.mtime }; }) ); // 按修改时间排序(最早的在前,保持时间顺序) filesWithStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime()); // 读取所有文件并合并消息 const allMessages = []; for (const file of filesWithStats) { try { const fileContent = await readFile(file.path, 'utf-8'); const historyData = JSON.parse(fileContent); const messages = historyData.messages || []; allMessages.push(...messages); } catch (error) { // 读取聊天记录文件失败,继续处理其他文件 } } 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) { return { success: false, error: error.message }; } }); // 读取聊天记录(合并所有历史文件) ipcMain.handle('read-chat-history', async (event, workflowFolderPath) => { try { const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath); const historyDir = join(absoluteWorkflowPath, 'history'); // 检查目录是否存在 try { const files = await readdir(historyDir, { withFileTypes: true }); // 过滤出 JSON 文件(chat_*.json) const jsonFiles = files .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json')) .map(file => ({ name: file.name, path: join(historyDir, file.name) })); if (jsonFiles.length === 0) { return { success: true, messages: [] }; // 没有文件时返回空数组 } // 获取所有文件的统计信息(修改时间),用于排序 const filesWithStats = await Promise.all( jsonFiles.map(async (file) => { const stats = await stat(file.path); return { ...file, mtime: stats.mtime }; }) ); // 按修改时间排序(最早的在前,保持时间顺序) filesWithStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime()); // 读取所有文件并合并消息 const allMessages = []; for (const file of filesWithStats) { try { const fileContent = await readFile(file.path, 'utf-8'); const historyData = JSON.parse(fileContent); const messages = historyData.messages || []; allMessages.push(...messages); } catch (error) { // 读取聊天记录文件失败,继续处理其他文件 } } 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) { return { success: false, error: error.message }; } }); // 提取聊天记录(通过OCR) ipcMain.handle('extract-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath, region = null, friendRgb = null, myRgb = null) => { return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath, region, friendRgb, myRgb); }); // 获取最后一条消息 ipcMain.handle('get-last-chat-message', async (event, ipPort, friendAvatarPath, myAvatarPath) => { return await getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath); }); // OCR 聊天记录(向后兼容,重定向到 extract-chat-history) ipcMain.handle('ocr-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => { return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath); }); } /** * 提取聊天记录(通过OCR) * @param {string} ipPort - 设备 ID/IP:Port * @param {string} friendAvatarPath - 好友头像路径 * @param {string} myAvatarPath - 我的头像路径 * @param {string} workflowFolderPath - 工作流文件夹路径(可选) * @param {Object} region - 识别区域(可选,包含四个顶点坐标的 corners 对象) * @param {string} friendRgb - 好友对话框RGB颜色(格式:"(r,g,b)") * @param {string} myRgb - 我的对话框RGB颜色(格式:"(r,g,b)") * @returns {Promise<{success: boolean, error?: string, messages?: Array}>} */ export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath = null, region = null, friendRgb = null, myRgb = null) { try { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } // 1. 获取设备分辨率 const resolutionResult = await getDeviceResolution(ipPort); if (!resolutionResult.success) { return { success: false, error: '获取设备分辨率失败' }; } const { width, height } = resolutionResult; // 2. 获取屏幕截图 const screenshotResult = await captureScreenshot(ipPort, { format: 'png' }); if (!screenshotResult.success || !screenshotResult.data) { return { success: false, error: '获取屏幕截图失败' }; } // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录) let screenshotPath; let tmpDir = null; // 用于跟踪需要删除的临时目录 if (workflowFolderPath) { // 确保 workflowFolderPath 是绝对路径 let absoluteWorkflowPath = workflowFolderPath; if (!isAbsolute(workflowFolderPath)) { // 如果已经是 static/processing/xxx 格式,直接拼接(去掉开头的 static/processing) if (workflowFolderPath.startsWith('static/processing/')) { const folderName = workflowFolderPath.replace('static/processing/', ''); absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName); } else if (workflowFolderPath.startsWith('static\\processing\\')) { const folderName = workflowFolderPath.replace('static\\processing\\', ''); absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName); } else { // 如果只是文件夹名,需要加上 static/processing absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath); } } const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_'); tmpDir = join(absoluteWorkflowPath, 'tmp', timestamp); await mkdir(tmpDir, { recursive: true }); screenshotPath = join(tmpDir, 'screenshot.png'); } else { const tempDir = join(__dirname, '..'); screenshotPath = join(tempDir, 'temp_screenshot_chat.png'); } const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64'); await writeFile(screenshotPath, screenshotBuffer); // 验证文件是否成功写入 try { const { access, constants } = await import('fs/promises'); await access(screenshotPath, constants.F_OK); // 截图已保存日志(不显示) } catch (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 regionArg = region ? JSON.stringify(region) : 'None'; const result = await extractChatHistoryFromFunc(screenshotPath, friendAvatarArg, myAvatarArg, width, height, workflowFolderArg, regionArg, friendRgb, myRgb); return result; } finally { // 5. 使用完后,先判断 tmp 目录总大小是否超过 20MB,再决定是否删除临时目录 if (tmpDir) { try { // 获取 tmp 目录的父目录(工作流目录下的 tmp 文件夹) const tmpParentDir = dirname(tmpDir); // 检查 tmp 目录下所有文件和子目录的总大小 let totalSize = 0; const filesToDelete = []; try { const entries = await readdir(tmpParentDir, { withFileTypes: true }); for (const entry of entries) { const entryPath = join(tmpParentDir, entry.name); try { if (entry.isFile()) { const stats = await stat(entryPath); totalSize += stats.size; } else if (entry.isDirectory()) { // 递归计算子目录大小 const dirSize = await calculateDirSize(entryPath); totalSize += dirSize; filesToDelete.push({ path: entryPath, size: dirSize, isDir: true }); } } catch (e) { // 忽略无法访问的文件 } } } catch (e) { // 如果无法读取目录,直接删除临时目录 await rm(tmpDir, { recursive: true, force: true }); return; } const maxSize = 20 * 1024 * 1024; // 20MB // 如果总大小超过 20MB,删除时间最早的目录/文件 if (totalSize > maxSize) { // 获取所有目录的修改时间并排序 const dirsWithTime = []; for (const item of filesToDelete) { if (item.isDir) { try { const stats = await stat(item.path); dirsWithTime.push({ ...item, mtime: stats.mtime }); } catch (e) { // 忽略无法访问的目录 } } } // 按修改时间排序(最早的在前) dirsWithTime.sort((a, b) => a.mtime - b.mtime); // 删除最早的目录直到总大小小于 20MB for (const dirInfo of dirsWithTime) { if (totalSize <= maxSize) { break; } try { await rm(dirInfo.path, { recursive: true, force: true }); totalSize -= dirInfo.size; } catch (e) { // 忽略删除失败 } } } // 如果临时目录仍然存在且总大小未超过限制,也删除它(保持原有行为) try { const tmpDirExists = await stat(tmpDir).then(() => true).catch(() => false); if (tmpDirExists) { await rm(tmpDir, { recursive: true, force: true }); } } catch (rmError) { // 忽略删除失败 } } catch (error) { // 清理失败不影响主流程 } } } } catch (error) { if (error.message && error.message.includes('timeout')) { return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' }; } return { success: false, error: error.message }; } } /** * 获取最后一条消息(带发送者信息) * @param {string} ipPort - 设备 ID/IP:Port * @param {string} friendAvatarPath - 好友头像路径 * @param {string} myAvatarPath - 我的头像路径 * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>} */ export async function getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath) { try { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } // 1. 获取设备分辨率 const resolutionResult = await getDeviceResolution(ipPort); if (!resolutionResult.success) { return { success: false, error: '获取设备分辨率失败' }; } const { width, height } = resolutionResult; // 2. 获取屏幕截图 const screenshotResult = await captureScreenshot(ipPort, { format: 'png' }); if (!screenshotResult.success || !screenshotResult.data) { return { success: false, error: '获取屏幕截图失败' }; } // 3. 保存截图到临时文件 const tempDir = join(__dirname, '..'); const screenshotPath = join(tempDir, 'temp_screenshot_chat.png'); const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64'); await writeFile(screenshotPath, screenshotBuffer); // 4. 调用 JS 实现获取最后一条消息 // 转换头像路径为绝对路径 let friendAvatarArg = null; if (friendAvatarPath) { if (isAbsolute(friendAvatarPath)) { friendAvatarArg = friendAvatarPath; } else { friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath); } } let myAvatarArg = null; if (myAvatarPath) { if (isAbsolute(myAvatarPath)) { myAvatarArg = myAvatarPath; } else { myAvatarArg = join(__dirname, '..', 'static', 'processing', myAvatarPath); } } const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/'); const normalizedFriendAvatar = friendAvatarArg ? friendAvatarArg.replace(/\\/g, '/') : null; const normalizedMyAvatar = myAvatarArg ? myAvatarArg.replace(/\\/g, '/') : null; const result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height); if (result.success) { } return result; } catch (error) { if (error.message && error.message.includes('timeout')) { return { success: false, error: '获取最后一条消息超时,请检查网络连接或稍后重试' }; } return { success: false, error: error.message }; } }