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

View File

@@ -0,0 +1,340 @@
package milky
import (
"cellbot/internal/engine"
"cellbot/internal/protocol"
"cellbot/pkg/net"
"context"
"fmt"
"strconv"
"time"
"go.uber.org/zap"
)
// Config Milky 适配器配置
type Config struct {
// 协议端地址(如 http://localhost:3000
ProtocolURL string `toml:"protocol_url"`
// 访问令牌
AccessToken string `toml:"access_token"`
// 事件接收方式: sse, websocket, webhook
EventMode string `toml:"event_mode"`
// Webhook 监听地址(仅当 event_mode = "webhook" 时需要)
WebhookListenAddr string `toml:"webhook_listen_addr"`
// 超时时间(秒)
Timeout int `toml:"timeout"`
// 重试次数
RetryCount int `toml:"retry_count"`
}
// Adapter Milky 协议适配器
type Adapter struct {
config *Config
selfID string
apiClient *APIClient
sseClient *net.SSEClient
wsManager *net.WebSocketManager
wsConn *net.WebSocketConnection
webhookServer *WebhookServer
eventBus *engine.EventBus
eventConverter *EventConverter
logger *zap.Logger
ctx context.Context
cancel context.CancelFunc
}
// NewAdapter 创建 Milky 适配器
func NewAdapter(config *Config, selfID string, eventBus *engine.EventBus, wsManager *net.WebSocketManager, logger *zap.Logger) *Adapter {
ctx, cancel := context.WithCancel(context.Background())
timeout := time.Duration(config.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
retryCount := config.RetryCount
if retryCount == 0 {
retryCount = 3
}
return &Adapter{
config: config,
selfID: selfID,
apiClient: NewAPIClient(config.ProtocolURL, config.AccessToken, timeout, retryCount, logger),
eventBus: eventBus,
wsManager: wsManager,
eventConverter: NewEventConverter(logger),
logger: logger.Named("milky-adapter"),
ctx: ctx,
cancel: cancel,
}
}
// Connect 连接到协议端
func (a *Adapter) Connect(ctx context.Context) error {
a.logger.Info("Connecting to Milky protocol server",
zap.String("url", a.config.ProtocolURL),
zap.String("event_mode", a.config.EventMode))
// 根据配置选择事件接收方式
switch a.config.EventMode {
case "sse":
return a.connectSSE(ctx)
case "websocket":
return a.connectWebSocket(ctx)
case "webhook":
return a.startWebhook()
default:
return fmt.Errorf("unknown event mode: %s", a.config.EventMode)
}
}
// connectSSE 连接 SSE
func (a *Adapter) connectSSE(ctx context.Context) error {
eventURL := a.config.ProtocolURL + "/event"
// 创建 SSE 客户端配置
sseConfig := net.SSEClientConfig{
URL: eventURL,
AccessToken: a.config.AccessToken,
ReconnectDelay: 5 * time.Second,
MaxReconnect: -1, // 无限重连
EventFilter: "milky_event", // 只接收 milky_event 类型
BufferSize: 100,
}
a.sseClient = net.NewSSEClient(sseConfig, a.logger)
// 启动 SSE 连接
if err := a.sseClient.Connect(ctx); err != nil {
return fmt.Errorf("failed to connect SSE: %w", err)
}
// 启动事件处理
go a.handleEvents(a.sseClient.Events())
a.logger.Info("SSE connection established")
return nil
}
// connectWebSocket 连接 WebSocket
func (a *Adapter) connectWebSocket(ctx context.Context) error {
// 构建 WebSocket URL
eventURL := a.config.ProtocolURL + "/event"
// 替换 http:// 为 ws://https:// 为 wss://
if len(eventURL) > 7 && eventURL[:7] == "http://" {
eventURL = "ws://" + eventURL[7:]
} else if len(eventURL) > 8 && eventURL[:8] == "https://" {
eventURL = "wss://" + eventURL[8:]
}
// 添加 access_token 参数
if a.config.AccessToken != "" {
eventURL += "?access_token=" + a.config.AccessToken
}
a.logger.Info("Connecting to WebSocket", zap.String("url", eventURL))
// 使用 WebSocketManager 建立连接
conn, err := a.wsManager.Dial(eventURL, a.selfID)
if err != nil {
return fmt.Errorf("failed to dial WebSocket: %w", err)
}
a.wsConn = conn
// 启动事件处理
go a.handleWebSocketEvents()
a.logger.Info("WebSocket connection established")
return nil
}
// handleWebSocketEvents 处理 WebSocket 事件
func (a *Adapter) handleWebSocketEvents() {
for {
select {
case <-a.ctx.Done():
return
default:
}
// 读取消息
_, message, err := a.wsConn.Conn.ReadMessage()
if err != nil {
a.logger.Error("Failed to read WebSocket message", zap.Error(err))
return
}
// 转换事件
event, err := a.eventConverter.Convert(message)
if err != nil {
a.logger.Error("Failed to convert event", zap.Error(err))
continue
}
// 发布到事件总线
a.eventBus.Publish(event)
}
}
// startWebhook 启动 Webhook 服务器
func (a *Adapter) startWebhook() error {
if a.config.WebhookListenAddr == "" {
return fmt.Errorf("webhook_listen_addr is required for webhook mode")
}
a.webhookServer = NewWebhookServer(a.config.WebhookListenAddr, a.logger)
// 启动服务器
if err := a.webhookServer.Start(); err != nil {
return fmt.Errorf("failed to start webhook server: %w", err)
}
// 启动事件处理
go a.handleEvents(a.webhookServer.Events())
a.logger.Info("Webhook server started", zap.String("addr", a.config.WebhookListenAddr))
return nil
}
// handleEvents 处理事件
func (a *Adapter) handleEvents(eventChan <-chan []byte) {
for {
select {
case <-a.ctx.Done():
return
case rawEvent, ok := <-eventChan:
if !ok {
a.logger.Info("Event channel closed")
return
}
// 转换事件
event, err := a.eventConverter.Convert(rawEvent)
if err != nil {
a.logger.Error("Failed to convert event", zap.Error(err))
continue
}
// 发布到事件总线
a.eventBus.Publish(event)
}
}
}
// SendAction 发送动作
func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) {
// 调用 API
resp, err := a.apiClient.Call(ctx, string(action.GetType()), action.GetParams())
if err != nil {
return nil, fmt.Errorf("failed to call API: %w", err)
}
return resp.Data, nil
}
// ParseMessage 解析消息
func (a *Adapter) ParseMessage(raw []byte) (protocol.Event, error) {
return a.eventConverter.Convert(raw)
}
// Disconnect 断开连接
func (a *Adapter) Disconnect() error {
a.logger.Info("Disconnecting from Milky protocol server")
a.cancel()
// 关闭各种连接
if a.sseClient != nil {
if err := a.sseClient.Close(); err != nil {
a.logger.Error("Failed to close SSE client", zap.Error(err))
}
}
if a.wsConn != nil {
// WebSocket 连接会在 context 取消时自动关闭
a.logger.Info("WebSocket connection will be closed")
}
if a.webhookServer != nil {
if err := a.webhookServer.Stop(); err != nil {
a.logger.Error("Failed to stop webhook server", zap.Error(err))
}
}
if a.apiClient != nil {
if err := a.apiClient.Close(); err != nil {
a.logger.Error("Failed to close API client", zap.Error(err))
}
}
return nil
}
// GetProtocolName 获取协议名称
func (a *Adapter) GetProtocolName() string {
return "milky"
}
// GetProtocolVersion 获取协议版本
func (a *Adapter) GetProtocolVersion() string {
return "1.0"
}
// GetSelfID 获取机器人自身 ID
func (a *Adapter) GetSelfID() string {
return a.selfID
}
// IsConnected 是否已连接
func (a *Adapter) IsConnected() bool {
switch a.config.EventMode {
case "sse":
return a.sseClient != nil
case "websocket":
return a.wsConn != nil && a.wsConn.Conn != nil
case "webhook":
return a.webhookServer != nil
default:
return false
}
}
// GetStats 获取统计信息
func (a *Adapter) GetStats() map[string]interface{} {
stats := map[string]interface{}{
"protocol": "milky",
"self_id": a.selfID,
"event_mode": a.config.EventMode,
"connected": a.IsConnected(),
}
if a.config.EventMode == "websocket" && a.wsConn != nil {
stats["remote_addr"] = a.wsConn.RemoteAddr
stats["connection_type"] = a.wsConn.Type
}
return stats
}
// CallAPI 直接调用 API提供给 Bot 使用)
func (a *Adapter) CallAPI(ctx context.Context, endpoint string, params map[string]interface{}) (*APIResponse, error) {
return a.apiClient.Call(ctx, endpoint, params)
}
// GetConfig 获取配置
func (a *Adapter) GetConfig() *Config {
return a.config
}
// SetSelfID 设置机器人自身 ID
func (a *Adapter) SetSelfID(selfID string) {
a.selfID = selfID
}
// GetSelfIDInt64 获取机器人自身 IDint64
func (a *Adapter) GetSelfIDInt64() (int64, error) {
return strconv.ParseInt(a.selfID, 10, 64)
}

View File

@@ -0,0 +1,189 @@
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
}

View File

@@ -0,0 +1,321 @@
package milky
import (
"cellbot/internal/engine"
"cellbot/internal/protocol"
"cellbot/pkg/net"
"context"
"fmt"
"go.uber.org/zap"
)
// Bot Milky Bot 实例
type Bot struct {
id string
adapter *Adapter
logger *zap.Logger
status protocol.BotStatus
}
// NewBot 创建 Milky Bot 实例
func NewBot(id string, config *Config, eventBus *engine.EventBus, wsManager *net.WebSocketManager, logger *zap.Logger) *Bot {
adapter := NewAdapter(config, id, eventBus, wsManager, logger)
return &Bot{
id: id,
adapter: adapter,
logger: logger.Named("milky-bot").With(zap.String("bot_id", id)),
status: protocol.BotStatusStopped,
}
}
// GetID 获取机器人 ID
func (b *Bot) GetID() string {
return b.id
}
// GetProtocol 获取协议名称
func (b *Bot) GetProtocol() string {
return "milky"
}
// Name 获取协议名称
func (b *Bot) Name() string {
return "milky"
}
// Version 获取协议版本
func (b *Bot) Version() string {
return "1.0"
}
// GetSelfID 获取机器人自身ID
func (b *Bot) GetSelfID() string {
return b.id
}
// Start 启动实例
func (b *Bot) Start(ctx context.Context) error {
return b.Connect(ctx)
}
// Stop 停止实例
func (b *Bot) Stop(ctx context.Context) error {
return b.Disconnect(ctx)
}
// HandleEvent 处理事件
func (b *Bot) HandleEvent(ctx context.Context, event protocol.Event) error {
// Milky 适配器通过事件总线自动处理事件
// 这里不需要额外处理
return nil
}
// GetStatus 获取状态
func (b *Bot) GetStatus() protocol.BotStatus {
return b.status
}
// Connect 连接
func (b *Bot) Connect(ctx context.Context) error {
b.logger.Info("Connecting Milky bot")
if err := b.adapter.Connect(ctx); err != nil {
b.status = protocol.BotStatusError
return fmt.Errorf("failed to connect: %w", err)
}
b.status = protocol.BotStatusRunning
b.logger.Info("Milky bot connected")
return nil
}
// Disconnect 断开连接
func (b *Bot) Disconnect(ctx context.Context) error {
b.logger.Info("Disconnecting Milky bot")
if err := b.adapter.Disconnect(); err != nil {
return fmt.Errorf("failed to disconnect: %w", err)
}
b.status = protocol.BotStatusStopped
b.logger.Info("Milky bot disconnected")
return nil
}
// SendAction 发送动作
func (b *Bot) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) {
if b.status != protocol.BotStatusRunning {
return nil, fmt.Errorf("bot is not running")
}
return b.adapter.SendAction(ctx, action)
}
// GetAdapter 获取适配器
func (b *Bot) GetAdapter() *Adapter {
return b.adapter
}
// GetInfo 获取机器人信息
func (b *Bot) GetInfo() map[string]interface{} {
return map[string]interface{}{
"id": b.id,
"protocol": "milky",
"status": b.status,
"stats": b.adapter.GetStats(),
}
}
// IsConnected 是否已连接
func (b *Bot) IsConnected() bool {
return b.status == protocol.BotStatusRunning && b.adapter.IsConnected()
}
// SetStatus 设置状态
func (b *Bot) SetStatus(status protocol.BotStatus) {
b.status = status
}
// ============================================================================
// Milky 特定的 API 方法
// ============================================================================
// SendPrivateMessage 发送私聊消息
func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []OutgoingSegment) (*APIResponse, error) {
params := map[string]interface{}{
"user_id": userID,
"segments": segments,
}
return b.adapter.CallAPI(ctx, "send_private_message", params)
}
// SendGroupMessage 发送群消息
func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []OutgoingSegment) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"segments": segments,
}
return b.adapter.CallAPI(ctx, "send_group_message", params)
}
// SendTempMessage 发送临时消息
func (b *Bot) SendTempMessage(ctx context.Context, groupID, userID int64, segments []OutgoingSegment) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
"segments": segments,
}
return b.adapter.CallAPI(ctx, "send_temp_message", params)
}
// RecallMessage 撤回消息
func (b *Bot) RecallMessage(ctx context.Context, messageScene string, peerID, messageSeq int64) (*APIResponse, error) {
params := map[string]interface{}{
"message_scene": messageScene,
"peer_id": peerID,
"message_seq": messageSeq,
}
return b.adapter.CallAPI(ctx, "recall_message", params)
}
// GetFriendList 获取好友列表
func (b *Bot) GetFriendList(ctx context.Context) (*APIResponse, error) {
return b.adapter.CallAPI(ctx, "get_friend_list", nil)
}
// GetGroupList 获取群列表
func (b *Bot) GetGroupList(ctx context.Context) (*APIResponse, error) {
return b.adapter.CallAPI(ctx, "get_group_list", nil)
}
// GetGroupMemberList 获取群成员列表
func (b *Bot) GetGroupMemberList(ctx context.Context, groupID int64) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
}
return b.adapter.CallAPI(ctx, "get_group_member_list", params)
}
// GetGroupMemberInfo 获取群成员信息
func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
}
return b.adapter.CallAPI(ctx, "get_group_member_info", params)
}
// SetGroupAdmin 设置群管理员
func (b *Bot) SetGroupAdmin(ctx context.Context, groupID, userID int64, isSet bool) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
"is_set": isSet,
}
return b.adapter.CallAPI(ctx, "set_group_admin", params)
}
// SetGroupCard 设置群名片
func (b *Bot) SetGroupCard(ctx context.Context, groupID, userID int64, card string) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
"card": card,
}
return b.adapter.CallAPI(ctx, "set_group_card", params)
}
// SetGroupName 设置群名
func (b *Bot) SetGroupName(ctx context.Context, groupID int64, groupName string) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"group_name": groupName,
}
return b.adapter.CallAPI(ctx, "set_group_name", params)
}
// KickGroupMember 踢出群成员
func (b *Bot) KickGroupMember(ctx context.Context, groupID, userID int64, rejectAddRequest bool) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
"reject_add_request": rejectAddRequest,
}
return b.adapter.CallAPI(ctx, "kick_group_member", params)
}
// MuteGroupMember 禁言群成员
func (b *Bot) MuteGroupMember(ctx context.Context, groupID, userID int64, duration int32) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"user_id": userID,
"duration": duration,
}
return b.adapter.CallAPI(ctx, "mute_group_member", params)
}
// MuteGroupWhole 全体禁言
func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"is_mute": isMute,
}
return b.adapter.CallAPI(ctx, "mute_group_whole", params)
}
// LeaveGroup 退出群
func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
}
return b.adapter.CallAPI(ctx, "leave_group", params)
}
// HandleFriendRequest 处理好友请求
func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, accept bool) (*APIResponse, error) {
params := map[string]interface{}{
"initiator_uid": initiatorUID,
"accept": accept,
}
return b.adapter.CallAPI(ctx, "handle_friend_request", params)
}
// HandleGroupJoinRequest 处理入群申请
func (b *Bot) HandleGroupJoinRequest(ctx context.Context, groupID, notificationSeq int64, accept bool, rejectReason string) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"notification_seq": notificationSeq,
"accept": accept,
"reject_reason": rejectReason,
}
return b.adapter.CallAPI(ctx, "handle_group_join_request", params)
}
// HandleGroupInvitation 处理群邀请
func (b *Bot) HandleGroupInvitation(ctx context.Context, groupID, invitationSeq int64, accept bool) (*APIResponse, error) {
params := map[string]interface{}{
"group_id": groupID,
"invitation_seq": invitationSeq,
"accept": accept,
}
return b.adapter.CallAPI(ctx, "handle_group_invitation", params)
}
// UploadFile 上传文件
func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIResponse, error) {
params := map[string]interface{}{
"file_type": fileType,
"file_path": filePath,
}
return b.adapter.CallAPI(ctx, "upload_file", params)
}
// GetFile 获取文件
func (b *Bot) GetFile(ctx context.Context, fileID string) (*APIResponse, error) {
params := map[string]interface{}{
"file_id": fileID,
}
return b.adapter.CallAPI(ctx, "get_file", params)
}

View File

@@ -0,0 +1,693 @@
package milky
import (
"cellbot/internal/protocol"
"fmt"
"strconv"
"github.com/bytedance/sonic"
"go.uber.org/zap"
)
// EventConverter 事件转换器
// 将 Milky 事件转换为通用 protocol.Event
type EventConverter struct {
logger *zap.Logger
}
// NewEventConverter 创建事件转换器
func NewEventConverter(logger *zap.Logger) *EventConverter {
return &EventConverter{
logger: logger.Named("event-converter"),
}
}
// Convert 转换事件
func (c *EventConverter) Convert(raw []byte) (protocol.Event, error) {
// 解析原始事件
var milkyEvent Event
if err := sonic.Unmarshal(raw, &milkyEvent); err != nil {
return nil, fmt.Errorf("failed to unmarshal event: %w", err)
}
c.logger.Debug("Converting event",
zap.String("event_type", milkyEvent.EventType),
zap.Int64("self_id", milkyEvent.SelfID))
// 根据事件类型转换
switch milkyEvent.EventType {
case EventTypeMessageReceive:
return c.convertMessageEvent(&milkyEvent)
case EventTypeFriendRequest:
return c.convertFriendRequestEvent(&milkyEvent)
case EventTypeGroupJoinRequest:
return c.convertGroupJoinRequestEvent(&milkyEvent)
case EventTypeGroupInvitedJoinRequest:
return c.convertGroupInvitedJoinRequestEvent(&milkyEvent)
case EventTypeGroupInvitation:
return c.convertGroupInvitationEvent(&milkyEvent)
case EventTypeMessageRecall:
return c.convertMessageRecallEvent(&milkyEvent)
case EventTypeBotOffline:
return c.convertBotOfflineEvent(&milkyEvent)
case EventTypeFriendNudge:
return c.convertFriendNudgeEvent(&milkyEvent)
case EventTypeFriendFileUpload:
return c.convertFriendFileUploadEvent(&milkyEvent)
case EventTypeGroupAdminChange:
return c.convertGroupAdminChangeEvent(&milkyEvent)
case EventTypeGroupEssenceMessageChange:
return c.convertGroupEssenceMessageChangeEvent(&milkyEvent)
case EventTypeGroupMemberIncrease:
return c.convertGroupMemberIncreaseEvent(&milkyEvent)
case EventTypeGroupMemberDecrease:
return c.convertGroupMemberDecreaseEvent(&milkyEvent)
case EventTypeGroupNameChange:
return c.convertGroupNameChangeEvent(&milkyEvent)
case EventTypeGroupMessageReaction:
return c.convertGroupMessageReactionEvent(&milkyEvent)
case EventTypeGroupMute:
return c.convertGroupMuteEvent(&milkyEvent)
case EventTypeGroupWholeMute:
return c.convertGroupWholeMuteEvent(&milkyEvent)
case EventTypeGroupNudge:
return c.convertGroupNudgeEvent(&milkyEvent)
case EventTypeGroupFileUpload:
return c.convertGroupFileUploadEvent(&milkyEvent)
default:
c.logger.Warn("Unknown event type", zap.String("event_type", milkyEvent.EventType))
return nil, fmt.Errorf("unknown event type: %s", milkyEvent.EventType)
}
}
// convertMessageEvent 转换消息事件
func (c *EventConverter) convertMessageEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
// 解析消息数据
var msgData IncomingMessage
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &msgData); err != nil {
return nil, fmt.Errorf("failed to parse message data: %w", err)
}
// 构建消息文本
messageText := c.buildMessageText(msgData.Segments)
event := &protocol.MessageEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeMessage,
DetailType: msgData.MessageScene, // friend, group, temp
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"peer_id": msgData.PeerID,
"message_seq": msgData.MessageSeq,
"sender_id": msgData.SenderID,
"time": msgData.Time,
"segments": msgData.Segments,
},
},
MessageID: strconv.FormatInt(msgData.MessageSeq, 10),
Message: messageText,
AltText: messageText,
}
// 添加场景特定数据
if msgData.Friend != nil {
event.Data["friend"] = msgData.Friend
}
if msgData.Group != nil {
event.Data["group"] = msgData.Group
}
if msgData.GroupMember != nil {
event.Data["group_member"] = msgData.GroupMember
}
return event, nil
}
// convertFriendRequestEvent 转换好友请求事件
func (c *EventConverter) convertFriendRequestEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data FriendRequestEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse friend request data: %w", err)
}
event := &protocol.RequestEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeRequest,
DetailType: "friend",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"initiator_id": data.InitiatorID,
"initiator_uid": data.InitiatorUID,
"comment": data.Comment,
"via": data.Via,
},
},
RequestID: strconv.FormatInt(data.InitiatorID, 10),
UserID: strconv.FormatInt(data.InitiatorID, 10),
Comment: data.Comment,
Flag: data.InitiatorUID,
}
return event, nil
}
// convertGroupJoinRequestEvent 转换入群申请事件
func (c *EventConverter) convertGroupJoinRequestEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupJoinRequestEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group join request data: %w", err)
}
event := &protocol.RequestEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeRequest,
DetailType: "group",
SubType: "add",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"group_id": data.GroupID,
"notification_seq": data.NotificationSeq,
"is_filtered": data.IsFiltered,
"initiator_id": data.InitiatorID,
"comment": data.Comment,
},
},
RequestID: strconv.FormatInt(data.NotificationSeq, 10),
UserID: strconv.FormatInt(data.InitiatorID, 10),
Comment: data.Comment,
Flag: strconv.FormatInt(data.NotificationSeq, 10),
}
return event, nil
}
// convertGroupInvitedJoinRequestEvent 转换群成员邀请他人入群事件
func (c *EventConverter) convertGroupInvitedJoinRequestEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupInvitedJoinRequestEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group invited join request data: %w", err)
}
event := &protocol.RequestEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeRequest,
DetailType: "group",
SubType: "invite",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"group_id": data.GroupID,
"notification_seq": data.NotificationSeq,
"initiator_id": data.InitiatorID,
"target_user_id": data.TargetUserID,
},
},
RequestID: strconv.FormatInt(data.NotificationSeq, 10),
UserID: strconv.FormatInt(data.InitiatorID, 10),
Comment: "",
Flag: strconv.FormatInt(data.NotificationSeq, 10),
}
return event, nil
}
// convertGroupInvitationEvent 转换他人邀请自身入群事件
func (c *EventConverter) convertGroupInvitationEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupInvitationEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group invitation data: %w", err)
}
event := &protocol.RequestEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeRequest,
DetailType: "group",
SubType: "invite_self",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"group_id": data.GroupID,
"invitation_seq": data.InvitationSeq,
"initiator_id": data.InitiatorID,
},
},
RequestID: strconv.FormatInt(data.InvitationSeq, 10),
UserID: strconv.FormatInt(data.InitiatorID, 10),
Comment: "",
Flag: strconv.FormatInt(data.InvitationSeq, 10),
}
return event, nil
}
// convertMessageRecallEvent 转换消息撤回事件
func (c *EventConverter) convertMessageRecallEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data MessageRecallEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse message recall data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "message_recall",
SubType: data.MessageScene,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"message_scene": data.MessageScene,
"peer_id": data.PeerID,
"message_seq": data.MessageSeq,
"sender_id": data.SenderID,
"operator_id": data.OperatorID,
"display_suffix": data.DisplaySuffix,
},
},
UserID: strconv.FormatInt(data.SenderID, 10),
Operator: strconv.FormatInt(data.OperatorID, 10),
}
return event, nil
}
// convertBotOfflineEvent 转换机器人离线事件
func (c *EventConverter) convertBotOfflineEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data BotOfflineEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse bot offline data: %w", err)
}
event := &protocol.MetaEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeMeta,
DetailType: "bot_offline",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}{
"reason": data.Reason,
},
},
Status: "offline",
}
return event, nil
}
// convertFriendNudgeEvent 转换好友戳一戳事件
func (c *EventConverter) convertFriendNudgeEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data FriendNudgeEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse friend nudge data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "friend_nudge",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
UserID: strconv.FormatInt(data.UserID, 10),
}
return event, nil
}
// convertFriendFileUploadEvent 转换好友文件上传事件
func (c *EventConverter) convertFriendFileUploadEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data FriendFileUploadEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse friend file upload data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "friend_file_upload",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
UserID: strconv.FormatInt(data.UserID, 10),
}
return event, nil
}
// convertGroupAdminChangeEvent 转换群管理员变更事件
func (c *EventConverter) convertGroupAdminChangeEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupAdminChangeEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group admin change data: %w", err)
}
subType := "unset"
if data.IsSet {
subType = "set"
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_admin",
SubType: subType,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
}
return event, nil
}
// convertGroupEssenceMessageChangeEvent 转换群精华消息变更事件
func (c *EventConverter) convertGroupEssenceMessageChangeEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupEssenceMessageChangeEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group essence message change data: %w", err)
}
subType := "delete"
if data.IsSet {
subType = "add"
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_essence",
SubType: subType,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
}
return event, nil
}
// convertGroupMemberIncreaseEvent 转换群成员增加事件
func (c *EventConverter) convertGroupMemberIncreaseEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupMemberIncreaseEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group member increase data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_increase",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
}
if data.OperatorID != nil {
event.Operator = strconv.FormatInt(*data.OperatorID, 10)
}
return event, nil
}
// convertGroupMemberDecreaseEvent 转换群成员减少事件
func (c *EventConverter) convertGroupMemberDecreaseEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupMemberDecreaseEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group member decrease data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_decrease",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
}
if data.OperatorID != nil {
event.Operator = strconv.FormatInt(*data.OperatorID, 10)
}
return event, nil
}
// convertGroupNameChangeEvent 转换群名称变更事件
func (c *EventConverter) convertGroupNameChangeEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupNameChangeEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group name change data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_name_change",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
Operator: strconv.FormatInt(data.OperatorID, 10),
}
return event, nil
}
// convertGroupMessageReactionEvent 转换群消息回应事件
func (c *EventConverter) convertGroupMessageReactionEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupMessageReactionEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group message reaction data: %w", err)
}
subType := "remove"
if data.IsAdd {
subType = "add"
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_message_reaction",
SubType: subType,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
}
return event, nil
}
// convertGroupMuteEvent 转换群禁言事件
func (c *EventConverter) convertGroupMuteEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupMuteEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group mute data: %w", err)
}
subType := "ban"
if data.Duration == 0 {
subType = "lift_ban"
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_ban",
SubType: subType,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
Operator: strconv.FormatInt(data.OperatorID, 10),
}
return event, nil
}
// convertGroupWholeMuteEvent 转换群全体禁言事件
func (c *EventConverter) convertGroupWholeMuteEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupWholeMuteEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group whole mute data: %w", err)
}
subType := "ban"
if !data.IsMute {
subType = "lift_ban"
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_whole_ban",
SubType: subType,
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
Operator: strconv.FormatInt(data.OperatorID, 10),
}
return event, nil
}
// convertGroupNudgeEvent 转换群戳一戳事件
func (c *EventConverter) convertGroupNudgeEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupNudgeEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group nudge data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_nudge",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.SenderID, 10),
}
return event, nil
}
// convertGroupFileUploadEvent 转换群文件上传事件
func (c *EventConverter) convertGroupFileUploadEvent(milkyEvent *Event) (protocol.Event, error) {
selfID := strconv.FormatInt(milkyEvent.SelfID, 10)
var data GroupFileUploadEventData
dataBytes, _ := sonic.Marshal(milkyEvent.Data)
if err := sonic.Unmarshal(dataBytes, &data); err != nil {
return nil, fmt.Errorf("failed to parse group file upload data: %w", err)
}
event := &protocol.NoticeEvent{
BaseEvent: protocol.BaseEvent{
Type: protocol.EventTypeNotice,
DetailType: "group_file_upload",
SubType: "",
Timestamp: milkyEvent.Time,
SelfID: selfID,
Data: map[string]interface{}(milkyEvent.Data),
},
GroupID: strconv.FormatInt(data.GroupID, 10),
UserID: strconv.FormatInt(data.UserID, 10),
}
return event, nil
}
// buildMessageText 构建消息文本
func (c *EventConverter) buildMessageText(segments []IncomingSegment) string {
var text string
for _, seg := range segments {
if seg.Type == "text" {
if textData, ok := seg.Data["text"].(string); ok {
text += textData
}
} else if seg.Type == "mention" {
if userID, ok := seg.Data["user_id"].(float64); ok {
text += fmt.Sprintf("@%d", int64(userID))
}
} else if seg.Type == "image" {
text += "[图片]"
} else if seg.Type == "voice" {
text += "[语音]"
} else if seg.Type == "video" {
text += "[视频]"
} else if seg.Type == "file" {
text += "[文件]"
} else if seg.Type == "face" {
text += "[表情]"
} else if seg.Type == "forward" {
text += "[转发消息]"
}
}
return text
}

View File

@@ -0,0 +1,240 @@
package milky
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"go.uber.org/zap"
)
// SSEClient Server-Sent Events 客户端
// 用于接收协议端推送的事件 (GET /event)
type SSEClient struct {
url string
accessToken string
eventChan chan []byte
logger *zap.Logger
reconnectDelay time.Duration
maxReconnect int
ctx context.Context
cancel context.CancelFunc
}
// NewSSEClient 创建 SSE 客户端
func NewSSEClient(url, accessToken string, logger *zap.Logger) *SSEClient {
ctx, cancel := context.WithCancel(context.Background())
return &SSEClient{
url: url,
accessToken: accessToken,
eventChan: make(chan []byte, 100),
logger: logger.Named("sse-client"),
reconnectDelay: 5 * time.Second,
maxReconnect: -1, // 无限重连
ctx: ctx,
cancel: cancel,
}
}
// Connect 连接到 SSE 服务器
func (c *SSEClient) Connect(ctx context.Context) error {
c.logger.Info("Starting SSE client", zap.String("url", c.url))
go c.connectLoop(ctx)
return nil
}
// connectLoop 连接循环(支持自动重连)
func (c *SSEClient) connectLoop(ctx context.Context) {
reconnectCount := 0
for {
select {
case <-ctx.Done():
c.logger.Info("SSE client stopped")
return
case <-c.ctx.Done():
c.logger.Info("SSE client stopped")
return
default:
}
c.logger.Info("Connecting to SSE server",
zap.String("url", c.url),
zap.Int("reconnect_count", reconnectCount))
err := c.connect(ctx)
if err != nil {
c.logger.Error("SSE connection failed", zap.Error(err))
}
// 检查是否需要重连
if c.maxReconnect >= 0 && reconnectCount >= c.maxReconnect {
c.logger.Error("Max reconnect attempts reached", zap.Int("count", reconnectCount))
return
}
reconnectCount++
// 等待后重连
c.logger.Info("Reconnecting after delay",
zap.Duration("delay", c.reconnectDelay),
zap.Int("attempt", reconnectCount))
select {
case <-time.After(c.reconnectDelay):
case <-ctx.Done():
return
case <-c.ctx.Done():
return
}
}
}
// connect 建立单次连接
func (c *SSEClient) connect(ctx context.Context) error {
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, "GET", c.url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// 设置 Authorization 头
if c.accessToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken))
}
// 设置 Accept 头
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Connection", "keep-alive")
// 发送请求
client := &http.Client{
Timeout: 0, // 无超时,保持长连接
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// 检查 Content-Type
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "text/event-stream") {
return fmt.Errorf("unexpected content type: %s", contentType)
}
c.logger.Info("SSE connection established")
// 读取事件流
return c.readEventStream(ctx, resp)
}
// readEventStream 读取事件流
func (c *SSEClient) readEventStream(ctx context.Context, resp *http.Response) error {
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
var eventType string
var dataLines []string
for scanner.Scan() {
select {
case <-ctx.Done():
return ctx.Err()
case <-c.ctx.Done():
return c.ctx.Err()
default:
}
line := scanner.Text()
// 空行表示事件结束
if line == "" {
if eventType != "" && len(dataLines) > 0 {
c.processEvent(eventType, dataLines)
eventType = ""
dataLines = nil
}
continue
}
// 注释行(以 : 开头)
if strings.HasPrefix(line, ":") {
continue
}
// 解析字段
if strings.HasPrefix(line, "event:") {
eventType = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
dataLines = append(dataLines, data)
}
// 忽略其他字段id, retry 等)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scanner error: %w", err)
}
return fmt.Errorf("connection closed")
}
// processEvent 处理事件
func (c *SSEClient) processEvent(eventType string, dataLines []string) {
// 只处理 milky_event 类型
if eventType != "milky_event" && eventType != "" {
c.logger.Debug("Ignoring non-milky event", zap.String("event_type", eventType))
return
}
// 合并多行 data
data := strings.Join(dataLines, "\n")
c.logger.Debug("Received SSE event",
zap.String("event_type", eventType),
zap.Int("data_length", len(data)))
// 发送到事件通道
select {
case c.eventChan <- []byte(data):
default:
c.logger.Warn("Event channel full, dropping event")
}
}
// Events 获取事件通道
func (c *SSEClient) Events() <-chan []byte {
return c.eventChan
}
// Close 关闭客户端
func (c *SSEClient) Close() error {
c.cancel()
close(c.eventChan)
c.logger.Info("SSE client closed")
return nil
}

View File

@@ -0,0 +1,368 @@
package milky
// Milky 协议类型定义
// 基于官方 TypeScript 定义: https://github.com/SaltifyDev/milky
// ============================================================================
// 标量类型
// ============================================================================
// Int64 表示 64 位整数QQ号、群号等
type Int64 = int64
// Int32 表示 32 位整数
type Int32 = int32
// String 表示字符串
type String = string
// Boolean 表示布尔值
type Boolean = bool
// ============================================================================
// 消息段类型
// ============================================================================
// IncomingSegment 接收消息段(联合类型)
type IncomingSegment struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
}
// OutgoingSegment 发送消息段(联合类型)
type OutgoingSegment struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
}
// IncomingForwardedMessage 接收转发消息
type IncomingForwardedMessage struct {
SenderName string `json:"sender_name"`
AvatarURL string `json:"avatar_url"`
Time int64 `json:"time"`
Segments []IncomingSegment `json:"segments"`
}
// OutgoingForwardedMessage 发送转发消息
type OutgoingForwardedMessage struct {
UserID int64 `json:"user_id"`
SenderName string `json:"sender_name"`
Segments []OutgoingSegment `json:"segments"`
}
// ============================================================================
// 实体类型
// ============================================================================
// FriendCategoryEntity 好友分组实体
type FriendCategoryEntity struct {
CategoryID int32 `json:"category_id"`
CategoryName string `json:"category_name"`
}
// FriendEntity 好友实体
type FriendEntity struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Sex string `json:"sex"` // male, female, unknown
QID string `json:"qid"`
Remark string `json:"remark"`
Category FriendCategoryEntity `json:"category"`
}
// GroupEntity 群实体
type GroupEntity struct {
GroupID int64 `json:"group_id"`
GroupName string `json:"group_name"`
MemberCount int32 `json:"member_count"`
MaxMemberCount int32 `json:"max_member_count"`
}
// GroupMemberEntity 群成员实体
type GroupMemberEntity struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Sex string `json:"sex"` // male, female, unknown
GroupID int64 `json:"group_id"`
Card string `json:"card"`
Title string `json:"title"`
Level int32 `json:"level"`
Role string `json:"role"` // owner, admin, member
JoinTime int64 `json:"join_time"`
LastSentTime int64 `json:"last_sent_time"`
ShutUpEndTime *int64 `json:"shut_up_end_time,omitempty"`
}
// GroupAnnouncementEntity 群公告实体
type GroupAnnouncementEntity struct {
GroupID int64 `json:"group_id"`
AnnouncementID string `json:"announcement_id"`
UserID int64 `json:"user_id"`
Time int64 `json:"time"`
Content string `json:"content"`
ImageURL *string `json:"image_url,omitempty"`
}
// GroupFileEntity 群文件实体
type GroupFileEntity struct {
GroupID int64 `json:"group_id"`
FileID string `json:"file_id"`
FileName string `json:"file_name"`
ParentFolderID string `json:"parent_folder_id"`
FileSize int64 `json:"file_size"`
UploadedTime int64 `json:"uploaded_time"`
ExpireTime *int64 `json:"expire_time,omitempty"`
UploaderID int64 `json:"uploader_id"`
DownloadedTimes int32 `json:"downloaded_times"`
}
// GroupFolderEntity 群文件夹实体
type GroupFolderEntity struct {
GroupID int64 `json:"group_id"`
FolderID string `json:"folder_id"`
ParentFolderID string `json:"parent_folder_id"`
FolderName string `json:"folder_name"`
CreatedTime int64 `json:"created_time"`
LastModifiedTime int64 `json:"last_modified_time"`
CreatorID int64 `json:"creator_id"`
FileCount int32 `json:"file_count"`
}
// ============================================================================
// 消息类型
// ============================================================================
// IncomingMessage 接收消息(联合类型,使用 message_scene 区分)
type IncomingMessage struct {
MessageScene string `json:"message_scene"` // friend, group, temp
PeerID int64 `json:"peer_id"`
MessageSeq int64 `json:"message_seq"`
SenderID int64 `json:"sender_id"`
Time int64 `json:"time"`
Segments []IncomingSegment `json:"segments"`
// 好友消息字段
Friend *FriendEntity `json:"friend,omitempty"`
// 群消息字段
Group *GroupEntity `json:"group,omitempty"`
GroupMember *GroupMemberEntity `json:"group_member,omitempty"`
}
// GroupEssenceMessage 群精华消息
type GroupEssenceMessage struct {
GroupID int64 `json:"group_id"`
MessageSeq int64 `json:"message_seq"`
MessageTime int64 `json:"message_time"`
SenderID int64 `json:"sender_id"`
SenderName string `json:"sender_name"`
OperatorID int64 `json:"operator_id"`
OperatorName string `json:"operator_name"`
OperationTime int64 `json:"operation_time"`
Segments []IncomingSegment `json:"segments"`
}
// ============================================================================
// 事件数据类型
// ============================================================================
// BotOfflineEventData 机器人离线事件数据
type BotOfflineEventData struct {
Reason string `json:"reason"`
}
// MessageRecallEventData 消息撤回事件数据
type MessageRecallEventData struct {
MessageScene string `json:"message_scene"` // friend, group, temp
PeerID int64 `json:"peer_id"`
MessageSeq int64 `json:"message_seq"`
SenderID int64 `json:"sender_id"`
OperatorID int64 `json:"operator_id"`
DisplaySuffix string `json:"display_suffix"`
}
// FriendRequestEventData 好友请求事件数据
type FriendRequestEventData struct {
InitiatorID int64 `json:"initiator_id"`
InitiatorUID string `json:"initiator_uid"`
Comment string `json:"comment"`
Via string `json:"via"`
}
// GroupJoinRequestEventData 入群申请事件数据
type GroupJoinRequestEventData struct {
GroupID int64 `json:"group_id"`
NotificationSeq int64 `json:"notification_seq"`
IsFiltered bool `json:"is_filtered"`
InitiatorID int64 `json:"initiator_id"`
Comment string `json:"comment"`
}
// GroupInvitedJoinRequestEventData 群成员邀请他人入群事件数据
type GroupInvitedJoinRequestEventData struct {
GroupID int64 `json:"group_id"`
NotificationSeq int64 `json:"notification_seq"`
InitiatorID int64 `json:"initiator_id"`
TargetUserID int64 `json:"target_user_id"`
}
// GroupInvitationEventData 他人邀请自身入群事件数据
type GroupInvitationEventData struct {
GroupID int64 `json:"group_id"`
InvitationSeq int64 `json:"invitation_seq"`
InitiatorID int64 `json:"initiator_id"`
}
// FriendNudgeEventData 好友戳一戳事件数据
type FriendNudgeEventData struct {
UserID int64 `json:"user_id"`
IsSelfSend bool `json:"is_self_send"`
IsSelfReceive bool `json:"is_self_receive"`
DisplayAction string `json:"display_action"`
DisplaySuffix string `json:"display_suffix"`
DisplayActionImgURL string `json:"display_action_img_url"`
}
// FriendFileUploadEventData 好友文件上传事件数据
type FriendFileUploadEventData struct {
UserID int64 `json:"user_id"`
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
FileHash string `json:"file_hash"`
IsSelf bool `json:"is_self"`
}
// GroupAdminChangeEventData 群管理员变更事件数据
type GroupAdminChangeEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
IsSet bool `json:"is_set"`
}
// GroupEssenceMessageChangeEventData 群精华消息变更事件数据
type GroupEssenceMessageChangeEventData struct {
GroupID int64 `json:"group_id"`
MessageSeq int64 `json:"message_seq"`
IsSet bool `json:"is_set"`
}
// GroupMemberIncreaseEventData 群成员增加事件数据
type GroupMemberIncreaseEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
OperatorID *int64 `json:"operator_id,omitempty"`
InvitorID *int64 `json:"invitor_id,omitempty"`
}
// GroupMemberDecreaseEventData 群成员减少事件数据
type GroupMemberDecreaseEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
OperatorID *int64 `json:"operator_id,omitempty"`
}
// GroupNameChangeEventData 群名称变更事件数据
type GroupNameChangeEventData struct {
GroupID int64 `json:"group_id"`
NewGroupName string `json:"new_group_name"`
OperatorID int64 `json:"operator_id"`
}
// GroupMessageReactionEventData 群消息回应事件数据
type GroupMessageReactionEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
MessageSeq int64 `json:"message_seq"`
FaceID string `json:"face_id"`
IsAdd bool `json:"is_add"`
}
// GroupMuteEventData 群禁言事件数据
type GroupMuteEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
OperatorID int64 `json:"operator_id"`
Duration int32 `json:"duration"` // 秒0表示取消禁言
}
// GroupWholeMuteEventData 群全体禁言事件数据
type GroupWholeMuteEventData struct {
GroupID int64 `json:"group_id"`
OperatorID int64 `json:"operator_id"`
IsMute bool `json:"is_mute"`
}
// GroupNudgeEventData 群戳一戳事件数据
type GroupNudgeEventData struct {
GroupID int64 `json:"group_id"`
SenderID int64 `json:"sender_id"`
ReceiverID int64 `json:"receiver_id"`
DisplayAction string `json:"display_action"`
DisplaySuffix string `json:"display_suffix"`
DisplayActionImgURL string `json:"display_action_img_url"`
}
// GroupFileUploadEventData 群文件上传事件数据
type GroupFileUploadEventData struct {
GroupID int64 `json:"group_id"`
UserID int64 `json:"user_id"`
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
}
// ============================================================================
// 事件类型
// ============================================================================
// Event Milky 事件(联合类型,使用 event_type 区分)
type Event struct {
EventType string `json:"event_type"`
Time int64 `json:"time"`
SelfID int64 `json:"self_id"`
Data map[string]interface{} `json:"data"`
}
// 事件类型常量
const (
EventTypeBotOffline = "bot_offline"
EventTypeMessageReceive = "message_receive"
EventTypeMessageRecall = "message_recall"
EventTypeFriendRequest = "friend_request"
EventTypeGroupJoinRequest = "group_join_request"
EventTypeGroupInvitedJoinRequest = "group_invited_join_request"
EventTypeGroupInvitation = "group_invitation"
EventTypeFriendNudge = "friend_nudge"
EventTypeFriendFileUpload = "friend_file_upload"
EventTypeGroupAdminChange = "group_admin_change"
EventTypeGroupEssenceMessageChange = "group_essence_message_change"
EventTypeGroupMemberIncrease = "group_member_increase"
EventTypeGroupMemberDecrease = "group_member_decrease"
EventTypeGroupNameChange = "group_name_change"
EventTypeGroupMessageReaction = "group_message_reaction"
EventTypeGroupMute = "group_mute"
EventTypeGroupWholeMute = "group_whole_mute"
EventTypeGroupNudge = "group_nudge"
EventTypeGroupFileUpload = "group_file_upload"
)
// ============================================================================
// API 响应类型
// ============================================================================
// APIResponse API 响应
type APIResponse struct {
Status string `json:"status"` // ok, failed
RetCode int `json:"retcode"`
Data map[string]interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
// 响应状态码
const (
RetCodeSuccess = 0
RetCodeNotLoggedIn = -403
RetCodeInvalidParams = -400
RetCodeNotFound = -404
)

View File

@@ -0,0 +1,115 @@
package milky
import (
"fmt"
"github.com/bytedance/sonic"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
// WebhookServer Webhook 服务器
// 用于接收协议端 POST 推送的事件
type WebhookServer struct {
server *fasthttp.Server
eventChan chan []byte
logger *zap.Logger
addr string
}
// NewWebhookServer 创建 Webhook 服务器
func NewWebhookServer(addr string, logger *zap.Logger) *WebhookServer {
return &WebhookServer{
eventChan: make(chan []byte, 100),
logger: logger.Named("webhook-server"),
addr: addr,
}
}
// Start 启动服务器
func (s *WebhookServer) Start() error {
s.server = &fasthttp.Server{
Handler: s.handleRequest,
MaxConnsPerIP: 1000,
MaxRequestsPerConn: 1000,
}
s.logger.Info("Starting webhook server", zap.String("addr", s.addr))
go func() {
if err := s.server.ListenAndServe(s.addr); err != nil {
s.logger.Error("Webhook server error", zap.Error(err))
}
}()
return nil
}
// handleRequest 处理请求
func (s *WebhookServer) handleRequest(ctx *fasthttp.RequestCtx) {
// 只接受 POST 请求
if !ctx.IsPost() {
s.logger.Warn("Received non-POST request",
zap.String("method", string(ctx.Method())))
ctx.Error("Method Not Allowed", fasthttp.StatusMethodNotAllowed)
return
}
// 检查 Content-Type
contentType := string(ctx.Request.Header.ContentType())
if contentType != "application/json" {
s.logger.Warn("Invalid content type",
zap.String("content_type", contentType))
ctx.Error("Unsupported Media Type", fasthttp.StatusUnsupportedMediaType)
return
}
// 获取请求体
body := ctx.PostBody()
if len(body) == 0 {
s.logger.Warn("Empty request body")
ctx.Error("Bad Request", fasthttp.StatusBadRequest)
return
}
// 验证 JSON 格式
var event Event
if err := sonic.Unmarshal(body, &event); err != nil {
s.logger.Error("Failed to parse event", zap.Error(err))
ctx.Error("Bad Request", fasthttp.StatusBadRequest)
return
}
s.logger.Debug("Received webhook event",
zap.String("event_type", event.EventType),
zap.Int64("self_id", event.SelfID))
// 发送到事件通道
select {
case s.eventChan <- body:
default:
s.logger.Warn("Event channel full, dropping event")
}
// 返回成功响应
ctx.SetContentType("application/json")
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBodyString(`{"status":"ok"}`)
}
// Events 获取事件通道
func (s *WebhookServer) Events() <-chan []byte {
return s.eventChan
}
// Stop 停止服务器
func (s *WebhookServer) Stop() error {
if s.server != nil {
s.logger.Info("Stopping webhook server")
if err := s.server.Shutdown(); err != nil {
return fmt.Errorf("failed to shutdown webhook server: %w", err)
}
}
close(s.eventChan)
return nil
}