xiaoyukkkk commited on
Commit
e42f78b
·
verified ·
1 Parent(s): 5f304a8

Upload 9 files

Browse files
Files changed (3) hide show
  1. core/account.py +30 -3
  2. core/config.py +308 -0
  3. 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
- ACCOUNTS_FILE = "accounts.json"
 
 
 
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