All checks were successful
Beta Release / beta (push) Successful in 5m9s
Major additions: - RAG pipeline (indexing, chunking, search) with sidebar upload button - Memory system with CRUD API - Plugins and lessons modules - MCP discovery and MCP server - Advanced skills (auto-create, conditional, improver) - Agent browser/image support, delegate, sessions - File editor with CodeMirror in split panes - Markdown rendering via react-markdown + KaTeX + highlight.js - Raw markdown toggle - PWA manifest + service worker - Extension UI redesign with new design tokens and studio-style chat - Pipeline API for chat streaming - Mobile responsive layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
211 lines
4.8 KiB
Go
211 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|