TheSmallHanCat commited on
Commit
29f247e
·
1 Parent(s): 006c5d6

fix: 错误次数计数、调试配置持久化、用量统计计算、日志输出

Browse files
src/api/admin.py CHANGED
@@ -594,11 +594,11 @@ async def update_debug_config(
594
  ):
595
  """Update debug configuration"""
596
  try:
597
- # Import config instance
598
- from ..core.config import config
599
 
600
- # Update in-memory config
601
- config.set_debug_enabled(request.enabled)
602
 
603
  status = "enabled" if request.enabled else "disabled"
604
  return {"success": True, "message": f"Debug mode {status}", "enabled": request.enabled}
 
594
  ):
595
  """Update debug configuration"""
596
  try:
597
+ # Update debug config in database
598
+ await db.update_debug_config(enabled=request.enabled)
599
 
600
+ # 🔥 Hot reload: sync database config to memory
601
+ await db.reload_config_to_memory()
602
 
603
  status = "enabled" if request.enabled else "disabled"
604
  return {"success": True, "message": f"Debug mode {status}", "enabled": request.enabled}
src/core/database.py CHANGED
@@ -127,6 +127,27 @@ class Database:
127
  VALUES (1, ?, ?, ?)
128
  """, (cache_enabled, cache_timeout, cache_base_url))
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  async def check_and_migrate_db(self, config_dict: dict = None):
131
  """Check database integrity and perform migrations if needed
132
 
@@ -198,6 +219,7 @@ class Database:
198
  ("today_video_count", "INTEGER DEFAULT 0"),
199
  ("today_error_count", "INTEGER DEFAULT 0"),
200
  ("today_date", "DATE"),
 
201
  ]
202
 
203
  for col_name, col_type in stats_columns_to_add:
@@ -273,6 +295,7 @@ class Database:
273
  today_video_count INTEGER DEFAULT 0,
274
  today_error_count INTEGER DEFAULT 0,
275
  today_date DATE,
 
276
  FOREIGN KEY (token_id) REFERENCES tokens(id)
277
  )
278
  """)
@@ -355,6 +378,19 @@ class Database:
355
  )
356
  """)
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  # Create indexes
359
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
360
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
@@ -669,7 +705,13 @@ class Database:
669
  await db.commit()
670
 
671
  async def increment_error_count(self, token_id: int):
672
- """Increment error count with daily reset"""
 
 
 
 
 
 
673
  from datetime import date
674
  async with aiosqlite.connect(self.db_path) as db:
675
  today = str(date.today())
@@ -682,16 +724,18 @@ class Database:
682
  await db.execute("""
683
  UPDATE token_stats
684
  SET error_count = error_count + 1,
 
685
  today_error_count = 1,
686
  today_date = ?,
687
  last_error_at = CURRENT_TIMESTAMP
688
  WHERE token_id = ?
689
  """, (today, token_id))
690
  else:
691
- # Same day, just increment both
692
  await db.execute("""
693
  UPDATE token_stats
694
  SET error_count = error_count + 1,
 
695
  today_error_count = today_error_count + 1,
696
  today_date = ?,
697
  last_error_at = CURRENT_TIMESTAMP
@@ -699,6 +743,21 @@ class Database:
699
  """, (today, token_id))
700
  await db.commit()
701
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  # Config operations
703
  async def get_admin_config(self) -> Optional[AdminConfig]:
704
  """Get admin configuration"""
@@ -876,6 +935,11 @@ class Database:
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"""
@@ -924,3 +988,57 @@ class Database:
924
  """, (new_enabled, new_timeout, new_base_url))
925
 
926
  await db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  VALUES (1, ?, ?, ?)
128
  """, (cache_enabled, cache_timeout, cache_base_url))
129
 
130
+ # Ensure debug_config has a row
131
+ cursor = await db.execute("SELECT COUNT(*) FROM debug_config")
132
+ count = await cursor.fetchone()
133
+ if count[0] == 0:
134
+ debug_enabled = False
135
+ log_requests = True
136
+ log_responses = True
137
+ mask_token = True
138
+
139
+ if config_dict:
140
+ debug_config = config_dict.get("debug", {})
141
+ debug_enabled = debug_config.get("enabled", False)
142
+ log_requests = debug_config.get("log_requests", True)
143
+ log_responses = debug_config.get("log_responses", True)
144
+ mask_token = debug_config.get("mask_token", True)
145
+
146
+ await db.execute("""
147
+ INSERT INTO debug_config (id, enabled, log_requests, log_responses, mask_token)
148
+ VALUES (1, ?, ?, ?, ?)
149
+ """, (debug_enabled, log_requests, log_responses, mask_token))
150
+
151
  async def check_and_migrate_db(self, config_dict: dict = None):
152
  """Check database integrity and perform migrations if needed
153
 
 
219
  ("today_video_count", "INTEGER DEFAULT 0"),
220
  ("today_error_count", "INTEGER DEFAULT 0"),
221
  ("today_date", "DATE"),
222
+ ("consecutive_error_count", "INTEGER DEFAULT 0"), # 🆕 连续错误计数
223
  ]
224
 
225
  for col_name, col_type in stats_columns_to_add:
 
295
  today_video_count INTEGER DEFAULT 0,
296
  today_error_count INTEGER DEFAULT 0,
297
  today_date DATE,
298
+ consecutive_error_count INTEGER DEFAULT 0,
299
  FOREIGN KEY (token_id) REFERENCES tokens(id)
300
  )
301
  """)
 
378
  )
379
  """)
380
 
381
+ # Debug config table
382
+ await db.execute("""
383
+ CREATE TABLE IF NOT EXISTS debug_config (
384
+ id INTEGER PRIMARY KEY DEFAULT 1,
385
+ enabled BOOLEAN DEFAULT 0,
386
+ log_requests BOOLEAN DEFAULT 1,
387
+ log_responses BOOLEAN DEFAULT 1,
388
+ mask_token BOOLEAN DEFAULT 1,
389
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
390
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
391
+ )
392
+ """)
393
+
394
  # Create indexes
395
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
396
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
 
705
  await db.commit()
706
 
707
  async def increment_error_count(self, token_id: int):
708
+ """Increment error count with daily reset
709
+
710
+ Updates two counters:
711
+ - error_count: Historical total errors (never reset)
712
+ - consecutive_error_count: Consecutive errors (reset on success/enable)
713
+ - today_error_count: Today's errors (reset on date change)
714
+ """
715
  from datetime import date
716
  async with aiosqlite.connect(self.db_path) as db:
717
  today = str(date.today())
 
724
  await db.execute("""
725
  UPDATE token_stats
726
  SET error_count = error_count + 1,
727
+ consecutive_error_count = consecutive_error_count + 1,
728
  today_error_count = 1,
729
  today_date = ?,
730
  last_error_at = CURRENT_TIMESTAMP
731
  WHERE token_id = ?
732
  """, (today, token_id))
733
  else:
734
+ # Same day, just increment all counters
735
  await db.execute("""
736
  UPDATE token_stats
737
  SET error_count = error_count + 1,
738
+ consecutive_error_count = consecutive_error_count + 1,
739
  today_error_count = today_error_count + 1,
740
  today_date = ?,
741
  last_error_at = CURRENT_TIMESTAMP
 
743
  """, (today, token_id))
744
  await db.commit()
745
 
746
+ async def reset_error_count(self, token_id: int):
747
+ """Reset consecutive error count (only reset consecutive_error_count, keep error_count and today_error_count)
748
+
749
+ This is called when:
750
+ - Token is manually enabled by admin
751
+ - Request succeeds (resets consecutive error counter)
752
+
753
+ Note: error_count (total historical errors) is NEVER reset
754
+ """
755
+ async with aiosqlite.connect(self.db_path) as db:
756
+ await db.execute("""
757
+ UPDATE token_stats SET consecutive_error_count = 0 WHERE token_id = ?
758
+ """, (token_id,))
759
+ await db.commit()
760
+
761
  # Config operations
762
  async def get_admin_config(self) -> Optional[AdminConfig]:
763
  """Get admin configuration"""
 
935
  config.set_image_timeout(generation_config.image_timeout)
936
  config.set_video_timeout(generation_config.video_timeout)
937
 
938
+ # Reload debug config
939
+ debug_config = await self.get_debug_config()
940
+ if debug_config:
941
+ config.set_debug_enabled(debug_config.enabled)
942
+
943
  # Cache config operations
944
  async def get_cache_config(self) -> CacheConfig:
945
  """Get cache configuration"""
 
988
  """, (new_enabled, new_timeout, new_base_url))
989
 
990
  await db.commit()
991
+
992
+ # Debug config operations
993
+ async def get_debug_config(self) -> 'DebugConfig':
994
+ """Get debug configuration"""
995
+ from .models import DebugConfig
996
+ async with aiosqlite.connect(self.db_path) as db:
997
+ db.row_factory = aiosqlite.Row
998
+ cursor = await db.execute("SELECT * FROM debug_config WHERE id = 1")
999
+ row = await cursor.fetchone()
1000
+ if row:
1001
+ return DebugConfig(**dict(row))
1002
+ # Return default if not found
1003
+ return DebugConfig(enabled=False, log_requests=True, log_responses=True, mask_token=True)
1004
+
1005
+ async def update_debug_config(
1006
+ self,
1007
+ enabled: bool = None,
1008
+ log_requests: bool = None,
1009
+ log_responses: bool = None,
1010
+ mask_token: bool = None
1011
+ ):
1012
+ """Update debug configuration"""
1013
+ async with aiosqlite.connect(self.db_path) as db:
1014
+ db.row_factory = aiosqlite.Row
1015
+ # Get current values
1016
+ cursor = await db.execute("SELECT * FROM debug_config WHERE id = 1")
1017
+ row = await cursor.fetchone()
1018
+
1019
+ if row:
1020
+ current = dict(row)
1021
+ # Use new values if provided, otherwise keep existing
1022
+ new_enabled = enabled if enabled is not None else current.get("enabled", False)
1023
+ new_log_requests = log_requests if log_requests is not None else current.get("log_requests", True)
1024
+ new_log_responses = log_responses if log_responses is not None else current.get("log_responses", True)
1025
+ new_mask_token = mask_token if mask_token is not None else current.get("mask_token", True)
1026
+
1027
+ await db.execute("""
1028
+ UPDATE debug_config
1029
+ SET enabled = ?, log_requests = ?, log_responses = ?, mask_token = ?, updated_at = CURRENT_TIMESTAMP
1030
+ WHERE id = 1
1031
+ """, (new_enabled, new_log_requests, new_log_responses, new_mask_token))
1032
+ else:
1033
+ # Insert default row if not exists
1034
+ new_enabled = enabled if enabled is not None else False
1035
+ new_log_requests = log_requests if log_requests is not None else True
1036
+ new_log_responses = log_responses if log_responses is not None else True
1037
+ new_mask_token = mask_token if mask_token is not None else True
1038
+
1039
+ await db.execute("""
1040
+ INSERT INTO debug_config (id, enabled, log_requests, log_responses, mask_token)
1041
+ VALUES (1, ?, ?, ?, ?)
1042
+ """, (new_enabled, new_log_requests, new_log_responses, new_mask_token))
1043
+
1044
+ await db.commit()
src/core/logger.py CHANGED
@@ -239,5 +239,14 @@ class DebugLogger:
239
  except Exception as e:
240
  self.logger.error(f"Error logging info: {e}")
241
 
 
 
 
 
 
 
 
 
 
242
  # Global debug logger instance
243
  debug_logger = DebugLogger()
 
239
  except Exception as e:
240
  self.logger.error(f"Error logging info: {e}")
241
 
242
+ def log_warning(self, message: str):
243
+ """Log warning message to log.txt"""
244
+ if not config.debug_enabled:
245
+ return
246
+ try:
247
+ self.logger.warning(f"⚠️ [{self._format_timestamp()}] {message}")
248
+ except Exception as e:
249
+ self.logger.error(f"Error logging warning: {e}")
250
+
251
  # Global debug logger instance
252
  debug_logger = DebugLogger()
src/core/models.py CHANGED
@@ -64,6 +64,8 @@ class TokenStats(BaseModel):
64
  today_video_count: int = 0
65
  today_error_count: int = 0
66
  today_date: Optional[str] = None
 
 
67
 
68
 
69
  class Task(BaseModel):
@@ -127,6 +129,17 @@ class CacheConfig(BaseModel):
127
  updated_at: Optional[datetime] = None
128
 
129
 
 
 
 
 
 
 
 
 
 
 
 
130
  # OpenAI Compatible Request Models
131
  class ChatMessage(BaseModel):
132
  """Chat message"""
 
64
  today_video_count: int = 0
65
  today_error_count: int = 0
66
  today_date: Optional[str] = None
67
+ # 连续错误计数 (用于自动禁用判断)
68
+ consecutive_error_count: int = 0
69
 
70
 
71
  class Task(BaseModel):
 
129
  updated_at: Optional[datetime] = None
130
 
131
 
132
+ class DebugConfig(BaseModel):
133
+ """Debug configuration"""
134
+ id: int = 1
135
+ enabled: bool = False
136
+ log_requests: bool = True
137
+ log_responses: bool = True
138
+ mask_token: bool = True
139
+ created_at: Optional[datetime] = None
140
+ updated_at: Optional[datetime] = None
141
+
142
+
143
  # OpenAI Compatible Request Models
144
  class ChatMessage(BaseModel):
145
  """Chat message"""
src/main.py CHANGED
@@ -62,6 +62,10 @@ async def lifespan(app: FastAPI):
62
  config.set_image_timeout(generation_config.image_timeout)
63
  config.set_video_timeout(generation_config.video_timeout)
64
 
 
 
 
 
65
  # Initialize concurrency manager
66
  tokens = await token_manager.get_all_tokens()
67
  await concurrency_manager.initialize(tokens)
 
62
  config.set_image_timeout(generation_config.image_timeout)
63
  config.set_video_timeout(generation_config.video_timeout)
64
 
65
+ # Load debug configuration from database
66
+ debug_config = await db.get_debug_config()
67
+ config.set_debug_enabled(debug_config.enabled)
68
+
69
  # Initialize concurrency manager
70
  tokens = await token_manager.get_all_tokens()
71
  await concurrency_manager.initialize(tokens)
src/services/generation_handler.py CHANGED
@@ -440,10 +440,17 @@ class GenerationHandler:
440
  yield self._create_stream_chunk("缓存图片中...\n")
441
  cached_filename = await self.file_cache.download_and_cache(image_url, "image")
442
  local_url = f"{self._get_base_url()}/tmp/{cached_filename}"
 
 
443
  except Exception as e:
444
  debug_logger.log_error(f"Failed to cache image: {str(e)}")
445
  # 缓存失败不影响结果返回,使用原始URL
446
  local_url = image_url
 
 
 
 
 
447
 
448
  # 返回结果
449
  if stream:
@@ -689,13 +696,20 @@ class GenerationHandler:
689
  if config.cache_enabled:
690
  try:
691
  if stream:
692
- yield self._create_stream_chunk("缓存视频...\n")
693
  cached_filename = await self.file_cache.download_and_cache(video_url, "video")
694
  local_url = f"{self._get_base_url()}/tmp/{cached_filename}"
 
 
695
  except Exception as e:
696
  debug_logger.log_error(f"Failed to cache video: {str(e)}")
697
  # 缓存失败不影响结果返回,使用原始URL
698
  local_url = video_url
 
 
 
 
 
699
 
700
  # 更新数据库
701
  task_id = operation["operation"]["name"]
 
440
  yield self._create_stream_chunk("缓存图片中...\n")
441
  cached_filename = await self.file_cache.download_and_cache(image_url, "image")
442
  local_url = f"{self._get_base_url()}/tmp/{cached_filename}"
443
+ if stream:
444
+ yield self._create_stream_chunk("✅ 图片缓存成功,准备返回缓存地址...\n")
445
  except Exception as e:
446
  debug_logger.log_error(f"Failed to cache image: {str(e)}")
447
  # 缓存失败不影响结果返回,使用原始URL
448
  local_url = image_url
449
+ if stream:
450
+ yield self._create_stream_chunk(f"⚠️ 缓存失败: {str(e)}\n正在返回源链接...\n")
451
+ else:
452
+ if stream:
453
+ yield self._create_stream_chunk("缓存已关闭,正在返回源链接...\n")
454
 
455
  # 返回结果
456
  if stream:
 
696
  if config.cache_enabled:
697
  try:
698
  if stream:
699
+ yield self._create_stream_chunk("正在缓存视频文件...\n")
700
  cached_filename = await self.file_cache.download_and_cache(video_url, "video")
701
  local_url = f"{self._get_base_url()}/tmp/{cached_filename}"
702
+ if stream:
703
+ yield self._create_stream_chunk("✅ 视频缓存成功,准备返回缓存地址...\n")
704
  except Exception as e:
705
  debug_logger.log_error(f"Failed to cache video: {str(e)}")
706
  # 缓存失败不影响结果返回,使用原始URL
707
  local_url = video_url
708
+ if stream:
709
+ yield self._create_stream_chunk(f"⚠️ 缓存失败: {str(e)}\n正在返回源链接...\n")
710
+ else:
711
+ if stream:
712
+ yield self._create_stream_chunk("缓存已关闭,正在返回源链接...\n")
713
 
714
  # 更新数据库
715
  task_id = operation["operation"]["name"]
src/services/token_manager.py CHANGED
@@ -37,17 +37,10 @@ class TokenManager:
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"""
@@ -365,17 +358,25 @@ class TokenManager:
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:
 
37
 
38
  async def enable_token(self, token_id: int):
39
  """Enable a token and reset error count"""
40
+ # Enable the token
41
  await self.db.update_token(token_id, is_active=True)
42
+ # Reset error count when enabling (only reset total error_count, keep today_error_count)
43
+ await self.db.reset_error_count(token_id)
 
 
 
 
 
 
 
 
44
 
45
  async def disable_token(self, token_id: int):
46
  """Disable a token"""
 
358
  """Record token error and auto-disable if threshold reached"""
359
  await self.db.increment_token_stats(token_id, "error")
360
 
361
+ # Check if should auto-disable token (based on consecutive errors)
362
  stats = await self.db.get_token_stats(token_id)
363
  admin_config = await self.db.get_admin_config()
364
 
365
+ if stats and stats.consecutive_error_count >= admin_config.error_ban_threshold:
366
  debug_logger.log_warning(
367
+ f"[TOKEN_BAN] Token {token_id} consecutive error count ({stats.consecutive_error_count}) "
368
  f"reached threshold ({admin_config.error_ban_threshold}), auto-disabling"
369
  )
370
  await self.disable_token(token_id)
371
 
372
+ async def record_success(self, token_id: int):
373
+ """Record successful request (reset consecutive error count)
374
+
375
+ This method resets error_count to 0, which is used for auto-disable threshold checking.
376
+ Note: today_error_count and historical statistics are NOT reset.
377
+ """
378
+ await self.db.reset_error_count(token_id)
379
+
380
  # ========== 余额刷新 ==========
381
 
382
  async def refresh_credits(self, token_id: int) -> int:
static/manage.html CHANGED
@@ -287,7 +287,7 @@
287
  <input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
288
  <span class="text-sm font-medium">启用调试模式</span>
289
  </label>
290
- <p class="text-xs text-muted-foreground mt-2">开启后,详细的上游API请求和响应日志将写入 <code class="bg-muted px-1 py-0.5 rounded">logs.txt</code> 文件,重启生效</p>
291
  </div>
292
  <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 border border-yellow-200 dark:border-yellow-800">
293
  <p class="text-xs text-yellow-800 dark:text-yellow-200">
 
287
  <input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
288
  <span class="text-sm font-medium">启用调试模式</span>
289
  </label>
290
+ <p class="text-xs text-muted-foreground mt-2">开启后,详细的上游API请求和响应日志将写入 <code class="bg-muted px-1 py-0.5 rounded">logs.txt</code> 文件</p>
291
  </div>
292
  <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 border border-yellow-200 dark:border-yellow-800">
293
  <p class="text-xs text-yellow-800 dark:text-yellow-200">