From 5570fa35a68747554353efc6668d72a54fcf5fed Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Tue, 27 Jan 2026 00:22:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=97=B6=E9=97=B4=E8=AE=A1=E7=AE=97=E5=8F=8A?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=8A=B6=E6=80=81=E6=98=BE=E7=A4=BA=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin.py | 21 ++++++++++++++++++++- src/core/database.py | 35 +++++++++++++++++++++++++++++++---- static/manage.html | 4 ++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/api/admin.py b/src/api/admin.py index c659729..5fd6809 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -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": "用户手动取消任务"}', diff --git a/src/core/database.py b/src/core/database.py index 1a3f298..7564e17 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -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 ) """) @@ -1199,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() diff --git a/static/manage.html b/static/manage.html index cb9b7f5..462f93e 100644 --- a/static/manage.html +++ b/static/manage.html @@ -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='-';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=`
${progress.toFixed(0)}%
${taskStatusText}
`}else if(isFailed){progressHtml='失败'}else if(isCompleted&&l.status_code===200){progressHtml='已完成'}let actionHtml='';if(isProcessing&&l.task_id){actionHtml='
'}return `${l.operation}${l.token_email||'未知'}${statusText}${progressHtml}${l.duration===-1?'处理中':l.duration.toFixed(2)+'秒'}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}${actionHtml}`}).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='-';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=`
${progress.toFixed(0)}%
${taskStatusText}
`}else if(isFailed){progressHtml='失败'}else if(isCompleted&&l.status_code===200){progressHtml='已完成'}let actionHtml='';if(isProcessing&&l.task_id){actionHtml='
'}return `${l.operation}${l.token_email||'未知'}${statusText}${progressHtml}${l.duration===-1?'处理中':l.duration.toFixed(2)+'秒'}${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}${actionHtml}`}).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+=`

生成进度

任务正在生成中...

${log.task_status?`

状态: ${log.task_status}

`:''}
`}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+=`

生成结果

文件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 if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}catch(e){if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}}detailHtml+=`

基本信息

操作: ${log.operation}
状态码: ${log.status_code===-1?'生成中':log.status_code}
耗时: ${log.duration===-1?'生成中':log.duration.toFixed(2)+'秒'}
时间: ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}
`;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+=`

生成进度

任务正在生成中...

${log.task_status?`

状态: ${log.task_status}

`:''}
`}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+=`

生成结果

文件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 if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}catch(e){if(log.response_body&&log.response_body!=='{}'){detailHtml+=`

错误信息

${log.response_body}
`}}}detailHtml+=`

基本信息

操作: ${log.operation}
状态码: ${log.status_code===-1?((log.progress||0)>0?'生成中':'排队中'):log.status_code}
耗时: ${log.duration===-1?'生成中':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')}}, 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')}},