162 lines
4.8 KiB
Go
162 lines
4.8 KiB
Go
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())
|
||
}
|