mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-09 10:37:28 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
623b401850 |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user