client.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package agentserver
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "time"
  13. )
  14. // ErrNotFound 上游返回 404
  15. var ErrNotFound = errors.New("agentserver: not found")
  16. // Client AgentServer HTTP 客户端,使用平台 API Key 鉴权。
  17. // 管理类请求(会话创建/改名/删除、归属反查)使用带超时的 http.Client;
  18. // 代理转发(含 SSE 长连接)使用不设整体超时的 streamHTTP,由调用方的 context 控制生命周期。
  19. type Client struct {
  20. baseURL string
  21. apiKey string
  22. http *http.Client
  23. streamHTTP *http.Client
  24. }
  25. // NewClient 创建客户端;baseURL 形如 http://127.0.0.1:8787(不含 /v1)
  26. func NewClient(baseURL, apiKey string) *Client {
  27. transport := &http.Transport{
  28. Proxy: nil, // 内网服务,不走系统代理
  29. MaxIdleConns: 100,
  30. MaxIdleConnsPerHost: 50,
  31. IdleConnTimeout: 90 * time.Second,
  32. }
  33. return &Client{
  34. baseURL: strings.TrimRight(baseURL, "/"),
  35. apiKey: apiKey,
  36. http: &http.Client{Transport: transport, Timeout: 30 * time.Second},
  37. streamHTTP: &http.Client{Transport: transport},
  38. }
  39. }
  40. // Configured 是否已配置地址和 API Key
  41. func (c *Client) Configured() bool {
  42. return c != nil && c.baseURL != "" && c.apiKey != ""
  43. }
  44. // Forward 转发一次请求(代理用):path 以 /v1/ 开头,rawQuery 不含问号。
  45. // 返回的响应由调用方负责关闭 Body。
  46. func (c *Client) Forward(ctx context.Context, method, path, rawQuery string, header http.Header, body io.Reader) (*http.Response, error) {
  47. u := c.baseURL + path
  48. if rawQuery != "" {
  49. u += "?" + rawQuery
  50. }
  51. req, err := http.NewRequestWithContext(ctx, method, u, body)
  52. if err != nil {
  53. return nil, err
  54. }
  55. for k, vs := range header {
  56. for _, v := range vs {
  57. req.Header.Add(k, v)
  58. }
  59. }
  60. req.Header.Set("Authorization", "Bearer "+c.apiKey)
  61. return c.streamHTTP.Do(req)
  62. }
  63. // CreateSession POST /v1/sessions;profileId 为空时使用 API Key 的默认 Profile
  64. func (c *Client) CreateSession(ctx context.Context, profileId string) (*SessionDto, error) {
  65. body := map[string]any{}
  66. if profileId != "" {
  67. body["profileId"] = profileId
  68. }
  69. var out SessionDto
  70. if err := c.doJSON(ctx, http.MethodPost, "/v1/sessions", body, &out); err != nil {
  71. return nil, err
  72. }
  73. return &out, nil
  74. }
  75. // PatchSession PATCH /v1/sessions/:id
  76. func (c *Client) PatchSession(ctx context.Context, sessionId string, body map[string]any) error {
  77. return c.doJSON(ctx, http.MethodPatch, "/v1/sessions/"+url.PathEscape(sessionId), body, nil)
  78. }
  79. // DeleteSession DELETE /v1/sessions/:id;上游已不存在时视为成功
  80. func (c *Client) DeleteSession(ctx context.Context, sessionId string) error {
  81. err := c.doJSON(ctx, http.MethodDelete, "/v1/sessions/"+url.PathEscape(sessionId), nil, nil)
  82. if errors.Is(err, ErrNotFound) {
  83. return nil
  84. }
  85. return err
  86. }
  87. // GetRun GET /v1/runs/:id(归属反查用)
  88. func (c *Client) GetRun(ctx context.Context, runId string) (*RunDto, error) {
  89. var out RunDto
  90. if err := c.doJSON(ctx, http.MethodGet, "/v1/runs/"+url.PathEscape(runId), nil, &out); err != nil {
  91. return nil, err
  92. }
  93. return &out, nil
  94. }
  95. // ListFiles GET /v1/sessions/:id/files(归属反查用)
  96. func (c *Client) ListFiles(ctx context.Context, sessionId string) ([]FileDto, error) {
  97. var out []FileDto
  98. if err := c.doJSON(ctx, http.MethodGet, "/v1/sessions/"+url.PathEscape(sessionId)+"/files", nil, &out); err != nil {
  99. return nil, err
  100. }
  101. return out, nil
  102. }
  103. // PendingInteractions GET /v1/interactions?sessionId=(归属反查用)
  104. func (c *Client) PendingInteractions(ctx context.Context, sessionId string) ([]InteractionRefDto, error) {
  105. var out []InteractionRefDto
  106. if err := c.doJSON(ctx, http.MethodGet, "/v1/interactions?sessionId="+url.QueryEscape(sessionId), nil, &out); err != nil {
  107. return nil, err
  108. }
  109. return out, nil
  110. }
  111. func (c *Client) doJSON(ctx context.Context, method, path string, in any, out any) error {
  112. var body io.Reader
  113. if in != nil {
  114. b, err := json.Marshal(in)
  115. if err != nil {
  116. return err
  117. }
  118. body = bytes.NewReader(b)
  119. }
  120. req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
  121. if err != nil {
  122. return err
  123. }
  124. req.Header.Set("Authorization", "Bearer "+c.apiKey)
  125. if in != nil {
  126. req.Header.Set("Content-Type", "application/json")
  127. }
  128. resp, err := c.http.Do(req)
  129. if err != nil {
  130. return fmt.Errorf("请求 AgentServer 失败: %w", err)
  131. }
  132. defer resp.Body.Close()
  133. data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
  134. if resp.StatusCode == http.StatusNotFound {
  135. return ErrNotFound
  136. }
  137. if resp.StatusCode >= 300 {
  138. var eb ErrorBody
  139. if json.Unmarshal(data, &eb) == nil && eb.Error.Message != "" {
  140. return fmt.Errorf("AgentServer %d %s: %s", resp.StatusCode, eb.Error.Code, eb.Error.Message)
  141. }
  142. return fmt.Errorf("AgentServer %d: %s", resp.StatusCode, truncate(string(data), 200))
  143. }
  144. if out != nil && len(data) > 0 {
  145. if err := json.Unmarshal(data, out); err != nil {
  146. return fmt.Errorf("解析 AgentServer 响应失败: %w", err)
  147. }
  148. }
  149. return nil
  150. }
  151. func truncate(s string, n int) string {
  152. if len(s) <= n {
  153. return s
  154. }
  155. return s[:n] + "…"
  156. }