read-last-message.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /**
  2. * 读取最后一条消息
  3. * 支持两种模式:
  4. * 1. 从变量读取(如果提供了 inputData)- 从 chatHistory 文本或消息数组中提取最后一条消息
  5. * 2. 从 history 文件夹读取(如果没有提供 inputData)- 从最新的聊天记录文件中读取
  6. */
  7. const electronAPI = { readLastMessage: () => ({ success: false, error: 'readLastMessage 需在主进程实现' }) }
  8. const tagName = 'read-last-message'
  9. const schema = {
  10. description: '读取最后一条消息,包括消息内容和发送者角色。支持从变量或 history 文件夹读取。',
  11. inputs: {
  12. inputData: '输入数据(可选)- 可以是聊天记录文本或消息数组,如果不提供则从 history 文件夹读取',
  13. textVariable: '输出变量名(保存消息文本)',
  14. senderVariable: '输出变量名(保存发送者角色:friend 或 me)',
  15. },
  16. outputs: {
  17. textVariable: '最后一条消息的文本内容',
  18. senderVariable: '最后一条消息的发送者角色(friend 或 me)',
  19. },
  20. };
  21. /**
  22. * 解析 chat-history.txt 格式的 JSON 字符串为消息数组
  23. * 格式:{"data":"时间戳","friend":"消息","me":"消息"},允许重复键
  24. * @param {string} jsonString - JSON 字符串
  25. * @returns {Array<{sender: string, text: string}>}
  26. */
  27. function parseChatHistoryTxtFormat(jsonString) {
  28. const messages = [];
  29. if (!jsonString || typeof jsonString !== 'string') {
  30. return messages;
  31. }
  32. try {
  33. // 由于JSON不支持重复键,我们需要从文本解析每一行
  34. const lines = jsonString.split('\n');
  35. for (const line of lines) {
  36. // 匹配格式:\t"key":"value", 或 \t"key":"value"
  37. const match = line.match(/^\s*"([^"]+)":\s*"([^"]*)"\s*,?\s*$/);
  38. if (match) {
  39. const key = match[1];
  40. const value = match[2];
  41. // 只处理 friend 和 me 键,忽略 data 键(时间戳)
  42. if (key === 'friend' || key === 'me') {
  43. messages.push({
  44. sender: key,
  45. text: value
  46. });
  47. }
  48. }
  49. }
  50. } catch (e) {
  51. // 解析失败,返回空数组
  52. return messages;
  53. }
  54. return messages;
  55. }
  56. /**
  57. * 解析聊天记录文本为结构化数据
  58. * @param {string} chatHistoryText - 聊天记录文本(格式:好友: xxx\n我: xxx)
  59. * @returns {Array<{sender: string, text: string}>}
  60. */
  61. function parseChatHistoryText(chatHistoryText) {
  62. const messages = [];
  63. const lines = chatHistoryText.split('\n').filter(line => line.trim());
  64. for (const line of lines) {
  65. // 匹配格式:对方: xxx、好友: xxx 或 我: xxx
  66. const match = line.match(/^(对方|好友|我):\s*(.+)$/);
  67. if (match) {
  68. const senderLabel = match[1];
  69. const sender = (senderLabel === '对方' || senderLabel === '好友') ? 'friend' : 'me';
  70. const text = match[2].trim();
  71. messages.push({ sender, text });
  72. }
  73. }
  74. return messages;
  75. }
  76. /**
  77. * 执行读取最后一条消息
  78. * @param {Object} params - 参数对象
  79. * @param {string} params.folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  80. * @param {string|Array} params.inputData - 输入数据(可选)- 聊天记录文本或消息数组
  81. * @param {string} params.textVariable - 输出变量名(保存消息文本)
  82. * @param {string} params.senderVariable - 输出变量名(保存发送者角色)
  83. * @returns {Promise<{success: boolean, error?: string, text?: string, sender?: string}>}
  84. */
  85. async function executeReadLastMessage({ folderPath, inputData, textVariable, senderVariable }) {
  86. try {
  87. if (!textVariable && !senderVariable) {
  88. return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' };
  89. }
  90. let messages = [];
  91. let lastMessage = null;
  92. // 如果提供了 inputData,从变量读取
  93. if (inputData !== undefined && inputData !== null) {
  94. if (typeof inputData === 'string') {
  95. // 如果是字符串,先尝试作为JSON字符串解析
  96. try {
  97. const parsed = JSON.parse(inputData);
  98. if (Array.isArray(parsed)) {
  99. // 是JSON数组格式
  100. messages = parsed;
  101. } else if (typeof parsed === 'object' && parsed !== null) {
  102. // 是 chat-history.txt 格式的对象(允许重复键)
  103. // 格式:{"data":"时间戳","friend":"消息","me":"消息"}
  104. // 需要从文本解析,因为 JSON.parse 会丢失重复键
  105. messages = parseChatHistoryTxtFormat(inputData);
  106. } else {
  107. // JSON解析成功但不是数组或对象,按文本格式解析
  108. messages = parseChatHistoryText(inputData);
  109. }
  110. } catch (e) {
  111. // JSON解析失败,尝试解析为 chat-history.txt 格式
  112. // 如果包含 "data" 和 "friend"/"me" 键,可能是 chat-history.txt 格式
  113. if (inputData.includes('"data"') && (inputData.includes('"friend"') || inputData.includes('"me"'))) {
  114. messages = parseChatHistoryTxtFormat(inputData);
  115. } else {
  116. // 按文本格式解析(例如:"对方: xxx\n我: yyy")
  117. messages = parseChatHistoryText(inputData);
  118. }
  119. }
  120. } else if (Array.isArray(inputData)) {
  121. // 如果已经是数组,直接使用
  122. messages = inputData;
  123. } else {
  124. return { success: false, error: 'inputData 必须是字符串或消息数组' };
  125. }
  126. if (messages.length === 0) {
  127. // 如果数组为空,返回空值而不是报错(允许第一次运行或历史记录为空的情况)
  128. return {
  129. success: true,
  130. text: '',
  131. sender: ''
  132. };
  133. }
  134. // 改进的算法:合并多行消息 + 过滤系统消息 + 识别最后一条真正的聊天消息
  135. //
  136. // 算法步骤:
  137. // 1. 先合并同一发送者的连续消息(y坐标相近,可能是同一消息的多行)- 仅当有 y 坐标时
  138. // 2. 过滤系统消息和时间戳
  139. // 3. 从后往前查找最后一条真正的聊天消息
  140. // 检查消息是否有 y 坐标(来自 OCR 识别)或没有(来自 chat-history.txt 格式)
  141. const hasYCoordinate = messages.some(msg => msg && typeof msg === 'object' && (msg.y !== undefined || msg.confidence !== undefined));
  142. // 步骤1: 合并多行消息(仅当有 y 坐标时,即来自 OCR 识别)
  143. let mergedMessages = [];
  144. if (hasYCoordinate) {
  145. // 有 y 坐标,需要合并多行消息
  146. for (let i = 0; i < messages.length; i++) {
  147. const msg = messages[i];
  148. if (!msg || typeof msg !== 'object') {
  149. continue;
  150. }
  151. const text = (msg.text || msg.message || '').trim();
  152. const sender = msg.sender || msg.role || '';
  153. const y = msg.y || 0;
  154. const confidence = msg.confidence || 1.0;
  155. if (!text || confidence < 0.6) {
  156. continue;
  157. }
  158. // 检查是否可以与前一条消息合并
  159. if (mergedMessages.length > 0) {
  160. const lastMerged = mergedMessages[mergedMessages.length - 1];
  161. const lastY = lastMerged.y || 0;
  162. const yDiff = Math.abs(y - lastY);
  163. // 如果发送者相同,y坐标相近(<100像素),且上一条消息文本不完整,则合并
  164. if (lastMerged.sender === sender &&
  165. yDiff < 100 &&
  166. lastMerged.text &&
  167. !/[。!?.!?]$/.test(lastMerged.text)) {
  168. // 合并消息
  169. lastMerged.text = (lastMerged.text + text).trim();
  170. // 更新y坐标为最新的(更靠下的)
  171. lastMerged.y = Math.max(lastMerged.y || 0, y);
  172. continue;
  173. }
  174. }
  175. // 不能合并,添加为新消息
  176. mergedMessages.push({
  177. text: text,
  178. sender: sender,
  179. y: y,
  180. confidence: confidence
  181. });
  182. }
  183. } else {
  184. // 没有 y 坐标(来自 chat-history.txt 格式),直接使用原始消息
  185. mergedMessages = messages.map(msg => ({
  186. text: (msg.text || msg.message || '').trim(),
  187. sender: msg.sender || msg.role || '',
  188. y: 0,
  189. confidence: 1.0
  190. })).filter(msg => msg.text); // 过滤空消息
  191. }
  192. // 步骤2和3: 过滤系统消息,从后往前查找最后一条真正的聊天消息
  193. const systemMessagePatterns = [
  194. /撤回|撤销|revoke/i,
  195. /^(昨天|今天|明天)\s*\d{1,2}:\d{2}$/,
  196. /^\d{1,2}:\d{2}$/, // 时间戳格式(如 "03:07", "02:38")
  197. /^\d{4}\/\d{1,2}\/\d{1,2}\s+\d{1,2}:\d{2}$/,
  198. /^[QWA-Z,。·\s]{1,3}$/i, // 单字符或短字符串(可能是键盘按键)
  199. /^[く·]{1,2}$/, // 特殊字符
  200. /^[牙く]{1,2}$/, // 特殊字符(如"牙"可能是标题)
  201. /^Q础\s*\d+$/i, // 误识别(如"Q础 34")
  202. /^[··]{1,2}$/, // 多个点
  203. /^\d{1,2}$/, // 单个或两个数字(如 "17")
  204. /^[a-zA-Z]{1,2}$/i, // 单个或两个字母
  205. /^[^\u4e00-\u9fa5a-zA-Z0-9]{1,2}$/ // 单个或两个非中英数字符
  206. ];
  207. // 从后往前遍历合并后的消息,找到第一条非系统消息
  208. for (let i = mergedMessages.length - 1; i >= 0; i--) {
  209. const msg = mergedMessages[i];
  210. const text = (msg.text || '').trim();
  211. const sender = msg.sender || '';
  212. // 跳过空消息
  213. if (!text) {
  214. continue;
  215. }
  216. // 检查是否是系统消息
  217. let isSystemMessage = false;
  218. for (const pattern of systemMessagePatterns) {
  219. if (pattern.test(text)) {
  220. isSystemMessage = true;
  221. break;
  222. }
  223. }
  224. // 如果找到非系统消息,使用它
  225. if (!isSystemMessage && (sender === 'friend' || sender === 'me')) {
  226. lastMessage = {
  227. text: text,
  228. sender: sender
  229. };
  230. break;
  231. }
  232. }
  233. // 如果没有找到有效的消息,返回空
  234. if (!lastMessage) {
  235. return {
  236. success: true,
  237. text: '',
  238. sender: ''
  239. };
  240. }
  241. // 确保消息有 text 和 sender 字段
  242. const text = lastMessage.text || lastMessage.message || '';
  243. const sender = lastMessage.sender || lastMessage.role || '';
  244. } else {
  245. // 如果没有提供 inputData,从 history 文件夹读取
  246. if (!electronAPI.readLastMessage) {
  247. return { success: false, error: '读取最后一条消息 API 不可用' };
  248. }
  249. const result = await electronAPI.readLastMessage(folderPath);
  250. if (!result.success) {
  251. return { success: false, error: `读取最后一条消息失败: ${result.error}` };
  252. }
  253. lastMessage = {
  254. text: result.text || '',
  255. sender: result.sender || ''
  256. };
  257. }
  258. // 返回最后一条消息的文本和发送者
  259. return {
  260. success: true,
  261. text: lastMessage.text || lastMessage.message || '',
  262. sender: lastMessage.sender || lastMessage.role || ''
  263. };
  264. } catch (error) {
  265. return { success: false, error: error.message || '读取最后一条消息失败' };
  266. }
  267. }
  268. module.exports = { tagName, schema, executeReadLastMessage }