Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
AnotiaWang committed Mar 12, 2023
0 parents commit c819d18
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
config.yml
wechat_cache.json
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
83 changes: 83 additions & 0 deletions handler/message.go
Original file line number Diff line number Diff line change
@@ -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()
}
8 changes: 8 additions & 0 deletions logicerr/chat_completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package logicerr

import "errors"

var (
ChatCompletionFailedError = errors.New("请求接口失败")
DecodeJSONFailedError = errors.New("解析 JSON 失败")
)
10 changes: 10 additions & 0 deletions logicerr/config.go
Original file line number Diff line number Diff line change
@@ -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")
)
65 changes: 65 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -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()
}
93 changes: 93 additions & 0 deletions model/chat_completion.go
Original file line number Diff line number Diff line change
@@ -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
}
83 changes: 83 additions & 0 deletions model/config.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit c819d18

Please sign in to comment.