# bridges/napcat.py — NapCat OneBot 11 反向 WebSocket 桥(QQ 入站传输层) # NapCat 作为 WS 客户端反向连入本模块的 0.0.0.0:9089。 # 职责: 接收 OneBot 事件 -> 归一化 -> 交给 dispatch.handle_message。 # 出站适配(NapcatChannel)与 OneBot 协议工具在 bot/qq_channel.py(与 tg_channel.py 同级)。 import json import os import sys import time from websockets.server import serve from websockets.exceptions import ConnectionClosed # 无入口引导(main.py)直接导入本模块时,也能找到 bot/ 根目录下的平台层 _BOT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if _BOT_ROOT not in sys.path: sys.path.insert(0, _BOT_ROOT) from channel import register_channel, send_message from dispatch import handle_message import identity import identity_cmd from qq_channel import (NapcatChannel, BOT_QQ, get_plain_text, get_image_infos, is_at_me, is_reply_to_me, re_sub_at) WEBSOCKET_PORT = 9089 FRIEND_PROMPT_DEDUP_SECONDS = 30 _friend_prompted_at = {} async def _prompt_unbound_friend(channel: NapcatChannel, user_id): """好友建立后仅向未绑定 QQ 发送一次绑定指引。""" try: user_id = int(user_id) except (TypeError, ValueError): return False if identity.resolve("qq", user_id) is not None: return False now = time.monotonic() if now - _friend_prompted_at.get(user_id, 0) < FRIEND_PROMPT_DEDUP_SECONDS: return False try: sent = await send_message(channel, "private", user_id, identity_cmd.UNBOUND_TEXT) except Exception as exc: print(f"[qq] 好友 {user_id} 绑定提示发送失败: {type(exc).__name__}") return False if sent is False: print(f"[qq] 好友 {user_id} 绑定提示未送达") return False _friend_prompted_at[user_id] = now print(f"[qq] 已向新好友 {user_id} 发送绑定指引") return True async def handle_connection(websocket, channel: NapcatChannel): print("[qq] NapCat 客户端已连接") channel.attach(websocket) try: async for raw_message in websocket: try: data = json.loads(raw_message) except json.JSONDecodeError: continue # API 回执(send_msg 的 echo 响应)优先分流,不进入事件处理 if channel.resolve_receipt(data): continue post_type = data.get("post_type") # 不改变好友申请审批策略;只在好友关系实际建立后提示绑定。 if post_type == "notice" and data.get("notice_type") == "friend_add": await _prompt_unbound_friend(channel, data.get("user_id")) continue if post_type != "message": continue msg_type = data.get("message_type") segments = data.get("message", []) raw_text = get_plain_text(segments) user_id = data.get("user_id") if msg_type == "group": group_id = data.get("group_id") target_id = group_id at_me = is_at_me(segments, BOT_QQ) reply_me = is_reply_to_me(segments, BOT_QQ) is_mention = at_me or reply_me # @bot 时剥离 @ 前缀(与旧逻辑一致) if is_mention: raw_text = re_sub_at(raw_text) else: target_id = user_id is_mention = False image_infos = get_image_infos(segments) try: await handle_message(channel, msg_type, user_id, target_id, raw_text, image_infos, is_mention) except Exception as e: print(f"[qq] 消息处理异常: {e}") except ConnectionClosed: print("[qq] NapCat 客户端已断开") except Exception as e: print(f"[qq] 连接异常: {e}") finally: # 只清理本连接的状态:重连重叠时不得误清新连接的 ws 与在途回执 channel.detach(websocket) async def start_napcat_bridge(admin_qq=None): """启动 OneBot 11 反向 WS 服务(QQ 通道)。在 main() 的事件循环中 await。""" channel = NapcatChannel(admin_qq) register_channel(channel) print(f"[qq] WebSocket 服务器已在端口 {WEBSOCKET_PORT} 上运行,等待 NapCat 反向连接...") print(f"[qq] 机器人 QQ: {BOT_QQ}") await serve(handle_connection_factory(channel), os.environ.get("NAPCAT_WS_HOST", "127.0.0.1"), WEBSOCKET_PORT) def handle_connection_factory(channel): async def _h(ws): await handle_connection(ws, channel) return _h