340 lines
9.9 KiB
Go
340 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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"
|
|
)
|
|
|
|
const version = "1.0.0"
|
|
|
|
func main() {
|
|
// Load configuration
|
|
configPath := os.Getenv("CONFIG_PATH")
|
|
if configPath == "" {
|
|
configPath = "configs/config.yaml"
|
|
}
|
|
|
|
cfg, err := config.LoadConfig(configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize logger
|
|
log := logger.NewLogger(cfg.Logging.Level, cfg.Logging.Format)
|
|
log.Info("SEO Optimizer starting",
|
|
"version", version,
|
|
"config", configPath,
|
|
)
|
|
|
|
// Initialize WordPress client
|
|
wpClient := wordpress.NewClient(
|
|
cfg.WordPress.BaseURL,
|
|
cfg.WordPress.Username,
|
|
cfg.WordPress.AppPassword,
|
|
cfg.WordPress.Timeout,
|
|
log,
|
|
)
|
|
|
|
log.Info("WordPress client initialized",
|
|
"base_url", cfg.WordPress.BaseURL,
|
|
"username", cfg.WordPress.Username,
|
|
)
|
|
|
|
// Initialize LLM client
|
|
var llmClient agent.LLMClient
|
|
switch cfg.LLM.Provider {
|
|
case "claude":
|
|
llmClient = agent.NewClaudeClient(
|
|
cfg.Claude.APIKey,
|
|
cfg.Claude.Model,
|
|
cfg.Claude.MaxTokens,
|
|
cfg.Claude.Temperature,
|
|
log,
|
|
)
|
|
log.Info("Claude client initialized",
|
|
"model", cfg.Claude.Model,
|
|
"max_tokens", cfg.Claude.MaxTokens,
|
|
)
|
|
case "deepseek":
|
|
llmClient = agent.NewDeepSeekClient(
|
|
cfg.DeepSeek.APIKey,
|
|
cfg.DeepSeek.BaseURL,
|
|
cfg.DeepSeek.Model,
|
|
cfg.DeepSeek.MaxTokens,
|
|
cfg.DeepSeek.Temperature,
|
|
log,
|
|
)
|
|
log.Info("DeepSeek client initialized",
|
|
"model", cfg.DeepSeek.Model,
|
|
"max_tokens", cfg.DeepSeek.MaxTokens,
|
|
"base_url", cfg.DeepSeek.BaseURL,
|
|
)
|
|
default:
|
|
log.Error("Unsupported LLM provider", "provider", cfg.LLM.Provider)
|
|
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 {
|
|
log.Error("Failed to initialize storage", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Storage initialized", "base_path", cfg.Storage.BasePath)
|
|
|
|
// Initialize audit storage
|
|
auditStore := storage.NewAuditStorage(cfg.Audit.BasePath, log)
|
|
if err := auditStore.Initialize(); err != nil {
|
|
log.Error("Failed to initialize audit storage", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Audit storage initialized", "base_path", cfg.Audit.BasePath)
|
|
|
|
// Initialize idea storage
|
|
ideaStore := storage.NewIdeaStorage(cfg.Ideas.BasePath, log)
|
|
if err := ideaStore.Initialize(); err != nil {
|
|
log.Error("Failed to initialize idea storage", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Idea storage initialized", "base_path", cfg.Ideas.BasePath)
|
|
|
|
// Initialize outreach storage
|
|
outreachStore := storage.NewOutreachStorage(cfg.Outreach.BasePath, log)
|
|
if err := outreachStore.Initialize(); err != nil {
|
|
log.Error("Failed to initialize outreach storage", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Info("Outreach storage initialized", "base_path", cfg.Outreach.BasePath)
|
|
|
|
// Initialize newsletter storage
|
|
newsStore := storage.NewNewsletterStorage(cfg.Newsletter.BasePath, log)
|
|
if err := newsStore.Initialize(); err != nil {
|
|
log.Error("Failed to initialize newsletter storage", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
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")
|
|
|
|
// Context with cancellation for graceful shutdown
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Start API server if enabled
|
|
var apiServer *api.Server
|
|
if cfg.API.Enabled {
|
|
apiServer = api.NewServer(optimizer, wpClient, store, auditStore, ideaService, newsService, linkService, imageAuditService, translationService, version, cfg.API.Port, log)
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
if err := apiServer.Start(); err != nil && err != http.ErrServerClosed {
|
|
log.Error("API server failed", "error", err)
|
|
}
|
|
}()
|
|
|
|
// Start periodic job cleanup
|
|
go apiServer.StartPeriodicCleanup(ctx, 1*time.Hour, 24*time.Hour)
|
|
|
|
log.Info("API server started", "port", cfg.API.Port)
|
|
}
|
|
|
|
// Start scheduler if enabled
|
|
var sched *scheduler.Scheduler
|
|
if cfg.Scheduler.Enabled {
|
|
sched = scheduler.NewScheduler(
|
|
optimizer,
|
|
cfg.Scheduler.IntervalHours,
|
|
cfg.Scheduler.Languages,
|
|
log,
|
|
)
|
|
|
|
go func() {
|
|
if err := sched.Start(ctx); err != nil && err != context.Canceled {
|
|
log.Error("Scheduler failed", "error", err)
|
|
}
|
|
}()
|
|
|
|
log.Info("Scheduler started",
|
|
"interval", fmt.Sprintf("%.1fh", cfg.Scheduler.IntervalHours),
|
|
"languages", cfg.Scheduler.Languages,
|
|
)
|
|
}
|
|
|
|
// 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, auditStore, log)
|
|
go func() {
|
|
if err := ideaSched.Start(ctx); err != nil && err != context.Canceled {
|
|
log.Error("Idea scheduler failed", "error", err)
|
|
}
|
|
}()
|
|
log.Info("Idea scheduler started", "interval", cfg.Ideas.IntervalHours)
|
|
}
|
|
|
|
// Start newsletter scheduler if enabled
|
|
var newsSched *scheduler.NewsletterScheduler
|
|
if cfg.Newsletter.Enabled {
|
|
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)
|
|
}
|
|
}()
|
|
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)
|
|
|
|
sig := <-sigCh
|
|
log.Info("Shutdown signal received", "signal", sig.String())
|
|
|
|
// Graceful shutdown
|
|
log.Info("Initiating graceful shutdown...")
|
|
|
|
// Cancel context to stop scheduler
|
|
cancel()
|
|
|
|
// Shutdown API server if running
|
|
if apiServer != nil {
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := apiServer.Shutdown(shutdownCtx); err != nil {
|
|
log.Error("API server shutdown error", "error", err)
|
|
} else {
|
|
log.Info("API server shut down successfully")
|
|
}
|
|
}
|
|
|
|
// Stop scheduler
|
|
if sched != nil {
|
|
sched.Stop()
|
|
log.Info("Scheduler stopped")
|
|
}
|
|
|
|
if ideaSched != nil {
|
|
ideaSched.Stop()
|
|
log.Info("Idea scheduler stopped")
|
|
}
|
|
|
|
if newsSched != nil {
|
|
newsSched.Stop()
|
|
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")
|
|
}
|