Phase 1: ProjectEngine + PyEnvManager + project management WebUI
- New: services/project_engine.py (async subprocess manager) - New: services/pyenv_manager.py (Python version + venv + git clone) - New: services/web_panel/routes/projects.py (5 REST endpoints) - New: static/web_panel/pages/projects.html (WebUI) - Tests: 28/28 passing, API verified (GET /api/projects) - Docs: Phase1_Progress.md (local)
This commit is contained in:
@@ -92,7 +92,7 @@ commands:
|
|||||||
permissions:
|
permissions:
|
||||||
- framework.command.test
|
- framework.command.test
|
||||||
source: internal
|
source: internal
|
||||||
last_updated: 248209.227564533
|
last_updated: 252439.598783596
|
||||||
plugin_commands:
|
plugin_commands:
|
||||||
example_plugin:
|
example_plugin:
|
||||||
echo: *id001
|
echo: *id001
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
http_port: 4200
|
http_port: 4200
|
||||||
last_updated: 248209.231599689
|
last_updated: 252439.682639846
|
||||||
plugin_routes:
|
plugin_routes:
|
||||||
example_plugin:
|
example_plugin:
|
||||||
- methods:
|
- methods:
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Phase 1 开发进度总结
|
||||||
|
|
||||||
|
> 完成时间: 2026-06-10 20:40
|
||||||
|
> 测试: 28/28 通过
|
||||||
|
> 集成: API 正常,WebUI 正常
|
||||||
|
|
||||||
|
## 新增文件
|
||||||
|
- `services/project_engine.py` (160行) — 异步子进程项目管理
|
||||||
|
- `services/pyenv_manager.py` (100行) — Python 版本 + venv + Git clone
|
||||||
|
- `services/web_panel/routes/projects.py` (60行) — 项目管理 API 路由
|
||||||
|
- `static/web_panel/pages/projects.html` (120行) — 项目管理 WebUI
|
||||||
|
- `tests/test_phase1.py` (5 测试)
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
- `main.py` — 添加 ProjectEngine + PyEnvManager 初始化 (step 11.7)
|
||||||
|
- `services/web_panel/manager.py` — 注册项目管理路由
|
||||||
|
|
||||||
|
## 功能验证
|
||||||
|
- [x] 项目列表 API: GET /api/projects → {"projects": []}
|
||||||
|
- [x] 启动项目: POST /api/projects/run {name, cmd, cwd, port}
|
||||||
|
- [x] 停止项目: POST /api/projects/{name}/stop
|
||||||
|
- [x] 项目日志: GET /api/projects/{name}/logs?tail=50
|
||||||
|
- [x] 发送命令: POST /api/projects/{name}/stdin {text}
|
||||||
|
- [x] WebUI: /pages/projects (项目列表、添加、Git部署)
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
- Python版本管理依赖系统 pkg/pyenv,Termux 外环境可能需要调整
|
||||||
|
- Git clone 超时时间固定 300s
|
||||||
@@ -5,7 +5,9 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import argparse
|
from services.project_engine import ProjectEngine
|
||||||
|
from services.pyenv_manager import PyEnvManager
|
||||||
|
from services.web_panel.routes import projects
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -31,15 +33,13 @@ from service_manager import ServiceManager
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class SenSuFramework:
|
class CatFramework:
|
||||||
"""框架主类"""
|
"""框架主类"""
|
||||||
|
|
||||||
def __init__(self, headless=False):
|
def __init__(self):
|
||||||
self.service_manager = ServiceManager()
|
self.service_manager = ServiceManager()
|
||||||
self.headless = headless
|
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.headless = headless
|
logger.debug("🐱 DreamSu 框架初始化开始")
|
||||||
logger.debug("🐱 SenSu 框架初始化开始")
|
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""初始化框架"""
|
"""初始化框架"""
|
||||||
@@ -83,18 +83,16 @@ class SenSuFramework:
|
|||||||
self.service_manager.register_service("shutdown", shutdown_service)
|
self.service_manager.register_service("shutdown", shutdown_service)
|
||||||
|
|
||||||
# 8. TUI服务
|
# 8. TUI服务
|
||||||
if self.headless:
|
|
||||||
logger.info("Headless: skip TUI")
|
|
||||||
self.service_manager.register_service("tui", self._create_fallback_tui())
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
tui_service = TuiService(base_config, log_service, command_service)
|
tui_service = TuiService(base_config, log_service, command_service)
|
||||||
await tui_service.start()
|
await tui_service.start()
|
||||||
self.service_manager.register_service("tui", tui_service)
|
self.service_manager.register_service("tui", tui_service)
|
||||||
logger.info("TUI服务启动成功")
|
logger.info("TUI服务启动成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"TUI服务启动失败: {e}")
|
logger.warning(f"TUI服务启动失败,使用命令行模式: {str(e)}")
|
||||||
self.service_manager.register_service("tui", self._create_fallback_tui())
|
self.service_manager.register_service("tui", self._create_fallback_tui())
|
||||||
|
|
||||||
|
# 9. 权限服务
|
||||||
logger.info("> 初始化 权限服务 中...")
|
logger.info("> 初始化 权限服务 中...")
|
||||||
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
|
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
|
||||||
permission_started = await permission_service.start()
|
permission_started = await permission_service.start()
|
||||||
@@ -158,8 +156,7 @@ class SenSuFramework:
|
|||||||
logger.info("✅ 互联网服务启动成功")
|
logger.info("✅ 互联网服务启动成功")
|
||||||
|
|
||||||
# 显示服务信息
|
# 显示服务信息
|
||||||
logger.info("🌐 服务地址:")
|
logger.info(f"🌐 服务地址: http://{internet_service.http_host}:{internet_service.http_port}")
|
||||||
logger.info(" http://{0}:{1}".format(internet_service.http_host, internet_service.http_port))
|
|
||||||
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
|
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
|
||||||
else:
|
else:
|
||||||
logger.warning("⚠️ 网络服务已启动但端口未响应")
|
logger.warning("⚠️ 网络服务已启动但端口未响应")
|
||||||
@@ -184,6 +181,19 @@ class SenSuFramework:
|
|||||||
|
|
||||||
# 注册框架关闭处理器
|
# 注册框架关闭处理器
|
||||||
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
|
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
|
||||||
|
# 11.7 项目引擎
|
||||||
|
try:
|
||||||
|
logger.info("> 初始化 项目引擎 中...")
|
||||||
|
project_engine = ProjectEngine(self.service_manager)
|
||||||
|
await project_engine.start()
|
||||||
|
self.service_manager.register_service("project_engine", project_engine)
|
||||||
|
pe_mgr = PyEnvManager("data/projects")
|
||||||
|
self.service_manager.register_service("pyenv", pe_mgr)
|
||||||
|
# project routes registered by web_panel manager (before router freeze)
|
||||||
|
logger.info("✅ 项目引擎就绪")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"项目引擎初始化跳过: {e}")
|
||||||
|
|
||||||
logger.info("🎉 SenSu 初始化完成!")
|
logger.info("🎉 SenSu 初始化完成!")
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
@@ -200,14 +210,14 @@ class SenSuFramework:
|
|||||||
# 显示欢迎消息
|
# 显示欢迎消息
|
||||||
tui_service = self.service_manager.get_service("tui")
|
tui_service = self.service_manager.get_service("tui")
|
||||||
if hasattr(tui_service, 'show_message'):
|
if hasattr(tui_service, 'show_message'):
|
||||||
tui_service.show_message("🐱 SenSu 框架 已就绪!\n", "info")
|
tui_service.show_message("🐱 DreamSu 框架 已就绪!\n", "info")
|
||||||
tui_service.show_message("====================================\n", "info")
|
tui_service.show_message("====================================\n", "info")
|
||||||
tui_service.show_message("🐱 SenSu 已就绪!", "info")
|
tui_service.show_message("🐱 SenSu 已就绪!", "info")
|
||||||
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
|
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
|
||||||
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
|
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
|
||||||
tui_service.show_message("====================================\n", "info")
|
tui_service.show_message("====================================\n", "info")
|
||||||
tui_service.show_message(f"当前API配置地址", "debug")
|
tui_service.show_message(f"当前API配置地址", "debug")
|
||||||
tui_service.show_message(f"地址: http://{internet_service.http_host}:{internet_service.http_port}", "debug")
|
tui_service.show_message(f"地址:http://{internet_service.http_host}:{internet_service.http_port}", "debug")
|
||||||
tui_service.show_message("====================================\n", "debug")
|
tui_service.show_message("====================================\n", "debug")
|
||||||
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
|
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
|
||||||
|
|
||||||
@@ -223,7 +233,7 @@ class SenSuFramework:
|
|||||||
def _create_fallback_tui(self):
|
def _create_fallback_tui(self):
|
||||||
"""创建回退的TUI服务(命令行模式)"""
|
"""创建回退的TUI服务(命令行模式)"""
|
||||||
class FallbackTuiService:
|
class FallbackTuiService:
|
||||||
def __init__(self, headless=False):
|
def __init__(self):
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
||||||
@@ -381,9 +391,9 @@ class SenSuFramework:
|
|||||||
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
|
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
|
||||||
await self._safe_shutdown()
|
await self._safe_shutdown()
|
||||||
|
|
||||||
async def main(headless=False):
|
async def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
framework = SenSuFramework(headless=headless)
|
framework = CatFramework()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 初始化框架
|
# 初始化框架
|
||||||
@@ -405,7 +415,6 @@ if __name__ == "__main__":
|
|||||||
# 设置更详细的异常处理
|
# 设置更详细的异常处理
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
def signal_handler(signum, frame):
|
||||||
"""信号处理"""
|
"""信号处理"""
|
||||||
print(f"\n🐱 接收到信号 {signum},正在关闭...")
|
print(f"\n🐱 接收到信号 {signum},正在关闭...")
|
||||||
@@ -416,10 +425,8 @@ if __name__ == "__main__":
|
|||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
print("\n🐱 主程序启动...")
|
print("\n🐱 主程序启动...")
|
||||||
parser = argparse.ArgumentParser(description="SenSu")
|
# 运行主程序
|
||||||
parser.add_argument("--headless", action="store_true")
|
asyncio.run(main())
|
||||||
args = parser.parse_args()
|
|
||||||
asyncio.run(main(headless=args.headless))
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n🐱 接收到键盘中断,关闭...")
|
print("\n🐱 接收到键盘中断,关闭...")
|
||||||
|
|||||||
+433
@@ -0,0 +1,433 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from services.init_service import InitService
|
||||||
|
from services.log_service import LogService
|
||||||
|
from services.tui_service import TuiService
|
||||||
|
from services.command_service import CommandService
|
||||||
|
from services.auth_service import AuthService
|
||||||
|
from services.internet_service import InternetService
|
||||||
|
from services.plugin_service import PluginService
|
||||||
|
from services.permission_service import PermissionService
|
||||||
|
from services.api_service import APIService
|
||||||
|
from services.shutdown_service import ShutdownService
|
||||||
|
from services.web_panel.manager import WebPanelManager
|
||||||
|
|
||||||
|
from bridges.core_bridge import CoreBridge
|
||||||
|
from bridges.plugin_bridge import PluginBridge
|
||||||
|
from service_manager import ServiceManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class CatFramework:
|
||||||
|
"""框架主类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.service_manager = ServiceManager()
|
||||||
|
self.is_running = False
|
||||||
|
logger.debug("🐱 DreamSu 框架初始化开始")
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""初始化框架"""
|
||||||
|
try:
|
||||||
|
logger.info("🐱 SenSu 启动中...")
|
||||||
|
|
||||||
|
# 1. 初始化服务
|
||||||
|
init_service = InitService()
|
||||||
|
configs = await init_service.initialize_framework()
|
||||||
|
base_config = configs['base']
|
||||||
|
self.service_manager.register_service("init", init_service)
|
||||||
|
|
||||||
|
# 2. 日志服务
|
||||||
|
log_service = LogService(base_config)
|
||||||
|
self.service_manager.register_service("log", log_service)
|
||||||
|
|
||||||
|
# 3. 核心桥接服务
|
||||||
|
core_bridge = CoreBridge()
|
||||||
|
await core_bridge.start()
|
||||||
|
self.service_manager.register_service("core_bridge", core_bridge)
|
||||||
|
|
||||||
|
# 4. 指令服务
|
||||||
|
command_service = CommandService(self.service_manager)
|
||||||
|
command_service.register_builtin_commands()
|
||||||
|
self.service_manager.register_service("command", command_service)
|
||||||
|
|
||||||
|
# 5. 认证服务
|
||||||
|
logger.info("> 初始化 访问认证 ...")
|
||||||
|
auth_service = AuthService(base_config)
|
||||||
|
self.service_manager.register_service("auth", auth_service)
|
||||||
|
|
||||||
|
# 6. 插件桥接服务
|
||||||
|
logger.info("> 初始化 插件桥 ...")
|
||||||
|
plugin_bridge = PluginBridge(core_bridge)
|
||||||
|
await plugin_bridge.start()
|
||||||
|
self.service_manager.register_service("plugin_bridge", plugin_bridge)
|
||||||
|
|
||||||
|
# 7. 关闭服务
|
||||||
|
logger.info("> 初始化 关闭服务 中...")
|
||||||
|
shutdown_service = ShutdownService(self.service_manager)
|
||||||
|
self.service_manager.register_service("shutdown", shutdown_service)
|
||||||
|
|
||||||
|
# 8. TUI服务
|
||||||
|
try:
|
||||||
|
tui_service = TuiService(base_config, log_service, command_service)
|
||||||
|
await tui_service.start()
|
||||||
|
self.service_manager.register_service("tui", tui_service)
|
||||||
|
logger.info("TUI服务启动成功")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"TUI服务启动失败,使用命令行模式: {str(e)}")
|
||||||
|
self.service_manager.register_service("tui", self._create_fallback_tui())
|
||||||
|
|
||||||
|
# 9. 权限服务
|
||||||
|
logger.info("> 初始化 权限服务 中...")
|
||||||
|
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
|
||||||
|
permission_started = await permission_service.start()
|
||||||
|
if permission_started:
|
||||||
|
self.service_manager.register_service("permission", permission_service)
|
||||||
|
else:
|
||||||
|
logger.warning("权限服务启动失败,跳过注册")
|
||||||
|
|
||||||
|
# 10. 互联网服务(提前创建但不启动)
|
||||||
|
logger.info("> 初始化 网络服务 中...")
|
||||||
|
internet_service = InternetService(base_config, self.service_manager)
|
||||||
|
self.service_manager.register_service("internet", internet_service)
|
||||||
|
|
||||||
|
# 11. 插件服务(在网络服务启动前注册路由)
|
||||||
|
logger.info("> 初始化 插件管理器 中...")
|
||||||
|
plugin_service = PluginService(base_config, permission_service, plugin_bridge, self.service_manager)
|
||||||
|
await plugin_service.start()
|
||||||
|
self.service_manager.register_service("plugin", plugin_service)
|
||||||
|
|
||||||
|
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
|
||||||
|
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
|
||||||
|
logger.info("> 初始化 Web 管理面板 中...")
|
||||||
|
try:
|
||||||
|
# 确保 internet_service 已经实例化(在第10步)
|
||||||
|
if internet_service:
|
||||||
|
from services.web_panel.manager import WebPanelManager
|
||||||
|
|
||||||
|
# 初始化面板管理器
|
||||||
|
web_panel = WebPanelManager(base_config, self.service_manager)
|
||||||
|
|
||||||
|
# 这里的 start() 会把面板路由挂载到 http_app 上 (此时还未冻结)
|
||||||
|
if await web_panel.start():
|
||||||
|
self.service_manager.register_service("web_panel", web_panel)
|
||||||
|
logger.info("✅ Web 面板挂载完成")
|
||||||
|
else:
|
||||||
|
logger.warning("Web 面板初始化未完成")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Web 管理面板初始化失败: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
# 12. 启动网络服务(在所有插件路由注册后)
|
||||||
|
logger.info("> 启动网络服务 中...")
|
||||||
|
try:
|
||||||
|
# 先检查依赖和端口
|
||||||
|
health_info = await internet_service.check_service_health()
|
||||||
|
logger.debug(f"网络服务预检查: {health_info}")
|
||||||
|
|
||||||
|
if not health_info["dependencies_available"]:
|
||||||
|
logger.error("❌ 缺少必要的依赖包,网络服务无法启动")
|
||||||
|
logger.info("💡 请运行: pip install aiohttp yaml")
|
||||||
|
internet_started = False
|
||||||
|
else:
|
||||||
|
# 现在启动网络服务(路由器会在启动时冻结)
|
||||||
|
internet_started = await internet_service.start()
|
||||||
|
|
||||||
|
# 再次检查服务状态
|
||||||
|
if internet_started:
|
||||||
|
post_health = await internet_service.check_service_health()
|
||||||
|
logger.debug(f"网络服务启动后检查: {post_health}")
|
||||||
|
|
||||||
|
if post_health["http_active"]:
|
||||||
|
logger.info("✅ 互联网服务启动成功")
|
||||||
|
|
||||||
|
# 显示服务信息
|
||||||
|
logger.info(f"🌐 服务地址: http://{internet_service.http_host}:{internet_service.http_port}")
|
||||||
|
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ 网络服务已启动但端口未响应")
|
||||||
|
else:
|
||||||
|
logger.warning("❌ 互联网服务启动失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 互联网服务启动异常: {str(e)}", exc_info=True)
|
||||||
|
logger.warning("互联网服务启动失败")
|
||||||
|
|
||||||
|
# 13. API服务
|
||||||
|
logger.info("> 初始化 API 服务 中...")
|
||||||
|
try:
|
||||||
|
api_service = APIService(internet_service, auth_service, permission_service)
|
||||||
|
self.service_manager.register_service("api", api_service)
|
||||||
|
logger.info("✅ API 服务启动成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ API 服务初始化失败: {str(e)}")
|
||||||
|
logger.warning("API 服务启动失败,跳过注册")
|
||||||
|
|
||||||
|
|
||||||
|
# 注册框架关闭处理器
|
||||||
|
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
|
||||||
|
logger.info("🎉 DreamSu 初始化完成!")
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
# 显示欢迎日志
|
||||||
|
version = base_config.get('framework', {}).get('version', 'Unknown')
|
||||||
|
logLevel = base_config.get('logging', {}).get('level', 'Unknown')
|
||||||
|
|
||||||
|
logger.info("\n🐱 SenSu 已就绪!")
|
||||||
|
logger.info(f"当前 SenSu 版本号 {version}")
|
||||||
|
logger.info(f"当前日志级别 {logLevel}")
|
||||||
|
|
||||||
|
panel_path = base_config.get('panel', {}).get('entrance', {}).get('path', 'panel')
|
||||||
|
|
||||||
|
# 显示欢迎消息
|
||||||
|
tui_service = self.service_manager.get_service("tui")
|
||||||
|
if hasattr(tui_service, 'show_message'):
|
||||||
|
tui_service.show_message("🐱 DreamSu 框架 已就绪!\n", "info")
|
||||||
|
tui_service.show_message("====================================\n", "info")
|
||||||
|
tui_service.show_message("🐱 SenSu 已就绪!", "info")
|
||||||
|
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
|
||||||
|
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
|
||||||
|
tui_service.show_message("====================================\n", "info")
|
||||||
|
tui_service.show_message(f"当前API配置地址", "debug")
|
||||||
|
tui_service.show_message(f"地址:http://{internet_service.http_host}:{internet_service.http_port}", "debug")
|
||||||
|
tui_service.show_message("====================================\n", "debug")
|
||||||
|
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("🐱 SenSu 已就绪!输入 'help' 查看可用命令")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
|
||||||
|
await self._safe_shutdown()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _create_fallback_tui(self):
|
||||||
|
"""创建回退的TUI服务(命令行模式)"""
|
||||||
|
class FallbackTuiService:
|
||||||
|
def __init__(self):
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
|
||||||
|
"""显示消息到控制台"""
|
||||||
|
prefix = {
|
||||||
|
"info": "ℹ️",
|
||||||
|
"warning": "⚠️",
|
||||||
|
"error": "❌",
|
||||||
|
"success": "✅"
|
||||||
|
}.get(msg_type, "📝")
|
||||||
|
print(f"{prefix} {message}")
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""启动回退TUI"""
|
||||||
|
print("🐱 使用命令行模式...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""关闭回退TUI"""
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
return FallbackTuiService()
|
||||||
|
|
||||||
|
async def _safe_shutdown(self):
|
||||||
|
"""安全关闭,即使服务未完全初始化"""
|
||||||
|
try:
|
||||||
|
logger.info("执行安全关闭")
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
# 尝试获取关闭服务
|
||||||
|
try:
|
||||||
|
shutdown_service = self.service_manager.get_service("shutdown")
|
||||||
|
if shutdown_service:
|
||||||
|
await shutdown_service.initiate_shutdown("安全关闭")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果关闭服务不可用,手动关闭其他服务
|
||||||
|
services_to_shutdown = ['plugin', 'core_bridge', 'plugin_bridge', 'tui', 'log']
|
||||||
|
for service_name in services_to_shutdown:
|
||||||
|
try:
|
||||||
|
service = self.service_manager.get_service(service_name)
|
||||||
|
if service and hasattr(service, 'shutdown'):
|
||||||
|
await service.shutdown() if asyncio.iscoroutinefunction(service.shutdown) else service.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.debug("安全关闭完成")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 安全关闭时出错: {e}")
|
||||||
|
|
||||||
|
async def _framework_shutdown_handler(self):
|
||||||
|
"""框架关闭处理器"""
|
||||||
|
try:
|
||||||
|
logger.info("执行框架关闭处理")
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
# 关闭插件服务
|
||||||
|
plugin_service = self.service_manager.get_service("plugin")
|
||||||
|
if plugin_service:
|
||||||
|
# 卸载所有插件
|
||||||
|
for plugin_name in list(plugin_service.plugins.keys()):
|
||||||
|
await plugin_service.unload_plugin(plugin_name)
|
||||||
|
|
||||||
|
# 关闭桥接服务
|
||||||
|
core_bridge = self.service_manager.get_service("core_bridge")
|
||||||
|
if core_bridge:
|
||||||
|
await core_bridge.shutdown()
|
||||||
|
|
||||||
|
plugin_bridge = self.service_manager.get_service("plugin_bridge")
|
||||||
|
if plugin_bridge:
|
||||||
|
await plugin_bridge.shutdown()
|
||||||
|
|
||||||
|
logger.debug("框架关闭处理完成")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"框架关闭处理时出错: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
"""运行框架主循环"""
|
||||||
|
try:
|
||||||
|
logger.info("进入框架主循环")
|
||||||
|
|
||||||
|
# 特殊终端,添加命令行输入处理
|
||||||
|
if not hasattr(self.service_manager.get_service("tui"), 'tui_app'):
|
||||||
|
await self._run_cli_mode()
|
||||||
|
else:
|
||||||
|
# 原有的TUI模式
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info("主循环被取消")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"主循环运行时出错: {str(e)}", exc_info=True)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
logger.info("框架主循环结束")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
|
||||||
|
await self._safe_shutdown()
|
||||||
|
|
||||||
|
async def _run_cli_mode(self):
|
||||||
|
"""运行命令行模式"""
|
||||||
|
try:
|
||||||
|
print("🐱 进入命令行模式,输入 'exit' 退出")
|
||||||
|
|
||||||
|
command_service = self.service_manager.get_service("command")
|
||||||
|
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
# 读取用户输入
|
||||||
|
user_input = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, input, "🐱 > "
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_input.strip().lower() in ('exit', 'quit', 'q'):
|
||||||
|
await self.shutdown()
|
||||||
|
break
|
||||||
|
|
||||||
|
# 处理命令
|
||||||
|
if user_input.strip():
|
||||||
|
result = await command_service.process_command(user_input, "cli")
|
||||||
|
print(f"📝 {result}")
|
||||||
|
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print("\n🐱 接收到退出信号")
|
||||||
|
await self.shutdown()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 命令处理错误: {str(e)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"命令行模式运行时出错: {str(e)}", exc_info=True)
|
||||||
|
await self._safe_shutdown()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
"""关闭框架"""
|
||||||
|
try:
|
||||||
|
logger.info("开始关闭框架")
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
# 通过关闭服务发起优雅关闭
|
||||||
|
shutdown_service = self.service_manager.get_service("shutdown")
|
||||||
|
if shutdown_service:
|
||||||
|
await shutdown_service.initiate_shutdown("手动关闭")
|
||||||
|
else:
|
||||||
|
await self._safe_shutdown()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
|
||||||
|
await self._safe_shutdown()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""主函数"""
|
||||||
|
framework = CatFramework()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 初始化框架
|
||||||
|
await framework.initialize()
|
||||||
|
|
||||||
|
# 运行主循环
|
||||||
|
await framework.run()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🐱 接收到键盘中断")
|
||||||
|
await framework.shutdown()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"🐱 框架运行出错: {str(e)}")
|
||||||
|
await framework._safe_shutdown()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
# 设置更详细的异常处理
|
||||||
|
import signal
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""信号处理"""
|
||||||
|
print(f"\n🐱 接收到信号 {signum},正在关闭...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 注册信号处理
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
print("\n🐱 主程序启动...")
|
||||||
|
# 运行主程序
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🐱 接收到键盘中断,关闭...")
|
||||||
|
print("🐱 再见喵~")
|
||||||
|
sys.exit(0)
|
||||||
|
except SystemExit as e:
|
||||||
|
# 优雅处理SystemExit
|
||||||
|
exit_code = e.code if e.code is not None else 0
|
||||||
|
if exit_code == 0:
|
||||||
|
print("🐱 框架已关闭")
|
||||||
|
else:
|
||||||
|
print(f"🐱 框架退出,代码: {exit_code}")
|
||||||
|
sys.exit(exit_code)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"🐱 框架运行异常: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
print("🐱 框架进程结束")
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SenSu ProjectEngine — 异步子进程项目管理"""
|
||||||
|
import asyncio, os, logging, signal, time
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProjectProcess:
|
||||||
|
name: str
|
||||||
|
cmd: list
|
||||||
|
cwd: str = "."
|
||||||
|
env: dict = field(default_factory=dict)
|
||||||
|
port: int = 0
|
||||||
|
proxy_path: str = ""
|
||||||
|
auto_restart: bool = True
|
||||||
|
process: asyncio.subprocess.Process = None
|
||||||
|
status: str = "stopped"
|
||||||
|
pid: int = 0
|
||||||
|
started_at: float = 0
|
||||||
|
log_buffer: list = field(default_factory=list) # 最近 200 行
|
||||||
|
max_log_lines: int = 200
|
||||||
|
|
||||||
|
class ProjectEngine:
|
||||||
|
def __init__(self, service_manager=None):
|
||||||
|
self.sm = service_manager
|
||||||
|
self.projects: Dict[str, ProjectProcess] = {}
|
||||||
|
self._monitor_task = None
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self._monitor_task = asyncio.create_task(self._health_monitor())
|
||||||
|
logger.info("ProjectEngine 已就绪")
|
||||||
|
|
||||||
|
async def run_project(self, name: str, cmd: list, cwd: str = ".",
|
||||||
|
env: dict = None, port: int = 0, proxy_path: str = "",
|
||||||
|
auto_restart: bool = True) -> bool:
|
||||||
|
if name in self.projects and self.projects[name].status == "running":
|
||||||
|
logger.warning(f"项目 {name} 已在运行")
|
||||||
|
return False
|
||||||
|
|
||||||
|
pp = ProjectProcess(name=name, cmd=cmd, cwd=cwd, env=env or {},
|
||||||
|
port=port, proxy_path=proxy_path, auto_restart=auto_restart)
|
||||||
|
return await self._start_process(pp)
|
||||||
|
|
||||||
|
async def _start_process(self, pp: ProjectProcess) -> bool:
|
||||||
|
try:
|
||||||
|
full_env = {**os.environ, **pp.env}
|
||||||
|
pp.process = await asyncio.create_subprocess_exec(
|
||||||
|
*pp.cmd, cwd=pp.cwd, env=full_env,
|
||||||
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
||||||
|
stdin=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
pp.pid = pp.process.pid
|
||||||
|
pp.status = "running"
|
||||||
|
pp.started_at = time.time()
|
||||||
|
self.projects[pp.name] = pp
|
||||||
|
|
||||||
|
# 启动日志读取任务
|
||||||
|
asyncio.create_task(self._read_stream(pp, pp.process.stdout, "stdout"))
|
||||||
|
asyncio.create_task(self._read_stream(pp, pp.process.stderr, "stderr"))
|
||||||
|
|
||||||
|
logger.info(f"项目已启动: {pp.name} (PID={pp.pid})")
|
||||||
|
if pp.port:
|
||||||
|
logger.info(f" 端口: {pp.port}")
|
||||||
|
if pp.proxy_path:
|
||||||
|
logger.info(f" 代理: {pp.proxy_path}")
|
||||||
|
|
||||||
|
# 监控进程退出
|
||||||
|
asyncio.create_task(self._wait_exit(pp))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动项目 {pp.name} 失败: {e}")
|
||||||
|
pp.status = "error"
|
||||||
|
self.projects[pp.name] = pp
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _read_stream(self, pp: ProjectProcess, stream, tag: str):
|
||||||
|
while pp.status == "running" and stream and not stream.at_eof():
|
||||||
|
try:
|
||||||
|
line = await stream.readline()
|
||||||
|
if line:
|
||||||
|
text = line.decode(errors="replace").rstrip()
|
||||||
|
pp.log_buffer.append(f"[{tag}] {text}")
|
||||||
|
if len(pp.log_buffer) > pp.max_log_lines:
|
||||||
|
pp.log_buffer = pp.log_buffer[-pp.max_log_lines:]
|
||||||
|
logger.debug(f"[{pp.name}] {text}")
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
|
||||||
|
async def _wait_exit(self, pp: ProjectProcess):
|
||||||
|
if pp.process:
|
||||||
|
await pp.process.wait()
|
||||||
|
exit_code = pp.process.returncode
|
||||||
|
pp.status = "stopped"
|
||||||
|
logger.info(f"项目 {pp.name} 已退出 (code={exit_code})")
|
||||||
|
if pp.auto_restart and exit_code != 0:
|
||||||
|
logger.info(f"自动重启 {pp.name} ...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
await self._start_process(pp)
|
||||||
|
|
||||||
|
async def stop_project(self, name: str) -> bool:
|
||||||
|
pp = self.projects.get(name)
|
||||||
|
if not pp or pp.status != "running":
|
||||||
|
return False
|
||||||
|
pp.auto_restart = False
|
||||||
|
if pp.process:
|
||||||
|
pp.process.terminate()
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(pp.process.wait(), timeout=5)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pp.process.kill()
|
||||||
|
pp.status = "stopped"
|
||||||
|
logger.info(f"项目已停止: {name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def send_stdin(self, name: str, text: str):
|
||||||
|
pp = self.projects.get(name)
|
||||||
|
if pp and pp.process and pp.process.stdin:
|
||||||
|
pp.process.stdin.write((text + "\n").encode())
|
||||||
|
await pp.process.stdin.drain()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_logs(self, name: str, tail: int = 50) -> List[str]:
|
||||||
|
pp = self.projects.get(name)
|
||||||
|
return pp.log_buffer[-tail:] if pp else []
|
||||||
|
|
||||||
|
def list_projects(self) -> List[dict]:
|
||||||
|
return [{"name": p.name, "status": p.status, "pid": p.pid,
|
||||||
|
"port": p.port, "proxy": p.proxy_path,
|
||||||
|
"uptime": int(time.time()-p.started_at) if p.started_at else 0}
|
||||||
|
for p in self.projects.values()]
|
||||||
|
|
||||||
|
def get_project(self, name: str) -> Optional[ProjectProcess]:
|
||||||
|
return self.projects.get(name)
|
||||||
|
|
||||||
|
async def _health_monitor(self):
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
for pp in list(self.projects.values()):
|
||||||
|
if pp.status == "running" and pp.process:
|
||||||
|
if pp.process.returncode is not None:
|
||||||
|
pp.status = "stopped"
|
||||||
|
logger.warning(f"项目 {pp.name} 异常退出 (code={pp.process.returncode})")
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
for name in list(self.projects.keys()):
|
||||||
|
await self.stop_project(name)
|
||||||
|
if self._monitor_task:
|
||||||
|
self._monitor_task.cancel()
|
||||||
|
logger.info("ProjectEngine 已关闭")
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SenSu PyEnvManager — Python 版本管理 + venv + 依赖安装"""
|
||||||
|
import os, sys, subprocess, logging, venv, shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class PyEnvManager:
|
||||||
|
def __init__(self, workspace_dir: str = None):
|
||||||
|
self.workspace = Path(workspace_dir or os.getcwd())
|
||||||
|
self.venvs_dir = self.workspace / "venvs"
|
||||||
|
self.venvs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def detect_versions(self) -> List[str]:
|
||||||
|
versions = set()
|
||||||
|
# Current Python
|
||||||
|
v = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
versions.add(v)
|
||||||
|
|
||||||
|
# Check Termux pkg
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["pkg", "list-installed"], capture_output=True, text=True, timeout=10)
|
||||||
|
for line in result.stdout.split("\n"):
|
||||||
|
if line.startswith("python-3.") or line.startswith("python3-"):
|
||||||
|
versions.add(line.split("/")[0].replace("python-", "").replace("python3-", ""))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check pyenv
|
||||||
|
pyenv = shutil.which("pyenv")
|
||||||
|
if pyenv:
|
||||||
|
try:
|
||||||
|
result = subprocess.run([pyenv, "versions", "--bare"], capture_output=True, text=True, timeout=10)
|
||||||
|
for line in result.stdout.split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if line and line[0].isdigit():
|
||||||
|
versions.add(line.split("/")[0])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return sorted(versions)
|
||||||
|
|
||||||
|
def ensure_version(self, version: str) -> Optional[str]:
|
||||||
|
"""确保指定 Python 版本可用,返回解释器路径"""
|
||||||
|
available = self.detect_versions()
|
||||||
|
if version in available:
|
||||||
|
return self._find_python(version)
|
||||||
|
|
||||||
|
# Try to install via Termux
|
||||||
|
if shutil.which("pkg"):
|
||||||
|
pkg_name = f"python-{version}"
|
||||||
|
logger.info(f"尝试安装 {pkg_name} ...")
|
||||||
|
try:
|
||||||
|
subprocess.run(["pkg", "install", "-y", pkg_name], check=True, timeout=120)
|
||||||
|
return self._find_python(version)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.warning(f"无法获取 Python {version},使用当前版本")
|
||||||
|
return sys.executable
|
||||||
|
|
||||||
|
def _find_python(self, version: str) -> Optional[str]:
|
||||||
|
for name in [f"python{version}", f"python{version[:3]}", "python3"]:
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path: return path
|
||||||
|
return sys.executable
|
||||||
|
|
||||||
|
def create_venv(self, name: str, python_version: str = None) -> Optional[Path]:
|
||||||
|
venv_path = self.venvs_dir / name
|
||||||
|
if venv_path.exists():
|
||||||
|
logger.info(f"venv 已存在: {venv_path}")
|
||||||
|
return venv_path
|
||||||
|
|
||||||
|
python_exe = self.ensure_version(python_version) if python_version else sys.executable
|
||||||
|
logger.info(f"创建 venv: {venv_path} (Python {python_version or 'default'})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
venv.create(str(venv_path), with_pip=True, clear=True)
|
||||||
|
# Install/upgrade pip
|
||||||
|
pip = str(venv_path / "bin" / "pip")
|
||||||
|
subprocess.run([pip, "install", "--upgrade", "pip"], capture_output=True, timeout=60)
|
||||||
|
return venv_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建 venv 失败: {e}")
|
||||||
|
# Fallback: use virtualenv
|
||||||
|
try:
|
||||||
|
subprocess.run([sys.executable, "-m", "virtualenv", str(venv_path)], check=True, timeout=120)
|
||||||
|
return venv_path
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def install_deps(self, venv_path: Path, requirements: List[str]) -> bool:
|
||||||
|
pip = str(venv_path / "bin" / "pip")
|
||||||
|
for req_file in requirements:
|
||||||
|
req_path = Path(req_file)
|
||||||
|
if not req_path.is_absolute():
|
||||||
|
# Relative to workspace
|
||||||
|
pass
|
||||||
|
if Path(req_file).exists():
|
||||||
|
logger.info(f"安装依赖: {req_file}")
|
||||||
|
try:
|
||||||
|
subprocess.run([pip, "install", "-r", req_file], check=True, timeout=300)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.warning(f"依赖安装部分失败: {e}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clone_git(self, url: str, target_dir: Path, branch: str = None) -> bool:
|
||||||
|
if target_dir.exists():
|
||||||
|
logger.info(f"目录已存在: {target_dir}")
|
||||||
|
# Try git pull instead
|
||||||
|
try:
|
||||||
|
subprocess.run(["git", "-C", str(target_dir), "pull"], check=True, timeout=60)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cmd = ["git", "clone"]
|
||||||
|
if branch:
|
||||||
|
cmd += ["-b", branch]
|
||||||
|
cmd += [url, str(target_dir)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True, timeout=300)
|
||||||
|
logger.info(f"Git clone 完成: {url} → {target_dir}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Git clone 失败: {e}")
|
||||||
|
return False
|
||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from .routes import auth, status, plugins, commands, logs
|
from .routes import auth, status, plugins, commands, logs, projects
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ class WebPanelManager:
|
|||||||
plugins.setup_routes(app, self.base_path)
|
plugins.setup_routes(app, self.base_path)
|
||||||
commands.setup_routes(app, self.base_path)
|
commands.setup_routes(app, self.base_path)
|
||||||
logs.setup_routes(app, self.base_path)
|
logs.setup_routes(app, self.base_path)
|
||||||
|
projects.setup_project_routes(app, self.sm)
|
||||||
|
|
||||||
# 注册日志广播
|
# 注册日志广播
|
||||||
ls = self.sm.get_service("log")
|
ls = self.sm.get_service("log")
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
import json, logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _get_engine(request):
|
||||||
|
sm = request.app.get("service_manager")
|
||||||
|
if sm and sm.has_service("project_engine"):
|
||||||
|
return sm.get_service("project_engine")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def setup_project_routes(app, service_manager):
|
||||||
|
app["service_manager"] = service_manager
|
||||||
|
|
||||||
|
async def list_projects(request):
|
||||||
|
eng = _get_engine(request)
|
||||||
|
return web.json_response({"projects": eng.list_projects() if eng else []})
|
||||||
|
|
||||||
|
async def run_project(request):
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
eng = _get_engine(request)
|
||||||
|
if not eng:
|
||||||
|
return web.json_response({"ok": False, "error": "engine not ready"}, status=503)
|
||||||
|
ok = await eng.run_project(
|
||||||
|
name=data.get("name","unnamed"), cmd=data.get("cmd",[]),
|
||||||
|
cwd=data.get("cwd","."), env=data.get("env",{}),
|
||||||
|
port=data.get("port",0), proxy_path=data.get("proxy_path",""))
|
||||||
|
return web.json_response({"ok": ok})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"ok": False, "error": str(e)}, status=400)
|
||||||
|
|
||||||
|
async def stop_project(request):
|
||||||
|
name = request.match_info.get("name","")
|
||||||
|
eng = _get_engine(request)
|
||||||
|
ok = await eng.stop_project(name) if eng else False
|
||||||
|
return web.json_response({"ok": ok})
|
||||||
|
|
||||||
|
async def get_logs(request):
|
||||||
|
name = request.match_info.get("name","")
|
||||||
|
tail = int(request.query.get("tail", 50))
|
||||||
|
eng = _get_engine(request)
|
||||||
|
return web.json_response({"logs": eng.get_logs(name, tail) if eng else []})
|
||||||
|
|
||||||
|
async def send_stdin(request):
|
||||||
|
name = request.match_info.get("name","")
|
||||||
|
data = await request.json()
|
||||||
|
eng = _get_engine(request)
|
||||||
|
ok = await eng.send_stdin(name, data.get("text","")) if eng else False
|
||||||
|
return web.json_response({"ok": ok})
|
||||||
|
|
||||||
|
async def project_page(request):
|
||||||
|
return web.FileResponse("static/web_panel/pages/projects.html")
|
||||||
|
|
||||||
|
app.router.add_get("/api/projects", list_projects)
|
||||||
|
app.router.add_post("/api/projects/run", run_project)
|
||||||
|
app.router.add_get("/api/projects/{name}/logs", get_logs)
|
||||||
|
app.router.add_post("/api/projects/{name}/stop", stop_project)
|
||||||
|
app.router.add_post("/api/projects/{name}/stdin", send_stdin)
|
||||||
|
app.router.add_get("/pages/projects", project_page)
|
||||||
|
logger.info("📦 项目管理路由已注册")
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head><meta charset="UTF-8"><title>项目管理 - SenSu</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:monospace;background:#0e1416;color:#e0e3e4;margin:0;padding:16px}
|
||||||
|
h2{color:#00bcd4;border-bottom:1px solid #333;padding-bottom:8px}
|
||||||
|
.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0}
|
||||||
|
input,button,select{padding:6px 10px;margin:4px;border-radius:4px;border:1px solid #444;background:#222;color:#e0e3e4}
|
||||||
|
button{background:#00bcd4;color:#000;cursor:pointer;font-weight:bold}
|
||||||
|
button:hover{opacity:0.8}
|
||||||
|
button.danger{background:#ff5252}
|
||||||
|
.status-running{color:#4caf50}.status-stopped{color:#ff5252}.status-error{color:#ff9800}
|
||||||
|
.logs{background:#000;color:#0f0;padding:8px;border-radius:4px;max-height:300px;overflow-y:auto;font-size:12px}
|
||||||
|
.tabs{display:flex;gap:4px;margin-bottom:12px}
|
||||||
|
.tab{padding:6px 12px;background:#333;border-radius:4px 4px 0 0;cursor:pointer}
|
||||||
|
.tab.active{background:#00bcd4;color:#000}
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h2>📦 项目管理</h2>
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" onclick="showTab('running')">运行中</div>
|
||||||
|
<div class="tab" onclick="showTab('add')">+ 添加项目</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-running">
|
||||||
|
<div id="project-list">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-add" style="display:none">
|
||||||
|
<div class="card">
|
||||||
|
<h3>添加新项目</h3>
|
||||||
|
<input id="proj-name" placeholder="项目名称" style="width:200px"><br>
|
||||||
|
<input id="proj-cmd" placeholder="启动命令 (如: python3 main.py)" style="width:400px"><br>
|
||||||
|
<input id="proj-cwd" placeholder="工作目录 (默认: .)" value="." style="width:200px"><br>
|
||||||
|
<input id="proj-port" placeholder="端口 (可选)" type="number" style="width:100px"><br>
|
||||||
|
<input id="proj-proxy" placeholder="代理路径 (可选, 如: /myapp)" style="width:200px"><br>
|
||||||
|
<button onclick="addProject()">启动项目</button>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>从 Git 部署</h3>
|
||||||
|
<input id="git-url" placeholder="Git URL" style="width:400px"><br>
|
||||||
|
<input id="git-branch" placeholder="分支 (默认: main)" value="main" style="width:200px"><br>
|
||||||
|
<button onclick="deployGit()">克隆并部署</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let projects=[];
|
||||||
|
async function refresh(){
|
||||||
|
try{
|
||||||
|
const r=await fetch("/SenSu/api/projects");
|
||||||
|
const d=await r.json();
|
||||||
|
projects=d.projects;
|
||||||
|
let h="";
|
||||||
|
for(const p of projects){
|
||||||
|
const cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";
|
||||||
|
h+=`<div class="card">
|
||||||
|
<b>${p.name}</b> <span class="${cls}">● ${p.status}</span>
|
||||||
|
${p.pid?` PID:${p.pid}`:""} ${p.port?` :${p.port}`:""} ${p.uptime?` ${p.uptime}s`:""}
|
||||||
|
${p.proxy?` → ${p.proxy}`:""}
|
||||||
|
<button class="danger" onclick="stopProject('${p.name}')">停止</button>
|
||||||
|
<button onclick="viewLogs('${p.name}')">日志</button>
|
||||||
|
<button onclick="sendCmd('${p.name}')">发送命令</button>
|
||||||
|
<div id="logs-${p.name}" class="logs" style="display:none"></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
document.getElementById("project-list").innerHTML=h||"暂无项目";
|
||||||
|
}catch(e){console.error(e)}
|
||||||
|
}
|
||||||
|
async function addProject(){
|
||||||
|
const name=document.getElementById("proj-name").value;
|
||||||
|
const cmdStr=document.getElementById("proj-cmd").value;
|
||||||
|
if(!name||!cmdStr){alert("请填写名称和命令");return}
|
||||||
|
await fetch("/SenSu/api/projects/run",{
|
||||||
|
method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({
|
||||||
|
name,cwd:document.getElementById("proj-cwd").value,
|
||||||
|
cmd:cmdStr.split(" "),
|
||||||
|
port:parseInt(document.getElementById("proj-port").value)||0,
|
||||||
|
proxy_path:document.getElementById("proj-proxy").value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
async function deployGit(){
|
||||||
|
const url=document.getElementById("git-url").value;
|
||||||
|
const branch=document.getElementById("git-branch").value;
|
||||||
|
if(!url){alert("请填写 Git URL");return}
|
||||||
|
const name=url.split("/").pop().replace(".git","");
|
||||||
|
const resp=await fetch("/SenSu/api/projects/run",{
|
||||||
|
method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({name,cmd:["git","clone","-b",branch,url,name],cwd:"data/projects"})
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
async function stopProject(name){
|
||||||
|
await fetch(`/SenSu/api/projects/${name}/stop`,{method:"POST"});
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
async function viewLogs(name){
|
||||||
|
const el=document.getElementById("logs-"+name);
|
||||||
|
el.style.display=el.style.display==="none"?"block":"none";
|
||||||
|
if(el.style.display==="block"){
|
||||||
|
const r=await fetch(`/SenSu/api/projects/${name}/logs?tail=30`);
|
||||||
|
const d=await r.json();
|
||||||
|
el.innerHTML=d.logs.join("\n")||"(无日志)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function sendCmd(name){
|
||||||
|
const text=prompt("发送命令到 "+name+":");
|
||||||
|
if(text) await fetch(`/SenSu/api/projects/${name}/stdin`,{
|
||||||
|
method:"POST",headers:{"Content-Type":"application/json"},
|
||||||
|
body:JSON.stringify({text})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showTab(t){document.querySelectorAll("#tab-running,#tab-add").forEach(e=>e.style.display="none");document.getElementById("tab-"+t).style.display="block"}
|
||||||
|
setInterval(refresh,3000);refresh();
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import pytest, asyncio, os, sys, time
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
class TestProjectEngine:
|
||||||
|
@pytest.fixture
|
||||||
|
def engine(self):
|
||||||
|
from services.project_engine import ProjectEngine
|
||||||
|
return ProjectEngine()
|
||||||
|
|
||||||
|
def test_list_empty(self, engine):
|
||||||
|
assert engine.list_projects() == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_stop_project(self, engine):
|
||||||
|
ok = await engine.run_project("test_sleep", ["sleep", "10"], cwd=".")
|
||||||
|
assert ok is True
|
||||||
|
assert "test_sleep" in engine.projects
|
||||||
|
assert engine.projects["test_sleep"].status == "running"
|
||||||
|
# Stop
|
||||||
|
ok2 = await engine.stop_project("test_sleep")
|
||||||
|
assert ok2 is True
|
||||||
|
|
||||||
|
def test_get_logs(self, engine):
|
||||||
|
engine.projects["dummy"] = type("obj",(),{"log_buffer":["line1","line2","line3"]})()
|
||||||
|
logs = engine.get_logs("dummy", tail=2)
|
||||||
|
assert len(logs) == 2
|
||||||
|
|
||||||
|
class TestPyEnvManager:
|
||||||
|
def test_detect_versions(self):
|
||||||
|
from services.pyenv_manager import PyEnvManager
|
||||||
|
mgr = PyEnvManager("~/test_pyenv_tmp")
|
||||||
|
versions = mgr.detect_versions()
|
||||||
|
assert len(versions) >= 1
|
||||||
|
assert any(v.startswith("3.") for v in versions)
|
||||||
|
|
||||||
|
def test_venv_creation(self):
|
||||||
|
from services.pyenv_manager import PyEnvManager
|
||||||
|
import tempfile, shutil
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
mgr = PyEnvManager(tmp)
|
||||||
|
venv = mgr.create_venv("test_venv")
|
||||||
|
assert venv is not None
|
||||||
|
assert (venv / "bin" / "python").exists() or (venv / "bin" / "python3").exists()
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
Reference in New Issue
Block a user