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()) }