thread-master/backend/internal/logic/copy_mission/inspire_copy_mission_logic.go

191 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.CopyMissionInspirationHandlerReq) (*types.CopyMissionInspirationData, error) {
tenantID, uid, err := actorFrom(l.ctx)
if err != nil {
return nil, err
}
personaID := strings.TrimSpace(req.PersonaID)
userKeyword := strings.TrimSpace(req.Keyword)
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 := ""
trendSourceLabel := ""
llmOnly := true
research, researchErr := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid)
if researchErr == nil {
memberCtx, memberErr := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research)
if memberErr == nil {
trendSnippets = libviral.CollectMissionInspireThreadsTrends(l.ctx, libviral.MissionInspireThreadsInput{
Keyword: userKeyword,
PersonaBrief: persona.Brief,
StyleBenchmark: persona.StyleBenchmark,
Pillars: append([]string(nil), persona.CopyResearchMap.Pillars...),
Questions: append([]string(nil), persona.CopyResearchMap.Questions...),
Member: memberCtx,
Crawler: l.makeCrawlerSearchFn(tenantID, uid),
})
if len(trendSnippets) > 0 {
trendSourceLabel = memberCtx.DiscoverPathLabel()
llmOnly = false
}
if len(trendSnippets) == 0 && placement.WebSearchAvailable(research) {
webClient := websearch.New(memberCtx.WebSearchConfig())
trendSnippets = libviral.CollectMissionInspireTrends(
l.ctx,
webClient,
memberCtx,
persona.Brief+" "+userKeyword,
persona.StyleBenchmark,
)
webSearchProvider = memberCtx.WebSearchProviderLabel()
trendSourceLabel = webSearchProvider
llmOnly = len(trendSnippets) == 0
}
}
}
webSearchUsed := len(trendSnippets) > 0 && webSearchProvider != ""
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,
UserKeyword: userKeyword,
TrendSnippets: trendSnippets,
WebSearchProvider: trendSourceLabel,
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 := "已依近期 Threads 熱門素材產出任務靈感,可微調後建立任務"
if webSearchUsed {
message = "已依近期網路趨勢產出任務靈感,可微調後建立任務"
}
if len(trendSnippets) == 0 {
message = "未取得外部趨勢素材,已改由 LLM 依人設與關鍵字推測靈感;同步 Chrome Session、完成 Threads API 或補上 BraveExa 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
}
func (l *InspireCopyMissionLogic) makeCrawlerSearchFn(tenantID, ownerUID string) placement.CrawlerSearchFn {
return func(ctx context.Context, member placement.MemberContext, keyword string, limit int) ([]placement.DiscoverPost, error) {
accountID := strings.TrimSpace(member.ActiveAccountID)
if accountID == "" {
return nil, app.For(code.Setting).InputMissingRequired("請先選定經營帳號並同步 Chrome Session")
}
session, err := l.svcCtx.ThreadsAccount.GetBrowserSession(ctx, tenantID, ownerUID, accountID)
if err != nil {
return nil, err
}
return placement.RunExecCrawlerSearch(ctx, session.StorageState, keyword, limit)
}
}