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:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
_patches_applied/
|
||||||
|
*.bak.*
|
||||||
|
.env
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
@@ -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` 来启动框架
|
||||||
|
```
|
||||||
@@ -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'
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)}")
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"example_plugin": "granted"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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. APIService(API服务)
|
||||||
|
|
||||||
|
## 二、目录结构详细分析
|
||||||
|
|
||||||
|
### 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`入手,逐步理解框架的各个组件,然后根据业务需求开发定制插件。
|
||||||
@@ -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 # 验证工具
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -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("🐱 框架进程结束")
|
||||||
@@ -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)
|
||||||
@@ -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: "🐱 你好喵~"
|
||||||
@@ -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: "执行框架命令"
|
||||||
@@ -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
|
||||||
@@ -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("所有服务关闭完成")
|
||||||
@@ -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("所有服务关闭完成")
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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)}")
|
||||||
|
|
||||||
@@ -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)
|
||||||
@@ -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)}")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from .manager import WebPanelManager
|
||||||
|
__all__ = ["WebPanelManager"]
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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", [])
|
||||||
|
})
|
||||||
@@ -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)})
|
||||||
@@ -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)
|
||||||
@@ -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})
|
||||||
@@ -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())
|
||||||
@@ -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
|
||||||
@@ -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')
|
||||||
@@ -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}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
@@ -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(); }
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -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: () => {}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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`;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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: () => {}
|
||||||
|
};
|
||||||
@@ -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}")
|
||||||
@@ -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}插件操作"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
@@ -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'
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user