├── .env_template
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── .idea
├── .gitignore
├── ding.iml
├── modules.xml
└── vcs.xml
├── Dockerfile
├── README.md
├── bot
├── cozebot
│ └── coze.go
├── difybot
│ └── dify.go
└── dingtalk
│ ├── ding.go
│ └── dingMessage.go
├── clients
└── DingTalkStreamClient.go
├── conf
└── conf.go
├── consts
├── ding.go
├── img.png
├── imgStream.png
└── imgStream2.png
├── docker-compose.yml
├── go.mod
├── go.sum
├── handlers
├── dify.go
├── ding.go
└── test.go
├── main.go
├── middlewares
└── logs.go
├── models
└── baiduaodio.go
├── utils
├── channelManager.go
└── stringUtils.go
└── voices
├── baidu.go
└── xunfei.go
/.env_template:
--------------------------------------------------------------------------------
1 | API_KEY=your_api_key_here
2 | API_URL=https://api.example.com/endpoint
3 | CLIENT_ID=your_client_id_here
4 | CLIENT_SECRET=your_client_secret_here
5 | Ding_Topic=/v1.0/im/bot/messages/get
6 | Output_Type=Stream
7 | REDIS_ADDR=localhost:6379
8 | REDIS_PASSWORD=your_redis_password
9 | VOICE_KEYWORDS=你好
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Server
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: '1.17' # 根据你的 Go 版本调整
20 |
21 | - name: Build Docker image
22 | run: |
23 | docker build -t your-dockerhub-username/your-app-name:latest .
24 |
25 | - name: Log in to Docker Hub
26 | run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin
27 |
28 | - name: Push Docker image
29 | run: docker push your-dockerhub-username/your-app-name:latest
30 |
31 | deploy:
32 | runs-on: ubuntu-latest
33 | needs: build
34 |
35 | steps:
36 | - name: Deploy to server
37 | run: |
38 | ssh user@your-server-ip << 'EOF'
39 | docker pull your-dockerhub-username/your-app-name:latest
40 | docker stop your-container-name || true
41 | docker rm your-container-name || true
42 | docker run -d --name your-container-name -p 80:80 your-dockerhub-username/your-app-name:latest
43 | EOF
44 | env:
45 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | tmp
3 | .github
4 | consts/private
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/ding.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official Golang image as a base image
2 | FROM golang:1.21-alpine
3 |
4 |
5 | ENV GOPROXY=https://goproxy.cn,direct
6 |
7 | # Set the Current Working Directory inside the container
8 | WORKDIR /app
9 |
10 | # Copy go mod and sum files
11 | COPY go.mod go.sum ./
12 |
13 | # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
14 | RUN go mod download
15 |
16 | # Copy the source code into the container
17 | COPY . .
18 |
19 | # Build the Go app
20 | RUN go build -o main .
21 |
22 | # Expose port 8080 to the outside world
23 | EXPOSE 7777
24 |
25 | # Command to run the executable
26 | CMD ["./main"]
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dify-on-Dingding-go
2 | 实现钉钉机器人接入dify工作流,完成ai问答
3 |
4 | dify官网: https://dify.ai/
5 |
6 |
7 | 目前功能:
8 | 1. 钉钉机器人接收文本、语音、表情包、图片消息
9 | 2. 钉钉机器人发送 纯文本、Markdown、流式卡片
10 | 3. 实时接收dify流, 使用钉钉机器人发送到聊天会话中
11 |
12 | 支持docker 部署
13 |
14 | # 配置使用
15 |
16 | 1. 修改.env_template文件 为.env
17 | 2. 设置.env文件内的环境变量
18 |
19 |
20 | API_KEY: dify的api_key 要改
21 |
22 | API_URL: dify 的api接口 要改
23 |
24 | CLIENT_ID : 钉钉机器人应用的id 要改
25 |
26 | CLIENT_SECRET:钉钉机器人应用的secret 要改
27 |
28 | Ding_Topic=/v1.0/im/bot/messages/get 默认的钉钉topic 不用改
29 |
30 | Output_Type:Stream 机器人输出内容模式, Text为文本, Stream为流输出,Markdown为Markdown格式输出
31 |
32 |
33 | # 部署
34 |
35 | ## docker compose部署 (*推荐*)
36 | 配置好上面的变量后,在项目目录下执行下面命令,即可与机器人对话
37 | docker-compose up -d
38 |
39 | ## 本地部署
40 | 需要配go的环境和安装redis,然后编译执行main.go
41 | ## Markdown模式
42 |
43 | 
44 |
45 | ## 流输出模式
46 | 
47 |
48 | 
49 |
--------------------------------------------------------------------------------
/bot/cozebot/coze.go:
--------------------------------------------------------------------------------
1 | package cozebot
2 |
--------------------------------------------------------------------------------
/bot/difybot/dify.go:
--------------------------------------------------------------------------------
1 | package difybot
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "ding/utils"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "github.com/go-redis/redis/v8"
11 | "io"
12 | "io/ioutil"
13 | "net/http"
14 | "os"
15 | "strings"
16 | "sync"
17 | "time"
18 | )
19 |
20 | type difySession struct {
21 | ConversationID string
22 | Expiry time.Time
23 | }
24 |
25 | type difyClient struct {
26 | ApiBase string
27 | DifyApiKey string
28 | RedisClient *redis.Client // Redis客户端
29 | mu sync.Mutex // 保护 Sessions 免受并发访问问题
30 | }
31 |
32 | var DifyClient difyClient
33 |
34 | func InitDifyClient() {
35 | API_KEY := os.Getenv("API_KEY")
36 | API_URL := os.Getenv("API_URL")
37 | REDIS_ADDR := os.Getenv("REDIS_ADDR")
38 | REDIS_PASSWORD := os.Getenv("REDIS_PASSWORD")
39 | DifyClient = difyClient{
40 | ApiBase: API_URL,
41 | DifyApiKey: API_KEY,
42 | RedisClient: redis.NewClient(&redis.Options{
43 | Addr: REDIS_ADDR,
44 | Password: REDIS_PASSWORD,
45 | DB: 0, // 使用默认数据库
46 | }),
47 | }
48 | // 检查Redis连接
49 | ctx := context.Background()
50 | _, err := DifyClient.RedisClient.Ping(ctx).Result()
51 | if err != nil {
52 | fmt.Println("Error connecting to Redis:", err)
53 | os.Exit(1)
54 | }
55 |
56 | // 清空所有以 $:LWCP_v1 开头的键
57 | var cursor uint64
58 | var n int
59 | for {
60 | var keys []string
61 | var err error
62 | keys, cursor, err = DifyClient.RedisClient.Scan(ctx, cursor, "$:LWCP_v1*", 10).Result()
63 | if err != nil {
64 | fmt.Println("Error scanning keys:", err)
65 | os.Exit(1)
66 | }
67 |
68 | if len(keys) > 0 {
69 | n += len(keys)
70 | if _, err := DifyClient.RedisClient.Del(ctx, keys...).Result(); err != nil {
71 | fmt.Println("Error deleting keys:", err)
72 | os.Exit(1)
73 | }
74 | }
75 |
76 | if cursor == 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("Deleted %d keys\n", n)
82 |
83 | }
84 |
85 | type RequestBody struct {
86 | Inputs map[string]interface{} `json:"inputs"`
87 | Query string `json:"query"`
88 | ResponseMode string `json:"response_mode"`
89 | ConversationID string `json:"conversation_id,omitempty"`
90 | User string `json:"user,omitempty"`
91 | }
92 |
93 | type ApiResponse struct {
94 | Event string `json:"event"`
95 | TaskID string `json:"task_id"`
96 | ID string `json:"id"`
97 | MessageID string `json:"message_id"`
98 | ConversationID string `json:"conversation_id"`
99 | Mode string `json:"mode"`
100 | Answer string `json:"answer"`
101 | Metadata map[string]interface{} `json:"metadata"`
102 | CreatedAt int64 `json:"created_at"`
103 | }
104 |
105 | type StreamingEvent struct {
106 | Event string `json:"event"`
107 | TaskID string `json:"task_id,omitempty"`
108 | WorkflowRunID string `json:"workflow_run_id,omitempty"`
109 | MessageID string `json:"message_id,omitempty"`
110 | ConversationID string `json:"conversation_id,omitempty"`
111 | ID string `json:"id,omitempty"`
112 | Data map[string]interface{} `json:"data,omitempty"`
113 | Metadata map[string]interface{} `json:"metadata,omitempty"`
114 | Answer string `json:"answer,omitempty"`
115 | CreatedAt int64 `json:"created_at,omitempty"`
116 | FinishedAt int64 `json:"finished_at,omitempty"`
117 | }
118 |
119 | // 添加会话
120 | func (client *difyClient) AddSession(userID, conversationID string) {
121 | client.mu.Lock()
122 | defer client.mu.Unlock()
123 |
124 | session := difySession{
125 | ConversationID: conversationID,
126 | Expiry: time.Now().Add(30 * time.Minute),
127 | }
128 |
129 | // 使用Redis存储会话
130 | ctx := context.Background()
131 | sessionData, err := json.Marshal(session)
132 | if err != nil {
133 | fmt.Println("Error marshalling session data:", err)
134 | return
135 | }
136 |
137 | err = client.RedisClient.Set(ctx, userID, sessionData, 30*time.Minute).Err()
138 | if err != nil {
139 | fmt.Println("Error setting session data in Redis:", err)
140 | }
141 |
142 | }
143 |
144 | // 获取会话
145 | func (client *difyClient) GetSession(userID string) (string, bool) {
146 | client.mu.Lock()
147 | defer client.mu.Unlock()
148 |
149 | // 从Redis获取会话
150 | ctx := context.Background()
151 | sessionData, err := client.RedisClient.Get(ctx, userID).Result()
152 | if err == redis.Nil {
153 | // 会话不存在
154 | return "", false
155 | } else if err != nil {
156 | fmt.Println("Error getting session data from Redis:", err)
157 | return "", false
158 | }
159 |
160 | var session difySession
161 | err = json.Unmarshal([]byte(sessionData), &session)
162 | if err != nil {
163 | fmt.Println("Error unmarshalling session data:", err)
164 | return "", false
165 | }
166 |
167 | if time.Now().After(session.Expiry) {
168 | // 会话已过期
169 | client.RedisClient.Del(ctx, userID)
170 | return "", false
171 | }
172 | return session.ConversationID, true
173 |
174 | }
175 |
176 | func (client *difyClient) CallAPIBlock(query, conversationID, userID string) (string, error) {
177 |
178 | // 构建请求体
179 | requestBody := RequestBody{
180 | Inputs: make(map[string]interface{}),
181 | Query: query,
182 | ResponseMode: "blocking",
183 | ConversationID: conversationID,
184 | User: userID,
185 | }
186 |
187 | // 将请求体转换为JSON
188 | jsonData, err := json.Marshal(requestBody)
189 | if err != nil {
190 | return "", err
191 | }
192 |
193 | // 创建HTTP请求
194 | req, err := http.NewRequest("POST", client.ApiBase+"/chat-messages", bytes.NewBuffer(jsonData))
195 | if err != nil {
196 | return "", err
197 | }
198 |
199 | // 设置请求头
200 | req.Header.Set("Content-Type", "application/json")
201 | req.Header.Set("Authorization", "Bearer "+client.DifyApiKey)
202 |
203 | // 发送请求
204 | clientHTTP := &http.Client{}
205 | resp, err := clientHTTP.Do(req)
206 | if err != nil {
207 | return "", err
208 | }
209 | defer resp.Body.Close()
210 |
211 | // 读取响应
212 | body, err := io.ReadAll(resp.Body)
213 | if err != nil {
214 | return "", err
215 | }
216 |
217 | // 检查响应状态码
218 | if resp.StatusCode != http.StatusOK {
219 | return "", fmt.Errorf("API request failed with status: %d, response: %s", resp.StatusCode, string(body))
220 | }
221 | var response ApiResponse
222 | err = json.Unmarshal(body, &response)
223 | if err != nil {
224 | fmt.Println("【CallAPI】转换异常", err)
225 | return "", err
226 | }
227 | client.AddSession(userID, response.ConversationID)
228 | return response.Answer, nil
229 | }
230 |
231 | func (client *difyClient) CallAPIStreaming(query, userID string, conversationID string, permission int) (*http.Response, error) {
232 |
233 | // 初始化客户端
234 | clientHttp := &http.Client{}
235 | // 构建请求体
236 | requestBody := RequestBody{
237 | Inputs: make(map[string]interface{}),
238 | Query: query,
239 | ResponseMode: "streaming",
240 | ConversationID: conversationID,
241 | User: userID,
242 | }
243 |
244 | // 将请求体转换为JSON
245 | jsonData, err := json.Marshal(requestBody)
246 | if err != nil {
247 | return nil, err
248 | }
249 | // 创建请求
250 | req, err := http.NewRequest("POST", client.ApiBase+"/chat-messages", bytes.NewBuffer(jsonData))
251 | if err != nil {
252 | fmt.Println("Error creating request:", err)
253 | return nil, err
254 | }
255 |
256 | // 设置必要的请求头
257 | req.Header.Set("Content-Type", "application/json")
258 |
259 | req.Header.Set("Authorization", "Bearer "+client.DifyApiKey)
260 |
261 | // 发送请求
262 | resp, err := clientHttp.Do(req)
263 | if err != nil {
264 | fmt.Println("Error sending request:", err)
265 | return nil, err
266 | }
267 | //defer resp.Body.Close()
268 |
269 | // 检查响应状态码
270 | if resp.StatusCode != http.StatusOK {
271 | // 读取响应体
272 | bodyBytes, err := ioutil.ReadAll(resp.Body)
273 | if err != nil {
274 | fmt.Println("Error reading response body:", err)
275 | return nil, err
276 | }
277 | bodyString := string(bodyBytes)
278 |
279 | // 打印错误信息
280 | fmt.Printf("Error: received non-200 response code: %d\n", resp.StatusCode)
281 | fmt.Printf("Response body: %s\n", bodyString)
282 | return nil, errors.New("Error: received non-200 response code")
283 | }
284 |
285 | return resp, nil
286 |
287 | }
288 | func (client *difyClient) ProcessEvent(userID string, event StreamingEvent, answerBuilder *strings.Builder, cm *utils.ChannelManager) error {
289 | //println(event.Event)
290 | switch event.Event {
291 | case "message":
292 | {
293 | answerBuilder.WriteString(event.Answer)
294 | select {
295 | case cm.DataCh <- answerBuilder.String():
296 | time.Sleep(10)
297 | default:
298 | }
299 | }
300 | case "agent_message":
301 | {
302 | answerBuilder.WriteString(event.Answer)
303 | select {
304 | case cm.DataCh <- answerBuilder.String():
305 | time.Sleep(10)
306 | default:
307 | }
308 | }
309 | case "message_end":
310 | {
311 | // 发送停止信号
312 | cm.CloseChannel()
313 | client.AddSession(userID, event.ConversationID)
314 | }
315 | case "message_replace":
316 | {
317 |
318 | }
319 | case "error":
320 | {
321 | // 发送停止信号
322 | cm.CloseChannel()
323 | return errors.New("dify err")
324 | }
325 | case "workflow_started":
326 | {
327 |
328 | }
329 | case "workflow_finished":
330 | {
331 |
332 | }
333 | case "node_started":
334 | {
335 |
336 | }
337 | case "node_finished":
338 | {
339 |
340 | }
341 |
342 | }
343 | return nil
344 |
345 | }
346 |
--------------------------------------------------------------------------------
/bot/dingtalk/ding.go:
--------------------------------------------------------------------------------
1 | package dingbot
2 |
3 | import (
4 | "context"
5 | "ding/bot/difybot"
6 | "ding/clients"
7 | "ding/consts"
8 | selfutils "ding/utils"
9 | "encoding/json"
10 | "fmt"
11 | dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
12 | "github.com/alibabacloud-go/dingtalk/robot_1_0"
13 | "github.com/alibabacloud-go/tea/tea"
14 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
15 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
16 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger"
17 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils"
18 | "os"
19 | "strings"
20 | "time"
21 | )
22 |
23 | // 初始化钉钉机器人
24 | func StartDingRobot() {
25 |
26 | DingVarInit()
27 | defer DingChannelDestory()
28 |
29 | logger.SetLogger(logger.NewStdTestLogger())
30 | clientId := os.Getenv("CLIENT_ID")
31 | clientSecret := os.Getenv("CLIENT_SECRET")
32 | topic := os.Getenv("Ding_Topic")
33 | cli := &client.StreamClient{}
34 | if os.Getenv("Output_Type") == consts.OutputTypeText {
35 | //纯文本或markdown输出
36 | cli = client.NewStreamClient(
37 | client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)),
38 | client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()),
39 | client.WithSubscription(utils.SubscriptionTypeKCallback, topic, chatbot.NewDefaultChatBotFrameHandler(OnChatReceiveText).OnEventReceived),
40 | )
41 | } else if os.Getenv("Output_Type") == consts.OutputTypeStream {
42 | clients.DingTalkStreamClientInit()
43 | // 流式输出
44 | cli = client.NewStreamClient(
45 | client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
46 | cli.RegisterChatBotCallbackRouter(OnChatBotStreamingMessageReceived)
47 | } else if os.Getenv("Output_Type") == consts.OutputTypeMarkDown {
48 | clients.DingTalkStreamClientInit()
49 | // 流式输出
50 | cli = client.NewStreamClient(
51 | client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
52 | cli.RegisterChatBotCallbackRouter(OnChatReceiveMarkDown)
53 | }
54 | err := cli.Start(context.Background())
55 | if err != nil {
56 | panic(err)
57 | }
58 |
59 | defer cli.Close()
60 |
61 | select {}
62 | }
63 |
64 | func OnChatReceiveText(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
65 | replyMsgStr := strings.TrimSpace(data.Text.Content)
66 | replier := chatbot.NewChatbotReplier()
67 |
68 | conversationID, exists := difybot.DifyClient.GetSession(data.SenderId)
69 | if exists {
70 | fmt.Println("Conversation ID for user:", data.SenderId, "is", conversationID)
71 | } else {
72 | conversationID = ""
73 | fmt.Println("No conversation ID found for user:", data.SenderId)
74 | }
75 |
76 | res, err := difybot.DifyClient.CallAPIBlock(replyMsgStr, conversationID, data.SenderId)
77 | if err != nil {
78 | fmt.Println(err)
79 | return nil, err
80 | }
81 | fmt.Println(res)
82 |
83 | if err := replier.SimpleReplyText(ctx, data.SessionWebhook, []byte(res)); err != nil {
84 | return nil, err
85 | }
86 | return []byte(""), nil
87 |
88 | }
89 |
90 | func OnChatBotStreamingMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
91 | // create an uniq card id to identify a card instance while updating
92 | // see: https://open.dingtalk.com/document/orgapp/robots-send-interactive-cards (cardBizId)
93 | // 数据过滤
94 | replier := chatbot.NewChatbotReplier()
95 | permission := 0
96 | if !selfutils.StringInSlice(data.Msgtype, dingSupportType) {
97 | res := "不支持的消息格式"
98 | if err := replier.SimpleReplyText(ctx, data.SessionWebhook, []byte(res)); err != nil {
99 | return nil, err
100 | }
101 | return nil, nil
102 | }
103 | if data.ConversationType == "2" {
104 | // group chat; 群聊
105 | fmt.Println("钉钉接收群消息:")
106 | } else {
107 | fmt.Println("钉钉接收私聊消息:")
108 | }
109 |
110 | receivedMsgStr := ""
111 | imageCodeList := []string{}
112 | imageUrlList := []string{}
113 | //robotClient := robot_1_0.Client{}
114 |
115 | switch data.Msgtype {
116 | case consts.ReceivedTypeText:
117 |
118 | receivedMsgStr = strings.TrimSpace(data.Text.Content)
119 | fmt.Printf("[DingTalk]receive text msg: %s\n", receivedMsgStr)
120 | case consts.ReceivedTypeVoice:
121 | fmt.Printf("[DingTalk]receive voice msg: %s\n", data.Content)
122 | for key, value := range data.Content.(map[string]interface{}) {
123 | if key == "recognition" {
124 | recognitionText := value.(string)
125 | fmt.Println(recognitionText)
126 | //data.Text.Content = recognitionText
127 | //if !selfutils.ContainsKeywords(recognitionText, consts.VoicePrefix) {
128 | // return []byte(""), nil
129 | //}
130 | receivedMsgStr = recognitionText
131 |
132 | }
133 | }
134 | case consts.ReceivedTypeImage:
135 | fmt.Printf("[DingTalk]receive image msg: %s", receivedMsgStr)
136 | for key, value := range data.Content.(map[string]interface{}) {
137 | if key == "downloadCode" {
138 | downloadCode := value.(string)
139 | fmt.Println(downloadCode)
140 | imageCodeList = append(imageCodeList, downloadCode)
141 | // 请求图片Url链接
142 | DownloadReq := robot_1_0.RobotMessageFileDownloadRequest{
143 | DownloadCode: &downloadCode,
144 | }
145 | download, err := clients.DingtalkClient1.RobotMessageFileDownload(&DownloadReq)
146 | if err != nil {
147 | return nil, err
148 | }
149 | fmt.Println(*download.Body.DownloadUrl)
150 | if download.Body.DownloadUrl != nil {
151 | imageUrlList = append(imageUrlList, *download.Body.DownloadUrl)
152 | }
153 | }
154 | }
155 |
156 | }
157 | // 将消息放入队列
158 | messageQueue <- &DingMessage{
159 | Ctx: ctx,
160 | Data: data,
161 | MsgType: data.Msgtype,
162 | Permission: permission,
163 | ReceivedMsgStr: receivedMsgStr,
164 | IsGroup: data.ConversationType == "2",
165 | ImageCodeList: imageCodeList,
166 | ImageUrlList: imageUrlList,
167 | }
168 |
169 | return []byte(""), nil
170 | }
171 |
172 | func OnChatReceiveMarkDown(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
173 |
174 | replyMsgStr := strings.TrimSpace(data.Text.Content)
175 | replier := chatbot.NewChatbotReplier()
176 |
177 | conversationID, exists := difybot.DifyClient.GetSession(data.SenderId)
178 | if exists {
179 | fmt.Println("Conversation ID for user:", data.SenderId, "is", conversationID)
180 | } else {
181 | conversationID = ""
182 | fmt.Println("No conversation ID found for user:", data.SenderId)
183 | }
184 |
185 | res, err := difybot.DifyClient.CallAPIBlock(replyMsgStr, conversationID, data.SenderId)
186 | if err != nil {
187 | fmt.Println(err)
188 | return nil, err
189 | }
190 | fmt.Println(res)
191 | if err := replier.SimpleReplyMarkdown(ctx, data.SessionWebhook, []byte(""), []byte(res)); err != nil {
192 | return nil, err
193 | }
194 |
195 | return []byte(""), nil
196 |
197 | }
198 |
199 | func UpdateDingTalkCard(cardData string, cardInstanceId string) error {
200 | //fmt.Println("发送内容:", content)
201 |
202 | timeStart := time.Now()
203 | updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
204 | CardBizId: tea.String(cardInstanceId),
205 | CardData: tea.String(cardData),
206 | }
207 | _, err := clients.DingtalkClient1.UpdateInteractiveCard(updateRequest)
208 | if err != nil {
209 | return err
210 | }
211 | elapsed := time.Since(timeStart)
212 | fmt.Printf("updateDingTalkCard 执行时间: %s\n", elapsed)
213 | return nil
214 | }
215 | func sendInteractiveCard(cardInstanceId string, msg *DingMessage) {
216 | // send interactive card; 发送交互式卡片
217 | cardData := fmt.Sprintf(consts.MessageCardTemplateWithTitle1, "")
218 | sendOptions := &dingtalkim_1_0.SendRobotInteractiveCardRequestSendOptions{}
219 | request := &dingtalkim_1_0.SendRobotInteractiveCardRequest{
220 | CardTemplateId: tea.String("StandardCard"),
221 | CardBizId: tea.String(cardInstanceId),
222 | CardData: tea.String(cardData),
223 | RobotCode: tea.String(clients.DingtalkClient1.ClientID),
224 | SendOptions: sendOptions,
225 | PullStrategy: tea.Bool(false),
226 | }
227 | if msg.IsGroup {
228 | // group chat; 群聊
229 | fmt.Println("钉钉接收群消息:", msg.Data.Text.Content)
230 | request.SetOpenConversationId(msg.Data.ConversationId)
231 |
232 | } else {
233 | // ConversationType == "1": private chat; 单聊
234 | fmt.Println("钉钉接收私聊消息:", msg.Data.Text.Content)
235 | receiverBytes, err := json.Marshal(map[string]string{"userId": msg.Data.SenderStaffId})
236 | if err != nil {
237 | fmt.Println("私聊序列化失败")
238 | return
239 | }
240 | request.SetSingleChatReceiver(string(receiverBytes))
241 | }
242 | _, err := clients.DingtalkClient1.SendInteractiveCard(request)
243 | if err != nil {
244 | fmt.Println("发送卡片失败")
245 | return
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/bot/dingtalk/dingMessage.go:
--------------------------------------------------------------------------------
1 | package dingbot
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "ding/bot/difybot"
7 | "ding/consts"
8 | selfutils "ding/utils"
9 | "encoding/json"
10 | "fmt"
11 | "github.com/google/uuid"
12 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
13 | "strings"
14 | "sync"
15 | "time"
16 | )
17 |
18 | type DingMessage struct {
19 | Ctx context.Context
20 | Data *chatbot.BotCallbackDataModel
21 | MsgType string
22 | Permission int
23 | IsGroup bool
24 | CardInstanceId string
25 | ReceivedMsgStr string
26 | ConversationID string
27 | ImageCodeList []string
28 | ImageUrlList []string
29 | ProcessStartTime time.Time
30 | ProcessEndTime time.Time
31 | ProcessDurTime time.Duration
32 | }
33 |
34 | var (
35 | messageQueue chan *DingMessage
36 | wg sync.WaitGroup
37 | dingSupportType []string
38 | )
39 |
40 | func DingVarInit() {
41 | messageQueue = make(chan *DingMessage, 1000) // 设置队列容量
42 | dingSupportType = []string{"text", "audio", "picture"}
43 |
44 | numConsumers := 5
45 | // 启动多个消费者
46 | for i := 0; i < numConsumers; i++ {
47 | wg.Add(1)
48 | go messageConsumer()
49 | }
50 | }
51 | func DingChannelDestory() {
52 | close(messageQueue)
53 | }
54 |
55 | func messageConsumer() {
56 | defer wg.Done()
57 | for msg := range messageQueue {
58 | // 处理消息的逻辑
59 | msg.processMessage()
60 | }
61 | }
62 |
63 | func (msg *DingMessage) startProcessing() {
64 | msg.ProcessStartTime = time.Now()
65 | }
66 |
67 | func (msg *DingMessage) endProcessing() {
68 | msg.ProcessEndTime = time.Now()
69 | msg.ProcessDurTime = msg.ProcessEndTime.Sub(msg.ProcessStartTime)
70 | fmt.Println("Duration:", msg.ProcessDurTime)
71 | }
72 | func (msg *DingMessage) processMessage() {
73 | msg.startProcessing()
74 | if msg.ReceivedMsgStr != "" {
75 | // 获取用户sessionId
76 | userID := msg.Data.SenderId
77 | conversationID, exists := difybot.DifyClient.GetSession(msg.Data.SenderId)
78 | if exists {
79 | fmt.Println("Conversation ID for user:", userID, "is", conversationID)
80 | } else {
81 | conversationID = ""
82 | fmt.Println("No conversation ID found for user:", userID)
83 | }
84 | msg.ConversationID = conversationID
85 | // 调用dify API 获取工作流
86 | difyResp, err := difybot.DifyClient.CallAPIStreaming(msg.ReceivedMsgStr, userID, conversationID, msg.Permission)
87 | if err != nil {
88 | fmt.Println("Error CallAPIStreaming:", err)
89 | return
90 | }
91 | defer difyResp.Body.Close()
92 | // 发送卡片
93 | u, err := uuid.NewUUID()
94 | if err != nil {
95 | fmt.Println("生成uuid错误")
96 | return
97 | }
98 | cardInstanceId := u.String()
99 | msg.CardInstanceId = cardInstanceId
100 | // 接收流返回
101 | var answerBuilder strings.Builder
102 | cm := selfutils.NewChannelManager()
103 | defer func() {
104 | if !cm.IsClosed() {
105 | cm.CloseChannel()
106 | }
107 | }()
108 |
109 | go func(cm *selfutils.ChannelManager, cardInstanceId string) {
110 | var lastContent string
111 | timer := time.NewTicker(200 * time.Millisecond) // 每200ms触发一次
112 | defer timer.Stop()
113 | for {
114 | select {
115 | case content := <-cm.DataCh:
116 | {
117 | //fmt.Println("接收到的内容", content)
118 | lastContent = content
119 | }
120 | case <-timer.C:
121 | if lastContent != "" {
122 | go func(content string) {
123 | cardData := fmt.Sprintf(consts.MessageCardTemplateWithTitle1, content)
124 | err := UpdateDingTalkCard(cardData, cardInstanceId)
125 | if err != nil {
126 | fmt.Println("Error updating DingTalk card:", err)
127 | }
128 | }(lastContent)
129 | lastContent = ""
130 | }
131 | case <-cm.CloseCh: // 接收到停止信号,退出循环
132 | return
133 | //case <-cm.CardStart:
134 | //sendInteractiveCard(cardInstanceId, msg)
135 | //cm.CardStart = nil
136 | }
137 |
138 | }
139 | }(cm, cardInstanceId)
140 | sendInteractiveCard(cardInstanceId, msg)
141 | streamScanner := bufio.NewScanner(difyResp.Body)
142 | for streamScanner.Scan() {
143 | var event difybot.StreamingEvent
144 | line := streamScanner.Text()
145 | if line == "" {
146 | continue
147 | }
148 | if strings.HasPrefix(line, "data: ") {
149 | line = strings.TrimPrefix(line, "data: ")
150 | }
151 | if err = json.Unmarshal([]byte(line), &event); err != nil {
152 | fmt.Println("Error decoding JSON:", err)
153 | continue
154 | }
155 |
156 | err = difybot.DifyClient.ProcessEvent(userID, event, &answerBuilder, cm)
157 | if err != nil {
158 | cardData := fmt.Sprintf(consts.MessageCardTemplateWithoutTitle, "服务器内部错误")
159 | err = UpdateDingTalkCard(cardData, cardInstanceId)
160 | fmt.Printf("processEvent err %s\n", err)
161 | return
162 | }
163 |
164 | }
165 |
166 | if err = streamScanner.Err(); err != nil {
167 | fmt.Println("Error reading response:", err)
168 | return
169 | }
170 | if !cm.IsClosed() {
171 | cm.CloseChannel()
172 | }
173 | fmt.Println("Final Answer:", answerBuilder.String())
174 | time.Sleep(300)
175 | cardData := fmt.Sprintf(consts.MessageCardTemplateWithoutTitle, answerBuilder.String())
176 | err = UpdateDingTalkCard(cardData, cardInstanceId)
177 | if err != nil {
178 | fmt.Println("Error updating DingTalk card:", err)
179 | }
180 | // 结束处理
181 | msg.endProcessing()
182 |
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/clients/DingTalkStreamClient.go:
--------------------------------------------------------------------------------
1 | package clients
2 |
3 | import (
4 | openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
5 | dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
6 | dingtalkoauth2_1_0 "github.com/alibabacloud-go/dingtalk/oauth2_1_0"
7 | "github.com/alibabacloud-go/dingtalk/robot_1_0"
8 | util "github.com/alibabacloud-go/tea-utils/v2/service"
9 | "github.com/alibabacloud-go/tea/tea"
10 | "os"
11 | "time"
12 | )
13 |
14 | type DingTalkClient struct {
15 | ClientID string
16 | clientSecret string
17 | accessToken string
18 | tokenExpireAt time.Time
19 | imClient *dingtalkim_1_0.Client
20 | oauthClient *dingtalkoauth2_1_0.Client
21 | robotClient *robot_1_0.Client
22 | }
23 |
24 | var (
25 | DingtalkClient1 *DingTalkClient = nil
26 | )
27 |
28 | func DingTalkStreamClientInit() {
29 | clientId := os.Getenv("CLIENT_ID")
30 | clientSecret := os.Getenv("CLIENT_SECRET")
31 | DingtalkClient1 = NewDingTalkClient(clientId, clientSecret)
32 | }
33 | func NewDingTalkClient(clientId, clientSecret string) *DingTalkClient {
34 | config := &openapi.Config{}
35 | config.Protocol = tea.String("https")
36 | config.RegionId = tea.String("central")
37 | imClient, _ := dingtalkim_1_0.NewClient(config)
38 | oauthClient, _ := dingtalkoauth2_1_0.NewClient(config)
39 | robotClient, _ := robot_1_0.NewClient(config)
40 | return &DingTalkClient{
41 | ClientID: clientId,
42 | clientSecret: clientSecret,
43 | imClient: imClient,
44 | oauthClient: oauthClient,
45 | robotClient: robotClient,
46 | }
47 | }
48 |
49 | func (c *DingTalkClient) GetAccessToken() (string, error) {
50 | // 检查当前 token 是否过期
51 | if time.Now().Before(c.tokenExpireAt) {
52 | return c.accessToken, nil
53 | }
54 | request := &dingtalkoauth2_1_0.GetAccessTokenRequest{
55 | AppKey: tea.String(c.ClientID),
56 | AppSecret: tea.String(c.clientSecret),
57 | }
58 | response, tryErr := func() (_resp *dingtalkoauth2_1_0.GetAccessTokenResponse, _e error) {
59 | defer func() {
60 | if r := tea.Recover(recover()); r != nil {
61 | _e = r
62 | }
63 | }()
64 | _resp, _err := c.oauthClient.GetAccessToken(request)
65 | if _err != nil {
66 | return nil, _err
67 | }
68 |
69 | return _resp, nil
70 | }()
71 | if tryErr != nil {
72 | return "", tryErr
73 | }
74 | c.accessToken = *response.Body.AccessToken
75 | c.tokenExpireAt = time.Now().Add(1 * time.Hour)
76 |
77 | return *response.Body.AccessToken, nil
78 | }
79 |
80 | func (c *DingTalkClient) SendInteractiveCard(request *dingtalkim_1_0.SendRobotInteractiveCardRequest) (*dingtalkim_1_0.SendRobotInteractiveCardResponse, error) {
81 | accessToken, err := c.GetAccessToken()
82 | if err != nil {
83 | return nil, err
84 | }
85 |
86 | headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
87 | XAcsDingtalkAccessToken: tea.String(accessToken),
88 | }
89 | response, tryErr := func() (_resp *dingtalkim_1_0.SendRobotInteractiveCardResponse, _e error) {
90 | defer func() {
91 | if r := tea.Recover(recover()); r != nil {
92 | _e = r
93 | }
94 | }()
95 | _resp, _e = c.imClient.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
96 | if _e != nil {
97 | return
98 | }
99 | return
100 | }()
101 | if tryErr != nil {
102 | return nil, tryErr
103 | }
104 | return response, nil
105 | }
106 |
107 | func (c *DingTalkClient) UpdateInteractiveCard(request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
108 | accessToken, err := c.GetAccessToken()
109 | if err != nil {
110 | return nil, err
111 | }
112 |
113 | headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
114 | XAcsDingtalkAccessToken: tea.String(accessToken),
115 | }
116 | response, tryErr := func() (_resp *dingtalkim_1_0.UpdateRobotInteractiveCardResponse, _e error) {
117 | defer func() {
118 | if r := tea.Recover(recover()); r != nil {
119 | _e = r
120 | }
121 | }()
122 | _resp, _e = c.imClient.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
123 | if _e != nil {
124 | return
125 | }
126 | return
127 | }()
128 | if tryErr != nil {
129 | return nil, tryErr
130 | }
131 | return response, nil
132 | }
133 |
134 | func (c *DingTalkClient) RobotMessageFileDownload(request *robot_1_0.RobotMessageFileDownloadRequest) (*robot_1_0.RobotMessageFileDownloadResponse, error) {
135 | accessToken, err := c.GetAccessToken()
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | headers := &robot_1_0.RobotMessageFileDownloadHeaders{
141 | XAcsDingtalkAccessToken: tea.String(accessToken),
142 | }
143 | request.RobotCode = &c.ClientID
144 | response, tryErr := func() (_resp *robot_1_0.RobotMessageFileDownloadResponse, _e error) {
145 | defer func() {
146 | if r := tea.Recover(recover()); r != nil {
147 | _e = r
148 | }
149 | }()
150 | _resp, _e = c.robotClient.RobotMessageFileDownloadWithOptions(request, headers, &util.RuntimeOptions{})
151 |
152 | //_resp, _e = c.imClient.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
153 | if _e != nil {
154 | return
155 | }
156 | return
157 | }()
158 | if tryErr != nil {
159 | return nil, tryErr
160 | }
161 | return response, nil
162 | }
163 |
--------------------------------------------------------------------------------
/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "ding/consts"
5 | "fmt"
6 | "github.com/joho/godotenv"
7 | "os"
8 | "strings"
9 | )
10 |
11 | func LoadConfig() error {
12 | // 尝试加载 .env 文件
13 | err := godotenv.Load()
14 | if err != nil {
15 | // 如果 .env 文件不存在,尝试加载 .env_template 文件
16 | if os.IsNotExist(err) {
17 | err = godotenv.Load(".env_template")
18 | if err != nil {
19 | return err
20 | }
21 | } else {
22 | return err
23 | }
24 | }
25 | // 从环境变量中获取关键词
26 | VoiceKeywords := os.Getenv("VOICE_KEYWORDS")
27 | if VoiceKeywords == "" {
28 | fmt.Println("No keywords found in environment")
29 | }
30 | // 将语音关键词字符串分割为 slice
31 | consts.VoicePrefix = strings.Split(VoiceKeywords, ",")
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/consts/ding.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | MessageCardTemplateWithTitle1 = `
5 | {
6 | "config": {
7 | "autoLayout": true,
8 | "enableForward": true
9 | },
10 | "contents": [
11 | {
12 | "type": "markdown",
13 | "text": "",
14 | "id": "text_1693929551595"
15 | },
16 | {
17 | "type": "divider",
18 | "id": "divider_1693929551595"
19 | },
20 | {
21 | "type": "markdown",
22 | "text": "%s ",
23 | "id": "markdown_1693929674245"
24 | }
25 | ]
26 | }
27 | `
28 |
29 | MessageCardTemplateWithoutTitle = `
30 | {
31 | "config": {
32 | "autoLayout": true,
33 | "enableForward": true
34 | },
35 | "contents": [
36 | {
37 | "type": "markdown",
38 | "text": "%s",
39 | "id": "markdown_1693929674245"
40 | }
41 | ]
42 | }
43 | `
44 | )
45 |
46 | const (
47 | OutputTypeText = "Text"
48 | OutputTypeStream = "Stream"
49 | OutputTypeMarkDown = "MarkDown"
50 |
51 | ReceivedTypeText = "text"
52 | ReceivedTypeImage = "picture"
53 | ReceivedTypeVoice = "audio"
54 | )
55 |
56 | var VoicePrefix = []string{}
57 |
--------------------------------------------------------------------------------
/consts/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyang38/dify-on-dingding-go/a3cd43941038e009daa9f781d4d9e847dbc24d43/consts/img.png
--------------------------------------------------------------------------------
/consts/imgStream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyang38/dify-on-dingding-go/a3cd43941038e009daa9f781d4d9e847dbc24d43/consts/imgStream.png
--------------------------------------------------------------------------------
/consts/imgStream2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MAyang38/dify-on-dingding-go/a3cd43941038e009daa9f781d4d9e847dbc24d43/consts/imgStream2.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | ports:
9 | - "7777:7777"
10 | environment:
11 | REDIS_ADDR: redis:6379
12 | REDIS_PASSWORD: your_redis_password
13 | depends_on:
14 | - redis
15 |
16 | redis:
17 | image: "redis:latest"
18 | ports:
19 | - "6379:6379"
20 | volumes:
21 | - redis-data:/data
22 | command: ["redis-server", "--requirepass", "your_redis_password"]
23 |
24 | volumes:
25 | redis-data:
26 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module ding
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6
7 | github.com/alibabacloud-go/dingtalk v1.6.71
8 | github.com/alibabacloud-go/tea v1.2.1
9 | github.com/alibabacloud-go/tea-utils/v2 v2.0.5
10 | github.com/cloudwego/hertz v0.9.1
11 | github.com/go-redis/redis/v8 v8.11.5
12 | github.com/google/uuid v1.3.0
13 | github.com/joho/godotenv v1.5.1
14 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0
15 | )
16 |
17 | require (
18 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
19 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
20 | github.com/alibabacloud-go/gateway-dingtalk v1.0.2 // indirect
21 | github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
22 | github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
23 | github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
24 | github.com/aliyun/credentials-go v1.3.1 // indirect
25 | github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect
26 | github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect
27 | github.com/bytedance/sonic v1.8.1 // indirect
28 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
29 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
30 | github.com/clbanning/mxj/v2 v2.5.5 // indirect
31 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
32 | github.com/fsnotify/fsnotify v1.5.4 // indirect
33 | github.com/golang/protobuf v1.5.0 // indirect
34 | github.com/gorilla/websocket v1.5.0 // indirect
35 | github.com/henrylee2cn/ameda v1.4.10 // indirect
36 | github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect
37 | github.com/json-iterator/go v1.1.12 // indirect
38 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect
39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
40 | github.com/modern-go/reflect2 v1.0.2 // indirect
41 | github.com/nyaruka/phonenumbers v1.0.55 // indirect
42 | github.com/tidwall/gjson v1.14.4 // indirect
43 | github.com/tidwall/match v1.1.1 // indirect
44 | github.com/tidwall/pretty v1.2.0 // indirect
45 | github.com/tjfoc/gmsm v1.3.2 // indirect
46 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
47 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
48 | golang.org/x/net v0.17.0 // indirect
49 | golang.org/x/sys v0.13.0 // indirect
50 | google.golang.org/protobuf v1.27.1 // indirect
51 | gopkg.in/ini.v1 v1.56.0 // indirect
52 | )
53 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
2 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
3 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6 h1:y1K+zKhpWcxso8zqI03CcYuwgyZPFwQdwAQOXAeuOVM=
4 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
5 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50=
6 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
7 | github.com/alibabacloud-go/dingtalk v1.6.71 h1:wpwIOtsmEavXePYzhYU4dE+xNjsi99VpaKORgFrN/4o=
8 | github.com/alibabacloud-go/dingtalk v1.6.71/go.mod h1:62whWaRXvNFOuYdhyP6H5O/V0WqQpwCNeUi14rcBNuY=
9 | github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
10 | github.com/alibabacloud-go/gateway-dingtalk v1.0.2 h1:+etjmc64QTmYvHlc6eFkH9y2DOc3UPcyD2nF3IXsVqw=
11 | github.com/alibabacloud-go/gateway-dingtalk v1.0.2/go.mod h1:JUvHpkJtlPFpgJcfXqc9Y4mk2JnoRn5XpKbRz38jJho=
12 | github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
13 | github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
14 | github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
15 | github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
16 | github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
17 | github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
18 | github.com/alibabacloud-go/tea v1.2.1 h1:rFF1LnrAdhaiPmKwH5xwYOKlMh66CqRwPUTzIK74ask=
19 | github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
20 | github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
21 | github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
22 | github.com/alibabacloud-go/tea-utils/v2 v2.0.1/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
23 | github.com/alibabacloud-go/tea-utils/v2 v2.0.5 h1:EUakYEUAwr6L3wLT0vejIw2rc0IA1RSXDwLnIb3f2vU=
24 | github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
25 | github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
26 | github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
27 | github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
28 | github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28=
29 | github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
30 | github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I=
31 | github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM=
32 | github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM=
33 | github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
34 | github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4=
35 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
36 | github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
37 | github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
38 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
39 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
40 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
41 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
42 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
43 | github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
44 | github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
45 | github.com/cloudwego/hertz v0.9.1 h1:+jK9A6MDNTUVy6q/zSOlhbnp1fFMiOaPIsq0jlOfjZE=
46 | github.com/cloudwego/hertz v0.9.1/go.mod h1:cs8dH6unM4oaJ5k9m6pqbgLBPqakGWMG0+cthsxitsg=
47 | github.com/cloudwego/netpoll v0.6.0 h1:JRMkrA1o8k/4quxzg6Q1XM+zIhwZsyoWlq6ef+ht31U=
48 | github.com/cloudwego/netpoll v0.6.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ=
49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
50 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
51 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
52 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
53 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
54 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
55 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
56 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
57 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
58 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
59 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
60 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
61 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
62 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
63 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
64 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
65 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
66 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
67 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
68 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
69 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
70 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
71 | github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
72 | github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk=
73 | github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
74 | github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0=
75 | github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ=
76 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
77 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
78 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
79 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
80 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
81 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
82 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
83 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
84 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
85 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
86 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
87 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
88 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
89 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
90 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
91 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
92 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
93 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
94 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
95 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
96 | github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
97 | github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
98 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
99 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
100 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
101 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
102 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0 h1:DL64ORGMk6AUB8q5LbRp8KRFn4oHhdrSepBmbMrtmNo=
103 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
104 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
105 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
106 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
107 | github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
108 | github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
109 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
110 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
111 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
112 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
113 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
114 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
115 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
116 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
117 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
118 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
119 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
120 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
121 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
122 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
123 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
124 | github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
125 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
126 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
127 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
128 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
129 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
130 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
131 | github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
132 | github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
133 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
134 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
135 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
136 | github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
137 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
138 | golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
139 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
140 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
143 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
144 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
145 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
146 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
147 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
148 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
149 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
150 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
151 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
152 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
153 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
154 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
155 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
156 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
157 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
158 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
159 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
160 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
161 | golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
162 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
163 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
164 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
165 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
166 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
167 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
168 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
169 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
170 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
171 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
172 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
173 | golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
174 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
175 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
176 | golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
177 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
178 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
179 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
180 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
181 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
182 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
183 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
184 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
185 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
186 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
187 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
188 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
189 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
190 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
191 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
192 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
193 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
194 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
195 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
196 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
197 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
198 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
199 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
200 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
201 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
202 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
203 | golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
204 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
205 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
206 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
207 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
208 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
209 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
210 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
211 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
212 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
213 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
214 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
215 | gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
216 | gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
217 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
218 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
219 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
220 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
221 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
222 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
223 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
224 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
225 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
226 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
227 |
--------------------------------------------------------------------------------
/handlers/dify.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "context"
5 | "github.com/cloudwego/hertz/pkg/app"
6 | "github.com/cloudwego/hertz/pkg/protocol/consts"
7 | )
8 |
9 | type difyHandlers struct{}
10 |
11 | var DifyTandlers difyHandlers
12 |
13 | // DifyTandlers 处理 /chat-message 路由
14 | func (h *difyHandlers) ChatMessageHandler(ctx context.Context, c *app.RequestContext) {
15 | c.String(consts.StatusOK, "Hello Hertz!")
16 | }
17 |
--------------------------------------------------------------------------------
/handlers/ding.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
--------------------------------------------------------------------------------
/handlers/test.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "context"
5 | "github.com/cloudwego/hertz/pkg/app"
6 | "github.com/cloudwego/hertz/pkg/protocol/consts"
7 | )
8 |
9 | type testHandlers struct{}
10 |
11 | var TestTandlers testHandlers
12 |
13 | // HelloHandler 处理 /hello 路由
14 | func (h *testHandlers) HelloHandler(ctx context.Context, c *app.RequestContext) {
15 | c.String(consts.StatusOK, "Hello Hertz!")
16 | }
17 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "ding/bot/difybot"
5 | dingbot "ding/bot/dingtalk"
6 | "ding/conf"
7 | "fmt"
8 | )
9 |
10 | func main() {
11 |
12 | err := conf.LoadConfig()
13 | if err != nil {
14 | fmt.Print("加载环境变量出错")
15 | return
16 | }
17 |
18 | // 初始化dify和钉钉机器人
19 | difybot.InitDifyClient()
20 | dingbot.StartDingRobot()
21 |
22 | //hertz http框架 未来支持提供接口调用
23 | //h := server.Default()
24 | //// 添加请求日志中间件
25 | //h.Use(middlewares.RequestLogger())
26 | //h.GET("/hello", handlers.TestTandlers.HelloHandler)
27 | //h.POST("/dify/chat-message", handlers.DifyTandlers.ChatMessageHandler)
28 | //h.Spin()
29 | }
30 |
--------------------------------------------------------------------------------
/middlewares/logs.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/cloudwego/hertz/pkg/app"
7 | "github.com/cloudwego/hertz/pkg/common/hlog"
8 | "time"
9 | )
10 |
11 | // RequestLogger 中间件函数
12 | func RequestLogger() app.HandlerFunc {
13 | return func(ctx context.Context, c *app.RequestContext) {
14 | startTime := time.Now()
15 |
16 | // 继续处理请求
17 | c.Next(ctx)
18 |
19 | // 记录请求日志
20 | duration := time.Since(startTime)
21 | method := string(c.Method())
22 | path := string(c.Request.URI().PathOriginal())
23 | statusCode := c.Response.StatusCode()
24 | clientIP := c.ClientIP()
25 |
26 | logMessage := fmt.Sprintf("[%s] %s %s %s %d %s",
27 | startTime.Format(time.RFC3339),
28 | clientIP,
29 | method,
30 | path,
31 | statusCode,
32 | duration,
33 | )
34 |
35 | hlog.Info(logMessage)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/models/baiduaodio.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type AccessTokenResponse struct {
4 | AccessToken string `json:"access_token"`
5 | ExpiresIn int `json:"expires_in"`
6 | }
7 |
8 | type VoicdeRespBody struct {
9 | CorpusNo string `json:"corpus_no"`
10 | ErrMsg string `json:"err_msg"`
11 | ErrNo int `json:"err_no"`
12 | Result []string `json:"result"`
13 | Sn string `json:"sn"`
14 | }
15 |
--------------------------------------------------------------------------------
/utils/channelManager.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type ChannelManager struct {
8 | DataCh chan string
9 | CloseCh chan struct{}
10 | CardStart chan struct{}
11 | closeOnce sync.Once
12 | mutex sync.Mutex
13 | isClosed bool
14 | }
15 |
16 | func NewChannelManager() *ChannelManager {
17 | return &ChannelManager{
18 | DataCh: make(chan string, 1),
19 | CloseCh: make(chan struct{}),
20 | CardStart: make(chan struct{}),
21 | }
22 | }
23 |
24 | func (cm *ChannelManager) CloseChannel() {
25 | cm.closeOnce.Do(func() {
26 | close(cm.CloseCh)
27 | close(cm.DataCh)
28 | close(cm.CardStart)
29 | cm.mutex.Lock()
30 | defer cm.mutex.Unlock()
31 | cm.isClosed = true
32 | })
33 | }
34 |
35 | func (cm *ChannelManager) IsClosed() bool {
36 | cm.mutex.Lock()
37 | defer cm.mutex.Unlock()
38 | return cm.isClosed
39 | }
40 |
--------------------------------------------------------------------------------
/utils/stringUtils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "strings"
4 |
5 | // 判断字符串a 是否在list中
6 | func StringInSlice(a string, list []string) bool {
7 | for _, b := range list {
8 | if b == a {
9 | return true
10 | }
11 | }
12 | return false
13 | }
14 |
15 | // 判断字符串s是否包含任意关键词keywords
16 | func ContainsKeywords(s string, keywords []string) bool {
17 | for _, keyword := range keywords {
18 | if strings.Contains(s, keyword) {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
--------------------------------------------------------------------------------
/voices/baidu.go:
--------------------------------------------------------------------------------
1 | package audio
2 |
3 | import (
4 | "ding/models"
5 | "encoding/base64"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "io/ioutil"
10 | "log"
11 | "net/http"
12 | "os"
13 | "strings"
14 | "time"
15 | )
16 |
17 | type BaiduVoice struct {
18 | ClientID string
19 | ClientSecret string
20 | Token string
21 | Expire time.Time
22 | }
23 |
24 | type VoiceData struct {
25 | Format string `json:"format"`
26 | Rate int `json:"rate"`
27 | Channel int `json:"channel"`
28 | Token interface{} `json:"token"`
29 | DevPid int `json:"dev_pid"`
30 | Cuid string `json:"cuid"`
31 | Len int `json:"len"`
32 | Speech string `json:"speech"`
33 | }
34 |
35 | var BaiduVoicdeCli *BaiduVoice
36 |
37 | const tokenUrlTemplpate = "https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials"
38 |
39 | func BaiduVoiceInit() {
40 | BaiduVoicdeCli = &BaiduVoice{
41 | ClientID: os.Getenv("BaiduClientId"),
42 | ClientSecret: os.Getenv("BaiduClientSecret"),
43 | Expire: time.Now(),
44 | }
45 | }
46 | func (c *BaiduVoice) VoiceToText(filePath string) (string, error) {
47 | url := "https://vop.baidu.com/server_api"
48 | // 指定本地文件路径
49 | // 读取文件内容
50 | fileBytes, err := ioutil.ReadFile(filePath)
51 | if err != nil {
52 | log.Fatalf("Error reading file: %v", err)
53 | }
54 |
55 | // 计算文件大小(字节数)
56 | fileSize := len(fileBytes)
57 | fmt.Printf("File size: %d bytes\n", fileSize)
58 |
59 | // 将文件内容转换为Base64编码
60 | token, err := c.GetAccessToken()
61 | if err != nil {
62 | return "", err
63 | }
64 |
65 | base64String := base64.StdEncoding.EncodeToString(fileBytes)
66 | voiceData := VoiceData{
67 | Format: "pcm",
68 | Rate: 16000,
69 | Channel: 1,
70 | DevPid: 1537,
71 | Token: token,
72 | Cuid: "TZu3ZUWS8wQQ7Sa3gdYQI4aFGb08xTG1",
73 | Len: fileSize,
74 | Speech: base64String,
75 | }
76 | // 将VoiceData结构体编码为JSON
77 | jsonData, err := json.Marshal(voiceData)
78 | if err != nil {
79 | log.Fatalf("Error marshalling JSON: %v", err)
80 | }
81 | payload := strings.NewReader(string(jsonData))
82 | client := &http.Client{}
83 | req, err := http.NewRequest("POST", url, payload)
84 |
85 | if err != nil {
86 | fmt.Println(err)
87 | return "", err
88 | }
89 | req.Header.Add("Content-Type", "application/json")
90 | req.Header.Add("Accept", "application/json")
91 |
92 | res, err := client.Do(req)
93 | if err != nil {
94 | fmt.Println(err)
95 | return "", err
96 | }
97 | defer res.Body.Close()
98 |
99 | body, err := ioutil.ReadAll(res.Body)
100 | if err != nil {
101 | fmt.Println(err)
102 | return "", err
103 | }
104 | voiceRespBody := models.VoicdeRespBody{}
105 | _ = json.Unmarshal(body, &voiceRespBody)
106 | fmt.Println(voiceRespBody)
107 | if voiceRespBody.ErrNo != 0 {
108 | return "", errors.New(voiceRespBody.ErrMsg)
109 | }
110 | return voiceRespBody.Result[0], nil
111 | }
112 |
113 | /**
114 | * 使用 AK,SK 生成鉴权签名(Access Token)
115 | * @return string 鉴权签名信息(Access Token)
116 | */
117 | func (c *BaiduVoice) GetAccessToken() (string, error) {
118 | if time.Now().Before(c.Expire) {
119 | return c.Token, nil
120 | }
121 | url := "https://aip.baidubce.com/oauth/2.0/token"
122 | postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", c.ClientID, c.ClientSecret)
123 | fmt.Printf(postData)
124 | resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(postData))
125 | if err != nil {
126 | fmt.Println(err)
127 | return "", err
128 | }
129 | defer resp.Body.Close()
130 | body, err := ioutil.ReadAll(resp.Body)
131 | fmt.Printf(string(body))
132 | if err != nil {
133 | fmt.Println(err)
134 | return "", err
135 | }
136 | accessTokenObj := models.AccessTokenResponse{}
137 | _ = json.Unmarshal(body, &accessTokenObj)
138 | c.Token = accessTokenObj.AccessToken
139 | c.Expire = time.Now().Add(time.Duration(accessTokenObj.ExpiresIn) * time.Second)
140 | return accessTokenObj.AccessToken, nil
141 | }
142 |
--------------------------------------------------------------------------------
/voices/xunfei.go:
--------------------------------------------------------------------------------
1 | package audio
2 |
3 | import (
4 | "bytes"
5 | "crypto/hmac"
6 | "crypto/md5"
7 | "crypto/sha1"
8 | "encoding/base64"
9 | "encoding/json"
10 | "fmt"
11 | "io/ioutil"
12 | "net/http"
13 | "net/url"
14 | "os"
15 | "strconv"
16 | "time"
17 | )
18 |
19 | const (
20 | lfasrHost = "https://raasr.xfyun.cn/v2/api"
21 | apiUpload = "/upload"
22 | apiGetResult = "/getResult"
23 | )
24 |
25 | type RequestApi struct {
26 | AppID string
27 | SecretKey string
28 | UploadFilePath string
29 | Timestamp string
30 | Signa string
31 | }
32 |
33 | // SpeechResult 代表整个识别结果的结构
34 | type SpeechResult struct {
35 | Code string `json:"code"`
36 | Content SpeechContent `json:"content"`
37 | DescInfo string `json:"descInfo"`
38 | }
39 |
40 | // SpeechContent 代表 result 部分的结构
41 | type SpeechContent struct {
42 | OrderInfo OrderInfo `json:"orderInfo"`
43 | OrderResult string `json:"orderResult"`
44 | }
45 |
46 | // OrderInfo 代表 orderInfo 部分的结构
47 | type OrderInfo struct {
48 | ExpireTime float64 `json:"expireTime"`
49 | FailType int `json:"failType"`
50 | OrderID string `json:"orderId"`
51 | OriginalDuration int `json:"originalDuration"`
52 | RealDuration int `json:"realDuration"`
53 | Status int `json:"status"`
54 | }
55 |
56 | // SpeechResultData 代表 orderResult 中的数据结构
57 | type SpeechResultData struct {
58 | Lattice []LatticeItem `json:"lattice"`
59 | Lattice2 []Lattice2Item `json:"lattice2"`
60 | }
61 |
62 | // LatticeItem 代表 lattice 部分的结构
63 | type LatticeItem struct {
64 | Json1Best string `json:"json_1best"`
65 | }
66 |
67 | // Lattice2Item 代表 lattice2 部分的结构
68 | type Lattice2Item struct {
69 | Lid string `json:"lid"`
70 | End string `json:"end"`
71 | Begin string `json:"begin"`
72 | Json1Best Lattice2Best `json:"json_1best"`
73 | }
74 |
75 | // Lattice2Best 代表 lattice2 中 json_1best 部分的结构
76 | type Lattice2Best struct {
77 | St SpeechText `json:"st"`
78 | }
79 |
80 | // SpeechText 代表语音识别的详细文本结构
81 | type SpeechText struct {
82 | Sc string `json:"sc"`
83 | Pa string `json:"pa"`
84 | Rt []SpeechRT `json:"rt"`
85 | }
86 |
87 | // SpeechRT 代表 rt 部分的结构
88 | type SpeechRT struct {
89 | Ws []SpeechWS `json:"ws"`
90 | }
91 |
92 | // SpeechWS 代表 ws 部分的结构
93 | type SpeechWS struct {
94 | Cw []SpeechCW `json:"cw"`
95 | }
96 |
97 | // SpeechCW 代表 cw 部分的结构
98 | type SpeechCW struct {
99 | W string `json:"w"`
100 | Wp string `json:"wp"`
101 | Wc string `json:"wc"`
102 | }
103 |
104 | func extractTextFromResult(orderResult string) (string, error) {
105 | var result SpeechResultData
106 |
107 | err := json.Unmarshal([]byte(orderResult), &result)
108 | if err != nil {
109 | return "", err
110 | }
111 |
112 | var text string
113 |
114 | // 从 lattice2 中提取文本
115 | for _, lattice2 := range result.Lattice2 {
116 | st := lattice2.Json1Best.St
117 | for _, rtItem := range st.Rt {
118 | for _, wsItem := range rtItem.Ws {
119 | for _, cwItem := range wsItem.Cw {
120 | text += cwItem.W
121 | }
122 | }
123 | }
124 | }
125 |
126 | return text, nil
127 | }
128 |
129 | func (api *RequestApi) getSigna() string {
130 | appid := api.AppID
131 | secretKey := api.SecretKey
132 | ts := api.Timestamp
133 |
134 | // 1. 获取 baseString
135 |
136 | baseString := appid + ts
137 |
138 | // 2. 对 baseString 进行 MD5
139 | md5Hash := md5.Sum([]byte(baseString))
140 | md5HashString := fmt.Sprintf("%x", md5Hash)
141 |
142 | // 3. 使用 secretKey 对 MD5 结果进行 HMAC-SHA1 加密
143 | hmacHash := hmac.New(sha1.New, []byte(secretKey))
144 | hmacHash.Write([]byte(md5HashString))
145 | signa := base64.StdEncoding.EncodeToString(hmacHash.Sum(nil))
146 |
147 | return signa
148 | }
149 |
150 | func (api *RequestApi) upload() (map[string]interface{}, error) {
151 | fmt.Println("上传部分:")
152 | filePath := api.UploadFilePath
153 | file, err := os.Open(filePath)
154 | if err != nil {
155 | return nil, err
156 | }
157 | defer file.Close()
158 |
159 | fileInfo, err := file.Stat()
160 | if err != nil {
161 | return nil, err
162 | }
163 | fileLen := fileInfo.Size()
164 | fileName := fileInfo.Name()
165 |
166 | param := url.Values{}
167 | param.Add("appId", api.AppID)
168 | param.Add("signa", api.Signa)
169 | param.Add("ts", api.Timestamp)
170 | param.Add("fileSize", strconv.FormatInt(fileLen, 10))
171 | param.Add("fileName", fileName)
172 | param.Add("duration", "200")
173 |
174 | url := lfasrHost + apiUpload + "?" + param.Encode()
175 | fmt.Println("upload参数:", param)
176 | fmt.Println("upload_url:", url)
177 |
178 | data, err := ioutil.ReadFile(filePath)
179 | if err != nil {
180 | return nil, err
181 | }
182 |
183 | resp, err := http.Post(url, "application/json", bytes.NewReader(data))
184 | if err != nil {
185 | return nil, err
186 | }
187 | defer resp.Body.Close()
188 |
189 | body, err := ioutil.ReadAll(resp.Body)
190 | if err != nil {
191 | return nil, err
192 | }
193 |
194 | result := make(map[string]interface{})
195 | err = json.Unmarshal(body, &result)
196 | if err != nil {
197 | return nil, err
198 | }
199 |
200 | fmt.Println("upload resp:", result)
201 | return result, nil
202 | }
203 |
204 | func (api *RequestApi) getResult() (*SpeechResult, error) {
205 | uploadResp, err := api.upload()
206 | if err != nil {
207 | return nil, err
208 | }
209 |
210 | orderId := uploadResp["content"].(map[string]interface{})["orderId"].(string)
211 |
212 | param := url.Values{}
213 | param.Add("appId", api.AppID)
214 | param.Add("signa", api.Signa)
215 | param.Add("ts", api.Timestamp)
216 | param.Add("orderId", orderId)
217 | param.Add("resultType", "transfer,predict")
218 |
219 | url := lfasrHost + apiGetResult + "?" + param.Encode()
220 | fmt.Println("查询部分:")
221 | fmt.Println("get result参数:", param)
222 |
223 | //var result map[string]interface{}
224 | var result SpeechResult
225 | status := 3
226 |
227 | for status == 3 {
228 | resp, err := http.Post(url, "application/json", nil)
229 | if err != nil {
230 | return nil, err
231 | }
232 | defer resp.Body.Close()
233 |
234 | body, err := ioutil.ReadAll(resp.Body)
235 | if err != nil {
236 | return nil, err
237 | }
238 |
239 | err = json.Unmarshal(body, &result)
240 | if err != nil {
241 | return nil, err
242 | }
243 |
244 | fmt.Println(result)
245 | status = result.Content.OrderInfo.Status
246 | if status == 4 {
247 | break
248 | }
249 |
250 | time.Sleep(5 * time.Second)
251 | }
252 |
253 | fmt.Println("get_result resp:", result)
254 | return &result, nil
255 | }
256 |
257 | func XunfeiHandler() {
258 | ts := strconv.FormatInt(time.Now().Unix(), 10)
259 | api := RequestApi{
260 | AppID: os.Getenv("XUNFEI_APPID"),
261 | SecretKey: os.Getenv("XUNFEI_SecretKey"),
262 | UploadFilePath: "audio/aigei_com.wav",
263 | Timestamp: ts,
264 | Signa: "",
265 | }
266 | api.Signa = api.getSigna()
267 |
268 | maps, err := api.getResult()
269 | if err != nil {
270 | fmt.Println("Error:", err)
271 | }
272 | str := maps.Content.OrderResult
273 | text, err := extractTextFromResult(str)
274 | if err != nil {
275 | fmt.Println("Error:", err)
276 | return
277 | }
278 |
279 | fmt.Println("Extracted Text:", text)
280 | }
281 |
--------------------------------------------------------------------------------