Skip to content

Commit 6101bc9

Browse files
committed
feat(ai): add comment review endpoint and enhance AI configuration options
- Introduced a new endpoint `/comment-review/test` in `AiController` for testing AI comment review functionality. - Implemented logic to validate comment text and check AI review settings before processing. - Enhanced `configs.dsl.util.ts` and `configs.schema.ts` to support new action type for UI components, allowing for dynamic action button configurations. - Updated Zod schema utilities to accommodate new action options for better UI integration. These changes improve the AI comment review capabilities and enhance the configuration flexibility for UI components. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 3c09a82 commit 6101bc9

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-2
lines changed

apps/core/src/modules/ai/ai.controller.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { Auth } from '~/common/decorators/auth.decorator'
44
import { BizException } from '~/common/exceptions/biz.exception'
55
import { ErrorCodeEnum } from '~/constants/error-code.constant'
66
import { ConfigsService } from '../configs/configs.service'
7+
import { AI_PROMPTS } from './ai.prompts'
8+
import { AiService } from './ai.service'
79
import { AIProviderType } from './ai.types'
810
import type { IModelRuntime, ModelInfo } from './runtime'
911
import { createModelRuntime, createRuntimeForModelList } from './runtime'
@@ -31,9 +33,17 @@ interface TestConnectionDto {
3133
model?: string
3234
}
3335

36+
interface TestCommentReviewDto {
37+
text: string
38+
author?: string
39+
}
40+
3441
@ApiController('ai')
3542
export class AiController {
36-
constructor(private readonly configsService: ConfigsService) {}
43+
constructor(
44+
private readonly configsService: ConfigsService,
45+
private readonly aiService: AiService,
46+
) {}
3747

3848
@Get('/models')
3949
@Auth()
@@ -152,6 +162,79 @@ export class AiController {
152162
}
153163
}
154164

165+
@Post('/comment-review/test')
166+
@Auth()
167+
async testCommentReview(
168+
@Body() body: TestCommentReviewDto,
169+
): Promise<{ isSpam: boolean; score?: number; reason?: string }> {
170+
const { text } = body
171+
172+
if (!text?.trim()) {
173+
throw new BizException(
174+
ErrorCodeEnum.ContentNotFoundCantProcess,
175+
'Comment text is required',
176+
)
177+
}
178+
179+
const commentConfig = await this.configsService.get('commentOptions')
180+
if (!commentConfig.aiReview) {
181+
throw new BizException(
182+
ErrorCodeEnum.AINotEnabled,
183+
'AI review is not enabled',
184+
)
185+
}
186+
187+
try {
188+
const runtime = await this.aiService.getCommentReviewModel()
189+
190+
const reviewType = commentConfig.aiReviewType || 'binary'
191+
const threshold = commentConfig.aiReviewThreshold || 5
192+
193+
if (reviewType === 'score') {
194+
const promptConfig = AI_PROMPTS.comment.score(text)
195+
const result = await runtime.generateStructured({
196+
...promptConfig,
197+
})
198+
199+
const isSpam =
200+
result.output.score >= threshold ||
201+
result.output.hasSensitiveContent === true
202+
203+
return {
204+
isSpam,
205+
score: result.output.score,
206+
reason: result.output.hasSensitiveContent
207+
? '包含敏感内容'
208+
: isSpam
209+
? `评分 ${result.output.score} 超过阈值 ${threshold}`
210+
: undefined,
211+
}
212+
} else {
213+
const promptConfig = AI_PROMPTS.comment.spam(text)
214+
const result = await runtime.generateStructured({
215+
...promptConfig,
216+
})
217+
218+
const isSpam =
219+
result.output.isSpam || result.output.hasSensitiveContent === true
220+
221+
return {
222+
isSpam,
223+
reason: result.output.hasSensitiveContent
224+
? '包含敏感内容'
225+
: result.output.isSpam
226+
? '判定为垃圾评论'
227+
: undefined,
228+
}
229+
}
230+
} catch (error: any) {
231+
throw new BizException(
232+
ErrorCodeEnum.AIException,
233+
error?.message || 'AI comment review test failed',
234+
)
235+
}
236+
}
237+
155238
private async resolveModelListConfig(body: FetchModelsDto) {
156239
const needsLookup =
157240
!!body.providerId && (!body.apiKey || !body.type || !body.endpoint)

apps/core/src/modules/configs/configs.dsl.util.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type UIComponent =
1212
| 'switch'
1313
| 'select'
1414
| 'tags'
15+
| 'action'
1516

1617
export interface UIConfig {
1718
component: UIComponent
@@ -24,6 +25,18 @@ export interface UIConfig {
2425
* When the condition is not met, the field and all its nested children are hidden.
2526
*/
2627
showWhen?: Record<string, string | string[]>
28+
/**
29+
* Action button configuration (only used when component is 'action')
30+
*/
31+
actionId?: string
32+
actionLabel?: string
33+
actionVariant?:
34+
| 'default'
35+
| 'primary'
36+
| 'info'
37+
| 'success'
38+
| 'warning'
39+
| 'error'
2740
}
2841

2942
export interface FormField {
@@ -164,6 +177,7 @@ function inferUIComponent(
164177
if (uiOptions?.type === 'password') return 'password'
165178
if (uiOptions?.type === 'textarea') return 'textarea'
166179
if (uiOptions?.type === 'select') return 'select'
180+
if (uiOptions?.type === 'action') return 'action'
167181

168182
const unwrapped = unwrapZodType(schema)
169183

@@ -266,6 +280,18 @@ function extractField(key: string, schema: z.ZodTypeAny): FormField {
266280
}
267281
}
268282

283+
if (component === 'action') {
284+
if (uiOptions?.actionId) {
285+
field.ui.actionId = uiOptions.actionId
286+
}
287+
if (uiOptions?.actionLabel) {
288+
field.ui.actionLabel = uiOptions.actionLabel
289+
}
290+
if (uiOptions?.actionVariant) {
291+
field.ui.actionVariant = uiOptions.actionVariant
292+
}
293+
}
294+
269295
const unwrapped = unwrapZodType(schema)
270296
if (unwrapped instanceof z.ZodObject && component !== 'select') {
271297
const nestedFields = extractFields(unwrapped)

apps/core/src/modules/configs/configs.schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ export const CommentOptionsSchema = section('评论设置', {
104104
'AI 审核阈值',
105105
{ description: '分数大于多少时会被归类为垃圾评论,范围为 1-10, 默认为 5' },
106106
),
107+
testAiReview: field.action('测试 AI 审核', 'test-ai-review', {
108+
description: '输入测试内容,验证 AI 审核功能是否正常工作',
109+
actionLabel: '测试',
110+
showWhen: { aiReview: 'true' },
111+
}),
107112
disableComment: field.toggle(z.boolean().optional(), '全站禁止评论', {
108113
description: '敏感时期专用',
109114
}),

apps/core/src/modules/configs/configs.zod-schema.util.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { z } from 'zod'
44
* UI Options for JSON Schema form rendering
55
*/
66
export interface UIOptions {
7-
type?: 'password' | 'textarea' | 'select' | 'hidden'
7+
type?: 'password' | 'textarea' | 'select' | 'hidden' | 'action'
88
halfGrid?: boolean
99
hide?: boolean
1010
connect?: boolean
@@ -16,6 +16,18 @@ export interface UIOptions {
1616
* @example { provider: ['smtp', 'sendgrid'] } - show when provider equals any of these
1717
*/
1818
showWhen?: Record<string, string | string[]>
19+
/**
20+
* Action button configuration (only used when type is 'action')
21+
*/
22+
actionId?: string
23+
actionLabel?: string
24+
actionVariant?:
25+
| 'default'
26+
| 'primary'
27+
| 'info'
28+
| 'success'
29+
| 'warning'
30+
| 'error'
1931
}
2032

2133
/**
@@ -148,6 +160,34 @@ export const field = {
148160
title,
149161
'ui:options': { type: 'hidden' },
150162
}),
163+
164+
action: (
165+
title: string,
166+
actionId: string,
167+
options?: {
168+
description?: string
169+
actionLabel?: string
170+
actionVariant?:
171+
| 'default'
172+
| 'primary'
173+
| 'info'
174+
| 'success'
175+
| 'warning'
176+
| 'error'
177+
showWhen?: Record<string, string | string[]>
178+
},
179+
) =>
180+
withMeta(z.literal('__action__').optional(), {
181+
title,
182+
description: options?.description,
183+
'ui:options': {
184+
type: 'action',
185+
actionId,
186+
actionLabel: options?.actionLabel || title,
187+
actionVariant: options?.actionVariant,
188+
showWhen: options?.showWhen,
189+
},
190+
}),
151191
}
152192

153193
/**

0 commit comments

Comments
 (0)