优化 ui
This commit is contained in:
Binary file not shown.
+2
-1
@@ -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")
|
||||
|
||||
Binary file not shown.
@@ -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}
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
@@ -175,3 +175,4 @@ class LoginBatchRunner:
|
||||
self.db.commit()
|
||||
|
||||
self._push_log("info", f"批量登录任务 {batch_id} 完成")
|
||||
self._push_log("result", "")
|
||||
|
||||
Reference in New Issue
Block a user