feat(imitation): add article imitation create flow

Introduce 仿写创作: new list and create views, backend imitation service
and prompt templates, worker task routing for imitation jobs, and one-click
rewrite triggers from question citation sources. Surface source article
URL/title on article list/detail, and restrict failed articles to delete-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:36:44 +08:00
parent 46d0a7aea0
commit 40d9a6cc63
29 changed files with 2678 additions and 120 deletions
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/jackc/pgx/v5"
@@ -28,6 +29,7 @@ type ArticleGenerationWorker struct {
rabbitMQ *rabbitmq.Client
templateService *tenantapp.TemplateService
promptRuleService *tenantapp.PromptRuleGenerationService
imitationService *tenantapp.ArticleImitationService
kolWorker *KolGenerationWorker
logger *zap.Logger
retryInterval time.Duration
@@ -41,6 +43,7 @@ func NewArticleGenerationWorker(
rabbitMQClient *rabbitmq.Client,
templateService *tenantapp.TemplateService,
promptRuleService *tenantapp.PromptRuleGenerationService,
imitationService *tenantapp.ArticleImitationService,
kolWorker *KolGenerationWorker,
logger *zap.Logger,
cfg config.GenerationConfig,
@@ -60,6 +63,7 @@ func NewArticleGenerationWorker(
rabbitMQ: rabbitMQClient,
templateService: templateService,
promptRuleService: promptRuleService,
imitationService: imitationService,
kolWorker: kolWorker,
logger: logger,
retryInterval: defaultArticleGenerationWorkerRetryInterval,
@@ -145,11 +149,7 @@ func (w *ArticleGenerationWorker) handleDelivery(parent context.Context, deliver
return
}
if task.ID > 0 {
if task.TaskType == kolGenerationTaskType && w.kolWorker != nil {
w.kolWorker.HandleTaskFailure(ctx, task, "task_load", err)
} else {
_ = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, err)
}
w.handleTaskFailure(ctx, task, "task_load", err)
if ackErr := delivery.Ack(false); ackErr != nil && w.logger != nil {
w.logger.Warn("article generation ack failed after task load failure",
zap.Error(ackErr),
@@ -172,25 +172,28 @@ func (w *ArticleGenerationWorker) handleDelivery(parent context.Context, deliver
return
}
task.TaskType = strings.TrimSpace(task.TaskType)
switch {
case task.ArticleID <= 0 || task.QuotaReservationID <= 0:
if task.TaskType == kolGenerationTaskType && w.kolWorker != nil {
w.kolWorker.HandleTaskFailure(ctx, task, "task_load", fmt.Errorf("generation task references are incomplete"))
} else {
_ = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, fmt.Errorf("generation task references are incomplete"))
}
w.handleTaskFailure(ctx, task, "task_load", fmt.Errorf("generation task references are incomplete"))
case task.TaskType == "template":
w.templateService.ProcessQueuedTask(ctx, task)
case task.TaskType == "custom_generation":
w.promptRuleService.ProcessQueuedTask(ctx, task)
case task.TaskType == "imitation":
if w.imitationService != nil {
w.imitationService.ProcessQueuedTask(ctx, task)
} else {
w.handleTaskFailure(ctx, task, "task_load", fmt.Errorf("imitation generation worker is not configured"))
}
case task.TaskType == kolGenerationTaskType:
if w.kolWorker != nil {
w.kolWorker.Process(ctx, task)
} else {
_ = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, fmt.Errorf("kol generation worker is not configured"))
w.handleTaskFailure(ctx, task, "task_load", fmt.Errorf("kol generation worker is not configured"))
}
default:
_ = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, fmt.Errorf("unsupported generation task type: %s", task.TaskType))
w.handleTaskFailure(ctx, task, "task_load", fmt.Errorf("unsupported generation task type: %s", task.TaskType))
}
if err := delivery.Ack(false); err != nil && w.logger != nil {
@@ -202,6 +205,18 @@ func (w *ArticleGenerationWorker) handleDelivery(parent context.Context, deliver
}
}
func (w *ArticleGenerationWorker) handleTaskFailure(ctx context.Context, task tenantapp.ClaimedGenerationTask, stage string, taskErr error) {
task.TaskType = strings.TrimSpace(task.TaskType)
switch {
case task.TaskType == kolGenerationTaskType && w.kolWorker != nil:
w.kolWorker.HandleTaskFailure(ctx, task, stage, taskErr)
case task.TaskType == "imitation" && w.imitationService != nil:
w.imitationService.HandleTaskFailure(ctx, task, stage, taskErr)
default:
_ = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, taskErr)
}
}
func (w *ArticleGenerationWorker) loadTask(ctx context.Context, taskID int64) (tenantapp.ClaimedGenerationTask, bool, error) {
var (
task tenantapp.ClaimedGenerationTask