feat: 支持昵称变更记录 & 交互方式优化

This commit is contained in:
digua
2025-11-27 00:48:00 +08:00
parent 9667dc615a
commit e19a4474fe
17 changed files with 701 additions and 151 deletions
+99 -10
View File
@@ -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()
}
}
+12
View File
@@ -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 []
}
})
/**
* 获取每小时活跃度分布
*/
+7 -7
View File
@@ -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,
}
}
},
}
+7 -7
View File
@@ -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,
}
}
},
}
+3 -1
View File
@@ -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
View File
@@ -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