feat: AI antispam (#2406)

* Init

* Fix: add AiModule dependency to CommentModule and inject AiService in CommentService

* Finallize
This commit is contained in:
nyaruta
2025-03-25 13:37:08 +08:00
committed by GitHub
parent 2f4600bead
commit 4ec4814959
5 changed files with 76 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'
import { GatewayModule } from '~/processors/gateway/gateway.module' import { GatewayModule } from '~/processors/gateway/gateway.module'
import { AiModule } from '../ai/ai.module'
import { ReaderModule } from '../reader/reader.module' import { ReaderModule } from '../reader/reader.module'
import { ServerlessModule } from '../serverless/serverless.module' import { ServerlessModule } from '../serverless/serverless.module'
import { UserModule } from '../user/user.module' import { UserModule } from '../user/user.module'
@@ -17,6 +18,7 @@ import { CommentService } from './comment.service'
GatewayModule, GatewayModule,
forwardRef(() => ServerlessModule), forwardRef(() => ServerlessModule),
forwardRef(() => ReaderModule), forwardRef(() => ReaderModule),
forwardRef(() => AiModule),
], ],
}) })
export class CommentModule {} export class CommentModule {}

View File

@@ -33,6 +33,7 @@ import { InjectModel } from '~/transformers/model.transformer'
import { scheduleManager } from '~/utils/schedule.util' import { scheduleManager } from '~/utils/schedule.util'
import { getAvatar, hasChinese } from '~/utils/tool.util' import { getAvatar, hasChinese } from '~/utils/tool.util'
import { AiService } from '../ai/ai.service'
import { ConfigsService } from '../configs/configs.service' import { ConfigsService } from '../configs/configs.service'
import { ReaderModel } from '../reader/reader.model' import { ReaderModel } from '../reader/reader.model'
import { ReaderService } from '../reader/reader.service' import { ReaderService } from '../reader/reader.service'
@@ -62,6 +63,8 @@ export class CommentService implements OnModuleInit {
private readonly mailService: EmailService, private readonly mailService: EmailService,
private readonly configsService: ConfigsService, private readonly configsService: ConfigsService,
@Inject(forwardRef(() => AiService))
private readonly aiService: AiService,
@Inject(forwardRef(() => ServerlessService)) @Inject(forwardRef(() => ServerlessService))
private readonly serverlessService: ServerlessService, private readonly serverlessService: ServerlessService,
private readonly eventManager: EventManagerService, private readonly eventManager: EventManagerService,
@@ -146,6 +149,24 @@ export class CommentService implements OnModuleInit {
return true return true
} }
if (commentOptions.aiReview) {
const openai = await this.aiService.getOpenAiChain()
const { aiReviewType, aiReviewThreshold } = commentOptions
const runnable = openai
const prompt =
aiReviewType === 'score'
? 'Check the comment and return a risk score directly. Higher means more risky (1-10). Outputs should only be a number'
: 'Check if the comment is spam or not. Outputs should be true or false(Lowercase)'
const result = (await runnable.invoke([`${prompt}:${doc.text}`]))
.content
if (aiReviewType === 'score') {
return (result as any) > aiReviewThreshold
}
return result === 'true'
}
return false return false
})() })()
if (res) { if (res) {
@@ -525,9 +546,9 @@ export class CommentService implements OnModuleInit {
const location = const location =
`${result.countryName || ''}${ `${result.countryName || ''}${
result.regionName && result.regionName !== result.cityName result.regionName && result.regionName !== result.cityName
? `${result.regionName}` ? String(result.regionName)
: '' : ''
}${result.cityName ? `${result.cityName}` : ''}` || undefined }${result.cityName ? String(result.cityName) : ''}` || undefined
if (location) await this.commentModel.updateOne({ _id: id }, { location }) if (location) await this.commentModel.updateOne({ _id: id }, { location })
} }

View File

@@ -27,6 +27,9 @@ export const generateDefaultConfig: () => IConfig = () => ({
}, },
commentOptions: { commentOptions: {
antiSpam: false, antiSpam: false,
aiReview: false,
aiReviewType: 'binary',
aiReviewThreshold: 5,
disableComment: false, disableComment: false,
blockIps: [], blockIps: [],
disableNoChinese: false, disableNoChinese: false,

View File

@@ -10,6 +10,7 @@ import {
IsOptional, IsOptional,
IsString, IsString,
IsUrl, IsUrl,
Max,
Min, Min,
ValidateNested, ValidateNested,
} from 'class-validator' } from 'class-validator'
@@ -124,6 +125,41 @@ export class CommentOptionsDto {
@JSONSchemaToggleField('反垃圾评论') @JSONSchemaToggleField('反垃圾评论')
antiSpam: boolean antiSpam: boolean
@IsBoolean()
@IsOptional()
@JSONSchemaToggleField('开启 AI 审核')
aiReview: boolean
@IsString()
@IsOptional()
@JSONSchemaPlainField('AI 审核方式', {
description: '默认为是非,可以选择评分',
'ui:options': {
type: 'select',
values: [
{
label: '是非',
value: 'binary',
},
{
label: '评分',
value: 'score',
},
],
},
})
aiReviewType: string
@IsInt()
@Transform(({ value: val }) => Number.parseInt(val))
@Min(1)
@Max(10)
@IsOptional()
@JSONSchemaNumberField('AI 审核阈值', {
description: '分数大于多少时会被归类为垃圾评论, 范围为 1-10, 默认为 5',
})
aiReviewThreshold: number
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
@JSONSchemaToggleField('全站禁止评论', { description: '敏感时期专用' }) @JSONSchemaToggleField('全站禁止评论', { description: '敏感时期专用' })

View File

@@ -13,7 +13,6 @@ import { RedisKeys } from '~/constants/cache.constant'
import { EventBusEvents } from '~/constants/event-bus.constant' import { EventBusEvents } from '~/constants/event-bus.constant'
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant' import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
import { EventManagerService } from '~/processors/helper/helper.event.service' import { EventManagerService } from '~/processors/helper/helper.event.service'
import { CacheService } from '~/processors/redis/cache.service'
import { RedisService } from '~/processors/redis/redis.service' import { RedisService } from '~/processors/redis/redis.service'
import { SubPubBridgeService } from '~/processors/redis/subpub.service' import { SubPubBridgeService } from '~/processors/redis/subpub.service'
import { InjectModel } from '~/transformers/model.transformer' import { InjectModel } from '~/transformers/model.transformer'
@@ -192,6 +191,18 @@ export class ConfigsService {
if (!dto) { if (!dto) {
throw new BadRequestException('设置不存在') throw new BadRequestException('设置不存在')
} }
// 如果是评论设置,并且尝试启用 AI 审核,就检查 AI 配置
if (key === 'commentOptions' && (value as any).aiReview === true) {
const aiConfig = await this.get('ai')
const { openAiEndpoint, openAiKey } = aiConfig
if (!openAiEndpoint || !openAiKey) {
throw new BadRequestException(
'OpenAI API Key/Endpoint 未设置,无法启用 AI 评论审核',
)
}
}
const instanceValue = this.validWithDto(dto, value) const instanceValue = this.validWithDto(dto, value)
encryptObject(instanceValue) encryptObject(instanceValue)