Spaces:
Running
Running
Upload 10 files
Browse files- .env.example +2 -2
- README.md +105 -24
- 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 |
-
#
|
| 37 |
-
#
|
| 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 |
-
|
| 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 |
-
-
|
|
|
|
| 178 |
- JWT失败和请求失败都会触发熔断
|
| 179 |
```
|
| 180 |
|
|
@@ -235,19 +240,21 @@ ACCOUNTS_CONFIG='[
|
|
| 235 |
|
| 236 |
### 访问端点
|
| 237 |
|
| 238 |
-
| 端点
|
| 239 |
-
|
|
| 240 |
-
| `/{PATH_PREFIX}/v1/models`
|
| 241 |
-
| `/{PATH_PREFIX}/v1/chat/completions`
|
| 242 |
-
| `/{PATH_PREFIX}/admin`
|
| 243 |
-
| `/{PATH_PREFIX}/admin/accounts`
|
| 244 |
-
| `/{PATH_PREFIX}/admin/accounts-config`
|
| 245 |
-
| `/{PATH_PREFIX}/admin/accounts-config`
|
| 246 |
-
| `/{PATH_PREFIX}/admin/accounts/{id}`
|
| 247 |
-
| `/{PATH_PREFIX}/admin/
|
| 248 |
-
| `/{PATH_PREFIX}/admin/
|
| 249 |
-
| `/
|
| 250 |
-
| `/
|
|
|
|
|
|
|
| 251 |
|
| 252 |
**访问示例**:
|
| 253 |
|
|
@@ -345,18 +352,92 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 345 |
### 2. 账户熔断后如何恢复?
|
| 346 |
|
| 347 |
账户连续失败3次后会自动熔断(标记为不可用):
|
| 348 |
-
- ⏰
|
| 349 |
-
- 🔄
|
| 350 |
-
- ✅
|
| 351 |
|
| 352 |
可在管理面板实时查看账户状态和失败计数。
|
| 353 |
|
| 354 |
-
### 3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
- **临时存储**: 图片保存在 `./images/`,可通过 URL 访问
|
| 357 |
- **重启后会丢失**,建议使用持久化存储
|
| 358 |
|
| 359 |
-
###
|
| 360 |
|
| 361 |
**自动检测**(推荐):
|
| 362 |
- 不设置 `BASE_URL` 环境变量
|
|
@@ -393,14 +474,14 @@ Deno.serve(handler);
|
|
| 393 |
|
| 394 |
配置反向代理后,将 `BASE_URL` 设置为你的自定义域名即可。
|
| 395 |
|
| 396 |
-
###
|
| 397 |
|
| 398 |
- **API_KEY**: 保护聊天接口 (`/v1/chat/completions`)
|
| 399 |
- **ADMIN_KEY**: 保护管理面板 (`/admin`)
|
| 400 |
|
| 401 |
可以设置相同的值,也可以分开
|
| 402 |
|
| 403 |
-
###
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 1404 |
|
| 1405 |
retry_count += 1
|
| 1406 |
|
|
@@ -1410,7 +1529,10 @@ async def chat(
|
|
| 1410 |
|
| 1411 |
# 特殊处理HTTPException,提取状态码和详情
|
| 1412 |
if isinstance(e, HTTPException):
|
| 1413 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|