Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
1.7 KiB
Go
73 lines
1.7 KiB
Go
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
|
|
}
|