This commit is contained in:
rainerosion 2023-04-15 15:09:10 +08:00
commit 074feb0f8d
20 changed files with 687 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
storage.json

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM golang:1.16 as builder
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o chatgpt
FROM alpine:3.17.2
WORKDIR /app
COPY --from=builder /app/chatgpt .
ENTRYPOINT ["./chatgpt"]

3
Makefile Normal file
View File

@ -0,0 +1,3 @@
build:
docker build -t yy194131/chatgpt:$(version) .
docker push yy194131/chatgpt:$(version)

39
README.md Normal file
View File

@ -0,0 +1,39 @@
## 微信机器人
最近ChatGPT异常火爆想到将其接入到个人微信是件比较有趣的事所以有了这个项目。项目基于[openwechat](https://github.com/eatmoreapple/openwechat)开发支持ChatGPT和New Bing其中New Bing[依赖于new-bing项目](../new-bing)提供的http接口。
## 目前实现了以下功能
+ 群聊@回复
+ 私聊回复
+ 自动通过回复
## 注册openai
ChatGPT注册可以参考[这里](https://juejin.cn/post/7173447848292253704)
## 部署
```
# 获取项目
git clone https://github.com/bujnlc8/gptbing
# 进入项目目录
cd gptbing/wechatbot
# 复制配置文件, 改成实际的值,注意去掉#注释
copy config.json.example config.json
# 启动项目
go run main.go
或者docker部署
bash start.sh 0.0.2 # 代理地址根据实际情况修改,运行之后`docker logs wechatbot`会打印出登录地址
```
## 说明
本项目fork至[https://github.com/djun/wechatbot](https://github.com/djun/wechatbot),修改使之支持`ChatGPT`和`New Bing`,在此致谢!
**⚠️ 有一定的几率导致被微信封号,请谨慎使用,由此导致的封号,本人概不负责**

34
bootstrap/bootstrap.go Normal file
View File

@ -0,0 +1,34 @@
package bootstrap
import (
"log"
"github.com/bujnlc8/wechatbot/handlers"
"github.com/bujnlc8/wechatbot/utils"
"github.com/eatmoreapple/openwechat"
)
func Run() {
//bot := openwechat.DefaultBot()
bot := openwechat.DefaultBot(openwechat.Desktop) // 桌面模式,上面登录不上的可以尝试切换这种模式
// 注册消息处理函数
bot.MessageHandler = handlers.Handler
// 注册登陆二维码回调
bot.UUIDCallback = openwechat.PrintlnQrcodeUrl
// 创建热存储容器对象
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
// 执行热登录
err := bot.HotLogin(reloadStorage)
if err != nil {
if err = bot.Login(); err != nil {
log.Printf("login error: %v \n", err)
return
}
}
// 阻塞主goroutine, 直到发生异常或者用户主动退出
if err := bot.Block(); err != nil {
utils.SendSimpleEmail("[!]wechatbot异常退出", err.Error())
}
}

8
config.json.example Normal file
View File

@ -0,0 +1,8 @@
{
"api_key": "", # chatgpt api key
"auto_pass": true, # 是否自动通过好友申请
"bing_chat_url": "" # New Bing 聊天接口
"bing_chat_wake_word": "#bing", # new Bing唤醒词
"gpt_chat_wake_word": "#gpt" # ChatGPT唤醒词
"gpt_message_cache": 1 # 消息缓存数量
}

64
config/config.go Normal file
View File

@ -0,0 +1,64 @@
package config
import (
"encoding/json"
"log"
"os"
"sync"
)
// Configuration 项目配置
type Configuration struct {
// gpt apikey
ApiKey string `json:"api_key"`
// 自动通过好友
AutoPass bool `json:"auto_pass"`
// bing 聊天接口
BingChatUrl string `json:"bing_chat_url"`
GptChatUrl string `json:"gpt_chat_url"`
// 机器人唤醒词
BingChatWakeWord string `json:"bing_chat_wake_word"`
GptChatWakeWord string `json:"gpt_chat_wake_word"`
// 消息缓存数量
GptMessageCache int `json:"gpt_message_cache"`
// gpt上下文清空指令
GptCleanContext string `json:"gpt_clean_context"`
}
var config *Configuration
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
}
// 如果环境变量有配置,读取环境变量
ApiKey := os.Getenv("ApiKey")
AutoPass := os.Getenv("AutoPass")
BingChatUrl := os.Getenv("BingChatUrl")
if ApiKey != "" {
config.ApiKey = ApiKey
}
if AutoPass == "true" {
config.AutoPass = true
}
if BingChatUrl != "" {
config.BingChatUrl = BingChatUrl
}
})
return config
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/bujnlc8/wechatbot
go 1.16
require github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7 h1:5vryo6dsmZZn1pYo8TrcqEUJq075QMpdDwLZz+oMfLs=
github.com/eatmoreapple/openwechat v1.4.2-0.20230321053318-1c102bdea8d7/go.mod h1:ZxMcq7IpVWVU9JG7ERjExnm5M8/AQ6yZTtX30K3rwRQ=

92
gpt/bing.go Normal file
View File

@ -0,0 +1,92 @@
package gpt
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"github.com/bujnlc8/wechatbot/config"
)
const Referer = "https://servicewechat.com/wxee7496be5b68b740"
type BingQueryParam struct {
Q string `json:"q"`
SID string `json:"sid"`
AutoReset string `json:"auto_reset"`
}
type BingResponse struct {
Data BingResponseData `json:"data"`
Cookie string `json:"cookie"`
}
type BingResponseData struct {
Suggests []string `json:"suggests"`
Status string `json:"status"`
Text string `json:"text"`
Message string `json:"message"`
}
func BingSearch(msg string, nickName string) (string, error) {
requestBody := BingQueryParam{
Q: msg,
SID: nickName,
AutoReset: "1",
}
requestData, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
log.Printf("request bing json string : %v", string(requestData))
BingChatUrl := config.LoadConfig().BingChatUrl
req, err := http.NewRequest("POST", BingChatUrl, bytes.NewBuffer(requestData))
if err != nil {
return "", err
}
req.Header.Set("Referer", Referer)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
return "非常抱歉😭,网络异常,请稍后重试", err
}
if response.StatusCode != 200 {
return "非常抱歉😭,网络异常,请稍后重试 [" + strconv.Itoa(response.StatusCode) + "]", nil
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "响应异常,请稍后再试", err
}
bingResponse := &BingResponse{}
log.Println(string(body))
err = json.Unmarshal(body, bingResponse)
if err != nil {
return "", err
}
if bingResponse.Data.Status == "Success" {
if strings.Contains(bingResponse.Data.Text, "New topic") {
return bingResponse.Data.Text + "\n请重新开始对话", nil
}
return bingResponse.Data.Text, nil
} else {
if bingResponse.Data.Status == "Throttled" {
return "这真是愉快,但你已达到每日限制。是否明天再聊?", nil
} else {
if strings.Contains(bingResponse.Data.Message, "has expired") {
return "本轮对话已过期,请重新开始。", nil
} else {
return "抱歉😭,发生错误:" + bingResponse.Data.Message + ",请重试", nil
}
}
}
}

15
gpt/bing_test.go Normal file
View File

@ -0,0 +1,15 @@
package gpt
import (
"fmt"
"testing"
)
func TestBing(t *testing.T) {
reply, err := BingSearch("今天北京的天气怎么样", "nickname")
if err != nil{
t.Error(err)
}
fmt.Printf("%+v\n", reply)
}

113
gpt/gpt.go Normal file
View File

@ -0,0 +1,113 @@
package gpt
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"github.com/bujnlc8/wechatbot/config"
)
var BASEURL = config.LoadConfig().GptChatUrl
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// ChatGPTResponseBody 请求体
type ChatGPTResponseBody struct {
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 {
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
// ChatGPTRequestBody 响应体
type ChatGPTRequestBody struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float32 `json:"temperature"`
Messages []Message `json:"messages"`
}
var MessageCacheRegistry = make(map[string][]Message)
func CleanContext(nickName string) (string, error) {
messageCache := []Message{}
MessageCacheRegistry[nickName] = messageCache
return "用户[" + nickName + "]上下文已清空", nil
}
func Completions(msg string, nickName string) (string, error) {
cache := config.LoadConfig().GptMessageCache
messageCache := MessageCacheRegistry[nickName]
message := Message{Role: "user", Content: msg}
if messageCache == nil || len(messageCache) == 0 {
messageCache = []Message{message}
} else {
messageCache = append(messageCache, message)
// 只保留20条
if len(messageCache) > cache {
messageCache = messageCache[(len(messageCache) - cache):]
}
}
MessageCacheRegistry[nickName] = messageCache
requestBody := ChatGPTRequestBody{
Model: "gpt-3.5-turbo",
MaxTokens: 4096,
Temperature: 1.2,
Messages: messageCache,
}
requestData, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
log.Printf("request gpt json string : %v", string(requestData))
req, err := http.NewRequest("POST", BASEURL+"completions", bytes.NewBuffer(requestData))
if err != nil {
return "", err
}
apiKey := config.LoadConfig().ApiKey
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
return "", err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
gptResponseBody := &ChatGPTResponseBody{}
log.Println(string(body))
err = json.Unmarshal(body, gptResponseBody)
if err != nil {
return "", err
}
var reply string
if len(gptResponseBody.Choices) > 0 {
for _, v := range gptResponseBody.Choices {
messageCache = append(messageCache, v.Message)
MessageCacheRegistry[nickName] = messageCache
reply = v.Message.Content
break
}
}
log.Printf("gpt response text: %s \n", reply)
return reply, nil
}

14
gpt/gpt_test.go Normal file
View File

@ -0,0 +1,14 @@
package gpt
import (
"fmt"
"testing"
)
func TestGpt(t *testing.T) {
reply, err := Completions("今天北京的天气怎么样", "nickname")
if err != nil {
t.Error(err)
}
fmt.Printf("%+v\n", reply)
}

View File

@ -0,0 +1,95 @@
package handlers
import (
"github.com/bujnlc8/wechatbot/config"
"log"
"strings"
"github.com/bujnlc8/wechatbot/gpt"
"github.com/eatmoreapple/openwechat"
)
var _ MessageHandlerInterface = (*GroupMessageHandler)(nil)
// GroupMessageHandler 群消息处理
type GroupMessageHandler struct {
}
// handle 处理消息
func (g *GroupMessageHandler) handle(msg *openwechat.Message) error {
bingWakeWord := config.LoadConfig().BingChatWakeWord
gptWakeWord := config.LoadConfig().GptChatWakeWord
if msg.IsText() {
return g.ReplyText(msg)
} else {
if strings.Contains(msg.Content, gptWakeWord) || strings.Contains(msg.Content, bingWakeWord) {
msg.ReplyText("目前我只支持文字哦~")
}
}
if msg.IsPaiYiPai() {
msg.ReplyText("我是机器人🤖️,会拍坏的哦~")
}
return nil
}
// NewGroupMessageHandler 创建群消息处理器
func NewGroupMessageHandler() MessageHandlerInterface {
return &GroupMessageHandler{}
}
// ReplyText 发送文本消息到群
func (g *GroupMessageHandler) ReplyText(msg *openwechat.Message) error {
sender, err := msg.Sender()
group := openwechat.Group{User: sender}
log.Printf("Received Group %v Text Msg : %v", group.NickName, msg.Content)
bingWakeWord := config.LoadConfig().BingChatWakeWord
gptWakeWord := config.LoadConfig().GptChatWakeWord
// @GPTBot 或者 @bing的消息才处理
if !(strings.Contains(msg.Content, gptWakeWord) || strings.Contains(msg.Content, bingWakeWord)) {
return nil
}
requestText := strings.TrimSpace(strings.ReplaceAll(msg.Content, gptWakeWord, ""))
var reply = ""
if strings.EqualFold(msg.Content, config.LoadConfig().GptCleanContext) {
cleanReply, _ := gpt.CleanContext(group.UserName)
reply = "\n" + cleanReply
} else if strings.Contains(msg.Content, bingWakeWord) {
requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, bingWakeWord, ""))
reply, err = gpt.BingSearch(requestText, group.UserName)
if reply != "" && strings.HasPrefix(reply, "[") {
reply = "\n" + reply
}
} else {
reply, err = gpt.Completions(requestText, group.UserName)
}
if err != nil {
log.Printf("gpt request error: %v \n", err)
msg.ReplyText("机器人神了,我一会发现了就去修。")
return err
}
if reply == "" {
msg.ReplyText("机器人响应为空")
return nil
}
// 获取@我的用户
groupSender, err := msg.SenderInGroup()
if err != nil {
log.Printf("get sender in group error :%v \n", err)
return err
}
// 回复@我的用户
reply = strings.TrimSpace(reply)
reply = strings.Trim(reply, "\n")
atText := "@" + groupSender.NickName
replyText := atText + " " + reply
_, err = msg.ReplyText(replyText)
if err != nil {
log.Printf("response group error: %v \n", err)
}
return err
}

52
handlers/handler.go Normal file
View File

@ -0,0 +1,52 @@
package handlers
import (
"log"
"github.com/bujnlc8/wechatbot/config"
"github.com/eatmoreapple/openwechat"
)
// MessageHandlerInterface 消息处理接口
type MessageHandlerInterface interface {
handle(*openwechat.Message) error
ReplyText(*openwechat.Message) error
}
type HandlerType string
const (
GroupHandler = "group"
UserHandler = "user"
)
// handlers 所有消息类型类型的处理器
var handlers map[HandlerType]MessageHandlerInterface
func init() {
handlers = make(map[HandlerType]MessageHandlerInterface)
handlers[GroupHandler] = NewGroupMessageHandler()
handlers[UserHandler] = NewUserMessageHandler()
}
// Handler 全局处理入口
func Handler(msg *openwechat.Message) {
log.Printf("hadler Received msg : %v", msg.Content)
// 处理群消息
if msg.IsSendByGroup() {
handlers[GroupHandler].handle(msg)
return
}
// 好友申请
if msg.IsFriendAdd() {
if config.LoadConfig().AutoPass {
_, err := msg.Agree("你好我是基于chatGPT引擎开发的微信机器人你可以向我提问任何问题。")
if err != nil {
log.Fatalf("add friend agree error : %v", err)
return
}
}
}
// 私聊
handlers[UserHandler].handle(msg)
}

View File

@ -0,0 +1,78 @@
package handlers
import (
"github.com/bujnlc8/wechatbot/config"
"log"
"strings"
"github.com/bujnlc8/wechatbot/gpt"
"github.com/eatmoreapple/openwechat"
)
var _ MessageHandlerInterface = (*UserMessageHandler)(nil)
// UserMessageHandler 私聊消息处理
type UserMessageHandler struct {
}
// handle 处理消息
func (g *UserMessageHandler) handle(msg *openwechat.Message) error {
if msg.IsText() {
return g.ReplyText(msg)
}
if msg.IsSendByFriend() {
if msg.IsPaiYiPai() {
msg.ReplyText("我是机器人🤖️,会拍坏的哦~")
} else {
msg.ReplyText("目前我只支持文字哦~")
}
}
return nil
}
// NewUserMessageHandler 创建私聊处理器
func NewUserMessageHandler() MessageHandlerInterface {
return &UserMessageHandler{}
}
// ReplyText 发送文本消息到群
func (g *UserMessageHandler) ReplyText(msg *openwechat.Message) error {
// 接收私聊消息
sender, err := msg.Sender()
log.Printf("Received User %v Text Msg : %v", sender.UserName, msg.Content)
requestText := strings.TrimSpace(msg.Content)
requestText = strings.Trim(msg.Content, "\n")
bingWakeWord := config.LoadConfig().BingChatWakeWord
gptWakeWord := config.LoadConfig().GptChatWakeWord
var reply = ""
if strings.Contains(msg.Content, bingWakeWord) {
requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, bingWakeWord, ""))
reply, err = gpt.BingSearch(requestText, sender.UserName)
} else if strings.Contains(msg.Content, gptWakeWord) {
requestText = strings.TrimSpace(strings.ReplaceAll(msg.Content, gptWakeWord, ""))
reply, err = gpt.Completions(requestText, sender.UserName)
} else {
//可以考虑对接其他机器人恢复信息
}
if err != nil {
log.Printf("gpt request error: %v \n", err)
msg.ReplyText("机器人神了,我一会发现了就去修。")
return err
}
if reply == "" {
msg.ReplyText("机器人响应为空")
return nil
}
// 回复用户
reply = strings.TrimSpace(reply)
reply = strings.Trim(reply, "\n")
_, err = msg.ReplyText(reply)
if err != nil {
log.Printf("response user error: %v \n", err)
}
return err
}

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"github.com/bujnlc8/wechatbot/bootstrap"
)
func main() {
bootstrap.Run()
}

9
start.sh Normal file
View File

@ -0,0 +1,9 @@
docker pull yy194131/chatgpt:$1
count=$(docker ps -a | grep wechatbot | wc -l)
if [ $count -gt 0 ]; then
docker stop wechatbot && docker rm wechatbot
fi
# 代理地址改成实际的地址
docker run --name wechatbot -d -e https_proxy=代理地址 -v $(pwd)/config.json:/app/config.json yy194131/chatgpt:$1

37
utils/send_mail.go Normal file
View File

@ -0,0 +1,37 @@
package utils
import (
"log"
"net/smtp"
"os"
)
const (
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = "587"
)
// 用qq邮箱发送邮件从New Bing写的代码修改而来
func SendSimpleEmail(subject string, body string) {
sender := os.Getenv("EMAIL_SENDER")
passwd := os.Getenv("EMAIL_PASSWD")
recipient := os.Getenv("EMAIL_RECIPIENT")
if len(recipient) == 0 {
recipient = sender
}
auth := smtp.PlainAuth("", sender, passwd, SMTP_SERVER)
header := make(map[string]string)
header["To"] = recipient
header["From"] = sender
header["Subject"] = subject
headerStr := ""
for k, v := range header {
headerStr += k + ": " + v + "\r\n"
}
message := headerStr + "\r\n\r\n" + body
err := smtp.SendMail(SMTP_SERVER+":"+SMTP_PORT, auth, sender, []string{recipient}, []byte(message))
if err != nil {
log.Fatal(err)
}
log.Println("Email sent successfully")
}