package mcstatus import ( "context" "fmt" "strings" "time" "cellbot/internal/database" "cellbot/internal/engine" "cellbot/internal/protocol" "cellbot/pkg/utils" "go.uber.org/zap" ) // ServerBind 服务器绑定模型 type ServerBind struct { ID string `gorm:"primaryKey"` // group_id ServerIP string `gorm:"not null"` // 服务器IP } // TableName 指定表名(基础表名,实际使用时需要添加 botID 前缀) func (ServerBind) TableName() string { return "mc_server_binds" } var dbService database.Database func init() { // 注册命令(数据库将在依赖注入时初始化) engine.OnCommand("/mcs"). Priority(100). Handle(handleMCSCommand) engine.OnCommand("/mcsBind"). Priority(100). Handle(handleMCSBindCommand) } // InitDatabase 初始化数据库(由依赖注入调用) func InitDatabase(database database.Database) error { dbService = database return nil } // handleMCSCommand 处理 /mcs 命令 func handleMCSCommand(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error { logger.Info("handleMCSCommand called", zap.String("self_id", event.GetSelfID()), zap.String("detail_type", event.GetDetailType())) data := event.GetData() // 获取原始消息内容 rawMessage, ok := data["raw_message"].(string) if !ok { logger.Warn("raw_message not found in event data") return nil } logger.Info("Processing /mcs command", zap.String("raw_message", rawMessage)) // 解析命令参数(/mcs 后面的内容) var serverIP string parts := strings.Fields(rawMessage) if len(parts) > 1 { serverIP = strings.Join(parts[1:], " ") } // 如果没有提供 IP,尝试从群绑定或默认配置获取 if serverIP == "" { groupID, _ := data["group_id"] botID := event.GetSelfID() if groupID != nil && dbService != nil && botID != "" { db, err := dbService.GetDB(botID) if err != nil { logger.Warn("Failed to get database for bot", zap.String("bot_id", botID), zap.Error(err)) } else { var bind ServerBind tableName := database.GetTableName(botID, "mc_server_binds") if err := db.Table(tableName).Where("id = ?", fmt.Sprintf("%v", groupID)).First(&bind).Error; err != nil { logger.Debug("No server bind found for group", zap.String("bot_id", botID), zap.Any("group_id", groupID), zap.Error(err)) } else { serverIP = bind.ServerIP } } } // 如果还是没有,使用默认值 if serverIP == "" { serverIP = "mc.hypixel.net" // 默认服务器 } } // 解析服务器地址和端口 host := serverIP port := 25565 if strings.Contains(serverIP, ":") { parts := strings.Split(serverIP, ":") host = parts[0] fmt.Sscanf(parts[1], "%d", &port) } logger.Info("Querying Minecraft server", zap.String("host", host), zap.Int("port", port)) // 查询服务器状态 status, err := Ping(host, port, 10*time.Second) if err != nil { logger.Error("Failed to ping server", zap.Error(err)) // 使用 ReplyText 发送错误消息 errorMsg := fmt.Sprintf("查询失败: %v", err) event.ReplyText(ctx, botManager, logger, errorMsg) return err } // 构建 HTML 模板 htmlTemplate := buildStatusHTML(status) // 配置截图选项 opts := &utils.ScreenshotOptions{ Width: 1200, Height: 800, Timeout: 60 * time.Second, WaitTime: 3 * time.Second, FullPage: false, Quality: 90, Logger: logger, } // 使用独立的 context 进行截图,避免受 dispatcher context 影响 // 如果 dispatcher context 被取消,截图操作仍能完成 screenshotCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() // 渲染并截图 chain, err := utils.ScreenshotHTMLToMessageChain(screenshotCtx, htmlTemplate, opts) if err != nil { logger.Error("Failed to render status image", zap.Error(err)) return err } // 使用 Reply 发送图片 err = event.Reply(ctx, botManager, logger, chain) if err != nil { logger.Error("Failed to send status image", zap.Error(err)) return err } logger.Info("Minecraft server status sent", zap.String("host", host), zap.Int("port", port)) return nil } // handleMCSBindCommand 处理 /mcsBind 命令 func handleMCSBindCommand(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error { data := event.GetData() // 只允许群聊使用 if event.GetDetailType() != "group" { return event.ReplyText(ctx, botManager, logger, "此命令只能在群聊中使用") } // 获取原始消息内容 rawMessage, ok := data["raw_message"].(string) if !ok { return nil } // 解析命令参数(/mcsBind 后面的内容) parts := strings.Fields(rawMessage) if len(parts) < 2 { groupID, _ := data["group_id"] errorMsg := protocol.NewMessageChain( protocol.NewTextSegment("用法: /mcsBind <服务器IP>"), ) action := &protocol.BaseAction{ Type: protocol.ActionTypeSendGroupMessage, Params: map[string]interface{}{ "group_id": groupID, "message": errorMsg, }, } selfID := event.GetSelfID() bot, ok := botManager.Get(selfID) if !ok { bots := botManager.GetAll() if len(bots) > 0 { bot = bots[0] } } if bot != nil { bot.SendAction(ctx, action) } return nil } serverIP := strings.Join(parts[1:], " ") groupID, _ := data["group_id"] groupIDStr := fmt.Sprintf("%v", groupID) botID := event.GetSelfID() // 保存绑定到数据库 if dbService != nil && botID != "" { db, err := dbService.GetDB(botID) if err == nil { // 自动迁移表(如果不存在) tableName := database.GetTableName(botID, "mc_server_binds") if err := db.Table(tableName).AutoMigrate(&ServerBind{}); err != nil { logger.Error("Failed to migrate table", zap.Error(err)) } else { bind := ServerBind{ ID: groupIDStr, ServerIP: serverIP, } // 使用 Save 方法,如果存在则更新,不存在则创建 if err := db.Table(tableName).Save(&bind).Error; err != nil { logger.Error("Failed to save server bind", zap.Error(err)) } } } } // 使用 ReplyText 发送确认消息 successMsg := fmt.Sprintf("已绑定服务器 %s 到本群", serverIP) err := event.ReplyText(ctx, botManager, logger, successMsg) if err != nil { return err } logger.Info("Minecraft server bound", zap.String("group_id", groupIDStr), zap.String("server_ip", serverIP)) return nil } // buildStatusHTML 构建服务器状态 HTML func buildStatusHTML(status *ServerStatus) string { iconHTML := "" if status.Favicon != "" { iconHTML = fmt.Sprintf(`icon`, status.Favicon) } return fmt.Sprintf(`
%s
Minecraft 服务器状态
服务器地址: %s
延迟: %d ms
版本: %s (协议 %d)
在线人数: %d / %d
服务器描述:
%s
`, iconHTML, fmt.Sprintf("%s:%d", status.Host, status.Port), status.Latency, status.Version.Name, status.Version.Protocol, status.Players.Online, status.Players.Max, status.Description) }