All checks were successful
PR Check / check (pull_request) Successful in 57s
Audit corrections (security, concurrency, stability): - chat_engine: bound resp.Choices[0] access, release tool slot per-iteration - conversation_multi: synchronous save under existing lock (was racy fire-and-forget) - workflow/engine: short-circuit on failed deps (no more infinite busy-wait); track failed/skipped status - handlers_workflow: rune-aware truncate for plan goal (UTF-8 safe) - server: CORS limited to localhost origins (was wildcard) - handlers_info / terminal: mask API keys and SSH passwords as "***" in GET responses; preserve stored secret if "***" sent on update - terminal: sshpass uses -e + SSHPASS env var (was both -p and -e) - handlers_chat: MaxBytesReader 50 MB on /api/chat - image_cache: 10 MB cap per image - handlers_config: font size <= 72; profile-save unmarshal errors propagated - handlers_info: /lsp/auto-install ProjectDir restricted to user home - Shell.jsx: parenthesized resize-condition (operator precedence) - orchestrator_test: CleanAIResponse capitalization (fixes failing vet) New features: - platform: detect OS name (Debian, Ubuntu, Windows 11, macOS X.Y) and inject in Studio system prompt next to the date - agents: default timeout 30 min for crush_run/claude_run (cap also 30 min) - agents: new cwd, wsl_distro, wsl_user params; on Windows hosts launch via "wsl -d <distro> -u <user> --cd <cwd> --" - agents: new claude_run tool (mirror of crush_run for Claude Code CLI) - terminal: list installed WSL distros individually in new-tab menu (Windows only) - studio: system prompt rewritten around BMAD-METHOD personas + mandatory delegation template - studio: "Réflexion avancée" toggle — inactive provider produces a preliminary report injected as [RAPPORT PRÉALABLE] context for the active provider - studio: "Historique compressé" toggle — collapses past tool calls to last action only, with "Tout afficher" expansion
219 lines
8.1 KiB
Go
219 lines
8.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/muyue/muyue/internal/agent"
|
|
"github.com/muyue/muyue/internal/config"
|
|
"github.com/muyue/muyue/internal/installer"
|
|
"github.com/muyue/muyue/internal/scanner"
|
|
"github.com/muyue/muyue/internal/workflow"
|
|
)
|
|
|
|
type Server struct {
|
|
config *config.MuyueConfig
|
|
scanResult *scanner.ScanResult
|
|
mux *http.ServeMux
|
|
convStore *ConversationStore
|
|
shellConvStore *ShellConvStore
|
|
consumption *consumptionStore
|
|
agentRegistry *agent.Registry
|
|
agentToolsJSON json.RawMessage
|
|
shellAgentRegistry *agent.Registry
|
|
shellAgentToolsJSON json.RawMessage
|
|
workflowEngine *workflow.Engine
|
|
activeCrushAgents atomic.Int32
|
|
activeClaudeAgents atomic.Int32
|
|
}
|
|
|
|
func NewServer(cfg *config.MuyueConfig) *Server {
|
|
s := &Server{
|
|
mux: http.NewServeMux(),
|
|
}
|
|
// Auto-initialize config if nil or if no config file exists on disk
|
|
if cfg == nil || !config.Exists() {
|
|
defaultCfg := config.Default()
|
|
if cfg != nil {
|
|
// Preserve any user-provided settings from cfg
|
|
defaultCfg.Profile = cfg.Profile
|
|
defaultCfg.AI = cfg.AI
|
|
defaultCfg.Tools = cfg.Tools
|
|
defaultCfg.BMAD = cfg.BMAD
|
|
defaultCfg.Terminal = cfg.Terminal
|
|
}
|
|
// Save initial config to establish the file for first-time usage
|
|
if err := config.Save(defaultCfg); err != nil {
|
|
_ = err
|
|
}
|
|
cfg = defaultCfg
|
|
}
|
|
s.config = cfg
|
|
s.scanResult = scanner.ScanSystem()
|
|
s.convStore = NewConversationStore()
|
|
s.shellConvStore = NewShellConvStore()
|
|
s.consumption = newConsumptionStore()
|
|
s.agentRegistry = agent.DefaultRegistry()
|
|
tools := s.agentRegistry.OpenAITools()
|
|
toolsJSON, _ := json.Marshal(tools)
|
|
s.agentToolsJSON = json.RawMessage(toolsJSON)
|
|
|
|
s.shellAgentRegistry = agent.NewRegistry()
|
|
terminalTool, _ := agent.NewTerminalTool()
|
|
s.shellAgentRegistry.Register(terminalTool)
|
|
shellTools := s.shellAgentRegistry.OpenAITools()
|
|
shellToolsJSON, _ := json.Marshal(shellTools)
|
|
s.shellAgentToolsJSON = json.RawMessage(shellToolsJSON)
|
|
|
|
s.workflowEngine, _ = workflow.NewEngine(s.agentRegistry)
|
|
s.initStarship()
|
|
s.routes()
|
|
return s
|
|
}
|
|
|
|
func (s *Server) routes() {
|
|
s.mux.HandleFunc("/api/info", s.handleInfo)
|
|
s.mux.HandleFunc("/api/system", s.handleSystem)
|
|
s.mux.HandleFunc("/api/tools", s.handleTools)
|
|
s.mux.HandleFunc("/api/config", s.handleConfig)
|
|
s.mux.HandleFunc("/api/providers", s.handleProviders)
|
|
s.mux.HandleFunc("/api/skills", s.handleSkills)
|
|
s.mux.HandleFunc("/api/lsp", s.handleLSP)
|
|
s.mux.HandleFunc("/api/mcp", s.handleMCP)
|
|
s.mux.HandleFunc("/api/updates", s.handleUpdates)
|
|
s.mux.HandleFunc("/api/install", s.handleInstall)
|
|
s.mux.HandleFunc("/api/scan", s.handleScan)
|
|
s.mux.HandleFunc("/api/editors", s.handleEditors)
|
|
s.mux.HandleFunc("/api/preferences", s.handleUpdatePreferences)
|
|
s.mux.HandleFunc("/api/terminal", s.handleTerminal)
|
|
s.mux.HandleFunc("/api/ws/terminal", s.handleTerminalWS)
|
|
s.mux.HandleFunc("/api/terminal/sessions", s.handleTerminalSessions)
|
|
s.mux.HandleFunc("/api/terminal/sessions/", s.handleTerminalSessionsDelete)
|
|
s.mux.HandleFunc("/api/terminal/themes", s.handleGetTerminalThemes)
|
|
s.mux.HandleFunc("/api/terminal/settings", s.handleSaveTerminalSettings)
|
|
s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure)
|
|
s.mux.HandleFunc("/api/config/profile", s.handleSaveProfile)
|
|
s.mux.HandleFunc("/api/config/provider", s.handleSaveProvider)
|
|
s.mux.HandleFunc("/api/config/reset", s.handleResetConfig)
|
|
s.mux.HandleFunc("/api/starship/apply-theme", s.handleApplyStarshipTheme)
|
|
s.mux.HandleFunc("/api/providers/validate", s.handleValidateProvider)
|
|
s.mux.HandleFunc("/api/update/run", s.handleRunUpdate)
|
|
s.mux.HandleFunc("/api/images/", s.handleServeImage)
|
|
s.mux.HandleFunc("/api/chat", s.handleChat)
|
|
s.mux.HandleFunc("/api/chat/history", s.handleChatHistory)
|
|
s.mux.HandleFunc("/api/chat/clear", s.handleChatClear)
|
|
s.mux.HandleFunc("/api/chat/summarize", s.handleChatSummarize)
|
|
s.mux.HandleFunc("/api/tool/call", s.handleToolCall)
|
|
s.mux.HandleFunc("/api/tools/list", s.handleToolList)
|
|
s.mux.HandleFunc("/api/shell/chat", s.handleShellChat)
|
|
s.mux.HandleFunc("/api/shell/chat/history", s.handleShellChatHistory)
|
|
s.mux.HandleFunc("/api/shell/chat/clear", s.handleShellChatClear)
|
|
s.mux.HandleFunc("/api/shell/analyze", s.handleShellAnalyze)
|
|
s.mux.HandleFunc("/api/shell/analysis", s.handleShellAnalysisGet)
|
|
s.mux.HandleFunc("/api/workflow", s.handleWorkflowCreate)
|
|
s.mux.HandleFunc("/api/workflow/list", s.handleWorkflowList)
|
|
s.mux.HandleFunc("/api/workflow/", s.handleWorkflowGet)
|
|
s.mux.HandleFunc("/api/workflow/plan", s.handleWorkflowPlan)
|
|
s.mux.HandleFunc("/api/workflow/execute/", s.handleWorkflowExecute)
|
|
s.mux.HandleFunc("/api/workflow/approve/", s.handleWorkflowApprove)
|
|
s.mux.HandleFunc("/api/conversations", s.handleListConversations)
|
|
s.mux.HandleFunc("/api/conversations/search", s.handleSearchConversations)
|
|
s.mux.HandleFunc("/api/conversations/export", s.handleExportConversation)
|
|
s.mux.HandleFunc("/api/conversations/", s.handleDeleteConversation)
|
|
s.mux.HandleFunc("/api/lsp/install", s.handleLSPInstall)
|
|
s.mux.HandleFunc("/api/skills/deploy", s.handleSkillsDeploy)
|
|
s.mux.HandleFunc("/api/skills/undeploy", s.handleSkillsUndeploy)
|
|
s.mux.HandleFunc("/api/ssh/connections", s.handleSSHConnections)
|
|
s.mux.HandleFunc("/api/ssh/test", s.handleSSHTest)
|
|
|
|
s.mux.HandleFunc("/api/mcp/status", s.handleMCPStatus)
|
|
s.mux.HandleFunc("/api/mcp/registry", s.handleMCPRegistry)
|
|
s.mux.HandleFunc("/api/lsp/health", s.handleLSPHealth)
|
|
s.mux.HandleFunc("/api/lsp/auto-install", s.handleLSPAutoInstall)
|
|
s.mux.HandleFunc("/api/lsp/editor-config", s.handleLSPEditorConfig)
|
|
s.mux.HandleFunc("/api/skills/validate", s.handleSkillValidate)
|
|
s.mux.HandleFunc("/api/skills/test", s.handleSkillTest)
|
|
s.mux.HandleFunc("/api/skills/export", s.handleSkillExport)
|
|
s.mux.HandleFunc("/api/skills/import", s.handleSkillImport)
|
|
s.mux.HandleFunc("/api/dashboard/status", s.handleDashboardStatus)
|
|
s.mux.HandleFunc("/api/ai/task", s.handleAITask)
|
|
s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota)
|
|
s.mux.HandleFunc("/api/providers/consumption", s.handleProvidersConsumption)
|
|
s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands)
|
|
s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses)
|
|
s.mux.HandleFunc("/api/system/metrics", s.handleSystemMetrics)
|
|
}
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/api/ws/") || strings.HasPrefix(r.URL.Path, "/api/images/") {
|
|
s.mux.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if origin := r.Header.Get("Origin"); isAllowedOrigin(origin) {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Vary", "Origin")
|
|
}
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
s.mux.ServeHTTP(w, r)
|
|
}
|
|
|
|
func isAllowedOrigin(origin string) bool {
|
|
if origin == "" {
|
|
return false
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(origin, "http://127.0.0.1"),
|
|
strings.HasPrefix(origin, "http://localhost"),
|
|
strings.HasPrefix(origin, "http://[::1]"),
|
|
strings.HasPrefix(origin, "https://127.0.0.1"),
|
|
strings.HasPrefix(origin, "https://localhost"),
|
|
strings.HasPrefix(origin, "https://[::1]"):
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const maxCrushAgents = 2
|
|
const maxClaudeAgents = 2
|
|
|
|
func (s *Server) AcquireAgentSlot(toolName string) (release func(), err error) {
|
|
var counter *atomic.Int32
|
|
var max int32
|
|
switch toolName {
|
|
case "crush_run":
|
|
counter = &s.activeCrushAgents
|
|
max = maxCrushAgents
|
|
case "claude_run":
|
|
counter = &s.activeClaudeAgents
|
|
max = maxClaudeAgents
|
|
default:
|
|
return func() {}, nil
|
|
}
|
|
current := counter.Add(1)
|
|
if current > max {
|
|
counter.Add(-1)
|
|
return nil, fmt.Errorf("Limite de %d agents %s atteinte", max, toolName)
|
|
}
|
|
return func() { counter.Add(-1) }, nil
|
|
}
|
|
|
|
func (s *Server) initStarship() {
|
|
if _, err := exec.LookPath("starship"); err != nil {
|
|
inst := installer.New(s.config)
|
|
if result := inst.InstallTool("starship"); !result.Success {
|
|
return
|
|
}
|
|
}
|
|
ApplyStarshipTheme(s.config.Terminal.PromptTheme)
|
|
}
|