Windy 是一个正在重做中的 QQ Bot SDK 与启动端项目。当前重点支持 QQ 官方 Bot 适配器,并提供插件、命令、消息内容、Hook 事件管线等基础能力。
当前状态:QQ 官方 Bot 适配器可用;Milky 适配器已预留结构,协议接入尚未完成。
Windy/
Windy/ 启动端控制台程序
Windy.SDK/ SDK 主体
ExamplePlugin/ 示例插件
主要目录:
Windy.SDK/Adaptor/ 适配器抽象与具体适配器
Windy.SDK/Command/ 命令系统
Windy.SDK/Events/ 通用消息与事件参数
Windy.SDK/Hooks/ Hook 管线
Windy.SDK/Plugin/ 插件基类与加载器
Windy.SDK/Utils/ JsonTool 等工具
构建主体端:
dotnet build "Windy.slnx"构建示例插件:
dotnet build "ExamplePlugin\ExamplePlugin.csproj"复制插件到主体端输出目录:
Copy-Item -LiteralPath "ExamplePlugin\bin\Debug\net10.0\ExamplePlugin.dll" -Destination "Windy\bin\Debug\net10.0\Plugins\ExamplePlugin.dll" -Force运行主体端:
dotnet run --project "Windy\Windy.csproj"启动端会读取:
Windy\bin\Debug\net10.0\Config\Windy.json
如果不存在,会自动创建默认配置。
示例:
{
"OwnerOpenID": "",
"Debug": false,
"Adaptors": [
{
"Name": "qq-official",
"Enabled": true,
"Settings": {
"AppId": "",
"ClientSecret": "",
"Sandbox": "false"
}
},
{
"Name": "milky",
"Enabled": false,
"Settings": {
"Endpoint": "ws://127.0.0.1:3001",
"AccessToken": ""
}
}
],
"Plugins": {
"Directory": "Plugins"
}
}注意:不要把真实 ClientSecret 提交到仓库。
插件继承 WindyPlugin:
using Windy.SDK.Adaptor;
using Windy.SDK.Plugin;
public sealed class Plugin : WindyPlugin
{
public override string Name => "Example";
public override string Version => "1.0.0";
public override string Author => "Cjx";
public override string Description => "示例插件";
public override AdaptorType RequiredAdaptor => AdaptorType.QQOfficial;
public override void Initialize()
{
// 这里注册命令以外的 Hook 或读取插件配置。
}
}插件只能声明一个适配器类型:
public override AdaptorType RequiredAdaptor => AdaptorType.QQOfficial;插件初始化前 SDK 会注入:
Adaptor // 当前插件绑定的适配器实例
Hooks // Hook 注册器命令使用 Attribute 注册:
using Windy.SDK.Command;
using Windy.SDK.Events;
[Command("ID", "查看当前消息信息", MessageScene.Group)]
[Command("ID", "查看当前消息信息", MessageScene.GroupAt)]
public async Task TestUID(CommandArgs args)
{
await args.Adaptor.SendMessage($"{args.Message.GroupId}\n{args.Message.AuthorId}\n{args.Message.AuthorName}");
}CommandArgs 常用字段:
args.CommandName
args.Parameters
args.Adaptor
args.Message.Content
args.Message.AuthorId
args.Message.AuthorName
args.Message.GroupId
args.Message.MessageId
args.Message.EventId
args.Message.Attachments
args.Message.RawQQ 官方适配器会在命令执行前清理机器人自己的 AT 标签。例如:
<@机器人ID> 测试
进入命令系统后会变成:
测试
只会移除 mentions[].is_you == true 的机器人自身 AT,不会删除 AT 其他人的内容。
被动回复当前消息:
await args.Adaptor.SendMessage("hello");主动发送群消息:
await Adaptor.SendMessage(groupOpenId, "hello");混合消息内容:
MessageContent content = new MessageContent()
.AddText("文本")
.AddImage(imageBytes)
.AddMarkdown("# 标题")
.AddButton(keyboard);
await args.Adaptor.SendMessage(content);Markdown + 按钮:
ButtonKeyboard keyboard = new()
{
Rows =
[
new ButtonRow
{
Buttons =
[
new MessageButton { RenderLabel = "菜单", ActionData = "/菜单" },
new MessageButton { RenderLabel = "返回", ActionData = "/返回" },
],
},
],
};
MessageContent content = new MessageContent()
.AddMarkdown("# **黑体大标题**\n\n- 功能1\n- 功能2\n- 功能3")
.AddButton(keyboard);
await args.Adaptor.SendMessage(content);命名空间:
using Windy.SDK.Adaptor.QQOfficial;用法:
QQOfficialLabel.At("user_openid")
QQOfficialLabel.AtEveryone()
QQOfficialLabel.CommandEnter("/菜单")
QQOfficialLabel.CommandInput("/查询", "点我查询", reference: true)
QQOfficialLabel.Channel("channel_id")
QQOfficialLabel.Emoji("4")兼容旧拼写:
QQOfficicalLabel.At("user_openid")Hook 支持:
优先级 priority
Handled 截断后续 Hook
消息 Hook 拦截命令执行
QQ 官方事件类型化 Args
消息 Hook:
RegisterMessageHook(OnMessageAsync, priority: 0);
private static Task OnMessageAsync(MessageEventArgs args)
{
if (args.Content == "hook拦截")
{
args.Handled = true;
return args.Adaptor.SendMessage("这条消息不会继续进入命令系统.");
}
return Task.CompletedTask;
}QQ 官方事件 Hook:
using Windy.SDK.Adaptor.QQOfficial;
this.RegisterGroupAddRobotHook(OnGroupAddRobotAsync, priority: 100);
private static Task OnGroupAddRobotAsync(QQOfficialGroupOperationEventArgs args)
{
Message.Yellow($"group={args.GroupOpenId}, operator={args.OperatorMemberOpenId}, timestamp={args.Timestamp}");
args.Handled = true;
return Task.CompletedTask;
}群管理事件常用 Args:
args.GroupOpenId
args.OperatorMemberOpenId
args.Timestamp
args.Handled群成员事件常用 Args:
args.GroupOpenId
args.MemberOpenId
args.OperatorMemberOpenId
args.Timestamp
args.HandledQQOfficialEventType 包含:
Ready
Resumed
GroupAtMessageCreate
GroupMessageCreate
C2CMessageCreate
GroupAddRobot
GroupDelRobot
GroupMemberAdd
GroupMemberRemove
GroupMsgReceive
GroupMsgReject
FriendAdd
FriendDel
C2CMsgReceive
C2CMsgReject
SubscribeMessageStatus
InteractionCreate
MessageAuditPass
MessageAuditReject
AtMessageCreate
MessageCreate
DirectMessageCreate
Unknown也可以用通用枚举 Hook:
this.RegisterQQOfficialEventHook(QQOfficialEventType.AtMessageCreate, OnGenericOfficialEventAsync);当前支持:
WebSocket Gateway 连接
AccessToken 获取
心跳
op=7 Reconnect 重连
op=6 Resume 恢复会话
文本消息
图片/富媒体消息
Markdown 消息
按钮消息
Markdown + 按钮同发
文本交互标签
群消息与 AT 消息解析
发送人 OpenID 与用户名解析
机器人 AT 标签清理
类型化事件 Hook
MilkyAdaptor 已实现基础接入,参考了本地 Sora 的 Milky 适配器结构:HTTP API 调用 /api/{action},WebSocket 事件连接 /event。
当前支持:
WebSocket 事件接收
断线自动重连
send_group_message
send_private_message
upload_group_file
upload_private_file
文本消息
图片消息(byte[] 会转 base64://)
语音消息(record)
视频消息
文件上传
提及用户
提及全体
消息接收解析
附件提取
Milky 不支持 QQ 官方 Markdown 和按钮消息。SDK 会把 Markdown 降级为普通文本,按钮消息会跳过并输出日志。
Milky 配置示例:
{
"Name": "milky",
"Enabled": true,
"Settings": {
"Endpoint": "ws://127.0.0.1:3001",
"AccessToken": "",
"ReconnectInterval": "5000",
"DropSelfMessage": "true"
}
}也可以使用拆分配置:
{
"Name": "milky",
"Enabled": true,
"Settings": {
"Host": "127.0.0.1",
"Port": "3000",
"Prefix": "milky",
"UseTls": "false",
"AccessToken": ""
}
}QQ 官方 Bot 的部分事件是否推送取决于开放平台权限、事件订阅、机器人类型和实际配置。SDK 会尽量把收到的事件抛到 Hook 管线,未收到的事件需要先检查开放平台侧配置。