├── favicon.ico ├── .gitignore ├── log └── .gitignore ├── wechat.service ├── Makefile ├── config.json ├── robot.go ├── crypt.go ├── token.go ├── README.md ├── config.go ├── handlers.go ├── main.go └── LICENSE /favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.json 3 | 4 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.log 3 | 4 | -------------------------------------------------------------------------------- /wechat.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WeChat Public Accounts Service 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | WorkingDirectory={{ wechat-ai path }} 9 | ExecStart={{ wechat-ai path }}/wechat-ai 10 | Restart=abort 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | Bin = wechat-ai 2 | BinDir = $(shell pwd) 3 | Service = wechat.service 4 | 5 | .PHONY: all 6 | all: wechat-ai 7 | 8 | .PHONY: wechat-ai 9 | wechat-ai: 10 | go fmt && go build 11 | 12 | .PHONY: service 13 | service: 14 | sed -i "s|{{ wechat-ai path }}|$(BinDir)|" $(Service) 15 | @cp -f $(Service) /lib/systemd/system/ 16 | 17 | .PHONY: clean 18 | clean: 19 | go clean 20 | @rm -f log/*.log 21 | 22 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "listen": "127.0.0.1:8080", 3 | "log_file": "log/wechat.log", 4 | "log_format": "[WeChat] ${time_rfc3339}\t${remote_ip}\t${method}\t${uri}\t${status} ${referer} ${host}\n", 5 | "weixin": { 6 | "appid": "这里填写微信公众号AppID", 7 | "secret": "这里填写微信公众号AppSecret", 8 | "token": "这里填写微信公众号设置的Token", 9 | "encodingAESKey": "这里填写微信公众号上生成的encodingAESKey" 10 | }, 11 | "ai": { 12 | "greeting": "哨兵张嘎不知道您说的是什么,请重新提问", 13 | "url": "http://www.tuling123.com/openapi/api", 14 | "key": "这里填写图灵机器人网站上生成的ApiKey" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /robot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "github.com/levigross/grequests" 8 | ) 9 | 10 | // 机器人回复的数据的json绑定对象 11 | type aiReply struct { 12 | code int 13 | Text string `json:"text"` 14 | } 15 | 16 | // 对机器人API发送请求 17 | func tlAI(info string) string { 18 | tlURL := config.Ai.ApiUrl 19 | ro := &grequests.RequestOptions{ 20 | Params: map[string]string{ 21 | "key": config.Ai.ApiKey, 22 | "info": info, 23 | }, 24 | } 25 | 26 | r, err := grequests.Get(tlURL, ro) 27 | if err != nil { 28 | log.Println(err) 29 | return "" 30 | } 31 | 32 | defer r.Close() 33 | reply := new(aiReply) 34 | json.Unmarshal([]byte(r.String()), reply) 35 | return reply.Text 36 | } 37 | -------------------------------------------------------------------------------- /crypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/labstack/echo" 11 | ) 12 | 13 | // 对请求的参数做SHA1校验 14 | func makeSignature(timestamp, nonce string) string { 15 | sl := []string{config.Wx.Token, timestamp, nonce} 16 | sort.Strings(sl) 17 | s := sha1.New() 18 | io.WriteString(s, strings.Join(sl, "")) 19 | return fmt.Sprintf("%x", s.Sum(nil)) 20 | } 21 | 22 | // 检查url中的参数 23 | func validateUrl(ctx echo.Context) bool { 24 | timestamp := ctx.QueryParam("timestamp") 25 | nonce := ctx.QueryParam("nonce") 26 | signatureGen := makeSignature(timestamp, nonce) 27 | signatureIn := ctx.QueryParam("signature") 28 | 29 | if signatureGen != signatureIn { 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/levigross/grequests" 7 | ) 8 | 9 | type Token struct { 10 | AccessToken string `json:"access_token"` 11 | Expire string `json:"expires_in"` 12 | } 13 | 14 | // 获取access_token 15 | func (t *Token) Get() []byte { 16 | var args = map[string]string{ 17 | "appid": config.Wx.AppID, 18 | "secret": config.Wx.AppSecret, 19 | "grant_type": "client_credential", 20 | } 21 | 22 | ro := &grequests.RequestOptions{ 23 | Params: args, 24 | } 25 | 26 | res, _ := grequests.Get("https://api.weixin.qq.com/cgi-bin/token", ro) 27 | return res.Bytes() 28 | } 29 | 30 | func (t *Token) Parse(at []byte) (err error) { 31 | if err = json.Unmarshal(at, t); err != nil { 32 | return err 33 | } 34 | 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeChat-AI 2 | 3 | 微信公众号聊天机器人,类似于微信小冰的功能 4 | 5 | ## 项目特点 6 | 7 | - 使用了Echo框架作为Web服务 8 | - 使用了[silenceper/wechat](https://github.com/silenceper/wechat)微信SDK做微信消息处理 9 | - 使用了图灵机器人网站的AI接口 10 | - 使用JSON格式保存配置文件 11 | 12 | 13 | ## 安装 14 | 15 | ```bash 16 | git clone https://github.com/gnuos/wechat-ai 17 | cd wechat-ai 18 | go get -v . 19 | go build 20 | ``` 21 | 22 | ## 快速开始 23 | 24 | 在安装之后,会生成一个wechat-ai文件,修改config.json配置文件,把相关的参数的值替换为你自己的环境,然后在项目目录中运行./wechat-ai,就启动成功了。 25 | 如果你用的操作系统是通过systemd管理服务的,可以将 wechat.service 文件复制到/lib/systemd/system/目录下面,然后将文件中的wechat-ai执行路径修改为你的路径,再执行systemctl daemon-reload命令重新加载服务文件,就可以通过systemd管理wechat-ai了。 26 | 27 | 28 | ## 开发文档 29 | 30 | 由于本项目主要是基于[Echo](https://echo.labstack.com/)和[silenceper/wechat](https://github.com/silenceper/wechat)框架的,如果要修改本项目的代码或者做研究学习,请参考以上两个项目的文档。 31 | 32 | 33 | ## License 34 | 35 | Apache License, Version 2.0 36 | 37 | 38 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | // 配置文件的json绑定对象 10 | type Config struct { 11 | Listen string `json:"listen"` 12 | LogFile string `json:"log_file"` 13 | LogFormat string `json:"log_format"` 14 | Wx Wexin `json:"weixin"` 15 | Ai AI `json:"ai"` 16 | } 17 | 18 | type Wexin struct { 19 | AppID string `json:"appid"` 20 | AppSecret string `json:"secret"` 21 | Token string `json:"token"` 22 | EncodingAESKey string `json:"encodingAESKey"` 23 | } 24 | 25 | type AI struct { 26 | Greeting string `json:"greeting"` 27 | ApiUrl string `json:"url"` 28 | ApiKey string `json:"key"` 29 | } 30 | 31 | // 获取配置文件中的配置参数 32 | func ParseConfig(path string) (config *Config, err error) { 33 | file, err := os.Open(path) 34 | if err != nil { 35 | return 36 | } 37 | defer file.Close() 38 | data, err := ioutil.ReadAll(file) 39 | if err != nil { 40 | return 41 | } 42 | config = &Config{} 43 | if err = json.Unmarshal(data, config); err != nil { 44 | return nil, err 45 | } 46 | 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo" 8 | "github.com/silenceper/wechat" 9 | "github.com/silenceper/wechat/message" 10 | ) 11 | 12 | // 路由 web.HEAD("/*", Null) 13 | func Null(ctx echo.Context) error { 14 | return nil 15 | } 16 | 17 | // 路由 web.GET("/ip", GetIP) 18 | func GetIP(ctx echo.Context) error { 19 | var resp struct { 20 | IP string `json:"ip"` 21 | } 22 | resp.IP = ctx.RealIP() 23 | 24 | return ctx.JSON(http.StatusOK, &resp) 25 | 26 | } 27 | 28 | // 路由 web.Any("/", Default) 29 | func Default(ctx echo.Context) error { 30 | // 从配置文件获取微信的AppID和其他参数 31 | var wxConf = &wechat.Config{ 32 | AppID: config.Wx.AppID, 33 | AppSecret: config.Wx.AppSecret, 34 | Token: config.Wx.Token, 35 | EncodingAESKey: config.Wx.EncodingAESKey, 36 | } 37 | 38 | // 检查请求来源 39 | if !validateUrl(ctx) { 40 | log.Println("Wechat Service: this http request is not from Wechat platform!") 41 | return ctx.String(http.StatusForbidden, `{"message": 403}`) 42 | } 43 | 44 | if echostr := ctx.QueryParam("echostr"); echostr != "" { 45 | return ctx.String(http.StatusOK, echostr) 46 | } 47 | 48 | wc := wechat.NewWechat(wxConf) 49 | server := wc.GetServer(ctx.Request(), ctx.Response().Writer) 50 | 51 | //设置接收消息的处理方法 52 | server.SetMessageHandler(func(msg message.MixMessage) *message.Reply { 53 | text := message.NewText(tlAI(msg.Content)) 54 | return &message.Reply{message.MsgTypeText, text} 55 | }) 56 | 57 | //处理消息接收以及回复 58 | err := server.Serve() 59 | if err != nil { 60 | log.Println(err) 61 | return err 62 | } 63 | //发送回复的消息 64 | return server.Send() 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/labstack/echo" 8 | "github.com/labstack/echo/middleware" 9 | "github.com/labstack/gommon/log" 10 | ) 11 | 12 | var config *Config 13 | 14 | // 定义Web服务的Server头 15 | func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc { 16 | return func(c echo.Context) error { 17 | c.Response().Header().Set(echo.HeaderServer, "WeChat") 18 | return next(c) 19 | } 20 | } 21 | 22 | func main() { 23 | var configFile string 24 | var debug uint 25 | var err error 26 | 27 | // 从config.json文件中获取配置参数 28 | flag.StringVar(&configFile, "c", "config.json", "specify config file") 29 | flag.UintVar(&debug, "d", 0, "debug mode") 30 | flag.Parse() 31 | config, err = ParseConfig(configFile) 32 | if err != nil { 33 | panic("a vailid json config file must exist") 34 | } 35 | 36 | web := echo.New() 37 | 38 | web.Use(ServerHeader) 39 | 40 | // 设置日志文件格式 41 | loggerConfig := middleware.LoggerConfig{ 42 | Format: config.LogFormat, 43 | } 44 | if config.LogFile != "" { 45 | f, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | defer f.Close() 51 | 52 | loggerConfig.Output = f 53 | web.Logger.SetOutput(f) 54 | 55 | if debug == 1 { 56 | web.Debug = true 57 | } 58 | } else { 59 | web.Debug = true 60 | } 61 | 62 | web.Use(middleware.LoggerWithConfig(loggerConfig)) 63 | web.Use(middleware.Recover()) 64 | 65 | if web.Debug { 66 | web.Logger.SetLevel(log.DEBUG) 67 | } 68 | 69 | // 定义跨域请求所需授权的域和方法 70 | web.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 71 | AllowOrigins: []string{"*"}, 72 | AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.POST, echo.DELETE}, 73 | })) 74 | 75 | // 定义路由 76 | web.HEAD("/*", Null) 77 | web.Any("/", Default) 78 | web.GET("/ip", GetIP) 79 | web.GET("/favicon.ico", func(ctx echo.Context) error { 80 | return ctx.File("favicon.ico") 81 | }) 82 | 83 | // 启动Web服务 84 | web.Start(config.Listen) 85 | } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------