mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
profile: restrict detail queries to trigger sender
This commit is contained in:
@@ -41,7 +41,7 @@ AI 可以自动调用多种工具来完成复杂任务:
|
||||
- 记忆管理(添加和修改对话记忆)
|
||||
- 技能管理(沉淀、加载、迭代、删除可复用知识技能)
|
||||
- 聊天历史搜索(按关键词、发送者、时间范围检索群聊消息,需启用历史消息上下文)
|
||||
- 用户画像查询(按当前发送者、QQ号或群名片读取已归纳的长期画像)
|
||||
- 用户画像查询(仅允许读取当前对话触发者本人的长期画像详情)
|
||||
|
||||
## 权限列表
|
||||
|
||||
@@ -282,11 +282,12 @@ searchHistoryMaxRecords: 5000
|
||||
|
||||
下一次正常群聊会自动携带触发者和最近发言者的认识。现有好感度、Bot 代号、标签和主观印象会与证据驱动的
|
||||
长期画像按同一个人合并渲染,并明确给出长期画像条目数;私聊也会携带对方的可靠画像摘要和条目数。
|
||||
模型需要完整细节时可主动调用 `queryUserProfile`,按 QQ 号、群名片、昵称或好友备注读取完整画像。两套画像数据
|
||||
仍独立保存,自动画像不会修改好感度。
|
||||
模型需要完整细节时可主动调用 `queryUserProfile`,但只能读取当前对话触发者本人的完整画像;其他群友仅使用
|
||||
上下文中已注入的摘要。两套画像数据仍独立保存,自动画像不会修改好感度。
|
||||
|
||||
`queryUserProfile` 解析出唯一联系人后才按需调用一次 `queryProfile()`,并在内存中缓存 10 分钟;周期联系人
|
||||
同步不会触发该调用,因此群成员总量不会放大资料卡请求数。
|
||||
`queryUserProfile` 会在工具层校验查询对象;请求他人画像时固定返回“出于隐私考虑,禁止查询他人画像详情”,
|
||||
且不会读取画像数据库或资料卡。查询触发者本人时才按需调用一次 `queryProfile()`,并在内存中缓存 10 分钟;
|
||||
周期联系人同步不会触发该调用,因此群成员总量不会放大资料卡请求数。
|
||||
|
||||
好友昵称、群名片、群角色、签名、年龄等联系人公开资料只作为人物识别提示,不直接视为画像证据。画像模型
|
||||
仍必须依据对应用户本人在聊天历史中的发言,才能新增或确认长期画像条目。
|
||||
@@ -505,7 +506,7 @@ fallbackCooldownMinutes: 5
|
||||
12. **WeatherService** - 天气查询
|
||||
13. **SearchChatHistory** - 在当前群聊或私聊中使用 SQLite 全文索引搜索聊天历史,支持发送者、时间范围和游标翻页
|
||||
14. **GetChatHistoryContext** - 根据历史消息 ID 获取目标消息前后的对话上下文
|
||||
15. **QueryUserProfile** - 按当前发送者、QQ 号、群名片、昵称或好友备注读取长期画像,并针对该联系人按需读取公开资料卡
|
||||
15. **QueryUserProfile** - 仅读取当前对话触发者本人的长期画像,并按需读取其公开资料卡;禁止查询他人详情
|
||||
|
||||
`SearchChatHistory` 的常用参数只有消息内容、发送者、时间范围和翻页游标;普通文字和可解析的 `@` 目标会合并检索,结果默认按最近消息返回,
|
||||
单页大小和匹配策略由插件控制。`senderId` 与 `senderName` 可以同时提供,此时 QQ 号作为精确筛选条件;有 QQ 号时名称不参与筛选。
|
||||
@@ -541,7 +542,7 @@ JChatGPT 维护对每位用户的画像,由好感度、Bot 自定义代号、
|
||||
### 注入到上下文
|
||||
- 群聊:合并列出当前相关群友的关系状态,以及可靠长期画像的摘要和条目数
|
||||
- 私聊:合并注入对方的关系状态,以及可靠长期画像的摘要和条目数
|
||||
- 模型需要画像明细时主动调用 `queryUserProfile`,查询始终返回完整条目,不必把全部条目常驻在上下文中
|
||||
- 模型需要当前触发者本人的画像明细时主动调用 `queryUserProfile`;其他人的详情禁止查询,正常对话使用摘要
|
||||
- 只有好感度数值、其它关系字段和可靠长期画像均为空的用户不会被列出,避免提示词噪声
|
||||
|
||||
### 配置选项
|
||||
|
||||
@@ -68,7 +68,7 @@ object UserProfileContextRenderer {
|
||||
.append(summary.normalized().take(maxChars))
|
||||
} ?: profile?.takeIf { it.items.isNotEmpty() }?.let {
|
||||
append(" | 画像认识:已有").append(it.items.size)
|
||||
.append("条记录,可用 queryUserProfile 查询详情")
|
||||
.append("条记录(摘要暂缺)")
|
||||
}
|
||||
|
||||
profile?.items?.asSequence()
|
||||
|
||||
@@ -7,8 +7,6 @@ import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import net.mamoe.mirai.contact.User
|
||||
@@ -35,24 +33,8 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
class QueryUserProfileAgent : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "queryUserProfile",
|
||||
description = "查询你已归纳的用户画像,并按需读取该联系人的公开资料卡。用于了解某个群友/好友的长期信息、兴趣、表达方式、关系备注。默认查询当前对话发送者;在群聊中也可用QQ号、群名片或昵称查询别人。",
|
||||
parameters = Parameters.buildJsonObject {
|
||||
put("type", "object")
|
||||
putJsonObject("properties") {
|
||||
putJsonObject("userId") {
|
||||
put("type", "integer")
|
||||
put("description", "要查询的用户QQ号;留空时查询当前发送者")
|
||||
}
|
||||
putJsonObject("name") {
|
||||
put("type", "string")
|
||||
put("description", "群名片、昵称或好友备注关键词;仅在未填写userId时用于群聊中匹配用户")
|
||||
}
|
||||
putJsonObject("includeItems") {
|
||||
put("type", "boolean")
|
||||
put("description", "是否返回画像条目明细,默认true")
|
||||
}
|
||||
}
|
||||
}
|
||||
description = "查询本轮触发消息发送者的完整用户画像及公开资料卡。",
|
||||
parameters = Parameters.Empty,
|
||||
)
|
||||
) {
|
||||
override val isEnabled: Boolean
|
||||
@@ -62,8 +44,15 @@ class QueryUserProfileAgent : BaseAgent(
|
||||
get() = "查询用户画像中..."
|
||||
|
||||
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||
val userId = resolveUserId(args, event)
|
||||
?: return "未找到唯一匹配用户。请提供 userId,或使用更明确的群名片、昵称、好友备注。"
|
||||
val requestedUserId = if (hasLegacyProfileTarget(args)) {
|
||||
runCatching { resolveUserId(args, event) }.getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (!isOwnProfileQuery(args, event.sender.id, requestedUserId)) {
|
||||
return PROFILE_QUERY_PRIVACY_REFUSAL
|
||||
}
|
||||
val userId = event.sender.id
|
||||
val (profile, supportStats) = runCatching {
|
||||
val snapshot = UserProfileStore.load(userId)
|
||||
snapshot to if (snapshot == null) emptyMap() else UserProfileStore.loadSupportStats(userId)
|
||||
@@ -268,6 +257,23 @@ class QueryUserProfileAgent : BaseAgent(
|
||||
private fun String.normalized(): String = trim().replace(Regex("\\s+"), " ")
|
||||
}
|
||||
|
||||
internal const val PROFILE_QUERY_PRIVACY_REFUSAL = "出于隐私考虑,禁止查询他人画像详情"
|
||||
|
||||
internal fun isOwnProfileQuery(
|
||||
args: JsonObject?,
|
||||
senderId: Long,
|
||||
requestedUserId: Long?,
|
||||
): Boolean {
|
||||
if (args?.keys?.any { it !in LEGACY_PROFILE_QUERY_ARGUMENTS } == true) return false
|
||||
return !hasLegacyProfileTarget(args) || requestedUserId == senderId
|
||||
}
|
||||
|
||||
private val LEGACY_PROFILE_QUERY_ARGUMENTS = setOf("userId", "name", "includeItems")
|
||||
private val LEGACY_PROFILE_TARGET_ARGUMENTS = setOf("userId", "name")
|
||||
|
||||
private fun hasLegacyProfileTarget(args: JsonObject?): Boolean =
|
||||
args?.keys?.any { it in LEGACY_PROFILE_TARGET_ARGUMENTS } == true
|
||||
|
||||
internal fun selectProfileItems(
|
||||
items: List<UserProfileItem>,
|
||||
): List<UserProfileItem> = items.sortedWith(
|
||||
|
||||
@@ -1,13 +1,70 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import top.jie65535.mirai.profile.ProfileCategory
|
||||
import top.jie65535.mirai.profile.ProfileConfidence
|
||||
import top.jie65535.mirai.profile.ProfileItemSupportStats
|
||||
import top.jie65535.mirai.profile.UserProfileItem
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class QueryUserProfileAgentTest {
|
||||
@Test
|
||||
fun exposesNoTargetParametersToTheModel() {
|
||||
val function = QueryUserProfileAgent().tool.function
|
||||
val schema = requireNotNull(function.parameters).schema.jsonObject
|
||||
|
||||
assertEquals("查询本轮触发消息发送者的完整用户画像及公开资料卡。", function.description)
|
||||
assertTrue(schema.getValue("properties").jsonObject.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAllowsTheTriggeringUsersProfile() {
|
||||
val senderId = 100L
|
||||
|
||||
assertTrue(isOwnProfileQuery(null, senderId, requestedUserId = null))
|
||||
assertTrue(
|
||||
isOwnProfileQuery(
|
||||
buildJsonObject { put("includeItems", false) },
|
||||
senderId,
|
||||
requestedUserId = null,
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
isOwnProfileQuery(
|
||||
buildJsonObject { put("userId", senderId) },
|
||||
senderId,
|
||||
requestedUserId = senderId,
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
isOwnProfileQuery(
|
||||
buildJsonObject { put("userId", 200L) },
|
||||
senderId,
|
||||
requestedUserId = 200L,
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
isOwnProfileQuery(
|
||||
buildJsonObject { put("name", "小李") },
|
||||
senderId,
|
||||
requestedUserId = null,
|
||||
)
|
||||
)
|
||||
assertFalse(
|
||||
isOwnProfileQuery(
|
||||
buildJsonObject { put("qq", 200L) },
|
||||
senderId,
|
||||
requestedUserId = null,
|
||||
)
|
||||
)
|
||||
assertEquals("出于隐私考虑,禁止查询他人画像详情", PROFILE_QUERY_PRIVACY_REFUSAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsAllItemsInStableOrder() {
|
||||
val items = listOf(
|
||||
|
||||
Reference in New Issue
Block a user