416 lines
17 KiB
Go
416 lines
17 KiB
Go
|
|
package usecase
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"archive/zip"
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"encoding/xml"
|
|||
|
|
"math"
|
|||
|
|
"path/filepath"
|
|||
|
|
"regexp"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"haixun-backend/internal/library/clock"
|
|||
|
|
app "haixun-backend/internal/library/errors"
|
|||
|
|
"haixun-backend/internal/library/errors/code"
|
|||
|
|
"haixun-backend/internal/model/content_ops/domain/entity"
|
|||
|
|
domrepo "haixun-backend/internal/model/content_ops/domain/repository"
|
|||
|
|
domusecase "haixun-backend/internal/model/content_ops/domain/usecase"
|
|||
|
|
|
|||
|
|
"github.com/google/uuid"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type useCase struct{ repo domrepo.Repository }
|
|||
|
|
|
|||
|
|
func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &useCase{repo: repo} }
|
|||
|
|
|
|||
|
|
func requireActor(tenantID, ownerUID, personaID string) error {
|
|||
|
|
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" || strings.TrimSpace(personaID) == "" {
|
|||
|
|
return app.For(code.Persona).InputMissingRequired("tenant_id, owner_uid, and persona_id are required")
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func clampScore(n int) int {
|
|||
|
|
if n < 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
if n > 100 {
|
|||
|
|
return 100
|
|||
|
|
}
|
|||
|
|
return n
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func finalScore(heat, fit, interaction, extend, risk int) float64 {
|
|||
|
|
score := float64(clampScore(heat))*0.25 + float64(clampScore(fit))*0.30 + float64(clampScore(interaction))*0.15 + float64(clampScore(extend))*0.15 + 70*0.10 - float64(clampScore(risk))*0.20
|
|||
|
|
return math.Round(score*10) / 10
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) CreateTopicCandidate(ctx context.Context, req domusecase.TopicCandidateInput) (*domusecase.TopicCandidateSummary, error) {
|
|||
|
|
if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
name := strings.TrimSpace(req.Name)
|
|||
|
|
if name == "" {
|
|||
|
|
return nil, app.For(code.Persona).InputMissingRequired("topic name is required")
|
|||
|
|
}
|
|||
|
|
now := clock.NowUnixNano()
|
|||
|
|
item := &entity.TopicCandidate{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, Name: name, Source: strings.TrimSpace(req.Source), Category: strings.TrimSpace(req.Category), SeedQuery: strings.TrimSpace(req.SeedQuery), TrendReason: strings.TrimSpace(req.TrendReason), TrendKeywords: req.TrendKeywords, HeatScore: clampScore(req.HeatScore), FitScore: clampScore(req.FitScore), InteractionScore: clampScore(req.InteractionScore), ExtendScore: clampScore(req.ExtendScore), RiskScore: clampScore(req.RiskScore), RecommendedMissions: req.RecommendedMissions, Status: "candidate", CreateAt: now, UpdateAt: now}
|
|||
|
|
item.FinalScore = finalScore(item.HeatScore, item.FitScore, item.InteractionScore, item.ExtendScore, item.RiskScore)
|
|||
|
|
if err := u.repo.CreateTopicCandidate(ctx, item); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := topicSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListTopicCandidates(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.TopicCandidateSummary, error) {
|
|||
|
|
items, err := u.repo.ListTopicCandidates(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.TopicCandidateSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, topicSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) CreateContentPlan(ctx context.Context, req domusecase.ContentPlanInput) (*domusecase.ContentPlanSummary, error) {
|
|||
|
|
if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if strings.TrimSpace(req.Topic) == "" || strings.TrimSpace(req.Mission) == "" {
|
|||
|
|
return nil, app.For(code.Persona).InputMissingRequired("topic and mission are required")
|
|||
|
|
}
|
|||
|
|
now := clock.NowUnixNano()
|
|||
|
|
item := &entity.ContentPlan{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, TopicCandidateID: strings.TrimSpace(req.TopicCandidateID), Topic: strings.TrimSpace(req.Topic), Mission: strings.TrimSpace(req.Mission), TargetAudience: strings.TrimSpace(req.TargetAudience), Angle: strings.TrimSpace(req.Angle), OpeningType: strings.TrimSpace(req.OpeningType), BodyType: strings.TrimSpace(req.BodyType), Emotion: strings.TrimSpace(req.Emotion), EndingType: strings.TrimSpace(req.EndingType), CtaType: strings.TrimSpace(req.CtaType), RiskLevel: normalizeRisk(req.RiskLevel), RequiresHumanReview: req.RequiresHumanReview || normalizeRisk(req.RiskLevel) != "low", Avoid: req.Avoid, SelectedKnowledge: req.SelectedKnowledge, Status: "planned", CreateAt: now, UpdateAt: now}
|
|||
|
|
if err := u.repo.CreateContentPlan(ctx, item); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := planSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) UpdateContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string, patch domusecase.ContentPlanPatch) (*domusecase.ContentPlanSummary, error) {
|
|||
|
|
m := map[string]any{"update_at": clock.NowUnixNano()}
|
|||
|
|
setString := func(key string, value *string) {
|
|||
|
|
if value != nil {
|
|||
|
|
m[key] = strings.TrimSpace(*value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
setString("topic", patch.Topic)
|
|||
|
|
setString("mission", patch.Mission)
|
|||
|
|
setString("target_audience", patch.TargetAudience)
|
|||
|
|
setString("angle", patch.Angle)
|
|||
|
|
setString("opening_type", patch.OpeningType)
|
|||
|
|
setString("body_type", patch.BodyType)
|
|||
|
|
setString("emotion", patch.Emotion)
|
|||
|
|
setString("ending_type", patch.EndingType)
|
|||
|
|
setString("cta_type", patch.CtaType)
|
|||
|
|
setString("status", patch.Status)
|
|||
|
|
if patch.RiskLevel != nil {
|
|||
|
|
m["risk_level"] = normalizeRisk(*patch.RiskLevel)
|
|||
|
|
}
|
|||
|
|
if patch.RequiresHumanReview != nil {
|
|||
|
|
m["requires_human_review"] = *patch.RequiresHumanReview
|
|||
|
|
}
|
|||
|
|
if patch.Avoid != nil {
|
|||
|
|
m["avoid"] = patch.Avoid
|
|||
|
|
}
|
|||
|
|
if patch.SelectedKnowledge != nil {
|
|||
|
|
m["selected_knowledge"] = patch.SelectedKnowledge
|
|||
|
|
}
|
|||
|
|
item, err := u.repo.UpdateContentPlan(ctx, tenantID, ownerUID, personaID, id, m)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := planSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) GetContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string) (*domusecase.ContentPlanSummary, error) {
|
|||
|
|
item, err := u.repo.GetContentPlan(ctx, tenantID, ownerUID, personaID, id)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := planSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListContentPlans(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.ContentPlanSummary, error) {
|
|||
|
|
items, err := u.repo.ListContentPlans(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.ContentPlanSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, planSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) CreateFeedbackEvent(ctx context.Context, req domusecase.FeedbackEventInput) (*domusecase.FeedbackEventSummary, error) {
|
|||
|
|
if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
decision := strings.TrimSpace(req.Decision)
|
|||
|
|
if decision == "" {
|
|||
|
|
return nil, app.For(code.Persona).InputMissingRequired("decision is required")
|
|||
|
|
}
|
|||
|
|
item := &entity.FeedbackEvent{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, ContentPlanID: strings.TrimSpace(req.ContentPlanID), DraftID: strings.TrimSpace(req.DraftID), Decision: decision, Note: strings.TrimSpace(req.Note), Snapshot: strings.TrimSpace(req.Snapshot), CreateAt: clock.NowUnixNano()}
|
|||
|
|
if err := u.repo.CreateFeedbackEvent(ctx, item); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := feedbackSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListFeedbackEvents(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.FeedbackEventSummary, error) {
|
|||
|
|
items, err := u.repo.ListFeedbackEvents(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.FeedbackEventSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, feedbackSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) CreateKnowledgeSource(ctx context.Context, req domusecase.KnowledgeSourceInput) (*domusecase.KnowledgeSourceSummary, error) {
|
|||
|
|
if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
raw, err := decodeSourceContent(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, app.For(code.Persona).InputInvalidFormat(err.Error())
|
|||
|
|
}
|
|||
|
|
chunksText := splitChunks(raw, 900)
|
|||
|
|
now := clock.NowUnixNano()
|
|||
|
|
sourceID := uuid.NewString()
|
|||
|
|
item := &entity.KnowledgeSource{ID: sourceID, TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, SourceType: strings.TrimSpace(req.SourceType), Title: strings.TrimSpace(req.Title), Filename: strings.TrimSpace(req.Filename), RawContent: raw, ParsedStatus: "parsed", ChunkCount: len(chunksText), CreateAt: now}
|
|||
|
|
chunks := make([]*entity.KnowledgeChunk, 0, len(chunksText))
|
|||
|
|
for _, text := range chunksText {
|
|||
|
|
chunks = append(chunks, &entity.KnowledgeChunk{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, SourceID: sourceID, Content: text, Topics: inferTopics(text), StyleTags: inferStyleTags(text), RiskLevel: inferRisk(text), CreateAt: now})
|
|||
|
|
}
|
|||
|
|
if err := u.repo.CreateKnowledgeSource(ctx, item, chunks); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := sourceSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListKnowledgeSources(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.KnowledgeSourceSummary, error) {
|
|||
|
|
items, err := u.repo.ListKnowledgeSources(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.KnowledgeSourceSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, sourceSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListKnowledgeChunks(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.KnowledgeChunkSummary, error) {
|
|||
|
|
items, err := u.repo.ListKnowledgeChunks(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.KnowledgeChunkSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, chunkSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) CreateFormulaPool(ctx context.Context, req domusecase.FormulaPoolInput) (*domusecase.FormulaPoolSummary, error) {
|
|||
|
|
if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if strings.TrimSpace(req.Type) == "" || strings.TrimSpace(req.Name) == "" {
|
|||
|
|
return nil, app.For(code.Persona).InputMissingRequired("type and name are required")
|
|||
|
|
}
|
|||
|
|
now := clock.NowUnixNano()
|
|||
|
|
weight := req.Weight
|
|||
|
|
if weight <= 0 {
|
|||
|
|
weight = 50
|
|||
|
|
}
|
|||
|
|
item := &entity.FormulaPool{ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, Type: strings.TrimSpace(req.Type), Name: strings.TrimSpace(req.Name), Pattern: strings.TrimSpace(req.Pattern), UseCases: req.UseCases, Avoid: req.Avoid, Weight: weight, CreateAt: now, UpdateAt: now}
|
|||
|
|
if err := u.repo.CreateFormulaPool(ctx, item); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
out := formulaSummary(*item)
|
|||
|
|
return &out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) ListFormulaPools(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.FormulaPoolSummary, error) {
|
|||
|
|
items, err := u.repo.ListFormulaPools(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if len(items) == 0 {
|
|||
|
|
_ = u.seedDefaultFormulas(ctx, tenantID, ownerUID, personaID)
|
|||
|
|
items, _ = u.repo.ListFormulaPools(ctx, tenantID, ownerUID, personaID, limit)
|
|||
|
|
}
|
|||
|
|
out := make([]domusecase.FormulaPoolSummary, 0, len(items))
|
|||
|
|
for _, item := range items {
|
|||
|
|
out = append(out, formulaSummary(item))
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (u *useCase) seedDefaultFormulas(ctx context.Context, tenantID, ownerUID, personaID string) error {
|
|||
|
|
defs := []domusecase.FormulaPoolInput{{Type: "opening", Name: "問題開場", Pattern: "用讀者正在問的具體問題開場", UseCases: []string{"互動文"}, Weight: 70}, {Type: "body", Name: "先共鳴再整理", Pattern: "先接住情緒,再整理 2-3 個可執行觀點", UseCases: []string{"共鳴文", "知識文"}, Weight: 75}, {Type: "emotion", Name: "被理解感", Pattern: "講出讀者不一定說出口的焦慮或委屈", UseCases: []string{"情緒共鳴文"}, Weight: 70}, {Type: "cta", Name: "經驗募集型", Pattern: "邀請讀者補充自己的經驗,不硬導購", UseCases: []string{"互動文"}, Weight: 65}}
|
|||
|
|
for _, def := range defs {
|
|||
|
|
def.TenantID = tenantID
|
|||
|
|
def.OwnerUID = ownerUID
|
|||
|
|
def.PersonaID = personaID
|
|||
|
|
if _, err := u.CreateFormulaPool(ctx, def); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func decodeSourceContent(req domusecase.KnowledgeSourceInput) (string, error) {
|
|||
|
|
if strings.TrimSpace(req.ContentBase64) != "" {
|
|||
|
|
data, err := base64.StdEncoding.DecodeString(req.ContentBase64)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
if strings.EqualFold(filepath.Ext(req.Filename), ".xlsx") {
|
|||
|
|
return parseXLSX(data)
|
|||
|
|
}
|
|||
|
|
return string(data), nil
|
|||
|
|
}
|
|||
|
|
return strings.TrimSpace(req.Content), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type xlsxText struct {
|
|||
|
|
T string `xml:",chardata"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseXLSX(data []byte) (string, error) {
|
|||
|
|
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
var b strings.Builder
|
|||
|
|
for _, f := range zr.File {
|
|||
|
|
if !strings.HasPrefix(f.Name, "xl/sharedStrings") && !strings.HasPrefix(f.Name, "xl/worksheets") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
rc, err := f.Open()
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
var texts []xlsxText
|
|||
|
|
_ = xml.NewDecoder(rc).Decode(&texts)
|
|||
|
|
_ = rc.Close()
|
|||
|
|
for _, t := range texts {
|
|||
|
|
if s := strings.TrimSpace(t.T); s != "" {
|
|||
|
|
b.WriteString(s)
|
|||
|
|
b.WriteString("\n")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
text := strings.TrimSpace(b.String())
|
|||
|
|
if text == "" {
|
|||
|
|
return "", nil
|
|||
|
|
}
|
|||
|
|
return text, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func splitChunks(raw string, max int) []string {
|
|||
|
|
raw = strings.TrimSpace(raw)
|
|||
|
|
if raw == "" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
paras := strings.Split(raw, "\n")
|
|||
|
|
var out []string
|
|||
|
|
var b strings.Builder
|
|||
|
|
for _, p := range paras {
|
|||
|
|
p = strings.TrimSpace(p)
|
|||
|
|
if p == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if b.Len()+len([]rune(p)) > max && b.Len() > 0 {
|
|||
|
|
out = append(out, b.String())
|
|||
|
|
b.Reset()
|
|||
|
|
}
|
|||
|
|
if b.Len() > 0 {
|
|||
|
|
b.WriteString("\n")
|
|||
|
|
}
|
|||
|
|
b.WriteString(p)
|
|||
|
|
}
|
|||
|
|
if b.Len() > 0 {
|
|||
|
|
out = append(out, b.String())
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var topicRE = regexp.MustCompile(`[A-Za-z0-9_\p{Han}]{2,12}`)
|
|||
|
|
|
|||
|
|
func inferTopics(text string) []string {
|
|||
|
|
seen := map[string]bool{}
|
|||
|
|
var out []string
|
|||
|
|
for _, m := range topicRE.FindAllString(text, 20) {
|
|||
|
|
if !seen[m] && len(out) < 8 {
|
|||
|
|
seen[m] = true
|
|||
|
|
out = append(out, m)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
func inferStyleTags(text string) []string {
|
|||
|
|
var out []string
|
|||
|
|
if strings.Contains(text, "我") {
|
|||
|
|
out = append(out, "第一人稱")
|
|||
|
|
}
|
|||
|
|
if strings.Contains(text, "?") || strings.Contains(text, "?") {
|
|||
|
|
out = append(out, "提問")
|
|||
|
|
}
|
|||
|
|
if strings.Contains(text, "。") {
|
|||
|
|
out = append(out, "自然標點")
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
func inferRisk(text string) string {
|
|||
|
|
lower := strings.ToLower(text)
|
|||
|
|
for _, k := range []string{"醫", "藥", "amh", "投資", "法律"} {
|
|||
|
|
if strings.Contains(lower, k) {
|
|||
|
|
return "medium"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "low"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeRisk(raw string) string {
|
|||
|
|
raw = strings.ToLower(strings.TrimSpace(raw))
|
|||
|
|
if raw == "high" || raw == "low" {
|
|||
|
|
return raw
|
|||
|
|
}
|
|||
|
|
return "medium"
|
|||
|
|
}
|
|||
|
|
func topicSummary(item entity.TopicCandidate) domusecase.TopicCandidateSummary {
|
|||
|
|
return domusecase.TopicCandidateSummary{ID: item.ID, PersonaID: item.PersonaID, Name: item.Name, Source: item.Source, Category: item.Category, SeedQuery: item.SeedQuery, TrendReason: item.TrendReason, TrendKeywords: item.TrendKeywords, HeatScore: item.HeatScore, FitScore: item.FitScore, InteractionScore: item.InteractionScore, ExtendScore: item.ExtendScore, RiskScore: item.RiskScore, FinalScore: item.FinalScore, RecommendedMissions: item.RecommendedMissions, Status: item.Status, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
|
|||
|
|
}
|
|||
|
|
func planSummary(item entity.ContentPlan) domusecase.ContentPlanSummary {
|
|||
|
|
return domusecase.ContentPlanSummary{ID: item.ID, PersonaID: item.PersonaID, TopicCandidateID: item.TopicCandidateID, Topic: item.Topic, Mission: item.Mission, TargetAudience: item.TargetAudience, Angle: item.Angle, OpeningType: item.OpeningType, BodyType: item.BodyType, Emotion: item.Emotion, EndingType: item.EndingType, CtaType: item.CtaType, RiskLevel: item.RiskLevel, RequiresHumanReview: item.RequiresHumanReview, Avoid: item.Avoid, SelectedKnowledge: item.SelectedKnowledge, Status: item.Status, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
|
|||
|
|
}
|
|||
|
|
func feedbackSummary(item entity.FeedbackEvent) domusecase.FeedbackEventSummary {
|
|||
|
|
return domusecase.FeedbackEventSummary{ID: item.ID, PersonaID: item.PersonaID, ContentPlanID: item.ContentPlanID, DraftID: item.DraftID, Decision: item.Decision, Note: item.Note, Snapshot: item.Snapshot, CreateAt: item.CreateAt}
|
|||
|
|
}
|
|||
|
|
func sourceSummary(item entity.KnowledgeSource) domusecase.KnowledgeSourceSummary {
|
|||
|
|
return domusecase.KnowledgeSourceSummary{ID: item.ID, PersonaID: item.PersonaID, SourceType: item.SourceType, Title: item.Title, Filename: item.Filename, ParsedStatus: item.ParsedStatus, ChunkCount: item.ChunkCount, CreateAt: item.CreateAt}
|
|||
|
|
}
|
|||
|
|
func chunkSummary(item entity.KnowledgeChunk) domusecase.KnowledgeChunkSummary {
|
|||
|
|
return domusecase.KnowledgeChunkSummary{ID: item.ID, PersonaID: item.PersonaID, SourceID: item.SourceID, Content: item.Content, Topics: item.Topics, StyleTags: item.StyleTags, RiskLevel: item.RiskLevel, CreateAt: item.CreateAt}
|
|||
|
|
}
|
|||
|
|
func formulaSummary(item entity.FormulaPool) domusecase.FormulaPoolSummary {
|
|||
|
|
return domusecase.FormulaPoolSummary{ID: item.ID, PersonaID: item.PersonaID, Type: item.Type, Name: item.Name, Pattern: item.Pattern, UseCases: item.UseCases, Avoid: item.Avoid, Weight: item.Weight, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt}
|
|||
|
|
}
|