Browse Source

feat: 添加stream模式的支持

eryajf 1 year ago
parent
commit
8c33d7636e
9 changed files with 349 additions and 144 deletions
  1. 8 6
      README.md
  2. 2 0
      config.example.yml
  3. 9 0
      config/config.go
  4. 1 0
      docker-compose.yml
  5. 2 0
      go.mod
  6. 4 0
      go.sum
  7. 203 138
      main.go
  8. 84 0
      public/example_bot.go
  9. 36 0
      public/example_event.go

+ 8 - 6
README.md

@@ -28,7 +28,7 @@
 <p align='center'>
   😀企联AI共创计划正式开启😀
 </p>
-  
+
 <p align='center'>
    https://fork-way.feishu.cn/docx/Gvztd1iVXoXOsVxF2ujcnPPenDf
 </p>
@@ -46,8 +46,8 @@
 查看更多内容: https://connect-ai.forkway.cn
 
 企业客户咨询:13995928702(River)
-  
-  
+
+
 <p> 🌉 基于GO语言实现的钉钉集成ChatGPT机器人 🌉</p>
 
 [![Auth](https://img.shields.io/badge/Auth-eryajf-ff69b4)](https://github.com/eryajf)
@@ -101,14 +101,14 @@ AIGC的热潮正在各行各业掀起巨大的变革,我们看到各大社群
   - A奖励:小队完成度奖励,鼓励小队长参与项目,能够在指定时间内完成课题规定的基本内容,队长应获得一定的奖励。
   - B奖励:项目优秀度奖励,根据项目复杂度、组内配合度、产品创意度,以及期中和期末用户体验打分,评选出部分优秀项目的队长和核心队员,并给予相应奖励。
   - C奖励:成员活跃度奖励,考虑到设计和测试身份的特殊性,无法单独带领项目。因此,我们将评选出优秀设计师和优秀测试反馈员,以表彰他们在项目中的积极参与和贡献。
- 
+
  做出下面奖励安排
   - A奖励项目完成度:京东E卡300 * 10
   - B奖励项目优秀度:
     - 杰出奖: iPhone14 * 1 + 京东E卡300 * 3
     - 优秀奖: PS5 * 1 + 京东E卡300 * 3
   - C奖励成员活跃度:京东E卡300 * 4
- 
+
 我们队员有
 - [EX-chatGPT](https://github.com/circlestarzero/EX-chatGPT)和[ChatPaper的维护者](https://github.com/kaixindelele/ChatPaper)-->[cc](https://github.com/circlestarzero)
 - [钉钉GPT的维护者](https://github.com/eryajf/chatgpt-dingtalk)-->[eryajf](https://github.com/eryajf)
@@ -222,7 +222,7 @@ $ docker run -itd --name chatgpt -p 8090:8090 \
   -e DEFAULT_MODE="单聊" -e MAX_REQUEST=0 -e PORT=8090 \
   -e SERVICE_URL="你当前服务外网可访问的URL" -e CHAT_TYPE="0" \
   -e ALLOW_GROUPS=a,b -e ALLOW_OUTGOING_GROUPS=a,b -e ALLOW_USERS=a,b -e DENY_USERS=a,b -e VIP_USERS=a,b -e ADMIN_USERS=a,b -e APP_SECRETS="xxx,yyy" \
-  -e SENSITIVE_WORDS="aa,bb" \
+  -e SENSITIVE_WORDS="aa,bb" -e RUN_MODE="http" \
   -e AZURE_ON="false" -e AZURE_API_VERSION="" -e AZURE_RESOURCE_NAME="" \
   -e AZURE_DEPLOYMENT_NAME="" -e AZURE_OPENAI_TOKEN="" \
   -e DINGTALK_CREDENTIALS="your_client_id1:secret1,your_client_id2:secret2" \
@@ -486,6 +486,8 @@ $ go run main.go
 log_level: "info"
 # openai api_key
 api_key: "xxxxxxxxx"
+# 运行模式,http 或者 stream ,当前默认为http,等stream全面开放之后,这个模式将会是默认的启动模式
+run_mode: "http"
 # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
 base_url: ""
 # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-0314", "gpt-4", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单

+ 2 - 0
config.example.yml

@@ -2,6 +2,8 @@
 log_level: "info"
 # openai api_key
 api_key: "xxxxxxxxx"
+# 运行模式,http 或者 stream ,当前默认为http,等stream全面开放之后,这个模式将会是默认的启动模式
+run_mode: "http"
 # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
 base_url: ""
 # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-0314", "gpt-4", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单

+ 9 - 0
config/config.go

@@ -25,6 +25,8 @@ type Configuration struct {
 	LogLevel string `yaml:"log_level"`
 	// gpt apikey
 	ApiKey string `yaml:"api_key"`
+	// 运行模式
+	RunMode string `yaml:"run_mode"`
 	// 请求的 URL 地址
 	BaseURL string `yaml:"base_url"`
 	// 使用模型
@@ -97,6 +99,10 @@ func LoadConfig() *Configuration {
 		if apiKey != "" {
 			config.ApiKey = apiKey
 		}
+		runMode := os.Getenv("RUN_MODE")
+		if runMode != "" {
+			config.RunMode = runMode
+		}
 		baseURL := os.Getenv("BASE_URL")
 		if baseURL != "" {
 			config.BaseURL = baseURL
@@ -216,6 +222,9 @@ func LoadConfig() *Configuration {
 	if config.LogLevel == "" {
 		config.LogLevel = "info"
 	}
+	if config.RunMode == "" {
+		config.LogLevel = "http"
+	}
 	if config.Model == "" {
 		config.Model = "gpt-3.5-turbo"
 	}

+ 1 - 0
docker-compose.yml

@@ -8,6 +8,7 @@ services:
     environment:
       LOG_LEVEL: "info" # 应用的日志级别 info/debug
       APIKEY: xxxxxx  # 你的 api_key
+      RUN_MODE: ""  # 运行模式,http 或者 stream ,当前默认为http,等stream全面开放之后,这个模式将会是默认的启动模式
       BASE_URL: ""  # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议
       MODEL: "gpt-3.5-turbo" # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-0314", "gpt-4", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单
       SESSION_TIMEOUT: 600 # 会话超时时间,默认600秒,在会话时间内所有发送给机器人的信息会作为上下文

+ 2 - 0
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/gin-gonic/gin v1.9.0
 	github.com/glebarez/sqlite v1.7.0
 	github.com/go-resty/resty/v2 v2.7.0
+	github.com/open-dingtalk/dingtalk-stream-sdk-go v0.0.1
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/sashabaranov/go-openai v1.6.1
 	github.com/solywsh/chatgpt v0.0.14
@@ -34,6 +35,7 @@ require (
 	github.com/goccy/go-json v0.10.0 // indirect
 	github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
 	github.com/google/uuid v1.3.0 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect

+ 4 - 0
go.sum

@@ -65,6 +65,8 @@ github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R1
 github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -99,6 +101,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
 github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
+github.com/open-dingtalk/dingtalk-stream-sdk-go v0.0.1 h1:F7c4ZWg5FuL0giOVg0slzPmYLwbuQqTjsu5BkUL6VEU=
+github.com/open-dingtalk/dingtalk-stream-sdk-go v0.0.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
 github.com/pandodao/tokenizer-go v0.2.0 h1:NhfI8fGvQkDld2cZCag6NEU3pJ/ugU9zoY1R/zi9YCs=
 github.com/pandodao/tokenizer-go v0.2.0/go.mod h1:t6qFbaleKxbv0KNio2XUN/mfGM5WKv4haPXDQWVDG00=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=

+ 203 - 138
main.go

@@ -14,6 +14,11 @@ import (
 	"github.com/eryajf/chatgpt-dingtalk/pkg/process"
 	"github.com/eryajf/chatgpt-dingtalk/public"
 	"github.com/gin-gonic/gin"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
+	loger "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/payload"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/utils"
 )
 
 func init() {
@@ -21,10 +26,63 @@ func init() {
 	logger.InitLogger(public.Config.LogLevel)
 }
 func main() {
-	Start()
+	if public.Config.RunMode == "http" {
+		StartHttp()
+	} else {
+		for _, credential := range public.Config.Credentials {
+			StartStream(credential.ClientID, credential.ClientSecret)
+		}
+		select {}
+	}
+}
+func StartStream(clientId, clientSecret string) {
+	loger.SetLogger(loger.NewStdTestLogger())
+	cli := client.NewStreamClient(
+		client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)),
+		client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()),
+		client.WithSubscription(utils.SubscriptionTypeKCallback, payload.BotMessageCallbackTopic, chatbot.NewDefaultChatBotFrameHandler(OnChatReceive).OnEventReceived),
+	)
+	err := cli.Start(context.Background())
+	if err != nil {
+		panic(err)
+	}
+
+	defer cli.Close()
+
+	select {}
+}
+
+func OnChatReceive(ctx context.Context, data *chatbot.BotCallbackDataModel) (err error) {
+	msgObj := dingbot.ReceiveMsg{
+		ConversationID: data.ConversationId,
+		AtUsers: []struct {
+			DingtalkID string "json:\"dingtalkId\""
+		}{},
+		ChatbotUserID:             data.ChatbotUserId,
+		MsgID:                     data.MsgId,
+		SenderNick:                data.SenderNick,
+		IsAdmin:                   data.IsAdmin,
+		SenderStaffId:             data.SenderStaffId,
+		SessionWebhookExpiredTime: data.SessionWebhookExpiredTime,
+		CreateAt:                  data.CreateAt,
+		ConversationType:          data.ConversationType,
+		SenderID:                  data.SenderId,
+		ConversationTitle:         data.ConversationTitle,
+		IsInAtList:                data.IsInAtList,
+		SessionWebhook:            data.SessionWebhook,
+		Text:                      dingbot.Text(data.Text),
+		RobotCode:                 "",
+		Msgtype:                   dingbot.MsgType(data.Msgtype),
+	}
+	clientId := public.Config.Credentials[0].ClientID
+	var c gin.Context
+	c.Set(public.DingTalkClientIdKeyName, clientId)
+	DoRequest(msgObj, &c)
+
+	return nil
 }
 
-func Start() {
+func StartHttp() {
 	app := gin.Default()
 	app.POST("/", func(c *gin.Context) {
 		var msgObj dingbot.ReceiveMsg
@@ -32,142 +90,7 @@ func Start() {
 		if err != nil {
 			return
 		}
-		// 先校验回调是否合法
-		clientId, checkOk := public.CheckRequestWithCredentials(c.GetHeader("timestamp"), c.GetHeader("sign"))
-		if !checkOk {
-			logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!")
-			return
-		}
-		// 通过 context 传递 OAuth ClientID,用于后续流程中调用钉钉OpenAPI
-		c.Set(public.DingTalkClientIdKeyName, clientId)
-		// 为了兼容存量老用户,暂时保留 public.CheckRequest 方法,将来升级到 Stream 模式后,建议去除该方法,采用上面的 CheckRequestWithCredentials
-		if !public.CheckRequest(c.GetHeader("timestamp"), c.GetHeader("sign")) && msgObj.SenderStaffId != "" {
-			logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!")
-			return
-		} else if !public.JudgeOutgoingGroup(msgObj.ConversationID) && msgObj.SenderStaffId == "" {
-			logger.Warning("该请求不合法,可能是未经允许的普通群outgoing机器人调用所致,请知悉!")
-			return
-		}
-		// 再校验回调参数是否有价值
-		if msgObj.Text.Content == "" || msgObj.ChatbotUserID == "" {
-			logger.Warning("从钉钉回调过来的内容为空,根据过往的经验,或许重新创建一下机器人,能解决这个问题")
-			return
-		}
-		// 去除问题的前后空格
-		msgObj.Text.Content = strings.TrimSpace(msgObj.Text.Content)
-		if public.JudgeSensitiveWord(msgObj.Text.Content) {
-			logger.Info(fmt.Sprintf("🙋 %s提问的问题中包含敏感词汇,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content))
-			_, err = msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您提问的问题中包含敏感词汇,请审核自己的对话内容之后再进行!**")
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-			return
-		}
-		// 打印钉钉回调过来的请求明细,调试时打开
-		logger.Debug(fmt.Sprintf("dingtalk callback parameters: %#v", msgObj))
-
-		if public.Config.ChatType != "0" && msgObj.ConversationType != public.Config.ChatType {
-			logger.Info(fmt.Sprintf("🙋 %s使用了禁用的聊天方式", msgObj.SenderNick))
-			_, err = msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,管理员禁用了这种聊天方式,请选择其他聊天方式与机器人对话!**")
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-			return
-		}
-
-		// 查询群ID,发送指令后,可通过查看日志来获取
-		if msgObj.ConversationType == "2" && msgObj.Text.Content == "群ID" {
-			if msgObj.RobotCode == "normal" {
-				logger.Info(fmt.Sprintf("🙋 outgoing机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID))
-			} else {
-				logger.Info(fmt.Sprintf("🙋 企业内部机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID))
-			}
-			//_, err = msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), msgObj.ConversationID)
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-			return
-		}
-
-		// 不在允许群组,不在允许用户(包括在黑名单),满足任一条件,拒绝会话;管理员不受限制
-		if msgObj.ConversationType == "2" && !public.JudgeGroup(msgObj.ConversationID) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" {
-			logger.Info(fmt.Sprintf("🙋『%s』群组未被验证通过,群ID: %#v,userid:%#v, 昵称: %#v,消息: %#v", msgObj.ConversationTitle, msgObj.ConversationID, msgObj.SenderStaffId, msgObj.SenderNick, msgObj.Text.Content))
-			_, err = msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,该群组未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。")
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-			return
-		} else if !public.JudgeUsers(msgObj.SenderStaffId) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" {
-			logger.Info(fmt.Sprintf("🙋 %s身份信息未被验证通过,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content))
-			_, err = msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您的身份信息未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。")
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-			return
-		}
-		if len(msgObj.Text.Content) == 0 || msgObj.Text.Content == "帮助" {
-			// 欢迎信息
-			_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), public.Config.Help)
-			if err != nil {
-				logger.Warning(fmt.Errorf("send message error: %v", err))
-				return
-			}
-		} else {
-			logger.Info(fmt.Sprintf("🙋 %s发起的问题: %#v", msgObj.SenderNick, msgObj.Text.Content))
-			// 除去帮助之外的逻辑分流在这里处理
-			switch {
-			case strings.HasPrefix(msgObj.Text.Content, "#图片"):
-				err := process.ImageGenerate(c, &msgObj)
-				if err != nil {
-					logger.Warning(fmt.Errorf("process request: %v", err))
-					return
-				}
-				return
-			case strings.HasPrefix(msgObj.Text.Content, "#查对话"):
-				err := process.SelectHistory(&msgObj)
-				if err != nil {
-					logger.Warning(fmt.Errorf("process request: %v", err))
-					return
-				}
-				return
-			case strings.HasPrefix(msgObj.Text.Content, "#域名"):
-				err := process.DomainMsg(&msgObj)
-				if err != nil {
-					logger.Warning(fmt.Errorf("process request: %v", err))
-					return
-				}
-				return
-			case strings.HasPrefix(msgObj.Text.Content, "#证书"):
-				err := process.DomainCertMsg(&msgObj)
-				if err != nil {
-					logger.Warning(fmt.Errorf("process request: %v", err))
-					return
-				}
-				return
-			default:
-				msgObj.Text.Content, err = process.GeneratePrompt(msgObj.Text.Content)
-				// err不为空:提示词之后没有文本 -> 直接返回提示词所代表的内容
-				if err != nil {
-					_, err = msgObj.ReplyToDingtalk(string(dingbot.TEXT), msgObj.Text.Content)
-					if err != nil {
-						logger.Warning(fmt.Errorf("send message error: %v", err))
-						return
-					}
-					return
-				}
-				err := process.ProcessRequest(&msgObj)
-				if err != nil {
-					logger.Warning(fmt.Errorf("process request: %v", err))
-					return
-				}
-				return
-			}
-		}
+		DoRequest(msgObj, c)
 	})
 	// 解析生成后的图片
 	app.GET("/images/:filename", func(c *gin.Context) {
@@ -227,3 +150,145 @@ func Start() {
 	}
 	logger.Info("Server exiting!")
 }
+
+func DoRequest(msgObj dingbot.ReceiveMsg, c *gin.Context) {
+	// 先校验回调是否合法
+	if public.Config.RunMode == "http" {
+		clientId, checkOk := public.CheckRequestWithCredentials(c.GetHeader("timestamp"), c.GetHeader("sign"))
+		if !checkOk {
+			logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!")
+			return
+		}
+		// 通过 context 传递 OAuth ClientID,用于后续流程中调用钉钉OpenAPI
+		c.Set(public.DingTalkClientIdKeyName, clientId)
+	}
+	// 为了兼容存量老用户,暂时保留 public.CheckRequest 方法,将来升级到 Stream 模式后,建议去除该方法,采用上面的 CheckRequestWithCredentials
+	// if !public.CheckRequest(c.GetHeader("timestamp"), c.GetHeader("sign")) && msgObj.SenderStaffId != "" {
+	// 	logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!")
+	// 	return
+	// } else if !public.JudgeOutgoingGroup(msgObj.ConversationID) && msgObj.SenderStaffId == "" {
+	// 	logger.Warning("该请求不合法,可能是未经允许的普通群outgoing机器人调用所致,请知悉!")
+	// 	return
+	// }
+	// 再校验回调参数是否有价值
+	if msgObj.Text.Content == "" || msgObj.ChatbotUserID == "" {
+		logger.Warning("从钉钉回调过来的内容为空,根据过往的经验,或许重新创建一下机器人,能解决这个问题")
+		return
+	}
+	// 去除问题的前后空格
+	msgObj.Text.Content = strings.TrimSpace(msgObj.Text.Content)
+	if public.JudgeSensitiveWord(msgObj.Text.Content) {
+		logger.Info(fmt.Sprintf("🙋 %s提问的问题中包含敏感词汇,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content))
+		_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您提问的问题中包含敏感词汇,请审核自己的对话内容之后再进行!**")
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+			return
+		}
+		return
+	}
+	// 打印钉钉回调过来的请求明细,调试时打开
+	logger.Debug(fmt.Sprintf("dingtalk callback parameters: %#v", msgObj))
+
+	if public.Config.ChatType != "0" && msgObj.ConversationType != public.Config.ChatType {
+		logger.Info(fmt.Sprintf("🙋 %s使用了禁用的聊天方式", msgObj.SenderNick))
+		_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,管理员禁用了这种聊天方式,请选择其他聊天方式与机器人对话!**")
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+			return
+		}
+		return
+	}
+
+	// 查询群ID,发送指令后,可通过查看日志来获取
+	if msgObj.ConversationType == "2" && msgObj.Text.Content == "群ID" {
+		if msgObj.RobotCode == "normal" {
+			logger.Info(fmt.Sprintf("🙋 outgoing机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID))
+		} else {
+			logger.Info(fmt.Sprintf("🙋 企业内部机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID))
+		}
+		// _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), msgObj.ConversationID)
+		// if err != nil {
+		// 	logger.Warning(fmt.Errorf("send message error: %v", err))
+		// 	return
+		// }
+		return
+	}
+
+	// 不在允许群组,不在允许用户(包括在黑名单),满足任一条件,拒绝会话;管理员不受限制
+	if msgObj.ConversationType == "2" && !public.JudgeGroup(msgObj.ConversationID) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" {
+		logger.Info(fmt.Sprintf("🙋『%s』群组未被验证通过,群ID: %#v,userid:%#v, 昵称: %#v,消息: %#v", msgObj.ConversationTitle, msgObj.ConversationID, msgObj.SenderStaffId, msgObj.SenderNick, msgObj.Text.Content))
+		_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,该群组未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。")
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+			return
+		}
+		return
+	} else if !public.JudgeUsers(msgObj.SenderStaffId) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" {
+		logger.Info(fmt.Sprintf("🙋 %s身份信息未被验证通过,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content))
+		_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您的身份信息未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。")
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+			return
+		}
+		return
+	}
+	if len(msgObj.Text.Content) == 0 || msgObj.Text.Content == "帮助" {
+		// 欢迎信息
+		_, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), public.Config.Help)
+		if err != nil {
+			logger.Warning(fmt.Errorf("send message error: %v", err))
+			return
+		}
+	} else {
+		logger.Info(fmt.Sprintf("🙋 %s发起的问题: %#v", msgObj.SenderNick, msgObj.Text.Content))
+		// 除去帮助之外的逻辑分流在这里处理
+		switch {
+		case strings.HasPrefix(msgObj.Text.Content, "#图片"):
+			err := process.ImageGenerate(c, &msgObj)
+			if err != nil {
+				logger.Warning(fmt.Errorf("process request: %v", err))
+				return
+			}
+			return
+		case strings.HasPrefix(msgObj.Text.Content, "#查对话"):
+			err := process.SelectHistory(&msgObj)
+			if err != nil {
+				logger.Warning(fmt.Errorf("process request: %v", err))
+				return
+			}
+			return
+		case strings.HasPrefix(msgObj.Text.Content, "#域名"):
+			err := process.DomainMsg(&msgObj)
+			if err != nil {
+				logger.Warning(fmt.Errorf("process request: %v", err))
+				return
+			}
+			return
+		case strings.HasPrefix(msgObj.Text.Content, "#证书"):
+			err := process.DomainCertMsg(&msgObj)
+			if err != nil {
+				logger.Warning(fmt.Errorf("process request: %v", err))
+				return
+			}
+			return
+		default:
+			var err error
+			msgObj.Text.Content, err = process.GeneratePrompt(msgObj.Text.Content)
+			// err不为空:提示词之后没有文本 -> 直接返回提示词所代表的内容
+			if err != nil {
+				_, err = msgObj.ReplyToDingtalk(string(dingbot.TEXT), msgObj.Text.Content)
+				if err != nil {
+					logger.Warning(fmt.Errorf("send message error: %v", err))
+					return
+				}
+				return
+			}
+			err = process.ProcessRequest(&msgObj)
+			if err != nil {
+				logger.Warning(fmt.Errorf("process request: %v", err))
+				return
+			}
+			return
+		}
+	}
+}

+ 84 - 0
public/example_bot.go

@@ -0,0 +1,84 @@
+package public
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/logger"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/payload"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/utils"
+)
+
+/**
+ * @Author linya.jj
+ * @Date 2023/3/22 18:30
+ */
+
+func OnBotCallback(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) {
+	frameResp := &payload.DataFrameResponse{
+		Code: 200,
+		Headers: payload.DataFrameHeader{
+			payload.DataFrameHeaderKContentType: payload.DataFrameContentTypeKJson,
+			payload.DataFrameHeaderKMessageId:   df.GetMessageId(),
+		},
+		Message: "ok",
+		Data:    "",
+	}
+
+	return frameResp, nil
+}
+
+func OnChatReceive(ctx context.Context, data *chatbot.BotCallbackDataModel) error {
+	requestBody := map[string]interface{}{
+		"msgtype": "text",
+		"text": map[string]interface{}{
+			"content": fmt.Sprintf("msg received: [%s]", data.Text.Content),
+		},
+	}
+
+	requestJsonBody, _ := json.Marshal(requestBody)
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, data.SessionWebhook, bytes.NewReader(requestJsonBody))
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "*/*")
+
+	httpClient := &http.Client{
+		Transport: http.DefaultTransport,
+		Timeout:   5 * time.Second, //设置超时,包含connection时间、任意重定向时间、读取response body时间
+	}
+
+	_, err = httpClient.Do(req)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func RunBotListener(clientId, clientSecret string) {
+	logger.SetLogger(logger.NewStdTestLogger())
+
+	cli := client.NewStreamClient(
+		client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)),
+		client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()),
+		client.WithSubscription(utils.SubscriptionTypeKCallback, payload.BotMessageCallbackTopic, chatbot.NewDefaultChatBotFrameHandler(OnChatReceive).OnEventReceived),
+	)
+
+	err := cli.Start(context.Background())
+	if err != nil {
+		panic(err)
+	}
+
+	defer cli.Close()
+
+	select {}
+}

+ 36 - 0
public/example_event.go

@@ -0,0 +1,36 @@
+package public
+
+import (
+	"context"
+
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/event"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/logger"
+	"github.com/open-dingtalk/dingtalk-stream-sdk-go/utils"
+)
+
+/**
+ * @Author linya.jj
+ * @Date 2023/3/22 18:30
+ */
+
+func RunEventListener(clientId, clientSecret string) {
+	logger.SetLogger(logger.NewStdTestLogger())
+
+	eventHandler := event.NewDefaultEventFrameHandler(event.EventHandlerDoNothing)
+
+	cli := client.NewStreamClient(
+		client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)),
+		client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()),
+		client.WithSubscription(utils.SubscriptionTypeKEvent, "*", eventHandler.OnEventReceived),
+	)
+
+	err := cli.Start(context.Background())
+	if err != nil {
+		panic(err)
+	}
+
+	defer cli.Close()
+
+	select {}
+}