z / app /utils /token_pool.py
bg5's picture
Upload 85 files
712a3c7 verified
Raw History Blame Contribute Delete
36.6 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Token 池管理器 - 基于数据库的 Token 轮询和健康检查
核心功能:
1. Token 轮询机制 - 负载均衡和容错
2. Z.AI 官方认证接口验证 - 基于 role 字段区分用户类型
3. Token 健康度监控 - 自动禁用失败 Token
4. 数据库集成 - 与 TokenDAO 协同工作
"""
import asyncio
import time
from dataclasses import dataclass
from asyncio import Lock
from typing import Dict, List, Optional, Set, Tuple
import httpx
from app.core.resin_compat import apply_resin_account_header
from app.core.upstream_urls import build_upstream_url
from app.core.httpx_errors import normalize_httpx_exception
from app.utils.logger import logger, log_exception
from app.utils.utlis import mask_token
# ==================== Token 状态管理 ====================
TOKEN_LOAD_BALANCE_ROUND_ROBIN = "round_robin"
TOKEN_LOAD_BALANCE_LEAST_LOADED = "least_loaded"
TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED = "smooth_weighted_round_robin"
SUPPORTED_TOKEN_LOAD_BALANCE_STRATEGIES = {
TOKEN_LOAD_BALANCE_ROUND_ROBIN,
TOKEN_LOAD_BALANCE_LEAST_LOADED,
TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED,
}
def normalize_token_load_balance_strategy(strategy: Optional[str]) -> str:
normalized = str(strategy or "").strip().lower()
if normalized in SUPPORTED_TOKEN_LOAD_BALANCE_STRATEGIES:
return normalized
return TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED
@dataclass
class TokenStatus:
"""Token 运行时状态(内存中)"""
token: str
token_id: int # 数据库 ID,用于同步统计
token_type: str = "unknown" # "user", "guest", "unknown"
priority: int = 0
is_available: bool = True
failure_count: int = 0
cooldown_until: float = 0.0
last_failure_time: float = 0.0
last_success_time: float = 0.0
last_selected_time: float = 0.0
active_requests: int = 0
current_weight: int = 0
total_requests: int = 0
successful_requests: int = 0
db_synced_successful_requests: int = 0
db_synced_failed_requests: int = 0
@property
def success_rate(self) -> float:
"""成功率"""
if self.total_requests == 0:
return 1.0
return self.successful_requests / self.total_requests
@property
def failed_requests(self) -> int:
"""失败次数。"""
return max(0, self.total_requests - self.successful_requests)
@property
def is_healthy(self) -> bool:
"""
Token 健康状态判断
健康标准:
1. 必须是认证用户 Token (token_type = "user")
2. 当前可用 (is_available = True)
3. 成功率 >= 50% 或总请求数 <= 3(新 Token 容错)
注意:
- guest Token 永远不健康
- unknown Token 永远不健康
"""
# guest 和 unknown token 永远不健康
if self.token_type != "user":
return False
# 不可用的 token 不健康
if not self.is_available:
return False
# 新 token 容错:请求数很少时,只要没触发连续错误熔断就健康
if self.total_requests <= 3:
return self.failure_count == 0
# 基于成功率判断
return self.success_rate >= 0.5
# ==================== Token 验证服务 ====================
class ZAITokenValidator:
"""Z.AI Token 验证器(使用官方认证接口)"""
@staticmethod
def _response_size_bytes(response: httpx.Response) -> int:
content_length = response.headers.get("content-length")
if content_length and content_length.isdigit():
return int(content_length)
return len(response.content or b"")
@staticmethod
def auth_url() -> str:
return build_upstream_url("/api/v1/auths/")
@staticmethod
def get_headers(token: str) -> Dict[str, str]:
"""构建认证请求头"""
headers = {
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Authorization": f"Bearer {token}",
"Connection": "keep-alive",
"Content-Type": "application/json",
"DNT": "1",
"Referer": "https://chat.z.ai/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"'
}
apply_resin_account_header(headers, token)
return headers
@classmethod
async def validate_token(cls, token: str) -> Tuple[str, bool, Optional[str]]:
"""
验证 Token 有效性并返回类型
Args:
token: 待验证的 Token
Returns:
(token_type, is_valid, error_message)
- token_type: "user" | "guest" | "unknown"
- is_valid: True 表示是有效的认证用户 Token
- error_message: 失败原因(仅在 is_valid=False 时有值)
"""
started_at = time.perf_counter()
masked_token = mask_token(token)
try:
async with httpx.AsyncClient(timeout=15.0, trust_env=False) as client:
auth_url = cls.auth_url()
response = await client.get(
auth_url,
headers=cls.get_headers(token)
)
elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.debug(
"[token.validate] token=%s status=%s bytes=%s elapsed_ms=%.1f",
masked_token,
response.status_code,
cls._response_size_bytes(response),
elapsed_ms,
)
# 解析响应
return cls._parse_auth_response(response)
except httpx.TimeoutException as exc:
return (
"unknown",
False,
normalize_httpx_exception(
exc,
method="GET",
url=cls.auth_url(),
context="token.validate",
),
)
except httpx.ConnectError as exc:
return (
"unknown",
False,
normalize_httpx_exception(
exc,
method="GET",
url=cls.auth_url(),
context="token.validate",
),
)
except Exception as e:
return (
"unknown",
False,
normalize_httpx_exception(
e,
method="GET",
url=cls.auth_url(),
context="token.validate",
),
)
@staticmethod
def _parse_auth_response(response: httpx.Response) -> Tuple[str, bool, Optional[str]]:
"""
解析 Z.AI 认证接口响应
响应格式示例:
{
"id": "...",
"email": "user@example.com",
"role": "user" # 或 "guest"
}
验证规则:
- role: "user" → 认证用户 Token(有效,可添加)
- role: "guest" → 匿名用户 Token(无效,拒绝添加)
- 其他情况 → 无效 Token
"""
# 检查 HTTP 状态码
if response.status_code != 200:
return ("unknown", False, f"HTTP {response.status_code}")
try:
data = response.json()
# 验证响应格式
if not isinstance(data, dict):
return ("unknown", False, "无效的响应格式")
# 检查是否包含错误信息
if "error" in data or "message" in data:
error_msg = data.get("error") or data.get("message", "未知错误")
return ("unknown", False, str(error_msg))
# 核心验证:检查 role 字段
role = data.get("role")
if role == "user":
return ("user", True, None)
elif role == "guest":
return ("guest", False, "匿名用户 Token 不允许添加")
else:
return ("unknown", False, f"未知 role: {role}")
except (ValueError, Exception) as e:
return ("unknown", False, f"解析响应失败: {str(e)}")
# ==================== Token 池管理器 ====================
class TokenPool:
"""Token 池管理器(数据库驱动)"""
def __init__(
self,
tokens: List[Tuple[int, str, str]], # [(token_id, token_value, token_type[, priority]), ...]
failure_threshold: int = 3,
recovery_timeout: int = 1800,
strategy: str = TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED,
):
"""
初始化 Token 池
Args:
tokens: Token 列表 [(token_id, token_value, token_type), ...]
failure_threshold: 失败阈值,超过此次数将标记为不可用
recovery_timeout: 恢复超时时间(秒),失败 Token 在此时间后重新尝试
"""
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self._lock = Lock()
self._current_index = 0
self.strategy = self._normalize_strategy(strategy)
# 初始化 Token 状态(内存中)
self.token_statuses: Dict[str, TokenStatus] = {}
self.token_id_map: Dict[str, int] = {} # token -> token_id 映射
self._user_tokens: List[str] = []
for token_record in tokens:
if len(token_record) >= 4:
token_id, token_value, token_type, priority = token_record[:4]
else:
token_id, token_value, token_type = token_record[:3]
priority = 0
if token_value and token_value not in self.token_statuses:
self.token_statuses[token_value] = TokenStatus(
token=token_value,
token_id=token_id,
token_type=token_type,
priority=int(priority or 0),
)
self.token_id_map[token_value] = token_id
if token_type == "user":
self._user_tokens.append(token_value)
if not self.token_statuses:
logger.warning("⚠️ Token 池为空,将依赖匿名模式")
@staticmethod
def _normalize_strategy(strategy: Optional[str]) -> str:
return normalize_token_load_balance_strategy(strategy)
@staticmethod
def _priority_weight(priority: int) -> int:
return max(1, int(priority or 0) + 1)
async def set_strategy(self, strategy: str) -> None:
normalized = self._normalize_strategy(strategy)
async with self._lock:
self.strategy = normalized
for status in self.token_statuses.values():
status.current_weight = 0
async def release_token_allocation(self, token: str) -> None:
"""释放一次已占用但未完成记账的 token 并发槽位。"""
async with self._lock:
status = self.token_statuses.get(token)
if status is None:
return
status.active_requests = max(0, status.active_requests - 1)
def _select_round_robin(
self,
candidates: List[Tuple[int, str, TokenStatus]],
total_tokens: int,
current_time: float,
) -> Optional[str]:
if not candidates:
return None
index, token, status = candidates[0]
status.active_requests += 1
status.last_selected_time = current_time
self._current_index = (index + 1) % total_tokens
return token
def _select_least_loaded(
self,
candidates: List[Tuple[int, str, TokenStatus]],
total_tokens: int,
current_time: float,
) -> Optional[str]:
if not candidates:
return None
index, token, status = min(
candidates,
key=lambda item: (
item[2].active_requests,
item[2].total_requests,
item[2].last_selected_time,
item[0],
),
)
status.active_requests += 1
status.last_selected_time = current_time
self._current_index = (index + 1) % total_tokens
return token
def _select_smooth_weighted(
self,
candidates: List[Tuple[int, str, TokenStatus]],
total_tokens: int,
current_time: float,
) -> Optional[str]:
if not candidates:
return None
min_active_requests = min(status.active_requests for _, _, status in candidates)
shortlist = [
(index, token, status)
for index, token, status in candidates
if status.active_requests == min_active_requests
]
total_weight = 0
selected: Optional[Tuple[int, str, TokenStatus]] = None
for candidate in shortlist:
index, token, status = candidate
weight = self._priority_weight(status.priority)
status.current_weight += weight
total_weight += weight
if selected is None:
selected = candidate
continue
selected_index, selected_token, selected_status = selected
if (
status.current_weight > selected_status.current_weight
or (
status.current_weight == selected_status.current_weight
and (
status.total_requests < selected_status.total_requests
or (
status.total_requests == selected_status.total_requests
and (
status.last_selected_time < selected_status.last_selected_time
or (
status.last_selected_time == selected_status.last_selected_time
and index < selected_index
)
)
)
)
)
):
selected = candidate
if selected is None:
return None
index, token, status = selected
status.current_weight -= total_weight
status.active_requests += 1
status.last_selected_time = current_time
self._current_index = (index + 1) % total_tokens
return token
async def get_next_token(self, exclude_tokens: Optional[Set[str]] = None) -> Optional[str]:
"""
获取下一个可用的认证用户 Token(可配置调度算法)
Returns:
可用的 Token 字符串,如果没有可用 Token 则返回 None
"""
async with self._lock:
if not self._user_tokens:
return None
excluded = exclude_tokens or set()
current_time = time.time()
total_tokens = len(self._user_tokens)
start_index = self._current_index % total_tokens if total_tokens else 0
candidate_tokens: List[Tuple[int, str, TokenStatus]] = []
for offset in range(total_tokens):
index = (start_index + offset) % total_tokens
token = self._user_tokens[index]
if token in excluded:
continue
status = self.token_statuses.get(token)
if status is None:
continue
if self._is_token_available(status, current_time):
candidate_tokens.append((index, token, status))
if candidate_tokens:
if self.strategy == TOKEN_LOAD_BALANCE_ROUND_ROBIN:
return self._select_round_robin(
candidate_tokens,
total_tokens,
current_time,
)
if self.strategy == TOKEN_LOAD_BALANCE_LEAST_LOADED:
return self._select_least_loaded(
candidate_tokens,
total_tokens,
current_time,
)
return self._select_smooth_weighted(
candidate_tokens,
total_tokens,
current_time,
)
logger.warning("⚠️ 没有可用的认证用户 Token")
return None
def _is_token_available(self, status: TokenStatus, current_time: float) -> bool:
"""检查 token 当前是否可用于轮询,必要时自动从熔断状态恢复。"""
if status.token_type != "user":
return False
if status.is_available:
return True
if status.cooldown_until and current_time >= status.cooldown_until:
status.is_available = True
status.failure_count = 0
status.cooldown_until = 0.0
logger.info("✅ Token 熔断冷却结束,恢复可用: %s...", status.token[:20])
return True
return False
async def mark_token_success(self, token: str):
"""标记 Token 使用成功"""
async with self._lock:
if token in self.token_statuses:
status = self.token_statuses[token]
status.total_requests += 1
status.successful_requests += 1
status.active_requests = max(0, status.active_requests - 1)
status.last_success_time = time.time()
status.failure_count = 0
status.cooldown_until = 0.0
status.is_available = True
async def mark_token_failure(self, token: str, error: Exception = None):
"""标记 Token 使用失败"""
async with self._lock:
if token in self.token_statuses:
status = self.token_statuses[token]
current_time = time.time()
status.total_requests += 1
status.active_requests = max(0, status.active_requests - 1)
status.failure_count += 1
status.last_failure_time = current_time
if status.failure_count >= self.failure_threshold:
status.is_available = False
status.cooldown_until = current_time + self.recovery_timeout
logger.warning(
"Token 已熔断: %s... (连续失败 %s 次, 冷却 %ss)",
token[:20],
status.failure_count,
self.recovery_timeout,
)
async def record_token_success(self, token: str, dao=None):
"""标记成功,并默认异步批量同步数据库统计。"""
await self.mark_token_success(token)
token_id = self.get_token_id(token)
if token_id is None:
return
if dao is None:
from app.services.token_dao import get_token_dao
from app.services.token_stats_writer import get_token_stats_writer
writer = get_token_stats_writer()
enqueued = writer.enqueue(
token=token,
token_id=token_id,
successful_requests=1,
)
if enqueued:
return
dao = get_token_dao()
try:
await dao.record_success(token_id)
except Exception as e:
log_exception(logger, "❌ 同步 Token 成功统计失败")
return
await self.mark_stats_synced(token=token, successful_requests=1)
async def record_token_failure(self, token: str, error: Exception = None, dao=None):
"""标记失败,并默认异步批量同步数据库统计。"""
await self.mark_token_failure(token, error)
token_id = self.get_token_id(token)
if token_id is None:
return
if dao is None:
from app.services.token_dao import get_token_dao
from app.services.token_stats_writer import get_token_stats_writer
writer = get_token_stats_writer()
enqueued = writer.enqueue(
token=token,
token_id=token_id,
failed_requests=1,
)
if enqueued:
return
dao = get_token_dao()
try:
await dao.record_failure(token_id)
except Exception as e:
log_exception(logger, "❌ 同步 Token 失败统计失败")
return
await self.mark_stats_synced(token=token, failed_requests=1)
async def mark_stats_synced(
self,
*,
token: str,
successful_requests: int = 0,
failed_requests: int = 0,
) -> None:
async with self._lock:
if token not in self.token_statuses:
return
status = self.token_statuses[token]
status.db_synced_successful_requests += max(0, successful_requests)
status.db_synced_failed_requests += max(0, failed_requests)
def get_token_id(self, token: str) -> Optional[int]:
"""获取 Token 的数据库 ID"""
return self.token_id_map.get(token)
async def get_pool_status(self) -> Dict:
"""获取 Token 池状态信息"""
async with self._lock:
current_time = time.time()
available_count = sum(
1
for token in self._user_tokens
if (status := self.token_statuses.get(token))
and self._is_token_available(status, current_time)
)
total_count = len(self.token_statuses)
healthy_count = sum(1 for status in self.token_statuses.values() if status.is_healthy)
# 统计各类型 Token
user_count = sum(1 for s in self.token_statuses.values() if s.token_type == "user")
guest_count = sum(1 for s in self.token_statuses.values() if s.token_type == "guest")
unknown_count = sum(1 for s in self.token_statuses.values() if s.token_type == "unknown")
status_info = {
"strategy": self.strategy,
"total_tokens": total_count,
"available_tokens": available_count,
"unavailable_tokens": total_count - available_count,
"healthy_tokens": healthy_count,
"unhealthy_tokens": total_count - healthy_count,
"user_tokens": user_count,
"guest_tokens": guest_count,
"unknown_tokens": unknown_count,
"current_index": self._current_index,
"tokens": []
}
for token, status in self.token_statuses.items():
status_info["tokens"].append({
"token": f"{token[:10]}...{token[-10:]}",
"token_id": status.token_id,
"token_type": status.token_type,
"is_available": status.is_available,
"failure_count": status.failure_count,
"cooldown_until": status.cooldown_until,
"priority": status.priority,
"selection_weight": self._priority_weight(status.priority),
"active_requests": status.active_requests,
"current_weight": status.current_weight,
"success_count": status.successful_requests,
"success_rate": f"{status.success_rate:.2%}",
"total_requests": status.total_requests,
"is_healthy": status.is_healthy,
"last_failure_time": status.last_failure_time,
"last_success_time": status.last_success_time,
"last_selected_time": status.last_selected_time,
})
return status_info
async def update_token_type(self, token: str, token_type: str):
"""更新 Token 类型(用于健康检查后更新)"""
async with self._lock:
if token in self.token_statuses:
old_type = self.token_statuses[token].token_type
self.token_statuses[token].token_type = token_type
if old_type != token_type:
logger.debug("更新 Token 类型: %s... %s -> %s", token[:20], old_type, token_type)
async def health_check_token(self, token: str) -> bool:
"""
异步健康检查单个 Token(使用 Z.AI 官方认证接口)
Args:
token: 要检查的 Token
Returns:
Token 是否健康(True = 有效的认证用户 Token)
"""
token_type, is_valid, error_message = await ZAITokenValidator.validate_token(token)
# 更新 Token 类型
await self.update_token_type(token, token_type)
# 更新状态
if is_valid:
await self.record_token_success(token)
else:
await self.record_token_failure(
token,
Exception(error_message or "验证失败"),
)
return is_valid
async def health_check_all(self):
"""异步健康检查所有 Token"""
if not self.token_statuses:
logger.debug("⚠️ Token 池为空,跳过健康检查")
return
total_tokens = len(self.token_statuses)
concurrency = 10
started_at = time.perf_counter()
logger.info(
"[token.health_check] start total_tokens=%s mode=concurrency concurrency=%s",
total_tokens,
concurrency,
)
# 并发执行所有 Token 的健康检查
semaphore = asyncio.Semaphore(concurrency)
async def _check_with_limit(token: str):
async with semaphore:
return await self.health_check_token(token)
tasks = [
_check_with_limit(token)
for token in self.token_statuses.keys()
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 统计结果
healthy_count = sum(1 for r in results if r is True)
failed_count = sum(1 for r in results if r is False)
exception_count = sum(1 for r in results if isinstance(r, Exception))
health_rate = (healthy_count / total_tokens) * 100 if total_tokens > 0 else 0
elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.info(
"[token.health_check] done total_tokens=%s healthy=%s failed=%s exceptions=%s mode=concurrency concurrency=%s elapsed_ms=%.1f",
total_tokens,
healthy_count,
failed_count,
exception_count,
concurrency,
elapsed_ms,
)
if healthy_count == 0 and total_tokens > 0:
logger.warning(f"⚠️ 健康检查完成: 0/{total_tokens} 个 Token 健康 - 请检查 Token 配置")
elif failed_count > 0:
logger.warning(f"⚠️ 健康检查完成: {healthy_count}/{total_tokens} 个 Token 健康 ({health_rate:.1f}%)")
else:
logger.info(f"✅ 健康检查完成: {healthy_count}/{total_tokens} 个 Token 健康")
if exception_count > 0:
logger.error(f"💥 {exception_count} 个 Token 检查异常")
async def sync_from_database(self, provider: str = "zai"):
"""
从数据库同步 Token 状态(禁用/启用状态)
Args:
provider: 提供商名称
说明:
- 从数据库读取最新的 Token 启用状态
- 如果数据库中 Token 被禁用,则从池中移除
- 如果数据库中有新增的启用 Token,则添加到池中
- 保留现有 Token 的运行时统计(请求数、成功率等)
"""
from app.services.token_dao import get_token_dao
dao = get_token_dao()
# 从数据库加载所有启用的认证用户 Token
token_records = await dao.get_tokens_by_provider(provider, enabled_only=True)
# 构建数据库中的 Token 映射
db_tokens = {
record["token"]: (
record["id"],
record.get("token_type", "unknown"),
int(record.get("priority", 0) or 0),
)
for record in token_records
if record.get("token_type") != "guest" # 过滤 guest token
}
async with self._lock:
# 1. 移除已在数据库中禁用的 Token
should_reset_weights = False
tokens_to_remove = []
for token_value in list(self.token_statuses.keys()):
if token_value not in db_tokens:
tokens_to_remove.append(token_value)
for token_value in tokens_to_remove:
del self.token_statuses[token_value]
del self.token_id_map[token_value]
if tokens_to_remove:
should_reset_weights = True
# 2. 添加新启用的 Token
new_tokens_count = 0
ordered_user_tokens: List[str] = []
for token_value, (token_id, token_type, priority) in db_tokens.items():
if token_type == "user":
ordered_user_tokens.append(token_value)
if token_value not in self.token_statuses:
self.token_statuses[token_value] = TokenStatus(
token=token_value,
token_id=token_id,
token_type=token_type,
priority=priority,
)
self.token_id_map[token_value] = token_id
new_tokens_count += 1
should_reset_weights = True
# 3. 更新现有 Token 的类型(如果数据库中有更新)
for token_value, (token_id, token_type, priority) in db_tokens.items():
if token_value in self.token_statuses:
status = self.token_statuses[token_value]
old_type = status.token_type
if old_type != token_type:
status.token_type = token_type
should_reset_weights = True
if status.priority != priority:
status.priority = priority
should_reset_weights = True
if should_reset_weights and self.strategy == TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED:
for status in self.token_statuses.values():
status.current_weight = 0
self._user_tokens = ordered_user_tokens
if self._user_tokens:
self._current_index %= len(self._user_tokens)
else:
self._current_index = 0
logger.info(
f"✅ Token 池同步完成: "
f"当前 {len(self.token_statuses)} 个 Token "
f"(移除 {len(tokens_to_remove)}, 新增 {new_tokens_count})"
)
# ==================== 全局实例管理 ====================
_token_pool: Optional[TokenPool] = None
_pool_lock = Lock()
def get_token_pool() -> Optional[TokenPool]:
"""获取全局 Token 池实例"""
return _token_pool
async def initialize_token_pool_from_db(
provider: str = "zai",
failure_threshold: int = 3,
recovery_timeout: int = 1800,
strategy: str = TOKEN_LOAD_BALANCE_SMOOTH_WEIGHTED,
) -> Optional[TokenPool]:
"""
从数据库初始化全局 Token 池
Args:
provider: 提供商名称(当前仅使用 zai)
failure_threshold: 失败阈值
recovery_timeout: 恢复超时时间(秒)
Returns:
TokenPool 实例(即使没有 Token 也会创建空池)
"""
global _token_pool
from app.services.token_dao import get_token_dao
dao = get_token_dao()
# 从数据库加载 Token(只加载启用的认证用户 Token)
token_records = await dao.get_tokens_by_provider(provider, enabled_only=True)
# 转换为 TokenPool 所需格式
tokens = []
if token_records:
tokens = [
(
record["id"],
record["token"],
record.get("token_type", "unknown"),
int(record.get("priority", 0) or 0),
)
for record in token_records
]
# 过滤掉 guest token(不应该在数据库中,但防御性检查)
user_tokens = [
token_record
for token_record in tokens
if str(token_record[2]) != "guest"
]
if len(user_tokens) < len(tokens):
guest_count = len(tokens) - len(user_tokens)
logger.debug("过滤了 %s 个匿名用户 Token", guest_count)
tokens = user_tokens
# 始终创建 Token 池实例(即使为空)
async with _pool_lock:
_token_pool = TokenPool(
tokens,
failure_threshold,
recovery_timeout,
strategy=strategy,
)
if not tokens:
logger.warning(f"⚠️ {provider} 没有有效的认证用户 Token,已创建空 Token 池")
else:
logger.debug("从数据库初始化 Token 池(%s),共 %s 个 Token", provider, len(tokens))
return _token_pool
async def sync_token_stats_to_db():
"""
将内存中的 Token 统计同步到数据库
应在服务关闭或定期调用,确保统计数据不丢失
"""
pool = get_token_pool()
if not pool:
return
from app.services.token_stats_writer import get_token_stats_writer
await get_token_stats_writer().flush_now()
from app.services.token_dao import get_token_dao
dao = get_token_dao()
pending_updates = []
async with pool._lock:
for token, status in pool.token_statuses.items():
pending_success = max(
0,
status.successful_requests - status.db_synced_successful_requests,
)
pending_failure = max(
0,
status.failed_requests - status.db_synced_failed_requests,
)
if pending_success > 0 or pending_failure > 0:
pending_updates.append(
(
token,
status.token_id,
pending_success,
pending_failure,
)
)
for token, token_id, pending_success, pending_failure in pending_updates:
for _ in range(pending_success):
await dao.record_success(token_id)
for _ in range(pending_failure):
await dao.record_failure(token_id)
async with pool._lock:
if token in pool.token_statuses:
status = pool.token_statuses[token]
status.db_synced_successful_requests += pending_success
status.db_synced_failed_requests += pending_failure
logger.info("✅ Token 统计已同步到数据库")