diff --git a/console/main.go b/console/main.go index 83df2d6..0b12fcb 100644 --- a/console/main.go +++ b/console/main.go @@ -68,7 +68,10 @@ type server struct { authUser string authPass string - + // --- ai/ask feature (удалить блок целиком чтобы выкосить) --- + llmURL string // FISSION_LLM_URL + llmKey string // FISSION_LLM_KEY + // --- end ai/ask feature --- tokenMu sync.Mutex cachedJWT string @@ -138,6 +141,10 @@ func main() { authUser: authUser, authPass: authPass, testMode: os.Getenv("FISSION_TEST_MODE") == "true", + // --- ai/ask feature --- + llmURL: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"), + llmKey: os.Getenv("FISSION_LLM_KEY"), + // --- end ai/ask feature --- } mux := http.NewServeMux() @@ -210,6 +217,9 @@ func main() { 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)) + // --- ai/ask feature (удалить строку чтобы выкосить роут) --- + mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk)) + // --- end ai/ask feature --- httpServer := &http.Server{ Addr: ":" + port, @@ -1886,3 +1896,97 @@ func (s *server) handleAICheck(w http.ResponseWriter, r *http.Request) { "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 struct { + Question string `json:"question"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 4*1024)).Decode(&req); err != nil { + writeJSONError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) + return + } + if strings.TrimSpace(req.Question) == "" { + writeJSONError(w, http.StatusBadRequest, "question is required") + return + } + + body, _ := json.Marshal(map[string]any{ + "model": "gpt-oss-120b", + "messages": []map[string]string{ + { + "role": "system", + "content": "Ты помощник по Fission — serverless платформе на Kubernetes. Отвечай кратко и по делу. Если вопрос не про Fission или программирование — вежливо скажи что не знаешь.", + }, + {"role": "user", "content": req.Question}, + }, + "max_tokens": 1024, + }) + + 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 +// ============================================================================= diff --git a/console/ui/index.html b/console/ui/index.html index f7c2be4..366725f 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -852,5 +852,75 @@ checkAuth(); + + +
+
+ 💬 Ассистент + +
+
+
Привет! Задай любой вопрос про Fission или программирование.
+
+ + +
+
+
+ + +