diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 768e91e..dfdc4db 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -1,9 +1,13 @@ package api import ( + "bytes" "encoding/json" + "io" "net/http" "os/exec" + "strings" + "time" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/lsp" @@ -529,3 +533,95 @@ func (s *Server) handleRunUpdate(w http.ResponseWriter, r *http.Request) { "updated": len(needsUpdate), }) } + +func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, "POST only", http.StatusMethodNotAllowed) + return + } + var body struct { + Name string `json:"name"` + APIKey string `json:"api_key"` + Model string `json:"model"` + BaseURL string `json:"base_url"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } + if body.APIKey == "" { + writeError(w, "api_key required", http.StatusBadRequest) + return + } + + baseURL := body.BaseURL + if baseURL == "" { + for _, p := range s.config.AI.Providers { + if p.Name == body.Name { + baseURL = p.BaseURL + break + } + } + } + if baseURL == "" { + switch body.Name { + case "minimax": + baseURL = "https://api.minimax.io/v1" + case "openai": + baseURL = "https://api.openai.com/v1" + case "anthropic": + baseURL = "https://api.anthropic.com/v1" + default: + baseURL = "https://api.minimax.io/v1" + } + } + + model := body.Model + if model == "" { + for _, p := range s.config.AI.Providers { + if p.Name == body.Name { + model = p.Model + break + } + } + } + if model == "" { + model = "MiniMax-Text-01" + } + + reqBody, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": []map[string]string{{"role": "user", "content": "Hi"}}, + "max_tokens": 5, + "stream": false, + }) + + url := strings.TrimRight(baseURL, "/") + "/chat/completions" + req, err := http.NewRequest("POST", url, bytes.NewReader(reqBody)) + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+body.APIKey) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + writeError(w, "connection failed: "+err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + writeError(w, "invalid_api_key", http.StatusUnauthorized) + return + } + if resp.StatusCode != http.StatusOK { + writeError(w, "api_error: "+string(respBody), http.StatusBadGateway) + return + } + + writeJSON(w, map[string]interface{}{"status": "valid"}) +} diff --git a/internal/api/server.go b/internal/api/server.go index affd3fd..5f7ffa3 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -46,6 +46,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure) s.mux.HandleFunc("/api/config/profile", s.handleSaveProfile) s.mux.HandleFunc("/api/config/provider", s.handleSaveProvider) + s.mux.HandleFunc("/api/providers/validate", s.handleValidateProvider) s.mux.HandleFunc("/api/update/run", s.handleRunUpdate) s.mux.HandleFunc("/api/chat", s.handleChat) s.mux.HandleFunc("/api/chat/history", s.handleChatHistory) diff --git a/web/src/api/client.js b/web/src/api/client.js index 55a02d0..03eb34d 100644 --- a/web/src/api/client.js +++ b/web/src/api/client.js @@ -28,6 +28,7 @@ const api = { savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }), saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }), saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }), + validateProvider: (provider) => request('/providers/validate', { method: 'POST', body: JSON.stringify(provider) }), runUpdate: (tool) => request('/update/run', { method: 'POST', body: JSON.stringify({ tool: tool || '' }) }), runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }), getTerminalSessions: () => request('/terminal/sessions'), diff --git a/web/src/components/Config.jsx b/web/src/components/Config.jsx index d3c1157..1c10848 100644 --- a/web/src/components/Config.jsx +++ b/web/src/components/Config.jsx @@ -252,48 +252,79 @@ function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEdi } function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) { + const [validating, setValidating] = useState(null) + const [validationStatus, setValidationStatus] = useState(null) + + const handleValidate = async (name, apiKey, model, baseUrl) => { + setValidating(name) + setValidationStatus(null) + try { + await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl }) + setValidationStatus({ provider: name, valid: true }) + } catch (err) { + const msg = err.message || '' + if (msg.includes('invalid_api_key')) { + setValidationStatus({ provider: name, valid: false, error: t('config.keyInvalid') }) + } else { + setValidationStatus({ provider: name, valid: false, error: `${t('config.connectionFailed')}: ${msg}` }) + } + } + setValidating(null) + } + return (