diff --git a/Processor/ProcessGroupMessage.go b/Processor/ProcessGroupMessage.go index 12703daa..8bb836b4 100644 --- a/Processor/ProcessGroupMessage.go +++ b/Processor/ProcessGroupMessage.go @@ -42,7 +42,6 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error { mylog.Printf("Error storing ID: %v", err) return nil } - //映射str的messageID到int messageID64, err := idmap.StoreIDv2(data.ID) if err != nil { @@ -100,7 +99,6 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error { //储存当前群或频道号的类型 idmap.WriteConfigv2(fmt.Sprint(GroupID64), "type", "group") echo.AddMsgType(AppIDString, GroupID64, "group") - // 调试 PrintStructWithFieldNames(groupMsg) diff --git a/config/config.go b/config/config.go index 4d10ff09..6d2a9634 100644 --- a/config/config.go +++ b/config/config.go @@ -46,6 +46,7 @@ type Settings struct { DeveloperLog bool `yaml:"developer_log"` Username string `yaml:"server_user_name"` Password string `yaml:"server_user_password"` + ImageLimit int `yaml:"image_sizelimit"` } // LoadConfig 从文件中加载配置并初始化单例配置 @@ -367,3 +368,16 @@ func GetServerUserPassword() string { } return instance.Settings.Password } + +// GetImageLimit 返回 ImageLimit 的值 +func GetImageLimit() int { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get image limit value.") + return 0 // 或者返回一个默认的 ImageLimit 值 + } + + return instance.Settings.ImageLimit +} diff --git a/go.mod b/go.mod index 85327ffa..69c24bbb 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/google/uuid v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mvdan/xurls v1.1.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect ) replace github.com/tencent-connect/botgo => ./botgo diff --git a/go.sum b/go.sum index 84585040..4f06c76d 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww= github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/handlers/get_group_member_info.go b/handlers/get_group_member_info.go new file mode 100644 index 00000000..3b1ce185 --- /dev/null +++ b/handlers/get_group_member_info.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "github.com/hoshinonyaruko/gensokyo/callapi" + "github.com/hoshinonyaruko/gensokyo/mylog" + "github.com/tencent-connect/botgo/openapi" +) + +// 初始化handler,在程序启动时会被调用 +func init() { + callapi.RegisterHandler("get_group_member_info", getGroupMemberInfo) +} + +// 成员信息的结构定义 +type MemberInfo struct { + UserID int64 `json:"user_id"` + GroupID int64 `json:"group_id"` + Nickname string `json:"nickname"` + Card string `json:"card"` + Sex string `json:"sex"` + Age int32 `json:"age"` + Area string `json:"area"` + JoinTime int32 `json:"join_time"` + LastSentTime int32 `json:"last_sent_time"` + Level string `json:"level"` + Role string `json:"role"` + Unfriendly bool `json:"unfriendly"` + Title string `json:"title"` + TitleExpireTime int64 `json:"title_expire_time"` + CardChangeable bool `json:"card_changeable"` + ShutUpTimestamp int64 `json:"shut_up_timestamp"` +} + +// 构建单个成员的响应数据 +func buildResponseForSingleMember(memberInfo *MemberInfo, echoValue interface{}) map[string]interface{} { + // 构建成员数据的映射 + memberMap := map[string]interface{}{ + "group_id": memberInfo.GroupID, + "user_id": memberInfo.UserID, + "nickname": memberInfo.Nickname, + "card": memberInfo.Card, + "sex": memberInfo.Sex, + "age": memberInfo.Age, + "area": memberInfo.Area, + "join_time": memberInfo.JoinTime, + "last_sent_time": memberInfo.LastSentTime, + "level": memberInfo.Level, + "role": memberInfo.Role, + "unfriendly": memberInfo.Unfriendly, + "title": memberInfo.Title, + "title_expire_time": memberInfo.TitleExpireTime, + "card_changeable": memberInfo.CardChangeable, + "shut_up_timestamp": memberInfo.ShutUpTimestamp, + } + + // 构建完整的响应映射 + response := map[string]interface{}{ + "retcode": 0, + "status": "ok", + "data": memberMap, + "echo": echoValue, + } + + return response +} + +// getGroupMemberInfo是处理获取群成员信息的函数 +func getGroupMemberInfo(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.OpenAPI, message callapi.ActionMessage) { + // 使用虚拟数据构造 MemberInfo + memberInfo := &MemberInfo{ + UserID: 123456789, // 虚拟的 QQ 号 + GroupID: 987654321, // 虚拟的群号 + Nickname: "主人", // 虚拟昵称 + Card: "主人", + Sex: "unknown", // 性别未知 + Age: 20, // 虚拟年龄 + Area: "虚拟地区", + JoinTime: 1630416000, // 虚拟加群时间戳 + LastSentTime: 1630502400, // 虚拟最后发言时间戳 + Level: "1", // 虚拟成员等级 + Role: "member", // 角色为普通成员 + Unfriendly: false, // 没有不良记录 + Title: "虚拟头衔", + TitleExpireTime: 1630598800, // 虚拟头衔过期时间 + CardChangeable: true, // 允许修改群名片 + ShutUpTimestamp: 0, // 不在禁言中 + } + + // 构建响应JSON + responseJSON := buildResponseForSingleMember(memberInfo, message.Echo) + mylog.Printf("get_group_member_info: %s\n", responseJSON) + + // 发送响应回去 + err := client.SendMessage(responseJSON) + if err != nil { + mylog.Printf("发送消息时出错: %v", err) + } +} diff --git a/handlers/message_parser.go b/handlers/message_parser.go index 51b77112..ee685b17 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -4,6 +4,7 @@ import ( "path/filepath" "regexp" "runtime" + "strconv" "strings" "github.com/hoshinonyaruko/gensokyo/callapi" @@ -160,6 +161,9 @@ func parseMessageContent(paramsMessage callapi.ParamsContent) (string, map[strin // at处理和链接处理 func transformMessageText(messageText string) string { + // 首先,将AppID替换为BotID + messageText = strings.ReplaceAll(messageText, AppID, BotID) + // 使用正则表达式来查找所有[CQ:at,qq=数字]的模式 re := regexp.MustCompile(`\[CQ:at,qq=(\d+)\]`) messageText = re.ReplaceAllStringFunc(messageText, func(m string) string { @@ -167,8 +171,9 @@ func transformMessageText(messageText string) string { if len(submatches) > 1 { realUserID, err := idmap.RetrieveRowByIDv2(submatches[1]) if err != nil { + // 如果出错,也替换成相应的格式,但使用原始QQ号 mylog.Printf("Error retrieving user ID: %v", err) - return m // 如果出错,返回原始匹配 + return "<@!" + submatches[1] + ">" } return "<@!" + realUserID + ">" } @@ -208,11 +213,21 @@ func RevertTransformedText(data interface{}) string { // 使用正则表达式来查找所有<@!数字>的模式 re := regexp.MustCompile(`<@!(\d+)>`) - // 使用正则表达式来替换找到的模式为[CQ:at,qq=数字] + // 使用正则表达式来替换找到的模式为[CQ:at,qq=用户ID] messageText = re.ReplaceAllStringFunc(messageText, func(m string) string { submatches := re.FindStringSubmatch(m) if len(submatches) > 1 { - return "[CQ:at,qq=" + submatches[1] + "]" + //映射用户id + userID64, err := idmap.StoreIDv2(submatches[1]) + if err != nil { + //如果储存失败(数据库损坏)返回原始值 + mylog.Printf("Error storing ID: %v", err) + return "[CQ:at,qq=" + submatches[1] + "]" + } + //类型转换 + userIDStr := strconv.FormatInt(userID64, 10) + //经过转换的cq码 + return "[CQ:at,qq=" + userIDStr + "]" } return m }) diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index 3eae0096..b59cc2bb 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -158,9 +158,19 @@ func generateGroupMessage(id string, foundItems map[string][]string, messageText MsgType: 0, // 默认文本类型 } } + // 首先压缩图片 默认不压缩 + compressedData, err := images.CompressSingleImage(imageData) + if err != nil { + mylog.Printf("Error compressing image: %v", err) + return &dto.MessageToCreate{ + Content: "错误: 压缩图片失败", + MsgID: id, + MsgType: 0, // 默认文本类型 + } + } // base64编码 - base64Encoded := base64.StdEncoding.EncodeToString(imageData) + base64Encoded := base64.StdEncoding.EncodeToString(compressedData) // 上传base64编码的图片并获取其URL imageURL, err := images.UploadBase64ImageToServer(base64Encoded) @@ -204,8 +214,18 @@ func generateGroupMessage(id string, foundItems map[string][]string, messageText mylog.Printf("failed to decode base64 image: %v", err) return nil } + // 首先压缩图片 默认不压缩 + compressedData, err := images.CompressSingleImage(fileImageData) + if err != nil { + mylog.Printf("Error compressing image: %v", err) + return &dto.MessageToCreate{ + Content: "错误: 压缩图片失败", + MsgID: id, + MsgType: 0, // 默认文本类型 + } + } // 将解码的图片数据转换回base64格式并上传 - imageURL, err := images.UploadBase64ImageToServer(base64.StdEncoding.EncodeToString(fileImageData)) + imageURL, err := images.UploadBase64ImageToServer(base64.StdEncoding.EncodeToString(compressedData)) if err != nil { mylog.Printf("failed to upload base64 image: %v", err) return nil diff --git a/handlers/send_guild_channel_msg.go b/handlers/send_guild_channel_msg.go index 8c011be7..28c3cc4b 100644 --- a/handlers/send_guild_channel_msg.go +++ b/handlers/send_guild_channel_msg.go @@ -8,6 +8,7 @@ import ( "github.com/hoshinonyaruko/gensokyo/callapi" "github.com/hoshinonyaruko/gensokyo/config" "github.com/hoshinonyaruko/gensokyo/idmap" + "github.com/hoshinonyaruko/gensokyo/images" "github.com/hoshinonyaruko/gensokyo/mylog" "github.com/hoshinonyaruko/gensokyo/echo" @@ -143,9 +144,18 @@ func generateReplyMessage(id string, foundItems map[string][]string, messageText } return &reply, false } - + // 首先压缩图片 + compressedData, err := images.CompressSingleImage(imageData) + if err != nil { + mylog.Printf("Error compressing image: %v", err) + return &dto.MessageToCreate{ + Content: "错误: 压缩图片失败", + MsgID: id, + MsgType: 0, // 默认文本类型 + }, false + } //base64编码 - base64Encoded := base64.StdEncoding.EncodeToString(imageData) + base64Encoded := base64.StdEncoding.EncodeToString(compressedData) // 当作base64图来处理 reply = dto.MessageToCreate{ diff --git a/handlers/send_private_msg.go b/handlers/send_private_msg.go index 3db98c8b..2e67f50b 100644 --- a/handlers/send_private_msg.go +++ b/handlers/send_private_msg.go @@ -73,7 +73,7 @@ func handleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open // 优先发送文本信息 if messageText != "" { - groupReply := generatePrivateMessage(messageID, nil, messageText) + groupReply := generateGroupMessage(messageID, nil, messageText) // 进行类型断言 groupMessage, ok := groupReply.(*dto.MessageToCreate) @@ -96,7 +96,8 @@ func handleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open var singleItem = make(map[string][]string) singleItem[key] = urls - groupReply := generatePrivateMessage(messageID, singleItem, "") + //先试试用群里一样的处理逻辑,看看能跑不 + groupReply := generateGroupMessage(messageID, singleItem, "") // 进行类型断言 richMediaMessage, ok := groupReply.(*dto.RichMediaMessage) @@ -119,6 +120,7 @@ func handleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open } } +// 这里是只有群私聊会用到 func generatePrivateMessage(id string, foundItems map[string][]string, messageText string) interface{} { if imageURLs, ok := foundItems["local_image"]; ok && len(imageURLs) > 0 { // 本地发图逻辑 todo 适配base64图片 diff --git a/images/Compress.go b/images/Compress.go new file mode 100644 index 00000000..00ca902b --- /dev/null +++ b/images/Compress.go @@ -0,0 +1,207 @@ +package images + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/draw" + "image/gif" + "image/jpeg" + "image/png" + "io" + "sync" +) + +type Compressor struct { + QualityStep int // Quality adjustment step + MinQuality int // Minimum quality + MaxQuality int // Maximum quality + ThresholdKB int // Size threshold in KB +} + +func NewCompressor(thresholdKB, qualityStep, minQuality, maxQuality int) *Compressor { + return &Compressor{ + QualityStep: qualityStep, + MinQuality: minQuality, + MaxQuality: maxQuality, + ThresholdKB: thresholdKB, + } +} + +// CompressImage handles image compression based on format. +func (c *Compressor) CompressImage(imageData io.Reader) ([]byte, error) { + if c.ThresholdKB == 0 { + return io.ReadAll(imageData) + } + + // Create a buffer to copy the imageData and determine the image format. + buffer := bytes.NewBuffer(nil) + tee := io.TeeReader(imageData, buffer) + + // Decode image using the buffer so we don't lose the initial bytes. + img, format, err := image.Decode(tee) + if err != nil { + return nil, fmt.Errorf("decoding image failed: %w", err) + } + + // For GIFs, use the buffer which contains all bytes read from the original imageData. + if format == "gif" { + return c.handleGIF(buffer) + } + + // For non-GIFs, check the initial size using a fresh buffer. + buf := &bytes.Buffer{} + switch format { + case "jpeg": + err = jpeg.Encode(buf, img, nil) + case "png": + err = png.Encode(buf, img) + default: + return nil, fmt.Errorf("unsupported image format: %s", format) + } + + if err != nil { + return nil, fmt.Errorf("encoding image failed: %w", err) + } + + if buf.Len() <= c.ThresholdKB*1024 { + // If the image is already below the threshold, return the original encoded bytes. + return buf.Bytes(), nil + } + + // Apply format-specific compression. + switch format { + case "jpeg": + return c.compressJPEG(img) + case "png": + return c.compressPNG(img) + } + + return nil, fmt.Errorf("unsupported image format: %s", format) +} + +// handleGIF decodes and processes a GIF image. +func (c *Compressor) handleGIF(imageData io.Reader) ([]byte, error) { + gifImg, err := gif.DecodeAll(imageData) + if err != nil { + return nil, fmt.Errorf("decoding GIF image failed: %w", err) + } + return c.compressGIF(gifImg) +} + +func (c *Compressor) compressJPEG(img image.Image) ([]byte, error) { + quality := c.MaxQuality + buf := &bytes.Buffer{} + + for { + opts := jpeg.Options{Quality: quality} + buf.Reset() + err := jpeg.Encode(buf, img, &opts) + if err != nil { + return nil, fmt.Errorf("JPEG encoding failed at quality %d: %w", quality, err) + } + + if buf.Len() <= c.ThresholdKB*1024 || quality <= c.MinQuality { + break + } + + quality -= c.QualityStep + if quality < c.MinQuality { + quality = c.MinQuality + } + } + + return buf.Bytes(), nil +} + +func (c *Compressor) compressPNG(img image.Image) ([]byte, error) { + // Convert PNG to JPEG with a white background + b := img.Bounds() + whiteBg := image.NewRGBA(b) + draw.Draw(whiteBg, b, image.NewUniform(color.White), image.Point{}, draw.Src) + draw.Draw(whiteBg, b, img, b.Min, draw.Over) + return c.compressJPEG(whiteBg) +} + +func (c *Compressor) compressGIF(originalGIF *gif.GIF) ([]byte, error) { + // Create a new GIF to hold the compressed frames + var compressedGIF gif.GIF + compressedGIF.LoopCount = originalGIF.LoopCount + compressedGIF.Disposal = originalGIF.Disposal + compressedGIF.Config = originalGIF.Config + compressedGIF.BackgroundIndex = originalGIF.BackgroundIndex + + for i, srcFrame := range originalGIF.Image { + // Convert frame to RGBA to avoid paletted color issues + b := srcFrame.Bounds() + frame := image.NewRGBA(b) + draw.Draw(frame, b, srcFrame, b.Min, draw.Over) + + // Create a white image same size of the frame + whiteImage := image.NewRGBA(b) + draw.Draw(whiteImage, b, &image.Uniform{color.White}, image.ZP, draw.Src) + + // Draw the frame onto the white image to remove transparency + draw.Draw(whiteImage, b, frame, b.Min, draw.Over) + + // Compress the frame + compressedFrame, err := c.compressJPEG(whiteImage) + if err != nil { + return nil, fmt.Errorf("compressing GIF frame failed: %w", err) + } + + // Decode the compressed frame back to image + jpgFrame, _, err := image.Decode(bytes.NewReader(compressedFrame)) + if err != nil { + return nil, fmt.Errorf("decoding JPEG frame failed: %w", err) + } + + // Convert back to paletted image for GIF + palettedFrame := image.NewPaletted(b, srcFrame.Palette) + draw.FloydSteinberg.Draw(palettedFrame, b, jpgFrame, b.Min) + + compressedGIF.Image = append(compressedGIF.Image, palettedFrame) + compressedGIF.Delay = append(compressedGIF.Delay, originalGIF.Delay[i]) + } + + var buf bytes.Buffer + if err := gif.EncodeAll(&buf, &compressedGIF); err != nil { + return nil, fmt.Errorf("encoding compressed GIF failed: %w", err) + } + + return buf.Bytes(), nil +} + +func ProcessImages(imageData []io.Reader, compressor *Compressor) ([][]byte, error) { + var wg sync.WaitGroup + mu := &sync.Mutex{} + compressedImages := make([][]byte, len(imageData)) + errChan := make(chan error, len(imageData)) // 错误通道,缓冲以避免阻塞 + + wg.Add(len(imageData)) + for i, data := range imageData { + go func(idx int, imgData io.Reader) { + defer wg.Done() + compressed, err := compressor.CompressImage(imgData) + mu.Lock() + compressedImages[idx] = compressed + mu.Unlock() + if err != nil { + errChan <- fmt.Errorf("compressing image at index %d failed: %w", idx, err) + } + }(i, data) + } + + wg.Wait() + close(errChan) // 处理完所有goroutine后关闭错误通道 + + // 检查错误通道中是否有错误 + for err := range errChan { + if err != nil { + return nil, err // 可以返回第一个错误或累积所有错误 + } + } + + return compressedImages, nil +} diff --git a/images/easycompress.go b/images/easycompress.go new file mode 100644 index 00000000..c428f4eb --- /dev/null +++ b/images/easycompress.go @@ -0,0 +1,39 @@ +package images + +import ( + "bytes" + + "github.com/hoshinonyaruko/gensokyo/config" +) + +// 默认压缩参数 +const ( + defaultQualityStep = 10 + defaultMinQuality = 25 + defaultMaxQuality = 75 +) + +// CompressSingleImage 接收一个图片的 []byte 数据,并根据设定阈值返回压缩后的数据或原始数据。 +func CompressSingleImage(imageBytes []byte) ([]byte, error) { + // 获取压缩阈值 + thresholdKB := config.GetImageLimit() + + // 如果阈值为0,则直接返回原始图片数据,不进行压缩 + if thresholdKB == 0 { + return imageBytes, nil + } + + // 创建压缩器实例 + compressor := NewCompressor(thresholdKB, defaultQualityStep, defaultMinQuality, defaultMaxQuality) + + // 创建一个读取器来读取 imageBytes 数据 + reader := bytes.NewReader(imageBytes) + + // 调用 CompressImage 方法来压缩图片 + compressedImage, err := compressor.CompressImage(reader) + if err != nil { + return nil, err // 压缩出错时返回错误 + } + + return compressedImage, nil // 返回压缩后的图片数据 +} diff --git a/main.go b/main.go index db0332ae..ae0b22cf 100644 --- a/main.go +++ b/main.go @@ -186,7 +186,7 @@ func main() { // 确保所有wsClients都已初始化 if len(wsClients) != len(conf.Settings.WsAddress) { log.Println("Error: Not all wsClients are initialized!") - log.Fatalln("Failed to initialize all WebSocketClients.") + //log.Fatalln("Failed to initialize all WebSocketClients.") } else { log.Println("All wsClients are successfully initialized.") p = Processor.NewProcessor(api, apiV2, &conf.Settings, wsClients) diff --git a/mylog/mylog.go b/mylog/mylog.go index c798e3c1..d42149ef 100644 --- a/mylog/mylog.go +++ b/mylog/mylog.go @@ -27,7 +27,7 @@ type EnhancedLogEntry struct { } // 我们的日志频道,所有的 WebSocket 客户端都会在此监听日志事件 -var logChannel = make(chan EnhancedLogEntry, 100) +var logChannel = make(chan EnhancedLogEntry, 1000) func Println(v ...interface{}) { log.Println(v...) @@ -47,7 +47,14 @@ func emitLog(level, message string) { Level: level, Message: message, } - logChannel <- entry + // 非阻塞发送,如果通道满了就尝试备份日志。 + select { + case logChannel <- entry: + // 日志成功发送到通道。 + default: + // 通道满了,备份日志到文件或数据库。 + //backupLog(entry) + } } // 返回日志通道,以便我们的 WebSocket 服务端可以监听和广播日志事件 @@ -75,7 +82,7 @@ func WsHandlerWithDependencies(c *gin.Context) { lock.Unlock() // 输出新的 WebSocket 客户端连接信息 - fmt.Println("新的WebSocket客户端已连接!") + fmt.Println("新的webui用户已连接!") // Start a goroutine for heartbeats go func() { @@ -86,66 +93,91 @@ func WsHandlerWithDependencies(c *gin.Context) { }() go client.writePump() + go client.readPump() for logEntry := range LogChannel() { lock.RLock() - if len(wsClients) == 0 { - lock.RUnlock() - continue - } - + // 去掉对wsClients长度的检查,已经在emitLog里面做了防阻塞处理 for client := range wsClients { - client.send <- logEntry + select { + case client.send <- logEntry: + // 成功发送日志到客户端 + default: + // 客户端的send通道满了,可以选择断开客户端连接或者其他处理 + } } lock.RUnlock() } } -func (c *Client) writePump() { +func (c *Client) readPump() { defer func() { lock.Lock() - delete(wsClients, c) + delete(wsClients, c) // 从客户端集合中移除当前客户端 lock.Unlock() - c.conn.Close() + c.conn.Close() // 关闭WebSocket连接 }() - ticker := time.NewTicker(30 * time.Second) // 每30秒发送一次心跳 - defer ticker.Stop() + // 设置读取超时时间 + c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)); return nil }) + + for { + _, message, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + fmt.Printf("websocket closed unexpectedly: %v", err) + } else { + fmt.Println("读取websocket出错:", err) + } + break + } - lastActiveTime := time.Now() // 上次活跃的时间 + // 检查收到的消息是否为心跳 + if string(message) == "heartbeat" { + //fmt.Println("收到心跳,客户端活跃") + c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + // 更新客户端的活跃时间,或执行其它心跳相关逻辑 + } + } +} + +func (c *Client) writePump() { + defer func() { + lock.Lock() + delete(wsClients, c) // 从客户端集合中移除当前客户端 + lock.Unlock() + c.conn.Close() // 关闭WebSocket连接 + }() + + // 设置心跳发送间隔 + heartbeatTicker := time.NewTicker(10 * time.Second) + defer heartbeatTicker.Stop() for { select { case message, ok := <-c.send: if !ok { - // 如果send通道已经关闭,那么直接返回 + // 如果send通道已经关闭,那么直接退出 return } + // 更新写入超时时间 + c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) err := c.conn.WriteJSON(message) if err != nil { + // 如果写入websocket出错,输出错误并退出 fmt.Println("发送到websocket出错:", err) return - } else { - // 输出消息发送成功的信息 - fmt.Printf("消息成功发送给客户端: %s\n", message) } - - case <-ticker.C: // 定时器触发 - if time.Since(lastActiveTime) > 1*time.Minute { - // 如果超过1分钟没有收到Pong消息,则关闭连接 - return - } - c.conn.WriteMessage(websocket.PingMessage, nil) - - default: - messageType, _, err := c.conn.ReadMessage() - if err != nil { + case <-heartbeatTicker.C: + // 发送心跳消息 + c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + // 如果写入心跳失败,输出错误并退出 + fmt.Println("发送心跳失败:", err) return } - if messageType == websocket.PongMessage { - // 更新上次活跃的时间 - lastActiveTime = time.Now() - } + //fmt.Println("发送心跳,维持连接活跃") } } } diff --git a/template/config_template.go b/template/config_template.go index 3c2e1f10..3b5f63ab 100644 --- a/template/config_template.go +++ b/template/config_template.go @@ -47,6 +47,7 @@ settings: developer_log : false #开启开发者日志 默认关闭 server_user_name : "useradmin" #默认网页面板用户名 server_user_password : "admin" #默认网页面板密码 + image_sizelimit : 0 #代表kb 腾讯api要求图片1500ms完成传输 如果图片发不出 请提升上行或设置此值 默认为0 不压缩 ` const Logo = ` ' diff --git a/template/config_template.yml b/template/config_template.yml index 0b3055e7..6c8d0d52 100644 --- a/template/config_template.yml +++ b/template/config_template.yml @@ -38,4 +38,5 @@ settings: key: "" #密钥路径 Apache(crt文件、key文件)示例: "C:\\123.key" \需要双写成\\ developer_log : true #开启开发者日志 server_user_name : "useradmin" #默认网页面板用户名 - server_user_password : "admin" #默认网页面板密码 \ No newline at end of file + server_user_password : "admin" #默认网页面板密码 + image_sizelimit : 0 #代表kb 腾讯api要求图片1500ms完成传输 如果图片发不出 请提升上行或设置此值 默认为0 不压缩 \ No newline at end of file diff --git a/wsclient/ws.go b/wsclient/ws.go index a2c292fd..0622ec50 100644 --- a/wsclient/ws.go +++ b/wsclient/ws.go @@ -134,18 +134,36 @@ func (c *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64) { return case <-time.After(10 * time.Second): message := map[string]interface{}{ - "meta_event_type": "heartbeat", "post_type": "meta_event", - "self_id": botID, - "status": "ok", + "meta_event_type": "heartbeat", "time": int(time.Now().Unix()), + "self_id": botID, + "status": map[string]interface{}{ + "app_enabled": true, + "app_good": true, + "app_initialized": true, + "good": true, + "online": true, + "plugins_good": nil, + "stat": map[string]int{ + "packet_received": 34933, + "packet_sent": 8513, + "packet_lost": 0, + "message_received": 24674, + "message_sent": 1663, + "disconnect_times": 0, + "lost_times": 0, + "last_message_time": int(time.Now().Unix()) - 10, // 假设最后一条消息是10秒前收到的 + }, + }, + "interval": 10000, // 以毫秒为单位 } c.SendMessage(message) } } } -const maxRetryAttempts = 5 +const maxRetryAttempts = 30 // NewWebSocketClient 创建 WebSocketClient 实例,接受 WebSocket URL、botID 和 openapi.OpenAPI 实例 func NewWebSocketClient(urlStr string, botID uint64, api openapi.OpenAPI, apiv2 openapi.OpenAPI) (*WebSocketClient, error) {