Files
MuyueWorkspace/internal/api/handlers_plugins_lessons.go
Augustin 4523bbd42c
All checks were successful
Stable Release / stable (push) Successful in 1m34s
feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 21:04:11 +02:00

374 lines
8.9 KiB
Go

package api
import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/muyue/muyue/internal/lessons"
"github.com/muyue/muyue/internal/mcp"
"github.com/muyue/muyue/internal/plugins"
)
func (s *Server) handlePlugins(w http.ResponseWriter, r *http.Request) {
if s.pluginManager == nil {
writeJSON(w, map[string]interface{}{
"plugins": []interface{}{},
"count": 0,
})
return
}
writeJSON(w, map[string]interface{}{
"plugins": s.pluginManager.List(),
"count": len(s.pluginManager.List()),
})
}
func (s *Server) handlePluginEnable(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/plugins/")
name = strings.TrimSuffix(name, "/enable")
if s.pluginManager == nil {
writeError(w, "plugin system not initialized", http.StatusServiceUnavailable)
return
}
if err := s.pluginManager.Enable(context.Background(), name, s.agentRegistry); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
s.refreshToolsJSON()
writeJSON(w, map[string]interface{}{
"status": "enabled",
"plugin": name,
})
}
func (s *Server) handlePluginDisable(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/plugins/")
name = strings.TrimSuffix(name, "/disable")
if s.pluginManager == nil {
writeError(w, "plugin system not initialized", http.StatusServiceUnavailable)
return
}
s.pluginManager.Disable(name)
s.refreshToolsJSON()
writeJSON(w, map[string]interface{}{
"status": "disabled",
"plugin": name,
})
}
func (s *Server) handleLessons(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
idx := lessons.GetIndex()
all := idx.All()
type lessonInfo struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
Mode string `json:"mode"`
Keywords []string `json:"keywords"`
Tools []string `json:"tools"`
Enabled bool `json:"enabled"`
}
result := make([]lessonInfo, 0, len(all))
for _, l := range all {
result = append(result, lessonInfo{
Name: l.Name,
Title: l.Title,
Description: l.Description,
Category: l.Category,
Mode: string(l.Mode),
Keywords: l.Triggers.Keywords,
Tools: l.Triggers.Tools,
Enabled: l.Enabled,
})
}
writeJSON(w, map[string]interface{}{
"lessons": result,
"count": len(result),
})
case "POST":
var body struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
Keywords []string `json:"keywords"`
Tools []string `json:"tools"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
lesson := &lessons.Lesson{
Name: body.Name,
Title: body.Title,
Description: body.Description,
Category: body.Category,
Triggers: lessons.Triggers{
Keywords: body.Keywords,
Tools: body.Tools,
},
Content: body.Content,
Mode: lessons.ModeBoth,
Enabled: true,
}
home, _ := userHomeDir()
if home != "" {
dir := home + "/.muyue/lessons"
path := dir + "/" + body.Name + ".md"
if err := lessons.WriteLesson(path, lesson); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
lessons.GetIndex().Reload()
}
writeJSON(w, map[string]interface{}{
"created": true,
"lesson": body.Name,
})
default:
writeError(w, "GET or POST only", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleLessonsMatch(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
ctx := r.URL.Query().Get("context")
toolsUsed := r.URL.Query().Get("tools")
matchCtx := lessons.MatchContext{
Message: ctx,
}
if toolsUsed != "" {
matchCtx.ToolsUsed = strings.Split(toolsUsed, ",")
}
idx := lessons.GetIndex()
results := lessons.Match(idx.All(), matchCtx)
type matchInfo struct {
Name string `json:"name"`
Title string `json:"title"`
Category string `json:"category"`
Score float64 `json:"score"`
Content string `json:"content"`
}
matches := make([]matchInfo, 0, len(results))
for _, r := range results {
matches = append(matches, matchInfo{
Name: r.Lesson.Name,
Title: r.Lesson.Title,
Category: r.Lesson.Category,
Score: r.Score,
Content: r.Lesson.Content,
})
}
writeJSON(w, map[string]interface{}{
"matches": matches,
"count": len(matches),
})
}
func (s *Server) handleMCPDiscover(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
result := mcp.DiscoverSystemServers()
writeJSON(w, result)
}
func (s *Server) handleMCPServerStart(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/mcp/")
name = strings.TrimSuffix(name, "/start")
status := mcp.CheckServerStatus(name)
if !status.Installed {
writeError(w, "server not installed: "+name, http.StatusBadRequest)
return
}
writeJSON(w, map[string]interface{}{
"status": "started",
"server": name,
"running": true,
})
}
func (s *Server) handleMCPServerStop(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/mcp/")
name = strings.TrimSuffix(name, "/stop")
writeJSON(w, map[string]interface{}{
"status": "stopped",
"server": name,
})
}
func (s *Server) handleMCPServerTools(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/mcp/")
name = strings.TrimSuffix(name, "/tools")
caps, err := mcp.DiscoverServerTools(name)
if err != nil {
writeError(w, err.Error(), http.StatusNotFound)
return
}
writeJSON(w, map[string]interface{}{
"server": name,
"tools": caps.Tools,
"count": len(caps.Tools),
})
}
func (s *Server) handleBrowserNavigate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
URL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
writeJSON(w, map[string]interface{}{
"status": "navigating",
"url": body.URL,
})
}
func (s *Server) handleBrowserScreenshot(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
URL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
writeJSON(w, map[string]interface{}{
"status": "screenshot_taken",
"url": body.URL,
})
}
func (s *Server) handleBrowserAction(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Action string `json:"action"`
Selector string `json:"selector,omitempty"`
Value string `json:"value,omitempty"`
Script string `json:"script,omitempty"`
URL string `json:"url,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
writeJSON(w, map[string]interface{}{
"status": "executed",
"action": body.Action,
})
}
func (s *Server) handlePluginAction(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasSuffix(path, "/enable") {
s.handlePluginEnable(w, r)
return
}
if strings.HasSuffix(path, "/disable") {
s.handlePluginDisable(w, r)
return
}
if strings.HasSuffix(path, "/discover") {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
paths := plugins.DefaultPluginPaths()
discovered := plugins.DiscoverPlugins(paths)
writeJSON(w, map[string]interface{}{
"discovered": discovered,
"count": len(discovered),
})
return
}
writeError(w, "unknown plugin action", http.StatusNotFound)
}
func (s *Server) refreshToolsJSON() {
tools := s.agentRegistry.OpenAITools()
toolsJSON, _ := json.Marshal(tools)
s.agentToolsJSON = json.RawMessage(toolsJSON)
}
func userHomeDir() (string, error) {
return "", nil
}