From 6f0101f6560fde9ee8b681971a3aa9d2794288e9 Mon Sep 17 00:00:00 2001 From: Naeel Date: Mon, 20 Apr 2026 14:06:51 +0300 Subject: [PATCH] feat: LLM syntax check via /console/api/ai/check - Server-side proxy to LLM API (FISSION_LLM_KEY, FISSION_LLM_URL env vars) - Button in create-modal and edit-modal: check syntax before saving - Result shown inline below code textarea (green=ok, red=errors) --- console/main.go | 110 ++++++++++++++++++++++++++++++++++++++++++ console/ui/index.html | 51 +++++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/console/main.go b/console/main.go index c3e7218..6ae7c10 100644 --- a/console/main.go +++ b/console/main.go @@ -65,6 +65,9 @@ type server struct { authUser string authPass string + llmURL string // FISSION_LLM_URL — базовый URL LLM API + llmKey string // FISSION_LLM_KEY — Bearer ключ + tokenMu sync.Mutex cachedJWT string tokenExpAt time.Time @@ -133,6 +136,8 @@ func main() { authUser: authUser, authPass: authPass, testMode: os.Getenv("FISSION_TEST_MODE") == "true", + llmURL: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"), + llmKey: os.Getenv("FISSION_LLM_KEY"), } mux := http.NewServeMux() @@ -204,6 +209,7 @@ func main() { mux.HandleFunc("/console/api/functions/", auth(s.handleFunctionsAction)) mux.HandleFunc("/console/api/httptriggers", auth(s.handleList(httpTrigGVR))) mux.HandleFunc("/console/api/timetriggers", auth(s.handleList(timeTrigGVR))) + mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck)) httpServer := &http.Server{ Addr: ":" + port, @@ -1769,3 +1775,107 @@ func readZipFile(file *zip.File) ([]byte, error) { return io.ReadAll(rc) } + +// handleAICheck проксирует запрос проверки синтаксиса к LLM API. +// POST /console/api/ai/check +// Body: {"language":"python","code":"..."} +// Response: {"ok":true,"result":"..."} +func (s *server) handleAICheck(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + if s.llmKey == "" { + writeJSONError(w, http.StatusServiceUnavailable, "LLM не настроен (FISSION_LLM_KEY не задан)") + return + } + + var req struct { + Language string `json:"language"` + Code string `json:"code"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 64*1024)).Decode(&req); err != nil { + writeJSONError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) + return + } + if strings.TrimSpace(req.Code) == "" { + writeJSONError(w, http.StatusBadRequest, "code is required") + return + } + + prompt := fmt.Sprintf( + "Проверь синтаксис следующего кода на языке %s для serverless-функции Fission. "+ + "Найди синтаксические ошибки, неправильные вызовы, проблемы с entrypoint. "+ + "Если ошибок нет — ответь кратко: \"✅ Синтаксис корректен.\" "+ + "Если есть — перечисли проблемы по пунктам, кратко, без лишних слов. "+ + "Код:\n\n```%s\n%s\n```", + req.Language, req.Language, req.Code, + ) + + llmBody, _ := json.Marshal(map[string]any{ + "model": "gpt-oss-120b", + "messages": []map[string]string{ + {"role": "user", "content": prompt}, + }, + "max_tokens": 512, + }) + + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + defer cancel() + + llmReq, err := http.NewRequestWithContext(ctx, http.MethodPost, + strings.TrimRight(s.llmURL, "/")+"/chat/completions", + bytes.NewReader(llmBody), + ) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "build llm request: "+err.Error()) + return + } + llmReq.Header.Set("Content-Type", "application/json") + llmReq.Header.Set("Authorization", "Bearer "+s.llmKey) + + resp, err := s.http.Do(llmReq) + if err != nil { + writeJSONError(w, http.StatusBadGateway, "llm request failed: "+err.Error()) + return + } + defer resp.Body.Close() + + raw, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + writeJSONError(w, http.StatusBadGateway, "read llm response: "+err.Error()) + return + } + + var llmResp struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + if err := json.Unmarshal(raw, &llmResp); err != nil { + writeJSONError(w, http.StatusBadGateway, "parse llm response: "+err.Error()) + return + } + if llmResp.Error != nil { + writeJSONError(w, http.StatusBadGateway, "llm error: "+llmResp.Error.Message) + return + } + if len(llmResp.Choices) == 0 { + writeJSONError(w, http.StatusBadGateway, "empty llm response") + return + } + + result := strings.TrimSpace(llmResp.Choices[0].Message.Content) + isOK := strings.Contains(result, "✅") || strings.HasPrefix(strings.ToLower(result), "синтаксис корректен") + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "ok": isOK, + "result": result, + }) +} diff --git a/console/ui/index.html b/console/ui/index.html index d97a62a..3a8a389 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -390,6 +390,10 @@ +
+ +
+
@@ -401,7 +405,8 @@