11 月初,腾讯 QQ 正内测「QQ 频道」的消息逐渐传开了。
「QQ 频道」是一个全新的功能,QQ 称其为「娱乐协作新方式」,你可以通过它找到志同道合的伙伴,即便你们来自不同地方从事着不同职业,依旧能凭借着相同的喜好,在共同频道里聊天、直播、开黑、创作……当然,你能加入的不止一个频道。
这可不是个简单的功能,手机 QQ 甚至在底部 Tab 栏为「QQ 频道」开辟了一个独立入口,并放置在第二个按钮位,原
展、搞路由器进展2)
这就是本次项目的开端。
二、实现过程
路由器型号:newifi3(cpu=mtk7621a;512MB+32MB,没有写错顺序这一点后面会谈)
2.1 准备工作(网络已有详细的教程,这里不赘述)
2.1.1 go语言环境准备
1、到官网或者镜像下载go的编译器(不知道专业语言是不是叫这个)
https://golang.google.cn/dl/
2、下载windows版本,双击执行安装程序,添加环境变量
3、到vscode中安装相关依赖,测试打印hello world
4、修改go代理
2.1.2 路由器准备
步骤1-3目的:让路由器上网(我这边的环境只能用手机开热点),
1、进入路由器管理界面
2、点击搜索,找到手机热点,输入密码
3、ping百度测试路由器
4、opkg更新,安装nohup
2.2 修改go env,交叉编译go-cqhttp并测试
1、在终端中输入代码Go env -w GOOS=linux GOARCH=mipsle GOMIPS=softfloat, 修改go env
输入go env检查是否修改成功
修改成功
2、go-cqhttp源码的文件夹下,输入代码:go build -o cqhttp
等待一会儿在当前目录下会生成二进制文件cqhttp。注意这一步最好要能科学上网,修改host也行
编译成功,生成二进制文件
3、利用winscp把cqhttp转移到路由器中,打开putty连接路由器,cd到文件夹下,提权,测试运行(配置cqhttp的步骤和之前类似,这里不赘述)
登录成功,警告不用管
2.3 新建文件夹创建main.go文件实现监听路由功能
1、创捷解析json格式的结构体,本项目中会接受到两个json,分别来自百度的图像文字识别结果,以及cqhttp返回的群成员信息
type Namelistdata struct {
Data []Namelist //虽然原json是小写,但是构建结构体的value时首字母改成大写才行
//Message string
//Retcode string
//Status string
}
type Namelist struct {
// Age int
// Area string
Card string //群名片
// Card_changeable bool
// Group_id int
// Join_time int
// Last_sent_time int
// Level int
//Nickname string //qq昵称
// Role string
// Sex string
// Shut_up_timestamp int
// Title string
// Title_expire_time int
// Unfriendly bool
User_id uint32 //用string接会变成科学计数法
}
type Wordsresult struct {
Words_result []Words //虽然原json是小写,但是构建结构体的value时首字母改成大写才行
}
type Words struct {
Words string
}
2、导包,本次项目用到了5个本地包和一个来自gihub功能类似flask的gin包
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
3、创建函数解析事件上报中的图像url地址
// 传递json解码后的string消息,注意要把json字符转义
func cqmessageToUrl(body map[string]string) []string {
a := strings.Split(body["message"], "]") //分割出多条cq模板的信息
var url []string //声明空切片接住url
for _, item := range a {
b := strings.Split(item, ",")
for _, item2 := range b {
if strings.Contains(item2, "url=") { //以下为json字符转义
item2 = strings.Replace(item2, "&", "&", -1)
item2 = strings.Replace(item2, "[", "[", -1)
item2 = strings.Replace(item2, "]", "]", -1)
item2 = strings.Replace(item2, "^", ",", -1)
fmt.Printf("%vn", item2) //item2即为url值
url = append(url, item2)
}
}
}
fmt.Printf("获取msg提取url的结果: %vn", url)
return url
}
4、创建函数调用百度的图像文字识别api
在上次项目中是将图像url转为base64格式后传入百度api,经过研究发现百度api可以直接接受url,省去了转换的一步。
const API_KEY = "9Gwmw0R*******zauoTa" //保密信息,隐去
const SECRET_KEY = "roLP*******83" //保密信息,隐去
// 以下为百度提供的文字识别函数
func ocrMain(pictureUrl string) Wordsresult {
url := "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token=" + GetAccessToken()
pictureUrl = pictureUrl + "&detect_direction=false¶graph=false&probability=false"
payload := strings.NewReader(pictureUrl)
client := &http.Client{}
req, err := http.NewRequest("POST", url, payload)
if err != nil {
fmt.Println(err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
}
//newbody := map[string]any{} 用这个方法接json格式会得到[]interface {},又不能遍历,有点难处理
//_ = json.Unmarshal([]byte(body), &newbody)
//_ = json.Unmarshal(body, &newbody)
//fmt.Printf("%v 类型是: %T n", newbody, newbody) []byte(body)似乎影响不大,最后为依然是map[string]interface {}类型
var jsonObject Wordsresult
json.Unmarshal([]byte(body), &jsonObject)
fmt.Printf("百度识别结果:%v n", jsonObject)
return jsonObject
}
/**
* 使用 AK,SK 生成鉴权签名(Access Token)
* @return string 鉴权签名信息(Access Token)
*/
func GetAccessToken() string {
url := "https://aip.baidubce.com/oauth/2.0/token"
postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", API_KEY, SECRET_KEY)
resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(postData))
if err != nil {
fmt.Println(err)
return ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return ""
}
accessTokenObj := map[string]any{}
_ = json.Unmarshal([]byte(body), &accessTokenObj)
//fmt.Printf("%v 类型是: %T n", accessTokenObj["access_token"], accessTokenObj["access_token"]) 似乎两者没有区别,都是strin类型
//fmt.Printf("%v 类型是: %T n", accessTokenObj["access_token"].(string), accessTokenObj["access_token"].(string))
return accessTokenObj["access_token"].(string)
}
5、创建函数与cqhttp通信,拿到群成员信息
// 解析群成员
func all_nameList(groupid string) Namelistdata {
tocq := "http://127.0.0.1:5700/get_group_member_list?group_id=" + groupid
respons, err := http.Get(tocq)
if err != nil {
fmt.Println(err)
}
byteDate, _ := io.ReadAll(respons.Body)
newbody := map[string]any{} //打印原始结果
_ = json.Unmarshal(byteDate, &newbody)
fmt.Printf("%v n", newbody)
var jsonObject Namelistdata
json.Unmarshal([]byte(byteDate), &jsonObject)
fmt.Printf("获取群成员信息结果:%vn ", jsonObject)
// var jsonObject2 Namelistdata
// d := json.NewDecoder(bytes.NewReader([]byte(byteDate)))
// d.UseNumber()
// err = d.Decode(&jsonObject2)
// if err != nil {
// fmt.Println(err)
// }
// fmt.Printf("测试数字:%vn ", jsonObject2)
return jsonObject
}
6、创建函数将群成员信息与图像解析信息匹配,并且构建cq格式的at消息
func Urltoat(ocrresult Wordsresult, groupdata Namelistdata) string {
var unnameid []uint32
for i := 0; i < len(ocrresult.Words_result); i++ {
for j := 0; j < len(groupdata.Data); j++ {
if strings.Contains(groupdata.Data[j].Card, ocrresult.Words_result[i].Words) {
fmt.Println(groupdata.Data[j])
unnameid = append(unnameid, groupdata.Data[j].User_id)
}
}
}
fmt.Println(unnameid)
str := fmt.Sprintf("%d", unnameid[0])
result := "[CQ:at,qq=" + str + "]"
//result := "[CQ:at,qq=" + unnameid[0] + "]" //测试qq号保存为strin类型能否正常使用
for _, item := range unnameid[1:] {
//fmt.Println(strconv.Itoa(unnameid[i]))
//fmt.Println(strconv.Itoa(item))
str = fmt.Sprintf("%d", item)
result = result + "[CQ:at,qq=" + str + "]"
//result = result + "[CQ:at,qq=" + item + "]" //测试qq号保存为strin类型能否正常使用
} //构造cq消息,实现at效果
fmt.Println(result)
return result
}
8、创建函数与cqhttp通信,发送群消息
func sendGroupMassage(message string, group_id string) {
sendmessage := "http://127.0.0.1:5700/send_group_msg?group_id=" + group_id + "&message=" + message
respons, err := http.Get(sendmessage)
if err != nil {
fmt.Println(err)
}
byteDate, _ := io.ReadAll(respons.Body)
fmt.Printf("%vn %vn %T", string(byteDate), byteDate, byteDate)
return
}
9、主函数判断条件并调用
var groupid string = "*****" //填入群号
func main() {
engine := gin.Default()
engine.POST("/", func(c *gin.Context) {
data, _ := c.GetRawData()
//fmt.Printf("%v %Tn", string(data), data) 使用string(data)方法可以完全打印上报的事件,但是后续要解码匹配message之类的方法不熟,先搁置
var body map[string]string
json.Unmarshal(data, &body) //解码json消息,但似乎解码不全,数字类型的拿不到
fmt.Println(body) //打印事件上报的结果
messageUrl := cqmessageToUrl(body) //调用cqmessageToUrl函数,返回url的切片数组
if len(messageUrl) != 0 && strings.Contains(body["message"], "图片解析") {
groupdata := all_nameList(groupid) //调用all_nameList函数,返回群成员的信息
fmt.Sprintln(groupdata)
for i, _ := range messageUrl {
a := ocrMain(messageUrl[i])
sendGroupMassage(Urltoat(a, groupdata)+"请打卡", groupid)
}
}
})
engine.Run("127.0.0.1:5701")
}
2.4 交叉编译路由监控的代码,winscp导入路由器,使用nohup分别启动,同时运行
go mod init main
go mod tidy
go build -o spyRoute
编译成功
2.5 用top命令查找程序PID,用kill命令关闭程序
成功后台运行
三、效果演示
本项目重点是用go语言实现监控路由,以及路由器中同时运行两个程序,已充分体现在章节2“实现过程”中。
而且,本项目效果和上次一致,故不再演示。
四、代码总结——踩过的坑
第一次写go代码,难免会出现低级语法错误导致的报错,考虑到相关的坑都价值不大,相关错误就不再记录,把篇幅留给自认值得的东西。
4.1
在go语言中,json文件还要用结构体解析而且还需要Unmarshal()解码,我是没想到的。Python可以直接调用函数转成字典,然后是键值对索引。第一次接触不方便的模式确实很想吐槽。想着用interfac{}格式接收json,但是后续居然不能遍历,导致我只能老老实实去写结构体。
4.2
想着把解析结构体的代码另外放在一个package里,然后通过给main包导入的形式调用,但是在vscode中导包语句总是有红线报错,折腾好久找各种教程也无法导入。如果忽略红线,每次在vscode中按下保存键,写好的导包语句就消失。所以最终只好把全部的结构体以及函数都放在main包下,非常冗杂。
4.3
因为路由器是32位,存储范围是“-2147483648 ~2147483647”,而现在qq号又大多是十位数,很容易溢出。如果在结构体中写int类型,在windows测试可以拿到的qq号,放到路由器上只会显示0,也即拿不到结果。改成string类型接受又会转成科学计数法,最终选择了无符号的uint32类型,存储范围是“0 ~ 4294967295”
4.4
json里是小写的健,在创建结构体时要首字母大写,才能拿到解析结果。
4.5
使用gin监听路由的各种教程,都是在浏览器中打印展示效果,可是我并不需要这样,而是要在命令窗口打印。
4.6
上次项目运行过程中发现oppo手机qq发送的图片在cqhttp事件上报中产生的url地址无法解析,经过群友点拨,实际不是框架问题,而是需要json字符转义,相关内容已写入函数中。但Ios的依然有问题。
……
五、项目总结:
5.1 折腾历程:
5.1.1
手头这台路由器我拿到手时就经过了刷机,是padavan系统而且有很多插件。刷机改为openwrt的原因有两个:一来,当时还没有计划用U盘外挂,需要腾出存储空间。
二来决定性原因是,padavan的软件源已经失效,根本无法下载python相关的库,更改为大佬的镜像软件源也是各种问题,不得不刷机。
直接在网站“https://openwrt.ai/”找到固件,因为已经有breed在路由器里面,所以傻瓜操作即可。
5.1.2
电脑与路由器交互时一定注意看有没有插网线,有没有禁用网卡。有次通过管理地址一直连不上路由器,多次重启包括刷机那样的重启也不行,我还以为变砖了。最后发现是电脑禁用了网卡,打开就能连上路由器了。禁用网卡是因为在当时的网络拓扑下,电脑网络走路由器是不通外网的,只有连接WiFi才行,又因为不会在电脑上控制调试网络包走向,所以只好直接禁用网卡或者拔网线。还好是虚惊一场,长了教训。
5.1.3
最开始的想法是让路由器跑python监控路由的代码,本以为只需要把上次的结果换个位置就可以运行,甚至不需要写一份报告来总结。没想到python的管理器pip无法正常使用,重装了好几次也无法调用。想到基于go语言的cqhttp能够编译和运行在路由器上,干脆直接用go全部重写监听路由的代码。
5.2 接触go语言:
5.2.1
起初让AI转写python为go偷懒,但是由于完全不懂go,AI的结果语法是否错误,运行过程中报错了也不知道怎么修改。最终还是要从零学习一门新的语言,整个项目的难度陡然增加。
5.2.2
最开始不知道怎么编译cqhttp的源码,文档也没写,根据既往知识应该有个makefile文件入口,也没有。干脆直接go build试试,没想到有了二进制结果,在路由器上运行也成了。
5.2.3
监听路由的功能最好一边写一边在windows平台测试,确保没啥问题后再交叉编译到路由器上测试。
5.3 关于路由器:
5.3.1
没想到路由器可以一边连接WiFi,一边发射WiFi,手机就无法做到这一功能。
5.3.2
路由器的存储大小比例和往常接触的电子设备都不一样,比如手机通常是8+256,说明运存是8gb而内部存储是256gb,电脑也类似,总之硬盘空间都比运存要打。而我使用的newifi3却是32m内部存储,500m运行内存。幸好有个usb口可以外界u盘扩容,不然本项目直接夭折。
5.4 后续更新方向:
5.4.1
在发布了机器人开发视频博主的qq交流群中,发现更加高级的东西。原来官方已经有频道机器人了,而且有现成的文档,后面看看怎么折腾。
5.4.2
由于cqhttp以及监听路由代码运行过程中都会产生大量日志,而目前的部署没有采取任何存储管理措施,可能跑起来一两天或者半个月,整个路由器的存储空间包括U盘都会被吃掉。后续看看要怎么优化,相关操作应该牵涉Linux系统的一些知识。
5.5 项目收获与回味:
5.5.1
通过本次项目不仅学习了go语言,也顺便了解Linux系统上的一些操作,例如挂载u盘,nohup后台运行等
5.5.2
学习了如何修改host的GitHub源,使得编译成功。以及利用软件自动添加host,相关教程见:
5.5.3
因为有毕业论文的实验要做以及一些其他事情,这次项目完成周期相对比较长。正是由于战线太长,甚至有时空闲时还忘记了要推进。
以后再碰到长周期的项目,还是要注意及时记录进展,不管是推进了主线,还是掉进了坑里扑腾,都是项目背后的实现历程。
六、致谢
感谢网上的各种教学视频,还有学校图书馆提供的书籍,方便了学习和使用go语言。感谢“大鸟转转转酒吧”qq群优秀的讨论氛围,感谢群友关于url地址解析的点拨、关于修改host连接GitHub的点拨。