diff --git a/electron/main.ts b/electron/main.ts index c2dbda7..63b22e5 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2241,6 +2241,10 @@ function registerIpcHandlers() { return chatService.getNewMessages(sessionId, minTime, limit) }) + ipcMain.handle('chat:getAntiRevokeSessions', async () => { + return chatService.getAntiRevokeSessions() + }) + ipcMain.handle('chat:updateMessage', async (_, sessionId: string, localId: number, createTime: number, newContent: string) => { return chatService.updateMessage(sessionId, localId, createTime, newContent) }) diff --git a/electron/preload.ts b/electron/preload.ts index 7ba371e..d79c8f9 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -185,6 +185,7 @@ contextBridge.exposeInMainWorld('electronAPI', { chat: { connect: () => ipcRenderer.invoke('chat:connect'), getSessions: () => ipcRenderer.invoke('chat:getSessions'), + getAntiRevokeSessions: () => ipcRenderer.invoke('chat:getAntiRevokeSessions'), getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames), getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'), getContactTypeCounts: () => ipcRenderer.invoke('chat:getContactTypeCounts'), diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 104bf89..628ecf0 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -666,6 +666,9 @@ class ChatService { if (this.connected && wcdbService.isReady()) { return { success: true } } + if (!wcdbService.isReady()) { + this.monitorSetup = false + } const result = await this.connect() if (!result.success) { this.connected = false @@ -709,6 +712,7 @@ class ChatService { console.error('ChatService: 关闭数据库失败:', e) } this.connected = false + this.monitorSetup = false } /** @@ -745,8 +749,12 @@ class ChatService { try { const connectResult = await this.ensureConnected() if (!connectResult.success) return { success: false, error: connectResult.error } - const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) - return await wcdbService.checkMessageAntiRevokeTriggers(normalizedIds) + const { validIds, invalidRows } = await this.filterAntiRevokeSessionIds(sessionIds) + const result = validIds.length > 0 + ? await wcdbService.checkMessageAntiRevokeTriggers(validIds) + : { success: true, rows: [] } + if (!result.success) return result + return { success: true, rows: [...(result.rows || []), ...invalidRows] } } catch (e) { return { success: false, error: String(e) } } @@ -760,8 +768,12 @@ class ChatService { try { const connectResult = await this.ensureConnected() if (!connectResult.success) return { success: false, error: connectResult.error } - const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) - return await wcdbService.installMessageAntiRevokeTriggers(normalizedIds) + const { validIds, invalidRows } = await this.filterAntiRevokeSessionIds(sessionIds) + const result = validIds.length > 0 + ? await wcdbService.installMessageAntiRevokeTriggers(validIds) + : { success: true, rows: [] } + if (!result.success) return result + return { success: true, rows: [...(result.rows || []), ...invalidRows] } } catch (e) { return { success: false, error: String(e) } } @@ -775,8 +787,12 @@ class ChatService { try { const connectResult = await this.ensureConnected() if (!connectResult.success) return { success: false, error: connectResult.error } - const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) - return await wcdbService.uninstallMessageAntiRevokeTriggers(normalizedIds) + const { validIds, invalidRows } = await this.filterAntiRevokeSessionIds(sessionIds) + const result = validIds.length > 0 + ? await wcdbService.uninstallMessageAntiRevokeTriggers(validIds) + : { success: true, rows: [] } + if (!result.success) return result + return { success: true, rows: [...(result.rows || []), ...invalidRows] } } catch (e) { return { success: false, error: String(e) } } @@ -934,6 +950,191 @@ class ChatService { } } + async getAntiRevokeSessions(): Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }> { + try { + const result = await this.getSessions() + if (!result.success || !Array.isArray(result.sessions)) { + return { success: false, error: result.error || '获取会话失败' } + } + + return { + success: true, + sessions: result.sessions.filter((session) => !String(session.username || '').startsWith('gh_')) + } + } catch (e) { + console.error('ChatService: 获取防撤回会话列表失败:', e) + return { success: false, error: String(e) } + } + } + + private getSessionUsername(row: Record): string { + return String( + row.username || + row.user_name || + row.userName || + row.usrName || + row.UsrName || + row.talker || + row.talker_id || + row.talkerId || + '' + ).trim() + } + + private isAntiRevokeContactRow(username: string, row: Record): boolean { + if (!username) return false + if (username.endsWith('@chatroom')) return true + if (username.startsWith('gh_')) return false + + const localType = this.getRowInt(row, ['local_type', 'localType', 'WCDB_CT_local_type'], Number.NaN) + const lowered = username.toLowerCase() + if (this.isEnterpriseOpenimUsername(username)) { + return this.isAllowedEnterpriseOpenimByLocalType(username, localType) + } + if (lowered.startsWith('weixin') && lowered !== 'weixin') return true + return localType === 1 && !FRIEND_EXCLUDE_USERNAMES.has(username) + } + + private async loadAntiRevokeContactMap(usernames: string[]): Promise> { + const targets = Array.from(new Set((usernames || []).map((value) => String(value || '').trim()).filter(Boolean))) + const map = new Map() + if (targets.length === 0) return map + + try { + const contactResult = await wcdbService.getContactsCompact(targets) + if (!contactResult.success || !Array.isArray(contactResult.contacts)) return map + + for (const row of contactResult.contacts as Record[]) { + const username = String(row.username || '').trim() + if (!username || !this.isAntiRevokeContactRow(username, row)) continue + map.set(username, { + displayName: String(row.remark || row.nick_name || row.nickName || row.alias || username).trim() + }) + } + } catch { + return map + } + + return map + } + + private async hasAntiRevokeMessageTables(sessionId: string): Promise { + try { + const tableStatsResult = await wcdbService.getMessageTableStats(sessionId) + if (!tableStatsResult.success || !Array.isArray(tableStatsResult.tables)) return false + return tableStatsResult.tables.some((row: Record) => { + const tableName = String(row.table_name || row.tableName || '').trim() + return tableName.length > 0 + }) + } catch { + return false + } + } + + private async buildAntiRevokeSessionsFromRows(rows: Record[]): Promise { + if (rows.length > 0 && (rows[0]._error || rows[0]._info)) return [] + + const candidateRows: Array<{ username: string; row: Record }> = [] + const privateCandidateIds: string[] = [] + const openimLocalTypeMap = await this.loadContactLocalTypeMapForEnterpriseOpenim(rows.map((row) => this.getSessionUsername(row))) + + for (const row of rows) { + const username = this.getSessionUsername(row) + if (!username) continue + + let sessionLocalType = this.getSessionLocalType(row) + if (!Number.isFinite(sessionLocalType) && this.isEnterpriseOpenimUsername(username)) { + sessionLocalType = openimLocalTypeMap.get(username) + } + if (!this.shouldKeepSession(username, sessionLocalType)) continue + + if (username.endsWith('@chatroom')) { + candidateRows.push({ username, row }) + } else { + privateCandidateIds.push(username) + candidateRows.push({ username, row }) + } + } + + const contactMap = await this.loadAntiRevokeContactMap(privateCandidateIds) + const sessions: ChatSession[] = [] + const myWxid = this.configService.get('myWxid') + const now = Date.now() + + for (const { username, row } of candidateRows) { + const isGroup = username.endsWith('@chatroom') + if (!isGroup && !contactMap.has(username)) continue + if (!await this.hasAntiRevokeMessageTables(username)) continue + + const sortTs = parseInt( + row.sort_timestamp || + row.sortTimestamp || + row.sort_time || + row.sortTime || + '0', + 10 + ) + const lastTs = parseInt( + row.last_timestamp || + row.lastTimestamp || + row.last_msg_time || + row.lastMsgTime || + String(sortTs), + 10 + ) + const summary = this.cleanString(row.summary || row.digest || row.last_msg || row.lastMsg || '') + const lastMsgType = parseInt(row.last_msg_type || row.lastMsgType || '0', 10) + const cached = this.avatarCache.get(username) + const contact = contactMap.get(username) + + const session: ChatSession = { + username, + type: parseInt(row.type || '0', 10), + unreadCount: parseInt(row.unread_count || row.unreadCount || row.unreadcount || '0', 10), + summary: summary || this.getMessageTypeLabel(lastMsgType), + sortTimestamp: sortTs, + lastTimestamp: lastTs, + lastMsgType, + displayName: contact?.displayName || cached?.displayName || username, + avatarUrl: cached?.avatarUrl, + lastMsgSender: row.last_msg_sender, + lastSenderDisplayName: row.last_sender_display_name, + selfWxid: myWxid + } + + const cachedStatus = this.sessionStatusCache.get(username) + if (cachedStatus && now - cachedStatus.updatedAt <= this.sessionStatusCacheTtlMs) { + session.isFolded = cachedStatus.isFolded + session.isMuted = cachedStatus.isMuted + } + + sessions.push(session) + } + + return sessions + } + + private async filterAntiRevokeSessionIds(sessionIds: string[]): Promise<{ + validIds: string[] + invalidRows: Array<{ sessionId: string; success: false; error: string }> + }> { + const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) + if (normalizedIds.length === 0) return { validIds: [], invalidRows: [] } + + const sessionsResult = await this.getAntiRevokeSessions() + const allowedIds = new Set((sessionsResult.sessions || []).map((session) => session.username)) + const validIds = normalizedIds.filter((sessionId) => allowedIds.has(sessionId)) + const invalidRows = normalizedIds + .filter((sessionId) => !allowedIds.has(sessionId)) + .map((sessionId) => ({ + sessionId, + success: false as const, + error: '该会话不是联系人或群聊,或不存在可安装防撤回的消息表' + })) + + return { validIds, invalidRows } + } + private async addMissingOfficialSessions(sessions: ChatSession[], myWxid?: string): Promise { const existing = new Set(sessions.map((session) => String(session.username || '').trim()).filter(Boolean)) try { diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index 8354141..e33dc64 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -92,6 +92,9 @@ export class WcdbService { this.setPaths(this.resourcesPath, this.userDataPath) } this.setLogEnabled(this.logEnabled) + if (this.monitorListener) { + this.callWorker<{ success?: boolean }>('setMonitor').catch(() => { }) + } } catch (e) { // Failed to create worker diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 95a4404..9025a73 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -6,7 +6,7 @@ import { useThemeStore, themes } from '../stores/themeStore' import { useAnalyticsStore } from '../stores/analyticsStore' import { dialog } from '../services/ipc' import * as configService from '../services/config' -import type { ContactInfo } from '../types/models' +import type { ChatSession, ContactInfo } from '../types/models' import { Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy, RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor, @@ -265,6 +265,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [messagePushFilterSearchKeyword, setMessagePushFilterSearchKeyword] = useState('') const [messagePushTypeFilter, setMessagePushTypeFilter] = useState('all') const [messagePushContactOptions, setMessagePushContactOptions] = useState([]) + const [antiRevokeSessions, setAntiRevokeSessions] = useState([]) const [antiRevokeSearchKeyword, setAntiRevokeSearchKeyword] = useState('') const [antiRevokeSelectedIds, setAntiRevokeSelectedIds] = useState>(new Set()) const [antiRevokeStatusMap, setAntiRevokeStatusMap] = useState>({}) @@ -771,10 +772,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) const getCurrentAntiRevokeSessionIds = (): string[] => - normalizeSessionIds(chatSessions.map((session) => session.username)) + normalizeSessionIds(antiRevokeSessions.map((session) => session.username)) - const ensureAntiRevokeSessionsLoaded = async (): Promise => { - const current = getCurrentAntiRevokeSessionIds() + const ensureChatSessionsLoaded = async (): Promise => { + const current = normalizeSessionIds(chatSessions.map((session) => session.username)) if (current.length > 0) return current const sessionsResult = await window.electronAPI.chat.getSessions() if (!sessionsResult.success || !sessionsResult.sessions) { @@ -784,6 +785,27 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { return normalizeSessionIds(sessionsResult.sessions.map((session) => session.username)) } + const ensureAntiRevokeSessionsLoaded = async (): Promise => { + const current = getCurrentAntiRevokeSessionIds() + if (current.length > 0) return current + const sessionsResult = await window.electronAPI.chat.getAntiRevokeSessions() + if (!sessionsResult.success || !sessionsResult.sessions) { + throw new Error(sessionsResult.error || '加载会话失败') + } + const nextSessions = sessionsResult.sessions + const nextIds = normalizeSessionIds(nextSessions.map((session) => session.username)) + setAntiRevokeSessions(nextSessions) + setAntiRevokeSelectedIds((prev) => { + const allowed = new Set(nextIds) + return new Set(Array.from(prev).filter((sessionId) => allowed.has(sessionId))) + }) + setAntiRevokeStatusMap((prev) => { + const allowed = new Set(nextIds) + return Object.fromEntries(Object.entries(prev).filter(([sessionId]) => allowed.has(sessionId))) + }) + return nextIds + } + const markAntiRevokeRowsLoading = (sessionIds: string[]) => { setAntiRevokeStatusMap((prev) => { const next = { ...prev } @@ -995,11 +1017,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { let canceled = false ;(async () => { try { - // 两个 Tab 都需要会话列表;antiRevoke 还需要额外检查防撤回状态 - const sessionIds = await ensureAntiRevokeSessionsLoaded() - if (canceled) return if (activeTab === 'antiRevoke') { - await handleRefreshAntiRevokeStatus(sessionIds) + await ensureAntiRevokeSessionsLoaded() + } else { + await ensureChatSessionsLoaded() } } catch (e: any) { if (!canceled) { @@ -2030,7 +2051,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } const renderAntiRevokeTab = () => { - const sortedSessions = [...chatSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) + const sortedSessions = [...antiRevokeSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) const keyword = antiRevokeSearchKeyword.trim().toLowerCase() const filteredSessions = sortedSessions.filter((session) => { if (!keyword) return true @@ -4809,4 +4830,3 @@ export default SettingsPage - diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 7bf4cda..6f9febd 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -271,6 +271,7 @@ export interface ElectronAPI { chat: { connect: () => Promise<{ success: boolean; error?: string }> getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }> + getAntiRevokeSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }> getSessionStatuses: (usernames: string[]) => Promise<{ success: boolean map?: Record