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