feat: 新增群成员管理 & 改造成员表模型

This commit is contained in:
digua
2025-12-04 23:49:57 +08:00
parent f1ae1c8e32
commit ac99203075
29 changed files with 1516 additions and 261 deletions

View File

@@ -70,7 +70,7 @@ export function getNightOwlAnalysis(sessionId: string, filter?: TimeFilter): any
msg.sender_id as senderId,
msg.ts,
m.platform_id as platformId,
m.name
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
@@ -347,7 +347,7 @@ export function getDragonKingAnalysis(sessionId: string, filter?: TimeFilter): a
strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date,
msg.sender_id,
m.platform_id,
m.name,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
COUNT(*) as msg_count
FROM message msg
JOIN member m ON msg.sender_id = m.id
@@ -417,7 +417,7 @@ export function getDivingAnalysis(sessionId: string, filter?: TimeFilter): any {
SELECT
m.id as member_id,
m.platform_id,
m.name,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
MAX(msg.ts) as last_ts
FROM member m
JOIN message msg ON m.id = msg.sender_id
@@ -475,7 +475,7 @@ export function getCheckInAnalysis(sessionId: string, filter?: TimeFilter): any
`
SELECT
msg.sender_id as senderId,
m.name,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
DATE(${tsExpr}, 'unixepoch', 'localtime') as day
FROM message msg
JOIN member m ON msg.sender_id = m.id

View File

@@ -20,9 +20,9 @@ export function getMonologueAnalysis(sessionId: string, filter?: TimeFilter): an
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND m.name != '系统消息' AND msg.type = 0"
whereClause += " AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0"
} else {
whereClause = " WHERE m.name != '系统消息' AND msg.type = 0"
whereClause = " WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0"
}
const messages = db
@@ -33,7 +33,7 @@ export function getMonologueAnalysis(sessionId: string, filter?: TimeFilter): an
msg.sender_id as senderId,
msg.ts,
m.platform_id as platformId,
m.name
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}
@@ -200,7 +200,7 @@ export function getMemeBattleAnalysis(sessionId: string, filter?: TimeFilter): a
msg.type,
msg.ts,
m.platform_id as platformId,
m.name
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}

View File

@@ -30,9 +30,9 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND m.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 m.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
@@ -44,7 +44,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
msg.content,
msg.ts,
m.platform_id as platformId,
m.name
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}
@@ -259,10 +259,10 @@ export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter):
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause +=
" AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
" AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
} else {
whereClause =
" WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
" WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
}
const rows = db
@@ -271,7 +271,7 @@ export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter):
SELECT
m.id as memberId,
m.platform_id as platformId,
m.name,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
TRIM(msg.content) as content,
COUNT(*) as count
FROM message msg

View File

@@ -27,9 +27,9 @@ export function getMentionAnalysis(sessionId: string, filter?: TimeFilter): any
const members = db
.prepare(
`
SELECT id, platform_id as platformId, name
SELECT id, platform_id as platformId, COALESCE(group_nickname, account_name, platform_id) as name
FROM member
WHERE name != '系统消息'
WHERE COALESCE(account_name, '') != '系统消息'
`
)
.all() as Array<{ id: number; platformId: string; name: string }>
@@ -67,9 +67,9 @@ export function getMentionAnalysis(sessionId: string, filter?: TimeFilter): any
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
whereClause += " AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
} else {
whereClause = " WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
whereClause = " WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
}
const messages = db
@@ -352,9 +352,9 @@ export function getLaughAnalysis(sessionId: string, filter?: TimeFilter, keyword
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL"
whereClause += " AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL"
} else {
whereClause = " WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL"
whereClause = " WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL"
}
const messages = db
@@ -364,7 +364,7 @@ export function getLaughAnalysis(sessionId: string, filter?: TimeFilter, keyword
msg.sender_id as senderId,
msg.content,
m.platform_id as platformId,
m.name
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}

View File

@@ -38,7 +38,7 @@ export function getRecentMessages(
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 排除系统消息只获取文本消息type=0
const systemFilter = "AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content != ''"
const systemFilter = "AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content != ''"
// 查询总数
const countSql = `
@@ -56,7 +56,7 @@ export function getRecentMessages(
const sql = `
SELECT
msg.id,
m.name as senderName,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
@@ -79,63 +79,79 @@ export function getRecentMessages(
/**
* 关键词搜索消息
* @param sessionId 会话 ID
* @param keywords 关键词数组OR 逻辑)
* @param keywords 关键词数组OR 逻辑),可以为空数组
* @param filter 时间过滤器
* @param limit 返回数量限制
* @param offset 偏移量(分页)
* @param senderId 可选的发送者成员 ID用于筛选特定成员的消息
*/
export function searchMessages(
sessionId: string,
keywords: string[],
filter?: TimeFilter,
limit: number = 20,
offset: number = 0
offset: number = 0,
senderId?: number
): { messages: SearchMessageResult[]; total: number } {
const db = openDatabase(sessionId)
if (!db) return { messages: [], total: 0 }
// 构建关键词条件OR 逻辑)
const keywordConditions = keywords.map(() => `msg.content LIKE ?`).join(' OR ')
const keywordParams = keywords.map((k) => `%${k}%`)
let keywordCondition = '1=1' // 默认条件(始终为真)
const keywordParams: string[] = []
if (keywords.length > 0) {
keywordCondition = `(${keywords.map(() => `msg.content LIKE ?`).join(' OR ')})`
keywordParams.push(...keywords.map((k) => `%${k}%`))
}
// 构建时间过滤条件
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 排除系统消息
const systemFilter = "AND m.name != '系统消息'"
const systemFilter = "AND COALESCE(m.account_name, '') != '系统消息'"
// 构建发送者筛选条件
let senderCondition = ''
const senderParams: number[] = []
if (senderId !== undefined) {
senderCondition = 'AND msg.sender_id = ?'
senderParams.push(senderId)
}
// 查询总数
const countSql = `
SELECT COUNT(*) as total
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE (${keywordConditions})
WHERE ${keywordCondition}
${timeCondition}
${systemFilter}
${senderCondition}
`
const totalRow = db.prepare(countSql).get(...keywordParams, ...timeParams) as { total: number }
const totalRow = db.prepare(countSql).get(...keywordParams, ...timeParams, ...senderParams) as { total: number }
const total = totalRow?.total || 0
// 查询消息
const sql = `
SELECT
msg.id,
m.name as senderName,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
msg.type
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE (${keywordConditions})
WHERE ${keywordCondition}
${timeCondition}
${systemFilter}
${senderCondition}
ORDER BY msg.ts DESC
LIMIT ? OFFSET ?
`
const rows = db.prepare(sql).all(...keywordParams, ...timeParams, limit, offset) as SearchMessageResult[]
const rows = db.prepare(sql).all(...keywordParams, ...timeParams, ...senderParams, limit, offset) as SearchMessageResult[]
return { messages: rows, total }
}
@@ -159,14 +175,14 @@ export function getMessageContext(
const sql = `
SELECT
msg.id,
m.name as senderName,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
msg.type
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE m.name != '系统消息'
WHERE COALESCE(m.account_name, '') != '系统消息'
AND msg.ts BETWEEN ? AND ?
ORDER BY msg.ts ASC
LIMIT ?
@@ -183,3 +199,82 @@ export function getMessageContext(
return rows
}
/**
* 获取两个成员之间的对话
* 提取两人相邻发言形成的对话片段
* @param sessionId 会话 ID
* @param memberId1 成员1的 ID
* @param memberId2 成员2的 ID
* @param filter 时间过滤器
* @param limit 返回消息数量限制
*/
export function getConversationBetween(
sessionId: string,
memberId1: number,
memberId2: number,
filter?: TimeFilter,
limit: number = 100
): { messages: SearchMessageResult[]; total: number; member1Name: string; member2Name: string } {
const db = openDatabase(sessionId)
if (!db) return { messages: [], total: 0, member1Name: '', member2Name: '' }
// 获取成员名称
const member1 = db.prepare(`
SELECT COALESCE(group_nickname, account_name, platform_id) as name
FROM member WHERE id = ?
`).get(memberId1) as { name: string } | undefined
const member2 = db.prepare(`
SELECT COALESCE(group_nickname, account_name, platform_id) as name
FROM member WHERE id = ?
`).get(memberId2) as { name: string } | undefined
if (!member1 || !member2) {
return { messages: [], total: 0, member1Name: '', member2Name: '' }
}
// 构建时间过滤条件
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 查询两人之间的所有消息
const countSql = `
SELECT COUNT(*) as total
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE msg.sender_id IN (?, ?)
${timeCondition}
AND msg.content IS NOT NULL AND msg.content != ''
`
const totalRow = db.prepare(countSql).get(memberId1, memberId2, ...timeParams) as { total: number }
const total = totalRow?.total || 0
// 查询消息
const sql = `
SELECT
msg.id,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
msg.type
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE msg.sender_id IN (?, ?)
${timeCondition}
AND msg.content IS NOT NULL AND msg.content != ''
ORDER BY msg.ts DESC
LIMIT ?
`
const rows = db.prepare(sql).all(memberId1, memberId2, ...timeParams, limit) as SearchMessageResult[]
// 返回时按时间正序排列(便于阅读对话)
return {
messages: rows.reverse(),
total,
member1Name: member1.name,
member2Name: member2.name,
}
}

View File

@@ -6,7 +6,15 @@
import Database from 'better-sqlite3'
import * as fs from 'fs'
import * as path from 'path'
import { openDatabase, getDbDir, getDbPath, buildTimeFilter, buildSystemMessageFilter, type TimeFilter } from '../core'
import {
openDatabase,
closeDatabase,
getDbDir,
getDbPath,
buildTimeFilter,
buildSystemMessageFilter,
type TimeFilter,
} from '../core'
// ==================== 基础查询 ====================
@@ -40,7 +48,7 @@ export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[]
const { clause, params } = buildTimeFilter(filter)
const msgFilterBase = clause ? clause.replace('WHERE', 'AND') : ''
const msgFilterWithSystem = msgFilterBase + " AND m.name != '系统消息'"
const msgFilterWithSystem = msgFilterBase + " AND COALESCE(m.account_name, '') != '系统消息'"
const totalClauseWithSystem = buildSystemMessageFilter(clause)
const totalMessages = (
@@ -60,11 +68,11 @@ export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[]
SELECT
m.id as memberId,
m.platform_id as platformId,
m.name,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id ${msgFilterWithSystem}
WHERE m.name != '系统消息'
WHERE COALESCE(m.account_name, '') != '系统消息'
GROUP BY m.id
HAVING messageCount > 0
ORDER BY messageCount DESC
@@ -287,13 +295,13 @@ export function getMemberNameHistory(sessionId: string, memberId: number): any[]
const rows = db
.prepare(
`
SELECT name, start_ts as startTs, end_ts as endTs
SELECT name_type as nameType, 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 }>
.all(memberId) as Array<{ nameType: string; name: string; startTs: number; endTs: number | null }>
return rows
}
@@ -336,7 +344,7 @@ export function getAllSessions(): any[] {
`SELECT COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE m.name != '系统消息'`
WHERE COALESCE(m.account_name, '') != '系统消息'`
)
.get() as { count: number }
).count
@@ -345,7 +353,7 @@ export function getAllSessions(): any[] {
.prepare(
`SELECT COUNT(*) as count
FROM member
WHERE name != '系统消息'`
WHERE COALESCE(account_name, '') != '系统消息'`
)
.get() as { count: number }
).count
@@ -387,7 +395,7 @@ export function getSession(sessionId: string): any | null {
`SELECT COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE m.name != '系统消息'`
WHERE COALESCE(m.account_name, '') != '系统消息'`
)
.get() as { count: number }
).count
@@ -397,7 +405,7 @@ export function getSession(sessionId: string): any | null {
.prepare(
`SELECT COUNT(*) as count
FROM member
WHERE name != '系统消息'`
WHERE COALESCE(account_name, '') != '系统消息'`
)
.get() as { count: number }
).count
@@ -414,3 +422,159 @@ export function getSession(sessionId: string): any | null {
}
}
// ==================== 成员管理 ====================
/**
* 成员信息(含统计数据)
*/
interface MemberWithStats {
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string[]
messageCount: number
}
// 用于标记已检查过 aliases 字段的会话
const aliasesCheckedSessions = new Set<string>()
/**
* 确保 member 表有 aliases 字段(数据库迁移)
* 用于兼容旧数据库
*/
function ensureAliasesColumn(sessionId: string): void {
// 每个会话只检查一次
if (aliasesCheckedSessions.has(sessionId)) return
const dbPath = getDbPath(sessionId)
if (!fs.existsSync(dbPath)) return
// 先关闭可能缓存的只读连接
closeDatabase(sessionId)
// 使用写入模式打开数据库检查并添加字段
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
try {
// 检查 aliases 字段是否存在
const columns = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
const hasAliases = columns.some((col) => col.name === 'aliases')
if (!hasAliases) {
// 添加 aliases 字段
db.exec("ALTER TABLE member ADD COLUMN aliases TEXT DEFAULT '[]'")
console.log(`[Worker] Added aliases column to member table in session ${sessionId}`)
}
// 标记为已检查
aliasesCheckedSessions.add(sessionId)
} finally {
db.close()
}
}
/**
* 获取所有成员列表(含消息数和别名)
*/
export function getMembers(sessionId: string): MemberWithStats[] {
// 先确保数据库有 aliases 字段(兼容旧数据库)
ensureAliasesColumn(sessionId)
const db = openDatabase(sessionId)
if (!db) return []
const rows = db
.prepare(
`
SELECT
m.id,
m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE COALESCE(m.group_nickname, m.account_name, m.platform_id) != '系统消息'
GROUP BY m.id
ORDER BY messageCount DESC
`
)
.all() as Array<{
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
messageCount: number
}>
return rows.map((row) => ({
id: row.id,
platformId: row.platformId,
accountName: row.accountName,
groupNickname: row.groupNickname,
aliases: row.aliases ? JSON.parse(row.aliases) : [],
messageCount: row.messageCount,
}))
}
/**
* 更新成员别名
*/
export function updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): boolean {
const dbPath = getDbPath(sessionId)
if (!fs.existsSync(dbPath)) {
return false
}
try {
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
const stmt = db.prepare('UPDATE member SET aliases = ? WHERE id = ?')
stmt.run(JSON.stringify(aliases), memberId)
db.close()
return true
} catch (error) {
console.error('[Worker] Failed to update member aliases:', error)
return false
}
}
/**
* 删除成员及其所有消息
*/
export function deleteMember(sessionId: string, memberId: number): boolean {
const dbPath = getDbPath(sessionId)
if (!fs.existsSync(dbPath)) {
return false
}
try {
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
// 使用事务删除成员及其相关数据
const deleteTransaction = db.transaction(() => {
// 1. 删除该成员的消息
db.prepare('DELETE FROM message WHERE sender_id = ?').run(memberId)
// 2. 删除该成员的昵称历史
db.prepare('DELETE FROM member_name_history WHERE member_id = ?').run(memberId)
// 3. 删除成员记录
db.prepare('DELETE FROM member WHERE id = ?').run(memberId)
})
deleteTransaction()
db.close()
return true
} catch (error) {
console.error('[Worker] Failed to delete member:', error)
return false
}
}

View File

@@ -16,6 +16,10 @@ export {
getMemberNameHistory,
getAllSessions,
getSession,
// 成员管理
getMembers,
updateMemberAliases,
deleteMember,
} from './basic'
// 高级分析
@@ -33,5 +37,4 @@ export {
} from './advanced'
// AI 查询
export { searchMessages, getMessageContext, getRecentMessages } from './ai'
export { searchMessages, getMessageContext, getRecentMessages, getConversationBetween } from './ai'