read-last-message.js 11 KB

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