chat-history.js 12 KB

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