127 lines
4.1 KiB
Go
127 lines
4.1 KiB
Go
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
|
||
}
|
||
|
||
var copyMapFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
|
||
|
||
func BuildCopyResearchMapSystemPrompt() string {
|
||
return strings.TrimSpace(`你是 Threads 爆款/對標研究顧問。目標是幫創作者找到「高互動、值得仿寫」的參考貼文與對標方向。
|
||
|
||
規則:
|
||
1. contentGoal 要寫:找到近期互動佳、結構可模仿的爆款貼文,分析 hook/節奏/文案公式
|
||
2. pillars:可模仿的內容方向(語錄型、故事型、清單型等),至少 4 個
|
||
3. questions:受眾會搜尋/關心的短問題,5+ 個,適合當爆款掃描關鍵字
|
||
4. exclusions:不要模仿的內容(業配、純晒照、無結構閒聊等),至少 4 個
|
||
5. suggestedTags:2~4 字短詞,10 個左右,用於 Threads 搜尋爆款
|
||
6. benchmarkNotes:一句話說明怎樣算「值得仿的爆款」(互動、留言品質、hook 清楚)
|
||
7. 繁體中文;只回傳 JSON:audienceSummary, 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 out CopyResearchMap
|
||
if err := json.Unmarshal(payload, &out); err != nil {
|
||
return CopyResearchMap{}, fmt.Errorf("parse copy research map: %w", err)
|
||
}
|
||
out.AudienceSummary = strings.TrimSpace(out.AudienceSummary)
|
||
out.ContentGoal = strings.TrimSpace(out.ContentGoal)
|
||
out.BenchmarkNotes = strings.TrimSpace(out.BenchmarkNotes)
|
||
out.Questions = cleanLines(out.Questions)
|
||
out.Pillars = cleanLines(out.Pillars)
|
||
out.Exclusions = cleanLines(out.Exclusions)
|
||
out.SuggestedTags = cleanLines(out.SuggestedTags)
|
||
if out.AudienceSummary == "" && len(out.SuggestedTags) == 0 {
|
||
return CopyResearchMap{}, fmt.Errorf("copy research map missing content")
|
||
}
|
||
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
|
||
}
|