mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat: 一些适配
This commit is contained in:
@@ -58,6 +58,26 @@ export interface Message {
|
||||
encrypVer?: number
|
||||
cdnThumbUrl?: string
|
||||
voiceDurationSeconds?: number
|
||||
// Type 49 细分字段
|
||||
linkTitle?: string // 链接/文件标题
|
||||
linkUrl?: string // 链接 URL
|
||||
linkThumb?: string // 链接缩略图
|
||||
fileName?: string // 文件名
|
||||
fileSize?: number // 文件大小
|
||||
fileExt?: string // 文件扩展名
|
||||
xmlType?: string // XML 中的 type 字段
|
||||
// 名片消息
|
||||
cardUsername?: string // 名片的微信ID
|
||||
cardNickname?: string // 名片的昵称
|
||||
// 聊天记录
|
||||
chatRecordTitle?: string // 聊天记录标题
|
||||
chatRecordList?: Array<{
|
||||
datatype: number
|
||||
sourcename: string
|
||||
sourcetime: string
|
||||
datadesc: string
|
||||
datatitle?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
@@ -106,6 +126,9 @@ class ChatService {
|
||||
timeColumn?: string
|
||||
name2IdTable?: string
|
||||
}>()
|
||||
// 缓存会话表信息,避免每次查询
|
||||
private sessionTablesCache = new Map<string, Array<{ tableName: string; dbPath: string }>>()
|
||||
private readonly sessionTablesCacheTtl = 300000 // 5分钟
|
||||
|
||||
constructor() {
|
||||
this.configService = new ConfigService()
|
||||
@@ -1023,6 +1046,26 @@ class ChatService {
|
||||
let encrypVer: number | undefined
|
||||
let cdnThumbUrl: string | undefined
|
||||
let voiceDurationSeconds: number | undefined
|
||||
// Type 49 细分字段
|
||||
let linkTitle: string | undefined
|
||||
let linkUrl: string | undefined
|
||||
let linkThumb: string | undefined
|
||||
let fileName: string | undefined
|
||||
let fileSize: number | undefined
|
||||
let fileExt: string | undefined
|
||||
let xmlType: string | undefined
|
||||
// 名片消息
|
||||
let cardUsername: string | undefined
|
||||
let cardNickname: string | undefined
|
||||
// 聊天记录
|
||||
let chatRecordTitle: string | undefined
|
||||
let chatRecordList: Array<{
|
||||
datatype: number
|
||||
sourcename: string
|
||||
sourcetime: string
|
||||
datadesc: string
|
||||
datatitle?: string
|
||||
}> | undefined
|
||||
|
||||
if (localType === 47 && content) {
|
||||
const emojiInfo = this.parseEmojiInfo(content)
|
||||
@@ -1040,6 +1083,23 @@ class ChatService {
|
||||
videoMd5 = this.parseVideoMd5(content)
|
||||
} else if (localType === 34 && content) {
|
||||
voiceDurationSeconds = this.parseVoiceDurationSeconds(content)
|
||||
} else if (localType === 42 && content) {
|
||||
// 名片消息
|
||||
const cardInfo = this.parseCardInfo(content)
|
||||
cardUsername = cardInfo.username
|
||||
cardNickname = cardInfo.nickname
|
||||
} else if (localType === 49 && content) {
|
||||
// Type 49 消息(链接、文件、小程序、转账等)
|
||||
const type49Info = this.parseType49Message(content)
|
||||
xmlType = type49Info.xmlType
|
||||
linkTitle = type49Info.linkTitle
|
||||
linkUrl = type49Info.linkUrl
|
||||
linkThumb = type49Info.linkThumb
|
||||
fileName = type49Info.fileName
|
||||
fileSize = type49Info.fileSize
|
||||
fileExt = type49Info.fileExt
|
||||
chatRecordTitle = type49Info.chatRecordTitle
|
||||
chatRecordList = type49Info.chatRecordList
|
||||
} else if (localType === 244813135921 || (content && content.includes('<type>57</type>'))) {
|
||||
const quoteInfo = this.parseQuoteMessage(content)
|
||||
quotedContent = quoteInfo.content
|
||||
@@ -1066,7 +1126,18 @@ class ChatService {
|
||||
voiceDurationSeconds,
|
||||
aesKey,
|
||||
encrypVer,
|
||||
cdnThumbUrl
|
||||
cdnThumbUrl,
|
||||
linkTitle,
|
||||
linkUrl,
|
||||
linkThumb,
|
||||
fileName,
|
||||
fileSize,
|
||||
fileExt,
|
||||
xmlType,
|
||||
cardUsername,
|
||||
cardNickname,
|
||||
chatRecordTitle,
|
||||
chatRecordList
|
||||
})
|
||||
const last = messages[messages.length - 1]
|
||||
if ((last.localType === 3 || last.localType === 34) && (last.localId === 0 || last.createTime === 0)) {
|
||||
@@ -1164,17 +1235,35 @@ class ChatService {
|
||||
return `[链接] ${title}`
|
||||
case '6':
|
||||
return `[文件] ${title}`
|
||||
case '19':
|
||||
return `[聊天记录] ${title}`
|
||||
case '33':
|
||||
case '36':
|
||||
return `[小程序] ${title}`
|
||||
case '57':
|
||||
// 引用消息,title 就是回复的内容
|
||||
return title
|
||||
case '2000':
|
||||
return `[转账] ${title}`
|
||||
default:
|
||||
return title
|
||||
}
|
||||
}
|
||||
return '[消息]'
|
||||
|
||||
// 如果没有 title,根据 type 返回默认标签
|
||||
switch (type) {
|
||||
case '6':
|
||||
return '[文件]'
|
||||
case '19':
|
||||
return '[聊天记录]'
|
||||
case '33':
|
||||
case '36':
|
||||
return '[小程序]'
|
||||
case '2000':
|
||||
return '[转账]'
|
||||
default:
|
||||
return '[消息]'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1458,6 +1547,185 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析名片消息
|
||||
* 格式: <msg username="wxid_xxx" nickname="昵称" ... />
|
||||
*/
|
||||
private parseCardInfo(content: string): { username?: string; nickname?: string } {
|
||||
try {
|
||||
if (!content) return {}
|
||||
|
||||
// 提取 username
|
||||
const username = this.extractXmlAttribute(content, 'msg', 'username') || undefined
|
||||
|
||||
// 提取 nickname
|
||||
const nickname = this.extractXmlAttribute(content, 'msg', 'nickname') || undefined
|
||||
|
||||
return { username, nickname }
|
||||
} catch (e) {
|
||||
console.error('[ChatService] 名片解析失败:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Type 49 消息(链接、文件、小程序、转账等)
|
||||
* 根据 <appmsg><type>X</type> 区分不同类型
|
||||
*/
|
||||
private parseType49Message(content: string): {
|
||||
xmlType?: string
|
||||
linkTitle?: string
|
||||
linkUrl?: string
|
||||
linkThumb?: string
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
fileExt?: string
|
||||
chatRecordTitle?: string
|
||||
chatRecordList?: Array<{
|
||||
datatype: number
|
||||
sourcename: string
|
||||
sourcetime: string
|
||||
datadesc: string
|
||||
datatitle?: string
|
||||
}>
|
||||
} {
|
||||
try {
|
||||
if (!content) return {}
|
||||
|
||||
// 提取 appmsg 中的 type
|
||||
const xmlType = this.extractXmlValue(content, 'type')
|
||||
if (!xmlType) return {}
|
||||
|
||||
const result: any = { xmlType }
|
||||
|
||||
// 提取通用字段
|
||||
const title = this.extractXmlValue(content, 'title')
|
||||
const url = this.extractXmlValue(content, 'url')
|
||||
|
||||
switch (xmlType) {
|
||||
case '6': {
|
||||
// 文件消息
|
||||
result.fileName = title || this.extractXmlValue(content, 'filename')
|
||||
result.linkTitle = result.fileName
|
||||
|
||||
// 提取文件大小
|
||||
const fileSizeStr = this.extractXmlValue(content, 'totallen') ||
|
||||
this.extractXmlValue(content, 'filesize')
|
||||
if (fileSizeStr) {
|
||||
const size = parseInt(fileSizeStr, 10)
|
||||
if (!isNaN(size)) {
|
||||
result.fileSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// 提取文件扩展名
|
||||
const fileExt = this.extractXmlValue(content, 'fileext')
|
||||
if (fileExt) {
|
||||
result.fileExt = fileExt
|
||||
} else if (result.fileName) {
|
||||
// 从文件名提取扩展名
|
||||
const match = /\.([^.]+)$/.exec(result.fileName)
|
||||
if (match) {
|
||||
result.fileExt = match[1]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case '19': {
|
||||
// 聊天记录
|
||||
result.chatRecordTitle = title || '聊天记录'
|
||||
|
||||
// 解析聊天记录列表
|
||||
const recordList: Array<{
|
||||
datatype: number
|
||||
sourcename: string
|
||||
sourcetime: string
|
||||
datadesc: string
|
||||
datatitle?: string
|
||||
}> = []
|
||||
|
||||
// 查找所有 <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')
|
||||
const datadesc = this.extractXmlValue(itemXml, 'datadesc')
|
||||
const datatitle = this.extractXmlValue(itemXml, 'datatitle')
|
||||
|
||||
if (sourcename && datadesc) {
|
||||
recordList.push({
|
||||
datatype: datatypeStr ? parseInt(datatypeStr, 10) : 0,
|
||||
sourcename,
|
||||
sourcetime: sourcetime || '',
|
||||
datadesc,
|
||||
datatitle: datatitle || undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (recordList.length > 0) {
|
||||
result.chatRecordList = recordList
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case '33':
|
||||
case '36': {
|
||||
// 小程序
|
||||
result.linkTitle = title
|
||||
result.linkUrl = url
|
||||
|
||||
// 提取缩略图
|
||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
if (thumbUrl) {
|
||||
result.linkThumb = thumbUrl
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
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) {
|
||||
result.linkTitle = feedesc
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// 其他类型,提取通用字段
|
||||
result.linkTitle = title
|
||||
result.linkUrl = url
|
||||
|
||||
const thumbUrl = this.extractXmlValue(content, 'thumburl') ||
|
||||
this.extractXmlValue(content, 'cdnthumburl')
|
||||
if (thumbUrl) {
|
||||
result.linkThumb = thumbUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('[ChatService] Type 49 消息解析失败:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
//手动查找 media_*.db 文件(当 WCDB DLL 不支持 listMediaDbs 时的 fallback)
|
||||
private async findMediaDbsManually(): Promise<string[]> {
|
||||
try {
|
||||
@@ -3198,19 +3466,35 @@ class ChatService {
|
||||
|
||||
async getMessageById(sessionId: string, localId: number): Promise<{ success: boolean; message?: Message; error?: string }> {
|
||||
try {
|
||||
// 1. 获取该会话所在的消息表
|
||||
// 注意:这里使用 getMessageTableStats 而不是 getMessageTables,因为前者包含 db_path
|
||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
||||
if (!tableStats.success || !tableStats.tables || tableStats.tables.length === 0) {
|
||||
return { success: false, error: '未找到会话消息表' }
|
||||
// 1. 尝试从缓存获取会话表信息
|
||||
let tables = this.sessionTablesCache.get(sessionId)
|
||||
|
||||
if (!tables) {
|
||||
// 缓存未命中,查询数据库
|
||||
const tableStats = await wcdbService.getMessageTableStats(sessionId)
|
||||
if (!tableStats.success || !tableStats.tables || tableStats.tables.length === 0) {
|
||||
return { success: false, error: '未找到会话消息表' }
|
||||
}
|
||||
|
||||
// 提取表信息并缓存
|
||||
tables = tableStats.tables
|
||||
.map(t => ({
|
||||
tableName: t.table_name || t.name,
|
||||
dbPath: t.db_path
|
||||
}))
|
||||
.filter(t => t.tableName && t.dbPath) as Array<{ tableName: string; dbPath: string }>
|
||||
|
||||
if (tables.length > 0) {
|
||||
this.sessionTablesCache.set(sessionId, tables)
|
||||
// 设置过期清理
|
||||
setTimeout(() => {
|
||||
this.sessionTablesCache.delete(sessionId)
|
||||
}, this.sessionTablesCacheTtl)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 遍历表查找消息 (通常只有一个主表,但可能有归档)
|
||||
for (const tableInfo of tableStats.tables) {
|
||||
const tableName = tableInfo.table_name || tableInfo.name
|
||||
const dbPath = tableInfo.db_path
|
||||
if (!tableName || !dbPath) continue
|
||||
|
||||
for (const { tableName, dbPath } of tables) {
|
||||
// 构造查询
|
||||
const sql = `SELECT * FROM ${tableName} WHERE local_id = ${localId} LIMIT 1`
|
||||
const result = await wcdbService.execQuery('message', dbPath, sql)
|
||||
|
||||
Reference in New Issue
Block a user