├── .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 | ![img.png](consts%2Fimg.png) 44 | 45 | ## 流输出模式 46 | ![img.png](consts/imgStream2.png) 47 | 48 | ![img.png](consts/imgStream.png) 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": "![loading](https://dify-oss-test.oss-cn-shanghai.aliyuncs.com/gif/loading_gif50.gif)", 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 | --------------------------------------------------------------------------------