feat(s2a): Add dynamic group fetching with credentials validation
- Add backend endpoint `/api/s2a/fetch-groups` to fetch S2A groups using arbitrary credentials without depending on global config - Implement `handleFetchGroups` handler with proper error handling and multiple auth header support (Bearer token, X-API-Key, X-Admin-Key) - Add frontend API function `fetchGroupsWithCredentials` to call the new endpoint and parse S2A standard response format - Add group fetching UI in S2AConfig edit mode with refresh button and loading state - Implement batch group name caching in list view to display human-readable group names instead of IDs - Deduplicate API requests by grouping profiles with identical credentials to avoid redundant calls - Add fallback to display group ID when name is unavailable
This commit is contained in:
@@ -124,6 +124,7 @@ func startServer(cfg *config.Config) {
|
||||
mux.HandleFunc("/api/s2a/clean-errors", api.CORS(api.HandleCleanErrorAccounts)) // 清理错误账号
|
||||
mux.HandleFunc("/api/s2a/cleaner/settings", api.CORS(handleCleanerSettings)) // 清理服务设置
|
||||
mux.HandleFunc("/api/s2a/profiles", api.CORS(handleS2AProfiles)) // S2A 配置预设管理
|
||||
mux.HandleFunc("/api/s2a/fetch-groups", api.CORS(handleFetchGroups)) // 用任意凭据获取分组列表
|
||||
|
||||
// 统计 API
|
||||
mux.HandleFunc("/api/stats/max-rpm", api.CORS(api.HandleGetMaxRPM)) // 今日最高 RPM
|
||||
@@ -667,6 +668,59 @@ func handleS2AProxy(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(bodyBytes)
|
||||
}
|
||||
|
||||
// handleFetchGroups 使用任意 api_base + admin_key 获取 S2A 分组列表(不依赖 config.Global)
|
||||
func handleFetchGroups(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ApiBase string `json:"api_base"`
|
||||
AdminKey string `json:"admin_key"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.Error(w, http.StatusBadRequest, "请求格式错误")
|
||||
return
|
||||
}
|
||||
if req.ApiBase == "" || req.AdminKey == "" {
|
||||
api.Error(w, http.StatusBadRequest, "api_base 和 admin_key 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
targetURL := strings.TrimRight(req.ApiBase, "/") + "/api/v1/admin/groups/all"
|
||||
|
||||
proxyReq, err := http.NewRequest("GET", targetURL, nil)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, "创建请求失败")
|
||||
return
|
||||
}
|
||||
|
||||
proxyReq.Header.Set("Authorization", "Bearer "+req.AdminKey)
|
||||
proxyReq.Header.Set("X-API-Key", req.AdminKey)
|
||||
proxyReq.Header.Set("X-Admin-Key", req.AdminKey)
|
||||
proxyReq.Header.Set("Content-Type", "application/json")
|
||||
proxyReq.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(proxyReq)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusBadGateway, fmt.Sprintf("请求 S2A 失败: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusBadGateway, "读取响应失败")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
w.Write(bodyBytes)
|
||||
}
|
||||
|
||||
func handleMailServices(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
|
||||
Reference in New Issue
Block a user