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
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import secrets
import logging
from aiohttp import web
from ..utils.auth import panel_auth
logger = logging.getLogger(__name__)
# 全局 Session 存储 (内存型)
# 格式: { "token_string": { "username": "...", "perms": [...] } }
PANEL_SESSION_STORE = {}
def setup_routes(app, prefix=''):
"""注册面板认证路由"""
# 🟢 关键:将 Session Store 挂载到 app,供拦截器读取
app['panel_session_store'] = PANEL_SESSION_STORE
# 路由注册
app.router.add_post(f'{prefix}/api/login', handle_login)
# 退出和状态检查都需要拦截
app.router.add_post(f'{prefix}/api/logout', panel_auth(handle_logout))
app.router.add_get(f'{prefix}/api/auth/status', panel_auth(handle_auth_status))
async def handle_login(req):
"""处理面板登录"""
try:
data = await req.json()
username = data.get('username')
password = data.get('password')
cfg = req.app.get('panel_config', {})
cfg_user = cfg.get('username', 'admin')
cfg_pass = cfg.get('password', 'admin')
# 校验配置中的账号密码
if username == cfg_user and password == cfg_pass:
# 登录成功:生成 Token
token = secrets.token_hex(16)
# 写入 Session Store
user_info = {
"username": username,
"perms": ["admin"],
"login_time": __import__('time').time()
}
PANEL_SESSION_STORE[token] = user_info
logger.info(f"✅ 面板登录成功: {username} (Session: {token[:4]}...)")
resp = web.json_response({"success": True, "username": username})
# 设置 Cookie
resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax")
return resp
else:
logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误")
return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401)
except Exception as e:
logger.error(f"登录异常: {e}")
return web.json_response({"error": str(e)}, status=500)
async def handle_logout(req):
"""处理退出登录"""
token = req.cookies.get("panel_token")
if token and token in PANEL_SESSION_STORE:
del PANEL_SESSION_STORE[token]
logger.info(f"👋 用户退出登录")
resp = web.json_response({"success": True})
resp.del_cookie("panel_token")
return resp
async def handle_auth_status(req):
"""获取当前认证状态 (被 panel_auth 拦截,能进来说明已认证)"""
user = req.get('user', {})
return web.json_response({
"authenticated": True,
"username": user.get("username", "Unknown"),
"perms": user.get("perms", [])
})
+15
View File
@@ -0,0 +1,15 @@
from aiohttp import web
from ..utils.auth import panel_auth
def setup_routes(app, prefix=''):
app.router.add_post(f'{prefix}/api/command', panel_auth(exec_cmd))
async def exec_cmd(req):
d = await req.json()
cs = req.app.get('service_manager').get_service("command")
if not cs: return web.json_response({"error": "Missing"}, 503)
try:
res = await cs.execute_command(d.get('command',''))
return web.json_response({"success": True, "output": str(res)})
except Exception as e:
return web.json_response({"success": False, "error": str(e)})
+30
View File
@@ -0,0 +1,30 @@
import json, asyncio
from aiohttp import web
from ..utils.auth import panel_auth
active_ws = set()
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/logs/ws', panel_auth(ws_handler))
async def ws_handler(req):
ws = web.WebSocketResponse(heartbeat=30.0)
await ws.prepare(req)
active_ws.add(ws)
try:
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
d = json.loads(msg.data)
if d.get('action') == 'set_level':
ls = req.app.get('log_service')
if ls: ls.set_level(d.get('level','INFO'))
finally: active_ws.discard(ws)
return ws
def broadcast_log(log_record):
if not active_ws: return
payload = json.dumps({"type":"log", "level":log_record.get('level','INFO'),
"message":log_record.get('simple_message',''), "timestamp":log_record.get('timestamp',0)})
for ws in list(active_ws):
if not ws.closed: asyncio.ensure_future(ws.send_str(payload))
else: active_ws.discard(ws)
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from aiohttp import web
from ..utils.auth import panel_auth
logger = logging.getLogger(__name__)
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/plugins', panel_auth(list_plugins))
app.router.add_post(f'{prefix}/api/plugins/{{name}}/{{action}}', panel_auth(manage_plugin))
app.router.add_get(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(get_perms))
app.router.add_post(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(set_perms))
async def list_plugins(req):
sm = req.app.get('service_manager')
if not sm:
return web.json_response({"error": "Service Manager 未初始化"}, status=503)
ps = sm.get_service("plugin")
if not ps:
return web.json_response({"plugins": []})
data = []
for name, info in ps.plugin_info.items():
data.append({
"name": name,
"version": getattr(info, 'version', '?'),
"running": name in ps.plugins,
"enabled": True
})
return web.json_response({"plugins": data})
async def manage_plugin(req):
sm = req.app.get('service_manager')
if not sm: return web.json_response({"error": "SM Missing"}, 503)
name = req.match_info['name']
action = req.match_info['action']
ps = sm.get_service("plugin")
if not ps: return web.json_response({"error": "Plugin Service Missing"}, 503)
try:
if action in ('disable', 'unload'):
await ps.unload_plugin(name)
elif action == 'enable':
await ps.load_plugin(name)
elif action == 'reload':
await ps.unload_plugin(name)
await ps.load_plugin(name)
return web.json_response({"success": True, "msg": "操作成功"})
except Exception as e:
logger.error(f"插件操作失败: {e}")
return web.json_response({"success": False, "error": str(e)})
async def get_perms(req):
return web.json_response({"plugin": req.match_info['name'], "permissions": ["read", "write"]})
async def set_perms(req):
return web.json_response({"success": True})
+27
View File
@@ -0,0 +1,27 @@
import time
from aiohttp import web
from ..utils.auth import panel_auth
from ..utils.system_info import SystemInfoCollector
collector = SystemInfoCollector()
def setup_routes(app, prefix=''):
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
async def get_framework(req):
sm = req.app.get('service_manager')
if not sm: return web.json_response({"error": "Missing"}, 500)
ps = sm.get_service("plugin")
# 🟢 修复:使用 sm.start_time 属性
uptime = time.time() - getattr(sm, 'start_time', time.time())
return web.json_response({
"version": "Alpha_0.2.0",
"uptime": int(uptime), # 取整秒
"plugins": len(ps.plugins) if ps else 0
})
async def get_system(req):
return web.json_response(collector.get_all())