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