2026-07-01 08:42:51 +00:00
|
|
|
|
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 各 2~5 點字串陣列。`)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-03 14:42:19 +00:00
|
|
|
|
}
|