From 09b0479fb382223b82776609a7a970909eb65397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:50:44 +0800 Subject: [PATCH] Add files via upload --- internal/app/app.go | 1 + internal/handler/config.go | 74 +++++++++++++++++++++++++++++++++++++ internal/handler/openapi.go | 45 ++++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/internal/app/app.go b/internal/app/app.go index b3b86e1b..12f28565 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -876,6 +876,7 @@ func setupRoutes( protected.POST("/config/apply", configHandler.ApplyConfig) protected.POST("/config/test-openai", configHandler.TestOpenAI) protected.POST("/config/test-vision", configHandler.TestVision) + protected.POST("/config/list-models", configHandler.ListModels) // 系统设置 - 终端(执行命令,提高运维效率) protected.POST("/terminal/run", terminalHandler.RunCommand) diff --git a/internal/handler/config.go b/internal/handler/config.go index 0073d009..a0261363 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -1068,6 +1068,80 @@ func (h *ConfigHandler) TestOpenAI(c *gin.Context) { }) } +// ListModelsRequest 获取模型列表请求(OpenAI 兼容 GET /models)。 +type ListModelsRequest struct { + Provider string `json:"provider"` + BaseURL string `json:"base_url"` + APIKey string `json:"api_key"` +} + +// ListModels 代理调用上游 GET /models,返回可用模型 id 列表。 +func (h *ConfigHandler) ListModels(c *gin.Context) { + var req ListModelsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + return + } + + provider := strings.TrimSpace(req.Provider) + if provider == "" { + provider = "openai" + } + if strings.EqualFold(provider, "claude") { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "supported": false, + "error": "Claude (Anthropic Messages API) 不支持自动获取模型列表,请手动填写", + }) + return + } + + if strings.TrimSpace(req.APIKey) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "API Key 不能为空"}) + return + } + + baseURL := strings.TrimSuffix(strings.TrimSpace(req.BaseURL), "/") + if baseURL == "" { + baseURL = "https://api.openai.com/v1" + } + + tmpCfg := &config.OpenAIConfig{ + Provider: provider, + BaseURL: baseURL, + APIKey: strings.TrimSpace(req.APIKey), + } + client := openai.NewClient(tmpCfg, nil, h.logger) + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + models, err := client.ListModels(ctx) + if err != nil { + if apiErr, ok := err.(*openai.APIError); ok { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "supported": true, + "error": fmt.Sprintf("API 返回错误 (HTTP %d): %s", apiErr.StatusCode, apiErr.Body), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": false, + "supported": true, + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "supported": true, + "models": models, + "count": len(models), + }) +} + // TestVisionRequest 测试 Vision 模型连接;vision.api_key/base_url 留空时可传 openai 段作回退。 type TestVisionRequest struct { Vision config.VisionConfig `json:"vision"` diff --git a/internal/handler/openapi.go b/internal/handler/openapi.go index 7a7a19f6..da07001a 100644 --- a/internal/handler/openapi.go +++ b/internal/handler/openapi.go @@ -5030,6 +5030,51 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) { }, }, }, + "/api/config/list-models": map[string]interface{}{ + "post": map[string]interface{}{ + "tags": []string{"配置管理"}, + "summary": "获取模型列表", + "description": "代理调用 OpenAI 兼容 GET /models,返回可用模型 id 列表。Claude 不支持。", + "operationId": "listModels", + "requestBody": map[string]interface{}{ + "required": true, + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "object", + "required": []string{"api_key"}, + "properties": map[string]interface{}{ + "provider": map[string]interface{}{"type": "string", "description": "LLM提供商(openai/claude)", "example": "openai"}, + "base_url": map[string]interface{}{"type": "string", "description": "API基地址(可选)"}, + "api_key": map[string]interface{}{"type": "string", "description": "API密钥"}, + }, + }, + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "获取结果", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "success": map[string]interface{}{"type": "boolean"}, + "supported": map[string]interface{}{"type": "boolean"}, + "error": map[string]interface{}{"type": "string"}, + "models": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}}, + "count": map[string]interface{}{"type": "integer"}, + }, + }, + }, + }, + }, + "400": map[string]interface{}{"description": "参数错误"}, + "401": map[string]interface{}{"description": "未授权"}, + }, + }, + }, // ==================== 终端 ==================== "/api/terminal/run": map[string]interface{}{