|
|
@@ -0,0 +1,611 @@
|
|
|
+/**
|
|
|
+ * 文件读写操作模块
|
|
|
+ * 负责文件系统的读写操作,包括聊天记录的保存和读取
|
|
|
+ */
|
|
|
+
|
|
|
+import { ipcMain } from 'electron';
|
|
|
+import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
|
|
|
+import { join, dirname, isAbsolute } from 'path';
|
|
|
+import { fileURLToPath } from 'url';
|
|
|
+import { captureScreenshot } from './adb/screenshot.js';
|
|
|
+import { getDeviceResolution } from './adb/device-info.js';
|
|
|
+import { extractChatHistory as extractChatHistoryFromFunc, getLastMessage as getLastMessageFromFunc, ocrFullScreen as ocrFullScreenFromFunc } from './func/ocr-chat-history.js';
|
|
|
+
|
|
|
+const __filename = fileURLToPath(import.meta.url);
|
|
|
+const __dirname = dirname(__filename);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取最新的聊天记录文件
|
|
|
+ * @param {string} absoluteWorkflowPath - 工作流文件夹的绝对路径
|
|
|
+ * @returns {Promise<Object|null>} 最新的文件信息,如果不存在则返回 null
|
|
|
+ */
|
|
|
+async function getLatestHistoryFile(absoluteWorkflowPath) {
|
|
|
+ try {
|
|
|
+ const historyDir = join(absoluteWorkflowPath, 'history');
|
|
|
+ const files = await readdir(historyDir, { withFileTypes: true });
|
|
|
+
|
|
|
+ // 过滤出 JSON 文件(chat_*.json)
|
|
|
+ const jsonFiles = files
|
|
|
+ .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
|
|
|
+ .map(file => ({
|
|
|
+ name: file.name,
|
|
|
+ path: join(historyDir, file.name)
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (jsonFiles.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取所有文件的统计信息(修改时间)
|
|
|
+ const filesWithStats = await Promise.all(
|
|
|
+ jsonFiles.map(async (file) => {
|
|
|
+ const stats = await stat(file.path);
|
|
|
+ return {
|
|
|
+ ...file,
|
|
|
+ mtime: stats.mtime
|
|
|
+ };
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ // 按修改时间排序(最新的在前)
|
|
|
+ filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
|
+
|
|
|
+ // 返回最新的文件
|
|
|
+ return filesWithStats[0];
|
|
|
+ } catch (error) {
|
|
|
+ // 如果目录不存在或读取失败,返回 null
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将相对路径转换为绝对路径
|
|
|
+ * @param {string} workflowFolderPath - 工作流文件夹路径(相对或绝对)
|
|
|
+ * @returns {string} 绝对路径
|
|
|
+ */
|
|
|
+function getAbsoluteWorkflowPath(workflowFolderPath) {
|
|
|
+ if (isAbsolute(workflowFolderPath)) {
|
|
|
+ return workflowFolderPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (workflowFolderPath.startsWith('static/processing/')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static/processing/', '');
|
|
|
+ return join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else if (workflowFolderPath.startsWith('static\\processing\\')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static\\processing\\', '');
|
|
|
+ return join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else {
|
|
|
+ return join(__dirname, '..', 'static', 'processing', workflowFolderPath);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 注册文件读写相关的 IPC handlers
|
|
|
+ */
|
|
|
+export function registerIpcHandlers() {
|
|
|
+ // 确保目录存在
|
|
|
+ ipcMain.handle('ensure-directory', async (event, dirPath) => {
|
|
|
+ try {
|
|
|
+ await mkdir(dirPath, { recursive: true });
|
|
|
+ return { success: true };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 写入文本文件
|
|
|
+ ipcMain.handle('write-text-file', async (event, filePath, content) => {
|
|
|
+ try {
|
|
|
+ await writeFile(filePath, content, 'utf-8');
|
|
|
+ return { success: true };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取文本文件
|
|
|
+ ipcMain.handle('read-text-file', async (event, filePath) => {
|
|
|
+ try {
|
|
|
+ const content = await readFile(filePath, 'utf-8');
|
|
|
+ return { success: true, content };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存聊天记录到 history 文件夹
|
|
|
+ ipcMain.handle('save-chat-history', async (event, workflowFolderPath, historyData) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+ const historyDir = join(absoluteWorkflowPath, 'history');
|
|
|
+ await mkdir(historyDir, { recursive: true });
|
|
|
+
|
|
|
+ // 获取最新的历史文件
|
|
|
+ const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
|
|
|
+
|
|
|
+ // 如果存在最新文件,对比内容
|
|
|
+ if (latestFile) {
|
|
|
+ try {
|
|
|
+ const latestContent = await readFile(latestFile.path, 'utf-8');
|
|
|
+ const latestData = JSON.parse(latestContent);
|
|
|
+
|
|
|
+ // 对比消息数量:如果新的消息数量没有增加,不保存
|
|
|
+ if (latestData.messages && Array.isArray(latestData.messages)) {
|
|
|
+ const latestMessageCount = latestData.messages.length;
|
|
|
+ const newMessageCount = historyData.messages ? historyData.messages.length : 0;
|
|
|
+
|
|
|
+ if (newMessageCount <= latestMessageCount) {
|
|
|
+ console.log(`聊天记录未增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),跳过保存`);
|
|
|
+ return { success: true, skipped: true, reason: 'no_new_messages' };
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`聊天记录已增加(最新文件: ${latestMessageCount} 条,当前: ${newMessageCount} 条),将保存`);
|
|
|
+ }
|
|
|
+ } catch (compareError) {
|
|
|
+ console.warn('对比历史文件失败,继续保存:', compareError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成文件名(使用时间戳)
|
|
|
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
|
+ const fileName = `chat_${timestamp}.json`;
|
|
|
+ const filePath = join(historyDir, fileName);
|
|
|
+
|
|
|
+ // 保存到文件
|
|
|
+ await writeFile(filePath, JSON.stringify(historyData, null, 2), 'utf-8');
|
|
|
+
|
|
|
+ console.log(`聊天记录已保存到: ${filePath}`);
|
|
|
+
|
|
|
+ // 限制 history 文件夹中最多保存 10 个文件,超过则删除最早的文件
|
|
|
+ try {
|
|
|
+ const files = await readdir(historyDir, { withFileTypes: true });
|
|
|
+ // 过滤出 JSON 文件(chat_*.json)
|
|
|
+ const jsonFiles = files
|
|
|
+ .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
|
|
|
+ .map(file => ({
|
|
|
+ name: file.name,
|
|
|
+ path: join(historyDir, file.name)
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 如果文件数量超过 10 个,删除最早的文件
|
|
|
+ if (jsonFiles.length > 10) {
|
|
|
+ // 获取所有文件的统计信息(创建时间或修改时间)
|
|
|
+ const filesWithStats = await Promise.all(
|
|
|
+ jsonFiles.map(async (file) => {
|
|
|
+ const stats = await stat(file.path);
|
|
|
+ return {
|
|
|
+ ...file,
|
|
|
+ birthtime: stats.birthtime || stats.mtime,
|
|
|
+ mtime: stats.mtime
|
|
|
+ };
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ // 按时间排序(最早的在前)
|
|
|
+ filesWithStats.sort((a, b) => {
|
|
|
+ const timeA = a.birthtime.getTime();
|
|
|
+ const timeB = b.birthtime.getTime();
|
|
|
+ return timeA - timeB;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 删除最早的文件,直到只剩下 10 个
|
|
|
+ const filesToDelete = filesWithStats.slice(0, filesWithStats.length - 10);
|
|
|
+ for (const file of filesToDelete) {
|
|
|
+ try {
|
|
|
+ await rm(file.path, { force: true });
|
|
|
+ console.log(`已删除最早的聊天记录文件: ${file.name}`);
|
|
|
+ } catch (deleteError) {
|
|
|
+ console.warn(`删除文件失败: ${file.name}`, deleteError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (cleanupError) {
|
|
|
+ // 清理失败不影响保存操作
|
|
|
+ console.warn('清理旧聊天记录文件时出错:', cleanupError);
|
|
|
+ }
|
|
|
+
|
|
|
+ return { success: true, filePath };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存聊天记录失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存聊天记录总结
|
|
|
+ ipcMain.handle('save-chat-history-summary', async (event, workflowFolderPath, summary) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+ const historyDir = join(absoluteWorkflowPath, 'history');
|
|
|
+ await mkdir(historyDir, { recursive: true });
|
|
|
+
|
|
|
+ const summaryFilePath = join(historyDir, 'summary.txt');
|
|
|
+ await writeFile(summaryFilePath, summary, 'utf-8');
|
|
|
+
|
|
|
+ console.log(`聊天记录总结已保存到: ${summaryFilePath}`);
|
|
|
+ return { success: true };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存聊天记录总结失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取聊天记录总结
|
|
|
+ ipcMain.handle('get-chat-history-summary', async (event, workflowFolderPath) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+ const summaryFilePath = join(absoluteWorkflowPath, 'history', 'summary.txt');
|
|
|
+
|
|
|
+ // 检查文件是否存在
|
|
|
+ try {
|
|
|
+ const { access, constants } = await import('fs/promises');
|
|
|
+ await access(summaryFilePath, constants.F_OK);
|
|
|
+ const summary = await readFile(summaryFilePath, 'utf-8');
|
|
|
+ return { success: true, summary: summary.trim() };
|
|
|
+ } catch (error) {
|
|
|
+ // 文件不存在,返回空字符串
|
|
|
+ return { success: true, summary: '' };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('读取聊天记录总结失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取最新聊天记录的所有消息
|
|
|
+ ipcMain.handle('read-latest-chat-history', async (event, workflowFolderPath) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+
|
|
|
+ // 获取最新的聊天记录文件
|
|
|
+ const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
|
|
|
+
|
|
|
+ if (!latestFile) {
|
|
|
+ return { success: false, error: '未找到聊天记录文件' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件内容
|
|
|
+ const fileContent = await readFile(latestFile.path, 'utf-8');
|
|
|
+ const historyData = JSON.parse(fileContent);
|
|
|
+
|
|
|
+ // 返回消息数组
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ messages: historyData.messages || []
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('读取最新聊天记录失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取最新聊天记录的最后一条消息
|
|
|
+ ipcMain.handle('read-last-message', async (event, workflowFolderPath) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+
|
|
|
+ // 获取最新的聊天记录文件
|
|
|
+ const latestFile = await getLatestHistoryFile(absoluteWorkflowPath);
|
|
|
+
|
|
|
+ if (!latestFile) {
|
|
|
+ return { success: false, error: '未找到聊天记录文件' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件内容
|
|
|
+ const fileContent = await readFile(latestFile.path, 'utf-8');
|
|
|
+ const historyData = JSON.parse(fileContent);
|
|
|
+
|
|
|
+ // 获取最后一条消息
|
|
|
+ const messages = historyData.messages || [];
|
|
|
+ if (messages.length === 0) {
|
|
|
+ return { success: false, error: '聊天记录为空' };
|
|
|
+ }
|
|
|
+
|
|
|
+ const lastMessage = messages[messages.length - 1];
|
|
|
+
|
|
|
+ // 返回最后一条消息的文本和发送者
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ text: lastMessage.text || '',
|
|
|
+ sender: lastMessage.sender || ''
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('读取最后一条消息失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取聊天记录(合并所有历史文件)
|
|
|
+ ipcMain.handle('read-chat-history', async (event, workflowFolderPath) => {
|
|
|
+ try {
|
|
|
+ const absoluteWorkflowPath = getAbsoluteWorkflowPath(workflowFolderPath);
|
|
|
+ const historyDir = join(absoluteWorkflowPath, 'history');
|
|
|
+
|
|
|
+ // 检查目录是否存在
|
|
|
+ try {
|
|
|
+ const files = await readdir(historyDir, { withFileTypes: true });
|
|
|
+
|
|
|
+ // 过滤出 JSON 文件(chat_*.json)
|
|
|
+ const jsonFiles = files
|
|
|
+ .filter(file => file.isFile() && file.name.startsWith('chat_') && file.name.endsWith('.json'))
|
|
|
+ .map(file => ({
|
|
|
+ name: file.name,
|
|
|
+ path: join(historyDir, file.name)
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (jsonFiles.length === 0) {
|
|
|
+ return { success: true, messages: [] }; // 没有文件时返回空数组
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取所有文件的统计信息(修改时间),用于排序
|
|
|
+ const filesWithStats = await Promise.all(
|
|
|
+ jsonFiles.map(async (file) => {
|
|
|
+ const stats = await stat(file.path);
|
|
|
+ return {
|
|
|
+ ...file,
|
|
|
+ mtime: stats.mtime
|
|
|
+ };
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ // 按修改时间排序(最早的在前,保持时间顺序)
|
|
|
+ filesWithStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
|
|
+
|
|
|
+ // 读取所有文件并合并消息
|
|
|
+ const allMessages = [];
|
|
|
+ for (const file of filesWithStats) {
|
|
|
+ try {
|
|
|
+ const fileContent = await readFile(file.path, 'utf-8');
|
|
|
+ const historyData = JSON.parse(fileContent);
|
|
|
+ const messages = historyData.messages || [];
|
|
|
+ allMessages.push(...messages);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(`读取聊天记录文件失败: ${file.name}`, error);
|
|
|
+ // 继续处理其他文件
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ messages: allMessages,
|
|
|
+ fileCount: filesWithStats.length,
|
|
|
+ totalMessages: allMessages.length
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ // 目录不存在或读取失败
|
|
|
+ if (error.code === 'ENOENT') {
|
|
|
+ return { success: true, messages: [] }; // 目录不存在时返回空数组
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('读取聊天记录失败:', error);
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 提取聊天记录(通过OCR)
|
|
|
+ ipcMain.handle('extract-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => {
|
|
|
+ return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取最后一条消息
|
|
|
+ ipcMain.handle('get-last-chat-message', async (event, ipPort, friendAvatarPath, myAvatarPath) => {
|
|
|
+ return await getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath);
|
|
|
+ });
|
|
|
+
|
|
|
+ // OCR 聊天记录(向后兼容,重定向到 extract-chat-history)
|
|
|
+ ipcMain.handle('ocr-chat-history', async (event, ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath) => {
|
|
|
+ return await extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 提取聊天记录(通过OCR)
|
|
|
+ * @param {string} ipPort - 设备 ID/IP:Port
|
|
|
+ * @param {string} friendAvatarPath - 好友头像路径
|
|
|
+ * @param {string} myAvatarPath - 我的头像路径
|
|
|
+ * @param {string} workflowFolderPath - 工作流文件夹路径(可选)
|
|
|
+ * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
|
|
|
+ */
|
|
|
+export async function extractChatHistory(ipPort, friendAvatarPath, myAvatarPath, workflowFolderPath = null) {
|
|
|
+ try {
|
|
|
+ if (!ipPort) {
|
|
|
+ return { success: false, error: '缺少设备 ID' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 获取设备分辨率
|
|
|
+ const resolutionResult = await getDeviceResolution(ipPort);
|
|
|
+ if (!resolutionResult.success) {
|
|
|
+ return { success: false, error: '获取设备分辨率失败' };
|
|
|
+ }
|
|
|
+ const { width, height } = resolutionResult;
|
|
|
+
|
|
|
+ // 2. 获取屏幕截图
|
|
|
+ const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
|
|
|
+ if (!screenshotResult.success || !screenshotResult.data) {
|
|
|
+ return { success: false, error: '获取屏幕截图失败' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 保存截图到临时文件(如果提供了工作流文件夹,保存到 tmp/时间戳 目录)
|
|
|
+ let screenshotPath;
|
|
|
+ let tmpDir = null; // 用于跟踪需要删除的临时目录
|
|
|
+ if (workflowFolderPath) {
|
|
|
+ // 确保 workflowFolderPath 是绝对路径
|
|
|
+ let absoluteWorkflowPath = workflowFolderPath;
|
|
|
+ if (!isAbsolute(workflowFolderPath)) {
|
|
|
+ // 如果已经是 static/processing/xxx 格式,直接拼接(去掉开头的 static/processing)
|
|
|
+ if (workflowFolderPath.startsWith('static/processing/')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static/processing/', '');
|
|
|
+ absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else if (workflowFolderPath.startsWith('static\\processing\\')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static\\processing\\', '');
|
|
|
+ absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else {
|
|
|
+ // 如果只是文件夹名,需要加上 static/processing
|
|
|
+ absoluteWorkflowPath = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19).replace('T', '_');
|
|
|
+ tmpDir = join(absoluteWorkflowPath, 'tmp', timestamp);
|
|
|
+ await mkdir(tmpDir, { recursive: true });
|
|
|
+ screenshotPath = join(tmpDir, 'screenshot.png');
|
|
|
+ } else {
|
|
|
+ const tempDir = join(__dirname, '..');
|
|
|
+ screenshotPath = join(tempDir, 'temp_screenshot_chat.png');
|
|
|
+ }
|
|
|
+ const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
|
|
|
+ await writeFile(screenshotPath, screenshotBuffer);
|
|
|
+
|
|
|
+ // 验证文件是否成功写入
|
|
|
+ try {
|
|
|
+ const { access, constants } = await import('fs/promises');
|
|
|
+ await access(screenshotPath, constants.F_OK);
|
|
|
+ console.log(`截图已保存到: ${screenshotPath}`);
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`截图文件写入验证失败: ${screenshotPath}`, err);
|
|
|
+ return { success: false, error: `截图文件写入失败: ${err.message}` };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 4. 调用 JS 函数提取聊天记录
|
|
|
+ // 转换头像路径为绝对路径
|
|
|
+ let friendAvatarArg = null;
|
|
|
+ if (friendAvatarPath) {
|
|
|
+ if (isAbsolute(friendAvatarPath)) {
|
|
|
+ friendAvatarArg = friendAvatarPath;
|
|
|
+ } else {
|
|
|
+ friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let myAvatarArg = null;
|
|
|
+ if (myAvatarPath) {
|
|
|
+ if (isAbsolute(myAvatarPath)) {
|
|
|
+ myAvatarArg = myAvatarPath;
|
|
|
+ } else {
|
|
|
+ myAvatarArg = join(__dirname, '..', 'static', 'processing', myAvatarPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果提供了工作流文件夹路径,转换为绝对路径
|
|
|
+ let workflowFolderArg = null;
|
|
|
+ if (workflowFolderPath) {
|
|
|
+ if (isAbsolute(workflowFolderPath)) {
|
|
|
+ workflowFolderArg = workflowFolderPath;
|
|
|
+ } else {
|
|
|
+ if (workflowFolderPath.startsWith('static/processing/')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static/processing/', '');
|
|
|
+ workflowFolderArg = join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else if (workflowFolderPath.startsWith('static\\processing\\')) {
|
|
|
+ const folderName = workflowFolderPath.replace('static\\processing\\', '');
|
|
|
+ workflowFolderArg = join(__dirname, '..', 'static', 'processing', folderName);
|
|
|
+ } else {
|
|
|
+ workflowFolderArg = join(__dirname, '..', 'static', 'processing', workflowFolderPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await extractChatHistoryFromFunc(screenshotPath, friendAvatarArg, myAvatarArg, width, height, workflowFolderArg);
|
|
|
+ return result;
|
|
|
+ } finally {
|
|
|
+ // 5. 使用完后删除临时目录
|
|
|
+ if (tmpDir) {
|
|
|
+ try {
|
|
|
+ await rm(tmpDir, { recursive: true, force: true });
|
|
|
+ console.log(`已删除临时目录: ${tmpDir}`);
|
|
|
+ } catch (rmError) {
|
|
|
+ console.warn(`删除临时目录失败: ${tmpDir}`, rmError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('提取聊天记录失败:', error);
|
|
|
+ if (error.message && error.message.includes('timeout')) {
|
|
|
+ return { success: false, error: '提取聊天记录超时,请检查网络连接或稍后重试' };
|
|
|
+ }
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取最后一条消息(带发送者信息)
|
|
|
+ * @param {string} ipPort - 设备 ID/IP:Port
|
|
|
+ * @param {string} friendAvatarPath - 好友头像路径
|
|
|
+ * @param {string} myAvatarPath - 我的头像路径
|
|
|
+ * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>}
|
|
|
+ */
|
|
|
+export async function getLastChatMessage(ipPort, friendAvatarPath, myAvatarPath) {
|
|
|
+ try {
|
|
|
+ if (!ipPort) {
|
|
|
+ return { success: false, error: '缺少设备 ID' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 获取设备分辨率
|
|
|
+ const resolutionResult = await getDeviceResolution(ipPort);
|
|
|
+ if (!resolutionResult.success) {
|
|
|
+ return { success: false, error: '获取设备分辨率失败' };
|
|
|
+ }
|
|
|
+ const { width, height } = resolutionResult;
|
|
|
+
|
|
|
+ // 2. 获取屏幕截图
|
|
|
+ const screenshotResult = await captureScreenshot(ipPort, { format: 'png' });
|
|
|
+ if (!screenshotResult.success || !screenshotResult.data) {
|
|
|
+ return { success: false, error: '获取屏幕截图失败' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 保存截图到临时文件
|
|
|
+ const tempDir = join(__dirname, '..');
|
|
|
+ const screenshotPath = join(tempDir, 'temp_screenshot_chat.png');
|
|
|
+ const screenshotBuffer = Buffer.from(screenshotResult.data, 'base64');
|
|
|
+ await writeFile(screenshotPath, screenshotBuffer);
|
|
|
+
|
|
|
+ // 4. 调用 JS 实现获取最后一条消息
|
|
|
+ // 转换头像路径为绝对路径
|
|
|
+ let friendAvatarArg = null;
|
|
|
+ if (friendAvatarPath) {
|
|
|
+ if (isAbsolute(friendAvatarPath)) {
|
|
|
+ friendAvatarArg = friendAvatarPath;
|
|
|
+ } else {
|
|
|
+ friendAvatarArg = join(__dirname, '..', 'static', 'processing', friendAvatarPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let myAvatarArg = null;
|
|
|
+ if (myAvatarPath) {
|
|
|
+ if (isAbsolute(myAvatarPath)) {
|
|
|
+ myAvatarArg = myAvatarPath;
|
|
|
+ } else {
|
|
|
+ myAvatarArg = join(__dirname, '..', 'static', 'processing', myAvatarPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
|
|
|
+ const normalizedFriendAvatar = friendAvatarArg ? friendAvatarArg.replace(/\\/g, '/') : null;
|
|
|
+ const normalizedMyAvatar = myAvatarArg ? myAvatarArg.replace(/\\/g, '/') : null;
|
|
|
+
|
|
|
+ const result = await getLastMessageFromFunc(normalizedScreenshotPath, normalizedFriendAvatar, normalizedMyAvatar, width, height);
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ // 确保正确显示UTF-8编码的中文
|
|
|
+ const displayText = result.text || '';
|
|
|
+ try {
|
|
|
+ const textStr = Buffer.isBuffer(displayText)
|
|
|
+ ? displayText.toString('utf8')
|
|
|
+ : String(displayText);
|
|
|
+ console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, textStr);
|
|
|
+ } catch (e) {
|
|
|
+ console.log(`最后一条消息 [${result.sender || 'unknown'}]:`, displayText);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取最后一条消息失败:', error);
|
|
|
+ if (error.message && error.message.includes('timeout')) {
|
|
|
+ return { success: false, error: '获取最后一条消息超时,请检查网络连接或稍后重试' };
|
|
|
+ }
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|