fix: 修复proxy_service和account_service中不当的顶层import
- proxy_service.py: 将requests和WhitelistManager改为函数内延迟import, 避免启动时加载不需要的依赖;移除未使用的get_exit_ip_via_proxy导入 - account_service.py: 移除未使用的func、joinedload、AuditLog、 user_has_permission顶层导入
This commit is contained in:
@@ -3,9 +3,10 @@ import {
|
||||
Table, Button, Modal, Input, Select, message, Popconfirm, Typography, Tag, Space,
|
||||
Row, Col, Card, Statistic,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import { accountApi, userApi, type AccountItem, type UserInfo } from '../api/modules';
|
||||
import { getUser, hasPerm } from '../store/auth';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@@ -27,12 +28,12 @@ export default function AccountsPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchTagInput, setBatchTagInput] = useState('');
|
||||
const [batchTagVisible, setBatchTagVisible] = useState(false);
|
||||
const user = getUser();
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canViewAll = hasPerm(user, 'account:view_all');
|
||||
const canImport = hasPerm(user, 'account:import');
|
||||
const canAssign = hasPerm(user, 'account:assign');
|
||||
const canDelete = hasPerm(user, 'account:delete');
|
||||
const canViewAll = can('account:view_all');
|
||||
const canImport = can('account:import');
|
||||
const canAssign = can('account:assign');
|
||||
const canDelete = can('account:delete');
|
||||
|
||||
const loadAccounts = async () => {
|
||||
setLoading(true);
|
||||
@@ -52,14 +53,18 @@ export default function AccountsPage() {
|
||||
try {
|
||||
const data = await userApi.list();
|
||||
setUsers(data.filter((u) => u.role === 'support'));
|
||||
} catch {}
|
||||
} catch {
|
||||
// 忽略客服列表加载失败,账号列表仍可继续使用。
|
||||
}
|
||||
};
|
||||
|
||||
const loadTags = async () => {
|
||||
try {
|
||||
const data = await accountApi.listTags();
|
||||
setTags(data);
|
||||
} catch {}
|
||||
} catch {
|
||||
// 忽略标签加载失败,页面会退化为无标签筛选。
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -167,7 +172,7 @@ export default function AccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
const columns: TableProps<AccountItem>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function AssignmentsPage() {
|
||||
try {
|
||||
const data = await accountApi.assignmentsSummary();
|
||||
setSupportUsers(data.support_users);
|
||||
return data.support_users as SupportUser[];
|
||||
return data.support_users;
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
return [];
|
||||
@@ -54,7 +54,7 @@ export default function AssignmentsPage() {
|
||||
const users = await loadSummary();
|
||||
if (users.length > 0) {
|
||||
const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER);
|
||||
const savedUser = savedId ? users.find((u: SupportUserItemItem) => u.id === Number(savedId)) : null;
|
||||
const savedUser = savedId ? users.find((u: SupportUserItem) => u.id === Number(savedId)) : null;
|
||||
setSelectedUser(savedUser || users[0]);
|
||||
}
|
||||
};
|
||||
@@ -394,4 +394,4 @@ export default function AssignmentsPage() {
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd';
|
||||
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { cookieApi, type CookieItem } from '../api/modules';
|
||||
import { getUser, hasPerm } from '../store/auth';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
@@ -14,10 +14,10 @@ export default function CookiePage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const user = getUser();
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canView = hasPerm(user, 'cookie:view');
|
||||
const canExport = hasPerm(user, 'cookie:export');
|
||||
const canView = can('cookie:view');
|
||||
const canExport = can('cookie:export');
|
||||
|
||||
const loadCookies = async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ReloadOutlined, DeleteOutlined, SearchOutlined, EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { logApi, type HttpLogEntry } from '../api/modules';
|
||||
import { hasPerm, getUser } from '../store/auth';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
@@ -35,7 +35,7 @@ export default function HttpLogsPage() {
|
||||
const [level, setLevel] = useState<string | undefined>(undefined);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [detailEntry, setDetailEntry] = useState<HttpLogEntry | null>(null);
|
||||
const user = getUser();
|
||||
const { canAny } = usePermissions();
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -78,7 +78,7 @@ export default function HttpLogsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const canManage = user && (hasPerm(user, 'audit:view') || hasPerm(user, 'login:batch'));
|
||||
const canManage = canAny(['audit:view', 'login:batch']);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Table, Button, Select, message, Tag, Space, Spin, InputNumber, Tooltip, Popconfirm, theme,
|
||||
Table, Button, Select, message, Tag, Space, InputNumber, Tooltip, Popconfirm, theme,
|
||||
} from 'antd';
|
||||
import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DownOutlined, UpOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { accountApi, loginApi, type AccountItem, type LoginTaskItem } from '../api/modules';
|
||||
import { getUser, hasPerm } from '../store/auth';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
@@ -35,20 +37,16 @@ export default function LoginTasksPage() {
|
||||
const [tasks, setTasks] = useState<LoginTaskItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [batchId, setBatchId] = useState<string | null>(null);
|
||||
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [concurrency, setConcurrency] = useState(3);
|
||||
const [maxProxyRetries, setMaxProxyRetries] = useState(10);
|
||||
const [logVisible, setLogVisible] = useState(true);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||
const user = getUser();
|
||||
const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs();
|
||||
const { can } = usePermissions();
|
||||
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const canBatch = hasPerm(user, 'login:batch');
|
||||
const canBatch = can('login:batch');
|
||||
|
||||
// 从账号中提取所有标签
|
||||
const allTags = useMemo(() => {
|
||||
@@ -113,20 +111,15 @@ export default function LoginTasksPage() {
|
||||
try {
|
||||
const data = await loginApi.listTasks(batchId || undefined);
|
||||
setTasks(data);
|
||||
} catch {}
|
||||
} catch {
|
||||
// 忽略轮询失败,下一次定时刷新会继续尝试。
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadAccounts(), loadTasks()]);
|
||||
}, []);
|
||||
|
||||
// 日志自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (logVisible && logEndRef.current) {
|
||||
logEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [logs, logVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(loadTasks, 3000);
|
||||
return () => clearInterval(timer);
|
||||
@@ -139,31 +132,15 @@ export default function LoginTasksPage() {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setLogs([]);
|
||||
try {
|
||||
const result = await loginApi.createBatch(accountIds, 5, concurrency, maxProxyRetries);
|
||||
setBatchId(result.batch_id);
|
||||
message.success(`已创建登录任务,共 ${result.count} 个账号`);
|
||||
|
||||
// 连接 WebSocket
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/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);
|
||||
setBatchId(null);
|
||||
};
|
||||
ws.onerror = () => {
|
||||
setWsConnected(false);
|
||||
};
|
||||
connectLogs(`/api/login/ws/login/${result.batch_id}`, {
|
||||
onClose: () => setBatchId(null),
|
||||
onResult: () => setBatchId(null),
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -432,50 +409,13 @@ export default function LoginTasksPage() {
|
||||
</div>
|
||||
|
||||
{/* 实时日志 - 底部可折叠 */}
|
||||
<div style={{ flexShrink: 0, borderTop: `1px solid ${token.colorBorderSecondary}`, marginTop: 4 }}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', padding: '4px 0', userSelect: 'none' }}
|
||||
onClick={() => setLogVisible((v) => !v)}
|
||||
>
|
||||
<span style={{ fontWeight: 500, fontSize: 13 }}>实时日志</span>
|
||||
{logVisible ? <UpOutlined style={{ marginLeft: 6, fontSize: 10 }} /> : <DownOutlined style={{ marginLeft: 6, fontSize: 10 }} />}
|
||||
{logs.length > 0 && <span style={{ marginLeft: 8, fontSize: 12, color: token.colorTextTertiary }}>{logs.length} 条</span>}
|
||||
{wsConnected && <Tag color="processing" style={{ marginLeft: 8 }}>连接中</Tag>}
|
||||
</div>
|
||||
{logVisible && (
|
||||
<div
|
||||
style={{
|
||||
height: '20vh',
|
||||
overflow: 'auto',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
padding: 4,
|
||||
backgroundColor: token.colorBgLayout,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<Spin spinning={wsConnected} size="small" />
|
||||
) : (
|
||||
logs.map((log, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
color:
|
||||
log.level === 'error' ? token.colorError :
|
||||
log.level === 'success' ? token.colorSuccess :
|
||||
log.level === 'warning' ? token.colorWarning :
|
||||
token.colorText,
|
||||
}}
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RealtimeLogPanel
|
||||
logs={logs}
|
||||
connected={wsConnected}
|
||||
collapsible
|
||||
spinWhenEmpty
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { Form, Input, Switch, Button, Card, message, Row, Col, theme } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Input, Switch, Button, Card, message, Row, Col } from 'antd';
|
||||
import { proxyApi, type ProxyConfig } from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const WS_BASE = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}`;
|
||||
|
||||
export default function ProxyPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testingWl, setTestingWl] = useState(false);
|
||||
const [configLoaded, setConfigLoaded] = useState(false);
|
||||
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const { logs, connect: connectLogs, close: closeLogs } = useWebSocketLogs();
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
@@ -37,30 +35,21 @@ export default function ProxyPage() {
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
return () => {
|
||||
wsRef.current?.close();
|
||||
closeLogs();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const appendLog = (level: string, msg: string) => {
|
||||
setLogs((prev) => [...prev, { level, message: msg }]);
|
||||
};
|
||||
|
||||
const connectWs = (testId: string) => {
|
||||
wsRef.current?.close();
|
||||
setLogs([]);
|
||||
const ws = new WebSocket(`${WS_BASE}/api/proxy/ws/test/${testId}`);
|
||||
wsRef.current = ws;
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.level === 'heartbeat') return;
|
||||
if (msg.level === 'result') return;
|
||||
appendLog(msg.level, msg.message);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
wsRef.current = null;
|
||||
setTesting(false);
|
||||
setTestingWl(false);
|
||||
};
|
||||
connectLogs(`/api/proxy/ws/test/${testId}`, {
|
||||
onClose: () => {
|
||||
setTesting(false);
|
||||
setTestingWl(false);
|
||||
},
|
||||
onResult: () => {
|
||||
setTesting(false);
|
||||
setTestingWl(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -78,7 +67,6 @@ export default function ProxyPage() {
|
||||
|
||||
const handleTestProxy = async () => {
|
||||
setTesting(true);
|
||||
setLogs([]);
|
||||
try {
|
||||
const result = await proxyApi.test();
|
||||
if (result.test_id) connectWs(result.test_id);
|
||||
@@ -90,7 +78,6 @@ export default function ProxyPage() {
|
||||
|
||||
const handleTestWhitelist = async () => {
|
||||
setTestingWl(true);
|
||||
setLogs([]);
|
||||
try {
|
||||
const result = await proxyApi.testWhitelist();
|
||||
if (result.test_id) connectWs(result.test_id);
|
||||
@@ -101,13 +88,6 @@ export default function ProxyPage() {
|
||||
};
|
||||
|
||||
|
||||
const logColors: Record<string, string> = {
|
||||
error: token.colorError,
|
||||
success: token.colorSuccess,
|
||||
warning: token.colorWarning,
|
||||
info: token.colorText,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
|
||||
@@ -166,22 +146,15 @@ export default function ProxyPage() {
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
<Card
|
||||
<RealtimeLogPanel
|
||||
mode="card"
|
||||
logs={logs}
|
||||
title="实时日志"
|
||||
size="small"
|
||||
style={{ flex: 1, overflow: 'hidden' }}
|
||||
styles={{ body: { height: '100%', overflow: 'auto', fontFamily: 'monospace', fontSize: 12, padding: '8px 16px' } }}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<span style={{ color: token.colorTextTertiary }}>点击"测试代理"或"测试白名单"查看日志</span>
|
||||
) : (
|
||||
logs.map((log, i) => (
|
||||
<div key={i} style={{ color: logColors[log.level] || token.colorText, lineHeight: '20px' }}>
|
||||
{log.message}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Card>
|
||||
emptyText={'点击"测试代理"或"测试白名单"查看日志'}
|
||||
height="100%"
|
||||
bodyStyle={{ minHeight: 160 }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
import { userApi, type UserInfo } from '../api/modules';
|
||||
import { getUser, hasPerm } from '../store/auth';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
@@ -50,8 +50,9 @@ export default function UsersPage() {
|
||||
const [selectedPerms, setSelectedPerms] = useState<string[]>([]);
|
||||
const [useCustom, setUseCustom] = useState(false);
|
||||
const [permLoading, setPermLoading] = useState(false);
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canAssignPerm = hasPerm(getUser(), 'user:assign_permissions');
|
||||
const canAssignPerm = can('user:assign_permissions');
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
|
||||
Reference in New Issue
Block a user