feat: 添加带有消息处理和路由功能的NEW_napcat_adapter插件

- 为NEW_napcat_adapter插件实现了核心模块,包括消息处理、事件处理和路由。
- 创建了MessageHandler、MetaEventHandler和NoticeHandler来处理收到的消息和事件。
- 开发了SendHandler,用于向Napcat发送回消息。
引入了StreamRouter来管理多个聊天流,确保消息的顺序和高效处理。
- 增加了对各种消息类型和格式的支持,包括文本、图像和通知。
- 建立了一个用于监控和调试的日志系统。
This commit is contained in:
Windpicker-owo
2025-11-24 13:24:55 +08:00
parent b08c70dfa6
commit 36fce6ca98
28 changed files with 3041 additions and 824 deletions

View File

@@ -0,0 +1,427 @@
# NEW_napcat_adapter
基于 mofox-bus v2.x 的 Napcat 适配器(使用 BaseAdapter 架构)
## 🏗️ 架构设计
本插件采用 **BaseAdapter 继承模式** 重写,完全抛弃旧版 maim_message 库,改用 mofox-bus 的 TypedDict 数据结构。
### 核心组件
- **NapcatAdapter**: 继承自 `mofox_bus.AdapterBase`,负责 OneBot 11 协议与 MessageEnvelope 的双向转换
- **WebSocketAdapterOptions**: 自动管理 WebSocket 连接,提供 incoming_parser 和 outgoing_encoder
- **CoreMessageSink**: 通过 `InProcessCoreSink` 将消息递送到核心系统
- **Handlers**: 独立的消息处理器,分为 to_core接收和 to_napcat发送两个方向
## 📁 项目结构
```
NEW_napcat_adapter/
├── plugin.py # ✅ 主插件文件BaseAdapter实现
├── _manifest.json # 插件清单
└── src/
├── event_models.py # ✅ OneBot事件类型常量
├── common/
│ └── core_sink.py # ✅ 全局CoreSink访问点
├── utils/
│ ├── utils.py # ⏳ 工具函数(待实现)
│ ├── qq_emoji_list.py # ⏳ QQ表情映射待实现
│ ├── video_handler.py # ⏳ 视频处理(待实现)
│ └── message_chunker.py # ⏳ 消息切片(待实现)
├── websocket/
│ └── (无需单独实现使用WebSocketAdapterOptions)
├── database/
│ └── database.py # ⏳ 数据库模型(待实现)
└── handlers/
├── to_core/ # Napcat → MessageEnvelope 方向
│ ├── message_handler.py # ⏳ 消息处理(部分完成)
│ ├── notice_handler.py # ⏳ 通知处理(待完成)
│ └── meta_event_handler.py # ⏳ 元事件(待完成)
└── to_napcat/ # MessageEnvelope → Napcat API 方向
└── send_handler.py # ⏳ 发送处理(部分完成)
```
## 🚀 快速开始
### 使用方式
1. **配置文件**: 在 `config/plugins/NEW_napcat_adapter.toml` 中配置 WebSocket URL 和其他参数
2. **启动插件**: 插件自动在系统启动时加载
3. **WebSocket连接**: 自动连接到 Napcat OneBot 11 服务器
## 🔑 核心数据结构
### MessageEnvelope (mofox-bus v2.x)
```python
from mofox_bus import MessageEnvelope, SegPayload, MessageInfoPayload
# 创建消息信封
envelope: MessageEnvelope = {
"direction": "input",
"message_info": {
"message_type": "group",
"message_id": "12345",
"self_id": "bot_qq",
"user_info": {
"user_id": "sender_qq",
"user_name": "发送者",
"user_displayname": "昵称"
},
"group_info": {
"group_id": "group_id",
"group_name": "群名"
},
"to_me": False
},
"message_segment": {
"type": "seglist",
"data": [
{"type": "text", "data": "hello"},
{"type": "image", "data": "base64_data"}
]
},
"raw_message": "hello[图片]",
"platform": "napcat",
"message_id": "12345",
"timestamp_ms": 1234567890
}
```
### BaseAdapter 核心方法
```python
class NapcatAdapter(BaseAdapter):
async def from_platform_message(self, message: dict[str, Any]) -> MessageEnvelope | None:
"""将 OneBot 11 事件转换为 MessageEnvelope"""
# 路由到对应的 Handler
async def _send_platform_message(self, envelope: MessageEnvelope) -> dict[str, Any]:
"""将 MessageEnvelope 转换为 OneBot 11 API 调用"""
# 调用 SendHandler 处理
```
## 📝 实现进度
### ✅ 已完成的核心架构
1. **BaseAdapter 实现** (plugin.py)
- ✅ WebSocket 自动连接管理
- ✅ from_platform_message() 事件路由
- ✅ _send_platform_message() 消息发送
- ✅ API 响应池机制echo-based request-response
- ✅ CoreSink 集成
2. **Handler 基础结构**
- ✅ MessageHandler 骨架text、image、at 基本实现)
- ✅ NoticeHandler 骨架
- ✅ MetaEventHandler 骨架
- ✅ SendHandler 骨架(基本类型转换)
3. **辅助组件**
- ✅ event_models.py事件类型常量
- ✅ core_sink.py全局 CoreSink 访问)
- ✅ 配置 Schema 定义
### ⏳ 部分完成的功能
4. **消息类型处理** (MessageHandler)
- ✅ 基础消息类型text, image, at
- ❌ 高级消息类型face, reply, forward, video, json, file, rps, dice, shake
5. **发送处理** (SendHandler)
- ✅ 基础 SegPayload 转换text, image
- ❌ 高级 Seg 类型emoji, voice, voiceurl, music, videourl, file, command
### ❌ 待实现的功能
6. **通知事件处理** (NoticeHandler)
- ❌ 戳一戳事件
- ❌ 表情回应事件
- ❌ 撤回事件
- ❌ 禁言事件
7. **工具函数** (utils.py)
- ❌ get_group_info
- ❌ get_member_info
- ❌ get_image_base64
- ❌ get_message_detail
- ❌ get_record_detail
8. **权限系统**
- ❌ check_allow_to_chat()
- ❌ 群组黑名单/白名单
- ❌ 私聊黑名单/白名单
- ❌ QQ机器人检测
9. **其他组件**
- ❌ 视频处理器
- ❌ 消息切片器
- ❌ 数据库模型
- ❌ QQ 表情映射表
## 📋 下一步工作
### 优先级 1完善消息处理参考旧版 recv_handler/message_handler.py
1. **完整实现 MessageHandler.handle_raw_message()**
- [ ] face表情消息段
- [ ] reply回复消息段
- [ ] forward转发消息段解析
- [ ] video视频消息段
- [ ] jsonJSON卡片消息段
- [ ] file文件消息段
- [ ] rps/dice/shake特殊消息
2. **实现工具函数**(参考旧版 utils.py
- [ ] `get_group_info()` - 获取群组信息
- [ ] `get_member_info()` - 获取成员信息
- [ ] `get_image_base64()` - 下载图片并转Base64
- [ ] `get_message_detail()` - 获取消息详情
- [ ] `get_record_detail()` - 获取语音详情
3. **实现权限检查**
- [ ] `check_allow_to_chat()` - 检查是否允许聊天
- [ ] 群组白名单/黑名单逻辑
- [ ] 私聊白名单/黑名单逻辑
- [ ] QQ机器人检测ban_qq_bot
### 优先级 2完善发送处理参考旧版 send_handler.py
4. **完整实现 SendHandler._convert_seg_to_onebot()**
- [ ] emoji表情回应命令
- [ ] voice语音消息段
- [ ] voiceurl语音URL消息段
- [ ] music音乐卡片消息段
- [ ] videourl视频URL消息段
- [ ] file文件消息段
- [ ] command命令消息段
5. **实现命令处理**
- [ ] GROUP_BAN禁言
- [ ] GROUP_KICK踢人
- [ ] SEND_POKE戳一戳
- [ ] DELETE_MSG撤回消息
- [ ] GROUP_WHOLE_BAN全员禁言
- [ ] SET_GROUP_CARD设置群名片
- [ ] SET_GROUP_ADMIN设置管理员
### 优先级 3补全其他组件参考旧版对应文件
6. **NoticeHandler 实现**
- [ ] 戳一戳通知notify.poke
- [ ] 表情回应通知notice.group_emoji_like
- [ ] 消息撤回通知notice.group_recall
- [ ] 禁言通知notice.group_ban
7. **辅助组件**
- [ ] `qq_emoji_list.py` - QQ表情ID映射表
- [ ] `video_handler.py` - 视频处理ffmpeg封面提取
- [ ] `message_chunker.py` - 消息分块与重组
- [ ] `database.py` - 数据库模型(如有需要)
### 优先级 4测试与优化
8. **功能测试**
- [ ] 文本消息收发
- [ ] 图片消息收发
- [ ] @消息处理
- [ ] 表情/语音/视频消息
- [ ] 转发消息解析
- [ ] 所有命令功能
- [ ] 通知事件处理
9. **性能优化**
- [ ] 消息处理并发性能
- [ ] API响应池性能
- [ ] 内存占用优化
## 🔍 关键实现细节
### 1. MessageEnvelope vs 旧版 MessageBase
**不再使用 Seg dataclass**,全部使用 TypedDict
```python
# ❌ 旧版maim_message
from mofox_bus import Seg, MessageBase
seg = Seg(type="text", data="hello")
message = MessageBase(message_info=info, message_segment=seg)
# ✅ 新版mofox-bus v2.x
from mofox_bus import SegPayload, MessageEnvelope
seg_payload: SegPayload = {"type": "text", "data": "hello"}
envelope: MessageEnvelope = {
"direction": "input",
"message_info": {...},
"message_segment": seg_payload,
...
}
```
### 2. Handler 架构模式
**接收方向** (to_core):
```python
class MessageHandler:
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
async def handle_raw_message(self, data: dict[str, Any]) -> MessageEnvelope:
# 1. 解析 OneBot 11 数据
# 2. 构建 message_infoMessageInfoPayload
# 3. 转换消息段为 SegPayload
# 4. 返回完整的 MessageEnvelope
```
**发送方向** (to_napcat):
```python
class SendHandler:
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
async def handle_message(self, envelope: MessageEnvelope) -> dict[str, Any]:
# 1. 从 envelope 提取 message_segment
# 2. 递归转换 SegPayload → OneBot 格式
# 3. 调用 adapter.send_napcat_api() 发送
```
### 3. API 调用模式(响应池)
```python
# 在 NapcatAdapter 中
async def send_napcat_api(self, action: str, params: dict[str, Any]) -> dict[str, Any]:
# 1. 生成唯一 echo
echo = f"{action}_{uuid.uuid4()}"
# 2. 创建 Future 等待响应
future = asyncio.Future()
self._response_pool[echo] = future
# 3. 发送请求(通过 WebSocket
await self._send_request({"action": action, "params": params, "echo": echo})
# 4. 等待响应(带超时)
try:
result = await asyncio.wait_for(future, timeout=10.0)
return result
finally:
self._response_pool.pop(echo, None)
# 响应回来时(在 incoming_parser 中)
def _handle_api_response(data: dict[str, Any]):
echo = data.get("echo")
if echo in adapter._response_pool:
adapter._response_pool[echo].set_result(data)
```
### 4. 类型提示技巧
处理 TypedDict 的严格类型检查:
```python
# 使用 type: ignore 标注(编译时是 TypedDict运行时是 dict
envelope: MessageEnvelope = {
"direction": "input",
...
} # type: ignore[typeddict-item]
# 或在函数签名中使用 dict[str, Any]
async def from_platform_message(self, message: dict[str, Any]) -> MessageEnvelope | None:
...
return envelope # type: ignore[return-value]
```
## 🔍 测试检查清单
- [ ] 文本消息接收/发送
- [ ] 图片消息接收/发送
- [ ] 语音消息接收/发送
- [ ] 视频消息接收/发送
- [ ] @消息接收/发送
- [ ] 回复消息接收/发送
- [ ] 转发消息接收
- [ ] JSON消息接收
- [ ] 文件消息接收/发送
- [ ] 禁言命令
- [ ] 踢人命令
- [ ] 戳一戳命令
- [ ] 表情回应命令
- [ ] 通知事件处理
- [ ] 元事件处理
## 📚 参考资料
- **mofox-bus 文档**: 查看 `mofox_bus/types.py` 了解 TypedDict 定义
- **BaseAdapter 示例**: 参考 `docs/mofox_bus_demo_adapter.py`
- **旧版实现**: `src/plugins/built_in/napcat_adapter_plugin/` (仅参考逻辑)
- **OneBot 11 协议**: [OneBot 11 标准](https://github.com/botuniverse/onebot-11)
## ⚠️ 重要注意事项
1. **完全抛弃旧版数据结构**
- ❌ 不再使用 `Seg` dataclass
- ❌ 不再使用 `MessageBase`
- ✅ 全部使用 `SegPayload`TypedDict
- ✅ 全部使用 `MessageEnvelope`TypedDict
2. **BaseAdapter 生命周期**
- `__init__()` 中初始化同步资源
- `start()` 中执行异步初始化WebSocket连接自动建立
- `stop()` 中清理资源WebSocket自动断开
3. **WebSocketAdapterOptions 自动管理**
- 无需手动管理 WebSocket 连接
- incoming_parser 自动解析接收数据
- outgoing_encoder 自动编码发送数据
- 重连机制由基类处理
4. **CoreSink 依赖注入**
- 必须在插件加载后调用 `set_core_sink(sink)`
- 通过 `get_core_sink()` 全局访问
- 用于将消息递送到核心系统
5. **类型安全与灵活性平衡**
- TypedDict 在编译时提供类型检查
- 运行时仍是普通 dict可灵活操作
- 必要时使用 `type: ignore` 抑制误报
6. **参考旧版但不照搬**
- 旧版逻辑流程可参考
- 数据结构需完全重写
- API调用模式已改变响应池
## 📊 预估工作量
- ✅ 核心架构: **已完成** (BaseAdapter + Handlers 骨架)
- ⏳ 消息处理完善: **4-6 小时** (所有消息类型 + 工具函数)
- ⏳ 发送处理完善: **3-4 小时** (所有 Seg 类型 + 命令)
- ⏳ 通知事件处理: **2-3 小时** (poke/emoji_like/recall/ban)
- ⏳ 测试调试: **2-4 小时** (全流程测试)
- **总剩余时间: 11-17 小时**
## ✅ 完成标准
当以下条件全部满足时,重写完成:
1. ✅ BaseAdapter 架构实现完成
2. ⏳ 所有 OneBot 11 消息类型支持
3. ⏳ 所有发送消息段类型支持
4. ⏳ 所有通知事件正确处理
5. ⏳ 权限系统集成完成
6. ⏳ 与旧版功能完全对等
7. ⏳ 所有测试用例通过
---
**最后更新**: 2025-11-23
**架构状态**: ✅ 核心架构完成
**实现状态**: ⏳ 消息处理部分完成,需完善细节
**预计完成**: 根据优先级,核心功能预计 1-2 个工作日

View File

@@ -0,0 +1,16 @@
from src.plugin_system.base.plugin_metadata import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="napcat_plugin",
description="基于OneBot 11协议的NapCat QQ协议插件提供完整的QQ机器人API接口使用现有adapter连接",
usage="该插件提供 `napcat_tool` tool。",
version="1.0.0",
author="Windpicker_owo",
license="GPL-v3.0-or-later",
repository_url="https://github.com/Windpicker-owo",
keywords=["qq", "bot", "napcat", "onebot", "api", "websocket"],
categories=["protocol"],
extra={
"is_built_in": False,
},
)

View File

@@ -0,0 +1,330 @@
"""
Napcat 适配器(基于 MoFox-Bus 完全重写版)
核心流程:
1. Napcat WebSocket 连接 → 接收 OneBot 格式消息
2. from_platform_message: OneBot dict → MessageEnvelope
3. CoreSink → 推送到 MoFox-Bot 核心
4. 核心回复 → _send_platform_message: MessageEnvelope → OneBot API 调用
"""
from __future__ import annotations
import asyncio
import uuid
from typing import Any, ClassVar, Dict, List, Optional
import orjson
import websockets
from mofox_bus import CoreMessageSink, MessageEnvelope, WebSocketAdapterOptions
from src.common.logger import get_logger
from src.plugin_system import register_plugin
from src.plugin_system.base import BaseAdapter, BasePlugin
from src.plugin_system.apis import config_api
from .src.handlers.to_core.message_handler import MessageHandler
from .src.handlers.to_core.notice_handler import NoticeHandler
from .src.handlers.to_core.meta_event_handler import MetaEventHandler
from .src.handlers.to_napcat.send_handler import SendHandler
logger = get_logger("napcat_adapter")
class NapcatAdapter(BaseAdapter):
"""Napcat 适配器 - 完全基于 mofox-bus 架构"""
adapter_name = "napcat_adapter"
adapter_version = "2.0.0"
adapter_author = "MoFox Team"
adapter_description = "基于 MoFox-Bus 的 Napcat/OneBot 11 适配器"
platform = "qq"
run_in_subprocess = False
subprocess_entry = None
def __init__(self, core_sink: CoreMessageSink, plugin: Optional[BasePlugin] = None):
"""初始化 Napcat 适配器"""
# 从插件配置读取 WebSocket URL
if plugin:
mode = config_api.get_plugin_config(plugin.config, "napcat_server.mode", "reverse")
host = config_api.get_plugin_config(plugin.config, "napcat_server.host", "localhost")
port = config_api.get_plugin_config(plugin.config, "napcat_server.port", 8095)
url = config_api.get_plugin_config(plugin.config, "napcat_server.url", "")
access_token = config_api.get_plugin_config(plugin.config, "napcat_server.access_token", "")
if mode == "forward" and url:
ws_url = url
else:
ws_url = f"ws://{host}:{port}"
headers = {}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
else:
ws_url = "ws://127.0.0.1:8095"
headers = {}
# 配置 WebSocket 传输
transport = WebSocketAdapterOptions(
url=ws_url,
headers=headers if headers else None,
incoming_parser=self._parse_napcat_message,
outgoing_encoder=self._encode_napcat_response,
)
super().__init__(core_sink, plugin=plugin, transport=transport)
# 初始化处理器
self.message_handler = MessageHandler(self)
self.notice_handler = NoticeHandler(self)
self.meta_event_handler = MetaEventHandler(self)
self.send_handler = SendHandler(self)
# 响应池:用于存储等待的 API 响应
self._response_pool: Dict[str, asyncio.Future] = {}
self._response_timeout = 30.0
# WebSocket 连接(用于发送 API 请求)
# 注意_ws 继承自 BaseAdapter是 WebSocketLike 协议类型
self._napcat_ws = None # 可选的额外连接引用
async def on_adapter_loaded(self) -> None:
"""适配器加载时的初始化"""
logger.info("Napcat 适配器正在启动...")
# 设置处理器配置
if self.plugin:
self.message_handler.set_plugin_config(self.plugin.config)
self.notice_handler.set_plugin_config(self.plugin.config)
self.meta_event_handler.set_plugin_config(self.plugin.config)
self.send_handler.set_plugin_config(self.plugin.config)
logger.info("Napcat 适配器已加载")
async def on_adapter_unloaded(self) -> None:
"""适配器卸载时的清理"""
logger.info("Napcat 适配器正在关闭...")
# 清理响应池
for future in self._response_pool.values():
if not future.done():
future.cancel()
self._response_pool.clear()
logger.info("Napcat 适配器已关闭")
def _parse_napcat_message(self, raw: str | bytes) -> Any:
"""解析 Napcat/OneBot 消息"""
try:
if isinstance(raw, bytes):
data = orjson.loads(raw)
else:
data = orjson.loads(raw)
return data
except Exception as e:
logger.error(f"解析 Napcat 消息失败: {e}")
raise
def _encode_napcat_response(self, envelope: MessageEnvelope) -> bytes:
"""编码响应消息为 Napcat 格式(暂未使用,通过 API 调用发送)"""
return orjson.dumps(envelope)
async def from_platform_message(self, raw: Dict[str, Any]) -> MessageEnvelope: # type: ignore[override]
"""
将 Napcat/OneBot 原始消息转换为 MessageEnvelope
这是核心转换方法,处理:
- message 事件 → 消息
- notice 事件 → 通知(戳一戳、表情回复等)
- meta_event 事件 → 元事件(心跳、生命周期)
- API 响应 → 存入响应池
"""
post_type = raw.get("post_type")
# API 响应(没有 post_type有 echo
if post_type is None and "echo" in raw:
echo = raw.get("echo")
if echo and echo in self._response_pool:
future = self._response_pool[echo]
if not future.done():
future.set_result(raw)
# API 响应不需要转换为 MessageEnvelope返回空信封
return self._create_empty_envelope()
# 消息事件
if post_type == "message":
return await self.message_handler.handle_raw_message(raw) # type: ignore[return-value]
# 通知事件
elif post_type == "notice":
return await self.notice_handler.handle_notice(raw) # type: ignore[return-value]
# 元事件
elif post_type == "meta_event":
return await self.meta_event_handler.handle_meta_event(raw) # type: ignore[return-value]
# 未知事件类型
else:
logger.warning(f"未知的事件类型: {post_type}")
return self._create_empty_envelope() # type: ignore[return-value]
async def _send_platform_message(self, envelope: MessageEnvelope) -> None: # type: ignore[override]
"""
将 MessageEnvelope 转换并发送到 Napcat
这里不直接通过 WebSocket 发送 envelope
而是调用 Napcat APIsend_group_msg, send_private_msg 等)
"""
await self.send_handler.handle_message(envelope)
def _create_empty_envelope(self) -> MessageEnvelope: # type: ignore[return]
"""创建一个空的消息信封(用于不需要处理的事件)"""
import time
return {
"direction": "incoming",
"message_info": {
"platform": self.platform,
"message_id": str(uuid.uuid4()),
"time": time.time(),
},
"message_segment": {"type": "text", "data": "[系统事件]"},
"timestamp_ms": int(time.time() * 1000),
}
async def send_napcat_api(self, action: str, params: Dict[str, Any], timeout: float = 30.0) -> Dict[str, Any]:
"""
发送 Napcat API 请求并等待响应
Args:
action: API 动作名称(如 send_group_msg
params: API 参数
timeout: 超时时间(秒)
Returns:
API 响应数据
"""
if not self._ws:
raise RuntimeError("WebSocket 连接未建立")
# 生成唯一的 echo ID
echo = str(uuid.uuid4())
# 创建 Future 用于等待响应
future = asyncio.Future()
self._response_pool[echo] = future
# 构造请求
request = orjson.dumps({
"action": action,
"params": params,
"echo": echo,
})
try:
# 发送请求
await self._ws.send(request)
# 等待响应
response = await asyncio.wait_for(future, timeout=timeout)
return response
except asyncio.TimeoutError:
logger.error(f"API 请求超时: {action}")
raise
except Exception as e:
logger.error(f"API 请求失败: {action}, 错误: {e}")
raise
finally:
# 清理响应池
self._response_pool.pop(echo, None)
def get_ws_connection(self):
"""获取 WebSocket 连接(用于发送 API 请求)"""
if not self._ws:
raise RuntimeError("WebSocket 连接未建立")
return self._ws
@register_plugin
class NapcatAdapterPlugin(BasePlugin):
"""Napcat 适配器插件"""
plugin_name = "napcat_adapter_plugin"
enable_plugin = True
plugin_version = "2.0.0"
plugin_author = "MoFox Team"
plugin_description = "Napcat/OneBot 11 适配器(基于 MoFox-Bus 重写)"
# 配置 Schema
config_schema: ClassVar[dict] = {
"plugin": {
"name": {"type": str, "default": "napcat_adapter_plugin"},
"version": {"type": str, "default": "2.0.0"},
"enabled": {"type": bool, "default": True},
},
"napcat_server": {
"mode": {
"type": str,
"default": "reverse",
"description": "连接模式reverse=反向连接(作为服务器), forward=正向连接(作为客户端)",
},
"host": {"type": str, "default": "localhost"},
"port": {"type": int, "default": 8095},
"url": {"type": str, "default": "", "description": "正向连接时的完整URL"},
"access_token": {"type": str, "default": ""},
},
"features": {
"group_list_type": {"type": str, "default": "blacklist"},
"group_list": {"type": list, "default": []},
"private_list_type": {"type": str, "default": "blacklist"},
"private_list": {"type": list, "default": []},
"ban_user_id": {"type": list, "default": []},
"ban_qq_bot": {"type": bool, "default": False},
},
}
def __init__(self, plugin_dir: str = "", metadata: Any = None):
# 如果没有提供参数,创建一个默认的元数据
if metadata is None:
from src.plugin_system.base.plugin_metadata import PluginMetadata
metadata = PluginMetadata(
name=self.plugin_name,
version=self.plugin_version,
author=self.plugin_author,
description=self.plugin_description,
usage="",
dependencies=[],
python_dependencies=[],
)
if not plugin_dir:
from pathlib import Path
plugin_dir = str(Path(__file__).parent)
super().__init__(plugin_dir, metadata)
self._adapter: Optional[NapcatAdapter] = None
async def on_plugin_loaded(self):
"""插件加载时启动适配器"""
logger.info("Napcat 适配器插件正在加载...")
# 获取核心 Sink
from src.common.core_sink import get_core_sink
core_sink = get_core_sink()
# 创建并启动适配器
self._adapter = NapcatAdapter(core_sink, plugin=self)
await self._adapter.start()
logger.info("Napcat 适配器插件已加载")
async def on_plugin_unloaded(self):
"""插件卸载时停止适配器"""
if self._adapter:
await self._adapter.stop()
logger.info("Napcat 适配器插件已卸载")
def get_plugin_components(self) -> list:
"""返回适配器组件"""
return [(NapcatAdapter.get_adapter_info(), NapcatAdapter)]

View File

@@ -0,0 +1 @@
"""工具模块"""

View File

@@ -0,0 +1,310 @@
from enum import Enum
class MetaEventType:
lifecycle = "lifecycle" # 生命周期
class Lifecycle:
connect = "connect" # 生命周期 - WebSocket 连接成功
heartbeat = "heartbeat" # 心跳
class MessageType: # 接受消息大类
private = "private" # 私聊消息
class Private:
friend = "friend" # 私聊消息 - 好友
group = "group" # 私聊消息 - 群临时
group_self = "group_self" # 私聊消息 - 群中自身发送
other = "other" # 私聊消息 - 其他
group = "group" # 群聊消息
class Group:
normal = "normal" # 群聊消息 - 普通
anonymous = "anonymous" # 群聊消息 - 匿名消息
notice = "notice" # 群聊消息 - 系统提示
class NoticeType: # 通知事件
friend_recall = "friend_recall" # 私聊消息撤回
group_recall = "group_recall" # 群聊消息撤回
notify = "notify"
group_ban = "group_ban" # 群禁言
group_msg_emoji_like = "group_msg_emoji_like" # 群聊表情回复
group_upload = "group_upload" # 群文件上传
class Notify:
poke = "poke" # 戳一戳
input_status = "input_status" # 正在输入
class GroupBan:
ban = "ban" # 禁言
lift_ban = "lift_ban" # 解除禁言
class RealMessageType: # 实际消息分类
text = "text" # 纯文本
face = "face" # qq表情
image = "image" # 图片
record = "record" # 语音
video = "video" # 视频
at = "at" # @某人
rps = "rps" # 猜拳魔法表情
dice = "dice" # 骰子
shake = "shake" # 私聊窗口抖动(只收)
poke = "poke" # 群聊戳一戳
share = "share" # 链接分享json形式
reply = "reply" # 回复消息
forward = "forward" # 转发消息
node = "node" # 转发消息节点
json = "json" # json消息
file = "file" # 文件
class MessageSentType:
private = "private"
class Private:
friend = "friend"
group = "group"
group = "group"
class Group:
normal = "normal"
class CommandType(Enum):
"""命令类型"""
GROUP_BAN = "set_group_ban" # 禁言用户
GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言
GROUP_KICK = "set_group_kick" # 踢出群聊
SEND_POKE = "send_poke" # 戳一戳
DELETE_MSG = "delete_msg" # 撤回消息
AI_VOICE_SEND = "ai_voice_send" # AI语音发送
SET_EMOJI_LIKE = "set_msg_emoji_like" # 设置表情回应
SEND_AT_MESSAGE = "send_at_message" # 发送@消息
SEND_LIKE = "send_like" # 发送点赞
def __str__(self) -> str:
return self.value
# 支持的消息格式
ACCEPT_FORMAT = [
"text",
"image",
"emoji",
"reply",
"voice",
"command",
"voiceurl",
"music",
"videourl",
"file",
]
# 插件名称
PLUGIN_NAME = "NEW_napcat_adapter"
# QQ表情映射表
QQ_FACE = {
"0": "[表情:惊讶]",
"1": "[表情:撇嘴]",
"2": "[表情:色]",
"3": "[表情:发呆]",
"4": "[表情:得意]",
"5": "[表情:流泪]",
"6": "[表情:害羞]",
"7": "[表情:闭嘴]",
"8": "[表情:睡]",
"9": "[表情:大哭]",
"10": "[表情:尴尬]",
"11": "[表情:发怒]",
"12": "[表情:调皮]",
"13": "[表情:呲牙]",
"14": "[表情:微笑]",
"15": "[表情:难过]",
"16": "[表情:酷]",
"18": "[表情:抓狂]",
"19": "[表情:吐]",
"20": "[表情:偷笑]",
"21": "[表情:可爱]",
"22": "[表情:白眼]",
"23": "[表情:傲慢]",
"24": "[表情:饥饿]",
"25": "[表情:困]",
"26": "[表情:惊恐]",
"27": "[表情:流汗]",
"28": "[表情:憨笑]",
"29": "[表情:悠闲]",
"30": "[表情:奋斗]",
"31": "[表情:咒骂]",
"32": "[表情:疑问]",
"33": "[表情:嘘]",
"34": "[表情:晕]",
"35": "[表情:折磨]",
"36": "[表情:衰]",
"37": "[表情:骷髅]",
"38": "[表情:敲打]",
"39": "[表情:再见]",
"41": "[表情:发抖]",
"42": "[表情:爱情]",
"43": "[表情:跳跳]",
"46": "[表情:猪头]",
"49": "[表情:拥抱]",
"53": "[表情:蛋糕]",
"56": "[表情:刀]",
"59": "[表情:便便]",
"60": "[表情:咖啡]",
"63": "[表情:玫瑰]",
"64": "[表情:凋谢]",
"66": "[表情:爱心]",
"67": "[表情:心碎]",
"74": "[表情:太阳]",
"75": "[表情:月亮]",
"76": "[表情:赞]",
"77": "[表情:踩]",
"78": "[表情:握手]",
"79": "[表情:胜利]",
"85": "[表情:飞吻]",
"86": "[表情:怄火]",
"89": "[表情:西瓜]",
"96": "[表情:冷汗]",
"97": "[表情:擦汗]",
"98": "[表情:抠鼻]",
"99": "[表情:鼓掌]",
"100": "[表情:糗大了]",
"101": "[表情:坏笑]",
"102": "[表情:左哼哼]",
"103": "[表情:右哼哼]",
"104": "[表情:哈欠]",
"105": "[表情:鄙视]",
"106": "[表情:委屈]",
"107": "[表情:快哭了]",
"108": "[表情:阴险]",
"109": "[表情:左亲亲]",
"110": "[表情:吓]",
"111": "[表情:可怜]",
"112": "[表情:菜刀]",
"114": "[表情:篮球]",
"116": "[表情:示爱]",
"118": "[表情:抱拳]",
"119": "[表情:勾引]",
"120": "[表情:拳头]",
"121": "[表情:差劲]",
"123": "[表情NO]",
"124": "[表情OK]",
"125": "[表情:转圈]",
"129": "[表情:挥手]",
"137": "[表情:鞭炮]",
"144": "[表情:喝彩]",
"146": "[表情:爆筋]",
"147": "[表情:棒棒糖]",
"169": "[表情:手枪]",
"171": "[表情:茶]",
"172": "[表情:眨眼睛]",
"173": "[表情:泪奔]",
"174": "[表情:无奈]",
"175": "[表情:卖萌]",
"176": "[表情:小纠结]",
"177": "[表情:喷血]",
"178": "[表情:斜眼笑]",
"179": "[表情doge]",
"181": "[表情:戳一戳]",
"182": "[表情:笑哭]",
"183": "[表情:我最美]",
"185": "[表情:羊驼]",
"187": "[表情:幽灵]",
"201": "[表情:点赞]",
"212": "[表情:托腮]",
"262": "[表情:脑阔疼]",
"263": "[表情:沧桑]",
"264": "[表情:捂脸]",
"265": "[表情:辣眼睛]",
"266": "[表情:哦哟]",
"267": "[表情:头秃]",
"268": "[表情:问号脸]",
"269": "[表情:暗中观察]",
"270": "[表情emm]",
"271": "[表情:吃瓜]",
"272": "[表情:呵呵哒]",
"273": "[表情:我酸了]",
"277": "[表情:滑稽狗头]",
"281": "[表情:翻白眼]",
"282": "[表情:敬礼]",
"283": "[表情:狂笑]",
"284": "[表情:面无表情]",
"285": "[表情:摸鱼]",
"286": "[表情:魔鬼笑]",
"287": "[表情:哦]",
"289": "[表情:睁眼]",
"293": "[表情:摸锦鲤]",
"294": "[表情:期待]",
"295": "[表情:拿到红包]",
"297": "[表情:拜谢]",
"298": "[表情:元宝]",
"299": "[表情:牛啊]",
"300": "[表情:胖三斤]",
"302": "[表情:左拜年]",
"303": "[表情:右拜年]",
"305": "[表情:右亲亲]",
"306": "[表情:牛气冲天]",
"307": "[表情:喵喵]",
"311": "[表情打call]",
"312": "[表情:变形]",
"314": "[表情:仔细分析]",
"317": "[表情:菜汪]",
"318": "[表情:崇拜]",
"319": "[表情:比心]",
"320": "[表情:庆祝]",
"323": "[表情:嫌弃]",
"324": "[表情:吃糖]",
"325": "[表情:惊吓]",
"326": "[表情:生气]",
"332": "[表情:举牌牌]",
"333": "[表情:烟花]",
"334": "[表情:虎虎生威]",
"336": "[表情:豹富]",
"337": "[表情:花朵脸]",
"338": "[表情:我想开了]",
"339": "[表情:舔屏]",
"341": "[表情:打招呼]",
"342": "[表情酸Q]",
"343": "[表情:我方了]",
"344": "[表情:大怨种]",
"345": "[表情:红包多多]",
"346": "[表情:你真棒棒]",
"347": "[表情:大展宏兔]",
"349": "[表情:坚强]",
"350": "[表情:贴贴]",
"351": "[表情:敲敲]",
"352": "[表情:咦]",
"353": "[表情:拜托]",
"354": "[表情:尊嘟假嘟]",
"355": "[表情:耶]",
"356": "[表情666]",
"357": "[表情:裂开]",
"392": "[表情:龙年快乐]",
"393": "[表情:新年中龙]",
"394": "[表情:新年大龙]",
"395": "[表情:略略略]",
"396": "[表情:龙年快乐]",
"424": "[表情:按钮]",
}
__all__ = [
"MetaEventType",
"MessageType",
"NoticeType",
"RealMessageType",
"MessageSentType",
"CommandType",
"ACCEPT_FORMAT",
"PLUGIN_NAME",
"QQ_FACE",
]

View File

@@ -0,0 +1 @@
"""处理器模块"""

View File

@@ -0,0 +1 @@
"""接收方向处理器"""

View File

@@ -0,0 +1,126 @@
"""消息处理器 - 将 Napcat OneBot 消息转换为 MessageEnvelope"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from src.common.logger import get_logger
from src.plugin_system.apis import config_api
if TYPE_CHECKING:
from ...plugin import NapcatAdapter
logger = get_logger("napcat_adapter.message_handler")
class MessageHandler:
"""处理来自 Napcat 的消息事件"""
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
self.plugin_config: Optional[Dict[str, Any]] = None
def set_plugin_config(self, config: Dict[str, Any]) -> None:
"""设置插件配置"""
self.plugin_config = config
async def handle_raw_message(self, raw: Dict[str, Any]):
"""
处理原始消息并转换为 MessageEnvelope
Args:
raw: OneBot 原始消息数据
Returns:
MessageEnvelope (dict)
"""
from mofox_bus import MessageEnvelope, SegPayload, MessageInfoPayload, UserInfoPayload, GroupInfoPayload
message_type = raw.get("message_type")
message_id = str(raw.get("message_id", ""))
message_time = time.time()
# 构造用户信息
sender_info = raw.get("sender", {})
user_info: UserInfoPayload = {
"platform": "qq",
"user_id": str(sender_info.get("user_id", "")),
"user_nickname": sender_info.get("nickname", ""),
"user_cardname": sender_info.get("card", ""),
"user_avatar": sender_info.get("avatar", ""),
}
# 构造群组信息(如果是群消息)
group_info: Optional[GroupInfoPayload] = None
if message_type == "group":
group_id = raw.get("group_id")
if group_id:
group_info = {
"platform": "qq",
"group_id": str(group_id),
"group_name": "", # 可以通过 API 获取
}
# 解析消息段
message_segments = raw.get("message", [])
seg_list: List[SegPayload] = []
for seg in message_segments:
seg_type = seg.get("type", "")
seg_data = seg.get("data", {})
# 转换为 SegPayload
if seg_type == "text":
seg_list.append({
"type": "text",
"data": seg_data.get("text", "")
})
elif seg_type == "image":
# 这里需要下载图片并转换为 base64简化版本
seg_list.append({
"type": "image",
"data": seg_data.get("url", "") # 实际应该转换为 base64
})
elif seg_type == "at":
seg_list.append({
"type": "at",
"data": f"{seg_data.get('qq', '')}"
})
# 其他消息类型...
# 构造 MessageInfoPayload
message_info = {
"platform": "qq",
"message_id": message_id,
"time": message_time,
"user_info": user_info,
"format_info": {
"content_format": ["text", "image"], # 根据实际消息类型设置
"accept_format": ["text", "image", "emoji", "voice"],
},
}
# 添加群组信息(如果存在)
if group_info:
message_info["group_info"] = group_info
# 构造 MessageEnvelope
envelope = {
"direction": "incoming",
"message_info": message_info,
"message_segment": {"type": "seglist", "data": seg_list} if len(seg_list) > 1 else (seg_list[0] if seg_list else {"type": "text", "data": ""}),
"raw_message": raw.get("raw_message", ""),
"platform": "qq",
"message_id": message_id,
"timestamp_ms": int(message_time * 1000),
}
return envelope

View File

@@ -0,0 +1,41 @@
"""元事件处理器"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Optional
from src.common.logger import get_logger
if TYPE_CHECKING:
from ...plugin import NapcatAdapter
logger = get_logger("napcat_adapter.meta_event_handler")
class MetaEventHandler:
"""处理 Napcat 元事件(心跳、生命周期)"""
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
self.plugin_config: Optional[Dict[str, Any]] = None
def set_plugin_config(self, config: Dict[str, Any]) -> None:
"""设置插件配置"""
self.plugin_config = config
async def handle_meta_event(self, raw: Dict[str, Any]):
"""处理元事件"""
# 简化版本:返回一个空的 MessageEnvelope
import time
import uuid
return {
"direction": "incoming",
"message_info": {
"platform": "qq",
"message_id": str(uuid.uuid4()),
"time": time.time(),
},
"message_segment": {"type": "text", "data": "[元事件]"},
"timestamp_ms": int(time.time() * 1000),
}

View File

@@ -0,0 +1,41 @@
"""通知事件处理器"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Optional
from src.common.logger import get_logger
if TYPE_CHECKING:
from ...plugin import NapcatAdapter
logger = get_logger("napcat_adapter.notice_handler")
class NoticeHandler:
"""处理 Napcat 通知事件(戳一戳、表情回复等)"""
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
self.plugin_config: Optional[Dict[str, Any]] = None
def set_plugin_config(self, config: Dict[str, Any]) -> None:
"""设置插件配置"""
self.plugin_config = config
async def handle_notice(self, raw: Dict[str, Any]):
"""处理通知事件"""
# 简化版本:返回一个空的 MessageEnvelope
import time
import uuid
return {
"direction": "incoming",
"message_info": {
"platform": "qq",
"message_id": str(uuid.uuid4()),
"time": time.time(),
},
"message_segment": {"type": "text", "data": "[通知事件]"},
"timestamp_ms": int(time.time() * 1000),
}

View File

@@ -0,0 +1 @@
"""发送方向处理器"""

View File

@@ -0,0 +1,77 @@
"""发送处理器 - 将 MessageEnvelope 转换并发送到 Napcat"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Optional
from src.common.logger import get_logger
if TYPE_CHECKING:
from ...plugin import NapcatAdapter
logger = get_logger("napcat_adapter.send_handler")
class SendHandler:
"""处理向 Napcat 发送消息"""
def __init__(self, adapter: "NapcatAdapter"):
self.adapter = adapter
self.plugin_config: Optional[Dict[str, Any]] = None
def set_plugin_config(self, config: Dict[str, Any]) -> None:
"""设置插件配置"""
self.plugin_config = config
async def handle_message(self, envelope) -> None:
"""
处理发送消息
将 MessageEnvelope 转换为 OneBot API 调用
"""
message_info = envelope.get("message_info", {})
message_segment = envelope.get("message_segment", {})
# 获取群组和用户信息
group_info = message_info.get("group_info")
user_info = message_info.get("user_info")
# 构造消息内容
message = self._convert_seg_to_onebot(message_segment)
# 发送消息
if group_info:
# 发送群消息
group_id = group_info.get("group_id")
if group_id:
await self.adapter.send_napcat_api("send_group_msg", {
"group_id": int(group_id),
"message": message,
})
elif user_info:
# 发送私聊消息
user_id = user_info.get("user_id")
if user_id:
await self.adapter.send_napcat_api("send_private_msg", {
"user_id": int(user_id),
"message": message,
})
def _convert_seg_to_onebot(self, seg: Dict[str, Any]) -> list:
"""将 SegPayload 转换为 OneBot 消息格式"""
seg_type = seg.get("type", "")
seg_data = seg.get("data", "")
if seg_type == "text":
return [{"type": "text", "data": {"text": seg_data}}]
elif seg_type == "image":
return [{"type": "image", "data": {"file": f"base64://{seg_data}"}}]
elif seg_type == "seglist":
# 递归处理列表
result = []
for sub_seg in seg_data:
result.extend(self._convert_seg_to_onebot(sub_seg))
return result
else:
# 默认作为文本
return [{"type": "text", "data": {"text": str(seg_data)}}]

View File

@@ -0,0 +1,350 @@
"""
按聊天流分配消费者的消息路由系统
核心思想:
- 为每个活跃的聊天流stream_id创建独立的消息队列和消费者协程
- 同一聊天流的消息由同一个 worker 处理,保证顺序性
- 不同聊天流的消息并发处理,提高吞吐量
- 动态管理流的生命周期,自动清理不活跃的流
"""
import asyncio
import time
from typing import Dict, Optional
from src.common.logger import get_logger
logger = get_logger("stream_router")
class StreamConsumer:
"""单个聊天流的消息消费者
维护独立的消息队列和处理协程
"""
def __init__(self, stream_id: str, queue_maxsize: int = 100):
self.stream_id = stream_id
self.queue: asyncio.Queue = asyncio.Queue(maxsize=queue_maxsize)
self.worker_task: Optional[asyncio.Task] = None
self.last_active_time = time.time()
self.is_running = False
# 性能统计
self.stats = {
"total_messages": 0,
"total_processing_time": 0.0,
"queue_overflow_count": 0,
}
async def start(self) -> None:
"""启动消费者"""
if not self.is_running:
self.is_running = True
self.worker_task = asyncio.create_task(self._process_loop())
logger.debug(f"Stream Consumer 启动: {self.stream_id}")
async def stop(self) -> None:
"""停止消费者"""
self.is_running = False
if self.worker_task:
self.worker_task.cancel()
try:
await self.worker_task
except asyncio.CancelledError:
pass
logger.debug(f"Stream Consumer 停止: {self.stream_id}")
async def enqueue(self, message: dict) -> None:
"""将消息加入队列"""
self.last_active_time = time.time()
try:
# 使用 put_nowait 避免阻塞路由器
self.queue.put_nowait(message)
except asyncio.QueueFull:
self.stats["queue_overflow_count"] += 1
logger.warning(
f"Stream {self.stream_id} 队列已满 "
f"({self.queue.qsize()}/{self.queue.maxsize})"
)
try:
self.queue.get_nowait()
self.queue.put_nowait(message)
logger.debug(f"Stream {self.stream_id} 丢弃最旧消息,添加新消息")
except asyncio.QueueEmpty:
pass
async def _process_loop(self) -> None:
"""消息处理循环"""
# 延迟导入,避免循环依赖
from .recv_handler.message_handler import message_handler
from .recv_handler.meta_event_handler import meta_event_handler
from .recv_handler.notice_handler import notice_handler
logger.info(f"Stream {self.stream_id} 处理循环启动")
try:
while self.is_running:
try:
# 等待消息1秒超时
message = await asyncio.wait_for(
self.queue.get(),
timeout=1.0
)
start_time = time.time()
# 处理消息
post_type = message.get("post_type")
if post_type == "message":
await message_handler.handle_raw_message(message)
elif post_type == "meta_event":
await meta_event_handler.handle_meta_event(message)
elif post_type == "notice":
await notice_handler.handle_notice(message)
else:
logger.warning(f"未知的 post_type: {post_type}")
processing_time = time.time() - start_time
# 更新统计
self.stats["total_messages"] += 1
self.stats["total_processing_time"] += processing_time
self.last_active_time = time.time()
self.queue.task_done()
# 性能监控每100条消息输出一次
if self.stats["total_messages"] % 100 == 0:
avg_time = self.stats["total_processing_time"] / self.stats["total_messages"]
logger.info(
f"Stream {self.stream_id[:30]}... 统计: "
f"消息数={self.stats['total_messages']}, "
f"平均耗时={avg_time:.3f}秒, "
f"队列长度={self.queue.qsize()}"
)
# 动态延迟:队列空时短暂休眠
if self.queue.qsize() == 0:
await asyncio.sleep(0.01)
except asyncio.TimeoutError:
# 超时是正常的,继续循环
continue
except asyncio.CancelledError:
logger.info(f"Stream {self.stream_id} 处理循环被取消")
break
except Exception as e:
logger.error(f"Stream {self.stream_id} 处理消息时出错: {e}", exc_info=True)
# 继续处理下一条消息
await asyncio.sleep(0.1)
finally:
logger.info(f"Stream {self.stream_id} 处理循环结束")
def get_stats(self) -> dict:
"""获取性能统计"""
avg_time = (
self.stats["total_processing_time"] / self.stats["total_messages"]
if self.stats["total_messages"] > 0
else 0
)
return {
"stream_id": self.stream_id,
"queue_size": self.queue.qsize(),
"total_messages": self.stats["total_messages"],
"avg_processing_time": avg_time,
"queue_overflow_count": self.stats["queue_overflow_count"],
"last_active_time": self.last_active_time,
}
class StreamRouter:
"""流路由器
负责将消息路由到对应的聊天流队列
动态管理聊天流的生命周期
"""
def __init__(
self,
max_streams: int = 500,
stream_timeout: int = 600,
stream_queue_size: int = 100,
cleanup_interval: int = 60,
):
self.streams: Dict[str, StreamConsumer] = {}
self.lock = asyncio.Lock()
self.max_streams = max_streams
self.stream_timeout = stream_timeout
self.stream_queue_size = stream_queue_size
self.cleanup_interval = cleanup_interval
self.cleanup_task: Optional[asyncio.Task] = None
self.is_running = False
async def start(self) -> None:
"""启动路由器"""
if not self.is_running:
self.is_running = True
self.cleanup_task = asyncio.create_task(self._cleanup_loop())
logger.info(
f"StreamRouter 已启动 - "
f"最大流数: {self.max_streams}, "
f"超时: {self.stream_timeout}秒, "
f"队列大小: {self.stream_queue_size}"
)
async def stop(self) -> None:
"""停止路由器"""
self.is_running = False
if self.cleanup_task:
self.cleanup_task.cancel()
try:
await self.cleanup_task
except asyncio.CancelledError:
pass
# 停止所有流消费者
logger.info(f"正在停止 {len(self.streams)} 个流消费者...")
for consumer in self.streams.values():
await consumer.stop()
self.streams.clear()
logger.info("StreamRouter 已停止")
async def route_message(self, message: dict) -> None:
"""路由消息到对应的流"""
stream_id = self._extract_stream_id(message)
# 快速路径:流已存在
if stream_id in self.streams:
await self.streams[stream_id].enqueue(message)
return
# 慢路径:需要创建新流
async with self.lock:
# 双重检查
if stream_id not in self.streams:
# 检查流数量限制
if len(self.streams) >= self.max_streams:
logger.warning(
f"达到最大流数量限制 ({self.max_streams})"
f"尝试清理不活跃的流..."
)
await self._cleanup_inactive_streams()
# 清理后仍然超限,记录警告但继续创建
if len(self.streams) >= self.max_streams:
logger.error(
f"清理后仍达到最大流数量 ({len(self.streams)}/{self.max_streams})"
)
# 创建新流
consumer = StreamConsumer(stream_id, self.stream_queue_size)
self.streams[stream_id] = consumer
await consumer.start()
logger.info(f"创建新的 Stream Consumer: {stream_id} (总流数: {len(self.streams)})")
await self.streams[stream_id].enqueue(message)
def _extract_stream_id(self, message: dict) -> str:
"""从消息中提取 stream_id
返回格式: platform:id:type
例如: qq:123456:group 或 qq:789012:private
"""
post_type = message.get("post_type")
# 非消息类型,使用默认流(避免创建过多流)
if post_type not in ["message", "notice"]:
return "system:meta_event"
# 消息类型
if post_type == "message":
message_type = message.get("message_type")
if message_type == "group":
group_id = message.get("group_id")
return f"qq:{group_id}:group"
elif message_type == "private":
user_id = message.get("user_id")
return f"qq:{user_id}:private"
# notice 类型
elif post_type == "notice":
group_id = message.get("group_id")
if group_id:
return f"qq:{group_id}:group"
user_id = message.get("user_id")
if user_id:
return f"qq:{user_id}:private"
# 未知类型,使用通用流
return "unknown:unknown"
async def _cleanup_inactive_streams(self) -> None:
"""清理不活跃的流"""
current_time = time.time()
to_remove = []
for stream_id, consumer in self.streams.items():
if current_time - consumer.last_active_time > self.stream_timeout:
to_remove.append(stream_id)
for stream_id in to_remove:
await self.streams[stream_id].stop()
del self.streams[stream_id]
logger.debug(f"清理不活跃的流: {stream_id}")
if to_remove:
logger.info(
f"清理了 {len(to_remove)} 个不活跃的流 "
f"(当前活跃流: {len(self.streams)}/{self.max_streams})"
)
async def _cleanup_loop(self) -> None:
"""定期清理循环"""
logger.info(f"清理循环已启动,间隔: {self.cleanup_interval}")
try:
while self.is_running:
await asyncio.sleep(self.cleanup_interval)
await self._cleanup_inactive_streams()
except asyncio.CancelledError:
logger.info("清理循环已停止")
def get_all_stats(self) -> list[dict]:
"""获取所有流的统计信息"""
return [consumer.get_stats() for consumer in self.streams.values()]
def get_summary(self) -> dict:
"""获取路由器摘要"""
total_messages = sum(c.stats["total_messages"] for c in self.streams.values())
total_queue_size = sum(c.queue.qsize() for c in self.streams.values())
total_overflows = sum(c.stats["queue_overflow_count"] for c in self.streams.values())
# 计算平均队列长度
avg_queue_size = total_queue_size / len(self.streams) if self.streams else 0
# 找出最繁忙的流
busiest_stream = None
if self.streams:
busiest_stream = max(
self.streams.values(),
key=lambda c: c.stats["total_messages"]
).stream_id
return {
"total_streams": len(self.streams),
"max_streams": self.max_streams,
"total_messages_processed": total_messages,
"total_queue_size": total_queue_size,
"avg_queue_size": avg_queue_size,
"total_queue_overflows": total_overflows,
"busiest_stream": busiest_stream,
}
# 全局路由器实例
stream_router = StreamRouter()

View File

@@ -236,8 +236,6 @@ class NapcatAdapterPlugin(BasePlugin):
def enable_plugin(self) -> bool:
"""通过配置文件动态控制插件启用状态"""
# 如果已经通过配置加载了状态,使用配置中的值
if hasattr(self, "_is_enabled"):
return self._is_enabled
# 否则使用默认值(禁用状态)
return False