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

121 lines
3.6 KiB
Go
Raw Permalink 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"
"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
}