refactor: 消除前端52处any类型+删除冗余requirements.txt+调试print改日志

- 删除 requirements.txt,pyproject.toml 为唯一依赖源(含 requests[socks] extra)
- core/geetest 下 5 处 print() 替换为 loguru logger.debug()
- api/modules.ts 新增 13 个响应接口,消除所有 api.xxx<any, any>
- 新建 utils/error.ts 提供 getErrorMessage(e: unknown) 安全取消息
- 11 个页面组件 catch (e: any) 改为 catch (e: unknown)
- useState<any[]>、columns: any[]、render 参数全部类型化
- TypeScript 类型检查零错误通过
This commit is contained in:
yml2213
2026-06-23 07:14:08 +08:00
parent 453a637480
commit 12c31c2e09
16 changed files with 305 additions and 189 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import requests
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image from PIL import Image
from loguru import logger
REQUEST_TIMEOUT = (3.05, 12) REQUEST_TIMEOUT = (3.05, 12)
@@ -60,7 +61,7 @@ def restore_geetest_image(input_path:str, output_path:str) -> None:
new_img.paste(l, box=(_ % 26 * 10, 80 if _ > 25 else 0)) new_img.paste(l, box=(_ % 26 * 10, 80 if _ > 25 else 0))
new_img.save(output_path) new_img.save(output_path)
print(f"图像已还原并保存到: {output_path}") logger.debug("图像已还原并保存到: {}", output_path)
# 下载图片 # 下载图片
def download_picture(bg:str, fullbg:str, slice:str) -> int: def download_picture(bg:str, fullbg:str, slice:str) -> int:
+4 -8
View File
@@ -3,6 +3,7 @@ import requests
import json import json
import re import re
from typing import Mapping, Optional, Tuple from typing import Mapping, Optional, Tuple
from loguru import logger
REQUEST_TIMEOUT = (10, 30) REQUEST_TIMEOUT = (10, 30)
PASSPORT_REFERER = "https://passport.douyu.com/" PASSPORT_REFERER = "https://passport.douyu.com/"
@@ -263,13 +264,8 @@ def get_picture(gt:str, challenge:str) -> tuple[str, str, list[int], str, str, s
timeout=REQUEST_TIMEOUT, timeout=REQUEST_TIMEOUT,
) )
response.raise_for_status() response.raise_for_status()
print(response.text) logger.debug("极验 get.php 原始响应: {}", response.text[:500])
match = re.search(r'\((.*)\)$', response.text) match = re.search(r'\((.*)\)$', response.text)
if match:
json_str = match.group(1)
data = json.loads(json_str)
# 检查验证码类型
if 'data' in data and isinstance(data['data'], dict): if 'data' in data and isinstance(data['data'], dict):
# 新版极验格式 # 新版极验格式
inner_data = data['data'] inner_data = data['data']
@@ -317,12 +313,12 @@ def req_end(gt:str, challenge:str, w:str) -> dict:
timeout=REQUEST_TIMEOUT, timeout=REQUEST_TIMEOUT,
) )
response.raise_for_status() response.raise_for_status()
print(response.text) logger.debug("极验 ajax.php 原始响应: {}", response.text[:500])
match = re.search(r'\((.*)\)$', response.text) match = re.search(r'\((.*)\)$', response.text)
if match: if match:
json_str = match.group(1) json_str = match.group(1)
data = json.loads(json_str) data = json.loads(json_str)
print(f"极验验证响应: {data}") logger.debug("极验验证响应: {}", data)
# 检查验证是否成功 # 检查验证是否成功
if data.get('success') == 1: if data.get('success') == 1:
+2 -1
View File
@@ -1,6 +1,7 @@
import time import time
import random import random
import json import json
from loguru import logger
from core.geetest.common.trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \ from core.geetest.common.trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
H H
from core.geetest.common.crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5 from core.geetest.common.crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
@@ -106,7 +107,7 @@ def get_w3(str_16:str, challenge:str, hkjl:int, c:list[int], s:str, gt:str) -> s
trajectory = get_slide_track(hkjl)[0] trajectory = get_slide_track(hkjl)[0]
print(trajectory) logger.debug("滑动轨迹: {}", trajectory)
# trajectory = generator.generate(target_x) # trajectory = generator.generate(target_x)
userresponse = H(trajectory[-1][0], challenge) userresponse = H(trajectory[-1][0], challenge)
-13
View File
@@ -1,13 +0,0 @@
requests>=2.31.0
pycryptodome>=3.19.0
numpy>=1.24.0
opencv-python-headless>=4.8.0
Pillow>=10.0.0
loguru>=0.7.0
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
sqlalchemy>=2.0.0
python-jose[cryptography]>=3.3.0
bcrypt>=4.0.0
pydantic>=2.0.0
python-multipart>=0.0.9
+167 -26
View File
@@ -1,5 +1,22 @@
import api from './index'; import api from './index';
// ==================== 通用响应类型 ====================
interface MessageResponse {
message: string;
success: boolean;
}
interface MessageCountResponse extends MessageResponse {
count: number;
}
interface MessageDeletedResponse extends MessageResponse {
deleted: number;
}
// ==================== Auth ====================
export interface LoginResult { export interface LoginResult {
access_token: string; access_token: string;
token_type: string; token_type: string;
@@ -8,6 +25,16 @@ export interface LoginResult {
permissions: string[]; permissions: string[];
} }
export interface CurrentUser {
id: number;
username: string;
role: string;
role_label: string;
is_active: boolean;
remark: string | null;
permissions: string[];
}
export interface UserInfo { export interface UserInfo {
id: number; id: number;
username: string; username: string;
@@ -18,13 +45,127 @@ export interface UserInfo {
custom_permissions?: string[] | null; custom_permissions?: string[] | null;
} }
// ==================== Account ====================
export interface AccountItem {
id: number;
username: string;
password?: string | null;
email?: string | null;
email_password?: string | null;
tag: string;
assigned_to: number | null;
assigned_username: string | null;
remark: string;
created_at: string | null;
}
export interface AssignmentsSummary {
support_users: SupportUserItem[];
unassigned_count: number;
}
export interface SupportUserItem {
id: number;
username: string;
assigned_count: number;
}
// ==================== Login Task ====================
export interface LoginTaskItem {
id: number;
batch_id: string;
account_id: number;
account_username: string;
status: string;
cookie: string;
message: string;
created_by: number;
created_at: string | null;
finished_at: string | null;
}
export interface BatchLoginResult {
batch_id: string;
count: number;
success: boolean;
}
// ==================== Cookie ====================
export interface CookieItem {
id: number;
batch_id: string;
account_id: number;
account_username: string;
assigned_to: number | null;
assigned_username: string | null;
created_at: string | null;
cookie: string;
cookie_preview: string;
}
// ==================== Proxy ====================
export interface ProxyConfig {
enabled: boolean;
api_url: string;
http: string;
https: string;
whitelist_enabled: boolean;
whitelist_uid: string;
whitelist_ukey: string;
}
export interface ProxyTestResult {
test_id: string;
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 {
permissions: Record<string, string>;
role_permissions: Record<string, string[]>;
}
// ==================== API 定义 ====================
export const authApi = { export const authApi = {
login: (username: string, password: string) => login: (username: string, password: string) =>
api.post<any, LoginResult>('/auth/login', { username, password }), api.post<any, LoginResult>('/auth/login', { username, password }),
me: () => api.get<any, any>('/auth/me'), me: () => api.get<any, CurrentUser>('/auth/me'),
logout: () => api.post<any, any>('/auth/logout'), logout: () => api.post<any, MessageResponse>('/auth/logout'),
}; };
export const userApi = { export const userApi = {
@@ -33,55 +174,55 @@ export const userApi = {
api.post<any, UserInfo>('/users', data), api.post<any, UserInfo>('/users', data),
update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) => update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) =>
api.put<any, UserInfo>(`/users/${id}`, data), api.put<any, UserInfo>(`/users/${id}`, data),
delete: (id: number) => api.delete<any, any>(`/users/${id}`), delete: (id: number) => api.delete<any, MessageResponse>(`/users/${id}`),
listPermissions: () => api.get<any, any>('/users/permissions/list'), listPermissions: () => api.get<any, PermissionsListResult>('/users/permissions/list'),
}; };
export const accountApi = { export const accountApi = {
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) => list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
api.get<any, any[]>('/accounts', { params }), api.get<any, AccountItem[]>('/accounts', { params }),
import: (text: string) => api.post<any, any>('/accounts/import', { text }), import: (text: string) => api.post<any, MessageCountResponse>('/accounts/import', { text }),
assign: (id: number, assigned_to: number | null) => assign: (id: number, assigned_to: number | null) =>
api.put<any, any>(`/accounts/${id}/assign`, { assigned_to }), api.put<any, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
batchAssign: (account_ids: number[], assigned_to: number | null) => batchAssign: (account_ids: number[], assigned_to: number | null) =>
api.post<any, any>('/accounts/batch-assign', { account_ids, assigned_to }), api.post<any, MessageCountResponse>('/accounts/batch-assign', { account_ids, assigned_to }),
assignmentsSummary: () => assignmentsSummary: () =>
api.get<any, any>('/accounts/assignments/summary'), api.get<any, AssignmentsSummary>('/accounts/assignments/summary'),
setTag: (id: number, tag: string) => setTag: (id: number, tag: string) =>
api.put<any, any>(`/accounts/${id}/tag`, { tag }), api.put<any, MessageResponse>(`/accounts/${id}/tag`, { tag }),
batchTag: (account_ids: number[], tag: string) => batchTag: (account_ids: number[], tag: string) =>
api.put<any, any>('/accounts/batch-tag', { account_ids, tag }), api.put<any, MessageCountResponse>('/accounts/batch-tag', { account_ids, tag }),
listTags: () => api.get<any, string[]>('/accounts/tags/list'), listTags: () => api.get<any, string[]>('/accounts/tags/list'),
delete: (id: number) => api.delete<any, any>(`/accounts/${id}`), delete: (id: number) => api.delete<any, MessageResponse>(`/accounts/${id}`),
batchDelete: (account_ids: number[]) => batchDelete: (account_ids: number[]) =>
api.delete<any, any>('/accounts/batch/delete', { params: { account_ids: account_ids.join(',') } }), api.delete<any, MessageDeletedResponse>('/accounts/batch/delete', { params: { account_ids: account_ids.join(',') } }),
}; };
export const loginApi = { export const loginApi = {
createBatch: (account_ids: number[], max_geetest_retries?: number, concurrency?: number, max_proxy_retries?: number) => createBatch: (account_ids: number[], max_geetest_retries?: number, concurrency?: number, max_proxy_retries?: number) =>
api.post<any, any>('/login/batch', { account_ids, max_geetest_retries, concurrency, max_proxy_retries }), api.post<any, BatchLoginResult>('/login/batch', { account_ids, max_geetest_retries, concurrency, max_proxy_retries }),
listTasks: (batch_id?: string) => listTasks: (batch_id?: string) =>
api.get<any, any[]>('/login/tasks', { params: batch_id ? { batch_id } : {} }), api.get<any, LoginTaskItem[]>('/login/tasks', { params: batch_id ? { batch_id } : {} }),
stop: (batch_id: string) => api.post<any, any>(`/login/stop/${batch_id}`), stop: (batch_id: string) => api.post<any, MessageResponse>(`/login/stop/${batch_id}`),
deleteTask: (id: number) => api.delete<any, any>(`/login/tasks/${id}`), deleteTask: (id: number) => api.delete<any, MessageResponse>(`/login/tasks/${id}`),
deleteTasks: (ids: number[]) => api.delete<any, any>(`/login/tasks`, { params: { task_ids: ids.join(',') } }), deleteTasks: (ids: number[]) => api.delete<any, MessageDeletedResponse>(`/login/tasks`, { params: { task_ids: ids.join(',') } }),
}; };
export const cookieApi = { export const cookieApi = {
list: () => api.get<any, any[]>('/cookies'), list: () => api.get<any, CookieItem[]>('/cookies'),
exportCsv: (format?: string) => api.get('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }), exportCsv: (format?: string) => api.get('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
delete: (id: number) => api.delete<any, any>(`/cookies/${id}`), delete: (id: number) => api.delete<any, MessageResponse>(`/cookies/${id}`),
}; };
export const proxyApi = { export const proxyApi = {
get: () => api.get<any, any>('/proxy'), get: () => api.get<any, ProxyConfig>('/proxy'),
update: (data: any) => api.put<any, any>('/proxy', data), update: (data: ProxyConfig) => api.put<any, ProxyConfig>('/proxy', data),
test: () => api.post<any, any>('/proxy/test'), test: () => api.post<any, ProxyTestResult>('/proxy/test'),
testWhitelist: () => api.post<any, any>('/proxy/whitelist/test'), testWhitelist: () => api.post<any, ProxyTestResult>('/proxy/whitelist/test'),
}; };
export const logApi = { export const logApi = {
listHttp: (params?: { limit?: number; offset?: number; category?: string; level?: string; keyword?: string }) => listHttp: (params?: { limit?: number; offset?: number; category?: string; level?: string; keyword?: string }) =>
api.get<any, { items: any[]; total: number }>('/logs/http', { params }), api.get<any, HttpLogListResult>('/logs/http', { params }),
clearHttp: () => api.delete<any, { success: boolean; cleared: number }>('/logs/http'), clearHttp: () => api.delete<any, HttpLogClearResult>('/logs/http'),
}; };
+1 -1
View File
@@ -39,7 +39,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
if (!user) return null; if (!user) return null;
const menuItems: any[] = []; const menuItems: { key: string; label: string; icon: React.ReactNode }[] = [];
// Dashboard - 所有人可见 // Dashboard - 所有人可见
menuItems.push({ key: '/', label: '概览', icon: <DashboardOutlined /> }); menuItems.push({ key: '/', label: '概览', icon: <DashboardOutlined /> });
+25 -24
View File
@@ -4,8 +4,9 @@ import {
Row, Col, Card, Statistic, Row, Col, Card, Statistic,
} from 'antd'; } from 'antd';
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons'; import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
import { accountApi, userApi } from '../api/modules'; import { accountApi, userApi, type AccountItem, type UserInfo } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
import { getErrorMessage } from '../utils/error';
const { TextArea } = Input; const { TextArea } = Input;
const { Text } = Typography; const { Text } = Typography;
@@ -15,8 +16,8 @@ const TAG_COLORS = [
]; ];
export default function AccountsPage() { export default function AccountsPage() {
const [accounts, setAccounts] = useState<any[]>([]); const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<UserInfo[]>([]);
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [importOpen, setImportOpen] = useState(false); const [importOpen, setImportOpen] = useState(false);
@@ -36,12 +37,12 @@ export default function AccountsPage() {
const loadAccounts = async () => { const loadAccounts = async () => {
setLoading(true); setLoading(true);
try { try {
const params: any = {}; const params: { tag?: string } = {};
if (tagFilter) params.tag = tagFilter; if (tagFilter) params.tag = tagFilter;
const data = await accountApi.list(params); const data = await accountApi.list(params);
setAccounts(data); setAccounts(data);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -50,7 +51,7 @@ export default function AccountsPage() {
const loadUsers = async () => { const loadUsers = async () => {
try { try {
const data = await userApi.list(); const data = await userApi.list();
setUsers(data.filter((u: any) => u.role === 'support')); setUsers(data.filter((u) => u.role === 'support'));
} catch {} } catch {}
}; };
@@ -92,8 +93,8 @@ export default function AccountsPage() {
setImportText(''); setImportText('');
loadAccounts(); loadAccounts();
loadTags(); loadTags();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setImporting(false); setImporting(false);
} }
@@ -104,8 +105,8 @@ export default function AccountsPage() {
await accountApi.assign(accountId, assignedTo); await accountApi.assign(accountId, assignedTo);
message.success('已分配'); message.success('已分配');
loadAccounts(); loadAccounts();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -115,8 +116,8 @@ export default function AccountsPage() {
message.success('标签已更新'); message.success('标签已更新');
loadAccounts(); loadAccounts();
loadTags(); loadTags();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -133,8 +134,8 @@ export default function AccountsPage() {
setSelectedRowKeys([]); setSelectedRowKeys([]);
loadAccounts(); loadAccounts();
loadTags(); loadTags();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -145,8 +146,8 @@ export default function AccountsPage() {
setSelectedRowKeys((prev) => prev.filter((k) => k !== id)); setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadAccounts(); loadAccounts();
loadTags(); loadTags();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -161,19 +162,19 @@ export default function AccountsPage() {
setSelectedRowKeys([]); setSelectedRowKeys([]);
loadAccounts(); loadAccounts();
loadTags(); loadTags();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
const columns: any[] = [ const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 }, { title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户名', dataIndex: 'username' }, { title: '用户名', dataIndex: 'username' },
{ {
title: '标签', title: '标签',
dataIndex: 'tag', dataIndex: 'tag',
width: 120, width: 120,
render: (tag: string, record: any) => { render: (tag: string, record: AccountItem) => {
if (!tag) { if (!tag) {
if (canImport) { if (canImport) {
return ( return (
@@ -224,7 +225,7 @@ export default function AccountsPage() {
columns.push({ columns.push({
title: '分配给', title: '分配给',
dataIndex: 'assigned_username', dataIndex: 'assigned_username',
render: (_: any, record: any) => { render: (_: unknown, record: AccountItem) => {
if (canAssign) { if (canAssign) {
return ( return (
<Select <Select
@@ -233,7 +234,7 @@ export default function AccountsPage() {
placeholder="未分配" placeholder="未分配"
value={record.assigned_to} value={record.assigned_to}
onChange={(val) => handleAssign(record.id, val ?? null)} onChange={(val) => handleAssign(record.id, val ?? null)}
options={users.map((u: any) => ({ value: u.id, label: u.username }))} options={users.map((u) => ({ value: u.id, label: u.username }))}
/> />
); );
} }
@@ -245,7 +246,7 @@ export default function AccountsPage() {
columns.push({ columns.push({
title: '操作', title: '操作',
width: 80, width: 80,
render: (_: any, record: any) => ( render: (_: unknown, record: AccountItem) => (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}> <Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />}></Button> <Button danger size="small" icon={<DeleteOutlined />}></Button>
</Popconfirm> </Popconfirm>
+19 -32
View File
@@ -7,31 +7,18 @@ import {
UserOutlined, CheckCircleOutlined, TeamOutlined, UserOutlined, CheckCircleOutlined, TeamOutlined,
SwapOutlined, ClearOutlined, UsergroupAddOutlined, SwapOutlined, ClearOutlined, UsergroupAddOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { accountApi } from '../api/modules'; import { accountApi, type AccountItem, type SupportUserItem } from '../api/modules';
import { getErrorMessage } from '../utils/error';
const { Text } = Typography; const { Text } = Typography;
const STORAGE_KEY_SELECTED_USER = 'assignments_selected_user_id'; const STORAGE_KEY_SELECTED_USER = 'assignments_selected_user_id';
interface SupportUser {
id: number;
username: string;
assigned_count: number;
}
interface AccountItem {
id: number;
username: string;
tag: string;
assigned_to: number | null;
assigned_username: string | null;
}
export default function AssignmentsPage() { export default function AssignmentsPage() {
const { token } = theme.useToken(); const { token } = theme.useToken();
const [accounts, setAccounts] = useState<AccountItem[]>([]); const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [supportUsers, setSupportUsers] = useState<SupportUser[]>([]); const [supportUsers, setSupportUsers] = useState<SupportUserItem[]>([]);
const [selectedUser, setSelectedUser] = useState<SupportUser | null>(null); const [selectedUser, setSelectedUser] = useState<SupportUserItem | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [filterTag, setFilterTag] = useState<string | undefined>(undefined); const [filterTag, setFilterTag] = useState<string | undefined>(undefined);
@@ -44,8 +31,8 @@ export default function AssignmentsPage() {
const data = await accountApi.assignmentsSummary(); const data = await accountApi.assignmentsSummary();
setSupportUsers(data.support_users); setSupportUsers(data.support_users);
return data.support_users as SupportUser[]; return data.support_users as SupportUser[];
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
return []; return [];
} }
}; };
@@ -55,8 +42,8 @@ export default function AssignmentsPage() {
try { try {
const all = await accountApi.list({ has_cookie: true }); const all = await accountApi.list({ has_cookie: true });
setAccounts(all); setAccounts(all);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -67,7 +54,7 @@ export default function AssignmentsPage() {
const users = await loadSummary(); const users = await loadSummary();
if (users.length > 0) { if (users.length > 0) {
const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER); const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER);
const savedUser = savedId ? users.find((u: SupportUser) => u.id === Number(savedId)) : null; const savedUser = savedId ? users.find((u: SupportUserItemItem) => u.id === Number(savedId)) : null;
setSelectedUser(savedUser || users[0]); setSelectedUser(savedUser || users[0]);
} }
}; };
@@ -105,7 +92,7 @@ export default function AssignmentsPage() {
}, [activeTab, unassignedAccounts, assignedAccounts, searchText, filterTag]); }, [activeTab, unassignedAccounts, assignedAccounts, searchText, filterTag]);
// 切换客服 // 切换客服
const handleSelectUser = (user: SupportUser) => { const handleSelectUser = (user: SupportUserItem) => {
setSelectedUser(user); setSelectedUser(user);
setSelectedRowKeys([]); setSelectedRowKeys([]);
setActiveTab('unassigned'); setActiveTab('unassigned');
@@ -131,10 +118,10 @@ export default function AssignmentsPage() {
setSelectedRowKeys([]); setSelectedRowKeys([]);
await loadAccounts(); await loadAccounts();
const users = await loadSummary(); const users = await loadSummary();
const refreshed = users.find((u: SupportUser) => u.id === selectedUser.id); const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser.id);
if (refreshed) setSelectedUser(refreshed); if (refreshed) setSelectedUser(refreshed);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setAssigning(false); setAssigning(false);
} }
@@ -153,10 +140,10 @@ export default function AssignmentsPage() {
setSelectedRowKeys([]); setSelectedRowKeys([]);
await loadAccounts(); await loadAccounts();
const users = await loadSummary(); const users = await loadSummary();
const refreshed = users.find((u: SupportUser) => u.id === selectedUser?.id); const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser?.id);
if (refreshed) setSelectedUser(refreshed); if (refreshed) setSelectedUser(refreshed);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setAssigning(false); setAssigning(false);
} }
@@ -185,10 +172,10 @@ export default function AssignmentsPage() {
message.success(`已将 ${ids.length} 个账号分配给 ${selectedUser.username}`); message.success(`已将 ${ids.length} 个账号分配给 ${selectedUser.username}`);
await loadAccounts(); await loadAccounts();
const users = await loadSummary(); const users = await loadSummary();
const refreshed = users.find((u: SupportUser) => u.id === selectedUser.id); const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser.id);
if (refreshed) setSelectedUser(refreshed); if (refreshed) setSelectedUser(refreshed);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setAssigning(false); setAssigning(false);
} }
+17 -16
View File
@@ -1,15 +1,16 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd'; 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 { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi } from '../api/modules'; import { cookieApi, type CookieItem } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
import { formatTime } from '../utils/time'; import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
const { Text } = Typography; const { Text } = Typography;
export default function CookiePage() { export default function CookiePage() {
const { token } = theme.useToken(); const { token } = theme.useToken();
const [cookies, setCookies] = useState<any[]>([]); const [cookies, setCookies] = useState<CookieItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
@@ -23,8 +24,8 @@ export default function CookiePage() {
try { try {
const data = await cookieApi.list(); const data = await cookieApi.list();
setCookies(data); setCookies(data);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -37,15 +38,15 @@ export default function CookiePage() {
const handleExport = async (format: string = 'csv') => { const handleExport = async (format: string = 'csv') => {
try { try {
const blob = await cookieApi.exportCsv(format); const blob = await cookieApi.exportCsv(format);
const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob as any])); const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]));
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv'; a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv';
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
message.success('已导出'); message.success('已导出');
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -83,8 +84,8 @@ export default function CookiePage() {
message.success('已删除'); message.success('已删除');
setSelectedRowKeys((prev) => prev.filter((k) => k !== id)); setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadCookies(); loadCookies();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -98,8 +99,8 @@ export default function CookiePage() {
); );
}); });
const columns: any[] = [ const columns = [
{ title: 'ID', dataIndex: 'id', width: 60, align: 'center' }, { title: 'ID', dataIndex: 'id', width: 60, align: 'center' as const },
{ {
title: '账号', title: '账号',
dataIndex: 'account_username', dataIndex: 'account_username',
@@ -110,7 +111,7 @@ export default function CookiePage() {
title: '分配', title: '分配',
dataIndex: 'assigned_username', dataIndex: 'assigned_username',
width: 100, width: 100,
align: 'center', align: 'center' as const,
render: (name: string | null) => render: (name: string | null) =>
name ? <Tag color="green">{name}</Tag> : <Text type="secondary" style={{ fontSize: 12 }}></Text>, name ? <Tag color="green">{name}</Tag> : <Text type="secondary" style={{ fontSize: 12 }}></Text>,
}, },
@@ -127,15 +128,15 @@ export default function CookiePage() {
title: '时间', title: '时间',
dataIndex: 'created_at', dataIndex: 'created_at',
width: 180, width: 180,
align: 'center', align: 'center' as const,
render: (val: string) => formatTime(val), render: (val: string) => formatTime(val),
}, },
{ {
title: '操作', title: '操作',
width: 130, width: 130,
align: 'center', align: 'center' as const,
fixed: 'right', fixed: 'right' as const,
render: (_: any, record: any) => ( render: (_: unknown, record: CookieItem) => (
<Space size={4}> <Space size={4}>
<Button <Button
size="small" size="small"
+3 -3
View File
@@ -1,6 +1,6 @@
import { Card, Col, Row, Statistic, theme } from 'antd'; import { Card, Col, Row, Statistic, theme } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { accountApi, loginApi } from '../api/modules'; import { accountApi, loginApi, type LoginTaskItem } from '../api/modules';
export default function DashboardPage() { export default function DashboardPage() {
const { token } = theme.useToken(); const { token } = theme.useToken();
@@ -12,8 +12,8 @@ export default function DashboardPage() {
setStats({ setStats({
accounts: accounts.length, accounts: accounts.length,
tasks: tasks.length, tasks: tasks.length,
success: tasks.filter((t: any) => t.status === 'success').length, success: tasks.filter((t: LoginTaskItem) => t.status === 'success').length,
failed: tasks.filter((t: any) => ['failed', 'error'].includes(t.status)).length, failed: tasks.filter((t: LoginTaskItem) => ['failed', 'error'].includes(t.status)).length,
}); });
}) })
.catch(() => {}); .catch(() => {});
+7 -21
View File
@@ -6,26 +6,12 @@ import {
import { import {
ReloadOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, ReloadOutlined, DeleteOutlined, SearchOutlined, EyeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { logApi } from '../api/modules'; import { logApi, type HttpLogEntry } from '../api/modules';
import { hasPerm, getUser } from '../store/auth'; import { hasPerm, getUser } from '../store/auth';
import { getErrorMessage } from '../utils/error';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
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;
}
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
douyu_login: '斗鱼登录', douyu_login: '斗鱼登录',
geetest: '极验', geetest: '极验',
@@ -63,8 +49,8 @@ export default function HttpLogsPage() {
}); });
setLogs(res.items || []); setLogs(res.items || []);
setTotal(res.total || 0); setTotal(res.total || 0);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message || '获取日志失败'); message.error(getErrorMessage(e) || '获取日志失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -87,8 +73,8 @@ export default function HttpLogsPage() {
const res = await logApi.clearHttp(); const res = await logApi.clearHttp();
message.success(`已清空 ${res.cleared} 条日志`); message.success(`已清空 ${res.cleared} 条日志`);
fetchLogs(); fetchLogs();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message || '清空失败'); message.error(getErrorMessage(e) || '清空失败');
} }
}; };
@@ -166,7 +152,7 @@ export default function HttpLogsPage() {
{ {
title: '操作', title: '操作',
width: 60, width: 60,
render: (_: any, record: HttpLogEntry) => ( render: (_: unknown, record: HttpLogEntry) => (
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => setDetailEntry(record)} /> <Button type="link" size="small" icon={<EyeOutlined />} onClick={() => setDetailEntry(record)} />
), ),
}, },
+3 -2
View File
@@ -4,6 +4,7 @@ import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { authApi } from '../api/modules'; import { authApi } from '../api/modules';
import { setAuth, type AuthUser } from '../store/auth'; import { setAuth, type AuthUser } from '../store/auth';
import { getErrorMessage } from '../utils/error';
import { useTheme } from '../store/theme'; import { useTheme } from '../store/theme';
const { Title } = Typography; const { Title } = Typography;
@@ -27,8 +28,8 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) {
message.success('登录成功'); message.success('登录成功');
onLogin?.(); // 触发 App 重渲染 onLogin?.(); // 触发 App 重渲染
navigate('/', { replace: true }); navigate('/', { replace: true });
} catch (e: any) { } catch (e: unknown) {
message.error(e.message || '登录失败'); message.error(getErrorMessage(e) || '登录失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
+22 -16
View File
@@ -3,9 +3,10 @@ import {
Table, Button, Select, message, Tag, Space, Spin, InputNumber, Tooltip, Popconfirm, theme, Table, Button, Select, message, Tag, Space, Spin, InputNumber, Tooltip, Popconfirm, theme,
} from 'antd'; } from 'antd';
import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DownOutlined, UpOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons'; import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DownOutlined, UpOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons';
import { accountApi, loginApi } from '../api/modules'; import { accountApi, loginApi, type AccountItem, type LoginTaskItem } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
import { formatTime } from '../utils/time'; import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
pending: 'default', pending: 'default',
@@ -23,10 +24,15 @@ const STATUS_LABELS: Record<string, string> = {
error: '异常', error: '异常',
}; };
interface SelectGroupOption {
label: string;
options: { value: number; label: string }[];
}
export default function LoginTasksPage() { export default function LoginTasksPage() {
const [accounts, setAccounts] = useState<any[]>([]); const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [tasks, setTasks] = useState<any[]>([]); const [tasks, setTasks] = useState<LoginTaskItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [batchId, setBatchId] = useState<string | null>(null); const [batchId, setBatchId] = useState<string | null>(null);
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]); const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
@@ -67,8 +73,8 @@ export default function LoginTasksPage() {
try { try {
const data = await accountApi.list(); const data = await accountApi.list();
setAccounts(data); setAccounts(data);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -158,8 +164,8 @@ export default function LoginTasksPage() {
ws.onerror = () => { ws.onerror = () => {
setWsConnected(false); setWsConnected(false);
}; };
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -191,8 +197,8 @@ export default function LoginTasksPage() {
try { try {
await loginApi.stop(batchId); await loginApi.stop(batchId);
message.success('已发送停止信号'); message.success('已发送停止信号');
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
} }
}; };
@@ -203,8 +209,8 @@ export default function LoginTasksPage() {
message.success('已删除'); message.success('已删除');
loadTasks(); loadTasks();
setSelectedRowKeys((prev) => prev.filter((k) => k !== taskId)); setSelectedRowKeys((prev) => prev.filter((k) => k !== taskId));
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -218,8 +224,8 @@ export default function LoginTasksPage() {
message.success(`已删除 ${selectedRowKeys.length} 个任务`); message.success(`已删除 ${selectedRowKeys.length} 个任务`);
setSelectedRowKeys([]); setSelectedRowKeys([]);
loadTasks(); loadTasks();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -239,7 +245,7 @@ export default function LoginTasksPage() {
{ {
title: '操作', title: '操作',
width: 120, width: 120,
render: (_: any, record: any) => ( render: (_: unknown, record: LoginTaskItem) => (
<Space size={4}> <Space size={4}>
{['failed', 'error'].includes(record.status) && !wsConnected && ( {['failed', 'error'].includes(record.status) && !wsConnected && (
<Button <Button
@@ -298,7 +304,7 @@ export default function LoginTasksPage() {
noTag.push({ value: a.id, label: a.username }); noTag.push({ value: a.id, label: a.username });
} }
}); });
const result: any[] = []; const result: SelectGroupOption[] = [];
Object.keys(grouped).sort().forEach((tag) => { Object.keys(grouped).sort().forEach((tag) => {
result.push({ label: tag, options: grouped[tag] }); result.push({ label: tag, options: grouped[tag] });
}); });
@@ -312,7 +318,7 @@ export default function LoginTasksPage() {
size="small" size="small"
filterOption={(input, option) => { filterOption={(input, option) => {
if (!option) return false; if (!option) return false;
const label = (option as any).label as string || ''; const label = (option as { label?: string }).label || '';
return label.toLowerCase().includes(input.toLowerCase()); return label.toLowerCase().includes(input.toLowerCase());
}} }}
dropdownRender={(menu) => ( dropdownRender={(menu) => (
+11 -10
View File
@@ -1,6 +1,7 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { Form, Input, Switch, Button, Card, message, Row, Col, theme } from 'antd'; import { Form, Input, Switch, Button, Card, message, Row, Col, theme } from 'antd';
import { proxyApi } from '../api/modules'; import { proxyApi, type ProxyConfig } from '../api/modules';
import { getErrorMessage } from '../utils/error';
const WS_BASE = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}`; const WS_BASE = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}`;
@@ -26,8 +27,8 @@ export default function ProxyPage() {
whitelist_uid: data.whitelist_uid ?? '', whitelist_uid: data.whitelist_uid ?? '',
whitelist_ukey: data.whitelist_ukey ?? '', whitelist_ukey: data.whitelist_ukey ?? '',
}); });
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setConfigLoaded(true); setConfigLoaded(true);
} }
@@ -66,10 +67,10 @@ export default function ProxyPage() {
setLoading(true); setLoading(true);
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
await proxyApi.update(values); await proxyApi.update(values as ProxyConfig);
message.success('已保存'); message.success('已保存');
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -81,8 +82,8 @@ export default function ProxyPage() {
try { try {
const result = await proxyApi.test(); const result = await proxyApi.test();
if (result.test_id) connectWs(result.test_id); if (result.test_id) connectWs(result.test_id);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
setTesting(false); setTesting(false);
} }
}; };
@@ -93,8 +94,8 @@ export default function ProxyPage() {
try { try {
const result = await proxyApi.testWhitelist(); const result = await proxyApi.testWhitelist();
if (result.test_id) connectWs(result.test_id); if (result.test_id) connectWs(result.test_id);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
setTestingWl(false); setTestingWl(false);
} }
}; };
+14 -13
View File
@@ -6,6 +6,7 @@ import {
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons';
import { userApi, type UserInfo } from '../api/modules'; import { userApi, type UserInfo } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
import { getErrorMessage } from '../utils/error';
const ROLE_OPTIONS = [ const ROLE_OPTIONS = [
{ value: 'super_admin', label: '超级管理员' }, { value: 'super_admin', label: '超级管理员' },
@@ -57,8 +58,8 @@ export default function UsersPage() {
try { try {
const data = await userApi.list(); const data = await userApi.list();
setUsers(data); setUsers(data);
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -89,7 +90,7 @@ export default function UsersPage() {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
if (editing) { if (editing) {
const updateData: any = { role: values.role, remark: values.remark }; const updateData: { role: string; remark: string; password?: string } = { role: values.role, remark: values.remark };
if (values.password) updateData.password = values.password; if (values.password) updateData.password = values.password;
await userApi.update(editing.id, updateData); await userApi.update(editing.id, updateData);
message.success('已更新'); message.success('已更新');
@@ -99,8 +100,8 @@ export default function UsersPage() {
} }
setModalOpen(false); setModalOpen(false);
loadUsers(); loadUsers();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -109,8 +110,8 @@ export default function UsersPage() {
await userApi.delete(id); await userApi.delete(id);
message.success('已删除'); message.success('已删除');
loadUsers(); loadUsers();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} }
}; };
@@ -131,8 +132,8 @@ export default function UsersPage() {
setUseCustom(false); setUseCustom(false);
setSelectedPerms(data.role_permissions?.[user.role] || []); setSelectedPerms(data.role_permissions?.[user.role] || []);
} }
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setPermLoading(false); setPermLoading(false);
} }
@@ -148,8 +149,8 @@ export default function UsersPage() {
message.success('权限已更新'); message.success('权限已更新');
setPermModalOpen(false); setPermModalOpen(false);
loadUsers(); loadUsers();
} catch (e: any) { } catch (e: unknown) {
message.error(e.message); message.error(getErrorMessage(e));
} finally { } finally {
setPermLoading(false); setPermLoading(false);
} }
@@ -175,7 +176,7 @@ export default function UsersPage() {
title: '权限模式', title: '权限模式',
dataIndex: 'custom_permissions', dataIndex: 'custom_permissions',
width: 100, width: 100,
render: (_: any, record: UserInfo) => render: (_: unknown, record: UserInfo) =>
record.custom_permissions !== null && record.custom_permissions !== undefined record.custom_permissions !== null && record.custom_permissions !== undefined
? <Tag color="orange"></Tag> ? <Tag color="orange"></Tag>
: <Tag></Tag>, : <Tag></Tag>,
@@ -190,7 +191,7 @@ export default function UsersPage() {
{ {
title: '操作', title: '操作',
width: 200, width: 200,
render: (_: any, record: UserInfo) => ( render: (_: unknown, record: UserInfo) => (
<Space> <Space>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button> <Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
{canAssignPerm && ( {canAssignPerm && (
+6
View File
@@ -0,0 +1,6 @@
/** 从 unknown 类型的 catch 错误中安全提取 message */
export function getErrorMessage(e: unknown): string {
if (e instanceof Error) return e.message;
if (typeof e === 'string') return e;
return '未知错误';
}