package matrix import ( "encoding/json" "fmt" "regexp" "strings" libprompt "haixun-backend/internal/library/prompt" "haixun-backend/internal/library/threadspost" ) type Row struct { SortOrder int `json:"sort_order"` SearchTag string `json:"search_tag"` Angle string `json:"angle"` Hook string `json:"hook"` Text string `json:"text"` ReferenceNotes string `json:"reference_notes"` SourcePermalinks []string `json:"source_permalinks"` Rationale string `json:"rationale"` } type GenerateResult struct { Rows []Row `json:"rows"` } type MaterialPost struct { SearchTag string Author string Text string Permalink string Priority string } type GenerateInput struct { Persona string TopicLabel string AudienceBrief string ProductBrief string Posts []MaterialPost Count int } var codeFenceRE = regexp.MustCompile(`(?s)^` + "```(?:json)?\\s*(.*?)\\s*" + "```$") func BuildUserPrompt(in GenerateInput) (string, error) { count := in.Count if count <= 0 { count = 5 } personaBlock := "" if strings.TrimSpace(in.Persona) != "" { personaBlock = "人設與語氣:\n" + strings.TrimSpace(in.Persona) + "\n" } audience := strings.TrimSpace(in.AudienceBrief) if audience == "" { audience = "(未指定)" } product := strings.TrimSpace(in.ProductBrief) if product == "" { product = "(尚未填寫)" } topic := strings.TrimSpace(in.TopicLabel) if topic == "" { topic = "未指定" } return libprompt.MatrixPlacementUser(map[string]string{ "persona_block": personaBlock, "topic_label": topic, "audience_line": audience, "product_brief": product, "post_count": fmt.Sprintf("%d", len(in.Posts)), "materials_block": buildMaterialsBlock(in.Posts), "count": fmt.Sprintf("%d", count), }) } func buildMaterialsBlock(posts []MaterialPost) string { if len(posts) == 0 { return "(無素材)" } lines := make([]string, 0, len(posts)) for i, post := range posts { lines = append(lines, fmt.Sprintf( "%d. [%s/%s] @%s\n%s\n連結:%s", i+1, strings.TrimSpace(post.Priority), strings.TrimSpace(post.SearchTag), strings.TrimSpace(post.Author), strings.TrimSpace(post.Text), strings.TrimSpace(post.Permalink), )) } return strings.Join(lines, "\n\n") } func ParseGenerateOutput(raw string) (GenerateResult, error) { payload, err := extractJSONObject(raw) if err != nil { return GenerateResult{}, err } var out GenerateResult if err := json.Unmarshal(payload, &out); err != nil { return GenerateResult{}, fmt.Errorf("parse matrix json: %w", err) } if len(out.Rows) == 0 { return GenerateResult{}, fmt.Errorf("matrix rows missing") } for i := range out.Rows { out.Rows[i].Text = trimText(out.Rows[i].Text) if out.Rows[i].Text == "" { return GenerateResult{}, fmt.Errorf("matrix row %d empty", i+1) } if out.Rows[i].SortOrder <= 0 { out.Rows[i].SortOrder = i + 1 } } return out, nil } func trimText(text string) string { text = strings.TrimSpace(text) runes := []rune(text) if len(runes) > threadspost.MaxPublishRunes { return string(runes[:threadspost.MaxPublishRunes]) } return text } func extractJSONObject(raw string) ([]byte, error) { raw = strings.TrimSpace(raw) if m := codeFenceRE.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("matrix output missing json object") } return []byte(raw[start : end+1]), nil }