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

138 lines
3.5 KiB
Go
Raw Normal View History

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
}