chat-history.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * 聊天历史记录管理模块
  3. * 负责保存聊天记录、生成AI总结、读取历史总结、读取所有聊天记录
  4. */
  5. const electronAPI = require('../../node-api.js')
  6. /**
  7. * 保存聊天记录到 history 文件夹
  8. * @param {string|Array} chatHistory - 聊天记录(可以是JSON字符串、文本格式或消息数组)
  9. * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  10. * @returns {Promise<{success: boolean, error?: string, filePath?: string}>}
  11. */
  12. async function saveChatHistory(chatHistory, folderPath) {
  13. try {
  14. if (!chatHistory) {
  15. return { success: false, error: '聊天记录为空' };
  16. }
  17. if (!electronAPI.saveChatHistory) {
  18. return { success: false, error: '保存聊天记录 API 不可用' };
  19. }
  20. // 解析聊天记录,转换为消息数组
  21. let newMessages = [];
  22. if (Array.isArray(chatHistory)) {
  23. // 如果已经是数组,直接使用
  24. newMessages = chatHistory;
  25. } else if (typeof chatHistory === 'string') {
  26. // 如果是字符串,尝试解析
  27. // 先检查是否是 chat-history.txt 格式(包含 "data" 和 "friend"/"me" 键)
  28. if (chatHistory.includes('"data"') && (chatHistory.includes('"friend"') || chatHistory.includes('"me"'))) {
  29. // 是 chat-history.txt 格式
  30. newMessages = parseChatHistoryTxtFormat(chatHistory);
  31. } else {
  32. // 尝试作为标准JSON字符串解析
  33. try {
  34. const parsed = JSON.parse(chatHistory);
  35. if (Array.isArray(parsed)) {
  36. newMessages = parsed;
  37. } else {
  38. // 如果不是数组,按文本格式解析
  39. newMessages = parseChatHistoryText(chatHistory);
  40. }
  41. } catch (e) {
  42. // JSON解析失败,按文本格式解析
  43. newMessages = parseChatHistoryText(chatHistory);
  44. }
  45. }
  46. }
  47. if (newMessages.length === 0) {
  48. return { success: false, error: '聊天记录解析后为空' };
  49. }
  50. // 读取历史聊天记录,进行去重
  51. let historyMessages = [];
  52. try {
  53. if (electronAPI.readChatHistory) {
  54. const historyResult = await electronAPI.readChatHistory(folderPath);
  55. if (historyResult.success && historyResult.messages && Array.isArray(historyResult.messages)) {
  56. historyMessages = historyResult.messages;
  57. }
  58. }
  59. } catch (error) {
  60. // 读取历史聊天记录失败,将保存所有新消息
  61. }
  62. // 对历史消息构建查找集合(用于去重)
  63. const historyMessageMap = new Map();
  64. historyMessages.forEach((msg, index) => {
  65. const normalizedText = (msg.text || '').trim();
  66. const key = `${msg.sender || ''}:${normalizedText}`;
  67. // 如果同一个 key 已存在,保留索引较小的(更早的消息)
  68. if (!historyMessageMap.has(key)) {
  69. historyMessageMap.set(key, {
  70. index,
  71. sender: msg.sender,
  72. text: normalizedText
  73. });
  74. }
  75. });
  76. // 找出真正新的消息(不在历史记录中的)
  77. const uniqueNewMessages = [];
  78. const duplicateMessages = [];
  79. for (const newMsg of newMessages) {
  80. const normalizedText = (newMsg.text || '').trim();
  81. const key = `${newMsg.sender || ''}:${normalizedText}`;
  82. // 检查是否已存在于历史记录中
  83. if (!historyMessageMap.has(key)) {
  84. // 这是新消息
  85. uniqueNewMessages.push(newMsg);
  86. } else {
  87. // 消息已存在,记录为重复消息
  88. const existingMsg = historyMessageMap.get(key);
  89. duplicateMessages.push({
  90. sender: existingMsg.sender,
  91. text: normalizedText.substring(0, 50) + (normalizedText.length > 50 ? '...' : '')
  92. });
  93. }
  94. }
  95. if (uniqueNewMessages.length === 0) {
  96. // console.log(`没有新消息(所有 ${newMessages.length} 条消息都与历史记录重合),跳过保存`);
  97. // if (duplicateMessages.length > 0) {
  98. // console.log(`重复消息示例: ${duplicateMessages[0].sender}: ${duplicateMessages[0].text}`);
  99. // }
  100. return { success: true, skipped: true, reason: 'no_new_messages' };
  101. }
  102. // 合并消息:历史消息 + 新的唯一消息
  103. const mergedMessages = [...historyMessages, ...uniqueNewMessages];
  104. // console.log(`将保存 ${uniqueNewMessages.length} 条新消息(已剔除 ${duplicateMessages.length} 条重复消息)`);
  105. // if (duplicateMessages.length > 0 && duplicateMessages.length <= 5) {
  106. // console.log(`重复消息: ${duplicateMessages.map(m => `${m.sender}: ${m.text}`).join('; ')}`);
  107. // } else if (duplicateMessages.length > 5) {
  108. // console.log(`重复消息示例: ${duplicateMessages.slice(0, 3).map(m => `${m.sender}: ${m.text}`).join('; ')} ...`);
  109. // }
  110. // 构建保存的数据结构
  111. const now = new Date();
  112. const historyData = {
  113. timestamp: now.toISOString(),
  114. messageCount: mergedMessages.length,
  115. messages: mergedMessages
  116. };
  117. // 通过 IPC 调用主进程保存聊天记录到 chat-history.txt(新格式)
  118. // 使用新的 API 保存为 chat-history.txt 格式
  119. if (electronAPI.saveChatHistoryTxt) {
  120. const result = await electronAPI.saveChatHistoryTxt(folderPath, uniqueNewMessages);
  121. if (!result.success) {
  122. return { success: false, error: result.error };
  123. }
  124. return { success: true, filePath: result.filePath };
  125. }
  126. // 向后兼容:如果没有新API,使用旧API
  127. const result = await electronAPI.saveChatHistory(folderPath, historyData);
  128. if (!result.success) {
  129. return { success: false, error: result.error };
  130. }
  131. return { success: true, filePath: result.filePath };
  132. } catch (error) {
  133. return { success: false, error: error.message };
  134. }
  135. }
  136. /**
  137. * 生成聊天记录的AI总结
  138. * @param {string} chatHistory - 聊天记录文本
  139. * @param {string} folderPath - 工作流文件夹路径
  140. * @param {string} modelName - AI模型名称(可选,默认 'gpt-5-nano-ca')
  141. * @returns {Promise<{success: boolean, error?: string, summary?: string}>}
  142. */
  143. async function generateHistorySummary(chatHistory, folderPath, modelName = 'gpt-5-nano-ca') {
  144. try {
  145. if (!chatHistory || !chatHistory.trim()) {
  146. return { success: false, error: '聊天记录为空' };
  147. }
  148. // 读取历史总结(如果存在)
  149. const existingSummary = await getHistorySummary(folderPath);
  150. const summaryContext = existingSummary ? `\n\n之前的总结:\n${existingSummary}` : '';
  151. // 构建AI提示词
  152. const prompt = `请总结以下微信聊天记录的主要内容、话题和氛围。总结要简洁明了,控制在100字以内,帮助理解对话的上下文和情感倾向。
  153. 聊天记录:
  154. ${chatHistory}${summaryContext}
  155. 请直接返回总结内容,不要包含其他说明文字。`;
  156. // 调用AI API
  157. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  158. method: 'POST',
  159. headers: { 'Content-Type': 'application/json' },
  160. body: JSON.stringify({
  161. prompt: prompt,
  162. modelName: modelName
  163. })
  164. });
  165. if (!response.ok) {
  166. return { success: false, error: `AI请求失败: ${response.statusText}` };
  167. }
  168. const data = await response.json();
  169. const summary = data.data?.output_text || data.text || data.content || '';
  170. if (!summary || !summary.trim()) {
  171. return { success: false, error: 'AI返回的总结为空' };
  172. }
  173. // 通过 IPC 调用主进程保存总结
  174. if (!electronAPI.saveChatHistorySummary) {
  175. return { success: false, error: '保存聊天记录总结 API 不可用' };
  176. }
  177. const saveResult = await electronAPI.saveChatHistorySummary(folderPath, summary.trim());
  178. if (!saveResult.success) {
  179. return { success: false, error: saveResult.error };
  180. }
  181. // console.log(`聊天记录总结已保存`);
  182. return { success: true, summary: summary.trim() };
  183. } catch (error) {
  184. return { success: false, error: error.message };
  185. }
  186. }
  187. /**
  188. * 读取历史聊天记录的总结
  189. * @param {string} folderPath - 工作流文件夹路径
  190. * @returns {Promise<string>} 返回总结文本,如果不存在则返回空字符串
  191. */
  192. async function getHistorySummary(folderPath) {
  193. try {
  194. if (!electronAPI.getChatHistorySummary) {
  195. return '';
  196. }
  197. const result = await electronAPI.getChatHistorySummary(folderPath);
  198. if (!result.success) {
  199. // 文件不存在是正常情况,返回空字符串
  200. return '';
  201. }
  202. return result.summary || '';
  203. } catch (error) {
  204. return '';
  205. }
  206. }
  207. /**
  208. * 解析 chat-history.txt 格式的 JSON 字符串为消息数组
  209. * 格式:{"data":"时间戳","friend":"消息","me":"消息"},允许重复键
  210. * @param {string} jsonString - JSON 字符串
  211. * @returns {Array<{sender: string, text: string}>}
  212. */
  213. function parseChatHistoryTxtFormat(jsonString) {
  214. const messages = [];
  215. if (!jsonString || typeof jsonString !== 'string') {
  216. return messages;
  217. }
  218. try {
  219. // 由于JSON不支持重复键,我们需要从文本解析每一行
  220. const lines = jsonString.split('\n');
  221. for (const line of lines) {
  222. // 匹配格式:\t"key":"value", 或 \t"key":"value"
  223. const match = line.match(/^\s*"([^"]+)":\s*"([^"]*)"\s*,?\s*$/);
  224. if (match) {
  225. const key = match[1];
  226. const value = match[2];
  227. // 只处理 friend 和 me 键,忽略 data 键(时间戳)
  228. if (key === 'friend' || key === 'me') {
  229. messages.push({
  230. sender: key,
  231. text: value
  232. });
  233. }
  234. }
  235. }
  236. } catch (e) {
  237. // 解析失败,返回空数组
  238. return messages;
  239. }
  240. return messages;
  241. }
  242. /**
  243. * 解析聊天记录文本为结构化数据
  244. * @param {string} chatHistoryText - 聊天记录文本(格式:好友: xxx\n我: xxx)
  245. * @returns {Array<{sender: string, text: string}>}
  246. */
  247. function parseChatHistoryText(chatHistoryText) {
  248. const messages = [];
  249. const lines = chatHistoryText.split('\n').filter(line => line.trim());
  250. for (const line of lines) {
  251. // 匹配格式:对方: xxx、好友: xxx 或 我: xxx
  252. const match = line.match(/^(对方|好友|我):\s*(.+)$/);
  253. if (match) {
  254. const senderLabel = match[1];
  255. const sender = (senderLabel === '对方' || senderLabel === '好友') ? 'friend' : 'me';
  256. const text = match[2].trim();
  257. messages.push({ sender, text });
  258. }
  259. }
  260. return messages;
  261. }
  262. /**
  263. * 读取所有聊天记录(合并所有历史文件)
  264. * @param {string} folderPath - 工作流文件夹路径(相对路径,如 "static/processing/微信聊天自动发送工作流")
  265. * @returns {Promise<{success: boolean, error?: string, messages?: Array}>}
  266. */
  267. async function readAllChatHistory(folderPath) {
  268. try {
  269. // 优先使用新的 API,如果没有则使用旧的(向后兼容)
  270. const api = electronAPI.readChatHistory || electronAPI.readAllChatHistory;
  271. if (!api) {
  272. return { success: false, error: '读取聊天记录 API 不可用' };
  273. }
  274. const result = await api(folderPath);
  275. if (!result.success) {
  276. return { success: false, error: result.error };
  277. }
  278. // console.log(`已读取聊天记录,共 ${result.fileCount || 0} 个文件,${result.totalMessages || 0} 条消息`);
  279. return {
  280. success: true,
  281. messages: result.messages || [],
  282. fileCount: result.fileCount || 0,
  283. totalMessages: result.totalMessages || 0
  284. };
  285. } catch (error) {
  286. return { success: false, error: error.message || '读取聊天记录失败' };
  287. }
  288. }
  289. module.exports = { saveChatHistory, generateHistorySummary, getHistorySummary, readAllChatHistory }