/** * 读取最后一条消息 * 支持两种模式: * 1. 从变量读取(如果提供了 inputData)- 从 chatHistory 文本或消息数组中提取最后一条消息 * 2. 从 history 文件夹读取(如果没有提供 inputData)- 从最新的聊天记录文件中读取 */ const electronAPI = { readLastMessage: () => ({ success: false, error: 'readLastMessage 需在主进程实现' }) } const tagName = 'read-last-message' const schema = { description: '读取最后一条消息,包括消息内容和发送者角色。支持从变量或 history 文件夹读取。', inputs: { inputData: '输入数据(可选)- 可以是聊天记录文本或消息数组,如果不提供则从 history 文件夹读取', textVariable: '输出变量名(保存消息文本)', senderVariable: '输出变量名(保存发送者角色:friend 或 me)', }, outputs: { textVariable: '最后一条消息的文本内容', senderVariable: '最后一条消息的发送者角色(friend 或 me)', }, }; /** * 解析 chat-history.txt 格式的 JSON 字符串为消息数组 * 格式:{"data":"时间戳","friend":"消息","me":"消息"},允许重复键 * @param {string} jsonString - JSON 字符串 * @returns {Array<{sender: string, text: string}>} */ function parseChatHistoryTxtFormat(jsonString) { const messages = []; if (!jsonString || typeof jsonString !== 'string') { return messages; } try { // 由于JSON不支持重复键,我们需要从文本解析每一行 const lines = jsonString.split('\n'); for (const line of lines) { // 匹配格式:\t"key":"value", 或 \t"key":"value" const match = line.match(/^\s*"([^"]+)":\s*"([^"]*)"\s*,?\s*$/); if (match) { const key = match[1]; const value = match[2]; // 只处理 friend 和 me 键,忽略 data 键(时间戳) if (key === 'friend' || key === 'me') { messages.push({ sender: key, text: value }); } } } } catch (e) { // 解析失败,返回空数组 return messages; } return messages; } /** * 解析聊天记录文本为结构化数据 * @param {string} chatHistoryText - 聊天记录文本(格式:好友: xxx\n我: xxx) * @returns {Array<{sender: string, text: string}>} */ function parseChatHistoryText(chatHistoryText) { const messages = []; const lines = chatHistoryText.split('\n').filter(line => line.trim()); for (const line of lines) { // 匹配格式:对方: xxx、好友: xxx 或 我: xxx const match = line.match(/^(对方|好友|我):\s*(.+)$/); if (match) { const senderLabel = match[1]; const sender = (senderLabel === '对方' || senderLabel === '好友') ? 'friend' : 'me'; const text = match[2].trim(); messages.push({ sender, text }); } } return messages; } /** * 执行读取最后一条消息 * @param {Object} params - 参数对象 * @param {string} params.folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流") * @param {string|Array} params.inputData - 输入数据(可选)- 聊天记录文本或消息数组 * @param {string} params.textVariable - 输出变量名(保存消息文本) * @param {string} params.senderVariable - 输出变量名(保存发送者角色) * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>} */ async function executeReadLastMessage({ folderPath, inputData, textVariable, senderVariable }) { try { if (!textVariable && !senderVariable) { return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' }; } let messages = []; let lastMessage = null; // 如果提供了 inputData,从变量读取 if (inputData !== undefined && inputData !== null) { if (typeof inputData === 'string') { // 如果是字符串,先尝试作为JSON字符串解析 try { const parsed = JSON.parse(inputData); if (Array.isArray(parsed)) { // 是JSON数组格式 messages = parsed; } else if (typeof parsed === 'object' && parsed !== null) { // 是 chat-history.txt 格式的对象(允许重复键) // 格式:{"data":"时间戳","friend":"消息","me":"消息"} // 需要从文本解析,因为 JSON.parse 会丢失重复键 messages = parseChatHistoryTxtFormat(inputData); } else { // JSON解析成功但不是数组或对象,按文本格式解析 messages = parseChatHistoryText(inputData); } } catch (e) { // JSON解析失败,尝试解析为 chat-history.txt 格式 // 如果包含 "data" 和 "friend"/"me" 键,可能是 chat-history.txt 格式 if (inputData.includes('"data"') && (inputData.includes('"friend"') || inputData.includes('"me"'))) { messages = parseChatHistoryTxtFormat(inputData); } else { // 按文本格式解析(例如:"对方: xxx\n我: yyy") messages = parseChatHistoryText(inputData); } } } else if (Array.isArray(inputData)) { // 如果已经是数组,直接使用 messages = inputData; } else { return { success: false, error: 'inputData 必须是字符串或消息数组' }; } if (messages.length === 0) { // 如果数组为空,返回空值而不是报错(允许第一次运行或历史记录为空的情况) return { success: true, text: '', sender: '' }; } // 改进的算法:合并多行消息 + 过滤系统消息 + 识别最后一条真正的聊天消息 // // 算法步骤: // 1. 先合并同一发送者的连续消息(y坐标相近,可能是同一消息的多行)- 仅当有 y 坐标时 // 2. 过滤系统消息和时间戳 // 3. 从后往前查找最后一条真正的聊天消息 // 检查消息是否有 y 坐标(来自 OCR 识别)或没有(来自 chat-history.txt 格式) const hasYCoordinate = messages.some(msg => msg && typeof msg === 'object' && (msg.y !== undefined || msg.confidence !== undefined)); // 步骤1: 合并多行消息(仅当有 y 坐标时,即来自 OCR 识别) let mergedMessages = []; if (hasYCoordinate) { // 有 y 坐标,需要合并多行消息 for (let i = 0; i < messages.length; i++) { const msg = messages[i]; if (!msg || typeof msg !== 'object') { continue; } const text = (msg.text || msg.message || '').trim(); const sender = msg.sender || msg.role || ''; const y = msg.y || 0; const confidence = msg.confidence || 1.0; if (!text || confidence < 0.6) { continue; } // 检查是否可以与前一条消息合并 if (mergedMessages.length > 0) { const lastMerged = mergedMessages[mergedMessages.length - 1]; const lastY = lastMerged.y || 0; const yDiff = Math.abs(y - lastY); // 如果发送者相同,y坐标相近(<100像素),且上一条消息文本不完整,则合并 if (lastMerged.sender === sender && yDiff < 100 && lastMerged.text && !/[。!?.!?]$/.test(lastMerged.text)) { // 合并消息 lastMerged.text = (lastMerged.text + text).trim(); // 更新y坐标为最新的(更靠下的) lastMerged.y = Math.max(lastMerged.y || 0, y); continue; } } // 不能合并,添加为新消息 mergedMessages.push({ text: text, sender: sender, y: y, confidence: confidence }); } } else { // 没有 y 坐标(来自 chat-history.txt 格式),直接使用原始消息 mergedMessages = messages.map(msg => ({ text: (msg.text || msg.message || '').trim(), sender: msg.sender || msg.role || '', y: 0, confidence: 1.0 })).filter(msg => msg.text); // 过滤空消息 } // 步骤2和3: 过滤系统消息,从后往前查找最后一条真正的聊天消息 const systemMessagePatterns = [ /撤回|撤销|revoke/i, /^(昨天|今天|明天)\s*\d{1,2}:\d{2}$/, /^\d{1,2}:\d{2}$/, // 时间戳格式(如 "03:07", "02:38") /^\d{4}\/\d{1,2}\/\d{1,2}\s+\d{1,2}:\d{2}$/, /^[QWA-Z,。·\s]{1,3}$/i, // 单字符或短字符串(可能是键盘按键) /^[く·]{1,2}$/, // 特殊字符 /^[牙く]{1,2}$/, // 特殊字符(如"牙"可能是标题) /^Q础\s*\d+$/i, // 误识别(如"Q础 34") /^[··]{1,2}$/, // 多个点 /^\d{1,2}$/, // 单个或两个数字(如 "17") /^[a-zA-Z]{1,2}$/i, // 单个或两个字母 /^[^\u4e00-\u9fa5a-zA-Z0-9]{1,2}$/ // 单个或两个非中英数字符 ]; // 从后往前遍历合并后的消息,找到第一条非系统消息 for (let i = mergedMessages.length - 1; i >= 0; i--) { const msg = mergedMessages[i]; const text = (msg.text || '').trim(); const sender = msg.sender || ''; // 跳过空消息 if (!text) { continue; } // 检查是否是系统消息 let isSystemMessage = false; for (const pattern of systemMessagePatterns) { if (pattern.test(text)) { isSystemMessage = true; break; } } // 如果找到非系统消息,使用它 if (!isSystemMessage && (sender === 'friend' || sender === 'me')) { lastMessage = { text: text, sender: sender }; break; } } // 如果没有找到有效的消息,返回空 if (!lastMessage) { return { success: true, text: '', sender: '' }; } // 确保消息有 text 和 sender 字段 const text = lastMessage.text || lastMessage.message || ''; const sender = lastMessage.sender || lastMessage.role || ''; } else { // 如果没有提供 inputData,从 history 文件夹读取 if (!electronAPI.readLastMessage) { return { success: false, error: '读取最后一条消息 API 不可用' }; } const result = await electronAPI.readLastMessage(folderPath); if (!result.success) { return { success: false, error: `读取最后一条消息失败: ${result.error}` }; } lastMessage = { text: result.text || '', sender: result.sender || '' }; } // 返回最后一条消息的文本和发送者 return { success: true, text: lastMessage.text || lastMessage.message || '', sender: lastMessage.sender || lastMessage.role || '' }; } catch (error) { return { success: false, error: error.message || '读取最后一条消息失败' }; } } module.exports = { tagName, schema, executeReadLastMessage }