Files
codexautopool/frontend/src/pages/CodexProxyConfig.tsx

449 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react'
import {
Globe, Plus, Trash2, ToggleLeft, ToggleRight,
Loader2, Save, RefreshCcw, CheckCircle, XCircle,
AlertTriangle, Clock
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
interface CodexProxy {
id: number
proxy_url: string
description: string
is_enabled: boolean
last_used_at: string | null
success_count: number
fail_count: number
created_at: string
}
interface ProxyStats {
total: number
enabled: number
disabled: number
}
export default function CodexProxyConfig() {
const [proxies, setProxies] = useState<CodexProxy[]>([])
const [stats, setStats] = useState<ProxyStats>({ total: 0, enabled: 0, disabled: 0 })
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
// 单个添加
const [newProxyUrl, setNewProxyUrl] = useState('')
const [newDescription, setNewDescription] = useState('')
// 批量添加
const [batchMode, setBatchMode] = useState(false)
const [batchInput, setBatchInput] = useState('')
// 获取代理列表
const fetchProxies = useCallback(async () => {
setLoading(true)
try {
const res = await fetch('/api/codex-proxy')
const data = await res.json()
if (data.code === 0 && data.data) {
setProxies(data.data.proxies || [])
setStats(data.data.stats || { total: 0, enabled: 0, disabled: 0 })
}
} catch (error) {
console.error('获取代理列表失败:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchProxies()
}, [fetchProxies])
// 添加代理
const handleAddProxy = async () => {
if (!newProxyUrl.trim()) return
setSaving(true)
setMessage(null)
try {
const res = await fetch('/api/codex-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
proxy_url: newProxyUrl.trim(),
description: newDescription.trim(),
}),
})
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: '代理添加成功' })
setNewProxyUrl('')
setNewDescription('')
fetchProxies()
} else {
setMessage({ type: 'error', text: data.message || '添加失败' })
}
} catch {
setMessage({ type: 'error', text: '网络错误' })
} finally {
setSaving(false)
}
}
// 批量添加
const handleBatchAdd = async () => {
const lines = batchInput.split('\n').filter(line => line.trim())
if (lines.length === 0) return
setSaving(true)
setMessage(null)
try {
const res = await fetch('/api/codex-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxies: lines }),
})
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: `成功添加 ${data.data.added}/${data.data.total} 个代理` })
setBatchInput('')
fetchProxies()
} else {
setMessage({ type: 'error', text: data.message || '添加失败' })
}
} catch {
setMessage({ type: 'error', text: '网络错误' })
} finally {
setSaving(false)
}
}
// 切换启用状态
const handleToggle = async (id: number) => {
try {
const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'PUT' })
const data = await res.json()
if (data.code === 0) {
fetchProxies()
}
} catch (error) {
console.error('切换状态失败:', error)
}
}
// 删除代理
const handleDelete = async (id: number) => {
if (!confirm('确定要删除这个代理吗?')) return
try {
const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'DELETE' })
const data = await res.json()
if (data.code === 0) {
fetchProxies()
}
} catch (error) {
console.error('删除失败:', error)
}
}
// 清空所有
const handleClearAll = async () => {
if (!confirm('确定要清空所有代理吗?此操作不可恢复!')) return
try {
const res = await fetch('/api/codex-proxy?all=true', { method: 'DELETE' })
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: '已清空所有代理' })
fetchProxies()
}
} catch (error) {
console.error('清空失败:', error)
}
}
// 格式化代理显示
const formatProxyDisplay = (url: string) => {
if (url.includes('@')) {
const parts = url.split('@')
return parts[parts.length - 1]
}
return url.replace(/^https?:\/\//, '').replace(/^socks5:\/\//, '')
}
// 格式化时间
const formatTime = (timeStr: string | null) => {
if (!timeStr) return '从未'
const date = new Date(timeStr)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 计算成功率
const getSuccessRate = (proxy: CodexProxy) => {
const total = proxy.success_count + proxy.fail_count
if (total === 0) return null
return Math.round((proxy.success_count / total) * 100)
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<Globe className="h-7 w-7 text-purple-500" />
CodexAuth
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
CodexAuth API 使
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={fetchProxies}
icon={<RefreshCcw className="h-4 w-4" />}
>
</Button>
{proxies.length > 0 && (
<Button
variant="danger"
onClick={handleClearAll}
icon={<Trash2 className="h-4 w-4" />}
>
</Button>
)}
</div>
</div>
{/* Message */}
{message && (
<div className={`p-3 rounded-lg text-sm ${message.type === 'success'
? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400'
}`}>
{message.text}
</div>
)}
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<Card>
<CardContent className="py-4">
<div className="text-center">
<div className="text-2xl font-bold text-slate-900 dark:text-slate-100">
{stats.total}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400">
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{stats.enabled}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400">
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="py-4">
<div className="text-center">
<div className="text-2xl font-bold text-slate-400">
{stats.disabled}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400">
</div>
</div>
</CardContent>
</Card>
</div>
{/* Add Proxy */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Plus className="h-5 w-5 text-blue-500" />
</CardTitle>
<button
onClick={() => setBatchMode(!batchMode)}
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
{batchMode ? '单个添加' : '批量添加'}
</button>
</CardHeader>
<CardContent className="space-y-4">
{batchMode ? (
<>
<textarea
value={batchInput}
onChange={(e) => setBatchInput(e.target.value)}
placeholder="每行一个代理地址,格式:&#10;http://user:pass@host:port&#10;http://host:port&#10;socks5://host:port"
rows={6}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm font-mono"
/>
<Button
onClick={handleBatchAdd}
disabled={saving || !batchInput.trim()}
icon={saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
>
{saving ? '添加中...' : '批量添加'}
</Button>
</>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="代理地址"
value={newProxyUrl}
onChange={(e) => setNewProxyUrl(e.target.value)}
placeholder="http://user:pass@host:port"
onKeyDown={(e) => e.key === 'Enter' && handleAddProxy()}
/>
<Input
label="备注(可选)"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
placeholder="代理描述"
/>
</div>
<Button
onClick={handleAddProxy}
disabled={saving || !newProxyUrl.trim()}
icon={saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
>
{saving ? '添加中...' : '添加代理'}
</Button>
</>
)}
</CardContent>
</Card>
{/* Proxy List */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<span className="text-sm text-slate-500">
{proxies.length}
</span>
</CardHeader>
<CardContent>
{proxies.length === 0 ? (
<div className="text-center py-8 text-slate-500 dark:text-slate-400">
<Globe className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p></p>
<p className="text-sm">CodexAuth 使</p>
</div>
) : (
<div className="space-y-3">
{proxies.map((proxy) => {
const successRate = getSuccessRate(proxy)
return (
<div
key={proxy.id}
className={`p-4 rounded-lg border ${proxy.is_enabled
? 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700'
: 'bg-slate-50 dark:bg-slate-800/50 border-slate-200 dark:border-slate-700 opacity-60'
}`}
>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-slate-900 dark:text-slate-100 truncate">
{formatProxyDisplay(proxy.proxy_url)}
</span>
{proxy.description && (
<span className="text-xs text-slate-500 dark:text-slate-400">
({proxy.description})
</span>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-xs text-slate-500 dark:text-slate-400">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatTime(proxy.last_used_at)}
</span>
<span className="flex items-center gap-1">
<CheckCircle className="h-3 w-3 text-green-500" />
{proxy.success_count}
</span>
<span className="flex items-center gap-1">
<XCircle className="h-3 w-3 text-red-500" />
{proxy.fail_count}
</span>
{successRate !== null && (
<span className={`flex items-center gap-1 ${successRate >= 80 ? 'text-green-600' : successRate >= 50 ? 'text-yellow-600' : 'text-red-600'
}`}>
<AlertTriangle className="h-3 w-3" />
{successRate}%
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleToggle(proxy.id)}
className="p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
title={proxy.is_enabled ? '禁用' : '启用'}
>
{proxy.is_enabled ? (
<ToggleRight className="h-6 w-6 text-green-500" />
) : (
<ToggleLeft className="h-6 w-6 text-slate-400" />
)}
</button>
<button
onClick={() => handleDelete(proxy.id)}
className="p-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded text-red-500"
title="删除"
>
<Trash2 className="h-5 w-5" />
</button>
</div>
</div>
</div>
)
})}
</div>
)}
</CardContent>
</Card>
{/* Info */}
<Card>
<CardContent>
<div className="text-sm text-slate-500 dark:text-slate-400">
<p className="font-medium mb-2">使</p>
<ul className="list-disc list-inside space-y-1">
<li> CodexAuth API 使</li>
<li> HTTP / HTTPS / SOCKS5 </li>
<li><code className="px-1 bg-slate-100 dark:bg-slate-800 rounded">http://user:pass@host:port</code></li>
<li>/</li>
<li>使</li>
</ul>
</div>
</CardContent>
</Card>
</div>
)
}