package viral import ( "encoding/json" "fmt" "strings" ) // MissionResearchMap is the structured copy-mission research map (single LLM call). type MissionResearchMap struct { AudienceSummary string `json:"audienceSummary"` ContentGoal string `json:"contentGoal"` Questions []string `json:"questions"` Pillars []string `json:"pillars"` Exclusions []string `json:"exclusions"` SuggestedTags []SuggestedTag `json:"suggestedTags"` BenchmarkNotes string `json:"benchmarkNotes"` } func BuildMissionResearchMapSystemPrompt() string { return strings.TrimSpace(`你是 Threads 爆款/對標研究顧問。目標:幫創作者找到「近期熱門、高互動、值得仿寫」的話題與搜尋方向。 規則: 1. audienceSummary:必填,2~4 句描述「受眾是誰」(年齡/情境/痛點/會在 Threads 搜什麼),不要只寫人設本人 2. 聚焦「最近會在 Threads 被搜、被討論」的話題,不要寫成學術報告 3. contentGoal:找到近期互動佳、結構可模仿的爆款貼文 4. pillars:可模仿方向(語錄型、故事型、清單型等),至少 4 個;**必須是字串陣列**,不要用 {title:...} 物件 5. questions:受眾會搜的短問題,5+ 個;字串陣列 6. exclusions:不要模仿的內容,至少 4 個;字串陣列 7. suggestedTags:6~8 個「像真人會在 Threads 搜尋框打的字」 - 每個含 tag, reason, searchIntent(痛點|知識|經驗|對比|工具|語錄), searchType(短詞|情境|語錄) - tag 2~10 字,不要標點、不要完整句子、不要像文章標題 8. benchmarkNotes:怎樣算值得仿的爆款(互動、hook 清楚) 9. 繁體中文;只回傳 JSON(含 audienceSummary)`) } func BuildMissionResearchMapUserPrompt(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【這次想找什麼】\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") } if aud := strings.TrimSpace(in.PersonaAudienceSummary); aud != "" { b.WriteString("\n【人設層級受眾研究(請延伸為本次任務受眾,勿只複製)】\n") b.WriteString(aud) if goal := strings.TrimSpace(in.PersonaContentGoal); goal != "" { b.WriteString("\n內容目標參考:") b.WriteString(goal) } if len(in.PersonaQuestions) > 0 { b.WriteString("\n受眾提問參考:") b.WriteString(strings.Join(in.PersonaQuestions, "、")) } if len(in.PersonaPillars) > 0 { b.WriteString("\n內容支柱參考:") b.WriteString(strings.Join(in.PersonaPillars, "、")) } b.WriteString("\n") } b.WriteString("\n請產出拷貝任務研究地圖 JSON(必填 audienceSummary 與 suggestedTags 陣列)。") return b.String() } func ParseMissionResearchMapOutput(raw string) (MissionResearchMap, error) { payload, err := extractCopyMapJSON(raw) if err != nil { return MissionResearchMap{}, err } var root map[string]json.RawMessage if err := json.Unmarshal(payload, &root); err != nil { return MissionResearchMap{}, fmt.Errorf("parse mission research map: %w", err) } out := MissionResearchMap{ 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: parseFlexibleSuggestedTags(pickRawMessage(root, "suggestedTags", "suggested_tags")), } if strings.TrimSpace(out.AudienceSummary) == "" { return MissionResearchMap{}, fmt.Errorf("mission research map missing audienceSummary") } if len(out.SuggestedTags) < 4 { return MissionResearchMap{}, fmt.Errorf("mission research map needs at least 4 suggested tags") } return out, nil } func cleanSuggestedTags(tags []SuggestedTag) []SuggestedTag { out := []SuggestedTag{} seen := map[string]struct{}{} for _, item := range tags { tag := strings.TrimSpace(item.Tag) if tag == "" { continue } if _, ok := seen[tag]; ok { continue } seen[tag] = struct{}{} out = append(out, SuggestedTag{ Tag: tag, Reason: strings.TrimSpace(item.Reason), SearchIntent: strings.TrimSpace(item.SearchIntent), SearchType: strings.TrimSpace(item.SearchType), }) } return out } func ToEntityMissionResearchMap(m MissionResearchMap) map[string]any { tags := make([]map[string]any, 0, len(m.SuggestedTags)) for _, item := range m.SuggestedTags { tags = append(tags, map[string]any{ "tag": item.Tag, "reason": item.Reason, "search_intent": item.SearchIntent, "search_type": item.SearchType, }) } return map[string]any{ "audience_summary": m.AudienceSummary, "content_goal": m.ContentGoal, "questions": m.Questions, "pillars": m.Pillars, "exclusions": m.Exclusions, "suggested_tags": tags, "benchmark_notes": m.BenchmarkNotes, } }