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
+12 -134
View File
@@ -13,6 +13,7 @@ import (
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/llm"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
@@ -474,7 +475,7 @@ func (s *TemplateService) executeOutlineTask(ctx context.Context, repo repositor
ResponseFormat: &llm.ResponseFormat{
Type: llm.ResponseFormatTypeJSONObject,
Name: "article_outline",
Description: "文章大纲 JSON 对象,必须包含 outline 数组字段。",
Description: prompts.OutlineResponseFormatDescription(),
},
}, nil)
if err != nil {
@@ -666,56 +667,12 @@ func buildAnalyzePrompt(templateKey, templateName string, req AnalyzeTaskRequest
basePrompt := strings.TrimSpace(renderPromptTemplate(analyzePromptTemplate, contextPayload))
if basePrompt != "" {
contextJSON, _ := json.MarshalIndent(contextPayload, "", " ")
outputExample := `{
"brand_summary": "一句简洁的品牌/主题描述",
"keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
"competitors": [
{
"name": "竞品名",
"website": "https://example.com",
"description": "一句简介"
}
]
}`
return strings.TrimSpace(fmt.Sprintf("%s\n\n模板上下文:\n%s\n\nJSON 输出示例:\n%s", basePrompt, string(contextJSON), outputExample))
return prompts.TemplateContextWithJSONExample(basePrompt, string(contextJSON), prompts.AnalyzeOutputExample())
}
}
contextJSON, _ := json.MarshalIndent(contextPayload, "", " ")
outputExample := `{
"brand_summary": "一句简洁的品牌/主题描述",
"keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
"competitors": [
{
"name": "竞品名",
"website": "https://example.com",
"description": "一句简介"
}
]
}`
return strings.TrimSpace(fmt.Sprintf(`
你是一位专业的 GEO 内容策略师。
任务:
1. 分析品牌/主题上下文。
2. 推荐最相关的 GEO 文章关键词。
3. 推荐可信的竞品网站或竞争品牌,用于对比或引用。
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 关键词应为简洁的搜索短语。
- 竞品应去重且真实。不确定时 website 字段可留空,但不要编造虚假链接。
- brand_summary 限 1-2 句。
- 最多返回 6 个竞品和 5 个关键词。
模板上下文:
%s
JSON 输出示例:
%s
`, string(contextJSON), outputExample))
return prompts.AnalyzeFallbackPrompt(string(contextJSON))
}
func buildTitlePrompt(templateKey, templateName string, req TitleTaskRequest, titlePromptTemplate *string) string {
@@ -725,115 +682,36 @@ func buildTitlePrompt(templateKey, templateName string, req TitleTaskRequest, ti
if basePrompt != "" {
sections := []string{basePrompt}
if contextBlock := buildPromptContext(contextPayload); contextBlock != "" {
sections = append(sections, "当前上下文:\n"+contextBlock)
sections = append(sections, prompts.PromptContextSection(contextBlock))
}
sections = append(sections, strings.Join([]string{
"输出要求:",
"- 仅返回 JSON,不要用 Markdown 代码块包裹。",
"- 返回恰好 5 个字符串的 JSON 数组。",
"- 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。",
}, "\n"))
sections = append(sections, prompts.TitleCustomOutputRequirementsSection())
return strings.Join(sections, "\n\n")
}
}
contextJSON, _ := json.MarshalIndent(contextPayload, "", " ")
outputExample := `[
"标题 1",
"标题 2",
"标题 3",
"标题 4",
"标题 5"
]`
return strings.TrimSpace(fmt.Sprintf(`
你是一位专业的 GEO 内容策略师。
任务:
为当前内容摘要生成 5 个优质文章标题候选。
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 返回 5 个字符串的 JSON 数组,不要返回对象。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 标题应实用、具体,并契合当前模板的内容方向。
- 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。
- 避免泛泛的广告文案和空洞口号。
模板上下文:
%s
JSON 输出示例:
%s
`, string(contextJSON), outputExample))
return prompts.TitleFallbackPrompt(string(contextJSON))
}
func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest, outlinePromptTemplate *string) string {
contextPayload := outlinePromptParams(templateKey, templateName, req)
outputExample := `{
"outline": [
{
"outline": "网站列表",
"children": [
{ "outline": "竞品 A" },
{ "outline": "竞品 B" }
]
},
{
"outline": "文章关键要点",
"children": [
{ "outline": "要点 1" },
{ "outline": "要点 2" }
]
}
]
}`
outputExample := prompts.OutlineOutputExample()
if outlinePromptTemplate != nil && strings.TrimSpace(*outlinePromptTemplate) != "" {
basePrompt := strings.TrimSpace(renderPromptTemplate(outlinePromptTemplate, contextPayload))
if basePrompt != "" {
sections := []string{basePrompt}
if contextBlock := buildPromptContext(contextPayload); contextBlock != "" {
sections = append(sections, "当前上下文:\n"+contextBlock)
sections = append(sections, prompts.PromptContextSection(contextBlock))
}
sections = append(sections, strings.Join([]string{
"输出要求:",
"- 仅返回 JSON,不要用 Markdown 代码块包裹。",
`- 返回 JSON 对象,格式固定为 {"outline":[...]}`,
`- outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]}`,
"- 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。",
"- 尊重当前关键词、竞品和关键要点,因为用户可能已手动编辑。",
"- 不要返回额外字段,不要返回纯数组,不要返回说明文字。",
}, "\n"))
sections = append(sections, "JSON 输出示例:\n"+outputExample)
sections = append(sections, prompts.OutlineCustomOutputRequirementsSection())
sections = append(sections, prompts.JSONOutputExampleSection(outputExample))
return strings.Join(sections, "\n\n")
}
}
contextJSON, _ := json.MarshalIndent(contextPayload, "", " ")
return strings.TrimSpace(fmt.Sprintf(`
你是一位专业的 GEO 内容策略师。
任务:
为当前内容摘要生成一份实用的文章大纲。
规则:
- 仅返回 JSON,不要用 Markdown 代码块包裹。
- 返回 JSON 对象,格式固定为 {"outline":[...]}。
- outline 字段中的每个顶层节点 "outline" 值必须保持已选段落标签原文。
- 在合适的位置为每个顶层段落添加简洁的子大纲条目。
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
- 尊重当前标题、关键词、竞品、关键要点和品牌上下文,因为用户可能已手动编辑。
- 大纲应具体、不含推广语气,适合后续完整成文。
- 不要返回额外字段,不要返回纯数组,不要返回说明文字。
模板上下文:
%s
JSON 输出示例:
%s
`, string(contextJSON), outputExample))
return prompts.OutlineFallbackPrompt(string(contextJSON))
}
func titlePromptParams(templateKey, templateName string, req TitleTaskRequest) map[string]interface{} {