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

121 lines
3.6 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package viral
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"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
}
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 爆款複製策略師根據參考爆文為使用者撰寫同結構同節奏但完全原創的貼文
規則
- 複製的是爆款公式hook 手法情緒節奏不是抄襲原文
- 文筆必須像創作者本人套用其 8D 風格策略
- text 主文 500 Threads API 硬上限 #話題標籤
- 爆款互動最佳 80220 12 行強 hook口語精簡超過 300 字互動通常下降
- 只回傳一個 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")
}
if style := strings.TrimSpace(input.StyleProfile); style != "" {
b.WriteString("\n8D 風格策略:\n")
b.WriteString(style)
b.WriteString("\n")
}
author := strings.TrimSpace(input.AuthorName)
if author == "" {
author = "匿名"
}
if analysis := strings.TrimSpace(input.StructureAnalysis); analysis != "" {
b.WriteString("\n爆款結構分析仿寫時套用\n")
b.WriteString(analysis)
b.WriteString("\n")
}
b.WriteString("\n原文參考@")
b.WriteString(author)
b.WriteString(",只學結構不抄內容):\n")
b.WriteString(strings.TrimSpace(input.OriginalText))
b.WriteString("\n\n請產出一篇可發布的複製版貼文 JSON。")
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 = trimText(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 trimText(text string) string {
text = strings.TrimSpace(text)
runes := []rune(text)
if len(runes) > threadspost.MaxPublishRunes {
return string(runes[:threadspost.MaxPublishRunes])
}
return text
}
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
}