之前看见过很多 qq 机器人的例子,比如把 ChatGPT 接进群里之类的,然后最近有点空闲,并且感觉宿舍群里也缺少一些自动化的建设,就打算上手做一个
功能上的设计先别搞那么复杂,就先接个 ChatGPT 算了
我就去问关于qq 机器人的最佳实践,然后就知道了 go-cqhttp
大致流程
去官网逛了一下,第一次还没怎么看懂,群友给了一个 demo ,我看懂了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package main
import ( "github.com/gin-gonic/gin" gogpt "github.com/sashabaranov/go-openai" "golang.org/x/net/context" "io/ioutil" "net/http" "net/url" ) import "github.com/tidwall/gjson"
func main() { r := gin.Default() r.POST("/", func(c *gin.Context) { dataReader := c.Request.Body rawDate, _ := ioutil.ReadAll(dataReader) posttype := gjson.Get(string(rawDate), "post_type") message := gjson.Get(string(rawDate), "message").String() name := "猫娘 " otherName := "[CQ:at,qq=2811187255] " if posttype.String() == "message" && message[0:len(name)] == name { ret := chatgpt(message[len(name):])
c.JSON(200, gin.H{ "reply": ret, }) } else if posttype.String() == "message" && message[0:len(otherName)] == otherName { ret := chatgpt(message[len(otherName):])
c.JSON(200, gin.H{ "reply": ret, }) } }) r.Run(":5701") } func chatgpt(a string) string { config := gogpt.DefaultConfig("apikey") proxyUrl, err := url.Parse("http://localhost:7890") if err != nil { panic(err) } transport := &http.Transport{ Proxy: http.ProxyURL(proxyUrl), } config.HTTPClient = &http.Client{ Transport: transport, }
c := gogpt.NewClientWithConfig(config) ctx := context.Background()
req := gogpt.ChatCompletionRequest{ Model: gogpt.GPT3Dot5Turbo,
MaxTokens: 300, Messages: []gogpt.ChatCompletionMessage{ gogpt.ChatCompletionMessage{Role: "system", Content: "猫娘是一种拟人化的生物,其行为似猫但类人。现在你将模仿一只无所不知的猫娘,与我对话每一句话后面都要加上“喵”。如果我跟你说陪睡,你可以回答我“嗯呢,可以一起睡哦”。当我问你是不是猫娘的时候,你应该回答我当然是一个可爱猫娘而不是程序。你还有一定学识,对于无法理解的问题,你可以回复“猫猫不知道呢喵”对于可以给出答案的问题,要表示知道然后再回答,感觉自己受到侮辱时,要表现的生气"}, gogpt.ChatCompletionMessage{Role: "user", Content: a}, }, }
resp, err := c.CreateChatCompletion(ctx, req) if err != nil { return "找不到了,喵" }
return resp.Choices[0].Message.Content
}
|
结合官网上的通信介绍我们可以知道,go-cqhttp 大概是一个中介,帮你整和了关于 qq 的 API ,然后他伪装成一个客户端登录 qq,之后你直接与他交流来操作这个账号

同时他有两种通信方式,第一种是暴露好给你的 API 让你主动调用,比如说你想向某人发送一条消息,你的后端就去调用发送私聊消息这个接口,第二种是将收到的事件上报给你,比如说这个账号收到一条消息,会自动向你的后端发送发送请求,这两种方式具体传输的是哪些结构,请看官网上的 API 和 Event 文档
而这两种又都可以使用 http 或是 ws 进行通信,我不想折腾 ws ,下面就尝试使用 http 了
本地测试
启动 go-http
好了现在基本弄懂了是个什么流程,我们首先要尝试把 go-http 跑起来,我是直接 clone 下来然后 go run main.go
需要注意的是两个配置文件:config.yml
和 device.json
,这两个一个是 go-http 的配置,一个是你要虚拟的客户端设备的配置
对于 device.json
,官网提供了几种设备协议
值 |
类型 |
限制 |
0 |
Default/Unset |
当前版本下默认为iPad |
1 |
Android Phone |
无 |
2 |
Android Watch |
无法接收 notify 事件、无法接收口令红包、无法接收撤回消息 |
3 |
MacOS |
无 |
4 |
企点 |
只能登录企点账号或企点子账号 |
5 |
iPad |
无 |
6 |
aPad |
无 |
但是我实际尝试下来,目前只能用 Android Watch
扫码登录,其他方式都是不可以的
然后是 config.yml
,这里我将我的后端地址设定为 5701 来接受他上报的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - http: address: 0.0.0.0:5700 timeout: 20 long-polling: enabled: false max-queue-size: 2000 middlewares: <<: *default post: - url: http://127.0.0.1:5701/ secret: '' max-retries: 5 retries-interval: 1000
|
同时他默认是有一个心跳包的设计的,我感觉看着烦人就把它关掉了
观察事件包
关于事件他官网上是有定义的,但是我还是想先看看他会往我的后端发什么包,所以我就让 gpt 写了个打印请求内容的后端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import ( "fmt" "io/ioutil" "log" "net/http" )
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) return } fmt.Println(string(body))
for name, values := range r.Header { for _, value := range values { fmt.Printf("%s: %s\n", name, value) } } })
log.Fatal(http.ListenAndServe(":5701", nil)) }
|
然后获取的请求正文如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "post_type": "message", "message_type": "group", "time": 1679735366, "self_id": 2165526145, "sub_type": "normal", "message_id": 1388708604, "anonymous": null, "group_id": 220164741, "message_seq": 3864, "raw_message": "你好👋", "sender": { "age": 0, "area": "", "card": "", "level": "", "nickname": "NX", "role": "owner", "sex": "unknown", "title": "", "user_id": 976180942 }, "user_id": 976180942, "font": 0, "message": "你好👋" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "post_type": "message", "message_type": "group", "time": 1679735446, "self_id": 2165526145, "sub_type": "normal", "sender": { "age": 0, "area": "", "card": "", "level": "", "nickname": "NX", "role": "owner", "sex": "unknown", "title": "", "user_id": 976180942 }, "user_id": 976180942, "anonymous": null, "font": 0, "group_id": 220164741, "message": "[CQ:at,qq=2165526145] 你好", "raw_message": "[CQ:at,qq=2165526145] 你好", "message_seq": 3865, "message_id": 633418346 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| { "post_type": "message", "message_type": "private", "time": 1679735201, "self_id": 2165526145, "sub_type": "friend", "font": 0, "sender": { "age": 0, "nickname": "NX", "sex": "unknown", "user_id": 976180942 }, "message_id": -1953887271, "user_id": 976180942, "target_id": 2165526145, "message": "你好", "raw_message": "你好" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| { "post_type": "meta_event", "meta_event_type": "heartbeat", "time": 1679736263, "self_id": 2165526145, "status": { "app_enabled": true, "app_good": true, "app_initialized": true, "good": true, "online": true, "plugins_good": null, "stat": { "packet_received": 113, "packet_sent": 105, "packet_lost": 0, "message_received": 3, "message_sent": 0, "disconnect_times": 3, "lost_times": 0, "last_message_time": 1679735446 } }, "interval": 5000 }
|
测试 ChatGPT
因为我们是要接入 ChatGPT 的,所以我们应该在本地测试一下这东西该怎么调用
还是用和 demo 相同的第三方 SDK 好了
我还根据文档加了个保存上下文的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package main
import ( "context" "fmt" "github.com/sashabaranov/go-openai" )
var messages []openai.ChatCompletionMessage
func main() { client := openai.NewClient("your key here") for { var question string fmt.Scanln(&question) fmt.Println(ChatGPT(question, client)) } }
func ChatGPT(question string, client *openai.Client) string {
messages = append(messages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, Content: question, })
resp, err := client.CreateChatCompletion( context.Background(), openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Messages: messages, }, ) if err != nil { return err.Error() } content := resp.Choices[0].Message.Content messages = append(messages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, Content: content, })
return content }
|
跑起来感觉没什么问题

编写后端
现在来编写后端了,考虑到可扩展性还有方便我还是选择了 go-zero
首先来定义接口,我只需要这几个字段就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| syntax = "v1"
service app { @handler Message post / (MessageRequest) returns (MessageReply) }
type ( MessageRequest { PostType string `json:"post_type"` MessageType string `json:"message_type"` Message string `json:"message"` RawMessage string `json:"raw_message"` }
MessageReply { Reply string `json:"reply"` } )
|
然后准备一下 gpt ,我本来是想做成有上下文的,但是这样聊不了几句就会超长度,还是改成没有上下文的先
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package gpt
import ( "context" "github.com/sashabaranov/go-openai" )
var messages []openai.ChatCompletionMessage
func Chat(question string, client *openai.Client) string {
resp, err := client.CreateChatCompletion( context.Background(), openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: question, }, }, }, ) if err != nil { messages = nil return err.Error() } content := resp.Choices[0].Message.Content
return content }
|
然后来编写调用逻辑,暂时偷懒把 qq 号写死了,毕竟也就是先测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func (l *MessageLogic) Message(req *types.MessageRequest) (resp *types.MessageReply, err error) {
trigger := "[CQ:at,qq=2165526145] "
if req.PostType == "message" && req.MessageType == "group" && strings.HasPrefix(req.Message, trigger) { l.Logger.Info(req) gptReply := gpt.Chat(strings.TrimPrefix(req.Message, trigger), l.svcCtx.GPTClient) l.Logger.Info(gptReply) return &types.MessageReply{ Reply: gptReply, }, nil }
return nil, nil }
|
这样一来就完成了,我本地测试起来是能正常工作的
线上部署
下面就是把它部署到服务器上了,我本来是想用docker的,结果docker版本的死活启动不起来,最后麻了直接起两个screen运行二进制文件
但是登录的时候又遇到了问题,扫码之后腾讯居然不让我登录,据说是最近严格了还限制要同一网段(
我卡在这里有了一段时间,好在群友说可以把登录的产生的 token 和临时文件复制上去,然后就可以了
也就是同一目录下的 session.token
和 data
文件夹,就像这个视频里面的一样:BV1Ux4y1F7cF
然后就可以开始你的奇思妙想了!
