feat(generation): migrate article generation and template-assist to RabbitMQ queues

Replace in-process generation with queue-based async execution via
RabbitMQ. Add generation task runtime for lifecycle management, article
generation payload/queue abstractions, and template-assist queue publisher.
Refactor prompt generation service to support batch generation with
per-item error tracking. Update stream hub to bridge queue events to SSE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 14:20:26 +08:00
parent e8f48c6d37
commit ce13331e26
14 changed files with 951 additions and 173 deletions
@@ -51,6 +51,7 @@ func NewArticleHandler(a *bootstrap.App) *ArticleHandler {
promptGenerateSvc: app.NewPromptRuleGenerationService(
a.DB,
a.LLM,
a.RabbitMQ,
knowledgeSvc,
a.GenerationStreams,
a.Config.Generation,
@@ -369,6 +370,11 @@ func (h *ArticleHandler) Stream(c *gin.Context) {
return
}
fallback := buildGenerationSnapshot(detail)
if snapshot, ok := h.streams.Snapshot(id); !ok || (fallback.Done && snapshot.Status != fallback.Status) {
h.streams.Seed(fallback)
}
events, snapshot, cancel := h.streams.Subscribe(id)
defer cancel()
@@ -377,20 +383,10 @@ func (h *ArticleHandler) Stream(c *gin.Context) {
return
}
flusher.Flush()
if snapshot.Done {
return
}
} else {
fallback := stream.GenerationEvent{
Type: "snapshot",
ArticleID: detail.ID,
Status: detail.GenerateStatus,
Done: detail.GenerateStatus == "completed" || detail.GenerateStatus == "failed",
UpdatedAt: time.Now().UTC(),
}
if detail.Title != nil {
fallback.Title = *detail.Title
}
if detail.MarkdownContent != nil {
fallback.Content = *detail.MarkdownContent
}
if err := writeSSE(c, fallback.Type, fallback); err != nil {
return
}
@@ -455,6 +451,32 @@ func writeSSE(c *gin.Context, event string, payload interface{}) error {
return nil
}
func buildGenerationSnapshot(detail *app.ArticleDetailResponse) stream.GenerationEvent {
event := stream.GenerationEvent{
Type: "snapshot",
ArticleID: detail.ID,
Status: detail.GenerateStatus,
Done: detail.GenerateStatus == "completed" || detail.GenerateStatus == "failed",
UpdatedAt: time.Now().UTC(),
}
if detail.Title != nil {
event.Title = *detail.Title
}
if detail.MarkdownContent != nil {
event.Content = *detail.MarkdownContent
}
if detail.GenerateStatus == "completed" {
event.Type = "completed"
}
if detail.GenerateStatus == "failed" {
event.Type = "error"
if detail.GenerationErrorMessage != nil {
event.Error = *detail.GenerationErrorMessage
}
}
return event
}
func resolveOptimizeStreamErrorMessage(appErr *response.AppError) string {
if appErr == nil {
return "optimize selection failed"