From 28cea4c3bd71dcf22a5c7fe9201f7d5c16bf4144 Mon Sep 17 00:00:00 2001 From: lpf Date: Fri, 13 Feb 2026 14:11:59 +0800 Subject: [PATCH] fix --- cmd/clawgo/main.go | 249 +++++++++++++++++++++++++++++++++++++-- pkg/agent/loop.go | 19 +++ pkg/channels/telegram.go | 26 +++- 3 files changed, 283 insertions(+), 11 deletions(-) diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index 5ad3f5d..615db51 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -14,10 +14,12 @@ import ( "fmt" "io" "os" + "os/exec" "os/signal" "path/filepath" "reflect" + "runtime" "strconv" "strings" "syscall" @@ -39,6 +41,7 @@ import ( const version = "0.1.0" const logo = "🦞" +const gatewayServiceName = "clawgo-gateway.service" var globalConfigPathOverride string @@ -171,6 +174,8 @@ func main() { } case "version", "--version", "-v": fmt.Printf("%s clawgo v%s\n", logo, version) + case "uninstall": + uninstallCmd() default: fmt.Printf("Unknown command: %s\n", command) printHelp() @@ -223,18 +228,29 @@ func printHelp() { fmt.Println("Commands:") fmt.Println(" onboard Initialize clawgo configuration and workspace") fmt.Println(" agent Interact with the agent directly") - fmt.Println(" gateway Start clawgo gateway") + fmt.Println(" gateway Register/manage gateway service") fmt.Println(" status Show clawgo status") fmt.Println(" config Get/set config values") fmt.Println(" cron Manage scheduled tasks") fmt.Println(" login Configure CLIProxyAPI upstream") fmt.Println(" channel Test and manage messaging channels") fmt.Println(" skills Manage skills (install, list, remove)") + fmt.Println(" uninstall Uninstall clawgo components") fmt.Println(" version Show version information") fmt.Println() fmt.Println("Global options:") fmt.Println(" --config Use custom config file") fmt.Println(" --debug, -d Enable debug logging") + fmt.Println() + fmt.Println("Gateway service:") + fmt.Println(" clawgo gateway # register service") + fmt.Println(" clawgo gateway start|stop|restart|status") + fmt.Println(" clawgo gateway run # run foreground") + fmt.Println() + fmt.Println("Uninstall:") + fmt.Println(" clawgo uninstall # remove gateway service") + fmt.Println(" clawgo uninstall --purge # also remove config/workspace dir") + fmt.Println(" clawgo uninstall --remove-bin # also remove current executable") } func onboard() { @@ -604,14 +620,28 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { } func gatewayCmd() { - // Check for --debug flag args := os.Args[2:] - for _, arg := range args { - if arg == "--debug" || arg == "-d" { - logger.SetLevel(logger.DEBUG) - fmt.Println("🔍 Debug mode enabled") - break + if len(args) == 0 { + if err := gatewayInstallServiceCmd(); err != nil { + fmt.Printf("Error registering gateway service: %v\n", err) + os.Exit(1) } + return + } + + switch args[0] { + case "run": + // continue to foreground runtime below + case "start", "stop", "restart", "status": + if err := gatewayServiceControlCmd(args[0]); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + return + default: + fmt.Printf("Unknown gateway command: %s\n", args[0]) + fmt.Println("Usage: clawgo gateway [run|start|stop|restart|status]") + return } cfg, err := loadConfig() @@ -734,6 +764,141 @@ func gatewayCmd() { } } +func gatewayInstallServiceCmd() error { + scope, unitPath, err := detectGatewayServiceScopeAndPath() + if err != nil { + return err + } + + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve executable path failed: %w", err) + } + exePath, _ = filepath.Abs(exePath) + configPath := getConfigPath() + workDir := filepath.Dir(exePath) + + unitContent := buildGatewayUnitContent(scope, exePath, configPath, workDir) + if err := os.MkdirAll(filepath.Dir(unitPath), 0755); err != nil { + return fmt.Errorf("create service directory failed: %w", err) + } + if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil { + return fmt.Errorf("write service unit failed: %w", err) + } + + if err := runSystemctl(scope, "daemon-reload"); err != nil { + return err + } + if err := runSystemctl(scope, "enable", gatewayServiceName); err != nil { + return err + } + + fmt.Printf("✓ Gateway service registered: %s (%s)\n", gatewayServiceName, scope) + fmt.Printf(" Unit file: %s\n", unitPath) + fmt.Println(" Start service: clawgo gateway start") + fmt.Println(" Restart service: clawgo gateway restart") + fmt.Println(" Stop service: clawgo gateway stop") + return nil +} + +func gatewayServiceControlCmd(action string) error { + scope, _, err := detectInstalledGatewayService() + if err != nil { + return err + } + return runSystemctl(scope, action, gatewayServiceName) +} + +func detectGatewayServiceScopeAndPath() (string, string, error) { + // Linux-only systemd integration + if runtime.GOOS != "linux" { + return "", "", fmt.Errorf("gateway service registration currently supports Linux systemd only") + } + if strings.ToLower(strings.TrimSpace(os.Getenv("CLAWGO_GATEWAY_SCOPE"))) == "user" { + return userGatewayUnitPath() + } + if strings.ToLower(strings.TrimSpace(os.Getenv("CLAWGO_GATEWAY_SCOPE"))) == "system" { + return "system", "/etc/systemd/system/" + gatewayServiceName, nil + } + if os.Geteuid() == 0 { + return "system", "/etc/systemd/system/" + gatewayServiceName, nil + } + return userGatewayUnitPath() +} + +func userGatewayUnitPath() (string, string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", "", fmt.Errorf("resolve user home failed: %w", err) + } + return "user", filepath.Join(home, ".config", "systemd", "user", gatewayServiceName), nil +} + +func detectInstalledGatewayService() (string, string, error) { + systemPath := "/etc/systemd/system/" + gatewayServiceName + if info, err := os.Stat(systemPath); err == nil && !info.IsDir() { + return "system", systemPath, nil + } + + scope, userPath, err := userGatewayUnitPath() + if err != nil { + return "", "", err + } + if info, err := os.Stat(userPath); err == nil && !info.IsDir() { + return scope, userPath, nil + } + + return "", "", fmt.Errorf("gateway service not registered. Run: clawgo gateway") +} + +func buildGatewayUnitContent(scope, exePath, configPath, workDir string) string { + quotedExec := fmt.Sprintf("%q gateway run --config %q", exePath, configPath) + installTarget := "default.target" + if scope == "system" { + installTarget = "multi-user.target" + } + home, err := os.UserHomeDir() + if err != nil { + home = filepath.Dir(configPath) + } + + return fmt.Sprintf(`[Unit] +Description=ClawGo Gateway +After=network.target + +[Service] +Type=simple +WorkingDirectory=%s +ExecStart=%s +Restart=always +RestartSec=3 +Environment=CLAWGO_CONFIG=%s +Environment=HOME=%s + +[Install] +WantedBy=%s +`, workDir, quotedExec, configPath, home, installTarget) +} + +func runSystemctl(scope string, args ...string) error { + cmdArgs := make([]string, 0, len(args)+1) + if scope == "user" { + cmdArgs = append(cmdArgs, "--user") + } + cmdArgs = append(cmdArgs, args...) + + cmd := exec.Command("systemctl", cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if scope == "user" { + return fmt.Errorf("systemctl --user %s failed: %w", strings.Join(args, " "), err) + } + return fmt.Errorf("systemctl %s failed: %w", strings.Join(args, " "), err) + } + return nil +} + func buildGatewayRuntime(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBus, cronService *cron.CronService) (*agent.AgentLoop, *channels.Manager, error) { provider, err := providers.CreateProvider(cfg) if err != nil { @@ -1796,3 +1961,73 @@ func channelTestCmd() { fmt.Println("✓ Test message sent successfully!") } + +func uninstallCmd() { + purge := false + removeBin := false + + for _, arg := range os.Args[2:] { + switch arg { + case "--purge": + purge = true + case "--remove-bin": + removeBin = true + } + } + + // 1) Remove gateway service if registered. + if err := uninstallGatewayService(); err != nil { + fmt.Printf("Gateway service uninstall warning: %v\n", err) + } else { + fmt.Println("✓ Gateway service uninstalled") + } + + // 2) Remove runtime pid file. + pidPath := filepath.Join(filepath.Dir(getConfigPath()), "gateway.pid") + _ = os.Remove(pidPath) + + // 3) Optional purge config/workspace. + if purge { + configDir := filepath.Dir(getConfigPath()) + if err := os.RemoveAll(configDir); err != nil { + fmt.Printf("Failed to remove config directory %s: %v\n", configDir, err) + os.Exit(1) + } + fmt.Printf("✓ Removed config/workspace directory: %s\n", configDir) + } + + // 4) Optional remove current executable. + if removeBin { + exePath, err := os.Executable() + if err != nil { + fmt.Printf("Failed to resolve executable path: %v\n", err) + os.Exit(1) + } + if err := os.Remove(exePath); err != nil { + fmt.Printf("Failed to remove executable %s: %v\n", exePath, err) + os.Exit(1) + } + fmt.Printf("✓ Removed executable: %s\n", exePath) + } +} + +func uninstallGatewayService() error { + scope, unitPath, err := detectInstalledGatewayService() + if err != nil { + // Service not present is not fatal for uninstall command. + return nil + } + + // Ignore stop/disable errors to keep uninstall idempotent. + _ = runSystemctl(scope, "stop", gatewayServiceName) + _ = runSystemctl(scope, "disable", gatewayServiceName) + + if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove unit file failed: %w", err) + } + + if err := runSystemctl(scope, "daemon-reload"); err != nil { + return err + } + return nil +} diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 35601b1..ec4138f 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "syscall" + "time" "clawgo/pkg/bus" "clawgo/pkg/config" @@ -251,19 +252,28 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) "tools_json": formatToolsForLog(providerToolDefs), }) + llmStart := time.Now() response, err := al.callLLMWithModelFallback(ctx, messages, providerToolDefs, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, }) + llmElapsed := time.Since(llmStart) if err != nil { logger.ErrorCF("agent", "LLM call failed", map[string]interface{}{ "iteration": iteration, "error": err.Error(), + "elapsed": llmElapsed.String(), }) return "", fmt.Errorf("LLM call failed: %w", err) } + logger.InfoCF("agent", "LLM call completed", + map[string]interface{}{ + "iteration": iteration, + "elapsed": llmElapsed.String(), + "model": al.model, + }) if len(response.ToolCalls) == 0 { finalContent = response.Content @@ -458,19 +468,28 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe "tools_json": formatToolsForLog(providerToolDefs), }) + llmStart := time.Now() response, err := al.callLLMWithModelFallback(ctx, messages, providerToolDefs, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, }) + llmElapsed := time.Since(llmStart) if err != nil { logger.ErrorCF("agent", "LLM call failed in system message", map[string]interface{}{ "iteration": iteration, "error": err.Error(), + "elapsed": llmElapsed.String(), }) return "", fmt.Errorf("LLM call failed: %w", err) } + logger.InfoCF("agent", "LLM call completed (system message)", + map[string]interface{}{ + "iteration": iteration, + "elapsed": llmElapsed.String(), + "model": al.model, + }) if len(response.ToolCalls) == 0 { finalContent = response.Content diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 8e04d24..69fc23e 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -96,8 +96,8 @@ func (c *TelegramChannel) Stop(ctx context.Context) error { log.Println("Stopping Telegram bot...") c.setRunning(false) - // In telego v1.x, the long polling is stopped by canceling the context - // passed to UpdatesViaLongPolling. We don't need a separate Stop call + // In telego v1.x, the long polling is stopped by canceling the context + // passed to UpdatesViaLongPolling. We don't need a separate Stop call // if we use the parent context correctly. return nil @@ -116,8 +116,11 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // Stop thinking animation if stop, ok := c.stopThinking.Load(msg.ChatID); ok { + log.Printf("Telegram thinking stop signal: chat_id=%s", msg.ChatID) close(stop.(chan struct{})) c.stopThinking.Delete(msg.ChatID) + } else { + log.Printf("Telegram thinking stop skipped: no stop channel found for chat_id=%s", msg.ChatID) } htmlContent := markdownToTelegramHTML(msg.Content) @@ -125,6 +128,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // Try to edit placeholder if pID, ok := c.placeholders.Load(msg.ChatID); ok { c.placeholders.Delete(msg.ChatID) + log.Printf("Telegram editing thinking placeholder: chat_id=%s message_id=%d", msg.ChatID, pID.(int)) _, err := c.bot.EditMessageText(ctx, &telego.EditMessageTextParams{ ChatID: chatID, @@ -134,9 +138,13 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err }) if err == nil { + log.Printf("Telegram placeholder updated successfully: chat_id=%s", msg.ChatID) return nil } + log.Printf("Telegram placeholder update failed, fallback to new message: chat_id=%s err=%v", msg.ChatID, err) // Fallback to new message if edit fails + } else { + log.Printf("Telegram placeholder not found, sending new message: chat_id=%s", msg.ChatID) } _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, htmlContent).WithParseMode(telego.ModeHTML)) @@ -144,6 +152,9 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err if err != nil { log.Printf("HTML parse failed, falling back to plain text: %v", err) _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, msg.Content)) + if err != nil { + log.Printf("Telegram plain-text fallback send failed: chat_id=%s err=%v", msg.ChatID, err) + } return err } @@ -259,11 +270,13 @@ func (c *TelegramChannel) handleMessage(message *telego.Message) { stopChan := make(chan struct{}) c.stopThinking.Store(fmt.Sprintf("%d", chatID), stopChan) + log.Printf("Telegram thinking started: chat_id=%d", chatID) pMsg, err := c.bot.SendMessage(context.Background(), telegoutil.Message(telegoutil.ID(chatID), "Thinking... 💭")) if err == nil { pID := pMsg.MessageID c.placeholders.Store(fmt.Sprintf("%d", chatID), pID) + log.Printf("Telegram thinking placeholder created: chat_id=%d message_id=%d", chatID, pID) go func(cid int64, mid int, stop <-chan struct{}) { dots := []string{".", "..", "..."} @@ -274,18 +287,23 @@ func (c *TelegramChannel) handleMessage(message *telego.Message) { for { select { case <-stop: + log.Printf("Telegram thinking animation stopped: chat_id=%d", cid) return case <-ticker.C: i++ text := fmt.Sprintf("Thinking%s %s", dots[i%len(dots)], emotes[i%len(emotes)]) - _, _ = c.bot.EditMessageText(context.Background(), &telego.EditMessageTextParams{ + if _, err := c.bot.EditMessageText(context.Background(), &telego.EditMessageTextParams{ ChatID: telegoutil.ID(cid), MessageID: mid, Text: text, - }) + }); err != nil { + log.Printf("Telegram thinking animation edit failed: chat_id=%d message_id=%d err=%v", cid, mid, err) + } } } }(chatID, pID, stopChan) + } else { + log.Printf("Telegram thinking placeholder create failed: chat_id=%d err=%v", chatID, err) } metadata := map[string]string{