2026-06-25 08:20:03 +00:00
|
|
|
|
package style8d
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var dimensionLabels = map[string]string{
|
|
|
|
|
|
"d1Tone": "D1 語氣人格",
|
|
|
|
|
|
"d2Structure": "D2 結構模板",
|
|
|
|
|
|
"d3Interaction": "D3 互動方式",
|
|
|
|
|
|
"d4Topics": "D4 主題分布",
|
|
|
|
|
|
"d5Rhythm": "D5 發文節奏",
|
|
|
|
|
|
"d6Visual": "D6 視覺語法",
|
|
|
|
|
|
"d7Conversion": "D7 轉換方式",
|
|
|
|
|
|
"d8Risk": "D8 風險紅線",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var dimensionOrder = []string{
|
|
|
|
|
|
"d1Tone", "d2Structure", "d3Interaction", "d4Topics",
|
|
|
|
|
|
"d5Rhythm", "d6Visual", "d7Conversion", "d8Risk",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ParseStoredProfile decodes the persona.style_profile JSON blob.
|
|
|
|
|
|
func ParseStoredProfile(raw string) (*StoredProfile, bool) {
|
|
|
|
|
|
raw = strings.TrimSpace(raw)
|
|
|
|
|
|
if raw == "" {
|
|
|
|
|
|
return nil, false
|
|
|
|
|
|
}
|
|
|
|
|
|
var profile StoredProfile
|
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &profile); err != nil {
|
|
|
|
|
|
return nil, false
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(profile.Analysis) == 0 && strings.TrimSpace(profile.PersonaDraft) == "" {
|
|
|
|
|
|
return nil, false
|
|
|
|
|
|
}
|
|
|
|
|
|
return &profile, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HasReady8D returns true when 8D analysis exists and can drive copy generation.
|
|
|
|
|
|
func HasReady8D(personaText, styleProfileJSON string) bool {
|
|
|
|
|
|
if profile, ok := ParseStoredProfile(styleProfileJSON); ok {
|
|
|
|
|
|
if strings.TrimSpace(profile.PersonaDraft) != "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, key := range dimensionOrder {
|
|
|
|
|
|
if summary := strings.TrimSpace(profile.Analysis[key].Summary); summary != "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(personaText) != "" && strings.TrimSpace(styleProfileJSON) != ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BuildStyle8DPromptBlock formats D1–D8 summaries for LLM prompts.
|
|
|
|
|
|
func BuildStyle8DPromptBlock(styleProfileJSON string) string {
|
|
|
|
|
|
profile, ok := ParseStoredProfile(styleProfileJSON)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
lines := make([]string, 0, len(dimensionOrder))
|
|
|
|
|
|
for _, key := range dimensionOrder {
|
|
|
|
|
|
summary := strings.TrimSpace(profile.Analysis[key].Summary)
|
|
|
|
|
|
if summary == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
label := dimensionLabels[key]
|
|
|
|
|
|
if label == "" {
|
|
|
|
|
|
label = key
|
|
|
|
|
|
}
|
|
|
|
|
|
lines = append(lines, label+":"+summary)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(lines) == 0 {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
|
b.WriteString("【8D 風格策略】\n產文必須遵守:\n")
|
|
|
|
|
|
b.WriteString(strings.Join(lines, "\n"))
|
|
|
|
|
|
return b.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ResolvePersonaBlock picks the best persona voice block for copy generation.
|
|
|
|
|
|
// Priority: explicit persona field → 8D personaDraft → brief fallback; always append 8D strategy when present.
|
|
|
|
|
|
func ResolvePersonaBlock(personaText, styleProfileJSON, brief string) string {
|
|
|
|
|
|
var parts []string
|
|
|
|
|
|
|
|
|
|
|
|
voice := strings.TrimSpace(personaText)
|
|
|
|
|
|
if voice == "" {
|
|
|
|
|
|
if profile, ok := ParseStoredProfile(styleProfileJSON); ok {
|
|
|
|
|
|
voice = strings.TrimSpace(profile.PersonaDraft)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if voice == "" {
|
|
|
|
|
|
voice = strings.TrimSpace(brief)
|
|
|
|
|
|
}
|
|
|
|
|
|
if voice != "" {
|
|
|
|
|
|
parts = append(parts, "【人設語氣】\n"+voice)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if block := BuildStyle8DPromptBlock(styleProfileJSON); block != "" {
|
|
|
|
|
|
parts = append(parts, block)
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
2026-06-25 09:34:28 +00:00
|
|
|
|
}
|