Spaces:
Running
Running
Upload 9 files
Browse files- core/account.py +30 -3
- core/config.py +308 -0
- core/uptime.py +78 -0
core/account.py
CHANGED
|
@@ -18,8 +18,11 @@ if TYPE_CHECKING:
|
|
| 18 |
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
| 21 |
-
# 配置文件路径
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
@dataclass
|
|
@@ -403,7 +406,19 @@ def reload_accounts(
|
|
| 403 |
session_cache_ttl_seconds: int,
|
| 404 |
global_stats: dict
|
| 405 |
) -> MultiAccountManager:
|
| 406 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
multi_account_mgr.global_session_cache.clear()
|
| 408 |
new_mgr = load_multi_account_config(
|
| 409 |
http_client,
|
|
@@ -413,6 +428,18 @@ def reload_accounts(
|
|
| 413 |
session_cache_ttl_seconds,
|
| 414 |
global_stats
|
| 415 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
logger.info(f"[CONFIG] 配置已重载,当前账户数: {len(new_mgr.accounts)}")
|
| 417 |
return new_mgr
|
| 418 |
|
|
|
|
| 18 |
|
| 19 |
logger = logging.getLogger(__name__)
|
| 20 |
|
| 21 |
+
# 配置文件路径 - 自动检测环境
|
| 22 |
+
if os.path.exists("/data"):
|
| 23 |
+
ACCOUNTS_FILE = "/data/accounts.json" # HF Pro 持久化
|
| 24 |
+
else:
|
| 25 |
+
ACCOUNTS_FILE = "data/accounts.json" # 本地存储(统一到 data 目录)
|
| 26 |
|
| 27 |
|
| 28 |
@dataclass
|
|
|
|
| 406 |
session_cache_ttl_seconds: int,
|
| 407 |
global_stats: dict
|
| 408 |
) -> MultiAccountManager:
|
| 409 |
+
"""重新加载账户配置(保留现有账户的运行时状态)"""
|
| 410 |
+
# 保存现有账户的运行时状态
|
| 411 |
+
old_states = {}
|
| 412 |
+
for account_id, account_mgr in multi_account_mgr.accounts.items():
|
| 413 |
+
old_states[account_id] = {
|
| 414 |
+
"is_available": account_mgr.is_available,
|
| 415 |
+
"last_error_time": account_mgr.last_error_time,
|
| 416 |
+
"last_429_time": account_mgr.last_429_time,
|
| 417 |
+
"error_count": account_mgr.error_count,
|
| 418 |
+
"conversation_count": account_mgr.conversation_count
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
# 清空会话缓存并重新加载配置
|
| 422 |
multi_account_mgr.global_session_cache.clear()
|
| 423 |
new_mgr = load_multi_account_config(
|
| 424 |
http_client,
|
|
|
|
| 428 |
session_cache_ttl_seconds,
|
| 429 |
global_stats
|
| 430 |
)
|
| 431 |
+
|
| 432 |
+
# 恢复现有账户的运行时状态
|
| 433 |
+
for account_id, state in old_states.items():
|
| 434 |
+
if account_id in new_mgr.accounts:
|
| 435 |
+
account_mgr = new_mgr.accounts[account_id]
|
| 436 |
+
account_mgr.is_available = state["is_available"]
|
| 437 |
+
account_mgr.last_error_time = state["last_error_time"]
|
| 438 |
+
account_mgr.last_429_time = state["last_429_time"]
|
| 439 |
+
account_mgr.error_count = state["error_count"]
|
| 440 |
+
account_mgr.conversation_count = state["conversation_count"]
|
| 441 |
+
logger.debug(f"[CONFIG] 账户 {account_id} 运行时状态已恢复")
|
| 442 |
+
|
| 443 |
logger.info(f"[CONFIG] 配置已重载,当前账户数: {len(new_mgr.accounts)}")
|
| 444 |
return new_mgr
|
| 445 |
|
core/config.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
统一配置管理系统
|
| 3 |
+
|
| 4 |
+
优先级规则:
|
| 5 |
+
1. 环境变量(最高优先级)
|
| 6 |
+
2. YAML 配置文件
|
| 7 |
+
3. 默认值(最低优先级)
|
| 8 |
+
|
| 9 |
+
配置分类:
|
| 10 |
+
- 安全配置:仅从环境变量读取,不可热更新(ADMIN_KEY, PATH_PREFIX, SESSION_SECRET_KEY)
|
| 11 |
+
- 业务配置:环境变量 > YAML,支持热更新(API_KEY, PROXY, 重试策略等)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import yaml
|
| 16 |
+
import secrets
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Optional, List
|
| 19 |
+
from pydantic import BaseModel, Field, validator
|
| 20 |
+
from dotenv import load_dotenv
|
| 21 |
+
|
| 22 |
+
# 加载 .env 文件
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ==================== 配置模型定义 ====================
|
| 27 |
+
|
| 28 |
+
class BasicConfig(BaseModel):
|
| 29 |
+
"""基础配置"""
|
| 30 |
+
api_key: str = Field(default="", description="API访问密钥(留空则公开访问)")
|
| 31 |
+
base_url: str = Field(default="", description="服务器URL(留空则自动检测)")
|
| 32 |
+
proxy: str = Field(default="", description="代理地址")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ImageGenerationConfig(BaseModel):
|
| 36 |
+
"""图片生成配置"""
|
| 37 |
+
enabled: bool = Field(default=True, description="是否启用图片生成")
|
| 38 |
+
supported_models: List[str] = Field(
|
| 39 |
+
default=["gemini-3-pro-preview"],
|
| 40 |
+
description="支持图片生成的模型列表"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class RetryConfig(BaseModel):
|
| 45 |
+
"""重试策略配置"""
|
| 46 |
+
max_new_session_tries: int = Field(default=5, ge=1, le=20, description="新会话尝试账户数")
|
| 47 |
+
max_request_retries: int = Field(default=3, ge=1, le=10, description="请求失败重试次数")
|
| 48 |
+
max_account_switch_tries: int = Field(default=5, ge=1, le=20, description="账户切换尝试次数")
|
| 49 |
+
account_failure_threshold: int = Field(default=3, ge=1, le=10, description="账户失败阈值")
|
| 50 |
+
rate_limit_cooldown_seconds: int = Field(default=600, ge=60, le=3600, description="429冷却时间(秒)")
|
| 51 |
+
session_cache_ttl_seconds: int = Field(default=3600, ge=300, le=86400, description="会话缓存时间(秒)")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class PublicDisplayConfig(BaseModel):
|
| 55 |
+
"""公开展示配置"""
|
| 56 |
+
logo_url: str = Field(default="", description="Logo URL")
|
| 57 |
+
chat_url: str = Field(default="", description="开始对话链接")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class SessionConfig(BaseModel):
|
| 61 |
+
"""Session配置"""
|
| 62 |
+
expire_hours: int = Field(default=24, ge=1, le=168, description="Session过期时间(小时)")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class SecurityConfig(BaseModel):
|
| 66 |
+
"""安全配置(仅从环境变量读取,不可热更新)"""
|
| 67 |
+
admin_key: str = Field(default="", description="管理员密钥(必需)")
|
| 68 |
+
path_prefix: str = Field(default="", description="路径前缀(隐藏管理端点)")
|
| 69 |
+
session_secret_key: str = Field(..., description="Session密钥")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class AppConfig(BaseModel):
|
| 73 |
+
"""应用配置(统一管理)"""
|
| 74 |
+
# 安全配置(仅从环境变量)
|
| 75 |
+
security: SecurityConfig
|
| 76 |
+
|
| 77 |
+
# 业务配置(环境变量 > YAML > 默认值)
|
| 78 |
+
basic: BasicConfig
|
| 79 |
+
image_generation: ImageGenerationConfig
|
| 80 |
+
retry: RetryConfig
|
| 81 |
+
public_display: PublicDisplayConfig
|
| 82 |
+
session: SessionConfig
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ==================== 配置管理器 ====================
|
| 86 |
+
|
| 87 |
+
class ConfigManager:
|
| 88 |
+
"""配置管理器(单例)"""
|
| 89 |
+
|
| 90 |
+
def __init__(self, yaml_path: str = None):
|
| 91 |
+
# 自动检测环境并设置默认路径
|
| 92 |
+
if yaml_path is None:
|
| 93 |
+
if os.path.exists("/data"):
|
| 94 |
+
yaml_path = "/data/settings.yaml" # HF Pro 持久化
|
| 95 |
+
else:
|
| 96 |
+
yaml_path = "data/settings.yaml" # 本地存储
|
| 97 |
+
self.yaml_path = Path(yaml_path)
|
| 98 |
+
self._config: Optional[AppConfig] = None
|
| 99 |
+
self.load()
|
| 100 |
+
|
| 101 |
+
def load(self):
|
| 102 |
+
"""
|
| 103 |
+
加载配置
|
| 104 |
+
|
| 105 |
+
优先级规则:
|
| 106 |
+
1. 安全配置(ADMIN_KEY, PATH_PREFIX, SESSION_SECRET_KEY):仅从环境变量读取
|
| 107 |
+
2. 其他配置:YAML > 环境变量 > 默认值
|
| 108 |
+
"""
|
| 109 |
+
# 1. 加载 YAML 配置
|
| 110 |
+
yaml_data = self._load_yaml()
|
| 111 |
+
|
| 112 |
+
# 2. 加载安全配置(仅从环境变量,不允许 Web 修改)
|
| 113 |
+
security_config = SecurityConfig(
|
| 114 |
+
admin_key=os.getenv("ADMIN_KEY", ""),
|
| 115 |
+
path_prefix=os.getenv("PATH_PREFIX", ""),
|
| 116 |
+
session_secret_key=os.getenv("SESSION_SECRET_KEY", self._generate_secret())
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# 3. 加载基础配置(YAML > 环境变量 > 默认值)
|
| 120 |
+
basic_data = yaml_data.get("basic", {})
|
| 121 |
+
basic_config = BasicConfig(
|
| 122 |
+
api_key=basic_data.get("api_key") or os.getenv("API_KEY", ""),
|
| 123 |
+
base_url=basic_data.get("base_url") or os.getenv("BASE_URL", ""),
|
| 124 |
+
proxy=basic_data.get("proxy") or os.getenv("PROXY", "")
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# 4. 加载其他配置(从 YAML)
|
| 128 |
+
image_generation_config = ImageGenerationConfig(
|
| 129 |
+
**yaml_data.get("image_generation", {})
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
retry_config = RetryConfig(
|
| 133 |
+
**yaml_data.get("retry", {})
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
public_display_config = PublicDisplayConfig(
|
| 137 |
+
**yaml_data.get("public_display", {})
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
session_config = SessionConfig(
|
| 141 |
+
**yaml_data.get("session", {})
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# 5. 构建完整配置
|
| 145 |
+
self._config = AppConfig(
|
| 146 |
+
security=security_config,
|
| 147 |
+
basic=basic_config,
|
| 148 |
+
image_generation=image_generation_config,
|
| 149 |
+
retry=retry_config,
|
| 150 |
+
public_display=public_display_config,
|
| 151 |
+
session=session_config
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def _load_yaml(self) -> dict:
|
| 155 |
+
"""加载 YAML 文件"""
|
| 156 |
+
if self.yaml_path.exists():
|
| 157 |
+
try:
|
| 158 |
+
with open(self.yaml_path, 'r', encoding='utf-8') as f:
|
| 159 |
+
return yaml.safe_load(f) or {}
|
| 160 |
+
except Exception as e:
|
| 161 |
+
print(f"[WARN] 加载配置文件失败: {e},使用默认配置")
|
| 162 |
+
return {}
|
| 163 |
+
|
| 164 |
+
def _generate_secret(self) -> str:
|
| 165 |
+
"""生成随机密钥"""
|
| 166 |
+
return secrets.token_urlsafe(32)
|
| 167 |
+
|
| 168 |
+
def save_yaml(self, data: dict):
|
| 169 |
+
"""保存 YAML 配置"""
|
| 170 |
+
self.yaml_path.parent.mkdir(exist_ok=True)
|
| 171 |
+
with open(self.yaml_path, 'w', encoding='utf-8') as f:
|
| 172 |
+
yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
| 173 |
+
|
| 174 |
+
def reload(self):
|
| 175 |
+
"""重新加载配置(热更新)"""
|
| 176 |
+
self.load()
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def config(self) -> AppConfig:
|
| 180 |
+
"""获取配置"""
|
| 181 |
+
return self._config
|
| 182 |
+
|
| 183 |
+
# ==================== 便捷访问属性 ====================
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def api_key(self) -> str:
|
| 187 |
+
"""API访问密钥"""
|
| 188 |
+
return self._config.basic.api_key
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def admin_key(self) -> str:
|
| 192 |
+
"""管理员密钥"""
|
| 193 |
+
return self._config.security.admin_key
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def path_prefix(self) -> str:
|
| 197 |
+
"""路径前缀"""
|
| 198 |
+
return self._config.security.path_prefix
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def session_secret_key(self) -> str:
|
| 202 |
+
"""Session密钥"""
|
| 203 |
+
return self._config.security.session_secret_key
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def proxy(self) -> str:
|
| 207 |
+
"""代理地址"""
|
| 208 |
+
return self._config.basic.proxy
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def base_url(self) -> str:
|
| 212 |
+
"""服务器URL"""
|
| 213 |
+
return self._config.basic.base_url
|
| 214 |
+
|
| 215 |
+
@property
|
| 216 |
+
def logo_url(self) -> str:
|
| 217 |
+
"""Logo URL"""
|
| 218 |
+
return self._config.public_display.logo_url
|
| 219 |
+
|
| 220 |
+
@property
|
| 221 |
+
def chat_url(self) -> str:
|
| 222 |
+
"""开始对话链接"""
|
| 223 |
+
return self._config.public_display.chat_url
|
| 224 |
+
|
| 225 |
+
@property
|
| 226 |
+
def image_generation_enabled(self) -> bool:
|
| 227 |
+
"""是否启用图片生成"""
|
| 228 |
+
return self._config.image_generation.enabled
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def image_generation_models(self) -> List[str]:
|
| 232 |
+
"""支持图片生成的模型列表"""
|
| 233 |
+
return self._config.image_generation.supported_models
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def session_expire_hours(self) -> int:
|
| 237 |
+
"""Session过期时间(小时)"""
|
| 238 |
+
return self._config.session.expire_hours
|
| 239 |
+
|
| 240 |
+
@property
|
| 241 |
+
def max_new_session_tries(self) -> int:
|
| 242 |
+
"""新会话尝试账户数"""
|
| 243 |
+
return self._config.retry.max_new_session_tries
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def max_request_retries(self) -> int:
|
| 247 |
+
"""请求失败重试次数"""
|
| 248 |
+
return self._config.retry.max_request_retries
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def max_account_switch_tries(self) -> int:
|
| 252 |
+
"""账户切换尝试次数"""
|
| 253 |
+
return self._config.retry.max_account_switch_tries
|
| 254 |
+
|
| 255 |
+
@property
|
| 256 |
+
def account_failure_threshold(self) -> int:
|
| 257 |
+
"""账户失败阈值"""
|
| 258 |
+
return self._config.retry.account_failure_threshold
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def rate_limit_cooldown_seconds(self) -> int:
|
| 262 |
+
"""429冷却时间(秒)"""
|
| 263 |
+
return self._config.retry.rate_limit_cooldown_seconds
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def session_cache_ttl_seconds(self) -> int:
|
| 267 |
+
"""会话缓存时间(秒)"""
|
| 268 |
+
return self._config.retry.session_cache_ttl_seconds
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# ==================== 全局配置管理器 ====================
|
| 272 |
+
|
| 273 |
+
config_manager = ConfigManager()
|
| 274 |
+
|
| 275 |
+
# 注意:不要直接引用 config_manager.config,因为 reload() 后引用会失效
|
| 276 |
+
# 应该始终通过 config_manager.config 访问配置
|
| 277 |
+
def get_config() -> AppConfig:
|
| 278 |
+
"""获取当前配置(支持热更新)"""
|
| 279 |
+
return config_manager.config
|
| 280 |
+
|
| 281 |
+
# 为了向后兼容,保留 config 变量,但使用属性访问
|
| 282 |
+
class _ConfigProxy:
|
| 283 |
+
"""配置代理,确保始终访问最新配置"""
|
| 284 |
+
@property
|
| 285 |
+
def basic(self):
|
| 286 |
+
return config_manager.config.basic
|
| 287 |
+
|
| 288 |
+
@property
|
| 289 |
+
def security(self):
|
| 290 |
+
return config_manager.config.security
|
| 291 |
+
|
| 292 |
+
@property
|
| 293 |
+
def image_generation(self):
|
| 294 |
+
return config_manager.config.image_generation
|
| 295 |
+
|
| 296 |
+
@property
|
| 297 |
+
def retry(self):
|
| 298 |
+
return config_manager.config.retry
|
| 299 |
+
|
| 300 |
+
@property
|
| 301 |
+
def public_display(self):
|
| 302 |
+
return config_manager.config.public_display
|
| 303 |
+
|
| 304 |
+
@property
|
| 305 |
+
def session(self):
|
| 306 |
+
return config_manager.config.session
|
| 307 |
+
|
| 308 |
+
config = _ConfigProxy()
|
core/uptime.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Uptime 实时监控追踪器
|
| 3 |
+
类似 Uptime Kuma 的心跳监控,显示最近请求状态
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from collections import deque
|
| 7 |
+
from datetime import datetime, timezone, timedelta
|
| 8 |
+
from typing import Dict, List
|
| 9 |
+
|
| 10 |
+
# 北京时区 UTC+8
|
| 11 |
+
BEIJING_TZ = timezone(timedelta(hours=8))
|
| 12 |
+
|
| 13 |
+
# 每个服务保留最近 60 条心跳记录
|
| 14 |
+
MAX_HEARTBEATS = 60
|
| 15 |
+
|
| 16 |
+
# 服务配置
|
| 17 |
+
SERVICES = {
|
| 18 |
+
"api_service": {"name": "API 服务", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 19 |
+
"account_pool": {"name": "服务资源", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 20 |
+
"gemini-2.5-flash": {"name": "Gemini 2.5 Flash", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 21 |
+
"gemini-2.5-pro": {"name": "Gemini 2.5 Pro", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 22 |
+
"gemini-3-flash-preview": {"name": "Gemini 3 Flash Preview", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 23 |
+
"gemini-3-pro-preview": {"name": "Gemini 3 Pro Preview", "heartbeats": deque(maxlen=MAX_HEARTBEATS)},
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
SUPPORTED_MODELS = ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-flash-preview", "gemini-3-pro-preview"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def record_request(service: str, success: bool):
|
| 30 |
+
"""记录请求心跳"""
|
| 31 |
+
if service not in SERVICES:
|
| 32 |
+
return
|
| 33 |
+
|
| 34 |
+
SERVICES[service]["heartbeats"].append({
|
| 35 |
+
"time": datetime.now(BEIJING_TZ).strftime("%H:%M:%S"),
|
| 36 |
+
"success": success
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_realtime_status() -> Dict:
|
| 41 |
+
"""获取实时状态数据"""
|
| 42 |
+
result = {"services": {}}
|
| 43 |
+
|
| 44 |
+
for service_id, service_data in SERVICES.items():
|
| 45 |
+
heartbeats = list(service_data["heartbeats"])
|
| 46 |
+
total = len(heartbeats)
|
| 47 |
+
success = sum(1 for h in heartbeats if h["success"])
|
| 48 |
+
|
| 49 |
+
# 计算可用率
|
| 50 |
+
uptime = (success / total * 100) if total > 0 else 100.0
|
| 51 |
+
|
| 52 |
+
# 最近状态
|
| 53 |
+
last_status = "unknown"
|
| 54 |
+
if heartbeats:
|
| 55 |
+
last_status = "up" if heartbeats[-1]["success"] else "down"
|
| 56 |
+
|
| 57 |
+
result["services"][service_id] = {
|
| 58 |
+
"name": service_data["name"],
|
| 59 |
+
"status": last_status,
|
| 60 |
+
"uptime": round(uptime, 1),
|
| 61 |
+
"total": total,
|
| 62 |
+
"success": success,
|
| 63 |
+
"heartbeats": heartbeats[-MAX_HEARTBEATS:] # 最近的心跳
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
result["updated_at"] = datetime.now(BEIJING_TZ).strftime("%Y-%m-%d %H:%M:%S")
|
| 67 |
+
return result
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# 兼容旧接口
|
| 71 |
+
async def get_uptime_summary(days: int = 90) -> Dict:
|
| 72 |
+
"""兼容旧接口,返回实时数据"""
|
| 73 |
+
return get_realtime_status()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
async def uptime_aggregation_task():
|
| 77 |
+
"""后台任务(保留兼容性,实际不需要聚合)"""
|
| 78 |
+
pass
|