diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 2662667..0445a1b 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -174,6 +174,7 @@ func startServer(cfg *config.Config) { // CodexAuth 代理池 API mux.HandleFunc("/api/codex-proxy", api.CORS(api.HandleCodexProxies)) mux.HandleFunc("/api/codex-proxy/test", api.CORS(api.HandleCodexProxies)) + mux.HandleFunc("/api/codex-proxy/stats", api.CORS(api.HandleCodexProxyStats)) // Owner 降级 API mux.HandleFunc("/api/demote/owner", api.CORS(api.HandleDemoteOwner)) diff --git a/backend/internal/api/codex_proxy.go b/backend/internal/api/codex_proxy.go index f03954d..54b0cfa 100644 --- a/backend/internal/api/codex_proxy.go +++ b/backend/internal/api/codex_proxy.go @@ -32,6 +32,20 @@ func HandleCodexProxies(w http.ResponseWriter, r *http.Request) { } } +// HandleCodexProxyStats 获取代理池统计 +func HandleCodexProxyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + Error(w, http.StatusMethodNotAllowed, "仅支持 GET") + return + } + if database.Instance == nil { + Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + stats := database.Instance.GetCodexProxyStats() + Success(w, stats) +} + // listCodexProxies 获取代理列表 func listCodexProxies(w http.ResponseWriter, r *http.Request) { proxies, err := database.Instance.GetCodexProxies() diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index c5c2d18..a2fbd8a 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -855,8 +855,14 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul return false } - // 注册 - _, err := register.APIRegister(currentEmail, currentPassword, name, birthdate, req.Proxy, memberLogPrefix) + // 注册 - 从代理池获取随机代理 + regProxy := req.Proxy + if database.Instance != nil { + if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" { + regProxy = poolProxy + } + } + _, err := register.APIRegister(currentEmail, currentPassword, name, birthdate, regProxy, memberLogPrefix) if err != nil { logger.Error(fmt.Sprintf("%s [注册失败] %v", memberLogPrefix, err), currentEmail, "team") continue diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx index c253540..62126d0 100644 --- a/frontend/src/pages/Monitor.tsx +++ b/frontend/src/pages/Monitor.tsx @@ -54,8 +54,8 @@ export default function Monitor() { const [checkInterval, setCheckInterval] = useState(60) const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingInterval, setPollingInterval] = useState(60) - const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理 - const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示) + const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理池 + const [proxyPoolStats, setProxyPoolStats] = useState<{ total: number; enabled: number } | null>(null) // 代理池统计 const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式 // 自动补号配置 @@ -238,12 +238,16 @@ export default function Monitor() { // 从后端加载监控设置 const loadMonitorSettings = async () => { try { - // 加载全局代理配置(始终执行) - const configRes = await fetch('/api/config') + // 加载代理池统计和全局配置(并行) + const [configRes, proxyStatsRes, settingsRes] = await Promise.all([ + fetch('/api/config'), + fetch('/api/codex-proxy/stats'), + fetch('/api/monitor/settings'), + ]) + if (configRes.ok) { const configJson = await configRes.json() if (configJson.code === 0 && configJson.data) { - setGlobalProxy(configJson.data.default_proxy || '') // 加载授权方式配置 if (configJson.data.auth_method) { setAuthMethod(configJson.data.auth_method === 'api' ? 'api' : 'browser') @@ -251,10 +255,16 @@ export default function Monitor() { } } + if (proxyStatsRes.ok) { + const proxyJson = await proxyStatsRes.json() + if (proxyJson.code === 0 && proxyJson.data) { + setProxyPoolStats(proxyJson.data) + } + } + // 加载监控设置 - const res = await fetch('/api/monitor/settings') - if (res.ok) { - const json = await res.json() + if (settingsRes.ok) { + const json = await settingsRes.json() if (json.code === 0 && json.data) { const s = json.data const target = s.target || 50 @@ -525,12 +535,12 @@ export default function Monitor() { 0 + ? `代理池: ${proxyPoolStats.enabled} 个可用 / ${proxyPoolStats.total} 个总计` + : '代理池为空,请先在"代理池配置"页面添加代理' } /> @@ -757,10 +767,10 @@ export default function Monitor() { type="checkbox" checked={autoRegUseProxy} onChange={(e) => setAutoRegUseProxy(e.target.checked)} - disabled={!globalProxy} + disabled={!proxyPoolStats || proxyPoolStats.enabled === 0} className="h-4 w-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500 disabled:opacity-50" /> - 使用代理 + 使用代理池{proxyPoolStats && proxyPoolStats.enabled > 0 ? ` (${proxyPoolStats.enabled})` : ''} diff --git a/frontend/src/pages/Upload.tsx b/frontend/src/pages/Upload.tsx index e7af65e..bd81eee 100644 --- a/frontend/src/pages/Upload.tsx +++ b/frontend/src/pages/Upload.tsx @@ -72,28 +72,33 @@ export default function Upload() { const [membersPerTeam, setMembersPerTeam] = useState(4) const [concurrentTeams, setConcurrentTeams] = useState(2) const [concurrentS2A, setConcurrentS2A] = useState(2) // 入库并发数 - const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理 + const [useProxy, setUseProxy] = useState(false) // 是否使用代理池 const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库 const [processCount, setProcessCount] = useState(0) // 处理数量,0表示全部 const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式 + const [proxyPoolStats, setProxyPoolStats] = useState<{ total: number; enabled: number } | null>(null) - // 获取全局代理地址 - const globalProxy = config.proxy?.default || '' - - // 加载全局配置中的授权方式 + // 加载全局配置中的授权方式和代理池统计 useEffect(() => { - const fetchAuthMethod = async () => { + const fetchConfig = async () => { try { - const res = await fetch('/api/config') - const data = await res.json() - if (data.code === 0 && data.data?.auth_method) { - setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser') + const [configRes, proxyRes] = await Promise.all([ + fetch('/api/config'), + fetch('/api/codex-proxy/stats'), + ]) + const configData = await configRes.json() + if (configData.code === 0 && configData.data?.auth_method) { + setAuthMethod(configData.data.auth_method === 'api' ? 'api' : 'browser') + } + const proxyData = await proxyRes.json() + if (proxyData.code === 0 && proxyData.data) { + setProxyPoolStats(proxyData.data) } } catch (error) { - console.error('Failed to fetch auth method:', error) + console.error('Failed to fetch config:', error) } } - fetchAuthMethod() + fetchConfig() }, []) const hasConfig = config.s2a.apiBase && config.s2a.adminKey @@ -214,7 +219,7 @@ export default function Upload() { concurrent_s2a: concurrentS2A, // 入库并发数 browser_type: 'chromedp', // 固定使用 chromedp headless: true, // 始终使用无头模式 - proxy: useProxy ? globalProxy : '', + proxy: useProxy ? 'pool:random' : '', include_owner: includeOwner, // 母号也入库 process_count: processCount, // 处理数量,0表示全部 }), @@ -232,7 +237,7 @@ export default function Upload() { alert('启动失败') } setLoading(false) - }, [stats, membersPerTeam, concurrentTeams, concurrentS2A, useProxy, globalProxy, includeOwner, processCount, fetchStatus]) + }, [stats, membersPerTeam, concurrentTeams, concurrentS2A, useProxy, includeOwner, processCount, fetchStatus]) // 停止处理 const handleStop = useCallback(async () => { @@ -508,12 +513,12 @@ export default function Upload() { 0 + ? `代理池: ${proxyPoolStats.enabled} 个可用 / ${proxyPoolStats.total} 个总计` + : '代理池为空,请先在"代理池配置"页面添加代理' } />