192 lines
5.9 KiB
Go
192 lines
5.9 KiB
Go
|
|
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)
|
||
|
|
}
|