feat: Implement Codex proxy configuration page with CRUD and testing capabilities, supported by new backend API and services for error handling, team processing, mail, and ChatGPT registration.

This commit is contained in:
2026-02-03 04:33:37 +08:00
parent f867e20c0e
commit 637753ddaa
5 changed files with 248 additions and 216 deletions

View File

@@ -4,7 +4,7 @@ import {
Loader2, Save, RefreshCcw, CheckCircle, XCircle,
AlertTriangle, Clock, MapPin, Play, PlayCircle
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { Card, CardHeader, CardTitle, CardContent, Button, Input, useToast } from '../components/common'
interface CodexProxy {
id: number
@@ -32,7 +32,7 @@ export default function CodexProxyConfig() {
const [saving, setSaving] = useState(false)
const [testingIds, setTestingIds] = useState<Set<number>>(new Set())
const [testingAll, setTestingAll] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
const toast = useToast()
// 单个添加
const [newProxyUrl, setNewProxyUrl] = useState('')
@@ -67,7 +67,6 @@ export default function CodexProxyConfig() {
const handleAddProxy = async () => {
if (!newProxyUrl.trim()) return
setSaving(true)
setMessage(null)
try {
const res = await fetch('/api/codex-proxy', {
method: 'POST',
@@ -79,15 +78,29 @@ export default function CodexProxyConfig() {
})
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: '代理添加成功' })
// 局部添加新代理到列表
const newProxy: CodexProxy = {
id: data.data.id,
proxy_url: newProxyUrl.trim(),
description: newDescription.trim(),
is_enabled: true,
last_used_at: null,
success_count: 0,
fail_count: 0,
location: '',
last_test_at: null,
created_at: new Date().toISOString(),
}
setProxies(prev => [...prev, newProxy])
setStats(prev => ({ ...prev, total: prev.total + 1, enabled: prev.enabled + 1 }))
setNewProxyUrl('')
setNewDescription('')
fetchProxies()
toast.success('代理添加成功')
} else {
setMessage({ type: 'error', text: data.message || '添加失败' })
toast.error(data.message || '添加失败')
}
} catch {
setMessage({ type: 'error', text: '网络错误' })
toast.error('网络错误')
} finally {
setSaving(false)
}
@@ -98,7 +111,6 @@ export default function CodexProxyConfig() {
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',
@@ -107,14 +119,15 @@ export default function CodexProxyConfig() {
})
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: `成功添加 ${data.data.added}/${data.data.total} 个代理` })
toast.success(`成功添加 ${data.data.added}/${data.data.total} 个代理`)
setBatchInput('')
// 批量添加后需要刷新列表获取新的代理ID
fetchProxies()
} else {
setMessage({ type: 'error', text: data.message || '添加失败' })
toast.error(data.message || '添加失败')
}
} catch {
setMessage({ type: 'error', text: '网络错误' })
toast.error('网络错误')
} finally {
setSaving(false)
}
@@ -126,10 +139,23 @@ export default function CodexProxyConfig() {
const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'PUT' })
const data = await res.json()
if (data.code === 0) {
fetchProxies()
// 局部更新状态
setProxies(prev => prev.map(p =>
p.id === id ? { ...p, is_enabled: !p.is_enabled } : p
))
// 更新统计
const proxy = proxies.find(p => p.id === id)
if (proxy) {
if (proxy.is_enabled) {
setStats(prev => ({ ...prev, enabled: prev.enabled - 1, disabled: prev.disabled + 1 }))
} else {
setStats(prev => ({ ...prev, enabled: prev.enabled + 1, disabled: prev.disabled - 1 }))
}
}
}
} catch (error) {
console.error('切换状态失败:', error)
toast.error('切换状态失败')
}
}
@@ -140,10 +166,23 @@ export default function CodexProxyConfig() {
const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'DELETE' })
const data = await res.json()
if (data.code === 0) {
fetchProxies()
// 局部删除
const proxy = proxies.find(p => p.id === id)
setProxies(prev => prev.filter(p => p.id !== id))
// 更新统计
if (proxy) {
setStats(prev => ({
...prev,
total: prev.total - 1,
enabled: proxy.is_enabled ? prev.enabled - 1 : prev.enabled,
disabled: proxy.is_enabled ? prev.disabled : prev.disabled - 1,
}))
}
toast.success('代理已删除')
}
} catch (error) {
console.error('删除失败:', error)
toast.error('删除失败')
}
}
@@ -151,24 +190,37 @@ export default function CodexProxyConfig() {
const handleTestProxy = async (id: number) => {
if (testingIds.has(id)) return
setTestingIds(prev => new Set(prev).add(id))
// 找到当前代理用于显示
const proxy = proxies.find(p => p.id === id)
const proxyDisplay = proxy ? formatProxyDisplay(proxy.proxy_url) : `ID:${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 }
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
))
// 使用 toast 提示成功
const location = data.data.location || '未知'
const ip = data.data.ip || ''
toast.success(`${proxyDisplay} 测试成功 → ${ip} (${location})`)
} else {
alert(`测试失败: ${data.message}`)
// 虽然失败也需要刷新列表以获取最新的统计数据
fetchProxies()
// 测试失败,局部更新失败计数
setProxies(prev => prev.map(p =>
p.id === id
? { ...p, fail_count: p.fail_count + 1 }
: p
))
toast.error(`${proxyDisplay} 测试失败: ${data.message || '未知错误'}`)
}
} catch (error) {
console.error('测试代理出错:', error)
alert('网络错误,测试失败')
toast.error(`${proxyDisplay} 网络错误,测试失败`)
} finally {
setTestingIds(prev => {
const next = new Set(prev)
@@ -182,7 +234,6 @@ export default function CodexProxyConfig() {
const handleTestAll = async () => {
if (testingAll || proxies.length === 0) return
setTestingAll(true)
setMessage(null)
let successCount = 0
let failCount = 0
@@ -236,10 +287,15 @@ export default function CodexProxyConfig() {
// 刷新列表获取最新数据
await fetchProxies()
setTestingAll(false)
setMessage({
type: successCount > 0 ? 'success' : 'error',
text: `测试完成: ${successCount} 成功, ${failCount} 失败`
})
// 使用 toast 提示结果
if (successCount > 0 && failCount === 0) {
toast.success(`一键测试完成: 全部 ${successCount} 个代理测试成功`)
} else if (successCount === 0 && failCount > 0) {
toast.error(`一键测试完成: 全部 ${failCount} 个代理测试失败`)
} else {
toast.info(`一键测试完成: ${successCount} 成功, ${failCount} 失败`)
}
}
// 清空所有
@@ -249,11 +305,14 @@ export default function CodexProxyConfig() {
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()
// 局部清空
setProxies([])
setStats({ total: 0, enabled: 0, disabled: 0 })
toast.success('已清空所有代理')
}
} catch (error) {
console.error('清空失败:', error)
toast.error('清空失败')
}
}
@@ -326,16 +385,6 @@ export default function CodexProxyConfig() {
</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>