mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-24 15:40:19 +08:00
feat: 聊天记录导入优化
This commit is contained in:
@@ -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 使用 platformId,nickname 使用群昵称)
|
||||
// 发送成员(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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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]: '其他',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user