diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
new file mode 100644
index 0000000..b2b1091
--- /dev/null
+++ b/.github/workflows/docker-image.yml
@@ -0,0 +1,61 @@
+# This is a basic workflow to help you get started with Actions
+
+name: build docker image
+
+# Controls when the action will run.
+on:
+ push:
+ branches:
+ - main
+
+# Allows you to run this workflow manually from the Actions tab
+ # 可以手动触发
+ workflow_dispatch:
+ inputs:
+ logLevel:
+ description: 'Log level'
+ required: true
+ default: 'warning'
+ tags:
+ description: 'Test scenario tags'
+
+jobs:
+ buildx:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Get current date
+ id: date
+ run: echo "::set-output name=today::$(date +'%Y-%m-%d_%H-%M')"
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Available platforms
+ run: echo ${{ steps.buildx.outputs.platforms }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: ./Dockerfile
+ # 所需要的体系结构,可以在 Available platforms 步骤中获取所有的可用架构
+ platforms: linux/amd64,linux/arm64/v8
+ # 镜像推送时间
+ push: ${{ github.event_name != 'pull_request' }}
+ # 给清单打上多个标签
+ tags: |
+ ${{ secrets.DOCKERHUB_USERNAME }}/wechatbot:${{ steps.date.outputs.today }}
+ ${{ secrets.DOCKERHUB_USERNAME }}/wechatbot:latest
\ No newline at end of file
diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml
new file mode 100644
index 0000000..f7fbd55
--- /dev/null
+++ b/.github/workflows/go-release.yml
@@ -0,0 +1,26 @@
+name: build
+
+on:
+ release:
+ types: [created] # 表示在创建新的 Release 时触发
+
+jobs:
+ build-go-binary:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ goos: [linux, windows, darwin] # 需要打包的系统
+ goarch: [amd64, arm64] # 需要打包的架构
+ exclude: # 排除某些平台和架构
+ - goarch: arm64
+ goos: windows
+ steps:
+ - uses: actions/checkout@v3
+ - uses: wangyoucao577/go-release-action@v1.30
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }} # 一个默认的变量,用来实现往 Release 中添加文件
+ goos: ${{ matrix.goos }}
+ goarch: ${{ matrix.goarch }}
+ goversion: 1.16 # 可以指定编译使用的 Golang 版本
+ binary_name: "wechatbot" # 可以指定二进制文件的名称
+ extra_files: README.md config.dev.json # 需要包含的额外文件
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index caccd51..f7dea7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,8 @@
*.so
*.dylib
.idea/
+.vscode/
+wechatbot
storage.json
# Test binary, built with `go test -c`
@@ -15,3 +17,4 @@ storage.json
# Dependency directories (remove the comment below to include it)
# vendor/
+/config.json
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6523b78
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,65 @@
+#FROM golang:1.17.10 AS builder
+#
+## ENV GOPROXY https://goproxy.io
+#
+#RUN mkdir /app
+#ADD . /app/
+#WORKDIR /app
+#RUN go build -o wechatbot .
+#
+#FROM centos:centos7
+#RUN mkdir /app
+#WORKDIR /app
+#COPY --from=builder /app/ .
+#RUN chmod +x wechatbot && cp config.dev.json config.json && yum -y install vim net-tools telnet wget curl && yum clean all
+#
+#CMD ./wechatbot
+
+# wechatbot/Dockerfile
+
+# 使用 golang 官方镜像提供 Go 运行环境,并且命名为 buidler 以便后续引用
+FROM golang:1.16-alpine as builder
+
+# 启用 Go Modules 并设置 GOPROXY
+ENV GO111MODULE on
+ENV GOPROXY https://goproxy.cn
+
+# 更新安装源
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
+
+# 安装 git
+RUN apk --no-cache add git
+
+# 设置工作目录
+WORKDIR /app
+
+# 将当前项目所在目录代码拷贝到镜像中
+COPY . .
+
+# 下载依赖
+RUN go mod download
+
+# 构建二进制文件,添加来一些额外参数以便可以在 Alpine 中运行它
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o wechatbot
+
+# 下面是第二阶段的镜像构建,和之前保持一致
+FROM alpine:latest
+
+# 更新安装源
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
+
+# 安装相关软件
+RUN apk update && apk add --no-cache bash supervisor ca-certificates
+
+# 和上个阶段一样设置工作目录
+RUN mkdir /app
+WORKDIR /app
+
+# 而是从上一个阶段构建的 builder容器中拉取
+COPY --from=builder /app/wechatbot .
+ADD supervisord.conf /etc/supervisord.conf
+ADD config.dev.json /app/config.dev.json
+RUN cp config.dev.json config.json
+
+# 通过 Supervisor 管理服务
+CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a1a8300
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+.PHONY: build
+build:
+ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -o wechatbot ./main.go
+
+.PHONY: docker
+docker:
+ docker build . -t wechatbot:latest
diff --git a/README.md b/README.md
index cc69d9b..840d6e7 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,166 @@
# wechatbot
-最近chatGPT异常火爆,想到将其接入到个人微信是件比较有趣的事,所以有了这个项目。项目基于[openwechat](https://github.com/eatmoreapple/openwechat)
-开发
-###目前实现了以下功能
- + 群聊@回复
- + 私聊回复
- + 自动通过回复
-
-# 注册openai
-chatGPT注册可以参考[这里](https://juejin.cn/post/7173447848292253704)
-
-# 安装使用
+
+> 本项目是 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美元免费额度。
+
+> 本项目基于第二种方式实现,模型之间具体差异可以参考[官方文档](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`字段。会清空上下文
+
+# 使用前提
+
+> * ~~目前只支持在windows上运行因为需要弹窗扫码登录微信,后续会支持linux~~ 已支持
+> * 有openai账号,并且创建好api_key,注册事项可以参考[此文章](https://juejin.cn/post/7173447848292253704) 。
+> * 应用可以参考这篇文章 [此文章](https://juejin.cn/post/7176813187705077816) 。
+> * 微信必须实名认证。
+
+# 注意事项
+
+> * 项目仅供娱乐,滥用可能有微信封禁的风险,请勿用于商业用途。
+> * 请注意收发敏感信息,本项目不做信息过滤。
+
+# 使用docker运行
+
+你可以使用docker快速运行本项目。
+
+`第一种:基于环境变量运行`
+
+```sh
+# 运行项目,环境变量参考下方配置说明
+$ docker run -itd --name wechatbot --restart=always \
+ -e APIKEY=换成你的key \
+ -e AUTO_PASS=false \
+ -e SESSION_TIMEOUT=60s \
+ -e MODEL=text-davinci-003 \
+ -e MAX_TOKENS=512 \
+ -e TEMPREATURE=0.9 \
+ -e REPLY_PREFIX=我是来自机器人回复: \
+ -e SESSION_CLEAR_TOKEN=下一个问题 \
+ docker.mirrors.sjtug.sjtu.edu.cn/qingshui869413421/wechatbot:latest
+
+# 查看二维码
+$ docker exec -it wechatbot bash
+$ tail -f -n 50 /app/run.log
+```
+
+运行命令中映射的配置文件参考下边的配置文件说明。
+
+`第二种:基于配置文件挂载运行`
+
+```sh
+# 复制配置文件,根据自己实际情况,调整配置里的内容
+$ cp config.dev.json config.json # 其中 config.dev.json 从项目的根目录获取
+
+# 运行项目
+$ docker run -itd --name wechatbot -v `pwd`/config.json:/app/config.json docker.mirrors.sjtug.sjtu.edu.cn/qingshui869413421/wechatbot:latest
+
+# 查看二维码
+$ docker exec -it wechatbot bash
+$ 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语言编程的同学)`
+
````
# 获取项目
-git clone https://github.com/869413421/wechatbot.git
+$ git clone https://github.com/869413421/wechatbot.git
# 进入项目目录
-cd wechatbot
+$ cd wechatbot
# 复制配置文件
-copy config.dev.json config.json
+$ copy config.dev.json config.json
# 启动项目
-go run main.go
+$ go run main.go
+````
+
+# 配置文件说明
+
+````
+{
+ "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: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: 会话清空口令,默认`下一个问题`
+````
+
+# 使用示例
+### 私聊
+
+
+
+### 群聊@回复
+
+
-启动前需替换config中的api_key
diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go
index 7049740..7a3166f 100644
--- a/bootstrap/bootstrap.go
+++ b/bootstrap/bootstrap.go
@@ -1,31 +1,35 @@
package bootstrap
import (
- "github.com/869413421/wechatbot/handlers"
+ "fmt"
+ "github.com/qingconglaixueit/wechatbot/handlers"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
"github.com/eatmoreapple/openwechat"
- "log"
)
-
-
func Run() {
//bot := openwechat.DefaultBot()
bot := openwechat.DefaultBot(openwechat.Desktop) // 桌面模式,上面登录不上的可以尝试切换这种模式
// 注册消息处理函数
- bot.MessageHandler = handlers.Handler
+ handler, err := handlers.NewHandler()
+ if err != nil {
+ logger.Danger("register error: %v", err)
+ return
+ }
+ bot.MessageHandler = handler
+
// 注册登陆二维码回调
- bot.UUIDCallback = openwechat.PrintlnQrcodeUrl
+ bot.UUIDCallback = handlers.QrCodeCallBack
// 创建热存储容器对象
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
+
// 执行热登录
- err := bot.HotLogin(reloadStorage)
+ err = bot.HotLogin(reloadStorage, true)
if err != nil {
- if err = bot.Login(); err != nil {
- log.Printf("login error: %v \n", err)
- return
- }
+ logger.Warning(fmt.Sprintf("login error: %v ", err))
+ return
}
// 阻塞主goroutine, 直到发生异常或者用户主动退出
bot.Block()
diff --git a/config.dev.json b/config.dev.json
index b78e4db..84bf81c 100644
--- a/config.dev.json
+++ b/config.dev.json
@@ -1,4 +1,10 @@
{
"api_key": "your api key",
- "auto_pass": true
+ "auto_pass": true,
+ "session_timeout": 60,
+ "max_tokens": 1024,
+ "model": "text-davinci-003",
+ "temperature": 1,
+ "reply_prefix": "来自机器人回复:",
+ "session_clear_token": "清空会话"
}
diff --git a/config/config.go b/config/config.go
index 6afede1..25b916a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,17 +2,33 @@ package config
import (
"encoding/json"
+ "fmt"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
"log"
"os"
+ "strconv"
"sync"
+ "time"
)
// Configuration 项目配置
type Configuration struct {
- // gtp apikey
+ // gpt apikey
ApiKey string `json:"api_key"`
// 自动通过好友
AutoPass bool `json:"auto_pass"`
+ // 会话超时时间
+ SessionTimeout time.Duration `json:"session_timeout"`
+ // GPT请求最大字符数
+ MaxTokens uint `json:"max_tokens"`
+ // GPT模型
+ Model string `json:"model"`
+ // 热度
+ Temperature float64 `json:"temperature"`
+ // 回复前缀
+ ReplyPrefix string `json:"reply_prefix"`
+ // 清空会话口令
+ SessionClearToken string `json:"session_clear_token"`
}
var config *Configuration
@@ -21,30 +37,85 @@ var once sync.Once
// LoadConfig 加载配置
func LoadConfig() *Configuration {
once.Do(func() {
- // 从文件中读取
- config = &Configuration{}
- f, err := os.Open("config.json")
- if err != nil {
- log.Fatalf("open config err: %v", err)
- return
- }
- defer f.Close()
- encoder := json.NewDecoder(f)
- err = encoder.Decode(config)
- if err != nil {
- log.Fatalf("decode config err: %v", err)
- return
+ // 给配置赋默认值
+ config = &Configuration{
+ AutoPass: false,
+ SessionTimeout: 60,
+ MaxTokens: 512,
+ Model: "text-davinci-003",
+ Temperature: 0.9,
+ SessionClearToken: "下一个问题",
}
- // 如果环境变量有配置,读取环境变量
- ApiKey := os.Getenv("ApiKey")
- AutoPass := os.Getenv("AutoPass")
+ // 判断配置文件是否存在,存在直接JSON读取
+ _, err := os.Stat("config.json")
+ if err == nil {
+ f, err := os.Open("config.json")
+ if err != nil {
+ log.Fatalf("open config err: %v", err)
+ return
+ }
+ defer f.Close()
+ encoder := json.NewDecoder(f)
+ err = encoder.Decode(config)
+ if err != nil {
+ log.Fatalf("decode config err: %v", err)
+ return
+ }
+ }
+ // 有环境变量使用环境变量
+ ApiKey := os.Getenv("APIKEY")
+ AutoPass := os.Getenv("AUTO_PASS")
+ SessionTimeout := os.Getenv("SESSION_TIMEOUT")
+ Model := os.Getenv("MODEL")
+ MaxTokens := os.Getenv("MAX_TOKENS")
+ Temperature := os.Getenv("TEMPREATURE")
+ ReplyPrefix := os.Getenv("REPLY_PREFIX")
+ SessionClearToken := os.Getenv("SESSION_CLEAR_TOKEN")
if ApiKey != "" {
config.ApiKey = ApiKey
}
if AutoPass == "true" {
config.AutoPass = true
}
+ if SessionTimeout != "" {
+ duration, err := time.ParseDuration(SessionTimeout)
+ if err != nil {
+ logger.Danger(fmt.Sprintf("config session timeout err: %v ,get is %v", err, SessionTimeout))
+ return
+ }
+ config.SessionTimeout = duration
+ }
+ if Model != "" {
+ config.Model = Model
+ }
+ if MaxTokens != "" {
+ max, err := strconv.Atoi(MaxTokens)
+ if err != nil {
+ logger.Danger(fmt.Sprintf("config MaxTokens err: %v ,get is %v", err, MaxTokens))
+ return
+ }
+ config.MaxTokens = uint(max)
+ }
+ if Temperature != "" {
+ temp, err := strconv.ParseFloat(Temperature, 64)
+ if err != nil {
+ logger.Danger(fmt.Sprintf("config Temperature err: %v ,get is %v", err, Temperature))
+ return
+ }
+ config.Temperature = temp
+ }
+ if ReplyPrefix != "" {
+ config.ReplyPrefix = ReplyPrefix
+ }
+ if SessionClearToken != "" {
+ config.SessionClearToken = SessionClearToken
+ }
+
})
+ if config.ApiKey == "" {
+ logger.Danger("config err: api key required")
+ }
+
return config
}
diff --git a/go.mod b/go.mod
index a3f92ad..391ae9b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,16 @@
-module github.com/869413421/wechatbot
+module github.com/qingconglaixueit/wechatbot
go 1.16
-require github.com/eatmoreapple/openwechat v1.2.1
+require (
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/eatmoreapple/openwechat v1.2.1
+ github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
+ github.com/otiai10/openaigo v1.0.0
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/qingconglaixueit/abing_logger v0.0.2
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+ go.uber.org/zap v1.24.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 6fe8c3d..fcc71fa 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,81 @@
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
+github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.4.1 h1:HOVBfKP1oXIc0wWo9hZ8JLdZtyCPWqjvmFDuVZ0yv2Y=
+github.com/otiai10/mint v1.4.1/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI=
+github.com/otiai10/openaigo v1.0.0 h1:E4SoShWjMVltBAkM4/QsxGPhPKZlpKp0GSK6Ov1MSOg=
+github.com/otiai10/openaigo v1.0.0/go.mod h1:792bx6AWTS61weDi2EzKpHHnTF4eDMAlJ5GvAk/mgPg=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/qingconglaixueit/abing_logger v0.0.2 h1:KPv+dzAwVYqB/nPViVy8YhQDORm7/ejQ4JSkAs8RnpA=
+github.com/qingconglaixueit/abing_logger v0.0.2/go.mod h1:jXdlkJitAiZFx9IM9M0U1P+8myv0ieQRPznThQA6myg=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gtp/gtp.go b/gpt/gpt.go
similarity index 56%
rename from gtp/gtp.go
rename to gpt/gpt.go
index 15cd8e2..ec6c39b 100644
--- a/gtp/gtp.go
+++ b/gpt/gpt.go
@@ -1,35 +1,43 @@
-package gtp
+package gpt
import (
"bytes"
"encoding/json"
- "github.com/869413421/wechatbot/config"
+ "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/"
// ChatGPTResponseBody 请求体
type ChatGPTResponseBody struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int `json:"created"`
- Model string `json:"model"`
- Choices []map[string]interface{} `json:"choices"`
- Usage map[string]interface{} `json:"usage"`
+ ID string `json:"id"`
+ Object string `json:"object"`
+ Created int `json:"created"`
+ Model string `json:"model"`
+ Choices []ChoiceItem `json:"choices"`
+ Usage map[string]interface{} `json:"usage"`
}
type ChoiceItem struct {
+ Text string `json:"text"`
+ Index int `json:"index"`
+ Logprobs int `json:"logprobs"`
+ FinishReason string `json:"finish_reason"`
}
// ChatGPTRequestBody 响应体
type ChatGPTRequestBody struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
- MaxTokens int `json:"max_tokens"`
- Temperature float32 `json:"temperature"`
+ MaxTokens uint `json:"max_tokens"`
+ Temperature float64 `json:"temperature"`
TopP int `json:"top_p"`
FrequencyPenalty int `json:"frequency_penalty"`
PresencePenalty int `json:"presence_penalty"`
@@ -41,11 +49,12 @@ 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) {
+ cfg := config.LoadConfig()
requestBody := ChatGPTRequestBody{
- Model: "text-davinci-003",
+ Model: cfg.Model,
Prompt: msg,
- MaxTokens: 2048,
- Temperature: 0.7,
+ MaxTokens: cfg.MaxTokens,
+ Temperature: cfg.Temperature,
TopP: 1,
FrequencyPenalty: 0,
PresencePenalty: 0,
@@ -55,7 +64,7 @@ func Completions(msg string) (string, error) {
if err != nil {
return "", err
}
- log.Printf("request gtp json string : %v", string(requestData))
+ logger.Info(fmt.Sprintf("request gpt json string : %v", string(requestData)))
req, err := http.NewRequest("POST", BASEURL+"completions", bytes.NewBuffer(requestData))
if err != nil {
return "", err
@@ -64,17 +73,21 @@ func Completions(msg string) (string, error) {
apiKey := config.LoadConfig().ApiKey
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
- client := &http.Client{}
+ client := &http.Client{Timeout: 30 * time.Second}
response, err := client.Do(req)
if err != nil {
return "", 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
}
+ logger.Info(fmt.Sprintf("response gpt json string : %v", string(body)))
gptResponseBody := &ChatGPTResponseBody{}
log.Println(string(body))
@@ -82,13 +95,11 @@ func Completions(msg string) (string, error) {
if err != nil {
return "", err
}
+
var reply string
if len(gptResponseBody.Choices) > 0 {
- for _, v := range gptResponseBody.Choices {
- reply = v["text"].(string)
- break
- }
+ reply = gptResponseBody.Choices[0].Text
}
- log.Printf("gpt response text: %s \n", reply)
+ logger.Info(fmt.Sprintf("gpt response text: %s ", reply))
return reply, nil
}
diff --git a/gpt/gpt35.go b/gpt/gpt35.go
new file mode 100644
index 0000000..5ab0536
--- /dev/null
+++ b/gpt/gpt35.go
@@ -0,0 +1,41 @@
+// @Author Bing
+// @Date 2023/3/6 20:31:00
+// @Desc
+package gpt
+
+import (
+ "context"
+ "github.com/otiai10/openaigo"
+ "github.com/qingconglaixueit/abing_logger"
+ "github.com/qingconglaixueit/wechatbot/config"
+)
+
+type MyGpt struct {
+ C *openaigo.Client
+}
+
+func NewGpr35() *openaigo.Client {
+ cfg := config.LoadConfig()
+ return openaigo.NewClient(cfg.ApiKey)
+}
+
+func (c *MyGpt) Gpt3P5(req string) string {
+ request := openaigo.ChatCompletionRequestBody{
+ Model: "gpt-3.5-turbo",
+ Messages: []openaigo.ChatMessage{
+ {Role: "user", Content: req},
+ },
+ }
+ ctx := context.Background()
+ rsp, err := c.C.Chat(ctx, request)
+ if err != nil {
+ abing_logger.SugarLogger.Errorf("gpt client chat erorr:%+v", err)
+ return ""
+ }
+
+ if len(rsp.Choices) == 0 || rsp.Choices[0].Message.Content == "" {
+ return ""
+ }
+
+ return rsp.Choices[0].Message.Content
+}
diff --git a/handlers/group_msg_handler.go b/handlers/group_msg_handler.go
index ec0560a..4fb5fde 100644
--- a/handlers/group_msg_handler.go
+++ b/handlers/group_msg_handler.go
@@ -1,9 +1,12 @@
package handlers
import (
- "github.com/869413421/wechatbot/gtp"
+ "errors"
+ "fmt"
"github.com/eatmoreapple/openwechat"
- "log"
+ "github.com/qingconglaixueit/wechatbot/gpt"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
+ "github.com/qingconglaixueit/wechatbot/service"
"strings"
)
@@ -11,61 +14,165 @@ var _ MessageHandlerInterface = (*GroupMessageHandler)(nil)
// GroupMessageHandler 群消息处理
type GroupMessageHandler struct {
+ // 获取自己
+ self *openwechat.Self
+ // 群
+ group *openwechat.Group
+ // 接收到消息
+ msg *openwechat.Message
+ // 发送的用户
+ sender *openwechat.User
+ // 实现的用户业务
+ service service.UserServiceInterface
}
-// handle 处理消息
-func (g *GroupMessageHandler) handle(msg *openwechat.Message) error {
- if msg.IsText() {
- return g.ReplyText(msg)
+func GroupMessageContextHandler() func(ctx *openwechat.MessageContext) {
+ return func(ctx *openwechat.MessageContext) {
+ msg := ctx.Message
+ // 获取用户消息处理器
+ handler, err := NewGroupMessageHandler(msg)
+ if err != nil {
+ logger.Warning(fmt.Sprintf("init group message handler error: %s", err))
+ return
+ }
+
+ // 处理用户消息
+ err = handler.handle()
+ if err != nil {
+ logger.Warning(fmt.Sprintf("handle group message error: %s", err))
+ }
}
- return nil
}
// NewGroupMessageHandler 创建群消息处理器
-func NewGroupMessageHandler() MessageHandlerInterface {
- return &GroupMessageHandler{}
+func NewGroupMessageHandler(msg *openwechat.Message) (MessageHandlerInterface, error) {
+ sender, err := msg.Sender()
+ if err != nil {
+ return nil, err
+ }
+ group := &openwechat.Group{User: sender}
+ groupSender, err := msg.SenderInGroup()
+ if err != nil {
+ return nil, err
+ }
+
+ userService := service.NewUserService(c, groupSender)
+ handler := &GroupMessageHandler{
+ self: sender.Self,
+ msg: msg,
+ group: group,
+ sender: groupSender,
+ service: userService,
+ }
+ return handler, nil
+
}
-// ReplyText 发送文本消息到群
-func (g *GroupMessageHandler) ReplyText(msg *openwechat.Message) error {
- // 接收群消息
- sender, err := msg.Sender()
- group := openwechat.Group{sender}
- log.Printf("Received Group %v Text Msg : %v", group.NickName, msg.Content)
+// handle 处理消息
+func (g *GroupMessageHandler) handle() error {
+ if g.msg.IsText() {
+ return g.ReplyText()
+ }
+ return nil
+}
+
+// ReplyText 发息送文本消到群
+func (g *GroupMessageHandler) ReplyText() error {
+ logger.Info(fmt.Sprintf("Received Group %v Text Msg : %v", g.group.NickName, g.msg.Content))
+ var (
+ err error
+ reply string
+ )
- // 不是@的不处理
- if !msg.IsAt() {
+ // 1.不是@的不处理
+ if !g.msg.IsAt() {
return nil
}
- // 替换掉@文本,然后向GPT发起请求
- replaceText := "@" + sender.Self.NickName
- requestText := strings.TrimSpace(strings.ReplaceAll(msg.Content, replaceText, ""))
- reply, err := gtp.Completions(requestText)
- if err != nil {
- log.Printf("gtp request error: %v \n", err)
- msg.ReplyText("机器人神了,我一会发现了就去修。")
- return err
- }
- if reply == "" {
+ // 2.获取请求的文本,如果为空字符串不处理
+ requestText := g.getRequestText()
+ if requestText == "" {
+ logger.Info("user message is null")
return nil
}
- // 获取@我的用户
- groupSender, err := msg.SenderInGroup()
+ // 3.请求GPT获取回复
+ reply, err = gpt.Completions(requestText)
if err != nil {
- log.Printf("get sender in group error :%v \n", err)
+ // 2.1 将GPT请求失败信息输出给用户,省得整天来问又不知道日志在哪里。
+ errMsg := fmt.Sprintf("gpt request error: %v", err)
+ _, err = g.msg.ReplyText(errMsg)
+ if err != nil {
+ return errors.New(fmt.Sprintf("response group error: %v ", err))
+ }
return err
}
- // 回复@我的用户
- reply = strings.TrimSpace(reply)
- reply = strings.Trim(reply, "\n")
- atText := "@" + groupSender.NickName
- replyText := atText + reply
- _, err = msg.ReplyText(replyText)
+ // 4.设置上下文,并响应信息给用户
+ g.service.SetUserSessionContext(requestText, reply)
+ _, err = g.msg.ReplyText(g.buildReplyText(reply))
if err != nil {
- log.Printf("response group error: %v \n", err)
+ return errors.New(fmt.Sprintf("response user error: %v ", err))
}
+
+ // 5.返回错误信息
return err
}
+
+// getRequestText 获取请求接口的文本,要做一些清洗
+func (g *GroupMessageHandler) getRequestText() string {
+ // 1.去除空格以及换行
+ requestText := strings.TrimSpace(g.msg.Content)
+ requestText = strings.Trim(g.msg.Content, "\n")
+
+ // 2.替换掉当前用户名称
+ replaceText := "@" + g.self.NickName
+ requestText = strings.TrimSpace(strings.ReplaceAll(g.msg.Content, replaceText, ""))
+ if requestText == "" {
+ return ""
+ }
+
+ // 3.获取上下文,拼接在一起,如果字符长度超出4000,截取为4000。(GPT按字符长度算),达芬奇3最大为4068,也许后续为了适应要动态进行判断。
+ sessionText := g.service.GetUserSessionContext()
+ if sessionText != "" {
+ requestText = sessionText + "\n" + requestText
+ }
+ if len(requestText) >= 4000 {
+ requestText = requestText[:4000]
+ }
+
+ // 4.检查用户发送文本是否包含结束标点符号
+ punctuation := ",.;!?,。!?、…"
+ runeRequestText := []rune(requestText)
+ lastChar := string(runeRequestText[len(runeRequestText)-1:])
+ if strings.Index(punctuation, lastChar) < 0 {
+ requestText = requestText + "?" // 判断最后字符是否加了标点,没有的话加上句号,避免openai自动补齐引起混乱。
+ }
+
+ // 5.返回请求文本
+ return requestText
+}
+
+// buildReply 构建回复文本
+func (g *GroupMessageHandler) buildReplyText(reply string) string {
+ // 1.获取@我的用户
+ atText := "@" + g.sender.NickName
+ textSplit := strings.Split(reply, "\n\n")
+ if len(textSplit) > 1 {
+ trimText := textSplit[0]
+ reply = strings.Trim(reply, trimText)
+ }
+ reply = strings.TrimSpace(reply)
+ if reply == "" {
+ return atText + " 请求得不到任何有意义的回复,请具体提出问题。"
+ }
+
+ // 2.拼接回复,@我的用户,问题,回复
+ replaceText := "@" + g.self.NickName
+ question := strings.TrimSpace(strings.ReplaceAll(g.msg.Content, replaceText, ""))
+ reply = atText + "\n" + question + "\n --------------------------------\n" + reply
+ reply = strings.Trim(reply, "\n")
+
+ // 3.返回回复的内容
+ return reply
+}
diff --git a/handlers/handler.go b/handlers/handler.go
index 50d2b54..7a608df 100644
--- a/handlers/handler.go
+++ b/handlers/handler.go
@@ -1,53 +1,71 @@
package handlers
import (
- "github.com/869413421/wechatbot/config"
+ "fmt"
+ "github.com/qingconglaixueit/wechatbot/config"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
"github.com/eatmoreapple/openwechat"
+ "github.com/patrickmn/go-cache"
+ "github.com/skip2/go-qrcode"
"log"
+ "runtime"
+ "strings"
+ "time"
)
+var c = cache.New(config.LoadConfig().SessionTimeout, time.Minute*5)
+
// MessageHandlerInterface 消息处理接口
type MessageHandlerInterface interface {
- handle(*openwechat.Message) error
- ReplyText(*openwechat.Message) error
+ handle() error
+ ReplyText() error
}
-type HandlerType string
-
-const (
- GroupHandler = "group"
- UserHandler = "user"
-)
+// QrCodeCallBack 登录扫码回调,
+func QrCodeCallBack(uuid string) {
+ if runtime.GOOS == "windows" {
+ // 运行在Windows系统上
+ openwechat.PrintlnQrcodeUrl(uuid)
+ } else {
+ log.Println("login in linux")
+ url := "https://login.weixin.qq.com/l/" + uuid
+ log.Printf("如果二维码无法扫描,请缩小控制台尺寸,或更换命令行工具,缩小二维码像素")
+ q, _ := qrcode.New(url, qrcode.High)
+ fmt.Println(q.ToSmallString(true))
+ }
+}
-// handlers 所有消息类型类型的处理器
-var handlers map[HandlerType]MessageHandlerInterface
+func NewHandler() (msgFunc func(msg *openwechat.Message), err error) {
+ dispatcher := openwechat.NewMessageMatchDispatcher()
-func init() {
- handlers = make(map[HandlerType]MessageHandlerInterface)
- handlers[GroupHandler] = NewGroupMessageHandler()
- handlers[UserHandler] = NewUserMessageHandler()
-}
+ // 清空会话
+ dispatcher.RegisterHandler(func(message *openwechat.Message) bool {
+ return strings.Contains(message.Content, config.LoadConfig().SessionClearToken)
+ }, TokenMessageContextHandler())
-// Handler 全局处理入口
-func Handler(msg *openwechat.Message) {
- log.Printf("hadler Received msg : %v", msg.Content)
// 处理群消息
- if msg.IsSendByGroup() {
- handlers[GroupHandler].handle(msg)
- return
- }
+ dispatcher.RegisterHandler(func(message *openwechat.Message) bool {
+ return message.IsSendByGroup()
+ }, GroupMessageContextHandler())
// 好友申请
- if msg.IsFriendAdd() {
+ dispatcher.RegisterHandler(func(message *openwechat.Message) bool {
+ return message.IsFriendAdd()
+ }, func(ctx *openwechat.MessageContext) {
+ msg := ctx.Message
if config.LoadConfig().AutoPass {
- _, err := msg.Agree("你好我是基于chatGPT引擎开发的微信机器人,你可以向我提问任何问题。")
+ _, err := msg.Agree("")
if err != nil {
- log.Fatalf("add friend agree error : %v", err)
+ logger.Warning(fmt.Sprintf("add friend agree error : %v", err))
return
}
}
- }
+ })
// 私聊
- handlers[UserHandler].handle(msg)
+ // 获取用户消息处理器
+ dispatcher.RegisterHandler(func(message *openwechat.Message) bool {
+ return !(strings.Contains(message.Content, config.LoadConfig().SessionClearToken) || message.IsSendByGroup() || message.IsFriendAdd())
+ }, UserMessageContextHandler())
+ return openwechat.DispatchMessage(dispatcher), nil
}
diff --git a/handlers/token_msg_handler.go b/handlers/token_msg_handler.go
new file mode 100644
index 0000000..13393c3
--- /dev/null
+++ b/handlers/token_msg_handler.go
@@ -0,0 +1,79 @@
+package handlers
+
+import (
+ "fmt"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
+ "github.com/qingconglaixueit/wechatbot/service"
+ "github.com/eatmoreapple/openwechat"
+)
+
+var _ MessageHandlerInterface = (*TokenMessageHandler)(nil)
+
+// TokenMessageHandler 口令消息处理器
+type TokenMessageHandler struct {
+ // 接收到消息
+ msg *openwechat.Message
+ // 发送的用户
+ sender *openwechat.User
+ // 实现的用户业务
+ service service.UserServiceInterface
+}
+
+func TokenMessageContextHandler() func(ctx *openwechat.MessageContext) {
+ return func(ctx *openwechat.MessageContext) {
+ msg := ctx.Message
+ // 获取口令消息处理器
+ handler, err := NewTokenMessageHandler(msg)
+ if err != nil {
+ logger.Warning(fmt.Sprintf("init token message handler error: %s", err))
+ }
+
+ // 获取口令消息处理器
+ err = handler.handle()
+ if err != nil {
+ logger.Warning(fmt.Sprintf("handle token message error: %s", err))
+ }
+
+ }
+}
+
+// NewTokenMessageHandler 口令消息处理器
+func NewTokenMessageHandler(msg *openwechat.Message) (MessageHandlerInterface, error) {
+ sender, err := msg.Sender()
+ if err != nil {
+ return nil, err
+ }
+ if msg.IsComeFromGroup() {
+ sender, err = msg.SenderInGroup()
+ }
+ userService := service.NewUserService(c, sender)
+ handler := &TokenMessageHandler{
+ msg: msg,
+ sender: sender,
+ service: userService,
+ }
+
+ return handler, nil
+}
+
+// handle 处理口令
+func (t *TokenMessageHandler) handle() error {
+ return t.ReplyText()
+}
+
+// ReplyText 回复清空口令
+func (t *TokenMessageHandler) ReplyText() error {
+ logger.Info("user clear token")
+ t.service.ClearUserSessionContext()
+ var err error
+ if t.msg.IsComeFromGroup() {
+ if !t.msg.IsAt() {
+ return err
+ }
+ atText := "@" + t.sender.NickName + "上下文已经清空,请问下一个问题。"
+ _, err = t.msg.ReplyText(atText)
+ } else {
+ _, err = t.msg.ReplyText("上下文已经清空,请问下一个问题。")
+ }
+ return err
+}
diff --git a/handlers/user_msg_handler.go b/handlers/user_msg_handler.go
index e2e250f..8558526 100644
--- a/handlers/user_msg_handler.go
+++ b/handlers/user_msg_handler.go
@@ -1,9 +1,13 @@
package handlers
import (
- "github.com/869413421/wechatbot/gtp"
+ "errors"
+ "fmt"
"github.com/eatmoreapple/openwechat"
- "log"
+ "github.com/qingconglaixueit/wechatbot/config"
+ "github.com/qingconglaixueit/wechatbot/gpt"
+ "github.com/qingconglaixueit/wechatbot/pkg/logger"
+ "github.com/qingconglaixueit/wechatbot/service"
"strings"
)
@@ -11,46 +15,137 @@ var _ MessageHandlerInterface = (*UserMessageHandler)(nil)
// UserMessageHandler 私聊消息处理
type UserMessageHandler struct {
+ // 接收到消息
+ msg *openwechat.Message
+ // 发送的用户
+ sender *openwechat.User
+ // 实现的用户业务
+ service service.UserServiceInterface
}
-// handle 处理消息
-func (g *UserMessageHandler) handle(msg *openwechat.Message) error {
- if msg.IsText() {
- return g.ReplyText(msg)
+func UserMessageContextHandler() func(ctx *openwechat.MessageContext) {
+ return func(ctx *openwechat.MessageContext) {
+ msg := ctx.Message
+ handler, err := NewUserMessageHandler(msg)
+ if err != nil {
+ logger.Warning(fmt.Sprintf("init user message handler error: %s", err))
+ }
+
+ // 处理用户消息
+ err = handler.handle()
+ if err != nil {
+ logger.Warning(fmt.Sprintf("handle user message error: %s", err))
+ }
}
- return nil
}
// NewUserMessageHandler 创建私聊处理器
-func NewUserMessageHandler() MessageHandlerInterface {
- return &UserMessageHandler{}
+func NewUserMessageHandler(message *openwechat.Message) (MessageHandlerInterface, error) {
+ sender, err := message.Sender()
+ if err != nil {
+ return nil, err
+ }
+ userService := service.NewUserService(c, sender)
+ handler := &UserMessageHandler{
+ msg: message,
+ sender: sender,
+ service: userService,
+ }
+
+ return handler, nil
+}
+
+// handle 处理消息
+func (h *UserMessageHandler) handle() error {
+ if h.msg.IsText() {
+ return h.ReplyText()
+ }
+ return nil
}
// ReplyText 发送文本消息到群
-func (g *UserMessageHandler) ReplyText(msg *openwechat.Message) error {
- // 接收私聊消息
- sender, err := msg.Sender()
- log.Printf("Received User %v Text Msg : %v", sender.NickName, msg.Content)
-
- // 向GPT发起请求
- requestText := strings.TrimSpace(msg.Content)
- requestText = strings.Trim(msg.Content, "\n")
- reply, err := gtp.Completions(requestText)
+func (h *UserMessageHandler) ReplyText() error {
+ logger.Info(fmt.Sprintf("Received User %v Text Msg : %v", h.sender.NickName, h.msg.Content))
+ var (
+ reply string
+ err error
+ )
+ // 1.获取上下文,如果字符串为空不处理
+ requestText := h.getRequestText()
+ if requestText == "" {
+ logger.Info("user message is null")
+ return nil
+ }
+ logger.Info(fmt.Sprintf("h.sender.NickName == %+v", h.sender.NickName))
+ // 2.向GPT发起请求,如果回复文本等于空,不回复
+ reply, err = gpt.Completions(h.getRequestText())
if err != nil {
- log.Printf("gtp request error: %v \n", err)
- msg.ReplyText("机器人神了,我一会发现了就去修。")
+ // 2.1 将GPT请求失败信息输出给用户,省得整天来问又不知道日志在哪里。
+ errMsg := fmt.Sprintf("gpt request error: %v", err)
+ _, err = h.msg.ReplyText(errMsg)
+ if err != nil {
+ return errors.New(fmt.Sprintf("response user error: %v ", err))
+ }
return err
}
- if reply == "" {
- return nil
- }
- // 回复用户
- reply = strings.TrimSpace(reply)
- reply = strings.Trim(reply, "\n")
- _, err = msg.ReplyText(reply)
+ // 2.设置上下文,回复用户
+ h.service.SetUserSessionContext(requestText, reply)
+ _, err = h.msg.ReplyText(buildUserReply(reply))
if err != nil {
- log.Printf("response user error: %v \n", err)
+ return errors.New(fmt.Sprintf("response user error: %v ", err))
}
+
+ // 3.返回错误
return err
}
+
+// getRequestText 获取请求接口的文本,要做一些清晰
+func (h *UserMessageHandler) getRequestText() string {
+ // 1.去除空格以及换行
+ requestText := strings.TrimSpace(h.msg.Content)
+ requestText = strings.Trim(h.msg.Content, "\n")
+
+ // 2.获取上下文,拼接在一起,如果字符长度超出4000,截取为4000。(GPT按字符长度算),达芬奇3最大为4068,也许后续为了适应要动态进行判断。
+ sessionText := h.service.GetUserSessionContext()
+ if sessionText != "" {
+ requestText = sessionText + "\n" + requestText
+ }
+ if len(requestText) >= 4000 {
+ requestText = requestText[:4000]
+ }
+
+ // 3.检查用户发送文本是否包含结束标点符号
+ punctuation := ",.;!?,。!?、…"
+ runeRequestText := []rune(requestText)
+ lastChar := string(runeRequestText[len(runeRequestText)-1:])
+ if strings.Index(punctuation, lastChar) < 0 {
+ requestText = requestText + "?" // 判断最后字符是否加了标点,没有的话加上句号,避免openai自动补齐引起混乱。
+ }
+
+ // 4.返回请求文本
+ return requestText
+}
+
+// buildUserReply 构建用户回复
+func buildUserReply(reply string) string {
+ // 1.去除空格问号以及换行号,如果为空,返回一个默认值提醒用户
+ textSplit := strings.Split(reply, "\n\n")
+ if len(textSplit) > 1 {
+ trimText := textSplit[0]
+ reply = strings.Trim(reply, trimText)
+ }
+ reply = strings.TrimSpace(reply)
+
+ reply = strings.TrimSpace(reply)
+ if reply == "" {
+ return "请求得不到任何有意义的回复,请具体提出问题。"
+ }
+
+ // 2.如果用户有配置前缀,加上前缀
+ reply = config.LoadConfig().ReplyPrefix + "\n" + reply
+ reply = strings.Trim(reply, "\n")
+
+ // 3.返回拼接好的字符串
+ return reply
+}
diff --git a/main.go b/main.go
index 0e18cf1..17d0fbc 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,7 @@
package main
import (
- "github.com/869413421/wechatbot/bootstrap"
+ "github.com/qingconglaixueit/wechatbot/bootstrap"
)
func main() {
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
new file mode 100644
index 0000000..bdc0b4b
--- /dev/null
+++ b/pkg/logger/logger.go
@@ -0,0 +1,40 @@
+package logger
+
+import (
+ "log"
+ "os"
+ "sync"
+)
+
+var Logger *log.Logger
+var once sync.Once
+
+func init() {
+ once.Do(func() {
+ Logger = log.New(os.Stdout, "INFO", log.LstdFlags|log.Llongfile)
+ })
+}
+
+// Info 详情
+func Info(args ...interface{}) {
+ Logger.SetPrefix("[INFO]")
+ Logger.Println(args...)
+}
+
+// Danger 错误 为什么不命名为 error?避免和 error 类型重名
+func Danger(args ...interface{}) {
+ Logger.SetPrefix("[ERROR]")
+ Logger.Fatal(args...)
+}
+
+// Warning 警告
+func Warning(args ...interface{}) {
+ Logger.SetPrefix("[WARNING]")
+ Logger.Println(args...)
+}
+
+// DeBug debug
+func DeBug(args ...interface{}) {
+ Logger.SetPrefix("[DeBug]")
+ Logger.Println(args...)
+}
diff --git a/rule/rule.go b/rule/rule.go
new file mode 100644
index 0000000..f9f3608
--- /dev/null
+++ b/rule/rule.go
@@ -0,0 +1,56 @@
+package rule
+
+import (
+ "sync"
+ "time"
+)
+
+const (
+ STARTTIME = 9
+ ENDTIME = 21
+)
+
+type Rule struct{}
+
+var isWork = true
+var Grule = &Rule{}
+var lock sync.Mutex
+
+func (r *Rule) SetWork(work bool) {
+ lock.Lock()
+ defer lock.Unlock()
+ isWork = work
+ return
+}
+func (r *Rule) GetWork() bool {
+ lock.Lock()
+ defer lock.Unlock()
+ return isWork
+}
+
+// 判断时间在今天的早上 9点到 晚上 9 点区间内
+func (r *Rule) IsWorkTime(s int, e int) bool {
+ if s < 0 || s > 24 {
+ s = STARTTIME
+ }
+ if e < 0 || e > 24 || e <= s {
+ e = ENDTIME
+ }
+ t := time.Now()
+ startTime := time.Date(t.Year(), t.Month(), t.Day(), s, 0, 0, 0, time.Local)
+ endTime := time.Date(t.Year(), t.Month(), t.Day(), e, 0, 0, 0, time.Local)
+ // 判断当前时间是否在当天的 STARTTIME -- ENDTIME
+ if t.Unix() > startTime.Unix() && t.Unix() < endTime.Unix() {
+ return true
+ }
+ return false
+}
+
+func (r *Rule) InSlice(str string, sli []string) bool {
+ for _, v := range sli {
+ if v == str {
+ return true
+ }
+ }
+ return false
+}
diff --git a/service/user.go b/service/user.go
new file mode 100644
index 0000000..a128aad
--- /dev/null
+++ b/service/user.go
@@ -0,0 +1,62 @@
+package service
+
+import (
+ "github.com/qingconglaixueit/wechatbot/config"
+ "github.com/eatmoreapple/openwechat"
+ "github.com/patrickmn/go-cache"
+ "time"
+)
+
+// UserServiceInterface 用户业务接口
+type UserServiceInterface interface {
+ GetUserSessionContext() string
+ SetUserSessionContext(question, reply string)
+ ClearUserSessionContext()
+}
+
+var _ UserServiceInterface = (*UserService)(nil)
+
+// UserService 用戶业务
+type UserService struct {
+ // 缓存
+ cache *cache.Cache
+ // 用户
+ user *openwechat.User
+}
+
+// NewUserService 创建新的业务层
+func NewUserService(cache *cache.Cache, user *openwechat.User) UserServiceInterface {
+ return &UserService{
+ cache: cache,
+ user: user,
+ }
+}
+
+// ClearUserSessionContext 清空GTP上下文,接收文本中包含`我要问下一个问题`,并且Unicode 字符数量不超过20就清空
+func (s *UserService) ClearUserSessionContext() {
+ s.cache.Delete(s.user.ID())
+}
+
+// GetUserSessionContext 获取用户会话上下文文本
+func (s *UserService) GetUserSessionContext() string {
+ // 1.获取上次会话信息,如果没有直接返回空字符串
+ sessionContext, ok := s.cache.Get(s.user.ID())
+ if !ok {
+ return ""
+ }
+
+ // 2.如果字符长度超过等于4000,强制清空会话(超过GPT会报错)。
+ contextText := sessionContext.(string)
+ if len(contextText) >= 4000 {
+ s.cache.Delete(s.user.ID())
+ }
+
+ // 3.返回上文
+ return contextText
+}
+
+// SetUserSessionContext 设置用户会话上下文文本,question用户提问内容,GTP回复内容
+func (s *UserService) SetUserSessionContext(question, reply string) {
+ value := question + "\n" + reply
+ s.cache.Set(s.user.ID(), value, time.Second*config.LoadConfig().SessionTimeout)
+}
diff --git a/supervisord.conf b/supervisord.conf
new file mode 100644
index 0000000..dba32a9
--- /dev/null
+++ b/supervisord.conf
@@ -0,0 +1,13 @@
+[supervisord]
+nodaemon=true
+
+[program:wechatbot] ; 程序名称,在 supervisorctl 中通过这个值来对程序进行一系列的操作
+autorestart=True ; 程序异常退出后自动重启
+autostart=True ; 在 supervisord 启动的时候也自动启动
+redirect_stderr=True ; 把 stderr 重定向到 stdout,默认 false
+command=/app/wechatbot ; 启动命令,与手动在命令行启动的命令是一样的
+user=root ; 用哪个用户启动
+stdout_logfile_maxbytes = 20MB ; stdout 日志文件大小,默认 50MB
+stdout_logfile_backups = 20 ; stdout 日志文件备份数
+; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)
+stdout_logfile = /app/run.log
\ No newline at end of file