架构概览
进程模型
Waylay 运行在 QQ 的 Electron 主进程中。通过补丁 package.json 的 main 字段,将入口从 QQ 的加密应用替换为 Waylay 的启动脚本。
QQ Electron 进程
├── wrapper.node (NTQQ 原生模块,C++ 编译)
├── Waylay Bridge (src/bridge.js)
│ ├── NodeIQQNTWrapperEngine — QQ 内核引擎
│ ├── NodeIKernelLoginService — 登录服务
│ ├── NodeIQQNTWrapperSession — 会话管理(48 个子服务)
│ └── Kernel Listeners — 事件监听器
├── OneBot v11 Adapter (src/onebot/)
│ ├── Forward WebSocket Server (:3001)
│ └── Reverse WebSocket Clients
├── Milky Adapter (src/milky/)
│ ├── HTTP API Server (POST /api/:action)
│ ├── SSE / WebSocket 事件推送
│ └── Webhook 推送
└── Bridge Server (src/server.js)
└── Bridge WebSocket (:13000)源码结构
src/
├── index.js # 入口:解析环境变量,启动 bridge + server + onebot + milky
├── electron-entry.js # Electron 入口补丁
├── bridge.js # 核心:加载 wrapper.node,初始化引擎/登录/会话
├── listener.js # 创建内核事件监听器(Proxy 模式)
├── server.js # Bridge WebSocket/HTTP 服务器
├── onebot/
│ ├── adapter.js # OneBot v11 适配器(正向/反向 WS)
│ ├── actions.js # OneBot v11 Action 处理器
│ ├── events.js # 内核事件 → OneBot v11 事件翻译器 + 缓存
│ └── message.js # NTQQ 元素 ↔ OneBot v11 消息段转换(异步媒体处理)
└── milky/
├── adapter.js # Milky 协议适配器(HTTP/WS/SSE/Webhook)
├── actions.js # Milky Action 处理器
├── events.js # 内核事件 → Milky 事件翻译器
└── message.js # NTQQ 元素 ↔ Milky 消息段转换数据流
接收消息
QQ 服务器
→ wrapper.node (C++ 层)
→ nodeIKernelMsgListener.onRecvMsg (事件回调)
→ EventTranslator._onRecvMsg (翻译为 OneBot 事件)
├→ OneBotAdapter._broadcast (推送给所有连接的框架)
└→ _triggerImageDownloads (请求 NTQQ 下载图片到本地)
→ downloadRichMedia → onRichMediaDownloadComplete发送消息
Bot 框架 发送 { action: "send_group_msg", ... }
→ OneBotAdapter._handleAction
→ handlers.send_group_msg
→ await oneBotToNt (消息段 → NTQQ 元素,异步下载媒体)
→ registerMedia (媒体文件注册到 NTQQ 路径)
→ session.getMsgService().sendMsg (调用原生 API)
→ wrapper.node → QQ 服务器媒体下载(curl)、音视频探测(ffprobe)、缩略图生成(ffmpeg)均为异步执行,不阻塞事件循环。
查询请求(亚毫秒)
Bot 框架 发送 { action: "get_group_list" }
→ handlers.get_group_list
→ eventTranslator.getGroupList() (直接从内存 Map 读取)
← 立即返回缓存数据缓存架构
Waylay 的查询性能来自事件驱动的内存缓存:
| 缓存 | 数据源事件 | 用途 |
|---|---|---|
_groupList | onGroupListUpdate | get_group_list、get_group_info |
_groupMembers | onMemberListChange、onMemberInfoChange | get_group_member_list、get_group_member_info、@ 解析 |
_buddyList | onBuddyListChange | get_friend_list |
_uinToUid / _uidToUin | 所有消息和事件 | UIN ↔ UID 转换 |
_msgCache | onRecvMsg | get_msg、delete_msg |
缓存在登录成功后由内核事件自动填充,无需主动拉取。启动时还会触发一次 _preloadGroupMembers() 预加载全部群成员。
图片代理
NTQQ 3.2.27 的 CDN URL 不包含 rkey 鉴权参数,且无头模式下不会自动下载接收到的图片。Waylay 通过以下机制解决:
- 自动下载:收到含图片的消息时,调用
downloadRichMedia请求 NTQQ 下载到本地(nt_data/Pic/或nt_data/Emoji/emoji-recv/) - 本地代理:OneBot WS 端口同时提供 HTTP 文件服务,路径为
/file/{md5}.{ext} - 等待机制:代理请求会等待最多 10 秒,让 NTQQ 完成下载
- 缓存查找:优先查内存缓存,未命中则遍历 NTQQ 的 Pic 和 Emoji 目录
Bot 框架请求图片
→ GET /file/{md5}.{ext} (OneBot WS 端口)
→ 查内存缓存 → 未命中则搜索 nt_data 目录
→ 文件存在:返回图片
→ 文件不存在:等待 NTQQ 下载完成(最多 10s)Listener 机制
NTQQ 的 wrapper.node 使用回调模式。所有数据通过注册的 Listener 对象异步推送:
javascript
// bridge.js — 注册监听器
const listener = createListener(bridge._onEvent.bind(bridge));
session.init(config, listener);javascript
// listener.js — Proxy 模式捕获所有回调
function createListener(handler) {
return new Proxy({}, {
get(target, listenerName) {
return new Proxy({}, {
get(target2, eventName) {
return (...args) => handler({
listenerName, eventName, data: args[0]
});
}
});
}
});
}这种 Proxy 模式可以捕获 wrapper.node 调用的任何 Listener 回调,无需预先知道所有事件名称。