Files
home 08bd41e75c feat: travel service with 2GIS routing, POI, hotels + finance providers + UI overhaul
- Add travel-svc microservice (Amadeus, TravelPayouts, 2GIS, OpenRouteService)
- Add travel orchestrator with parallel collectors (events, POI, hotels, flights)
- Add 2GIS road routing with transport cost calculation (car/bus/taxi)
- Add TravelMap (2GIS MapGL) and TravelWidgets components
- Add useTravelChat hook for streaming travel agent responses
- Add finance heatmap providers refactor
- Add SearXNG settings, API proxy routes, Docker compose updates
- Update Dockerfiles, config, types, and all UI pages for consistency

Made-with: Cursor
2026-03-01 21:58:32 +03:00

243 lines
6.2 KiB
Go

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: true,
}))
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
}