thread-master/backend/internal/library/matrix/generate.go

143 lines
3.5 KiB
Go
Raw Permalink 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 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
}