重构与优化,旨在解决遗留的性能问题并优化用户体验,本次提交遗留了较多的待测功能

This commit is contained in:
cc
2026-03-18 23:49:50 +08:00
parent 4c32bf5934
commit 48c4197b16
22 changed files with 2726 additions and 1598 deletions

47
electron/exportWorker.ts Normal file
View File

@@ -0,0 +1,47 @@
import { parentPort, workerData } from 'worker_threads'
import { wcdbService } from './services/wcdbService'
import { exportService, ExportOptions } from './services/exportService'
interface ExportWorkerConfig {
sessionIds: string[]
outputDir: string
options: ExportOptions
resourcesPath?: string
userDataPath?: string
logEnabled?: boolean
}
const config = workerData as ExportWorkerConfig
process.env.WEFLOW_WORKER = '1'
if (config.resourcesPath) {
process.env.WCDB_RESOURCES_PATH = config.resourcesPath
}
wcdbService.setPaths(config.resourcesPath || '', config.userDataPath || '')
wcdbService.setLogEnabled(config.logEnabled === true)
async function run() {
const result = await exportService.exportSessions(
Array.isArray(config.sessionIds) ? config.sessionIds : [],
String(config.outputDir || ''),
config.options || { format: 'json' },
(progress) => {
parentPort?.postMessage({
type: 'export:progress',
data: progress
})
}
)
parentPort?.postMessage({
type: 'export:result',
data: result
})
}
run().catch((error) => {
parentPort?.postMessage({
type: 'export:error',
error: String(error)
})
})

View File

@@ -17,7 +17,6 @@ import { annualReportService } from './services/annualReportService'
import { exportService, ExportOptions, ExportProgress } from './services/exportService'
import { KeyService } from './services/keyService'
import { KeyServiceMac } from './services/keyServiceMac'
import { KeyServiceLinux} from "./services/keyServiceLinux"
import { voiceTranscribeService } from './services/voiceTranscribeService'
import { videoService } from './services/videoService'
import { snsService, isVideoUrl } from './services/snsService'
@@ -96,7 +95,7 @@ let keyService: any
if (process.platform === 'darwin') {
keyService = new KeyServiceMac()
} else if (process.platform === 'linux') {
// const { KeyServiceLinux } = require('./services/keyServiceLinux')
const { KeyServiceLinux } = require('./services/keyServiceLinux')
keyService = new KeyServiceLinux()
} else {
keyService = new KeyService()
@@ -1629,7 +1628,7 @@ function registerIpcHandlers() {
ipcMain.handle('chat:getVoiceTranscript', async (event, sessionId: string, msgId: string, createTime?: number) => {
return chatService.getVoiceTranscript(sessionId, msgId, createTime, (text) => {
event.sender.send('chat:voiceTranscriptPartial', { msgId, text })
event.sender.send('chat:voiceTranscriptPartial', { sessionId, msgId, createTime, text })
})
})
@@ -1641,10 +1640,6 @@ function registerIpcHandlers() {
return chatService.searchMessages(keyword, sessionId, limit, offset, beginTimestamp, endTimestamp)
})
ipcMain.handle('chat:execQuery', async (_, kind: string, path: string | null, sql: string) => {
return chatService.execQuery(kind, path, sql)
})
ipcMain.handle('sns:getTimeline', async (_, limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) => {
return snsService.getTimeline(limit, offset, usernames, keyword, startTime, endTime)
})
@@ -1856,7 +1851,83 @@ function registerIpcHandlers() {
}
}
return exportService.exportSessions(sessionIds, outputDir, options, onProgress)
const runMainFallback = async (reason: string) => {
console.warn(`[fallback-export-main] ${reason}`)
return exportService.exportSessions(sessionIds, outputDir, options, onProgress)
}
const cfg = configService || new ConfigService()
configService = cfg
const logEnabled = cfg.get('logEnabled')
const resourcesPath = app.isPackaged
? join(process.resourcesPath, 'resources')
: join(app.getAppPath(), 'resources')
const userDataPath = app.getPath('userData')
const workerPath = join(__dirname, 'exportWorker.js')
const runWorker = async () => {
return await new Promise<any>((resolve, reject) => {
const worker = new Worker(workerPath, {
workerData: {
sessionIds,
outputDir,
options,
resourcesPath,
userDataPath,
logEnabled
}
})
let settled = false
const finalizeResolve = (value: any) => {
if (settled) return
settled = true
worker.removeAllListeners()
void worker.terminate()
resolve(value)
}
const finalizeReject = (error: Error) => {
if (settled) return
settled = true
worker.removeAllListeners()
void worker.terminate()
reject(error)
}
worker.on('message', (msg: any) => {
if (msg && msg.type === 'export:progress') {
onProgress(msg.data as ExportProgress)
return
}
if (msg && msg.type === 'export:result') {
finalizeResolve(msg.data)
return
}
if (msg && msg.type === 'export:error') {
finalizeReject(new Error(String(msg.error || '导出 Worker 执行失败')))
}
})
worker.on('error', (error) => {
finalizeReject(error instanceof Error ? error : new Error(String(error)))
})
worker.on('exit', (code) => {
if (settled) return
if (code === 0) {
finalizeResolve({ success: false, successCount: 0, failCount: 0, error: '导出 Worker 未返回结果' })
} else {
finalizeReject(new Error(`导出 Worker 异常退出: ${code}`))
}
})
})
}
try {
return await runWorker()
} catch (error) {
return runMainFallback(error instanceof Error ? error.message : String(error))
}
})
ipcMain.handle('export:exportSession', async (_, sessionId: string, outputPath: string, options: ExportOptions) => {

View File

@@ -215,13 +215,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
getMessageDateCounts: (sessionId: string) => ipcRenderer.invoke('chat:getMessageDateCounts', sessionId),
resolveVoiceCache: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:resolveVoiceCache', sessionId, msgId),
getVoiceTranscript: (sessionId: string, msgId: string, createTime?: number) => ipcRenderer.invoke('chat:getVoiceTranscript', sessionId, msgId, createTime),
onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => {
const listener = (_: any, payload: { msgId: string; text: string }) => callback(payload)
onVoiceTranscriptPartial: (callback: (payload: { sessionId?: string; msgId: string; createTime?: number; text: string }) => void) => {
const listener = (_: any, payload: { sessionId?: string; msgId: string; createTime?: number; text: string }) => callback(payload)
ipcRenderer.on('chat:voiceTranscriptPartial', listener)
return () => ipcRenderer.removeListener('chat:voiceTranscriptPartial', listener)
},
execQuery: (kind: string, path: string | null, sql: string) =>
ipcRenderer.invoke('chat:execQuery', kind, path, sql),
getContacts: () => ipcRenderer.invoke('chat:getContacts'),
getMessage: (sessionId: string, localId: number) =>
ipcRenderer.invoke('chat:getMessage', sessionId, localId),
@@ -352,7 +350,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke('export:exportSession', sessionId, outputPath, options),
exportContacts: (outputDir: string, options: any) =>
ipcRenderer.invoke('export:exportContacts', outputDir, options),
onProgress: (callback: (payload: { current: number; total: number; currentSession: string; currentSessionId?: string; phase: string }) => void) => {
onProgress: (callback: (payload: {
current: number
total: number
currentSession: string
currentSessionId?: string
phase: string
phaseProgress?: number
phaseTotal?: number
phaseLabel?: string
collectedMessages?: number
exportedMessages?: number
estimatedTotalMessages?: number
writtenFiles?: number
}) => void) => {
ipcRenderer.on('export:progress', (_, payload) => callback(payload))
return () => ipcRenderer.removeAllListeners('export:progress')
}

View File

@@ -68,29 +68,14 @@ class AnalyticsService {
return new Set(this.getExcludedUsernamesList())
}
private escapeSqlValue(value: string): string {
return value.replace(/'/g, "''")
}
private async getAliasMap(usernames: string[]): Promise<Record<string, string>> {
const map: Record<string, string> = {}
if (usernames.length === 0) return map
// C++ 层不支持参数绑定,直接内联转义后的字符串值
const chunkSize = 200
for (let i = 0; i < usernames.length; i += chunkSize) {
const chunk = usernames.slice(i, i + chunkSize)
const inList = chunk.map((u) => `'${this.escapeSqlValue(u)}'`).join(',')
const sql = `SELECT username, alias FROM contact WHERE username IN (${inList})`
const result = await wcdbService.execQuery('contact', null, sql)
if (!result.success || !result.rows) continue
for (const row of result.rows as Record<string, any>[]) {
const username = row.username || ''
const alias = row.alias || ''
if (username && alias) {
map[username] = alias
}
}
const result = await wcdbService.getContactAliasMap(usernames)
if (!result.success || !result.map) return map
for (const [username, alias] of Object.entries(result.map)) {
if (username && alias) map[username] = alias
}
return map

View File

@@ -278,16 +278,16 @@ class AnnualReportService {
return cached || null
}
const result = await wcdbService.execQuery('message', dbPath, `PRAGMA table_info(${this.quoteSqlIdentifier(tableName)})`)
if (!result.success || !Array.isArray(result.rows) || result.rows.length === 0) {
const result = await wcdbService.getMessageTableColumns(dbPath, tableName)
if (!result.success || !Array.isArray(result.columns) || result.columns.length === 0) {
this.availableYearsColumnCache.set(cacheKey, '')
return null
}
const candidates = ['create_time', 'createtime', 'msg_create_time', 'msg_time', 'msgtime', 'time']
const columns = new Set<string>()
for (const row of result.rows as Record<string, any>[]) {
const name = String(row.name || row.column_name || row.columnName || '').trim().toLowerCase()
for (const columnName of result.columns) {
const name = String(columnName || '').trim().toLowerCase()
if (name) columns.add(name)
}
@@ -309,10 +309,11 @@ class AnnualReportService {
const tried = new Set<string>()
const queryByColumn = async (column: string): Promise<{ first: number; last: number } | null> => {
const sql = `SELECT MIN(${this.quoteSqlIdentifier(column)}) AS first_ts, MAX(${this.quoteSqlIdentifier(column)}) AS last_ts FROM ${this.quoteSqlIdentifier(tableName)}`
const result = await wcdbService.execQuery('message', dbPath, sql)
if (!result.success || !Array.isArray(result.rows) || result.rows.length === 0) return null
const row = result.rows[0] as Record<string, any>
const result = await wcdbService.getMessageTableTimeRange(dbPath, tableName)
if (!result.success || !result.data) return null
const row = result.data as Record<string, any>
const actualColumn = String(row.column || '').trim().toLowerCase()
if (column && actualColumn && column.toLowerCase() !== actualColumn) return null
const first = this.toUnixTimestamp(row.first_ts ?? row.firstTs ?? row.min_ts ?? row.minTs)
const last = this.toUnixTimestamp(row.last_ts ?? row.lastTs ?? row.max_ts ?? row.maxTs)
return { first, last }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -230,10 +230,9 @@ class GroupAnalyticsService {
}
try {
const escapedChatroomId = chatroomId.replace(/'/g, "''")
const roomResult = await wcdbService.execQuery('contact', null, `SELECT * FROM chat_room WHERE username='${escapedChatroomId}' LIMIT 1`)
if (roomResult.success && roomResult.rows && roomResult.rows.length > 0) {
const owner = tryResolve(roomResult.rows[0])
const roomExt = await wcdbService.getChatRoomExtBuffer(chatroomId)
if (roomExt.success && roomExt.extBuffer) {
const owner = tryResolve({ ext_buffer: roomExt.extBuffer })
if (owner) return owner
}
} catch {
@@ -273,13 +272,12 @@ class GroupAnalyticsService {
}
try {
const sql = 'SELECT ext_buffer FROM chat_room WHERE username = ? LIMIT 1'
const result = await wcdbService.execQuery('contact', null, sql, [chatroomId])
if (!result.success || !result.rows || result.rows.length === 0) {
const result = await wcdbService.getChatRoomExtBuffer(chatroomId)
if (!result.success || !result.extBuffer) {
return nicknameMap
}
const extBuffer = this.decodeExtBuffer((result.rows[0] as any).ext_buffer)
const extBuffer = this.decodeExtBuffer(result.extBuffer)
if (!extBuffer) return nicknameMap
this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries())
return nicknameMap
@@ -583,19 +581,9 @@ class GroupAnalyticsService {
const batch = candidates.slice(i, i + batchSize)
if (batch.length === 0) continue
const inList = batch.map((username) => `'${username.replace(/'/g, "''")}'`).join(',')
const lightweightSql = `
SELECT username, user_name, encrypt_username, encrypt_user_name, remark, nick_name, alias, local_type
FROM contact
WHERE username IN (${inList})
`
let result = await wcdbService.execQuery('contact', null, lightweightSql)
if (!result.success || !result.rows) {
// 兼容历史/变体列名,轻查询失败时回退全字段查询,避免好友标识丢失
result = await wcdbService.execQuery('contact', null, `SELECT * FROM contact WHERE username IN (${inList})`)
}
if (!result.success || !result.rows) continue
appendContactsToLookup(result.rows as Record<string, unknown>[])
const result = await wcdbService.getContactsCompact(batch)
if (!result.success || !result.contacts) continue
appendContactsToLookup(result.contacts as Record<string, unknown>[])
}
return lookup
}
@@ -774,31 +762,111 @@ class GroupAnalyticsService {
return ''
}
private normalizeCursorTimestamp(value: number): number {
if (!Number.isFinite(value) || value <= 0) return 0
const normalized = Math.floor(value)
return normalized > 10000000000 ? Math.floor(normalized / 1000) : normalized
}
private extractRowSenderUsername(row: Record<string, any>): string {
const candidates = [
row.sender_username,
row.senderUsername,
row.sender,
row.WCDB_CT_sender_username
]
for (const candidate of candidates) {
const value = String(candidate || '').trim()
if (value) return value
}
for (const [key, value] of Object.entries(row)) {
const normalizedKey = key.toLowerCase()
if (
normalizedKey === 'sender_username' ||
normalizedKey === 'senderusername' ||
normalizedKey === 'sender' ||
normalizedKey === 'wcdb_ct_sender_username'
) {
const normalizedValue = String(value || '').trim()
if (normalizedValue) return normalizedValue
}
}
return ''
}
private parseSingleMessageRow(row: Record<string, any>): Message | null {
try {
const mapped = chatService.mapRowsToMessagesForApi([row])
return Array.isArray(mapped) && mapped.length > 0 ? mapped[0] : null
} catch {
return null
}
}
private async openMemberMessageCursor(
chatroomId: string,
batchSize: number,
ascending: boolean,
startTime: number,
endTime: number
): Promise<{ success: boolean; cursor?: number; error?: string }> {
const beginTimestamp = this.normalizeCursorTimestamp(startTime)
const endTimestamp = this.normalizeCursorTimestamp(endTime)
const liteResult = await wcdbService.openMessageCursorLite(chatroomId, batchSize, ascending, beginTimestamp, endTimestamp)
if (liteResult.success && liteResult.cursor) return liteResult
return wcdbService.openMessageCursor(chatroomId, batchSize, ascending, beginTimestamp, endTimestamp)
}
private async collectMessagesByMember(
chatroomId: string,
memberUsername: string,
startTime: number,
endTime: number
): Promise<{ success: boolean; data?: Message[]; error?: string }> {
const batchSize = 500
const batchSize = 800
const matchedMessages: Message[] = []
let offset = 0
const senderMatchCache = new Map<string, boolean>()
const matchesTargetSender = (sender: string | null | undefined): boolean => {
const key = String(sender || '').trim().toLowerCase()
if (!key) return false
const cached = senderMatchCache.get(key)
if (typeof cached === 'boolean') return cached
const matched = this.isSameAccountIdentity(memberUsername, sender)
senderMatchCache.set(key, matched)
return matched
}
while (true) {
const batch = await chatService.getMessages(chatroomId, offset, batchSize, startTime, endTime, true)
if (!batch.success || !batch.messages) {
return { success: false, error: batch.error || '获取群消息失败' }
}
const cursorResult = await this.openMemberMessageCursor(chatroomId, batchSize, true, startTime, endTime)
if (!cursorResult.success || !cursorResult.cursor) {
return { success: false, error: cursorResult.error || '创建群消息游标失败' }
}
for (const message of batch.messages) {
if (this.isSameAccountIdentity(memberUsername, message.senderUsername)) {
matchedMessages.push(message)
const cursor = cursorResult.cursor
try {
while (true) {
const batch = await wcdbService.fetchMessageBatch(cursor)
if (!batch.success) {
return { success: false, error: batch.error || '获取群消息失败' }
}
}
const rows = Array.isArray(batch.rows) ? batch.rows as Record<string, any>[] : []
if (rows.length === 0) break
const fetchedCount = batch.messages.length
if (fetchedCount <= 0 || !batch.hasMore) break
offset += fetchedCount
for (const row of rows) {
const senderFromRow = this.extractRowSenderUsername(row)
if (senderFromRow && !matchesTargetSender(senderFromRow)) {
continue
}
const message = this.parseSingleMessageRow(row)
if (!message) continue
if (matchesTargetSender(message.senderUsername)) {
matchedMessages.push(message)
}
}
if (!batch.hasMore) break
}
} finally {
await wcdbService.closeMessageCursor(cursor)
}
return { success: true, data: matchedMessages }
@@ -832,57 +900,93 @@ class GroupAnalyticsService {
: 0
const matchedMessages: Message[] = []
const batchSize = Math.max(limit * 2, 100)
const senderMatchCache = new Map<string, boolean>()
const matchesTargetSender = (sender: string | null | undefined): boolean => {
const key = String(sender || '').trim().toLowerCase()
if (!key) return false
const cached = senderMatchCache.get(key)
if (typeof cached === 'boolean') return cached
const matched = this.isSameAccountIdentity(normalizedMemberUsername, sender)
senderMatchCache.set(key, matched)
return matched
}
const batchSize = Math.max(limit * 4, 240)
let hasMore = false
while (matchedMessages.length < limit) {
const batch = await chatService.getMessages(
normalizedChatroomId,
cursor,
batchSize,
startTimeValue,
endTimeValue,
false
)
if (!batch.success || !batch.messages) {
return { success: false, error: batch.error || '获取群成员消息失败' }
}
const cursorResult = await this.openMemberMessageCursor(
normalizedChatroomId,
batchSize,
false,
startTimeValue,
endTimeValue
)
if (!cursorResult.success || !cursorResult.cursor) {
return { success: false, error: cursorResult.error || '创建群成员消息游标失败' }
}
const currentMessages = batch.messages
const nextCursor = typeof batch.nextOffset === 'number'
? Math.max(cursor, Math.floor(batch.nextOffset))
: cursor + currentMessages.length
let consumedRows = 0
const dbCursor = cursorResult.cursor
let overflowMatchFound = false
for (const message of currentMessages) {
if (!this.isSameAccountIdentity(normalizedMemberUsername, message.senderUsername)) {
continue
try {
while (matchedMessages.length < limit) {
const batch = await wcdbService.fetchMessageBatch(dbCursor)
if (!batch.success) {
return { success: false, error: batch.error || '获取群成员消息失败' }
}
if (matchedMessages.length < limit) {
const rows = Array.isArray(batch.rows) ? batch.rows as Record<string, any>[] : []
if (rows.length === 0) {
hasMore = false
break
}
let startIndex = 0
if (cursor > consumedRows) {
const skipCount = Math.min(cursor - consumedRows, rows.length)
consumedRows += skipCount
startIndex = skipCount
if (startIndex >= rows.length) {
if (!batch.hasMore) {
hasMore = false
break
}
continue
}
}
for (let index = startIndex; index < rows.length; index += 1) {
const row = rows[index]
consumedRows += 1
const senderFromRow = this.extractRowSenderUsername(row)
if (senderFromRow && !matchesTargetSender(senderFromRow)) {
continue
}
const message = this.parseSingleMessageRow(row)
if (!message) continue
if (!matchesTargetSender(message.senderUsername)) {
continue
}
matchedMessages.push(message)
} else {
overflowMatchFound = true
if (matchedMessages.length >= limit) {
cursor = consumedRows
hasMore = index < rows.length - 1 || batch.hasMore === true
break
}
}
if (matchedMessages.length >= limit) break
cursor = consumedRows
if (!batch.hasMore) {
hasMore = false
break
}
}
cursor = nextCursor
if (overflowMatchFound) {
hasMore = true
break
}
if (currentMessages.length === 0 || !batch.hasMore) {
hasMore = false
break
}
if (matchedMessages.length >= limit) {
hasMore = true
break
}
} finally {
await wcdbService.closeMessageCursor(dbCursor)
}
return {

View File

@@ -55,14 +55,8 @@ type DecryptResult = {
isThumb?: boolean // 是否是缩略图(没有高清图时返回缩略图)
}
type HardlinkState = {
imageTable?: string
dirTable?: string
}
export class ImageDecryptService {
private configService = new ConfigService()
private hardlinkCache = new Map<string, HardlinkState>()
private resolvedCache = new Map<string, string>()
private pending = new Map<string, Promise<DecryptResult>>()
private readonly defaultV1AesKey = 'cfcd208495d565ef'
@@ -683,45 +677,19 @@ export class ImageDecryptService {
private async resolveHardlinkPath(accountDir: string, md5: string, _sessionId?: string): Promise<string | null> {
try {
const hardlinkPath = this.resolveHardlinkDbPath(accountDir)
if (!hardlinkPath) {
return null
}
const ready = await this.ensureWcdbReady()
if (!ready) {
this.logInfo('[ImageDecrypt] hardlink db not ready')
return null
}
const state = await this.getHardlinkState(accountDir, hardlinkPath)
if (!state.imageTable) {
this.logInfo('[ImageDecrypt] hardlink table missing', { hardlinkPath })
return null
}
const resolveResult = await wcdbService.resolveImageHardlink(md5, accountDir)
if (!resolveResult.success || !resolveResult.data) return null
const fileName = String(resolveResult.data.file_name || '').trim()
const fullPath = String(resolveResult.data.full_path || '').trim()
if (!fileName) return null
const escapedMd5 = this.escapeSqlString(md5)
const rowResult = await wcdbService.execQuery(
'media',
hardlinkPath,
`SELECT dir1, dir2, file_name FROM ${state.imageTable} WHERE lower(md5) = lower('${escapedMd5}') LIMIT 1`
)
const row = rowResult.success && rowResult.rows ? rowResult.rows[0] : null
if (!row) {
this.logInfo('[ImageDecrypt] hardlink row miss', { md5, table: state.imageTable })
return null
}
const dir1 = this.getRowValue(row, 'dir1')
const dir2 = this.getRowValue(row, 'dir2')
const fileName = this.getRowValue(row, 'file_name') ?? this.getRowValue(row, 'fileName')
if (dir1 === undefined || dir2 === undefined || !fileName) {
this.logInfo('[ImageDecrypt] hardlink row incomplete', { row })
return null
}
const lowerFileName = fileName.toLowerCase()
const lowerFileName = String(fileName).toLowerCase()
if (lowerFileName.endsWith('.dat')) {
const baseLower = lowerFileName.slice(0, -4)
if (!this.isLikelyImageDatBase(baseLower) && !this.looksLikeMd5(baseLower)) {
@@ -730,57 +698,11 @@ export class ImageDecryptService {
}
}
// dir1 和 dir2 是 rowid需要从 dir2id 表查询对应的目录名
let dir1Name: string | null = null
let dir2Name: string | null = null
if (state.dirTable) {
try {
// 通过 rowid 查询目录名
const dir1Result = await wcdbService.execQuery(
'media',
hardlinkPath,
`SELECT username FROM ${state.dirTable} WHERE rowid = ${Number(dir1)} LIMIT 1`
)
if (dir1Result.success && dir1Result.rows && dir1Result.rows.length > 0) {
const value = this.getRowValue(dir1Result.rows[0], 'username')
if (value) dir1Name = String(value)
}
const dir2Result = await wcdbService.execQuery(
'media',
hardlinkPath,
`SELECT username FROM ${state.dirTable} WHERE rowid = ${Number(dir2)} LIMIT 1`
)
if (dir2Result.success && dir2Result.rows && dir2Result.rows.length > 0) {
const value = this.getRowValue(dir2Result.rows[0], 'username')
if (value) dir2Name = String(value)
}
} catch {
// ignore
}
if (fullPath && existsSync(fullPath)) {
this.logInfo('[ImageDecrypt] hardlink path hit', { fullPath })
return fullPath
}
if (!dir1Name || !dir2Name) {
this.logInfo('[ImageDecrypt] hardlink dir resolve miss', { dir1, dir2, dir1Name, dir2Name })
return null
}
// 构建路径: msg/attach/{dir1Name}/{dir2Name}/Img/{fileName}
const possiblePaths = [
join(accountDir, 'msg', 'attach', dir1Name, dir2Name, 'Img', fileName),
join(accountDir, 'msg', 'attach', dir1Name, dir2Name, 'mg', fileName),
join(accountDir, 'msg', 'attach', dir1Name, dir2Name, fileName),
]
for (const fullPath of possiblePaths) {
if (existsSync(fullPath)) {
this.logInfo('[ImageDecrypt] hardlink path hit', { fullPath })
return fullPath
}
}
this.logInfo('[ImageDecrypt] hardlink path miss', { possiblePaths })
this.logInfo('[ImageDecrypt] hardlink path miss', { fullPath, md5 })
return null
} catch {
// ignore
@@ -788,35 +710,6 @@ export class ImageDecryptService {
return null
}
private async getHardlinkState(accountDir: string, hardlinkPath: string): Promise<HardlinkState> {
const cached = this.hardlinkCache.get(hardlinkPath)
if (cached) return cached
const imageResult = await wcdbService.execQuery(
'media',
hardlinkPath,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'image_hardlink_info%' ORDER BY name DESC LIMIT 1"
)
const dirResult = await wcdbService.execQuery(
'media',
hardlinkPath,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'dir2id%' LIMIT 1"
)
const imageTable = imageResult.success && imageResult.rows && imageResult.rows.length > 0
? this.getRowValue(imageResult.rows[0], 'name')
: undefined
const dirTable = dirResult.success && dirResult.rows && dirResult.rows.length > 0
? this.getRowValue(dirResult.rows[0], 'name')
: undefined
const state: HardlinkState = {
imageTable: imageTable ? String(imageTable) : undefined,
dirTable: dirTable ? String(dirTable) : undefined
}
this.logInfo('[ImageDecrypt] hardlink state', { hardlinkPath, imageTable: state.imageTable, dirTable: state.dirTable })
this.hardlinkCache.set(hardlinkPath, state)
return state
}
private async ensureWcdbReady(): Promise<boolean> {
if (wcdbService.isReady()) return true
const dbPath = this.configService.get('dbPath')
@@ -1992,7 +1885,6 @@ export class ImageDecryptService {
async clearCache(): Promise<{ success: boolean; error?: string }> {
this.resolvedCache.clear()
this.hardlinkCache.clear()
this.pending.clear()
this.updateFlags.clear()
this.cacheIndexed = false

View File

@@ -136,7 +136,7 @@ export class KeyServiceMac {
if (sipStatus.enabled) {
return {
success: false,
error: 'SIP (系统完整性保护) 已开启,无法获取密钥。请关闭 SIP 后重试。\n\n关闭方法\n1. 重启 Mac 并按住 Command + R 进入恢复模式\n2. 打开终端,输入: csrutil disable\n3. 重启电脑'
error: 'SIP (系统完整性保护) 已开启,无法获取密钥。请关闭 SIP 后重试。\n\n关闭方法\n1. Intel 芯片:重启 Mac 并按住 Command + R 进入恢复模式\n2. Apple 芯片M 系列):关机后长按开机(指纹)键,选择“设置(选项)”进入恢复模式\n3. 打开终端,输入: csrutil disable\n4. 重启电脑'
}
}

View File

@@ -663,100 +663,24 @@ class SnsService {
}
async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
const collect = (rows?: any[]): string[] => {
if (!Array.isArray(rows)) return []
const usernames: string[] = []
for (const row of rows) {
const raw = row?.user_name ?? row?.userName ?? row?.username ?? Object.values(row || {})[0]
const username = typeof raw === 'string' ? raw.trim() : String(raw || '').trim()
if (username) usernames.push(username)
}
return usernames
const result = await wcdbService.getSnsUsernames()
if (!result.success) {
return { success: false, error: result.error || '获取朋友圈联系人失败' }
}
const primary = await wcdbService.execQuery(
'sns',
null,
"SELECT DISTINCT user_name FROM SnsTimeLine WHERE user_name IS NOT NULL AND user_name <> ''"
)
const fallback = await wcdbService.execQuery(
'sns',
null,
"SELECT DISTINCT userName FROM SnsTimeLine WHERE userName IS NOT NULL AND userName <> ''"
)
const merged = Array.from(new Set([
...collect(primary.rows),
...collect(fallback.rows)
]))
// 任一查询成功且拿到用户名即视为成功,避免因为列名差异导致误判为空。
if (merged.length > 0) {
return { success: true, usernames: merged }
}
// 两条查询都成功但无数据,说明确实没有朋友圈发布者。
if (primary.success || fallback.success) {
return { success: true, usernames: [] }
}
return { success: false, error: primary.error || fallback.error || '获取朋友圈联系人失败' }
return { success: true, usernames: result.usernames || [] }
}
private async getExportStatsFromTableCount(myWxid?: string): Promise<{ totalPosts: number; totalFriends: number; myPosts: number | null }> {
let totalPosts = 0
let totalFriends = 0
let myPosts: number | null = null
const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine')
if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) {
totalPosts = this.parseCountValue(postCountResult.rows[0])
}
if (totalPosts > 0) {
const friendCountPrimary = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(DISTINCT user_name) AS total FROM SnsTimeLine WHERE user_name IS NOT NULL AND user_name <> ''"
)
if (friendCountPrimary.success && friendCountPrimary.rows && friendCountPrimary.rows.length > 0) {
totalFriends = this.parseCountValue(friendCountPrimary.rows[0])
} else {
const friendCountFallback = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(DISTINCT userName) AS total FROM SnsTimeLine WHERE userName IS NOT NULL AND userName <> ''"
)
if (friendCountFallback.success && friendCountFallback.rows && friendCountFallback.rows.length > 0) {
totalFriends = this.parseCountValue(friendCountFallback.rows[0])
}
}
}
const normalizedMyWxid = this.toOptionalString(myWxid)
if (normalizedMyWxid) {
const myPostPrimary = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(1) AS total FROM SnsTimeLine WHERE user_name = ?",
[normalizedMyWxid]
)
if (myPostPrimary.success && myPostPrimary.rows && myPostPrimary.rows.length > 0) {
myPosts = this.parseCountValue(myPostPrimary.rows[0])
} else {
const myPostFallback = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(1) AS total FROM SnsTimeLine WHERE userName = ?",
[normalizedMyWxid]
)
if (myPostFallback.success && myPostFallback.rows && myPostFallback.rows.length > 0) {
myPosts = this.parseCountValue(myPostFallback.rows[0])
}
}
const result = await wcdbService.getSnsExportStats(normalizedMyWxid || undefined)
if (!result.success || !result.data) {
return { totalPosts: 0, totalFriends: 0, myPosts: normalizedMyWxid ? 0 : null }
}
return {
totalPosts: Number(result.data.totalPosts || 0),
totalFriends: Number(result.data.totalFriends || 0),
myPosts: result.data.myPosts === null || result.data.myPosts === undefined ? null : Number(result.data.myPosts || 0)
}
return { totalPosts, totalFriends, myPosts }
}
async getExportStats(options?: {

View File

@@ -70,7 +70,7 @@ class VideoService {
/**
* 从 video_hardlink_info_v4 表查询视频文件名
* 使用 wcdbService.execQuery 查询加密的 hardlink.db
* 使用 wcdb 专属接口查询加密的 hardlink.db
*/
private async queryVideoFileName(md5: string): Promise<string | undefined> {
const dbPath = this.getDbPath()
@@ -103,17 +103,11 @@ class VideoService {
if (existsSync(p)) {
try {
this.log('尝试加密 hardlink.db', { path: p })
const escapedMd5 = md5.replace(/'/g, "''")
const sql = `SELECT file_name FROM video_hardlink_info_v4 WHERE md5 = '${escapedMd5}' LIMIT 1`
const result = await wcdbService.execQuery('media', p, sql)
if (result.success && result.rows && result.rows.length > 0) {
const row = result.rows[0]
if (row?.file_name) {
const realMd5 = String(row.file_name).replace(/\.[^.]+$/, '')
this.log('加密 hardlink.db 命中', { file_name: row.file_name, realMd5 })
return realMd5
}
const result = await wcdbService.resolveVideoHardlinkMd5(md5, p)
if (result.success && result.data?.resolved_md5) {
const realMd5 = String(result.data.resolved_md5)
this.log('加密 hardlink.db 命中', { file_name: result.data.file_name, realMd5 })
return realMd5
}
this.log('加密 hardlink.db 未命中', { path: p, result: JSON.stringify(result).slice(0, 200) })
} catch (e) {

View File

@@ -5,47 +5,6 @@ import { tmpdir } from 'os'
// DLL 初始化错误信息,用于帮助用户诊断问题
let lastDllInitError: string | null = null
/**
* 解析 extra_bufferprotobuf中的免打扰状态
* - field 12 (tag 0x60): 值非0 = 免打扰
* 折叠状态通过 contact.flag & 0x10000000 判断
*/
function parseExtraBuffer(raw: Buffer | string | null | undefined): { isMuted: boolean } {
if (!raw) return { isMuted: false }
// execQuery 返回的 BLOB 列是十六进制字符串,需要先解码
const buf: Buffer = typeof raw === 'string' ? Buffer.from(raw, 'hex') : raw
if (buf.length === 0) return { isMuted: false }
let isMuted = false
let i = 0
const len = buf.length
const readVarint = (): number => {
let result = 0, shift = 0
while (i < len) {
const b = buf[i++]
result |= (b & 0x7f) << shift
shift += 7
if (!(b & 0x80)) break
}
return result
}
while (i < len) {
const tag = readVarint()
const fieldNum = tag >>> 3
const wireType = tag & 0x07
if (wireType === 0) {
const val = readVarint()
if (fieldNum === 12 && val !== 0) isMuted = true
} else if (wireType === 2) {
const sz = readVarint()
i += sz
} else if (wireType === 5) { i += 4
} else if (wireType === 1) { i += 8
} else { break }
}
return { isMuted }
}
export function getLastDllInitError(): string | null {
return lastDllInitError
}
@@ -86,6 +45,11 @@ export class WcdbCore {
private wcdbGetMessageMeta: any = null
private wcdbGetContact: any = null
private wcdbGetContactStatus: any = null
private wcdbGetContactTypeCounts: any = null
private wcdbGetContactsCompact: any = null
private wcdbGetContactAliasMap: any = null
private wcdbGetContactFriendFlags: any = null
private wcdbGetChatRoomExtBuffer: any = null
private wcdbGetMessageTableStats: any = null
private wcdbGetAggregateStats: any = null
private wcdbGetAvailableYears: any = null
@@ -106,9 +70,24 @@ export class WcdbCore {
private wcdbGetEmoticonCdnUrl: any = null
private wcdbGetDbStatus: any = null
private wcdbGetVoiceData: any = null
private wcdbGetVoiceDataBatch: any = null
private wcdbGetMediaSchemaSummary: any = null
private wcdbGetSessionMessageCounts: any = null
private wcdbGetSessionMessageTypeStats: any = null
private wcdbGetSessionMessageTypeStatsBatch: any = null
private wcdbGetSessionMessageDateCounts: any = null
private wcdbGetSessionMessageDateCountsBatch: any = null
private wcdbGetMessagesByType: any = null
private wcdbGetHeadImageBuffers: any = null
private wcdbSearchMessages: any = null
private wcdbGetSnsTimeline: any = null
private wcdbGetSnsAnnualStats: any = null
private wcdbGetSnsUsernames: any = null
private wcdbGetSnsExportStats: any = null
private wcdbGetMessageTableColumns: any = null
private wcdbGetMessageTableTimeRange: any = null
private wcdbResolveImageHardlink: any = null
private wcdbResolveVideoHardlinkMd5: any = null
private wcdbInstallSnsBlockDeleteTrigger: any = null
private wcdbUninstallSnsBlockDeleteTrigger: any = null
private wcdbCheckSnsBlockDeleteTrigger: any = null
@@ -719,6 +698,32 @@ export class WcdbCore {
this.wcdbGetContactStatus = null
}
try {
this.wcdbGetContactTypeCounts = this.lib.func('int32 wcdb_get_contact_type_counts(int64 handle, _Out_ void** outJson)')
} catch {
this.wcdbGetContactTypeCounts = null
}
try {
this.wcdbGetContactsCompact = this.lib.func('int32 wcdb_get_contacts_compact(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
} catch {
this.wcdbGetContactsCompact = null
}
try {
this.wcdbGetContactAliasMap = this.lib.func('int32 wcdb_get_contact_alias_map(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
} catch {
this.wcdbGetContactAliasMap = null
}
try {
this.wcdbGetContactFriendFlags = this.lib.func('int32 wcdb_get_contact_friend_flags(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
} catch {
this.wcdbGetContactFriendFlags = null
}
try {
this.wcdbGetChatRoomExtBuffer = this.lib.func('int32 wcdb_get_chat_room_ext_buffer(int64 handle, const char* chatroomId, _Out_ void** outJson)')
} catch {
this.wcdbGetChatRoomExtBuffer = null
}
// wcdb_status wcdb_get_message_table_stats(wcdb_handle handle, const char* session_id, char** out_json)
this.wcdbGetMessageTableStats = this.lib.func('int32 wcdb_get_message_table_stats(int64 handle, const char* sessionId, _Out_ void** outJson)')
@@ -821,6 +826,51 @@ export class WcdbCore {
} catch {
this.wcdbGetVoiceData = null
}
try {
this.wcdbGetVoiceDataBatch = this.lib.func('int32 wcdb_get_voice_data_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
} catch {
this.wcdbGetVoiceDataBatch = null
}
try {
this.wcdbGetMediaSchemaSummary = this.lib.func('int32 wcdb_get_media_schema_summary(int64 handle, const char* dbPath, _Out_ void** outJson)')
} catch {
this.wcdbGetMediaSchemaSummary = null
}
try {
this.wcdbGetSessionMessageCounts = this.lib.func('int32 wcdb_get_session_message_counts(int64 handle, const char* sessionIdsJson, _Out_ void** outJson)')
} catch {
this.wcdbGetSessionMessageCounts = null
}
try {
this.wcdbGetSessionMessageTypeStats = this.lib.func('int32 wcdb_get_session_message_type_stats(int64 handle, const char* sessionId, int32 beginTimestamp, int32 endTimestamp, _Out_ void** outJson)')
} catch {
this.wcdbGetSessionMessageTypeStats = null
}
try {
this.wcdbGetSessionMessageTypeStatsBatch = this.lib.func('int32 wcdb_get_session_message_type_stats_batch(int64 handle, const char* sessionIdsJson, const char* optionsJson, _Out_ void** outJson)')
} catch {
this.wcdbGetSessionMessageTypeStatsBatch = null
}
try {
this.wcdbGetSessionMessageDateCounts = this.lib.func('int32 wcdb_get_session_message_date_counts(int64 handle, const char* sessionId, _Out_ void** outJson)')
} catch {
this.wcdbGetSessionMessageDateCounts = null
}
try {
this.wcdbGetSessionMessageDateCountsBatch = this.lib.func('int32 wcdb_get_session_message_date_counts_batch(int64 handle, const char* sessionIdsJson, _Out_ void** outJson)')
} catch {
this.wcdbGetSessionMessageDateCountsBatch = null
}
try {
this.wcdbGetMessagesByType = this.lib.func('int32 wcdb_get_messages_by_type(int64 handle, const char* sessionId, int64 localType, int32 ascending, int32 limit, int32 offset, _Out_ void** outJson)')
} catch {
this.wcdbGetMessagesByType = null
}
try {
this.wcdbGetHeadImageBuffers = this.lib.func('int32 wcdb_get_head_image_buffers(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
} catch {
this.wcdbGetHeadImageBuffers = null
}
// wcdb_status wcdb_search_messages(wcdb_handle handle, const char* session_id, const char* keyword, int32_t limit, int32_t offset, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
try {
@@ -842,6 +892,36 @@ export class WcdbCore {
} catch {
this.wcdbGetSnsAnnualStats = null
}
try {
this.wcdbGetSnsUsernames = this.lib.func('int32 wcdb_get_sns_usernames(int64 handle, _Out_ void** outJson)')
} catch {
this.wcdbGetSnsUsernames = null
}
try {
this.wcdbGetSnsExportStats = this.lib.func('int32 wcdb_get_sns_export_stats(int64 handle, const char* myWxid, _Out_ void** outJson)')
} catch {
this.wcdbGetSnsExportStats = null
}
try {
this.wcdbGetMessageTableColumns = this.lib.func('int32 wcdb_get_message_table_columns(int64 handle, const char* dbPath, const char* tableName, _Out_ void** outJson)')
} catch {
this.wcdbGetMessageTableColumns = null
}
try {
this.wcdbGetMessageTableTimeRange = this.lib.func('int32 wcdb_get_message_table_time_range(int64 handle, const char* dbPath, const char* tableName, _Out_ void** outJson)')
} catch {
this.wcdbGetMessageTableTimeRange = null
}
try {
this.wcdbResolveImageHardlink = this.lib.func('int32 wcdb_resolve_image_hardlink(int64 handle, const char* md5, const char* accountDir, _Out_ void** outJson)')
} catch {
this.wcdbResolveImageHardlink = null
}
try {
this.wcdbResolveVideoHardlinkMd5 = this.lib.func('int32 wcdb_resolve_video_hardlink_md5(int64 handle, const char* md5, const char* dbPath, _Out_ void** outJson)')
} catch {
this.wcdbResolveVideoHardlinkMd5 = null
}
// wcdb_status wcdb_install_sns_block_delete_trigger(wcdb_handle handle, char** out_error)
try {
@@ -1392,6 +1472,197 @@ export class WcdbCore {
}
}
async getSessionMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetSessionMessageCounts) return this.getMessageCounts(sessionIds)
try {
const outPtr = [null as any]
const result = this.wcdbGetSessionMessageCounts(this.handle, JSON.stringify(sessionIds || []), outPtr)
if (result !== 0 || !outPtr[0]) {
return { success: false, error: `获取会话消息总数失败: ${result}` }
}
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析会话消息总数失败' }
const raw = JSON.parse(jsonStr) || {}
const counts: Record<string, number> = {}
for (const sid of sessionIds || []) {
const value = Number(raw?.[sid] ?? 0)
counts[sid] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
}
return { success: true, counts }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getSessionMessageTypeStats(
sessionId: string,
beginTimestamp: number = 0,
endTimestamp: number = 0
): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetSessionMessageTypeStats) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetSessionMessageTypeStats(
this.handle,
sessionId,
this.normalizeTimestamp(beginTimestamp),
this.normalizeTimestamp(endTimestamp),
outPtr
)
if (result !== 0 || !outPtr[0]) {
return { success: false, error: `获取会话类型统计失败: ${result}` }
}
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析会话类型统计失败' }
return { success: true, data: JSON.parse(jsonStr) || {} }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getSessionMessageTypeStatsBatch(
sessionIds: string[],
options?: {
beginTimestamp?: number
endTimestamp?: number
quickMode?: boolean
includeGroupSenderCount?: boolean
}
): Promise<{ success: boolean; data?: Record<string, any>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
const normalizedSessionIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
if (normalizedSessionIds.length === 0) return { success: true, data: {} }
if (!this.wcdbGetSessionMessageTypeStatsBatch) {
const data: Record<string, any> = {}
for (const sessionId of normalizedSessionIds) {
const single = await this.getSessionMessageTypeStats(
sessionId,
options?.beginTimestamp || 0,
options?.endTimestamp || 0
)
if (single.success) {
data[sessionId] = single.data || {}
}
}
return { success: true, data }
}
try {
const outPtr = [null as any]
const optionsJson = JSON.stringify({
begin: this.normalizeTimestamp(options?.beginTimestamp || 0),
end: this.normalizeTimestamp(options?.endTimestamp || 0),
quick_mode: options?.quickMode === true,
include_group_sender_count: options?.includeGroupSenderCount !== false
})
const result = this.wcdbGetSessionMessageTypeStatsBatch(
this.handle,
JSON.stringify(normalizedSessionIds),
optionsJson,
outPtr
)
if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取会话类型统计失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析批量会话类型统计失败' }
return { success: true, data: JSON.parse(jsonStr) || {} }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getSessionMessageDateCounts(sessionId: string): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetSessionMessageDateCounts) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetSessionMessageDateCounts(this.handle, sessionId, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取会话日消息统计失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析会话日消息统计失败' }
const raw = JSON.parse(jsonStr) || {}
const counts: Record<string, number> = {}
for (const [dateKey, value] of Object.entries(raw)) {
const count = Number(value)
if (!dateKey || !Number.isFinite(count) || count <= 0) continue
counts[String(dateKey)] = Math.floor(count)
}
return { success: true, counts }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getSessionMessageDateCountsBatch(sessionIds: string[]): Promise<{ success: boolean; data?: Record<string, Record<string, number>>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
const normalizedSessionIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
if (normalizedSessionIds.length === 0) return { success: true, data: {} }
if (!this.wcdbGetSessionMessageDateCountsBatch) {
const data: Record<string, Record<string, number>> = {}
for (const sessionId of normalizedSessionIds) {
const single = await this.getSessionMessageDateCounts(sessionId)
data[sessionId] = single.success && single.counts ? single.counts : {}
}
return { success: true, data }
}
try {
const outPtr = [null as any]
const result = this.wcdbGetSessionMessageDateCountsBatch(this.handle, JSON.stringify(normalizedSessionIds), outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取会话日消息统计失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析批量会话日消息统计失败' }
const raw = JSON.parse(jsonStr) || {}
const data: Record<string, Record<string, number>> = {}
for (const sessionId of normalizedSessionIds) {
const source = raw?.[sessionId] || {}
const next: Record<string, number> = {}
for (const [dateKey, value] of Object.entries(source)) {
const count = Number(value)
if (!dateKey || !Number.isFinite(count) || count <= 0) continue
next[String(dateKey)] = Math.floor(count)
}
data[sessionId] = next
}
return { success: true, data }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getMessagesByType(
sessionId: string,
localType: number,
ascending = false,
limit = 0,
offset = 0
): Promise<{ success: boolean; rows?: any[]; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetMessagesByType) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetMessagesByType(
this.handle,
sessionId,
BigInt(localType),
ascending ? 1 : 0,
Math.max(0, Math.floor(limit || 0)),
Math.max(0, Math.floor(offset || 0)),
outPtr
)
if (result !== 0 || !outPtr[0]) return { success: false, error: `按类型读取消息失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析按类型消息失败' }
const rows = JSON.parse(jsonStr)
return { success: true, rows: Array.isArray(rows) ? rows : [] }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getDisplayNames(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
@@ -1766,24 +2037,25 @@ export class WcdbCore {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
}
if (!this.wcdbGetContactStatus) {
return { success: false, error: '接口未就绪' }
}
try {
// 分批查询,避免 SQL 过长execQuery 不支持参数绑定,直接拼 SQL
const BATCH = 200
const outPtr = [null as any]
const code = this.wcdbGetContactStatus(this.handle, JSON.stringify(usernames || []), outPtr)
if (code !== 0 || !outPtr[0]) {
return { success: false, error: `获取会话状态失败: ${code}` }
}
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析会话状态失败' }
const rawMap = JSON.parse(jsonStr) || {}
const map: Record<string, { isFolded: boolean; isMuted: boolean }> = {}
for (let i = 0; i < usernames.length; i += BATCH) {
const batch = usernames.slice(i, i + BATCH)
const inList = batch.map(u => `'${u.replace(/'/g, "''")}'`).join(',')
const sql = `SELECT username, flag, extra_buffer FROM contact WHERE username IN (${inList})`
const result = await this.execQuery('contact', null, sql)
if (!result.success || !result.rows) continue
for (const row of result.rows) {
const uname: string = row.username
// 折叠flag bit 28 (0x10000000)
const flag = parseInt(row.flag ?? '0', 10)
const isFolded = (flag & 0x10000000) !== 0
// 免打扰extra_buffer field 12 非0
const { isMuted } = parseExtraBuffer(row.extra_buffer)
map[uname] = { isFolded, isMuted }
for (const username of usernames || []) {
const state = rawMap[username] || {}
map[username] = {
isFolded: Boolean(state.isFolded),
isMuted: Boolean(state.isMuted)
}
}
return { success: true, map }
@@ -1792,6 +2064,128 @@ export class WcdbCore {
}
}
async getMessageTableColumns(dbPath: string, tableName: string): Promise<{ success: boolean; columns?: string[]; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetMessageTableColumns) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetMessageTableColumns(this.handle, dbPath, tableName, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取消息表列失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析消息表列失败' }
const columns = JSON.parse(jsonStr)
return { success: true, columns: Array.isArray(columns) ? columns.map((c: any) => String(c || '')) : [] }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getMessageTableTimeRange(dbPath: string, tableName: string): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetMessageTableTimeRange) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetMessageTableTimeRange(this.handle, dbPath, tableName, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取消息表时间范围失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析消息表时间范围失败' }
const data = JSON.parse(jsonStr) || {}
return { success: true, data }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getContactTypeCounts(): Promise<{ success: boolean; counts?: { private: number; group: number; official: number; former_friend: number }; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetContactTypeCounts) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const code = this.wcdbGetContactTypeCounts(this.handle, outPtr)
if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人分类统计失败: ${code}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析联系人分类统计失败' }
const raw = JSON.parse(jsonStr) || {}
return {
success: true,
counts: {
private: Number(raw.private || 0),
group: Number(raw.group || 0),
official: Number(raw.official || 0),
former_friend: Number(raw.former_friend || 0)
}
}
} catch (e) {
return { success: false, error: String(e) }
}
}
async getContactsCompact(usernames: string[] = []): Promise<{ success: boolean; contacts?: any[]; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetContactsCompact) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const payload = Array.isArray(usernames) && usernames.length > 0 ? JSON.stringify(usernames) : null
const code = this.wcdbGetContactsCompact(this.handle, payload, outPtr)
if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人列表失败: ${code}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析联系人列表失败' }
const contacts = JSON.parse(jsonStr)
return { success: true, contacts: Array.isArray(contacts) ? contacts : [] }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getContactAliasMap(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetContactAliasMap) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const code = this.wcdbGetContactAliasMap(this.handle, JSON.stringify(usernames || []), outPtr)
if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人 alias 失败: ${code}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析联系人 alias 失败' }
const map = JSON.parse(jsonStr)
return { success: true, map }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getContactFriendFlags(usernames: string[]): Promise<{ success: boolean; map?: Record<string, boolean>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetContactFriendFlags) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const code = this.wcdbGetContactFriendFlags(this.handle, JSON.stringify(usernames || []), outPtr)
if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人好友标记失败: ${code}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析联系人好友标记失败' }
const map = JSON.parse(jsonStr)
return { success: true, map }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getChatRoomExtBuffer(chatroomId: string): Promise<{ success: boolean; extBuffer?: string; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetChatRoomExtBuffer) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const code = this.wcdbGetChatRoomExtBuffer(this.handle, chatroomId, outPtr)
if (code !== 0 || !outPtr[0]) return { success: false, error: `获取群聊 ext_buffer 失败: ${code}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析群聊 ext_buffer 失败' }
const data = JSON.parse(jsonStr) || {}
const extBuffer = String(data.ext_buffer || '').trim()
return { success: true, extBuffer: extBuffer || undefined }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getAggregateStats(sessionIds: string[], beginTimestamp: number = 0, endTimestamp: number = 0): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
@@ -2078,8 +2472,11 @@ export class WcdbCore {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
}
const startedAt = Date.now()
try {
if (!this.wcdbExecQuery) return { success: false, error: '接口未就绪' }
const fallbackFlag = /fallback|diag|diagnostic/i.test(String(sql || ''))
this.writeLog(`[audit:execQuery] kind=${kind} path=${path || ''} sql_len=${String(sql || '').length} fallback=${fallbackFlag ? 1 : 0}`)
// 如果提供了参数,使用参数化查询(需要 C++ 层支持)
// 注意:当前 wcdbExecQuery 可能不支持参数化,这是一个占位符实现
@@ -2114,12 +2511,14 @@ export class WcdbCore {
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析查询结果失败' }
const rows = JSON.parse(jsonStr)
this.writeLog(`[audit:execQuery] done kind=${kind} cost_ms=${Date.now() - startedAt} rows=${Array.isArray(rows) ? rows.length : -1}`)
if (isContactQuery) {
const count = Array.isArray(rows) ? rows.length : -1
this.writeLog(`[diag:execQuery] contact query ok rows=${count} kind=${kind} path=${effectivePath} sql="${this.formatSqlForLog(sql)}"`, true)
}
return { success: true, rows }
} catch (e) {
this.writeLog(`[audit:execQuery] fail kind=${kind} cost_ms=${Date.now() - startedAt} err=${String(e)}`)
const isContactQuery = String(kind).toLowerCase() === 'contact' || /\bfrom\s+contact\b/i.test(String(sql))
if (isContactQuery) {
this.writeLog(`[diag:execQuery] contact query exception kind=${kind} path=${path || ''} sql="${this.formatSqlForLog(sql)}" err=${String(e)}`, true)
@@ -2209,6 +2608,93 @@ export class WcdbCore {
}
}
async getVoiceDataBatch(
requests: Array<{ session_id: string; create_time: number; local_id?: number; svr_id?: string | number; candidates?: string[] }>
): Promise<{ success: boolean; rows?: Array<{ index: number; hex?: string }>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetVoiceDataBatch) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const payload = JSON.stringify(Array.isArray(requests) ? requests : [])
const result = this.wcdbGetVoiceDataBatch(this.handle, payload, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取语音数据失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析批量语音数据失败' }
const rows = JSON.parse(jsonStr)
const normalized = Array.isArray(rows) ? rows.map((row: any) => ({
index: Number(row?.index ?? 0),
hex: row?.hex ? String(row.hex) : undefined
})) : []
return { success: true, rows: normalized }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getMediaSchemaSummary(dbPath: string): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetMediaSchemaSummary) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetMediaSchemaSummary(this.handle, dbPath, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取媒体表结构摘要失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析媒体表结构摘要失败' }
const data = JSON.parse(jsonStr) || {}
return { success: true, data }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getHeadImageBuffers(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetHeadImageBuffers) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetHeadImageBuffers(this.handle, JSON.stringify(usernames || []), outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取头像二进制失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析头像二进制失败' }
const map = JSON.parse(jsonStr) || {}
return { success: true, map }
} catch (e) {
return { success: false, error: String(e) }
}
}
async resolveImageHardlink(md5: string, accountDir?: string): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbResolveImageHardlink) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbResolveImageHardlink(this.handle, md5, accountDir || null, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `解析图片 hardlink 失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析图片 hardlink 响应失败' }
const data = JSON.parse(jsonStr) || {}
return { success: true, data }
} catch (e) {
return { success: false, error: String(e) }
}
}
async resolveVideoHardlinkMd5(md5: string, dbPath?: string): Promise<{ success: boolean; data?: any; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbResolveVideoHardlinkMd5) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbResolveVideoHardlinkMd5(this.handle, md5, dbPath || null, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `解析视频 hardlink 失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析视频 hardlink 响应失败' }
const data = JSON.parse(jsonStr) || {}
return { success: true, data }
} catch (e) {
return { success: false, error: String(e) }
}
}
/**
* 数据收集初始化
*/
@@ -2373,6 +2859,45 @@ export class WcdbCore {
return { success: false, error: String(e) }
}
}
async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetSnsUsernames) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetSnsUsernames(this.handle, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取朋友圈用户名失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析朋友圈用户名失败' }
const usernames = JSON.parse(jsonStr)
return { success: true, usernames: Array.isArray(usernames) ? usernames.map((u: any) => String(u || '').trim()).filter(Boolean) : [] }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getSnsExportStats(myWxid?: string): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
if (!this.wcdbGetSnsExportStats) return { success: false, error: '接口未就绪' }
try {
const outPtr = [null as any]
const result = this.wcdbGetSnsExportStats(this.handle, myWxid || null, outPtr)
if (result !== 0 || !outPtr[0]) return { success: false, error: `获取朋友圈导出统计失败: ${result}` }
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) return { success: false, error: '解析朋友圈导出统计失败' }
const raw = JSON.parse(jsonStr) || {}
return {
success: true,
data: {
totalPosts: Number(raw.total_posts || 0),
totalFriends: Number(raw.total_friends || 0),
myPosts: raw.my_posts === null || raw.my_posts === undefined ? null : Number(raw.my_posts || 0)
}
}
} catch (e) {
return { success: false, error: String(e) }
}
}
/**
* 为朋友圈安装删除
*/

View File

@@ -222,6 +222,48 @@ export class WcdbService {
return this.callWorker('getMessageCounts', { sessionIds })
}
async getSessionMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
return this.callWorker('getSessionMessageCounts', { sessionIds })
}
async getSessionMessageTypeStats(
sessionId: string,
beginTimestamp: number = 0,
endTimestamp: number = 0
): Promise<{ success: boolean; data?: any; error?: string }> {
return this.callWorker('getSessionMessageTypeStats', { sessionId, beginTimestamp, endTimestamp })
}
async getSessionMessageTypeStatsBatch(
sessionIds: string[],
options?: {
beginTimestamp?: number
endTimestamp?: number
quickMode?: boolean
includeGroupSenderCount?: boolean
}
): Promise<{ success: boolean; data?: Record<string, any>; error?: string }> {
return this.callWorker('getSessionMessageTypeStatsBatch', { sessionIds, options })
}
async getSessionMessageDateCounts(sessionId: string): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
return this.callWorker('getSessionMessageDateCounts', { sessionId })
}
async getSessionMessageDateCountsBatch(sessionIds: string[]): Promise<{ success: boolean; data?: Record<string, Record<string, number>>; error?: string }> {
return this.callWorker('getSessionMessageDateCountsBatch', { sessionIds })
}
async getMessagesByType(
sessionId: string,
localType: number,
ascending = false,
limit = 0,
offset = 0
): Promise<{ success: boolean; rows?: any[]; error?: string }> {
return this.callWorker('getMessagesByType', { sessionId, localType, ascending, limit, offset })
}
/**
* 获取联系人昵称
*/
@@ -287,6 +329,14 @@ export class WcdbService {
return this.callWorker('getMessageMeta', { dbPath, tableName, limit, offset })
}
async getMessageTableColumns(dbPath: string, tableName: string): Promise<{ success: boolean; columns?: string[]; error?: string }> {
return this.callWorker('getMessageTableColumns', { dbPath, tableName })
}
async getMessageTableTimeRange(dbPath: string, tableName: string): Promise<{ success: boolean; data?: any; error?: string }> {
return this.callWorker('getMessageTableTimeRange', { dbPath, tableName })
}
/**
* 获取联系人详情
*/
@@ -301,6 +351,26 @@ export class WcdbService {
return this.callWorker('getContactStatus', { usernames })
}
async getContactTypeCounts(): Promise<{ success: boolean; counts?: { private: number; group: number; official: number; former_friend: number }; error?: string }> {
return this.callWorker('getContactTypeCounts')
}
async getContactsCompact(usernames: string[] = []): Promise<{ success: boolean; contacts?: any[]; error?: string }> {
return this.callWorker('getContactsCompact', { usernames })
}
async getContactAliasMap(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
return this.callWorker('getContactAliasMap', { usernames })
}
async getContactFriendFlags(usernames: string[]): Promise<{ success: boolean; map?: Record<string, boolean>; error?: string }> {
return this.callWorker('getContactFriendFlags', { usernames })
}
async getChatRoomExtBuffer(chatroomId: string): Promise<{ success: boolean; extBuffer?: string; error?: string }> {
return this.callWorker('getChatRoomExtBuffer', { chatroomId })
}
/**
* 获取聚合统计数据
*/
@@ -372,7 +442,7 @@ export class WcdbService {
}
/**
* 执行 SQL 查询(支持参数化查询
* 执行 SQL 查询(仅主进程内部使用fallback/diagnostic/低频兼容
*/
async execQuery(kind: string, path: string | null, sql: string, params: any[] = []): Promise<{ success: boolean; rows?: any[]; error?: string }> {
return this.callWorker('execQuery', { kind, path, sql, params })
@@ -417,6 +487,28 @@ export class WcdbService {
return this.callWorker('getVoiceData', { sessionId, createTime, candidates, localId, svrId })
}
async getVoiceDataBatch(
requests: Array<{ session_id: string; create_time: number; local_id?: number; svr_id?: string | number; candidates?: string[] }>
): Promise<{ success: boolean; rows?: Array<{ index: number; hex?: string }>; error?: string }> {
return this.callWorker('getVoiceDataBatch', { requests })
}
async getMediaSchemaSummary(dbPath: string): Promise<{ success: boolean; data?: any; error?: string }> {
return this.callWorker('getMediaSchemaSummary', { dbPath })
}
async getHeadImageBuffers(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
return this.callWorker('getHeadImageBuffers', { usernames })
}
async resolveImageHardlink(md5: string, accountDir?: string): Promise<{ success: boolean; data?: any; error?: string }> {
return this.callWorker('resolveImageHardlink', { md5, accountDir })
}
async resolveVideoHardlinkMd5(md5: string, dbPath?: string): Promise<{ success: boolean; data?: any; error?: string }> {
return this.callWorker('resolveVideoHardlinkMd5', { md5, dbPath })
}
/**
* 获取朋友圈
*/
@@ -431,6 +523,14 @@ export class WcdbService {
return this.callWorker('getSnsAnnualStats', { beginTimestamp, endTimestamp })
}
async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
return this.callWorker('getSnsUsernames')
}
async getSnsExportStats(myWxid?: string): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
return this.callWorker('getSnsExportStats', { myWxid })
}
/**
* 安装朋友圈删除拦截
*/

View File

@@ -59,6 +59,24 @@ if (parentPort) {
case 'getMessageCounts':
result = await core.getMessageCounts(payload.sessionIds)
break
case 'getSessionMessageCounts':
result = await core.getSessionMessageCounts(payload.sessionIds)
break
case 'getSessionMessageTypeStats':
result = await core.getSessionMessageTypeStats(payload.sessionId, payload.beginTimestamp, payload.endTimestamp)
break
case 'getSessionMessageTypeStatsBatch':
result = await core.getSessionMessageTypeStatsBatch(payload.sessionIds, payload.options)
break
case 'getSessionMessageDateCounts':
result = await core.getSessionMessageDateCounts(payload.sessionId)
break
case 'getSessionMessageDateCountsBatch':
result = await core.getSessionMessageDateCountsBatch(payload.sessionIds)
break
case 'getMessagesByType':
result = await core.getMessagesByType(payload.sessionId, payload.localType, payload.ascending, payload.limit, payload.offset)
break
case 'getDisplayNames':
result = await core.getDisplayNames(payload.usernames)
break
@@ -89,12 +107,33 @@ if (parentPort) {
case 'getMessageMeta':
result = await core.getMessageMeta(payload.dbPath, payload.tableName, payload.limit, payload.offset)
break
case 'getMessageTableColumns':
result = await core.getMessageTableColumns(payload.dbPath, payload.tableName)
break
case 'getMessageTableTimeRange':
result = await core.getMessageTableTimeRange(payload.dbPath, payload.tableName)
break
case 'getContact':
result = await core.getContact(payload.username)
break
case 'getContactStatus':
result = await core.getContactStatus(payload.usernames)
break
case 'getContactTypeCounts':
result = await core.getContactTypeCounts()
break
case 'getContactsCompact':
result = await core.getContactsCompact(payload.usernames)
break
case 'getContactAliasMap':
result = await core.getContactAliasMap(payload.usernames)
break
case 'getContactFriendFlags':
result = await core.getContactFriendFlags(payload.usernames)
break
case 'getChatRoomExtBuffer':
result = await core.getChatRoomExtBuffer(payload.chatroomId)
break
case 'getAggregateStats':
result = await core.getAggregateStats(payload.sessionIds, payload.beginTimestamp, payload.endTimestamp)
break
@@ -149,12 +188,33 @@ if (parentPort) {
console.error('[wcdbWorker] getVoiceData failed:', result.error)
}
break
case 'getVoiceDataBatch':
result = await core.getVoiceDataBatch(payload.requests)
break
case 'getMediaSchemaSummary':
result = await core.getMediaSchemaSummary(payload.dbPath)
break
case 'getHeadImageBuffers':
result = await core.getHeadImageBuffers(payload.usernames)
break
case 'resolveImageHardlink':
result = await core.resolveImageHardlink(payload.md5, payload.accountDir)
break
case 'resolveVideoHardlinkMd5':
result = await core.resolveVideoHardlinkMd5(payload.md5, payload.dbPath)
break
case 'getSnsTimeline':
result = await core.getSnsTimeline(payload.limit, payload.offset, payload.usernames, payload.keyword, payload.startTime, payload.endTime)
break
case 'getSnsAnnualStats':
result = await core.getSnsAnnualStats(payload.beginTimestamp, payload.endTimestamp)
break
case 'getSnsUsernames':
result = await core.getSnsUsernames()
break
case 'getSnsExportStats':
result = await core.getSnsExportStats(payload.myWxid)
break
case 'installSnsBlockDeleteTrigger':
result = await core.installSnsBlockDeleteTrigger()
break