胡言乱语
最近在学muduo, 但是由于c++水平太弱, 导致我脑子快给想秃了。于是今天早上心血来潮想做点好的东西消遣下, 给qq机器人接入个ChatGPT。我想实现的最终效果是这样的(这是别的群的机器人, 即灵感来源):


它应该支持:
- 冷却时间(CD)
- 回复内容是markdown转换的图片, 且回复包含消息询问者的问题和询问者的昵称。且告知它CD时间。
实现方案制定
1.首先需要一个qq机器人, 恰好有个开源的golang实现go-cqhttp, 它的接口很友好, 用起来比较舒适。接口文档点击此处进入。
2.除了机器人, 我们还需要一个http处理程序, go-cqhttp收到消息时回调我们的http处理程序, 接收消息并做出响应。这里我们选择了gin作为开发框架。
3.需要把chatgpt的markdown内容转化为图片, 如果我们询问gpt能否写一个golang的hello world程序, gpt的回答通常是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 当然可以!以下是一份简单的 Go 代码,可以输出 "Hello, World!":
```go package main
import "fmt"
func main() { fmt.Println("Hello, World!") } ```
你可以将这段代码保存为 `hello.go` 文件,然后在终端中使用以下命令来编译并运行它:
``` go run hello.go ```
程序将输出 "Hello, World!" 到终端中。
|
也就是说, gpt的内容包含了markdown格式的内容, 最好把它渲染成图片。要实现这个, 我想到了以下思路:
a.用公开的api接口, 比如vertopal, 但是接口可能不稳定, 而且做起来比较麻烦, 我们不用这种方法
b.先用blackfriday先转成html格式, 然后把内容拼进一个比较好的html模板, 调用无头浏览器截图。这种方法很好, 能完美解决我们的问题, 且可定制度高。我们使用这种方法。
c.用别人造好的轮子来截图网页, gowitness是个不错的选择。这个库知名度挺高, 目前有2k star, 而且支持很屌。既能作为cli工具, 又能开http服务, 功能特别强大。但是很可惜, 这个工具只能截图http/https的网页, 不能截图本地html文件的图片。因此这个方法不行。
如何把markdown转换成html标签?
下面介绍了blackfriday(为什么名字是黑色星期五?)的基本玩法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main
import ( "fmt"
"github.com/russross/blackfriday/v2" )
func main() { fmt.Print(string(blackfriday.Run([]byte("### Hello World")))) }
|
如何根据html生成图片文件?
要调用无头浏览器, 用chromedp, 对比其它库, 它对chrome driver的支持好。下面是实现html转文件的代码:
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 ( "context" "encoding/json" "fmt" "os"
"github.com/chromedp/chromedp" )
func main() {
ctx, cancel := chromedp.NewContext(context.Background()) defer cancel()
result := []byte{}
chromedp.Run(ctx, fullScreenshot("file:///home/markity/Documents/Code/QQBot/program/example.html", 90, &result)) f, _ := os.Create("result.png") f.Write(result) }
func fullScreenshot(urlstr string, quality int, res *[]byte) chromedp.Tasks { return chromedp.Tasks{ chromedp.Navigate(urlstr), chromedp.FullScreenshot(res, quality), } }
|
这样就完全截图一个网页。我们要做的就是先拼接好网页, 然后调用无头浏览器截图就行了。
Bot Hello World: 如何实现一个QQ群echo服务?
先来做个简单的hello world级别的echo服务。我们需要监听群员的消息, 如果有人at机器人并说一句话, 机器人就at此群员并回显他的话。要做到这样的echo服务, 我们需要编写go-cqhttp的http回调程序, 其中要做到以下几点:
0.注意首先保证你已经配置好了go-cqhttp, 填好账号密码。填写好回调地址(即我们写的gin回调程序的url): 将servers
-http
-post
修改如下, 我们的回调程序将运行在http://127.0.0.1:8080
。
1 2 3 4
| post: # 反向HTTP POST地址列表 - url: 'http://127.0.0.1:8080' # 地址 # secret: '' # 密钥 max-retries: 3 # 最大重试,0 时禁用
|
1.在go-cqhttp请求(post方法)我们的回调程序时, 先读出消息本体, 即解析一下json数据就行
2.甄别回调的消息包类型, go-cqhttp有心跳包, 消息包等多种包, 我们需要的只有消息包。再甄别消息是否来自某个特定的群聊, 这里直接判断群聊号码就行了。再甄别用户是否at了机器人, 这里判断raw_message是否以[CQ:at,qq=机器人QQ号]
开头就行了。这种[CQ:xxx,xx=xxx]
叫CQ代码, 是qq的特殊消息类型, 包含at, qq表情, 图片, 表情包, 它会被qq客户端转义, 显示出特殊的内容。
消息包也有私聊和群聊的区别, 这是一个群聊的json数据包, 此处过多展开太无聊了, 提一下得了, 要了解更多直接去看go-cqhttp的文档:
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
| { "post_type":"message", "message_type":"group", "time":1679534793, "self_id":3402002560, "sub_type":"normal", "message_seq":894528, "group_id":805574759, "message":"那你得帅,还得有身材", "raw_message":"那你得帅,还得有身材", "sender": { "age":0, "area":"", "card":"萌小瓜", "level":"", "nickname":"瓜瓜回去吧", "role":"member", "sex":"unknown", "title":"", "user_id":10174609 }, "user_id":10174609, "message_id":1775522907, "anonymous":null, "font":0 }
|
3.拿取用户的输入, 然后调用cq-http的相应http接口做出回应。
下面是实现代码, 机器人的账号是3402002560, 群号是334903763, 程序里写死了:
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "strings"
"github.com/gin-gonic/gin" )
type EventHeaderStruct struct { Time int64 `json:"time"` PostType string `json:"post_type"` SelfID int64 `json:"self_id"` }
type sender struct {
UserID int64 `json:"user_id"` NickName string `json:"nickname"` Sex string `json:"sex"` Age int32 `json:"age"`
GroupID int64 `json:"group_id"`
Card string `json:"card"` Area string `json:"area"` Level string `json:"level"` Role string `json:"role"` Title string `json:"title"` }
type EventMessageStrcut struct { EventHeaderStruct MessageType string `json:"message_type"` SubType string `json:"sub_type"` MessageID int32 `json:"message_id"` RawMessage string `json:"raw_message"` Sender sender `json:"sender"` GroupID int64 `json:"group_id"` }
type SendMsgStruct struct { MessageType string `json:"message_type"` UserID int64 `json:"user_id"` GroupID int64 `json:"group_id"` Message string `json:"message"` AutoEscape bool `json:"auto_escape"` }
func GetSendToUserBytes(msg string, groupID int64, senderUserID int64) []byte { sendMsgStruct := SendMsgStruct{ MessageType: "group", GroupID: groupID, Message: fmt.Sprintf("[CQ:at,qq=%v]", senderUserID) + msg, AutoEscape: false, } b, _ := json.Marshal(&sendMsgStruct) return b }
func main() { r := gin.Default() r.POST("/", func(c *gin.Context) { buf, _ := io.ReadAll(c.Request.Body) var header EventHeaderStruct json.Unmarshal(buf, &header) if header.PostType != "message" { c.Status(200) return }
var message EventMessageStrcut json.Unmarshal(buf, &message)
if message.MessageType != "group" || (message.GroupID != 334903763) { c.Status(200) return }
if !strings.HasPrefix(message.RawMessage, "[CQ:at,qq=3402002560]") { return }
userSendToRobotMsg := message.RawMessage[21:] resp, err := http.Post("http://127.0.0.1:5700/send_msg", "application/json", bytes.NewBuffer(GetSendToUserBytes(userSendToRobotMsg, message.GroupID, message.Sender.UserID))) if err != nil { log.Printf("failed to Post to cqhttp service: %v\n", err) return } resp.Body.Close() })
r.Run() }
|
接下来运行go-cqhttp和gin服务, 机器人过一会就能工作了, 下面的命令可以快速启动go-cqhttp。

接下来?
完整实现的代码我已经开源到github上了, 点击此处查看。
有了上面的工具, 实现一个QQ GPT服务就唾手可得了, 但由于篇幅原因, 这里只讲到了最基础的东西。没有涉及GPT接口的调用, 也没有涉及冷却时间的实现。关于GPT的接口调用, 可以自行查询接口文档, 也可以结合我的代码看看。关于CD冷却时间, 直接做张表查询就行了, 这个很简单。对于查询调用GPT接口, 回复用户信息的任务, 最好开一个单独的协程执行消息任务, 这样不会发生竞态, 也不用锁, 也不会因为GPT请求太快而发生风控。此外, 单独的任务协程可以避免go-cqhttp的timeout导致的消息重放(这样可能导致用户提问一次而收到多次回复信息)。
最后来展示下最终实现效果:

