package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gooseek/backend/pkg/config" "github.com/gooseek/backend/pkg/middleware" ) type ChatRequest struct { Message struct { MessageID string `json:"messageId"` ChatID string `json:"chatId"` Content string `json:"content"` } `json:"message"` OptimizationMode string `json:"optimizationMode"` Sources []string `json:"sources"` History [][]string `json:"history"` Files []string `json:"files"` ChatModel ChatModel `json:"chatModel"` EmbeddingModel ChatModel `json:"embeddingModel"` SystemInstructions string `json:"systemInstructions"` Locale string `json:"locale"` AnswerMode string `json:"answerMode"` ResponsePrefs *struct { Format string `json:"format"` Length string `json:"length"` Tone string `json:"tone"` } `json:"responsePrefs"` LearningMode bool `json:"learningMode"` } type ChatModel struct { ProviderID string `json:"providerId"` Key string `json:"key"` } func main() { cfg, err := config.Load() if err != nil { log.Fatal("Failed to load config:", err) } app := fiber.New(fiber.Config{ StreamRequestBody: true, BodyLimit: 50 * 1024 * 1024, ReadTimeout: time.Minute, WriteTimeout: 5 * time.Minute, IdleTimeout: 2 * time.Minute, }) app.Use(logger.New()) app.Use(cors.New()) app.Get("/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) }) app.Get("/ready", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ready"}) }) app.Get("/metrics", func(c *fiber.Ctx) error { c.Set("Content-Type", "text/plain; charset=utf-8") return c.SendString( "# HELP gooseek_up Service is up (1) or down (0)\n" + "# TYPE gooseek_up gauge\n" + "gooseek_up 1\n", ) }) app.Get("/api/v1/config", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "values": fiber.Map{ "version": 1, "setupComplete": true, "preferences": fiber.Map{}, }, "fields": fiber.Map{}, "modelProviders": []interface{}{}, "envOnlyMode": true, }) }) chat := app.Group("/api/v1/chat", middleware.JWT(middleware.JWTConfig{ Secret: cfg.JWTSecret, AuthSvcURL: cfg.AuthSvcURL, AllowGuest: false, })) chat.Post("/", func(c *fiber.Ctx) error { var req ChatRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } if req.Message.Content == "" { return c.Status(400).JSON(fiber.Map{"error": "Message content required"}) } isDiscoverSummary := strings.HasPrefix(req.Message.Content, "Summary: ") && len(req.Message.Content) > 9 var summaryURL string if isDiscoverSummary { summaryURL = strings.TrimSpace(strings.TrimPrefix(req.Message.Content, "Summary: ")) } agentURL := strings.TrimSuffix(cfg.AgentSvcURL, "/") + "/api/v1/agents/search" httpReq, err := http.NewRequest("POST", agentURL, bytes.NewReader(c.Body())) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } httpReq.Header.Set("Content-Type", "application/json") if auth := c.Get("Authorization"); auth != "" { httpReq.Header.Set("Authorization", auth) } client := &http.Client{Timeout: 5 * time.Minute} resp, err := client.Do(httpReq) if err != nil { log.Printf("Agent service error: %v", err) return c.Status(503).JSON(fiber.Map{"error": "Agent service unavailable"}) } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) resp.Body.Close() return c.Status(resp.StatusCode).Send(body) } c.Set("Content-Type", "application/x-ndjson") c.Set("Cache-Control", "no-cache") c.Set("Connection", "keep-alive") if isDiscoverSummary && summaryURL != "" && cfg.DiscoverSvcURL != "" { collected := make([]string, 0) scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 1024*1024), 10*1024*1024) var result bytes.Buffer for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) != "" { collected = append(collected, line) result.WriteString(line + "\n") } } resp.Body.Close() if len(collected) > 0 { go saveArticleSummary(cfg.DiscoverSvcURL, summaryURL, collected) } return c.SendString(result.String()) } return c.SendStream(resp.Body) }) port := cfg.ChatSvcPort log.Printf("chat-svc listening on :%d", port) log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } func init() { if os.Getenv("PORT") == "" { os.Setenv("PORT", "3005") } } func saveArticleSummary(discoverSvcURL, articleURL string, events []string) { if discoverSvcURL == "" || articleURL == "" || len(events) == 0 { return } payload := map[string]interface{}{ "url": articleURL, "events": events, } body, err := json.Marshal(payload) if err != nil { log.Printf("article-summary marshal error: %v", err) return } url := strings.TrimSuffix(discoverSvcURL, "/") + "/api/v1/discover/article-summary" maxRetries := 5 retryDelay := 2 * time.Second for attempt := 1; attempt <= maxRetries; attempt++ { req, err := http.NewRequest("POST", url, bytes.NewReader(body)) if err != nil { log.Printf("article-summary request error (attempt %d): %v", attempt, err) continue } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 2 * time.Minute} resp, err := client.Do(req) if err != nil { log.Printf("article-summary save error (attempt %d): %v", attempt, err) if attempt < maxRetries { time.Sleep(retryDelay) } continue } resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { log.Printf("article-summary saved: %s", articleURL[:min(60, len(articleURL))]) return } log.Printf("article-summary save failed (attempt %d): status %d", attempt, resp.StatusCode) if attempt < maxRetries { time.Sleep(retryDelay) } } log.Printf("article-summary save failed after %d retries: %s", maxRetries, articleURL[:min(60, len(articleURL))]) } func min(a, b int) int { if a < b { return a } return b }