diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index ead58f2..5026c0d 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -115,7 +115,9 @@ func main() { case "agent": agentCmd() case "gateway": - maybePromptAndEscalateRoot("gateway") + if shouldPromptGatewayRoot(os.Args) { + maybePromptAndEscalateRoot("gateway") + } gatewayCmd() case "status": statusCmd() @@ -426,11 +428,11 @@ Ultra-lightweight personal AI assistant written in Go, inspired by nanobot. MIT License - Free and open source ## Repository -https://github.com/sipeed/clawgo +https://github.com/YspCoder/clawgo ## Contact -Issues: https://github.com/sipeed/clawgo/issues -Discussions: https://github.com/sipeed/clawgo/discussions +Issues: https://github.com/YspCoder/clawgo/issues +Discussions: https://github.com/YspCoder/clawgo/discussions --- @@ -906,6 +908,11 @@ func maybePromptAndEscalateRoot(command string) { os.Exit(0) } +func shouldPromptGatewayRoot(args []string) bool { + // Only prompt on plain `clawgo gateway` registration flow. + return len(args) == 2 && args[1] == "gateway" +} + func isInteractiveStdin() bool { info, err := os.Stdin.Stat() if err != nil { @@ -1651,7 +1658,7 @@ func skillsHelp() { fmt.Println() fmt.Println("Examples:") fmt.Println(" clawgo skills list") - fmt.Println(" clawgo skills install sipeed/clawgo-skills/weather") + fmt.Println(" clawgo skills install YspCoder/clawgo-skills/weather") fmt.Println(" clawgo skills install-builtin") fmt.Println(" clawgo skills list-builtin") fmt.Println(" clawgo skills remove weather") @@ -1678,7 +1685,7 @@ func skillsListCmd(loader *skills.SkillsLoader) { func skillsInstallCmd(installer *skills.SkillInstaller) { if len(os.Args) < 4 { fmt.Println("Usage: clawgo skills install ") - fmt.Println("Example: clawgo skills install sipeed/clawgo-skills/weather") + fmt.Println("Example: clawgo skills install YspCoder/clawgo-skills/weather") return } diff --git a/config.example.json b/config.example.json index 72e86db..14f5279 100644 --- a/config.example.json +++ b/config.example.json @@ -50,7 +50,8 @@ "proxy": { "api_key": "YOUR_CLIPROXYAPI_KEY", "api_base": "http://localhost:8080/v1", - "auth": "bearer" + "auth": "bearer", + "timeout_sec": 90 } }, "tools": { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0ef5888..58f1363 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -31,7 +31,6 @@ import ( var errGatewayNotRunningSlash = errors.New("gateway not running") -const llmCallTimeout = 90 * time.Second const perSessionQueueSize = 64 type sessionWorker struct { @@ -53,6 +52,7 @@ type AgentLoop struct { orchestrator *tools.Orchestrator running atomic.Bool compactionCfg config.ContextCompactionConfig + llmCallTimeout time.Duration workersMu sync.Mutex workers map[string]*sessionWorker } @@ -137,6 +137,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers tools: toolsRegistry, orchestrator: orchestrator, compactionCfg: cfg.Agents.Defaults.ContextCompaction, + llmCallTimeout: time.Duration(cfg.Providers.Proxy.TimeoutSec) * time.Second, workers: make(map[string]*sessionWorker), } @@ -561,7 +562,7 @@ func (al *AgentLoop) runLLMToolLoop( }) llmStart := time.Now() - llmCtx, cancelLLM := context.WithTimeout(ctx, llmCallTimeout) + llmCtx, cancelLLM := context.WithTimeout(ctx, al.llmCallTimeout) response, err := al.callLLMWithModelFallback(llmCtx, messages, providerToolDefs, map[string]interface{}{ "max_tokens": 8192, "temperature": 0.7, @@ -680,7 +681,7 @@ func (al *AgentLoop) runLLMToolLoop( }) finalizeMessages = sanitizeMessagesForToolCalling(finalizeMessages) - llmCtx, cancelLLM := context.WithTimeout(ctx, llmCallTimeout) + llmCtx, cancelLLM := context.WithTimeout(ctx, al.llmCallTimeout) finalResp, err := al.callLLMWithModelFallback(llmCtx, finalizeMessages, nil, map[string]interface{}{ "max_tokens": 1024, "temperature": 0.3, diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 5560769..4844b50 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -63,6 +63,13 @@ func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*Telegr }, nil } +func withTelegramAPITimeout(ctx context.Context) (context.Context, context.CancelFunc) { + if ctx == nil { + ctx = context.Background() + } + return context.WithTimeout(ctx, telegramAPICallTimeout) +} + func (c *TelegramChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { c.transcriber = transcriber } @@ -84,7 +91,9 @@ func (c *TelegramChannel) Start(ctx context.Context) error { c.setRunning(true) - botInfo, err := c.bot.GetMe(context.Background()) + getMeCtx, cancelGetMe := withTelegramAPITimeout(ctx) + botInfo, err := c.bot.GetMe(getMeCtx) + cancelGetMe() if err != nil { return fmt.Errorf("failed to get bot info: %w", err) } @@ -205,13 +214,12 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err } chatID := telegoutil.ID(chatIDInt) - // Stop thinking animation - if stop, ok := c.stopThinking.Load(msg.ChatID); ok { + // Stop thinking animation first to avoid animation/update races. + if stop, ok := c.stopThinking.LoadAndDelete(msg.ChatID); ok { logger.DebugCF("telegram", "Telegram thinking stop signal", map[string]interface{}{ logger.FieldChatID: msg.ChatID, }) safeCloseSignal(stop) - c.stopThinking.Delete(msg.ChatID) } else { logger.DebugCF("telegram", "Telegram thinking stop skipped (not found)", map[string]interface{}{ logger.FieldChatID: msg.ChatID, @@ -222,18 +230,21 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // Try to edit placeholder if pID, ok := c.placeholders.Load(msg.ChatID); ok { - c.placeholders.Delete(msg.ChatID) + // Always reset placeholder state even when edit/send fails. + defer c.placeholders.Delete(msg.ChatID) logger.DebugCF("telegram", "Telegram editing thinking placeholder", map[string]interface{}{ logger.FieldChatID: msg.ChatID, "message_id": pID.(int), }) - _, err := c.bot.EditMessageText(ctx, &telego.EditMessageTextParams{ + editCtx, cancelEdit := withTelegramAPITimeout(ctx) + _, err := c.bot.EditMessageText(editCtx, &telego.EditMessageTextParams{ ChatID: chatID, MessageID: pID.(int), Text: htmlContent, ParseMode: telego.ModeHTML, }) + cancelEdit() if err == nil { logger.DebugCF("telegram", "Telegram placeholder updated", map[string]interface{}{ @@ -252,14 +263,18 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err }) } - _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, htmlContent).WithParseMode(telego.ModeHTML)) + sendCtx, cancelSend := withTelegramAPITimeout(ctx) + _, err = c.bot.SendMessage(sendCtx, telegoutil.Message(chatID, htmlContent).WithParseMode(telego.ModeHTML)) + cancelSend() if err != nil { logger.WarnCF("telegram", "HTML parse failed, fallback to plain text", map[string]interface{}{ logger.FieldError: err.Error(), }) plain := plainTextFromTelegramHTML(htmlContent) - _, err = c.bot.SendMessage(ctx, telegoutil.Message(chatID, plain)) + sendPlainCtx, cancelSendPlain := withTelegramAPITimeout(ctx) + _, err = c.bot.SendMessage(sendPlainCtx, telegoutil.Message(chatID, plain)) + cancelSendPlain() if err != nil { logger.ErrorCF("telegram", "Telegram plain-text fallback send failed", map[string]interface{}{ logger.FieldChatID: msg.ChatID, diff --git a/pkg/config/config.go b/pkg/config/config.go index eb7f299..07eed03 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -107,9 +107,10 @@ type ProvidersConfig struct { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"` - APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"` - Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"` + APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"` + APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"` + Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"` + TimeoutSec int `json:"timeout_sec" env:"CLAWGO_PROVIDERS_PROXY_TIMEOUT_SEC"` } type GatewayConfig struct { @@ -276,7 +277,8 @@ func DefaultConfig() *Config { }, Providers: ProvidersConfig{ Proxy: ProviderConfig{ - APIBase: "http://localhost:8080/v1", + APIBase: "http://localhost:8080/v1", + TimeoutSec: 90, }, }, Gateway: GatewayConfig{ diff --git a/pkg/config/validate.go b/pkg/config/validate.go index ee7acb6..bef09d1 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -39,6 +39,9 @@ func Validate(cfg *Config) []error { if cfg.Providers.Proxy.APIBase == "" { errs = append(errs, fmt.Errorf("providers.proxy.api_base is required")) } + if cfg.Providers.Proxy.TimeoutSec <= 0 { + errs = append(errs, fmt.Errorf("providers.proxy.timeout_sec must be > 0")) + } if cfg.Gateway.Port <= 0 || cfg.Gateway.Port > 65535 { errs = append(errs, fmt.Errorf("gateway.port must be in 1..65535")) diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index a09d4c1..45b9308 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -24,18 +24,18 @@ type HTTPProvider struct { apiKey string apiBase string authMode string + timeout time.Duration httpClient *http.Client } -const defaultChatTimeout = 90 * time.Second - -func NewHTTPProvider(apiKey, apiBase, authMode string) *HTTPProvider { +func NewHTTPProvider(apiKey, apiBase, authMode string, timeout time.Duration) *HTTPProvider { return &HTTPProvider{ apiKey: apiKey, apiBase: apiBase, authMode: authMode, + timeout: timeout, httpClient: &http.Client{ - Timeout: defaultChatTimeout, + Timeout: timeout, }, } } @@ -50,7 +50,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too "model": model, "messages_count": len(messages), "tools_count": len(tools), - "timeout": defaultChatTimeout.String(), + "timeout": p.timeout.String(), }) requestBody := map[string]interface{}{ @@ -208,6 +208,9 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) { if apiBase == "" { return nil, fmt.Errorf("no API base (CLIProxyAPI) configured") } + if cfg.Providers.Proxy.TimeoutSec <= 0 { + return nil, fmt.Errorf("invalid providers.proxy.timeout_sec: %d", cfg.Providers.Proxy.TimeoutSec) + } - return NewHTTPProvider(apiKey, apiBase, authMode), nil + return NewHTTPProvider(apiKey, apiBase, authMode, time.Duration(cfg.Providers.Proxy.TimeoutSec)*time.Second), nil } diff --git a/pkg/skills/installer.go b/pkg/skills/installer.go index f616cd6..f644c54 100644 --- a/pkg/skills/installer.go +++ b/pkg/skills/installer.go @@ -93,7 +93,7 @@ func (si *SkillInstaller) Uninstall(skillName string) error { } func (si *SkillInstaller) ListAvailableSkills(ctx context.Context) ([]AvailableSkill, error) { - url := "https://raw.githubusercontent.com/sipeed/clawgo-skills/main/skills.json" + url := "https://raw.githubusercontent.com/YspCoder/clawgo-skills/main/skills.json" client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", url, nil) diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index c14632c..4867d0f 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -39,6 +39,11 @@ func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool { allowPatterns = append(allowPatterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(p)+`\b`)) } + allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds)) + for _, p := range cfg.AllowedCmds { + allowPatterns = append(allowPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`)) + } + return &ExecTool{ workingDir: workspace, timeout: cfg.Timeout, diff --git a/skills/context7/SKILL.md b/skills/context7/SKILL.md index e9a283e..8ce322e 100644 --- a/skills/context7/SKILL.md +++ b/skills/context7/SKILL.md @@ -15,5 +15,5 @@ npx tsx /root/.clawgo/skills/context7/query.ts context Example: ```bash -npx tsx /root/.clawgo/skills/context7/query.ts context sipeed/clawgo "How does the skill system work?" +npx tsx /root/.clawgo/skills/context7/query.ts context YspCoder/clawgo "How does the skill system work?" ```