feat: 更新服务 — ZIP下载/备份/回滚/重启

UpdateService:
- 分支感知: 从 base_config 读取 branch, 拼接 archive/{branch}.zip
- 定时检查: 每6h 拉取 version.json 比对版本
- 更新前自动打包备份 (zip, 保留最新5个)
- 保护用户配置: base_config.yaml / plugins/*/config.yaml / data/
- 子进程 pip install 依赖

API:
- GET /api/updates — 更新状态
- POST /api/updates/check — 手动检查
- POST /api/updates/apply — 执行更新

回滚:
- python main.py --rollback  — 恢复到最新备份
- 无备份时提示

重启:
- .restart_flag 信号文件 → 优雅退出 → systemd 拉起

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-13 20:32:42 +08:00
parent 6e0ed23012
commit c20417ff93
7 changed files with 401 additions and 2 deletions
+61
View File
@@ -12,6 +12,8 @@ from services.pyenv_manager import PyEnvManager
from services.proxy_service import ProxyService
from services.web_panel.utils.system_info import SystemInfoCollector
from services.sensu_db import SenSuDB
from services.update_service import UpdateService
from services.update_service import UpdateService
import os
from pathlib import Path
@@ -223,6 +225,13 @@ class SenSuFramework:
await proxy_service.start()
self.service_manager.register_service("proxy", proxy_service)
logger.info("✅ 项目引擎+反向代理就绪")
logger.info("> 初始化 更新检测 中...")
update_service = UpdateService(self.service_manager)
await update_service.start()
self.service_manager.register_service("update", update_service)
logger.info("✅ 更新检测就绪")
except Exception as e:
logger.warning(f"引擎/代理初始化跳过: {e}")
@@ -355,7 +364,14 @@ class SenSuFramework:
# 特殊终端,添加命令行输入处理
tui = self.service_manager.get_service("tui")
if self.headless:
import os as _os
restart_flag = _os.path.join(_os.path.dirname(__file__), ".restart_flag")
while self.is_running:
if _os.path.exists(restart_flag):
logger.info("🔄 检测到重启标记, 执行优雅重启...")
_os.remove(restart_flag)
await self._restart_framework()
break
try: await asyncio.sleep(1)
except asyncio.CancelledError: break
elif not hasattr(tui, 'tui_app'):
@@ -377,6 +393,13 @@ class SenSuFramework:
except Exception as e:
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def _restart_framework(self):
"""优雅重启: 关闭所有服务后退出 (由 systemd/supervisor 自动拉起)"""
logger.info("🔄 执行优雅重启...")
await self.shutdown()
import sys as _sys
_sys.exit(0)
async def _run_cli_mode(self):
"""运行命令行模式"""
@@ -429,6 +452,38 @@ class SenSuFramework:
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
def rollback():
"""回滚到最新备份"""
import zipfile, shutil, os, glob
backup_dir = os.path.join(os.path.dirname(__file__), "data", "backups")
if not os.path.isdir(backup_dir):
print("📭 没有备份目录")
return
backups = sorted(glob.glob(os.path.join(backup_dir, "SenSu_backup_*.zip")), key=os.path.getmtime, reverse=True)
if not backups:
print("📭 没有可用的备份包")
return
latest = backups[0]
name = os.path.basename(latest)
print(f"📦 回滚到: {name}")
try:
import tempfile
with tempfile.TemporaryDirectory() as tmp:
with zipfile.ZipFile(latest, "r") as zf:
zf.extractall(tmp)
project_root = os.path.dirname(__file__)
for item in os.listdir(tmp):
src = os.path.join(tmp, item)
dst = os.path.join(project_root, item)
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
print("✅ 回滚完成, 请重启 SenSu")
except Exception as e:
print(f"❌ 回滚失败: {e}")
async def main(headless=False):
"""主函数"""
framework = SenSuFramework(headless=headless)
@@ -466,7 +521,13 @@ if __name__ == "__main__":
# 运行主程序
parser = argparse.ArgumentParser(description="SenSu")
parser.add_argument("--headless", action="store_true")
parser.add_argument("--rollback", action="store_true", help="回滚到最新备份")
args = parser.parse_args()
if args.rollback:
rollback()
sys.exit(0)
asyncio.run(main(headless=args.headless))
except KeyboardInterrupt: