profile: add progressive user profiles

Add cache-expiry profile maintenance and contextual injection, reorganize runtime code by responsibility, remove automatic favorability decay, and prepare version 1.15.0.
This commit is contained in:
2026-08-02 21:45:34 +08:00
parent 2ed39e9fe8
commit 23299b2ae7
59 changed files with 4799 additions and 1340 deletions
@@ -0,0 +1,122 @@
package top.jie65535.mirai.profile
import net.mamoe.mirai.message.data.MessageSourceKind
import java.nio.file.Files
import java.sql.DriverManager
import kotlin.io.path.absolutePathString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ProfileHistoryReaderTest {
@Test
fun keepsEqualTimestampTargetMessagesTogetherAndLoadsGroupContext() {
val directory = Files.createTempDirectory("jchatgpt-profile-history-test-")
val database = directory.resolve("history.sqlite")
try {
DriverManager.getConnection("jdbc:sqlite:${database.absolutePathString()}").use { connection ->
connection.createStatement().use { statement ->
statement.executeUpdate(
"""
CREATE TABLE message_record(
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_id INTEGER NOT NULL,
from_id INTEGER NOT NULL,
target_id INTEGER NOT NULL,
ids TEXT,
internal_ids TEXT,
time INTEGER NOT NULL,
kind INTEGER NOT NULL,
code TEXT NOT NULL,
recalled INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
}
connection.prepareStatement(
"INSERT INTO message_record(" +
"bot_id, from_id, target_id, time, kind, code, recalled" +
") VALUES (1, ?, ?, ?, ?, ?, 0)"
).use { statement ->
fun insert(fromId: Long, groupId: Long, time: Int, text: String) {
statement.setLong(1, fromId)
statement.setLong(2, groupId)
statement.setInt(3, time)
statement.setInt(4, MessageSourceKind.GROUP.ordinal)
statement.setString(5, """[{"type":"PlainText","content":"$text"}]""")
statement.executeUpdate()
}
insert(TARGET, 10, 100, "目标发言一")
insert(OTHER, 10, 110, "用于理解语境的回复")
insert(TARGET, 10, 130, "目标发言二")
insert(TARGET, 20, 130, "同一秒的另一群发言")
insert(OTHER, 20, 140, "后续上下文")
insert(TARGET, 10, 200, "下一批目标发言")
}
}
val reader = ProfileHistoryReader(database.toFile())
val bounds = assertNotNull(reader.findUserTimeBounds(TARGET))
assertEquals(100, bounds.startTime)
assertEquals(201, bounds.endTime)
val first = assertNotNull(
reader.loadNextBatch(
userId = TARGET,
startTime = bounds.startTime,
snapshotEndTime = bounds.endTime,
targetMessageLimit = 2,
maxEpisodes = 1,
episodeGapSeconds = 60,
contextBeforeMessages = 2,
contextAfterMessages = 2,
contextCoreMessages = 20,
maxMessageChars = 200,
)
)
assertEquals(131, first.endTime)
assertEquals(3, first.evidenceByRef.values.count { it.record.fromId == TARGET })
assertTrue(first.messages.any { it.record.fromId == OTHER })
assertEquals("TARGET", first.aliases[TARGET])
val second = assertNotNull(
reader.loadNextBatch(
userId = TARGET,
startTime = first.endTime,
snapshotEndTime = bounds.endTime,
targetMessageLimit = 2,
maxEpisodes = 10,
episodeGapSeconds = 60,
contextBeforeMessages = 2,
contextAfterMessages = 2,
contextCoreMessages = 20,
maxMessageChars = 200,
)
)
assertEquals(201, second.endTime)
assertEquals(1, second.evidenceByRef.values.count { it.record.fromId == TARGET })
val conversation = assertNotNull(
reader.loadConversationBatch(
botId = 1,
groupId = 10,
startTime = 90,
endTime = 150,
messageLimit = 10,
maxMessageChars = 200,
)
)
assertTrue(conversation.authoredTextCharsByUser.getValue(TARGET) > 0)
assertTrue(conversation.messages.all { it.record.targetId == 10L })
assertTrue(conversation.messages.all { it.record.time in 90 until 150 })
} finally {
directory.toFile().deleteRecursively()
}
}
companion object {
private const val TARGET = 100L
private const val OTHER = 200L
}
}