diff --git a/console/Dockerfile b/console/Dockerfile index a8491c9..1019e31 100644 --- a/console/Dockerfile +++ b/console/Dockerfile @@ -6,7 +6,7 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o fission-console . FROM alpine:3.20 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates nodejs python3 ruby perl php83 COPY --from=builder /build/fission-console /fission-console EXPOSE 8090 ENTRYPOINT ["/fission-console"] diff --git a/console/main.go b/console/main.go index 6ae7c10..83df2d6 100644 --- a/console/main.go +++ b/console/main.go @@ -14,7 +14,10 @@ import ( "log" "net" "net/http" + "go/parser" + "go/token" "os" + "os/exec" "regexp" "sort" "strconv" @@ -65,8 +68,7 @@ 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 @@ -136,8 +138,6 @@ 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() @@ -1776,7 +1776,17 @@ func readZipFile(file *zip.File) ([]byte, error) { return io.ReadAll(rc) } -// handleAICheck проксирует запрос проверки синтаксиса к LLM API. +// goParse проверяет синтаксис Go-кода через go/parser. +func goParse(code string) (*token.FileSet, error) { + fset := token.NewFileSet() + _, err := parser.ParseFile(fset, "code.go", code, parser.AllErrors) + if err != nil { + return nil, err + } + return fset, nil +} + +// handleAICheck проверяет синтаксис кода через реальный линтер языка. // POST /console/api/ai/check // Body: {"language":"python","code":"..."} // Response: {"ok":true,"result":"..."} @@ -1785,10 +1795,6 @@ func (s *server) handleAICheck(w http.ResponseWriter, r *http.Request) { 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"` @@ -1799,79 +1805,80 @@ func (s *server) handleAICheck(w http.ResponseWriter, r *http.Request) { return } if strings.TrimSpace(req.Code) == "" { - writeJSONError(w, http.StatusBadRequest, "code is required") + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"ok": false, "result": "Код пустой."}) 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 + // Определяем команду и расширение файла по языку + type linterCfg struct { + ext string + cmd []string } - 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 + langs := map[string]linterCfg{ + "nodejs": {ext: ".js", cmd: []string{"node", "--check"}}, + "python": {ext: ".py", cmd: []string{"python3", "-m", "py_compile"}}, + "ruby": {ext: ".rb", cmd: []string{"ruby", "-c"}}, + "php": {ext: ".php", cmd: []string{"php", "-l"}}, + "perl": {ext: ".pl", cmd: []string{"perl", "-c"}}, + "go": {ext: ".go", cmd: nil}, // go проверяем через go/parser } - 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()) + cfg, ok := langs[req.Language] + if !ok { + writeJSONError(w, http.StatusBadRequest, "unsupported language: "+req.Language) 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 - } + var isOK bool + var result string - result := strings.TrimSpace(llmResp.Choices[0].Message.Content) - isOK := strings.Contains(result, "✅") || strings.HasPrefix(strings.ToLower(result), "синтаксис корректен") + if req.Language == "go" { + // Go: используем go/parser прямо в процессе — без внешних команд + _, parseErr := goParse(req.Code) + if parseErr == nil { + isOK = true + result = "✅ Синтаксис корректен." + } else { + isOK = false + result = parseErr.Error() + } + } else { + // Записываем код во временный файл + tmpf, err := os.CreateTemp("", "fission-lint-*"+cfg.ext) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "tmp file: "+err.Error()) + return + } + defer os.Remove(tmpf.Name()) + if _, err := tmpf.WriteString(req.Code); err != nil { + tmpf.Close() + writeJSONError(w, http.StatusInternalServerError, "write tmp: "+err.Error()) + return + } + tmpf.Close() + + args := append(cfg.cmd, tmpf.Name()) + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + //nolint:gosec — cfg.cmd содержит только захардкоженные команды из langsMap + out, err := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput() + outStr := strings.TrimSpace(string(out)) + // Убираем путь к tmp-файлу из вывода — юзеру незачем его видеть + outStr = strings.ReplaceAll(outStr, tmpf.Name(), "") + + if err == nil { + isOK = true + result = "✅ Синтаксис корректен." + } else { + isOK = false + if outStr != "" { + result = outStr + } else { + result = "Ошибка синтаксиса (линтер вернул код " + fmt.Sprintf("%v", err) + ")" + } + } + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ diff --git a/console/ui/index.html b/console/ui/index.html index 3a8a389..f7c2be4 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -391,7 +391,7 @@ return {"ok": True, "msg": "hello from fission console"}
- +
@@ -425,7 +425,7 @@
- +
@@ -625,7 +625,7 @@ document.getElementById('e-env').value = fn.environment || ''; document.getElementById('e-entry').value = fn.entrypoint || ''; document.getElementById('e-code').value = fn.code || ''; - // Определяем язык по имени environment для подсказки AI + // Определяем язык по имени environment для линтера var envName = (fn.environment || '').toLowerCase(); var lang = 'python'; if (envName.includes('node')) lang = 'nodejs'; @@ -837,7 +837,7 @@ resEl.textContent = 'Ошибка: ' + e.message; } finally { btn.disabled = false; - btn.textContent = '✨ Проверить синтаксис'; + btn.textContent = '🔍 Проверить синтаксис'; } }