Files
gemini-balance/app/service/key/key_manager.py
snaily 920228d3aa feat: 实现API密钥的单独和批量删除功能
本次更新引入了删除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密钥。
2025-05-08 21:58:26 +08:00

309 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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."
)