Skip to content

架构概览

进程模型

Waylay 运行在 QQ 的 Electron 主进程中。通过补丁 package.jsonmain 字段,将入口从 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 的查询性能来自事件驱动的内存缓存:

缓存数据源事件用途
_groupListonGroupListUpdateget_group_listget_group_info
_groupMembersonMemberListChangeonMemberInfoChangeget_group_member_listget_group_member_info、@ 解析
_buddyListonBuddyListChangeget_friend_list
_uinToUid / _uidToUin所有消息和事件UIN ↔ UID 转换
_msgCacheonRecvMsgget_msgdelete_msg

缓存在登录成功后由内核事件自动填充,无需主动拉取。启动时还会触发一次 _preloadGroupMembers() 预加载全部群成员。

图片代理

NTQQ 3.2.27 的 CDN URL 不包含 rkey 鉴权参数,且无头模式下不会自动下载接收到的图片。Waylay 通过以下机制解决:

  1. 自动下载:收到含图片的消息时,调用 downloadRichMedia 请求 NTQQ 下载到本地(nt_data/Pic/nt_data/Emoji/emoji-recv/
  2. 本地代理:OneBot WS 端口同时提供 HTTP 文件服务,路径为 /file/{md5}.{ext}
  3. 等待机制:代理请求会等待最多 10 秒,让 NTQQ 完成下载
  4. 缓存查找:优先查内存缓存,未命中则遍历 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 回调,无需预先知道所有事件名称。

Released under the Apache 2.0 License.