From c819d188af64adc98c71fe3a6a4c4095c7ad1b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=89=BE=E8=BF=AA?= <62269186+AnotiaWang@users.noreply.github.com> Date: Mon, 13 Mar 2023 00:25:36 +0800 Subject: [PATCH] Init --- .gitignore | 3 ++ go.mod | 8 ++++ go.sum | 6 +++ handler/message.go | 83 +++++++++++++++++++++++++++++++++ logicerr/chat_completion.go | 8 ++++ logicerr/config.go | 10 ++++ main.go | 65 ++++++++++++++++++++++++++ model/chat_completion.go | 93 +++++++++++++++++++++++++++++++++++++ model/config.go | 83 +++++++++++++++++++++++++++++++++ 9 files changed, 359 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handler/message.go create mode 100644 logicerr/chat_completion.go create mode 100644 logicerr/config.go create mode 100644 main.go create mode 100644 model/chat_completion.go create mode 100644 model/config.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a224f6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +config.yml +wechat_cache.json diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..03cfe71 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module ChatGPT_WeChat_Bot + +go 1.19 + +require ( + github.com/eatmoreapple/openwechat v1.4.1 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d711194 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/eatmoreapple/openwechat v1.4.1 h1:hIVEr2Xaj+r1SXzdTigqhIXiuu6TZd+NPWdEVVt/qeM= +github.com/eatmoreapple/openwechat v1.4.1/go.mod h1:ZxMcq7IpVWVU9JG7ERjExnm5M8/AQ6yZTtX30K3rwRQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/handler/message.go b/handler/message.go new file mode 100644 index 0000000..4882b66 --- /dev/null +++ b/handler/message.go @@ -0,0 +1,83 @@ +package handler + +import ( + "ChatGPT_WeChat_Bot/model" + "context" + "github.com/eatmoreapple/openwechat" + "log" + "strings" +) + +func Default(ctx context.Context) openwechat.MessageHandler { + self := ctx.Value(model.SelfKey).(*openwechat.Self) + dispatcher := openwechat.NewMessageMatchDispatcher() + + dispatcher.OnText(func(msgCtx *openwechat.MessageContext) { + msg := msgCtx.Message + + if len(msg.Content) <= 4 { + return + } + if strings.Index(msg.Content, " ") == 0 { + + } + if msg.Content[:4] == ":bot" { + query := msg.Content[4:] + + response, err := model.ChatCompletion(ctx, model.MakeMessage(query)) + if err != nil { + log.Println("ChatCompletion error: " + err.Error()) + return + } + log.Println("回复消息: " + response) + + receiver, err := msg.Receiver() + if err != nil { + log.Println("Receiver error: " + err.Error()) + return + } + log.Printf("receiver: %v, isSendBySelf: %v", receiver, msg.IsSendBySelf()) + sender, err := msg.Sender() + if err != nil { + log.Println("Sender error: " + err.Error()) + return + } + log.Printf("sender: %v, isSendBySelf: %v", sender, msg.IsSendBySelf()) + + if receiver != nil && receiver.UserName == "filehelper" { + log.Println("Reply to filehelper") + fh := self.FileHelper() + _, err := self.SendTextToFriend(fh, response) + if err != nil { + log.Println("SendTextToFriend error: " + err.Error()) + return + } + } else if receiver != nil && msg.IsSendBySelf() { + log.Println("Is send by self, isGroup:", receiver.IsGroup(), ", isSelf:", receiver.IsSelf(), ", isFriend:", receiver.IsFriend()) + if receiver.IsGroup() { + group, _ := receiver.AsGroup() + log.Println("Reply to group") + _, err := self.SendTextToGroup(group, response) + if err != nil { + log.Println("SendTextToGroup error: " + err.Error()) + } + } else { + user, _ := receiver.AsFriend() + log.Println("Reply to user") + _, err := self.SendTextToFriend(user, response) + if err != nil { + log.Println("SendTextToFriend error: " + err.Error()) + } + } + } else { + log.Println("Fallback reply to message") + _, err := msgCtx.ReplyText(response) + if err != nil { + log.Println("ReplyText error: " + err.Error()) + } + } + } + }) + + return dispatcher.AsMessageHandler() +} diff --git a/logicerr/chat_completion.go b/logicerr/chat_completion.go new file mode 100644 index 0000000..1be4df5 --- /dev/null +++ b/logicerr/chat_completion.go @@ -0,0 +1,8 @@ +package logicerr + +import "errors" + +var ( + ChatCompletionFailedError = errors.New("请求接口失败") + DecodeJSONFailedError = errors.New("解析 JSON 失败") +) diff --git a/logicerr/config.go b/logicerr/config.go new file mode 100644 index 0000000..d932d17 --- /dev/null +++ b/logicerr/config.go @@ -0,0 +1,10 @@ +package logicerr + +import "errors" + +var ( + ConfigFileNotFoundError = errors.New("未找到配置文件 config.yml") + OpenAISKNotSetError = errors.New("请在 config.yml 中设置 OpenAI SecretKey (SK)") + OpenAIModelNotSetError = errors.New("请在 config.yml 中设置要使用的 OpenAI 模型") + OpenAIEndpointNotSetError = errors.New("请在 config.yml 中设置 OpenAI API Endpoint") +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..ef15e9e --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "ChatGPT_WeChat_Bot/handler" + "ChatGPT_WeChat_Bot/logicerr" + "ChatGPT_WeChat_Bot/model" + "context" + "fmt" + "github.com/eatmoreapple/openwechat" + "log" +) + +func main() { + config := &model.Config{} + + if err := config.Load(); err != nil { + if err == logicerr.ConfigFileNotFoundError { + err = model.InitConfigFile() + if err != nil { + log.Fatal("创建配置文件 config.yml 失败: " + err.Error()) + } + log.Println("已创建配置文件 config.yml,请参考文档,填写必要的字段。") + return + } + panic(err) + } else if err := config.Validate(); err != nil { + log.Fatal(err.Error()) + } + + ctx := context.Background() + ctx = context.WithValue(ctx, model.ConfigKey, config) + + var bot *openwechat.Bot + + if config.WeChat.DesktopMode != nil && *config.WeChat.DesktopMode { + bot = openwechat.DefaultBot(openwechat.Desktop) + } else { + bot = openwechat.DefaultBot() + } + + reloadStorage := openwechat.NewFileHotReloadStorage("wechat_cache.json") + defer reloadStorage.Close() + + // 注册登陆二维码回调 + bot.UUIDCallback = openwechat.PrintlnQrcodeUrl + + // 登陆 + if err := bot.HotLogin(reloadStorage, openwechat.NewRetryLoginOption()); err != nil { + fmt.Println(err) + return + } + + // 获取登陆的用户 + self, err := bot.GetCurrentUser() + if err != nil { + fmt.Println(err) + return + } + ctx = context.WithValue(ctx, model.SelfKey, self) + + bot.MessageHandler = handler.Default(ctx) + + // 阻塞主 goroutine, 直到发生异常或者用户主动退出 + bot.Block() +} diff --git a/model/chat_completion.go b/model/chat_completion.go new file mode 100644 index 0000000..e421a7d --- /dev/null +++ b/model/chat_completion.go @@ -0,0 +1,93 @@ +package model + +import ( + "ChatGPT_WeChat_Bot/logicerr" + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "strings" +) + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type ChatCompletionRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` +} + +type ChatCompletionResponse struct { + Error struct { + Message string `json:"message"` + Type string `json:"type"` + Param string `json:"param"` + Code string `json:"code"` + } `json:"error"` + Id string `json:"id"` + Object string `json:"object"` + Created int32 `json:"created"` + Model string `json:"model"` + + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` + + Choices []struct { + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` + } +} + +func MakeMessage(content string) []Message { + messages := make([]Message, 0) + // TODO: config.OpenAI.SystemMessage + messages = append(messages, Message{ + Role: "user", + Content: content, + }) + + return messages +} + +func ChatCompletion(ctx context.Context, messages []Message) (string, error) { + config := ctx.Value(ConfigKey).(*Config) + + data, _ := json.Marshal(ChatCompletionRequest{ + Model: config.OpenAI.Model, + Messages: messages, + }) + + req, _ := http.NewRequest("POST", config.OpenAI.Endpoint, bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+config.OpenAI.SecretKey) + + client := http.Client{} + resp, _ := client.Do(req) + defer resp.Body.Close() + + body := ChatCompletionResponse{} + err := json.NewDecoder(resp.Body).Decode(&body) + if err != nil { + return "", errors.New(logicerr.DecodeJSONFailedError.Error() + ": " + err.Error()) + } + + if body.Error.Message != "" { + return "", errors.New(logicerr.ChatCompletionFailedError.Error() + ": " + body.Error.Message) + } + + content := strings.TrimFunc(body.Choices[0].Message.Content, func(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' + }) + + return content, nil +} diff --git a/model/config.go b/model/config.go new file mode 100644 index 0000000..0503690 --- /dev/null +++ b/model/config.go @@ -0,0 +1,83 @@ +package model + +import ( + "ChatGPT_WeChat_Bot/logicerr" + "fmt" + "gopkg.in/yaml.v2" + "os" +) + +type contextKey struct { + name string +} + +type Config struct { + WeChat *WeChatConfig `yaml:"wechat"` + OpenAI *OpenAIConfig `yaml:"openai"` +} + +var ConfigKey = &contextKey{"config"} +var SelfKey = &contextKey{"self"} + +type WeChatConfig struct { + // 使用 PC 模式而不是网页版,或许可以解决部分新号无法使用的问题 + DesktopMode *bool `yaml:"desktopMode"` +} + +type OpenAIConfig struct { + Endpoint string `yaml:"endpoint"` + Model string `yaml:"model"` + SecretKey string `yaml:"secretKey"` + Prefix string `yaml:"prefix"` +} + +func InitConfigFile() error { + str := `# ChatGPT WeChat Bot 配置文件 +wechat: + # 是否使用 PC 模式而不是网页版,或许可以解决部分新号无法使用的问题 + desktopMode: false +openai: + endpoint: https://api.openai.com/v1/chat/completions + # 模型 + model: gpt-3.5-turbo + # OpenAI SecretKey (SK) + secretKey: + # 前缀 + prefix: ChatGPT, +` + err := os.WriteFile("./config.yml", []byte(str), 0644) + if err != nil { + fmt.Println(err.Error()) + return err + } + return nil +} + +func (c *Config) Load() error { + yamlFile, err := os.ReadFile("./config.yml") + if err != nil { + if os.IsNotExist(err) { + return logicerr.ConfigFileNotFoundError + } + fmt.Println(err.Error()) + return err + } + err = yaml.Unmarshal(yamlFile, c) + if err != nil { + fmt.Println(err.Error()) + } + return nil +} + +func (c *Config) Validate() error { + if c.OpenAI.SecretKey == "" { + return logicerr.OpenAISKNotSetError + } else if c.OpenAI.Model == "" { + return logicerr.OpenAIModelNotSetError + } else if c.OpenAI.Endpoint == "" { + return logicerr.OpenAIEndpointNotSetError + } else if c.OpenAI.Prefix == "" { + c.OpenAI.Prefix = "ChatGPT," + } + return nil +}