| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package agentserver
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
- )
- // ErrNotFound 上游返回 404
- var ErrNotFound = errors.New("agentserver: not found")
- // Client AgentServer HTTP 客户端,使用平台 API Key 鉴权。
- // 管理类请求(会话创建/改名/删除、归属反查)使用带超时的 http.Client;
- // 代理转发(含 SSE 长连接)使用不设整体超时的 streamHTTP,由调用方的 context 控制生命周期。
- type Client struct {
- baseURL string
- apiKey string
- http *http.Client
- streamHTTP *http.Client
- }
- // NewClient 创建客户端;baseURL 形如 http://127.0.0.1:8787(不含 /v1)
- func NewClient(baseURL, apiKey string) *Client {
- transport := &http.Transport{
- Proxy: nil, // 内网服务,不走系统代理
- MaxIdleConns: 100,
- MaxIdleConnsPerHost: 50,
- IdleConnTimeout: 90 * time.Second,
- }
- return &Client{
- baseURL: strings.TrimRight(baseURL, "/"),
- apiKey: apiKey,
- http: &http.Client{Transport: transport, Timeout: 30 * time.Second},
- streamHTTP: &http.Client{Transport: transport},
- }
- }
- // Configured 是否已配置地址和 API Key
- func (c *Client) Configured() bool {
- return c != nil && c.baseURL != "" && c.apiKey != ""
- }
- // Forward 转发一次请求(代理用):path 以 /v1/ 开头,rawQuery 不含问号。
- // 返回的响应由调用方负责关闭 Body。
- func (c *Client) Forward(ctx context.Context, method, path, rawQuery string, header http.Header, body io.Reader) (*http.Response, error) {
- u := c.baseURL + path
- if rawQuery != "" {
- u += "?" + rawQuery
- }
- req, err := http.NewRequestWithContext(ctx, method, u, body)
- if err != nil {
- return nil, err
- }
- for k, vs := range header {
- for _, v := range vs {
- req.Header.Add(k, v)
- }
- }
- req.Header.Set("Authorization", "Bearer "+c.apiKey)
- return c.streamHTTP.Do(req)
- }
- // CreateSession POST /v1/sessions;profileId 为空时使用 API Key 的默认 Profile
- func (c *Client) CreateSession(ctx context.Context, profileId string) (*SessionDto, error) {
- body := map[string]any{}
- if profileId != "" {
- body["profileId"] = profileId
- }
- var out SessionDto
- if err := c.doJSON(ctx, http.MethodPost, "/v1/sessions", body, &out); err != nil {
- return nil, err
- }
- return &out, nil
- }
- // PatchSession PATCH /v1/sessions/:id
- func (c *Client) PatchSession(ctx context.Context, sessionId string, body map[string]any) error {
- return c.doJSON(ctx, http.MethodPatch, "/v1/sessions/"+url.PathEscape(sessionId), body, nil)
- }
- // DeleteSession DELETE /v1/sessions/:id;上游已不存在时视为成功
- func (c *Client) DeleteSession(ctx context.Context, sessionId string) error {
- err := c.doJSON(ctx, http.MethodDelete, "/v1/sessions/"+url.PathEscape(sessionId), nil, nil)
- if errors.Is(err, ErrNotFound) {
- return nil
- }
- return err
- }
- // GetRun GET /v1/runs/:id(归属反查用)
- func (c *Client) GetRun(ctx context.Context, runId string) (*RunDto, error) {
- var out RunDto
- if err := c.doJSON(ctx, http.MethodGet, "/v1/runs/"+url.PathEscape(runId), nil, &out); err != nil {
- return nil, err
- }
- return &out, nil
- }
- // ListFiles GET /v1/sessions/:id/files(归属反查用)
- func (c *Client) ListFiles(ctx context.Context, sessionId string) ([]FileDto, error) {
- var out []FileDto
- if err := c.doJSON(ctx, http.MethodGet, "/v1/sessions/"+url.PathEscape(sessionId)+"/files", nil, &out); err != nil {
- return nil, err
- }
- return out, nil
- }
- // PendingInteractions GET /v1/interactions?sessionId=(归属反查用)
- func (c *Client) PendingInteractions(ctx context.Context, sessionId string) ([]InteractionRefDto, error) {
- var out []InteractionRefDto
- if err := c.doJSON(ctx, http.MethodGet, "/v1/interactions?sessionId="+url.QueryEscape(sessionId), nil, &out); err != nil {
- return nil, err
- }
- return out, nil
- }
- func (c *Client) doJSON(ctx context.Context, method, path string, in any, out any) error {
- var body io.Reader
- if in != nil {
- b, err := json.Marshal(in)
- if err != nil {
- return err
- }
- body = bytes.NewReader(b)
- }
- req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
- if err != nil {
- return err
- }
- req.Header.Set("Authorization", "Bearer "+c.apiKey)
- if in != nil {
- req.Header.Set("Content-Type", "application/json")
- }
- resp, err := c.http.Do(req)
- if err != nil {
- return fmt.Errorf("请求 AgentServer 失败: %w", err)
- }
- defer resp.Body.Close()
- data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
- if resp.StatusCode == http.StatusNotFound {
- return ErrNotFound
- }
- if resp.StatusCode >= 300 {
- var eb ErrorBody
- if json.Unmarshal(data, &eb) == nil && eb.Error.Message != "" {
- return fmt.Errorf("AgentServer %d %s: %s", resp.StatusCode, eb.Error.Code, eb.Error.Message)
- }
- return fmt.Errorf("AgentServer %d: %s", resp.StatusCode, truncate(string(data), 200))
- }
- if out != nil && len(data) > 0 {
- if err := json.Unmarshal(data, out); err != nil {
- return fmt.Errorf("解析 AgentServer 响应失败: %w", err)
- }
- }
- return nil
- }
- func truncate(s string, n int) string {
- if len(s) <= n {
- return s
- }
- return s[:n] + "…"
- }
|