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.
This commit is contained in:
lafay
2026-01-05 00:40:09 +08:00
parent ac0dfb64c9
commit 44fe05ff62
30 changed files with 6311 additions and 182 deletions

313
pkg/net/httpclient.go Normal file
View File

@@ -0,0 +1,313 @@
package net
import (
"context"
"fmt"
"sync"
"time"
"cellbot/internal/engine"
"cellbot/internal/protocol"
"github.com/bytedance/sonic"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
// HTTPClient HTTP客户端用于正向HTTP连接
type HTTPClient struct {
client *fasthttp.Client
logger *zap.Logger
eventBus *engine.EventBus
botID string
baseURL string
timeout time.Duration
retryCount int
}
// HTTPClientConfig HTTP客户端配置
type HTTPClientConfig struct {
BotID string
BaseURL string
Timeout time.Duration
RetryCount int
}
// NewHTTPClient 创建HTTP客户端
func NewHTTPClient(config HTTPClientConfig, logger *zap.Logger, eventBus *engine.EventBus) *HTTPClient {
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
if config.RetryCount == 0 {
config.RetryCount = 3
}
return &HTTPClient{
client: &fasthttp.Client{
ReadTimeout: config.Timeout,
WriteTimeout: config.Timeout,
MaxConnsPerHost: 100,
},
logger: logger.Named("http-client"),
eventBus: eventBus,
botID: config.BotID,
baseURL: config.BaseURL,
timeout: config.Timeout,
retryCount: config.RetryCount,
}
}
// SendAction 发送动作请求正向HTTP
func (hc *HTTPClient) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) {
// 序列化动作为JSON
data, err := sonic.Marshal(action)
if err != nil {
return nil, fmt.Errorf("failed to marshal action: %w", err)
}
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
url := hc.baseURL + "/action"
req.SetRequestURI(url)
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.SetBody(data)
hc.logger.Debug("Sending action",
zap.String("url", url),
zap.String("action", string(action.GetType())))
// 重试机制
var lastErr error
for i := 0; i <= hc.retryCount; i++ {
if i > 0 {
hc.logger.Info("Retrying action request",
zap.Int("attempt", i),
zap.Int("max", hc.retryCount))
time.Sleep(time.Duration(i) * time.Second)
}
err := hc.client.DoTimeout(req, resp, hc.timeout)
if err != nil {
lastErr = fmt.Errorf("request failed: %w", err)
continue
}
if resp.StatusCode() != fasthttp.StatusOK {
lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode())
continue
}
// 解析响应
var result map[string]interface{}
if err := sonic.Unmarshal(resp.Body(), &result); err != nil {
lastErr = fmt.Errorf("failed to parse response: %w", err)
continue
}
hc.logger.Info("Action sent successfully",
zap.String("action", string(action.GetType())))
return result, nil
}
return nil, fmt.Errorf("action failed after %d retries: %w", hc.retryCount, lastErr)
}
// PollEvents 轮询事件正向HTTP
func (hc *HTTPClient) PollEvents(ctx context.Context, interval time.Duration) error {
ticker := time.NewTicker(interval)
defer ticker.Stop()
hc.logger.Info("Starting event polling",
zap.Duration("interval", interval))
for {
select {
case <-ticker.C:
if err := hc.fetchEvents(ctx); err != nil {
hc.logger.Error("Failed to fetch events", zap.Error(err))
}
case <-ctx.Done():
hc.logger.Info("Event polling stopped")
return ctx.Err()
}
}
}
// fetchEvents 获取事件
func (hc *HTTPClient) fetchEvents(ctx context.Context) error {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
url := hc.baseURL + "/events"
req.SetRequestURI(url)
req.Header.SetMethod("GET")
err := hc.client.DoTimeout(req, resp, hc.timeout)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
if resp.StatusCode() != fasthttp.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}
// 解析事件列表
var events []protocol.BaseEvent
if err := sonic.Unmarshal(resp.Body(), &events); err != nil {
return fmt.Errorf("failed to parse events: %w", err)
}
// 发布事件到事件总线
for i := range events {
hc.logger.Debug("Event received",
zap.String("type", string(events[i].Type)),
zap.String("detail_type", events[i].DetailType))
hc.eventBus.Publish(&events[i])
}
return nil
}
// HTTPWebhookServer HTTP Webhook服务器用于反向HTTP连接
type HTTPWebhookServer struct {
server *fasthttp.Server
logger *zap.Logger
eventBus *engine.EventBus
handlers map[string]*WebhookHandler
mu sync.RWMutex
}
// WebhookHandler Webhook处理器
type WebhookHandler struct {
BotID string
Secret string
Validator func([]byte, string) bool
}
// NewHTTPWebhookServer 创建HTTP Webhook服务器
func NewHTTPWebhookServer(logger *zap.Logger, eventBus *engine.EventBus) *HTTPWebhookServer {
return &HTTPWebhookServer{
logger: logger.Named("webhook-server"),
eventBus: eventBus,
handlers: make(map[string]*WebhookHandler),
}
}
// RegisterWebhook 注册Webhook处理器
func (hws *HTTPWebhookServer) RegisterWebhook(path string, handler *WebhookHandler) {
hws.mu.Lock()
defer hws.mu.Unlock()
hws.handlers[path] = handler
hws.logger.Info("Webhook registered",
zap.String("path", path),
zap.String("bot_id", handler.BotID))
}
// UnregisterWebhook 注销Webhook处理器
func (hws *HTTPWebhookServer) UnregisterWebhook(path string) {
hws.mu.Lock()
defer hws.mu.Unlock()
delete(hws.handlers, path)
hws.logger.Info("Webhook unregistered", zap.String("path", path))
}
// Start 启动Webhook服务器
func (hws *HTTPWebhookServer) Start(addr string) error {
hws.server = &fasthttp.Server{
Handler: hws.handleWebhook,
}
hws.logger.Info("Starting webhook server", zap.String("address", addr))
go func() {
if err := hws.server.ListenAndServe(addr); err != nil {
hws.logger.Error("Webhook server error", zap.Error(err))
}
}()
return nil
}
// Stop 停止Webhook服务器
func (hws *HTTPWebhookServer) Stop() error {
if hws.server != nil {
hws.logger.Info("Stopping webhook server")
return hws.server.Shutdown()
}
return nil
}
// handleWebhook 处理Webhook请求
func (hws *HTTPWebhookServer) handleWebhook(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
hws.mu.RLock()
handler, exists := hws.handlers[path]
hws.mu.RUnlock()
if !exists {
ctx.Error("Webhook not found", fasthttp.StatusNotFound)
return
}
// 验证签名(如果配置了)
if handler.Secret != "" && handler.Validator != nil {
signature := string(ctx.Request.Header.Peek("X-Signature"))
if !handler.Validator(ctx.PostBody(), signature) {
hws.logger.Warn("Invalid webhook signature",
zap.String("path", path),
zap.String("bot_id", handler.BotID))
ctx.Error("Invalid signature", fasthttp.StatusUnauthorized)
return
}
}
// 解析事件
var event protocol.BaseEvent
if err := sonic.Unmarshal(ctx.PostBody(), &event); err != nil {
hws.logger.Error("Failed to parse webhook event",
zap.Error(err),
zap.String("path", path))
ctx.Error("Invalid event format", fasthttp.StatusBadRequest)
return
}
// 设置BotID
if event.SelfID == "" {
event.SelfID = handler.BotID
}
// 设置时间戳
if event.Timestamp == 0 {
event.Timestamp = time.Now().Unix()
}
// 确保Data字段不为nil
if event.Data == nil {
event.Data = make(map[string]interface{})
}
hws.logger.Info("Webhook event received",
zap.String("path", path),
zap.String("bot_id", handler.BotID),
zap.String("type", string(event.Type)),
zap.String("detail_type", event.DetailType))
// 发布到事件总线
hws.eventBus.Publish(&event)
// 返回成功响应
ctx.SetContentType("application/json")
ctx.SetBodyString(`{"success":true}`)
}