package copy_mission import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/library/placement" "haixun-backend/internal/library/style8d" libviral "haixun-backend/internal/library/viral" "haixun-backend/internal/library/websearch" domai "haixun-backend/internal/model/ai/domain/usecase" aiusecase "haixun-backend/internal/model/ai/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" ) type InspireCopyMissionLogic struct { ctx context.Context svcCtx *svc.ServiceContext } func NewInspireCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InspireCopyMissionLogic { return &InspireCopyMissionLogic{ctx: ctx, svcCtx: svcCtx} } func (l *InspireCopyMissionLogic) InspireCopyMission(req *types.PersonaCopyMissionsPath) (*types.CopyMissionInspirationData, error) { tenantID, uid, err := actorFrom(l.ctx) if err != nil { return nil, err } personaID := strings.TrimSpace(req.PersonaID) persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID) if err != nil { return nil, err } missionList, err := l.svcCtx.CopyMission.List(l.ctx, tenantID, uid, personaID) if err != nil { return nil, err } recentLabels := make([]string, 0, 8) recentSeeds := make([]string, 0, 8) for _, mission := range missionList.List { if mission.Status == "archived" { continue } if label := strings.TrimSpace(mission.Label); label != "" { recentLabels = append(recentLabels, label) } if seed := strings.TrimSpace(mission.SeedQuery); seed != "" { recentSeeds = append(recentSeeds, seed) } if len(recentLabels) >= 8 && len(recentSeeds) >= 8 { break } } trendSnippets := []libviral.MissionInspireTrendSnippet{} webSearchProvider := "" llmOnly := true research, researchErr := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid) if researchErr == nil && placement.WebSearchAvailable(research) { memberCtx, memberErr := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research) if memberErr == nil { webClient := websearch.New(memberCtx.WebSearchConfig()) trendSnippets = libviral.CollectMissionInspireTrends( l.ctx, webClient, memberCtx, persona.Brief, persona.StyleBenchmark, ) webSearchProvider = memberCtx.WebSearchProviderLabel() llmOnly = len(trendSnippets) == 0 } } webSearchUsed := len(trendSnippets) > 0 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 } personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief) result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, Credential: domai.Credential{ APIKey: credential.APIKey, }, System: libviral.BuildMissionInspireSystemPrompt(), Messages: []domai.Message{ { Role: "user", Content: libviral.BuildMissionInspireUserPrompt(libviral.MissionInspireInput{ PersonaDisplayName: persona.DisplayName, PersonaBrief: persona.Brief, PersonaBlock: personaBlock, StyleBenchmark: persona.StyleBenchmark, PersonaAudience: persona.CopyResearchMap.AudienceSummary, PersonaContentGoal: persona.CopyResearchMap.ContentGoal, PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...), PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...), RecentMissionLabels: recentLabels, RecentSeedQueries: recentSeeds, TrendSnippets: trendSnippets, WebSearchProvider: webSearchProvider, LLMOnly: llmOnly, }), }, }, }) if err != nil { return nil, err } parsed, err := libviral.ParseMissionInspireOutput(result.Text) if err != nil { return nil, app.For(code.AI).SvcThirdParty("靈感骰子 LLM 回傳無法解析:" + err.Error()) } sources := make([]types.CopyMissionInspirationSourceData, 0, len(trendSnippets)) for _, item := range trendSnippets { sources = append(sources, types.CopyMissionInspirationSourceData{ Query: item.Query, Title: item.Title, Snippet: item.Snippet, URL: item.URL, }) } message := "已依近期趨勢產出任務靈感,可微調後建立任務" if !webSearchUsed { message = "未設定搜尋 API key,已改由 LLM 依人設推測靈感;補上 Brave/Exa key 可取得更貼近熱搜的結果" } return &types.CopyMissionInspirationData{ Label: parsed.Label, SeedQuery: parsed.SeedQuery, Brief: parsed.Brief, TrendReason: parsed.TrendReason, TrendKeywords: parsed.TrendKeywords, Sources: sources, WebSearchUsed: webSearchUsed, Message: message, }, nil }