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

147 lines
6.2 KiB
Go
Raw 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"
"haixun-backend/internal/library/llmjson"
)
// 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. 必須保留使用者原始題目的核心主詞、情境與目的。例如「備孕男生要吃什麼保健品」不可簡化成「老公吃什麼」
3. contentGoal用延伸知識與人設產出可發的 Threads 內容;爆款貼文只作為可選參考,不是必經來源
4. pillars延伸知識支柱至少 4 個;**必須是字串陣列**,每項要能支撐一篇內容,例如營養素、檢查、生活習慣、常見迷思
5. questions受眾會搜尋或想問的完整問題5+ 個;字串陣列,需保留主詞與情境
6. exclusions不要寫的內容至少 4 個;包含過度醫療承諾、偏方、恐嚇、與題目無關的家庭日常
7. suggestedTags68 個「搜尋查詢」,不是短 hashtag
- 每個含 tag, reason, searchIntent痛點|知識|經驗|對比|工具|帳號), searchType查詢|情境|帳號)
- tag 622 字,必須保留核心意圖;可以像真人搜尋句,例如「備孕男生保健品」「精蟲品質吃什麼」「男性備孕葉酸鋅」
- 不要輸出「老公吃什麼」「多閱讀」這種失去主題的泛詞
8. benchmarkNotes整理延伸知識重點與人工看相似帳號時的觀察方向可列 35 個重點
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。請把 suggestedTags 當成完整搜尋查詢,不要壓成短 hashtag。")
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 := llmjson.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,
}
}