From 5857ab2b46f7f5a8ce837b87805e43d1533ce26f Mon Sep 17 00:00:00 2001 From: SanaeFox <36219542+Hoshinonyaruko@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:59:08 +0800 Subject: [PATCH] Beta129 (#302) * beta122 * beta123 * beta124 * beta124 * beta124 * beta127 * beta128 * beta129 --- Processor/ProcessGroupMessage.go | 2 + Processor/ProcessGuildATMessage.go | 4 + Processor/ProcessGuildNormalMessage.go | 4 + Processor/Processor.go | 76 ++++++++++-- botgo/dto/interaction.go | 34 ++++-- botgo/dto/keyboard/keyboard.go | 8 +- config/config.go | 35 ++++++ handlers/message_parser.go | 4 +- handlers/send_group_msg.go | 83 +++++++++++-- main.go | 3 +- template/config_template.go | 9 +- wsclient/ws.go | 158 +++++++++++++++++-------- 12 files changed, 329 insertions(+), 91 deletions(-) diff --git a/Processor/ProcessGroupMessage.go b/Processor/ProcessGroupMessage.go index 87d2e8a1..91ece57a 100644 --- a/Processor/ProcessGroupMessage.go +++ b/Processor/ProcessGroupMessage.go @@ -169,6 +169,8 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error { echo.AddMsgType(AppIDString, GroupID64, "group") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(GroupID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) // 调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/ProcessGuildATMessage.go b/Processor/ProcessGuildATMessage.go index c9426919..6bd646d8 100644 --- a/Processor/ProcessGuildATMessage.go +++ b/Processor/ProcessGuildATMessage.go @@ -111,6 +111,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { //todo 完善频道转换 //懒message_id池 echo.AddLazyMessageId(data.ChannelID, data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(onebotMsg) @@ -264,6 +266,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { echo.AddMsgType(AppIDString, ChannelID64, "guild") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(ChannelID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/ProcessGuildNormalMessage.go b/Processor/ProcessGuildNormalMessage.go index da7c32a6..1c280e7e 100644 --- a/Processor/ProcessGuildNormalMessage.go +++ b/Processor/ProcessGuildNormalMessage.go @@ -111,6 +111,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { //todo 完善频道ob信息 //懒message_id池 echo.AddLazyMessageId(data.ChannelID, data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(onebotMsg) @@ -272,6 +274,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { echo.AddMsgType(AppIDString, ChannelID64, "guild") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(ChannelID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/Processor.go b/Processor/Processor.go index b95d58ba..cc104dca 100644 --- a/Processor/Processor.go +++ b/Processor/Processor.go @@ -112,6 +112,18 @@ type OnebotPrivateMessage struct { IsBindedUserId bool `json:"is_binded_user_id,omitempty"` //当前用户号号是否是binded后的 } +// onebotv11标准扩展 +type OnebotInteractionNotice struct { + GroupID int64 `json:"group_id,omitempty"` + NoticeType string `json:"notice_type,omitempty"` + PostType string `json:"post_type,omitempty"` + SelfID int64 `json:"self_id,omitempty"` + SubType string `json:"sub_type,omitempty"` + Time int64 `json:"time,omitempty"` + UserID int64 `json:"user_id,omitempty"` + Data *dto.WSInteractionData `json:"data,omitempty"` +} + type PrivateSender struct { Nickname string `json:"nickname"` UserID int64 `json:"user_id"` // Can be either string or int depending on logic @@ -123,19 +135,63 @@ func FoxTimestamp() int64 { // ProcessInlineSearch 处理内联查询 func (p *Processors) ProcessInlineSearch(data *dto.WSInteractionData) error { - //ctx := context.Background() // 或从更高级别传递一个上下文 + // 转换appid + var userid64 int64 + var GroupID64 int64 + var err error + var fromgid, fromuid string + if data.GroupOpenID != "" { + fromgid = data.GroupOpenID + fromuid = data.GroupMemberOpenID + } else { + fromgid = data.ChannelID + fromuid = "0" + } + if config.GetIdmapPro() { + //将真实id转为int userid64 + GroupID64, userid64, err = idmap.StoreIDv2Pro(fromgid, fromuid) + if err != nil { + mylog.Fatalf("Error storing ID: %v", err) + } + //当参数不全 + _, _ = idmap.StoreIDv2(fromgid) + _, _ = idmap.StoreIDv2(fromuid) + if !config.GetHashIDValue() { + mylog.Fatalf("避坑日志:你开启了高级id转换,请设置hash_id为true,并且删除idmaps并重启") + } + } else { + // 映射str的GroupID到int + GroupID64, err = idmap.StoreIDv2(fromgid) + if err != nil { + mylog.Errorf("failed to convert ChannelID to int: %v", err) + return nil + } + // 映射str的userid到int + userid64, err = idmap.StoreIDv2(fromuid) + if err != nil { + mylog.Printf("Error storing ID: %v", err) + return nil + } + } + notice := &OnebotInteractionNotice{ + GroupID: GroupID64, + NoticeType: "interaction", + PostType: "notice", + SelfID: int64(p.Settings.AppID), + SubType: "create", + Time: time.Now().Unix(), + UserID: userid64, + Data: data, + } - // 在这里处理内联查询 - // 这可能涉及解析查询、调用某些API、获取结果并格式化为响应 - // ... + //调试 + PrintStructWithFieldNames(notice) - // 示例:发送响应 - // response := "Received your interaction!" // 创建响应消息 - // err := p.api.PostInteractionResponse(ctx, response) // 替换为您的OpenAPI方法 - // if err != nil { - // return err - // } + // Convert OnebotGroupMessage to map and send + noticeMap := structToMap(notice) + //上报信息到onebotv11应用端(正反ws) + p.BroadcastMessageToAll(noticeMap) return nil } diff --git a/botgo/dto/interaction.go b/botgo/dto/interaction.go index 9d633c5b..31544046 100644 --- a/botgo/dto/interaction.go +++ b/botgo/dto/interaction.go @@ -1,16 +1,20 @@ package dto -import "encoding/json" - // Interaction 互动行为对象 type Interaction struct { - ID string `json:"id,omitempty"` // 互动行为唯一标识 - ApplicationID string `json:"application_id,omitempty"` // 应用ID - Type InteractionType `json:"type,omitempty"` // 互动类型 - Data *InteractionData `json:"data,omitempty"` // 互动数据 - GuildID string `json:"guild_id,omitempty"` // 频道 ID - ChannelID string `json:"channel_id,omitempty"` // 子频道 ID - Version uint32 `json:"version,omitempty"` // 版本,默认为 1 + ID string `json:"id,omitempty"` // 平台方事件 ID + Type InteractionType `json:"type,omitempty"` // 消息按钮: 11, 单聊快捷菜单: 12 + Scene string `json:"scene,omitempty"` // 事件发生的场景 + ChatType int `json:"chat_type,omitempty"` // 频道场景: 0, 群聊场景: 1, 单聊场景: 2 + Timestamp string `json:"timestamp,omitempty"` // 触发时间 RFC 3339 格式 + GuildID string `json:"guild_id,omitempty"` // 频道的 openid + ChannelID string `json:"channel_id,omitempty"` // 文字子频道的 openid + UserOpenID string `json:"user_openid,omitempty"` // 单聊按钮触发的用户 openid + GroupOpenID string `json:"group_openid,omitempty"` // 群的 openid + GroupMemberOpenID string `json:"group_member_openid,omitempty"` // 群成员 openid + Data *InteractionData `json:"data,omitempty"` // 互动数据 + Version uint32 `json:"version,omitempty"` // 版本,默认为 1 + ApplicationID string `json:"application_id,omitempty"` // 机器人的 appid } // InteractionType 互动类型 @@ -25,9 +29,15 @@ const ( // InteractionData 互动数据 type InteractionData struct { - Name string `json:"name,omitempty"` // 标题 - Type InteractionDataType `json:"type,omitempty"` // 数据类型,不同数据类型对应不同的 resolved 数据 - Resolved json.RawMessage `json:"resolved,omitempty"` // 跟不同的互动类型和数据类型有关系的数据 + Name string `json:"name,omitempty"` // 标题 + Type InteractionDataType `json:"type,omitempty"` // 数据类型 + Resolved struct { + ButtonData string `json:"button_data,omitempty"` // 操作按钮的 data 字段值 + ButtonID string `json:"button_id,omitempty"` // 操作按钮的 id 字段值 + UserID string `json:"user_id,omitempty"` // 操作的用户 userid + FeatureID string `json:"feature_id,omitempty"` // 操作按钮的 feature id + MessageID string `json:"message_id,omitempty"` // 操作的消息 id + } `json:"resolved,omitempty"` } // InteractionDataType 互动数据类型 diff --git a/botgo/dto/keyboard/keyboard.go b/botgo/dto/keyboard/keyboard.go index b6c26149..c1087fb5 100644 --- a/botgo/dto/keyboard/keyboard.go +++ b/botgo/dto/keyboard/keyboard.go @@ -55,7 +55,7 @@ type RenderData struct { // Action 按纽点击操作 type Action struct { - Type ActionType `json:"type,omitempty"` // 操作类型 + Type ActionType `json:"type,omitempty"` // 操作类型 设置 0 跳转按钮:http 或 小程序 客户端识别 scheme,设置 1 回调按钮:回调后台接口, data 传给后台,设置 2 指令按钮:自动在输入框插入 @bot data Permission *Permission `json:"permission,omitempty"` // 可操作 ClickLimit uint32 `json:"click_limit,omitempty"` // 可点击的次数, 默认不限 Data string `json:"data,omitempty"` // 操作相关数据 @@ -68,10 +68,10 @@ type Action struct { // Permission 按纽操作权限 type Permission struct { - // Type 操作权限类型 + // Type 操作权限类型 0 指定用户可操作,1 仅管理者可操作,2 所有人可操作,3 指定身份组可操作(仅频道可用) Type PermissionType `json:"type,omitempty"` - // SpecifyRoleIDs 身份组 + // SpecifyRoleIDs 身份组(仅频道可用) SpecifyRoleIDs []string `json:"specify_role_ids,omitempty"` - // SpecifyUserIDs 指定 UserID + // SpecifyUserIDs 指定 UserID 有权限的用户 id 的列表 SpecifyUserIDs []string `json:"specify_user_ids,omitempty"` } diff --git a/config/config.go b/config/config.go index 9f8a7940..78ddf764 100644 --- a/config/config.go +++ b/config/config.go @@ -133,6 +133,9 @@ type Settings struct { TransFormApiIds bool `yaml:"transform_api_ids"` CustomTemplateID string `yaml:"custom_template_id"` KeyBoardID string `yaml:"keyboard_id"` + Uin int64 `yaml:"uin"` + VwhitePrefixMode bool `yaml:"v_white_prefix_mode"` + Enters []string `yaml:"enters"` } // LoadConfig 从文件中加载配置并初始化单例配置 @@ -1676,3 +1679,35 @@ func GetKeyBoardID() string { } return instance.Settings.KeyBoardID } + +// 获取Uin String +func GetUinStr() string { + mu.Lock() + defer mu.Unlock() + if instance != nil { + return fmt.Sprintf("%d", instance.Settings.Uin) + } + return "0" +} + +// 获取 VV GetVwhitePrefixMode 的值 +func GetVwhitePrefixMode() bool { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to VwhitePrefixMode value.") + return false + } + return instance.Settings.VwhitePrefixMode +} + +// 获取Enters的值 +func GetEnters() []string { + mu.Lock() + defer mu.Unlock() + if instance != nil { + return instance.Settings.Enters + } + return nil // 返回nil,如果instance为nil +} diff --git a/handlers/message_parser.go b/handlers/message_parser.go index 4298ce71..48e3519a 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -503,8 +503,8 @@ func RevertTransformedText(data interface{}, msgtype string, api openapi.OpenAPI aliases := config.GetAlias() messageText = processMessageText(messageText, aliases) //mylog.Printf("4[%v]", messageText) - // 检查是否启用白名单模式 - if config.GetWhitePrefixMode() && matchedPrefix != nil { + // 检查是否启用二级白名单模式 + if config.GetVwhitePrefixMode() && matchedPrefix != nil { // 获取白名单反转标志 whiteBypassRevers := config.GetWhiteBypassRevers() diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index 93b869e7..651827d6 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -85,6 +85,11 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap //由于实现了Params的自定义unmarshell 所以可以类型安全的断言为string messageID = echo.GetLazyMessagesId(message.Params.GroupID.(string)) mylog.Printf("GetLazyMessagesId: %v", messageID) + //如果应用端传递了user_id 就让at不要顺序乱套 + if message.Params.UserID != nil { + messageID = echo.GetLazyMessagesId(message.Params.UserID.(string)) + mylog.Printf("GetLazyMessagesIdv2: %v", messageID) + } if messageID != "" { //尝试发送栈内信息 SSM = true @@ -1082,6 +1087,10 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage mylog.Printf("获取图片宽高出错") } imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height) + // 将所有的\r\n替换为\r + messageText = strings.ReplaceAll(messageText, "\r\n", "\r") + // 将所有的\n替换为\r + messageText = strings.ReplaceAll(messageText, "\n", "\r") // 检查messageText是否以\r开头 if !strings.HasPrefix(messageText, "\r") { messageText = "\r" + messageText @@ -1108,26 +1117,64 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage buttonCount := 0 // 当前行的按钮计数 for _, whiteLabel := range whiteList { - // 使用 strconv.Atoi 检查 whiteLabel 是否为纯数字 - if _, err := strconv.Atoi(whiteLabel); err == nil { - // 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代 - continue + // 如果whiteList的成员数大于或等于15,才检查whiteLabel是否为纯数字 + if len(whiteList) >= 15 { + if _, err := strconv.Atoi(whiteLabel); err == nil { + // 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代 + // 避免 因为纯数字按钮太多导致混乱,但是少量的纯数字按钮可以允许 + continue + } + } + // 检查msg_on_touch是否已经以whiteLabel结尾 + //场景 按钮递归时 比如 随机meme 再来一次,同时随机meme是*类型一级指令 + var dataLabel string + if !strings.HasSuffix(msg_on_touch, whiteLabel) { + dataLabel = msg_on_touch + whiteLabel + } else { + dataLabel = msg_on_touch } + var actiontype keyboard.ActionType + var permission *keyboard.Permission + var actiondata string + //检查是否设置了enter数组 + enter := checkDataLabelPrefix(dataLabel) + switch whiteLabel { + case "邀请机器人": + botuin := config.GetUinStr() + botappid := config.GetAppIDStr() + boturl := BuildQQBotShareLink(botuin, botappid) + actiontype = 0 + actiondata = boturl + permission = &keyboard.Permission{ + Type: 2, // 所有人可操作 + } + case "测试按钮回调": + actiontype = 1 + actiondata = "收到就代表是管理员哦" + permission = &keyboard.Permission{ + Type: 1, // 仅管理可操作 + } + default: + actiontype = 2 //帮用户输入指令 + actiondata = dataLabel //从虚拟前缀的二级指令组合md按钮 + permission = &keyboard.Permission{ + Type: 2, // 所有人可操作 + } + } // 创建按钮 button := &keyboard.Button{ RenderData: &keyboard.RenderData{ Label: whiteLabel, VisitedLabel: whiteLabel, - Style: 1, + Style: 1, //蓝色边缘 }, Action: &keyboard.Action{ - Type: 2, // 帮用户输入二级指令 - Permission: &keyboard.Permission{ - Type: 2, //所有人可操作 - }, - Data: msg_on_touch + " " + whiteLabel, + Type: actiontype, + Permission: permission, + Data: actiondata, UnsupportTips: "请升级新版手机QQ", + Enter: enter, }, } @@ -1153,3 +1200,19 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage } return md, kb, transmd } + +// 构建QQ群机器人分享链接的函数 +func BuildQQBotShareLink(uin string, appid string) string { + return fmt.Sprintf("https://qun.qq.com/qunpro/robot/qunshare?robot_uin=%s&robot_appid=%s", uin, appid) +} + +// 检查dataLabel是否以config中getenters返回的任一字符串开头 +func checkDataLabelPrefix(dataLabel string) bool { + enters := config.GetEnters() + for _, enter := range enters { + if enter != "" && strings.HasPrefix(dataLabel, enter) { + return true + } + } + return false +} diff --git a/main.go b/main.go index e45c8c6a..b2ca9fdd 100644 --- a/main.go +++ b/main.go @@ -523,7 +523,6 @@ func DirectMessageHandler() event.DirectMessageEventHandler { // CreateMessageHandler 处理消息事件 私域的事件 不at信息 func CreateMessageHandler() event.MessageEventHandler { return func(event *dto.WSPayload, data *dto.WSMessageData) error { - log.Println("收到私域信息", data) return p.ProcessGuildNormalMessage(data) } } @@ -531,7 +530,7 @@ func CreateMessageHandler() event.MessageEventHandler { // InteractionHandler 处理内联交互事件 func InteractionHandler() event.InteractionEventHandler { return func(event *dto.WSPayload, data *dto.WSInteractionData) error { - log.Println(data) + mylog.Printf("收到按钮回调:%v", data) return p.ProcessInlineSearch(data) } } diff --git a/template/config_template.go b/template/config_template.go index bec2f464..cda74bc4 100644 --- a/template/config_template.go +++ b/template/config_template.go @@ -6,7 +6,8 @@ settings: #反向ws设置 ws_address: ["ws://:"] # WebSocket服务的地址 支持多个["","",""] ws_token: ["","",""] #连接wss地址时服务器所需的token,按顺序一一对应,如果是ws地址,没有密钥,请留空. - app_id: 12345 # 你的应用ID + app_id: 12345 # 你的应用ID + uin : 0 # 你的机器人QQ号,点击机器人资料卡查看 token: "" # 你的应用令牌 client_secret: "" # 你的客户端密钥 @@ -19,7 +20,7 @@ settings: # - "MemberEventHandler" # 频道成员新增 # - "ChannelEventHandler" # 频道事件 # - "CreateMessageHandler" # 频道不at信息 私域机器人需要开启 公域机器人开启会连接失败 - # - "InteractionHandler" # 添加频道互动回应 + # - "InteractionHandler" # 添加频道互动回应 卡片按钮data回调事件 # - "GroupATMessageEventHandler" # 群at信息 仅频道机器人时候需要注释 # - "C2CMessageEventHandler" # 群私聊 仅频道机器人时候需要注释 # - "ThreadEventHandler" # 发帖事件 (当前版本已禁用) @@ -92,6 +93,7 @@ settings: add_at_group : false #自动在群聊指令前加上at,某些机器人写法特别,必须有at才反应时,请打开,默认请关闭(如果需要at,不需要at指令混杂,请优化代码适配群场景,群场景目前没有at概念) white_prefix_mode : false #公域 过审用 指令白名单模式开关 如果审核严格 请开启并设置白名单指令 以白名单开头的指令会被通过,反之被拦截 + v_white_prefix_mode : true #虚拟二级指令(虚拟前缀)的白名单,默认开启,有二级指令帮助的效果 white_prefixs : [""] #可设置多个 比如设置 机器人 测试 则只有信息以机器人 测试开头会相应 remove_prefix remove_at 需为true时生效 white_bypass : [] #格式[1,2,3],白名单不生效的群或用户(私聊时),用于设置自己的灰度沙箱群/灰度沙箱私聊,避免开发测试时反复开关白名单的不便,请勿用于生产环境. white_enable : [true,true,true,true,true] #指令白名单生效范围,5个分别对应,频道公(ATMessageEventHandler),频道私(CreateMessageHandler),频道私聊,群,群私聊,改成false,这个范围就不生效指令白名单(使用场景:群全量,频道私域的机器人,或有私信资质的机器人) @@ -101,11 +103,12 @@ settings: black_prefix_mode : false #公私域 过审用 指令黑名单模式开关 过滤被审核打回的指令不响应 无需改机器人后端 black_prefixs : [""] #可设置多个 比如设置 查询 则查询开头的信息均被拦截 防止审核失败 alias : ["",""] #两两成对,指令替换,"a","b","c","d"代表将a开头替换为b开头,c开头替换为d开头. + enters : ["",""] #自动md卡片点击直接触发,小众功能,满足以下条件:应用端支持双向echo+设置了visual_prefixs和whiteList visual_prefixs : #虚拟前缀 与white_prefixs配合使用 处理流程自动忽略该前缀 remove_prefix remove_at 需为true时生效 - prefix: "" #虚拟前缀开头 例 你有3个指令 帮助 测试 查询 将 prefix 设置为 工具类 后 则可通过 工具类 帮助 触发机器人 whiteList: [""] #开关状态取决于 white_prefix_mode 为每一个二级指令头设计独立的白名单 - No_White_Response : "" + No_White_Response : "" #带有*代表不忽略掉,但是应用二级白名单的普通指令 如果 whiteList=邀请机器人 就是邀请机器人按钮 - prefix: "" whiteList: [""] No_White_Response : "" diff --git a/wsclient/ws.go b/wsclient/ws.go index b5eff231..5227ffa2 100644 --- a/wsclient/ws.go +++ b/wsclient/ws.go @@ -25,13 +25,14 @@ type WebSocketClient struct { cancel context.CancelFunc mutex sync.Mutex // 用于同步写入和重连操作的互斥锁 isReconnecting bool - sendFailures chan map[string]interface{} + sendFailures []map[string]interface{} // 存储失败的消息 + } // 发送json信息给onebot应用端 -func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { - c.mutex.Lock() // 在写操作之前锁定 - defer c.mutex.Unlock() // 确保在函数返回时解锁 +func (client *WebSocketClient) SendMessage(message map[string]interface{}) error { + client.mutex.Lock() // 在写操作之前锁定 + defer client.mutex.Unlock() // 确保在函数返回时解锁 msgBytes, err := json.Marshal(message) if err != nil { @@ -39,13 +40,11 @@ func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { return err } - err = c.conn.WriteMessage(websocket.TextMessage, msgBytes) + err = client.conn.WriteMessage(websocket.TextMessage, msgBytes) if err != nil { mylog.Println("Error sending message:", err) - // 发送失败,将消息放入channel - go func() { - c.sendFailures <- message - }() + // 发送失败,将消息添加到切片 + client.sendFailures = append(client.sendFailures, message) return err } @@ -53,72 +52,133 @@ func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { } // 处理onebotv11应用端发来的信息 -func (c *WebSocketClient) handleIncomingMessages(ctx context.Context, cancel context.CancelFunc) { +func (client *WebSocketClient) handleIncomingMessages(ctx context.Context, cancel context.CancelFunc) { for { - _, msg, err := c.conn.ReadMessage() + _, msg, err := client.conn.ReadMessage() if err != nil { mylog.Println("WebSocket connection closed:", err) cancel() // 取消心跳 goroutine - if !c.isReconnecting { - go c.Reconnect() + if !client.isReconnecting { + go client.Reconnect() } return // 退出循环,不再尝试读取消息 } - go c.recvMessage(msg) + go client.recvMessage(msg) } } // 断线重连 func (client *WebSocketClient) Reconnect() { - client.mutex.Lock() - if client.isReconnecting { - client.mutex.Unlock() - return // 如果已经有其他携程在重连了,就直接返回 + client.isReconnecting = true + + addresses := config.GetWsAddress() + tokens := config.GetWsToken() + + var token string + for index, address := range addresses { + if address == client.urlStr && index < len(tokens) { + token = tokens[index] + break + } } - // 暂存旧的 sendFailures channel,不要关闭它 - oldSendFailures := client.sendFailures + // 检查URL中是否有access_token参数 + mp := getParamsFromURI(client.urlStr) + if val, ok := mp["access_token"]; ok { + token = val + } - client.isReconnecting = true - client.mutex.Unlock() + headers := http.Header{ + "User-Agent": []string{"CQHttp/4.15.0"}, + "X-Client-Role": []string{"Universal"}, + "X-Self-ID": []string{fmt.Sprintf("%d", client.botID)}, + } + + if token != "" { + headers["Authorization"] = []string{"Token " + token} + } + mylog.Printf("准备使用token[%s]重新连接到[%s]\n", token, client.urlStr) + dialer := websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + } + + var conn *websocket.Conn + var err error + + maxRetryAttempts := config.GetReconnecTimes() + retryCount := 0 + for { + mylog.Println("Dialing URL:", client.urlStr) + conn, _, err = dialer.Dial(client.urlStr, headers) + if err != nil { + retryCount++ + if retryCount > maxRetryAttempts { + mylog.Printf("Exceeded maximum retry attempts for WebSocket[%v]: %v\n", client.urlStr, err) + return + } + mylog.Printf("Failed to connect to WebSocket[%v]: %v, retrying in 5 seconds...\n", client.urlStr, err) + time.Sleep(5 * time.Second) // sleep for 5 seconds before retrying + } else { + mylog.Printf("Successfully connected to %s.\n", client.urlStr) // 输出连接成功提示 + break // successfully connected, break the loop + } + } + // 复用现有的client完成重连 + client.conn = conn + + // 再次发送元事件 + message := map[string]interface{}{ + "meta_event_type": "lifecycle", + "post_type": "meta_event", + "self_id": client.botID, + "sub_type": "connect", + "time": int(time.Now().Unix()), + } + + mylog.Printf("Message: %+v\n", message) + + err = client.SendMessage(message) + if err != nil { + // handle error + mylog.Printf("Error sending message: %v\n", err) + } + + //退出老的sendHeartbeat和handleIncomingMessages + client.cancel() + + // Starting goroutine for heartbeats and another for listening to messages + ctx, cancel := context.WithCancel(context.Background()) + + client.cancel = cancel + heartbeatinterval := config.GetHeartBeatInterval() + go client.sendHeartbeat(ctx, client.botID, heartbeatinterval) + go client.handleIncomingMessages(ctx, cancel) defer func() { - client.mutex.Lock() client.isReconnecting = false - client.mutex.Unlock() }() - reconnecttimes := config.GetReconnecTimes() - newClient, err := NewWebSocketClient(client.urlStr, client.botID, client.api, client.apiv2, reconnecttimes) - if err == nil && newClient != nil { - client.mutex.Lock() // 在替换连接之前锁定 - client.cancel() // 退出旧的连接 - client.conn = newClient.conn - client.api = newClient.api - client.apiv2 = newClient.apiv2 - client.cancel = newClient.cancel // 更新取消函数 - client.mutex.Unlock() - // 重发失败的消息 - newClient.processFailedMessages(oldSendFailures) - mylog.Println("Successfully reconnected to WebSocket.") - return - } + + mylog.Printf("Successfully reconnected to WebSocket.") } // 处理发送失败的消息 -func (client *WebSocketClient) processFailedMessages(failuresChan chan map[string]interface{}) { - for failedMessage := range failuresChan { +func (client *WebSocketClient) processFailedMessages() { + for _, failedMessage := range client.sendFailures { + // 尝试重新发送消息 err := client.SendMessage(failedMessage) if err != nil { - // 处理重发失败的情况,比如重新放入 channel 或记录日志 - mylog.Println("Error re-sending message:", err) + mylog.Printf("Error resending message: %v\n", err) } } + // 清空失败消息列表 + client.sendFailures = []map[string]interface{}{} } // 处理信息,调用腾讯api -func (c *WebSocketClient) recvMessage(msg []byte) { +func (client *WebSocketClient) recvMessage(msg []byte) { var message callapi.ActionMessage //mylog.Println("Received from onebotv11 server raw:", string(msg)) err := json.Unmarshal(msg, &message) @@ -128,7 +188,7 @@ func (c *WebSocketClient) recvMessage(msg []byte) { } mylog.Println("Received from onebotv11 server:", TruncateMessage(message, 800)) // 调用callapi - callapi.CallAPIFromDict(c, c.api, c.apiv2, message) + callapi.CallAPIFromDict(client, client.api, client.apiv2, message) } // 截断信息 @@ -148,7 +208,7 @@ func TruncateMessage(message callapi.ActionMessage, maxLength int) string { } // 发送心跳包 -func (c *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heartbeatinterval int) { +func (client *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heartbeatinterval int) { for { select { case <-ctx.Done(): @@ -179,7 +239,9 @@ func (c *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heart }, "interval": 10000, // 以毫秒为单位 } - c.SendMessage(message) + client.SendMessage(message) + // 重发失败的消息 + client.processFailedMessages() } } } @@ -244,7 +306,7 @@ func NewWebSocketClient(urlStr string, botID uint64, api openapi.OpenAPI, apiv2 apiv2: apiv2, botID: botID, urlStr: urlStr, - sendFailures: make(chan map[string]interface{}, 100), + sendFailures: []map[string]interface{}{}, } // Sending initial message similar to your setupB function