first commit

This commit is contained in:
2026-06-14 20:31:10 +08:00
parent c33b143176
commit 1ed3f576fa
51 changed files with 3362 additions and 810 deletions

View File

@@ -1,11 +1,9 @@
package handlers
import (
"fmt"
"net/http"
"os"
"runtime"
"strings"
"github.com/gin-gonic/gin"
"sproutclaw-web/internal/config"
@@ -17,7 +15,7 @@ import (
// RegisterSettings registers all settings-related routes.
func RegisterSettings(r *gin.RouterGroup, cfg *config.Config, database *db.DB, piClient *rpc.Client) {
r.GET("/settings", handleGetSettings(cfg))
r.GET("/settings", handleGetSettings(cfg, database))
r.POST("/settings/skills/toggle", handleToggleSkill(cfg, piClient))
r.POST("/settings/extensions/toggle", handleToggleExtension(cfg, piClient))
r.POST("/settings/mcp/server/toggle", handleToggleMCPServer(cfg, piClient))
@@ -29,53 +27,67 @@ func RegisterSettings(r *gin.RouterGroup, cfg *config.Config, database *db.DB, p
r.GET("/environment", handleEnvironment(cfg))
}
func handleGetSettings(cfg *config.Config) gin.HandlerFunc {
func handleGetSettings(cfg *config.Config, database *db.DB) gin.HandlerFunc {
return func(c *gin.Context) {
skills := services.ListSkills(cfg.AgentDir)
extensions := services.ListExtensions(cfg.AgentDir)
mcpServers := services.ReadMCPServers(cfg.McpConfigFile, cfg.McpCacheFile)
systemPrompt, _ := services.ReadSystemPrompt(cfg.SystemPromptFile)
modelsConfig, _ := services.ReadModelsConfig(cfg.ModelsConfigFile)
userAvatar, _ := database.GetConfig("userAvatarUrl")
agentAvatar, _ := database.GetConfig("agentAvatarUrl")
c.JSON(http.StatusOK, models.SettingsResponse{
Skills: skills,
Extensions: extensions,
MCPServers: mcpServers,
SystemPrompt: systemPrompt,
ModelsConfig: modelsConfig,
c.JSON(http.StatusOK, gin.H{
"systemPrompt": systemPrompt,
"systemPromptPath": cfg.SystemPromptFile,
"modelsConfig": modelsConfig,
"modelsConfigPath": cfg.ModelsConfigFile,
"userAvatarUrl": userAvatar,
"agentAvatarUrl": agentAvatar,
"skills": services.ListSkills(cfg.AgentDir),
"extensions": services.ListExtensions(cfg.AgentDir),
"extensionsPath": cfg.ExtensionsDir,
"mcpTools": services.ReadMCPServers(cfg.McpConfigFile, cfg.McpCacheFile),
"mcpConfigPath": cfg.McpConfigFile,
"mcpCachePath": cfg.McpCacheFile,
})
}
}
func handleToggleSkill(cfg *config.Config, piClient *rpc.Client) gin.HandlerFunc {
return func(c *gin.Context) {
var req models.ToggleRequest
var req models.SkillToggleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if err := services.ToggleSkill(cfg.AgentDir, req.ID, req.Enabled); err != nil {
if req.Path == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "skill path 无效"})
return
}
if err := services.ToggleSkill(cfg.AgentDir, req.Path, req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
func handleToggleExtension(cfg *config.Config, piClient *rpc.Client) gin.HandlerFunc {
return func(c *gin.Context) {
var req models.ToggleRequest
var req models.ExtensionToggleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if err := services.ToggleExtension(cfg.AgentDir, req.ID, req.Enabled); err != nil {
if req.Path == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "extension path 无效"})
return
}
if err := services.ToggleExtension(cfg.AgentDir, req.Path, req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
@@ -86,12 +98,16 @@ func handleToggleMCPServer(cfg *config.Config, piClient *rpc.Client) gin.Handler
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if req.Server == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "server 名称无效"})
return
}
if err := services.ToggleMCPServer(cfg.McpConfigFile, req.Server, req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
@@ -102,19 +118,23 @@ func handleToggleMCPTool(cfg *config.Config, piClient *rpc.Client) gin.HandlerFu
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if req.Server == "" || req.Tool == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "server / tool 名称无效"})
return
}
if err := services.ToggleMCPTool(cfg.McpConfigFile, req.Server, req.Tool, req.Enabled); err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
func handleReload(piClient *rpc.Client) gin.HandlerFunc {
return func(c *gin.Context) {
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
@@ -125,12 +145,17 @@ func handleSetModelsConfig(cfg *config.Config, piClient *rpc.Client) gin.Handler
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if err := services.WriteModelsConfig(cfg.ModelsConfigFile, req.Content); err != nil {
// Guard: the field must be present (not just empty) to avoid clobbering.
if req.ModelsConfig == nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "缺少 modelsConfig 字段"})
return
}
if err := services.WriteModelsConfig(cfg.ModelsConfigFile, *req.ModelsConfig); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true, "modelsConfigPath": cfg.ModelsConfigFile})
}
}
@@ -141,12 +166,18 @@ func handleSetSystemPrompt(cfg *config.Config, piClient *rpc.Client) gin.Handler
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if err := services.WriteSystemPrompt(cfg.SystemPromptFile, req.Content); err != nil {
// Guard: the field must be present. A missing field would otherwise
// silently write an empty string and wipe the prompt file.
if req.SystemPrompt == nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "缺少 systemPrompt 字段"})
return
}
if err := services.WriteSystemPrompt(cfg.SystemPromptFile, *req.SystemPrompt); err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
return
}
reloadAgent(piClient)
c.JSON(http.StatusOK, models.OKResponse{OK: true})
c.JSON(http.StatusOK, gin.H{"ok": true, "systemPromptPath": cfg.SystemPromptFile})
}
}
@@ -157,27 +188,27 @@ func handleSetAvatars(database *db.DB) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
return
}
if req.UserAvatar != "" {
_ = database.SetConfig("userAvatar", req.UserAvatar)
}
if req.AssistantAvatar != "" {
_ = database.SetConfig("assistantAvatar", req.AssistantAvatar)
}
c.JSON(http.StatusOK, models.OKResponse{OK: true})
userURL := normalizeAvatarURL(req.UserAvatarURL)
agentURL := normalizeAvatarURL(req.AgentAvatarURL)
_ = database.SetConfig("userAvatarUrl", userURL)
_ = database.SetConfig("agentAvatarUrl", agentURL)
c.JSON(http.StatusOK, gin.H{"ok": true, "userAvatarUrl": userURL, "agentAvatarUrl": agentURL})
}
}
func handleEnvironment(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
hostname, _ := os.Hostname()
lanAddrs := getLANAddresses()
cwd, _ := os.Getwd()
c.JSON(http.StatusOK, gin.H{
"platform": runtime.GOOS,
"arch": runtime.GOARCH,
"hostname": hostname,
"version": runtime.Version(),
"port": cfg.Port,
"lan": lanAddrs,
"nodeVersion": runtime.Version(),
"platform": runtime.GOOS,
"arch": runtime.GOARCH,
"hostname": hostname,
"pid": os.Getpid(),
"cwd": cwd,
"port": cfg.Port,
"repoRoot": cfg.RepoRoot,
})
}
}
@@ -185,31 +216,3 @@ func handleEnvironment(cfg *config.Config) gin.HandlerFunc {
func reloadAgent(piClient *rpc.Client) {
_, _ = piClient.SendCmd(models.RPCCommand{Type: "reload"})
}
func getLANAddresses() []string {
// simplified returns first non-loopback IPv4
var addrs []string
hostname, err := os.Hostname()
if err != nil {
return addrs
}
_ = hostname
// Use a simple approach: read network interfaces via fmt
// For a full implementation, net.InterfaceAddrs() would be used
ifaces := getNetworkAddresses()
for _, a := range ifaces {
if !strings.HasPrefix(a, "127.") && !strings.HasPrefix(a, "::") {
addrs = append(addrs, a)
}
}
return addrs
}
func getNetworkAddresses() []string {
var result []string
hostname, _ := os.Hostname()
if hostname != "" {
result = append(result, fmt.Sprintf("%s (hostname)", hostname))
}
return result
}