profile: require pending messages for bulk runs

This commit is contained in:
2026-08-04 23:57:10 +08:00
parent 9d2a155cf6
commit 31803396a8
5 changed files with 64 additions and 12 deletions
+13 -3
View File
@@ -90,20 +90,30 @@ object PluginCommands : CompositeCommand(
suspend fun CommandSender.profileAnalyzeGroup(groupIds: String = "", batches: Int = 1) {
require(batches > 0) { "batches 必须是正数" }
val analyzeAllGroups = groupIds.isBlank()
val minimumMessages = PluginConfig.profileBulkGroupMinPendingMessages.coerceAtLeast(1)
val parsedGroupIds = if (analyzeAllGroups) {
UserProfileAnalysisService.listPendingHistoryGroupIds()
UserProfileAnalysisService.listPendingHistoryGroupIds(minimumMessages)
} else {
parseProfileGroupIds(groupIds)
}
if (parsedGroupIds.isEmpty()) {
sendMessage("没有待推进的群画像:历史库中无有效群消息,或所有群均已追平当前快照。")
sendMessage(
if (analyzeAllGroups) {
"没有达到启动条件的群画像:历史库中无有效群消息、群已追平," +
"或尚未处理的消息少于 $minimumMessages 条。"
} else {
"没有可推进的指定群画像。"
}
)
return
}
val runToken = UserProfileAnalysisService.newRunToken()
sendMessage(
"已启动 ${parsedGroupIds.size} 个群的画像分析,每群最多推进 $batches 个批次。" +
if (analyzeAllGroups) {
" 画像请求并发上限 ${normalizeMaxConcurrentRequests(PluginConfig.profileMaxConcurrentRequests)}"
" 全量启动门槛 $minimumMessages 条待处理消息," +
"画像请求并发上限 " +
"${normalizeMaxConcurrentRequests(PluginConfig.profileMaxConcurrentRequests)}"
} else {
""
}
+3
View File
@@ -96,6 +96,9 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("每个画像分析批次最多读取目标用户多少条消息")
val profileBatchTargetMessages: Int by value(120)
@ValueDescription("全量推进群画像时,群内至少有多少条尚未处理的消息才启动;显式指定群号不受限制")
val profileBulkGroupMinPendingMessages: Int by value(20)
@ValueDescription("每个画像分析批次最多包含多少个离散对话片段;同一秒的消息仍会一起处理")
val profileBatchMaxEpisodes: Int by value(16)
@@ -32,17 +32,21 @@ object UserProfileAnalysisService {
return report
}
suspend fun listPendingHistoryGroupIds(): List<Long> {
suspend fun listPendingHistoryGroupIds(
minimumMessages: Int = PluginConfig.profileBulkGroupMinPendingMessages,
): List<Long> {
check(PluginConfig.profileEnabled) { "历史用户画像分析未启用" }
check(UserProfileStore.isAvailable) { "用户画像数据库不可用" }
return withContext(Dispatchers.IO) {
val historyBounds = ProfileHistoryReader(resolveHistoryFile()).listGroupTimeBounds()
val reader = ProfileHistoryReader(resolveHistoryFile())
val historyBounds = reader.listGroupTimeBounds()
val cursors = UserProfileStore.loadGroupCursors()
.associateBy { cursor -> cursor.botId to cursor.groupId }
historyBounds.asSequence()
.filter { bounds ->
isGroupAnalysisPending(bounds, cursors[bounds.botId to bounds.groupId])
}
val pendingRanges = historyBounds.mapNotNull { bounds ->
pendingGroupAnalysisRange(bounds, cursors[bounds.botId to bounds.groupId])
}
reader.filterGroupRangesByMinimumMessageCount(pendingRanges, minimumMessages)
.asSequence()
.map(ProfileHistoryReader.GroupTimeBounds::groupId)
.toList()
}
@@ -758,4 +762,14 @@ object UserProfileAnalysisService {
internal fun isGroupAnalysisPending(
bounds: ProfileHistoryReader.GroupTimeBounds,
cursor: GroupProfileCursor?,
): Boolean = cursor == null || cursor.cursorTime < maxOf(cursor.snapshotEndTime, bounds.endTime)
): Boolean = pendingGroupAnalysisRange(bounds, cursor) != null
internal fun pendingGroupAnalysisRange(
bounds: ProfileHistoryReader.GroupTimeBounds,
cursor: GroupProfileCursor?,
): ProfileHistoryReader.GroupTimeBounds? {
val startTime = cursor?.cursorTime ?: bounds.startTime
val endTime = maxOf(cursor?.snapshotEndTime ?: bounds.endTime, bounds.endTime)
return bounds.copy(startTime = startTime, endTime = endTime)
.takeIf { startTime < endTime }
}
@@ -47,6 +47,28 @@ class UserProfileAnalysisServiceTest {
GroupProfileCursor(BOT, GROUP, cursorTime = 500, snapshotEndTime = 500),
)
)
assertEquals(bounds, pendingGroupAnalysisRange(bounds, null))
assertEquals(
bounds.copy(startTime = 300),
pendingGroupAnalysisRange(
bounds,
GroupProfileCursor(BOT, GROUP, cursorTime = 300, snapshotEndTime = 500),
)
)
assertEquals(
bounds.copy(startTime = 400, endTime = 600),
pendingGroupAnalysisRange(
bounds,
GroupProfileCursor(BOT, GROUP, cursorTime = 400, snapshotEndTime = 600),
)
)
assertNull(
pendingGroupAnalysisRange(
bounds,
GroupProfileCursor(BOT, GROUP, cursorTime = 500, snapshotEndTime = 500),
)
)
}
@Test