From 2f00ffdb9c21a66b86a1a444e4b6347ca9dae4c0 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Mon, 8 Jan 2024 13:47:06 +0800
Subject: [PATCH 1/7] beta122

---
 readme.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/readme.md b/readme.md
index d8f7a8ee..90341e90 100644
--- a/readme.md
+++ b/readme.md
@@ -40,6 +40,9 @@ _✨ 基于 [OneBot](https://github.com/howmanybots/onebot/blob/master/README.md
   ·
   <a href="https://github.com/hoshinonyaruko/gensokyo/blob/master/CONTRIBUTING.md">参与贡献</a>
 </p>
+<p align="center">
+  <a href="https://gensokyo.bot">项目主页:gensokyo.bot</a>
+</p>
 
 ## 引用
 - [`tencent-connect/botgo`](https://github.com/tencent-connect/botgo): 本项目引用了此项目,并做了一点改动.

From 2158740465a2f59dd5ae7727c71b8b63158088a1 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Thu, 11 Jan 2024 17:13:42 +0800
Subject: [PATCH 2/7] beta123

---
 handlers/send_msg.go | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/handlers/send_msg.go b/handlers/send_msg.go
index 1982ae06..b06dc2b5 100644
--- a/handlers/send_msg.go
+++ b/handlers/send_msg.go
@@ -33,13 +33,15 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 	if msgType == "" {
 		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
-	//新增 内存获取不到从数据库获取
+	//顺序有讲究
 	if msgType == "" {
-		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
+		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
 	}
+	//新增 内存获取不到从数据库获取
 	if msgType == "" {
-		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
+		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
 	}
+
 	var idInt64 int64
 	var err error
 

From ea41561628f1d01fd60b334592e65bb3cef4c156 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Thu, 11 Jan 2024 19:38:41 +0800
Subject: [PATCH 3/7] beta124

---
 handlers/send_msg.go         |  5 ++++-
 handlers/send_private_msg.go | 12 ++++++------
 2 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/handlers/send_msg.go b/handlers/send_msg.go
index b06dc2b5..fa6562f8 100644
--- a/handlers/send_msg.go
+++ b/handlers/send_msg.go
@@ -42,13 +42,15 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
 	}
 
-	var idInt64 int64
+	var idInt64, idInt642 int64
 	var err error
 
 	if message.Params.GroupID != "" {
 		idInt64, err = ConvertToInt64(message.Params.GroupID)
+		idInt642, err = ConvertToInt64(message.Params.UserID)
 	} else if message.Params.UserID != "" {
 		idInt64, err = ConvertToInt64(message.Params.UserID)
+		idInt642, err = ConvertToInt64(message.Params.GroupID)
 	}
 
 	//设置递归 对直接向gsk发送action时有效果
@@ -101,6 +103,7 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 	//重置递归类型
 	if echo.GetMapping(idInt64) <= 0 {
 		echo.AddMsgType(config.GetAppIDStr(), idInt64, "")
+		echo.AddMsgType(config.GetAppIDStr(), idInt642, "")
 	}
 	echo.AddMapping(idInt64, echo.GetMapping(idInt64)-1)
 
diff --git a/handlers/send_private_msg.go b/handlers/send_private_msg.go
index 43bcba67..6dcefe7d 100644
--- a/handlers/send_private_msg.go
+++ b/handlers/send_private_msg.go
@@ -29,14 +29,14 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open
 		msgType = echo.GetMsgTypeByKey(echoStr)
 	}
 
-	//如果获取不到 就用group_id获取信息类型
-	if msgType == "" {
-		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
-	}
 	//如果获取不到 就用user_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
+	//顺序,私聊优先从UserID推断类型会更准确
+	if msgType == "" {
+		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
+	}
 	//新增 内存获取不到从数据库获取
 	if msgType == "" {
 		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
@@ -68,7 +68,7 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open
 	}
 
 	switch msgType {
-	case "group_private","group":
+	case "group_private", "group":
 		//私聊信息
 		var UserID string
 		if config.GetIdmapPro() {
@@ -266,7 +266,7 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open
 				retmsg, _ = SendResponse(client, err, &message)
 			}
 		}
-	case "guild_private","guild":
+	case "guild_private", "guild":
 		//当收到发私信调用 并且来源是频道
 		retmsg, _ = HandleSendGuildChannelPrivateMsg(client, api, apiv2, message, nil, nil)
 	default:

From c8561ec14daf76e5daa679773968a9f78465a7d1 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Thu, 11 Jan 2024 19:41:19 +0800
Subject: [PATCH 4/7] beta124

---
 handlers/send_msg.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/handlers/send_msg.go b/handlers/send_msg.go
index fa6562f8..843e744e 100644
--- a/handlers/send_msg.go
+++ b/handlers/send_msg.go
@@ -46,11 +46,11 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 	var err error
 
 	if message.Params.GroupID != "" {
-		idInt64, err = ConvertToInt64(message.Params.GroupID)
-		idInt642, err = ConvertToInt64(message.Params.UserID)
+		idInt64, _ = ConvertToInt64(message.Params.GroupID)
+		idInt642, _ = ConvertToInt64(message.Params.UserID)
 	} else if message.Params.UserID != "" {
-		idInt64, err = ConvertToInt64(message.Params.UserID)
-		idInt642, err = ConvertToInt64(message.Params.GroupID)
+		idInt64, _ = ConvertToInt64(message.Params.UserID)
+		idInt642, _ = ConvertToInt64(message.Params.GroupID)
 	}
 
 	//设置递归 对直接向gsk发送action时有效果

From 22865d4b69358b20317304ca685d340b81f03896 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Thu, 11 Jan 2024 20:07:19 +0800
Subject: [PATCH 5/7] beta124

---
 handlers/send_msg.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/handlers/send_msg.go b/handlers/send_msg.go
index 843e744e..00960811 100644
--- a/handlers/send_msg.go
+++ b/handlers/send_msg.go
@@ -29,18 +29,22 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
 	}
+	mylog.Printf("测试1:%v", msgType)
 	//如果获取不到 就用user_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
+	mylog.Printf("测试2:%v", msgType)
 	//顺序有讲究
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
 	}
+	mylog.Printf("测试3:%v", msgType)
 	//新增 内存获取不到从数据库获取
 	if msgType == "" {
 		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
 	}
+	mylog.Printf("测试4:%v", msgType)
 
 	var idInt64, idInt642 int64
 	var err error

From d51f4190ed57f9052fb2c069e46c35877ab820b9 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Thu, 11 Jan 2024 20:25:13 +0800
Subject: [PATCH 6/7] beta127

---
 handlers/send_guild_channel_msg.go | 7 ++-----
 handlers/send_msg.go               | 8 --------
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/handlers/send_guild_channel_msg.go b/handlers/send_guild_channel_msg.go
index c84ed3e5..c625902b 100644
--- a/handlers/send_guild_channel_msg.go
+++ b/handlers/send_guild_channel_msg.go
@@ -34,20 +34,17 @@ func HandleSendGuildChannelMsg(client callapi.Client, api openapi.OpenAPI, apiv2
 		// 当 message.Echo 是字符串类型时执行此块
 		msgType = echo.GetMsgTypeByKey(echoStr)
 	}
-	//如果获取不到 就用group_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
 	}
-	//如果获取不到 就用user_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
-	//新增 内存获取不到从数据库获取
 	if msgType == "" {
-		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
+		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
 	}
 	if msgType == "" {
-		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
+		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
 	}
 	//当不转换频道信息时(不支持频道私聊)
 	if msgType == "" {
diff --git a/handlers/send_msg.go b/handlers/send_msg.go
index 00960811..f5ba50e9 100644
--- a/handlers/send_msg.go
+++ b/handlers/send_msg.go
@@ -25,26 +25,18 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope
 		// 当 message.Echo 是字符串类型时执行此块
 		msgType = echo.GetMsgTypeByKey(echoStr)
 	}
-	//如果获取不到 就用group_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
 	}
-	mylog.Printf("测试1:%v", msgType)
-	//如果获取不到 就用user_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
-	mylog.Printf("测试2:%v", msgType)
-	//顺序有讲究
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
 	}
-	mylog.Printf("测试3:%v", msgType)
-	//新增 内存获取不到从数据库获取
 	if msgType == "" {
 		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
 	}
-	mylog.Printf("测试4:%v", msgType)
 
 	var idInt64, idInt642 int64
 	var err error

From d6efb107bdf6660cb93751fd4b611655a172b105 Mon Sep 17 00:00:00 2001
From: cosmo <applegm@me.com>
Date: Fri, 12 Jan 2024 22:27:57 +0800
Subject: [PATCH 7/7] beta128

---
 Processor/ProcessC2CMessage.go           |   4 +
 Processor/ProcessChannelDirectMessage.go |   6 +
 Processor/ProcessGroupMessage.go         |   2 +
 Processor/ProcessGuildATMessage.go       |   9 +-
 Processor/ProcessGuildNormalMessage.go   |   4 +
 botgo/dto/keyboard/keyboard.go           |   4 +
 config/config.go                         |  26 ++++
 echo/echo.go                             |   9 ++
 handlers/send_group_msg.go               | 185 +++++++++++++++++++----
 images/format.go                         | 180 ++++++++++++++++++++++
 template/config_template.go              |   2 +
 11 files changed, 404 insertions(+), 27 deletions(-)
 create mode 100644 images/format.go

diff --git a/Processor/ProcessC2CMessage.go b/Processor/ProcessC2CMessage.go
index 2b734017..465ae443 100644
--- a/Processor/ProcessC2CMessage.go
+++ b/Processor/ProcessC2CMessage.go
@@ -118,6 +118,8 @@ func (p *Processors) ProcessC2CMessage(data *dto.WSC2CMessageData) error {
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			privateMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 将当前s和appid和message进行映射
 		echo.AddMsgID(AppIDString, s, data.ID)
@@ -220,6 +222,8 @@ func (p *Processors) ProcessC2CMessage(data *dto.WSC2CMessageData) error {
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			groupMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 获取MasterID数组
 		masterIDs := config.GetMasterID()
diff --git a/Processor/ProcessChannelDirectMessage.go b/Processor/ProcessChannelDirectMessage.go
index 53eb6ee7..e27faa94 100644
--- a/Processor/ProcessChannelDirectMessage.go
+++ b/Processor/ProcessChannelDirectMessage.go
@@ -141,6 +141,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData)
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			privateMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 将当前s和appid和message进行映射
 		echo.AddMsgID(AppIDString, s, data.ID)
@@ -218,6 +220,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData)
 			// 根据条件判断是否添加Echo字段
 			if config.GetTwoWayEcho() {
 				onebotMsg.Echo = echostr
+				//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+				echo.AddMsgIDv3(AppIDString, echostr, messageText)
 			}
 			// 获取MasterID数组
 			masterIDs := config.GetMasterID()
@@ -368,6 +372,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData)
 			// 根据条件判断是否添加Echo字段
 			if config.GetTwoWayEcho() {
 				groupMsg.Echo = echostr
+				//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+				echo.AddMsgIDv3(AppIDString, echostr, messageText)
 			}
 			// 获取MasterID数组
 			masterIDs := config.GetMasterID()
diff --git a/Processor/ProcessGroupMessage.go b/Processor/ProcessGroupMessage.go
index 28387413..87d2e8a1 100644
--- a/Processor/ProcessGroupMessage.go
+++ b/Processor/ProcessGroupMessage.go
@@ -135,6 +135,8 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error {
 	// 根据条件判断是否添加Echo字段
 	if config.GetTwoWayEcho() {
 		groupMsg.Echo = echostr
+		//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+		echo.AddMsgIDv3(AppIDString, echostr, messageText)
 	}
 	// 获取MasterID数组
 	masterIDs := config.GetMasterID()
diff --git a/Processor/ProcessGuildATMessage.go b/Processor/ProcessGuildATMessage.go
index e043dfba..c9426919 100644
--- a/Processor/ProcessGuildATMessage.go
+++ b/Processor/ProcessGuildATMessage.go
@@ -75,7 +75,12 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error {
 			SubType: "channel",
 			Time:    t.Unix(),
 			Avatar:  data.Author.Avatar,
-			Echo:    echostr,
+		}
+		// 根据条件判断是否添加Echo字段
+		if config.GetTwoWayEcho() {
+			onebotMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 获取MasterID数组
 		masterIDs := config.GetMasterID()
@@ -226,6 +231,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error {
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			groupMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 获取MasterID数组
 		masterIDs := config.GetMasterID()
diff --git a/Processor/ProcessGuildNormalMessage.go b/Processor/ProcessGuildNormalMessage.go
index 71b66abc..da7c32a6 100644
--- a/Processor/ProcessGuildNormalMessage.go
+++ b/Processor/ProcessGuildNormalMessage.go
@@ -78,6 +78,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error {
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			onebotMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 获取MasterID数组
 		masterIDs := config.GetMasterID()
@@ -225,6 +227,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error {
 		// 根据条件判断是否添加Echo字段
 		if config.GetTwoWayEcho() {
 			groupMsg.Echo = echostr
+			//用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文
+			echo.AddMsgIDv3(AppIDString, echostr, messageText)
 		}
 		// 获取MasterID数组
 		masterIDs := config.GetMasterID()
diff --git a/botgo/dto/keyboard/keyboard.go b/botgo/dto/keyboard/keyboard.go
index f53b4a74..b6c26149 100644
--- a/botgo/dto/keyboard/keyboard.go
+++ b/botgo/dto/keyboard/keyboard.go
@@ -60,6 +60,10 @@ type Action struct {
 	ClickLimit           uint32      `json:"click_limit,omitempty"`              // 可点击的次数, 默认不限
 	Data                 string      `json:"data,omitempty"`                     // 操作相关数据
 	AtBotShowChannelList bool        `json:"at_bot_show_channel_list,omitempty"` // false:当前 true:弹出展示子频道选择器
+	UnsupportTips        string      `json:"unsupport_tips"`                     //2024-1-12 新增字段
+	AnChor               int         `json:"anchor"`                             //本字段仅在指令按钮下有效,设置后后会忽略 action.enter 配置。设置为 1 时 ,点击按钮自动唤起启手Q选图器,其他值暂无效果。(仅支持手机端版本 8983+ 的单聊场景,桌面端不支持)
+	Enter                bool        `json:"enter"`                              //指令按钮可用,点击按钮后直接自动发送 data,默认 false。支持版本 8983
+	Reply                bool        `json:"reply"`                              //指令按钮可用,指令是否带引用回复本消息,默认 false。支持版本 8983
 }
 
 // Permission 按纽操作权限
diff --git a/config/config.go b/config/config.go
index 021ed06f..9f8a7940 100644
--- a/config/config.go
+++ b/config/config.go
@@ -131,6 +131,8 @@ type Settings struct {
 	WhiteEnable            []bool               `yaml:"white_enable"`
 	IdentifyAppids         []int64              `yaml:"identify_appids"`
 	TransFormApiIds        bool                 `yaml:"transform_api_ids"`
+	CustomTemplateID       string               `yaml:"custom_template_id"`
+	KeyBoardID             string               `yaml:"keyboard_id"`
 }
 
 // LoadConfig 从文件中加载配置并初始化单例配置
@@ -1650,3 +1652,27 @@ func GetTransFormApiIds() bool {
 	}
 	return instance.Settings.TransFormApiIds
 }
+
+// 获取 CustomTemplateID 的值
+func GetCustomTemplateID() string {
+	mu.Lock()
+	defer mu.Unlock()
+
+	if instance == nil {
+		mylog.Println("Warning: instance is nil when trying to get CustomTemplateID.")
+		return ""
+	}
+	return instance.Settings.CustomTemplateID
+}
+
+// 获取 KeyBoardIDD 的值
+func GetKeyBoardID() string {
+	mu.Lock()
+	defer mu.Unlock()
+
+	if instance == nil {
+		mylog.Println("Warning: instance is nil when trying to get KeyBoardID.")
+		return ""
+	}
+	return instance.Settings.KeyBoardID
+}
diff --git a/echo/echo.go b/echo/echo.go
index c898215a..fc840e4f 100644
--- a/echo/echo.go
+++ b/echo/echo.go
@@ -106,6 +106,15 @@ func AddMsgIDv3(appid string, s string, msgID string) {
 	globalEchoMapping.msgIDMapping[key] = msgID
 }
 
+// GetMsgIDv3 返回给定appid和s的msgID
+func GetMsgIDv3(appid string, s string) string {
+	key := globalEchoMapping.GenerateKeyv3(appid, s)
+	globalEchoMapping.mu.Lock()
+	defer globalEchoMapping.mu.Unlock()
+
+	return globalEchoMapping.msgIDMapping[key]
+}
+
 // 添加group和userid对应的messageid
 func AddMsgIDv2(appid string, groupid int64, userid int64, msgID string) {
 	key := globalEchoMapping.GenerateKeyv2(appid, groupid, userid)
diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go
index 9905aada..93b869e7 100644
--- a/handlers/send_group_msg.go
+++ b/handlers/send_group_msg.go
@@ -4,10 +4,12 @@ import (
 	"bytes"
 	"context"
 	"encoding/base64"
+	"fmt"
 	"io"
 	"net/http"
 	"os"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/hoshinonyaruko/gensokyo/callapi"
@@ -18,6 +20,7 @@ import (
 	"github.com/hoshinonyaruko/gensokyo/mylog"
 	"github.com/hoshinonyaruko/gensokyo/silk"
 	"github.com/tencent-connect/botgo/dto"
+	"github.com/tencent-connect/botgo/dto/keyboard"
 	"github.com/tencent-connect/botgo/openapi"
 )
 
@@ -33,22 +36,18 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap
 		// 当 message.Echo 是字符串类型时执行此块
 		msgType = echo.GetMsgTypeByKey(echoStr)
 	}
-	//如果获取不到 就用user_id获取信息类型
-	if msgType == "" {
-		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
-	}
-
-	//如果获取不到 就用group_id获取信息类型
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID)
 	}
-	//新增 内存获取不到从数据库获取
 	if msgType == "" {
-		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
+		msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID)
 	}
 	if msgType == "" {
 		msgType = GetMessageTypeByGroupidV2(message.Params.GroupID)
 	}
+	if msgType == "" {
+		msgType = GetMessageTypeByUseridV2(message.Params.UserID)
+	}
 	mylog.Printf("send_group_msg获取到信息类型:%v", msgType)
 	var idInt64 int64
 	var err error
@@ -202,26 +201,52 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap
 				mylog.Printf("Error: Expected RichMediaMessage type for key ")
 				return "", nil
 			}
-			// 上传图片并获取FileInfo
-			fileInfo, err := uploadMedia(context.TODO(), message.Params.GroupID.(string), richMediaMessage, apiv2)
-			if err != nil {
-				mylog.Printf("上传图片失败: %v", err)
-				return "", nil // 或其他错误处理
-			}
-			// 创建包含文本和图像信息的消息
-			msgseq = echo.GetMappingSeq(messageID)
-			echo.AddMappingSeq(messageID, msgseq+1)
-			groupMessage := &dto.MessageToCreate{
-				Content: messageText, // 添加文本内容
-				Media: dto.Media{
-					FileInfo: fileInfo, // 添加图像信息
-				},
-				MsgID:   messageID,
-				MsgSeq:  msgseq,
-				MsgType: 7, // 假设7是组合消息类型
+			var groupMessage *dto.MessageToCreate
+			var transmd bool
+			var md *dto.Markdown
+			var kb *keyboard.MessageKeyboard
+			//判断是否需要自动转换md
+			if config.GetTwoWayEcho() {
+				md, kb, transmd = auto_md(message, messageText, richMediaMessage)
 			}
-			groupMessage.Timestamp = time.Now().Unix() // 设置时间戳
 
+			//如果没有转换成md发送
+			if !transmd {
+				// 上传图片并获取FileInfo
+				fileInfo, err := uploadMedia(context.TODO(), message.Params.GroupID.(string), richMediaMessage, apiv2)
+				if err != nil {
+					mylog.Printf("上传图片失败: %v", err)
+					return "", nil // 或其他错误处理
+				}
+				// 创建包含文本和图像信息的消息
+				msgseq = echo.GetMappingSeq(messageID)
+				echo.AddMappingSeq(messageID, msgseq+1)
+				groupMessage = &dto.MessageToCreate{
+					Content: messageText, // 添加文本内容
+					Media: dto.Media{
+						FileInfo: fileInfo, // 添加图像信息
+					},
+					MsgID:   messageID,
+					MsgSeq:  msgseq,
+					MsgType: 7, // 假设7是组合消息类型
+				}
+				groupMessage.Timestamp = time.Now().Unix() // 设置时间戳
+			} else {
+				//将kb和md组合成groupMessage并用MsgType=2发送
+
+				msgseq = echo.GetMappingSeq(messageID)
+				echo.AddMappingSeq(messageID, msgseq+1)
+				groupMessage = &dto.MessageToCreate{
+					Content:  "markdown", // 添加文本内容
+					MsgID:    messageID,
+					MsgSeq:   msgseq,
+					Markdown: md,
+					Keyboard: kb,
+					MsgType:  2, // 假设7是组合消息类型
+				}
+				groupMessage.Timestamp = time.Now().Unix() // 设置时间戳
+
+			}
 			// 发送组合消息
 			ret, err = apiv2.PostGroupMessage(context.TODO(), message.Params.GroupID.(string), groupMessage)
 			if err != nil {
@@ -1020,3 +1045,111 @@ func SendStackMessages(apiv2 openapi.OpenAPI, messageid string, originalGroupID
 
 	}
 }
+
+func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage *dto.RichMediaMessage) (md *dto.Markdown, kb *keyboard.MessageKeyboard, transmd bool) {
+	if echoStr, ok := message.Echo.(string); ok {
+		// 当 message.Echo 是字符串类型时才执行此块
+		msg_on_touch := echo.GetMsgIDv3(config.GetAppIDStr(), echoStr)
+		mylog.Printf("msg_on_touch:%v", msg_on_touch)
+		// 判断是否是 GetVisualkPrefixs 数组开头的文本
+		visualkPrefixs := config.GetVisualkPrefixs()
+		var matchedPrefix *config.VisualPrefixConfig
+		// 去掉前缀开头的*
+		for i, vp := range visualkPrefixs {
+			if strings.HasPrefix(vp.Prefix, "*") {
+				visualkPrefixs[i].Prefix = strings.TrimPrefix(vp.Prefix, "*")
+			}
+		}
+
+		for _, vp := range visualkPrefixs {
+			if strings.HasPrefix(msg_on_touch, vp.Prefix) {
+				if len(msg_on_touch) >= len(vp.Prefix) {
+					if msg_on_touch != " " {
+						transmd = true
+						matchedPrefix = &vp
+						break // 匹配到了
+					}
+				}
+			}
+		}
+		if transmd {
+			//将messageText和groupReply组合成一个md
+			// 处理 Markdown
+			CustomTemplateID := config.GetCustomTemplateID()
+			imgURL := richMediaMessage.URL
+			height, width, err := images.GetImageDimensions(imgURL)
+			if err != nil {
+				mylog.Printf("获取图片宽高出错")
+			}
+			imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height)
+			// 检查messageText是否以\r开头
+			if !strings.HasPrefix(messageText, "\r") {
+				messageText = "\r" + messageText
+			}
+			// 创建 MarkdownParams 的实例
+			mdParams := []*dto.MarkdownParams{
+				{Key: "text_start", Values: []string{" "}}, //空着
+				{Key: "img_dec", Values: []string{imgDesc}},
+				{Key: "img_url", Values: []string{imgURL}},
+				{Key: "text_end", Values: []string{messageText}},
+			}
+			// 组合模板 Markdown
+			md = &dto.Markdown{
+				CustomTemplateID: CustomTemplateID,
+				Params:           mdParams,
+			}
+			whiteList := matchedPrefix.WhiteList
+			// 创建 CustomKeyboard
+			customKeyboard := &keyboard.CustomKeyboard{
+				Rows: []*keyboard.Row{},
+			}
+
+			var currentRow *keyboard.Row
+			buttonCount := 0 // 当前行的按钮计数
+
+			for _, whiteLabel := range whiteList {
+				// 使用 strconv.Atoi 检查 whiteLabel 是否为纯数字
+				if _, err := strconv.Atoi(whiteLabel); err == nil {
+					// 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代
+					continue
+				}
+
+				// 创建按钮
+				button := &keyboard.Button{
+					RenderData: &keyboard.RenderData{
+						Label:        whiteLabel,
+						VisitedLabel: whiteLabel,
+						Style:        1,
+					},
+					Action: &keyboard.Action{
+						Type: 2, // 帮用户输入二级指令
+						Permission: &keyboard.Permission{
+							Type: 2, //所有人可操作
+						},
+						Data:          msg_on_touch + " " + whiteLabel,
+						UnsupportTips: "请升级新版手机QQ",
+					},
+				}
+
+				// 如果当前行为空或已满(4个按钮),则创建一个新行
+				if currentRow == nil || buttonCount == 4 {
+					currentRow = &keyboard.Row{}
+					customKeyboard.Rows = append(customKeyboard.Rows, currentRow)
+					buttonCount = 0 // 重置按钮计数
+				}
+
+				// 将按钮添加到当前行
+				currentRow.Buttons = append(currentRow.Buttons, button)
+				buttonCount++
+			}
+
+			// 在循环结束后,最后一行可能不满4个按钮,但已经被正确处理
+
+			// 创建 MessageKeyboard 并设置其 Content
+			kb = &keyboard.MessageKeyboard{
+				Content: customKeyboard,
+			}
+		}
+	}
+	return md, kb, transmd
+}
diff --git a/images/format.go b/images/format.go
new file mode 100644
index 00000000..a5cb61c2
--- /dev/null
+++ b/images/format.go
@@ -0,0 +1,180 @@
+package images
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+)
+
+// 宽度 高度
+func GetImageDimensions(url string) (int, int, error) {
+	if strings.HasSuffix(url, ".png") {
+		return getPNGDimensions(url)
+	} else if strings.HasSuffix(url, ".jpg") || strings.HasSuffix(url, ".jpeg") {
+		return getJpegDimensions(url)
+	} else if strings.HasSuffix(url, ".gif") {
+		return getGIFDimensions(url)
+	}
+	return 0, 0, fmt.Errorf("unsupported image format")
+}
+
+func getPNGDimensions(url string) (int, int, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return 0, 0, fmt.Errorf("error occurred while making request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return 0, 0, fmt.Errorf("failed to download image with status code: %d", resp.StatusCode)
+	}
+
+	// 只读取前33字节
+	buffer := make([]byte, 33)
+	_, err = io.ReadFull(resp.Body, buffer)
+	if err != nil {
+		return 0, 0, fmt.Errorf("error occurred while reading the header: %w", err)
+	}
+
+	// 检查PNG文件头
+	if bytes.HasPrefix(buffer, []byte("\x89PNG\r\n\x1a\n")) {
+		var width, height int32
+		reader := bytes.NewReader(buffer[16:24])
+		err := binary.Read(reader, binary.BigEndian, &width)
+		if err != nil {
+			return 0, 0, fmt.Errorf("error occurred while reading width: %w", err)
+		}
+		err = binary.Read(reader, binary.BigEndian, &height)
+		if err != nil {
+			return 0, 0, fmt.Errorf("error occurred while reading height: %w", err)
+		}
+		return int(width), int(height), nil
+	}
+
+	width, height, err := getJpegDimensions(url)
+	return width, height, err
+}
+
+func getJpegDimensions(url string) (int, int, error) {
+	response, err := http.Get(url)
+	if err != nil {
+		return 0, 0, fmt.Errorf("error occurred while making request: %w", err)
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return 0, 0, fmt.Errorf("failed to download image with status code: %d", response.StatusCode)
+	}
+
+	bytesRead := 0 // 用于跟踪读取的字节数
+	for {
+		b := make([]byte, 1)
+		_, err := response.Body.Read(b)
+		bytesRead++ // 更新读取的字节数
+		if err == io.EOF {
+			return 0, 0, fmt.Errorf("reached end of file before finding dimensions")
+		} else if err != nil {
+			return 0, 0, fmt.Errorf("error occurred while reading byte: %w", err)
+		}
+
+		if b[0] == 0xFF {
+			nextByte := make([]byte, 1)
+			_, err := response.Body.Read(nextByte)
+			if err == io.EOF {
+				return 0, 0, fmt.Errorf("reached end of file before finding SOI marker")
+			} else if err != nil {
+				return 0, 0, fmt.Errorf("error occurred while reading next byte: %w", err)
+			}
+
+			if nextByte[0] == 0xD8 { // SOI
+				continue
+			} else if (nextByte[0] >= 0xC0 && nextByte[0] <= 0xCF) && nextByte[0] != 0xC4 && nextByte[0] != 0xC8 && nextByte[0] != 0xCC {
+				c := make([]byte, 3)
+				_, err := response.Body.Read(c)
+				if err != nil {
+					return 0, 0, fmt.Errorf("error occurred while skipping bytes: %w", err)
+				}
+				heightBytes := make([]byte, 2)
+				widthBytes := make([]byte, 2)
+				_, err = response.Body.Read(heightBytes)
+				if err != nil {
+					return 0, 0, fmt.Errorf("error occurred while reading height: %w", err)
+				}
+				_, err = response.Body.Read(widthBytes)
+				if err != nil {
+					return 0, 0, fmt.Errorf("error occurred while reading width: %w", err)
+				}
+				// 使用binary.BigEndian.Uint16将字节转换为uint16
+				height := binary.BigEndian.Uint16(heightBytes)
+				width := binary.BigEndian.Uint16(widthBytes)
+				return int(height), int(width), nil
+			} else {
+				time.Sleep(5 * time.Millisecond)
+				lengthBytes := make([]byte, 2)
+				_, err := response.Body.Read(lengthBytes)
+				if err != nil {
+					return 0, 0, fmt.Errorf("error occurred while reading segment length: %w", err)
+				}
+
+				length := binary.BigEndian.Uint16(lengthBytes)
+				bytesToSkip := int(length) - 2
+
+				// 循环读取并跳过指定的字节数
+				for bytesToSkip > 0 {
+					bufferSize := bytesToSkip
+					if bufferSize > 512 { // 可以调整这个值以优化性能和内存使用
+						bufferSize = 512 //如果成功率低 就继续减少它
+					}
+					buffer := make([]byte, bufferSize)
+					n, err := response.Body.Read(buffer)
+					if err != nil {
+						if err == io.EOF {
+							break // 数据流结束
+						}
+						return 0, 0, fmt.Errorf("error occurred while skipping segment: %w", err)
+					}
+					bytesToSkip -= n
+				}
+			}
+		}
+	}
+}
+
+func getGIFDimensions(url string) (int, int, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return 0, 0, fmt.Errorf("error occurred while making request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return 0, 0, fmt.Errorf("failed to download image with status code: %d", resp.StatusCode)
+	}
+
+	// 仅读取前10字节
+	buffer := make([]byte, 10)
+	_, err = io.ReadFull(resp.Body, buffer)
+	if err != nil {
+		return 0, 0, fmt.Errorf("error occurred while reading the header: %w", err)
+	}
+
+	if bytes.HasPrefix(buffer, []byte("GIF")) {
+		var width, height int16
+		reader := bytes.NewReader(buffer[6:10])
+		err := binary.Read(reader, binary.LittleEndian, &width)
+		if err != nil {
+			return 0, 0, fmt.Errorf("error occurred while reading width: %w", err)
+		}
+		err = binary.Read(reader, binary.LittleEndian, &height)
+		if err != nil {
+			return 0, 0, fmt.Errorf("error occurred while reading height: %w", err)
+		}
+		return int(width), int(height), nil
+	}
+
+	return 0, 0, fmt.Errorf("not a valid GIF file")
+}
diff --git a/template/config_template.go b/template/config_template.go
index f01a81d8..bec2f464 100644
--- a/template/config_template.go
+++ b/template/config_template.go
@@ -128,6 +128,8 @@ settings:
   custom_bot_name : "Gensokyo全域机器人"                   #自定义机器人名字,会在api调用中返回,默认Gensokyo全域机器人
  
   twoway_echo : false               #是否采用双向echo,根据机器人选择,獭獭\早苗 true 红色问答\椛椛 或者其他 请使用 false
+  custom_template_id : ""           #自动转换图文信息到md所需要的id *需要应用端支持双方向echo
+  keyboard_id : ""                  #自动转换图文信息到md所需要的按钮id *需要应用端支持双方向echo
   lazy_message_id : false           #false=message_id 条条准确对应 true=message_id 按时间范围随机对应(适合主动推送bot)前提,有足够多的活跃信息刷新id池
   
   visible_ip : false                #转换url时,如果server_dir是ip true将以ip形式发出url 默认隐藏url 将server_dir配置为自己域名可以转换url