| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- /**
- * 读取最后一条消息
- * 支持两种模式:
- * 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 }
|