feat(schedule-tasks): support auto-publish via media accounts
Replace the legacy target_platform string on schedule tasks with a workspace-aware auto-publish payload (auto_publish + publish_account_ids JSONB + cover_asset_url + cover_image_asset_id) and migrate the schema, sqlc queries, generated models, domain struct, ScheduleTaskService DTOs, and dispatch worker to round-trip the new fields. PromptRuleGeneration gains a WithPublishJobService hook so executeGeneration can enqueue an auto-publish job once the article is ready, and worker-generate wires the publish-job service in. On the admin-web side, extract PublishArticleModal's account-card builders into a shared publish-account-cards module, rebuild GenerateTaskDrawer's schedule mode around account selection plus a CoverPickerModal, and surface the new "auto publish" column on ScheduleTaskTab. Shared types and i18n strings cover the new fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
@@ -31,6 +32,8 @@ type PromptRuleGenerationService struct {
|
||||
articleTimeout time.Duration
|
||||
maxOutputTokens int64
|
||||
cache sharedcache.Cache
|
||||
publishJobs *PublishJobService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type promptRuleGenerationJob struct {
|
||||
@@ -46,10 +49,9 @@ type promptRuleGenerationJob struct {
|
||||
}
|
||||
|
||||
type GenerateFromRuleRequest struct {
|
||||
PromptRuleID int64 `json:"prompt_rule_id" binding:"required"`
|
||||
BrandID *int64 `json:"brand_id"`
|
||||
TargetPlatform *string `json:"target_platform"`
|
||||
ExtraParams map[string]interface{} `json:"extra_params"`
|
||||
PromptRuleID int64 `json:"prompt_rule_id" binding:"required"`
|
||||
BrandID *int64 `json:"brand_id"`
|
||||
ExtraParams map[string]interface{} `json:"extra_params"`
|
||||
}
|
||||
|
||||
type GenerateFromRuleResponse struct {
|
||||
@@ -69,16 +71,20 @@ type GenerateFromRuleItem struct {
|
||||
}
|
||||
|
||||
type SchedulePromptRuleGenerationInput struct {
|
||||
ScheduleTaskID int64
|
||||
OperatorID *int64
|
||||
TenantID int64
|
||||
PromptRuleID int64
|
||||
BrandID *int64
|
||||
Name string
|
||||
TargetPlatform *string
|
||||
EnableWebSearch bool
|
||||
GenerateCount int
|
||||
ScheduledFor time.Time
|
||||
ScheduleTaskID int64
|
||||
OperatorID *int64
|
||||
TenantID int64
|
||||
PromptRuleID int64
|
||||
BrandID *int64
|
||||
Name string
|
||||
WorkspaceID int64
|
||||
AutoPublish bool
|
||||
PublishAccountIDs []string
|
||||
CoverAssetURL *string
|
||||
CoverImageAssetID *int64
|
||||
EnableWebSearch bool
|
||||
GenerateCount int
|
||||
ScheduledFor time.Time
|
||||
}
|
||||
|
||||
type promptRuleGenerationRecord struct {
|
||||
@@ -96,13 +102,12 @@ type promptRuleGenerationRecord struct {
|
||||
const promptRuleGeneratingTitlePlaceholder = "标题生成中"
|
||||
|
||||
type enqueuePromptRuleGenerationInput struct {
|
||||
OperatorID *int64
|
||||
TenantID int64
|
||||
PromptRuleID int64
|
||||
BrandID *int64
|
||||
TargetPlatform *string
|
||||
ExtraParams map[string]interface{}
|
||||
FallbackName string
|
||||
OperatorID *int64
|
||||
TenantID int64
|
||||
PromptRuleID int64
|
||||
BrandID *int64
|
||||
ExtraParams map[string]interface{}
|
||||
FallbackName string
|
||||
}
|
||||
|
||||
func NewPromptRuleGenerationService(
|
||||
@@ -138,6 +143,16 @@ func (s *PromptRuleGenerationService) WithCache(c sharedcache.Cache) *PromptRule
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) WithPublishJobService(publishJobs *PublishJobService) *PromptRuleGenerationService {
|
||||
s.publishJobs = publishJobs
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) WithLogger(logger *zap.Logger) *PromptRuleGenerationService {
|
||||
s.logger = logger
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req GenerateFromRuleRequest) (*GenerateFromRuleResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
extra := clonePromptRuleExtraParams(req.ExtraParams)
|
||||
@@ -159,13 +174,12 @@ func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req
|
||||
var lastErr error
|
||||
for idx := 0; idx < generateCount; idx++ {
|
||||
result, enqueueErr := s.enqueuePromptRuleGeneration(ctx, enqueuePromptRuleGenerationInput{
|
||||
OperatorID: &actor.UserID,
|
||||
TenantID: actor.TenantID,
|
||||
PromptRuleID: req.PromptRuleID,
|
||||
BrandID: req.BrandID,
|
||||
TargetPlatform: req.TargetPlatform,
|
||||
ExtraParams: clonePromptRuleExtraParams(extra),
|
||||
FallbackName: "即时任务",
|
||||
OperatorID: &actor.UserID,
|
||||
TenantID: actor.TenantID,
|
||||
PromptRuleID: req.PromptRuleID,
|
||||
BrandID: req.BrandID,
|
||||
ExtraParams: clonePromptRuleExtraParams(extra),
|
||||
FallbackName: "即时任务",
|
||||
})
|
||||
if enqueueErr != nil {
|
||||
lastErr = enqueueErr
|
||||
@@ -193,6 +207,19 @@ func (s *PromptRuleGenerationService) EnqueueScheduledGeneration(ctx context.Con
|
||||
"schedule_task_id": input.ScheduleTaskID,
|
||||
"enable_web_search": input.EnableWebSearch,
|
||||
}
|
||||
if input.WorkspaceID > 0 {
|
||||
extra["workspace_id"] = input.WorkspaceID
|
||||
}
|
||||
if input.AutoPublish {
|
||||
extra["schedule_auto_publish"] = true
|
||||
extra["schedule_publish_account_ids"] = input.PublishAccountIDs
|
||||
if input.CoverAssetURL != nil {
|
||||
extra["schedule_cover_asset_url"] = strings.TrimSpace(*input.CoverAssetURL)
|
||||
}
|
||||
if input.CoverImageAssetID != nil {
|
||||
extra["schedule_cover_image_asset_id"] = *input.CoverImageAssetID
|
||||
}
|
||||
}
|
||||
if input.GenerateCount > 0 {
|
||||
extra["generate_count"] = input.GenerateCount
|
||||
}
|
||||
@@ -204,13 +231,12 @@ func (s *PromptRuleGenerationService) EnqueueScheduledGeneration(ctx context.Con
|
||||
}
|
||||
|
||||
return s.enqueuePromptRuleGeneration(ctx, enqueuePromptRuleGenerationInput{
|
||||
OperatorID: input.OperatorID,
|
||||
TenantID: input.TenantID,
|
||||
PromptRuleID: input.PromptRuleID,
|
||||
BrandID: input.BrandID,
|
||||
TargetPlatform: input.TargetPlatform,
|
||||
ExtraParams: extra,
|
||||
FallbackName: "定时任务",
|
||||
OperatorID: input.OperatorID,
|
||||
TenantID: input.TenantID,
|
||||
PromptRuleID: input.PromptRuleID,
|
||||
BrandID: input.BrandID,
|
||||
ExtraParams: extra,
|
||||
FallbackName: "定时任务",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -254,10 +280,12 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
inputParams := map[string]interface{}{
|
||||
"prompt_rule_id": input.PromptRuleID,
|
||||
"task_name": taskName,
|
||||
"target_platform": strings.TrimSpace(derefString(input.TargetPlatform)),
|
||||
"enable_web_search": extractBool(extra, "enable_web_search"),
|
||||
"generate_count": generateCount,
|
||||
}
|
||||
if workspaceID := extractInt(extra, "workspace_id"); workspaceID > 0 {
|
||||
inputParams["workspace_id"] = workspaceID
|
||||
}
|
||||
if generationMode != "" {
|
||||
inputParams["generation_mode"] = generationMode
|
||||
}
|
||||
@@ -267,10 +295,15 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
if scheduledFor := strings.TrimSpace(extractString(extra, "scheduled_for")); scheduledFor != "" {
|
||||
inputParams["scheduled_for"] = scheduledFor
|
||||
}
|
||||
|
||||
platformIDs := parsePlatformIDs(derefString(input.TargetPlatform))
|
||||
if len(platformIDs) > 0 {
|
||||
inputParams["target_platforms"] = platformIDs
|
||||
if extractBool(extra, "schedule_auto_publish") {
|
||||
inputParams["schedule_auto_publish"] = true
|
||||
inputParams["schedule_publish_account_ids"] = extractStringList(extra["schedule_publish_account_ids"], 64)
|
||||
if coverURL := strings.TrimSpace(extractString(extra, "schedule_cover_asset_url")); coverURL != "" {
|
||||
inputParams["schedule_cover_asset_url"] = coverURL
|
||||
}
|
||||
if coverImageAssetID := extractInt(extra, "schedule_cover_image_asset_id"); coverImageAssetID > 0 {
|
||||
inputParams["schedule_cover_image_asset_id"] = coverImageAssetID
|
||||
}
|
||||
}
|
||||
if input.BrandID != nil {
|
||||
inputParams["brand_id"] = *input.BrandID
|
||||
@@ -301,7 +334,16 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
}
|
||||
|
||||
inputJSON, _ := json.Marshal(inputParams)
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, platformIDs, nil, NullableInt64Input{})
|
||||
var coverAssetURL *string
|
||||
var coverImageAssetID NullableInt64Input
|
||||
if coverURL := strings.TrimSpace(extractString(inputParams, "schedule_cover_asset_url")); coverURL != "" {
|
||||
coverAssetURL = &coverURL
|
||||
}
|
||||
if rawCoverImageAssetID := int64(extractInt(inputParams, "schedule_cover_image_asset_id")); rawCoverImageAssetID > 0 {
|
||||
coverImageAssetID.Set = true
|
||||
coverImageAssetID.Value = &rawCoverImageAssetID
|
||||
}
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, nil, coverAssetURL, coverImageAssetID)
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -557,6 +599,74 @@ func (s *PromptRuleGenerationService) executeGeneration(ctx context.Context, job
|
||||
if s.streamEnabled {
|
||||
s.streams.Complete(job.ArticleID, job.TaskID, title, content)
|
||||
}
|
||||
|
||||
s.enqueueScheduleAutoPublish(cleanupCtx, job, title)
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) enqueueScheduleAutoPublish(ctx context.Context, job promptRuleGenerationJob, title string) {
|
||||
if s == nil || s.publishJobs == nil || !extractBool(job.InputParams, "schedule_auto_publish") {
|
||||
return
|
||||
}
|
||||
|
||||
workspaceID := int64(extractInt(job.InputParams, "workspace_id"))
|
||||
accountIDs := extractStringList(job.InputParams["schedule_publish_account_ids"], 64)
|
||||
coverURL := strings.TrimSpace(extractString(job.InputParams, "schedule_cover_asset_url"))
|
||||
if workspaceID <= 0 || job.OperatorID <= 0 || len(accountIDs) == 0 {
|
||||
s.logAutoPublishWarn("schedule auto publish skipped because configuration is incomplete", nil,
|
||||
zap.Int64("generation_task_id", job.TaskID),
|
||||
zap.Int64("article_id", job.ArticleID),
|
||||
zap.Int64("workspace_id", workspaceID),
|
||||
zap.Int("account_count", len(accountIDs)),
|
||||
zap.Bool("has_cover", coverURL != ""),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
accounts := make([]CreatePublishJobAccountRequest, 0, len(accountIDs))
|
||||
for _, accountID := range accountIDs {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
continue
|
||||
}
|
||||
accounts = append(accounts, CreatePublishJobAccountRequest{AccountID: accountID})
|
||||
}
|
||||
if len(accounts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
publishTitle := strings.TrimSpace(title)
|
||||
if publishTitle == "" {
|
||||
publishTitle = "定时生成文章"
|
||||
}
|
||||
|
||||
if _, err := s.publishJobs.Create(ctx, auth.Actor{
|
||||
TenantID: job.TenantID,
|
||||
UserID: job.OperatorID,
|
||||
PrimaryWorkspaceID: workspaceID,
|
||||
}, CreatePublishJobRequest{
|
||||
Title: publishTitle,
|
||||
ContentRef: map[string]any{
|
||||
"article_id": job.ArticleID,
|
||||
},
|
||||
Accounts: accounts,
|
||||
}); err != nil {
|
||||
s.logAutoPublishWarn("schedule auto publish enqueue failed", err,
|
||||
zap.Int64("generation_task_id", job.TaskID),
|
||||
zap.Int64("article_id", job.ArticleID),
|
||||
zap.Int64("workspace_id", workspaceID),
|
||||
zap.Int("account_count", len(accounts)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) logAutoPublishWarn(message string, err error, fields ...zap.Field) {
|
||||
if s == nil || s.logger == nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
}
|
||||
s.logger.Warn(message, fields...)
|
||||
}
|
||||
|
||||
func (s *PromptRuleGenerationService) failGeneration(ctx context.Context, job promptRuleGenerationJob, title, stage string, genErr error) {
|
||||
@@ -632,9 +742,6 @@ func buildPromptRuleGenerationPrompt(rule *promptRuleGenerationRecord, params ma
|
||||
if rule.DefaultWordCount != nil && *rule.DefaultWordCount > 0 {
|
||||
supplements = append(supplements, prompts.PromptRuleWordCountSupplement(*rule.DefaultWordCount))
|
||||
}
|
||||
if target := strings.TrimSpace(extractString(params, "target_platform")); target != "" {
|
||||
supplements = append(supplements, prompts.PromptRuleTargetPlatformSupplement(target))
|
||||
}
|
||||
|
||||
if len(supplements) > 0 {
|
||||
sections = append(sections, prompts.PromptRuleSupplementSection(supplements))
|
||||
@@ -678,9 +785,6 @@ func buildPromptRuleKnowledgeQuery(rule *promptRuleGenerationRecord, params map[
|
||||
if keywords := extractStringList(params["keywords"], 16); len(keywords) > 0 {
|
||||
parts = append(parts, strings.Join(keywords, "\n"))
|
||||
}
|
||||
if target := strings.TrimSpace(extractString(params, "target_platform")); target != "" {
|
||||
parts = append(parts, target)
|
||||
}
|
||||
if rule != nil {
|
||||
if text := strings.TrimSpace(rule.PromptContent); text != "" {
|
||||
parts = append(parts, text)
|
||||
|
||||
Reference in New Issue
Block a user