概述
对 v2.30.0(1228ac2)做代码审查时,在 core/adapter/ 发现 7 处缺陷。共同特征是适配器会以用户不可见的方式"脑死亡"——进程还在、注册表状态正常,但收发消息已经停了。对 7×24 运行的场景影响较大。
1. NapCat 接收循环遇到非 ConnectionClosed 异常直接退出,QQ 永久静默失联(High)
位置:core/adapter/src/qq/napcat_client/client.py:173-191
while not self.shutdown_event.is_set():
try:
async for message in self.websocket:
try:
data = json.loads(message)
await self.handle_message(data)
except json.JSONDecodeError:
logger.error(f"❌ 无法解析消息: {message}")
except websockets.exceptions.ConnectionClosed:
...
except Exception as e:
logger.error(f"❌ 监听错误: {e}")
break
实际行为:只有 ConnectionClosed 走重连分支,任何其他异常都会 break 彻底结束接收循环,且不重连、不通知上层。内层 try 只捕获 JSONDecodeError,handle_message(data) 抛出的异常(例如服务端下发的 JSON 是数组而非 dict 时,data.get("echo") 触发 AttributeError)会直接命中这个 break。
之后所有 send_action 只会超时,收消息完全停止,而适配器对象看起来仍在运行,只能靠翻日志发现。
期望行为:非 ConnectionClosed 异常也应记录后重连(或至少上报状态),而非静默退出循环。
2. BiliBiliAdapter 未实现 stop(),停止/更新/删除全部失败并中断关闭流程(High)
位置:core/adapter/src/bilibili/bilibili.py:26(类中无 stop 方法)、core/adapter/adapter_registry.py:467-469、core/lifecycle.py:283-284
adapter = self._adapters.get(name)
if adapter:
await adapter.stop()
实际行为:SocialMediaAdapter 基类未定义 stop(core/adapter/adapter_utils.py:98-121),BiliBiliAdapter 也没实现,因此对启用的 Bilibili 适配器调用 stop_adapter() 必然抛 AttributeError。影响链:
- 进程关停:
lifecycle.py:284 的 stop_adapters() 没有 per-adapter 的 try/except,AttributeError 会中断循环,后续其他适配器不会被停止,event_bus.stop()、数据库 dispose 等清理全部被跳过。
- WebUI 更新配置:
update_adapter(adapter_registry.py:404-412)中 stop_adapter 抛异常后整个 reload 被 except 吞掉,旧实例仍留在 _adapters 里,_start_listening 的轮询任务永不取消 —— 表现为改了配置不生效或新旧行为混杂。
- 删除适配器:
delete_adapter 虽捕获了异常,但没有任何机制取消 self.listening_task,删除后该任务继续每 20 秒拉评论并向事件总线发布事件。
期望行为:实现 BiliBiliAdapter.stop() 取消 listening_task;同时建议 stop_adapters() 对每个适配器单独 try/except,避免一个适配器拖垮整个关闭流程。
3. Bilibili 评论游标共用,会永久丢弃用户评论(High)
位置:core/adapter/src/bilibili/bilibili.py:196、:208、:217、:231
主评论与楼中楼共用同一个 self.last_process_ts 游标,但两者时间戳是交错的:楼层列表按主评论 ctime 升序遍历,而 bot 某条旧主评论下的子回复可能比后面的新主评论更晚发出。
复现(纯数据即可):bot 在 t=100 发过评论,用户 A 在 t=110 发主评论,用户 B 在 t=115 回复 bot 的楼。遍历顺序 bot(100) → A(110):处理 bot 楼时先把子回复(115)处理掉并把 last_process_ts 推进到 115,随后轮到 A(110) 时 110 > 115 不成立 —— A 的评论被永久跳过,无任何日志。反向场景同理。
另外 :196/:217 用严格大于比较,同秒内的多条评论也可能漏掉一条。
期望行为:主评论与子回复各自维护游标,或改用已处理评论 ID 集合去重。
4. Telegram 使用 filters.ALL:频道帖子崩溃、编辑消息被重复回复(Medium)
位置:core/adapter/src/telegram/telegram.py:99-100
# Register message handler for text, images, voice, etc. (excluding edited messages)
self.app.add_handler(MessageHandler(filters.ALL, self._on_message))
实际行为:注释声称排除编辑消息,但在 PTB v21(requirements.txt 锁定 python-telegram-bot~=21.3)中 filters.ALL 会同时接收 message、edited_message、channel_post、edited_channel_post。两个后果:
- 频道帖子崩溃:channel post 的
msg.from_user 为 None,chat.type == "channel" 落入私聊分支后 :226 的 str(user.id) 抛 AttributeError。bot 被加进任何频道后每条帖子都报错并丢弃。
- 编辑消息重复回复:用户编辑一条消息会作为全新消息再次发布到事件总线,bot 对同一内容回复两次。
期望行为:改用 filters.UpdateType.MESSAGES 之类的过滤器,使实际行为与注释一致。
5. QQ 出站消息链只对首个元素做类型分派,混合链中的附件被静默丢弃(Medium)
位置:core/adapter/src/qq/qq.py:149-153、core/adapter/src/qq/napcat_client/utils.py:91-178
ele = message_chain[0]
if isinstance(ele, Poke):
await self.bot.send_poke(user_id=ele.pid, group_id=group_id)
实际行为:_process_outgoing_message 会把 Poke/File/Video/Forward 原样放进列表(qq.py:825-832),但 send_group_message/send_direct_message 只检查 message_chain[0]。若链形如 [Text("给你发个文件"), File(...)],会走通用路径进入 QQMessageChain.to_list() —— 该方法的 if/elif 没有兜底分支,非 QQMessageType.* 的元素被无声跳过:文本发出去了,文件消失,无日志也无错误返回。LLM 生成"文字 + 附件"回复时必然触发。
期望行为:遍历整条消息链分派,或对无法序列化的元素记录警告并在返回结果中标记失败。
6. weixin_oc 长轮询错误路径无退避,形成紧循环(Medium)
位置:core/adapter/src/weixin_oc/weixin_oc.py:466-497、:824-837
_poll_inbound_updates 在 ret != 0 或 errcode != 0(非 -14)时正常 return,而 _run_loop 只在抛异常时才 sleep(5)。服务器持续快速返回业务错误(如账号被风控)时,循环会以 HTTP 往返速度全速空转刷请求,既刷爆日志又可能加重封禁。
另外 :468 的 ret is not None 是死代码(ret 来自 int(... or 0),永远不为 None)。
期望行为:错误返回路径同样施加退避 sleep。
7. 权限名单不做类型归一化,deny_list 模式下可能失效放行(Medium)
位置:core/adapter/adapter_utils.py:44-55
if group_allow_list and isinstance(group_allow_list, list):
self.group_list = group_allow_list
实际行为:配置里的列表元素原样使用,而所有适配器都用 str(chat.id) in self.group_list 比较。若用户手工编辑 JSON 把群号写成整数 [123456],"123456" in [123456] 为 False:
- allow_list 模式表现为"配置了却收不到消息"(用户能发现)
- deny_list 模式则表现为拉黑不生效、被拒绝的群继续被处理(安全侧 fail-open,且无任何告警)
期望行为:初始化时归一化为字符串,[str(x) for x in ...]。
补充:其他观察
- 后台任务普遍不保存引用且 done callback 从不调用
t.exception():adapter_registry.py:460-461、qq.py:134-135、weixin_oc.py:775-776。适配器 start_blocking() 抛异常时只会在 GC 时出现 "Task exception was never retrieved"。顺带一个日志问题:adapter_registry.py:461 的 callback 在任务结束时打印 "Started adapter",对 Bilibili 这类阻塞到死的适配器等于在它死亡时打印"已启动"。
- NapCat 重连有 20 次硬上限(
client.py:125-138),之后 close() 置位 shutdown_event 永不再连,一次超过 ~15 分钟的 NapCat 维护就意味着永久掉线需人工重启。
bilibili.py:126-128、:138 使用 print 而非 logger,send_comment 失败被吞掉。
bilibili.py:43 的 self.config.get("listening_interval") 若旧配置缺键返回 None,asyncio.sleep(None) 抛 TypeError 杀死监听任务。
- 测试覆盖:
tests/test_adapter_manager.py 仅 2 个用例(测适配器名不含冒号)。NapCat 重连状态机、QQMessageChain.to_list()、Telegram UTF-16 entity 切片、权限名单判定、Bilibili 评论游标均无测试,而其中多数是纯函数或可注入 fake client 的逻辑。
KiraAI version: v2.30.0(1228ac2)
Environment: 静态代码审查,非特定 OS/Python 版本相关
概述
对
v2.30.0(1228ac2)做代码审查时,在core/adapter/发现 7 处缺陷。共同特征是适配器会以用户不可见的方式"脑死亡"——进程还在、注册表状态正常,但收发消息已经停了。对 7×24 运行的场景影响较大。1. NapCat 接收循环遇到非
ConnectionClosed异常直接退出,QQ 永久静默失联(High)位置:
core/adapter/src/qq/napcat_client/client.py:173-191实际行为:只有
ConnectionClosed走重连分支,任何其他异常都会break彻底结束接收循环,且不重连、不通知上层。内层 try 只捕获JSONDecodeError,handle_message(data)抛出的异常(例如服务端下发的 JSON 是数组而非 dict 时,data.get("echo")触发AttributeError)会直接命中这个break。之后所有
send_action只会超时,收消息完全停止,而适配器对象看起来仍在运行,只能靠翻日志发现。期望行为:非
ConnectionClosed异常也应记录后重连(或至少上报状态),而非静默退出循环。2.
BiliBiliAdapter未实现stop(),停止/更新/删除全部失败并中断关闭流程(High)位置:
core/adapter/src/bilibili/bilibili.py:26(类中无stop方法)、core/adapter/adapter_registry.py:467-469、core/lifecycle.py:283-284实际行为:
SocialMediaAdapter基类未定义stop(core/adapter/adapter_utils.py:98-121),BiliBiliAdapter也没实现,因此对启用的 Bilibili 适配器调用stop_adapter()必然抛AttributeError。影响链:lifecycle.py:284的stop_adapters()没有 per-adapter 的 try/except,AttributeError会中断循环,后续其他适配器不会被停止,event_bus.stop()、数据库 dispose 等清理全部被跳过。update_adapter(adapter_registry.py:404-412)中stop_adapter抛异常后整个 reload 被 except 吞掉,旧实例仍留在_adapters里,_start_listening的轮询任务永不取消 —— 表现为改了配置不生效或新旧行为混杂。delete_adapter虽捕获了异常,但没有任何机制取消self.listening_task,删除后该任务继续每 20 秒拉评论并向事件总线发布事件。期望行为:实现
BiliBiliAdapter.stop()取消listening_task;同时建议stop_adapters()对每个适配器单独 try/except,避免一个适配器拖垮整个关闭流程。3. Bilibili 评论游标共用,会永久丢弃用户评论(High)
位置:
core/adapter/src/bilibili/bilibili.py:196、:208、:217、:231主评论与楼中楼共用同一个
self.last_process_ts游标,但两者时间戳是交错的:楼层列表按主评论ctime升序遍历,而 bot 某条旧主评论下的子回复可能比后面的新主评论更晚发出。复现(纯数据即可):bot 在 t=100 发过评论,用户 A 在 t=110 发主评论,用户 B 在 t=115 回复 bot 的楼。遍历顺序 bot(100) → A(110):处理 bot 楼时先把子回复(115)处理掉并把
last_process_ts推进到 115,随后轮到 A(110) 时110 > 115不成立 —— A 的评论被永久跳过,无任何日志。反向场景同理。另外
:196/:217用严格大于比较,同秒内的多条评论也可能漏掉一条。期望行为:主评论与子回复各自维护游标,或改用已处理评论 ID 集合去重。
4. Telegram 使用
filters.ALL:频道帖子崩溃、编辑消息被重复回复(Medium)位置:
core/adapter/src/telegram/telegram.py:99-100实际行为:注释声称排除编辑消息,但在 PTB v21(
requirements.txt锁定python-telegram-bot~=21.3)中filters.ALL会同时接收message、edited_message、channel_post、edited_channel_post。两个后果:msg.from_user为None,chat.type == "channel"落入私聊分支后:226的str(user.id)抛AttributeError。bot 被加进任何频道后每条帖子都报错并丢弃。期望行为:改用
filters.UpdateType.MESSAGES之类的过滤器,使实际行为与注释一致。5. QQ 出站消息链只对首个元素做类型分派,混合链中的附件被静默丢弃(Medium)
位置:
core/adapter/src/qq/qq.py:149-153、core/adapter/src/qq/napcat_client/utils.py:91-178实际行为:
_process_outgoing_message会把Poke/File/Video/Forward原样放进列表(qq.py:825-832),但send_group_message/send_direct_message只检查message_chain[0]。若链形如[Text("给你发个文件"), File(...)],会走通用路径进入QQMessageChain.to_list()—— 该方法的 if/elif 没有兜底分支,非QQMessageType.*的元素被无声跳过:文本发出去了,文件消失,无日志也无错误返回。LLM 生成"文字 + 附件"回复时必然触发。期望行为:遍历整条消息链分派,或对无法序列化的元素记录警告并在返回结果中标记失败。
6. weixin_oc 长轮询错误路径无退避,形成紧循环(Medium)
位置:
core/adapter/src/weixin_oc/weixin_oc.py:466-497、:824-837_poll_inbound_updates在ret != 0或errcode != 0(非 -14)时正常return,而_run_loop只在抛异常时才sleep(5)。服务器持续快速返回业务错误(如账号被风控)时,循环会以 HTTP 往返速度全速空转刷请求,既刷爆日志又可能加重封禁。另外
:468的ret is not None是死代码(ret来自int(... or 0),永远不为 None)。期望行为:错误返回路径同样施加退避 sleep。
7. 权限名单不做类型归一化,deny_list 模式下可能失效放行(Medium)
位置:
core/adapter/adapter_utils.py:44-55实际行为:配置里的列表元素原样使用,而所有适配器都用
str(chat.id) in self.group_list比较。若用户手工编辑 JSON 把群号写成整数[123456],"123456" in [123456]为False:期望行为:初始化时归一化为字符串,
[str(x) for x in ...]。补充:其他观察
t.exception():adapter_registry.py:460-461、qq.py:134-135、weixin_oc.py:775-776。适配器start_blocking()抛异常时只会在 GC 时出现 "Task exception was never retrieved"。顺带一个日志问题:adapter_registry.py:461的 callback 在任务结束时打印 "Started adapter",对 Bilibili 这类阻塞到死的适配器等于在它死亡时打印"已启动"。client.py:125-138),之后close()置位shutdown_event永不再连,一次超过 ~15 分钟的 NapCat 维护就意味着永久掉线需人工重启。bilibili.py:126-128、:138使用print而非 logger,send_comment失败被吞掉。bilibili.py:43的self.config.get("listening_interval")若旧配置缺键返回None,asyncio.sleep(None)抛TypeError杀死监听任务。tests/test_adapter_manager.py仅 2 个用例(测适配器名不含冒号)。NapCat 重连状态机、QQMessageChain.to_list()、Telegram UTF-16 entity 切片、权限名单判定、Bilibili 评论游标均无测试,而其中多数是纯函数或可注入 fake client 的逻辑。KiraAI version:
v2.30.0(1228ac2)Environment: 静态代码审查,非特定 OS/Python 版本相关