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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user