haixunMaster/haixun-backend/internal/library/viral/mission_research_map.go

144 lines
5.6 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"
"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必填24 句描述「受眾是誰」(年齡/情境/痛點/會在 Threads 搜什麼),不要只寫人設本人
2. 聚焦「最近會在 Threads 被搜、被討論」的話題,不要寫成學術報告
3. contentGoal找到近期互動佳、結構可模仿的爆款貼文
4. pillars可模仿方向語錄型、故事型、清單型等至少 4 個;**必須是字串陣列**,不要用 {title:...} 物件
5. questions受眾會搜的短問題5+ 個;字串陣列
6. exclusions不要模仿的內容至少 4 個;字串陣列
7. suggestedTags68 個「像真人會在 Threads 搜尋框打的字」
- 每個含 tag, reason, searchIntent痛點|知識|經驗|對比|工具|語錄), searchType短詞|情境|語錄)
- tag 210 字,不要標點、不要完整句子、不要像文章標題
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,
}
}