feat: Implement initial Codex Pool backend server with comprehensive APIs for configuration, S2A integration, owner management, batch processing, monitoring, and corresponding frontend pages.

This commit is contained in:
2026-02-01 03:45:53 +08:00
parent 8560a33f36
commit e27e36b0e0
8 changed files with 297 additions and 31 deletions

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -98,6 +99,7 @@ func startServer(cfg *config.Config) {
// 基础 API // 基础 API
mux.HandleFunc("/api/health", api.CORS(handleHealth)) mux.HandleFunc("/api/health", api.CORS(handleHealth))
mux.HandleFunc("/api/config", api.CORS(handleConfig)) mux.HandleFunc("/api/config", api.CORS(handleConfig))
mux.HandleFunc("/api/proxy/test", api.CORS(handleProxyTest)) // 代理测试
// 日志 API // 日志 API
mux.HandleFunc("/api/logs", api.CORS(handleGetLogs)) 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)) mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
// 母号封禁检查 API // 母号封禁检查 API
mux.HandleFunc("/api/db/owners/ban-check", api.CORS(api.HandleManualBanCheck)) // 手动触发检查 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/status", api.CORS(api.HandleBanCheckStatus)) // 检查状态
mux.HandleFunc("/api/db/owners/ban-check/settings", api.CORS(api.HandleBanCheckSettings)) // 配置 mux.HandleFunc("/api/db/owners/ban-check/settings", api.CORS(api.HandleBanCheckSettings)) // 配置
// 注册测试 API // 注册测试 API
@@ -956,3 +958,84 @@ func handleCleanerSettings(w http.ResponseWriter, r *http.Request) {
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法") 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)
}

View File

@@ -11,12 +11,13 @@ import (
// MonitorSettings 监控设置 // MonitorSettings 监控设置
type MonitorSettings struct { type MonitorSettings struct {
Target int `json:"target"` Target int `json:"target"`
AutoAdd bool `json:"auto_add"` AutoAdd bool `json:"auto_add"`
MinInterval int `json:"min_interval"` MinInterval int `json:"min_interval"`
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒) CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
PollingEnabled bool `json:"polling_enabled"` PollingEnabled bool `json:"polling_enabled"`
PollingInterval int `json:"polling_interval"` PollingInterval int `json:"polling_interval"`
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
} }
// HandleGetMonitorSettings 获取监控设置 // HandleGetMonitorSettings 获取监控设置
@@ -32,12 +33,13 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
} }
settings := MonitorSettings{ settings := MonitorSettings{
Target: 50, Target: 50,
AutoAdd: false, AutoAdd: false,
MinInterval: 300, MinInterval: 300,
CheckInterval: 60, CheckInterval: 60,
PollingEnabled: false, PollingEnabled: false,
PollingInterval: 60, PollingInterval: 60,
ReplenishUseProxy: false,
} }
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" { if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
@@ -66,6 +68,9 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
settings.PollingInterval = v settings.PollingInterval = v
} }
} }
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
settings.ReplenishUseProxy = true
}
Success(w, settings) 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 { if err := database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval)); err != nil {
saveErrors = append(saveErrors, "polling_interval: "+err.Error()) 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 { if len(saveErrors) > 0 {
errMsg := "保存监控设置部分失败: " + saveErrors[0] errMsg := "保存监控设置部分失败: " + saveErrors[0]

View File

@@ -14,7 +14,7 @@ import (
// S2AAccountItem S2A 账号信息 // S2AAccountItem S2A 账号信息
type S2AAccountItem struct { type S2AAccountItem struct {
ID int `json:"id"` ID int `json:"id"`
Email string `json:"email"` Email string `json:"account"` // S2A API 返回的字段名是 account
Status string `json:"status"` Status string `json:"status"`
} }

View File

@@ -46,6 +46,10 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
priority: serverConfig.priority || 0, priority: serverConfig.priority || 0,
groupIds: serverConfig.group_ids || [], groupIds: serverConfig.group_ids || [],
}, },
proxy: {
default: serverConfig.default_proxy || '',
enabled: serverConfig.proxy_enabled || false,
},
})) }))
// 更新站点名称 // 更新站点名称
if (serverConfig.site_name) { if (serverConfig.site_name) {

View File

@@ -10,7 +10,10 @@ import {
XCircle, XCircle,
Save, Save,
Loader2, Loader2,
Globe Globe,
Wifi,
WifiOff,
HelpCircle
} from 'lucide-react' } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { useConfig } from '../hooks/useConfig' import { useConfig } from '../hooks/useConfig'
@@ -18,23 +21,29 @@ import { useConfig } from '../hooks/useConfig'
export default function Config() { export default function Config() {
const { config, isConnected, refreshConfig } = useConfig() const { config, isConnected, refreshConfig } = useConfig()
const [siteName, setSiteName] = useState('') const [siteName, setSiteName] = useState('')
const [defaultProxy, setDefaultProxy] = useState('')
const [saving, setSaving] = useState(false) 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) const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
// 加载站点名称配置 // 加载站点名称和代理配置
useEffect(() => { useEffect(() => {
const fetchSiteName = async () => { const fetchConfig = async () => {
try { try {
const res = await fetch('/api/config') const res = await fetch('/api/config')
const data = await res.json() const data = await res.json()
if (data.code === 0 && data.data) { if (data.code === 0 && data.data) {
setSiteName(data.data.site_name || 'Codex Pool') setSiteName(data.data.site_name || 'Codex Pool')
setDefaultProxy(data.data.default_proxy || '')
} }
} catch (error) { } 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 = [ const configItems = [
{ {
to: '/config/s2a', to: '/config/s2a',
@@ -178,6 +246,72 @@ export default function Config() {
</p> </p>
</div> </div>
{/* 代理地址配置 */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-2 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
</label>
{/* 代理状态徽章 */}
{proxyStatus === 'success' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
<Wifi className="h-3 w-3" />
</span>
)}
{proxyStatus === 'error' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
<WifiOff className="h-3 w-3" />
</span>
)}
{proxyStatus === 'unknown' && defaultProxy && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400">
<HelpCircle className="h-3 w-3" />
</span>
)}
</div>
<div className="flex gap-3">
<Input
value={defaultProxy}
onChange={(e) => {
setDefaultProxy(e.target.value)
setProxyStatus('unknown')
}}
placeholder="http://127.0.0.1:7890"
className="flex-1"
/>
<Button
size="sm"
onClick={handleSaveProxy}
disabled={savingProxy}
icon={savingProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
className="shrink-0"
>
{savingProxy ? '保存中...' : '保存代理'}
</Button>
<Button
size="sm"
variant="outline"
onClick={handleTestProxy}
disabled={testingProxy || !defaultProxy.trim()}
icon={testingProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Wifi className="h-4 w-4" />}
className="shrink-0"
>
{testingProxy ? '测试中...' : '测试连接'}
</Button>
</div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{proxyOriginIP && (
<span className="ml-2 text-green-600 dark:text-green-400">
IP: {proxyOriginIP}
</span>
)}
</p>
</div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -67,6 +67,8 @@ export default function Monitor() {
const [checkInterval, setCheckInterval] = useState(60) const [checkInterval, setCheckInterval] = useState(60)
const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingEnabled, setPollingEnabled] = useState(false)
const [pollingInterval, setPollingInterval] = useState(60) const [pollingInterval, setPollingInterval] = useState(60)
const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
// 倒计时状态 // 倒计时状态
const [countdown, setCountdown] = useState(60) const [countdown, setCountdown] = useState(60)
@@ -129,6 +131,7 @@ export default function Monitor() {
check_interval: checkInterval, check_interval: checkInterval,
polling_enabled: pollingEnabled, polling_enabled: pollingEnabled,
polling_interval: pollingInterval, polling_interval: pollingInterval,
replenish_use_proxy: replenishUseProxy,
}), }),
}) })
const data = await res.json() const data = await res.json()
@@ -253,6 +256,7 @@ export default function Monitor() {
// 从后端加载监控设置 // 从后端加载监控设置
const loadMonitorSettings = async () => { const loadMonitorSettings = async () => {
try { try {
// 加载监控设置
const res = await fetch('/api/monitor/settings') const res = await fetch('/api/monitor/settings')
if (res.ok) { if (res.ok) {
const json = await res.json() const json = await res.json()
@@ -264,6 +268,7 @@ export default function Monitor() {
const checkIntervalVal = s.check_interval || 60 const checkIntervalVal = s.check_interval || 60
const pollingEnabledVal = s.polling_enabled || false const pollingEnabledVal = s.polling_enabled || false
const interval = s.polling_interval || 60 const interval = s.polling_interval || 60
const replenishUseProxyVal = s.replenish_use_proxy || false
setTargetInput(target) setTargetInput(target)
setAutoAdd(autoAddVal) setAutoAdd(autoAddVal)
@@ -271,11 +276,21 @@ export default function Monitor() {
setCheckInterval(checkIntervalVal) setCheckInterval(checkIntervalVal)
setPollingEnabled(pollingEnabledVal) setPollingEnabled(pollingEnabledVal)
setPollingInterval(interval) setPollingInterval(interval)
setReplenishUseProxy(replenishUseProxyVal)
savedPollingIntervalRef.current = interval savedPollingIntervalRef.current = interval
setCountdown(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) { } catch (e) {
@@ -511,6 +526,15 @@ export default function Monitor() {
description="开启后,当号池不足时自动补充账号" description="开启后,当号池不足时自动补充账号"
/> />
</div> </div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch
checked={replenishUseProxy}
onChange={setReplenishUseProxy}
disabled={!autoAdd || !globalProxy}
label="补号时使用代理"
description={globalProxy ? `当前代理: ${globalProxy}` : '请先在系统配置中设置代理地址'}
/>
</div>
<Input <Input
label="最小间隔 (秒)" label="最小间隔 (秒)"
type="number" type="number"

View File

@@ -70,10 +70,13 @@ export default function Upload() {
const [membersPerTeam, setMembersPerTeam] = useState(4) const [membersPerTeam, setMembersPerTeam] = useState(4)
const [concurrentTeams, setConcurrentTeams] = useState(2) const [concurrentTeams, setConcurrentTeams] = useState(2)
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
const [proxy, setProxy] = useState('') const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库 const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
const [processCount, setProcessCount] = useState(0) // 处理数量0表示全部 const [processCount, setProcessCount] = useState(0) // 处理数量0表示全部
// 获取全局代理地址
const globalProxy = config.proxy?.default || ''
const hasConfig = config.s2a.apiBase && config.s2a.adminKey const hasConfig = config.s2a.apiBase && config.s2a.adminKey
// Load stats // Load stats
@@ -191,7 +194,7 @@ export default function Upload() {
concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1), concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1),
browser_type: browserType, browser_type: browserType,
headless: true, // 始终使用无头模式 headless: true, // 始终使用无头模式
proxy, proxy: useProxy ? globalProxy : '',
include_owner: includeOwner, // 母号也入库 include_owner: includeOwner, // 母号也入库
process_count: processCount, // 处理数量0表示全部 process_count: processCount, // 处理数量0表示全部
}), }),
@@ -209,7 +212,7 @@ export default function Upload() {
alert('启动失败') alert('启动失败')
} }
setLoading(false) setLoading(false)
}, [stats, membersPerTeam, concurrentTeams, browserType, proxy, includeOwner, processCount, fetchStatus]) }, [stats, membersPerTeam, concurrentTeams, browserType, useProxy, globalProxy, includeOwner, processCount, fetchStatus])
// 停止处理 // 停止处理
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
@@ -469,13 +472,15 @@ export default function Upload() {
</div> </div>
</div> </div>
<Input <div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
label="代理地址(可选)" <Switch
placeholder="http://127.0.0.1:7890" checked={useProxy}
value={proxy} onChange={setUseProxy}
onChange={(e) => setProxy(e.target.value)} disabled={isRunning || !globalProxy}
disabled={isRunning} label="使用全局代理"
/> description={globalProxy ? `当前代理: ${globalProxy}` : '请先在系统配置中设置代理地址'}
/>
</div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"> <div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch <Switch

View File

@@ -175,6 +175,10 @@ export interface AppConfig {
email: { email: {
services: MailServiceConfig[] // 多个邮箱服务配置 services: MailServiceConfig[] // 多个邮箱服务配置
} }
proxy: {
default: string // 全局默认代理地址
enabled: boolean // 是否启用代理
}
} }
// 默认配置 // 默认配置
@@ -203,6 +207,10 @@ export const defaultConfig: AppConfig = {
}, },
], ],
}, },
proxy: {
default: '',
enabled: false,
},
} }
// 检查结果 // 检查结果