TheSmallHanCat commited on
Commit
586f720
·
1 Parent(s): 0d06c50

fix: 配置热重载、错误处理

Browse files
config/setting.toml CHANGED
@@ -7,7 +7,6 @@ admin_password = "admin"
7
  labs_base_url = "https://labs.google/fx/api"
8
  api_base_url = "https://aisandbox-pa.googleapis.com/v1"
9
  timeout = 120
10
- max_retries = 3
11
  poll_interval = 3.0
12
  max_poll_attempts = 200
13
 
@@ -29,6 +28,9 @@ proxy_url = ""
29
  image_timeout = 300
30
  video_timeout = 1500
31
 
 
 
 
32
  [cache]
33
  enabled = false
34
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
 
7
  labs_base_url = "https://labs.google/fx/api"
8
  api_base_url = "https://aisandbox-pa.googleapis.com/v1"
9
  timeout = 120
 
10
  poll_interval = 3.0
11
  max_poll_attempts = 200
12
 
 
28
  image_timeout = 300
29
  video_timeout = 1500
30
 
31
+ [admin]
32
+ error_ban_threshold = 3
33
+
34
  [cache]
35
  enabled = false
36
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
config/setting_warp.toml CHANGED
@@ -7,7 +7,6 @@ admin_password = "admin"
7
  labs_base_url = "https://labs.google/fx/api"
8
  api_base_url = "https://aisandbox-pa.googleapis.com/v1"
9
  timeout = 120
10
- max_retries = 3
11
  poll_interval = 3.0
12
  max_poll_attempts = 200
13
 
@@ -29,6 +28,9 @@ proxy_url = "socks5://warp:1080"
29
  image_timeout = 300
30
  video_timeout = 1500
31
 
 
 
 
32
  [cache]
33
  enabled = false
34
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
 
7
  labs_base_url = "https://labs.google/fx/api"
8
  api_base_url = "https://aisandbox-pa.googleapis.com/v1"
9
  timeout = 120
 
10
  poll_interval = 3.0
11
  max_poll_attempts = 200
12
 
 
28
  image_timeout = 300
29
  video_timeout = 1500
30
 
31
+ [admin]
32
+ error_ban_threshold = 3
33
+
34
  [cache]
35
  enabled = false
36
  timeout = 7200 # 缓存超时时间(秒), 默认2小时
src/api/admin.py CHANGED
@@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, Header
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel
5
  from typing import Optional, List
 
6
  from ..core.auth import AuthManager
7
  from ..core.database import Database
8
  from ..services.token_manager import TokenManager
@@ -15,6 +16,9 @@ token_manager: TokenManager = None
15
  proxy_manager: ProxyManager = None
16
  db: Database = None
17
 
 
 
 
18
 
19
  def set_dependencies(tm: TokenManager, pm: ProxyManager, database: Database):
20
  """Set service instances"""
@@ -76,6 +80,10 @@ class UpdateDebugConfigRequest(BaseModel):
76
  enabled: bool
77
 
78
 
 
 
 
 
79
  class ST2ATRequest(BaseModel):
80
  """ST转AT请求"""
81
  st: str
@@ -84,16 +92,15 @@ class ST2ATRequest(BaseModel):
84
  # ========== Auth Middleware ==========
85
 
86
  async def verify_admin_token(authorization: str = Header(None)):
87
- """Verify admin token"""
88
  if not authorization or not authorization.startswith("Bearer "):
89
  raise HTTPException(status_code=401, detail="Missing authorization")
90
 
91
  token = authorization[7:]
92
- admin_config = await db.get_admin_config()
93
 
94
- # Simple token verification: check if matches api_key
95
- if token != admin_config.api_key:
96
- raise HTTPException(status_code=401, detail="Invalid admin token")
97
 
98
  return token
99
 
@@ -102,19 +109,32 @@ async def verify_admin_token(authorization: str = Header(None)):
102
 
103
  @router.post("/api/admin/login")
104
  async def admin_login(request: LoginRequest):
105
- """Admin login"""
106
  admin_config = await db.get_admin_config()
107
 
108
  if not AuthManager.verify_admin(request.username, request.password):
109
  raise HTTPException(status_code=401, detail="Invalid credentials")
110
 
 
 
 
 
 
 
111
  return {
112
  "success": True,
113
- "token": admin_config.api_key,
114
  "username": admin_config.username
115
  }
116
 
117
 
 
 
 
 
 
 
 
118
  @router.post("/api/admin/change-password")
119
  async def change_password(
120
  request: ChangePasswordRequest,
@@ -127,10 +147,16 @@ async def change_password(
127
  if not AuthManager.verify_admin(admin_config.username, request.old_password):
128
  raise HTTPException(status_code=400, detail="旧密码错误")
129
 
130
- # Update password
131
  await db.update_admin_config(password=request.new_password)
132
 
133
- return {"success": True, "message": "密码修改成功"}
 
 
 
 
 
 
134
 
135
 
136
  # ========== Token Management ==========
@@ -412,6 +438,10 @@ async def update_generation_config(
412
  ):
413
  """Update generation timeout configuration"""
414
  await db.update_generation_config(request.image_timeout, request.video_timeout)
 
 
 
 
415
  return {"success": True, "message": "生成配置更新成功"}
416
 
417
 
@@ -444,6 +474,12 @@ async def login(request: LoginRequest):
444
  return await admin_login(request)
445
 
446
 
 
 
 
 
 
 
447
  @router.get("/api/stats")
448
  async def get_stats(token: str = Depends(verify_admin_token)):
449
  """Get statistics for dashboard"""
@@ -510,11 +546,23 @@ async def get_admin_config(token: str = Depends(verify_admin_token)):
510
  return {
511
  "admin_username": admin_config.username,
512
  "api_key": admin_config.api_key,
513
- "error_ban_threshold": 3, # Default value
514
  "debug_enabled": config.debug_enabled # Return actual debug status
515
  }
516
 
517
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  @router.post("/api/admin/password")
519
  async def update_admin_password(
520
  request: ChangePasswordRequest,
@@ -529,8 +577,13 @@ async def update_api_key(
529
  request: UpdateAPIKeyRequest,
530
  token: str = Depends(verify_admin_token)
531
  ):
532
- """Update API key"""
 
533
  await db.update_admin_config(api_key=request.new_api_key)
 
 
 
 
534
  return {"success": True, "message": "API Key更新成功"}
535
 
536
 
@@ -565,7 +618,12 @@ async def update_generation_timeout(
565
  token: str = Depends(verify_admin_token)
566
  ):
567
  """Update generation timeout configuration"""
568
- return await update_generation_config(request, token)
 
 
 
 
 
569
 
570
 
571
  # ========== AT Auto Refresh Config ==========
@@ -622,9 +680,8 @@ async def update_cache_enabled(
622
  enabled = request.get("enabled", False)
623
  await db.update_cache_config(enabled=enabled)
624
 
625
- # Update runtime config
626
- from ..core.config import config
627
- config.set_cache_enabled(enabled)
628
 
629
  return {"success": True, "message": f"缓存已{'启用' if enabled else '禁用'}"}
630
 
@@ -641,14 +698,8 @@ async def update_cache_config_full(
641
 
642
  await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url)
643
 
644
- # Update runtime config
645
- from ..core.config import config
646
- if enabled is not None:
647
- config.set_cache_enabled(enabled)
648
- if timeout is not None:
649
- config.set_cache_timeout(timeout)
650
- if base_url is not None:
651
- config.set_cache_base_url(base_url)
652
 
653
  return {"success": True, "message": "缓存配置更新成功"}
654
 
@@ -662,8 +713,7 @@ async def update_cache_base_url(
662
  base_url = request.get("base_url", "")
663
  await db.update_cache_config(base_url=base_url)
664
 
665
- # Update runtime config
666
- from ..core.config import config
667
- config.set_cache_base_url(base_url)
668
 
669
  return {"success": True, "message": "缓存Base URL更新成功"}
 
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel
5
  from typing import Optional, List
6
+ import secrets
7
  from ..core.auth import AuthManager
8
  from ..core.database import Database
9
  from ..services.token_manager import TokenManager
 
16
  proxy_manager: ProxyManager = None
17
  db: Database = None
18
 
19
+ # Store active admin session tokens (in production, use Redis or database)
20
+ active_admin_tokens = set()
21
+
22
 
23
  def set_dependencies(tm: TokenManager, pm: ProxyManager, database: Database):
24
  """Set service instances"""
 
80
  enabled: bool
81
 
82
 
83
+ class UpdateAdminConfigRequest(BaseModel):
84
+ error_ban_threshold: int
85
+
86
+
87
  class ST2ATRequest(BaseModel):
88
  """ST转AT请求"""
89
  st: str
 
92
  # ========== Auth Middleware ==========
93
 
94
  async def verify_admin_token(authorization: str = Header(None)):
95
+ """Verify admin session token (NOT API key)"""
96
  if not authorization or not authorization.startswith("Bearer "):
97
  raise HTTPException(status_code=401, detail="Missing authorization")
98
 
99
  token = authorization[7:]
 
100
 
101
+ # Check if token is in active session tokens
102
+ if token not in active_admin_tokens:
103
+ raise HTTPException(status_code=401, detail="Invalid or expired admin token")
104
 
105
  return token
106
 
 
109
 
110
  @router.post("/api/admin/login")
111
  async def admin_login(request: LoginRequest):
112
+ """Admin login - returns session token (NOT API key)"""
113
  admin_config = await db.get_admin_config()
114
 
115
  if not AuthManager.verify_admin(request.username, request.password):
116
  raise HTTPException(status_code=401, detail="Invalid credentials")
117
 
118
+ # Generate independent session token
119
+ session_token = f"admin-{secrets.token_urlsafe(32)}"
120
+
121
+ # Store in active tokens
122
+ active_admin_tokens.add(session_token)
123
+
124
  return {
125
  "success": True,
126
+ "token": session_token, # Session token (NOT API key)
127
  "username": admin_config.username
128
  }
129
 
130
 
131
+ @router.post("/api/admin/logout")
132
+ async def admin_logout(token: str = Depends(verify_admin_token)):
133
+ """Admin logout - invalidate session token"""
134
+ active_admin_tokens.discard(token)
135
+ return {"success": True, "message": "退出登录成功"}
136
+
137
+
138
  @router.post("/api/admin/change-password")
139
  async def change_password(
140
  request: ChangePasswordRequest,
 
147
  if not AuthManager.verify_admin(admin_config.username, request.old_password):
148
  raise HTTPException(status_code=400, detail="旧密码错误")
149
 
150
+ # Update password in database
151
  await db.update_admin_config(password=request.new_password)
152
 
153
+ # 🔥 Hot reload: sync database config to memory
154
+ await db.reload_config_to_memory()
155
+
156
+ # 🔑 Invalidate all admin session tokens (force re-login for security)
157
+ active_admin_tokens.clear()
158
+
159
+ return {"success": True, "message": "密码修改成功,请重新登录"}
160
 
161
 
162
  # ========== Token Management ==========
 
438
  ):
439
  """Update generation timeout configuration"""
440
  await db.update_generation_config(request.image_timeout, request.video_timeout)
441
+
442
+ # 🔥 Hot reload: sync database config to memory
443
+ await db.reload_config_to_memory()
444
+
445
  return {"success": True, "message": "生成配置更新成功"}
446
 
447
 
 
474
  return await admin_login(request)
475
 
476
 
477
+ @router.post("/api/logout")
478
+ async def logout(token: str = Depends(verify_admin_token)):
479
+ """Logout endpoint (alias for /api/admin/logout)"""
480
+ return await admin_logout(token)
481
+
482
+
483
  @router.get("/api/stats")
484
  async def get_stats(token: str = Depends(verify_admin_token)):
485
  """Get statistics for dashboard"""
 
546
  return {
547
  "admin_username": admin_config.username,
548
  "api_key": admin_config.api_key,
549
+ "error_ban_threshold": admin_config.error_ban_threshold,
550
  "debug_enabled": config.debug_enabled # Return actual debug status
551
  }
552
 
553
 
554
+ @router.post("/api/admin/config")
555
+ async def update_admin_config(
556
+ request: UpdateAdminConfigRequest,
557
+ token: str = Depends(verify_admin_token)
558
+ ):
559
+ """Update admin configuration (error_ban_threshold)"""
560
+ # Update error_ban_threshold in database
561
+ await db.update_admin_config(error_ban_threshold=request.error_ban_threshold)
562
+
563
+ return {"success": True, "message": "配置更新成功"}
564
+
565
+
566
  @router.post("/api/admin/password")
567
  async def update_admin_password(
568
  request: ChangePasswordRequest,
 
577
  request: UpdateAPIKeyRequest,
578
  token: str = Depends(verify_admin_token)
579
  ):
580
+ """Update API key (for external API calls, NOT for admin login)"""
581
+ # Update API key in database
582
  await db.update_admin_config(api_key=request.new_api_key)
583
+
584
+ # 🔥 Hot reload: sync database config to memory
585
+ await db.reload_config_to_memory()
586
+
587
  return {"success": True, "message": "API Key更新成功"}
588
 
589
 
 
618
  token: str = Depends(verify_admin_token)
619
  ):
620
  """Update generation timeout configuration"""
621
+ await db.update_generation_config(request.image_timeout, request.video_timeout)
622
+
623
+ # 🔥 Hot reload: sync database config to memory
624
+ await db.reload_config_to_memory()
625
+
626
+ return {"success": True, "message": "生成配置更新成功"}
627
 
628
 
629
  # ========== AT Auto Refresh Config ==========
 
680
  enabled = request.get("enabled", False)
681
  await db.update_cache_config(enabled=enabled)
682
 
683
+ # 🔥 Hot reload: sync database config to memory
684
+ await db.reload_config_to_memory()
 
685
 
686
  return {"success": True, "message": f"缓存已{'启用' if enabled else '禁用'}"}
687
 
 
698
 
699
  await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url)
700
 
701
+ # 🔥 Hot reload: sync database config to memory
702
+ await db.reload_config_to_memory()
 
 
 
 
 
 
703
 
704
  return {"success": True, "message": "缓存配置更新成功"}
705
 
 
713
  base_url = request.get("base_url", "")
714
  await db.update_cache_config(base_url=base_url)
715
 
716
+ # 🔥 Hot reload: sync database config to memory
717
+ await db.reload_config_to_memory()
 
718
 
719
  return {"success": True, "message": "缓存Base URL更新成功"}
src/core/database.py CHANGED
@@ -55,6 +55,7 @@ class Database:
55
  admin_username = "admin"
56
  admin_password = "admin"
57
  api_key = "han1234"
 
58
 
59
  if config_dict:
60
  global_config = config_dict.get("global", {})
@@ -62,10 +63,13 @@ class Database:
62
  admin_password = global_config.get("admin_password", "admin")
63
  api_key = global_config.get("api_key", "han1234")
64
 
 
 
 
65
  await db.execute("""
66
- INSERT INTO admin_config (id, username, password, api_key)
67
- VALUES (1, ?, ?, ?)
68
- """, (admin_username, admin_password, api_key))
69
 
70
  # Ensure proxy_config has a row
71
  cursor = await db.execute("SELECT COUNT(*) FROM proxy_config")
@@ -178,6 +182,15 @@ class Database:
178
  except Exception as e:
179
  print(f" ✗ Failed to add column '{col_name}': {e}")
180
 
 
 
 
 
 
 
 
 
 
181
  # Check and add missing columns to token_stats table
182
  if await self._table_exists(db, "token_stats"):
183
  stats_columns_to_add = [
@@ -305,6 +318,7 @@ class Database:
305
  username TEXT DEFAULT 'admin',
306
  password TEXT DEFAULT 'admin',
307
  api_key TEXT DEFAULT 'han1234',
 
308
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
309
  )
310
  """)
@@ -829,6 +843,39 @@ class Database:
829
 
830
  await db.commit()
831
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  # Cache config operations
833
  async def get_cache_config(self) -> CacheConfig:
834
  """Get cache configuration"""
 
55
  admin_username = "admin"
56
  admin_password = "admin"
57
  api_key = "han1234"
58
+ error_ban_threshold = 3
59
 
60
  if config_dict:
61
  global_config = config_dict.get("global", {})
 
63
  admin_password = global_config.get("admin_password", "admin")
64
  api_key = global_config.get("api_key", "han1234")
65
 
66
+ admin_config = config_dict.get("admin", {})
67
+ error_ban_threshold = admin_config.get("error_ban_threshold", 3)
68
+
69
  await db.execute("""
70
+ INSERT INTO admin_config (id, username, password, api_key, error_ban_threshold)
71
+ VALUES (1, ?, ?, ?, ?)
72
+ """, (admin_username, admin_password, api_key, error_ban_threshold))
73
 
74
  # Ensure proxy_config has a row
75
  cursor = await db.execute("SELECT COUNT(*) FROM proxy_config")
 
182
  except Exception as e:
183
  print(f" ✗ Failed to add column '{col_name}': {e}")
184
 
185
+ # Check and add missing columns to admin_config table
186
+ if await self._table_exists(db, "admin_config"):
187
+ if not await self._column_exists(db, "admin_config", "error_ban_threshold"):
188
+ try:
189
+ await db.execute("ALTER TABLE admin_config ADD COLUMN error_ban_threshold INTEGER DEFAULT 3")
190
+ print(" ✓ Added column 'error_ban_threshold' to admin_config table")
191
+ except Exception as e:
192
+ print(f" ✗ Failed to add column 'error_ban_threshold': {e}")
193
+
194
  # Check and add missing columns to token_stats table
195
  if await self._table_exists(db, "token_stats"):
196
  stats_columns_to_add = [
 
318
  username TEXT DEFAULT 'admin',
319
  password TEXT DEFAULT 'admin',
320
  api_key TEXT DEFAULT 'han1234',
321
+ error_ban_threshold INTEGER DEFAULT 3,
322
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
323
  )
324
  """)
 
843
 
844
  await db.commit()
845
 
846
+ async def reload_config_to_memory(self):
847
+ """
848
+ Reload all configuration from database to in-memory Config instance.
849
+ This should be called after any configuration update to ensure hot-reload.
850
+
851
+ Includes:
852
+ - Admin config (username, password, api_key)
853
+ - Cache config (enabled, timeout, base_url)
854
+ - Generation config (image_timeout, video_timeout)
855
+ - Proxy config will be handled by ProxyManager
856
+ """
857
+ from .config import config
858
+
859
+ # Reload admin config
860
+ admin_config = await self.get_admin_config()
861
+ if admin_config:
862
+ config.set_admin_username_from_db(admin_config.username)
863
+ config.set_admin_password_from_db(admin_config.password)
864
+ config.api_key = admin_config.api_key
865
+
866
+ # Reload cache config
867
+ cache_config = await self.get_cache_config()
868
+ if cache_config:
869
+ config.set_cache_enabled(cache_config.cache_enabled)
870
+ config.set_cache_timeout(cache_config.cache_timeout)
871
+ config.set_cache_base_url(cache_config.cache_base_url or "")
872
+
873
+ # Reload generation config
874
+ generation_config = await self.get_generation_config()
875
+ if generation_config:
876
+ config.set_image_timeout(generation_config.image_timeout)
877
+ config.set_video_timeout(generation_config.video_timeout)
878
+
879
  # Cache config operations
880
  async def get_cache_config(self) -> CacheConfig:
881
  """Get cache configuration"""
src/core/models.py CHANGED
@@ -100,6 +100,7 @@ class AdminConfig(BaseModel):
100
  username: str
101
  password: str
102
  api_key: str
 
103
 
104
 
105
  class ProxyConfig(BaseModel):
 
100
  username: str
101
  password: str
102
  api_key: str
103
+ error_ban_threshold: int = 3 # Auto-disable token after N consecutive errors
104
 
105
 
106
  class ProxyConfig(BaseModel):
src/services/token_manager.py CHANGED
@@ -36,8 +36,18 @@ class TokenManager:
36
  await self.db.delete_token(token_id)
37
 
38
  async def enable_token(self, token_id: int):
39
- """Enable a token"""
40
  await self.db.update_token(token_id, is_active=True)
 
 
 
 
 
 
 
 
 
 
41
 
42
  async def disable_token(self, token_id: int):
43
  """Disable a token"""
@@ -352,9 +362,20 @@ class TokenManager:
352
  await self.db.increment_token_stats(token_id, "image")
353
 
354
  async def record_error(self, token_id: int):
355
- """Record token error"""
356
  await self.db.increment_token_stats(token_id, "error")
357
 
 
 
 
 
 
 
 
 
 
 
 
358
  # ========== 余额刷新 ==========
359
 
360
  async def refresh_credits(self, token_id: int) -> int:
 
36
  await self.db.delete_token(token_id)
37
 
38
  async def enable_token(self, token_id: int):
39
+ """Enable a token and reset error count"""
40
  await self.db.update_token(token_id, is_active=True)
41
+ # Reset error count when enabling
42
+ async with self.db._lock if hasattr(self.db, '_lock') else asyncio.Lock():
43
+ import aiosqlite
44
+ async with aiosqlite.connect(self.db.db_path) as db:
45
+ await db.execute("""
46
+ UPDATE token_stats
47
+ SET error_count = 0, today_error_count = 0
48
+ WHERE token_id = ?
49
+ """, (token_id,))
50
+ await db.commit()
51
 
52
  async def disable_token(self, token_id: int):
53
  """Disable a token"""
 
362
  await self.db.increment_token_stats(token_id, "image")
363
 
364
  async def record_error(self, token_id: int):
365
+ """Record token error and auto-disable if threshold reached"""
366
  await self.db.increment_token_stats(token_id, "error")
367
 
368
+ # Check if should auto-disable token
369
+ stats = await self.db.get_token_stats(token_id)
370
+ admin_config = await self.db.get_admin_config()
371
+
372
+ if stats and stats.error_count >= admin_config.error_ban_threshold:
373
+ debug_logger.log_warning(
374
+ f"[TOKEN_BAN] Token {token_id} error count ({stats.error_count}) "
375
+ f"reached threshold ({admin_config.error_ban_threshold}), auto-disabling"
376
+ )
377
+ await self.disable_token(token_id)
378
+
379
  # ========== 余额刷新 ==========
380
 
381
  async def refresh_credits(self, token_id: int) -> int: