feat: CI/CD pipeline + Learning/Medicine/Travel services
- Add Gitea Actions workflow for automated build & deploy - Add K8s manifests: webui, travel-svc, medicine-svc, sandbox-svc - Update kustomization for localhost:5000 registry - Add ingress for gooseek.ru and api.gooseek.ru - Learning cabinet with onboarding, courses, sandbox integration - Medicine service with symptom analysis and doctor matching - Travel service with itinerary planning - Server setup scripts (NVIDIA/CUDA, K3s, Gitea runner) Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -40,6 +41,24 @@ func NewTravelDataClient(baseURL string) *TravelDataClient {
|
||||
}
|
||||
|
||||
func (c *TravelDataClient) doWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
// http.Request.Clone does NOT recreate the Body. If we retry a request with a Body,
|
||||
// we must be able to recreate it; otherwise retries will send an empty body and may
|
||||
// fail with ContentLength/body length mismatch.
|
||||
var bodyCopy []byte
|
||||
if req.Body != nil && req.GetBody == nil {
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read request body for retry: %w", err)
|
||||
}
|
||||
_ = req.Body.Close()
|
||||
bodyCopy = b
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(bodyCopy)), nil
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(bodyCopy))
|
||||
req.ContentLength = int64(len(bodyCopy))
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
@@ -51,7 +70,18 @@ func (c *TravelDataClient) doWithRetry(ctx context.Context, req *http.Request) (
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req.Clone(ctx))
|
||||
reqAttempt := req.Clone(ctx)
|
||||
if req.GetBody != nil {
|
||||
rc, err := req.GetBody()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
reqAttempt.Body = rc
|
||||
reqAttempt.ContentLength = req.ContentLength
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(reqAttempt)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
@@ -306,13 +336,16 @@ func (c *TravelDataClient) SearchHotels(ctx context.Context, lat, lng float64, c
|
||||
|
||||
// PlaceResult represents a place from 2GIS Places API.
|
||||
type PlaceResult struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
Type string `json:"type"`
|
||||
Purpose string `json:"purpose"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
Type string `json:"type"`
|
||||
Purpose string `json:"purpose"`
|
||||
Rating float64 `json:"rating"`
|
||||
ReviewCount int `json:"reviewCount"`
|
||||
Schedule map[string]string `json:"schedule,omitempty"`
|
||||
}
|
||||
|
||||
func (c *TravelDataClient) SearchPlaces(ctx context.Context, query string, lat, lng float64, radius int) ([]PlaceResult, error) {
|
||||
|
||||
Reference in New Issue
Block a user