package api import ( "encoding/json" "fmt" "net/http" "os" "os/exec" "runtime" "strings" "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.buildShellSystemPromptV2(req)) if req.Stream { s.handleShellChatStreamV2(w, orb) } else { s.handleShellChatNonStreamV2(w, orb) } } func (s *Server) buildShellSystemPromptV2(_ ShellChatRequest) string { var sb strings.Builder sb.WriteString(`Tu es l'Analyste Système de Muyue. Tu es un expert en administration système et développement. Tu aides l'utilisateur à comprendre son système, diagnostiquer des problèmes, et optimiser son environnement. RÈGLES STRICTES: - Tu ne peux JAMAIS exécuter de commande ou de code - Tu ne peux que analyser, expliquer, et proposer des solutions - Quand tu proposes du code ou des commandes, mets-les dans des blocs de code markdown avec le langage spécifié - L'utilisateur pourra les copier ou les envoyer directement au terminal depuis les boutons `) analysis := LoadSystemAnalysis() if analysis != "" { sb.WriteString("=== ANALYSE SYSTÈME ACTUELLE ===\n") sb.WriteString(analysis) sb.WriteString("\n=== FIN DE L'ANALYSE ===\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") } return sb.String() } func (s *Server) handleShellChatStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) { SetupSSEHeaders(w) flusher, canFlush := w.(http.Flusher) sseWriter := NewSSEWriter(w) // Rebuild history into orchestrator history := s.shellConvStore.Get() for _, m := range history[:len(history)-1] { // all except last user msg if m.Role == "system" { continue } // Pre-load orchestrator history orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content}) } lastUserMsg := history[len(history)-1].Content var finalContent string result, err := orb.SendStream(lastUserMsg, func(chunk string) { finalContent = chunk sseWriter.Write(map[string]interface{}{"content": chunk}) if canFlush { flusher.Flush() } }) if err != nil { sseWriter.Write(map[string]interface{}{"error": err.Error()}) return } content := result if content == "" { content = finalContent } s.shellConvStore.Add("assistant", cleanThinkingTags(content)) sseWriter.Write(map[string]interface{}{ "done": "true", "tokens": s.shellConvStore.ApproxTokens(), }) } func (s *Server) handleShellChatNonStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) { history := s.shellConvStore.Get() for _, m := range history[:len(history)-1] { if m.Role == "system" { continue } orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content}) } lastUserMsg := history[len(history)-1].Content result, err := orb.Send(lastUserMsg) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } s.shellConvStore.Add("assistant", cleanThinkingTags(result)) writeJSON(w, map[string]interface{}{ "content": result, "tokens": s.shellConvStore.ApproxTokens(), }) } 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 sur le système de l'utilisateur. Génère un rapport d'analyse concis et structuré en markdown qui inclut: 1. Un résumé de l'état du système 2. Les points d'attention (performance, sécurité, configuration) 3. Des recommandations spécifiques d'optimisation 4. Les outils manquants qui pourraient être utiles 5. L'état du réseau et des connexions Sois concret et technique. Le rapport sera utilisé comme contexte 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}) }