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:
@@ -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}
|
||||
Reference in New Issue
Block a user