feat: Enhance Kol Generation Service with web search and knowledge group support

- Added `EnableWebSearch` and `KnowledgeGroupIDs` fields to `KolGenerationSubmitRequest`.
- Updated `Submit` method in `KolGenerationService` to handle new request fields.
- Integrated web search and knowledge group validation based on card configuration.
- Introduced caching mechanisms in `KolPackageService`, `KolPromptService`, and `KolSubscriptionAdminService` to improve performance.
- Implemented knowledge context resolution in `KolGenerationWorker` to enrich prompts with relevant knowledge snippets.
- Added utility functions for handling card configuration in both backend and frontend.
- Created tests for knowledge extraction and rendering to ensure accuracy and reliability.
This commit is contained in:
2026-04-18 13:47:32 +08:00
parent 3ef0807456
commit b2605abd6a
27 changed files with 2051 additions and 171 deletions
+65 -12
View File
@@ -71,6 +71,7 @@ type ArticleListParams struct {
SourceType *string
GenerationMode *string
TemplateID *int64
KolPromptID *int64
PromptRuleID *int64
Keyword *string
CreatedFrom *time.Time
@@ -147,7 +148,7 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe
}
func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, params ArticleListParams) (*ArticleListResponse, error) {
sourceType := params.SourceType
sourceTypes := normalizeArticleSourceTypes(params.SourceType)
if params.Page < 1 {
params.Page = 1
@@ -182,11 +183,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
countArgs = append(countArgs, *params.PublishStatus)
argIdx++
}
if sourceType != nil {
countQuery += ` AND a.source_type = $` + strconv.Itoa(argIdx)
countArgs = append(countArgs, *sourceType)
argIdx++
}
countQuery, countArgs, argIdx = appendArticleSourceTypeFilter(countQuery, countArgs, argIdx, sourceTypes)
if params.GenerationMode != nil {
countQuery += ` AND a.source_type = 'custom_generation' AND COALESCE(NULLIF(gt.input_params_json ->> 'generation_mode', ''), 'instant') = $` + strconv.Itoa(argIdx)
countArgs = append(countArgs, *params.GenerationMode)
@@ -197,6 +194,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
countArgs = append(countArgs, *params.TemplateID)
argIdx++
}
if params.KolPromptID != nil {
countQuery += ` AND a.kol_prompt_id = $` + strconv.Itoa(argIdx)
countArgs = append(countArgs, *params.KolPromptID)
argIdx++
}
if params.PromptRuleID != nil {
countQuery += ` AND a.prompt_rule_id = $` + strconv.Itoa(argIdx)
countArgs = append(countArgs, *params.PromptRuleID)
@@ -223,7 +225,8 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
}
listQuery := `SELECT a.id, a.source_type, a.template_id, a.kol_prompt_id, a.prompt_rule_id, a.generate_status, a.publish_status, a.created_at,
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, COALESCE(av.word_count, 0), av.source_label, t.template_name, pr.name, a.wizard_state_json, gt.input_params_json
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, COALESCE(av.word_count, 0), av.source_label,
COALESCE(t.template_name, NULLIF(gt.input_params_json ->> 'prompt_name', ''), NULLIF(gt.input_params_json ->> 'package_name', '')), pr.name, a.wizard_state_json, gt.input_params_json
FROM articles a
LEFT JOIN article_versions av ON av.id = a.current_version_id
LEFT JOIN article_templates t ON t.id = a.template_id
@@ -249,11 +252,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
listArgs = append(listArgs, *params.PublishStatus)
listIdx++
}
if sourceType != nil {
listQuery += ` AND a.source_type = $` + strconv.Itoa(listIdx)
listArgs = append(listArgs, *sourceType)
listIdx++
}
listQuery, listArgs, listIdx = appendArticleSourceTypeFilter(listQuery, listArgs, listIdx, sourceTypes)
if params.GenerationMode != nil {
listQuery += ` AND a.source_type = 'custom_generation' AND COALESCE(NULLIF(gt.input_params_json ->> 'generation_mode', ''), 'instant') = $` + strconv.Itoa(listIdx)
listArgs = append(listArgs, *params.GenerationMode)
@@ -264,6 +263,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
listArgs = append(listArgs, *params.TemplateID)
listIdx++
}
if params.KolPromptID != nil {
listQuery += ` AND a.kol_prompt_id = $` + strconv.Itoa(listIdx)
listArgs = append(listArgs, *params.KolPromptID)
listIdx++
}
if params.PromptRuleID != nil {
listQuery += ` AND a.prompt_rule_id = $` + strconv.Itoa(listIdx)
listArgs = append(listArgs, *params.PromptRuleID)
@@ -1009,6 +1013,55 @@ func resolveWordCount(markdown *string, stored int) int {
return stored
}
func normalizeArticleSourceTypes(sourceType *string) []string {
if sourceType == nil {
return nil
}
parts := strings.Split(*sourceType, ",")
out := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
func appendArticleSourceTypeFilter(
query string,
args []interface{},
argIdx int,
sourceTypes []string,
) (string, []interface{}, int) {
if len(sourceTypes) == 0 {
return query, args, argIdx
}
placeholders := make([]string, 0, len(sourceTypes))
for _, sourceType := range sourceTypes {
placeholders = append(placeholders, `$`+strconv.Itoa(argIdx))
args = append(args, sourceType)
argIdx++
}
if len(placeholders) == 1 {
query += ` AND a.source_type = ` + placeholders[0]
} else {
query += ` AND a.source_type IN (` + strings.Join(placeholders, ", ") + `)`
}
return query, args, argIdx
}
func resolveArticleGenerationMode(sourceType string, inputParamsJSON []byte) *string {
if sourceType != "custom_generation" {
return nil