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

155 lines
6.7 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package viral
import (
"encoding/json"
"fmt"
"strings"
2026-06-28 11:58:15 +00:00
"haixun-backend/internal/library/llmjson"
2026-06-26 08:37:04 +00:00
)
// 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 {
2026-06-28 08:28:42 +00:00
return strings.TrimSpace(`你是 Threads 拷貝任務的延伸知識與相似帳號研究顧問目標把使用者的完整意圖延伸成可產文的知識地圖並提供適合人工參考的相似帳號搜尋方向不要把題目壓扁成泛泛短詞
2026-06-26 08:37:04 +00:00
規則
1. audienceSummary必填24 句描述受眾是誰年齡情境痛點會在 Threads 搜什麼不要只寫人設本人
2026-06-28 08:28:42 +00:00
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 個重點
2026-06-26 08:37:04 +00:00
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")
}
2026-06-28 08:28:42 +00:00
b.WriteString("\n請產出拷貝任務研究地圖 JSON。請把 suggestedTags 當成完整搜尋查詢,不要壓成短 hashtag。")
2026-06-26 08:37:04 +00:00
return b.String()
}
2026-06-28 12:25:39 +00:00
func MissionResearchMapJSONOnlyRetryPrompt() string {
return strings.TrimSpace(`上次回覆沒有包含可解析的 JSON****輸出一個 JSON 物件
- 直接以 { 開頭 } 結尾不要任何思考過程說明文字或 markdown 圍欄
- 需含 audienceSummarycontentGoalquestionspillarsexclusionssuggestedTagsbenchmarkNotes 欄位
- suggestedTags 至少 4 每項含 tag/reason/searchIntent/searchType
- 不要使用 ... 省略也不要加註解或尾端逗號`)
}
2026-06-26 08:37:04 +00:00
func ParseMissionResearchMapOutput(raw string) (MissionResearchMap, error) {
payload, err := extractCopyMapJSON(raw)
if err != nil {
return MissionResearchMap{}, err
}
var root map[string]json.RawMessage
2026-06-28 11:58:15 +00:00
if err := llmjson.Unmarshal(payload, &root); err != nil {
2026-06-26 08:37:04 +00:00
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,
}
}