-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c819d18
Showing
9 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.idea | ||
config.yml | ||
wechat_cache.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 失败") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |