Files
cellbot/internal/adapter/milky/api_client.go
lafay 44fe05ff62 chore: update dependencies and improve bot configuration
- Upgrade Go version to 1.24.0 and update toolchain.
- Update various dependencies in go.mod and go.sum, including:
  - Upgrade `fasthttp/websocket` to v1.5.12
  - Upgrade `fsnotify/fsnotify` to v1.9.0
  - Upgrade `valyala/fasthttp` to v1.58.0
  - Add new dependencies for `bytedance/sonic` and `google/uuid`.
- Refactor bot configuration in config.toml to support multiple bot protocols, including "milky" and "onebot11".
- Modify internal configuration structures to accommodate new bot settings.
- Enhance event dispatcher with metrics tracking and asynchronous processing capabilities.
- Implement WebSocket connection management with heartbeat and reconnection logic.
- Update server handling for bot management and event publishing.
2026-01-05 00:40:09 +08:00

190 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}