diff --git a/electron/main.ts b/electron/main.ts index 4e331bc..cace612 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -724,6 +724,10 @@ function registerIpcHandlers() { return chatService.getLatestMessages(sessionId, limit) }) + ipcMain.handle('chat:getNewMessages', async (_, sessionId: string, minTime: number, limit?: number) => { + return chatService.getNewMessages(sessionId, minTime, limit) + }) + ipcMain.handle('chat:getContact', async (_, username: string) => { return await chatService.getContact(username) }) @@ -1170,7 +1174,7 @@ function checkForUpdatesOnStartup() { // 检查该版本是否被用户忽略 const ignoredVersion = configService?.get('ignoredUpdateVersion') if (ignoredVersion === latestVersion) { - console.log(`版本 ${latestVersion} 已被用户忽略,跳过更新提示`) + return } diff --git a/electron/preload-env.ts b/electron/preload-env.ts index 70d36d0..3476a0b 100644 --- a/electron/preload-env.ts +++ b/electron/preload-env.ts @@ -29,7 +29,7 @@ function enforceLocalDllPriority() { process.env.PATH = dllPaths } - console.log('[WeFlow] Environment PATH updated to enforce local DLL priority:', dllPaths) + } try { diff --git a/electron/preload.ts b/electron/preload.ts index c0bfb69..628e4cb 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -111,6 +111,8 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit, startTime, endTime, ascending), getLatestMessages: (sessionId: string, limit?: number) => ipcRenderer.invoke('chat:getLatestMessages', sessionId, limit), + getNewMessages: (sessionId: string, minTime: number, limit?: number) => + ipcRenderer.invoke('chat:getNewMessages', sessionId, minTime, limit), getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username), getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username), getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'), @@ -132,7 +134,11 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('chat:execQuery', kind, path, sql), getContacts: () => ipcRenderer.invoke('chat:getContacts'), getMessage: (sessionId: string, localId: number) => - ipcRenderer.invoke('chat:getMessage', sessionId, localId) + ipcRenderer.invoke('chat:getMessage', sessionId, localId), + onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => { + ipcRenderer.on('wcdb-change', callback) + return () => ipcRenderer.removeListener('wcdb-change', callback) + } }, diff --git a/electron/services/analyticsService.ts b/electron/services/analyticsService.ts index e9d965e..8c04476 100644 --- a/electron/services/analyticsService.ts +++ b/electron/services/analyticsService.ts @@ -107,7 +107,11 @@ class AnalyticsService { if (match) return match[1] return trimmed } - return trimmed + + const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private isPrivateSession(username: string, cleanedWxid: string): boolean { @@ -245,6 +249,9 @@ class AnalyticsService { } private async computeAggregateByCursor(sessionIds: string[], beginTimestamp = 0, endTimestamp = 0): Promise { + const wxid = this.configService.get('myWxid') + const cleanedWxid = wxid ? this.cleanAccountDirName(wxid) : '' + const aggregate = { total: 0, sent: 0, @@ -269,8 +276,22 @@ class AnalyticsService { if (endTimestamp > 0 && createTime > endTimestamp) return const localType = parseInt(row.local_type || row.type || '1', 10) - const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? 0 - const isSend = String(isSendRaw) === '1' || isSendRaw === 1 || isSendRaw === true + const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend + let isSend = String(isSendRaw) === '1' || isSendRaw === 1 || isSendRaw === true + + // 如果底层没有提供 is_send,则根据发送者用户名推断 + const senderUsername = row.sender_username || row.senderUsername || row.sender + if (isSendRaw === undefined || isSendRaw === null) { + if (senderUsername && (cleanedWxid)) { + const senderLower = String(senderUsername).toLowerCase() + const myWxidLower = cleanedWxid.toLowerCase() + isSend = ( + senderLower === myWxidLower || + // 兼容非 wxid 开头的账号(如果文件夹名带后缀,如 custom_backup,而 sender 是 custom) + (myWxidLower.startsWith(senderLower + '_')) + ) + } + } aggregate.total += 1 sessionStat.total += 1 diff --git a/electron/services/annualReportService.ts b/electron/services/annualReportService.ts index 4caf50e..a4a31d5 100644 --- a/electron/services/annualReportService.ts +++ b/electron/services/annualReportService.ts @@ -115,8 +115,9 @@ class AnnualReportService { return trimmed } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - if (suffixMatch) return suffixMatch[1] - return trimmed + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private async ensureConnectedWithConfig( @@ -596,9 +597,22 @@ class AnnualReportService { if (!createTime) continue const isSendRaw = row.computed_is_send ?? row.is_send ?? '0' - const isSent = parseInt(isSendRaw, 10) === 1 + let isSent = parseInt(isSendRaw, 10) === 1 const localType = parseInt(row.local_type || row.type || '1', 10) + // 兼容逻辑 + if (isSendRaw === undefined || isSendRaw === null || isSendRaw === '0') { + const sender = String(row.sender_username || row.sender || row.talker || '').toLowerCase() + if (sender) { + const rawLower = rawWxid.toLowerCase() + const cleanedLower = cleanedWxid.toLowerCase() + if (sender === rawLower || sender === cleanedLower || + rawLower.startsWith(sender + '_') || cleanedLower.startsWith(sender + '_')) { + isSent = true + } + } + } + // 响应速度 & 对话发起 if (!conversationStarts.has(sessionId)) { conversationStarts.set(sessionId, { initiated: 0, received: 0 }) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 688c5bd..a53c374 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -1,5 +1,5 @@ import { join, dirname, basename, extname } from 'path' -import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs' +import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, watch } from 'fs' import * as path from 'path' import * as fs from 'fs' import * as https from 'https' @@ -7,7 +7,7 @@ import * as http from 'http' import * as fzstd from 'fzstd' import * as crypto from 'crypto' import Database from 'better-sqlite3' -import { app } from 'electron' +import { app, BrowserWindow } from 'electron' import { ConfigService } from './config' import { wcdbService } from './wcdbService' import { MessageCacheService } from './messageCacheService' @@ -152,9 +152,9 @@ class ChatService { } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - if (suffixMatch) return suffixMatch[1] + const cleaned = suffixMatch ? suffixMatch[1] : trimmed - return trimmed + return cleaned } /** @@ -186,6 +186,9 @@ class ChatService { this.connected = true + // 设置数据库监控 + this.setupDbMonitor() + // 预热 listMediaDbs 缓存(后台异步执行,不阻塞连接) this.warmupMediaDbsCache() @@ -196,6 +199,24 @@ class ChatService { } } + private monitorSetup = false + + private setupDbMonitor() { + if (this.monitorSetup) return + this.monitorSetup = true + + // 使用 C++ DLL 内部的文件监控 (ReadDirectoryChangesW) + // 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更 + wcdbService.setMonitor((type, json) => { + // 广播给所有渲染进程窗口 + BrowserWindow.getAllWindows().forEach((win) => { + if (!win.isDestroyed()) { + win.webContents.send('wcdb-change', { type, json }) + } + }) + }) + } + /** * 预热 media 数据库列表缓存(后台异步执行) */ @@ -543,7 +564,7 @@ class ChatService { FROM contact ` - console.log('查询contact.db...') + const contactResult = await wcdbService.execQuery('contact', null, contactQuery) if (!contactResult.success || !contactResult.rows) { @@ -551,13 +572,13 @@ class ChatService { return { success: false, error: contactResult.error || '查询联系人失败' } } - console.log('查询到', contactResult.rows.length, '条联系人记录') + const rows = contactResult.rows as Record[] // 调试:显示前5条数据样本 - console.log('📋 前5条数据样本:') + rows.slice(0, 5).forEach((row, idx) => { - console.log(` ${idx + 1}. username: ${row.username}, local_type: ${row.local_type}, remark: ${row.remark || '无'}, nick_name: ${row.nick_name || '无'}`) + }) // 调试:统计local_type分布 @@ -566,7 +587,7 @@ class ChatService { const lt = row.local_type || 0 localTypeStats.set(lt, (localTypeStats.get(lt) || 0) + 1) }) - console.log('📊 local_type分布:', Object.fromEntries(localTypeStats)) + // 获取会话表的最后联系时间用于排序 const lastContactTimeMap = new Map() @@ -642,13 +663,8 @@ class ChatService { }) } - console.log('过滤后得到', contacts.length, '个有效联系人') - console.log('📊 按类型统计:', { - friends: contacts.filter(c => c.type === 'friend').length, - groups: contacts.filter(c => c.type === 'group').length, - officials: contacts.filter(c => c.type === 'official').length, - other: contacts.filter(c => c.type === 'other').length - }) + + // 按最近联系时间排序 contacts.sort((a, b) => { @@ -665,7 +681,7 @@ class ChatService { // 移除临时的lastContactTime字段 const result = contacts.map(({ lastContactTime, ...rest }) => rest) - console.log('返回', result.length, '个联系人') + return { success: true, contacts: result } } catch (e) { console.error('ChatService: 获取通讯录失败:', e) @@ -731,7 +747,7 @@ class ChatService { // 如果需要跳过消息(offset > 0),逐批获取但不返回 if (offset > 0) { - console.log(`[ChatService] 跳过消息: offset=${offset}`) + let skipped = 0 while (skipped < offset) { const skipBatch = await wcdbService.fetchMessageBatch(state.cursor) @@ -740,17 +756,17 @@ class ChatService { return { success: false, error: skipBatch.error || '跳过消息失败' } } if (!skipBatch.rows || skipBatch.rows.length === 0) { - console.log('[ChatService] 跳过时没有更多消息') + return { success: true, messages: [], hasMore: false } } skipped += skipBatch.rows.length state.fetched += skipBatch.rows.length if (!skipBatch.hasMore) { - console.log('[ChatService] 跳过时已到达末尾') + return { success: true, messages: [], hasMore: false } } } - console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}`) + } } else if (state && offset !== state.fetched) { // offset 与 fetched 不匹配,说明状态不一致 @@ -913,6 +929,40 @@ class ChatService { } } + async getNewMessages(sessionId: string, minTime: number, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; error?: string }> { + try { + const connectResult = await this.ensureConnected() + if (!connectResult.success) { + return { success: false, error: connectResult.error || '数据库未连接' } + } + + const res = await wcdbService.getNewMessages(sessionId, minTime, limit) + if (!res.success || !res.messages) { + return { success: false, error: res.error || '获取新消息失败' } + } + + // 转换为 Message 对象 + const messages = this.mapRowsToMessages(res.messages as Record[]) + const normalized = this.normalizeMessageOrder(messages) + + // 并发检查并修复缺失 CDN URL 的表情包 + const fixPromises: Promise[] = [] + for (const msg of normalized) { + if (msg.localType === 47 && !msg.emojiCdnUrl && msg.emojiMd5) { + fixPromises.push(this.fallbackEmoticon(msg)) + } + } + if (fixPromises.length > 0) { + await Promise.allSettled(fixPromises) + } + + return { success: true, messages: normalized } + } catch (e) { + console.error('ChatService: 获取增量消息失败:', e) + return { success: false, error: String(e) } + } + } + private normalizeMessageOrder(messages: Message[]): Message[] { if (messages.length < 2) return messages const first = messages[0] @@ -1019,13 +1069,19 @@ class ChatService { if (senderUsername && (myWxidLower || cleanedWxidLower)) { const senderLower = String(senderUsername).toLowerCase() - const expectedIsSend = (senderLower === myWxidLower || senderLower === cleanedWxidLower) ? 1 : 0 + const expectedIsSend = ( + senderLower === myWxidLower || + senderLower === cleanedWxidLower || + // 兼容非 wxid 开头的账号(如果文件夹名带后缀,如 custom_backup,而 sender 是 custom) + (myWxidLower && myWxidLower.startsWith(senderLower + '_')) || + (cleanedWxidLower && cleanedWxidLower.startsWith(senderLower + '_')) + ) ? 1 : 0 if (isSend === null) { isSend = expectedIsSend // [DEBUG] Issue #34: 记录 isSend 推断过程 if (expectedIsSend === 0 && localType === 1) { // 仅在被判为接收且是文本消息时记录,避免刷屏 - // console.log(`[ChatService] inferred isSend=0: sender=${senderUsername}, myWxid=${myWxid} (cleaned=${cleanedWxid})`) + // } } } else if (senderUsername && !myWxid) { @@ -1249,7 +1305,7 @@ class ChatService { return title } } - + // 如果没有 title,根据 type 返回默认标签 switch (type) { case '6': @@ -1607,10 +1663,10 @@ class ChatService { // 文件消息 result.fileName = title || this.extractXmlValue(content, 'filename') result.linkTitle = result.fileName - + // 提取文件大小 - const fileSizeStr = this.extractXmlValue(content, 'totallen') || - this.extractXmlValue(content, 'filesize') + const fileSizeStr = this.extractXmlValue(content, 'totallen') || + this.extractXmlValue(content, 'filesize') if (fileSizeStr) { const size = parseInt(fileSizeStr, 10) if (!isNaN(size)) { @@ -1635,7 +1691,7 @@ class ChatService { case '19': { // 聊天记录 result.chatRecordTitle = title || '聊天记录' - + // 解析聊天记录列表 const recordList: Array<{ datatype: number @@ -1648,10 +1704,10 @@ class ChatService { // 查找所有 标签 const recordItemRegex = /([\s\S]*?)<\/recorditem>/gi let match: RegExpExecArray | null - + while ((match = recordItemRegex.exec(content)) !== null) { const itemXml = match[1] - + const datatypeStr = this.extractXmlValue(itemXml, 'datatype') const sourcename = this.extractXmlValue(itemXml, 'sourcename') const sourcetime = this.extractXmlValue(itemXml, 'sourcetime') @@ -1680,10 +1736,10 @@ class ChatService { // 小程序 result.linkTitle = title result.linkUrl = url - + // 提取缩略图 const thumbUrl = this.extractXmlValue(content, 'thumburl') || - this.extractXmlValue(content, 'cdnthumburl') + this.extractXmlValue(content, 'cdnthumburl') if (thumbUrl) { result.linkThumb = thumbUrl } @@ -1693,11 +1749,11 @@ class ChatService { case '2000': { // 转账 result.linkTitle = title || '[转账]' - + // 可以提取转账金额等信息 const payMemo = this.extractXmlValue(content, 'pay_memo') const feedesc = this.extractXmlValue(content, 'feedesc') - + if (payMemo) { result.linkTitle = payMemo } else if (feedesc) { @@ -1710,9 +1766,9 @@ class ChatService { // 其他类型,提取通用字段 result.linkTitle = title result.linkUrl = url - + const thumbUrl = this.extractXmlValue(content, 'thumburl') || - this.extractXmlValue(content, 'cdnthumburl') + this.extractXmlValue(content, 'cdnthumburl') if (thumbUrl) { result.linkThumb = thumbUrl } @@ -2132,7 +2188,7 @@ class ChatService { private decodeMaybeCompressed(raw: any, fieldName: string = 'unknown'): string { if (!raw) return '' - // console.log(`[ChatService] Decoding ${fieldName}: type=${typeof raw}`, raw) + // // 如果是 Buffer/Uint8Array if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) { @@ -2148,7 +2204,7 @@ class ChatService { const bytes = Buffer.from(raw, 'hex') if (bytes.length > 0) { const result = this.decodeBinaryContent(bytes, raw) - // console.log(`[ChatService] HEX decoded result: ${result}`) + // return result } } @@ -2200,7 +2256,7 @@ class ChatService { // 如果提供了 fallbackValue,且解码结果看起来像二进制垃圾,则返回 fallbackValue if (fallbackValue && replacementCount > 0) { - // console.log(`[ChatService] Binary garbage detected, using fallback: ${fallbackValue}`) + // return fallbackValue } @@ -2794,7 +2850,7 @@ class ChatService { const t1 = Date.now() const msgResult = await this.getMessageByLocalId(sessionId, localId) const t2 = Date.now() - console.log(`[Voice] getMessageByLocalId: ${t2 - t1}ms`) + if (msgResult.success && msgResult.message) { const msg = msgResult.message as any @@ -2813,7 +2869,7 @@ class ChatService { // 检查 WAV 内存缓存 const wavCache = this.voiceWavCache.get(cacheKey) if (wavCache) { - console.log(`[Voice] 内存缓存命中,总耗时: ${Date.now() - startTime}ms`) + return { success: true, data: wavCache.toString('base64') } } @@ -2825,7 +2881,7 @@ class ChatService { const wavData = readFileSync(wavFilePath) // 同时缓存到内存 this.cacheVoiceWav(cacheKey, wavData) - console.log(`[Voice] 文件缓存命中,总耗时: ${Date.now() - startTime}ms`) + return { success: true, data: wavData.toString('base64') } } catch (e) { console.error('[Voice] 读取缓存文件失败:', e) @@ -2855,7 +2911,7 @@ class ChatService { // 从数据库读取 silk 数据 const silkData = await this.getVoiceDataFromMediaDb(msgCreateTime, candidates) const t4 = Date.now() - console.log(`[Voice] getVoiceDataFromMediaDb: ${t4 - t3}ms`) + if (!silkData) { return { success: false, error: '未找到语音数据 (请确保已在微信中播放过该语音)' } @@ -2865,7 +2921,7 @@ class ChatService { // 使用 silk-wasm 解码 const pcmData = await this.decodeSilkToPcm(silkData, 24000) const t6 = Date.now() - console.log(`[Voice] decodeSilkToPcm: ${t6 - t5}ms`) + if (!pcmData) { return { success: false, error: 'Silk 解码失败' } @@ -2875,7 +2931,7 @@ class ChatService { // PCM -> WAV const wavData = this.createWavBuffer(pcmData, 24000) const t8 = Date.now() - console.log(`[Voice] createWavBuffer: ${t8 - t7}ms`) + // 缓存 WAV 数据到内存 this.cacheVoiceWav(cacheKey, wavData) @@ -2883,7 +2939,7 @@ class ChatService { // 缓存 WAV 数据到文件(异步,不阻塞返回) this.cacheVoiceWavToFile(cacheKey, wavData) - console.log(`[Voice] 总耗时: ${Date.now() - startTime}ms`) + return { success: true, data: wavData.toString('base64') } } catch (e) { console.error('ChatService: getVoiceData 失败:', e) @@ -2920,11 +2976,11 @@ class ChatService { let mediaDbFiles: string[] if (this.mediaDbsCache) { mediaDbFiles = this.mediaDbsCache - console.log(`[Voice] listMediaDbs (缓存): 0ms`) + } else { const mediaDbsResult = await wcdbService.listMediaDbs() const t2 = Date.now() - console.log(`[Voice] listMediaDbs: ${t2 - t1}ms`) + let files = mediaDbsResult.success && mediaDbsResult.data ? (mediaDbsResult.data as string[]) : [] @@ -2956,7 +3012,7 @@ class ChatService { "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'VoiceInfo%'" ) const t4 = Date.now() - console.log(`[Voice] 查询VoiceInfo表: ${t4 - t3}ms`) + if (!tablesResult.success || !tablesResult.rows || tablesResult.rows.length === 0) { continue @@ -2969,7 +3025,7 @@ class ChatService { `PRAGMA table_info('${voiceTable}')` ) const t6 = Date.now() - console.log(`[Voice] 查询表结构: ${t6 - t5}ms`) + if (!columnsResult.success || !columnsResult.rows) { continue @@ -3006,7 +3062,7 @@ class ChatService { "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Name2Id%'" ) const t8 = Date.now() - console.log(`[Voice] 查询Name2Id表: ${t8 - t7}ms`) + const name2IdTable = (name2IdTablesResult.success && name2IdTablesResult.rows && name2IdTablesResult.rows.length > 0) ? name2IdTablesResult.rows[0].name @@ -3033,7 +3089,7 @@ class ChatService { `SELECT user_name, rowid FROM ${schema.name2IdTable} WHERE user_name IN (${candidatesStr})` ) const t10 = Date.now() - console.log(`[Voice] 查询chat_name_id: ${t10 - t9}ms`) + if (name2IdResult.success && name2IdResult.rows && name2IdResult.rows.length > 0) { // 构建 chat_name_id 列表 @@ -3046,13 +3102,13 @@ class ChatService { `SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.chatNameIdColumn} IN (${chatNameIdsStr}) AND ${schema.timeColumn} = ${createTime} LIMIT 1` ) const t12 = Date.now() - console.log(`[Voice] 策略1查询语音: ${t12 - t11}ms`) + if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) { const row = voiceResult.rows[0] const silkData = this.decodeVoiceBlob(row.data) if (silkData) { - console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`) + return silkData } } @@ -3066,13 +3122,13 @@ class ChatService { `SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} = ${createTime} LIMIT 1` ) const t14 = Date.now() - console.log(`[Voice] 策略2查询语音: ${t14 - t13}ms`) + if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) { const row = voiceResult.rows[0] const silkData = this.decodeVoiceBlob(row.data) if (silkData) { - console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`) + return silkData } } @@ -3085,13 +3141,13 @@ class ChatService { `SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} BETWEEN ${createTime - 5} AND ${createTime + 5} ORDER BY ABS(${schema.timeColumn} - ${createTime}) LIMIT 1` ) const t16 = Date.now() - console.log(`[Voice] 策略3查询语音: ${t16 - t15}ms`) + if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) { const row = voiceResult.rows[0] const silkData = this.decodeVoiceBlob(row.data) if (silkData) { - console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`) + return silkData } } @@ -3322,7 +3378,7 @@ class ChatService { senderWxid?: string ): Promise<{ success: boolean; transcript?: string; error?: string }> { const startTime = Date.now() - console.log(`[Transcribe] 开始转写: sessionId=${sessionId}, msgId=${msgId}, createTime=${createTime}`) + try { let msgCreateTime = createTime @@ -3333,12 +3389,12 @@ class ChatService { const t1 = Date.now() const msgResult = await this.getMessageById(sessionId, parseInt(msgId, 10)) const t2 = Date.now() - console.log(`[Transcribe] getMessageById: ${t2 - t1}ms`) + if (msgResult.success && msgResult.message) { msgCreateTime = msgResult.message.createTime serverId = msgResult.message.serverId - console.log(`[Transcribe] 获取到 createTime=${msgCreateTime}, serverId=${serverId}`) + } } @@ -3349,19 +3405,19 @@ class ChatService { // 使用正确的 cacheKey(包含 createTime) const cacheKey = this.getVoiceCacheKey(sessionId, msgId, msgCreateTime) - console.log(`[Transcribe] cacheKey=${cacheKey}`) + // 检查转写缓存 const cached = this.voiceTranscriptCache.get(cacheKey) if (cached) { - console.log(`[Transcribe] 缓存命中,总耗时: ${Date.now() - startTime}ms`) + return { success: true, transcript: cached } } // 检查是否正在转写 const pending = this.voiceTranscriptPending.get(cacheKey) if (pending) { - console.log(`[Transcribe] 正在转写中,等待结果`) + return pending } @@ -3370,7 +3426,7 @@ class ChatService { // 检查内存中是否有 WAV 数据 let wavData = this.voiceWavCache.get(cacheKey) if (wavData) { - console.log(`[Transcribe] WAV内存缓存命中,大小: ${wavData.length} bytes`) + } else { // 检查文件缓存 const voiceCacheDir = this.getVoiceCacheDir() @@ -3378,7 +3434,7 @@ class ChatService { if (existsSync(wavFilePath)) { try { wavData = readFileSync(wavFilePath) - console.log(`[Transcribe] WAV文件缓存命中,大小: ${wavData.length} bytes`) + // 同时缓存到内存 this.cacheVoiceWav(cacheKey, wavData) } catch (e) { @@ -3388,39 +3444,39 @@ class ChatService { } if (!wavData) { - console.log(`[Transcribe] WAV缓存未命中,调用 getVoiceData`) + const t3 = Date.now() // 调用 getVoiceData 获取并解码 const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId, senderWxid) const t4 = Date.now() - console.log(`[Transcribe] getVoiceData: ${t4 - t3}ms, success=${voiceResult.success}`) + if (!voiceResult.success || !voiceResult.data) { console.error(`[Transcribe] 语音解码失败: ${voiceResult.error}`) return { success: false, error: voiceResult.error || '语音解码失败' } } wavData = Buffer.from(voiceResult.data, 'base64') - console.log(`[Transcribe] WAV数据大小: ${wavData.length} bytes`) + } // 转写 - console.log(`[Transcribe] 开始调用 transcribeWavBuffer`) + const t5 = Date.now() const result = await voiceTranscribeService.transcribeWavBuffer(wavData, (text) => { - console.log(`[Transcribe] 部分结果: ${text}`) + onPartial?.(text) }) const t6 = Date.now() - console.log(`[Transcribe] transcribeWavBuffer: ${t6 - t5}ms, success=${result.success}`) + if (result.success && result.transcript) { - console.log(`[Transcribe] 转写成功: ${result.transcript}`) + this.cacheVoiceTranscript(cacheKey, result.transcript) } else { console.error(`[Transcribe] 转写失败: ${result.error}`) } - console.log(`[Transcribe] 总耗时: ${Date.now() - startTime}ms`) + return result } catch (error) { console.error(`[Transcribe] 异常:`, error) @@ -3468,7 +3524,7 @@ class ChatService { try { // 1. 尝试从缓存获取会话表信息 let tables = this.sessionTablesCache.get(sessionId) - + if (!tables) { // 缓存未命中,查询数据库 const tableStats = await wcdbService.getMessageTableStats(sessionId) diff --git a/electron/services/dualReportService.ts b/electron/services/dualReportService.ts index 3d9a857..a4305c3 100644 --- a/electron/services/dualReportService.ts +++ b/electron/services/dualReportService.ts @@ -74,8 +74,9 @@ class DualReportService { return trimmed } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - if (suffixMatch) return suffixMatch[1] - return trimmed + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private async ensureConnectedWithConfig( @@ -202,7 +203,12 @@ class DualReportService { if (!sender) return false const rawLower = rawWxid ? rawWxid.toLowerCase() : '' const cleanedLower = cleanedWxid ? cleanedWxid.toLowerCase() : '' - return sender === rawLower || sender === cleanedLower + return !!( + sender === rawLower || + sender === cleanedLower || + (rawLower && rawLower.startsWith(sender + '_')) || + (cleanedLower && cleanedLower.startsWith(sender + '_')) + ) } private async getFirstMessages( diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 87dc4e4..bd91034 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -157,8 +157,9 @@ class ExportService { return trimmed } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - if (suffixMatch) return suffixMatch[1] - return trimmed + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> { @@ -968,11 +969,11 @@ class ExportService { const emojiMd5 = msg.emojiMd5 if (!emojiUrl && !emojiMd5) { - console.log('[ExportService] 表情消息缺少 url 和 md5, localId:', msg.localId, 'content:', msg.content?.substring(0, 200)) + return null } - console.log('[ExportService] 导出表情:', { localId: msg.localId, emojiMd5, emojiUrl: emojiUrl?.substring(0, 100) }) + const key = emojiMd5 || String(msg.localId) // 根据 URL 判断扩展名 diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index 3e09181..eb6bb68 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -79,8 +79,13 @@ class GroupAnalyticsService { if (trimmed.toLowerCase().startsWith('wxid_')) { const match = trimmed.match(/^(wxid_[^_]+)/i) if (match) return match[1] + return trimmed } - return trimmed + + const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private async ensureConnected(): Promise<{ success: boolean; error?: string }> { diff --git a/electron/services/imageDecryptService.ts b/electron/services/imageDecryptService.ts index 1fc3648..4b1691f 100644 --- a/electron/services/imageDecryptService.ts +++ b/electron/services/imageDecryptService.ts @@ -380,9 +380,9 @@ export class ImageDecryptService { } const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - if (suffixMatch) return suffixMatch[1] - - return trimmed + const cleaned = suffixMatch ? suffixMatch[1] : trimmed + + return cleaned } private async resolveDatPath( @@ -1136,7 +1136,7 @@ export class ImageDecryptService { // 扫描所有可能的缓存根目录 const allRoots = this.getAllCacheRoots() this.logInfo('开始索引缓存', { roots: allRoots.length }) - + for (const root of allRoots) { try { this.indexCacheDir(root, 3, 0) // 增加深度到3,支持 sessionId/YYYY-MM 结构 @@ -1144,7 +1144,7 @@ export class ImageDecryptService { this.logError('索引目录失败', e, { root }) } } - + this.logInfo('缓存索引完成', { entries: this.resolvedCache.size }) this.cacheIndexed = true this.cacheIndexing = null @@ -1175,7 +1175,7 @@ export class ImageDecryptService { // 默认路径 roots.push(join(documentsPath, 'WeFlow', 'Images')) roots.push(join(documentsPath, 'WeFlow', 'images')) - + // 兼容旧路径(如果有的话) roots.push(join(documentsPath, 'WeFlowData', 'Images')) diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index bdec7de..e12a22d 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -116,13 +116,13 @@ export class KeyService { // 检查是否已经有本地副本,如果有就使用它 if (existsSync(localPath)) { - console.log(`使用已存在的 DLL 本地副本: ${localPath}`) + return localPath } - console.log(`检测到网络路径 DLL,正在复制到本地: ${originalPath} -> ${localPath}`) + copyFileSync(originalPath, localPath) - console.log('DLL 本地化成功') + return localPath } catch (e) { console.error('DLL 本地化失败:', e) @@ -146,7 +146,7 @@ export class KeyService { // 检查是否为网络路径,如果是则本地化 if (this.isNetworkPath(dllPath)) { - console.log('检测到网络路径,将进行本地化处理') + dllPath = this.localizeNetworkDll(dllPath) } @@ -347,7 +347,7 @@ export class KeyService { if (pid) { const runPath = await this.getProcessExecutablePath(pid) if (runPath && existsSync(runPath)) { - console.log('发现正在运行的微信进程,使用路径:', runPath) + return runPath } } diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index 4d5c2cf..05f0f7f 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -57,15 +57,11 @@ class SnsService { } async getTimeline(limit: number = 20, offset: number = 0, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: SnsPost[]; error?: string }> { - console.log('[SnsService] getTimeline called with:', { limit, offset, usernames, keyword, startTime, endTime }) + const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime) - console.log('[SnsService] getSnsTimeline result:', { - success: result.success, - timelineCount: result.timeline?.length, - error: result.error - }) + if (result.success && result.timeline) { const enrichedTimeline = result.timeline.map((post: any, index: number) => { @@ -121,11 +117,11 @@ class SnsService { } }) - console.log('[SnsService] Returning enriched timeline with', enrichedTimeline.length, 'posts') + return { ...result, timeline: enrichedTimeline } } - console.log('[SnsService] Returning result:', result) + return result } async debugResource(url: string): Promise<{ success: boolean; status?: number; headers?: any; error?: string }> { diff --git a/electron/services/voiceTranscribeService.ts b/electron/services/voiceTranscribeService.ts index 5f594c2..9140c21 100644 --- a/electron/services/voiceTranscribeService.ts +++ b/electron/services/voiceTranscribeService.ts @@ -224,12 +224,12 @@ export class VoiceTranscribeService { let finalTranscript = '' worker.on('message', (msg: any) => { - console.log('[VoiceTranscribe] Worker 消息:', msg) + if (msg.type === 'partial') { onPartial?.(msg.text) } else if (msg.type === 'final') { finalTranscript = msg.text - console.log('[VoiceTranscribe] 最终文本:', finalTranscript) + resolve({ success: true, transcript: finalTranscript }) worker.terminate() } else if (msg.type === 'error') { diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index b6b17c9..a5167b3 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -60,6 +60,10 @@ export class WcdbCore { private wcdbGetSnsTimeline: any = null private wcdbGetSnsAnnualStats: any = null private wcdbVerifyUser: any = null + private wcdbStartMonitorPipe: any = null + private wcdbStopMonitorPipe: any = null + private monitorPipeClient: any = null + private avatarUrlCache: Map = new Map() private readonly avatarCacheTtlMs = 10 * 60 * 1000 private logTimer: NodeJS.Timeout | null = null @@ -79,6 +83,136 @@ export class WcdbCore { } } + // 使用命名管道 IPC + startMonitor(callback: (type: string, json: string) => void): boolean { + if (!this.wcdbStartMonitorPipe) { + this.writeLog('startMonitor: wcdbStartMonitorPipe not available') + return false + } + + try { + const result = this.wcdbStartMonitorPipe() + if (result !== 0) { + this.writeLog(`startMonitor: wcdbStartMonitorPipe failed with ${result}`) + return false + } + + const net = require('net') + const PIPE_PATH = '\\\\.\\pipe\\weflow_monitor' + + setTimeout(() => { + this.monitorPipeClient = net.createConnection(PIPE_PATH, () => { + this.writeLog('Monitor pipe connected') + }) + + let buffer = '' + this.monitorPipeClient.on('data', (data: Buffer) => { + buffer += data.toString('utf8') + const lines = buffer.split('\n') + buffer = lines.pop() || '' + for (const line of lines) { + if (line.trim()) { + try { + const parsed = JSON.parse(line) + callback(parsed.action || 'update', line) + } catch { + callback('update', line) + } + } + } + }) + + this.monitorPipeClient.on('error', (err: Error) => { + this.writeLog(`Monitor pipe error: ${err.message}`) + }) + + this.monitorPipeClient.on('close', () => { + this.writeLog('Monitor pipe closed') + this.monitorPipeClient = null + }) + }, 100) + + this.writeLog('Monitor started via named pipe IPC') + return true + } catch (e) { + console.error('startMonitor failed:', e) + return false + } + } + + stopMonitor(): void { + if (this.monitorPipeClient) { + this.monitorPipeClient.destroy() + this.monitorPipeClient = null + } + if (this.wcdbStopMonitorPipe) { + this.wcdbStopMonitorPipe() + } + } + + // 保留旧方法签名以兼容 + setMonitor(callback: (type: string, json: string) => void): boolean { + return this.startMonitor(callback) + } + + /** + * 获取指定时间之后的新消息(增量更新) + */ + getNewMessages(sessionId: string, minTime: number, limit: number = 1000): { success: boolean; messages?: any[]; error?: string } { + if (!this.handle || !this.wcdbOpenMessageCursorLite || !this.wcdbFetchMessageBatch || !this.wcdbCloseMessageCursor) { + return { success: false, error: 'Database not handled or functions missing' } + } + + // 1. Open Cursor + const cursorPtr = Buffer.alloc(8) // int64* + // wcdb_open_message_cursor_lite(handle, sessionId, batchSize, ascending, beginTime, endTime, outCursor) + // ascending=1 (ASC) to get messages AFTER minTime ordered by time + // beginTime = minTime + 1 (to avoid duplicate of the last message) + // Actually, let's use minTime, user logic might handle duplication or we just pass strictly greater + // C++ logic: create_time >= beginTimestamp. So if we want new messages, passing lastTimestamp + 1 is safer. + const openRes = this.wcdbOpenMessageCursorLite(this.handle, sessionId, limit, 1, minTime, 0, cursorPtr) + + if (openRes !== 0) { + return { success: false, error: `Open cursor failed: ${openRes}` } + } + + // Read int64 from buffer + const cursor = cursorPtr.readBigInt64LE(0) + + // 2. Fetch Batch + const outJsonPtr = Buffer.alloc(8) // void** + const outHasMorePtr = Buffer.alloc(4) // int32* + + // fetch_message_batch(handle, cursor, outJson, outHasMore) + const fetchRes = this.wcdbFetchMessageBatch(this.handle, cursor, outJsonPtr, outHasMorePtr) + + let messages: any[] = [] + if (fetchRes === 0) { + const jsonPtr = outJsonPtr.readBigInt64LE(0) // void* address + if (jsonPtr !== 0n) { + // koffi decode string + const jsonStr = this.koffi.decode(jsonPtr, 'string') + this.wcdbFreeString(jsonPtr) // Must free + if (jsonStr) { + try { + messages = JSON.parse(jsonStr) + } catch (e) { + console.error('Parse messages failed', e) + } + } + } + } + + // 3. Close Cursor + this.wcdbCloseMessageCursor(this.handle, cursor) + + if (fetchRes !== 0) { + return { success: false, error: `Fetch batch failed: ${fetchRes}` } + } + + return { success: true, messages } + } + /** * 获取 DLL 路径 */ @@ -122,7 +256,7 @@ export class WcdbCore { if (!force && !this.isLogEnabled()) return const line = `[${new Date().toISOString()}] ${message}` // 同时输出到控制台和文件 - console.log('[WCDB]', message) + try { const base = this.userDataPath || process.env.WCDB_LOG_DIR || process.cwd() const dir = join(base, 'logs') @@ -262,10 +396,10 @@ export class WcdbCore { let protectionOk = false for (const resPath of resourcePaths) { try { - // console.log(`[WCDB] 尝试 InitProtection: ${resPath}`) + // protectionOk = this.wcdbInitProtection(resPath) if (protectionOk) { - // console.log(`[WCDB] InitProtection 成功: ${resPath}`) + // break } } catch (e) { @@ -454,6 +588,17 @@ export class WcdbCore { this.wcdbGetSnsAnnualStats = null } + // Named pipe IPC for monitoring (replaces callback) + try { + this.wcdbStartMonitorPipe = this.lib.func('int32 wcdb_start_monitor_pipe()') + this.wcdbStopMonitorPipe = this.lib.func('void wcdb_stop_monitor_pipe()') + this.writeLog('Monitor pipe functions loaded') + } catch (e) { + console.warn('Failed to load monitor pipe functions:', e) + this.wcdbStartMonitorPipe = null + this.wcdbStopMonitorPipe = null + } + // void VerifyUser(int64_t hwnd_ptr, const char* message, char* out_result, int max_len) try { this.wcdbVerifyUser = this.lib.func('void VerifyUser(int64 hwnd, const char* message, _Out_ char* outResult, int maxLen)') diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index 107c2c8..c8ca667 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -23,6 +23,7 @@ export class WcdbService { private resourcesPath: string | null = null private userDataPath: string | null = null private logEnabled = false + private monitorListener: ((type: string, json: string) => void) | null = null constructor() { this.initWorker() @@ -47,8 +48,16 @@ export class WcdbService { try { this.worker = new Worker(finalPath) - this.worker.on('message', (msg: WorkerMessage) => { - const { id, result, error } = msg + this.worker.on('message', (msg: any) => { + const { id, result, error, type, payload } = msg + + if (type === 'monitor') { + if (this.monitorListener) { + this.monitorListener(payload.type, payload.json) + } + return + } + const p = this.pending.get(id) if (p) { this.pending.delete(id) @@ -122,6 +131,15 @@ export class WcdbService { this.callWorker('setLogEnabled', { enabled }).catch(() => { }) } + /** + * 设置数据库监控回调 + */ + setMonitor(callback: (type: string, json: string) => void): void { + this.monitorListener = callback; + // Notify worker to enable monitor + this.callWorker('setMonitor').catch(() => { }); + } + /** * 检查服务是否就绪 */ @@ -187,6 +205,13 @@ export class WcdbService { return this.callWorker('getMessages', { sessionId, limit, offset }) } + /** + * 获取新消息(增量刷新) + */ + async getNewMessages(sessionId: string, minTime: number, limit: number = 1000): Promise<{ success: boolean; messages?: any[]; error?: string }> { + return this.callWorker('getNewMessages', { sessionId, minTime, limit }) + } + /** * 获取消息总数 */ diff --git a/electron/transcribeWorker.ts b/electron/transcribeWorker.ts index 6e353a0..e5a18d1 100644 --- a/electron/transcribeWorker.ts +++ b/electron/transcribeWorker.ts @@ -80,17 +80,17 @@ function isLanguageAllowed(result: any, allowedLanguages: string[]): boolean { } const langTag = result.lang - console.log('[TranscribeWorker] 检测到语言标记:', langTag) + // 检查是否在允许的语言列表中 for (const lang of allowedLanguages) { if (LANGUAGE_TAGS[lang] === langTag) { - console.log('[TranscribeWorker] 语言匹配,允许:', lang) + return true } } - console.log('[TranscribeWorker] 语言不在白名单中,过滤掉') + return false } @@ -117,7 +117,7 @@ async function run() { allowedLanguages = ['zh'] } - console.log('[TranscribeWorker] 使用的语言白名单:', allowedLanguages) + // 1. 初始化识别器 (SenseVoiceSmall) const recognizerConfig = { @@ -145,15 +145,15 @@ async function run() { recognizer.decode(stream) const result = recognizer.getResult(stream) - console.log('[TranscribeWorker] 识别完成 - 结果对象:', JSON.stringify(result, null, 2)) + // 3. 检查语言是否在白名单中 if (isLanguageAllowed(result, allowedLanguages)) { const processedText = richTranscribePostProcess(result.text) - console.log('[TranscribeWorker] 语言匹配,返回文本:', processedText) + parentPort.postMessage({ type: 'final', text: processedText }) } else { - console.log('[TranscribeWorker] 语言不匹配,返回空文本') + parentPort.postMessage({ type: 'final', text: '' }) } diff --git a/electron/wcdbWorker.ts b/electron/wcdbWorker.ts index 259b372..cf3e89a 100644 --- a/electron/wcdbWorker.ts +++ b/electron/wcdbWorker.ts @@ -19,6 +19,16 @@ if (parentPort) { core.setLogEnabled(payload.enabled) result = { success: true } break + case 'setMonitor': + core.setMonitor((type, json) => { + parentPort!.postMessage({ + id: -1, + type: 'monitor', + payload: { type, json } + }) + }) + result = { success: true } + break case 'testConnection': result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid) break @@ -38,6 +48,9 @@ if (parentPort) { case 'getMessages': result = await core.getMessages(payload.sessionId, payload.limit, payload.offset) break + case 'getNewMessages': + result = await core.getNewMessages(payload.sessionId, payload.minTime, payload.limit) + break case 'getMessageCount': result = await core.getMessageCount(payload.sessionId) break diff --git a/package-lock.json b/package-lock.json index ee1d0f4..409c9c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-router-dom": "^7.1.1", + "react-virtuoso": "^4.18.1", "sherpa-onnx-node": "^1.10.38", "silk-wasm": "^3.7.1", "wechat-emojis": "^1.0.2", @@ -7380,12 +7381,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmmirror.com/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", @@ -8050,6 +8045,16 @@ "react-dom": ">=18" } }, + "node_modules/react-virtuoso": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/react-virtuoso/-/react-virtuoso-4.18.1.tgz", + "integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", diff --git a/package.json b/package.json index 4ba7a88..5319db6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-router-dom": "^7.1.1", + "react-virtuoso": "^4.18.1", "sherpa-onnx-node": "^1.10.38", "silk-wasm": "^3.7.1", "wechat-emojis": "^1.0.2", diff --git a/resources/wcdb_api.dll b/resources/wcdb_api.dll index d63b6bd..8c4fb39 100644 Binary files a/resources/wcdb_api.dll and b/resources/wcdb_api.dll differ diff --git a/src/App.tsx b/src/App.tsx index dff779a..a5121c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -241,18 +241,18 @@ function App() { if (!onboardingDone) { await configService.setOnboardingDone(true) } - console.log('检测到已保存的配置,正在自动连接...') + const result = await window.electronAPI.chat.connect() if (result.success) { - console.log('自动连接成功') + setDbConnected(true, dbPath) // 如果当前在欢迎页,跳转到首页 if (window.location.hash === '#/' || window.location.hash === '') { navigate('/home') } } else { - console.log('自动连接失败:', result.error) + // 如果错误信息包含 VC++ 或 DLL 相关内容,不清除配置,只提示用户 // 其他错误可能需要重新配置 const errorMsg = result.error || '' diff --git a/src/pages/AnnualReportPage.tsx b/src/pages/AnnualReportPage.tsx index d0aa943..626f315 100644 --- a/src/pages/AnnualReportPage.tsx +++ b/src/pages/AnnualReportPage.tsx @@ -91,7 +91,7 @@ function AnnualReportPage() {

年度报告

-

选择年份,生成你的微信聊天年度回顾

+

选择年份,回顾你在微信里的点点滴滴

diff --git a/src/pages/AnnualReportWindow.tsx b/src/pages/AnnualReportWindow.tsx index afa4765..b4b0155 100644 --- a/src/pages/AnnualReportWindow.tsx +++ b/src/pages/AnnualReportWindow.tsx @@ -917,7 +917,7 @@ function AnnualReportWindow() {
-

只要你想
我一直在

+

你只管说
我一直在

{/* 双向奔赴 */} @@ -1025,7 +1025,7 @@ function AnnualReportWindow() {

其中 {midnightKing.displayName} 常常在深夜中陪着你。 -
你和Ta的对话占深夜期间聊天的 {midnightKing.percentage}%。 +
你和Ta的对话占你深夜期间聊天的 {midnightKing.percentage}%

)} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 2b12197..a5c8b3d 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -286,6 +286,11 @@ function ChatPage(_props: ChatPageProps) { setSessions ]) + // 同步 currentSessionId 到 ref + useEffect(() => { + currentSessionRef.current = currentSessionId + }, [currentSessionId]) + // 加载会话列表(优化:先返回基础数据,异步加载联系人信息) const loadSessions = async (options?: { silent?: boolean }) => { if (options?.silent) { @@ -301,6 +306,19 @@ function ChatPage(_props: ChatPageProps) { const nextSessions = options?.silent ? mergeSessions(sessionsArray) : sessionsArray // 确保 nextSessions 也是数组 if (Array.isArray(nextSessions)) { + // 【核心优化】检查当前会话是否有更新(通过 lastTimestamp 对比) + const currentId = currentSessionRef.current + if (currentId) { + const newSession = nextSessions.find(s => s.username === currentId) + const oldSession = sessionsRef.current.find(s => s.username === currentId) + + // 如果会话存在且时间戳变大(有新消息)或者之前没有该会话 + if (newSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) { + console.log(`[Frontend] Detected update for current session ${currentId}, refreshing messages...`) + void handleIncrementalRefresh() + } + } + setSessions(nextSessions) // 立即启动联系人信息加载,不再延迟 500ms void enrichSessionsContactInfo(nextSessions) @@ -330,14 +348,14 @@ function ChatPage(_props: ChatPageProps) { // 防止重复加载 if (isEnrichingRef.current) { - console.log('[性能监控] 联系人信息正在加载中,跳过重复请求') + return } isEnrichingRef.current = true enrichCancelledRef.current = false - console.log(`[性能监控] 开始加载联系人信息,会话数: ${sessions.length}`) + const totalStart = performance.now() // 移除初始 500ms 延迟,让后台加载与 UI 渲染并行 @@ -352,12 +370,12 @@ function ChatPage(_props: ChatPageProps) { // 找出需要加载联系人信息的会话(没有头像或者没有显示名称的) const needEnrich = sessions.filter(s => !s.avatarUrl || !s.displayName || s.displayName === s.username) if (needEnrich.length === 0) { - console.log('[性能监控] 所有联系人信息已缓存,跳过加载') + isEnrichingRef.current = false return } - console.log(`[性能监控] 需要加载的联系人信息: ${needEnrich.length} 个`) + // 进一步减少批次大小,每批3个,避免DLL调用阻塞 const batchSize = 3 @@ -366,7 +384,7 @@ function ChatPage(_props: ChatPageProps) { for (let i = 0; i < needEnrich.length; i += batchSize) { // 如果正在滚动,暂停加载 if (isScrollingRef.current) { - console.log('[性能监控] 检测到滚动,暂停加载联系人信息') + // 等待滚动结束 while (isScrollingRef.current && !enrichCancelledRef.current) { await new Promise(resolve => setTimeout(resolve, 200)) @@ -410,9 +428,9 @@ function ChatPage(_props: ChatPageProps) { const totalTime = performance.now() - totalStart if (!enrichCancelledRef.current) { - console.log(`[性能监控] 联系人信息加载完成,总耗时: ${totalTime.toFixed(2)}ms, 已加载: ${loadedCount}/${needEnrich.length}`) + } else { - console.log(`[性能监控] 联系人信息加载被取消,已加载: ${loadedCount}/${needEnrich.length}`) + } } catch (e) { console.error('加载联系人信息失败:', e) @@ -514,7 +532,7 @@ function ChatPage(_props: ChatPageProps) { // 如果是自己的信息且当前个人头像为空,同步更新 if (myWxid && username === myWxid && contact.avatarUrl && !myAvatarUrl) { - console.log('[ChatPage] 从联系人同步获取到个人头像') + setMyAvatarUrl(contact.avatarUrl) } @@ -542,6 +560,50 @@ function ChatPage(_props: ChatPageProps) { // 刷新当前会话消息(增量更新新消息) const [isRefreshingMessages, setIsRefreshingMessages] = useState(false) + + /** + * 极速增量刷新:基于最后一条消息时间戳,获取后续新消息 + * (由用户建议:记住上一条消息时间,自动取之后的并渲染,然后后台兜底全量同步) + */ + const handleIncrementalRefresh = async () => { + if (!currentSessionId || isRefreshingMessages) return + + // 找出当前已渲染消息中的最大时间戳 + const lastMsg = messages[messages.length - 1] + const minTime = lastMsg?.createTime || 0 + + // 1. 优先执行增量查询并渲染(第一步) + try { + const result = await (window.electronAPI.chat as any).getNewMessages(currentSessionId, minTime) as { + success: boolean; + messages?: Message[]; + error?: string + } + + if (result.success && result.messages && result.messages.length > 0) { + // 过滤去重 + const existingKeys = new Set(messages.map(getMessageKey)) + const newOnes = result.messages.filter(m => !existingKeys.has(getMessageKey(m))) + + if (newOnes.length > 0) { + appendMessages(newOnes, false) + flashNewMessages(newOnes.map(getMessageKey)) + // 滚动到底部 + requestAnimationFrame(() => { + if (messageListRef.current) { + messageListRef.current.scrollTop = messageListRef.current.scrollHeight + } + }) + } + } + } catch (e) { + console.warn('[IncrementalRefresh] 失败,将依赖全量同步兜底:', e) + } + + // 2. 后台兜底:执行之前的完整游标刷新,确保没有遗漏(比如跨库的消息) + void handleRefreshMessages() + } + const handleRefreshMessages = async () => { if (!currentSessionId || isRefreshingMessages) return setJumpStartTime(0) @@ -584,6 +646,31 @@ function ChatPage(_props: ChatPageProps) { } } + // 监听数据库变更实时刷新 + useEffect(() => { + const handleDbChange = (_event: any, data: { type: string; json: string }) => { + try { + const payload = JSON.parse(data.json) + const tableName = payload.table + + // 会话列表更新(主要靠这个触发,因为 wcdb_api 已经只监控 session 了) + if (tableName === 'Session' || tableName === 'session') { + void loadSessions({ silent: true }) + } + } catch (e) { + console.error('解析数据库变更通知失败:', e) + } + } + + if (window.electronAPI.chat.onWcdbChange) { + const removeListener = window.electronAPI.chat.onWcdbChange(handleDbChange) + return () => { + removeListener() + } + } + return () => { } + }, [loadSessions, handleRefreshMessages]) + // 加载消息 const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0) => { const listEl = messageListRef.current @@ -621,7 +708,7 @@ function ChatPage(_props: ChatPageProps) { .map(m => m.senderUsername as string) )] if (unknownSenders.length > 0) { - console.log(`[性能监控] 预取消息发送者信息: ${unknownSenders.length} 个`) + // 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求 const batchPromise = loadContactInfoBatch(unknownSenders) unknownSenders.forEach(username => { @@ -1549,23 +1636,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o useEffect(() => { if (!isVideo) return - console.log('[Video Debug] Full message object:', JSON.stringify(message, null, 2)) - console.log('[Video Debug] Message keys:', Object.keys(message)) - console.log('[Video Debug] Message:', { - localId: message.localId, - localType: message.localType, - hasVideoMd5: !!message.videoMd5, - hasContent: !!message.content, - hasParsedContent: !!message.parsedContent, - hasRawContent: !!(message as any).rawContent, - contentPreview: message.content?.substring(0, 200), - parsedContentPreview: message.parsedContent?.substring(0, 200), - rawContentPreview: (message as any).rawContent?.substring(0, 200) - }) + + + // 优先使用数据库中的 videoMd5 if (message.videoMd5) { - console.log('[Video Debug] Using videoMd5 from message:', message.videoMd5) + setVideoMd5(message.videoMd5) return } @@ -1573,11 +1650,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o // 尝试从多个可能的字段获取原始内容 const contentToUse = message.content || (message as any).rawContent || message.parsedContent if (contentToUse) { - console.log('[Video Debug] Parsing MD5 from content, length:', contentToUse.length) + window.electronAPI.video.parseVideoMd5(contentToUse).then((result: { success: boolean; md5?: string; error?: string }) => { - console.log('[Video Debug] Parse result:', result) + if (result && result.success && result.md5) { - console.log('[Video Debug] Parsed MD5:', result.md5) + setVideoMd5(result.md5) } else { console.error('[Video Debug] Failed to parse MD5:', result) @@ -2061,11 +2138,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o String(message.localId), message.createTime ) - console.log('[ChatPage] 调用转写:', { - sessionId: session.username, - msgId: message.localId, - createTime: message.createTime - }) + if (result.success) { const transcriptText = (result.transcript || '').trim() voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText) @@ -2138,14 +2211,14 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o useEffect(() => { if (!isVideo || !isVideoVisible || videoInfo || videoLoading) return if (!videoMd5) { - console.log('[Video Debug] No videoMd5 available yet') + return } - console.log('[Video Debug] Loading video info for MD5:', videoMd5) + setVideoLoading(true) window.electronAPI.video.getVideoInfo(videoMd5).then((result: { success: boolean; exists: boolean; videoUrl?: string; coverUrl?: string; thumbUrl?: string; error?: string }) => { - console.log('[Video Debug] getVideoInfo result:', result) + if (result && result.success) { setVideoInfo({ exists: result.exists, @@ -2642,7 +2715,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o const fileName = message.fileName || title || '文件' const fileSize = message.fileSize const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || '' - + // 根据扩展名选择图标 const getFileIcon = () => { const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'] @@ -2662,7 +2735,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o ) } - + return (
@@ -2682,10 +2755,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o if (appMsgType === '2000') { try { const content = message.rawContent || message.content || message.parsedContent || '' - + // 添加调试日志 - console.log('[Transfer Debug] Raw content:', content.substring(0, 500)) - + + const parser = new DOMParser() const doc = parser.parseFromString(content, 'text/xml') @@ -2693,11 +2766,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o const payMemo = doc.querySelector('pay_memo')?.textContent || '' const paysubtype = doc.querySelector('paysubtype')?.textContent || '1' - console.log('[Transfer Debug] Parsed:', { feedesc, payMemo, paysubtype, title }) + // paysubtype: 1=待收款, 3=已收款 const isReceived = paysubtype === '3' - + // 如果 feedesc 为空,使用 title 作为降级 const displayAmount = feedesc || title || '微信转账' @@ -2743,7 +2816,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
- +
diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index c4d071f..27868e7 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -41,15 +41,10 @@ function ContactsPage() { return } const contactsResult = await window.electronAPI.chat.getContacts() - console.log('📞 getContacts结果:', contactsResult) + if (contactsResult.success && contactsResult.contacts) { - console.log('📊 总联系人数:', contactsResult.contacts.length) - console.log('📊 按类型统计:', { - friends: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'friend').length, - groups: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'group').length, - officials: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'official').length, - other: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'other').length - }) + + // 获取头像URL const usernames = contactsResult.contacts.map((c: ContactInfo) => c.username) diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 6e512a5..6140902 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -149,7 +149,7 @@ export default function SnsPage() { const currentPosts = postsRef.current if (currentPosts.length > 0) { const topTs = currentPosts[0].createTime - console.log('[SnsPage] Fetching newer posts starts from:', topTs + 1); + const result = await window.electronAPI.sns.getTimeline( limit, @@ -281,10 +281,10 @@ export default function SnsPage() { const checkSchema = async () => { try { const schema = await window.electronAPI.chat.execQuery('sns', null, "PRAGMA table_info(SnsTimeLine)"); - console.log('[SnsPage] SnsTimeLine Schema:', schema); + if (schema.success && schema.rows) { const columns = schema.rows.map((r: any) => r.name); - console.log('[SnsPage] Available columns:', columns); + } } catch (e) { console.error('[SnsPage] Failed to check schema:', e); @@ -335,7 +335,7 @@ export default function SnsPage() { // deltaY < 0 表示向上滚,scrollTop === 0 表示已经在最顶端 if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) { - console.log('[SnsPage] Wheel-up detected at top, loading newer posts...'); + loadPosts({ direction: 'newer' }) } } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index aeff9dd..e00eeeb 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -77,6 +77,11 @@ export interface ElectronAPI { messages?: Message[] error?: string }> + getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{ + success: boolean + messages?: Message[] + error?: string + }> getContact: (username: string) => Promise getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null> getContacts: () => Promise<{ @@ -110,6 +115,7 @@ export interface ElectronAPI { onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => () => void execQuery: (kind: string, path: string | null, sql: string) => Promise<{ success: boolean; rows?: any[]; error?: string }> getMessage: (sessionId: string, localId: number) => Promise<{ success: boolean; message?: Message; error?: string }> + onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => () => void } image: {