2026-06-25 08:20:03 +00:00
|
|
|
|
package viral
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type MissionInspireInput struct {
|
2026-06-25 09:34:28 +00:00
|
|
|
|
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
|
2026-06-25 08:20:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type MissionInspireTrendSnippet struct {
|
|
|
|
|
|
Query string
|
|
|
|
|
|
Title string
|
|
|
|
|
|
Snippet string
|
|
|
|
|
|
URL string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type MissionInspireOutput struct {
|
2026-06-25 09:34:28 +00:00
|
|
|
|
Label string
|
|
|
|
|
|
SeedQuery string
|
|
|
|
|
|
Brief string
|
|
|
|
|
|
TrendReason string
|
|
|
|
|
|
TrendKeywords []string
|
2026-06-25 08:20:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func BuildMissionInspireSystemPrompt() string {
|
|
|
|
|
|
return strings.TrimSpace(`你是 Threads 拷貝忍者的「靈感骰子」顧問。根據近期網路熱搜、Google Trends 類訊號與創作者人設,產出一組**全新**拷貝任務草稿。
|
|
|
|
|
|
|
|
|
|
|
|
規則:
|
|
|
|
|
|
1. 若【近期趨勢訊號】有內容,從中挑一個適合在 Threads 海巡的方向;不要編造不存在的時事
|
|
|
|
|
|
2. 若趨勢訊號為空(未連線網路搜尋),**必須**改依人設、受眾痛點與常見 Threads 討論型態推測「近期可能被搜尋」的話題,並在 trendReason 說明推測理由(不要假裝有外部熱搜來源)
|
|
|
|
|
|
3. label:任務名稱,6~18 字,像企劃案標題,不要標點堆疊
|
|
|
|
|
|
4. seedQuery:種子關鍵字/近期熱詞,2~6 個詞用頓號或空格分隔,適合當 Threads 搜尋起點
|
|
|
|
|
|
5. brief:這次想找什麼,2~4 句,說明要海巡哪類爆款、語氣或格式偏好
|
|
|
|
|
|
6. trendReason:1~2 句,為何選這個趨勢(可引用訊號來源大意,不要貼 URL)
|
|
|
|
|
|
7. trendKeywords:3~6 個相關熱詞(字串陣列)
|
|
|
|
|
|
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
|
2026-06-25 09:34:28 +00:00
|
|
|
|
}
|