(options: ParseOptions)
| 143 | // ==================== 解析器实现 ==================== |
| 144 | |
| 145 | async function* parseTxt(options: ParseOptions): AsyncGenerator<ParseEvent, void, unknown> { |
| 146 | const { filePath, batchSize = 5000, onProgress, onLog } = options |
| 147 | |
| 148 | const totalBytes = getFileSize(filePath) |
| 149 | let bytesRead = 0 |
| 150 | let messagesProcessed = 0 |
| 151 | let skippedLines = 0 // 跳过的无效行计数 |
| 152 | |
| 153 | // 发送初始进度 |
| 154 | const initialProgress = createProgress('parsing', 0, totalBytes, 0, '') |
| 155 | yield { type: 'progress', data: initialProgress } |
| 156 | onProgress?.(initialProgress) |
| 157 | |
| 158 | // 记录解析开始 |
| 159 | onLog?.('info', `开始解析 QQ TXT 文件,大小: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`) |
| 160 | |
| 161 | // 收集数据 |
| 162 | let groupName = '未知群聊' |
| 163 | const memberMap = new Map<string, MemberInfo>() |
| 164 | const messages: ParsedMessage[] = [] |
| 165 | |
| 166 | // 当前正在解析的消息 |
| 167 | let currentMessage: { |
| 168 | timestamp: number |
| 169 | platformId: string |
| 170 | nickname: string |
| 171 | contentLines: string[] |
| 172 | } | null = null |
| 173 | |
| 174 | // 保存当前消息 |
| 175 | const saveCurrentMessage = () => { |
| 176 | if (currentMessage) { |
| 177 | const content = currentMessage.contentLines.join('\n').trim() |
| 178 | const type = detectMessageType(content) |
| 179 | |
| 180 | messages.push({ |
| 181 | senderPlatformId: currentMessage.platformId, |
| 182 | senderAccountName: currentMessage.nickname, // QQ TXT 格式只有一个昵称,作为账号名称追踪历史 |
| 183 | // 不设置 senderGroupNickname,避免同一昵称被重复追踪 |
| 184 | timestamp: currentMessage.timestamp, |
| 185 | type, |
| 186 | content: content || null, |
| 187 | }) |
| 188 | |
| 189 | // 更新成员信息(保留最新昵称) |
| 190 | memberMap.set(currentMessage.platformId, { |
| 191 | platformId: currentMessage.platformId, |
| 192 | nickname: currentMessage.nickname, |
| 193 | }) |
| 194 | |
| 195 | messagesProcessed++ |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | // 逐行读取文件 |
| 200 | const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }) |
| 201 | const rl = readline.createInterface({ |
| 202 | input: fileStream, |
nothing calls this directly
no test coverage detected