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:
133
internal/api/agent_sessions.go
Normal file
133
internal/api/agent_sessions.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
336
internal/api/handlers_files.go
Normal file
336
internal/api/handlers_files.go
Normal 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
|
||||
}
|
||||
52
internal/api/handlers_image.go
Normal file
52
internal/api/handlers_image.go
Normal 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))
|
||||
}
|
||||
256
internal/api/handlers_memory.go
Normal file
256
internal/api/handlers_memory.go
Normal 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,
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
268
internal/api/handlers_rag.go
Normal file
268
internal/api/handlers_rag.go
Normal 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})
|
||||
}
|
||||
210
internal/api/handlers_skills_advanced.go
Normal file
210
internal/api/handlers_skills_advanced.go
Normal 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
283
internal/api/pipeline.go
Normal 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})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user