From 627f06d0a0c6aef0475a8a89fde49324b0e083f2 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 22 Jun 2026 14:21:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/web.db | Bin 36864 -> 36864 bytes web/backend/__pycache__/main.cpython-312.pyc | Bin 1945 -> 2035 bytes web/backend/main.py | 3 +- .../routers/__pycache__/login.cpython-312.pyc | Bin 6856 -> 7450 bytes web/backend/routers/cookies.py | 95 +++++++++++++++ web/backend/routers/login.py | 36 ++++-- .../__pycache__/login_service.cpython-312.pyc | Bin 8424 -> 8476 bytes web/backend/services/login_service.py | 1 + web/frontend/src/App.tsx | 2 + web/frontend/src/api/modules.ts | 6 + web/frontend/src/layouts/MainLayout.tsx | 89 +++++++++------ web/frontend/src/pages/CookiePage.tsx | 108 ++++++++++++++++++ web/frontend/src/pages/LoginTasksPage.tsx | 9 +- web/frontend/src/pages/ProxyPage.tsx | 34 +++--- 14 files changed, 322 insertions(+), 61 deletions(-) create mode 100644 web/backend/routers/cookies.py create mode 100644 web/frontend/src/pages/CookiePage.tsx diff --git a/data/web.db b/data/web.db index f29ea739c849e5d962d960273c0d60ab07136c1d..e4be75424a404f9c577b067bc3b85c5b1f6a1f36 100644 GIT binary patch delta 2545 zcmbVOO^@S98Sd$xZ8Yrkqz4WNEfNu{95*Ug`J|a3e3pmX%dxAR`${mL3okn6AsUq3WTlGF~ zJ3RkpZw$-`>*cg?!5MkD0in+(k3ix(f}J2gM&!d%^)x~XPU12krxJMu4Z|~qihOA z;jBm*^q7tMIaq1=6%@65tTLYr7h_6~%9CTR36*7aZN$}PX;2zWIHrolj^eaBh6Lm_ z?kTcZO~Oc+26FxMVj9iY@xp9KGsSMi9<`oZ>}pn8^-q`U7SH)82aoH`xN=$z38Byd zN7FC2%^FzO$9TeNQ?@@|_yJkNHwDz_@$J6bRn%!ib_WE!;3@`C% zqIZ;SC@oMlH?`Tgs#{H~7OFzeYMj)7IV`2`^*+_MChdGW#Tc}$rW06tQmw=~I=;}o zwl(aotPvssyA+774735|h_=6$L^Zp`P&vMl~S(BGVl7(8!e-)osU8wd1DGiD<2?@o8tNr4`xJm8^``QanY3 zC^iOST6kcbh=J(!7wU<~HkBzPaZ0l1kZeX3WJ|U6^(fZU`k);5NWDFzR(tD_+i|6; z6gP!3AajngjuwPxjz6`$vz4|fdX_#VF503r(-y;4nL&+);FJedVH%+e&#Bg9 z`<(X_s}6g^d7~juCt}N*BfYUmPRu!i?ek7uZA7kOAs*LB6R3dcpqXgVb*UAo#N`WW z*zsGyIu^HO*sJg?^yj9RI{qbxiEv-)fUV=yR^K@-+Y&7+A|n&siP_24ZBM2~7IDv$ zL2Afo*q@8`Vw}2pWsu89YPV*P+?F=DEYoI}g%=2kU=s`&#i8m`tN1Y|Xtfn#| zPM0ak6~V}@*&|y=#6YsHPwT`;iFA)-CpvMvgrt*|r41_JzqH#@9nRA(n44Yd+(7Mc zJZMv*2l69(w2`YFM|BCY?bxGOj3vRannKN$1aTuPHFp%x2_|hdGu)f1CC?7V|0mo3 zQZ4+dswFQc+smt_DmoZJiI$XJ+*G7wEfH_jK^c(K!>EEtH-!@U)$FD)-O3hOMv=!X z!VKiue?x(}Ob^bBczU9)@LZY8@pP_)b7eM%aqgJo0bn`IVV>Tm@K@g1()Sa$NIs~ZE`l5?kyAm2wy^ZP;YXWDH@v*=5hz+X zzGkGcs|Do2Feh5%q~C4GoNe>#;T3U0n4NG=crjpD3Sn*=9$WD<+8~@4d6u7O3>?$r zu|DQFV>`((FVccB2cf}n6Rgn&HZ({9mS-tb1GJ$}F!Q&c|KaiHA3XWd`;XuG6$Frd z1F&yEm<4PBphCjnJWX*d&%F?$3pCF25J5zRx4%wq$ZsF?$OUPPHI6q8Rs$5AXqICd zPk#B}i{HLW#?wz9JpTCM)8GB(?|*#v@$WzT;_dgIe)>U{$v*ky*DvIT1q$U6gK5a1 z8zIExDW1gegWG#4@3#(CgqJn}W)N!tP6wAiODYMe9u@$X=NOg)_+}O=Ppk$2?d8w)XR|Ih delta 85 zcmZozz|^pSX@WGP@mYQ9$w#^Dbjqpcg-byjdg&ByO=~q$cK+WE9B* zIa)wm%nKwM7#{G5Oh~!FEq#GSx=3#FdX`0!Abmx0AOfVXNEJrtPtIbMlLkrL;)svW zP0Y-TkJpsC#U3A@lAjzOU!*d5F>4VwA4s_nh~S?r&1M)5vJPwpNK;;7ZYoeEstva| zY;yBcN^?@}ij07Y8G*Q10Z4veW@Kc%$sqEWf$uJZ)?Ehay9`E88D#G>I9_LPoV=KA a*JN*YE)MhQb`$NcFepuqWw&4fISv3u*i~`$ScVxF;P9*h>;Et zvtX9?WCJEmQLz-!7KSMCREZR^Yz+p6A`YNu#3$!5y=r%*>0A*Oa`)9v`2QpBx`wq&)c< zYZ03uNIBo+WHv){kWFCiAoY2Pxv4-!sD|I-u*uC&Da}c>D>4KsW(4A5zR9QAWLPY& SGFVK0&9-auc6JLEkevYM^;-J? diff --git a/web/backend/main.py b/web/backend/main.py index c1deca0..c8fe990 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -6,7 +6,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from .database import init_db -from .routers import auth, users, accounts, login, proxy +from .routers import auth, users, accounts, login, proxy, cookies @asynccontextmanager @@ -36,6 +36,7 @@ app.include_router(users.router) app.include_router(accounts.router) app.include_router(login.router) app.include_router(proxy.router) +app.include_router(cookies.router) @app.get("/api/health") diff --git a/web/backend/routers/__pycache__/login.cpython-312.pyc b/web/backend/routers/__pycache__/login.cpython-312.pyc index 4f77e011dffc8ef0b4f77ebc0c308cf8c7650069..d66f9860e30dc62f085f3f8cf126d0622952892e 100644 GIT binary patch delta 2138 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 diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py new file mode 100644 index 0000000..dbeb7d6 --- /dev/null +++ b/web/backend/routers/cookies.py @@ -0,0 +1,95 @@ +"""Cookie 管理路由""" + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +import io +import csv + +from ..database import get_db +from ..models import User, LoginTask, Account +from ..deps import get_current_user, require_permission +from ..permissions import has_permission + +router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"]) + + +@router.get("") +def list_cookies( + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """查看登录成功的 Cookie 列表。""" + query = db.query(LoginTask).filter(LoginTask.status == "success") + + # 客服只能看自己账号的 + if not has_permission(current.role, "login:view_all"): + query = query.join(Account, LoginTask.account_id == Account.id).filter( + Account.assigned_to == current.id + ) + + tasks = query.order_by(LoginTask.finished_at.desc()).all() + result = [] + for t in tasks: + acc = db.query(Account).filter(Account.id == t.account_id).first() + item = { + "id": t.id, + "batch_id": t.batch_id, + "account_id": t.account_id, + "account_username": acc.username if acc else "", + "created_at": t.finished_at.isoformat() if t.finished_at else None, + } + # 只有有 cookie:view 权限才返回 cookie 内容 + if has_permission(current.role, "cookie:view"): + cookie = t.cookie or "" + item["cookie"] = cookie + item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie + else: + item["cookie"] = "" + item["cookie_preview"] = "***" + result.append(item) + return result + + +@router.get("/export") +def export_cookies( + db: Session = Depends(get_db), + current: User = Depends(require_permission("cookie:export")), +): + """导出 Cookie 为 CSV。""" + tasks = db.query(LoginTask).filter( + LoginTask.status == "success" + ).order_by(LoginTask.finished_at.desc()).all() + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(["账号", "Cookie", "时间"]) + + for t in tasks: + acc = db.query(Account).filter(Account.id == t.account_id).first() + username = acc.username if acc else "" + writer.writerow([username, t.cookie or "", t.finished_at.isoformat() if t.finished_at else ""]) + + output.seek(0) + return StreamingResponse( + iter([output.getvalue()]), + media_type="text/csv", + headers={"Content-Disposition": "attachment; filename=cookies.csv"}, + ) + + +@router.delete("/{task_id}") +def delete_cookie( + task_id: int, + db: Session = Depends(get_db), + current: User = Depends(require_permission("cookie:export")), +): + """删除一条 Cookie 记录。""" + task = db.query(LoginTask).filter(LoginTask.id == task_id).first() + if not task: + raise HTTPException(status_code=404, detail="记录不存在") + task.cookie = "" + task.status = "failed" + task.message = "Cookie已清除" + db.commit() + return {"message": "已删除", "success": True} diff --git a/web/backend/routers/login.py b/web/backend/routers/login.py index 096829b..3087a4e 100644 --- a/web/backend/routers/login.py +++ b/web/backend/routers/login.py @@ -15,12 +15,12 @@ from ..services.login_service import LoginBatchRunner router = APIRouter(prefix="/api/login", tags=["登录任务"]) -# 运行中的批次: batch_id -> {runner, log_queue, loop} +# 运行中的批次: batch_id -> {log_queue, loop, runner} _active_batches: dict[str, dict] = {} @router.post("/batch") -def create_batch( +async def create_batch( req: LoginBatchRequest, db: Session = Depends(get_db), current: User = Depends(require_permission("login:batch")), @@ -46,6 +46,10 @@ def create_batch( if not valid_ids: raise HTTPException(status_code=403, detail="没有可登录的账号") + # 在主事件循环中创建 log_queue,传给后台线程 + log_queue = asyncio.Queue() + loop = asyncio.get_running_loop() + # 创建执行器(用独立的 DB 会话,因为在线程中运行) thread_db = SessionLocal() runner = LoginBatchRunner( @@ -55,10 +59,19 @@ def create_batch( creator_role=current.role, max_geetest_retries=req.max_geetest_retries, proxy_config=proxy, + log_queue=log_queue, + loop=loop, ) batch_id = runner.batch_id + # 先注册到全局,再启动线程,确保 WebSocket 连接时能找到 + _active_batches[batch_id] = { + "log_queue": log_queue, + "loop": loop, + "runner": runner, + } + # 启动线程 thread = threading.Thread(target=runner.run, daemon=True) thread.start() @@ -114,21 +127,24 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str): """WebSocket 推送登录实时日志。""" await websocket.accept() - log_queue = asyncio.Queue() - loop = asyncio.get_running_loop() + # 从已注册的批次中获取 log_queue(由 create_batch 创建) + batch = _active_batches.get(batch_id) + if not batch: + await websocket.send_json({"level": "error", "message": "批次不存在或已结束"}) + await websocket.close() + return - # 查找已运行的批次,或等待新批次 - # 简化:直接把 log_queue 注册到全局,前端创建批次后连 ws - _active_batches[batch_id] = { - "log_queue": log_queue, - "loop": loop, - } + log_queue: asyncio.Queue = batch["log_queue"] try: while True: try: msg = await asyncio.wait_for(log_queue.get(), timeout=30) await websocket.send_json(msg) + # 收到 result 表示任务结束 + if msg.get("level") == "result": + await asyncio.sleep(0.1) + break except asyncio.TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: diff --git a/web/backend/services/__pycache__/login_service.cpython-312.pyc b/web/backend/services/__pycache__/login_service.cpython-312.pyc index 4432412f13e1a0dd26ba8feb5065e003df4ed63b..0833bb013a282249da46c2b8b10b701244cb422b 100644 GIT binary patch delta 204 zcmaFiILC?iG%qg~0}xC-Z;_R-k@qJr(>{*L1sqbFmH4i)GuBL2WD{jcQAiP){7!VK z@R}6fTHZR|a>jCo3g$?Laz;(X&CA5Zm^f~+6{QxJ=9FySBazO?S`+{@aI&_PHRJBh z=~7Y5B3C%vZ*cJR^L6q~s8}F$l|$vn7vagrWg|rbxR@?*aR$n;UJ$kq6lc9C&Kf8) z*+kBpF==vxTovQq$-m|HutYMls!rY`zmPF_a-4!Yqs8Q93Wod%OpL0Z7(ir^GSD^v D3^qLP delta 176 zcmbQ^^um$%G%qg~0}$LhWszmQk@qJr(=3k31sqbFmH4i)PnHpzEj%lQx0bh#x16z@ zp@KP*p`1}uVe=_5F{aHgB+?labU+f@$tA72C~|C5ar z@wZ?)Z^0SJ#d?9!K9H65A}ebk*W_$DZ^p#Q+vTbl_f0mE-@_Wg$m+p3`IG!Y#-Pbl b6x} /> } /> } /> + } /> } /> } /> diff --git a/web/frontend/src/api/modules.ts b/web/frontend/src/api/modules.ts index 3c2de94..bf3d354 100644 --- a/web/frontend/src/api/modules.ts +++ b/web/frontend/src/api/modules.ts @@ -52,6 +52,12 @@ export const loginApi = { stop: (batch_id: string) => api.post(`/login/stop/${batch_id}`), }; +export const cookieApi = { + list: () => api.get('/cookies'), + exportCsv: () => api.get('/cookies/export', { responseType: 'blob' }), + delete: (id: number) => api.delete(`/cookies/${id}`), +}; + export const proxyApi = { get: () => api.get('/proxy'), update: (data: any) => api.put('/proxy', data), diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index 005cef2..82b9fd8 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -1,14 +1,15 @@ import { useEffect, useState } from 'react'; -import { Layout, Menu, Dropdown, Avatar, Space, Typography } from 'antd'; +import { Layout, Menu, Avatar, Space, Typography, Button } from 'antd'; import { DashboardOutlined, UserOutlined, LogoutOutlined, - CloudServerOutlined, TeamOutlined, ApiOutlined, + CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined, + MenuFoldOutlined, MenuUnfoldOutlined, } from '@ant-design/icons'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import { getUser, clearAuth, hasPerm, type AuthUser } from '../store/auth'; import { authApi } from '../api/modules'; -const { Header, Sider, Content } = Layout; +const { Sider, Content } = Layout; const { Text } = Typography; const ROLE_LABELS: Record = { @@ -44,6 +45,11 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { menuItems.push({ key: '/login-tasks', label: '登录任务', icon: }); } + // Cookie 管理 + if (hasPerm(user, 'cookie:view')) { + menuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: }); + } + // 代理配置 if (hasPerm(user, 'proxy:manage')) { menuItems.push({ key: '/proxy', label: '代理配置', icon: }); @@ -59,30 +65,30 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { await authApi.logout(); } catch {} clearAuth(); - onLogout?.(); // 触发 App 重渲染 + onLogout?.(); navigate('/login', { replace: true }); }; - const userMenu = { - items: [ - { - key: 'logout', - label: '退出登录', - icon: , - onClick: handleLogout, - }, - ], - }; - return ( - - + +
- {collapsed ? '鱼' : '斗鱼登录后台'} + + {collapsed ? '鱼' : '斗鱼登录后台'} + +
void }) { selectedKeys={[location.pathname]} items={menuItems} onClick={({ key }) => navigate(key)} + style={{ flex: 1, overflow: 'auto', marginTop: 8 }} /> +
+ + } size="small" /> + {!collapsed && ( + <> + {user.username} + + ({ROLE_LABELS[user.role] || user.role}) + + + )} + + {!collapsed && ( + + )} +
-
- - - } /> - {user.username} - ({ROLE_LABELS[user.role] || user.role}) - - -
- +
diff --git a/web/frontend/src/pages/CookiePage.tsx b/web/frontend/src/pages/CookiePage.tsx new file mode 100644 index 0000000..465ad8b --- /dev/null +++ b/web/frontend/src/pages/CookiePage.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm } from 'antd'; +import { DownloadOutlined, DeleteOutlined } from '@ant-design/icons'; +import { cookieApi } from '../api/modules'; +import { getUser, hasPerm } from '../store/auth'; + +export default function CookiePage() { + const [cookies, setCookies] = useState([]); + const [loading, setLoading] = useState(false); + const user = getUser(); + + const canView = hasPerm(user, 'cookie:view'); + const canExport = hasPerm(user, 'cookie:export'); + + const loadCookies = async () => { + setLoading(true); + try { + const data = await cookieApi.list(); + setCookies(data); + } catch (e: any) { + message.error(e.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadCookies(); + }, []); + + const handleExport = async () => { + try { + const res = await cookieApi.exportCsv(); + const url = URL.createObjectURL(new Blob([res.data])); + const a = document.createElement('a'); + a.href = url; + a.download = 'cookies.csv'; + a.click(); + URL.revokeObjectURL(url); + message.success('已导出'); + } catch (e: any) { + message.error(e.message); + } + }; + + const handleDelete = async (id: number) => { + try { + await cookieApi.delete(id); + message.success('已删除'); + loadCookies(); + } catch (e: any) { + message.error(e.message); + } + }; + + const columns: any[] = [ + { title: 'ID', dataIndex: 'id', width: 60 }, + { title: '账号', dataIndex: 'account_username' }, + { + title: 'Cookie', + dataIndex: 'cookie_preview', + ellipsis: true, + render: (val: string) => { + if (!canView) return ***; + return {val}; + }, + }, + { title: '时间', dataIndex: 'created_at', width: 180 }, + ]; + + if (canExport) { + columns.push({ + title: '操作', + width: 80, + render: (_: any, record: any) => ( + handleDelete(record.id)}> + + + ), + }); + } + + return ( +
+
+

Cookie 管理

+ {canExport && ( + + )} +
+ + + + + + + + ); +} diff --git a/web/frontend/src/pages/LoginTasksPage.tsx b/web/frontend/src/pages/LoginTasksPage.tsx index 6459757..de7578a 100644 --- a/web/frontend/src/pages/LoginTasksPage.tsx +++ b/web/frontend/src/pages/LoginTasksPage.tsx @@ -29,6 +29,7 @@ export default function LoginTasksPage() { const [loading, setLoading] = useState(false); const [batchId, setBatchId] = useState(null); const [logs, setLogs] = useState<{ level: string; message: string }[]>([]); + const [wsConnected, setWsConnected] = useState(false); const wsRef = useRef(null); const user = getUser(); @@ -75,13 +76,19 @@ export default function LoginTasksPage() { const wsUrl = `ws://${window.location.hostname}:8000/api/login/ws/login/${result.batch_id}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; + setWsConnected(true); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.level === 'heartbeat') return; + if (msg.level === 'result') return; setLogs((prev) => [...prev, msg]); }; ws.onclose = () => { wsRef.current = null; + setWsConnected(false); + }; + ws.onerror = () => { + setWsConnected(false); }; } catch (e: any) { message.error(e.message); @@ -183,7 +190,7 @@ export default function LoginTasksPage() { bodyStyle={{ maxHeight: 500, overflow: 'auto', fontFamily: 'monospace', fontSize: 12 }} > {logs.length === 0 ? ( - + ) : ( logs.map((log, i) => (
-
+
+

代理配置

@@ -122,6 +122,7 @@ export default function ProxyPage() { form={form} layout="vertical" disabled={!configLoaded} + size="small" initialValues={{ enabled: false, whitelist_enabled: false, @@ -131,37 +132,38 @@ export default function ProxyPage() { whitelist_uid: '', whitelist_ukey: '', }} + style={{ flexShrink: 0 }} > - +
- - + + - + - + - + - + - - + + - + - + - + @@ -170,8 +172,8 @@ export default function ProxyPage() { {logs.length === 0 ? ( 点击"测试代理"或"测试白名单"查看日志