mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-11 10:30:02 +08:00
feat(web): 添加页面可见性重连机制和WebSocket支持
- 前端app.js添加页面可见性监听和WebSocket重连逻辑 - 后端registration.py集成TaskManager支持WebSocket推送 - 更新依赖添加websockets库支持 - 优化批量任务状态管理和日志推送
This commit is contained in:
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"pydantic-settings>=2.0.0",
|
"pydantic-settings>=2.0.0",
|
||||||
"sqlalchemy>=2.0.0",
|
"sqlalchemy>=2.0.0",
|
||||||
"aiosqlite>=0.19.0",
|
"aiosqlite>=0.19.0",
|
||||||
|
"websockets>=16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -420,6 +420,9 @@ async def run_batch_registration(
|
|||||||
|
|
||||||
使用线程池执行每个注册任务,避免阻塞主事件循环
|
使用线程池执行每个注册任务,避免阻塞主事件循环
|
||||||
"""
|
"""
|
||||||
|
# 初始化 TaskManager 批量任务(支持 WebSocket 推送)
|
||||||
|
task_manager.init_batch(batch_id, len(task_uuids))
|
||||||
|
|
||||||
batch_tasks[batch_id] = {
|
batch_tasks[batch_id] = {
|
||||||
"total": len(task_uuids),
|
"total": len(task_uuids),
|
||||||
"completed": 0,
|
"completed": 0,
|
||||||
@@ -430,18 +433,31 @@ async def run_batch_registration(
|
|||||||
"current_index": 0
|
"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:
|
try:
|
||||||
for i, task_uuid in enumerate(task_uuids):
|
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:
|
with get_db() as db:
|
||||||
for remaining_uuid in task_uuids[i:]:
|
for remaining_uuid in task_uuids[i:]:
|
||||||
crud.update_registration_task(db, remaining_uuid, status="cancelled")
|
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} 已取消")
|
logger.info(f"批量任务 {batch_id} 已取消")
|
||||||
break
|
break
|
||||||
|
|
||||||
batch_tasks[batch_id]["current_index"] = i
|
update_batch_status(current_index=i)
|
||||||
|
|
||||||
# 运行单个注册任务(使用线程池)
|
# 运行单个注册任务(使用线程池)
|
||||||
await run_registration_task(
|
await run_registration_task(
|
||||||
@@ -452,22 +468,38 @@ async def run_batch_registration(
|
|||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
task = crud.get_registration_task(db, task_uuid)
|
task = crud.get_registration_task(db, task_uuid)
|
||||||
if task:
|
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":
|
if task.status == "completed":
|
||||||
batch_tasks[batch_id]["success"] += 1
|
new_success += 1
|
||||||
|
add_batch_log(f"[成功] 第 {new_success} 个账号注册成功")
|
||||||
elif task.status == "failed":
|
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)
|
wait_time = random.randint(interval_min, interval_max)
|
||||||
logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后继续下一个任务")
|
logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后继续下一个任务")
|
||||||
await asyncio.sleep(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:
|
except Exception as e:
|
||||||
logger.error(f"批量任务 {batch_id} 异常: {e}")
|
logger.error(f"批量任务 {batch_id} 异常: {e}")
|
||||||
|
add_batch_log(f"[错误] 批量任务异常: {str(e)}")
|
||||||
|
update_batch_status(finished=True, status="failed")
|
||||||
finally:
|
finally:
|
||||||
batch_tasks[batch_id]["finished"] = True
|
batch_tasks[batch_id]["finished"] = True
|
||||||
|
|
||||||
@@ -616,6 +648,7 @@ async def cancel_batch(batch_id: str):
|
|||||||
raise HTTPException(status_code=400, detail="批量任务已完成")
|
raise HTTPException(status_code=400, detail="批量任务已完成")
|
||||||
|
|
||||||
batch["cancelled"] = True
|
batch["cancelled"] = True
|
||||||
|
task_manager.cancel_batch(batch_id)
|
||||||
return {"success": True, "message": "批量任务取消请求已提交"}
|
return {"success": True, "message": "批量任务取消请求已提交"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
118
static/js/app.js
118
static/js/app.js
@@ -30,6 +30,8 @@ let batchWebSocket = null; // 批量任务 WebSocket
|
|||||||
let useWebSocket = true; // 是否使用 WebSocket
|
let useWebSocket = true; // 是否使用 WebSocket
|
||||||
let wsHeartbeatInterval = null; // 心跳定时器
|
let wsHeartbeatInterval = null; // 心跳定时器
|
||||||
let batchWsHeartbeatInterval = null; // 批量任务心跳定时器
|
let batchWsHeartbeatInterval = null; // 批量任务心跳定时器
|
||||||
|
let activeTaskUuid = null; // 当前活跃的单任务 UUID(用于页面重新可见时重连)
|
||||||
|
let activeBatchId = null; // 当前活跃的批量任务 ID(用于页面重新可见时重连)
|
||||||
|
|
||||||
// DOM 元素
|
// DOM 元素
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -78,6 +80,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadAvailableServices();
|
loadAvailableServices();
|
||||||
loadRecentAccounts();
|
loadRecentAccounts();
|
||||||
startAccountsPolling();
|
startAccountsPolling();
|
||||||
|
initVisibilityReconnect();
|
||||||
|
restoreActiveTask();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 事件监听
|
// 事件监听
|
||||||
@@ -313,6 +317,9 @@ async function handleSingleRegistration(requestData) {
|
|||||||
const data = await api.post('/registration/start', requestData);
|
const data = await api.post('/registration/start', requestData);
|
||||||
|
|
||||||
currentTask = data;
|
currentTask = data;
|
||||||
|
activeTaskUuid = data.task_uuid; // 保存用于重连
|
||||||
|
// 持久化到 sessionStorage,跨页面导航后可恢复
|
||||||
|
sessionStorage.setItem('activeTask', JSON.stringify({ task_uuid: data.task_uuid, mode: 'single' }));
|
||||||
addLog('info', `[系统] 任务已创建: ${data.task_uuid}`);
|
addLog('info', `[系统] 任务已创建: ${data.task_uuid}`);
|
||||||
showTaskStatus(data);
|
showTaskStatus(data);
|
||||||
updateTaskStatus('running');
|
updateTaskStatus('running');
|
||||||
@@ -476,12 +483,15 @@ async function handleBatchRegistration(requestData) {
|
|||||||
const data = await api.post('/registration/batch', requestData);
|
const data = await api.post('/registration/batch', requestData);
|
||||||
|
|
||||||
currentBatch = data;
|
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.batch_id}`);
|
||||||
addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`);
|
addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`);
|
||||||
showBatchStatus(data);
|
showBatchStatus(data);
|
||||||
|
|
||||||
// 开始轮询批量状态
|
// 优先使用 WebSocket
|
||||||
startBatchPolling(data.batch_id);
|
connectBatchWebSocket(data.batch_id);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addLog('error', `[错误] 启动失败: ${error.message}`);
|
addLog('error', `[错误] 启动失败: ${error.message}`);
|
||||||
@@ -845,6 +855,11 @@ function resetButtons() {
|
|||||||
// 重置最终状态标志
|
// 重置最终状态标志
|
||||||
taskFinalStatus = null;
|
taskFinalStatus = null;
|
||||||
batchFinalStatus = null;
|
batchFinalStatus = null;
|
||||||
|
// 清除活跃任务标识
|
||||||
|
activeTaskUuid = null;
|
||||||
|
activeBatchId = null;
|
||||||
|
// 清除 sessionStorage 持久化状态
|
||||||
|
sessionStorage.removeItem('activeTask');
|
||||||
// 断开 WebSocket
|
// 断开 WebSocket
|
||||||
disconnectWebSocket();
|
disconnectWebSocket();
|
||||||
disconnectBatchWebSocket();
|
disconnectBatchWebSocket();
|
||||||
@@ -979,6 +994,9 @@ async function handleOutlookBatchRegistration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentBatch = { batch_id: data.batch_id, ...data };
|
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.batch_id}`);
|
||||||
addLog('info', `[系统] 总数: ${data.total}, 跳过已注册: ${data.skipped}, 待注册: ${data.to_register}`);
|
addLog('info', `[系统] 总数: ${data.total}, 跳过已注册: ${data.skipped}, 待注册: ${data.to_register}`);
|
||||||
|
|
||||||
@@ -1177,3 +1195,99 @@ function startOutlookBatchPolling(batchId) {
|
|||||||
|
|
||||||
batchPollingInterval.lastLogIndex = 0;
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
webui.py
1
webui.py
@@ -78,6 +78,7 @@ def start_webui():
|
|||||||
"reload": settings.debug,
|
"reload": settings.debug,
|
||||||
"log_level": "info" if settings.debug else "warning",
|
"log_level": "info" if settings.debug else "warning",
|
||||||
"access_log": settings.debug,
|
"access_log": settings.debug,
|
||||||
|
"ws": "websockets",
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
Reference in New Issue
Block a user