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
+12
View File
@@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
logs/
*.log
_patches_applied/
*.bak.*
.env
*.swp
*.swo
*~
.DS_Store
+145
View File
@@ -0,0 +1,145 @@
# 🐱 SenSu
一个功能强大的Python后端框架,具有插件化架构和丰富的功能集。
## ✨ 特性
- 🎨 **TUI界面**: 基于Textual的终端用户界面
- 📝 **强大日志系统**: 多输出、文件切割、实时日志流
- 🔌 **插件化架构**: 热加载、权限管理、插件隔离
- 🌐 **网络服务**: WebSocket、HTTP API、反向代理
- 🔐 **认证系统**: 用户认证、令牌管理、权限验证
- 🔄 **消息桥接**: 模块间通信、插件间通信
-**高性能**: 异步架构、协程支持
- 🛡️ **安全**: 权限验证、输入验证、错误隔离
## 🚀 快速开始
### 安装依赖
```bash
pip install -r requirements.txt
```
### 运行框架
```bash
python main.py
```
### 基本命令
在TUI底部输入框中输入命令:
- `help` - 显示帮助信息
- `status` - 显示框架状态
- `history` - 显示命令历史
## 📁 项目结构
```
project_root/
├── main.py # 框架主入口
├── service_manager.py # 服务管理器
├── requirements.txt # 依赖包列表
├── README.md # 项目说明
├── config/ # 运行时生成的配置文件
│ ├── framework/ # 框架核心配置
│ ├── plugins/ # 插件配置
│ ├── services/ # 服务配置
│ └── permissions/ # 权限配置
├── logs/ # 日志文件目录
│ ├── debug/ # debug级别日志
│ └── runtime/ # 运行时日志
├── services/ # 核心服务模块
│ ├── init_service.py # 初始化服务
│ ├── log_service.py # 日志服务
│ ├── tui_service.py # TUI服务
│ ├── command_service.py # 指令服务
│ ├── auth_service.py # 认证服务
│ ├── internet_service.py # 互联网服务
│ ├── plugin_service.py # 插件服务
│ ├── permission_service.py # 权限服务
│ ├── api_service.py # API服务
│ └── shutdown_service.py # 关闭服务
├── bridges/ # 桥接模块
│ ├── core_bridge.py # 核心桥接
│ └── plugin_bridge.py # 插件桥接
├── fmfuncs/ # 框架功能集
│ ├── file_utils.py # 文件操作工具
│ ├── config_utils.py # 配置工具
│ ├── validation_utils.py # 验证工具
│ ├── network_utils.py # 网络工具
│ └── plugin_utils.py # 插件工具
├── plugins/ # 插件目录
│ └── example_plugin/ # 示例插件
└── gui/ # GUI接口
└── api.py # GUI操作接口
```
## 🔌 插件开发
### 创建插件
1.`plugins/` 目录下创建插件文件夹
2. 创建必要的配置文件:
- `__init__.py` - 插件主模块
- `config.yaml` - 插件配置
- `permissions.yaml` - 权限申请
### 插件示例
参考 `plugins/example_plugin/` 目录中的示例插件。
## 🔧 配置说明
框架配置位于 `config/framework/` 目录:
- `base_config.yaml` - 基础框架配置
- `permission_rules.yaml` - 权限规则配置
## 📡 API接口
框架提供以下API接口:
- WebSocket服务: `ws://localhost:8765`
- HTTP API服务: `http://localhost:8000`
- GUI API服务: `http://localhost:8080`
## 🐛 问题排查
查看 `logs/` 目录中的日志文件获取详细错误信息。
## 📄 许可证
MIT License
## 🤝 贡献
欢迎提交Issue和Pull Request
```
这个完整的Python后端框架包含了这些功能:
- ✅ TUI渲染界面(三部分布局)
- ✅ 强大的日志处理模块
- ✅ 初始化系统和指令模块
- ✅ 核心桥接和插件桥接
- ✅ 互联网模块集(WebSocket、HTTP API
- ✅ 插件管理器(热加载、错误隔离)
- ✅ 权限管理器(权限申请和验证)
- ✅ API管理器
- ✅ 优雅的关闭方法
- ✅ 丰富的debug日志
- ✅ GUI API接口
- ✅ 清晰的目录结构
每个文件都有完整的错误处理和详细的日志记录 可以直接运行 `python main.py` 来启动框架
```
+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)}")
+45
View File
@@ -0,0 +1,45 @@
framework:
debug: true
name: SenSu
version: Alpha_0.2.0
logging:
debug_level_file: true
level: DEBUG
max_file_size: 10MB
max_log_files: 20
plugins:
auto_load: true
hot_reload: true
max_retry_count: 3
# TUI配置
tui:
enabled: true
refresh_rate: 30
# TUI布局配置
layout:
grid_rows: "4fr 5fr 1fr" # 三行布局:日志区域、消息区域、输入区域的比例
# TUI样式配置
styles:
log_area: "border: solid green; overflow-y: auto;"
message_area: "border: solid yellow; overflow-y: auto;"
input_area: "border: solid red;"
# TUI日志显示配置
log_display:
max_lines: 200
# 互联网服务配置
internet:
websocket:
host: "0.0.0.0"
port: 4240
# 其他websocket配置...
http:
host: "0.0.0.0"
port: 4200
# 其他http配置...
panel:
entrance:
path: "/SenSu"
username: "admin"
password: "admin"
+15
View File
@@ -0,0 +1,15 @@
# 权限规则定义
permission_levels:
- "read"
- "write"
- "execute"
- "admin"
default_permissions:
- "framework.status.read"
- "plugin.self.info.read"
admin_permissions:
- "framework.*"
- "plugin.*"
- "service.*"
@@ -0,0 +1,9 @@
{
"example_plugin": [
"framework.event.subscribe",
"framework.command.execute",
"plugin.example.execute",
"plugin.example.read",
"plugin.example.write"
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"10c4eb6e": {
"plugin_name": "example_plugin",
"permissions": [],
"timestamp": 484215.183089762
},
"d6f52ecd": {
"plugin_name": "example_plugin",
"permissions": [
"plugin.example.read",
"plugin.example.write",
"plugin.example.execute",
"framework.event.subscribe",
"framework.command.execute"
],
"timestamp": 484729.685713733
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"example_plugin": "granted"
}
+96
View File
@@ -0,0 +1,96 @@
commands:
autoscroll:
description: '滚动控制: 切换自动滚动'
permissions:
- framework.tui.control
source: internal
chat_broadcast: &id001
description: 向所有聊天客户端广播消息
permissions:
- plugin.example.chat.broadcast
source: plugin.example_plugin
help:
description: 显示帮助信息
permissions:
- framework.command.help.read
source: internal
history:
description: 显示命令历史
permissions:
- framework.command.history.read
source: internal
netdiag:
description: 网络服务诊断
permissions:
- framework.network.diagnose
source: internal
network_info: &id002
description: 显示插件网络信息
permissions: []
source: plugin.example_plugin
permissions:
description: '权限管理: 显示权限状态'
permissions:
- framework.permission.read
source: internal
pm_plugin_status:
description: '权限管理: 查看插件权限状态'
permissions:
- framework.permission.read
source: internal
pmallow:
description: '权限管理: 同意权限请求'
permissions:
- framework.permission.read
source: internal
pmdeny:
description: '权限管理: 拒绝权限请求'
permissions:
- framework.permission.read
source: internal
pmhelp:
description: '权限管理: 显示权限命令帮助'
permissions:
- framework.permission.read
source: internal
pmignore:
description: '权限管理: 暂时忽略权限请求'
permissions:
- framework.permission.read
source: internal
pmpending:
description: '权限管理: 查看待授权请求列表'
permissions:
- framework.permission.read
source: internal
pmrequests:
description: '权限管理: 查看待授权请求列表(别名)'
permissions:
- framework.permission.read
source: internal
pmtest:
description: '权限管理: 测试权限配置文件'
permissions:
- framework.permission.read
source: internal
scroll:
description: '滚动控制: 手动滚动到底部'
permissions:
- framework.tui.control
source: internal
status:
description: 显示框架状态
permissions:
- framework.status.read
source: internal
testlog:
description: 生成测试日志
permissions:
- framework.command.test
source: internal
last_updated: 119133.773274654
plugin_commands:
example_plugin:
chat_broadcast: *id001
network_info: *id002
total_commands: 18
+17
View File
@@ -0,0 +1,17 @@
http_port: 8000
last_updated: 119133.817076582
plugin_routes:
example_plugin:
- methods:
- GET
path: /plugin/example_plugin/api/info
require_auth: false
- methods:
- POST
path: /plugin/example_plugin/api/echo
require_auth: true
- methods:
- WEBSOCKET
path: /plugin/example_plugin/ws/chat
require_auth: true
websocket_port: 8765
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import asyncio
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
async def debug_log_format():
"""调试日志格式"""
try:
from services.log_service import LogService
from services.init_service import InitService
print("🐱 调试日志格式...")
# 初始化配置
init_service = InitService()
configs = await init_service.initialize_framework()
base_config = configs['base']
# 创建日志服务
log_service = LogService(base_config)
# 等待日志服务初始化
await asyncio.sleep(1)
# 添加测试消费者
def test_consumer(log_record):
print("📝 消费者收到的日志记录:")
print(f" 所有键: {list(log_record.keys())}")
if 'simple_message' in log_record:
print(f" simple_message: {log_record['simple_message']}")
if 'formatted_message' in log_record:
print(f" formatted_message: {log_record['formatted_message']}")
if 'level' in log_record:
print(f" level: {log_record['level']}")
print("---")
log_service.add_log_consumer(test_consumer)
# 测试日志记录
import logging
logger = logging.getLogger("test_logger")
print("\n🔧 发送测试日志...")
logger.info("这是一条测试信息日志")
logger.debug("这是一条测试调试日志")
logger.warning("这是一条测试警告日志")
logger.error("这是一条测试错误日志")
# 等待日志处理完成
await asyncio.sleep(0.5)
except Exception as e:
print(f"❌ 调试失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(debug_log_format())
File diff suppressed because it is too large Load Diff
+398
View File
@@ -0,0 +1,398 @@
## 一、项目架构与设计思路
### 1.1 核心设计理念
SenSu框架采用了**服务化、插件化、事件驱动**的设计理念:
- **模块化服务架构**:每个功能都是一个独立服务(Service),通过ServiceManager统一管理
- **异步优先**:全面采用`asyncio`,支持高并发处理
- **插件隔离**:插件有独立的命名空间和权限控制
- **桥接通信**:通过消息桥接实现模块间解耦通信
### 1.2 框架启动流程
```
main.py → CatFramework.initialize() → 依次初始化12个核心服务
```
**启动顺序**
1. InitService(配置加载)
2. LogService(日志系统)
3. CoreBridge(核心消息桥接)
4. CommandService(命令系统)
5. AuthService(认证系统)
6. PluginBridge(插件桥接)
7. ShutdownService(优雅关闭)
8. TuiService(终端界面)
9. PermissionService(权限管理)
10. InternetService(网络服务)
11. PluginService(插件管理)
12. APIServiceAPI服务)
## 二、目录结构详细分析
### 2.1 核心目录说明
```
SenSu-Alpha0.2/
├── main.py # 框架主入口,定义CatFramework类
├── service_manager.py # 服务管理器(全局服务注册表)
├── requirements.txt # Python依赖包
├── README.md # 项目文档
├── bridges/ # 消息桥接系统
│ ├── __init__.py
│ ├── core_bridge.py # 核心模块间通信(发布-订阅模式)
│ ├── plugin_bridge.py # 插件间通信
│ └── plugin_network_bridge.py # 插件网络桥接
├── services/ # 核心服务模块(核心业务逻辑)
│ ├── init_service.py # 框架初始化
│ ├── log_service.py # 日志服务(多输出、文件切割)
│ ├── tui_service.py # TUI终端界面(基于Textual
│ ├── command_service.py # 命令处理系统
│ ├── auth_service.py # 认证系统
│ ├── internet_service.py # 网络服务(HTTP+WebSocket
│ ├── plugin_service.py # 插件管理器
│ ├── permission_service.py # 权限验证器
│ ├── api_service.py # API端点管理
│ └── shutdown_service.py # 优雅关闭
├── fmfuncs/ # 框架功能集(工具函数)
│ └── plugin_command_decorator.py # 插件命令装饰器
├── utils/ # 通用工具类
│ ├── file_utils.py # 文件操作
│ ├── config_utils.py # 配置管理
│ ├── validation_utils.py # 数据验证
│ ├── network_utils.py # 网络工具
│ └── plugin_utils.py # 插件工具
├── config/ # 运行时配置文件
│ ├── framework/ # 框架核心配置
│ ├── plugins/ # 插件配置
│ ├── services/ # 服务配置
│ └── permissions/ # 权限配置
├── plugins/ # 插件目录
│ └── example_plugin/ # 示例插件
└── gui/ # GUI接口(预留)
└── api.py # Web API接口
```
### 2.3 配置管理系统
框架使用**两级配置**
1. **Base Config** (`config/framework/base_config.yaml`): 框架基础配置
2. **Runtime Config**: 运行时动态生成的配置(保存在config目录)
## 三、核心模块深度解析
### 3.1 服务管理器(ServiceManager
**作用**:全局服务注册表,实现依赖注入
```python
# 注册服务
service_manager.register_service("log", log_service)
# 获取服务
log_service = service_manager.get_service("log")
```
### 3.2 桥接系统(Bridges
**核心设计**
- **CoreBridge**: 模块间通信,支持`MessageType`枚举
- **PluginBridge**: 插件间通信,支持`PluginMessageType`枚举
- **消息格式**topic + data + timestamp
**消息类型**
```python
class MessageType(Enum):
EVENT = "event" # 事件通知
COMMAND = "command" # 命令执行
DATA = "data" # 数据传输
STATUS = "status" # 状态更新
ERROR = "error" # 错误报告
```
### 3.3 日志系统(LogService
**特性**
- 支持控制台和文件双输出
- 按级别分离(runtime/debug
- 自动文件切割和清理
- 日志消费者模式(TUI实时显示)
- 彩色日志输出
**配置示例**
```yaml
logging:
level: DEBUG
debug_level_file: true
max_file_size: 10MB
max_log_files: 20
```
### 3.4 TUI界面(TuiService
**基于Textual框架的三栏布局**
1. **日志区域**4fr: 显示所有日志输出
2. **消息区域**5fr: 显示系统消息和命令结果
3. **输入区域**1fr: 命令输入框
**特性**
- 实时日志捕获和显示
- 命令自动补全(预留)
- 滚动控制(自动/手动)
- 彩色消息显示
### 3.5 网络服务(InternetService
**功能**
- HTTP服务器(aiohttp
- WebSocket服务器
- 插件路由自动注册
- 反向代理支持(预留)
**端口配置**
```yaml
internet:
websocket:
port: 8765
http:
port: 8000
```
### 3.6 插件系统(PluginService
**关键特性**
- 热加载/卸载
- 权限隔离
- 命令自动注册
- 错误隔离(插件崩溃不影响框架)
- 网络路由自动注册
## 四、插件开发详解
### 4.1 插件目录结构
```
plugins/
└── example_plugin/
├── __init__.py # 插件主类(必须包含Plugin类)
├── config.yaml # 插件配置
└── permissions.yaml # 权限申请
```
### 4.2 插件主类模板
```python
class Plugin:
def __init__(self, plugin_name: str, config: Dict, bridge):
self.plugin_name = plugin_name
self.config = config
self.bridge = bridge # PluginBridge实例
self.network_bridge = None # PluginNetworkBridge实例
async def initialize(self):
"""插件初始化"""
# 1. 创建网络桥接
self.network_bridge = PluginNetworkBridge(...)
# 2. 注册网络路由
await self.network_bridge.register_http_route(...)
await self.network_bridge.register_websocket(...)
# 3. 注册事件处理器
self.bridge.subscribe_plugin(...)
# 使用装饰器注册命令
@plugin_command(name="mycmd", description="我的命令")
async def my_command(self, *args):
return "命令执行结果"
async def shutdown(self):
"""插件关闭"""
# 清理资源
```
### 4.3 权限申请文件(permissions.yaml
```yaml
plugin_name: "example_plugin"
permissions:
- "plugin.example.read"
- "plugin.example.write"
- "plugin.example.execute"
- "framework.event.subscribe"
- "framework.command.execute"
```
### 4.4 插件配置文件(config.yaml
```yaml
name: "ExamplePlugin"
version: "1.0.0"
description: "插件描述"
author: "作者名"
settings:
enabled: true
auto_start: true
log_level: "INFO"
features:
# 插件特有配置
```
### 4.5 插件命令装饰器
框架提供了`@plugin_command`装饰器:
```python
from fmfuncs.plugin_command_decorator import plugin_command
@plugin_command(name="echo", description="回显消息")
async def cmd_echo(self, *args):
return " ".join(args)
# 简化版
@plugin_command()
async def hello(self, *args):
'''打招呼命令'''
return "Hello World!"
```
### 4.6 插件网络功能
**HTTP路由注册**
```python
await self.network_bridge.register_http_route(
"/api/info",
self._handle_api_info,
methods=["GET"],
require_auth=False
)
```
**WebSocket注册**
```python
await self.network_bridge.register_websocket(
"/chat",
self._handle_websocket_chat
)
```
## 五、命令系统详解
### 5.1 内置命令
框架提供丰富的内置命令:
- `help` - 显示帮助
- `status` - 框架状态
- `history` - 命令历史
- `testlog` - 测试日志生成
- `netdiag` - 网络诊断
- `permissions` - 权限管理
- `scroll` - 滚动控制
- `autoscroll` - 自动滚动开关
### 5.2 权限管理命令
框架提供完整的权限管理命令集(pm前缀):
- `pmallow` - 同意权限请求
- `pmdeny` - 拒绝权限请求
- `pmignore` - 忽略权限请求
- `permissions` - 显示权限状态
- `pmpending` - 查看待授权请求
- `pmhelp` - 权限命令帮助
### 5.3 命令注册机制
**插件命令注册流程**
1. PluginService扫描插件方法
2. 识别`@plugin_command`装饰器
3. 注册到CommandService
4. 命令格式:`命令名 [参数...]`
## 六、开发建议与最佳实践
### 6.1 插件开发建议
1. **错误处理**:插件内应妥善处理异常,避免影响框架
2. **资源管理**:在`shutdown`方法中清理所有资源
3. **异步安全**:确保异步方法正确处理取消和超时
4. **权限最小化**:只申请必要的权限
### 6.2 性能优化
1. **异步IO**:所有网络和文件操作使用异步版本
2. **连接池**:数据库/网络连接使用连接池
3. **缓存机制**:频繁读取的数据适当缓存
4. **懒加载**:大型资源按需加载
### 6.3 安全性考虑
1. **输入验证**:所有外部输入都应验证
2. **权限验证**:敏感操作前检查权限
3. **日志脱敏**:避免在日志中记录敏感信息
4. **API限流**:防止API被滥用
## 七、框架优势与特点
### 7.1 优势
1. **完整的生态**:日志、网络、UI、插件系统一应俱全
2. **良好的扩展性**:插件系统设计完善
3. **生产级质量**:完善的错误处理和日志记录
4. **开发者友好**:详细的文档和示例插件
### 7.2 适用场景
1. **后台管理工具**:需要终端界面的管理工具
2. **API网关**:插件化路由和认证
3. **自动化平台**:可扩展的任务调度和执行
4. **监控系统**:实时数据展示和告警
### 7.3 技术栈亮点
- **异步架构**asyncio全面应用
- **现代化UI**:基于Textual的TUI
- **微服务理念**:服务化模块设计
- **企业级特性**:权限、认证、日志一应俱全
## 八、后续发展建议
### 8.1 功能增强
1. **数据库支持**:添加ORM或数据库连接池
2. **任务队列**:集成Celery或类似系统
3. **监控指标**:集成Prometheus指标导出
4. **配置文件热重载**:支持运行时配置更新
### 8.2 易用性改进
1. **插件市场**:在线插件安装和管理
2. **配置生成器**:图形化配置界面
3. **调试工具**:集成调试和性能分析
4. **文档生成**:自动生成API文档
### 8.3 生态建设
1. **插件模板**:快速创建插件的脚手架
2. **测试框架**:插件测试工具
3. **CI/CD集成**:自动化测试和部署
4. **社区建设**:建立插件开发者社区
## 总结
SenSu框架是一个设计精良、功能完整的Python后端框架,具有以下核心价值:
1. **工程化设计**:服务化架构、完善的错误处理、详细的日志
2. **强大的插件系统**:支持热加载、权限隔离、网络路由自动注册
3. **现代化的用户体验**:基于Textual的TUI界面,美观实用
4. **企业级特性**:完整的权限管理、认证系统、网络服务
框架代码结构清晰,文档详细,适合作为:
- 企业级后台系统的基础框架
- 插件化应用的核心引擎
- 学习和研究现代Python框架设计的优秀案例
对于想要基于此框架进行开发的开发者,建议从`example_plugin`入手,逐步理解框架的各个组件,然后根据业务需求开发定制插件。
+44
View File
@@ -0,0 +1,44 @@
SenSu/
├── main.py # 框架主入口
├── requirements.txt # 依赖包列表
├── README.md # 项目说明
├── config/ # 运行时生成的配置文件
│ ├── framework/ # 框架核心配置
│ ├── plugins/ # 插件配置
│ ├── services/ # 服务配置
│ └── permissions/ # 权限配置
├── logs/ # 日志文件目录
│ ├── debug/ # debug级别日志
│ └── runtime/ # 运行时日志
├── fmfuncs/ # 框架功能集
│ ├── __init__.py
│ ├── tui_renderer.py # TUI渲染器
│ ├── log_handler.py # 日志处理模块
│ ├── init_system.py # 初始化系统
│ ├── command_handler.py # 指令处理模块
│ ├── bridge_core.py # 核心桥模块
│ ├── bridge_plugin.py # 插件桥模块
│ ├── internet_module.py # 互联网模块集
│ ├── auth_system.py # 访问验证系统
│ ├── plugin_manager.py # 插件管理器
│ ├── permission_validator.py # 权限验证器
│ ├── api_manager.py # API管理器
│ └── shutdown_handler.py # 终止处理器
├── plugins/ # 插件目录
│ └── example_plugin/ # 示例插件结构
│ ├── __init__.py
│ ├── permissions.yaml
│ └── config.yaml
├── gui/ # GUI接口目录
│ └── api.py # GUI操作接口
└── utils/ # 工具函数
├── __init__.py
├── file_utils.py # 文件操作工具
├── config_utils.py # 配置工具
└── validation_utils.py # 验证工具
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from typing import List, Callable, Optional
from functools import wraps
logger = logging.getLogger(__name__)
def plugin_command(name: Optional[str] = None,
description: Optional[str] = None,
permissions: Optional[List[str]] = None):
"""
插件命令装饰器
用法:
@plugin_command(name="mycmd", description="我的命令", permissions=["read"])
async def my_command_handler(self, *args):
return "命令执行结果"
或者简化版:
@plugin_command()
async def mycmd(self, *args):
'''我的命令描述'''
return "命令执行结果"
"""
def decorator(func: Callable):
# 设置命令属性
func._is_plugin_command = True
func._command_name = name or func.__name__
# 优先使用装饰器参数,其次使用文档字符串,最后使用默认描述
if description:
func._command_description = description
elif func.__doc__:
# 提取文档字符串的第一行作为描述
doc_lines = [line.strip() for line in func.__doc__.split('\n') if line.strip()]
func._command_description = doc_lines[0] if doc_lines else f"命令: {func.__name__}"
else:
func._command_description = f"命令: {func.__name__}"
func._command_permissions = permissions or []
@wraps(func)
async def wrapper(self, *args, **kwargs):
"""包装器确保返回字符串结果并处理异常"""
try:
logger.debug(f"执行插件命令: {func._command_name}, 参数: {args}")
# 调用原始方法
result = await func(self, *args, **kwargs)
# 确保返回字符串
if result is None:
return "✅ 命令执行完成"
elif not isinstance(result, str):
return str(result)
else:
return result
except Exception as e:
logger.error(f"插件命令执行失败 {func._command_name}: {str(e)}", exc_info=True)
return f"❌ 命令执行错误: {str(e)}"
return wrapper
return decorator
def command(name: Optional[str] = None, description: Optional[str] = None):
"""简化版命令装饰器"""
return plugin_command(name=name, description=description)
# 同步命令装饰器(不推荐,但提供兼容性)
def sync_plugin_command(name: Optional[str] = None,
description: Optional[str] = None,
permissions: Optional[List[str]] = None):
"""同步插件命令装饰器"""
def decorator(func: Callable):
func._is_plugin_command = True
func._command_name = name or func.__name__
if description:
func._command_description = description
elif func.__doc__:
doc_lines = [line.strip() for line in func.__doc__.split('\n') if line.strip()]
func._command_description = doc_lines[0] if doc_lines else f"命令: {func.__name__}"
else:
func._command_description = f"命令: {func.__name__}"
func._command_permissions = permissions or []
@wraps(func)
def wrapper(self, *args, **kwargs):
"""同步命令包装器"""
try:
logger.debug(f"执行同步插件命令: {func._command_name}, 参数: {args}")
result = func(self, *args, **kwargs)
if result is None:
return "✅ 命令执行完成"
elif not isinstance(result, str):
return str(result)
else:
return result
except Exception as e:
logger.error(f"同步插件命令执行失败 {func._command_name}: {str(e)}", exc_info=True)
return f"❌ 命令执行错误: {str(e)}"
return wrapper
return decorator
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Any, Optional
from aiohttp import web
import json
logger = logging.getLogger(__name__)
class GUIAPI:
"""GUI API接口 - 为未来的GUI界面提供操作接口"""
def __init__(self, service_manager):
self.service_manager = service_manager
self.app = web.Application()
self.setup_routes()
logger.debug("GUIAPI初始化开始")
def setup_routes(self):
"""设置API路由"""
try:
# 框架状态接口
self.app.router.add_get('/api/framework/status', self.handle_framework_status)
self.app.router.add_get('/api/framework/health', self.handle_framework_health)
# 日志接口
self.app.router.add_get('/api/logs/recent', self.handle_recent_logs)
self.app.router.add_get('/api/logs/stream', self.handle_log_stream)
# 插件接口
self.app.router.add_get('/api/plugins', self.handle_plugins_list)
self.app.router.add_post('/api/plugins/{plugin_name}/reload', self.handle_plugin_reload)
self.app.router.add_post('/api/plugins/{plugin_name}/unload', self.handle_plugin_unload)
# 命令接口
self.app.router.add_post('/api/command', self.handle_command_execute)
self.app.router.add_get('/api/command/history', self.handle_command_history)
# 服务接口
self.app.router.add_get('/api/services', self.handle_services_list)
self.app.router.add_get('/api/services/{service_name}/status', self.handle_service_status)
logger.debug("GUI API路由设置完成")
except Exception as e:
logger.error(f"设置GUI API路由时出错: {str(e)}", exc_info=True)
raise
async def handle_framework_status(self, request):
"""处理框架状态请求"""
try:
logger.debug("处理框架状态API请求")
status_info = {
"framework": {
"name": "CatFramework",
"version": "1.0.0",
"status": "running",
"uptime": "0s" # 实际应该计算运行时间
},
"services": {
"total": 0,
"running": 0
},
"plugins": {
"total": 0,
"loaded": 0
}
}
# 获取服务状态
try:
service_manager = self.service_manager.get_service("service_manager")
if service_manager:
status_info["services"]["total"] = len(service_manager.services)
status_info["services"]["running"] = len(service_manager.services)
except:
pass
# 获取插件状态
try:
plugin_service = self.service_manager.get_service("plugin")
if plugin_service:
status_info["plugins"]["total"] = len(plugin_service.plugin_info)
status_info["plugins"]["loaded"] = len(plugin_service.plugins)
except:
pass
return web.json_response(status_info)
except Exception as e:
logger.error(f"处理框架状态请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_framework_health(self, request):
"""处理框架健康检查"""
try:
logger.debug("处理框架健康检查API请求")
health_data = {
"status": "healthy",
"timestamp": asyncio.get_event_loop().time(),
"components": {
"core_services": "healthy",
"plugins": "healthy",
"network": "healthy"
}
}
return web.json_response(health_data)
except Exception as e:
logger.error(f"处理框架健康检查时出错: {str(e)}", exc_info=True)
return web.json_response({"status": "unhealthy", "error": str(e)}, status=503)
async def handle_recent_logs(self, request):
"""处理最近日志请求"""
try:
logger.debug("处理最近日志API请求")
count = int(request.query.get('count', 50))
level = request.query.get('level', '')
log_service = self.service_manager.get_service("log")
if not log_service:
return web.json_response({"error": "Log service not available"}, status=503)
logs = log_service.get_recent_logs(count)
# 按级别过滤
if level:
logs = [log for log in logs if log['level'].lower() == level.lower()]
return web.json_response({"logs": logs, "count": len(logs)})
except Exception as e:
logger.error(f"处理最近日志请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_log_stream(self, request):
"""处理日志流请求(SSE"""
try:
logger.debug("处理日志流API请求")
response = web.StreamResponse()
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Cache-Control'] = 'no-cache'
response.headers['Connection'] = 'keep-alive'
await response.prepare(request)
# 这里应该实现真正的日志流
# 暂时发送测试数据
try:
while True:
test_log = {
"timestamp": asyncio.get_event_loop().time(),
"level": "INFO",
"message": "Log stream test message",
"source": "gui_api"
}
event_data = f"data: {json.dumps(test_log)}\n\n"
await response.write(event_data.encode('utf-8'))
await asyncio.sleep(5)
except asyncio.CancelledError:
logger.debug("日志流连接关闭")
finally:
await response.write_eof()
return response
except Exception as e:
logger.error(f"处理日志流请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_plugins_list(self, request):
"""处理插件列表请求"""
try:
logger.debug("处理插件列表API请求")
plugin_service = self.service_manager.get_service("plugin")
if not plugin_service:
return web.json_response({"error": "Plugin service not available"}, status=503)
plugins_info = []
for plugin_name, plugin_info in plugin_service.plugin_info.items():
plugins_info.append({
"name": plugin_info.name,
"version": plugin_info.version,
"description": plugin_info.description,
"author": plugin_info.author,
"enabled": plugin_info.enabled,
"loaded": plugin_info.loaded,
"error_count": plugin_info.error_count,
"permissions": plugin_info.permissions
})
return web.json_response({"plugins": plugins_info})
except Exception as e:
logger.error(f"处理插件列表请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_plugin_reload(self, request):
"""处理插件重载请求"""
try:
plugin_name = request.match_info['plugin_name']
logger.debug(f"处理插件重载API请求: {plugin_name}")
plugin_service = self.service_manager.get_service("plugin")
if not plugin_service:
return web.json_response({"error": "Plugin service not available"}, status=503)
# 先卸载再加载
unload_success = await plugin_service.unload_plugin(plugin_name)
if unload_success:
load_success = await plugin_service.load_plugin(plugin_name)
result = {"reloaded": load_success}
else:
result = {"reloaded": False, "error": "Unload failed"}
return web.json_response(result)
except Exception as e:
logger.error(f"处理插件重载请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_plugin_unload(self, request):
"""处理插件卸载请求"""
try:
plugin_name = request.match_info['plugin_name']
logger.debug(f"处理插件卸载API请求: {plugin_name}")
plugin_service = self.service_manager.get_service("plugin")
if not plugin_service:
return web.json_response({"error": "Plugin service not available"}, status=503)
success = await plugin_service.unload_plugin(plugin_name)
return web.json_response({"unloaded": success})
except Exception as e:
logger.error(f"处理插件卸载请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_command_execute(self, request):
"""处理命令执行请求"""
try:
data = await request.json()
command = data.get('command', '')
source = data.get('source', 'gui')
logger.debug(f"处理命令执行API请求: {command}")
command_service = self.service_manager.get_service("command")
if not command_service:
return web.json_response({"error": "Command service not available"}, status=503)
result = await command_service.process_command(command, source)
return web.json_response({
"command": command,
"result": str(result),
"success": True
})
except Exception as e:
logger.error(f"处理命令执行请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_command_history(self, request):
"""处理命令历史请求"""
try:
limit = int(request.query.get('limit', 10))
logger.debug(f"处理命令历史API请求,限制: {limit}")
command_service = self.service_manager.get_service("command")
if not command_service:
return web.json_response({"error": "Command service not available"}, status=503)
history = command_service.get_command_history(limit)
return web.json_response({"history": history})
except Exception as e:
logger.error(f"处理命令历史请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_services_list(self, request):
"""处理服务列表请求"""
try:
logger.debug("处理服务列表API请求")
service_manager = self.service_manager.get_service("service_manager")
if not service_manager:
return web.json_response({"error": "Service manager not available"}, status=503)
services_info = []
for name, service in service_manager.services.items():
services_info.append({
"name": name,
"type": type(service).__name__,
"status": "running" # 简化状态
})
return web.json_response({"services": services_info})
except Exception as e:
logger.error(f"处理服务列表请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_service_status(self, request):
"""处理服务状态请求"""
try:
service_name = request.match_info['service_name']
logger.debug(f"处理服务状态API请求: {service_name}")
try:
service = self.service_manager.get_service(service_name)
status_info = {
"name": service_name,
"available": True,
"status": "running"
}
# 可以添加特定服务的状态检查
if hasattr(service, 'is_running'):
status_info["status"] = "running" if service.is_running else "stopped"
return web.json_response(status_info)
except ValueError:
return web.json_response({"error": f"Service {service_name} not found"}, status=404)
except Exception as e:
logger.error(f"处理服务状态请求时出错: {str(e)}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def start(self, host: str = "localhost", port: int = 8080):
"""启动GUI API服务器"""
try:
logger.info(f"启动GUI API服务器: {host}:{port}")
runner = web.AppRunner(self.app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
logger.debug("GUI API服务器启动完成")
return runner
except Exception as e:
logger.error(f"启动GUI API服务器时出错: {str(e)}", exc_info=True)
raise
async def shutdown(self):
"""关闭GUI API"""
try:
logger.info("关闭GUI API")
# 清理资源
logger.debug("GUI API关闭完成")
except Exception as e:
logger.error(f"关闭GUI API时出错: {str(e)}", exc_info=True)
+433
View File
@@ -0,0 +1,433 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import signal
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from services.init_service import InitService
from services.log_service import LogService
from services.tui_service import TuiService
from services.command_service import CommandService
from services.auth_service import AuthService
from services.internet_service import InternetService
from services.plugin_service import PluginService
from services.permission_service import PermissionService
from services.api_service import APIService
from services.shutdown_service import ShutdownService
from services.web_panel.manager import WebPanelManager
from bridges.core_bridge import CoreBridge
from bridges.plugin_bridge import PluginBridge
from service_manager import ServiceManager
logger = logging.getLogger(__name__)
class SenSuFramework:
"""框架主类"""
def __init__(self):
self.service_manager = ServiceManager()
self.is_running = False
logger.debug("🐱 SenSu 框架初始化开始")
async def initialize(self):
"""初始化框架"""
try:
logger.info("🐱 SenSu 启动中...")
# 1. 初始化服务
init_service = InitService()
configs = await init_service.initialize_framework()
base_config = configs['base']
self.service_manager.register_service("init", init_service)
# 2. 日志服务
log_service = LogService(base_config)
self.service_manager.register_service("log", log_service)
# 3. 核心桥接服务
core_bridge = CoreBridge()
await core_bridge.start()
self.service_manager.register_service("core_bridge", core_bridge)
# 4. 指令服务
command_service = CommandService(self.service_manager)
command_service.register_builtin_commands()
self.service_manager.register_service("command", command_service)
# 5. 认证服务
logger.info("> 初始化 访问认证 ...")
auth_service = AuthService(base_config)
self.service_manager.register_service("auth", auth_service)
# 6. 插件桥接服务
logger.info("> 初始化 插件桥 ...")
plugin_bridge = PluginBridge(core_bridge)
await plugin_bridge.start()
self.service_manager.register_service("plugin_bridge", plugin_bridge)
# 7. 关闭服务
logger.info("> 初始化 关闭服务 中...")
shutdown_service = ShutdownService(self.service_manager)
self.service_manager.register_service("shutdown", shutdown_service)
# 8. TUI服务
try:
tui_service = TuiService(base_config, log_service, command_service)
await tui_service.start()
self.service_manager.register_service("tui", tui_service)
logger.info("TUI服务启动成功")
except Exception as e:
logger.warning(f"TUI服务启动失败,使用命令行模式: {str(e)}")
self.service_manager.register_service("tui", self._create_fallback_tui())
# 9. 权限服务
logger.info("> 初始化 权限服务 中...")
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
permission_started = await permission_service.start()
if permission_started:
self.service_manager.register_service("permission", permission_service)
else:
logger.warning("权限服务启动失败,跳过注册")
# 10. 互联网服务(提前创建但不启动)
logger.info("> 初始化 网络服务 中...")
internet_service = InternetService(base_config, self.service_manager)
self.service_manager.register_service("internet", internet_service)
# 11. 插件服务(在网络服务启动前注册路由)
logger.info("> 初始化 插件管理器 中...")
plugin_service = PluginService(base_config, permission_service, plugin_bridge, self.service_manager)
await plugin_service.start()
self.service_manager.register_service("plugin", plugin_service)
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
logger.info("> 初始化 Web 管理面板 中...")
try:
# 确保 internet_service 已经实例化(在第10步)
if internet_service:
from services.web_panel.manager import WebPanelManager
# 初始化面板管理器
web_panel = WebPanelManager(base_config, self.service_manager)
# 这里的 start() 会把面板路由挂载到 http_app 上 (此时还未冻结)
if await web_panel.start():
self.service_manager.register_service("web_panel", web_panel)
logger.info("✅ Web 面板挂载完成")
else:
logger.warning("Web 面板初始化未完成")
except Exception as e:
logger.error(f"❌ Web 管理面板初始化失败: {str(e)}", exc_info=True)
# 12. 启动网络服务(在所有插件路由注册后)
logger.info("> 启动网络服务 中...")
try:
# 先检查依赖和端口
health_info = await internet_service.check_service_health()
logger.debug(f"网络服务预检查: {health_info}")
if not health_info["dependencies_available"]:
logger.error("❌ 缺少必要的依赖包,网络服务无法启动")
logger.info("💡 请运行: pip install aiohttp yaml")
internet_started = False
else:
# 现在启动网络服务(路由器会在启动时冻结)
internet_started = await internet_service.start()
# 再次检查服务状态
if internet_started:
post_health = await internet_service.check_service_health()
logger.debug(f"网络服务启动后检查: {post_health}")
if post_health["http_active"]:
logger.info("✅ 互联网服务启动成功")
# 显示服务信息
logger.info(f"🌐 服务地址: http://{internet_service.http_host}:{internet_service.http_port}")
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
else:
logger.warning("⚠️ 网络服务已启动但端口未响应")
else:
logger.warning("❌ 互联网服务启动失败")
except Exception as e:
logger.error(f"❌ 互联网服务启动异常: {str(e)}", exc_info=True)
logger.warning("互联网服务启动失败")
# 13. API服务
logger.info("> 初始化 API 服务 中...")
try:
api_service = APIService(internet_service, auth_service, permission_service)
self.service_manager.register_service("api", api_service)
logger.info("✅ API 服务启动成功")
except Exception as e:
logger.error(f"❌ API 服务初始化失败: {str(e)}")
logger.warning("API 服务启动失败,跳过注册")
# 注册框架关闭处理器
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
logger.info("🎉 SenSu 初始化完成!")
self.is_running = True
# 显示欢迎日志
version = base_config.get('framework', {}).get('version', 'Unknown')
logLevel = base_config.get('logging', {}).get('level', 'Unknown')
logger.info("\n🐱 SenSu 已就绪!")
logger.info(f"当前 SenSu 版本号 {version}")
logger.info(f"当前日志级别 {logLevel}")
panel_path = base_config.get('panel', {}).get('entrance', {}).get('path', 'panel')
# 显示欢迎消息
tui_service = self.service_manager.get_service("tui")
if hasattr(tui_service, 'show_message'):
tui_service.show_message("🐱 SenSu 框架 已就绪!\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message("🐱 SenSu 已就绪!", "info")
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message(f"当前API配置地址", "debug")
tui_service.show_message(f"地址:http://{internet_service.http_host}:{internet_service.http_port}", "debug")
tui_service.show_message("====================================\n", "debug")
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
else:
print("🐱 SenSu 已就绪!输入 'help' 查看可用命令")
except Exception as e:
logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
await self._safe_shutdown()
raise
def _create_fallback_tui(self):
"""创建回退的TUI服务(命令行模式)"""
class FallbackTuiService:
def __init__(self):
self.is_running = True
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""显示消息到控制台"""
prefix = {
"info": "",
"warning": "⚠️",
"error": "",
"success": ""
}.get(msg_type, "📝")
print(f"{prefix} {message}")
async def start(self):
"""启动回退TUI"""
print("🐱 使用命令行模式...")
return True
def shutdown(self):
"""关闭回退TUI"""
self.is_running = False
return FallbackTuiService()
async def _safe_shutdown(self):
"""安全关闭,即使服务未完全初始化"""
try:
logger.info("执行安全关闭")
self.is_running = False
# 尝试获取关闭服务
try:
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("安全关闭")
return
except Exception as e:
logger.warning(f"Shutdown service unavailable: {e}")
# 如果关闭服务不可用,手动关闭其他服务
services_to_shutdown = ['plugin', 'core_bridge', 'plugin_bridge', 'tui', 'log']
for service_name in services_to_shutdown:
try:
service = self.service_manager.get_service(service_name)
if service and hasattr(service, 'shutdown'):
await service.shutdown() if asyncio.iscoroutinefunction(service.shutdown) else service.shutdown()
except Exception as e:
logger.debug(f"Service shutdown skip: {e}")
logger.debug("安全关闭完成")
except Exception as e:
print(f"❌ 安全关闭时出错: {e}")
async def _framework_shutdown_handler(self):
"""框架关闭处理器"""
try:
logger.info("执行框架关闭处理")
self.is_running = False
# 关闭插件服务
plugin_service = self.service_manager.get_service("plugin")
if plugin_service:
# 卸载所有插件
for plugin_name in list(plugin_service.plugins.keys()):
await plugin_service.unload_plugin(plugin_name)
# 关闭桥接服务
core_bridge = self.service_manager.get_service("core_bridge")
if core_bridge:
await core_bridge.shutdown()
plugin_bridge = self.service_manager.get_service("plugin_bridge")
if plugin_bridge:
await plugin_bridge.shutdown()
logger.debug("框架关闭处理完成")
except Exception as e:
logger.error(f"框架关闭处理时出错: {str(e)}", exc_info=True)
async def run(self):
"""运行框架主循环"""
try:
logger.info("进入框架主循环")
# 特殊终端,添加命令行输入处理
if not hasattr(self.service_manager.get_service("tui"), 'tui_app'):
await self._run_cli_mode()
else:
# 原有的TUI模式
while self.is_running:
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info("主循环被取消")
break
except Exception as e:
logger.error(f"主循环运行时出错: {str(e)}", exc_info=True)
await asyncio.sleep(5)
logger.info("框架主循环结束")
except Exception as e:
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def _run_cli_mode(self):
"""运行命令行模式"""
try:
print("🐱 进入命令行模式,输入 'exit' 退出")
command_service = self.service_manager.get_service("command")
while self.is_running:
try:
# 读取用户输入
user_input = await asyncio.get_event_loop().run_in_executor(
None, input, "🐱 > "
)
if user_input.strip().lower() in ('exit', 'quit', 'q'):
await self.shutdown()
break
# 处理命令
if user_input.strip():
result = await command_service.process_command(user_input, "cli")
print(f"📝 {result}")
except (KeyboardInterrupt, EOFError):
print("\n🐱 接收到退出信号")
await self.shutdown()
break
except Exception as e:
print(f"❌ 命令处理错误: {str(e)}")
except Exception as e:
logger.error(f"命令行模式运行时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def shutdown(self):
"""关闭框架"""
try:
logger.info("开始关闭框架")
self.is_running = False
# 通过关闭服务发起优雅关闭
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("手动关闭")
else:
await self._safe_shutdown()
except Exception as e:
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def main():
"""主函数"""
framework = SenSuFramework()
try:
# 初始化框架
await framework.initialize()
# 运行主循环
await framework.run()
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断")
await framework.shutdown()
except Exception as e:
print(f"🐱 框架运行出错: {str(e)}")
await framework._safe_shutdown()
sys.exit(1)
if __name__ == "__main__":
try:
# 设置更详细的异常处理
import signal
def signal_handler(signum, frame):
"""信号处理"""
print(f"\n🐱 接收到信号 {signum},正在关闭...")
sys.exit(0)
# 注册信号处理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("\n🐱 主程序启动...")
# 运行主程序
asyncio.run(main())
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断,关闭...")
print("🐱 再见喵~")
sys.exit(0)
except SystemExit as e:
# 优雅处理SystemExit
exit_code = e.code if e.code is not None else 0
if exit_code == 0:
print("🐱 框架已关闭")
else:
print(f"🐱 框架退出,代码: {exit_code}")
sys.exit(exit_code)
except Exception as e:
print(f"🐱 框架运行异常: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
print("🐱 框架进程结束")
+386
View File
@@ -0,0 +1,386 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, Any
from aiohttp import web
import json
# 导入命令装饰器
try:
from fmfuncs.plugin_command_decorator import plugin_command, command
except ImportError:
# 回退方案
def plugin_command(name=None, description=None, permissions=None):
def decorator(func):
return func
return decorator
command = plugin_command
# 导入网络桥接类 - 修正路径
try:
from bridges.plugin_network_bridge import PluginNetworkBridge
except ImportError:
# 如果导入失败,创建一个虚拟类
class PluginNetworkBridge:
def __init__(self, plugin_name, internet_service, plugin_bridge):
self.plugin_name = plugin_name
logger.warning(f"PluginNetworkBridge 不可用,插件 {plugin_name} 将以无网络模式运行")
async def register_http_route(self, *args, **kwargs):
logger.warning("网络功能不可用,跳过HTTP路由注册")
async def register_websocket(self, *args, **kwargs):
logger.warning("网络功能不可用,跳过WebSocket注册")
async def broadcast_websocket(self, *args, **kwargs):
logger.warning("网络功能不可用,无法广播消息")
def get_network_info(self):
return {
'plugin_name': self.plugin_name,
'registered_routes': [],
'websocket_handlers': [],
'base_url': '网络服务不可用'
}
async def setup_data_transfer(self, *args, **kwargs):
logger.warning("网络功能不可用,跳过数据传输设置")
logger = logging.getLogger(__name__)
class Plugin:
"""示例插件 - 展示命令注册和网络交互"""
def __init__(self, plugin_name: str, config: Dict, bridge):
self.plugin_name = plugin_name
self.config = config
self.bridge = bridge
self.network_bridge = None
self.is_running = False
logger.debug(f"示例插件初始化: {plugin_name}")
async def initialize(self):
"""初始化插件 - 安全版本"""
try:
logger.info(f"初始化示例插件: {self.plugin_name}")
# 安全地获取网络服务
internet_service = None
try:
internet_service = self.bridge.service_manager.get_service("internet")
logger.debug(f"网络服务获取: {internet_service is not None}")
except (ValueError, AttributeError) as e:
logger.warning(f"网络服务不可用: {str(e)}")
except Exception as e:
logger.error(f"获取网络服务时出错: {str(e)}")
# 只有在网络服务可用时才设置网络功能
if internet_service:
try:
# 创建网络桥接
self.network_bridge = PluginNetworkBridge(
self.plugin_name, internet_service, self.bridge
)
# 注册网络路由
await self._setup_network_routes()
logger.info(f"插件网络功能初始化完成: {self.plugin_name}")
except Exception as e:
logger.error(f"设置网络功能时出错: {str(e)}")
logger.info("插件将以无网络模式运行")
else:
logger.info(f"插件 {self.plugin_name} 将以无网络模式运行")
# 创建虚拟网络桥接以便命令能正常工作
self.network_bridge = PluginNetworkBridge(self.plugin_name, None, self.bridge)
# 注册事件处理器(不依赖网络服务)
self.bridge.subscribe_plugin(
self.plugin_name,
"event.framework.start",
self._handle_framework_start
)
self.is_running = True
logger.debug(f"示例插件初始化完成: {self.plugin_name}")
except Exception as e:
logger.error(f"初始化示例插件时出错: {str(e)}", exc_info=True)
raise
async def _setup_network_routes(self):
"""设置网络路由 - 安全版本"""
try:
if not self.network_bridge:
logger.warning("网络桥接不可用,跳过路由设置")
return
# 注册HTTP API端点
await self.network_bridge.register_http_route(
"/api/info",
self._handle_api_info,
methods=["GET"],
require_auth=False
)
await self.network_bridge.register_http_route(
"/api/echo",
self._handle_api_echo,
methods=["POST"],
require_auth=True
)
# 注册WebSocket端点
await self.network_bridge.register_websocket(
"/chat",
self._handle_websocket_chat
)
# 设置跨端数据传输
await self.network_bridge.setup_data_transfer(
self._handle_cross_platform_data
)
logger.info(f"示例插件网络路由设置完成: {self.plugin_name}")
except Exception as e:
logger.error(f"设置网络路由时出错: {str(e)}", exc_info=True)
# 不抛出异常,让插件继续运行
async def register_delayed_routes(self, internet_service):
"""延迟注册网络路由(在网络服务启动后调用)"""
try:
logger.info(f"为插件 {self.plugin_name} 延迟注册网络路由")
# 重新创建网络桥接,使用真实的网络服务
if internet_service:
try:
# 重新初始化网络桥接
self.network_bridge = PluginNetworkBridge(
self.plugin_name, internet_service, self.bridge
)
# 重新设置网络路由
await self._setup_network_routes()
logger.info(f"插件 {self.plugin_name} 网络功能重新初始化完成")
except Exception as e:
logger.error(f"重新初始化网络桥接时出错: {str(e)}")
logger.info(f"插件 {self.plugin_name} 将继续使用无网络模式")
else:
logger.warning(f"网络服务不可用,插件 {self.plugin_name} 保持无网络模式")
except Exception as e:
logger.error(f"延迟注册网络路由时出错: {str(e)}")
async def _handle_api_info(self, request):
"""处理API信息请求"""
try:
info = {
"plugin_name": self.plugin_name,
"version": self.config.get('version', '1.0.0'),
"description": self.config.get('description', '示例插件'),
"network_info": self.network_bridge.get_network_info() if self.network_bridge else None,
"timestamp": asyncio.get_event_loop().time()
}
return web.json_response(info)
except Exception as e:
logger.error(f"处理API信息请求时出错: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def _handle_api_echo(self, request):
"""处理API回显请求"""
try:
data = await request.json()
response = {
"plugin_name": self.plugin_name,
"echo": data,
"timestamp": asyncio.get_event_loop().time()
}
return web.json_response(response)
except Exception as e:
logger.error(f"处理API回显请求时出错: {str(e)}")
return web.json_response({"error": str(e)}, status=400)
async def _handle_websocket_chat(self, ws, request):
"""处理WebSocket聊天"""
try:
logger.info(f"WebSocket聊天连接建立: {self.plugin_name}")
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
# 处理不同类型的消息
if data.get('type') == 'message':
# 广播消息给所有客户端
if self.network_bridge:
await self.network_bridge.broadcast_websocket({
"type": "message",
"from": data.get('user', 'anonymous'),
"content": data.get('content', ''),
"timestamp": asyncio.get_event_loop().time()
})
except json.JSONDecodeError:
logger.warning(f"收到无效的JSON消息: {msg.data}")
elif msg.type == web.WSMsgType.ERROR:
logger.error(f"WebSocket错误: {ws.exception()}")
except Exception as e:
logger.error(f"WebSocket聊天处理出错: {str(e)}")
finally:
logger.info(f"WebSocket聊天连接关闭: {self.plugin_name}")
async def _handle_cross_platform_data(self, event_type: str, data: Dict):
"""处理跨端数据"""
try:
logger.info(f"收到跨端数据: {event_type}")
# 在这里处理来自其他平台的数据
if event_type == "network.data.receive":
# 广播到WebSocket
if self.network_bridge:
await self.network_bridge.broadcast_websocket({
"type": "cross_platform",
"source": data.get('source', 'unknown'),
"data": data.get('data', {}),
"timestamp": asyncio.get_event_loop().time()
})
except Exception as e:
logger.error(f"处理跨端数据时出错: {str(e)}")
async def _handle_framework_start(self, event_type: str, data: Dict):
"""处理框架启动事件"""
try:
logger.info(f"框架启动事件: {event_type}")
# 发送欢迎消息
if self.network_bridge:
await self.network_bridge.broadcast_websocket({
"type": "system",
"message": f"插件 {self.plugin_name} 已启动,框架已就绪",
"timestamp": asyncio.get_event_loop().time()
})
except Exception as e:
logger.error(f"处理框架启动事件时出错: {str(e)}")
# 确保所有网络相关方法都检查 network_bridge
@plugin_command(name="chat_broadcast",
description="向所有聊天客户端广播消息",
permissions=["plugin.example.chat.broadcast"])
async def cmd_chat_broadcast(self, *args):
"""向所有聊天客户端广播消息"""
try:
if not args:
return "❌ 请提供要广播的消息内容"
message = " ".join(args)
if self.network_bridge:
await self.network_bridge.broadcast_websocket({
"type": "broadcast",
"from": "system",
"content": message,
"timestamp": asyncio.get_event_loop().time()
})
return f"✅ 已广播消息: {message}"
else:
return "❌ 网络服务不可用,无法广播消息"
except Exception as e:
logger.error(f"广播消息时出错: {str(e)}")
return f"❌ 广播失败: {str(e)}"
@plugin_command(name="network_info",
description="显示插件网络信息")
async def cmd_network_info(self, *args):
"""显示插件的网络配置信息"""
try:
if not self.network_bridge:
result = ["🌐 **插件网络信息:**"]
result.append("❌ 网络服务不可用")
result.append("\n💡 **网络服务状态:**")
# 尝试获取网络服务状态
try:
internet_service = self.bridge.service_manager.get_service("internet")
if internet_service:
result.append(" ✅ 网络服务已注册")
health_info = await internet_service.check_service_health()
result.append(f" 🔄 服务运行: {'✅ 是' if health_info.get('is_running') else '❌ 否'}")
result.append(f" 🌐 HTTP活跃: {'✅ 是' if health_info.get('http_active') else '❌ 否'}")
else:
result.append(" ❌ 网络服务未注册")
except:
result.append(" ❓ 无法获取网络服务状态")
result.append("\n🔧 **建议:**")
result.append(" - 检查网络服务启动日志")
result.append(" - 使用 'services' 命令查看服务状态")
result.append(" - 使用 'netdiag' 命令进行网络诊断")
return "\n".join(result)
info = self.network_bridge.get_network_info()
result = ["🌐 **插件网络信息:**"]
result.append(f" 插件名称: {info['plugin_name']}")
result.append(f" 基础URL: {info['base_url']}")
result.append(f" HTTP路由数: {len(info['registered_routes'])}")
result.append(f" WebSocket处理器数: {len(info['websocket_handlers'])}")
if info['registered_routes']:
result.append("\n📡 **注册的HTTP路由:**")
for route in info['registered_routes']:
result.append(f" {route['path']} [{','.join(route['methods'])}]")
if info['websocket_handlers']:
result.append("\n🔗 **注册的WebSocket:**")
for ws in info['websocket_handlers']:
result.append(f" {ws['path']}")
return "\n".join(result)
except Exception as e:
logger.error(f"获取网络信息时出错: {str(e)}")
return f"❌ 获取网络信息失败: {str(e)}"
# ... 其余方法保持不变 ...
async def shutdown(self):
"""关闭插件"""
try:
logger.info(f"关闭示例插件: {self.plugin_name}")
self.is_running = False
# 只有在网络桥接可用时才发送关闭通知
if self.network_bridge and hasattr(self.network_bridge, 'broadcast_websocket'):
try:
await self.network_bridge.broadcast_websocket({
"type": "system",
"message": f"插件 {self.plugin_name} 正在关闭",
"timestamp": asyncio.get_event_loop().time()
})
except Exception as e:
logger.warning(f"发送关闭通知失败: {str(e)}")
# 清理资源
self.bridge.cleanup_plugin_subscriptions(self.plugin_name)
logger.debug(f"示例插件关闭完成: {self.plugin_name}")
except Exception as e:
logger.error(f"关闭示例插件时出错: {str(e)}", exc_info=True)
+16
View File
@@ -0,0 +1,16 @@
# 示例插件配置
name: "ExamplePlugin"
version: "1.0.0"
description: "这是一个示例插件,用于演示插件系统"
author: "CatFramework Team"
# 插件特定配置
settings:
enabled: true
auto_start: true
log_level: "INFO"
# 示例功能配置
features:
echo_enabled: true
greeting_message: "🐱 你好喵~"
+16
View File
@@ -0,0 +1,16 @@
# 示例插件权限申请
plugin_name: "example_plugin"
permissions:
- "plugin.example.read"
- "plugin.example.write"
- "plugin.example.execute"
- "framework.event.subscribe"
- "framework.command.execute"
# 权限说明
permission_descriptions:
plugin.example.read: "读取示例插件数据"
plugin.example.write: "写入示例插件数据"
plugin.example.execute: "执行示例插件操作"
framework.event.subscribe: "订阅框架事件"
framework.command.execute: "执行框架命令"
+8
View File
@@ -0,0 +1,8 @@
rich>=13.0.0
textual>=0.40.0
websockets>=12.0
aiohttp>=3.9.0
pyyaml>=6.0
watchdog>=3.0.0
asyncio-mqtt>=0.16.0
psutil>=5.9.0
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from typing import Dict, Any, List, Optional
import asyncio
logger = logging.getLogger(__name__)
class ServiceManager:
"""服务管理器 - 管理所有框架服务的生命周期"""
def __init__(self):
self.services: Dict[str, Any] = {}
self._startup_order: List[str] = []
self._health_checks: Dict[str, Any] = {}
logger.debug("ServiceManager初始化完成")
def register_service(self, name: str, service_instance,
depends_on: Optional[List[str]] = None,
health_check=None):
"""注册服务"""
if name in self.services:
logger.warning(f"服务 {name} 已存在,将被覆盖")
self.services[name] = service_instance
self._startup_order.append(name)
if health_check:
self._health_checks[name] = health_check
logger.debug(f"服务 {name} 注册成功")
def get_service(self, name: str):
"""获取服务"""
service = self.services.get(name)
if not service:
raise ValueError(f"服务 {name} 未找到。可用: {list(self.services.keys())}")
return service
def has_service(self, name: str) -> bool:
"""检查服务是否已注册"""
return name in self.services
async def check_health(self, name: str = None) -> Dict[str, bool]:
"""健康检查"""
results = {}
names = [name] if name else list(self._health_checks.keys())
for n in names:
if n in self._health_checks:
try:
r = self._health_checks[n]()
if asyncio.iscoroutine(r): r = await r
results[n] = bool(r)
except Exception as e:
logger.warning(f"服务 {n} 健康检查失败: {e}")
results[n] = False
return results
@property
def startup_order(self) -> List[str]:
return list(self._startup_order)
def shutdown_all(self):
"""关闭所有服务"""
logger.info("开始关闭所有服务")
for name in reversed(self._startup_order):
service = self.services.get(name)
if service and hasattr(service, 'shutdown'):
try:
if asyncio.iscoroutinefunction(service.shutdown):
try:
loop = asyncio.get_running_loop()
loop.create_task(service.shutdown())
except RuntimeError:
service.shutdown()
else:
service.shutdown()
logger.debug(f"服务 {name} 关闭成功")
except Exception as e:
logger.error(f"关闭服务 {name} 时出错: {e}")
logger.info("所有服务关闭完成")
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logger = logging.getLogger(__name__)
class ServiceManager:
"""服务管理器"""
def __init__(self):
self.services = {}
# 记录框架启动时间
self.start_time = time.time()
logger.debug("ServiceManager初始化完成")
def register_service(self, name: str, service_instance):
"""注册服务"""
try:
if name in self.services:
logger.warning(f"服务 {name} 已存在,将被覆盖")
self.services[name] = service_instance
logger.debug(f"服务 {name} 注册成功")
except Exception as e:
logger.error(f"注册服务 {name} 时出错: {str(e)}", exc_info=True)
raise
def get_service(self, name: str):
"""获取服务"""
try:
service = self.services.get(name)
if not service:
logger.error(f"服务 {name} 不存在")
raise ValueError(f"服务 {name} 未找到")
logger.debug(f"成功获取服务 {name}")
return service
except Exception as e:
logger.error(f"获取服务 {name} 时出错: {str(e)}", exc_info=True)
raise
def shutdown_all(self):
"""关闭所有服务"""
logger.info("开始关闭所有服务")
for name, service in self.services.items():
try:
if hasattr(service, 'shutdown'):
service.shutdown()
logger.debug(f"服务 {name} 关闭成功")
except Exception as e:
logger.error(f"关闭服务 {name} 时出错: {str(e)}", exc_info=True)
logger.info("所有服务关闭完成")
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Optional
from dataclasses import dataclass
from aiohttp import web
import json
logger = logging.getLogger(__name__)
@dataclass
class APIEndpoint:
"""API端点数据类"""
path: str
methods: List[str]
handler: callable
plugin_name: str
require_auth: bool
admin_only: bool
class APIService:
"""API服务 - 管理插件的API端点"""
def __init__(self, internet_service, auth_service, permission_service):
self.internet_service = internet_service
self.auth_service = auth_service
self.permission_service = permission_service
self.endpoints: Dict[str, APIEndpoint] = {}
self.plugin_endpoints: Dict[str, List[str]] = {}
logger.debug("APIService初始化开始")
async def register_endpoint(self, plugin_name: str, path: str, methods: List[str],
handler: callable, require_auth: bool = True,
admin_only: bool = False) -> bool:
"""注册API端点"""
try:
logger.debug(f"注册API端点: {plugin_name} -> {path} {methods}")
# 权限检查 - 只有admin插件可以操作接口
if not self.permission_service.check_plugin_permission(plugin_name, "admin"):
logger.error(f"插件 {plugin_name} 无权限注册API端点")
return False
# 创建端点键
endpoint_key = f"{plugin_name}:{path}"
# 检查端点是否已存在
if endpoint_key in self.endpoints:
logger.warning(f"API端点已存在: {endpoint_key}")
return False
# 创建端点实例
endpoint = APIEndpoint(
path=path,
methods=methods,
handler=handler,
plugin_name=plugin_name,
require_auth=require_auth,
admin_only=admin_only
)
# 注册到互联网服务
for method in methods:
internet_endpoint_key = f"{method}:{path}"
self.internet_service.endpoints[internet_endpoint_key] = endpoint
# 保存端点信息
self.endpoints[endpoint_key] = endpoint
# 更新插件端点映射
if plugin_name not in self.plugin_endpoints:
self.plugin_endpoints[plugin_name] = []
self.plugin_endpoints[plugin_name].append(endpoint_key)
logger.info(f"API端点注册成功: {endpoint_key}")
return True
except Exception as e:
logger.error(f"注册API端点时出错: {str(e)}", exc_info=True)
return False
async def unregister_endpoint(self, plugin_name: str, path: str) -> bool:
"""注销API端点"""
try:
logger.debug(f"注销API端点: {plugin_name} -> {path}")
endpoint_key = f"{plugin_name}:{path}"
if endpoint_key not in self.endpoints:
logger.warning(f"API端点不存在: {endpoint_key}")
return False
endpoint = self.endpoints[endpoint_key]
# 从互联网服务中移除
for method in endpoint.methods:
internet_endpoint_key = f"{method}:{path}"
if internet_endpoint_key in self.internet_service.endpoints:
del self.internet_service.endpoints[internet_endpoint_key]
# 从端点映射中移除
del self.endpoints[endpoint_key]
# 从插件端点列表中移除
if plugin_name in self.plugin_endpoints:
if endpoint_key in self.plugin_endpoints[plugin_name]:
self.plugin_endpoints[plugin_name].remove(endpoint_key)
logger.info(f"API端点注销成功: {endpoint_key}")
return True
except Exception as e:
logger.error(f"注销API端点时出错: {str(e)}", exc_info=True)
return False
async def unregister_all_plugin_endpoints(self, plugin_name: str) -> bool:
"""注销插件的所有API端点"""
try:
logger.debug(f"注销插件所有API端点: {plugin_name}")
if plugin_name not in self.plugin_endpoints:
logger.debug(f"插件无注册的API端点: {plugin_name}")
return True
endpoints_to_remove = self.plugin_endpoints[plugin_name][:]
success_count = 0
for endpoint_key in endpoints_to_remove:
# 从endpoint_key中提取path
parts = endpoint_key.split(':', 1)
if len(parts) == 2:
path = parts[1]
success = await self.unregister_endpoint(plugin_name, path)
if success:
success_count += 1
logger.info(f"插件API端点清理完成: {plugin_name} -> 成功 {success_count}/{len(endpoints_to_remove)}")
return success_count == len(endpoints_to_remove)
except Exception as e:
logger.error(f"注销插件所有API端点时出错: {str(e)}", exc_info=True)
return False
def get_plugin_endpoints(self, plugin_name: str) -> List[Dict]:
"""获取插件的API端点列表"""
try:
if plugin_name not in self.plugin_endpoints:
return []
endpoints_info = []
for endpoint_key in self.plugin_endpoints[plugin_name]:
if endpoint_key in self.endpoints:
endpoint = self.endpoints[endpoint_key]
endpoints_info.append({
'path': endpoint.path,
'methods': endpoint.methods,
'require_auth': endpoint.require_auth,
'admin_only': endpoint.admin_only
})
logger.debug(f"获取插件API端点列表: {plugin_name} -> {len(endpoints_info)}")
return endpoints_info
except Exception as e:
logger.error(f"获取插件API端点列表时出错: {str(e)}", exc_info=True)
return []
def get_all_endpoints(self) -> List[Dict]:
"""获取所有API端点"""
try:
all_endpoints = []
for endpoint_key, endpoint in self.endpoints.items():
all_endpoints.append({
'plugin': endpoint.plugin_name,
'path': endpoint.path,
'methods': endpoint.methods,
'require_auth': endpoint.require_auth,
'admin_only': endpoint.admin_only
})
logger.debug(f"获取所有API端点: {len(all_endpoints)}")
return all_endpoints
except Exception as e:
logger.error(f"获取所有API端点时出错: {str(e)}", exc_info=True)
return []
async def shutdown(self):
"""关闭API服务"""
try:
logger.info("关闭API服务")
# 注销所有端点
all_plugins = list(self.plugin_endpoints.keys())
for plugin_name in all_plugins:
await self.unregister_all_plugin_endpoints(plugin_name)
self.endpoints.clear()
self.plugin_endpoints.clear()
logger.debug("API服务关闭完成")
except Exception as e:
logger.error(f"关闭API服务时出错: {str(e)}", exc_info=True)
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
import hashlib
import secrets
from typing import Dict, List, Optional
from dataclasses import dataclass
import time
import json
logger = logging.getLogger(__name__)
@dataclass
class User:
"""用户数据类"""
username: str
password_hash: str
permissions: List[str]
is_active: bool = True
created_at: float = None
@dataclass
class Token:
"""令牌数据类"""
token: str
username: str
permissions: List[str]
created_at: float
expires_at: float
is_valid: bool = True
class AuthService:
"""认证服务 - 处理用户认证和权限验证"""
def __init__(self, config: Dict):
self.config = config
self.users: Dict[str, User] = {}
self.tokens: Dict[str, Token] = {}
self.token_expiry_hours = 24
self.secret_key = secrets.token_hex(32)
logger.debug("AuthService初始化开始")
# 初始化默认用户
self._init_default_users()
def _init_default_users(self):
"""初始化默认用户"""
try:
# 创建默认管理员用户
admin_password_hash = self._hash_password(os.environ.get("SENSU_ADMIN_PASSWORD","admin123"))
admin_user = User(
username="admin",
password_hash=admin_password_hash,
permissions=["admin"],
created_at=time.time()
)
self.users["admin"] = admin_user
# 创建默认API用户
api_password_hash = self._hash_password(os.environ.get("SENSU_API_PASSWORD","api123"))
api_user = User(
username="api",
password_hash=api_password_hash,
permissions=["framework.status.read", "plugin.info.read"],
created_at=time.time()
)
self.users["api"] = api_user
logger.debug("默认用户初始化完成")
except Exception as e:
logger.error(f"初始化默认用户时出错: {str(e)}", exc_info=True)
raise
def _hash_password(self, password: str) -> str:
"""哈希密码"""
try:
salt = "catframework_salt" # 实际应该使用随机盐
return hashlib.sha256((password + salt).encode()).hexdigest()
except Exception as e:
logger.error(f"哈希密码时出错: {str(e)}", exc_info=True)
raise
def authenticate_user(self, username: str, password: str) -> Optional[Token]:
"""用户认证"""
try:
logger.debug(f"用户认证尝试: {username}")
if username not in self.users:
logger.warning(f"用户不存在: {username}")
return None
user = self.users[username]
if not user.is_active:
logger.warning(f"用户已被禁用: {username}")
return None
password_hash = self._hash_password(password)
if user.password_hash != password_hash:
logger.warning(f"密码错误: {username}")
return None
# 创建令牌
token = self._create_token(user)
logger.debug(f"用户认证成功: {username}")
return token
except Exception as e:
logger.error(f"用户认证时出错: {str(e)}", exc_info=True)
return None
def _create_token(self, user: User) -> Token:
"""创建令牌"""
try:
token_str = secrets.token_hex(32)
created_at = time.time()
expires_at = created_at + (self.token_expiry_hours * 3600)
token = Token(
token=token_str,
username=user.username,
permissions=user.permissions,
created_at=created_at,
expires_at=expires_at
)
self.tokens[token_str] = token
logger.debug(f"创建令牌: {user.username}, 有效期: {self.token_expiry_hours}小时")
return token
except Exception as e:
logger.error(f"创建令牌时出错: {str(e)}", exc_info=True)
raise
def validate_token(self, token_str: str) -> Optional[Token]:
"""验证令牌"""
try:
if token_str not in self.tokens:
logger.debug("令牌不存在")
return None
token = self.tokens[token_str]
# 检查令牌是否有效
if not token.is_valid:
logger.debug("令牌已失效")
return None
# 检查令牌是否过期
if time.time() > token.expires_at:
logger.debug("令牌已过期")
token.is_valid = False
return None
logger.debug(f"令牌验证成功: {token.username}")
return token
except Exception as e:
logger.error(f"验证令牌时出错: {str(e)}", exc_info=True)
return None
def revoke_token(self, token_str: str) -> bool:
"""撤销令牌"""
try:
if token_str in self.tokens:
self.tokens[token_str].is_valid = False
logger.debug(f"令牌已撤销: {token_str}")
return True
else:
logger.warning(f"要撤销的令牌不存在: {token_str}")
return False
except Exception as e:
logger.error(f"撤销令牌时出错: {str(e)}", exc_info=True)
return False
def check_permission(self, token_str: str, permission: str) -> bool:
"""检查权限"""
try:
token = self.validate_token(token_str)
if not token:
return False
# 检查admin权限
if "admin" in token.permissions:
return True
# 检查具体权限
has_permission = permission in token.permissions
logger.debug(f"权限检查: {token.username} -> {permission} = {has_permission}")
return has_permission
except Exception as e:
logger.error(f"检查权限时出错: {str(e)}", exc_info=True)
return False
def create_user(self, username: str, password: str, permissions: List[str]) -> bool:
"""创建用户"""
try:
if username in self.users:
logger.warning(f"用户已存在: {username}")
return False
password_hash = self._hash_password(password)
user = User(
username=username,
password_hash=password_hash,
permissions=permissions,
created_at=time.time()
)
self.users[username] = user
logger.debug(f"用户创建成功: {username}, 权限: {permissions}")
return True
except Exception as e:
logger.error(f"创建用户时出错: {str(e)}", exc_info=True)
return False
def get_user_info(self, username: str) -> Optional[Dict]:
"""获取用户信息"""
try:
if username not in self.users:
return None
user = self.users[username]
return {
"username": user.username,
"permissions": user.permissions,
"is_active": user.is_active,
"created_at": user.created_at
}
except Exception as e:
logger.error(f"获取用户信息时出错: {str(e)}", exc_info=True)
return None
def cleanup_expired_tokens(self):
"""清理过期令牌"""
try:
current_time = time.time()
expired_tokens = []
for token_str, token in self.tokens.items():
if current_time > token.expires_at:
expired_tokens.append(token_str)
for token_str in expired_tokens:
del self.tokens[token_str]
if expired_tokens:
logger.debug(f"清理了 {len(expired_tokens)} 个过期令牌")
except Exception as e:
logger.error(f"清理过期令牌时出错: {str(e)}", exc_info=True)
def shutdown(self):
"""关闭认证服务"""
try:
logger.info("关闭认证服务")
self.cleanup_expired_tokens()
self.users.clear()
self.tokens.clear()
logger.debug("认证服务关闭完成")
except Exception as e:
logger.error(f"关闭认证服务时出错: {str(e)}", exc_info=True)
+604
View File
@@ -0,0 +1,604 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import shlex
from typing import Dict, List, Callable, Any
from dataclasses import dataclass
import os
logger = logging.getLogger(__name__)
@dataclass
class Command:
"""命令数据类"""
name: str
handler: Callable
description: str
permissions: List[str]
source: str = "internal"
class CommandService:
"""指令服务"""
def __init__(self, service_manager=None):
self.commands: Dict[str, Command] = {}
self.command_history: List[Dict] = []
self.max_history_size = 100
self.service_manager = service_manager # 添加服务管理器引用
logger.debug("CommandService初始化开始")
def register_command(self, name: str, handler: Callable, description: str = "",
permissions: List[str] = None, source: str = "plugins"):
"""注册命令"""
# 来源如 internal plugins system 等
try:
if name in self.commands:
logger.warning(f"命令 {name} 已存在,将被覆盖")
self.commands[name] = Command(
name=name,
handler=handler,
description=description or f"命令: {name}",
permissions=permissions or [],
source=source
)
logger.debug(f"注册命令: {name} (来源: {source})")
except Exception as e:
logger.error(f"注册命令 {name} 时出错: {str(e)}", exc_info=True)
raise
async def _handle_permission_command(self, command: str, args: List[str], source: str) -> str:
"""处理权限相关命令"""
try:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
return await permission_service.process_permission_command(command, args)
except Exception as e:
logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True)
return f"❌ 处理权限命令时出错: {str(e)}"
async def process_command(self, command_string: str, source: str = "unknown") -> Any:
"""处理命令"""
try:
logger.debug(f"处理命令: '{command_string}' (来源: {source})")
# 解析命令
parts = shlex.split(command_string.strip())
if not parts:
logger.warning("空命令")
return "空命令"
command_name = parts[0]
args = parts[1:]
# 记录命令历史
self._add_to_history(command_string, source)
# 检查权限命令
permission_commands = ['pmallow', 'pmdeny', 'pmignore', 'permissions',
'pmpending', 'pmrequests', 'pm_plugin_status', 'pmhelp', 'pmtest']
if command_name in permission_commands:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
# 直接调用权限服务处理命令
return await permission_service.process_permission_command(command_name, args)
# 查找其他命令
if command_name not in self.commands:
logger.warning(f"未知命令: {command_name}")
return f"未知命令: {command_name}"
command = self.commands[command_name]
# 执行命令
try:
result = await self._execute_command(command, args, source)
logger.debug(f"命令执行成功: {command_name}")
return result
except Exception as e:
logger.error(f"命令执行失败 {command_name}: {str(e)}", exc_info=True)
return f"命令执行错误: {str(e)}"
except Exception as e:
logger.error(f"处理命令时出错: {str(e)}", exc_info=True)
return f"命令处理错误: {str(e)}"
async def _execute_command(self, command: Command, args: List[str], source: str) -> Any:
"""执行命令"""
try:
# 检查处理器类型
if asyncio.iscoroutinefunction(command.handler):
result = await command.handler(*args)
else:
result = command.handler(*args)
logger.debug(f"命令 {command.name} 执行完成")
return result
except TypeError as e:
logger.error(f"命令参数错误 {command.name}: {str(e)}", exc_info=True)
raise ValueError(f"参数错误: {str(e)}")
except Exception as e:
logger.error(f"命令执行异常 {command.name}: {str(e)}", exc_info=True)
raise
def _add_to_history(self, command: str, source: str):
"""添加到命令历史"""
try:
history_entry = {
"command": command,
"source": source,
"timestamp": asyncio.get_event_loop().time()
}
self.command_history.append(history_entry)
# 限制历史记录大小
if len(self.command_history) > self.max_history_size:
self.command_history.pop(0)
logger.debug(f"命令历史记录添加,当前大小: {len(self.command_history)}")
except Exception as e:
logger.error(f"添加命令历史时出错: {str(e)}", exc_info=True)
def get_command_list(self) -> List[Dict]:
"""获取命令列表"""
try:
command_list = []
for name, cmd in self.commands.items():
command_list.append({
"name": name,
"description": cmd.description,
"permissions": cmd.permissions,
"source": cmd.source
})
logger.debug(f"获取命令列表,共 {len(command_list)} 个命令")
return command_list
except Exception as e:
logger.error(f"获取命令列表时出错: {str(e)}", exc_info=True)
return []
def get_command_history(self, limit: int = 10) -> List[Dict]:
"""获取命令历史"""
try:
history = self.command_history[-limit:]
logger.debug(f"获取命令历史,返回 {len(history)} 条记录")
return history
except Exception as e:
logger.error(f"获取命令历史时出错: {str(e)}", exc_info=True)
return []
def register_builtin_commands(self):
"""注册内置命令"""
try:
logger.debug("开始注册内置命令")
# 帮助命令
self.register_command(
name="help",
handler=self._cmd_help,
description="显示帮助信息",
permissions=["framework.command.help.read"],
source="internal"
)
# 测试日志命令
self.register_command(
name="testlog",
handler=self._cmd_test_log,
description="生成测试日志",
permissions=["framework.command.test"],
source="internal"
)
# 状态命令
self.register_command(
name="status",
handler=self._cmd_status,
description="显示框架状态",
permissions=["framework.status.read"],
source="internal"
)
# 历史命令
self.register_command(
name="history",
handler=self._cmd_history,
description="显示命令历史",
permissions=["framework.command.history.read"],
source="internal"
)
# 网络诊断
self.register_command(
name="netdiag",
handler=self._cmd_netdiag,
description="网络服务诊断",
permissions=["framework.network.diagnose"],
source="internal"
)
# 权限管理命令组 - 保留注册但不使用(在process_command中直接处理)
# 这些注册是为了在help命令中显示
permission_commands = [
("pmallow", "权限管理: 同意权限请求"),
("pmdeny", "权限管理: 拒绝权限请求"),
("pmignore", "权限管理: 暂时忽略权限请求"),
("permissions", "权限管理: 显示权限状态"),
("pmpending", "权限管理: 查看待授权请求列表"),
("pmrequests", "权限管理: 查看待授权请求列表(别名)"),
("pm_plugin_status", "权限管理: 查看插件权限状态"),
("pmtest", "权限管理: 测试权限配置文件"),
("pmhelp", "权限管理: 显示权限命令帮助")
]
for cmd_name, description in permission_commands:
self.register_command(
name=cmd_name,
handler=self._cmd_permission, # 使用统一的备用处理器
description=description,
permissions=["framework.permission.read"],
source="internal"
)
# 滚动控制命令组
scroll_commands = [
("scroll", "滚动控制: 手动滚动到底部"),
("autoscroll", "滚动控制: 切换自动滚动")
]
# 脚手架命令
self.register_command(
name="create-plugin",
handler=self._cmd_create_plugin,
description="创建新插件脚手架",
permissions=["framework.scaffold.plugin"],
source="internal"
)
logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令")
for cmd_name, description in scroll_commands:
self.register_command(
name=cmd_name,
handler=self._cmd_scroll_control,
description=description,
permissions=["framework.tui.control"],
source="internal"
)
logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令")
except Exception as e:
logger.error(f"注册内置命令时出错: {str(e)}", exc_info=True)
raise
async def _cmd_netdiag(self, *args) -> str:
"""网络诊断命令"""
try:
internet_service = self.service_manager.get_service("internet")
result = ["🔧 **网络服务诊断报告**"]
result.append("=" * 50)
if not internet_service:
result.append("❌ 网络服务未注册")
result.append("\n💡 **可能的原因:**")
result.append(" 1. 网络服务启动失败")
result.append(" 2. 依赖包缺失 (aiohttp)")
result.append(" 3. 端口被占用")
result.append(" 4. 权限不足")
result.append("\n🔧 **解决方案:**")
result.append(" - 检查上方日志中的错误信息")
result.append(" - 运行: pip install aiohttp")
result.append(" - 尝试更换端口号")
result.append(" - 使用 sudo (如果需要)")
return "\n".join(result)
# 获取健康信息
health_info = await internet_service.check_service_health()
result.append(f"🔄 服务运行: {'✅ 是' if health_info.get('is_running') else '❌ 否'}")
result.append(f"🔌 HTTP端口: {health_info.get('http_port', 'N/A')}")
result.append(f"📡 WebSocket端口: {health_info.get('websocket_port', 'N/A')}")
result.append(f"🌐 HTTP活跃: {'✅ 是' if health_info.get('http_active') else '❌ 否'}")
result.append(f"📦 依赖状态: {'✅ 正常' if health_info.get('dependencies_available') else '❌ 缺失'}")
if health_info.get('error'):
result.append(f"❌ 错误信息: {health_info['error']}")
# 端口占用检查
if not health_info.get('http_active') and health_info.get('is_running'):
result.append("\n⚠️ **端口问题检测:**")
result.append(" HTTP服务已启动但端口未响应")
result.append(" 可能被防火墙阻止或配置错误")
# 路由信息
routes = internet_service.get_plugin_routes()
total_routes = sum(len(plugin_routes) for plugin_routes in routes.values())
result.append(f"\n🛣️ 注册路由: {total_routes}")
for plugin_name, plugin_routes in routes.items():
result.append(f" 📍 {plugin_name}: {len(plugin_routes)} 个路由")
return "\n".join(result)
except Exception as e:
logger.error(f"网络诊断命令执行失败: {str(e)}")
return f"❌ 网络诊断失败: {str(e)}"
async def _cmd_scroll_control(self, *args) -> str:
"""处理滚动控制命令"""
try:
tui_service = self.service_manager.get_service("tui")
if not tui_service:
return "❌ TUI服务不可用"
if not args:
return "🔧 滚动控制命令\n💡 使用: scroll [log|message|all]\n💡 使用: autoscroll [on|off|toggle] [log|message|all]"
command = args[0].lower()
if command == "scroll":
target = args[1] if len(args) > 1 else "all"
if target not in ["log", "message", "all"]:
return "❌ 无效的目标,请使用: log, message, all"
return tui_service.scroll_to_bottom(target)
elif command == "autoscroll":
if len(args) < 2:
return "❌ 请指定操作: on, off, toggle"
action = args[1].lower()
target = args[2] if len(args) > 2 else "all"
if target not in ["log", "message", "all"]:
return "❌ 无效的目标,请使用: log, message, all"
if action == "on":
return tui_service.toggle_auto_scroll(target, True)
elif action == "off":
return tui_service.toggle_auto_scroll(target, False)
elif action == "toggle":
return tui_service.toggle_auto_scroll(target, None)
else:
return "❌ 无效的操作,请使用: on, off, toggle"
else:
return "❌ 未知滚动命令\n💡 可用命令: scroll, autoscroll"
except Exception as e:
logger.error(f"处理滚动命令时出错: {str(e)}")
return f"❌ 滚动命令错误: {str(e)}"
async def _cmd_permission(self, *args) -> str:
"""处理权限相关命令 - 备用处理器"""
try:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
# 如果没有参数,显示通用帮助
if not args:
return "🔐 权限管理命令\n💡 使用 pmhelp 查看详细帮助"
# 否则直接转发到权限服务
command_name = str(args[0]).lower()
permission_args = [str(arg) for arg in args[1:]] if len(args) > 1 else []
return await permission_service.process_permission_command(command_name, permission_args)
except Exception as e:
logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True)
return f"❌ 权限命令错误: {str(e)}"
async def _cmd_help(self, *args) -> str:
"""帮助命令处理器"""
try:
commands = self.get_command_list()
if not commands:
return "❌ 没有可用的命令"
help_text = ["📋 **可用命令:**", ""]
# 按来源分组显示命令
commands_by_source = {}
for cmd in commands:
source = cmd['source']
if source not in commands_by_source:
commands_by_source[source] = []
commands_by_source[source].append(cmd)
# 显示内置命令
if 'internal' in commands_by_source:
help_text.append("🔧 **内置命令:**")
for cmd in commands_by_source['internal']:
help_text.append(f" 🟢 {cmd['name']:15} - {cmd['description']}")
help_text.append("")
# 显示插件命令
if 'plugin' in commands_by_source:
help_text.append("🔌 **插件命令:**")
for cmd in commands_by_source['plugin']:
help_text.append(f" 🟡 {cmd['name']:15} - {cmd['description']}")
help_text.append("")
# 显示系统命令
if 'system' in commands_by_source:
help_text.append("⚙️ **系统命令:**")
for cmd in commands_by_source['system']:
help_text.append(f" 🔵 {cmd['name']:15} - {cmd['description']}")
# 添加使用提示
help_text.extend([
"",
"💡 **使用提示:**",
" - 输入命令名称执行命令",
" - 使用 'status' 查看框架状态",
" - 使用 'history' 查看命令历史",
" - 使用 'permissions' 管理插件权限"
])
return "\n".join(help_text)
except Exception as e:
logger.error(f"处理help命令时出错: {str(e)}", exc_info=True)
return f"❌ 帮助命令错误: {str(e)}"
async def _cmd_status(self, *args) -> str:
"""状态命令处理器"""
try:
status_info = [
f"命令服务状态:",
f" 注册命令数: {len(self.commands)}",
f" 历史记录数: {len(self.command_history)}",
f" 最大历史大小: {self.max_history_size}"
]
return "\n".join(status_info)
except Exception as e:
logger.error(f"处理status命令时出错: {str(e)}", exc_info=True)
return f"状态命令错误: {str(e)}"
async def _cmd_history(self, *args) -> str:
"""历史命令处理器"""
try:
limit = 10
if args and args[0].isdigit():
limit = min(int(args[0]), 50) # 限制最大50条
history = self.get_command_history(limit)
if not history:
return "没有命令历史"
history_text = [f"最近 {len(history)} 条命令历史:"]
for i, entry in enumerate(reversed(history), 1):
history_text.append(f" {i}. [{entry['source']}] {entry['command']}")
return "\n".join(history_text)
except Exception as e:
logger.error(f"处理history命令时出错: {str(e)}", exc_info=True)
return f"历史命令错误: {str(e)}"
async def _cmd_test_log(self, *args) -> str:
"""测试日志命令"""
try:
logger.debug("这是一条DEBUG测试日志")
logger.info("这是一条INFO测试日志")
logger.warning("这是一条WARNING测试日志")
logger.error("这是一条ERROR测试日志")
return "✅ 测试日志已生成,请检查TUI显示"
except Exception as e:
return f"❌ 测试日志生成失败: {str(e)}"
async def _cmd_create_plugin(self, *args) -> str:
"""创建新插件脚手架"""
try:
import re
import shutil
from pathlib import Path
from string import Template
# 1. 参数解析
if not args:
return "❌ 用法: create-plugin <插件名> [--author <作者>] [--desc <描述>]\n💡 插件名需为小写字母/数字/下划线,如: my_cool_plugin"
plugin_name = args[0]
author = "Unknown"
description = "暂无描述"
# 解析可选参数
i = 1
while i < len(args):
if args[i] == "--author" and i + 1 < len(args):
author = args[i+1]
i += 2
elif args[i] == "--desc" and i + 1 < len(args):
description = args[i+1]
i += 2
else:
i += 1
# 2. 命名校验
if not re.match(r'^[a-z][a-z0-9_]*$', plugin_name):
return "❌ 插件名格式错误。请使用小写字母开头,仅包含小写字母、数字和下划线(如: data_sync)"
plugin_dir = Path("plugins") / plugin_name
if plugin_dir.exists():
return f"❌ 插件目录已存在: {plugin_dir}"
# 3. 模板路径
template_dir = Path(os.getenv("SENSU_CODE_DIR", ".")) / "templates" / "plugin"
if not template_dir.exists():
return "❌ 模板目录不存在: templates/plugin/"
# 4. 创建目录与渲染文件
plugin_dir.mkdir(parents=True, exist_ok=True)
context = {
"plugin_name": plugin_name,
"author": author,
"description": description
}
for template_file in template_dir.iterdir():
if template_file.is_file() and template_file.name.endswith(".template"):
with open(template_file, 'r', encoding='utf-8') as f:
tpl = Template(f.read())
content = tpl.safe_substitute(context)
target_name = template_file.stem
target_path = plugin_dir / target_name
with open(target_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.debug(f"脚手架文件生成: {target_path}")
return (
f"✅ 插件脚手架创建成功!\n"
f"📁 路径: {plugin_dir}\n"
f"👤 作者: {author}\n"
f"📝 描述: {description}\n\n"
f"🔧 下一步:\n"
f" 1. 编辑 {plugin_dir}/__init__.py 实现业务逻辑\n"
f" 2. 运行框架自动加载插件\n"
f" 3. 使用 `help` 查看可用命令"
)
except Exception as e:
logger.error(f"创建插件脚手架失败: {str(e)}", exc_info=True)
return f"❌ 创建失败: {str(e)}"
def shutdown(self):
"""关闭指令服务"""
try:
logger.info("关闭指令服务")
self.commands.clear()
self.command_history.clear()
logger.debug("指令服务关闭完成")
except Exception as e:
logger.error(f"关闭指令服务时出错: {str(e)}", exc_info=True)
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from pathlib import Path
from typing import Dict, Any
import yaml
import importlib.util
import sys
import os
logger = logging.getLogger(__name__)
class InitService:
"""初始化服务"""
def __init__(self, config_path: str = "config/framework"):
self.config_path = Path(config_path)
self.configs: Dict[str, Any] = {}
self.fmfuncs_loaded = False
logger.debug("InitService初始化开始")
async def initialize_framework(self):
"""初始化框架"""
try:
logger.info("开始初始化框架")
# 1. 加载配置
await self._load_configs()
# 2. 创建必要目录
await self._create_directories()
# 3. 加载框架功能集
await self._load_fmfuncs()
# 4. 验证初始化状态
await self._validate_init()
logger.info("框架初始化完成")
return self.configs
except Exception as e:
logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
raise
async def _load_configs(self):
"""加载配置文件"""
try:
logger.debug("开始加载配置文件")
if not self.config_path.exists():
logger.warning(f"配置路径不存在: {self.config_path},将创建默认配置")
self.config_path.mkdir(parents=True, exist_ok=True)
# 加载基础配置
base_config_file = self.config_path / "base_config.yaml"
if base_config_file.exists():
with open(base_config_file, 'r', encoding='utf-8') as f:
self.configs['base'] = yaml.safe_load(f)
logger.debug("基础配置加载成功")
else:
logger.warning("基础配置文件不存在,使用默认配置")
self.configs['base'] = self._get_default_base_config()
self._save_config(base_config_file, self.configs['base'])
# 加载权限规则
permission_file = self.config_path / "permission_rules.yaml"
if permission_file.exists():
with open(permission_file, 'r', encoding='utf-8') as f:
self.configs['permission_rules'] = yaml.safe_load(f)
logger.debug("权限规则配置加载成功")
else:
logger.warning("权限规则文件不存在,使用默认配置")
self.configs['permission_rules'] = self._get_default_permission_rules()
self._save_config(permission_file, self.configs['permission_rules'])
logger.debug(f"配置文件加载完成,共加载 {len(self.configs)} 个配置集")
except Exception as e:
logger.error(f"加载配置文件时出错: {str(e)}", exc_info=True)
raise
async def _create_directories(self):
"""创建必要目录"""
try:
logger.debug("开始创建必要目录")
directories = [
"config/plugins",
"config/services",
"config/permissions",
"logs/runtime",
"logs/debug",
"plugins",
"utils",
"fmfuncs"
]
for dir_path in directories:
path = Path(dir_path)
path.mkdir(parents=True, exist_ok=True)
logger.debug(f"创建目录: {dir_path}")
logger.debug("目录创建完成")
except Exception as e:
logger.error(f"创建目录时出错: {str(e)}", exc_info=True)
raise
async def _load_fmfuncs(self):
"""加载框架功能集"""
try:
logger.debug("开始加载框架功能集")
fmfuncs_path = Path(os.getenv("SENSU_CODE_DIR", ".")) / "fmfuncs"
if not fmfuncs_path.exists():
logger.warning("fmfuncs目录不存在,跳过加载")
return
# 动态加载所有Python文件
for py_file in fmfuncs_path.glob("*.py"):
if py_file.name == "__init__.py":
continue
try:
module_name = f"fmfuncs.{py_file.stem}"
spec = importlib.util.spec_from_file_location(module_name, py_file)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
logger.debug(f"加载框架功能: {module_name}")
except Exception as e:
logger.error(f"加载框架功能 {py_file} 时出错: {str(e)}", exc_info=True)
continue
self.fmfuncs_loaded = True
logger.debug("框架功能集加载完成")
except Exception as e:
logger.error(f"加载框架功能集时出错: {str(e)}", exc_info=True)
raise
async def _validate_init(self):
"""验证初始化状态"""
try:
logger.debug("开始验证初始化状态")
required_configs = ['base', 'permission_rules']
for config_name in required_configs:
if config_name not in self.configs:
logger.error(f"缺少必要配置: {config_name}")
raise ValueError(f"缺少必要配置: {config_name}")
required_dirs = ['config', 'logs', 'plugins']
for dir_name in required_dirs:
if not Path(dir_name).exists():
logger.error(f"必要目录不存在: {dir_name}")
raise ValueError(f"必要目录不存在: {dir_name}")
logger.debug("初始化状态验证通过")
except Exception as e:
logger.error(f"验证初始化状态时出错: {str(e)}", exc_info=True)
raise
def _get_default_base_config(self) -> Dict:
"""获取默认基础配置"""
return {
'framework': {
'name': 'SenSu',
'version': 'Alpha_0.2.0',
'debug': True
},
'logging': {
'level': 'INFO',
'debug_level_file': True,
'max_log_files': 20,
'max_file_size': '10MB'
},
'tui': {
'layout': {
'grid-rows': '4fr 5fr 1fr'
}
},
'services': {
'internet': {
'ws_port': 8765,
'api_port': 8000,
'enable_reverse_proxy': False
}
},
'plugins': {
'auto_load': True,
'hot_reload': True,
'max_retry_count': 3
}
}
def _get_default_permission_rules(self) -> Dict:
"""获取默认权限规则"""
return {
'permission_levels': ['read', 'write', 'execute', 'admin'],
'default_permissions': [
'framework.status.read',
'plugin.self.info.read'
],
'admin_permissions': [
'framework.*',
'plugin.*',
'service.*'
]
}
def _save_config(self, file_path: Path, config: Dict):
"""保存配置到文件"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
logger.debug(f"配置保存到: {file_path}")
except Exception as e:
logger.error(f"保存配置到 {file_path} 时出错: {str(e)}", exc_info=True)
def get_config(self, config_name: str) -> Dict:
"""获取配置"""
try:
config = self.configs.get(config_name)
if not config:
logger.error(f"配置不存在: {config_name}")
raise ValueError(f"配置 {config_name} 不存在")
logger.debug(f"获取配置: {config_name}")
return config
except Exception as e:
logger.error(f"获取配置 {config_name} 时出错: {str(e)}", exc_info=True)
raise
def shutdown(self):
"""关闭初始化服务"""
try:
logger.info("关闭初始化服务")
self.configs.clear()
logger.debug("初始化服务关闭完成")
except Exception as e:
logger.error(f"关闭初始化服务时出错: {str(e)}", exc_info=True)
+464
View File
@@ -0,0 +1,464 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, List, Callable, Any, Optional
from pathlib import Path
import aiohttp
from aiohttp import web
import json
import ssl
logger = logging.getLogger(__name__)
class InternetService:
"""网络服务 - 管理HTTP和WebSocket服务器"""
def __init__(self, config: Dict, service_manager):
self.config = config
self.service_manager = service_manager
self.http_app = web.Application()
self.http_runner = None
self.ws_connections: Dict[str, List] = {}
self.plugin_routes: Dict[str, List] = {}
self.is_running = False
# 从配置获取端口
internet_config = config.get('internet', {})
ws_config = internet_config.get('websocket', {})
self.ws_host = ws_config.get('host', '0.0.0.0')
self.ws_port = ws_config.get('port', 8765)
http_config = internet_config.get('http', {})
self.http_host = http_config.get('host', '0.0.0.0')
self.http_port = http_config.get('port', 8000)
logger.debug("InternetService初始化开始")
async def start(self):
"""启动网络服务 - 精准错误处理与端口复用版"""
try:
logger.info("启动网络服务")
self._setup_default_routes()
# 初始化 AppRunner
self.http_runner = web.AppRunner(self.http_app)
await self.http_runner.setup()
# 1. 启动 HTTP 站点
try:
self.site = web.TCPSite(
self.http_runner, self.http_host, self.http_port,
reuse_address=True, reuse_port=True
)
await self.site.start()
logger.info(f"✅ HTTP 服务已绑定: {self.http_host}:{self.http_port}")
except OSError as e:
logger.error(f"❌ HTTP 端口 {self.http_port} 绑定失败: {e}")
await self.http_runner.cleanup()
return False
# 2. 启动 WebSocket 站点 (独立端口)
try:
self.ws_site = web.TCPSite(
self.http_runner, self.ws_host, self.ws_port,
reuse_address=True, reuse_port=True
)
await self.ws_site.start()
logger.info(f"✅ WebSocket 服务已绑定: {self.ws_host}:{self.ws_port}")
except OSError as e:
logger.error(f"❌ WebSocket 端口 {self.ws_port} 绑定失败: {e}")
logger.warning("💡 WS端口可能处于 TIME_WAIT,请等待30秒或更换 config 中的 websocket.port")
await self.http_runner.cleanup() # 回滚已启动的 HTTP
return False
await self.save_network_config()
self.is_running = True
logger.info("🌐 网络服务启动完成")
return True
except Exception as e:
logger.error(f"❌ 启动网络服务异常: {str(e)}", exc_info=True)
return False
async def check_service_health(self) -> Dict[str, Any]:
"""检查服务健康状况"""
try:
import socket
health_info = {
"is_running": self.is_running,
"http_port": self.http_port,
"websocket_port": self.ws_port,
"http_active": False,
"dependencies_available": self._check_dependencies(),
"error": None
}
# 检查端口是否在监听
if self.is_running:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('localhost', self.http_port))
sock.close()
health_info["http_active"] = (result == 0)
except Exception as e:
health_info["error"] = f"端口检查失败: {str(e)}"
return health_info
except Exception as e:
return {
"is_running": False,
"error": f"健康检查失败: {str(e)}"
}
def _check_dependencies(self) -> bool:
"""检查必要的依赖包"""
try:
import aiohttp
import yaml
return True
except ImportError as e:
logger.error(f"❌ 缺少依赖包: {str(e)}")
return False
async def save_network_config(self):
"""保存网络配置"""
try:
config_path = Path("config") / "services" / "network_routes.yaml"
config_path.parent.mkdir(parents=True, exist_ok=True)
config_data = {
"plugin_routes": self.get_plugin_routes(),
"last_updated": asyncio.get_event_loop().time(),
"http_port": self.http_port,
"websocket_port": self.ws_port
}
import yaml
with open(config_path, 'w', encoding='utf-8') as f:
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2)
logger.info(f"网络配置已保存: {config_path}")
except Exception as e:
logger.error(f"保存网络配置时出错: {str(e)}")
def _setup_default_routes(self):
"""设置默认路由"""
# 健康检查端点
self.http_app.router.add_get('/health', self._handle_health_check)
# 插件API端点
self.http_app.router.add_get('/api/plugins', self._handle_get_plugins)
self.http_app.router.add_get('/api/commands', self._handle_get_commands)
# 数据接收端点
self.http_app.router.add_post('/api/data', self._handle_data_receive)
logger.debug("默认路由设置完成")
async def register_plugin_route(self, plugin_name: str, route_path: str,
handler: Callable, methods: List[str] = ["GET"],
require_auth: bool = True):
"""为插件注册HTTP路由 - 修复冻结路由器问题"""
try:
# 规范化路径
if not route_path.startswith('/'):
route_path = '/' + route_path
full_path = f"/{plugin_name}{route_path}"
# 创建包装器处理权限验证
async def wrapped_handler(request):
try:
# 权限验证
if require_auth:
auth_result = await self._check_plugin_auth(plugin_name, request)
if not auth_result['allowed']:
return web.json_response(
{"error": "权限不足", "details": auth_result['reason']},
status=403
)
# 调用插件处理器
return await handler(request)
except Exception as e:
logger.error(f"插件路由处理出错 {full_path}: {str(e)}")
return web.json_response(
{"error": "内部服务器错误", "details": str(e)},
status=500
)
# 检查路由器是否已冻结
if hasattr(self.http_app.router, '_frozen') and self.http_app.router._frozen:
logger.warning(f"路由器已冻结,无法注册新路由: {full_path}")
logger.info("💡 建议: 在启动网络服务前注册所有插件路由")
return
# 注册路由
for method in methods:
self.http_app.router.add_route(method.upper(), full_path, wrapped_handler)
# 记录路由信息
if plugin_name not in self.plugin_routes:
self.plugin_routes[plugin_name] = []
self.plugin_routes[plugin_name].append({
'path': full_path,
'methods': methods,
'require_auth': require_auth
})
logger.info(f"注册插件路由: {plugin_name} -> {full_path} [{','.join(methods)}]")
except RuntimeError as e:
if "frozen router" in str(e):
logger.error(f"❌ 无法注册路由 {full_path}: 路由器已冻结")
logger.info("💡 解决方案: 在启动网络服务前注册插件路由")
else:
logger.error(f"注册插件路由时出错: {str(e)}", exc_info=True)
raise
except Exception as e:
logger.error(f"注册插件路由时出错: {str(e)}", exc_info=True)
raise
async def register_plugin_websocket(self, plugin_name: str, ws_path: str,
handler: Callable, require_auth: bool = True):
"""为插件注册WebSocket路由"""
try:
# 规范化路径
if not ws_path.startswith('/'):
ws_path = '/' + ws_path
full_path = f"/plugin/{plugin_name}/ws{ws_path}"
async def websocket_handler(request):
try:
# 权限验证
if require_auth:
auth_result = await self._check_plugin_auth(plugin_name, request)
if not auth_result['allowed']:
return web.json_response(
{"error": "WebSocket连接权限不足"},
status=403
)
# 建立WebSocket连接
ws = web.WebSocketResponse()
await ws.prepare(request)
# 记录连接
connection_id = f"{plugin_name}_{id(ws)}"
if plugin_name not in self.ws_connections:
self.ws_connections[plugin_name] = []
self.ws_connections[plugin_name].append(ws)
logger.debug(f"WebSocket连接建立: {connection_id}")
# 调用插件处理器
await handler(ws, request)
# 清理连接
self.ws_connections[plugin_name].remove(ws)
return ws
except Exception as e:
logger.error(f"WebSocket处理出错 {full_path}: {str(e)}")
return web.json_response(
{"error": "WebSocket连接失败"},
status=500
)
# 注册WebSocket路由
self.http_app.router.add_route('GET', full_path, websocket_handler)
# 记录路由信息
if plugin_name not in self.plugin_routes:
self.plugin_routes[plugin_name] = []
self.plugin_routes[plugin_name].append({
'path': full_path,
'methods': ['WEBSOCKET'],
'require_auth': require_auth
})
logger.info(f"注册插件WebSocket: {plugin_name} -> {full_path}")
except Exception as e:
logger.error(f"注册插件WebSocket时出错: {str(e)}", exc_info=True)
raise
async def _check_plugin_auth(self, plugin_name: str, request) -> Dict[str, Any]:
"""检查插件权限"""
try:
# 获取权限服务
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return {"allowed": False, "reason": "权限服务不可用"}
# 检查插件是否有网络访问权限
if not permission_service.has_permission(plugin_name, "plugin.network.access"):
return {"allowed": False, "reason": "插件没有网络访问权限"}
# 检查API密钥(如果配置了)
api_key = request.headers.get('X-API-Key')
if api_key:
# 验证API密钥逻辑
valid_keys = self.config.get('api_keys', [])
if api_key not in valid_keys:
return {"allowed": False, "reason": "无效的API密钥"}
return {"allowed": True, "reason": "权限验证通过"}
except Exception as e:
logger.error(f"权限检查时出错: {str(e)}")
return {"allowed": False, "reason": "权限检查失败"}
async def broadcast_to_websockets(self, plugin_name: str, message: Dict):
"""向插件的所有WebSocket连接广播消息"""
try:
if plugin_name not in self.ws_connections:
return
message_json = json.dumps(message, ensure_ascii=False)
disconnected = []
for ws in self.ws_connections[plugin_name]:
try:
if not ws.closed:
await ws.send_str(message_json)
else:
disconnected.append(ws)
except Exception as e:
logger.error(f"WebSocket广播消息失败: {str(e)}")
disconnected.append(ws)
# 清理断开连接的WebSocket
for ws in disconnected:
self.ws_connections[plugin_name].remove(ws)
logger.debug(f"WebSocket广播完成: {plugin_name} -> {len(self.ws_connections[plugin_name])} 个连接")
except Exception as e:
logger.error(f"WebSocket广播时出错: {str(e)}", exc_info=True)
# 默认路由处理器
async def _handle_health_check(self, request):
"""健康检查端点"""
return web.json_response({
"status": "healthy",
"service": "internet",
"timestamp": asyncio.get_event_loop().time()
})
async def _handle_get_plugins(self, request):
"""获取插件列表"""
try:
plugin_service = self.service_manager.get_service("plugin")
if not plugin_service:
return web.json_response({"error": "插件服务不可用"}, status=503)
plugins_info = []
for name, info in plugin_service.plugin_info.items():
plugins_info.append({
"name": name,
"version": info.version,
"description": info.description,
"enabled": info.enabled,
"loaded": info.loaded
})
return web.json_response({
"plugins": plugins_info,
"count": len(plugins_info)
})
except Exception as e:
logger.error(f"获取插件列表时出错: {str(e)}")
return web.json_response({"error": "内部服务器错误"}, status=500)
async def _handle_get_commands(self, request):
"""获取命令列表"""
try:
command_service = self.service_manager.get_service("command")
if not command_service:
return web.json_response({"error": "命令服务不可用"}, status=503)
command_list = command_service.get_command_list()
return web.json_response({
"commands": command_list,
"count": len(command_list)
})
except Exception as e:
logger.error(f"获取命令列表时出错: {str(e)}")
return web.json_response({"error": "内部服务器错误"}, status=500)
async def _handle_data_receive(self, request):
"""处理跨端数据传输"""
try:
data = await request.json()
# 获取插件桥接服务
plugin_bridge = self.service_manager.get_service("plugin_bridge")
if plugin_bridge:
# 广播数据到所有插件
await plugin_bridge.broadcast_to_plugins(
"network.data.receive",
{
"source": request.remote,
"data": data,
"timestamp": asyncio.get_event_loop().time()
}
)
return web.json_response({
"status": "success",
"message": "数据接收成功",
"timestamp": asyncio.get_event_loop().time()
})
except Exception as e:
logger.error(f"处理跨端数据时出错: {str(e)}")
return web.json_response({
"error": "数据接收失败",
"details": str(e)
}, status=400)
def get_plugin_routes(self, plugin_name: str = None) -> Dict:
"""获取插件路由信息"""
if plugin_name:
return self.plugin_routes.get(plugin_name, [])
else:
return self.plugin_routes.copy()
async def shutdown(self):
"""关闭网络服务"""
try:
logger.info("关闭网络服务")
self.is_running = False
# 关闭所有WebSocket连接
for plugin_name, connections in self.ws_connections.items():
for ws in connections:
if not ws.closed:
await ws.close()
self.ws_connections[plugin_name].clear()
# 关闭HTTP服务器
if self.http_runner:
await self.http_runner.cleanup()
logger.debug("网络服务关闭完成")
except Exception as e:
logger.error(f"关闭网络服务时出错: {str(e)}", exc_info=True)
+332
View File
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import logging.handlers
import os
import asyncio
from pathlib import Path
from typing import Dict, List, Callable
import json
from datetime import datetime
logger = logging.getLogger(__name__)
class LogService:
"""日志服务"""
def __init__(self, config: Dict):
self.config = config
self.log_consumers: List[Callable] = []
self.log_buffer: List[Dict] = []
self.buffer_size = 100
self.log_dir = Path("logs")
self.is_initialized = False
self._in_emit = False
# 文件数量限制
self.max_log_files_per_folder = self.config['logging']['max_log_files'] # 每个文件夹文件数量上限
# 立即初始化日志系统
self._setup_logging_sync()
def _setup_logging_sync(self):
"""同步设置日志系统 - 增强版"""
try:
# 创建日志目录
self.log_dir.mkdir(exist_ok=True)
(self.log_dir / "debug").mkdir(exist_ok=True)
(self.log_dir / "runtime").mkdir(exist_ok=True)
# 清理旧日志文件(在创建新文件之前)
self._cleanup_old_log_files()
# 生成基于时间戳和会话ID的日志文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
session_id = os.urandom(4).hex() # 生成8位随机会话ID
runtime_log_file = f"framework_{timestamp}_{session_id}.log"
debug_log_file = f"debug_{timestamp}_{session_id}.log"
# 保存当前会话的日志文件名(用于后续引用)
self.current_session_logs = {
'runtime': runtime_log_file,
'debug': debug_log_file,
'timestamp': timestamp,
'session_id': session_id
}
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
# 清除现有处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建过滤器实例
shared_filter = self.SafeLogFilter(self)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO))
console_formatter = logging.Formatter(
'%(asctime)s [%(levelname)-8s] %(name)s: %(message)s',
datefmt='%H:%M:%S'
)
console_handler.setFormatter(console_formatter)
console_handler.addFilter(shared_filter)
root_logger.addHandler(console_handler)
# 文件处理器
if self.config['logging'].get('enable_file_logging', True):
file_formatter = logging.Formatter(
'%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 运行时文件处理器
runtime_handler = logging.handlers.RotatingFileHandler(
self.log_dir / "runtime" / runtime_log_file,
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
backupCount=self.config['logging'].get('max_log_files', 3)
)
runtime_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO))
runtime_handler.setFormatter(file_formatter)
runtime_handler.addFilter(shared_filter)
root_logger.addHandler(runtime_handler)
# Debug文件处理器
if self.config['logging'].get('debug_level_file', True):
debug_handler = logging.handlers.RotatingFileHandler(
self.log_dir / "debug" / debug_log_file,
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
backupCount=self.config['logging'].get('max_log_files', 3)
)
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(file_formatter)
debug_handler.addFilter(shared_filter)
root_logger.addHandler(debug_handler)
self.is_initialized = True
logger.info(f"✅ 日志系统初始化完成 - 会话ID: {session_id}")
logger.info(f"📝 运行时日志: logs/runtime/{runtime_log_file}")
logger.info(f"🐛 调试日志: logs/debug/{debug_log_file}")
except Exception as e:
logger.error(f"日志系统设置失败: {e}")
# 回退到基础配置
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
self.is_initialized = True
def _cleanup_old_log_files(self):
"""清理旧的日志文件,保持每个文件夹文件上限"""
try:
logger.debug("开始清理旧日志文件...")
# 清理 runtime 文件夹
runtime_dir = self.log_dir / "runtime"
if runtime_dir.exists():
runtime_files = list(runtime_dir.glob("*.log"))
self._remove_old_files(runtime_files, "runtime")
# 清理 debug 文件夹
debug_dir = self.log_dir / "debug"
if debug_dir.exists():
debug_files = list(debug_dir.glob("*.log"))
self._remove_old_files(debug_files, "debug")
except Exception as e:
logger.error(f"清理旧日志文件时出错: {e}")
def _remove_old_files(self, files: List[Path], folder_name: str):
"""删除最旧的文件,直到文件数量不超过限制"""
try:
if len(files) <= self.max_log_files_per_folder:
logger.debug(f"{folder_name} 文件夹文件数量正常: {len(files)}/{self.max_log_files_per_folder}")
return
# 按修改时间排序(最旧的在前)
files_sorted = sorted(files, key=lambda x: x.stat().st_mtime)
# 计算需要删除的文件数量
files_to_remove = len(files_sorted) - self.max_log_files_per_folder
if files_to_remove > 0:
logger.info(f"清理 {folder_name} 文件夹: 删除 {files_to_remove} 个旧日志文件")
for i in range(files_to_remove):
old_file = files_sorted[i]
try:
old_file.unlink()
logger.debug(f"删除旧日志文件: {old_file.name}")
except Exception as e:
logger.error(f"删除文件失败 {old_file}: {e}")
logger.info(f"{folder_name} 文件夹清理完成: {self.max_log_files_per_folder} 个文件")
except Exception as e:
logger.error(f"删除 {folder_name} 文件夹旧文件时出错: {e}")
def cleanup_log_files(self):
"""手动清理日志文件(可以定期调用)"""
try:
logger.info("开始手动清理日志文件...")
self._cleanup_old_log_files()
logger.info("日志文件清理完成")
except Exception as e:
logger.error(f"手动清理日志文件时出错: {e}")
def get_log_file_counts(self) -> Dict[str, int]:
"""获取当前日志文件数量统计"""
try:
runtime_count = len(list((self.log_dir / "runtime").glob("*.log")))
debug_count = len(list((self.log_dir / "debug").glob("*.log")))
return {
"runtime": runtime_count,
"debug": debug_count,
"max_limit": self.max_log_files_per_folder
}
except Exception as e:
logger.error(f"获取日志文件统计时出错: {e}")
return {"runtime": 0, "debug": 0, "max_limit": self.max_log_files_per_folder}
def get_current_session_info(self) -> Dict:
"""获取当前会话的日志信息"""
return getattr(self, 'current_session_logs', {})
def _parse_size(self, size_str: str) -> int:
"""解析文件大小字符串"""
try:
units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3}
number = ''.join(filter(str.isdigit, size_str))
unit = ''.join(filter(str.isalpha, size_str)).upper()
return int(number) * units.get(unit, 1)
except Exception:
return 10 * 1024 * 1024
def add_log_consumer(self, callback: Callable):
"""添加日志消费者"""
if callback not in self.log_consumers:
self.log_consumers.append(callback)
logger.debug(f"添加日志消费者,总数: {len(self.log_consumers)}")
def emit_log(self, log_record: Dict):
"""发射日志到消费者"""
if self._in_emit:
return
self._in_emit = True
try:
# 格式化日志记录
timestamp = log_record['timestamp']
level = log_record['level']
name = log_record['name']
message = log_record['message']
# 转换时间戳
if isinstance(timestamp, (int, float)):
timestamp_str = datetime.fromtimestamp(timestamp).strftime('%H:%M:%S')
else:
timestamp_str = str(timestamp)
formatted_record = {
'timestamp': timestamp,
'timestamp_str': timestamp_str,
'level': level,
'name': name,
'message': message,
'module': log_record.get('module', ''),
'line': log_record.get('line', 0),
'formatted_message': f"{timestamp_str} [{level:8}] {name}: {message}",
'simple_message': f"{timestamp_str} [{level:8}] {message}",
'original_message': message
}
# 添加到缓冲区
self.log_buffer.append(formatted_record)
if len(self.log_buffer) > self.buffer_size:
self.log_buffer.pop(0)
# 发送给消费者
for consumer in self.log_consumers:
try:
# 检查TUI级别过滤
if hasattr(consumer, '_tui_level_filter'):
tui_level = consumer._tui_level_filter
if self._should_display_for_tui(level, tui_level):
consumer(formatted_record)
else:
# 默认发送所有日志
consumer(formatted_record)
except Exception as e:
logger.error(f"日志消费者处理出错: {e}")
except Exception as e:
logger.error(f"发射日志时出错: {e}")
finally:
self._in_emit = False
def _should_display_for_tui(self, log_level: str, tui_level: str) -> bool:
"""检查日志是否应该显示在TUI中"""
level_priority = {
'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50
}
log_priority = level_priority.get(log_level, 0)
tui_priority = level_priority.get(tui_level, 0)
return log_priority >= tui_priority
def add_tui_log_consumer(self, callback: Callable, tui_level: str = "INFO"):
"""专门为TUI添加日志消费者"""
callback._tui_level_filter = tui_level
if callback not in self.log_consumers:
self.log_consumers.append(callback)
logger.debug(f"添加TUI日志消费者,级别: {tui_level}")
def get_recent_logs(self, count: int = 50) -> List[Dict]:
"""获取最近的日志"""
return self.log_buffer[-count:]
def shutdown(self):
"""关闭日志服务"""
self.log_consumers.clear()
logging.shutdown()
logger.info("日志服务已关闭")
class SafeLogFilter(logging.Filter):
"""安全的日志过滤器"""
def __init__(self, log_service):
super().__init__()
self.log_service = log_service
self._in_filter = False
def filter(self, record):
"""过滤日志记录"""
if self._in_filter:
return True
self._in_filter = True
try:
# 创建日志记录
log_record = {
'timestamp': record.created,
'name': record.name,
'level': record.levelname,
'message': record.getMessage(),
'module': record.module,
'line': record.lineno
}
# 发射日志记录
self.log_service.emit_log(log_record)
except Exception:
pass
finally:
self._in_filter = False
return True
File diff suppressed because it is too large Load Diff
+476
View File
@@ -0,0 +1,476 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import importlib.util
import sys
import inspect
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable
from dataclasses import dataclass
import yaml
import traceback
from fmfuncs.plugin_command_decorator import plugin_command, command
logger = logging.getLogger(__name__)
@dataclass
class PluginInfo:
"""插件信息数据类"""
name: str
version: str
description: str
author: str
enabled: bool
loaded: bool
error_count: int
permissions: List[str]
plugin_path: Path
commands: Dict[str, Dict] = None # 新增命令信息
class PluginService:
"""插件服务 - 管理插件的加载、卸载和运行"""
def __init__(self, config: Dict, permission_service, bridge_service, service_manager):
self.config = config
self.permission_service = permission_service
self.bridge_service = bridge_service
self.service_manager = service_manager # 新增服务管理器
self.bridge_service.service_manager = self.service_manager
self.plugins: Dict[str, Any] = {}
self.plugin_info: Dict[str, PluginInfo] = {}
self.plugins_dir = Path("plugins")
self.is_running = False
logger.debug("PluginService初始化开始")
async def start(self):
"""启动插件服务"""
try:
logger.info("启动插件服务")
# 创建插件目录
self.plugins_dir.mkdir(exist_ok=True)
# 自动加载插件
if self.config['plugins']['auto_load']:
await self.load_all_plugins()
await self.save_command_config()
self.is_running = True
logger.info("插件服务启动完成")
except Exception as e:
logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True)
raise
async def load_all_plugins(self):
"""加载所有插件"""
try:
logger.debug("开始加载所有插件")
if not self.plugins_dir.exists():
logger.warning("插件目录不存在,跳过加载")
return
loaded_count = 0
error_count = 0
# 遍历插件目录
for plugin_dir in self.plugins_dir.iterdir():
if plugin_dir.is_dir():
try:
success = await self.load_plugin(plugin_dir.name)
if success:
loaded_count += 1
else:
error_count += 1
except Exception as e:
logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True)
error_count += 1
logger.info(f"插件加载完成: 成功 {loaded_count}, 失败 {error_count}")
except Exception as e:
logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True)
raise
async def load_plugin(self, plugin_name: str) -> bool:
"""加载单个插件 - 支持异步权限处理"""
try:
logger.debug(f"开始加载插件: {plugin_name}")
plugin_path = self.plugins_dir / plugin_name
if not plugin_path.exists():
logger.error(f"插件目录不存在: {plugin_path}")
return False
# 检查插件配置文件
config_file = plugin_path / "config.yaml"
if not config_file.exists():
logger.error(f"插件配置文件不存在: {config_file}")
return False
# 加载插件配置
with open(config_file, 'r', encoding='utf-8') as f:
plugin_config = yaml.safe_load(f)
# 检查权限文件
permission_file = plugin_path / "permissions.yaml"
if not permission_file.exists():
logger.error(f"插件权限文件不存在: {permission_file}")
return False
# 加载权限配置
with open(permission_file, 'r', encoding='utf-8') as f:
permission_config = yaml.safe_load(f)
# 验证插件信息
required_fields = ['name', 'version', 'description', 'author']
for field in required_fields:
if field not in plugin_config:
logger.error(f"插件配置缺少必要字段: {field}")
return False
# 检查主模块
main_module = plugin_path / "__init__.py"
if not main_module.exists():
logger.error(f"插件主模块不存在: {main_module}")
return False
# 动态加载插件模块
module_name = f"plugins.{plugin_name}"
spec = importlib.util.spec_from_file_location(module_name, main_module)
if not spec:
logger.error(f"无法创建模块规范: {module_name}")
return False
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
logger.debug(f"插件模块加载成功: {module_name}")
except Exception as e:
logger.error(f"执行插件模块时出错: {str(e)}", exc_info=True)
return False
# 获取插件类实例
if not hasattr(module, 'Plugin'):
logger.error(f"插件类 'Plugin' 不存在: {module_name}")
return False
# 权限申请和验证 - 非阻塞版本
permissions = permission_config.get('permissions', [])
if permissions:
# 非阻塞权限请求,立即返回True让插件继续加载
permission_result = await self.permission_service.request_permissions(plugin_name, permissions)
if not permission_result:
logger.warning(f"插件权限申请失败: {plugin_name}")
# 即使权限申请失败,也允许插件以受限模式运行
logger.info(f"插件 {plugin_name} 将以受限模式运行")
# 实例化插件
try:
plugin_instance = module.Plugin(
plugin_name=plugin_name,
config=plugin_config,
bridge=self.bridge_service
)
# 初始化插件
if hasattr(plugin_instance, 'initialize'):
if asyncio.iscoroutinefunction(plugin_instance.initialize):
await plugin_instance.initialize()
else:
plugin_instance.initialize()
# 扫描并注册插件命令
plugin_commands = await self._scan_and_register_commands(plugin_name, plugin_instance, plugin_config)
# 注册插件
self.plugins[plugin_name] = plugin_instance
# 保存插件信息
self.plugin_info[plugin_name] = PluginInfo(
name=plugin_config['name'],
version=plugin_config['version'],
description=plugin_config['description'],
author=plugin_config['author'],
enabled=True,
loaded=True,
error_count=0,
permissions=permissions,
plugin_path=plugin_path,
commands=plugin_commands
)
logger.info(f"插件加载成功: {plugin_name} v{plugin_config['version']}, 注册了 {len(plugin_commands)} 个命令")
return True
except Exception as e:
logger.error(f"实例化插件时出错: {str(e)}", exc_info=True)
return False
except Exception as e:
logger.error(f"加载插件 {plugin_name} 时出错: {str(e)}", exc_info=True)
return False
async def _scan_and_register_commands(self, plugin_name: str, plugin_instance: Any, plugin_config: Dict) -> Dict[str, Dict]:
"""扫描并注册插件命令 - 修正版本"""
try:
logger.debug(f"扫描插件命令: {plugin_name}")
command_service = self.service_manager.get_service("command")
if not command_service:
logger.error("命令服务不可用,无法注册插件命令")
return {}
# 扫描插件中的命令方法
command_methods = {}
for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod):
# 检查方法是否有命令装饰器或符合命名约定
if (hasattr(method, '_is_plugin_command') or
name.startswith('cmd_') or
name.startswith('command_')):
command_name = self._get_command_name(name, method, plugin_config)
command_description = self._get_command_description(name, method, plugin_config)
command_permissions = self._get_command_permissions(name, method, plugin_config)
# 修正:使用正确的source格式
command_service.register_command(
name=command_name,
handler=method,
description=command_description,
permissions=command_permissions,
source=f"plugin.{plugin_name}" # 使用 plugin.插件名 格式
)
command_methods[command_name] = {
'method_name': name,
'description': command_description,
'permissions': command_permissions
}
logger.debug(f"注册插件命令: {command_name} -> {name}")
return command_methods
except Exception as e:
logger.error(f"扫描插件命令时出错: {str(e)}", exc_info=True)
return {}
def _get_command_name(self, method_name: str, method: Callable, plugin_config: Dict) -> str:
"""获取命令名称"""
try:
# 如果方法有装饰器指定的名称
if hasattr(method, '_command_name'):
return getattr(method, '_command_name')
# 从方法名提取命令名
if method_name.startswith('cmd_'):
return method_name[4:]
elif method_name.startswith('command_'):
return method_name[8:]
else:
return method_name
except Exception as e:
logger.error(f"获取命令名称时出错: {str(e)}")
return method_name
def _get_command_description(self, method_name: str, method: Callable, plugin_config: Dict) -> str:
"""获取命令描述"""
try:
# 如果方法有装饰器指定的描述
if hasattr(method, '_command_description'):
return getattr(method, '_command_description')
# 使用方法的文档字符串
if method.__doc__:
# 提取第一行作为描述
doc_lines = method.__doc__.strip().split('\n')
return doc_lines[0].strip()
# 默认描述
return f"插件命令: {method_name}"
except Exception as e:
logger.error(f"获取命令描述时出错: {str(e)}")
return f"插件命令: {method_name}"
def _get_command_permissions(self, method_name: str, method: Callable, plugin_config: Dict) -> List[str]:
"""获取命令权限"""
try:
# 如果方法有装饰器指定的权限
if hasattr(method, '_command_permissions'):
return getattr(method, '_command_permissions')
# 从插件配置中获取默认权限
default_permissions = plugin_config.get('default_command_permissions', [])
return default_permissions.copy()
except Exception as e:
logger.error(f"获取命令权限时出错: {str(e)}")
return []
async def unload_plugin(self, plugin_name: str) -> bool:
"""卸载插件"""
try:
logger.debug(f"开始卸载插件: {plugin_name}")
if plugin_name not in self.plugins:
logger.warning(f"插件未加载: {plugin_name}")
return False
plugin_instance = self.plugins[plugin_name]
plugin_info = self.plugin_info[plugin_name]
# 注销插件命令
await self._unregister_plugin_commands(plugin_name)
# 调用插件的清理方法
try:
if hasattr(plugin_instance, 'shutdown'):
if asyncio.iscoroutinefunction(plugin_instance.shutdown):
await plugin_instance.shutdown()
else:
plugin_instance.shutdown()
except Exception as e:
logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True)
# 从模块缓存中移除
module_name = f"plugins.{plugin_name}"
if module_name in sys.modules:
del sys.modules[module_name]
# 移除插件实例和信息
del self.plugins[plugin_name]
plugin_info.loaded = False
plugin_info.enabled = False
logger.info(f"插件卸载成功: {plugin_name}")
return True
except Exception as e:
logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True)
return False
async def _unregister_plugin_commands(self, plugin_name: str):
"""注销插件命令"""
try:
command_service = self.service_manager.get_service("command")
if not command_service:
return
# 从命令服务中移除该插件的所有命令
commands_to_remove = []
for cmd_name, cmd_info in command_service.commands.items():
if cmd_info.source.startswith(f"plugin.{plugin_name}"):
commands_to_remove.append(cmd_name)
for cmd_name in commands_to_remove:
del command_service.commands[cmd_name]
logger.debug(f"注销插件命令: {cmd_name}")
logger.info(f"已注销插件 {plugin_name}{len(commands_to_remove)} 个命令")
except Exception as e:
logger.error(f"注销插件命令时出错: {str(e)}", exc_info=True)
async def save_command_config(self):
"""保存命令配置到文件"""
try:
command_service = self.service_manager.get_service("command")
if not command_service:
logger.error("命令服务不可用")
return False
command_list = command_service.get_command_list()
config_path = Path("config") / "plugins" / "commands.yaml"
# 确保目录存在
config_path.parent.mkdir(parents=True, exist_ok=True)
config_data = {
"commands": {},
"plugin_commands": {},
"last_updated": asyncio.get_event_loop().time(),
"total_commands": len(command_list)
}
# 按来源分组命令
for cmd in command_list:
cmd_info = {
"description": cmd['description'],
"permissions": cmd['permissions'],
"source": cmd['source']
}
config_data["commands"][cmd['name']] = cmd_info
# 按插件分组
if cmd['source'].startswith("plugin."):
plugin_name = cmd['source'].split('.', 1)[1]
if plugin_name not in config_data["plugin_commands"]:
config_data["plugin_commands"][plugin_name] = {}
config_data["plugin_commands"][plugin_name][cmd['name']] = cmd_info
with open(config_path, 'w', encoding='utf-8') as f:
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2)
logger.info(f"命令配置已保存: {config_path}, 共 {len(command_list)} 个命令")
return True
except Exception as e:
logger.error(f"保存命令配置时出错: {str(e)}", exc_info=True)
return False
async def register_delayed_routes(self, internet_service):
"""注册延迟的路由(在网络服务启动后)"""
try:
if not internet_service:
logger.warning("网络服务不可用,跳过延迟路由注册")
return
for plugin_name, plugin_instance in self.plugins.items():
try:
# 检查插件是否有延迟注册方法
if hasattr(plugin_instance, 'register_delayed_routes'):
await plugin_instance.register_delayed_routes(internet_service)
logger.info(f"延迟注册插件路由: {plugin_name}")
else:
# 如果插件没有延迟注册方法,尝试重新初始化网络功能
await self._reinitialize_plugin_network(plugin_instance, internet_service)
except Exception as e:
logger.error(f"延迟注册插件 {plugin_name} 路由时出错: {str(e)}")
except Exception as e:
logger.error(f"注册延迟路由时出错: {str(e)}")
async def _reinitialize_plugin_network(self, plugin_instance, internet_service):
"""重新初始化插件的网络功能"""
try:
plugin_name = plugin_instance.plugin_name
# 检查插件是否有网络桥接
if hasattr(plugin_instance, 'network_bridge'):
# 重新创建网络桥接
plugin_instance.network_bridge = PluginNetworkBridge(
plugin_name, internet_service, plugin_instance.bridge
)
# 重新设置网络路由
if hasattr(plugin_instance, '_setup_network_routes'):
await plugin_instance._setup_network_routes()
logger.info(f"重新初始化插件网络功能: {plugin_name}")
except Exception as e:
logger.error(f"重新初始化插件网络功能时出错: {str(e)}")
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import signal
import sys
from typing import List, Callable
from enum import Enum
logger = logging.getLogger(__name__)
class ShutdownPriority(Enum):
"""关闭优先级枚举"""
HIGHEST = 0
HIGH = 1
NORMAL = 2
LOW = 3
LOWEST = 4
class ShutdownService:
"""关闭服务 - 管理框架的优雅关闭"""
def __init__(self, service_manager):
self.service_manager = service_manager
self.shutdown_handlers: List[Callable] = []
self.is_shutting_down = False
self.shutdown_timeout = 30 # 秒
logger.debug("ShutdownService初始化开始")
# 注册信号处理
self._register_signal_handlers()
def _register_signal_handlers(self):
"""注册信号处理"""
try:
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
logger.debug("信号处理器注册完成")
except Exception as e:
logger.error(f"注册信号处理器时出错: {str(e)}", exc_info=True)
def _signal_handler(self, signum, frame):
"""信号处理函数"""
try:
signal_name = signal.Signals(signum).name
logger.info(f"接收到信号: {signal_name}")
asyncio.create_task(self.initiate_shutdown())
except Exception as e:
logger.error(f"处理信号时出错: {str(e)}", exc_info=True)
sys.exit(1)
def register_shutdown_handler(self, handler: Callable, priority: ShutdownPriority = ShutdownPriority.NORMAL):
"""注册关闭处理器"""
try:
self.shutdown_handlers.append((priority.value, handler))
# 按优先级排序
self.shutdown_handlers.sort(key=lambda x: x[0])
logger.debug(f"注册关闭处理器,优先级: {priority.name}, 当前总数: {len(self.shutdown_handlers)}")
except Exception as e:
logger.error(f"注册关闭处理器时出错: {str(e)}", exc_info=True)
async def initiate_shutdown(self, reason: str = "正常关闭"):
"""发起关闭流程"""
try:
if self.is_shutting_down:
logger.warning("关闭流程已在进行中")
return
self.is_shutting_down = True
logger.info(f"开始框架关闭流程 - 原因: {reason}")
# 执行关闭处理器
await self._execute_shutdown_handlers()
# 关闭服务管理器
self.service_manager.shutdown_all()
logger.info("框架关闭完成")
# 退出程序
sys.exit(0)
except Exception as e:
logger.error(f"关闭流程出错: {str(e)}", exc_info=True)
sys.exit(1)
async def _execute_shutdown_handlers(self):
"""执行关闭处理器"""
try:
logger.debug(f"开始执行 {len(self.shutdown_handlers)} 个关闭处理器")
for priority, handler in self.shutdown_handlers:
try:
handler_name = handler.__name__ if hasattr(handler, '__name__') else str(handler)
logger.debug(f"执行关闭处理器: {handler_name} (优先级: {priority})")
if asyncio.iscoroutinefunction(handler):
await asyncio.wait_for(handler(), timeout=self.shutdown_timeout)
else:
# 在事件循环中运行同步函数
await asyncio.get_event_loop().run_in_executor(None, handler)
logger.debug(f"关闭处理器完成: {handler_name}")
except asyncio.TimeoutError:
logger.error(f"关闭处理器超时: {handler_name}")
except Exception as e:
logger.error(f"关闭处理器出错 {handler_name}: {str(e)}", exc_info=True)
logger.debug("所有关闭处理器执行完成")
except Exception as e:
logger.error(f"执行关闭处理器时出错: {str(e)}", exc_info=True)
def emergency_shutdown(self):
"""紧急关闭"""
try:
logger.critical("执行紧急关闭")
sys.exit(1)
except Exception as e:
logger.critical(f"紧急关闭时出错: {str(e)}")
os._exit(1)
def shutdown(self):
"""关闭关闭服务"""
try:
logger.info("关闭ShutdownService")
self.shutdown_handlers.clear()
logger.debug("ShutdownService关闭完成")
except Exception as e:
logger.error(f"关闭ShutdownService时出错: {str(e)}", exc_info=True)
+806
View File
@@ -0,0 +1,806 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import sys
import io
import time
from textual.app import App
from textual.containers import Container, ScrollableContainer
from textual.widgets import Static, Input, Header, Footer
from textual.reactive import reactive
from typing import List, Dict
import asyncio
from datetime import datetime
logger = logging.getLogger(__name__)
class SystemExitGraceful(Exception):
"""优雅的系统退出异常"""
pass
class LogDisplay(Static):
"""日志显示组件 - 直接捕获所有日志输出"""
def __init__(self):
super().__init__("日志显示区域 - 等待日志输入...")
self.log_lines: List[str] = []
self.max_lines = 500
self.auto_scroll_enabled = True # 启用自动滚动
# 保存原始的logging处理器和格式器
self.original_handlers = []
self.original_formatters = {}
logger.debug("LogDisplay初始化完成")
def start_capture(self):
"""开始捕获所有日志输出"""
try:
# 获取根日志记录器
root_logger = logging.getLogger()
# 保存原始处理器和它们的格式器
self.original_handlers = root_logger.handlers.copy()
for handler in self.original_handlers:
self.original_formatters[handler] = handler.formatter
# 清除所有现有处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 添加我们的自定义处理器
custom_handler = self.TUILogHandler(self)
custom_handler.setLevel(logging.DEBUG) # 捕获所有级别的日志
# 强制使用包含彩色级别的格式器
formatter = self.ColoredFormatter(
'%(asctime)s %(levelname_color)s %(name)s: %(message)s',
datefmt='%H:%M:%S'
)
custom_handler.setFormatter(formatter)
root_logger.addHandler(custom_handler)
# 同时重定向stdout和stderr作为备份
self.original_stdout = sys.stdout
self.original_stderr = sys.stderr
sys.stdout = self.TUIOutput(self)
sys.stderr = self.TUIOutput(self, is_error=True)
print("✅ TUI日志捕获已启动 - 捕获所有日志输出")
except Exception as e:
logger.error(f"启动日志捕获失败: {e}")
def stop_capture(self):
"""停止捕获输出"""
try:
# 恢复logging处理器
root_logger = logging.getLogger()
# 移除我们的处理器
for handler in root_logger.handlers[:]:
if hasattr(handler, 'log_display'):
root_logger.removeHandler(handler)
# 恢复原始处理器和格式器
for handler in self.original_handlers:
# 恢复格式器
if handler in self.original_formatters:
handler.setFormatter(self.original_formatters[handler])
root_logger.addHandler(handler)
# 恢复stdout和stderr
sys.stdout = self.original_stdout
sys.stderr = self.original_stderr
print("🛑 TUI日志捕获已停止")
except Exception as e:
logger.error(f"停止日志捕获失败: {e}")
def add_log_line(self, line: str):
"""添加日志行到TUI显示"""
try:
# 添加到缓冲区
self.log_lines.append(line)
if len(self.log_lines) > self.max_lines:
self.log_lines.pop(0)
# 更新显示
display_content = "\n".join(self.log_lines)
self.update(display_content)
# 自动滚动到底部
if self.auto_scroll_enabled:
self.scroll_to_bottom()
except Exception as e:
# 如果TUI更新失败,回退到原始输出
if hasattr(self, 'original_stdout'):
self.original_stdout.write(f"TUI日志显示错误: {e}\n")
def scroll_to_bottom(self):
"""滚动到底部"""
try:
# 获取父容器(ScrollableContainer
parent = self.parent
if parent and hasattr(parent, 'scroll_end'):
parent.scroll_end()
except Exception as e:
# 忽略滚动错误,不影响主要功能
pass
def toggle_auto_scroll(self, enabled: bool = None):
"""切换自动滚动状态"""
if enabled is None:
self.auto_scroll_enabled = not self.auto_scroll_enabled
else:
self.auto_scroll_enabled = enabled
logger.debug(f"日志自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
return self.auto_scroll_enabled
class ColoredFormatter(logging.Formatter):
"""带颜色的日志格式器 - 增强版"""
# ANSI颜色代码
COLORS = {
'DEBUG': '\033[36m', # 青色 - DEBUG信息
'INFO': '\033[32m', # 绿色 - 正常信息
'WARNING': '\033[33m', # 黄色 - 警告信息
'ERROR': '\033[31m', # 红色 - 错误信息
'CRITICAL': '\033[35m', # 紫色 - 严重错误
'RESET': '\033[0m' # 重置颜色
}
# 级别显示宽度
LEVEL_WIDTH = 8
def format(self, record):
"""格式化日志记录,为级别添加颜色"""
try:
# 为级别添加颜色和固定宽度
levelname = record.levelname
if levelname in self.COLORS:
# 添加颜色并保持固定宽度
colored_level = f"{self.COLORS[levelname]}[{levelname:<{self.LEVEL_WIDTH}}]{self.COLORS['RESET']}"
record.levelname_color = colored_level
else:
record.levelname_color = f"[{levelname:<{self.LEVEL_WIDTH}}]"
# 调用父类格式化方法
formatted_message = super().format(record)
return formatted_message
except Exception:
# 如果格式化失败,返回简单格式
return f"{record.asctime} [{record.levelname}] {record.name}: {record.getMessage()}"
class TUILogHandler(logging.Handler):
"""自定义logging处理器,同时输出到终端和TUI"""
def __init__(self, log_display):
super().__init__()
self.log_display = log_display
def emit(self, record):
"""处理日志记录"""
try:
# 格式化日志记录(使用我们的格式器)
formatted_message = self.format(record)
# 输出到原始终端(通过原始处理器,但使用我们的格式器)
for original_handler in self.log_display.original_handlers:
if original_handler.level <= record.levelno:
# 临时使用我们的格式器来确保级别显示一致
original_handler.setFormatter(self.formatter)
original_handler.emit(record)
# 恢复原始格式器
original_formatter = self.log_display.original_formatters.get(original_handler)
if original_formatter:
original_handler.setFormatter(original_formatter)
# 添加到TUI显示
self.log_display.add_log_line(formatted_message)
except Exception as e:
# 如果处理失败,使用简单格式
try:
simple_message = f"{datetime.now().strftime('%H:%M:%S')} [{record.levelname:8}] {record.name}: {record.getMessage()}"
self.log_display.add_log_line(simple_message)
except:
pass
class TUIOutput(io.TextIOBase):
"""自定义输出流,捕获print等输出"""
def __init__(self, log_display, is_error=False):
self.log_display = log_display
self.is_error = is_error
self.original_stream = sys.stderr if is_error else sys.stdout
# 颜色定义
self.COLORS = {
'INFO': '\033[32m', # 绿色
'ERROR': '\033[31m', # 红色
'RESET': '\033[0m' # 重置颜色
}
def write(self, text):
"""写入文本"""
try:
# 写入到原始终端
self.original_stream.write(text)
self.original_stream.flush()
# 如果文本不是空的,添加到TUI
if text.strip():
# 添加简单的时间戳和级别
timestamp = datetime.now().strftime('%H:%M:%S')
level = "ERROR" if self.is_error else "INFO"
# 添加颜色
if level in self.COLORS:
colored_level = f"{self.COLORS[level]}[{level}]{self.COLORS['RESET']}"
else:
colored_level = f"[{level}]"
# 分割多行文本
lines = text.split('\n')
for line in lines:
if line.strip(): # 忽略空行
log_line = f"{timestamp} {colored_level} {line.strip()}"
self.log_display.add_log_line(log_line)
return len(text)
except Exception:
# 如果TUI处理失败,只输出到终端
self.original_stream.write(text)
self.original_stream.flush()
return len(text)
def flush(self):
"""刷新缓冲区"""
self.original_stream.flush()
def close(self):
"""关闭流"""
pass
class MessageDisplay(Static):
"""消息显示组件 - 增强版"""
def __init__(self):
super().__init__("消息区域")
self.current_messages: List[Dict] = []
self.max_messages = 200 # 更大的消息容量
self.auto_scroll_enabled = True # 启用自动滚动
# ANSI颜色代码
self.COLORS = {
'INFO': '\033[37m', # 黑底白字
'DEBUG': '\033[36m', # 青色 - DEBUG信息
'SUCCESS': '\033[32m', # 绿色 - 正常信息
'WARNING': '\033[33m', # 黄色 - 警告信息
'ERROR': '\033[31m', # 红色 - 错误信息
'COMMAND': '\033[40;37m', # 灰底白字 - 命令信息
'RESET': '\033[0m' # 重置颜色
}
self.message_types = {
'info': {'icon': f"{self.COLORS['INFO']}[INFO ]{self.COLORS['RESET']}", 'color': 'white'},
'success': {'icon': f"{self.COLORS['SUCCESS']}[SUCCESS]{self.COLORS['RESET']}", 'color': 'green'},
'error': {'icon': f"{self.COLORS['ERROR']}[ERROR ]{self.COLORS['RESET']}", 'color': 'red'},
'warning': {'icon': f"{self.COLORS['WARNING']}[WARNING]{self.COLORS['RESET']}", 'color': 'yellow'},
'debug': {'icon': f"{self.COLORS['DEBUG']}[DEBUG ]{self.COLORS['RESET']}", 'color': 'cyan'},
'command': {'icon': f"{self.COLORS['COMMAND']}[COMMAND]{self.COLORS['RESET']}", 'color': 'meow'}
}
logger.debug("MessageDisplay初始化完成")
def add_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""添加消息 - 支持多行消息"""
try:
# 分割多行消息为单独的消息
lines = message.strip().split('\n')
for line in lines:
if line.strip(): # 忽略空行
message_data = {
"text": line.strip(),
"type": msg_type,
"persistent": persistent,
"timestamp": asyncio.get_event_loop().time(),
"display_time": datetime.now().strftime('%H:%M:%S')
}
self.current_messages.append(message_data)
# 智能消息管理
self._manage_messages()
self._update_display()
except Exception as e:
logger.error(f"添加消息时出错: {str(e)}")
def reset_display(self):
"""重置显示状态"""
try:
# 清空所有消息
self.current_messages.clear()
# 更新显示
self.update("消息区域已重置")
# 强制刷新
self.refresh()
except Exception as e:
logger.error(f"重置消息显示时出错: {str(e)}")
def _manage_messages(self):
"""智能管理消息数量"""
try:
# 计算非持久化消息的数量
non_persistent_messages = [msg for msg in self.current_messages if not msg['persistent']]
if len(non_persistent_messages) > self.max_messages:
# 移除最旧的非持久化消息
for i, msg in enumerate(self.current_messages):
if not msg['persistent']:
self.current_messages.pop(i)
break
except Exception as e:
logger.error(f"管理消息时出错: {str(e)}")
def _update_display(self):
"""更新显示 - 带时间戳的格式化消息"""
try:
if not self.current_messages:
display_text = "📭 暂无消息"
else:
display_text = []
for msg in self.current_messages:
# 获取消息类型配置
msg_config = self.message_types.get(msg['type'], self.message_types['info'])
icon = msg_config['icon']
# 构建显示行
persistent_mark = "🔒 " if msg['persistent'] else ""
time_stamp = f"[{msg['display_time']}] " if len(self.current_messages) > 1 else ""
display_line = f"{time_stamp}{persistent_mark}{icon} {msg['text']}"
display_text.append(display_line)
display_text = "\n".join(display_text)
self.update(display_text)
# 自动滚动到底部
if self.auto_scroll_enabled:
self.scroll_to_bottom()
except Exception as e:
logger.error(f"更新消息显示时出错: {str(e)}")
def scroll_to_bottom(self):
"""滚动到底部"""
try:
# 获取父容器(ScrollableContainer
parent = self.parent
if parent and hasattr(parent, 'scroll_end'):
parent.scroll_end()
except Exception as e:
# 忽略滚动错误,不影响主要功能
pass
def toggle_auto_scroll(self, enabled: bool = None):
"""切换自动滚动状态"""
if enabled is None:
self.auto_scroll_enabled = not self.auto_scroll_enabled
else:
self.auto_scroll_enabled = enabled
logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
return self.auto_scroll_enabled
class TUIFramework(App):
"""TUI框架应用"""
def __init__(self, config, log_service, command_service):
super().__init__()
self.config = config
self.log_service = log_service
self.command_service = command_service
self.log_display = LogDisplay()
self.message_display = MessageDisplay()
self.command_input = None
self.CSS = self._generate_css()
def _generate_css(self):
"""根据配置动态生成CSS - 增强版"""
try:
tui_config = self.config.get('tui', {})
layout_config = tui_config.get('layout', {})
styles_config = tui_config.get('styles', {})
# 获取布局配置,使用默认值
grid_rows = layout_config.get('grid_rows', '7fr 2fr 1fr')
# 获取样式配置,使用默认值
log_area_style = styles_config.get('log_area', 'border: solid green; overflow-y: auto;')
message_area_style = styles_config.get('message_area', 'border: solid yellow; overflow-y: auto;')
input_area_style = styles_config.get('input_area', 'border: solid red;')
css = f"""
Screen {{
layout: grid;
grid-size: 1 3;
grid-rows: {grid_rows};
}}
#log-area {{
{log_area_style}
overflow-y: auto;
scrollbar-size: 1 1;
}}
#message-area {{
{message_area_style}
overflow-y: auto;
scrollbar-size: 1 1;
}}
#input-area {{
{input_area_style}
}}
/* 自定义滚动条样式 */
ScrollableContainer {{
scrollbar-color: #666 #222;
scrollbar-color-hover: #888 #333;
overflow-y: auto;
}}
/* 确保内容正确换行 */
Static {{
width: 100%;
content-align: left middle;
overflow-y: auto;
}}
"""
logger.debug(f"生成的TUI CSS:\n{css}")
return css
except Exception as e:
logger.error(f"生成TUI CSS时出错: {str(e)}", exc_info=True)
# 返回默认CSS作为回退
return """
Screen {
layout: grid;
grid-size: 1 3;
grid-rows: 7fr 2fr 1fr;
}
#log-area {
border: solid green;
overflow-y: auto;
scrollbar-size: 1 1;
}
#message-area {
border: solid yellow;
overflow-y: auto;
scrollbar-size: 1 1;
}
#input-area {
border: solid red;
}
/* 自定义滚动条样式 */
ScrollableContainer {
scrollbar-color: #666 #222;
scrollbar-color-hover: #888 #333;
overflow-y: auto;
}
/* 确保内容正确换行 */
Static {
width: 100%;
content-align: left middle;
overflow-y: auto;
}
"""
def compose(self):
"""组合界面"""
yield Header()
yield ScrollableContainer(
self.log_display,
id="log-area"
)
yield ScrollableContainer(
self.message_display,
id="message-area"
)
self.command_input = Input(placeholder="输入指令...", id="command-input")
yield Container(
self.command_input,
id="input-area"
)
yield Footer()
async def on_mount(self):
"""挂载完成事件"""
try:
# 开始捕获所有输出
self.log_display.start_capture()
# 设置输入框焦点
if self.command_input:
self.command_input.focus()
# 显示欢迎消息
self.show_message("🐱 SenSu TUI 已就绪!输入 'help' 查看命令\n", "info")
print("✅ TUI已启动,开始捕获所有输出")
except Exception as e:
print(f"❌ TUI挂载时出错: {str(e)}")
async def on_input_submitted(self, event):
"""输入提交事件"""
try:
if hasattr(event, 'input') and event.input.id == "command-input":
command = event.value
event.input.value = "" # 清空输入框
if command.strip():
print(f"执行命令: {command}")
# 在消息区域显示正在处理
self.show_message(f"执行命令: {command}", "command")
# 发送到指令服务处理
result = await self.command_service.process_command(command, "tui")
# 显示命令结果
if result:
self.show_message(f"结果: {result}", "success")
else:
self.show_message("命令执行完成", "success")
except Exception as e:
print(f"❌ 指令处理错误: {str(e)}")
self.show_message(f"指令处理错误: {str(e)}", "error")
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""显示消息"""
try:
self.message_display.add_message(message, msg_type, persistent)
except Exception as e:
print(f"❌ 显示TUI消息时出错: {str(e)}")
def clear_messages(self, clear_persistent: bool = False):
"""清空消息区域"""
try:
self.message_display.clear_messages(clear_persistent)
except Exception as e:
print(f"❌ 清空消息时出错: {str(e)}")
async def action_quit(self):
"""重写退出动作 - 最佳方案:优雅关闭"""
try:
logger.info("🐱 TUI接收到退出信号,开始关闭流程")
# 显示关闭消息
self.show_message("🐱 正在关闭框架...", "info", persistent=True)
# 停止捕获输出
self.log_display.stop_capture()
# 使用异步任务来优雅关闭,避免阻塞
asyncio.create_task(self._async_graceful_shutdown())
except Exception as e:
logger.error(f"TUI退出处理时出错: {str(e)}")
# 紧急退出
import os
os._exit(0)
async def _async_graceful_shutdown(self):
"""异步优雅关闭"""
try:
# 给一点时间显示消息
self.show_message("🐱 3...", "info", persistent=True)
await asyncio.sleep(1)
self.show_message("🐱 2..", "info", persistent=True)
await asyncio.sleep(1)
self.show_message("🐱 1.", "info", persistent=True)
await asyncio.sleep(1)
logger.info("🐱 执行异步关闭")
logger.debug("使用事件循环停止")
# 获取当前事件循环
loop = asyncio.get_event_loop()
# 停止所有运行中的任务(除了当前任务)
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task()]
if tasks:
logger.debug(f"取消 {len(tasks)} 个运行中的任务")
for task in tasks:
task.cancel()
# 等待任务取消完成
await asyncio.gather(*tasks, return_exceptions=True)
# 停止事件循环
loop.stop()
logger.info("🐱 事件循环已停止,框架关闭完成")
except Exception as e:
logger.error(f"异步关闭失败: {str(e)}")
# 最后的手段
import os
os._exit(0)
def shutdown(self):
"""关闭TUI"""
try:
# 停止捕获输出
self.log_display.stop_capture()
self.action_quit()
self.exit()
print("🛑 TUI已关闭")
except Exception as e:
print(f"❌ 关闭TUI时出错: {str(e)}")
class TuiService:
"""TUI服务"""
def __init__(self, config: Dict, log_service, command_service):
self.config = config
self.log_service = log_service
self.command_service = command_service
self.tui_app = None
self._message_queue = asyncio.Queue()
self._message_processor_task = None
async def start(self):
"""启动TUI"""
try:
if not self.config.get('tui', {}).get('enabled', True):
print("TUI已禁用")
return
print("启动TUI服务")
self.tui_app = TUIFramework(self.config, self.log_service, self.command_service)
# 设置动态标题
self._setup_title()
# 启动消息处理任务
self._message_processor_task = asyncio.create_task(self._process_message_queue())
# 在后台运行TUI
asyncio.create_task(self._run_tui())
except Exception as e:
print(f"启动TUI服务时出错: {str(e)}")
raise
def toggle_auto_scroll(self, target: str = "all", enabled: bool = None):
"""切换自动滚动状态"""
try:
if not self.tui_app:
return "❌ TUI未启动"
result = []
if target in ["all", "log"]:
log_state = self.tui_app.log_display.toggle_auto_scroll(enabled)
result.append(f"📜 日志自动滚动: {'✅ 启用' if log_state else '❌ 禁用'}")
if target in ["all", "message"]:
msg_state = self.tui_app.message_display.toggle_auto_scroll(enabled)
result.append(f"💬 消息自动滚动: {'✅ 启用' if msg_state else '❌ 禁用'}")
return "\n".join(result)
except Exception as e:
return f"❌ 切换自动滚动失败: {str(e)}"
def scroll_to_bottom(self, target: str = "all"):
"""手动滚动到底部"""
try:
if not self.tui_app:
return "❌ TUI未启动"
result = []
if target in ["all", "log"]:
self.tui_app.log_display.scroll_to_bottom()
result.append("📜 日志区域已滚动到底部")
if target in ["all", "message"]:
self.tui_app.message_display.scroll_to_bottom()
result.append("💬 消息区域已滚动到底部")
return "\n".join(result)
except Exception as e:
return f"❌ 滚动到底部失败: {str(e)}"
async def _process_message_queue(self):
"""处理消息队列,避免消息过多导致界面卡顿"""
try:
while True:
# 从队列中获取消息
message_data = await self._message_queue.get()
if message_data is None: # 停止信号
break
message, msg_type, persistent = message_data
# 显示消息
if self.tui_app:
self.tui_app.show_message(message, msg_type, persistent)
# 小延迟避免消息过快
await asyncio.sleep(0.05)
except asyncio.CancelledError:
logger.debug("消息处理任务被取消")
except Exception as e:
logger.error(f"消息处理任务出错: {str(e)}")
def _setup_title(self):
"""设置TUI标题"""
try:
framework_config = self.config.get('framework', {})
name = framework_config.get('name', 'SenSu')
version = framework_config.get('version', 'Unknown')
debug_mode = framework_config.get('debug', False)
# 构建标题
title_parts = [f"🐱 {name} Ver.{version}"]
if debug_mode:
title_parts.append("[DEBUG]")
self.tui_app.title = " ".join(title_parts)
self.tui_app.sub_title = "Based DreamSu Framework"
logger.debug(f"设置TUI标题: {self.tui_app.title}")
logger.debug(f"设置TUI副标题: {self.tui_app.sub_title}")
except Exception as e:
logger.error(f"设置TUI标题时出错: {e}")
self.tui_app.title = "🐱 SenSu - Based DreamSu Framework" # 默认标题
async def _run_tui(self):
"""运行TUI"""
try:
await self.tui_app.run_async()
except Exception as e:
print(f"运行TUI时出错: {str(e)}")
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""显示消息"""
try:
if self.tui_app:
# 将消息放入队列,由后台任务处理
self._message_queue.put_nowait((message, msg_type, persistent))
except Exception as e:
print(f"通过TUI服务显示消息时出错: {str(e)}")
def shutdown(self):
"""关闭TUI服务"""
try:
if self.tui_app:
self.tui_app.shutdown()
print("TUI服务已关闭")
except Exception as e:
print(f"关闭TUI服务时出错: {str(e)}")
+2
View File
@@ -0,0 +1,2 @@
from .manager import WebPanelManager
__all__ = ["WebPanelManager"]
+31
View File
@@ -0,0 +1,31 @@
import logging
logger = logging.getLogger(__name__)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import functools
from aiohttp import web
def panel_auth(handler):
"""面板专用鉴权装饰器(替代子应用中间件)"""
@functools.wraps(handler)
async def wrapper(request, *args, **kwargs):
token = request.cookies.get("panel_token")
if not token and request.headers.get("Authorization", "").startswith("Bearer "):
token = request.headers["Authorization"].split(" ", 1)[1]
auth_svc = request.app.get('auth_service')
is_valid = False
if token and auth_svc:
try:
v = await auth_svc.validate_token(token)
is_valid = bool(v)
except: pass
elif not auth_svc:
is_valid = False # 认证不可用时拒绝
if not is_valid:
return web.json_response({"error": "未认证或会话过期"}, status=401)
return await handler(request, *args, **kwargs)
return wrapper
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import logging
from pathlib import Path
from aiohttp import web
from .routes import auth, status, plugins, commands, logs
logger = logging.getLogger(__name__)
class WebPanelManager:
def __init__(self, config: dict, service_manager):
panel_cfg = config.get('panel', {}).get('entrance', {})
self.base_path = panel_cfg.get('path', '/panel')
self.panel_user = os.environ.get('SENSU_PANEL_USER', panel_cfg.get('username', 'admin'))
self.panel_pass = os.environ.get('SENSU_PANEL_PASS', panel_cfg.get('password', 'admin'))
self.base_path = f"/{self.base_path.strip('/')}"
self.sm = service_manager
self.project_root = Path(__file__).resolve().parent.parent.parent
async def start(self):
internet = self.sm.get_service("internet")
if not internet or not internet.http_app:
logger.error("❌ 网络服务未就绪,无法注册面板路由")
return False
app = internet.http_app
logger.info(f"🌐 向网络服务注册面板路由 (前缀: {self.base_path})...")
# 依赖注入
app['service_manager'] = self.sm
app['auth_service'] = self.sm.get_service("auth")
app['log_service'] = self.sm.get_service("log")
app['panel_config'] = {
'username': self.panel_user,
'password': self.panel_pass,
'index_path': self.project_root / "static" / "web_panel" / "index.html",
'home_path': self.project_root / "static" / "web_panel" / "home.html" # 🟢 新增
}
# 注册静态文件
# URL 前缀: /SenSu/static/ -> 物理路径: .../static/web_panel/
static_dir = self.project_root / "static" / "web_panel"
if static_dir.exists():
app.router.add_static(f'{self.base_path}/static/', path=str(static_dir))
logger.info(f"📂 静态资源已挂载: {self.base_path}/static/")
else:
logger.warning(f"⚠️ 静态目录缺失: {static_dir}")
# 注册首页 (登录页)
app.router.add_get(self.base_path, self._redirect_slash)
app.router.add_get(f'{self.base_path}/', self._serve_index)
# 🟢 新增: 注册面板主页 (/SenSu/home.html -> home.html)
app.router.add_get(f'{self.base_path}/home.html', self._serve_home)
# 注册 API 路由
auth.setup_routes(app, self.base_path)
status.setup_routes(app, self.base_path)
plugins.setup_routes(app, self.base_path)
commands.setup_routes(app, self.base_path)
logs.setup_routes(app, self.base_path)
# 注册日志广播
ls = self.sm.get_service("log")
if ls and hasattr(ls, 'add_log_consumer'):
ls.add_log_consumer(logs.broadcast_log)
logger.info("📡 日志广播已连接")
logger.info(f"✅ 面板路由注册完成 (复用原有网络服务路由器)")
return True
async def _redirect_slash(self, req):
return web.HTTPFound(f'{self.base_path}/')
async def _serve_index(self, req):
"""提供登录页"""
path = req.app['panel_config']['index_path']
if path.exists(): return web.FileResponse(path)
return web.Response(text=f"❌ 找不到 index.html\n路径: {path}", status=404)
async def _serve_home(self, req):
"""提供面板主页"""
path = req.app['panel_config']['home_path']
if path.exists(): return web.FileResponse(path)
return web.Response(text=f"❌ 找不到 home.html\n路径: {path}", status=404)
+39
View File
@@ -0,0 +1,39 @@
from aiohttp import web
from .utils.response import json_res
import logging
logger = logging.getLogger(__name__)
# 白名单 (相对于子应用的路径)
WHITE_LIST = {
"/api/login",
"/api/auth/status",
"/",
"/static/"
}
async def auth_middleware(app, handler):
async def mid(req):
path = req.path
# 检查白名单
if any(path.startswith(w) for w in WHITE_LIST):
return await handler(req)
# 提取 Token
token = req.cookies.get("panel_token")
if not token and req.headers.get("Authorization", "").startswith("Bearer "):
token = req.headers["Authorization"].split(" ", 1)[1]
valid, info = False, {}
# 验证 Token (简单内存验证,后期可接 Redis/DB)
session_store = app.get('session_store', {})
if token and token in session_store:
valid, info = True, session_store[token]
if valid:
req['user'] = info
return await handler(req)
return json_res({"error": "未认证"}, 401)
return mid
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import secrets
import logging
from aiohttp import web
from ..utils.auth import panel_auth
logger = logging.getLogger(__name__)
# 全局 Session 存储 (内存型)
# 格式: { "token_string": { "username": "...", "perms": [...] } }
PANEL_SESSION_STORE = {}
def setup_routes(app, prefix=''):
"""注册面板认证路由"""
# 🟢 关键:将 Session Store 挂载到 app,供拦截器读取
app['panel_session_store'] = PANEL_SESSION_STORE
# 路由注册
app.router.add_post(f'{prefix}/api/login', handle_login)
# 退出和状态检查都需要拦截
app.router.add_post(f'{prefix}/api/logout', panel_auth(handle_logout))
app.router.add_get(f'{prefix}/api/auth/status', panel_auth(handle_auth_status))
async def handle_login(req):
"""处理面板登录"""
try:
data = await req.json()
username = data.get('username')
password = data.get('password')
cfg = req.app.get('panel_config', {})
cfg_user = cfg.get('username', 'admin')
cfg_pass = cfg.get('password', 'admin')
# 校验配置中的账号密码
if username == cfg_user and password == cfg_pass:
# 登录成功:生成 Token
token = secrets.token_hex(16)
# 写入 Session Store
user_info = {
"username": username,
"perms": ["admin"],
"login_time": __import__('time').time()
}
PANEL_SESSION_STORE[token] = user_info
logger.info(f"✅ 面板登录成功: {username} (Session: {token[:4]}...)")
resp = web.json_response({"success": True, "username": username})
# 设置 Cookie
resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax")
return resp
else:
logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误")
return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401)
except Exception as e:
logger.error(f"登录异常: {e}")
return web.json_response({"error": str(e)}, status=500)
async def handle_logout(req):
"""处理退出登录"""
token = req.cookies.get("panel_token")
if token and token in PANEL_SESSION_STORE:
del PANEL_SESSION_STORE[token]
logger.info(f"👋 用户退出登录")
resp = web.json_response({"success": True})
resp.del_cookie("panel_token")
return resp
async def handle_auth_status(req):
"""获取当前认证状态 (被 panel_auth 拦截,能进来说明已认证)"""
user = req.get('user', {})
return web.json_response({
"authenticated": True,
"username": user.get("username", "Unknown"),
"perms": user.get("perms", [])
})
+15
View File
@@ -0,0 +1,15 @@
from aiohttp import web
from ..utils.auth import panel_auth
def setup_routes(app, prefix=''):
app.router.add_post(f'{prefix}/api/command', panel_auth(exec_cmd))
async def exec_cmd(req):
d = await req.json()
cs = req.app.get('service_manager').get_service("command")
if not cs: return web.json_response({"error": "Missing"}, 503)
try:
res = await cs.execute_command(d.get('command',''))
return web.json_response({"success": True, "output": str(res)})
except Exception as e:
return web.json_response({"success": False, "error": str(e)})
+30
View File
@@ -0,0 +1,30 @@
import json, asyncio
from aiohttp import web
from ..utils.auth import panel_auth
active_ws = set()
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/logs/ws', panel_auth(ws_handler))
async def ws_handler(req):
ws = web.WebSocketResponse(heartbeat=30.0)
await ws.prepare(req)
active_ws.add(ws)
try:
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
d = json.loads(msg.data)
if d.get('action') == 'set_level':
ls = req.app.get('log_service')
if ls: ls.set_level(d.get('level','INFO'))
finally: active_ws.discard(ws)
return ws
def broadcast_log(log_record):
if not active_ws: return
payload = json.dumps({"type":"log", "level":log_record.get('level','INFO'),
"message":log_record.get('simple_message',''), "timestamp":log_record.get('timestamp',0)})
for ws in list(active_ws):
if not ws.closed: asyncio.ensure_future(ws.send_str(payload))
else: active_ws.discard(ws)
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from aiohttp import web
from ..utils.auth import panel_auth
logger = logging.getLogger(__name__)
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/plugins', panel_auth(list_plugins))
app.router.add_post(f'{prefix}/api/plugins/{{name}}/{{action}}', panel_auth(manage_plugin))
app.router.add_get(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(get_perms))
app.router.add_post(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(set_perms))
async def list_plugins(req):
sm = req.app.get('service_manager')
if not sm:
return web.json_response({"error": "Service Manager 未初始化"}, status=503)
ps = sm.get_service("plugin")
if not ps:
return web.json_response({"plugins": []})
data = []
for name, info in ps.plugin_info.items():
data.append({
"name": name,
"version": getattr(info, 'version', '?'),
"running": name in ps.plugins,
"enabled": True
})
return web.json_response({"plugins": data})
async def manage_plugin(req):
sm = req.app.get('service_manager')
if not sm: return web.json_response({"error": "SM Missing"}, 503)
name = req.match_info['name']
action = req.match_info['action']
ps = sm.get_service("plugin")
if not ps: return web.json_response({"error": "Plugin Service Missing"}, 503)
try:
if action in ('disable', 'unload'):
await ps.unload_plugin(name)
elif action == 'enable':
await ps.load_plugin(name)
elif action == 'reload':
await ps.unload_plugin(name)
await ps.load_plugin(name)
return web.json_response({"success": True, "msg": "操作成功"})
except Exception as e:
logger.error(f"插件操作失败: {e}")
return web.json_response({"success": False, "error": str(e)})
async def get_perms(req):
return web.json_response({"plugin": req.match_info['name'], "permissions": ["read", "write"]})
async def set_perms(req):
return web.json_response({"success": True})
+27
View File
@@ -0,0 +1,27 @@
import time
from aiohttp import web
from ..utils.auth import panel_auth
from ..utils.system_info import SystemInfoCollector
collector = SystemInfoCollector()
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
async def get_framework(req):
sm = req.app.get('service_manager')
if not sm: return web.json_response({"error": "Missing"}, 500)
ps = sm.get_service("plugin")
# 🟢 修复:使用 sm.start_time 属性
uptime = time.time() - getattr(sm, 'start_time', time.time())
return web.json_response({
"version": "Alpha_0.2.0",
"uptime": int(uptime), # 取整秒
"plugins": len(ps.plugins) if ps else 0
})
async def get_system(req):
return web.json_response(collector.get_all())
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import functools
from aiohttp import web
def panel_auth(handler):
"""面板专用鉴权装饰器:基于面板自有的 Session Store 验证"""
@functools.wraps(handler)
async def wrapper(request, *args, **kwargs):
# 1. 获取 Token
token = request.cookies.get("panel_token")
if not token and request.headers.get("Authorization", "").startswith("Bearer "):
token = request.headers["Authorization"].split(" ", 1)[1]
is_valid = False
# 2. 从面板 Session Store 验证
session_store = request.app.get('panel_session_store', {})
if token and token in session_store:
is_valid = True
# 验证通过,将用户信息注入 request 供后续使用
request['user'] = session_store[token]
# 3. 拦截逻辑 (不再依赖外部 AuthService,确保安全隔离)
if not is_valid:
# 返回 401 并附带提示,前端可据此判断状态
return web.json_response({
"error": "未认证或会话已过期",
"status": 401
}, status=401)
return await handler(request, *args, **kwargs)
return wrapper
+7
View File
@@ -0,0 +1,7 @@
from aiohttp import web
def json_res(data, status=200, cookie=None):
resp = web.json_response(data, status=status)
if cookie: resp.set_cookie(cookie["n"], cookie["v"], max_age=cookie.get("m", 86400), httponly=True)
return resp
def get_user(req):
return req.get('user')
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import platform
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
def _is_android() -> bool:
"""检测是否为 Android 环境 (Termux 等)"""
return (
'ANDROID_ROOT' in os.environ or
os.path.exists('/system/bin/getprop') or
platform.release().lower().find('android') != -1
)
class SystemInfoCollector:
def __init__(self):
self.is_android = 'ANDROID_ROOT' in os.environ or os.path.exists('/system/bin/getprop')
self.psutil = None
if not self.is_android:
try:
import psutil
self.psutil = psutil
except ImportError as e:
logger.debug(f"psutil not available: {e}")
else:
logger.info("🤖 Android 平台识别,启用原生采集")
def get_all(self):
return {
"platform": {
"system": platform.system(),
"machine": platform.machine(),
"python": platform.python_version()
},
"cpu": self._get_cpu(),
"memory": self._get_memory(),
"network": self._get_network()
}
def _get_cpu(self):
if self.psutil:
return {
"percent": self.psutil.cpu_percent(interval=0.1),
"cores": self.psutil.cpu_count(),
"load_avg": os.getloadavg() if hasattr(os, 'getloadavg') else [0,0,0]
}
# Android 估算:负载率 = (1分钟负载 / 核心数) * 100
try:
load = os.getloadavg()
cores = os.cpu_count() or 1
percent = min(100.0, (load[0] / cores) * 100)
return {"percent": round(percent, 1), "cores": cores, "load_avg": load}
except Exception as e:
logger.error(f"System info collection error: {e}", exc_info=True)
return {"percent": 0, "cores": 0, "load_avg": [0,0,0]}
def _get_memory(self):
if self.psutil:
m = self.psutil.virtual_memory()
return {"total_gb": round(m.total/1073741824, 1), "used_gb": round(m.used/1073741824, 1), "percent": m.percent}
try:
mem = {}
with open('/proc/meminfo') as f:
for line in f:
parts = line.split()
if len(parts) >= 2: mem[parts[0].rstrip(':')] = int(parts[1]) * 1024
t, a = mem.get('MemTotal', 1), mem.get('MemAvailable', mem.get('MemFree', 0))
return {"total_gb": round(t/1073741824, 1), "used_gb": round((t-a)/1073741824, 1), "percent": round(((t-a)/t)*100, 1)}
except Exception as e:
logger.warning(f"Memory info failed: {e}")
return {"total_gb": 0, "used_gb": 0, "percent": 0}
def _get_network(self):
if self.psutil:
io = self.psutil.net_io_counters()
return {"rx": round(io.bytes_recv/1048576, 1), "tx": round(io.bytes_sent/1048576, 1)}
# Android 解析 /proc/net/dev
try:
rx = 0
with open('/proc/net/dev', 'r') as f:
for line in f:
if ':' in line and 'lo' not in line: # 排除 lo 回环
parts = line.split(':')[1].split()
rx += int(parts[0]) # RX bytes
return {"rx": round(rx/1048576, 1), "tx": 0}
except Exception as e:
logger.warning(f"Network info failed: {e}")
return {"rx": 0, "tx": 0}
+159
View File
@@ -0,0 +1,159 @@
:root {
--bg-dark: #121218; --bg-panel: #1e1e24; --bg-card: #252530;
--bg-hover: #2a2a35; --text-main: #e0e0e0; --text-dim: #888890;
--accent: #7aa2f7; --accent-glow: rgba(122, 162, 247, 0.3);
--success: #9ece6a; --warn: #e0af68; --error: #f7768e;
--border: #363642; --sidebar-w: 240px; --sidebar-collapsed: 64px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg-dark); color: var(--text-main); font-family: system-ui, -apple-system, sans-serif; height: 100vh; overflow: hidden; }
/* 登录页 */
.login-wrapper { display: flex; align-items: center; justify-content: center; height: 100vh; background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-dark) 100%); }
.login-card { background: var(--bg-card); padding: 2.5rem; border-radius: 16px; width: 340px; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid var(--border); }
.login-card h2 { color: var(--accent); margin-bottom: 1.5rem; font-size: 1.8rem; }
.input-group { margin-bottom: 1rem; text-align: left; }
.input-group label { display: block; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 4px; }
.input-group input { width: 100%; padding: 12px; background: var(--bg-dark); border: 1px solid var(--border); color: white; border-radius: 8px; font-size: 1rem; }
.input-group input:focus { border-color: var(--accent); outline: none; }
.btn-primary { width: 100%; padding: 12px; background: var(--accent); color: #000; font-weight: bold; border: none; border-radius: 8px; cursor: pointer; transition: 0.2s; margin-top: 10px; }
.btn-primary:hover { background: var(--accent-glow); color: white; }
.err-msg { color: var(--error); font-size: 0.8rem; margin-top: 10px; min-height: 1rem; }
/* 主框架布局 */
.app-frame { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: 60px 1fr; height: 100vh; transition: 0.3s ease; }
.app-frame.collapsed { grid-template-columns: var(--sidebar-collapsed) 1fr; }
header.top-bar { grid-column: 1 / -1; background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; }
.top-bar .title { font-weight: bold; font-size: 1.1rem; color: var(--accent); }
.top-bar .user-info { display: flex; align-items: center; gap: 1rem; font-size: 0.9rem; color: var(--text-dim); }
.logout-btn { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 4px 10px; border-radius: 4px; cursor: pointer; }
.logout-btn:hover { border-color: var(--error); color: var(--error); }
aside.sidebar { background: var(--bg-panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 1rem 0; overflow: hidden; transition: 0.3s; }
.nav-item { display: flex; align-items: center; padding: 12px 16px; color: var(--text-dim); text-decoration: none; cursor: pointer; transition: 0.2s; white-space: nowrap; gap: 12px; margin: 2px 8px; border-radius: 8px; }
.nav-item:hover, .nav-item.active { background: var(--bg-hover); color: var(--accent); }
.nav-item svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; }
.toggle-sidebar { margin-top: auto; padding: 12px; text-align: center; cursor: pointer; color: var(--text-dim); border-top: 1px solid var(--border); }
.toggle-sidebar:hover { color: var(--accent); }
main.content-area { position: relative; overflow: hidden; background: var(--bg-dark); display: flex; flex-direction: column; }
.progress-bar { position: absolute; top: 0; left: 0; height: 3px; background: var(--accent); width: 0; transition: width 0.3s, opacity 0.3s; opacity: 0; z-index: 100; }
.progress-bar.active { opacity: 1; }
.page-container { flex: 1; padding: 1.5rem; overflow-y: auto; }
/* 仪表盘网格 */
.dash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; }
.stat-card { background: var(--bg-card); border-radius: 12px; padding: 1.2rem; border: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.5rem; }
.stat-card h3 { font-size: 0.85rem; color: var(--text-dim); display: flex; justify-content: space-between; }
.stat-value { font-size: 2rem; font-weight: bold; color: var(--text-main); }
.stat-sub { font-size: 0.8rem; color: var(--text-dim); }
.mini-chart { width: 100%; height: 60px; background: rgba(0,0,0,0.2); border-radius: 6px; margin-top: 8px; }
/* 终端/日志样式 */
.terminal { background: #0a0a0f; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; height: 75vh; display: flex; flex-direction: column; }
.term-header { background: var(--bg-card); padding: 8px 12px; font-family: monospace; font-size: 0.8rem; color: var(--text-dim); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; }
.term-body { flex: 1; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 0.85rem; color: #ccc; line-height: 1.5; }
.term-input-area { display: flex; border-top: 1px solid var(--border); }
.term-input { flex: 1; background: var(--bg-panel); border: none; padding: 12px; color: white; font-family: monospace; outline: none; }
.log-entry { margin-bottom: 2px; border-bottom: 1px solid #1a1a24; padding: 2px 0; }
.log-INFO { color: var(--accent); } .log-WARNING { color: var(--warn); } .log-ERROR { color: var(--error); }
/* 插件列表 */
.plugin-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.plugin-card { background: var(--bg-card); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.plugin-info h4 { color: var(--accent); margin-bottom: 4px; }
.plugin-info p { font-size: 0.8rem; color: var(--text-dim); }
.badge { padding: 2px 8px; border-radius: 10px; font-size: 0.7rem; font-weight: bold; }
.badge-run { background: rgba(158, 206, 106, 0.2); color: var(--success); }
.badge-stop { background: rgba(247, 118, 142, 0.2); color: var(--error); }
.plugin-act { display: flex; gap: 8px; }
.btn-sm { padding: 4px 10px; background: var(--bg-hover); border: 1px solid var(--border); color: var(--text-dim); border-radius: 4px; cursor: pointer; font-size: 0.75rem; }
.btn-sm:hover { color: var(--accent); border-color: var(--accent); }
/* 响应式 */
@media (max-width: 768px) {
.app-frame { grid-template-columns: var(--sidebar-collapsed) 1fr; }
.nav-item span { display: none; }
.toggle-sidebar { display: none; }
}
/* =========================================
仪表盘右侧栏布局扩展
========================================= */
.dash-layout {
display: flex;
gap: 1.5rem;
height: calc(100vh - 140px); /* 减去 Header 和 Padding */
overflow: hidden;
}
.dash-main {
flex: 1;
overflow-y: auto;
padding-right: 5px;
/* 自定义滚动条 */
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
/* 右侧固定侧边栏 */
.dash-sidebar {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
padding-right: 5px;
}
/* 侧边卡片样式 */
.side-card {
background: var(--bg-card);
border-radius: 12px;
padding: 1.2rem;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 0.8rem;
transition: 0.3s;
}
.side-card:hover { border-color: var(--accent); }
.side-card h3 {
font-size: 0.9rem;
color: var(--accent);
margin: 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 6px;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
padding: 4px 0;
color: var(--text-dim);
}
.info-value {
color: var(--text-main);
font-family: 'Consolas', monospace;
font-weight: 600;
text-align: right;
max-width: 60%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 响应式适配:平板/手机自动切换为上下布局 */
@media (max-width: 1100px) {
.dash-layout { flex-direction: column; height: auto; overflow: visible; }
.dash-sidebar { width: 100%; flex-direction: row; flex-wrap: wrap; overflow: visible; }
.side-card { flex: 1; min-width: 280px; }
}
+51
View File
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SenSu 面板</title>
<!-- 🟢 加上 static 前缀 -->
<link rel="stylesheet" href="./static/css/style.css">
</head>
<body>
<div id="app" class="app-frame">
<header class="top-bar">
<div class="title">🐱 SenSu Alpha <span id="ver-badge" style="font-size:0.7em; opacity:0.5; margin-left:4px;"></span></div>
<div class="user-info">
<span id="uname">Loading...</span>
<button class="logout-btn" onclick="doLogout()">退出</button>
</div>
</header>
<aside class="sidebar">
<a class="nav-item active" data-page="dashboard">
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
<span>仪表盘</span>
</a>
<a class="nav-item" data-page="logs">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
<span>实时日志</span>
</a>
<a class="nav-item" data-page="console">
<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 14H4v-4h8v4zm0-6H4V8h8v4zm8 6h-8v-4h8v4zm0-6h-8V8h8v4z"/></svg>
<span>控制台</span>
</a>
<a class="nav-item" data-page="plugins">
<svg viewBox="0 0 24 24"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
<span>插件管理</span>
</a>
<div class="toggle-sidebar" onclick="toggleSidebar()"></div>
</aside>
<main class="content-area">
<div id="progress" class="progress-bar"></div>
<div id="page-content" class="page-container"></div>
</main>
</div>
<!-- 🟢 1. 先加载图表库 -->
<script src="./static/js/chart.js"></script>
<!-- 🟢 2. 再加载主逻辑 -->
<script src="./static/js/app.js"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SenSu 登录</title>
<link rel="stylesheet" href="./static/css/style.css">
</head>
<body>
<div class="login-wrapper">
<div class="login-card">
<h2>🐱 SenSu Login</h2>
<div class="input-group"><label>用户名</label><input type="text" id="u" value="admin"></div>
<div class="input-group"><label>密码</label><input type="password" id="p" value="admin"></div>
<button class="btn-primary" onclick="doLogin()">进入系统</button>
<div id="err" class="err-msg"></div>
</div>
</div>
<script>
async function doLogin() {
const u = document.getElementById('u').value;
const p = document.getElementById('p').value;
const err = document.getElementById('err');
err.textContent = "验证中...";
try {
const res = await fetch('./api/login', {
method: 'POST', headers: {'Content-Type': 'application/json'},
credentials: 'include', body: JSON.stringify({username: u, password: p})
});
const data = await res.json();
if(res.ok && data.success) window.location.href = './home.html';
else err.textContent = data.msg || "凭证错误";
} catch(e) { err.textContent = "网络异常"; }
}
</script>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
const BASE = '/panel';
export const api = {
get: async (url) => {
const res = await fetch(`${BASE}${url}`, { credentials: 'include' });
return res.json();
},
post: async (url, data) => {
const res = await fetch(`${BASE}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
});
return res.json();
}
};
export async function initAuth() {
// 登录逻辑绑定到 DOM
const loginForm = document.querySelector('#login-form');
if(loginForm) {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const u = document.getElementById('username').value;
const p = document.getElementById('password').value;
const res = await api.post('/api/login', { username: u, password: p });
if (res.success) location.reload(); // 登录成功刷新
});
}
return await api.get('/api/auth/status');
}
+77
View File
@@ -0,0 +1,77 @@
// 初始化检查
window.onload = async () => {
try {
const res = await fetch('./api/auth/status', { credentials: 'include' });
if(res.status === 401) { window.location.href = './index.html'; return; }
const data = await res.json();
if(!data.authenticated) { window.location.href = './index.html'; return; }
document.getElementById('uname').textContent = data.username || 'Admin';
loadPage('dashboard'); // 默认加载
} catch(e) { window.location.href = './index.html'; }
};
// 路由加载器
async function loadPage(pageName) {
const content = document.getElementById('page-content');
const bar = document.getElementById('progress');
// 侧边栏高亮
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.querySelector(`.nav-item[data-page="${pageName}"]`)?.classList.add('active');
// 进度条动画
bar.classList.add('active'); bar.style.width = '0%';
await new Promise(r => requestAnimationFrame(() => { bar.style.width = '80%'; setTimeout(r, 100); }));
try {
// 🟢 关键修改:fetch 路径必须包含 /static/
const html = await fetch(`./static/pages/${pageName}.html`).then(r => {
if(!r.ok) throw new Error('404');
return r.text();
});
content.innerHTML = html;
// 动态加载对应 JS 模块
// 🟢 关键修改:script src 路径必须包含 /static/
const script = document.createElement('script');
script.src = `./static/pages/${pageName}.js?t=${Date.now()}`;
script.onload = () => {
// 触发模块初始化
const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
if(window[moduleName]?.init) {
window[moduleName].init();
}
bar.style.width = '100%';
setTimeout(() => bar.classList.remove('active'), 200);
};
script.onerror = () => {
throw new Error('JS Load Failed');
};
document.head.appendChild(script);
} catch(e) {
content.innerHTML = `<div style="color:var(--error); text-align:center; margin-top:20vh;">页面加载失败: ${e.message}</div>`;
bar.style.background = 'var(--error)';
setTimeout(() => { bar.style.width = '100%'; setTimeout(() => { bar.classList.remove('active'); bar.style.background = 'var(--accent)'; }, 200); }, 100);
}
}
// 侧边栏切换
function toggleSidebar() {
document.getElementById('app').classList.toggle('collapsed');
}
// 退出登录
async function doLogout() {
await fetch('./api/logout', { method: 'POST', credentials: 'include' });
window.location.href = './index.html';
}
// 点击侧边栏事件委托
document.addEventListener('click', (e) => {
const nav = e.target.closest('.nav-item');
if(nav) { loadPage(nav.dataset.page); e.preventDefault(); }
});
+51
View File
@@ -0,0 +1,51 @@
class MiniChart {
constructor(canvasId, color = '#7aa2f7') {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.color = color;
this.data = new Array(60).fill(0); // 60秒历史
this.maxVal = 100;
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width - 24;
this.canvas.height = 60;
this.draw();
}
update(val) {
this.data.push(val);
if(this.data.length > 60) this.data.shift();
this.maxVal = Math.max(...this.data, 100);
this.draw();
}
draw() {
if(!this.ctx) return;
const { width, height } = this.canvas;
this.ctx.clearRect(0, 0, width, height);
this.ctx.strokeStyle = this.color;
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.data.forEach((v, i) => {
const x = (i / 59) * width;
const y = height - (v / this.maxVal) * (height - 10);
if(i === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
});
this.ctx.stroke();
// 填充渐变
this.ctx.lineTo(width, height);
this.ctx.lineTo(0, height);
this.ctx.fillStyle = this.color + '20';
this.ctx.fill();
}
}
window.MiniChart = MiniChart;
+29
View File
@@ -0,0 +1,29 @@
import { initAuth, api } from './api.js';
import { initDashboard } from './modules/dashboard.js';
import { initPlugins } from './modules/plugins.js';
import { initLogs } from './modules/logs.js';
import { initCommands } from './modules/commands.js';
document.addEventListener('DOMContentLoaded', async () => {
// 1. 检查登录状态
const authState = await initAuth();
if (!authState.authenticated) return; // 停留在登录页
// 2. 初始化各模块
initDashboard();
initPlugins();
initLogs();
initCommands();
// 3. Tab 切换逻辑
document.querySelectorAll('.tabs button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tabId = `tab-${btn.dataset.tab}`;
document.querySelectorAll('.content section').forEach(s => s.classList.add('hidden'));
document.getElementById(tabId).classList.remove('hidden');
});
});
});
+10
View File
@@ -0,0 +1,10 @@
<div class="terminal">
<div class="term-header">💻 FRAMEWORK CONSOLE</div>
<div class="term-body" id="cmd-box" style="font-size:13px; color:#ccc;">
<div style="color:var(--success)">SenSu Console Ready. Type 'help' or 'status'.</div>
</div>
<div class="term-input-area">
<span style="padding:10px; color:var(--accent)">$</span>
<input type="text" id="cmd-input" class="term-input" placeholder="输入命令并回车..." autocomplete="off">
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
window.ConsoleModule = {
init: () => {
const box = document.getElementById('cmd-box');
const input = document.getElementById('cmd-input');
input.focus();
const append = (text, cls='') => {
box.innerHTML += `<div style="margin-top:2px;${cls?'color:'+cls:''}">${text}</div>`;
box.scrollTop = box.scrollHeight;
};
input.onkeydown = async (e) => {
if(e.key === 'Enter' && input.value.trim()) {
const cmd = input.value.trim();
append(`$ ${cmd}`, 'var(--accent)');
input.value = '';
try {
const res = await fetch('./api/command', {
method:'POST', headers:{'Content-Type':'application/json'}, credentials:'include',
body: JSON.stringify({command: cmd})
});
const d = await res.json();
append(d.success ? d.output : `Error: ${d.error}`, d.success ? '#ccc' : 'var(--error)');
} catch(err) { append(`Network Error: ${err.message}`, 'var(--error)'); }
}
};
},
destroy: () => {}
};
+70
View File
@@ -0,0 +1,70 @@
<div class="dash-layout">
<!-- 🟢 左侧:动态监控区 -->
<div class="dash-main">
<div class="dash-grid">
<div class="stat-card">
<h3>⏳ 运行时间</h3>
<div class="stat-value" id="d-uptime">--</div>
<div class="stat-sub" id="d-ver-badge">v?.?.?</div>
</div>
<div class="stat-card">
<h3>📦 插件状态</h3>
<div class="stat-value" id="d-plugins">--</div>
<div class="stat-sub">已加载 / 活跃</div>
</div>
<div class="stat-card">
<h3>🧠 内存使用</h3>
<div class="stat-value" id="d-mem">--%</div>
<canvas id="chart-mem" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>⚡ CPU 负载</h3>
<div class="stat-value" id="d-cpu">--%</div>
<canvas id="chart-cpu" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>🌐 网络接收</h3>
<div class="stat-value" id="d-net">--</div>
<canvas id="chart-net" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>💾 进程内存</h3>
<div class="stat-value" id="d-proc-mem">--</div>
<canvas id="chart-proc-mem" class="mini-chart"></canvas>
</div>
</div>
</div>
<!-- 🟢 右侧:高优先级信息栏 -->
<aside class="dash-sidebar">
<!-- 1. 硬件平台信息 -->
<div class="side-card">
<h3>💻 硬件平台</h3>
<div class="info-row"><span>操作系统</span><span class="info-value" id="info-os">--</span></div>
<div class="info-row"><span>硬件架构</span><span class="info-value" id="info-arch">--</span></div>
<div class="info-row"><span>核心数 (L/P)</span><span class="info-value" id="info-cores">--</span></div>
<div class="info-row"><span>物理内存</span><span class="info-value" id="info-mem-total">--</span></div>
<div class="info-row"><span>运行环境</span><span class="info-value" id="info-env">--</span></div>
</div>
<!-- 2. 框架简易信息 -->
<div class="side-card">
<h3>🐱 框架状态</h3>
<div class="info-row"><span>版本号</span><span class="info-value" id="info-fw-ver">--</span></div>
<div class="info-row"><span>访问地址</span><span class="info-value" id="info-addr">--</span></div>
<div class="info-row"><span>进程 PID</span><span class="info-value" id="info-pid">--</span></div>
<div class="info-row"><span>系统负载 (15m)</span><span class="info-value" id="info-load">--</span></div>
<div class="info-row" style="border:none; padding-top:8px;">
<span>运行状态</span>
<span class="info-value" style="color:var(--success)">🟢 Online</span>
</div>
</div>
<!-- 3. 快捷操作 (占位) -->
<div class="side-card">
<h3>🛠️ 快捷操作</h3>
<button class="btn-sm" style="width:100%; margin-bottom:6px" onclick="window.PluginsModule?.refresh()">🔄 刷新插件列表</button>
<button class="btn-sm" style="width:100%; color:var(--error); border-color:var(--error)" onclick="doLogout()">🚪 退出登录</button>
</div>
</aside>
</div>
+105
View File
@@ -0,0 +1,105 @@
window.DashboardModule = {
charts: {},
init: () => {
// 1. 初始化图表实例
window.DashboardModule.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
};
// 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启)
fetchSystemStaticInfo();
// 3. 启动实时数据轮询
fetchDash();
window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新
},
destroy: () => {
clearInterval(window._dashInterval);
window.DashboardModule.charts = {};
}
};
async function fetchSystemStaticInfo() {
try {
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// 硬件信息
if(sys.platform) {
document.getElementById('info-os').textContent = sys.platform.system || '--';
document.getElementById('info-arch').textContent = sys.platform.machine || '--';
document.getElementById('info-env').textContent = sys.platform.env || 'Standard';
}
if(sys.cpu) {
const c = sys.cpu.cores || 0;
document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核
}
if(sys.memory) {
document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB';
}
// 框架信息 (部分需结合 API)
const host = window.location.hostname + (window.location.port ? ':'+window.location.port : '');
document.getElementById('info-addr').textContent = host;
} catch(e) {}
}
async function fetchDash() {
try {
const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json());
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// --- 左侧动态数据更新 ---
if(fw) {
document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0);
document.getElementById('d-plugins').textContent = fw.plugins || 0;
if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?');
}
if(sys) {
// 内存
const m = sys.memory?.percent || 0;
document.getElementById('d-mem').textContent = m + '%';
window.DashboardModule.charts.mem.update(m);
// CPU (兼容 Android null 情况)
let cpuVal = sys.cpu?.percent;
if (cpuVal === null || cpuVal === undefined) {
const load = sys.cpu?.load_avg?.[0] || 0;
const cores = sys.cpu?.cores || 1;
cpuVal = Math.min(100, (load / cores) * 100);
}
document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%';
window.DashboardModule.charts.cpu.update(cpuVal);
// 网络 (RX 总量)
const netRx = sys.network?.rx || 0;
document.getElementById('d-net').textContent = netRx + ' MB';
window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势
// 进程内存
const pm = sys.process?.memory_mb || 0;
document.getElementById('d-proc-mem').textContent = pm + ' MB';
window.DashboardModule.charts.proc_mem.update(pm);
// --- 右侧动态数据更新 ---
if(sys.process) {
document.getElementById('info-pid').textContent = sys.process.pid || '--';
}
if(sys.cpu?.load_avg) {
document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
} catch(e) { console.warn("Dashboard fetch error", e); }
}
// 辅助:秒数转时间格式
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
+4
View File
@@ -0,0 +1,4 @@
<div class="terminal">
<div class="term-header">📡 LIVE LOG STREAM (WebSocket)</div>
<div class="term-body" id="log-box" style="font-size:13px;"></div>
</div>
+24
View File
@@ -0,0 +1,24 @@
window.LogsModule = {
ws: null,
init: () => {
const box = document.getElementById('log-box');
const connect = () => {
window.LogsModule.ws = new WebSocket(`ws://${location.host}${window.location.pathname.replace(/\/$/,'')}/api/logs/ws`);
window.LogsModule.ws.onopen = () => box.innerHTML += `<div style="color:var(--success)">🟢 Connected</div>`;
window.LogsModule.ws.onmessage = e => {
try {
const d = JSON.parse(e.data);
if(d.type === 'log') {
const cls = d.level === 'ERROR' ? 'log-ERROR' : d.level === 'WARNING' ? 'log-WARNING' : 'log-INFO';
const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : '--';
box.innerHTML += `<div class="log-entry"><span style="color:#555;margin-right:5px">${t}</span><span class="${cls}">[${d.level}]</span> ${d.message}</div>`;
box.scrollTop = box.scrollHeight;
}
} catch(e){}
};
window.LogsModule.ws.onclose = setTimeout(connect, 3000);
};
connect();
},
destroy: () => window.LogsModule.ws?.close()
};
+5
View File
@@ -0,0 +1,5 @@
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h2 style="color:var(--accent);">📦 插件管理</h2>
<button class="btn-sm" onclick="window.PluginsModule.refresh()">🔄 刷新</button>
</div>
<div id="plugin-list" class="plugin-list">加载中...</div>
+36
View File
@@ -0,0 +1,36 @@
window.PluginsModule = {
init: () => window.PluginsModule.refresh(),
refresh: async () => {
const list = document.getElementById('plugin-list');
if(!list) return;
list.innerHTML = '加载中...';
try {
const res = await fetch('./api/plugins', {credentials:'include'});
const data = await res.json();
if(!data.plugins?.length) { list.innerHTML = '<div style="color:var(--text-dim)">暂无已加载插件</div>'; return; }
list.innerHTML = data.plugins.map(p => `
<div class="plugin-card">
<div class="plugin-info">
<h4>${p.name} <span class="badge ${p.running?'badge-run':'badge-stop'}">${p.running?'RUNNING':'STOPPED'}</span></h4>
<p>v${p.version||'1.0.0'} | ${p.enabled?'已启用':'已禁用'}</p>
</div>
<div class="plugin-act">
${p.running
? `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
: `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
}
<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
</div>
</div>
`).join('');
} catch(e) { list.innerHTML = '加载失败'; }
},
act: async (name, action) => {
try {
await fetch(`./api/plugins/${name}/${action}`, {method:'POST', credentials:'include'});
setTimeout(window.PluginsModule.refresh, 500);
} catch(e) { alert('操作失败'); }
},
destroy: () => {}
};
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, Any
from aiohttp import web
try:
from fmfuncs.plugin_command_decorator import plugin_command, command
except ImportError:
def plugin_command(name=None, description=None, permissions=None):
def decorator(func): return func
return decorator
command = plugin_command
try:
from bridges.plugin_network_bridge import PluginNetworkBridge
except ImportError:
class PluginNetworkBridge:
def __init__(self, *args): pass
async def register_http_route(self, *a, **k): pass
async def register_websocket(self, *a, **k): pass
async def broadcast_websocket(self, *a, **k): pass
def get_network_info(self): return {'plugin_name': '', 'registered_routes': [], 'websocket_handlers': [], 'base_url': '不可用'}
async def setup_data_transfer(self, *a, **k): pass
logger = logging.getLogger(__name__)
class Plugin:
"""${plugin_name} 插件"""
def __init__(self, plugin_name: str, config: Dict, bridge):
self.plugin_name = plugin_name
self.config = config
self.bridge = bridge
self.network_bridge = None
self.is_running = False
logger.debug(f"插件初始化: {plugin_name}")
async def initialize(self):
logger.info(f"初始化插件: {self.plugin_name}")
self.is_running = True
logger.debug(f"插件初始化完成: {self.plugin_name}")
async def shutdown(self):
logger.info(f"关闭插件: {self.plugin_name}")
self.is_running = False
self.bridge.cleanup_plugin_subscriptions(self.plugin_name)
logger.debug(f"插件关闭完成: {self.plugin_name}")
+9
View File
@@ -0,0 +1,9 @@
name: "${plugin_name}"
version: "1.0.0"
description: "${description}"
author: "${author}"
settings:
enabled: true
auto_start: true
log_level: "INFO"
@@ -0,0 +1,10 @@
plugin_name: "${plugin_name}"
permissions:
- "plugin.${plugin_name}.read"
- "plugin.${plugin_name}.write"
- "plugin.${plugin_name}.execute"
permission_descriptions:
plugin.${plugin_name}.read: "读取${plugin_name}插件数据"
plugin.${plugin_name}.write: "写入${plugin_name}插件数据"
plugin.${plugin_name}.execute: "执行${plugin_name}插件操作"
View File
+4
View File
@@ -0,0 +1,4 @@
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
+12
View File
@@ -0,0 +1,12 @@
import pytest,os
from services.auth_service import AuthService
class TestAuthService:
def test_hash_consistent(self):
s=AuthService({});assert s._hash_password("x")==s._hash_password("x")
def test_hash_len(self):
assert len(AuthService({})._hash_password("x"))==64
def test_invalid_login(self):
assert AuthService({}).authenticate_user("admin","WRONG") is None
def test_valid_login(self):
pw=os.environ.get("SENSU_ADMIN_PASSWORD","admin123")
assert AuthService({}).authenticate_user("admin",pw) is not None
+49
View File
@@ -0,0 +1,49 @@
import pytest, asyncio
from service_manager import ServiceManager
class MockService:
def __init__(self, name="mock"):
self.name = name
self.shutdown_called = False
def shutdown(self):
self.shutdown_called = True
return True
class AsyncMockService:
def __init__(self, name="async_mock"):
self.name = name
self.shutdown_called = False
async def shutdown(self):
self.shutdown_called = True
class TestServiceManager:
def test_register_get(self):
sm = ServiceManager(); s = MockService()
sm.register_service("t", s)
assert sm.get_service("t") is s
def test_missing_raises(self):
with pytest.raises(ValueError):
ServiceManager().get_service("x")
def test_has_service(self):
sm = ServiceManager()
sm.register_service("a", MockService())
assert sm.has_service("a") and not sm.has_service("b")
def test_shutdown_sync(self):
sm = ServiceManager(); s = MockService()
sm.register_service("s", s)
sm.shutdown_all()
assert s.shutdown_called
def test_health(self):
sm = ServiceManager()
sm.register_service("h", MockService(), health_check=lambda: True)
assert asyncio.run(sm.check_health())["h"]
def test_startup_order(self):
sm = ServiceManager()
sm.register_service("1", MockService())
sm.register_service("2", MockService())
assert sm.startup_order == ["1", "2"]
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
框架功能集 - 提供各种工具函数和工具类
"""
from .file_utils import FileUtils
from .config_utils import ConfigUtils
from .validation_utils import ValidationUtils
from .network_utils import NetworkUtils
from .plugin_utils import PluginUtils
__all__ = [
'FileUtils',
'ConfigUtils',
'ValidationUtils',
'NetworkUtils',
'PluginUtils'
]
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import yaml
import json
from pathlib import Path
from typing import Dict, Any, Optional
import copy
logger = logging.getLogger(__name__)
class ConfigUtils:
"""配置工具类"""
@staticmethod
def load_yaml_config(file_path: str, default_config: Dict = None) -> Dict:
"""加载YAML配置文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"YAML配置文件不存在: {file_path}")
if default_config:
ConfigUtils.save_yaml_config(file_path, default_config)
logger.debug(f"已创建默认YAML配置: {file_path}")
return default_config or {}
with open(path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
logger.debug(f"YAML配置加载成功: {file_path}")
return config or {}
except yaml.YAMLError as e:
logger.error(f"YAML配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
except Exception as e:
logger.error(f"加载YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
@staticmethod
def save_yaml_config(file_path: str, config: Dict) -> bool:
"""保存YAML配置文件"""
try:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2)
logger.debug(f"YAML配置保存成功: {file_path}")
return True
except Exception as e:
logger.error(f"保存YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def load_json_config(file_path: str, default_config: Dict = None) -> Dict:
"""加载JSON配置文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"JSON配置文件不存在: {file_path}")
if default_config:
ConfigUtils.save_json_config(file_path, default_config)
logger.debug(f"已创建默认JSON配置: {file_path}")
return default_config or {}
with open(path, 'r', encoding='utf-8') as f:
config = json.load(f)
logger.debug(f"JSON配置加载成功: {file_path}")
return config
except json.JSONDecodeError as e:
logger.error(f"JSON配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
except Exception as e:
logger.error(f"加载JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
@staticmethod
def save_json_config(file_path: str, config: Dict) -> bool:
"""保存JSON配置文件"""
try:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
logger.debug(f"JSON配置保存成功: {file_path}")
return True
except Exception as e:
logger.error(f"保存JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def get_nested_value(config: Dict, key_path: str, default: Any = None) -> Any:
"""获取嵌套配置值"""
try:
keys = key_path.split('.')
current = config
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
logger.debug(f"配置键不存在: {key_path}")
return default
logger.debug(f"获取嵌套配置值: {key_path} -> {current}")
return current
except Exception as e:
logger.error(f"获取嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
return default
@staticmethod
def set_nested_value(config: Dict, key_path: str, value: Any) -> bool:
"""设置嵌套配置值"""
try:
keys = key_path.split('.')
current = config
# 遍历到最后一个键的父级
for key in keys[:-1]:
if key not in current or not isinstance(current[key], dict):
current[key] = {}
current = current[key]
# 设置值
current[keys[-1]] = value
logger.debug(f"设置嵌套配置值: {key_path} -> {value}")
return True
except Exception as e:
logger.error(f"设置嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def merge_configs(base_config: Dict, override_config: Dict) -> Dict:
"""合并配置(深度合并)"""
try:
result = copy.deepcopy(base_config)
for key, value in override_config.items():
if (key in result and
isinstance(result[key], dict) and
isinstance(value, dict)):
# 递归合并字典
result[key] = ConfigUtils.merge_configs(result[key], value)
else:
# 直接覆盖
result[key] = copy.deepcopy(value)
logger.debug("配置合并完成")
return result
except Exception as e:
logger.error(f"合并配置时出错: {str(e)}", exc_info=True)
return base_config
@staticmethod
def validate_config_structure(config: Dict, schema: Dict) -> bool:
"""验证配置结构"""
try:
def _validate(current_config, current_schema, path=""):
for key, expected_type in current_schema.items():
full_path = f"{path}.{key}" if path else key
if key not in current_config:
logger.error(f"配置缺少必要字段: {full_path}")
return False
actual_value = current_config[key]
expected_type_name = expected_type.__name__ if hasattr(expected_type, '__name__') else str(expected_type)
if not isinstance(actual_value, expected_type):
logger.error(f"配置类型错误 {full_path}: 期望 {expected_type_name}, 实际 {type(actual_value).__name__}")
return False
# 如果是字典且schema有嵌套定义,递归验证
if (isinstance(expected_type, dict) and
isinstance(actual_value, dict)):
if not _validate(actual_value, expected_type, full_path):
return False
return True
result = _validate(config, schema)
if result:
logger.debug("配置结构验证通过")
else:
logger.error("配置结构验证失败")
return result
except Exception as e:
logger.error(f"验证配置结构时出错: {str(e)}", exc_info=True)
return False
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
import shutil
from pathlib import Path
from typing import List, Optional
import hashlib
logger = logging.getLogger(__name__)
class FileUtils:
"""文件操作工具类"""
@staticmethod
def ensure_directory(directory_path: str) -> bool:
"""确保目录存在"""
try:
path = Path(directory_path)
path.mkdir(parents=True, exist_ok=True)
logger.debug(f"目录已确保存在: {directory_path}")
return True
except Exception as e:
logger.error(f"创建目录时出错 {directory_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def safe_write(file_path: str, content: str, backup: bool = True) -> bool:
"""安全写入文件(支持备份)"""
try:
path = Path(file_path)
# 备份原文件
if backup and path.exists():
backup_path = path.with_suffix(path.suffix + '.bak')
shutil.copy2(path, backup_path)
logger.debug(f"文件已备份: {backup_path}")
# 写入新内容
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
logger.debug(f"文件写入成功: {file_path}, 大小: {len(content)} 字节")
return True
except Exception as e:
logger.error(f"写入文件时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def safe_read(file_path: str, default: str = "") -> str:
"""安全读取文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"文件不存在: {file_path}")
return default
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
logger.debug(f"文件读取成功: {file_path}, 大小: {len(content)} 字节")
return content
except Exception as e:
logger.error(f"读取文件时出错 {file_path}: {str(e)}", exc_info=True)
return default
@staticmethod
def list_files(directory: str, pattern: str = "*", recursive: bool = False) -> List[Path]:
"""列出目录中的文件"""
try:
path = Path(directory)
if not path.exists():
logger.warning(f"目录不存在: {directory}")
return []
if recursive:
files = list(path.rglob(pattern))
else:
files = list(path.glob(pattern))
# 过滤出文件(非目录)
files = [f for f in files if f.is_file()]
logger.debug(f"列出文件: {directory}, 模式: {pattern}, 找到 {len(files)} 个文件")
return files
except Exception as e:
logger.error(f"列出文件时出错 {directory}: {str(e)}", exc_info=True)
return []
@staticmethod
def calculate_file_hash(file_path: str, algorithm: str = "md5") -> Optional[str]:
"""计算文件哈希值"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"文件不存在: {file_path}")
return None
hash_func = getattr(hashlib, algorithm)()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_func.update(chunk)
file_hash = hash_func.hexdigest()
logger.debug(f"文件哈希计算完成: {file_path} -> {algorithm}:{file_hash}")
return file_hash
except Exception as e:
logger.error(f"计算文件哈希时出错 {file_path}: {str(e)}", exc_info=True)
return None
@staticmethod
def cleanup_old_files(directory: str, pattern: str, keep_count: int) -> int:
"""清理旧文件,保留指定数量的最新文件"""
try:
files = FileUtils.list_files(directory, pattern)
if len(files) <= keep_count:
logger.debug(f"文件数量未超过限制,无需清理: {directory}")
return 0
# 按修改时间排序
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# 删除旧文件
removed_count = 0
for file_to_remove in files[keep_count:]:
try:
file_to_remove.unlink()
removed_count += 1
logger.debug(f"删除旧文件: {file_to_remove}")
except Exception as e:
logger.error(f"删除文件时出错 {file_to_remove}: {str(e)}", exc_info=True)
logger.info(f"文件清理完成: {directory}, 删除 {removed_count} 个文件")
return removed_count
except Exception as e:
logger.error(f"清理旧文件时出错 {directory}: {str(e)}", exc_info=True)
return 0
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import socket
import asyncio
import re
from typing import Optional, Tuple
import aiohttp
import ssl
logger = logging.getLogger(__name__)
class NetworkUtils:
"""网络工具类"""
@staticmethod
async def check_port_available(host: str, port: int) -> bool:
"""检查端口是否可用"""
try:
# 尝试创建socket连接
reader, writer = await asyncio.open_connection(host, port)
writer.close()
await writer.wait_closed()
logger.debug(f"端口 {host}:{port} 已被占用")
return False
except (ConnectionRefusedError, asyncio.TimeoutError):
logger.debug(f"端口 {host}:{port} 可用")
return True
except Exception as e:
logger.error(f"检查端口可用性时出错 {host}:{port}: {str(e)}", exc_info=True)
return False
@staticmethod
async def find_available_port(host: str = "localhost", start_port: int = 8000,
max_attempts: int = 100) -> Optional[int]:
"""查找可用端口"""
try:
for port in range(start_port, start_port + max_attempts):
if await NetworkUtils.check_port_available(host, port):
logger.debug(f"找到可用端口: {host}:{port}")
return port
logger.warning(f"在范围 {start_port}-{start_port + max_attempts} 内未找到可用端口")
return None
except Exception as e:
logger.error(f"查找可用端口时出错: {str(e)}", exc_info=True)
return None
@staticmethod
async def http_request(url: str, method: str = "GET", headers: dict = None,
data: dict = None, timeout: int = 30) -> Tuple[bool, dict]:
"""发送HTTP请求"""
try:
logger.debug(f"发送HTTP请求: {method} {url}")
timeout_obj = aiohttp.ClientTimeout(total=timeout)
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
async with session.request(method, url, headers=headers, json=data) as response:
response_data = await response.text()
result = {
"status": response.status,
"headers": dict(response.headers),
"data": response_data,
"url": str(response.url)
}
logger.debug(f"HTTP请求完成: {method} {url} -> 状态 {response.status}")
return True, result
except asyncio.TimeoutError:
logger.error(f"HTTP请求超时: {method} {url}")
return False, {"error": "请求超时"}
except aiohttp.ClientError as e:
logger.error(f"HTTP客户端错误: {method} {url} -> {str(e)}")
return False, {"error": str(e)}
except Exception as e:
logger.error(f"HTTP请求时出错: {method} {url} -> {str(e)}", exc_info=True)
return False, {"error": str(e)}
@staticmethod
def get_local_ip() -> str:
"""获取本地IP地址"""
try:
# 创建一个socket连接来获取本地IP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
logger.debug(f"获取本地IP: {local_ip}")
return local_ip
except Exception as e:
logger.error(f"获取本地IP时出错: {str(e)}", exc_info=True)
return "127.0.0.1"
@staticmethod
def is_valid_hostname(hostname: str) -> bool:
"""验证主机名格式"""
try:
if len(hostname) > 255:
return False
if hostname[-1] == ".":
hostname = hostname[:-1]
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
except Exception as e:
logger.error(f"验证主机名时出错: {str(e)}", exc_info=True)
return False
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import inspect
import re
from typing import Dict, List, Any, Callable
from pathlib import Path
import importlib
logger = logging.getLogger(__name__)
class PluginUtils:
"""插件工具类"""
@staticmethod
def validate_plugin_structure(plugin_path: Path) -> bool:
"""验证插件结构"""
try:
logger.debug(f"验证插件结构: {plugin_path}")
required_files = [
"__init__.py",
"config.yaml",
"permissions.yaml"
]
# 检查必需文件
for file_name in required_files:
if not (plugin_path / file_name).exists():
logger.error(f"插件缺少必需文件: {file_name}")
return False
# 检查主模块是否有Plugin类
try:
spec = importlib.util.spec_from_file_location("plugin_module", plugin_path / "__init__.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, 'Plugin'):
logger.error("插件主模块缺少Plugin类")
return False
# 检查Plugin类是否有必要方法
plugin_class = module.Plugin
required_methods = ['initialize', 'shutdown']
for method_name in required_methods:
if not hasattr(plugin_class, method_name):
logger.error(f"Plugin类缺少必要方法: {method_name}")
return False
logger.debug(f"插件结构验证通过: {plugin_path.name}")
return True
except Exception as e:
logger.error(f"验证插件类时出错: {str(e)}", exc_info=True)
return False
except Exception as e:
logger.error(f"验证插件结构时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def get_plugin_dependencies(plugin_path: Path) -> List[str]:
"""获取插件依赖"""
try:
config_file = plugin_path / "config.yaml"
if not config_file.exists():
return []
import yaml
with open(config_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
dependencies = config.get('dependencies', [])
if isinstance(dependencies, list):
logger.debug(f"获取插件依赖: {plugin_path.name} -> {dependencies}")
return dependencies
else:
logger.warning(f"插件依赖格式错误: {plugin_path.name}")
return []
except Exception as e:
logger.error(f"获取插件依赖时出错: {str(e)}", exc_info=True)
return []
@staticmethod
def scan_plugin_methods(plugin_instance) -> Dict[str, List[str]]:
"""扫描插件方法"""
try:
logger.debug(f"扫描插件方法: {type(plugin_instance).__name__}")
methods_info = {
"public_methods": [],
"private_methods": [],
"async_methods": [],
"event_handlers": []
}
for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod):
# 跳过特殊方法
if name.startswith('_') and not name.startswith('__'):
methods_info["private_methods"].append(name)
elif not name.startswith('_'):
methods_info["public_methods"].append(name)
# 检查是否为异步方法
if inspect.iscoroutinefunction(method):
methods_info["async_methods"].append(name)
# 检查是否为事件处理器
if name.startswith('handle_') or name.startswith('on_'):
methods_info["event_handlers"].append(name)
logger.debug(f"插件方法扫描完成: 公共{len(methods_info['public_methods'])}个, 私有{len(methods_info['private_methods'])}")
return methods_info
except Exception as e:
logger.error(f"扫描插件方法时出错: {str(e)}", exc_info=True)
return {}
@staticmethod
def create_plugin_skeleton(plugin_name: str, plugin_path: Path) -> bool:
"""创建插件骨架"""
try:
logger.debug(f"创建插件骨架: {plugin_name} -> {plugin_path}")
# 创建插件目录
plugin_path.mkdir(parents=True, exist_ok=True)
# 创建主模块文件
init_content = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, Any
logger = logging.getLogger(__name__)
class Plugin:
"""{plugin_name} 插件"""
def __init__(self, plugin_name: str, config: Dict, bridge):
self.plugin_name = plugin_name
self.config = config
self.bridge = bridge
self.is_running = False
logger.debug(f"插件初始化: {{plugin_name}}")
async def initialize(self):
"""初始化插件"""
try:
logger.info(f"初始化插件: {{self.plugin_name}}")
# 在这里注册事件处理器和命令
# 示例: self.bridge.subscribe_plugin(self.plugin_name, "event.name", self.handler)
self.is_running = True
logger.debug(f"插件初始化完成: {{self.plugin_name}}")
except Exception as e:
logger.error(f"初始化插件时出错: {{str(e)}}", exc_info=True)
raise
async def shutdown(self):
"""关闭插件"""
try:
logger.info(f"关闭插件: {{self.plugin_name}}")
self.is_running = False
# 清理资源
self.bridge.cleanup_plugin_subscriptions(self.plugin_name)
logger.debug(f"插件关闭完成: {{self.plugin_name}}")
except Exception as e:
logger.error(f"关闭插件时出错: {{str(e)}}", exc_info=True)
# 在这里添加你的插件方法
async def example_method(self, message: str) -> str:
"""示例方法"""
try:
logger.debug(f"插件方法调用: {{message}}")
return f"插件响应: {{message}}"
except Exception as e:
logger.error(f"插件方法调用出错: {{str(e)}}", exc_info=True)
raise
'''.format(plugin_name=plugin_name)
with open(plugin_path / "__init__.py", 'w', encoding='utf-8') as f:
f.write(init_content)
# 创建配置文件
config_content = f'''# {plugin_name} 插件配置
name: "{plugin_name}"
version: "1.0.0"
description: "{plugin_name} 插件描述"
author: "插件作者"
# 插件特定配置
settings:
enabled: true
auto_start: true
log_level: "INFO"
# 依赖配置
dependencies: []
'''
with open(plugin_path / "config.yaml", 'w', encoding='utf-8') as f:
f.write(config_content)
# 创建权限文件
permissions_content = f'''# {plugin_name} 插件权限申请
plugin_name: "{plugin_name}"
permissions:
- "plugin.{plugin_name}.read"
- "plugin.{plugin_name}.write"
# 权限说明
permission_descriptions:
plugin.{plugin_name}.read: "读取{plugin_name}插件数据"
plugin.{plugin_name}.write: "写入{plugin_name}插件数据"
'''
with open(plugin_path / "permissions.yaml", 'w', encoding='utf-8') as f:
f.write(permissions_content)
logger.info(f"插件骨架创建完成: {plugin_name}")
return True
except Exception as e:
logger.error(f"创建插件骨架时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_plugin_permissions(plugin_path: Path, requested_permissions: List[str]) -> bool:
"""验证插件权限申请"""
try:
logger.debug(f"验证插件权限: {plugin_path.name}")
# 检查权限格式
for permission in requested_permissions:
if not isinstance(permission, str):
logger.error(f"权限格式错误: {permission}")
return False
# 检查权限命名规范
if not re.match(r'^[a-z][a-z0-9_.]*$', permission):
logger.error(f"权限命名不规范: {permission}")
return False
logger.debug(f"插件权限验证通过: {len(requested_permissions)} 个权限")
return True
except Exception as e:
logger.error(f"验证插件权限时出错: {str(e)}", exc_info=True)
return False
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import re
import os
from typing import Any, List, Optional, Callable, Dict
from urllib.parse import urlparse
import ipaddress
logger = logging.getLogger(__name__)
class ValidationUtils:
"""验证工具类"""
@staticmethod
def is_valid_email(email: str) -> bool:
"""验证邮箱格式"""
try:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
result = bool(re.match(pattern, email))
logger.debug(f"邮箱验证: {email} -> {result}")
return result
except Exception as e:
logger.error(f"验证邮箱时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_url(url: str) -> bool:
"""验证URL格式"""
try:
result = urlparse(url)
is_valid = all([result.scheme, result.netloc])
logger.debug(f"URL验证: {url} -> {is_valid}")
return is_valid
except Exception as e:
logger.error(f"验证URL时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_ip(ip: str) -> bool:
"""验证IP地址格式"""
try:
ipaddress.ip_address(ip)
logger.debug(f"IP地址验证: {ip} -> True")
return True
except ValueError:
logger.debug(f"IP地址验证: {ip} -> False")
return False
except Exception as e:
logger.error(f"验证IP地址时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_port(port: int) -> bool:
"""验证端口号"""
try:
is_valid = 1 <= port <= 65535
logger.debug(f"端口验证: {port} -> {is_valid}")
return is_valid
except Exception as e:
logger.error(f"验证端口时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_string(value: Any, min_length: int = 0, max_length: int = None,
pattern: str = None) -> bool:
"""验证字符串"""
try:
if not isinstance(value, str):
logger.debug(f"字符串验证失败: 不是字符串类型")
return False
if len(value) < min_length:
logger.debug(f"字符串验证失败: 长度小于 {min_length}")
return False
if max_length and len(value) > max_length:
logger.debug(f"字符串验证失败: 长度大于 {max_length}")
return False
if pattern and not re.match(pattern, value):
logger.debug(f"字符串验证失败: 不匹配模式 {pattern}")
return False
logger.debug(f"字符串验证通过: 长度 {len(value)}")
return True
except Exception as e:
logger.error(f"验证字符串时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_number(value: Any, min_value: float = None, max_value: float = None) -> bool:
"""验证数字"""
try:
if not isinstance(value, (int, float)):
# 尝试转换
try:
value = float(value)
except (ValueError, TypeError):
logger.debug(f"数字验证失败: 无法转换为数字")
return False
if min_value is not None and value < min_value:
logger.debug(f"数字验证失败: 值小于 {min_value}")
return False
if max_value is not None and value > max_value:
logger.debug(f"数字验证失败: 值大于 {max_value}")
return False
logger.debug(f"数字验证通过: {value}")
return True
except Exception as e:
logger.error(f"验证数字时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_list(value: Any, min_length: int = 0, max_length: int = None,
item_validator: Callable = None) -> bool:
"""验证列表"""
try:
if not isinstance(value, list):
logger.debug(f"列表验证失败: 不是列表类型")
return False
if len(value) < min_length:
logger.debug(f"列表验证失败: 长度小于 {min_length}")
return False
if max_length and len(value) > max_length:
logger.debug(f"列表验证失败: 长度大于 {max_length}")
return False
if item_validator:
for i, item in enumerate(value):
if not item_validator(item):
logger.debug(f"列表验证失败: 第 {i} 项验证失败")
return False
logger.debug(f"列表验证通过: 长度 {len(value)}")
return True
except Exception as e:
logger.error(f"验证列表时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_dict(value: Any, required_keys: List[str] = None,
key_validators: Dict[str, Callable] = None) -> bool:
"""验证字典"""
try:
if not isinstance(value, dict):
logger.debug(f"字典验证失败: 不是字典类型")
return False
# 检查必需键
if required_keys:
for key in required_keys:
if key not in value:
logger.debug(f"字典验证失败: 缺少必需键 {key}")
return False
# 检查键值验证器
if key_validators:
for key, validator in key_validators.items():
if key in value and not validator(value[key]):
logger.debug(f"字典验证失败: 键 {key} 的值验证失败")
return False
logger.debug(f"字典验证通过: 键数 {len(value)}")
return True
except Exception as e:
logger.error(f"验证字典时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_file_path(file_path: str, check_exists: bool = True,
check_readable: bool = False, check_writable: bool = False) -> bool:
"""验证文件路径"""
try:
from pathlib import Path
path = Path(file_path)
if check_exists and not path.exists():
logger.debug(f"文件路径验证失败: 文件不存在")
return False
if check_readable and not os.access(path, os.R_OK):
logger.debug(f"文件路径验证失败: 文件不可读")
return False
if check_writable and not os.access(path, os.W_OK):
logger.debug(f"文件路径验证失败: 文件不可写")
return False
logger.debug(f"文件路径验证通过: {file_path}")
return True
except Exception as e:
logger.error(f"验证文件路径时出错: {str(e)}", exc_info=True)
return False