├── .fleet └── run.json ├── .github └── workflows │ ├── build.yml │ └── docker-image.yml ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── main.go └── static │ ├── images │ └── qrcode.jpg │ ├── keyword.json │ └── templates │ └── index.html ├── config.yml ├── go.mod ├── go.sum └── pkg ├── controller └── index_controller.go ├── logger ├── logger.go └── logger_test.go ├── model ├── config.go ├── message.go └── tuling.go ├── provider ├── provider.go └── yml_config_provider.go ├── service ├── keyword_service.go └── wechat_service.go ├── third-party ├── ark │ ├── doubao.go │ └── doubao_test.go ├── assistant_service.go ├── dashscope │ ├── client.go │ ├── dashscope.go │ └── dashscope_test.go ├── openai │ ├── openai.go │ └── openai_test.go └── tuling │ └── tuling.go └── util ├── address_util.go ├── json_util.go └── message_util.go /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "go", 5 | "name": "weChatRobot-go", 6 | "goExecPath": "/usr/local/go/bin/go", 7 | "buildParams": [ 8 | "$PROJECT_DIR$/cmd/main.go", 9 | ], 10 | "environment": { 11 | "OPENAI_API_KEY": "", 12 | "TULING_API_KEY": "", 13 | "OPENAI_BASE_DOMAIN": "", 14 | "OPENAI_PROXY": "" 15 | }, 16 | "runParams": [ 17 | "-config", 18 | "config.yml" 19 | ] 20 | }, 21 | ] 22 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.21.8' 18 | - name: Build 19 | run: make 20 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Build the Docker image 15 | run: docker build -t wechatrobot-go:$(date +%s) . 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.DS_Store 3 | __debug_bin 4 | 5 | bin/ 6 | vendor/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "WeChatRobot-go", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "env": { 10 | "OPENAI_API_KEY": "", 11 | "TULING_API_KEY": "", 12 | "OPENAI_BASE_DOMAIN": "", 13 | "OPENAI_PROXY": "" 14 | }, 15 | "program": "${workspaceFolder}/cmd/main.go", 16 | "args": [ 17 | "-config", 18 | "config.yml" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.8 AS build 2 | 3 | WORKDIR /src/weChatRobot-go 4 | 5 | COPY . ./ 6 | 7 | RUN make 8 | 9 | FROM scratch 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=build /src/weChatRobot-go/bin/weChatRobot_* ./weChatRobot 14 | COPY --from=build /src/weChatRobot-go/config.yml ./ 15 | 16 | EXPOSE 8080 17 | 18 | ENTRYPOINT ["/app/weChatRobot"] 19 | CMD ["-config", "/app/config.yml"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOOS=$(shell go env GOOS) 2 | GOARCH=$(shell go env GOARCH) 3 | 4 | GO_BUILD=CGO_ENABLED=0 go build -trimpath -mod=vendor 5 | 6 | .DEFAULT_GOAL := build 7 | 8 | .PHONY: all 9 | all: darwin_amd64 darwin_arm64 linux_amd64 linux_arm64 windows_amd64 10 | 11 | .PHONY: darwin_amd64 12 | darwin_amd64: 13 | GOOS=darwin GOARCH=amd64 $(MAKE) build 14 | 15 | .PHONY: darwin_arm64 16 | darwin_arm64: 17 | GOOS=darwin GOARCH=arm64 $(MAKE) build 18 | 19 | .PHONY: linux_amd64 20 | linux_amd64: 21 | GOOS=linux GOARCH=amd64 $(MAKE) build 22 | 23 | .PHONY: linux_arm64 24 | linux_arm64: 25 | GOOS=linux GOARCH=arm64 $(MAKE) build 26 | 27 | .PHONY: windows_amd64 28 | windows_amd64: 29 | GOOS=windows GOARCH=amd64 EXTENSION=.exe $(MAKE) build 30 | 31 | .PHONY: build 32 | build: vendor 33 | $(GO_BUILD) -o ./bin/weChatRobot_$(GOOS)_$(GOARCH)$(EXTENSION) ./cmd/ 34 | 35 | .PHONY: vendor 36 | vendor: 37 | go mod vendor -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weChatRobot 2 | 3 | 一个基于微信公众号的智能聊天机器人项目,支持根据关键字或者调用OpenAI、通义千问等大语言模型服务回复内容。 4 | 5 | 本项目还有Java实现的版本: 6 | 7 | ![qrcode](cmd/static/images/qrcode.jpg "扫码关注,体验智能机器人") 8 | 9 | ## 项目介绍 10 | 11 | 本项目是一个微信公众号项目,需配合微信公众号使用,在微信公众号配置本项目运行的服务器域名,用户关注公众号后,向公众号发送任意信息,公众号会根据用户发送的内容自动回复。 12 | 13 | ## 第三方依赖 14 | 15 | - [gin](https://github.com/gin-gonic/gin) 16 | - [gjson](https://github.com/tidwall/gjson) 17 | - [yaml](https://gopkg.in/yaml.v3) 18 | - [openaigo](https://github.com/otiai10/openaigo) 19 | - [volcengine-go-sdk](https://github.com/volcengine/volcengine-go-sdk) 20 | 21 | ## 支持的功能 22 | 23 | + [x] 自定义关键字回复内容 24 | + [x] 调用火山方舟(豆包)接口回复内容(需配置环境变量:`ARK_API_KEY`,`ARK_ENDPOINT_ID`) 25 | + [x] 调用OpenAI接口回复内容(需配置环境变量:`OPENAI_API_KEY`) 26 | + [x] 调用通义千问接口回复内容(需配置环境变量:`DASHSCOPE_API_KEY`) 27 | + [x] 调用图灵机器人(V2)接口回复内容(需配置环境变量:`TULING_API_KEY`) 28 | 29 | ## 使用说明 30 | 31 | 需要有微信公众号的帐号,没有的请戳[微信公众号申请](https://mp.weixin.qq.com/cgi-bin/readtemplate?t=register/step1_tmpl&lang=zh_CN) 32 | 33 | 内容响应来源的优先级`自定义关键字 > OpenAI > 通义千问 > 图灵机器人` 34 | 35 | 在微信公众号后台配置回调URL为/weChat/receiveMessage>,其中``替换成你自己的域名,token与`config.yml`里面配置的保持一致即可 36 | 37 | ### 火山方舟(豆包) 38 | 39 | 如果需要使用火山方舟的回复内容则需要 40 | [创建火山方舟的API Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey) 41 | 和 42 | [创建接入点](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint/create) 43 | 并配置在启动参数或者环境变量中 44 | 45 | ### OpenAI 46 | 47 | 1. 如果需要使用OpenAI的回复内容则需要[创建OpenAI的API Key](https://platform.openai.com/account/api-keys)并配置在启动参数或者环境变量中 48 | 2. 可以通过配置启动参数或者环境变量`OPENAI_SERVER_URL`指定访问OpenAI服务的baseUrl 49 | 3. 可以通过配置启动参数或者环境变量`OPENAI_PROXY`使用代理服务访问OpenAI 50 | 51 | ### 通义千问 52 | 53 | 如果需要使用通义千问的回复内容则需要[创建通义千问的API Key](https://bailian.console.aliyun.com/#/api_key)并配置在启动参数或者环境变量中 54 | 55 | ### 图灵机器人 56 | 57 | 如果需要使用图灵机器人的回复内容则需要[注册图灵机器人帐号](http://tuling123.com/register/email.jhtml)获取相应的ApiKey并配置在启动参数或者环境变量中 58 | 59 | ## 本地开发 60 | 61 | ### GoLand 62 | 63 | 需要配置Program Arguments为`-config config.yml`,然后运行main.go 64 | 65 | ### VS Code 66 | 67 | 可以直接使用`launch.json`的配置,里面还包含了环境变量的配置直接设置即可 68 | 69 | ## 编译运行 70 | 71 | ### 通过Makefile构建 72 | 73 | 构建适合当前系统的可执行文件 74 | 75 | ```shell 76 | make 77 | ``` 78 | 79 | 构建指定平台架构的可执行文件 80 | 81 | ```shell 82 | make linux_amd64 83 | ``` 84 | 85 | 编译全平台的可执行文件 86 | 87 | ```shell 88 | make all 89 | ``` 90 | 91 | 生成的可执行文件在`bin`目录下,执行`./bin/weChatRobot_darwin_arm64 -config config.yml`启动运行,其中`_darwin_arm64`后缀不同系统架构不一样 92 | 93 | ## Docker运行 94 | 95 | 构建适用于当前操作系统架构的镜像 96 | 97 | ```shell 98 | docker build --no-cache -t wechatrobot-go:latest . 99 | ``` 100 | 101 | 构建指定架构的镜像 102 | 103 | ```shell 104 | docker buildx build --no-cache -t wechatrobot-go:latest --platform=linux/amd64 -o type=docker . 105 | ``` 106 | 107 | 后台启动镜像 108 | 109 | ```shell 110 | docker run --name wechatrobot-go -p 8080:8080 -d wechatrobot-go:latest 111 | ``` 112 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "github.com/gin-gonic/gin" 10 | "html/template" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "path" 15 | "time" 16 | "weChatRobot-go/pkg/controller" 17 | "weChatRobot-go/pkg/logger" 18 | "weChatRobot-go/pkg/model" 19 | "weChatRobot-go/pkg/provider" 20 | "weChatRobot-go/pkg/service" 21 | ) 22 | 23 | //go:embed static/templates 24 | var templateFS embed.FS 25 | 26 | //go:embed static/images 27 | var imagesFS embed.FS 28 | 29 | //go:embed static/keyword.json 30 | var keywordBytes []byte 31 | 32 | func main() { 33 | var configFile string 34 | flag.StringVar(&configFile, "config", "", "配置文件") 35 | flag.Parse() 36 | 37 | if err := runApp(configFile); err != nil { 38 | logger.FatalWithErr(err, "process config error") 39 | } 40 | } 41 | 42 | func runApp(configFile string) error { 43 | var config *model.Config 44 | var err error 45 | if config, err = getConfig(configFile); err != nil { 46 | return err 47 | } 48 | 49 | logger.SetLevel(config.LoggerConfig.Level) 50 | 51 | service.InitKeywordMap(keywordBytes) 52 | 53 | router := setupRouter(config) 54 | srv := &http.Server{ 55 | Addr: fmt.Sprintf(":%d", config.AppConfig.Port), 56 | Handler: router, 57 | } 58 | 59 | //启动新的协程处理端口监听事件 60 | go func() { 61 | logger.Info("Listening and serving HTTP", "addr", "http://127.0.0.1"+srv.Addr) 62 | if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 63 | logger.FatalWithErr(err, "Server startup failed") 64 | } 65 | }() 66 | 67 | // 等待中断信号 68 | quit := make(chan os.Signal) 69 | signal.Notify(quit, os.Interrupt) 70 | <-quit 71 | logger.Info("Shutdown Server") 72 | 73 | // 配置一个5秒自动超时关闭的context 74 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 75 | // 如果context提前结束,则需要保证cancel方法能被执行 76 | defer cancel() 77 | // 调用Server的Shutdown方法优雅地停止服务(达到超时时间返回或者提前返回) 78 | if err = srv.Shutdown(ctx); err != nil { 79 | logger.FatalWithErr(err, "Server Shutdown failed") 80 | } 81 | logger.Info("Server gracefully stopped") 82 | 83 | return nil 84 | } 85 | 86 | func getConfig(configFile string) (*model.Config, error) { 87 | if configFile == "" { 88 | return nil, fmt.Errorf("config file not specified") 89 | } 90 | 91 | fileExt := path.Ext(configFile) 92 | if fileExt == ".yml" || fileExt == ".yaml" { 93 | return provider.NewFile(configFile).RetrieveConfig() 94 | } else { 95 | return nil, fmt.Errorf("config file only support .yml or .yaml format") 96 | } 97 | } 98 | 99 | func setupRouter(config *model.Config) *gin.Engine { 100 | gin.SetMode(config.AppConfig.Mode) 101 | 102 | router := gin.Default() 103 | //模板文件 104 | templates := template.Must(template.New("").ParseFS(templateFS, "static/templates/*.html")) 105 | router.SetHTMLTemplate(templates) 106 | //静态文件 107 | router.StaticFS("/public", http.FS(imagesFS)) 108 | 109 | router.GET("/", controller.IndexHandler) 110 | 111 | ws := controller.NewMessageController(&config.WechatConfig) 112 | weChatGroup := router.Group("weChat") 113 | { 114 | //签名回调 115 | weChatGroup.GET("/receiveMessage", ws.ReceiveMessage) 116 | //接收发送给公众号的消息 117 | weChatGroup.POST("/receiveMessage", ws.ReceiveMessage) 118 | } 119 | return router 120 | } 121 | -------------------------------------------------------------------------------- /cmd/static/images/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinDai/weChatRobot-go/9d4228aae4be2bbc18470cd3005085e8369012a9/cmd/static/images/qrcode.jpg -------------------------------------------------------------------------------- /cmd/static/keyword.json: -------------------------------------------------------------------------------- 1 | { 2 | "你是谁": { 3 | "type": "text", 4 | "Content": "我是小明的私人管家" 5 | }, 6 | "JVM参数": { 7 | "type": "news", 8 | "Articles": [ 9 | { 10 | "Title": "JVM参数使用手册", 11 | "Description": "JVM提供了大量的参数配置,可以通过配置这些参数对JVM进行调优、记录GC日志等等", 12 | "PicUrl": "http://mmbiz.qpic.cn/sz_mmbiz_jpg/QLaVCssAVyhFQIq66BXZ1gM26ibEziax8gV8mo66DDZ0icIf9MwqHeRbiaStsBuCT1wniajakTMjjgia4SM28SVemb1A/0?wx_fmt=jpeg", 13 | "Url": "https://mp.weixin.qq.com/s/65F6JztQxvxqO0beNbklIw" 14 | } 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /cmd/static/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 微信机器人 6 | 7 | 8 |
9 |

欢迎扫码关注,体验智能回复机器人

10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | application: 2 | port: 8080 #监听端口 3 | mode: release #GIN启动模式,支持 test debug release 4 | wechat: 5 | token: "atming_" #对应微信公众号设置里面的Token 6 | logger: 7 | level: INFO #日志级别,支持 DEBUG INFO WARN ERROR -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module weChatRobot-go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/otiai10/openaigo v1.1.0 8 | github.com/tidwall/gjson v1.17.1 9 | github.com/volcengine/volcengine-go-sdk v1.0.177 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/bytedance/sonic v1.9.1 // indirect 15 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.14.0 // indirect 21 | github.com/goccy/go-json v0.10.2 // indirect 22 | github.com/google/uuid v1.3.0 // indirect 23 | github.com/jmespath/go-jmespath v0.4.0 // indirect 24 | github.com/json-iterator/go v1.1.12 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 26 | github.com/leodido/go-urn v1.2.4 // indirect 27 | github.com/mattn/go-isatty v0.0.19 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 31 | github.com/tidwall/match v1.1.1 // indirect 32 | github.com/tidwall/pretty v1.2.1 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.2.11 // indirect 35 | github.com/volcengine/volc-sdk-golang v1.0.23 // indirect 36 | golang.org/x/arch v0.3.0 // indirect 37 | golang.org/x/crypto v0.9.0 // indirect 38 | golang.org/x/net v0.10.0 // indirect 39 | golang.org/x/sys v0.8.0 // indirect 40 | golang.org/x/text v0.9.0 // indirect 41 | google.golang.org/protobuf v1.30.0 // indirect 42 | gopkg.in/yaml.v2 v2.2.8 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 4 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 5 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 6 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 7 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 8 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 10 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 16 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 17 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 18 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 19 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 20 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 21 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 22 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 23 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 24 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 26 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 27 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 28 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 29 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 30 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 31 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 32 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 33 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 34 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 35 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 38 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 39 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 40 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 41 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 42 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 43 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 44 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 45 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 46 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 48 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 49 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 51 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 54 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 55 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 56 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 57 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 58 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 59 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 60 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 61 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 62 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 63 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 64 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 65 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 68 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 69 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 70 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 71 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 72 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 73 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 76 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 77 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 78 | github.com/otiai10/mint v1.4.1 h1:HOVBfKP1oXIc0wWo9hZ8JLdZtyCPWqjvmFDuVZ0yv2Y= 79 | github.com/otiai10/mint v1.4.1/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= 80 | github.com/otiai10/openaigo v1.1.0 h1:zRvGBqZUW5PCMgdkJNsPVTBd8tOLCMTipXE5wD2pdTg= 81 | github.com/otiai10/openaigo v1.1.0/go.mod h1:792bx6AWTS61weDi2EzKpHHnTF4eDMAlJ5GvAk/mgPg= 82 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 83 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 84 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 90 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 91 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 92 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 93 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 96 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 97 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 98 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 99 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 100 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 101 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 102 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 103 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 104 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 105 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 106 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 107 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 108 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 109 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 110 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 111 | github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= 112 | github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= 113 | github.com/volcengine/volcengine-go-sdk v1.0.177 h1:Z5D8BZAR1ilH7bLtRjBVP/I0QOIk7G/xuLvjeSJIax0= 114 | github.com/volcengine/volcengine-go-sdk v1.0.177/go.mod h1:gfEDc1s7SYaGoY+WH2dRrS3qiuDJMkwqyfXWCa7+7oA= 115 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 116 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 117 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 118 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 119 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 120 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 121 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 122 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 123 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 124 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 125 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 129 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 130 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 131 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 132 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 140 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 142 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 143 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 144 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 145 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 146 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 147 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 148 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 149 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 151 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 152 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 153 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 154 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 155 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 156 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 157 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 158 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 159 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 160 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 161 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 162 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 163 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 164 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 165 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 166 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 167 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 168 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 169 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 172 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 173 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 175 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 178 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 180 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 181 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 182 | -------------------------------------------------------------------------------- /pkg/controller/index_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "weChatRobot-go/pkg/logger" 8 | "weChatRobot-go/pkg/model" 9 | "weChatRobot-go/pkg/service" 10 | ) 11 | 12 | func IndexHandler(c *gin.Context) { 13 | c.HTML(http.StatusOK, "index.html", nil) 14 | } 15 | 16 | type MessageController struct { 17 | wechatService *service.WechatService 18 | } 19 | 20 | func NewMessageController(wc *model.WechatConfig) *MessageController { 21 | wechatService := service.NewWechatService(wc) 22 | return &MessageController{ 23 | wechatService: wechatService, 24 | } 25 | } 26 | 27 | // ReceiveMessage 收到微信回调信息 28 | func (mc *MessageController) ReceiveMessage(c *gin.Context) { 29 | if c.Request.Method == "GET" { 30 | signature := c.Query("signature") 31 | timestamp := c.Query("timestamp") 32 | nonce := c.Query("nonce") 33 | if mc.wechatService.CheckSignature(signature, timestamp, nonce) { 34 | _, _ = fmt.Fprint(c.Writer, c.Query("echostr")) 35 | } else { 36 | _, _ = fmt.Fprint(c.Writer, "你是谁?你想干嘛?") 37 | } 38 | } else { 39 | var reqMessage model.ReqMessage 40 | if err := c.ShouldBindXML(&reqMessage); err != nil { 41 | _, _ = fmt.Fprint(c.Writer, "系统处理消息异常") 42 | logger.Error(err, "解析XML出错") 43 | return 44 | } 45 | 46 | logger.Info("收到消息", "reqMessage", reqMessage) 47 | respXmlStr := mc.wechatService.GetResponseMessage(reqMessage) 48 | logger.Info("响应消息", "respXmlStr", respXmlStr) 49 | 50 | _, _ = fmt.Fprint(c.Writer, respXmlStr) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | var lvl slog.LevelVar 10 | 11 | func init() { 12 | lvl.Set(slog.LevelInfo) 13 | opts := slog.HandlerOptions{ 14 | //AddSource: true, 15 | Level: &lvl, 16 | } 17 | slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &opts))) 18 | } 19 | 20 | func SetLevel(level string) { 21 | level = strings.ToUpper(level) 22 | switch level { 23 | case slog.LevelDebug.String(): 24 | lvl.Set(slog.LevelDebug) 25 | case slog.LevelInfo.String(): 26 | lvl.Set(slog.LevelInfo) 27 | case slog.LevelWarn.String(): 28 | lvl.Set(slog.LevelWarn) 29 | case slog.LevelError.String(): 30 | lvl.Set(slog.LevelError) 31 | } 32 | } 33 | 34 | func Debug(msg string, v ...any) { 35 | slog.Debug(msg, v...) 36 | } 37 | 38 | func Info(msg string, v ...any) { 39 | slog.Info(msg, v...) 40 | } 41 | 42 | func Warn(msg string, v ...any) { 43 | slog.Warn(msg, v...) 44 | } 45 | 46 | func Error(err error, msg string, v ...any) { 47 | slog.Error(msg, append([]interface{}{"err", err}, v...)...) 48 | } 49 | 50 | func FatalWithErr(err error, format string, v ...any) { 51 | Error(err, format, v...) 52 | os.Exit(1) 53 | } 54 | 55 | func Fatal(format string, v ...any) { 56 | Error(nil, format, v...) 57 | os.Exit(1) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestLogger(t *testing.T) { 9 | SetLevel("DEBUG") 10 | Debug("this is a debug msg", "key1", "value1", "key2", "value2") 11 | Info("this is a info msg", "key1", "value1", "key2", "value2") 12 | Warn("this is a warn msg", "key1", "value1", "key2", "value2") 13 | Error(fmt.Errorf("error reason"), "this is a error msg", "key1", "value1", "key2", "value2") 14 | FatalWithErr(fmt.Errorf("fatal reason"), "this is a fatal msg", "key1", "value1", "key2", "value2") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/model/config.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Config struct { 4 | AppConfig AppConfig `yaml:"application"` 5 | WechatConfig WechatConfig `yaml:"wechat"` 6 | LoggerConfig LoggerConfig `yaml:"logger"` 7 | } 8 | 9 | type AppConfig struct { 10 | Port int `yaml:"port"` 11 | Mode string `yaml:"mode"` 12 | } 13 | 14 | type WechatConfig struct { 15 | Token string `yaml:"token"` 16 | } 17 | 18 | type LoggerConfig struct { 19 | Level string `yaml:"level"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "encoding/xml" 4 | 5 | const MsgTypeEvent = "event" 6 | const MsgTypeText = "text" 7 | const MsgTypeNews = "news" 8 | 9 | const EventTypeSubscribe = "subscribe" 10 | const EventTypeUnsubscribe = "unsubscribe" 11 | 12 | // ReqMessage 接收消息结构体 13 | type ReqMessage struct { 14 | ToUserName string 15 | FromUserName string 16 | CreateTime int64 17 | MsgType string 18 | Event string 19 | Content string 20 | MsgId int64 21 | } 22 | 23 | // RespMessage 响应消息基础结构体 24 | type RespMessage struct { 25 | ToUserName string 26 | FromUserName string 27 | CreateTime int64 28 | MsgType string 29 | // 若不标记XMLName, 则解析后的xml名为该结构体的名称 30 | XMLName xml.Name `xml:"xml"` 31 | } 32 | 33 | // RespTextMessage 文本响应消息结构体 34 | type RespTextMessage struct { 35 | RespMessage 36 | Content string 37 | } 38 | 39 | // RespNewsMessage 图文响应消息结构体 40 | type RespNewsMessage struct { 41 | RespMessage 42 | ArticleCount int 43 | Articles []ArticleItem 44 | } 45 | 46 | // ArticleItem 图文结构体 47 | type ArticleItem struct { 48 | Article Article 49 | } 50 | 51 | // Article 图文结构体 52 | type Article struct { 53 | Title string 54 | Description string 55 | PicUrl string 56 | Url string 57 | XMLName xml.Name `xml:"item"` 58 | } 59 | -------------------------------------------------------------------------------- /pkg/model/tuling.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //图灵机器人响应码常量 4 | 5 | const ParamErrCode = 4000 //请求参数格式错误 6 | const NoResultCode = 5000 //无解析结果 7 | const NoApiTimesCode = 4003 //该apikey没有可用请求次数 8 | const SuccessCode = 0 9 | 10 | const TextResultType = "text" 11 | 12 | type ReqParam struct { 13 | ReqType int `json:"reqType"` 14 | Perception Perception `json:"perception"` 15 | UserInfo UserInfo `json:"userInfo"` 16 | } 17 | 18 | type Perception struct { 19 | InputText InputText `json:"inputText"` 20 | } 21 | 22 | type InputText struct { 23 | Text string `json:"text"` 24 | } 25 | 26 | type UserInfo struct { 27 | ApiKey string `json:"apiKey"` 28 | UserId string `json:"userId"` 29 | } 30 | -------------------------------------------------------------------------------- /pkg/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "weChatRobot-go/pkg/model" 5 | ) 6 | 7 | type Provider interface { 8 | RetrieveConfig() (*model.Config, error) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/provider/yml_config_provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "errors" 5 | "gopkg.in/yaml.v3" 6 | "os" 7 | "weChatRobot-go/pkg/model" 8 | ) 9 | 10 | type ymlConfigProvider struct { 11 | filePath string 12 | } 13 | 14 | // NewFile returns a new Provider that reads the configuration from the given file. 15 | func NewFile(filePath string) Provider { 16 | return &ymlConfigProvider{ 17 | filePath: filePath, 18 | } 19 | } 20 | 21 | func (ycp *ymlConfigProvider) RetrieveConfig() (*model.Config, error) { 22 | if ycp.filePath == "" { 23 | return nil, errors.New("config file not specified") 24 | } 25 | 26 | var config model.Config 27 | 28 | // 读取 YAML 文件内容 29 | data, err := os.ReadFile(ycp.filePath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = yaml.Unmarshal(data, &config) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &config, nil 39 | 40 | } 41 | -------------------------------------------------------------------------------- /pkg/service/keyword_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/tidwall/gjson" 5 | "weChatRobot-go/pkg/logger" 6 | ) 7 | 8 | var keywordResultMap gjson.Result 9 | 10 | func InitKeywordMap(keywordBytes []byte) { 11 | if !gjson.ValidBytes(keywordBytes) { 12 | logger.Warn("关键字JSON文件格式不正确,跳过解析") 13 | return 14 | } 15 | 16 | result := gjson.ParseBytes(keywordBytes) 17 | keywordResultMap = result 18 | 19 | for k, v := range keywordResultMap.Map() { 20 | logger.Info("初始化关键字map", k, v.Value()) 21 | } 22 | } 23 | 24 | func GetResultByKeyword(keyword string) gjson.Result { 25 | result := keywordResultMap.Get(keyword) 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /pkg/service/wechat_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "encoding/xml" 7 | "sort" 8 | "strings" 9 | "weChatRobot-go/pkg/logger" 10 | "weChatRobot-go/pkg/model" 11 | thirdparty "weChatRobot-go/pkg/third-party" 12 | "weChatRobot-go/pkg/third-party/ark" 13 | "weChatRobot-go/pkg/third-party/dashscope" 14 | "weChatRobot-go/pkg/third-party/openai" 15 | "weChatRobot-go/pkg/third-party/tuling" 16 | "weChatRobot-go/pkg/util" 17 | ) 18 | 19 | type WechatService struct { 20 | config *model.WechatConfig 21 | assistants []thirdparty.AssistantService 22 | } 23 | 24 | func NewWechatService(wc *model.WechatConfig) *WechatService { 25 | return &WechatService{ 26 | config: wc, 27 | assistants: []thirdparty.AssistantService{ 28 | ark.NewDoubao(), 29 | openai.NewOpenAI(), 30 | dashscope.NewDashscope(), 31 | tuling.NewTuling(), 32 | }, 33 | } 34 | } 35 | 36 | // CheckSignature 校验签名 37 | func (ws *WechatService) CheckSignature(signature, timestamp, nonce string) bool { 38 | if signature == "" || timestamp == "" || nonce == "" { 39 | return false 40 | } 41 | 42 | arr := []string{ws.config.Token, timestamp, nonce} 43 | // 将token、timestamp、nonce三个参数进行字典序排序 44 | sort.Strings(arr) 45 | //拼接字符串 46 | content := strings.Join(arr, "") 47 | //sha1签名 48 | sha := sha1.New() 49 | sha.Write([]byte(content)) 50 | sha1Value := hex.EncodeToString(sha.Sum(nil)) 51 | 52 | return signature == sha1Value 53 | } 54 | 55 | func (ws *WechatService) GetResponseMessage(reqMessage model.ReqMessage) string { 56 | var respMessage interface{} 57 | if reqMessage.MsgType == model.MsgTypeEvent { 58 | respMessage = getRespMessageByEvent(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Event) 59 | } else if reqMessage.MsgType == model.MsgTypeText { 60 | respMessage = getRespMessageByKeyword(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content) 61 | 62 | for _, ai := range ws.assistants { 63 | respMessage = ai.ProcessText(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content) 64 | if respMessage != nil { 65 | break 66 | } 67 | } 68 | } else { 69 | respMessage = util.BuildRespTextMessage(reqMessage.ToUserName, reqMessage.FromUserName, "我只对文字感兴趣[悠闲]") 70 | } 71 | 72 | if respMessage == nil { 73 | //最后兜底,如果没有响应,则返回输入的文字 74 | respMessage = util.BuildRespTextMessage(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content) 75 | } 76 | 77 | var respXmlStr []byte 78 | var err error 79 | if respXmlStr, err = xml.Marshal(&respMessage); err != nil { 80 | logger.Error(err, "XML编码出错") 81 | return "" 82 | } 83 | 84 | return string(respXmlStr) 85 | } 86 | 87 | func getRespMessageByEvent(fromUserName, toUserName, event string) interface{} { 88 | if event == model.EventTypeSubscribe { 89 | return util.BuildRespTextMessage(fromUserName, toUserName, "谢谢关注!可以开始跟我聊天啦😁") 90 | } else if event == model.EventTypeUnsubscribe { 91 | logger.Info("用户取消了订阅", "fromUserName", fromUserName) 92 | } 93 | return nil 94 | } 95 | 96 | func getRespMessageByKeyword(fromUserName, toUserName, keyword string) interface{} { 97 | v := GetResultByKeyword(keyword) 98 | if v.Exists() { 99 | msgType := v.Get("type").String() 100 | if msgType == "" { 101 | return nil 102 | } 103 | 104 | if msgType == model.MsgTypeText { 105 | content := v.Get("Content").String() 106 | return util.BuildRespTextMessage(fromUserName, toUserName, content) 107 | } else if msgType == model.MsgTypeNews { 108 | articleArray := v.Get("Articles").Array() 109 | var articleLength = len(articleArray) 110 | if articleLength == 0 { 111 | return nil 112 | } 113 | 114 | var articles = make([]model.ArticleItem, articleLength) 115 | for i, articleJson := range articleArray { 116 | var article model.Article 117 | article.Title = articleJson.Get("Title").String() 118 | article.Description = articleJson.Get("Description").String() 119 | article.PicUrl = articleJson.Get("PicUrl").String() 120 | article.Url = articleJson.Get("Url").String() 121 | 122 | var articleItem model.ArticleItem 123 | articleItem.Article = article 124 | articles[i] = articleItem 125 | } 126 | return util.BuildRespNewsMessage(fromUserName, toUserName, articles) 127 | } 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /pkg/third-party/ark/doubao.go: -------------------------------------------------------------------------------- 1 | package ark 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "github.com/volcengine/volcengine-go-sdk/service/arkruntime" 7 | "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" 8 | "github.com/volcengine/volcengine-go-sdk/volcengine" 9 | "net/http" 10 | "os" 11 | "weChatRobot-go/pkg/logger" 12 | "weChatRobot-go/pkg/util" 13 | ) 14 | 15 | type Doubao struct { 16 | client *arkruntime.Client 17 | endpointId string 18 | } 19 | 20 | func NewDoubao() *Doubao { 21 | apiKey := os.Getenv("ARK_API_KEY") 22 | endpointId := os.Getenv("ARK_ENDPOINT_ID") 23 | var client *arkruntime.Client 24 | if apiKey != "" && endpointId != "" { 25 | client = arkruntime.NewClientWithApiKey( 26 | apiKey, 27 | arkruntime.WithBaseUrl("https://ark.cn-beijing.volces.com/api/v3"), 28 | arkruntime.WithRegion("cn-beijing"), 29 | arkruntime.WithHTTPClient(&http.Client{ 30 | Transport: &http.Transport{ 31 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 32 | }, 33 | }), 34 | ) 35 | } 36 | 37 | return &Doubao{ 38 | client: client, 39 | endpointId: endpointId, 40 | } 41 | } 42 | 43 | func (doubao *Doubao) ProcessText(fromUserName, toUserName, content string) interface{} { 44 | if doubao.client == nil { 45 | return nil 46 | } 47 | 48 | request := model.ChatCompletionRequest{ 49 | Model: doubao.endpointId, 50 | Messages: []*model.ChatCompletionMessage{ 51 | { 52 | Role: model.ChatMessageRoleSystem, 53 | Content: &model.ChatCompletionMessageContent{ 54 | StringValue: volcengine.String("你是一个AI助手,尽量保证回复内容在200个字符以内"), 55 | }, 56 | }, 57 | { 58 | Role: model.ChatMessageRoleUser, 59 | Content: &model.ChatCompletionMessageContent{ 60 | StringValue: volcengine.String(content), 61 | }, 62 | }, 63 | }, 64 | } 65 | 66 | var response model.ChatCompletionResponse 67 | var err error 68 | if response, err = doubao.client.CreateChatCompletion(context.Background(), request); err != nil { 69 | logger.Error(err, "doubao Completion error") 70 | return nil 71 | } 72 | return util.BuildRespTextMessage(fromUserName, toUserName, *response.Choices[0].Message.Content.StringValue) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/third-party/ark/doubao_test.go: -------------------------------------------------------------------------------- 1 | package ark_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "weChatRobot-go/pkg/third-party/ark" 7 | ) 8 | 9 | func TestGetRespMessage(t *testing.T) { 10 | doubao := ark.NewDoubao() 11 | var respMessage = doubao.ProcessText("aaa", "bbb", "什么是GPT") 12 | fmt.Printf("respMessage:%v", respMessage) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/third-party/assistant_service.go: -------------------------------------------------------------------------------- 1 | package third_party 2 | 3 | type AssistantService interface { 4 | ProcessText(fromUserName, toUserName, content string) interface{} 5 | } 6 | -------------------------------------------------------------------------------- /pkg/third-party/dashscope/client.go: -------------------------------------------------------------------------------- 1 | package dashscope 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "weChatRobot-go/pkg/logger" 10 | ) 11 | 12 | const generationUrl = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" 13 | 14 | type Client struct { 15 | apiKey string 16 | httpClient *http.Client 17 | } 18 | 19 | type GenerationParam struct { 20 | Model string `json:"model"` 21 | Input Input `json:"input"` 22 | } 23 | 24 | type Input struct { 25 | Messages []Message `json:"messages"` 26 | } 27 | 28 | type Message struct { 29 | Role string `json:"role"` 30 | Content string `json:"content"` 31 | } 32 | 33 | type GenerationResult struct { 34 | Output struct { 35 | Text string `json:"text"` 36 | FinishReason string `json:"finish_reason"` 37 | } `json:"output"` 38 | } 39 | 40 | func (client *Client) call(param *GenerationParam) (*GenerationResult, error) { 41 | // 转换为JSON格式 42 | payload, err := json.Marshal(param) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // 构建请求 48 | req, err := http.NewRequest("POST", generationUrl, bytes.NewBuffer(payload)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | // 设置请求头部 54 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.apiKey)) 55 | req.Header.Set("Content-Type", "application/json") 56 | 57 | resp, err := client.httpClient.Do(req) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer resp.Body.Close() 62 | 63 | if resp.StatusCode != http.StatusOK { 64 | var errorResponse struct { 65 | Code string `json:"code"` 66 | Message string `json:"message"` 67 | } 68 | 69 | err = json.NewDecoder(resp.Body).Decode(&errorResponse) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return nil, fmt.Errorf("API error: %s - %s", errorResponse.Code, errorResponse.Message) 75 | } 76 | 77 | result := &GenerationResult{} 78 | var respBytes []byte 79 | if respBytes, err = io.ReadAll(resp.Body); err != nil { 80 | logger.Error(err, "读取通义千问响应内容报错") 81 | return nil, nil 82 | } 83 | err = json.Unmarshal(respBytes, &result) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return result, nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/third-party/dashscope/dashscope.go: -------------------------------------------------------------------------------- 1 | package dashscope 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "os" 7 | "weChatRobot-go/pkg/logger" 8 | "weChatRobot-go/pkg/util" 9 | ) 10 | 11 | type Dashscope struct { 12 | client *Client 13 | } 14 | 15 | func NewDashscope() *Dashscope { 16 | apiKey := os.Getenv("DASHSCOPE_API_KEY") 17 | var client *Client 18 | if apiKey != "" { 19 | client = &Client{ 20 | apiKey: apiKey, 21 | httpClient: &http.Client{ 22 | Transport: &http.Transport{ 23 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 24 | }, 25 | }, 26 | } 27 | } 28 | 29 | return &Dashscope{ 30 | client: client, 31 | } 32 | } 33 | 34 | func (d *Dashscope) ProcessText(fromUserName, toUserName, content string) interface{} { 35 | if d.client == nil { 36 | return nil 37 | } 38 | 39 | param := &GenerationParam{ 40 | Model: "qwen-turbo", 41 | Input: Input{ 42 | Messages: []Message{ 43 | {Role: "system", Content: "你是一个AI助手,尽量保证回复内容在200个字符以内"}, 44 | {Role: "user", Content: content}, 45 | }, 46 | }, 47 | } 48 | 49 | var result *GenerationResult 50 | var err error 51 | if result, err = d.client.call(param); err != nil { 52 | logger.Error(err, "dashscope generation error") 53 | return nil 54 | } 55 | 56 | return util.BuildRespTextMessage(fromUserName, toUserName, result.Output.Text) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/third-party/dashscope/dashscope_test.go: -------------------------------------------------------------------------------- 1 | package dashscope 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "weChatRobot-go/pkg/util" 7 | ) 8 | 9 | func TestGetRespMessage(t *testing.T) { 10 | dashscope := NewDashscope() 11 | var respMessage = dashscope.ProcessText("aaa", "bbb", "什么是GPT") 12 | fmt.Printf("respMessage:%v", util.ToJsonString(respMessage)) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/third-party/openai/openai.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "context" 5 | "github.com/otiai10/openaigo" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "weChatRobot-go/pkg/logger" 10 | "weChatRobot-go/pkg/util" 11 | ) 12 | 13 | type OpenAI struct { 14 | client *openaigo.Client 15 | } 16 | 17 | func NewOpenAI() *OpenAI { 18 | apiKey := os.Getenv("OPENAI_API_KEY") 19 | var client *openaigo.Client 20 | if apiKey != "" { 21 | baseDomain := os.Getenv("OPENAI_BASE_DOMAIN") 22 | if baseDomain != "" && !util.ValidateAddress(baseDomain) { 23 | logger.Fatal("OPENAI_BASE_DOMAIN is not valid", "openaiBaseDomain", baseDomain) 24 | } 25 | 26 | proxyAddress := os.Getenv("OPENAI_PROXY") 27 | if proxyAddress != "" && !util.ValidateAddress(proxyAddress) { 28 | logger.Fatal("OPENAI_PROXY is not valid", "openaiBaseDomain", baseDomain) 29 | } 30 | 31 | client = openaigo.NewClient(apiKey) 32 | if baseDomain != "" { 33 | client.BaseURL = "https://" + baseDomain + "/v1" 34 | } 35 | if proxyAddress != "" { 36 | proxyUrl, _ := url.Parse("http://" + proxyAddress) 37 | transport := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} 38 | client.HTTPClient = &http.Client{Transport: transport} 39 | } 40 | } 41 | 42 | return &OpenAI{ 43 | client: client, 44 | } 45 | } 46 | 47 | func (openai *OpenAI) ProcessText(fromUserName, toUserName, content string) interface{} { 48 | if openai.client == nil { 49 | return nil 50 | } 51 | 52 | request := openaigo.ChatCompletionRequestBody{ 53 | Model: "gpt-3.5-turbo", 54 | Messages: []openaigo.ChatMessage{ 55 | {Role: "system", Content: "你是一个AI助手,尽量保证回复内容在200个字符以内"}, 56 | {Role: "user", Content: content}, 57 | }, 58 | } 59 | ctx := context.Background() 60 | 61 | var response openaigo.ChatCompletionResponse 62 | var err error 63 | if response, err = openai.client.Chat(ctx, request); err != nil { 64 | logger.Error(err, "GPT Completion error") 65 | return nil 66 | } 67 | return util.BuildRespTextMessage(fromUserName, toUserName, response.Choices[0].Message.Content) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/third-party/openai/openai_test.go: -------------------------------------------------------------------------------- 1 | package openai_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "weChatRobot-go/pkg/third-party/openai" 7 | ) 8 | 9 | func TestGetRespMessage(t *testing.T) { 10 | chatGPT := openai.NewOpenAI() 11 | var respMessage = chatGPT.ProcessText("aaa", "bbb", "什么是GPT") 12 | fmt.Printf("respMessage:%v", respMessage) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/third-party/tuling/tuling.go: -------------------------------------------------------------------------------- 1 | package tuling 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/tidwall/gjson" 8 | "io" 9 | "net/http" 10 | "os" 11 | "sync/atomic" 12 | "weChatRobot-go/pkg/logger" 13 | "weChatRobot-go/pkg/model" 14 | "weChatRobot-go/pkg/util" 15 | ) 16 | 17 | const tulingApiUrl = "https://openapi.tuling123.com/openapi/api/v2" 18 | 19 | type Tuling struct { 20 | apiKey string 21 | // 对微信传过来的userName做映射,因为有些userName的格式是图灵API不支持的 22 | userNameIdMap map[string]int32 23 | userIdAdder int32 24 | } 25 | 26 | func NewTuling() *Tuling { 27 | apiKey := os.Getenv("TULING_API_KEY") 28 | return &Tuling{ 29 | apiKey: apiKey, 30 | userNameIdMap: make(map[string]int32), 31 | userIdAdder: 0, 32 | } 33 | } 34 | 35 | // ProcessText 从图灵机器人获取响应消息 36 | func (t *Tuling) ProcessText(fromUserName, toUserName, content string) interface{} { 37 | if t.apiKey == "" { 38 | return nil 39 | } 40 | 41 | userId := t.getUserId(toUserName) 42 | req := model.ReqParam{ 43 | ReqType: 0, 44 | Perception: model.Perception{InputText: model.InputText{ 45 | Text: content, 46 | }}, 47 | UserInfo: model.UserInfo{ 48 | ApiKey: t.apiKey, 49 | UserId: fmt.Sprintf("%d", userId), 50 | }, 51 | } 52 | 53 | reqJsonBytes, _ := json.Marshal(req) 54 | reqJson := string(reqJsonBytes) 55 | logger.Info("请求图灵机器人", "reqJson", reqJson) 56 | 57 | var resp *http.Response 58 | var err error 59 | if resp, err = http.Post(tulingApiUrl, "application/json", bytes.NewReader(reqJsonBytes)); err != nil { 60 | logger.Error(err, "从图灵机器人获取响应内容报错") 61 | return nil 62 | } 63 | 64 | var respBytes []byte 65 | if respBytes, err = io.ReadAll(resp.Body); err != nil { 66 | logger.Error(err, "读取图灵机器人响应内容报错") 67 | return nil 68 | } 69 | 70 | respStr := string(respBytes) 71 | logger.Info("收到图灵机器人响应内容", "respStr", respStr) 72 | 73 | if !gjson.Valid(respStr) { 74 | logger.Warn("图灵机器人响应内容不是json格式,无法解析") 75 | return nil 76 | } 77 | 78 | respJson := gjson.Parse(respStr) 79 | code := respJson.Get("intent.code").Int() 80 | switch code { 81 | case model.ParamErrCode: 82 | return util.BuildRespTextMessage(fromUserName, toUserName, "我不是很理解你说的话") 83 | case model.NoResultCode: 84 | return util.BuildRespTextMessage(fromUserName, toUserName, "我竟无言以对!") 85 | case model.NoApiTimesCode: 86 | return util.BuildRespTextMessage(fromUserName, toUserName, "我今天已经说了太多话了,有点累,明天再来找我聊天吧!") 87 | case model.SuccessCode: 88 | var respTextMessage interface{} 89 | resultArray := respJson.Get("results").Array() 90 | for _, result := range resultArray { 91 | if result.Get("resultType").String() == model.TextResultType { 92 | valueMap := result.Get("values") 93 | respTextMessage = util.BuildRespTextMessage(fromUserName, toUserName, valueMap.Get("text").String()) 94 | break 95 | } 96 | } 97 | if respTextMessage != nil { 98 | return respTextMessage 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (t *Tuling) getUserId(userName string) int32 { 106 | if userId, ok := t.userNameIdMap[userName]; ok { 107 | return userId 108 | } else { 109 | userId := atomic.AddInt32(&t.userIdAdder, 1) 110 | t.userNameIdMap[userName] = userId 111 | return userId 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/util/address_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func ValidateAddress(address string) bool { 11 | parts := strings.Split(address, ":") 12 | if len(parts) > 2 { 13 | return false 14 | } 15 | 16 | host := parts[0] 17 | port := "" 18 | 19 | if len(parts) == 2 { 20 | port = parts[1] 21 | } 22 | 23 | // 校验Host部分 24 | if !isIP(host) && !isDomainName(host) { 25 | return false 26 | } 27 | 28 | // 校验Port部分 29 | if port != "" && !isPort(port) { 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | 36 | func isPort(port string) bool { 37 | portNum, err := strconv.Atoi(port) 38 | if err != nil { 39 | return false 40 | } 41 | 42 | if portNum < 0 || portNum > 65535 { 43 | return false 44 | } 45 | 46 | return true 47 | } 48 | 49 | func isIP(ip string) bool { 50 | if net.ParseIP(ip) == nil { 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | func isDomainName(domain string) bool { 57 | domainRegex := `^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$` 58 | match, _ := regexp.MatchString(domainRegex, domain) 59 | return match 60 | } 61 | -------------------------------------------------------------------------------- /pkg/util/json_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "encoding/json" 4 | 5 | func ToJsonString(v any) string { 6 | bytes, _ := json.Marshal(v) 7 | return string(bytes) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/util/message_util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | "weChatRobot-go/pkg/model" 6 | ) 7 | 8 | func BuildRespTextMessage(fromUserName, toUserName, content string) model.RespTextMessage { 9 | respMessage := model.RespTextMessage{ 10 | Content: content, 11 | } 12 | respMessage.FromUserName = fromUserName 13 | respMessage.ToUserName = toUserName 14 | respMessage.CreateTime = time.Now().Unix() 15 | respMessage.MsgType = "text" 16 | return respMessage 17 | } 18 | 19 | func BuildRespNewsMessage(fromUserName, toUserName string, articles []model.ArticleItem) model.RespNewsMessage { 20 | respMessage := model.RespNewsMessage{ 21 | ArticleCount: len(articles), 22 | Articles: articles, 23 | } 24 | respMessage.FromUserName = fromUserName 25 | respMessage.ToUserName = toUserName 26 | respMessage.CreateTime = time.Now().Unix() 27 | respMessage.MsgType = "news" 28 | return respMessage 29 | } 30 | --------------------------------------------------------------------------------