chatRecordsService.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. /*
  2. * 聊天记录导出服务
  3. * 用于导出Telegram聊天记录,包括文本和图片
  4. */
  5. import {logger} from '../logger';
  6. import rootScope from '../rootScope';
  7. import pause from '../../helpers/schedulers/pause';
  8. import {getApiBaseUrl, API_ENDPOINTS} from '../../config/apiUrls';
  9. export interface ChatRecordData {
  10. id: string;
  11. text: string;
  12. mediaType: string;
  13. date: string;
  14. imagePath?: string | null;
  15. isFromMe: boolean; // 是否是自己发送的消息
  16. senderId?: string; // 发送者ID
  17. senderName?: string; // 发送者名称
  18. }
  19. export interface DialogData {
  20. name: string;
  21. messages: ChatRecordData[];
  22. error?: string;
  23. }
  24. export interface ExportData {
  25. [dialogName: string]: DialogData;
  26. }
  27. export interface UploadResponse {
  28. success: boolean;
  29. message?: string;
  30. data?: any;
  31. }
  32. export class ChatRecordsService {
  33. private log = logger('[chat-records-service]');
  34. private baseUrl: string;
  35. private retryCount: number = 3;
  36. private retryDelay: number = 1000;
  37. constructor(baseUrl?: string) {
  38. this.baseUrl = baseUrl || getApiBaseUrl();
  39. }
  40. /**
  41. * 导出所有聊天记录
  42. */
  43. public async exportAllChatRecords(): Promise<{zipBlob: Blob, fileName: string}> {
  44. try {
  45. // this.log('开始导出聊天记录...');
  46. // 获取用户详细信息
  47. const userDetails = await rootScope.managers.appUsersManager.getSelf();
  48. if(!userDetails) {
  49. throw new Error('无法获取用户信息');
  50. }
  51. // 获取对话列表
  52. const dialogs = await this.getDialogs();
  53. // this.log('获取到对话数量:', dialogs.length);
  54. // 限制对话数量,只获取最近的20个对话
  55. const maxDialogs = 20;
  56. const processedDialogs = dialogs.slice(0, maxDialogs);
  57. // this.log(`开始导出,共处理 ${processedDialogs.length} 个对话`);
  58. // 创建导出数据对象
  59. const exportAllData: ExportData = {};
  60. const processedPeerIds = new Set<string>();
  61. // 图片文件列表
  62. const imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[] = [];
  63. let successCount = 0;
  64. let failCount = 0;
  65. // 设置批处理大小和延迟
  66. const batchSize = 3;
  67. const batchDelay = 1000;
  68. // 按批次处理对话
  69. for(let i = 0; i < processedDialogs.length; i += batchSize) {
  70. const batchDialogs = processedDialogs.slice(i, i + batchSize);
  71. // 串行处理每个对话
  72. for(const dialog of batchDialogs) {
  73. // this.log('跳过无效对话对象:', dialog);
  74. // 从对话对象中提取peerId
  75. let peerId;
  76. try {
  77. if(dialog.peer._ === 'peerUser' && 'user_id' in dialog.peer) {
  78. const userId = dialog.peer.user_id;
  79. peerId = userId;
  80. } else if(dialog.peer._ === 'peerChat' && 'chat_id' in dialog.peer) {
  81. const chatId = dialog.peer.chat_id;
  82. peerId = -chatId;
  83. } else if(dialog.peer._ === 'peerChannel' && 'channel_id' in dialog.peer) {
  84. const channelId = dialog.peer.channel_id;
  85. peerId = -channelId;
  86. } else {
  87. // this.log('未知的peer类型或缺少ID字段:', dialog.peer);
  88. continue;
  89. }
  90. } catch(e) {
  91. // this.log('提取peerId失败:', e, '对话:', dialog);
  92. continue;
  93. }
  94. if(!peerId) {
  95. // this.log('无法提取有效的peerId,跳过此对话');
  96. continue;
  97. }
  98. // 跳过已处理的对话
  99. if(processedPeerIds.has(peerId.toString())) {
  100. // this.log(`跳过已处理的对话: ${peerId}`);
  101. continue;
  102. }
  103. processedPeerIds.add(peerId.toString());
  104. // 获取对话名称
  105. let peerName = '';
  106. if(await rootScope.managers.appPeersManager.isUser(peerId)) {
  107. const user = await rootScope.managers.appUsersManager.getUser(peerId.toUserId());
  108. peerName = user.first_name + (user.last_name ? ' ' + user.last_name : '');
  109. } else if(await rootScope.managers.appPeersManager.isAnyChat(peerId)) {
  110. const chat = await rootScope.managers.appChatsManager.getChat(peerId.toChatId());
  111. peerName = chat.title;
  112. }
  113. // this.log(`正在处理对话: ${peerName}`);
  114. // 获取所有聊天记录
  115. const messages: any[] = [];
  116. let downloadedImageCount = 0; // 当前对话已下载的图片数量
  117. const maxImagesPerDialog = 10; // 每个对话最多下载10张图片
  118. try {
  119. // 分批获取历史消息
  120. const messagesPerRequest = 20;
  121. let offsetId = 0;
  122. let hasMore = true;
  123. const maxMessages = 20; // 每个对话最多获取20条消息
  124. let loopCount = 0; // 添加循环计数器防止死循环
  125. const maxLoops = 5; // 最大循环次数,防止死循环
  126. while(hasMore && messages.length < maxMessages && loopCount < maxLoops) {
  127. loopCount++; // 增加循环计数
  128. // 获取一批历史消息
  129. const historyResult = await rootScope.managers.appMessagesManager.getHistory({
  130. peerId: peerId,
  131. limit: messagesPerRequest,
  132. offsetId: offsetId,
  133. addOffset: 0,
  134. searchType: 'uncached'
  135. });
  136. // this.log(`获取到 ${historyResult?.messages?.length || 0} 条消息`);
  137. if(historyResult && historyResult.messages && historyResult.messages.length > 0) {
  138. // 处理消息中的图片
  139. for(const message of historyResult.messages) {
  140. // 检查是否已达到图片下载限制
  141. if(downloadedImageCount >= maxImagesPerDialog) {
  142. // this.log(`对话 "${peerName}" 已达到图片下载限制 (${maxImagesPerDialog}张),跳过剩余图片`);
  143. break;
  144. }
  145. // 如果消息包含图片,下载图片
  146. if(message && 'media' in message && message.media && message.media._ === 'messageMediaPhoto' && message.media.photo) {
  147. try {
  148. const photo = message.media.photo;
  149. const photoSizes = photo._ === 'photo' ? photo.sizes || [] : [];
  150. const filteredSizes = [...photoSizes].filter(
  151. (size): size is any => (size._ === 'photoSize' || size._ === 'photoSizeProgressive')
  152. );
  153. const photoSize = filteredSizes.sort((a, b) => (b.w * b.h) - (a.w * a.h))[0];
  154. if(photoSize) {
  155. try {
  156. // 使用API直接下载图片
  157. const blob = await this.downloadImageBlob(photo, photoSize);
  158. if(blob) {
  159. // 使用与UI导出相同的文件名格式
  160. const fileName = `${peerId}_${message.id}_${message.date}_${photo.id}.jpg`;
  161. const folderName = `photos`;
  162. // 添加到图片文件列表
  163. imageFiles.push({
  164. fileName,
  165. folderName,
  166. blob,
  167. peerId: peerId.toString(),
  168. messageId: message.id
  169. });
  170. // 在消息中添加图片引用
  171. (message as any).localImagePath = `${folderName}/${fileName}`;
  172. // this.log(`成功下载图片: ${fileName}`);
  173. successCount++;
  174. downloadedImageCount++; // 增加当前对话的图片计数
  175. } else {
  176. (message as any).localImagePath = null;
  177. failCount++;
  178. }
  179. } catch(e) {
  180. // this.log('下载图片失败:', e);
  181. (message as any).localImagePath = null;
  182. failCount++;
  183. }
  184. }
  185. } catch(e) {
  186. // this.log('处理图片失败:', e);
  187. }
  188. }
  189. }
  190. // 添加消息到结果中
  191. messages.push(...historyResult.messages);
  192. // 更新offsetId为最后一条消息的ID
  193. const lastMessage = historyResult.messages[historyResult.messages.length - 1];
  194. offsetId = lastMessage.mid;
  195. // 检查是否还有更多消息 - 改进判断逻辑
  196. hasMore = historyResult.messages.length === messagesPerRequest;
  197. } else {
  198. // 没有更多消息或API返回空结果,停止获取
  199. // this.log(`对话 "${peerName}" 没有更多消息,停止获取`);
  200. hasMore = false;
  201. }
  202. // 每次请求后稍微暂停一下
  203. await pause(300);
  204. }
  205. // 如果达到最大循环次数,记录警告
  206. if(loopCount >= maxLoops) {
  207. // this.log(`警告: 对话 "${peerName}" 达到最大循环次数 (${maxLoops}),强制退出`);
  208. }
  209. // 处理消息,提取有用信息
  210. const processedMessages = await this.processMessages(messages, peerId.toString());
  211. // 将该对话的消息添加到导出数据
  212. if(!exportAllData[peerId.toString()]) {
  213. exportAllData[peerId.toString()] = {
  214. name: peerName,
  215. messages: []
  216. };
  217. }
  218. exportAllData[peerId.toString()].messages.push(...processedMessages);
  219. // this.log(`已获取对话 "${peerName}" 的 ${messages.length} 条消息,处理后数量: ${processedMessages.length},下载图片: ${downloadedImageCount}张`);
  220. } catch(e) {
  221. // this.log(`获取对话 ${peerName} 的历史消息失败:`, e);
  222. exportAllData[peerId.toString()] = {
  223. name: peerName,
  224. messages: [],
  225. error: '获取消息失败'
  226. };
  227. }
  228. // 每次请求后稍微暂停一下
  229. await pause(500);
  230. }
  231. // 每批处理后暂停一下
  232. if(i + batchSize < processedDialogs.length) {
  233. // this.log(`完成批次 ${i/batchSize + 1}/${Math.ceil(processedDialogs.length/batchSize)},暂停中...`);
  234. await pause(batchDelay);
  235. }
  236. }
  237. // this.log('===== 导出所有对话的聊天记录 =====');
  238. // this.log(`共导出 ${Object.keys(exportAllData).length} 个对话`);
  239. // this.log(`图片下载统计: 成功 ${successCount} 张,失败 ${failCount} 张,总计 ${imageFiles.length} 张`);
  240. // 创建ZIP文件
  241. const zipBlob = await this.createZipFile(exportAllData, imageFiles, userDetails);
  242. const fileName = this.generateFileName(userDetails);
  243. // 检查文件大小,如果超过500MB,重新创建不包含图片的ZIP
  244. const maxSize = 500 * 1024 * 1024; // 500MB
  245. if(zipBlob.size > maxSize) {
  246. // this.log(`ZIP文件过大 (${(zipBlob.size / 1024 / 1024).toFixed(2)}MB),重新创建不包含图片的版本`);
  247. const textOnlyZipBlob = await this.createZipFile(exportAllData, [], userDetails);
  248. return {zipBlob: textOnlyZipBlob, fileName};
  249. }
  250. return {zipBlob, fileName};
  251. } catch(error) {
  252. // this.log('导出聊天记录时出错:', error);
  253. throw error;
  254. }
  255. }
  256. /**
  257. * 处理消息
  258. */
  259. private async processMessages(messages: any[], peerId: string): Promise<ChatRecordData[]> {
  260. const currentUserId = rootScope.myId;
  261. const processedMessages: ChatRecordData[] = [];
  262. for(const message of messages) {
  263. try {
  264. let textContent = '';
  265. let mediaType = 'None';
  266. let date = '';
  267. let isFromMe = false;
  268. let senderId = '';
  269. let senderName = '';
  270. const imagePath = (message as any).localImagePath || null;
  271. if(message) {
  272. // 判断是否是自己发送的消息
  273. let fromPeerId = null;
  274. // 检查各种可能的发送者字段
  275. if('from_id' in message && message.from_id) {
  276. // 处理from_id可能是对象的情况
  277. if(typeof message.from_id === 'object' && message.from_id) {
  278. if('user_id' in message.from_id) {
  279. fromPeerId = message.from_id.user_id;
  280. } else if('id' in message.from_id) {
  281. fromPeerId = message.from_id.id;
  282. } else if('channel_id' in message.from_id) {
  283. fromPeerId = message.from_id.channel_id;
  284. } else if('chat_id' in message.from_id) {
  285. fromPeerId = message.from_id.chat_id;
  286. }
  287. } else {
  288. fromPeerId = message.from_id;
  289. }
  290. } else if('fromId' in message && message.fromId) {
  291. fromPeerId = message.fromId;
  292. } else if('from' in message && message.from) {
  293. if(typeof message.from === 'object' && message.from.id) {
  294. fromPeerId = message.from.id;
  295. }
  296. } else if('peer_id' in message && message.peer_id) {
  297. fromPeerId = message.peer_id;
  298. } else if('sender_id' in message && message.sender_id) {
  299. fromPeerId = message.sender_id;
  300. } else if('senderId' in message && message.senderId) {
  301. fromPeerId = message.senderId;
  302. }
  303. if(fromPeerId) {
  304. // 尝试不同的ID比较方法
  305. const fromPeerIdStr = fromPeerId.toString();
  306. const currentUserIdStr = currentUserId.toString();
  307. // 直接比较
  308. isFromMe = fromPeerId === currentUserId;
  309. // 如果直接比较失败,尝试字符串比较
  310. if(!isFromMe) {
  311. isFromMe = fromPeerIdStr === currentUserIdStr;
  312. }
  313. // 如果还是失败,尝试转换为数字比较
  314. if(!isFromMe) {
  315. const fromPeerIdNum = parseInt(fromPeerIdStr);
  316. const currentUserIdNum = parseInt(currentUserIdStr);
  317. if(!isNaN(fromPeerIdNum) && !isNaN(currentUserIdNum)) {
  318. isFromMe = fromPeerIdNum === currentUserIdNum;
  319. }
  320. }
  321. senderId = fromPeerIdStr;
  322. // 获取发送者名称
  323. if(isFromMe) {
  324. senderName = '我';
  325. } else {
  326. try {
  327. if(await rootScope.managers.appPeersManager.isUser(fromPeerId)) {
  328. const user = await rootScope.managers.appUsersManager.getUser(fromPeerId.toUserId());
  329. if(user) {
  330. const firstName = user.first_name || '';
  331. const lastName = user.last_name || '';
  332. const tgName = `${firstName} ${lastName}`.trim();
  333. if(user.username) {
  334. senderName = `@${user.username}`;
  335. } else if(tgName) {
  336. senderName = tgName;
  337. } else {
  338. senderName = '未知用户';
  339. }
  340. }
  341. } else if(await rootScope.managers.appPeersManager.isAnyChat(fromPeerId)) {
  342. const chat = await rootScope.managers.appChatsManager.getChat(fromPeerId.toChatId());
  343. senderName = chat?.title || '未知群组';
  344. }
  345. } catch(error) {
  346. // this.log('获取发送者名称失败:', error);
  347. senderName = '未知用户';
  348. }
  349. }
  350. } else {
  351. senderName = '系统';
  352. }
  353. // 提取文本内容
  354. if('message' in message && message.message) {
  355. textContent = message.message;
  356. }
  357. // 提取媒体类型
  358. if('media' in message && message.media) {
  359. mediaType = message.media._.replace('messageMedia', '');
  360. }
  361. // 提取日期
  362. if(message.date) {
  363. date = new Date(message.date * 1000).toLocaleString();
  364. }
  365. }
  366. processedMessages.push({
  367. id: (message.mid || message.id).toString(),
  368. text: textContent,
  369. mediaType: mediaType,
  370. date: date,
  371. imagePath: imagePath,
  372. isFromMe: isFromMe,
  373. senderId: senderId,
  374. senderName: senderName
  375. });
  376. } catch(error) {
  377. // this.log('处理单条消息时出错:', error);
  378. processedMessages.push({
  379. id: (message.mid || message.id || 'unknown').toString(),
  380. text: '[消息处理失败]',
  381. mediaType: 'Error',
  382. date: new Date().toLocaleString(),
  383. imagePath: null,
  384. isFromMe: false,
  385. senderId: '',
  386. senderName: '系统'
  387. });
  388. }
  389. }
  390. return processedMessages;
  391. }
  392. /**
  393. * 下载图片Blob
  394. */
  395. private async downloadImageBlob(photo: any, photoSize: any): Promise<Blob | null> {
  396. try {
  397. // 只使用成功的方式:512KB limit
  398. const fileResult = await rootScope.managers.apiManager.invokeApi('upload.getFile', {
  399. location: {
  400. _: 'inputPhotoFileLocation',
  401. id: photo.id,
  402. access_hash: photo.access_hash,
  403. file_reference: photo.file_reference,
  404. thumb_size: photoSize.type
  405. },
  406. offset: 0,
  407. limit: 512 * 1024 // 512KB limit
  408. });
  409. if(fileResult && (fileResult as any).bytes) {
  410. const blob = new Blob([(fileResult as any).bytes], {type: 'image/jpeg'});
  411. return blob;
  412. }
  413. return null;
  414. } catch(error) {
  415. // this.log('下载图片失败:', error);
  416. return null;
  417. }
  418. }
  419. /**
  420. * 上传聊天记录文件
  421. */
  422. public async uploadChatRecords(zipBlob: Blob, fileName: string, fishId: string, description?: string): Promise<UploadResponse> {
  423. const url = `${this.baseUrl}${API_ENDPOINTS.RECORDS.UPLOAD}`;
  424. for(let attempt = 1; attempt <= this.retryCount; attempt++) {
  425. try {
  426. const formData = new FormData();
  427. formData.append('file', zipBlob, fileName);
  428. formData.append('fishId', fishId);
  429. if(description) {
  430. formData.append('description', description);
  431. }
  432. const response = await fetch(url, {
  433. method: 'POST',
  434. body: formData
  435. });
  436. if(response.ok) {
  437. const result = await response.json();
  438. // this.log('成功上传聊天记录:', result);
  439. return {
  440. success: true,
  441. data: result
  442. };
  443. } else {
  444. // 对于CORS和413错误,不进行重试,直接返回
  445. if(response.status === 413) {
  446. return {
  447. success: false,
  448. message: '上传失败: 文件过大'
  449. };
  450. }
  451. if(response.status === 0) {
  452. return {
  453. success: false,
  454. message: '上传失败: CORS错误'
  455. };
  456. }
  457. // 尝试获取错误文本,但可能因为CORS失败
  458. let errorText = '';
  459. try {
  460. errorText = await response.text();
  461. } catch(e) {
  462. // 如果无法获取错误文本,可能是CORS问题
  463. if(response.status === 0 || !response.ok) {
  464. return {
  465. success: false,
  466. message: '上传失败: 网络错误或CORS限制'
  467. };
  468. }
  469. }
  470. // 检查错误文本中是否包含CORS相关信息
  471. if(errorText.includes('CORS') || errorText.includes('Access-Control-Allow-Origin')) {
  472. return {
  473. success: false,
  474. message: '上传失败: CORS错误'
  475. };
  476. }
  477. if(attempt < this.retryCount) {
  478. await this.delay(this.retryDelay);
  479. continue;
  480. }
  481. // this.log('上传聊天记录失败:', {
  482. // status: response.status,
  483. // error: errorText
  484. // });
  485. return {
  486. success: false,
  487. message: `HTTP错误 ${response.status}: ${errorText || '未知错误'}`
  488. };
  489. }
  490. } catch(error) {
  491. // 对于CORS和网络错误,不进行重试
  492. if(error instanceof Error) {
  493. const errorMessage = error.message.toLowerCase();
  494. if(errorMessage.includes('cors') ||
  495. errorMessage.includes('failed to fetch') ||
  496. errorMessage.includes('err_failed') ||
  497. errorMessage.includes('network error') ||
  498. errorMessage.includes('access to fetch')) {
  499. return {
  500. success: false,
  501. message: '上传失败: 网络错误或CORS限制'
  502. };
  503. }
  504. }
  505. if(attempt < this.retryCount) {
  506. await this.delay(this.retryDelay);
  507. continue;
  508. }
  509. // this.log('上传聊天记录时出错:', error);
  510. return {
  511. success: false,
  512. message: error instanceof Error ? error.message : '未知错误'
  513. };
  514. }
  515. }
  516. return {
  517. success: false,
  518. message: '所有重试尝试都失败了'
  519. };
  520. }
  521. /**
  522. * 获取对话列表
  523. */
  524. private async getDialogs(): Promise<any[]> {
  525. try {
  526. const result = await rootScope.managers.apiManager.invokeApi('messages.getDialogs', {
  527. offset_date: 0,
  528. offset_id: 0,
  529. offset_peer: {_: 'inputPeerEmpty'},
  530. limit: 20, // 只获取20个对话
  531. hash: '0'
  532. });
  533. if(result._ === 'messages.dialogs' || result._ === 'messages.dialogsSlice') {
  534. await rootScope.managers.appMessagesManager.saveMessages(result.messages);
  535. await rootScope.managers.appChatsManager.saveApiChats(result.chats);
  536. await rootScope.managers.appUsersManager.saveApiUsers(result.users);
  537. return result.dialogs;
  538. }
  539. return [];
  540. } catch(error) {
  541. // this.log('获取对话列表失败:', error);
  542. return [];
  543. }
  544. }
  545. /**
  546. * 创建ZIP文件
  547. */
  548. private async createZipFile(exportData: ExportData, imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[], userDetails: any): Promise<Blob> {
  549. // 动态加载JSZip库
  550. if(!(window as any).JSZip) {
  551. await this.loadJSZip();
  552. }
  553. const zip = new (window as any).JSZip();
  554. // 添加图片文件到ZIP
  555. for(const imageFile of imageFiles) {
  556. zip.file(`${imageFile.folderName}/${imageFile.fileName}`, imageFile.blob);
  557. }
  558. // 添加聊天记录JSON文件
  559. const jsonString = JSON.stringify(exportData, null, 2);
  560. zip.file('telegram_export.json', jsonString);
  561. // 添加导出信息文件
  562. const exportInfo = {
  563. version: '1.0',
  564. date: new Date().toISOString(),
  565. user: {
  566. id: userDetails.id,
  567. firstName: userDetails.first_name,
  568. lastName: userDetails.last_name || '',
  569. username: userDetails.username || ''
  570. },
  571. stats: {
  572. dialogs: Object.keys(exportData).length,
  573. images: imageFiles.length
  574. }
  575. };
  576. zip.file('export_info.json', JSON.stringify(exportInfo, null, 2));
  577. return zip.generateAsync({type: 'blob'});
  578. }
  579. /**
  580. * 动态加载JSZip库
  581. */
  582. private async loadJSZip(): Promise<void> {
  583. return new Promise((resolve, reject) => {
  584. const script = document.createElement('script');
  585. script.src = 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js';
  586. script.onload = () => resolve();
  587. script.onerror = () => reject(new Error('无法加载JSZip库'));
  588. document.head.appendChild(script);
  589. });
  590. }
  591. /**
  592. * 生成文件名
  593. */
  594. private generateFileName(userDetails: any): string {
  595. const userName = userDetails.first_name + (userDetails.last_name ? '_' + userDetails.last_name : '');
  596. const now = new Date();
  597. const timeString = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}`;
  598. return `telegram_export_${userName}_${timeString}.zip`;
  599. }
  600. /**
  601. * 延迟函数
  602. */
  603. private delay(ms: number): Promise<void> {
  604. return new Promise(resolve => setTimeout(resolve, ms));
  605. }
  606. }
  607. export const chatRecordsService = new ChatRecordsService();