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,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)}")
|
||||
Reference in New Issue
Block a user