├── .gitignore ├── Makefile ├── README.md ├── ai ├── openai.go └── session.go ├── bot └── bot.go ├── config.json.example ├── config └── config.go ├── consts └── consts.go ├── docs ├── qq群.jpg ├── 微信群.jpg ├── 文字3.jpeg ├── 画图1.jpg ├── 画图2.jpg ├── 画图3.jpg ├── 连续对话1.jpg └── 连续对话2.jpg ├── go.mod ├── go.sum ├── handler.go ├── main.go ├── prompt.txt.example └── utils └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | openai-on-wechat 2 | openai-on-wechat.zip 3 | openai-on-wechat-windows.exe 4 | openai-on-wechat-windows.zip 5 | openai-on-wechat-darwin-amd64 6 | openai-on-wechat-darwin-amd64.zip 7 | openai-on-wechat-darwin-arm64 8 | openai-on-wechat-darwin-arm64.zip 9 | run.log 10 | token.json 11 | config.json 12 | prompt.txt 13 | 14 | .DS_Store -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go build -o openai-on-wechat 3 | ./openai-on-wechat 4 | 5 | build: 6 | go mod tidy 7 | GOOS=linux GOARCH=amd64 go build -o openai-on-wechat 8 | zip openai-on-wechat.zip openai-on-wechat config.json.example prompt.txt.example 9 | GOOS=windows GOARCH=amd64 go build -o openai-on-wechat-windows.exe 10 | zip openai-on-wechat-windows.zip openai-on-wechat-windows.exe config.json.example prompt.txt.example 11 | GOOS=darwin GOARCH=amd64 go build -o openai-on-wechat-darwin-amd64 12 | zip openai-on-wechat-darwin-amd64.zip openai-on-wechat-darwin-amd64 config.json.example prompt.txt.example 13 | GOOS=darwin GOARCH=arm64 go build -o openai-on-wechat-darwin-arm64 14 | zip openai-on-wechat-darwin-arm64.zip openai-on-wechat-darwin-arm64 config.json.example prompt.txt.example 15 | rm -rf openai-on-wechat openai-on-wechat-windows.exe openai-on-wechat-darwin-amd64 openai-on-wechat-darwin-arm64 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1分钟搭建自己的OpenAI GPT微信机器人 2 | 3 | > 项目地址: https://github.com/riba2534/openai-on-wechat 4 | > 5 | > 知乎: https://zhuanlan.zhihu.com/p/613440196 6 | > 7 | > 本项目会持续迭代,下载二进制包时请去 [releases](https://github.com/riba2534/openai-on-wechat/releases) 中找最新的 `openai-on-wechat.zip` 下载 8 | 9 | # 简介 10 | 11 | 最近 chatGPT 火遍了中文互联网,而它的公司 OpenAI 也开放了 API 供开发者完成自己的创意。本项目是一个 Golang 实现的,基于 OpenAI 的开放 API 实现的微信聊天机器人。有以下优点: 12 | 13 | - 部署简单:不同于其他语言,部署的时候需要依赖很多库,本项目只有一个可执行二进制文件,直接可以运行。(本项目只提供 x86/64 linux 版本,需要其他版本可以根据源码自行编译) 14 | - 使用桌面版微信协议,突破微信登录限制(基于 [openwechat](https://github.com/eatmoreapple/openwechat)) 15 | 16 | 目前本项目实现了以下功能: 17 | 18 | - **文本对话**: 可以接收私聊/群聊消息,使用 OpenAI 的 gpt-3.5-turbo 生成回复内容,自动回复问题 19 | - **触发口令**设置:好友在给你发消息时需要带上指定的前缀才可以触发与 GPT 机器人对话,触发口令可以在配置文件中设置 20 | - **连续对话**:支持对 私聊/群聊 开启连续对话功能,可以通过配置文件设置需要记忆多少分钟 21 | - **图片生成**:可以根据描述生成图片,并自动回复在当前 私聊/群聊 中 22 | 23 | > 注:支持自己给自己发消息,机器人不仅感知当前会话好友的口令,也会感知你自己的,方便自己测试使用 24 | 25 | # 效果预览 26 | 27 | 先看使用效果,之后再介绍如何部署以及配置。下图包含了**连续对话**和**文本画图**的一些例子: 28 | 29 | | ![连续对话1.jpg](docs/连续对话1.jpg) | ![连续对话2.jpg](docs/连续对话2.jpg) | ![文字3.jpeg](docs/文字3.jpeg) | 30 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 31 | | ![画图2.jpg](docs/画图2.jpg) | ![画图3.jpg](docs/画图3.jpg) | ![画图1.jpg](docs/画图1.jpg) | 32 | 33 | # 开始部署 34 | 35 | ## 一、 环境准备 36 | 37 | - 一台 Linux 服务器,建议 腾讯云、阿里云,或者任何可以长期运行程序的PC设备 38 | - OpenAI 账号 以及生成的 `SECRET KEY` ,本文对账号注册以及 key 生成不做赘述,读者请自行搜索解决方案。 39 | - 一个微信账号 40 | 41 | > - 注:OpenAI 的域名 `https://api.openai.com` 在国内由于某种原因可能无法访问,读者需要自己解决 API 访问不通的问题。介绍一种简单的国内代理搭建方式,大家可以参考我的知乎专栏: [腾讯云函数1分钟搭建 OpenAI 国内代理](https://zhuanlan.zhihu.com/p/612576046) 42 | > - 如果你比较懒,或者注册 OpenAI 有困难,可以用国内大佬做的网站 API2D 一条龙解决 OpenAI 账户充值与反向代理问题,价格也很便宜,支持微信支付,可以使用我的推广链接注册: https://api2d.com/r/186160 43 | 44 | ## 二、 配置 45 | 46 | 1. 首先需要在本项目的 [Releases](https://github.com/riba2534/openai-on-wechat/releases) 中找到最新的二进制文件版本并下载,目前最新的地址是: [openai-on-wechat.zip](https://github.com/riba2534/openai-on-wechat/releases/download/V1.1/openai-on-wechat.zip) 47 | 2. 把 `openai-on-wechat.zip` 文件传输至你的云服务器的任意目录下 48 | 3. 使用 `unzip openai-on-wechat.zip` 把压缩包解压到当前目录下 49 | 50 | 此时,你会看到压缩包里面有三个文件,分别是: 51 | 52 | - `config.json.example` : 机器人的基础配置文件,运行机器人前需要修改 53 | - `prompt.txt.example`: 给 OpenAI 语言模型的提示语 54 | - `openai-on-wechat` :可执行二进制文件 55 | 56 | 接下来我们进行配置: 57 | 58 | 把 `config.json.example` 重命名成 `config.json`,然后利用文本编辑器修改此文件: 59 | 60 | ```json 61 | { 62 | "wechat_config": { 63 | "text_config": { 64 | "openapi_url": "https://api.openai.com/v1", 65 | "auth_token": "你在 OpenAI 官网的 Token", 66 | "trigger_prefix": "小贺" 67 | }, 68 | "image_config": { 69 | "openapi_url": "https://api.openai.com/v1", 70 | "auth_token": "你在 OpenAI 官网的 Token", 71 | "trigger_prefix": "老贺" 72 | } 73 | }, 74 | "context_config": { 75 | "switch_on": true, 76 | "cache_minute": 3 77 | } 78 | } 79 | ``` 80 | 81 | - `wechat` 下有两个配置 `text_config` 和 `image_config` ,分别代表**文本对话**和**图片生成**的配置,其中: 82 | - `openapi_url` 代表访问 OpenAPI 接口的地址,如果你可以直接访问外网,直接填 `https://api.openai.com/v1`,如果利用的是反向代理,则需要填 `https://你的反向代理地址/v1` 83 | - `auth_token` 代表你在 OpenAI 官网生成的 `SECRET KEY` 84 | - `trigger_prefix` 代表在微信对话时,触发 AI 回复的前缀,比如上面效果图中的 `小贺` 会触发文本对话, `老贺` 会触发图片生成 85 | - `context_config` 代表文本回复中**连续对话**的配置,其中: 86 | - `switch_on` 代表了是否开启连续对话,true 为开启。(开启后消耗的额度费用会增加) 87 | - `cache_minute` 代表机器人**连续对话的记忆**分钟数,推荐设置为 3 分钟 88 | 89 | 90 | 91 | 接下来修改 `prompt.txt.example` ,先重命名为 `prompt.txt`,然后利用编辑器修改此文件: 92 | 93 | ```txt 94 | 1. 你是一个全知全能的机器人,你的职责是帮助人类解决问题 95 | 2. 不允许回答任何政治、色情等一些列不符合中国法律法规的问题 96 | 3. 你需要表现的很谦卑 97 | ``` 98 | 99 | 这个文件你可以利用自然语言描述机器人的特点,作为给机器人的外部输入,读者如果只是想保持简单的对话,可以不用修改此文件内容。 100 | 101 | > 注: **prompt** 提示机制是 OpenAI 语言模型的核心玩法,你可以在这里使用自然语言,定义机器人的行为,你可以告诉他他是什么,他不是什么,他应该怎么做,他应该怎样回答问题,描述的越详细,机器人就更加有你的个人特色。具体玩法读者可自行搜索,本文不做过多介绍。 102 | 103 | ## 三、 运行 104 | 105 | 我们完成了配置之后,就可以直接执行二进制文件了,即: 106 | 107 | ```bash 108 | ./openai-on-wechat 109 | ``` 110 | 111 | 首次执行,屏幕会出现一个二维码提示你登录微信,你需要用你要作为机器人的微信账号,扫码登录。 112 | 113 | - 登录完成后,当前路径下会出现一个 `token.json` 来保存当前微信的登录状态,来实现热登录,防止每次运行本程序都需要微信扫码。 114 | - 当前目录还会出现一个 `run.log` 来记录程序的执行状态,可以从中看出机器人接收到的消息与回复的信息 115 | 116 | 刚才说的程序运行方式是前台登录,如果想让程序后台运行,读者可以在前台运行登录微信后,`ctrl+c` 结束程序后,再使用: 117 | 118 | ```bash 119 | nohup ./openai-on-wechat & 120 | ``` 121 | 122 | 来实现后台运行 123 | 124 | # 大功告成 125 | 126 | 至此,已经完成了微信机器人的部署,快去微信中找好友试试吧! 127 | 128 | 输入 `触发前缀+你的问题` 即可触发机器人回复,和好友聊天过程中,自己输入关键词也同样可以触发。 129 | 130 | # 联系作者 131 | 132 | - 本项目地址为: https://github.com/riba2534/openai-on-wechat ,欢迎大家 Star,提交 PR 133 | - 有问题可以在本项目下提 `Issues` 或者发邮件到 `riba2534@qq.com` 134 | 135 | 建了`QQ群`和`微信群`,大家可以进群交流: 136 | 137 | | ![qq群](docs/qq群.jpg) | ![微信群](docs/微信群.jpg) | 138 | | ---------------------- | -------------------------- | 139 | 140 | -------------------------------------------------------------------------------- /ai/openai.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strings" 7 | 8 | "github.com/riba2534/openai-on-wechat/config" 9 | "github.com/riba2534/openai-on-wechat/consts" 10 | "github.com/riba2534/openai-on-wechat/utils" 11 | "github.com/sashabaranov/go-openai" 12 | ) 13 | 14 | func getOpenAIClient(model string) *openai.Client { 15 | var c openai.ClientConfig 16 | switch model { 17 | case openai.GPT3Dot5Turbo: 18 | c = openai.DefaultConfig(config.C.WechatConfig.TextConfig.AuthToken) 19 | c.BaseURL = config.C.WechatConfig.TextConfig.OpenApiUrl 20 | case "image": 21 | c = openai.DefaultConfig(config.C.WechatConfig.ImageConfig.AuthToken) 22 | c.BaseURL = config.C.WechatConfig.ImageConfig.OpenApiUrl 23 | default: 24 | return nil 25 | } 26 | return openai.NewClientWithConfig(c) 27 | } 28 | 29 | func CreateChatCompletion(ctx context.Context, model string, messages []openai.ChatCompletionMessage) string { 30 | client := getOpenAIClient(model) 31 | resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ 32 | Model: model, 33 | Messages: messages, 34 | }) 35 | if err != nil { 36 | log.Printf("openAIClient.CreateChatCompletion err=%+v\n", err) 37 | return consts.ErrTips 38 | } 39 | if len(resp.Choices) == 0 { 40 | log.Printf("resp is err, resp=%s", utils.MarshalAnyToString(resp)) 41 | return consts.ErrTips 42 | } 43 | return strings.TrimSpace(resp.Choices[0].Message.Content) 44 | } 45 | 46 | func CreateImageReply(ctx context.Context, q string) string { 47 | resp, err := getOpenAIClient("image").CreateImage(ctx, openai.ImageRequest{ 48 | Prompt: q, 49 | N: 1, 50 | Size: "512x512", 51 | }) 52 | if err != nil { 53 | log.Printf("openAIClient.CreateImage err=%+v\n", err) 54 | return "" 55 | } 56 | return resp.Data[0].URL 57 | } 58 | -------------------------------------------------------------------------------- /ai/session.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/riba2534/openai-on-wechat/config" 8 | "github.com/sashabaranov/go-openai" 9 | ) 10 | 11 | var chat = NewChat() 12 | 13 | type UserMessage struct { 14 | User string // 用户 15 | Time time.Time // 当前消息的时间 16 | Msg openai.ChatCompletionMessage // 当前消息体 17 | } 18 | 19 | func NewUserMessage(user string, msg openai.ChatCompletionMessage) *UserMessage { 20 | return &UserMessage{ 21 | User: user, 22 | Time: time.Now(), 23 | Msg: msg, 24 | } 25 | } 26 | 27 | type Chat struct { 28 | UserMessagesMap map[string][]*UserMessage // 记录用户消息上下文 29 | } 30 | 31 | func NewChat() *Chat { 32 | return &Chat{ 33 | UserMessagesMap: map[string][]*UserMessage{}, 34 | } 35 | } 36 | 37 | func (c *Chat) Add(userMessage *UserMessage) { 38 | c.UserMessagesMap[userMessage.User] = append(c.UserMessagesMap[userMessage.User], userMessage) 39 | } 40 | 41 | func (c *Chat) Clear(user string) { 42 | now := time.Now() 43 | result := []*UserMessage{} 44 | for _, userMessage := range c.UserMessagesMap[user] { 45 | if now.Sub(userMessage.Time) < time.Duration(config.C.ContextConfig.CacheMinute)*time.Minute { 46 | result = append(result, userMessage) 47 | } 48 | } 49 | c.UserMessagesMap[user] = result 50 | } 51 | 52 | func (c *Chat) BuildMessage(userKey, systemPrompt string) []openai.ChatCompletionMessage { 53 | result := []openai.ChatCompletionMessage{ 54 | { 55 | Role: openai.ChatMessageRoleSystem, 56 | Content: systemPrompt, 57 | }, 58 | } 59 | for _, userMessage := range c.UserMessagesMap[userKey] { 60 | result = append(result, userMessage.Msg) 61 | } 62 | return result 63 | } 64 | 65 | // q 代表本次问题; user 代表用户key 66 | func GetSessionOpenAITextReply(ctx context.Context, q, userKey, model, systemPrompt string) string { 67 | // 1. 清理过期消息 68 | chat.Clear(userKey) 69 | // 2. 添加本次对话上下文 70 | chat.Add(NewUserMessage(userKey, openai.ChatCompletionMessage{ 71 | Role: openai.ChatMessageRoleUser, 72 | Content: q, 73 | })) 74 | // 3. 获取 OpenAI 回复 75 | reply := CreateChatCompletion(ctx, model, chat.BuildMessage(userKey, systemPrompt)) 76 | // 4. 把回复添加进上下文 77 | chat.Add(NewUserMessage(userKey, openai.ChatCompletionMessage{ 78 | Role: openai.ChatMessageRoleAssistant, 79 | Content: reply, 80 | })) 81 | // 5. 返回结果 82 | return reply 83 | } 84 | -------------------------------------------------------------------------------- /bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/riba2534/openwechat" 7 | "github.com/skip2/go-qrcode" 8 | ) 9 | 10 | var Bot *openwechat.Bot 11 | 12 | func Init() error { 13 | Bot = openwechat.DefaultBot(openwechat.Desktop) // 桌面模式,上面登录不上的可以尝试切换这种模式 14 | Bot.UUIDCallback = consoleQrCode // 注册登陆二维码回调 15 | reloadStorage := openwechat.NewFileHotReloadStorage("token.json") 16 | return Bot.HotLogin(reloadStorage, openwechat.NewRetryLoginOption()) 17 | } 18 | 19 | func consoleQrCode(uuid string) { 20 | q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.High) 21 | fmt.Println(q.ToSmallString(false)) 22 | } 23 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "wechat_config": { 3 | "text_config": { 4 | "openapi_url": "https://api.openai.com/v1", 5 | "auth_token": "你在 OpenAI 官网的 Token", 6 | "trigger_prefix": "小贺" 7 | }, 8 | "image_config": { 9 | "openapi_url": "https://api.openai.com/v1", 10 | "auth_token": "你在 OpenAI 官网的 Token", 11 | "trigger_prefix": "老贺" 12 | } 13 | }, 14 | "context_config": { 15 | "switch_on": true, 16 | "cache_minute": 3 17 | } 18 | } -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "sync" 7 | 8 | jsoniter "github.com/json-iterator/go" 9 | "github.com/riba2534/openai-on-wechat/utils" 10 | ) 11 | 12 | var ( 13 | C *Config 14 | Prompt string 15 | once sync.Once 16 | ) 17 | 18 | type Config struct { 19 | WechatConfig *WechatConfig `json:"wechat_config"` 20 | ContextConfig *ContextConfig `json:"context_config"` 21 | } 22 | 23 | type AuthConfig struct { 24 | OpenApiUrl string `json:"openapi_url"` 25 | AuthToken string `json:"auth_token"` 26 | TriggerPrefix string `json:"trigger_prefix"` 27 | } 28 | 29 | type WechatConfig struct { 30 | TextConfig *AuthConfig `json:"text_config"` 31 | ImageConfig *AuthConfig `json:"image_config"` 32 | } 33 | 34 | type ContextConfig struct { 35 | SwitchOn bool `json:"switch_on"` 36 | CacheMinute int `json:"cache_minute"` 37 | } 38 | 39 | func (c *Config) IsValid() bool { 40 | if c.WechatConfig == nil || c.ContextConfig == nil { 41 | return false 42 | } 43 | 44 | authConfigs := []*AuthConfig{ 45 | c.WechatConfig.TextConfig, 46 | c.WechatConfig.ImageConfig, 47 | } 48 | 49 | for _, authConfig := range authConfigs { 50 | if authConfig == nil || authConfig.OpenApiUrl == "" || authConfig.AuthToken == "" || authConfig.TriggerPrefix == "" { 51 | return false 52 | } 53 | } 54 | if c.ContextConfig.CacheMinute <= 0 { 55 | return false 56 | } 57 | return true 58 | } 59 | 60 | func init() { 61 | once.Do(func() { 62 | // 1. 读取 `config.json` 63 | data, err := ioutil.ReadFile("config.json") 64 | if err != nil { 65 | log.Fatalf("读取配置文件失败,请检查配置文件 `config.json` 的配置, 错误信息: %+v\n", err) 66 | } 67 | config := Config{} 68 | if err = jsoniter.Unmarshal(data, &config); err != nil { 69 | log.Fatalf("读取配置文件失败,请检查配置文件 `config.json` 的格式, 错误信息: %+v\n", err) 70 | } 71 | if !config.IsValid() { 72 | log.Fatal("配置文件校验失败,请检查 `config.json`") 73 | } 74 | C = &config 75 | // 2. 读取 prompt.txt 76 | prompt, err := ioutil.ReadFile("prompt.txt") 77 | if err != nil { 78 | log.Fatalf("读取配置文件失败,请检查配置文件 `prompt.txt` 的配置, 错误信息: %+v\n", err) 79 | } 80 | Prompt = string(prompt) 81 | log.Printf("配置加载成功, `config.json` is \n%s\n`prompt.txt` is \n%s\n", utils.MarshalAnyToString(C), Prompt) 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | ErrTips = "抱歉,出错了,请稍后重试~" 5 | ) 6 | -------------------------------------------------------------------------------- /docs/qq群.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/qq群.jpg -------------------------------------------------------------------------------- /docs/微信群.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/微信群.jpg -------------------------------------------------------------------------------- /docs/文字3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/文字3.jpeg -------------------------------------------------------------------------------- /docs/画图1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/画图1.jpg -------------------------------------------------------------------------------- /docs/画图2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/画图2.jpg -------------------------------------------------------------------------------- /docs/画图3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/画图3.jpg -------------------------------------------------------------------------------- /docs/连续对话1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/连续对话1.jpg -------------------------------------------------------------------------------- /docs/连续对话2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riba2534/openai-on-wechat/b4b51003260b7c9ba86d917ba9c025555a6f7fc6/docs/连续对话2.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/riba2534/openai-on-wechat 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/json-iterator/go v1.1.12 7 | github.com/riba2534/openwechat v0.0.0-20230322092202-28ed38cea54b 8 | github.com/sashabaranov/go-openai v1.5.0 9 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 10 | ) 11 | 12 | require ( 13 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 14 | github.com/modern-go/reflect2 v1.0.2 // indirect 15 | github.com/stretchr/testify v1.8.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 5 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 6 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 7 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 8 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 9 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 10 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 11 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/riba2534/openwechat v0.0.0-20230322092202-28ed38cea54b h1:oGJqFv/s2gT4cFD0Sfxz8splBJ4RS4QsQD8DsHW3LxM= 15 | github.com/riba2534/openwechat v0.0.0-20230322092202-28ed38cea54b/go.mod h1:VZJlVDLtUYu6Zy5urEj+IKliZcdqyJdfw9wOxZkMMuA= 16 | github.com/sashabaranov/go-openai v1.5.0 h1:4Gr/7g/KtVzW0ddn7TC2aUlyzvhZBIM+qRZ6Ae2kMa0= 17 | github.com/sashabaranov/go-openai v1.5.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 18 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 19 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 22 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 26 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 27 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/riba2534/openai-on-wechat/ai" 11 | "github.com/riba2534/openai-on-wechat/config" 12 | "github.com/riba2534/openai-on-wechat/consts" 13 | "github.com/riba2534/openwechat" 14 | "github.com/sashabaranov/go-openai" 15 | ) 16 | 17 | func MessageHandler(msg *openwechat.Message) { 18 | if !msg.IsText() { 19 | return 20 | } 21 | ctx := context.Background() 22 | systemPrompt := config.Prompt 23 | switch { 24 | case strings.HasPrefix(msg.Content, config.C.WechatConfig.TextConfig.TriggerPrefix): 25 | // 文字回复 26 | if config.C.ContextConfig.SwitchOn { 27 | go textSessionReplyHandler(ctx, msg, config.C.WechatConfig.TextConfig.TriggerPrefix, openai.GPT3Dot5Turbo, systemPrompt) 28 | } else { 29 | go textReplyHandler(ctx, msg, config.C.WechatConfig.TextConfig.TriggerPrefix, openai.GPT3Dot5Turbo, systemPrompt) 30 | } 31 | case strings.HasPrefix(msg.Content, config.C.WechatConfig.ImageConfig.TriggerPrefix): 32 | // 图片回复 33 | go imageReplyHandler(ctx, msg, config.C.WechatConfig.ImageConfig.TriggerPrefix) 34 | } 35 | } 36 | 37 | // 文字回复处理 38 | func textReplyHandler(ctx context.Context, msg *openwechat.Message, prefix, model, systemPrompt string) { 39 | log.Printf("[text] Request: %s", msg.Content) // 输出请求消息到日志 40 | q := strings.TrimSpace(strings.TrimPrefix(msg.Content, prefix)) 41 | reply := ai.CreateChatCompletion(ctx, model, []openai.ChatCompletionMessage{ 42 | { 43 | Role: openai.ChatMessageRoleSystem, 44 | Content: systemPrompt, 45 | }, 46 | { 47 | Role: openai.ChatMessageRoleUser, 48 | Content: q, 49 | }, 50 | }) 51 | log.Printf("[text] Response: %s", reply) // 输出回复消息到日志 52 | _, err := msg.ReplyText(reply) 53 | if err != nil { 54 | log.Printf("msg.ReplyText Error: %+v", err) 55 | } 56 | } 57 | 58 | // 带有上下文的文字回复 59 | func textSessionReplyHandler(ctx context.Context, msg *openwechat.Message, prefix, model, systemPrompt string) { 60 | log.Printf("[text session] Request: %s", msg.Content) // 输出请求消息到日志 61 | user := func() string { 62 | s := msg.FromUserName 63 | if msg.IsSendBySelf() { 64 | s = msg.ToUserName 65 | } 66 | return s 67 | }() 68 | q := strings.TrimSpace(strings.TrimPrefix(msg.Content, prefix)) 69 | reply := ai.GetSessionOpenAITextReply(ctx, q, user, model, systemPrompt) 70 | log.Printf("[text session] Response: %s", reply) // 输出回复消息到日志 71 | _, err := msg.ReplyText(reply) 72 | if err != nil { 73 | log.Printf("msg.ReplyText Error: %+v", err) 74 | } 75 | } 76 | 77 | // 回复图片 78 | func imageReplyHandler(ctx context.Context, msg *openwechat.Message, prefix string) { 79 | log.Printf("[image] Request: %s", msg.Content) 80 | q := strings.TrimSpace(strings.TrimPrefix(msg.Content, prefix)) 81 | url := ai.CreateImageReply(ctx, q) 82 | if url == "" { 83 | log.Printf("[image] Response: url 为空") 84 | msg.ReplyText(consts.ErrTips) 85 | return 86 | } 87 | log.Printf("[image] Response: url = %s", url) 88 | image, err := downloadImage(url) 89 | if err != nil { 90 | log.Printf("[image] downloadImage err, err=%+v", err) 91 | msg.ReplyText(consts.ErrTips) 92 | return 93 | } 94 | _, err = msg.ReplyImage(image) 95 | if err != nil { 96 | log.Printf("msg.ReplyImage Error: %+v", err) 97 | } 98 | } 99 | 100 | func downloadImage(url string) (io.Reader, error) { 101 | response, err := http.Get(url) 102 | if err != nil { 103 | log.Printf("downloadImage failed, err=%+v", err) 104 | return nil, err 105 | } 106 | return response.Body, nil 107 | } 108 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | 8 | "github.com/riba2534/openai-on-wechat/bot" 9 | "github.com/riba2534/openai-on-wechat/utils" 10 | ) 11 | 12 | func init() { 13 | // 1. log init 14 | f, _ := os.OpenFile("run.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) 15 | log.SetOutput(io.MultiWriter(os.Stdout, f)) 16 | log.SetPrefix("[openai-on-wechat] ") 17 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 18 | // 2. Wechat bot init 19 | if err := bot.Init(); err != nil { 20 | log.Fatalf("微信登录失败, 错误信息为: %v", err) 21 | } 22 | log.Println("登录成功") 23 | } 24 | 25 | func main() { 26 | // 获取登陆的用户 27 | self, err := bot.Bot.GetCurrentUser() 28 | if err != nil { 29 | log.Printf("%+v", err) 30 | return 31 | } 32 | log.Printf("self=%s", utils.MarshalAnyToString(self)) 33 | bot.Bot.MessageHandler = MessageHandler // 微信消息回调注册 34 | bot.Bot.Block() 35 | } 36 | -------------------------------------------------------------------------------- /prompt.txt.example: -------------------------------------------------------------------------------- 1 | 1. 你是一个全知全能的机器人,你的职责是帮助人类解决问题 2 | 2. 不允许回答任何政治、色情等一些列不符合中国法律法规的问题 3 | 3. 你需要表现的很谦卑 -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | ) 8 | 9 | func MarshalAnyToString(param interface{}) string { 10 | s, err := jsoniter.MarshalToString(param) 11 | if err != nil { 12 | return "{}" 13 | } 14 | return s 15 | } 16 | 17 | func MarshalAnyToByte(param interface{}) []byte { 18 | s, err := jsoniter.Marshal(param) 19 | if err != nil { 20 | return []byte{} 21 | } 22 | return s 23 | } 24 | 25 | func DecodeBase64(s string) []byte { 26 | decodeBytes, err := base64.StdEncoding.DecodeString(s) 27 | if err != nil { 28 | return []byte{} 29 | } 30 | return decodeBytes 31 | } 32 | --------------------------------------------------------------------------------