起因

  • 故事的起因是这样的,笨蛋楠楠不好好学习,每天到家就躺床上玩手机。需要我每天督促她。于是乎就想看一下QQ机器人相关技术,然后做个定时任务就好了,毕竟懒才是第一生产力,如果让我每天手动发的话,长此以往会累死的。

初步调研

go-cqhttp

Mrs4s commented on Oct 10, 2023
由于QQ官方针对协议库的围追堵截 持续👊🐔 , 不断更新加密方案, 我们已无力继续维护此项目.
在未来 sign-server 方案彻底被官方封死之后 go-cqhttp 将无法继续使用.
同时NTQQ的出现让我们可以使用官方 完美 实现的协议实现来继续开发Bot, 不再担心由于协议实现不完美而导致被识别.
我们建议所有QQBot项目开始做好迁移至无头NTQQ或类似基于官方客户端技术的准备以应对未来的彻底封锁,
如果你的 go-cqhttp 还能继续使用, 不建议立即迁移, 但请开始阅读相关文档并做好迁移准备

推荐项目:

如果你想在电脑/服务器上部署bot -> https://chronocat.vercel.app/blog/0050 如果你想在Android 手机/模拟器上部署bot -> https://github.com/linxinrao/Shamrock 以上项目均为调用官方协议实现

以上项目均被请喝茶了,只能说有缘再见了.

相关问题可以在这个issue下讨论

协议库的时代已经过去, 接下来是Hook官方客户端的时代了, 感谢大家三年来的支持

其实go-cqhttp项目最初只是想做一个能在路由器上跑的酷Q

——————————————————————
什么是无头NTQQ?

众所周知, QQ官方最新推出的 NTQQ 客户端使用了 electron 技术, 该技术可以非常方便的跨平台同时使用前端已有的技术栈进行客户端开发.
NTQQ 客户端项目分为前后端两个部分, 前端是使用 Web 技术开发的 UI 界面供用户交互,后端使用 nodejs addons 技术包装了一个库来处理客户端逻辑和与服务端通信 (wrapper.node).
这个库的作用和 go-cqhttp 非常相似, 所以我们完全可以将前端删除只与这个库交互, 并引出 API 来为我们的Bot服务.
从服务端视角来说我们的 Bot 和正常客户端一样, 因为都是通过 wrapper.node 与服务端通信. 并且由于是官方根据内部文档开发的模块, 我们可以说这是一个 完美 的 go-cqhttp.

优点: 无头模式下相对低的占用.
缺点: 可能会受未来QQ更新的影响.

Shamrock项目是什么原理?

Shamrock 项目使用 xposed 的 hook 技术来实现远程操作 AndroidQQ 客户端.
优点: 不容易受未来更新封堵的影响.
缺点: 需要运行一个完整 AndroidOS 环境.

如果你的服务器资源足够充足, 我个人建议观望并跟进 Shamrock 项目. xposed 是久经考验且生态完善的技术.

mirai

  • mirai 是一个在全平台下运行,提供 QQ Android 协议支持的高效率机器人库。
  • 乍一看好像还没凉,遂尝试了一番,经过磕磕绊绊之后,虽说可以收发消息,但是相当不稳定,而且还有封号的风险
  • 一看论坛,也是风中残烛了,遂放弃。

R.I.P

  • 新时代已经没有能承载他们的船了

LLOneBot

  • 由于在go-cqhttp中,作者提到了QQ最新退出的NTQQ客户端,而我很久之前就已经装过LiteLoaderQQNT这个插件了。那么肯定有基于这个新客户端开发的机器人技术吧,然后就找到了这个项目,真是相见恨晚啊,前面两个旧时代的残党我花了一个晚上捋清了来龙去脉,浪费了宝贵的8小时打游戏时间,下次我要更快更强。
  • 装好插件配置好后,我们仅仅需要修改这几个地方

收发消息

  • 这个是LLOneBot给出的收发消息的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import uvicorn
from fastapi import FastAPI, Request

app = FastAPI()


@app.post("/")
async def root(request: Request):
data = await request.json() # 获取事件数据
print(data)
return {}

if __name__ == "__main__":
uvicorn.run(app, port=8080)
  • 运行这个 Python 代码后,会在本地 8080 端口启动一个 HTTP 服务
  • 当有事件发生时,LLOneBot 会向 http://localhost:8080/ 发送 POST JSON 请求,具体事件数据可以查看 事件
1
2
3
4
5
6
7
8
9
10
11
import requests

requests.post('http://localhost:3000/send_private_msg', json={
'user_id': 123456,
'message': [{
'type': 'text',
'data': {
'text': 'Hello, World!'
}
}]
})
  • 其中 send_private_msg 是 OneBot V11 的 发送私聊消息 API,具体 API 可以查看 API 文档
  • user_id 是 QQ 号,message 是消息内容
  • 这里以文本消息格式为例,type 表示消息类型,type: text 表示文本消息,data 是消息内容,text 表示文本内容
  • 更多的消息内容的格式可以查看 消息类型
  • 有了最最基础的收发消息的能力了,剩下的就好说了,接收楠楠的消息,判断其意图,如果是已经下班到家,那么向延迟队列中插入一条提醒学习的任务,2小时后触发即可。对于意图判断,可以接入大模型来实现,同时大模型也会给出更温馨的回答(岂可修,那楠楠到底是会更喜欢AI一点还是更喜欢我一点)。

接入大模型

  • 既然已经可以收发消息了,那么剩下的就是接入大模型API,温馨提醒一下楠楠该学习啦。
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
import uvicorn
import logging
import requests
import json

import yaml
from fastapi import FastAPI, Request
from openai import OpenAI

from models import Msg

from logging_config import setup_logging

setup_logging()

logger = logging.getLogger(__name__)


def load_config():
with open(r"config.yaml", "r", encoding="utf-8") as file:
return yaml.safe_load(file)


def init_llm(api_key):
client = OpenAI(
api_key=api_key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
return client


app = FastAPI()

config = load_config()
llm = init_llm(api_key=config['openai']['api_key'])


@app.post("/")
async def root(request: Request):
data = await request.json()
try:
msg_obj = Msg.parse_obj(data)
qq = msg_obj.user_id
logger.info(f"QQ:{qq},Nickname -- {msg_obj.sender.nickname}{msg_obj.raw_message}")
if qq == config['girl_friend']['qq']:
logger.info("Received a message from girl friend")
answer = llm_answer(msg_obj.raw_message, llm, config['girl_friend']['system_prompt'])
send_message(qq, answer)

except Exception:
logger.error(f"data parse error:{data}")


def llm_answer(question, client, prompt):
completion = client.chat.completions.create(
model="qwen-max",
messages=[
{
'role': 'system',
'content': prompt
},
{'role': 'user', 'content': '楠楠对你说:' + question}
]
)
content = completion.choices[0].message.content
logging.info(f'LLM Response:{content}')
return content


def send_message(qq, message):
url = config['send_msg_url']
data = {
"user_id": qq,
"message": [
{
"type": "text",
"data": {
"text": message
}
}
]
}
response = requests.post(url, data=json.dumps(data))
logger.info(f"send message to {qq} result:{response.text}")


if __name__ == "__main__":
uvicorn.run(app, port=8080)
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
from typing import List, Dict

from pydantic import BaseModel


class Sender(BaseModel):
user_id: int
nickname: str
card: str


class MessageContent(BaseModel):
type: str
data: Dict[str, str]


class Msg(BaseModel):
self_id: int
user_id: int
time: int
message_id: int
real_id: int
message_seq: int
message_type: str
sender: Sender
raw_message: str
font: int
sub_type: str
message: List[MessageContent]
message_format: str
post_type: str

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

def setup_logging():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.FileHandler("app.log", encoding="utf-8"),
logging.StreamHandler(),
],
)

1
2
3
4
5
6
7
8
send_msg_url: http://localhost:3000/send_private_msg        # 发送消息的接口
openai:
api_key: YOUR_API_KEY # 我这里接入的是阿里系的通义千问,提供对应的API_KEY即可

girl_friend:
name: 楠楠 # 设置称呼,可以让AI以这个称呼她
qq: XXX # 设置QQ号
system_prompt: YOUR_SYSTEM_PROMPT # 为智能体初始化的prompt

初步效果

结语

  • 以上内容仅仅是一个简单的示例,通过LLOneBot和大模型API实现了一个定制化的QQ智能体,能够完成消息接收、处理和发送。当然,这只是一个起点,未来可以进一步扩展,例如加入更多的智能化处理、提供接口输入当日的学习计划,随后让智能体定时发送提醒、支持自定义prompt来提供情绪价值(国产大模型应该不会回复一些奇奇怪怪的内容吧?)。
  • 如果你也对智能体开发感兴趣,不妨参考一下这个示例,设计一个属于自己的智能助手吧。
  • 既然都看到这里了,不妨给这个项目点个Star吧,多谢!