mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-03 10:57:35 +08:00
fix bug
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -29,10 +28,12 @@ import (
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/channels"
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/configops"
|
||||
"clawgo/pkg/cron"
|
||||
"clawgo/pkg/heartbeat"
|
||||
"clawgo/pkg/logger"
|
||||
"clawgo/pkg/providers"
|
||||
"clawgo/pkg/sentinel"
|
||||
"clawgo/pkg/skills"
|
||||
"clawgo/pkg/voice"
|
||||
|
||||
@@ -659,6 +660,21 @@ func gatewayCmd() {
|
||||
30*60,
|
||||
true,
|
||||
)
|
||||
sentinelService := sentinel.NewService(
|
||||
getConfigPath(),
|
||||
cfg.WorkspacePath(),
|
||||
cfg.Sentinel.IntervalSec,
|
||||
cfg.Sentinel.AutoHeal,
|
||||
func(message string) {
|
||||
if cfg.Sentinel.NotifyChannel != "" && cfg.Sentinel.NotifyChatID != "" {
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: cfg.Sentinel.NotifyChannel,
|
||||
ChatID: cfg.Sentinel.NotifyChatID,
|
||||
Content: "[Sentinel] " + message,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -695,6 +711,10 @@ func gatewayCmd() {
|
||||
fmt.Printf("Error starting heartbeat service: %v\n", err)
|
||||
}
|
||||
fmt.Println("✓ Heartbeat service started")
|
||||
if cfg.Sentinel.Enabled {
|
||||
sentinelService.Start()
|
||||
fmt.Println("✓ Sentinel service started")
|
||||
}
|
||||
|
||||
if err := channelManager.StartAll(ctx); err != nil {
|
||||
fmt.Printf("Error starting channels: %v\n", err)
|
||||
@@ -727,6 +747,25 @@ func gatewayCmd() {
|
||||
|
||||
if runtimeSame {
|
||||
configureLogging(newCfg)
|
||||
sentinelService.Stop()
|
||||
sentinelService = sentinel.NewService(
|
||||
getConfigPath(),
|
||||
newCfg.WorkspacePath(),
|
||||
newCfg.Sentinel.IntervalSec,
|
||||
newCfg.Sentinel.AutoHeal,
|
||||
func(message string) {
|
||||
if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" {
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: newCfg.Sentinel.NotifyChannel,
|
||||
ChatID: newCfg.Sentinel.NotifyChatID,
|
||||
Content: "[Sentinel] " + message,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
if newCfg.Sentinel.Enabled {
|
||||
sentinelService.Start()
|
||||
}
|
||||
cfg = newCfg
|
||||
fmt.Println("✓ Config hot-reload applied (logging/metadata only)")
|
||||
continue
|
||||
@@ -744,6 +783,25 @@ func gatewayCmd() {
|
||||
channelManager = newChannelManager
|
||||
agentLoop = newAgentLoop
|
||||
cfg = newCfg
|
||||
sentinelService.Stop()
|
||||
sentinelService = sentinel.NewService(
|
||||
getConfigPath(),
|
||||
newCfg.WorkspacePath(),
|
||||
newCfg.Sentinel.IntervalSec,
|
||||
newCfg.Sentinel.AutoHeal,
|
||||
func(message string) {
|
||||
if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" {
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: newCfg.Sentinel.NotifyChannel,
|
||||
ChatID: newCfg.Sentinel.NotifyChatID,
|
||||
Content: "[Sentinel] " + message,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
if newCfg.Sentinel.Enabled {
|
||||
sentinelService.Start()
|
||||
}
|
||||
|
||||
if err := channelManager.StartAll(ctx); err != nil {
|
||||
fmt.Printf("✗ Reload failed (start channels): %v\n", err)
|
||||
@@ -755,6 +813,7 @@ func gatewayCmd() {
|
||||
fmt.Println("\nShutting down...")
|
||||
cancel()
|
||||
heartbeatService.Stop()
|
||||
sentinelService.Stop()
|
||||
cronService.Stop()
|
||||
agentLoop.Stop()
|
||||
channelManager.StopAll(ctx)
|
||||
@@ -1111,181 +1170,35 @@ func configCheckCmd() {
|
||||
}
|
||||
|
||||
func loadConfigAsMap(path string) (map[string]interface{}, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
defaultCfg := config.DefaultConfig()
|
||||
defData, mErr := json.Marshal(defaultCfg)
|
||||
if mErr != nil {
|
||||
return nil, mErr
|
||||
}
|
||||
var cfgMap map[string]interface{}
|
||||
if uErr := json.Unmarshal(defData, &cfgMap); uErr != nil {
|
||||
return nil, uErr
|
||||
}
|
||||
return cfgMap, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfgMap map[string]interface{}
|
||||
if err := json.Unmarshal(data, &cfgMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfgMap, nil
|
||||
return configops.LoadConfigAsMap(path)
|
||||
}
|
||||
|
||||
func normalizeConfigPath(path string) string {
|
||||
p := strings.TrimSpace(path)
|
||||
p = strings.Trim(p, ".")
|
||||
parts := strings.Split(p, ".")
|
||||
for i, part := range parts {
|
||||
if part == "enable" {
|
||||
parts[i] = "enabled"
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
return configops.NormalizeConfigPath(path)
|
||||
}
|
||||
|
||||
func parseConfigValue(raw string) interface{} {
|
||||
v := strings.TrimSpace(raw)
|
||||
lv := strings.ToLower(v)
|
||||
if lv == "true" {
|
||||
return true
|
||||
}
|
||||
if lv == "false" {
|
||||
return false
|
||||
}
|
||||
if lv == "null" {
|
||||
return nil
|
||||
}
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil && strings.Contains(v, ".") {
|
||||
return f
|
||||
}
|
||||
if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) {
|
||||
return v[1 : len(v)-1]
|
||||
}
|
||||
return v
|
||||
return configops.ParseConfigValue(raw)
|
||||
}
|
||||
|
||||
func setMapValueByPath(root map[string]interface{}, path string, value interface{}) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("path is empty")
|
||||
}
|
||||
parts := strings.Split(path, ".")
|
||||
cur := root
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
key := parts[i]
|
||||
if key == "" {
|
||||
return fmt.Errorf("invalid path: %s", path)
|
||||
}
|
||||
next, ok := cur[key]
|
||||
if !ok {
|
||||
child := map[string]interface{}{}
|
||||
cur[key] = child
|
||||
cur = child
|
||||
continue
|
||||
}
|
||||
child, ok := next.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("path segment is not object: %s", key)
|
||||
}
|
||||
cur = child
|
||||
}
|
||||
last := parts[len(parts)-1]
|
||||
if last == "" {
|
||||
return fmt.Errorf("invalid path: %s", path)
|
||||
}
|
||||
cur[last] = value
|
||||
return nil
|
||||
return configops.SetMapValueByPath(root, path, value)
|
||||
}
|
||||
|
||||
func getMapValueByPath(root map[string]interface{}, path string) (interface{}, bool) {
|
||||
if path == "" {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(path, ".")
|
||||
var cur interface{} = root
|
||||
for _, key := range parts {
|
||||
obj, ok := cur.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
next, ok := obj[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
cur = next
|
||||
}
|
||||
return cur, true
|
||||
return configops.GetMapValueByPath(root, path)
|
||||
}
|
||||
|
||||
func writeConfigAtomicWithBackup(configPath string, data []byte) (string, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backupPath := configPath + ".bak"
|
||||
if oldData, err := os.ReadFile(configPath); err == nil {
|
||||
if err := os.WriteFile(backupPath, oldData, 0644); err != nil {
|
||||
return "", fmt.Errorf("write backup failed: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("read existing config failed: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
|
||||
return "", fmt.Errorf("write temp config failed: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("atomic replace config failed: %w", err)
|
||||
}
|
||||
return backupPath, nil
|
||||
return configops.WriteConfigAtomicWithBackup(configPath, data)
|
||||
}
|
||||
|
||||
func rollbackConfigFromBackup(configPath, backupPath string) error {
|
||||
backupData, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read backup failed: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := configPath + ".rollback.tmp"
|
||||
if err := os.WriteFile(tmpPath, backupData, 0644); err != nil {
|
||||
return fmt.Errorf("write rollback temp failed: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return fmt.Errorf("rollback replace failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
return configops.RollbackConfigFromBackup(configPath, backupPath)
|
||||
}
|
||||
|
||||
func triggerGatewayReload() (bool, error) {
|
||||
pidPath := filepath.Join(filepath.Dir(getConfigPath()), "gateway.pid")
|
||||
data, err := os.ReadFile(pidPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w (pid file not found: %s)", errGatewayNotRunning, pidPath)
|
||||
}
|
||||
|
||||
pidStr := strings.TrimSpace(string(data))
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil || pid <= 0 {
|
||||
return true, fmt.Errorf("invalid gateway pid: %q", pidStr)
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("find process failed: %w", err)
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGHUP); err != nil {
|
||||
return true, fmt.Errorf("send SIGHUP failed: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
return configops.TriggerGatewayReload(getConfigPath(), errGatewayNotRunning)
|
||||
}
|
||||
|
||||
func statusCmd() {
|
||||
|
||||
Reference in New Issue
Block a user