mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-22 15:39:41 +08:00
feat: 一些非常帅气的优化
This commit is contained in:
@@ -724,6 +724,10 @@ function registerIpcHandlers() {
|
||||
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) => {
|
||||
return await chatService.getContact(username)
|
||||
})
|
||||
@@ -1170,7 +1174,7 @@ function checkForUpdatesOnStartup() {
|
||||
// 检查该版本是否被用户忽略
|
||||
const ignoredVersion = configService?.get('ignoredUpdateVersion')
|
||||
if (ignoredVersion === latestVersion) {
|
||||
console.log(`版本 ${latestVersion} 已被用户忽略,跳过更新提示`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function enforceLocalDllPriority() {
|
||||
process.env.PATH = dllPaths
|
||||
}
|
||||
|
||||
console.log('[WeFlow] Environment PATH updated to enforce local DLL priority:', dllPaths)
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -111,6 +111,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit, startTime, endTime, ascending),
|
||||
getLatestMessages: (sessionId: string, limit?: number) =>
|
||||
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),
|
||||
getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username),
|
||||
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
||||
@@ -132,7 +134,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.invoke('chat:execQuery', kind, path, sql),
|
||||
getContacts: () => ipcRenderer.invoke('chat:getContacts'),
|
||||
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]
|
||||
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 {
|
||||
@@ -245,6 +249,9 @@ class AnalyticsService {
|
||||
}
|
||||
|
||||
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 = {
|
||||
total: 0,
|
||||
sent: 0,
|
||||
@@ -269,8 +276,22 @@ class AnalyticsService {
|
||||
if (endTimestamp > 0 && createTime > endTimestamp) return
|
||||
|
||||
const localType = parseInt(row.local_type || row.type || '1', 10)
|
||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? 0
|
||||
const isSend = String(isSendRaw) === '1' || isSendRaw === 1 || isSendRaw === true
|
||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend
|
||||
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
|
||||
sessionStat.total += 1
|
||||
|
||||
@@ -115,8 +115,9 @@ class AnnualReportService {
|
||||
return trimmed
|
||||
}
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
if (suffixMatch) return suffixMatch[1]
|
||||
return trimmed
|
||||
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
private async ensureConnectedWithConfig(
|
||||
@@ -596,9 +597,22 @@ class AnnualReportService {
|
||||
if (!createTime) continue
|
||||
|
||||
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)
|
||||
|
||||
// 兼容逻辑
|
||||
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)) {
|
||||
conversationStarts.set(sessionId, { initiated: 0, received: 0 })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 fs from 'fs'
|
||||
import * as https from 'https'
|
||||
@@ -7,7 +7,7 @@ import * as http from 'http'
|
||||
import * as fzstd from 'fzstd'
|
||||
import * as crypto from 'crypto'
|
||||
import Database from 'better-sqlite3'
|
||||
import { app } from 'electron'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { ConfigService } from './config'
|
||||
import { wcdbService } from './wcdbService'
|
||||
import { MessageCacheService } from './messageCacheService'
|
||||
@@ -152,9 +152,9 @@ class ChatService {
|
||||
}
|
||||
|
||||
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.setupDbMonitor()
|
||||
|
||||
// 预热 listMediaDbs 缓存(后台异步执行,不阻塞连接)
|
||||
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 数据库列表缓存(后台异步执行)
|
||||
*/
|
||||
@@ -543,7 +564,7 @@ class ChatService {
|
||||
FROM contact
|
||||
`
|
||||
|
||||
console.log('查询contact.db...')
|
||||
|
||||
const contactResult = await wcdbService.execQuery('contact', null, contactQuery)
|
||||
|
||||
if (!contactResult.success || !contactResult.rows) {
|
||||
@@ -551,13 +572,13 @@ class ChatService {
|
||||
return { success: false, error: contactResult.error || '查询联系人失败' }
|
||||
}
|
||||
|
||||
console.log('查询到', contactResult.rows.length, '条联系人记录')
|
||||
|
||||
const rows = contactResult.rows as Record<string, any>[]
|
||||
|
||||
// 调试:显示前5条数据样本
|
||||
console.log('📋 前5条数据样本:')
|
||||
|
||||
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分布
|
||||
@@ -566,7 +587,7 @@ class ChatService {
|
||||
const lt = row.local_type || 0
|
||||
localTypeStats.set(lt, (localTypeStats.get(lt) || 0) + 1)
|
||||
})
|
||||
console.log('📊 local_type分布:', Object.fromEntries(localTypeStats))
|
||||
|
||||
|
||||
// 获取会话表的最后联系时间用于排序
|
||||
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) => {
|
||||
@@ -665,7 +681,7 @@ class ChatService {
|
||||
// 移除临时的lastContactTime字段
|
||||
const result = contacts.map(({ lastContactTime, ...rest }) => rest)
|
||||
|
||||
console.log('返回', result.length, '个联系人')
|
||||
|
||||
return { success: true, contacts: result }
|
||||
} catch (e) {
|
||||
console.error('ChatService: 获取通讯录失败:', e)
|
||||
@@ -731,7 +747,7 @@ class ChatService {
|
||||
|
||||
// 如果需要跳过消息(offset > 0),逐批获取但不返回
|
||||
if (offset > 0) {
|
||||
console.log(`[ChatService] 跳过消息: offset=${offset}`)
|
||||
|
||||
let skipped = 0
|
||||
while (skipped < offset) {
|
||||
const skipBatch = await wcdbService.fetchMessageBatch(state.cursor)
|
||||
@@ -740,17 +756,17 @@ class ChatService {
|
||||
return { success: false, error: skipBatch.error || '跳过消息失败' }
|
||||
}
|
||||
if (!skipBatch.rows || skipBatch.rows.length === 0) {
|
||||
console.log('[ChatService] 跳过时没有更多消息')
|
||||
|
||||
return { success: true, messages: [], hasMore: false }
|
||||
}
|
||||
skipped += skipBatch.rows.length
|
||||
state.fetched += skipBatch.rows.length
|
||||
if (!skipBatch.hasMore) {
|
||||
console.log('[ChatService] 跳过时已到达末尾')
|
||||
|
||||
return { success: true, messages: [], hasMore: false }
|
||||
}
|
||||
}
|
||||
console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}`)
|
||||
|
||||
}
|
||||
} else if (state && offset !== state.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[] {
|
||||
if (messages.length < 2) return messages
|
||||
const first = messages[0]
|
||||
@@ -1019,13 +1069,19 @@ class ChatService {
|
||||
|
||||
if (senderUsername && (myWxidLower || cleanedWxidLower)) {
|
||||
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) {
|
||||
isSend = expectedIsSend
|
||||
// [DEBUG] Issue #34: 记录 isSend 推断过程
|
||||
if (expectedIsSend === 0 && localType === 1) {
|
||||
// 仅在被判为接收且是文本消息时记录,避免刷屏
|
||||
// console.log(`[ChatService] inferred isSend=0: sender=${senderUsername}, myWxid=${myWxid} (cleaned=${cleanedWxid})`)
|
||||
//
|
||||
}
|
||||
}
|
||||
} else if (senderUsername && !myWxid) {
|
||||
@@ -1249,7 +1305,7 @@ class ChatService {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有 title,根据 type 返回默认标签
|
||||
switch (type) {
|
||||
case '6':
|
||||
@@ -1607,10 +1663,10 @@ class ChatService {
|
||||
// 文件消息
|
||||
result.fileName = title || this.extractXmlValue(content, 'filename')
|
||||
result.linkTitle = result.fileName
|
||||
|
||||
|
||||
// 提取文件大小
|
||||
const fileSizeStr = this.extractXmlValue(content, 'totallen') ||
|
||||
this.extractXmlValue(content, 'filesize')
|
||||
const fileSizeStr = this.extractXmlValue(content, 'totallen') ||
|
||||
this.extractXmlValue(content, 'filesize')
|
||||
if (fileSizeStr) {
|
||||
const size = parseInt(fileSizeStr, 10)
|
||||
if (!isNaN(size)) {
|
||||
@@ -1635,7 +1691,7 @@ class ChatService {
|
||||
case '19': {
|
||||
// 聊天记录
|
||||
result.chatRecordTitle = title || '聊天记录'
|
||||
|
||||
|
||||
// 解析聊天记录列表
|
||||
const recordList: Array<{
|
||||
datatype: number
|
||||
@@ -1648,10 +1704,10 @@ class ChatService {
|
||||
// 查找所有 <recorditem> 标签
|
||||
const recordItemRegex = /<recorditem>([\s\S]*?)<\/recorditem>/gi
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
|
||||
while ((match = recordItemRegex.exec(content)) !== null) {
|
||||
const itemXml = match[1]
|
||||
|
||||
|
||||
const datatypeStr = this.extractXmlValue(itemXml, 'datatype')
|
||||
const sourcename = this.extractXmlValue(itemXml, 'sourcename')
|
||||
const sourcetime = this.extractXmlValue(itemXml, 'sourcetime')
|
||||
@@ -1680,10 +1736,10 @@ class ChatService {
|
||||
// 小程序
|
||||
result.linkTitle = title
|
||||
result.linkUrl = url
|
||||
|
||||
|
||||
// 提取缩略图
|
||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
if (thumbUrl) {
|
||||
result.linkThumb = thumbUrl
|
||||
}
|
||||
@@ -1693,11 +1749,11 @@ class ChatService {
|
||||
case '2000': {
|
||||
// 转账
|
||||
result.linkTitle = title || '[转账]'
|
||||
|
||||
|
||||
// 可以提取转账金额等信息
|
||||
const payMemo = this.extractXmlValue(content, 'pay_memo')
|
||||
const feedesc = this.extractXmlValue(content, 'feedesc')
|
||||
|
||||
|
||||
if (payMemo) {
|
||||
result.linkTitle = payMemo
|
||||
} else if (feedesc) {
|
||||
@@ -1710,9 +1766,9 @@ class ChatService {
|
||||
// 其他类型,提取通用字段
|
||||
result.linkTitle = title
|
||||
result.linkUrl = url
|
||||
|
||||
|
||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
if (thumbUrl) {
|
||||
result.linkThumb = thumbUrl
|
||||
}
|
||||
@@ -2132,7 +2188,7 @@ class ChatService {
|
||||
private decodeMaybeCompressed(raw: any, fieldName: string = 'unknown'): string {
|
||||
if (!raw) return ''
|
||||
|
||||
// console.log(`[ChatService] Decoding ${fieldName}: type=${typeof raw}`, raw)
|
||||
//
|
||||
|
||||
// 如果是 Buffer/Uint8Array
|
||||
if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
|
||||
@@ -2148,7 +2204,7 @@ class ChatService {
|
||||
const bytes = Buffer.from(raw, 'hex')
|
||||
if (bytes.length > 0) {
|
||||
const result = this.decodeBinaryContent(bytes, raw)
|
||||
// console.log(`[ChatService] HEX decoded result: ${result}`)
|
||||
//
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -2200,7 +2256,7 @@ class ChatService {
|
||||
|
||||
// 如果提供了 fallbackValue,且解码结果看起来像二进制垃圾,则返回 fallbackValue
|
||||
if (fallbackValue && replacementCount > 0) {
|
||||
// console.log(`[ChatService] Binary garbage detected, using fallback: ${fallbackValue}`)
|
||||
//
|
||||
return fallbackValue
|
||||
}
|
||||
|
||||
@@ -2794,7 +2850,7 @@ class ChatService {
|
||||
const t1 = Date.now()
|
||||
const msgResult = await this.getMessageByLocalId(sessionId, localId)
|
||||
const t2 = Date.now()
|
||||
console.log(`[Voice] getMessageByLocalId: ${t2 - t1}ms`)
|
||||
|
||||
|
||||
if (msgResult.success && msgResult.message) {
|
||||
const msg = msgResult.message as any
|
||||
@@ -2813,7 +2869,7 @@ class ChatService {
|
||||
// 检查 WAV 内存缓存
|
||||
const wavCache = this.voiceWavCache.get(cacheKey)
|
||||
if (wavCache) {
|
||||
console.log(`[Voice] 内存缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return { success: true, data: wavCache.toString('base64') }
|
||||
}
|
||||
|
||||
@@ -2825,7 +2881,7 @@ class ChatService {
|
||||
const wavData = readFileSync(wavFilePath)
|
||||
// 同时缓存到内存
|
||||
this.cacheVoiceWav(cacheKey, wavData)
|
||||
console.log(`[Voice] 文件缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return { success: true, data: wavData.toString('base64') }
|
||||
} catch (e) {
|
||||
console.error('[Voice] 读取缓存文件失败:', e)
|
||||
@@ -2855,7 +2911,7 @@ class ChatService {
|
||||
// 从数据库读取 silk 数据
|
||||
const silkData = await this.getVoiceDataFromMediaDb(msgCreateTime, candidates)
|
||||
const t4 = Date.now()
|
||||
console.log(`[Voice] getVoiceDataFromMediaDb: ${t4 - t3}ms`)
|
||||
|
||||
|
||||
if (!silkData) {
|
||||
return { success: false, error: '未找到语音数据 (请确保已在微信中播放过该语音)' }
|
||||
@@ -2865,7 +2921,7 @@ class ChatService {
|
||||
// 使用 silk-wasm 解码
|
||||
const pcmData = await this.decodeSilkToPcm(silkData, 24000)
|
||||
const t6 = Date.now()
|
||||
console.log(`[Voice] decodeSilkToPcm: ${t6 - t5}ms`)
|
||||
|
||||
|
||||
if (!pcmData) {
|
||||
return { success: false, error: 'Silk 解码失败' }
|
||||
@@ -2875,7 +2931,7 @@ class ChatService {
|
||||
// PCM -> WAV
|
||||
const wavData = this.createWavBuffer(pcmData, 24000)
|
||||
const t8 = Date.now()
|
||||
console.log(`[Voice] createWavBuffer: ${t8 - t7}ms`)
|
||||
|
||||
|
||||
// 缓存 WAV 数据到内存
|
||||
this.cacheVoiceWav(cacheKey, wavData)
|
||||
@@ -2883,7 +2939,7 @@ class ChatService {
|
||||
// 缓存 WAV 数据到文件(异步,不阻塞返回)
|
||||
this.cacheVoiceWavToFile(cacheKey, wavData)
|
||||
|
||||
console.log(`[Voice] 总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return { success: true, data: wavData.toString('base64') }
|
||||
} catch (e) {
|
||||
console.error('ChatService: getVoiceData 失败:', e)
|
||||
@@ -2920,11 +2976,11 @@ class ChatService {
|
||||
let mediaDbFiles: string[]
|
||||
if (this.mediaDbsCache) {
|
||||
mediaDbFiles = this.mediaDbsCache
|
||||
console.log(`[Voice] listMediaDbs (缓存): 0ms`)
|
||||
|
||||
} else {
|
||||
const mediaDbsResult = await wcdbService.listMediaDbs()
|
||||
const t2 = Date.now()
|
||||
console.log(`[Voice] listMediaDbs: ${t2 - t1}ms`)
|
||||
|
||||
|
||||
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%'"
|
||||
)
|
||||
const t4 = Date.now()
|
||||
console.log(`[Voice] 查询VoiceInfo表: ${t4 - t3}ms`)
|
||||
|
||||
|
||||
if (!tablesResult.success || !tablesResult.rows || tablesResult.rows.length === 0) {
|
||||
continue
|
||||
@@ -2969,7 +3025,7 @@ class ChatService {
|
||||
`PRAGMA table_info('${voiceTable}')`
|
||||
)
|
||||
const t6 = Date.now()
|
||||
console.log(`[Voice] 查询表结构: ${t6 - t5}ms`)
|
||||
|
||||
|
||||
if (!columnsResult.success || !columnsResult.rows) {
|
||||
continue
|
||||
@@ -3006,7 +3062,7 @@ class ChatService {
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Name2Id%'"
|
||||
)
|
||||
const t8 = Date.now()
|
||||
console.log(`[Voice] 查询Name2Id表: ${t8 - t7}ms`)
|
||||
|
||||
|
||||
const name2IdTable = (name2IdTablesResult.success && name2IdTablesResult.rows && name2IdTablesResult.rows.length > 0)
|
||||
? name2IdTablesResult.rows[0].name
|
||||
@@ -3033,7 +3089,7 @@ class ChatService {
|
||||
`SELECT user_name, rowid FROM ${schema.name2IdTable} WHERE user_name IN (${candidatesStr})`
|
||||
)
|
||||
const t10 = Date.now()
|
||||
console.log(`[Voice] 查询chat_name_id: ${t10 - t9}ms`)
|
||||
|
||||
|
||||
if (name2IdResult.success && name2IdResult.rows && name2IdResult.rows.length > 0) {
|
||||
// 构建 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`
|
||||
)
|
||||
const t12 = Date.now()
|
||||
console.log(`[Voice] 策略1查询语音: ${t12 - t11}ms`)
|
||||
|
||||
|
||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||
const row = voiceResult.rows[0]
|
||||
const silkData = this.decodeVoiceBlob(row.data)
|
||||
if (silkData) {
|
||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return silkData
|
||||
}
|
||||
}
|
||||
@@ -3066,13 +3122,13 @@ class ChatService {
|
||||
`SELECT ${schema.dataColumn} AS data FROM ${schema.voiceTable} WHERE ${schema.timeColumn} = ${createTime} LIMIT 1`
|
||||
)
|
||||
const t14 = Date.now()
|
||||
console.log(`[Voice] 策略2查询语音: ${t14 - t13}ms`)
|
||||
|
||||
|
||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||
const row = voiceResult.rows[0]
|
||||
const silkData = this.decodeVoiceBlob(row.data)
|
||||
if (silkData) {
|
||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
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`
|
||||
)
|
||||
const t16 = Date.now()
|
||||
console.log(`[Voice] 策略3查询语音: ${t16 - t15}ms`)
|
||||
|
||||
|
||||
if (voiceResult.success && voiceResult.rows && voiceResult.rows.length > 0) {
|
||||
const row = voiceResult.rows[0]
|
||||
const silkData = this.decodeVoiceBlob(row.data)
|
||||
if (silkData) {
|
||||
console.log(`[Voice] getVoiceDataFromMediaDb总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return silkData
|
||||
}
|
||||
}
|
||||
@@ -3322,7 +3378,7 @@ class ChatService {
|
||||
senderWxid?: string
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
const startTime = Date.now()
|
||||
console.log(`[Transcribe] 开始转写: sessionId=${sessionId}, msgId=${msgId}, createTime=${createTime}`)
|
||||
|
||||
|
||||
try {
|
||||
let msgCreateTime = createTime
|
||||
@@ -3333,12 +3389,12 @@ class ChatService {
|
||||
const t1 = Date.now()
|
||||
const msgResult = await this.getMessageById(sessionId, parseInt(msgId, 10))
|
||||
const t2 = Date.now()
|
||||
console.log(`[Transcribe] getMessageById: ${t2 - t1}ms`)
|
||||
|
||||
|
||||
if (msgResult.success && msgResult.message) {
|
||||
msgCreateTime = msgResult.message.createTime
|
||||
serverId = msgResult.message.serverId
|
||||
console.log(`[Transcribe] 获取到 createTime=${msgCreateTime}, serverId=${serverId}`)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3349,19 +3405,19 @@ class ChatService {
|
||||
|
||||
// 使用正确的 cacheKey(包含 createTime)
|
||||
const cacheKey = this.getVoiceCacheKey(sessionId, msgId, msgCreateTime)
|
||||
console.log(`[Transcribe] cacheKey=${cacheKey}`)
|
||||
|
||||
|
||||
// 检查转写缓存
|
||||
const cached = this.voiceTranscriptCache.get(cacheKey)
|
||||
if (cached) {
|
||||
console.log(`[Transcribe] 缓存命中,总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return { success: true, transcript: cached }
|
||||
}
|
||||
|
||||
// 检查是否正在转写
|
||||
const pending = this.voiceTranscriptPending.get(cacheKey)
|
||||
if (pending) {
|
||||
console.log(`[Transcribe] 正在转写中,等待结果`)
|
||||
|
||||
return pending
|
||||
}
|
||||
|
||||
@@ -3370,7 +3426,7 @@ class ChatService {
|
||||
// 检查内存中是否有 WAV 数据
|
||||
let wavData = this.voiceWavCache.get(cacheKey)
|
||||
if (wavData) {
|
||||
console.log(`[Transcribe] WAV内存缓存命中,大小: ${wavData.length} bytes`)
|
||||
|
||||
} else {
|
||||
// 检查文件缓存
|
||||
const voiceCacheDir = this.getVoiceCacheDir()
|
||||
@@ -3378,7 +3434,7 @@ class ChatService {
|
||||
if (existsSync(wavFilePath)) {
|
||||
try {
|
||||
wavData = readFileSync(wavFilePath)
|
||||
console.log(`[Transcribe] WAV文件缓存命中,大小: ${wavData.length} bytes`)
|
||||
|
||||
// 同时缓存到内存
|
||||
this.cacheVoiceWav(cacheKey, wavData)
|
||||
} catch (e) {
|
||||
@@ -3388,39 +3444,39 @@ class ChatService {
|
||||
}
|
||||
|
||||
if (!wavData) {
|
||||
console.log(`[Transcribe] WAV缓存未命中,调用 getVoiceData`)
|
||||
|
||||
const t3 = Date.now()
|
||||
// 调用 getVoiceData 获取并解码
|
||||
const voiceResult = await this.getVoiceData(sessionId, msgId, msgCreateTime, serverId, senderWxid)
|
||||
const t4 = Date.now()
|
||||
console.log(`[Transcribe] getVoiceData: ${t4 - t3}ms, success=${voiceResult.success}`)
|
||||
|
||||
|
||||
if (!voiceResult.success || !voiceResult.data) {
|
||||
console.error(`[Transcribe] 语音解码失败: ${voiceResult.error}`)
|
||||
return { success: false, error: voiceResult.error || '语音解码失败' }
|
||||
}
|
||||
wavData = Buffer.from(voiceResult.data, 'base64')
|
||||
console.log(`[Transcribe] WAV数据大小: ${wavData.length} bytes`)
|
||||
|
||||
}
|
||||
|
||||
// 转写
|
||||
console.log(`[Transcribe] 开始调用 transcribeWavBuffer`)
|
||||
|
||||
const t5 = Date.now()
|
||||
const result = await voiceTranscribeService.transcribeWavBuffer(wavData, (text) => {
|
||||
console.log(`[Transcribe] 部分结果: ${text}`)
|
||||
|
||||
onPartial?.(text)
|
||||
})
|
||||
const t6 = Date.now()
|
||||
console.log(`[Transcribe] transcribeWavBuffer: ${t6 - t5}ms, success=${result.success}`)
|
||||
|
||||
|
||||
if (result.success && result.transcript) {
|
||||
console.log(`[Transcribe] 转写成功: ${result.transcript}`)
|
||||
|
||||
this.cacheVoiceTranscript(cacheKey, result.transcript)
|
||||
} else {
|
||||
console.error(`[Transcribe] 转写失败: ${result.error}`)
|
||||
}
|
||||
|
||||
console.log(`[Transcribe] 总耗时: ${Date.now() - startTime}ms`)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`[Transcribe] 异常:`, error)
|
||||
@@ -3468,7 +3524,7 @@ class ChatService {
|
||||
try {
|
||||
// 1. 尝试从缓存获取会话表信息
|
||||
let tables = this.sessionTablesCache.get(sessionId)
|
||||
|
||||
|
||||
if (!tables) {
|
||||
// 缓存未命中,查询数据库
|
||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
||||
|
||||
@@ -74,8 +74,9 @@ class DualReportService {
|
||||
return trimmed
|
||||
}
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
if (suffixMatch) return suffixMatch[1]
|
||||
return trimmed
|
||||
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
private async ensureConnectedWithConfig(
|
||||
@@ -202,7 +203,12 @@ class DualReportService {
|
||||
if (!sender) return false
|
||||
const rawLower = rawWxid ? rawWxid.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(
|
||||
|
||||
@@ -157,8 +157,9 @@ class ExportService {
|
||||
return trimmed
|
||||
}
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
if (suffixMatch) return suffixMatch[1]
|
||||
return trimmed
|
||||
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
||||
@@ -968,11 +969,11 @@ class ExportService {
|
||||
const emojiMd5 = msg.emojiMd5
|
||||
|
||||
if (!emojiUrl && !emojiMd5) {
|
||||
console.log('[ExportService] 表情消息缺少 url 和 md5, localId:', msg.localId, 'content:', msg.content?.substring(0, 200))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[ExportService] 导出表情:', { localId: msg.localId, emojiMd5, emojiUrl: emojiUrl?.substring(0, 100) })
|
||||
|
||||
|
||||
const key = emojiMd5 || String(msg.localId)
|
||||
// 根据 URL 判断扩展名
|
||||
|
||||
@@ -79,8 +79,13 @@ class GroupAnalyticsService {
|
||||
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
||||
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
||||
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 }> {
|
||||
|
||||
@@ -380,9 +380,9 @@ export class ImageDecryptService {
|
||||
}
|
||||
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
if (suffixMatch) return suffixMatch[1]
|
||||
|
||||
return trimmed
|
||||
const cleaned = suffixMatch ? suffixMatch[1] : trimmed
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
private async resolveDatPath(
|
||||
@@ -1136,7 +1136,7 @@ export class ImageDecryptService {
|
||||
// 扫描所有可能的缓存根目录
|
||||
const allRoots = this.getAllCacheRoots()
|
||||
this.logInfo('开始索引缓存', { roots: allRoots.length })
|
||||
|
||||
|
||||
for (const root of allRoots) {
|
||||
try {
|
||||
this.indexCacheDir(root, 3, 0) // 增加深度到3,支持 sessionId/YYYY-MM 结构
|
||||
@@ -1144,7 +1144,7 @@ export class ImageDecryptService {
|
||||
this.logError('索引目录失败', e, { root })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.logInfo('缓存索引完成', { entries: this.resolvedCache.size })
|
||||
this.cacheIndexed = true
|
||||
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, 'WeFlowData', 'Images'))
|
||||
|
||||
|
||||
@@ -116,13 +116,13 @@ export class KeyService {
|
||||
|
||||
// 检查是否已经有本地副本,如果有就使用它
|
||||
if (existsSync(localPath)) {
|
||||
console.log(`使用已存在的 DLL 本地副本: ${localPath}`)
|
||||
|
||||
return localPath
|
||||
}
|
||||
|
||||
console.log(`检测到网络路径 DLL,正在复制到本地: ${originalPath} -> ${localPath}`)
|
||||
|
||||
copyFileSync(originalPath, localPath)
|
||||
console.log('DLL 本地化成功')
|
||||
|
||||
return localPath
|
||||
} catch (e) {
|
||||
console.error('DLL 本地化失败:', e)
|
||||
@@ -146,7 +146,7 @@ export class KeyService {
|
||||
|
||||
// 检查是否为网络路径,如果是则本地化
|
||||
if (this.isNetworkPath(dllPath)) {
|
||||
console.log('检测到网络路径,将进行本地化处理')
|
||||
|
||||
dllPath = this.localizeNetworkDll(dllPath)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ export class KeyService {
|
||||
if (pid) {
|
||||
const runPath = await this.getProcessExecutablePath(pid)
|
||||
if (runPath && existsSync(runPath)) {
|
||||
console.log('发现正在运行的微信进程,使用路径:', 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 }> {
|
||||
console.log('[SnsService] getTimeline called with:', { 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) {
|
||||
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 }
|
||||
}
|
||||
|
||||
console.log('[SnsService] Returning result:', result)
|
||||
|
||||
return result
|
||||
}
|
||||
async debugResource(url: string): Promise<{ success: boolean; status?: number; headers?: any; error?: string }> {
|
||||
|
||||
@@ -224,12 +224,12 @@ export class VoiceTranscribeService {
|
||||
let finalTranscript = ''
|
||||
|
||||
worker.on('message', (msg: any) => {
|
||||
console.log('[VoiceTranscribe] Worker 消息:', msg)
|
||||
|
||||
if (msg.type === 'partial') {
|
||||
onPartial?.(msg.text)
|
||||
} else if (msg.type === 'final') {
|
||||
finalTranscript = msg.text
|
||||
console.log('[VoiceTranscribe] 最终文本:', finalTranscript)
|
||||
|
||||
resolve({ success: true, transcript: finalTranscript })
|
||||
worker.terminate()
|
||||
} else if (msg.type === 'error') {
|
||||
|
||||
@@ -60,6 +60,10 @@ export class WcdbCore {
|
||||
private wcdbGetSnsTimeline: any = null
|
||||
private wcdbGetSnsAnnualStats: 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 readonly avatarCacheTtlMs = 10 * 60 * 1000
|
||||
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 路径
|
||||
*/
|
||||
@@ -122,7 +256,7 @@ export class WcdbCore {
|
||||
if (!force && !this.isLogEnabled()) return
|
||||
const line = `[${new Date().toISOString()}] ${message}`
|
||||
// 同时输出到控制台和文件
|
||||
console.log('[WCDB]', message)
|
||||
|
||||
try {
|
||||
const base = this.userDataPath || process.env.WCDB_LOG_DIR || process.cwd()
|
||||
const dir = join(base, 'logs')
|
||||
@@ -262,10 +396,10 @@ export class WcdbCore {
|
||||
let protectionOk = false
|
||||
for (const resPath of resourcePaths) {
|
||||
try {
|
||||
// console.log(`[WCDB] 尝试 InitProtection: ${resPath}`)
|
||||
//
|
||||
protectionOk = this.wcdbInitProtection(resPath)
|
||||
if (protectionOk) {
|
||||
// console.log(`[WCDB] InitProtection 成功: ${resPath}`)
|
||||
//
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -454,6 +588,17 @@ export class WcdbCore {
|
||||
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)
|
||||
try {
|
||||
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 userDataPath: string | null = null
|
||||
private logEnabled = false
|
||||
private monitorListener: ((type: string, json: string) => void) | null = null
|
||||
|
||||
constructor() {
|
||||
this.initWorker()
|
||||
@@ -47,8 +48,16 @@ export class WcdbService {
|
||||
try {
|
||||
this.worker = new Worker(finalPath)
|
||||
|
||||
this.worker.on('message', (msg: WorkerMessage) => {
|
||||
const { id, result, error } = msg
|
||||
this.worker.on('message', (msg: any) => {
|
||||
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)
|
||||
if (p) {
|
||||
this.pending.delete(id)
|
||||
@@ -122,6 +131,15 @@ export class WcdbService {
|
||||
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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新消息(增量刷新)
|
||||
*/
|
||||
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
|
||||
console.log('[TranscribeWorker] 检测到语言标记:', langTag)
|
||||
|
||||
|
||||
// 检查是否在允许的语言列表中
|
||||
for (const lang of allowedLanguages) {
|
||||
if (LANGUAGE_TAGS[lang] === langTag) {
|
||||
console.log('[TranscribeWorker] 语言匹配,允许:', lang)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[TranscribeWorker] 语言不在白名单中,过滤掉')
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ async function run() {
|
||||
allowedLanguages = ['zh']
|
||||
}
|
||||
|
||||
console.log('[TranscribeWorker] 使用的语言白名单:', allowedLanguages)
|
||||
|
||||
|
||||
// 1. 初始化识别器 (SenseVoiceSmall)
|
||||
const recognizerConfig = {
|
||||
@@ -145,15 +145,15 @@ async function run() {
|
||||
recognizer.decode(stream)
|
||||
const result = recognizer.getResult(stream)
|
||||
|
||||
console.log('[TranscribeWorker] 识别完成 - 结果对象:', JSON.stringify(result, null, 2))
|
||||
|
||||
|
||||
// 3. 检查语言是否在白名单中
|
||||
if (isLanguageAllowed(result, allowedLanguages)) {
|
||||
const processedText = richTranscribePostProcess(result.text)
|
||||
console.log('[TranscribeWorker] 语言匹配,返回文本:', processedText)
|
||||
|
||||
parentPort.postMessage({ type: 'final', text: processedText })
|
||||
} else {
|
||||
console.log('[TranscribeWorker] 语言不匹配,返回空文本')
|
||||
|
||||
parentPort.postMessage({ type: 'final', text: '' })
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ if (parentPort) {
|
||||
core.setLogEnabled(payload.enabled)
|
||||
result = { success: true }
|
||||
break
|
||||
case 'setMonitor':
|
||||
core.setMonitor((type, json) => {
|
||||
parentPort!.postMessage({
|
||||
id: -1,
|
||||
type: 'monitor',
|
||||
payload: { type, json }
|
||||
})
|
||||
})
|
||||
result = { success: true }
|
||||
break
|
||||
case 'testConnection':
|
||||
result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid)
|
||||
break
|
||||
@@ -38,6 +48,9 @@ if (parentPort) {
|
||||
case 'getMessages':
|
||||
result = await core.getMessages(payload.sessionId, payload.limit, payload.offset)
|
||||
break
|
||||
case 'getNewMessages':
|
||||
result = await core.getNewMessages(payload.sessionId, payload.minTime, payload.limit)
|
||||
break
|
||||
case 'getMessageCount':
|
||||
result = await core.getMessageCount(payload.sessionId)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user