feat: 聊天记录导入优化

This commit is contained in:
digua
2025-12-11 00:33:33 +08:00
parent 95c0eb1c7e
commit 14e54958e8
5 changed files with 139 additions and 15 deletions
+34 -4
View File
@@ -67,12 +67,41 @@ const GROUP_NAME_REGEX = /^消息对象:(.+)$/
function detectMessageType(content: string): MessageType {
const trimmed = content.trim()
// 基础消息类型
if (trimmed === '[图片]') return MessageType.IMAGE
if (trimmed === '[表情]') return MessageType.EMOJI
if (trimmed === '[语音]') return MessageType.VOICE
if (trimmed === '[视频]') return MessageType.VIDEO
if (trimmed === '[文件]') return MessageType.FILE
if (trimmed === '[位置]' || trimmed === '[地理位置]') return MessageType.LOCATION
if (trimmed === '[链接]' || trimmed === '[卡片消息]') return MessageType.LINK
// 交互消息类型
if (trimmed === '[红包]' || trimmed.includes('发出了红包')) return MessageType.RED_PACKET
if (trimmed === '[转账]' || trimmed.includes('向你转账')) return MessageType.TRANSFER
if (trimmed.includes('拍了拍') || trimmed === '[拍一拍]') return MessageType.POKE
if (trimmed === '[语音通话]' || trimmed === '[视频通话]' || trimmed.includes('通话时长')) return MessageType.CALL
if (trimmed === '[分享]' || trimmed === '[音乐]' || trimmed === '[小程序]') return MessageType.SHARE
if (trimmed.startsWith('[回复]')) return MessageType.REPLY
if (trimmed === '[转发]' || trimmed === '[聊天记录]') return MessageType.FORWARD
// 系统消息类型
if (trimmed.includes('撤回了一条消息') || trimmed === '[撤回]') return MessageType.RECALL
if (
trimmed.includes('加入了群聊') ||
trimmed.includes('退出了群聊') ||
trimmed.includes('被移出群聊') ||
trimmed.includes('修改了群名称') ||
trimmed.includes('成为新群主') ||
trimmed.includes('群公告')
) {
return MessageType.SYSTEM
}
// 其他方括号包裹的特殊消息
if (trimmed.startsWith('[') && trimmed.endsWith(']')) return MessageType.OTHER
return MessageType.TEXT
}
@@ -144,7 +173,8 @@ async function* parseTxt(options: ParseOptions): AsyncGenerator<ParseEvent, void
messages.push({
senderPlatformId: currentMessage.platformId,
senderName: currentMessage.nickname, // 用于昵称历史追踪
senderAccountName: currentMessage.nickname, // QQ TXT 格式只有一个昵称,作为账号名称追踪历史
// 不设置 senderGroupNickname,避免同一昵称被重复追踪
timestamp: currentMessage.timestamp,
type,
content: content || null,
@@ -246,11 +276,11 @@ async function* parseTxt(options: ParseOptions): AsyncGenerator<ParseEvent, void
}
yield { type: 'meta', data: meta }
// 发送成员(name 使用 platformIdnickname 使用群昵称
// 发送成员(QQ TXT 格式只有一个昵称,只设置 accountName 避免重复追踪
const members: ParsedMember[] = Array.from(memberMap.values()).map((m) => ({
platformId: m.platformId,
name: m.platformId, // name 使用 ID
nickname: m.nickname, // nickname 使用群昵称
accountName: m.nickname, // QQ TXT 格式只有昵称,作为账号名称
// 不设置 groupNickname,避免同一昵称被重复追踪
}))
yield { type: 'members', data: members }
@@ -103,7 +103,16 @@ interface MemberInfo {
// ==================== 消息类型转换 ====================
function convertMessageType(messageType: number | undefined, content: V4Message['content']): MessageType {
function convertMessageType(
messageType: number | undefined,
content: V4Message['content'],
isRecalled?: boolean
): MessageType {
// 撤回消息
if (isRecalled) {
return MessageType.RECALL
}
// 检查资源类型
if (content.resources && content.resources.length > 0) {
const resourceType = content.resources[0].type
@@ -117,6 +126,8 @@ function convertMessageType(messageType: number | undefined, content: V4Message[
return MessageType.VOICE
case 'file':
return MessageType.FILE
case 'location':
return MessageType.LOCATION
}
}
@@ -125,6 +136,49 @@ function convertMessageType(messageType: number | undefined, content: V4Message[
return MessageType.EMOJI
}
// 根据文本内容判断特殊消息类型
const text = content.text?.trim() || ''
// 红包消息
if (text.includes('QQ红包') || text.includes('发出了红包') || text === '[红包]') {
return MessageType.RED_PACKET
}
// 转账消息
if (text.includes('转账') || text === '[转账]') {
return MessageType.TRANSFER
}
// 拍一拍/戳一戳
if (text.includes('拍了拍') || text.includes('戳了戳') || text === '[拍一拍]') {
return MessageType.POKE
}
// 通话消息
if (text.includes('语音通话') || text.includes('视频通话') || text.includes('通话时长')) {
return MessageType.CALL
}
// 分享消息
if (text === '[分享]' || text === '[音乐]' || text === '[小程序]') {
return MessageType.SHARE
}
// 链接/卡片消息
if (text === '[链接]' || text === '[卡片消息]') {
return MessageType.LINK
}
// 位置消息
if (text === '[位置]' || text === '[地理位置]') {
return MessageType.LOCATION
}
// 转发消息
if (text === '[转发]' || text === '[聊天记录]') {
return MessageType.FORWARD
}
// 根据 messageType 判断
switch (messageType) {
case 1:
@@ -136,7 +190,7 @@ function convertMessageType(messageType: number | undefined, content: V4Message[
case 7:
return MessageType.VIDEO
case 9:
return MessageType.TEXT // 回复消息
return MessageType.REPLY // 回复消息
default:
return MessageType.TEXT
}
@@ -250,7 +304,9 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
if (timestamp === null || !isValidYear(timestamp)) return null
// 消息类型
const type = msg.isSystemMessage ? MessageType.SYSTEM : convertMessageType(msg.messageType, msg.content)
const type = msg.isSystemMessage
? MessageType.SYSTEM
: convertMessageType(msg.messageType, msg.content, msg.isRecalled)
// 文本内容
let textContent = msg.content?.text || ''
@@ -30,9 +30,11 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
whereClause +=
" AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
} else {
whereClause = " WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
whereClause =
" WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
}
const messages = db
@@ -310,7 +312,7 @@ export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter):
}
const member = memberMap.get(row.memberId)!
if (member.catchphrases.length < 5) {
if (member.catchphrases.length < 10) {
member.catchphrases.push({
content: row.content,
count: row.count,
@@ -327,4 +329,3 @@ export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter):
return { members }
}
+2 -2
View File
@@ -37,10 +37,10 @@ const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发
// Tab 配置
const tabs = [
{ id: 'overview', label: '总览', icon: 'i-heroicons-chart-pie' },
// { id: 'ranking', label: '群榜单', icon: 'i-heroicons-trophy' },
{ id: 'ranking', label: '群榜单', icon: 'i-heroicons-trophy' },
{ id: 'quotes', label: '群语录', icon: 'i-heroicons-chat-bubble-bottom-center-text' },
{ id: 'relationships', label: '群关系', icon: 'i-heroicons-heart' },
// { id: 'timeline', label: '群趋势', icon: 'i-heroicons-chart-bar' },
{ id: 'timeline', label: '群趋势', icon: 'i-heroicons-chart-bar' },
{ id: 'members', label: '群成员', icon: 'i-heroicons-user-group' },
{ id: 'ai', label: 'AI实验室', icon: 'i-heroicons-sparkles' },
{ id: 'sql', label: 'SQL实验室', icon: 'i-heroicons-command-line' },
+39 -2
View File
@@ -7,29 +7,66 @@
/**
* 消息类型枚举
*
* 分类说明:
* - 基础消息 (0-19): 常见的内容类型
* - 交互消息 (20-39): 涉及互动的消息类型
* - 系统消息 (80-89): 系统相关消息
* - 其他 (99): 未知或无法分类的消息
*/
export enum MessageType {
// ========== 基础消息类型 (0-19) ==========
TEXT = 0, // 文本消息
IMAGE = 1, // 图片
VOICE = 2, // 语音
VIDEO = 3, // 视频
FILE = 4, // 文件
EMOJI = 5, // 表情包/贴纸
SYSTEM = 6, // 系统消息(入群/退群/撤回等)
OTHER = 99, // 其他
LINK = 7, // 链接/卡片(分享的网页、文章等)
LOCATION = 8, // 位置/地理位置
// ========== 交互消息类型 (20-39) ==========
RED_PACKET = 20, // 红包
TRANSFER = 21, // 转账
POKE = 22, // 拍一拍/戳一戳
CALL = 23, // 语音/视频通话
SHARE = 24, // 分享(音乐、小程序等)
REPLY = 25, // 引用回复
FORWARD = 26, // 转发消息
// ========== 系统消息类型 (80-89) ==========
SYSTEM = 80, // 系统消息(入群/退群/群公告等)
RECALL = 81, // 撤回消息
// ========== 其他 (99) ==========
OTHER = 99, // 其他/未知
}
/**
* 消息类型名称映射
*/
export const MESSAGE_TYPE_NAMES: Record<number, string> = {
// 基础消息类型
[MessageType.TEXT]: '文字',
[MessageType.IMAGE]: '图片',
[MessageType.VOICE]: '语音',
[MessageType.VIDEO]: '视频',
[MessageType.FILE]: '文件',
[MessageType.EMOJI]: '表情',
[MessageType.LINK]: '链接',
[MessageType.LOCATION]: '位置',
// 交互消息类型
[MessageType.RED_PACKET]: '红包',
[MessageType.TRANSFER]: '转账',
[MessageType.POKE]: '拍一拍',
[MessageType.CALL]: '通话',
[MessageType.SHARE]: '分享',
[MessageType.REPLY]: '回复',
[MessageType.FORWARD]: '转发',
// 系统消息类型
[MessageType.SYSTEM]: '系统',
[MessageType.RECALL]: '撤回',
// 其他
[MessageType.OTHER]: '其他',
}