feat: add knowledge management functionality with CRUD operations and database schema

- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations.
- Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta.
- Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
This commit is contained in:
2026-04-05 17:14:13 +08:00
parent 134dd063c3
commit 446f865cdf
62 changed files with 9503 additions and 2664 deletions
+50 -95
View File
@@ -7,14 +7,20 @@ import (
"strings"
"github.com/geo-platform/tenant-api/internal/shared/contentstats"
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
)
var promptVariablePattern = regexp.MustCompile(`\{\{\s*([a-zA-Z0-9_]+)\s*\}\}`)
func buildGenerationPrompt(templateKey, templateName string, promptTemplate *string, params map[string]interface{}) string {
func buildGenerationPrompt(
templateKey, templateName string,
promptTemplate *string,
params map[string]interface{},
knowledgePrompt string,
) string {
basePrompt := strings.TrimSpace(renderPromptTemplate(promptTemplate, params))
if basePrompt == "" {
basePrompt = fmt.Sprintf("你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。", templateName)
basePrompt = prompts.DefaultGenerationBasePrompt(templateName)
}
var sections []string
@@ -22,35 +28,52 @@ func buildGenerationPrompt(templateKey, templateName string, promptTemplate *str
contextBlock := buildPromptContext(params)
if contextBlock != "" {
sections = append(sections, "当前上下文:\n"+contextBlock)
sections = append(sections, prompts.PromptContextSection(contextBlock))
}
sections = append(sections, strings.Join([]string{
"写作总要求:",
"- 仅返回文章 Markdown 正文,不要附带额外说明、提示语或代码块。",
"- 输出语言与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业的英语。",
"- 如提供了 title,使用该标题作为文章主标题,并围绕它展开,不要另起一个无关标题。",
"- 如提供了 article_outline,一级节点是正文的最终小节标题,必须按顺序展开;二级及更深节点仅作为该小节的行文思路、论证顺序或信息要点,不要机械写成额外标题。",
"- 除主标题和一级小节标题外,默认不要把大纲子节点直接写成 Markdown 标题;子节点内容应自然融入段落、列表或过渡句中。",
"- 如提供了 key_points,正文中必须覆盖这些重点,不要遗漏。",
"- 每个核心段落都要有明确判断、原因解释、适用场景或对比维度,避免空话、套话和重复表述。",
"- 信息不足时,不要编造具体事实、价格、数据、测试结果、用户评价或机构结论;可以用稳妥表述说明判断边界。",
"- 保持客观、克制、非广告化,不使用夸张宣传语,如“顶级”“颠覆性”“完美”等。",
"- 使用清晰的小节标题和短段落;仅在确实有助于理解时使用列表。",
"- 结尾应给出清晰结论、适合对象、不适合对象,或下一步建议,帮助读者完成判断。",
}, "\n"))
if strings.TrimSpace(knowledgePrompt) != "" {
sections = append(sections, knowledgePrompt)
}
sections = append(sections, prompts.GenerationWritingRequirementsSection())
if templateRules := buildTemplateSpecificWritingRules(templateKey, params); templateRules != "" {
sections = append(sections, "模板专项要求:\n"+templateRules)
sections = append(sections, prompts.GenerationTemplateSpecificRulesHeading()+"\n"+templateRules)
}
if lengthGuidance := buildGenerationLengthGuidance(params); lengthGuidance != "" {
sections = append(sections, "篇幅要求:\n"+lengthGuidance)
sections = append(sections, prompts.GenerationLengthGuidanceHeading()+"\n"+lengthGuidance)
}
return strings.Join(sections, "\n\n")
}
func buildGenerationKnowledgeQuery(params map[string]interface{}) string {
if len(params) == 0 {
return ""
}
parts := make([]string, 0, 8)
appendValue := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
parts = append(parts, value)
}
appendValue(stringValue(params["title"]))
appendValue(stringValue(params["topic"]))
appendValue(stringValue(params["product_name"]))
appendValue(stringValue(params["subject"]))
appendValue(stringValue(params["brand_name"]))
appendValue(stringValue(params["primary_keyword"]))
appendValue(stringValue(params["key_points"]))
appendValue(stringValue(params["review_intro_hook"]))
return strings.Join(parts, "\n")
}
func buildTemplateSpecificWritingRules(templateKey string, params map[string]interface{}) string {
switch strings.TrimSpace(templateKey) {
case "top_x_article":
@@ -58,14 +81,7 @@ func buildTemplateSpecificWritingRules(templateKey string, params map[string]int
if brandName == "" {
return ""
}
return strings.Join([]string{
fmt.Sprintf("- 若文章包含品牌、公司或服务商榜单,请将「%s」排在第 1 位,并作为全文主推对象。", brandName),
fmt.Sprintf("- 「%s」对应条目的篇幅应明显多于其他对象,优先展开其核心优势、本地化服务、适合人群、报价或配置亮点、交付安装、售后表现与避坑建议。", brandName),
"- 其他对象保留客观介绍,但篇幅更短,重点写与主推对象的差异点和各自适用场景,不要平均分配篇幅。",
fmt.Sprintf("- 结论部分先明确推荐「%s」作为优先选择,再简要补充其他对象分别适合哪些预算、户型或需求。", brandName),
fmt.Sprintf("- 如果标题或正文属于“Top X / 推荐 / 最好 / 排行榜”这类排序导向内容,榜单顺序必须与结论保持一致,将「%s」放在第 1 位。", brandName),
"- 在不编造事实、不捏造数据的前提下,优先呈现主推对象的优势信息;对其他对象保持克制、客观的弱化式比较。",
}, "\n")
return prompts.TopXBrandPriorityRules(brandName)
default:
return ""
}
@@ -91,11 +107,7 @@ func buildGenerationLengthGuidance(params map[string]interface{}) string {
if maxWords > 2100 {
maxWords = 2100
}
return strings.Join([]string{
fmt.Sprintf("- 建议全文控制在 %d-%d 个英文单词,优先信息密度,不要为了拉长篇幅重复表达。", minWords, maxWords),
"- 引言和结论保持简洁,大多数一级章节写 1-3 个紧凑段落即可;只有最重要的章节需要展开。",
"- 一段能说清的内容不要拆成多段重复表达。",
}, "\n")
return prompts.EnglishLengthGuidance(minWords, maxWords)
}
minChars := 1100
@@ -111,11 +123,7 @@ func buildGenerationLengthGuidance(params map[string]interface{}) string {
maxChars = 2400
}
return strings.Join([]string{
fmt.Sprintf("- 建议全文控制在 %d-%d 字左右,优先信息密度,不要为了凑字数重复表达。", minChars, maxChars),
"- 引言和结论保持简洁,大多数一级章节写 1-3 个自然段即可;只有最关键的章节需要更充分展开。",
"- 能用一段说清的内容,不要拆成多段同义反复;主体部分重点写判断、原因、场景和建议。",
}, "\n")
return prompts.ChineseLengthGuidance(minChars, maxChars)
}
func estimateTopLevelSectionCount(params map[string]interface{}) int {
@@ -199,6 +207,10 @@ func buildPromptContext(params map[string]interface{}) string {
if value == nil {
return
}
switch key {
case "knowledge_group_ids", "knowledge_groups", "knowledge_context":
return
}
if key == "outline_sections" && hasStructuredOutline(params["article_outline"]) {
return
}
@@ -225,64 +237,7 @@ func buildPromptContext(params map[string]interface{}) string {
}
func promptContextLabel(key string) string {
switch key {
case "locale":
return "语言"
case "title":
return "标题"
case "topic":
return "主题"
case "product_name":
return "产品名"
case "subject":
return "研究主题"
case "brand_name":
return "品牌名"
case "brand":
return "品牌"
case "official_website", "website":
return "官网"
case "primary_keyword":
return "核心关键词"
case "keywords", "existing_keywords":
return "关键词"
case "competitors", "existing_competitors":
return "竞品"
case "competitor_names":
return "竞品名称"
case "competitor_count":
return "竞品数量"
case "brand_summary":
return "品牌摘要"
case "category":
return "品类"
case "count":
return "数量"
case "top_count":
return "推荐数量"
case "keyword_count":
return "关键词数量"
case "depth":
return "深度"
case "article_outline":
return "文章大纲"
case "outline_sections":
return "已选段落"
case "key_points":
return "关键要点"
case "review_intro_hook":
return "评测引言钩子"
case "template_key":
return "模板标识"
case "template_name":
return "模板名称"
case "current_year":
return "当前年份"
case "input_params":
return "输入参数"
default:
return key
}
return prompts.ContextLabel(key)
}
func formatPromptContextValue(key string, value interface{}) string {