From 22aa485536c804521d9e3e7300a6b635de3a3805 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 18:20:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=AE=A1=E6=A0=B8=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=20=E2=80=94=205=20=E9=A1=B9=E4=BF=AE=E5=A4=8D=20(?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E5=86=99=E5=85=A5=E4=B8=A2=E5=A4=B1/?= =?UTF-8?q?=E6=96=97=E9=B1=BCWS=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F/?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E6=A0=A1=E9=AA=8C/=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=AE=A2=E9=98=85/=E7=94=BB=E5=83=8Fkey=E7=A8=B3=E5=AE=9A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1 routers/huya.py: 批量 Web 登录 login_channel/status 回填在 helper 内部 commit 之后赋值且未再提交 → 丢失; 补 db.commit() 并统一 status=active B2 routers/login.py + services/login_service.py: 斗鱼登录 WS 断线即 pop 批次, 刷新后无法重连订阅 → 对齐虎牙模式 (BatchRegistry.mark_finished, 仅结束后清理) B3 routers/login.py: WS 增加批次归属校验 (非 view_all 仅可订阅自建批次) S1 LoginTasksPage: effectiveBatchId 从任务列表推导活跃批次, 刷新后停止按钮/ 日志订阅自动恢复 S4 services/huya_service.py: username 仅在为空时回填, 防止 udb_passport 覆盖 登录名导致一号一设备绑定 key 漂移 验证: 77 后端单测 OK; tsc + vite build OK --- web/backend/routers/huya.py | 4 +++ web/backend/routers/login.py | 24 +++++++++++++- web/backend/services/huya_service.py | 11 +++++-- web/backend/services/login_service.py | 15 +++++++++ web/frontend/src/pages/LoginTasksPage.tsx | 40 ++++++++++++++++++----- 5 files changed, 82 insertions(+), 12 deletions(-) diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 3d0b9e0..918ec4c 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -932,7 +932,11 @@ def password_login_selected_accounts( tag=account.tag or "", username_hint=username, ) + # save_huya_login_cookie_to_account 内部已 commit,渠道/状态回填需再次提交, + # 否则 get_db 关闭会话时丢弃(GUI 登录渠道列会显示"仅导入")。 + saved.status = "active" saved.login_channel = "web" + db.commit() success_count += 1 results.append({ "line": account.id, diff --git a/web/backend/routers/login.py b/web/backend/routers/login.py index f35883c..c22d879 100644 --- a/web/backend/routers/login.py +++ b/web/backend/routers/login.py @@ -185,6 +185,10 @@ def stop_batch( ): batch = batch_registry.get(batch_id) if batch: + if batch.get("finished"): + # 已结束但 WS 尚未清理(或从未有 WS 订阅):回收注册表条目。 + batch_registry.pop(batch_id) + raise HTTPException(status_code=404, detail="批次已结束") batch["runner"].stop() return {"message": "已发送停止信号", "success": True} raise HTTPException(status_code=404, detail="批次不存在或已结束") @@ -199,6 +203,21 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str): await websocket.close(code=1008, reason="未授权") return + # 归属校验:非 view_all 只能订阅自己创建的批次(与虎牙批次一致)。 + if not user_has_permission(user, "login:view_all"): + check_db = SessionLocal() + try: + owned = ( + check_db.query(LoginTask.id) + .filter(LoginTask.batch_id == batch_id, LoginTask.created_by == user.id) + .first() + ) + finally: + check_db.close() + if not owned: + await websocket.close(code=1008, reason="无权访问该任务批次") + return + await websocket.accept() # 从已注册的批次中获取 log_queue(由 create_batch 创建) @@ -224,4 +243,7 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str): except WebSocketDisconnect: pass finally: - batch_registry.pop(batch_id) + # 只在批次真正结束后清理注册表,客户端断线/刷新页面后重连仍可继续订阅。 + latest = batch_registry.get(batch_id) + if latest and latest.get("finished"): + batch_registry.pop(batch_id) diff --git a/web/backend/services/huya_service.py b/web/backend/services/huya_service.py index 4e9c60a..512c91f 100644 --- a/web/backend/services/huya_service.py +++ b/web/backend/services/huya_service.py @@ -229,7 +229,10 @@ def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str | else: account.uid = parsed["uid"] or account.uid account.yyuid = parsed["yyuid"] or account.yyuid - account.username = parsed["username"] or account.username + # 已存在账号保留原有 username(登录名/设备画像 key),避免重复导入或登录 + # 前后 udb_passport 差异导致一号一设备绑定断裂。 + if not account.username: + account.username = parsed["username"] or "" account.cookie = parsed["cookie"] account.game_phone = parsed["game_phone"] or account.game_phone if tag: @@ -254,7 +257,11 @@ def save_huya_login_cookie_to_account( account.uid = parsed["uid"] or account.uid account.yyuid = parsed["yyuid"] or account.yyuid - account.username = parsed["username"] or account.username + # 保留已有 username(登录名): 设备画像/指纹状态目录按登录用户名分账号隔离, + # 若被 cookie 里的 udb_passport 覆盖, 下一次登录会拿到不同的 key → 生成全新设备, + # 一号一设备绑定断裂且绑定页出现两条记录。仅在无用户名时回填。 + if not account.username: + account.username = parsed["username"] or "" account.cookie = parsed["cookie"] account.game_phone = parsed["game_phone"] or account.game_phone if tag: diff --git a/web/backend/services/login_service.py b/web/backend/services/login_service.py index 6719ee7..b540f77 100644 --- a/web/backend/services/login_service.py +++ b/web/backend/services/login_service.py @@ -467,6 +467,8 @@ class LoginBatchRunner: self._push_log("info", f"批量{action_name}任务 {batch_id} 完成") self._push_log("result", "") finally: + # 标记批次结束:WS 端据此决定何时清理注册表(断线重连可继续订阅日志)。 + batch_registry.mark_finished(batch_id) # 确保 DB Session 被关闭,避免连接泄漏 self.db.close() @@ -485,11 +487,24 @@ class BatchRegistry: "loop": loop, "runner": runner, "owner_id": owner_id, + "finished": False, + "finished_at": None, } def get(self, batch_id: str): return self._batches.get(batch_id) + def mark_finished(self, batch_id: str): + """标记批次已结束(幂等;不在本注册表的批次为无操作)。 + + 与虎牙批次一致:WS 端只在 finished 后 pop,客户端断线重连仍可订阅 + 到运行中批次的实时日志。 + """ + batch = self._batches.get(batch_id) + if batch: + batch["finished"] = True + batch["finished_at"] = time.time() + def pop(self, batch_id: str): return self._batches.pop(batch_id, None) diff --git a/web/frontend/src/pages/LoginTasksPage.tsx b/web/frontend/src/pages/LoginTasksPage.tsx index 91b7694..ba396a1 100644 --- a/web/frontend/src/pages/LoginTasksPage.tsx +++ b/web/frontend/src/pages/LoginTasksPage.tsx @@ -213,6 +213,27 @@ export default function LoginTasksPage() { [tasks], ); + // 刷新/切页后 batchId 内存态丢失:从任务列表推导仍在运行的批次, + // 保证停止按钮与日志订阅在页面恢复后依然可用。 + const activeBatchIdFromTasks = useMemo( + () => tasks.find((task) => ['pending', 'running'].includes(task.status))?.batch_id || null, + [tasks], + ); + const effectiveBatchId = batchId || activeBatchIdFromTasks; + + // 活跃批次存在但未订阅时自动重连实时日志(配合后端 WS 仅在批次结束后清理注册表)。 + const wsSubscribedBatchRef = useRef(null); + useEffect(() => { + if (!effectiveBatchId || wsConnected) return; + if (wsSubscribedBatchRef.current === effectiveBatchId) return; + wsSubscribedBatchRef.current = effectiveBatchId; + connectLogs(`/api/login/ws/login/${effectiveBatchId}`, { + onClose: () => { setBatchId(null); setStarting(false); }, + onResult: () => { setBatchId(null); setStarting(false); }, + onError: () => { setBatchId(null); setStarting(false); }, + }); + }, [effectiveBatchId, wsConnected, connectLogs]); + useEffect(() => { void loadTags(); void loadTasks(); @@ -249,6 +270,7 @@ export default function LoginTasksPage() { mode, }); setBatchId(result.batch_id); + wsSubscribedBatchRef.current = result.batch_id; message.success(`已创建${mode === 'check' ? '检测' : '登录'}任务,共 ${result.count} 个账号`); connectLogs(`/api/login/ws/login/${result.batch_id}`, { @@ -287,9 +309,9 @@ export default function LoginTasksPage() { }; const handleStop = async () => { - if (batchId) { + if (effectiveBatchId) { try { - await loginApi.stop(batchId); + await loginApi.stop(effectiveBatchId); message.success('已发送停止信号'); } catch (e: unknown) { message.error(getErrorMessage(e)); @@ -522,11 +544,11 @@ export default function LoginTasksPage() { > 开始登录 - {batchId && ( - - )} + {effectiveBatchId && ( + + )} )} @@ -539,7 +561,7 @@ export default function LoginTasksPage() { 成功 {successCount} 失败 {failedCount} {checkedCount > 0 && 检测 {checkedCount}} - {batchId && 批次: {batchId}} + {effectiveBatchId && 批次: {effectiveBatchId}}
{selectedRowKeys.length > 0 && ( @@ -658,7 +680,7 @@ export default function LoginTasksPage() {