468 lines
12 KiB
Go
468 lines
12 KiB
Go
|
|
package onebot11
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"sync"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"cellbot/internal/engine"
|
|||
|
|
"cellbot/internal/protocol"
|
|||
|
|
"cellbot/pkg/net"
|
|||
|
|
|
|||
|
|
"github.com/bytedance/sonic"
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Adapter OneBot11协议适配器
|
|||
|
|
type Adapter struct {
|
|||
|
|
config *Config
|
|||
|
|
logger *zap.Logger
|
|||
|
|
wsManager *net.WebSocketManager
|
|||
|
|
httpClient *HTTPClient
|
|||
|
|
wsWaiter *WSResponseWaiter
|
|||
|
|
eventBus *engine.EventBus
|
|||
|
|
selfID string
|
|||
|
|
connected bool
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
wsConnection *net.WebSocketConnection
|
|||
|
|
ctx context.Context
|
|||
|
|
cancel context.CancelFunc
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Config OneBot11配置
|
|||
|
|
type Config struct {
|
|||
|
|
// 连接配置
|
|||
|
|
ConnectionType string `json:"connection_type"` // ws, ws-reverse, http, http-post
|
|||
|
|
Host string `json:"host"`
|
|||
|
|
Port int `json:"port"`
|
|||
|
|
AccessToken string `json:"access_token"`
|
|||
|
|
|
|||
|
|
// WebSocket配置
|
|||
|
|
WSUrl string `json:"ws_url"` // 正向WS地址
|
|||
|
|
WSReverseUrl string `json:"ws_reverse_url"` // 反向WS监听地址
|
|||
|
|
Heartbeat int `json:"heartbeat"` // 心跳间隔(秒)
|
|||
|
|
ReconnectInterval int `json:"reconnect_interval"` // 重连间隔(秒)
|
|||
|
|
|
|||
|
|
// HTTP配置
|
|||
|
|
HTTPUrl string `json:"http_url"` // 正向HTTP地址
|
|||
|
|
HTTPPostUrl string `json:"http_post_url"` // HTTP POST上报地址
|
|||
|
|
Secret string `json:"secret"` // 签名密钥
|
|||
|
|
Timeout int `json:"timeout"` // 超时时间(秒)
|
|||
|
|
|
|||
|
|
// 其他配置
|
|||
|
|
SelfID string `json:"self_id"` // 机器人QQ号
|
|||
|
|
Nickname string `json:"nickname"` // 机器人昵称
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewAdapter 创建OneBot11适配器
|
|||
|
|
func NewAdapter(config *Config, logger *zap.Logger, wsManager *net.WebSocketManager, eventBus *engine.EventBus) *Adapter {
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
|
|||
|
|
timeout := time.Duration(config.Timeout) * time.Second
|
|||
|
|
if timeout == 0 {
|
|||
|
|
timeout = 30 * time.Second
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
adapter := &Adapter{
|
|||
|
|
config: config,
|
|||
|
|
logger: logger.Named("onebot11"),
|
|||
|
|
wsManager: wsManager,
|
|||
|
|
wsWaiter: NewWSResponseWaiter(timeout, logger),
|
|||
|
|
eventBus: eventBus,
|
|||
|
|
selfID: config.SelfID,
|
|||
|
|
ctx: ctx,
|
|||
|
|
cancel: cancel,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果使用HTTP连接,初始化HTTP客户端
|
|||
|
|
if config.ConnectionType == "http" && config.HTTPUrl != "" {
|
|||
|
|
adapter.httpClient = NewHTTPClient(config.HTTPUrl, config.AccessToken, timeout, logger)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return adapter
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Name 获取协议名称
|
|||
|
|
func (a *Adapter) Name() string {
|
|||
|
|
return "OneBot"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Version 获取协议版本
|
|||
|
|
func (a *Adapter) Version() string {
|
|||
|
|
return "11"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Connect 建立连接
|
|||
|
|
func (a *Adapter) Connect(ctx context.Context) error {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
defer a.mu.Unlock()
|
|||
|
|
|
|||
|
|
if a.connected {
|
|||
|
|
return fmt.Errorf("already connected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.logger.Info("Starting OneBot11 connection",
|
|||
|
|
zap.String("connection_type", a.config.ConnectionType),
|
|||
|
|
zap.String("self_id", a.selfID))
|
|||
|
|
|
|||
|
|
switch a.config.ConnectionType {
|
|||
|
|
case "ws":
|
|||
|
|
return a.connectWebSocket(ctx)
|
|||
|
|
case "ws-reverse":
|
|||
|
|
return a.connectWebSocketReverse(ctx)
|
|||
|
|
case "http":
|
|||
|
|
return a.connectHTTP(ctx)
|
|||
|
|
case "http-post":
|
|||
|
|
return a.connectHTTPPost(ctx)
|
|||
|
|
default:
|
|||
|
|
return fmt.Errorf("unsupported connection type: %s", a.config.ConnectionType)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Disconnect 断开连接
|
|||
|
|
func (a *Adapter) Disconnect(ctx context.Context) error {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
defer a.mu.Unlock()
|
|||
|
|
|
|||
|
|
if !a.connected {
|
|||
|
|
a.logger.Debug("Already disconnected, skipping")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.logger.Info("Disconnecting OneBot11 adapter",
|
|||
|
|
zap.String("connection_type", a.config.ConnectionType))
|
|||
|
|
|
|||
|
|
// 取消上下文
|
|||
|
|
if a.cancel != nil {
|
|||
|
|
a.cancel()
|
|||
|
|
a.logger.Debug("Context cancelled")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关闭WebSocket连接
|
|||
|
|
if a.wsConnection != nil {
|
|||
|
|
a.logger.Info("Closing WebSocket connection",
|
|||
|
|
zap.String("connection_id", a.wsConnection.ID))
|
|||
|
|
a.wsManager.RemoveConnection(a.wsConnection.ID)
|
|||
|
|
a.wsConnection = nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关闭HTTP客户端
|
|||
|
|
if a.httpClient != nil {
|
|||
|
|
if err := a.httpClient.Close(); err != nil {
|
|||
|
|
a.logger.Error("Failed to close HTTP client", zap.Error(err))
|
|||
|
|
} else {
|
|||
|
|
a.logger.Debug("HTTP client closed")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.connected = false
|
|||
|
|
a.logger.Info("OneBot11 adapter disconnected successfully")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsConnected 检查连接状态
|
|||
|
|
func (a *Adapter) IsConnected() bool {
|
|||
|
|
a.mu.RLock()
|
|||
|
|
defer a.mu.RUnlock()
|
|||
|
|
return a.connected
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetSelfID 获取机器人自身ID
|
|||
|
|
func (a *Adapter) GetSelfID() string {
|
|||
|
|
return a.selfID
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SendAction 发送动作
|
|||
|
|
func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) {
|
|||
|
|
// 序列化为OneBot11格式
|
|||
|
|
data, err := a.SerializeAction(action)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch a.config.ConnectionType {
|
|||
|
|
case "ws", "ws-reverse":
|
|||
|
|
return a.sendActionWebSocket(data)
|
|||
|
|
case "http":
|
|||
|
|
return a.sendActionHTTP(data)
|
|||
|
|
default:
|
|||
|
|
return nil, fmt.Errorf("unsupported connection type for sending action: %s", a.config.ConnectionType)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HandleEvent 处理事件
|
|||
|
|
func (a *Adapter) HandleEvent(ctx context.Context, event protocol.Event) error {
|
|||
|
|
a.logger.Debug("Handling event",
|
|||
|
|
zap.String("type", string(event.GetType())),
|
|||
|
|
zap.String("detail_type", event.GetDetailType()))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ParseMessage 解析原始消息为Event
|
|||
|
|
func (a *Adapter) ParseMessage(raw []byte) (protocol.Event, error) {
|
|||
|
|
var rawEvent RawEvent
|
|||
|
|
if err := sonic.Unmarshal(raw, &rawEvent); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to unmarshal raw event: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return a.convertToEvent(&rawEvent)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SerializeAction 序列化Action为协议格式
|
|||
|
|
func (a *Adapter) SerializeAction(action protocol.Action) ([]byte, error) {
|
|||
|
|
// 转换为OneBot11格式
|
|||
|
|
ob11ActionName := ConvertAction(action)
|
|||
|
|
|
|||
|
|
// 检查是否有未转换的动作类型(如果转换后的名称与原始类型相同,说明没有匹配到)
|
|||
|
|
originalType := string(action.GetType())
|
|||
|
|
if ob11ActionName == originalType {
|
|||
|
|
a.logger.Warn("Action type not converted, using original type",
|
|||
|
|
zap.String("action_type", originalType),
|
|||
|
|
zap.String("hint", "This action type may not be supported by OneBot11"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ob11Action := &OB11Action{
|
|||
|
|
Action: ob11ActionName,
|
|||
|
|
Params: action.GetParams(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sonic.Marshal(ob11Action)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// connectWebSocket 正向WebSocket连接
|
|||
|
|
func (a *Adapter) connectWebSocket(ctx context.Context) error {
|
|||
|
|
if a.config.WSUrl == "" {
|
|||
|
|
return fmt.Errorf("ws_url is required for ws connection")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.logger.Info("Connecting to OneBot WebSocket server",
|
|||
|
|
zap.String("url", a.config.WSUrl),
|
|||
|
|
zap.Bool("has_token", a.config.AccessToken != ""))
|
|||
|
|
|
|||
|
|
// 添加访问令牌到URL
|
|||
|
|
wsURL := a.config.WSUrl
|
|||
|
|
if a.config.AccessToken != "" {
|
|||
|
|
wsURL += "?access_token=" + a.config.AccessToken
|
|||
|
|
a.logger.Debug("Added access token to WebSocket URL")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.logger.Info("Dialing WebSocket...",
|
|||
|
|
zap.String("full_url", wsURL))
|
|||
|
|
|
|||
|
|
wsConn, err := a.wsManager.Dial(wsURL, a.selfID)
|
|||
|
|
if err != nil {
|
|||
|
|
a.logger.Error("Failed to connect WebSocket",
|
|||
|
|
zap.String("url", a.config.WSUrl),
|
|||
|
|
zap.Error(err))
|
|||
|
|
return fmt.Errorf("failed to connect websocket: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.wsConnection = wsConn
|
|||
|
|
a.connected = true
|
|||
|
|
|
|||
|
|
a.logger.Info("WebSocket connected successfully",
|
|||
|
|
zap.String("url", a.config.WSUrl),
|
|||
|
|
zap.String("remote_addr", wsConn.RemoteAddr),
|
|||
|
|
zap.String("connection_id", wsConn.ID))
|
|||
|
|
|
|||
|
|
// 启动消息接收处理
|
|||
|
|
go a.handleWebSocketMessages()
|
|||
|
|
|
|||
|
|
a.logger.Info("WebSocket message handler started")
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// connectWebSocketReverse 反向WebSocket连接
|
|||
|
|
func (a *Adapter) connectWebSocketReverse(ctx context.Context) error {
|
|||
|
|
// 反向WebSocket由客户端主动连接到服务器
|
|||
|
|
// WebSocket服务器会在主Server中启动
|
|||
|
|
// 这里只需要标记为已连接状态,等待客户端通过HTTP服务器连接
|
|||
|
|
a.connected = true
|
|||
|
|
|
|||
|
|
a.logger.Info("OneBot11 adapter ready for reverse WebSocket connections",
|
|||
|
|
zap.String("bot_id", a.selfID),
|
|||
|
|
zap.String("listen_addr", a.config.WSReverseUrl))
|
|||
|
|
|
|||
|
|
// 注意:实际的WebSocket服务器由pkg/net/server.go提供
|
|||
|
|
// OneBot客户端需要连接到 ws://host:port/ws?bot_id=<selfID>
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// connectHTTP 正向HTTP连接
|
|||
|
|
func (a *Adapter) connectHTTP(ctx context.Context) error {
|
|||
|
|
if a.config.HTTPUrl == "" {
|
|||
|
|
return fmt.Errorf("http_url is required for http connection")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建HTTP客户端
|
|||
|
|
// TODO: 实现HTTP轮询
|
|||
|
|
a.connected = true
|
|||
|
|
|
|||
|
|
a.logger.Info("HTTP connected",
|
|||
|
|
zap.String("url", a.config.HTTPUrl))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// connectHTTPPost HTTP POST上报
|
|||
|
|
func (a *Adapter) connectHTTPPost(ctx context.Context) error {
|
|||
|
|
if a.config.HTTPPostUrl == "" {
|
|||
|
|
return fmt.Errorf("http_post_url is required for http-post connection")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HTTP POST由客户端主动推送事件
|
|||
|
|
a.connected = true
|
|||
|
|
|
|||
|
|
a.logger.Info("HTTP POST ready",
|
|||
|
|
zap.String("url", a.config.HTTPPostUrl))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// sendActionWebSocket 通过WebSocket发送动作
|
|||
|
|
func (a *Adapter) sendActionWebSocket(data []byte) (map[string]interface{}, error) {
|
|||
|
|
if a.wsConnection == nil {
|
|||
|
|
return nil, fmt.Errorf("websocket connection not established")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析请求以获取或添加echo
|
|||
|
|
var req OB11Action
|
|||
|
|
if err := sonic.Unmarshal(data, &req); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to unmarshal action: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有echo,生成一个
|
|||
|
|
if req.Echo == "" {
|
|||
|
|
req.Echo = GenerateEcho()
|
|||
|
|
var err error
|
|||
|
|
data, err = sonic.Marshal(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to marshal action with echo: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送消息
|
|||
|
|
if err := a.wsConnection.SendMessage(data); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to send action: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 等待响应
|
|||
|
|
resp, err := a.wsWaiter.Wait(req.Echo)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查响应状态
|
|||
|
|
if resp.Status != "ok" && resp.Status != "async" {
|
|||
|
|
return resp.Data, fmt.Errorf("action failed (retcode=%d)", resp.RetCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return resp.Data, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// sendActionHTTP 通过HTTP发送动作
|
|||
|
|
func (a *Adapter) sendActionHTTP(data []byte) (map[string]interface{}, error) {
|
|||
|
|
if a.httpClient == nil {
|
|||
|
|
return nil, fmt.Errorf("http client not initialized")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析请求
|
|||
|
|
var req OB11Action
|
|||
|
|
if err := sonic.Unmarshal(data, &req); err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to unmarshal action: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用HTTP API
|
|||
|
|
resp, err := a.httpClient.Call(a.ctx, req.Action, req.Params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查响应状态
|
|||
|
|
if resp.Status != "ok" && resp.Status != "async" {
|
|||
|
|
return resp.Data, fmt.Errorf("action failed (retcode=%d)", resp.RetCode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return resp.Data, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleWebSocketMessages 处理WebSocket消息
|
|||
|
|
func (a *Adapter) handleWebSocketMessages() {
|
|||
|
|
a.logger.Info("WebSocket message handler started, waiting for messages...")
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-a.ctx.Done():
|
|||
|
|
a.logger.Info("Context cancelled, stopping WebSocket message handler")
|
|||
|
|
return
|
|||
|
|
default:
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if a.wsConnection == nil || a.wsConnection.Conn == nil {
|
|||
|
|
a.logger.Warn("WebSocket connection is nil, stopping message handler")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 读取消息
|
|||
|
|
_, message, err := a.wsConnection.Conn.ReadMessage()
|
|||
|
|
if err != nil {
|
|||
|
|
a.logger.Error("Failed to read WebSocket message",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("connection_id", a.wsConnection.ID))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
a.logger.Debug("Received WebSocket message",
|
|||
|
|
zap.Int("size", len(message)),
|
|||
|
|
zap.String("preview", string(message[:min(len(message), 200)])))
|
|||
|
|
|
|||
|
|
// 尝试解析为响应
|
|||
|
|
var resp OB11Response
|
|||
|
|
if err := sonic.Unmarshal(message, &resp); err == nil {
|
|||
|
|
// 如果有echo字段,说明是API响应
|
|||
|
|
if resp.Echo != "" {
|
|||
|
|
a.logger.Debug("Received API response",
|
|||
|
|
zap.String("echo", resp.Echo),
|
|||
|
|
zap.String("status", resp.Status),
|
|||
|
|
zap.Int("retcode", resp.RetCode))
|
|||
|
|
a.wsWaiter.Notify(&resp)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 否则当作事件处理
|
|||
|
|
a.logger.Info("Received OneBot event",
|
|||
|
|
zap.ByteString("raw_event", message))
|
|||
|
|
|
|||
|
|
// 解析事件
|
|||
|
|
a.logger.Info("Parsing OneBot event...")
|
|||
|
|
event, err := a.ParseMessage(message)
|
|||
|
|
if err != nil {
|
|||
|
|
a.logger.Error("Failed to parse event",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.ByteString("raw_message", message))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发布事件到事件总线
|
|||
|
|
a.logger.Info("Publishing event to event bus",
|
|||
|
|
zap.String("event_type", string(event.GetType())),
|
|||
|
|
zap.String("detail_type", event.GetDetailType()),
|
|||
|
|
zap.String("self_id", event.GetSelfID()))
|
|||
|
|
|
|||
|
|
a.eventBus.Publish(event)
|
|||
|
|
|
|||
|
|
a.logger.Info("Event published successfully")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func min(a, b int) int {
|
|||
|
|
if a < b {
|
|||
|
|
return a
|
|||
|
|
}
|
|||
|
|
return b
|
|||
|
|
}
|