package copy_mission import ( "context" "fmt" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libmatrix "haixun-backend/internal/library/matrix" libprompt "haixun-backend/internal/library/prompt" "haixun-backend/internal/library/style8d" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" copydraftentity "haixun-backend/internal/model/copy_draft/domain/entity" copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" missionentity "haixun-backend/internal/model/copy_mission/domain/entity" missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" ) type GenerateCopyMissionMatrixLogic struct { ctx context.Context svcCtx *svc.ServiceContext } func NewGenerateCopyMissionMatrixLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateCopyMissionMatrixLogic { return &GenerateCopyMissionMatrixLogic{ctx: ctx, svcCtx: svcCtx} } func (l *GenerateCopyMissionMatrixLogic) GenerateCopyMissionMatrix( req *types.GenerateCopyMissionMatrixHandlerReq, ) (*types.GenerateCopyMissionMatrixData, error) { tenantID, uid, err := actorFrom(l.ctx) if err != nil { return nil, err } personaID := strings.TrimSpace(req.PersonaID) missionID := strings.TrimSpace(req.ID) mission, err := l.svcCtx.CopyMission.Get(l.ctx, tenantID, uid, personaID, missionID) if err != nil { return nil, err } if mission.Status != string(missionentity.StatusScanned) && mission.Status != string(missionentity.StatusDrafted) { return nil, app.For(code.Persona).ResInvalidState("請先完成海巡再產出內容矩陣") } if len(mission.SelectedTags) == 0 { return nil, app.For(code.Persona).InputMissingRequired("請先選擇海巡標籤") } persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) if err != nil { return nil, err } if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) { return nil, app.For(code.Persona).InputMissingRequired("請先完成人設 8D 對標分析") } personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) count := req.Count if count <= 0 { count = 5 } if count > 12 { count = 12 } posts, err := l.svcCtx.ScanPost.ListForPersona(l.ctx, scanpostdomain.PersonaListRequest{ TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, CopyMissionID: missionID, Limit: 12, }) if err != nil { return nil, err } samples := matrixSamplesFromScanPosts(posts) researchBlock := libmatrix.FormatCopyResearchMapBlock( mission.ResearchMap.AudienceSummary, mission.ResearchMap.ContentGoal, mission.ResearchMap.Questions, mission.ResearchMap.Pillars, mission.ResearchMap.Exclusions, ) userPrompt, err := libmatrix.BuildCopyUserPrompt(libmatrix.CopyGenerateInput{ Count: count, TopicLabel: mission.Label, TopicBrief: mission.Brief, ResearchMap: researchBlock, SelectedTags: mission.SelectedTags, ViralSamples: samples, PersonaBlock: personaBlock, }) if err != nil { return nil, app.For(code.AI).SysInternal("matrix user prompt load failed") } systemPrompt, err := libprompt.MatrixCopySystem() if err != nil { return nil, app.For(code.AI).SysInternal("matrix system prompt load failed") } 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 } result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, Credential: domai.Credential{ APIKey: credential.APIKey, }, System: systemPrompt, Messages: []domai.Message{ {Role: "user", Content: userPrompt}, }, }) if err != nil { return nil, err } parsed, err := libmatrix.ParseGenerateOutput(result.Text) if err != nil { return nil, app.For(code.AI).SvcThirdParty("內容矩陣 LLM 回傳無法解析:" + err.Error()) } createReqs := make([]copydraftdomain.CreateRequest, 0, len(parsed.Rows)) for _, row := range parsed.Rows { createReqs = append(createReqs, copydraftdomain.CreateRequest{ CopyMissionID: missionID, DraftType: copydraftentity.DraftTypeMatrix, SortOrder: row.SortOrder, Text: row.Text, Angle: row.Angle, Hook: row.Hook, Rationale: row.Rationale, ReferenceNotes: row.ReferenceNotes, Sources: row.SourcePermalinks, }) } saved, err := l.svcCtx.CopyDraft.ReplaceMissionMatrix(l.ctx, tenantID, uid, personaID, missionID, createReqs) if err != nil { return nil, err } drafted := missionentity.StatusDrafted _, _ = l.svcCtx.CopyMission.Update(l.ctx, missiondomain.UpdateRequest{ TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, MissionID: missionID, Patch: missiondomain.MissionPatch{ Status: &drafted, }, }) drafts := make([]types.CopyDraftData, 0, len(saved)) for _, item := range saved { drafts = append(drafts, toCopyDraftData(item)) } return &types.GenerateCopyMissionMatrixData{ Drafts: drafts, Message: fmt.Sprintf("已產出 %d 篇矩陣草稿", len(drafts)), }, nil } func matrixSamplesFromScanPosts(posts []scanpostdomain.ScanPostSummary) string { samples := make([]libmatrix.ViralPostSample, 0, len(posts)) for _, post := range posts { replies := make([]libmatrix.ViralReplySample, 0, len(post.Replies)) for _, reply := range post.Replies { replies = append(replies, libmatrix.ViralReplySample{ Author: reply.Author, Text: reply.Text, }) } samples = append(samples, libmatrix.ViralPostSample{ Author: post.Author, LikeCount: post.LikeCount, SearchTag: post.SearchTag, Text: post.Text, Replies: replies, }) } return libmatrix.FormatViralSamples(samples) }