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

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:
Augustin
2026-04-27 21:01:08 +02:00
parent 62c20eb174
commit 4523bbd42c
50 changed files with 11144 additions and 469 deletions

View File

@@ -0,0 +1,133 @@
package api
import (
"fmt"
"os"
"strings"
"sync"
"time"
)
type AgentSession struct {
ID string `json:"id"`
Type string `json:"type"`
PID int `json:"pid"`
Command string `json:"command"`
StartedAt string `json:"started_at"`
Status string `json:"status"`
Output string `json:"output,omitempty"`
Cwd string `json:"cwd,omitempty"`
}
type AgentSessionTracker struct {
mu sync.RWMutex
sessions map[string]*AgentSession
}
func NewAgentSessionTracker() *AgentSessionTracker {
return &AgentSessionTracker{
sessions: make(map[string]*AgentSession),
}
}
func (t *AgentSessionTracker) Discover() []AgentSession {
t.mu.Lock()
defer t.mu.Unlock()
activePIDs := make(map[int]bool)
for _, s := range t.sessions {
activePIDs[s.PID] = true
}
for _, name := range []string{"crush", "claude"} {
pids := findProcessesByName(name)
for _, pid := range pids {
if !activePIDs[pid] {
session := &AgentSession{
ID: fmt.Sprintf("%s-%d-%d", name, pid, time.Now().UnixMilli()),
Type: name,
PID: pid,
Command: getProcessCommand(pid),
StartedAt: time.Now().Format(time.RFC3339),
Status: "running",
}
t.sessions[session.ID] = session
}
}
}
var result []AgentSession
for _, s := range t.sessions {
if s.Status == "running" {
if !isProcessAlive(s.PID) {
s.Status = "completed"
}
}
result = append(result, *s)
}
return result
}
func (t *AgentSessionTracker) Get(id string) *AgentSession {
t.mu.RLock()
defer t.mu.RUnlock()
s, ok := t.sessions[id]
if !ok {
return nil
}
snapshot := *s
return &snapshot
}
func findProcessesByName(name string) []int {
data, err := os.ReadFile("/proc/" + name + "/stat")
_ = data
_ = err
var pids []int
entries, err := os.ReadDir("/proc")
if err != nil {
return pids
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
var pid int
if _, err := fmt.Sscanf(entry.Name(), "%d", &pid); err != nil {
continue
}
if pid <= 0 || pid == os.Getpid() {
continue
}
cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if err != nil {
continue
}
cmdStr := string(cmdline)
if strings.Contains(cmdStr, name) {
pids = append(pids, pid)
}
}
return pids
}
func getProcessCommand(pid int) string {
out, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if err != nil {
return ""
}
return strings.ReplaceAll(string(out), "\x00", " ")
}
func isProcessAlive(pid int) bool {
_, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
return err == nil
}

View File

@@ -213,6 +213,13 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
orb.SetSystemPrompt(studioPrompt.String())
orb.SetTools(s.agentToolsJSON)
if memBlock := s.buildMemoryContext(enrichedMessage); memBlock != "" {
orb.AppendHistory(orchestrator.Message{
Role: "system",
Content: orchestrator.TextContent(memBlock),
})
}
// Auto-force advanced reflection while a browser-test session is active:
// the user is doing AI-driven UI testing, where having a second model
// produce a preliminary report (when one is configured) materially

View File

@@ -0,0 +1,336 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/mcpserver"
)
func (s *Server) handleFileContent(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
path := r.URL.Query().Get("path")
if path == "" {
writeError(w, "path parameter required", http.StatusBadRequest)
return
}
home, _ := os.UserHomeDir()
path = strings.ReplaceAll(path, "~", home)
if !filepath.IsAbs(path) {
writeError(w, "path must be absolute", http.StatusBadRequest)
return
}
data, err := os.ReadFile(path)
if err != nil {
writeError(w, fmt.Sprintf("Error reading file: %v", err), http.StatusNotFound)
return
}
ext := strings.ToLower(filepath.Ext(path))
lang := "text"
switch ext {
case ".go":
lang = "go"
case ".js", ".jsx":
lang = "javascript"
case ".ts", ".tsx":
lang = "typescript"
case ".py":
lang = "python"
case ".json":
lang = "json"
case ".yaml", ".yml":
lang = "yaml"
case ".md":
lang = "markdown"
case ".css":
lang = "css"
case ".html":
lang = "html"
case ".sh", ".bash":
lang = "shell"
case ".rs":
lang = "rust"
case ".java":
lang = "java"
}
stat, _ := os.Stat(path)
modTime := ""
if stat != nil {
modTime = stat.ModTime().Format("2006-01-02T15:04:05Z07:00")
}
writeJSON(w, map[string]interface{}{
"path": path,
"content": string(data),
"lang": lang,
"size": len(data),
"modTime": modTime,
})
case http.MethodPut:
var body struct {
Path string `json:"path"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if body.Path == "" {
writeError(w, "path required", http.StatusBadRequest)
return
}
home, _ := os.UserHomeDir()
path := strings.ReplaceAll(body.Path, "~", home)
if !filepath.IsAbs(path) {
writeError(w, "path must be absolute", http.StatusBadRequest)
return
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
writeError(w, fmt.Sprintf("Error creating directory: %v", err), http.StatusInternalServerError)
return
}
if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil {
writeError(w, fmt.Sprintf("Error writing file: %v", err), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"status": "ok",
"path": path,
"size": len(body.Content),
})
default:
writeError(w, "GET/PUT only", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleMuyueMCPServerStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]interface{}{
"enabled": s.mcpServer != nil,
"running": s.mcpServer != nil,
"port": s.getMCPServerPort(),
})
}
func (s *Server) handleMuyueMCPServerStart(w http.ResponseWriter, r *http.Request) {
if s.mcpServer != nil {
writeJSON(w, map[string]string{"status": "already_running"})
return
}
s.startMCPServer()
writeJSON(w, map[string]interface{}{
"status": "started",
"port": s.getMCPServerPort(),
})
}
func (s *Server) handleMuyueMCPServerStop(w http.ResponseWriter, r *http.Request) {
if s.mcpServer == nil {
writeJSON(w, map[string]string{"status": "not_running"})
return
}
s.mcpServer.Stop()
s.mcpServer = nil
writeJSON(w, map[string]string{"status": "stopped"})
}
func (s *Server) getMCPServerPort() int {
if s.mcpServer == nil {
return 0
}
return s.mcpServer.Port()
}
func (s *Server) startMCPServer() {
port := 8096
if s.config != nil {
}
s.mcpServer = mcpserver.New(port)
s.mcpServer.Start()
}
func (s *Server) handleAgentSessionsList(w http.ResponseWriter, r *http.Request) {
sessions := s.agentTracker.Discover()
writeJSON(w, map[string]interface{}{
"sessions": sessions,
})
}
func (s *Server) handleAgentSessionOutput(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/agent-sessions/")
if id == "" {
writeError(w, "session id required", http.StatusBadRequest)
return
}
session := s.agentTracker.Get(id)
if session == nil {
writeError(w, "session not found", http.StatusNotFound)
return
}
writeJSON(w, session)
}
func (s *Server) handleWorkspaceList(w http.ResponseWriter, r *http.Request) {
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
writeJSON(w, map[string]interface{}{"workspaces": []interface{}{}})
return
}
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
var workspaces []map[string]interface{}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
name := strings.TrimSuffix(entry.Name(), ".json")
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
var ws map[string]interface{}
if err := json.Unmarshal(data, &ws); err != nil {
continue
}
ws["name"] = name
workspaces = append(workspaces, ws)
}
if workspaces == nil {
workspaces = []map[string]interface{}{}
}
writeJSON(w, map[string]interface{}{"workspaces": workspaces})
}
func (s *Server) handleWorkspaceSave(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Name string `json:"name"`
Layout string `json:"layout"`
Tabs string `json:"tabs"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if body.Name == "" {
writeError(w, "name required", http.StatusBadRequest)
return
}
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
wsData := map[string]interface{}{
"name": body.Name,
"layout": body.Layout,
"tabs": body.Tabs,
"updated": fmt.Sprintf("%d", time.Now().Unix()),
}
data, err := json.MarshalIndent(wsData, "", " ")
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.WriteFile(filepath.Join(dir, body.Name+".json"), data, 0644); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"status": "ok"})
}
func (s *Server) handleWorkspaceGet(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/api/workspace/")
if name == "" {
writeError(w, "name required", http.StatusBadRequest)
return
}
if r.Method == "DELETE" {
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if err := os.Remove(filepath.Join(dir, name+".json")); err != nil {
writeError(w, "workspace not found", http.StatusNotFound)
return
}
writeJSON(w, map[string]string{"status": "ok"})
return
}
dir, err := configWorkspacesDir()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := os.ReadFile(filepath.Join(dir, name+".json"))
if err != nil {
writeError(w, "workspace not found", http.StatusNotFound)
return
}
var result map[string]interface{}
json.Unmarshal(data, &result)
writeJSON(w, result)
}
func configWorkspacesDir() (string, error) {
configDir, err := config.ConfigDir()
if err != nil {
return "", err
}
dir := filepath.Join(configDir, "workspaces")
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("create workspaces dir: %w", err)
}
return dir, nil
}

View File

@@ -0,0 +1,52 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/muyue/muyue/internal/agent"
)
func (s *Server) handleImageGenerate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
Prompt string `json:"prompt"`
Size string `json:"size"`
Style string `json:"style"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "invalid request: "+err.Error())
return
}
if req.Prompt == "" {
jsonError(w, "prompt is required")
return
}
imgTool, err := agent.NewImageGenerationTool(s.config)
if err != nil {
jsonError(w, "image tool init: "+err.Error())
return
}
args := map[string]interface{}{
"prompt": req.Prompt,
"size": req.Size,
"style": req.Style,
}
result, err := imgTool.Execute(args)
if err != nil {
jsonError(w, fmt.Sprintf("generation failed: %v", err))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(result))
}

View File

@@ -0,0 +1,256 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/muyue/muyue/internal/memory"
)
func (s *Server) ensureMemoryStore() (*memory.Store, error) {
if s.memoryStore == nil {
store, err := memory.NewStore()
if err != nil {
return nil, err
}
s.memoryStore = store
}
return s.memoryStore, nil
}
func (s *Server) handleMemoryList(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
var memType memory.MemoryType
if t := r.URL.Query().Get("type"); t != "" {
memType = memory.MemoryType(t)
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
memories, err := store.List(memType, limit, offset)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
count, _ := store.Count()
writeJSON(w, map[string]interface{}{
"memories": memories,
"count": len(memories),
"total": count,
})
}
func (s *Server) handleMemoryCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Type string `json:"type"`
Key string `json:"key"`
Content string `json:"content"`
Tags string `json:"tags,omitempty"`
Source string `json:"source,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
if body.Key == "" || body.Content == "" {
writeError(w, "key and content are required", http.StatusBadRequest)
return
}
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
memType := memory.MemoryType(body.Type)
if memType == "" {
memType = memory.TypeFact
}
m := &memory.Memory{
Type: memType,
Key: body.Key,
Content: body.Content,
Tags: body.Tags,
Source: body.Source,
Confidence: body.Confidence,
}
if m.Confidence == 0 {
m.Confidence = 0.5
}
if err := store.Store(m); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"created": true,
"memory": m,
})
}
func (s *Server) handleMemoryDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
writeError(w, "DELETE only", http.StatusMethodNotAllowed)
return
}
id := strings.TrimPrefix(r.URL.Path, "/api/memory/")
if id == "" {
writeError(w, "memory id required", http.StatusBadRequest)
return
}
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
if err := store.Delete(id); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"deleted": true,
"id": id,
})
}
func (s *Server) handleMemoryOperation(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/memory/")
if path == "" {
s.handleMemoryList(w, r)
return
}
switch r.Method {
case "DELETE":
s.handleMemoryDelete(w, r)
case "GET":
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
m, err := store.Get(path)
if err != nil {
writeError(w, err.Error(), http.StatusNotFound)
return
}
writeJSON(w, m)
default:
writeError(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleMemorySearch(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query().Get("q")
if query == "" {
writeError(w, "query parameter 'q' is required", http.StatusBadRequest)
return
}
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
results, err := store.Search(query, limit)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"results": results,
"count": len(results),
"query": query,
})
}
func (s *Server) handleMemoryRecall(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query().Get("q")
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
injector := memory.NewInjector(store)
contextBlock, err := injector.BuildContextBlock(query)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"context": contextBlock,
"query": query,
})
}
func (s *Server) handleMemoryContext(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
store, err := s.ensureMemoryStore()
if err != nil {
writeError(w, "memory store: "+err.Error(), http.StatusInternalServerError)
return
}
preferences, _ := store.RecallPreferences()
facts, _ := store.RecallFacts()
recentCutoff := time.Now().Add(-24 * time.Hour)
recent, _ := store.RecallRecent(recentCutoff, 10)
writeJSON(w, map[string]interface{}{
"preferences": preferences,
"facts": facts,
"recent": recent,
})
}

View 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
}

View File

@@ -0,0 +1,268 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/rag"
)
func (s *Server) handleRAGIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
s.ensureRAGStore()
if r.Header.Get("Content-Type") == "application/json" {
var req struct {
Text string `json:"text"`
Name string `json:"name"`
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "invalid request: "+err.Error())
return
}
if req.Text == "" {
jsonError(w, "text is required")
return
}
if req.Name == "" {
req.Name = "document-" + time.Now().Format("20060102-150405")
}
if req.Type == "" {
req.Type = "text"
}
s.indexText(w, req.Text, req.Name, req.Type)
return
}
if err := r.ParseMultipartForm(32 << 20); err != nil {
jsonError(w, "invalid multipart: "+err.Error())
return
}
file, header, err := r.FormFile("file")
if err != nil {
jsonError(w, "file is required")
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
jsonError(w, "reading file: "+err.Error())
return
}
name := header.Filename
ext := strings.ToLower(filepath.Ext(name))
docType := "text"
switch ext {
case ".md", ".markdown":
docType = "markdown"
case ".go", ".js", ".ts", ".py", ".java", ".rs", ".jsx", ".tsx":
docType = "code"
}
s.indexText(w, string(data), name, docType)
}
func (s *Server) indexText(w http.ResponseWriter, text, name, docType string) {
var chunks []rag.Chunk
switch docType {
case "markdown":
chunks = rag.ChunkMarkdown(text, 500)
case "code":
lang := strings.TrimPrefix(filepath.Ext(name), ".")
chunks = rag.ChunkCode(text, lang, 300)
default:
chunks = rag.ChunkText(text, 500)
}
if len(chunks) == 0 {
jsonError(w, "no content to index")
return
}
docID := uuid.New().String()[:8]
doc := rag.Document{
ID: docID,
Name: name,
Type: docType,
Chunks: len(chunks),
IndexedAt: time.Now(),
Size: int64(len(text)),
}
var chunkRecords []rag.ChunkRecord
var texts []string
for _, c := range chunks {
texts = append(texts, c.Content)
chunkRecords = append(chunkRecords, rag.ChunkRecord{
DocumentID: docID,
Content: c.Content,
StartPos: c.StartPos,
EndPos: c.EndPos,
Metadata: c.Metadata,
})
}
embClient := s.getEmbeddingClient()
if embClient != nil {
embeddings, err := embClient.Embed(texts, "")
if err == nil {
for i := range chunkRecords {
if i < len(embeddings) {
chunkRecords[i].Embedding = embeddings[i]
}
}
}
}
if err := s.ragStore.StoreDocument(doc, chunkRecords); err != nil {
jsonError(w, "storing document: "+err.Error())
return
}
jsonResp(w, map[string]interface{}{
"id": docID,
"name": name,
"chunks": len(chunks),
"type": docType,
})
}
func (s *Server) handleRAGSearch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
s.ensureRAGStore()
var req struct {
Query string `json:"query"`
Limit int `json:"limit"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "invalid request: "+err.Error())
return
}
if req.Query == "" {
jsonError(w, "query is required")
return
}
if req.Limit <= 0 {
req.Limit = 5
}
embClient := s.getEmbeddingClient()
var results []rag.SearchResult
var err error
if embClient != nil {
queryEmb, embErr := embClient.EmbedSingle(req.Query, "")
if embErr == nil {
results, err = s.ragStore.Search(queryEmb, req.Limit)
}
}
if err != nil || len(results) == 0 {
results, err = s.ragStore.SearchKeyword(req.Query, req.Limit)
if err != nil {
jsonError(w, "search error: "+err.Error())
return
}
}
jsonResp(w, map[string]interface{}{
"results": results,
"query": req.Query,
"count": len(results),
})
}
func (s *Server) handleRAGStatus(w http.ResponseWriter, r *http.Request) {
s.ensureRAGStore()
status, err := s.ragStore.Status()
if err != nil {
jsonError(w, "status error: "+err.Error())
return
}
jsonResp(w, status)
}
func (s *Server) handleRAGDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
s.ensureRAGStore()
id := strings.TrimPrefix(r.URL.Path, "/api/rag/index/")
if id == "" {
jsonError(w, "document id is required")
return
}
if err := s.ragStore.DeleteDocument(id); err != nil {
jsonError(w, "delete error: "+err.Error())
return
}
jsonResp(w, map[string]interface{}{"deleted": id})
}
func (s *Server) ensureRAGStore() {
if s.ragStore != nil {
return
}
configDir, err := config.ConfigDir()
if err != nil {
return
}
store, err := rag.NewStore(configDir)
if err != nil {
fmt.Fprintf(os.Stderr, "RAG store init error: %v\n", err)
return
}
s.ragStore = store
}
func (s *Server) getEmbeddingClient() *rag.EmbeddingClient {
for _, p := range s.config.AI.Providers {
if p.Active && p.APIKey != "" {
baseURL := p.BaseURL
if baseURL == "" {
baseURL = "https://api.openai.com/v1"
}
return rag.NewEmbeddingClient(p.APIKey, baseURL)
}
}
return nil
}
func (s *Server) handleRAGDocuments(w http.ResponseWriter, r *http.Request) {
s.ensureRAGStore()
docs, err := s.ragStore.ListDocuments()
if err != nil {
jsonError(w, "list error: "+err.Error())
return
}
if docs == nil {
docs = []rag.Document{}
}
jsonResp(w, map[string]interface{}{"documents": docs})
}

View File

@@ -0,0 +1,210 @@
package api
import (
"encoding/json"
"net/http"
"strings"
"github.com/muyue/muyue/internal/skills"
)
func (s *Server) handleSkillAutoCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
var body struct {
Snippets []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"snippets"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
var snippets []skills.ConversationSnippet
for _, s := range body.Snippets {
snippets = append(snippets, skills.ConversationSnippet{
Role: s.Role,
Content: s.Content,
})
}
proposals := skills.AnalyzeConversation(snippets)
var results []map[string]interface{}
for i := range proposals {
p := &proposals[i]
if err := skills.SaveProposal(p); err != nil {
continue
}
results = append(results, map[string]interface{}{
"name": p.Name,
"description": p.Description,
"confidence": p.Confidence,
"category": p.Category,
"tags": p.SuggestedTags,
})
}
writeJSON(w, map[string]interface{}{
"proposals": results,
"count": len(results),
})
}
func (s *Server) handleSkillDetail(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/skills/detail/")
if strings.HasSuffix(path, "/improve") {
name := strings.TrimSuffix(path, "/improve")
s.handleSkillImprove(w, r, name)
return
}
if strings.HasSuffix(path, "/history") {
name := strings.TrimSuffix(path, "/history")
s.handleSkillHistoryGet(w, r, name)
return
}
writeError(w, "unknown skill action", http.StatusNotFound)
}
func (s *Server) handleSkillImprove(w http.ResponseWriter, r *http.Request, name string) {
if r.Method != "POST" {
writeError(w, "POST only", http.StatusMethodNotAllowed)
return
}
skill, err := skills.Get(name)
if err != nil {
writeError(w, err.Error(), http.StatusNotFound)
return
}
var body struct {
Context string `json:"context,omitempty"`
Apply bool `json:"apply,omitempty"`
}
if r.Body != nil {
json.NewDecoder(r.Body).Decode(&body)
}
improver, err := skills.NewSkillImprover()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
suggestions, err := improver.Analyze(skill, body.Context)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if body.Apply && len(suggestions) > 0 {
if err := improver.ApplyImprovement(name, suggestions[0]); err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
updated, _ := skills.Get(name)
writeJSON(w, map[string]interface{}{
"applied": true,
"suggestion": suggestions[0],
"updated": updated,
})
return
}
writeJSON(w, map[string]interface{}{
"skill": skill.Name,
"suggestions": suggestions,
"count": len(suggestions),
})
}
func (s *Server) handleSkillHistoryGet(w http.ResponseWriter, r *http.Request, name string) {
if r.Method != "GET" {
writeError(w, "GET only", http.StatusMethodNotAllowed)
return
}
improver, err := skills.NewSkillImprover()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
history, err := improver.GetHistory(name)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"skill": name,
"history": history,
"count": len(history),
})
}
func (s *Server) handleSkillProposals(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
proposals, err := skills.LoadProposals()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"proposals": proposals,
"count": len(proposals),
})
case "POST":
var body struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, "invalid request body", http.StatusBadRequest)
return
}
proposals, err := skills.LoadProposals()
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
var target *skills.AutoCreateProposal
for i := range proposals {
if proposals[i].Name == body.Name {
target = &proposals[i]
break
}
}
if target == nil {
writeError(w, "proposal not found", http.StatusNotFound)
return
}
skill, err := skills.CreateFromProposal(target)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
skills.DeleteProposal(body.Name)
writeJSON(w, map[string]interface{}{
"created": true,
"skill": skill,
})
default:
writeError(w, "GET or POST only", http.StatusMethodNotAllowed)
}
}

283
internal/api/pipeline.go Normal file
View File

@@ -0,0 +1,283 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
type Filter interface {
Name() string
Process(ctx context.Context, req *FilterRequest) (*FilterResponse, error)
}
type FilterRequest struct {
UserMessage string `json:"user_message"`
Provider string `json:"provider"`
Model string `json:"model"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type FilterResponse struct {
Allowed bool `json:"allowed"`
Modified string `json:"modified,omitempty"`
Reason string `json:"reason,omitempty"`
TokenCount int `json:"token_count,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type Pipeline struct {
mu sync.RWMutex
filters map[string]Filter
enabled map[string]bool
stats map[string]*FilterStats
}
type FilterStats struct {
Invocations int64 `json:"invocations"`
Blocked int64 `json:"blocked"`
LastUsed time.Time `json:"last_used"`
}
func NewPipeline() *Pipeline {
p := &Pipeline{
filters: make(map[string]Filter),
enabled: make(map[string]bool),
stats: make(map[string]*FilterStats),
}
p.Register(&RateLimitFilter{})
p.Register(&TokenCountFilter{})
p.Register(&LoggingFilter{})
p.Register(&ToxicityFilter{})
for name := range p.filters {
p.enabled[name] = true
}
return p
}
func (p *Pipeline) Register(f Filter) {
p.mu.Lock()
defer p.mu.Unlock()
p.filters[f.Name()] = f
p.stats[f.Name()] = &FilterStats{}
}
func (p *Pipeline) Run(ctx context.Context, req *FilterRequest) (string, error) {
p.mu.RLock()
defer p.mu.RUnlock()
for name, filter := range p.filters {
if !p.enabled[name] {
continue
}
resp, err := filter.Process(ctx, req)
if p.stats[name] != nil {
p.stats[name].Invocations++
p.stats[name].LastUsed = time.Now()
}
if err != nil {
continue
}
if !resp.Allowed {
if p.stats[name] != nil {
p.stats[name].Blocked++
}
return "", fmt.Errorf("blocked by filter %s: %s", name, resp.Reason)
}
if resp.Modified != "" {
req.UserMessage = resp.Modified
}
}
return req.UserMessage, nil
}
func (p *Pipeline) Toggle(name string, enabled bool) error {
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := p.filters[name]; !ok {
return fmt.Errorf("filter not found: %s", name)
}
p.enabled[name] = enabled
return nil
}
func (p *Pipeline) IsEnabled(name string) bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.enabled[name]
}
func (p *Pipeline) ListFilters() []map[string]interface{} {
p.mu.RLock()
defer p.mu.RUnlock()
var result []map[string]interface{}
for name, filter := range p.filters {
entry := map[string]interface{}{
"name": name,
"enabled": p.enabled[name],
}
if stats, ok := p.stats[name]; ok {
entry["invocations"] = stats.Invocations
entry["blocked"] = stats.Blocked
entry["last_used"] = stats.LastUsed
}
_ = filter
result = append(result, entry)
}
return result
}
// ── Built-in Filters ──
type RateLimitFilter struct {
mu sync.Mutex
counters map[string][]time.Time
}
func (f *RateLimitFilter) Name() string { return "rate_limit" }
func (f *RateLimitFilter) Process(ctx context.Context, req *FilterRequest) (*FilterResponse, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.counters == nil {
f.counters = make(map[string][]time.Time)
}
key := req.Provider
now := time.Now()
cutoff := now.Add(-time.Minute)
var recent []time.Time
for _, t := range f.counters[key] {
if t.After(cutoff) {
recent = append(recent, t)
}
}
recent = append(recent, now)
f.counters[key] = recent
limit := 30
if len(recent) > limit {
return &FilterResponse{
Allowed: false,
Reason: fmt.Sprintf("rate limit exceeded: %d requests/minute (limit: %d)", len(recent), limit),
}, nil
}
return &FilterResponse{Allowed: true}, nil
}
type TokenCountFilter struct{}
func (f *TokenCountFilter) Name() string { return "token_count" }
func (f *TokenCountFilter) Process(ctx context.Context, req *FilterRequest) (*FilterResponse, error) {
count := len(req.UserMessage) / 4
if count > 50000 {
return &FilterResponse{
Allowed: true,
TokenCount: count,
Reason: fmt.Sprintf("large message: ~%d tokens", count),
}, nil
}
return &FilterResponse{Allowed: true, TokenCount: count}, nil
}
type LoggingFilter struct{}
func (f *LoggingFilter) Name() string { return "logging" }
func (f *LoggingFilter) Process(ctx context.Context, req *FilterRequest) (*FilterResponse, error) {
return &FilterResponse{Allowed: true, Metadata: map[string]string{
"provider": req.Provider,
"model": req.Model,
}}, nil
}
type ToxicityFilter struct{}
func (f *ToxicityFilter) Name() string { return "toxicity" }
func (f *ToxicityFilter) Process(ctx context.Context, req *FilterRequest) (*FilterResponse, error) {
return &FilterResponse{Allowed: true}, nil
}
// ── Pipeline HTTP handlers ──
func (s *Server) handlePipelineFilters(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
filters := s.pipeline.ListFilters()
if filters == nil {
filters = []map[string]interface{}{}
}
jsonResp(w, map[string]interface{}{"filters": filters})
return
}
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
func (s *Server) handlePipelineToggle(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
name := ""
if parts := splitPath(r.URL.Path); len(parts) > 0 {
name = parts[len(parts)-1]
}
if strings.HasSuffix(r.URL.Path, "/toggle") {
name = strings.TrimSuffix(name, "/toggle")
}
var req struct {
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "invalid request")
return
}
if err := s.pipeline.Toggle(name, req.Enabled); err != nil {
jsonError(w, err.Error())
return
}
jsonResp(w, map[string]interface{}{"name": name, "enabled": req.Enabled})
}
func splitPath(p string) []string {
var parts []string
for _, s := range strings.Split(p, "/") {
if s != "" {
parts = append(parts, s)
}
}
return parts
}
func jsonResp(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func jsonError(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

View File

@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -11,6 +12,11 @@ import (
"github.com/muyue/muyue/internal/agent"
"github.com/muyue/muyue/internal/config"
"github.com/muyue/muyue/internal/installer"
"github.com/muyue/muyue/internal/lessons"
"github.com/muyue/muyue/internal/memory"
"github.com/muyue/muyue/internal/mcpserver"
"github.com/muyue/muyue/internal/plugins"
"github.com/muyue/muyue/internal/rag"
"github.com/muyue/muyue/internal/scanner"
"github.com/muyue/muyue/internal/workflow"
)
@@ -27,9 +33,16 @@ type Server struct {
shellAgentRegistry *agent.Registry
shellAgentToolsJSON json.RawMessage
workflowEngine *workflow.Engine
pluginManager *plugins.Manager
hookRegistry *plugins.HookRegistry
browserTestStore *BrowserTestStore
memoryStore *memory.Store
ragStore *rag.Store
pipeline *Pipeline
activeCrushAgents atomic.Int32
activeClaudeAgents atomic.Int32
mcpServer *mcpserver.MCPServer
agentTracker *AgentSessionTracker
}
func NewServer(cfg *config.MuyueConfig) *Server {
@@ -76,6 +89,33 @@ func NewServer(cfg *config.MuyueConfig) *Server {
s.shellAgentToolsJSON = json.RawMessage(shellToolsJSON)
s.workflowEngine, _ = workflow.NewEngine(s.agentRegistry)
if cfg.Lessons.Enabled {
lessons.EnsureBuiltinLessons()
}
s.hookRegistry = plugins.NewHookRegistry()
s.pluginManager = plugins.NewManager(s.hookRegistry)
pluginPaths := cfg.Plugins.Paths
if len(pluginPaths) == 0 {
pluginPaths = plugins.DefaultPluginPaths()
}
discovered := plugins.DiscoverPlugins(pluginPaths)
for _, dp := range discovered {
if dp.Valid {
p, err := plugins.LoadExecutablePlugin(dp)
if err == nil {
s.pluginManager.Register(p)
}
}
}
s.pluginManager.EnableFromConfig(context.Background(), cfg.Plugins.Enabled, s.agentRegistry)
s.pipeline = NewPipeline()
s.agentTracker = NewAgentSessionTracker()
s.initStarship()
s.routes()
return s
@@ -108,6 +148,7 @@ func (s *Server) routes() {
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/generate", s.handleImageGenerate)
s.mux.HandleFunc("/api/images/", s.handleServeImage)
s.mux.HandleFunc("/api/chat", s.handleChat)
s.mux.HandleFunc("/api/chat/history", s.handleChatHistory)
@@ -157,6 +198,41 @@ func (s *Server) routes() {
s.mux.HandleFunc("/api/test/sessions", s.handleBrowserTestSessions)
s.mux.HandleFunc("/api/test/console/", s.handleBrowserTestConsole)
s.mux.HandleFunc("/api/ws/browser-test", s.handleBrowserTestWS)
s.mux.HandleFunc("/api/skills/auto-create", s.handleSkillAutoCreate)
s.mux.HandleFunc("/api/skills/proposals", s.handleSkillProposals)
s.mux.HandleFunc("/api/skills/detail/", s.handleSkillDetail)
s.mux.HandleFunc("/api/plugins", s.handlePlugins)
s.mux.HandleFunc("/api/plugins/", s.handlePluginAction)
s.mux.HandleFunc("/api/lessons", s.handleLessons)
s.mux.HandleFunc("/api/lessons/match", s.handleLessonsMatch)
s.mux.HandleFunc("/api/mcp/discover", s.handleMCPDiscover)
s.mux.HandleFunc("/api/browser/navigate", s.handleBrowserNavigate)
s.mux.HandleFunc("/api/browser/screenshot", s.handleBrowserScreenshot)
s.mux.HandleFunc("/api/browser/action", s.handleBrowserAction)
s.mux.HandleFunc("/api/rag/index", s.handleRAGIndex)
s.mux.HandleFunc("/api/rag/search", s.handleRAGSearch)
s.mux.HandleFunc("/api/rag/status", s.handleRAGStatus)
s.mux.HandleFunc("/api/rag/documents", s.handleRAGDocuments)
s.mux.HandleFunc("/api/rag/index/", s.handleRAGDelete)
s.mux.HandleFunc("/api/pipeline/filters", s.handlePipelineFilters)
s.mux.HandleFunc("/api/pipeline/filters/", s.handlePipelineToggle)
s.mux.HandleFunc("/api/memory", s.handleMemoryList)
s.mux.HandleFunc("/api/memory/create", s.handleMemoryCreate)
s.mux.HandleFunc("/api/memory/", s.handleMemoryOperation)
s.mux.HandleFunc("/api/memory/search", s.handleMemorySearch)
s.mux.HandleFunc("/api/memory/recall", s.handleMemoryRecall)
s.mux.HandleFunc("/api/memory/context", s.handleMemoryContext)
s.mux.HandleFunc("/api/files/content", s.handleFileContent)
s.mux.HandleFunc("/api/mcp-server/status", s.handleMuyueMCPServerStatus)
s.mux.HandleFunc("/api/mcp-server/start", s.handleMuyueMCPServerStart)
s.mux.HandleFunc("/api/mcp-server/stop", s.handleMuyueMCPServerStop)
s.mux.HandleFunc("/api/agent-sessions", s.handleAgentSessionsList)
s.mux.HandleFunc("/api/agent-sessions/", s.handleAgentSessionOutput)
s.mux.HandleFunc("/api/workspaces", s.handleWorkspaceList)
s.mux.HandleFunc("/api/workspace", s.handleWorkspaceSave)
s.mux.HandleFunc("/api/workspace/", s.handleWorkspaceGet)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -227,3 +303,16 @@ func (s *Server) initStarship() {
}
ApplyStarshipTheme(s.config.Terminal.PromptTheme)
}
func (s *Server) buildMemoryContext(query string) string {
store, err := s.ensureMemoryStore()
if err != nil {
return ""
}
injector := memory.NewInjector(store)
ctx, err := injector.BuildContextBlock(query)
if err != nil {
return ""
}
return ctx
}