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 }