优化登录重试与配置清理
This commit is contained in:
@@ -24,21 +24,25 @@ def _fmt_dt(dt) -> str | None:
|
||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
||||
|
||||
|
||||
def _visible_cookie_tasks_query(db: Session, current: User):
|
||||
"""返回当前用户可见的成功 Cookie 任务查询。"""
|
||||
query = db.query(LoginTask).filter(LoginTask.status == "success")
|
||||
|
||||
# 无全量登录任务权限时,只能操作分配给自己的账号 Cookie。
|
||||
if not user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id).filter(
|
||||
Account.assigned_to == current.id
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
@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 user_has_permission(current, "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()
|
||||
tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all()
|
||||
|
||||
# 批量查账号,避免 N+1 查询
|
||||
account_ids = [t.account_id for t in tasks]
|
||||
@@ -81,9 +85,7 @@ def export_cookies(
|
||||
csv: 账号, Cookie, 时间
|
||||
custom: 账号----密码----ck
|
||||
"""
|
||||
tasks = db.query(LoginTask).filter(
|
||||
LoginTask.status == "success"
|
||||
).order_by(LoginTask.finished_at.desc()).all()
|
||||
tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all()
|
||||
|
||||
# 批量查账号,避免 N+1
|
||||
account_ids = [t.account_id for t in tasks]
|
||||
@@ -136,7 +138,7 @@ def delete_cookies_batch(
|
||||
ids = [int(x) for x in task_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="无效的ID")
|
||||
tasks = db.query(LoginTask).filter(LoginTask.id.in_(ids)).all()
|
||||
tasks = _visible_cookie_tasks_query(db, current).filter(LoginTask.id.in_(ids)).all()
|
||||
for t in tasks:
|
||||
t.cookie = ""
|
||||
t.status = "failed"
|
||||
@@ -152,7 +154,7 @@ def delete_cookie(
|
||||
current: User = Depends(require_permission("cookie:export")),
|
||||
):
|
||||
"""删除一条 Cookie 记录。"""
|
||||
task = db.query(LoginTask).filter(LoginTask.id == task_id).first()
|
||||
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
task.cookie = ""
|
||||
|
||||
@@ -54,7 +54,6 @@ async def create_batch(
|
||||
account_ids=valid_ids,
|
||||
created_by=current.id,
|
||||
creator_permissions=get_user_permissions(current),
|
||||
max_geetest_retries=req.max_geetest_retries,
|
||||
max_proxy_retries=req.max_proxy_retries,
|
||||
max_login_retries=req.max_login_retries,
|
||||
max_total_time=req.max_total_time,
|
||||
|
||||
@@ -124,10 +124,9 @@ class AccountOut(BaseModel):
|
||||
# ---- 登录任务 ----
|
||||
class LoginBatchRequest(BaseModel):
|
||||
account_ids: list[int]
|
||||
max_geetest_retries: int = 5
|
||||
max_proxy_retries: int = 0 # 代理切换次数,0=无限切换直到成功
|
||||
max_login_retries: int = 3 # 登录整体重试次数(换代理从头重跑)
|
||||
max_total_time: float = 300 # 单账号登录总时长上限(秒),超时则放弃
|
||||
max_login_retries: int = 0 # 登录整体重试次数,0=无限重试直到成功
|
||||
max_total_time: float = 0 # 单账号登录总时长上限,0=不限制
|
||||
concurrency: int = 3 # 并发数,1-10
|
||||
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@ class LoginBatchRunner:
|
||||
account_ids: list[int],
|
||||
created_by: int,
|
||||
creator_permissions: list[str],
|
||||
max_geetest_retries: int = 5,
|
||||
max_proxy_retries: int = 0,
|
||||
max_login_retries: int = 3,
|
||||
max_total_time: float = 300,
|
||||
max_login_retries: int = 0,
|
||||
max_total_time: float = 0,
|
||||
proxy_config: Optional[ProxyConfigModel] = None,
|
||||
log_queue: Optional[asyncio.Queue] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
@@ -41,7 +40,6 @@ class LoginBatchRunner:
|
||||
self.account_ids = account_ids
|
||||
self.created_by = created_by
|
||||
self.creator_permissions = creator_permissions
|
||||
self.max_geetest_retries = max_geetest_retries
|
||||
self.max_proxy_retries = max_proxy_retries
|
||||
self.max_login_retries = max_login_retries
|
||||
self.max_total_time = max_total_time
|
||||
@@ -200,7 +198,6 @@ class LoginBatchRunner:
|
||||
loginer = DouyuLogin(
|
||||
account,
|
||||
proxy=proxy_dict,
|
||||
max_geetest_retries=self.max_geetest_retries,
|
||||
max_proxy_retries=self.max_proxy_retries,
|
||||
max_login_retries=self.max_login_retries,
|
||||
max_total_time=self.max_total_time,
|
||||
|
||||
@@ -12,7 +12,8 @@ import ProxyPage from './pages/ProxyPage';
|
||||
import UsersPage from './pages/UsersPage';
|
||||
import CookiePage from './pages/CookiePage';
|
||||
import { getUser } from './store/auth';
|
||||
import { ThemeProvider, useTheme } from './store/theme';
|
||||
import { ThemeProvider } from './store/theme';
|
||||
import { useTheme } from './store/useTheme';
|
||||
|
||||
function AppContent() {
|
||||
// 用 state 驱动重渲染,登录/登出时调 refreshAuth()
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { BatchLoginResult, LoginTaskItem, MessageDeletedResponse, MessageRe
|
||||
|
||||
interface CreateBatchParams {
|
||||
account_ids: number[];
|
||||
max_geetest_retries?: number;
|
||||
concurrency?: number;
|
||||
max_proxy_retries?: number;
|
||||
max_login_retries?: number;
|
||||
max_total_time?: number;
|
||||
}
|
||||
|
||||
@@ -121,33 +121,6 @@ export interface ProxyTestResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// ==================== Log ====================
|
||||
|
||||
export interface HttpLogEntry {
|
||||
timestamp: string;
|
||||
ts: number;
|
||||
category: string;
|
||||
tag: string;
|
||||
method: string;
|
||||
url: string;
|
||||
proxy: string | null;
|
||||
request: { headers: Record<string, string>; body: string };
|
||||
response: { status_code: number | null; headers: Record<string, string>; body: string };
|
||||
duration_ms: number | null;
|
||||
error: string | null;
|
||||
level: string;
|
||||
}
|
||||
|
||||
export interface HttpLogListResult {
|
||||
items: HttpLogEntry[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface HttpLogClearResult {
|
||||
success: boolean;
|
||||
cleared: number;
|
||||
}
|
||||
|
||||
// ==================== Permissions ====================
|
||||
|
||||
export interface PermissionsListResult {
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { getUser, clearAuth, type AuthUser } from '../store/auth';
|
||||
import { authApi } from '../api/modules';
|
||||
import { useTheme, type ThemeMode } from '../store/theme';
|
||||
import { type ThemeMode } from '../store/theme';
|
||||
import { useTheme } from '../store/useTheme';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
@@ -24,7 +24,11 @@ function copyToClipboard(text: string): Promise<void> {
|
||||
try {
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
ok ? resolve() : reject(new Error('execCommand copy failed'));
|
||||
if (ok) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('execCommand copy failed'));
|
||||
}
|
||||
} catch (e) {
|
||||
document.body.removeChild(ta);
|
||||
reject(e);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { authApi } from '../api/modules';
|
||||
import { setAuth, type AuthUser } from '../store/auth';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { useTheme } from '../store/theme';
|
||||
import { useTheme } from '../store/useTheme';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Table, Button, Select, message, Tag, Space, InputNumber, Tooltip, Popconfirm, theme,
|
||||
} from 'antd';
|
||||
import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { accountApi, loginApi, type AccountItem, type LoginTaskItem } from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
@@ -42,14 +42,9 @@ export default function LoginTasksPage() {
|
||||
const v = localStorage.getItem('login_concurrency');
|
||||
return v ? Math.max(1, Math.min(10, Number(v) || 3)) : 3;
|
||||
});
|
||||
const [maxProxyRetries, setMaxProxyRetries] = useState(() => {
|
||||
const v = localStorage.getItem('login_max_proxy_retries');
|
||||
return v ? Math.max(0, Number(v) || 0) : 0;
|
||||
});
|
||||
|
||||
// 值变化时自动持久化
|
||||
useEffect(() => { localStorage.setItem('login_concurrency', String(concurrency)); }, [concurrency]);
|
||||
useEffect(() => { localStorage.setItem('login_max_proxy_retries', String(maxProxyRetries)); }, [maxProxyRetries]);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs();
|
||||
const { can } = usePermissions();
|
||||
@@ -145,11 +140,7 @@ export default function LoginTasksPage() {
|
||||
try {
|
||||
const result = await loginApi.createBatch({
|
||||
account_ids: accountIds,
|
||||
max_geetest_retries: 5,
|
||||
concurrency,
|
||||
max_proxy_retries: maxProxyRetries,
|
||||
max_login_retries: 3,
|
||||
max_total_time: 300,
|
||||
});
|
||||
setBatchId(result.batch_id);
|
||||
message.success(`已创建登录任务,共 ${result.count} 个账号`);
|
||||
@@ -347,19 +338,6 @@ export default function LoginTasksPage() {
|
||||
/>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
<Tooltip title="代理切换次数,0=无限切换直到成功">
|
||||
<Space size={4}>
|
||||
<SwapOutlined style={{ color: token.colorTextSecondary }} />
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={999}
|
||||
value={maxProxyRetries}
|
||||
onChange={(v) => setMaxProxyRetries(v ?? 10)}
|
||||
style={{ width: 55 }}
|
||||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import {
|
||||
DEFAULT_THEME_MODE,
|
||||
THEME_STORAGE_KEY,
|
||||
ThemeContext,
|
||||
type ThemeMode,
|
||||
} from './themeContext';
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
const STORAGE_KEY = 'theme_mode';
|
||||
const DEFAULT_MODE: ThemeMode = 'system';
|
||||
|
||||
interface ThemeContextValue {
|
||||
mode: ThemeMode;
|
||||
isDark: boolean;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
mode: DEFAULT_MODE,
|
||||
isDark: false,
|
||||
setMode: () => {},
|
||||
toggle: () => {},
|
||||
});
|
||||
export type { ThemeMode } from './themeContext';
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [mode, setModeState] = useState<ThemeMode>(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return (saved as ThemeMode) || DEFAULT_MODE;
|
||||
const saved = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
return (saved as ThemeMode) || DEFAULT_THEME_MODE;
|
||||
});
|
||||
|
||||
// 监听系统主题变化
|
||||
@@ -41,7 +30,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const setMode = useCallback((newMode: ThemeMode) => {
|
||||
setModeState(newMode);
|
||||
localStorage.setItem(STORAGE_KEY, newMode);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newMode);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
@@ -59,7 +48,3 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
export const DEFAULT_THEME_MODE: ThemeMode = 'system';
|
||||
export const THEME_STORAGE_KEY = 'theme_mode';
|
||||
|
||||
export interface ThemeContextValue {
|
||||
mode: ThemeMode;
|
||||
isDark: boolean;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<ThemeContextValue>({
|
||||
mode: DEFAULT_THEME_MODE,
|
||||
isDark: false,
|
||||
setMode: () => {},
|
||||
toggle: () => {},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from './themeContext';
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
Reference in New Issue
Block a user