mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-05 15:08:14 +00:00
修复 #597;实现 #556;修复 #623与 #543;修复卡片图片问题
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { join, dirname, basename, extname } from 'path'
|
||||
import { join, dirname, basename, extname } from 'path'
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, watch, promises as fsPromises } from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
@@ -558,6 +558,51 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
async checkAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const connectResult = await this.ensureConnected()
|
||||
if (!connectResult.success) return { success: false, error: connectResult.error }
|
||||
const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
return await wcdbService.checkMessageAntiRevokeTriggers(normalizedIds)
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async installAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const connectResult = await this.ensureConnected()
|
||||
if (!connectResult.success) return { success: false, error: connectResult.error }
|
||||
const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
return await wcdbService.installMessageAntiRevokeTriggers(normalizedIds)
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const connectResult = await this.ensureConnected()
|
||||
if (!connectResult.success) return { success: false, error: connectResult.error }
|
||||
const normalizedIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
return await wcdbService.uninstallMessageAntiRevokeTriggers(normalizedIds)
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(优化:先返回基础数据,不等待联系人信息加载)
|
||||
*/
|
||||
@@ -1773,18 +1818,9 @@ class ChatService {
|
||||
}
|
||||
|
||||
private getMessageSourceInfo(row: Record<string, any>): { dbName?: string; tableName?: string; dbPath?: string } {
|
||||
const dbPath = String(
|
||||
this.getRowField(row, ['_db_path', 'db_path', 'dbPath', 'database_path', 'databasePath', 'source_db_path'])
|
||||
|| ''
|
||||
).trim()
|
||||
const explicitDbName = String(
|
||||
this.getRowField(row, ['db_name', 'dbName', 'database_name', 'databaseName', 'db', 'database', 'source_db'])
|
||||
|| ''
|
||||
).trim()
|
||||
const tableName = String(
|
||||
this.getRowField(row, ['table_name', 'tableName', 'table', 'source_table', 'sourceTable'])
|
||||
|| ''
|
||||
).trim()
|
||||
const dbPath = String(row._db_path || row.db_path || '').trim()
|
||||
const explicitDbName = String(row.db_name || '').trim()
|
||||
const tableName = String(row.table_name || '').trim()
|
||||
const dbName = explicitDbName || (dbPath ? basename(dbPath, extname(dbPath)) : '')
|
||||
return {
|
||||
dbName: dbName || undefined,
|
||||
@@ -3201,7 +3237,7 @@ class ChatService {
|
||||
if (!batch.success) break
|
||||
const rows = Array.isArray(batch.rows) ? batch.rows as Record<string, any>[] : []
|
||||
for (const row of rows) {
|
||||
const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 1)
|
||||
const localType = this.getRowInt(row, ['local_type'], 1)
|
||||
if (localType === 50) {
|
||||
counters.callMessages += 1
|
||||
continue
|
||||
@@ -3216,8 +3252,8 @@ class ChatService {
|
||||
}
|
||||
if (localType !== 49) continue
|
||||
|
||||
const rawMessageContent = this.getRowField(row, ['message_content', 'messageContent', 'msg_content', 'msgContent', 'content', 'WCDB_CT_message_content'])
|
||||
const rawCompressContent = this.getRowField(row, ['compress_content', 'compressContent', 'compressed_content', 'compressedContent', 'WCDB_CT_compress_content'])
|
||||
const rawMessageContent = row.message_content
|
||||
const rawCompressContent = row.compress_content
|
||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
const xmlType = this.extractType49XmlTypeForStats(content)
|
||||
if (xmlType === '2000') counters.transferMessages += 1
|
||||
@@ -3270,7 +3306,7 @@ class ChatService {
|
||||
for (const row of rows) {
|
||||
stats.totalMessages += 1
|
||||
|
||||
const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 1)
|
||||
const localType = this.getRowInt(row, ['local_type'], 1)
|
||||
if (localType === 34) stats.voiceMessages += 1
|
||||
if (localType === 3) stats.imageMessages += 1
|
||||
if (localType === 43) stats.videoMessages += 1
|
||||
@@ -3279,8 +3315,8 @@ class ChatService {
|
||||
if (localType === 8589934592049) stats.transferMessages += 1
|
||||
if (localType === 8594229559345) stats.redPacketMessages += 1
|
||||
if (localType === 49) {
|
||||
const rawMessageContent = this.getRowField(row, ['message_content', 'messageContent', 'msg_content', 'msgContent', 'content', 'WCDB_CT_message_content'])
|
||||
const rawCompressContent = this.getRowField(row, ['compress_content', 'compressContent', 'compressed_content', 'compressedContent', 'WCDB_CT_compress_content'])
|
||||
const rawMessageContent = row.message_content
|
||||
const rawCompressContent = row.compress_content
|
||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
const xmlType = this.extractType49XmlTypeForStats(content)
|
||||
if (xmlType === '2000') stats.transferMessages += 1
|
||||
@@ -3289,7 +3325,7 @@ class ChatService {
|
||||
|
||||
const createTime = this.getRowInt(
|
||||
row,
|
||||
['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'],
|
||||
['create_time'],
|
||||
0
|
||||
)
|
||||
if (createTime > 0) {
|
||||
@@ -3302,7 +3338,7 @@ class ChatService {
|
||||
}
|
||||
|
||||
if (sessionId.endsWith('@chatroom')) {
|
||||
const sender = String(this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) || '').trim()
|
||||
const sender = String(row.sender_username || '').trim()
|
||||
const senderKeys = this.buildIdentityKeys(sender)
|
||||
if (senderKeys.length > 0) {
|
||||
senderIdentities.add(senderKeys[0])
|
||||
@@ -3310,7 +3346,7 @@ class ChatService {
|
||||
stats.groupMyMessages = (stats.groupMyMessages || 0) + 1
|
||||
}
|
||||
} else {
|
||||
const isSend = this.coerceRowNumber(this.getRowField(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send']))
|
||||
const isSend = this.coerceRowNumber(row.computed_is_send ?? row.is_send)
|
||||
if (Number.isFinite(isSend) && isSend === 1) {
|
||||
stats.groupMyMessages = (stats.groupMyMessages || 0) + 1
|
||||
}
|
||||
@@ -3744,32 +3780,18 @@ class ChatService {
|
||||
const messages: Message[] = []
|
||||
for (const row of rows) {
|
||||
const sourceInfo = this.getMessageSourceInfo(row)
|
||||
const rawMessageContent = this.getRowField(row, [
|
||||
'message_content',
|
||||
'messageContent',
|
||||
'content',
|
||||
'msg_content',
|
||||
'msgContent',
|
||||
'WCDB_CT_message_content',
|
||||
'WCDB_CT_messageContent'
|
||||
]);
|
||||
const rawCompressContent = this.getRowField(row, [
|
||||
'compress_content',
|
||||
'compressContent',
|
||||
'compressed_content',
|
||||
'WCDB_CT_compress_content',
|
||||
'WCDB_CT_compressContent'
|
||||
]);
|
||||
const rawMessageContent = row.message_content
|
||||
const rawCompressContent = row.compress_content
|
||||
|
||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent);
|
||||
const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 1)
|
||||
const isSendRaw = this.getRowField(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send'])
|
||||
const localType = this.getRowInt(row, ['local_type'], 1)
|
||||
const isSendRaw = row.computed_is_send ?? row.is_send
|
||||
const parsedRawIsSend = isSendRaw === null ? null : parseInt(isSendRaw, 10)
|
||||
const senderUsername = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username'])
|
||||
const senderUsername = row.sender_username
|
||||
|| this.extractSenderUsernameFromContent(content)
|
||||
|| null
|
||||
const { isSend } = this.resolveMessageIsSend(parsedRawIsSend, senderUsername)
|
||||
const createTime = this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0)
|
||||
const createTime = this.getRowInt(row, ['create_time'], 0)
|
||||
|
||||
if (senderUsername && !myWxid) {
|
||||
// [DEBUG] Issue #34: 未配置 myWxid,无法判断是否发送
|
||||
@@ -3954,10 +3976,10 @@ class ChatService {
|
||||
if (!quotedSender && type49Info.quotedSender !== undefined) quotedSender = type49Info.quotedSender
|
||||
}
|
||||
|
||||
const localId = this.getRowInt(row, ['local_id', 'localId', 'LocalId', 'msg_local_id', 'msgLocalId', 'MsgLocalId', 'msg_id', 'msgId', 'MsgId', 'id', 'WCDB_CT_local_id'], 0)
|
||||
const serverIdRaw = this.normalizeUnsignedIntegerToken(this.getRowField(row, ['server_id', 'serverId', 'ServerId', 'msg_server_id', 'msgServerId', 'MsgServerId', 'WCDB_CT_server_id']))
|
||||
const serverId = this.getRowInt(row, ['server_id', 'serverId', 'ServerId', 'msg_server_id', 'msgServerId', 'MsgServerId', 'WCDB_CT_server_id'], 0)
|
||||
const sortSeq = this.getRowInt(row, ['sort_seq', 'sortSeq', 'seq', 'sequence', 'WCDB_CT_sort_seq'], createTime)
|
||||
const localId = this.getRowInt(row, ['local_id'], 0)
|
||||
const serverIdRaw = this.normalizeUnsignedIntegerToken(row.server_id)
|
||||
const serverId = this.getRowInt(row, ['server_id'], 0)
|
||||
const sortSeq = this.getRowInt(row, ['sort_seq'], createTime)
|
||||
|
||||
messages.push({
|
||||
messageKey: this.buildMessageKey({
|
||||
@@ -4404,18 +4426,7 @@ class ChatService {
|
||||
}
|
||||
|
||||
private parseImageDatNameFromRow(row: Record<string, any>): string | undefined {
|
||||
const packed = this.getRowField(row, [
|
||||
'packed_info_data',
|
||||
'packed_info',
|
||||
'packedInfoData',
|
||||
'packedInfo',
|
||||
'PackedInfoData',
|
||||
'PackedInfo',
|
||||
'WCDB_CT_packed_info_data',
|
||||
'WCDB_CT_packed_info',
|
||||
'WCDB_CT_PackedInfoData',
|
||||
'WCDB_CT_PackedInfo'
|
||||
])
|
||||
const packed = row.packed_info_data
|
||||
const buffer = this.decodePackedInfo(packed)
|
||||
if (!buffer || buffer.length === 0) return undefined
|
||||
const printable: number[] = []
|
||||
@@ -5303,14 +5314,14 @@ class ChatService {
|
||||
row: Record<string, any>,
|
||||
rawContent: string
|
||||
): Promise<string | null> {
|
||||
const directSender = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username'])
|
||||
const directSender = row.sender_username
|
||||
|| this.extractSenderUsernameFromContent(rawContent)
|
||||
if (directSender) {
|
||||
return directSender
|
||||
}
|
||||
|
||||
const dbPath = this.getRowField(row, ['db_path', 'dbPath', '_db_path'])
|
||||
const realSenderId = this.getRowField(row, ['real_sender_id', 'realSenderId'])
|
||||
const dbPath = row._db_path
|
||||
const realSenderId = row.real_sender_id
|
||||
if (!dbPath || realSenderId === null || realSenderId === undefined || String(realSenderId).trim() === '') {
|
||||
return null
|
||||
}
|
||||
@@ -5359,7 +5370,7 @@ class ChatService {
|
||||
50: '[通话]',
|
||||
10000: '[系统消息]',
|
||||
244813135921: '[引用消息]',
|
||||
266287972401: '[拍一拍]',
|
||||
266287972401: '拍一拍',
|
||||
81604378673: '[聊天记录]',
|
||||
154618822705: '[小程序]',
|
||||
8594229559345: '[红包]',
|
||||
@@ -5468,7 +5479,7 @@ class ChatService {
|
||||
* XML: <msg><appmsg...><title>"XX"拍了拍"XX"相信未来!</title>...</msg>
|
||||
*/
|
||||
private cleanPatMessage(content: string): string {
|
||||
if (!content) return '[拍一拍]'
|
||||
if (!content) return '拍一拍'
|
||||
|
||||
// 1. 优先从 XML <title> 标签提取内容
|
||||
const titleMatch = /<title>([\s\S]*?)<\/title>/i.exec(content)
|
||||
@@ -5478,14 +5489,14 @@ class ChatService {
|
||||
.replace(/\]\]>/g, '')
|
||||
.trim()
|
||||
if (title) {
|
||||
return `[拍一拍] ${title}`
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 尝试匹配标准的 "A拍了拍B" 格式
|
||||
const match = /^(.+?拍了拍.+?)(?:[\r\n]|$|ງ|wxid_)/.exec(content)
|
||||
if (match) {
|
||||
return `[拍一拍] ${match[1].trim()}`
|
||||
return match[1].trim()
|
||||
}
|
||||
|
||||
// 3. 如果匹配失败,尝试清理掉疑似的 garbage (wxid, 乱码)
|
||||
@@ -5499,10 +5510,10 @@ class ChatService {
|
||||
|
||||
// 如果清理后还有内容,返回
|
||||
if (cleaned && cleaned.length > 1 && !cleaned.includes('xml')) {
|
||||
return `[拍一拍] ${cleaned}`
|
||||
return cleaned
|
||||
}
|
||||
|
||||
return '[拍一拍]'
|
||||
return '拍一拍'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7520,11 +7531,7 @@ class ChatService {
|
||||
|
||||
for (const row of result.messages) {
|
||||
let message = await this.parseMessage(row, { source: 'search', sessionId })
|
||||
const resolvedSessionId = String(
|
||||
sessionId ||
|
||||
this.getRowField(row, ['_session_id', 'session_id', 'sessionId', 'talker', 'username'])
|
||||
|| ''
|
||||
).trim()
|
||||
const resolvedSessionId = String(sessionId || row._session_id || '').trim()
|
||||
const needsDetailHydration = isGroupSearch &&
|
||||
Boolean(sessionId) &&
|
||||
message.localId > 0 &&
|
||||
@@ -7559,32 +7566,18 @@ class ChatService {
|
||||
private async parseMessage(row: any, options?: { source?: 'search' | 'detail'; sessionId?: string }): Promise<Message> {
|
||||
const sourceInfo = this.getMessageSourceInfo(row)
|
||||
const rawContent = this.decodeMessageContent(
|
||||
this.getRowField(row, [
|
||||
'message_content',
|
||||
'messageContent',
|
||||
'content',
|
||||
'msg_content',
|
||||
'msgContent',
|
||||
'WCDB_CT_message_content',
|
||||
'WCDB_CT_messageContent'
|
||||
]),
|
||||
this.getRowField(row, [
|
||||
'compress_content',
|
||||
'compressContent',
|
||||
'compressed_content',
|
||||
'WCDB_CT_compress_content',
|
||||
'WCDB_CT_compressContent'
|
||||
])
|
||||
row.message_content,
|
||||
row.compress_content
|
||||
)
|
||||
// 这里复用 parseMessagesBatch 里面的解析逻辑,为了简单我这里先写个基础的
|
||||
// 实际项目中建议抽取 parseRawMessage(row) 供多处使用
|
||||
const localId = this.getRowInt(row, ['local_id', 'localId', 'LocalId', 'msg_local_id', 'msgLocalId', 'MsgLocalId', 'msg_id', 'msgId', 'MsgId', 'id', 'WCDB_CT_local_id'], 0)
|
||||
const serverIdRaw = this.normalizeUnsignedIntegerToken(this.getRowField(row, ['server_id', 'serverId', 'ServerId', 'msg_server_id', 'msgServerId', 'MsgServerId', 'WCDB_CT_server_id']))
|
||||
const serverId = this.getRowInt(row, ['server_id', 'serverId', 'ServerId', 'msg_server_id', 'msgServerId', 'MsgServerId', 'WCDB_CT_server_id'], 0)
|
||||
const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 0)
|
||||
const createTime = this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0)
|
||||
const sortSeq = this.getRowInt(row, ['sort_seq', 'sortSeq', 'seq', 'sequence', 'WCDB_CT_sort_seq'], createTime)
|
||||
const rawIsSend = this.getRowField(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send'])
|
||||
const localId = this.getRowInt(row, ['local_id'], 0)
|
||||
const serverIdRaw = this.normalizeUnsignedIntegerToken(row.server_id)
|
||||
const serverId = this.getRowInt(row, ['server_id'], 0)
|
||||
const localType = this.getRowInt(row, ['local_type'], 0)
|
||||
const createTime = this.getRowInt(row, ['create_time'], 0)
|
||||
const sortSeq = this.getRowInt(row, ['sort_seq'], createTime)
|
||||
const rawIsSend = row.computed_is_send ?? row.is_send
|
||||
const senderUsername = await this.resolveSenderUsernameForMessageRow(row, rawContent)
|
||||
const sendState = this.resolveMessageIsSend(rawIsSend === null ? null : parseInt(rawIsSend, 10), senderUsername)
|
||||
const msg: Message = {
|
||||
@@ -7612,8 +7605,8 @@ class ChatService {
|
||||
}
|
||||
|
||||
if (msg.localId === 0 || msg.createTime === 0) {
|
||||
const rawLocalId = this.getRowField(row, ['local_id', 'localId', 'LocalId', 'msg_local_id', 'msgLocalId', 'MsgLocalId', 'msg_id', 'msgId', 'MsgId', 'id', 'WCDB_CT_local_id'])
|
||||
const rawCreateTime = this.getRowField(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'])
|
||||
const rawLocalId = row.local_id
|
||||
const rawCreateTime = row.create_time
|
||||
console.warn('[ChatService] parseMessage raw keys', {
|
||||
rawLocalId,
|
||||
rawLocalIdType: rawLocalId ? typeof rawLocalId : 'null',
|
||||
|
||||
@@ -61,6 +61,7 @@ interface ConfigSchema {
|
||||
windowCloseBehavior: 'ask' | 'tray' | 'quit'
|
||||
quoteLayout: 'quote-top' | 'quote-bottom'
|
||||
wordCloudExcludeWords: string[]
|
||||
exportWriteLayout: 'A' | 'B' | 'C'
|
||||
}
|
||||
|
||||
// 需要 safeStorage 加密的字段(普通模式)
|
||||
@@ -133,7 +134,8 @@ export class ConfigService {
|
||||
messagePushEnabled: false,
|
||||
windowCloseBehavior: 'ask',
|
||||
quoteLayout: 'quote-top',
|
||||
wordCloudExcludeWords: []
|
||||
wordCloudExcludeWords: [],
|
||||
exportWriteLayout: 'A'
|
||||
}
|
||||
|
||||
const storeOptions: any = {
|
||||
|
||||
@@ -430,6 +430,8 @@ class ExportService {
|
||||
let lastSessionId = ''
|
||||
let lastCollected = 0
|
||||
let lastExported = 0
|
||||
const MIN_PROGRESS_EMIT_INTERVAL_MS = 250
|
||||
const MESSAGE_PROGRESS_DELTA_THRESHOLD = 500
|
||||
|
||||
const commit = (progress: ExportProgress) => {
|
||||
onProgress(progress)
|
||||
@@ -454,9 +456,9 @@ class ExportService {
|
||||
const shouldEmit = force ||
|
||||
phase !== lastPhase ||
|
||||
sessionId !== lastSessionId ||
|
||||
collectedDelta >= 200 ||
|
||||
exportedDelta >= 200 ||
|
||||
(now - lastSentAt >= 120)
|
||||
collectedDelta >= MESSAGE_PROGRESS_DELTA_THRESHOLD ||
|
||||
exportedDelta >= MESSAGE_PROGRESS_DELTA_THRESHOLD ||
|
||||
(now - lastSentAt >= MIN_PROGRESS_EMIT_INTERVAL_MS)
|
||||
|
||||
if (shouldEmit && pending) {
|
||||
commit(pending)
|
||||
@@ -3537,20 +3539,11 @@ class ExportService {
|
||||
console.log(`[Export] 使用缩略图替代 (localId=${msg.localId}): ${thumbResult.localPath}`)
|
||||
result.localPath = thumbResult.localPath
|
||||
} else {
|
||||
console.log(`[Export] 缩略图也获取失败 (localId=${msg.localId}): error=${thumbResult.error || '未知'}`)
|
||||
// 最后尝试:直接从 imageStore 获取缓存的缩略图 data URL
|
||||
const { imageStore } = await import('../main')
|
||||
const cachedThumb = imageStore?.getCachedImage(sessionId, imageMd5, imageDatName)
|
||||
if (cachedThumb) {
|
||||
console.log(`[Export] 从 imageStore 获取到缓存缩略图 (localId=${msg.localId})`)
|
||||
result.localPath = cachedThumb
|
||||
} else {
|
||||
console.log(`[Export] 所有方式均失败 → 将显示 [图片] 占位符`)
|
||||
if (missingRunCacheKey) {
|
||||
this.mediaRunMissingImageKeys.add(missingRunCacheKey)
|
||||
}
|
||||
return null
|
||||
console.log(`[Export] 缩略图也获取失败,所有方式均失败 → 将显示 [图片] 占位符`)
|
||||
if (missingRunCacheKey) {
|
||||
this.mediaRunMissingImageKeys.add(missingRunCacheKey)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3559,7 +3552,7 @@ class ExportService {
|
||||
const imageKey = (imageMd5 || imageDatName || 'image').replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
|
||||
// 从 data URL 或 file URL 获取实际路径
|
||||
let sourcePath = result.localPath
|
||||
let sourcePath: string = result.localPath!
|
||||
if (sourcePath.startsWith('data:')) {
|
||||
// 是 data URL,需要保存为文件
|
||||
const base64Data = sourcePath.split(',')[1]
|
||||
@@ -8389,22 +8382,22 @@ class ExportService {
|
||||
|
||||
const metric = aggregatedData?.[sessionId]
|
||||
const totalCount = Number.isFinite(metric?.totalMessages)
|
||||
? Math.max(0, Math.floor(metric!.totalMessages))
|
||||
? Math.max(0, Math.floor(metric?.totalMessages ?? 0))
|
||||
: 0
|
||||
const voiceCount = Number.isFinite(metric?.voiceMessages)
|
||||
? Math.max(0, Math.floor(metric!.voiceMessages))
|
||||
? Math.max(0, Math.floor(metric?.voiceMessages ?? 0))
|
||||
: 0
|
||||
const imageCount = Number.isFinite(metric?.imageMessages)
|
||||
? Math.max(0, Math.floor(metric!.imageMessages))
|
||||
? Math.max(0, Math.floor(metric?.imageMessages ?? 0))
|
||||
: 0
|
||||
const videoCount = Number.isFinite(metric?.videoMessages)
|
||||
? Math.max(0, Math.floor(metric!.videoMessages))
|
||||
? Math.max(0, Math.floor(metric?.videoMessages ?? 0))
|
||||
: 0
|
||||
const emojiCount = Number.isFinite(metric?.emojiMessages)
|
||||
? Math.max(0, Math.floor(metric!.emojiMessages))
|
||||
? Math.max(0, Math.floor(metric?.emojiMessages ?? 0))
|
||||
: 0
|
||||
const lastTimestamp = Number.isFinite(metric?.lastTimestamp)
|
||||
? Math.max(0, Math.floor(metric!.lastTimestamp))
|
||||
? Math.max(0, Math.floor(metric?.lastTimestamp ?? 0))
|
||||
: undefined
|
||||
const cachedCountRaw = Number(cachedVoiceCountMap[sessionId] || 0)
|
||||
const sessionCachedVoiceCount = Math.min(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { join, dirname, basename } from 'path'
|
||||
import { join, dirname, basename } from 'path'
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
@@ -92,6 +92,9 @@ export class WcdbCore {
|
||||
private wcdbResolveImageHardlinkBatch: any = null
|
||||
private wcdbResolveVideoHardlinkMd5: any = null
|
||||
private wcdbResolveVideoHardlinkMd5Batch: any = null
|
||||
private wcdbInstallMessageAntiRevokeTrigger: any = null
|
||||
private wcdbUninstallMessageAntiRevokeTrigger: any = null
|
||||
private wcdbCheckMessageAntiRevokeTrigger: any = null
|
||||
private wcdbInstallSnsBlockDeleteTrigger: any = null
|
||||
private wcdbUninstallSnsBlockDeleteTrigger: any = null
|
||||
private wcdbCheckSnsBlockDeleteTrigger: any = null
|
||||
@@ -163,7 +166,7 @@ export class WcdbCore {
|
||||
pipePath = this.koffi.decode(namePtr[0], 'char', -1)
|
||||
this.wcdbFreeString(namePtr[0])
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
this.connectMonitorPipe(pipePath)
|
||||
return true
|
||||
@@ -181,7 +184,7 @@ export class WcdbCore {
|
||||
setTimeout(() => {
|
||||
if (!this.monitorCallback) return
|
||||
|
||||
this.monitorPipeClient = net.createConnection(this.monitorPipePath, () => {})
|
||||
this.monitorPipeClient = net.createConnection(this.monitorPipePath, () => { })
|
||||
|
||||
let buffer = ''
|
||||
this.monitorPipeClient.on('data', (data: Buffer) => {
|
||||
@@ -273,7 +276,7 @@ export class WcdbCore {
|
||||
const isArm64 = process.arch === 'arm64'
|
||||
const libName = isMac ? 'libwcdb_api.dylib' : isLinux ? 'libwcdb_api.so' : 'wcdb_api.dll'
|
||||
const subDir = isMac ? 'macos' : isLinux ? 'linux' : (isArm64 ? 'arm64' : '')
|
||||
|
||||
|
||||
const envDllPath = process.env.WCDB_DLL_PATH
|
||||
if (envDllPath && envDllPath.length > 0) {
|
||||
return envDllPath
|
||||
@@ -313,7 +316,7 @@ export class WcdbCore {
|
||||
'-2302': 'WCDB 初始化异常,请重试',
|
||||
'-2303': 'WCDB 未能成功初始化',
|
||||
}
|
||||
const msg = messages[String(code) as keyof typeof messages]
|
||||
const msg = messages[String(code) as unknown as keyof typeof messages]
|
||||
return msg ? `${msg} (错误码: ${code})` : `操作失败,错误码: ${code}`
|
||||
}
|
||||
|
||||
@@ -643,7 +646,7 @@ export class WcdbCore {
|
||||
const dllDir = dirname(dllPath)
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isLinux = process.platform === 'linux'
|
||||
|
||||
|
||||
// 预加载依赖库
|
||||
if (isMac) {
|
||||
const wcdbCorePath = join(dllDir, 'libWCDB.dylib')
|
||||
@@ -1077,6 +1080,27 @@ export class WcdbCore {
|
||||
this.wcdbResolveVideoHardlinkMd5Batch = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_install_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, char** out_error)
|
||||
try {
|
||||
this.wcdbInstallMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_install_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ void** outError)')
|
||||
} catch {
|
||||
this.wcdbInstallMessageAntiRevokeTrigger = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_uninstall_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, char** out_error)
|
||||
try {
|
||||
this.wcdbUninstallMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_uninstall_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ void** outError)')
|
||||
} catch {
|
||||
this.wcdbUninstallMessageAntiRevokeTrigger = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_check_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, int32_t* out_installed)
|
||||
try {
|
||||
this.wcdbCheckMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_check_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ int32* outInstalled)')
|
||||
} catch {
|
||||
this.wcdbCheckMessageAntiRevokeTrigger = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_install_sns_block_delete_trigger(wcdb_handle handle, char** out_error)
|
||||
try {
|
||||
this.wcdbInstallSnsBlockDeleteTrigger = this.lib.func('int32 wcdb_install_sns_block_delete_trigger(int64 handle, _Out_ void** outError)')
|
||||
@@ -1337,12 +1361,12 @@ export class WcdbCore {
|
||||
const raw = String(jsonStr || '')
|
||||
if (!raw) return []
|
||||
// 热路径优化:仅在检测到 16+ 位整数字段时才进行字符串包裹,避免每批次多轮全量 replace。
|
||||
const needsInt64Normalize = /"(?:server_id|serverId|ServerId|msg_server_id|msgServerId|MsgServerId)"\s*:\s*-?\d{16,}/.test(raw)
|
||||
const needsInt64Normalize = /"server_id"\s*:\s*-?\d{16,}/.test(raw)
|
||||
if (!needsInt64Normalize) {
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
const normalized = raw.replace(
|
||||
/("(?:server_id|serverId|ServerId|msg_server_id|msgServerId|MsgServerId)"\s*:\s*)(-?\d{16,})/g,
|
||||
/("server_id"\s*:\s*)(-?\d{16,})/g,
|
||||
'$1"$2"'
|
||||
)
|
||||
return JSON.parse(normalized)
|
||||
@@ -1655,6 +1679,9 @@ export class WcdbCore {
|
||||
const outCount = [0]
|
||||
const result = this.wcdbGetMessageCount(this.handle, sessionId, outCount)
|
||||
if (result !== 0) {
|
||||
if (result === -7) {
|
||||
return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
|
||||
}
|
||||
return { success: false, error: `获取消息总数失败: ${result}` }
|
||||
}
|
||||
return { success: true, count: outCount[0] }
|
||||
@@ -1685,6 +1712,9 @@ export class WcdbCore {
|
||||
const sessionId = normalizedSessionIds[i]
|
||||
const outCount = [0]
|
||||
const result = this.wcdbGetMessageCount(this.handle, sessionId, outCount)
|
||||
if (result === -7) {
|
||||
return { success: false, error: `message schema mismatch:会话 ${sessionId} 的消息表结构不匹配` }
|
||||
}
|
||||
counts[sessionId] = result === 0 && Number.isFinite(outCount[0]) ? Math.max(0, Math.floor(outCount[0])) : 0
|
||||
|
||||
if (i > 0 && i % 160 === 0) {
|
||||
@@ -1704,6 +1734,9 @@ export class WcdbCore {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbGetSessionMessageCounts(this.handle, JSON.stringify(sessionIds || []), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) {
|
||||
if (result === -7) {
|
||||
return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
|
||||
}
|
||||
return { success: false, error: `获取会话消息总数失败: ${result}` }
|
||||
}
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
@@ -2661,7 +2694,9 @@ export class WcdbCore {
|
||||
)
|
||||
const hint = result === -3
|
||||
? `创建游标失败: ${result}(消息数据库未找到)。如果你最近重装过微信,请尝试重新指定数据目录后重试`
|
||||
: `创建游标失败: ${result},请查看日志`
|
||||
: result === -7
|
||||
? 'message schema mismatch:当前账号消息表结构与程序要求不一致'
|
||||
: `创建游标失败: ${result},请查看日志`
|
||||
return { success: false, error: hint }
|
||||
}
|
||||
return { success: true, cursor: outCursor[0] }
|
||||
@@ -2719,6 +2754,9 @@ export class WcdbCore {
|
||||
`openMessageCursorLite failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
|
||||
true
|
||||
)
|
||||
if (result === -7) {
|
||||
return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
|
||||
}
|
||||
return { success: false, error: `创建游标失败: ${result},请查看日志` }
|
||||
}
|
||||
return { success: true, cursor: outCursor[0] }
|
||||
@@ -2790,14 +2828,14 @@ export class WcdbCore {
|
||||
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 可能不支持参数化,这是一个占位符实现
|
||||
// TODO: 需要更新 C++ 层的 wcdb_exec_query 以支持参数绑定
|
||||
if (params && params.length > 0) {
|
||||
console.warn('[wcdbCore] execQuery: 参数化查询暂未在 C++ 层实现,将使用原始 SQL(可能存在注入风险)')
|
||||
}
|
||||
|
||||
|
||||
const normalizedKind = String(kind || '').toLowerCase()
|
||||
const isContactQuery = normalizedKind === 'contact' || /\bfrom\s+contact\b/i.test(String(sql))
|
||||
let effectivePath = path || ''
|
||||
@@ -3481,6 +3519,122 @@ export class WcdbCore {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async installMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
||||
const normalizedSessionId = String(sessionId || '').trim()
|
||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||
try {
|
||||
const outPtr = [null]
|
||||
const status = this.wcdbInstallMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outPtr)
|
||||
let msg = ''
|
||||
if (outPtr[0]) {
|
||||
try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
|
||||
try { this.wcdbFreeString(outPtr[0]) } catch { }
|
||||
}
|
||||
if (status === 1) {
|
||||
return { success: true, alreadyInstalled: true }
|
||||
}
|
||||
if (status !== 0) {
|
||||
return { success: false, error: msg || `DLL error ${status}` }
|
||||
}
|
||||
return { success: true, alreadyInstalled: false }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
||||
const normalizedSessionId = String(sessionId || '').trim()
|
||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||
try {
|
||||
const outPtr = [null]
|
||||
const status = this.wcdbUninstallMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outPtr)
|
||||
let msg = ''
|
||||
if (outPtr[0]) {
|
||||
try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
|
||||
try { this.wcdbFreeString(outPtr[0]) } catch { }
|
||||
}
|
||||
if (status !== 0) {
|
||||
return { success: false, error: msg || `DLL error ${status}` }
|
||||
}
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async checkMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; installed?: boolean; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' }
|
||||
const normalizedSessionId = String(sessionId || '').trim()
|
||||
if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
|
||||
try {
|
||||
const outInstalled = [0]
|
||||
const status = this.wcdbCheckMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outInstalled)
|
||||
if (status !== 0) {
|
||||
return { success: false, error: `DLL error ${status}` }
|
||||
}
|
||||
return { success: true, installed: outInstalled[0] === 1 }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async checkMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
||||
return { success: true, rows: [] }
|
||||
}
|
||||
const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
const rows: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }> = []
|
||||
for (const sessionId of uniqueIds) {
|
||||
const result = await this.checkMessageAntiRevokeTrigger(sessionId)
|
||||
rows.push({ sessionId, success: result.success, installed: result.installed, error: result.error })
|
||||
}
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
async installMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
||||
return { success: true, rows: [] }
|
||||
}
|
||||
const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
const rows: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }> = []
|
||||
for (const sessionId of uniqueIds) {
|
||||
const result = await this.installMessageAntiRevokeTrigger(sessionId)
|
||||
rows.push({ sessionId, success: result.success, alreadyInstalled: result.alreadyInstalled, error: result.error })
|
||||
}
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
async uninstallMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
|
||||
success: boolean
|
||||
rows?: Array<{ sessionId: string; success: boolean; error?: string }>
|
||||
error?: string
|
||||
}> {
|
||||
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
||||
return { success: true, rows: [] }
|
||||
}
|
||||
const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
|
||||
const rows: Array<{ sessionId: string; success: boolean; error?: string }> = []
|
||||
for (const sessionId of uniqueIds) {
|
||||
const result = await this.uninstallMessageAntiRevokeTrigger(sessionId)
|
||||
rows.push({ sessionId, success: result.success, error: result.error })
|
||||
}
|
||||
return { success: true, rows }
|
||||
}
|
||||
|
||||
/**
|
||||
* 为朋友圈安装删除
|
||||
*/
|
||||
|
||||
@@ -561,6 +561,24 @@ export class WcdbService {
|
||||
return this.callWorker('getSnsExportStats', { myWxid })
|
||||
}
|
||||
|
||||
async checkMessageAntiRevokeTriggers(
|
||||
sessionIds: string[]
|
||||
): Promise<{ success: boolean; rows?: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }>; error?: string }> {
|
||||
return this.callWorker('checkMessageAntiRevokeTriggers', { sessionIds })
|
||||
}
|
||||
|
||||
async installMessageAntiRevokeTriggers(
|
||||
sessionIds: string[]
|
||||
): Promise<{ success: boolean; rows?: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }>; error?: string }> {
|
||||
return this.callWorker('installMessageAntiRevokeTriggers', { sessionIds })
|
||||
}
|
||||
|
||||
async uninstallMessageAntiRevokeTriggers(
|
||||
sessionIds: string[]
|
||||
): Promise<{ success: boolean; rows?: Array<{ sessionId: string; success: boolean; error?: string }>; error?: string }> {
|
||||
return this.callWorker('uninstallMessageAntiRevokeTriggers', { sessionIds })
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装朋友圈删除拦截
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user