2026-07-01 08:42:51 +00:00
|
|
|
|
package formula
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2026-07-02 14:52:41 +00:00
|
|
|
|
libllmjson "haixun-backend/internal/library/llmjson"
|
2026-07-01 08:42:51 +00:00
|
|
|
|
libownpost "haixun-backend/internal/library/ownpost"
|
|
|
|
|
|
domai "haixun-backend/internal/model/ai/domain/usecase"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var pastedFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
|
|
|
|
|
|
|
|
|
|
|
|
type AnalyzePastedInput struct {
|
|
|
|
|
|
PostText string
|
|
|
|
|
|
AuthorName string
|
|
|
|
|
|
PersonaBlock string
|
|
|
|
|
|
LikeCount int
|
|
|
|
|
|
ReplyCount int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PastedAnalysis 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 BuildAnalyzePastedPrompt(in AnalyzePastedInput) (system string, user string) {
|
|
|
|
|
|
personaBlock := strings.TrimSpace(in.PersonaBlock)
|
|
|
|
|
|
if personaBlock == "" {
|
|
|
|
|
|
personaBlock = "(未指定人設,請依貼文本身語氣分析)"
|
|
|
|
|
|
}
|
|
|
|
|
|
postText := strings.TrimSpace(in.PostText)
|
|
|
|
|
|
if postText == "" {
|
|
|
|
|
|
postText = "(貼文內容未提供)"
|
|
|
|
|
|
}
|
|
|
|
|
|
author := strings.TrimSpace(in.AuthorName)
|
|
|
|
|
|
if author == "" {
|
|
|
|
|
|
author = "匿名"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
system = strings.TrimSpace(`
|
2026-07-02 14:52:41 +00:00
|
|
|
|
你是 Threads / Instagram 貼文結構分析師。任務是拆解「外部貼文」哪些地方可借鏡,再說明如何用指定人設的語言指紋重新說一次;不要產生會讓文案機械的填空模板。
|
2026-07-01 08:42:51 +00:00
|
|
|
|
|
|
|
|
|
|
規則:
|
|
|
|
|
|
- 用台灣繁體中文。
|
2026-07-02 14:52:41 +00:00
|
|
|
|
- formula:整理可借鏡的敘事/資訊推進方式,但要提醒不可照搬句型。
|
|
|
|
|
|
- post_template:不要寫填空骨架;改寫成「此人設可採用的鬆結構與注意事項」。
|
|
|
|
|
|
- replication_tips:重點放在如何換成人設語言,包括段落節奏、標點換行、知識轉譯與 CTA。
|
2026-07-01 08:42:51 +00:00
|
|
|
|
- 只輸出一個 JSON 物件,不要 markdown,欄位:
|
|
|
|
|
|
summary, wins, improvements, formula, post_template, hook_pattern, structure, replication_tips, avoid
|
2026-07-02 14:52:41 +00:00
|
|
|
|
- formula / structure / hook_pattern 必須是字串,不要用陣列或巢狀物件
|
2026-07-01 08:42:51 +00:00
|
|
|
|
- wins / improvements / replication_tips / avoid 各 2~5 點字串陣列。`)
|
|
|
|
|
|
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
|
b.WriteString("【人設脈絡】\n")
|
|
|
|
|
|
b.WriteString(personaBlock)
|
|
|
|
|
|
b.WriteString("\n\n【貼文】\n作者:")
|
|
|
|
|
|
b.WriteString(author)
|
|
|
|
|
|
if in.LikeCount > 0 || in.ReplyCount > 0 {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf(" · %d 讚 · %d 留言", in.LikeCount, in.ReplyCount))
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
b.WriteString(postText)
|
|
|
|
|
|
b.WriteString("\n\n請分析爆款結構並輸出 JSON。")
|
|
|
|
|
|
user = b.String()
|
|
|
|
|
|
return system, user
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func AnalyzePasted(ctx context.Context, ai domai.UseCase, req domai.GenerateRequest, in AnalyzePastedInput) (*PastedAnalysis, error) {
|
|
|
|
|
|
system, user := BuildAnalyzePastedPrompt(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
|
|
|
|
|
|
}
|
|
|
|
|
|
return ParsePastedAnalysis(out.Text)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ParsePastedAnalysis(raw string) (*PastedAnalysis, error) {
|
|
|
|
|
|
payload, err := extractPastedJSON(raw)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-07-02 14:52:41 +00:00
|
|
|
|
var flex map[string]any
|
|
|
|
|
|
if err := libllmjson.Unmarshal(payload, &flex); err != nil {
|
2026-07-01 08:42:51 +00:00
|
|
|
|
return nil, fmt.Errorf("parse pasted formula json: %w", err)
|
|
|
|
|
|
}
|
2026-07-02 14:52:41 +00:00
|
|
|
|
review := pastedAnalysisFromMap(flex)
|
2026-07-01 08:42:51 +00:00
|
|
|
|
if review.Summary == "" && review.Formula == "" && review.PostTemplate == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("AI 未回傳有效公式內容")
|
|
|
|
|
|
}
|
2026-07-02 14:52:41 +00:00
|
|
|
|
return review, nil
|
2026-07-01 08:42:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func FromOwnPostReview(review *libownpost.PostFormulaReview) *PastedAnalysis {
|
|
|
|
|
|
if review == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return &PastedAnalysis{
|
|
|
|
|
|
Summary: review.Summary,
|
|
|
|
|
|
Wins: review.Wins,
|
|
|
|
|
|
Improvements: review.Improvements,
|
|
|
|
|
|
Formula: review.Formula,
|
|
|
|
|
|
PostTemplate: review.PostTemplate,
|
|
|
|
|
|
HookPattern: review.HookPattern,
|
|
|
|
|
|
Structure: review.Structure,
|
|
|
|
|
|
ReplicationTips: review.ReplicationTips,
|
|
|
|
|
|
Avoid: review.Avoid,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func FromViralAnalysis(hook, structure, emotion, strategy string, takeaways []string) *PastedAnalysis {
|
|
|
|
|
|
summary := strings.TrimSpace(emotion)
|
|
|
|
|
|
if summary == "" {
|
|
|
|
|
|
summary = "爆款結構分析"
|
|
|
|
|
|
}
|
|
|
|
|
|
formula := strings.TrimSpace(strategy)
|
|
|
|
|
|
if structure != "" {
|
|
|
|
|
|
if formula != "" {
|
|
|
|
|
|
formula = structure + " → " + formula
|
|
|
|
|
|
} else {
|
|
|
|
|
|
formula = structure
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
template := ""
|
|
|
|
|
|
if hook != "" && structure != "" {
|
|
|
|
|
|
template = "[" + hook + "] → [" + structure + "] → [你的觀點] → [互動收尾]"
|
|
|
|
|
|
}
|
|
|
|
|
|
return &PastedAnalysis{
|
|
|
|
|
|
Summary: summary,
|
|
|
|
|
|
Wins: takeaways,
|
|
|
|
|
|
Formula: formula,
|
|
|
|
|
|
PostTemplate: template,
|
|
|
|
|
|
HookPattern: hook,
|
|
|
|
|
|
Structure: structure,
|
|
|
|
|
|
ReplicationTips: takeaways,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func extractPastedJSON(raw string) ([]byte, error) {
|
|
|
|
|
|
raw = strings.TrimSpace(raw)
|
|
|
|
|
|
if m := pastedFenceRE.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-02 14:52:41 +00:00
|
|
|
|
}
|