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:必填,2~4 句描述「受眾是誰」(情境、痛點、會搜什麼) 2. contentGoal 要寫:找到近期互動佳、結構可模仿的爆款貼文,分析 hook/節奏/文案公式 3. pillars:可模仿的內容方向(語錄型、故事型、清單型等),至少 4 個 4. questions:受眾會搜尋/關心的短問題,5+ 個,適合當爆款掃描關鍵字 5. exclusions:不要模仿的內容(業配、純晒照、無結構閒聊等),至少 4 個 6. suggestedTags:2~4 字短詞,10 個左右,用於 Threads 搜尋爆款 7. benchmarkNotes:一句話說明怎樣算「值得仿的爆款」(互動、留言品質、hook 清楚) 8. 繁體中文;只回傳 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 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 }