diff --git a/src/main/kotlin/profile/UserProfileAnalysisService.kt b/src/main/kotlin/profile/UserProfileAnalysisService.kt index 76f2c2e..932d2c2 100644 --- a/src/main/kotlin/profile/UserProfileAnalysisService.kt +++ b/src/main/kotlin/profile/UserProfileAnalysisService.kt @@ -8,6 +8,8 @@ import top.jie65535.mirai.JChatGPT import top.jie65535.mirai.config.PluginConfig import top.jie65535.mirai.data.ChatHistoryStore import top.jie65535.mirai.llm.LargeLanguageModels +import top.jie65535.mirai.llm.ModelRequestRejectedException +import top.jie65535.mirai.llm.ModelSafetyRejectionException import top.jie65535.mirai.util.RetryBackoff import java.io.File import java.security.MessageDigest @@ -334,15 +336,23 @@ object UserProfileAnalysisService { break } - val report = analyzeConversationBatch( - batch = batch, - minAuthoredTextChars = PluginConfig.profileAutoMinAuthoredTextChars, - model = model, - retryMax = PluginConfig.profileRetryMax, - summaryMaxLength = PluginConfig.profileSummaryMaxLength, - onRetryFailure = { message, cause -> JChatGPT.logger.warning(message, cause) }, - onCommittedOperations = ProfileOperationLogger::log, - ) + val report = try { + analyzeConversationBatch( + batch = batch, + minAuthoredTextChars = PluginConfig.profileAutoMinAuthoredTextChars, + model = model, + retryMax = PluginConfig.profileRetryMax, + summaryMaxLength = PluginConfig.profileSummaryMaxLength, + onRetryFailure = { message, cause -> JChatGPT.logger.warning(message, cause) }, + onCommittedOperations = ProfileOperationLogger::log, + ) + } catch (cause: ModelSafetyRejectionException) { + JChatGPT.logger.warning( + "群 $groupId 会话画像 [${batch.startTime}, ${batch.endTime}) 被模型安全策略拒绝," + + "已跳过该批并继续后续历史 code=${cause.errorCode ?: "unknown"}" + ) + null + } cursor = cursor.copy( cursorTime = batch.endTime, updatedAt = System.currentTimeMillis(), @@ -545,6 +555,14 @@ object UserProfileAnalysisService { return result to reductions } catch (cause: Exception) { if (cause is CancellationException) throw cause + if (cause is ModelRequestRejectedException) { + onRetryFailure( + "群 ${batch.groupId} 会话画像 [${batch.startTime}, ${batch.endTime}) " + + "被模型拒绝,已停止重试", + cause, + ) + throw cause + } lastFailure = cause handleRetryFailure( attempt = attempt, @@ -591,6 +609,14 @@ object UserProfileAnalysisService { return result to reduction } catch (cause: Exception) { if (cause is CancellationException) throw cause + if (cause is ModelRequestRejectedException) { + JChatGPT.logger.warning( + "用户 ${batch.userId} 画像批次 [${batch.startTime}, ${batch.endTime}) " + + "被模型拒绝,已停止重试", + cause, + ) + throw cause + } lastFailure = cause handleRetryFailure( attempt = attempt, @@ -631,6 +657,13 @@ object UserProfileAnalysisService { return result to plan } catch (cause: Exception) { if (cause is CancellationException) throw cause + if (cause is ModelRequestRejectedException) { + JChatGPT.logger.warning( + "用户 ${profile.userId} 画像压缩被模型拒绝,已停止重试", + cause, + ) + throw cause + } lastFailure = cause handleRetryFailure( attempt = attempt, diff --git a/src/test/kotlin/profile/UserProfileAnalysisServiceTest.kt b/src/test/kotlin/profile/UserProfileAnalysisServiceTest.kt index 2f661bb..e3d0580 100644 --- a/src/test/kotlin/profile/UserProfileAnalysisServiceTest.kt +++ b/src/test/kotlin/profile/UserProfileAnalysisServiceTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import net.mamoe.mirai.message.data.MessageSourceKind import top.jie65535.mirai.data.ChatMessageRecord +import top.jie65535.mirai.llm.ModelSafetyRejectionException import java.io.IOException import java.nio.file.Files import java.util.concurrent.atomic.AtomicInteger @@ -122,6 +123,25 @@ class UserProfileAnalysisServiceTest { assertFalse(UserProfileStore.isConversationProcessed(INPUT_HASH)) } + @Test + fun safetyRejectionDoesNotRetryOrCommit() = withProfileStore { + val model = FakeConversationProfileModel { + throw ModelSafetyRejectionException( + errorType = "invalid_request_error", + errorCode = "cyber_policy", + message = "blocked by policy", + ) + } + + assertFailsWith { + analyze(batch(), model, retryMax = 3) + } + + assertEquals(1, model.calls) + assertNull(UserProfileStore.load(USER_A)) + assertFalse(UserProfileStore.isConversationProcessed(INPUT_HASH)) + } + @Test fun skipsConversationAlreadyProcessedByAnEarlierCall() = withProfileStore { val model = FakeConversationProfileModel { successfulResult() }