1 Commits

Author SHA1 Message Date
lpf
623b401850 Fix config reload and subagent config feedback 2026-03-06 18:45:43 +08:00
3 changed files with 168 additions and 150 deletions

View File

@@ -154,8 +154,12 @@ func gatewayCmd() {
} }
return out return out
}) })
reloadReqCh := make(chan struct{}, 1)
registryServer.SetConfigAfterHook(func() { registryServer.SetConfigAfterHook(func() {
_ = requestGatewayReloadSignal() select {
case reloadReqCh <- struct{}{}:
default:
}
}) })
registryServer.SetSubagentHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) { registryServer.SetSubagentHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
return agentLoop.HandleSubagentRuntime(cctx, action, args) return agentLoop.HandleSubagentRuntime(cctx, action, args)
@@ -309,77 +313,35 @@ func gatewayCmd() {
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, gatewayNotifySignals()...) signal.Notify(sigChan, gatewayNotifySignals()...)
for { applyReload := func() {
sig := <-sigChan fmt.Println("\n↻ Reloading config...")
switch { newCfg, err := config.LoadConfig(getConfigPath())
case isGatewayReloadSignal(sig): if err != nil {
fmt.Println("\n↻ Reloading config...") fmt.Printf(" Reload failed (load config): %v\n", err)
newCfg, err := config.LoadConfig(getConfigPath()) return
if err != nil { }
fmt.Printf("✗ Reload failed (load config): %v\n", err) if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
continue applyMaximumPermissionPolicy(newCfg)
} }
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") { configureCronServiceRuntime(cronService, newCfg)
applyMaximumPermissionPolicy(newCfg) heartbeatService.Stop()
} heartbeatService = buildHeartbeatService(newCfg, msgBus)
configureCronServiceRuntime(cronService, newCfg) if err := heartbeatService.Start(); err != nil {
heartbeatService.Stop() fmt.Printf("Error starting heartbeat service: %v\n", err)
heartbeatService = buildHeartbeatService(newCfg, msgBus) }
if err := heartbeatService.Start(); err != nil {
fmt.Printf("Error starting heartbeat service: %v\n", err)
}
if reflect.DeepEqual(cfg, newCfg) { if reflect.DeepEqual(cfg, newCfg) {
fmt.Println("✓ Config unchanged, skip reload") fmt.Println("✓ Config unchanged, skip reload")
continue return
} }
runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) && runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) &&
reflect.DeepEqual(cfg.Providers, newCfg.Providers) && reflect.DeepEqual(cfg.Providers, newCfg.Providers) &&
reflect.DeepEqual(cfg.Tools, newCfg.Tools) && reflect.DeepEqual(cfg.Tools, newCfg.Tools) &&
reflect.DeepEqual(cfg.Channels, newCfg.Channels) reflect.DeepEqual(cfg.Channels, newCfg.Channels)
if runtimeSame { if runtimeSame {
configureLogging(newCfg) 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.SetManager(channelManager)
sentinelService.Start()
}
cfg = newCfg
runtimecfg.Set(cfg)
fmt.Println("✓ Config hot-reload applied (logging/metadata only)")
continue
}
newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService)
if err != nil {
fmt.Printf("✗ Reload failed (init runtime): %v\n", err)
continue
}
channelManager.StopAll(ctx)
agentLoop.Stop()
channelManager = newChannelManager
agentLoop = newAgentLoop
cfg = newCfg
runtimecfg.Set(cfg)
sentinelService.Stop() sentinelService.Stop()
sentinelService = sentinel.NewService( sentinelService = sentinel.NewService(
getConfigPath(), getConfigPath(),
@@ -397,27 +359,77 @@ func gatewayCmd() {
}, },
) )
if newCfg.Sentinel.Enabled { if newCfg.Sentinel.Enabled {
sentinelService.SetManager(channelManager)
sentinelService.Start() sentinelService.Start()
} }
sentinelService.SetManager(channelManager) cfg = newCfg
runtimecfg.Set(cfg)
if err := channelManager.StartAll(ctx); err != nil { fmt.Println("✓ Config hot-reload applied (logging/metadata only)")
fmt.Printf("✗ Reload failed (start channels): %v\n", err)
continue
}
go agentLoop.Run(ctx)
fmt.Println("✓ Config hot-reload applied")
default:
fmt.Println("\nShutting down...")
cancel()
heartbeatService.Stop()
sentinelService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("✓ Gateway stopped")
return return
} }
newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService)
if err != nil {
fmt.Printf("✗ Reload failed (init runtime): %v\n", err)
return
}
channelManager.StopAll(ctx)
agentLoop.Stop()
channelManager = newChannelManager
agentLoop = newAgentLoop
cfg = newCfg
runtimecfg.Set(cfg)
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()
}
sentinelService.SetManager(channelManager)
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("✗ Reload failed (start channels): %v\n", err)
return
}
go agentLoop.Run(ctx)
fmt.Println("✓ Config hot-reload applied")
}
for {
select {
case <-reloadReqCh:
applyReload()
case sig := <-sigChan:
switch {
case isGatewayReloadSignal(sig):
applyReload()
default:
fmt.Println("\nShutting down...")
cancel()
heartbeatService.Stop()
sentinelService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("✓ Gateway stopped")
return
}
}
} }
} }

View File

@@ -88,15 +88,49 @@ func extractSubagentDescription(content string) string {
} }
func formatCreatedSubagentForUser(result map[string]interface{}, configPath string) string { func formatCreatedSubagentForUser(result map[string]interface{}, configPath string) string {
subagent, _ := result["subagent"].(map[string]interface{})
role := ""
displayName := ""
toolAllowlist := interface{}(nil)
systemPromptFile := ""
if subagent != nil {
if v, _ := subagent["role"].(string); v != "" {
role = v
}
if v, _ := subagent["display_name"].(string); v != "" {
displayName = v
}
if tools, ok := subagent["tools"].(map[string]interface{}); ok {
toolAllowlist = tools["allowlist"]
}
if v, _ := subagent["system_prompt_file"].(string); v != "" {
systemPromptFile = v
}
}
routingKeywords := interface{}(nil)
if rules, ok := result["rules"].([]interface{}); ok {
agentID, _ := result["agent_id"].(string)
for _, raw := range rules {
rule, ok := raw.(map[string]interface{})
if !ok {
continue
}
if strings.TrimSpace(fmt.Sprint(rule["agent_id"])) != agentID {
continue
}
routingKeywords = rule["keywords"]
break
}
}
return fmt.Sprintf( return fmt.Sprintf(
"subagent 已写入 config.json。\npath: %s\nagent_id: %v\nrole: %v\ndisplay_name: %v\ntool_allowlist: %v\nrouting_keywords: %v\nsystem_prompt_file: %v", "subagent 已写入 config.json。\npath: %s\nagent_id: %v\nrole: %v\ndisplay_name: %v\ntool_allowlist: %v\nrouting_keywords: %v\nsystem_prompt_file: %v",
configPath, configPath,
result["agent_id"], result["agent_id"],
result["role"], role,
result["display_name"], displayName,
result["tool_allowlist"], toolAllowlist,
result["routing_keywords"], routingKeywords,
result["system_prompt_file"], systemPromptFile,
) )
} }

View File

@@ -1,71 +1,43 @@
package agent package agent
import ( import (
"context"
"path/filepath"
"strings" "strings"
"testing" "testing"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
) )
func TestMaybeHandleSubagentConfigIntentCreatePersistsImmediately(t *testing.T) { func TestFormatCreatedSubagentForUserReadsNestedFields(t *testing.T) {
workspace := t.TempDir() t.Parallel()
configPath := filepath.Join(workspace, "config.json")
cfg := config.DefaultConfig()
cfg.Agents.Router.Enabled = true
cfg.Agents.Subagents["main"] = config.SubagentConfig{
Enabled: true,
Type: "router",
Role: "orchestrator",
SystemPromptFile: "agents/main/AGENT.md",
}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("save config failed: %v", err)
}
runtimecfg.Set(cfg)
t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) })
loop := &AgentLoop{configPath: configPath} out := formatCreatedSubagentForUser(map[string]interface{}{
out, handled, err := loop.maybeHandleSubagentConfigIntent(context.Background(), bus.InboundMessage{ "agent_id": "coder",
SessionKey: "main", "subagent": map[string]interface{}{
Channel: "cli", "role": "coding",
Content: "创建一个负责回归测试和验证修复结果的子代理", "display_name": "Code Agent",
}) "system_prompt_file": "agents/coder/AGENT.md",
if err != nil { "tools": map[string]interface{}{
t.Fatalf("create subagent failed: %v", err) "allowlist": []interface{}{"filesystem", "shell"},
} },
if !handled || !strings.Contains(out, "已写入 config.json") { },
t.Fatalf("expected immediate persist response, got handled=%v out=%q", handled, out) "rules": []interface{}{
} map[string]interface{}{
if !strings.Contains(out, configPath) { "agent_id": "coder",
t.Fatalf("expected response to include config path, got %q", out) "keywords": []interface{}{"code", "fix"},
} },
},
}, "/tmp/config.json")
reloaded, err := config.LoadConfig(configPath) for _, want := range []string{
if err != nil { "agent_id: coder",
t.Fatalf("reload config failed: %v", err) "role: coding",
} "display_name: Code Agent",
if _, ok := reloaded.Agents.Subagents["tester"]; !ok { "system_prompt_file: agents/coder/AGENT.md",
t.Fatalf("expected tester subagent to persist, got %+v", reloaded.Agents.Subagents) "routing_keywords: [code fix]",
} } {
} if !strings.Contains(out, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, out)
func TestMaybeHandleSubagentConfigIntentConfirmCancelNoLongerHandled(t *testing.T) {
loop := &AgentLoop{}
for _, content := range []string{"确认创建", "取消创建"} {
out, handled, err := loop.maybeHandleSubagentConfigIntent(context.Background(), bus.InboundMessage{
SessionKey: "main",
Channel: "cli",
Content: content,
})
if err != nil {
t.Fatalf("unexpected error for %q: %v", content, err)
}
if handled || out != "" {
t.Fatalf("expected %q to pass through, got handled=%v out=%q", content, handled, out)
} }
} }
if strings.Contains(out, "<nil>") {
t.Fatalf("did not expect nil placeholders, got:\n%s", out)
}
} }