Skip to content

Commit b2444ad

Browse files
committed
feat(ai): add min text length threshold for auto summary/insights
Add summaryMinTextLength (default 100) and insightsMinTextLength (default 300) configs. When article body is shorter than the threshold, OnCreate/OnUpdate auto hooks skip task dispatch. Manual generation is unaffected. Set to 0 to disable.
1 parent 7c44207 commit b2444ad

6 files changed

Lines changed: 152 additions & 0 deletions

File tree

apps/core/src/modules/ai/ai-insights/ai-insights.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,22 @@ export class AiInsightsService implements OnModuleInit {
575575
) {
576576
return
577577
}
578+
579+
const minLen = aiConfig.insightsMinTextLength ?? 0
580+
if (minLen > 0) {
581+
try {
582+
const { article } = await this.resolveArticleForInsights(event.id)
583+
if ((article.text?.length ?? 0) < minLen) {
584+
this.logger.debug(
585+
`AI auto insights skipped (text below threshold ${minLen}): article=${event.id}`,
586+
)
587+
return
588+
}
589+
} catch {
590+
return
591+
}
592+
}
593+
578594
this.logger.log(`AI auto insights task created: article=${event.id}`)
579595
await this.aiTaskService.createInsightsTask({ refId: event.id })
580596
}
@@ -596,6 +612,13 @@ export class AiInsightsService implements OnModuleInit {
596612
} catch {
597613
return
598614
}
615+
const minLen = aiConfig.insightsMinTextLength ?? 0
616+
if (minLen > 0 && (article.text?.length ?? 0) < minLen) {
617+
this.logger.debug(
618+
`AI auto insights skipped (text below threshold ${minLen}): article=${event.id}`,
619+
)
620+
return
621+
}
599622
const newHash = this.computeContentHash(article.text)
600623
const existing = await this.aiInsightsModel.find({
601624
refId: event.id,

apps/core/src/modules/ai/ai-summary/ai-summary.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,21 @@ export class AiSummaryService implements OnModuleInit {
729729
return
730730
}
731731

732+
const minLen = aiConfig.summaryMinTextLength ?? 0
733+
if (minLen > 0) {
734+
try {
735+
const { document } = await this.resolveArticleForSummary(event.id)
736+
if ((document.text?.length ?? 0) < minLen) {
737+
this.logger.debug(
738+
`AI auto summary skipped (text below threshold ${minLen}): article=${event.id}`,
739+
)
740+
return
741+
}
742+
} catch {
743+
return
744+
}
745+
}
746+
732747
this.logger.log(`AI auto summary task created: article=${event.id}`)
733748
await this.aiTaskService.createSummaryTask({
734749
refId: event.id,
@@ -756,6 +771,14 @@ export class AiSummaryService implements OnModuleInit {
756771
return
757772
}
758773

774+
const minLen = aiConfig.summaryMinTextLength ?? 0
775+
if (minLen > 0 && (document.text?.length ?? 0) < minLen) {
776+
this.logger.debug(
777+
`AI auto summary skipped (text below threshold ${minLen}): article=${id}`,
778+
)
779+
return
780+
}
781+
759782
const existingSummaries = await this.aiSummaryModel.find({ refId: id })
760783
if (!existingSummaries.length) {
761784
return

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,15 @@ export const generateDefaultConfig: () => IConfig = () => ({
109109
enableAutoGenerateSummaryOnCreate: false,
110110
enableAutoGenerateSummaryOnUpdate: false,
111111
summaryTargetLanguages: [],
112+
summaryMinTextLength: 100,
112113
insightsModel: undefined,
113114
insightsTranslationModel: undefined,
114115
enableInsights: false,
115116
enableAutoGenerateInsightsOnCreate: false,
116117
enableAutoGenerateInsightsOnUpdate: false,
117118
enableAutoTranslateInsights: false,
118119
insightsTargetLanguages: [],
120+
insightsMinTextLength: 300,
119121
},
120122
oauth: {
121123
providers: [],

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,18 @@ export const AISchema = section('AI 设定', {
385385
'自动生成摘要的目标语言列表,使用 [ISO 639-1 语言代码](https://www.w3schools.com/tags/ref_language_codes.asp),如 ["zh", "en", "ja"]',
386386
},
387387
),
388+
summaryMinTextLength: field.number(
389+
z.preprocess(
390+
(val) =>
391+
val === '' || val === null || val === undefined ? val : Number(val),
392+
z.number().int().min(0).optional(),
393+
),
394+
'摘要自动生成最小文本长度',
395+
{
396+
description:
397+
'正文字符数低于此值时跳过自动钩子(OnCreate/OnUpdate),仅影响自动触发;0 表示不限。默认 100',
398+
},
399+
),
388400
translationModel: field.plain(
389401
AIModelAssignmentSchema.optional(),
390402
'翻译功能模型',
@@ -445,6 +457,18 @@ export const AISchema = section('AI 设定', {
445457
description: 'ISO 639-1 列表;源语言自动排除',
446458
},
447459
),
460+
insightsMinTextLength: field.number(
461+
z.preprocess(
462+
(val) =>
463+
val === '' || val === null || val === undefined ? val : Number(val),
464+
z.number().int().min(0).optional(),
465+
),
466+
'Insights 自动生成最小文本长度',
467+
{
468+
description:
469+
'正文字符数低于此值时跳过自动钩子(OnCreate/OnUpdate),仅影响自动触发;0 表示不限。默认 300',
470+
},
471+
),
448472
})
449473
export class AIDto extends createZodDto(AISchema) {}
450474
export type AIConfig = z.infer<typeof AISchema>

apps/core/test/src/modules/ai/ai-insights.service.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,36 @@ describe('AiInsightsService', () => {
129129
expect(taskSvc.createInsightsTask).toHaveBeenCalledWith({ refId: 'a' })
130130
})
131131

132+
it('handleCreateArticle skips when text below insightsMinTextLength', async () => {
133+
mockConfigService.get.mockResolvedValue({
134+
enableInsights: true,
135+
enableAutoGenerateInsightsOnCreate: true,
136+
insightsMinTextLength: 100,
137+
})
138+
mockDatabaseService.findGlobalById.mockResolvedValue({
139+
type: CollectionRefTypes.Post,
140+
document: { title: 'T', text: 'short', lang: 'zh' },
141+
})
142+
const taskSvc: any = (service as any).aiTaskService
143+
await service.handleCreateArticle({ id: 'a' })
144+
expect(taskSvc.createInsightsTask).not.toHaveBeenCalled()
145+
})
146+
147+
it('handleCreateArticle enqueues when text meets insightsMinTextLength', async () => {
148+
mockConfigService.get.mockResolvedValue({
149+
enableInsights: true,
150+
enableAutoGenerateInsightsOnCreate: true,
151+
insightsMinTextLength: 5,
152+
})
153+
mockDatabaseService.findGlobalById.mockResolvedValue({
154+
type: CollectionRefTypes.Post,
155+
document: { title: 'T', text: 'long enough body', lang: 'zh' },
156+
})
157+
const taskSvc: any = (service as any).aiTaskService
158+
await service.handleCreateArticle({ id: 'a' })
159+
expect(taskSvc.createInsightsTask).toHaveBeenCalledWith({ refId: 'a' })
160+
})
161+
132162
it('handleDeleteArticle cascades', async () => {
133163
await service.handleDeleteArticle({ id: 'a' })
134164
expect(mockModel.deleteMany).toHaveBeenCalledWith({ refId: 'a' })

apps/core/test/src/modules/ai/ai-summary.service.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,56 @@ describe('AiSummaryService', () => {
227227
})
228228
})
229229

230+
describe('handleCreateArticle threshold', () => {
231+
it('skips when text below summaryMinTextLength', async () => {
232+
mockConfigService.get.mockResolvedValue({
233+
enableSummary: true,
234+
enableAutoGenerateSummaryOnCreate: true,
235+
summaryTargetLanguages: ['zh'],
236+
summaryMinTextLength: 100,
237+
})
238+
mockDatabaseService.findGlobalById.mockResolvedValue({
239+
document: { title: 'T', text: 'short' },
240+
type: CollectionRefTypes.Post,
241+
})
242+
const taskSvc: any = (service as any).aiTaskService
243+
await service.handleCreateArticle({ id: 'a' })
244+
expect(taskSvc.createSummaryTask).not.toHaveBeenCalled()
245+
})
246+
247+
it('enqueues when text meets summaryMinTextLength', async () => {
248+
mockConfigService.get.mockResolvedValue({
249+
enableSummary: true,
250+
enableAutoGenerateSummaryOnCreate: true,
251+
summaryTargetLanguages: ['zh'],
252+
summaryMinTextLength: 5,
253+
})
254+
mockDatabaseService.findGlobalById.mockResolvedValue({
255+
document: { title: 'T', text: 'long enough body' },
256+
type: CollectionRefTypes.Post,
257+
})
258+
const taskSvc: any = (service as any).aiTaskService
259+
await service.handleCreateArticle({ id: 'a' })
260+
expect(taskSvc.createSummaryTask).toHaveBeenCalledWith({
261+
refId: 'a',
262+
targetLanguages: ['zh'],
263+
})
264+
})
265+
266+
it('enqueues without fetching article when threshold is 0', async () => {
267+
mockConfigService.get.mockResolvedValue({
268+
enableSummary: true,
269+
enableAutoGenerateSummaryOnCreate: true,
270+
summaryTargetLanguages: ['zh'],
271+
summaryMinTextLength: 0,
272+
})
273+
const taskSvc: any = (service as any).aiTaskService
274+
await service.handleCreateArticle({ id: 'a' })
275+
expect(mockDatabaseService.findGlobalById).not.toHaveBeenCalled()
276+
expect(taskSvc.createSummaryTask).toHaveBeenCalled()
277+
})
278+
})
279+
230280
describe('getSummaryByArticleId', () => {
231281
it('should return valid summary from database', async () => {
232282
const text = mockArticle.text

0 commit comments

Comments
 (0)