thread-master/internal/library/viral/research_map.go

141 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package viral
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
type CopyResearchMap struct {
AudienceSummary string `json:"audienceSummary"`
ContentGoal string `json:"contentGoal"`
Questions []string `json:"questions"`
Pillars []string `json:"pillars"`
Exclusions []string `json:"exclusions"`
SuggestedTags []string `json:"suggestedTags"`
BenchmarkNotes string `json:"benchmarkNotes"`
}
type CopyResearchMapInput struct {
Label string
SeedQuery string
Brief string
Persona string
StyleBenchmark string
PersonaAudienceSummary string
PersonaContentGoal string
PersonaQuestions []string
PersonaPillars []string
}
var copyMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
func BuildCopyResearchMapSystemPrompt() string {
return strings.TrimSpace(`你是 Threads 爆款/對標研究顧問。目標是幫創作者找到「高互動、值得仿寫」的參考貼文與對標方向。
規則:
1. audienceSummary必填24 句描述「受眾是誰」(情境、痛點、會搜什麼)
2. contentGoal 要寫:找到近期互動佳、結構可模仿的爆款貼文,分析 hook節奏文案公式
3. pillars可模仿的內容方向語錄型、故事型、清單型等至少 4 個
4. questions受眾會搜尋關心的短問題5+ 個,適合當爆款掃描關鍵字
5. exclusions不要模仿的內容業配、純晒照、無結構閒聊等至少 4 個
6. suggestedTags24 字短詞10 個左右,用於 Threads 搜尋爆款
7. benchmarkNotes一句話說明怎樣算「值得仿的爆款」互動、留言品質、hook 清楚)
8. 繁體中文;只回傳 JSONaudienceSummary, contentGoal, questions, pillars, exclusions, suggestedTags, benchmarkNotes`)
}
func BuildCopyResearchMapUserPrompt(in CopyResearchMapInput) string {
var b strings.Builder
b.WriteString("【主題】")
b.WriteString(strings.TrimSpace(in.Label))
b.WriteString("\n【種子關鍵字】")
b.WriteString(strings.TrimSpace(in.SeedQuery))
b.WriteString("\n【Brief】\n")
b.WriteString(strings.TrimSpace(in.Brief))
if p := strings.TrimSpace(in.Persona); p != "" {
b.WriteString("\n【人設】\n")
b.WriteString(p)
}
if bench := strings.TrimSpace(in.StyleBenchmark); bench != "" {
b.WriteString("\n【對標帳號】@")
b.WriteString(strings.TrimPrefix(bench, "@"))
b.WriteString("\n")
}
b.WriteString("\n請產出拷貝忍者研究地圖 JSON。")
return b.String()
}
func ParseCopyResearchMapOutput(raw string) (CopyResearchMap, error) {
payload, err := extractCopyMapJSON(raw)
if err != nil {
return CopyResearchMap{}, err
}
var root map[string]json.RawMessage
if err := json.Unmarshal(payload, &root); err != nil {
return CopyResearchMap{}, fmt.Errorf("parse copy research map: %w", err)
}
tagObjs := parseFlexibleSuggestedTags(pickRawMessage(root, "suggestedTags", "suggested_tags"))
tagStrs := make([]string, 0, len(tagObjs))
for _, item := range tagObjs {
if item.Tag != "" {
tagStrs = append(tagStrs, item.Tag)
}
}
out := CopyResearchMap{
AudienceSummary: firstJSONString(root, "audienceSummary", "audience_summary"),
ContentGoal: firstJSONString(root, "contentGoal", "content_goal"),
BenchmarkNotes: firstJSONString(root, "benchmarkNotes", "benchmark_notes"),
Questions: parseFlexibleStringList(pickRawMessage(root, "questions")),
Pillars: parseFlexibleStringList(pickRawMessage(root, "pillars")),
Exclusions: parseFlexibleStringList(pickRawMessage(root, "exclusions")),
SuggestedTags: cleanLines(tagStrs),
}
if strings.TrimSpace(out.AudienceSummary) == "" {
return CopyResearchMap{}, fmt.Errorf("copy research map missing audienceSummary")
}
return out, nil
}
func ToEntityResearchMap(m CopyResearchMap) map[string]any {
return map[string]any{
"audience_summary": m.AudienceSummary,
"content_goal": m.ContentGoal,
"questions": m.Questions,
"pillars": m.Pillars,
"exclusions": m.Exclusions,
"suggested_tags": m.SuggestedTags,
"benchmark_notes": m.BenchmarkNotes,
}
}
func cleanLines(items []string) []string {
out := make([]string, 0, len(items))
seen := map[string]struct{}{}
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
out = append(out, item)
}
return out
}
func extractCopyMapJSON(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := copyMapFenceRE.FindStringSubmatch(raw); len(m) == 2 {
raw = strings.TrimSpace(m[1])
}
start := strings.Index(raw, "{")
end := strings.LastIndex(raw, "}")
if start < 0 || end <= start {
return nil, fmt.Errorf("copy research map missing json")
}
return []byte(raw[start : end+1]), nil
}