mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-06-15 20:40:42 +08:00
feat: 支持平台消息id和回复id,同时进行表迁移
This commit is contained in:
@@ -106,11 +106,14 @@ function createDatabase(sessionId: string): Database.Database {
|
||||
ts INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
content TEXT,
|
||||
reply_to_message_id TEXT DEFAULT NULL,
|
||||
platform_message_id TEXT DEFAULT NULL,
|
||||
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_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);
|
||||
`)
|
||||
|
||||
@@ -201,8 +204,8 @@ export function importData(parseResult: ParseResult): string {
|
||||
const groupNicknameTracker = new Map<string, { currentName: string; lastSeenTs: number }>()
|
||||
|
||||
const insertMessage = db.prepare(`
|
||||
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content, reply_to_message_id, platform_message_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
const insertNameHistory = db.prepare(`
|
||||
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.timestamp,
|
||||
msg.type,
|
||||
msg.content
|
||||
msg.content,
|
||||
msg.replyToMessageId || null,
|
||||
msg.platformMessageId || null
|
||||
)
|
||||
|
||||
// 追踪 account_name 变化
|
||||
|
||||
@@ -56,15 +56,37 @@ const migrations: Migration[] = [
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
description: '添加 roles 字段到 member 表',
|
||||
userMessage: '支持成员角色(群主、管理员等)',
|
||||
description: '添加 roles、reply_to_message_id、platform_message_id 字段',
|
||||
userMessage: '支持成员角色、消息回复关系和回复内容预览',
|
||||
up: (db) => {
|
||||
// 检查 roles 列是否已存在(防止重复执行)
|
||||
const tableInfo = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
|
||||
const hasRolesColumn = tableInfo.some((col) => col.name === 'roles')
|
||||
const memberTableInfo = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
|
||||
const hasRolesColumn = memberTableInfo.some((col) => col.name === 'roles')
|
||||
if (!hasRolesColumn) {
|
||||
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
|
||||
type: number
|
||||
content: string | null
|
||||
replyToMessageId?: string
|
||||
}
|
||||
|
||||
/** 任意 JSONL 行 */
|
||||
@@ -215,6 +216,7 @@ async function* parseChatLabJsonl(options: ParseOptions): AsyncGenerator<ParseEv
|
||||
timestamp: parsed.timestamp,
|
||||
type: parsed.type as MessageType,
|
||||
content: parsed.content,
|
||||
replyToMessageId: parsed.replyToMessageId,
|
||||
})
|
||||
messagesProcessed++
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ interface ChatLabMessage {
|
||||
timestamp: number // 秒级时间戳
|
||||
type: number // MessageType
|
||||
content: string | null
|
||||
replyToMessageId?: string // 回复的目标消息 ID(平台原始 ID)
|
||||
}
|
||||
|
||||
interface ChatLabMember {
|
||||
@@ -204,6 +205,7 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
|
||||
timestamp: msg.timestamp,
|
||||
type: msg.type,
|
||||
content: msg.content,
|
||||
replyToMessageId: msg.replyToMessageId,
|
||||
})
|
||||
|
||||
messagesProcessed++
|
||||
|
||||
@@ -387,6 +387,7 @@ async function* parseDiscordExporter(options: ParseOptions): AsyncGenerator<Pars
|
||||
timestamp: parseTimestamp(msg.timestamp),
|
||||
type: messageType,
|
||||
content: content || null,
|
||||
replyToMessageId: msg.reference?.messageId || undefined,
|
||||
})
|
||||
|
||||
messagesProcessed++
|
||||
|
||||
@@ -181,6 +181,8 @@ function createDatabaseWithoutIndexes(sessionId: string): Database.Database {
|
||||
ts INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
content TEXT,
|
||||
reply_to_message_id TEXT DEFAULT NULL,
|
||||
platform_message_id TEXT DEFAULT NULL,
|
||||
FOREIGN KEY(sender_id) REFERENCES member(id)
|
||||
);
|
||||
`)
|
||||
@@ -195,6 +197,7 @@ function createIndexes(db: Database.Database): void {
|
||||
db.exec(`
|
||||
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_platform_id ON message(platform_message_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 insertMessage = db.prepare(`
|
||||
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO message (sender_id, sender_account_name, sender_group_nickname, ts, type, content, reply_to_message_id, platform_message_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
const insertNameHistory = db.prepare(`
|
||||
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.timestamp,
|
||||
msg.type,
|
||||
safeContent
|
||||
safeContent,
|
||||
msg.replyToMessageId || null,
|
||||
msg.platformMessageId || null
|
||||
)
|
||||
messageInsertTime += Date.now() - t0
|
||||
messageCountInBatch++
|
||||
|
||||
@@ -21,6 +21,9 @@ export interface MessageResult {
|
||||
content: string
|
||||
timestamp: number
|
||||
type: number
|
||||
replyToMessageId: string | null
|
||||
replyToContent: string | null
|
||||
replyToSenderName: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +56,9 @@ interface DbMessageRow {
|
||||
content: string
|
||||
timestamp: 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) : '',
|
||||
timestamp: Number(row.timestamp),
|
||||
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,
|
||||
msg.content,
|
||||
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
|
||||
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
|
||||
${timeCondition}
|
||||
${SYSTEM_FILTER}
|
||||
@@ -218,9 +232,14 @@ export function getAllRecentMessages(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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
|
||||
${timeCondition}
|
||||
ORDER BY msg.ts DESC
|
||||
@@ -296,9 +315,14 @@ export function searchMessages(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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}
|
||||
${timeCondition}
|
||||
${senderCondition}
|
||||
@@ -381,9 +405,14 @@ export function getMessageContext(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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})
|
||||
ORDER BY msg.id ASC
|
||||
`
|
||||
@@ -435,9 +464,14 @@ export function getMessagesBefore(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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 < ?
|
||||
${timeCondition}
|
||||
${keywordCondition}
|
||||
@@ -500,9 +534,14 @@ export function getMessagesAfter(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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 > ?
|
||||
${timeCondition}
|
||||
${keywordCondition}
|
||||
@@ -585,9 +624,14 @@ export function getConversationBetween(
|
||||
m.avatar,
|
||||
msg.content,
|
||||
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
|
||||
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 (?, ?)
|
||||
${timeCondition}
|
||||
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="[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
|
||||
class="whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-200"
|
||||
v-html="highlightContent(message.content || '')"
|
||||
@@ -320,12 +333,14 @@ function highlightContent(content: string): string {
|
||||
"zh-CN": {
|
||||
"viewContext": "查看上下文",
|
||||
"contextTitle": "消息上下文(前后各10条)",
|
||||
"noContext": "暂无上下文"
|
||||
"noContext": "暂无上下文",
|
||||
"replyTo": "回复"
|
||||
},
|
||||
"en-US": {
|
||||
"viewContext": "View Context",
|
||||
"contextTitle": "Message Context (10 before and after)",
|
||||
"noContext": "No context available"
|
||||
"noContext": "No context available",
|
||||
"replyTo": "Reply to"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
@@ -168,6 +168,7 @@ export interface DbMessage {
|
||||
ts: number // 时间戳(秒)
|
||||
type: MessageType // 消息类型
|
||||
content: string | null // 纯文本内容
|
||||
reply_to_message_id: string | null // 回复的目标消息 ID(平台原始 ID)
|
||||
}
|
||||
|
||||
// ==================== Parser 解析结果 ====================
|
||||
@@ -187,12 +188,14 @@ export interface ParsedMember {
|
||||
* 解析后的消息
|
||||
*/
|
||||
export interface ParsedMessage {
|
||||
platformMessageId?: string // 消息的平台原始 ID(用于回复关联查询)
|
||||
senderPlatformId: string // 发送者平台ID
|
||||
senderAccountName: string // 发送时的账号名称
|
||||
senderGroupNickname?: string // 发送时的群昵称(可为空)
|
||||
timestamp: number // 时间戳(秒)
|
||||
type: MessageType // 消息类型
|
||||
content: string | null // 内容
|
||||
replyToMessageId?: string // 回复的目标消息 ID(平台原始 ID,可为空)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -182,4 +182,7 @@ export interface ChatRecordMessage {
|
||||
content: string
|
||||
timestamp: number
|
||||
type: number
|
||||
replyToMessageId: string | null // 回复的目标消息 ID(平台原始 ID)
|
||||
replyToContent: string | null // 被回复消息的内容预览
|
||||
replyToSenderName: string | null // 被回复消息的发送者名称
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user