mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-25 02:09:32 +08:00
本次更新引入了删除API密钥的功能,包括前端界面和后端逻辑。
主要变更:
- **API路由 (`app/router/config_routes.py`):**
- 添加了新的API端点 `/keys/{key_to_delete}` 用于删除单个密钥。
- 添加了新的API端点 `/keys/delete-selected` 用于批量删除选定的密钥。
- 增加了对请求体 `DeleteKeysRequest` 的Pydantic模型定义。
- 在删除操作前进行身份验证。
- **配置服务 (`app/service/config/config_service.py`):**
- 实现了 `delete_key` 方法来处理单个密钥的删除逻辑。
- 实现了 `delete_selected_keys` 方法来处理批量密钥的删除逻辑。
- 确保在删除操作后更新配置。
- **密钥管理器 (`app/service/key/key_manager.py`):**
- 更新了 `remove_key` 方法,以确保从活动密钥列表中正确移除密钥。
- 改进了 `reset_instance` 方法,在重置时保留下一个密钥提示(`_preserved_next_key_in_cycle`),以防止在配置重载后立即丢失轮换状态。
- **前端JavaScript (`app/static/js/keys_status.js`):**
- 添加了 `showSingleKeyDeleteConfirmModal` 函数,用于显示单个密钥删除的确认模态框。
- 添加了 `executeSingleKeyDelete` 函数,用于执行单个密钥的删除请求。
- 添加了 `showDeleteConfirmationModal` 函数,用于显示批量删除密钥的确认模态框。
- 添加了 `executeDeleteSelectedKeys` 函数,用于执行批量删除密钥的请求。
- 更新了UI交互,包括按钮状态(加载中、禁用)和结果通知。
- **HTML模板 (`app/templates/keys_status.html`):**
- 为有效密钥和无效密钥列表中的每个密钥添加了“删除”按钮。
- 为有效密钥和无效密钥列表添加了“批量删除”按钮。
- 添加了用于单个密钥删除和批量删除的确认模态框HTML结构。
- 调整了现有模态框的样式,以提高视觉一致性。
这些更改增强了密钥管理功能,允许用户更灵活地管理其API密钥。
309 lines
14 KiB
Python
309 lines
14 KiB
Python
import asyncio
|
||
from itertools import cycle
|
||
from typing import Dict
|
||
|
||
from app.config.config import settings
|
||
from app.log.logger import get_key_manager_logger
|
||
|
||
logger = get_key_manager_logger()
|
||
|
||
|
||
class KeyManager:
|
||
def __init__(self, api_keys: list):
|
||
self.api_keys = api_keys
|
||
self.key_cycle = cycle(api_keys)
|
||
self.key_cycle_lock = asyncio.Lock()
|
||
self.failure_count_lock = asyncio.Lock()
|
||
self.key_failure_counts: Dict[str, int] = {key: 0 for key in api_keys}
|
||
self.MAX_FAILURES = settings.MAX_FAILURES
|
||
self.paid_key = settings.PAID_KEY
|
||
|
||
async def get_paid_key(self) -> str:
|
||
return self.paid_key
|
||
|
||
async def get_next_key(self) -> str:
|
||
"""获取下一个API key"""
|
||
async with self.key_cycle_lock:
|
||
return next(self.key_cycle)
|
||
|
||
async def is_key_valid(self, key: str) -> bool:
|
||
"""检查key是否有效"""
|
||
async with self.failure_count_lock:
|
||
return self.key_failure_counts[key] < self.MAX_FAILURES
|
||
|
||
async def reset_failure_counts(self):
|
||
"""重置所有key的失败计数"""
|
||
async with self.failure_count_lock:
|
||
for key in self.key_failure_counts:
|
||
self.key_failure_counts[key] = 0
|
||
|
||
async def reset_key_failure_count(self, key: str) -> bool:
|
||
"""重置指定key的失败计数"""
|
||
async with self.failure_count_lock:
|
||
if key in self.key_failure_counts:
|
||
self.key_failure_counts[key] = 0
|
||
logger.info(f"Reset failure count for key: {key}")
|
||
return True
|
||
logger.warning(
|
||
f"Attempt to reset failure count for non-existent key: {key}"
|
||
)
|
||
return False
|
||
|
||
async def get_next_working_key(self) -> str:
|
||
"""获取下一可用的API key"""
|
||
initial_key = await self.get_next_key()
|
||
current_key = initial_key
|
||
|
||
while True:
|
||
if await self.is_key_valid(current_key):
|
||
return current_key
|
||
|
||
current_key = await self.get_next_key()
|
||
if current_key == initial_key:
|
||
# await self.reset_failure_counts() 取消重置
|
||
return current_key
|
||
|
||
async def handle_api_failure(self, api_key: str, retries: int) -> str:
|
||
"""处理API调用失败"""
|
||
async with self.failure_count_lock:
|
||
self.key_failure_counts[api_key] += 1
|
||
if self.key_failure_counts[api_key] >= self.MAX_FAILURES:
|
||
logger.warning(
|
||
f"API key {api_key} has failed {self.MAX_FAILURES} times"
|
||
)
|
||
if retries < settings.MAX_RETRIES:
|
||
return await self.get_next_working_key()
|
||
else:
|
||
return ""
|
||
|
||
def get_fail_count(self, key: str) -> int:
|
||
"""获取指定密钥的失败次数"""
|
||
return self.key_failure_counts.get(key, 0)
|
||
|
||
async def get_keys_by_status(self) -> dict:
|
||
"""获取分类后的API key列表,包括失败次数"""
|
||
valid_keys = {}
|
||
invalid_keys = {}
|
||
|
||
async with self.failure_count_lock:
|
||
for key in self.api_keys:
|
||
fail_count = self.key_failure_counts[key]
|
||
if fail_count < self.MAX_FAILURES:
|
||
valid_keys[key] = fail_count
|
||
else:
|
||
invalid_keys[key] = fail_count
|
||
|
||
return {"valid_keys": valid_keys, "invalid_keys": invalid_keys}
|
||
|
||
async def get_first_valid_key(self) -> str:
|
||
"""获取第一个有效的API key"""
|
||
async with self.failure_count_lock:
|
||
for key in self.key_failure_counts:
|
||
if self.key_failure_counts[key] < self.MAX_FAILURES:
|
||
return key
|
||
# 如果所有 key 都无效,或者列表为空,则尝试返回第一个(如果列表不为空)
|
||
# 或者根据具体逻辑处理,这里保持原样,可能在空列表或全无效时需要调整
|
||
if self.api_keys:
|
||
return self.api_keys[0]
|
||
# 如果 api_keys 为空,这里会出问题。实际应用中应有非空保证或更好处理。
|
||
# 为了保持接口一致性,如果列表为空,可能应该抛出异常或返回特定值。
|
||
# 暂且假设 api_keys 不会为空,或者调用者处理后续的空 key 问题。
|
||
# 根据现有代码,如果api_keys为空,self.api_keys[0]会报错。
|
||
# 如果没有有效key且列表不空,返回第一个。若列表为空,这里会出IndexError。
|
||
# 更安全的做法是:
|
||
if not self.api_keys:
|
||
logger.warning("API key list is empty, cannot get first valid key.")
|
||
# Depending on desired behavior, either raise error or return an indicator like "" or None
|
||
# For now, let's allow it to potentially fail if a key is expected by caller
|
||
# but it's better to be explicit. Let's return empty string for consistency with handle_api_failure
|
||
return ""
|
||
return self.api_keys[
|
||
0
|
||
] # Fallback to the first key if no key is "valid" but list is not empty
|
||
|
||
|
||
_singleton_instance = None
|
||
_singleton_lock = asyncio.Lock()
|
||
_preserved_failure_counts: Dict[str, int] | None = None
|
||
_preserved_old_api_keys_for_reset: list | None = None
|
||
_preserved_next_key_in_cycle: str | None = None
|
||
|
||
|
||
async def get_key_manager_instance(api_keys: list = None) -> KeyManager:
|
||
"""
|
||
获取 KeyManager 单例实例。
|
||
|
||
如果尚未创建实例,将使用提供的 api_keys 初始化 KeyManager。
|
||
如果已创建实例,则忽略 api_keys 参数,返回现有单例。
|
||
如果在重置后调用,会尝试恢复之前的状态(失败计数、循环位置)。
|
||
"""
|
||
global _singleton_instance, _preserved_failure_counts, _preserved_old_api_keys_for_reset, _preserved_next_key_in_cycle
|
||
|
||
async with _singleton_lock:
|
||
if _singleton_instance is None:
|
||
if api_keys is None:
|
||
# This case needs careful handling. If it's the very first call, api_keys are required.
|
||
# If it's after a reset and no api_keys are provided, what should happen?
|
||
# The original ValueError was "API keys are required to initialize the KeyManager".
|
||
# Let's assume if api_keys is None here, it's an error unless we are restoring from non-None _preserved_old_api_keys_for_reset.
|
||
# However, the user's request implies new api_keys will be part of the reset flow.
|
||
# For now, stick to a strict requirement for api_keys if _singleton_instance is None.
|
||
raise ValueError(
|
||
"API keys are required to initialize or re-initialize the KeyManager instance."
|
||
)
|
||
if not api_keys: # Handle case where api_keys is an empty list
|
||
logger.warning(
|
||
"Initializing KeyManager with an empty list of API keys."
|
||
)
|
||
# Consider if this should be an error or allowed. Current KeyManager supports it.
|
||
|
||
_singleton_instance = KeyManager(api_keys)
|
||
logger.info(
|
||
f"KeyManager instance created/re-created with {len(api_keys)} API keys."
|
||
)
|
||
|
||
# 1. 恢复失败计数
|
||
if _preserved_failure_counts:
|
||
# Initialize new instance's failure_counts for all new keys to 0
|
||
current_failure_counts = {
|
||
key: 0 for key in _singleton_instance.api_keys
|
||
}
|
||
# Inherit counts for keys that exist in both old and new lists
|
||
for key, count in _preserved_failure_counts.items():
|
||
if key in current_failure_counts:
|
||
current_failure_counts[key] = count
|
||
_singleton_instance.key_failure_counts = current_failure_counts
|
||
logger.info("Inherited failure counts for applicable keys.")
|
||
_preserved_failure_counts = None # Clear after use
|
||
|
||
# 2. 调整 key_cycle 的起始点
|
||
start_key_for_new_cycle = None
|
||
if (
|
||
_preserved_old_api_keys_for_reset
|
||
and _preserved_next_key_in_cycle
|
||
and _singleton_instance.api_keys # Ensure new api_keys list is not empty
|
||
):
|
||
try:
|
||
# Find the index of the preserved next key in the *old* list
|
||
start_idx_in_old = _preserved_old_api_keys_for_reset.index(
|
||
_preserved_next_key_in_cycle
|
||
)
|
||
|
||
# Iterate through the old key list (circularly) starting from _preserved_next_key_in_cycle
|
||
# Find the first key that also exists in the new api_keys list
|
||
for i in range(len(_preserved_old_api_keys_for_reset)):
|
||
current_old_key_idx = (start_idx_in_old + i) % len(
|
||
_preserved_old_api_keys_for_reset
|
||
)
|
||
key_candidate = _preserved_old_api_keys_for_reset[
|
||
current_old_key_idx
|
||
]
|
||
if key_candidate in _singleton_instance.api_keys:
|
||
start_key_for_new_cycle = key_candidate
|
||
break
|
||
except ValueError:
|
||
logger.warning(
|
||
f"Preserved next key '{_preserved_next_key_in_cycle}' not found in preserved old API keys. "
|
||
"New cycle will start from the beginning of the new list."
|
||
)
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Error determining start key for new cycle from preserved state: {e}. "
|
||
"New cycle will start from the beginning."
|
||
)
|
||
|
||
if start_key_for_new_cycle and _singleton_instance.api_keys:
|
||
try:
|
||
# Find the index of the determined start_key in the new api_keys list
|
||
target_idx = _singleton_instance.api_keys.index(
|
||
start_key_for_new_cycle
|
||
)
|
||
# Advance the new cycle by calling next() target_idx times
|
||
# This positions the cycle so that the *next* call to next() will yield start_key_for_new_cycle
|
||
for _ in range(target_idx):
|
||
next(_singleton_instance.key_cycle)
|
||
logger.info(
|
||
f"Key cycle in new instance advanced. Next call to get_next_key() will yield: {start_key_for_new_cycle}"
|
||
)
|
||
except ValueError:
|
||
# This should not happen if start_key_for_new_cycle was correctly found in api_keys
|
||
logger.warning(
|
||
f"Determined start key '{start_key_for_new_cycle}' not found in new API keys during cycle advancement. "
|
||
"New cycle will start from the beginning."
|
||
)
|
||
except (
|
||
StopIteration
|
||
): # Should not happen with cycle unless api_keys is empty, handled by _singleton_instance.api_keys check
|
||
logger.error(
|
||
"StopIteration while advancing key cycle, implies empty new API key list previously missed."
|
||
)
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Error advancing new key cycle: {e}. Cycle will start from beginning."
|
||
)
|
||
else:
|
||
if _singleton_instance.api_keys:
|
||
logger.info(
|
||
"New key cycle will start from the beginning of the new API key list (no specific start key determined or needed)."
|
||
)
|
||
else:
|
||
logger.info(
|
||
"New key cycle not applicable as the new API key list is empty."
|
||
)
|
||
|
||
# 清理所有保存的状态
|
||
_preserved_old_api_keys_for_reset = None
|
||
_preserved_next_key_in_cycle = None
|
||
# _preserved_failure_counts already cleared
|
||
|
||
return _singleton_instance
|
||
|
||
|
||
async def reset_key_manager_instance():
|
||
"""
|
||
重置 KeyManager 单例实例。
|
||
将保存当前实例的状态(失败计数、旧 API keys、下一个 key 提示)
|
||
以供下一次 get_key_manager_instance 调用时恢复。
|
||
"""
|
||
global _singleton_instance, _preserved_failure_counts, _preserved_old_api_keys_for_reset, _preserved_next_key_in_cycle
|
||
async with _singleton_lock:
|
||
if _singleton_instance:
|
||
# 1. 保存失败计数
|
||
_preserved_failure_counts = _singleton_instance.key_failure_counts.copy()
|
||
|
||
# 2. 保存旧的 API keys 列表
|
||
_preserved_old_api_keys_for_reset = _singleton_instance.api_keys.copy()
|
||
|
||
# 3. 保存 key_cycle 的下一个 key 提示
|
||
# This should be the key that get_next_key() would return next.
|
||
try:
|
||
if (
|
||
_singleton_instance.api_keys
|
||
): # Only if there are keys to cycle through
|
||
# Calling get_next_key() consumes one key and returns it. This is the key
|
||
# we want the new cycle to effectively start with.
|
||
_preserved_next_key_in_cycle = (
|
||
await _singleton_instance.get_next_key()
|
||
)
|
||
else:
|
||
_preserved_next_key_in_cycle = None # No keys, so no next key
|
||
except (
|
||
StopIteration
|
||
): # Should be caught by "if _singleton_instance.api_keys"
|
||
logger.warning(
|
||
"Could not preserve next key hint: key cycle was empty or exhausted in old instance."
|
||
)
|
||
_preserved_next_key_in_cycle = None
|
||
except Exception as e:
|
||
logger.error(f"Error preserving next key hint during reset: {e}")
|
||
_preserved_next_key_in_cycle = None
|
||
|
||
_singleton_instance = None
|
||
logger.info(
|
||
"KeyManager instance has been reset. State (failure counts, old keys, next key hint) preserved for next instantiation."
|
||
)
|
||
else:
|
||
logger.info(
|
||
"KeyManager instance was not set (or already reset), no reset action performed."
|
||
)
|