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

143 lines
3.5 KiB
Go
Raw Permalink Normal View History

2026-06-26 08:37:04 +00:00
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
}