From ef49e3e670a2d4362280e317f4db91141e8440cd Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Sat, 24 Jan 2026 19:19:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8E=BB=E6=B0=B4?= =?UTF-8?q?=E5=8D=B0=E5=A4=B1=E8=B4=A5=E8=87=AA=E5=8A=A8=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E3=80=81=E4=BC=98=E5=8C=96=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=80=BB=E8=BE=91=E5=8F=8A=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin.py | 36 ++++++++-- src/core/database.py | 20 ++++-- src/core/models.py | 1 + src/services/generation_handler.py | 108 ++++++++++++++++++----------- static/manage.html | 25 ++++++- 5 files changed, 131 insertions(+), 59 deletions(-) diff --git a/src/api/admin.py b/src/api/admin.py index baf0915..21e1c71 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -154,6 +154,7 @@ class UpdateWatermarkFreeConfigRequest(BaseModel): parse_method: Optional[str] = "third_party" # "third_party" or "custom" custom_parse_url: Optional[str] = None custom_parse_token: Optional[str] = None + fallback_on_failure: Optional[bool] = True # Auto fallback to watermarked video on failure class UpdateCallLogicConfigRequest(BaseModel): call_mode: Optional[str] = None # "default" or "polling" @@ -430,14 +431,16 @@ async def batch_enable_all(request: BatchDisableRequest = None, token: str = Dep @router.post("/api/tokens/batch/delete-disabled") async def batch_delete_disabled(request: BatchDisableRequest = None, token: str = Depends(verify_admin_token)): - """Delete selected tokens or all disabled tokens""" + """Delete selected disabled tokens or all disabled tokens""" try: if request and request.token_ids: - # Delete only selected tokens + # Delete only selected tokens that are disabled deleted_count = 0 for token_id in request.token_ids: - await token_manager.delete_token(token_id) - deleted_count += 1 + token_obj = await db.get_token(token_id) + if token_obj and not token_obj.is_active: + await token_manager.delete_token(token_id) + deleted_count += 1 else: # Delete all disabled tokens (backward compatibility) tokens = await db.get_all_tokens() @@ -449,7 +452,7 @@ async def batch_delete_disabled(request: BatchDisableRequest = None, token: str return { "success": True, - "message": f"已删除 {deleted_count} 个Token", + "message": f"已删除 {deleted_count} 个禁用Token", "deleted_count": deleted_count } except Exception as e: @@ -472,6 +475,23 @@ async def batch_disable_selected(request: BatchDisableRequest, token: str = Depe except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@router.post("/api/tokens/batch/delete-selected") +async def batch_delete_selected(request: BatchDisableRequest, token: str = Depends(verify_admin_token)): + """Delete selected tokens (regardless of their status)""" + try: + deleted_count = 0 + for token_id in request.token_ids: + await token_manager.delete_token(token_id) + deleted_count += 1 + + return { + "success": True, + "message": f"已删除 {deleted_count} 个Token", + "deleted_count": deleted_count + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @router.post("/api/tokens/batch/update-proxy") async def batch_update_proxy(request: BatchUpdateProxyRequest, token: str = Depends(verify_admin_token)): """Batch update proxy for selected tokens""" @@ -864,7 +884,8 @@ async def get_watermark_free_config(token: str = Depends(verify_admin_token)) -> "watermark_free_enabled": config_obj.watermark_free_enabled, "parse_method": config_obj.parse_method, "custom_parse_url": config_obj.custom_parse_url, - "custom_parse_token": config_obj.custom_parse_token + "custom_parse_token": config_obj.custom_parse_token, + "fallback_on_failure": config_obj.fallback_on_failure } @router.post("/api/watermark-free/config") @@ -878,7 +899,8 @@ async def update_watermark_free_config( request.watermark_free_enabled, request.parse_method, request.custom_parse_url, - request.custom_parse_token + request.custom_parse_token, + request.fallback_on_failure ) # Update in-memory config diff --git a/src/core/database.py b/src/core/database.py index e57d145..1b746b6 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -105,6 +105,7 @@ class Database: parse_method = "third_party" custom_parse_url = None custom_parse_token = None + fallback_on_failure = True # Default to True if config_dict: watermark_config = config_dict.get("watermark_free", {}) @@ -112,15 +113,16 @@ class Database: parse_method = watermark_config.get("parse_method", "third_party") custom_parse_url = watermark_config.get("custom_parse_url", "") custom_parse_token = watermark_config.get("custom_parse_token", "") + fallback_on_failure = watermark_config.get("fallback_on_failure", True) # Convert empty strings to None custom_parse_url = custom_parse_url if custom_parse_url else None custom_parse_token = custom_parse_token if custom_parse_token else None await db.execute(""" - INSERT INTO watermark_free_config (id, watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token) - VALUES (1, ?, ?, ?, ?) - """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token)) + INSERT INTO watermark_free_config (id, watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token, fallback_on_failure) + VALUES (1, ?, ?, ?, ?, ?) + """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token, fallback_on_failure)) # Ensure cache_config has a row cursor = await db.execute("SELECT COUNT(*) FROM cache_config") @@ -251,6 +253,7 @@ class Database: ("parse_method", "TEXT DEFAULT 'third_party'"), ("custom_parse_url", "TEXT"), ("custom_parse_token", "TEXT"), + ("fallback_on_failure", "BOOLEAN DEFAULT 1"), ] for col_name, col_type in columns_to_add: @@ -406,6 +409,7 @@ class Database: parse_method TEXT DEFAULT 'third_party', custom_parse_url TEXT, custom_parse_token TEXT, + fallback_on_failure BOOLEAN DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) @@ -1048,10 +1052,11 @@ class Database: return WatermarkFreeConfig(watermark_free_enabled=False, parse_method="third_party") async def update_watermark_free_config(self, enabled: bool, parse_method: str = None, - custom_parse_url: str = None, custom_parse_token: str = None): + custom_parse_url: str = None, custom_parse_token: str = None, + fallback_on_failure: bool = None): """Update watermark-free configuration""" async with aiosqlite.connect(self.db_path) as db: - if parse_method is None and custom_parse_url is None and custom_parse_token is None: + if parse_method is None and custom_parse_url is None and custom_parse_token is None and fallback_on_failure is None: # Only update enabled status await db.execute(""" UPDATE watermark_free_config @@ -1063,9 +1068,10 @@ class Database: await db.execute(""" UPDATE watermark_free_config SET watermark_free_enabled = ?, parse_method = ?, custom_parse_url = ?, - custom_parse_token = ?, updated_at = CURRENT_TIMESTAMP + custom_parse_token = ?, fallback_on_failure = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1 - """, (enabled, parse_method or "third_party", custom_parse_url, custom_parse_token)) + """, (enabled, parse_method or "third_party", custom_parse_url, custom_parse_token, + fallback_on_failure if fallback_on_failure is not None else True)) await db.commit() # Cache config operations diff --git a/src/core/models.py b/src/core/models.py index 9442e33..113c05e 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -110,6 +110,7 @@ class WatermarkFreeConfig(BaseModel): parse_method: str # Read from database, initialized from setting.toml on first startup custom_parse_url: Optional[str] = None # Read from database, initialized from setting.toml on first startup custom_parse_token: Optional[str] = None # Read from database, initialized from setting.toml on first startup + fallback_on_failure: bool = True # Auto fallback to watermarked video on failure, default True created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index 1cce893..6218f5f 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -986,11 +986,15 @@ class GenerationHandler: watermark_free_config = await self.db.get_watermark_free_config() watermark_free_enabled = watermark_free_config.watermark_free_enabled + # Initialize variables + local_url = None + watermark_free_failed = False + if watermark_free_enabled: # Watermark-free mode: post video and get watermark-free URL - debug_logger.log_info(f"Entering watermark-free mode for task {task_id}") + debug_logger.log_info(f"[Watermark-Free] Entering watermark-free mode for task {task_id}") generation_id = item.get("id") - debug_logger.log_info(f"Generation ID: {generation_id}") + debug_logger.log_info(f"[Watermark-Free] Generation ID: {generation_id}") if not generation_id: raise Exception("Generation ID not found in video draft") @@ -1090,60 +1094,80 @@ class GenerationHandler: ) except Exception as publish_error: - # Fallback to normal mode if publish fails + # Watermark-free mode failed + watermark_free_failed = True + import traceback + error_traceback = traceback.format_exc() debug_logger.log_error( - error_message=f"Watermark-free mode failed: {str(publish_error)}", + error_message=f"[Watermark-Free] ❌ FAILED - Error: {str(publish_error)}", status_code=500, - response_text=str(publish_error) + response_text=f"{str(publish_error)}\n\nTraceback:\n{error_traceback}" ) - if stream: - yield self._format_stream_chunk( - reasoning_content=f"Warning: Failed to get watermark-free version - {str(publish_error)}\nFalling back to normal video...\n" - ) - # Use downloadable_url instead of url - url = item.get("downloadable_url") or item.get("url") - if not url: - raise Exception("Video URL not found") - if config.cache_enabled: - try: - cached_filename = await self.file_cache.download_and_cache(url, "video", token_id=token_id) - local_url = f"{self._get_base_url()}/tmp/{cached_filename}" - except Exception as cache_error: - local_url = url - else: - local_url = url - else: - # Normal mode: use downloadable_url instead of url - url = item.get("downloadable_url") or item.get("url") - if url: - # Cache video file (if cache enabled) - if config.cache_enabled: + + # Check if fallback is enabled + if watermark_config.fallback_on_failure: + debug_logger.log_info(f"[Watermark-Free] Fallback enabled, falling back to normal mode (original URL)") if stream: yield self._format_stream_chunk( - reasoning_content="**Video Generation Completed**\n\nVideo generation successful. Now caching the video file...\n" + reasoning_content=f"⚠️ Warning: Failed to get watermark-free version - {str(publish_error)}\nFalling back to normal video...\n" ) + else: + # Fallback disabled, mark task as failed + debug_logger.log_error( + error_message=f"[Watermark-Free] Fallback disabled, marking task as failed", + status_code=500, + response_text=str(publish_error) + ) + if stream: + yield self._format_stream_chunk( + reasoning_content=f"❌ Error: Failed to get watermark-free version - {str(publish_error)}\nFallback is disabled. Task marked as failed.\n" + ) + # Re-raise the exception to mark task as failed + raise - try: - cached_filename = await self.file_cache.download_and_cache(url, "video", token_id=token_id) - local_url = f"{self._get_base_url()}/tmp/{cached_filename}" - if stream: + # If watermark-free mode is disabled or failed (with fallback enabled), use normal mode + if not watermark_free_enabled or (watermark_free_failed and watermark_config.fallback_on_failure): + # Normal mode: use downloadable_url instead of url + url = item.get("downloadable_url") or item.get("url") + if not url: + raise Exception("Video URL not found in draft") + + debug_logger.log_info(f"Using original URL from draft: {url[:100]}...") + + if config.cache_enabled: + # Show appropriate message based on mode + if stream and not watermark_free_failed: + # Normal mode (watermark-free disabled) + yield self._format_stream_chunk( + reasoning_content="**Video Generation Completed**\n\nVideo generation successful. Now caching the video file...\n" + ) + + try: + cached_filename = await self.file_cache.download_and_cache(url, "video", token_id=token_id) + local_url = f"{self._get_base_url()}/tmp/{cached_filename}" + if stream: + if watermark_free_failed: + yield self._format_stream_chunk( + reasoning_content="Video file cached successfully (fallback mode). Preparing final response...\n" + ) + else: yield self._format_stream_chunk( reasoning_content="Video file cached successfully. Preparing final response...\n" ) - except Exception as cache_error: - # Fallback to original URL if caching fails - local_url = url - if stream: - yield self._format_stream_chunk( - reasoning_content=f"Warning: Failed to cache file - {str(cache_error)}\nUsing original URL instead...\n" - ) - else: - # Cache disabled: use original URL directly + except Exception as cache_error: local_url = url if stream: yield self._format_stream_chunk( - reasoning_content="**Video Generation Completed**\n\nCache is disabled. Using original URL directly...\n" + reasoning_content=f"Warning: Failed to cache file - {str(cache_error)}\nUsing original URL instead...\n" ) + else: + # Cache disabled + local_url = url + if stream and not watermark_free_failed: + # Normal mode (watermark-free disabled) + yield self._format_stream_chunk( + reasoning_content="**Video Generation Completed**\n\nCache is disabled. Using original URL directly...\n" + ) # Task completed await self.db.update_task( diff --git a/static/manage.html b/static/manage.html index 9996bd4..ba750a2 100644 --- a/static/manage.html +++ b/static/manage.html @@ -223,6 +223,15 @@ 清理禁用 +