feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
All checks were successful
Stable Release / stable (push) Successful in 1m34s
All checks were successful
Stable Release / stable (push) Successful in 1m34s
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>
This commit is contained in:
373
internal/api/handlers_plugins_lessons.go
Normal file
373
internal/api/handlers_plugins_lessons.go
Normal file
@@ -0,0 +1,373 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user