mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-19 00:47:28 +08:00
fix
This commit is contained in:
@@ -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 <path> 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user