Files
RRRosmontis 34089d0042 Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
2026-11-11 00:25:30 +08:00

123 lines
4.8 KiB
Python

# 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