Merge remote-tracking branch 'origin/main'

This commit is contained in:
lpf
2026-03-10 09:37:52 +08:00
87 changed files with 761 additions and 364 deletions

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
FROM golang:1.25.5-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN rm -rf cmd/workspace \
&& mkdir -p cmd/workspace \
&& cp -a workspace/. cmd/workspace/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -buildvcs=false -ldflags="-s -w" -o /out/clawgo ./cmd
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/sh clawgo
USER clawgo
WORKDIR /home/clawgo
COPY --from=builder /out/clawgo /usr/local/bin/clawgo
ENV CLAWGO_CONFIG=/home/clawgo/.clawgo/config.json
EXPOSE 18790
VOLUME ["/home/clawgo/.clawgo"]
ENTRYPOINT ["/bin/sh", "-c", "if [ ! -f \"$CLAWGO_CONFIG\" ]; then /usr/local/bin/clawgo onboard; fi; exec /usr/local/bin/clawgo gateway run --config \"$CLAWGO_CONFIG\""]

View File

@@ -3,7 +3,7 @@
# Build variables
BINARY_NAME=clawgo
BUILD_DIR=build
CMD_DIR=cmd/$(BINARY_NAME)
CMD_DIR=cmd
MAIN_GO=$(CMD_DIR)/main.go
# Version
@@ -46,7 +46,7 @@ WORKSPACE_DIR?=$(CLAWGO_HOME)/workspace
WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills
BUILTIN_SKILLS_DIR=$(CURDIR)/skills
WORKSPACE_SOURCE_DIR=$(CURDIR)/workspace
EMBED_WORKSPACE_DIR=$(CURDIR)/cmd/$(BINARY_NAME)/workspace
EMBED_WORKSPACE_DIR=$(CURDIR)/cmd/workspace
EMBED_WEBUI_DIR=$(EMBED_WORKSPACE_DIR)/webui
DEV_CONFIG?=$(if $(wildcard $(CURDIR)/config.json),$(CURDIR)/config.json,$(CLAWGO_HOME)/config.json)
DEV_ARGS?=--debug gateway run
@@ -189,11 +189,11 @@ package-all: build-all
fi
@echo "Package complete: $(BUILD_DIR)"
## sync-embed-workspace: Sync workspace seed files and built WebUI into cmd/clawgo/workspace for go:embed
## sync-embed-workspace: Sync workspace seed files and built WebUI into cmd/workspace for go:embed
sync-embed-workspace: sync-embed-workspace-base sync-embed-webui
@echo "✓ Embed assets ready in $(EMBED_WORKSPACE_DIR)"
## sync-embed-workspace-base: Sync root workspace files into cmd/clawgo/workspace for go:embed
## sync-embed-workspace-base: Sync root workspace files into cmd/workspace for go:embed
sync-embed-workspace-base:
@echo "Syncing workspace seed files for embedding..."
@if [ ! -d "$(WORKSPACE_SOURCE_DIR)" ]; then \

View File

@@ -7,8 +7,8 @@ import (
"path/filepath"
"strings"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
func copyDirectory(src, dst string) error {

View File

@@ -9,11 +9,11 @@ import (
"path/filepath"
"strings"
"clawgo/pkg/agent"
"clawgo/pkg/bus"
"clawgo/pkg/cron"
"clawgo/pkg/logger"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/agent"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/chzyer/readline"
)
@@ -27,7 +27,7 @@ func agentCmd() {
switch args[i] {
case "--debug", "-d":
logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled")
fmt.Println("馃攳 Debug mode enabled")
case "-m", "--message":
if i+1 < len(args) {
message = args[i+1]

View File

@@ -10,9 +10,9 @@ import (
"strings"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/channels"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/channels"
"github.com/YspCoder/clawgo/pkg/config"
qrterminal "github.com/mdp/qrterminal/v3"
)
@@ -55,7 +55,7 @@ func channelHelp() {
func channelTestCmd() {
to := ""
channelName := ""
message := "This is a test message from ClawGo 🦞"
message := "This is a test message from ClawGo 馃"
args := os.Args[3:]
for i := 0; i < len(args); i++ {
@@ -104,11 +104,11 @@ func channelTestCmd() {
fmt.Printf("Sending test message to %s (%s)...\n", channelName, to)
if err := mgr.SendToChannel(ctx, channelName, to, message); err != nil {
fmt.Printf("Failed to send message: %v\n", err)
fmt.Printf("鉁?Failed to send message: %v\n", err)
os.Exit(1)
}
fmt.Println("Test message sent successfully!")
fmt.Println("鉁?Test message sent successfully!")
}
func whatsAppChannelCmd() {

View File

@@ -9,8 +9,8 @@ import (
"strconv"
"strings"
"clawgo/pkg/config"
"clawgo/pkg/configops"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/configops"
)
func configCmd() {
@@ -100,7 +100,7 @@ func configSetCmd() {
return
}
fmt.Printf("Updated %s = %v\n", path, value)
fmt.Printf("鉁?Updated %s = %v\n", path, value)
running, err := triggerGatewayReload()
if err != nil {
if running {
@@ -113,7 +113,7 @@ func configSetCmd() {
}
fmt.Printf("Updated config file. Hot reload not applied: %v\n", err)
} else {
fmt.Println("Gateway hot reload signal sent")
fmt.Println("鉁?Gateway hot reload signal sent")
}
}
@@ -150,7 +150,7 @@ func configReloadCmd() {
fmt.Printf("Hot reload not applied: %v\n", err)
return
}
fmt.Println("Gateway hot reload signal sent")
fmt.Println("鉁?Gateway hot reload signal sent")
}
func configCheckCmd() {
@@ -161,11 +161,11 @@ func configCheckCmd() {
}
validationErrors := config.Validate(cfg)
if len(validationErrors) == 0 {
fmt.Println("Config validation passed")
fmt.Println("鉁?Config validation passed")
return
}
fmt.Println("Config validation failed:")
fmt.Println("鉁?Config validation failed:")
for _, ve := range validationErrors {
fmt.Printf(" - %v\n", ve)
}
@@ -265,7 +265,7 @@ func providerCmd() {
os.Exit(1)
}
fmt.Println("Provider configuration saved.")
fmt.Println("鉁?Provider configuration saved.")
running, err := triggerGatewayReload()
if err != nil {
if running {
@@ -275,7 +275,7 @@ func providerCmd() {
fmt.Printf("Gateway not running, reload skipped: %v\n", err)
return
}
fmt.Println("Gateway hot reload signal sent")
fmt.Println("鉁?Gateway hot reload signal sent")
}
func providerNames(cfg *config.Config) []string {

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"time"
"clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/cron"
)
func cronCmd() {
@@ -177,15 +177,15 @@ func cronAddCmd(storePath string) {
return
}
fmt.Printf("Added job '%s' (%s)\n", job.Name, job.ID)
fmt.Printf("鉁?Added job '%s' (%s)\n", job.Name, job.ID)
}
func cronRemoveCmd(storePath, jobID string) {
cs := cron.NewCronService(storePath, nil)
if cs.RemoveJob(jobID) {
fmt.Printf("Removed job %s\n", jobID)
fmt.Printf("鉁?Removed job %s\n", jobID)
} else {
fmt.Printf("Job %s not found\n", jobID)
fmt.Printf("鉁?Job %s not found\n", jobID)
}
}
@@ -205,8 +205,8 @@ func cronEnableCmd(storePath string, disable bool) {
if disable {
status = "disabled"
}
fmt.Printf("Job '%s' %s\n", job.Name, status)
fmt.Printf("鉁?Job '%s' %s\n", job.Name, status)
} else {
fmt.Printf("Job %s not found\n", jobID)
fmt.Printf("鉁?Job %s not found\n", jobID)
}
}

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"net/url"
"os"
"os/exec"
"os/signal"
@@ -12,18 +13,18 @@ import (
"strings"
"time"
"clawgo/pkg/agent"
"clawgo/pkg/api"
"clawgo/pkg/bus"
"clawgo/pkg/channels"
"clawgo/pkg/config"
"clawgo/pkg/cron"
"clawgo/pkg/heartbeat"
"clawgo/pkg/logger"
"clawgo/pkg/nodes"
"clawgo/pkg/providers"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/sentinel"
"github.com/YspCoder/clawgo/pkg/agent"
"github.com/YspCoder/clawgo/pkg/api"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/channels"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/heartbeat"
"github.com/YspCoder/clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/sentinel"
"github.com/pion/webrtc/v4"
)
@@ -86,6 +87,9 @@ func gatewayCmd() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if shouldEmbedWhatsAppBridge(cfg) {
cfg.Channels.WhatsApp.BridgeURL = embeddedWhatsAppBridgeURL(cfg)
}
agentLoop, channelManager, err := buildGatewayRuntime(ctx, cfg, msgBus, cronService)
if err != nil {
@@ -103,30 +107,26 @@ func gatewayCmd() {
enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 {
fmt.Printf("Channels enabled: %s\n", enabledChannels)
fmt.Printf("鉁?Channels enabled: %s\n", enabledChannels)
} else {
fmt.Println("Warning: No channels enabled")
fmt.Println("鈿?Warning: No channels enabled")
}
fmt.Printf("Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
fmt.Printf("鉁?Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
fmt.Println("Press Ctrl+C to stop. Send SIGHUP to hot-reload config.")
if err := cronService.Start(); err != nil {
fmt.Printf("Error starting cron service: %v\n", err)
}
fmt.Println("Cron service started")
fmt.Println("鉁?Cron service started")
if err := heartbeatService.Start(); err != nil {
fmt.Printf("Error starting heartbeat service: %v\n", err)
}
fmt.Println("Heartbeat service started")
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)
fmt.Println("鉁?Sentinel service started")
}
registryServer := api.NewServer(cfg.Gateway.Host, cfg.Gateway.Port, cfg.Gateway.Token, nodes.DefaultManager())
@@ -223,6 +223,10 @@ func gatewayCmd() {
registryServer.SetToolsCatalogHandler(func() interface{} {
return agentLoop.GetToolCatalog()
})
whatsAppBridge, whatsAppEmbedded := setupEmbeddedWhatsAppBridge(ctx, cfg)
if whatsAppBridge != nil {
registryServer.SetWhatsAppBridge(whatsAppBridge, embeddedWhatsAppBridgeBasePath)
}
registryServer.SetCronHandler(func(action string, args map[string]interface{}) (interface{}, error) {
getStr := func(k string) string {
v, _ := args[k].(string)
@@ -363,7 +367,11 @@ func gatewayCmd() {
if err := registryServer.Start(ctx); err != nil {
fmt.Printf("Error starting node registry server: %v\n", err)
} else {
fmt.Printf("Node registry server started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
fmt.Printf("鉁?Node registry server started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
}
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("Error starting channels: %v\n", err)
}
go agentLoop.Run(ctx)
@@ -373,10 +381,10 @@ func gatewayCmd() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, gatewayNotifySignals()...)
applyReload := func() {
fmt.Println("\nReloading config...")
fmt.Println("\n鈫?Reloading config...")
newCfg, err := config.LoadConfig(getConfigPath())
if err != nil {
fmt.Printf("Reload failed (load config): %v\n", err)
fmt.Printf("鉁?Reload failed (load config): %v\n", err)
return
}
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
@@ -390,10 +398,14 @@ func gatewayCmd() {
}
if reflect.DeepEqual(cfg, newCfg) {
fmt.Println("Config unchanged, skip reload")
fmt.Println("鉁?Config unchanged, skip reload")
return
}
if shouldEmbedWhatsAppBridge(newCfg) {
newCfg.Channels.WhatsApp.BridgeURL = embeddedWhatsAppBridgeURL(newCfg)
}
runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) &&
reflect.DeepEqual(cfg.Providers, newCfg.Providers) &&
reflect.DeepEqual(cfg.Tools, newCfg.Tools) &&
@@ -425,24 +437,32 @@ func gatewayCmd() {
cfg = newCfg
runtimecfg.Set(cfg)
configureGatewayNodeP2P(agentLoop, registryServer, cfg)
fmt.Println("Config hot-reload applied (logging/metadata only)")
fmt.Println("鉁?Config hot-reload applied (logging/metadata only)")
return
}
newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService)
if err != nil {
fmt.Printf("Reload failed (init runtime): %v\n", err)
fmt.Printf("鉁?Reload failed (init runtime): %v\n", err)
return
}
newWhatsAppBridge, _ := setupEmbeddedWhatsAppBridge(ctx, newCfg)
channelManager.StopAll(ctx)
agentLoop.Stop()
if whatsAppBridge != nil {
whatsAppBridge.Stop()
}
channelManager = newChannelManager
agentLoop = newAgentLoop
cfg = newCfg
whatsAppBridge = newWhatsAppBridge
whatsAppEmbedded = newWhatsAppBridge != nil
runtimecfg.Set(cfg)
configureGatewayNodeP2P(agentLoop, registryServer, cfg)
registryServer.SetWhatsAppBridge(whatsAppBridge, embeddedWhatsAppBridgeBasePath)
sentinelService.Stop()
sentinelService = sentinel.NewService(
getConfigPath(),
@@ -465,11 +485,11 @@ func gatewayCmd() {
sentinelService.SetManager(channelManager)
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("Reload failed (start channels): %v\n", err)
fmt.Printf("鉁?Reload failed (start channels): %v\n", err)
return
}
go agentLoop.Run(ctx)
fmt.Println("Config hot-reload applied")
fmt.Println("鉁?Config hot-reload applied")
}
for {
@@ -483,18 +503,23 @@ func gatewayCmd() {
default:
fmt.Println("\nShutting down...")
cancel()
if whatsAppEmbedded && whatsAppBridge != nil {
whatsAppBridge.Stop()
}
heartbeatService.Stop()
sentinelService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("Gateway stopped")
fmt.Println("鉁?Gateway stopped")
return
}
}
}
}
const embeddedWhatsAppBridgeBasePath = "/whatsapp"
func runGatewayStartupCompactionCheck(parent context.Context, agentLoop *agent.AgentLoop) {
if agentLoop == nil {
return
@@ -581,7 +606,7 @@ func gatewayInstallServiceCmd() error {
return err
}
fmt.Printf("Gateway service registered: %s (%s)\n", gatewayServiceName, scope)
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")
@@ -741,9 +766,9 @@ func buildGatewayRuntime(ctx context.Context, cfg *config.Config, msgBus *bus.Me
startupInfo := agentLoop.GetStartupInfo()
toolsInfo := startupInfo["tools"].(map[string]interface{})
skillsInfo := startupInfo["skills"].(map[string]interface{})
fmt.Println("\n📦 Agent Status:")
fmt.Printf(" Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"])
fmt.Println("\n馃摝 Agent Status:")
fmt.Printf(" 鈥?Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" 鈥?Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"])
logger.InfoCF("agent", logger.C0098,
map[string]interface{}{
@@ -843,3 +868,65 @@ func buildHeartbeatService(cfg *config.Config, msgBus *bus.MessageBus) *heartbea
return "queued", nil
}, hbInterval, cfg.Agents.Defaults.Heartbeat.Enabled, cfg.Agents.Defaults.Heartbeat.PromptTemplate)
}
func setupEmbeddedWhatsAppBridge(ctx context.Context, cfg *config.Config) (*channels.WhatsAppBridgeService, bool) {
if cfg == nil || !cfg.Channels.WhatsApp.Enabled || !shouldEmbedWhatsAppBridge(cfg) {
return nil, false
}
cfg.Channels.WhatsApp.BridgeURL = embeddedWhatsAppBridgeURL(cfg)
stateDir := filepath.Join(filepath.Dir(getConfigPath()), "channels", "whatsapp")
svc := channels.NewWhatsAppBridgeService(fmt.Sprintf("%s:%d", cfg.Gateway.Host, cfg.Gateway.Port), stateDir, false)
if err := svc.StartEmbedded(ctx); err != nil {
fmt.Printf("Error starting embedded WhatsApp bridge: %v\n", err)
return nil, false
}
return svc, true
}
func shouldEmbedWhatsAppBridge(cfg *config.Config) bool {
raw := strings.TrimSpace(cfg.Channels.WhatsApp.BridgeURL)
if raw == "" {
return true
}
hostPort := comparableBridgeHostPort(raw)
if hostPort == "" {
return false
}
if hostPort == "127.0.0.1:3001" || hostPort == "localhost:3001" {
return true
}
return hostPort == comparableGatewayHostPort(cfg.Gateway.Host, cfg.Gateway.Port)
}
func embeddedWhatsAppBridgeURL(cfg *config.Config) string {
host := strings.TrimSpace(cfg.Gateway.Host)
switch host {
case "", "0.0.0.0", "::", "[::]":
host = "127.0.0.1"
}
return fmt.Sprintf("ws://%s:%d%s/ws", host, cfg.Gateway.Port, embeddedWhatsAppBridgeBasePath)
}
func comparableBridgeHostPort(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if !strings.Contains(raw, "://") {
return strings.ToLower(raw)
}
u, err := url.Parse(raw)
if err != nil {
return ""
}
return strings.ToLower(strings.TrimSpace(u.Host))
}
func comparableGatewayHostPort(host string, port int) string {
host = strings.TrimSpace(strings.ToLower(host))
switch host {
case "", "0.0.0.0", "::", "[::]":
host = "127.0.0.1"
}
return fmt.Sprintf("%s:%d", host, port)
}

View File

@@ -19,14 +19,14 @@ import (
"sync"
"time"
"clawgo/pkg/agent"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/cron"
"clawgo/pkg/nodes"
"clawgo/pkg/providers"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/agent"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/tools"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v4"
)
@@ -151,11 +151,11 @@ func nodeRegisterCmd(args []string) {
fmt.Printf("Error registering node: %v\n", err)
os.Exit(1)
}
fmt.Printf("Node registered: %s -> %s\n", info.ID, opts.GatewayBase)
fmt.Printf("鉁?Node registered: %s -> %s\n", info.ID, opts.GatewayBase)
if !opts.Watch {
return
}
fmt.Printf("Heartbeat loop started: every %ds\n", opts.HeartbeatSec)
fmt.Printf("鉁?Heartbeat loop started: every %ds\n", opts.HeartbeatSec)
if err := runNodeHeartbeatLoop(client, opts, info); err != nil {
fmt.Printf("Heartbeat loop stopped: %v\n", err)
os.Exit(1)
@@ -175,7 +175,7 @@ func nodeHeartbeatCmd(args []string) {
fmt.Printf("Error sending heartbeat: %v\n", err)
os.Exit(1)
}
fmt.Printf("Heartbeat sent: %s -> %s\n", opts.ID, opts.GatewayBase)
fmt.Printf("鉁?Heartbeat sent: %s -> %s\n", opts.ID, opts.GatewayBase)
}
func parseNodeRegisterArgs(args []string, cfg *config.Config) (nodeRegisterOptions, error) {
@@ -409,23 +409,23 @@ func runNodeHeartbeatLoop(client *http.Client, opts nodeRegisterOptions, info no
for {
if err := runNodeHeartbeatSocket(ctx, opts, info); err != nil {
if ctx.Err() != nil {
fmt.Println("Node heartbeat stopped")
fmt.Println("鉁?Node heartbeat stopped")
return nil
}
fmt.Printf("Warning: node socket closed for %s: %v\n", info.ID, err)
}
if ctx.Err() != nil {
fmt.Println("Node heartbeat stopped")
fmt.Println("鉁?Node heartbeat stopped")
return nil
}
if regErr := postNodeRegister(ctx, client, opts.GatewayBase, opts.Token, info); regErr != nil {
fmt.Printf("Warning: re-register failed for %s: %v\n", info.ID, regErr)
} else {
fmt.Printf("Node re-registered: %s\n", info.ID)
fmt.Printf("鉁?Node re-registered: %s\n", info.ID)
}
select {
case <-ctx.Done():
fmt.Println("Node heartbeat stopped")
fmt.Println("鉁?Node heartbeat stopped")
return nil
case <-time.After(2 * time.Second):
}
@@ -466,7 +466,7 @@ func runNodeHeartbeatSocket(ctx context.Context, opts nodeRegisterOptions, info
if err := waitNodeAck(ctx, acks, errs, "registered", info.ID); err != nil {
return err
}
fmt.Printf("Node socket connected: %s\n", info.ID)
fmt.Printf("鉁?Node socket connected: %s\n", info.ID)
ticker := time.NewTicker(time.Duration(opts.HeartbeatSec) * time.Second)
pingTicker := time.NewTicker(nodeSocketPingInterval(opts.HeartbeatSec))
@@ -493,7 +493,7 @@ func runNodeHeartbeatSocket(ctx context.Context, opts nodeRegisterOptions, info
if err := waitNodeAck(ctx, acks, errs, "heartbeat", info.ID); err != nil {
return err
}
fmt.Printf("Heartbeat ok: %s\n", info.ID)
fmt.Printf("鉁?Heartbeat ok: %s\n", info.ID)
}
}
}

View File

@@ -11,10 +11,10 @@ import (
"testing"
"time"
"clawgo/pkg/agent"
"clawgo/pkg/config"
"clawgo/pkg/nodes"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/agent"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/providers"
)
type stubNodeProvider struct {

View File

@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
type onboardOptions struct {

View File

@@ -3,7 +3,7 @@ package main
import (
"testing"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestParseOnboardOptionsSyncWebUI(t *testing.T) {

View File

@@ -8,8 +8,8 @@ import (
"strings"
"time"
"clawgo/pkg/config"
"clawgo/pkg/skills"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/skills"
)
func skillsCmd() {

View File

@@ -8,8 +8,8 @@ import (
"sort"
"strings"
"clawgo/pkg/nodes"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/providers"
)
func statusCmd() {
@@ -24,16 +24,16 @@ func statusCmd() {
fmt.Printf("%s clawgo Status\n\n", logo)
if _, err := os.Stat(configPath); err == nil {
fmt.Println("Config:", configPath, "")
fmt.Println("Config:", configPath, "[ok]")
} else {
fmt.Println("Config:", configPath, "")
fmt.Println("Config:", configPath, "[missing]")
}
workspace := cfg.WorkspacePath()
if _, err := os.Stat(workspace); err == nil {
fmt.Println("Workspace:", workspace, "")
fmt.Println("Workspace:", workspace, "[ok]")
} else {
fmt.Println("Workspace:", workspace, "")
fmt.Println("Workspace:", workspace, "[missing]")
}
if _, err := os.Stat(configPath); err == nil {
@@ -59,7 +59,7 @@ func statusCmd() {
hasKey := strings.TrimSpace(activeProvider.APIKey) != ""
status := "not set"
if hasKey {
status = ""
status = "configured"
}
fmt.Printf("Provider API Key: %s\n", status)
fmt.Printf("Logging: %v\n", cfg.Logging.Enabled)

View File

@@ -8,7 +8,7 @@ import (
"strings"
"testing"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestStatusCmdUsesActiveProviderDetails(t *testing.T) {
@@ -71,7 +71,7 @@ func TestStatusCmdUsesActiveProviderDetails(t *testing.T) {
if !strings.Contains(out, "Provider API Base: https://backup.example/v1") {
t.Fatalf("expected active provider api base in output, got: %s", out)
}
if !strings.Contains(out, "Provider API Key: ") {
if !strings.Contains(out, "Provider API Key: configured") {
t.Fatalf("expected active provider api key status in output, got: %s", out)
}
if !strings.Contains(out, "Nodes P2P: enabled=true transport=webrtc") {

View File

@@ -12,8 +12,8 @@ import (
"fmt"
"os"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
//go:embed workspace
@@ -22,7 +22,7 @@ var embeddedFiles embed.FS
var version = "dev"
var buildTime = "unknown"
const logo = "🦞"
const logo = "馃"
const gatewayServiceName = "clawgo-gateway.service"
const envRootGranted = "CLAWGO_ROOT_GRANTED"

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
clawgo:
build:
context: .
dockerfile: Dockerfile
container_name: clawgo
restart: unless-stopped
ports:
- "18790:18790"
environment:
TZ: Asia/Shanghai
CLAWGO_CONFIG: /home/clawgo/.clawgo/config.json
volumes:
- ./.clawgo:/home/clawgo/.clawgo

69
go.mod
View File

@@ -1,6 +1,6 @@
module clawgo
module github.com/YspCoder/clawgo
go 1.25.5
go 1.25.7
require (
github.com/bwmarrin/discordgo v0.29.0
@@ -9,20 +9,22 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/mdp/qrterminal/v3 v3.2.1
github.com/mymmrac/telego v1.6.0
github.com/mymmrac/telego v1.7.0
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/pion/webrtc/v4 v4.1.6
github.com/pion/webrtc/v4 v4.2.9
github.com/robfig/cron/v3 v3.0.1
github.com/tencent-connect/botgo v0.2.1
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.19.0
golang.org/x/time v0.14.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.11
modernc.org/sqlite v1.46.1
rsc.io/qr v0.2.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
@@ -32,31 +34,32 @@ require (
github.com/coder/websocket v1.8.14 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.41 // indirect
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/ice/v4 v4.2.1 // indirect
github.com/pion/interceptor v0.1.44 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.23 // indirect
github.com/pion/sctp v1.8.40 // indirect
github.com/pion/sdp/v3 v3.0.16 // indirect
github.com/pion/srtp/v3 v3.0.8 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.8 // indirect
github.com/pion/turn/v4 v4.1.1 // indirect
github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.10.1 // indirect
github.com/pion/sctp v1.9.2 // indirect
github.com/pion/sdp/v3 v3.0.18 // indirect
github.com/pion/srtp/v3 v3.0.10 // indirect
github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -65,21 +68,19 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/util v0.9.6 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect
)

65
go.sum
View File

@@ -1,6 +1,8 @@
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
@@ -49,6 +51,8 @@ github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Px
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -88,8 +92,12 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -111,6 +119,8 @@ github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFe
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -125,38 +135,70 @@ github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/ice/v4 v4.2.1 h1:XPRYXaLiFq3LFDG7a7bMrmr3mFr27G/gtXN3v/TVfxY=
github.com/pion/ice/v4 v4.2.1/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c=
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=
github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.8.23 h1:kxX3bN4nM97DPrVBGq5I/Xcl332HnTHeP1Swx3/MCnU=
github.com/pion/rtp v1.8.23/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=
github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
github.com/pion/webrtc/v4 v4.2.9 h1:DZIh1HAhPIL3RvwEDFsmL5hfPSLEpxsQk9/Jir2vkJE=
github.com/pion/webrtc/v4 v4.2.9/go.mod h1:9EmLZve0H76eTzf8v2FmchZ6tcBXtDgpfTEu+drW6SY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -203,8 +245,12 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -222,6 +268,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -232,6 +280,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -253,9 +303,13 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -264,6 +318,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -288,6 +344,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -306,6 +364,8 @@ golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -346,16 +406,21 @@ modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

@@ -8,9 +8,9 @@ import (
"strings"
"time"
"clawgo/pkg/logger"
"clawgo/pkg/providers"
"clawgo/pkg/skills"
"github.com/YspCoder/clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/skills"
)
type ContextBuilder struct {

View File

@@ -23,17 +23,17 @@ import (
"time"
"unicode/utf8"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/cron"
"clawgo/pkg/ekg"
"clawgo/pkg/logger"
"clawgo/pkg/nodes"
"clawgo/pkg/providers"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/scheduling"
"clawgo/pkg/session"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/ekg"
"github.com/YspCoder/clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/scheduling"
"github.com/YspCoder/clawgo/pkg/session"
"github.com/YspCoder/clawgo/pkg/tools"
)
type AgentLoop struct {

View File

@@ -3,8 +3,8 @@ package agent
import (
"testing"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestNewAgentLoopDisablesNodeP2PByDefault(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"context"
"testing"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/providers"
)
type stubLLMProvider struct{}

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/bus"
)
func TestPrepareOutboundSubagentNoReplyFallback(t *testing.T) {

View File

@@ -7,9 +7,9 @@ import (
"testing"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
)
func TestAppendDailySummaryLogUsesSubagentNamespaceAndTitle(t *testing.T) {

View File

@@ -5,10 +5,10 @@ import (
"strings"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/tools"
)
func (al *AgentLoop) maybeAutoRoute(ctx context.Context, msg bus.InboundMessage) (string, bool, error) {

View File

@@ -4,10 +4,10 @@ import (
"context"
"testing"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/tools"
)
func TestResolveAutoRouteTarget(t *testing.T) {
@@ -27,9 +27,9 @@ func TestResolveAutoRouteTargetRulesFirst(t *testing.T) {
cfg.Agents.Router.Strategy = "rules_first"
cfg.Agents.Subagents["coder"] = config.SubagentConfig{Enabled: true, Role: "coding", SystemPromptFile: "agents/coder/AGENT.md"}
cfg.Agents.Subagents["tester"] = config.SubagentConfig{Enabled: true, Role: "testing", SystemPromptFile: "agents/tester/AGENT.md"}
cfg.Agents.Router.Rules = []config.AgentRouteRule{{AgentID: "coder", Keywords: []string{"登录", "bug"}}}
cfg.Agents.Router.Rules = []config.AgentRouteRule{{AgentID: "coder", Keywords: []string{"鐧诲綍", "bug"}}}
agentID, task := resolveAutoRouteTarget(cfg, "请帮我修复登录接口的 bug 并改代码")
agentID, task := resolveAutoRouteTarget(cfg, "璇峰府鎴戜慨澶嶇櫥褰曟帴鍙g殑 bug 骞舵敼浠g爜")
if agentID != "coder" || task == "" {
t.Fatalf("expected coder route, got %s / %s", agentID, task)
}
@@ -113,7 +113,7 @@ func TestMaybeAutoRouteDispatchesRulesFirstMatch(t *testing.T) {
Channel: "cli",
ChatID: "direct",
SessionKey: "main",
Content: "请做一次回归测试并验证这个修复",
Content: "璇峰仛涓€娆″洖褰掓祴璇曞苟楠岃瘉杩欎釜淇",
})
if err != nil {
t.Fatalf("rules-first auto route failed: %v", err)

View File

@@ -10,9 +10,9 @@ import (
"strings"
"time"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/tools"
)
func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) {

View File

@@ -7,9 +7,9 @@ import (
"testing"
"time"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/tools"
)
func TestHandleSubagentRuntimeDispatchAndWait(t *testing.T) {

View File

@@ -14,10 +14,10 @@ import (
"sync"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/ekg"
"clawgo/pkg/providers"
"clawgo/pkg/scheduling"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/ekg"
"github.com/YspCoder/clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/scheduling"
)
type plannedTask struct {

View File

@@ -4,8 +4,8 @@ import (
"context"
"testing"
"clawgo/pkg/bus"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/providers"
)
func TestSplitPlannedSegmentsDoesNotSplitPlainNewlines(t *testing.T) {

View File

@@ -7,7 +7,7 @@ import (
"sync"
"sync/atomic"
"clawgo/pkg/scheduling"
"github.com/YspCoder/clawgo/pkg/scheduling"
)
const (

View File

@@ -6,8 +6,8 @@ import (
"path/filepath"
"strings"
"clawgo/pkg/bus"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/tools"
)
func (al *AgentLoop) maybeHandleSubagentConfigIntent(ctx context.Context, msg bus.InboundMessage) (string, bool, error) {

View File

@@ -5,8 +5,8 @@ import (
"strings"
"testing"
"clawgo/pkg/nodes"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/tools"
)
func TestDispatchNodeSubagentTaskUsesNodeAgentTask(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"strings"
"testing"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/tools"
)
func TestBuildSubagentTaskInputPrefersPromptFile(t *testing.T) {

View File

@@ -19,12 +19,12 @@ type triggerAudit struct {
}
type triggerEvent struct {
Time string `json:"time"`
Trigger string `json:"trigger"`
Channel string `json:"channel"`
Session string `json:"session"`
Time string `json:"time"`
Trigger string `json:"trigger"`
Channel string `json:"channel"`
Session string `json:"session"`
Suppressed bool `json:"suppressed,omitempty"`
Error string `json:"error,omitempty"`
Error string `json:"error,omitempty"`
}
func newTriggerAudit(workspace string) *triggerAudit {
@@ -37,10 +37,10 @@ func (ta *triggerAudit) Record(trigger, channel, session string, suppressed bool
}
trigger = normalizeTrigger(trigger)
e := triggerEvent{
Time: time.Now().UTC().Format(time.RFC3339),
Trigger: trigger,
Channel: channel,
Session: session,
Time: time.Now().UTC().Format(time.RFC3339),
Trigger: trigger,
Channel: channel,
Session: session,
Suppressed: suppressed,
}
if err != nil {

View File

@@ -28,10 +28,10 @@ import (
"sync"
"time"
"clawgo/pkg/channels"
cfgpkg "clawgo/pkg/config"
"clawgo/pkg/nodes"
"clawgo/pkg/tools"
"github.com/YspCoder/clawgo/pkg/channels"
cfgpkg "github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/tools"
"github.com/gorilla/websocket"
"rsc.io/qr"
)
@@ -71,6 +71,8 @@ type Server struct {
liveRuntimeOn bool
liveSubagentMu sync.Mutex
liveSubagents map[string]*liveSubagentGroup
whatsAppBridge *channels.WhatsAppBridgeService
whatsAppBase string
}
var nodesWebsocketUpgrader = websocket.Upgrader{
@@ -311,6 +313,42 @@ func (s *Server) SetNodeWebRTCTransport(t *nodes.WebRTCTransport) {
func (s *Server) SetNodeP2PStatusHandler(fn func() map[string]interface{}) {
s.nodeP2PStatus = fn
}
func (s *Server) SetWhatsAppBridge(service *channels.WhatsAppBridgeService, basePath string) {
s.whatsAppBridge = service
s.whatsAppBase = strings.TrimSpace(basePath)
}
func (s *Server) handleWhatsAppBridgeWS(w http.ResponseWriter, r *http.Request) {
if s.whatsAppBridge == nil {
http.Error(w, "whatsapp bridge unavailable", http.StatusServiceUnavailable)
return
}
s.whatsAppBridge.ServeWS(w, r)
}
func (s *Server) handleWhatsAppBridgeStatus(w http.ResponseWriter, r *http.Request) {
if s.whatsAppBridge == nil {
http.Error(w, "whatsapp bridge unavailable", http.StatusServiceUnavailable)
return
}
s.whatsAppBridge.ServeStatus(w, r)
}
func (s *Server) handleWhatsAppBridgeLogout(w http.ResponseWriter, r *http.Request) {
if s.whatsAppBridge == nil {
http.Error(w, "whatsapp bridge unavailable", http.StatusServiceUnavailable)
return
}
s.whatsAppBridge.ServeLogout(w, r)
}
func joinServerRoute(base, endpoint string) string {
base = strings.TrimRight(strings.TrimSpace(base), "/")
if base == "" || base == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return base + "/" + strings.TrimPrefix(endpoint, "/")
}
func (s *Server) rememberNodeConnection(nodeID, connID string) {
nodeID = strings.TrimSpace(nodeID)
@@ -440,6 +478,16 @@ func (s *Server) Start(ctx context.Context) error {
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
mux.HandleFunc("/webui/api/logs/live", s.handleWebUILogsLive)
mux.HandleFunc("/webui/api/logs/recent", s.handleWebUILogsRecent)
if strings.TrimSpace(s.whatsAppBase) != "" {
base := strings.TrimRight(strings.TrimSpace(s.whatsAppBase), "/")
if base == "" {
base = "/whatsapp"
}
mux.HandleFunc(base, s.handleWhatsAppBridgeWS)
mux.HandleFunc(joinServerRoute(base, "ws"), s.handleWhatsAppBridgeWS)
mux.HandleFunc(joinServerRoute(base, "status"), s.handleWhatsAppBridgeStatus)
mux.HandleFunc(joinServerRoute(base, "logout"), s.handleWhatsAppBridgeLogout)
}
s.server = &http.Server{Addr: s.addr, Handler: mux}
go func() {
<-ctx.Done()

View File

@@ -15,8 +15,8 @@ import (
"testing"
"time"
cfgpkg "clawgo/pkg/config"
"clawgo/pkg/nodes"
cfgpkg "github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/gorilla/websocket"
)
@@ -120,6 +120,53 @@ func TestHandleWebUIWhatsAppQR(t *testing.T) {
}
}
func TestHandleWebUIWhatsAppStatusWithNestedBridgePath(t *testing.T) {
t.Parallel()
bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/whatsapp/status":
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"state": "connected",
"connected": true,
"logged_in": true,
"bridge_addr": "127.0.0.1:7788",
"user_jid": "8613012345678@s.whatsapp.net",
"qr_available": false,
"last_event": "connected",
"updated_at": "2026-03-09T12:00:00+08:00",
})
default:
http.NotFound(w, r)
}
}))
defer bridge.Close()
tmp := t.TempDir()
cfgPath := filepath.Join(tmp, "config.json")
cfg := cfgpkg.DefaultConfig()
cfg.Logging.Enabled = false
cfg.Channels.WhatsApp.Enabled = true
cfg.Channels.WhatsApp.BridgeURL = "ws" + strings.TrimPrefix(bridge.URL, "http") + "/whatsapp/ws"
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
t.Fatalf("save config: %v", err)
}
srv := NewServer("127.0.0.1", 0, "", nil)
srv.SetConfigPath(cfgPath)
req := httptest.NewRequest(http.MethodGet, "/webui/api/whatsapp/status", nil)
rec := httptest.NewRecorder()
srv.handleWebUIWhatsAppStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), `"bridge_running":true`) {
t.Fatalf("expected bridge_running=true, got: %s", rec.Body.String())
}
}
func TestHandleWebUIConfigRequiresConfirmForProviderAPIBaseChange(t *testing.T) {
t.Parallel()

View File

@@ -1,8 +1,8 @@
package bus
import (
"clawgo/pkg/logger"
"context"
"github.com/YspCoder/clawgo/pkg/logger"
"sync"
"time"
)

View File

@@ -10,8 +10,8 @@ import (
"sync/atomic"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/logger"
)
type Channel interface {

View File

@@ -6,8 +6,8 @@ import (
"testing"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type recordingChannel struct {
@@ -15,11 +15,11 @@ type recordingChannel struct {
sent []bus.OutboundMessage
}
func (r *recordingChannel) Name() string { return "test" }
func (r *recordingChannel) Start(ctx context.Context) error { return nil }
func (r *recordingChannel) Stop(ctx context.Context) error { return nil }
func (r *recordingChannel) IsRunning() bool { return true }
func (r *recordingChannel) IsAllowed(senderID string) bool { return true }
func (r *recordingChannel) Name() string { return "test" }
func (r *recordingChannel) Start(ctx context.Context) error { return nil }
func (r *recordingChannel) Stop(ctx context.Context) error { return nil }
func (r *recordingChannel) IsRunning() bool { return true }
func (r *recordingChannel) IsAllowed(senderID string) bool { return true }
func (r *recordingChannel) HealthCheck(ctx context.Context) error { return nil }
func (r *recordingChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
r.mu.Lock()
@@ -78,7 +78,6 @@ func TestBaseChannel_HandleMessage_ContentHashFallbackDedupe(t *testing.T) {
}
}
func TestDispatchOutbound_DifferentButtonsShouldNotDeduplicate(t *testing.T) {
mb := bus.NewMessageBus()
mgr, err := NewManager(&config.Config{}, mb)

View File

@@ -8,9 +8,9 @@ import (
"fmt"
"sync"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
)

View File

@@ -1,11 +1,11 @@
package channels
import (
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"context"
"fmt"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
"io"
"net/http"
"os"

View File

@@ -22,9 +22,9 @@ import (
larksheets "github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3"
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
type FeishuChannel struct {
@@ -135,7 +135,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
logger.WarnCF("feishu", logger.C0045, map[string]interface{}{logger.FieldError: lerr.Error(), logger.FieldChatID: msg.ChatID})
continue
}
links = append(links, fmt.Sprintf("表格%d: %s", i+1, link))
links = append(links, fmt.Sprintf("琛ㄦ牸%d: %s", i+1, link))
}
if len(links) > 0 {
if strings.TrimSpace(workMsg.Content) != "" {
@@ -903,9 +903,9 @@ func normalizeFeishuText(s string) string {
// Headers: "## title" -> "title"
s = regexp.MustCompile(`(?m)^#{1,6}\s+`).ReplaceAllString(s, "")
// Bullet styles
s = regexp.MustCompile(`(?m)^[-*]\s+`).ReplaceAllString(s, "")
s = regexp.MustCompile(`(?m)^[-*]\s+`).ReplaceAllString(s, "鈥?")
// Ordered list to bullet for readability
s = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(s, "")
s = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(s, "鈥?")
// Bold/italic/strike markers
s = regexp.MustCompile(`\*\*(.*?)\*\*`).ReplaceAllString(s, `$1`)
s = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(s, `$1`)

View File

@@ -7,9 +7,9 @@ import (
"net"
"sync"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
type MaixCamChannel struct {
@@ -161,7 +161,7 @@ func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) {
w, _ := msg.Data["w"].(float64)
h, _ := msg.Data["h"].(float64)
content := fmt.Sprintf("📷 Person detected!\nClass: %s\nConfidence: %.2f%%\nPosition: (%.0f, %.0f)\nSize: %.0fx%.0f",
content := fmt.Sprintf("馃摲 Person detected!\nClass: %s\nConfidence: %.2f%%\nPosition: (%.0f, %.0f)\nSize: %.0fx%.0f",
classInfo, score*100, x, y, w, h)
metadata := map[string]string{

View File

@@ -16,9 +16,9 @@ import (
"sync/atomic"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
)

View File

@@ -13,9 +13,9 @@ import (
"github.com/tencent-connect/botgo/token"
"golang.org/x/oauth2"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
type QQChannel struct {

View File

@@ -20,9 +20,9 @@ import (
"github.com/mymmrac/telego/telegoutil"
"golang.org/x/time/rate"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
const (
@@ -1144,15 +1144,15 @@ func markdownToTelegramHTML(text string) string {
text = escapeHTML(text)
text = regexp.MustCompile("(?m)^#{1,6}\\s+(.+)$").ReplaceAllString(text, "<b>$1</b>")
text = regexp.MustCompile("(?m)^>\\s*(.*)$").ReplaceAllString(text, "$1")
text = regexp.MustCompile("(?m)^>\\s*(.*)$").ReplaceAllString(text, "鈹?$1")
text = regexp.MustCompile("\\[([^\\]]+)\\]\\(([^)]+)\\)").ReplaceAllString(text, `<a href="$2">$1</a>`)
text = regexp.MustCompile("\\*\\*(.+?)\\*\\*").ReplaceAllString(text, "<b>$1</b>")
text = regexp.MustCompile("__(.+?)__").ReplaceAllString(text, "<b>$1</b>")
text = regexp.MustCompile("\\*([^*\\n]+)\\*").ReplaceAllString(text, "<i>$1</i>")
text = regexp.MustCompile("_([^_\\n]+)_").ReplaceAllString(text, "<i>$1</i>")
text = regexp.MustCompile("~~(.+?)~~").ReplaceAllString(text, "<s>$1</s>")
text = regexp.MustCompile("(?m)^[-*]\\s+").ReplaceAllString(text, "")
text = regexp.MustCompile("(?m)^\\d+\\.\\s+").ReplaceAllString(text, "")
text = regexp.MustCompile("(?m)^[-*]\\s+").ReplaceAllString(text, "鈥?")
text = regexp.MustCompile("(?m)^\\d+\\.\\s+").ReplaceAllString(text, "鈥?")
for i, code := range inlineCodes.codes {
escaped := escapeHTML(code)

View File

@@ -5,7 +5,7 @@ import (
"errors"
"sync"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/logger"
)
func truncateString(s string, maxLen int) string {

View File

@@ -13,9 +13,9 @@ import (
"github.com/gorilla/websocket"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
type WhatsAppChannel struct {

View File

@@ -67,6 +67,7 @@ type WhatsAppBridgeService struct {
status WhatsAppBridgeStatus
wsClientsMu sync.Mutex
markReadFn func(ctx context.Context, ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error
localOnly bool
}
type whatsappBridgeWSMessage struct {
@@ -100,6 +101,34 @@ func NewWhatsAppBridgeService(addr, stateDir string, printQR bool) *WhatsAppBrid
}
func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
if err := s.startRuntime(ctx); err != nil {
return err
}
mux := http.NewServeMux()
s.RegisterRoutes(mux, "")
s.httpServer = &http.Server{
Addr: s.addr,
Handler: mux,
}
ln, err := net.Listen("tcp", s.addr)
if err != nil {
return fmt.Errorf("listen whatsapp bridge: %w", err)
}
if err := s.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func (s *WhatsAppBridgeService) StartEmbedded(ctx context.Context) error {
s.localOnly = true
return s.startRuntime(ctx)
}
func (s *WhatsAppBridgeService) startRuntime(ctx context.Context) error {
if strings.TrimSpace(s.addr) == "" {
return fmt.Errorf("bridge address is required")
}
@@ -116,26 +145,13 @@ func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
runCtx, cancel := context.WithCancel(ctx)
s.cancel = cancel
mux := http.NewServeMux()
mux.HandleFunc("/", s.handleWS)
mux.HandleFunc("/ws", s.handleWS)
mux.HandleFunc("/status", s.handleStatus)
mux.HandleFunc("/logout", s.handleLogout)
s.httpServer = &http.Server{
Addr: s.addr,
Handler: mux,
}
ln, err := net.Listen("tcp", s.addr)
if err != nil {
return fmt.Errorf("listen whatsapp bridge: %w", err)
}
go func() {
<-runCtx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = s.httpServer.Shutdown(shutdownCtx)
if s.httpServer != nil {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = s.httpServer.Shutdown(shutdownCtx)
}
s.closeWSClients()
if s.client != nil {
s.client.Disconnect()
@@ -148,10 +164,6 @@ func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
go func() {
_ = s.connectClient(runCtx)
}()
if err := s.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
@@ -161,6 +173,17 @@ func (s *WhatsAppBridgeService) Stop() {
}
}
func (s *WhatsAppBridgeService) RegisterRoutes(mux *http.ServeMux, basePath string) {
if mux == nil {
return
}
basePath = normalizeBridgeBasePath(basePath)
mux.HandleFunc(basePath, s.ServeWS)
mux.HandleFunc(joinBridgeRoute(basePath, "ws"), s.ServeWS)
mux.HandleFunc(joinBridgeRoute(basePath, "status"), s.ServeStatus)
mux.HandleFunc(joinBridgeRoute(basePath, "logout"), s.ServeLogout)
}
func (s *WhatsAppBridgeService) StatusSnapshot() WhatsAppBridgeStatus {
s.statusMu.RLock()
defer s.statusMu.RUnlock()
@@ -421,11 +444,29 @@ func (s *WhatsAppBridgeService) handleWS(w http.ResponseWriter, r *http.Request)
}
}
func (s *WhatsAppBridgeService) ServeWS(w http.ResponseWriter, r *http.Request) {
s.wrapHandler(s.handleWS)(w, r)
}
func (s *WhatsAppBridgeService) wrapHandler(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if s.localOnly && !isLoopbackRequest(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next(w, r)
}
}
func (s *WhatsAppBridgeService) handleStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(s.StatusSnapshot())
}
func (s *WhatsAppBridgeService) ServeStatus(w http.ResponseWriter, r *http.Request) {
s.wrapHandler(s.handleStatus)(w, r)
}
func (s *WhatsAppBridgeService) handleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -795,36 +836,16 @@ func ParseWhatsAppBridgeListenAddr(raw string) (string, error) {
return raw, nil
}
func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
s.wrapHandler(s.handleLogout)(w, r)
}
func BridgeStatusURL(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = "/status"
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
return bridgeEndpointURL(raw, "status")
}
func BridgeLogoutURL(raw string) (string, error) {
statusURL, err := BridgeStatusURL(raw)
if err != nil {
return "", err
}
return strings.TrimSuffix(statusURL, "/status") + "/logout", nil
return bridgeEndpointURL(raw, "logout")
}
func normalizeWhatsAppRecipientJID(raw string) (types.JID, error) {
@@ -874,3 +895,73 @@ func extractWhatsAppMessageText(msg *waProto.Message) string {
return ""
}
}
func bridgeEndpointURL(raw, endpoint string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = bridgeSiblingPath(u.Path, endpoint)
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}
func bridgeSiblingPath(pathValue, endpoint string) string {
pathValue = strings.TrimSpace(pathValue)
if endpoint == "" {
endpoint = "status"
}
if pathValue == "" || pathValue == "/" {
return "/" + endpoint
}
trimmed := strings.TrimSuffix(pathValue, "/")
if strings.HasSuffix(trimmed, "/ws") {
return strings.TrimSuffix(trimmed, "/ws") + "/" + endpoint
}
return trimmed + "/" + endpoint
}
func normalizeBridgeBasePath(basePath string) string {
basePath = strings.TrimSpace(basePath)
if basePath == "" || basePath == "/" {
return "/"
}
if !strings.HasPrefix(basePath, "/") {
basePath = "/" + basePath
}
return strings.TrimSuffix(basePath, "/")
}
func joinBridgeRoute(basePath, endpoint string) string {
basePath = normalizeBridgeBasePath(basePath)
if basePath == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return basePath + "/" + strings.TrimPrefix(endpoint, "/")
}
func isLoopbackRequest(r *http.Request) bool {
if r == nil {
return false
}
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
if err != nil {
host = strings.TrimSpace(r.RemoteAddr)
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/bus"
waProto "go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto"
@@ -53,6 +53,16 @@ func TestBridgeStatusURL(t *testing.T) {
}
}
func TestBridgeStatusURLWithNestedPath(t *testing.T) {
got, err := BridgeStatusURL("ws://localhost:7788/whatsapp/ws")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "http://localhost:7788/whatsapp/status" {
t.Fatalf("got %q", got)
}
}
func TestNormalizeWhatsAppRecipientJID(t *testing.T) {
tests := []struct {
input string

View File

@@ -3,7 +3,7 @@ package channels
import (
"testing"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestWhatsAppShouldHandleIncomingMessage(t *testing.T) {

View File

@@ -9,7 +9,7 @@ import (
"strings"
"syscall"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
func LoadConfigAsMap(path string) (map[string]interface{}, error) {

View File

@@ -10,7 +10,7 @@ import (
"sync"
"time"
"clawgo/pkg/lifecycle"
"github.com/YspCoder/clawgo/pkg/lifecycle"
robcron "github.com/robfig/cron/v3"
)

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"clawgo/pkg/lifecycle"
"github.com/YspCoder/clawgo/pkg/lifecycle"
)
type HeartbeatService struct {

View File

@@ -3,11 +3,11 @@ package providers
import (
"bufio"
"bytes"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"context"
"encoding/json"
"fmt"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
"io"
"net/http"
"net/url"

View File

@@ -3,7 +3,7 @@ package runtimecfg
import (
"sync/atomic"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
var current atomic.Value // *config.Config

View File

@@ -8,10 +8,10 @@ import (
"sync"
"time"
"clawgo/pkg/channels"
"clawgo/pkg/config"
"clawgo/pkg/lifecycle"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/channels"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/lifecycle"
"github.com/YspCoder/clawgo/pkg/logger"
)
type AlertFunc func(msg string)

View File

@@ -6,8 +6,8 @@ import (
"net/http"
"time"
"clawgo/pkg/config"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/logger"
)
type Server struct {

View File

@@ -12,7 +12,7 @@ import (
"sync"
"time"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/providers"
)
type Session struct {

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"time"
"clawgo/pkg/browser"
"github.com/YspCoder/clawgo/pkg/browser"
)
type BrowserTool struct {

View File

@@ -46,13 +46,13 @@ func (t *CameraTool) Execute(ctx context.Context, args map[string]interface{}) (
} else {
filename = fmt.Sprintf("snap_%d.jpg", time.Now().Unix())
}
// Ensure filename is safe and within workspace
filename = filepath.Clean(filename)
if filepath.IsAbs(filename) {
return "", fmt.Errorf("filename must be relative to workspace")
}
outputPath := filepath.Join(t.workspace, filename)
// Check if video device exists

View File

@@ -6,7 +6,7 @@ import (
"fmt"
"strings"
"clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/cron"
)
type CronTool struct {

View File

@@ -21,7 +21,7 @@ import (
"sync/atomic"
"time"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
const mcpProtocolVersion = "2025-06-18"

View File

@@ -15,7 +15,7 @@ import (
"testing"
"time"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestMCPToolListServers(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"strings"
"sync"
"clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/bus"
)
type SendCallback func(channel, chatID, action, content, media, messageID, emoji string, buttons [][]bus.Button) error

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/nodes"
)
// NodesTool provides an OpenClaw-style control surface for paired nodes.

View File

@@ -15,7 +15,7 @@ import (
"sync/atomic"
"time"
"clawgo/pkg/events"
"github.com/YspCoder/clawgo/pkg/events"
)
type processSession struct {

View File

@@ -7,7 +7,7 @@ import (
"sync/atomic"
"time"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/logger"
)
type ToolRegistry struct {

View File

@@ -6,7 +6,7 @@ import (
"sync"
"time"
"clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/cron"
)
type RemindTool struct {

View File

@@ -5,7 +5,7 @@ import (
"path/filepath"
"testing"
"clawgo/pkg/cron"
"github.com/YspCoder/clawgo/pkg/cron"
)
func TestRemindTool_UsesToolContextForDeliveryTarget(t *testing.T) {
@@ -15,7 +15,7 @@ func TestRemindTool_UsesToolContextForDeliveryTarget(t *testing.T) {
tool.SetContext("telegram", "chat-123")
_, err := tool.Execute(context.Background(), map[string]interface{}{
"message": "喝水",
"message": "鍠濇按",
"time_expr": "10m",
})
if err != nil {

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/providers"
)
type SessionInfo struct {

View File

@@ -12,7 +12,7 @@ import (
"strings"
"time"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
type ExecTool struct {

View File

@@ -10,7 +10,7 @@ import (
"strings"
"time"
"clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/config"
)
type SkillExecTool struct {

View File

@@ -10,9 +10,9 @@ import (
"sync"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/ekg"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/ekg"
"github.com/YspCoder/clawgo/pkg/providers"
)
type SubagentTask struct {

View File

@@ -5,9 +5,9 @@ import (
"fmt"
"strings"
"clawgo/pkg/config"
"clawgo/pkg/configops"
"clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/configops"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
)
func DraftConfigSubagent(description, agentIDHint string) map[string]interface{} {
@@ -287,11 +287,11 @@ func normalizeKeywords(items []string) []string {
func inferDraftRole(lower string) string {
switch {
case containsDraftKeyword(lower, "test", "regression", "qa", "回归", "测试", "验证"):
case containsDraftKeyword(lower, "test", "regression", "qa", "鍥炲綊", "娴嬭瘯", "楠岃瘉"):
return "testing"
case containsDraftKeyword(lower, "doc", "docs", "readme", "文档", "说明"):
case containsDraftKeyword(lower, "doc", "docs", "readme", "鏂囨。", "璇存槑"):
return "docs"
case containsDraftKeyword(lower, "research", "investigate", "analyze", "调研", "分析", "研究"):
case containsDraftKeyword(lower, "research", "investigate", "analyze", "璋冪爺", "鍒嗘瀽", "鐮旂┒"):
return "research"
default:
return "coding"
@@ -301,7 +301,7 @@ func inferDraftRole(lower string) string {
func inferDraftAgentID(role, lower string) string {
switch role {
case "testing":
if containsDraftKeyword(lower, "review", "审查", "reviewer") {
if containsDraftKeyword(lower, "review", "瀹℃煡", "reviewer") {
return "reviewer"
}
return "tester"
@@ -310,10 +310,10 @@ func inferDraftAgentID(role, lower string) string {
case "research":
return "researcher"
default:
if containsDraftKeyword(lower, "frontend", "ui", "前端") {
if containsDraftKeyword(lower, "frontend", "ui", "鍓嶇") {
return "frontend-coder"
}
if containsDraftKeyword(lower, "backend", "api", "后端") {
if containsDraftKeyword(lower, "backend", "api", "鍚庣") {
return "backend-coder"
}
return "coder"
@@ -356,19 +356,19 @@ func inferDraftKeywords(role, lower string) []string {
seed := []string{}
switch role {
case "testing":
seed = []string{"test", "regression", "verify", "回归", "测试", "验证"}
seed = []string{"test", "regression", "verify", "鍥炲綊", "娴嬭瘯", "楠岃瘉"}
case "docs":
seed = []string{"docs", "readme", "document", "文档", "说明"}
seed = []string{"docs", "readme", "document", "鏂囨。", "璇存槑"}
case "research":
seed = []string{"research", "analyze", "investigate", "调研", "分析", "研究"}
seed = []string{"research", "analyze", "investigate", "璋冪爺", "鍒嗘瀽", "鐮旂┒"}
default:
seed = []string{"code", "implement", "fix", "refactor", "代码", "实现", "修复", "重构"}
seed = []string{"code", "implement", "fix", "refactor", "浠g爜", "瀹炵幇", "淇", "閲嶆瀯"}
}
if containsDraftKeyword(lower, "frontend", "前端", "ui") {
seed = append(seed, "frontend", "ui", "前端")
if containsDraftKeyword(lower, "frontend", "鍓嶇", "ui") {
seed = append(seed, "frontend", "ui", "鍓嶇")
}
if containsDraftKeyword(lower, "backend", "后端", "api") {
seed = append(seed, "backend", "api", "后端")
if containsDraftKeyword(lower, "backend", "鍚庣", "api") {
seed = append(seed, "backend", "api", "鍚庣")
}
return normalizeKeywords(seed)
}
@@ -376,13 +376,13 @@ func inferDraftKeywords(role, lower string) []string {
func inferDraftSystemPrompt(role, description string) string {
switch role {
case "testing":
return "你负责测试、验证、回归检查与风险反馈。任务描述:" + description
return "浣犺礋璐f祴璇曘€侀獙璇併€佸洖褰掓鏌ヤ笌椋庨櫓鍙嶉銆備换鍔℃弿杩帮細" + description
case "docs":
return "你负责文档编写、结构整理和说明补全。任务描述:" + description
return "浣犺礋璐f枃妗g紪鍐欍€佺粨鏋勬暣鐞嗗拰璇存槑琛ュ叏銆備换鍔℃弿杩帮細" + description
case "research":
return "你负责调研、分析、比较方案,并输出结论与依据。任务描述:" + description
return "浣犺礋璐h皟鐮斻€佸垎鏋愩€佹瘮杈冩柟妗堬紝骞惰緭鍑虹粨璁轰笌渚濇嵁銆備换鍔℃弿杩帮細" + description
default:
return "你负责代码实现与重构,输出具体修改建议和变更结果。任务描述:" + description
return "浣犺礋璐d唬鐮佸疄鐜颁笌閲嶆瀯锛岃緭鍑哄叿浣撲慨鏀瑰缓璁拰鍙樻洿缁撴灉銆備换鍔℃弿杩帮細" + description
}
}

View File

@@ -6,8 +6,8 @@ import (
"path/filepath"
"testing"
"clawgo/pkg/config"
"clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
)
func TestSubagentConfigToolUpsert(t *testing.T) {
@@ -34,7 +34,7 @@ func TestSubagentConfigToolUpsert(t *testing.T) {
"role": "testing",
"notify_main_policy": "internal_only",
"display_name": "Review Agent",
"description": "负责回归与评审",
"description": "handles review and regression checks",
"system_prompt_file": "agents/reviewer/AGENT.md",
"routing_keywords": []interface{}{"review", "regression"},
"tool_allowlist": []interface{}{"shell", "sessions"},

View File

@@ -11,9 +11,9 @@ import (
"sync"
"time"
"clawgo/pkg/config"
"clawgo/pkg/nodes"
"clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
)
type SubagentProfile struct {

View File

@@ -6,9 +6,9 @@ import (
"testing"
"time"
"clawgo/pkg/config"
"clawgo/pkg/nodes"
"clawgo/pkg/runtimecfg"
"github.com/YspCoder/clawgo/pkg/config"
"github.com/YspCoder/clawgo/pkg/nodes"
"github.com/YspCoder/clawgo/pkg/runtimecfg"
)
func TestSubagentProfileStoreNormalization(t *testing.T) {

View File

@@ -9,8 +9,8 @@ import (
"testing"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/providers"
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/providers"
)
func TestSubagentSpawnEnforcesTaskQuota(t *testing.T) {

View File

@@ -11,7 +11,7 @@ import (
"strings"
"time"
"clawgo/pkg/logger"
"github.com/YspCoder/clawgo/pkg/logger"
)
const (