From 51ba54856d4db582d970183c9bf02a56a76aee23 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Tue, 3 Feb 2026 02:39:08 +0800 Subject: [PATCH] feat: Add automated ChatGPT account registration with backend API, TLS client, and fingerprinting, alongside new frontend pages for configuration, monitoring, and upload. --- backend/cmd/main.go | 3 + backend/go.mod | 16 +- backend/go.sum | 40 ++ backend/internal/api/codex_proxy.go | 172 ++++++++ backend/internal/api/team_process.go | 132 +++--- backend/internal/auth/codex_api.go | 20 +- backend/internal/auth/rod.go | 377 ----------------- backend/internal/client/fingerprints.go | 178 +++++--- backend/internal/client/tls.go | 260 ++++++++++-- backend/internal/database/sqlite.go | 197 +++++++++ backend/internal/register/chatgpt.go | 164 ++++---- frontend/src/App.tsx | 3 +- frontend/src/components/layout/Sidebar.tsx | 2 + frontend/src/pages/CodexProxyConfig.tsx | 448 +++++++++++++++++++++ frontend/src/pages/Config.tsx | 26 +- frontend/src/pages/Monitor.tsx | 10 +- frontend/src/pages/Upload.tsx | 7 +- frontend/src/pages/index.ts | 1 + 18 files changed, 1382 insertions(+), 674 deletions(-) create mode 100644 backend/internal/api/codex_proxy.go delete mode 100644 backend/internal/auth/rod.go create mode 100644 frontend/src/pages/CodexProxyConfig.tsx diff --git a/backend/cmd/main.go b/backend/cmd/main.go index a497f82..71a2b26 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -162,6 +162,9 @@ func startServer(cfg *config.Config) { mux.HandleFunc("/api/team-reg/import", api.CORS(api.HandleTeamRegImport)) mux.HandleFunc("/api/team-reg/clear-logs", api.CORS(api.HandleTeamRegClearLogs)) + // CodexAuth 代理池 API + mux.HandleFunc("/api/codex-proxy", api.CORS(api.HandleCodexProxies)) + // 嵌入的前端静态文件 if web.IsEmbedded() { webFS := web.GetFileSystem() diff --git a/backend/go.mod b/backend/go.mod index b002d78..0be5e94 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,22 +13,34 @@ require ( github.com/go-rod/rod v0.116.2 github.com/go-rod/stealth v0.4.9 github.com/mattn/go-sqlite3 v1.14.33 - github.com/refraction-networking/utls v1.6.7 + github.com/refraction-networking/utls v1.8.1 golang.org/x/net v0.48.0 ) require ( + github.com/Noooste/azuretls-client v1.12.12 // indirect + github.com/Noooste/fhttp v1.0.15 // indirect + github.com/Noooste/go-socks4 v0.0.2 // indirect + github.com/Noooste/uquic-go v1.0.5 // indirect + github.com/Noooste/utls v1.3.20 // indirect + github.com/Noooste/websocket v1.0.3 // indirect github.com/bdandy/go-errors v1.2.2 // indirect github.com/bdandy/go-socks4 v1.2.3 // indirect github.com/bogdanfinn/quic-go-utls v1.0.7-utls // indirect github.com/bogdanfinn/utls v1.7.7-barnius // indirect github.com/chromedp/sysutil v1.1.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/gaukas/clienthellod v0.4.2 // indirect + github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/google/gopacket v1.1.19 // indirect github.com/klauspost/compress v1.18.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect diff --git a/backend/go.sum b/backend/go.sum index cd209a5..e8efc7c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,15 @@ +github.com/Noooste/azuretls-client v1.12.12 h1:v+VjuZ0FBWM8b6ry8rwXusZVBeguft06HxiES79ILPo= +github.com/Noooste/azuretls-client v1.12.12/go.mod h1:ON+SmiBm4Zy5vAhJmBNZk61Y7nqf4iM/b1MC1lN47Bk= +github.com/Noooste/fhttp v1.0.15 h1:sYRWOKgr1x4L+wA6REMJCs4Z/lFOSJmuQHSIXMXCcPs= +github.com/Noooste/fhttp v1.0.15/go.mod h1:YZtq+i2M11Y22UiOR6gjNSLMNLiPhURh6M44oFVQ1TE= +github.com/Noooste/go-socks4 v0.0.2 h1:DwHCYiCEAdjfNrQOFIid7qgKCll7ubhGS1ji5O8FYng= +github.com/Noooste/go-socks4 v0.0.2/go.mod h1:+oOgtOFRsU8FoK7NBOhHSjiH5pveY8LgYNF5XcqVgjE= +github.com/Noooste/uquic-go v1.0.5 h1:HWfrxhxgB1a9Y2Au5mfFs2Y5Dy13OQIwa86D/kULPtE= +github.com/Noooste/uquic-go v1.0.5/go.mod h1:1y+qiy23PqLKudi4kQiJ0b3zXXYcyctEBRfZPTuyBz4= +github.com/Noooste/utls v1.3.20 h1:QzBNGGJ184bNMLodOzvM9YWc4vZ36QodIjqFQOHoZ88= +github.com/Noooste/utls v1.3.20/go.mod h1:XEy+VEbTxmH6krfSG5YT7wDbjHTEi2zUXTG33R0PAAg= +github.com/Noooste/websocket v1.0.3 h1:drW7tvZ3YqzqI9wApnaH1Q0syFMXO7gbLlsBWjZvMNA= +github.com/Noooste/websocket v1.0.3/go.mod h1:Qhw0Rtuju/fPPbcb3R5XGq7poa51qPDL462jTltl9nQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= @@ -20,8 +32,16 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= +github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gaukas/clienthellod v0.4.2 h1:LPJ+LSeqt99pqeCV4C0cllk+pyWmERisP7w6qWr7eqE= +github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA= +github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= +github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= @@ -35,10 +55,16 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s= @@ -49,6 +75,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= +github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= @@ -74,20 +102,32 @@ github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/api/codex_proxy.go b/backend/internal/api/codex_proxy.go new file mode 100644 index 0000000..0141b72 --- /dev/null +++ b/backend/internal/api/codex_proxy.go @@ -0,0 +1,172 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "codex-pool/internal/database" +) + +// HandleCodexProxies 处理代理池请求 +func HandleCodexProxies(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + listCodexProxies(w, r) + case http.MethodPost: + addCodexProxy(w, r) + case http.MethodDelete: + deleteCodexProxy(w, r) + case http.MethodPut: + toggleCodexProxy(w, r) + default: + Error(w, http.StatusMethodNotAllowed, "方法不允许") + } +} + +// listCodexProxies 获取代理列表 +func listCodexProxies(w http.ResponseWriter, r *http.Request) { + proxies, err := database.Instance.GetCodexProxies() + if err != nil { + Error(w, http.StatusInternalServerError, "获取代理列表失败: "+err.Error()) + return + } + + stats := database.Instance.GetCodexProxyStats() + + Success(w, map[string]interface{}{ + "proxies": proxies, + "stats": stats, + }) +} + +// AddProxyRequest 添加代理请求 +type AddProxyRequest struct { + ProxyURL string `json:"proxy_url"` + Description string `json:"description"` + Proxies []string `json:"proxies"` // 批量添加 +} + +// addCodexProxy 添加代理 +func addCodexProxy(w http.ResponseWriter, r *http.Request) { + var req AddProxyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + Error(w, http.StatusBadRequest, "请求参数错误") + return + } + + // 批量添加 + if len(req.Proxies) > 0 { + // 过滤空行和格式化 + var validProxies []string + for _, p := range req.Proxies { + p = strings.TrimSpace(p) + if p != "" { + // 如果没有协议前缀,自动添加 http:// + if !strings.HasPrefix(p, "http://") && !strings.HasPrefix(p, "https://") && !strings.HasPrefix(p, "socks5://") { + p = "http://" + p + } + validProxies = append(validProxies, p) + } + } + + if len(validProxies) == 0 { + Error(w, http.StatusBadRequest, "没有有效的代理地址") + return + } + + count, err := database.Instance.AddCodexProxies(validProxies) + if err != nil { + Error(w, http.StatusInternalServerError, "批量添加代理失败: "+err.Error()) + return + } + + Success(w, map[string]interface{}{ + "added": count, + "total": len(validProxies), + "message": "批量添加成功", + }) + return + } + + // 单个添加 + if req.ProxyURL == "" { + Error(w, http.StatusBadRequest, "代理地址不能为空") + return + } + + proxyURL := strings.TrimSpace(req.ProxyURL) + if !strings.HasPrefix(proxyURL, "http://") && !strings.HasPrefix(proxyURL, "https://") && !strings.HasPrefix(proxyURL, "socks5://") { + proxyURL = "http://" + proxyURL + } + + id, err := database.Instance.AddCodexProxy(proxyURL, req.Description) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + Error(w, http.StatusConflict, "代理地址已存在") + return + } + Error(w, http.StatusInternalServerError, "添加代理失败: "+err.Error()) + return + } + + Success(w, map[string]interface{}{ + "id": id, + "message": "添加成功", + }) +} + +// deleteCodexProxy 删除代理 +func deleteCodexProxy(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Query().Get("id") + if idStr == "" { + // 如果没有 id 参数,检查是否要清空所有 + if r.URL.Query().Get("all") == "true" { + err := database.Instance.ClearCodexProxies() + if err != nil { + Error(w, http.StatusInternalServerError, "清空代理失败: "+err.Error()) + return + } + Success(w, map[string]string{"message": "已清空所有代理"}) + return + } + Error(w, http.StatusBadRequest, "缺少代理 ID") + return + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + Error(w, http.StatusBadRequest, "无效的代理 ID") + return + } + + if err := database.Instance.DeleteCodexProxy(id); err != nil { + Error(w, http.StatusInternalServerError, "删除代理失败: "+err.Error()) + return + } + + Success(w, map[string]string{"message": "删除成功"}) +} + +// toggleCodexProxy 切换代理启用状态 +func toggleCodexProxy(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Query().Get("id") + if idStr == "" { + Error(w, http.StatusBadRequest, "缺少代理 ID") + return + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + Error(w, http.StatusBadRequest, "无效的代理 ID") + return + } + + if err := database.Instance.ToggleCodexProxy(id); err != nil { + Error(w, http.StatusInternalServerError, "切换代理状态失败: "+err.Error()) + return + } + + Success(w, map[string]string{"message": "状态已切换"}) +} diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 4d3e51f..0c869ec 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -66,6 +66,25 @@ type TeamProcessState struct { var teamProcessState = &TeamProcessState{} +// getProxyDisplay 获取代理显示名称(隐藏密码) +func getProxyDisplay(proxy string) string { + if proxy == "" { + return "无代理" + } + // 尝试解析 URL,只返回 host 部分 + if strings.Contains(proxy, "@") { + parts := strings.Split(proxy, "@") + if len(parts) >= 2 { + return parts[len(parts)-1] // 返回 @ 后面的 host:port 部分 + } + } + // 去掉协议前缀 + proxy = strings.TrimPrefix(proxy, "http://") + proxy = strings.TrimPrefix(proxy, "https://") + proxy = strings.TrimPrefix(proxy, "socks5://") + return proxy +} + // HandleTeamProcess POST /api/team/process - 启动 Team 批量处理 func HandleTeamProcess(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -593,7 +612,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } // 注册 - _, err := registerWithTimeout(currentEmail, currentPassword, name, birthdate, req.Proxy) + _, err := register.APIRegister(currentEmail, currentPassword, name, birthdate, req.Proxy, memberLogPrefix) if err != nil { logger.Error(fmt.Sprintf("%s [注册失败] %v", memberLogPrefix, err), currentEmail, "team") continue @@ -753,15 +772,23 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul continue } - // 根据配置选择浏览器自动化 + // 根据配置选择授权方式 var code string - // 根据全局配置决定授权方式 if config.Global.AuthMethod == "api" { // 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL - code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger) - } else if req.BrowserType == "rod" { - code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) + // 从代理池随机选择代理 + proxyToUse := req.Proxy + if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" { + proxyToUse = poolProxy + logger.Info(fmt.Sprintf("%s 使用代理池: %s", memberLogPrefix, getProxyDisplay(poolProxy)), memberChild.Email, "team") + } + code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger) + // 更新代理统计 + if proxyToUse != req.Proxy && proxyToUse != "" { + database.Instance.UpdateCodexProxyStats(proxyToUse, err == nil) + } } else { + // 使用 Chromedp 浏览器自动化 code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) } if err != nil { @@ -858,10 +885,19 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul // 根据全局配置决定授权方式 if config.Global.AuthMethod == "api" { // 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL - code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger) - } else if req.BrowserType == "rod" { - code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) + // 从代理池随机选择代理 + proxyToUse := req.Proxy + if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" { + proxyToUse = poolProxy + logger.Info(fmt.Sprintf("%s 使用代理池: %s", ownerLogPrefix, getProxyDisplay(poolProxy)), owner.Email, "team") + } + code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger) + // 更新代理统计 + if proxyToUse != req.Proxy && proxyToUse != "" { + database.Instance.UpdateCodexProxyStats(proxyToUse, err == nil) + } } else { + // 使用 Chromedp 浏览器自动化 code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) } if err != nil { @@ -909,81 +945,3 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul return result } - -// registerWithTimeout 带超时的注册(遇到 403 会换指纹重试) -func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) { - const maxInitRetries = 3 - var reg *register.ChatGPTReg - var initErr error - - // 初始化阶段:遇到 403 换指纹重试 - for attempt := 0; attempt < maxInitRetries; attempt++ { - var err error - reg, err = register.New(proxy) - if err != nil { - return nil, err - } - - if err := reg.InitSession(); err != nil { - initErr = err - // 检查是否是 403 错误,换指纹重试 - if strings.Contains(err.Error(), "403") { - continue - } - return nil, fmt.Errorf("初始化失败: %v", err) - } - - if err := reg.GetAuthorizeURL(email); err != nil { - // 403 也可能在这里出现 - if strings.Contains(err.Error(), "403") { - initErr = err - continue - } - return nil, fmt.Errorf("获取授权URL失败: %v", err) - } - - if err := reg.StartAuthorize(); err != nil { - if strings.Contains(err.Error(), "403") { - initErr = err - continue - } - return nil, fmt.Errorf("启动授权失败: %v", err) - } - - // 初始化成功,跳出重试循环 - initErr = nil - break - } - - if initErr != nil { - return nil, fmt.Errorf("初始化失败(重试%d次): %v", maxInitRetries, initErr) - } - - if err := reg.Register(email, password); err != nil { - return nil, fmt.Errorf("注册失败: %v", err) - } - - if err := reg.SendVerificationEmail(); err != nil { - return nil, fmt.Errorf("发送邮件失败: %v", err) - } - - // 短超时获取验证码 - otpCode, err := mail.GetVerificationCode(email, 5*time.Second) - if err != nil { - otpCode, err = mail.GetVerificationCode(email, 15*time.Second) - if err != nil { - return nil, fmt.Errorf("验证码获取超时") - } - } - - if err := reg.ValidateOTP(otpCode); err != nil { - return nil, fmt.Errorf("OTP验证失败: %v", err) - } - - if err := reg.CreateAccount(name, birthdate); err != nil { - return nil, fmt.Errorf("创建账户失败: %v", err) - } - - _ = reg.GetSessionToken() - return reg, nil -} diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index 12f81e0..d735e25 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -75,8 +75,18 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) return nil, fmt.Errorf("proxy dial error: %w", err) } - // 发送 CONNECT 请求 - connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetHost, targetHost) + // 构建 CONNECT 请求,支持代理认证 + connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", targetHost, targetHost) + + // 添加代理认证头 + if rt.proxyURL.User != nil { + username := rt.proxyURL.User.Username() + password, _ := rt.proxyURL.User.Password() + auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", auth) + } + connectReq += "\r\n" + _, err = conn.Write([]byte(connectReq)) if err != nil { conn.Close() @@ -493,10 +503,10 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) } c.logStep(StepInputPassword, "密码验证成功") - + // 解析密码验证响应 json.Unmarshal(body, &data) - + // 检查下一步是什么 nextPageType := "" if page, ok := data["page"].(map[string]interface{}); ok { @@ -504,7 +514,7 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { nextPageType = pt } } - + // 如果需要邮箱验证,这是新账号的问题 if nextPageType == "email_otp_verification" { c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程") diff --git a/backend/internal/auth/rod.go b/backend/internal/auth/rod.go deleted file mode 100644 index 8a5568c..0000000 --- a/backend/internal/auth/rod.go +++ /dev/null @@ -1,377 +0,0 @@ -package auth - -import ( - "fmt" - "os" - "strings" - "time" - - "codex-pool/internal/proxyutil" - - "github.com/go-rod/rod" - "github.com/go-rod/rod/lib/launcher" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/stealth" -) - -// RodAuth 使用 Rod + Stealth 完成 OAuth 授权 -type RodAuth struct { - browser *rod.Browser - headless bool - proxy string - proxyUser string - proxyPass string - profile BrowserProfile // 随机浏览器配置 -} - -// getChromiumPath 获取 Chromium 路径 -func getChromiumPath() string { - // 优先使用环境变量 - if path := os.Getenv("CHROME_BIN"); path != "" { - if _, err := os.Stat(path); err == nil { - return path - } - } - if path := os.Getenv("CHROME_PATH"); path != "" { - if _, err := os.Stat(path); err == nil { - return path - } - } - // Alpine Linux 默认路径 - paths := []string{ - "/usr/bin/chromium-browser", - "/usr/bin/chromium", - "/usr/bin/google-chrome", - "/usr/bin/google-chrome-stable", - } - for _, path := range paths { - if _, err := os.Stat(path); err == nil { - return path - } - } - return "" // 让 Rod 自动下载 -} - -// NewRodAuth 创建 Rod 授权器 -func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { - // 获取随机浏览器配置 - profile := GetRandomBrowserProfile() - - var proxyServer string - var proxyUser string - var proxyPass string - if proxy != "" { - info, err := proxyutil.Parse(proxy) - if err != nil { - return nil, fmt.Errorf("代理格式错误: %v", err) - } - if info.URL != nil { - proxy = info.URL.String() // normalized - } - if info.Server != nil { - proxyServer = info.Server.String() - } - proxyUser = info.Username - proxyPass = info.Password - } - - l := launcher.New(). - Headless(headless). - Set("disable-blink-features", "AutomationControlled"). - Set("disable-dev-shm-usage"). - Set("no-sandbox"). - Set("disable-gpu"). - Set("disable-extensions"). - Set("disable-background-networking"). - Set("disable-sync"). - Set("disable-translate"). - Set("metrics-recording-only"). - Set("no-first-run"). - Set("disable-infobars"). - Set("disable-automation"). - // 使用随机语言和窗口大小 - Set("lang", strings.Split(profile.AcceptLang, ",")[0]). - Set("window-size", fmt.Sprintf("%d,%d", profile.Width, profile.Height)). - // 随机 User-Agent - UserDataDir(""). - Set("user-agent", profile.UserAgent) - - // 使用系统 Chromium(如果存在) - if chromiumPath := getChromiumPath(); chromiumPath != "" { - l = l.Bin(chromiumPath) - } - - if proxyServer != "" { - l = l.Proxy(proxyServer) - } - - controlURL, err := l.Launch() - if err != nil { - return nil, fmt.Errorf("启动浏览器失败: %v", err) - } - - browser := rod.New().ControlURL(controlURL) - if err := browser.Connect(); err != nil { - return nil, fmt.Errorf("连接浏览器失败: %v", err) - } - - return &RodAuth{ - browser: browser, - headless: headless, - proxy: proxy, - proxyUser: proxyUser, - proxyPass: proxyPass, - profile: profile, - }, nil -} - -// Close 关闭浏览器 -func (r *RodAuth) Close() { - if r.browser != nil { - r.browser.Close() - } -} - -// CompleteOAuth 完成 OAuth 授权 -func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) { - return r.CompleteOAuthLogged(authURL, email, password, teamID, nil) -} - -// CompleteOAuthLogged 完成 OAuth 授权(带日志回调) -func (r *RodAuth) CompleteOAuthLogged(authURL, email, password, teamID string, logger *AuthLogger) (string, error) { - // 日志辅助函数 - logStep := func(step AuthStep, format string, args ...interface{}) { - if logger != nil { - logger.LogStep(step, format, args...) - } - } - logError := func(step AuthStep, format string, args ...interface{}) { - if logger != nil { - logger.LogError(step, format, args...) - } - } - - // Handle proxy auth (407) in headless mode. - // When Fetch domain is enabled without patterns, requests will be paused and must be continued. - // 只在代理需要认证时才启用 Fetch 域 - if r.proxy != "" && r.proxyUser != "" { - authBrowser, cancel := r.browser.WithCancel() - defer cancel() - - restoreFetch := authBrowser.EnableDomain("", &proto.FetchEnable{HandleAuthRequests: true}) - defer restoreFetch() - - wait := authBrowser.EachEvent( - func(e *proto.FetchRequestPaused) { - _ = proto.FetchContinueRequest{RequestID: e.RequestID}.Call(authBrowser) - }, - func(e *proto.FetchAuthRequired) { - resp := &proto.FetchAuthChallengeResponse{ - Response: proto.FetchAuthChallengeResponseResponseDefault, - } - if e.AuthChallenge != nil && e.AuthChallenge.Source == proto.FetchAuthChallengeSourceProxy { - if r.proxyUser != "" { - resp.Response = proto.FetchAuthChallengeResponseResponseProvideCredentials - resp.Username = r.proxyUser - resp.Password = r.proxyPass - } else { - // Fail fast if the proxy requires auth but user didn't provide credentials. - resp.Response = proto.FetchAuthChallengeResponseResponseCancelAuth - } - } - _ = proto.FetchContinueWithAuth{RequestID: e.RequestID, AuthChallengeResponse: resp}.Call(authBrowser) - }, - ) - go wait() - } - - page, err := stealth.Page(r.browser) - if err != nil { - return "", fmt.Errorf("创建页面失败: %v", err) - } - defer page.Close() - - // 设置随机窗口大小 - _ = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ - Width: r.profile.Width, - Height: r.profile.Height, - DeviceScaleFactor: r.profile.PixelRatio, - Mobile: false, - }) - - // 注入额外的反检测脚本 - antiDetectionJS := GetAntiDetectionJS(r.profile) - _, _ = page.Evaluate(&rod.EvalOptions{ - JS: antiDetectionJS, - ByValue: true, - AwaitPromise: false, - ThisObj: nil, - }) - - // 设置合理的超时时间 60 秒 - page = page.Timeout(60 * time.Second) - - if err := page.Navigate(authURL); err != nil { - logError(StepNavigate, "访问授权页失败: %v", err) - return "", fmt.Errorf("访问授权URL失败: %v", err) - } - - page.MustWaitDOMStable() - - // 获取当前URL - info, _ := page.Info() - currentURL := info.URL - logStep(StepNavigate, "页面加载完成 | URL: %s", currentURL) - - if code := r.checkForCode(page); code != "" { - logStep(StepComplete, "授权成功(快速通道)") - return code, nil - } - - // 使用10秒超时查找邮箱输入框 - emailInput, err := page.Timeout(10 * time.Second).Element("input[name='email'], input[type='email'], input[name='username'], input[id='email'], input[autocomplete='email']") - if err != nil { - info, _ := page.Info() - logError(StepInputEmail, "未找到邮箱输入框 | URL: %s", info.URL) - return "", fmt.Errorf("未找到邮箱输入框") - } - - logStep(StepInputEmail, "邮箱已填写") - emailInput.MustSelectAllText().MustInput(email) - time.Sleep(200 * time.Millisecond) - - // 点击提交按钮 - if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], div._ctas_1alro_13 button, button[name='action']"); btn != nil { - btn.MustClick() - } - - // 等待页面跳转(等待 URL 变化或密码框出现,最多 10 秒) - passwordSelector := "input[name='current-password'], input[autocomplete='current-password'], input[type='password'], input[name='password'], input[id='password']" - var passwordFound bool - for i := 0; i < 20; i++ { - time.Sleep(500 * time.Millisecond) - - if code := r.checkForCode(page); code != "" { - logStep(StepComplete, "授权成功") - return code, nil - } - - info, _ = page.Info() - // 检查是否有密码输入框(页面已跳转) - if pwdInput, _ := page.Timeout(100 * time.Millisecond).Element(passwordSelector); pwdInput != nil { - passwordFound = true - break - } - - // 如果 URL 已经变化(包含 password),也跳出 - if strings.Contains(info.URL, "password") { - break - } - } - - // 获取当前URL用于调试 - info, _ = page.Info() - logStep(StepInputPassword, "查找密码框 | URL: %s", info.URL) - - // 使用10秒超时查找密码输入框(优先使用 current-password) - var passwordInput *rod.Element - if passwordFound { - passwordInput, err = page.Timeout(2 * time.Second).Element(passwordSelector) - } else { - passwordInput, err = page.Timeout(10 * time.Second).Element(passwordSelector) - } - if err != nil { - info, _ := page.Info() - logError(StepInputPassword, "未找到密码输入框 | URL: %s", info.URL) - return "", fmt.Errorf("未找到密码输入框") - } - - logStep(StepInputPassword, "密码已填写") - passwordInput.MustSelectAllText().MustInput(password) - time.Sleep(200 * time.Millisecond) - - logStep(StepSubmitPassword, "正在登录...") - if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], div._ctas_1alro_13 button, button[name='action']"); btn != nil { - btn.MustClick() - } - - // 等待授权回调(最多20秒) - for i := 0; i < 40; i++ { - time.Sleep(500 * time.Millisecond) - - if code := r.checkForCode(page); code != "" { - logStep(StepComplete, "授权成功") - return code, nil - } - - info, _ := page.Info() - currentURL := info.URL - - if strings.Contains(currentURL, "consent") { - logStep(StepConsent, "处理授权同意... | URL: %s", currentURL) - // 同意页面的确认按钮(第二个按钮) - if btn, _ := page.Timeout(500 * time.Millisecond).Element("div._ctas_1alro_13 div:nth-child(2) button, button[type='submit'], div._ctas_1alro_13 button"); btn != nil { - btn.Click(proto.InputMouseButtonLeft, 1) - } - } - - if strings.Contains(currentURL, "authorize") && teamID != "" { - logStep(StepSelectWorkspace, "选择工作区...") - wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID) - if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil { - wsBtn.Click(proto.InputMouseButtonLeft, 1) - } - } - } - - logError(StepWaitCallback, "授权超时") - return "", fmt.Errorf("授权超时") -} - -// checkForCode 检查 URL 中是否包含 code -func (r *RodAuth) checkForCode(page *rod.Page) string { - info, err := page.Info() - if err != nil { - return "" - } - if strings.Contains(info.URL, "code=") { - return ExtractCodeFromCallbackURL(info.URL) - } - return "" -} - -// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权 -func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { - return CompleteWithRodLogged(authURL, email, password, teamID, headless, proxy, nil) -} - -// CompleteWithRodLogged 使用 Rod + Stealth 完成 S2A 授权(带日志回调) -func CompleteWithRodLogged(authURL, email, password, teamID string, headless bool, proxy string, logger *AuthLogger) (string, error) { - // 日志辅助函数 - logStep := func(step AuthStep, format string, args ...interface{}) { - if logger != nil { - logger.LogStep(step, format, args...) - } - } - logError := func(step AuthStep, format string, args ...interface{}) { - if logger != nil { - logger.LogError(step, format, args...) - } - } - - logStep(StepBrowserStart, "正在启动 Rod 浏览器...") - auth, err := NewRodAuth(headless, proxy) - if err != nil { - logError(StepBrowserStart, "启动失败: %v", err) - return "", err - } - defer auth.Close() - - logStep(StepBrowserStart, "浏览器启动成功") - return auth.CompleteOAuthLogged(authURL, email, password, teamID, logger) -} - -// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名) -func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { - return CompleteWithRod(authURL, email, password, teamID, headless, proxy) -} diff --git a/backend/internal/client/fingerprints.go b/backend/internal/client/fingerprints.go index bd41164..27be8a5 100644 --- a/backend/internal/client/fingerprints.go +++ b/backend/internal/client/fingerprints.go @@ -7,101 +7,150 @@ import ( ) // ============================================================ -// 浏览器指纹配置文件 -// 整合 tls-client 高成功率指纹 +// 指纹配置文件 - Super Max Plus Ultra 版 +// 整合 tls-client, spoofed-round-tripper, azuretls, surf 高成功率指纹 // ============================================================ +// FingerprintType 指纹类型 +type FingerprintType int + +const ( + FingerprintTLSClient FingerprintType = iota // bogdanfinn/tls-client + FingerprintSpoofedRT // juzeon/spoofed-round-tripper + FingerprintAzureTLS // Noooste/azuretls-client +) + // BrowserFingerprint 浏览器指纹完整配置 type BrowserFingerprint struct { - Browser string - Version string - Platform string - Mobile bool - TLSProfile profiles.ClientProfile + Browser string + Version string + Platform string + Mobile bool + Type FingerprintType + TLSProfile profiles.ClientProfile // tls-client / spoofed-round-tripper + AzureBrowser string // azuretls-client browser type } // ============================================================ -// Firefox 指纹池 (100% 成功率) +// Firefox 指纹池 (100% 成功率) - tls-client // ============================================================ var firefoxFingerprints = []BrowserFingerprint{ // 最新版本 - {Browser: "firefox", Version: "135", Platform: "Windows", TLSProfile: profiles.Firefox_135}, - {Browser: "firefox", Version: "135", Platform: "macOS", TLSProfile: profiles.Firefox_135}, - {Browser: "firefox", Version: "135", Platform: "Linux", TLSProfile: profiles.Firefox_135}, - {Browser: "firefox", Version: "133", Platform: "Windows", TLSProfile: profiles.Firefox_133}, - {Browser: "firefox", Version: "133", Platform: "macOS", TLSProfile: profiles.Firefox_133}, - {Browser: "firefox", Version: "133", Platform: "Linux", TLSProfile: profiles.Firefox_133}, - {Browser: "firefox", Version: "132", Platform: "Windows", TLSProfile: profiles.Firefox_132}, - {Browser: "firefox", Version: "132", Platform: "macOS", TLSProfile: profiles.Firefox_132}, - {Browser: "firefox", Version: "123", Platform: "Windows", TLSProfile: profiles.Firefox_123}, - {Browser: "firefox", Version: "120", Platform: "Windows", TLSProfile: profiles.Firefox_120}, - {Browser: "firefox", Version: "120", Platform: "macOS", TLSProfile: profiles.Firefox_120}, - {Browser: "firefox", Version: "117", Platform: "Windows", TLSProfile: profiles.Firefox_117}, - {Browser: "firefox", Version: "110", Platform: "Windows", TLSProfile: profiles.Firefox_110}, - {Browser: "firefox", Version: "108", Platform: "Windows", TLSProfile: profiles.Firefox_108}, - {Browser: "firefox", Version: "106", Platform: "Windows", TLSProfile: profiles.Firefox_106}, - {Browser: "firefox", Version: "105", Platform: "Windows", TLSProfile: profiles.Firefox_105}, - {Browser: "firefox", Version: "104", Platform: "Windows", TLSProfile: profiles.Firefox_104}, - {Browser: "firefox", Version: "102", Platform: "Windows", TLSProfile: profiles.Firefox_102}, + {Browser: "firefox", Version: "135", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_135}, + {Browser: "firefox", Version: "135", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_135}, + {Browser: "firefox", Version: "135", Platform: "Linux", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_135}, + {Browser: "firefox", Version: "133", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_133}, + {Browser: "firefox", Version: "133", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_133}, + {Browser: "firefox", Version: "133", Platform: "Linux", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_133}, + {Browser: "firefox", Version: "132", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_132}, + {Browser: "firefox", Version: "132", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_132}, + {Browser: "firefox", Version: "123", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_123}, + {Browser: "firefox", Version: "120", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_120}, + {Browser: "firefox", Version: "120", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_120}, + {Browser: "firefox", Version: "117", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_117}, + {Browser: "firefox", Version: "110", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_110}, + {Browser: "firefox", Version: "108", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_108}, + {Browser: "firefox", Version: "106", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_106}, + {Browser: "firefox", Version: "105", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_105}, + {Browser: "firefox", Version: "104", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_104}, + {Browser: "firefox", Version: "102", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Firefox_102}, } // ============================================================ -// Safari 指纹池 (100% 成功率) +// Safari 指纹池 (100% 成功率) - tls-client // ============================================================ var safariFingerprints = []BrowserFingerprint{ // macOS Safari - {Browser: "safari", Version: "16.0", Platform: "macOS", TLSProfile: profiles.Safari_16_0}, - {Browser: "safari", Version: "15.6.1", Platform: "macOS", TLSProfile: profiles.Safari_15_6_1}, + {Browser: "safari", Version: "16.0", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Safari_16_0}, + {Browser: "safari", Version: "15.6.1", Platform: "macOS", Type: FingerprintTLSClient, TLSProfile: profiles.Safari_15_6_1}, // iOS Safari - {Browser: "safari", Version: "18.5", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_18_5}, - {Browser: "safari", Version: "18.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_18_0}, - {Browser: "safari", Version: "17.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_17_0}, - {Browser: "safari", Version: "16.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_16_0}, - {Browser: "safari", Version: "15.6", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_15_6}, - {Browser: "safari", Version: "15.5", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_15_5}, + {Browser: "safari", Version: "18.5", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_18_5}, + {Browser: "safari", Version: "18.0", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_18_0}, + {Browser: "safari", Version: "17.0", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_17_0}, + {Browser: "safari", Version: "16.0", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_16_0}, + {Browser: "safari", Version: "15.6", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_15_6}, + {Browser: "safari", Version: "15.5", Platform: "iOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_IOS_15_5}, // iPadOS Safari - {Browser: "safari", Version: "15.6", Platform: "iPadOS", Mobile: true, TLSProfile: profiles.Safari_Ipad_15_6}, + {Browser: "safari", Version: "15.6", Platform: "iPadOS", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Safari_Ipad_15_6}, } // ============================================================ -// Opera 指纹池 (高成功率) +// Opera 指纹池 (高成功率) - tls-client // ============================================================ var operaFingerprints = []BrowserFingerprint{ - {Browser: "opera", Version: "91", Platform: "Windows", TLSProfile: profiles.Opera_91}, - {Browser: "opera", Version: "90", Platform: "Windows", TLSProfile: profiles.Opera_90}, - {Browser: "opera", Version: "89", Platform: "Windows", TLSProfile: profiles.Opera_89}, + {Browser: "opera", Version: "91", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Opera_91}, + {Browser: "opera", Version: "90", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Opera_90}, + {Browser: "opera", Version: "89", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Opera_89}, } // ============================================================ -// OkHttp 指纹池 (100% 成功率 - Android 原生) +// OkHttp 指纹池 (100% 成功率 - Android 原生) - tls-client // ============================================================ var okhttpFingerprints = []BrowserFingerprint{ - {Browser: "okhttp", Version: "13", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android13}, - {Browser: "okhttp", Version: "12", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android12}, - {Browser: "okhttp", Version: "11", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android11}, - {Browser: "okhttp", Version: "10", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android10}, - {Browser: "okhttp", Version: "9", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android9}, - {Browser: "okhttp", Version: "8", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android8}, - {Browser: "okhttp", Version: "7", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android7}, + {Browser: "okhttp", Version: "13", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android13}, + {Browser: "okhttp", Version: "12", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android12}, + {Browser: "okhttp", Version: "11", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android11}, + {Browser: "okhttp", Version: "10", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android10}, + {Browser: "okhttp", Version: "9", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android9}, + {Browser: "okhttp", Version: "8", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android8}, + {Browser: "okhttp", Version: "7", Platform: "Android", Mobile: true, Type: FingerprintTLSClient, TLSProfile: profiles.Okhttp4Android7}, } // ============================================================ -// Chrome 指纹池 (测试通过的版本) +// Chrome 指纹池 (测试通过的版本) - tls-client // 注意: 新版 Chrome (120+) 在 tls-client 中被检测,只保留旧版本 // ============================================================ var chromeFingerprints = []BrowserFingerprint{ // 只保留测试通过的旧版本 - {Browser: "chrome", Version: "112", Platform: "Windows", TLSProfile: profiles.Chrome_112}, - {Browser: "chrome", Version: "111", Platform: "Windows", TLSProfile: profiles.Chrome_111}, + {Browser: "chrome", Version: "112", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Chrome_112}, + {Browser: "chrome", Version: "111", Platform: "Windows", Type: FingerprintTLSClient, TLSProfile: profiles.Chrome_111}, } // ============================================================ -// 合并所有指纹 +// juzeon/spoofed-round-tripper 指纹池 +// ============================================================ + +var spoofedRTFingerprints = []BrowserFingerprint{ + {Browser: "firefox", Version: "135", Platform: "Windows", Type: FingerprintSpoofedRT, TLSProfile: profiles.Firefox_135}, + {Browser: "safari", Version: "16.0", Platform: "macOS", Type: FingerprintSpoofedRT, TLSProfile: profiles.Safari_16_0}, +} + +// ============================================================ +// Noooste/azuretls-client 指纹池 (HTTP/2 + HTTP/3 支持) +// 支持最新浏览器版本,自动更新指纹 +// ============================================================ + +var azureTLSFingerprints = []BrowserFingerprint{ + // Chrome - 最新版本 (自动获取) + {Browser: "chrome", Version: "latest", Platform: "Windows", Type: FingerprintAzureTLS, AzureBrowser: "chrome"}, + {Browser: "chrome", Version: "latest", Platform: "macOS", Type: FingerprintAzureTLS, AzureBrowser: "chrome"}, + {Browser: "chrome", Version: "latest", Platform: "Linux", Type: FingerprintAzureTLS, AzureBrowser: "chrome"}, + + // Firefox - 最新版本 + {Browser: "firefox", Version: "latest", Platform: "Windows", Type: FingerprintAzureTLS, AzureBrowser: "firefox"}, + {Browser: "firefox", Version: "latest", Platform: "macOS", Type: FingerprintAzureTLS, AzureBrowser: "firefox"}, + {Browser: "firefox", Version: "latest", Platform: "Linux", Type: FingerprintAzureTLS, AzureBrowser: "firefox"}, + + // Safari - 最新版本 + {Browser: "safari", Version: "latest", Platform: "macOS", Type: FingerprintAzureTLS, AzureBrowser: "safari"}, + + // Edge - 最新版本 + {Browser: "edge", Version: "latest", Platform: "Windows", Type: FingerprintAzureTLS, AzureBrowser: "edge"}, + + // iOS Safari + {Browser: "safari", Version: "latest", Platform: "iOS", Mobile: true, Type: FingerprintAzureTLS, AzureBrowser: "ios"}, + + // Opera + {Browser: "opera", Version: "latest", Platform: "Windows", Type: FingerprintAzureTLS, AzureBrowser: "opera"}, +} + +// ============================================================ +// 合并所有指纹 - 均衡权重分配 // ============================================================ var allFingerprints []BrowserFingerprint @@ -109,21 +158,34 @@ var allFingerprints []BrowserFingerprint func init() { allFingerprints = make([]BrowserFingerprint, 0, 100) - // Firefox (高优先级,多份) - allFingerprints = append(allFingerprints, firefoxFingerprints...) + // ============================================================ + // 权重策略: tls-client ~50%, azuretls ~30%, spoofed-rt ~20% + // ============================================================ + + // tls-client Firefox allFingerprints = append(allFingerprints, firefoxFingerprints...) - // Safari (高成功率) + // tls-client Safari allFingerprints = append(allFingerprints, safariFingerprints...) - // Opera + // tls-client Opera allFingerprints = append(allFingerprints, operaFingerprints...) - // OkHttp (Android) + // tls-client OkHttp (Android) allFingerprints = append(allFingerprints, okhttpFingerprints...) - // Chrome (低优先级) + // tls-client Chrome allFingerprints = append(allFingerprints, chromeFingerprints...) + + // spoofed-round-tripper (权重加倍) + allFingerprints = append(allFingerprints, spoofedRTFingerprints...) + allFingerprints = append(allFingerprints, spoofedRTFingerprints...) + allFingerprints = append(allFingerprints, spoofedRTFingerprints...) + + // azuretls-client (权重加倍,最新浏览器指纹) + allFingerprints = append(allFingerprints, azureTLSFingerprints...) + allFingerprints = append(allFingerprints, azureTLSFingerprints...) + allFingerprints = append(allFingerprints, azureTLSFingerprints...) } // GetRandomFingerprint 获取随机指纹 diff --git a/backend/internal/client/tls.go b/backend/internal/client/tls.go index e92fb00..09f9fcc 100644 --- a/backend/internal/client/tls.go +++ b/backend/internal/client/tls.go @@ -8,21 +8,36 @@ import ( "math/rand" "net/http" "net/url" - "strings" + "time" "codex-pool/internal/proxyutil" + "github.com/Noooste/azuretls-client" "github.com/andybalholm/brotli" http2 "github.com/bogdanfinn/fhttp" tls_client "github.com/bogdanfinn/tls-client" ) -// TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端 +// ClientType 客户端类型 +type ClientType int + +const ( + ClientTypeTLS ClientType = iota // bogdanfinn/tls-client + ClientTypeAzureTLS // Noooste/azuretls-client +) + +// TLSClient 统一的 HTTP 客户端接口,支持多种 TLS 指纹和浏览器模拟 type TLSClient struct { - client tls_client.HttpClient + // 内部客户端 + tlsClient tls_client.HttpClient + azureSession *azuretls.Session + clientType ClientType + + // 指纹信息 fingerprint BrowserFingerprint userAgent string acceptLang string + proxy string } // 语言偏好池 @@ -37,7 +52,7 @@ var languagePrefs = []string{ "es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7", } -// New 创建一个新的 TLS 客户端(使用随机指纹) +// New 创建一个新的 TLS 客户端 (使用随机指纹) func New(proxyStr string) (*TLSClient, error) { // 获取随机桌面端指纹 fp := GetRandomDesktopFingerprint() @@ -45,7 +60,36 @@ func New(proxyStr string) (*TLSClient, error) { } // NewWithFingerprint 使用指定指纹创建客户端 -func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, error) { +func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (client *TLSClient, err error) { + // 添加 panic 恢复 + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("创建客户端时发生 panic: %v", r) + client = nil + } + }() + + acceptLang := languagePrefs[rand.Intn(len(languagePrefs))] + userAgent := generateUserAgent(fp) + + client = &TLSClient{ + fingerprint: fp, + userAgent: userAgent, + acceptLang: acceptLang, + proxy: proxyStr, + } + + // 根据指纹类型创建对应的客户端 + switch fp.Type { + case FingerprintAzureTLS: + return createAzureTLSClient(client, fp, proxyStr) + default: + return createTLSClient(client, fp, proxyStr) + } +} + +// createTLSClient 创建 tls-client 客户端 +func createTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string) (*TLSClient, error) { jar := tls_client.NewCookieJar() options := []tls_client.HttpClientOption{ @@ -54,6 +98,7 @@ func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, err tls_client.WithRandomTLSExtensionOrder(), tls_client.WithCookieJar(jar), tls_client.WithInsecureSkipVerify(), + tls_client.WithNotFollowRedirects(), } if proxyStr != "" { @@ -64,20 +109,45 @@ func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, err options = append(options, tls_client.WithProxyUrl(normalized)) } - client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) + tlsClient, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) if err != nil { return nil, err } - acceptLang := languagePrefs[rand.Intn(len(languagePrefs))] - userAgent := generateUserAgent(fp) + c.tlsClient = tlsClient + c.clientType = ClientTypeTLS + return c, nil +} - return &TLSClient{ - client: client, - fingerprint: fp, - userAgent: userAgent, - acceptLang: acceptLang, - }, nil +// createAzureTLSClient 创建 azuretls-client 客户端 +func createAzureTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string) (*TLSClient, error) { + session := azuretls.NewSession() + + browser := fp.AzureBrowser + if browser == "" { + browser = azuretls.Chrome + } + + session.Browser = browser + session.GetClientHelloSpec = azuretls.GetBrowserClientHelloFunc(browser) + session.SetTimeout(90 * time.Second) + + if proxyStr != "" { + normalized, err := proxyutil.Normalize(proxyStr) + if err != nil { + return nil, err + } + if err := session.SetProxy(normalized); err != nil { + return nil, fmt.Errorf("azuretls set proxy failed: %w", err) + } + } + + session.MaxRedirects = 0 + session.InsecureSkipVerify = true + + c.azureSession = session + c.clientType = ClientTypeAzureTLS + return c, nil } // generateUserAgent 根据指纹生成 User-Agent @@ -87,6 +157,22 @@ func generateUserAgent(fp BrowserFingerprint) string { linuxVersions := []string{"X11; Linux x86_64", "X11; Ubuntu; Linux x86_64", "X11; Fedora; Linux x86_64"} version := fp.Version + if version == "latest" { + switch fp.Browser { + case "chrome": + version = "133" + case "firefox": + version = "135" + case "safari": + version = "18.3" + case "edge": + version = "133" + case "opera": + version = "117" + default: + version = "133" + } + } switch fp.Browser { case "chrome": @@ -144,11 +230,10 @@ func generateUserAgent(fp BrowserFingerprint) string { return "okhttp/4.12.0" } - // 默认 Chrome return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", version) } -// getDefaultHeaders 获取默认请求头(根据浏览器类型返回不同的头) +// getDefaultHeaders 获取默认请求头 func (c *TLSClient) getDefaultHeaders() map[string]string { headers := map[string]string{ "User-Agent": c.userAgent, @@ -222,14 +307,18 @@ func (c *TLSClient) getPlatformHeader() string { } } -// GetFingerprintInfo 获取指纹信息字符串(用于日志输出) -func (c *TLSClient) GetFingerprintInfo() string { - fp := c.fingerprint - return fmt.Sprintf("%s/%s (%s)", fp.Browser, fp.Version, fp.Platform) -} - // Do 执行 HTTP 请求 func (c *TLSClient) Do(req *http.Request) (*http.Response, error) { + switch c.clientType { + case ClientTypeAzureTLS: + return c.doAzureTLS(req) + default: + return c.doTLS(req) + } +} + +// doTLS 使用 tls-client 执行请求 +func (c *TLSClient) doTLS(req *http.Request) (*http.Response, error) { fhttpReq, err := http2.NewRequest(req.Method, req.URL.String(), req.Body) if err != nil { return nil, err @@ -247,7 +336,7 @@ func (c *TLSClient) Do(req *http.Request) (*http.Response, error) { } } - resp, err := c.client.Do(fhttpReq) + resp, err := c.tlsClient.Do(fhttpReq) if err != nil { return nil, err } @@ -261,7 +350,7 @@ func (c *TLSClient) Do(req *http.Request) (*http.Response, error) { } } - stdResp := &http.Response{ + return &http.Response{ Status: resp.Status, StatusCode: resp.StatusCode, Proto: resp.Proto, @@ -274,23 +363,104 @@ func (c *TLSClient) Do(req *http.Request) (*http.Response, error) { Close: resp.Close, Uncompressed: resp.Uncompressed, Request: finalReq, + }, nil +} + +// doAzureTLS 使用 azuretls 执行请求 +func (c *TLSClient) doAzureTLS(req *http.Request) (*http.Response, error) { + orderedHeaders := azuretls.OrderedHeaders{} + for key, value := range c.getDefaultHeaders() { + if req.Header.Get(key) == "" { + orderedHeaders = append(orderedHeaders, []string{key, value}) + } + } + for key, values := range req.Header { + if len(values) > 0 { + orderedHeaders = append(orderedHeaders, []string{key, values[0]}) + } } - return stdResp, nil + azureReq := &azuretls.Request{ + Method: req.Method, + Url: req.URL.String(), + OrderedHeaders: orderedHeaders, + DisableRedirects: true, + } + + if req.Body != nil { + bodyBytes, _ := io.ReadAll(req.Body) + azureReq.Body = bodyBytes + } + + resp, err := c.azureSession.Do(azureReq) + if err != nil { + return nil, err + } + + respHeaders := make(http.Header) + for key, values := range resp.Header { + respHeaders[key] = values + } + + return &http.Response{ + Status: fmt.Sprintf("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode)), + StatusCode: resp.StatusCode, + Header: respHeaders, + Body: io.NopCloser(bytes.NewReader(resp.Body)), + Request: req, + }, nil +} + +// DoWithRedirect 执行请求并手动处理重定向 +func (c *TLSClient) DoWithRedirect(req *http.Request, maxRedirects int) (*http.Response, error) { + for i := 0; i <= maxRedirects; i++ { + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 300 && resp.StatusCode < 400 { + location := resp.Header.Get("Location") + if location == "" { + return resp, nil + } + + redirectURL, err := url.Parse(location) + if err != nil { + return resp, nil + } + + if !redirectURL.IsAbs() { + redirectURL = req.URL.ResolveReference(redirectURL) + } + + req, err = http.NewRequest(http.MethodGet, redirectURL.String(), nil) + if err != nil { + return nil, err + } + + resp.Body.Close() + continue + } + + return resp, nil + } + + return nil, fmt.Errorf("too many redirects") } // Get 执行 GET 请求 func (c *TLSClient) Get(urlStr string) (*http.Response, error) { - req, err := http.NewRequest("GET", urlStr, nil) + req, err := http.NewRequest(http.MethodGet, urlStr, nil) if err != nil { return nil, err } - return c.Do(req) + return c.DoWithRedirect(req, 10) } // Post 执行 POST 请求 func (c *TLSClient) Post(urlStr string, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest("POST", urlStr, body) + req, err := http.NewRequest(http.MethodPost, urlStr, body) if err != nil { return nil, err } @@ -298,11 +468,6 @@ func (c *TLSClient) Post(urlStr string, contentType string, body io.Reader) (*ht return c.Do(req) } -// PostForm 执行 POST 表单请求 -func (c *TLSClient) PostForm(urlStr string, data url.Values) (*http.Response, error) { - return c.Post(urlStr, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) -} - // PostJSON 执行 POST JSON 请求 func (c *TLSClient) PostJSON(urlStr string, body io.Reader) (*http.Response, error) { return c.Post(urlStr, "application/json", body) @@ -310,11 +475,15 @@ func (c *TLSClient) PostJSON(urlStr string, body io.Reader) (*http.Response, err // GetCookie 获取指定 URL 的 Cookie func (c *TLSClient) GetCookie(urlStr string, name string) string { + if c.clientType == ClientTypeAzureTLS { + return "" // azuretls cookie 管理待完善,目前主要依赖 tls-client 或手动 handle + } + u, err := url.Parse(urlStr) if err != nil { return "" } - cookies := c.client.GetCookies(u) + cookies := c.tlsClient.GetCookies(u) for _, cookie := range cookies { if cookie.Name == name { return cookie.Value @@ -325,11 +494,15 @@ func (c *TLSClient) GetCookie(urlStr string, name string) string { // SetCookie 设置 Cookie func (c *TLSClient) SetCookie(urlStr string, cookie *http.Cookie) { + if c.clientType == ClientTypeAzureTLS { + return + } + u, err := url.Parse(urlStr) if err != nil { return } - c.client.SetCookies(u, []*http2.Cookie{ + c.tlsClient.SetCookies(u, []*http2.Cookie{ { Name: cookie.Name, Value: cookie.Value, @@ -371,3 +544,20 @@ func ReadBodyString(resp *http.Response) (string, error) { } return string(body), nil } + +// GetFingerprintInfo 获取指纹信息字符串(用于日志输出) +func (c *TLSClient) GetFingerprintInfo() string { + fp := c.fingerprint + typeName := "tls-client" + if c.clientType == ClientTypeAzureTLS { + typeName = "azuretls" + } + return fmt.Sprintf("%s/%s %s (%s)", typeName, fp.Browser, fp.Version, fp.Platform) +} + +// Close 关闭客户端 +func (c *TLSClient) Close() { + if c.azureSession != nil { + c.azureSession.Close() + } +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go index 6e3ad14..7c775d1 100644 --- a/backend/internal/database/sqlite.go +++ b/backend/internal/database/sqlite.go @@ -125,6 +125,20 @@ func (d *DB) createTables() error { ); CREATE INDEX IF NOT EXISTS idx_batch_team_results_batch_id ON batch_team_results(batch_id); + + -- CodexAuth 代理池表 + CREATE TABLE IF NOT EXISTS codex_auth_proxies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + proxy_url TEXT NOT NULL UNIQUE, + description TEXT, + is_enabled INTEGER DEFAULT 1, + last_used_at DATETIME, + success_count INTEGER DEFAULT 0, + fail_count INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_codex_auth_proxies_enabled ON codex_auth_proxies(is_enabled); `) return err } @@ -737,6 +751,189 @@ func (d *DB) GetBatchRunWithResults(batchID int64) (*BatchRun, []BatchTeamResult return &run, results, nil } +// CodexProxy CodexAuth 代理配置 +type CodexProxy struct { + ID int64 `json:"id"` + ProxyURL string `json:"proxy_url"` + Description string `json:"description"` + IsEnabled bool `json:"is_enabled"` + LastUsedAt *time.Time `json:"last_used_at,omitempty"` + SuccessCount int `json:"success_count"` + FailCount int `json:"fail_count"` + CreatedAt time.Time `json:"created_at"` +} + +// AddCodexProxy 添加代理 +func (d *DB) AddCodexProxy(proxyURL, description string) (int64, error) { + result, err := d.db.Exec(` + INSERT INTO codex_auth_proxies (proxy_url, description, is_enabled, created_at) + VALUES (?, ?, 1, CURRENT_TIMESTAMP) + `, proxyURL, description) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +// AddCodexProxies 批量添加代理 +func (d *DB) AddCodexProxies(proxies []string) (int, error) { + tx, err := d.db.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + stmt, err := tx.Prepare(` + INSERT OR IGNORE INTO codex_auth_proxies (proxy_url, is_enabled, created_at) + VALUES (?, 1, CURRENT_TIMESTAMP) + `) + if err != nil { + return 0, err + } + defer stmt.Close() + + count := 0 + for _, proxy := range proxies { + result, err := stmt.Exec(proxy) + if err != nil { + continue + } + affected, _ := result.RowsAffected() + if affected > 0 { + count++ + } + } + + if err := tx.Commit(); err != nil { + return 0, err + } + return count, nil +} + +// GetCodexProxies 获取代理列表 +func (d *DB) GetCodexProxies() ([]CodexProxy, error) { + rows, err := d.db.Query(` + SELECT id, proxy_url, COALESCE(description, ''), is_enabled, last_used_at, success_count, fail_count, created_at + FROM codex_auth_proxies + ORDER BY created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var proxies []CodexProxy + for rows.Next() { + var p CodexProxy + var lastUsedAt sql.NullTime + err := rows.Scan(&p.ID, &p.ProxyURL, &p.Description, &p.IsEnabled, &lastUsedAt, &p.SuccessCount, &p.FailCount, &p.CreatedAt) + if err != nil { + continue + } + if lastUsedAt.Valid { + p.LastUsedAt = &lastUsedAt.Time + } + proxies = append(proxies, p) + } + return proxies, nil +} + +// GetEnabledCodexProxies 获取已启用的代理列表 +func (d *DB) GetEnabledCodexProxies() ([]CodexProxy, error) { + rows, err := d.db.Query(` + SELECT id, proxy_url, COALESCE(description, ''), is_enabled, last_used_at, success_count, fail_count, created_at + FROM codex_auth_proxies + WHERE is_enabled = 1 + ORDER BY success_count DESC, fail_count ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var proxies []CodexProxy + for rows.Next() { + var p CodexProxy + var lastUsedAt sql.NullTime + err := rows.Scan(&p.ID, &p.ProxyURL, &p.Description, &p.IsEnabled, &lastUsedAt, &p.SuccessCount, &p.FailCount, &p.CreatedAt) + if err != nil { + continue + } + if lastUsedAt.Valid { + p.LastUsedAt = &lastUsedAt.Time + } + proxies = append(proxies, p) + } + return proxies, nil +} + +// GetRandomCodexProxy 随机获取一个已启用的代理 +func (d *DB) GetRandomCodexProxy() (string, error) { + var proxyURL string + err := d.db.QueryRow(` + SELECT proxy_url FROM codex_auth_proxies + WHERE is_enabled = 1 + ORDER BY RANDOM() + LIMIT 1 + `).Scan(&proxyURL) + if err == sql.ErrNoRows { + return "", nil + } + if err != nil { + return "", err + } + // 更新最后使用时间 + d.db.Exec("UPDATE codex_auth_proxies SET last_used_at = CURRENT_TIMESTAMP WHERE proxy_url = ?", proxyURL) + return proxyURL, nil +} + +// UpdateCodexProxyStats 更新代理统计 +func (d *DB) UpdateCodexProxyStats(proxyURL string, success bool) error { + if success { + _, err := d.db.Exec("UPDATE codex_auth_proxies SET success_count = success_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE proxy_url = ?", proxyURL) + return err + } + _, err := d.db.Exec("UPDATE codex_auth_proxies SET fail_count = fail_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE proxy_url = ?", proxyURL) + return err +} + +// ToggleCodexProxy 切换代理启用状态 +func (d *DB) ToggleCodexProxy(id int64) error { + _, err := d.db.Exec("UPDATE codex_auth_proxies SET is_enabled = 1 - is_enabled WHERE id = ?", id) + return err +} + +// DeleteCodexProxy 删除代理 +func (d *DB) DeleteCodexProxy(id int64) error { + _, err := d.db.Exec("DELETE FROM codex_auth_proxies WHERE id = ?", id) + return err +} + +// ClearCodexProxies 清空所有代理 +func (d *DB) ClearCodexProxies() error { + _, err := d.db.Exec("DELETE FROM codex_auth_proxies") + return err +} + +// GetCodexProxyStats 获取代理统计 +func (d *DB) GetCodexProxyStats() map[string]int { + stats := map[string]int{ + "total": 0, + "enabled": 0, + "disabled": 0, + } + + var total, enabled, disabled int + d.db.QueryRow("SELECT COUNT(*) FROM codex_auth_proxies").Scan(&total) + d.db.QueryRow("SELECT COUNT(*) FROM codex_auth_proxies WHERE is_enabled = 1").Scan(&enabled) + d.db.QueryRow("SELECT COUNT(*) FROM codex_auth_proxies WHERE is_enabled = 0").Scan(&disabled) + stats["total"] = total + stats["enabled"] = enabled + stats["disabled"] = disabled + + return stats +} + // Close 关闭数据库 func (d *DB) Close() error { if d.db != nil { diff --git a/backend/internal/register/chatgpt.go b/backend/internal/register/chatgpt.go index 7a5f3b3..0568daf 100644 --- a/backend/internal/register/chatgpt.go +++ b/backend/internal/register/chatgpt.go @@ -11,6 +11,7 @@ import ( "time" "codex-pool/internal/client" + "codex-pool/internal/logger" "codex-pool/internal/mail" ) @@ -236,6 +237,7 @@ func (r *ChatGPTReg) CreateAccount(name, birthdate string) error { return fmt.Errorf("创建账户失败,状态码: %d", resp.StatusCode) } + // 处理可能出现的继续 URL var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err == nil { if continueURL, ok := result["continue_url"].(string); ok && continueURL != "" { @@ -274,142 +276,134 @@ func (r *ChatGPTReg) GetSessionToken() error { return fmt.Errorf("响应中没有 accessToken") } -// Run 完整的注册流程 -func Run(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) { - return RunWithRetry(email, password, name, birthdate, proxy, 3) +// Run 执行单次注册(由 main.go 调用) +func Run(email, password, realName, birthdate, proxy string) (*ChatGPTReg, error) { + return APIRegister(email, password, realName, birthdate, proxy, "[RegTest]") } -// RunWithRetry 带重试的注册流程 -// 当验证码获取超过5秒,就换新邮箱重新注册 -func RunWithRetry(email, password, name, birthdate, proxy string, maxRetries int) (*ChatGPTReg, error) { - for attempt := 0; attempt < maxRetries; attempt++ { - if attempt > 0 { - // 重试时生成新邮箱 - email = mail.GenerateEmail() - password = GeneratePassword() - fmt.Printf(" [Retry %d] New email: %s\n", attempt, email) +// APIRegister 使用 API 完成注册 (集成 403 重试机制) +func APIRegister(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) { + var reg *ChatGPTReg + var lastErr error + + // 403 重试机制 - 最多重试 3 次,每次换新指纹 + for retry := 0; retry < 3; retry++ { + var err error + reg, err = New(proxy) + if err != nil { + lastErr = err + if retry < 2 { + logger.Warning(fmt.Sprintf("%s 客户端创建失败,重试 %d/3...", logPrefix, retry+1), email, "register") + continue + } + return nil, fmt.Errorf("客户端创建失败: %v", err) } - reg, err := runOnce(email, password, name, birthdate, proxy) - if err == nil { - return reg, nil + fpInfo := reg.Client.GetFingerprintInfo() + if retry == 0 { + logger.Info(fmt.Sprintf("%s 初始化会话... [%s]", logPrefix, fpInfo), email, "register") + } else { + logger.Warning(fmt.Sprintf("%s 403 重试 %d/3,换指纹 [%s]", logPrefix, retry, fpInfo), email, "register") } - // 如果不是验证码超时错误,直接返回 - if !strings.Contains(err.Error(), "验证码获取超时") { - return nil, err + if err := reg.InitSession(); err != nil { + if strings.Contains(err.Error(), "403") { + lastErr = err + reg.Client.Close() + continue // 403 则换指纹重试 + } + return nil, fmt.Errorf("初始化失败: %v", err) } - fmt.Printf(" [!] OTP timeout, retrying with new email...\n") + // 初始化成功 + lastErr = nil + break } - return nil, fmt.Errorf("注册失败: 已重试 %d 次", maxRetries) -} - -// runOnce 执行一次注册流程(使用短超时获取验证码) -func runOnce(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) { - reg, err := New(proxy) - if err != nil { - return nil, err + if lastErr != nil { + return nil, fmt.Errorf("初始化失败: %v (已重试3次)", lastErr) } - // 初始化 - if err := reg.InitSession(); err != nil { - return nil, fmt.Errorf("初始化失败: %v", err) - } + // 获取授权 URL if err := reg.GetAuthorizeURL(email); err != nil { return nil, fmt.Errorf("获取授权URL失败: %v", err) } + + // 开始授权流程 if err := reg.StartAuthorize(); err != nil { - return nil, fmt.Errorf("启动授权失败: %v", err) + return nil, fmt.Errorf("授权流程失败: %v", err) } - // 注册 + // 注册账户 if err := reg.Register(email, password); err != nil { - return nil, fmt.Errorf("注册失败: %v", err) + return nil, fmt.Errorf("注册账号失败: %v", err) } + + // 发送验证邮件 if err := reg.SendVerificationEmail(); err != nil { - return nil, fmt.Errorf("发送邮件失败: %v", err) + return nil, fmt.Errorf("发送验证邮件失败: %v", err) } + logger.Success(fmt.Sprintf("%s 已发送验证邮件", logPrefix), email, "register") - // 先用5秒超时尝试获取验证码 - otpCode, err := mail.GetVerificationCode(email, 5*time.Second) + // 获取验证码 (带超时 90s) + logger.Info(fmt.Sprintf("%s 等待验证码...", logPrefix), email, "register") + otpCode, err := mail.GetVerificationCode(email, 90*time.Second) if err != nil { - // 5秒内没获取到,再等120秒(总共等待更多时间) - otpCode, err = mail.GetVerificationCode(email, 120*time.Second) - if err != nil { - return nil, fmt.Errorf("验证码获取超时") - } + return nil, err } + logger.Success(fmt.Sprintf("%s 验证码: %s", logPrefix, otpCode), email, "register") + // 验证 OTP if err := reg.ValidateOTP(otpCode); err != nil { - return nil, fmt.Errorf("OTP验证失败: %v", err) + return nil, fmt.Errorf("验证OTP失败: %v", err) } // 创建账户 - if err := reg.CreateAccount(name, birthdate); err != nil { + if err := reg.CreateAccount(realName, birthdate); err != nil { return nil, fmt.Errorf("创建账户失败: %v", err) } - // 获取 Token + // 预热 session (可选) _ = reg.GetSessionToken() return reg, nil } -// ==================== 工具函数 ==================== - -// GenerateName 生成随机姓名 -func GenerateName() string { - firstNames := []string{"James", "John", "Robert", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Charles"} - lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"} - return firstNames[rand.Intn(len(firstNames))] + " " + lastNames[rand.Intn(len(lastNames))] -} - // GenerateUUID 生成 UUID func GenerateUUID() string { b := make([]byte, 16) rand.Read(b) - b[6] = (b[6] & 0x0f) | 0x40 - b[8] = (b[8] & 0x3f) | 0x80 - return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) } -// GenerateBirthdate 生成随机生日 +// GenerateName 随机生成姓名 +func GenerateName() string { + firstNames := []string{"James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", "William", "Elizabeth"} + lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"} + return firstNames[rand.Intn(len(firstNames))] + " " + lastNames[rand.Intn(len(lastNames))] +} + +// GenerateBirthdate 随机生成生日 func GenerateBirthdate() string { - year := 2000 + rand.Intn(5) - month := 1 + rand.Intn(12) - day := 1 + rand.Intn(28) - return fmt.Sprintf("%d-%02d-%02d", year, month, day) + year := rand.Intn(20) + 1980 + month := rand.Intn(12) + 1 + day := rand.Intn(28) + 1 + return fmt.Sprintf("%04d-%02d-%02d", year, month, day) } -// GeneratePassword 生成随机密码 +// GeneratePassword 随机生成密码 func GeneratePassword() string { - const ( - upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - lower = "abcdefghijklmnopqrstuvwxyz" - digits = "0123456789" - special = "!@#$%" - ) - - b := make([]byte, 13) - for i := 0; i < 2; i++ { - b[i] = upper[rand.Intn(len(upper))] + chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" + p := make([]byte, 12) + for i := range p { + p[i] = chars[rand.Intn(len(chars))] } - for i := 2; i < 10; i++ { - b[i] = lower[rand.Intn(len(lower))] - } - for i := 10; i < 12; i++ { - b[i] = digits[rand.Intn(len(digits))] - } - b[12] = special[rand.Intn(len(special))] - - return string(b) + return string(p) } -func truncateStr(s string, maxLen int) string { - if len(s) <= maxLen { +func truncateStr(s string, max int) string { + if len(s) <= max { return s } - return s[:maxLen] + "..." + return s[:max] + "..." } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6629b65..3c363cb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { Routes, Route } from 'react-router-dom' import { ConfigProvider, RecordsProvider } from './context' import { Layout } from './components/layout' -import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner, TeamReg } from './pages' +import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner, TeamReg, CodexProxyConfig } from './pages' function App() { return ( @@ -19,6 +19,7 @@ function App() { } /> } /> } /> + } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index dea4c7b..82ed79d 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -15,6 +15,7 @@ import { Cog, Trash2, UserPlus, + Globe, } from 'lucide-react' interface SidebarProps { @@ -45,6 +46,7 @@ const navItems: NavItem[] = [ { to: '/config', icon: Cog, label: '配置概览' }, { to: '/config/s2a', icon: Server, label: 'S2A 配置' }, { to: '/config/email', icon: Mail, label: '邮箱配置' }, + { to: '/config/codex-proxy', icon: Globe, label: '代理池' }, ] }, ] diff --git a/frontend/src/pages/CodexProxyConfig.tsx b/frontend/src/pages/CodexProxyConfig.tsx new file mode 100644 index 0000000..b619385 --- /dev/null +++ b/frontend/src/pages/CodexProxyConfig.tsx @@ -0,0 +1,448 @@ +import { useState, useEffect, useCallback } from 'react' +import { + Globe, Plus, Trash2, ToggleLeft, ToggleRight, + Loader2, Save, RefreshCcw, CheckCircle, XCircle, + AlertTriangle, Clock +} from 'lucide-react' +import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' + +interface CodexProxy { + id: number + proxy_url: string + description: string + is_enabled: boolean + last_used_at: string | null + success_count: number + fail_count: number + created_at: string +} + +interface ProxyStats { + total: number + enabled: number + disabled: number +} + +export default function CodexProxyConfig() { + const [proxies, setProxies] = useState([]) + const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null) + + // 单个添加 + const [newProxyUrl, setNewProxyUrl] = useState('') + const [newDescription, setNewDescription] = useState('') + + // 批量添加 + const [batchMode, setBatchMode] = useState(false) + const [batchInput, setBatchInput] = useState('') + + // 获取代理列表 + const fetchProxies = useCallback(async () => { + setLoading(true) + try { + const res = await fetch('/api/codex-proxy') + const data = await res.json() + if (data.code === 0 && data.data) { + setProxies(data.data.proxies || []) + setStats(data.data.stats || { total: 0, enabled: 0, disabled: 0 }) + } + } catch (error) { + console.error('获取代理列表失败:', error) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchProxies() + }, [fetchProxies]) + + // 添加代理 + const handleAddProxy = async () => { + if (!newProxyUrl.trim()) return + setSaving(true) + setMessage(null) + try { + const res = await fetch('/api/codex-proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + proxy_url: newProxyUrl.trim(), + description: newDescription.trim(), + }), + }) + const data = await res.json() + if (data.code === 0) { + setMessage({ type: 'success', text: '代理添加成功' }) + setNewProxyUrl('') + setNewDescription('') + fetchProxies() + } else { + setMessage({ type: 'error', text: data.message || '添加失败' }) + } + } catch { + setMessage({ type: 'error', text: '网络错误' }) + } finally { + setSaving(false) + } + } + + // 批量添加 + const handleBatchAdd = async () => { + 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', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxies: lines }), + }) + const data = await res.json() + if (data.code === 0) { + setMessage({ type: 'success', text: `成功添加 ${data.data.added}/${data.data.total} 个代理` }) + setBatchInput('') + fetchProxies() + } else { + setMessage({ type: 'error', text: data.message || '添加失败' }) + } + } catch { + setMessage({ type: 'error', text: '网络错误' }) + } finally { + setSaving(false) + } + } + + // 切换启用状态 + const handleToggle = async (id: number) => { + try { + const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'PUT' }) + const data = await res.json() + if (data.code === 0) { + fetchProxies() + } + } catch (error) { + console.error('切换状态失败:', error) + } + } + + // 删除代理 + const handleDelete = async (id: number) => { + if (!confirm('确定要删除这个代理吗?')) return + try { + const res = await fetch(`/api/codex-proxy?id=${id}`, { method: 'DELETE' }) + const data = await res.json() + if (data.code === 0) { + fetchProxies() + } + } catch (error) { + console.error('删除失败:', error) + } + } + + // 清空所有 + const handleClearAll = async () => { + if (!confirm('确定要清空所有代理吗?此操作不可恢复!')) return + try { + 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() + } + } catch (error) { + console.error('清空失败:', error) + } + } + + // 格式化代理显示 + const formatProxyDisplay = (url: string) => { + if (url.includes('@')) { + const parts = url.split('@') + return parts[parts.length - 1] + } + return url.replace(/^https?:\/\//, '').replace(/^socks5:\/\//, '') + } + + // 格式化时间 + const formatTime = (timeStr: string | null) => { + if (!timeStr) return '从未' + const date = new Date(timeStr) + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + } + + // 计算成功率 + const getSuccessRate = (proxy: CodexProxy) => { + const total = proxy.success_count + proxy.fail_count + if (total === 0) return null + return Math.round((proxy.success_count / total) * 100) + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Header */} +
+
+

+ + CodexAuth 代理池 +

+

+ 管理 CodexAuth API 授权使用的代理池,支持随机轮换 +

+
+
+ + {proxies.length > 0 && ( + + )} +
+
+ + {/* Message */} + {message && ( +
+ {message.text} +
+ )} + + {/* Stats */} +
+ + +
+
+ {stats.total} +
+
+ 总数 +
+
+
+
+ + +
+
+ {stats.enabled} +
+
+ 已启用 +
+
+
+
+ + +
+
+ {stats.disabled} +
+
+ 已禁用 +
+
+
+
+
+ + {/* Add Proxy */} + + + + + 添加代理 + + + + + {batchMode ? ( + <> +