chat: require owner presence for group triggers
Build and Test / build (push) Canceled after 0s

This commit is contained in:
2026-09-05 22:03:49 +08:00
parent 5e9c9d9990
commit dd3aaa417f
5 changed files with 105 additions and 0 deletions
+15
View File
@@ -68,6 +68,21 @@ chatFallbackModelAliases: []
修改配置后执行 `/jgpt reload`。其他模型、工具和实验功能按需配置,缺少依赖或凭据的可选工具不会启用。
### 群聊触发限制
默认开启 `requireOwnerInGroup`:只有配置的主人也在群内,JChatGPT 才允许触发群聊对话。请在 `Config.yml` 中填写主人的 QQ
```yaml
ownerId: 123456789 # 替换为你的 QQ 号
requireOwnerInGroup: true
```
此限制优先于聊天权限,覆盖 `@Bot`、引用回复、关键词以及连续会话触发。主人未配置、QQ 号无效、不在群内或成员查询失败时,均静默忽略,不发送拒绝提示。检查使用 Overflow 提供的当前群成员信息,不使用历史联系人快照;成员变动的生效时间取决于 Overflow 的同步。
私聊仍按原有逻辑处理,聊天记录继续保存,画像相关配置、管理命令和其他插件不受此开关影响。开关限制新的消息触发,不会强制取消已经开始的模型请求或工具调用。设为 `false` 可恢复原有群聊触发行为,修改后执行 `/jgpt reload`
升级后此开关也默认开启;未配置 `ownerId` 时,所有群聊对话触发都会被忽略。
## 使用
- 群聊中 `@Bot`,或回复 Bot 的消息
+13
View File
@@ -32,6 +32,7 @@ import top.jie65535.mirai.config.ModelConfig
import top.jie65535.mirai.config.ModelConfigMigration
import top.jie65535.mirai.conversation.ConversationContext
import top.jie65535.mirai.conversation.ConversationEngine
import top.jie65535.mirai.conversation.allowsGroupChatTrigger
import top.jie65535.mirai.data.ChatHistoryStore
import top.jie65535.mirai.data.ChatMessageRecord
import top.jie65535.mirai.data.ContactSnapshotRefresher
@@ -175,6 +176,18 @@ object JChatGPT : KotlinPlugin(
private suspend fun onMessage(event: MessageEvent) {
if (LargeLanguageModels.chat == null) return
if (event is GroupMessageEvent && !allowsGroupChatTrigger(
requireOwnerInGroup = PluginConfig.requireOwnerInGroup,
ownerId = PluginConfig.ownerId,
isMember = { ownerId -> event.group[ownerId] != null },
onFailure = { cause ->
logger.warning(
"检查主人是否在群内失败,忽略群聊触发:bot=${event.bot.id}, group=${event.group.id}",
cause,
)
},
)) return
if (ConversationEngine.isExpectedUser(event)) {
if (shouldIgnoreBecauseMuted(event)) return
if (ConversationEngine.resumeObserved(event)) return
+3
View File
@@ -21,6 +21,9 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("主人QQ,AI可以通过工具向主人发起请求,会等待一段时间")
val ownerId: Long by value()
@ValueDescription("是否仅在主人(ownerId)也在的群内允许聊天触发,默认开启;主人未配置、不在群内或无法确认时静默忽略。仅限制群聊,不影响私聊和聊天记录保存")
val requireOwnerInGroup: Boolean by value(true)
@ValueDescription("OpenAI API base url")
val openAiApi: String by value("https://dashscope.aliyuncs.com/compatible-mode/v1/")
@@ -0,0 +1,21 @@
package top.jie65535.mirai.conversation
import kotlinx.coroutines.CancellationException
internal fun allowsGroupChatTrigger(
requireOwnerInGroup: Boolean,
ownerId: Long,
isMember: (Long) -> Boolean,
onFailure: (Exception) -> Unit,
): Boolean {
if (!requireOwnerInGroup) return true
if (ownerId <= 0) return false
return try {
isMember(ownerId)
} catch (cause: CancellationException) {
throw cause
} catch (cause: Exception) {
onFailure(cause)
false
}
}
@@ -0,0 +1,53 @@
package top.jie65535.mirai.conversation
import kotlinx.coroutines.CancellationException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertFailsWith
import kotlin.test.assertSame
import kotlin.test.assertTrue
class GroupChatTriggerPolicyTest {
@Test
fun disabledRestrictionDoesNotLookUpOwner() {
assertTrue(allowsGroupChatTrigger(false, 0, { error("Unexpected lookup") }, { throw it }))
}
@Test
fun missingOrInvalidOwnerBlocksWithoutLookup() {
for (ownerId in listOf(0L, -1L)) {
assertFalse(allowsGroupChatTrigger(true, ownerId, { error("Unexpected lookup") }, { throw it }))
}
}
@Test
fun configuredOwnerMustBePresentAndMembershipIsCheckedAgainOnEachTrigger() {
val members = mutableSetOf(123L, 456L)
val isMember: (Long) -> Boolean = { ownerId ->
assertEquals(123L, ownerId)
ownerId in members
}
assertTrue(allowsGroupChatTrigger(true, 123L, isMember, { throw it }))
members.remove(123L)
assertFalse(allowsGroupChatTrigger(true, 123L, isMember, { throw it }))
members.add(123L)
assertTrue(allowsGroupChatTrigger(true, 123L, isMember, { throw it }))
}
@Test
fun unavailableMemberLookupBlocksAndReportsFailure() {
val failure = UnsupportedOperationException("Member lookup unavailable")
var reported: Exception? = null
assertFalse(allowsGroupChatTrigger(true, 123L, { throw failure }, { reported = it }))
assertSame(failure, reported)
}
@Test
fun cancellationPropagatesWithoutReportingLookupFailure() {
val cancellation = CancellationException("Cancelled")
assertSame(cancellation, assertFailsWith<CancellationException> {
allowsGroupChatTrigger(true, 123L, { throw cancellation }, { error("Unexpected failure report") })
})
}
}