Skip to content

feat: add NapCat OneBot channel support - #40

Merged
Playa-0v0 merged 4 commits into
Playa-0v0:masterfrom
Tobi1chi:master
Aug 28, 2026
Merged

feat: add NapCat OneBot channel support#40
Playa-0v0 merged 4 commits into
Playa-0v0:masterfrom
Tobi1chi:master

Conversation

@Tobi1chi

@Tobi1chi Tobi1chi commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

概要

为 Cyrene 增加 QQ / NapCat OneBot 渠道支持。

主要改动

  • 新增 NapCat OneBot 适配器及反向 WebSocket 连接支持
  • 新增 QQ 渠道的消息收发、会话路由和渠道配置
  • 支持文本、图片、音频、文件等消息类型的标准化与发送
  • 增加媒体处理、权限策略、设置面板及相关测试
  • 保留微信、飞书等现有渠道行为

验证

  • 本次 PR 相关测试:50 个通过
  • npm run build:通过
  • 本机完整测试:2605 个通过,16 个失败,原因是 macOS 环境下已有的 Windows 路径兼容性用例;另有 2 个跳过

其他

  • 已将目标仓库最新 master 合入 PR 分支并解决冲突
  • 当前 PR 已恢复为可合并状态

Tobi1chi and others added 3 commits August 28, 2026 01:39
1. onebot-action-client: catch stream callback errors on the packet chain.
   A throwing onPacket left rejected promises unhandled (Node >= 15 raises
   unhandledRejection and crashes the Electron main process); NapCat keeps
   pushing chunks after a client-side failure, so every subsequent packet
   produced a new unhandled rejection. First error is now stored and the
   promise rejects at the terminal packet.

2. onebot-media: validate stream chunk contiguity and write offsets.
   Chunk indexes only deduplicated, never checked for holes, so a stream
   skipping an index wrote a sparse file that passed byte-count, chunk-count
   and total_bytes checks - a corrupted image/file was handed to the agent.
   Also bound chunk write offsets by file size to prevent oversized sparse
   files from a hostile NapCat.

3. dispatcher/history-log: migrate channel history from senderId-keyed to
   chatId-keyed sessionIds. Feishu p2p chatId (oc_xxx) and senderId (ou_xxx)
   live in different ID spaces, so existing Feishu users lost their sliding
   window context on upgrade. Old files are copied to the new key once
   (idempotent, never overwrites).
1. onebot-reverse-ws: require an Access Token for any non-loopback
   listen address, not only literal 0.0.0.0. LAN IPs, the WSL vEthernet
   adapter and IPv6 :: previously allowed unauthenticated clients that
   could impersonate NapCat (forge whitelisted user_id message events)
   and trigger SSRF via media URL downloads. The settings panel now
   pre-generates a token when saving a WSL/custom non-loopback config
   without one, prompting the user to copy it to NapCat first (tokens
   are never echoed back after saving).

2. onebot-media: stream URL downloads instead of buffering the whole
   body. A missing or forged content-length (chunked responses) made
   arrayBuffer() read unbounded data into memory before the size check,
   allowing main-process OOM; the reader now aborts as soon as the
   8 MiB threshold is exceeded.

3. onebot-reverse-ws: stop() no longer hangs on idle keep-alive
   connections. The 404 handler replies with keep-alive, so a single
   browser or port-scanner probe left a connection that kept
   server.close() (and app quit) waiting for the 5s keep-alive timeout;
   closeAllConnections() is now called with a 3s fallback timer.

4. onebot-normalizer: emit a visible placeholder for unsupported
   message segments (location/share/forward/poke/...) instead of
   silently dropping them, so the agent knows such content exists.

Docs updated for the token requirement. Full suite: 2638 tests pass.
@Playa-0v0

Copy link
Copy Markdown
Owner

PR Review Comment

非常感谢这个 PR!整体代码质量很高。我完成了完整的代码审查 + PoC 复现验证,发现若干问题。其中 3 个 Blocker 问题 + 4 个问题由 Cyrene 定位+几个codex找的出问题并未修复,3+4已在分支修复,对应两个提交:d300953834872d,所有修复均附带回归测试,全量测试 2638/2638 全部通过。

已修复(commit: d300953

  1. 流回调抛出异常导致主进程崩溃
    onebot‑action‑client.tspacketChain.then(() => onPacket(packet)) 缺少 rejection handler。
    onPacket 存在大量抛出异常路径(重复 file_info、chunk 大小不匹配、超过 100MiB 等)。一旦抛出异常,NapCat 会继续下发后续数据包,每一个数据包都会生成新的未处理 rejected Promise。
    src/main 没有全局 unhandledRejection 兜底,Node.js ≥15 遇到未捕获 Promise rejection 默认直接崩溃主进程。

Fix:链路中捕获首条错误,等到 terminal 数据包到达时统一 reject。

  1. 流式下载会生成损坏文件却全部校验通过(PoC 可复现)
    onebot‑media.ts chunk 索引仅做重复校验,没有校验索引空洞。例如传入分片 {0,1,3} 跳过 2,磁盘生成带空洞的稀疏文件;但最终字节数、chunk 数量、total_bytes 三项校验全部判定通过,损坏附件会原样交付给 Agent。
    同时写入偏移 index * chunkSize 没有上界限制,恶意 NapCat 可构造 GB 级别稀疏文件占用磁盘。

Fix:写入前校验偏移上界;下载完成校验分片索引集合必须完整覆盖 {0..n‑1}

  1. 飞书老用户会话历史静默丢失(回归问题)
    dispatcher.ts makeSessionId(msg.channel, msg.chatId) 将 key 从 senderId 修改为 chatId
    飞书 P2P 会话中 chatId(oc_xxx)senderId(ou_xxx) 属于两套 ID 命名空间,升级后旧会话滑窗文件无法匹配加载。微信两个字段取值一致不受该问题影响。

Fix:新增 migrateHistory(),幂等迁移,一次性将旧 key 的会话文件拷贝至新 key,不做覆盖。

已修复(commit: 834872d

  1. 非回环地址监听没有强制 Access Token
    onebot‑reverse‑ws.ts 原先仅对字面地址 0.0.0.0 强制校验 token。局域网IP、WSL/vEthernet、IPv6 :: 均可无鉴权访问。攻击者只要网络可达端口,即可伪装成 NapCat,伪造白名单 user_id 注入消息;还可以借助 downloadUrl 的 HTTP(S) 请求触发 SSRF。

Fix:新增 isLoopbackHost() 工具函数,非回环地址一律强制 Access Token;设置面板检测到非回环监听且未配置 token 时自动生成 token 并提示用户复制配置到 NapCat。

  1. downloadUrl 存在无边界内存占用风险
    onebot‑media.ts 使用 arrayBuffer() 将响应完整读入内存后再做大小校验。遇到 chunked 编码无 content‑length 的资源,会将数 GB 数据全部加载进内存。

Fix:改用 getReader() 流式读取并累加大小,超过 8MiB 立即 abort 请求。

  1. stop() 会永久挂起阻塞退出
    onebot‑reverse‑ws.ts 的 404 handler 返回 keep‑alive 响应。浏览器、端口扫描器访问端口后会留下空闲长连接;server.close() 需要等待 5s keep‑alive 超时才会执行回调,阻塞应用正常退出流程。

Fix:调用 closeAllConnections(),同时增加 3s 超时兜底。

  1. 未知消息段直接静默丢弃
    onebot‑normalizer.ts,位置、分享、合并转发等未适配消息段直接被丢弃,Agent 完全感知不到原始消息存在。

Fix:输出占位文本 [未支持的消息类型:xxx],保留原始消息痕迹。

不阻塞合并,后续通过 Issue 跟进(Codex 扫描发现)

  • 队列已满时静默丢弃消息,且 dedupe 标记提前写入(napcat‑adapter.ts:293);断线重连补发消息会被去重逻辑拦截,消息永久丢失。
  • onebot‑media.test.ts:81 SHA‑256 单元测试逻辑无效:mock 将代码内部计算的哈希原样返回,hash mismatch 分支完全没有测试覆盖。
  • uploadStream 对文件做两次读取:完整读文件计算哈希,之后再流式读取一遍用于上传。
  • 对 bot 自身的 @ 仍然调用 get_group_member_info,属于无效网络请求(虽然有缓存缓解)。
  • 每一条入站消息同步执行磁盘读 + safeStorage 解密配置(napcat‑adapter.ts:270),高消息吞吐量场景产生严重 IO 放大。

@Playa-0v0
Playa-0v0 merged commit 0480178 into Playa-0v0:master Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants