From 2f6fc345a9183606775594193007e290c2dd343d Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Tue, 23 Dec 2025 13:01:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=83=AD=E9=87=8D=E8=BD=BD=E3=80=81=E8=BF=87=E8=BD=BD=E6=83=85?= =?UTF-8?q?=E5=86=B5=E4=B8=8D=E8=AE=A1=E5=85=A5=E9=94=99=E8=AF=AF=E9=98=88?= =?UTF-8?q?=E5=80=BC=E8=AE=A1=E6=95=B0=E3=80=81=E8=AF=B7=E6=B1=82=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=98=BE=E7=A4=BA=E8=AF=A6=E7=BB=86=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=20fix:=20=E4=BF=AE=E6=94=B9=E8=BF=9B=E5=BA=A6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=8E=A5=E5=8F=A3=20refactor:=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=8E=BB=E6=B0=B4=E5=8D=B0=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E8=A7=86=E9=A2=91=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close #38 --- src/api/admin.py | 13 +++++- src/core/database.py | 75 +++++++++++++++++++++--------- src/core/logger.py | 59 ++++++++++++++--------- src/services/generation_handler.py | 40 ++++++++++++---- src/services/sora_client.py | 2 +- src/services/token_manager.py | 22 +++++---- static/manage.html | 56 +++++++++++++++++----- 7 files changed, 194 insertions(+), 73 deletions(-) diff --git a/src/api/admin.py b/src/api/admin.py index 563b112..a57e682 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -701,9 +701,20 @@ async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)): "operation": log.get("operation"), "status_code": log.get("status_code"), "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] +@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 @router.post("/api/cache/config") async def update_cache_timeout( diff --git a/src/core/database.py b/src/core/database.py index 3669b28..fd570dc 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -741,8 +741,13 @@ class Database: """, (today, token_id)) await db.commit() - async def increment_error_count(self, token_id: int): - """Increment error count (both total and consecutive)""" + async def increment_error_count(self, token_id: int, increment_consecutive: bool = True): + """Increment error count + + Args: + token_id: Token ID + increment_consecutive: Whether to increment consecutive error count (False for overload errors) + """ from datetime import date async with aiosqlite.connect(self.db_path) as db: today = str(date.today()) @@ -752,26 +757,46 @@ class Database: # If date changed, reset today's error count if row and row[0] != today: - await db.execute(""" - UPDATE token_stats - SET error_count = error_count + 1, - consecutive_error_count = consecutive_error_count + 1, - today_error_count = 1, - today_date = ?, - last_error_at = CURRENT_TIMESTAMP - WHERE token_id = ? - """, (today, token_id)) + if increment_consecutive: + await db.execute(""" + UPDATE token_stats + SET error_count = error_count + 1, + consecutive_error_count = consecutive_error_count + 1, + today_error_count = 1, + today_date = ?, + last_error_at = CURRENT_TIMESTAMP + 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: - # Same day, just increment all counters - await db.execute(""" - UPDATE token_stats - SET error_count = error_count + 1, - consecutive_error_count = consecutive_error_count + 1, - today_error_count = today_error_count + 1, - today_date = ?, - last_error_at = CURRENT_TIMESTAMP - WHERE token_id = ? - """, (today, token_id)) + # Same day, just increment counters + if increment_consecutive: + await db.execute(""" + UPDATE token_stats + SET error_count = error_count + 1, + consecutive_error_count = consecutive_error_count + 1, + today_error_count = today_error_count + 1, + today_date = ?, + last_error_at = CURRENT_TIMESTAMP + 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() async def reset_error_count(self, token_id: int): @@ -848,7 +873,13 @@ class Database: """, (limit,)) rows = await cursor.fetchall() 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 async def get_admin_config(self) -> AdminConfig: """Get admin configuration""" diff --git a/src/core/logger.py b/src/core/logger.py index c64f1b9..2b2b044 100644 --- a/src/core/logger.py +++ b/src/core/logger.py @@ -67,16 +67,20 @@ class DebugLogger: proxy: Optional[str] = None ): """Log API request details to log.txt""" - + + # Check if debug mode is enabled + if not config.debug_enabled: + return + try: self._write_separator() self.logger.info(f"🔵 [REQUEST] {self._format_timestamp()}") self._write_separator("-") - + # Basic info self.logger.info(f"Method: {method}") self.logger.info(f"URL: {url}") - + # Headers self.logger.info("\n📋 Headers:") masked_headers = dict(headers) @@ -85,10 +89,10 @@ class DebugLogger: if auth_value.startswith("Bearer "): token = auth_value[7:] masked_headers["Authorization"] = f"Bearer {self._mask_token(token)}" - + for key, value in masked_headers.items(): self.logger.info(f" {key}: {value}") - + # Body if body is not None: self.logger.info("\n📦 Request Body:") @@ -97,7 +101,7 @@ class DebugLogger: self.logger.info(body_str) else: self.logger.info(str(body)) - + # Files if files: self.logger.info("\n📎 Files:") @@ -112,14 +116,14 @@ class DebugLogger: except (AttributeError, TypeError): # Fallback for objects that don't support iteration self.logger.info(" ") - + # Proxy if proxy: self.logger.info(f"\n🌐 Proxy: {proxy}") - + self._write_separator() self.logger.info("") # Empty line - + except Exception as e: self.logger.error(f"Error logging request: {e}") @@ -131,25 +135,29 @@ class DebugLogger: duration_ms: Optional[float] = None ): """Log API response details to log.txt""" - + + # Check if debug mode is enabled + if not config.debug_enabled: + return + try: self._write_separator() self.logger.info(f"🟢 [RESPONSE] {self._format_timestamp()}") self._write_separator("-") - + # Status status_emoji = "✅" if 200 <= status_code < 300 else "❌" self.logger.info(f"Status: {status_code} {status_emoji}") - + # Duration if duration_ms is not None: self.logger.info(f"Duration: {duration_ms:.2f}ms") - + # Headers self.logger.info("\n📋 Response Headers:") for key, value in headers.items(): self.logger.info(f" {key}: {value}") - + # Body self.logger.info("\n📦 Response Body:") if isinstance(body, (dict, list)): @@ -169,7 +177,7 @@ class DebugLogger: self.logger.info(body) else: self.logger.info(str(body)) - + self._write_separator() self.logger.info("") # Empty line @@ -183,17 +191,21 @@ class DebugLogger: response_text: Optional[str] = None ): """Log API error details to log.txt""" - + + # Check if debug mode is enabled + if not config.debug_enabled: + return + try: self._write_separator() self.logger.info(f"🔴 [ERROR] {self._format_timestamp()}") self._write_separator("-") - + if status_code: self.logger.info(f"Status Code: {status_code}") - + self.logger.info(f"Error Message: {error_message}") - + if response_text: self.logger.info("\n📦 Error Response:") # Try to parse as JSON @@ -207,15 +219,20 @@ class DebugLogger: self.logger.info(f"{response_text[:2000]}... (truncated)") else: self.logger.info(response_text) - + self._write_separator() self.logger.info("") # Empty line - + except Exception as e: self.logger.error(f"Error logging error: {e}") def log_info(self, message: str): """Log general info message to log.txt""" + + # Check if debug mode is enabled + if not config.debug_enabled: + return + try: self.logger.info(f"ℹ️ [{self._format_timestamp()}] {message}") except Exception as e: diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index 3a00f8f..ca484ae 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -404,13 +404,31 @@ class GenerationHandler: if is_video and self.concurrency_manager: await self.concurrency_manager.release_video(token_obj.id) - # Log successful request + # Log successful request with complete task info 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( token_obj.id, f"generate_{model_config['type']}", {"model": model, "prompt": prompt, "has_image": image is not None}, - {"task_id": task_id, "status": "success"}, + response_data, 200, duration ) @@ -427,9 +445,11 @@ class GenerationHandler: if is_video and token_obj and self.concurrency_manager: await self.concurrency_manager.release_video(token_obj.id) - # Record error + # Record error (check if it's an overload error) 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 duration = time.time() - start_time @@ -1254,9 +1274,11 @@ class GenerationHandler: await self.token_manager.record_success(token_obj.id, is_video=True) except Exception as e: - # Record error + # Record error (check if it's an overload error) 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( error_message=f"Character and video generation failed: {str(e)}", status_code=500, @@ -1341,9 +1363,11 @@ class GenerationHandler: await self.token_manager.record_success(token_obj.id, is_video=True) except Exception as e: - # Record error + # Record error (check if it's an overload error) 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( error_message=f"Remix generation failed: {str(e)}", status_code=500, diff --git a/src/services/sora_client.py b/src/services/sora_client.py index d135695..0068b35 100644 --- a/src/services/sora_client.py +++ b/src/services/sora_client.py @@ -291,7 +291,7 @@ class SoraClient: Returns: 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 return result if isinstance(result, list) else [] diff --git a/src/services/token_manager.py b/src/services/token_manager.py index 578a596..0da8ad2 100644 --- a/src/services/token_manager.py +++ b/src/services/token_manager.py @@ -982,16 +982,22 @@ class TokenManager: else: await self.db.increment_image_count(token_id) - async def record_error(self, token_id: int): - """Record token error""" - await self.db.increment_error_count(token_id) + async def record_error(self, token_id: int, is_overload: bool = False): + """Record token error - # Check if should ban - stats = await self.db.get_token_stats(token_id) - admin_config = await self.db.get_admin_config() + Args: + token_id: Token ID + 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: - await self.db.update_token_status(token_id, False) + # Check if should ban (only if not overload error) + 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): """Record successful request (reset error count)""" diff --git a/static/manage.html b/static/manage.html index 9369ed3..1f61ffc 100644 --- a/static/manage.html +++ b/static/manage.html @@ -328,7 +328,7 @@ 启用调试模式 -

开启后,详细的上游API请求和响应日志将写入 logs.txt 文件,重启生效

+

开启后,详细的上游API请求和响应日志将写入 logs.txt 文件,立即生效

@@ -345,21 +345,30 @@

请求日志

- +
+ + +
- - - - - + + + + + + @@ -370,6 +379,26 @@ + + +

© 2025 TheSmallHanCat && Tibbar. All rights reserved.

@@ -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')}}, 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)}}, - 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=>`
`).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=>``).join('')}catch(e){console.error('加载日志失败:',e)}}, 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+=`

生成结果

文件URL:

${item.url}
`}else{detailHtml+=`

响应数据

${JSON.stringify(responseBody,null,2)}
`}}else{detailHtml+=`

响应数据

${JSON.stringify(responseBody,null,2)}
`}}else{detailHtml+=`

响应信息

无响应数据

`}}catch(e){detailHtml+=`

响应数据

${log.response_body||'无'}
`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`

错误原因

${responseBody.error.message||responseBody.error||'未知错误'}

`}else{detailHtml+=`

错误信息

${log.response_body||'无错误信息'}
`}}catch(e){detailHtml+=`

错误信息

${log.response_body||'无错误信息'}
`}}detailHtml+=`

基本信息

操作: ${log.operation}
状态码: ${log.status_code}
耗时: ${log.duration.toFixed(2)}秒
时间: ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}
`;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)}, 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()}};
操作Token邮箱状态码耗时(秒)时间操作Token邮箱状态码耗时(秒)时间详情
${l.operation}${l.token_email||'未知'}${l.status_code}${l.duration.toFixed(2)}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}
${l.operation}${l.token_email||'未知'}${l.status_code}${l.duration.toFixed(2)}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}