Files
cellbot/internal/adapter/milky/api_client.go

190 lines
4.5 KiB
Go
Raw Normal View History

package milky
import (
"context"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
// APIClient Milky API 客户端
// 用于调用协议端的 API (POST /api/:api)
type APIClient struct {
baseURL string
accessToken string
httpClient *fasthttp.Client
logger *zap.Logger
timeout time.Duration
retryCount int
}
// NewAPIClient 创建 API 客户端
func NewAPIClient(baseURL, accessToken string, timeout time.Duration, retryCount int, logger *zap.Logger) *APIClient {
if timeout == 0 {
timeout = 30 * time.Second
}
if retryCount == 0 {
retryCount = 3
}
return &APIClient{
baseURL: baseURL,
accessToken: accessToken,
httpClient: &fasthttp.Client{
ReadTimeout: timeout,
WriteTimeout: timeout,
MaxConnsPerHost: 100,
},
logger: logger.Named("api-client"),
timeout: timeout,
retryCount: retryCount,
}
}
// Call 调用 API
// endpoint: API 端点名称(如 "send_private_message"
// input: 输入参数(会被序列化为 JSON
// 返回: 响应数据和错误
func (c *APIClient) Call(ctx context.Context, endpoint string, input interface{}) (*APIResponse, error) {
// 序列化输入参数
var inputData []byte
var err error
if input == nil {
inputData = []byte("{}")
} else {
inputData, err = sonic.Marshal(input)
if err != nil {
return nil, fmt.Errorf("failed to marshal input: %w", err)
}
}
// 构建 URL
url := fmt.Sprintf("%s/api/%s", c.baseURL, endpoint)
c.logger.Debug("Calling API",
zap.String("endpoint", endpoint),
zap.String("url", url))
// 重试机制
var lastErr error
for i := 0; i <= c.retryCount; i++ {
if i > 0 {
c.logger.Info("Retrying API call",
zap.String("endpoint", endpoint),
zap.Int("attempt", i),
zap.Int("max", c.retryCount))
// 指数退避
backoff := time.Duration(i) * time.Second
select {
case <-time.After(backoff):
case <-ctx.Done():
return nil, ctx.Err()
}
}
resp, err := c.doRequest(ctx, url, inputData)
if err != nil {
lastErr = err
continue
}
return resp, nil
}
return nil, fmt.Errorf("API call failed after %d retries: %w", c.retryCount, lastErr)
}
// doRequest 执行单次请求
func (c *APIClient) doRequest(ctx context.Context, url string, inputData []byte) (*APIResponse, error) {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
// 设置请求
req.SetRequestURI(url)
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.SetBody(inputData)
// 设置 Authorization 头
if c.accessToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken))
}
// 发送请求
err := c.httpClient.DoTimeout(req, resp, c.timeout)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
// 检查 HTTP 状态码
statusCode := resp.StatusCode()
switch statusCode {
case 401:
return nil, fmt.Errorf("unauthorized: access token invalid or missing")
case 404:
return nil, fmt.Errorf("API not found: %s", url)
case 415:
return nil, fmt.Errorf("unsupported media type: Content-Type must be application/json")
case 200:
// 继续处理
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
// 解析响应
var apiResp APIResponse
if err := sonic.Unmarshal(resp.Body(), &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// 检查业务状态
if apiResp.Status != "ok" {
c.logger.Warn("API call failed",
zap.String("status", apiResp.Status),
zap.Int("retcode", apiResp.RetCode),
zap.String("message", apiResp.Message))
return &apiResp, fmt.Errorf("API error (retcode=%d): %s", apiResp.RetCode, apiResp.Message)
}
c.logger.Debug("API call succeeded",
zap.String("status", apiResp.Status),
zap.Int("retcode", apiResp.RetCode))
return &apiResp, nil
}
// CallWithoutRetry 调用 API不重试
func (c *APIClient) CallWithoutRetry(ctx context.Context, endpoint string, input interface{}) (*APIResponse, error) {
// 序列化输入参数
var inputData []byte
var err error
if input == nil {
inputData = []byte("{}")
} else {
inputData, err = sonic.Marshal(input)
if err != nil {
return nil, fmt.Errorf("failed to marshal input: %w", err)
}
}
// 构建 URL
url := fmt.Sprintf("%s/api/%s", c.baseURL, endpoint)
return c.doRequest(ctx, url, inputData)
}
// Close 关闭客户端
func (c *APIClient) Close() error {
// fasthttp.Client 不需要显式关闭
return nil
}