| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024 |
- /**
- * 文件读写操作模块
- * 负责文件系统的读写操作,包括聊天记录的保存和读取
- */
- 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<number>} 目录总大小(字节)
- */
- 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<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 {
- // 如果 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} 字节)` };
- }
- console.log(`[save-base64-image] 文件保存成功: ${absoluteSavePath}, 大小: ${fileStats.size} 字节`);
- return { success: true, fileSize: fileStats.size };
- } catch (error) {
- console.error(`[save-base64-image] 保存失败: ${savePath}`, 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) {
- // 日志写入失败不应该影响主流程,只记录错误
- console.error('追加日志失败:', 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) {
- console.error('保存聊天记录到 chat-history.txt 失败:', 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) {
- console.warn('读取最新文件失败,将创建新文件:', 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) {
- console.warn(`删除文件失败: ${file.name}`, deleteError);
- }
- }
- if (filesToDelete.length > 0) {
- console.log(`已清理 ${filesToDelete.length} 个文件,当前总大小: ${((totalSize) / 1024 / 1024).toFixed(2)}MB`);
- }
- } catch (cleanupError) {
- // 清理失败不影响保存操作
- console.warn('清理旧聊天记录文件时出错:', cleanupError);
- }
- return { success: true, filePath: targetFilePath };
- } 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-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) {
- 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 };
- }
- });
- // 读取聊天记录(合并所有历史文件)
- 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, 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) {
- 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 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) {
- 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 };
- }
- }
|