import { useState, useEffect, useCallback } from 'react' import { Trash2, Clock, Loader2, Save, RefreshCw, CheckCircle, XCircle, ToggleLeft, ToggleRight, AlertTriangle, ChevronLeft, ChevronRight, ScrollText } from 'lucide-react' import { Card, CardHeader, CardTitle, CardContent, Button } from '../components/common' interface CleanerStatus { running: boolean enabled: boolean interval: number last_clean_time: string } interface CleanerLogEntry { timestamp: string level: string message: string email?: string module?: string } export default function Cleaner() { const [loading, setLoading] = useState(true) const [cleanEnabled, setCleanEnabled] = useState(false) const [cleanInterval, setCleanInterval] = useState(3600) const [savingClean, setSavingClean] = useState(false) const [cleaning, setCleaning] = useState(false) const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null) const [status, setStatus] = useState(null) // 清理日志状态 const [logEntries, setLogEntries] = useState([]) const [logPage, setLogPage] = useState(1) const [logTotalPages, setLogTotalPages] = useState(0) const [logTotal, setLogTotal] = useState(0) const [logLoading, setLogLoading] = useState(false) const logPageSize = 5 // 加载清理设置 const fetchCleanerSettings = async () => { setLoading(true) try { const res = await fetch('/api/s2a/cleaner/settings') const data = await res.json() if (data.code === 0 && data.data) { setCleanEnabled(data.data.enabled || false) setCleanInterval(data.data.interval || 3600) setStatus(data.data.status || null) } } catch (error) { console.error('Failed to fetch cleaner settings:', error) } finally { setLoading(false) } } // 加载清理日志 const fetchCleanerLogs = useCallback(async (page = 1) => { setLogLoading(true) try { const params = new URLSearchParams({ module: 'cleaner', page: String(page), page_size: String(logPageSize), }) const res = await fetch(`/api/logs/query?${params}`) const data = await res.json() if (data.code === 0 && data.data) { setLogEntries(data.data.logs || []) setLogTotalPages(data.data.total_pages || 0) setLogTotal(data.data.total || 0) } } catch (error) { console.error('Failed to fetch cleaner logs:', error) } finally { setLogLoading(false) } }, [logPageSize]) useEffect(() => { fetchCleanerSettings() }, []) useEffect(() => { fetchCleanerLogs(logPage) }, [logPage, fetchCleanerLogs]) // 保存清理设置 const handleSaveCleanerSettings = async () => { setSavingClean(true) setMessage(null) try { const res = await fetch('/api/s2a/cleaner/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: cleanEnabled, interval: cleanInterval, }), }) const data = await res.json() if (data.code === 0) { setMessage({ type: 'success', text: '清理设置已保存' }) // 刷新状态 fetchCleanerSettings() } else { setMessage({ type: 'error', text: data.message || '保存失败' }) } } catch { setMessage({ type: 'error', text: '网络错误' }) } finally { setSavingClean(false) } } // 手动清理错误账号 const handleCleanNow = async () => { if (!confirm('确认立即清理所有错误账号?\n\n此操作将删除 S2A 号池中所有状态为"错误"的账号。')) return setCleaning(true) setMessage(null) try { const res = await fetch('/api/s2a/clean-errors', { method: 'POST' }) const data = await res.json() if (data.code === 0) { setMessage({ type: 'success', text: data.data.message || '清理完成' }) // 刷新状态和日志 fetchCleanerSettings() fetchCleanerLogs(logPage) } else { setMessage({ type: 'error', text: data.message || '清理失败' }) } } catch { setMessage({ type: 'error', text: '网络错误' }) } finally { setCleaning(false) } } // 格式化时间间隔 const formatInterval = (seconds: number): string => { if (seconds < 60) return `${seconds} 秒` if (seconds < 3600) return `${Math.floor(seconds / 60)} 分钟` return `${Math.floor(seconds / 3600)} 小时` } // 格式化上次清理时间 const formatLastCleanTime = (timeStr: string): string => { if (!timeStr || timeStr === '0001-01-01T00:00:00Z') return '从未执行' try { const date = new Date(timeStr) return date.toLocaleString('zh-CN') } catch { return '未知' } } // 计算下次清理时间 const getNextCleanTime = (): string => { if (!cleanEnabled) return '未启用' if (!status?.last_clean_time || status.last_clean_time === '0001-01-01T00:00:00Z') { return '即将执行' } try { const lastTime = new Date(status.last_clean_time) const nextTime = new Date(lastTime.getTime() + cleanInterval * 1000) // 如果下次时间已过,说明即将执行 if (nextTime <= new Date()) { return '即将执行' } return nextTime.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } catch { return '未知' } } // 日志级别样式 const levelColors: Record = { success: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20', error: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20', warning: 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20', info: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20', } const levelLabels: Record = { success: 'SUCCESS', error: 'ERROR', warning: 'WARN', info: 'INFO', } if (loading) { return (
) } return (
{/* Header */}

定期清理

自动清理 S2A 号池中的错误账号

{/* Message */} {message && (
{message.type === 'success' ? ( ) : ( )} {message.text}
)} {/* Status Cards */}
{/* 清理状态 */}

清理服务

{cleanEnabled ? '已启用' : '已禁用'}

{cleanEnabled ? ( ) : ( )}
{/* 清理间隔 */}

清理间隔

{formatInterval(cleanInterval)}

下次清理: {getNextCleanTime()}

{/* 上次清理 */}

上次清理

{status ? formatLastCleanTime(status.last_clean_time) : '从未执行'}

{/* 清理设置 */} 清理设置
{/* 清理间隔选择 */}

每隔指定时间自动清理 S2A 中的错误账号

{/* 手动清理 */}

立即执行一次清理操作,删除所有错误账号

{/* 清理日志 */} 清理日志
共 {logTotal} 条
{logLoading && logEntries.length === 0 ? (
加载中...
) : logEntries.length === 0 ? (

暂无清理日志

) : (
{logEntries.map((log, i) => (
{new Date(log.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })} {levelLabels[log.level] || log.level?.toUpperCase()} {log.message}
))}
)}
{logTotalPages > 1 && (
第 {logPage} / {logTotalPages} 页
)}
{/* 说明信息 */}

功能说明

  • 定期清理功能会自动删除 S2A 号池中状态为"error"的账号
  • 清理操作是不可逆的,删除的账号无法恢复
  • 建议设置合理的清理间隔,避免过于频繁的清理操作
  • 母号使用完毕或检测为无效后会自动从列表中删除
  • 启用后需要点击"保存设置"才会生效
) }