mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-11 16:41:02 +08:00
feat: 支持昵称变更记录 & 交互方式优化
This commit is contained in:
@@ -94,6 +94,16 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 成员昵称历史表
|
||||
CREATE TABLE IF NOT EXISTS member_name_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
member_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
start_ts INTEGER NOT NULL,
|
||||
end_ts INTEGER,
|
||||
FOREIGN KEY(member_id) REFERENCES member(id)
|
||||
);
|
||||
|
||||
-- 消息表
|
||||
CREATE TABLE IF NOT EXISTS message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -107,6 +117,7 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_message_ts ON message(ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_sender ON message(sender_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_name_history_member_id ON member_name_history(member_id);
|
||||
`)
|
||||
|
||||
return db
|
||||
@@ -158,34 +169,84 @@ export function importData(parseResult: ParseResult): string {
|
||||
const insertMember = db.prepare(`
|
||||
INSERT OR IGNORE INTO member (platform_id, name) VALUES (?, ?)
|
||||
`)
|
||||
const updateMemberName = db.prepare(`
|
||||
UPDATE member SET name = ? WHERE platform_id = ?
|
||||
`)
|
||||
const getMemberId = db.prepare(`
|
||||
SELECT id FROM member WHERE platform_id = ?
|
||||
`)
|
||||
|
||||
const memberIdMap = new Map<string, number>()
|
||||
|
||||
// 初始化成员表(使用初始昵称)
|
||||
for (const member of parseResult.members) {
|
||||
insertMember.run(member.platformId, member.name)
|
||||
// 更新为最新昵称
|
||||
updateMemberName.run(member.name, member.platformId)
|
||||
const row = getMemberId.get(member.platformId) as { id: number }
|
||||
memberIdMap.set(member.platformId, row.id)
|
||||
}
|
||||
|
||||
// 批量插入消息
|
||||
// 按时间戳升序排序消息(用于追踪昵称变化)
|
||||
const sortedMessages = [...parseResult.messages].sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
// 追踪每个成员的昵称历史
|
||||
// Map<platformId, { currentName: string, lastSeenTs: number }>
|
||||
const nicknameTracker = new Map<string, { currentName: string; lastSeenTs: number }>()
|
||||
|
||||
// 准备 SQL 语句
|
||||
const insertMessage = db.prepare(`
|
||||
INSERT INTO message (sender_id, ts, type, content) VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
const insertNameHistory = db.prepare(`
|
||||
INSERT INTO member_name_history (member_id, name, start_ts, end_ts)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
const updateMemberName = db.prepare(`
|
||||
UPDATE member SET name = ? WHERE platform_id = ?
|
||||
`)
|
||||
const updateNameHistoryEndTs = db.prepare(`
|
||||
UPDATE member_name_history
|
||||
SET end_ts = ?
|
||||
WHERE member_id = ? AND end_ts IS NULL
|
||||
`)
|
||||
|
||||
for (const msg of parseResult.messages) {
|
||||
// 遍历排序后的消息
|
||||
for (const msg of sortedMessages) {
|
||||
const senderId = memberIdMap.get(msg.senderPlatformId)
|
||||
if (senderId !== undefined) {
|
||||
insertMessage.run(senderId, msg.timestamp, msg.type, msg.content)
|
||||
if (senderId === undefined) continue
|
||||
|
||||
// 插入消息
|
||||
insertMessage.run(senderId, msg.timestamp, msg.type, msg.content)
|
||||
|
||||
// 从消息中获取发送者的昵称
|
||||
const currentName = msg.senderName
|
||||
const tracker = nicknameTracker.get(msg.senderPlatformId)
|
||||
|
||||
if (!tracker) {
|
||||
// 首次出现,记录初始昵称
|
||||
nicknameTracker.set(msg.senderPlatformId, {
|
||||
currentName,
|
||||
lastSeenTs: msg.timestamp,
|
||||
})
|
||||
// 插入第一条昵称历史记录(end_ts 为 NULL,表示当前使用中)
|
||||
insertNameHistory.run(senderId, currentName, msg.timestamp, null)
|
||||
} else if (tracker.currentName !== currentName) {
|
||||
// 昵称发生变化
|
||||
// 1. 关闭旧昵称的时间段(end_ts 设为当前消息时间戳)
|
||||
updateNameHistoryEndTs.run(msg.timestamp, senderId)
|
||||
|
||||
// 2. 插入新昵称记录
|
||||
insertNameHistory.run(senderId, currentName, msg.timestamp, null)
|
||||
|
||||
// 3. 更新追踪器
|
||||
tracker.currentName = currentName
|
||||
tracker.lastSeenTs = msg.timestamp
|
||||
} else {
|
||||
// 昵称未变化,仅更新最后见到的时间戳
|
||||
tracker.lastSeenTs = msg.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 member 表中的最新昵称
|
||||
for (const [platformId, tracker] of nicknameTracker.entries()) {
|
||||
updateMemberName.run(tracker.currentName, platformId)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[Database] Executing transaction...')
|
||||
@@ -418,7 +479,7 @@ export function getAvailableYears(sessionId: string): number[] {
|
||||
`
|
||||
SELECT DISTINCT CAST(strftime('%Y', ts, 'unixepoch', 'localtime') AS INTEGER) as year
|
||||
FROM message
|
||||
ORDER BY year
|
||||
ORDER BY year DESC
|
||||
`
|
||||
)
|
||||
.all() as Array<{ year: number }>
|
||||
@@ -632,3 +693,31 @@ export function getDbDirectory(): string {
|
||||
ensureDbDir()
|
||||
return getDbDir()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成员的历史昵称记录
|
||||
*/
|
||||
export function getMemberNameHistory(
|
||||
sessionId: string,
|
||||
memberId: number
|
||||
): Array<{ name: string; startTs: number; endTs: number | null }> {
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return []
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT name, start_ts as startTs, end_ts as endTs
|
||||
FROM member_name_history
|
||||
WHERE member_id = ?
|
||||
ORDER BY start_ts DESC
|
||||
`
|
||||
)
|
||||
.all(memberId) as Array<{ name: string; startTs: number; endTs: number | null }>
|
||||
|
||||
return rows
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +273,18 @@ const mainIpcMain = (win: BrowserWindow) => {
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取成员历史昵称
|
||||
*/
|
||||
ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => {
|
||||
try {
|
||||
return database.getMemberNameHistory(sessionId, memberId)
|
||||
} catch (error) {
|
||||
console.error('获取成员历史昵称失败:', error)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取每小时活跃度分布
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MessageType,
|
||||
type ParseResult,
|
||||
type ParsedMember,
|
||||
type ParsedMessage
|
||||
type ParsedMessage,
|
||||
} from '../../../src/types/chat'
|
||||
|
||||
/**
|
||||
@@ -157,7 +157,7 @@ export const qqJsonParser: ChatParser = {
|
||||
const meta = {
|
||||
name: data.chatInfo.name,
|
||||
platform: ChatPlatform.QQ,
|
||||
type: data.chatInfo.type === 'group' ? ChatType.GROUP : ChatType.PRIVATE
|
||||
type: data.chatInfo.type === 'group' ? ChatType.GROUP : ChatType.PRIVATE,
|
||||
}
|
||||
|
||||
// 收集成员信息(使用 Map 去重,保留最新昵称)
|
||||
@@ -172,7 +172,7 @@ export const qqJsonParser: ChatParser = {
|
||||
// 更新成员信息(保留最新昵称)
|
||||
memberMap.set(platformId, {
|
||||
platformId,
|
||||
name: msg.sender.name || platformId
|
||||
name: msg.sender.name || platformId,
|
||||
})
|
||||
|
||||
// 转换时间戳(QQ 导出是毫秒,需要转为秒)
|
||||
@@ -191,17 +191,17 @@ export const qqJsonParser: ChatParser = {
|
||||
|
||||
messages.push({
|
||||
senderPlatformId: platformId,
|
||||
senderName: msg.sender.name || platformId,
|
||||
timestamp,
|
||||
type,
|
||||
content: textContent || null
|
||||
content: textContent || null,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
meta,
|
||||
members: Array.from(memberMap.values()),
|
||||
messages
|
||||
messages,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MessageType,
|
||||
type ParseResult,
|
||||
type ParsedMember,
|
||||
type ParsedMessage
|
||||
type ParsedMessage,
|
||||
} from '../../../src/types/chat'
|
||||
|
||||
/**
|
||||
@@ -146,9 +146,10 @@ export const qqTxtParser: ChatParser = {
|
||||
if (content) {
|
||||
messages.push({
|
||||
senderPlatformId: currentSender.platformId,
|
||||
senderName: currentSender.name,
|
||||
timestamp: currentTimestamp,
|
||||
type: detectMessageType(content),
|
||||
content
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,7 @@ export const qqTxtParser: ChatParser = {
|
||||
// 更新成员信息(保留最新昵称)
|
||||
memberMap.set(qqNumber, {
|
||||
platformId: qqNumber,
|
||||
name
|
||||
name,
|
||||
})
|
||||
|
||||
currentSender = { platformId: qqNumber, name }
|
||||
@@ -198,11 +199,10 @@ export const qqTxtParser: ChatParser = {
|
||||
meta: {
|
||||
name: groupName,
|
||||
platform: ChatPlatform.QQ,
|
||||
type: ChatType.GROUP // TXT 导出通常是群聊
|
||||
type: ChatType.GROUP, // TXT 导出通常是群聊
|
||||
},
|
||||
members: Array.from(memberMap.values()),
|
||||
messages
|
||||
messages,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Vendored
+3
-1
@@ -2,10 +2,11 @@ import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type {
|
||||
AnalysisSession,
|
||||
MemberActivity,
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
MessageType,
|
||||
ImportProgress
|
||||
ImportProgress,
|
||||
} from '../../src/types/chat'
|
||||
|
||||
interface TimeFilter {
|
||||
@@ -21,6 +22,7 @@ interface ChatApi {
|
||||
deleteSession: (sessionId: string) => Promise<boolean>
|
||||
getAvailableYears: (sessionId: string) => Promise<number[]>
|
||||
getMemberActivity: (sessionId: string, filter?: TimeFilter) => Promise<MemberActivity[]>
|
||||
getMemberNameHistory: (sessionId: string, memberId: number) => Promise<MemberNameHistory[]>
|
||||
getHourlyActivity: (sessionId: string, filter?: TimeFilter) => Promise<HourlyActivity[]>
|
||||
getDailyActivity: (sessionId: string, filter?: TimeFilter) => Promise<DailyActivity[]>
|
||||
getMessageTypeDistribution: (
|
||||
|
||||
+15
-16
@@ -3,10 +3,11 @@ import { electronAPI } from '@electron-toolkit/preload'
|
||||
import type {
|
||||
AnalysisSession,
|
||||
MemberActivity,
|
||||
MemberNameHistory,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
MessageType,
|
||||
ImportProgress
|
||||
ImportProgress,
|
||||
} from '../../src/types/chat'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -18,7 +19,7 @@ const api = {
|
||||
'check-update',
|
||||
'get-gpu-acceleration',
|
||||
'set-gpu-acceleration',
|
||||
'save-gpu-acceleration'
|
||||
'save-gpu-acceleration',
|
||||
]
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, data)
|
||||
@@ -33,7 +34,7 @@ const api = {
|
||||
},
|
||||
removeListener: (channel: string, func: (...args: unknown[]) => void) => {
|
||||
ipcRenderer.removeListener(channel, func)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Chat Analysis API
|
||||
@@ -83,30 +84,28 @@ const chatApi = {
|
||||
/**
|
||||
* 获取成员活跃度排行
|
||||
*/
|
||||
getMemberActivity: (
|
||||
sessionId: string,
|
||||
filter?: { startTs?: number; endTs?: number }
|
||||
): Promise<MemberActivity[]> => {
|
||||
getMemberActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<MemberActivity[]> => {
|
||||
return ipcRenderer.invoke('chat:getMemberActivity', sessionId, filter)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取成员历史昵称
|
||||
*/
|
||||
getMemberNameHistory: (sessionId: string, memberId: number): Promise<MemberNameHistory[]> => {
|
||||
return ipcRenderer.invoke('chat:getMemberNameHistory', sessionId, memberId)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取每小时活跃度分布
|
||||
*/
|
||||
getHourlyActivity: (
|
||||
sessionId: string,
|
||||
filter?: { startTs?: number; endTs?: number }
|
||||
): Promise<HourlyActivity[]> => {
|
||||
getHourlyActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<HourlyActivity[]> => {
|
||||
return ipcRenderer.invoke('chat:getHourlyActivity', sessionId, filter)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取每日活跃度趋势
|
||||
*/
|
||||
getDailyActivity: (
|
||||
sessionId: string,
|
||||
filter?: { startTs?: number; endTs?: number }
|
||||
): Promise<DailyActivity[]> => {
|
||||
getDailyActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<DailyActivity[]> => {
|
||||
return ipcRenderer.invoke('chat:getDailyActivity', sessionId, filter)
|
||||
},
|
||||
|
||||
@@ -152,7 +151,7 @@ const chatApi = {
|
||||
return () => {
|
||||
ipcRenderer.removeListener('chat:importProgress', handler)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
Reference in New Issue
Block a user