This commit is contained in:
2026-01-26 17:07:35 +01:00
parent ec82340f42
commit 6c185f2212
37 changed files with 3888 additions and 171 deletions
+21 -1
View File
@@ -91,6 +91,8 @@ func main() {
fmt.Fprintln(os.Stderr, " newsletter [status]")
fmt.Fprintln(os.Stderr, " newsletter generate")
fmt.Fprintln(os.Stderr, " newsletter delete <id>")
fmt.Fprintln(os.Stderr, " orphans")
fmt.Fprintln(os.Stderr, " translate <post_id> [language]")
fmt.Fprintln(os.Stderr, " audit [limit]")
fmt.Fprintln(os.Stderr, " version")
fmt.Fprintln(os.Stderr, " shell")
@@ -339,6 +341,24 @@ func executeCommand(c *client, cmd string, args []string) error {
status := args[0]
handleSimpleGet(c, "/api/v1/newsletter?status="+status)
}
case "orphans":
handleSimpleGet(c, "/api/v1/orphans?source=en&target=es")
case "translate":
if len(args) < 1 {
return fmt.Errorf("translate requires <post_id>")
}
postID, err := parseIntArg("post_id", args[0])
if err != nil {
return err
}
targetLang := "es"
if len(args) > 1 {
targetLang = args[1]
}
handleSimplePost(c, "/api/v1/translate", map[string]interface{}{
"post_id": postID,
"target_lang": targetLang,
})
case "audit":
limit := 100
if len(args) > 0 {
@@ -372,7 +392,7 @@ func runInteractive(c *client) {
return
}
if line == "help" {
fmt.Println("Commands: health, version, pending [summary|text <draft_id>], optimize <id> [lang], status <job_id>, changes <post_id>, approve <draft_id>, reject <draft_id>, ideas [status|generate|draft <id>|publish <id>|delete <id>], outreach [status|generate|delete <id>], newsletter [status|generate|delete <id>], audit [limit], exit")
fmt.Println("Commands: health, version, pending [summary|text <draft_id>], optimize <id> [lang], status <job_id>, changes <post_id>, approve <draft_id>, reject <draft_id>, ideas [status|generate|draft <id>|publish <id>|delete <id>], outreach [status|generate|delete <id>], newsletter [status|generate|delete <id>], orphans, translate <post_id> [lang], audit [limit], exit")
continue
}
tokens := strings.Fields(line)
+88 -3
View File
@@ -6,18 +6,22 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"seo-optimizer/internal/agent"
"seo-optimizer/internal/api"
"seo-optimizer/internal/brokenlinks"
"seo-optimizer/internal/config"
"seo-optimizer/internal/ideas"
"seo-optimizer/internal/imageaudit"
"seo-optimizer/internal/logger"
"seo-optimizer/internal/newsletter"
"seo-optimizer/internal/scheduler"
"seo-optimizer/internal/seo"
"seo-optimizer/internal/storage"
"seo-optimizer/internal/translation"
"seo-optimizer/internal/wordpress"
)
@@ -91,6 +95,22 @@ func main() {
os.Exit(1)
}
// Initialize DeepSeek client for translations (optional)
var translationClient *agent.DeepSeekClient
if strings.TrimSpace(cfg.DeepSeek.APIKey) != "" {
translationClient = agent.NewDeepSeekClient(
cfg.DeepSeek.APIKey,
cfg.DeepSeek.BaseURL,
cfg.DeepSeek.Model,
cfg.DeepSeek.MaxTokens,
cfg.DeepSeek.Temperature,
log,
)
log.Info("DeepSeek translation client initialized", "model", cfg.DeepSeek.Model)
} else {
log.Warn("DeepSeek API key missing; translation feature will be unavailable")
}
// Initialize storage
store := storage.NewOptimizationStorage(cfg.Storage.BasePath, log)
if err := store.Initialize(); err != nil {
@@ -131,12 +151,43 @@ func main() {
}
log.Info("Newsletter storage initialized", "base_path", cfg.Newsletter.BasePath)
// Initialize broken link storage
linkStore := storage.NewBrokenLinkStorage(cfg.LinkChecker.BasePath, log)
if err := linkStore.Initialize(); err != nil {
log.Error("Failed to initialize broken link storage", "error", err)
os.Exit(1)
}
log.Info("Broken link storage initialized", "base_path", cfg.LinkChecker.BasePath)
// Initialize image audit storage
imageAuditStore := storage.NewImageAuditStorage(cfg.ImageAudit.BasePath, log)
if err := imageAuditStore.Initialize(); err != nil {
log.Error("Failed to initialize image audit storage", "error", err)
os.Exit(1)
}
log.Info("Image audit storage initialized", "base_path", cfg.ImageAudit.BasePath)
// Initialize idea service
ideaService := ideas.NewService(wpClient, llmClient, nil, ideaStore, cfg.Ideas, outreachStore, cfg.Outreach, cfg.Runtime.DryRun, log)
// Initialize newsletter service
newsService := newsletter.NewService(llmClient, nil, newsStore, cfg.Newsletter, log)
// Initialize broken link service
linkService, err := brokenlinks.NewService(wpClient, linkStore, cfg.LinkChecker, log)
if err != nil {
log.Error("Failed to initialize broken link service", "error", err)
os.Exit(1)
}
imageAuditService, err := imageaudit.NewService(wpClient, imageAuditStore, cfg.ImageAudit, log)
if err != nil {
log.Error("Failed to initialize image audit service", "error", err)
os.Exit(1)
}
translationService := translation.NewService(wpClient, translationClient, log)
// Initialize optimizer
optimizer := seo.NewOptimizer(wpClient, llmClient, cfg.LLM.Provider, store, cfg, log)
log.Info("Optimizer initialized")
@@ -148,7 +199,7 @@ func main() {
// Start API server if enabled
var apiServer *api.Server
if cfg.API.Enabled {
apiServer = api.NewServer(optimizer, wpClient, store, auditStore, ideaService, newsService, version, cfg.API.Port, log)
apiServer = api.NewServer(optimizer, wpClient, store, auditStore, ideaService, newsService, linkService, imageAuditService, translationService, version, cfg.API.Port, log)
// Start server in goroutine
go func() {
@@ -188,7 +239,7 @@ func main() {
// Start idea scheduler if enabled
var ideaSched *scheduler.IdeaScheduler
if cfg.Ideas.Enabled || cfg.Outreach.Enabled {
ideaSched = scheduler.NewIdeaScheduler(ideaService, cfg.Ideas.IntervalHours, cfg.Ideas.Enabled, cfg.Outreach.Enabled, log)
ideaSched = scheduler.NewIdeaScheduler(ideaService, cfg.Ideas.IntervalHours, cfg.Ideas.Enabled, cfg.Outreach.Enabled, auditStore, log)
go func() {
if err := ideaSched.Start(ctx); err != nil && err != context.Canceled {
log.Error("Idea scheduler failed", "error", err)
@@ -200,7 +251,7 @@ func main() {
// Start newsletter scheduler if enabled
var newsSched *scheduler.NewsletterScheduler
if cfg.Newsletter.Enabled {
newsSched = scheduler.NewNewsletterScheduler(newsService, cfg.Newsletter.IntervalHours, log)
newsSched = scheduler.NewNewsletterScheduler(newsService, cfg.Newsletter.IntervalHours, auditStore, log)
go func() {
if err := newsSched.Start(ctx); err != nil && err != context.Canceled {
log.Error("Newsletter scheduler failed", "error", err)
@@ -209,6 +260,30 @@ func main() {
log.Info("Newsletter scheduler started", "interval", cfg.Newsletter.IntervalHours)
}
// Start broken link scheduler if enabled
var linkSched *scheduler.BrokenLinkScheduler
if cfg.LinkChecker.Enabled {
linkSched = scheduler.NewBrokenLinkScheduler(linkService, cfg.LinkChecker.IntervalHours, auditStore, log)
go func() {
if err := linkSched.Start(ctx); err != nil && err != context.Canceled {
log.Error("Broken link scheduler failed", "error", err)
}
}()
log.Info("Broken link scheduler started", "interval", cfg.LinkChecker.IntervalHours)
}
// Start image audit scheduler if enabled
var imageAuditSched *scheduler.ImageAuditScheduler
if cfg.ImageAudit.Enabled {
imageAuditSched = scheduler.NewImageAuditScheduler(imageAuditService, cfg.ImageAudit.IntervalHours, auditStore, log)
go func() {
if err := imageAuditSched.Start(ctx); err != nil && err != context.Canceled {
log.Error("Image audit scheduler failed", "error", err)
}
}()
log.Info("Image audit scheduler started", "interval", cfg.ImageAudit.IntervalHours)
}
// Wait for interrupt signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
@@ -250,5 +325,15 @@ func main() {
log.Info("Newsletter scheduler stopped")
}
if linkSched != nil {
linkSched.Stop()
log.Info("Broken link scheduler stopped")
}
if imageAuditSched != nil {
imageAuditSched.Stop()
log.Info("Image audit scheduler stopped")
}
log.Info("SEO Optimizer stopped gracefully")
}