package viral import ( "encoding/json" "fmt" "regexp" "strings" "haixun-backend/internal/library/copyvoice" "haixun-backend/internal/library/threadspost" ) var codeFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```") type ReplicateInput struct { TopicLabel string TopicBrief string Persona string StyleProfile string OriginalText string AuthorName string StructureAnalysis string NarrativeSeed string } type ReplicateResult struct { Angle string `json:"angle"` Hook string `json:"hook"` Text string `json:"text"` Rationale string `json:"rationale"` StructureNotes string `json:"structureNotes"` } func BuildSystemPrompt() string { return strings.TrimSpace(`你是 Threads / Instagram 創作者。讀完參考爆文後,用指定人設的語言指紋寫一篇全新貼文。 ` + copyvoice.SystemRules() + ` 輸出: - text 主文 ≤ 500 字(Threads API 硬上限,含 #話題標籤) - 參考原文只學情緒力度、資訊密度與節奏;最後必須換成人設會講出口的話 - 篇幅、行數、標點與換行優先遵守人設語言指紋;沒有指紋時才貼近參考原文 - text 排版要像真人手機發文:可使用自然標點、空行、列點與少量 emoji;不要完全無標點,也不要每句硬換行,除非人設樣本就是這樣 - 只回傳一個 JSON 物件,欄位:angle, hook, text, rationale, structureNotes`) } func BuildUserPrompt(input ReplicateInput) string { var b strings.Builder b.WriteString("主題:") b.WriteString(strings.TrimSpace(input.TopicLabel)) b.WriteString("\n") if brief := strings.TrimSpace(input.TopicBrief); brief != "" { b.WriteString("Brief:") b.WriteString(brief) b.WriteString("\n") } if persona := strings.TrimSpace(input.Persona); persona != "" { b.WriteString("\n人設定位、語言指紋與禁忌(最高優先):\n") b.WriteString(persona) b.WriteString("\n") b.WriteString(copyvoice.PersonaCalibrationNote()) b.WriteString("\n") } if style := strings.TrimSpace(input.StyleProfile); style != "" { b.WriteString("\n8D 風格策略:\n") b.WriteString(style) b.WriteString("\n") } if analysis := strings.TrimSpace(input.StructureAnalysis); analysis != "" { b.WriteString("\n敘事分析(只學故事感,不要套句型或段落公式):\n") b.WriteString(analysis) b.WriteString("\n") } author := strings.TrimSpace(input.AuthorName) if author == "" { author = "匿名" } original := strings.TrimSpace(input.OriginalText) if note := threadspost.MimicLengthGuidance(original); note != "" { b.WriteString("\n") b.WriteString(note) b.WriteString("\n") } b.WriteString("\n原文參考(@") b.WriteString(author) b.WriteString(",只感受情緒、敘事與篇幅,禁止抄句型):\n") b.WriteString(original) b.WriteString("\n\n本次敘事切入:") b.WriteString(copyvoice.NarrativeLens(input.NarrativeSeed)) b.WriteString("\n\n請產出一篇可發布的全新貼文 JSON。hook 欄位只放內部分析,不要把模板句型寫進 text。") return b.String() } func ParseReplicateOutput(raw string) (ReplicateResult, error) { payload, err := extractJSONObject(raw) if err != nil { return ReplicateResult{}, err } var out ReplicateResult if err := json.Unmarshal(payload, &out); err != nil { return ReplicateResult{}, fmt.Errorf("parse viral replica json: %w", err) } out.Text = threadspost.FormatDraftText(out.Text) if out.Text == "" { return ReplicateResult{}, fmt.Errorf("replica text missing") } out.Angle = strings.TrimSpace(out.Angle) out.Hook = strings.TrimSpace(out.Hook) out.Rationale = strings.TrimSpace(out.Rationale) out.StructureNotes = strings.TrimSpace(out.StructureNotes) return out, nil } func extractJSONObject(raw string) ([]byte, error) { raw = strings.TrimSpace(raw) if m := codeFenceRE.FindStringSubmatch(raw); len(m) == 2 { raw = strings.TrimSpace(m[1]) } start := strings.Index(raw, "{") end := strings.LastIndex(raw, "}") if start < 0 || end <= start { return nil, fmt.Errorf("replica output missing json object") } return []byte(raw[start : end+1]), nil }