- 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.
190 lines
4.5 KiB
Go
190 lines
4.5 KiB
Go
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
|
||
}
|