diff --git a/resources/macos/libwcdb_api.dylib b/resources/macos/libwcdb_api.dylib index 1c855b2..16351e8 100755 Binary files a/resources/macos/libwcdb_api.dylib and b/resources/macos/libwcdb_api.dylib differ diff --git a/src/main/index.ts b/src/main/index.ts index 0e4733f..61735d1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -112,6 +112,7 @@ import { import { installSafeConsole } from './safe-log' import { agentHubService } from './services/agent-hub-service' import { groupExitMonitorService } from './services/group-exit-monitor-service' +import { wechatActionLogService } from './services/wechat-action-log-service' import { personalWechatSendService } from './services/personal-wechat-send-service' import { getPersonalWechatSendCapability } from './services/personal-wechat-capability-service' import { scheduledReportService } from './services/scheduled-report-service' @@ -1079,11 +1080,14 @@ app.whenReady().then(async () => { return contacts }) - ipcMain.handle('db:getContactAvatars', async (_, usernames: string[]) => { - const avatars = await chat.getContactAvatars(usernames) - if (chat.isReady()) mergeBootstrapAvatars(chat.getCurrentAccountRoot(), avatars) - return avatars - }) + ipcMain.handle( + 'db:getContactAvatars', + async (_, usernames: string[], options?: { refresh?: boolean }) => { + const avatars = await chat.getContactAvatars(usernames, options) + if (chat.isReady()) mergeBootstrapAvatars(chat.getCurrentAccountRoot(), avatars) + return avatars + } + ) ipcMain.handle( 'db:getMessages', @@ -1130,6 +1134,9 @@ app.whenReady().then(async () => { }) ipcMain.handle('group-exit-monitor:getState', () => groupExitMonitorService.getState()) + ipcMain.handle('group-exit-monitor:setEnabled', (_, enabled: boolean) => + groupExitMonitorService.setEnabled(enabled === true) + ) ipcMain.handle( 'group-exit-monitor:setGroups', (_, roomIds: string[], notificationRoomIds?: string[]) => @@ -1146,6 +1153,7 @@ app.whenReady().then(async () => { ipcMain.handle('group-exit-monitor:markRead', (_, readAt?: number) => groupExitMonitorService.markRead(readAt) ) + ipcMain.handle('wechat-action-log:list', () => wechatActionLogService.list()) ipcMain.handle('db:search', (_, keyword: string) => chat.searchMessages(keyword)) ipcMain.handle( diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index 48adb79..438500a 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -44,6 +44,8 @@ export interface FormattedContact { avatar?: string wechatNickname?: string remark?: string + alias?: string + wechatId?: string isFolded?: boolean isMuted?: boolean } @@ -202,6 +204,8 @@ export function listContacts(filter?: string): FormattedContact[] { avatar: typeof user.avatar === 'string' ? user.avatar : undefined, wechatNickname: user.wechatNickname, remark: user.remark, + alias: user.alias, + wechatId: user.wechatId, isFolded: user.isFolded, isMuted: user.isMuted }) @@ -259,13 +263,18 @@ export async function getGroupNamesAsync(): Promise> { return names } -export async function getContactAvatars(usernames: string[]): Promise> { +export async function getContactAvatars( + usernames: string[], + options?: { refresh?: boolean } +): Promise> { if (!dbRef) return {} const normalized = Array.from( new Set((usernames || []).map((username) => String(username || '').trim()).filter(Boolean)) ) if (normalized.length === 0) return {} - return dbRef.getWcdb4Client().getAvatarUrlsAsync(normalized) + const client = dbRef.getWcdb4Client() + if (options?.refresh) client.invalidateAvatarCache(normalized) + return client.getAvatarUrlsAsync(normalized) } function listSourceMessages( diff --git a/src/main/services/group-exit-monitor-service.ts b/src/main/services/group-exit-monitor-service.ts index 06f4898..ca5604e 100644 --- a/src/main/services/group-exit-monitor-service.ts +++ b/src/main/services/group-exit-monitor-service.ts @@ -17,6 +17,7 @@ import { } from '../../shared/group-exit-monitor' type StoredState = { + enabled?: boolean accountRoot?: string events?: Partial[] lastReadAt?: number @@ -70,6 +71,7 @@ type GroupExitActionGateway = Pick & class GroupExitMonitorService { private readonly actionGateway: GroupExitActionGateway private active = false + private enabled = true private nativeMonitorActive = false private snapshots = new Map() private changeTimer: NodeJS.Timeout | null = null @@ -103,7 +105,8 @@ class GroupExitMonitorService { this.ensureLoaded() return { events: [...this.events], - running: this.active && chat.isReady(), + enabled: this.enabled, + running: this.enabled && this.active && chat.isReady(), nativeMonitorActive: this.nativeMonitorActive, monitoredGroupCount: this.snapshots.size, monitorSelectionConfigured: this.monitorSelectionConfigured, @@ -143,6 +146,11 @@ class GroupExitMonitorService { this.active = true this.nativeMonitorActive = nativeMonitorActive this.checkQueued = false + if (!this.enabled) { + this.save() + this.broadcast() + return + } this.initializing = true const scopeGeneration = ++this.scopeGeneration this.eventSequence += 1 @@ -178,7 +186,7 @@ class GroupExitMonitorService { } notifyDatabaseChanged(rawPayload: string): void { - if (!this.active || !isContactEvent(rawPayload)) return + if (!this.enabled || !this.active || !isContactEvent(rawPayload)) return try { chat.getChatDb()?.getWcdb4Client().invalidateGroupNicknameCache() } catch { @@ -193,6 +201,8 @@ class GroupExitMonitorService { } async checkNow(): Promise { + this.ensureLoaded() + if (!this.enabled) return this.getState() if (!this.active && chat.isReady()) await this.start(false) await this.check() return this.getState() @@ -223,7 +233,31 @@ class GroupExitMonitorService { this.save() this.broadcast() // 建立基线放到后台,保存配置可以立即返回。 - if (this.active) void this.check() + if (this.enabled && this.active) void this.check() + return this.getState() + } + + async setEnabled(enabled: boolean): Promise { + this.ensureLoaded() + if (this.enabled === enabled) return this.getState() + + this.enabled = enabled + this.eventSequence += 1 + this.scopeGeneration += 1 + this.checkQueued = false + this.hydrationQueue.clear() + if (this.changeTimer) clearTimeout(this.changeTimer) + this.changeTimer = null + + if (enabled) { + // 用户主动暂停期间的成员变化不补报;重新开启后从当前状态建立新基线。 + this.snapshots.clear() + this.lastCheckedAt = undefined + this.groupNamesRefreshPending = true + } + this.save() + this.broadcast() + if (enabled && this.active && chat.isReady()) await this.check() return this.getState() } @@ -261,6 +295,7 @@ class GroupExitMonitorService { const currentRoot = chat.getCurrentAccountRoot() if ( !this.active || + !this.enabled || !chat.isReady() || !currentRoot || !this.accountRoot || @@ -296,7 +331,7 @@ class GroupExitMonitorService { const result = await this.readMemberships() const membershipCostMs = Date.now() - startedAt let changedGroups = 0 - if (result.groups && this.active && scopeGeneration === this.scopeGeneration) { + if (result.groups && this.enabled && this.active && scopeGeneration === this.scopeGeneration) { changedGroups = await this.applyCurrentMemberships(result.groups, scopeGeneration) } console.log( @@ -367,7 +402,9 @@ class GroupExitMonitorService { const notifications: Array<{ group: GroupSnapshotRecord; event: GroupExitMonitorEvent }> = [] let changedGroups = 0 for (const membership of groups) { - if (!this.active || scopeGeneration !== this.scopeGeneration) return changedGroups + if (!this.enabled || !this.active || scopeGeneration !== this.scopeGeneration) { + return changedGroups + } if (membership.membersValid === false) continue const previous = this.snapshots.get(membership.roomId) // 空数组可能是查询失败,先保留旧基线,避免误报和覆盖最后有效快照。 @@ -407,7 +444,9 @@ class GroupExitMonitorService { } } - if (!this.active || scopeGeneration !== this.scopeGeneration) return changedGroups + if (!this.enabled || !this.active || scopeGeneration !== this.scopeGeneration) { + return changedGroups + } this.lastCheckedAt = Date.now() // 先把事件和新基线作为同一检查点落盘,再执行可失败的通知动作。 this.save() @@ -419,7 +458,7 @@ class GroupExitMonitorService { } private startNextSnapshotHydration(): void { - if (!this.active) { + if (!this.enabled || !this.active) { this.finishHydrationBatch() return } @@ -635,6 +674,7 @@ class GroupExitMonitorService { this.loaded = true try { const stored = fs.readJsonSync(this.filePath()) as StoredState + this.enabled = stored.enabled !== false this.events = normalizeEvents(stored.events) this.actionGateway.registerMemberEvents?.(this.events) this.lastReadAt = Number(stored.lastReadAt) || 0 @@ -654,6 +694,7 @@ class GroupExitMonitorService { } catch { // 首次启动或文件损坏时从空记录开始。 this.events = [] + this.enabled = true this.lastReadAt = 0 this.monitorSelectionConfigured = true this.monitoredRoomIds.clear() @@ -671,6 +712,7 @@ class GroupExitMonitorService { this.filePath(), { accountRoot: this.accountRoot, + enabled: this.enabled, events: this.events, lastReadAt: this.lastReadAt, monitorSelectionConfigured: this.monitorSelectionConfigured, diff --git a/src/main/services/wechat-action-gateway.ts b/src/main/services/wechat-action-gateway.ts index a0f8c1f..4c49387 100644 --- a/src/main/services/wechat-action-gateway.ts +++ b/src/main/services/wechat-action-gateway.ts @@ -21,6 +21,7 @@ import { personalWechatSendService } from './personal-wechat-send-service' const MAX_AUDIT_RECORDS = 500 const MAX_CONTENT_PREVIEW_LENGTH = 240 +export const AUTOMATION_SEND_INTERVAL_MS = 3_000 const AUTOMATION_PURPOSE_ALLOWLIST = new Set(['scheduled_report', 'member_left_notification']) export interface WechatActionGatewayDependencies { @@ -34,6 +35,7 @@ export interface WechatActionGatewayDependencies { | undefined getUserDataPath?: () => string now?: () => Date + wait?: (milliseconds: number) => Promise } interface LoadedAuditState { @@ -46,12 +48,16 @@ export interface WechatActionPolicyContext { } const defaultDependencies = (): Required< - Pick + Pick< + WechatActionGatewayDependencies, + 'getCapability' | 'send' | 'getUserDataPath' | 'now' | 'wait' + > > => ({ getCapability: () => personalWechatCapabilityService.getPersonalWechatSendCapability(), send: (request) => personalWechatSendService.send(request), getUserDataPath: () => app.getPath('userData'), - now: () => new Date() + now: () => new Date(), + wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)) }) /** @@ -60,12 +66,20 @@ const defaultDependencies = (): Required< */ export class WechatActionGateway { private readonly deps: Required< - Pick + Pick< + WechatActionGatewayDependencies, + 'getCapability' | 'send' | 'getUserDataPath' | 'now' | 'wait' + > > & - Omit + Omit< + WechatActionGatewayDependencies, + 'getCapability' | 'send' | 'getUserDataPath' | 'now' | 'wait' + > private readonly memberEvents = new Map() private readonly inFlight = new Map>() private auditState: LoadedAuditState | null = null + private automationSendTail: Promise = Promise.resolve() + private lastAutomationSendStartedAt: number | undefined constructor(deps: WechatActionGatewayDependencies = {}) { this.deps = { ...defaultDependencies(), ...deps } @@ -87,6 +101,10 @@ export class WechatActionGateway { this.memberEvents.clear() } + listAuditRecords(): WechatActionAuditRecord[] { + return this.loadAuditState().records.map((record) => ({ ...record })) + } + async execute(request: WechatActionRequest): Promise { const normalized = normalizeActionRequest(request) const idempotencyKey = normalized ? this.idempotencyKey(normalized) : undefined @@ -176,7 +194,7 @@ export class WechatActionGateway { let sendResult: PersonalWechatSendResult try { - sendResult = await this.deps.send(toPersonalWechatSendRequest(request)) + sendResult = await this.send(request) } catch (error) { return this.finishFailed( actionId, @@ -201,6 +219,26 @@ export class WechatActionGateway { return this.finishSent(actionId, request, idempotencyKey, startedAt, sendResult) } + private send(request: WechatActionRequest): Promise { + const sendRequest = toPersonalWechatSendRequest(request) + if (request.triggerType !== 'automation') return this.deps.send(sendRequest) + + const queued = this.automationSendTail.then(async () => { + const now = this.deps.now().getTime() + const remaining = this.lastAutomationSendStartedAt !== undefined + ? Math.max(0, AUTOMATION_SEND_INTERVAL_MS - (now - this.lastAutomationSendStartedAt)) + : 0 + if (remaining > 0) await this.deps.wait(remaining) + this.lastAutomationSendStartedAt = this.deps.now().getTime() + return this.deps.send(sendRequest) + }) + this.automationSendTail = queued.then( + () => undefined, + () => undefined + ) + return queued + } + private async resolveEventContext( request: WechatActionRequest ): Promise { @@ -364,6 +402,7 @@ export class WechatActionGateway { ...(request.executionId ? { executionId: request.executionId } : {}), recipientType: request.recipient.type, recipientId: request.recipient.id, + ...(request.recipient.name ? { recipientName: request.recipient.name } : {}), contentType: request.content.type, ...contentAudit(request.content), createdAt: result.startedAt, diff --git a/src/main/services/wechat-action-log-service.ts b/src/main/services/wechat-action-log-service.ts new file mode 100644 index 0000000..3f0e517 --- /dev/null +++ b/src/main/services/wechat-action-log-service.ts @@ -0,0 +1,55 @@ +import type { ActionLogEntry } from '../../shared/action-log' +import type { Contact } from '../../shared/types' +import type { WechatActionAuditRecord } from '../../shared/wechat-action' +import * as chat from './chat-service' +import { wechatActionGateway } from './wechat-action-gateway' + +export interface WechatActionLogServiceDependencies { + listAuditRecords?: () => WechatActionAuditRecord[] + listContacts?: () => Contact[] +} + +export class WechatActionLogService { + private readonly listAuditRecords: () => WechatActionAuditRecord[] + private readonly listContacts: () => Contact[] + + constructor(deps: WechatActionLogServiceDependencies = {}) { + this.listAuditRecords = deps.listAuditRecords || (() => wechatActionGateway.listAuditRecords()) + this.listContacts = deps.listContacts || (() => chat.listContacts()) + } + + list(): ActionLogEntry[] { + let contacts: Contact[] = [] + try { + contacts = this.listContacts() + } catch { + // 数据库未连接时仍可查看历史审计。 + } + const names = new Map( + contacts.map((contact) => [ + contact.m_nsUsrName, + contact.m_nsNickName?.trim() || contact.remark?.trim() || contact.m_nsUsrName + ]) + ) + + return this.listAuditRecords() + .map((record) => ({ + id: record.actionId, + category: 'wechat_send' as const, + source: record.origin, + purpose: record.purpose, + timestamp: record.finishedAt || record.startedAt || record.createdAt, + recipientType: record.recipientType, + recipientId: record.recipientId, + recipientName: record.recipientName || names.get(record.recipientId), + contentType: record.contentType, + contentPreview: record.contentPreview, + status: record.sendStatus, + errorCode: record.errorCode, + reason: record.decisionReason + })) + .sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp)) + } +} + +export const wechatActionLogService = new WechatActionLogService() diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index 2632fd4..2ffe611 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -13,6 +13,8 @@ export interface Wcdb4Session { avatar?: string wechatNickname?: string remark?: string + alias?: string + wechatId?: string isFolded?: boolean isMuted?: boolean raw: Record @@ -1030,6 +1032,14 @@ export class Wcdb4Client { this.sessionDisplayNamesHydrated = false } + invalidateAvatarCache(usernames?: string[]): void { + if (!usernames) { + this.avatarCache.clear() + return + } + for (const username of this.uniq(usernames)) this.avatarCache.delete(username) + } + /** 清理群备注缓存,不影响会话缓存。 */ invalidateGroupNicknameCache(): void { this.groupNicknameCache.clear() @@ -2813,12 +2823,22 @@ export class Wcdb4Client { 'contactRemark', 'contact_remark' ]) + const alias = this.pickString(row, ['alias', 'aliasName', 'alias_name', 'm_nsAlias']) + const wechatId = this.pickString(row, [ + 'wechatId', + 'wechat_id', + 'wxAccount', + 'wx_account', + 'account' + ]) const status = this.sessionStatusCache.get(username) return { username, nickname, wechatNickname, remark, + alias, + wechatId: wechatId || alias, isFolded: status?.isFolded, isMuted: status?.isMuted, raw: row diff --git a/src/main/wechat-db.ts b/src/main/wechat-db.ts index 23b6d4a..cfaf787 100644 --- a/src/main/wechat-db.ts +++ b/src/main/wechat-db.ts @@ -7,6 +7,8 @@ export interface UserContact { avatar?: string wechatNickname?: string remark?: string + alias?: string + wechatId?: string isFolded?: boolean isMuted?: boolean } @@ -102,15 +104,21 @@ export class WechatDb { avatar: session.avatar, wechatNickname: session.wechatNickname, remark: session.remark, + alias: session.alias, + wechatId: session.wechatId, isFolded: session.isFolded, isMuted: session.isMuted })) .filter((contact) => { if (!keyword) return true - return ( - contact.m_nsUsrName.toLowerCase().includes(keyword) || - contact.nickname.toLowerCase().includes(keyword) - ) + return [ + contact.remark, + contact.wechatNickname, + contact.nickname, + contact.alias, + contact.wechatId, + contact.m_nsUsrName + ].some((value) => value?.toLocaleLowerCase().includes(keyword)) }) } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 70857ee..a63cf3f 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -82,6 +82,7 @@ import type { } from '../shared/personal-wechat-voice-runtime' import type { AppLogEntry } from '../shared/app-log' import type { GroupExitMonitorState } from '../shared/group-exit-monitor' +import type { ActionLogEntry } from '../shared/action-log' import type { AppUpdateCheckResult, AppUpdateInstallResult, @@ -213,7 +214,10 @@ declare global { updatedAt: number } | null> getContacts: (filter?: string) => Promise - getContactAvatars: (usernames: string[]) => Promise> + getContactAvatars: ( + usernames: string[], + options?: { refresh?: boolean } + ) => Promise> getCachedMessages: ( userMd5: string, startTime?: number, @@ -258,6 +262,7 @@ declare global { }[] } | null> getGroupExitMonitorState: () => Promise + setGroupExitMonitorEnabled: (enabled: boolean) => Promise setGroupExitMonitorGroups: ( roomIds: string[], notificationRoomIds?: string[] @@ -266,6 +271,7 @@ declare global { checkGroupExitMonitorNow: () => Promise clearGroupExitMonitorEvents: () => Promise markGroupExitMonitorRead: (readAt?: number) => Promise + listWechatActionLogs: () => Promise onGroupExitMonitorState: (callback: (state: GroupExitMonitorState) => void) => () => void search: (keyword: string) => Promise searchKnowledge: (request: KnowledgeSearchIpcRequest) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index 8eabe81..50019de 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -55,6 +55,7 @@ import type { import type { AppLogEntry } from '../shared/app-log' import type { AppUpdateState } from '../shared/app-update' import type { GroupExitMonitorState } from '../shared/group-exit-monitor' +import type { ActionLogEntry } from '../shared/action-log' import type { CacheSummary } from '../shared/cache' import type { ExportRequest, ExportJobProgress } from '../shared/export' import type { ImageDecoderSelectionResult, ImageDecoderStatus } from '../shared/image-decryption' @@ -119,7 +120,8 @@ const api = { getBootstrapCache: () => ipcRenderer.invoke('db:getBootstrapCache'), getStartupCache: () => ipcRenderer.invoke('db:getStartupCache'), getContacts: (filter?: string) => ipcRenderer.invoke('db:getContacts', filter), - getContactAvatars: (usernames: string[]) => ipcRenderer.invoke('db:getContactAvatars', usernames), + getContactAvatars: (usernames: string[], options?: { refresh?: boolean }) => + ipcRenderer.invoke('db:getContactAvatars', usernames, options), getCachedMessages: (userMd5: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('db:getCachedMessages', userMd5, startTime, endTime), getCachedMessagePage: (userMd5: string, startTime?: number, endTime?: number) => @@ -133,6 +135,8 @@ const api = { getGroupSnapshot: (userMd5: string) => ipcRenderer.invoke('db:getGroupSnapshot', userMd5), getGroupExitMonitorState: (): Promise => ipcRenderer.invoke('group-exit-monitor:getState'), + setGroupExitMonitorEnabled: (enabled: boolean): Promise => + ipcRenderer.invoke('group-exit-monitor:setEnabled', enabled), setGroupExitMonitorGroups: ( roomIds: string[], notificationRoomIds?: string[] @@ -148,6 +152,8 @@ const api = { ipcRenderer.invoke('group-exit-monitor:clearEvents'), markGroupExitMonitorRead: (readAt?: number): Promise => ipcRenderer.invoke('group-exit-monitor:markRead', readAt), + listWechatActionLogs: (): Promise => + ipcRenderer.invoke('wechat-action-log:list'), onGroupExitMonitorState: (callback: (state: GroupExitMonitorState) => void) => { const listener = (_event: Electron.IpcRendererEvent, state: GroupExitMonitorState): void => callback(state) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2692870..5f5a496 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -43,6 +43,9 @@ import { runtimePlatform, supportsPersonalWechatSend } from './utils/runtime-env import { useToast } from './components/ui' import { AppUpdatePrompt } from './features/app-update/AppUpdatePrompt' import { GroupExitMonitorWorkspace } from './features/group-exit-monitor/GroupExitMonitorWorkspace' +import { LogsWorkspace } from './features/logs/LogsWorkspace' +import { selectContactAvatarRefreshUsernames } from './utils/contact-avatar' +import { filterContacts } from './utils/contact-search' const SIDEBAR_MIN_WIDTH = 260 const SIDEBAR_MAX_WIDTH = 380 @@ -137,16 +140,6 @@ const areMessagesEquivalent = (left: Message[], right: Message[]): boolean => { return true } -const filterContactList = (list: Contact[], keyword: string): Contact[] => { - const lower = keyword.trim().toLowerCase() - if (!lower) return list - return list.filter((contact) => - [contact.m_nsNickName, contact.m_nsUsrName, contact.remark, contact.wechatNickname].some( - (value) => value?.toLowerCase().includes(lower) - ) - ) -} - type GroupSnapshot = { roomId: string memberCount: number @@ -598,7 +591,7 @@ function App(): React.ReactElement { options?.onProgress?.('正在加载联系人...', 35) const list = await window.api.getContacts() setContacts(list) - setFilteredContacts(filterContactList(list, options?.filterKeyword || '')) + setFilteredContacts(filterContacts(list, options?.filterKeyword || '')) const runId = ++contactAvatarHydrationRunRef.current const hydrate = (): Promise => hydrateContactAvatars(list, runId, options?.onProgress) if (options?.waitForAvatars) { @@ -615,21 +608,7 @@ function App(): React.ReactElement { runId: number, onProgress?: (message: string, percent?: number) => void ): Promise => { - const usernames = Array.from( - new Set( - list - .map((contact) => contact.m_nsUsrName) - .filter((username, index) => { - const contact = list[index] - return ( - username && - !contact.avatar && - !username.startsWith('Group_') && - !username.startsWith('Unknown_') - ) - }) - ) - ) + const usernames = selectContactAvatarRefreshUsernames(list) onProgress?.( usernames.length ? `正在加载头像 0/${usernames.length}...` : '头像缓存已就绪', usernames.length ? 55 : 90 @@ -641,7 +620,7 @@ function App(): React.ReactElement { const chunk = usernames.slice(index, index + chunkSize) if (chunk.length === 0) continue try { - const avatars = await window.api.getContactAvatars(chunk) + const avatars = await window.api.getContactAvatars(chunk, { refresh: true }) if (runId !== contactAvatarHydrationRunRef.current) return loadedCount += chunk.length onProgress?.( @@ -1052,7 +1031,7 @@ function App(): React.ReactElement { if (!contact) return try { - const avatars = await window.api.getContactAvatars([contact.m_nsUsrName]) + const avatars = await window.api.getContactAvatars([contact.m_nsUsrName], { refresh: true }) const avatar = avatars[contact.m_nsUsrName] if (avatar) { const updateContact = (item: Contact): Contact => @@ -1457,7 +1436,7 @@ function App(): React.ReactElement { ]) const handleSearchContacts = (keyword: string): void => { - setFilteredContacts(filterContactList(contacts, keyword)) + setFilteredContacts(filterContacts(contacts, keyword)) } const handleRefreshContacts = async (filterKeyword: string): Promise => { @@ -1944,6 +1923,8 @@ function App(): React.ReactElement { onOpenSendSettings={openWechatSendSettings} /> ) + case 'logs': + return case 'agent-hub': return case 'api': diff --git a/src/renderer/src/components/conversation/ConversationItem.tsx b/src/renderer/src/components/conversation/ConversationItem.tsx index c000d6c..5deb607 100644 --- a/src/renderer/src/components/conversation/ConversationItem.tsx +++ b/src/renderer/src/components/conversation/ConversationItem.tsx @@ -16,22 +16,29 @@ export function ConversationItem({ const wxid = contact.m_nsUsrName const displayName = nickname || wxid || '未命名会话' const initial = (displayName || wxid || '?').charAt(0) - const [repairedAvatar, setRepairedAvatar] = useState<{ username: string; source: string }>() + const [repairedAvatar, setRepairedAvatar] = useState<{ + username: string + source: string + replaces: string + }>() const [failedAvatar, setFailedAvatar] = useState<{ username: string; source: string }>() - const repairedSource = repairedAvatar?.username === wxid ? repairedAvatar.source : undefined + const repairedSource = + repairedAvatar?.username === wxid && repairedAvatar.replaces === contact.avatar + ? repairedAvatar.source + : undefined const avatar = repairedSource || contact.avatar const avatarFailed = failedAvatar?.username === wxid && failedAvatar.source === avatar const handleAvatarError = (): void => { if (!avatar || avatarFailed) return setFailedAvatar({ username: wxid, source: avatar }) - if (contact.type !== 'group' || avatar.startsWith('data:')) return + if (avatar.startsWith('data:')) return void window.api - .getContactAvatars([wxid]) + .getContactAvatars([wxid], { refresh: true }) .then((avatars) => { const fallback = avatars[wxid] if (!fallback || fallback === avatar) return - setRepairedAvatar({ username: wxid, source: fallback }) + setRepairedAvatar({ username: wxid, source: fallback, replaces: avatar }) setFailedAvatar(undefined) }) .catch(() => undefined) diff --git a/src/renderer/src/components/layout/PrimaryNavigation.tsx b/src/renderer/src/components/layout/PrimaryNavigation.tsx index 7aa9cac..c9d90eb 100644 --- a/src/renderer/src/components/layout/PrimaryNavigation.tsx +++ b/src/renderer/src/components/layout/PrimaryNavigation.tsx @@ -46,6 +46,13 @@ function NavIcon({ page }: NavIconProps): React.ReactElement { ) + case 'logs': + return ( + + ) case 'export': return (

实时关注微信群成员变化,成员数量减少时自动留下退群动态。

+