package formula import ( "context" "fmt" "regexp" "strings" libllmjson "haixun-backend/internal/library/llmjson" 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(` 你是 Threads / Instagram 貼文結構分析師。任務是拆解「外部貼文」哪些地方可借鏡,再說明如何用指定人設的語言指紋重新說一次;不要產生會讓文案機械的填空模板。 規則: - 用台灣繁體中文。 - formula:整理可借鏡的敘事/資訊推進方式,但要提醒不可照搬句型。 - post_template:不要寫填空骨架;改寫成「此人設可採用的鬆結構與注意事項」。 - replication_tips:重點放在如何換成人設語言,包括段落節奏、標點換行、知識轉譯與 CTA。 - 只輸出一個 JSON 物件,不要 markdown,欄位: summary, wins, improvements, formula, post_template, hook_pattern, structure, replication_tips, avoid - formula / structure / hook_pattern 必須是字串,不要用陣列或巢狀物件 - 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 } var flex map[string]any if err := libllmjson.Unmarshal(payload, &flex); err != nil { return nil, fmt.Errorf("parse pasted formula json: %w", err) } review := pastedAnalysisFromMap(flex) if review.Summary == "" && review.Formula == "" && review.PostTemplate == "" { return nil, fmt.Errorf("AI 未回傳有效公式內容") } return review, nil } 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 }