thread-master/backend/internal/library/personacopy/formula_draft.go

162 lines
4.8 KiB
Go
Raw Permalink Normal View History

package personacopy
import (
"context"
"fmt"
"strings"
libformula "haixun-backend/internal/library/formula"
"haixun-backend/internal/library/placement"
"haixun-backend/internal/library/style8d"
"haixun-backend/internal/library/websearch"
domai "haixun-backend/internal/model/ai/domain/usecase"
aiusecase "haixun-backend/internal/model/ai/usecase"
contentformuladomain "haixun-backend/internal/model/content_formula/domain/usecase"
copydraftentity "haixun-backend/internal/model/copy_draft/domain/entity"
copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase"
personadomain "haixun-backend/internal/model/persona/domain/usecase"
placementusecase "haixun-backend/internal/model/placement/usecase"
threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
)
type FormulaDraftInput struct {
TenantID string
OwnerUID string
PersonaID string
AccountID string
FormulaID string
Topic string
Brief string
UseWebSearch bool
DraftCount int
}
type FormulaDraftDeps struct {
Persona personadomain.UseCase
ContentFormula contentformuladomain.UseCase
CopyDraft copydraftdomain.UseCase
ThreadsAccount threadsaccountdomain.UseCase
Placement placementusecase.UseCase
AI domai.UseCase
}
func RunFormulaDraft(ctx context.Context, deps FormulaDraftDeps, in FormulaDraftInput, progress ProgressFn) (int, error) {
if progress == nil {
progress = func(string, int) {}
}
topic := strings.TrimSpace(in.Topic)
if topic == "" {
return 0, fmt.Errorf("topic is required")
}
count := in.DraftCount
if count <= 0 {
count = 1
}
if count > 5 {
count = 5
}
progress("讀取人設與寫法公式…", 10)
persona, err := deps.Persona.Get(ctx, in.TenantID, in.OwnerUID, in.PersonaID)
if err != nil {
return 0, err
}
if !style8d.HasReady8D(persona.Persona, persona.StyleProfile) {
return 0, fmt.Errorf("請先完成人設 8D 對標分析")
}
formula, err := deps.ContentFormula.Get(ctx, in.TenantID, in.OwnerUID, in.AccountID, in.FormulaID)
if err != nil {
return 0, err
}
credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, in.TenantID, in.OwnerUID)
if err != nil {
return 0, err
}
providerID, err := aiusecase.MapWorkerProvider(credential.Provider)
if err != nil {
return 0, err
}
aiReq := domai.GenerateRequest{
Provider: providerID,
Model: credential.Model,
Credential: domai.Credential{APIKey: credential.APIKey},
}
personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief)
researchNotes := ""
if in.UseWebSearch {
progress("搜尋網路參考資料…", 20)
researchNotes = searchFormulaNotes(ctx, deps, in.TenantID, in.OwnerUID, topic, in.Brief)
}
created := 0
for i := 0; i < count; i++ {
progress(fmt.Sprintf("呼叫 AI 產生草稿 %d/%d%s / %s…", i+1, count, credential.Provider, credential.Model), 35+(i*40/count))
generated, genErr := libformula.GenerateDraft(ctx, deps.AI, aiReq, libformula.GenerateInput{
Topic: topic,
Brief: in.Brief,
PersonaBlock: personaBlock,
ResearchNotes: researchNotes,
Formula: *formula,
})
if genErr != nil {
return created, genErr
}
if _, saveErr := deps.CopyDraft.Create(ctx, copydraftdomain.CreateRequest{
TenantID: in.TenantID,
OwnerUID: in.OwnerUID,
PersonaID: in.PersonaID,
FormulaID: formula.ID,
DraftType: copydraftentity.DraftTypeFormula,
Text: generated.Text,
TopicTag: generated.TopicTag,
Angle: generated.Angle,
Hook: generated.Hook,
Rationale: generated.Rationale,
ReferenceNotes: generated.StructureNotes,
Sources: []string{formula.Label},
}); saveErr != nil {
return created, saveErr
}
created++
}
progress(fmt.Sprintf("已產生 %d 篇草稿", created), 100)
return created, nil
}
func searchFormulaNotes(ctx context.Context, deps FormulaDraftDeps, tenantID, uid, topic, brief string) string {
research, err := deps.Placement.ResearchSettings(ctx, tenantID, uid)
if err != nil || !placement.WebSearchAvailable(research) {
return ""
}
memberCtx, err := deps.ThreadsAccount.ResolveMemberPlacementContext(ctx, tenantID, uid, research)
if err != nil {
return ""
}
webClient := websearch.New(memberCtx.WebSearchConfig())
if !webClient.Enabled() {
return ""
}
resp, err := webClient.Search(ctx, websearch.SearchOptions{
Query: topic + " " + strings.TrimSpace(brief),
Limit: 5,
Mode: websearch.ModeKnowledgeExpand,
})
if err != nil || len(resp.Results) == 0 {
return ""
}
var b strings.Builder
for _, snip := range resp.Results {
if snip.Title != "" {
b.WriteString("- ")
b.WriteString(snip.Title)
b.WriteString("\n")
}
if snip.Snippet != "" {
b.WriteString(snip.Snippet)
b.WriteString("\n")
}
}
return strings.TrimSpace(b.String())
}