From 86ea3fc544ffa303e63b25a4cbed8ce85f3d7d88 Mon Sep 17 00:00:00 2001 From: Sheyiyuan <2125107118@qq.com> Date: Sat, 7 Dec 2024 17:16:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96api=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- .idea/inspectionProfiles/Project_Default.xml | 6 + LOG/log.go | 4 +- core/api.go | 295 ++++++++++++------- core/appAdmin.go | 58 ---- core/app_admin.go | 81 +++++ core/app_core.go | 55 ++++ core/cmdList.go | 31 -- core/events_handler.go | 25 +- core/web_socket.go | 20 +- main.go | 2 +- readme.md | 2 +- typed/typed.go | 157 +--------- utils.go | 22 +- wba/wind.go | 289 ++++++++++++++++++ 15 files changed, 675 insertions(+), 375 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 core/appAdmin.go create mode 100644 core/app_admin.go create mode 100644 core/app_core.go delete mode 100644 core/cmdList.go create mode 100644 wba/wind.go diff --git a/.gitignore b/.gitignore index 491bbc5..b31d724 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /scripts /ProjectWIND /data -**/.DS_Store \ No newline at end of file +**/.DS_Store +/app_demo/ \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5c6858b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/LOG/log.go b/LOG/log.go index 9cbf822..f38f4b6 100644 --- a/LOG/log.go +++ b/LOG/log.go @@ -12,12 +12,12 @@ func DEBUG(text string, msg ...interface{}) { func INFO(text string, msg ...interface{}) { msgText := fmt.Sprintf(text, msg...) - log.Println("[INFO] ", msgText) + log.Println("[INFO] ", msgText) } func WARN(text string, msg ...interface{}) { msgText := fmt.Sprintf(text, msg...) - log.Println("[WARN] ", msgText) + log.Println("[WARN] ", msgText) } func ERROR(text string, msg ...interface{}) { diff --git a/core/api.go b/core/api.go index 193cee8..c072f24 100644 --- a/core/api.go +++ b/core/api.go @@ -1,26 +1,31 @@ package core import ( - "ProjectWIND/typed" + "ProjectWIND/LOG" + "ProjectWIND/wba" + "crypto/rand" "encoding/json" - "errors" + "fmt" ) +type apiInfo struct{} + /* 关于API的说明: 1.所有API请求按照OneBot11标准,使用JSON格式进行数据交换。api命名为由原文档中蛇形命名法改为双驼峰命名法。 -2.无响应的API请求使用ws协议处理,有响应的API请求使用http协议处理。 +2.无响应的API请求使用ws协议处理,有响应的API需添加echo字段。 3.wind会从配置文件中读取API请求的url,请确保正确填写。 */ //1.无响应API,使用ws协议处理 -func SendMsg(msg typed.MessageEventInfo, message string, autoEscape bool) error { +// SendMsg 发送消息(自动判断消息类型) +func (a *apiInfo) SendMsg(msg wba.MessageEventInfo, message string, autoEscape bool) { // 构建发送消息的JSON数据 - var messageData typed.APIRequestInfo + var messageData wba.APIRequestInfo messageType := msg.MessageType @@ -38,202 +43,260 @@ func SendMsg(msg typed.MessageEventInfo, message string, autoEscape bool) error } default: { - return errors.New("invalid type") + LOG.ERROR("发送消息(SendMsg)时,消息类型错误: %v", messageType) } } messageData.Params.Message = message messageData.Params.AutoEscape = autoEscape messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("发送消息时,构建JSON数据失败: %v", err) + return } // 发送消息 err = wsAPI(messageJson) - return err + if err != nil { + LOG.ERROR("发送消息时,发送失败: %v", err) + return + } + LOG.INFO("发送消息(SendMsg)(至:%v-%v:%v-%v):%v", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname, message) + return } -func SendPrivateMsg(msg typed.MessageEventInfo, message string, autoEscape bool) error { +// SendPrivateMsg 发送私聊消息 +func (a *apiInfo) SendPrivateMsg(msg wba.MessageEventInfo, message string, autoEscape bool) { // 构建发送消息的JSON数据 - var messageData typed.APIRequestInfo + var messageData wba.APIRequestInfo messageData.Action = "send_private_msg" messageData.Params.UserId = msg.UserId messageData.Params.Message = message messageData.Params.AutoEscape = autoEscape messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("发送消息(SendPrivateMsg)时,构建JSON数据失败: %v", err) + return } // 发送消息 err = wsAPI(messageJson) - return err + if err != nil { + LOG.ERROR("发送消息(SendPrivateMsg)时,发送失败: %v", err) + return + } + LOG.INFO("发送消息(SendPrivateMsg)(至:%v-%v:%v-%v):%v", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname, message) + return } -func SendGroupMsg(msg typed.MessageEventInfo, message string, autoEscape bool) error { +// SendGroupMsg 发送群消息 +func (a *apiInfo) SendGroupMsg(msg wba.MessageEventInfo, message string, autoEscape bool) { // 构建发送消息的JSON数据 - var messageData typed.APIRequestInfo + var messageData wba.APIRequestInfo messageData.Action = "send_group_msg" messageData.Params.GroupId = msg.GroupId messageData.Params.Message = message messageData.Params.AutoEscape = autoEscape messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("发送消息(SendGroupMsg)时,构建JSON数据失败: %v", err) + return } // 发送消息 err = wsAPI(messageJson) - return err + if err != nil { + LOG.ERROR("发送消息(SendGroupMsg)时,发送失败: %v", err) + return + } + LOG.INFO("发送消息(SendGroupMsg)(至:%v-%v:%v-%v):%v", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname, message) + return } -func DeleteMsg(msg typed.MessageEventInfo, msgId int64) error { +// DeleteMsg 撤回消息 +func (a *apiInfo) DeleteMsg(msg wba.MessageEventInfo) { // 构建删除消息的JSON数据 - var messageData typed.APIRequestInfo + var messageData wba.APIRequestInfo messageData.Action = "delete_msg" messageData.Params.MessageId = msg.MessageId messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("撤回消息(DeleteMsg)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return err + if err != nil { + LOG.ERROR("撤回消息(DeleteMsg)时,发送失败: %v", err) + return + } + LOG.INFO("撤回消息(DeleteMsg):[id:%v]%v", msg.MessageId, msg.RawMessage) + return } -func sendLike(userId int64, times int) error { +// SendLike 发送赞 +func (a *apiInfo) SendLike(userId int64, times int) { // 构建发送赞的JSON数据 - var messageData typed.APIRequestInfo + var messageData wba.APIRequestInfo messageData.Action = "send_like" messageData.Params.UserId = userId messageData.Params.Times = times messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("发送赞(SendLike)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("发送赞(SendLike)时,发送失败: %v", err) + return + } + LOG.INFO("发送赞(SendLike)(至:%v):%v", userId, times) + return } -func setGroupKick(groupId int64, userId int64, rejectAddRequest bool) error { - var messageData typed.APIRequestInfo +// SetGroupKick 将指定用户移出群聊(需要群主或管理员权限) +func (a *apiInfo) SetGroupKick(groupId int64, userId int64, rejectAddRequest bool) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_kick" messageData.Params.GroupId = groupId messageData.Params.UserId = userId messageData.Params.RejectAddRequest = rejectAddRequest messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("移出群聊(SetGroupKick)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("移出群聊(SetGroupKick)时,发送失败: %v", err) + return + } + LOG.INFO("移出群聊(SetGroupKick)(从:%v-%v):%v", groupId, userId, rejectAddRequest) + return } -func setGroupBan(groupId int64, userId int64, duration int32) error { - var messageData typed.APIRequestInfo +// SetGroupBan 将指定用户禁言(需要群主或管理员权限) +func (a *apiInfo) SetGroupBan(groupId int64, userId int64, duration int32) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_ban" messageData.Params.GroupId = groupId messageData.Params.UserId = userId messageData.Params.Duration = duration messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("禁言群成员(SetGroupBan)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil -} -func setGroupAnonymousBan(groupId int64, flag string, duration int32) error { - var messageData typed.APIRequestInfo - messageData.Action = "set_group_anonymous_ban" - messageData.Params.GroupId = groupId - messageData.Params.Flag = flag - messageData.Params.Duration = duration - messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("禁言群成员(SetGroupBan)时,执行失败: %v", err) + return } - err = wsAPI(messageJson) - return nil + LOG.INFO("禁言群成员(SetGroupBan)(在:%v-%v):%v", groupId, userId, duration) + return } -func setGroupWholeBan(groupId int64, enable bool) error { - var messageData typed.APIRequestInfo +// SetGroupWholeBan 设置全员禁言(需要群主或管理员权限) +func (a *apiInfo) SetGroupWholeBan(groupId int64, enable bool) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_whole_ban" messageData.Params.GroupId = groupId messageData.Params.Enable = enable messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置全员禁言(SetGroupWholeBan)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("设置全员禁言(SetGroupWholeBan)时,执行失败: %v", err) + return + } + LOG.INFO("设置全员禁言(SetGroupWholeBan)(在:%v):%v", groupId, enable) + return } -func setGroupAdmin(groupId int64, userId int64, enable bool) error { - var messageData typed.APIRequestInfo +// SetGroupAdmin 设置群管理员(需要群主权限) +func (a *apiInfo) SetGroupAdmin(groupId int64, userId int64, enable bool) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_admin" messageData.Params.GroupId = groupId messageData.Params.UserId = userId messageData.Params.Enable = enable messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置群管理员(SetGroupAdmin)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil -} - -func setGroupAnonymous(groupId int64, enable bool) error { - var messageData typed.APIRequestInfo - messageData.Action = "set_group_anonymous" - messageData.Params.GroupId = groupId - messageData.Params.Enable = enable - messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置群管理员(SetGroupAdmin)时,执行失败: %v", err) + return } - err = wsAPI(messageJson) - return nil + LOG.INFO("设置群管理员(SetGroupAdmin)(在:%v-%v):%v", groupId, userId, enable) + return } -func setGroupCard(groupId int64, userId int64, card string) error { - var messageData typed.APIRequestInfo +// SetGroupCard 设置群名片(需要群主或管理员权限) +func (a *apiInfo) SetGroupCard(groupId int64, userId int64, card string) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_card" messageData.Params.GroupId = groupId messageData.Params.UserId = userId messageData.Params.Card = card messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置群名片(SetGroupCard)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("设置群名片(SetGroupCard)时,执行失败: %v", err) + return + } + LOG.INFO("设置群名片(SetGroupCard)(在:%v-%v):%v", groupId, userId, card) + return } -func setGroupName(groupId int64, groupName string) error { - var messageData typed.APIRequestInfo +// SetGroupName 设置群名称(可能需要群主或管理员权限) +func (a *apiInfo) SetGroupName(groupId int64, groupName string) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_name" messageData.Params.GroupId = groupId messageData.Params.GroupName = groupName messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置群名称(SetGroupName)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("设置群名称(SetGroupName)时,执行失败: %v", err) + return + } + LOG.INFO("设置群名称(SetGroupName)(在:%v):%v", groupId, groupName) + return } -func setGroupLeave(groupId int64, isDismiss bool) error { - var messageData typed.APIRequestInfo +// SetGroupLeave 退出群聊 +func (a *apiInfo) SetGroupLeave(groupId int64, isDismiss bool) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_leave" messageData.Params.GroupId = groupId messageData.Params.IsDismiss = isDismiss messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("退出群聊(SetGroupLeave)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("退出群聊(SetGroupLeave)时,执行失败: %v", err) + return + } + LOG.INFO("退出群聊(SetGroupLeave)(在:%v):%v", groupId, isDismiss) + return } -func setGroupSpecialTitle(groupId int64, userId int64, specialTitle string, duration int32) error { - var messageData typed.APIRequestInfo +// SetGroupSpecialTitle 设置群专属头衔(需要群主权限) +func (a *apiInfo) SetGroupSpecialTitle(groupId int64, userId int64, specialTitle string, duration int32) { + var messageData wba.APIRequestInfo messageData.Action = "set_group_special_title" messageData.Params.GroupId = groupId messageData.Params.UserId = userId @@ -241,46 +304,76 @@ func setGroupSpecialTitle(groupId int64, userId int64, specialTitle string, dura messageData.Params.Duration = duration messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("设置群特殊头衔(SetGroupSpecialTitle)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("设置群特殊头衔(SetGroupSpecialTitle)时,执行失败: %v", err) + return + } + LOG.INFO("设置群特殊头衔(SetGroupSpecialTitle)(在:%v-%v):%v-%v", groupId, userId, specialTitle, duration) + return } -func setFriendAddRequest(flag string, approve bool, remark string) error { - var messageData typed.APIRequestInfo +// SetFriendAddRequest 处理加好友请求 +func (a *apiInfo) SetFriendAddRequest(flag string, approve bool, remark string) { + var messageData wba.APIRequestInfo messageData.Action = "set_friend_add_request" messageData.Params.Flag = flag messageData.Params.Approve = approve messageData.Params.Remark = remark messageJson, err := json.Marshal(messageData) if err != nil { - return err + LOG.ERROR("处理加好友请求(SetFriendAddRequest)时,构建JSON数据失败: %v", err) + return } err = wsAPI(messageJson) - return nil + if err != nil { + LOG.ERROR("处理加好友请求(SetFriendAddRequest)时,执行失败: %v", err) + return + } + LOG.INFO("处理加好友请求(SetFriendAddRequest)(在:%v):%v-%v-%v", flag, approve, remark) + return +} + +// SetGroupAddRequest 处理加群请求/邀请 +func (a *apiInfo) SetGroupAddRequest(flag string, subType string, approve bool, reason string) { + var messageData wba.APIRequestInfo + messageData.Action = "set_group_add_request" + messageData.Params.Flag = flag + messageData.Params.SubType = subType + messageData.Params.Approve = approve + messageData.Params.Reason = reason + messageJson, err := json.Marshal(messageData) + if err != nil { + LOG.ERROR("处理加群请求/邀请(SetGroupAddRequest)时,构建JSON数据失败: %v", err) + return + } + err = wsAPI(messageJson) + if err != nil { + LOG.ERROR("处理加群请求/邀请(SetGroupAddRequest)时,执行失败: %v", err) + return + } + LOG.INFO("处理加群请求/邀请(SetGroupAddRequest)(在:%v-%v-%v):%v", flag, subType, approve, reason) + return } // 2.有响应API,使用http协议处理 -func GetMsg(messageId int32) (typed.MessageEventInfo, error) { - // 构建获取消息的JSON数据 - var requestData typed.ParamsInfo - var msg typed.MessageEventInfo - action := "get_msg" - requestData.MessageId = messageId - body, err := json.Marshal(requestData) + +var AppApi apiInfo + +func GenerateUUID() (string, error) { + uuid := make([]byte, 16) + _, err := rand.Read(uuid) if err != nil { - return typed.MessageEventInfo{}, err + return "", err } - // 发送请求 - _, response, err := httpAPI("POST", action, body) - if err != nil { - return typed.MessageEventInfo{}, err - } - // 解析响应 - err = json.Unmarshal(response, &msg) - if err != nil { - return typed.MessageEventInfo{}, err - } - return msg, nil + + // 设置UUID版本号(版本4),将第6字节的高4位设置为0100 + uuid[6] = (uuid[6] & 0x0F) | 0x40 + // 设置UUID变体(RFC 4122规范定义的变体),将第8字节的高4位设置为10 + uuid[8] = (uuid[8] & 0x3F) | 0x80 + + return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil } diff --git a/core/appAdmin.go b/core/appAdmin.go deleted file mode 100644 index 3005ac1..0000000 --- a/core/appAdmin.go +++ /dev/null @@ -1,58 +0,0 @@ -package core - -import ( - "ProjectWIND/LOG" - "os" - "path/filepath" - "plugin" -) - -func reloadApps() { - appsDir := "./data/app/" - appFiles, err := os.ReadDir(appsDir) - total := 0 - success := 0 - if err != nil { - LOG.ERROR("Error reading apps directory:%v", err) - return - } - - for _, file := range appFiles { - totalDelta, successDelta := reloadAPP(file, appsDir) - total += totalDelta - success += successDelta - } -} - -func reloadAPP(file os.DirEntry, appsDir string) (totalDelta int, successDelta int) { - if file.IsDir() { - return 0, 0 - } - - ext := filepath.Ext(file.Name()) - if ext == ".so" || (ext == ".dll" && os.PathSeparator == '\\') { - pluginPath := filepath.Join(appsDir, file.Name()) - p, err := plugin.Open(pluginPath) - if err != nil { - LOG.ERROR("Error opening app %s: %v\n", pluginPath, err) - return 1, 0 - } - - initSymbol, err := p.Lookup("init") - if err != nil { - LOG.ERROR("Error finding init function in app %s: %v\n", pluginPath, err) - return 1, 0 - } - - initFunc, ok := initSymbol.(func()) - if !ok { - LOG.ERROR("init symbol in app %s is not a function\n", pluginPath) - return 1, 0 - } - - initFunc() - LOG.INFO("App %s initialized successfully\n", pluginPath) - return 1, 1 - } - return 0, 0 -} diff --git a/core/app_admin.go b/core/app_admin.go new file mode 100644 index 0000000..ecd0073 --- /dev/null +++ b/core/app_admin.go @@ -0,0 +1,81 @@ +package core + +import ( + "ProjectWIND/LOG" + "ProjectWIND/wba" + "os" + "path/filepath" + "plugin" +) + +var CmdMap = make(map[string]wba.Cmd) + +func ReloadApps() (total int, success int) { + appsDir := "./data/app/" + appFiles, err := os.ReadDir(appsDir) + total = 0 + success = 0 + if err != nil { + LOG.ERROR("Error reading apps directory:%v", err) + return + } + + for _, file := range appFiles { + totalDelta, successDelta := reloadAPP(file, appsDir) + total += totalDelta + success += successDelta + } + CmdMap = mergeMaps(CmdMap, AppCore.CmdMap) + return total, success +} + +func reloadAPP(file os.DirEntry, appsDir string) (totalDelta int, successDelta int) { + if file.IsDir() { + return 0, 0 + } + + ext := filepath.Ext(file.Name()) + if ext == ".so" || (ext == ".dll" && os.PathSeparator == '\\') { + pluginPath := filepath.Join(appsDir, file.Name()) + p, err := plugin.Open(pluginPath) + if err != nil { + LOG.ERROR("Error opening app %s: %v\n", pluginPath, err) + return 1, 0 + } + + initSymbol, err := p.Lookup("Application") + if err != nil { + LOG.ERROR("Error finding interface Application in app %s: %v", pluginPath, err) + return 1, 0 + } + + app, ok := initSymbol.(wba.APP) + if !ok { + LOG.ERROR("init symbol in app %s is not a right type", pluginPath) + return 1, 0 + } + + err = app.Init(&AppApi) + if err != nil { + LOG.ERROR("Error initializing app %s: %v", pluginPath, err) + } + + //CmdMap = mergeMaps(CmdMap, app.Get()) + LOG.INFO("App %s initialized successfully", pluginPath) + return 1, 1 + + } + return 0, 0 +} + +func mergeMaps(map1, map2 map[string]wba.Cmd) map[string]wba.Cmd { + // 合并map1和map2到map3中 + map3 := make(map[string]wba.Cmd) + for key, value := range map1 { + map3[key] = value + } + for key, value := range map2 { + map3[key] = value + } + return map3 +} diff --git a/core/app_core.go b/core/app_core.go new file mode 100644 index 0000000..d9480d2 --- /dev/null +++ b/core/app_core.go @@ -0,0 +1,55 @@ +package core + +import ( + "ProjectWIND/LOG" + "ProjectWIND/wba" + "errors" +) + +type CmdListInfo map[string]wba.Cmd + +type AppInfo struct { + CmdMap map[string]wba.Cmd +} + +func (app AppInfo) Get() AppInfo { + return app +} + +func (app *AppInfo) Run(cmd string, args []string, msg wba.MessageEventInfo) error { + _, ok := app.CmdMap[cmd] + if !ok { + return errors.New("cmd not found") + } + app.CmdMap[cmd].SOLVE(args, msg) + return nil +} + +func (app *AppInfo) Init(Api wba.WindAPI) error { + return nil +} + +func (app *AppInfo) GetCmd() map[string]wba.Cmd { + return app.CmdMap +} + +func NewCmd(name string, help string, solve func(args []string, msg wba.MessageEventInfo)) wba.Cmd { + return wba.Cmd{ + NAME: name, + DESC: help, + SOLVE: solve, + } +} + +var AppCore = AppInfo{ + CmdMap: CmdListInfo{ + "bot": NewCmd( + "bot", + "显示WIND版本信息", + func(args []string, msg wba.MessageEventInfo) { + AppApi.SendMsg(msg, "WIND 0.1.0", false) + LOG.INFO("发送核心版本信息:(至:%v-%v:%v-%v)", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname) + }, + ), + }, +} diff --git a/core/cmdList.go b/core/cmdList.go deleted file mode 100644 index 1100e41..0000000 --- a/core/cmdList.go +++ /dev/null @@ -1,31 +0,0 @@ -package core - -import ( - "ProjectWIND/LOG" - "ProjectWIND/typed" -) - -var CmdList = map[string]typed.ExtInfo{ - "bot": ExtCore, - "help": ExtCore, -} - -var ExtCore = typed.ExtInfo{ - Run: func(cmd string, args []string, msg typed.MessageEventInfo) error { - if cmd == "help" { - err := SendMsg(msg, "假装有帮助信息", false) - LOG.INFO("发送核心帮助信息:(至:%v-%v:%v-%v)", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname) - if err != nil { - return err - } - } - if cmd == "bot" { - err := SendMsg(msg, "WIND 0.1.0", false) - LOG.INFO("发送核心版本信息:(至:%v-%v:%v-%v)", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname) - if err != nil { - return err - } - } - return nil - }, -} diff --git a/core/events_handler.go b/core/events_handler.go index ffc8803..ba47c59 100644 --- a/core/events_handler.go +++ b/core/events_handler.go @@ -2,14 +2,14 @@ package core import ( "ProjectWIND/LOG" - "ProjectWIND/typed" + "ProjectWIND/wba" "encoding/json" "fmt" "strings" ) func HandleMessage(msgJson []byte) { - var msg typed.MessageEventInfo + var msg wba.MessageEventInfo err := json.Unmarshal(msgJson, &msg) if err != nil { LOG.ERROR("Unmarshalling message: %v", err) @@ -18,18 +18,16 @@ func HandleMessage(msgJson []byte) { LOG.INFO("收到消息:(来自:%v-%v:%v-%v)%v", msg.MessageType, msg.GroupId, msg.UserId, msg.Sender.Nickname, msg.RawMessage) //如果消息文本内容为bot,发送框架信息。 cmd, args := CmdSplit(msg) - _, ok := CmdList[cmd] + _, ok := CmdMap[cmd] if ok { - err = CmdList[cmd].Run(cmd, args, msg) - if err != nil { - LOG.ERROR("消息发送失败: %v", err) - } + LOG.DEBUG("执行命令:%v %v", cmd, args) + CmdMap[cmd].SOLVE(args, msg) } // TODO: 处理消息内容 } func HandleNotice(msgJson []byte) { - var notice typed.NoticeEventInfo + var notice wba.NoticeEventInfo err := json.Unmarshal(msgJson, ¬ice) if err != nil { LOG.ERROR("Unmarshalling notice: %v", err) @@ -38,7 +36,7 @@ func HandleNotice(msgJson []byte) { } func HandleRequest(msgJson []byte) { - var request typed.NoticeEventInfo + var request wba.NoticeEventInfo err := json.Unmarshal(msgJson, &request) if err != nil { LOG.ERROR("Unmarshalling request: %v", err) @@ -47,7 +45,7 @@ func HandleRequest(msgJson []byte) { } func HandleMetaEvent(msgJson []byte) { - var meta typed.NoticeEventInfo + var meta wba.NoticeEventInfo err := json.Unmarshal(msgJson, &meta) if err != nil { LOG.ERROR("Unmarshalling meta: %v", err) @@ -55,7 +53,7 @@ func HandleMetaEvent(msgJson []byte) { // TODO: 处理元事件 } -func CmdSplit(msg typed.MessageEventInfo) (string, []string) { +func CmdSplit(msg wba.MessageEventInfo) (string, []string) { text := msg.RawMessage if strings.HasPrefix(text, fmt.Sprintf("[CQ:at,qq=%d]", msg.SelfId)) { text = strings.TrimPrefix(text, fmt.Sprintf("[CQ:at,qq=%d]", msg.SelfId)) @@ -68,9 +66,10 @@ func CmdSplit(msg typed.MessageEventInfo) (string, []string) { for _, prefix := range cmdPrefix { if strings.HasPrefix(text, prefix) { text = strings.TrimPrefix(text, prefix) - for cmd := range CmdList { + for cmd := range CmdMap { if strings.HasPrefix(text, cmd) { text = strings.TrimPrefix(text, cmd) + text = strings.TrimPrefix(text, " ") return cmd, strings.Split(text, " ") } } @@ -79,7 +78,7 @@ func CmdSplit(msg typed.MessageEventInfo) (string, []string) { return "", []string{} } -func statusCheck(msg typed.MessageEventInfo) bool { +func statusCheck(msg wba.MessageEventInfo) bool { //TODO: 检查当前组群工作状态 return false } diff --git a/core/web_socket.go b/core/web_socket.go index d23371f..0b9bdcd 100644 --- a/core/web_socket.go +++ b/core/web_socket.go @@ -15,7 +15,7 @@ import ( var gProtocolAddr string // WebSocketHandler 接收WebSocket连接处的消息并处理 -func WebSocketHandler(protocolAddr string) error { +func WebSocketHandler(protocolAddr string, token string) error { // 保存全局变量 gProtocolAddr = protocolAddr // 解析连接URL @@ -25,10 +25,18 @@ func WebSocketHandler(protocolAddr string) error { return err } - conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + // 创建一个带有自定义头的HTTP请求 + req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - LOG.ERROR("Dial error: %v", err) - return err + LOG.FATAL("创建请求出错:%v", err) + } + req.Header.Set("Authorization", "Bearer "+token) + // 配置WebSocket连接升级器 + dialer := websocket.DefaultDialer + // 使用升级器建立WebSocket连接 + conn, _, err := dialer.Dial(req.URL.String(), req.Header) + if err != nil { + LOG.FATAL("建立WebSocket连接出错:%v", err) } defer func(conn *websocket.Conn) { err := conn.Close() @@ -107,8 +115,8 @@ func processMessage(messageType int, message []byte) { } default: { - // 打印接收到的消息 - LOG.WARN("Received unknown event: %s", message) + // 此处为api请求响应数据,通过channel返回给调用者 + return } } } diff --git a/main.go b/main.go index e28911d..8a4d9c7 100644 --- a/main.go +++ b/main.go @@ -39,8 +39,8 @@ func main() { } if cmdArgs[0] == "-p" || cmdArgs[0] == "--protocol" { // 连接到协议端 - startProtocol() go AutoSave() + startProtocol() return } fmt.Println("Invalid command.") diff --git a/readme.md b/readme.md index a08810d..8216bfd 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ TODO: - ✅ 协议端通信 - ✅ 日志输出 - ✅ 文件初始化 -- ❌ 事件处理 +- ✅ 事件处理 - ❌ 数据库交互 - ❌ 插件系统 - ❌ 用户系统 diff --git a/typed/typed.go b/typed/typed.go index ce40384..738008e 100644 --- a/typed/typed.go +++ b/typed/typed.go @@ -3,163 +3,8 @@ package typed type CoreConfigInfo struct { CoreName string `json:"core_name"` ProtocolAddr string `json:"protocol_addr"` + Token string `json:"token"` WebUIPort uint16 `json:"webui_port"` PasswordHash string `json:"password_hash"` ServiceName string `json:"service_name"` } - -type MessageEventInfo struct { - Time int64 `json:"time,omitempty"` - SelfId int64 `json:"self_id,omitempty"` - PostType string `json:"post_type,omitempty"` - MessageType string `json:"message_type,omitempty"` - SubType string `json:"sub_type,omitempty"` - MessageId int32 `json:"message_id,omitempty"` - GroupId int64 `json:"group_id,omitempty"` - UserId int64 `json:"user_id,omitempty"` - Anonymous AnonymousInfo `json:"anonymous"` - Message []MessageInfo `json:"message,omitempty"` - RawMessage string `json:"raw_message,omitempty"` - Font int32 `json:"font,omitempty"` - Sender SenderInfo `json:"sender"` -} - -type NoticeEventInfo struct { - Time int64 `json:"time,omitempty"` - SelfId int64 `json:"self_id,omitempty"` - PostType string `json:"post_type,omitempty"` - NoticeType string `json:"notice_type,omitempty"` - GroupId int64 `json:"group_id,omitempty"` - UserId int64 `json:"user_id,omitempty"` - File FileInfo `json:"file,omitempty"` - SubType string `json:"sub_type,omitempty"` - OperatorId int64 `json:"operator_id,omitempty"` - Duration int64 `json:"duration,omitempty"` - MessageId int64 `json:"message,omitempty"` - TargetId int64 `json:"target_id,omitempty"` - HonorType string `json:"honor_type,omitempty"` -} - -type RequestEventInfo struct { - Time int64 `json:"time,omitempty"` - SelfId int64 `json:"self_id,omitempty"` - PostType string `json:"post_type,omitempty"` - RequestType string `json:"request_type,omitempty"` - SubType string `json:"sub_type,omitempty"` - UserId int64 `json:"user_id,omitempty"` - Comment string `json:"comment,omitempty"` - Flag string `json:"flag,omitempty"` - GroupId int64 `json:"group_id,omitempty"` -} - -type MetaEventInfo struct { - Time int64 `json:"time,omitempty"` - SelfId int64 `json:"self_id,omitempty"` - PostType string `json:"post_type,omitempty"` - MetaEventType string `json:"meta_event_type,omitempty"` - SubType string `json:"sub_type,omitempty"` - Status string `json:"status,omitempty"` - Interval int64 `json:"interval,omitempty"` -} - -type FileInfo struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Size int64 `json:"size,omitempty"` - Busid int64 `json:"bucket,omitempty"` -} - -type SenderInfo struct { - UserId int64 `json:"user_id,omitempty"` - Nickname string `json:"nickname,omitempty"` - Card string `json:"card,omitempty"` - Sex string `json:"sex,omitempty"` - Age int32 `json:"age,omitempty"` - Area string `json:"area,omitempty"` - Level string `json:"level,omitempty"` - Role string `json:"role,omitempty"` - Title string `json:"title,omitempty"` -} - -type AnonymousInfo struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Flag string `json:"flag,omitempty"` -} - -type MessageInfo struct { - Type string `json:"type,omitempty"` - Data MessageDataInfo `json:"data"` -} - -type MessageDataInfo struct { - Type string `json:"type,omitempty"` - Text string `json:"text,omitempty"` - Id string `json:"id,omitempty"` - File string `json:"file,omitempty"` - Url string `json:"url,omitempty"` - Magic string `json:"magic,omitempty"` - Qq string `json:"qq,omitempty"` - Title string `json:"title,omitempty"` - Content string `json:"content,omitempty"` - Image string `json:"image,omitempty"` - Audio string `json:"audio,omitempty"` - Lat string `json:"lat,omitempty"` - Lon string `json:"lon,omitempty"` - Data string `json:"data,omitempty"` -} - -type APIRequestInfo struct { - Action string `json:"action,omitempty"` - Params ParamsInfo `json:"params"` - Echo string `json:"echo,omitempty"` -} - -type ParamsInfo struct { - Message string `json:"message,omitempty"` - UserId int64 `json:"user_id,omitempty"` - GroupId int64 `json:"group_id,omitempty"` - AutoEscape bool `json:"auto_escape,omitempty"` - MessageId int32 `json:"message_id,omitempty"` - Id string `json:"id,omitempty"` - RejectAddRequest bool `json:"reject_add_request,omitempty"` - Duration int32 `json:"duration,omitempty"` - Enable bool `json:"enable,omitempty"` - Card string `json:"card,omitempty"` - GroupName string `json:"group_name,omitempty"` - IsDismiss bool `json:"is_dismiss,omitempty"` - SpecialTitle string `json:"special_title,omitempty"` - Flag string `json:"flag,omitempty"` - Approve bool `json:"approve,omitempty"` - Remark string `json:"remark,omitempty"` - Type string `json:"type,omitempty"` - SubType string `json:"sub_type,omitempty"` - Reason string `json:"reason,omitempty"` - NoCache bool `json:"no_cache,omitempty"` - File string `json:"file,omitempty"` - Times int `json:"times,omitempty"` -} - -type ExtInfo struct { - Inform func() extInformation - Run func(cmd string, args []string, msg MessageEventInfo) error - Init func() error - CmdList func() []string -} - -func (e *ExtInfo) SetInform(name string, version string, author string) { - inform := extInformation{ - Name: name, - Version: version, - Author: author, - } - e.Inform = func() extInformation { - return inform - } -} - -type extInformation struct { - Name string - Version string - Author string -} diff --git a/utils.go b/utils.go index 22ce811..015abef 100644 --- a/utils.go +++ b/utils.go @@ -15,7 +15,7 @@ import ( func initCore() string { // 初始化日志记录器 - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.SetFlags(log.Ldate | log.Ltime) log.SetPrefix("[WIND] ") LOG.INFO("正在初始化WIND配置文件...") @@ -128,6 +128,9 @@ func checkAndUpdateConfig(configPath string) error { if coreConfig.PasswordHash == "" { coreConfig.PasswordHash = "" } + if coreConfig.Token == "" { + coreConfig.Token = "" + } formattedJSON, err := json.MarshalIndent(coreConfig, "", " ") if err != nil { @@ -193,7 +196,7 @@ func startWebUI() { //初始化 logFile := initCore() // 设置日志输出到文件 - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.SetFlags(log.Ldate | log.Ltime) log.SetPrefix("[WIND] ") // 打开日志文件 file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) @@ -220,7 +223,7 @@ func registerService() { //初始化 logFile := initCore() // 设置日志输出到文件 - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.SetFlags(log.Ldate | log.Ltime) log.SetPrefix("[WIND] ") // 打开日志文件 file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) @@ -242,7 +245,7 @@ func startProtocol() { //初始化 logFile := initCore() // 设置日志输出到文件 - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.SetFlags(log.Ldate | log.Ltime) log.SetPrefix("[WIND] ") // 打开日志文件 file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) @@ -257,6 +260,7 @@ func startProtocol() { }(file) // 设置日志输出到文件 log.SetOutput(io.MultiWriter(os.Stdout, file)) + ReloadApps() //从配置文件中读取配置信息 LOG.INFO("正在启动WIND协议服务...") var config typed.CoreConfigInfo @@ -278,10 +282,12 @@ func startProtocol() { } //获取协议地址 protocolAddr := config.ProtocolAddr + //获取token + token := config.Token //链接协议 // 启动 WebSocket 处理程序 LOG.INFO("正在启动WebSocket链接程序...") - err = core.WebSocketHandler(protocolAddr) + err = core.WebSocketHandler(protocolAddr, token) if err != nil { // 如果发生错误,记录错误并退出程序 LOG.FATAL("Failed to start WebSocket link program: %v", err) @@ -296,3 +302,9 @@ func AutoSave() { time.Sleep(time.Second * 60) } } + +func ReloadApps() { + LOG.INFO("正在重新加载应用...") + total, success := core.ReloadApps() + LOG.INFO("应用重新加载完成,共加载%d个应用,成功加载%d个应用。", total, success) +} diff --git a/wba/wind.go b/wba/wind.go new file mode 100644 index 0000000..93963dc --- /dev/null +++ b/wba/wind.go @@ -0,0 +1,289 @@ +package wba + +import ( + "errors" + "fmt" +) + +type APP interface { + Get() AppInfo + Run(cmd string, args []string, msg MessageEventInfo) error + Init(api WindAPI) error +} + +type WindAPI interface { + SendMsg(msg MessageEventInfo, message string, autoEscape bool) + SendPrivateMsg(msg MessageEventInfo, message string, autoEscape bool) + SendGroupMsg(msg MessageEventInfo, message string, autoEscape bool) + DeleteMsg(msg MessageEventInfo) + SendLike(userId int64, times int) + SetGroupKick(groupId int64, userId int64, rejectAddRequest bool) + SetGroupBan(groupId int64, userId int64, duration int32) + SetGroupWholeBan(groupId int64, enable bool) + SetGroupAdmin(groupId int64, userId int64, enable bool) + SetGroupLeave(groupId int64, isDismiss bool) + SetGroupCard(groupId int64, userId int64, card string) + SetGroupName(groupId int64, groupName string) + SetGroupSpecialTitle(groupId int64, userId int64, specialTitle string, duration int32) + SetFriendAddRequest(flag string, approve bool, remark string) + SetGroupAddRequest(flag string, subType string, approve bool, reason string) +} + +type AppInfo struct { + name string + version string + author string + description string + namespace string + webUrl string + license string + appType string + rule string + CmdMap map[string]Cmd +} + +func (ei AppInfo) Get() AppInfo { + return ei +} + +func (ei *AppInfo) Run(cmd string, args []string, msg MessageEventInfo) error { + _, ok := ei.CmdMap[cmd] + if !ok { + return errors.New("cmd not found") + } + ei.CmdMap[cmd].SOLVE(args, msg) + return nil +} + +func (ei *AppInfo) Init(api WindAPI) error { + Wind = api + return nil +} + +func (ei *AppInfo) AddCmd(name string, cmd Cmd) error { + ei.CmdMap[name] = cmd + return nil +} + +type AppInfoOption func(ei *AppInfo) + +func WithName(name string) AppInfoOption { + return func(ei *AppInfo) { + ei.name = name + } +} + +func WithVersion(version string) AppInfoOption { + return func(ei *AppInfo) { + ei.version = version + } +} + +func WithAuthor(author string) AppInfoOption { + return func(ei *AppInfo) { + ei.author = author + } +} + +func WithDescription(description string) AppInfoOption { + return func(ei *AppInfo) { + ei.description = description + } +} + +func WithNamespace(namespace string) AppInfoOption { + return func(ei *AppInfo) { + ei.namespace = namespace + } +} + +func WithWebUrl(webUrl string) AppInfoOption { + return func(ei *AppInfo) { + ei.webUrl = webUrl + } +} + +func WithLicense(license string) AppInfoOption { + return func(ei *AppInfo) { + ei.license = license + } +} + +func WithAppType(appType string) AppInfoOption { + return func(ei *AppInfo) { + ei.appType = appType + } +} + +func WithRule(rule string) AppInfoOption { + return func(ei *AppInfo) { + ei.rule = fmt.Sprintf("rule_%s", rule) + } +} + +func NewApp(opts ...AppInfoOption) AppInfo { + Ext := AppInfo{ + name: "Wind", + version: "v1.0.0", + author: "Wind", + description: "A simple and easy-to-use bot framework", + namespace: "wind", + webUrl: "https://github.com/Sheyiyuan/wind_app_model", + license: "MIT", + appType: "fun", + rule: "none", + CmdMap: make(map[string]Cmd), + } + for _, opt := range opts { + opt(&Ext) + } + return Ext +} + +func NewCmd(name string, description string, solve func(args []string, msg MessageEventInfo)) Cmd { + return Cmd{ + NAME: name, + DESC: description, + SOLVE: solve, + } +} + +type Cmd struct { + NAME string + DESC string + SOLVE func(args []string, msg MessageEventInfo) +} + +type MessageEventInfo struct { + Time int64 `json:"time,omitempty"` + SelfId int64 `json:"self_id,omitempty"` + PostType string `json:"post_type,omitempty"` + MessageType string `json:"message_type,omitempty"` + SubType string `json:"sub_type,omitempty"` + MessageId int32 `json:"message_id,omitempty"` + GroupId int64 `json:"group_id,omitempty"` + UserId int64 `json:"user_id,omitempty"` + Anonymous AnonymousInfo `json:"anonymous"` + Message []MessageInfo `json:"message,omitempty"` + RawMessage string `json:"raw_message,omitempty"` + Font int32 `json:"font,omitempty"` + Sender SenderInfo `json:"sender"` +} + +type NoticeEventInfo struct { + Time int64 `json:"time,omitempty"` + SelfId int64 `json:"self_id,omitempty"` + PostType string `json:"post_type,omitempty"` + NoticeType string `json:"notice_type,omitempty"` + GroupId int64 `json:"group_id,omitempty"` + UserId int64 `json:"user_id,omitempty"` + File FileInfo `json:"file,omitempty"` + SubType string `json:"sub_type,omitempty"` + OperatorId int64 `json:"operator_id,omitempty"` + Duration int64 `json:"duration,omitempty"` + MessageId int64 `json:"message,omitempty"` + TargetId int64 `json:"target_id,omitempty"` + HonorType string `json:"honor_type,omitempty"` +} + +type RequestEventInfo struct { + Time int64 `json:"time,omitempty"` + SelfId int64 `json:"self_id,omitempty"` + PostType string `json:"post_type,omitempty"` + RequestType string `json:"request_type,omitempty"` + SubType string `json:"sub_type,omitempty"` + UserId int64 `json:"user_id,omitempty"` + Comment string `json:"comment,omitempty"` + Flag string `json:"flag,omitempty"` + GroupId int64 `json:"group_id,omitempty"` +} + +type MetaEventInfo struct { + Time int64 `json:"time,omitempty"` + SelfId int64 `json:"self_id,omitempty"` + PostType string `json:"post_type,omitempty"` + MetaEventType string `json:"meta_event_type,omitempty"` + SubType string `json:"sub_type,omitempty"` + Status string `json:"status,omitempty"` + Interval int64 `json:"interval,omitempty"` +} + +type FileInfo struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Size int64 `json:"size,omitempty"` + Busid int64 `json:"bucket,omitempty"` +} + +type SenderInfo struct { + UserId int64 `json:"user_id,omitempty"` + Nickname string `json:"nickname,omitempty"` + Card string `json:"card,omitempty"` + Sex string `json:"sex,omitempty"` + Age int32 `json:"age,omitempty"` + Area string `json:"area,omitempty"` + Level string `json:"level,omitempty"` + Role string `json:"role,omitempty"` + Title string `json:"title,omitempty"` +} + +type AnonymousInfo struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Flag string `json:"flag,omitempty"` +} + +type MessageInfo struct { + Type string `json:"type,omitempty"` + Data MessageDataInfo `json:"data"` +} + +type MessageDataInfo struct { + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + Id string `json:"id,omitempty"` + File string `json:"file,omitempty"` + Url string `json:"url,omitempty"` + Magic string `json:"magic,omitempty"` + Qq string `json:"qq,omitempty"` + Title string `json:"title,omitempty"` + Content string `json:"content,omitempty"` + Image string `json:"image,omitempty"` + Audio string `json:"audio,omitempty"` + Lat string `json:"lat,omitempty"` + Lon string `json:"lon,omitempty"` + Data string `json:"data,omitempty"` +} + +type ParamsInfo struct { + Message string `json:"message,omitempty"` + UserId int64 `json:"user_id,omitempty"` + GroupId int64 `json:"group_id,omitempty"` + AutoEscape bool `json:"auto_escape,omitempty"` + MessageId int32 `json:"message_id,omitempty"` + Id string `json:"id,omitempty"` + RejectAddRequest bool `json:"reject_add_request,omitempty"` + Duration int32 `json:"duration,omitempty"` + Enable bool `json:"enable,omitempty"` + Card string `json:"card,omitempty"` + GroupName string `json:"group_name,omitempty"` + IsDismiss bool `json:"is_dismiss,omitempty"` + SpecialTitle string `json:"special_title,omitempty"` + Flag string `json:"flag,omitempty"` + Approve bool `json:"approve,omitempty"` + Remark string `json:"remark,omitempty"` + Type string `json:"type,omitempty"` + SubType string `json:"sub_type,omitempty"` + Reason string `json:"reason,omitempty"` + NoCache bool `json:"no_cache,omitempty"` + File string `json:"file,omitempty"` + Times int `json:"times,omitempty"` +} + +type APIRequestInfo struct { + Action string `json:"action,omitempty"` + Params ParamsInfo `json:"params"` + Echo string `json:"echo,omitempty"` +} + +var Wind WindAPI