feat: AI antispam (#2406)
* Init * Fix: add AiModule dependency to CommentModule and inject AiService in CommentService * Finallize
This commit is contained in:
@@ -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 {}
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: '敏感时期专用' })
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user