Commit ·
a665c0c
1
Parent(s): c86abf4
feat: 触发429自动禁用和解禁
Browse files- src/core/database.py +5 -1
- src/core/models.py +4 -0
- src/main.py +21 -0
- src/services/generation_handler.py +6 -1
- src/services/token_manager.py +96 -1
src/core/database.py
CHANGED
|
@@ -193,6 +193,8 @@ class Database:
|
|
| 193 |
("video_enabled", "BOOLEAN DEFAULT 1"),
|
| 194 |
("image_concurrency", "INTEGER DEFAULT -1"),
|
| 195 |
("video_concurrency", "INTEGER DEFAULT -1"),
|
|
|
|
|
|
|
| 196 |
]
|
| 197 |
|
| 198 |
for col_name, col_type in columns_to_add:
|
|
@@ -262,7 +264,9 @@ class Database:
|
|
| 262 |
image_enabled BOOLEAN DEFAULT 1,
|
| 263 |
video_enabled BOOLEAN DEFAULT 1,
|
| 264 |
image_concurrency INTEGER DEFAULT -1,
|
| 265 |
-
video_concurrency INTEGER DEFAULT -1
|
|
|
|
|
|
|
| 266 |
)
|
| 267 |
""")
|
| 268 |
|
|
|
|
| 193 |
("video_enabled", "BOOLEAN DEFAULT 1"),
|
| 194 |
("image_concurrency", "INTEGER DEFAULT -1"),
|
| 195 |
("video_concurrency", "INTEGER DEFAULT -1"),
|
| 196 |
+
("ban_reason", "TEXT"), # 禁用原因
|
| 197 |
+
("banned_at", "TIMESTAMP"), # 禁用时间
|
| 198 |
]
|
| 199 |
|
| 200 |
for col_name, col_type in columns_to_add:
|
|
|
|
| 264 |
image_enabled BOOLEAN DEFAULT 1,
|
| 265 |
video_enabled BOOLEAN DEFAULT 1,
|
| 266 |
image_concurrency INTEGER DEFAULT -1,
|
| 267 |
+
video_concurrency INTEGER DEFAULT -1,
|
| 268 |
+
ban_reason TEXT,
|
| 269 |
+
banned_at TIMESTAMP
|
| 270 |
)
|
| 271 |
""")
|
| 272 |
|
src/core/models.py
CHANGED
|
@@ -38,6 +38,10 @@ class Token(BaseModel):
|
|
| 38 |
image_concurrency: int = -1 # -1表示无限制
|
| 39 |
video_concurrency: int = -1 # -1表示无限制
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
class Project(BaseModel):
|
| 43 |
"""Project model for VideoFX"""
|
|
|
|
| 38 |
image_concurrency: int = -1 # -1表示无限制
|
| 39 |
video_concurrency: int = -1 # -1表示无限制
|
| 40 |
|
| 41 |
+
# 429禁用相关
|
| 42 |
+
ban_reason: Optional[str] = None # 禁用原因: "429_rate_limit" 或 None
|
| 43 |
+
banned_at: Optional[datetime] = None # 禁用时间
|
| 44 |
+
|
| 45 |
|
| 46 |
class Project(BaseModel):
|
| 47 |
"""Project model for VideoFX"""
|
src/main.py
CHANGED
|
@@ -73,10 +73,24 @@ async def lifespan(app: FastAPI):
|
|
| 73 |
# Start file cache cleanup task
|
| 74 |
await generation_handler.file_cache.start_cleanup_task()
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
print(f"✓ Database initialized")
|
| 77 |
print(f"✓ Total tokens: {len(tokens)}")
|
| 78 |
print(f"✓ Cache: {'Enabled' if config.cache_enabled else 'Disabled'} (timeout: {config.cache_timeout}s)")
|
| 79 |
print(f"✓ File cache cleanup task started")
|
|
|
|
| 80 |
print(f"✓ Server running on http://{config.server_host}:{config.server_port}")
|
| 81 |
print("=" * 60)
|
| 82 |
|
|
@@ -86,7 +100,14 @@ async def lifespan(app: FastAPI):
|
|
| 86 |
print("Flow2API Shutting down...")
|
| 87 |
# Stop file cache cleanup task
|
| 88 |
await generation_handler.file_cache.stop_cleanup_task()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
print("✓ File cache cleanup task stopped")
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
# Initialize components
|
|
|
|
| 73 |
# Start file cache cleanup task
|
| 74 |
await generation_handler.file_cache.start_cleanup_task()
|
| 75 |
|
| 76 |
+
# Start 429 auto-unban task
|
| 77 |
+
import asyncio
|
| 78 |
+
async def auto_unban_task():
|
| 79 |
+
"""定时任务:每小时检查并解禁429被禁用的token"""
|
| 80 |
+
while True:
|
| 81 |
+
try:
|
| 82 |
+
await asyncio.sleep(3600) # 每小时执行一次
|
| 83 |
+
await token_manager.auto_unban_429_tokens()
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"❌ Auto-unban task error: {e}")
|
| 86 |
+
|
| 87 |
+
auto_unban_task_handle = asyncio.create_task(auto_unban_task())
|
| 88 |
+
|
| 89 |
print(f"✓ Database initialized")
|
| 90 |
print(f"✓ Total tokens: {len(tokens)}")
|
| 91 |
print(f"✓ Cache: {'Enabled' if config.cache_enabled else 'Disabled'} (timeout: {config.cache_timeout}s)")
|
| 92 |
print(f"✓ File cache cleanup task started")
|
| 93 |
+
print(f"✓ 429 auto-unban task started (runs every hour)")
|
| 94 |
print(f"✓ Server running on http://{config.server_host}:{config.server_port}")
|
| 95 |
print("=" * 60)
|
| 96 |
|
|
|
|
| 100 |
print("Flow2API Shutting down...")
|
| 101 |
# Stop file cache cleanup task
|
| 102 |
await generation_handler.file_cache.stop_cleanup_task()
|
| 103 |
+
# Stop auto-unban task
|
| 104 |
+
auto_unban_task_handle.cancel()
|
| 105 |
+
try:
|
| 106 |
+
await auto_unban_task_handle
|
| 107 |
+
except asyncio.CancelledError:
|
| 108 |
+
pass
|
| 109 |
print("✓ File cache cleanup task stopped")
|
| 110 |
+
print("✓ 429 auto-unban task stopped")
|
| 111 |
|
| 112 |
|
| 113 |
# Initialize components
|
src/services/generation_handler.py
CHANGED
|
@@ -358,7 +358,12 @@ class GenerationHandler:
|
|
| 358 |
if stream:
|
| 359 |
yield self._create_stream_chunk(f"❌ {error_msg}\n")
|
| 360 |
if token:
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
yield self._create_error_response(error_msg)
|
| 363 |
|
| 364 |
# 记录失败日志
|
|
|
|
| 358 |
if stream:
|
| 359 |
yield self._create_stream_chunk(f"❌ {error_msg}\n")
|
| 360 |
if token:
|
| 361 |
+
# 检测429错误,立即禁用token
|
| 362 |
+
if "429" in str(e) or "HTTP Error 429" in str(e):
|
| 363 |
+
debug_logger.log_warning(f"[429_BAN] Token {token.id} 遇到429错误,立即禁用")
|
| 364 |
+
await self.token_manager.ban_token_for_429(token.id)
|
| 365 |
+
else:
|
| 366 |
+
await self.token_manager.record_error(token.id)
|
| 367 |
yield self._create_error_response(error_msg)
|
| 368 |
|
| 369 |
# 记录失败日志
|
src/services/token_manager.py
CHANGED
|
@@ -179,7 +179,10 @@ class TokenManager:
|
|
| 179 |
image_concurrency: Optional[int] = None,
|
| 180 |
video_concurrency: Optional[int] = None
|
| 181 |
):
|
| 182 |
-
"""Update token (支持修改project_id和project_name)
|
|
|
|
|
|
|
|
|
|
| 183 |
update_fields = {}
|
| 184 |
|
| 185 |
if st is not None:
|
|
@@ -203,6 +206,25 @@ class TokenManager:
|
|
| 203 |
if video_concurrency is not None:
|
| 204 |
update_fields["video_concurrency"] = video_concurrency
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
if update_fields:
|
| 207 |
await self.db.update_token(token_id, **update_fields)
|
| 208 |
|
|
@@ -377,6 +399,79 @@ class TokenManager:
|
|
| 377 |
"""
|
| 378 |
await self.db.reset_error_count(token_id)
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
# ========== 余额刷新 ==========
|
| 381 |
|
| 382 |
async def refresh_credits(self, token_id: int) -> int:
|
|
|
|
| 179 |
image_concurrency: Optional[int] = None,
|
| 180 |
video_concurrency: Optional[int] = None
|
| 181 |
):
|
| 182 |
+
"""Update token (支持修改project_id和project_name)
|
| 183 |
+
|
| 184 |
+
当用户编辑保存token时,如果token未过期,自动清空429禁用状态
|
| 185 |
+
"""
|
| 186 |
update_fields = {}
|
| 187 |
|
| 188 |
if st is not None:
|
|
|
|
| 206 |
if video_concurrency is not None:
|
| 207 |
update_fields["video_concurrency"] = video_concurrency
|
| 208 |
|
| 209 |
+
# 检查token是否因429被禁用,如果是且未过期,则清空429状态
|
| 210 |
+
token = await self.db.get_token(token_id)
|
| 211 |
+
if token and token.ban_reason == "429_rate_limit":
|
| 212 |
+
# 检查token是否过期
|
| 213 |
+
is_expired = False
|
| 214 |
+
if token.at_expires:
|
| 215 |
+
now = datetime.now(timezone.utc)
|
| 216 |
+
if token.at_expires.tzinfo is None:
|
| 217 |
+
at_expires_aware = token.at_expires.replace(tzinfo=timezone.utc)
|
| 218 |
+
else:
|
| 219 |
+
at_expires_aware = token.at_expires
|
| 220 |
+
is_expired = at_expires_aware <= now
|
| 221 |
+
|
| 222 |
+
# 如果未过期,清空429禁用状态
|
| 223 |
+
if not is_expired:
|
| 224 |
+
debug_logger.log_info(f"[UPDATE_TOKEN] Token {token_id} 编辑保存,清空429禁用状态")
|
| 225 |
+
update_fields["ban_reason"] = None
|
| 226 |
+
update_fields["banned_at"] = None
|
| 227 |
+
|
| 228 |
if update_fields:
|
| 229 |
await self.db.update_token(token_id, **update_fields)
|
| 230 |
|
|
|
|
| 399 |
"""
|
| 400 |
await self.db.reset_error_count(token_id)
|
| 401 |
|
| 402 |
+
async def ban_token_for_429(self, token_id: int):
|
| 403 |
+
"""因429错误立即禁用token
|
| 404 |
+
|
| 405 |
+
Args:
|
| 406 |
+
token_id: Token ID
|
| 407 |
+
"""
|
| 408 |
+
debug_logger.log_warning(f"[429_BAN] 禁用Token {token_id} (原因: 429 Rate Limit)")
|
| 409 |
+
await self.db.update_token(
|
| 410 |
+
token_id,
|
| 411 |
+
is_active=False,
|
| 412 |
+
ban_reason="429_rate_limit",
|
| 413 |
+
banned_at=datetime.now(timezone.utc)
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
async def auto_unban_429_tokens(self):
|
| 417 |
+
"""自动解禁因429被禁用的token
|
| 418 |
+
|
| 419 |
+
规则:
|
| 420 |
+
- 距离禁用时间12小时后自动解禁
|
| 421 |
+
- 仅解禁未过期的token
|
| 422 |
+
- 仅解禁因429被禁用的token
|
| 423 |
+
"""
|
| 424 |
+
all_tokens = await self.db.get_all_tokens()
|
| 425 |
+
now = datetime.now(timezone.utc)
|
| 426 |
+
|
| 427 |
+
for token in all_tokens:
|
| 428 |
+
# 跳过非429禁用的token
|
| 429 |
+
if token.ban_reason != "429_rate_limit":
|
| 430 |
+
continue
|
| 431 |
+
|
| 432 |
+
# 跳过未禁用的token
|
| 433 |
+
if token.is_active:
|
| 434 |
+
continue
|
| 435 |
+
|
| 436 |
+
# 跳过没有禁用时间的token
|
| 437 |
+
if not token.banned_at:
|
| 438 |
+
continue
|
| 439 |
+
|
| 440 |
+
# 检查token是否已过期
|
| 441 |
+
if token.at_expires:
|
| 442 |
+
# 确保时区一致
|
| 443 |
+
if token.at_expires.tzinfo is None:
|
| 444 |
+
at_expires_aware = token.at_expires.replace(tzinfo=timezone.utc)
|
| 445 |
+
else:
|
| 446 |
+
at_expires_aware = token.at_expires
|
| 447 |
+
|
| 448 |
+
# 如果已过期,跳过
|
| 449 |
+
if at_expires_aware <= now:
|
| 450 |
+
debug_logger.log_info(f"[AUTO_UNBAN] Token {token.id} 已过期,跳过解禁")
|
| 451 |
+
continue
|
| 452 |
+
|
| 453 |
+
# 确保banned_at时区一致
|
| 454 |
+
if token.banned_at.tzinfo is None:
|
| 455 |
+
banned_at_aware = token.banned_at.replace(tzinfo=timezone.utc)
|
| 456 |
+
else:
|
| 457 |
+
banned_at_aware = token.banned_at
|
| 458 |
+
|
| 459 |
+
# 检查是否已过12小时
|
| 460 |
+
time_since_ban = now - banned_at_aware
|
| 461 |
+
if time_since_ban.total_seconds() >= 12 * 3600: # 12小时
|
| 462 |
+
debug_logger.log_info(
|
| 463 |
+
f"[AUTO_UNBAN] 解禁Token {token.id} (禁用时间: {banned_at_aware}, "
|
| 464 |
+
f"已过 {time_since_ban.total_seconds() / 3600:.1f} 小时)"
|
| 465 |
+
)
|
| 466 |
+
await self.db.update_token(
|
| 467 |
+
token.id,
|
| 468 |
+
is_active=True,
|
| 469 |
+
ban_reason=None,
|
| 470 |
+
banned_at=None
|
| 471 |
+
)
|
| 472 |
+
# 重置错误计数
|
| 473 |
+
await self.db.reset_error_count(token.id)
|
| 474 |
+
|
| 475 |
# ========== 余额刷新 ==========
|
| 476 |
|
| 477 |
async def refresh_credits(self, token_id: int) -> int:
|