diff --git a/.gitignore b/.gitignore index f7dea7e..f07d35b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,6 @@ *.dylib .idea/ .vscode/ -wechatbot -storage.json # Test binary, built with `go test -c` *.test @@ -16,5 +14,6 @@ storage.json *.out # Dependency directories (remove the comment below to include it) -# vendor/ -/config.json +config.json +storage.json +vendor diff --git a/README.md b/README.md index 840d6e7..b0df538 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,48 @@ -# wechatbot +# chatgpt_wechat_robot +个人微信接入ChatGPT,实现和GPT机器人互动聊天。支持私聊回复和群聊艾特回复。 -> 本项目是 fork 他人的项目来进行学习和使用,请勿商用,可以下载下来做自定义的功能 -> 最近ChatGPT异常火爆,本项目可以将个人微信化身GPT机器人, -> 项目基于[openwechat](https://github.com/eatmoreapple/openwechat) 开发。 - -> `友链:`[chatgpt-dingtalk](https://github.com/eryajf/chatgpt-dingtalk) 本项目可以将GPT机器人集成到钉钉群聊中。 - - -### 目前实现了以下功能 +### 实现功能 * GPT机器人模型热度可配置 * 提问增加上下文 -* 指令清空上下文(指令:根据配置) -* 机器人群聊@回复 +* 指令清空上下文 * 机器人私聊回复 +* 机器人群聊@回复 * 私聊回复前缀设置 * 好友添加自动通过可配置 -* ~~增加每天工作的起始时间和结束时间,只有在该时间段才会对外提供 chatgpt 服务~~ -* ~~增加 vip 用户在任意时段都可享受 chatgpt 服务,只需要在 \wechatbot\handlers\group_msg_handler.go 中 的 VipUserList 切片中, -加入具体的 vip 昵称~~ -# 实现机制 -目前机器人有两种实现方式 -* 逆向功能,扒取官网API,通过抓取cookie获取GPT响应信息,`优点:`效果与官网一致,`缺点:`cookie会过期需要不定时更新。 -* 基于openai官网提供的API,`优点`:模型以及各种参数可以自由配置,`缺点:`效果达不到官网智能,且API收费,新账号有18美元免费额度。 +### 实现机制 +基于openai官网提供的API,`优点`:模型以及各种参数可以自由配置,`缺点:`效果达不到官网智能,且API收费,新账号有18美元免费额度。 -> 本项目基于第二种方式实现,模型之间具体差异可以参考[官方文档](https://beta.openai.com/docs/models/overview), 详细[参数示例](https://beta.openai.com/examples) 。 +> 模型之间具体差异可以参考[官方文档](https://beta.openai.com/docs/models/overview), 详细[参数示例](https://beta.openai.com/examples) 。 -# 常见问题 -* 如无法登录 login error: write storage.json: bad file descriptor 删除掉storage.json文件重新登录。 -* 如无法登录 login error: wechat network error: Get "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage": 301 response missing Location header 一般是微信登录权限问题,先确保PC端能否正常登录。 -* 其他无法登录问题,依然尝试删除掉storage.json文件,结束进程(linux一般是kill -9 进程id)之后重启程序,重新扫码登录,(如为docket部署,Supervisord进程管理工具会自动重启程序)。 -* ~~机器人无法正常回复,检查ApiKey能否正常使用,控制台日志中有详细错误信息~~ 新版本会机器人会直接输出,因为被问得好烦了。 -* linux中二维码无法扫描,缩小命令行功能,让二维码像素尽可能清晰。(无法从代码层面解决) -* 机器人一直答非所问,可能因为上下文累积过多。切换不同问题时,发送指令:启动时配置的`session_clear_token`字段。会清空上下文 +### 常见问题 +> 如无法登录`login error: write storage.json: bad file descriptor` +删除掉storage.json文件重新登录。 -# 使用前提 +> 如无法登录`login error: wechat network error: Get "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage": 301 response missing Location header` +一般是微信登录权限问题,先确保PC端能否正常登录。 -> * ~~目前只支持在windows上运行因为需要弹窗扫码登录微信,后续会支持linux~~ 已支持 -> * 有openai账号,并且创建好api_key,注册事项可以参考[此文章](https://juejin.cn/post/7173447848292253704) 。 -> * 应用可以参考这篇文章 [此文章](https://juejin.cn/post/7176813187705077816) 。 -> * 微信必须实名认证。 +> 其他无法登录问题 +尝试删除掉storage.json文件,结束进程(linux一般是kill -9 进程id)之后重启程序,重新扫码登录。 +如果为docket部署,Supervisord进程管理工具会自动重启程序。 -# 注意事项 +> 机器人一直答非所问 +可能因为上下文累积过多。切换不同问题时,发送指令:启动时配置的`session_clear_token`字段。会清空上下文 -> * 项目仅供娱乐,滥用可能有微信封禁的风险,请勿用于商业用途。 -> * 请注意收发敏感信息,本项目不做信息过滤。 +### 使用前提 +* 有openai账号,并且创建好api_key,注册事项可以参考[此文章](https://juejin.cn/post/7173447848292253704) 。 +* 应用可以参考这篇文章 [此文章](https://juejin.cn/post/7176813187705077816) 。 +* 微信必须实名认证。 -# 使用docker运行 +### 注意事项 +* 项目仅供娱乐,滥用可能有微信封禁的风险,请勿用于商业用途。 +* 请注意收发敏感信息,本项目不做信息过滤。 +### docker运行 你可以使用docker快速运行本项目。 -`第一种:基于环境变量运行` +#### 1. 基于环境变量运行 ```sh # 运行项目,环境变量参考下方配置说明 @@ -73,7 +64,7 @@ $ tail -f -n 50 /app/run.log 运行命令中映射的配置文件参考下边的配置文件说明。 -`第二种:基于配置文件挂载运行` +#### 2. 基于配置文件挂载运行 ```sh # 复制配置文件,根据自己实际情况,调整配置里的内容 @@ -89,78 +80,39 @@ $ tail -f -n 50 /app/run.log 其中配置文件参考下边的配置文件说明。 -# 快速开始 -`第一种:直接下载二进制(适合对编程不了解的同学)` - -> 非技术人员请直接下载release中的[压缩包](https://github.com/869413421/wechatbot/releases) ,请根据自己系统以及架构选择合适的压缩包,下载之后直接解压运行。 - -下载之后,在本地解压,即可看到可执行程序,与配置文件: - -``` -# windows -1.下载压缩包解压 -2.复制文件中config.dev.json更改为config.json -3.将config.json中的api_key替换为自己的 -4.双击exe,扫码登录 - -# linux -$ tar xf wechatbot-v0.0.2-darwin-arm64.tar.gz -$ cd wechatbot-v0.0.2-darwin-arm64 -$ cp config.dev.json # 根据情况调整配置文件内容 -$ ./wechatbot # 直接运行 - -# 如果要守护在后台运行 -$ nohup ./wechatbot &> run.log & -$ tail -f run.log -``` - -`第二种:基于源码运行(适合了解go语言编程的同学)` +### 源码运行 +适合了解go语言编程的同学 ```` # 获取项目 -$ git clone https://github.com/869413421/wechatbot.git +$ git clone https://github.com/ZYallers/chatgpt_wechat_robot.git # 进入项目目录 -$ cd wechatbot +$ cd chatgpt_wechat_robot # 复制配置文件 -$ copy config.dev.json config.json +$ cp config.dev.json config.json # 启动项目 $ go run main.go ```` -# 配置文件说明 +### 配置说明 -```` +```json { - "api_key": "your api key", - "auto_pass": true, - "session_timeout": 60, - "max_tokens": 1024, - "model": "text-davinci-003", - "temperature": 1, - "reply_prefix": "来自机器人回复:", - "session_clear_token": "清空会话" + "api_key": "your api key", # openai账号里设置的api_key + "auto_pass": true, # 是否自动通过好友添加 + "session_timeout": 60, # 会话超时时间,默认60秒,单位秒,在会话时间内所有发送给机器人的信息会作为上下文 + "max_tokens": 1024, # GPT响应字符数,最大2048,默认值512。会影响接口响应速度,字符越大响应越慢 + "model": "text-davinci-003", # GPT选用模型,默认text-davinci-003,具体选项参考官网训练场 + "temperature": 1, # GPT热度,0到1,默认0.9,数字越大创造力越强,但更偏离训练事实,越低越接近训练事实 + "reply_prefix": "来自机器人回复:", # 私聊回复前缀 + "session_clear_token": "清空会话" # 会话清空口令,默认`下一个问题` } +``` -api_key:openai api_key -auto_pass:是否自动通过好友添加 -session_timeout:会话超时时间,默认60秒,单位秒,在会话时间内所有发送给机器人的信息会作为上下文。 -max_tokens: GPT响应字符数,最大2048,默认值512。max_tokens会影响接口响应速度,字符越大响应越慢。 -model: GPT选用模型,默认text-davinci-003,具体选项参考官网训练场 -temperature: GPT热度,0到1,默认0.9。数字越大创造力越强,但更偏离训练事实,越低越接近训练事实 -reply_prefix: 私聊回复前缀 -session_clear_token: 会话清空口令,默认`下一个问题` -```` - -# 使用示例 -### 私聊 - - - -### 群聊@回复 - - - +### 友情提示 +本项目是 fork 他人的项目来进行学习和使用,请勿商用,可以下载下来做自定义的功能。 +项目基于[openwechat](https://github.com/eatmoreapple/openwechat) 开发。 \ No newline at end of file diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 7a3166f..1708bb1 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -2,9 +2,10 @@ package bootstrap import ( "fmt" + "github.com/eatmoreapple/openwechat" "github.com/qingconglaixueit/wechatbot/handlers" "github.com/qingconglaixueit/wechatbot/pkg/logger" - "github.com/eatmoreapple/openwechat" + "os" ) func Run() { @@ -14,23 +15,32 @@ func Run() { // 注册消息处理函数 handler, err := handlers.NewHandler() if err != nil { - logger.Danger("register error: %v", err) + logger.Danger(fmt.Sprintf("handlers.NewHandler error: %v", err)) return } bot.MessageHandler = handler // 注册登陆二维码回调 - bot.UUIDCallback = handlers.QrCodeCallBack + bot.UUIDCallback = openwechat.PrintlnQrcodeUrl // 创建热存储容器对象 reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") // 执行热登录 - err = bot.HotLogin(reloadStorage, true) + err = bot.HotLogin(reloadStorage) if err != nil { - logger.Warning(fmt.Sprintf("login error: %v ", err)) - return + if err := os.Remove("storage.json"); err != nil { + logger.Warning(fmt.Sprintf("os.Remove storage.json error: %v", err)) + return + } + reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") + err = bot.HotLogin(reloadStorage) + if err != nil { + logger.Warning(fmt.Sprintf("bot.HotLogin error: %v", err)) + return + } } + // 阻塞主goroutine, 直到发生异常或者用户主动退出 - bot.Block() + _ = bot.Block() } diff --git a/config/config.go b/config/config.go index 25b916a..b323578 100644 --- a/config/config.go +++ b/config/config.go @@ -3,12 +3,12 @@ package config import ( "encoding/json" "fmt" - "github.com/qingconglaixueit/wechatbot/pkg/logger" - "log" "os" "strconv" "sync" "time" + + "github.com/qingconglaixueit/wechatbot/pkg/logger" ) // Configuration 项目配置 @@ -44,7 +44,7 @@ func LoadConfig() *Configuration { MaxTokens: 512, Model: "text-davinci-003", Temperature: 0.9, - SessionClearToken: "下一个问题", + SessionClearToken: "下个问题", } // 判断配置文件是否存在,存在直接JSON读取 @@ -52,14 +52,14 @@ func LoadConfig() *Configuration { if err == nil { f, err := os.Open("config.json") if err != nil { - log.Fatalf("open config err: %v", err) + logger.Danger(fmt.Sprintf("open config error: %v", err)) return } defer f.Close() encoder := json.NewDecoder(f) err = encoder.Decode(config) if err != nil { - log.Fatalf("decode config err: %v", err) + logger.Danger(fmt.Sprintf("decode config error: %v", err)) return } } @@ -81,7 +81,7 @@ func LoadConfig() *Configuration { if SessionTimeout != "" { duration, err := time.ParseDuration(SessionTimeout) if err != nil { - logger.Danger(fmt.Sprintf("config session timeout err: %v ,get is %v", err, SessionTimeout)) + logger.Danger(fmt.Sprintf("config session timeout error: %v, get is %v", err, SessionTimeout)) return } config.SessionTimeout = duration @@ -92,7 +92,7 @@ func LoadConfig() *Configuration { if MaxTokens != "" { max, err := strconv.Atoi(MaxTokens) if err != nil { - logger.Danger(fmt.Sprintf("config MaxTokens err: %v ,get is %v", err, MaxTokens)) + logger.Danger(fmt.Sprintf("config max tokens error: %v ,get is %v", err, MaxTokens)) return } config.MaxTokens = uint(max) @@ -100,7 +100,7 @@ func LoadConfig() *Configuration { if Temperature != "" { temp, err := strconv.ParseFloat(Temperature, 64) if err != nil { - logger.Danger(fmt.Sprintf("config Temperature err: %v ,get is %v", err, Temperature)) + logger.Danger(fmt.Sprintf("config temperature error: %v, get is %v", err, Temperature)) return } config.Temperature = temp @@ -114,7 +114,7 @@ func LoadConfig() *Configuration { }) if config.ApiKey == "" { - logger.Danger("config err: api key required") + logger.Danger("config error: api key required") } return config diff --git a/gpt/gpt.go b/gpt/gpt.go index ec6c39b..02ab74b 100644 --- a/gpt/gpt.go +++ b/gpt/gpt.go @@ -5,15 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/qingconglaixueit/wechatbot/config" - "github.com/qingconglaixueit/wechatbot/pkg/logger" "io/ioutil" "log" "net/http" "time" -) -const BASEURL = "https://api.openai.com/v1/" + "github.com/qingconglaixueit/wechatbot/config" +) // ChatGPTResponseBody 请求体 type ChatGPTResponseBody struct { @@ -23,6 +21,12 @@ type ChatGPTResponseBody struct { Model string `json:"model"` Choices []ChoiceItem `json:"choices"` Usage map[string]interface{} `json:"usage"` + Error struct { + Message string `json:"message"` + Type string `json:"type"` + Param interface{} `json:"param"` + Code interface{} `json:"code"` + } `json:"error"` } type ChoiceItem struct { @@ -49,7 +53,37 @@ type ChatGPTRequestBody struct { //-H "Authorization: Bearer your chatGPT key" //-d '{"model": "text-davinci-003", "prompt": "give me good song", "temperature": 0, "max_tokens": 7}' func Completions(msg string) (string, error) { + var gptResponseBody *ChatGPTResponseBody + var resErr error + for retry := 1; retry <= 3; retry++ { + if retry > 1 { + time.Sleep(time.Duration(retry-1) * 100 * time.Millisecond) + } + gptResponseBody, resErr = httpRequestCompletions(msg, retry) + if resErr != nil { + log.Printf("gpt request(%d) error: %v\n", retry, resErr) + continue + } + if gptResponseBody.Error.Message == "" { + break + } + } + if resErr != nil { + return "", resErr + } + var reply string + if gptResponseBody != nil && len(gptResponseBody.Choices) > 0 { + reply = gptResponseBody.Choices[0].Text + } + return reply, nil +} + +func httpRequestCompletions(msg string, runtimes int) (*ChatGPTResponseBody, error) { cfg := config.LoadConfig() + if cfg.ApiKey == "" { + return nil, errors.New("api key required") + } + requestBody := ChatGPTRequestBody{ Model: cfg.Model, Prompt: msg, @@ -60,46 +94,37 @@ func Completions(msg string) (string, error) { PresencePenalty: 0, } requestData, err := json.Marshal(requestBody) - if err != nil { - return "", err + return nil, fmt.Errorf("json.Marshal requestBody error: %v", err) } - logger.Info(fmt.Sprintf("request gpt json string : %v", string(requestData))) - req, err := http.NewRequest("POST", BASEURL+"completions", bytes.NewBuffer(requestData)) + + log.Printf("gpt request(%d) json: %s\n", runtimes, string(requestData)) + + req, err := http.NewRequest(http.MethodPost, "https://api.openai.com/v1/completions", bytes.NewBuffer(requestData)) if err != nil { - return "", err + return nil, fmt.Errorf("http.NewRequest error: %v", err) } - apiKey := config.LoadConfig().ApiKey req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+apiKey) - client := &http.Client{Timeout: 30 * time.Second} + req.Header.Set("Authorization", "Bearer "+cfg.ApiKey) + client := &http.Client{Timeout: 15 * time.Second} response, err := client.Do(req) if err != nil { - return "", err + return nil, fmt.Errorf("client.Do error: %v", err) } defer response.Body.Close() - if response.StatusCode != 200 { - body, _ := ioutil.ReadAll(response.Body) - return "", errors.New(fmt.Sprintf("请求GTP出错了,gpt api status code not equals 200,code is %d ,details: %v ", response.StatusCode, string(body))) - } + body, err := ioutil.ReadAll(response.Body) if err != nil { - return "", err + return nil, fmt.Errorf("ioutil.ReadAll error: %v", err) } - logger.Info(fmt.Sprintf("response gpt json string : %v", string(body))) + + log.Printf("gpt response(%d) json: %s\n", runtimes, string(body)) gptResponseBody := &ChatGPTResponseBody{} - log.Println(string(body)) err = json.Unmarshal(body, gptResponseBody) if err != nil { - return "", err - } - - var reply string - if len(gptResponseBody.Choices) > 0 { - reply = gptResponseBody.Choices[0].Text + return nil, fmt.Errorf("json.Marshal responseBody error: %v", err) } - logger.Info(fmt.Sprintf("gpt response text: %s ", reply)) - return reply, nil + return gptResponseBody, nil } diff --git a/handlers/group_msg_handler.go b/handlers/group_msg_handler.go index 4fb5fde..c9aafe0 100644 --- a/handlers/group_msg_handler.go +++ b/handlers/group_msg_handler.go @@ -1,13 +1,16 @@ package handlers import ( - "errors" "fmt" + "log" + "math/rand" + "strings" + "time" + "github.com/eatmoreapple/openwechat" "github.com/qingconglaixueit/wechatbot/gpt" "github.com/qingconglaixueit/wechatbot/pkg/logger" "github.com/qingconglaixueit/wechatbot/service" - "strings" ) var _ MessageHandlerInterface = (*GroupMessageHandler)(nil) @@ -32,14 +35,14 @@ func GroupMessageContextHandler() func(ctx *openwechat.MessageContext) { // 获取用户消息处理器 handler, err := NewGroupMessageHandler(msg) if err != nil { - logger.Warning(fmt.Sprintf("init group message handler error: %s", err)) + logger.Warning(fmt.Sprintf("init group message handler error: %v", err)) return } // 处理用户消息 err = handler.handle() if err != nil { - logger.Warning(fmt.Sprintf("handle group message error: %s", err)) + logger.Warning(fmt.Sprintf("handle group message error: %v", err)) } } } @@ -78,7 +81,16 @@ func (g *GroupMessageHandler) handle() error { // ReplyText 发息送文本消到群 func (g *GroupMessageHandler) ReplyText() error { - logger.Info(fmt.Sprintf("Received Group %v Text Msg : %v", g.group.NickName, g.msg.Content)) + if time.Now().Unix()-g.msg.CreateTime > 60 { + return nil + } + + maxInt := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(5) + time.Sleep(time.Duration(maxInt+1) * time.Second) + + log.Printf("Received Group[%v], Content[%v], CreateTime[%v]", g.group.NickName, g.msg.Content, + time.Unix(g.msg.CreateTime, 0).Format("2006/01/02 15:04:05")) + var ( err error reply string @@ -92,18 +104,20 @@ func (g *GroupMessageHandler) ReplyText() error { // 2.获取请求的文本,如果为空字符串不处理 requestText := g.getRequestText() if requestText == "" { - logger.Info("user message is null") + log.Println("group message is empty") return nil } // 3.请求GPT获取回复 reply, err = gpt.Completions(requestText) if err != nil { - // 2.1 将GPT请求失败信息输出给用户,省得整天来问又不知道日志在哪里。 - errMsg := fmt.Sprintf("gpt request error: %v", err) - _, err = g.msg.ReplyText(errMsg) + text := err.Error() + if strings.Contains(err.Error(), "context deadline exceeded") { + text = deadlineExceededText + } + _, err = g.msg.ReplyText(text) if err != nil { - return errors.New(fmt.Sprintf("response group error: %v ", err)) + return fmt.Errorf("reply group error: %v", err) } return err } @@ -112,7 +126,7 @@ func (g *GroupMessageHandler) ReplyText() error { g.service.SetUserSessionContext(requestText, reply) _, err = g.msg.ReplyText(g.buildReplyText(reply)) if err != nil { - return errors.New(fmt.Sprintf("response user error: %v ", err)) + return fmt.Errorf("reply group error: %v ", err) } // 5.返回错误信息 @@ -132,7 +146,7 @@ func (g *GroupMessageHandler) getRequestText() string { return "" } - // 3.获取上下文,拼接在一起,如果字符长度超出4000,截取为4000。(GPT按字符长度算),达芬奇3最大为4068,也许后续为了适应要动态进行判断。 + // 3.获取上下文拼接在一起,如果字符长度超出4000截取为4000(GPT按字符长度算),达芬奇3最大为4068,也许后续为了适应要动态进行判断 sessionText := g.service.GetUserSessionContext() if sessionText != "" { requestText = sessionText + "\n" + requestText @@ -146,7 +160,7 @@ func (g *GroupMessageHandler) getRequestText() string { runeRequestText := []rune(requestText) lastChar := string(runeRequestText[len(runeRequestText)-1:]) if strings.Index(punctuation, lastChar) < 0 { - requestText = requestText + "?" // 判断最后字符是否加了标点,没有的话加上句号,避免openai自动补齐引起混乱。 + requestText = requestText + "?" // 判断最后字符是否加了标点,没有的话加上句号,避免openai自动补齐引起混乱 } // 5.返回请求文本 @@ -164,13 +178,14 @@ func (g *GroupMessageHandler) buildReplyText(reply string) string { } reply = strings.TrimSpace(reply) if reply == "" { - return atText + " 请求得不到任何有意义的回复,请具体提出问题。" + return atText + " " + deadlineExceededText } - // 2.拼接回复,@我的用户,问题,回复 + // 2.拼接回复, @我的用户, 问题, 回复 replaceText := "@" + g.self.NickName question := strings.TrimSpace(strings.ReplaceAll(g.msg.Content, replaceText, "")) - reply = atText + "\n" + question + "\n --------------------------------\n" + reply + hr := strings.Repeat("-", 36) + reply = atText + "\n" + question + "\n" + hr + "\n" + reply reply = strings.Trim(reply, "\n") // 3.返回回复的内容 diff --git a/handlers/handler.go b/handlers/handler.go index 7a608df..66b6647 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -2,10 +2,10 @@ package handlers import ( "fmt" - "github.com/qingconglaixueit/wechatbot/config" - "github.com/qingconglaixueit/wechatbot/pkg/logger" "github.com/eatmoreapple/openwechat" "github.com/patrickmn/go-cache" + "github.com/qingconglaixueit/wechatbot/config" + "github.com/qingconglaixueit/wechatbot/pkg/logger" "github.com/skip2/go-qrcode" "log" "runtime" @@ -13,6 +13,8 @@ import ( "time" ) +const deadlineExceededText = "请求GPT服务器超时[裂开]得不到回复,请重新发送问题[旺柴]" + var c = cache.New(config.LoadConfig().SessionTimeout, time.Minute*5) // MessageHandlerInterface 消息处理接口 @@ -23,13 +25,11 @@ type MessageHandlerInterface interface { // QrCodeCallBack 登录扫码回调, func QrCodeCallBack(uuid string) { - if runtime.GOOS == "windows" { - // 运行在Windows系统上 + if runtime.GOOS == "windows" { // 运行在Windows系统上 openwechat.PrintlnQrcodeUrl(uuid) } else { - log.Println("login in linux") url := "https://login.weixin.qq.com/l/" + uuid - log.Printf("如果二维码无法扫描,请缩小控制台尺寸,或更换命令行工具,缩小二维码像素") + log.Println("如果二维码无法扫描,请缩小控制台尺寸,或更换命令行工具,缩小二维码像素。") q, _ := qrcode.New(url, qrcode.High) fmt.Println(q.ToSmallString(true)) } diff --git a/handlers/token_msg_handler.go b/handlers/token_msg_handler.go index 13393c3..651c6c0 100644 --- a/handlers/token_msg_handler.go +++ b/handlers/token_msg_handler.go @@ -2,9 +2,11 @@ package handlers import ( "fmt" + "github.com/eatmoreapple/openwechat" "github.com/qingconglaixueit/wechatbot/pkg/logger" "github.com/qingconglaixueit/wechatbot/service" - "github.com/eatmoreapple/openwechat" + "math/rand" + "time" ) var _ MessageHandlerInterface = (*TokenMessageHandler)(nil) @@ -63,17 +65,19 @@ func (t *TokenMessageHandler) handle() error { // ReplyText 回复清空口令 func (t *TokenMessageHandler) ReplyText() error { - logger.Info("user clear token") + maxInt := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(5) + time.Sleep(time.Duration(maxInt+1) * time.Second) + t.service.ClearUserSessionContext() var err error if t.msg.IsComeFromGroup() { if !t.msg.IsAt() { return err } - atText := "@" + t.sender.NickName + "上下文已经清空,请问下一个问题。" + atText := "@" + t.sender.NickName + "上下文已经清空,请问下个问题" _, err = t.msg.ReplyText(atText) } else { - _, err = t.msg.ReplyText("上下文已经清空,请问下一个问题。") + _, err = t.msg.ReplyText("上下文已经清空,请问下个问题") } return err } diff --git a/handlers/user_msg_handler.go b/handlers/user_msg_handler.go index 8558526..1e2a63e 100644 --- a/handlers/user_msg_handler.go +++ b/handlers/user_msg_handler.go @@ -1,14 +1,17 @@ package handlers import ( - "errors" "fmt" + "log" + "math/rand" + "strings" + "time" + "github.com/eatmoreapple/openwechat" "github.com/qingconglaixueit/wechatbot/config" "github.com/qingconglaixueit/wechatbot/gpt" "github.com/qingconglaixueit/wechatbot/pkg/logger" "github.com/qingconglaixueit/wechatbot/service" - "strings" ) var _ MessageHandlerInterface = (*UserMessageHandler)(nil) @@ -65,7 +68,16 @@ func (h *UserMessageHandler) handle() error { // ReplyText 发送文本消息到群 func (h *UserMessageHandler) ReplyText() error { - logger.Info(fmt.Sprintf("Received User %v Text Msg : %v", h.sender.NickName, h.msg.Content)) + if time.Now().Unix()-h.msg.CreateTime > 60 { + return nil + } + + maxInt := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(5) + time.Sleep(time.Duration(maxInt+1) * time.Second) + + log.Printf("Received User[%v], Content[%v], CreateTime[%v]", h.sender.NickName, h.msg.Content, + time.Unix(h.msg.CreateTime, 0).Format("2006/01/02 15:04:05")) + var ( reply string err error @@ -73,18 +85,20 @@ func (h *UserMessageHandler) ReplyText() error { // 1.获取上下文,如果字符串为空不处理 requestText := h.getRequestText() if requestText == "" { - logger.Info("user message is null") + log.Println("user message is empty") return nil } - logger.Info(fmt.Sprintf("h.sender.NickName == %+v", h.sender.NickName)) + // 2.向GPT发起请求,如果回复文本等于空,不回复 reply, err = gpt.Completions(h.getRequestText()) if err != nil { - // 2.1 将GPT请求失败信息输出给用户,省得整天来问又不知道日志在哪里。 - errMsg := fmt.Sprintf("gpt request error: %v", err) - _, err = h.msg.ReplyText(errMsg) + text := err.Error() + if strings.Contains(err.Error(), "context deadline exceeded") { + text = deadlineExceededText + } + _, err = h.msg.ReplyText(text) if err != nil { - return errors.New(fmt.Sprintf("response user error: %v ", err)) + return fmt.Errorf("reply user error: %v ", err) } return err } @@ -93,7 +107,7 @@ func (h *UserMessageHandler) ReplyText() error { h.service.SetUserSessionContext(requestText, reply) _, err = h.msg.ReplyText(buildUserReply(reply)) if err != nil { - return errors.New(fmt.Sprintf("response user error: %v ", err)) + return fmt.Errorf("reply user error: %v ", err) } // 3.返回错误 @@ -135,11 +149,9 @@ func buildUserReply(reply string) string { trimText := textSplit[0] reply = strings.Trim(reply, trimText) } - reply = strings.TrimSpace(reply) - reply = strings.TrimSpace(reply) if reply == "" { - return "请求得不到任何有意义的回复,请具体提出问题。" + return deadlineExceededText } // 2.如果用户有配置前缀,加上前缀 diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index bdc0b4b..8f65033 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -24,7 +24,7 @@ func Info(args ...interface{}) { // Danger 错误 为什么不命名为 error?避免和 error 类型重名 func Danger(args ...interface{}) { Logger.SetPrefix("[ERROR]") - Logger.Fatal(args...) + Logger.Println(args...) } // Warning 警告 @@ -35,6 +35,6 @@ func Warning(args ...interface{}) { // DeBug debug func DeBug(args ...interface{}) { - Logger.SetPrefix("[DeBug]") + Logger.SetPrefix("[DEBUG]") Logger.Println(args...) } diff --git a/rule/rule.go b/rule/rule.go index f9f3608..1b83a85 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -22,13 +22,14 @@ func (r *Rule) SetWork(work bool) { isWork = work return } + func (r *Rule) GetWork() bool { lock.Lock() defer lock.Unlock() return isWork } -// 判断时间在今天的早上 9点到 晚上 9 点区间内 +// 判断时间在今天的早上9点到晚上9点区间内 func (r *Rule) IsWorkTime(s int, e int) bool { if s < 0 || s > 24 { s = STARTTIME