建立日期:2026-11-13 状态:阶段 0 完成,偏移签名方案已实现
| 问题 | 决策 |
|---|---|
| 合并方式 | 重写式融合,不是 merge 仓库 |
| 基座 | SnowLuma |
| NapCat 的定位 | 能力来源 + 偏移逆向能力独立保留 |
| 第一个交付物 | packages/native-offsets(签名扫描替代硬编码偏移表) |
两边的 I/O 底座是平行的两套:
NapCat: QQ 主进程 → NodeIKernel API → napcat-core → napcat-onebot → action
SnowLuma: QQ 进程 → 注入 DLL → 命名管道 → protocol(SSO) → core → onebot → action
napcat-onebot 的 214 个 action 全部依赖 NapCatCore(NodeIKernel session),
SnowLuma 的 action 依赖 BridgeInterface(封包)。没有共享层可以插进去。
| # | 理由 | 证据 |
|---|---|---|
| 1 | 架构方向正确 | 进程外 hook 是 QQ 生态的必然方向 |
| 2 | 迁移量不对称 | 补插件系统 ≈ 20% 工作量;换 hook ≈ 90% |
| 3 | 多账号 | SnowLuma 原生支持,NapCat 架构上做不到 |
| 4 | Linux 原生 | 实测通过(见 reports/PHASE0-LINUX-VERIFICATION.md) |
| 5 | 不依赖版本表 | 对「上游已死」是决定性的 |
如果你最看重现成插件生态,NapCat 是唯一选择。但上游已死,生态也不会再增长。
unified/
├─ packages/
│ └─ native-offsets/ # ← 本阶段唯一交付物
│ ├─ src/
│ │ ├─ signature.ts # 通配符签名解析 + 扫描
│ │ ├─ binary.ts # PE/ELF 解析 + 三套坐标换算
│ │ ├─ resolver.ts # 主流程 + 交叉验证
│ │ └─ signatures/
│ │ └─ napcat-hooks.ts # NapCat send/recv 签名(实测采集)
│ └─ tests/ # 37 个测试
├─ tools/
│ ├─ resolve-offsets.ts # 运行时 CLI
│ └─ reversing/ # 离线逆向工具(Python)
│ ├─ analyze-hook-sig.py # 验证签名唯一性
│ ├─ offset_resolver.py # 通用坐标解析(来自 LLQQNT)
│ └─ README.md
└─ docs/
├─ MERGE-PLAN.md # 本文件
├─ OFFSET-MAINTENANCE.md # 偏移维护指南
└─ NAPCAT-OFFSET-REVERSING.md # NapCat 逆向能力(独立保留)
后续阶段会把 SnowLuma 的 14 个包和 NapCat 的能力移植进来。 当前只落地了第一个包,因为它是解决实际问题的最小可交付单元。
不是随意选的,而是因为它是唯一一个能立刻解决实际阻塞的模块:
阶段 0 验证时发现:
Linux QQ 3.2.35-54395 + NapCat napi2native
→ initHook 用 3.2.34 的偏移
→ Segmentation fault
NapCat 的 napi2native.json 硬编码了每个版本的偏移,表里没有的版本直接崩溃。
没人会再维护那张表。
实测发现 NapCat 的偏移是 RVA(不是 file offset),且 hook 点精确落在 标准 x64 函数序言上。这意味着可以用字节特征定位,不需要版本表。
$ pnpm resolve-offsets <wrapper.node>
[HIT] napcat.wrapper.send
file_offset=0xc3e22f rva=0xc3ee2f va=0x180c3ee2f section=.text
[HIT] napcat.wrapper.recv
file_offset=0x24a5755 rva=0x24a6355 va=0x1824a6355 section=.text
=> 全部签名命中唯一,可直接用于 hook。
解析出的 RVA 与 NapCat 静态表精确一致,证明可无缝替代。
你要求「NapCat 逆向新 wrapper.node 算出偏移作为独立保留」。已实现:
| 签名扫描 | 偏移逆向 | |
|---|---|---|
| 位置 | packages/native-offsets |
docs/NAPCAT-OFFSET-REVERSING.md + tools/reversing/ |
| 输入 | 二进制 + 字节特征 | 二进制 + 符号/调用关系 |
| 适用 | 常规升级(首选) | 签名失效时(兜底) |
| 自动化 | ✅ 全自动 | ❌ 人工 |
| 跨版本 | 预期稳定 | 每次都要重做 |
工作流:
QQ 升级
↓
pnpm resolve-offsets
↓
命中唯一? ──是──→ 直接可用(最常见)
│
否
↓
按 NAPCAT-OFFSET-REVERSING.md 手工定位
↓
用 analyze-hook-sig.py 验证新位置
↓
更新签名表
tools/reversing/ 保留了两个 Python 工具:
analyze-hook-sig.py —— 从实测偏移出发,分析唯一性、生成签名候选offset_resolver.py —— 通用 PE/ELF 坐标换算(来自 LLQQNT,处理文件校验补丁)| 项 | Windows | Linux |
|---|---|---|
| SnowLuma 构建 | ✅ | ✅ |
| SnowLuma typecheck | ✅ 14/14 | ✅ 14/14 |
| SnowLuma 测试 | ⚠️ 1501/1504 | ⚠️ 1501/1504 |
| SnowLuma 注入 | ✅ | ✅ |
| SnowLuma 握手 | ✅ QHP1 | ✅ QHP1 |
| NapCat 构建 | ✅ | — |
| NapCat 封包 hook | ✅ 表内有版本 | ❌ 表内无版本 |
Test Files 3 passed (3)
Tests 37 passed (37)
包含对真实 117MB wrapper.node 的集成测试:
0xc3ee2f ✓ 与 NapCat 表一致0x24a6355 ✓ 与 NapCat 表一致.text 且以函数序言开头2026-11-13 实测的真实缺口(用运行时 ACTION_REGISTRY.executableNames 对比,排除测试项):
已完成(3 个,都是「已有能力换个名字暴露」):
| Action | 做法 |
|---|---|
set_custom_face_desc |
modify_custom_face 的别名 —— 同一个 OIDB 0x902e |
get_qzone_comment_list |
get_qzone_msg_detail 的别名 —— 同一份数据(含 commentlist 分页) |
upload_images_to_qun_album |
新实现:循环调用 groupAlbum.upload,逐张返回结果 |
剩余 9 个:
| 优先级 | Action | 阻塞点 |
|---|---|---|
| P2 | get_robot_uin_range |
需逆向 NodeIKernel 的 RobotService 对应 OIDB |
| P2 | 在线文件传输(5 个) | 需逆向 OnlineApi 的协议 |
| P3 | get_guild_list / get_guild_service_profile |
QQ 频道,需从零逆向协议 |
| ❌ | send_packet |
依赖封包 hook,Linux 3.2.35 不可用 |
重要修正:之前两次缺口分析都是错的。 第一次说「23 个」,第二次说「13 个」,实际是 12 个(
set_custom_face_desc补上后剩 11 个, 再补 2 个别名后剩 9 个)。错误原因:
- grep
name: 'xxx'漏掉数组形式name: ['xxx', 'alias']comm对比前没排序- 没有运行时验证,只看静态文本
正确做法:用
ACTION_REGISTRY.executableNames拿到真实清单再对比。
另注:
ocr_image、fetch_ptt_text、send_group_sign、set_group_sign、get_rkey之前被我列为缺口,实际 SnowLuma 早就实现了。
2026-11-13 完成:packages/plugin,30 个测试通过。
| 组件 | 状态 |
|---|---|
| 公共契约(ctx / 钩子 / 配置 schema) | ✅ |
| 加载器(白名单 / 目录校验 / 敏感词 / 防逃逸) | ✅ |
| 管理器(生命周期 / 事件分发 / 配置持久化) | ✅ |
| action 注册表(只增不改 / 自动回滚) | ✅ |
| 接入 SnowLuma 运行时 | ✅ 事件分发 + action 回退 |
| WebUI 插件管理页 | ⏳ 未做 |
| 插件市场 | ⏳ 未做 |
接入点(2026-11-13 完成):
| 文件 | 改动 |
|---|---|
snowluma/src/packages/onebot/src/instance.ts |
setPluginBridge() + 事件分发 |
snowluma/src/packages/onebot/src/api-handler.ts |
setPluginActionResolver() + 回退 |
snowluma/src/packages/onebot/src/manager.ts |
转发给所有实例 |
unified/packages/plugin/src/snowluma-bridge.ts |
适配器 |
测试:新增 5 个(plugin-action-fallback.test.ts)+ 6 个桥接测试。
关键设计决策(详见 PLUGIN-SYSTEM.md):
ctx.core(NodeIKernel session)在 SnowLuma 里不存在,插件只能注册 action下一步:接入 OneBotInstance(事件管道末尾 dispatch、api-handler 插件回退)。
NapCat 的插件系统是最有价值的独有资产。移植要点:
plugin_init(ctx) / plugin_unload() / 配置 schemactx 接口要重新设计:NapCat 的 ctx.core 是 NapCatCore(NodeIKernel),
SnowLuma 是 BridgeInterface(封包),语义不同完整调查见 LINUX-HOOK-INVESTIGATION.md。
结论:QQ Linux 3.2.35 把网络层搬到了主程序的 libuv,wrapper.node 不再参与网络 I/O。
NapCat 的封包 hook 架构在 Linux 3.2.35 上已失效。
但主路径(NodeIKernel)完全可用 —— 已在 VPS 上实测 NapCat v4.18.42 跑通 (WebUI 6099、二维码登录、OneBot 网络适配均正常)。
频道(guild)、在线文件传输、群管理员权限细分。 频道需要逆向频道协议,成本最高,建议最后做或不做。
| 风险 | 等级 | 现状 |
|---|---|---|
| 上游已死 | 🔴 | 已确认,本项目即应对措施 |
| 原生二进制无源码 | 🔴 | 两边都是预编译,失效无法自修 |
| 许可限制 | 🟠 | 仅自用,公开需授权 |
| 签名跨版本未验证 | 🟡 | 只有 9.9.36-53644 单版本实测 |
| Linux 签名缺失 | 🟡 | 封包 hook 在 Linux 暂不可用 |
这是当前方案最大的未知。9.9.36-53644 上唯一命中,但没有第二个版本的 二进制做对照。要真正确认跨版本稳定,需要:
wrapper.nodepnpm resolve-offsets如果拿不到旧版本,就只能等 QQ 下次升级时自然验证。
上级目录 LLQQNT/ 是独立项目(LiteLoaderQQNT fork),解决 QQ 文件校验绕过。
它的 OFFSET-RESOLUTION.md 记录了同样的方法论:
| LLQQNT | 本项目 | |
|---|---|---|
| 目标 | 校验函数 call memcmp |
MSF 网络层函数入口 |
| 签名 | 18 B,无通配 | 53/110 B,有通配 |
| 跨版本 | 9.9.32→9.9.36 未改 | 待验证 |
LLQQNT 的 §10「sig 失效时怎么重新定特征」可直接套用。
offset_resolver.py 已复制到 tools/reversing/ 复用其坐标换算逻辑。