背景
最近chatGPT实在太火了,不谈下都不好意思说自己在技术圈混了,刚好前段时间公司里面在举办一个企业微信机器人比赛,然后就用注册了openai的一个账号,用python写了一个玩玩,但是想想不过瘾只能公司内部体验,于是乎又花了2个小时写了一个基于微信的聊天机器人,这里和大家交流下。
现实步骤
- 首先要能绕开openai 国内的访问限制,这一步骤大家自己想办法;
- 需要注册openai账号,这一步骤大家也自己想办法;
- 实现模拟登录微信账号,找到一个不错的开源代码,在我的github里面有;
- 在接收到微信信息时调用openai的api,这里我使用了go sdk 开源地址也在在我的github里。
相关功能
我的简单demo代码开源拉了github地址点这里
功能介绍:
- 支持回复微信文本消息
- 支持文本回复图片消息
- 支持群消息at回复
- 支持上下文记忆
- 支持基于网页链接地址读取总结文件
演示图片
群里时at它
单聊时直接发送消息,支持根据语义来返回图片
实现原理
- 调用微信桌面客户端协议登录微信号
- 监听微信文本消息进行回复
- 在回复消息之前对消息进行语义识别,这里单独写了一个prompt来识别文本的语义是来生成图片还是进行文本回复就行,如果是生成图片的话,就直接回复图片。
源码
package mainimport ("bytes""context""encoding/base64""fmt""image/png""os""strings""time""github.com/eatmoreapple/openwechat"openai "github.com/sashabaranov/go-openai"
)// OpenaiWechat
type OpenaiWechat struct {ChatBot *openwechat.Bot // chat botOpenai *openai.Client // openai clientuserDialogue map[string][]openai.ChatCompletionMessage // save user gialogue context
}var singleton *OpenaiWechat// firstUserDialogue
var firstUserDialogue = []openai.ChatCompletionMessage{{Role: openai.ChatMessageRoleSystem,Content: "你是一个非常有帮助的助手",},{Role: openai.ChatMessageRoleUser,Content: "请问我怎么规划我的职业发展呢?",},{Role: openai.ChatMessageRoleAssistant,Content: `你好,我很高兴能帮助你。职业发展是一个非常重要的话题,你可以从以下几个方面考虑:你的兴趣爱好和技能:你可以考虑你的兴趣爱好和技能,看看哪些职业与你的兴趣爱好和技能相匹配。这样可以让你在工作中感到更有成就感和满足感。行业前景:你可以考虑一些行业的前景,看看哪些行业在未来几年内会有更好的发展前景。这样可以让你在职业发展中更有保障。学历和培训:你可以考虑你的学历和培训,看看哪些职业需要更高的学历和培训。这样可以让你更好地规划你的职业发展。希望这些建议能帮到你。你还有其他问题吗?`,},
}func init() {//bot := openwechat.DefaultBot()bot := openwechat.DefaultBot(openwechat.Desktop) // deskop mode,you can switch deskop mode if defualt can not login// Register message handler functionbot.MessageHandler = func(msg *openwechat.Message) {replyChatMsg(msg)}// Register login QR code callbackbot.UUIDCallback = openwechat.PrintlnQrcodeUrl// loginif err := bot.Login(); err != nil {fmt.Println(err)return}client := openai.NewClient(os.Getenv("OPENAI_KEY")) // your openai key//client := openai.NewClient("")singleton = &OpenaiWechat{Openai: client,ChatBot: bot,}singleton.userDialogue = make(map[string][]openai.ChatCompletionMessage)// lock goroutine, until an exception occurs or the user actively exitsbot.Block()
}
func main() {
}// replyChatMsg
func replyChatMsg(msg *openwechat.Message) error {if msg.IsSendByGroup() { // only accept group messages and send them to yourselfif !msg.IsAt() {return nil}} else if !msg.IsSendByFriend() { // non-group messages only accept messages from your own friendsreturn nil}if msg.IsSendBySelf() { // self sent to self no replyreturn nil}// only handle text messagesif msg.IsText() {msg.Content = strings.Replace(msg.Content, "@GPT3.5 ", "", 1)fmt.Println(msg.Content)// simple match processingif isImage, _ := isImageContent(msg.Content); isImage {return replyImage(msg)}return replyText(msg)}return nil}// 图片生成校验
var imageMessage = []openai.ChatCompletionMessage{{Role: openai.ChatMessageRoleSystem,Content: "你现在是一个语义识别助手,用户输入一个文本,你根据文本的内容来判断用户是不是想生成图片,是的话你就回复是,不是的话你就回复否,记住只能回复:是 或者 否",},{Role: openai.ChatMessageRoleUser,Content: "我想生成一张小花猫的图片",},{Role: openai.ChatMessageRoleAssistant,Content: "是",},{Role: openai.ChatMessageRoleUser,Content: "请问冬天下雨我该穿什么衣服",},{Role: openai.ChatMessageRoleAssistant,Content: "否",},
}// isImageConntent
func isImageContent(content string) (bool, error) {temp := append(imageMessage, openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser,Content: content,})result, err := callOpenaiChat(temp)if err != nil {fmt.Printf("isImageConntent callOpenaiChat error: %v\n", err)return false, err}fmt.Printf("isImageContent result: %s", result)if strings.TrimSpace(result) == "是" {return true, nil}return false, nil}// replyImage
func replyImage(msg *openwechat.Message) error {path, err := generateImage(msg)if err != nil {fmt.Printf("replyImage generateImage error: %v\n", err)return err}img, err := os.Open(path)if err != nil {fmt.Printf("replyImage Open error: %v\n", err)return err}defer img.Close()msg.ReplyImage(img)return nil
}// replyText
func replyText(msg *openwechat.Message) error {messages, err := genMessage(msg)if err != nil {return err}result, err := callOpenaiChat(messages)if err != nil {fmt.Println(err)return err}msg.ReplyText(result)addResultToMessage(result, msg)return nil
}// dialogue context max length
const maxLength = 33// temporary folder to save pictures
const filePath = "./images/"// genMessage
func genMessage(msg *openwechat.Message) ([]openai.ChatCompletionMessage, error) {// TODOif _, ok := singleton.userDialogue[msg.FromUserName]; !ok {singleton.userDialogue[msg.FromUserName] = firstUserDialogue} else if len(singleton.userDialogue[msg.FromUserName]) >= maxLength {singleton.userDialogue[msg.FromUserName] = singleton.userDialogue[msg.FromUserName][2:]}element := openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser,Content: msg.Content,}singleton.userDialogue[msg.FromUserName] = append(singleton.userDialogue[msg.FromUserName], element)return singleton.userDialogue[msg.FromUserName], nil
}// addResultToMessage
func addResultToMessage(result string, msg *openwechat.Message) error {element := openai.ChatCompletionMessage{Role: openai.ChatMessageRoleAssistant,Content: result,}singleton.userDialogue[msg.FromUserName] = append(singleton.userDialogue[msg.FromUserName], element)return nil
}// generateImage
func generateImage(msg *openwechat.Message) (string, error) {ctx := context.Background()// Sample image by linkreqUrl := openai.ImageRequest{Prompt: msg.Content,Size: openai.CreateImageSize512x512,ResponseFormat: openai.CreateImageResponseFormatB64JSON,N: 1,}respBase64, err := singleton.Openai.CreateImage(ctx, reqUrl)if err != nil {fmt.Printf("Image creation error: %v\n", err)return "", err}imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON)if err != nil {fmt.Printf("Base64 decode error: %v\n", err)return "", err}r := bytes.NewReader(imgBytes)imgData, err := png.Decode(r)if err != nil {fmt.Printf("PNG decode error: %v\n", err)return "", err}filePath := fmt.Sprintf("%s%d.png", filePath, time.Now().UnixMicro())file, err := os.Create(filePath)if err != nil {fmt.Printf("File creation error: %v\n", err)return "", err}defer file.Close()if err := png.Encode(file, imgData); err != nil {fmt.Printf("PNG encode error: %v\n", err)return "", err}return filePath, nil
}// callOpenaiChat
func callOpenaiChat(messages []openai.ChatCompletionMessage) (string, error) {resp, err := singleton.Openai.CreateChatCompletion(context.Background(),openai.ChatCompletionRequest{Model: openai.GPT3Dot5Turbo,Messages: messages,MaxTokens: 512,},)if err != nil {fmt.Printf("ChatCompletion error: %v\n", err)return "", err}fmt.Println(resp.Choices[0].Message.Content)return resp.Choices[0].Message.Content, nil
}
注意
- 微信号最好不要用自己常用的微信号,因为模拟登录微信属于作弊行为很有可能会被封号;
- 注册的新的微信号可能无法马上模拟登录成功微信,需要实名认证之后,过一段时间才能成功登录。
- 注册的openai账号里面默认有18刀,调用不同的模型话费的tokens不一样,测试的时候注意测试自己的消耗速度,图片比文本更耗钱。