package main import ( "bufio" "context" "encoding/json" "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/gooseek/backend/internal/computer" "github.com/gooseek/backend/internal/computer/connectors" "github.com/gooseek/backend/internal/db" "github.com/gooseek/backend/internal/llm" "github.com/gooseek/backend/pkg/config" "github.com/gooseek/backend/pkg/middleware" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/recover" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("Failed to load config: %v", err) } var database *db.PostgresDB maxRetries := 30 for i := 0; i < maxRetries; i++ { database, err = db.NewPostgresDB(cfg.DatabaseURL) if err == nil { log.Println("PostgreSQL connected successfully") break } log.Printf("Waiting for database (attempt %d/%d): %v", i+1, maxRetries, err) time.Sleep(2 * time.Second) } if err != nil { log.Fatalf("Failed to connect to database after %d attempts: %v", maxRetries, err) } taskRepo := db.NewComputerTaskRepo(database.DB()) memoryRepo := db.NewComputerMemoryRepo(database.DB()) artifactRepo := db.NewComputerArtifactRepo(database.DB()) if err := taskRepo.Migrate(); err != nil { log.Printf("Task repo migration warning: %v", err) } if err := memoryRepo.Migrate(); err != nil { log.Printf("Memory repo migration warning: %v", err) } if err := artifactRepo.Migrate(); err != nil { log.Printf("Artifact repo migration warning: %v", err) } registry := llm.NewModelRegistry() setupModels(registry, cfg) connectorHub := connectors.NewConnectorHub() setupConnectors(connectorHub, cfg) comp := computer.NewComputer(computer.ComputerConfig{ MaxParallelTasks: 10, MaxSubTasks: 20, TaskTimeout: 30 * time.Minute, SubTaskTimeout: 5 * time.Minute, TotalBudget: 1.0, EnableSandbox: true, EnableScheduling: true, SandboxImage: getEnv("SANDBOX_IMAGE", "gooseek/sandbox:latest"), }, computer.Dependencies{ Registry: registry, TaskRepo: taskRepo, MemoryRepo: memoryRepo, }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() comp.StartScheduler(ctx) app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) }, }) app.Use(recover.New()) app.Use(cors.New(cors.Config{ AllowOrigins: "*", AllowHeaders: "Origin, Content-Type, Accept, Authorization", AllowMethods: "GET, POST, PUT, DELETE, OPTIONS", })) app.Use(middleware.Logging(middleware.LoggingConfig{})) app.Get("/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "status": "ok", "service": "computer-svc", "models": registry.Count(), }) }) api := app.Group("/api/v1/computer") api.Post("/execute", func(c *fiber.Ctx) error { var req struct { Query string `json:"query"` UserID string `json:"userId"` Options computer.ExecuteOptions `json:"options"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "invalid request body"}) } if req.Query == "" { return c.Status(400).JSON(fiber.Map{"error": "query is required"}) } if req.UserID == "" || req.UserID == "anonymous" { req.UserID = "00000000-0000-0000-0000-000000000000" } task, err := comp.Execute(c.Context(), req.UserID, req.Query, req.Options) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(task) }) api.Get("/tasks", func(c *fiber.Ctx) error { userID := c.Query("userId", "") limit := c.QueryInt("limit", 20) offset := c.QueryInt("offset", 0) if userID == "" || userID == "anonymous" { userID = "00000000-0000-0000-0000-000000000000" } tasks, err := comp.GetUserTasks(c.Context(), userID, limit, offset) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "tasks": tasks, "count": len(tasks), }) }) api.Get("/tasks/:id", func(c *fiber.Ctx) error { taskID := c.Params("id") task, err := comp.GetStatus(c.Context(), taskID) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "task not found"}) } return c.JSON(task) }) api.Get("/tasks/:id/stream", func(c *fiber.Ctx) error { taskID := c.Params("id") c.Set("Content-Type", "text/event-stream") c.Set("Cache-Control", "no-cache") c.Set("Connection", "keep-alive") c.Set("Transfer-Encoding", "chunked") eventCh, err := comp.Stream(c.Context(), taskID) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { for event := range eventCh { data, _ := json.Marshal(event) fmt.Fprintf(w, "data: %s\n\n", data) w.Flush() } }) return nil }) api.Post("/tasks/:id/resume", func(c *fiber.Ctx) error { taskID := c.Params("id") var req struct { UserInput string `json:"userInput"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "invalid request body"}) } if err := comp.Resume(c.Context(), taskID, req.UserInput); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"status": "resumed"}) }) api.Delete("/tasks/:id", func(c *fiber.Ctx) error { taskID := c.Params("id") if err := comp.Cancel(c.Context(), taskID); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"status": "cancelled"}) }) api.Get("/tasks/:id/artifacts", func(c *fiber.Ctx) error { taskID := c.Params("id") artifacts, err := artifactRepo.GetByTaskID(c.Context(), taskID) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "artifacts": artifacts, "count": len(artifacts), }) }) api.Get("/artifacts/:id", func(c *fiber.Ctx) error { artifactID := c.Params("id") artifact, err := artifactRepo.GetByID(c.Context(), artifactID) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "artifact not found"}) } return c.JSON(artifact) }) api.Get("/artifacts/:id/download", func(c *fiber.Ctx) error { artifactID := c.Params("id") artifact, err := artifactRepo.GetByID(c.Context(), artifactID) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "artifact not found"}) } if artifact.MimeType != "" { c.Set("Content-Type", artifact.MimeType) } else { c.Set("Content-Type", "application/octet-stream") } c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", artifact.Name)) return c.Send(artifact.Content) }) api.Get("/models", func(c *fiber.Ctx) error { models := registry.GetAll() return c.JSON(fiber.Map{ "models": models, "count": len(models), }) }) api.Get("/connectors", func(c *fiber.Ctx) error { info := connectorHub.GetInfo() return c.JSON(fiber.Map{ "connectors": info, "count": len(info), }) }) api.Post("/connectors/:id/execute", func(c *fiber.Ctx) error { connectorID := c.Params("id") var req struct { Action string `json:"action"` Params map[string]interface{} `json:"params"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "invalid request body"}) } result, err := connectorHub.Execute(c.Context(), connectorID, req.Action, req.Params) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(result) }) port := getEnv("COMPUTER_SVC_PORT", "3030") addr := ":" + port go func() { log.Printf("Computer service starting on %s", addr) if err := app.Listen(addr); err != nil { log.Fatalf("Failed to start server: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down...") comp.StopScheduler() app.Shutdown() } func setupModels(registry *llm.ModelRegistry, cfg *config.Config) { // Timeweb Cloud AI (приоритетный провайдер для России) if cfg.TimewebAgentAccessID != "" && cfg.TimewebAPIKey != "" { timewebClient, err := llm.NewTimewebClient(llm.TimewebConfig{ ProviderID: "timeweb", ModelKey: "gpt-4o", BaseURL: cfg.TimewebAPIBaseURL, AgentAccessID: cfg.TimewebAgentAccessID, APIKey: cfg.TimewebAPIKey, ProxySource: cfg.TimewebProxySource, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "timeweb-gpt-4o", Provider: "timeweb", Model: "gpt-4o", Capabilities: []llm.ModelCapability{llm.CapSearch, llm.CapFast, llm.CapVision, llm.CapCoding, llm.CapCreative, llm.CapReasoning}, CostPer1K: 0.005, MaxContext: 128000, MaxTokens: 16384, Priority: 0, Description: "GPT-4o via Timeweb Cloud AI", }, timewebClient) log.Println("Timeweb GPT-4o registered") } else { log.Printf("Failed to create Timeweb client: %v", err) } timewebMiniClient, err := llm.NewTimewebClient(llm.TimewebConfig{ ProviderID: "timeweb", ModelKey: "gpt-4o-mini", BaseURL: cfg.TimewebAPIBaseURL, AgentAccessID: cfg.TimewebAgentAccessID, APIKey: cfg.TimewebAPIKey, ProxySource: cfg.TimewebProxySource, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "timeweb-gpt-4o-mini", Provider: "timeweb", Model: "gpt-4o-mini", Capabilities: []llm.ModelCapability{llm.CapFast, llm.CapCoding}, CostPer1K: 0.00015, MaxContext: 128000, MaxTokens: 16384, Priority: 0, Description: "GPT-4o-mini via Timeweb Cloud AI", }, timewebMiniClient) log.Println("Timeweb GPT-4o-mini registered") } } // OpenAI прямой (fallback если Timeweb недоступен) if cfg.OpenAIAPIKey != "" { openaiClient, err := llm.NewOpenAIClient(llm.ProviderConfig{ ProviderID: "openai", ModelKey: "gpt-4o", APIKey: cfg.OpenAIAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "gpt-4o", Provider: "openai", Model: "gpt-4o", Capabilities: []llm.ModelCapability{llm.CapSearch, llm.CapFast, llm.CapVision, llm.CapCoding, llm.CapCreative}, CostPer1K: 0.005, MaxContext: 128000, MaxTokens: 16384, Priority: 10, }, openaiClient) } miniClient, err := llm.NewOpenAIClient(llm.ProviderConfig{ ProviderID: "openai", ModelKey: "gpt-4o-mini", APIKey: cfg.OpenAIAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "gpt-4o-mini", Provider: "openai", Model: "gpt-4o-mini", Capabilities: []llm.ModelCapability{llm.CapFast, llm.CapCoding}, CostPer1K: 0.00015, MaxContext: 128000, MaxTokens: 16384, Priority: 10, }, miniClient) } } if cfg.AnthropicAPIKey != "" { opusClient, err := llm.NewAnthropicClient(llm.ProviderConfig{ ProviderID: "anthropic", ModelKey: "claude-3-opus-20240229", APIKey: cfg.AnthropicAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "claude-3-opus", Provider: "anthropic", Model: "claude-3-opus-20240229", Capabilities: []llm.ModelCapability{llm.CapReasoning, llm.CapCoding, llm.CapCreative, llm.CapLongContext}, CostPer1K: 0.015, MaxContext: 200000, MaxTokens: 4096, Priority: 1, }, opusClient) } sonnetClient, err := llm.NewAnthropicClient(llm.ProviderConfig{ ProviderID: "anthropic", ModelKey: "claude-3-5-sonnet-20241022", APIKey: cfg.AnthropicAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "claude-3-sonnet", Provider: "anthropic", Model: "claude-3-5-sonnet-20241022", Capabilities: []llm.ModelCapability{llm.CapCoding, llm.CapCreative, llm.CapFast}, CostPer1K: 0.003, MaxContext: 200000, MaxTokens: 8192, Priority: 1, }, sonnetClient) } } if cfg.GeminiAPIKey != "" { geminiClient, err := llm.NewGeminiClient(llm.ProviderConfig{ ProviderID: "gemini", ModelKey: "gemini-1.5-pro", APIKey: cfg.GeminiAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "gemini-1.5-pro", Provider: "gemini", Model: "gemini-1.5-pro", Capabilities: []llm.ModelCapability{llm.CapLongContext, llm.CapSearch, llm.CapVision, llm.CapMath}, CostPer1K: 0.00125, MaxContext: 2000000, MaxTokens: 8192, Priority: 1, }, geminiClient) } flashClient, err := llm.NewGeminiClient(llm.ProviderConfig{ ProviderID: "gemini", ModelKey: "gemini-1.5-flash", APIKey: cfg.GeminiAPIKey, }) if err == nil { registry.Register(llm.ModelSpec{ ID: "gemini-1.5-flash", Provider: "gemini", Model: "gemini-1.5-flash", Capabilities: []llm.ModelCapability{llm.CapFast, llm.CapVision}, CostPer1K: 0.000075, MaxContext: 1000000, MaxTokens: 8192, Priority: 2, }, flashClient) } } log.Printf("Registered %d models", registry.Count()) } func setupConnectors(hub *connectors.ConnectorHub, cfg *config.Config) { if smtpHost := getEnv("SMTP_HOST", ""); smtpHost != "" { emailConn := connectors.NewEmailConnector(connectors.EmailConfig{ SMTPHost: smtpHost, SMTPPort: getEnvInt("SMTP_PORT", 587), Username: getEnv("SMTP_USERNAME", ""), Password: getEnv("SMTP_PASSWORD", ""), FromAddress: getEnv("SMTP_FROM", ""), FromName: getEnv("SMTP_FROM_NAME", "GooSeek Computer"), UseTLS: true, AllowHTML: true, }) hub.Register(emailConn) log.Println("Email connector registered") } if botToken := getEnv("TELEGRAM_BOT_TOKEN", ""); botToken != "" { tgConn := connectors.NewTelegramConnector(connectors.TelegramConfig{ BotToken: botToken, }) hub.Register(tgConn) log.Println("Telegram connector registered") } webhookConn := connectors.NewWebhookConnector(connectors.WebhookConfig{ Timeout: 30 * time.Second, MaxRetries: 3, }) hub.Register(webhookConn) log.Println("Webhook connector registered") if s3Endpoint := getEnv("S3_ENDPOINT", ""); s3Endpoint != "" { storageConn, err := connectors.NewStorageConnector(connectors.StorageConfig{ Endpoint: s3Endpoint, AccessKeyID: getEnv("S3_ACCESS_KEY", ""), SecretAccessKey: getEnv("S3_SECRET_KEY", ""), BucketName: getEnv("S3_BUCKET", "gooseek-artifacts"), UseSSL: getEnv("S3_USE_SSL", "true") == "true", Region: getEnv("S3_REGION", "us-east-1"), PublicURL: getEnv("S3_PUBLIC_URL", ""), }) if err == nil { hub.Register(storageConn) log.Println("Storage connector registered") } } } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { var i int fmt.Sscanf(value, "%d", &i) return i } return defaultValue }