feat: Implement browser-based OAuth authentication using Chromedp and Rod, add an upload page, and introduce team processing API.
This commit is contained in:
@@ -32,13 +32,14 @@ type TeamProcessRequest struct {
|
|||||||
// Owner 账号列表
|
// Owner 账号列表
|
||||||
Owners []TeamOwner `json:"owners"`
|
Owners []TeamOwner `json:"owners"`
|
||||||
// 配置
|
// 配置
|
||||||
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
||||||
ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量
|
ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量
|
||||||
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
ConcurrentS2A int `json:"concurrent_s2a"` // 入库并发数(默认2)
|
||||||
Headless bool `json:"headless"` // 是否无头模式
|
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
||||||
Proxy string `json:"proxy"` // 代理设置
|
Headless bool `json:"headless"` // 是否无头模式
|
||||||
IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A
|
Proxy string `json:"proxy"` // 代理设置
|
||||||
ProcessCount int `json:"process_count"` // 处理数量,0表示全部
|
IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A
|
||||||
|
ProcessCount int `json:"process_count"` // 处理数量,0表示全部
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamProcessResult 团队处理结果
|
// TeamProcessResult 团队处理结果
|
||||||
@@ -125,6 +126,12 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.BrowserType == "" {
|
if req.BrowserType == "" {
|
||||||
req.BrowserType = "chromedp" // 默认使用 Chromedp
|
req.BrowserType = "chromedp" // 默认使用 Chromedp
|
||||||
}
|
}
|
||||||
|
if req.ConcurrentS2A <= 0 {
|
||||||
|
req.ConcurrentS2A = 2 // 默认入库并发数为 2
|
||||||
|
}
|
||||||
|
if req.ConcurrentS2A > 4 {
|
||||||
|
req.ConcurrentS2A = 4 // 最大入库并发数为 4(避免浏览器资源耗尽)
|
||||||
|
}
|
||||||
if req.Proxy == "" && config.Global != nil {
|
if req.Proxy == "" && config.Global != nil {
|
||||||
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
||||||
}
|
}
|
||||||
@@ -549,6 +556,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
registerMember := func(memberIdx int, email, password string) bool {
|
registerMember := func(memberIdx int, email, password string) bool {
|
||||||
name := register.GenerateName()
|
name := register.GenerateName()
|
||||||
birthdate := register.GenerateBirthdate()
|
birthdate := register.GenerateBirthdate()
|
||||||
|
memberLogPrefix := fmt.Sprintf("%s [成员 %d]", logPrefix, memberIdx+1)
|
||||||
|
regStartTime := time.Now()
|
||||||
|
|
||||||
for attempt := 0; attempt < 2; attempt++ { // 最多尝试2次(首次+1次重试)
|
for attempt := 0; attempt < 2; attempt++ { // 最多尝试2次(首次+1次重试)
|
||||||
// 检查是否应该停止
|
// 检查是否应该停止
|
||||||
@@ -562,13 +571,14 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
// 重试时使用新邮箱
|
// 重试时使用新邮箱
|
||||||
currentEmail = mail.GenerateEmail()
|
currentEmail = mail.GenerateEmail()
|
||||||
currentPassword = register.GeneratePassword()
|
currentPassword = register.GeneratePassword()
|
||||||
logger.Warning(fmt.Sprintf("%s [成员 %d] 重试, 新邮箱: %s", logPrefix, memberIdx+1, currentEmail), currentEmail, "team")
|
logger.Warning(fmt.Sprintf("%s 重试 (第%d次), 新邮箱: %s", memberLogPrefix, attempt+1, currentEmail), currentEmail, "team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送邀请
|
// 发送邀请
|
||||||
|
logger.Info(fmt.Sprintf("%s [发送邀请] %s", memberLogPrefix, currentEmail), currentEmail, "team")
|
||||||
if err := inviter.SendInvites([]string{currentEmail}); err != nil {
|
if err := inviter.SendInvites([]string{currentEmail}); err != nil {
|
||||||
errStr := err.Error()
|
errStr := err.Error()
|
||||||
logger.Error(fmt.Sprintf("%s [成员 %d] 邀请失败: %v", logPrefix, memberIdx+1, err), currentEmail, "team")
|
logger.Error(fmt.Sprintf("%s [邀请失败] %v", memberLogPrefix, err), currentEmail, "team")
|
||||||
|
|
||||||
// 检测 Team 已达邀请上限(401 或 maximum number of seats)
|
// 检测 Team 已达邀请上限(401 或 maximum number of seats)
|
||||||
if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") {
|
if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") {
|
||||||
@@ -577,31 +587,37 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s [邀请成功]", memberLogPrefix), currentEmail, "team")
|
||||||
|
|
||||||
// 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记)
|
// 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记)
|
||||||
if isTeamExhausted() {
|
if isTeamExhausted() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建注册日志记录器
|
||||||
|
regLogger := NewRegisterLogger(memberLogPrefix, currentEmail)
|
||||||
|
|
||||||
// 注册
|
// 注册
|
||||||
_, err := registerWithTimeout(currentEmail, currentPassword, name, birthdate, req.Proxy)
|
_, err := registerWithTimeoutLogged(currentEmail, currentPassword, name, birthdate, req.Proxy, regLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("%s [成员 %d] 注册失败: %v", logPrefix, memberIdx+1, err), currentEmail, "team")
|
logger.Error(fmt.Sprintf("%s [注册失败] %v", memberLogPrefix, err), currentEmail, "team")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功
|
// 成功
|
||||||
|
regDuration := time.Since(regStartTime)
|
||||||
memberMu.Lock()
|
memberMu.Lock()
|
||||||
children[memberIdx] = MemberAccount{Email: currentEmail, Password: currentPassword, Success: true}
|
children[memberIdx] = MemberAccount{Email: currentEmail, Password: currentPassword, Success: true}
|
||||||
memberMu.Unlock()
|
memberMu.Unlock()
|
||||||
logger.Success(fmt.Sprintf("%s [成员 %d] ✓ 注册成功", logPrefix, memberIdx+1), currentEmail, "team")
|
logger.Success(fmt.Sprintf("%s ✓ 注册成功 (耗时: %.1fs)", memberLogPrefix, regDuration.Seconds()), currentEmail, "team")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一轮:并发注册4个成员
|
// 第一轮:并发注册4个成员
|
||||||
logger.Info(fmt.Sprintf("%s 开始并发注册 %d 个成员", logPrefix, req.MembersPerTeam), owner.Email, "team")
|
logger.Info(fmt.Sprintf("%s ════════ 开始注册阶段 ════════ 目标: %d 个成员", logPrefix, req.MembersPerTeam), owner.Email, "team")
|
||||||
|
regPhaseStartTime := time.Now()
|
||||||
for i := 0; i < req.MembersPerTeam; i++ {
|
for i := 0; i < req.MembersPerTeam; i++ {
|
||||||
memberWg.Add(1)
|
memberWg.Add(1)
|
||||||
go func(idx int) {
|
go func(idx int) {
|
||||||
@@ -666,7 +682,8 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
if len(failedSlots) > 0 {
|
if len(failedSlots) > 0 {
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("%d 个成员注册失败", len(failedSlots)))
|
result.Errors = append(result.Errors, fmt.Sprintf("%d 个成员注册失败", len(failedSlots)))
|
||||||
}
|
}
|
||||||
logger.Info(fmt.Sprintf("%s 注册完成: %d/%d 成功", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team")
|
regPhaseDuration := time.Since(regPhaseStartTime)
|
||||||
|
logger.Info(fmt.Sprintf("%s ════════ 注册阶段完成 ════════ 成功: %d/%d, 耗时: %.1fs", logPrefix, result.Registered, req.MembersPerTeam, regPhaseDuration.Seconds()), owner.Email, "team")
|
||||||
|
|
||||||
// 如果没有任何成员注册成功,跳过入库步骤
|
// 如果没有任何成员注册成功,跳过入库步骤
|
||||||
if len(registeredChildren) == 0 {
|
if len(registeredChildren) == 0 {
|
||||||
@@ -676,91 +693,182 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: S2A 授权入库(成员)- 带重试
|
// Step 4: S2A 授权入库(成员)- 并发入库
|
||||||
|
logger.Info(fmt.Sprintf("%s ════════ 开始入库阶段 ════════ 共 %d 个成员, 并发数: %d", logPrefix, len(registeredChildren), req.ConcurrentS2A), owner.Email, "team")
|
||||||
|
s2aStartTime := time.Now()
|
||||||
|
|
||||||
|
// 入库结果
|
||||||
|
type S2AResult struct {
|
||||||
|
Index int
|
||||||
|
Email string
|
||||||
|
Success bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
s2aResults := make(chan S2AResult, len(registeredChildren))
|
||||||
|
s2aSem := make(chan struct{}, req.ConcurrentS2A) // 并发控制信号量
|
||||||
|
|
||||||
|
var s2aWg sync.WaitGroup
|
||||||
|
|
||||||
for i, child := range registeredChildren {
|
for i, child := range registeredChildren {
|
||||||
if !teamProcessState.Running {
|
if !teamProcessState.Running {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var s2aSuccess bool
|
s2aWg.Add(1)
|
||||||
for attempt := 0; attempt < 2; attempt++ { // 最多重试1次
|
go func(memberIdx int, memberChild MemberAccount) {
|
||||||
if attempt > 0 {
|
defer s2aWg.Done()
|
||||||
logger.Warning(fmt.Sprintf("%s [成员 %d] 入库重试...", logPrefix, i+1), child.Email, "team")
|
|
||||||
|
// 获取信号量
|
||||||
|
s2aSem <- struct{}{}
|
||||||
|
defer func() { <-s2aSem }()
|
||||||
|
|
||||||
|
memberStartTime := time.Now()
|
||||||
|
memberLogPrefix := fmt.Sprintf("%s [成员 %d]", logPrefix, memberIdx+1)
|
||||||
|
|
||||||
|
logger.Info(fmt.Sprintf("%s 开始入库 | 邮箱: %s", memberLogPrefix, memberChild.Email), memberChild.Email, "team")
|
||||||
|
|
||||||
|
var s2aSuccess bool
|
||||||
|
var lastError string
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 2; attempt++ { // 最多重试1次
|
||||||
|
if attempt > 0 {
|
||||||
|
logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", memberLogPrefix, attempt+1), memberChild.Email, "team")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志回调
|
||||||
|
authLogger := auth.NewAuthLogger(memberChild.Email, logPrefix, memberIdx+1, func(entry auth.AuthLogEntry) {
|
||||||
|
stepName := auth.StepName(entry.Step)
|
||||||
|
if entry.IsError {
|
||||||
|
logger.Error(fmt.Sprintf("%s [%s] %s (%.1fs)", memberLogPrefix, stepName, entry.Message, entry.Duration.Seconds()), memberChild.Email, "team")
|
||||||
|
} else {
|
||||||
|
logger.Info(fmt.Sprintf("%s [%s] %s", memberLogPrefix, stepName, entry.Message), memberChild.Email, "team")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取授权 URL
|
||||||
|
logger.Info(fmt.Sprintf("%s 获取 S2A 授权 URL...", memberLogPrefix), memberChild.Email, "team")
|
||||||
|
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Sprintf("获取授权URL失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, lastError), memberChild.Email, "team")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s 授权 URL 获取成功, SessionID: %s", memberLogPrefix, s2aResp.Data.SessionID[:8]+"..."), memberChild.Email, "team")
|
||||||
|
|
||||||
|
// 根据配置选择浏览器自动化
|
||||||
|
var code string
|
||||||
|
if req.BrowserType == "rod" {
|
||||||
|
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||||
|
} else {
|
||||||
|
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Sprintf("浏览器授权失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s (耗时: %.1fs)", memberLogPrefix, lastError, authLogger.TotalDuration().Seconds()), memberChild.Email, "team")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s 浏览器授权成功, 授权码: %s... (耗时: %.1fs)", memberLogPrefix, code[:8], authLogger.TotalDuration().Seconds()), memberChild.Email, "team")
|
||||||
|
|
||||||
|
// 提交到 S2A
|
||||||
|
logger.Info(fmt.Sprintf("%s 正在提交到 S2A...", memberLogPrefix), memberChild.Email, "team")
|
||||||
|
_, err = auth.SubmitS2AOAuth(
|
||||||
|
config.Global.S2AApiBase,
|
||||||
|
config.Global.S2AAdminKey,
|
||||||
|
s2aResp.Data.SessionID,
|
||||||
|
code,
|
||||||
|
memberChild.Email,
|
||||||
|
config.Global.Concurrency,
|
||||||
|
config.Global.Priority,
|
||||||
|
config.Global.GroupIDs,
|
||||||
|
config.Global.ProxyID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Sprintf("S2A提交失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, lastError), memberChild.Email, "team")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s2aSuccess = true
|
||||||
|
memberDuration := time.Since(memberStartTime)
|
||||||
|
logger.Success(fmt.Sprintf("%s ✓ 入库成功 (总耗时: %.1fs)", memberLogPrefix, memberDuration.Seconds()), memberChild.Email, "team")
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
s2aResults <- S2AResult{
|
||||||
if err != nil {
|
Index: memberIdx,
|
||||||
logger.Error(fmt.Sprintf("%s [成员 %d] 获取授权URL失败: %v", logPrefix, i+1, err), child.Email, "team")
|
Email: memberChild.Email,
|
||||||
continue
|
Success: s2aSuccess,
|
||||||
|
Error: lastError,
|
||||||
}
|
}
|
||||||
|
}(i, child)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据配置选择浏览器自动化
|
// 等待所有入库完成
|
||||||
var code string
|
go func() {
|
||||||
if req.BrowserType == "rod" {
|
s2aWg.Wait()
|
||||||
code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy)
|
close(s2aResults)
|
||||||
} else {
|
}()
|
||||||
code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("%s [成员 %d] 浏览器授权失败: %v", logPrefix, i+1, err), child.Email, "team")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交到 S2A
|
// 收集入库结果
|
||||||
_, err = auth.SubmitS2AOAuth(
|
for s2aRes := range s2aResults {
|
||||||
config.Global.S2AApiBase,
|
if s2aRes.Success {
|
||||||
config.Global.S2AAdminKey,
|
|
||||||
s2aResp.Data.SessionID,
|
|
||||||
code,
|
|
||||||
child.Email,
|
|
||||||
config.Global.Concurrency,
|
|
||||||
config.Global.Priority,
|
|
||||||
config.Global.GroupIDs,
|
|
||||||
config.Global.ProxyID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("%s [成员 %d] S2A提交失败: %v", logPrefix, i+1, err), child.Email, "team")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s2aSuccess = true
|
|
||||||
result.AddedToS2A++
|
result.AddedToS2A++
|
||||||
logger.Success(fmt.Sprintf("%s [成员 %d] ✓ 入库成功", logPrefix, i+1), child.Email, "team")
|
} else {
|
||||||
break
|
result.Errors = append(result.Errors, fmt.Sprintf("成员 %d 入库失败: %s", s2aRes.Index+1, s2aRes.Error))
|
||||||
}
|
|
||||||
|
|
||||||
if !s2aSuccess {
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("成员 %d 入库失败", i+1))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s2aDuration := time.Since(s2aStartTime)
|
||||||
|
logger.Info(fmt.Sprintf("%s ════════ 入库阶段完成 ════════ 成功: %d/%d, 耗时: %.1fs", logPrefix, result.AddedToS2A, len(registeredChildren), s2aDuration.Seconds()), owner.Email, "team")
|
||||||
|
|
||||||
// Step 5: 母号也入库(如果开启)- 带重试
|
// Step 5: 母号也入库(如果开启)- 带重试
|
||||||
if req.IncludeOwner && teamProcessState.Running {
|
if req.IncludeOwner && teamProcessState.Running {
|
||||||
logger.Info(fmt.Sprintf("%s 开始将母号入库到 S2A", logPrefix), owner.Email, "team")
|
ownerLogPrefix := fmt.Sprintf("%s [母号 ]", logPrefix)
|
||||||
|
ownerStartTime := time.Now()
|
||||||
|
logger.Info(fmt.Sprintf("%s ════════ 开始母号入库 ════════", logPrefix), owner.Email, "team")
|
||||||
|
logger.Info(fmt.Sprintf("%s 开始入库 | 邮箱: %s", ownerLogPrefix, owner.Email), owner.Email, "team")
|
||||||
|
|
||||||
var ownerSuccess bool
|
var ownerSuccess bool
|
||||||
|
var lastError string
|
||||||
for attempt := 0; attempt < 2; attempt++ { // 最多重试1次
|
for attempt := 0; attempt < 2; attempt++ { // 最多重试1次
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
logger.Warning(fmt.Sprintf("%s [母号] 入库重试...", logPrefix), owner.Email, "team")
|
logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", ownerLogPrefix, attempt+1), owner.Email, "team")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建日志回调
|
||||||
|
authLogger := auth.NewAuthLogger(owner.Email, logPrefix, 0, func(entry auth.AuthLogEntry) {
|
||||||
|
stepName := auth.StepName(entry.Step)
|
||||||
|
if entry.IsError {
|
||||||
|
logger.Error(fmt.Sprintf("%s [%s] %s (%.1fs)", ownerLogPrefix, stepName, entry.Message, entry.Duration.Seconds()), owner.Email, "team")
|
||||||
|
} else {
|
||||||
|
logger.Info(fmt.Sprintf("%s [%s] %s", ownerLogPrefix, stepName, entry.Message), owner.Email, "team")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.Info(fmt.Sprintf("%s 获取 S2A 授权 URL...", ownerLogPrefix), owner.Email, "team")
|
||||||
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("%s [母号] 获取授权URL失败: %v", logPrefix, err), owner.Email, "team")
|
lastError = fmt.Sprintf("获取授权URL失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s", ownerLogPrefix, lastError), owner.Email, "team")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s 授权 URL 获取成功, SessionID: %s", ownerLogPrefix, s2aResp.Data.SessionID[:8]+"..."), owner.Email, "team")
|
||||||
|
|
||||||
var code string
|
var code string
|
||||||
if req.BrowserType == "rod" {
|
if req.BrowserType == "rod" {
|
||||||
code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||||
} else {
|
} else {
|
||||||
code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("%s [母号] 浏览器授权失败: %v", logPrefix, err), owner.Email, "team")
|
lastError = fmt.Sprintf("浏览器授权失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s (耗时: %.1fs)", ownerLogPrefix, lastError, authLogger.TotalDuration().Seconds()), owner.Email, "team")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s 浏览器授权成功, 授权码: %s... (耗时: %.1fs)", ownerLogPrefix, code[:8], authLogger.TotalDuration().Seconds()), owner.Email, "team")
|
||||||
|
|
||||||
|
logger.Info(fmt.Sprintf("%s 正在提交到 S2A...", ownerLogPrefix), owner.Email, "team")
|
||||||
_, err = auth.SubmitS2AOAuth(
|
_, err = auth.SubmitS2AOAuth(
|
||||||
config.Global.S2AApiBase,
|
config.Global.S2AApiBase,
|
||||||
config.Global.S2AAdminKey,
|
config.Global.S2AAdminKey,
|
||||||
@@ -773,19 +881,22 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
config.Global.ProxyID,
|
config.Global.ProxyID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("%s [母号] S2A提交失败: %v", logPrefix, err), owner.Email, "team")
|
lastError = fmt.Sprintf("S2A提交失败: %v", err)
|
||||||
|
logger.Error(fmt.Sprintf("%s %s", ownerLogPrefix, lastError), owner.Email, "team")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerSuccess = true
|
ownerSuccess = true
|
||||||
result.AddedToS2A++
|
result.AddedToS2A++
|
||||||
logger.Success(fmt.Sprintf("%s [母号 ] ✓ 入库成功", logPrefix), owner.Email, "team")
|
ownerDuration := time.Since(ownerStartTime)
|
||||||
|
logger.Success(fmt.Sprintf("%s ✓ 入库成功 (总耗时: %.1fs)", ownerLogPrefix, ownerDuration.Seconds()), owner.Email, "team")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ownerSuccess {
|
if !ownerSuccess {
|
||||||
result.Errors = append(result.Errors, "母号入库失败")
|
result.Errors = append(result.Errors, fmt.Sprintf("母号入库失败: %s", lastError))
|
||||||
}
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s ════════ 母号入库完成 ════════", logPrefix), owner.Email, "team")
|
||||||
}
|
}
|
||||||
|
|
||||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||||
@@ -798,45 +909,97 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterLogger 注册日志记录器
|
||||||
|
type RegisterLogger struct {
|
||||||
|
logPrefix string
|
||||||
|
email string
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegisterLogger 创建注册日志记录器
|
||||||
|
func NewRegisterLogger(logPrefix, email string) *RegisterLogger {
|
||||||
|
return &RegisterLogger{
|
||||||
|
logPrefix: logPrefix,
|
||||||
|
email: email,
|
||||||
|
startTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStep 记录步骤
|
||||||
|
func (l *RegisterLogger) LogStep(step string) {
|
||||||
|
logger.Info(fmt.Sprintf("%s [%s]", l.logPrefix, step), l.email, "team")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStepDone 记录步骤完成
|
||||||
|
func (l *RegisterLogger) LogStepDone(step string, duration time.Duration) {
|
||||||
|
logger.Info(fmt.Sprintf("%s [%s] 完成 (%.1fs)", l.logPrefix, step, duration.Seconds()), l.email, "team")
|
||||||
|
}
|
||||||
|
|
||||||
// registerWithTimeout 带超时的注册
|
// registerWithTimeout 带超时的注册
|
||||||
func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) {
|
func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) {
|
||||||
|
return registerWithTimeoutLogged(email, password, name, birthdate, proxy, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerWithTimeoutLogged 带超时和日志的注册
|
||||||
|
func registerWithTimeoutLogged(email, password, name, birthdate, proxy string, regLogger *RegisterLogger) (*register.ChatGPTReg, error) {
|
||||||
|
logStep := func(step string) {
|
||||||
|
if regLogger != nil {
|
||||||
|
regLogger.LogStep(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reg, err := register.New(proxy)
|
reg, err := register.New(proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("初始化会话")
|
||||||
if err := reg.InitSession(); err != nil {
|
if err := reg.InitSession(); err != nil {
|
||||||
return nil, fmt.Errorf("初始化失败: %v", err)
|
return nil, fmt.Errorf("初始化失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("获取授权URL")
|
||||||
if err := reg.GetAuthorizeURL(email); err != nil {
|
if err := reg.GetAuthorizeURL(email); err != nil {
|
||||||
return nil, fmt.Errorf("获取授权URL失败: %v", err)
|
return nil, fmt.Errorf("获取授权URL失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("启动授权")
|
||||||
if err := reg.StartAuthorize(); err != nil {
|
if err := reg.StartAuthorize(); err != nil {
|
||||||
return nil, fmt.Errorf("启动授权失败: %v", err)
|
return nil, fmt.Errorf("启动授权失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("提交注册信息")
|
||||||
if err := reg.Register(email, password); err != nil {
|
if err := reg.Register(email, password); err != nil {
|
||||||
return nil, fmt.Errorf("注册失败: %v", err)
|
return nil, fmt.Errorf("注册失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("发送验证邮件")
|
||||||
if err := reg.SendVerificationEmail(); err != nil {
|
if err := reg.SendVerificationEmail(); err != nil {
|
||||||
return nil, fmt.Errorf("发送邮件失败: %v", err)
|
return nil, fmt.Errorf("发送邮件失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 短超时获取验证码
|
// 短超时获取验证码
|
||||||
|
logStep("等待验证码 (5s)")
|
||||||
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logStep("等待验证码 (15s)")
|
||||||
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("验证码获取超时")
|
return nil, fmt.Errorf("验证码获取超时")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("验证OTP")
|
||||||
if err := reg.ValidateOTP(otpCode); err != nil {
|
if err := reg.ValidateOTP(otpCode); err != nil {
|
||||||
return nil, fmt.Errorf("OTP验证失败: %v", err)
|
return nil, fmt.Errorf("OTP验证失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("创建账户")
|
||||||
if err := reg.CreateAccount(name, birthdate); err != nil {
|
if err := reg.CreateAccount(name, birthdate); err != nil {
|
||||||
return nil, fmt.Errorf("创建账户失败: %v", err)
|
return nil, fmt.Errorf("创建账户失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep("获取会话令牌")
|
||||||
_ = reg.GetSessionToken()
|
_ = reg.GetSessionToken()
|
||||||
return reg, nil
|
return reg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
122
backend/internal/auth/auth_log.go
Normal file
122
backend/internal/auth/auth_log.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthStep 授权步骤
|
||||||
|
type AuthStep string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StepBrowserStart AuthStep = "browser_start" // 启动浏览器
|
||||||
|
StepNavigate AuthStep = "navigate" // 访问授权页面
|
||||||
|
StepInputEmail AuthStep = "input_email" // 输入邮箱
|
||||||
|
StepSubmitEmail AuthStep = "submit_email" // 提交邮箱
|
||||||
|
StepInputPassword AuthStep = "input_password" // 输入密码
|
||||||
|
StepSubmitPassword AuthStep = "submit_password" // 提交密码
|
||||||
|
StepSelectWorkspace AuthStep = "select_workspace" // 选择工作区
|
||||||
|
StepConsent AuthStep = "consent" // 授权同意
|
||||||
|
StepWaitCallback AuthStep = "wait_callback" // 等待回调
|
||||||
|
StepExtractCode AuthStep = "extract_code" // 提取授权码
|
||||||
|
StepComplete AuthStep = "complete" // 完成授权
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthLogEntry 授权日志条目
|
||||||
|
type AuthLogEntry struct {
|
||||||
|
Step AuthStep
|
||||||
|
Message string
|
||||||
|
Duration time.Duration
|
||||||
|
Timestamp time.Time
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthLogger 授权日志记录器
|
||||||
|
type AuthLogger struct {
|
||||||
|
email string
|
||||||
|
teamPrefix string
|
||||||
|
memberIdx int
|
||||||
|
callback func(entry AuthLogEntry)
|
||||||
|
startTime time.Time
|
||||||
|
stepStart time.Time
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthLogger 创建授权日志记录器
|
||||||
|
func NewAuthLogger(email, teamPrefix string, memberIdx int, callback func(entry AuthLogEntry)) *AuthLogger {
|
||||||
|
return &AuthLogger{
|
||||||
|
email: email,
|
||||||
|
teamPrefix: teamPrefix,
|
||||||
|
memberIdx: memberIdx,
|
||||||
|
callback: callback,
|
||||||
|
startTime: time.Now(),
|
||||||
|
stepStart: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStep 记录步骤
|
||||||
|
func (l *AuthLogger) LogStep(step AuthStep, format string, args ...interface{}) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
entry := AuthLogEntry{
|
||||||
|
Step: step,
|
||||||
|
Message: fmt.Sprintf(format, args...),
|
||||||
|
Duration: now.Sub(l.stepStart),
|
||||||
|
Timestamp: now,
|
||||||
|
IsError: false,
|
||||||
|
}
|
||||||
|
l.stepStart = now
|
||||||
|
|
||||||
|
if l.callback != nil {
|
||||||
|
l.callback(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError 记录错误
|
||||||
|
func (l *AuthLogger) LogError(step AuthStep, format string, args ...interface{}) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
entry := AuthLogEntry{
|
||||||
|
Step: step,
|
||||||
|
Message: fmt.Sprintf(format, args...),
|
||||||
|
Duration: now.Sub(l.stepStart),
|
||||||
|
Timestamp: now,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
l.stepStart = now
|
||||||
|
|
||||||
|
if l.callback != nil {
|
||||||
|
l.callback(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalDuration 获取总耗时
|
||||||
|
func (l *AuthLogger) TotalDuration() time.Duration {
|
||||||
|
return time.Since(l.startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepName 获取步骤中文名称
|
||||||
|
func StepName(step AuthStep) string {
|
||||||
|
names := map[AuthStep]string{
|
||||||
|
StepBrowserStart: "启动浏览器",
|
||||||
|
StepNavigate: "访问授权页",
|
||||||
|
StepInputEmail: "输入邮箱",
|
||||||
|
StepSubmitEmail: "提交邮箱",
|
||||||
|
StepInputPassword: "输入密码",
|
||||||
|
StepSubmitPassword: "提交密码",
|
||||||
|
StepSelectWorkspace: "选择工作区",
|
||||||
|
StepConsent: "授权同意",
|
||||||
|
StepWaitCallback: "等待回调",
|
||||||
|
StepExtractCode: "提取授权码",
|
||||||
|
StepComplete: "完成授权",
|
||||||
|
}
|
||||||
|
if name, ok := names[step]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return string(step)
|
||||||
|
}
|
||||||
@@ -16,6 +16,24 @@ import (
|
|||||||
|
|
||||||
// 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) {
|
||||||
|
return CompleteWithChromedpLogged(authURL, email, password, teamID, headless, proxy, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteWithChromedpLogged 使用 chromedp 完成 S2A OAuth 授权(带日志回调)
|
||||||
|
func CompleteWithChromedpLogged(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, "正在启动 Chromedp 浏览器...")
|
||||||
// 获取随机浏览器配置
|
// 获取随机浏览器配置
|
||||||
profile := GetRandomBrowserProfile()
|
profile := GetRandomBrowserProfile()
|
||||||
|
|
||||||
@@ -132,14 +150,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...)
|
tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepNavigate, "正在访问授权页面...")
|
||||||
err := chromedp.Run(ctx, tasks...)
|
err := chromedp.Run(ctx, tasks...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logError(StepNavigate, "访问失败: %v", err)
|
||||||
return "", fmt.Errorf("访问失败: %v", err)
|
return "", fmt.Errorf("访问失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
if callbackURL != "" {
|
if callbackURL != "" {
|
||||||
|
logStep(StepExtractCode, "已捕获授权码回调")
|
||||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +179,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
`input[name="username"]`,
|
`input[name="username"]`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepInputEmail, "正在查找邮箱输入框...")
|
||||||
var emailFilled bool
|
var emailFilled bool
|
||||||
for _, sel := range emailSelectors {
|
for _, sel := range emailSelectors {
|
||||||
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
|
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
|
||||||
@@ -168,12 +190,14 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
emailFilled = true
|
emailFilled = true
|
||||||
|
logStep(StepInputEmail, "已输入邮箱")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !emailFilled {
|
if !emailFilled {
|
||||||
|
logError(StepInputEmail, "未找到邮箱输入框")
|
||||||
return "", fmt.Errorf("未找到邮箱输入框")
|
return "", fmt.Errorf("未找到邮箱输入框")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +210,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
`input[type="submit"]`,
|
`input[type="submit"]`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepSubmitEmail, "正在提交邮箱...")
|
||||||
for _, sel := range buttonSelectors {
|
for _, sel := range buttonSelectors {
|
||||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -196,14 +221,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
time.Sleep(1500 * time.Millisecond)
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
|
||||||
if callbackURL != "" {
|
if callbackURL != "" {
|
||||||
|
logStep(StepExtractCode, "已捕获授权码回调")
|
||||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||||||
if strings.Contains(currentURL, "code=") {
|
if strings.Contains(currentURL, "code=") {
|
||||||
|
logStep(StepExtractCode, "已获取授权码")
|
||||||
return ExtractCodeFromCallbackURL(currentURL), nil
|
return ExtractCodeFromCallbackURL(currentURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepInputPassword, "正在查找密码输入框...")
|
||||||
passwordSelectors := []string{
|
passwordSelectors := []string{
|
||||||
`input[name="current-password"]`,
|
`input[name="current-password"]`,
|
||||||
`input[name="password"]`,
|
`input[name="password"]`,
|
||||||
@@ -220,17 +248,20 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
passwordFilled = true
|
passwordFilled = true
|
||||||
|
logStep(StepInputPassword, "已输入密码")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !passwordFilled {
|
if !passwordFilled {
|
||||||
|
logError(StepInputPassword, "未找到密码输入框")
|
||||||
return "", fmt.Errorf("未找到密码输入框")
|
return "", fmt.Errorf("未找到密码输入框")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
logStep(StepSubmitPassword, "正在提交密码...")
|
||||||
for _, sel := range buttonSelectors {
|
for _, sel := range buttonSelectors {
|
||||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -238,6 +269,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepWaitCallback, "等待授权回调...")
|
||||||
for i := 0; i < 30; i++ {
|
for i := 0; i < 30; i++ {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
@@ -248,10 +280,12 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
var url string
|
var url string
|
||||||
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
|
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
|
||||||
if strings.Contains(url, "code=") {
|
if strings.Contains(url, "code=") {
|
||||||
|
logStep(StepExtractCode, "已获取授权码")
|
||||||
return ExtractCodeFromCallbackURL(url), nil
|
return ExtractCodeFromCallbackURL(url), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(url, "consent") {
|
if strings.Contains(url, "consent") {
|
||||||
|
logStep(StepConsent, "正在处理授权同意页面...")
|
||||||
for _, sel := range buttonSelectors {
|
for _, sel := range buttonSelectors {
|
||||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -262,6 +296,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(url, "authorize") && teamID != "" {
|
if strings.Contains(url, "authorize") && teamID != "" {
|
||||||
|
logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID)
|
||||||
err = chromedp.Run(ctx,
|
err = chromedp.Run(ctx,
|
||||||
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
|
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
|
||||||
)
|
)
|
||||||
@@ -270,8 +305,10 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
if callbackURL != "" {
|
if callbackURL != "" {
|
||||||
|
logStep(StepComplete, "授权完成")
|
||||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logError(StepWaitCallback, "授权超时")
|
||||||
return "", fmt.Errorf("授权超时")
|
return "", fmt.Errorf("授权超时")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,23 @@ 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) {
|
||||||
|
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.
|
// Handle proxy auth (407) in headless mode.
|
||||||
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
|
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
|
||||||
// 只在代理需要认证时才启用 Fetch 域
|
// 只在代理需要认证时才启用 Fetch 域
|
||||||
@@ -194,24 +211,31 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
|||||||
// 增加超时时间到 90 秒
|
// 增加超时时间到 90 秒
|
||||||
page = page.Timeout(90 * time.Second)
|
page = page.Timeout(90 * time.Second)
|
||||||
|
|
||||||
|
logStep(StepNavigate, "正在访问授权页面...")
|
||||||
if err := page.Navigate(authURL); err != nil {
|
if err := page.Navigate(authURL); err != nil {
|
||||||
|
logError(StepNavigate, "访问失败: %v", err)
|
||||||
return "", fmt.Errorf("访问授权URL失败: %v", err)
|
return "", fmt.Errorf("访问授权URL失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
page.MustWaitDOMStable()
|
page.MustWaitDOMStable()
|
||||||
|
|
||||||
if code := r.checkForCode(page); code != "" {
|
if code := r.checkForCode(page); code != "" {
|
||||||
|
logStep(StepExtractCode, "已捕获授权码回调")
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepInputEmail, "正在查找邮箱输入框...")
|
||||||
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
|
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logError(StepInputEmail, "未找到邮箱输入框")
|
||||||
return "", fmt.Errorf("未找到邮箱输入框")
|
return "", fmt.Errorf("未找到邮箱输入框")
|
||||||
}
|
}
|
||||||
|
|
||||||
emailInput.MustSelectAllText().MustInput(email)
|
emailInput.MustSelectAllText().MustInput(email)
|
||||||
|
logStep(StepInputEmail, "已输入邮箱")
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
logStep(StepSubmitEmail, "正在提交邮箱...")
|
||||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||||
btn.MustClick()
|
btn.MustClick()
|
||||||
}
|
}
|
||||||
@@ -219,25 +243,32 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
|||||||
time.Sleep(1500 * time.Millisecond)
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
|
||||||
if code := r.checkForCode(page); code != "" {
|
if code := r.checkForCode(page); code != "" {
|
||||||
|
logStep(StepExtractCode, "已获取授权码")
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepInputPassword, "正在查找密码输入框...")
|
||||||
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
|
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logError(StepInputPassword, "未找到密码输入框")
|
||||||
return "", fmt.Errorf("未找到密码输入框")
|
return "", fmt.Errorf("未找到密码输入框")
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordInput.MustSelectAllText().MustInput(password)
|
passwordInput.MustSelectAllText().MustInput(password)
|
||||||
|
logStep(StepInputPassword, "已输入密码")
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
logStep(StepSubmitPassword, "正在提交密码...")
|
||||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||||
btn.MustClick()
|
btn.MustClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logStep(StepWaitCallback, "等待授权回调...")
|
||||||
for i := 0; i < 66; i++ {
|
for i := 0; i < 66; i++ {
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
if code := r.checkForCode(page); code != "" {
|
if code := r.checkForCode(page); code != "" {
|
||||||
|
logStep(StepComplete, "授权完成")
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,12 +276,14 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
|||||||
currentURL := info.URL
|
currentURL := info.URL
|
||||||
|
|
||||||
if strings.Contains(currentURL, "consent") {
|
if strings.Contains(currentURL, "consent") {
|
||||||
|
logStep(StepConsent, "正在处理授权同意页面...")
|
||||||
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
|
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
|
||||||
btn.Click(proto.InputMouseButtonLeft, 1)
|
btn.Click(proto.InputMouseButtonLeft, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(currentURL, "authorize") && teamID != "" {
|
if strings.Contains(currentURL, "authorize") && teamID != "" {
|
||||||
|
logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID)
|
||||||
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
|
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
|
||||||
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
|
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
|
||||||
wsBtn.Click(proto.InputMouseButtonLeft, 1)
|
wsBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||||
@@ -258,6 +291,7 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logError(StepWaitCallback, "授权超时")
|
||||||
return "", fmt.Errorf("授权超时")
|
return "", fmt.Errorf("授权超时")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,13 +309,33 @@ func (r *RodAuth) checkForCode(page *rod.Page) string {
|
|||||||
|
|
||||||
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
|
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
|
||||||
func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
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)
|
auth, err := NewRodAuth(headless, proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logError(StepBrowserStart, "启动失败: %v", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer auth.Close()
|
defer auth.Close()
|
||||||
|
|
||||||
return auth.CompleteOAuth(authURL, email, password, teamID)
|
logStep(StepBrowserStart, "浏览器启动成功")
|
||||||
|
return auth.CompleteOAuthLogged(authURL, email, password, teamID, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
|
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default function Upload() {
|
|||||||
// 配置
|
// 配置
|
||||||
const [membersPerTeam, setMembersPerTeam] = useState(4)
|
const [membersPerTeam, setMembersPerTeam] = useState(4)
|
||||||
const [concurrentTeams, setConcurrentTeams] = useState(2)
|
const [concurrentTeams, setConcurrentTeams] = useState(2)
|
||||||
|
const [concurrentS2A, setConcurrentS2A] = useState(2) // 入库并发数
|
||||||
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
|
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
|
||||||
const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
|
const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
|
||||||
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
|
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
|
||||||
@@ -192,6 +193,7 @@ export default function Upload() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
members_per_team: membersPerTeam,
|
members_per_team: membersPerTeam,
|
||||||
concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1),
|
concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1),
|
||||||
|
concurrent_s2a: concurrentS2A, // 入库并发数
|
||||||
browser_type: browserType,
|
browser_type: browserType,
|
||||||
headless: true, // 始终使用无头模式
|
headless: true, // 始终使用无头模式
|
||||||
proxy: useProxy ? globalProxy : '',
|
proxy: useProxy ? globalProxy : '',
|
||||||
@@ -212,7 +214,7 @@ export default function Upload() {
|
|||||||
alert('启动失败')
|
alert('启动失败')
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [stats, membersPerTeam, concurrentTeams, browserType, useProxy, globalProxy, includeOwner, processCount, fetchStatus])
|
}, [stats, membersPerTeam, concurrentTeams, concurrentS2A, browserType, useProxy, globalProxy, includeOwner, processCount, fetchStatus])
|
||||||
|
|
||||||
// 停止处理
|
// 停止处理
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
@@ -422,9 +424,9 @@ export default function Upload() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<Input
|
<Input
|
||||||
label="每个 Team 成员数"
|
label="每 Team 成员数"
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={10}
|
max={10}
|
||||||
@@ -440,7 +442,17 @@ export default function Upload() {
|
|||||||
value={concurrentTeams}
|
value={concurrentTeams}
|
||||||
onChange={(e) => setConcurrentTeams(Number(e.target.value))}
|
onChange={(e) => setConcurrentTeams(Number(e.target.value))}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
hint={`最多 ${stats?.valid || 0} 个`}
|
hint={`最多 ${stats?.valid || 0}`}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="入库并发数"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={4}
|
||||||
|
value={concurrentS2A}
|
||||||
|
onChange={(e) => setConcurrentS2A(Math.min(4, Math.max(1, Number(e.target.value))))}
|
||||||
|
disabled={isRunning}
|
||||||
|
hint="1-4,推荐2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user