From 4ec4814959ee9034381910e06b6cd84fd2899768 Mon Sep 17 00:00:00 2001 From: nyaruta <168252381+nyaruta@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:37:08 +0800 Subject: [PATCH] feat: AI antispam (#2406) * Init * Fix: add AiModule dependency to CommentModule and inject AiService in CommentService * Finallize --- .../src/modules/comment/comment.module.ts | 2 ++ .../src/modules/comment/comment.service.ts | 25 +++++++++++-- .../src/modules/configs/configs.default.ts | 3 ++ apps/core/src/modules/configs/configs.dto.ts | 36 +++++++++++++++++++ .../src/modules/configs/configs.service.ts | 13 ++++++- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/apps/core/src/modules/comment/comment.module.ts b/apps/core/src/modules/comment/comment.module.ts index 96dabf09..a4a6c93b 100644 --- a/apps/core/src/modules/comment/comment.module.ts +++ b/apps/core/src/modules/comment/comment.module.ts @@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common' import { GatewayModule } from '~/processors/gateway/gateway.module' +import { AiModule } from '../ai/ai.module' import { ReaderModule } from '../reader/reader.module' import { ServerlessModule } from '../serverless/serverless.module' import { UserModule } from '../user/user.module' @@ -17,6 +18,7 @@ import { CommentService } from './comment.service' GatewayModule, forwardRef(() => ServerlessModule), forwardRef(() => ReaderModule), + forwardRef(() => AiModule), ], }) export class CommentModule {} diff --git a/apps/core/src/modules/comment/comment.service.ts b/apps/core/src/modules/comment/comment.service.ts index 9305e1c0..ca6bdfa0 100644 --- a/apps/core/src/modules/comment/comment.service.ts +++ b/apps/core/src/modules/comment/comment.service.ts @@ -33,6 +33,7 @@ import { InjectModel } from '~/transformers/model.transformer' import { scheduleManager } from '~/utils/schedule.util' import { getAvatar, hasChinese } from '~/utils/tool.util' +import { AiService } from '../ai/ai.service' import { ConfigsService } from '../configs/configs.service' import { ReaderModel } from '../reader/reader.model' import { ReaderService } from '../reader/reader.service' @@ -62,6 +63,8 @@ export class CommentService implements OnModuleInit { private readonly mailService: EmailService, private readonly configsService: ConfigsService, + @Inject(forwardRef(() => AiService)) + private readonly aiService: AiService, @Inject(forwardRef(() => ServerlessService)) private readonly serverlessService: ServerlessService, private readonly eventManager: EventManagerService, @@ -146,6 +149,24 @@ export class CommentService implements OnModuleInit { 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 })() if (res) { @@ -525,9 +546,9 @@ export class CommentService implements OnModuleInit { const location = `${result.countryName || ''}${ 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 }) } diff --git a/apps/core/src/modules/configs/configs.default.ts b/apps/core/src/modules/configs/configs.default.ts index 43dafdde..3c269527 100644 --- a/apps/core/src/modules/configs/configs.default.ts +++ b/apps/core/src/modules/configs/configs.default.ts @@ -27,6 +27,9 @@ export const generateDefaultConfig: () => IConfig = () => ({ }, commentOptions: { antiSpam: false, + aiReview: false, + aiReviewType: 'binary', + aiReviewThreshold: 5, disableComment: false, blockIps: [], disableNoChinese: false, diff --git a/apps/core/src/modules/configs/configs.dto.ts b/apps/core/src/modules/configs/configs.dto.ts index c73ec542..0e423a50 100644 --- a/apps/core/src/modules/configs/configs.dto.ts +++ b/apps/core/src/modules/configs/configs.dto.ts @@ -10,6 +10,7 @@ import { IsOptional, IsString, IsUrl, + Max, Min, ValidateNested, } from 'class-validator' @@ -124,6 +125,41 @@ export class CommentOptionsDto { @JSONSchemaToggleField('反垃圾评论') 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() @IsOptional() @JSONSchemaToggleField('全站禁止评论', { description: '敏感时期专用' }) diff --git a/apps/core/src/modules/configs/configs.service.ts b/apps/core/src/modules/configs/configs.service.ts index c068578c..988faa76 100644 --- a/apps/core/src/modules/configs/configs.service.ts +++ b/apps/core/src/modules/configs/configs.service.ts @@ -13,7 +13,6 @@ import { RedisKeys } from '~/constants/cache.constant' import { EventBusEvents } from '~/constants/event-bus.constant' import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant' import { EventManagerService } from '~/processors/helper/helper.event.service' -import { CacheService } from '~/processors/redis/cache.service' import { RedisService } from '~/processors/redis/redis.service' import { SubPubBridgeService } from '~/processors/redis/subpub.service' import { InjectModel } from '~/transformers/model.transformer' @@ -192,6 +191,18 @@ export class ConfigsService { if (!dto) { 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) encryptObject(instanceValue)