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

179 lines
6.0 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"
)
type MissionInspireInput struct {
PersonaDisplayName string
PersonaBrief string
PersonaBlock string
StyleBenchmark string
PersonaAudience string
PersonaContentGoal string
PersonaQuestions []string
PersonaPillars []string
RecentMissionLabels []string
RecentSeedQueries []string
TrendSnippets []MissionInspireTrendSnippet
WebSearchProvider string
LLMOnly bool
}
type MissionInspireTrendSnippet struct {
Query string
Title string
Snippet string
URL string
}
type MissionInspireOutput struct {
Label string
SeedQuery string
Brief string
TrendReason string
TrendKeywords []string
}
func BuildMissionInspireSystemPrompt() string {
return strings.TrimSpace(`你是 Threads 拷貝忍者的「靈感骰子」顧問。根據近期網路熱搜、Google Trends 類訊號與創作者人設,產出一組**全新**拷貝任務草稿。
規則:
1. 若【近期趨勢訊號】有內容,從中挑一個適合在 Threads 海巡的方向;不要編造不存在的時事
2. 若趨勢訊號為空(未連線網路搜尋),**必須**改依人設、受眾痛點與常見 Threads 討論型態推測「近期可能被搜尋」的話題,並在 trendReason 說明推測理由(不要假裝有外部熱搜來源)
3. label任務名稱618 字,像企劃案標題,不要標點堆疊
4. seedQuery種子關鍵字近期熱詞26 個詞用頓號或空格分隔,適合當 Threads 搜尋起點
5. brief這次想找什麼24 句,說明要海巡哪類爆款、語氣或格式偏好
6. trendReason12 句,為何選這個趨勢(可引用訊號來源大意,不要貼 URL
7. trendKeywords36 個相關熱詞(字串陣列)
8. 避開【近期已做過的任務】相同題材
9. 繁體中文;只回傳 JSON
{"label":"","seedQuery":"","brief":"","trendReason":"","trendKeywords":[]}`)
}
func BuildMissionInspireUserPrompt(in MissionInspireInput) string {
var b strings.Builder
if name := strings.TrimSpace(in.PersonaDisplayName); name != "" {
b.WriteString("【人設名稱】")
b.WriteString(name)
b.WriteString("\n")
}
if p := strings.TrimSpace(in.PersonaBlock); p != "" {
b.WriteString("【人設摘要】\n")
b.WriteString(p)
b.WriteString("\n")
}
if bench := strings.TrimSpace(in.StyleBenchmark); bench != "" {
b.WriteString("【對標帳號】@")
b.WriteString(strings.TrimPrefix(bench, "@"))
b.WriteString("\n")
}
if aud := strings.TrimSpace(in.PersonaAudience); aud != "" {
b.WriteString("【受眾方向】")
b.WriteString(aud)
b.WriteString("\n")
}
if goal := strings.TrimSpace(in.PersonaContentGoal); goal != "" {
b.WriteString("【內容目標】")
b.WriteString(goal)
b.WriteString("\n")
}
if len(in.PersonaPillars) > 0 {
b.WriteString("【內容支柱】")
b.WriteString(strings.Join(in.PersonaPillars, "、"))
b.WriteString("\n")
}
if len(in.RecentMissionLabels) > 0 || len(in.RecentSeedQueries) > 0 {
b.WriteString("【近期已做過的任務(請避開)】\n")
for _, label := range in.RecentMissionLabels {
if label = strings.TrimSpace(label); label != "" {
b.WriteString("- ")
b.WriteString(label)
b.WriteString("\n")
}
}
for _, seed := range in.RecentSeedQueries {
if seed = strings.TrimSpace(seed); seed != "" {
b.WriteString("- 種子:")
b.WriteString(seed)
b.WriteString("\n")
}
}
}
if in.LLMOnly {
b.WriteString("【模式】未設定 Web Search API key請純依人設與受眾推測靈感勿假裝有 Google 熱搜)\n")
} else if provider := strings.TrimSpace(in.WebSearchProvider); provider != "" {
b.WriteString("【趨勢來源】")
b.WriteString(provider)
b.WriteString(" 網路搜尋\n")
}
b.WriteString("【近期趨勢訊號】\n")
if len(in.TrendSnippets) == 0 {
b.WriteString("(無外部趨勢結果,請改用人設推測近期 Threads 可能熱議方向)\n")
} else {
for i, item := range in.TrendSnippets {
b.WriteString(fmt.Sprintf("[%d] 查詢:%s\n", i+1, strings.TrimSpace(item.Query)))
if title := strings.TrimSpace(item.Title); title != "" {
b.WriteString("標題:")
b.WriteString(title)
b.WriteString("\n")
}
if snippet := strings.TrimSpace(item.Snippet); snippet != "" {
b.WriteString(snippet)
b.WriteString("\n")
}
b.WriteString("\n")
}
}
b.WriteString("請產出拷貝任務靈感 JSON。")
return b.String()
}
func InspireTrendSearchQueries(personaBrief, styleBenchmark string) []string {
queries := []string{
"Google Trends 台灣 今日 熱門搜尋 關鍵字",
"Threads 台灣 熱門 話題 最近 一週",
"近期 網路 熱搜 關鍵字 台灣",
}
context := strings.TrimSpace(personaBrief + " " + styleBenchmark)
if context != "" {
trimmed := context
if len([]rune(trimmed)) > 24 {
trimmed = string([]rune(trimmed)[:24])
}
queries = append(queries, trimmed+" 熱門 話題 最近")
}
return queries
}
func ParseMissionInspireOutput(raw string) (MissionInspireOutput, error) {
payload, err := extractCopyMapJSON(raw)
if err != nil {
return MissionInspireOutput{}, err
}
var root map[string]json.RawMessage
if err := json.Unmarshal(payload, &root); err != nil {
return MissionInspireOutput{}, err
}
out := MissionInspireOutput{
Label: firstJSONString(root, "label", "title", "mission_label"),
SeedQuery: firstJSONString(root, "seedQuery", "seed_query", "seed", "keywords"),
Brief: firstJSONString(root, "brief", "description", "goal"),
TrendReason: firstJSONString(root, "trendReason", "trend_reason", "reason"),
}
if out.Label == "" || out.SeedQuery == "" || out.Brief == "" {
return MissionInspireOutput{}, fmt.Errorf("missing label, seedQuery, or brief")
}
if rawKW, ok := root["trendKeywords"]; ok {
out.TrendKeywords = parseFlexibleStringList(rawKW)
}
if len(out.TrendKeywords) == 0 {
if rawKW, ok := root["trend_keywords"]; ok {
out.TrendKeywords = parseFlexibleStringList(rawKW)
}
}
return out, nil
}