conversation: deduplicate profile context

This commit is contained in:
2026-08-05 20:42:44 +08:00
parent f416d889b3
commit 3c69bdeff4
6 changed files with 325 additions and 65 deletions
@@ -48,6 +48,7 @@ internal data class ConversationCache(
val lastActivityAt: Int, val lastActivityAt: Int,
val replyIndex: ReplyIndex, val replyIndex: ReplyIndex,
val imageIndex: ImageIndex, val imageIndex: ImageIndex,
val profileInjectionState: UserProfileInjectionState,
) { ) {
fun isExpired(ttlSeconds: Int): Boolean = fun isExpired(ttlSeconds: Int): Boolean =
OffsetDateTime.now().toEpochSecond().toInt() - lastActivityAt > ttlSeconds OffsetDateTime.now().toEpochSecond().toInt() - lastActivityAt > ttlSeconds
@@ -153,7 +154,7 @@ internal object ConversationContext {
return prompt.toString() return prompt.toString()
} }
fun getHistory(event: MessageEvent): String { fun getHistory(event: MessageEvent, profileInjectionState: UserProfileInjectionState): String {
val imageIndex = activeImageIndex(event.subject.id) val imageIndex = activeImageIndex(event.subject.id)
if (!JChatGPT.includeHistory) { if (!JChatGPT.includeHistory) {
return formatRecordContent(event.message, event.subject, imageIndex) return formatRecordContent(event.message, event.subject, imageIndex)
@@ -162,10 +163,20 @@ internal object ConversationContext {
.minusMinutes(PluginConfig.historyWindowMin.toLong()) .minusMinutes(PluginConfig.historyWindowMin.toLong())
.toEpochSecond() .toEpochSecond()
.toInt() .toInt()
return getAfterHistory(beforeTimestamp, event, PluginConfig.historyMessageLimit) return getAfterHistory(
time = beforeTimestamp,
event = event,
profileInjectionState = profileInjectionState,
limit = PluginConfig.historyMessageLimit,
)
} }
fun getAfterHistory(time: Int, event: MessageEvent, limit: Int? = null): String { fun getAfterHistory(
time: Int,
event: MessageEvent,
profileInjectionState: UserProfileInjectionState,
limit: Int? = null,
): String {
if (!JChatGPT.includeHistory) return "" if (!JChatGPT.includeHistory) return ""
val history = try { val history = try {
ChatHistoryStore.query( ChatHistoryStore.query(
@@ -191,7 +202,7 @@ internal object ConversationContext {
val replyIndex = activeReplyIndex(event.subject.id) val replyIndex = activeReplyIndex(event.subject.id)
val imageIndex = activeImageIndex(event.subject.id) val imageIndex = activeImageIndex(event.subject.id)
if (event is GroupMessageEvent) { if (event is GroupMessageEvent) {
appendUserProfileContext(result, history, event) appendUserProfileContext(result, history, event, profileInjectionState)
result.appendLine("## 近期群消息(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)") result.appendLine("## 近期群消息(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
history.forEach { record -> history.forEach { record ->
val showSender = lastUserId != record.fromId val showSender = lastUserId != record.fromId
@@ -201,7 +212,7 @@ internal object ConversationContext {
lastTime = record.time.toLong() lastTime = record.time.toLong()
} }
} else { } else {
appendPrivateUserContext(result, event) appendPrivateUserContext(result, event, profileInjectionState)
result.appendLine("## 近期对话(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)") result.appendLine("## 近期对话(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
history.forEach { record -> history.forEach { record ->
val showSender = lastUserId != record.fromId val showSender = lastUserId != record.fromId
@@ -273,6 +284,7 @@ internal object ConversationContext {
target: StringBuilder, target: StringBuilder,
history: List<ChatMessageRecord>, history: List<ChatMessageRecord>,
event: GroupMessageEvent, event: GroupMessageEvent,
profileInjectionState: UserProfileInjectionState,
) { ) {
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
val candidateIds = buildList { val candidateIds = buildList {
@@ -283,15 +295,21 @@ internal object ConversationContext {
.distinct() .distinct()
.take(PluginConfig.profileAutoInjectMaxUsers.coerceIn(1, 10)) .take(PluginConfig.profileAutoInjectMaxUsers.coerceIn(1, 10))
.toList() .toList()
val profiles = if (PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled && val shouldLoadProfiles = PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled
UserProfileStore.isAvailable val uncertainProfileIds = mutableSetOf<Long>()
) { val profiles = if (shouldLoadProfiles && UserProfileStore.isAvailable) {
candidateIds.mapNotNull { userId -> candidateIds.mapNotNull { userId ->
runCatching { UserProfileStore.load(userId) } runCatching { UserProfileStore.load(userId) }
.onFailure { JChatGPT.logger.warning("读取用户画像失败: user=$userId", it) } .onFailure {
uncertainProfileIds += userId
JChatGPT.logger.warning("读取用户画像失败: user=$userId", it)
}
.getOrNull() .getOrNull()
} }
} else emptyList() } else {
if (shouldLoadProfiles) uncertainProfileIds += candidateIds
emptyList()
}
val favorability = if (PluginConfig.enableFavorabilitySystem) { val favorability = if (PluginConfig.enableFavorabilitySystem) {
candidateIds.mapNotNull { id -> PluginData.userFavorability[id]?.let { id to it } }.toMap() candidateIds.mapNotNull { id -> PluginData.userFavorability[id]?.let { id to it } }.toMap()
} else emptyMap() } else emptyMap()
@@ -301,26 +319,41 @@ internal object ConversationContext {
val names = candidateIds.associateWith { id -> val names = candidateIds.associateWith { id ->
event.group[id]?.nameCardOrNick ?: snapshotNames[id] ?: id.toString() event.group[id]?.nameCardOrNick ?: snapshotNames[id] ?: id.toString()
} }
target.append( val renderedEntries = UserProfileContextRenderer.renderEntries(
UserProfileContextRenderer.render(
profiles = profiles, profiles = profiles,
favorabilityByUserId = favorability, favorabilityByUserId = favorability,
displayNames = names, displayNames = names,
activeUserIds = candidateIds.toSet(), activeUserIds = candidateIds.toSet(),
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars, summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
) )
val comparableCandidateIds = candidateIds.filter { userId ->
userId !in uncertainProfileIds || !profileInjectionState.hasSeen(userId)
}
target.append(
profileInjectionState.renderChanges(
candidateUserIds = comparableCandidateIds,
renderedEntries = renderedEntries,
sectionTitle = "你对相关群友的认识",
)
) )
} }
private fun appendPrivateUserContext(target: StringBuilder, event: MessageEvent) { private fun appendPrivateUserContext(
target: StringBuilder,
event: MessageEvent,
profileInjectionState: UserProfileInjectionState,
) {
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
val userId = event.sender.id val userId = event.sender.id
val profiles = if (PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled && val shouldLoadProfile = PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled
UserProfileStore.isAvailable var profileReadUncertain = shouldLoadProfile && !UserProfileStore.isAvailable
) { val profiles = if (shouldLoadProfile && UserProfileStore.isAvailable) {
listOfNotNull( listOfNotNull(
runCatching { UserProfileStore.load(userId) } runCatching { UserProfileStore.load(userId) }
.onFailure { JChatGPT.logger.warning("读取用户画像失败: user=$userId", it) } .onFailure {
profileReadUncertain = true
JChatGPT.logger.warning("读取用户画像失败: user=$userId", it)
}
.getOrNull() .getOrNull()
) )
} else emptyList() } else emptyList()
@@ -330,13 +363,21 @@ internal object ConversationContext {
val snapshotName = runCatching { val snapshotName = runCatching {
ContactSnapshotStore.loadDisplayName(event.bot.id, null, userId) ContactSnapshotStore.loadDisplayName(event.bot.id, null, userId)
}.getOrNull() }.getOrNull()
target.append( val renderedEntries = UserProfileContextRenderer.renderEntries(
UserProfileContextRenderer.render(
profiles = profiles, profiles = profiles,
favorabilityByUserId = favorability, favorabilityByUserId = favorability,
displayNames = mapOf(userId to (snapshotName ?: event.senderName)), displayNames = mapOf(userId to (snapshotName ?: event.senderName)),
activeUserIds = setOf(userId), activeUserIds = setOf(userId),
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars, summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
)
target.append(
profileInjectionState.renderChanges(
candidateUserIds = if (profileReadUncertain && profileInjectionState.hasSeen(userId)) {
emptyList()
} else {
listOf(userId)
},
renderedEntries = renderedEntries,
sectionTitle = "你对对方的认识", sectionTitle = "你对对方的认识",
) )
) )
@@ -146,16 +146,22 @@ internal object ConversationEngine {
JChatGPT.logger.info("使用缓存的对话上下文,包含 ${cache.history.size} 条互动消息") JChatGPT.logger.info("使用缓存的对话上下文,包含 ${cache.history.size} 条互动消息")
cache.history cache.history
} else mutableListOf() } else mutableListOf()
val profileInjectionState = cache?.profileInjectionState?.takeIf { reuseCache }
?: UserProfileInjectionState()
if (history.isEmpty() || cache == null) { if (history.isEmpty() || cache == null) {
val prompt = ConversationContext.getSystemPrompt(currentEvent) val prompt = ConversationContext.getSystemPrompt(currentEvent)
if (PluginConfig.logPrompt) JChatGPT.logger.info("Prompt: $prompt") if (PluginConfig.logPrompt) JChatGPT.logger.info("Prompt: $prompt")
history += ChatMessage(ChatRole.System, prompt) history += ChatMessage(ChatRole.System, prompt)
val historyText = ConversationContext.getHistory(currentEvent) val historyText = ConversationContext.getHistory(currentEvent, profileInjectionState)
JChatGPT.logger.info("注入聊天记录:\n$historyText") JChatGPT.logger.info("注入聊天记录:\n$historyText")
history += ChatMessage.User(historyText) history += ChatMessage.User(historyText)
} else { } else {
val newMessages = ConversationContext.getAfterHistory(cache.lastActivityAt, currentEvent) val newMessages = ConversationContext.getAfterHistory(
time = cache.lastActivityAt,
event = currentEvent,
profileInjectionState = profileInjectionState,
)
JChatGPT.logger.info("补充聊天记录:\n$newMessages") JChatGPT.logger.info("补充聊天记录:\n$newMessages")
history += ChatMessage.User( history += ChatMessage.User(
if (resumedWait == null) { if (resumedWait == null) {
@@ -281,6 +287,7 @@ internal object ConversationEngine {
startedAt = startedAt, startedAt = startedAt,
event = currentEvent, event = currentEvent,
pendingTrigger = pendingEvent != null, pendingTrigger = pendingEvent != null,
profileInjectionState = profileInjectionState,
) )
) )
done = false done = false
@@ -288,7 +295,13 @@ internal object ConversationEngine {
if (PluginConfig.enableContextCache) { if (PluginConfig.enableContextCache) {
ConversationContext.saveCache( ConversationContext.saveCache(
subjectId, subjectId,
ConversationCache(history, startedAt, replyIndex, imageIndex), ConversationCache(
history = history,
lastActivityAt = startedAt,
replyIndex = replyIndex,
imageIndex = imageIndex,
profileInjectionState = profileInjectionState,
),
) )
JChatGPT.logger.debug("已保存对话上下文到缓存") JChatGPT.logger.debug("已保存对话上下文到缓存")
} }
@@ -311,6 +324,7 @@ internal object ConversationEngine {
startedAt = startedAt, startedAt = startedAt,
event = currentEvent, event = currentEvent,
pendingTrigger = true, pendingTrigger = true,
profileInjectionState = profileInjectionState,
) )
) )
done = false done = false
@@ -492,6 +506,7 @@ internal object ConversationEngine {
startedAt: Int, startedAt: Int,
event: MessageEvent, event: MessageEvent,
pendingTrigger: Boolean, pendingTrigger: Boolean,
profileInjectionState: UserProfileInjectionState,
): String = buildString { ): String = buildString {
appendLine("## 系统提示") appendLine("## 系统提示")
append("本次运行最多还剩").append(remainingRounds).appendLine("轮。") append("本次运行最多还剩").append(remainingRounds).appendLine("轮。")
@@ -499,8 +514,16 @@ internal object ConversationEngine {
appendLine("如果没有什么要做的,可以提前结束。") appendLine("如果没有什么要做的,可以提前结束。")
if (pendingTrigger) appendLine("运行期间收到了新的显式触发,请优先处理水位后的新消息。") if (pendingTrigger) appendLine("运行期间收到了新的显式触发,请优先处理水位后的新消息。")
appendLine("当前时间:${dateTimeFormatter.format(OffsetDateTime.now())}") appendLine("当前时间:${dateTimeFormatter.format(OffsetDateTime.now())}")
val messages = ConversationContext.getAfterHistory(startedAt, event).ifEmpty { val messages = ConversationContext.getAfterHistory(
if (pendingTrigger && !JChatGPT.includeHistory) ConversationContext.getHistory(event) else "" time = startedAt,
event = event,
profileInjectionState = profileInjectionState,
).ifEmpty {
if (pendingTrigger && !JChatGPT.includeHistory) {
ConversationContext.getHistory(event, profileInjectionState)
} else {
""
}
} }
if (messages.isNotEmpty()) append("## 以下是上次运行至今的新消息\n\n$messages") if (messages.isNotEmpty()) append("## 以下是上次运行至今的新消息\n\n$messages")
} }
@@ -0,0 +1,54 @@
package top.jie65535.mirai.conversation
internal class UserProfileInjectionState {
private val renderedByUserId = mutableMapOf<Long, String?>()
private var guidanceInjected = false
fun hasSeen(userId: Long): Boolean = renderedByUserId.containsKey(userId)
fun renderChanges(
candidateUserIds: Collection<Long>,
renderedEntries: Map<Long, String>,
sectionTitle: String,
): String {
val changedEntries = mutableListOf<String>()
val clearedUserIds = mutableListOf<Long>()
candidateUserIds.distinct().forEach { userId ->
val current = renderedEntries[userId]
if (!renderedByUserId.containsKey(userId)) {
renderedByUserId[userId] = current
if (current != null) changedEntries += current
return@forEach
}
if (renderedByUserId[userId] == current) return@forEach
renderedByUserId[userId] = current
if (current == null) clearedUserIds += userId else changedEntries += current
}
if (changedEntries.isEmpty() && clearedUserIds.isEmpty()) return ""
val firstInjection = !guidanceInjected
guidanceInjected = true
return buildString {
append("## ").append(sectionTitle)
if (!firstInjection) append("(更新)")
appendLine()
if (firstInjection) {
appendLine(
"好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。" +
"仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。"
)
} else {
appendLine("以下仅列出新增或发生变化的条目;同一用户以本段为准,未列出的认识保持不变。")
}
changedEntries.forEach(::append)
clearedUserIds.forEach { userId ->
append("- 用户(").append(userId)
.appendLine("):当前已无可用的关系状态或画像认识,请忽略此前对应信息。")
}
appendLine()
}
}
}
@@ -11,6 +11,30 @@ object UserProfileContextRenderer {
summaryMaxChars: Int, summaryMaxChars: Int,
sectionTitle: String = "你对相关群友的认识", sectionTitle: String = "你对相关群友的认识",
): String { ): String {
val entries = renderEntries(
profiles = profiles,
favorabilityByUserId = favorabilityByUserId,
displayNames = displayNames,
activeUserIds = activeUserIds,
summaryMaxChars = summaryMaxChars,
)
if (entries.isEmpty()) return ""
return buildString {
append("## ").appendLine(sectionTitle)
appendLine(CONTEXT_GUIDANCE)
entries.values.forEach(::append)
appendLine()
}
}
internal fun renderEntries(
profiles: List<UserProfileSnapshot>,
favorabilityByUserId: Map<Long, FavorabilityInfo>,
displayNames: Map<Long, String>,
activeUserIds: Set<Long>,
summaryMaxChars: Int,
): Map<Long, String> {
val profilesByUserId = profiles val profilesByUserId = profiles
.filter { profile -> .filter { profile ->
profile.reliable && profile.reliable &&
@@ -20,18 +44,17 @@ object UserProfileContextRenderer {
val userIds = activeUserIds.filter { userId -> val userIds = activeUserIds.filter { userId ->
userId in profilesByUserId || favorabilityByUserId[userId]?.hasVisibleContext() == true userId in profilesByUserId || favorabilityByUserId[userId]?.hasVisibleContext() == true
} }
if (userIds.isEmpty()) return "" if (userIds.isEmpty()) return emptyMap()
val maxChars = summaryMaxChars.coerceAtLeast(50) val maxChars = summaryMaxChars.coerceAtLeast(50)
return buildString { return buildMap {
append("## ").appendLine(sectionTitle)
appendLine("好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。")
userIds.forEach { userId -> userIds.forEach { userId ->
val profile = profilesByUserId[userId] val profile = profilesByUserId[userId]
val favorability = favorabilityByUserId[userId] val favorability = favorabilityByUserId[userId]
val name = favorability?.name.orEmpty().ifBlank { val name = favorability?.name.orEmpty().ifBlank {
displayNames[userId].orEmpty().ifBlank { userId.toString() } displayNames[userId].orEmpty().ifBlank { userId.toString() }
} }
put(userId, buildString {
append("- ").append(name).append('(').append(userId).append(')') append("- ").append(name).append('(').append(userId).append(')')
favorability?.takeIf { it.hasVisibleContext() }?.let { info -> favorability?.takeIf { it.hasVisibleContext() }?.let { info ->
append(" | 好感度").append(if (info.value >= 0) "+" else "").append(info.value) append(" | 好感度").append(if (info.value >= 0) "+" else "").append(info.value)
@@ -44,7 +67,8 @@ object UserProfileContextRenderer {
append("").append(profile.items.size).append("条):") append("").append(profile.items.size).append("条):")
.append(summary.normalized().take(maxChars)) .append(summary.normalized().take(maxChars))
} ?: profile?.takeIf { it.items.isNotEmpty() }?.let { } ?: profile?.takeIf { it.items.isNotEmpty() }?.let {
append(" | 画像认识:已有").append(it.items.size).append("条记录,可用 queryUserProfile 查询详情") append(" | 画像认识:已有").append(it.items.size)
.append("条记录,可用 queryUserProfile 查询详情")
} }
profile?.items?.asSequence() profile?.items?.asSequence()
@@ -59,11 +83,14 @@ object UserProfileContextRenderer {
displayNames[relatedId].orEmpty().ifBlank { relatedId.toString() } displayNames[relatedId].orEmpty().ifBlank { relatedId.toString() }
} }
append(";与").append(relatedName).append("") append(";与").append(relatedName).append("")
.append(ProfilePersistentText.itemForDisplay(item.content, relationship = true).normalized()) .append(
ProfilePersistentText.itemForDisplay(item.content, relationship = true)
.normalized()
)
} }
appendLine() appendLine()
})
} }
appendLine()
} }
} }
@@ -71,4 +98,8 @@ object UserProfileContextRenderer {
value != 0 || name.isNotBlank() || tags.isNotEmpty() || impression.isNotBlank() value != 0 || name.isNotBlank() || tags.isNotEmpty() || impression.isNotBlank()
private fun String.normalized(): String = trim().replace(Regex("\\s+"), " ") private fun String.normalized(): String = trim().replace(Regex("\\s+"), " ")
private const val CONTEXT_GUIDANCE =
"好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。" +
"仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。"
} }
@@ -0,0 +1,95 @@
package top.jie65535.mirai.conversation
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFalse
class UserProfileInjectionStateTest {
@Test
fun injectsVisibleEntriesOnce() {
val state = UserProfileInjectionState()
val entries = mapOf(
100L to "- 小明(100) | 好感度+3\n",
200L to "- 小王(200) | 画像认识:熟悉 Kotlin\n",
)
val initial = state.renderChanges(entries.keys, entries, "你对相关群友的认识")
assertContains(initial, "## 你对相关群友的认识")
assertContains(initial, entries.getValue(100L).trim())
assertContains(initial, entries.getValue(200L).trim())
assertEquals("", state.renderChanges(entries.keys, entries, "你对相关群友的认识"))
}
@Test
fun emitsOnlyNewAndChangedUsers() {
val state = UserProfileInjectionState()
state.renderChanges(
candidateUserIds = listOf(100L, 200L),
renderedEntries = mapOf(
100L to "- 小明(100) | 好感度+3\n",
200L to "- 小王(200) | 好感度+1\n",
),
sectionTitle = "你对相关群友的认识",
)
val update = state.renderChanges(
candidateUserIds = listOf(100L, 200L, 300L),
renderedEntries = mapOf(
100L to "- 小明(100) | 好感度+3\n",
200L to "- 小王(200) | 好感度+5\n",
300L to "- 小李(300) | 主观印象:表达直接\n",
),
sectionTitle = "你对相关群友的认识",
)
assertContains(update, "## 你对相关群友的认识(更新)")
assertContains(update, "小王(200) | 好感度+5")
assertContains(update, "小李(300) | 主观印象:表达直接")
assertFalse(update.contains("小明(100)"))
}
@Test
fun explicitlyClearsPreviouslyVisibleContext() {
val state = UserProfileInjectionState()
state.renderChanges(
candidateUserIds = listOf(100L),
renderedEntries = mapOf(100L to "- 小明(100) | 好感度-2\n"),
sectionTitle = "你对相关群友的认识",
)
val update = state.renderChanges(
candidateUserIds = listOf(100L),
renderedEntries = emptyMap(),
sectionTitle = "你对相关群友的认识",
)
assertContains(update, "用户(100)")
assertContains(update, "请忽略此前对应信息")
assertEquals(
"",
state.renderChanges(listOf(100L), emptyMap(), "你对相关群友的认识"),
)
}
@Test
fun delaysGuidanceUntilAnInvisibleUserGetsContext() {
val state = UserProfileInjectionState()
assertEquals(
"",
state.renderChanges(listOf(100L), emptyMap(), "你对对方的认识"),
)
val firstVisible = state.renderChanges(
candidateUserIds = listOf(100L),
renderedEntries = mapOf(100L to "- 小明(100) | 画像认识:熟悉数据库\n"),
sectionTitle = "你对对方的认识",
)
assertContains(firstVisible, "## 你对对方的认识\n")
assertFalse(firstVisible.contains("(更新)"))
assertContains(firstVisible, "仅在当前话题相关时自然运用")
}
}
@@ -3,6 +3,7 @@ package top.jie65535.mirai.profile
import top.jie65535.mirai.data.FavorabilityInfo import top.jie65535.mirai.data.FavorabilityInfo
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContains import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
class UserProfileContextRendererTest { class UserProfileContextRendererTest {
@@ -80,6 +81,21 @@ class UserProfileContextRendererTest {
assertContains(rendered, "主观印象:长期活跃的老群友") assertContains(rendered, "主观印象:长期活跃的老群友")
} }
@Test
fun perUserEntriesIgnoreFavorabilityFieldsHiddenFromTheModel() {
fun render(reasons: List<String>) = UserProfileContextRenderer.renderEntries(
profiles = emptyList(),
favorabilityByUserId = mapOf(
100L to FavorabilityInfo(userId = 100, value = 5, reasons = reasons)
),
displayNames = mapOf(100L to "小明"),
activeUserIds = setOf(100L),
summaryMaxChars = 300,
)
assertEquals(render(listOf("旧原因")), render(listOf("更新但不展示的原因")))
}
private fun relationship(id: String, relatedUserId: Long, content: String) = UserProfileItem( private fun relationship(id: String, relatedUserId: Long, content: String) = UserProfileItem(
id = id, id = id,
category = ProfileCategory.RELATIONSHIP_NOTE, category = ProfileCategory.RELATIONSHIP_NOTE,