feat: 支持平台消息id和回复id,同时进行表迁移

This commit is contained in:
digua
2026-01-09 00:04:21 +08:00
committed by digua
parent d7291768b1
commit 32fd5139d4
10 changed files with 121 additions and 19 deletions
+8 -3
View File
@@ -106,11 +106,14 @@ function createDatabase(sessionId: string): Database.Database {
ts INTEGER NOT NULL, ts INTEGER NOT NULL,
type INTEGER NOT NULL, type INTEGER NOT NULL,
content TEXT, content TEXT,
reply_to_message_id TEXT DEFAULT NULL,
platform_message_id TEXT DEFAULT NULL,
FOREIGN KEY(sender_id) REFERENCES member(id) FOREIGN KEY(sender_id) REFERENCES member(id)
); );
CREATE INDEX IF NOT EXISTS idx_message_ts ON message(ts); 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_message_sender ON message(sender_id);
CREATE INDEX IF NOT EXISTS idx_message_platform_id ON message(platform_message_id);
CREATE INDEX IF NOT EXISTS idx_member_name_history_member_id ON member_name_history(member_id); CREATE INDEX IF NOT EXISTS idx_member_name_history_member_id ON member_name_history(member_id);
`) `)
@@ -201,8 +204,8 @@ export function importData(parseResult: ParseResult): string {
const groupNicknameTracker = new Map<string, { currentName: string; lastSeenTs: number }>() const groupNicknameTracker = new Map<string, { currentName: string; lastSeenTs: number }>()
const insertMessage = db.prepare(` const insertMessage = db.prepare(`
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content) INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content, reply_to_message_id, platform_message_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`) `)
const insertNameHistory = db.prepare(` const insertNameHistory = db.prepare(`
INSERT INTO member_name_history (member_id, name_type, name, start_ts, end_ts) INSERT INTO member_name_history (member_id, name_type, name, start_ts, end_ts)
@@ -230,7 +233,9 @@ export function importData(parseResult: ParseResult): string {
msg.senderGroupNickname || null, msg.senderGroupNickname || null,
msg.timestamp, msg.timestamp,
msg.type, msg.type,
msg.content msg.content,
msg.replyToMessageId || null,
msg.platformMessageId || null
) )
// 追踪 account_name 变化 // 追踪 account_name 变化
+26 -4
View File
@@ -56,15 +56,37 @@ const migrations: Migration[] = [
}, },
{ {
version: 2, version: 2,
description: '添加 roles 字段到 member 表', description: '添加 roles、reply_to_message_id、platform_message_id 字段',
userMessage: '支持成员角色(群主、管理员等)', userMessage: '支持成员角色、消息回复关系和回复内容预览',
up: (db) => { up: (db) => {
// 检查 roles 列是否已存在(防止重复执行) // 检查 roles 列是否已存在(防止重复执行)
const tableInfo = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }> const memberTableInfo = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
const hasRolesColumn = tableInfo.some((col) => col.name === 'roles') const hasRolesColumn = memberTableInfo.some((col) => col.name === 'roles')
if (!hasRolesColumn) { if (!hasRolesColumn) {
db.exec("ALTER TABLE member ADD COLUMN roles TEXT DEFAULT '[]'") db.exec("ALTER TABLE member ADD COLUMN roles TEXT DEFAULT '[]'")
} }
// 检查 message 表的列
const messageTableInfo = db.prepare('PRAGMA table_info(message)').all() as Array<{ name: string }>
// 检查 reply_to_message_id 列是否已存在
const hasReplyColumn = messageTableInfo.some((col) => col.name === 'reply_to_message_id')
if (!hasReplyColumn) {
db.exec('ALTER TABLE message ADD COLUMN reply_to_message_id TEXT DEFAULT NULL')
}
// 添加 platform_message_id 列(存储平台原始消息 ID,用于回复关联查询)
const hasPlatformMsgIdColumn = messageTableInfo.some((col) => col.name === 'platform_message_id')
if (!hasPlatformMsgIdColumn) {
db.exec('ALTER TABLE message ADD COLUMN platform_message_id TEXT DEFAULT NULL')
}
// 创建索引以加速回复查询
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_message_platform_id ON message(platform_message_id)')
} catch {
// 索引可能已存在
}
}, },
}, },
] ]
@@ -73,6 +73,7 @@ interface JsonlMessage {
timestamp: number timestamp: number
type: number type: number
content: string | null content: string | null
replyToMessageId?: string
} }
/** 任意 JSONL 行 */ /** 任意 JSONL 行 */
@@ -215,6 +216,7 @@ async function* parseChatLabJsonl(options: ParseOptions): AsyncGenerator<ParseEv
timestamp: parsed.timestamp, timestamp: parsed.timestamp,
type: parsed.type as MessageType, type: parsed.type as MessageType,
content: parsed.content, content: parsed.content,
replyToMessageId: parsed.replyToMessageId,
}) })
messagesProcessed++ messagesProcessed++
+2
View File
@@ -65,6 +65,7 @@ interface ChatLabMessage {
timestamp: number // 秒级时间戳 timestamp: number // 秒级时间戳
type: number // MessageType type: number // MessageType
content: string | null content: string | null
replyToMessageId?: string // 回复的目标消息 ID(平台原始 ID)
} }
interface ChatLabMember { interface ChatLabMember {
@@ -204,6 +205,7 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
timestamp: msg.timestamp, timestamp: msg.timestamp,
type: msg.type, type: msg.type,
content: msg.content, content: msg.content,
replyToMessageId: msg.replyToMessageId,
}) })
messagesProcessed++ messagesProcessed++
@@ -387,6 +387,7 @@ async function* parseDiscordExporter(options: ParseOptions): AsyncGenerator<Pars
timestamp: parseTimestamp(msg.timestamp), timestamp: parseTimestamp(msg.timestamp),
type: messageType, type: messageType,
content: content || null, content: content || null,
replyToMessageId: msg.reference?.messageId || undefined,
}) })
messagesProcessed++ messagesProcessed++
+8 -3
View File
@@ -181,6 +181,8 @@ function createDatabaseWithoutIndexes(sessionId: string): Database.Database {
ts INTEGER NOT NULL, ts INTEGER NOT NULL,
type INTEGER NOT NULL, type INTEGER NOT NULL,
content TEXT, content TEXT,
reply_to_message_id TEXT DEFAULT NULL,
platform_message_id TEXT DEFAULT NULL,
FOREIGN KEY(sender_id) REFERENCES member(id) FOREIGN KEY(sender_id) REFERENCES member(id)
); );
`) `)
@@ -195,6 +197,7 @@ function createIndexes(db: Database.Database): void {
db.exec(` db.exec(`
CREATE INDEX IF NOT EXISTS idx_message_ts ON message(ts); 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_message_sender ON message(sender_id);
CREATE INDEX IF NOT EXISTS idx_message_platform_id ON message(platform_message_id);
CREATE INDEX IF NOT EXISTS idx_member_name_history_member_id ON member_name_history(member_id); CREATE INDEX IF NOT EXISTS idx_member_name_history_member_id ON member_name_history(member_id);
`) `)
} }
@@ -268,8 +271,8 @@ export async function streamImport(filePath: string, requestId: string): Promise
`) `)
const getMemberId = db.prepare(`SELECT id FROM member WHERE platform_id = ?`) const getMemberId = db.prepare(`SELECT id FROM member WHERE platform_id = ?`)
const insertMessage = db.prepare(` const insertMessage = db.prepare(`
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content) INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content, reply_to_message_id, platform_message_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`) `)
const insertNameHistory = db.prepare(` const insertNameHistory = db.prepare(`
INSERT INTO member_name_history (member_id, name_type, name, start_ts, end_ts) VALUES (?, ?, ?, ?, ?) INSERT INTO member_name_history (member_id, name_type, name, start_ts, end_ts) VALUES (?, ?, ?, ?, ?)
@@ -467,7 +470,9 @@ export async function streamImport(filePath: string, requestId: string): Promise
msg.senderGroupNickname || null, msg.senderGroupNickname || null,
msg.timestamp, msg.timestamp,
msg.type, msg.type,
safeContent safeContent,
msg.replyToMessageId || null,
msg.platformMessageId || null
) )
messageInsertTime += Date.now() - t0 messageInsertTime += Date.now() - t0
messageCountInBatch++ messageCountInBatch++
+51 -7
View File
@@ -21,6 +21,9 @@ export interface MessageResult {
content: string content: string
timestamp: number timestamp: number
type: number type: number
replyToMessageId: string | null
replyToContent: string | null
replyToSenderName: string | null
} }
/** /**
@@ -53,6 +56,9 @@ interface DbMessageRow {
content: string content: string
timestamp: number timestamp: number
type: number type: number
reply_to_message_id: string | null
replyToContent: string | null
replyToSenderName: string | null
} }
/** /**
@@ -79,6 +85,9 @@ function sanitizeMessageRow(row: DbMessageRow): MessageResult {
content: row.content != null ? String(row.content) : '', content: row.content != null ? String(row.content) : '',
timestamp: Number(row.timestamp), timestamp: Number(row.timestamp),
type: Number(row.type), type: Number(row.type),
replyToMessageId: row.reply_to_message_id || null,
replyToContent: row.replyToContent || null,
replyToSenderName: row.replyToSenderName || null,
} }
} }
@@ -156,9 +165,14 @@ export function getRecentMessages(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE 1=1 WHERE 1=1
${timeCondition} ${timeCondition}
${SYSTEM_FILTER} ${SYSTEM_FILTER}
@@ -218,9 +232,14 @@ export function getAllRecentMessages(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE 1=1 WHERE 1=1
${timeCondition} ${timeCondition}
ORDER BY msg.ts DESC ORDER BY msg.ts DESC
@@ -296,9 +315,14 @@ export function searchMessages(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE ${keywordCondition} WHERE ${keywordCondition}
${timeCondition} ${timeCondition}
${senderCondition} ${senderCondition}
@@ -381,9 +405,14 @@ export function getMessageContext(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE msg.id IN (${placeholders}) WHERE msg.id IN (${placeholders})
ORDER BY msg.id ASC ORDER BY msg.id ASC
` `
@@ -435,9 +464,14 @@ export function getMessagesBefore(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE msg.id < ? WHERE msg.id < ?
${timeCondition} ${timeCondition}
${keywordCondition} ${keywordCondition}
@@ -500,9 +534,14 @@ export function getMessagesAfter(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE msg.id > ? WHERE msg.id > ?
${timeCondition} ${timeCondition}
${keywordCondition} ${keywordCondition}
@@ -585,9 +624,14 @@ export function getConversationBetween(
m.avatar, m.avatar,
msg.content, msg.content,
msg.ts as timestamp, msg.ts as timestamp,
msg.type msg.type,
msg.reply_to_message_id,
reply_msg.content as replyToContent,
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
FROM message msg FROM message msg
JOIN member m ON msg.sender_id = m.id JOIN member m ON msg.sender_id = m.id
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
WHERE msg.sender_id IN (?, ?) WHERE msg.sender_id IN (?, ?)
${timeCondition} ${timeCondition}
AND msg.content IS NOT NULL AND msg.content != '' AND msg.content IS NOT NULL AND msg.content != ''
@@ -250,6 +250,19 @@ function highlightContent(content: string): string {
class="relative inline-block rounded-lg px-3 py-2 transition-shadow" class="relative inline-block rounded-lg px-3 py-2 transition-shadow"
:class="[bubbleColor, isTarget ? 'ring-2 ring-yellow-400 dark:ring-yellow-500' : '']" :class="[bubbleColor, isTarget ? 'ring-2 ring-yellow-400 dark:ring-yellow-500' : '']"
> >
<!-- 回复引用样式 -->
<div
v-if="message.replyToMessageId"
class="mb-2 border-l-2 border-gray-300 dark:border-gray-600 pl-2 text-xs text-gray-500 dark:text-gray-400"
>
<span class="font-medium">{{ t('replyTo') }}</span>
<span v-if="message.replyToSenderName" class="ml-1 text-gray-600 dark:text-gray-300">
{{ message.replyToSenderName }}
</span>
<p v-if="message.replyToContent" class="mt-0.5 line-clamp-2 italic">
{{ message.replyToContent }}
</p>
</div>
<p <p
class="whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-200" class="whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-200"
v-html="highlightContent(message.content || '')" v-html="highlightContent(message.content || '')"
@@ -320,12 +333,14 @@ function highlightContent(content: string): string {
"zh-CN": { "zh-CN": {
"viewContext": "查看上下文", "viewContext": "查看上下文",
"contextTitle": "消息上下文(前后各10条)", "contextTitle": "消息上下文(前后各10条)",
"noContext": "暂无上下文" "noContext": "暂无上下文",
"replyTo": "回复"
}, },
"en-US": { "en-US": {
"viewContext": "View Context", "viewContext": "View Context",
"contextTitle": "Message Context (10 before and after)", "contextTitle": "Message Context (10 before and after)",
"noContext": "No context available" "noContext": "No context available",
"replyTo": "Reply to"
} }
} }
</i18n> </i18n>
+3
View File
@@ -168,6 +168,7 @@ export interface DbMessage {
ts: number // 时间戳(秒) ts: number // 时间戳(秒)
type: MessageType // 消息类型 type: MessageType // 消息类型
content: string | null // 纯文本内容 content: string | null // 纯文本内容
reply_to_message_id: string | null // 回复的目标消息 ID(平台原始 ID)
} }
// ==================== Parser 解析结果 ==================== // ==================== Parser 解析结果 ====================
@@ -187,12 +188,14 @@ export interface ParsedMember {
* 解析后的消息 * 解析后的消息
*/ */
export interface ParsedMessage { export interface ParsedMessage {
platformMessageId?: string // 消息的平台原始 ID(用于回复关联查询)
senderPlatformId: string // 发送者平台ID senderPlatformId: string // 发送者平台ID
senderAccountName: string // 发送时的账号名称 senderAccountName: string // 发送时的账号名称
senderGroupNickname?: string // 发送时的群昵称(可为空) senderGroupNickname?: string // 发送时的群昵称(可为空)
timestamp: number // 时间戳(秒) timestamp: number // 时间戳(秒)
type: MessageType // 消息类型 type: MessageType // 消息类型
content: string | null // 内容 content: string | null // 内容
replyToMessageId?: string // 回复的目标消息 ID(平台原始 ID,可为空)
} }
/** /**
+3
View File
@@ -182,4 +182,7 @@ export interface ChatRecordMessage {
content: string content: string
timestamp: number timestamp: number
type: number type: number
replyToMessageId: string | null // 回复的目标消息 ID(平台原始 ID)
replyToContent: string | null // 被回复消息的内容预览
replyToSenderName: string | null // 被回复消息的发送者名称
} }