mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
feat: 优化 Windows 缓存加载和图片显示
This commit is contained in:
@@ -131,7 +131,7 @@ export class ImageDecryptService {
|
||||
join(attachDir, dir1, dir2, 'Image', variant),
|
||||
join(attachDir, dir1, dir2, 'image', variant)
|
||||
]
|
||||
const found = candidates.find((candidate) => existsSync(candidate))
|
||||
const found = this.getLargestExistingPath(candidates, true)
|
||||
if (found) {
|
||||
console.log('[ImageDecrypt] prefix path hit:', found)
|
||||
return found
|
||||
@@ -157,9 +157,10 @@ export class ImageDecryptService {
|
||||
const imgDir = join(attachDir, sessDir, month, sub)
|
||||
if (!existsSync(imgDir)) continue
|
||||
|
||||
const found = variants
|
||||
.map((variant) => join(imgDir, variant))
|
||||
.find((candidate) => existsSync(candidate))
|
||||
const found = this.getLargestExistingPath(
|
||||
variants.map((variant) => join(imgDir, variant)),
|
||||
true
|
||||
)
|
||||
if (found) {
|
||||
console.log('[ImageDecrypt] found at:', found)
|
||||
return found
|
||||
@@ -418,9 +419,9 @@ export class ImageDecryptService {
|
||||
const base = this.normalizeDatBase(baseName)
|
||||
if (!base) return []
|
||||
return [
|
||||
`${base}_h.dat`,
|
||||
`${base}.dat`,
|
||||
`${base}_hd.dat`,
|
||||
`${base}_h.dat`,
|
||||
`${base}_c.dat`,
|
||||
`${base}_t.dat`,
|
||||
`${base}.thumb.dat`,
|
||||
@@ -435,13 +436,35 @@ export class ImageDecryptService {
|
||||
const ordered = allowThumbnail
|
||||
? variants
|
||||
: variants.filter((name) => !this.isThumbnailName(name))
|
||||
for (const variant of ordered) {
|
||||
const candidate = join(actualDir, variant)
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
const largest = this.getLargestExistingPath(
|
||||
ordered.map((variant) => join(actualDir, variant)),
|
||||
allowThumbnail
|
||||
)
|
||||
if (largest) return largest
|
||||
return inputPath
|
||||
}
|
||||
|
||||
private getLargestExistingPath(paths: string[], allowThumbnail: boolean): string | null {
|
||||
const toSized = (candidates: string[]): { candidate: string; size: number }[] =>
|
||||
candidates
|
||||
.filter((candidate) => existsSync(candidate))
|
||||
.map((candidate) => {
|
||||
try {
|
||||
return { candidate, size: statSync(candidate).size }
|
||||
} catch {
|
||||
return { candidate, size: 0 }
|
||||
}
|
||||
})
|
||||
.sort((left, right) => right.size - left.size)
|
||||
|
||||
const nonThumb = toSized(paths.filter((candidate) => !this.isThumbnailName(basename(candidate))))
|
||||
if (nonThumb[0]) return nonThumb[0].candidate
|
||||
if (!allowThumbnail) return null
|
||||
|
||||
const existing = toSized(paths)
|
||||
return existing[0]?.candidate || null
|
||||
}
|
||||
|
||||
private isThumbnailName(fileName: string): boolean {
|
||||
const lower = fileName.toLowerCase()
|
||||
return lower.includes('_t.dat') || lower.includes('_thumb.dat') || lower.includes('.thumb.dat')
|
||||
|
||||
+44
-5
@@ -24,6 +24,15 @@ import {
|
||||
getSettingsPath,
|
||||
AppSettings
|
||||
} from './services/settings-store'
|
||||
import {
|
||||
getBootstrapCache,
|
||||
getCachedMessages,
|
||||
mergeBootstrapAvatars,
|
||||
mergeCachedContactAvatars,
|
||||
saveBootstrapContacts,
|
||||
saveBootstrapSelf,
|
||||
saveCachedMessages
|
||||
} from './services/bootstrap-cache'
|
||||
import { installSafeConsole } from './safe-log'
|
||||
|
||||
// electron-vite can close the child's stdout/stderr after spawning Electron.
|
||||
@@ -228,14 +237,43 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('db:getContacts', (_, filter?: string) => chat.listContacts(filter))
|
||||
ipcMain.handle('db:getBootstrapCache', () => {
|
||||
if (!chat.isReady()) return null
|
||||
return getBootstrapCache(chat.getCurrentAccountRoot())
|
||||
})
|
||||
|
||||
ipcMain.handle('db:getContactAvatars', (_, usernames: string[]) =>
|
||||
chat.getContactAvatars(usernames)
|
||||
ipcMain.handle(
|
||||
'db:getCachedMessages',
|
||||
(_, userMd5: string, startTime?: number, endTime?: number) => {
|
||||
if (!chat.isReady()) return []
|
||||
return getCachedMessages(chat.getCurrentAccountRoot(), userMd5, startTime, endTime)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number) =>
|
||||
chat.listMessages(userMd5, startTime, endTime)
|
||||
ipcMain.handle('db:getContacts', (_, filter?: string) => {
|
||||
const accountRoot = chat.getCurrentAccountRoot()
|
||||
const contacts = accountRoot ? mergeCachedContactAvatars(accountRoot, chat.listContacts(filter)) : chat.listContacts(filter)
|
||||
if (!filter && chat.isReady() && accountRoot) {
|
||||
saveBootstrapContacts(accountRoot, contacts)
|
||||
}
|
||||
return contacts
|
||||
})
|
||||
|
||||
ipcMain.handle('db:getContactAvatars', (_, usernames: string[]) => {
|
||||
const avatars = chat.getContactAvatars(usernames)
|
||||
if (chat.isReady()) mergeBootstrapAvatars(chat.getCurrentAccountRoot(), avatars)
|
||||
return avatars
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'db:getMessages',
|
||||
(_, userMd5: string, startTime?: number, endTime?: number, options?: { limit?: number }) => {
|
||||
const messages = chat.listMessages(userMd5, startTime, endTime, options)
|
||||
if (chat.isReady()) {
|
||||
saveCachedMessages(chat.getCurrentAccountRoot(), userMd5, startTime, endTime, messages)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('db:getGroupSnapshot', (_, userMd5: string) => chat.getGroupSnapshot(userMd5))
|
||||
@@ -374,6 +412,7 @@ app.whenReady().then(async () => {
|
||||
ipcMain.handle('settings:getSelf', () => {
|
||||
const info = chat.getSelfAccountInfo()
|
||||
if (!info) return { ready: false }
|
||||
if (chat.isReady()) saveBootstrapSelf(chat.getCurrentAccountRoot(), info)
|
||||
return { ready: true, info }
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { app } from 'electron'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import type { Contact, Message } from '../../shared/types'
|
||||
|
||||
export interface CachedSelfInfo {
|
||||
wxid: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
accountRoot: string
|
||||
}
|
||||
|
||||
interface CachedMessageBucket {
|
||||
updatedAt: number
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
items: Message[]
|
||||
}
|
||||
|
||||
interface BootstrapCacheFile {
|
||||
version: 1
|
||||
platform: NodeJS.Platform
|
||||
accountRoot: string
|
||||
updatedAt: number
|
||||
self?: CachedSelfInfo
|
||||
contacts?: Contact[]
|
||||
messages?: Record<string, CachedMessageBucket>
|
||||
}
|
||||
|
||||
const CACHE_VERSION = 1
|
||||
const MAX_MESSAGE_BUCKETS = 24
|
||||
const MAX_MESSAGES_PER_BUCKET = 1200
|
||||
|
||||
function normalizeRoot(accountRoot?: string): string {
|
||||
return String(accountRoot || '').trim()
|
||||
}
|
||||
|
||||
function getCacheFile(accountRoot?: string): string {
|
||||
const normalizedRoot = normalizeRoot(accountRoot) || 'default'
|
||||
const hash = crypto
|
||||
.createHash('sha1')
|
||||
.update(`${process.platform}:${normalizedRoot}`)
|
||||
.digest('hex')
|
||||
.slice(0, 16)
|
||||
return path.join(app.getPath('userData'), 'cache', 'bootstrap', `${process.platform}-${hash}.json`)
|
||||
}
|
||||
|
||||
function readCacheFile(accountRoot?: string): BootstrapCacheFile | null {
|
||||
const normalizedRoot = normalizeRoot(accountRoot)
|
||||
if (!normalizedRoot) return null
|
||||
const file = getCacheFile(normalizedRoot)
|
||||
try {
|
||||
if (!fs.existsSync(file)) return null
|
||||
const raw = fs.readJsonSync(file) as Partial<BootstrapCacheFile>
|
||||
if (raw.version !== CACHE_VERSION || raw.platform !== process.platform) return null
|
||||
if (normalizeRoot(raw.accountRoot) !== normalizedRoot) return null
|
||||
return {
|
||||
version: CACHE_VERSION,
|
||||
platform: process.platform,
|
||||
accountRoot: normalizedRoot,
|
||||
updatedAt: Number(raw.updatedAt) || 0,
|
||||
self: raw.self,
|
||||
contacts: Array.isArray(raw.contacts) ? raw.contacts : [],
|
||||
messages: raw.messages && typeof raw.messages === 'object' ? raw.messages : {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[BootstrapCache] read failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeCacheFile(cache: BootstrapCacheFile): void {
|
||||
try {
|
||||
const file = getCacheFile(cache.accountRoot)
|
||||
fs.ensureDirSync(path.dirname(file))
|
||||
fs.writeJsonSync(file, cache, { spaces: 2 })
|
||||
} catch (error) {
|
||||
console.warn('[BootstrapCache] write failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function loadOrCreate(accountRoot?: string): BootstrapCacheFile | null {
|
||||
const normalizedRoot = normalizeRoot(accountRoot)
|
||||
if (!normalizedRoot) return null
|
||||
return (
|
||||
readCacheFile(normalizedRoot) || {
|
||||
version: CACHE_VERSION,
|
||||
platform: process.platform,
|
||||
accountRoot: normalizedRoot,
|
||||
updatedAt: Date.now(),
|
||||
contacts: [],
|
||||
messages: {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function messageBucketKey(userMd5: string, startTime?: number, endTime?: number): string {
|
||||
return `${userMd5}:${startTime ?? ''}:${endTime ?? ''}`
|
||||
}
|
||||
|
||||
function pruneMessageBuckets(messages: Record<string, CachedMessageBucket>): void {
|
||||
const entries = Object.entries(messages)
|
||||
if (entries.length <= MAX_MESSAGE_BUCKETS) return
|
||||
entries
|
||||
.sort((left, right) => (right[1].updatedAt || 0) - (left[1].updatedAt || 0))
|
||||
.slice(MAX_MESSAGE_BUCKETS)
|
||||
.forEach(([key]) => {
|
||||
delete messages[key]
|
||||
})
|
||||
}
|
||||
|
||||
export function getBootstrapCache(accountRoot?: string): {
|
||||
self?: CachedSelfInfo
|
||||
contacts: Contact[]
|
||||
updatedAt: number
|
||||
} | null {
|
||||
const cache = readCacheFile(accountRoot)
|
||||
if (!cache) return null
|
||||
return {
|
||||
self: cache.self,
|
||||
contacts: cache.contacts || [],
|
||||
updatedAt: cache.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeCachedContactAvatars(accountRoot: string, contacts: Contact[]): Contact[] {
|
||||
const cache = readCacheFile(accountRoot)
|
||||
if (!cache?.contacts?.length) return contacts
|
||||
const avatarByUsername = new Map(
|
||||
cache.contacts
|
||||
.filter((contact) => contact.m_nsUsrName && contact.avatar)
|
||||
.map((contact) => [contact.m_nsUsrName, contact.avatar as string])
|
||||
)
|
||||
if (avatarByUsername.size === 0) return contacts
|
||||
return contacts.map((contact) =>
|
||||
contact.avatar || !avatarByUsername.has(contact.m_nsUsrName)
|
||||
? contact
|
||||
: { ...contact, avatar: avatarByUsername.get(contact.m_nsUsrName) }
|
||||
)
|
||||
}
|
||||
|
||||
export function saveBootstrapSelf(accountRoot: string, self: CachedSelfInfo): void {
|
||||
const cache = loadOrCreate(accountRoot)
|
||||
if (!cache) return
|
||||
cache.self = self
|
||||
cache.updatedAt = Date.now()
|
||||
writeCacheFile(cache)
|
||||
}
|
||||
|
||||
export function saveBootstrapContacts(accountRoot: string, contacts: Contact[]): void {
|
||||
const cache = loadOrCreate(accountRoot)
|
||||
if (!cache) return
|
||||
const avatarByUsername = new Map(
|
||||
(cache.contacts || [])
|
||||
.filter((contact) => contact.m_nsUsrName && contact.avatar)
|
||||
.map((contact) => [contact.m_nsUsrName, contact.avatar as string])
|
||||
)
|
||||
cache.contacts = contacts.map((contact) =>
|
||||
contact.avatar || !avatarByUsername.has(contact.m_nsUsrName)
|
||||
? contact
|
||||
: { ...contact, avatar: avatarByUsername.get(contact.m_nsUsrName) }
|
||||
)
|
||||
cache.updatedAt = Date.now()
|
||||
writeCacheFile(cache)
|
||||
}
|
||||
|
||||
export function mergeBootstrapAvatars(accountRoot: string, avatars: Record<string, string>): void {
|
||||
const cache = loadOrCreate(accountRoot)
|
||||
if (!cache || !cache.contacts?.length) return
|
||||
let changed = false
|
||||
cache.contacts = cache.contacts.map((contact) => {
|
||||
const avatar = avatars[contact.m_nsUsrName]
|
||||
if (!avatar || contact.avatar === avatar) return contact
|
||||
changed = true
|
||||
return { ...contact, avatar }
|
||||
})
|
||||
if (!changed) return
|
||||
cache.updatedAt = Date.now()
|
||||
writeCacheFile(cache)
|
||||
}
|
||||
|
||||
export function getCachedMessages(
|
||||
accountRoot: string,
|
||||
userMd5: string,
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
): Message[] {
|
||||
const cache = readCacheFile(accountRoot)
|
||||
const bucket = cache?.messages?.[messageBucketKey(userMd5, startTime, endTime)]
|
||||
return bucket?.items || []
|
||||
}
|
||||
|
||||
export function saveCachedMessages(
|
||||
accountRoot: string,
|
||||
userMd5: string,
|
||||
startTime: number | undefined,
|
||||
endTime: number | undefined,
|
||||
messages: Message[]
|
||||
): void {
|
||||
const cache = loadOrCreate(accountRoot)
|
||||
if (!cache) return
|
||||
const nextMessages = cache.messages || {}
|
||||
nextMessages[messageBucketKey(userMd5, startTime, endTime)] = {
|
||||
updatedAt: Date.now(),
|
||||
startTime,
|
||||
endTime,
|
||||
items: messages.slice(-MAX_MESSAGES_PER_BUCKET)
|
||||
}
|
||||
pruneMessageBuckets(nextMessages)
|
||||
cache.messages = nextMessages
|
||||
cache.updatedAt = Date.now()
|
||||
writeCacheFile(cache)
|
||||
}
|
||||
@@ -155,17 +155,19 @@ export function getContactAvatars(usernames: string[]): Record<string, string> {
|
||||
export function listMessages(
|
||||
userMd5: string,
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
endTime?: number,
|
||||
options?: { limit?: number }
|
||||
): FormattedMessage[] {
|
||||
if (!dbRef) return []
|
||||
|
||||
const startedAt = Date.now()
|
||||
const wcdb4Client = dbRef.getWcdb4Client()
|
||||
const username = wcdb4Client.getUsernameByMd5(userMd5)
|
||||
const isGroupChat = Boolean(username?.endsWith('@chatroom'))
|
||||
console.log(
|
||||
`[ChatService] listMessages begin md5=${userMd5} username=${username || ''} start=${startTime || 0} end=${endTime || 0}`
|
||||
`[ChatService] listMessages begin md5=${userMd5} username=${username || ''} start=${startTime || 0} end=${endTime || 0} limit=${options?.limit || 0}`
|
||||
)
|
||||
const rawMessages = dbRef.getUserMessages(userMd5, startTime, endTime)
|
||||
const rawMessages = dbRef.getUserMessages(userMd5, startTime, endTime, options)
|
||||
console.log(
|
||||
`[ChatService] listMessages native done md5=${userMd5} raw=${rawMessages.length} cost=${Date.now() - startedAt}ms`
|
||||
)
|
||||
@@ -188,14 +190,14 @@ export function listMessages(
|
||||
if (typeof msg.senderAvatar === 'string') img = msg.senderAvatar
|
||||
if (typeof msg.senderNickname === 'string') name = msg.senderNickname
|
||||
}
|
||||
if (content && typeof content === 'string') {
|
||||
if (isGroupChat && content && typeof content === 'string') {
|
||||
const colonIndex = content.indexOf(':')
|
||||
if (colonIndex > 0) {
|
||||
const potentialWxid = content.substring(0, colonIndex)
|
||||
if (potentialWxid.startsWith('wxid_')) {
|
||||
senderId = senderId || potentialWxid
|
||||
name = name || potentialWxid
|
||||
content = content.substring(colonIndex + 1)
|
||||
const potentialSenderId = content.substring(0, colonIndex).trim()
|
||||
if (/^[a-zA-Z0-9_@.-]{3,64}$/.test(potentialSenderId)) {
|
||||
senderId = senderId || potentialSenderId
|
||||
name = name || potentialSenderId
|
||||
content = content.substring(colonIndex + 1).replace(/^\s+/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-15
@@ -24,6 +24,10 @@ export interface Wcdb4Message {
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Wcdb4MessageQueryOptions {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface Wcdb4GroupMember {
|
||||
m_nsUsrName: string
|
||||
nickname: string
|
||||
@@ -597,8 +601,6 @@ export class Wcdb4Client {
|
||||
.map((row) => this.normalizeSession(row))
|
||||
.filter((session) => session.username)
|
||||
|
||||
const sessionUsernames = sessions.map((session) => session.username)
|
||||
this.hydrateDisplayNames(sessionUsernames)
|
||||
this.cachedSessions = sessions.map((session) => ({
|
||||
...session,
|
||||
nickname: this.displayNameCache.get(session.username) || session.nickname || session.username
|
||||
@@ -621,13 +623,19 @@ export class Wcdb4Client {
|
||||
return this.cachedChatTables
|
||||
}
|
||||
|
||||
getMessages(username: string, startTime?: number, endTime?: number): Wcdb4Message[] {
|
||||
getMessages(
|
||||
username: string,
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
options: Wcdb4MessageQueryOptions = {}
|
||||
): Wcdb4Message[] {
|
||||
const startedAt = Date.now()
|
||||
const maxRows = this.normalizeMessageLimit(options.limit)
|
||||
console.log(
|
||||
`[WCDB4] getMessages begin username=${username} start=${startTime || 0} end=${endTime || 0}`
|
||||
`[WCDB4] getMessages begin username=${username} start=${startTime || 0} end=${endTime || 0} limit=${maxRows || 0}`
|
||||
)
|
||||
try {
|
||||
const cursorMessages = this.getMessagesByCursor(username, startTime, endTime)
|
||||
const cursorMessages = this.getMessagesByCursor(username, startTime, endTime, maxRows)
|
||||
if (cursorMessages) {
|
||||
console.log(
|
||||
`[WCDB4] getMessages cursor ok username=${username} rows=${cursorMessages.length} cost=${Date.now() - startedAt}ms`
|
||||
@@ -660,11 +668,12 @@ export class Wcdb4Client {
|
||||
const batch = Array.isArray(rows) ? rows : []
|
||||
allRows.push(...batch)
|
||||
if (batch.length < limit) break
|
||||
if (maxRows && allRows.length >= maxRows) break
|
||||
offset += limit
|
||||
}
|
||||
|
||||
if (allRows.length === 0) {
|
||||
const tableRows = this.getMessagesByTableScan(username, startTime, endTime)
|
||||
const tableRows = this.getMessagesByTableScan(username, startTime, endTime, maxRows)
|
||||
if (tableRows.length > 0) {
|
||||
console.log(
|
||||
`[WCDB4] getMessages table scan ok username=${username} rows=${tableRows.length} cost=${Date.now() - startedAt}ms`
|
||||
@@ -673,7 +682,7 @@ export class Wcdb4Client {
|
||||
}
|
||||
}
|
||||
|
||||
const messages = this.finalizeMessages(username, allRows, startTime, endTime)
|
||||
const messages = this.finalizeMessages(username, allRows, startTime, endTime, maxRows)
|
||||
console.log(
|
||||
`[WCDB4] getMessages direct ok username=${username} rows=${messages.length} cost=${Date.now() - startedAt}ms`
|
||||
)
|
||||
@@ -691,7 +700,8 @@ export class Wcdb4Client {
|
||||
private getMessagesByTableScan(
|
||||
username: string,
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
endTime?: number,
|
||||
limit?: number
|
||||
): Wcdb4Message[] {
|
||||
if (!this.wcdbGetMessageTableStats || !this.wcdbExecQuery) return []
|
||||
|
||||
@@ -722,7 +732,9 @@ export class Wcdb4Client {
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const sql = `SELECT * FROM ${this.quoteSqlIdentifier(table.tableName)}${whereSql} ORDER BY "create_time" ASC LIMIT 5000`
|
||||
const order = limit ? 'DESC' : 'ASC'
|
||||
const rowLimit = limit || 5000
|
||||
const sql = `SELECT * FROM ${this.quoteSqlIdentifier(table.tableName)}${whereSql} ORDER BY "create_time" ${order} LIMIT ${rowLimit}`
|
||||
const rows = this.callJson<Record<string, unknown>[]>((handle, outJson) =>
|
||||
this.wcdbExecQuery!(handle, 'message', table.dbPath, sql, outJson)
|
||||
)
|
||||
@@ -735,7 +747,7 @@ export class Wcdb4Client {
|
||||
}
|
||||
}
|
||||
|
||||
return this.finalizeMessages(username, allRows, startTime, endTime)
|
||||
return this.finalizeMessages(username, allRows, startTime, endTime, limit)
|
||||
}
|
||||
|
||||
getMyAvatarUrl(): string | undefined {
|
||||
@@ -773,7 +785,8 @@ export class Wcdb4Client {
|
||||
private getMessagesByCursor(
|
||||
username: string,
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
endTime?: number,
|
||||
limit?: number
|
||||
): Wcdb4Message[] | null {
|
||||
if (
|
||||
!this.wcdbOpenMessageCursor ||
|
||||
@@ -784,7 +797,7 @@ export class Wcdb4Client {
|
||||
}
|
||||
|
||||
const handle = this.ensureHandle()
|
||||
const batchSize = 1000
|
||||
const batchSize = limit ? Math.min(500, limit) : 1000
|
||||
const cursorOut: WcdbHandleOut = [0]
|
||||
const begin = this.normalizeTimestamp(startTime || 0)
|
||||
const end = this.normalizeTimestamp(endTime || 0)
|
||||
@@ -820,6 +833,7 @@ export class Wcdb4Client {
|
||||
}
|
||||
|
||||
if (!outHasMore[0]) break
|
||||
if (limit && allRows.length >= limit) break
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
@@ -829,18 +843,19 @@ export class Wcdb4Client {
|
||||
}
|
||||
}
|
||||
|
||||
return this.finalizeMessages(username, allRows, startTime, endTime)
|
||||
return this.finalizeMessages(username, allRows, startTime, endTime, limit)
|
||||
}
|
||||
|
||||
private finalizeMessages(
|
||||
username: string,
|
||||
rows: Record<string, unknown>[],
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
endTime?: number,
|
||||
limit?: number
|
||||
): Wcdb4Message[] {
|
||||
const messages = rows.map((row) => this.normalizeMessage(row))
|
||||
|
||||
return messages
|
||||
const sorted = messages
|
||||
.filter((message) => {
|
||||
const createTime = Number(message.msgCreateTime)
|
||||
if (startTime && createTime < startTime) return false
|
||||
@@ -848,6 +863,10 @@ export class Wcdb4Client {
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => Number(a.msgCreateTime) - Number(b.msgCreateTime))
|
||||
|
||||
const visibleMessages = limit && sorted.length > limit ? sorted.slice(-limit) : sorted
|
||||
|
||||
return visibleMessages
|
||||
.map((message) => {
|
||||
if (!message.sender) return message
|
||||
const senderNickname = this.displayNameCache.get(message.sender) || message.senderNickname
|
||||
@@ -869,6 +888,12 @@ export class Wcdb4Client {
|
||||
})
|
||||
}
|
||||
|
||||
private normalizeMessageLimit(limit?: number): number | undefined {
|
||||
const normalized = Number(limit)
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) return undefined
|
||||
return Math.max(1, Math.min(5000, Math.floor(normalized)))
|
||||
}
|
||||
|
||||
getGroupMembers(chatroomId: string): Wcdb4GroupMember[] {
|
||||
if (!this.wcdbGetGroupMembers || !chatroomId) return []
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Wcdb4Client } from './wcdb4-client'
|
||||
import { Wcdb4Client, Wcdb4MessageQueryOptions } from './wcdb4-client'
|
||||
|
||||
export interface UserContact {
|
||||
m_nsUsrName: string
|
||||
@@ -146,10 +146,15 @@ export class WechatDb {
|
||||
this.wcdb4Client.close()
|
||||
}
|
||||
|
||||
public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] {
|
||||
public getUserMessages(
|
||||
userMd5: string,
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
options?: Wcdb4MessageQueryOptions
|
||||
): WechatMessage[] {
|
||||
const username = this.chatMd5ToUsername.get(userMd5)
|
||||
if (!username) return []
|
||||
return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({
|
||||
return this.wcdb4Client.getMessages(username, startTime, endTime, options).map((message) => ({
|
||||
...message,
|
||||
...message.raw
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user