Files
live-hub-py/web/backend/routers/huya.py
T
2026-07-05 23:10:50 +08:00

870 lines
30 KiB
Python

"""虎牙基础管理路由"""
import asyncio
import csv
import io
import threading
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from core.huya import (
HuyaCredentialError,
HuyaLoginError,
login_huya_password,
login_huya_sms,
send_huya_sms_code,
)
from core.huya.cookie_utils import normalize_huya_cookie
from ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, get_current_user, require_permission
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User
from ..permissions import user_has_permission
from ..schemas import (
AccountAssign,
AccountTag,
BatchAssign,
HuyaAccountOut,
HuyaConfigOut,
HuyaConfigUpdate,
HuyaCookieImport,
HuyaGoodsOut,
HuyaPasswordAccountImport,
HuyaPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
HuyaRechargeGoodsOut,
HuyaSmsCodeRequest,
HuyaSmsLoginRequest,
HuyaTaskBatchRequest,
HuyaTaskOut,
)
from ..services.huya_service import (
HUYA_CONFIG_FIELDS,
SUPPORTED_TASK_TYPES,
apply_huya_config_defaults,
create_planned_tasks,
ensure_huya_config,
huya_config_value,
import_huya_cookies,
import_huya_password_accounts,
save_huya_login_cookie_to_account,
upsert_huya_cookie,
)
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
router = APIRouter(prefix="/api/huya", tags=["虎牙"])
def _has_huya_perm(user: User, permission: str) -> bool:
"""虎牙新权限兼容旧的 huya:account 大权限。"""
return user_has_permission(user, permission) or user_has_permission(user, "huya:account")
def _require_huya_perm(user: User, permission: str) -> None:
if not _has_huya_perm(user, permission):
raise HTTPException(status_code=403, detail="权限不足")
def _can_view_huya_all(user: User) -> bool:
return _has_huya_perm(user, "huya:view_all")
def _can_view_huya_assigned(user: User) -> bool:
return _has_huya_perm(user, "huya:view_assigned")
def _can_view_huya_cookie(user: User) -> bool:
return _has_huya_perm(user, "huya:cookie:view") or _has_huya_perm(user, "huya:cookie:export")
def _visible_huya_accounts_query(db: Session, current: User):
"""返回当前用户可见的虎牙账号查询。"""
query = db.query(HuyaAccount).options(joinedload(HuyaAccount.assigned_user))
if _can_view_huya_all(current):
return query
if _can_view_huya_assigned(current):
return query.filter(HuyaAccount.assigned_to == current.id)
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
def _fmt_cookie_preview(cookie: str) -> str:
if not cookie:
return ""
return cookie[:50] + "..." if len(cookie) > 50 else cookie
def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccountOut:
cookie = normalize_huya_cookie(account.cookie or "")
return HuyaAccountOut(
id=account.id,
uid=account.uid or "",
yyuid=account.yyuid or "",
username=account.username or "",
has_password=bool(account.account_password),
nickname=account.nickname or "",
cookie=cookie if include_cookie else "",
cookie_preview=_fmt_cookie_preview(cookie) if include_cookie else "***",
tag=account.tag or "",
remark=account.remark or "",
status=account.status or "",
points=account.points,
game_name=account.game_name or "",
game_channel=account.game_channel or "",
game_phone=account.game_phone or "",
assigned_to=account.assigned_to,
assigned_username=account.assigned_user.username if account.assigned_user else None,
created_at=account.created_at,
updated_at=account.updated_at,
)
def _task_out(task: HuyaTask) -> HuyaTaskOut:
account = task.account
return HuyaTaskOut(
id=task.id,
batch_id=task.batch_id,
account_id=task.account_id,
account_uid=account.uid if account else "",
account_nickname=account.nickname if account else "",
task_type=task.task_type,
status=task.status or "",
message=task.message or "",
result=task.result,
created_by=task.created_by,
created_at=task.created_at,
finished_at=task.finished_at,
)
def _config_out(config: HuyaConfig) -> HuyaConfigOut:
return HuyaConfigOut(
room_pid=huya_config_value("room_pid", config.room_pid),
sid=huya_config_value("sid", config.sid),
outer_act_id=huya_config_value("outer_act_id", config.outer_act_id),
bind_act_id=huya_config_value("bind_act_id", config.bind_act_id),
pay_channel=huya_config_value("pay_channel", config.pay_channel),
updated_at=config.updated_at,
)
def _cookie_out(account: HuyaAccount, include_cookie: bool) -> dict:
cookie = normalize_huya_cookie(account.cookie or "")
account_name = account.nickname or account.username or account.uid or str(account.id)
return {
"id": account.id,
"account_id": account.id,
"account_username": account_name,
"uid": account.uid or "",
"yyuid": account.yyuid or "",
"assigned_to": account.assigned_to,
"assigned_username": account.assigned_user.username if account.assigned_user else None,
"created_at": account.updated_at.isoformat() if account.updated_at else None,
"cookie": cookie if include_cookie else "",
"cookie_preview": _fmt_cookie_preview(cookie) if include_cookie else "***",
}
@router.get("/task-types")
def task_types(current: User = Depends(require_permission("huya:task"))):
"""返回当前规划的虎牙任务类型。"""
return SUPPORTED_TASK_TYPES
@router.get("/accounts", response_model=list[HuyaAccountOut])
def list_accounts(
assigned_only: bool = Query(False),
tag: str | None = Query(None),
has_cookie: bool = Query(False),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""查看虎牙 CK 账号。"""
query = _visible_huya_accounts_query(db, current)
if assigned_only and _can_view_huya_all(current):
query = query.filter(HuyaAccount.assigned_to.isnot(None))
if tag:
query = query.filter(HuyaAccount.tag == tag)
if has_cookie:
query = query.filter(HuyaAccount.cookie != "")
accounts = query.order_by(HuyaAccount.id.desc()).all()
include_cookie = _can_view_huya_cookie(current)
return [_account_out(account, include_cookie=include_cookie) for account in accounts]
@router.post("/accounts/import-cookies")
def import_cookies(
req: HuyaCookieImport,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""粘贴并导入虎牙 Cookie。"""
_require_huya_perm(current, "huya:import")
count, skipped = import_huya_cookies(db, req.text, req.tag)
return {
"message": f"导入/更新 {count} 条,跳过 {skipped} 条",
"success": True,
"count": count,
"skipped": skipped,
}
@router.post("/accounts/import-passwords")
def import_password_accounts(
req: HuyaPasswordAccountImport,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""导入虎牙账号密码,只入库不立即登录。"""
_require_huya_perm(current, "huya:import")
count, skipped = import_huya_password_accounts(db, req.text, req.tag)
return {
"message": f"导入/更新 {count} 个虎牙账号,跳过 {skipped} 条",
"success": True,
"count": count,
"skipped": skipped,
}
@router.post("/accounts/password-login")
def password_login_account(
req: HuyaPasswordLoginRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""使用账号密码登录虎牙,成功后保存 Cookie。"""
_require_huya_perm(current, "huya:import")
try:
result = login_huya_password(
username=req.username.strip(),
password=req.password,
cookie=req.cookie.strip() or None,
)
except HuyaCredentialError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except HuyaLoginError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"虎牙密码登录失败: {exc}") from exc
if not result.success or not result.cookie:
raise HTTPException(status_code=502, detail=result.message or "虎牙密码登录失败")
try:
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint=req.username)
except ValueError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {
"message": "登录成功,Cookie 已保存",
"success": True,
"account": _account_out(account),
"sdid": result.sdid,
}
@router.post("/accounts/sms-code")
def send_sms_code(
req: HuyaSmsCodeRequest,
current: User = Depends(get_current_user),
):
"""发送虎牙短信验证码,返回提交登录所需 state。"""
_require_huya_perm(current, "huya:import")
try:
result = send_huya_sms_code(
phone=req.phone.strip(),
cookie=req.cookie.strip() or None,
)
except HuyaLoginError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"虎牙短信发码失败: {exc}") from exc
if not result.success or not result.state:
raise HTTPException(status_code=502, detail=result.message or "虎牙短信发码失败")
return {
"message": result.message or "短信已发送",
"success": True,
"state": result.state,
"sdid": result.sdid,
"context": result.context,
"request_id": result.request_id,
}
@router.post("/accounts/sms-login")
def sms_login_account(
req: HuyaSmsLoginRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""提交虎牙短信验证码登录,成功后保存 Cookie。"""
_require_huya_perm(current, "huya:import")
try:
result = login_huya_sms(
authcode=req.authcode.strip(),
state=req.state.strip(),
phone=req.phone.strip(),
)
except HuyaLoginError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"虎牙短信登录失败: {exc}") from exc
if not result.success or not result.cookie:
raise HTTPException(status_code=502, detail=result.message or "虎牙短信登录失败")
try:
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint="")
phone = (req.phone or "").strip()
if phone:
account.game_phone = phone
account.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(account)
except ValueError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {
"message": "登录成功,Cookie 已保存",
"success": True,
"account": _account_out(account),
"sdid": result.sdid,
}
@router.post("/accounts/password-login/selected")
def password_login_selected_accounts(
req: HuyaPasswordLoginSelectedRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""对已导入的虎牙账号执行密码登录。"""
_require_huya_perm(current, "huya:import")
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
accounts = (
_visible_huya_accounts_query(db, current)
.filter(HuyaAccount.id.in_(req.account_ids))
.all()
)
account_map = {account.id: account for account in accounts}
results = []
success_count = 0
failed_count = 0
include_cookie = _can_view_huya_cookie(current)
for account_id in req.account_ids:
account = account_map.get(account_id)
if account is None:
failed_count += 1
results.append({
"line": account_id,
"username": "",
"success": False,
"message": "账号不存在或无权登录",
})
continue
username = (account.username or "").strip()
password = (account.account_password or "").strip()
if not username or not password:
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": "该账号未导入密码",
"account": _account_out(account, include_cookie=include_cookie),
})
continue
try:
result = login_huya_password(
username=username,
password=password,
cookie=account.cookie or None,
)
if not result.success or not result.cookie:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": result.message or "虎牙密码登录失败",
"account": _account_out(account, include_cookie=include_cookie),
"sdid": result.sdid,
})
continue
saved = save_huya_login_cookie_to_account(
db,
account,
result.cookie,
tag=account.tag or "",
username_hint=username,
)
success_count += 1
results.append({
"line": account.id,
"username": username,
"success": True,
"message": "登录成功,Cookie 已保存",
"account": _account_out(saved, include_cookie=include_cookie),
"sdid": result.sdid,
})
except HuyaCredentialError as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": str(exc),
"account": _account_out(account, include_cookie=include_cookie),
})
except (HuyaLoginError, ValueError) as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": str(exc),
"account": _account_out(account, include_cookie=include_cookie),
})
except Exception as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": f"虎牙密码登录失败: {exc}",
"account": _account_out(account, include_cookie=include_cookie),
})
return {
"message": f"登录完成:成功 {success_count} 条,失败 {failed_count} 条",
"success": True,
"count": success_count,
"failed": failed_count,
"results": results,
}
@router.delete("/accounts/batch")
def delete_accounts_batch(
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""批量删除虎牙 CK 账号及任务记录。"""
_require_huya_perm(current, "huya:delete")
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
if not ids:
raise HTTPException(status_code=400, detail="无效的账号ID")
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
db.commit()
return {"message": f"已删除 {deleted} 个虎牙账号", "deleted": deleted, "success": True}
@router.delete("/accounts/{account_id}")
def delete_account(
account_id: int,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""删除单个虎牙 CK 账号。"""
_require_huya_perm(current, "huya:delete")
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(synchronize_session=False)
db.delete(account)
db.commit()
return {"message": "已删除", "success": True}
@router.put("/accounts/{account_id}/assign")
def assign_account(
account_id: int,
req: AccountAssign,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""分配单个虎牙账号给客服。"""
_require_huya_perm(current, "huya:assign")
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
if req.assigned_to:
target = db.query(User).filter(User.id == req.assigned_to).first()
if not target:
raise HTTPException(status_code=404, detail="目标用户不存在")
if target.role != "support":
raise HTTPException(status_code=400, detail="只能分配给客服角色")
account.assigned_to = req.assigned_to
db.commit()
return {"message": "已分配", "success": True}
@router.post("/accounts/batch-assign")
def batch_assign_accounts(
req: BatchAssign,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""批量分配/取消分配虎牙账号给客服。"""
_require_huya_perm(current, "huya:assign")
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择账号")
if req.assigned_to is not None:
target = db.query(User).filter(User.id == req.assigned_to).first()
if not target:
raise HTTPException(status_code=404, detail="目标用户不存在")
if target.role != "support":
raise HTTPException(status_code=400, detail="只能分配给客服角色")
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update(
{HuyaAccount.assigned_to: req.assigned_to},
synchronize_session=False,
)
db.commit()
action = "分配" if req.assigned_to else "取消分配"
return {"message": f"已批量{action} {count} 个虎牙账号", "success": True, "count": count}
@router.get("/accounts/assignments/summary")
def assignments_summary(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""虎牙分配概览:每个客服分配了多少虎牙账号。"""
_require_huya_perm(current, "huya:assign")
results = (
db.query(User.id, User.username, func.count(HuyaAccount.id).label("count"))
.outerjoin(HuyaAccount, HuyaAccount.assigned_to == User.id)
.filter(User.role == "support")
.group_by(User.id, User.username)
.order_by(func.count(HuyaAccount.id).desc())
.all()
)
total_unassigned = (
db.query(func.count(HuyaAccount.id))
.filter(HuyaAccount.assigned_to.is_(None))
.scalar()
) or 0
return {
"support_users": [
{"id": uid, "username": uname, "assigned_count": cnt}
for uid, uname, cnt in results
],
"unassigned_count": total_unassigned,
}
@router.put("/accounts/{account_id}/tag")
def set_account_tag(
account_id: int,
req: AccountTag,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""设置单个虎牙账号标签。"""
_require_huya_perm(current, "huya:import")
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
account.tag = (req.tag or "").strip()
db.commit()
return {"message": "标签已更新", "success": True}
@router.put("/accounts/batch-tag")
def batch_tag(
req: AccountTag,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""批量设置虎牙账号标签。"""
_require_huya_perm(current, "huya:import")
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择账号")
tag = (req.tag or "").strip()
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update(
{HuyaAccount.tag: tag},
synchronize_session=False,
)
db.commit()
return {"message": f"已为 {count} 个虎牙账号设置标签", "success": True, "count": count}
@router.get("/accounts/tags/list")
def list_tags(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""获取虎牙账号标签列表。"""
tag_query = db.query(HuyaAccount.tag)
if not _can_view_huya_all(current):
if _can_view_huya_assigned(current):
tag_query = tag_query.filter(HuyaAccount.assigned_to == current.id)
else:
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
tags = tag_query.filter(HuyaAccount.tag != "", HuyaAccount.tag.isnot(None)).distinct().all()
return [item[0] for item in tags if item[0]]
@router.get("/cookies")
def list_cookies(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""查看当前用户可见的虎牙 Cookie。"""
include_cookie = _can_view_huya_cookie(current)
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "").order_by(HuyaAccount.updated_at.desc()).all()
return [_cookie_out(account, include_cookie=include_cookie) for account in accounts]
@router.get("/cookies/export")
def export_cookies(
format: str = "csv",
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""导出虎牙 Cookie,支持 csv 和 custom 格式。"""
_require_huya_perm(current, "huya:cookie:export")
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "").order_by(HuyaAccount.updated_at.desc()).all()
if format == "custom":
lines = []
for account in accounts:
username = account.username or account.nickname or account.uid or ""
lines.append(f"{username}----{normalize_huya_cookie(account.cookie or '')}")
content = "\r\n".join(lines)
filename = "huya_cookies_custom.txt"
media = "text/plain"
else:
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["账号", "UID", "YYUID", "Cookie", "时间"])
for account in accounts:
username = account.nickname or account.username or account.uid or ""
writer.writerow([
username,
account.uid or "",
account.yyuid or "",
normalize_huya_cookie(account.cookie or ""),
account.updated_at.isoformat() if account.updated_at else "",
])
content = output.getvalue()
filename = "huya_cookies.csv"
media = "text/csv"
return StreamingResponse(
iter([content]),
media_type=media,
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@router.delete("/cookies/batch")
def delete_cookies_batch(
account_ids: str = "",
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""批量清除虎牙 Cookie,保留账号记录。"""
_require_huya_perm(current, "huya:cookie:export")
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
if not ids:
raise HTTPException(status_code=400, detail="无效的账号ID")
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(ids)).all()
for account in accounts:
account.cookie = ""
account.status = "invalid"
db.commit()
return {"message": f"已清除 {len(accounts)} 条虎牙 Cookie", "deleted": len(accounts), "success": True}
@router.delete("/cookies/{account_id}")
def delete_cookie(
account_id: int,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""清除单个虎牙 Cookie,保留账号记录。"""
_require_huya_perm(current, "huya:cookie:export")
account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="记录不存在")
account.cookie = ""
account.status = "invalid"
db.commit()
return {"message": "已清除", "success": True}
@router.get("/config", response_model=HuyaConfigOut)
def get_config(
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:config")),
):
"""获取虎牙配置。"""
config = ensure_huya_config(db)
return _config_out(config)
@router.put("/config", response_model=HuyaConfigOut)
def update_config(
req: HuyaConfigUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:config")),
):
"""更新虎牙配置。"""
config = ensure_huya_config(db)
for field in HUYA_CONFIG_FIELDS:
value = getattr(req, field)
if value is not None:
setattr(config, field, value.strip())
apply_huya_config_defaults(config)
config.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(config)
return _config_out(config)
@router.get("/goods", response_model=list[HuyaGoodsOut])
def list_goods(
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""查看已缓存的虎牙商品快照。"""
rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.id.asc()).all()
return rows
@router.get("/recharge-goods", response_model=list[HuyaRechargeGoodsOut])
def list_recharge_goods(
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""查看已缓存的虎牙充值商品快照。"""
rows = db.query(HuyaRechargeGoodsSnapshot).order_by(HuyaRechargeGoodsSnapshot.id.asc()).all()
return rows
@router.post("/tasks/batch")
async def create_task_batch(
req: HuyaTaskBatchRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""创建虎牙任务记录并启动后台执行器。"""
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
try:
batch_id, count = create_planned_tasks(
db,
req.account_ids,
req.task_type,
current.id,
req.payload,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if count == 0:
raise HTTPException(status_code=400, detail="没有有效的虎牙账号")
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
thread_db = SessionLocal()
runner = HuyaBatchRunner(
db=thread_db,
batch_id=batch_id,
task_type=req.task_type,
payload=req.payload,
log_queue=log_queue,
loop=loop,
concurrency=req.concurrency,
)
huya_batch_registry.register(batch_id, log_queue, loop, runner)
thread = threading.Thread(target=runner.run, daemon=True)
thread.start()
return {"batch_id": batch_id, "count": count, "success": True}
@router.get("/tasks", response_model=list[HuyaTaskOut])
def list_tasks(
batch_id: str | None = None,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""查看虎牙任务记录。"""
query = db.query(HuyaTask).options(joinedload(HuyaTask.account))
if batch_id:
query = query.filter(HuyaTask.batch_id == batch_id)
tasks = query.order_by(HuyaTask.id.desc()).limit(300).all()
return [_task_out(task) for task in tasks]
@router.post("/stop/{batch_id}")
def stop_batch(
batch_id: str,
current: User = Depends(require_permission("huya:task")),
):
"""停止正在运行的虎牙批次。"""
batch = huya_batch_registry.get(batch_id)
if batch:
batch["runner"].stop()
return {"message": "已发送停止信号", "success": True}
raise HTTPException(status_code=404, detail="批次不存在或已结束")
@router.websocket("/ws/{batch_id}")
async def ws_huya_logs(websocket: WebSocket, batch_id: str):
"""虎牙实时日志推送通道。"""
user = authenticate_websocket(websocket)
if not user:
await websocket.close(code=1008, reason="未授权")
return
await websocket.accept()
batch = huya_batch_registry.get(batch_id)
if not batch:
await websocket.send_json({"level": "error", "message": "批次不存在或已结束"})
await websocket.close()
return
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)
if msg.get("level") == "result":
await asyncio.sleep(0.1)
break
except asyncio.TimeoutError:
await websocket.send_json({"level": "heartbeat", "message": ""})
except WebSocketDisconnect:
pass
finally:
huya_batch_registry.pop(batch_id)