/
amne
/
dca-agent
Обзор
Документация
Войти
/
amne
/
dca-agent
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
api/client.go
245 строк
7 KB
СеребристыйМак
feat: scoring fixes, filters, period analytics, weekly-report endpoint, monitor
13 июл 2026, 22:59
13 июл 2026, 22:59
bbb6a6f
Код
Авторство
О чём код?
package api import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "time" ) // AdminClient communicates with the admin server type AdminClient struct { BaseURL string Token string ClientID string Version string HTTP *http.Client } // RemoteConfig is the config received from admin server (NO API KEYS) type RemoteConfig struct { NormalizeTmpl string `json:"normalize_template"` ReportTmpl string `json:"report_template"` LLMModel string `json:"llm_model"` LLMTemp float64 `json:"llm_temperature"` MaxFileSize int64 `json:"max_file_size_mb"` MaxCallsDay int `json:"max_calls_per_day"` } // CallStat is billing data sent to admin after processing type CallStat struct { Filename string `json:"filename"` DurationSec float64 `json:"duration_sec"` TokensIn int `json:"tokens_in"` TokensOut int `json:"tokens_out"` Status string `json:"status"` Error string `json:"error,omitempty"` } // HeartbeatPayload is sent periodically type HeartbeatPayload struct { Status string `json:"status"` QueueSize int `json:"queue_size"` ProcessedToday int `json:"processed_today"` Uptime int64 `json:"uptime_seconds"` } // SubscriptionInfo is the client's subscription status from admin type SubscriptionInfo struct { Status string `json:"status"` DaysLeft int `json:"days_left"` DaysLimit int `json:"days_limit"` MinutesUsed int `json:"minutes_used"` MinutesLimit int `json:"minutes_limit"` } func NewAdminClient(baseURL, token, clientID, version string) *AdminClient { return &AdminClient{ BaseURL: baseURL, Token: token, ClientID: clientID, Version: version, HTTP: &http.Client{Timeout: 30 * time.Second}, } } // Register registers this agent with the admin server, returns remote config func (a *AdminClient) Register() (*RemoteConfig, error) { body := map[string]string{ "client_id": a.ClientID, "version": a.Version, } return a.doPOST("/api/agent/register", body) } // FetchConfig gets latest config from admin server func (a *AdminClient) FetchConfig() (*RemoteConfig, error) { return a.doPOST("/api/agent/config", map[string]string{"client_id": a.ClientID}) } // SendStat sends billing data after processing func (a *AdminClient) SendStat(stat *CallStat) error { _, err := a.doPOST("/api/agent/report", stat) if err != nil { return fmt.Errorf("send stat: %w", err) } return nil } // SendHeartbeat sends periodic status update func (a *AdminClient) SendHeartbeat(payload *HeartbeatPayload) error { _, err := a.doPOST("/api/agent/heartbeat", payload) if err != nil { return fmt.Errorf("heartbeat: %w", err) } return nil } // GenerateWeeklyReport sends aggregated call data to admin server for LLM processing. // Server returns generated markdown report. Returns (markdown, error). func (a *AdminClient) GenerateWeeklyReport(reportType, from, to, operator string, callData json.RawMessage) (string, error) { body := map[string]interface{}{ "client_id": a.ClientID, "report_type": reportType, "date_from": from, "date_to": to, "operator": operator, "calls": callData, } url := a.BaseURL + "/api/agent/weekly-report" jsonBody, _ := json.Marshal(body) req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+a.Token) req.Header.Set("X-Client-ID", a.ClientID) // Reports can take a while — longer timeout client := &http.Client{Timeout: 5 * time.Minute} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("send report request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("read response: %w", err) } if resp.StatusCode != 200 { return "", fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBody[:min(len(respBody), 500)])) } // Response: { "markdown": "..." } var result struct { Markdown string `json:"markdown"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("parse response: %w", err) } return result.Markdown, nil } // FetchSubscription gets subscription status from admin func (a *AdminClient) FetchSubscription() (*SubscriptionInfo, error) { if a.BaseURL == "" { return nil, fmt.Errorf("no admin URL") } url := a.BaseURL + "/api/agent/subscription" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+a.Token) req.Header.Set("X-Client-ID", a.ClientID) resp, err := a.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(bodyBytes)) } var sub SubscriptionInfo if err := json.Unmarshal(bodyBytes, &sub); err != nil { return nil, fmt.Errorf("parse response: %w", err) } return &sub, nil } func (a *AdminClient) doPOST(path string, payload interface{}) (*RemoteConfig, error) { jsonBody, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest("POST", a.BaseURL+path, bytes.NewReader(jsonBody)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+a.Token) req.Header.Set("X-Client-ID", a.ClientID) resp, err := a.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode == 401 || resp.StatusCode == 403 { return nil, fmt.Errorf("auth failed (%d): %s", resp.StatusCode, string(bodyBytes)) } if resp.StatusCode != 200 && resp.StatusCode != 201 { return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(bodyBytes)) } // report and heartbeat don't return config if path == "/api/agent/report" || path == "/api/agent/heartbeat" { return nil, nil } var cfg RemoteConfig if err := json.Unmarshal(bodyBytes, &cfg); err != nil { return nil, fmt.Errorf("parse response: %w", err) } return &cfg, nil } // StartHeartbeat runs periodic heartbeat in background func (a *AdminClient) StartHeartbeat(interval time.Duration, getStatus func() *HeartbeatPayload) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { payload := getStatus() if err := a.SendHeartbeat(payload); err != nil { log.Printf("[admin] heartbeat failed: %v", err) } } }() log.Printf("[admin] heartbeat every %s", interval) }