From 73150021315b30f06f31cdac8aa6c2ebecfb8393 Mon Sep 17 00:00:00 2001 From: cnlimiter Date: Mon, 16 Mar 2026 02:43:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=B7=BB=E5=8A=A0=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=8F=AF=E8=A7=81=E6=80=A7=E9=87=8D=E8=BF=9E=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E5=92=8CWebSocket=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端app.js添加页面可见性监听和WebSocket重连逻辑 - 后端registration.py集成TaskManager支持WebSocket推送 - 更新依赖添加websockets库支持 - 优化批量任务状态管理和日志推送 --- pyproject.toml | 1 + requirements.txt | Bin 452 -> 472 bytes src/web/routes/registration.py | 47 +++++++++++-- static/js/app.js | 118 ++++++++++++++++++++++++++++++++- webui.py | 1 + 5 files changed, 158 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5ec14c9..27adf61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pydantic-settings>=2.0.0", "sqlalchemy>=2.0.0", "aiosqlite>=0.19.0", + "websockets>=16.0", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index b2e6f4a96c4a147c4ce171bcd2e4848d5cf81d04..a0006cbd96dabecec3c489bc937fba17254097f5 100644 GIT binary patch delta 31 kcmX@Ye1my|2%~T`Loq`MLn1>SLkf^A0+O+lg&8*j0E3$c+yDRo delta 11 Scmcb?e1v&}2;*c8#ti@&Fay*8 diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index ac015a7..6484e41 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -420,6 +420,9 @@ async def run_batch_registration( 使用线程池执行每个注册任务,避免阻塞主事件循环 """ + # 初始化 TaskManager 批量任务(支持 WebSocket 推送) + task_manager.init_batch(batch_id, len(task_uuids)) + batch_tasks[batch_id] = { "total": len(task_uuids), "completed": 0, @@ -430,18 +433,31 @@ async def run_batch_registration( "current_index": 0 } + def add_batch_log(msg: str): + batch_tasks[batch_id]["logs"] = batch_tasks[batch_id].get("logs", []) + batch_tasks[batch_id]["logs"].append(msg) + task_manager.add_batch_log(batch_id, msg) + + def update_batch_status(**kwargs): + for key, value in kwargs.items(): + if key in batch_tasks[batch_id]: + batch_tasks[batch_id][key] = value + task_manager.update_batch_status(batch_id, **kwargs) + try: for i, task_uuid in enumerate(task_uuids): # 检查是否已取消 - if batch_tasks[batch_id]["cancelled"]: + if task_manager.is_batch_cancelled(batch_id) or batch_tasks[batch_id]["cancelled"]: # 取消剩余任务 with get_db() as db: for remaining_uuid in task_uuids[i:]: crud.update_registration_task(db, remaining_uuid, status="cancelled") + add_batch_log(f"[取消] 批量任务已取消") + update_batch_status(finished=True, status="cancelled") logger.info(f"批量任务 {batch_id} 已取消") break - batch_tasks[batch_id]["current_index"] = i + update_batch_status(current_index=i) # 运行单个注册任务(使用线程池) await run_registration_task( @@ -452,22 +468,38 @@ async def run_batch_registration( with get_db() as db: task = crud.get_registration_task(db, task_uuid) if task: - batch_tasks[batch_id]["completed"] += 1 + new_completed = batch_tasks[batch_id]["completed"] + 1 + new_success = batch_tasks[batch_id]["success"] + new_failed = batch_tasks[batch_id]["failed"] + if task.status == "completed": - batch_tasks[batch_id]["success"] += 1 + new_success += 1 + add_batch_log(f"[成功] 第 {new_success} 个账号注册成功") elif task.status == "failed": - batch_tasks[batch_id]["failed"] += 1 + new_failed += 1 + add_batch_log(f"[失败] 第 {new_failed} 个账号注册失败: {task.error_message}") + + update_batch_status( + completed=new_completed, + success=new_success, + failed=new_failed + ) # 如果不是最后一个任务,等待随机间隔 - if i < len(task_uuids) - 1 and not batch_tasks[batch_id]["cancelled"]: + if i < len(task_uuids) - 1 and not task_manager.is_batch_cancelled(batch_id): wait_time = random.randint(interval_min, interval_max) logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后继续下一个任务") await asyncio.sleep(wait_time) - logger.info(f"批量任务 {batch_id} 完成: 成功 {batch_tasks[batch_id]['success']}, 失败 {batch_tasks[batch_id]['failed']}") + if not task_manager.is_batch_cancelled(batch_id): + add_batch_log(f"[完成] 批量任务完成!成功: {batch_tasks[batch_id]['success']}, 失败: {batch_tasks[batch_id]['failed']}") + update_batch_status(finished=True, status="completed") + logger.info(f"批量任务 {batch_id} 完成: 成功 {batch_tasks[batch_id]['success']}, 失败 {batch_tasks[batch_id]['failed']}") except Exception as e: logger.error(f"批量任务 {batch_id} 异常: {e}") + add_batch_log(f"[错误] 批量任务异常: {str(e)}") + update_batch_status(finished=True, status="failed") finally: batch_tasks[batch_id]["finished"] = True @@ -616,6 +648,7 @@ async def cancel_batch(batch_id: str): raise HTTPException(status_code=400, detail="批量任务已完成") batch["cancelled"] = True + task_manager.cancel_batch(batch_id) return {"success": True, "message": "批量任务取消请求已提交"} diff --git a/static/js/app.js b/static/js/app.js index f87a386..f0edef6 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -30,6 +30,8 @@ let batchWebSocket = null; // 批量任务 WebSocket let useWebSocket = true; // 是否使用 WebSocket let wsHeartbeatInterval = null; // 心跳定时器 let batchWsHeartbeatInterval = null; // 批量任务心跳定时器 +let activeTaskUuid = null; // 当前活跃的单任务 UUID(用于页面重新可见时重连) +let activeBatchId = null; // 当前活跃的批量任务 ID(用于页面重新可见时重连) // DOM 元素 const elements = { @@ -78,6 +80,8 @@ document.addEventListener('DOMContentLoaded', () => { loadAvailableServices(); loadRecentAccounts(); startAccountsPolling(); + initVisibilityReconnect(); + restoreActiveTask(); }); // 事件监听 @@ -313,6 +317,9 @@ async function handleSingleRegistration(requestData) { const data = await api.post('/registration/start', requestData); currentTask = data; + activeTaskUuid = data.task_uuid; // 保存用于重连 + // 持久化到 sessionStorage,跨页面导航后可恢复 + sessionStorage.setItem('activeTask', JSON.stringify({ task_uuid: data.task_uuid, mode: 'single' })); addLog('info', `[系统] 任务已创建: ${data.task_uuid}`); showTaskStatus(data); updateTaskStatus('running'); @@ -476,12 +483,15 @@ async function handleBatchRegistration(requestData) { const data = await api.post('/registration/batch', requestData); currentBatch = data; + activeBatchId = data.batch_id; // 保存用于重连 + // 持久化到 sessionStorage,跨页面导航后可恢复 + sessionStorage.setItem('activeTask', JSON.stringify({ batch_id: data.batch_id, mode: 'batch', total: data.count })); addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`); addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`); showBatchStatus(data); - // 开始轮询批量状态 - startBatchPolling(data.batch_id); + // 优先使用 WebSocket + connectBatchWebSocket(data.batch_id); } catch (error) { addLog('error', `[错误] 启动失败: ${error.message}`); @@ -845,6 +855,11 @@ function resetButtons() { // 重置最终状态标志 taskFinalStatus = null; batchFinalStatus = null; + // 清除活跃任务标识 + activeTaskUuid = null; + activeBatchId = null; + // 清除 sessionStorage 持久化状态 + sessionStorage.removeItem('activeTask'); // 断开 WebSocket disconnectWebSocket(); disconnectBatchWebSocket(); @@ -979,6 +994,9 @@ async function handleOutlookBatchRegistration() { } currentBatch = { batch_id: data.batch_id, ...data }; + activeBatchId = data.batch_id; // 保存用于重连 + // 持久化到 sessionStorage,跨页面导航后可恢复 + sessionStorage.setItem('activeTask', JSON.stringify({ batch_id: data.batch_id, mode: isOutlookBatchMode ? 'outlook_batch' : 'batch', total: data.to_register })); addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`); addLog('info', `[系统] 总数: ${data.total}, 跳过已注册: ${data.skipped}, 待注册: ${data.to_register}`); @@ -1177,3 +1195,99 @@ function startOutlookBatchPolling(batchId) { batchPollingInterval.lastLogIndex = 0; } + +// ============== 页面可见性重连机制 ============== + +function initVisibilityReconnect() { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState !== 'visible') return; + + // 页面重新可见时,检查是否需要重连(针对同页面标签切换场景) + const wsDisconnected = !webSocket || webSocket.readyState === WebSocket.CLOSED; + const batchWsDisconnected = !batchWebSocket || batchWebSocket.readyState === WebSocket.CLOSED; + + // 单任务重连 + if (activeTaskUuid && !taskCompleted && wsDisconnected) { + console.log('[重连] 页面重新可见,重连单任务 WebSocket:', activeTaskUuid); + addLog('info', '[系统] 页面重新激活,正在重连任务监控...'); + connectWebSocket(activeTaskUuid); + } + + // 批量任务重连 + if (activeBatchId && !batchCompleted && batchWsDisconnected) { + console.log('[重连] 页面重新可见,重连批量任务 WebSocket:', activeBatchId); + addLog('info', '[系统] 页面重新激活,正在重连批量任务监控...'); + connectBatchWebSocket(activeBatchId); + } + }); +} + +// 页面加载时恢复进行中的任务(处理跨页面导航后回到注册页的情况) +async function restoreActiveTask() { + const saved = sessionStorage.getItem('activeTask'); + if (!saved) return; + + let state; + try { + state = JSON.parse(saved); + } catch { + sessionStorage.removeItem('activeTask'); + return; + } + + const { mode, task_uuid, batch_id, total } = state; + + if (mode === 'single' && task_uuid) { + // 查询任务是否仍在运行 + try { + const data = await api.get(`/registration/tasks/${task_uuid}`); + if (['completed', 'failed', 'cancelled'].includes(data.status)) { + sessionStorage.removeItem('activeTask'); + return; + } + // 任务仍在运行,恢复状态 + currentTask = data; + activeTaskUuid = task_uuid; + taskCompleted = false; + taskFinalStatus = null; + toastShown = false; + displayedLogs.clear(); + elements.startBtn.disabled = true; + elements.cancelBtn.disabled = false; + showTaskStatus(data); + updateTaskStatus(data.status); + addLog('info', `[系统] 检测到进行中的任务,正在重连监控... (${task_uuid.substring(0, 8)})`); + connectWebSocket(task_uuid); + } catch { + sessionStorage.removeItem('activeTask'); + } + } else if ((mode === 'batch' || mode === 'outlook_batch') && batch_id) { + // 查询批量任务是否仍在运行 + const endpoint = mode === 'outlook_batch' + ? `/registration/outlook-batch/${batch_id}` + : `/registration/batch/${batch_id}`; + try { + const data = await api.get(endpoint); + if (data.finished) { + sessionStorage.removeItem('activeTask'); + return; + } + // 批量任务仍在运行,恢复状态 + currentBatch = { batch_id, ...data }; + activeBatchId = batch_id; + isOutlookBatchMode = (mode === 'outlook_batch'); + batchCompleted = false; + batchFinalStatus = null; + toastShown = false; + displayedLogs.clear(); + elements.startBtn.disabled = true; + elements.cancelBtn.disabled = false; + showBatchStatus({ count: total || data.total }); + updateBatchProgress(data); + addLog('info', `[系统] 检测到进行中的批量任务,正在重连监控... (${batch_id.substring(0, 8)})`); + connectBatchWebSocket(batch_id); + } catch { + sessionStorage.removeItem('activeTask'); + } + } +} diff --git a/webui.py b/webui.py index 4f4b2a4..3c55899 100644 --- a/webui.py +++ b/webui.py @@ -78,6 +78,7 @@ def start_webui(): "reload": settings.debug, "log_level": "info" if settings.debug else "warning", "access_log": settings.debug, + "ws": "websockets", } logger = logging.getLogger(__name__)