profile: analyze all groups concurrently

This commit is contained in:
2026-08-03 15:38:28 +08:00
parent 2ba5752494
commit b96b732b92
5 changed files with 105 additions and 12 deletions
+60 -7
View File
@@ -33,6 +33,7 @@ import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.atomic.AtomicInteger
object PluginCommands : CompositeCommand(
JChatGPT, "jgpt", description = "J OpenAI ChatGPT"
@@ -85,11 +86,28 @@ object PluginCommands : CompositeCommand(
}
@SubCommand
suspend fun CommandSender.profileAnalyzeGroup(groupIds: String, batches: Int = 1) {
suspend fun CommandSender.profileAnalyzeGroup(groupIds: String = "", batches: Int = 1) {
require(batches > 0) { "batches 必须是正数" }
val parsedGroupIds = parseProfileGroupIds(groupIds)
val analyzeAllGroups = groupIds.isBlank()
val parsedGroupIds = if (analyzeAllGroups) {
UserProfileAnalysisService.listHistoryGroupIds()
} else {
parseProfileGroupIds(groupIds)
}
if (parsedGroupIds.isEmpty()) {
sendMessage("聊天记录中没有可分析的群消息。")
return
}
val runToken = UserProfileAnalysisService.newRunToken()
sendMessage("已启动 ${parsedGroupIds.size} 个群的画像分析,每群最多推进 $batches 个批次。")
sendMessage(
"已启动 ${parsedGroupIds.size} 个群的画像分析,每群最多推进 $batches 个批次。" +
if (analyzeAllGroups) " 全量模式不设并发上限。" else ""
)
val completedGroups = AtomicInteger()
val successfulGroups = AtomicInteger()
val alreadyRunningGroups = AtomicInteger()
val missingGroups = AtomicInteger()
val failedGroups = AtomicInteger()
parsedGroupIds.forEach { groupId ->
JChatGPT.launch {
try {
@@ -104,15 +122,50 @@ object PluginCommands : CompositeCommand(
)
}
when {
report.alreadyRunning -> sendMessage("$groupId 已有画像分析任务在运行。")
report.botId == null -> sendMessage("聊天记录中没有找到群 $groupId 的消息。")
else -> sendMessage(formatGroupProfileReport(report))
report.alreadyRunning -> {
alreadyRunningGroups.incrementAndGet()
if (analyzeAllGroups) {
JChatGPT.logger.info("$groupId 已有画像分析任务在运行")
} else {
sendMessage("$groupId 已有画像分析任务在运行。")
}
}
report.botId == null -> {
missingGroups.incrementAndGet()
if (analyzeAllGroups) {
JChatGPT.logger.warning("聊天记录中没有找到群 $groupId 的消息")
} else {
sendMessage("聊天记录中没有找到群 $groupId 的消息。")
}
}
else -> {
successfulGroups.incrementAndGet()
val resultMessage = formatGroupProfileReport(report)
if (analyzeAllGroups) {
JChatGPT.logger.info(resultMessage)
} else {
sendMessage(resultMessage)
}
}
}
} catch (cause: CancellationException) {
throw cause
} catch (cause: Exception) {
failedGroups.incrementAndGet()
JChatGPT.logger.error("$groupId 批量画像分析失败", cause)
sendMessage("$groupId 批量画像分析失败:${cause.message ?: cause::class.simpleName}")
if (!analyzeAllGroups) {
sendMessage("$groupId 批量画像分析失败:${cause.message ?: cause::class.simpleName}")
}
} finally {
if (analyzeAllGroups && completedGroups.incrementAndGet() == parsedGroupIds.size) {
sendMessage(
"全量群画像分析完成:成功 ${successfulGroups.get()}" +
"已在运行 ${alreadyRunningGroups.get()}" +
"无历史 ${missingGroups.get()},失败 ${failedGroups.get()}"
)
}
}
}
}
@@ -70,6 +70,25 @@ class ProfileHistoryReader(private val databaseFile: File) {
}
}
fun listGroupIds(): List<Long> = openReadConnection().use { connection ->
connection.prepareStatement(
"""
SELECT target_id, MAX(time) AS last_message_time
FROM message_record
WHERE kind = ? AND recalled = 0 AND target_id > 0
GROUP BY target_id
ORDER BY last_message_time DESC, target_id ASC
""".trimIndent()
).use { statement ->
statement.setInt(1, MessageSourceKind.GROUP.ordinal)
statement.executeQuery().use { results ->
buildList {
while (results.next()) add(results.getLong("target_id"))
}
}
}
}
fun loadNextBatch(
userId: Long,
startTime: Int,
@@ -32,6 +32,14 @@ object UserProfileAnalysisService {
return report
}
suspend fun listHistoryGroupIds(): List<Long> {
check(PluginConfig.profileEnabled) { "历史用户画像分析未启用" }
check(UserProfileStore.isAvailable) { "用户画像数据库不可用" }
return withContext(Dispatchers.IO) {
ProfileHistoryReader(resolveHistoryFile()).listGroupIds()
}
}
suspend fun analyze(
userId: Long,
maxBatches: Int,
@@ -37,14 +37,22 @@ class ProfileHistoryReaderTest {
connection.prepareStatement(
"INSERT INTO message_record(" +
"bot_id, from_id, target_id, time, kind, code, recalled" +
") VALUES (1, ?, ?, ?, ?, ?, 0)"
") VALUES (1, ?, ?, ?, ?, ?, ?)"
).use { statement ->
fun insert(fromId: Long, groupId: Long, time: Int, text: String) {
fun insert(
fromId: Long,
groupId: Long,
time: Int,
text: String,
kind: MessageSourceKind = MessageSourceKind.GROUP,
recalled: Int = 0,
) {
statement.setLong(1, fromId)
statement.setLong(2, groupId)
statement.setInt(3, time)
statement.setInt(4, MessageSourceKind.GROUP.ordinal)
statement.setInt(4, kind.ordinal)
statement.setString(5, """[{"type":"PlainText","content":"$text"}]""")
statement.setInt(6, recalled)
statement.executeUpdate()
}
insert(TARGET, 10, 100, "目标发言一")
@@ -54,10 +62,13 @@ class ProfileHistoryReaderTest {
insert(TARGET, 20, 130, "同一秒的另一群发言")
insert(OTHER, 20, 140, "后续上下文")
insert(TARGET, 10, 200, "下一批目标发言")
insert(TARGET, 30, 300, "私聊消息", kind = MessageSourceKind.FRIEND)
insert(TARGET, 40, 400, "已撤回群消息", recalled = 1)
}
}
val reader = ProfileHistoryReader(database.toFile())
assertEquals(listOf(10L, 20L), reader.listGroupIds())
val bounds = assertNotNull(reader.findUserTimeBounds(TARGET))
assertEquals(100, bounds.startTime)
assertEquals(201, bounds.endTime)