mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-04 02:04:42 +08:00
feat: 新增去水印失败自动回退配置、优化批量删除逻辑及错误处理机制
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -223,6 +223,15 @@
|
||||
</svg>
|
||||
<span>清理禁用</span>
|
||||
</button>
|
||||
<button onclick="batchDeleteSelected()" class="batch-dropdown-item red" title="删除所有选中的Token(不管是否禁用)">
|
||||
<svg class="batch-dropdown-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
</svg>
|
||||
<span>删除选中</span>
|
||||
</button>
|
||||
<button onclick="openBatchProxyModal()" class="batch-dropdown-item blue" title="批量修改选中Token的代理">
|
||||
<svg class="batch-dropdown-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -468,6 +477,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 回退开关 -->
|
||||
<div>
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="cfgFallbackOnFailure" class="h-4 w-4 rounded border-input" checked>
|
||||
<span class="text-sm font-medium">去水印失败后自动回退</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-2">开启后,去水印失败时自动回退到带水印视频;关闭后,去水印失败将标记任务为失败状态</p>
|
||||
</div>
|
||||
|
||||
<!-- 自定义解析配置 -->
|
||||
<div id="customParseOptions" style="display: none;" class="space-y-4">
|
||||
<div>
|
||||
@@ -979,7 +997,8 @@
|
||||
exportTokens=()=>{if(allTokens.length===0){showToast('没有Token可导出','error');return}const exportData=allTokens.map(t=>({email:t.email,access_token:t.token,session_token:t.st||null,refresh_token:t.rt||null,client_id:t.client_id||null,proxy_url:t.proxy_url||null,remark:t.remark||null,is_active:t.is_active,image_enabled:t.image_enabled!==false,video_enabled:t.video_enabled!==false,image_concurrency:t.image_concurrency||(-1),video_concurrency:t.video_concurrency||(-1)}));const dataStr=JSON.stringify(exportData,null,2);const dataBlob=new Blob([dataStr],{type:'application/json'});const url=URL.createObjectURL(dataBlob);const link=document.createElement('a');link.href=url;link.download=`tokens_${new Date().toISOString().split('T')[0]}.json`;document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);showToast(`已导出 ${allTokens.length} 个Token`,'success')},
|
||||
batchTestUpdate=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要测试的Token','info');return}if(!confirm(`⚠️ 警告\n\n此操作将请求上游获取选中的 ${selectedTokenIds.size} 个Token的状态信息,可能需要较长时间。\n\n确定要继续吗?`)){return}showToast('正在测试更新选中的Token...','info');try{const r=await apiRequest('/api/tokens/batch/test-update',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('测试更新失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('测试更新失败: '+e.message,'error')}},
|
||||
batchEnableAll=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要启用的Token','info');return}if(!confirm(`确定要启用选中的 ${selectedTokenIds.size} 个Token吗?\n\n此操作将重置这些Token的错误计数。`)){return}showToast('正在批量启用Token...','info');try{const r=await apiRequest('/api/tokens/batch/enable-all',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('批量启用失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('批量启用失败: '+e.message,'error')}},
|
||||
batchDeleteDisabled=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要删除的Token','info');return}if(!confirm(`⚠️ 第一次确认\n\n即将删除选中的 ${selectedTokenIds.size} 个Token。\n\n此操作不可恢复!确定要继续吗?`)){return}if(!confirm(`⚠️ 第二次确认\n\n你真的确定要删除这 ${selectedTokenIds.size} 个Token吗?\n\n删除后无法恢复!`)){return}if(!confirm(`⚠️ 最后确认\n\n这是最后一次确认!\n\n删除 ${selectedTokenIds.size} 个Token后将无法恢复。\n\n确定要执行删除操作吗?`)){return}showToast('正在删除选中的Token...','info');try{const r=await apiRequest('/api/tokens/batch/delete-disabled',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('删除失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('删除失败: '+e.message,'error')}},
|
||||
batchDeleteDisabled=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要删除的Token','info');return}const disabledCount=Array.from(selectedTokenIds).filter(id=>{const token=allTokens.find(t=>t.id===id);return token&&!token.is_active}).length;if(disabledCount===0){showToast('选中的Token中没有禁用的Token','info');return}if(!confirm(`⚠️ 警告\n\n选中了 ${selectedTokenIds.size} 个Token,其中 ${disabledCount} 个是禁用的。\n\n即将删除这 ${disabledCount} 个禁用Token。\n\n此操作不可恢复!确定要继续吗?`)){return}showToast('正在删除选中的禁用Token...','info');try{const r=await apiRequest('/api/tokens/batch/delete-disabled',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('删除失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('删除失败: '+e.message,'error')}},
|
||||
batchDeleteSelected=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要删除的Token','info');return}if(!confirm(`⚠️ 第一次确认\n\n即将删除选中的 ${selectedTokenIds.size} 个Token(包括正常和禁用的)。\n\n此操作不可恢复!确定要继续吗?`)){return}if(!confirm(`⚠️ 第二次确认\n\n你真的确定要删除这 ${selectedTokenIds.size} 个Token吗?\n\n删除后无法恢复!`)){return}if(!confirm(`⚠️ 最后确认\n\n这是最后一次确认!\n\n删除 ${selectedTokenIds.size} 个Token后将无法恢复。\n\n确定要执行删除操作吗?`)){return}showToast('正在删除选中的Token...','info');try{const r=await apiRequest('/api/tokens/batch/delete-selected',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('删除失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('删除失败: '+e.message,'error')}},
|
||||
toggleSelectAll=()=>{const checkbox=$('selectAllCheckbox');const checkboxes=document.querySelectorAll('.token-checkbox');if(checkbox.checked){checkboxes.forEach(cb=>{cb.checked=true;const tokenId=parseInt(cb.getAttribute('data-token-id'));selectedTokenIds.add(tokenId)})}else{checkboxes.forEach(cb=>{cb.checked=false});selectedTokenIds.clear()}},
|
||||
toggleTokenSelection=(tokenId,checked)=>{if(checked){selectedTokenIds.add(tokenId)}else{selectedTokenIds.delete(tokenId)}const allCheckboxes=document.querySelectorAll('.token-checkbox');const allChecked=Array.from(allCheckboxes).every(cb=>cb.checked);$('selectAllCheckbox').checked=allChecked},
|
||||
batchDisableSelected=async()=>{if(selectedTokenIds.size===0){showToast('请先选择要禁用的Token','info');return}if(!confirm(`确定要禁用选中的 ${selectedTokenIds.size} 个Token吗?`)){return}showToast('正在批量禁用Token...','info');try{const r=await apiRequest('/api/tokens/batch/disable-selected',{method:'POST',body:JSON.stringify({token_ids:Array.from(selectedTokenIds)})});if(!r)return;const d=await r.json();if(d.success){selectedTokenIds.clear();await refreshTokens();showToast(d.message,'success')}else{showToast('批量禁用失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('批量禁用失败: '+e.message,'error')}},
|
||||
@@ -995,8 +1014,8 @@
|
||||
setProxyStatus=(msg,type='muted')=>{const el=$('proxyStatusMessage');if(!el)return;if(!msg){el.textContent='';el.classList.add('hidden');return}el.textContent=msg;el.classList.remove('hidden','text-muted-foreground','text-green-600','text-red-600');if(type==='success')el.classList.add('text-green-600');else if(type==='error')el.classList.add('text-red-600');else el.classList.add('text-muted-foreground')},
|
||||
testProxyConfig=async()=>{const enabled=$('cfgProxyEnabled').checked;const url=$('cfgProxyUrl').value.trim();const testUrl=$('cfgProxyTestUrl').value.trim()||'https://sora.chatgpt.com';if(!enabled||!url){setProxyStatus('代理未启用或地址为空','error');return}try{setProxyStatus('正在测试代理连接...','muted');const r=await apiRequest('/api/proxy/test',{method:'POST',body:JSON.stringify({test_url:testUrl})});if(!r)return;const d=await r.json();if(d.success){setProxyStatus(`✓ ${d.message||'代理可用'} - 测试域名: ${d.test_url||testUrl}`,'success')}else{setProxyStatus(`✗ ${d.message||'代理不可用'} - 测试域名: ${d.test_url||testUrl}`,'error')}}catch(e){setProxyStatus('代理测试失败: '+e.message,'error')}},
|
||||
saveProxyConfig=async()=>{try{const r=await apiRequest('/api/proxy/config',{method:'POST',body:JSON.stringify({proxy_enabled:$('cfgProxyEnabled').checked,proxy_url:$('cfgProxyUrl').value.trim()})});if(!r)return;const d=await r.json();d.success?showToast('代理配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
loadWatermarkFreeConfig=async()=>{try{const r=await apiRequest('/api/watermark-free/config');if(!r)return;const d=await r.json();$('cfgWatermarkFreeEnabled').checked=d.watermark_free_enabled||false;$('cfgParseMethod').value=d.parse_method||'third_party';$('cfgCustomParseUrl').value=d.custom_parse_url||'';$('cfgCustomParseToken').value=d.custom_parse_token||'';toggleWatermarkFreeOptions();toggleCustomParseOptions()}catch(e){console.error('加载无水印模式配置失败:',e)}},
|
||||
saveWatermarkFreeConfig=async()=>{try{const enabled=$('cfgWatermarkFreeEnabled').checked,parseMethod=$('cfgParseMethod').value,customUrl=$('cfgCustomParseUrl').value.trim(),customToken=$('cfgCustomParseToken').value.trim();if(enabled&&parseMethod==='custom'){if(!customUrl)return showToast('请输入解析服务器地址','error');if(!customToken)return showToast('请输入访问密钥','error')}const r=await apiRequest('/api/watermark-free/config',{method:'POST',body:JSON.stringify({watermark_free_enabled:enabled,parse_method:parseMethod,custom_parse_url:customUrl||null,custom_parse_token:customToken||null})});if(!r)return;const d=await r.json();d.success?showToast('无水印模式配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
loadWatermarkFreeConfig=async()=>{try{const r=await apiRequest('/api/watermark-free/config');if(!r)return;const d=await r.json();$('cfgWatermarkFreeEnabled').checked=d.watermark_free_enabled||false;$('cfgParseMethod').value=d.parse_method||'third_party';$('cfgCustomParseUrl').value=d.custom_parse_url||'';$('cfgCustomParseToken').value=d.custom_parse_token||'';$('cfgFallbackOnFailure').checked=d.fallback_on_failure!==false;toggleWatermarkFreeOptions();toggleCustomParseOptions()}catch(e){console.error('加载无水印模式配置失败:',e)}},
|
||||
saveWatermarkFreeConfig=async()=>{try{const enabled=$('cfgWatermarkFreeEnabled').checked,parseMethod=$('cfgParseMethod').value,customUrl=$('cfgCustomParseUrl').value.trim(),customToken=$('cfgCustomParseToken').value.trim(),fallbackOnFailure=$('cfgFallbackOnFailure').checked;if(enabled&&parseMethod==='custom'){if(!customUrl)return showToast('请输入解析服务器地址','error');if(!customToken)return showToast('请输入访问密钥','error')}const r=await apiRequest('/api/watermark-free/config',{method:'POST',body:JSON.stringify({watermark_free_enabled:enabled,parse_method:parseMethod,custom_parse_url:customUrl||null,custom_parse_token:customToken||null,fallback_on_failure:fallbackOnFailure})});if(!r)return;const d=await r.json();d.success?showToast('无水印模式配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
toggleWatermarkFreeOptions=()=>{const enabled=$('cfgWatermarkFreeEnabled').checked;$('watermarkFreeOptions').style.display=enabled?'block':'none'},
|
||||
toggleCustomParseOptions=()=>{const method=$('cfgParseMethod').value;$('customParseOptions').style.display=method==='custom'?'block':'none'},
|
||||
toggleCacheOptions=()=>{const enabled=$('cfgCacheEnabled').checked;$('cacheOptions').style.display=enabled?'block':'none'},
|
||||
|
||||
Reference in New Issue
Block a user