package api import ( "encoding/json" "fmt" "io" "net/http" "time" "codex-pool/internal/config" "codex-pool/internal/logger" ) // S2AAccountItem S2A 账号信息 type S2AAccountItem struct { ID int `json:"id"` Email string `json:"account"` // S2A API 返回的字段名是 account Status string `json:"status"` } // S2AAccountsResponse S2A 账号列表响应 type S2AAccountsResponse struct { Code int `json:"code"` Message string `json:"message"` Data struct { Items []S2AAccountItem `json:"items"` Total int `json:"total"` Pages int `json:"pages"` } `json:"data"` } // S2ADeleteResponse S2A 删除响应 type S2ADeleteResponse struct { Code int `json:"code"` Message string `json:"message"` } // HandleCleanErrorAccounts POST /api/s2a/clean-errors - 批量删除错误账号 func HandleCleanErrorAccounts(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { Error(w, http.StatusMethodNotAllowed, "仅支持 POST") return } if config.Global == nil || config.Global.S2AApiBase == "" || config.Global.S2AAdminKey == "" { Error(w, http.StatusBadRequest, "S2A 配置未设置") return } logger.Status("清理错误账号中...", "", "s2a") // Step 1: 获取所有错误账号 errorAccounts, err := fetchAllErrorAccounts() if err != nil { logger.Error(fmt.Sprintf("获取错误账号列表失败: %v", err), "", "s2a") Error(w, http.StatusInternalServerError, fmt.Sprintf("获取错误账号列表失败: %v", err)) return } if len(errorAccounts) == 0 { logger.Info("没有错误账号需要清理", "", "s2a") Success(w, map[string]interface{}{ "message": "没有错误账号需要清理", "total": 0, "success": 0, "failed": 0, }) return } logger.Status(fmt.Sprintf("找到 %d 个错误账号,删除中...", len(errorAccounts)), "", "s2a") // Step 2: 逐条删除 success := 0 failed := 0 var details []map[string]interface{} for _, account := range errorAccounts { err := deleteS2AAccount(account.ID) if err != nil { failed++ details = append(details, map[string]interface{}{ "id": account.ID, "email": account.Email, "success": false, "error": err.Error(), }) logger.Warning(fmt.Sprintf("删除账号失败: ID=%d, Email=%s, Error=%v", account.ID, account.Email, err), account.Email, "s2a") } else { success++ details = append(details, map[string]interface{}{ "id": account.ID, "email": account.Email, "success": true, }) logger.Success(fmt.Sprintf("删除账号成功: ID=%d, Email=%s", account.ID, account.Email), account.Email, "s2a") } } logger.Success(fmt.Sprintf("清理错误账号完成: 成功=%d, 失败=%d, 总数=%d", success, failed, len(errorAccounts)), "", "s2a") Success(w, map[string]interface{}{ "message": fmt.Sprintf("清理完成: 成功 %d, 失败 %d", success, failed), "total": len(errorAccounts), "success": success, "failed": failed, "details": details, }) } // fetchAllErrorAccounts 分页获取所有错误账号 func fetchAllErrorAccounts() ([]S2AAccountItem, error) { var allAccounts []S2AAccountItem page := 1 pageSize := 100 client := &http.Client{Timeout: 30 * time.Second} for { url := fmt.Sprintf("%s/api/v1/admin/accounts?page=%d&page_size=%d&status=error&timezone=Asia/Shanghai", config.Global.S2AApiBase, page, pageSize) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %v", err) } setS2AHeaders(req) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %v", err) } var result S2AAccountsResponse if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %v", err) } if result.Code != 0 { return nil, fmt.Errorf("API 错误: %s", result.Message) } if len(result.Data.Items) == 0 { break } allAccounts = append(allAccounts, result.Data.Items...) logger.Info(fmt.Sprintf("获取错误账号: 第 %d 页, 本页 %d 个, 累计 %d 个", page, len(result.Data.Items), len(allAccounts)), "", "s2a") if page >= result.Data.Pages { break } page++ } return allAccounts, nil } // deleteS2AAccount 删除单个 S2A 账号 func deleteS2AAccount(accountID int) error { client := &http.Client{Timeout: 30 * time.Second} url := fmt.Sprintf("%s/api/v1/admin/accounts/%d", config.Global.S2AApiBase, accountID) req, err := http.NewRequest("DELETE", url, nil) if err != nil { return fmt.Errorf("创建请求失败: %v", err) } setS2AHeaders(req) resp, err := client.Do(req) if err != nil { return fmt.Errorf("请求失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("读取响应失败: %v", err) } var result S2ADeleteResponse if err := json.Unmarshal(body, &result); err != nil { return fmt.Errorf("解析响应失败: %v", err) } if result.Code != 0 { return fmt.Errorf("%s", result.Message) } return nil } // setS2AHeaders 设置 S2A 请求头 func setS2AHeaders(req *http.Request) { req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") // 认证方式: x-api-key 或 authorization if config.Global.S2AAdminKey != "" { req.Header.Set("X-API-Key", config.Global.S2AAdminKey) } // 也设置 Authorization 作为备用 req.Header.Set("Authorization", "Bearer "+config.Global.S2AAdminKey) }