mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-03-13 07:17:30 +08:00
feat: 增加详细日志记录并支持热重载、过载情况不计入错误阈值计数、请求日志显示详细信息
fix: 修改进度查询接口 refactor: 移除去水印功能中的公开视频处理逻辑 Close #38
This commit is contained in:
@@ -701,9 +701,20 @@ async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)):
|
|||||||
"operation": log.get("operation"),
|
"operation": log.get("operation"),
|
||||||
"status_code": log.get("status_code"),
|
"status_code": log.get("status_code"),
|
||||||
"duration": log.get("duration"),
|
"duration": log.get("duration"),
|
||||||
"created_at": log.get("created_at")
|
"created_at": log.get("created_at"),
|
||||||
|
"request_body": log.get("request_body"),
|
||||||
|
"response_body": log.get("response_body")
|
||||||
} for log in logs]
|
} for log in logs]
|
||||||
|
|
||||||
|
@router.delete("/api/logs")
|
||||||
|
async def clear_logs(token: str = Depends(verify_admin_token)):
|
||||||
|
"""Clear all logs"""
|
||||||
|
try:
|
||||||
|
await db.clear_all_logs()
|
||||||
|
return {"success": True, "message": "所有日志已清空"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
# Cache config endpoints
|
# Cache config endpoints
|
||||||
@router.post("/api/cache/config")
|
@router.post("/api/cache/config")
|
||||||
async def update_cache_timeout(
|
async def update_cache_timeout(
|
||||||
|
|||||||
@@ -741,8 +741,13 @@ class Database:
|
|||||||
""", (today, token_id))
|
""", (today, token_id))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def increment_error_count(self, token_id: int):
|
async def increment_error_count(self, token_id: int, increment_consecutive: bool = True):
|
||||||
"""Increment error count (both total and consecutive)"""
|
"""Increment error count
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_id: Token ID
|
||||||
|
increment_consecutive: Whether to increment consecutive error count (False for overload errors)
|
||||||
|
"""
|
||||||
from datetime import date
|
from datetime import date
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
today = str(date.today())
|
today = str(date.today())
|
||||||
@@ -752,26 +757,46 @@ class Database:
|
|||||||
|
|
||||||
# If date changed, reset today's error count
|
# If date changed, reset today's error count
|
||||||
if row and row[0] != today:
|
if row and row[0] != today:
|
||||||
await db.execute("""
|
if increment_consecutive:
|
||||||
UPDATE token_stats
|
await db.execute("""
|
||||||
SET error_count = error_count + 1,
|
UPDATE token_stats
|
||||||
consecutive_error_count = consecutive_error_count + 1,
|
SET error_count = error_count + 1,
|
||||||
today_error_count = 1,
|
consecutive_error_count = consecutive_error_count + 1,
|
||||||
today_date = ?,
|
today_error_count = 1,
|
||||||
last_error_at = CURRENT_TIMESTAMP
|
today_date = ?,
|
||||||
WHERE token_id = ?
|
last_error_at = CURRENT_TIMESTAMP
|
||||||
""", (today, token_id))
|
WHERE token_id = ?
|
||||||
|
""", (today, token_id))
|
||||||
|
else:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE token_stats
|
||||||
|
SET error_count = error_count + 1,
|
||||||
|
today_error_count = 1,
|
||||||
|
today_date = ?,
|
||||||
|
last_error_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE token_id = ?
|
||||||
|
""", (today, token_id))
|
||||||
else:
|
else:
|
||||||
# Same day, just increment all counters
|
# Same day, just increment counters
|
||||||
await db.execute("""
|
if increment_consecutive:
|
||||||
UPDATE token_stats
|
await db.execute("""
|
||||||
SET error_count = error_count + 1,
|
UPDATE token_stats
|
||||||
consecutive_error_count = consecutive_error_count + 1,
|
SET error_count = error_count + 1,
|
||||||
today_error_count = today_error_count + 1,
|
consecutive_error_count = consecutive_error_count + 1,
|
||||||
today_date = ?,
|
today_error_count = today_error_count + 1,
|
||||||
last_error_at = CURRENT_TIMESTAMP
|
today_date = ?,
|
||||||
WHERE token_id = ?
|
last_error_at = CURRENT_TIMESTAMP
|
||||||
""", (today, token_id))
|
WHERE token_id = ?
|
||||||
|
""", (today, token_id))
|
||||||
|
else:
|
||||||
|
await db.execute("""
|
||||||
|
UPDATE token_stats
|
||||||
|
SET error_count = error_count + 1,
|
||||||
|
today_error_count = today_error_count + 1,
|
||||||
|
today_date = ?,
|
||||||
|
last_error_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE token_id = ?
|
||||||
|
""", (today, token_id))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def reset_error_count(self, token_id: int):
|
async def reset_error_count(self, token_id: int):
|
||||||
@@ -848,7 +873,13 @@ class Database:
|
|||||||
""", (limit,))
|
""", (limit,))
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
async def clear_all_logs(self):
|
||||||
|
"""Clear all request logs"""
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute("DELETE FROM request_logs")
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
# Admin config operations
|
# Admin config operations
|
||||||
async def get_admin_config(self) -> AdminConfig:
|
async def get_admin_config(self) -> AdminConfig:
|
||||||
"""Get admin configuration"""
|
"""Get admin configuration"""
|
||||||
|
|||||||
@@ -67,16 +67,20 @@ class DebugLogger:
|
|||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""Log API request details to log.txt"""
|
"""Log API request details to log.txt"""
|
||||||
|
|
||||||
|
# Check if debug mode is enabled
|
||||||
|
if not config.debug_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info(f"🔵 [REQUEST] {self._format_timestamp()}")
|
self.logger.info(f"🔵 [REQUEST] {self._format_timestamp()}")
|
||||||
self._write_separator("-")
|
self._write_separator("-")
|
||||||
|
|
||||||
# Basic info
|
# Basic info
|
||||||
self.logger.info(f"Method: {method}")
|
self.logger.info(f"Method: {method}")
|
||||||
self.logger.info(f"URL: {url}")
|
self.logger.info(f"URL: {url}")
|
||||||
|
|
||||||
# Headers
|
# Headers
|
||||||
self.logger.info("\n📋 Headers:")
|
self.logger.info("\n📋 Headers:")
|
||||||
masked_headers = dict(headers)
|
masked_headers = dict(headers)
|
||||||
@@ -85,10 +89,10 @@ class DebugLogger:
|
|||||||
if auth_value.startswith("Bearer "):
|
if auth_value.startswith("Bearer "):
|
||||||
token = auth_value[7:]
|
token = auth_value[7:]
|
||||||
masked_headers["Authorization"] = f"Bearer {self._mask_token(token)}"
|
masked_headers["Authorization"] = f"Bearer {self._mask_token(token)}"
|
||||||
|
|
||||||
for key, value in masked_headers.items():
|
for key, value in masked_headers.items():
|
||||||
self.logger.info(f" {key}: {value}")
|
self.logger.info(f" {key}: {value}")
|
||||||
|
|
||||||
# Body
|
# Body
|
||||||
if body is not None:
|
if body is not None:
|
||||||
self.logger.info("\n📦 Request Body:")
|
self.logger.info("\n📦 Request Body:")
|
||||||
@@ -97,7 +101,7 @@ class DebugLogger:
|
|||||||
self.logger.info(body_str)
|
self.logger.info(body_str)
|
||||||
else:
|
else:
|
||||||
self.logger.info(str(body))
|
self.logger.info(str(body))
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
if files:
|
if files:
|
||||||
self.logger.info("\n📎 Files:")
|
self.logger.info("\n📎 Files:")
|
||||||
@@ -112,14 +116,14 @@ class DebugLogger:
|
|||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
# Fallback for objects that don't support iteration
|
# Fallback for objects that don't support iteration
|
||||||
self.logger.info(" <binary file data>")
|
self.logger.info(" <binary file data>")
|
||||||
|
|
||||||
# Proxy
|
# Proxy
|
||||||
if proxy:
|
if proxy:
|
||||||
self.logger.info(f"\n🌐 Proxy: {proxy}")
|
self.logger.info(f"\n🌐 Proxy: {proxy}")
|
||||||
|
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info("") # Empty line
|
self.logger.info("") # Empty line
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error logging request: {e}")
|
self.logger.error(f"Error logging request: {e}")
|
||||||
|
|
||||||
@@ -131,25 +135,29 @@ class DebugLogger:
|
|||||||
duration_ms: Optional[float] = None
|
duration_ms: Optional[float] = None
|
||||||
):
|
):
|
||||||
"""Log API response details to log.txt"""
|
"""Log API response details to log.txt"""
|
||||||
|
|
||||||
|
# Check if debug mode is enabled
|
||||||
|
if not config.debug_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info(f"🟢 [RESPONSE] {self._format_timestamp()}")
|
self.logger.info(f"🟢 [RESPONSE] {self._format_timestamp()}")
|
||||||
self._write_separator("-")
|
self._write_separator("-")
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
status_emoji = "✅" if 200 <= status_code < 300 else "❌"
|
status_emoji = "✅" if 200 <= status_code < 300 else "❌"
|
||||||
self.logger.info(f"Status: {status_code} {status_emoji}")
|
self.logger.info(f"Status: {status_code} {status_emoji}")
|
||||||
|
|
||||||
# Duration
|
# Duration
|
||||||
if duration_ms is not None:
|
if duration_ms is not None:
|
||||||
self.logger.info(f"Duration: {duration_ms:.2f}ms")
|
self.logger.info(f"Duration: {duration_ms:.2f}ms")
|
||||||
|
|
||||||
# Headers
|
# Headers
|
||||||
self.logger.info("\n📋 Response Headers:")
|
self.logger.info("\n📋 Response Headers:")
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
self.logger.info(f" {key}: {value}")
|
self.logger.info(f" {key}: {value}")
|
||||||
|
|
||||||
# Body
|
# Body
|
||||||
self.logger.info("\n📦 Response Body:")
|
self.logger.info("\n📦 Response Body:")
|
||||||
if isinstance(body, (dict, list)):
|
if isinstance(body, (dict, list)):
|
||||||
@@ -169,7 +177,7 @@ class DebugLogger:
|
|||||||
self.logger.info(body)
|
self.logger.info(body)
|
||||||
else:
|
else:
|
||||||
self.logger.info(str(body))
|
self.logger.info(str(body))
|
||||||
|
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info("") # Empty line
|
self.logger.info("") # Empty line
|
||||||
|
|
||||||
@@ -183,17 +191,21 @@ class DebugLogger:
|
|||||||
response_text: Optional[str] = None
|
response_text: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""Log API error details to log.txt"""
|
"""Log API error details to log.txt"""
|
||||||
|
|
||||||
|
# Check if debug mode is enabled
|
||||||
|
if not config.debug_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info(f"🔴 [ERROR] {self._format_timestamp()}")
|
self.logger.info(f"🔴 [ERROR] {self._format_timestamp()}")
|
||||||
self._write_separator("-")
|
self._write_separator("-")
|
||||||
|
|
||||||
if status_code:
|
if status_code:
|
||||||
self.logger.info(f"Status Code: {status_code}")
|
self.logger.info(f"Status Code: {status_code}")
|
||||||
|
|
||||||
self.logger.info(f"Error Message: {error_message}")
|
self.logger.info(f"Error Message: {error_message}")
|
||||||
|
|
||||||
if response_text:
|
if response_text:
|
||||||
self.logger.info("\n📦 Error Response:")
|
self.logger.info("\n📦 Error Response:")
|
||||||
# Try to parse as JSON
|
# Try to parse as JSON
|
||||||
@@ -207,15 +219,20 @@ class DebugLogger:
|
|||||||
self.logger.info(f"{response_text[:2000]}... (truncated)")
|
self.logger.info(f"{response_text[:2000]}... (truncated)")
|
||||||
else:
|
else:
|
||||||
self.logger.info(response_text)
|
self.logger.info(response_text)
|
||||||
|
|
||||||
self._write_separator()
|
self._write_separator()
|
||||||
self.logger.info("") # Empty line
|
self.logger.info("") # Empty line
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error logging error: {e}")
|
self.logger.error(f"Error logging error: {e}")
|
||||||
|
|
||||||
def log_info(self, message: str):
|
def log_info(self, message: str):
|
||||||
"""Log general info message to log.txt"""
|
"""Log general info message to log.txt"""
|
||||||
|
|
||||||
|
# Check if debug mode is enabled
|
||||||
|
if not config.debug_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"ℹ️ [{self._format_timestamp()}] {message}")
|
self.logger.info(f"ℹ️ [{self._format_timestamp()}] {message}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -404,13 +404,31 @@ class GenerationHandler:
|
|||||||
if is_video and self.concurrency_manager:
|
if is_video and self.concurrency_manager:
|
||||||
await self.concurrency_manager.release_video(token_obj.id)
|
await self.concurrency_manager.release_video(token_obj.id)
|
||||||
|
|
||||||
# Log successful request
|
# Log successful request with complete task info
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# Get complete task info from database
|
||||||
|
task_info = await self.db.get_task(task_id)
|
||||||
|
response_data = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": "success",
|
||||||
|
"prompt": prompt,
|
||||||
|
"model": model
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add result_urls if available
|
||||||
|
if task_info and task_info.result_urls:
|
||||||
|
try:
|
||||||
|
result_urls = json.loads(task_info.result_urls)
|
||||||
|
response_data["result_urls"] = result_urls
|
||||||
|
except:
|
||||||
|
response_data["result_urls"] = task_info.result_urls
|
||||||
|
|
||||||
await self._log_request(
|
await self._log_request(
|
||||||
token_obj.id,
|
token_obj.id,
|
||||||
f"generate_{model_config['type']}",
|
f"generate_{model_config['type']}",
|
||||||
{"model": model, "prompt": prompt, "has_image": image is not None},
|
{"model": model, "prompt": prompt, "has_image": image is not None},
|
||||||
{"task_id": task_id, "status": "success"},
|
response_data,
|
||||||
200,
|
200,
|
||||||
duration
|
duration
|
||||||
)
|
)
|
||||||
@@ -427,9 +445,11 @@ class GenerationHandler:
|
|||||||
if is_video and token_obj and self.concurrency_manager:
|
if is_video and token_obj and self.concurrency_manager:
|
||||||
await self.concurrency_manager.release_video(token_obj.id)
|
await self.concurrency_manager.release_video(token_obj.id)
|
||||||
|
|
||||||
# Record error
|
# Record error (check if it's an overload error)
|
||||||
if token_obj:
|
if token_obj:
|
||||||
await self.token_manager.record_error(token_obj.id)
|
error_str = str(e).lower()
|
||||||
|
is_overload = "heavy_load" in error_str or "under heavy load" in error_str
|
||||||
|
await self.token_manager.record_error(token_obj.id, is_overload=is_overload)
|
||||||
|
|
||||||
# Log failed request
|
# Log failed request
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
@@ -1254,9 +1274,11 @@ class GenerationHandler:
|
|||||||
await self.token_manager.record_success(token_obj.id, is_video=True)
|
await self.token_manager.record_success(token_obj.id, is_video=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Record error
|
# Record error (check if it's an overload error)
|
||||||
if token_obj:
|
if token_obj:
|
||||||
await self.token_manager.record_error(token_obj.id)
|
error_str = str(e).lower()
|
||||||
|
is_overload = "heavy_load" in error_str or "under heavy load" in error_str
|
||||||
|
await self.token_manager.record_error(token_obj.id, is_overload=is_overload)
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"Character and video generation failed: {str(e)}",
|
error_message=f"Character and video generation failed: {str(e)}",
|
||||||
status_code=500,
|
status_code=500,
|
||||||
@@ -1341,9 +1363,11 @@ class GenerationHandler:
|
|||||||
await self.token_manager.record_success(token_obj.id, is_video=True)
|
await self.token_manager.record_success(token_obj.id, is_video=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Record error
|
# Record error (check if it's an overload error)
|
||||||
if token_obj:
|
if token_obj:
|
||||||
await self.token_manager.record_error(token_obj.id)
|
error_str = str(e).lower()
|
||||||
|
is_overload = "heavy_load" in error_str or "under heavy load" in error_str
|
||||||
|
await self.token_manager.record_error(token_obj.id, is_overload=is_overload)
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"Remix generation failed: {str(e)}",
|
error_message=f"Remix generation failed: {str(e)}",
|
||||||
status_code=500,
|
status_code=500,
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ class SoraClient:
|
|||||||
Returns:
|
Returns:
|
||||||
List of pending tasks with progress information
|
List of pending tasks with progress information
|
||||||
"""
|
"""
|
||||||
result = await self._make_request("GET", "/nf/pending", token)
|
result = await self._make_request("GET", "/nf/pending/v2", token)
|
||||||
# The API returns a list directly
|
# The API returns a list directly
|
||||||
return result if isinstance(result, list) else []
|
return result if isinstance(result, list) else []
|
||||||
|
|
||||||
|
|||||||
@@ -982,16 +982,22 @@ class TokenManager:
|
|||||||
else:
|
else:
|
||||||
await self.db.increment_image_count(token_id)
|
await self.db.increment_image_count(token_id)
|
||||||
|
|
||||||
async def record_error(self, token_id: int):
|
async def record_error(self, token_id: int, is_overload: bool = False):
|
||||||
"""Record token error"""
|
"""Record token error
|
||||||
await self.db.increment_error_count(token_id)
|
|
||||||
|
|
||||||
# Check if should ban
|
Args:
|
||||||
stats = await self.db.get_token_stats(token_id)
|
token_id: Token ID
|
||||||
admin_config = await self.db.get_admin_config()
|
is_overload: Whether this is an overload error (heavy_load). If True, only increment total error count.
|
||||||
|
"""
|
||||||
|
await self.db.increment_error_count(token_id, increment_consecutive=not is_overload)
|
||||||
|
|
||||||
if stats and stats.consecutive_error_count >= admin_config.error_ban_threshold:
|
# Check if should ban (only if not overload error)
|
||||||
await self.db.update_token_status(token_id, False)
|
if not is_overload:
|
||||||
|
stats = await self.db.get_token_stats(token_id)
|
||||||
|
admin_config = await self.db.get_admin_config()
|
||||||
|
|
||||||
|
if stats and stats.consecutive_error_count >= admin_config.error_ban_threshold:
|
||||||
|
await self.db.update_token_status(token_id, False)
|
||||||
|
|
||||||
async def record_success(self, token_id: int, is_video: bool = False):
|
async def record_success(self, token_id: int, is_video: bool = False):
|
||||||
"""Record successful request (reset error count)"""
|
"""Record successful request (reset error count)"""
|
||||||
|
|||||||
@@ -328,7 +328,7 @@
|
|||||||
<input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
|
<input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
|
||||||
<span class="text-sm font-medium">启用调试模式</span>
|
<span class="text-sm font-medium">启用调试模式</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="text-xs text-muted-foreground mt-2">开启后,详细的上游API请求和响应日志将写入 <code class="bg-muted px-1 py-0.5 rounded">logs.txt</code> 文件,重启生效</p>
|
<p class="text-xs text-muted-foreground mt-2">开启后,详细的上游API请求和响应日志将写入 <code class="bg-muted px-1 py-0.5 rounded">logs.txt</code> 文件,立即生效</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 border border-yellow-200 dark:border-yellow-800">
|
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 border border-yellow-200 dark:border-yellow-800">
|
||||||
<p class="text-xs text-yellow-800 dark:text-yellow-200">
|
<p class="text-xs text-yellow-800 dark:text-yellow-200">
|
||||||
@@ -345,21 +345,30 @@
|
|||||||
<div class="rounded-lg border border-border bg-background">
|
<div class="rounded-lg border border-border bg-background">
|
||||||
<div class="flex items-center justify-between gap-4 p-4 border-b border-border">
|
<div class="flex items-center justify-between gap-4 p-4 border-b border-border">
|
||||||
<h3 class="text-lg font-semibold">请求日志</h3>
|
<h3 class="text-lg font-semibold">请求日志</h3>
|
||||||
<button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
|
<div class="flex gap-2">
|
||||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<button onclick="clearAllLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-red-50 hover:text-red-700 h-8 px-3 text-sm" title="清空日志">
|
||||||
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
<svg class="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
</button>
|
</svg>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
<button onclick="refreshLogs()" class="inline-flex items-center justify-center rounded-md transition-colors hover:bg-accent h-8 w-8" title="刷新">
|
||||||
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-full overflow-auto max-h-[600px]">
|
<div class="relative w-full overflow-auto max-h-[600px]">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="sticky top-0 bg-background">
|
<thead class="sticky top-0 bg-background">
|
||||||
<tr class="border-b border-border">
|
<tr class="border-b border-border">
|
||||||
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">操作</th>
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-32">操作</th>
|
||||||
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">Token邮箱</th>
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-40">Token邮箱</th>
|
||||||
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">状态码</th>
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-20">状态码</th>
|
||||||
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">耗时(秒)</th>
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-24">耗时(秒)</th>
|
||||||
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground">时间</th>
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-44">时间</th>
|
||||||
|
<th class="h-10 px-3 text-left align-middle font-medium text-muted-foreground w-20">详情</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="logsTableBody" class="divide-y divide-border">
|
<tbody id="logsTableBody" class="divide-y divide-border">
|
||||||
@@ -370,6 +379,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志详情模态框 -->
|
||||||
|
<div id="logDetailModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-background rounded-lg border border-border w-full max-w-3xl shadow-xl max-h-[80vh] flex flex-col">
|
||||||
|
<div class="flex items-center justify-between p-5 border-b border-border">
|
||||||
|
<h3 class="text-lg font-semibold">日志详情</h3>
|
||||||
|
<button onclick="closeLogDetailModal()" class="text-muted-foreground hover:text-foreground">
|
||||||
|
<svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 overflow-y-auto">
|
||||||
|
<div id="logDetailContent" class="space-y-4">
|
||||||
|
<!-- 动态填充 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<footer class="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
|
<footer class="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
|
||||||
<p>© 2025 <a href="https://linux.do/u/thesmallhancat/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">TheSmallHanCat</a> && <a href="https://linux.do/u/tibbar/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">Tibbar</a>. All rights reserved.</p>
|
<p>© 2025 <a href="https://linux.do/u/thesmallhancat/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">TheSmallHanCat</a> && <a href="https://linux.do/u/tibbar/summary" target="_blank" class="no-underline hover:underline" style="color: inherit;">Tibbar</a>. All rights reserved.</p>
|
||||||
@@ -691,8 +720,11 @@
|
|||||||
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
||||||
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
||||||
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
||||||
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
||||||
refreshLogs=async()=>{await loadLogs()},
|
refreshLogs=async()=>{await loadLogs()},
|
||||||
|
showLogDetail=(logId)=>{const log=window.allLogs.find(l=>l.id===logId);if(!log){showToast('日志不存在','error');return}const content=$('logDetailContent');let detailHtml='';if(log.status_code===200){try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody){if(responseBody.data&&responseBody.data.length>0){const item=responseBody.data[0];if(item.url){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">生成结果</h4><div class="rounded-md border border-border p-3 bg-muted/30"><p class="text-sm mb-2"><span class="font-medium">文件URL:</span></p><a href="${item.url}" target="_blank" class="text-blue-600 hover:underline text-xs break-all">${item.url}</a></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应信息</h4><p class="text-sm text-muted-foreground">无响应数据</p></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${log.response_body||'无'}</pre></div>`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误原因</h4><div class="rounded-md border border-red-200 p-3 bg-red-50"><p class="text-sm text-red-700">${responseBody.error.message||responseBody.error||'未知错误'}</p></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body||'无错误信息'}</pre></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body||'无错误信息'}</pre></div>`}}detailHtml+=`<div class="space-y-2 pt-4 border-t border-border"><h4 class="font-medium text-sm">基本信息</h4><div class="grid grid-cols-2 gap-2 text-sm"><div><span class="text-muted-foreground">操作:</span> ${log.operation}</div><div><span class="text-muted-foreground">状态码:</span> <span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration.toFixed(2)}秒</div><div><span class="text-muted-foreground">时间:</span> ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}</div></div></div>`;content.innerHTML=detailHtml;$('logDetailModal').classList.remove('hidden')},
|
||||||
|
closeLogDetailModal=()=>{$('logDetailModal').classList.add('hidden')},
|
||||||
|
clearAllLogs=async()=>{if(!confirm('确定要清空所有日志吗?此操作不可恢复!'))return;try{const r=await apiRequest('/api/logs',{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('日志已清空','success');await loadLogs()}else{showToast('清空失败: '+(d.message||'未知错误'),'error')}}catch(e){showToast('清空失败: '+e.message,'error')}},
|
||||||
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
||||||
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
||||||
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
||||||
|
|||||||
Reference in New Issue
Block a user