Files
coze-studio/backend/pkg/saasapi/client.go
2025-10-15 20:06:32 +08:00

281 lines
7.3 KiB
Go

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package saasapi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
// CozeAPIClient represents a client for coze.cn OpenAPI
type CozeAPIClient struct {
BaseURL string
APIKey string
HTTPClient *http.Client
MaxRetries int
}
// CozeAPIResponse represents the standard response format from coze.cn API
type CozeAPIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
// NewCozeAPIClient creates a new coze.cn API client
func NewCozeAPIClient() *CozeAPIClient {
return &CozeAPIClient{
BaseURL: getSaasOpenAPIUrl(),
APIKey: getSaasOpenAPIKey(),
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
MaxRetries: 1,
}
}
// Get performs a GET request to the coze.cn API
func (c *CozeAPIClient) Get(ctx context.Context, path string) (*CozeAPIResponse, error) {
return c.request(ctx, "GET", path, nil)
}
// GetWithQuery performs a GET request to the coze.cn API with query parameters
func (c *CozeAPIClient) GetWithQuery(ctx context.Context, path string, queryParams map[string]interface{}) (*CozeAPIResponse, error) {
return c.requestWithQuery(ctx, "GET", path, nil, queryParams)
}
// Post performs a POST request to the coze.cn API
func (c *CozeAPIClient) Post(ctx context.Context, path string, body interface{}) (*CozeAPIResponse, error) {
var bodyBytes []byte
var err error
if body != nil {
bodyBytes, err = json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
}
return c.request(ctx, "POST", path, bodyBytes)
}
// request is the core method for making HTTP requests to coze.cn API
func (c *CozeAPIClient) request(ctx context.Context, method, path string, body []byte) (*CozeAPIResponse, error) {
url := fmt.Sprintf("%s%s", c.BaseURL, path)
var req *http.Request
var err error
if body != nil {
req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
} else {
req, err = http.NewRequestWithContext(ctx, method, url, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Add API key if available
if c.APIKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
}
// Make request with retries
var resp *http.Response
for i := 0; i <= c.MaxRetries; i++ {
resp, err = c.HTTPClient.Do(req)
if err == nil {
break
}
if i < c.MaxRetries {
logs.CtxWarnf(ctx, "coze API request failed, retrying (%d/%d): %v", i+1, c.MaxRetries, err)
time.Sleep(time.Duration(i+1) * time.Second)
}
}
if err != nil {
return nil, fmt.Errorf("request failed after %d retries: %w", c.MaxRetries, err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Check HTTP status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var apiResp CozeAPIResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse API response: %w", err)
}
// Check API response code
if apiResp.Code != 0 {
return nil, fmt.Errorf("API returned error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
}
return &apiResp, nil
}
// requestWithQuery is the core method for making HTTP requests to coze.cn API with query parameters
func (c *CozeAPIClient) requestWithQuery(ctx context.Context, method, path string, body []byte, queryParams map[string]interface{}) (*CozeAPIResponse, error) {
baseURL := fmt.Sprintf("%s%s", c.BaseURL, path)
// Build query parameters
if len(queryParams) > 0 {
u, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
q := u.Query()
for key, value := range queryParams {
if value != nil {
switch v := value.(type) {
case string:
if v != "" {
q.Set(key, v)
}
case int:
q.Set(key, strconv.Itoa(v))
case bool:
q.Set(key, strconv.FormatBool(v))
case *string:
if v != nil && *v != "" {
q.Set(key, *v)
}
case *int:
if v != nil {
q.Set(key, strconv.Itoa(*v))
}
case *bool:
if v != nil {
q.Set(key, strconv.FormatBool(*v))
}
}
}
}
u.RawQuery = q.Encode()
baseURL = u.String()
}
var req *http.Request
var err error
if body != nil {
req, err = http.NewRequestWithContext(ctx, method, baseURL, bytes.NewReader(body))
} else {
req, err = http.NewRequestWithContext(ctx, method, baseURL, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Add API key if available
if c.APIKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
req.Header.Set("X-TT-ENV", "ppe_cozeopenmcp")
req.Header.Set("X-USE-PPE", "1")
}
// Make request with retries
var resp *http.Response
for i := 0; i <= c.MaxRetries; i++ {
resp, err = c.HTTPClient.Do(req)
if err == nil {
break
}
if i < c.MaxRetries {
logs.CtxWarnf(ctx, "coze API request failed, retrying (%d/%d): %v", i+1, c.MaxRetries, err)
time.Sleep(time.Duration(i+1) * time.Second)
}
}
if err != nil {
return nil, fmt.Errorf("request failed after %d retries: %w", c.MaxRetries, err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Check HTTP status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var apiResp CozeAPIResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse API response: %w", err)
}
// Check API response code
if apiResp.Code != 0 {
return nil, fmt.Errorf("API returned error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
}
return &apiResp, nil
}
// getEnvOrDefault returns environment variable value or default if not set
func getSaasOpenAPIUrl() string {
if value := os.Getenv(consts.CozeSaasAPIBaseURL); value != "" {
return value
}
return "https://api.coze.cn"
}
func getSaasOpenAPIKey() string {
if value := os.Getenv(consts.CozeSaasAPIKey); value != "" {
return value
}
return ""
}