first commit
This commit is contained in:
@@ -31,7 +31,6 @@ func handleChat(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate images
|
||||
if len(req.Images) > 0 {
|
||||
if err := services.ValidateImages(req.Images); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
|
||||
@@ -39,51 +38,40 @@ func handleChat(piClient *rpc.Client) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Try slash command dispatch
|
||||
result := services.TrySlashDispatch(req.Message)
|
||||
if result.Handled {
|
||||
if result.Message != "" {
|
||||
// unsupported command – return friendly message
|
||||
c.JSON(http.StatusOK, gin.H{"handled": true, "message": result.Message})
|
||||
return
|
||||
}
|
||||
// supported built-in command – forward to pi CLI as a command type
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: result.Command,
|
||||
Args: result.Args,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"handled": true, "result": resp.Result})
|
||||
return
|
||||
// normalize streamingBehavior: only "steer" / "followUp" are forwarded
|
||||
if req.StreamingBehavior != "steer" && req.StreamingBehavior != "followUp" {
|
||||
req.StreamingBehavior = ""
|
||||
}
|
||||
|
||||
// Regular prompt
|
||||
// Submit prompt; agent output streams back over SSE.
|
||||
if err := piClient.SubmitPrompt(req); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.OKResponse{OK: true})
|
||||
c.JSON(http.StatusAccepted, gin.H{"ok": true, "accepted": true})
|
||||
}
|
||||
}
|
||||
|
||||
func handleSteer(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req models.SteerRequest
|
||||
var req models.ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
err := piClient.SubmitPrompt(models.ChatRequest{
|
||||
Message: req.Message,
|
||||
StreamingBehavior: "steer",
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "steer",
|
||||
Message: req.Message,
|
||||
Images: req.Images,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: orDefault(resp.Error, "steer 失败")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.OKResponse{OK: true})
|
||||
}
|
||||
}
|
||||
@@ -95,11 +83,19 @@ func handleFollowUp(piClient *rpc.Client) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
req.StreamingBehavior = "follow_up"
|
||||
if err := piClient.SubmitPrompt(req); err != nil {
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "follow_up",
|
||||
Message: req.Message,
|
||||
Images: req.Images,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: orDefault(resp.Error, "follow_up 失败")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.OKResponse{OK: true})
|
||||
}
|
||||
}
|
||||
@@ -112,21 +108,24 @@ func handleBash(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "bash",
|
||||
Args: req.Command,
|
||||
Type: "bash",
|
||||
Command: req.Command,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Result)
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: orDefault(resp.Error, "命令执行失败")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func handleAbort(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
_, err := piClient.SendCmd(models.RPCCommand{Type: "abort"})
|
||||
if err != nil {
|
||||
if _, err := piClient.SendCmd(models.RPCCommand{Type: "abort"}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -136,8 +135,7 @@ func handleAbort(piClient *rpc.Client) gin.HandlerFunc {
|
||||
|
||||
func handleAbortRetry(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
_, err := piClient.SendCmd(models.RPCCommand{Type: "abort_retry"})
|
||||
if err != nil {
|
||||
if _, err := piClient.SendCmd(models.RPCCommand{Type: "abort_retry"}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -147,16 +145,16 @@ func handleAbortRetry(piClient *rpc.Client) gin.HandlerFunc {
|
||||
|
||||
func handleMessages(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req models.MessagesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: err.Error()})
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{Type: "get_messages"})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
snap := piClient.GetSnapshot()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"sessionFile": snap.SessionFile,
|
||||
"events": snap.ReplayEvents,
|
||||
})
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +171,7 @@ func handleSSE(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
ch := make(chan []byte, 64)
|
||||
ch := make(chan []byte, 256)
|
||||
client := &rpc.SSEClient{
|
||||
Ch: ch,
|
||||
Done: c.Request.Context().Done(),
|
||||
@@ -181,19 +179,19 @@ func handleSSE(piClient *rpc.Client) gin.HandlerFunc {
|
||||
piClient.RegisterSSEClient(client)
|
||||
defer piClient.UnregisterSSEClient(client)
|
||||
|
||||
// send initial keepalive
|
||||
_, _ = io.WriteString(c.Writer, ": keepalive\n\n")
|
||||
// initial comment to open the stream
|
||||
_, _ = io.WriteString(c.Writer, ": connected\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
case raw, ok := <-ch:
|
||||
case frame, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
frame := rpc.FormatSSEEvent(raw)
|
||||
// frame is already a fully-formatted "data: ...\n\n" SSE frame
|
||||
if _, err := c.Writer.Write(frame); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -203,12 +201,21 @@ func handleSSE(piClient *rpc.Client) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// jsonRaw is a helper to return pre-serialized JSON.
|
||||
// jsonRaw decodes pre-serialized JSON for re-emission via gin.
|
||||
func jsonRaw(raw json.RawMessage) any {
|
||||
if raw == nil {
|
||||
return nil
|
||||
if len(raw) == 0 {
|
||||
return gin.H{}
|
||||
}
|
||||
var v any
|
||||
_ = json.Unmarshal(raw, &v)
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return gin.H{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func orDefault(s, fallback string) string {
|
||||
if s == "" {
|
||||
return fallback
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"sproutclaw-web/internal/config"
|
||||
@@ -13,27 +11,23 @@ import (
|
||||
|
||||
// RegisterModels registers model-related routes.
|
||||
func RegisterModels(r *gin.RouterGroup, cfg *config.Config, piClient *rpc.Client) {
|
||||
r.GET("/models", handleGetModels(cfg, piClient))
|
||||
r.GET("/models", handleGetModels(piClient))
|
||||
r.POST("/model", handleSetModel(piClient))
|
||||
r.POST("/thinking", handleSetThinking(piClient))
|
||||
}
|
||||
|
||||
func handleGetModels(cfg *config.Config, piClient *rpc.Client) gin.HandlerFunc {
|
||||
func handleGetModels(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{Type: "list_models"})
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{Type: "get_available_models"})
|
||||
if err != nil {
|
||||
// fallback: read models.json
|
||||
data, ferr := os.ReadFile(cfg.ModelsConfigFile)
|
||||
if ferr != nil {
|
||||
c.JSON(http.StatusOK, models.ModelsResponse{Models: []models.ModelEntry{}})
|
||||
return
|
||||
}
|
||||
var raw any
|
||||
_ = json.Unmarshal(data, &raw)
|
||||
c.JSON(http.StatusOK, raw)
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Result))
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +47,11 @@ func handleSetModel(piClient *rpc.Client) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Result))
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"model": jsonRaw(resp.Data)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +63,17 @@ func handleSetThinking(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "set_thinking",
|
||||
Budget: req.Budget,
|
||||
Type: "set_thinking_level",
|
||||
Level: req.Level,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Result))
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.OKResponse{OK: true})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
@@ -47,7 +48,24 @@ func handleNewSession(piClient *rpc.Client) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Result))
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
// merge sessionFile from a follow-up get_state
|
||||
out := map[string]any{}
|
||||
if len(resp.Data) > 0 {
|
||||
_ = jsonUnmarshalInto(resp.Data, &out)
|
||||
}
|
||||
if state, serr := piClient.SendCmd(models.RPCCommand{Type: "get_state"}); serr == nil && state.Success {
|
||||
var sd struct {
|
||||
SessionFile string `json:"sessionFile"`
|
||||
}
|
||||
if jsonUnmarshalInto(state.Data, &sd) == nil && sd.SessionFile != "" {
|
||||
out["sessionFile"] = sd.SessionFile
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,20 +139,31 @@ func handleLoadSession(cfg *config.Config, piClient *rpc.Client) gin.HandlerFunc
|
||||
c.JSON(http.StatusForbidden, models.ErrorResponse{Error: "invalid path"})
|
||||
return
|
||||
}
|
||||
lines, err := services.ReadSessionMessages(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "switch_session",
|
||||
Path: req.Path,
|
||||
sw, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "switch_session",
|
||||
SessionPath: req.Path,
|
||||
CwdOverride: cfg.RepoRoot,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"messages": lines, "result": jsonRaw(resp.Result)})
|
||||
if !sw.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: sw.Error})
|
||||
return
|
||||
}
|
||||
mr, err := piClient.SendCmd(models.RPCCommand{Type: "get_messages"})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if !mr.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: mr.Error})
|
||||
return
|
||||
}
|
||||
// mr.Data is { messages: [...] }; forward it plus a session summary
|
||||
summary, _ := services.ReadSessionSummaryByPath(req.Path)
|
||||
c.JSON(http.StatusOK, gin.H{"messages": extractMessages(mr.Data), "session": summary})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,15 +178,25 @@ func handleActivateSession(cfg *config.Config, piClient *rpc.Client) gin.Handler
|
||||
c.JSON(http.StatusForbidden, models.ErrorResponse{Error: "invalid path"})
|
||||
return
|
||||
}
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "switch_session",
|
||||
Path: req.Path,
|
||||
sw, err := piClient.SendCmd(models.RPCCommand{
|
||||
Type: "switch_session",
|
||||
SessionPath: req.Path,
|
||||
CwdOverride: cfg.RepoRoot,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Result))
|
||||
if !sw.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: sw.Error})
|
||||
return
|
||||
}
|
||||
state, err := piClient.SendCmd(models.RPCCommand{Type: "get_state"})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "state": jsonRaw(state.Data)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +221,36 @@ func handleNameSession(cfg *config.Config) gin.HandlerFunc {
|
||||
|
||||
func handleSessionState(piClient *rpc.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
snap := piClient.GetSnapshot()
|
||||
c.JSON(http.StatusOK, models.SessionStateResponse{
|
||||
IsStreaming: snap.IsStreaming,
|
||||
SessionFile: snap.SessionFile,
|
||||
})
|
||||
resp, err := piClient.SendCmd(models.RPCCommand{Type: "get_state"})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: resp.Error})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, jsonRaw(resp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
// extractMessages pulls the "messages" array out of a get_messages data payload.
|
||||
func extractMessages(data json.RawMessage) any {
|
||||
if len(data) == 0 {
|
||||
return []any{}
|
||||
}
|
||||
var wrap struct {
|
||||
Messages json.RawMessage `json:"messages"`
|
||||
}
|
||||
if jsonUnmarshalInto(data, &wrap) == nil && len(wrap.Messages) > 0 {
|
||||
return jsonRaw(wrap.Messages)
|
||||
}
|
||||
return jsonRaw(data)
|
||||
}
|
||||
|
||||
func jsonUnmarshalInto(raw json.RawMessage, v any) error {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(raw, v)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
44
backend/internal/handlers/terminal.go
Normal file
44
backend/internal/handlers/terminal.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"sproutclaw-web/internal/config"
|
||||
"sproutclaw-web/internal/services"
|
||||
)
|
||||
|
||||
var terminalUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// RegisterTerminal registers web terminal routes.
|
||||
func RegisterTerminal(r *gin.RouterGroup, cfg *config.Config) {
|
||||
r.GET("/terminal", handleTerminalInfo(cfg))
|
||||
r.GET("/terminal/ws", handleTerminalWS(cfg))
|
||||
}
|
||||
|
||||
func handleTerminalInfo(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"repoRoot": cfg.RepoRoot,
|
||||
"platform": runtime.GOOS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleTerminalWS(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
conn, err := terminalUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := services.ServeWebTerminal(conn, cfg.RepoRoot); err != nil {
|
||||
_ = conn.WriteJSON(gin.H{"type": "error", "error": err.Error()})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,11 @@ func RegisterWebuiConfig(r *gin.RouterGroup, database *db.DB) {
|
||||
|
||||
func handleGetAvatars(database *db.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, _ := database.GetConfig("userAvatar")
|
||||
assistant, _ := database.GetConfig("assistantAvatar")
|
||||
user, _ := database.GetConfig("userAvatarUrl")
|
||||
agent, _ := database.GetConfig("agentAvatarUrl")
|
||||
c.JSON(http.StatusOK, models.AvatarsResponse{
|
||||
UserAvatar: normalizeAvatarURL(user),
|
||||
AssistantAvatar: normalizeAvatarURL(assistant),
|
||||
UserAvatarURL: normalizeAvatarURL(user),
|
||||
AgentAvatarURL: normalizeAvatarURL(agent),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user