2024-08-01 07:00信息 • 发布者: 朝正不朝歪

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的点拨。