110 lines
3.5 KiB
Go
110 lines
3.5 KiB
Go
|
|
package formula
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
domai "haixun-backend/internal/model/ai/domain/usecase"
|
|||
|
|
cfdom "haixun-backend/internal/model/content_formula/domain/usecase"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type GenerateInput struct {
|
|||
|
|
Topic string
|
|||
|
|
Brief string
|
|||
|
|
PersonaBlock string
|
|||
|
|
ResearchNotes string
|
|||
|
|
Formula cfdom.FormulaSummary
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type GeneratedDraft struct {
|
|||
|
|
Text string `json:"text"`
|
|||
|
|
Hook string `json:"hook"`
|
|||
|
|
Angle string `json:"angle"`
|
|||
|
|
Rationale string `json:"rationale"`
|
|||
|
|
StructureNotes string `json:"structure_notes"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func BuildGenerateSystemPrompt() string {
|
|||
|
|
return strings.TrimSpace(`你是 Threads 文案寫手。依「爆款公式」與「人設」為指定主題寫一篇新貼文。
|
|||
|
|
|
|||
|
|
規則:
|
|||
|
|
- 用台灣繁體中文,符合人設語氣。
|
|||
|
|
- 套用公式結構,但內容必須圍繞使用者主題,不可抄襲範例貼文。
|
|||
|
|
- 只回傳 JSON:text, hook, angle, rationale, structure_notes(繁體中文)。`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func BuildGenerateUserPrompt(in GenerateInput) string {
|
|||
|
|
var b strings.Builder
|
|||
|
|
b.WriteString("【主題】\n")
|
|||
|
|
b.WriteString(strings.TrimSpace(in.Topic))
|
|||
|
|
if brief := strings.TrimSpace(in.Brief); brief != "" {
|
|||
|
|
b.WriteString("\n補充:")
|
|||
|
|
b.WriteString(brief)
|
|||
|
|
}
|
|||
|
|
b.WriteString("\n\n【人設】\n")
|
|||
|
|
b.WriteString(strings.TrimSpace(in.PersonaBlock))
|
|||
|
|
b.WriteString("\n\n【公式】\n")
|
|||
|
|
b.WriteString(strings.TrimSpace(in.Formula.Formula))
|
|||
|
|
if tpl := strings.TrimSpace(in.Formula.PostTemplate); tpl != "" {
|
|||
|
|
b.WriteString("\n\n【發文模板】\n")
|
|||
|
|
b.WriteString(tpl)
|
|||
|
|
}
|
|||
|
|
if hook := strings.TrimSpace(in.Formula.HookPattern); hook != "" {
|
|||
|
|
b.WriteString("\n\n【Hook 模式】\n")
|
|||
|
|
b.WriteString(hook)
|
|||
|
|
}
|
|||
|
|
if structure := strings.TrimSpace(in.Formula.Structure); structure != "" {
|
|||
|
|
b.WriteString("\n\n【結構節奏】\n")
|
|||
|
|
b.WriteString(structure)
|
|||
|
|
}
|
|||
|
|
if len(in.Formula.ReplicationTips) > 0 {
|
|||
|
|
b.WriteString("\n\n【複製技巧】\n")
|
|||
|
|
b.WriteString(strings.Join(in.Formula.ReplicationTips, "\n"))
|
|||
|
|
}
|
|||
|
|
if len(in.Formula.Avoid) > 0 {
|
|||
|
|
b.WriteString("\n\n【避免】\n")
|
|||
|
|
b.WriteString(strings.Join(in.Formula.Avoid, "\n"))
|
|||
|
|
}
|
|||
|
|
if notes := strings.TrimSpace(in.ResearchNotes); notes != "" {
|
|||
|
|
b.WriteString("\n\n【周邊知識參考】\n")
|
|||
|
|
b.WriteString(notes)
|
|||
|
|
}
|
|||
|
|
if src := strings.TrimSpace(in.Formula.SourcePostText); src != "" {
|
|||
|
|
b.WriteString("\n\n【參考原文(僅學結構,勿抄內容)】\n")
|
|||
|
|
b.WriteString(src)
|
|||
|
|
}
|
|||
|
|
b.WriteString("\n\n請產出一篇可直接發布的 Threads 貼文 JSON。")
|
|||
|
|
return b.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GenerateDraft(ctx context.Context, ai domai.UseCase, req domai.GenerateRequest, in GenerateInput) (*GeneratedDraft, error) {
|
|||
|
|
temp := 0.75
|
|||
|
|
req.System = BuildGenerateSystemPrompt()
|
|||
|
|
req.Messages = []domai.Message{{Role: "user", Content: BuildGenerateUserPrompt(in)}}
|
|||
|
|
req.Temperature = &temp
|
|||
|
|
out, err := ai.GenerateText(ctx, req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return ParseGeneratedDraft(out.Text)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ParseGeneratedDraft(raw string) (*GeneratedDraft, error) {
|
|||
|
|
raw = strings.TrimSpace(raw)
|
|||
|
|
start := strings.Index(raw, "{")
|
|||
|
|
end := strings.LastIndex(raw, "}")
|
|||
|
|
if start < 0 || end <= start {
|
|||
|
|
return nil, fmt.Errorf("generate output missing json object")
|
|||
|
|
}
|
|||
|
|
var draft GeneratedDraft
|
|||
|
|
if err := json.Unmarshal([]byte(raw[start:end+1]), &draft); err != nil {
|
|||
|
|
return nil, fmt.Errorf("parse generated draft: %w", err)
|
|||
|
|
}
|
|||
|
|
draft.Text = strings.TrimSpace(draft.Text)
|
|||
|
|
if draft.Text == "" {
|
|||
|
|
return nil, fmt.Errorf("generated draft missing text")
|
|||
|
|
}
|
|||
|
|
return &draft, nil
|
|||
|
|
}
|