diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 72f7732..b535b63 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -32,8 +32,8 @@ type Config struct { func New() *Config { port := flag.Int("port", 19133, "HTTP port") - agentDir := flag.String("agent-dir", "", "pi agent directory (e.g. /path/to/.pi/agent)") - repoRoot := flag.String("repo-root", "", "pi repo root (pi CLI cwd); defaults to two levels above agent-dir") + agentDir := flag.String("agent-dir", "", "SproutClaw agent directory (e.g. /path/to/.sproutclaw/agent)") + repoRoot := flag.String("repo-root", "", "SproutClaw repo root (CLI cwd); defaults to two levels above agent-dir") dataDir := flag.String("data-dir", "", "sproutclaw-web data dir for SQLite (defaults to ./data)") piCmd := flag.String("pi-cmd", "", "pi CLI command (may include args, e.g. \"node /path/tsx.mjs /path/cli.ts\")") frontendDist := flag.String("frontend-dist", "", "path to frontend/dist (defaults to ../frontend/dist relative to binary)") @@ -41,7 +41,7 @@ func New() *Config { if *agentDir == "" { cwd, _ := os.Getwd() - *agentDir = filepath.Join(cwd, ".pi", "agent") + *agentDir = filepath.Join(cwd, ".sproutclaw", "agent") } cfg := &Config{ @@ -50,7 +50,7 @@ func New() *Config { RepoRoot: *repoRoot, DataDir: *dataDir, } - // repoRoot defaults to the directory two levels above agentDir (.pi/agent -> repo root) + // repoRoot defaults to the directory two levels above agentDir (.sproutclaw/agent -> repo root) if cfg.RepoRoot == "" { cfg.RepoRoot = filepath.Dir(filepath.Dir(*agentDir)) } diff --git a/backend/internal/handlers/restart.go b/backend/internal/handlers/restart.go new file mode 100644 index 0000000..fbe2d4e --- /dev/null +++ b/backend/internal/handlers/restart.go @@ -0,0 +1,99 @@ +package handlers + +import ( + "log" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +const defaultSystemdUnit = "sproutclaw-web.service" + +func handleRestart() gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + go func() { + time.Sleep(300 * time.Millisecond) + if unit, underSystemd := systemdServiceUnit(); underSystemd { + log.Printf("[restart] restarting via systemctl: %s", unit) + cmd := exec.Command("systemctl", "restart", unit) + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("[restart] systemctl restart failed: %v: %s", err, strings.TrimSpace(string(out))) + } + return + } + restartByReexec() + }() + } +} + +func restartByReexec() { + exe, err := os.Executable() + if err != nil { + log.Printf("[restart] get executable: %v", err) + os.Exit(1) + } + cmd := exec.Command(exe, os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + log.Printf("[restart] start new process: %v", err) + os.Exit(1) + } + os.Exit(0) +} + +func systemdServiceUnit() (string, bool) { + if runtime.GOOS != "linux" { + return "", false + } + if os.Getenv("INVOCATION_ID") == "" && !inSystemdCgroup() { + return "", false + } + if unit := strings.TrimSpace(os.Getenv("SPROUTCLAW_WEB_SYSTEMD_UNIT")); unit != "" { + return normalizeSystemdUnit(unit), true + } + if unit := readSystemdUnitFromCgroup(); unit != "" { + return unit, true + } + return defaultSystemdUnit, true +} + +func normalizeSystemdUnit(name string) string { + if strings.HasSuffix(name, ".service") { + return name + } + return name + ".service" +} + +func inSystemdCgroup() bool { + data, err := os.ReadFile("/proc/self/cgroup") + if err != nil { + return false + } + return strings.Contains(string(data), ".service") +} + +func readSystemdUnitFromCgroup() string { + data, err := os.ReadFile("/proc/self/cgroup") + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n") { + idx := strings.LastIndex(line, "/") + if idx < 0 { + continue + } + name := line[idx+1:] + if strings.HasSuffix(name, ".service") { + return name + } + } + return "" +} diff --git a/backend/internal/handlers/settings.go b/backend/internal/handlers/settings.go index c04619b..6006d94 100644 --- a/backend/internal/handlers/settings.go +++ b/backend/internal/handlers/settings.go @@ -2,10 +2,8 @@ package handlers import ( "fmt" - "log" "net/http" "os" - "os/exec" "runtime" "strings" "sync" @@ -37,28 +35,6 @@ func RegisterSettings(r *gin.RouterGroup, cfg *config.Config, database *db.DB, p var processStartTime = time.Now() -func handleRestart() gin.HandlerFunc { - return func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"ok": true}) - go func() { - time.Sleep(300 * time.Millisecond) - exe, err := os.Executable() - if err != nil { - log.Printf("[restart] get executable: %v", err) - os.Exit(1) - } - cmd := exec.Command(exe, os.Args[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - log.Printf("[restart] start new process: %v", err) - os.Exit(1) - } - os.Exit(0) - }() - } -} - func handleGetSettings(cfg *config.Config, database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { systemPrompt, _ := services.ReadSystemPrompt(cfg.SystemPromptFile) diff --git a/backend/internal/rpc/client.go b/backend/internal/rpc/client.go index bbcda2c..affb88a 100644 --- a/backend/internal/rpc/client.go +++ b/backend/internal/rpc/client.go @@ -80,7 +80,7 @@ func NewClient(piCmd string, piArgs []string, repoRoot, agentDir string, onExit args := append(append([]string{}, piArgs...), "--mode", "rpc") cmd := exec.Command(piCmd, args...) cmd.Dir = repoRoot - cmd.Env = append(os.Environ(), fmt.Sprintf("PI_CODING_AGENT_DIR=%s", agentDir)) + cmd.Env = append(os.Environ(), fmt.Sprintf("SPROUTCLAW_CODING_AGENT_DIR=%s", agentDir)) log.Printf("[sproutclaw-web] launching pi RPC: %s %s (cwd=%s)", piCmd, strings.Join(args, " "), repoRoot) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..47be694 --- /dev/null +++ b/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$SCRIPT_DIR/backend/dist/sproutclaw-web-backend" + +echo "[build] compiling Go backend -> $OUT" +cd "$SCRIPT_DIR/backend" +/usr/local/go/bin/go build -o "$OUT" ./cmd/server +chmod +x "$OUT" +echo "[build] done: $OUT" diff --git a/frontend/src/routes/SettingsPage.tsx b/frontend/src/routes/SettingsPage.tsx index da1ebaa..b7c233d 100644 --- a/frontend/src/routes/SettingsPage.tsx +++ b/frontend/src/routes/SettingsPage.tsx @@ -100,7 +100,7 @@ function extensionCategory(extension: ExtensionInfo): "local" | "npm" { } if (extension.source?.startsWith("npm:")) return "npm"; const pathHint = `${extension.path || ""} ${extension.location || ""}`.replace(/\\/g, "/"); - if (pathHint.includes("/.pi/agent/npm/node_modules/")) return "npm"; + if (pathHint.includes("/.sproutclaw/agent/npm/node_modules/")) return "npm"; return "local"; } @@ -701,8 +701,13 @@ export function SettingsPage() { } catch { // 后端重启时连接断开属正常现象,忽略错误 } - // 轮询直到后端恢复 + const deadline = Date.now() + 60_000; const poll = async () => { + if (Date.now() > deadline) { + setRestarting(false); + setStatus("后端重启超时,请检查 sproutclaw-web 服务状态", "error"); + return; + } try { await settingsApi.fetchSettings(); setStatus("后端已重启,正在刷新...", "ok"); diff --git a/run-backend.bat b/run-backend.bat index 2bbfaca..9779bd3 100644 --- a/run-backend.bat +++ b/run-backend.bat @@ -6,7 +6,7 @@ rem ------------------------------------------------------- cd /d "%~dp0backend" echo Starting Go backend on port %BACKEND_PORT%... -go run ./cmd/server --port %BACKEND_PORT% --agent-dir "%SPROUTCLAW_DIR%\.pi\agent" --pi-cmd "node %SPROUTCLAW_DIR%\packages\coding-agent\src\cli.ts" +go run ./cmd/server --port %BACKEND_PORT% --agent-dir "%SPROUTCLAW_DIR%\.sproutclaw\agent" --pi-cmd "node %SPROUTCLAW_DIR%\packages\coding-agent\src\cli.ts" echo. pause >nul