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] + "…" }