feat(dashboard): add quota monitoring, process list, and command history
All checks were successful
Beta Release / beta (push) Successful in 44s
All checks were successful
Beta Release / beta (push) Successful in 44s
- New API endpoints: /providers/quota, /recent-commands, /running-processes - New grid-based dashboard layout with cards for tools, quota, processes, commands - Improved OnboardingWizard with required API key validation and scanning feedback - Auto-initialize config on first run 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
@@ -2,8 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
@@ -415,3 +421,191 @@ func (s *Server) handleEditors(w http.ResponseWriter, r *http.Request) {
|
||||
editors := scanner.ScanEditors()
|
||||
writeJSON(w, map[string]interface{}{"editors": editors})
|
||||
}
|
||||
|
||||
func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
||||
type providerQuota struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
var results []providerQuota
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
for _, p := range s.config.AI.Providers {
|
||||
q := providerQuota{Name: p.Name, Active: p.Active}
|
||||
switch p.Name {
|
||||
case "minimax":
|
||||
if p.APIKey == "" {
|
||||
q.Error = "no API key"
|
||||
results = append(results, q)
|
||||
continue
|
||||
}
|
||||
req, _ := http.NewRequest("GET", "https://api.minimax.io/v1/token_plan/remains", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
q.Error = err.Error()
|
||||
} else {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
var data map[string]interface{}
|
||||
if json.Unmarshal(body, &data) == nil {
|
||||
if models, ok := data["model_remains"].([]interface{}); ok {
|
||||
filtered := make([]map[string]interface{}, 0)
|
||||
for _, m := range models {
|
||||
if mm, ok := m.(map[string]interface{}); ok {
|
||||
usage, _ := mm["current_interval_usage_count"].(float64)
|
||||
total, _ := mm["current_interval_total_count"].(float64)
|
||||
if total > 0 {
|
||||
filtered = append(filtered, map[string]interface{}{
|
||||
"model": mm["model_name"],
|
||||
"used": usage,
|
||||
"total": total,
|
||||
"remaining": total - usage,
|
||||
"weekly_used": mm["current_weekly_usage_count"],
|
||||
"weekly_total": mm["current_weekly_total_count"],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Data = map[string]interface{}{"models": filtered}
|
||||
q.Healthy = true
|
||||
}
|
||||
}
|
||||
}
|
||||
case "zai":
|
||||
if p.APIKey == "" {
|
||||
q.Error = "no API key"
|
||||
results = append(results, q)
|
||||
continue
|
||||
}
|
||||
req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
q.Error = err.Error()
|
||||
} else {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
var data map[string]interface{}
|
||||
if json.Unmarshal(body, &data) == nil {
|
||||
q.Data = data
|
||||
q.Healthy = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
q.Error = "quota not supported"
|
||||
}
|
||||
results = append(results, q)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"providers": results})
|
||||
}
|
||||
|
||||
func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) {
|
||||
home, _ := os.UserHomeDir()
|
||||
type cmdEntry struct {
|
||||
Cmd string `json:"cmd"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
|
||||
var entries []cmdEntry
|
||||
|
||||
for _, histFile := range []string{".bash_history", ".zsh_history"} {
|
||||
path := filepath.Join(home, histFile)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shell := "bash"
|
||||
if strings.Contains(histFile, "zsh") {
|
||||
shell = "zsh"
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
start := len(lines) - 25
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i := len(lines) - 1; i >= start; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, ": ") {
|
||||
parts := strings.SplitN(line, ";", 2)
|
||||
if len(parts) == 2 {
|
||||
line = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, cmdEntry{Cmd: line, Shell: shell})
|
||||
}
|
||||
}
|
||||
|
||||
max := 20
|
||||
if len(entries) > max {
|
||||
entries = entries[:max]
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{"commands": entries})
|
||||
}
|
||||
|
||||
func (s *Server) handleRunningProcesses(w http.ResponseWriter, r *http.Request) {
|
||||
type proc struct {
|
||||
PID int `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
Command string `json:"command"`
|
||||
CPU string `json:"cpu"`
|
||||
Mem string `json:"mem"`
|
||||
}
|
||||
var procs []proc
|
||||
|
||||
editors := []string{"code", "nvim", "vim", "emacs", "hx", "subl", "zed", "cursor"}
|
||||
langs := []string{"node", "python", "java", "go", "rustc", "cargo", "ruby", "php"}
|
||||
interesting := append(editors, langs...)
|
||||
interesting = append(interesting, "muyue")
|
||||
|
||||
cmd := exec.Command("ps", "aux")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]interface{}{"processes": procs})
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines[1:] {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 11 {
|
||||
continue
|
||||
}
|
||||
fullCmd := strings.Join(fields[10:], " ")
|
||||
name := filepath.Base(fields[10])
|
||||
matched := false
|
||||
for _, pattern := range interesting {
|
||||
if strings.Contains(name, pattern) || strings.Contains(strings.ToLower(fullCmd), pattern) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
var pid int
|
||||
fmt.Sscanf(fields[1], "%d", &pid)
|
||||
procs = append(procs, proc{
|
||||
PID: pid,
|
||||
Name: name,
|
||||
Command: fullCmd,
|
||||
CPU: fields[2],
|
||||
Mem: fields[3],
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{"processes": procs})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -23,9 +24,26 @@ type Server struct {
|
||||
|
||||
func NewServer(cfg *config.MuyueConfig) *Server {
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
mux: http.NewServeMux(),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
// Auto-initialize config if nil or if no config file exists on disk
|
||||
if cfg == nil || !config.Exists() {
|
||||
defaultCfg := config.Default()
|
||||
if cfg != nil {
|
||||
// Preserve any user-provided settings from cfg
|
||||
defaultCfg.Profile = cfg.Profile
|
||||
defaultCfg.AI = cfg.AI
|
||||
defaultCfg.Tools = cfg.Tools
|
||||
defaultCfg.BMAD = cfg.BMAD
|
||||
defaultCfg.Terminal = cfg.Terminal
|
||||
}
|
||||
// Save initial config to establish the file for first-time usage
|
||||
if err := config.Save(defaultCfg); err != nil {
|
||||
log.Printf("config: initial save failed: %v", err)
|
||||
}
|
||||
cfg = defaultCfg
|
||||
}
|
||||
s.config = cfg
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
s.convStore = NewConversationStore()
|
||||
s.agentRegistry = agent.DefaultRegistry()
|
||||
@@ -95,6 +113,9 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/skills/export", s.handleSkillExport)
|
||||
s.mux.HandleFunc("/api/skills/import", s.handleSkillImport)
|
||||
s.mux.HandleFunc("/api/dashboard/status", s.handleDashboardStatus)
|
||||
s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota)
|
||||
s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands)
|
||||
s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user