package milky import ( "context" "fmt" "time" "cellbot/pkg/net" "github.com/bytedance/sonic" "go.uber.org/zap" ) // APIClient Milky API 客户端 // 使用 pkg/net.HTTPClient 提供的通用 HTTP 请求功能 type APIClient struct { httpClient *net.HTTPClient baseURL string accessToken string logger *zap.Logger } // 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 } config := net.HTTPClientConfig{ BaseURL: baseURL, Timeout: timeout, RetryCount: retryCount, } return &APIClient{ httpClient: net.NewHTTPClient(config, logger.Named("api-client"), nil), baseURL: baseURL, accessToken: accessToken, logger: logger.Named("api-client"), } } // 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)) return c.doRequest(ctx, url, inputData) } // doRequest 执行单次请求 func (c *APIClient) doRequest(ctx context.Context, url string, inputData []byte) (*APIResponse, error) { // 使用 pkg/net.HTTPClient 的通用请求方法 config := net.GenericRequestConfig{ URL: url, Method: "POST", Body: inputData, AccessToken: c.accessToken, } resp, err := c.httpClient.DoGenericRequest(ctx, config) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } // 检查 HTTP 状态码 switch resp.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", resp.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(不重试) // 注意:pkg/net.HTTPClient 的通用请求已经内置了重试机制 // 这个方法保留是为了向后兼容,但仍然会使用底层的重试机制 func (c *APIClient) CallWithoutRetry(ctx context.Context, endpoint string, input interface{}) (*APIResponse, error) { return c.Call(ctx, endpoint, input) } // Close 关闭客户端 func (c *APIClient) Close() error { // pkg/net.HTTPClient 的 fasthttp.Client 不需要显式关闭 return nil }