Compare commits

...

2 Commits

Author SHA1 Message Date
TheSmallHanCat
5570fa35a6 fix: 修复任务取消时间计算及日志状态显示逻辑 2026-01-27 00:22:10 +08:00
TheSmallHanCat
06c2bea806 fix: 修复管理配置更新缺失字段及日志状态更新机制
- 修复update_admin_config方法未更新task_retry_enabled、task_max_retries、auto_disable_on_401字段的问题
- 新增finally块确保请求日志在异常情况下也能正确更新状态,避免卡在status_code=-1
2026-01-26 20:12:20 +08:00
4 changed files with 84 additions and 9 deletions

View File

@@ -1391,7 +1391,26 @@ async def cancel_task(task_id: str, token: str = Depends(verify_admin_token)):
for log in logs:
if log.get("task_id") == task_id and log.get("status_code") == -1:
import time
duration = time.time() - (log.get("created_at").timestamp() if log.get("created_at") else time.time())
from datetime import datetime
# Calculate duration
created_at = log.get("created_at")
if created_at:
# If created_at is a string, parse it
if isinstance(created_at, str):
try:
created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
duration = time.time() - created_at.timestamp()
except:
duration = 0
# If it's already a datetime object
elif isinstance(created_at, datetime):
duration = time.time() - created_at.timestamp()
else:
duration = 0
else:
duration = 0
await db.update_request_log(
log.get("id"),
response_body='{"error": "用户手动取消任务"}',

View File

@@ -180,6 +180,30 @@ class Database:
VALUES (1, ?)
""", (at_auto_refresh_enabled,))
# Ensure call_logic_config has a row
cursor = await db.execute("SELECT COUNT(*) FROM call_logic_config")
count = await cursor.fetchone()
if count[0] == 0:
# Get call logic config from config_dict if provided, otherwise use defaults
call_mode = "default"
polling_mode_enabled = False
if config_dict:
call_logic_config = config_dict.get("call_logic", {})
call_mode = call_logic_config.get("call_mode", "default")
# Normalize call_mode
if call_mode not in ("default", "polling"):
# Check legacy polling_mode_enabled field
polling_mode_enabled = call_logic_config.get("polling_mode_enabled", False)
call_mode = "polling" if polling_mode_enabled else "default"
else:
polling_mode_enabled = call_mode == "polling"
await db.execute("""
INSERT INTO call_logic_config (id, call_mode, polling_mode_enabled)
VALUES (1, ?, ?)
""", (call_mode, polling_mode_enabled))
async def check_and_migrate_db(self, config_dict: dict = None):
"""Check database integrity and perform migrations if needed
@@ -386,6 +410,9 @@ class Database:
admin_password TEXT DEFAULT 'admin',
api_key TEXT DEFAULT 'han1234',
error_ban_threshold INTEGER DEFAULT 3,
task_retry_enabled BOOLEAN DEFAULT 1,
task_max_retries INTEGER DEFAULT 3,
auto_disable_on_401 BOOLEAN DEFAULT 1,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
@@ -1010,9 +1037,12 @@ class Database:
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
UPDATE admin_config
SET admin_username = ?, admin_password = ?, api_key = ?, error_ban_threshold = ?, updated_at = CURRENT_TIMESTAMP
SET admin_username = ?, admin_password = ?, api_key = ?, error_ban_threshold = ?,
task_retry_enabled = ?, task_max_retries = ?, auto_disable_on_401 = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = 1
""", (config.admin_username, config.admin_password, config.api_key, config.error_ban_threshold))
""", (config.admin_username, config.admin_password, config.api_key, config.error_ban_threshold,
config.task_retry_enabled, config.task_max_retries, config.auto_disable_on_401))
await db.commit()
# Proxy config operations
@@ -1196,10 +1226,10 @@ class Database:
normalized = "polling" if call_mode == "polling" else "default"
polling_mode_enabled = normalized == "polling"
async with aiosqlite.connect(self.db_path) as db:
# Use INSERT OR REPLACE to ensure the row exists
await db.execute("""
UPDATE call_logic_config
SET polling_mode_enabled = ?, call_mode = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = 1
""", (polling_mode_enabled, normalized))
INSERT OR REPLACE INTO call_logic_config (id, call_mode, polling_mode_enabled, updated_at)
VALUES (1, ?, ?, CURRENT_TIMESTAMP)
""", (normalized, polling_mode_enabled))
await db.commit()

View File

@@ -521,6 +521,7 @@ class GenerationHandler:
task_id = None
is_first_chunk = True # Track if this is the first chunk
log_id = None # Initialize log_id
log_updated = False # Track if log has been updated
try:
# Create initial log entry BEFORE submitting task to upstream
@@ -680,6 +681,7 @@ class GenerationHandler:
status_code=200,
duration=duration
)
log_updated = True # Mark log as updated
except Exception as e:
# Release lock for image generation on error
@@ -727,6 +729,7 @@ class GenerationHandler:
status_code=status_code,
duration=duration
)
log_updated = True # Mark log as updated
else:
# Generic error
await self.db.update_request_log(
@@ -735,12 +738,35 @@ class GenerationHandler:
status_code=500,
duration=duration
)
log_updated = True # Mark log as updated
# Wrap exception with token_id information
if token_obj:
raise GenerationError(str(e), token_id=token_obj.id)
else:
raise e
finally:
# Ensure log is updated even if exception handling fails
# This prevents logs from being stuck at status_code = -1
if log_id and not log_updated:
try:
# Log was not updated in try or except blocks, update it now
duration = time.time() - start_time
await self.db.update_request_log(
log_id,
response_body=json.dumps({"error": "Task failed or interrupted during processing"}),
status_code=500,
duration=duration
)
debug_logger.log_info(f"Updated stuck log entry {log_id} from status -1 to 500 in finally block")
except Exception as finally_error:
# Don't let finally block errors break the flow
debug_logger.log_error(
error_message=f"Failed to update log in finally block: {str(finally_error)}",
status_code=500,
response_text=str(finally_error)
)
async def handle_generation_with_retry(self, model: str, prompt: str,
image: Optional[str] = None,
video: Optional[str] = None,

View File

@@ -1086,9 +1086,9 @@
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();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>{const isProcessing=l.status_code===-1&&l.task_status==='processing';const isFailed=l.task_status==='failed';const isCompleted=l.task_status==='completed';const statusText=isProcessing?'处理中':l.status_code;const statusClass=isProcessing?'bg-blue-50 text-blue-700':l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700';let progressHtml='<span class="text-xs text-muted-foreground">-</span>';if(isProcessing&&l.task_status){const taskStatusMap={processing:'生成中',completed:'已完成',failed:'失败'};const taskStatusText=taskStatusMap[l.task_status]||l.task_status;const progress=l.progress||0;progressHtml=`<div class="flex flex-col gap-1"><div class="flex items-center gap-2"><div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden"><div class="h-full bg-blue-500 transition-all" style="width:${progress}%"></div></div><span class="text-xs text-blue-600">${progress.toFixed(0)}%</span></div><span class="text-xs text-muted-foreground">${taskStatusText}</span></div>`}else if(isFailed){progressHtml='<span class="text-xs text-red-600">失败</span>'}else if(isCompleted&&l.status_code===200){progressHtml='<span class="text-xs text-green-600">已完成</span>'}let actionHtml='<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>';if(isProcessing&&l.task_id){actionHtml='<div class="flex gap-1"><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><button onclick="cancelTask(\''+l.task_id+'\')" class="inline-flex items-center justify-center rounded-md hover:bg-red-50 hover:text-red-700 h-7 px-2 text-xs">终止</button></div>'}return `<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 ${statusClass}">${statusText}</span></td><td class="py-2.5 px-3">${progressHtml}</td><td class="py-2.5 px-3">${l.duration===-1?'处理中':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">${actionHtml}</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=>{const isProcessing=l.status_code===-1;const isFailed=l.task_status==='failed';const isCompleted=l.task_status==='completed';const progress=l.progress||0;const statusText=isProcessing?(progress>0?'生成中':'排队中'):l.status_code;const statusClass=isProcessing?'bg-blue-50 text-blue-700':l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700';let progressHtml='<span class="text-xs text-muted-foreground">-</span>';if(isProcessing&&l.task_status){const taskStatusMap={processing:'生成中',completed:'已完成',failed:'失败'};const taskStatusText=taskStatusMap[l.task_status]||l.task_status;const progress=l.progress||0;progressHtml=`<div class="flex flex-col gap-1"><div class="flex items-center gap-2"><div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden"><div class="h-full bg-blue-500 transition-all" style="width:${progress}%"></div></div><span class="text-xs text-blue-600">${progress.toFixed(0)}%</span></div><span class="text-xs text-muted-foreground">${taskStatusText}</span></div>`}else if(isFailed){progressHtml='<span class="text-xs text-red-600">失败</span>'}else if(isCompleted&&l.status_code===200){progressHtml='<span class="text-xs text-green-600">已完成</span>'}let actionHtml='<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>';if(isProcessing&&l.task_id){actionHtml='<div class="flex gap-1"><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><button onclick="cancelTask(\''+l.task_id+'\')" class="inline-flex items-center justify-center rounded-md hover:bg-red-50 hover:text-red-700 h-7 px-2 text-xs">终止</button></div>'}return `<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 ${statusClass}">${statusText}</span></td><td class="py-2.5 px-3">${progressHtml}</td><td class="py-2.5 px-3">${l.duration===-1?'处理中':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">${actionHtml}</td></tr>`}).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===-1){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-blue-600">生成进度</h4><div class="rounded-md border border-blue-200 p-3 bg-blue-50"><p class="text-sm text-blue-700">任务正在生成中...</p>${log.task_status?`<p class="text-xs text-blue-600 mt-1">状态: ${log.task_status}</p>`:''}</div></div>`}else 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 if(log.response_body&&log.response_body!=='{}'){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){if(log.response_body&&log.response_body!=='{}'){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===-1?'bg-blue-50 text-blue-700':log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code===-1?'生成中':log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration===-1?'生成中':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')},
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===-1){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-blue-600">生成进度</h4><div class="rounded-md border border-blue-200 p-3 bg-blue-50"><p class="text-sm text-blue-700">任务正在生成中...</p>${log.task_status?`<p class="text-xs text-blue-600 mt-1">状态: ${log.task_status}</p>`:''}</div></div>`}else 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 if(log.response_body&&log.response_body!=='{}'){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){if(log.response_body&&log.response_body!=='{}'){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===-1?'bg-blue-50 text-blue-700':log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code===-1?((log.progress||0)>0?'生成中':'排队中'):log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration===-1?'生成中':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')}},
cancelTask=async(taskId)=>{if(!confirm('确定要终止这个任务吗?'))return;try{const r=await apiRequest(`/api/tasks/${taskId}/cancel`,{method:'POST'});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')}},