mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat: 一些非常帅气的优化
This commit is contained in:
@@ -724,6 +724,10 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getLatestMessages(sessionId, limit)
|
return chatService.getLatestMessages(sessionId, limit)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getNewMessages', async (_, sessionId: string, minTime: number, limit?: number) => {
|
||||||
|
return chatService.getNewMessages(sessionId, minTime, limit)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:getContact', async (_, username: string) => {
|
ipcMain.handle('chat:getContact', async (_, username: string) => {
|
||||||
return await chatService.getContact(username)
|
return await chatService.getContact(username)
|
||||||
})
|
})
|
||||||
@@ -1170,7 +1174,7 @@ function checkForUpdatesOnStartup() {
|
|||||||
// 检查该版本是否被用户忽略
|
// 检查该版本是否被用户忽略
|
||||||
const ignoredVersion = configService?.get('ignoredUpdateVersion')
|
const ignoredVersion = configService?.get('ignoredUpdateVersion')
|
||||||
if (ignoredVersion === latestVersion) {
|
if (ignoredVersion === latestVersion) {
|
||||||
console.log(`版本 ${latestVersion} 已被用户忽略,跳过更新提示`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function enforceLocalDllPriority() {
|
|||||||
process.env.PATH = dllPaths
|
process.env.PATH = dllPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WeFlow] Environment PATH updated to enforce local DLL priority:', dllPaths)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit, startTime, endTime, ascending),
|
ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit, startTime, endTime, ascending),
|
||||||
getLatestMessages: (sessionId: string, limit?: number) =>
|
getLatestMessages: (sessionId: string, limit?: number) =>
|
||||||
ipcRenderer.invoke('chat:getLatestMessages', sessionId, limit),
|
ipcRenderer.invoke('chat:getLatestMessages', sessionId, limit),
|
||||||
|
getNewMessages: (sessionId: string, minTime: number, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('chat:getNewMessages', sessionId, minTime, limit),
|
||||||
getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username),
|
getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username),
|
||||||
getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username),
|
getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username),
|
||||||
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
||||||
@@ -132,7 +134,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.invoke('chat:execQuery', kind, path, sql),
|
ipcRenderer.invoke('chat:execQuery', kind, path, sql),
|
||||||
getContacts: () => ipcRenderer.invoke('chat:getContacts'),
|
getContacts: () => ipcRenderer.invoke('chat:getContacts'),
|
||||||
getMessage: (sessionId: string, localId: number) =>
|
getMessage: (sessionId: string, localId: number) =>
|
||||||
ipcRenderer.invoke('chat:getMessage', sessionId, localId)
|
ipcRenderer.invoke('chat:getMessage', sessionId, localId),
|
||||||
|
onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => {
|
||||||
|
ipcRenderer.on('wcdb-change', callback)
|
||||||
|
return () => ipcRenderer.removeListener('wcdb-change', callback)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,11 @@ class AnalyticsService {
|
|||||||
if (match) return match[1]
|
if (match) return match[1]
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
return trimmed
|
|
||||||
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
|
|
||||||
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private isPrivateSession(username: string, cleanedWxid: string): boolean {
|
private isPrivateSession(username: string, cleanedWxid: string): boolean {
|
||||||
@@ -245,6 +249,9 @@ class AnalyticsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async computeAggregateByCursor(sessionIds: string[], beginTimestamp = 0, endTimestamp = 0): Promise<any> {
|
private async computeAggregateByCursor(sessionIds: string[], beginTimestamp = 0, endTimestamp = 0): Promise<any> {
|
||||||
|
const wxid = this.configService.get('myWxid')
|
||||||
|
const cleanedWxid = wxid ? this.cleanAccountDirName(wxid) : ''
|
||||||
|
|
||||||
const aggregate = {
|
const aggregate = {
|
||||||
total: 0,
|
total: 0,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
@@ -269,8 +276,22 @@ class AnalyticsService {
|
|||||||
if (endTimestamp > 0 && createTime > endTimestamp) return
|
if (endTimestamp > 0 && createTime > endTimestamp) return
|
||||||
|
|
||||||
const localType = parseInt(row.local_type || row.type || '1', 10)
|
const localType = parseInt(row.local_type || row.type || '1', 10)
|
||||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? 0
|
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend
|
||||||
const isSend = String(isSendRaw) === '1' || isSendRaw === 1 || isSendRaw === true
|
let isSend = String(isSendRaw) === '1' || isSendRaw === 1 || isSendRaw === true
|
||||||
|
|
||||||
|
// 如果底层没有提供 is_send,则根据发送者用户名推断
|
||||||
|
const senderUsername = row.sender_username || row.senderUsername || row.sender
|
||||||
|
if (isSendRaw === undefined || isSendRaw === null) {
|
||||||
|
if (senderUsername && (cleanedWxid)) {
|
||||||
|
const senderLower = String(senderUsername).toLowerCase()
|
||||||
|
const myWxidLower = cleanedWxid.toLowerCase()
|
||||||
|
isSend = (
|
||||||
|
senderLower === myWxidLower ||
|
||||||
|
// 兼容非 wxid 开头的账号(如果文件夹名带后缀,如 custom_backup,而 sender 是 custom)
|
||||||
|
(myWxidLower.startsWith(senderLower + '_'))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
aggregate.total += 1
|
aggregate.total += 1
|
||||||
sessionStat.total += 1
|
sessionStat.total += 1
|
||||||
|
|||||||
@@ -115,8 +115,9 @@ class AnnualReportService {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
if (suffixMatch) return suffixMatch[1]
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
return trimmed
|
|
||||||
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureConnectedWithConfig(
|
private async ensureConnectedWithConfig(
|
||||||
@@ -596,9 +597,22 @@ class AnnualReportService {
|
|||||||
if (!createTime) continue
|
if (!createTime) continue
|
||||||
|
|
||||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? '0'
|
const isSendRaw = row.computed_is_send ?? row.is_send ?? '0'
|
||||||
const isSent = parseInt(isSendRaw, 10) === 1
|
let isSent = parseInt(isSendRaw, 10) === 1
|
||||||
const localType = parseInt(row.local_type || row.type || '1', 10)
|
const localType = parseInt(row.local_type || row.type || '1', 10)
|
||||||
|
|
||||||
|
// 兼容逻辑
|
||||||
|
if (isSendRaw === undefined || isSendRaw === null || isSendRaw === '0') {
|
||||||
|
const sender = String(row.sender_username || row.sender || row.talker || '').toLowerCase()
|
||||||
|
if (sender) {
|
||||||
|
const rawLower = rawWxid.toLowerCase()
|
||||||
|
const cleanedLower = cleanedWxid.toLowerCase()
|
||||||
|
if (sender === rawLower || sender === cleanedLower ||
|
||||||
|
rawLower.startsWith(sender + '_') || cleanedLower.startsWith(sender + '_')) {
|
||||||
|
isSent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 响应速度 & 对话发起
|
// 响应速度 & 对话发起
|
||||||
if (!conversationStarts.has(sessionId)) {
|
if (!conversationStarts.has(sessionId)) {
|
||||||
conversationStarts.set(sessionId, { initiated: 0, received: 0 })
|
conversationStarts.set(sessionId, { initiated: 0, received: 0 })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { join, dirname, basename, extname } from 'path'
|
import { join, dirname, basename, extname } from 'path'
|
||||||
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs'
|
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, watch } from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
@@ -7,7 +7,7 @@ import * as http from 'http'
|
|||||||
import * as fzstd from 'fzstd'
|
import * as fzstd from 'fzstd'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import Database from 'better-sqlite3'
|
import Database from 'better-sqlite3'
|
||||||
import { app } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { ConfigService } from './config'
|
import { ConfigService } from './config'
|
||||||
import { wcdbService } from './wcdbService'
|
import { wcdbService } from './wcdbService'
|
||||||
import { MessageCacheService } from './messageCacheService'
|
import { MessageCacheService } from './messageCacheService'
|
||||||
@@ -152,9 +152,9 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
if (suffixMatch) return suffixMatch[1]
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
|
|
||||||
return trimmed
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +186,9 @@ class ChatService {
|
|||||||
|
|
||||||
this.connected = true
|
this.connected = true
|
||||||
|
|
||||||
|
// 设置数据库监控
|
||||||
|
this.setupDbMonitor()
|
||||||
|
|
||||||
// 预热 listMediaDbs 缓存(后台异步执行,不阻塞连接)
|
// 预热 listMediaDbs 缓存(后台异步执行,不阻塞连接)
|
||||||
this.warmupMediaDbsCache()
|
this.warmupMediaDbsCache()
|
||||||
|
|
||||||
@@ -196,6 +199,24 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private monitorSetup = false
|
||||||
|
|
||||||
|
private setupDbMonitor() {
|
||||||
|
if (this.monitorSetup) return
|
||||||
|
this.monitorSetup = true
|
||||||
|
|
||||||
|
// 使用 C++ DLL 内部的文件监控 (ReadDirectoryChangesW)
|
||||||
|
// 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更
|
||||||
|
wcdbService.setMonitor((type, json) => {
|
||||||
|
// 广播给所有渲染进程窗口
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
win.webContents.send('wcdb-change', { type, json })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预热 media 数据库列表缓存(后台异步执行)
|
* 预热 media 数据库列表缓存(后台异步执行)
|
||||||
*/
|
*/
|
||||||
@@ -543,7 +564,7 @@ class ChatService {
|
|||||||
FROM contact
|
FROM contact
|
||||||
`
|
`
|
||||||
|
|
||||||
console.log('查询contact.db...')
|
|
||||||
const contactResult = await wcdbService.execQuery('contact', null, contactQuery)
|
const contactResult = await wcdbService.execQuery('contact', null, contactQuery)
|
||||||
|
|
||||||
if (!contactResult.success || !contactResult.rows) {
|
if (!contactResult.success || !contactResult.rows) {
|
||||||
@@ -551,13 +572,13 @@ class ChatService {
|
|||||||
return { success: false, error: contactResult.error || '查询联系人失败' }
|
return { success: false, error: contactResult.error || '查询联系人失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('查询到', contactResult.rows.length, '条联系人记录')
|
|
||||||
const rows = contactResult.rows as Record<string, any>[]
|
const rows = contactResult.rows as Record<string, any>[]
|
||||||
|
|
||||||
// 调试:显示前5条数据样本
|
// 调试:显示前5条数据样本
|
||||||
console.log('📋 前5条数据样本:')
|
|
||||||
rows.slice(0, 5).forEach((row, idx) => {
|
rows.slice(0, 5).forEach((row, idx) => {
|
||||||
console.log(` ${idx + 1}. username: ${row.username}, local_type: ${row.local_type}, remark: ${row.remark || '无'}, nick_name: ${row.nick_name || '无'}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 调试:统计local_type分布
|
// 调试:统计local_type分布
|
||||||
@@ -566,7 +587,7 @@ class ChatService {
|
|||||||
const lt = row.local_type || 0
|
const lt = row.local_type || 0
|
||||||
localTypeStats.set(lt, (localTypeStats.get(lt) || 0) + 1)
|
localTypeStats.set(lt, (localTypeStats.get(lt) || 0) + 1)
|
||||||
})
|
})
|
||||||
console.log('📊 local_type分布:', Object.fromEntries(localTypeStats))
|
|
||||||
|
|
||||||
// 获取会话表的最后联系时间用于排序
|
// 获取会话表的最后联系时间用于排序
|
||||||
const lastContactTimeMap = new Map<string, number>()
|
const lastContactTimeMap = new Map<string, number>()
|
||||||
@@ -642,13 +663,8 @@ class ChatService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('过滤后得到', contacts.length, '个有效联系人')
|
|
||||||
console.log('📊 按类型统计:', {
|
|
||||||
friends: contacts.filter(c => c.type === 'friend').length,
|
|
||||||
groups: contacts.filter(c => c.type === 'group').length,
|
|
||||||
officials: contacts.filter(c => c.type === 'official').length,
|
|
||||||
other: contacts.filter(c => c.type === 'other').length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按最近联系时间排序
|
// 按最近联系时间排序
|
||||||
contacts.sort((a, b) => {
|
contacts.sort((a, b) => {
|
||||||
@@ -665,7 +681,7 @@ class ChatService {
|
|||||||
// 移除临时的lastContactTime字段
|
// 移除临时的lastContactTime字段
|
||||||
const result = contacts.map(({ lastContactTime, ...rest }) => rest)
|
const result = contacts.map(({ lastContactTime, ...rest }) => rest)
|
||||||
|
|
||||||
console.log('返回', result.length, '个联系人')
|
|
||||||
return { success: true, contacts: result }
|
return { success: true, contacts: result }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 获取通讯录失败:', e)
|
console.error('ChatService: 获取通讯录失败:', e)
|
||||||
@@ -731,7 +747,7 @@ class ChatService {
|
|||||||
|
|
||||||
// 如果需要跳过消息(offset > 0),逐批获取但不返回
|
// 如果需要跳过消息(offset > 0),逐批获取但不返回
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
console.log(`[ChatService] 跳过消息: offset=${offset}`)
|
|
||||||
let skipped = 0
|
let skipped = 0
|
||||||
while (skipped < offset) {
|
while (skipped < offset) {
|
||||||
const skipBatch = await wcdbService.fetchMessageBatch(state.cursor)
|
const skipBatch = await wcdbService.fetchMessageBatch(state.cursor)
|
||||||
@@ -740,17 +756,17 @@ class ChatService {
|
|||||||
return { success: false, error: skipBatch.error || '跳过消息失败' }
|
return { success: false, error: skipBatch.error || '跳过消息失败' }
|
||||||
}
|
}
|
||||||
if (!skipBatch.rows || skipBatch.rows.length === 0) {
|
if (!skipBatch.rows || skipBatch.rows.length === 0) {
|
||||||
console.log('[ChatService] 跳过时没有更多消息')
|
|
||||||
return { success: true, messages: [], hasMore: false }
|
return { success: true, messages: [], hasMore: false }
|
||||||
}
|
}
|
||||||
skipped += skipBatch.rows.length
|
skipped += skipBatch.rows.length
|
||||||
state.fetched += skipBatch.rows.length
|
state.fetched += skipBatch.rows.length
|
||||||
if (!skipBatch.hasMore) {
|
if (!skipBatch.hasMore) {
|
||||||
console.log('[ChatService] 跳过时已到达末尾')
|
|
||||||
return { success: true, messages: [], hasMore: false }
|
return { success: true, messages: [], hasMore: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}`)
|
|
||||||
}
|
}
|
||||||
} else if (state && offset !== state.fetched) {
|
} else if (state && offset !== state.fetched) {
|
||||||
// offset 与 fetched 不匹配,说明状态不一致
|
// offset 与 fetched 不匹配,说明状态不一致
|
||||||
@@ -913,6 +929,40 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNewMessages(sessionId: string, minTime: number, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; error?: string }> {
|
||||||
|
try {
|
||||||
|
const connectResult = await this.ensureConnected()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await wcdbService.getNewMessages(sessionId, minTime, limit)
|
||||||
|
if (!res.success || !res.messages) {
|
||||||
|
return { success: false, error: res.error || '获取新消息失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 Message 对象
|
||||||
|
const messages = this.mapRowsToMessages(res.messages as Record<string, any>[])
|
||||||
|
const normalized = this.normalizeMessageOrder(messages)
|
||||||
|
|
||||||
|
// 并发检查并修复缺失 CDN URL 的表情包
|
||||||
|
const fixPromises: Promise<void>[] = []
|
||||||
|
for (const msg of normalized) {
|
||||||
|
if (msg.localType === 47 && !msg.emojiCdnUrl && msg.emojiMd5) {
|
||||||
|
fixPromises.push(this.fallbackEmoticon(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fixPromises.length > 0) {
|
||||||
|
await Promise.allSettled(fixPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, messages: normalized }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ChatService: 获取增量消息失败:', e)
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeMessageOrder(messages: Message[]): Message[] {
|
private normalizeMessageOrder(messages: Message[]): Message[] {
|
||||||
if (messages.length < 2) return messages
|
if (messages.length < 2) return messages
|
||||||
const first = messages[0]
|
const first = messages[0]
|
||||||
@@ -1019,13 +1069,19 @@ class ChatService {
|
|||||||
|
|
||||||
if (senderUsername && (myWxidLower || cleanedWxidLower)) {
|
if (senderUsername && (myWxidLower || cleanedWxidLower)) {
|
||||||
const senderLower = String(senderUsername).toLowerCase()
|
const senderLower = String(senderUsername).toLowerCase()
|
||||||
const expectedIsSend = (senderLower === myWxidLower || senderLower === cleanedWxidLower) ? 1 : 0
|
const expectedIsSend = (
|
||||||
|
senderLower === myWxidLower ||
|
||||||
|
senderLower === cleanedWxidLower ||
|
||||||
|
// 兼容非 wxid 开头的账号(如果文件夹名带后缀,如 custom_backup,而 sender 是 custom)
|
||||||
|
(myWxidLower && myWxidLower.startsWith(senderLower + '_')) ||
|
||||||
|
(cleanedWxidLower && cleanedWxidLower.startsWith(senderLower + '_'))
|
||||||
|
) ? 1 : 0
|
||||||
if (isSend === null) {
|
if (isSend === null) {
|
||||||
isSend = expectedIsSend
|
isSend = expectedIsSend
|
||||||
// [DEBUG] Issue #34: 记录 isSend 推断过程
|
// [DEBUG] Issue #34: 记录 isSend 推断过程
|
||||||
if (expectedIsSend === 0 && localType === 1) {
|
if (expectedIsSend === 0 && localType === 1) {
|
||||||
// 仅在被判为接收且是文本消息时记录,避免刷屏
|
// 仅在被判为接收且是文本消息时记录,避免刷屏
|
||||||
// console.log(`[ChatService] inferred isSend=0: sender=${senderUsername}, myWxid=${myWxid} (cleaned=${cleanedWxid})`)
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (senderUsername && !myWxid) {
|
} else if (senderUsername && !myWxid) {
|
||||||
@@ -1249,7 +1305,7 @@ class ChatService {
|
|||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有 title,根据 type 返回默认标签
|
// 如果没有 title,根据 type 返回默认标签
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case '6':
|
case '6':
|
||||||
@@ -1607,10 +1663,10 @@ class ChatService {
|
|||||||
// 文件消息
|
// 文件消息
|
||||||
result.fileName = title || this.extractXmlValue(content, 'filename')
|
result.fileName = title || this.extractXmlValue(content, 'filename')
|
||||||
result.linkTitle = result.fileName
|
result.linkTitle = result.fileName
|
||||||
|
|
||||||
// 提取文件大小
|
// 提取文件大小
|
||||||
const fileSizeStr = this.extractXmlValue(content, 'totallen') ||
|
const fileSizeStr = this.extractXmlValue(content, 'totallen') ||
|
||||||
this.extractXmlValue(content, 'filesize')
|
this.extractXmlValue(content, 'filesize')
|
||||||
if (fileSizeStr) {
|
if (fileSizeStr) {
|
||||||
const size = parseInt(fileSizeStr, 10)
|
const size = parseInt(fileSizeStr, 10)
|
||||||
if (!isNaN(size)) {
|
if (!isNaN(size)) {
|
||||||
@@ -1635,7 +1691,7 @@ class ChatService {
|
|||||||
case '19': {
|
case '19': {
|
||||||
// 聊天记录
|
// 聊天记录
|
||||||
result.chatRecordTitle = title || '聊天记录'
|
result.chatRecordTitle = title || '聊天记录'
|
||||||
|
|
||||||
// 解析聊天记录列表
|
// 解析聊天记录列表
|
||||||
const recordList: Array<{
|
const recordList: Array<{
|
||||||
datatype: number
|
datatype: number
|
||||||
@@ -1648,10 +1704,10 @@ class ChatService {
|
|||||||
// 查找所有 <recorditem> 标签
|
// 查找所有 <recorditem> 标签
|
||||||
const recordItemRegex = /<recorditem>([\s\S]*?)<\/recorditem>/gi
|
const recordItemRegex = /<recorditem>([\s\S]*?)<\/recorditem>/gi
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
|
|
||||||
while ((match = recordItemRegex.exec(content)) !== null) {
|
while ((match = recordItemRegex.exec(content)) !== null) {
|
||||||
const itemXml = match[1]
|
const itemXml = match[1]
|
||||||
|
|
||||||
const datatypeStr = this.extractXmlValue(itemXml, 'datatype')
|
const datatypeStr = this.extractXmlValue(itemXml, 'datatype')
|
||||||
const sourcename = this.extractXmlValue(itemXml, 'sourcename')
|
const sourcename = this.extractXmlValue(itemXml, 'sourcename')
|
||||||
const sourcetime = this.extractXmlValue(itemXml, 'sourcetime')
|
const sourcetime = this.extractXmlValue(itemXml, 'sourcetime')
|
||||||
@@ -1680,10 +1736,10 @@ class ChatService {
|
|||||||
// 小程序
|
// 小程序
|
||||||
result.linkTitle = title
|
result.linkTitle = title
|
||||||
result.linkUrl = url
|
result.linkUrl = url
|
||||||
|
|
||||||
// 提取缩略图
|
// 提取缩略图
|
||||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||||
this.extractXmlValue(content, 'cdnthumburl')
|
this.extractXmlValue(content, 'cdnthumburl')
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
result.linkThumb = thumbUrl
|
result.linkThumb = thumbUrl
|
||||||
}
|
}
|
||||||
@@ -1693,11 +1749,11 @@ class ChatService {
|
|||||||
case '2000': {
|
case '2000': {
|
||||||
// 转账
|
// 转账
|
||||||
result.linkTitle = title || '[转账]'
|
result.linkTitle = title || '[转账]'
|
||||||
|
|
||||||
// 可以提取转账金额等信息
|
// 可以提取转账金额等信息
|
||||||
const payMemo = this.extractXmlValue(content, 'pay_memo')
|
const payMemo = this.extractXmlValue(content, 'pay_memo')
|
||||||
const feedesc = this.extractXmlValue(content, 'feedesc')
|
const feedesc = this.extractXmlValue(content, 'feedesc')
|
||||||
|
|
||||||
if (payMemo) {
|
if (payMemo) {
|
||||||
result.linkTitle = payMemo
|
result.linkTitle = payMemo
|
||||||
} else if (feedesc) {
|
} else if (feedesc) {
|
||||||
@@ -1710,9 +1766,9 @@ class ChatService {
|
|||||||
// 其他类型,提取通用字段
|
// 其他类型,提取通用字段
|
||||||
result.linkTitle = title
|
result.linkTitle = title
|
||||||
result.linkUrl = url
|
result.linkUrl = url
|
||||||
|
|
||||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||||
this.extractXmlValue(content, 'cdnthumburl')
|
this.extractXmlValue(content, 'cdnthumburl')
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
result.linkThumb = thumbUrl
|
result.linkThumb = thumbUrl
|
||||||
}
|
}
|
||||||
@@ -2132,7 +2188,7 @@ class ChatService {
|
|||||||
private decodeMaybeCompressed(raw: any, fieldName: string = 'unknown'): string {
|
private decodeMaybeCompressed(raw: any, fieldName: string = 'unknown'): string {
|
||||||
if (!raw) return ''
|
if (!raw) return ''
|
||||||
|
|
||||||
// console.log(`[ChatService] Decoding ${fieldName}: type=${typeof raw}`, raw)
|
//
|
||||||
|
|
||||||
// 如果是 Buffer/Uint8Array
|
// 如果是 Buffer/Uint8Array
|
||||||
if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
|
if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
|
||||||
@@ -2148,7 +2204,7 @@ class ChatService {
|
|||||||
const bytes = Buffer.from(raw, 'hex')
|
const bytes = Buffer.from(raw, 'hex')
|
||||||
if (bytes.length > 0) {
|
if (bytes.length > 0) {
|
||||||
const result = this.decodeBinaryContent(bytes, raw)
|
const result = this.decodeBinaryContent(bytes, raw)
|
||||||
// console.log(`[ChatService] HEX decoded result: ${result}`)
|
//
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2200,7 +2256,7 @@ class ChatService {
|
|||||||
|
|
||||||
// 如果提供了 fallbackValue,且解码结果看起来像二进制垃圾,则返回 fallbackValue
|
// 如果提供了 fallbackValue,且解码结果看起来像二进制垃圾,则返回 fallbackValue
|
||||||
if (fallbackValue && replacementCount > 0) {
|
if (fallbackValue && replacementCount > 0) {
|
||||||
// console.log(`[ChatService] Binary garbage detected, using fallback: ${fallbackValue}`)
|
//
|
||||||
return fallbackValue
|
return fallbackValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2794,7 +2850,7 @@ class ChatService {
|
|||||||
const t1 = Date.now()
|
const t1 = Date.now()
|
||||||
const msgResult = await this.getMessageByLocalId(sessionId, localId)
|
const msgResult = await this.getMessageByLocalId(sessionId, localId)
|
||||||
const t2 = Date.now()
|
const t2 = Date.now()
|
||||||
console.log(`[Voice] getMessageByLocalId: ${t2 - t1}ms`)
|
|
||||||
|
|
||||||
if (msgResult.success && msgResult.message) {
|
if (msgResult.success && msgResult.message) {
|
||||||
const msg = msgResult.message as any
|
const msg = msgResult.message as any
|
||||||
@@ -2813,7 +2869,7 @@ class ChatService {
|
|||||||
// 检查 WAV 内存缓存
|
// 检查 WAV 内存缓存
|
||||||
const wavCache = this.voiceWavCache.get(cacheKey)
|
const wavCache = this.voiceWavCache.get(cacheKey)
|
||||||
if (wavCache) {
|
if (wavCache) {
|
||||||
console.log(`[Voice] 内存缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return { success: true, data: wavCache.toString('base64') }
|
return { success: true, data: wavCache.toString('base64') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2825,7 +2881,7 @@ class ChatService {
|
|||||||
const wavData = readFileSync(wavFilePath)
|
const wavData = readFileSync(wavFilePath)
|
||||||
// 同时缓存到内存
|
// 同时缓存到内存
|
||||||
this.cacheVoiceWav(cacheKey, wavData)
|
this.cacheVoiceWav(cacheKey, wavData)
|
||||||
console.log(`[Voice] 文件缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return { success: true, data: wavData.toString('base64') }
|
return { success: true, data: wavData.toString('base64') }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Voice] 读取缓存文件失败:', e)
|
console.error('[Voice] 读取缓存文件失败:', e)
|
||||||
@@ -2855,7 +2911,7 @@ class ChatService {
|
|||||||
// 从数据库读取 silk 数据
|
// 从数据库读取 silk 数据
|
||||||
const silkData = await this.getVoiceDataFromMediaDb(msgCreateTime, candidates)
|
const silkData = await this.getVoiceDataFromMediaDb(msgCreateTime, candidates)
|
||||||
const t4 = Date.now()
|
const t4 = Date.now()
|
||||||
console.log(`[Voice] getVoiceDataFromMediaDb: ${t4 - t3}ms`)
|
|
||||||
|
|
||||||
if (!silkData) {
|
if (!silkData) {
|
||||||
return { success: false, error: '未找到语音数据 (请确保已在微信中播放过该语音)' }
|
return { success: false, error: '未找到语音数据 (请确保已在微信中播放过该语音)' }
|
||||||
@@ -2865,7 +2921,7 @@ class ChatService {
|
|||||||
// 使用 silk-wasm 解码
|
// 使用 silk-wasm 解码
|
||||||
const pcmData = await this.decodeSilkToPcm(silkData, 24000)
|
const pcmData = await this.decodeSilkToPcm(silkData, 24000)
|
||||||
const t6 = Date.now()
|
const t6 = Date.now()
|
||||||
console.log(`[Voice] decodeSilkToPcm: ${t6 - t5}ms`)
|
|
||||||
|
|
||||||
if (!pcmData) {
|
if (!pcmData) {
|
||||||
return { success: false, error: 'Silk 解码失败' }
|
return { success: false, error: 'Silk 解码失败' }
|
||||||
@@ -2875,7 +2931,7 @@ class ChatService {
|
|||||||
// PCM -> WAV
|
// PCM -> WAV
|
||||||
const wavData = this.createWavBuffer(pcmData, 24000)
|
const wavData = this.createWavBuffer(pcmData, 24000)
|
||||||
const t8 = Date.now()
|
const t8 = Date.now()
|
||||||
console.log(`[Voice] createWavBuffer: ${t8 - t7}ms`)
|
|
||||||
|
|
||||||
// 缓存 WAV 数据到内存
|
// 缓存 WAV 数据到内存
|
||||||
this.cacheVoiceWav(cacheKey, wavData)
|
this.cacheVoiceWav(cacheKey, wavData)
|
||||||
@@ -2883,7 +2939,7 @@ class ChatService {
|
|||||||
// 缓存 WAV 数据到文件(异步,不阻塞返回)
|
// 缓存 WAV 数据到文件(异步,不阻塞返回)
|
||||||
this.cacheVoiceWavToFile(cacheKey, wavData)
|
this.cacheVoiceWavToFile(cacheKey, wavData)
|
||||||
|
|
||||||
console.log(`[Voice] 总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return { success: true, data: wavData.toString('base64') }
|
return { success: true, data: wavData.toString('base64') }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: getVoiceData 失败:', e)
|
console.error('ChatService: getVoiceData 失败:', e)
|
||||||
@@ -2920,11 +2976,11 @@ class ChatService {
|
|||||||
let mediaDbFiles: string[]
|
let mediaDbFiles: string[]
|
||||||
if (this.mediaDbsCache) {
|
if (this.mediaDbsCache) {
|
||||||
mediaDbFiles = this.mediaDbsCache
|
mediaDbFiles = this.mediaDbsCache
|
||||||
console.log(`[Voice] listMediaDbs (缓存): 0ms`)
|
|
||||||
} else {
|
} else {
|
||||||
const mediaDbsResult = await wcdbService.listMediaDbs()
|
const mediaDbsResult = await wcdbService.listMediaDbs()
|
||||||
const t2 = Date.now()
|
const t2 = Date.now()
|
||||||
console.log(`[Voice] listMediaDbs: ${t2 - t1}ms`)
|
|
||||||
|
|
||||||
let files = mediaDbsResult.success && mediaDbsResult.data ? (mediaDbsResult.data as string[]) : []
|
let files = mediaDbsResult.success && mediaDbsResult.data ? (mediaDbsResult.data as string[]) : []
|
||||||
|
|
||||||
@@ -2956,7 +3012,7 @@ class ChatService {
|
|||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'VoiceInfo%'"
|
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'VoiceInfo%'"
|
||||||
)
|
)
|
||||||
const t4 = Date.now()
|
const t4 = Date.now()
|
||||||
console.log(`[Voice] 查询VoiceInfo表: ${t4 - t3}ms`)
|
|
||||||
|
|
||||||
if (!tablesResult.success || !tablesResult.rows || tablesResult.rows.length === 0) {
|
if (!tablesResult.success || !tablesResult.rows || tablesResult.rows.length === 0) {
|
||||||
continue
|
continue
|
||||||
@@ -2969,7 +3025,7 @@ class ChatService {
|
|||||||
`PRAGMA table_info('${voiceTable}')`
|
`PRAGMA table_info('${voiceTable}')`
|
||||||
)
|
)
|
||||||
const t6 = Date.now()
|
const t6 = Date.now()
|
||||||
console.log(`[Voice] 查询表结构: ${t6 - t5}ms`)
|
|
||||||
|
|
||||||
if (!columnsResult.success || !columnsResult.rows) {
|
if (!columnsResult.success || !columnsResult.rows) {
|
||||||
continue
|
continue
|
||||||
@@ -3006,7 +3062,7 @@ class ChatService {
|
|||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Name2Id%'"
|
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Name2Id%'"
|
||||||
)
|
)
|
||||||
const t8 = Date.now()
|
const t8 = Date.now()
|
||||||
console.log(`[Voice] 查询Name2Id表: ${t8 - t7}ms`)
|
|
||||||
|
|
||||||
const name2IdTable = (name2IdTablesResult.success && name2IdTablesResult.rows && name2IdTablesResult.rows.length > 0)
|
const name2IdTable = (name2IdTablesResult.success && name2IdTablesResult.rows && name2IdTablesResult.rows.length > 0)
|
||||||
? name2IdTablesResult.rows[0].name
|
? name2IdTablesResult.rows[0].name
|
||||||
@@ -3033,7 +3089,7 @@ class ChatService {
|
|||||||
`SELECT user_name, rowid FROM ${schema.name2IdTable} WHERE user_name IN (${candidatesStr})`
|
`SELECT user_name, rowid FROM ${schema.name2IdTable} WHERE user_name IN (${candidatesStr})`
|
||||||
)
|
)
|
||||||
const t10 = Date.now()
|
const t10 = Date.now()
|
||||||
console.log(`[Voice] 查询chat_name_id: ${t10 - t9}ms`)
|
|
||||||
|
|
||||||
if (name2IdResult.success && name2IdResult.rows && name2IdResult.rows.length > 0) {
|
if (name2IdResult.success && name2IdResult.rows && name2IdResult.rows.length > 0) {
|
||||||
// 构建 chat_name_id 列表
|
// 构建 chat_name_id 列表
|
||||||
@@ -3046,13 +3102,13 @@ class ChatService {
|
|||||||
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.chatNameIdColumn} IN (${chatNameIdsStr}) AND ${schema.timeColumn} = ${createTime} LIMIT 1`
|
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.chatNameIdColumn} IN (${chatNameIdsStr}) AND ${schema.timeColumn} = ${createTime} LIMIT 1`
|
||||||
)
|
)
|
||||||
const t12 = Date.now()
|
const t12 = Date.now()
|
||||||
console.log(`[Voice] 策略1查询语音: ${t12 - t11}ms`)
|
|
||||||
|
|
||||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||||
const row = voiceResult.rows[0]
|
const row = voiceResult.rows[0]
|
||||||
const silkData = this.decodeVoiceBlob(row.data)
|
const silkData = this.decodeVoiceBlob(row.data)
|
||||||
if (silkData) {
|
if (silkData) {
|
||||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return silkData
|
return silkData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3066,13 +3122,13 @@ class ChatService {
|
|||||||
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} = ${createTime} LIMIT 1`
|
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} = ${createTime} LIMIT 1`
|
||||||
)
|
)
|
||||||
const t14 = Date.now()
|
const t14 = Date.now()
|
||||||
console.log(`[Voice] 策略2查询语音: ${t14 - t13}ms`)
|
|
||||||
|
|
||||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||||
const row = voiceResult.rows[0]
|
const row = voiceResult.rows[0]
|
||||||
const silkData = this.decodeVoiceBlob(row.data)
|
const silkData = this.decodeVoiceBlob(row.data)
|
||||||
if (silkData) {
|
if (silkData) {
|
||||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return silkData
|
return silkData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3085,13 +3141,13 @@ class ChatService {
|
|||||||
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} BETWEEN ${createTime - 5} AND ${createTime + 5} ORDER BY ABS(${schema.timeColumn} - ${createTime}) LIMIT 1`
|
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} BETWEEN ${createTime - 5} AND ${createTime + 5} ORDER BY ABS(${schema.timeColumn} - ${createTime}) LIMIT 1`
|
||||||
)
|
)
|
||||||
const t16 = Date.now()
|
const t16 = Date.now()
|
||||||
console.log(`[Voice] 策略3查询语音: ${t16 - t15}ms`)
|
|
||||||
|
|
||||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||||
const row = voiceResult.rows[0]
|
const row = voiceResult.rows[0]
|
||||||
const silkData = this.decodeVoiceBlob(row.data)
|
const silkData = this.decodeVoiceBlob(row.data)
|
||||||
if (silkData) {
|
if (silkData) {
|
||||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return silkData
|
return silkData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3322,7 +3378,7 @@ class ChatService {
|
|||||||
senderWxid?: string
|
senderWxid?: string
|
||||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
console.log(`[Transcribe] 开始转写: sessionId=${sessionId}, msgId=${msgId}, createTime=${createTime}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let msgCreateTime = createTime
|
let msgCreateTime = createTime
|
||||||
@@ -3333,12 +3389,12 @@ class ChatService {
|
|||||||
const t1 = Date.now()
|
const t1 = Date.now()
|
||||||
const msgResult = await this.getMessageById(sessionId, parseInt(msgId, 10))
|
const msgResult = await this.getMessageById(sessionId, parseInt(msgId, 10))
|
||||||
const t2 = Date.now()
|
const t2 = Date.now()
|
||||||
console.log(`[Transcribe] getMessageById: ${t2 - t1}ms`)
|
|
||||||
|
|
||||||
if (msgResult.success && msgResult.message) {
|
if (msgResult.success && msgResult.message) {
|
||||||
msgCreateTime = msgResult.message.createTime
|
msgCreateTime = msgResult.message.createTime
|
||||||
serverId = msgResult.message.serverId
|
serverId = msgResult.message.serverId
|
||||||
console.log(`[Transcribe] 获取到 createTime=${msgCreateTime}, serverId=${serverId}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3349,19 +3405,19 @@ class ChatService {
|
|||||||
|
|
||||||
// 使用正确的 cacheKey(包含 createTime)
|
// 使用正确的 cacheKey(包含 createTime)
|
||||||
const cacheKey = this.getVoiceCacheKey(sessionId, msgId, msgCreateTime)
|
const cacheKey = this.getVoiceCacheKey(sessionId, msgId, msgCreateTime)
|
||||||
console.log(`[Transcribe] cacheKey=${cacheKey}`)
|
|
||||||
|
|
||||||
// 检查转写缓存
|
// 检查转写缓存
|
||||||
const cached = this.voiceTranscriptCache.get(cacheKey)
|
const cached = this.voiceTranscriptCache.get(cacheKey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`[Transcribe] 缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return { success: true, transcript: cached }
|
return { success: true, transcript: cached }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否正在转写
|
// 检查是否正在转写
|
||||||
const pending = this.voiceTranscriptPending.get(cacheKey)
|
const pending = this.voiceTranscriptPending.get(cacheKey)
|
||||||
if (pending) {
|
if (pending) {
|
||||||
console.log(`[Transcribe] 正在转写中,等待结果`)
|
|
||||||
return pending
|
return pending
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3370,7 +3426,7 @@ class ChatService {
|
|||||||
// 检查内存中是否有 WAV 数据
|
// 检查内存中是否有 WAV 数据
|
||||||
let wavData = this.voiceWavCache.get(cacheKey)
|
let wavData = this.voiceWavCache.get(cacheKey)
|
||||||
if (wavData) {
|
if (wavData) {
|
||||||
console.log(`[Transcribe] WAV内存缓存命中,大小: ${wavData.length} bytes`)
|
|
||||||
} else {
|
} else {
|
||||||
// 检查文件缓存
|
// 检查文件缓存
|
||||||
const voiceCacheDir = this.getVoiceCacheDir()
|
const voiceCacheDir = this.getVoiceCacheDir()
|
||||||
@@ -3378,7 +3434,7 @@ class ChatService {
|
|||||||
if (existsSync(wavFilePath)) {
|
if (existsSync(wavFilePath)) {
|
||||||
try {
|
try {
|
||||||
wavData = readFileSync(wavFilePath)
|
wavData = readFileSync(wavFilePath)
|
||||||
console.log(`[Transcribe] WAV文件缓存命中,大小: ${wavData.length} bytes`)
|
|
||||||
// 同时缓存到内存
|
// 同时缓存到内存
|
||||||
this.cacheVoiceWav(cacheKey, wavData)
|
this.cacheVoiceWav(cacheKey, wavData)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -3388,39 +3444,39 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!wavData) {
|
if (!wavData) {
|
||||||
console.log(`[Transcribe] WAV缓存未命中,调用 getVoiceData`)
|
|
||||||
const t3 = Date.now()
|
const t3 = Date.now()
|
||||||
// 调用 getVoiceData 获取并解码
|
// 调用 getVoiceData 获取并解码
|
||||||
const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId, senderWxid)
|
const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId, senderWxid)
|
||||||
const t4 = Date.now()
|
const t4 = Date.now()
|
||||||
console.log(`[Transcribe] getVoiceData: ${t4 - t3}ms, success=${voiceResult.success}`)
|
|
||||||
|
|
||||||
if (!voiceResult.success || !voiceResult.data) {
|
if (!voiceResult.success || !voiceResult.data) {
|
||||||
console.error(`[Transcribe] 语音解码失败: ${voiceResult.error}`)
|
console.error(`[Transcribe] 语音解码失败: ${voiceResult.error}`)
|
||||||
return { success: false, error: voiceResult.error || '语音解码失败' }
|
return { success: false, error: voiceResult.error || '语音解码失败' }
|
||||||
}
|
}
|
||||||
wavData = Buffer.from(voiceResult.data, 'base64')
|
wavData = Buffer.from(voiceResult.data, 'base64')
|
||||||
console.log(`[Transcribe] WAV数据大小: ${wavData.length} bytes`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转写
|
// 转写
|
||||||
console.log(`[Transcribe] 开始调用 transcribeWavBuffer`)
|
|
||||||
const t5 = Date.now()
|
const t5 = Date.now()
|
||||||
const result = await voiceTranscribeService.transcribeWavBuffer(wavData, (text) => {
|
const result = await voiceTranscribeService.transcribeWavBuffer(wavData, (text) => {
|
||||||
console.log(`[Transcribe] 部分结果: ${text}`)
|
|
||||||
onPartial?.(text)
|
onPartial?.(text)
|
||||||
})
|
})
|
||||||
const t6 = Date.now()
|
const t6 = Date.now()
|
||||||
console.log(`[Transcribe] transcribeWavBuffer: ${t6 - t5}ms, success=${result.success}`)
|
|
||||||
|
|
||||||
if (result.success && result.transcript) {
|
if (result.success && result.transcript) {
|
||||||
console.log(`[Transcribe] 转写成功: ${result.transcript}`)
|
|
||||||
this.cacheVoiceTranscript(cacheKey, result.transcript)
|
this.cacheVoiceTranscript(cacheKey, result.transcript)
|
||||||
} else {
|
} else {
|
||||||
console.error(`[Transcribe] 转写失败: ${result.error}`)
|
console.error(`[Transcribe] 转写失败: ${result.error}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Transcribe] 总耗时: ${Date.now() - startTime}ms`)
|
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Transcribe] 异常:`, error)
|
console.error(`[Transcribe] 异常:`, error)
|
||||||
@@ -3468,7 +3524,7 @@ class ChatService {
|
|||||||
try {
|
try {
|
||||||
// 1. 尝试从缓存获取会话表信息
|
// 1. 尝试从缓存获取会话表信息
|
||||||
let tables = this.sessionTablesCache.get(sessionId)
|
let tables = this.sessionTablesCache.get(sessionId)
|
||||||
|
|
||||||
if (!tables) {
|
if (!tables) {
|
||||||
// 缓存未命中,查询数据库
|
// 缓存未命中,查询数据库
|
||||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
||||||
|
|||||||
@@ -74,8 +74,9 @@ class DualReportService {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
if (suffixMatch) return suffixMatch[1]
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
return trimmed
|
|
||||||
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureConnectedWithConfig(
|
private async ensureConnectedWithConfig(
|
||||||
@@ -202,7 +203,12 @@ class DualReportService {
|
|||||||
if (!sender) return false
|
if (!sender) return false
|
||||||
const rawLower = rawWxid ? rawWxid.toLowerCase() : ''
|
const rawLower = rawWxid ? rawWxid.toLowerCase() : ''
|
||||||
const cleanedLower = cleanedWxid ? cleanedWxid.toLowerCase() : ''
|
const cleanedLower = cleanedWxid ? cleanedWxid.toLowerCase() : ''
|
||||||
return sender === rawLower || sender === cleanedLower
|
return !!(
|
||||||
|
sender === rawLower ||
|
||||||
|
sender === cleanedLower ||
|
||||||
|
(rawLower && rawLower.startsWith(sender + '_')) ||
|
||||||
|
(cleanedLower && cleanedLower.startsWith(sender + '_'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFirstMessages(
|
private async getFirstMessages(
|
||||||
|
|||||||
@@ -157,8 +157,9 @@ class ExportService {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
if (suffixMatch) return suffixMatch[1]
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
return trimmed
|
|
||||||
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
||||||
@@ -968,11 +969,11 @@ class ExportService {
|
|||||||
const emojiMd5 = msg.emojiMd5
|
const emojiMd5 = msg.emojiMd5
|
||||||
|
|
||||||
if (!emojiUrl && !emojiMd5) {
|
if (!emojiUrl && !emojiMd5) {
|
||||||
console.log('[ExportService] 表情消息缺少 url 和 md5, localId:', msg.localId, 'content:', msg.content?.substring(0, 200))
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ExportService] 导出表情:', { localId: msg.localId, emojiMd5, emojiUrl: emojiUrl?.substring(0, 100) })
|
|
||||||
|
|
||||||
const key = emojiMd5 || String(msg.localId)
|
const key = emojiMd5 || String(msg.localId)
|
||||||
// 根据 URL 判断扩展名
|
// 根据 URL 判断扩展名
|
||||||
|
|||||||
@@ -79,8 +79,13 @@ class GroupAnalyticsService {
|
|||||||
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
||||||
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
||||||
if (match) return match[1]
|
if (match) return match[1]
|
||||||
|
return trimmed
|
||||||
}
|
}
|
||||||
return trimmed
|
|
||||||
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
|
|
||||||
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureConnected(): Promise<{ success: boolean; error?: string }> {
|
private async ensureConnected(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
|||||||
@@ -380,9 +380,9 @@ export class ImageDecryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||||
if (suffixMatch) return suffixMatch[1]
|
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||||
|
|
||||||
return trimmed
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveDatPath(
|
private async resolveDatPath(
|
||||||
@@ -1136,7 +1136,7 @@ export class ImageDecryptService {
|
|||||||
// 扫描所有可能的缓存根目录
|
// 扫描所有可能的缓存根目录
|
||||||
const allRoots = this.getAllCacheRoots()
|
const allRoots = this.getAllCacheRoots()
|
||||||
this.logInfo('开始索引缓存', { roots: allRoots.length })
|
this.logInfo('开始索引缓存', { roots: allRoots.length })
|
||||||
|
|
||||||
for (const root of allRoots) {
|
for (const root of allRoots) {
|
||||||
try {
|
try {
|
||||||
this.indexCacheDir(root, 3, 0) // 增加深度到3,支持 sessionId/YYYY-MM 结构
|
this.indexCacheDir(root, 3, 0) // 增加深度到3,支持 sessionId/YYYY-MM 结构
|
||||||
@@ -1144,7 +1144,7 @@ export class ImageDecryptService {
|
|||||||
this.logError('索引目录失败', e, { root })
|
this.logError('索引目录失败', e, { root })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logInfo('缓存索引完成', { entries: this.resolvedCache.size })
|
this.logInfo('缓存索引完成', { entries: this.resolvedCache.size })
|
||||||
this.cacheIndexed = true
|
this.cacheIndexed = true
|
||||||
this.cacheIndexing = null
|
this.cacheIndexing = null
|
||||||
@@ -1175,7 +1175,7 @@ export class ImageDecryptService {
|
|||||||
// 默认路径
|
// 默认路径
|
||||||
roots.push(join(documentsPath, 'WeFlow', 'Images'))
|
roots.push(join(documentsPath, 'WeFlow', 'Images'))
|
||||||
roots.push(join(documentsPath, 'WeFlow', 'images'))
|
roots.push(join(documentsPath, 'WeFlow', 'images'))
|
||||||
|
|
||||||
// 兼容旧路径(如果有的话)
|
// 兼容旧路径(如果有的话)
|
||||||
roots.push(join(documentsPath, 'WeFlowData', 'Images'))
|
roots.push(join(documentsPath, 'WeFlowData', 'Images'))
|
||||||
|
|
||||||
|
|||||||
@@ -116,13 +116,13 @@ export class KeyService {
|
|||||||
|
|
||||||
// 检查是否已经有本地副本,如果有就使用它
|
// 检查是否已经有本地副本,如果有就使用它
|
||||||
if (existsSync(localPath)) {
|
if (existsSync(localPath)) {
|
||||||
console.log(`使用已存在的 DLL 本地副本: ${localPath}`)
|
|
||||||
return localPath
|
return localPath
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`检测到网络路径 DLL,正在复制到本地: ${originalPath} -> ${localPath}`)
|
|
||||||
copyFileSync(originalPath, localPath)
|
copyFileSync(originalPath, localPath)
|
||||||
console.log('DLL 本地化成功')
|
|
||||||
return localPath
|
return localPath
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('DLL 本地化失败:', e)
|
console.error('DLL 本地化失败:', e)
|
||||||
@@ -146,7 +146,7 @@ export class KeyService {
|
|||||||
|
|
||||||
// 检查是否为网络路径,如果是则本地化
|
// 检查是否为网络路径,如果是则本地化
|
||||||
if (this.isNetworkPath(dllPath)) {
|
if (this.isNetworkPath(dllPath)) {
|
||||||
console.log('检测到网络路径,将进行本地化处理')
|
|
||||||
dllPath = this.localizeNetworkDll(dllPath)
|
dllPath = this.localizeNetworkDll(dllPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ export class KeyService {
|
|||||||
if (pid) {
|
if (pid) {
|
||||||
const runPath = await this.getProcessExecutablePath(pid)
|
const runPath = await this.getProcessExecutablePath(pid)
|
||||||
if (runPath && existsSync(runPath)) {
|
if (runPath && existsSync(runPath)) {
|
||||||
console.log('发现正在运行的微信进程,使用路径:', runPath)
|
|
||||||
return runPath
|
return runPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,15 +57,11 @@ class SnsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTimeline(limit: number = 20, offset: number = 0, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: SnsPost[]; error?: string }> {
|
async getTimeline(limit: number = 20, offset: number = 0, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: SnsPost[]; error?: string }> {
|
||||||
console.log('[SnsService] getTimeline called with:', { limit, offset, usernames, keyword, startTime, endTime })
|
|
||||||
|
|
||||||
const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime)
|
const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime)
|
||||||
|
|
||||||
console.log('[SnsService] getSnsTimeline result:', {
|
|
||||||
success: result.success,
|
|
||||||
timelineCount: result.timeline?.length,
|
|
||||||
error: result.error
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.success && result.timeline) {
|
if (result.success && result.timeline) {
|
||||||
const enrichedTimeline = result.timeline.map((post: any, index: number) => {
|
const enrichedTimeline = result.timeline.map((post: any, index: number) => {
|
||||||
@@ -121,11 +117,11 @@ class SnsService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('[SnsService] Returning enriched timeline with', enrichedTimeline.length, 'posts')
|
|
||||||
return { ...result, timeline: enrichedTimeline }
|
return { ...result, timeline: enrichedTimeline }
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[SnsService] Returning result:', result)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
async debugResource(url: string): Promise<{ success: boolean; status?: number; headers?: any; error?: string }> {
|
async debugResource(url: string): Promise<{ success: boolean; status?: number; headers?: any; error?: string }> {
|
||||||
|
|||||||
@@ -224,12 +224,12 @@ export class VoiceTranscribeService {
|
|||||||
let finalTranscript = ''
|
let finalTranscript = ''
|
||||||
|
|
||||||
worker.on('message', (msg: any) => {
|
worker.on('message', (msg: any) => {
|
||||||
console.log('[VoiceTranscribe] Worker 消息:', msg)
|
|
||||||
if (msg.type === 'partial') {
|
if (msg.type === 'partial') {
|
||||||
onPartial?.(msg.text)
|
onPartial?.(msg.text)
|
||||||
} else if (msg.type === 'final') {
|
} else if (msg.type === 'final') {
|
||||||
finalTranscript = msg.text
|
finalTranscript = msg.text
|
||||||
console.log('[VoiceTranscribe] 最终文本:', finalTranscript)
|
|
||||||
resolve({ success: true, transcript: finalTranscript })
|
resolve({ success: true, transcript: finalTranscript })
|
||||||
worker.terminate()
|
worker.terminate()
|
||||||
} else if (msg.type === 'error') {
|
} else if (msg.type === 'error') {
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export class WcdbCore {
|
|||||||
private wcdbGetSnsTimeline: any = null
|
private wcdbGetSnsTimeline: any = null
|
||||||
private wcdbGetSnsAnnualStats: any = null
|
private wcdbGetSnsAnnualStats: any = null
|
||||||
private wcdbVerifyUser: any = null
|
private wcdbVerifyUser: any = null
|
||||||
|
private wcdbStartMonitorPipe: any = null
|
||||||
|
private wcdbStopMonitorPipe: any = null
|
||||||
|
private monitorPipeClient: any = null
|
||||||
|
|
||||||
private avatarUrlCache: Map<string, { url?: string; updatedAt: number }> = new Map()
|
private avatarUrlCache: Map<string, { url?: string; updatedAt: number }> = new Map()
|
||||||
private readonly avatarCacheTtlMs = 10 * 60 * 1000
|
private readonly avatarCacheTtlMs = 10 * 60 * 1000
|
||||||
private logTimer: NodeJS.Timeout | null = null
|
private logTimer: NodeJS.Timeout | null = null
|
||||||
@@ -79,6 +83,136 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用命名管道 IPC
|
||||||
|
startMonitor(callback: (type: string, json: string) => void): boolean {
|
||||||
|
if (!this.wcdbStartMonitorPipe) {
|
||||||
|
this.writeLog('startMonitor: wcdbStartMonitorPipe not available')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = this.wcdbStartMonitorPipe()
|
||||||
|
if (result !== 0) {
|
||||||
|
this.writeLog(`startMonitor: wcdbStartMonitorPipe failed with ${result}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const net = require('net')
|
||||||
|
const PIPE_PATH = '\\\\.\\pipe\\weflow_monitor'
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.monitorPipeClient = net.createConnection(PIPE_PATH, () => {
|
||||||
|
this.writeLog('Monitor pipe connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
let buffer = ''
|
||||||
|
this.monitorPipeClient.on('data', (data: Buffer) => {
|
||||||
|
buffer += data.toString('utf8')
|
||||||
|
const lines = buffer.split('\n')
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line)
|
||||||
|
callback(parsed.action || 'update', line)
|
||||||
|
} catch {
|
||||||
|
callback('update', line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.monitorPipeClient.on('error', (err: Error) => {
|
||||||
|
this.writeLog(`Monitor pipe error: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.monitorPipeClient.on('close', () => {
|
||||||
|
this.writeLog('Monitor pipe closed')
|
||||||
|
this.monitorPipeClient = null
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
this.writeLog('Monitor started via named pipe IPC')
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('startMonitor failed:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopMonitor(): void {
|
||||||
|
if (this.monitorPipeClient) {
|
||||||
|
this.monitorPipeClient.destroy()
|
||||||
|
this.monitorPipeClient = null
|
||||||
|
}
|
||||||
|
if (this.wcdbStopMonitorPipe) {
|
||||||
|
this.wcdbStopMonitorPipe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留旧方法签名以兼容
|
||||||
|
setMonitor(callback: (type: string, json: string) => void): boolean {
|
||||||
|
return this.startMonitor(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定时间之后的新消息(增量更新)
|
||||||
|
*/
|
||||||
|
getNewMessages(sessionId: string, minTime: number, limit: number = 1000): { success: boolean; messages?: any[]; error?: string } {
|
||||||
|
if (!this.handle || !this.wcdbOpenMessageCursorLite || !this.wcdbFetchMessageBatch || !this.wcdbCloseMessageCursor) {
|
||||||
|
return { success: false, error: 'Database not handled or functions missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Open Cursor
|
||||||
|
const cursorPtr = Buffer.alloc(8) // int64*
|
||||||
|
// wcdb_open_message_cursor_lite(handle, sessionId, batchSize, ascending, beginTime, endTime, outCursor)
|
||||||
|
// ascending=1 (ASC) to get messages AFTER minTime ordered by time
|
||||||
|
// beginTime = minTime + 1 (to avoid duplicate of the last message)
|
||||||
|
// Actually, let's use minTime, user logic might handle duplication or we just pass strictly greater
|
||||||
|
// C++ logic: create_time >= beginTimestamp. So if we want new messages, passing lastTimestamp + 1 is safer.
|
||||||
|
const openRes = this.wcdbOpenMessageCursorLite(this.handle, sessionId, limit, 1, minTime, 0, cursorPtr)
|
||||||
|
|
||||||
|
if (openRes !== 0) {
|
||||||
|
return { success: false, error: `Open cursor failed: ${openRes}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read int64 from buffer
|
||||||
|
const cursor = cursorPtr.readBigInt64LE(0)
|
||||||
|
|
||||||
|
// 2. Fetch Batch
|
||||||
|
const outJsonPtr = Buffer.alloc(8) // void**
|
||||||
|
const outHasMorePtr = Buffer.alloc(4) // int32*
|
||||||
|
|
||||||
|
// fetch_message_batch(handle, cursor, outJson, outHasMore)
|
||||||
|
const fetchRes = this.wcdbFetchMessageBatch(this.handle, cursor, outJsonPtr, outHasMorePtr)
|
||||||
|
|
||||||
|
let messages: any[] = []
|
||||||
|
if (fetchRes === 0) {
|
||||||
|
const jsonPtr = outJsonPtr.readBigInt64LE(0) // void* address
|
||||||
|
if (jsonPtr !== 0n) {
|
||||||
|
// koffi decode string
|
||||||
|
const jsonStr = this.koffi.decode(jsonPtr, 'string')
|
||||||
|
this.wcdbFreeString(jsonPtr) // Must free
|
||||||
|
if (jsonStr) {
|
||||||
|
try {
|
||||||
|
messages = JSON.parse(jsonStr)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse messages failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Close Cursor
|
||||||
|
this.wcdbCloseMessageCursor(this.handle, cursor)
|
||||||
|
|
||||||
|
if (fetchRes !== 0) {
|
||||||
|
return { success: false, error: `Fetch batch failed: ${fetchRes}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, messages }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 DLL 路径
|
* 获取 DLL 路径
|
||||||
*/
|
*/
|
||||||
@@ -122,7 +256,7 @@ export class WcdbCore {
|
|||||||
if (!force && !this.isLogEnabled()) return
|
if (!force && !this.isLogEnabled()) return
|
||||||
const line = `[${new Date().toISOString()}] ${message}`
|
const line = `[${new Date().toISOString()}] ${message}`
|
||||||
// 同时输出到控制台和文件
|
// 同时输出到控制台和文件
|
||||||
console.log('[WCDB]', message)
|
|
||||||
try {
|
try {
|
||||||
const base = this.userDataPath || process.env.WCDB_LOG_DIR || process.cwd()
|
const base = this.userDataPath || process.env.WCDB_LOG_DIR || process.cwd()
|
||||||
const dir = join(base, 'logs')
|
const dir = join(base, 'logs')
|
||||||
@@ -262,10 +396,10 @@ export class WcdbCore {
|
|||||||
let protectionOk = false
|
let protectionOk = false
|
||||||
for (const resPath of resourcePaths) {
|
for (const resPath of resourcePaths) {
|
||||||
try {
|
try {
|
||||||
// console.log(`[WCDB] 尝试 InitProtection: ${resPath}`)
|
//
|
||||||
protectionOk = this.wcdbInitProtection(resPath)
|
protectionOk = this.wcdbInitProtection(resPath)
|
||||||
if (protectionOk) {
|
if (protectionOk) {
|
||||||
// console.log(`[WCDB] InitProtection 成功: ${resPath}`)
|
//
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -454,6 +588,17 @@ export class WcdbCore {
|
|||||||
this.wcdbGetSnsAnnualStats = null
|
this.wcdbGetSnsAnnualStats = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Named pipe IPC for monitoring (replaces callback)
|
||||||
|
try {
|
||||||
|
this.wcdbStartMonitorPipe = this.lib.func('int32 wcdb_start_monitor_pipe()')
|
||||||
|
this.wcdbStopMonitorPipe = this.lib.func('void wcdb_stop_monitor_pipe()')
|
||||||
|
this.writeLog('Monitor pipe functions loaded')
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load monitor pipe functions:', e)
|
||||||
|
this.wcdbStartMonitorPipe = null
|
||||||
|
this.wcdbStopMonitorPipe = null
|
||||||
|
}
|
||||||
|
|
||||||
// void VerifyUser(int64_t hwnd_ptr, const char* message, char* out_result, int max_len)
|
// void VerifyUser(int64_t hwnd_ptr, const char* message, char* out_result, int max_len)
|
||||||
try {
|
try {
|
||||||
this.wcdbVerifyUser = this.lib.func('void VerifyUser(int64 hwnd, const char* message, _Out_ char* outResult, int maxLen)')
|
this.wcdbVerifyUser = this.lib.func('void VerifyUser(int64 hwnd, const char* message, _Out_ char* outResult, int maxLen)')
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class WcdbService {
|
|||||||
private resourcesPath: string | null = null
|
private resourcesPath: string | null = null
|
||||||
private userDataPath: string | null = null
|
private userDataPath: string | null = null
|
||||||
private logEnabled = false
|
private logEnabled = false
|
||||||
|
private monitorListener: ((type: string, json: string) => void) | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initWorker()
|
this.initWorker()
|
||||||
@@ -47,8 +48,16 @@ export class WcdbService {
|
|||||||
try {
|
try {
|
||||||
this.worker = new Worker(finalPath)
|
this.worker = new Worker(finalPath)
|
||||||
|
|
||||||
this.worker.on('message', (msg: WorkerMessage) => {
|
this.worker.on('message', (msg: any) => {
|
||||||
const { id, result, error } = msg
|
const { id, result, error, type, payload } = msg
|
||||||
|
|
||||||
|
if (type === 'monitor') {
|
||||||
|
if (this.monitorListener) {
|
||||||
|
this.monitorListener(payload.type, payload.json)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const p = this.pending.get(id)
|
const p = this.pending.get(id)
|
||||||
if (p) {
|
if (p) {
|
||||||
this.pending.delete(id)
|
this.pending.delete(id)
|
||||||
@@ -122,6 +131,15 @@ export class WcdbService {
|
|||||||
this.callWorker('setLogEnabled', { enabled }).catch(() => { })
|
this.callWorker('setLogEnabled', { enabled }).catch(() => { })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置数据库监控回调
|
||||||
|
*/
|
||||||
|
setMonitor(callback: (type: string, json: string) => void): void {
|
||||||
|
this.monitorListener = callback;
|
||||||
|
// Notify worker to enable monitor
|
||||||
|
this.callWorker('setMonitor').catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查服务是否就绪
|
* 检查服务是否就绪
|
||||||
*/
|
*/
|
||||||
@@ -187,6 +205,13 @@ export class WcdbService {
|
|||||||
return this.callWorker('getMessages', { sessionId, limit, offset })
|
return this.callWorker('getMessages', { sessionId, limit, offset })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取新消息(增量刷新)
|
||||||
|
*/
|
||||||
|
async getNewMessages(sessionId: string, minTime: number, limit: number = 1000): Promise<{ success: boolean; messages?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('getNewMessages', { sessionId, minTime, limit })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取消息总数
|
* 获取消息总数
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -80,17 +80,17 @@ function isLanguageAllowed(result: any, allowedLanguages: string[]): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const langTag = result.lang
|
const langTag = result.lang
|
||||||
console.log('[TranscribeWorker] 检测到语言标记:', langTag)
|
|
||||||
|
|
||||||
// 检查是否在允许的语言列表中
|
// 检查是否在允许的语言列表中
|
||||||
for (const lang of allowedLanguages) {
|
for (const lang of allowedLanguages) {
|
||||||
if (LANGUAGE_TAGS[lang] === langTag) {
|
if (LANGUAGE_TAGS[lang] === langTag) {
|
||||||
console.log('[TranscribeWorker] 语言匹配,允许:', lang)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[TranscribeWorker] 语言不在白名单中,过滤掉')
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ async function run() {
|
|||||||
allowedLanguages = ['zh']
|
allowedLanguages = ['zh']
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[TranscribeWorker] 使用的语言白名单:', allowedLanguages)
|
|
||||||
|
|
||||||
// 1. 初始化识别器 (SenseVoiceSmall)
|
// 1. 初始化识别器 (SenseVoiceSmall)
|
||||||
const recognizerConfig = {
|
const recognizerConfig = {
|
||||||
@@ -145,15 +145,15 @@ async function run() {
|
|||||||
recognizer.decode(stream)
|
recognizer.decode(stream)
|
||||||
const result = recognizer.getResult(stream)
|
const result = recognizer.getResult(stream)
|
||||||
|
|
||||||
console.log('[TranscribeWorker] 识别完成 - 结果对象:', JSON.stringify(result, null, 2))
|
|
||||||
|
|
||||||
// 3. 检查语言是否在白名单中
|
// 3. 检查语言是否在白名单中
|
||||||
if (isLanguageAllowed(result, allowedLanguages)) {
|
if (isLanguageAllowed(result, allowedLanguages)) {
|
||||||
const processedText = richTranscribePostProcess(result.text)
|
const processedText = richTranscribePostProcess(result.text)
|
||||||
console.log('[TranscribeWorker] 语言匹配,返回文本:', processedText)
|
|
||||||
parentPort.postMessage({ type: 'final', text: processedText })
|
parentPort.postMessage({ type: 'final', text: processedText })
|
||||||
} else {
|
} else {
|
||||||
console.log('[TranscribeWorker] 语言不匹配,返回空文本')
|
|
||||||
parentPort.postMessage({ type: 'final', text: '' })
|
parentPort.postMessage({ type: 'final', text: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ if (parentPort) {
|
|||||||
core.setLogEnabled(payload.enabled)
|
core.setLogEnabled(payload.enabled)
|
||||||
result = { success: true }
|
result = { success: true }
|
||||||
break
|
break
|
||||||
|
case 'setMonitor':
|
||||||
|
core.setMonitor((type, json) => {
|
||||||
|
parentPort!.postMessage({
|
||||||
|
id: -1,
|
||||||
|
type: 'monitor',
|
||||||
|
payload: { type, json }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
result = { success: true }
|
||||||
|
break
|
||||||
case 'testConnection':
|
case 'testConnection':
|
||||||
result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid)
|
result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid)
|
||||||
break
|
break
|
||||||
@@ -38,6 +48,9 @@ if (parentPort) {
|
|||||||
case 'getMessages':
|
case 'getMessages':
|
||||||
result = await core.getMessages(payload.sessionId, payload.limit, payload.offset)
|
result = await core.getMessages(payload.sessionId, payload.limit, payload.offset)
|
||||||
break
|
break
|
||||||
|
case 'getNewMessages':
|
||||||
|
result = await core.getNewMessages(payload.sessionId, payload.minTime, payload.limit)
|
||||||
|
break
|
||||||
case 'getMessageCount':
|
case 'getMessageCount':
|
||||||
result = await core.getMessageCount(payload.sessionId)
|
result = await core.getMessageCount(payload.sessionId)
|
||||||
break
|
break
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
|
"react-virtuoso": "^4.18.1",
|
||||||
"sherpa-onnx-node": "^1.10.38",
|
"sherpa-onnx-node": "^1.10.38",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
"wechat-emojis": "^1.0.2",
|
"wechat-emojis": "^1.0.2",
|
||||||
@@ -7380,12 +7381,6 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nan": {
|
|
||||||
"version": "2.25.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/nan/-/nan-2.25.0.tgz",
|
|
||||||
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -8050,6 +8045,16 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-virtuoso": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-virtuoso/-/react-virtuoso-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16 || >=17 || >= 18 || >= 19",
|
||||||
|
"react-dom": ">=16 || >=17 || >= 18 || >=19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-binary-file-arch": {
|
"node_modules/read-binary-file-arch": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
|
"react-virtuoso": "^4.18.1",
|
||||||
"sherpa-onnx-node": "^1.10.38",
|
"sherpa-onnx-node": "^1.10.38",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
"wechat-emojis": "^1.0.2",
|
"wechat-emojis": "^1.0.2",
|
||||||
|
|||||||
Binary file not shown.
@@ -241,18 +241,18 @@ function App() {
|
|||||||
if (!onboardingDone) {
|
if (!onboardingDone) {
|
||||||
await configService.setOnboardingDone(true)
|
await configService.setOnboardingDone(true)
|
||||||
}
|
}
|
||||||
console.log('检测到已保存的配置,正在自动连接...')
|
|
||||||
const result = await window.electronAPI.chat.connect()
|
const result = await window.electronAPI.chat.connect()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('自动连接成功')
|
|
||||||
setDbConnected(true, dbPath)
|
setDbConnected(true, dbPath)
|
||||||
// 如果当前在欢迎页,跳转到首页
|
// 如果当前在欢迎页,跳转到首页
|
||||||
if (window.location.hash === '#/' || window.location.hash === '') {
|
if (window.location.hash === '#/' || window.location.hash === '') {
|
||||||
navigate('/home')
|
navigate('/home')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('自动连接失败:', result.error)
|
|
||||||
// 如果错误信息包含 VC++ 或 DLL 相关内容,不清除配置,只提示用户
|
// 如果错误信息包含 VC++ 或 DLL 相关内容,不清除配置,只提示用户
|
||||||
// 其他错误可能需要重新配置
|
// 其他错误可能需要重新配置
|
||||||
const errorMsg = result.error || ''
|
const errorMsg = result.error || ''
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function AnnualReportPage() {
|
|||||||
<div className="annual-report-page">
|
<div className="annual-report-page">
|
||||||
<Sparkles size={32} className="header-icon" />
|
<Sparkles size={32} className="header-icon" />
|
||||||
<h1 className="page-title">年度报告</h1>
|
<h1 className="page-title">年度报告</h1>
|
||||||
<p className="page-desc">选择年份,生成你的微信聊天年度回顾</p>
|
<p className="page-desc">选择年份,回顾你在微信里的点点滴滴</p>
|
||||||
|
|
||||||
<div className="report-sections">
|
<div className="report-sections">
|
||||||
<section className="report-section">
|
<section className="report-section">
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ function AnnualReportWindow() {
|
|||||||
<Avatar url={selfAvatarUrl} name="我" size="lg" />
|
<Avatar url={selfAvatarUrl} name="我" size="lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="hero-desc">只要你想<br />我一直在</p>
|
<p className="hero-desc">你只管说<br />我一直在</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 双向奔赴 */}
|
{/* 双向奔赴 */}
|
||||||
@@ -1025,7 +1025,7 @@ function AnnualReportWindow() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="hero-desc">
|
<p className="hero-desc">
|
||||||
其中 <span className="hl">{midnightKing.displayName}</span> 常常在深夜中陪着你。
|
其中 <span className="hl">{midnightKing.displayName}</span> 常常在深夜中陪着你。
|
||||||
<br />你和Ta的对话占深夜期间聊天的 <span className="gold">{midnightKing.percentage}%</span>。
|
<br />你和Ta的对话占你深夜期间聊天的 <span className="gold">{midnightKing.percentage}%</span>。
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -286,6 +286,11 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setSessions
|
setSessions
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 同步 currentSessionId 到 ref
|
||||||
|
useEffect(() => {
|
||||||
|
currentSessionRef.current = currentSessionId
|
||||||
|
}, [currentSessionId])
|
||||||
|
|
||||||
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
|
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
|
||||||
const loadSessions = async (options?: { silent?: boolean }) => {
|
const loadSessions = async (options?: { silent?: boolean }) => {
|
||||||
if (options?.silent) {
|
if (options?.silent) {
|
||||||
@@ -301,6 +306,19 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const nextSessions = options?.silent ? mergeSessions(sessionsArray) : sessionsArray
|
const nextSessions = options?.silent ? mergeSessions(sessionsArray) : sessionsArray
|
||||||
// 确保 nextSessions 也是数组
|
// 确保 nextSessions 也是数组
|
||||||
if (Array.isArray(nextSessions)) {
|
if (Array.isArray(nextSessions)) {
|
||||||
|
// 【核心优化】检查当前会话是否有更新(通过 lastTimestamp 对比)
|
||||||
|
const currentId = currentSessionRef.current
|
||||||
|
if (currentId) {
|
||||||
|
const newSession = nextSessions.find(s => s.username === currentId)
|
||||||
|
const oldSession = sessionsRef.current.find(s => s.username === currentId)
|
||||||
|
|
||||||
|
// 如果会话存在且时间戳变大(有新消息)或者之前没有该会话
|
||||||
|
if (newSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) {
|
||||||
|
console.log(`[Frontend] Detected update for current session ${currentId}, refreshing messages...`)
|
||||||
|
void handleIncrementalRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSessions(nextSessions)
|
setSessions(nextSessions)
|
||||||
// 立即启动联系人信息加载,不再延迟 500ms
|
// 立即启动联系人信息加载,不再延迟 500ms
|
||||||
void enrichSessionsContactInfo(nextSessions)
|
void enrichSessionsContactInfo(nextSessions)
|
||||||
@@ -330,14 +348,14 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 防止重复加载
|
// 防止重复加载
|
||||||
if (isEnrichingRef.current) {
|
if (isEnrichingRef.current) {
|
||||||
console.log('[性能监控] 联系人信息正在加载中,跳过重复请求')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnrichingRef.current = true
|
isEnrichingRef.current = true
|
||||||
enrichCancelledRef.current = false
|
enrichCancelledRef.current = false
|
||||||
|
|
||||||
console.log(`[性能监控] 开始加载联系人信息,会话数: ${sessions.length}`)
|
|
||||||
const totalStart = performance.now()
|
const totalStart = performance.now()
|
||||||
|
|
||||||
// 移除初始 500ms 延迟,让后台加载与 UI 渲染并行
|
// 移除初始 500ms 延迟,让后台加载与 UI 渲染并行
|
||||||
@@ -352,12 +370,12 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
// 找出需要加载联系人信息的会话(没有头像或者没有显示名称的)
|
// 找出需要加载联系人信息的会话(没有头像或者没有显示名称的)
|
||||||
const needEnrich = sessions.filter(s => !s.avatarUrl || !s.displayName || s.displayName === s.username)
|
const needEnrich = sessions.filter(s => !s.avatarUrl || !s.displayName || s.displayName === s.username)
|
||||||
if (needEnrich.length === 0) {
|
if (needEnrich.length === 0) {
|
||||||
console.log('[性能监控] 所有联系人信息已缓存,跳过加载')
|
|
||||||
isEnrichingRef.current = false
|
isEnrichingRef.current = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[性能监控] 需要加载的联系人信息: ${needEnrich.length} 个`)
|
|
||||||
|
|
||||||
// 进一步减少批次大小,每批3个,避免DLL调用阻塞
|
// 进一步减少批次大小,每批3个,避免DLL调用阻塞
|
||||||
const batchSize = 3
|
const batchSize = 3
|
||||||
@@ -366,7 +384,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
for (let i = 0; i < needEnrich.length; i += batchSize) {
|
for (let i = 0; i < needEnrich.length; i += batchSize) {
|
||||||
// 如果正在滚动,暂停加载
|
// 如果正在滚动,暂停加载
|
||||||
if (isScrollingRef.current) {
|
if (isScrollingRef.current) {
|
||||||
console.log('[性能监控] 检测到滚动,暂停加载联系人信息')
|
|
||||||
// 等待滚动结束
|
// 等待滚动结束
|
||||||
while (isScrollingRef.current && !enrichCancelledRef.current) {
|
while (isScrollingRef.current && !enrichCancelledRef.current) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200))
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
@@ -410,9 +428,9 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
const totalTime = performance.now() - totalStart
|
const totalTime = performance.now() - totalStart
|
||||||
if (!enrichCancelledRef.current) {
|
if (!enrichCancelledRef.current) {
|
||||||
console.log(`[性能监控] 联系人信息加载完成,总耗时: ${totalTime.toFixed(2)}ms, 已加载: ${loadedCount}/${needEnrich.length}`)
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[性能监控] 联系人信息加载被取消,已加载: ${loadedCount}/${needEnrich.length}`)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载联系人信息失败:', e)
|
console.error('加载联系人信息失败:', e)
|
||||||
@@ -514,7 +532,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 如果是自己的信息且当前个人头像为空,同步更新
|
// 如果是自己的信息且当前个人头像为空,同步更新
|
||||||
if (myWxid && username === myWxid && contact.avatarUrl && !myAvatarUrl) {
|
if (myWxid && username === myWxid && contact.avatarUrl && !myAvatarUrl) {
|
||||||
console.log('[ChatPage] 从联系人同步获取到个人头像')
|
|
||||||
setMyAvatarUrl(contact.avatarUrl)
|
setMyAvatarUrl(contact.avatarUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +560,50 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
// 刷新当前会话消息(增量更新新消息)
|
// 刷新当前会话消息(增量更新新消息)
|
||||||
const [isRefreshingMessages, setIsRefreshingMessages] = useState(false)
|
const [isRefreshingMessages, setIsRefreshingMessages] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 极速增量刷新:基于最后一条消息时间戳,获取后续新消息
|
||||||
|
* (由用户建议:记住上一条消息时间,自动取之后的并渲染,然后后台兜底全量同步)
|
||||||
|
*/
|
||||||
|
const handleIncrementalRefresh = async () => {
|
||||||
|
if (!currentSessionId || isRefreshingMessages) return
|
||||||
|
|
||||||
|
// 找出当前已渲染消息中的最大时间戳
|
||||||
|
const lastMsg = messages[messages.length - 1]
|
||||||
|
const minTime = lastMsg?.createTime || 0
|
||||||
|
|
||||||
|
// 1. 优先执行增量查询并渲染(第一步)
|
||||||
|
try {
|
||||||
|
const result = await (window.electronAPI.chat as any).getNewMessages(currentSessionId, minTime) as {
|
||||||
|
success: boolean;
|
||||||
|
messages?: Message[];
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success && result.messages && result.messages.length > 0) {
|
||||||
|
// 过滤去重
|
||||||
|
const existingKeys = new Set(messages.map(getMessageKey))
|
||||||
|
const newOnes = result.messages.filter(m => !existingKeys.has(getMessageKey(m)))
|
||||||
|
|
||||||
|
if (newOnes.length > 0) {
|
||||||
|
appendMessages(newOnes, false)
|
||||||
|
flashNewMessages(newOnes.map(getMessageKey))
|
||||||
|
// 滚动到底部
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (messageListRef.current) {
|
||||||
|
messageListRef.current.scrollTop = messageListRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[IncrementalRefresh] 失败,将依赖全量同步兜底:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 后台兜底:执行之前的完整游标刷新,确保没有遗漏(比如跨库的消息)
|
||||||
|
void handleRefreshMessages()
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefreshMessages = async () => {
|
const handleRefreshMessages = async () => {
|
||||||
if (!currentSessionId || isRefreshingMessages) return
|
if (!currentSessionId || isRefreshingMessages) return
|
||||||
setJumpStartTime(0)
|
setJumpStartTime(0)
|
||||||
@@ -584,6 +646,31 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听数据库变更实时刷新
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDbChange = (_event: any, data: { type: string; json: string }) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(data.json)
|
||||||
|
const tableName = payload.table
|
||||||
|
|
||||||
|
// 会话列表更新(主要靠这个触发,因为 wcdb_api 已经只监控 session 了)
|
||||||
|
if (tableName === 'Session' || tableName === 'session') {
|
||||||
|
void loadSessions({ silent: true })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析数据库变更通知失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI.chat.onWcdbChange) {
|
||||||
|
const removeListener = window.electronAPI.chat.onWcdbChange(handleDbChange)
|
||||||
|
return () => {
|
||||||
|
removeListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => { }
|
||||||
|
}, [loadSessions, handleRefreshMessages])
|
||||||
|
|
||||||
// 加载消息
|
// 加载消息
|
||||||
const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0) => {
|
const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0) => {
|
||||||
const listEl = messageListRef.current
|
const listEl = messageListRef.current
|
||||||
@@ -621,7 +708,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
.map(m => m.senderUsername as string)
|
.map(m => m.senderUsername as string)
|
||||||
)]
|
)]
|
||||||
if (unknownSenders.length > 0) {
|
if (unknownSenders.length > 0) {
|
||||||
console.log(`[性能监控] 预取消息发送者信息: ${unknownSenders.length} 个`)
|
|
||||||
// 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求
|
// 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求
|
||||||
const batchPromise = loadContactInfoBatch(unknownSenders)
|
const batchPromise = loadContactInfoBatch(unknownSenders)
|
||||||
unknownSenders.forEach(username => {
|
unknownSenders.forEach(username => {
|
||||||
@@ -1549,23 +1636,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVideo) return
|
if (!isVideo) return
|
||||||
|
|
||||||
console.log('[Video Debug] Full message object:', JSON.stringify(message, null, 2))
|
|
||||||
console.log('[Video Debug] Message keys:', Object.keys(message))
|
|
||||||
console.log('[Video Debug] Message:', {
|
|
||||||
localId: message.localId,
|
|
||||||
localType: message.localType,
|
|
||||||
hasVideoMd5: !!message.videoMd5,
|
|
||||||
hasContent: !!message.content,
|
|
||||||
hasParsedContent: !!message.parsedContent,
|
|
||||||
hasRawContent: !!(message as any).rawContent,
|
|
||||||
contentPreview: message.content?.substring(0, 200),
|
|
||||||
parsedContentPreview: message.parsedContent?.substring(0, 200),
|
|
||||||
rawContentPreview: (message as any).rawContent?.substring(0, 200)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 优先使用数据库中的 videoMd5
|
// 优先使用数据库中的 videoMd5
|
||||||
if (message.videoMd5) {
|
if (message.videoMd5) {
|
||||||
console.log('[Video Debug] Using videoMd5 from message:', message.videoMd5)
|
|
||||||
setVideoMd5(message.videoMd5)
|
setVideoMd5(message.videoMd5)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1573,11 +1650,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
// 尝试从多个可能的字段获取原始内容
|
// 尝试从多个可能的字段获取原始内容
|
||||||
const contentToUse = message.content || (message as any).rawContent || message.parsedContent
|
const contentToUse = message.content || (message as any).rawContent || message.parsedContent
|
||||||
if (contentToUse) {
|
if (contentToUse) {
|
||||||
console.log('[Video Debug] Parsing MD5 from content, length:', contentToUse.length)
|
|
||||||
window.electronAPI.video.parseVideoMd5(contentToUse).then((result: { success: boolean; md5?: string; error?: string }) => {
|
window.electronAPI.video.parseVideoMd5(contentToUse).then((result: { success: boolean; md5?: string; error?: string }) => {
|
||||||
console.log('[Video Debug] Parse result:', result)
|
|
||||||
if (result && result.success && result.md5) {
|
if (result && result.success && result.md5) {
|
||||||
console.log('[Video Debug] Parsed MD5:', result.md5)
|
|
||||||
setVideoMd5(result.md5)
|
setVideoMd5(result.md5)
|
||||||
} else {
|
} else {
|
||||||
console.error('[Video Debug] Failed to parse MD5:', result)
|
console.error('[Video Debug] Failed to parse MD5:', result)
|
||||||
@@ -2061,11 +2138,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
String(message.localId),
|
String(message.localId),
|
||||||
message.createTime
|
message.createTime
|
||||||
)
|
)
|
||||||
console.log('[ChatPage] 调用转写:', {
|
|
||||||
sessionId: session.username,
|
|
||||||
msgId: message.localId,
|
|
||||||
createTime: message.createTime
|
|
||||||
})
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const transcriptText = (result.transcript || '').trim()
|
const transcriptText = (result.transcript || '').trim()
|
||||||
voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText)
|
voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText)
|
||||||
@@ -2138,14 +2211,14 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVideo || !isVideoVisible || videoInfo || videoLoading) return
|
if (!isVideo || !isVideoVisible || videoInfo || videoLoading) return
|
||||||
if (!videoMd5) {
|
if (!videoMd5) {
|
||||||
console.log('[Video Debug] No videoMd5 available yet')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Video Debug] Loading video info for MD5:', videoMd5)
|
|
||||||
setVideoLoading(true)
|
setVideoLoading(true)
|
||||||
window.electronAPI.video.getVideoInfo(videoMd5).then((result: { success: boolean; exists: boolean; videoUrl?: string; coverUrl?: string; thumbUrl?: string; error?: string }) => {
|
window.electronAPI.video.getVideoInfo(videoMd5).then((result: { success: boolean; exists: boolean; videoUrl?: string; coverUrl?: string; thumbUrl?: string; error?: string }) => {
|
||||||
console.log('[Video Debug] getVideoInfo result:', result)
|
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
setVideoInfo({
|
setVideoInfo({
|
||||||
exists: result.exists,
|
exists: result.exists,
|
||||||
@@ -2642,7 +2715,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
const fileName = message.fileName || title || '文件'
|
const fileName = message.fileName || title || '文件'
|
||||||
const fileSize = message.fileSize
|
const fileSize = message.fileSize
|
||||||
const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || ''
|
const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || ''
|
||||||
|
|
||||||
// 根据扩展名选择图标
|
// 根据扩展名选择图标
|
||||||
const getFileIcon = () => {
|
const getFileIcon = () => {
|
||||||
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
|
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
|
||||||
@@ -2662,7 +2735,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-message">
|
<div className="file-message">
|
||||||
<div className="file-icon">
|
<div className="file-icon">
|
||||||
@@ -2682,10 +2755,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
if (appMsgType === '2000') {
|
if (appMsgType === '2000') {
|
||||||
try {
|
try {
|
||||||
const content = message.rawContent || message.content || message.parsedContent || ''
|
const content = message.rawContent || message.content || message.parsedContent || ''
|
||||||
|
|
||||||
// 添加调试日志
|
// 添加调试日志
|
||||||
console.log('[Transfer Debug] Raw content:', content.substring(0, 500))
|
|
||||||
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(content, 'text/xml')
|
const doc = parser.parseFromString(content, 'text/xml')
|
||||||
|
|
||||||
@@ -2693,11 +2766,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
const payMemo = doc.querySelector('pay_memo')?.textContent || ''
|
const payMemo = doc.querySelector('pay_memo')?.textContent || ''
|
||||||
const paysubtype = doc.querySelector('paysubtype')?.textContent || '1'
|
const paysubtype = doc.querySelector('paysubtype')?.textContent || '1'
|
||||||
|
|
||||||
console.log('[Transfer Debug] Parsed:', { feedesc, payMemo, paysubtype, title })
|
|
||||||
|
|
||||||
// paysubtype: 1=待收款, 3=已收款
|
// paysubtype: 1=待收款, 3=已收款
|
||||||
const isReceived = paysubtype === '3'
|
const isReceived = paysubtype === '3'
|
||||||
|
|
||||||
// 如果 feedesc 为空,使用 title 作为降级
|
// 如果 feedesc 为空,使用 title 作为降级
|
||||||
const displayAmount = feedesc || title || '微信转账'
|
const displayAmount = feedesc || title || '微信转账'
|
||||||
|
|
||||||
@@ -2743,7 +2816,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
<div className="miniapp-message">
|
<div className="miniapp-message">
|
||||||
<div className="miniapp-icon">
|
<div className="miniapp-icon">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="miniapp-info">
|
<div className="miniapp-info">
|
||||||
|
|||||||
@@ -41,15 +41,10 @@ function ContactsPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const contactsResult = await window.electronAPI.chat.getContacts()
|
const contactsResult = await window.electronAPI.chat.getContacts()
|
||||||
console.log('📞 getContacts结果:', contactsResult)
|
|
||||||
if (contactsResult.success && contactsResult.contacts) {
|
if (contactsResult.success && contactsResult.contacts) {
|
||||||
console.log('📊 总联系人数:', contactsResult.contacts.length)
|
|
||||||
console.log('📊 按类型统计:', {
|
|
||||||
friends: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'friend').length,
|
|
||||||
groups: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'group').length,
|
|
||||||
officials: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'official').length,
|
|
||||||
other: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'other').length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取头像URL
|
// 获取头像URL
|
||||||
const usernames = contactsResult.contacts.map((c: ContactInfo) => c.username)
|
const usernames = contactsResult.contacts.map((c: ContactInfo) => c.username)
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export default function SnsPage() {
|
|||||||
const currentPosts = postsRef.current
|
const currentPosts = postsRef.current
|
||||||
if (currentPosts.length > 0) {
|
if (currentPosts.length > 0) {
|
||||||
const topTs = currentPosts[0].createTime
|
const topTs = currentPosts[0].createTime
|
||||||
console.log('[SnsPage] Fetching newer posts starts from:', topTs + 1);
|
|
||||||
|
|
||||||
const result = await window.electronAPI.sns.getTimeline(
|
const result = await window.electronAPI.sns.getTimeline(
|
||||||
limit,
|
limit,
|
||||||
@@ -281,10 +281,10 @@ export default function SnsPage() {
|
|||||||
const checkSchema = async () => {
|
const checkSchema = async () => {
|
||||||
try {
|
try {
|
||||||
const schema = await window.electronAPI.chat.execQuery('sns', null, "PRAGMA table_info(SnsTimeLine)");
|
const schema = await window.electronAPI.chat.execQuery('sns', null, "PRAGMA table_info(SnsTimeLine)");
|
||||||
console.log('[SnsPage] SnsTimeLine Schema:', schema);
|
|
||||||
if (schema.success && schema.rows) {
|
if (schema.success && schema.rows) {
|
||||||
const columns = schema.rows.map((r: any) => r.name);
|
const columns = schema.rows.map((r: any) => r.name);
|
||||||
console.log('[SnsPage] Available columns:', columns);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[SnsPage] Failed to check schema:', e);
|
console.error('[SnsPage] Failed to check schema:', e);
|
||||||
@@ -335,7 +335,7 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
// deltaY < 0 表示向上滚,scrollTop === 0 表示已经在最顶端
|
// deltaY < 0 表示向上滚,scrollTop === 0 表示已经在最顶端
|
||||||
if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) {
|
if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) {
|
||||||
console.log('[SnsPage] Wheel-up detected at top, loading newer posts...');
|
|
||||||
loadPosts({ direction: 'newer' })
|
loadPosts({ direction: 'newer' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/types/electron.d.ts
vendored
6
src/types/electron.d.ts
vendored
@@ -77,6 +77,11 @@ export interface ElectronAPI {
|
|||||||
messages?: Message[]
|
messages?: Message[]
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
messages?: Message[]
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getContact: (username: string) => Promise<Contact | null>
|
getContact: (username: string) => Promise<Contact | null>
|
||||||
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
||||||
getContacts: () => Promise<{
|
getContacts: () => Promise<{
|
||||||
@@ -110,6 +115,7 @@ export interface ElectronAPI {
|
|||||||
onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => () => void
|
onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => () => void
|
||||||
execQuery: (kind: string, path: string | null, sql: string) => Promise<{ success: boolean; rows?: any[]; error?: string }>
|
execQuery: (kind: string, path: string | null, sql: string) => Promise<{ success: boolean; rows?: any[]; error?: string }>
|
||||||
getMessage: (sessionId: string, localId: number) => Promise<{ success: boolean; message?: Message; error?: string }>
|
getMessage: (sessionId: string, localId: number) => Promise<{ success: boolean; message?: Message; error?: string }>
|
||||||
|
onWcdbChange: (callback: (event: any, data: { type: string; json: string }) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
|
|||||||
Reference in New Issue
Block a user