Initial commit: SenSu Alpha 0.2.0

- 13-service async plugin framework
- Textual TUI with CLI fallback
- Plugin hot-reload + permission system
- Web management panel (aiohttp)
- Bridge-based inter-module communication
- 10 regression tests

Fixes applied:
- PBKDF2-SHA256 auth (was plain SHA256)
- Auth bypass removed (was allow-all on fail)
- Bare excepts replaced with logged errors
- CatFramework/DreamSu -> SenSu naming unified
- ServiceManager: health checks + startup_order
- Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
2026-06-10 12:27:14 +08:00
commit e6875f0b4b
78 changed files with 14843 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
桥接模块 - 提供模块间和插件间的通信功能
"""
from .core_bridge import CoreBridge, MessageType
from .plugin_bridge import PluginBridge, PluginMessageType
__all__ = [
'CoreBridge',
'MessageType',
'PluginBridge',
'PluginMessageType'
]
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Callable, Any
from enum import Enum
import json
logger = logging.getLogger(__name__)
class MessageType(Enum):
"""消息类型枚举"""
EVENT = "event"
COMMAND = "command"
DATA = "data"
STATUS = "status"
ERROR = "error"
class CoreBridge:
"""核心桥接服务 - 用于框架模块间通信"""
def __init__(self):
self.subscribers: Dict[str, List[Callable]] = {}
self.message_queue: asyncio.Queue = asyncio.Queue()
self.is_running = False
self.processing_task = None
logger.debug("CoreBridge初始化开始")
async def start(self):
"""启动桥接服务"""
try:
logger.info("启动核心桥接服务")
self.is_running = True
self.processing_task = asyncio.create_task(self._process_messages())
logger.debug("核心桥接服务启动完成")
except Exception as e:
logger.error(f"启动核心桥接服务时出错: {str(e)}", exc_info=True)
raise
def subscribe(self, topic: str, callback: Callable):
"""订阅主题"""
try:
if topic not in self.subscribers:
self.subscribers[topic] = []
self.subscribers[topic].append(callback)
logger.debug(f"订阅主题: {topic}, 当前订阅者数: {len(self.subscribers[topic])}")
except Exception as e:
logger.error(f"订阅主题 {topic} 时出错: {str(e)}", exc_info=True)
raise
def unsubscribe(self, topic: str, callback: Callable):
"""取消订阅"""
try:
if topic in self.subscribers and callback in self.subscribers[topic]:
self.subscribers[topic].remove(callback)
logger.debug(f"取消订阅主题: {topic}, 剩余订阅者数: {len(self.subscribers[topic])}")
except Exception as e:
logger.error(f"取消订阅主题 {topic} 时出错: {str(e)}", exc_info=True)
async def publish(self, topic: str, message: Dict, msg_type: MessageType = MessageType.DATA):
"""发布消息"""
try:
message_data = {
"topic": topic,
"type": msg_type.value,
"data": message,
"timestamp": asyncio.get_event_loop().time()
}
await self.message_queue.put(message_data)
logger.debug(f"发布消息到主题: {topic}, 类型: {msg_type.value}")
except Exception as e:
logger.error(f"发布消息到主题 {topic} 时出错: {str(e)}", exc_info=True)
raise
async def _process_messages(self):
"""处理消息队列"""
try:
logger.debug("开始处理消息队列")
while self.is_running:
try:
# 等待消息,带超时以便检查运行状态
message = await asyncio.wait_for(self.message_queue.get(), timeout=1.0)
# 分发消息给订阅者
await self._dispatch_message(message)
# 标记任务完成
self.message_queue.task_done()
except asyncio.TimeoutError:
continue
except Exception as e:
logger.error(f"处理消息时出错: {str(e)}", exc_info=True)
continue
logger.debug("消息队列处理结束")
except Exception as e:
logger.error(f"消息队列处理循环出错: {str(e)}", exc_info=True)
async def _dispatch_message(self, message: Dict):
"""分发消息给订阅者"""
try:
topic = message["topic"]
if topic not in self.subscribers:
logger.debug(f"主题 {topic} 没有订阅者")
return
subscribers = self.subscribers[topic][:] # 复制列表避免在迭代时修改
# 并行调用所有订阅者
tasks = []
for callback in subscribers:
task = asyncio.create_task(self._call_subscriber(callback, message))
tasks.append(task)
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
logger.debug(f"消息分发完成,主题: {topic}, 订阅者数: {len(subscribers)}")
except Exception as e:
logger.error(f"分发消息时出错: {str(e)}", exc_info=True)
async def _call_subscriber(self, callback: Callable, message: Dict):
"""调用订阅者回调"""
try:
if asyncio.iscoroutinefunction(callback):
await callback(message)
else:
callback(message)
except Exception as e:
logger.error(f"调用订阅者回调时出错: {str(e)}", exc_info=True)
def get_subscriber_count(self, topic: str = None) -> int:
"""获取订阅者数量"""
try:
if topic:
count = len(self.subscribers.get(topic, []))
logger.debug(f"主题 {topic} 的订阅者数量: {count}")
return count
else:
total = sum(len(subs) for subs in self.subscribers.values())
logger.debug(f"总订阅者数量: {total}")
return total
except Exception as e:
logger.error(f"获取订阅者数量时出错: {str(e)}", exc_info=True)
return 0
async def shutdown(self):
"""关闭桥接服务"""
try:
logger.info("关闭核心桥接服务")
self.is_running = False
# 等待处理任务结束
if self.processing_task:
await asyncio.wait_for(self.processing_task, timeout=5.0)
# 清空队列和订阅者
self.subscribers.clear()
while not self.message_queue.empty():
try:
self.message_queue.get_nowait()
self.message_queue.task_done()
except asyncio.QueueEmpty:
break
logger.debug("核心桥接服务关闭完成")
except Exception as e:
logger.error(f"关闭核心桥接服务时出错: {str(e)}", exc_info=True)
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Callable, Any
from enum import Enum
import json
logger = logging.getLogger(__name__)
class PluginMessageType(Enum):
"""插件消息类型枚举"""
PLUGIN_EVENT = "plugin_event"
PLUGIN_DATA = "plugin_data"
PLUGIN_COMMAND = "plugin_command"
PLUGIN_REQUEST = "plugin_request"
PLUGIN_RESPONSE = "plugin_response"
class PluginBridge:
"""插件桥接服务 - 用于框架与插件、插件间通信"""
def __init__(self, core_bridge):
self.core_bridge = core_bridge
self.plugin_subscribers: Dict[str, Dict[str, List[Callable]]] = {}
self.plugin_message_queue: asyncio.Queue = asyncio.Queue()
self.is_running = False
self.processing_task = None
logger.debug("PluginBridge初始化开始")
async def start(self):
"""启动插件桥接服务"""
try:
logger.info("启动插件桥接服务")
self.is_running = True
self.processing_task = asyncio.create_task(self._process_plugin_messages())
# 订阅核心桥接的相关主题
self.core_bridge.subscribe("plugin.*", self._handle_core_plugin_message)
logger.debug("插件桥接服务启动完成")
except Exception as e:
logger.error(f"启动插件桥接服务时出错: {str(e)}", exc_info=True)
raise
def subscribe_plugin(self, plugin_name: str, topic: str, callback: Callable):
"""插件订阅主题"""
try:
if plugin_name not in self.plugin_subscribers:
self.plugin_subscribers[plugin_name] = {}
if topic not in self.plugin_subscribers[plugin_name]:
self.plugin_subscribers[plugin_name][topic] = []
self.plugin_subscribers[plugin_name][topic].append(callback)
logger.debug(f"插件 {plugin_name} 订阅主题: {topic}, 订阅者数: {len(self.plugin_subscribers[plugin_name][topic])}")
except Exception as e:
logger.error(f"插件订阅主题时出错: {str(e)}", exc_info=True)
raise
def unsubscribe_plugin(self, plugin_name: str, topic: str, callback: Callable):
"""插件取消订阅"""
try:
if (plugin_name in self.plugin_subscribers and
topic in self.plugin_subscribers[plugin_name] and
callback in self.plugin_subscribers[plugin_name][topic]):
self.plugin_subscribers[plugin_name][topic].remove(callback)
logger.debug(f"插件 {plugin_name} 取消订阅主题: {topic}")
except Exception as e:
logger.error(f"插件取消订阅时出错: {str(e)}", exc_info=True)
async def publish_to_plugin(self, target_plugin: str, topic: str, message: Dict,
msg_type: PluginMessageType = PluginMessageType.PLUGIN_DATA):
"""发布消息到指定插件"""
try:
message_data = {
"target_plugin": target_plugin,
"topic": topic,
"type": msg_type.value,
"data": message,
"timestamp": asyncio.get_event_loop().time()
}
await self.plugin_message_queue.put(message_data)
logger.debug(f"发布消息到插件 {target_plugin}, 主题: {topic}")
except Exception as e:
logger.error(f"发布消息到插件时出错: {str(e)}", exc_info=True)
raise
async def broadcast_to_plugins(self, topic: str, message: Dict,
exclude_plugins: List[str] = None,
msg_type: PluginMessageType = PluginMessageType.PLUGIN_DATA):
"""广播消息到所有插件"""
try:
exclude_plugins = exclude_plugins or []
for plugin_name in self.plugin_subscribers.keys():
if plugin_name not in exclude_plugins:
await self.publish_to_plugin(plugin_name, topic, message, msg_type)
logger.debug(f"广播消息到插件, 主题: {topic}, 排除: {exclude_plugins}")
except Exception as e:
logger.error(f"广播消息到插件时出错: {str(e)}", exc_info=True)
raise
async def _process_plugin_messages(self):
"""处理插件消息队列"""
try:
logger.debug("开始处理插件消息队列")
while self.is_running:
try:
# 等待消息,带超时
message = await asyncio.wait_for(self.plugin_message_queue.get(), timeout=1.0)
# 分发消息给目标插件
await self._dispatch_plugin_message(message)
# 标记任务完成
self.plugin_message_queue.task_done()
except asyncio.TimeoutError:
continue
except Exception as e:
logger.error(f"处理插件消息时出错: {str(e)}", exc_info=True)
continue
logger.debug("插件消息队列处理结束")
except Exception as e:
logger.error(f"插件消息队列处理循环出错: {str(e)}", exc_info=True)
async def _dispatch_plugin_message(self, message: Dict):
"""分发消息给插件订阅者"""
try:
target_plugin = message["target_plugin"]
topic = message["topic"]
if (target_plugin not in self.plugin_subscribers or
topic not in self.plugin_subscribers[target_plugin]):
logger.debug(f"插件 {target_plugin} 没有订阅主题 {topic}")
return
subscribers = self.plugin_subscribers[target_plugin][topic][:]
# 并行调用所有订阅者
tasks = []
for callback in subscribers:
task = asyncio.create_task(self._call_plugin_subscriber(callback, message))
tasks.append(task)
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
logger.debug(f"插件消息分发完成,目标: {target_plugin}, 主题: {topic}, 订阅者数: {len(subscribers)}")
except Exception as e:
logger.error(f"分发插件消息时出错: {str(e)}", exc_info=True)
async def _call_plugin_subscriber(self, callback: Callable, message: Dict):
"""调用插件订阅者回调"""
try:
if asyncio.iscoroutinefunction(callback):
await callback(message)
else:
callback(message)
except Exception as e:
logger.error(f"调用插件订阅者回调时出错: {str(e)}", exc_info=True)
async def _handle_core_plugin_message(self, message: Dict):
"""处理来自核心桥接的插件相关消息"""
try:
topic = message["topic"]
data = message["data"]
# 根据主题类型处理
if topic.startswith("plugin.event."):
# 广播插件事件
event_type = topic.replace("plugin.event.", "")
await self.broadcast_to_plugins(
f"event.{event_type}",
data,
msg_type=PluginMessageType.PLUGIN_EVENT
)
elif topic.startswith("plugin.broadcast."):
# 广播消息
broadcast_topic = topic.replace("plugin.broadcast.", "")
await self.broadcast_to_plugins(
broadcast_topic,
data,
msg_type=PluginMessageType.PLUGIN_DATA
)
logger.debug(f"处理核心插件消息: {topic}")
except Exception as e:
logger.error(f"处理核心插件消息时出错: {str(e)}", exc_info=True)
def get_plugin_subscriber_count(self, plugin_name: str = None) -> int:
"""获取插件订阅者数量"""
try:
if plugin_name:
if plugin_name not in self.plugin_subscribers:
return 0
total = sum(len(subs) for subs in self.plugin_subscribers[plugin_name].values())
logger.debug(f"插件 {plugin_name} 的订阅者数量: {total}")
return total
else:
total = 0
for plugin_subs in self.plugin_subscribers.values():
total += sum(len(subs) for subs in plugin_subs.values())
logger.debug(f"总插件订阅者数量: {total}")
return total
except Exception as e:
logger.error(f"获取插件订阅者数量时出错: {str(e)}", exc_info=True)
return 0
def cleanup_plugin_subscriptions(self, plugin_name: str):
"""清理插件的所有订阅"""
try:
if plugin_name in self.plugin_subscribers:
del self.plugin_subscribers[plugin_name]
logger.debug(f"清理插件订阅: {plugin_name}")
except Exception as e:
logger.error(f"清理插件订阅时出错: {str(e)}", exc_info=True)
async def shutdown(self):
"""关闭插件桥接服务"""
try:
logger.info("关闭插件桥接服务")
self.is_running = False
# 等待处理任务结束
if self.processing_task:
await asyncio.wait_for(self.processing_task, timeout=5.0)
# 清理所有订阅
self.plugin_subscribers.clear()
# 清空队列
while not self.plugin_message_queue.empty():
try:
self.plugin_message_queue.get_nowait()
self.plugin_message_queue.task_done()
except asyncio.QueueEmpty:
break
logger.debug("插件桥接服务关闭完成")
except Exception as e:
logger.error(f"关闭插件桥接服务时出错: {str(e)}", exc_info=True)
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Callable, Any
import json
logger = logging.getLogger(__name__)
class PluginNetworkBridge:
"""插件网络桥接 - 简化插件的网络交互"""
def __init__(self, plugin_name: str, internet_service, plugin_bridge):
self.plugin_name = plugin_name
self.internet_service = internet_service
self.plugin_bridge = plugin_bridge
self.registered_routes: List[Dict] = []
self.websocket_handlers: List[Dict] = []
logger.debug(f"插件网络桥接初始化: {plugin_name}")
def is_network_available(self):
"""检查网络服务是否可用"""
return self.internet_service is not None and hasattr(self.internet_service, 'register_plugin_route')
async def register_http_route(self, route_path: str, handler: Callable,
methods: List[str] = ["GET"], require_auth: bool = True):
"""注册HTTP路由"""
try:
if not self.is_network_available():
logger.warning("网络服务不可用,跳过HTTP路由注册")
return
await self.internet_service.register_plugin_route(
self.plugin_name, route_path, handler, methods, require_auth
)
self.registered_routes.append({
'type': 'http',
'path': route_path,
'methods': methods,
'require_auth': require_auth
})
logger.debug(f"插件 {self.plugin_name} 注册HTTP路由: {route_path}")
except Exception as e:
logger.error(f"注册HTTP路由时出错: {str(e)}")
# 不抛出异常,让插件继续运行
async def register_websocket(self, ws_path: str, handler: Callable, require_auth: bool = True):
"""注册WebSocket处理器"""
try:
if not self.is_network_available():
logger.warning("网络服务不可用,跳过WebSocket注册")
return
await self.internet_service.register_plugin_websocket(
self.plugin_name, ws_path, handler, require_auth
)
self.websocket_handlers.append({
'path': ws_path,
'require_auth': require_auth
})
logger.debug(f"插件 {self.plugin_name} 注册WebSocket: {ws_path}")
except Exception as e:
logger.error(f"注册WebSocket时出错: {str(e)}")
# 不抛出异常,让插件继续运行
async def broadcast_websocket(self, message: Dict):
"""向插件的所有WebSocket连接广播消息"""
try:
if not self.internet_service:
logger.warning("网络服务不可用,无法广播消息")
return
await self.internet_service.broadcast_to_websockets(self.plugin_name, message)
logger.debug(f"插件 {self.plugin_name} WebSocket广播: {len(message)} 字节")
except Exception as e:
logger.error(f"WebSocket广播时出错: {str(e)}")
async def send_data_to_client(self, client_id: str, message: Dict):
"""向特定客户端发送数据"""
try:
# 这里可以实现更精确的客户端消息发送
# 目前先使用广播
message['target_client'] = client_id
await self.broadcast_websocket(message)
except Exception as e:
logger.error(f"发送数据到客户端时出错: {str(e)}")
def get_network_info(self) -> Dict[str, Any]:
"""获取网络配置信息"""
if not self.internet_service:
return {
'plugin_name': self.plugin_name,
'registered_routes': [],
'websocket_handlers': [],
'base_url': '网络服务不可用'
}
return {
'plugin_name': self.plugin_name,
'registered_routes': self.registered_routes,
'websocket_handlers': self.websocket_handlers,
'base_url': f"http://{self.internet_service.http_host}:{self.internet_service.http_port}/{self.plugin_name}"
}
async def setup_data_transfer(self, data_handler: Callable):
"""设置跨端数据传输"""
try:
# 订阅网络数据接收事件
self.plugin_bridge.subscribe_plugin(
self.plugin_name,
"network.data.receive",
data_handler
)
logger.debug(f"插件 {self.plugin_name} 设置跨端数据传输")
except Exception as e:
logger.error(f"设置数据传输时出错: {str(e)}")