package top.jie65535.mirai import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService.Companion.hasPermission import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.isOperator import net.mamoe.mirai.event.EventPriority import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.events.FriendMessageEvent import net.mamoe.mirai.event.events.GroupMessageEvent import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessagePostSendEvent import net.mamoe.mirai.event.events.MessageRecallEvent import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.QuoteReply import net.mamoe.mirai.message.data.content import net.mamoe.mirai.utils.info import top.jie65535.mirai.command.PluginCommands import top.jie65535.mirai.config.PluginConfig import top.jie65535.mirai.conversation.ConversationContext import top.jie65535.mirai.conversation.ConversationEngine import top.jie65535.mirai.data.ChatHistoryStore import top.jie65535.mirai.data.ChatMessageRecord import top.jie65535.mirai.data.ContactSnapshotRefresher import top.jie65535.mirai.data.ContactSnapshotStore import top.jie65535.mirai.data.PluginData import top.jie65535.mirai.data.SkillStore import top.jie65535.mirai.data.TokenUsageStore import top.jie65535.mirai.llm.LargeLanguageModels import top.jie65535.mirai.profile.ProfileAutoMaintenance import top.jie65535.mirai.profile.ProfileDailyMaintenance import top.jie65535.mirai.profile.UserProfileStore import kotlin.random.Random object JChatGPT : KotlinPlugin( JvmPluginDescription( id = "top.jie65535.mirai.JChatGPT", name = "J ChatGPT", version = "1.15.0", ) { author("jie65535") } ) { internal var includeHistory: Boolean = false private var historyIndexJob: Job? = null val chatPermission = PermissionId("JChatGPT", "Chat") private var keyword: Regex? = null override fun onEnable() { PermissionService.INSTANCE.register(chatPermission, "JChatGPT Chat Permission") PluginConfig.reload() PluginData.reload() TokenUsageStore.init(dataFolder) SkillStore.init(dataFolder) includeHistory = try { ChatHistoryStore.init(dataFolder) { message, cause -> if (cause == null) logger.warning(message) else logger.warning(message, cause) } true } catch (cause: Throwable) { logger.error("初始化 SQLite 聊天记录失败,历史上下文与搜索将暂时禁用", cause) false } if (includeHistory && ChatHistoryStore.isSearchIndexAvailable && !ChatHistoryStore.isSearchIndexReady) { historyIndexJob = launch(Dispatchers.IO) { try { val processed = ChatHistoryStore.backfillSearchIndex( shouldContinue = { isActive }, onProgress = { count -> if (count % 10_000 == 0) { logger.info("聊天记录全文索引回填进度: 已处理 $count 条") } }, ) if (isActive && processed > 0) { logger.info("聊天记录全文索引回填完成: 共处理 $processed 条") } } catch (cause: CancellationException) { throw cause } catch (cause: Throwable) { logger.warning("聊天记录全文索引回填失败,后续启动将继续尝试", cause) } } } if (includeHistory) { runCatching { ContactSnapshotStore.init(dataFolder) } .onFailure { logger.error("初始化联系人快照数据库失败,联系人画像辅助将暂时禁用", it) } } runCatching { UserProfileStore.init(dataFolder) } .onFailure { logger.error("初始化用户画像数据库失败,画像分析将暂时禁用", it) } LargeLanguageModels.reload() ProfileDailyMaintenance.reload() PluginCommands.register() keyword = PluginConfig.callKeyword.takeIf(String::isNotEmpty)?.let(::Regex) val eventChannel = GlobalEventChannel.parentScope(this) eventChannel.subscribeAlways(priority = EventPriority.HIGHEST) { event -> ContactSnapshotRefresher.schedule(event.bot, "message") runCatching { ChatHistoryStore.record(event) } .onFailure { logger.warning("保存接收消息到 SQLite 失败", it) } } eventChannel.subscribeAlways>(priority = EventPriority.HIGHEST) { event -> runCatching { ChatHistoryStore.record(event) } .onFailure { logger.warning("保存发送消息到 SQLite 失败", it) } } eventChannel.subscribeAlways(priority = EventPriority.HIGHEST) { event -> runCatching { ChatHistoryStore.markRecalled(event) } .onFailure { logger.warning("更新 SQLite 消息撤回状态失败", it) } } eventChannel.subscribeAlways { event -> onMessage(event) } ContactSnapshotRefresher.scheduleAll("startup") logger.info { "Plugin loaded" } } override fun onDisable() { historyIndexJob?.cancel() historyIndexJob = null ConversationEngine.clear() ConversationContext.clearAll() ProfileAutoMaintenance.clear() ProfileDailyMaintenance.clear() ContactSnapshotRefresher.clear() UserProfileStore.close() ContactSnapshotStore.close() ChatHistoryStore.close() } fun clearContextCache() { ConversationContext.clearCache() } internal fun lookupReplyTarget(subjectId: Long, index: Int): ChatMessageRecord? = ConversationContext.lookupReplyTarget(subjectId, index) internal fun registerImage(subjectId: Long, imageId: String, imageUrl: String): Int? = ConversationContext.registerImage(subjectId, imageId, imageUrl) internal fun lookupImageUrl(subjectId: Long, index: Int): String? = ConversationContext.lookupImageUrl(subjectId, index) fun toMessage(contact: Contact, content: String): Message = ConversationContext.toMessage(contact, content) private suspend fun onMessage(event: MessageEvent) { if (LargeLanguageModels.chat == null) return if (ConversationEngine.isExpectedUser(event)) { if (shouldIgnoreBecauseMuted(event)) return if (ConversationEngine.resumeObserved(event)) return } if (!event.toCommandSender().hasPermission(chatPermission)) { if (event is GroupMessageEvent) { if (!PluginConfig.groupOpHasChatPermission || !event.sender.isOperator()) { if (event.sender.active.temperature < PluginConfig.temperaturePermission) return } } if (event is FriendMessageEvent && !PluginConfig.friendHasChatPermission) return } val triggered = event.message.contains(At(event.bot)) || keyword?.containsMatchIn(event.message.content) == true || event.message[QuoteReply]?.source?.fromId == event.bot.id if (!triggered) return if (shouldIgnoreBecauseMuted(event)) return if (PluginConfig.enableFavorabilitySystem && shouldIgnoreForFavorability(event)) return ConversationEngine.start(event) } private fun shouldIgnoreBecauseMuted(event: MessageEvent): Boolean { if (event !is GroupMessageEvent) return false val remainingSeconds = event.group.botMuteRemaining if (remainingSeconds <= 0) return false logger.info( "bot 在群 ${event.group.name}(${event.group.id}) 被禁言," + "剩余 $remainingSeconds 秒,忽略消息" ) return true } private suspend fun shouldIgnoreForFavorability(event: MessageEvent): Boolean { val info = PluginData.userFavorability[event.sender.id] ?: return false if (info.value >= 0) return false val probability = kotlin.math.abs(info.value).toDouble() / 100.0 if (Random.nextDouble() >= probability) return false logger.info( "根据好感度系统,用户 ${event.senderName}(${event.sender.id}) " + "(好感度: ${info.value}) 的消息被忽略,忽略概率: ${probability * 100}%" ) event.subject.sendMessage("[实验功能] 因好感度低,此消息已被忽略(${probability * 100}%)") return true } }