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:
306
internal/adapter/onebot11/action.go
Normal file
306
internal/adapter/onebot11/action.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package onebot11
|
||||
|
||||
import (
|
||||
"cellbot/internal/protocol"
|
||||
)
|
||||
|
||||
// OneBot11 API动作常量
|
||||
const (
|
||||
ActionSendPrivateMsg = "send_private_msg"
|
||||
ActionSendGroupMsg = "send_group_msg"
|
||||
ActionSendMsg = "send_msg"
|
||||
ActionDeleteMsg = "delete_msg"
|
||||
ActionGetMsg = "get_msg"
|
||||
ActionGetForwardMsg = "get_forward_msg"
|
||||
ActionSendLike = "send_like"
|
||||
ActionSetGroupKick = "set_group_kick"
|
||||
ActionSetGroupBan = "set_group_ban"
|
||||
ActionSetGroupAnonymousBan = "set_group_anonymous_ban"
|
||||
ActionSetGroupWholeBan = "set_group_whole_ban"
|
||||
ActionSetGroupAdmin = "set_group_admin"
|
||||
ActionSetGroupAnonymous = "set_group_anonymous"
|
||||
ActionSetGroupCard = "set_group_card"
|
||||
ActionSetGroupName = "set_group_name"
|
||||
ActionSetGroupLeave = "set_group_leave"
|
||||
ActionSetGroupSpecialTitle = "set_group_special_title"
|
||||
ActionSetFriendAddRequest = "set_friend_add_request"
|
||||
ActionSetGroupAddRequest = "set_group_add_request"
|
||||
ActionGetLoginInfo = "get_login_info"
|
||||
ActionGetStrangerInfo = "get_stranger_info"
|
||||
ActionGetFriendList = "get_friend_list"
|
||||
ActionGetGroupInfo = "get_group_info"
|
||||
ActionGetGroupList = "get_group_list"
|
||||
ActionGetGroupMemberInfo = "get_group_member_info"
|
||||
ActionGetGroupMemberList = "get_group_member_list"
|
||||
ActionGetGroupHonorInfo = "get_group_honor_info"
|
||||
ActionGetCookies = "get_cookies"
|
||||
ActionGetCsrfToken = "get_csrf_token"
|
||||
ActionGetCredentials = "get_credentials"
|
||||
ActionGetRecord = "get_record"
|
||||
ActionGetImage = "get_image"
|
||||
ActionCanSendImage = "can_send_image"
|
||||
ActionCanSendRecord = "can_send_record"
|
||||
ActionGetStatus = "get_status"
|
||||
ActionGetVersionInfo = "get_version_info"
|
||||
ActionSetRestart = "set_restart"
|
||||
ActionCleanCache = "clean_cache"
|
||||
)
|
||||
|
||||
// ConvertAction 将通用Action转换为OneBot11 Action
|
||||
func ConvertAction(action protocol.Action) string {
|
||||
switch action.GetType() {
|
||||
case protocol.ActionTypeSendPrivateMessage:
|
||||
return ActionSendPrivateMsg
|
||||
case protocol.ActionTypeSendGroupMessage:
|
||||
return ActionSendGroupMsg
|
||||
case protocol.ActionTypeDeleteMessage:
|
||||
return ActionDeleteMsg
|
||||
case protocol.ActionTypeGetUserInfo:
|
||||
return ActionGetStrangerInfo
|
||||
case protocol.ActionTypeGetFriendList:
|
||||
return ActionGetFriendList
|
||||
case protocol.ActionTypeGetGroupInfo:
|
||||
return ActionGetGroupInfo
|
||||
case protocol.ActionTypeGetGroupMemberList:
|
||||
return ActionGetGroupMemberList
|
||||
case protocol.ActionTypeSetGroupKick:
|
||||
return ActionSetGroupKick
|
||||
case protocol.ActionTypeSetGroupBan:
|
||||
return ActionSetGroupBan
|
||||
case protocol.ActionTypeSetGroupAdmin:
|
||||
return ActionSetGroupAdmin
|
||||
case protocol.ActionTypeSetGroupWholeBan:
|
||||
return ActionSetGroupWholeBan
|
||||
case protocol.ActionTypeGetStatus:
|
||||
return ActionGetStatus
|
||||
case protocol.ActionTypeGetVersion:
|
||||
return ActionGetVersionInfo
|
||||
default:
|
||||
return string(action.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// SendPrivateMessageAction 发送私聊消息动作
|
||||
type SendPrivateMessageAction struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Message interface{} `json:"message"`
|
||||
AutoEscape bool `json:"auto_escape,omitempty"`
|
||||
}
|
||||
|
||||
// SendGroupMessageAction 发送群消息动作
|
||||
type SendGroupMessageAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
Message interface{} `json:"message"`
|
||||
AutoEscape bool `json:"auto_escape,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteMessageAction 撤回消息动作
|
||||
type DeleteMessageAction struct {
|
||||
MessageID int32 `json:"message_id"`
|
||||
}
|
||||
|
||||
// GetMessageAction 获取消息动作
|
||||
type GetMessageAction struct {
|
||||
MessageID int32 `json:"message_id"`
|
||||
}
|
||||
|
||||
// SendLikeAction 发送好友赞动作
|
||||
type SendLikeAction struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Times int `json:"times,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupKickAction 群组踢人动作
|
||||
type SetGroupKickAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
RejectAddRequest bool `json:"reject_add_request,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupBanAction 群组禁言动作
|
||||
type SetGroupBanAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Duration int64 `json:"duration,omitempty"` // 禁言时长,单位秒,0表示取消禁言
|
||||
}
|
||||
|
||||
// SetGroupWholeBanAction 群组全员禁言动作
|
||||
type SetGroupWholeBanAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupAdminAction 设置群管理员动作
|
||||
type SetGroupAdminAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupCardAction 设置群名片动作
|
||||
type SetGroupCardAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Card string `json:"card,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupNameAction 设置群名动作
|
||||
type SetGroupNameAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
GroupName string `json:"group_name"`
|
||||
}
|
||||
|
||||
// SetGroupLeaveAction 退出群组动作
|
||||
type SetGroupLeaveAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
IsDismiss bool `json:"is_dismiss,omitempty"`
|
||||
}
|
||||
|
||||
// SetFriendAddRequestAction 处理加好友请求动作
|
||||
type SetFriendAddRequestAction struct {
|
||||
Flag string `json:"flag"`
|
||||
Approve bool `json:"approve,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
}
|
||||
|
||||
// SetGroupAddRequestAction 处理加群请求动作
|
||||
type SetGroupAddRequestAction struct {
|
||||
Flag string `json:"flag"`
|
||||
SubType string `json:"sub_type,omitempty"` // add 或 invite
|
||||
Approve bool `json:"approve,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// GetStrangerInfoAction 获取陌生人信息动作
|
||||
type GetStrangerInfoAction struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
NoCache bool `json:"no_cache,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroupInfoAction 获取群信息动作
|
||||
type GetGroupInfoAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
NoCache bool `json:"no_cache,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroupMemberInfoAction 获取群成员信息动作
|
||||
type GetGroupMemberInfoAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
NoCache bool `json:"no_cache,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroupMemberListAction 获取群成员列表动作
|
||||
type GetGroupMemberListAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
}
|
||||
|
||||
// GetGroupHonorInfoAction 获取群荣誉信息动作
|
||||
type GetGroupHonorInfoAction struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
Type string `json:"type"` // talkative, performer, legend, strong_newbie, emotion, all
|
||||
}
|
||||
|
||||
// GetCookiesAction 获取Cookies动作
|
||||
type GetCookiesAction struct {
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
// GetRecordAction 获取语音动作
|
||||
type GetRecordAction struct {
|
||||
File string `json:"file"`
|
||||
OutFormat string `json:"out_format"`
|
||||
}
|
||||
|
||||
// GetImageAction 获取图片动作
|
||||
type GetImageAction struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
// SetRestartAction 重启OneBot实现动作
|
||||
type SetRestartAction struct {
|
||||
Delay int `json:"delay,omitempty"` // 延迟毫秒数
|
||||
}
|
||||
|
||||
// ActionResponse API响应
|
||||
type ActionResponse struct {
|
||||
Status string `json:"status"`
|
||||
RetCode int `json:"retcode"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Wording string `json:"wording,omitempty"`
|
||||
}
|
||||
|
||||
// 响应状态码常量
|
||||
const (
|
||||
RetCodeOK = 0
|
||||
RetCodeAsyncStarted = 1 // 异步操作已开始
|
||||
RetCodeBadRequest = 1400 // 请求格式错误
|
||||
RetCodeUnauthorized = 1401 // 未授权
|
||||
RetCodeForbidden = 1403 // 禁止访问
|
||||
RetCodeNotFound = 1404 // 接口不存在
|
||||
RetCodeMethodNotAllowed = 1405 // 请求方法不支持
|
||||
RetCodeInternalError = 1500 // 内部错误
|
||||
)
|
||||
|
||||
// BuildActionRequest 构建动作请求
|
||||
func BuildActionRequest(action string, params map[string]interface{}, echo string) *OB11Action {
|
||||
return &OB11Action{
|
||||
Action: action,
|
||||
Params: params,
|
||||
Echo: echo,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSendPrivateMsg 构建发送私聊消息请求
|
||||
func BuildSendPrivateMsg(userID int64, message interface{}, autoEscape bool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"message": message,
|
||||
"auto_escape": autoEscape,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSendGroupMsg 构建发送群消息请求
|
||||
func BuildSendGroupMsg(groupID int64, message interface{}, autoEscape bool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"message": message,
|
||||
"auto_escape": autoEscape,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildDeleteMsg 构建撤回消息请求
|
||||
func BuildDeleteMsg(messageID int32) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"message_id": messageID,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSetGroupBan 构建群组禁言请求
|
||||
func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"user_id": userID,
|
||||
"duration": duration,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSetGroupKick 构建群组踢人请求
|
||||
func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"user_id": userID,
|
||||
"reject_add_request": rejectAddRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSetGroupCard 构建设置群名片请求
|
||||
func BuildSetGroupCard(groupID, userID int64, card string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"group_id": groupID,
|
||||
"user_id": userID,
|
||||
"card": card,
|
||||
}
|
||||
}
|
||||
467
internal/adapter/onebot11/adapter.go
Normal file
467
internal/adapter/onebot11/adapter.go
Normal file
@@ -0,0 +1,467 @@
|
||||
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
|
||||
}
|
||||
36
internal/adapter/onebot11/bot.go
Normal file
36
internal/adapter/onebot11/bot.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package onebot11
|
||||
|
||||
import (
|
||||
"cellbot/internal/engine"
|
||||
"cellbot/internal/protocol"
|
||||
"cellbot/pkg/net"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Bot OneBot11机器人实例
|
||||
type Bot struct {
|
||||
*protocol.BaseBotInstance
|
||||
adapter *Adapter
|
||||
}
|
||||
|
||||
// NewBot 创建OneBot11机器人实例
|
||||
func NewBot(id string, config *Config, logger *zap.Logger, wsManager *net.WebSocketManager, eventBus *engine.EventBus) *Bot {
|
||||
adapter := NewAdapter(config, logger, wsManager, eventBus)
|
||||
baseBot := protocol.NewBaseBotInstance(id, adapter, logger)
|
||||
|
||||
return &Bot{
|
||||
BaseBotInstance: baseBot,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAdapter 获取适配器
|
||||
func (b *Bot) GetAdapter() *Adapter {
|
||||
return b.adapter
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (b *Bot) GetConfig() *Config {
|
||||
return b.adapter.config
|
||||
}
|
||||
186
internal/adapter/onebot11/client.go
Normal file
186
internal/adapter/onebot11/client.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package onebot11
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/google/uuid"
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HTTPClient OneBot11 HTTP客户端
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
accessToken string
|
||||
httpClient *fasthttp.Client
|
||||
logger *zap.Logger
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewHTTPClient 创建HTTP客户端
|
||||
func NewHTTPClient(baseURL, accessToken string, timeout time.Duration, logger *zap.Logger) *HTTPClient {
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return &HTTPClient{
|
||||
baseURL: baseURL,
|
||||
accessToken: accessToken,
|
||||
httpClient: &fasthttp.Client{
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
MaxConnsPerHost: 100,
|
||||
},
|
||||
logger: logger.Named("http-client"),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 调用API
|
||||
func (c *HTTPClient) Call(ctx context.Context, action string, params map[string]interface{}) (*OB11Response, error) {
|
||||
// 构建请求数据
|
||||
reqData := map[string]interface{}{
|
||||
"action": action,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
data, err := sonic.Marshal(reqData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// 构建URL
|
||||
url := fmt.Sprintf("%s/%s", c.baseURL, action)
|
||||
|
||||
c.logger.Debug("Calling HTTP API",
|
||||
zap.String("action", action),
|
||||
zap.String("url", url))
|
||||
|
||||
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(data)
|
||||
|
||||
// 设置访问令牌
|
||||
if c.accessToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken))
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
if err := c.httpClient.DoTimeout(req, resp, c.timeout); err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查HTTP状态码
|
||||
statusCode := resp.StatusCode()
|
||||
if statusCode != 200 {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var ob11Resp OB11Response
|
||||
if err := sonic.Unmarshal(resp.Body(), &ob11Resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
// 检查业务状态
|
||||
if ob11Resp.Status != "ok" && ob11Resp.Status != "async" {
|
||||
return &ob11Resp, fmt.Errorf("API error (retcode=%d)", ob11Resp.RetCode)
|
||||
}
|
||||
|
||||
c.logger.Debug("HTTP API call succeeded",
|
||||
zap.String("action", action),
|
||||
zap.String("status", ob11Resp.Status))
|
||||
|
||||
return &ob11Resp, nil
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *HTTPClient) Close() error {
|
||||
// fasthttp.Client 不需要显式关闭
|
||||
return nil
|
||||
}
|
||||
|
||||
// WSResponseWaiter WebSocket响应等待器
|
||||
type WSResponseWaiter struct {
|
||||
pending map[string]chan *OB11Response
|
||||
mu sync.RWMutex
|
||||
logger *zap.Logger
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewWSResponseWaiter 创建WebSocket响应等待器
|
||||
func NewWSResponseWaiter(timeout time.Duration, logger *zap.Logger) *WSResponseWaiter {
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return &WSResponseWaiter{
|
||||
pending: make(map[string]chan *OB11Response),
|
||||
logger: logger.Named("ws-waiter"),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 等待响应
|
||||
func (w *WSResponseWaiter) Wait(echo string) (*OB11Response, error) {
|
||||
w.mu.Lock()
|
||||
ch := make(chan *OB11Response, 1)
|
||||
w.pending[echo] = ch
|
||||
w.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
w.mu.Lock()
|
||||
delete(w.pending, echo)
|
||||
close(ch)
|
||||
w.mu.Unlock()
|
||||
}()
|
||||
|
||||
select {
|
||||
case resp := <-ch:
|
||||
return resp, nil
|
||||
case <-time.After(w.timeout):
|
||||
return nil, fmt.Errorf("timeout waiting for response (echo=%s)", echo)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify 通知响应到达
|
||||
func (w *WSResponseWaiter) Notify(resp *OB11Response) {
|
||||
if resp.Echo == "" {
|
||||
return
|
||||
}
|
||||
|
||||
w.mu.RLock()
|
||||
ch, ok := w.pending[resp.Echo]
|
||||
w.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
w.logger.Warn("Received response for unknown echo",
|
||||
zap.String("echo", resp.Echo))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- resp:
|
||||
w.logger.Debug("Notified response",
|
||||
zap.String("echo", resp.Echo))
|
||||
default:
|
||||
w.logger.Warn("Failed to notify response: channel full",
|
||||
zap.String("echo", resp.Echo))
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateEcho 生成唯一的echo标识
|
||||
func GenerateEcho() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
355
internal/adapter/onebot11/event.go
Normal file
355
internal/adapter/onebot11/event.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package onebot11
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"cellbot/internal/protocol"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// convertToEvent 将OneBot11原始事件转换为通用事件
|
||||
func (a *Adapter) convertToEvent(raw *RawEvent) (protocol.Event, error) {
|
||||
baseEvent := &protocol.BaseEvent{
|
||||
Timestamp: raw.Time,
|
||||
SelfID: strconv.FormatInt(raw.SelfID, 10),
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
switch raw.PostType {
|
||||
case PostTypeMessage:
|
||||
return a.convertMessageEvent(raw, baseEvent)
|
||||
case PostTypeNotice:
|
||||
return a.convertNoticeEvent(raw, baseEvent)
|
||||
case PostTypeRequest:
|
||||
return a.convertRequestEvent(raw, baseEvent)
|
||||
case PostTypeMetaEvent:
|
||||
return a.convertMetaEvent(raw, baseEvent)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown post_type: %s", raw.PostType)
|
||||
}
|
||||
}
|
||||
|
||||
// convertMessageEvent 转换消息事件
|
||||
func (a *Adapter) convertMessageEvent(raw *RawEvent, base *protocol.BaseEvent) (protocol.Event, error) {
|
||||
base.Type = protocol.EventTypeMessage
|
||||
base.DetailType = raw.MessageType
|
||||
base.SubType = raw.SubType
|
||||
|
||||
// 构建消息数据
|
||||
base.Data["message_id"] = raw.MessageID
|
||||
base.Data["user_id"] = raw.UserID
|
||||
base.Data["message"] = raw.Message
|
||||
base.Data["raw_message"] = raw.RawMessage
|
||||
base.Data["font"] = raw.Font
|
||||
|
||||
if raw.GroupID > 0 {
|
||||
base.Data["group_id"] = raw.GroupID
|
||||
}
|
||||
|
||||
if raw.Sender != nil {
|
||||
senderData := map[string]interface{}{
|
||||
"user_id": raw.Sender.UserID,
|
||||
"nickname": raw.Sender.Nickname,
|
||||
}
|
||||
if raw.Sender.Sex != "" {
|
||||
senderData["sex"] = raw.Sender.Sex
|
||||
}
|
||||
if raw.Sender.Age > 0 {
|
||||
senderData["age"] = raw.Sender.Age
|
||||
}
|
||||
if raw.Sender.Card != "" {
|
||||
senderData["card"] = raw.Sender.Card
|
||||
}
|
||||
if raw.Sender.Role != "" {
|
||||
senderData["role"] = raw.Sender.Role
|
||||
}
|
||||
base.Data["sender"] = senderData
|
||||
}
|
||||
|
||||
if raw.Anonymous != nil {
|
||||
base.Data["anonymous"] = map[string]interface{}{
|
||||
"id": raw.Anonymous.ID,
|
||||
"name": raw.Anonymous.Name,
|
||||
"flag": raw.Anonymous.Flag,
|
||||
}
|
||||
}
|
||||
|
||||
// 解析消息段
|
||||
if segments, err := a.parseMessageSegments(raw.Message); err == nil {
|
||||
base.Data["message_segments"] = segments
|
||||
}
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// convertNoticeEvent 转换通知事件
|
||||
func (a *Adapter) convertNoticeEvent(raw *RawEvent, base *protocol.BaseEvent) (protocol.Event, error) {
|
||||
base.Type = protocol.EventTypeNotice
|
||||
base.DetailType = raw.NoticeType
|
||||
base.SubType = raw.SubType
|
||||
|
||||
base.Data["user_id"] = raw.UserID
|
||||
|
||||
if raw.GroupID > 0 {
|
||||
base.Data["group_id"] = raw.GroupID
|
||||
}
|
||||
|
||||
if raw.OperatorID > 0 {
|
||||
base.Data["operator_id"] = raw.OperatorID
|
||||
}
|
||||
|
||||
if raw.Duration > 0 {
|
||||
base.Data["duration"] = raw.Duration
|
||||
}
|
||||
|
||||
// 根据不同的通知类型添加特定数据
|
||||
switch raw.NoticeType {
|
||||
case NoticeTypeGroupBan:
|
||||
base.Data["duration"] = raw.Duration
|
||||
case NoticeTypeGroupUpload:
|
||||
// 文件上传信息
|
||||
if raw.File != nil {
|
||||
base.Data["file"] = map[string]interface{}{
|
||||
"id": raw.File.ID,
|
||||
"name": raw.File.Name,
|
||||
"size": raw.File.Size,
|
||||
"busid": raw.File.Busid,
|
||||
}
|
||||
}
|
||||
case NoticeTypeGroupRecall, NoticeTypeFriendRecall:
|
||||
base.Data["message_id"] = raw.MessageID
|
||||
case NoticeTypeNotify:
|
||||
// 处理通知子类型
|
||||
if raw.TargetID > 0 {
|
||||
base.Data["target_id"] = raw.TargetID
|
||||
}
|
||||
if raw.HonorType != "" {
|
||||
base.Data["honor_type"] = raw.HonorType
|
||||
}
|
||||
}
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// convertRequestEvent 转换请求事件
|
||||
func (a *Adapter) convertRequestEvent(raw *RawEvent, base *protocol.BaseEvent) (protocol.Event, error) {
|
||||
base.Type = protocol.EventTypeRequest
|
||||
base.DetailType = raw.RequestType
|
||||
base.SubType = raw.SubType
|
||||
|
||||
base.Data["user_id"] = raw.UserID
|
||||
base.Data["comment"] = raw.Comment
|
||||
base.Data["flag"] = raw.Flag
|
||||
|
||||
if raw.GroupID > 0 {
|
||||
base.Data["group_id"] = raw.GroupID
|
||||
}
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// convertMetaEvent 转换元事件
|
||||
func (a *Adapter) convertMetaEvent(raw *RawEvent, base *protocol.BaseEvent) (protocol.Event, error) {
|
||||
base.Type = protocol.EventTypeMeta
|
||||
base.DetailType = raw.MetaType
|
||||
|
||||
if raw.Status != nil {
|
||||
statusData := map[string]interface{}{
|
||||
"online": raw.Status.Online,
|
||||
"good": raw.Status.Good,
|
||||
}
|
||||
if raw.Status.Stat != nil {
|
||||
statusData["stat"] = map[string]interface{}{
|
||||
"packet_received": raw.Status.Stat.PacketReceived,
|
||||
"packet_sent": raw.Status.Stat.PacketSent,
|
||||
"packet_lost": raw.Status.Stat.PacketLost,
|
||||
"message_received": raw.Status.Stat.MessageReceived,
|
||||
"message_sent": raw.Status.Stat.MessageSent,
|
||||
"disconnect_times": raw.Status.Stat.DisconnectTimes,
|
||||
"lost_times": raw.Status.Stat.LostTimes,
|
||||
"last_message_time": raw.Status.Stat.LastMessageTime,
|
||||
}
|
||||
}
|
||||
base.Data["status"] = statusData
|
||||
}
|
||||
|
||||
if raw.Interval > 0 {
|
||||
base.Data["interval"] = raw.Interval
|
||||
}
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// parseMessageSegments 解析消息段
|
||||
func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, error) {
|
||||
if message == nil {
|
||||
return nil, fmt.Errorf("message is nil")
|
||||
}
|
||||
|
||||
// 如果是字符串,转换为文本消息段
|
||||
if str, ok := message.(string); ok {
|
||||
return []MessageSegment{
|
||||
{
|
||||
Type: SegmentTypeText,
|
||||
Data: map[string]interface{}{
|
||||
"text": str,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 如果是数组,解析为消息段数组
|
||||
var segments []MessageSegment
|
||||
data, err := sonic.Marshal(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
if err := sonic.Unmarshal(data, &segments); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal message segments: %w", err)
|
||||
}
|
||||
|
||||
return segments, nil
|
||||
}
|
||||
|
||||
// BuildMessage 构建OneBot11消息
|
||||
func BuildMessage(segments []MessageSegment) interface{} {
|
||||
if len(segments) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 如果只有一个文本消息段,直接返回文本
|
||||
if len(segments) == 1 && segments[0].Type == SegmentTypeText {
|
||||
if text, ok := segments[0].Data["text"].(string); ok {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
// BuildTextMessage 构建文本消息
|
||||
func BuildTextMessage(text string) []MessageSegment {
|
||||
return []MessageSegment{
|
||||
{
|
||||
Type: SegmentTypeText,
|
||||
Data: map[string]interface{}{
|
||||
"text": text,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildImageMessage 构建图片消息
|
||||
func BuildImageMessage(file string) []MessageSegment {
|
||||
return []MessageSegment{
|
||||
{
|
||||
Type: SegmentTypeImage,
|
||||
Data: map[string]interface{}{
|
||||
"file": file,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildAtMessage 构建@消息
|
||||
func BuildAtMessage(userID int64) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeAt,
|
||||
Data: map[string]interface{}{
|
||||
"qq": userID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildReplyMessage 构建回复消息
|
||||
func BuildReplyMessage(messageID int32) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeReply,
|
||||
Data: map[string]interface{}{
|
||||
"id": messageID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFaceMessage 构建表情消息
|
||||
func BuildFaceMessage(faceID int) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeFace,
|
||||
Data: map[string]interface{}{
|
||||
"id": faceID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildRecordMessage 构建语音消息
|
||||
func BuildRecordMessage(file string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeRecord,
|
||||
Data: map[string]interface{}{
|
||||
"file": file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildVideoMessage 构建视频消息
|
||||
func BuildVideoMessage(file string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeVideo,
|
||||
Data: map[string]interface{}{
|
||||
"file": file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildShareMessage 构建分享消息
|
||||
func BuildShareMessage(url, title string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeShare,
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"title": title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildLocationMessage 构建位置消息
|
||||
func BuildLocationMessage(lat, lon float64, title, content string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeLocation,
|
||||
Data: map[string]interface{}{
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"title": title,
|
||||
"content": content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildMusicMessage 构建音乐消息
|
||||
func BuildMusicMessage(musicType, musicID string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeMusic,
|
||||
Data: map[string]interface{}{
|
||||
"type": musicType,
|
||||
"id": musicID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildCustomMusicMessage 构建自定义音乐消息
|
||||
func BuildCustomMusicMessage(url, audio, title, content, image string) MessageSegment {
|
||||
return MessageSegment{
|
||||
Type: SegmentTypeMusic,
|
||||
Data: map[string]interface{}{
|
||||
"type": "custom",
|
||||
"url": url,
|
||||
"audio": audio,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"image": image,
|
||||
},
|
||||
}
|
||||
}
|
||||
187
internal/adapter/onebot11/types.go
Normal file
187
internal/adapter/onebot11/types.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package onebot11
|
||||
|
||||
// RawEvent OneBot11原始事件
|
||||
type RawEvent struct {
|
||||
Time int64 `json:"time"`
|
||||
SelfID int64 `json:"self_id"`
|
||||
PostType string `json:"post_type"`
|
||||
MessageType string `json:"message_type,omitempty"`
|
||||
SubType string `json:"sub_type,omitempty"`
|
||||
MessageID int32 `json:"message_id,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
GroupID int64 `json:"group_id,omitempty"`
|
||||
Message interface{} `json:"message,omitempty"`
|
||||
RawMessage string `json:"raw_message,omitempty"`
|
||||
Font int32 `json:"font,omitempty"`
|
||||
Sender *Sender `json:"sender,omitempty"`
|
||||
Anonymous *Anonymous `json:"anonymous,omitempty"`
|
||||
NoticeType string `json:"notice_type,omitempty"`
|
||||
OperatorID int64 `json:"operator_id,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
RequestType string `json:"request_type,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Flag string `json:"flag,omitempty"`
|
||||
MetaType string `json:"meta_event_type,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Interval int64 `json:"interval,omitempty"`
|
||||
File *FileInfo `json:"file,omitempty"` // 群文件上传信息
|
||||
TargetID int64 `json:"target_id,omitempty"` // 戳一戳、红包运气王目标ID
|
||||
HonorType string `json:"honor_type,omitempty"` // 群荣誉类型
|
||||
Extra map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
// Sender 发送者信息
|
||||
type Sender struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex string `json:"sex,omitempty"`
|
||||
Age int32 `json:"age,omitempty"`
|
||||
Card string `json:"card,omitempty"` // 群名片/备注
|
||||
Area string `json:"area,omitempty"` // 地区
|
||||
Level string `json:"level,omitempty"` // 成员等级
|
||||
Role string `json:"role,omitempty"` // 角色: owner, admin, member
|
||||
Title string `json:"title,omitempty"` // 专属头衔
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Busid int64 `json:"busid"`
|
||||
}
|
||||
|
||||
// Anonymous 匿名信息
|
||||
type Anonymous struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Flag string `json:"flag"`
|
||||
}
|
||||
|
||||
// Status 状态信息
|
||||
type Status struct {
|
||||
Online bool `json:"online"`
|
||||
Good bool `json:"good"`
|
||||
Stat *Stat `json:"stat,omitempty"`
|
||||
}
|
||||
|
||||
// Stat 统计信息
|
||||
type Stat struct {
|
||||
PacketReceived int64 `json:"packet_received"`
|
||||
PacketSent int64 `json:"packet_sent"`
|
||||
PacketLost int32 `json:"packet_lost"`
|
||||
MessageReceived int64 `json:"message_received"`
|
||||
MessageSent int64 `json:"message_sent"`
|
||||
DisconnectTimes int32 `json:"disconnect_times"`
|
||||
LostTimes int32 `json:"lost_times"`
|
||||
LastMessageTime int64 `json:"last_message_time"`
|
||||
}
|
||||
|
||||
// OB11Action OneBot11动作
|
||||
type OB11Action struct {
|
||||
Action string `json:"action"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
}
|
||||
|
||||
// OB11Response OneBot11响应
|
||||
type OB11Response struct {
|
||||
Status string `json:"status"`
|
||||
RetCode int `json:"retcode"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
}
|
||||
|
||||
// MessageSegment 消息段
|
||||
type MessageSegment struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// 消息段类型常量
|
||||
const (
|
||||
SegmentTypeText = "text"
|
||||
SegmentTypeFace = "face"
|
||||
SegmentTypeImage = "image"
|
||||
SegmentTypeRecord = "record"
|
||||
SegmentTypeVideo = "video"
|
||||
SegmentTypeAt = "at"
|
||||
SegmentTypeRPS = "rps"
|
||||
SegmentTypeDice = "dice"
|
||||
SegmentTypeShake = "shake"
|
||||
SegmentTypePoke = "poke"
|
||||
SegmentTypeAnonymous = "anonymous"
|
||||
SegmentTypeShare = "share"
|
||||
SegmentTypeContact = "contact"
|
||||
SegmentTypeLocation = "location"
|
||||
SegmentTypeMusic = "music"
|
||||
SegmentTypeReply = "reply"
|
||||
SegmentTypeForward = "forward"
|
||||
SegmentTypeNode = "node"
|
||||
SegmentTypeXML = "xml"
|
||||
SegmentTypeJSON = "json"
|
||||
)
|
||||
|
||||
// 事件类型常量
|
||||
const (
|
||||
PostTypeMessage = "message"
|
||||
PostTypeNotice = "notice"
|
||||
PostTypeRequest = "request"
|
||||
PostTypeMetaEvent = "meta_event"
|
||||
)
|
||||
|
||||
// 消息类型常量
|
||||
const (
|
||||
MessageTypePrivate = "private"
|
||||
MessageTypeGroup = "group"
|
||||
)
|
||||
|
||||
// 通知类型常量
|
||||
const (
|
||||
NoticeTypeGroupUpload = "group_upload"
|
||||
NoticeTypeGroupAdmin = "group_admin"
|
||||
NoticeTypeGroupDecrease = "group_decrease"
|
||||
NoticeTypeGroupIncrease = "group_increase"
|
||||
NoticeTypeGroupBan = "group_ban"
|
||||
NoticeTypeFriendAdd = "friend_add"
|
||||
NoticeTypeGroupRecall = "group_recall"
|
||||
NoticeTypeFriendRecall = "friend_recall"
|
||||
NoticeTypeNotify = "notify"
|
||||
)
|
||||
|
||||
// 通知子类型常量
|
||||
const (
|
||||
// 群管理员变动
|
||||
SubTypeSet = "set"
|
||||
SubTypeUnset = "unset"
|
||||
|
||||
// 群成员减少
|
||||
SubTypeLeave = "leave"
|
||||
SubTypeKick = "kick"
|
||||
SubTypeKickMe = "kick_me"
|
||||
|
||||
// 群成员增加
|
||||
SubTypeApprove = "approve"
|
||||
SubTypeInvite = "invite"
|
||||
|
||||
// 群禁言
|
||||
SubTypeBan = "ban"
|
||||
SubTypeLiftBan = "lift_ban"
|
||||
|
||||
// 通知类型
|
||||
SubTypePoke = "poke" // 戳一戳
|
||||
SubTypeLuckyKing = "lucky_king" // 红包运气王
|
||||
SubTypeHonor = "honor" // 群荣誉变更
|
||||
)
|
||||
|
||||
// 请求类型常量
|
||||
const (
|
||||
RequestTypeFriend = "friend"
|
||||
RequestTypeGroup = "group"
|
||||
)
|
||||
|
||||
// 元事件类型常量
|
||||
const (
|
||||
MetaEventTypeLifecycle = "lifecycle"
|
||||
MetaEventTypeHeartbeat = "heartbeat"
|
||||
)
|
||||
Reference in New Issue
Block a user