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