From e27e36b0e064dc01fe1c384e23f85baac5d59804 Mon Sep 17 00:00:00 2001
From: kyx236
Date: Sun, 1 Feb 2026 03:45:53 +0800
Subject: [PATCH] feat: Implement initial Codex Pool backend server with
comprehensive APIs for configuration, S2A integration, owner management,
batch processing, monitoring, and corresponding frontend pages.
---
backend/cmd/main.go | 87 ++++++++++++++-
backend/internal/api/monitor.go | 32 +++---
backend/internal/api/s2a_clean.go | 2 +-
frontend/src/context/ConfigContext.tsx | 4 +
frontend/src/pages/Config.tsx | 144 ++++++++++++++++++++++++-
frontend/src/pages/Monitor.tsx | 26 ++++-
frontend/src/pages/Upload.tsx | 25 +++--
frontend/src/types/index.ts | 8 ++
8 files changed, 297 insertions(+), 31 deletions(-)
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index c1ebe25..8c69f82 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -7,6 +7,7 @@ import (
"io"
"net"
"net/http"
+ "net/url"
"os"
"path/filepath"
"strconv"
@@ -98,6 +99,7 @@ func startServer(cfg *config.Config) {
// 基础 API
mux.HandleFunc("/api/health", api.CORS(handleHealth))
mux.HandleFunc("/api/config", api.CORS(handleConfig))
+ mux.HandleFunc("/api/proxy/test", api.CORS(handleProxyTest)) // 代理测试
// 日志 API
mux.HandleFunc("/api/logs", api.CORS(handleGetLogs))
@@ -126,8 +128,8 @@ func startServer(cfg *config.Config) {
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
// 母号封禁检查 API
- mux.HandleFunc("/api/db/owners/ban-check", api.CORS(api.HandleManualBanCheck)) // 手动触发检查
- mux.HandleFunc("/api/db/owners/ban-check/status", api.CORS(api.HandleBanCheckStatus)) // 检查状态
+ mux.HandleFunc("/api/db/owners/ban-check", api.CORS(api.HandleManualBanCheck)) // 手动触发检查
+ mux.HandleFunc("/api/db/owners/ban-check/status", api.CORS(api.HandleBanCheckStatus)) // 检查状态
mux.HandleFunc("/api/db/owners/ban-check/settings", api.CORS(api.HandleBanCheckSettings)) // 配置
// 注册测试 API
@@ -956,3 +958,84 @@ func handleCleanerSettings(w http.ResponseWriter, r *http.Request) {
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
}
}
+
+// handleProxyTest POST /api/proxy/test - 测试代理连接
+func handleProxyTest(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
+ return
+ }
+
+ var req struct {
+ ProxyURL string `json:"proxy_url"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ api.Error(w, http.StatusBadRequest, "请求格式错误")
+ return
+ }
+
+ proxyURL := req.ProxyURL
+ if proxyURL == "" {
+ api.Error(w, http.StatusBadRequest, "代理地址不能为空")
+ return
+ }
+
+ logger.Info(fmt.Sprintf("测试代理连接: %s", proxyURL), "", "proxy")
+
+ // 解析代理 URL
+ proxyParsed, err := parseProxyURL(proxyURL)
+ if err != nil {
+ logger.Error(fmt.Sprintf("代理地址格式错误: %v", err), "", "proxy")
+ api.Error(w, http.StatusBadRequest, fmt.Sprintf("代理地址格式错误: %v", err))
+ return
+ }
+
+ // 创建带代理的 HTTP 客户端
+ client := &http.Client{
+ Timeout: 15 * time.Second,
+ Transport: &http.Transport{
+ Proxy: http.ProxyURL(proxyParsed),
+ },
+ }
+
+ // 测试请求 - 使用 httpbin.org/ip 获取出口 IP
+ testURL := "https://httpbin.org/ip"
+ resp, err := client.Get(testURL)
+ if err != nil {
+ logger.Error(fmt.Sprintf("代理连接失败: %v", err), "", "proxy")
+ api.Error(w, http.StatusBadGateway, fmt.Sprintf("代理连接失败: %v", err))
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ logger.Error(fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode), "", "proxy")
+ api.Error(w, http.StatusBadGateway, fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode))
+ return
+ }
+
+ // 解析响应获取出口 IP
+ var ipResp struct {
+ Origin string `json:"origin"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&ipResp); err != nil {
+ logger.Warning(fmt.Sprintf("解析代理响应失败: %v", err), "", "proxy")
+ }
+
+ logger.Success(fmt.Sprintf("代理连接成功, 出口IP: %s", ipResp.Origin), "", "proxy")
+
+ api.Success(w, map[string]interface{}{
+ "connected": true,
+ "message": "代理连接成功",
+ "origin_ip": ipResp.Origin,
+ })
+}
+
+// parseProxyURL 解析代理 URL
+func parseProxyURL(proxyURL string) (*url.URL, error) {
+ // 如果没有协议前缀,默认添加 http://
+ if !strings.HasPrefix(proxyURL, "http://") && !strings.HasPrefix(proxyURL, "https://") && !strings.HasPrefix(proxyURL, "socks5://") {
+ proxyURL = "http://" + proxyURL
+ }
+ return url.Parse(proxyURL)
+}
diff --git a/backend/internal/api/monitor.go b/backend/internal/api/monitor.go
index 37180bb..b302d41 100644
--- a/backend/internal/api/monitor.go
+++ b/backend/internal/api/monitor.go
@@ -11,12 +11,13 @@ import (
// MonitorSettings 监控设置
type MonitorSettings struct {
- Target int `json:"target"`
- AutoAdd bool `json:"auto_add"`
- MinInterval int `json:"min_interval"`
- CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
- PollingEnabled bool `json:"polling_enabled"`
- PollingInterval int `json:"polling_interval"`
+ Target int `json:"target"`
+ AutoAdd bool `json:"auto_add"`
+ MinInterval int `json:"min_interval"`
+ CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
+ PollingEnabled bool `json:"polling_enabled"`
+ PollingInterval int `json:"polling_interval"`
+ ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
}
// HandleGetMonitorSettings 获取监控设置
@@ -32,12 +33,13 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
}
settings := MonitorSettings{
- Target: 50,
- AutoAdd: false,
- MinInterval: 300,
- CheckInterval: 60,
- PollingEnabled: false,
- PollingInterval: 60,
+ Target: 50,
+ AutoAdd: false,
+ MinInterval: 300,
+ CheckInterval: 60,
+ PollingEnabled: false,
+ PollingInterval: 60,
+ ReplenishUseProxy: false,
}
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
@@ -66,6 +68,9 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
settings.PollingInterval = v
}
}
+ if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
+ settings.ReplenishUseProxy = true
+ }
Success(w, settings)
}
@@ -120,6 +125,9 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
if err := database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval)); err != nil {
saveErrors = append(saveErrors, "polling_interval: "+err.Error())
}
+ if err := database.Instance.SetConfig("monitor_replenish_use_proxy", strconv.FormatBool(settings.ReplenishUseProxy)); err != nil {
+ saveErrors = append(saveErrors, "replenish_use_proxy: "+err.Error())
+ }
if len(saveErrors) > 0 {
errMsg := "保存监控设置部分失败: " + saveErrors[0]
diff --git a/backend/internal/api/s2a_clean.go b/backend/internal/api/s2a_clean.go
index 86eeb1a..6cf5ebc 100644
--- a/backend/internal/api/s2a_clean.go
+++ b/backend/internal/api/s2a_clean.go
@@ -14,7 +14,7 @@ import (
// S2AAccountItem S2A 账号信息
type S2AAccountItem struct {
ID int `json:"id"`
- Email string `json:"email"`
+ Email string `json:"account"` // S2A API 返回的字段名是 account
Status string `json:"status"`
}
diff --git a/frontend/src/context/ConfigContext.tsx b/frontend/src/context/ConfigContext.tsx
index 966fe43..adfeaa7 100644
--- a/frontend/src/context/ConfigContext.tsx
+++ b/frontend/src/context/ConfigContext.tsx
@@ -46,6 +46,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
priority: serverConfig.priority || 0,
groupIds: serverConfig.group_ids || [],
},
+ proxy: {
+ default: serverConfig.default_proxy || '',
+ enabled: serverConfig.proxy_enabled || false,
+ },
}))
// 更新站点名称
if (serverConfig.site_name) {
diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx
index 6c63a32..bce59cb 100644
--- a/frontend/src/pages/Config.tsx
+++ b/frontend/src/pages/Config.tsx
@@ -10,7 +10,10 @@ import {
XCircle,
Save,
Loader2,
- Globe
+ Globe,
+ Wifi,
+ WifiOff,
+ HelpCircle
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { useConfig } from '../hooks/useConfig'
@@ -18,23 +21,29 @@ import { useConfig } from '../hooks/useConfig'
export default function Config() {
const { config, isConnected, refreshConfig } = useConfig()
const [siteName, setSiteName] = useState('')
+ const [defaultProxy, setDefaultProxy] = useState('')
const [saving, setSaving] = useState(false)
+ const [savingProxy, setSavingProxy] = useState(false)
+ const [testingProxy, setTestingProxy] = useState(false)
+ const [proxyStatus, setProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
+ const [proxyOriginIP, setProxyOriginIP] = useState('')
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
- // 加载站点名称配置
+ // 加载站点名称和代理配置
useEffect(() => {
- const fetchSiteName = async () => {
+ const fetchConfig = async () => {
try {
const res = await fetch('/api/config')
const data = await res.json()
if (data.code === 0 && data.data) {
setSiteName(data.data.site_name || 'Codex Pool')
+ setDefaultProxy(data.data.default_proxy || '')
}
} catch (error) {
- console.error('Failed to fetch site name:', error)
+ console.error('Failed to fetch config:', error)
}
}
- fetchSiteName()
+ fetchConfig()
}, [])
// 保存站点名称
@@ -61,6 +70,65 @@ export default function Config() {
}
}
+ // 保存代理地址
+ const handleSaveProxy = async () => {
+ setSavingProxy(true)
+ setMessage(null)
+ try {
+ const res = await fetch('/api/config', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ default_proxy: defaultProxy }),
+ })
+ const data = await res.json()
+ if (data.code === 0) {
+ setMessage({ type: 'success', text: '代理地址已保存' })
+ setProxyStatus('unknown') // 保存后重置状态
+ setProxyOriginIP('')
+ refreshConfig()
+ } else {
+ setMessage({ type: 'error', text: data.message || '保存失败' })
+ }
+ } catch {
+ setMessage({ type: 'error', text: '网络错误' })
+ } finally {
+ setSavingProxy(false)
+ }
+ }
+
+ // 测试代理连接
+ const handleTestProxy = async () => {
+ if (!defaultProxy.trim()) {
+ setMessage({ type: 'error', text: '请先输入代理地址' })
+ return
+ }
+ setTestingProxy(true)
+ setMessage(null)
+ setProxyStatus('unknown')
+ setProxyOriginIP('')
+ try {
+ const res = await fetch('/api/proxy/test', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ proxy_url: defaultProxy }),
+ })
+ const data = await res.json()
+ if (data.code === 0 && data.data?.connected) {
+ setProxyStatus('success')
+ setProxyOriginIP(data.data.origin_ip || '')
+ setMessage({ type: 'success', text: `代理连接成功${data.data.origin_ip ? `, 出口IP: ${data.data.origin_ip}` : ''}` })
+ } else {
+ setProxyStatus('error')
+ setMessage({ type: 'error', text: data.message || '代理连接失败' })
+ }
+ } catch (e) {
+ setProxyStatus('error')
+ setMessage({ type: 'error', text: e instanceof Error ? e.message : '网络错误' })
+ } finally {
+ setTestingProxy(false)
+ }
+ }
+
const configItems = [
{
to: '/config/s2a',
@@ -178,6 +246,72 @@ export default function Config() {
该名称将显示在侧边栏标题和浏览器标签页
+
+ {/* 代理地址配置 */}
+
+
+
+ {/* 代理状态徽章 */}
+ {proxyStatus === 'success' && (
+
+
+ 代理可用
+
+ )}
+ {proxyStatus === 'error' && (
+
+
+ 连接失败
+
+ )}
+ {proxyStatus === 'unknown' && defaultProxy && (
+
+
+ 未测试
+
+ )}
+
+
+ {
+ setDefaultProxy(e.target.value)
+ setProxyStatus('unknown')
+ }}
+ placeholder="http://127.0.0.1:7890"
+ className="flex-1"
+ />
+ : }
+ className="shrink-0"
+ >
+ {savingProxy ? '保存中...' : '保存代理'}
+
+ : }
+ className="shrink-0"
+ >
+ {testingProxy ? '测试中...' : '测试连接'}
+
+
+
+ 设置全局默认代理地址,用于批量入库和自动补号
+ {proxyOriginIP && (
+
+ 出口IP: {proxyOriginIP}
+
+ )}
+
+
diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx
index f70058b..5ec55ca 100644
--- a/frontend/src/pages/Monitor.tsx
+++ b/frontend/src/pages/Monitor.tsx
@@ -67,6 +67,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 [countdown, setCountdown] = useState(60)
@@ -129,6 +131,7 @@ export default function Monitor() {
check_interval: checkInterval,
polling_enabled: pollingEnabled,
polling_interval: pollingInterval,
+ replenish_use_proxy: replenishUseProxy,
}),
})
const data = await res.json()
@@ -253,6 +256,7 @@ export default function Monitor() {
// 从后端加载监控设置
const loadMonitorSettings = async () => {
try {
+ // 加载监控设置
const res = await fetch('/api/monitor/settings')
if (res.ok) {
const json = await res.json()
@@ -264,6 +268,7 @@ export default function Monitor() {
const checkIntervalVal = s.check_interval || 60
const pollingEnabledVal = s.polling_enabled || false
const interval = s.polling_interval || 60
+ const replenishUseProxyVal = s.replenish_use_proxy || false
setTargetInput(target)
setAutoAdd(autoAddVal)
@@ -271,11 +276,21 @@ export default function Monitor() {
setCheckInterval(checkIntervalVal)
setPollingEnabled(pollingEnabledVal)
setPollingInterval(interval)
+ setReplenishUseProxy(replenishUseProxyVal)
savedPollingIntervalRef.current = interval
setCountdown(interval)
// 返回加载的配置用于后续刷新
- return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval }
+ return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval, replenishUseProxy: replenishUseProxyVal }
+ }
+ }
+
+ // 加载全局代理配置
+ const configRes = await fetch('/api/config')
+ if (configRes.ok) {
+ const configJson = await configRes.json()
+ if (configJson.code === 0 && configJson.data) {
+ setGlobalProxy(configJson.data.default_proxy || '')
}
}
} catch (e) {
@@ -511,6 +526,15 @@ export default function Monitor() {
description="开启后,当号池不足时自动补充账号"
/>
+
+
+
('chromedp')
- const [proxy, setProxy] = useState('')
+ const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
const [processCount, setProcessCount] = useState(0) // 处理数量,0表示全部
+ // 获取全局代理地址
+ const globalProxy = config.proxy?.default || ''
+
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
// Load stats
@@ -191,7 +194,7 @@ export default function Upload() {
concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1),
browser_type: browserType,
headless: true, // 始终使用无头模式
- proxy,
+ proxy: useProxy ? globalProxy : '',
include_owner: includeOwner, // 母号也入库
process_count: processCount, // 处理数量,0表示全部
}),
@@ -209,7 +212,7 @@ export default function Upload() {
alert('启动失败')
}
setLoading(false)
- }, [stats, membersPerTeam, concurrentTeams, browserType, proxy, includeOwner, processCount, fetchStatus])
+ }, [stats, membersPerTeam, concurrentTeams, browserType, useProxy, globalProxy, includeOwner, processCount, fetchStatus])
// 停止处理
const handleStop = useCallback(async () => {
@@ -469,13 +472,15 @@ export default function Upload() {
- setProxy(e.target.value)}
- disabled={isRunning}
- />
+
+
+