thread-master/backend/internal/library/ownpost/post_formula.go

162 lines
5.5 KiB
Go
Raw 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 ownpost
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
domai "haixun-backend/internal/model/ai/domain/usecase"
)
var formulaFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
type PostFormulaInput struct {
AccountName string
PersonaBlock string
PostText string
TopicTag string
MediaType string
LikeCount int
ReplyCount int
Views int
RepostCount int
QuoteCount int
Shares int
Timestamp string
}
type PostFormulaReview struct {
Summary string `json:"summary"`
Wins []string `json:"wins"`
Improvements []string `json:"improvements"`
Formula string `json:"formula"`
PostTemplate string `json:"post_template"`
HookPattern string `json:"hook_pattern"`
Structure string `json:"structure"`
ReplicationTips []string `json:"replication_tips"`
Avoid []string `json:"avoid"`
}
func BuildPostFormulaPrompt(in PostFormulaInput) (system string, user string) {
personaBlock := strings.TrimSpace(in.PersonaBlock)
if personaBlock == "" {
personaBlock = "(未指定人設,請依貼文本身語氣分析)"
}
postText := strings.TrimSpace(in.PostText)
if postText == "" {
postText = "(貼文內容未提供)"
}
system = strings.TrimSpace(`
你是 Threads 內容策略教練。任務是針對「已發布的單篇貼文」做誠實覆盤:好的要提炼成可複製公式,壞的要指出問題並給具體改善做法。不要重寫整篇貼文。
規則:
- 用台灣繁體中文。
- 必須同時寫優點與缺點;依成效數據判斷,成效普通或偏差時更要直言問題。
- 分析要具體、可執行,禁止空泛稱讚或空泛批評。
- formula把「做對的部分」整理成步驟化公式Hook→情境→轉折→觀點→收尾/互動)。
- post_template依 formula 產出可複製的發文骨架,用 [括號] 標示可替換欄位,讓使用者下次填空就能產文。
- improvements每點格式固定為「問題改善…」改善必須是可立刻執行的動作改哪一句、加什麼、刪什麼、換什麼角度
- 只輸出一個 JSON 物件,不要 markdown欄位
summary, wins, improvements, formula, post_template, hook_pattern, structure, replication_tips, avoid
- wins / improvements / replication_tips / avoid 各 25 點字串陣列。`)
var b strings.Builder
b.WriteString("【1. 人設】\n")
b.WriteString(personaBlock)
b.WriteString("\n\n帳號")
b.WriteString(strings.TrimSpace(in.AccountName))
b.WriteString("\n\n【2. 貼文內容】\n")
b.WriteString(postText)
if tag := strings.TrimSpace(in.TopicTag); tag != "" {
b.WriteString("\n話題標籤#")
b.WriteString(tag)
}
if mt := strings.TrimSpace(in.MediaType); mt != "" {
b.WriteString("\n媒體類型")
b.WriteString(mt)
}
if ts := strings.TrimSpace(in.Timestamp); ts != "" {
b.WriteString("\n發布時間")
b.WriteString(ts)
}
b.WriteString("\n\n【3. 成效數據】\n")
b.WriteString(fmt.Sprintf("讚 %d回覆 %d瀏覽 %d轉發 %d引用 %d分享 %d",
in.LikeCount, in.ReplyCount, in.Views, in.RepostCount, in.QuoteCount, in.Shares))
b.WriteString("\n\n請誠實覆盤做對了什麼、哪裡不好、怎麼改並把做對的部分整理成下次可複製的公式與發文模板 JSON。")
user = b.String()
return system, user
}
func GeneratePostFormula(ctx context.Context, ai domai.UseCase, req domai.GenerateRequest, in PostFormulaInput) (*PostFormulaReview, error) {
system, user := BuildPostFormulaPrompt(in)
temp := 0.65
req.System = system
req.Messages = []domai.Message{{Role: "user", Content: user}}
req.Temperature = &temp
out, err := ai.GenerateText(ctx, req)
if err != nil {
return nil, err
}
review, err := ParsePostFormulaOutput(out.Text)
if err != nil {
return nil, err
}
return review, nil
}
func ParsePostFormulaOutput(raw string) (*PostFormulaReview, error) {
payload, err := extractFormulaJSONObject(raw)
if err != nil {
return nil, err
}
var review PostFormulaReview
if err := json.Unmarshal(payload, &review); err != nil {
return nil, fmt.Errorf("parse post formula json: %w", err)
}
review.Summary = strings.TrimSpace(review.Summary)
review.Formula = strings.TrimSpace(review.Formula)
review.PostTemplate = strings.TrimSpace(review.PostTemplate)
review.HookPattern = strings.TrimSpace(review.HookPattern)
review.Structure = strings.TrimSpace(review.Structure)
review.Wins = trimStringList(review.Wins)
review.Improvements = trimStringList(review.Improvements)
review.ReplicationTips = trimStringList(review.ReplicationTips)
review.Avoid = trimStringList(review.Avoid)
if review.Summary == "" && review.Formula == "" && review.PostTemplate == "" {
return nil, fmt.Errorf("AI 未回傳有效覆盤內容")
}
return &review, nil
}
func trimStringList(items []string) []string {
if len(items) == 0 {
return nil
}
out := make([]string, 0, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item != "" {
out = append(out, item)
}
}
if len(out) == 0 {
return nil
}
return out
}
func extractFormulaJSONObject(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := formulaFenceRE.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("formula output missing json object")
}
return []byte(raw[start : end+1]), nil
}