feat: integrate creator hub douyin workflows
This commit is contained in:
@@ -3,6 +3,7 @@ package creator
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -184,6 +185,26 @@ func (s *Store) ListDueCompetitors(ctx context.Context, now time.Time) ([]Compet
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) validateWorkSource(ctx context.Context, platform, sourceType, sourceID string) error {
|
||||
var query string
|
||||
switch sourceType {
|
||||
case SourceOwned:
|
||||
query = `SELECT 1 FROM social_account WHERE id=$1 AND platform=$2`
|
||||
case SourceCompetitor:
|
||||
query = `SELECT 1 FROM creator_competitor WHERE id=$1 AND platform=$2`
|
||||
default:
|
||||
return ErrInvalid
|
||||
}
|
||||
var exists int
|
||||
if err := s.db.QueryRowContext(ctx, query, sourceID, platform).Scan(&exists); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrInvalid
|
||||
}
|
||||
return databaseError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time) (Work, bool, error) {
|
||||
input.Platform = strings.TrimSpace(input.Platform)
|
||||
input.WorkKey = strings.TrimSpace(input.WorkKey)
|
||||
@@ -200,6 +221,9 @@ func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time)
|
||||
utf8.RuneCountInString(input.CoverURL) > 2000 {
|
||||
return Work{}, false, ErrInvalid
|
||||
}
|
||||
if err := s.validateWorkSource(ctx, input.Platform, input.SourceType, input.SourceID); err != nil {
|
||||
return Work{}, false, err
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
@@ -532,6 +556,59 @@ func (s *Store) ConfirmRewrite(ctx context.Context, workID, requirement string)
|
||||
return s.GetMaterial(ctx, workID)
|
||||
}
|
||||
|
||||
func parseGeneratedRewrite(content string) (string, string, error) {
|
||||
var result struct {
|
||||
Title string `json:"title"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(content)), &result); err != nil {
|
||||
return "", "", fmt.Errorf("decode generated rewrite: %w", err)
|
||||
}
|
||||
result.Title, result.Script = strings.TrimSpace(result.Title), strings.TrimSpace(result.Script)
|
||||
if result.Title == "" || result.Script == "" || utf8.RuneCountInString(result.Title) > 1000 || utf8.RuneCountInString(result.Script) > 100000 {
|
||||
return "", "", ErrInvalid
|
||||
}
|
||||
return result.Title, result.Script, nil
|
||||
}
|
||||
|
||||
func (s *Store) GenerateRewrite(ctx context.Context, workID string, generator TextGenerator) (MaterialJob, error) {
|
||||
if generator == nil {
|
||||
return MaterialJob{}, fmt.Errorf("%w: text generator is not configured", ErrUnavailable)
|
||||
}
|
||||
settings, err := s.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return MaterialJob{}, err
|
||||
}
|
||||
if !settings.AIConfigured || strings.TrimSpace(settings.AIProvider) == "" || strings.TrimSpace(settings.AIModel) == "" {
|
||||
return MaterialJob{}, fmt.Errorf("%w: AI settings are not approved", ErrUnavailable)
|
||||
}
|
||||
job, err := s.GetMaterial(ctx, workID)
|
||||
if err != nil {
|
||||
return MaterialJob{}, err
|
||||
}
|
||||
if job.RewriteConfirmedAt == nil {
|
||||
return MaterialJob{}, ErrConflict
|
||||
}
|
||||
work, err := s.GetWork(ctx, workID)
|
||||
if err != nil {
|
||||
return MaterialJob{}, err
|
||||
}
|
||||
content, err := generator.Generate(ctx,
|
||||
"根据原作品和转写内容生成仿写草稿。只返回 JSON,不要 Markdown 或额外文字,格式必须是 {\"title\":\"标题\",\"script\":\"脚本\"}。不得编造原文没有的事实。",
|
||||
fmt.Sprintf("改写要求:%s\n原标题:%s\n原文案:%s\n转写:%s", job.RewriteRequirement, work.Title, work.Body, job.Transcript))
|
||||
if err != nil {
|
||||
return MaterialJob{}, err
|
||||
}
|
||||
title, script, err := parseGeneratedRewrite(content)
|
||||
if err != nil {
|
||||
return MaterialJob{}, err
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, `UPDATE creator_material_job SET generated_title = $2, generated_script = $3, updated_at = now() WHERE work_id = $1`, workID, title, script); err != nil {
|
||||
return MaterialJob{}, databaseError(err)
|
||||
}
|
||||
return s.GetMaterial(ctx, workID)
|
||||
}
|
||||
|
||||
func (s *Store) SaveRewrite(ctx context.Context, workID, title, script string) (MaterialJob, error) {
|
||||
title, script = strings.TrimSpace(title), strings.TrimSpace(script)
|
||||
if title == "" || script == "" || utf8.RuneCountInString(title) > 1000 || utf8.RuneCountInString(script) > 100000 {
|
||||
@@ -591,6 +668,48 @@ func (s *Store) GetComment(ctx context.Context, id string) (Comment, error) {
|
||||
return result, rowError(err)
|
||||
}
|
||||
|
||||
func pageBounds(page, pageSize int) (int, int, error) {
|
||||
if page < 1 || pageSize < 1 || pageSize > 100 {
|
||||
return 0, 0, ErrInvalid
|
||||
}
|
||||
if page > int(maxDurationSeconds) || pageSize > int(maxDurationSeconds)/page {
|
||||
return 0, 0, ErrInvalid
|
||||
}
|
||||
return (page - 1) * pageSize, pageSize, nil
|
||||
}
|
||||
|
||||
func slicePage[T any](items []T, page, pageSize int) (Page[T], error) {
|
||||
offset, limit, err := pageBounds(page, pageSize)
|
||||
if err != nil {
|
||||
return Page[T]{}, err
|
||||
}
|
||||
if offset > len(items) {
|
||||
offset = len(items)
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
data := append([]T(nil), items[offset:end]...)
|
||||
return Page[T]{Data: data, Page: page, PageSize: pageSize, Total: len(items), HasNext: end < len(items)}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListWorksPage(ctx context.Context, filter WorkFilter, page, pageSize int) (Page[Work], error) {
|
||||
items, err := s.ListWorks(ctx, filter)
|
||||
if err != nil {
|
||||
return Page[Work]{}, err
|
||||
}
|
||||
return slicePage(items, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Store) ListCommentsPage(ctx context.Context, platform, workID string, page, pageSize int) (Page[Comment], error) {
|
||||
items, err := s.ListComments(ctx, platform, workID)
|
||||
if err != nil {
|
||||
return Page[Comment]{}, err
|
||||
}
|
||||
return slicePage(items, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Store) ListComments(ctx context.Context, platform, workID string) ([]Comment, error) {
|
||||
query, args := commentSelect, []any{}
|
||||
where := []string{}
|
||||
|
||||
Reference in New Issue
Block a user