diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 42785a0..f348eb4 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -332,7 +332,7 @@ func handleClearLogsByModule(w http.ResponseWriter, r *http.Request) { }) } -// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5 +// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5&level=success func handleQueryLogs(w http.ResponseWriter, r *http.Request) { module := r.URL.Query().Get("module") if module == "" { @@ -342,6 +342,8 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) { page := 1 pageSize := 5 + level := r.URL.Query().Get("level") // 可选的日志级别过滤 + if v := r.URL.Query().Get("page"); v != "" { if p, err := strconv.Atoi(v); err == nil && p > 0 { page = p @@ -353,7 +355,14 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) { } } - entries, total := logger.GetLogsByModule(module, page, pageSize) + // 如果指定了 level,使用带级别过滤的函数 + var entries []logger.LogEntry + var total int + if level != "" { + entries, total = logger.GetLogsByModuleAndLevel(module, level, page, pageSize) + } else { + entries, total = logger.GetLogsByModule(module, page, pageSize) + } totalPages := (total + pageSize - 1) / pageSize api.Success(w, map[string]interface{}{ diff --git a/backend/cmd/test_browser_auth/main.go b/backend/cmd/test_browser_auth/main.go deleted file mode 100644 index f605bfe..0000000 --- a/backend/cmd/test_browser_auth/main.go +++ /dev/null @@ -1,433 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "strings" - "sync" - "sync/atomic" - "time" - - "codex-pool/internal/auth" - "codex-pool/internal/config" - "codex-pool/internal/invite" - "codex-pool/internal/mail" - "codex-pool/internal/register" -) - -type Account struct { - Account string `json:"account"` - Password string `json:"password"` - Token string `json:"token"` -} - -type MemberAccount struct { - Email string - Password string - Success bool -} - -const ( - MembersPerTeam = 4 // 每个 team 注册的成员数 - NumTeams = 2 // 并发运行的 team 数量 -) - -// ANSI 颜色码 -const ( - ColorReset = "\033[0m" - ColorRed = "\033[31m" - ColorGreen = "\033[32m" - ColorYellow = "\033[33m" - ColorBlue = "\033[34m" - ColorMagenta = "\033[35m" - ColorCyan = "\033[36m" - ColorWhite = "\033[37m" - ColorBold = "\033[1m" -) - -// Team 颜色 -var teamColors = []string{ - ColorCyan, // Team 1 - ColorMagenta, // Team 2 - ColorYellow, // Team 3 - ColorBlue, // Team 4 -} - -// TeamLogger 带颜色的Team日志 -type TeamLogger struct { - prefix string - color string - mu sync.Mutex -} - -func NewTeamLogger(teamIdx int) *TeamLogger { - color := teamColors[teamIdx%len(teamColors)] - return &TeamLogger{ - prefix: fmt.Sprintf("[Team %d]", teamIdx+1), - color: color, - } -} - -func (l *TeamLogger) Log(format string, args ...interface{}) { - l.mu.Lock() - defer l.mu.Unlock() - msg := fmt.Sprintf(format, args...) - fmt.Printf("%s%s%s %s\n", l.color, l.prefix, ColorReset, msg) -} - -func (l *TeamLogger) Success(format string, args ...interface{}) { - l.mu.Lock() - defer l.mu.Unlock() - msg := fmt.Sprintf(format, args...) - fmt.Printf("%s%s%s %s✓%s %s\n", l.color, l.prefix, ColorReset, ColorGreen, ColorReset, msg) -} - -func (l *TeamLogger) Error(format string, args ...interface{}) { - l.mu.Lock() - defer l.mu.Unlock() - msg := fmt.Sprintf(format, args...) - fmt.Printf("%s%s%s %s✗%s %s\n", l.color, l.prefix, ColorReset, ColorRed, ColorReset, msg) -} - -func (l *TeamLogger) Info(format string, args ...interface{}) { - l.mu.Lock() - defer l.mu.Unlock() - msg := fmt.Sprintf(format, args...) - fmt.Printf("%s%s%s %s→%s %s\n", l.color, l.prefix, ColorReset, ColorYellow, ColorReset, msg) -} - -// Highlight 整行绿色高亮(用于重要成功信息) -func (l *TeamLogger) Highlight(format string, args ...interface{}) { - l.mu.Lock() - defer l.mu.Unlock() - msg := fmt.Sprintf(format, args...) - fmt.Printf("%s%s %s✓ %s%s\n", ColorGreen, l.prefix, ColorBold, msg, ColorReset) -} - -func main() { - fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) - fmt.Printf("%s Multi-Team Concurrent Test (Chromedp)%s\n", ColorBold, ColorReset) - fmt.Printf(" - %d Teams running concurrently\n", NumTeams) - fmt.Printf(" - %d Members per team\n", MembersPerTeam) - fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) - fmt.Println() - - // 加载配置 - configPath := config.FindPath() - cfg, err := config.Load(configPath) - if err != nil { - fmt.Printf("%s[Error]%s Failed to load config: %v\n", ColorRed, ColorReset, err) - os.Exit(1) - } - - // 初始化邮箱服务 - if len(cfg.MailServices) > 0 { - mail.Init(cfg.MailServices) - } - - // 加载账号 - accountsFile := "accounts-3-20260130-052841.json" - data, err := os.ReadFile(accountsFile) - if err != nil { - fmt.Printf("%s[Error]%s Failed to read accounts file: %v\n", ColorRed, ColorReset, err) - os.Exit(1) - } - - var accounts []Account - if err := json.Unmarshal(data, &accounts); err != nil { - fmt.Printf("%s[Error]%s Failed to parse accounts file: %v\n", ColorRed, ColorReset, err) - os.Exit(1) - } - - if len(accounts) < NumTeams { - fmt.Printf("%s[Error]%s Need at least %d owner accounts, got %d\n", ColorRed, ColorReset, NumTeams, len(accounts)) - os.Exit(1) - } - - proxy := cfg.DefaultProxy - if proxy == "" { - proxy = "http://127.0.0.1:7890" - } - fmt.Printf("[Proxy] %s\n", proxy) - fmt.Println() - - // 显示 Owner 列表 - fmt.Printf("%s========================================%s\n", ColorBold, ColorReset) - fmt.Printf("%s[Owners]%s\n", ColorBold, ColorReset) - fmt.Printf("%s========================================%s\n", ColorBold, ColorReset) - for i := 0; i < NumTeams; i++ { - color := teamColors[i%len(teamColors)] - fmt.Printf(" %sTeam %d:%s %s\n", color, i+1, ColorReset, accounts[i].Account) - } - fmt.Println() - - // 并发运行多个 Team - var wg sync.WaitGroup - var totalRegistered int32 - var totalS2A int32 - startTime := time.Now() - - for teamIdx := 0; teamIdx < NumTeams; teamIdx++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - registered, s2a := runTeam(idx, accounts[idx], cfg, proxy) - atomic.AddInt32(&totalRegistered, int32(registered)) - atomic.AddInt32(&totalS2A, int32(s2a)) - }(teamIdx) - } - - wg.Wait() - totalDuration := time.Since(startTime) - - // 总结 - fmt.Println() - fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) - fmt.Printf("%s All Teams Complete%s\n", ColorBold, ColorReset) - fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset) - fmt.Printf(" Total Registered: %s%d/%d%s\n", ColorGreen, totalRegistered, NumTeams*MembersPerTeam, ColorReset) - fmt.Printf(" Total Added to S2A: %s%d%s\n", ColorGreen, totalS2A, ColorReset) - fmt.Printf(" Total Duration: %v\n", totalDuration) - fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset) -} - -// runTeam 运行单个 Team 的流程 -func runTeam(teamIdx int, owner Account, cfg *config.Config, proxy string) (registered, s2a int) { - log := NewTeamLogger(teamIdx) - - log.Log("Starting with owner: %s", owner.Account) - - // Step 1: 获取 Team ID - log.Info("Fetching Team ID...") - inviter := invite.NewWithProxy(owner.Token, proxy) - teamID, err := inviter.GetAccountID() - if err != nil { - log.Error("Failed to get Team ID: %v", err) - return 0, 0 - } - log.Success("Team ID: %s", teamID) - - // Step 2: 生成成员邮箱 - log.Info("Generating %d member emails...", MembersPerTeam) - children := make([]MemberAccount, MembersPerTeam) - for i := 0; i < MembersPerTeam; i++ { - children[i].Email = mail.GenerateEmail() - children[i].Password = register.GeneratePassword() - log.Log("[Member %d] Email: %s", i+1, children[i].Email) - } - - // 批量发送邀请 - log.Info("Sending invites...") - inviteEmails := make([]string, MembersPerTeam) - for i, c := range children { - inviteEmails[i] = c.Email - } - if err := inviter.SendInvites(inviteEmails); err != nil { - log.Error("Failed to send invites: %v", err) - return 0, 0 - } - log.Success("Sent %d invite(s)", len(inviteEmails)) - - // Step 3: 并发注册成员 - log.Info("Starting member registration...") - var memberWg sync.WaitGroup - var successCount int32 - memberMutex := sync.Mutex{} - - for i := range children { - memberWg.Add(1) - go func(memberIdx int) { - defer memberWg.Done() - - memberMutex.Lock() - email := children[memberIdx].Email - password := children[memberIdx].Password - memberMutex.Unlock() - - name := register.GenerateName() - birthdate := register.GenerateBirthdate() - - // 最多重试3次 - for attempt := 0; attempt < 3; attempt++ { - if attempt > 0 { - email = mail.GenerateEmail() - password = register.GeneratePassword() - log.Log("[Member %d] Retry %d - New email: %s", memberIdx+1, attempt, email) - - // 发送新邀请 - if err := inviter.SendInvites([]string{email}); err != nil { - log.Error("[Member %d] Failed to send retry invite: %v", memberIdx+1, err) - continue - } - log.Success("[Member %d] Sent retry invite", memberIdx+1) - } - - // 详细注册流程 - if err := registerMemberDetailed(log, memberIdx+1, email, password, name, birthdate, proxy); err != nil { - if strings.Contains(err.Error(), "验证码") { - log.Error("[Member %d] OTP timeout, will retry...", memberIdx+1) - continue - } - log.Error("[Member %d] Registration failed: %v", memberIdx+1, err) - return - } - - // 成功 - memberMutex.Lock() - children[memberIdx].Email = email - children[memberIdx].Password = password - children[memberIdx].Success = true - memberMutex.Unlock() - - atomic.AddInt32(&successCount, 1) - log.Success("[Member %d] Registration complete!", memberIdx+1) - return - } - - log.Error("[Member %d] Failed after 3 retries", memberIdx+1) - }(i) - } - - memberWg.Wait() - registered = int(successCount) - log.Success("Registration phase complete: %d/%d", registered, MembersPerTeam) - - // 收集成功的成员 - var registeredChildren []MemberAccount - for _, c := range children { - if c.Success { - registeredChildren = append(registeredChildren, c) - } - } - - if len(registeredChildren) == 0 { - log.Error("No members registered") - return registered, 0 - } - - // Step 4: 串行入库 - log.Info("Starting S2A authorization...") - - for i, child := range registeredChildren { - log.Log("[Member %d] Getting S2A auth URL...", i+1) - s2aResp, err := auth.GenerateS2AAuthURL(cfg.S2AApiBase, cfg.S2AAdminKey, cfg.ProxyID) - if err != nil { - log.Error("[Member %d] Auth URL failed: %v", i+1, err) - continue - } - log.Success("[Member %d] Got auth URL", i+1) - - log.Log("[Member %d] Running browser automation (Chromedp)...", i+1) - code, err := auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, true, proxy) - if err != nil { - log.Error("[Member %d] Browser auth failed: %v", i+1, err) - continue - } - log.Success("[Member %d] Browser auth complete", i+1) - - log.Log("[Member %d] Submitting to S2A...", i+1) - result, err := auth.SubmitS2AOAuth( - cfg.S2AApiBase, - cfg.S2AAdminKey, - s2aResp.Data.SessionID, - code, - child.Email, - cfg.Concurrency, - cfg.Priority, - cfg.GroupIDs, - cfg.ProxyID, - ) - if err != nil { - log.Error("[Member %d] S2A submit failed: %v", i+1, err) - continue - } - - log.Highlight("[Member %d] Added to S2A! ID=%d, Status=%s", i+1, result.Data.ID, result.Data.Status) - s2a++ - - time.Sleep(500 * time.Millisecond) - } - - log.Success("Team complete: %d registered, %d in S2A", registered, s2a) - return registered, s2a -} - -// registerMemberDetailed 详细的注册流程,带日志 -func registerMemberDetailed(log *TeamLogger, memberNum int, email, password, name, birthdate, proxy string) error { - prefix := fmt.Sprintf("[Member %d]", memberNum) - - log.Log("%s Creating TLS client...", prefix) - reg, err := register.New(proxy) - if err != nil { - return err - } - - log.Log("%s Initializing session...", prefix) - if err := reg.InitSession(); err != nil { - return fmt.Errorf("初始化失败: %v", err) - } - log.Success("%s Session initialized", prefix) - - log.Log("%s Getting authorize URL...", prefix) - if err := reg.GetAuthorizeURL(email); err != nil { - return fmt.Errorf("获取授权URL失败: %v", err) - } - log.Success("%s Got authorize URL", prefix) - - log.Log("%s Starting authorize flow...", prefix) - if err := reg.StartAuthorize(); err != nil { - return fmt.Errorf("启动授权失败: %v", err) - } - log.Success("%s Authorize flow started", prefix) - - log.Log("%s Registering account...", prefix) - if err := reg.Register(email, password); err != nil { - return fmt.Errorf("注册失败: %v", err) - } - log.Success("%s Account registered", prefix) - - log.Log("%s Sending verification email...", prefix) - if err := reg.SendVerificationEmail(); err != nil { - return fmt.Errorf("发送邮件失败: %v", err) - } - log.Success("%s Verification email sent", prefix) - - log.Log("%s Waiting for OTP code (5s timeout)...", prefix) - otpCode, err := mail.GetVerificationCode(email, 5*time.Second) - if err != nil { - log.Log("%s OTP not received in 5s, waiting 15s more...", prefix) - otpCode, err = mail.GetVerificationCode(email, 15*time.Second) - if err != nil { - return fmt.Errorf("验证码获取超时") - } - } - log.Success("%s Got OTP: %s", prefix, otpCode) - - log.Log("%s Validating OTP...", prefix) - if err := reg.ValidateOTP(otpCode); err != nil { - return fmt.Errorf("OTP验证失败: %v", err) - } - log.Success("%s OTP validated", prefix) - - log.Log("%s Creating account (name=%s, birthdate=%s)...", prefix, name, birthdate) - if err := reg.CreateAccount(name, birthdate); err != nil { - return fmt.Errorf("创建账户失败: %v", err) - } - log.Success("%s Account created", prefix) - - log.Log("%s Getting session token...", prefix) - _ = reg.GetSessionToken() - if reg.AccessToken != "" { - log.Success("%s Got access token: %s...", prefix, truncate(reg.AccessToken, 30)) - } - - return nil -} - -func truncate(s string, maxLen int) string { - if len(s) <= maxLen { - return s - } - return s[:maxLen] -} diff --git a/backend/internal/auth/browser_profiles.go b/backend/internal/auth/browser_profiles.go new file mode 100644 index 0000000..46b09d8 --- /dev/null +++ b/backend/internal/auth/browser_profiles.go @@ -0,0 +1,261 @@ +package auth + +import ( + "fmt" + "math/rand" +) + +// ============================================================ +// 浏览器自动化指纹配置 +// 用于 chromedp/rod 的随机指纹生成 +// ============================================================ + +// BrowserProfile 浏览器配置档案 +type BrowserProfile struct { + UserAgent string + Platform string + Width int + Height int + AcceptLang string + ChromeVer string + SecChUa string + WebGLVendor string + WebGLRender string + PixelRatio float64 + MaxTouch int + HardwareConc int // navigator.hardwareConcurrency +} + +// Windows 配置池 +var windowsProfiles = []BrowserProfile{ + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + Platform: "Win32", + Width: 1920, Height: 1080, + AcceptLang: "en-US,en;q=0.9", + ChromeVer: "133", + SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`, + WebGLVendor: "Google Inc. (NVIDIA)", + WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0, D3D11)", + PixelRatio: 1, + MaxTouch: 0, + HardwareConc: 12, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + Platform: "Win32", + Width: 2560, Height: 1440, + AcceptLang: "en-GB,en;q=0.9", + ChromeVer: "132", + SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`, + WebGLVendor: "Google Inc. (AMD)", + WebGLRender: "ANGLE (AMD, AMD Radeon RX 6800 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + PixelRatio: 1, + MaxTouch: 0, + HardwareConc: 16, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + Platform: "Win32", + Width: 1366, Height: 768, + AcceptLang: "en-US,en;q=0.9,zh-CN;q=0.8", + ChromeVer: "133", + SecChUa: `"Chromium";v="133", "Not/A)Brand";v="99", "Google Chrome";v="133"`, + WebGLVendor: "Google Inc. (Intel)", + WebGLRender: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)", + PixelRatio: 1.25, + MaxTouch: 0, + HardwareConc: 8, + }, + { + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + Platform: "Win32", + Width: 1680, Height: 1050, + AcceptLang: "de-DE,de;q=0.9,en;q=0.8", + ChromeVer: "131", + SecChUa: `"Chromium";v="131", "Not(A:Brand";v="99", "Google Chrome";v="131"`, + WebGLVendor: "Google Inc. (NVIDIA)", + WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Super Direct3D11 vs_5_0 ps_5_0, D3D11)", + PixelRatio: 1, + MaxTouch: 0, + HardwareConc: 6, + }, +} + +// macOS 配置池 +var macProfiles = []BrowserProfile{ + { + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + Platform: "MacIntel", + Width: 1440, Height: 900, + AcceptLang: "en-US,en;q=0.9", + ChromeVer: "133", + SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`, + WebGLVendor: "Google Inc. (Apple)", + WebGLRender: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", + PixelRatio: 2, + MaxTouch: 0, + HardwareConc: 10, + }, + { + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.110 Safari/537.36", + Platform: "MacIntel", + Width: 1920, Height: 1200, + AcceptLang: "en-GB,en;q=0.9,en-US;q=0.8", + ChromeVer: "132", + SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`, + WebGLVendor: "Google Inc. (Apple)", + WebGLRender: "ANGLE (Apple, Apple M2 Max, OpenGL 4.1)", + PixelRatio: 2, + MaxTouch: 0, + HardwareConc: 12, + }, + { + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + Platform: "MacIntel", + Width: 2560, Height: 1600, + AcceptLang: "fr-FR,fr;q=0.9,en;q=0.8", + ChromeVer: "131", + SecChUa: `"Chromium";v="131", "Not/A)Brand";v="99", "Google Chrome";v="131"`, + WebGLVendor: "Google Inc. (Apple)", + WebGLRender: "ANGLE (Apple, Apple M3, OpenGL 4.1)", + PixelRatio: 2, + MaxTouch: 0, + HardwareConc: 8, + }, +} + +// Linux 配置池 +var linuxProfiles = []BrowserProfile{ + { + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + Platform: "Linux x86_64", + Width: 1920, Height: 1080, + AcceptLang: "en-US,en;q=0.9", + ChromeVer: "133", + SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`, + WebGLVendor: "Google Inc. (AMD)", + WebGLRender: "ANGLE (AMD, AMD Radeon Graphics (renoir), OpenGL 4.6)", + PixelRatio: 1, + MaxTouch: 0, + HardwareConc: 8, + }, + { + UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + Platform: "Linux x86_64", + Width: 1920, Height: 1200, + AcceptLang: "en-GB,en;q=0.9", + ChromeVer: "132", + SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`, + WebGLVendor: "Google Inc. (NVIDIA)", + WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce RTX 2080, OpenGL 4.6)", + PixelRatio: 1, + MaxTouch: 0, + HardwareConc: 12, + }, +} + +// 合并所有配置 +var allBrowserProfiles []BrowserProfile + +func init() { + allBrowserProfiles = make([]BrowserProfile, 0, 20) + // Windows 权重更高(更常见) + allBrowserProfiles = append(allBrowserProfiles, windowsProfiles...) + allBrowserProfiles = append(allBrowserProfiles, windowsProfiles...) + allBrowserProfiles = append(allBrowserProfiles, macProfiles...) + allBrowserProfiles = append(allBrowserProfiles, linuxProfiles...) +} + +// GetRandomBrowserProfile 获取随机浏览器配置 +func GetRandomBrowserProfile() BrowserProfile { + return allBrowserProfiles[rand.Intn(len(allBrowserProfiles))] +} + +// GetAntiDetectionJS 获取反检测 JavaScript 代码 +func GetAntiDetectionJS(profile BrowserProfile) string { + return fmt.Sprintf(` + // 隐藏 webdriver + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined, + }); + + // 删除 CDP 检测 + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; + + // Chrome 对象 + window.chrome = { + runtime: {}, + loadTimes: function() { return {}; }, + csi: function() { return {}; }, + app: {}, + }; + + // 隐藏自动化特征 + Object.defineProperty(navigator, 'plugins', { + get: () => [ + { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, + { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' }, + { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }, + ], + }); + + Object.defineProperty(navigator, 'languages', { + get: () => ['%s'.split(',')[0], 'en'], + }); + + Object.defineProperty(navigator, 'platform', { + get: () => '%s', + }); + + Object.defineProperty(navigator, 'hardwareConcurrency', { + get: () => %d, + }); + + Object.defineProperty(navigator, 'deviceMemory', { + get: () => 8, + }); + + Object.defineProperty(screen, 'colorDepth', { + get: () => 24, + }); + + // 伪装 WebGL + const getParameterProxyHandler = { + apply: function(target, thisArg, args) { + const param = args[0]; + const gl = thisArg; + // UNMASKED_VENDOR_WEBGL + if (param === 37445) { + return '%s'; + } + // UNMASKED_RENDERER_WEBGL + if (param === 37446) { + return '%s'; + } + return Reflect.apply(target, thisArg, args); + } + }; + + const originalGetParameter = WebGLRenderingContext.prototype.getParameter; + WebGLRenderingContext.prototype.getParameter = new Proxy(originalGetParameter, getParameterProxyHandler); + + const originalGetParameter2 = WebGL2RenderingContext.prototype.getParameter; + WebGL2RenderingContext.prototype.getParameter = new Proxy(originalGetParameter2, getParameterProxyHandler); + + // Permissions API + const originalQuery = window.Permissions.prototype.query; + window.Permissions.prototype.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + ); + `, profile.AcceptLang, profile.Platform, profile.HardwareConc, profile.WebGLVendor, profile.WebGLRender) +} + +// GetBrowserProfileCount 获取浏览器配置数量 +func GetBrowserProfileCount() int { + return len(allBrowserProfiles) +} diff --git a/backend/internal/auth/chromedp.go b/backend/internal/auth/chromedp.go index e9b27b6..9421a42 100644 --- a/backend/internal/auth/chromedp.go +++ b/backend/internal/auth/chromedp.go @@ -10,11 +10,15 @@ import ( "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" + "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) // CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权 func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { + // 获取随机浏览器配置 + profile := GetRandomBrowserProfile() + var proxyServer string var proxyUser string var proxyPass string @@ -36,7 +40,16 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool chromedp.Flag("no-sandbox", true), chromedp.Flag("disable-dev-shm-usage", true), chromedp.Flag("disable-blink-features", "AutomationControlled"), - 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.Flag("disable-automation", true), + chromedp.Flag("disable-extensions", true), + chromedp.Flag("disable-infobars", true), + chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"), + // 使用随机 User-Agent + chromedp.UserAgent(profile.UserAgent), + // 使用随机窗口大小 + chromedp.WindowSize(profile.Width, profile.Height), + // 随机语言 + chromedp.Flag("accept-lang", profile.AcceptLang), ) if proxyServer != "" { @@ -99,9 +112,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool } }) + // 获取反检测脚本 + antiDetectionJS := GetAntiDetectionJS(profile) + // 构建运行任务 tasks := []chromedp.Action{ network.Enable(), + // 在每个新文档加载时注入反检测脚本 + chromedp.ActionFunc(func(ctx context.Context) error { + _, err := page.AddScriptToEvaluateOnNewDocument(antiDetectionJS).Do(ctx) + return err + }), chromedp.Navigate(authURL), chromedp.WaitReady("body"), } diff --git a/backend/internal/auth/rod.go b/backend/internal/auth/rod.go index 90c2a45..d120136 100644 --- a/backend/internal/auth/rod.go +++ b/backend/internal/auth/rod.go @@ -16,11 +16,12 @@ import ( // RodAuth 使用 Rod + Stealth 完成 OAuth 授权 type RodAuth struct { - browser *rod.Browser - headless bool - proxy string + browser *rod.Browser + headless bool + proxy string proxyUser string proxyPass string + profile BrowserProfile // 随机浏览器配置 } // getChromiumPath 获取 Chromium 路径 @@ -53,6 +54,9 @@ func getChromiumPath() string { // NewRodAuth 创建 Rod 授权器 func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { + // 获取随机浏览器配置 + profile := GetRandomBrowserProfile() + var proxyServer string var proxyUser string var proxyPass string @@ -82,7 +86,15 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { Set("disable-sync"). Set("disable-translate"). Set("metrics-recording-only"). - Set("no-first-run") + 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 != "" { @@ -104,11 +116,12 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { } return &RodAuth{ - browser: browser, - headless: headless, - proxy: proxy, + browser: browser, + headless: headless, + proxy: proxy, proxyUser: proxyUser, proxyPass: proxyPass, + profile: profile, }, nil } @@ -161,6 +174,23 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string } 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, + }) + // 增加超时时间到 90 秒 page = page.Timeout(90 * time.Second) diff --git a/backend/internal/client/fingerprints.go b/backend/internal/client/fingerprints.go new file mode 100644 index 0000000..bd41164 --- /dev/null +++ b/backend/internal/client/fingerprints.go @@ -0,0 +1,161 @@ +package client + +import ( + "math/rand" + + "github.com/bogdanfinn/tls-client/profiles" +) + +// ============================================================ +// 浏览器指纹配置文件 +// 整合 tls-client 高成功率指纹 +// ============================================================ + +// BrowserFingerprint 浏览器指纹完整配置 +type BrowserFingerprint struct { + Browser string + Version string + Platform string + Mobile bool + TLSProfile profiles.ClientProfile +} + +// ============================================================ +// Firefox 指纹池 (100% 成功率) +// ============================================================ + +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}, +} + +// ============================================================ +// Safari 指纹池 (100% 成功率) +// ============================================================ + +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}, + // 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}, + // iPadOS Safari + {Browser: "safari", Version: "15.6", Platform: "iPadOS", Mobile: true, TLSProfile: profiles.Safari_Ipad_15_6}, +} + +// ============================================================ +// Opera 指纹池 (高成功率) +// ============================================================ + +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}, +} + +// ============================================================ +// OkHttp 指纹池 (100% 成功率 - Android 原生) +// ============================================================ + +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}, +} + +// ============================================================ +// Chrome 指纹池 (测试通过的版本) +// 注意: 新版 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}, +} + +// ============================================================ +// 合并所有指纹 +// ============================================================ + +var allFingerprints []BrowserFingerprint + +func init() { + allFingerprints = make([]BrowserFingerprint, 0, 100) + + // Firefox (高优先级,多份) + allFingerprints = append(allFingerprints, firefoxFingerprints...) + allFingerprints = append(allFingerprints, firefoxFingerprints...) + + // Safari (高成功率) + allFingerprints = append(allFingerprints, safariFingerprints...) + + // Opera + allFingerprints = append(allFingerprints, operaFingerprints...) + + // OkHttp (Android) + allFingerprints = append(allFingerprints, okhttpFingerprints...) + + // Chrome (低优先级) + allFingerprints = append(allFingerprints, chromeFingerprints...) +} + +// GetRandomFingerprint 获取随机指纹 +func GetRandomFingerprint() BrowserFingerprint { + return allFingerprints[rand.Intn(len(allFingerprints))] +} + +// GetRandomDesktopFingerprint 获取随机桌面端指纹 +func GetRandomDesktopFingerprint() BrowserFingerprint { + desktopFps := make([]BrowserFingerprint, 0) + for _, fp := range allFingerprints { + if !fp.Mobile { + desktopFps = append(desktopFps, fp) + } + } + if len(desktopFps) == 0 { + return allFingerprints[rand.Intn(len(allFingerprints))] + } + return desktopFps[rand.Intn(len(desktopFps))] +} + +// GetTotalFingerprintCount 获取总指纹数量 +func GetTotalFingerprintCount() int { + return len(allFingerprints) +} + +// GetUniqueFingerprintCount 获取去重后的指纹数量 +func GetUniqueFingerprintCount() int { + unique := make(map[string]bool) + for _, fp := range allFingerprints { + key := fp.Browser + fp.Version + fp.Platform + unique[key] = true + } + return len(unique) +} diff --git a/backend/internal/client/tls.go b/backend/internal/client/tls.go index 34eb434..edd0b94 100644 --- a/backend/internal/client/tls.go +++ b/backend/internal/client/tls.go @@ -3,6 +3,7 @@ package client import ( "bytes" "compress/gzip" + "fmt" "io" "math/rand" "net/http" @@ -14,35 +15,46 @@ import ( "github.com/andybalholm/brotli" http2 "github.com/bogdanfinn/fhttp" tls_client "github.com/bogdanfinn/tls-client" - "github.com/bogdanfinn/tls-client/profiles" ) // TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端 type TLSClient struct { - client tls_client.HttpClient - userAgent string - chromeVer string - acceptLang string + client tls_client.HttpClient + fingerprint BrowserFingerprint + userAgent string + acceptLang string } // 语言偏好池 var languagePrefs = []string{ "en-US,en;q=0.9", "en-GB,en;q=0.9,en-US;q=0.8", + "en-US,en;q=0.9,de;q=0.8", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", + "en-US,en;q=0.9,fr;q=0.8", + "en-US,en;q=0.9,es;q=0.8", + "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", + "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() + return NewWithFingerprint(fp, proxyStr) +} + +// NewWithFingerprint 使用指定指纹创建客户端 +func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, error) { jar := tls_client.NewCookieJar() - chromeVer := "133" options := []tls_client.HttpClientOption{ - tls_client.WithTimeoutSeconds(60), - tls_client.WithClientProfile(profiles.Chrome_133), + tls_client.WithTimeoutSeconds(90), + tls_client.WithClientProfile(fp.TLSProfile), tls_client.WithRandomTLSExtensionOrder(), tls_client.WithCookieJar(jar), tls_client.WithInsecureSkipVerify(), + tls_client.WithNotFollowRedirects(), } if proxyStr != "" { @@ -59,46 +71,162 @@ func New(proxyStr string) (*TLSClient, error) { } acceptLang := languagePrefs[rand.Intn(len(languagePrefs))] - userAgent := generateUserAgent(chromeVer) + userAgent := generateUserAgent(fp) return &TLSClient{ - client: client, - userAgent: userAgent, - chromeVer: chromeVer, - acceptLang: acceptLang, + client: client, + fingerprint: fp, + userAgent: userAgent, + acceptLang: acceptLang, }, nil } -// generateUserAgent 生成随机化的 User-Agent -func generateUserAgent(chromeVer string) string { - winVersions := []string{ - "Windows NT 10.0; Win64; x64", - "Windows NT 10.0; WOW64", - } - winVer := winVersions[rand.Intn(len(winVersions))] +// generateUserAgent 根据指纹生成 User-Agent +func generateUserAgent(fp BrowserFingerprint) string { + winVersions := []string{"Windows NT 10.0; Win64; x64", "Windows NT 11.0; Win64; x64"} + macVersions := []string{"10_15_7", "11_0_0", "12_0_0", "13_0_0", "14_0", "14_5", "15_0", "15_2"} + linuxVersions := []string{"X11; Linux x86_64", "X11; Ubuntu; Linux x86_64", "X11; Fedora; Linux x86_64"} - return "Mozilla/5.0 (" + winVer + ") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVer + ".0.0.0 Safari/537.36" + version := fp.Version + + switch fp.Browser { + case "chrome": + switch fp.Platform { + case "Windows": + return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", + winVersions[rand.Intn(len(winVersions))], version) + case "macOS": + return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", + macVersions[rand.Intn(len(macVersions))], version) + case "Linux": + return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", + linuxVersions[rand.Intn(len(linuxVersions))], version) + case "Android": + return fmt.Sprintf("Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36", version) + } + + case "firefox": + switch fp.Platform { + case "Windows": + return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0", + winVersions[rand.Intn(len(winVersions))], version, version) + case "macOS": + return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s; rv:%s.0) Gecko/20100101 Firefox/%s.0", + macVersions[rand.Intn(len(macVersions))], version, version) + case "Linux": + return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0", + linuxVersions[rand.Intn(len(linuxVersions))], version, version) + } + + case "safari": + if fp.Mobile { + iosVersions := map[string]string{"18.5": "18_5", "18.0": "18_0", "17.0": "17_0", "16.0": "16_0", "15.6": "15_6", "15.5": "15_5"} + iosVer := iosVersions[version] + if iosVer == "" { + iosVer = "18_0" + } + if fp.Platform == "iPadOS" { + return fmt.Sprintf("Mozilla/5.0 (iPad; CPU OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version) + } + return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version) + } + return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Safari/605.1.15", + macVersions[rand.Intn(len(macVersions))], version) + + case "opera": + chromeVer := map[string]string{"91": "118", "90": "117", "89": "116"}[version] + if chromeVer == "" { + chromeVer = "118" + } + return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36 OPR/%s.0.0.0", + winVersions[rand.Intn(len(winVersions))], chromeVer, version) + + case "okhttp": + 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 { - secChUa := `"Chromium";v="` + c.chromeVer + `", "Not(A:Brand";v="99", "Google Chrome";v="` + c.chromeVer + `"` - - return map[string]string{ - "User-Agent": c.userAgent, - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Accept-Language": c.acceptLang, - "Accept-Encoding": "gzip, deflate, br, zstd", - "Cache-Control": "max-age=0", - "Sec-Ch-Ua": secChUa, - "Sec-Ch-Ua-Mobile": "?0", - "Sec-Ch-Ua-Platform": `"Windows"`, - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "none", - "Sec-Fetch-User": "?1", - "Upgrade-Insecure-Requests": "1", + headers := map[string]string{ + "User-Agent": c.userAgent, + "Accept-Language": c.acceptLang, + "Accept-Encoding": "gzip, deflate, br", } + + fp := c.fingerprint + switch fp.Browser { + case "firefox": + headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + headers["Upgrade-Insecure-Requests"] = "1" + headers["Sec-Fetch-Dest"] = "document" + headers["Sec-Fetch-Mode"] = "navigate" + headers["Sec-Fetch-Site"] = "none" + headers["Sec-Fetch-User"] = "?1" + headers["DNT"] = "1" + + case "safari": + headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + + case "okhttp": + headers["Accept"] = "*/*" + headers["Accept-Encoding"] = "gzip" + + default: // chrome, opera + secChUa := c.generateSecChUa() + headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + headers["Cache-Control"] = "max-age=0" + headers["Sec-Ch-Ua"] = secChUa + headers["Sec-Ch-Ua-Mobile"] = "?0" + if fp.Mobile { + headers["Sec-Ch-Ua-Mobile"] = "?1" + } + headers["Sec-Ch-Ua-Platform"] = c.getPlatformHeader() + headers["Sec-Fetch-Dest"] = "document" + headers["Sec-Fetch-Mode"] = "navigate" + headers["Sec-Fetch-Site"] = "none" + headers["Sec-Fetch-User"] = "?1" + headers["Upgrade-Insecure-Requests"] = "1" + } + + return headers +} + +// generateSecChUa 生成 Sec-Ch-Ua 头 +func (c *TLSClient) generateSecChUa() string { + ver := c.fingerprint.Version + switch c.fingerprint.Browser { + case "opera": + return fmt.Sprintf(`"Opera";v="%s", "Chromium";v="118", "Not(A:Brand";v="99"`, ver) + default: + notABrands := []string{`"Not(A:Brand";v="99"`, `"Not A(Brand";v="99"`, `"Not/A)Brand";v="99"`} + return fmt.Sprintf(`"Chromium";v="%s", %s, "Google Chrome";v="%s"`, ver, notABrands[rand.Intn(len(notABrands))], ver) + } +} + +// getPlatformHeader 获取平台头 +func (c *TLSClient) getPlatformHeader() string { + switch c.fingerprint.Platform { + case "macOS": + return `"macOS"` + case "Linux": + return `"Linux"` + case "iOS", "iPadOS": + return `"iOS"` + case "Android": + return `"Android"` + default: + return `"Windows"` + } +} + +// GetFingerprintInfo 获取指纹信息字符串(用于日志输出) +func (c *TLSClient) GetFingerprintInfo() string { + fp := c.fingerprint + return fmt.Sprintf("%s/%s (%s)", fp.Browser, fp.Version, fp.Platform) } // Do 执行 HTTP 请求 diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go index f7ad706..f9d11ec 100644 --- a/backend/internal/logger/logger.go +++ b/backend/internal/logger/logger.go @@ -243,6 +243,43 @@ func ClearLogs() { logs = make([]LogEntry, 0, 1000) } +// GetLogsByModuleAndLevel 按模块和级别筛选日志并分页(最新的在前) +func GetLogsByModuleAndLevel(module, level string, page, pageSize int) ([]LogEntry, int) { + logsMu.RLock() + defer logsMu.RUnlock() + + // 倒序收集匹配的日志 + var filtered []LogEntry + for i := len(logs) - 1; i >= 0; i-- { + if logs[i].Module == module { + // 如果指定了级别,则进行过滤 + if level != "" && logs[i].Level != level { + continue + } + filtered = append(filtered, logs[i]) + } + } + + total := len(filtered) + if page < 1 { + page = 1 + } + if pageSize <= 0 { + pageSize = 5 + } + + start := (page - 1) * pageSize + if start >= total { + return []LogEntry{}, total + } + end := start + pageSize + if end > total { + end = total + } + + return filtered[start:end], total +} + // ClearLogsByModule 按模块清除日志 func ClearLogsByModule(module string) int { logsMu.Lock() diff --git a/backend/team-reg b/backend/team-reg index a0cd0bd..b563771 100644 Binary files a/backend/team-reg and b/backend/team-reg differ diff --git a/frontend/src/pages/Cleaner.tsx b/frontend/src/pages/Cleaner.tsx index b32bc4e..9ca7314 100644 --- a/frontend/src/pages/Cleaner.tsx +++ b/frontend/src/pages/Cleaner.tsx @@ -52,7 +52,7 @@ export default function Cleaner() { } } - // 加载清理日志 + // 加载清理日志(仅显示 SUCCESS 级别) const fetchCleanerLogs = useCallback(async (page = 1) => { setLogLoading(true) try { @@ -60,6 +60,7 @@ export default function Cleaner() { module: 'cleaner', page: String(page), page_size: String(logPageSize), + level: 'success', // 只显示 SUCCESS 日志 }) const res = await fetch(`/api/logs/query?${params}`) const data = await res.json() @@ -176,21 +177,6 @@ export default function Cleaner() { } } - // 日志级别样式 - const levelColors: Record = { - success: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20', - error: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20', - warning: 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20', - info: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20', - } - - const levelLabels: Record = { - success: 'SUCCESS', - error: 'ERROR', - warning: 'WARN', - info: 'INFO', - } - if (loading) { return (
@@ -389,8 +375,8 @@ export default function Cleaner() { - - 清理日志 + + 清理记录
@@ -440,13 +426,14 @@ export default function Cleaner() { ) : logEntries.length === 0 ? (
-

暂无清理日志

+

暂无清理记录

) : (
{logEntries.map((log, i) => ( -
- +
+ + {new Date(log.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', @@ -455,9 +442,6 @@ export default function Cleaner() { second: '2-digit' })} - - {levelLabels[log.level] || log.level?.toUpperCase()} - {log.message}