feat(prompts): add prompts loader and configuration management
- Implemented a new prompts loader in `loader.go` to manage prompt templates and configurations. - Introduced caching mechanism for prompt configurations to optimize loading. - Added functions to apply platform-specific prompt template overrides. - Created unit tests in `loader_test.go` to validate prompt configuration loading and reloading behavior. - Ensured that the last valid configuration is retained in case of errors during reload.
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const promptsConfigEnv = "PROMPTS_CONFIG_FILE"
|
||||
|
||||
type promptsConfig struct {
|
||||
Runtime runtimePromptsConfig `yaml:"runtime"`
|
||||
PlatformTemplates map[string]platformTemplatePromptEntry `yaml:"platform_templates"`
|
||||
}
|
||||
|
||||
type runtimePromptsConfig struct {
|
||||
DefaultGenerationBasePromptTemplate string `yaml:"default_generation_base_prompt_template"`
|
||||
PromptContextHeading string `yaml:"prompt_context_heading"`
|
||||
GenerationWritingRequirementsSection string `yaml:"generation_writing_requirements_section"`
|
||||
GenerationTemplateSpecificRulesHeading string `yaml:"generation_template_specific_rules_heading"`
|
||||
GenerationLengthGuidanceHeading string `yaml:"generation_length_guidance_heading"`
|
||||
TopXBrandPriorityRulesTemplate string `yaml:"top_x_brand_priority_rules_template"`
|
||||
EnglishLengthGuidanceTemplate string `yaml:"english_length_guidance_template"`
|
||||
ChineseLengthGuidanceTemplate string `yaml:"chinese_length_guidance_template"`
|
||||
ContextLabels map[string]string `yaml:"context_labels"`
|
||||
TemplateContextWithJSONExampleTemplate string `yaml:"template_context_with_json_example_template"`
|
||||
JSONOutputExampleHeading string `yaml:"json_output_example_heading"`
|
||||
AnalyzeOutputExample string `yaml:"analyze_output_example"`
|
||||
AnalyzeFallbackPromptTemplate string `yaml:"analyze_fallback_prompt_template"`
|
||||
TitleCustomOutputRequirementsSection string `yaml:"title_custom_output_requirements_section"`
|
||||
TitleOutputExample string `yaml:"title_output_example"`
|
||||
TitleFallbackPromptTemplate string `yaml:"title_fallback_prompt_template"`
|
||||
OutlineOutputExample string `yaml:"outline_output_example"`
|
||||
OutlineCustomOutputRequirementsSection string `yaml:"outline_custom_output_requirements_section"`
|
||||
OutlineFallbackPromptTemplate string `yaml:"outline_fallback_prompt_template"`
|
||||
OutlineResponseFormatDescription string `yaml:"outline_response_format_description"`
|
||||
PromptRuleTaskNameSupplementFormat string `yaml:"prompt_rule_task_name_supplement_format"`
|
||||
PromptRuleSceneSupplementFormat string `yaml:"prompt_rule_scene_supplement_format"`
|
||||
PromptRuleToneSupplementFormat string `yaml:"prompt_rule_tone_supplement_format"`
|
||||
PromptRuleWordCountSupplementFormat string `yaml:"prompt_rule_word_count_supplement_format"`
|
||||
PromptRuleTargetPlatformSupplementFormat string `yaml:"prompt_rule_target_platform_supplement_format"`
|
||||
PromptRuleSupplementHeading string `yaml:"prompt_rule_supplement_heading"`
|
||||
PromptRuleOutputRequirementsSection string `yaml:"prompt_rule_output_requirements_section"`
|
||||
KnowledgePromptIntroLines []string `yaml:"knowledge_prompt_intro_lines"`
|
||||
KnowledgeSnippetLabelFormat string `yaml:"knowledge_snippet_label_format"`
|
||||
KnowledgeWebsiteMarkdown knowledgeWebsiteMarkdownConfig `yaml:"knowledge_website_markdown"`
|
||||
}
|
||||
|
||||
type knowledgeWebsiteMarkdownConfig struct {
|
||||
AssistantRole string `yaml:"assistant_role"`
|
||||
Task string `yaml:"task"`
|
||||
Requirements []string `yaml:"requirements"`
|
||||
TitlePrefix string `yaml:"title_prefix"`
|
||||
URLPrefix string `yaml:"url_prefix"`
|
||||
ScopeSingle string `yaml:"scope_single"`
|
||||
ScopeMultiFormat string `yaml:"scope_multi_format"`
|
||||
}
|
||||
|
||||
type platformTemplatePromptEntry struct {
|
||||
PromptTemplate string `yaml:"prompt_template"`
|
||||
AnalyzePromptTemplate string `yaml:"analyze_prompt_template"`
|
||||
TitlePromptTemplate string `yaml:"title_prompt_template"`
|
||||
OutlinePromptTemplate string `yaml:"outline_prompt_template"`
|
||||
}
|
||||
|
||||
type promptsCache struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
size int64
|
||||
cfg *promptsConfig
|
||||
}
|
||||
|
||||
var (
|
||||
promptsMu sync.RWMutex
|
||||
configuredFilePath string
|
||||
cachedPrompts promptsCache
|
||||
)
|
||||
|
||||
func SetConfigFile(path string) {
|
||||
promptsMu.Lock()
|
||||
defer promptsMu.Unlock()
|
||||
|
||||
configuredFilePath = strings.TrimSpace(path)
|
||||
cachedPrompts = promptsCache{}
|
||||
}
|
||||
|
||||
func loadPromptsConfig() *promptsConfig {
|
||||
path := resolvePromptsConfigFile()
|
||||
|
||||
info, statErr := os.Stat(path)
|
||||
|
||||
promptsMu.RLock()
|
||||
cached := cachedPrompts
|
||||
promptsMu.RUnlock()
|
||||
|
||||
if statErr == nil && cached.cfg != nil && cached.path == path && cached.modTime.Equal(info.ModTime()) && cached.size == info.Size() {
|
||||
return cached.cfg
|
||||
}
|
||||
if statErr != nil && cached.cfg != nil && cached.path == path {
|
||||
return cached.cfg
|
||||
}
|
||||
|
||||
promptsMu.Lock()
|
||||
defer promptsMu.Unlock()
|
||||
|
||||
if info, statErr = os.Stat(path); statErr == nil {
|
||||
if cachedPrompts.cfg != nil && cachedPrompts.path == path && cachedPrompts.modTime.Equal(info.ModTime()) && cachedPrompts.size == info.Size() {
|
||||
return cachedPrompts.cfg
|
||||
}
|
||||
} else if cachedPrompts.cfg != nil && cachedPrompts.path == path {
|
||||
return cachedPrompts.cfg
|
||||
}
|
||||
|
||||
if statErr != nil {
|
||||
panic(fmt.Sprintf("prompts: stat config %q failed: %v", path, statErr))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if cachedPrompts.cfg != nil && cachedPrompts.path == path {
|
||||
return cachedPrompts.cfg
|
||||
}
|
||||
panic(fmt.Sprintf("prompts: read config %q failed: %v", path, err))
|
||||
}
|
||||
|
||||
var cfg promptsConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
if cachedPrompts.cfg != nil && cachedPrompts.path == path {
|
||||
return cachedPrompts.cfg
|
||||
}
|
||||
panic(fmt.Sprintf("prompts: parse config %q failed: %v", path, err))
|
||||
}
|
||||
|
||||
cachedPrompts = promptsCache{
|
||||
path: path,
|
||||
modTime: info.ModTime(),
|
||||
size: info.Size(),
|
||||
cfg: &cfg,
|
||||
}
|
||||
|
||||
return cachedPrompts.cfg
|
||||
}
|
||||
|
||||
func loadRuntimePrompts() runtimePromptsConfig {
|
||||
return loadPromptsConfig().Runtime
|
||||
}
|
||||
|
||||
func resolvePromptsConfigFile() string {
|
||||
promptsMu.RLock()
|
||||
configured := configuredFilePath
|
||||
promptsMu.RUnlock()
|
||||
|
||||
if configured != "" {
|
||||
return configured
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv(promptsConfigEnv)); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv("CONFIG_PATH")); value != "" {
|
||||
return filepath.Join(filepath.Dir(value), "prompts.yml")
|
||||
}
|
||||
if _, file, _, ok := runtime.Caller(0); ok {
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..", "configs", "prompts.yml"))
|
||||
}
|
||||
return filepath.Join("configs", "prompts.yml")
|
||||
}
|
||||
|
||||
func platformTemplatePromptEntryFor(templateKey string) (platformTemplatePromptEntry, bool) {
|
||||
configs := loadPromptsConfig().PlatformTemplates
|
||||
if len(configs) == 0 {
|
||||
return platformTemplatePromptEntry{}, false
|
||||
}
|
||||
entry, ok := configs[strings.TrimSpace(templateKey)]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func ApplyPlatformTemplatePromptOverrides(templateKey string, promptTemplate *string, cardConfigJSON []byte) (*string, []byte) {
|
||||
entry, ok := platformTemplatePromptEntryFor(templateKey)
|
||||
if !ok {
|
||||
return clonePromptTemplate(promptTemplate), cloneBytes(cardConfigJSON)
|
||||
}
|
||||
|
||||
nextPrompt := clonePromptTemplate(promptTemplate)
|
||||
if value := strings.TrimSpace(entry.PromptTemplate); value != "" {
|
||||
nextPrompt = &value
|
||||
}
|
||||
|
||||
nextCardConfig := cloneBytes(cardConfigJSON)
|
||||
if len(nextCardConfig) == 0 {
|
||||
nextCardConfig = []byte("{}")
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(nextCardConfig, &payload); err != nil {
|
||||
return nextPrompt, nextCardConfig
|
||||
}
|
||||
|
||||
wizard, _ := payload["wizard"].(map[string]interface{})
|
||||
if wizard == nil {
|
||||
wizard = make(map[string]interface{})
|
||||
payload["wizard"] = wizard
|
||||
}
|
||||
|
||||
if value := strings.TrimSpace(entry.AnalyzePromptTemplate); value != "" {
|
||||
wizard["analyze_prompt_template"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(entry.TitlePromptTemplate); value != "" {
|
||||
wizard["title_prompt_template"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(entry.OutlinePromptTemplate); value != "" {
|
||||
wizard["outline_prompt_template"] = value
|
||||
}
|
||||
|
||||
marshaled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nextPrompt, nextCardConfig
|
||||
}
|
||||
return nextPrompt, marshaled
|
||||
}
|
||||
|
||||
func clonePromptTemplate(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneBytes(value []byte) []byte {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]byte, len(value))
|
||||
copy(cloned, value)
|
||||
return cloned
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlatformTemplateSeedsInjectPromptConfig(t *testing.T) {
|
||||
SetConfigFile("")
|
||||
t.Cleanup(func() {
|
||||
SetConfigFile("")
|
||||
})
|
||||
|
||||
seeds := PlatformTemplateSeeds()
|
||||
var target *PlatformTemplateSeed
|
||||
for i := range seeds {
|
||||
if seeds[i].Key == "top_x_article" {
|
||||
target = &seeds[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
t.Fatal("top_x_article seed not found")
|
||||
}
|
||||
if !strings.Contains(target.PromptTemplate, "经验丰富的行业观察型内容编辑") {
|
||||
t.Fatalf("PromptTemplate = %q, want yaml-backed template content", target.PromptTemplate)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(target.CardConfigJSON), &payload); err != nil {
|
||||
t.Fatalf("unmarshal card config: %v", err)
|
||||
}
|
||||
|
||||
wizard, _ := payload["wizard"].(map[string]interface{})
|
||||
if wizard == nil {
|
||||
t.Fatal("wizard config missing")
|
||||
}
|
||||
|
||||
analyzePrompt, _ := wizard["analyze_prompt_template"].(string)
|
||||
titlePrompt, _ := wizard["title_prompt_template"].(string)
|
||||
outlinePrompt, _ := wizard["outline_prompt_template"].(string)
|
||||
|
||||
if !strings.Contains(analyzePrompt, "推荐类文章做品牌与竞品分析") {
|
||||
t.Fatalf("analyze_prompt_template = %q, want yaml-backed analyze prompt", analyzePrompt)
|
||||
}
|
||||
if !strings.Contains(titlePrompt, "推荐类文章生成 5 个候选标题") {
|
||||
t.Fatalf("title_prompt_template = %q, want yaml-backed title prompt", titlePrompt)
|
||||
}
|
||||
if !strings.Contains(outlinePrompt, "推荐类文章生成实用大纲") {
|
||||
t.Fatalf("outline_prompt_template = %q, want yaml-backed outline prompt", outlinePrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptConfigReloadsAndKeepsLastGoodVersion(t *testing.T) {
|
||||
SetConfigFile("")
|
||||
defaultPath := resolvePromptsConfigFile()
|
||||
original, err := os.ReadFile(defaultPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read default prompts config: %v", err)
|
||||
}
|
||||
|
||||
tmpPath := filepath.Join(t.TempDir(), "prompts.yml")
|
||||
if err := os.WriteFile(tmpPath, original, 0o600); err != nil {
|
||||
t.Fatalf("write temp prompts config: %v", err)
|
||||
}
|
||||
|
||||
SetConfigFile(tmpPath)
|
||||
t.Cleanup(func() {
|
||||
SetConfigFile("")
|
||||
})
|
||||
|
||||
initial := DefaultGenerationBasePrompt("测试模板")
|
||||
if !strings.Contains(initial, "专业内容编辑") {
|
||||
t.Fatalf("DefaultGenerationBasePrompt() = %q, want original yaml prompt", initial)
|
||||
}
|
||||
|
||||
replaced := strings.Replace(
|
||||
string(original),
|
||||
"你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。",
|
||||
"请为模板「%s」输出新的实时版本。",
|
||||
1,
|
||||
)
|
||||
if err := os.WriteFile(tmpPath, []byte(replaced), 0o600); err != nil {
|
||||
t.Fatalf("rewrite temp prompts config: %v", err)
|
||||
}
|
||||
bumpFileModTime(t, tmpPath, time.Now().Add(2*time.Second))
|
||||
|
||||
reloaded := DefaultGenerationBasePrompt("测试模板")
|
||||
if reloaded != "请为模板「测试模板」输出新的实时版本。" {
|
||||
t.Fatalf("DefaultGenerationBasePrompt() after reload = %q, want updated yaml prompt", reloaded)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tmpPath, []byte("runtime:\n default_generation_base_prompt_template: ["), 0o600); err != nil {
|
||||
t.Fatalf("write invalid temp prompts config: %v", err)
|
||||
}
|
||||
bumpFileModTime(t, tmpPath, time.Now().Add(4*time.Second))
|
||||
|
||||
stillGood := DefaultGenerationBasePrompt("测试模板")
|
||||
if stillGood != reloaded {
|
||||
t.Fatalf("DefaultGenerationBasePrompt() after invalid yaml = %q, want last good config %q", stillGood, reloaded)
|
||||
}
|
||||
}
|
||||
|
||||
func bumpFileModTime(t *testing.T, path string, ts time.Time) {
|
||||
t.Helper()
|
||||
if err := os.Chtimes(path, ts, ts); err != nil {
|
||||
t.Fatalf("chtimes %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
@@ -6,339 +6,170 @@ import (
|
||||
)
|
||||
|
||||
func DefaultGenerationBasePrompt(templateName string) string {
|
||||
return fmt.Sprintf("你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。", templateName)
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().DefaultGenerationBasePromptTemplate), templateName)
|
||||
}
|
||||
|
||||
func PromptContextSection(contextBlock string) string {
|
||||
return "当前上下文:\n" + contextBlock
|
||||
return trimPrompt(loadRuntimePrompts().PromptContextHeading) + "\n" + contextBlock
|
||||
}
|
||||
|
||||
func GenerationWritingRequirementsSection() string {
|
||||
return strings.Join([]string{
|
||||
"写作总要求:",
|
||||
"- 仅返回文章 Markdown 正文,不要附带额外说明、提示语或代码块。",
|
||||
"- 输出语言与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业的英语。",
|
||||
"- 如提供了 title,使用该标题作为文章主标题,并围绕它展开,不要另起一个无关标题。",
|
||||
"- 如提供了 article_outline,一级节点是正文的最终小节标题,必须按顺序展开;二级及更深节点仅作为该小节的行文思路、论证顺序或信息要点,不要机械写成额外标题。",
|
||||
"- 除主标题和一级小节标题外,默认不要把大纲子节点直接写成 Markdown 标题;子节点内容应自然融入段落、列表或过渡句中。",
|
||||
"- 如提供了 key_points,正文中必须覆盖这些重点,不要遗漏。",
|
||||
"- 每个核心段落都要有明确判断、原因解释、适用场景或对比维度,避免空话、套话和重复表述。",
|
||||
"- 信息不足时,不要编造具体事实、价格、数据、测试结果、用户评价或机构结论;可以用稳妥表述说明判断边界。",
|
||||
"- 保持客观、克制、非广告化,不使用夸张宣传语,如“顶级”“颠覆性”“完美”等。",
|
||||
"- 使用清晰的小节标题和短段落;仅在确实有助于理解时使用列表。",
|
||||
"- 结尾应给出清晰结论、适合对象、不适合对象,或下一步建议,帮助读者完成判断。",
|
||||
}, "\n")
|
||||
return trimPrompt(loadRuntimePrompts().GenerationWritingRequirementsSection)
|
||||
}
|
||||
|
||||
func GenerationTemplateSpecificRulesHeading() string {
|
||||
return "模板专项要求:"
|
||||
return trimPrompt(loadRuntimePrompts().GenerationTemplateSpecificRulesHeading)
|
||||
}
|
||||
|
||||
func GenerationLengthGuidanceHeading() string {
|
||||
return "篇幅要求:"
|
||||
return trimPrompt(loadRuntimePrompts().GenerationLengthGuidanceHeading)
|
||||
}
|
||||
|
||||
func TopXBrandPriorityRules(brandName string) string {
|
||||
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 fmt.Sprintf(trimPrompt(loadRuntimePrompts().TopXBrandPriorityRulesTemplate), brandName)
|
||||
}
|
||||
|
||||
func EnglishLengthGuidance(minWords, maxWords int) string {
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("- 建议全文控制在 %d-%d 个英文单词,优先信息密度,不要为了拉长篇幅重复表达。", minWords, maxWords),
|
||||
"- 引言和结论保持简洁,大多数一级章节写 1-3 个紧凑段落即可;只有最重要的章节需要展开。",
|
||||
"- 一段能说清的内容不要拆成多段重复表达。",
|
||||
}, "\n")
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().EnglishLengthGuidanceTemplate), minWords, maxWords)
|
||||
}
|
||||
|
||||
func ChineseLengthGuidance(minChars, maxChars int) string {
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("- 建议全文控制在 %d-%d 字左右,优先信息密度,不要为了凑字数重复表达。", minChars, maxChars),
|
||||
"- 引言和结论保持简洁,大多数一级章节写 1-3 个自然段即可;只有最关键的章节需要更充分展开。",
|
||||
"- 能用一段说清的内容,不要拆成多段同义反复;主体部分重点写判断、原因、场景和建议。",
|
||||
}, "\n")
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().ChineseLengthGuidanceTemplate), minChars, maxChars)
|
||||
}
|
||||
|
||||
func ContextLabel(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
|
||||
if value, ok := loadRuntimePrompts().ContextLabels[key]; ok {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func TemplateContextWithJSONExample(basePrompt, contextJSON, outputExample string) string {
|
||||
return strings.TrimSpace(fmt.Sprintf("%s\n\n模板上下文:\n%s\n\nJSON 输出示例:\n%s", basePrompt, contextJSON, outputExample))
|
||||
return strings.TrimSpace(fmt.Sprintf(
|
||||
trimPrompt(loadRuntimePrompts().TemplateContextWithJSONExampleTemplate),
|
||||
basePrompt,
|
||||
contextJSON,
|
||||
outputExample,
|
||||
))
|
||||
}
|
||||
|
||||
func JSONOutputExampleSection(outputExample string) string {
|
||||
return "JSON 输出示例:\n" + outputExample
|
||||
return trimPrompt(loadRuntimePrompts().JSONOutputExampleHeading) + "\n" + outputExample
|
||||
}
|
||||
|
||||
func AnalyzeOutputExample() string {
|
||||
return `{
|
||||
"brand_summary": "一句简洁的品牌/主题描述",
|
||||
"keywords": ["关键词1", "关键词2", "关键词3", "关键词4", "关键词5"],
|
||||
"competitors": [
|
||||
{
|
||||
"name": "竞品名",
|
||||
"website": "https://example.com",
|
||||
"description": "一句简介"
|
||||
}
|
||||
]
|
||||
}`
|
||||
return trimPrompt(loadRuntimePrompts().AnalyzeOutputExample)
|
||||
}
|
||||
|
||||
func AnalyzeFallbackPrompt(contextJSON string) string {
|
||||
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
|
||||
`, contextJSON, AnalyzeOutputExample()))
|
||||
return strings.TrimSpace(fmt.Sprintf(
|
||||
trimPrompt(loadRuntimePrompts().AnalyzeFallbackPromptTemplate),
|
||||
contextJSON,
|
||||
AnalyzeOutputExample(),
|
||||
))
|
||||
}
|
||||
|
||||
func TitleCustomOutputRequirementsSection() string {
|
||||
return strings.Join([]string{
|
||||
"输出要求:",
|
||||
"- 仅返回 JSON,不要用 Markdown 代码块包裹。",
|
||||
"- 返回恰好 5 个字符串的 JSON 数组。",
|
||||
"- 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。",
|
||||
}, "\n")
|
||||
return trimPrompt(loadRuntimePrompts().TitleCustomOutputRequirementsSection)
|
||||
}
|
||||
|
||||
func TitleOutputExample() string {
|
||||
return `[
|
||||
"标题 1",
|
||||
"标题 2",
|
||||
"标题 3",
|
||||
"标题 4",
|
||||
"标题 5"
|
||||
]`
|
||||
return trimPrompt(loadRuntimePrompts().TitleOutputExample)
|
||||
}
|
||||
|
||||
func TitleFallbackPrompt(contextJSON string) string {
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
你是一位专业的 GEO 内容策略师。
|
||||
|
||||
任务:
|
||||
为当前内容摘要生成 5 个优质文章标题候选。
|
||||
|
||||
规则:
|
||||
- 仅返回 JSON,不要用 Markdown 代码块包裹。
|
||||
- 返回 5 个字符串的 JSON 数组,不要返回对象。
|
||||
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
|
||||
- 标题应实用、具体,并契合当前模板的内容方向。
|
||||
- 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。
|
||||
- 避免泛泛的广告文案和空洞口号。
|
||||
|
||||
模板上下文:
|
||||
%s
|
||||
|
||||
JSON 输出示例:
|
||||
%s
|
||||
`, contextJSON, TitleOutputExample()))
|
||||
return strings.TrimSpace(fmt.Sprintf(
|
||||
trimPrompt(loadRuntimePrompts().TitleFallbackPromptTemplate),
|
||||
contextJSON,
|
||||
TitleOutputExample(),
|
||||
))
|
||||
}
|
||||
|
||||
func OutlineOutputExample() string {
|
||||
return `{
|
||||
"outline": [
|
||||
{
|
||||
"outline": "网站列表",
|
||||
"children": [
|
||||
{ "outline": "竞品 A" },
|
||||
{ "outline": "竞品 B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"outline": "文章关键要点",
|
||||
"children": [
|
||||
{ "outline": "要点 1" },
|
||||
{ "outline": "要点 2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
return trimPrompt(loadRuntimePrompts().OutlineOutputExample)
|
||||
}
|
||||
|
||||
func OutlineCustomOutputRequirementsSection() string {
|
||||
return strings.Join([]string{
|
||||
"输出要求:",
|
||||
"- 仅返回 JSON,不要用 Markdown 代码块包裹。",
|
||||
`- 返回 JSON 对象,格式固定为 {"outline":[...]}`,
|
||||
`- outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]}`,
|
||||
"- 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。",
|
||||
"- 尊重当前关键词、竞品和关键要点,因为用户可能已手动编辑。",
|
||||
"- 不要返回额外字段,不要返回纯数组,不要返回说明文字。",
|
||||
}, "\n")
|
||||
return trimPrompt(loadRuntimePrompts().OutlineCustomOutputRequirementsSection)
|
||||
}
|
||||
|
||||
func OutlineFallbackPrompt(contextJSON string) string {
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
你是一位专业的 GEO 内容策略师。
|
||||
|
||||
任务:
|
||||
为当前内容摘要生成一份实用的文章大纲。
|
||||
|
||||
规则:
|
||||
- 仅返回 JSON,不要用 Markdown 代码块包裹。
|
||||
- 返回 JSON 对象,格式固定为 {"outline":[...]}。
|
||||
- outline 字段中的每个顶层节点 "outline" 值必须保持已选段落标签原文。
|
||||
- 在合适的位置为每个顶层段落添加简洁的子大纲条目。
|
||||
- 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。
|
||||
- 尊重当前标题、关键词、竞品、关键要点和品牌上下文,因为用户可能已手动编辑。
|
||||
- 大纲应具体、不含推广语气,适合后续完整成文。
|
||||
- 不要返回额外字段,不要返回纯数组,不要返回说明文字。
|
||||
|
||||
模板上下文:
|
||||
%s
|
||||
|
||||
JSON 输出示例:
|
||||
%s
|
||||
`, contextJSON, OutlineOutputExample()))
|
||||
return strings.TrimSpace(fmt.Sprintf(
|
||||
trimPrompt(loadRuntimePrompts().OutlineFallbackPromptTemplate),
|
||||
contextJSON,
|
||||
OutlineOutputExample(),
|
||||
))
|
||||
}
|
||||
|
||||
func OutlineResponseFormatDescription() string {
|
||||
return "文章大纲 JSON 对象,必须包含 outline 数组字段。"
|
||||
return trimPrompt(loadRuntimePrompts().OutlineResponseFormatDescription)
|
||||
}
|
||||
|
||||
func PromptRuleTaskNameSupplement(taskName string) string {
|
||||
return "任务名称:" + taskName
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().PromptRuleTaskNameSupplementFormat), taskName)
|
||||
}
|
||||
|
||||
func PromptRuleSceneSupplement(scene string) string {
|
||||
return "适用场景:" + scene
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().PromptRuleSceneSupplementFormat), scene)
|
||||
}
|
||||
|
||||
func PromptRuleToneSupplement(tone string) string {
|
||||
return "默认语气:" + tone
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().PromptRuleToneSupplementFormat), tone)
|
||||
}
|
||||
|
||||
func PromptRuleWordCountSupplement(wordCount int) string {
|
||||
return fmt.Sprintf("建议字数:%d 字左右", wordCount)
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().PromptRuleWordCountSupplementFormat), wordCount)
|
||||
}
|
||||
|
||||
func PromptRuleTargetPlatformSupplement(target string) string {
|
||||
return "目标发布平台:" + strings.ReplaceAll(target, ",", "、")
|
||||
return fmt.Sprintf(
|
||||
trimPrompt(loadRuntimePrompts().PromptRuleTargetPlatformSupplementFormat),
|
||||
strings.ReplaceAll(target, ",", "、"),
|
||||
)
|
||||
}
|
||||
|
||||
func PromptRuleSupplementSection(items []string) string {
|
||||
return "补充要求:\n- " + strings.Join(items, "\n- ")
|
||||
return trimPrompt(loadRuntimePrompts().PromptRuleSupplementHeading) + "\n- " + strings.Join(items, "\n- ")
|
||||
}
|
||||
|
||||
func PromptRuleOutputRequirementsSection() string {
|
||||
return "输出要求:\n1. 直接输出完整可发布的中文 Markdown 文章。\n2. 标题使用一级标题,并由 AI 根据正文内容自行生成,不要直接复用任务名称。\n3. 不要输出你的思考过程、解释或致歉。"
|
||||
return trimPrompt(loadRuntimePrompts().PromptRuleOutputRequirementsSection)
|
||||
}
|
||||
|
||||
func KnowledgePromptIntroLines() []string {
|
||||
return []string{
|
||||
"以下为可参考的知识库内容,只用于补充事实背景、术语定义、产品信息或表达素材:",
|
||||
"- 优先吸收并改写,不要逐段照抄。",
|
||||
"- 若知识库内容与用户明确输入冲突,以用户输入为准。",
|
||||
"- 信息不足时不要虚构事实。",
|
||||
}
|
||||
lines := loadRuntimePrompts().KnowledgePromptIntroLines
|
||||
return append([]string(nil), lines...)
|
||||
}
|
||||
|
||||
func KnowledgeSnippetLabel(index int) string {
|
||||
return fmt.Sprintf("知识片段 %d", index+1)
|
||||
return fmt.Sprintf(trimPrompt(loadRuntimePrompts().KnowledgeSnippetLabelFormat), index+1)
|
||||
}
|
||||
|
||||
func KnowledgeWebsiteMarkdownPrompt(title, rawURL, content string, chunkIndex, chunkCount int) string {
|
||||
scope := "以下是网页正文。"
|
||||
cfg := loadRuntimePrompts().KnowledgeWebsiteMarkdown
|
||||
|
||||
scope := trimPrompt(cfg.ScopeSingle)
|
||||
if chunkCount > 1 {
|
||||
scope = fmt.Sprintf("以下是网页正文的第 %d/%d 段。", chunkIndex+1, chunkCount)
|
||||
scope = fmt.Sprintf(trimPrompt(cfg.ScopeMultiFormat), chunkIndex+1, chunkCount)
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("你是知识库 Markdown 整理助手。\n")
|
||||
builder.WriteString("任务:把给定网页正文整理为适合知识库只读预览的 Markdown。\n")
|
||||
builder.WriteString(trimPrompt(cfg.AssistantRole))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(trimPrompt(cfg.Task))
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("要求:\n")
|
||||
builder.WriteString("1. 只根据原文整理,不要补充事实,不要总结扩写。\n")
|
||||
builder.WriteString("2. 保留标题、列表、表格、编号、联系方式等关键信息。\n")
|
||||
builder.WriteString("3. 如果原文明显是表格,请输出 Markdown 表格。\n")
|
||||
builder.WriteString("4. 不要输出解释,不要输出代码块包裹,只输出 Markdown 正文。\n")
|
||||
for _, item := range cfg.Requirements {
|
||||
builder.WriteString(trimPrompt(item))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if strings.TrimSpace(title) != "" {
|
||||
builder.WriteString("网页标题:")
|
||||
builder.WriteString(trimPrompt(cfg.TitlePrefix))
|
||||
builder.WriteString(title)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if strings.TrimSpace(rawURL) != "" {
|
||||
builder.WriteString("网页地址:")
|
||||
builder.WriteString(trimPrompt(cfg.URLPrefix))
|
||||
builder.WriteString(rawURL)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
@@ -347,3 +178,7 @@ func KnowledgeWebsiteMarkdownPrompt(title, rawURL, content string, chunkIndex, c
|
||||
builder.WriteString(content)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func trimPrompt(value string) string {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
@@ -8,25 +8,35 @@ type PlatformTemplateSeed struct {
|
||||
}
|
||||
|
||||
func PlatformTemplateSeeds() []PlatformTemplateSeed {
|
||||
return append([]PlatformTemplateSeed(nil), platformTemplateSeeds...)
|
||||
seeds := append([]PlatformTemplateSeed(nil), platformTemplateSeeds...)
|
||||
for i := range seeds {
|
||||
promptTemplate, cardConfigJSON := ApplyPlatformTemplatePromptOverrides(
|
||||
seeds[i].Key,
|
||||
stringPointer(seeds[i].PromptTemplate),
|
||||
[]byte(seeds[i].CardConfigJSON),
|
||||
)
|
||||
if promptTemplate != nil {
|
||||
seeds[i].PromptTemplate = *promptTemplate
|
||||
} else {
|
||||
seeds[i].PromptTemplate = ""
|
||||
}
|
||||
seeds[i].CardConfigJSON = string(cardConfigJSON)
|
||||
}
|
||||
return seeds
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
{
|
||||
Key: "top_x_article",
|
||||
Name: "Top X 文章",
|
||||
PromptTemplate: `你是一名经验丰富的内容编辑,请围绕「{{topic}}」撰写一篇完整的 Markdown 推荐类文章。
|
||||
文章目标:
|
||||
1. 帮读者快速理解这个主题下值得关注的 {{count}} 个方向、产品或对象。
|
||||
2. 说明每个对象的核心特点、适用场景、优缺点和选择建议。
|
||||
|
||||
写作要求:
|
||||
- 不要写成空洞的排行列表;每个对象都要有清晰的入选理由和判断依据。
|
||||
- 如果提供了 competitors、品牌或关键词上下文,要自然融入横向比较,但不要机械重复名单。
|
||||
- 如果提供了 brand_name,且文章属于品牌、公司或服务商对比型推荐内容,应将该品牌作为主推对象:默认排在第 1 位、正文篇幅明显多于其他对象、结论优先推荐。
|
||||
- 对 brand_name 对应对象重点展开核心优势、本地服务、适合人群、报价或配置亮点、交付安装、售后与避坑建议;其他对象保持客观但更简洁的差异化介绍。
|
||||
- 重点写筛选标准、差异点、适合谁、不适合谁,以及常见误区或避坑点。
|
||||
- 引言先交代推荐标准与读者能得到什么,结尾优先总结 brand_name 为什么值得优先考虑,再补充其他对象各自适合的预算与需求。`,
|
||||
Key: "top_x_article",
|
||||
Name: "Top X 文章",
|
||||
PromptTemplate: "",
|
||||
CardConfigJSON: `{
|
||||
"hero": {
|
||||
"accent": "Top X",
|
||||
@@ -87,9 +97,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
"topic": { "source": "primary_keyword_or_brand" },
|
||||
"count": { "source": "competitor_count", "fallback_number": 5 }
|
||||
},
|
||||
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为推荐类文章做品牌与竞品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n\n任务:\n1. 分析品牌/话题上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合推荐类文章的搜索关键词。\n3. 推荐最多 6 个可信竞品或对比对象。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应为简洁的搜索短语。\n- 竞品应去重且真实,不确定时 website 留空,不要编造 URL。",
|
||||
"title_prompt_template": "你正在为一篇推荐类文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n核心关键词: {{primary_keyword}}\n品牌名: {{brand_name}}\n推荐数量: {{top_count}}\n竞品名称: {{competitor_names}}\n\n要求:\n- 标题风格应契合实用选购指南、推荐合集、横向对比、避坑盘点等内容调性。\n- 不要机械重复模板名称或「Top X 文章」字样。\n- 每个标题应具体、可读,且角度各不相同。\n- 当 locale 为 zh-CN 时,优先使用自然的中文标题风格,如推荐、精选、怎么选、避坑、盘点等,但避免空洞标题党。",
|
||||
"outline_prompt_template": "你正在为一篇推荐类文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n核心关键词: {{primary_keyword}}\n品牌名: {{brand_name}}\n竞品名称: {{competitor_names}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 网站列表段落下按竞品逐一展开。\n- 内容应具体、有对比、有结论导向,避免泛泛而谈。\n- 使用与 locale 一致的语言。",
|
||||
"analyze_prompt_template": "",
|
||||
"title_prompt_template": "",
|
||||
"outline_prompt_template": "",
|
||||
"outline": {
|
||||
"allow_custom": true,
|
||||
"custom_placeholder": "补充一个额外结构,例如\"选购建议\"",
|
||||
@@ -112,19 +122,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Key: "product_review",
|
||||
Name: "产品评测文章",
|
||||
PromptTemplate: `你是一名资深产品评测编辑,请围绕「{{product_name}}」这款{{category}}产品撰写一篇完整的 Markdown 评测文章。
|
||||
文章目标:
|
||||
1. 帮读者判断这款产品是否值得关注或购买。
|
||||
2. 解释它的核心能力、实际使用感受、主要优缺点和适合人群。
|
||||
|
||||
写作要求:
|
||||
- 开头快速进入评测主题;如果提供了 review_intro_hook,则按对应风格自然起笔,但不要故作夸张。
|
||||
- 正文要覆盖产品定位、核心功能、使用体验、限制点、适用场景和购买建议。
|
||||
- 评价必须有判断,不要只罗列卖点;要说明为什么是优点、在什么情况下会变成限制。
|
||||
- 结论写清楚适合谁、不适合谁,以及在什么条件下值得买。
|
||||
- 全文保持客观、务实,不要写成品牌宣传稿。`,
|
||||
Key: "product_review",
|
||||
Name: "产品评测文章",
|
||||
PromptTemplate: "",
|
||||
CardConfigJSON: `{
|
||||
"hero": {
|
||||
"accent": "Review",
|
||||
@@ -180,7 +180,7 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
{ "key": "feeling", "label": "分享感受", "description": "用第一视角的真实感受建立代入感" }
|
||||
]
|
||||
},
|
||||
"outline_prompt_template": "你正在为一篇产品评测文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n标题: {{title}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n评测引言钩子: {{review_intro_hook}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,使用与请求一致的语言。\n- 当有引言段落时,子条目应体现所选的评测引言钩子风格。\n- 聚焦具体能力拆解、真实使用判断、优缺点权衡和适合人群。\n- 大纲务实、简洁、不含推广语气。"
|
||||
"outline_prompt_template": ""
|
||||
},
|
||||
"review": {
|
||||
"card": {
|
||||
@@ -190,8 +190,8 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
"alert_message": "后端会立即创建文章与任务记录,再由排队 worker 异步完成正文生成。"
|
||||
},
|
||||
"managed_fields": [],
|
||||
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为产品评测文章做品牌与产品分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n产品名: {{product_name}}\n品类: {{category}}\n\n任务:\n1. 分析产品与品牌上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个适合评测文章的搜索关键词,聚焦产品能力、使用场景和购买决策。\n3. 推荐最多 6 个可信的同类产品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词侧重评测视角,如「XX 评测」「XX 值不值得买」「XX 优缺点」。\n- 竞品应去重且真实,不确定时 website 留空。",
|
||||
"title_prompt_template": "你正在为一篇产品评测文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n产品名: {{product_name}}\n品类: {{category}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应像可信的评测、测评、购买建议类内容。\n- 用具体表达代替空洞口号。\n- 当 locale 为 zh-CN 时,标题应贴合中文内容平台风格,可使用评测、实测、值不值得买、优缺点、怎么选等表达。\n- 返回 5 个角度明确不同的标题。",
|
||||
"analyze_prompt_template": "",
|
||||
"title_prompt_template": "",
|
||||
"outline": {
|
||||
"allow_custom": true,
|
||||
"custom_placeholder": "补充一个额外结构,例如\"购买建议\"",
|
||||
@@ -212,18 +212,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Key: "research_report",
|
||||
Name: "研究报告文章",
|
||||
PromptTemplate: `你是一名研究分析师,请围绕「{{subject}}」撰写一篇完整的 Markdown 研究报告。
|
||||
文章目标:
|
||||
1. 提炼关键事实、趋势与结构化发现。
|
||||
2. 解释背后原因,并给出可执行的判断或建议。
|
||||
|
||||
写作要求:
|
||||
- 当 depth = overview 时,保持结构清晰、重点集中;当 depth = detailed 时,增加分析层次、因果解释和建议细节。
|
||||
- 不要泛泛复述背景,要突出关键发现、变化趋势、风险与影响。
|
||||
- 如果提供了品牌、关键词或参考对象,应把它们作为分析参照,而不是简单罗列。
|
||||
- 摘要部分先给核心结论,正文再展开依据,结尾落到行动建议或后续观察点。`,
|
||||
Key: "research_report",
|
||||
Name: "研究报告文章",
|
||||
PromptTemplate: "",
|
||||
CardConfigJSON: `{
|
||||
"hero": {
|
||||
"accent": "Report",
|
||||
@@ -278,9 +269,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
"derived_inputs": {
|
||||
"subject": { "source": "primary_keyword_or_brand" }
|
||||
},
|
||||
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为研究报告做主题与背景分析。\n模板: {{template_name}}\n语言: {{locale}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n\n任务:\n1. 分析研究主题上下文,给出 1-2 句主题摘要。\n2. 推荐最多 5 个适合研究报告的搜索关键词,侧重行业术语和趋势表达。\n3. 推荐最多 6 个可作为参照的品牌、机构或竞争对手。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应体现分析性视角,如「XX 市场分析」「XX 趋势」「XX 行业报告」。\n- 竞品应去重且真实,不确定时 website 留空。",
|
||||
"title_prompt_template": "你正在为一篇研究报告文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n\n要求:\n- 标题应具有分析性、聚焦感和洞察力。\n- 避免模糊的企业公关式措辞。\n- 当 locale 为 zh-CN 时,可使用研究报告、趋势洞察、观察、判断、分析等表达。\n- 返回 5 个角度各异但可信的报告风格标题。",
|
||||
"outline_prompt_template": "你正在为一篇研究报告文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n研究主题: {{subject}}\n深度: {{depth}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 关键发现段落应按数据驱动的方式组织子条目。\n- 内容应具有结构性、分析深度和可操作性。\n- 使用与 locale 一致的语言。",
|
||||
"analyze_prompt_template": "",
|
||||
"title_prompt_template": "",
|
||||
"outline_prompt_template": "",
|
||||
"outline": {
|
||||
"allow_custom": true,
|
||||
"custom_placeholder": "自定义输入,不超过10个字",
|
||||
@@ -304,19 +295,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Key: "brand_search_expansion",
|
||||
Name: "品牌词搜索扩写",
|
||||
PromptTemplate: `你是一名擅长搜索意图写作的内容编辑,请围绕「{{brand}} {{keyword}}」撰写一篇完整的 Markdown 品牌词搜索扩写文章。
|
||||
文章目标:
|
||||
1. 直接回答搜索用户最关心的问题。
|
||||
2. 解释品牌背景、核心信息和常见对比/疑问,承接后续转化或继续了解。
|
||||
|
||||
写作要求:
|
||||
- 开头优先回答搜索意图,不要长篇铺垫。
|
||||
- 正文应覆盖品牌是什么、用户为什么会搜这个词、常见疑问、对比点和下一步决策信息。
|
||||
- 如果提供了竞品或替代方案,按用户常见比较维度展开,不要空泛地说“各有优势”。
|
||||
- 语气客观、有信息量,避免企业宣传腔。
|
||||
- 结尾给出简洁总结,并提示用户接下来应该关注什么信息来继续判断。`,
|
||||
Key: "brand_search_expansion",
|
||||
Name: "品牌词搜索扩写",
|
||||
PromptTemplate: "",
|
||||
CardConfigJSON: `{
|
||||
"hero": {
|
||||
"accent": "Brand SEO",
|
||||
@@ -377,9 +358,9 @@ var platformTemplateSeeds = []PlatformTemplateSeed{
|
||||
"brand": { "source": "brand_name" },
|
||||
"keyword": { "source": "primary_keyword" }
|
||||
},
|
||||
"analyze_prompt_template": "你是一位专业的 GEO 内容策略师,正在为品牌词搜索扩写文章做品牌与意图分析。\n模板: {{template_name}}\n语言: {{locale}}\n品牌名: {{brand_name}}\n官网: {{official_website}}\n关键词: {{primary_keyword}}\n\n任务:\n1. 分析品牌与搜索意图上下文,给出 1-2 句品牌摘要。\n2. 推荐最多 5 个品牌词搜索相关的关键词,侧重搜索意图和用户疑问。\n3. 推荐最多 6 个可信的竞品或替代方案。\n\n规则:\n- 仅返回 JSON,不要用 Markdown 代码块包裹。\n- 使用与 locale 一致的语言。\n- 关键词应贴合搜索意图,如「XX 是什么」「XX 怎么样」「XX 对比 YY」。\n- 竞品应去重且真实,不确定时 website 留空。",
|
||||
"title_prompt_template": "你正在为一篇品牌词搜索扩写文章生成 5 个候选标题。\n模板: {{template_name}}\n语言: {{locale}}\n当前年份: {{current_year}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n官网: {{official_website}}\n\n要求:\n- 标题应适用于品牌搜索意图、对比、问答和解释型内容。\n- 避免空洞口号。\n- 当 locale 为 zh-CN 时,可使用是什么、怎么样、怎么选、对比、值得买吗等表达。\n- 返回 5 个不同的、贴合搜索意图的标题选项。",
|
||||
"outline_prompt_template": "你正在为一篇品牌词搜索扩写文章生成实用大纲。\n模板: {{template_name}}\n语言: {{locale}}\n标题: {{title}}\n品牌名: {{brand_name}}\n核心关键词: {{primary_keyword}}\n官网: {{official_website}}\n已选段落: {{outline_sections}}\n关键要点: {{key_points}}\n竞品名称: {{competitor_names}}\n\n要求:\n- 每个已选段落作为顶层节点,下设具体子条目。\n- 品牌概览段落应覆盖品牌定位、核心产品和差异化。\n- 对比段落应按竞品逐一展开对比维度。\n- 内容应回答搜索意图,语气客观有信息量。\n- 使用与 locale 一致的语言。",
|
||||
"analyze_prompt_template": "",
|
||||
"title_prompt_template": "",
|
||||
"outline_prompt_template": "",
|
||||
"outline": {
|
||||
"allow_custom": true,
|
||||
"custom_placeholder": "补充一个额外结构,例如\"常见问题\"",
|
||||
|
||||
Reference in New Issue
Block a user