mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-19 21:00:25 +08:00
feat: 支持头像导入
This commit is contained in:
@@ -70,7 +70,9 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
imported_at INTEGER NOT NULL
|
||||
imported_at INTEGER NOT NULL,
|
||||
group_id TEXT,
|
||||
group_avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
@@ -78,7 +80,8 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
platform_id TEXT NOT NULL UNIQUE,
|
||||
account_name TEXT,
|
||||
group_nickname TEXT,
|
||||
aliases TEXT DEFAULT '[]'
|
||||
aliases TEXT DEFAULT '[]',
|
||||
avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_name_history (
|
||||
@@ -134,18 +137,20 @@ export function importData(parseResult: ParseResult): string {
|
||||
try {
|
||||
const importTransaction = db.transaction(() => {
|
||||
const insertMeta = db.prepare(`
|
||||
INSERT INTO meta (name, platform, type, imported_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
insertMeta.run(
|
||||
parseResult.meta.name,
|
||||
parseResult.meta.platform,
|
||||
parseResult.meta.type,
|
||||
Math.floor(Date.now() / 1000)
|
||||
Math.floor(Date.now() / 1000),
|
||||
parseResult.meta.groupId || null,
|
||||
parseResult.meta.groupAvatar || null
|
||||
)
|
||||
|
||||
const insertMember = db.prepare(`
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, avatar) VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
const getMemberId = db.prepare(`
|
||||
SELECT id FROM member WHERE platform_id = ?
|
||||
@@ -154,7 +159,7 @@ export function importData(parseResult: ParseResult): string {
|
||||
const memberIdMap = new Map<string, number>()
|
||||
|
||||
for (const member of parseResult.members) {
|
||||
insertMember.run(member.platformId, member.accountName || null, member.groupNickname || null)
|
||||
insertMember.run(member.platformId, member.accountName || null, member.groupNickname || null, member.avatar || null)
|
||||
const row = getMemberId.get(member.platformId) as { id: number }
|
||||
memberIdMap.set(member.platformId, row.id)
|
||||
}
|
||||
@@ -312,6 +317,8 @@ export function getAllSessions(): AnalysisSession[] {
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath,
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -365,6 +372,8 @@ export function getSession(sessionId: string): AnalysisSession | null {
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath: getDbPath(sessionId),
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
}
|
||||
} finally {
|
||||
db.close()
|
||||
|
||||
@@ -391,11 +391,16 @@ export async function mergeFilesWithTempDb(
|
||||
if (member.groupNickname) {
|
||||
existing.groupNickname = member.groupNickname
|
||||
}
|
||||
// 头像使用最新的(覆盖更新)
|
||||
if (member.avatar) {
|
||||
existing.avatar = member.avatar
|
||||
}
|
||||
} else {
|
||||
memberMap.set(member.platformId, {
|
||||
platformId: member.platformId,
|
||||
accountName: member.accountName,
|
||||
groupNickname: member.groupNickname,
|
||||
avatar: member.avatar,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -453,6 +458,14 @@ export async function mergeFilesWithTempDb(
|
||||
const platforms = new Set(parseResults.map((r) => r.meta.platform))
|
||||
const platform = platforms.size === 1 ? parseResults[0].meta.platform : 'mixed'
|
||||
|
||||
// 确定群ID和群头像(仅当所有文件都来自同一个群时保留)
|
||||
const groupIds = new Set(parseResults.map((r) => r.meta.groupId).filter(Boolean))
|
||||
const groupId = groupIds.size === 1 ? parseResults.find((r) => r.meta.groupId)?.meta.groupId : undefined
|
||||
// 如果有唯一群ID,使用最后一个文件的群头像(可能是最新的)
|
||||
const groupAvatar = groupId
|
||||
? parseResults.filter((r) => r.meta.groupId === groupId).pop()?.meta.groupAvatar
|
||||
: undefined
|
||||
|
||||
// 构建来源信息
|
||||
const sources: MergeSource[] = parseResults.map(({ reader, source, meta }) => ({
|
||||
filename: source,
|
||||
@@ -463,15 +476,18 @@ export async function mergeFilesWithTempDb(
|
||||
// 构建 ChatLab 格式
|
||||
const chatLabData: ChatLabFormat = {
|
||||
chatlab: {
|
||||
version: '1.0.0',
|
||||
version: '0.0.1',
|
||||
exportedAt: Math.floor(Date.now() / 1000),
|
||||
generator: 'ChatLab Merge Tool',
|
||||
description: `合并自 ${parseResults.length} 个文件`,
|
||||
},
|
||||
meta: {
|
||||
name: outputName,
|
||||
platform: platform as ChatPlatform,
|
||||
type: parseResults[0].meta.type as ChatType,
|
||||
sources,
|
||||
groupId,
|
||||
groupAvatar,
|
||||
},
|
||||
members: Array.from(memberMap.values()),
|
||||
messages: mergedMessages,
|
||||
@@ -497,11 +513,14 @@ export async function mergeFilesWithTempDb(
|
||||
name: chatLabData.meta.name,
|
||||
platform: chatLabData.meta.platform,
|
||||
type: chatLabData.meta.type,
|
||||
groupId: chatLabData.meta.groupId,
|
||||
groupAvatar: chatLabData.meta.groupAvatar,
|
||||
},
|
||||
members: chatLabData.members.map((m) => ({
|
||||
platformId: m.platformId,
|
||||
accountName: m.accountName,
|
||||
groupNickname: m.groupNickname,
|
||||
avatar: m.avatar,
|
||||
})),
|
||||
messages: chatLabData.messages.map((msg) => ({
|
||||
senderPlatformId: msg.sender,
|
||||
|
||||
@@ -58,13 +58,16 @@ export function createTempDatabase(dbPath: string): Database.Database {
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
type TEXT NOT NULL,
|
||||
group_id TEXT,
|
||||
group_avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
platform_id TEXT PRIMARY KEY,
|
||||
account_name TEXT,
|
||||
group_nickname TEXT
|
||||
group_nickname TEXT,
|
||||
avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message (
|
||||
@@ -101,10 +104,10 @@ export class TempDbWriter {
|
||||
|
||||
// 准备语句
|
||||
this.insertMeta = this.db.prepare(`
|
||||
INSERT INTO meta (name, platform, type) VALUES (?, ?, ?)
|
||||
INSERT INTO meta (name, platform, type, group_id, group_avatar) VALUES (?, ?, ?, ?, ?)
|
||||
`)
|
||||
this.insertMember = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, avatar) VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
this.insertMessage = this.db.prepare(`
|
||||
INSERT INTO message (sender_platform_id, sender_account_name, sender_group_nickname, timestamp, type, content)
|
||||
@@ -119,7 +122,7 @@ export class TempDbWriter {
|
||||
* 写入元信息
|
||||
*/
|
||||
writeMeta(meta: ParsedMeta): void {
|
||||
this.insertMeta.run(meta.name, meta.platform, meta.type)
|
||||
this.insertMeta.run(meta.name, meta.platform, meta.type, meta.groupId || null, meta.groupAvatar || null)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +132,7 @@ export class TempDbWriter {
|
||||
for (const m of members) {
|
||||
if (!this.memberSet.has(m.platformId)) {
|
||||
this.memberSet.add(m.platformId)
|
||||
this.insertMember.run(m.platformId, m.accountName || null, m.groupNickname || null)
|
||||
this.insertMember.run(m.platformId, m.accountName || null, m.groupNickname || null, m.avatar || null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,10 +142,10 @@ export class TempDbWriter {
|
||||
*/
|
||||
writeMessages(messages: ParsedMessage[]): void {
|
||||
for (const msg of messages) {
|
||||
// 确保成员存在
|
||||
// 确保成员存在(消息中没有头像信息,设为 null)
|
||||
if (!this.memberSet.has(msg.senderPlatformId)) {
|
||||
this.memberSet.add(msg.senderPlatformId)
|
||||
this.insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null)
|
||||
this.insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null, null)
|
||||
}
|
||||
|
||||
this.insertMessage.run(
|
||||
@@ -202,13 +205,15 @@ export class TempDbReader {
|
||||
*/
|
||||
getMeta(): ParsedMeta | null {
|
||||
const row = this.db.prepare('SELECT * FROM meta LIMIT 1').get() as
|
||||
| { name: string; platform: string; type: string }
|
||||
| { name: string; platform: string; type: string; group_id: string | null; group_avatar: string | null }
|
||||
| undefined
|
||||
if (!row) return null
|
||||
return {
|
||||
name: row.name,
|
||||
platform: row.platform,
|
||||
type: row.type as 'group' | 'private',
|
||||
groupId: row.group_id || undefined,
|
||||
groupAvatar: row.group_avatar || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,11 +225,13 @@ export class TempDbReader {
|
||||
platform_id: string
|
||||
account_name: string | null
|
||||
group_nickname: string | null
|
||||
avatar: string | null
|
||||
}>
|
||||
return rows.map((r) => ({
|
||||
platformId: r.platform_id,
|
||||
accountName: r.account_name || r.platform_id, // 如果没有账号名称,使用 platformId
|
||||
groupNickname: r.group_nickname || undefined,
|
||||
avatar: r.avatar || undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ interface ChatLabMember {
|
||||
accountName: string // 账号名称
|
||||
groupNickname?: string // 群昵称
|
||||
aliases?: string[]
|
||||
avatar?: string // 头像(base64 Data URL)
|
||||
}
|
||||
|
||||
// ==================== 解析器实现 ====================
|
||||
@@ -125,6 +126,8 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
|
||||
name: metaObj.name || '未知群聊',
|
||||
platform: (metaObj.platform as ChatPlatform) || ChatPlatform.UNKNOWN,
|
||||
type: (metaObj.type as ChatType) || ChatType.GROUP,
|
||||
groupId: metaObj.groupId,
|
||||
groupAvatar: metaObj.groupAvatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +153,7 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
|
||||
platformId: m.platformId,
|
||||
accountName: m.accountName,
|
||||
groupNickname: m.groupNickname,
|
||||
avatar: m.avatar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ interface MemberInfo {
|
||||
platformId: string
|
||||
accountName: string // 账号名称(QQ原始昵称 sendNickName)
|
||||
groupNickname: string | undefined // 群昵称(sendMemberName,可为空)
|
||||
avatar: string | undefined // 头像(base64 Data URL)
|
||||
}
|
||||
|
||||
// ==================== 消息类型转换 ====================
|
||||
@@ -271,6 +272,163 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
|
||||
// 如果无法获取 senders 数量,默认为群聊(群聊是更常见的使用场景)
|
||||
const chatType = sendersCount > 0 ? (sendersCount > 2 ? ChatType.GROUP : ChatType.PRIVATE) : ChatType.GROUP // 默认为群聊
|
||||
|
||||
// 解析 avatars 对象(头像)
|
||||
// avatars 格式:{ "uin1": "data:image/jpeg;base64,...", "uin2": "..." }
|
||||
// 注意:base64 字符串很长,需要特殊处理匹配花括号
|
||||
const avatarsMap = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* 从字符串中提取 avatars 对象内容
|
||||
* 正确处理 JSON 字符串中的花括号匹配(考虑字符串内的转义字符)
|
||||
*/
|
||||
function extractAvatarsObject(content: string): string | null {
|
||||
const searchStr = '"avatars":'
|
||||
const startIdx = content.indexOf(searchStr)
|
||||
if (startIdx === -1) return null
|
||||
|
||||
let i = startIdx + searchStr.length
|
||||
// 跳过空白字符
|
||||
while (i < content.length && /\s/.test(content[i])) i++
|
||||
|
||||
if (content[i] !== '{') return null
|
||||
|
||||
// 从 { 开始匹配
|
||||
let braceDepth = 0
|
||||
let inString = false
|
||||
let escape = false
|
||||
const objStart = i
|
||||
|
||||
for (; i < content.length; i++) {
|
||||
const char = content[i]
|
||||
|
||||
if (escape) {
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '\\' && inString) {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') braceDepth++
|
||||
if (char === '}') {
|
||||
braceDepth--
|
||||
if (braceDepth === 0) {
|
||||
return content.slice(objStart, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// 先尝试从文件头解析(适用于成员较少的聊天)
|
||||
const avatarsContent = extractAvatarsObject(headContent)
|
||||
if (avatarsContent) {
|
||||
const avatarsObj = JSON.parse(avatarsContent) as Record<string, string>
|
||||
for (const [uin, avatar] of Object.entries(avatarsObj)) {
|
||||
if (avatar && typeof avatar === 'string' && avatar.startsWith('data:image/')) {
|
||||
avatarsMap.set(uin, avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// avatars 解析失败,继续不带头像
|
||||
}
|
||||
|
||||
// 如果文件头没有完整的 avatars(可能超出 100KB),尝试流式读取
|
||||
if (avatarsMap.size === 0) {
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
const avatarStream = fs.createReadStream(filePath, { encoding: 'utf-8' })
|
||||
|
||||
let avatarsContent = ''
|
||||
let inAvatars = false
|
||||
let braceDepth = 0
|
||||
let inString = false
|
||||
let escape = false
|
||||
|
||||
avatarStream.on('data', (chunk: string | Buffer) => {
|
||||
const str = typeof chunk === 'string' ? chunk : chunk.toString()
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i]
|
||||
|
||||
if (!inAvatars) {
|
||||
// 查找 "avatars": 的位置
|
||||
const searchStr = '"avatars":'
|
||||
if (str.slice(i, i + searchStr.length) === searchStr) {
|
||||
inAvatars = true
|
||||
// 跳过 "avatars": 和可能的空白
|
||||
i += searchStr.length - 1
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 开始收集 avatars 对象内容
|
||||
avatarsContent += char
|
||||
|
||||
if (escape) {
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '\\' && inString) {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') braceDepth++
|
||||
if (char === '}') {
|
||||
braceDepth--
|
||||
if (braceDepth === 0) {
|
||||
// avatars 对象结束
|
||||
avatarStream.destroy()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
avatarStream.on('close', () => {
|
||||
if (avatarsContent) {
|
||||
try {
|
||||
const avatarsObj = JSON.parse(avatarsContent) as Record<string, string>
|
||||
for (const [uin, avatar] of Object.entries(avatarsObj)) {
|
||||
if (avatar && typeof avatar === 'string' && avatar.startsWith('data:image/')) {
|
||||
avatarsMap.set(uin, avatar)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析失败
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
avatarStream.on('error', () => resolve())
|
||||
})
|
||||
} catch {
|
||||
// 流式解析失败,继续不带头像
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 meta
|
||||
const meta: ParsedMeta = {
|
||||
name: chatInfo.name === '未知群聊' ? extractNameFromFilePath(filePath) : chatInfo.name,
|
||||
@@ -303,6 +461,9 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
|
||||
const accountName = raw?.sendNickName || msg.sender.name || platformId // QQ 原始昵称
|
||||
const groupNickname = raw?.sendMemberName || undefined // 群昵称(可为空)
|
||||
|
||||
// 获取头像(通过 uin 查找)
|
||||
const avatar = avatarsMap.get(platformId)
|
||||
|
||||
// 更新成员信息(保留最新的名字)
|
||||
const existingMember = memberMap.get(platformId)
|
||||
if (!existingMember) {
|
||||
@@ -310,6 +471,7 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
|
||||
platformId,
|
||||
accountName,
|
||||
groupNickname,
|
||||
avatar,
|
||||
})
|
||||
} else {
|
||||
// 更新为最新的名字
|
||||
@@ -317,6 +479,10 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
|
||||
if (groupNickname) {
|
||||
existingMember.groupNickname = groupNickname
|
||||
}
|
||||
// 头像使用最新的(覆盖更新)
|
||||
if (avatar) {
|
||||
existingMember.avatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
// 解析时间戳
|
||||
@@ -386,6 +552,7 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
|
||||
platformId: m.platformId,
|
||||
accountName: m.accountName,
|
||||
groupNickname: m.groupNickname,
|
||||
avatar: m.avatar,
|
||||
}))
|
||||
yield { type: 'members', data: members }
|
||||
|
||||
|
||||
@@ -80,9 +80,17 @@ interface EchotraceMessage {
|
||||
isSend: number | null // 0=接收, 1=发送, null=系统
|
||||
senderUsername: string // 发送者微信ID
|
||||
senderDisplayName: string // 发送者显示名
|
||||
senderAvatarKey: string // 头像查找 key(通常与 senderUsername 相同)
|
||||
source: string
|
||||
}
|
||||
|
||||
// ==================== 头像信息结构 ====================
|
||||
|
||||
interface EchotraceAvatarInfo {
|
||||
displayName: string
|
||||
base64: string // 原始 base64,不包含 Data URL 前缀
|
||||
}
|
||||
|
||||
// ==================== 消息类型映射 ====================
|
||||
|
||||
/**
|
||||
@@ -133,6 +141,7 @@ function convertMessageType(typeStr: string): MessageType {
|
||||
interface MemberInfo {
|
||||
platformId: string
|
||||
accountName: string
|
||||
avatar: string | undefined // 头像(base64 Data URL)
|
||||
}
|
||||
|
||||
// ==================== 解析器实现 ====================
|
||||
@@ -180,11 +189,178 @@ async function* parseEchotrace(options: ParseOptions): AsyncGenerator<ParseEvent
|
||||
// 确定聊天名称
|
||||
const chatName = session?.displayName || session?.nickname || extractNameFromFilePath(filePath)
|
||||
|
||||
// 提取群ID(群聊类型时有值)
|
||||
// 群ID 格式:以 @chatroom 结尾
|
||||
const groupId = chatType === ChatType.GROUP && session?.wxid ? session.wxid : undefined
|
||||
|
||||
// 解析 avatars 对象(头像)
|
||||
// avatars 格式:{ "wxid": { "displayName": "...", "base64": "..." } }
|
||||
// 注意:base64 不包含 Data URL 前缀,需要添加
|
||||
const avatarsMap = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* 从字符串中提取 avatars 对象内容
|
||||
* 正确处理 JSON 字符串中的花括号匹配(考虑字符串内的转义字符)
|
||||
*/
|
||||
function extractAvatarsObject(content: string): string | null {
|
||||
const searchStr = '"avatars":'
|
||||
const startIdx = content.indexOf(searchStr)
|
||||
if (startIdx === -1) return null
|
||||
|
||||
let i = startIdx + searchStr.length
|
||||
// 跳过空白字符
|
||||
while (i < content.length && /\s/.test(content[i])) i++
|
||||
|
||||
if (content[i] !== '{') return null
|
||||
|
||||
// 从 { 开始匹配
|
||||
let braceDepth = 0
|
||||
let inString = false
|
||||
let escape = false
|
||||
const objStart = i
|
||||
|
||||
for (; i < content.length; i++) {
|
||||
const char = content[i]
|
||||
|
||||
if (escape) {
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '\\' && inString) {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') braceDepth++
|
||||
if (char === '}') {
|
||||
braceDepth--
|
||||
if (braceDepth === 0) {
|
||||
return content.slice(objStart, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// 先尝试从文件头解析(适用于成员较少的聊天)
|
||||
const avatarsContent = extractAvatarsObject(headContent)
|
||||
if (avatarsContent) {
|
||||
const avatarsObj = JSON.parse(avatarsContent) as Record<string, EchotraceAvatarInfo>
|
||||
for (const [wxid, avatarInfo] of Object.entries(avatarsObj)) {
|
||||
if (avatarInfo && typeof avatarInfo === 'object' && avatarInfo.base64) {
|
||||
// 添加 Data URL 前缀
|
||||
avatarsMap.set(wxid, `data:image/jpeg;base64,${avatarInfo.base64}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// avatars 解析失败,继续不带头像
|
||||
}
|
||||
|
||||
// 如果文件头没有完整的 avatars(可能超出 2000 字节),尝试流式读取
|
||||
if (avatarsMap.size === 0) {
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
const avatarStream = fs.createReadStream(filePath, { encoding: 'utf-8' })
|
||||
|
||||
let avatarsContent = ''
|
||||
let inAvatars = false
|
||||
let braceDepth = 0
|
||||
let inString = false
|
||||
let escape = false
|
||||
|
||||
avatarStream.on('data', (chunk: string | Buffer) => {
|
||||
const str = typeof chunk === 'string' ? chunk : chunk.toString()
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i]
|
||||
|
||||
if (!inAvatars) {
|
||||
// 查找 "avatars": 的位置
|
||||
const searchStr = '"avatars":'
|
||||
if (str.slice(i, i + searchStr.length) === searchStr) {
|
||||
inAvatars = true
|
||||
// 跳过 "avatars": 和可能的空白
|
||||
i += searchStr.length - 1
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 开始收集 avatars 对象内容
|
||||
avatarsContent += char
|
||||
|
||||
if (escape) {
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '\\' && inString) {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') braceDepth++
|
||||
if (char === '}') {
|
||||
braceDepth--
|
||||
if (braceDepth === 0) {
|
||||
// avatars 对象结束
|
||||
avatarStream.destroy()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
avatarStream.on('close', () => {
|
||||
if (avatarsContent) {
|
||||
try {
|
||||
const avatarsObj = JSON.parse(avatarsContent) as Record<string, EchotraceAvatarInfo>
|
||||
for (const [wxid, avatarInfo] of Object.entries(avatarsObj)) {
|
||||
if (avatarInfo && typeof avatarInfo === 'object' && avatarInfo.base64) {
|
||||
avatarsMap.set(wxid, `data:image/jpeg;base64,${avatarInfo.base64}`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析失败
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
avatarStream.on('error', () => resolve())
|
||||
})
|
||||
} catch {
|
||||
// 流式解析失败,继续不带头像
|
||||
}
|
||||
}
|
||||
|
||||
// 提取群头像(从 avatars 中获取群ID对应的头像)
|
||||
const groupAvatar = groupId ? avatarsMap.get(groupId) : undefined
|
||||
|
||||
// 发送 meta
|
||||
const meta: ParsedMeta = {
|
||||
name: chatName,
|
||||
platform: ChatPlatform.WECHAT,
|
||||
type: chatType,
|
||||
groupId,
|
||||
groupAvatar,
|
||||
}
|
||||
yield { type: 'meta', data: meta }
|
||||
|
||||
@@ -209,18 +385,34 @@ async function* parseEchotrace(options: ParseOptions): AsyncGenerator<ParseEvent
|
||||
}
|
||||
|
||||
const platformId = msg.senderUsername
|
||||
|
||||
// 跳过群"成员"(群ID以 @chatroom 结尾的消息)
|
||||
// 这些通常是系统消息,发送者是群本身,不是真正的成员
|
||||
if (platformId.endsWith('@chatroom')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const accountName = msg.senderDisplayName || platformId
|
||||
|
||||
// 获取头像(优先使用 senderAvatarKey,fallback 到 senderUsername)
|
||||
const avatarKey = msg.senderAvatarKey || msg.senderUsername
|
||||
const avatar = avatarsMap.get(avatarKey)
|
||||
|
||||
// 更新成员信息
|
||||
if (!memberMap.has(platformId)) {
|
||||
memberMap.set(platformId, {
|
||||
platformId,
|
||||
accountName,
|
||||
avatar,
|
||||
})
|
||||
} else {
|
||||
// 更新为最新的显示名
|
||||
const existing = memberMap.get(platformId)!
|
||||
existing.accountName = accountName
|
||||
// 头像使用最新的(覆盖更新)
|
||||
if (avatar) {
|
||||
existing.avatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
// 转换消息类型
|
||||
@@ -278,6 +470,7 @@ async function* parseEchotrace(options: ParseOptions): AsyncGenerator<ParseEvent
|
||||
const members: ParsedMember[] = Array.from(memberMap.values()).map((m) => ({
|
||||
platformId: m.platformId,
|
||||
accountName: m.accountName,
|
||||
avatar: m.avatar,
|
||||
}))
|
||||
yield { type: 'members', data: members }
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ParsedMeta {
|
||||
name: string
|
||||
platform: ChatPlatform
|
||||
type: ChatType
|
||||
groupId?: string // 群ID(群聊类型有值)
|
||||
groupAvatar?: string // 群头像(base64 Data URL)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,13 +65,16 @@ function createTempDatabase(dbPath: string): Database.Database {
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
type TEXT NOT NULL,
|
||||
group_id TEXT,
|
||||
group_avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
platform_id TEXT PRIMARY KEY,
|
||||
account_name TEXT,
|
||||
group_nickname TEXT
|
||||
group_nickname TEXT,
|
||||
avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message (
|
||||
@@ -141,7 +144,9 @@ function createDatabaseWithoutIndexes(sessionId: string): Database.Database {
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
imported_at INTEGER NOT NULL
|
||||
imported_at INTEGER NOT NULL,
|
||||
group_id TEXT,
|
||||
group_avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
@@ -149,7 +154,8 @@ function createDatabaseWithoutIndexes(sessionId: string): Database.Database {
|
||||
platform_id TEXT NOT NULL UNIQUE,
|
||||
account_name TEXT,
|
||||
group_nickname TEXT,
|
||||
aliases TEXT DEFAULT '[]'
|
||||
aliases TEXT DEFAULT '[]',
|
||||
avatar TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_name_history (
|
||||
@@ -241,10 +247,10 @@ export async function streamImport(filePath: string, requestId: string): Promise
|
||||
|
||||
// 准备语句
|
||||
const insertMeta = db.prepare(`
|
||||
INSERT INTO meta (name, platform, type, imported_at) VALUES (?, ?, ?, ?)
|
||||
INSERT INTO meta (name, platform, type, imported_at, group_id, group_avatar) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
const insertMember = db.prepare(`
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)
|
||||
INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, avatar) VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
const getMemberId = db.prepare(`SELECT id FROM member WHERE platform_id = ?`)
|
||||
const insertMessage = db.prepare(`
|
||||
@@ -347,14 +353,21 @@ export async function streamImport(filePath: string, requestId: string): Promise
|
||||
|
||||
onMeta: (meta: ParsedMeta) => {
|
||||
if (!metaInserted) {
|
||||
insertMeta.run(meta.name, meta.platform, meta.type, Math.floor(Date.now() / 1000))
|
||||
insertMeta.run(
|
||||
meta.name,
|
||||
meta.platform,
|
||||
meta.type,
|
||||
Math.floor(Date.now() / 1000),
|
||||
meta.groupId || null,
|
||||
meta.groupAvatar || null
|
||||
)
|
||||
metaInserted = true
|
||||
}
|
||||
},
|
||||
|
||||
onMembers: (members: ParsedMember[]) => {
|
||||
for (const member of members) {
|
||||
insertMember.run(member.platformId, member.accountName || null, member.groupNickname || null)
|
||||
insertMember.run(member.platformId, member.accountName || null, member.groupNickname || null, member.avatar || null)
|
||||
const row = getMemberId.get(member.platformId) as { id: number } | undefined
|
||||
if (row) {
|
||||
memberIdMap.set(member.platformId, row.id)
|
||||
@@ -387,7 +400,8 @@ export async function streamImport(filePath: string, requestId: string): Promise
|
||||
// 确保成员存在
|
||||
let t0 = Date.now()
|
||||
if (!memberIdMap.has(msg.senderPlatformId)) {
|
||||
insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null)
|
||||
// 消息中没有头像信息,设为 null
|
||||
insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null, null)
|
||||
const row = getMemberId.get(msg.senderPlatformId) as { id: number } | undefined
|
||||
if (row) {
|
||||
memberIdMap.set(msg.senderPlatformId, row.id)
|
||||
@@ -683,9 +697,9 @@ export async function streamParseFileInfo(filePath: string, requestId: string):
|
||||
const db = createTempDatabase(tempDbPath)
|
||||
|
||||
// 准备语句
|
||||
const insertMeta = db.prepare('INSERT INTO meta (name, platform, type) VALUES (?, ?, ?)')
|
||||
const insertMeta = db.prepare('INSERT INTO meta (name, platform, type, group_id, group_avatar) VALUES (?, ?, ?, ?, ?)')
|
||||
const insertMember = db.prepare(
|
||||
'INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)'
|
||||
'INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname, avatar) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
const insertMessage = db.prepare(`
|
||||
INSERT INTO message (sender_platform_id, sender_account_name, sender_group_nickname, timestamp, type, content)
|
||||
@@ -712,7 +726,13 @@ export async function streamParseFileInfo(filePath: string, requestId: string):
|
||||
onMeta: (parsedMeta) => {
|
||||
meta = parsedMeta
|
||||
if (!metaInserted) {
|
||||
insertMeta.run(parsedMeta.name, parsedMeta.platform, parsedMeta.type)
|
||||
insertMeta.run(
|
||||
parsedMeta.name,
|
||||
parsedMeta.platform,
|
||||
parsedMeta.type,
|
||||
parsedMeta.groupId || null,
|
||||
parsedMeta.groupAvatar || null
|
||||
)
|
||||
metaInserted = true
|
||||
}
|
||||
},
|
||||
@@ -721,7 +741,7 @@ export async function streamParseFileInfo(filePath: string, requestId: string):
|
||||
for (const m of parsedMembers) {
|
||||
if (!memberSet.has(m.platformId)) {
|
||||
memberSet.add(m.platformId)
|
||||
insertMember.run(m.platformId, m.accountName || null, m.groupNickname || null)
|
||||
insertMember.run(m.platformId, m.accountName || null, m.groupNickname || null, m.avatar || null)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -731,7 +751,8 @@ export async function streamParseFileInfo(filePath: string, requestId: string):
|
||||
// 确保成员存在
|
||||
if (!memberSet.has(msg.senderPlatformId)) {
|
||||
memberSet.add(msg.senderPlatformId)
|
||||
insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null)
|
||||
// 消息中没有头像信息,设为 null
|
||||
insertMember.run(msg.senderPlatformId, msg.senderAccountName || null, msg.senderGroupNickname || null, null)
|
||||
}
|
||||
|
||||
insertMessage.run(
|
||||
|
||||
@@ -69,6 +69,7 @@ export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[]
|
||||
m.id as memberId,
|
||||
m.platform_id as platformId,
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
|
||||
m.avatar as avatar,
|
||||
COUNT(msg.id) as messageCount
|
||||
FROM member m
|
||||
LEFT JOIN message msg ON m.id = msg.sender_id ${msgFilterWithSystem}
|
||||
@@ -82,6 +83,7 @@ export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[]
|
||||
memberId: number
|
||||
platformId: string
|
||||
name: string
|
||||
avatar: string | null
|
||||
messageCount: number
|
||||
}>
|
||||
|
||||
@@ -89,6 +91,7 @@ export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[]
|
||||
memberId: row.memberId,
|
||||
platformId: row.platformId,
|
||||
name: row.name,
|
||||
avatar: row.avatar,
|
||||
messageCount: row.messageCount,
|
||||
percentage: totalMessages > 0 ? Math.round((row.messageCount / totalMessages) * 10000) / 100 : 0,
|
||||
}))
|
||||
@@ -313,6 +316,8 @@ interface DbMeta {
|
||||
platform: string
|
||||
type: string
|
||||
imported_at: number
|
||||
group_id: string | null
|
||||
group_avatar: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,6 +372,8 @@ export function getAllSessions(): any[] {
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath,
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -419,6 +426,8 @@ export function getSession(sessionId: string): any | null {
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath: getDbPath(sessionId),
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,10 +443,13 @@ interface MemberWithStats {
|
||||
groupNickname: string | null
|
||||
aliases: string[]
|
||||
messageCount: number
|
||||
avatar: string | null
|
||||
}
|
||||
|
||||
// 用于标记已检查过 aliases 字段的会话
|
||||
const aliasesCheckedSessions = new Set<string>()
|
||||
// 用于标记已检查过 avatar 字段的会话
|
||||
const avatarCheckedSessions = new Set<string>()
|
||||
|
||||
/**
|
||||
* 确保 member 表有 aliases 字段(数据库迁移)
|
||||
@@ -476,11 +488,48 @@ function ensureAliasesColumn(sessionId: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有成员列表(含消息数和别名)
|
||||
* 确保 member 表有 avatar 字段(数据库迁移)
|
||||
* 用于兼容旧数据库
|
||||
*/
|
||||
export function ensureAvatarColumn(sessionId: string): void {
|
||||
// 每个会话只检查一次
|
||||
if (avatarCheckedSessions.has(sessionId)) return
|
||||
|
||||
const dbPath = getDbPath(sessionId)
|
||||
if (!fs.existsSync(dbPath)) return
|
||||
|
||||
// 先关闭可能缓存的只读连接
|
||||
closeDatabase(sessionId)
|
||||
|
||||
// 使用写入模式打开数据库检查并添加字段
|
||||
const db = new Database(dbPath)
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
try {
|
||||
// 检查 avatar 字段是否存在
|
||||
const columns = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
|
||||
const hasAvatar = columns.some((col) => col.name === 'avatar')
|
||||
|
||||
if (!hasAvatar) {
|
||||
// 添加 avatar 字段
|
||||
db.exec('ALTER TABLE member ADD COLUMN avatar TEXT')
|
||||
console.log(`[Worker] Added avatar column to member table in session ${sessionId}`)
|
||||
}
|
||||
|
||||
// 标记为已检查
|
||||
avatarCheckedSessions.add(sessionId)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有成员列表(含消息数、别名和头像)
|
||||
*/
|
||||
export function getMembers(sessionId: string): MemberWithStats[] {
|
||||
// 先确保数据库有 aliases 字段(兼容旧数据库)
|
||||
// 先确保数据库有 aliases 和 avatar 字段(兼容旧数据库)
|
||||
ensureAliasesColumn(sessionId)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return []
|
||||
@@ -494,6 +543,7 @@ export function getMembers(sessionId: string): MemberWithStats[] {
|
||||
m.account_name as accountName,
|
||||
m.group_nickname as groupNickname,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
COUNT(msg.id) as messageCount
|
||||
FROM member m
|
||||
LEFT JOIN message msg ON m.id = msg.sender_id
|
||||
@@ -508,6 +558,7 @@ export function getMembers(sessionId: string): MemberWithStats[] {
|
||||
accountName: string | null
|
||||
groupNickname: string | null
|
||||
aliases: string | null
|
||||
avatar: string | null
|
||||
messageCount: number
|
||||
}>
|
||||
|
||||
@@ -518,6 +569,7 @@ export function getMembers(sessionId: string): MemberWithStats[] {
|
||||
groupNickname: row.groupNickname,
|
||||
aliases: row.aliases ? JSON.parse(row.aliases) : [],
|
||||
messageCount: row.messageCount,
|
||||
avatar: row.avatar,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { openDatabase, buildTimeFilter, type TimeFilter } from '../core'
|
||||
import { ensureAvatarColumn } from './basic'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@@ -16,6 +17,7 @@ export interface MessageResult {
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
senderAliases: string[]
|
||||
senderAvatar: string | null
|
||||
content: string
|
||||
timestamp: number
|
||||
type: number
|
||||
@@ -40,13 +42,14 @@ export interface MessagesWithTotal {
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 数据库行类型(包含 aliases JSON 字符串)
|
||||
* 数据库行类型(包含 aliases JSON 字符串和头像)
|
||||
*/
|
||||
interface DbMessageRow {
|
||||
id: number
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
aliases: string | null
|
||||
avatar: string | null
|
||||
content: string
|
||||
timestamp: number
|
||||
type: number
|
||||
@@ -72,6 +75,7 @@ function sanitizeMessageRow(row: DbMessageRow): MessageResult {
|
||||
senderName: String(row.senderName || ''),
|
||||
senderPlatformId: String(row.senderPlatformId || ''),
|
||||
senderAliases: aliases,
|
||||
senderAvatar: row.avatar || null,
|
||||
content: row.content != null ? String(row.content) : '',
|
||||
timestamp: Number(row.timestamp),
|
||||
type: Number(row.type),
|
||||
@@ -119,6 +123,9 @@ export function getRecentMessages(
|
||||
filter?: TimeFilter,
|
||||
limit: number = 100
|
||||
): MessagesWithTotal {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return { messages: [], total: 0 }
|
||||
|
||||
@@ -146,6 +153,7 @@ export function getRecentMessages(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
@@ -185,6 +193,9 @@ export function searchMessages(
|
||||
offset: number = 0,
|
||||
senderId?: number
|
||||
): MessagesWithTotal {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return { messages: [], total: 0 }
|
||||
|
||||
@@ -223,6 +234,7 @@ export function searchMessages(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
@@ -257,6 +269,9 @@ export function getMessageContext(
|
||||
messageIds: number | number[],
|
||||
contextSize: number = 20
|
||||
): MessageResult[] {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return []
|
||||
|
||||
@@ -305,6 +320,7 @@ export function getMessageContext(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
@@ -336,6 +352,9 @@ export function getMessagesBefore(
|
||||
senderId?: number,
|
||||
keywords?: string[]
|
||||
): PaginatedMessages {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return { messages: [], hasMore: false }
|
||||
|
||||
@@ -355,6 +374,7 @@ export function getMessagesBefore(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
@@ -398,6 +418,9 @@ export function getMessagesAfter(
|
||||
senderId?: number,
|
||||
keywords?: string[]
|
||||
): PaginatedMessages {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return { messages: [], hasMore: false }
|
||||
|
||||
@@ -417,6 +440,7 @@ export function getMessagesAfter(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
@@ -458,6 +482,9 @@ export function getConversationBetween(
|
||||
filter?: TimeFilter,
|
||||
limit: number = 100
|
||||
): MessagesWithTotal & { member1Name: string; member2Name: string } {
|
||||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||||
ensureAvatarColumn(sessionId)
|
||||
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||||
|
||||
@@ -499,6 +526,7 @@ export function getConversationBetween(
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type
|
||||
|
||||
Reference in New Issue
Block a user