Initial commit: SenSu Alpha 0.2.0

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

Fixes applied:
- PBKDF2-SHA256 auth (was plain SHA256)
- Auth bypass removed (was allow-all on fail)
- Bare excepts replaced with logged errors
- CatFramework/DreamSu -> SenSu naming unified
- ServiceManager: health checks + startup_order
- Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
2026-06-10 12:27:14 +08:00
commit e6875f0b4b
78 changed files with 14843 additions and 0 deletions
+604
View File
@@ -0,0 +1,604 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import shlex
from typing import Dict, List, Callable, Any
from dataclasses import dataclass
import os
logger = logging.getLogger(__name__)
@dataclass
class Command:
"""命令数据类"""
name: str
handler: Callable
description: str
permissions: List[str]
source: str = "internal"
class CommandService:
"""指令服务"""
def __init__(self, service_manager=None):
self.commands: Dict[str, Command] = {}
self.command_history: List[Dict] = []
self.max_history_size = 100
self.service_manager = service_manager # 添加服务管理器引用
logger.debug("CommandService初始化开始")
def register_command(self, name: str, handler: Callable, description: str = "",
permissions: List[str] = None, source: str = "plugins"):
"""注册命令"""
# 来源如 internal plugins system 等
try:
if name in self.commands:
logger.warning(f"命令 {name} 已存在,将被覆盖")
self.commands[name] = Command(
name=name,
handler=handler,
description=description or f"命令: {name}",
permissions=permissions or [],
source=source
)
logger.debug(f"注册命令: {name} (来源: {source})")
except Exception as e:
logger.error(f"注册命令 {name} 时出错: {str(e)}", exc_info=True)
raise
async def _handle_permission_command(self, command: str, args: List[str], source: str) -> str:
"""处理权限相关命令"""
try:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
return await permission_service.process_permission_command(command, args)
except Exception as e:
logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True)
return f"❌ 处理权限命令时出错: {str(e)}"
async def process_command(self, command_string: str, source: str = "unknown") -> Any:
"""处理命令"""
try:
logger.debug(f"处理命令: '{command_string}' (来源: {source})")
# 解析命令
parts = shlex.split(command_string.strip())
if not parts:
logger.warning("空命令")
return "空命令"
command_name = parts[0]
args = parts[1:]
# 记录命令历史
self._add_to_history(command_string, source)
# 检查权限命令
permission_commands = ['pmallow', 'pmdeny', 'pmignore', 'permissions',
'pmpending', 'pmrequests', 'pm_plugin_status', 'pmhelp', 'pmtest']
if command_name in permission_commands:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
# 直接调用权限服务处理命令
return await permission_service.process_permission_command(command_name, args)
# 查找其他命令
if command_name not in self.commands:
logger.warning(f"未知命令: {command_name}")
return f"未知命令: {command_name}"
command = self.commands[command_name]
# 执行命令
try:
result = await self._execute_command(command, args, source)
logger.debug(f"命令执行成功: {command_name}")
return result
except Exception as e:
logger.error(f"命令执行失败 {command_name}: {str(e)}", exc_info=True)
return f"命令执行错误: {str(e)}"
except Exception as e:
logger.error(f"处理命令时出错: {str(e)}", exc_info=True)
return f"命令处理错误: {str(e)}"
async def _execute_command(self, command: Command, args: List[str], source: str) -> Any:
"""执行命令"""
try:
# 检查处理器类型
if asyncio.iscoroutinefunction(command.handler):
result = await command.handler(*args)
else:
result = command.handler(*args)
logger.debug(f"命令 {command.name} 执行完成")
return result
except TypeError as e:
logger.error(f"命令参数错误 {command.name}: {str(e)}", exc_info=True)
raise ValueError(f"参数错误: {str(e)}")
except Exception as e:
logger.error(f"命令执行异常 {command.name}: {str(e)}", exc_info=True)
raise
def _add_to_history(self, command: str, source: str):
"""添加到命令历史"""
try:
history_entry = {
"command": command,
"source": source,
"timestamp": asyncio.get_event_loop().time()
}
self.command_history.append(history_entry)
# 限制历史记录大小
if len(self.command_history) > self.max_history_size:
self.command_history.pop(0)
logger.debug(f"命令历史记录添加,当前大小: {len(self.command_history)}")
except Exception as e:
logger.error(f"添加命令历史时出错: {str(e)}", exc_info=True)
def get_command_list(self) -> List[Dict]:
"""获取命令列表"""
try:
command_list = []
for name, cmd in self.commands.items():
command_list.append({
"name": name,
"description": cmd.description,
"permissions": cmd.permissions,
"source": cmd.source
})
logger.debug(f"获取命令列表,共 {len(command_list)} 个命令")
return command_list
except Exception as e:
logger.error(f"获取命令列表时出错: {str(e)}", exc_info=True)
return []
def get_command_history(self, limit: int = 10) -> List[Dict]:
"""获取命令历史"""
try:
history = self.command_history[-limit:]
logger.debug(f"获取命令历史,返回 {len(history)} 条记录")
return history
except Exception as e:
logger.error(f"获取命令历史时出错: {str(e)}", exc_info=True)
return []
def register_builtin_commands(self):
"""注册内置命令"""
try:
logger.debug("开始注册内置命令")
# 帮助命令
self.register_command(
name="help",
handler=self._cmd_help,
description="显示帮助信息",
permissions=["framework.command.help.read"],
source="internal"
)
# 测试日志命令
self.register_command(
name="testlog",
handler=self._cmd_test_log,
description="生成测试日志",
permissions=["framework.command.test"],
source="internal"
)
# 状态命令
self.register_command(
name="status",
handler=self._cmd_status,
description="显示框架状态",
permissions=["framework.status.read"],
source="internal"
)
# 历史命令
self.register_command(
name="history",
handler=self._cmd_history,
description="显示命令历史",
permissions=["framework.command.history.read"],
source="internal"
)
# 网络诊断
self.register_command(
name="netdiag",
handler=self._cmd_netdiag,
description="网络服务诊断",
permissions=["framework.network.diagnose"],
source="internal"
)
# 权限管理命令组 - 保留注册但不使用(在process_command中直接处理)
# 这些注册是为了在help命令中显示
permission_commands = [
("pmallow", "权限管理: 同意权限请求"),
("pmdeny", "权限管理: 拒绝权限请求"),
("pmignore", "权限管理: 暂时忽略权限请求"),
("permissions", "权限管理: 显示权限状态"),
("pmpending", "权限管理: 查看待授权请求列表"),
("pmrequests", "权限管理: 查看待授权请求列表(别名)"),
("pm_plugin_status", "权限管理: 查看插件权限状态"),
("pmtest", "权限管理: 测试权限配置文件"),
("pmhelp", "权限管理: 显示权限命令帮助")
]
for cmd_name, description in permission_commands:
self.register_command(
name=cmd_name,
handler=self._cmd_permission, # 使用统一的备用处理器
description=description,
permissions=["framework.permission.read"],
source="internal"
)
# 滚动控制命令组
scroll_commands = [
("scroll", "滚动控制: 手动滚动到底部"),
("autoscroll", "滚动控制: 切换自动滚动")
]
# 脚手架命令
self.register_command(
name="create-plugin",
handler=self._cmd_create_plugin,
description="创建新插件脚手架",
permissions=["framework.scaffold.plugin"],
source="internal"
)
logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令")
for cmd_name, description in scroll_commands:
self.register_command(
name=cmd_name,
handler=self._cmd_scroll_control,
description=description,
permissions=["framework.tui.control"],
source="internal"
)
logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令")
except Exception as e:
logger.error(f"注册内置命令时出错: {str(e)}", exc_info=True)
raise
async def _cmd_netdiag(self, *args) -> str:
"""网络诊断命令"""
try:
internet_service = self.service_manager.get_service("internet")
result = ["🔧 **网络服务诊断报告**"]
result.append("=" * 50)
if not internet_service:
result.append("❌ 网络服务未注册")
result.append("\n💡 **可能的原因:**")
result.append(" 1. 网络服务启动失败")
result.append(" 2. 依赖包缺失 (aiohttp)")
result.append(" 3. 端口被占用")
result.append(" 4. 权限不足")
result.append("\n🔧 **解决方案:**")
result.append(" - 检查上方日志中的错误信息")
result.append(" - 运行: pip install aiohttp")
result.append(" - 尝试更换端口号")
result.append(" - 使用 sudo (如果需要)")
return "\n".join(result)
# 获取健康信息
health_info = await internet_service.check_service_health()
result.append(f"🔄 服务运行: {'✅ 是' if health_info.get('is_running') else '❌ 否'}")
result.append(f"🔌 HTTP端口: {health_info.get('http_port', 'N/A')}")
result.append(f"📡 WebSocket端口: {health_info.get('websocket_port', 'N/A')}")
result.append(f"🌐 HTTP活跃: {'✅ 是' if health_info.get('http_active') else '❌ 否'}")
result.append(f"📦 依赖状态: {'✅ 正常' if health_info.get('dependencies_available') else '❌ 缺失'}")
if health_info.get('error'):
result.append(f"❌ 错误信息: {health_info['error']}")
# 端口占用检查
if not health_info.get('http_active') and health_info.get('is_running'):
result.append("\n⚠️ **端口问题检测:**")
result.append(" HTTP服务已启动但端口未响应")
result.append(" 可能被防火墙阻止或配置错误")
# 路由信息
routes = internet_service.get_plugin_routes()
total_routes = sum(len(plugin_routes) for plugin_routes in routes.values())
result.append(f"\n🛣️ 注册路由: {total_routes}")
for plugin_name, plugin_routes in routes.items():
result.append(f" 📍 {plugin_name}: {len(plugin_routes)} 个路由")
return "\n".join(result)
except Exception as e:
logger.error(f"网络诊断命令执行失败: {str(e)}")
return f"❌ 网络诊断失败: {str(e)}"
async def _cmd_scroll_control(self, *args) -> str:
"""处理滚动控制命令"""
try:
tui_service = self.service_manager.get_service("tui")
if not tui_service:
return "❌ TUI服务不可用"
if not args:
return "🔧 滚动控制命令\n💡 使用: scroll [log|message|all]\n💡 使用: autoscroll [on|off|toggle] [log|message|all]"
command = args[0].lower()
if command == "scroll":
target = args[1] if len(args) > 1 else "all"
if target not in ["log", "message", "all"]:
return "❌ 无效的目标,请使用: log, message, all"
return tui_service.scroll_to_bottom(target)
elif command == "autoscroll":
if len(args) < 2:
return "❌ 请指定操作: on, off, toggle"
action = args[1].lower()
target = args[2] if len(args) > 2 else "all"
if target not in ["log", "message", "all"]:
return "❌ 无效的目标,请使用: log, message, all"
if action == "on":
return tui_service.toggle_auto_scroll(target, True)
elif action == "off":
return tui_service.toggle_auto_scroll(target, False)
elif action == "toggle":
return tui_service.toggle_auto_scroll(target, None)
else:
return "❌ 无效的操作,请使用: on, off, toggle"
else:
return "❌ 未知滚动命令\n💡 可用命令: scroll, autoscroll"
except Exception as e:
logger.error(f"处理滚动命令时出错: {str(e)}")
return f"❌ 滚动命令错误: {str(e)}"
async def _cmd_permission(self, *args) -> str:
"""处理权限相关命令 - 备用处理器"""
try:
permission_service = self.service_manager.get_service("permission")
if not permission_service:
return "❌ 权限服务不可用"
# 如果没有参数,显示通用帮助
if not args:
return "🔐 权限管理命令\n💡 使用 pmhelp 查看详细帮助"
# 否则直接转发到权限服务
command_name = str(args[0]).lower()
permission_args = [str(arg) for arg in args[1:]] if len(args) > 1 else []
return await permission_service.process_permission_command(command_name, permission_args)
except Exception as e:
logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True)
return f"❌ 权限命令错误: {str(e)}"
async def _cmd_help(self, *args) -> str:
"""帮助命令处理器"""
try:
commands = self.get_command_list()
if not commands:
return "❌ 没有可用的命令"
help_text = ["📋 **可用命令:**", ""]
# 按来源分组显示命令
commands_by_source = {}
for cmd in commands:
source = cmd['source']
if source not in commands_by_source:
commands_by_source[source] = []
commands_by_source[source].append(cmd)
# 显示内置命令
if 'internal' in commands_by_source:
help_text.append("🔧 **内置命令:**")
for cmd in commands_by_source['internal']:
help_text.append(f" 🟢 {cmd['name']:15} - {cmd['description']}")
help_text.append("")
# 显示插件命令
if 'plugin' in commands_by_source:
help_text.append("🔌 **插件命令:**")
for cmd in commands_by_source['plugin']:
help_text.append(f" 🟡 {cmd['name']:15} - {cmd['description']}")
help_text.append("")
# 显示系统命令
if 'system' in commands_by_source:
help_text.append("⚙️ **系统命令:**")
for cmd in commands_by_source['system']:
help_text.append(f" 🔵 {cmd['name']:15} - {cmd['description']}")
# 添加使用提示
help_text.extend([
"",
"💡 **使用提示:**",
" - 输入命令名称执行命令",
" - 使用 'status' 查看框架状态",
" - 使用 'history' 查看命令历史",
" - 使用 'permissions' 管理插件权限"
])
return "\n".join(help_text)
except Exception as e:
logger.error(f"处理help命令时出错: {str(e)}", exc_info=True)
return f"❌ 帮助命令错误: {str(e)}"
async def _cmd_status(self, *args) -> str:
"""状态命令处理器"""
try:
status_info = [
f"命令服务状态:",
f" 注册命令数: {len(self.commands)}",
f" 历史记录数: {len(self.command_history)}",
f" 最大历史大小: {self.max_history_size}"
]
return "\n".join(status_info)
except Exception as e:
logger.error(f"处理status命令时出错: {str(e)}", exc_info=True)
return f"状态命令错误: {str(e)}"
async def _cmd_history(self, *args) -> str:
"""历史命令处理器"""
try:
limit = 10
if args and args[0].isdigit():
limit = min(int(args[0]), 50) # 限制最大50条
history = self.get_command_history(limit)
if not history:
return "没有命令历史"
history_text = [f"最近 {len(history)} 条命令历史:"]
for i, entry in enumerate(reversed(history), 1):
history_text.append(f" {i}. [{entry['source']}] {entry['command']}")
return "\n".join(history_text)
except Exception as e:
logger.error(f"处理history命令时出错: {str(e)}", exc_info=True)
return f"历史命令错误: {str(e)}"
async def _cmd_test_log(self, *args) -> str:
"""测试日志命令"""
try:
logger.debug("这是一条DEBUG测试日志")
logger.info("这是一条INFO测试日志")
logger.warning("这是一条WARNING测试日志")
logger.error("这是一条ERROR测试日志")
return "✅ 测试日志已生成,请检查TUI显示"
except Exception as e:
return f"❌ 测试日志生成失败: {str(e)}"
async def _cmd_create_plugin(self, *args) -> str:
"""创建新插件脚手架"""
try:
import re
import shutil
from pathlib import Path
from string import Template
# 1. 参数解析
if not args:
return "❌ 用法: create-plugin <插件名> [--author <作者>] [--desc <描述>]\n💡 插件名需为小写字母/数字/下划线,如: my_cool_plugin"
plugin_name = args[0]
author = "Unknown"
description = "暂无描述"
# 解析可选参数
i = 1
while i < len(args):
if args[i] == "--author" and i + 1 < len(args):
author = args[i+1]
i += 2
elif args[i] == "--desc" and i + 1 < len(args):
description = args[i+1]
i += 2
else:
i += 1
# 2. 命名校验
if not re.match(r'^[a-z][a-z0-9_]*$', plugin_name):
return "❌ 插件名格式错误。请使用小写字母开头,仅包含小写字母、数字和下划线(如: data_sync)"
plugin_dir = Path("plugins") / plugin_name
if plugin_dir.exists():
return f"❌ 插件目录已存在: {plugin_dir}"
# 3. 模板路径
template_dir = Path(os.getenv("SENSU_CODE_DIR", ".")) / "templates" / "plugin"
if not template_dir.exists():
return "❌ 模板目录不存在: templates/plugin/"
# 4. 创建目录与渲染文件
plugin_dir.mkdir(parents=True, exist_ok=True)
context = {
"plugin_name": plugin_name,
"author": author,
"description": description
}
for template_file in template_dir.iterdir():
if template_file.is_file() and template_file.name.endswith(".template"):
with open(template_file, 'r', encoding='utf-8') as f:
tpl = Template(f.read())
content = tpl.safe_substitute(context)
target_name = template_file.stem
target_path = plugin_dir / target_name
with open(target_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.debug(f"脚手架文件生成: {target_path}")
return (
f"✅ 插件脚手架创建成功!\n"
f"📁 路径: {plugin_dir}\n"
f"👤 作者: {author}\n"
f"📝 描述: {description}\n\n"
f"🔧 下一步:\n"
f" 1. 编辑 {plugin_dir}/__init__.py 实现业务逻辑\n"
f" 2. 运行框架自动加载插件\n"
f" 3. 使用 `help` 查看可用命令"
)
except Exception as e:
logger.error(f"创建插件脚手架失败: {str(e)}", exc_info=True)
return f"❌ 创建失败: {str(e)}"
def shutdown(self):
"""关闭指令服务"""
try:
logger.info("关闭指令服务")
self.commands.clear()
self.command_history.clear()
logger.debug("指令服务关闭完成")
except Exception as e:
logger.error(f"关闭指令服务时出错: {str(e)}", exc_info=True)