mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-09 13:07:28 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4cf4a123b | ||
|
|
623b401850 | ||
|
|
61c46fd710 |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ func ensureConfigOnboard(configPath string, defaults *config.Config) (string, er
|
|||||||
if defaults == nil {
|
if defaults == nil {
|
||||||
return "", fmt.Errorf("defaults is nil")
|
return "", fmt.Errorf("defaults is nil")
|
||||||
}
|
}
|
||||||
|
if defaults.Gateway.Token == "" {
|
||||||
|
defaults.Gateway.Token = config.DefaultConfig().Gateway.Token
|
||||||
|
}
|
||||||
|
|
||||||
exists := true
|
exists := true
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
|||||||
31
cmd/clawgo/cmd_onboard_test.go
Normal file
31
cmd/clawgo/cmd_onboard_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"clawgo/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureConfigOnboardGeneratesGatewayToken(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := t.TempDir() + "/config.json"
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Gateway.Token = ""
|
||||||
|
|
||||||
|
state, err := ensureConfigOnboard(configPath, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ensureConfigOnboard failed: %v", err)
|
||||||
|
}
|
||||||
|
if state != "created" {
|
||||||
|
t.Fatalf("unexpected state: %s", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := config.LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load config failed: %v", err)
|
||||||
|
}
|
||||||
|
if loaded.Gateway.Token == "" {
|
||||||
|
t.Fatalf("expected gateway token to be generated")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,10 @@ func splitPlannedSegments(content string) []string {
|
|||||||
return bullet
|
return bullet
|
||||||
}
|
}
|
||||||
|
|
||||||
replaced := strings.NewReplacer(";", ";", "\n", ";", "。然后", ";", " 然后 ", ";", " and then ", ";")
|
// Only split implicit plans on strong separators. Plain newlines are often
|
||||||
|
// just formatting inside a single request, and "然后/and then" frequently
|
||||||
|
// describes execution order inside one task rather than separate tasks.
|
||||||
|
replaced := strings.NewReplacer(";", ";")
|
||||||
norm := replaced.Replace(content)
|
norm := replaced.Replace(content)
|
||||||
parts := strings.Split(norm, ";")
|
parts := strings.Split(norm, ";")
|
||||||
out := make([]string, 0, len(parts))
|
out := make([]string, 0, len(parts))
|
||||||
@@ -180,7 +183,7 @@ func (al *AgentLoop) publishPlannedTaskProgress(msg bus.InboundMessage, total in
|
|||||||
if body == "" {
|
if body == "" {
|
||||||
body = "(无输出)"
|
body = "(无输出)"
|
||||||
}
|
}
|
||||||
body = truncate(strings.ReplaceAll(body, "\n", " "), 280)
|
body = summarizePlannedTaskProgressBody(body, 6, 320)
|
||||||
content := fmt.Sprintf("进度 %d/%d:任务%d已%s\n%s", idx, total, idx, status, body)
|
content := fmt.Sprintf("进度 %d/%d:任务%d已%s\n%s", idx, total, idx, status, body)
|
||||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
@@ -189,6 +192,37 @@ func (al *AgentLoop) publishPlannedTaskProgress(msg bus.InboundMessage, total in
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summarizePlannedTaskProgressBody(body string, maxLines, maxChars int) string {
|
||||||
|
body = strings.ReplaceAll(body, "\r\n", "\n")
|
||||||
|
body = strings.TrimSpace(body)
|
||||||
|
if body == "" {
|
||||||
|
return "(无输出)"
|
||||||
|
}
|
||||||
|
lines := strings.Split(body, "\n")
|
||||||
|
out := make([]string, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, line)
|
||||||
|
if maxLines > 0 && len(out) >= maxLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return "(无输出)"
|
||||||
|
}
|
||||||
|
joined := strings.Join(out, "\n")
|
||||||
|
if maxChars > 0 && len(joined) > maxChars {
|
||||||
|
joined = truncate(joined, maxChars)
|
||||||
|
}
|
||||||
|
if len(lines) > len(out) && !strings.HasSuffix(joined, "...") {
|
||||||
|
joined += "\n..."
|
||||||
|
}
|
||||||
|
return joined
|
||||||
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) enrichTaskContentWithMemoryAndEKG(ctx context.Context, task plannedTask) string {
|
func (al *AgentLoop) enrichTaskContentWithMemoryAndEKG(ctx context.Context, task plannedTask) string {
|
||||||
base := strings.TrimSpace(task.Content)
|
base := strings.TrimSpace(task.Content)
|
||||||
if base == "" {
|
if base == "" {
|
||||||
|
|||||||
33
pkg/agent/session_planner_split_test.go
Normal file
33
pkg/agent/session_planner_split_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSplitPlannedSegmentsDoesNotSplitPlainNewlines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
content := "编写ai漫画创作平台demo\n让产品出方案,方案出完让前端后端开始编写,写完后交个测试过一下"
|
||||||
|
got := splitPlannedSegments(content)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 segment, got %d: %#v", len(got), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPlannedSegmentsStillSplitsBullets(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
content := "1. 先实现前端\n2. 再补测试"
|
||||||
|
got := splitPlannedSegments(content)
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 segments, got %d: %#v", len(got), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPlannedSegmentsStillSplitsSemicolons(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
content := "先实现前端;再补测试"
|
||||||
|
got := splitPlannedSegments(content)
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 segments, got %d: %#v", len(got), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
pkg/agent/session_planner_test.go
Normal file
23
pkg/agent/session_planner_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSummarizePlannedTaskProgressBodyPreservesUsefulLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "subagent 已写入 config.json。\npath: /root/.clawgo/config.json\nagent_id: tester\nrole: testing\ndisplay_name: Test Agent\ntool_allowlist: [filesystem shell]\nrouting_keywords: [test qa]\nsystem_prompt_file: agents/tester/AGENT.md"
|
||||||
|
out := summarizePlannedTaskProgressBody(body, 6, 320)
|
||||||
|
|
||||||
|
if !strings.Contains(out, "subagent 已写入 config.json。") {
|
||||||
|
t.Fatalf("expected title line, got:\n%s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "agent_id: tester") {
|
||||||
|
t.Fatalf("expected agent id line, got:\n%s", out)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "subagent 已写入 config.json。 path:") {
|
||||||
|
t.Fatalf("expected multi-line formatting, got:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -510,7 +512,7 @@ func DefaultConfig() *Config {
|
|||||||
Gateway: GatewayConfig{
|
Gateway: GatewayConfig{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 18790,
|
Port: 18790,
|
||||||
Token: "",
|
Token: generateGatewayToken(),
|
||||||
},
|
},
|
||||||
Cron: CronConfig{
|
Cron: CronConfig{
|
||||||
MinSleepSec: 1,
|
MinSleepSec: 1,
|
||||||
@@ -568,6 +570,14 @@ func DefaultConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateGatewayToken() string {
|
||||||
|
var buf [16]byte
|
||||||
|
if _, err := rand.Read(buf[:]); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "cg_" + hex.EncodeToString(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ package config
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestDefaultConfigGeneratesGatewayToken(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if cfg.Gateway.Token == "" {
|
||||||
|
t.Fatalf("expected default gateway token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateSubagentsAllowsKnownPeers(t *testing.T) {
|
func TestValidateSubagentsAllowsKnownPeers(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user