package api import ( "bytes" "context" "encoding/json" "fmt" "go/parser" "go/token" "io" "net/http" "os" "os/exec" "strings" "time" ) // handleAICheck проверяет синтаксис кода через реальный линтер языка. // POST /console/api/ai/check // Body: {"language":"python","code":"..."} // Response: {"ok":true,"result":"..."} // // Почему реальные линтеры, а не LLM: // LLM часто "исправляет" валидный код и врёт о наличии ошибок. // node --check / python3 -m py_compile / ruby -c / php -l — детерминированы и надёжны. // Go использует go/parser прямо в процессе — без внешних команд, быстро. func (s *Server) handleAICheck(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed") 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) == "" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"ok": false, "result": "Код пустой."}) return } // Конфигурация линтера для каждого языка type linterCfg struct { ext string cmd []string } 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"}}, "go": {ext: ".go", cmd: nil}, // go проверяется через go/parser в процессе } cfg, ok := langs[req.Language] if !ok { writeJSONError(w, http.StatusBadRequest, "unsupported language: "+req.Language) return } var isOK bool var result string if req.Language == "go" { // Go: используем go/parser прямо в процессе — без внешних команд и temp файлов fset := token.NewFileSet() _, parseErr := parser.ParseFile(fset, "code.go", req.Code, parser.AllErrors) if parseErr == nil { isOK = true result = "✅ Синтаксис корректен." } else { isOK = false result = parseErr.Error() } } else { // Записываем код во временный файл с нужным расширением // (линтеры ruby/php определяют режим проверки по расширению) 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 содержит только захардкоженные команды из langs (не user input) 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{ "ok": isOK, "result": result, }) } // ============================================================================= // ai/ask feature — удалить весь блок до "end ai/ask feature" чтобы выкосить // ============================================================================= // handleAIAsk отвечает на произвольный вопрос пользователя через LLM. // POST /console/api/ai/ask // Body: {"question":"..."} // Response: {"answer":"..."} func (s *Server) handleAIAsk(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, "AI-ассистент не настроен (FISSION_LLM_KEY не задан)") return } var req aiPromptRequest if err := json.NewDecoder(io.LimitReader(r.Body, 4*1024)).Decode(&req); err != nil { writeJSONError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } chatReq, err := buildLLMChatRequest(req) if err != nil { writeJSONError(w, http.StatusBadRequest, err.Error()) return } body, _ := json.Marshal(chatReq) 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(body), ) if err != nil { writeJSONError(w, http.StatusInternalServerError, 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 недоступен: "+err.Error()) return } defer resp.Body.Close() 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.NewDecoder(io.LimitReader(resp.Body, 64*1024)).Decode(&llmResp); err != nil { writeJSONError(w, http.StatusBadGateway, "parse error: "+err.Error()) return } if llmResp.Error != nil { writeJSONError(w, http.StatusBadGateway, llmResp.Error.Message) return } if len(llmResp.Choices) == 0 { writeJSONError(w, http.StatusBadGateway, "empty response from LLM") return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "answer": strings.TrimSpace(llmResp.Choices[0].Message.Content), }) } // ============================================================================= // end ai/ask feature // =============================================================================