This commit is contained in:
Your Name
2026-07-28 09:00:19 +08:00
parent 8ba13a8ff9
commit 153db97dc7
14 changed files with 793 additions and 75 deletions
+135 -37
View File
@@ -20,7 +20,7 @@ from settings import CORS_ORIGINS, SERVE_WEB, STATIC_DIR
from help_pages import serve_credential_tool
from web_static import mount_frontend
from pydantic import BaseModel, Field
from sqlalchemy import select, update, delete, text, func, case
from sqlalchemy import select, update, delete, text, func, case, or_, cast, String
from sqlalchemy.ext.asyncio import AsyncSession
from models.database import engine, Base, get_db, AsyncSessionLocal
@@ -1080,42 +1080,91 @@ async def get_accounts(
支持 q(昵称/抖音ID/手机号/账号ID 搜索)与 status 筛选。
Cookie 解析等重逻辑只对当前页执行。
"""
result = await db.execute(accounts_for_user(user))
accounts = result.scalars().all()
# 更新内存中的运行状态与数据库同步,以防异常断开
for acc in accounts:
if page is None:
result = await db.execute(accounts_for_user(user))
accounts = result.scalars().all()
# 兼容旧的全量接口行为。
for acc in accounts:
is_running = manager.is_running(acc.id)
if is_running and acc.status == "offline":
acc.status = "online"
elif not is_running and acc.status in ("online", "logging_in", "starting"):
acc.status = "offline"
return [_build_account_response(acc) for acc in accounts]
keyword = (q or "").strip().lower()
status_filter = (status or "").strip()
stmt = accounts_for_user(user)
running_account_ids = [
int(account_id)
for account_id, worker in list(manager.workers.items())
if worker and worker.is_running
]
runtime_running = Account.id.in_(running_account_ids)
effective_status = case(
(
runtime_running & (Account.status == "offline"),
"online",
),
(
(~runtime_running)
& Account.status.in_(("online", "logging_in", "starting")),
"offline",
),
else_=Account.status,
)
if keyword:
like_value = f"%{keyword}%"
search_conditions = [
func.lower(Account.username).like(like_value),
func.lower(Account.douyin_uid).like(like_value),
func.lower(Account.phone).like(like_value),
func.lower(Account.user_agent).like(like_value),
cast(Account.id, String).like(like_value),
]
matching_profiles = [
profile
for profile in list_device_profiles()
if keyword in str(profile.get("label") or "").lower()
or keyword in str(profile.get("platform") or "").lower()
]
if matching_profiles:
matching_uas = [profile["user_agent"] for profile in matching_profiles]
search_conditions.append(Account.user_agent.in_(matching_uas))
if any(profile.get("id") == "chrome_win120" for profile in matching_profiles):
search_conditions.append(Account.user_agent.is_(None))
stmt = stmt.where(or_(*search_conditions))
if status_filter and status_filter != "all":
if status_filter == "quota_disabled":
stmt = stmt.where(Account.quota_disabled.is_(True))
else:
stmt = stmt.where(
Account.quota_disabled.is_not(True),
effective_status == status_filter,
)
total_result = await db.execute(
stmt.with_only_columns(func.count(Account.id)).order_by(None)
)
total = int(total_result.scalar_one() or 0)
result = await db.execute(
stmt.order_by(Account.id.asc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = result.scalars().all()
# Only reconcile the current page. The old implementation hydrated every
# account including large Cookie/IM blobs on every refresh, which became
# visibly slow beyond a few hundred accounts.
for acc in items:
is_running = manager.is_running(acc.id)
if is_running and acc.status == "offline":
acc.status = "online"
elif not is_running and acc.status in ("online", "logging_in", "starting"):
acc.status = "offline"
if page is None:
return [_build_account_response(acc) for acc in accounts]
keyword = (q or "").strip().lower()
status_filter = (status or "").strip()
filtered = [
acc
for acc in accounts
if _account_matches_keyword(acc, keyword)
and (
not status_filter
or status_filter == "all"
or (
status_filter == "quota_disabled"
and bool(acc.quota_disabled)
)
or (
status_filter != "quota_disabled"
and not acc.quota_disabled
and acc.status == status_filter
)
)
]
total = len(filtered)
start = (page - 1) * page_size
items = filtered[start : start + page_size]
return {
"items": [_build_account_response(acc) for acc in items],
"total": total,
@@ -1330,14 +1379,55 @@ async def update_account(
@app.get("/api/reply-queues", response_model=ReplyQueueSummaryResponse)
async def get_reply_queue_summaries(
account_ids: Optional[str] = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""聚合当前用户可见账号的排队数量,供账号页低频轮询"""
allowed_ids = await owned_account_ids(db, user)
"""聚合可见账号的排队数量账号页可限定为当前页账号"""
requested_ids: list[int] | None = None
if account_ids is not None:
requested_ids = []
seen_ids: set[int] = set()
for raw in str(account_ids).split(","):
token = raw.strip()
if not token:
continue
try:
account_id = int(token)
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="账号编号格式错误")
if account_id > 0 and account_id not in seen_ids:
seen_ids.add(account_id)
requested_ids.append(account_id)
if len(requested_ids) > 100:
raise HTTPException(status_code=400, detail="单次最多查询 100 个账号的回复队列")
allowed_ids: set[int] | None
if requested_ids is None:
allowed_ids = await owned_account_ids(db, user)
worker_entries = list(manager.workers.items())
else:
if is_admin(user.role):
allowed_ids = set(requested_ids)
elif requested_ids:
owned_result = await db.execute(
select(Account.id).where(
Account.owner_id == user.id,
Account.id.in_(requested_ids),
)
)
allowed_ids = {int(row[0]) for row in owned_result.all()}
else:
allowed_ids = set()
worker_entries = [
(account_id, manager.workers.get(account_id))
for account_id in requested_ids
if account_id in allowed_ids and account_id in manager.workers
]
summaries: list[ReplyQueueSummaryItem] = []
total_pending = 0
for account_id, worker in list(manager.workers.items()):
for account_id, worker in worker_entries:
if allowed_ids is not None and account_id not in allowed_ids:
continue
service = worker._im_service if worker else None
@@ -1688,7 +1778,11 @@ async def validate_account_credential(
account = await get_owned_account(db, user, account_id)
cookie_data = _get_account_cookie_data(account)
assessment = await assess_account_credential(cookie_data, account.im_session_data)
assessment = await assess_account_credential(
cookie_data,
account.im_session_data,
startup_priority=True,
)
return CredentialValidateResponse(**assessment)
@@ -1718,7 +1812,11 @@ async def _start_account_rpa_impl(
return {"status": "running", "message": "RPA worker is already running."}
cookie_data = _get_account_cookie_data(account)
assessment = await assess_account_credential(cookie_data, account.im_session_data)
assessment = await assess_account_credential(
cookie_data,
account.im_session_data,
startup_priority=True,
)
login_mode = requested_login_mode or assessment["login_mode"]
reset_performed = False