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

View File

@@ -11,6 +11,7 @@ from app.exception.exceptions import setup_exception_handlers
from app.router.routes import setup_routers
from app.service.key.key_manager import get_key_manager_instance
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.scheduler.key_checker import start_scheduler, stop_scheduler
from app.service.update.update_service import check_for_updates
@@ -20,28 +21,11 @@ logger = get_application_logger()
# Define project paths using pathlib
# Assuming this file is at app/core/application.py
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"
TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates"
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
# Removed _get_current_version function definition, moved to helpers.py
# 初始化模板引擎,并添加全局变量
templates = Jinja2Templates(directory="app/templates")
@@ -88,7 +72,7 @@ def _stop_scheduler():
async def _perform_update_check(app: FastAPI):
"""Checks for updates and stores the info in app.state."""
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_available": update_available,
"latest_version": latest_version,
@@ -119,7 +103,7 @@ async def lifespan(app: FastAPI):
await _setup_database_and_config(settings) # Pass settings object
# 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_scheduler()
@@ -148,7 +132,7 @@ def create_app() -> FastAPI:
# 创建FastAPI应用
# Read version from file for consistency
current_version = _get_current_version()
current_version = get_current_version() # Use imported function
app = FastAPI(
title="Gemini Balance API",
description="Gemini API代理服务支持负载均衡和密钥管理",

View File

@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
from app.core.security import verify_auth_token
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.stats_service import StatsService
@@ -33,6 +33,7 @@ def setup_routers(app: FastAPI) -> None:
app.include_router(error_log_routes.router)
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
app.include_router(stats_routes.router) # 包含 stats API 路由
app.include_router(version_routes.router) # 包含 version API 路由
# 添加页面路由
setup_page_routes(app)

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="检查版本信息时发生内部错误")

View File

@@ -202,20 +202,9 @@
<span class="text-xs text-yellow-600 font-semibold">
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
</span>
{% if request and request.app.state.update_info %}
{% set update_info = request.app.state.update_info %}
<span class="mx-1">|</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 %}
<span id="version-info-container" class="inline-block">
<!-- Version info will be loaded here by JavaScript -->
</span>
</div>
<!-- 通用JS -->
@@ -279,6 +268,48 @@
}, 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>
{% block body_scripts %}{% endblock %}
</body>

View File

@@ -6,9 +6,19 @@ import re
import base64
import requests
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
# 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]:
"""
@@ -146,3 +156,21 @@ def is_valid_api_key(key: str) -> bool:
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