From 0675ce7c38ba4139bfce94c47e19275dbaed4017 Mon Sep 17 00:00:00 2001 From: Artem Yadelskyi Date: Fri, 20 Feb 2026 20:03:11 +0200 Subject: [PATCH] feat(fmt): Fix formatting --- cmd/picoclaw/cmd_agent.go | 10 +- cmd/picoclaw/cmd_gateway.go | 26 +++- cmd/picoclaw/cmd_onboard.go | 6 +- cmd/picoclaw/cmd_skills.go | 4 +- pkg/agent/context.go | 43 ++++-- pkg/agent/loop.go | 127 +++++++++++------ pkg/auth/oauth.go | 13 +- pkg/channels/dingtalk.go | 13 +- pkg/channels/feishu_64.go | 6 +- pkg/channels/line.go | 36 ++--- pkg/channels/manager.go | 44 +++--- pkg/channels/wecom.go | 25 ++-- pkg/channels/wecom_app.go | 38 ++--- pkg/channels/wecom_app_test.go | 42 +++++- pkg/channels/wecom_test.go | 48 +++++-- pkg/config/config.go | 168 +++++++++++----------- pkg/config/migration_test.go | 16 ++- pkg/migrate/migrate_test.go | 172 +++++++++++------------ pkg/providers/anthropic/provider_test.go | 44 +++--- pkg/providers/antigravity_provider.go | 118 ++++++++++------ pkg/providers/claude_provider_test.go | 11 +- pkg/providers/http_provider.go | 8 +- pkg/providers/openai_compat/provider.go | 39 +++-- pkg/providers/protocoltypes/types.go | 20 +-- pkg/providers/toolcall_utils.go | 4 +- pkg/providers/types.go | 28 ++-- pkg/skills/clawhub_registry.go | 5 +- pkg/skills/clawhub_registry_test.go | 7 +- pkg/skills/registry_test.go | 3 +- pkg/tools/shell_timeout_unix_test.go | 2 +- pkg/tools/skills_install.go | 30 ++-- pkg/tools/skills_install_test.go | 19 +-- pkg/tools/skills_search.go | 12 +- pkg/tools/skills_search_test.go | 20 ++- pkg/utils/download.go | 6 +- pkg/utils/zip.go | 13 +- 36 files changed, 731 insertions(+), 495 deletions(-) diff --git a/cmd/picoclaw/cmd_agent.go b/cmd/picoclaw/cmd_agent.go index cee9f68ec..6d6ff935f 100644 --- a/cmd/picoclaw/cmd_agent.go +++ b/cmd/picoclaw/cmd_agent.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/chzyer/readline" + "github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/logger" @@ -74,10 +75,10 @@ func agentCmd() { // Print agent startup info (only for interactive mode) startupInfo := agentLoop.GetStartupInfo() logger.InfoCF("agent", "Agent initialized", - map[string]interface{}{ - "tools_count": startupInfo["tools"].(map[string]interface{})["count"], - "skills_total": startupInfo["skills"].(map[string]interface{})["total"], - "skills_available": startupInfo["skills"].(map[string]interface{})["available"], + map[string]any{ + "tools_count": startupInfo["tools"].(map[string]any)["count"], + "skills_total": startupInfo["skills"].(map[string]any)["total"], + "skills_available": startupInfo["skills"].(map[string]any)["available"], }) if message != "" { @@ -104,7 +105,6 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { InterruptPrompt: "^C", EOFPrompt: "exit", }) - if err != nil { fmt.Printf("Error initializing readline: %v\n", err) fmt.Println("Falling back to simple input mode...") diff --git a/cmd/picoclaw/cmd_gateway.go b/cmd/picoclaw/cmd_gateway.go index 1f1bf5491..00ec0f96d 100644 --- a/cmd/picoclaw/cmd_gateway.go +++ b/cmd/picoclaw/cmd_gateway.go @@ -60,8 +60,8 @@ func gatewayCmd() { // Print agent startup info fmt.Println("\n📦 Agent Status:") startupInfo := agentLoop.GetStartupInfo() - toolsInfo := startupInfo["tools"].(map[string]interface{}) - skillsInfo := startupInfo["skills"].(map[string]interface{}) + toolsInfo := startupInfo["tools"].(map[string]any) + skillsInfo := startupInfo["skills"].(map[string]any) fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"]) fmt.Printf(" • Skills: %d/%d available\n", skillsInfo["available"], @@ -69,7 +69,7 @@ func gatewayCmd() { // Log to file as well logger.InfoCF("agent", "Agent initialized", - map[string]interface{}{ + map[string]any{ "tools_count": toolsInfo["count"], "skills_total": skillsInfo["total"], "skills_available": skillsInfo["available"], @@ -77,7 +77,14 @@ func gatewayCmd() { // Setup cron tool and service execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute - cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg) + cronService := setupCronTool( + agentLoop, + msgBus, + cfg.WorkspacePath(), + cfg.Agents.Defaults.RestrictToWorkspace, + execTimeout, + cfg, + ) heartbeatService := heartbeat.NewHeartbeatService( cfg.WorkspacePath(), @@ -181,7 +188,7 @@ func gatewayCmd() { healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port) go func() { if err := healthServer.Start(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF("health", "Health server error", map[string]any{"error": err.Error()}) } }() fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port) @@ -203,7 +210,14 @@ func gatewayCmd() { fmt.Println("✓ Gateway stopped") } -func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cfg *config.Config) *cron.CronService { +func setupCronTool( + agentLoop *agent.AgentLoop, + msgBus *bus.MessageBus, + workspace string, + restrict bool, + execTimeout time.Duration, + cfg *config.Config, +) *cron.CronService { cronStorePath := filepath.Join(workspace, "cron", "jobs.json") // Create cron service diff --git a/cmd/picoclaw/cmd_onboard.go b/cmd/picoclaw/cmd_onboard.go index 6e61e3267..1a9ebad61 100644 --- a/cmd/picoclaw/cmd_onboard.go +++ b/cmd/picoclaw/cmd_onboard.go @@ -55,7 +55,7 @@ func onboard() { func copyEmbeddedToTarget(targetDir string) error { // Ensure target directory exists - if err := os.MkdirAll(targetDir, 0755); err != nil { + if err := os.MkdirAll(targetDir, 0o755); err != nil { return fmt.Errorf("Failed to create target directory: %w", err) } @@ -85,12 +85,12 @@ func copyEmbeddedToTarget(targetDir string) error { targetPath := filepath.Join(targetDir, new_path) // Ensure target file's directory exists - if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err) } // Write file - if err := os.WriteFile(targetPath, data, 0644); err != nil { + if err := os.WriteFile(targetPath, data, 0o644); err != nil { return fmt.Errorf("Failed to write file %s: %w", targetPath, err) } diff --git a/cmd/picoclaw/cmd_skills.go b/cmd/picoclaw/cmd_skills.go index 32b7c62b8..2dd46756a 100644 --- a/cmd/picoclaw/cmd_skills.go +++ b/cmd/picoclaw/cmd_skills.go @@ -126,7 +126,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - if err := os.MkdirAll(filepath.Join(workspace, "skills"), 0755); err != nil { + if err := os.MkdirAll(filepath.Join(workspace, "skills"), 0o755); err != nil { fmt.Printf("\u2717 Failed to create skills directory: %v\n", err) os.Exit(1) } @@ -193,7 +193,7 @@ func skillsInstallBuiltinCmd(workspace string) { continue } - if err := os.MkdirAll(workspacePath, 0755); err != nil { + if err := os.MkdirAll(workspacePath, 0o755); err != nil { fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err) continue } diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 78f5f1ffa..e989ffaaf 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -96,7 +96,9 @@ func (cb *ContextBuilder) buildToolsSection() string { var sb strings.Builder sb.WriteString("## Available Tools\n\n") - sb.WriteString("**CRITICAL**: You MUST use tools to perform actions. Do NOT pretend to execute commands or schedule tasks.\n\n") + sb.WriteString( + "**CRITICAL**: You MUST use tools to perform actions. Do NOT pretend to execute commands or schedule tasks.\n\n", + ) sb.WriteString("You have access to the following tools:\n\n") for _, s := range summaries { sb.WriteString(s) @@ -157,7 +159,13 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string { return sb.String() } -func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary string, currentMessage string, media []string, channel, chatID string) []providers.Message { +func (cb *ContextBuilder) BuildMessages( + history []providers.Message, + summary string, + currentMessage string, + media []string, + channel, chatID string, +) []providers.Message { messages := []providers.Message{} systemPrompt := cb.BuildSystemPrompt() @@ -169,7 +177,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str // Log system prompt summary for debugging (debug mode only) logger.DebugCF("agent", "System prompt built", - map[string]interface{}{ + map[string]any{ "total_chars": len(systemPrompt), "total_lines": strings.Count(systemPrompt, "\n") + 1, "section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1, @@ -181,7 +189,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str preview = preview[:500] + "... (truncated)" } logger.DebugCF("agent", "System prompt preview", - map[string]interface{}{ + map[string]any{ "preview": preview, }) @@ -218,12 +226,12 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message switch msg.Role { case "tool": if len(sanitized) == 0 { - logger.DebugCF("agent", "Dropping orphaned leading tool message", map[string]interface{}{}) + logger.DebugCF("agent", "Dropping orphaned leading tool message", map[string]any{}) continue } last := sanitized[len(sanitized)-1] if last.Role != "assistant" || len(last.ToolCalls) == 0 { - logger.DebugCF("agent", "Dropping orphaned tool message", map[string]interface{}{}) + logger.DebugCF("agent", "Dropping orphaned tool message", map[string]any{}) continue } sanitized = append(sanitized, msg) @@ -231,12 +239,16 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message case "assistant": if len(msg.ToolCalls) > 0 { if len(sanitized) == 0 { - logger.DebugCF("agent", "Dropping assistant tool-call turn at history start", map[string]interface{}{}) + logger.DebugCF("agent", "Dropping assistant tool-call turn at history start", map[string]any{}) continue } prev := sanitized[len(sanitized)-1] if prev.Role != "user" && prev.Role != "tool" { - logger.DebugCF("agent", "Dropping assistant tool-call turn with invalid predecessor", map[string]interface{}{"prev_role": prev.Role}) + logger.DebugCF( + "agent", + "Dropping assistant tool-call turn with invalid predecessor", + map[string]any{"prev_role": prev.Role}, + ) continue } } @@ -250,7 +262,10 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message return sanitized } -func (cb *ContextBuilder) AddToolResult(messages []providers.Message, toolCallID, toolName, result string) []providers.Message { +func (cb *ContextBuilder) AddToolResult( + messages []providers.Message, + toolCallID, toolName, result string, +) []providers.Message { messages = append(messages, providers.Message{ Role: "tool", Content: result, @@ -259,7 +274,11 @@ func (cb *ContextBuilder) AddToolResult(messages []providers.Message, toolCallID return messages } -func (cb *ContextBuilder) AddAssistantMessage(messages []providers.Message, content string, toolCalls []map[string]interface{}) []providers.Message { +func (cb *ContextBuilder) AddAssistantMessage( + messages []providers.Message, + content string, + toolCalls []map[string]any, +) []providers.Message { msg := providers.Message{ Role: "assistant", Content: content, @@ -289,13 +308,13 @@ func (cb *ContextBuilder) loadSkills() string { } // GetSkillsInfo returns information about loaded skills. -func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} { +func (cb *ContextBuilder) GetSkillsInfo() map[string]any { allSkills := cb.skillsLoader.ListSkills() skillNames := make([]string, 0, len(allSkills)) for _, s := range allSkills { skillNames = append(skillNames, s.Name) } - return map[string]interface{}{ + return map[string]any{ "total": len(allSkills), "available": len(allSkills), "names": skillNames, diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index add183aaf..b36f4a0c4 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -80,7 +80,12 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers } // registerSharedTools registers tools that are shared across all agents (web, message, spawn). -func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *AgentRegistry, provider providers.LLMProvider) { +func registerSharedTools( + cfg *config.Config, + msgBus *bus.MessageBus, + registry *AgentRegistry, + provider providers.LLMProvider, +) { for _, agentID := range registry.ListAgentIDs() { agent, ok := registry.GetAgent(agentID) if !ok { @@ -123,7 +128,10 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches, ClawHub: skills.ClawHubConfig(cfg.Tools.Skills.Registries.ClawHub), }) - searchCache := skills.NewSearchCache(cfg.Tools.Skills.SearchCache.MaxSize, time.Duration(cfg.Tools.Skills.SearchCache.TTLSeconds)*time.Second) + searchCache := skills.NewSearchCache( + cfg.Tools.Skills.SearchCache.MaxSize, + time.Duration(cfg.Tools.Skills.SearchCache.TTLSeconds)*time.Second, + ) agent.Tools.Register(tools.NewFindSkillsTool(registryMgr, searchCache)) agent.Tools.Register(tools.NewInstallSkillTool(registryMgr, agent.Workspace)) @@ -226,7 +234,10 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri return al.ProcessDirectWithChannel(ctx, content, sessionKey, "cli", "direct") } -func (al *AgentLoop) ProcessDirectWithChannel(ctx context.Context, content, sessionKey, channel, chatID string) (string, error) { +func (al *AgentLoop) ProcessDirectWithChannel( + ctx context.Context, + content, sessionKey, channel, chatID string, +) (string, error) { msg := bus.InboundMessage{ Channel: channel, SenderID: "cron", @@ -263,7 +274,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) logContent = utils.Truncate(msg.Content, 80) } logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, logContent), - map[string]interface{}{ + map[string]any{ "channel": msg.Channel, "chat_id": msg.ChatID, "sender_id": msg.SenderID, @@ -302,7 +313,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } logger.InfoCF("agent", "Routed message", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "session_key": sessionKey, "matched_by": route.MatchedBy, @@ -325,7 +336,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe } logger.InfoCF("agent", "Processing system message", - map[string]interface{}{ + map[string]any{ "sender_id": msg.SenderID, "chat_id": msg.ChatID, }) @@ -350,7 +361,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe // Skip internal channels - only log, don't send to user if constants.IsInternalChannel(originChannel) { logger.InfoCF("agent", "Subagent completed (internal channel)", - map[string]interface{}{ + map[string]any{ "sender_id": msg.SenderID, "content_len": len(content), "channel": originChannel, @@ -383,7 +394,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt if !constants.IsInternalChannel(opts.Channel) { channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID) if err := al.RecordLastChannel(channelKey); err != nil { - logger.WarnCF("agent", "Failed to record last channel", map[string]interface{}{"error": err.Error()}) + logger.WarnCF("agent", "Failed to record last channel", map[string]any{"error": err.Error()}) } } } @@ -445,7 +456,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 9. Log response responsePreview := utils.Truncate(finalContent, 120) logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview), - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "session_key": opts.SessionKey, "iterations": iteration, @@ -456,7 +467,12 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt } // runLLMIteration executes the LLM call loop with tool handling. -func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) { +func (al *AgentLoop) runLLMIteration( + ctx context.Context, + agent *AgentInstance, + messages []providers.Message, + opts processOptions, +) (string, int, error) { iteration := 0 var finalContent string @@ -464,7 +480,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, iteration++ logger.DebugCF("agent", "LLM iteration", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "max": agent.MaxIterations, @@ -475,7 +491,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, // Log LLM request details logger.DebugCF("agent", "LLM request", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "model": agent.Model, @@ -488,7 +504,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, // Log full messages (detailed) logger.DebugCF("agent", "Full LLM request", - map[string]interface{}{ + map[string]any{ "iteration": iteration, "messages_json": formatMessagesForLog(messages), "tools_json": formatToolsForLog(providerToolDefs), @@ -502,7 +518,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, if len(agent.Candidates) > 1 && al.fallback != nil { fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates, func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) { - return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{ + return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{ "max_tokens": agent.MaxTokens, "temperature": agent.Temperature, }) @@ -514,11 +530,11 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, if fbResult.Provider != "" && len(fbResult.Attempts) > 0 { logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts", fbResult.Provider, fbResult.Model, len(fbResult.Attempts)+1), - map[string]interface{}{"agent_id": agent.ID, "iteration": iteration}) + map[string]any{"agent_id": agent.ID, "iteration": iteration}) } return fbResult.Response, nil } - return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{ + return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]any{ "max_tokens": agent.MaxTokens, "temperature": agent.Temperature, }) @@ -539,7 +555,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, strings.Contains(errMsg, "length") if isContextError && retry < maxRetries { - logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{ + logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]any{ "error": err.Error(), "retry": retry, }) @@ -566,7 +582,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, if err != nil { logger.ErrorCF("agent", "LLM call failed", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "error": err.Error(), @@ -578,7 +594,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, if len(response.ToolCalls) == 0 { finalContent = response.Content logger.InfoCF("agent", "LLM response without tool calls (direct answer)", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "content_chars": len(finalContent), @@ -597,7 +613,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, toolNames = append(toolNames, tc.Name) } logger.InfoCF("agent", "LLM requested tool calls", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "tools": toolNames, "count": len(normalizedToolCalls), @@ -641,7 +657,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, argsJSON, _ := json.Marshal(tc.Arguments) argsPreview := utils.Truncate(string(argsJSON), 200) logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview), - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "tool": tc.Name, "iteration": iteration, @@ -656,14 +672,21 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, // The agent will handle user notification via processSystemMessage if !result.Silent && result.ForUser != "" { logger.InfoCF("agent", "Async tool completed, agent will handle notification", - map[string]interface{}{ + map[string]any{ "tool": tc.Name, "content_len": len(result.ForUser), }) } } - toolResult := agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback) + toolResult := agent.Tools.ExecuteWithContext( + ctx, + tc.Name, + tc.Arguments, + opts.Channel, + opts.ChatID, + asyncCallback, + ) // Send ForUser content to user immediately if not Silent if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse { @@ -673,7 +696,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, Content: toolResult.ForUser, }) logger.DebugCF("agent", "Sent tool result to user", - map[string]interface{}{ + map[string]any{ "tool": tc.Name, "content_len": len(toolResult.ForUser), }) @@ -775,7 +798,10 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { // Append compression note to the original system prompt instead of adding a new system message // This avoids having two consecutive system messages which some APIs (like Zhipu) reject - compressionNote := fmt.Sprintf("\n\n[System Note: Emergency compression dropped %d oldest messages due to context limit]", droppedCount) + compressionNote := fmt.Sprintf( + "\n\n[System Note: Emergency compression dropped %d oldest messages due to context limit]", + droppedCount, + ) enhancedSystemPrompt := history[0] enhancedSystemPrompt.Content = enhancedSystemPrompt.Content + compressionNote newHistory = append(newHistory, enhancedSystemPrompt) @@ -787,7 +813,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { agent.Sessions.SetHistory(sessionKey, newHistory) agent.Sessions.Save(sessionKey) - logger.WarnCF("agent", "Forced compression executed", map[string]interface{}{ + logger.WarnCF("agent", "Forced compression executed", map[string]any{ "session_key": sessionKey, "dropped_msgs": droppedCount, "new_count": len(newHistory), @@ -795,8 +821,8 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { } // GetStartupInfo returns information about loaded tools and skills for logging. -func (al *AgentLoop) GetStartupInfo() map[string]interface{} { - info := make(map[string]interface{}) +func (al *AgentLoop) GetStartupInfo() map[string]any { + info := make(map[string]any) agent := al.registry.GetDefaultAgent() if agent == nil { @@ -805,7 +831,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} { // Tools info toolsList := agent.Tools.List() - info["tools"] = map[string]interface{}{ + info["tools"] = map[string]any{ "count": len(toolsList), "names": toolsList, } @@ -814,7 +840,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} { info["skills"] = agent.ContextBuilder.GetSkillsInfo() // Agents info - info["agents"] = map[string]interface{}{ + info["agents"] = map[string]any{ "count": len(al.registry.ListAgentIDs()), "ids": al.registry.ListAgentIDs(), } @@ -919,11 +945,21 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) { s1, _ := al.summarizeBatch(ctx, agent, part1, "") s2, _ := al.summarizeBatch(ctx, agent, part2, "") - mergePrompt := fmt.Sprintf("Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s", s1, s2) - resp, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: mergePrompt}}, nil, agent.Model, map[string]interface{}{ - "max_tokens": 1024, - "temperature": 0.3, - }) + mergePrompt := fmt.Sprintf( + "Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s", + s1, + s2, + ) + resp, err := agent.Provider.Chat( + ctx, + []providers.Message{{Role: "user", Content: mergePrompt}}, + nil, + agent.Model, + map[string]any{ + "max_tokens": 1024, + "temperature": 0.3, + }, + ) if err == nil { finalSummary = resp.Content } else { @@ -945,7 +981,12 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) { } // summarizeBatch summarizes a batch of messages. -func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string) (string, error) { +func (al *AgentLoop) summarizeBatch( + ctx context.Context, + agent *AgentInstance, + batch []providers.Message, + existingSummary string, +) (string, error) { var sb strings.Builder sb.WriteString("Provide a concise summary of this conversation segment, preserving core context and key points.\n") if existingSummary != "" { @@ -959,10 +1000,16 @@ func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, b } prompt := sb.String() - response, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, agent.Model, map[string]interface{}{ - "max_tokens": 1024, - "temperature": 0.3, - }) + response, err := agent.Provider.Chat( + ctx, + []providers.Message{{Role: "user", Content: prompt}}, + nil, + agent.Model, + map[string]any{ + "max_tokens": 1024, + "temperature": 0.3, + }, + ) if err != nil { return "", err } diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index 496a4674c..cf8c1c9c4 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -44,7 +44,9 @@ func OpenAIOAuthConfig() OAuthProviderConfig { // Client credentials are the same ones used by OpenCode/pi-ai for Cloud Code Assist access. func GoogleAntigravityOAuthConfig() OAuthProviderConfig { // These are the same client credentials used by the OpenCode antigravity plugin. - clientID := decodeBase64("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==") + clientID := decodeBase64( + "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==", + ) clientSecret := decodeBase64("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=") return OAuthProviderConfig{ Issuer: "https://accounts.google.com/o/oauth2/v2", @@ -129,8 +131,13 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) { fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL) } - fmt.Printf("Wait! If you are in a headless environment (like Coolify/VPS) and cannot reach localhost:%d,\n", cfg.Port) - fmt.Println("please complete the login in your local browser and then PASTE the final redirect URL (or just the code) here.") + fmt.Printf( + "Wait! If you are in a headless environment (like Coolify/VPS) and cannot reach localhost:%d,\n", + cfg.Port, + ) + fmt.Println( + "please complete the login in your local browser and then PASTE the final redirect URL (or just the code) here.", + ) fmt.Println("Waiting for authentication (browser or manual paste)...") // Start manual input in a goroutine diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go index 79cc85219..662fba3b7 100644 --- a/pkg/channels/dingtalk.go +++ b/pkg/channels/dingtalk.go @@ -10,6 +10,7 @@ import ( "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" + "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" @@ -108,7 +109,7 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID) } - logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{ + logger.DebugCF("dingtalk", "Sending message", map[string]any{ "chat_id": msg.ChatID, "preview": utils.Truncate(msg.Content, 100), }) @@ -120,12 +121,15 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // onChatBotMessageReceived implements the IChatBotMessageHandler function signature // This is called by the Stream SDK when a new message arrives // IChatBotMessageHandler is: func(c context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) -func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) { +func (c *DingTalkChannel) onChatBotMessageReceived( + ctx context.Context, + data *chatbot.BotCallbackDataModel, +) ([]byte, error) { // Extract message content from Text field content := data.Text.Content if content == "" { // Try to extract from Content interface{} if Text is empty - if contentMap, ok := data.Content.(map[string]interface{}); ok { + if contentMap, ok := data.Content.(map[string]any); ok { if textContent, ok := contentMap["content"].(string); ok { content = textContent } @@ -163,7 +167,7 @@ func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *ch metadata["peer_id"] = data.ConversationId } - logger.DebugCF("dingtalk", "Received message", map[string]interface{}{ + logger.DebugCF("dingtalk", "Received message", map[string]any{ "sender_nick": senderNick, "sender_id": senderID, "preview": utils.Truncate(content, 50), @@ -192,7 +196,6 @@ func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, c titleBytes, contentBytes, ) - if err != nil { return fmt.Errorf("failed to send reply: %w", err) } diff --git a/pkg/channels/feishu_64.go b/pkg/channels/feishu_64.go index 9e15fa3a7..42e74980f 100644 --- a/pkg/channels/feishu_64.go +++ b/pkg/channels/feishu_64.go @@ -65,7 +65,7 @@ func (c *FeishuChannel) Start(ctx context.Context) error { go func() { if err := wsClient.Start(runCtx); err != nil { - logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]interface{}{ + logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]any{ "error": err.Error(), }) } @@ -121,7 +121,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error return fmt.Errorf("feishu api error: code=%d msg=%s", resp.Code, resp.Msg) } - logger.DebugCF("feishu", "Feishu message sent", map[string]interface{}{ + logger.DebugCF("feishu", "Feishu message sent", map[string]any{ "chat_id": msg.ChatID, }) @@ -174,7 +174,7 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2 metadata["peer_id"] = chatID } - logger.InfoCF("feishu", "Feishu message received", map[string]interface{}{ + logger.InfoCF("feishu", "Feishu message received", map[string]any{ "sender_id": senderID, "chat_id": chatID, "preview": utils.Truncate(content, 80), diff --git a/pkg/channels/line.go b/pkg/channels/line.go index 9f7d2bde0..44134996f 100644 --- a/pkg/channels/line.go +++ b/pkg/channels/line.go @@ -75,11 +75,11 @@ func (c *LINEChannel) Start(ctx context.Context) error { // Fetch bot profile to get bot's userId for mention detection if err := c.fetchBotInfo(); err != nil { - logger.WarnCF("line", "Failed to fetch bot info (mention detection disabled)", map[string]interface{}{ + logger.WarnCF("line", "Failed to fetch bot info (mention detection disabled)", map[string]any{ "error": err.Error(), }) } else { - logger.InfoCF("line", "Bot info fetched", map[string]interface{}{ + logger.InfoCF("line", "Bot info fetched", map[string]any{ "bot_user_id": c.botUserID, "basic_id": c.botBasicID, "display_name": c.botDisplayName, @@ -100,12 +100,12 @@ func (c *LINEChannel) Start(ctx context.Context) error { } go func() { - logger.InfoCF("line", "LINE webhook server listening", map[string]interface{}{ + logger.InfoCF("line", "LINE webhook server listening", map[string]any{ "addr": addr, "path": path, }) if err := c.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("line", "Webhook server error", map[string]interface{}{ + logger.ErrorCF("line", "Webhook server error", map[string]any{ "error": err.Error(), }) } @@ -162,7 +162,7 @@ func (c *LINEChannel) Stop(ctx context.Context) error { shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := c.httpServer.Shutdown(shutdownCtx); err != nil { - logger.ErrorCF("line", "Webhook server shutdown error", map[string]interface{}{ + logger.ErrorCF("line", "Webhook server shutdown error", map[string]any{ "error": err.Error(), }) } @@ -182,7 +182,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - logger.ErrorCF("line", "Failed to read request body", map[string]interface{}{ + logger.ErrorCF("line", "Failed to read request body", map[string]any{ "error": err.Error(), }) http.Error(w, "Bad request", http.StatusBadRequest) @@ -200,7 +200,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) { Events []lineEvent `json:"events"` } if err := json.Unmarshal(body, &payload); err != nil { - logger.ErrorCF("line", "Failed to parse webhook payload", map[string]interface{}{ + logger.ErrorCF("line", "Failed to parse webhook payload", map[string]any{ "error": err.Error(), }) http.Error(w, "Bad request", http.StatusBadRequest) @@ -266,7 +266,7 @@ type lineMentionee struct { func (c *LINEChannel) processEvent(event lineEvent) { if event.Type != "message" { - logger.DebugCF("line", "Ignoring non-message event", map[string]interface{}{ + logger.DebugCF("line", "Ignoring non-message event", map[string]any{ "type": event.Type, }) return @@ -278,7 +278,7 @@ func (c *LINEChannel) processEvent(event lineEvent) { var msg lineMessage if err := json.Unmarshal(event.Message, &msg); err != nil { - logger.ErrorCF("line", "Failed to parse message", map[string]interface{}{ + logger.ErrorCF("line", "Failed to parse message", map[string]any{ "error": err.Error(), }) return @@ -286,7 +286,7 @@ func (c *LINEChannel) processEvent(event lineEvent) { // In group chats, only respond when the bot is mentioned if isGroup && !c.isBotMentioned(msg) { - logger.DebugCF("line", "Ignoring group message without mention", map[string]interface{}{ + logger.DebugCF("line", "Ignoring group message without mention", map[string]any{ "chat_id": chatID, }) return @@ -312,7 +312,7 @@ func (c *LINEChannel) processEvent(event lineEvent) { defer func() { for _, file := range localFiles { if err := os.Remove(file); err != nil { - logger.DebugCF("line", "Failed to cleanup temp file", map[string]interface{}{ + logger.DebugCF("line", "Failed to cleanup temp file", map[string]any{ "file": file, "error": err.Error(), }) @@ -374,7 +374,7 @@ func (c *LINEChannel) processEvent(event lineEvent) { metadata["peer_id"] = senderID } - logger.DebugCF("line", "Received message", map[string]interface{}{ + logger.DebugCF("line", "Received message", map[string]any{ "sender_id": senderID, "chat_id": chatID, "message_type": msg.Type, @@ -505,7 +505,7 @@ func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { tokenEntry := entry.(replyTokenEntry) if time.Since(tokenEntry.timestamp) < lineReplyTokenMaxAge { if err := c.sendReply(ctx, tokenEntry.token, msg.Content, quoteToken); err == nil { - logger.DebugCF("line", "Message sent via Reply API", map[string]interface{}{ + logger.DebugCF("line", "Message sent via Reply API", map[string]any{ "chat_id": msg.ChatID, "quoted": quoteToken != "", }) @@ -533,7 +533,7 @@ func buildTextMessage(content, quoteToken string) map[string]string { // sendReply sends a message using the LINE Reply API. func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteToken string) error { - payload := map[string]interface{}{ + payload := map[string]any{ "replyToken": replyToken, "messages": []map[string]string{buildTextMessage(content, quoteToken)}, } @@ -543,7 +543,7 @@ func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteT // sendPush sends a message using the LINE Push API. func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken string) error { - payload := map[string]interface{}{ + payload := map[string]any{ "to": to, "messages": []map[string]string{buildTextMessage(content, quoteToken)}, } @@ -553,19 +553,19 @@ func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken stri // sendLoading sends a loading animation indicator to the chat. func (c *LINEChannel) sendLoading(chatID string) { - payload := map[string]interface{}{ + payload := map[string]any{ "chatId": chatID, "loadingSeconds": 60, } if err := c.callAPI(c.ctx, lineLoadingEndpoint, payload); err != nil { - logger.DebugCF("line", "Failed to send loading indicator", map[string]interface{}{ + logger.DebugCF("line", "Failed to send loading indicator", map[string]any{ "error": err.Error(), }) } } // callAPI makes an authenticated POST request to the LINE API. -func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload interface{}) error { +func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload any) error { body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal payload: %w", err) diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index b80d1c8fb..75edaf49e 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -50,7 +50,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize Telegram channel") telegram, err := NewTelegramChannel(m.config, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]any{ "error": err.Error(), }) } else { @@ -63,7 +63,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize WhatsApp channel") whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]any{ "error": err.Error(), }) } else { @@ -76,7 +76,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize Feishu channel") feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]any{ "error": err.Error(), }) } else { @@ -89,7 +89,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize Discord channel") discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]any{ "error": err.Error(), }) } else { @@ -102,7 +102,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize MaixCam channel") maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]any{ "error": err.Error(), }) } else { @@ -115,7 +115,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize QQ channel") qq, err := NewQQChannel(m.config.Channels.QQ, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]any{ "error": err.Error(), }) } else { @@ -128,7 +128,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize DingTalk channel") dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]any{ "error": err.Error(), }) } else { @@ -141,7 +141,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize Slack channel") slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Slack channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize Slack channel", map[string]any{ "error": err.Error(), }) } else { @@ -154,7 +154,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize LINE channel") line, err := NewLINEChannel(m.config.Channels.LINE, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize LINE channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize LINE channel", map[string]any{ "error": err.Error(), }) } else { @@ -167,7 +167,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize OneBot channel") onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]any{ "error": err.Error(), }) } else { @@ -180,7 +180,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize WeCom channel") wecom, err := NewWeComBotChannel(m.config.Channels.WeCom, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize WeCom channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize WeCom channel", map[string]any{ "error": err.Error(), }) } else { @@ -193,7 +193,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize WeCom App channel") wecomApp, err := NewWeComAppChannel(m.config.Channels.WeComApp, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize WeCom App channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize WeCom App channel", map[string]any{ "error": err.Error(), }) } else { @@ -202,7 +202,7 @@ func (m *Manager) initChannels() error { } } - logger.InfoCF("channels", "Channel initialization completed", map[string]interface{}{ + logger.InfoCF("channels", "Channel initialization completed", map[string]any{ "enabled_channels": len(m.channels), }) @@ -226,11 +226,11 @@ func (m *Manager) StartAll(ctx context.Context) error { go m.dispatchOutbound(dispatchCtx) for name, channel := range m.channels { - logger.InfoCF("channels", "Starting channel", map[string]interface{}{ + logger.InfoCF("channels", "Starting channel", map[string]any{ "channel": name, }) if err := channel.Start(ctx); err != nil { - logger.ErrorCF("channels", "Failed to start channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to start channel", map[string]any{ "channel": name, "error": err.Error(), }) @@ -253,11 +253,11 @@ func (m *Manager) StopAll(ctx context.Context) error { } for name, channel := range m.channels { - logger.InfoCF("channels", "Stopping channel", map[string]interface{}{ + logger.InfoCF("channels", "Stopping channel", map[string]any{ "channel": name, }) if err := channel.Stop(ctx); err != nil { - logger.ErrorCF("channels", "Error stopping channel", map[string]interface{}{ + logger.ErrorCF("channels", "Error stopping channel", map[string]any{ "channel": name, "error": err.Error(), }) @@ -292,14 +292,14 @@ func (m *Manager) dispatchOutbound(ctx context.Context) { m.mu.RUnlock() if !exists { - logger.WarnCF("channels", "Unknown channel for outbound message", map[string]interface{}{ + logger.WarnCF("channels", "Unknown channel for outbound message", map[string]any{ "channel": msg.Channel, }) continue } if err := channel.Send(ctx, msg); err != nil { - logger.ErrorCF("channels", "Error sending message to channel", map[string]interface{}{ + logger.ErrorCF("channels", "Error sending message to channel", map[string]any{ "channel": msg.Channel, "error": err.Error(), }) @@ -315,13 +315,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) { return channel, ok } -func (m *Manager) GetStatus() map[string]interface{} { +func (m *Manager) GetStatus() map[string]any { m.mu.RLock() defer m.mu.RUnlock() - status := make(map[string]interface{}) + status := make(map[string]any) for name, channel := range m.channels { - status[name] = map[string]interface{}{ + status[name] = map[string]any{ "enabled": true, "running": channel.IsRunning(), } diff --git a/pkg/channels/wecom.go b/pkg/channels/wecom.go index 064568243..07bd8488c 100644 --- a/pkg/channels/wecom.go +++ b/pkg/channels/wecom.go @@ -134,7 +134,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error { } c.setRunning(true) - logger.InfoCF("wecom", "WeCom Bot channel started", map[string]interface{}{ + logger.InfoCF("wecom", "WeCom Bot channel started", map[string]any{ "address": addr, "path": webhookPath, }) @@ -142,7 +142,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error { // Start server in goroutine go func() { if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("wecom", "HTTP server error", map[string]interface{}{ + logger.ErrorCF("wecom", "HTTP server error", map[string]any{ "error": err.Error(), }) } @@ -178,7 +178,7 @@ func (c *WeComBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("wecom channel not running") } - logger.DebugCF("wecom", "Sending message via webhook", map[string]interface{}{ + logger.DebugCF("wecom", "Sending message via webhook", map[string]any{ "chat_id": msg.ChatID, "preview": utils.Truncate(msg.Content, 100), }) @@ -230,7 +230,7 @@ func (c *WeComBotChannel) handleVerification(ctx context.Context, w http.Respons // Reference: https://developer.work.weixin.qq.com/document/path/101033 decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, "") if err != nil { - logger.ErrorCF("wecom", "Failed to decrypt echostr", map[string]interface{}{ + logger.ErrorCF("wecom", "Failed to decrypt echostr", map[string]any{ "error": err.Error(), }) http.Error(w, "Decryption failed", http.StatusInternalServerError) @@ -273,7 +273,7 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp } if err := xml.Unmarshal(body, &encryptedMsg); err != nil { - logger.ErrorCF("wecom", "Failed to parse XML", map[string]interface{}{ + logger.ErrorCF("wecom", "Failed to parse XML", map[string]any{ "error": err.Error(), }) http.Error(w, "Invalid XML", http.StatusBadRequest) @@ -292,7 +292,7 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp // Reference: https://developer.work.weixin.qq.com/document/path/101033 decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, "") if err != nil { - logger.ErrorCF("wecom", "Failed to decrypt message", map[string]interface{}{ + logger.ErrorCF("wecom", "Failed to decrypt message", map[string]any{ "error": err.Error(), }) http.Error(w, "Decryption failed", http.StatusInternalServerError) @@ -302,7 +302,7 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp // Parse decrypted JSON message (AIBOT uses JSON format) var msg WeComBotMessage if err := json.Unmarshal([]byte(decryptedMsg), &msg); err != nil { - logger.ErrorCF("wecom", "Failed to parse decrypted message", map[string]interface{}{ + logger.ErrorCF("wecom", "Failed to parse decrypted message", map[string]any{ "error": err.Error(), }) http.Error(w, "Invalid message format", http.StatusBadRequest) @@ -320,8 +320,9 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp // processMessage processes the received message func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessage) { // Skip unsupported message types - if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" && msg.MsgType != "mixed" { - logger.DebugCF("wecom", "Skipping non-supported message type", map[string]interface{}{ + if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" && + msg.MsgType != "mixed" { + logger.DebugCF("wecom", "Skipping non-supported message type", map[string]any{ "msg_type": msg.MsgType, }) return @@ -332,7 +333,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag c.msgMu.Lock() if c.processedMsgs[msgID] { c.msgMu.Unlock() - logger.DebugCF("wecom", "Skipping duplicate message", map[string]interface{}{ + logger.DebugCF("wecom", "Skipping duplicate message", map[string]any{ "msg_id": msgID, }) return @@ -399,7 +400,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag metadata["sender_id"] = senderID } - logger.DebugCF("wecom", "Received message", map[string]interface{}{ + logger.DebugCF("wecom", "Received message", map[string]any{ "sender_id": senderID, "msg_type": msg.MsgType, "peer_kind": peerKind, @@ -468,7 +469,7 @@ func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID, content // handleHealth handles health check requests func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http.Request) { - status := map[string]interface{}{ + status := map[string]any{ "status": "ok", "running": c.IsRunning(), } diff --git a/pkg/channels/wecom_app.go b/pkg/channels/wecom_app.go index 63a1dd815..878504106 100644 --- a/pkg/channels/wecom_app.go +++ b/pkg/channels/wecom_app.go @@ -145,7 +145,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error { // Get initial access token if err := c.refreshAccessToken(); err != nil { - logger.WarnCF("wecom_app", "Failed to get initial access token", map[string]interface{}{ + logger.WarnCF("wecom_app", "Failed to get initial access token", map[string]any{ "error": err.Error(), }) } @@ -171,7 +171,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error { } c.setRunning(true) - logger.InfoCF("wecom_app", "WeCom App channel started", map[string]interface{}{ + logger.InfoCF("wecom_app", "WeCom App channel started", map[string]any{ "address": addr, "path": webhookPath, }) @@ -179,7 +179,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error { // Start server in goroutine go func() { if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("wecom_app", "HTTP server error", map[string]interface{}{ + logger.ErrorCF("wecom_app", "HTTP server error", map[string]any{ "error": err.Error(), }) } @@ -218,7 +218,7 @@ func (c *WeComAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("no valid access token available") } - logger.DebugCF("wecom_app", "Sending message", map[string]interface{}{ + logger.DebugCF("wecom_app", "Sending message", map[string]any{ "chat_id": msg.ChatID, "preview": utils.Truncate(msg.Content, 100), }) @@ -231,7 +231,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request) ctx := r.Context() // Log all incoming requests for debugging - logger.DebugCF("wecom_app", "Received webhook request", map[string]interface{}{ + logger.DebugCF("wecom_app", "Received webhook request", map[string]any{ "method": r.Method, "url": r.URL.String(), "path": r.URL.Path, @@ -250,7 +250,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request) return } - logger.WarnCF("wecom_app", "Method not allowed", map[string]interface{}{ + logger.WarnCF("wecom_app", "Method not allowed", map[string]any{ "method": r.Method, }) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -264,7 +264,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons nonce := query.Get("nonce") echostr := query.Get("echostr") - logger.DebugCF("wecom_app", "Handling verification request", map[string]interface{}{ + logger.DebugCF("wecom_app", "Handling verification request", map[string]any{ "msg_signature": msgSignature, "timestamp": timestamp, "nonce": nonce, @@ -280,7 +280,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons // Verify signature if !WeComVerifySignature(c.config.Token, msgSignature, timestamp, nonce, echostr) { - logger.WarnCF("wecom_app", "Signature verification failed", map[string]interface{}{ + logger.WarnCF("wecom_app", "Signature verification failed", map[string]any{ "token": c.config.Token, "msg_signature": msgSignature, "timestamp": timestamp, @@ -294,13 +294,13 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons // Decrypt echostr with CorpID verification // For WeCom App (自建应用), receiveid should be corp_id - logger.DebugCF("wecom_app", "Attempting to decrypt echostr", map[string]interface{}{ + logger.DebugCF("wecom_app", "Attempting to decrypt echostr", map[string]any{ "encoding_aes_key": c.config.EncodingAESKey, "corp_id": c.config.CorpID, }) decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, c.config.CorpID) if err != nil { - logger.ErrorCF("wecom_app", "Failed to decrypt echostr", map[string]interface{}{ + logger.ErrorCF("wecom_app", "Failed to decrypt echostr", map[string]any{ "error": err.Error(), "encoding_aes_key": c.config.EncodingAESKey, "corp_id": c.config.CorpID, @@ -309,7 +309,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons return } - logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]interface{}{ + logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]any{ "decrypted": decryptedEchoStr, }) @@ -349,7 +349,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp } if err := xml.Unmarshal(body, &encryptedMsg); err != nil { - logger.ErrorCF("wecom_app", "Failed to parse XML", map[string]interface{}{ + logger.ErrorCF("wecom_app", "Failed to parse XML", map[string]any{ "error": err.Error(), }) http.Error(w, "Invalid XML", http.StatusBadRequest) @@ -367,7 +367,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp // For WeCom App (自建应用), receiveid should be corp_id decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, c.config.CorpID) if err != nil { - logger.ErrorCF("wecom_app", "Failed to decrypt message", map[string]interface{}{ + logger.ErrorCF("wecom_app", "Failed to decrypt message", map[string]any{ "error": err.Error(), }) http.Error(w, "Decryption failed", http.StatusInternalServerError) @@ -377,7 +377,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp // Parse decrypted XML message var msg WeComXMLMessage if err := xml.Unmarshal([]byte(decryptedMsg), &msg); err != nil { - logger.ErrorCF("wecom_app", "Failed to parse decrypted message", map[string]interface{}{ + logger.ErrorCF("wecom_app", "Failed to parse decrypted message", map[string]any{ "error": err.Error(), }) http.Error(w, "Invalid message format", http.StatusBadRequest) @@ -396,7 +396,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessage) { // Skip non-text messages for now (can be extended) if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" { - logger.DebugCF("wecom_app", "Skipping non-supported message type", map[string]interface{}{ + logger.DebugCF("wecom_app", "Skipping non-supported message type", map[string]any{ "msg_type": msg.MsgType, }) return @@ -408,7 +408,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag c.msgMu.Lock() if c.processedMsgs[msgID] { c.msgMu.Unlock() - logger.DebugCF("wecom_app", "Skipping duplicate message", map[string]interface{}{ + logger.DebugCF("wecom_app", "Skipping duplicate message", map[string]any{ "msg_id": msgID, }) return @@ -441,7 +441,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag content := msg.Content - logger.DebugCF("wecom_app", "Received message", map[string]interface{}{ + logger.DebugCF("wecom_app", "Received message", map[string]any{ "sender_id": senderID, "msg_type": msg.MsgType, "preview": utils.Truncate(content, 50), @@ -462,7 +462,7 @@ func (c *WeComAppChannel) tokenRefreshLoop() { return case <-ticker.C: if err := c.refreshAccessToken(); err != nil { - logger.ErrorCF("wecom_app", "Failed to refresh access token", map[string]interface{}{ + logger.ErrorCF("wecom_app", "Failed to refresh access token", map[string]any{ "error": err.Error(), }) } @@ -628,7 +628,7 @@ func (c *WeComAppChannel) sendMarkdownMessage(ctx context.Context, accessToken, // handleHealth handles health check requests func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) { - status := map[string]interface{}{ + status := map[string]any{ "status": "ok", "running": c.IsRunning(), "has_token": c.getAccessToken() != "", diff --git a/pkg/channels/wecom_app_test.go b/pkg/channels/wecom_app_test.go index bc40806bb..6778520f3 100644 --- a/pkg/channels/wecom_app_test.go +++ b/pkg/channels/wecom_app_test.go @@ -399,7 +399,11 @@ func TestWeComAppHandleVerification(t *testing.T) { nonce := "test_nonce" signature := generateSignatureApp("test_token", timestamp, nonce, encryptedEchostr) - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, + nil, + ) w := httptest.NewRecorder() ch.handleVerification(context.Background(), w, req) @@ -429,7 +433,11 @@ func TestWeComAppHandleVerification(t *testing.T) { timestamp := "1234567890" nonce := "test_nonce" - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, + nil, + ) w := httptest.NewRecorder() ch.handleVerification(context.Background(), w, req) @@ -481,7 +489,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) { nonce := "test_nonce" signature := generateSignatureApp("test_token", timestamp, nonce, encrypted) - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -510,7 +522,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) { nonce := "test_nonce" signature := generateSignatureApp("test_token", timestamp, nonce, "") - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, strings.NewReader("invalid xml")) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + strings.NewReader("invalid xml"), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -532,7 +548,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) { timestamp := "1234567890" nonce := "test_nonce" - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -646,7 +666,11 @@ func TestWeComAppHandleWebhook(t *testing.T) { nonce := "test_nonce" signature := generateSignatureApp("test_token", timestamp, nonce, encoded) - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, + nil, + ) w := httptest.NewRecorder() ch.handleWebhook(w, req) @@ -669,7 +693,11 @@ func TestWeComAppHandleWebhook(t *testing.T) { nonce := "test_nonce" signature := generateSignatureApp("test_token", timestamp, nonce, encryptedWrapper.Encrypt) - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleWebhook(w, req) diff --git a/pkg/channels/wecom_test.go b/pkg/channels/wecom_test.go index c3f889c64..53cde2693 100644 --- a/pkg/channels/wecom_test.go +++ b/pkg/channels/wecom_test.go @@ -358,7 +358,11 @@ func TestWeComBotHandleVerification(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, encryptedEchostr) - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, + nil, + ) w := httptest.NewRecorder() ch.handleVerification(context.Background(), w, req) @@ -388,7 +392,11 @@ func TestWeComBotHandleVerification(t *testing.T) { timestamp := "1234567890" nonce := "test_nonce" - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, + nil, + ) w := httptest.NewRecorder() ch.handleVerification(context.Background(), w, req) @@ -437,7 +445,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, encrypted) - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -479,7 +491,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, encrypted) - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -508,7 +524,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, "") - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, strings.NewReader("invalid xml")) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + strings.NewReader("invalid xml"), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -530,7 +550,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) { timestamp := "1234567890" nonce := "test_nonce" - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleMessageCallback(context.Background(), w, req) @@ -625,7 +649,11 @@ func TestWeComBotHandleWebhook(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, encoded) - req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, nil) + req := httptest.NewRequest( + http.MethodGet, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, + nil, + ) w := httptest.NewRecorder() ch.handleWebhook(w, req) @@ -648,7 +676,11 @@ func TestWeComBotHandleWebhook(t *testing.T) { nonce := "test_nonce" signature := generateSignature("test_token", timestamp, nonce, encryptedWrapper.Encrypt) - req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData)) + req := httptest.NewRequest( + http.MethodPost, + "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, + bytes.NewReader(wrapperData), + ) w := httptest.NewRecorder() ch.handleWebhook(w, req) diff --git a/pkg/config/config.go b/pkg/config/config.go index 005631e4a..20556011a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,7 +26,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error { } // Try []interface{} to handle mixed types - var raw []interface{} + var raw []any if err := json.Unmarshal(data, &raw); err != nil { return err } @@ -167,16 +167,16 @@ type SessionConfig struct { } type AgentDefaults struct { - Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` - RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` - Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` + Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` + RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` + Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` + Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` ModelFallbacks []string `json:"model_fallbacks,omitempty"` - ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` + ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` - MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` - Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` - MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` + MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` } type ChannelsConfig struct { @@ -195,114 +195,114 @@ type ChannelsConfig struct { } type WhatsAppConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` } type TelegramConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` - Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` + Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` } type FeishuConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` - AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` - AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` - EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` + AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` + AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` + EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` } type DiscordConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"` } type MaixCamConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"` - Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"` - Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"` + Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"` + Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"` } type QQConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"` - AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"` + AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"` AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"` } type DingTalkConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"` - ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"` + ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"` ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"` } type SlackConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"` - BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"` - AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"` + BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"` + AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"` } type LINEConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"` - ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"` + ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"` ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"` - WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"` - WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"` - WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"` + WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"` + WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"` + WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"` } type OneBotConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"` - WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"` - AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"` - ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"` + WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"` + AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"` + ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"` GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` } type WeComConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"` EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"` - WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"` - WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"` - WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"` - WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"` - ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"` + WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"` + WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"` + WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"` + WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"` + ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"` } type WeComAppConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"` - CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"` - CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"` - AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"` + CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"` + CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"` + AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"` + Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"` EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"` - WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"` - WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"` - WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"` - ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"` + WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"` + WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"` + WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"` + ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"` } type HeartbeatConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5 } type DevicesConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"` MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"` } @@ -359,11 +359,11 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` - APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` - Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` - AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` - ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc` + APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` + APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` + AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` + ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` } type OpenAIProviderConfig struct { @@ -413,19 +413,19 @@ type GatewayConfig struct { } type BraveConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` } type DuckDuckGoConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` } type PerplexityConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` } @@ -458,7 +458,7 @@ type SkillsToolsConfig struct { } type SearchCacheConfig struct { - MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"` + MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"` TTLSeconds int `json:"ttl_seconds" env:"PICOCLAW_SKILLS_SEARCH_CACHE_TTL_SECONDS"` } @@ -467,14 +467,14 @@ type SkillsRegistriesConfig struct { } type ClawHubRegistryConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` - BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` - AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"` - SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"` - SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` - DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"` - Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"` - MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"` + Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` + BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` + AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"` + SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"` + SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` + DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"` + Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"` + MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"` MaxResponseSize int `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"` } @@ -517,11 +517,11 @@ func SaveConfig(path string, cfg *Config) error { } dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { return err } - return os.WriteFile(path, data, 0600) + return os.WriteFile(path, data, 0o600) } func (c *Config) WorkspacePath() string { diff --git a/pkg/config/migration_test.go b/pkg/config/migration_test.go index b9a333f9e..1e8139e68 100644 --- a/pkg/config/migration_test.go +++ b/pkg/config/migration_test.go @@ -361,7 +361,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) { Agents: AgentsConfig{ Defaults: AgentDefaults{ Provider: tt.providerAlias, - Model: strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1]), + Model: strings.TrimPrefix( + tt.expectedModel, + tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1], + ), }, }, Providers: ProvidersConfig{}, @@ -382,7 +385,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) { } // Need to fix the model name in config - cfg.Agents.Defaults.Model = strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1]) + cfg.Agents.Defaults.Model = strings.TrimPrefix( + tt.expectedModel, + tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1], + ) result := ConvertProvidersToModelList(cfg) if len(result) != 1 { @@ -515,7 +521,11 @@ func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) { func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) { result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4.6") if result != "openrouter/claude-sonnet-4.6" { - t.Errorf("buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4.6) = %q, want %q", result, "openrouter/claude-sonnet-4.6") + t.Errorf( + "buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4.6) = %q, want %q", + result, + "openrouter/claude-sonnet-4.6", + ) } } diff --git a/pkg/migrate/migrate_test.go b/pkg/migrate/migrate_test.go index 759fc9024..ccc00f72c 100644 --- a/pkg/migrate/migrate_test.go +++ b/pkg/migrate/migrate_test.go @@ -40,20 +40,20 @@ func TestCamelToSnake(t *testing.T) { } func TestConvertKeysToSnake(t *testing.T) { - input := map[string]interface{}{ + input := map[string]any{ "apiKey": "test-key", "apiBase": "https://example.com", - "nested": map[string]interface{}{ + "nested": map[string]any{ "maxTokens": float64(8192), - "allowFrom": []interface{}{"user1", "user2"}, - "deeperLevel": map[string]interface{}{ + "allowFrom": []any{"user1", "user2"}, + "deeperLevel": map[string]any{ "clientId": "abc", }, }, } result := convertKeysToSnake(input) - m, ok := result.(map[string]interface{}) + m, ok := result.(map[string]any) if !ok { t.Fatal("expected map[string]interface{}") } @@ -65,7 +65,7 @@ func TestConvertKeysToSnake(t *testing.T) { t.Error("expected key 'api_base' after conversion") } - nested, ok := m["nested"].(map[string]interface{}) + nested, ok := m["nested"].(map[string]any) if !ok { t.Fatal("expected nested map") } @@ -76,7 +76,7 @@ func TestConvertKeysToSnake(t *testing.T) { t.Error("expected key 'allow_from' in nested map") } - deeper, ok := nested["deeper_level"].(map[string]interface{}) + deeper, ok := nested["deeper_level"].(map[string]any) if !ok { t.Fatal("expected deeper_level map") } @@ -89,15 +89,15 @@ func TestLoadOpenClawConfig(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "openclaw.json") - openclawConfig := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + openclawConfig := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "apiKey": "sk-ant-test123", "apiBase": "https://api.anthropic.com", }, }, - "agents": map[string]interface{}{ - "defaults": map[string]interface{}{ + "agents": map[string]any{ + "defaults": map[string]any{ "maxTokens": float64(4096), "model": "claude-3-opus", }, @@ -108,7 +108,7 @@ func TestLoadOpenClawConfig(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(configPath, data, 0644); err != nil { + if err := os.WriteFile(configPath, data, 0o644); err != nil { t.Fatal(err) } @@ -117,11 +117,11 @@ func TestLoadOpenClawConfig(t *testing.T) { t.Fatalf("LoadOpenClawConfig: %v", err) } - providers, ok := result["providers"].(map[string]interface{}) + providers, ok := result["providers"].(map[string]any) if !ok { t.Fatal("expected providers map") } - anthropic, ok := providers["anthropic"].(map[string]interface{}) + anthropic, ok := providers["anthropic"].(map[string]any) if !ok { t.Fatal("expected anthropic map") } @@ -129,11 +129,11 @@ func TestLoadOpenClawConfig(t *testing.T) { t.Errorf("api_key = %v, want sk-ant-test123", anthropic["api_key"]) } - agents, ok := result["agents"].(map[string]interface{}) + agents, ok := result["agents"].(map[string]any) if !ok { t.Fatal("expected agents map") } - defaults, ok := agents["defaults"].(map[string]interface{}) + defaults, ok := agents["defaults"].(map[string]any) if !ok { t.Fatal("expected defaults map") } @@ -144,16 +144,16 @@ func TestLoadOpenClawConfig(t *testing.T) { func TestConvertConfig(t *testing.T) { t.Run("providers mapping", func(t *testing.T) { - data := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + data := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "api_key": "sk-ant-test", "api_base": "https://api.anthropic.com", }, - "openrouter": map[string]interface{}{ + "openrouter": map[string]any{ "api_key": "sk-or-test", }, - "groq": map[string]interface{}{ + "groq": map[string]any{ "api_key": "gsk-test", }, }, @@ -178,9 +178,9 @@ func TestConvertConfig(t *testing.T) { }) t.Run("unsupported provider warning", func(t *testing.T) { - data := map[string]interface{}{ - "providers": map[string]interface{}{ - "unknown_provider": map[string]interface{}{ + data := map[string]any{ + "providers": map[string]any{ + "unknown_provider": map[string]any{ "api_key": "sk-test", }, }, @@ -199,14 +199,14 @@ func TestConvertConfig(t *testing.T) { }) t.Run("channels mapping", func(t *testing.T) { - data := map[string]interface{}{ - "channels": map[string]interface{}{ - "telegram": map[string]interface{}{ + data := map[string]any{ + "channels": map[string]any{ + "telegram": map[string]any{ "enabled": true, "token": "tg-token-123", - "allow_from": []interface{}{"user1"}, + "allow_from": []any{"user1"}, }, - "discord": map[string]interface{}{ + "discord": map[string]any{ "enabled": true, "token": "disc-token-456", }, @@ -232,9 +232,9 @@ func TestConvertConfig(t *testing.T) { }) t.Run("unsupported channel warning", func(t *testing.T) { - data := map[string]interface{}{ - "channels": map[string]interface{}{ - "email": map[string]interface{}{ + data := map[string]any{ + "channels": map[string]any{ + "email": map[string]any{ "enabled": true, }, }, @@ -253,9 +253,9 @@ func TestConvertConfig(t *testing.T) { }) t.Run("agent defaults", func(t *testing.T) { - data := map[string]interface{}{ - "agents": map[string]interface{}{ - "defaults": map[string]interface{}{ + data := map[string]any{ + "agents": map[string]any{ + "defaults": map[string]any{ "model": "claude-3-opus", "max_tokens": float64(4096), "temperature": 0.5, @@ -287,7 +287,7 @@ func TestConvertConfig(t *testing.T) { }) t.Run("empty config", func(t *testing.T) { - data := map[string]interface{}{} + data := map[string]any{} cfg, warnings, err := ConvertConfig(data) if err != nil { @@ -389,9 +389,9 @@ func TestPlanWorkspaceMigration(t *testing.T) { srcDir := t.TempDir() dstDir := t.TempDir() - os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644) - os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0644) - os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0644) + os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644) + os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0o644) + os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0o644) actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) if err != nil { @@ -420,8 +420,8 @@ func TestPlanWorkspaceMigration(t *testing.T) { srcDir := t.TempDir() dstDir := t.TempDir() - os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644) - os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0644) + os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644) + os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0o644) actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) if err != nil { @@ -443,8 +443,8 @@ func TestPlanWorkspaceMigration(t *testing.T) { srcDir := t.TempDir() dstDir := t.TempDir() - os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644) - os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0644) + os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644) + os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0o644) actions, err := PlanWorkspaceMigration(srcDir, dstDir, true) if err != nil { @@ -463,8 +463,8 @@ func TestPlanWorkspaceMigration(t *testing.T) { dstDir := t.TempDir() memDir := filepath.Join(srcDir, "memory") - os.MkdirAll(memDir, 0755) - os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0644) + os.MkdirAll(memDir, 0o755) + os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0o644) actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) if err != nil { @@ -494,8 +494,8 @@ func TestPlanWorkspaceMigration(t *testing.T) { dstDir := t.TempDir() skillDir := filepath.Join(srcDir, "skills", "weather") - os.MkdirAll(skillDir, 0755) - os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0644) + os.MkdirAll(skillDir, 0o755) + os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0o644) actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) if err != nil { @@ -518,7 +518,7 @@ func TestFindOpenClawConfig(t *testing.T) { t.Run("finds openclaw.json", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "openclaw.json") - os.WriteFile(configPath, []byte("{}"), 0644) + os.WriteFile(configPath, []byte("{}"), 0o644) found, err := findOpenClawConfig(tmpDir) if err != nil { @@ -532,7 +532,7 @@ func TestFindOpenClawConfig(t *testing.T) { t.Run("falls back to config.json", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.json") - os.WriteFile(configPath, []byte("{}"), 0644) + os.WriteFile(configPath, []byte("{}"), 0o644) found, err := findOpenClawConfig(tmpDir) if err != nil { @@ -546,8 +546,8 @@ func TestFindOpenClawConfig(t *testing.T) { t.Run("prefers openclaw.json over config.json", func(t *testing.T) { tmpDir := t.TempDir() openclawPath := filepath.Join(tmpDir, "openclaw.json") - os.WriteFile(openclawPath, []byte("{}"), 0644) - os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0644) + os.WriteFile(openclawPath, []byte("{}"), 0o644) + os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0o644) found, err := findOpenClawConfig(tmpDir) if err != nil { @@ -593,19 +593,19 @@ func TestRunDryRun(t *testing.T) { picoClawHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") - os.MkdirAll(wsDir, 0755) - os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) - os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0644) + os.MkdirAll(wsDir, 0o755) + os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644) + os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0o644) - configData := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + configData := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "apiKey": "test-key", }, }, } data, _ := json.Marshal(configData) - os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) + os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644) opts := Options{ DryRun: true, @@ -634,33 +634,33 @@ func TestRunFullMigration(t *testing.T) { picoClawHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") - os.MkdirAll(wsDir, 0755) - os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0644) - os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644) - os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0644) + os.MkdirAll(wsDir, 0o755) + os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0o644) + os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644) + os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0o644) memDir := filepath.Join(wsDir, "memory") - os.MkdirAll(memDir, 0755) - os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0644) + os.MkdirAll(memDir, 0o755) + os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0o644) - configData := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + configData := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "apiKey": "sk-ant-migrate-test", }, - "openrouter": map[string]interface{}{ + "openrouter": map[string]any{ "apiKey": "sk-or-migrate-test", }, }, - "channels": map[string]interface{}{ - "telegram": map[string]interface{}{ + "channels": map[string]any{ + "telegram": map[string]any{ "enabled": true, "token": "tg-migrate-test", }, }, } data, _ := json.Marshal(configData) - os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) + os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644) opts := Options{ Force: true, @@ -754,7 +754,7 @@ func TestRunMutuallyExclusiveFlags(t *testing.T) { func TestBackupFile(t *testing.T) { tmpDir := t.TempDir() filePath := filepath.Join(tmpDir, "test.md") - os.WriteFile(filePath, []byte("original content"), 0644) + os.WriteFile(filePath, []byte("original content"), 0o644) if err := backupFile(filePath); err != nil { t.Fatalf("backupFile: %v", err) @@ -775,7 +775,7 @@ func TestCopyFile(t *testing.T) { srcPath := filepath.Join(tmpDir, "src.md") dstPath := filepath.Join(tmpDir, "dst.md") - os.WriteFile(srcPath, []byte("file content"), 0644) + os.WriteFile(srcPath, []byte("file content"), 0o644) if err := copyFile(srcPath, dstPath); err != nil { t.Fatalf("copyFile: %v", err) @@ -795,18 +795,18 @@ func TestRunConfigOnly(t *testing.T) { picoClawHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") - os.MkdirAll(wsDir, 0755) - os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) + os.MkdirAll(wsDir, 0o755) + os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644) - configData := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + configData := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "apiKey": "sk-config-only", }, }, } data, _ := json.Marshal(configData) - os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) + os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644) opts := Options{ Force: true, @@ -835,18 +835,18 @@ func TestRunWorkspaceOnly(t *testing.T) { picoClawHome := t.TempDir() wsDir := filepath.Join(openclawHome, "workspace") - os.MkdirAll(wsDir, 0755) - os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) + os.MkdirAll(wsDir, 0o755) + os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644) - configData := map[string]interface{}{ - "providers": map[string]interface{}{ - "anthropic": map[string]interface{}{ + configData := map[string]any{ + "providers": map[string]any{ + "anthropic": map[string]any{ "apiKey": "sk-ws-only", }, }, } data, _ := json.Marshal(configData) - os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) + os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644) opts := Options{ Force: true, diff --git a/pkg/providers/anthropic/provider_test.go b/pkg/providers/anthropic/provider_test.go index 08ac9c829..3d21c1d0b 100644 --- a/pkg/providers/anthropic/provider_test.go +++ b/pkg/providers/anthropic/provider_test.go @@ -15,7 +15,7 @@ func TestBuildParams_BasicMessage(t *testing.T) { messages := []Message{ {Role: "user", Content: "Hello"}, } - params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{ + params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{ "max_tokens": 1024, }) if err != nil { @@ -37,7 +37,7 @@ func TestBuildParams_SystemMessage(t *testing.T) { {Role: "system", Content: "You are helpful"}, {Role: "user", Content: "Hi"}, } - params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{}) + params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{}) if err != nil { t.Fatalf("buildParams() error: %v", err) } @@ -62,13 +62,13 @@ func TestBuildParams_ToolCallMessage(t *testing.T) { { ID: "call_1", Name: "get_weather", - Arguments: map[string]interface{}{"city": "SF"}, + Arguments: map[string]any{"city": "SF"}, }, }, }, {Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"}, } - params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{}) + params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{}) if err != nil { t.Fatalf("buildParams() error: %v", err) } @@ -84,17 +84,17 @@ func TestBuildParams_WithTools(t *testing.T) { Function: ToolFunctionDefinition{ Name: "get_weather", Description: "Get weather for a city", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "city": map[string]interface{}{"type": "string"}, + "properties": map[string]any{ + "city": map[string]any{"type": "string"}, }, - "required": []interface{}{"city"}, + "required": []any{"city"}, }, }, }, } - params, err := buildParams([]Message{{Role: "user", Content: "Hi"}}, tools, "claude-sonnet-4.6", map[string]interface{}{}) + params, err := buildParams([]Message{{Role: "user", Content: "Hi"}}, tools, "claude-sonnet-4.6", map[string]any{}) if err != nil { t.Fatalf("buildParams() error: %v", err) } @@ -154,19 +154,19 @@ func TestProvider_ChatRoundTrip(t *testing.T) { return } - var reqBody map[string]interface{} + var reqBody map[string]any json.NewDecoder(r.Body).Decode(&reqBody) - resp := map[string]interface{}{ + resp := map[string]any{ "id": "msg_test", "type": "message", "role": "assistant", "model": reqBody["model"], "stop_reason": "end_turn", - "content": []map[string]interface{}{ + "content": []map[string]any{ {"type": "text", "text": "Hello! How can I help you?"}, }, - "usage": map[string]interface{}{ + "usage": map[string]any{ "input_tokens": 15, "output_tokens": 8, }, @@ -178,7 +178,7 @@ func TestProvider_ChatRoundTrip(t *testing.T) { provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token")) messages := []Message{{Role: "user", Content: "Hello"}} - resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]interface{}{"max_tokens": 1024}) + resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 1024}) if err != nil { t.Fatalf("Chat() error: %v", err) } @@ -221,19 +221,19 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) { return } - var reqBody map[string]interface{} + var reqBody map[string]any json.NewDecoder(r.Body).Decode(&reqBody) - resp := map[string]interface{}{ + resp := map[string]any{ "id": "msg_test", "type": "message", "role": "assistant", "model": reqBody["model"], "stop_reason": "end_turn", - "content": []map[string]interface{}{ + "content": []map[string]any{ {"type": "text", "text": "ok"}, }, - "usage": map[string]interface{}{ + "usage": map[string]any{ "input_tokens": 1, "output_tokens": 1, }, @@ -247,7 +247,13 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) { return "refreshed-token", nil }, server.URL) - _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hello"}}, nil, "claude-sonnet-4.6", map[string]interface{}{}) + _, err := p.Chat( + t.Context(), + []Message{{Role: "user", Content: "hello"}}, + nil, + "claude-sonnet-4.6", + map[string]any{}, + ) if err != nil { t.Fatalf("Chat() error: %v", err) } diff --git a/pkg/providers/antigravity_provider.go b/pkg/providers/antigravity_provider.go index 6c6bf7830..cff67c88c 100644 --- a/pkg/providers/antigravity_provider.go +++ b/pkg/providers/antigravity_provider.go @@ -45,7 +45,13 @@ func NewAntigravityProvider() *AntigravityProvider { // Chat implements LLMProvider.Chat using the Cloud Code Assist v1internal API. // The v1internal endpoint wraps the standard Gemini request in an envelope with // project, model, request, requestType, userAgent, and requestId fields. -func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { +func (p *AntigravityProvider) Chat( + ctx context.Context, + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, +) (*LLMResponse, error) { accessToken, projectID, err := p.tokenSource() if err != nil { return nil, fmt.Errorf("antigravity auth: %w", err) @@ -58,7 +64,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool model = strings.TrimPrefix(model, "google-antigravity/") model = strings.TrimPrefix(model, "antigravity/") - logger.DebugCF("provider.antigravity", "Starting chat", map[string]interface{}{ + logger.DebugCF("provider.antigravity", "Starting chat", map[string]any{ "model": model, "project": projectID, "requestId": fmt.Sprintf("agent-%d-%s", time.Now().UnixMilli(), randomString(9)), @@ -68,7 +74,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool innerRequest := p.buildRequest(messages, tools, model, options) // Wrap in v1internal envelope (matches pi-ai SDK format) - envelope := map[string]interface{}{ + envelope := map[string]any{ "project": projectID, "model": model, "request": innerRequest, @@ -115,7 +121,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool } if resp.StatusCode != http.StatusOK { - logger.ErrorCF("provider.antigravity", "API call failed", map[string]interface{}{ + logger.ErrorCF("provider.antigravity", "API call failed", map[string]any{ "status_code": resp.StatusCode, "response": string(respBody), "model": model, @@ -133,7 +139,9 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool // Check for empty response (some models might return valid success but empty text) if llmResp.Content == "" && len(llmResp.ToolCalls) == 0 { - return nil, fmt.Errorf("antigravity: model returned an empty response (this model might be invalid or restricted)") + return nil, fmt.Errorf( + "antigravity: model returned an empty response (this model might be invalid or restricted)", + ) } return llmResp, nil @@ -167,13 +175,13 @@ type antigravityPart struct { } type antigravityFunctionCall struct { - Name string `json:"name"` - Args map[string]interface{} `json:"args"` + Name string `json:"name"` + Args map[string]any `json:"args"` } type antigravityFunctionResponse struct { - Name string `json:"name"` - Response map[string]interface{} `json:"response"` + Name string `json:"name"` + Response map[string]any `json:"response"` } type antigravityTool struct { @@ -181,9 +189,9 @@ type antigravityTool struct { } type antigravityFuncDecl struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Parameters interface{} `json:"parameters,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Parameters any `json:"parameters,omitempty"` } type antigravitySystemPrompt struct { @@ -195,7 +203,12 @@ type antigravityGenConfig struct { Temperature float64 `json:"temperature,omitempty"` } -func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest { +func (p *AntigravityProvider) buildRequest( + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, +) antigravityRequest { req := antigravityRequest{} toolCallNames := make(map[string]string) @@ -215,7 +228,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin Parts: []antigravityPart{{ FunctionResponse: &antigravityFunctionResponse{ Name: toolName, - Response: map[string]interface{}{ + Response: map[string]any{ "result": msg.Content, }, }, @@ -237,9 +250,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin for _, tc := range msg.ToolCalls { toolName, toolArgs, thoughtSignature := normalizeStoredToolCall(tc) if toolName == "" { - logger.WarnCF("provider.antigravity", "Skipping tool call with empty name in history", map[string]interface{}{ - "tool_call_id": tc.ID, - }) + logger.WarnCF( + "provider.antigravity", + "Skipping tool call with empty name in history", + map[string]any{ + "tool_call_id": tc.ID, + }, + ) continue } if tc.ID != "" { @@ -264,7 +281,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin Parts: []antigravityPart{{ FunctionResponse: &antigravityFunctionResponse{ Name: toolName, - Response: map[string]interface{}{ + Response: map[string]any{ "result": msg.Content, }, }, @@ -311,7 +328,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin return req } -func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, string) { +func normalizeStoredToolCall(tc ToolCall) (string, map[string]any, string) { name := tc.Name args := tc.Arguments thoughtSignature := "" @@ -324,11 +341,11 @@ func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, strin } if args == nil { - args = map[string]interface{}{} + args = map[string]any{} } if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" { - var parsed map[string]interface{} + var parsed map[string]any if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil { args = parsed } @@ -483,9 +500,12 @@ func (p *AntigravityProvider) parseSSEResponse(body string) (*LLMResponse, error Name: part.FunctionCall.Name, Arguments: part.FunctionCall.Args, Function: &FunctionCall{ - Name: part.FunctionCall.Name, - Arguments: string(argumentsJSON), - ThoughtSignature: extractPartThoughtSignature(part.ThoughtSignature, part.ThoughtSignatureSnake), + Name: part.FunctionCall.Name, + Arguments: string(argumentsJSON), + ThoughtSignature: extractPartThoughtSignature( + part.ThoughtSignature, + part.ThoughtSignatureSnake, + ), }, }) } @@ -556,24 +576,24 @@ var geminiUnsupportedKeywords = map[string]bool{ "maxProperties": true, } -func sanitizeSchemaForGemini(schema map[string]interface{}) map[string]interface{} { +func sanitizeSchemaForGemini(schema map[string]any) map[string]any { if schema == nil { return nil } - result := make(map[string]interface{}) + result := make(map[string]any) for k, v := range schema { if geminiUnsupportedKeywords[k] { continue } // Recursively sanitize nested objects switch val := v.(type) { - case map[string]interface{}: + case map[string]any: result[k] = sanitizeSchemaForGemini(val) - case []interface{}: - sanitized := make([]interface{}, len(val)) + case []any: + sanitized := make([]any, len(val)) for i, item := range val { - if m, ok := item.(map[string]interface{}); ok { + if m, ok := item.(map[string]any); ok { sanitized[i] = sanitizeSchemaForGemini(m) } else { sanitized[i] = item @@ -604,7 +624,9 @@ func createAntigravityTokenSource() func() (string, string, error) { return "", "", fmt.Errorf("loading auth credentials: %w", err) } if cred == nil { - return "", "", fmt.Errorf("no credentials for google-antigravity. Run: picoclaw auth login --provider google-antigravity") + return "", "", fmt.Errorf( + "no credentials for google-antigravity. Run: picoclaw auth login --provider google-antigravity", + ) } // Refresh if needed @@ -625,7 +647,9 @@ func createAntigravityTokenSource() func() (string, string, error) { } if cred.IsExpired() { - return "", "", fmt.Errorf("antigravity credentials expired. Run: picoclaw auth login --provider google-antigravity") + return "", "", fmt.Errorf( + "antigravity credentials expired. Run: picoclaw auth login --provider google-antigravity", + ) } projectID := cred.ProjectID @@ -633,7 +657,7 @@ func createAntigravityTokenSource() func() (string, string, error) { // Try to fetch project ID from API fetchedID, err := FetchAntigravityProjectID(cred.AccessToken) if err != nil { - logger.WarnCF("provider.antigravity", "Could not fetch project ID, using fallback", map[string]interface{}{ + logger.WarnCF("provider.antigravity", "Could not fetch project ID, using fallback", map[string]any{ "error": err.Error(), }) projectID = "rising-fact-p41fc" // Default fallback (same as OpenCode) @@ -650,8 +674,8 @@ func createAntigravityTokenSource() func() (string, string, error) { // FetchAntigravityProjectID retrieves the Google Cloud project ID from the loadCodeAssist endpoint. func FetchAntigravityProjectID(accessToken string) (string, error) { - reqBody, _ := json.Marshal(map[string]interface{}{ - "metadata": map[string]interface{}{ + reqBody, _ := json.Marshal(map[string]any{ + "metadata": map[string]any{ "ideType": "IDE_UNSPECIFIED", "platform": "PLATFORM_UNSPECIFIED", "pluginType": "GEMINI", @@ -695,7 +719,7 @@ func FetchAntigravityProjectID(accessToken string) (string, error) { // FetchAntigravityModels fetches available models from the Cloud Code Assist API. func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) { - reqBody, _ := json.Marshal(map[string]interface{}{ + reqBody, _ := json.Marshal(map[string]any{ "project": projectID, }) @@ -717,16 +741,20 @@ func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelIn body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("fetchAvailableModels failed (HTTP %d): %s", resp.StatusCode, truncateString(string(body), 200)) + return nil, fmt.Errorf( + "fetchAvailableModels failed (HTTP %d): %s", + resp.StatusCode, + truncateString(string(body), 200), + ) } var result struct { Models map[string]struct { DisplayName string `json:"displayName"` QuotaInfo struct { - RemainingFraction interface{} `json:"remainingFraction"` - ResetTime string `json:"resetTime"` - IsExhausted bool `json:"isExhausted"` + RemainingFraction any `json:"remainingFraction"` + ResetTime string `json:"resetTime"` + IsExhausted bool `json:"isExhausted"` } `json:"quotaInfo"` } `json:"models"` } @@ -797,10 +825,10 @@ func randomString(n int) string { func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte) error { var errResp struct { Error struct { - Code int `json:"code"` - Message string `json:"message"` - Status string `json:"status"` - Details []map[string]interface{} `json:"details"` + Code int `json:"code"` + Message string `json:"message"` + Status string `json:"status"` + Details []map[string]any `json:"details"` } `json:"error"` } @@ -813,7 +841,7 @@ func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte) // Try to extract quota reset info for _, detail := range errResp.Error.Details { if typeVal, ok := detail["@type"].(string); ok && strings.HasSuffix(typeVal, "ErrorInfo") { - if metadata, ok := detail["metadata"].(map[string]interface{}); ok { + if metadata, ok := detail["metadata"].(map[string]any); ok { if delay, ok := metadata["quotaResetDelay"].(string); ok { return fmt.Errorf("antigravity rate limit exceeded: %s (reset in %s)", msg, delay) } diff --git a/pkg/providers/claude_provider_test.go b/pkg/providers/claude_provider_test.go index b1bcd8b40..98e07bb80 100644 --- a/pkg/providers/claude_provider_test.go +++ b/pkg/providers/claude_provider_test.go @@ -8,6 +8,7 @@ import ( "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" + anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic" ) @@ -22,19 +23,19 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) { return } - var reqBody map[string]interface{} + var reqBody map[string]any json.NewDecoder(r.Body).Decode(&reqBody) - resp := map[string]interface{}{ + resp := map[string]any{ "id": "msg_test", "type": "message", "role": "assistant", "model": reqBody["model"], "stop_reason": "end_turn", - "content": []map[string]interface{}{ + "content": []map[string]any{ {"type": "text", "text": "Hello! How can I help you?"}, }, - "usage": map[string]interface{}{ + "usage": map[string]any{ "input_tokens": 15, "output_tokens": 8, }, @@ -48,7 +49,7 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) { provider := newClaudeProviderWithDelegate(delegate) messages := []Message{{Role: "user", Content: "Hello"}} - resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]interface{}{"max_tokens": 1024}) + resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 1024}) if err != nil { t.Fatalf("Chat() error: %v", err) } diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index eeaa9690a..d0c4344f3 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -28,7 +28,13 @@ func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField st } } -func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { +func (p *HTTPProvider) Chat( + ctx context.Context, + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, +) (*LLMResponse, error) { return p.delegate.Chat(ctx, messages, tools, model, options) } diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 6bc43a470..b8528953a 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -15,15 +15,17 @@ import ( "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) -type ToolCall = protocoltypes.ToolCall -type FunctionCall = protocoltypes.FunctionCall -type LLMResponse = protocoltypes.LLMResponse -type UsageInfo = protocoltypes.UsageInfo -type Message = protocoltypes.Message -type ToolDefinition = protocoltypes.ToolDefinition -type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition -type ExtraContent = protocoltypes.ExtraContent -type GoogleExtra = protocoltypes.GoogleExtra +type ( + ToolCall = protocoltypes.ToolCall + FunctionCall = protocoltypes.FunctionCall + LLMResponse = protocoltypes.LLMResponse + UsageInfo = protocoltypes.UsageInfo + Message = protocoltypes.Message + ToolDefinition = protocoltypes.ToolDefinition + ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition + ExtraContent = protocoltypes.ExtraContent + GoogleExtra = protocoltypes.GoogleExtra +) type Provider struct { apiKey string @@ -60,14 +62,20 @@ func NewProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string } } -func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { +func (p *Provider) Chat( + ctx context.Context, + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, +) (*LLMResponse, error) { if p.apiBase == "" { return nil, fmt.Errorf("API base not configured") } model = normalizeModel(model, p.apiBase) - requestBody := map[string]interface{}{ + requestBody := map[string]any{ "model": model, "messages": messages, } @@ -83,7 +91,8 @@ func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDef if fieldName == "" { // Fallback: detect from model name for backward compatibility lowerModel := strings.ToLower(model) - if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") || strings.Contains(lowerModel, "gpt-5") { + if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") || + strings.Contains(lowerModel, "gpt-5") { fieldName = "max_completion_tokens" } else { fieldName = "max_tokens" @@ -173,7 +182,7 @@ func parseResponse(body []byte) (*LLMResponse, error) { choice := apiResponse.Choices[0] toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls)) for _, tc := range choice.Message.ToolCalls { - arguments := make(map[string]interface{}) + arguments := make(map[string]any) name := "" // Extract thought_signature from Gemini/Google-specific extra content @@ -238,7 +247,7 @@ func normalizeModel(model, apiBase string) string { } } -func asInt(v interface{}) (int, bool) { +func asInt(v any) (int, bool) { switch val := v.(type) { case int: return val, true @@ -253,7 +262,7 @@ func asInt(v interface{}) (int, bool) { } } -func asFloat(v interface{}) (float64, bool) { +func asFloat(v any) (float64, bool) { switch val := v.(type) { case float64: return val, true diff --git a/pkg/providers/protocoltypes/types.go b/pkg/providers/protocoltypes/types.go index b7e7062b9..3a089ca47 100644 --- a/pkg/providers/protocoltypes/types.go +++ b/pkg/providers/protocoltypes/types.go @@ -1,13 +1,13 @@ package protocoltypes type ToolCall struct { - ID string `json:"id"` - Type string `json:"type,omitempty"` - Function *FunctionCall `json:"function,omitempty"` - Name string `json:"name,omitempty"` - Arguments map[string]interface{} `json:"arguments,omitempty"` - ThoughtSignature string `json:"-"` // Internal use only - ExtraContent *ExtraContent `json:"extra_content,omitempty"` + ID string `json:"id"` + Type string `json:"type,omitempty"` + Function *FunctionCall `json:"function,omitempty"` + Name string `json:"name,omitempty"` + Arguments map[string]any `json:"arguments,omitempty"` + ThoughtSignature string `json:"-"` // Internal use only + ExtraContent *ExtraContent `json:"extra_content,omitempty"` } type ExtraContent struct { @@ -50,7 +50,7 @@ type ToolDefinition struct { } type ToolFunctionDefinition struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters"` + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]any `json:"parameters"` } diff --git a/pkg/providers/toolcall_utils.go b/pkg/providers/toolcall_utils.go index c7c35ef42..49218b1b1 100644 --- a/pkg/providers/toolcall_utils.go +++ b/pkg/providers/toolcall_utils.go @@ -20,12 +20,12 @@ func NormalizeToolCall(tc ToolCall) ToolCall { // Ensure Arguments is not nil if normalized.Arguments == nil { - normalized.Arguments = map[string]interface{}{} + normalized.Arguments = map[string]any{} } // Parse Arguments from Function.Arguments if not already set if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" { - var parsed map[string]interface{} + var parsed map[string]any if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil { normalized.Arguments = parsed } diff --git a/pkg/providers/types.go b/pkg/providers/types.go index e783e6348..f711e7803 100644 --- a/pkg/providers/types.go +++ b/pkg/providers/types.go @@ -7,18 +7,26 @@ import ( "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) -type ToolCall = protocoltypes.ToolCall -type FunctionCall = protocoltypes.FunctionCall -type LLMResponse = protocoltypes.LLMResponse -type UsageInfo = protocoltypes.UsageInfo -type Message = protocoltypes.Message -type ToolDefinition = protocoltypes.ToolDefinition -type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition -type ExtraContent = protocoltypes.ExtraContent -type GoogleExtra = protocoltypes.GoogleExtra +type ( + ToolCall = protocoltypes.ToolCall + FunctionCall = protocoltypes.FunctionCall + LLMResponse = protocoltypes.LLMResponse + UsageInfo = protocoltypes.UsageInfo + Message = protocoltypes.Message + ToolDefinition = protocoltypes.ToolDefinition + ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition + ExtraContent = protocoltypes.ExtraContent + GoogleExtra = protocoltypes.GoogleExtra +) type LLMProvider interface { - Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) + Chat( + ctx context.Context, + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, + ) (*LLMResponse, error) GetDefaultModel() string } diff --git a/pkg/skills/clawhub_registry.go b/pkg/skills/clawhub_registry.go index e2a940afd..f78197bbe 100644 --- a/pkg/skills/clawhub_registry.go +++ b/pkg/skills/clawhub_registry.go @@ -214,7 +214,10 @@ func (c *ClawHubRegistry) GetSkillMeta(ctx context.Context, slug string) (*Skill // DownloadAndInstall fetches metadata (with fallback), resolves version, // downloads the skill ZIP, and extracts it to targetDir. // Returns an InstallResult for the caller to use for moderation decisions. -func (c *ClawHubRegistry) DownloadAndInstall(ctx context.Context, slug, version, targetDir string) (*InstallResult, error) { +func (c *ClawHubRegistry) DownloadAndInstall( + ctx context.Context, + slug, version, targetDir string, +) (*InstallResult, error) { if err := utils.ValidateSkillIdentifier(slug); err != nil { return nil, fmt.Errorf("invalid slug %q: error: %s", slug, err.Error()) } diff --git a/pkg/skills/clawhub_registry_test.go b/pkg/skills/clawhub_registry_test.go index d12e19504..65ee638da 100644 --- a/pkg/skills/clawhub_registry_test.go +++ b/pkg/skills/clawhub_registry_test.go @@ -11,9 +11,10 @@ import ( "path/filepath" "testing" - "github.com/sipeed/picoclaw/pkg/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/sipeed/picoclaw/pkg/utils" ) func newTestRegistry(serverURL, authToken string) *ClawHubRegistry { @@ -162,7 +163,7 @@ func TestExtractZipPathTraversal(t *testing.T) { // Write to temp file for extractZipFile. tmpZip := filepath.Join(t.TempDir(), "bad.zip") - require.NoError(t, os.WriteFile(tmpZip, buf.Bytes(), 0644)) + require.NoError(t, os.WriteFile(tmpZip, buf.Bytes(), 0o644)) tmpDir := t.TempDir() err = utils.ExtractZipFile(tmpZip, tmpDir) @@ -179,7 +180,7 @@ func TestExtractZipWithSubdirectories(t *testing.T) { // Write to temp file for extractZipFile. tmpZip := filepath.Join(t.TempDir(), "test.zip") - require.NoError(t, os.WriteFile(tmpZip, zipBuf, 0644)) + require.NoError(t, os.WriteFile(tmpZip, zipBuf, 0o644)) tmpDir := t.TempDir() targetDir := filepath.Join(tmpDir, "my-skill") diff --git a/pkg/skills/registry_test.go b/pkg/skills/registry_test.go index daecd5a59..a4694bd43 100644 --- a/pkg/skills/registry_test.go +++ b/pkg/skills/registry_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/utils" "github.com/stretchr/testify/assert" + + "github.com/sipeed/picoclaw/pkg/utils" ) // mockRegistry is a test double implementing SkillRegistry. diff --git a/pkg/tools/shell_timeout_unix_test.go b/pkg/tools/shell_timeout_unix_test.go index 4c6388b9b..04ef8e441 100644 --- a/pkg/tools/shell_timeout_unix_test.go +++ b/pkg/tools/shell_timeout_unix_test.go @@ -25,7 +25,7 @@ func TestShellTool_TimeoutKillsChildProcess(t *testing.T) { tool := NewExecTool(t.TempDir(), false) tool.SetTimeout(500 * time.Millisecond) - args := map[string]interface{}{ + args := map[string]any{ // Spawn a child process that would outlive the shell unless process-group kill is used. "command": "sleep 60 & echo $! > child.pid; wait", } diff --git a/pkg/tools/skills_install.go b/pkg/tools/skills_install.go index 6b05918ce..55c0b678d 100644 --- a/pkg/tools/skills_install.go +++ b/pkg/tools/skills_install.go @@ -42,23 +42,23 @@ func (t *InstallSkillTool) Description() string { return "Install a skill from a registry by slug. Downloads and extracts the skill into the workspace. Use find_skills first to discover available skills." } -func (t *InstallSkillTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *InstallSkillTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "slug": map[string]interface{}{ + "properties": map[string]any{ + "slug": map[string]any{ "type": "string", "description": "The unique slug of the skill to install (e.g., 'github', 'docker-compose')", }, - "version": map[string]interface{}{ + "version": map[string]any{ "type": "string", "description": "Specific version to install (optional, defaults to latest)", }, - "registry": map[string]interface{}{ + "registry": map[string]any{ "type": "string", "description": "Registry to install from (required, e.g., 'clawhub')", }, - "force": map[string]interface{}{ + "force": map[string]any{ "type": "boolean", "description": "Force reinstall if skill already exists (default false)", }, @@ -67,7 +67,7 @@ func (t *InstallSkillTool) Parameters() map[string]interface{} { } } -func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { +func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]any) *ToolResult { // Install lock to prevent concurrent directory operations. // Ideally this should be done at a `slug` level, currently, its at a `workspace` level. t.mu.Lock() @@ -94,7 +94,9 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac if !force { if _, err := os.Stat(targetDir); err == nil { - return ErrorResult(fmt.Sprintf("skill %q already installed at %s. Use force=true to reinstall.", slug, targetDir)) + return ErrorResult( + fmt.Sprintf("skill %q already installed at %s. Use force=true to reinstall.", slug, targetDir), + ) } } else { // Force: remove existing if present. @@ -108,7 +110,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac } // Ensure skills directory exists. - if err := os.MkdirAll(skillsDir, 0755); err != nil { + if err := os.MkdirAll(skillsDir, 0o755); err != nil { return ErrorResult(fmt.Sprintf("failed to create skills directory: %v", err)) } @@ -119,7 +121,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac rmErr := os.RemoveAll(targetDir) if rmErr != nil { logger.ErrorCF("tool", "Failed to remove partial install", - map[string]interface{}{ + map[string]any{ "tool": "install_skill", "target_dir": targetDir, "error": rmErr.Error(), @@ -133,7 +135,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac rmErr := os.RemoveAll(targetDir) if rmErr != nil { logger.ErrorCF("tool", "Failed to remove partial install", - map[string]interface{}{ + map[string]any{ "tool": "install_skill", "target_dir": targetDir, "error": rmErr.Error(), @@ -145,7 +147,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac // Write origin metadata. if err := writeOriginMeta(targetDir, registry.Name(), slug, result.Version); err != nil { logger.ErrorCF("tool", "Failed to write origin metadata", - map[string]interface{}{ + map[string]any{ "tool": "install_skill", "error": err.Error(), "target": targetDir, @@ -195,5 +197,5 @@ func writeOriginMeta(targetDir, registryName, slug, version string) error { return err } - return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0644) + return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0o644) } diff --git a/pkg/tools/skills_install_test.go b/pkg/tools/skills_install_test.go index e6941a950..676fcecc0 100644 --- a/pkg/tools/skills_install_test.go +++ b/pkg/tools/skills_install_test.go @@ -6,9 +6,10 @@ import ( "path/filepath" "testing" - "github.com/sipeed/picoclaw/pkg/skills" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/sipeed/picoclaw/pkg/skills" ) func TestInstallSkillToolName(t *testing.T) { @@ -18,14 +19,14 @@ func TestInstallSkillToolName(t *testing.T) { func TestInstallSkillToolMissingSlug(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) - result := tool.Execute(context.Background(), map[string]interface{}{}) + result := tool.Execute(context.Background(), map[string]any{}) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string") } func TestInstallSkillToolEmptySlug(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "slug": " ", }) assert.True(t, result.IsError) @@ -42,7 +43,7 @@ func TestInstallSkillToolUnsafeSlug(t *testing.T) { } for _, slug := range cases { - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "slug": slug, }) assert.True(t, result.IsError, "slug %q should be rejected", slug) @@ -53,10 +54,10 @@ func TestInstallSkillToolUnsafeSlug(t *testing.T) { func TestInstallSkillToolAlreadyExists(t *testing.T) { workspace := t.TempDir() skillDir := filepath.Join(workspace, "skills", "existing-skill") - require.NoError(t, os.MkdirAll(skillDir, 0755)) + require.NoError(t, os.MkdirAll(skillDir, 0o755)) tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "slug": "existing-skill", "registry": "clawhub", }) @@ -67,7 +68,7 @@ func TestInstallSkillToolAlreadyExists(t *testing.T) { func TestInstallSkillToolRegistryNotFound(t *testing.T) { workspace := t.TempDir() tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "slug": "some-skill", "registry": "nonexistent", }) @@ -80,7 +81,7 @@ func TestInstallSkillToolParameters(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) params := tool.Parameters() - props, ok := params["properties"].(map[string]interface{}) + props, ok := params["properties"].(map[string]any) assert.True(t, ok) assert.Contains(t, props, "slug") assert.Contains(t, props, "version") @@ -95,7 +96,7 @@ func TestInstallSkillToolParameters(t *testing.T) { func TestInstallSkillToolMissingRegistry(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "slug": "some-skill", }) assert.True(t, result.IsError) diff --git a/pkg/tools/skills_search.go b/pkg/tools/skills_search.go index b12949ec2..2b6cffd38 100644 --- a/pkg/tools/skills_search.go +++ b/pkg/tools/skills_search.go @@ -32,15 +32,15 @@ func (t *FindSkillsTool) Description() string { return "Search for installable skills from skill registries. Returns skill slugs, descriptions, versions, and relevance scores. Use this to discover skills before installing them with install_skill." } -func (t *FindSkillsTool) Parameters() map[string]interface{} { - return map[string]interface{}{ +func (t *FindSkillsTool) Parameters() map[string]any { + return map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ + "properties": map[string]any{ + "query": map[string]any{ "type": "string", "description": "Search query describing the desired skill capability (e.g., 'github integration', 'database management')", }, - "limit": map[string]interface{}{ + "limit": map[string]any{ "type": "integer", "description": "Maximum number of results to return (1-20, default 5)", "minimum": 1.0, @@ -51,7 +51,7 @@ func (t *FindSkillsTool) Parameters() map[string]interface{} { } } -func (t *FindSkillsTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { +func (t *FindSkillsTool) Execute(ctx context.Context, args map[string]any) *ToolResult { query, ok := args["query"].(string) query = strings.ToLower(strings.TrimSpace(query)) if !ok || query == "" { diff --git a/pkg/tools/skills_search_test.go b/pkg/tools/skills_search_test.go index 7e07b2775..0e5387cf5 100644 --- a/pkg/tools/skills_search_test.go +++ b/pkg/tools/skills_search_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/skills" "github.com/stretchr/testify/assert" + + "github.com/sipeed/picoclaw/pkg/skills" ) func TestFindSkillsToolName(t *testing.T) { @@ -15,14 +16,14 @@ func TestFindSkillsToolName(t *testing.T) { func TestFindSkillsToolMissingQuery(t *testing.T) { tool := NewFindSkillsTool(skills.NewRegistryManager(), nil) - result := tool.Execute(context.Background(), map[string]interface{}{}) + result := tool.Execute(context.Background(), map[string]any{}) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "query is required") } func TestFindSkillsToolEmptyQuery(t *testing.T) { tool := NewFindSkillsTool(skills.NewRegistryManager(), nil) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "query": " ", }) assert.True(t, result.IsError) @@ -35,7 +36,7 @@ func TestFindSkillsToolCacheHit(t *testing.T) { }) tool := NewFindSkillsTool(skills.NewRegistryManager(), cache) - result := tool.Execute(context.Background(), map[string]interface{}{ + result := tool.Execute(context.Background(), map[string]any{ "query": "github", }) @@ -48,7 +49,7 @@ func TestFindSkillsToolParameters(t *testing.T) { tool := NewFindSkillsTool(skills.NewRegistryManager(), nil) params := tool.Parameters() - props, ok := params["properties"].(map[string]interface{}) + props, ok := params["properties"].(map[string]any) assert.True(t, ok) assert.Contains(t, props, "query") assert.Contains(t, props, "limit") @@ -71,7 +72,14 @@ func TestFormatSearchResultsEmpty(t *testing.T) { func TestFormatSearchResultsWithData(t *testing.T) { results := []skills.SearchResult{ - {Slug: "github", Score: 0.95, DisplayName: "GitHub", Summary: "GitHub API integration", Version: "1.0.0", RegistryName: "clawhub"}, + { + Slug: "github", + Score: 0.95, + DisplayName: "GitHub", + Summary: "GitHub API integration", + Version: "1.0.0", + RegistryName: "clawhub", + }, } output := formatSearchResults("github", results, false) assert.Contains(t, output, "github") diff --git a/pkg/utils/download.go b/pkg/utils/download.go index 9fa7fbfa7..5d9a13a30 100644 --- a/pkg/utils/download.go +++ b/pkg/utils/download.go @@ -27,7 +27,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, // Attach context. req = req.WithContext(ctx) - logger.DebugCF("download", "Starting download", map[string]interface{}{ + logger.DebugCF("download", "Starting download", map[string]any{ "url": req.URL.String(), "max_bytes": maxBytes, }) @@ -52,7 +52,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, } tmpPath := tmpFile.Name() - logger.DebugCF("download", "Streaming to temp file", map[string]interface{}{ + logger.DebugCF("download", "Streaming to temp file", map[string]any{ "path": tmpPath, }) @@ -84,7 +84,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, return "", fmt.Errorf("failed to close temp file: %w", err) } - logger.DebugCF("download", "Download complete", map[string]interface{}{ + logger.DebugCF("download", "Download complete", map[string]any{ "path": tmpPath, "bytes_written": written, }) diff --git a/pkg/utils/zip.go b/pkg/utils/zip.go index cad91e420..919ce5a20 100644 --- a/pkg/utils/zip.go +++ b/pkg/utils/zip.go @@ -22,13 +22,13 @@ func ExtractZipFile(zipPath string, targetDir string) error { } defer reader.Close() - logger.DebugCF("zip", "Extracting ZIP", map[string]interface{}{ + logger.DebugCF("zip", "Extracting ZIP", map[string]any{ "zip_path": zipPath, "target_dir": targetDir, "entries": len(reader.File), }) - if err := os.MkdirAll(targetDir, 0755); err != nil { + if err := os.MkdirAll(targetDir, 0o755); err != nil { return fmt.Errorf("failed to create target dir: %w", err) } @@ -43,7 +43,8 @@ func ExtractZipFile(zipPath string, targetDir string) error { // Double-check the resolved path is within target directory (defense-in-depth). targetDirClean := filepath.Clean(targetDir) - if !strings.HasPrefix(filepath.Clean(destPath), targetDirClean+string(filepath.Separator)) && filepath.Clean(destPath) != targetDirClean { + if !strings.HasPrefix(filepath.Clean(destPath), targetDirClean+string(filepath.Separator)) && + filepath.Clean(destPath) != targetDirClean { return fmt.Errorf("zip entry escapes target dir: %q", f.Name) } @@ -55,14 +56,14 @@ func ExtractZipFile(zipPath string, targetDir string) error { } if f.FileInfo().IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { + if err := os.MkdirAll(destPath, 0o755); err != nil { return err } continue } // Ensure parent directory exists. - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { return err } @@ -98,7 +99,7 @@ func extractSingleFile(f *zip.File, destPath string) error { defer func() { if cerr := outFile.Close(); cerr != nil { _ = os.Remove(destPath) - logger.ErrorCF("zip", "Failed to close file", map[string]interface{}{ + logger.ErrorCF("zip", "Failed to close file", map[string]any{ "dest_path": destPath, "error": cerr.Error(), })