feat: Implement S2A, Rod, and Chromedp based authentication for external services, and introduce new frontend pages and backend APIs for monitoring, configuration, upload, and team processes.
This commit is contained in:
@@ -11,13 +11,14 @@ 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"` // 补号时使用代理
|
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
|
||||||
|
BrowserType string `json:"browser_type"` // 授权浏览器引擎: chromedp 或 rod
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleGetMonitorSettings 获取监控设置
|
// HandleGetMonitorSettings 获取监控设置
|
||||||
@@ -40,6 +41,7 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
PollingEnabled: false,
|
PollingEnabled: false,
|
||||||
PollingInterval: 60,
|
PollingInterval: 60,
|
||||||
ReplenishUseProxy: false,
|
ReplenishUseProxy: false,
|
||||||
|
BrowserType: "chromedp", // 默认使用 chromedp
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
|
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
|
||||||
@@ -71,6 +73,9 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
|
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
|
||||||
settings.ReplenishUseProxy = true
|
settings.ReplenishUseProxy = true
|
||||||
}
|
}
|
||||||
|
if val, _ := database.Instance.GetConfig("monitor_browser_type"); val != "" {
|
||||||
|
settings.BrowserType = val
|
||||||
|
}
|
||||||
|
|
||||||
Success(w, settings)
|
Success(w, settings)
|
||||||
}
|
}
|
||||||
@@ -128,6 +133,14 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := database.Instance.SetConfig("monitor_replenish_use_proxy", strconv.FormatBool(settings.ReplenishUseProxy)); err != nil {
|
if err := database.Instance.SetConfig("monitor_replenish_use_proxy", strconv.FormatBool(settings.ReplenishUseProxy)); err != nil {
|
||||||
saveErrors = append(saveErrors, "replenish_use_proxy: "+err.Error())
|
saveErrors = append(saveErrors, "replenish_use_proxy: "+err.Error())
|
||||||
}
|
}
|
||||||
|
// 浏览器类型默认为 chromedp
|
||||||
|
browserType := settings.BrowserType
|
||||||
|
if browserType != "chromedp" && browserType != "rod" {
|
||||||
|
browserType = "chromedp"
|
||||||
|
}
|
||||||
|
if err := database.Instance.SetConfig("monitor_browser_type", browserType); err != nil {
|
||||||
|
saveErrors = append(saveErrors, "browser_type: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if len(saveErrors) > 0 {
|
if len(saveErrors) > 0 {
|
||||||
errMsg := "保存监控设置部分失败: " + saveErrors[0]
|
errMsg := "保存监控设置部分失败: " + saveErrors[0]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"codex-pool/internal/invite"
|
"codex-pool/internal/invite"
|
||||||
"codex-pool/internal/logger"
|
"codex-pool/internal/logger"
|
||||||
"codex-pool/internal/mail"
|
"codex-pool/internal/mail"
|
||||||
|
"codex-pool/internal/proxyutil"
|
||||||
"codex-pool/internal/register"
|
"codex-pool/internal/register"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,6 +128,14 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.Proxy == "" && config.Global != nil {
|
if req.Proxy == "" && config.Global != nil {
|
||||||
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
||||||
}
|
}
|
||||||
|
if req.Proxy != "" {
|
||||||
|
normalized, err := proxyutil.Normalize(req.Proxy)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Proxy = normalized
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化状态
|
// 初始化状态
|
||||||
teamProcessState.Running = true
|
teamProcessState.Running = true
|
||||||
|
|||||||
@@ -6,12 +6,30 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codex-pool/internal/proxyutil"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/fetch"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
|
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
|
||||||
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||||
|
var proxyServer string
|
||||||
|
var proxyUser string
|
||||||
|
var proxyPass string
|
||||||
|
if proxy != "" {
|
||||||
|
info, err := proxyutil.Parse(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("代理格式错误: %v", err)
|
||||||
|
}
|
||||||
|
if info.Server != nil {
|
||||||
|
proxyServer = info.Server.String()
|
||||||
|
}
|
||||||
|
proxyUser = info.Username
|
||||||
|
proxyPass = info.Password
|
||||||
|
}
|
||||||
|
|
||||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
chromedp.Flag("headless", headless),
|
chromedp.Flag("headless", headless),
|
||||||
chromedp.Flag("disable-gpu", true),
|
chromedp.Flag("disable-gpu", true),
|
||||||
@@ -21,8 +39,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
|
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if proxy != "" {
|
if proxyServer != "" {
|
||||||
opts = append(opts, chromedp.ProxyServer(proxy))
|
opts = append(opts, chromedp.ProxyServer(proxyServer))
|
||||||
}
|
}
|
||||||
|
|
||||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
@@ -37,8 +55,36 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
var callbackURL string
|
var callbackURL string
|
||||||
|
|
||||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
if req, ok := ev.(*network.EventRequestWillBeSent); ok {
|
switch ev := ev.(type) {
|
||||||
url := req.Request.URL
|
case *fetch.EventRequestPaused:
|
||||||
|
// Fetch domain pauses requests; we must continue them to avoid stalling navigation.
|
||||||
|
reqID := ev.RequestID
|
||||||
|
go func() { _ = fetch.ContinueRequest(reqID).Do(ctx) }()
|
||||||
|
|
||||||
|
case *fetch.EventAuthRequired:
|
||||||
|
reqID := ev.RequestID
|
||||||
|
source := fetch.AuthChallengeSourceServer
|
||||||
|
if ev.AuthChallenge != nil {
|
||||||
|
source = ev.AuthChallenge.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
resp := &fetch.AuthChallengeResponse{Response: fetch.AuthChallengeResponseResponseDefault}
|
||||||
|
if source == fetch.AuthChallengeSourceProxy {
|
||||||
|
if proxyUser != "" {
|
||||||
|
resp.Response = fetch.AuthChallengeResponseResponseProvideCredentials
|
||||||
|
resp.Username = proxyUser
|
||||||
|
resp.Password = proxyPass
|
||||||
|
} else {
|
||||||
|
// Fail fast if the proxy requires auth but user didn't provide credentials.
|
||||||
|
resp.Response = fetch.AuthChallengeResponseResponseCancelAuth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = fetch.ContinueWithAuth(reqID, resp).Do(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
case *network.EventRequestWillBeSent:
|
||||||
|
url := ev.Request.URL
|
||||||
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
|
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
|
||||||
callbackURL = url
|
callbackURL = url
|
||||||
}
|
}
|
||||||
@@ -46,6 +92,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
})
|
})
|
||||||
|
|
||||||
err := chromedp.Run(ctx,
|
err := chromedp.Run(ctx,
|
||||||
|
// Handle proxy auth (407) in headless mode.
|
||||||
|
fetch.Enable().WithHandleAuthRequests(true),
|
||||||
network.Enable(),
|
network.Enable(),
|
||||||
chromedp.Navigate(authURL),
|
chromedp.Navigate(authURL),
|
||||||
chromedp.WaitReady("body"),
|
chromedp.WaitReady("body"),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codex-pool/internal/proxyutil"
|
||||||
|
|
||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
"github.com/go-rod/rod/lib/launcher"
|
"github.com/go-rod/rod/lib/launcher"
|
||||||
"github.com/go-rod/rod/lib/proto"
|
"github.com/go-rod/rod/lib/proto"
|
||||||
@@ -17,6 +19,8 @@ type RodAuth struct {
|
|||||||
browser *rod.Browser
|
browser *rod.Browser
|
||||||
headless bool
|
headless bool
|
||||||
proxy string
|
proxy string
|
||||||
|
proxyUser string
|
||||||
|
proxyPass string
|
||||||
}
|
}
|
||||||
|
|
||||||
// getChromiumPath 获取 Chromium 路径
|
// getChromiumPath 获取 Chromium 路径
|
||||||
@@ -49,6 +53,24 @@ func getChromiumPath() string {
|
|||||||
|
|
||||||
// NewRodAuth 创建 Rod 授权器
|
// NewRodAuth 创建 Rod 授权器
|
||||||
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||||
|
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().
|
l := launcher.New().
|
||||||
Headless(headless).
|
Headless(headless).
|
||||||
Set("disable-blink-features", "AutomationControlled").
|
Set("disable-blink-features", "AutomationControlled").
|
||||||
@@ -67,8 +89,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
|||||||
l = l.Bin(chromiumPath)
|
l = l.Bin(chromiumPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxy != "" {
|
if proxyServer != "" {
|
||||||
l = l.Proxy(proxy)
|
l = l.Proxy(proxyServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
controlURL, err := l.Launch()
|
controlURL, err := l.Launch()
|
||||||
@@ -85,6 +107,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
|||||||
browser: browser,
|
browser: browser,
|
||||||
headless: headless,
|
headless: headless,
|
||||||
proxy: proxy,
|
proxy: proxy,
|
||||||
|
proxyUser: proxyUser,
|
||||||
|
proxyPass: proxyPass,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +121,39 @@ func (r *RodAuth) Close() {
|
|||||||
|
|
||||||
// CompleteOAuth 完成 OAuth 授权
|
// CompleteOAuth 完成 OAuth 授权
|
||||||
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
|
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
|
||||||
|
// Handle proxy auth (407) in headless mode.
|
||||||
|
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
|
||||||
|
if r.proxy != "" {
|
||||||
|
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)
|
page, err := stealth.Page(r.browser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("创建页面失败: %v", err)
|
return "", fmt.Errorf("创建页面失败: %v", err)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codex-pool/internal/proxyutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -214,8 +216,13 @@ func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, erro
|
|||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
if proxyURL != "" {
|
if proxyURL != "" {
|
||||||
proxyURLParsed, _ := url.Parse(proxyURL)
|
info, err := proxyutil.Parse(proxyURL)
|
||||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("代理格式错误: %v", err)
|
||||||
|
}
|
||||||
|
if info.URL != nil {
|
||||||
|
client.Transport = &http.Transport{Proxy: http.ProxyURL(info.URL)}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := url.Values{
|
data := url.Values{
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codex-pool/internal/proxyutil"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
http2 "github.com/bogdanfinn/fhttp"
|
http2 "github.com/bogdanfinn/fhttp"
|
||||||
tls_client "github.com/bogdanfinn/tls-client"
|
tls_client "github.com/bogdanfinn/tls-client"
|
||||||
@@ -44,7 +46,11 @@ func New(proxyStr string) (*TLSClient, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if proxyStr != "" {
|
if proxyStr != "" {
|
||||||
options = append(options, tls_client.WithProxyUrl(proxyStr))
|
normalized, err := proxyutil.Normalize(proxyStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
options = append(options, tls_client.WithProxyUrl(normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
||||||
|
|||||||
107
backend/internal/proxyutil/proxy.go
Normal file
107
backend/internal/proxyutil/proxy.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package proxyutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info is a parsed proxy configuration.
|
||||||
|
//
|
||||||
|
// It supports the following input formats:
|
||||||
|
// - http://host:port
|
||||||
|
// - http://user:pass@host:port
|
||||||
|
// - host:port (defaults to http)
|
||||||
|
// - host:port:user:pass (defaults to http; password may contain ':')
|
||||||
|
// - user:pass@host:port (defaults to http)
|
||||||
|
type Info struct {
|
||||||
|
// URL is the normalized proxy URL (may include credentials).
|
||||||
|
URL *url.URL
|
||||||
|
// Server is the proxy server URL without credentials (scheme + host:port).
|
||||||
|
Server *url.URL
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(raw string) (*Info, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return &Info{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL-style proxy (has scheme)
|
||||||
|
if strings.Contains(raw, "://") {
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse proxy url: %w", err)
|
||||||
|
}
|
||||||
|
if u.Scheme == "" || u.Host == "" {
|
||||||
|
return nil, fmt.Errorf("invalid proxy url: %q", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
u = &url.URL{
|
||||||
|
Scheme: strings.ToLower(u.Scheme),
|
||||||
|
Host: u.Host,
|
||||||
|
User: u.User,
|
||||||
|
}
|
||||||
|
|
||||||
|
user := ""
|
||||||
|
pass := ""
|
||||||
|
if u.User != nil {
|
||||||
|
user = u.User.Username()
|
||||||
|
pass, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &url.URL{Scheme: u.Scheme, Host: u.Host}
|
||||||
|
return &Info{
|
||||||
|
URL: u,
|
||||||
|
Server: server,
|
||||||
|
Username: user,
|
||||||
|
Password: pass,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// user:pass@host:port (no scheme)
|
||||||
|
if strings.Contains(raw, "@") {
|
||||||
|
return Parse("http://" + raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// host:port[:user:pass]
|
||||||
|
parts := strings.Split(raw, ":")
|
||||||
|
switch {
|
||||||
|
case len(parts) == 2:
|
||||||
|
host, port := parts[0], parts[1]
|
||||||
|
hp := net.JoinHostPort(host, port)
|
||||||
|
u := &url.URL{Scheme: "http", Host: hp}
|
||||||
|
return &Info{URL: u, Server: &url.URL{Scheme: u.Scheme, Host: u.Host}}, nil
|
||||||
|
case len(parts) >= 4:
|
||||||
|
host, port := parts[0], parts[1]
|
||||||
|
user := parts[2]
|
||||||
|
pass := strings.Join(parts[3:], ":")
|
||||||
|
hp := net.JoinHostPort(host, port)
|
||||||
|
u := &url.URL{Scheme: "http", Host: hp, User: url.UserPassword(user, pass)}
|
||||||
|
return &Info{
|
||||||
|
URL: u,
|
||||||
|
Server: &url.URL{Scheme: u.Scheme, Host: u.Host},
|
||||||
|
Username: user,
|
||||||
|
Password: pass,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported proxy format: %q", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize returns a normalized proxy URL string.
|
||||||
|
// Empty input returns empty output.
|
||||||
|
func Normalize(raw string) (string, error) {
|
||||||
|
info, err := Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if info.URL == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return info.URL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ export default function Config() {
|
|||||||
setDefaultProxy(e.target.value)
|
setDefaultProxy(e.target.value)
|
||||||
setProxyStatus('unknown')
|
setProxyStatus('unknown')
|
||||||
}}
|
}}
|
||||||
placeholder="http://127.0.0.1:7890"
|
placeholder="http://127.0.0.1:7890 或 1.2.3.4:5678:user:pass"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -299,7 +299,7 @@ export default function Config() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||||
设置全局默认代理地址,用于批量入库和自动补号
|
设置全局默认代理地址,用于批量入库和自动补号。支持格式:http://host:port、http://user:pass@host:port、host:port:user:pass(默认按 http 解析)
|
||||||
{proxyOriginIP && (
|
{proxyOriginIP && (
|
||||||
<span className="ml-2 text-green-600 dark:text-green-400">
|
<span className="ml-2 text-green-600 dark:text-green-400">
|
||||||
出口IP: {proxyOriginIP}
|
出口IP: {proxyOriginIP}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default function Monitor() {
|
|||||||
const [pollingInterval, setPollingInterval] = useState(60)
|
const [pollingInterval, setPollingInterval] = useState(60)
|
||||||
const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理
|
const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理
|
||||||
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
|
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
|
||||||
|
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') // 授权浏览器引擎
|
||||||
|
|
||||||
// 倒计时状态
|
// 倒计时状态
|
||||||
const [countdown, setCountdown] = useState(60)
|
const [countdown, setCountdown] = useState(60)
|
||||||
@@ -132,6 +133,7 @@ export default function Monitor() {
|
|||||||
polling_enabled: pollingEnabled,
|
polling_enabled: pollingEnabled,
|
||||||
polling_interval: pollingInterval,
|
polling_interval: pollingInterval,
|
||||||
replenish_use_proxy: replenishUseProxy,
|
replenish_use_proxy: replenishUseProxy,
|
||||||
|
browser_type: browserType,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -269,6 +271,7 @@ export default function Monitor() {
|
|||||||
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
|
const replenishUseProxyVal = s.replenish_use_proxy || false
|
||||||
|
const browserTypeVal = s.browser_type || 'chromedp'
|
||||||
|
|
||||||
setTargetInput(target)
|
setTargetInput(target)
|
||||||
setAutoAdd(autoAddVal)
|
setAutoAdd(autoAddVal)
|
||||||
@@ -277,11 +280,12 @@ export default function Monitor() {
|
|||||||
setPollingEnabled(pollingEnabledVal)
|
setPollingEnabled(pollingEnabledVal)
|
||||||
setPollingInterval(interval)
|
setPollingInterval(interval)
|
||||||
setReplenishUseProxy(replenishUseProxyVal)
|
setReplenishUseProxy(replenishUseProxyVal)
|
||||||
|
setBrowserType(browserTypeVal as 'chromedp' | 'rod')
|
||||||
savedPollingIntervalRef.current = interval
|
savedPollingIntervalRef.current = interval
|
||||||
setCountdown(interval)
|
setCountdown(interval)
|
||||||
|
|
||||||
// 返回加载的配置用于后续刷新
|
// 返回加载的配置用于后续刷新
|
||||||
return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval, replenishUseProxy: replenishUseProxyVal }
|
return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval, replenishUseProxy: replenishUseProxyVal, browserType: browserTypeVal }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,8 +536,45 @@ export default function Monitor() {
|
|||||||
onChange={setReplenishUseProxy}
|
onChange={setReplenishUseProxy}
|
||||||
disabled={!autoAdd || !globalProxy}
|
disabled={!autoAdd || !globalProxy}
|
||||||
label="补号时使用代理"
|
label="补号时使用代理"
|
||||||
description={globalProxy ? `当前代理: ${globalProxy}` : '请先在系统配置中设置代理地址'}
|
description={
|
||||||
/>
|
globalProxy
|
||||||
|
? `当前代理: ${globalProxy}`
|
||||||
|
: '请先在系统配置中设置代理地址(支持 http://host:port、http://user:pass@host:port、host:port:user:pass)'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 浏览器选择器 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
授权浏览器引擎
|
||||||
|
</label>
|
||||||
|
<div className="flex rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBrowserType('chromedp')}
|
||||||
|
disabled={!autoAdd}
|
||||||
|
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 ${browserType === 'chromedp'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
|
||||||
|
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
Chromedp
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBrowserType('rod')}
|
||||||
|
disabled={!autoAdd}
|
||||||
|
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 border-l border-slate-200 dark:border-slate-700 ${browserType === 'rod'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
|
||||||
|
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
Rod
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
选择用于自动授权的浏览器引擎,Chromedp 为默认高效选项,Rod 为备选
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label="最小间隔 (秒)"
|
label="最小间隔 (秒)"
|
||||||
|
|||||||
@@ -478,7 +478,11 @@ export default function Upload() {
|
|||||||
onChange={setUseProxy}
|
onChange={setUseProxy}
|
||||||
disabled={isRunning || !globalProxy}
|
disabled={isRunning || !globalProxy}
|
||||||
label="使用全局代理"
|
label="使用全局代理"
|
||||||
description={globalProxy ? `当前代理: ${globalProxy}` : '请先在系统配置中设置代理地址'}
|
description={
|
||||||
|
globalProxy
|
||||||
|
? `当前代理: ${globalProxy}`
|
||||||
|
: '请先在系统配置中设置代理地址(支持 http://host:port、http://user:pass@host:port、host:port:user:pass)'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user