mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
重构与优化,旨在解决遗留的性能问题并优化用户体验,本次提交遗留了较多的待测功能
This commit is contained in:
47
electron/exportWorker.ts
Normal file
47
electron/exportWorker.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 重启电脑'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,47 +5,6 @@ import { tmpdir } from 'os'
|
||||
// DLL 初始化错误信息,用于帮助用户诊断问题
|
||||
let lastDllInitError: string | null = null
|
||||
|
||||
/**
|
||||
* 解析 extra_buffer(protobuf)中的免打扰状态
|
||||
* - 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) }
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为朋友圈安装删除
|
||||
*/
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装朋友圈删除拦截
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user