refactor: 集中版本逻辑并添加版本检查API

- 将 `get_current_version` 函数从 `application.py` 移动到 `helpers.py` 以实现更好的代码组织和可重用性。
- 在 `version_routes.py` 中引入新的 API 端点 `/api/version/check`,以提供当前版本、最新可用版本和更新状态。
- 更新了 `base.html`,通过调用新的 API 端点,使用 JavaScript 异步获取和显示版本信息。这取代了以前服务器端渲染版本信息的方式,并增加了定期检查。
- 移除了应用程序启动时(`lifespan` 函数)的自动更新检查,因为版本检查现在由前端通过 API 处理。
- 在 `routes.py` 中注册了新的版本路由。
This commit is contained in:
snaily
2025-04-26 03:04:40 +08:00
parent cd257a9406
commit 705d602dee
5 changed files with 119 additions and 37 deletions
+6 -22
View File
@@ -11,6 +11,7 @@ from app.exception.exceptions import setup_exception_handlers
from app.router.routes import setup_routers from app.router.routes import setup_routers
from app.service.key.key_manager import get_key_manager_instance from app.service.key.key_manager import get_key_manager_instance
from app.database.connection import connect_to_db, disconnect_from_db from app.database.connection import connect_to_db, disconnect_from_db
from app.utils.helpers import get_current_version # Import from helpers
from app.database.initialization import initialize_database from app.database.initialization import initialize_database
from app.scheduler.key_checker import start_scheduler, stop_scheduler from app.scheduler.key_checker import start_scheduler, stop_scheduler
from app.service.update.update_service import check_for_updates from app.service.update.update_service import check_for_updates
@@ -20,28 +21,11 @@ logger = get_application_logger()
# Define project paths using pathlib # Define project paths using pathlib
# Assuming this file is at app/core/application.py # Assuming this file is at app/core/application.py
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
VERSION_FILE_PATH = PROJECT_ROOT / "VERSION" # VERSION_FILE_PATH = PROJECT_ROOT / "VERSION" # Removed: Defined in helpers.py
STATIC_DIR = PROJECT_ROOT / "app" / "static" STATIC_DIR = PROJECT_ROOT / "app" / "static"
TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates" TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates"
# Removed _get_current_version function definition, moved to helpers.py
def _get_current_version(default_version: str = "0.0.0") -> str:
"""Reads the current version from the VERSION file."""
version_file = VERSION_FILE_PATH # Use Path object
try:
# Use Path object's open method
with version_file.open('r', encoding='utf-8') as f:
version = f.read().strip()
if not version:
logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.")
return default_version
return version
except FileNotFoundError:
logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.")
return default_version
except IOError as e:
logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.")
return default_version
# 初始化模板引擎,并添加全局变量 # 初始化模板引擎,并添加全局变量
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@@ -88,7 +72,7 @@ def _stop_scheduler():
async def _perform_update_check(app: FastAPI): async def _perform_update_check(app: FastAPI):
"""Checks for updates and stores the info in app.state.""" """Checks for updates and stores the info in app.state."""
update_available, latest_version, error_message = await check_for_updates() update_available, latest_version, error_message = await check_for_updates()
current_version = _get_current_version() # Read from VERSION file current_version = get_current_version() # Use imported function
update_info = { update_info = {
"update_available": update_available, "update_available": update_available,
"latest_version": latest_version, "latest_version": latest_version,
@@ -119,7 +103,7 @@ async def lifespan(app: FastAPI):
await _setup_database_and_config(settings) # Pass settings object await _setup_database_and_config(settings) # Pass settings object
# Perform update check after core components are ready # Perform update check after core components are ready
await _perform_update_check(app) # await _perform_update_check(app) # Removed: Version check moved to frontend API call
# Start the scheduler # Start the scheduler
_start_scheduler() _start_scheduler()
@@ -148,7 +132,7 @@ def create_app() -> FastAPI:
# 创建FastAPI应用 # 创建FastAPI应用
# Read version from file for consistency # Read version from file for consistency
current_version = _get_current_version() current_version = get_current_version() # Use imported function
app = FastAPI( app = FastAPI(
title="Gemini Balance API", title="Gemini Balance API",
description="Gemini API代理服务,支持负载均衡和密钥管理", description="Gemini API代理服务,支持负载均衡和密钥管理",
+2 -1
View File
@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
from app.core.security import verify_auth_token from app.core.security import verify_auth_token
from app.log.logger import get_routes_logger from app.log.logger import get_routes_logger
from app.router import error_log_routes, gemini_routes, openai_routes, config_routes, scheduler_routes, stats_routes # 新增导入 stats_routes from app.router import error_log_routes, gemini_routes, openai_routes, config_routes, scheduler_routes, stats_routes, version_routes # 新增导入 version_routes
from app.service.key.key_manager import get_key_manager_instance from app.service.key.key_manager import get_key_manager_instance
from app.service.stats_service import StatsService from app.service.stats_service import StatsService
@@ -33,6 +33,7 @@ def setup_routers(app: FastAPI) -> None:
app.include_router(error_log_routes.router) app.include_router(error_log_routes.router)
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由 app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
app.include_router(stats_routes.router) # 包含 stats API 路由 app.include_router(stats_routes.router) # 包含 stats API 路由
app.include_router(version_routes.router) # 包含 version API 路由
# 添加页面路由 # 添加页面路由
setup_page_routes(app) setup_page_routes(app)
+38
View File
@@ -0,0 +1,38 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
from app.service.update.update_service import check_for_updates
from app.utils.helpers import get_current_version
from app.log.logger import get_update_logger
router = APIRouter(prefix="/api/version", tags=["Version"])
logger = get_update_logger()
class VersionInfo(BaseModel):
current_version: str = Field(..., description="当前应用程序版本")
latest_version: Optional[str] = Field(None, description="可用的最新版本")
update_available: bool = Field(False, description="是否有可用更新")
error_message: Optional[str] = Field(None, description="检查更新时发生的错误信息")
@router.get("/check", response_model=VersionInfo, summary="检查应用程序更新")
async def get_version_info():
"""
检查当前应用程序版本与最新的 GitHub release 版本。
"""
try:
current_version = get_current_version() # Use imported function
update_available, latest_version, error_message = await check_for_updates()
# Log the result for debugging
logger.info(f"Version check API result: current={current_version}, latest={latest_version}, available={update_available}, error='{error_message}'")
return VersionInfo(
current_version=current_version,
latest_version=latest_version,
update_available=update_available,
error_message=error_message
)
except Exception as e:
logger.error(f"Error in /api/version/check endpoint: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="检查版本信息时发生内部错误")
+45 -14
View File
@@ -202,20 +202,9 @@
<span class="text-xs text-yellow-600 font-semibold"> <span class="text-xs text-yellow-600 font-semibold">
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗 <i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
</span> </span>
{% if request and request.app.state.update_info %} <span id="version-info-container" class="inline-block">
{% set update_info = request.app.state.update_info %} <!-- Version info will be loaded here by JavaScript -->
<span class="mx-1">|</span> </span>
<span class="text-xs text-gray-500">v{{ update_info.current_version }}</span>
{% if update_info.update_available %}
<span class="mx-1">|</span>
<a href="https://github.com/snailyp/gemini-balance/releases/latest" target="_blank" class="text-yellow-600 hover:text-yellow-800 transition duration-300 animate-pulse">
<i class="fas fa-arrow-up"></i> 新版本: v{{ update_info.latest_version }}
</a>
{% elif update_info.error_message and update_info.error_message != 'Checking...' %}
<span class="mx-1">|</span>
<span class="text-xs text-red-500" title="{{ update_info.error_message }}">更新检查失败</span>
{% endif %}
{% endif %}
</div> </div>
<!-- 通用JS --> <!-- 通用JS -->
@@ -279,6 +268,48 @@
}, 300); // Short delay to show spinner }, 300); // Short delay to show spinner
} }
// --- Version Check ---
const versionInfoContainer = document.getElementById('version-info-container');
async function fetchVersionInfo() {
if (!versionInfoContainer) return;
versionInfoContainer.innerHTML = '<span class="mx-1">|</span><span class="text-xs text-gray-400">检查更新中...</span>'; // Initial loading state
try {
const response = await fetch('/api/version/check');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
let versionHtml = `<span class="mx-1">|</span><span class="text-xs text-gray-500">v${data.current_version}</span>`;
if (data.update_available) {
versionHtml += `
<span class="mx-1">|</span>
<a href="https://github.com/snailyp/gemini-balance/releases/latest" target="_blank" class="text-yellow-600 hover:text-yellow-800 transition duration-300 animate-pulse">
<i class="fas fa-arrow-up"></i> 新版本: v${data.latest_version}
</a>`;
} else if (data.error_message) {
versionHtml += `
<span class="mx-1">|</span>
<span class="text-xs text-red-500" title="${data.error_message}">更新检查失败</span>`;
} else {
versionHtml += `<span class="mx-1">|</span><span class="text-xs text-green-500">已是最新</span>`; // Indicate up-to-date
}
versionInfoContainer.innerHTML = versionHtml;
} catch (error) {
console.error('Error fetching version info:', error);
versionInfoContainer.innerHTML = `<span class="mx-1">|</span><span class="text-xs text-red-500" title="无法连接到服务器或解析响应">更新检查失败</span>`;
}
}
// Fetch immediately on load
fetchVersionInfo();
// Fetch periodically (e.g., every hour)
setInterval(fetchVersionInfo, 3600000); // 3600000 ms = 1 hour
</script> </script>
{% block body_scripts %}{% endblock %} {% block body_scripts %}{% endblock %}
</body> </body>
+28
View File
@@ -6,9 +6,19 @@ import re
import base64 import base64
import requests import requests
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
import logging # Import logging
from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS
# Define logger for helper functions if needed, or use specific loggers
helper_logger = logging.getLogger("app.utils") # Or use a more specific logger if available
# Define project root and version file path here for get_current_version
# Assuming this file is at app/utils/helpers.py
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
VERSION_FILE_PATH = PROJECT_ROOT / "VERSION"
def extract_mime_type_and_data(base64_string: str) -> Tuple[Optional[str], str]: def extract_mime_type_and_data(base64_string: str) -> Tuple[Optional[str], str]:
""" """
@@ -146,3 +156,21 @@ def is_valid_api_key(key: str) -> bool:
return False return False
def get_current_version(default_version: str = "0.0.0") -> str:
"""Reads the current version from the VERSION file."""
version_file = VERSION_FILE_PATH # Use Path object defined above
try:
# Use Path object's open method
with version_file.open('r', encoding='utf-8') as f:
version = f.read().strip()
if not version:
helper_logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.")
return default_version
return version
except FileNotFoundError:
helper_logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.")
return default_version
except IOError as e:
helper_logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.")
return default_version