diff --git a/server/tasks.py b/server/tasks.py index 4763750..8160c84 100644 --- a/server/tasks.py +++ b/server/tasks.py @@ -75,6 +75,22 @@ class TaskManager: self._tasks: Dict[str, TaskRecord] = {} self._lock = threading.Lock() + def cleanup_old_tasks(self, max_age_seconds: int = 3600) -> int: + """清理超过指定时间的已完成任务""" + now = time.time() + with self._lock: + to_remove = [] + for task_id, rec in self._tasks.items(): + if rec.status in {"succeeded", "failed", "canceled"}: + age = now - rec.updated_at + if age > max_age_seconds: + to_remove.append(task_id) + + for task_id in to_remove: + del self._tasks[task_id] + + return len(to_remove) + # ---- public APIs ---- def create_chat_task( self, @@ -274,6 +290,27 @@ task_manager = TaskManager() tasks_bp = Blueprint("tasks", __name__) +def start_task_cleanup_scheduler(): + """启动任务清理定时器""" + def cleanup_loop(): + while True: + try: + count = task_manager.cleanup_old_tasks(3600) + if count > 0: + debug_log(f"[Task] 清理了 {count} 个旧任务") + except Exception as e: + debug_log(f"[Task] 清理任务失败: {e}") + time.sleep(600) # 每 10 分钟 + + thread = threading.Thread(target=cleanup_loop, daemon=True, name="TaskCleanup") + thread.start() + debug_log("[Task] 任务清理定时器已启动") + + +# 启动清理定时器 +start_task_cleanup_scheduler() + + @tasks_bp.route("/api/tasks", methods=["GET"]) @api_login_required def list_tasks_api(): diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index 51040c3..df6b7ed 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -609,12 +609,33 @@ export const taskPollingMethods = { }, handleTaskError(data: any) { + const errorMessage = data.message || '未知错误'; + const errorType = data.error_type || 'unknown'; + + let title = '任务执行失败'; + let message = errorMessage; + + // 根据错误类型提供友好提示 + if (errorType === 'api_error') { + title = 'API 调用失败'; + message = `模型服务异常:${errorMessage}`; + } else if (errorType === 'timeout') { + title = '任务超时'; + message = '任务执行时间过长,已自动停止'; + } else if (errorType === 'quota_exceeded') { + title = '配额不足'; + message = '您的使用配额已用尽'; + } + console.error('[TaskPolling] 任务错误:', data.message); this.uiPushToast({ - title: '任务执行失败', - message: data.message || '未知错误', - type: 'error' + title, + message, + type: 'error', + duration: 8000 }); + + // 清理状态 this.streamingMessage = false; this.taskInProgress = false; this.stopRequested = false; diff --git a/static/src/stores/task.ts b/static/src/stores/task.ts index 350b34d..812eeac 100644 --- a/static/src/stores/task.ts +++ b/static/src/stores/task.ts @@ -10,6 +10,7 @@ export const useTaskStore = defineStore('task', { taskStatus: 'idle' as 'idle' | 'running' | 'succeeded' | 'failed' | 'canceled', isPolling: false, pollingError: null as string | null, + pollingErrorCount: 0, // 新增错误计数 taskCreatedAt: null as number | null, taskUpdatedAt: null as number | null, }), @@ -74,11 +75,12 @@ export const useTaskStore = defineStore('task', { try { const response = await fetch( - `/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}` + `/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}`, + { signal: AbortSignal.timeout(5000) } // 5秒超时 ); if (!response.ok) { - throw new Error('轮询任务失败'); + throw new Error(`HTTP ${response.status}`); } const result = await response.json(); @@ -113,14 +115,27 @@ export const useTaskStore = defineStore('task', { this.stopPolling(); } + // 重置错误计数 this.pollingError = null; + this.pollingErrorCount = 0; } catch (error) { - console.error('[Task] 轮询失败:', error); + this.pollingErrorCount++; this.pollingError = error.message; - // 连续失败3次后停止轮询 - if (this.pollingError) { - // 可以在这里添加重试逻辑 + console.error('[Task] 轮询失败:', error); + + // 连续失败 5 次后停止 + if (this.pollingErrorCount >= 5) { + this.stopPolling(); + // 通知用户 + if ((window as any).__vueApp?.uiPushToast) { + (window as any).__vueApp.uiPushToast({ + title: '轮询失败', + message: '请刷新页面重试', + type: 'error', + duration: 10000 + }); + } } } },