All checks were successful
Beta Release / beta (push) Successful in 5m9s
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>
374 lines
8.9 KiB
Go
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
|
|
}
|