xiaoyukkkk commited on
Commit
0a78d0a
·
verified ·
1 Parent(s): 5077e8a

Upload 10 files

Browse files
Files changed (3) hide show
  1. .env.example +2 -2
  2. README.md +105 -24
  3. main.py +132 -10
.env.example CHANGED
@@ -33,8 +33,8 @@ ADMIN_KEY=your-admin-secret-key
33
  # 账户连续失败阈值(默认:3次)
34
  # ACCOUNT_FAILURE_THRESHOLD=3
35
 
36
- # 账户冷却时间,单位秒(默认:300秒=5分钟)
37
- # ACCOUNT_COOLDOWN_SECONDS=300
38
 
39
  # 会话缓存过期时间,单位秒(默认:3600秒=1小时)
40
  # SESSION_CACHE_TTL_SECONDS=3600
 
33
  # 账户连续失败阈值(默认:3次)
34
  # ACCOUNT_FAILURE_THRESHOLD=3
35
 
36
+ # 429限流错误冷却时间,单位秒(默认:600秒=10分钟)
37
+ # RATE_LIMIT_COOLDOWN_SECONDS=600
38
 
39
  # 会话缓存过期时间,单位秒(默认:3600秒=1小时)
40
  # SESSION_CACHE_TTL_SECONDS=3600
README.md CHANGED
@@ -30,14 +30,18 @@ license: mit
30
 
31
  ### 多账户管理
32
  - ✅ **多账户负载均衡** - 支持多账户轮询,故障自动转移
33
- - ✅ **智能熔断机制** - 账户连续失败自动熔断并冷却恢复
34
  - ✅ **三层重试策略** - 新会话重试、请求重试、账户切换
35
  - ✅ **智能会话复用** - 自动管理对话历史,缓存过期自动清理
36
  - ✅ **在线配置管理** - Web界面编辑账户配置,实时生效
 
 
 
37
 
38
  ### 系统功能
39
  - ✅ **JWT自动管理** - 无需手动刷新令牌
40
- - 📊 **可视化管理面板** - 实时监控账户状态、过期时间、失败计数
 
41
  - 📝 **公开日志系统** - 实时查看服务运行状态(内存最多3000条,自动淘汰)
42
  - 🔐 **双重认证保护** - API_KEY 保护聊天接口,ADMIN_KEY 保护管理面板
43
 
@@ -162,7 +166,7 @@ MAX_NEW_SESSION_TRIES=5 # 新会话尝试账户数(默认5)
162
  MAX_REQUEST_RETRIES=3 # 请求失败重试次数(默认3)
163
  MAX_ACCOUNT_SWITCH_TRIES=5 # 每次重试查找账户次数(默认5)
164
  ACCOUNT_FAILURE_THRESHOLD=3 # 账户失败阈值,达到后熔断(默认3)
165
- ACCOUNT_COOLDOWN_SECONDS=300 # 账户冷却时间,秒(默认300=5分钟)
166
  SESSION_CACHE_TTL_SECONDS=3600 # 会话缓存过期时间,秒(默认3600=1小时)
167
  ```
168
 
@@ -174,7 +178,8 @@ SESSION_CACHE_TTL_SECONDS=3600 # 会话缓存过期时间,秒(默认3600=1
174
  2. **请求失败重试**:对话过程中出错,自动重试并切换账户(最多重试3次)
175
  3. **智能熔断机制**:
176
  - 账户连续失败3次 → 自动标记为不可用
177
- - 冷却5分钟后自动恢复
 
178
  - JWT失败和请求失败都会触发熔断
179
  ```
180
 
@@ -235,19 +240,21 @@ ACCOUNTS_CONFIG='[
235
 
236
  ### 访问端点
237
 
238
- | 端点 | 方法 | 说明 |
239
- | -------------------------------------- | ------ | --------------------------- |
240
- | `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
241
- | `/{PATH_PREFIX}/v1/chat/completions` | POST | 聊天接口(需API_KEY) |
242
- | `/{PATH_PREFIX}/admin` | GET | 管理面板(需ADMIN_KEY) |
243
- | `/{PATH_PREFIX}/admin/accounts` | GET | 获取账户状态(需ADMIN_KEY) |
244
- | `/{PATH_PREFIX}/admin/accounts-config` | GET | 获取账户配置(需ADMIN_KEY) |
245
- | `/{PATH_PREFIX}/admin/accounts-config` | PUT | 更新账户配置(需ADMIN_KEY) |
246
- | `/{PATH_PREFIX}/admin/accounts/{id}` | DELETE | 删除指定账户(需ADMIN_KEY) |
247
- | `/{PATH_PREFIX}/admin/log` | GET | 获取系统日志(需ADMIN_KEY) |
248
- | `/{PATH_PREFIX}/admin/log` | DELETE | 清空系统日志(需ADMIN_KEY) |
249
- | `/public/log/html` | GET | 公开日志页面(无需认证) |
250
- | `/public/stats` | GET | 公开统计信息(无需认证) |
 
 
251
 
252
  **访问示例**:
253
 
@@ -345,18 +352,92 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
345
  ### 2. 账户熔断后如何恢复?
346
 
347
  账户连续失败3次后会自动熔断(标记为不可用):
348
- - ⏰ 冷却期:5分钟(可通过 `ACCOUNT_COOLDOWN_SECONDS` 配置)
349
- - 🔄 自动恢复:冷却期过后自动重新尝试
350
- - ✅ 成功后:失败计数重置为0,账户恢复正常
351
 
352
  可在管理面板实时查看账户状态和失败计数。
353
 
354
- ### 3. 图片生成后在哪里找到文件?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
  - **临时存储**: 图片保存在 `./images/`,可通过 URL 访问
357
  - **重启后会丢失**,建议使用持久化存储
358
 
359
- ### 4. 如何设置 BASE_URL?
360
 
361
  **自动检测**(推荐):
362
  - 不设置 `BASE_URL` 环境变量
@@ -393,14 +474,14 @@ Deno.serve(handler);
393
 
394
  配置反向代理后,将 `BASE_URL` 设置为你的自定义域名即可。
395
 
396
- ### 5. API_KEY 和 ADMIN_KEY 的区别?
397
 
398
  - **API_KEY**: 保护聊天接口 (`/v1/chat/completions`)
399
  - **ADMIN_KEY**: 保护管理面板 (`/admin`)
400
 
401
  可以设置相同的值,也可以分开
402
 
403
- ### 6. 如何查看日志?
404
 
405
  - **公开日志**: 访问 `/public/log/html` (无需密钥)
406
  - **管理面板**: 访问 `/admin?key=YOUR_ADMIN_KEY`
 
30
 
31
  ### 多账户管理
32
  - ✅ **多账户负载均衡** - 支持多账户轮询,故障自动转移
33
+ - ✅ **智能熔断机制** - 账户连续失败自动熔断,429限流10分钟后自动恢复
34
  - ✅ **三层重试策略** - 新会话重试、请求重试、账户切换
35
  - ✅ **智能会话复用** - 自动管理对话历史,缓存过期自动清理
36
  - ✅ **在线配置管理** - Web界面编辑账户配置,实时生效
37
+ - ✅ **账户过期自动禁用** - 设置过期时间,过期后自动禁用不可选用
38
+ - ✅ **手动禁用/启用** - 管理面板一键禁用/启用账户
39
+ - ✅ **错误永久禁用** - 普通错误触发熔断后永久禁用,需手动启用恢复
40
 
41
  ### 系统功能
42
  - ✅ **JWT自动管理** - 无需手动刷新令牌
43
+ - 📊 **可视化管理面板** - 实时监控账户状态、过期时间、失败计数、累计对话次数
44
+ - 📈 **账户使用统计** - 自动统计每个账户累计对话次数,持久化保存
45
  - 📝 **公开日志系统** - 实时查看服务运行状态(内存最多3000条,自动淘汰)
46
  - 🔐 **双重认证保护** - API_KEY 保护聊天接口,ADMIN_KEY 保护管理面板
47
 
 
166
  MAX_REQUEST_RETRIES=3 # 请求失败重试次数(默认3)
167
  MAX_ACCOUNT_SWITCH_TRIES=5 # 每次重试查找账户次数(默认5)
168
  ACCOUNT_FAILURE_THRESHOLD=3 # 账户失败阈值,达到后熔断(默认3)
169
+ RATE_LIMIT_COOLDOWN_SECONDS=600 # 429限流冷却时间,秒(默认600=10分钟)
170
  SESSION_CACHE_TTL_SECONDS=3600 # 会话缓存过期时间,秒(默认3600=1小时)
171
  ```
172
 
 
178
  2. **请求失败重试**:对话过程中出错,自动重试并切换账户(最多重试3次)
179
  3. **智能熔断机制**:
180
  - 账户连续失败3次 → 自动标记为不可用
181
+ - **429限流错误**:冷却10分钟后自动恢复
182
+ - **普通错误**:永久禁用,需手动启用
183
  - JWT失败和请求失败都会触发熔断
184
  ```
185
 
 
240
 
241
  ### 访问端点
242
 
243
+ | 端点 | 方法 | 说明 |
244
+ | ---------------------------------------- | ------ | --------------------------- |
245
+ | `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
246
+ | `/{PATH_PREFIX}/v1/chat/completions` | POST | 聊天接口(需API_KEY) |
247
+ | `/{PATH_PREFIX}/admin` | GET | 管理面板(需ADMIN_KEY) |
248
+ | `/{PATH_PREFIX}/admin/accounts` | GET | 获取账户状态(需ADMIN_KEY) |
249
+ | `/{PATH_PREFIX}/admin/accounts-config` | GET | 获取账户配置(需ADMIN_KEY) |
250
+ | `/{PATH_PREFIX}/admin/accounts-config` | PUT | 更新账户配置(需ADMIN_KEY) |
251
+ | `/{PATH_PREFIX}/admin/accounts/{id}` | DELETE | 删除指定账户(需ADMIN_KEY) |
252
+ | `/{PATH_PREFIX}/admin/accounts/{id}/disable` | PUT | 禁用指定账户(需ADMIN_KEY) |
253
+ | `/{PATH_PREFIX}/admin/accounts/{id}/enable` | PUT | 启用指定账户(需ADMIN_KEY) |
254
+ | `/{PATH_PREFIX}/admin/log` | GET | 获取系统日志(需ADMIN_KEY) |
255
+ | `/{PATH_PREFIX}/admin/log` | DELETE | 清空系统日志(需ADMIN_KEY) |
256
+ | `/public/log/html` | GET | 公开日志页面(无需认证) |
257
+ | `/public/stats` | GET | 公开统计信息(无需认证) |
258
 
259
  **访问示例**:
260
 
 
352
  ### 2. 账户熔断后如何恢复?
353
 
354
  账户连续失败3次后会自动熔断(标记为不可用):
355
+ - ⏰ **429限流错误**:冷却10分钟后自动恢复(可通过 `RATE_LIMIT_COOLDOWN_SECONDS` 配置)
356
+ - 🔄 **普通错误**:永久禁用,需在管理面板手动点击"启用"按钮恢复
357
+ - ✅ **成功后**:失败计数重置为0,账户恢复正常
358
 
359
  可在管理面板实时查看账户状态和失败计数。
360
 
361
+ ### 3. 账户禁用功能有哪些?
362
+
363
+ 管理面板提供完整的账户禁用管理功能,不同禁用状态有不同的恢复方式:
364
+
365
+ #### 📋 **账户状态说明**
366
+
367
+ | 状态 | 显示 | 颜色 | 自动恢复 | 恢复方式 | 倒计时 |
368
+ |------|------|------|---------|---------|--------|
369
+ | **正常** | 正常/即将过期 | 绿色/橙色 | - | - | ❌ |
370
+ | **过期禁用** | 过期禁用 | 灰色 | ❌ | 修改过期时间 | ❌ |
371
+ | **手动禁用** | 手动禁用 | 灰色 | ❌ | 点击"启用"按钮 | ❌ |
372
+ | **错误禁用** | 错误禁用 | 红色 | ❌ | 点击"启用"按钮 | ❌ |
373
+ | **429限流** | 429限流 | 橙色 | ✅ 10分钟 | 自动恢复或点击"启用" | ✅ |
374
+
375
+ #### ⚙️ **功能说明**
376
+
377
+ 1. **账户过期自动禁用**
378
+ - 在账户配置中设置 `expires_at` 字段(格式:`YYYY-MM-DD HH:MM:SS`)
379
+ - 过期后账户自动禁用,不参与轮询选择
380
+ - 页面显示灰色半透明卡片,仅保留"删除"按钮
381
+ - 需要修改过期时间才能重新启用
382
+
383
+ 2. **手动禁用/启用**
384
+ - 管理面板每个账户卡片都有"禁用"按钮
385
+ - 点击后立即禁用,不参与轮询选择
386
+ - 显示灰色半透明卡片,提供"启用"+"删除"按钮
387
+ - 点击"启用"按钮即可恢复
388
+
389
+ 3. **错误自动禁用(永久)**
390
+ - 账户连续失败3次触发(非429错误)
391
+ - 自动标记为不可用,永久禁用
392
+ - 显示红色半透明卡片,提供"启用"+"删除"按钮
393
+ - 需要手动点击"启用"按钮恢复
394
+
395
+ 4. **429限流自动禁用(临时)**
396
+ - 账户连续遇到429错误3次触发
397
+ - 自动冷却10分钟(可配置 `RATE_LIMIT_COOLDOWN_SECONDS`)
398
+ - 显示橙色卡片,带倒计时显示(如:`567秒 (429限流)`)
399
+ - 冷却期过后自动恢复,或手动点击"启用"立即恢复
400
+
401
+ #### 💡 **使用建议**
402
+
403
+ - **临时维护**:使用"手动禁用"功能暂时停用账户
404
+ - **账户轮换**:设置过期时间,到期自动禁用
405
+ - **故障排查**:错误禁用的账户需检查后再手动启用
406
+ - **429限流**:耐心等待10分钟自动恢复,或检查请求频率
407
+
408
+ ### 4. 账户对话次数统计如何工作?
409
+
410
+ 系统自动统计每个账户的累计对话次数,无需手动操作。
411
+
412
+ #### 📊 **统计说明**
413
+
414
+ - **自动计数**:每次聊天请求成功后自动 +1
415
+ - **持久化保存**:保存到 `stats.json` 文件,重启不丢失
416
+ - **实时显示**:管理面板账户卡片实时显示累计次数
417
+ - **数据位置**:`stats.json` → `account_conversations` 字段
418
+
419
+ #### 📈 **显示位置**
420
+
421
+ 管理面板账户卡片中,"剩余时长"行下方:
422
+ ```
423
+ 过期时间: 2025-12-31 23:59:59
424
+ 剩余时长: 72.5 小时
425
+ 累计对话: 123 次 ← 蓝色加粗显示
426
+ ```
427
+
428
+ #### 💡 **数据说明**
429
+
430
+ - 统计范围:仅统计成功的对话请求
431
+ - 失败请求:不计入累计次数
432
+ - 数据格式:`{"account_id": conversation_count}`
433
+ - 重置方式:目前需要手动编辑 `stats.json` 文件
434
+
435
+ ### 5. 图片生成后在哪里找到文件?
436
 
437
  - **临时存储**: 图片保存在 `./images/`,可通过 URL 访问
438
  - **重启后会丢失**,建议使用持久化存储
439
 
440
+ ### 6. 如何设置 BASE_URL?
441
 
442
  **自动检测**(推荐):
443
  - 不设置 `BASE_URL` 环境变量
 
474
 
475
  配置反向代理后,将 `BASE_URL` 设置为你的自定义域名即可。
476
 
477
+ ### 7. API_KEY 和 ADMIN_KEY 的区别?
478
 
479
  - **API_KEY**: 保护聊天接口 (`/v1/chat/completions`)
480
  - **ADMIN_KEY**: 保护管理面板 (`/admin`)
481
 
482
  可以设置相同的值,也可以分开
483
 
484
+ ### 8. 如何查看日志?
485
 
486
  - **公开日志**: 访问 `/public/log/html` (无需密钥)
487
  - **管理面板**: 访问 `/admin?key=YOUR_ADMIN_KEY`
main.py CHANGED
@@ -40,7 +40,8 @@ def load_stats():
40
  "total_visitors": 0,
41
  "total_requests": 0,
42
  "request_timestamps": [], # 最近1小时的请求时间戳
43
- "visitor_ips": {} # {ip: timestamp} 记录访问IP和时间
 
44
  }
45
 
46
  def save_stats(stats):
@@ -108,7 +109,7 @@ MAX_NEW_SESSION_TRIES = int(os.getenv("MAX_NEW_SESSION_TRIES", "5")) # 新会
108
  MAX_REQUEST_RETRIES = int(os.getenv("MAX_REQUEST_RETRIES", "3")) # 请求失败最多重试次数(默认3)
109
  MAX_ACCOUNT_SWITCH_TRIES = int(os.getenv("MAX_ACCOUNT_SWITCH_TRIES", "5")) # 每次重试找账户的最大尝试次数(默认5)
110
  ACCOUNT_FAILURE_THRESHOLD = int(os.getenv("ACCOUNT_FAILURE_THRESHOLD", "3")) # 账户连续失败阈值(默认3次)
111
- ACCOUNT_COOLDOWN_SECONDS = int(os.getenv("ACCOUNT_COOLDOWN_SECONDS", "300")) # 账户冷却时间(默认300秒=5分钟)
112
  SESSION_CACHE_TTL_SECONDS = int(os.getenv("SESSION_CACHE_TTL_SECONDS", "3600")) # 会话缓存过期时间(默认3600秒=1小时)
113
 
114
  # ---------- 模型映射配置 ----------
@@ -205,6 +206,7 @@ class AccountConfig:
205
  csesidx: str
206
  config_id: str
207
  expires_at: Optional[str] = None # 账户过期时间 (格式: "2025-12-23 10:59:21")
 
208
 
209
  def get_remaining_hours(self) -> Optional[float]:
210
  """计算账户剩余小时数"""
@@ -259,10 +261,18 @@ class AccountManager:
259
  self.jwt_manager: Optional['JWTManager'] = None # 延迟初始化
260
  self.is_available = True
261
  self.last_error_time = 0.0
 
262
  self.error_count = 0
 
263
 
264
  async def get_jwt(self, request_id: str = "") -> str:
265
  """获取 JWT token (带错误处理)"""
 
 
 
 
 
 
266
  try:
267
  if self.jwt_manager is None:
268
  # 延迟初始化 JWTManager (避免循环依赖)
@@ -277,17 +287,52 @@ class AccountManager:
277
  # 使用配置的失败阈值
278
  if self.error_count >= ACCOUNT_FAILURE_THRESHOLD:
279
  self.is_available = False
280
- logger.error(f"[ACCOUNT] [{self.config.account_id}] JWT获取连续失败{self.error_count}次,账户已标记为不可用")
281
  else:
282
  # 安全:只记录异常类型,不记录详细信息
283
  logger.warning(f"[ACCOUNT] [{self.config.account_id}] JWT获取失败({self.error_count}/{ACCOUNT_FAILURE_THRESHOLD}): {type(e).__name__}")
284
  raise
285
 
286
  def should_retry(self) -> bool:
287
- """检查账户是否可重试(使用配置的冷却期)"""
288
  if self.is_available:
289
  return True
290
- return time.time() - self.last_error_time > ACCOUNT_COOLDOWN_SECONDS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  class MultiAccountManager:
293
  """多账户协调器"""
@@ -359,6 +404,9 @@ class MultiAccountManager:
359
  def add_account(self, config: AccountConfig):
360
  """添加账户"""
361
  manager = AccountManager(config)
 
 
 
362
  self.accounts[config.account_id] = manager
363
  self.account_list.append(config.account_id)
364
  logger.info(f"[MULTI] [ACCOUNT] 添加账户: {config.account_id}")
@@ -380,10 +428,12 @@ class MultiAccountManager:
380
  raise HTTPException(503, f"Account {account_id} temporarily unavailable")
381
  return account
382
 
383
- # 轮询选择可用账户
384
  available_accounts = [
385
  acc_id for acc_id in self.account_list
386
  if self.accounts[acc_id].should_retry()
 
 
387
  ]
388
 
389
  if not available_accounts:
@@ -466,7 +516,8 @@ def load_multi_account_config() -> MultiAccountManager:
466
  host_c_oses=acc.get("host_c_oses"),
467
  csesidx=acc["csesidx"],
468
  config_id=acc["config_id"],
469
- expires_at=acc.get("expires_at")
 
470
  )
471
 
472
  # 检查账户是否已过期
@@ -514,6 +565,27 @@ def delete_account(account_id: str):
514
  save_accounts_to_file(filtered)
515
  reload_accounts()
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  # 验证必需的环境变量
518
  if not PATH_PREFIX:
519
  logger.error("[SYSTEM] 未配置 PATH_PREFIX 环境变量,请设置后重启")
@@ -1054,6 +1126,9 @@ async def admin_get_accounts(path_prefix: str, key: str = None, authorization: s
1054
  # 使用统一的格式化函数
1055
  status, status_color, remaining_display = format_account_expiration(remaining_hours)
1056
 
 
 
 
1057
  accounts_info.append({
1058
  "id": config.account_id,
1059
  "status": status,
@@ -1061,7 +1136,11 @@ async def admin_get_accounts(path_prefix: str, key: str = None, authorization: s
1061
  "remaining_hours": remaining_hours,
1062
  "remaining_display": remaining_display,
1063
  "is_available": account_manager.is_available,
1064
- "error_count": account_manager.error_count
 
 
 
 
1065
  })
1066
 
1067
  return {
@@ -1102,6 +1181,28 @@ async def admin_delete_account(path_prefix: str, account_id: str, key: str = Non
1102
  logger.error(f"[CONFIG] 删除账户失败: {str(e)}")
1103
  raise HTTPException(500, f"删除失败: {str(e)}")
1104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  @app.get("/{path_prefix}/admin/log")
1106
  @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1107
  async def admin_get_logs(
@@ -1389,18 +1490,36 @@ async def chat(
1389
  # 请求成功,重置账户失败计数
1390
  account_manager.is_available = True
1391
  account_manager.error_count = 0
 
 
 
 
 
 
 
 
 
1392
  break
1393
 
1394
  except (httpx.ConnectError, httpx.ReadTimeout, ssl.SSLError, HTTPException) as e:
1395
  # 记录当前失败的账户
1396
  failed_accounts.add(account_manager.config.account_id)
1397
 
 
 
 
1398
  # 增加账户失败计数(触发熔断机制)
1399
  account_manager.last_error_time = time.time()
 
 
 
1400
  account_manager.error_count += 1
1401
  if account_manager.error_count >= ACCOUNT_FAILURE_THRESHOLD:
1402
  account_manager.is_available = False
1403
- logger.error(f"[ACCOUNT] [{account_manager.config.account_id}] [req_{request_id}] 请求连续失败{account_manager.error_count}次,账户已标记为不可用")
 
 
 
1404
 
1405
  retry_count += 1
1406
 
@@ -1410,7 +1529,10 @@ async def chat(
1410
 
1411
  # 特殊处理HTTPException,提取状态码和详情
1412
  if isinstance(e, HTTPException):
1413
- logger.error(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] HTTP错误 {e.status_code}: {e.detail}")
 
 
 
1414
  else:
1415
  logger.error(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] {error_type}: {error_detail}")
1416
 
 
40
  "total_visitors": 0,
41
  "total_requests": 0,
42
  "request_timestamps": [], # 最近1小时的请求时间戳
43
+ "visitor_ips": {}, # {ip: timestamp} 记录访问IP和时间
44
+ "account_conversations": {} # {account_id: conversation_count} 账户对话次数
45
  }
46
 
47
  def save_stats(stats):
 
109
  MAX_REQUEST_RETRIES = int(os.getenv("MAX_REQUEST_RETRIES", "3")) # 请求失败最多重试次数(默认3)
110
  MAX_ACCOUNT_SWITCH_TRIES = int(os.getenv("MAX_ACCOUNT_SWITCH_TRIES", "5")) # 每次重试找账户的最大尝试次数(默认5)
111
  ACCOUNT_FAILURE_THRESHOLD = int(os.getenv("ACCOUNT_FAILURE_THRESHOLD", "3")) # 账户连续失败阈值(默认3次)
112
+ RATE_LIMIT_COOLDOWN_SECONDS = int(os.getenv("RATE_LIMIT_COOLDOWN_SECONDS", "600")) # 429错误冷却时间(默认600秒=10分钟)
113
  SESSION_CACHE_TTL_SECONDS = int(os.getenv("SESSION_CACHE_TTL_SECONDS", "3600")) # 会话缓存过期时间(默认3600秒=1小时)
114
 
115
  # ---------- 模型映射配置 ----------
 
206
  csesidx: str
207
  config_id: str
208
  expires_at: Optional[str] = None # 账户过期时间 (格式: "2025-12-23 10:59:21")
209
+ disabled: bool = False # 手动禁用状态
210
 
211
  def get_remaining_hours(self) -> Optional[float]:
212
  """计算账户剩余小时数"""
 
261
  self.jwt_manager: Optional['JWTManager'] = None # 延迟初始化
262
  self.is_available = True
263
  self.last_error_time = 0.0
264
+ self.last_429_time = 0.0 # 429错误专属时间戳
265
  self.error_count = 0
266
+ self.conversation_count = 0 # 累计对话次数
267
 
268
  async def get_jwt(self, request_id: str = "") -> str:
269
  """获取 JWT token (带错误处理)"""
270
+ # 检查账户是否过期
271
+ if self.config.is_expired():
272
+ self.is_available = False
273
+ logger.warning(f"[ACCOUNT] [{self.config.account_id}] 账户已过期,已自动禁用")
274
+ raise HTTPException(403, f"Account {self.config.account_id} has expired")
275
+
276
  try:
277
  if self.jwt_manager is None:
278
  # 延迟初始化 JWTManager (避免循环依赖)
 
287
  # 使用配置的失败阈值
288
  if self.error_count >= ACCOUNT_FAILURE_THRESHOLD:
289
  self.is_available = False
290
+ logger.error(f"[ACCOUNT] [{self.config.account_id}] JWT获取连续失败{self.error_count}次,账户已永久禁用")
291
  else:
292
  # 安全:只记录异常类型,不记录详细信息
293
  logger.warning(f"[ACCOUNT] [{self.config.account_id}] JWT获取失败({self.error_count}/{ACCOUNT_FAILURE_THRESHOLD}): {type(e).__name__}")
294
  raise
295
 
296
  def should_retry(self) -> bool:
297
+ """检查账户是否可重试(429错误10分钟后恢复,普通错误永久禁用)"""
298
  if self.is_available:
299
  return True
300
+
301
+ current_time = time.time()
302
+
303
+ # 检查429冷却期(10分钟后自动恢复)
304
+ if self.last_429_time > 0:
305
+ if current_time - self.last_429_time > RATE_LIMIT_COOLDOWN_SECONDS:
306
+ return True # 冷却期已过,可以重试
307
+ return False # 仍在冷却期
308
+
309
+ # 普通错误永久禁用
310
+ return False
311
+
312
+ def get_cooldown_info(self) -> tuple[int, str | None]:
313
+ """
314
+ 获取账户冷却信息
315
+
316
+ Returns:
317
+ (cooldown_seconds, cooldown_reason) 元组
318
+ - cooldown_seconds: 剩余冷却秒数,0表示无冷却,-1表示永久禁用
319
+ - cooldown_reason: 冷却原因,None表示无冷却
320
+ """
321
+ if self.is_available:
322
+ return (0, None)
323
+
324
+ current_time = time.time()
325
+
326
+ # 检查429冷却期(10分钟后自动恢复)
327
+ if self.last_429_time > 0:
328
+ remaining_429 = RATE_LIMIT_COOLDOWN_SECONDS - (current_time - self.last_429_time)
329
+ if remaining_429 > 0:
330
+ return (int(remaining_429), "429限流")
331
+ # 429冷却期已过,可以恢复
332
+ return (0, None)
333
+
334
+ # 普通错误永久禁用
335
+ return (-1, "错误禁用")
336
 
337
  class MultiAccountManager:
338
  """多账户协调器"""
 
404
  def add_account(self, config: AccountConfig):
405
  """添加账户"""
406
  manager = AccountManager(config)
407
+ # 从统计数据加载对话次数
408
+ if "account_conversations" in global_stats:
409
+ manager.conversation_count = global_stats["account_conversations"].get(config.account_id, 0)
410
  self.accounts[config.account_id] = manager
411
  self.account_list.append(config.account_id)
412
  logger.info(f"[MULTI] [ACCOUNT] 添加账户: {config.account_id}")
 
428
  raise HTTPException(503, f"Account {account_id} temporarily unavailable")
429
  return account
430
 
431
+ # 轮询选择可用账户(排除过期账户和手动禁用账户)
432
  available_accounts = [
433
  acc_id for acc_id in self.account_list
434
  if self.accounts[acc_id].should_retry()
435
+ and not self.accounts[acc_id].config.is_expired()
436
+ and not self.accounts[acc_id].config.disabled
437
  ]
438
 
439
  if not available_accounts:
 
516
  host_c_oses=acc.get("host_c_oses"),
517
  csesidx=acc["csesidx"],
518
  config_id=acc["config_id"],
519
+ expires_at=acc.get("expires_at"),
520
+ disabled=acc.get("disabled", False) # 读取手动禁用状态,默认为 False
521
  )
522
 
523
  # 检查账户是否已过期
 
565
  save_accounts_to_file(filtered)
566
  reload_accounts()
567
 
568
+ def update_account_disabled_status(account_id: str, disabled: bool):
569
+ """更新账户的禁用状态"""
570
+ accounts_data = load_accounts_from_source()
571
+
572
+ # 查找并更新账户
573
+ found = False
574
+ for i, acc in enumerate(accounts_data, 1):
575
+ if get_account_id(acc, i) == account_id:
576
+ acc["disabled"] = disabled
577
+ found = True
578
+ break
579
+
580
+ if not found:
581
+ raise ValueError(f"账户 {account_id} 不存在")
582
+
583
+ save_accounts_to_file(accounts_data)
584
+ reload_accounts()
585
+
586
+ status_text = "已禁用" if disabled else "已启用"
587
+ logger.info(f"[CONFIG] 账户 {account_id} {status_text}")
588
+
589
  # 验证必需的环境变量
590
  if not PATH_PREFIX:
591
  logger.error("[SYSTEM] 未配置 PATH_PREFIX 环境变量,请设置后重启")
 
1126
  # 使用统一的格式化函数
1127
  status, status_color, remaining_display = format_account_expiration(remaining_hours)
1128
 
1129
+ # 使用AccountManager的方法获取冷却信息
1130
+ cooldown_seconds, cooldown_reason = account_manager.get_cooldown_info()
1131
+
1132
  accounts_info.append({
1133
  "id": config.account_id,
1134
  "status": status,
 
1136
  "remaining_hours": remaining_hours,
1137
  "remaining_display": remaining_display,
1138
  "is_available": account_manager.is_available,
1139
+ "error_count": account_manager.error_count,
1140
+ "disabled": config.disabled, # 添加手动禁用状态
1141
+ "cooldown_seconds": cooldown_seconds, # 冷却剩余秒数
1142
+ "cooldown_reason": cooldown_reason, # 冷却原因
1143
+ "conversation_count": account_manager.conversation_count # 累计对话次数
1144
  })
1145
 
1146
  return {
 
1181
  logger.error(f"[CONFIG] 删除账户失败: {str(e)}")
1182
  raise HTTPException(500, f"删除失败: {str(e)}")
1183
 
1184
+ @app.put("/{path_prefix}/admin/accounts/{account_id}/disable")
1185
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1186
+ async def admin_disable_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1187
+ """手动禁用账户"""
1188
+ try:
1189
+ update_account_disabled_status(account_id, True)
1190
+ return {"status": "success", "message": f"账户 {account_id} 已禁用", "account_count": len(multi_account_mgr.accounts)}
1191
+ except Exception as e:
1192
+ logger.error(f"[CONFIG] 禁用账户失败: {str(e)}")
1193
+ raise HTTPException(500, f"禁用失败: {str(e)}")
1194
+
1195
+ @app.put("/{path_prefix}/admin/accounts/{account_id}/enable")
1196
+ @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1197
+ async def admin_enable_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1198
+ """启用账户"""
1199
+ try:
1200
+ update_account_disabled_status(account_id, False)
1201
+ return {"status": "success", "message": f"账户 {account_id} 已启用", "account_count": len(multi_account_mgr.accounts)}
1202
+ except Exception as e:
1203
+ logger.error(f"[CONFIG] 启用账户失败: {str(e)}")
1204
+ raise HTTPException(500, f"启用失败: {str(e)}")
1205
+
1206
  @app.get("/{path_prefix}/admin/log")
1207
  @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1208
  async def admin_get_logs(
 
1490
  # 请求成功,重置账户失败计数
1491
  account_manager.is_available = True
1492
  account_manager.error_count = 0
1493
+ account_manager.conversation_count += 1 # 增加对话次数
1494
+
1495
+ # 保存对话次数到统计数据
1496
+ with stats_lock:
1497
+ if "account_conversations" not in global_stats:
1498
+ global_stats["account_conversations"] = {}
1499
+ global_stats["account_conversations"][account_manager.config.account_id] = account_manager.conversation_count
1500
+ save_stats(global_stats)
1501
+
1502
  break
1503
 
1504
  except (httpx.ConnectError, httpx.ReadTimeout, ssl.SSLError, HTTPException) as e:
1505
  # 记录当前失败的账户
1506
  failed_accounts.add(account_manager.config.account_id)
1507
 
1508
+ # 检查是否为429错误(Rate Limit)
1509
+ is_rate_limit = isinstance(e, HTTPException) and e.status_code == 429
1510
+
1511
  # 增加账户失败计数(触发熔断机制)
1512
  account_manager.last_error_time = time.time()
1513
+ if is_rate_limit:
1514
+ account_manager.last_429_time = time.time()
1515
+
1516
  account_manager.error_count += 1
1517
  if account_manager.error_count >= ACCOUNT_FAILURE_THRESHOLD:
1518
  account_manager.is_available = False
1519
+ if is_rate_limit:
1520
+ logger.error(f"[ACCOUNT] [{account_manager.config.account_id}] [req_{request_id}] 遇到429错误{account_manager.error_count}次,账户已禁用(需休息{RATE_LIMIT_COOLDOWN_SECONDS}秒)")
1521
+ else:
1522
+ logger.error(f"[ACCOUNT] [{account_manager.config.account_id}] [req_{request_id}] 请求连续失败{account_manager.error_count}次,账户已永久禁用")
1523
 
1524
  retry_count += 1
1525
 
 
1529
 
1530
  # 特殊处理HTTPException,提取状态码和详情
1531
  if isinstance(e, HTTPException):
1532
+ if is_rate_limit:
1533
+ logger.error(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 遇到429限流错误,账户将休息{RATE_LIMIT_COOLDOWN_SECONDS}秒")
1534
+ else:
1535
+ logger.error(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] HTTP错误 {e.status_code}: {e.detail}")
1536
  else:
1537
  logger.error(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] {error_type}: {error_detail}")
1538