package api import ( "context" "encoding/json" "fmt" "net/http" "os" "os/exec" "runtime" "strings" "time" "unicode/utf8" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" ) type ShellChatRequest struct { Message string `json:"message"` Context string `json:"context,omitempty"` Cwd string `json:"cwd,omitempty"` Platform string `json:"platform,omitempty"` Stream bool `json:"stream"` } func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } if s.shellConvStore.AtLimit() { writeError(w, "context limit reached, use /clear", http.StatusBadRequest) return } var req ShellChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if req.Message == "" { writeError(w, "message is required", http.StatusBadRequest) return } s.shellConvStore.Add("user", req.Message) orb, err := orchestrator.New(s.config) if err != nil { writeError(w, err.Error(), http.StatusServiceUnavailable) return } orb.SetSystemPrompt(s.buildShellSystemPrompt(req)) orb.SetTools(s.shellAgentToolsJSON) if req.Stream { s.handleShellChatStream(w, orb) } else { s.handleShellChatNonStream(w, orb) } } func (s *Server) buildShellSystemPrompt(req ShellChatRequest) string { var sb strings.Builder sb.WriteString(shellSystemPromptBase) analysis := LoadSystemAnalysis() if analysis != "" { sb.WriteString("\n") sb.WriteString(analysis) sb.WriteString("\n\n\n") } sb.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) if hostname, err := os.Hostname(); err == nil { sb.WriteString("Hostname: " + hostname + "\n") } if user := os.Getenv("USER"); user != "" { sb.WriteString("User: " + user + "\n") } canSudo := !agent.NeedsSudoPassword() sb.WriteString(fmt.Sprintf("Root: %t\n", !canSudo)) if canSudo { sb.WriteString("⚠️ Session avec privilèges sudo sans mot de passe — les commandes sudo s'exécuteront directement.\n") } else { sb.WriteString("⚠️ Session sans sudo sans mot de passe — les commandes sudo/doas nécessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n") } now := time.Now() sb.WriteString(fmt.Sprintf("Date: %s\nHeure: %s\n", now.Format("02/01/2006"), now.Format("15:04:05"))) return sb.String() } func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { SetupSSEHeaders(w) flusher, canFlush := w.(http.Flusher) sseWriter := NewSSEWriter(w) ctx := context.Background() messages := s.buildShellContextMessages() engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) engine.SetLimiter(s.AcquireAgentSlot) engine.OnChunk(func(data map[string]interface{}) { if data == nil { return } sseWriter.Write(data) if canFlush { flusher.Flush() } }) finalContent, allToolCalls, allToolResults, err := engine.RunWithTools(ctx, messages) if err != nil { sseWriter.Write(map[string]interface{}{"error": err.Error()}) return } storeContent := finalContent if len(allToolCalls) > 0 { storeObj := map[string]interface{}{ "content": storeContent, "tool_calls": allToolCalls, "tool_results": allToolResults, } storeJSON, _ := json.Marshal(storeObj) storeContent = string(storeJSON) } s.shellConvStore.Add("assistant", storeContent) s.consumption.Record(engine.ProviderName(), engine.TotalTokens) sseWriter.Write(map[string]interface{}{ "done": "true", "tokens": s.shellConvStore.ApproxTokens(), }) } func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { ctx := context.Background() messages := s.buildShellContextMessages() engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) engine.SetLimiter(s.AcquireAgentSlot) finalContent, err := engine.RunNonStream(ctx, messages) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } s.shellConvStore.Add("assistant", finalContent) s.consumption.Record(engine.ProviderName(), engine.TotalTokens) writeJSON(w, map[string]interface{}{ "content": finalContent, "tokens": s.shellConvStore.ApproxTokens(), }) } func (s *Server) buildShellContextMessages() []orchestrator.Message { history := s.shellConvStore.Get() sysTokens := utf8.RuneCountInString(shellSystemPromptBase) / charsPerToken if analysis := LoadSystemAnalysis(); analysis != "" { sysTokens += utf8.RuneCountInString(analysis) / charsPerToken } sysTokens += 100 toolsTokens := utf8.RuneCountInString(string(s.shellAgentToolsJSON)) / charsPerToken responseMargin := 4000 overhead := sysTokens + toolsTokens + responseMargin available := shellMaxTokens - overhead if available < 1000 { available = 1000 } included := 0 tokensUsed := 0 for i := len(history) - 1; i >= 0; i-- { displayContent := extractDisplayContent(history[i].Role, history[i].Content) msgTokens := utf8.RuneCountInString(displayContent) / charsPerToken if msgTokens == 0 { msgTokens = 1 } if tokensUsed+msgTokens > available { break } tokensUsed += msgTokens included++ } start := len(history) - included if start < 0 { start = 0 } if start > 0 { _ = start } messages := make([]orchestrator.Message, 0, included) for _, m := range history[start:] { if m.Role == "system" { continue } displayContent := extractDisplayContent(m.Role, m.Content) messages = append(messages, orchestrator.Message{ Role: m.Role, Content: orchestrator.TextContent(displayContent), }) } return messages } func (s *Server) handleShellChatHistory(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } messages := s.shellConvStore.Get() writeJSON(w, map[string]interface{}{ "messages": messages, "tokens": s.shellConvStore.ApproxTokens(), "max_tokens": shellMaxTokens, "at_limit": s.shellConvStore.AtLimit(), }) } func (s *Server) handleShellChatClear(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } s.shellConvStore.Clear() writeJSON(w, map[string]interface{}{ "status": "ok", "tokens": 0, }) } func (s *Server) handleShellAnalyze(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var sysInfo strings.Builder sysInfo.WriteString("=== INFORMATIONS SYSTÈME ===\n") sysInfo.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) if hostname, err := os.Hostname(); err == nil { sysInfo.WriteString("Hostname: " + hostname + "\n") } if user := os.Getenv("USER"); user != "" { sysInfo.WriteString("User: " + user + "\n") } if data, err := os.ReadFile("/proc/cpuinfo"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "model name") { sysInfo.WriteString("CPU: " + strings.SplitN(line, ":", 2)[1] + "\n") break } } } if data, err := os.ReadFile("/proc/meminfo"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "MemTotal:") || strings.HasPrefix(line, "MemAvailable:") { sysInfo.WriteString(strings.TrimSpace(line) + "\n") } } } if out, err := exec.Command("df", "-h", "/").Output(); err == nil { lines := strings.Split(string(out), "\n") if len(lines) >= 2 { sysInfo.WriteString("Disk: " + strings.TrimSpace(lines[1]) + "\n") } } if out, err := exec.Command("ps", "aux", "--sort=-pcpu").Output(); err == nil { lines := strings.Split(string(out), "\n") sysInfo.WriteString(fmt.Sprintf("\nProcessus actifs (%d total):\n", len(lines)-1)) for i := 1; i < len(lines) && i <= 10; i++ { fields := strings.Fields(lines[i]) if len(fields) >= 11 { sysInfo.WriteString(fmt.Sprintf(" %-20s CPU:%-6s MEM:%-6s %s\n", fields[10], fields[2]+"%", fields[3]+"%", fields[0])) } } } if s.scanResult != nil { sysInfo.WriteString("\nOutils installés:\n") for _, t := range s.scanResult.Tools { status := "✗" if t.Installed { status = "✓" } sysInfo.WriteString(fmt.Sprintf(" %s %s %s\n", status, t.Name, t.Version)) } } orb, err := orchestrator.New(s.config) if err != nil { writeError(w, err.Error(), http.StatusServiceUnavailable) return } orb.SetSystemPrompt(agent.StudioSystemPrompt()) analysisPrompt := `Tu es un expert en administration système. Analyse les informations suivantes et génère un rapport structuré en markdown. STRUCTURE REQUISE : ## État du système - Résumé en 2-3 phrases de l'état général (OK/Attention/Critique) ## Points d'attention Liste les problèmes détectés par priorité : - **CRITIQUE** : problèmes de sécurité, espace disque < 10%, mémoire < 10% - **ATTENTION** : CPU élevé, services en échec, config non-optimale - **INFO** : améliorations possibles, mises à jour disponibles ## Recommandations Pour chaque point d'attention, donne UNE commande ou action corrective concrète. ## Outils manquants Liste les outils utiles non installés avec la commande d'installation. ## Réseau - Interfaces actives, ports en écoute, connectivité RÈGLES : - Pas de blabla générique — sois spécifique à CE système - Inclus les valeurs numériques réelles (%, Go, MHz) - Max 1500 mots - Le rapport sert de contexte persistant pour un assistant terminal ` + sysInfo.String() result, err := orb.Send(analysisPrompt) if err != nil { writeError(w, "analysis failed: "+err.Error(), http.StatusInternalServerError) return } SaveSystemAnalysis(result) writeJSON(w, map[string]interface{}{ "status": "ok", "analysis": result, }) } func (s *Server) handleShellAnalysisGet(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } analysis := LoadSystemAnalysis() if analysis == "" { writeJSON(w, map[string]interface{}{"analysis": nil}) return } writeJSON(w, map[string]interface{}{"analysis": analysis}) }