feat: Implement CodexAuth proxy pool management with a new frontend configuration page and a dedicated backend service for API, database, and proxy testing.

This commit is contained in:
2026-02-03 03:00:25 +08:00
parent 51ba54856d
commit b014226074
6 changed files with 283 additions and 28 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import {
Globe, Plus, Trash2, ToggleLeft, ToggleRight,
Loader2, Save, RefreshCcw, CheckCircle, XCircle,
AlertTriangle, Clock
AlertTriangle, Clock, MapPin, Play
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
@@ -14,6 +14,8 @@ interface CodexProxy {
last_used_at: string | null
success_count: number
fail_count: number
location: string
last_test_at: string | null
created_at: string
}
@@ -28,6 +30,7 @@ export default function CodexProxyConfig() {
const [stats, setStats] = useState<ProxyStats>({ total: 0, enabled: 0, disabled: 0 })
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [testingIds, setTestingIds] = useState<Set<number>>(new Set())
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
// 单个添加
@@ -143,6 +146,37 @@ export default function CodexProxyConfig() {
}
}
// 测试代理
const handleTestProxy = async (id: number) => {
if (testingIds.has(id)) return
setTestingIds(prev => new Set(prev).add(id))
try {
const res = await fetch(`/api/codex-proxy/test?id=${id}`, { method: 'POST' })
const data = await res.json()
if (data.code === 0) {
// 局部更新代理信息
setProxies(prev => prev.map(p =>
p.id === id
? { ...p, location: data.data.location, last_test_at: new Date().toISOString(), success_count: p.success_count + 1 }
: p
))
} else {
alert(`测试失败: ${data.message}`)
// 虽然失败也需要刷新列表以获取最新的统计数据
fetchProxies()
}
} catch (error) {
console.error('测试代理出错:', error)
alert('网络错误,测试失败')
} finally {
setTestingIds(prev => {
const next = new Set(prev)
next.delete(id)
return next
})
}
}
// 清空所有
const handleClearAll = async () => {
if (!confirm('确定要清空所有代理吗?此操作不可恢复!')) return
@@ -204,7 +238,7 @@ export default function CodexProxyConfig() {
CodexAuth
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
CodexAuth API 使
CodexAuth API 使
</p>
</div>
<div className="flex gap-2">
@@ -357,10 +391,12 @@ export default function CodexProxyConfig() {
<div className="space-y-3">
{proxies.map((proxy) => {
const successRate = getSuccessRate(proxy)
const istesting = testingIds.has(proxy.id)
return (
<div
key={proxy.id}
className={`p-4 rounded-lg border ${proxy.is_enabled
className={`p-4 rounded-lg border transition-all ${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'
}`}
@@ -371,16 +407,22 @@ export default function CodexProxyConfig() {
<span className="font-mono text-sm text-slate-900 dark:text-slate-100 truncate">
{formatProxyDisplay(proxy.proxy_url)}
</span>
{proxy.location && (
<span className="px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-[10px] font-bold flex items-center gap-0.5">
<MapPin className="h-2.5 w-2.5" />
{proxy.location}
</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">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 mt-1 text-xs text-slate-500 dark:text-slate-400">
<span className="flex items-center gap-1" title="最后测试时间">
<Clock className="h-3 w-3" />
{formatTime(proxy.last_used_at)}
: {formatTime(proxy.last_test_at)}
</span>
<span className="flex items-center gap-1">
<CheckCircle className="h-3 w-3 text-green-500" />
@@ -400,9 +442,21 @@ export default function CodexProxyConfig() {
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleTestProxy(proxy.id)}
disabled={istesting}
className={`p-1.5 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded text-blue-500 transition-colors ${istesting ? 'animate-pulse opacity-50' : ''}`}
title="立即测试"
>
{istesting ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Play className="h-5 w-5" />
)}
</button>
<button
onClick={() => handleToggle(proxy.id)}
className="p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
className="p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded transition-colors"
title={proxy.is_enabled ? '禁用' : '启用'}
>
{proxy.is_enabled ? (
@@ -413,7 +467,7 @@ export default function CodexProxyConfig() {
</button>
<button
onClick={() => handleDelete(proxy.id)}
className="p-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded text-red-500"
className="p-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded text-red-500 transition-colors"
title="删除"
>
<Trash2 className="h-5 w-5" />
@@ -437,8 +491,8 @@ export default function CodexProxyConfig() {
<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>
<li>使 ip-api.com</li>
<li>/</li>
</ul>
</div>
</CardContent>