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

144 lines
5.6 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
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,
}
}