package deerflow import ( "bytes" "encoding/json" "fmt" "io" "net/http" "sync" "time" "git.gozeros.com/wsk/eightbf-analysis-go/internal/config" ) // Auth 向 DeerFlow 换 Bearer,过期前复用。 type Auth struct { cfg config.Config mu sync.Mutex token string expiresAt time.Time httpClient *http.Client } func NewAuth(cfg config.Config) *Auth { return &Auth{ cfg: cfg, httpClient: &http.Client{Timeout: 30 * time.Second}, } } func (a *Auth) BearerToken() (string, error) { a.mu.Lock() defer a.mu.Unlock() now := time.Now() if a.token != "" && a.expiresAt.After(now.Add(2*time.Minute)) { return a.token, nil } body, _ := json.Marshal(map[string]string{ "username": a.cfg.DeerFlowUser, "password": a.cfg.DeerFlowPassword, }) req, err := http.NewRequest(http.MethodPost, a.cfg.DeerFlowBaseURL+"/api/v1/auth/login/username", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") resp, err := a.httpClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return "", fmt.Errorf("DeerFlow 登录失败 HTTP %d: %s", resp.StatusCode, string(raw)) } var parsed struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } if err := json.Unmarshal(raw, &parsed); err != nil { return "", err } if parsed.AccessToken == "" { return "", fmt.Errorf("DeerFlow 登录响应无 access_token") } if parsed.ExpiresIn <= 0 { parsed.ExpiresIn = 3600 } a.token = parsed.AccessToken a.expiresAt = now.Add(time.Duration(parsed.ExpiresIn) * time.Second) return a.token, nil }