diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..6a0ca2d
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+2.0.7
\ No newline at end of file
diff --git a/app/config/config.py b/app/config/config.py
index a2099b6..54d9ad0 100644
--- a/app/config/config.py
+++ b/app/config/config.py
@@ -21,7 +21,6 @@ from app.log.logger import Logger
class Settings(BaseSettings):
- """应用程序配置"""
# 数据库配置
MYSQL_HOST: str
MYSQL_PORT: int
@@ -69,6 +68,10 @@ class Settings(BaseSettings):
# 调度器配置
CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时
TIMEZONE: str = "Asia/Shanghai" # 默认时区
+
+ # github
+ GITHUB_REPO_OWNER: str = "snailyp"
+ GITHUB_REPO_NAME: str = "gemini-balance"
# 日志配置
LOG_LEVEL: str = "INFO" # 默认日志级别
diff --git a/app/core/application.py b/app/core/application.py
index d465306..ee3864a 100644
--- a/app/core/application.py
+++ b/app/core/application.py
@@ -4,6 +4,7 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
from app.config.config import settings, sync_initial_settings
from app.log.logger import get_application_logger
@@ -15,9 +16,41 @@ from app.core.initialization import initialize_app
from app.database.connection import connect_to_db, disconnect_from_db
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 # 导入更新检查服务
logger = get_application_logger()
+VERSION_FILE_PATH = "VERSION" # Path relative to project root
+
+def _get_current_version(default_version: str = "0.0.0") -> str:
+ """Reads the current version from the VERSION file."""
+ try:
+ # Assuming execution from project root d:/develop/pythonProjects/gemini-balance
+ with open(VERSION_FILE_PATH, 'r', encoding='utf-8') as f:
+ version = f.read().strip()
+ if not version:
+ logger.warning(f"VERSION file ('{VERSION_FILE_PATH}') is empty. Using default version '{default_version}'.")
+ return default_version
+ return version
+ except FileNotFoundError:
+ logger.warning(f"VERSION file not found at '{VERSION_FILE_PATH}'. Using default version '{default_version}'.")
+ return default_version
+ except IOError as e:
+ logger.error(f"Error reading VERSION file ('{VERSION_FILE_PATH}'): {e}. Using default version '{default_version}'.")
+ return default_version
+
+# 初始化模板引擎,并添加全局变量
+templates = Jinja2Templates(directory="app/templates")
+
+# 定义一个函数来更新模板全局变量
+def update_template_globals(app: FastAPI, update_info: dict):
+ # Jinja2Templates 实例没有直接更新全局变量的方法
+ # 我们需要在请求上下文中传递这些变量,或者修改 Jinja 环境
+ # 更简单的方法是将其存储在 app.state 中,并在渲染时传递
+ app.state.update_info = update_info
+ logger.info(f"Update info stored in app.state: {update_info}")
+
+
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
@@ -44,11 +77,29 @@ async def lifespan(app: FastAPI):
logger.info("KeyManager initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize application: {str(e)}")
- raise
+ # 不重新抛出,允许应用继续运行,但记录错误
+ # raise # 取消注释以在初始化失败时停止应用
+
+ # 检查更新 (在核心初始化之后)
+ update_available, latest_version, error_message = await check_for_updates()
+ update_info = {
+ "update_available": update_available,
+ "latest_version": latest_version,
+ "error_message": error_message,
+ "current_version": _get_current_version() # Read from VERSION file
+ }
+ # 将更新信息存储在 app.state 中
+ app.state.update_info = update_info
+ logger.info(f"Update check completed. Info: {update_info}")
+
+
+ # 启动调度器 (如果初始化成功)
+ try:
+ start_scheduler()
+ logger.info("Scheduler started successfully.")
+ except Exception as e:
+ logger.error(f"Failed to start scheduler: {e}")
- # 启动调度器
- start_scheduler()
- logger.info("Scheduler started successfully.")
yield # 应用程序运行期间
@@ -79,7 +130,15 @@ def create_app() -> FastAPI:
version="1.0.0",
lifespan=lifespan
)
-
+
+ # 初始化 app.state (如果尚未存在)
+ if not hasattr(app, "state"):
+ from starlette.datastructures import State
+ app.state = State()
+ # 确保 update_info 即使在 lifespan 之前访问也不会出错
+ app.state.update_info = {"update_available": False, "latest_version": None, "error_message": "Checking...", "current_version": _get_current_version()} # Read from VERSION file for initial state
+
+
# 配置静态文件
app.mount("/static", StaticFiles(directory="app/static"), name="static")
diff --git a/app/log/logger.py b/app/log/logger.py
index 1ccdbb1..13fa35c 100644
--- a/app/log/logger.py
+++ b/app/log/logger.py
@@ -199,4 +199,8 @@ def get_log_routes_logger():
def get_stats_logger():
- return Logger.setup_logger("stats")
\ No newline at end of file
+ return Logger.setup_logger("stats")
+
+
+def get_update_logger():
+ return Logger.setup_logger("update_service")
\ No newline at end of file
diff --git a/app/service/update/update_service.py b/app/service/update/update_service.py
new file mode 100644
index 0000000..1181702
--- /dev/null
+++ b/app/service/update/update_service.py
@@ -0,0 +1,108 @@
+import httpx
+from packaging import version
+from typing import Optional, Tuple
+
+from app.config.config import settings
+from app.log.logger import get_update_logger
+
+logger = get_update_logger()
+
+# GitHub repository details are read from settings (defined in app/config/config.py or environment variables)
+
+# GITHUB_API_URL will be constructed inside the function to ensure settings are loaded
+
+VERSION_FILE_PATH = "VERSION" # Path relative to project root
+
+async def check_for_updates() -> Tuple[bool, Optional[str], Optional[str]]:
+ """
+ 通过比较当前版本与最新的 GitHub release 来检查应用程序更新。
+
+ Returns:
+ Tuple[bool, Optional[str], Optional[str]]: 一个元组,包含:
+ - bool: 如果有可用更新则为 True,否则为 False。
+ - Optional[str]: 如果有可用更新,则为最新的版本字符串,否则为 None。
+ - Optional[str]: 如果检查失败,则为错误消息,否则为 None。
+ """
+ try:
+ # Read current version from VERSION file
+ # Ensure the path is correct relative to the execution context or use absolute path if needed
+ # Assuming execution from project root d:/develop/pythonProjects/gemini-balance
+ with open(VERSION_FILE_PATH, 'r', encoding='utf-8') as f:
+ current_v = f.read().strip()
+ if not current_v:
+ logger.error(f"VERSION file ('{VERSION_FILE_PATH}') is empty.")
+ return False, None, f"VERSION file ('{VERSION_FILE_PATH}') is empty."
+ except FileNotFoundError:
+ logger.error(f"VERSION file not found at '{VERSION_FILE_PATH}'. Make sure it exists in the project root.")
+ return False, None, f"VERSION file not found at '{VERSION_FILE_PATH}'."
+ except IOError as e:
+ logger.error(f"Error reading VERSION file ('{VERSION_FILE_PATH}'): {e}")
+ return False, None, f"Error reading VERSION file ('{VERSION_FILE_PATH}')."
+
+ logger.info(f"当前应用程序版本 (from {VERSION_FILE_PATH}): {current_v}")
+
+ # Check if repository details are configured in settings
+ if not settings.GITHUB_REPO_OWNER or not settings.GITHUB_REPO_NAME or \
+ settings.GITHUB_REPO_OWNER == "your_owner" or settings.GITHUB_REPO_NAME == "your_repo":
+ logger.warning("GitHub repository owner/name not configured in settings. Skipping update check.")
+ return False, None, "Update check skipped: Repository not configured in settings."
+
+ # Construct the API URL inside the function to ensure settings are loaded
+ github_api_url = f"https://api.github.com/repos/{settings.GITHUB_REPO_OWNER}/{settings.GITHUB_REPO_NAME}/releases/latest"
+ logger.debug(f"Checking for updates at URL: {github_api_url}") # Log the URL for debugging
+
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ # 添加 User-Agent 头,GitHub API 可能需要
+ headers = {
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": f"{settings.GITHUB_REPO_NAME}-UpdateChecker/1.0" # Use repo name from settings for User-Agent
+ }
+ response = await client.get(github_api_url, headers=headers) # Use the locally constructed URL
+ response.raise_for_status() # 对错误的 HTTP 状态码(4xx 或 5xx)抛出异常
+
+ latest_release = response.json()
+ latest_v_str = latest_release.get("tag_name")
+
+ if not latest_v_str:
+ logger.warning("在最新的 GitHub release 响应中找不到 'tag_name'。")
+ return False, None, "无法从 GitHub 解析最新版本。"
+
+ # 移除 tag 名称中可能存在的 'v' 前缀
+ if latest_v_str.startswith('v'):
+ latest_v_str = latest_v_str[1:]
+
+ logger.info(f"在 GitHub 上找到的最新版本: {latest_v_str}")
+
+ # 比较版本
+ current_version = version.parse(current_v)
+ latest_version = version.parse(latest_v_str)
+
+ if latest_version > current_version:
+ logger.info(f"有可用更新: {current_v} -> {latest_v_str}")
+ return True, latest_v_str, None
+ else:
+ logger.info("应用程序已是最新版本。")
+ return False, None, None
+
+ except httpx.HTTPStatusError as e:
+ logger.error(f"检查更新时发生 HTTP 错误: {e.response.status_code} - {e.response.text}")
+ # 避免向用户显示详细的错误文本
+ error_msg = f"获取更新信息失败 (HTTP {e.response.status_code})。"
+ if e.response.status_code == 404:
+ error_msg += " 请检查仓库名称是否正确或仓库是否有发布版本。"
+ elif e.response.status_code == 403:
+ error_msg += " API 速率限制或权限问题。"
+ return False, None, error_msg
+ except httpx.RequestError as e:
+ logger.error(f"检查更新时发生网络错误: {e}")
+ return False, None, "更新检查期间发生网络错误。"
+ except version.InvalidVersion:
+ # Note: latest_v_str might not be defined if the error occurs before fetching it.
+ # Consider adding a check or default value for logging.
+ latest_v_str_for_log = latest_v_str if 'latest_v_str' in locals() else 'N/A'
+ logger.error(f"发现无效的版本格式。当前 (from {VERSION_FILE_PATH}): '{current_v}', 最新: '{latest_v_str_for_log}'")
+ return False, None, "遇到无效的版本格式。"
+ except Exception as e:
+ logger.error(f"更新检查期间发生意外错误: {e}", exc_info=True)
+ return False, None, "发生意外错误。"
\ No newline at end of file
diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js
index 91bcd7c..aa186e4 100644
--- a/app/static/js/config_editor.js
+++ b/app/static/js/config_editor.js
@@ -702,21 +702,30 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) {
valueInput.value = isNaN(intValue) ? 0 : intValue;
valueInput.placeholder = '预算 (整数)';
valueInput.className = 'map-value-input w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50';
+ valueInput.min = 0; // 添加最小值
+ valueInput.max = 24576; // 添加最大值
valueInput.addEventListener('input', function() {
- this.value = this.value.replace(/[^0-9]/g, ''); // Remove non-digit characters
+ // 限制输入为0到24576之间的整数
+ let value = this.value.replace(/[^0-9]/g, '');
+ if (value !== '') {
+ value = parseInt(value, 10);
+ if (value < 0) value = 0;
+ if (value > 24576) value = 24576;
+ }
+ this.value = value;
});
- // Remove Button - Disabled
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none';
- removeBtn.innerHTML = '';
- removeBtn.title = '请从上方模型列表删除';
- removeBtn.disabled = true;
+ // Remove Button - Removed for budget map items
+ // const removeBtn = document.createElement('button');
+ // removeBtn.type = 'button';
+ // removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; // Kept original class for reference
+ // removeBtn.innerHTML = '';
+ // removeBtn.title = '请从上方模型列表删除';
+ // removeBtn.disabled = true;
mapItem.appendChild(keyInput);
mapItem.appendChild(valueInput);
- mapItem.appendChild(removeBtn);
+ // mapItem.appendChild(removeBtn); // Do not append the remove button
container.appendChild(mapItem);
}
diff --git a/app/templates/base.html b/app/templates/base.html
index 715693b..5e849d3 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -196,6 +196,20 @@
GitHub
+ {% if request and request.app.state.update_info %}
+ {% set update_info = request.app.state.update_info %}
+ |
+ v{{ update_info.current_version }}
+ {% if update_info.update_available %}
+ |
+
+ 新版本: v{{ update_info.latest_version }}
+
+ {% elif update_info.error_message and update_info.error_message != 'Checking...' %}
+ |
+ 更新检查失败
+ {% endif %}
+ {% endif %}
diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html
index e34e9cd..4758a0b 100644
--- a/app/templates/config_editor.html
+++ b/app/templates/config_editor.html
@@ -293,7 +293,7 @@
添加预算映射
-->
- 为每个思考模型设置预算(整数),此项与上方模型列表自动关联。
+ 为每个思考模型设置预算(整数,最大值 24576),此项与上方模型列表自动关联。
diff --git a/requirements.txt b/requirements.txt
index 9adc719..2efdaee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,3 +17,5 @@ aiomysql
databases
python-dotenv
apscheduler # 添加定时任务库
+
+packaging