diff --git a/backend/internal/api/monitor.go b/backend/internal/api/monitor.go index 952decb..b235438 100644 --- a/backend/internal/api/monitor.go +++ b/backend/internal/api/monitor.go @@ -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") diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 0325931..62c38b6 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -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") diff --git a/frontend/src/components/common/Switch.tsx b/frontend/src/components/common/Switch.tsx new file mode 100644 index 0000000..fd2e105 --- /dev/null +++ b/frontend/src/components/common/Switch.tsx @@ -0,0 +1,72 @@ +import { useId } from 'react' + +interface SwitchProps { + checked: boolean + onChange: (checked: boolean) => void + disabled?: boolean + label?: string + description?: string + className?: string +} + +export default function Switch({ + checked, + onChange, + disabled = false, + label, + description, + className = '', +}: SwitchProps) { + const id = useId() + + return ( +
+ {(label || description) && ( +
+ {label && ( + + )} + {description && ( +

+ {description} +

+ )} +
+ )} + +
+ ) +} + +export type { SwitchProps } diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index 577befb..72a80f7 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -31,3 +31,6 @@ export { ToastProvider, useToast } from './Toast' export { Tabs } from './Tabs' export type { TabItem } from './Tabs' + +export { default as Switch } from './Switch' +export type { SwitchProps } from './Switch' diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx index ab34c1c..0b1f3a7 100644 --- a/frontend/src/pages/Monitor.tsx +++ b/frontend/src/pages/Monitor.tsx @@ -14,7 +14,7 @@ import { Clock, Save, } from 'lucide-react' -import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' +import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common' import type { DashboardStats } from '../types' interface PoolStatus { @@ -148,11 +148,19 @@ export default function Monitor() { polling_interval: pollingInterval, }), }) - if (!res.ok) { - console.error('保存设置失败:', res.status) + const data = await res.json() + if (!res.ok || data.code !== 0) { + console.error('保存设置失败:', data.message || res.status) + alert('保存设置失败: ' + (data.message || '未知错误')) + setLoading(false) + return } + console.log('保存设置成功:', data) } catch (e) { console.error('保存设置失败:', e) + alert('保存设置失败: ' + (e instanceof Error ? e.message : '网络错误')) + setLoading(false) + return } // 更新本地状态 setPoolStatus(prev => prev ? { @@ -438,96 +446,98 @@ export default function Monitor() { - {/* 配置面板 */} + {/* 配置面板 - 使用 flex 布局让两卡片等高,底部按钮对齐 */}
{/* 目标设置 */} - + 号池目标设置 - - setTargetInput(Number(e.target.value))} - hint="期望保持的活跃账号数量" - /> -
- setAutoAdd(e.target.checked)} - className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" + +
+ setTargetInput(Number(e.target.value))} + hint="期望保持的活跃账号数量" + /> +
+ +
+ setMinInterval(Number(e.target.value))} + hint="两次自动补号的最小间隔" + disabled={!autoAdd} /> -
- setMinInterval(Number(e.target.value))} - hint="两次自动补号的最小间隔" - disabled={!autoAdd} - /> - +
+ +
{/* 轮询控制 */} - + 实时监控设置 - -
- - setPollingInterval(Number(e.target.value) || 60)} - className="w-full px-3 py-2 text-sm rounded-lg border transition-colors - bg-white dark:bg-slate-800 - text-slate-900 dark:text-slate-100 - border-slate-300 dark:border-slate-600 - focus:border-blue-500 focus:ring-blue-500 - focus:outline-none focus:ring-2" - /> -

- 自动刷新号池状态的间隔时间 (10-600秒) -

-
-
-
-
-

监控状态

-

- {pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'} -

+ +
+
+ + setPollingInterval(Number(e.target.value) || 60)} + className="w-full px-3 py-2 text-sm rounded-lg border transition-colors + bg-white dark:bg-slate-800 + text-slate-900 dark:text-slate-100 + border-slate-300 dark:border-slate-600 + focus:border-blue-500 focus:ring-blue-500 + focus:outline-none focus:ring-2" + /> +

+ 自动刷新号池状态的间隔时间 (10-600秒) +

+
+
+
+
+

监控状态

+

+ {pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'} +

+
+
-
-
+