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

191 lines
6.5 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
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}
}
2026-06-28 08:28:42 +00:00
func (l *InspireCopyMissionLogic) InspireCopyMission(req *types.CopyMissionInspirationHandlerReq) (*types.CopyMissionInspirationData, error) {
2026-06-26 08:37:04 +00:00
tenantID, uid, err := actorFrom(l.ctx)
if err != nil {
return nil, err
}
personaID := strings.TrimSpace(req.PersonaID)
2026-06-28 08:28:42 +00:00
userKeyword := strings.TrimSpace(req.Keyword)
2026-06-26 08:37:04 +00:00
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 := ""
2026-06-28 08:28:42 +00:00
trendSourceLabel := ""
2026-06-26 08:37:04 +00:00
llmOnly := true
research, researchErr := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid)
2026-06-28 08:28:42 +00:00
if researchErr == nil {
2026-06-26 08:37:04 +00:00
memberCtx, memberErr := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research)
if memberErr == nil {
2026-06-28 08:28:42 +00:00
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
}
2026-06-26 08:37:04 +00:00
}
}
2026-06-28 08:28:42 +00:00
webSearchUsed := len(trendSnippets) > 0 && webSearchProvider != ""
2026-06-26 08:37:04 +00:00
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,
2026-06-28 08:28:42 +00:00
UserKeyword: userKeyword,
2026-06-26 08:37:04 +00:00
TrendSnippets: trendSnippets,
2026-06-28 08:28:42 +00:00
WebSearchProvider: trendSourceLabel,
2026-06-26 08:37:04 +00:00
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,
})
}
2026-06-28 08:28:42 +00:00
message := "已依近期 Threads 熱門素材產出任務靈感,可微調後建立任務"
if webSearchUsed {
message = "已依近期網路趨勢產出任務靈感,可微調後建立任務"
}
if len(trendSnippets) == 0 {
message = "未取得外部趨勢素材,已改由 LLM 依人設與關鍵字推測靈感;同步 Chrome Session、完成 Threads API 或補上 BraveExa key 可更貼近熱搜"
2026-06-26 08:37:04 +00:00
}
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
}
2026-06-28 08:28:42 +00:00
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)
}
}