package persona import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/library/style8d" libviral "haixun-backend/internal/library/viral" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" copydraftusecase "haixun-backend/internal/model/copy_draft/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" ) type GeneratePersonaCopyDraftLogic struct { ctx context.Context svcCtx *svc.ServiceContext } func NewGeneratePersonaCopyDraftLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GeneratePersonaCopyDraftLogic { return &GeneratePersonaCopyDraftLogic{ctx: ctx, svcCtx: svcCtx} } func (l *GeneratePersonaCopyDraftLogic) GeneratePersonaCopyDraft( req *types.GeneratePersonaCopyDraftHandlerReq, ) (*types.GeneratePersonaCopyDraftData, error) { tenantID, uid, err := actorFrom(l.ctx) if err != nil { return nil, err } personaID := strings.TrimSpace(req.ID) scanPostID := strings.TrimSpace(req.ScanPostID) if scanPostID == "" { return nil, app.For(code.Persona).InputMissingRequired("scan_post_id is required") } persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) if err != nil { return nil, err } post, err := l.svcCtx.ScanPost.GetForPersona(l.ctx, tenantID, uid, personaID, scanPostID) if err != nil { return nil, err } topicLabel := strings.TrimSpace(post.SearchTag) topicBrief := strings.TrimSpace(persona.Brief) if missionID := strings.TrimSpace(post.CopyMissionID); missionID != "" { if mission, missionErr := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID); missionErr == nil { if label := strings.TrimSpace(mission.Label); label != "" { topicLabel = label } if brief := strings.TrimSpace(mission.Brief); brief != "" { topicBrief = brief } } } if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") } personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) credential, err := l.svcCtx.ThreadsAccount.ResolveMemberAiCredential(l.ctx, tenantID, uid) if err != nil { return nil, err } providerID, err := aiusecase.MapWorkerProvider(credential.Provider) if err != nil { return nil, err } analysisText := "" analyzeResult, analyzeErr := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, Credential: domai.Credential{ APIKey: credential.APIKey, }, System: libviral.BuildAnalyzeViralSystemPrompt(), Messages: []domai.Message{ { Role: "user", Content: libviral.BuildAnalyzeViralUserPrompt(libviral.AnalyzeViralInput{ PostText: post.Text, AuthorName: post.Author, LikeCount: post.LikeCount, ReplyCount: post.ReplyCount, SearchTag: post.SearchTag, TopicLabel: topicLabel, TopicBrief: topicBrief, Persona: personaBlock, }), }, }, }) if analyzeErr == nil { if parsed, parseErr := libviral.ParseAnalyzeViralOutput(analyzeResult.Text); parseErr == nil { analysisText = libviral.FormatAnalysisForReplicate(parsed) } } result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, Credential: domai.Credential{ APIKey: credential.APIKey, }, System: libviral.BuildSystemPrompt(), Messages: []domai.Message{ { Role: "user", Content: libviral.BuildUserPrompt(libviral.ReplicateInput{ TopicLabel: topicLabel, TopicBrief: topicBrief, Persona: personaBlock, StyleProfile: "", OriginalText: post.Text, AuthorName: post.Author, StructureAnalysis: analysisText, }), }, }, }) if err != nil { return nil, err } parsed, err := libviral.ParseReplicateOutput(result.Text) if err != nil { return nil, app.For(code.AI).SvcThirdParty("仿寫 LLM 回傳無法解析:" + err.Error()) } saved, err := l.svcCtx.CopyDraft.Create(l.ctx, copydraftusecase.CreateRequest{ TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, CopyMissionID: post.CopyMissionID, ScanPostID: scanPostID, DraftType: "replicate", Text: parsed.Text, Angle: parsed.Angle, Hook: parsed.Hook, Rationale: parsed.Rationale, ReferenceNotes: parsed.StructureNotes, Sources: []string{post.Permalink}, }) if err != nil { return nil, err } return &types.GeneratePersonaCopyDraftData{ Draft: types.CopyDraftData{ ID: saved.ID, PersonaID: saved.PersonaID, CopyMissionID: saved.CopyMissionID, ScanPostID: saved.ScanPostID, DraftType: saved.DraftType, SortOrder: saved.SortOrder, Text: saved.Text, Angle: saved.Angle, Hook: saved.Hook, Rationale: saved.Rationale, ReferenceNotes: saved.ReferenceNotes, Sources: saved.Sources, Status: saved.Status, CreateAt: saved.CreateAt, }, Message: "已產出仿寫草稿", }, nil }