feat: Implement batch team owner processing with file upload, configuration, and real-time status monitoring.
This commit is contained in:
@@ -71,25 +71,50 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
logger.Error("保存监控设置失败: 数据库未初始化", "", "monitor")
|
||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
var settings MonitorSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
logger.Error("保存监控设置失败: 解析请求失败 - "+err.Error(), "", "monitor")
|
||||
Error(w, http.StatusBadRequest, "解析请求失败")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("收到保存监控设置请求: target="+strconv.Itoa(settings.Target)+
|
||||
", auto_add="+strconv.FormatBool(settings.AutoAdd)+
|
||||
", polling="+strconv.FormatBool(settings.PollingEnabled), "", "monitor")
|
||||
|
||||
// 保存到数据库
|
||||
database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target))
|
||||
database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd))
|
||||
database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval))
|
||||
database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled))
|
||||
database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval))
|
||||
var saveErrors []string
|
||||
if err := database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target)); err != nil {
|
||||
saveErrors = append(saveErrors, "target: "+err.Error())
|
||||
}
|
||||
if err := database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd)); err != nil {
|
||||
saveErrors = append(saveErrors, "auto_add: "+err.Error())
|
||||
}
|
||||
if err := database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval)); err != nil {
|
||||
saveErrors = append(saveErrors, "min_interval: "+err.Error())
|
||||
}
|
||||
if err := database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled)); err != nil {
|
||||
saveErrors = append(saveErrors, "polling_enabled: "+err.Error())
|
||||
}
|
||||
if err := database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval)); err != nil {
|
||||
saveErrors = append(saveErrors, "polling_interval: "+err.Error())
|
||||
}
|
||||
|
||||
if len(saveErrors) > 0 {
|
||||
errMsg := "保存监控设置部分失败: " + saveErrors[0]
|
||||
logger.Error(errMsg, "", "monitor")
|
||||
Error(w, http.StatusInternalServerError, errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// 输出日志
|
||||
logger.Info("监控设置已保存: target="+strconv.Itoa(settings.Target)+
|
||||
logger.Success("监控设置已保存: target="+strconv.Itoa(settings.Target)+
|
||||
", auto_add="+strconv.FormatBool(settings.AutoAdd)+
|
||||
", polling="+strconv.FormatBool(settings.PollingEnabled)+
|
||||
", interval="+strconv.Itoa(settings.PollingInterval)+"s", "", "monitor")
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"codex-pool/internal/auth"
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/database"
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/logger"
|
||||
"codex-pool/internal/mail"
|
||||
@@ -21,9 +22,10 @@ import (
|
||||
type TeamProcessRequest struct {
|
||||
// Owner 账号列表
|
||||
Owners []struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
AccountID string `json:"account_id"` // 已存储的 account_id,如有则直接使用
|
||||
} `json:"owners"`
|
||||
// 配置
|
||||
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
||||
@@ -31,6 +33,7 @@ type TeamProcessRequest struct {
|
||||
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
||||
Headless bool `json:"headless"` // 是否无头模式
|
||||
Proxy string `json:"proxy"` // 代理设置
|
||||
IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A
|
||||
}
|
||||
|
||||
// TeamProcessResult 团队处理结果
|
||||
@@ -76,11 +79,34 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
// 如果没有传入 owners,从数据库获取待处理的母号
|
||||
if len(req.Owners) == 0 {
|
||||
Error(w, http.StatusBadRequest, "请提供至少一个 Owner 账号")
|
||||
return
|
||||
pendingOwners, err := database.Instance.GetPendingOwners()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("获取待处理账号失败: %v", err))
|
||||
return
|
||||
}
|
||||
if len(pendingOwners) == 0 {
|
||||
Error(w, http.StatusBadRequest, "没有待处理的母号,请先上传账号文件")
|
||||
return
|
||||
}
|
||||
// 转换为请求格式(包含已存储的 account_id)
|
||||
for _, o := range pendingOwners {
|
||||
req.Owners = append(req.Owners, struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
AccountID string `json:"account_id"`
|
||||
}{
|
||||
Email: o.Email,
|
||||
Password: o.Password,
|
||||
Token: o.Token,
|
||||
AccountID: o.AccountID, // 直接使用数据库中存储的 account_id
|
||||
})
|
||||
}
|
||||
logger.Info(fmt.Sprintf("从数据库加载 %d 个待处理母号", len(req.Owners)), "", "team")
|
||||
}
|
||||
|
||||
if req.MembersPerTeam <= 0 {
|
||||
req.MembersPerTeam = 4
|
||||
}
|
||||
@@ -225,17 +251,27 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
||||
logPrefix := fmt.Sprintf("[Team %d]", idx+1)
|
||||
logger.Info(fmt.Sprintf("%s Starting with owner: %s", logPrefix, owner.Email), owner.Email, "team")
|
||||
|
||||
// Step 1: 获取 Team ID
|
||||
// Step 1: 获取 Team ID(优先使用已存储的 account_id)
|
||||
var teamID string
|
||||
inviter := invite.NewWithProxy(owner.Token, req.Proxy)
|
||||
teamID, err := inviter.GetAccountID()
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err))
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team")
|
||||
return result
|
||||
|
||||
if owner.AccountID != "" {
|
||||
// 直接使用数据库中存储的 account_id
|
||||
teamID = owner.AccountID
|
||||
logger.Info(fmt.Sprintf("%s 使用已存储的 Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||
} else {
|
||||
// 如果没有存储,才请求 API 获取
|
||||
var err error
|
||||
teamID, err = inviter.GetAccountID()
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err))
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team")
|
||||
return result
|
||||
}
|
||||
logger.Success(fmt.Sprintf("%s 获取到 Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||
}
|
||||
result.TeamID = teamID
|
||||
logger.Success(fmt.Sprintf("%s Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||
|
||||
// Step 2: 生成成员邮箱并发送邀请
|
||||
type MemberAccount struct {
|
||||
@@ -328,7 +364,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
||||
}
|
||||
logger.Info(fmt.Sprintf("%s Registered: %d/%d", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team")
|
||||
|
||||
// Step 4: S2A 授权入库
|
||||
// Step 4: S2A 授权入库(成员)
|
||||
for i, child := range registeredChildren {
|
||||
if !teamProcessState.Running {
|
||||
break
|
||||
@@ -373,6 +409,44 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
||||
logger.Success(fmt.Sprintf("%s [Member %d] Added to S2A", logPrefix, i+1), child.Email, "team")
|
||||
}
|
||||
|
||||
// Step 5: 母号也入库(如果开启)
|
||||
if req.IncludeOwner && teamProcessState.Running {
|
||||
logger.Info(fmt.Sprintf("%s 开始将母号入库到 S2A", logPrefix), owner.Email, "team")
|
||||
|
||||
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Owner auth URL: %v", err))
|
||||
} else {
|
||||
var code string
|
||||
if req.BrowserType == "rod" {
|
||||
code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
||||
} else {
|
||||
code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
||||
}
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Owner browser: %v", err))
|
||||
} else {
|
||||
_, err = auth.SubmitS2AOAuth(
|
||||
config.Global.S2AApiBase,
|
||||
config.Global.S2AAdminKey,
|
||||
s2aResp.Data.SessionID,
|
||||
code,
|
||||
owner.Email,
|
||||
config.Global.Concurrency,
|
||||
config.Global.Priority,
|
||||
config.Global.GroupIDs,
|
||||
config.Global.ProxyID,
|
||||
)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Owner S2A: %v", err))
|
||||
} else {
|
||||
result.AddedToS2A++
|
||||
logger.Success(fmt.Sprintf("%s [Owner] Added to S2A", logPrefix), owner.Email, "team")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
logger.Success(fmt.Sprintf("%s Complete: %d registered, %d in S2A", logPrefix, result.Registered, result.AddedToS2A), owner.Email, "team")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user