feat(fmt): Fix formatting
This commit is contained in:
parent
ad8c2d48c8
commit
0675ce7c38
36 changed files with 731 additions and 495 deletions
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/agent"
|
"github.com/sipeed/picoclaw/pkg/agent"
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
|
@ -74,10 +75,10 @@ func agentCmd() {
|
||||||
// Print agent startup info (only for interactive mode)
|
// Print agent startup info (only for interactive mode)
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tools_count": startupInfo["tools"].(map[string]interface{})["count"],
|
"tools_count": startupInfo["tools"].(map[string]any)["count"],
|
||||||
"skills_total": startupInfo["skills"].(map[string]interface{})["total"],
|
"skills_total": startupInfo["skills"].(map[string]any)["total"],
|
||||||
"skills_available": startupInfo["skills"].(map[string]interface{})["available"],
|
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if message != "" {
|
if message != "" {
|
||||||
|
|
@ -104,7 +105,6 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
InterruptPrompt: "^C",
|
InterruptPrompt: "^C",
|
||||||
EOFPrompt: "exit",
|
EOFPrompt: "exit",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error initializing readline: %v\n", err)
|
fmt.Printf("Error initializing readline: %v\n", err)
|
||||||
fmt.Println("Falling back to simple input mode...")
|
fmt.Println("Falling back to simple input mode...")
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ func gatewayCmd() {
|
||||||
// Print agent startup info
|
// Print agent startup info
|
||||||
fmt.Println("\n📦 Agent Status:")
|
fmt.Println("\n📦 Agent Status:")
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
toolsInfo := startupInfo["tools"].(map[string]interface{})
|
toolsInfo := startupInfo["tools"].(map[string]any)
|
||||||
skillsInfo := startupInfo["skills"].(map[string]interface{})
|
skillsInfo := startupInfo["skills"].(map[string]any)
|
||||||
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
|
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
|
||||||
fmt.Printf(" • Skills: %d/%d available\n",
|
fmt.Printf(" • Skills: %d/%d available\n",
|
||||||
skillsInfo["available"],
|
skillsInfo["available"],
|
||||||
|
|
@ -69,7 +69,7 @@ func gatewayCmd() {
|
||||||
|
|
||||||
// Log to file as well
|
// Log to file as well
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tools_count": toolsInfo["count"],
|
"tools_count": toolsInfo["count"],
|
||||||
"skills_total": skillsInfo["total"],
|
"skills_total": skillsInfo["total"],
|
||||||
"skills_available": skillsInfo["available"],
|
"skills_available": skillsInfo["available"],
|
||||||
|
|
@ -77,7 +77,14 @@ func gatewayCmd() {
|
||||||
|
|
||||||
// Setup cron tool and service
|
// Setup cron tool and service
|
||||||
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
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(
|
heartbeatService := heartbeat.NewHeartbeatService(
|
||||||
cfg.WorkspacePath(),
|
cfg.WorkspacePath(),
|
||||||
|
|
@ -181,7 +188,7 @@ func gatewayCmd() {
|
||||||
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
go func() {
|
go func() {
|
||||||
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
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)
|
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")
|
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")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
// Create cron service
|
// Create cron service
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func onboard() {
|
||||||
|
|
||||||
func copyEmbeddedToTarget(targetDir string) error {
|
func copyEmbeddedToTarget(targetDir string) error {
|
||||||
// Ensure target directory exists
|
// 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)
|
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)
|
targetPath := filepath.Join(targetDir, new_path)
|
||||||
|
|
||||||
// Ensure target file's directory exists
|
// 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)
|
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file
|
// 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)
|
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
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)
|
fmt.Printf("\u2717 Failed to create skills directory: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +193,7 @@ func skillsInstallBuiltinCmd(workspace string) {
|
||||||
continue
|
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)
|
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,9 @@ func (cb *ContextBuilder) buildToolsSection() string {
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Available Tools\n\n")
|
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")
|
sb.WriteString("You have access to the following tools:\n\n")
|
||||||
for _, s := range summaries {
|
for _, s := range summaries {
|
||||||
sb.WriteString(s)
|
sb.WriteString(s)
|
||||||
|
|
@ -157,7 +159,13 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
return sb.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{}
|
messages := []providers.Message{}
|
||||||
|
|
||||||
systemPrompt := cb.BuildSystemPrompt()
|
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)
|
// Log system prompt summary for debugging (debug mode only)
|
||||||
logger.DebugCF("agent", "System prompt built",
|
logger.DebugCF("agent", "System prompt built",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"total_chars": len(systemPrompt),
|
"total_chars": len(systemPrompt),
|
||||||
"total_lines": strings.Count(systemPrompt, "\n") + 1,
|
"total_lines": strings.Count(systemPrompt, "\n") + 1,
|
||||||
"section_count": strings.Count(systemPrompt, "\n\n---\n\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)"
|
preview = preview[:500] + "... (truncated)"
|
||||||
}
|
}
|
||||||
logger.DebugCF("agent", "System prompt preview",
|
logger.DebugCF("agent", "System prompt preview",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"preview": preview,
|
"preview": preview,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -218,12 +226,12 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
||||||
switch msg.Role {
|
switch msg.Role {
|
||||||
case "tool":
|
case "tool":
|
||||||
if len(sanitized) == 0 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
last := sanitized[len(sanitized)-1]
|
last := sanitized[len(sanitized)-1]
|
||||||
if last.Role != "assistant" || len(last.ToolCalls) == 0 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
sanitized = append(sanitized, msg)
|
sanitized = append(sanitized, msg)
|
||||||
|
|
@ -231,12 +239,16 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
||||||
case "assistant":
|
case "assistant":
|
||||||
if len(msg.ToolCalls) > 0 {
|
if len(msg.ToolCalls) > 0 {
|
||||||
if len(sanitized) == 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
|
continue
|
||||||
}
|
}
|
||||||
prev := sanitized[len(sanitized)-1]
|
prev := sanitized[len(sanitized)-1]
|
||||||
if prev.Role != "user" && prev.Role != "tool" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +262,10 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
||||||
return sanitized
|
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{
|
messages = append(messages, providers.Message{
|
||||||
Role: "tool",
|
Role: "tool",
|
||||||
Content: result,
|
Content: result,
|
||||||
|
|
@ -259,7 +274,11 @@ func (cb *ContextBuilder) AddToolResult(messages []providers.Message, toolCallID
|
||||||
return messages
|
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{
|
msg := providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: content,
|
Content: content,
|
||||||
|
|
@ -289,13 +308,13 @@ func (cb *ContextBuilder) loadSkills() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSkillsInfo returns information about loaded skills.
|
// GetSkillsInfo returns information about loaded skills.
|
||||||
func (cb *ContextBuilder) GetSkillsInfo() map[string]interface{} {
|
func (cb *ContextBuilder) GetSkillsInfo() map[string]any {
|
||||||
allSkills := cb.skillsLoader.ListSkills()
|
allSkills := cb.skillsLoader.ListSkills()
|
||||||
skillNames := make([]string, 0, len(allSkills))
|
skillNames := make([]string, 0, len(allSkills))
|
||||||
for _, s := range allSkills {
|
for _, s := range allSkills {
|
||||||
skillNames = append(skillNames, s.Name)
|
skillNames = append(skillNames, s.Name)
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"total": len(allSkills),
|
"total": len(allSkills),
|
||||||
"available": len(allSkills),
|
"available": len(allSkills),
|
||||||
"names": skillNames,
|
"names": skillNames,
|
||||||
|
|
|
||||||
|
|
@ -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).
|
// 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() {
|
for _, agentID := range registry.ListAgentIDs() {
|
||||||
agent, ok := registry.GetAgent(agentID)
|
agent, ok := registry.GetAgent(agentID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -123,7 +128,10 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A
|
||||||
MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches,
|
MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches,
|
||||||
ClawHub: skills.ClawHubConfig(cfg.Tools.Skills.Registries.ClawHub),
|
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.NewFindSkillsTool(registryMgr, searchCache))
|
||||||
agent.Tools.Register(tools.NewInstallSkillTool(registryMgr, agent.Workspace))
|
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")
|
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{
|
msg := bus.InboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
SenderID: "cron",
|
SenderID: "cron",
|
||||||
|
|
@ -263,7 +274,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
logContent = utils.Truncate(msg.Content, 80)
|
logContent = utils.Truncate(msg.Content, 80)
|
||||||
}
|
}
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, logContent),
|
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,
|
"channel": msg.Channel,
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
|
|
@ -302,7 +313,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("agent", "Routed message",
|
logger.InfoCF("agent", "Routed message",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"session_key": sessionKey,
|
"session_key": sessionKey,
|
||||||
"matched_by": route.MatchedBy,
|
"matched_by": route.MatchedBy,
|
||||||
|
|
@ -325,7 +336,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoCF("agent", "Processing system message",
|
logger.InfoCF("agent", "Processing system message",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
"chat_id": msg.ChatID,
|
"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
|
// Skip internal channels - only log, don't send to user
|
||||||
if constants.IsInternalChannel(originChannel) {
|
if constants.IsInternalChannel(originChannel) {
|
||||||
logger.InfoCF("agent", "Subagent completed (internal channel)",
|
logger.InfoCF("agent", "Subagent completed (internal channel)",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"sender_id": msg.SenderID,
|
"sender_id": msg.SenderID,
|
||||||
"content_len": len(content),
|
"content_len": len(content),
|
||||||
"channel": originChannel,
|
"channel": originChannel,
|
||||||
|
|
@ -383,7 +394,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
if !constants.IsInternalChannel(opts.Channel) {
|
if !constants.IsInternalChannel(opts.Channel) {
|
||||||
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
|
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
|
||||||
if err := al.RecordLastChannel(channelKey); err != nil {
|
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
|
// 9. Log response
|
||||||
responsePreview := utils.Truncate(finalContent, 120)
|
responsePreview := utils.Truncate(finalContent, 120)
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview),
|
logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview),
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"session_key": opts.SessionKey,
|
"session_key": opts.SessionKey,
|
||||||
"iterations": iteration,
|
"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.
|
// 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
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
|
||||||
|
|
@ -464,7 +480,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
iteration++
|
iteration++
|
||||||
|
|
||||||
logger.DebugCF("agent", "LLM iteration",
|
logger.DebugCF("agent", "LLM iteration",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"max": agent.MaxIterations,
|
"max": agent.MaxIterations,
|
||||||
|
|
@ -475,7 +491,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
// Log LLM request details
|
// Log LLM request details
|
||||||
logger.DebugCF("agent", "LLM request",
|
logger.DebugCF("agent", "LLM request",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"model": agent.Model,
|
"model": agent.Model,
|
||||||
|
|
@ -488,7 +504,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
// Log full messages (detailed)
|
// Log full messages (detailed)
|
||||||
logger.DebugCF("agent", "Full LLM request",
|
logger.DebugCF("agent", "Full LLM request",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"messages_json": formatMessagesForLog(messages),
|
"messages_json": formatMessagesForLog(messages),
|
||||||
"tools_json": formatToolsForLog(providerToolDefs),
|
"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 {
|
if len(agent.Candidates) > 1 && al.fallback != nil {
|
||||||
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
|
||||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
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,
|
"max_tokens": agent.MaxTokens,
|
||||||
"temperature": agent.Temperature,
|
"temperature": agent.Temperature,
|
||||||
})
|
})
|
||||||
|
|
@ -514,11 +530,11 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
if fbResult.Provider != "" && len(fbResult.Attempts) > 0 {
|
if fbResult.Provider != "" && len(fbResult.Attempts) > 0 {
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts",
|
logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts",
|
||||||
fbResult.Provider, fbResult.Model, len(fbResult.Attempts)+1),
|
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 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,
|
"max_tokens": agent.MaxTokens,
|
||||||
"temperature": agent.Temperature,
|
"temperature": agent.Temperature,
|
||||||
})
|
})
|
||||||
|
|
@ -539,7 +555,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
strings.Contains(errMsg, "length")
|
strings.Contains(errMsg, "length")
|
||||||
|
|
||||||
if isContextError && retry < maxRetries {
|
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(),
|
"error": err.Error(),
|
||||||
"retry": retry,
|
"retry": retry,
|
||||||
})
|
})
|
||||||
|
|
@ -566,7 +582,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("agent", "LLM call failed",
|
logger.ErrorCF("agent", "LLM call failed",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
|
@ -578,7 +594,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
if len(response.ToolCalls) == 0 {
|
if len(response.ToolCalls) == 0 {
|
||||||
finalContent = response.Content
|
finalContent = response.Content
|
||||||
logger.InfoCF("agent", "LLM response without tool calls (direct answer)",
|
logger.InfoCF("agent", "LLM response without tool calls (direct answer)",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"content_chars": len(finalContent),
|
"content_chars": len(finalContent),
|
||||||
|
|
@ -597,7 +613,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
toolNames = append(toolNames, tc.Name)
|
toolNames = append(toolNames, tc.Name)
|
||||||
}
|
}
|
||||||
logger.InfoCF("agent", "LLM requested tool calls",
|
logger.InfoCF("agent", "LLM requested tool calls",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"tools": toolNames,
|
"tools": toolNames,
|
||||||
"count": len(normalizedToolCalls),
|
"count": len(normalizedToolCalls),
|
||||||
|
|
@ -641,7 +657,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
|
|
@ -656,14 +672,21 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
// The agent will handle user notification via processSystemMessage
|
// The agent will handle user notification via processSystemMessage
|
||||||
if !result.Silent && result.ForUser != "" {
|
if !result.Silent && result.ForUser != "" {
|
||||||
logger.InfoCF("agent", "Async tool completed, agent will handle notification",
|
logger.InfoCF("agent", "Async tool completed, agent will handle notification",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"content_len": len(result.ForUser),
|
"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
|
// Send ForUser content to user immediately if not Silent
|
||||||
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
||||||
|
|
@ -673,7 +696,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
Content: toolResult.ForUser,
|
Content: toolResult.ForUser,
|
||||||
})
|
})
|
||||||
logger.DebugCF("agent", "Sent tool result to user",
|
logger.DebugCF("agent", "Sent tool result to user",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": tc.Name,
|
"tool": tc.Name,
|
||||||
"content_len": len(toolResult.ForUser),
|
"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
|
// 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
|
// 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 := history[0]
|
||||||
enhancedSystemPrompt.Content = enhancedSystemPrompt.Content + compressionNote
|
enhancedSystemPrompt.Content = enhancedSystemPrompt.Content + compressionNote
|
||||||
newHistory = append(newHistory, enhancedSystemPrompt)
|
newHistory = append(newHistory, enhancedSystemPrompt)
|
||||||
|
|
@ -787,7 +813,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||||
agent.Sessions.SetHistory(sessionKey, newHistory)
|
agent.Sessions.SetHistory(sessionKey, newHistory)
|
||||||
agent.Sessions.Save(sessionKey)
|
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,
|
"session_key": sessionKey,
|
||||||
"dropped_msgs": droppedCount,
|
"dropped_msgs": droppedCount,
|
||||||
"new_count": len(newHistory),
|
"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.
|
// GetStartupInfo returns information about loaded tools and skills for logging.
|
||||||
func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
func (al *AgentLoop) GetStartupInfo() map[string]any {
|
||||||
info := make(map[string]interface{})
|
info := make(map[string]any)
|
||||||
|
|
||||||
agent := al.registry.GetDefaultAgent()
|
agent := al.registry.GetDefaultAgent()
|
||||||
if agent == nil {
|
if agent == nil {
|
||||||
|
|
@ -805,7 +831,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
||||||
|
|
||||||
// Tools info
|
// Tools info
|
||||||
toolsList := agent.Tools.List()
|
toolsList := agent.Tools.List()
|
||||||
info["tools"] = map[string]interface{}{
|
info["tools"] = map[string]any{
|
||||||
"count": len(toolsList),
|
"count": len(toolsList),
|
||||||
"names": toolsList,
|
"names": toolsList,
|
||||||
}
|
}
|
||||||
|
|
@ -814,7 +840,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
||||||
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
|
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
|
||||||
|
|
||||||
// Agents info
|
// Agents info
|
||||||
info["agents"] = map[string]interface{}{
|
info["agents"] = map[string]any{
|
||||||
"count": len(al.registry.ListAgentIDs()),
|
"count": len(al.registry.ListAgentIDs()),
|
||||||
"ids": 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, "")
|
s1, _ := al.summarizeBatch(ctx, agent, part1, "")
|
||||||
s2, _ := al.summarizeBatch(ctx, agent, part2, "")
|
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)
|
mergePrompt := fmt.Sprintf(
|
||||||
resp, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: mergePrompt}}, nil, agent.Model, map[string]interface{}{
|
"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,
|
"max_tokens": 1024,
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
finalSummary = resp.Content
|
finalSummary = resp.Content
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -945,7 +981,12 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// summarizeBatch summarizes a batch of messages.
|
// 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
|
var sb strings.Builder
|
||||||
sb.WriteString("Provide a concise summary of this conversation segment, preserving core context and key points.\n")
|
sb.WriteString("Provide a concise summary of this conversation segment, preserving core context and key points.\n")
|
||||||
if existingSummary != "" {
|
if existingSummary != "" {
|
||||||
|
|
@ -959,10 +1000,16 @@ func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, b
|
||||||
}
|
}
|
||||||
prompt := sb.String()
|
prompt := sb.String()
|
||||||
|
|
||||||
response, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, agent.Model, map[string]interface{}{
|
response, err := agent.Provider.Chat(
|
||||||
|
ctx,
|
||||||
|
[]providers.Message{{Role: "user", Content: prompt}},
|
||||||
|
nil,
|
||||||
|
agent.Model,
|
||||||
|
map[string]any{
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@ func OpenAIOAuthConfig() OAuthProviderConfig {
|
||||||
// Client credentials are the same ones used by OpenCode/pi-ai for Cloud Code Assist access.
|
// Client credentials are the same ones used by OpenCode/pi-ai for Cloud Code Assist access.
|
||||||
func GoogleAntigravityOAuthConfig() OAuthProviderConfig {
|
func GoogleAntigravityOAuthConfig() OAuthProviderConfig {
|
||||||
// These are the same client credentials used by the OpenCode antigravity plugin.
|
// These are the same client credentials used by the OpenCode antigravity plugin.
|
||||||
clientID := decodeBase64("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==")
|
clientID := decodeBase64(
|
||||||
|
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
||||||
|
)
|
||||||
clientSecret := decodeBase64("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=")
|
clientSecret := decodeBase64("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=")
|
||||||
return OAuthProviderConfig{
|
return OAuthProviderConfig{
|
||||||
Issuer: "https://accounts.google.com/o/oauth2/v2",
|
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("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.Printf(
|
||||||
fmt.Println("please complete the login in your local browser and then PASTE the final redirect URL (or just the code) here.")
|
"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)...")
|
fmt.Println("Waiting for authentication (browser or manual paste)...")
|
||||||
|
|
||||||
// Start manual input in a goroutine
|
// Start manual input in a goroutine
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
|
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"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)
|
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,
|
"chat_id": msg.ChatID,
|
||||||
"preview": utils.Truncate(msg.Content, 100),
|
"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
|
// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
|
||||||
// This is called by the Stream SDK when a new message arrives
|
// This is called by the Stream SDK when a new message arrives
|
||||||
// IChatBotMessageHandler is: func(c context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error)
|
// 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
|
// Extract message content from Text field
|
||||||
content := data.Text.Content
|
content := data.Text.Content
|
||||||
if content == "" {
|
if content == "" {
|
||||||
// Try to extract from Content interface{} if Text is empty
|
// 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 {
|
if textContent, ok := contentMap["content"].(string); ok {
|
||||||
content = textContent
|
content = textContent
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +167,7 @@ func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *ch
|
||||||
metadata["peer_id"] = data.ConversationId
|
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_nick": senderNick,
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"preview": utils.Truncate(content, 50),
|
"preview": utils.Truncate(content, 50),
|
||||||
|
|
@ -192,7 +196,6 @@ func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, c
|
||||||
titleBytes,
|
titleBytes,
|
||||||
contentBytes,
|
contentBytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send reply: %w", err)
|
return fmt.Errorf("failed to send reply: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (c *FeishuChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := wsClient.Start(runCtx); err != nil {
|
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(),
|
"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)
|
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,
|
"chat_id": msg.ChatID,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2
|
||||||
metadata["peer_id"] = chatID
|
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,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"preview": utils.Truncate(content, 80),
|
"preview": utils.Truncate(content, 80),
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
// Fetch bot profile to get bot's userId for mention detection
|
// Fetch bot profile to get bot's userId for mention detection
|
||||||
if err := c.fetchBotInfo(); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger.InfoCF("line", "Bot info fetched", map[string]interface{}{
|
logger.InfoCF("line", "Bot info fetched", map[string]any{
|
||||||
"bot_user_id": c.botUserID,
|
"bot_user_id": c.botUserID,
|
||||||
"basic_id": c.botBasicID,
|
"basic_id": c.botBasicID,
|
||||||
"display_name": c.botDisplayName,
|
"display_name": c.botDisplayName,
|
||||||
|
|
@ -100,12 +100,12 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.InfoCF("line", "LINE webhook server listening", map[string]interface{}{
|
logger.InfoCF("line", "LINE webhook server listening", map[string]any{
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
"path": path,
|
"path": path,
|
||||||
})
|
})
|
||||||
if err := c.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +162,7 @@ func (c *LINEChannel) Stop(ctx context.Context) error {
|
||||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := c.httpServer.Shutdown(shutdownCtx); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
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"`
|
Events []lineEvent `json:"events"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &payload); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
|
@ -266,7 +266,7 @@ type lineMentionee struct {
|
||||||
|
|
||||||
func (c *LINEChannel) processEvent(event lineEvent) {
|
func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
if event.Type != "message" {
|
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,
|
"type": event.Type,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -278,7 +278,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
|
|
||||||
var msg lineMessage
|
var msg lineMessage
|
||||||
if err := json.Unmarshal(event.Message, &msg); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -286,7 +286,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
|
|
||||||
// In group chats, only respond when the bot is mentioned
|
// In group chats, only respond when the bot is mentioned
|
||||||
if isGroup && !c.isBotMentioned(msg) {
|
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,
|
"chat_id": chatID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -312,7 +312,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, file := range localFiles {
|
for _, file := range localFiles {
|
||||||
if err := os.Remove(file); err != nil {
|
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,
|
"file": file,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -374,7 +374,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||||
metadata["peer_id"] = senderID
|
metadata["peer_id"] = senderID
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("line", "Received message", map[string]interface{}{
|
logger.DebugCF("line", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
"message_type": msg.Type,
|
"message_type": msg.Type,
|
||||||
|
|
@ -505,7 +505,7 @@ func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
tokenEntry := entry.(replyTokenEntry)
|
tokenEntry := entry.(replyTokenEntry)
|
||||||
if time.Since(tokenEntry.timestamp) < lineReplyTokenMaxAge {
|
if time.Since(tokenEntry.timestamp) < lineReplyTokenMaxAge {
|
||||||
if err := c.sendReply(ctx, tokenEntry.token, msg.Content, quoteToken); err == nil {
|
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,
|
"chat_id": msg.ChatID,
|
||||||
"quoted": quoteToken != "",
|
"quoted": quoteToken != "",
|
||||||
})
|
})
|
||||||
|
|
@ -533,7 +533,7 @@ func buildTextMessage(content, quoteToken string) map[string]string {
|
||||||
|
|
||||||
// sendReply sends a message using the LINE Reply API.
|
// sendReply sends a message using the LINE Reply API.
|
||||||
func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteToken string) error {
|
func (c *LINEChannel) sendReply(ctx context.Context, replyToken, content, quoteToken string) error {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"replyToken": replyToken,
|
"replyToken": replyToken,
|
||||||
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
"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.
|
// sendPush sends a message using the LINE Push API.
|
||||||
func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken string) error {
|
func (c *LINEChannel) sendPush(ctx context.Context, to, content, quoteToken string) error {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"to": to,
|
"to": to,
|
||||||
"messages": []map[string]string{buildTextMessage(content, quoteToken)},
|
"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.
|
// sendLoading sends a loading animation indicator to the chat.
|
||||||
func (c *LINEChannel) sendLoading(chatID string) {
|
func (c *LINEChannel) sendLoading(chatID string) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]any{
|
||||||
"chatId": chatID,
|
"chatId": chatID,
|
||||||
"loadingSeconds": 60,
|
"loadingSeconds": 60,
|
||||||
}
|
}
|
||||||
if err := c.callAPI(c.ctx, lineLoadingEndpoint, payload); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// callAPI makes an authenticated POST request to the LINE API.
|
// 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)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Telegram channel")
|
logger.DebugC("channels", "Attempting to initialize Telegram channel")
|
||||||
telegram, err := NewTelegramChannel(m.config, m.bus)
|
telegram, err := NewTelegramChannel(m.config, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -63,7 +63,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize WhatsApp channel")
|
logger.DebugC("channels", "Attempting to initialize WhatsApp channel")
|
||||||
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus)
|
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,7 +76,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Feishu channel")
|
logger.DebugC("channels", "Attempting to initialize Feishu channel")
|
||||||
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus)
|
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -89,7 +89,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Discord channel")
|
logger.DebugC("channels", "Attempting to initialize Discord channel")
|
||||||
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus)
|
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -102,7 +102,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize MaixCam channel")
|
logger.DebugC("channels", "Attempting to initialize MaixCam channel")
|
||||||
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus)
|
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -115,7 +115,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize QQ channel")
|
logger.DebugC("channels", "Attempting to initialize QQ channel")
|
||||||
qq, err := NewQQChannel(m.config.Channels.QQ, m.bus)
|
qq, err := NewQQChannel(m.config.Channels.QQ, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -128,7 +128,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize DingTalk channel")
|
logger.DebugC("channels", "Attempting to initialize DingTalk channel")
|
||||||
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus)
|
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -141,7 +141,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Slack channel")
|
logger.DebugC("channels", "Attempting to initialize Slack channel")
|
||||||
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus)
|
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -154,7 +154,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize LINE channel")
|
logger.DebugC("channels", "Attempting to initialize LINE channel")
|
||||||
line, err := NewLINEChannel(m.config.Channels.LINE, m.bus)
|
line, err := NewLINEChannel(m.config.Channels.LINE, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -167,7 +167,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize OneBot channel")
|
logger.DebugC("channels", "Attempting to initialize OneBot channel")
|
||||||
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
|
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -180,7 +180,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize WeCom channel")
|
logger.DebugC("channels", "Attempting to initialize WeCom channel")
|
||||||
wecom, err := NewWeComBotChannel(m.config.Channels.WeCom, m.bus)
|
wecom, err := NewWeComBotChannel(m.config.Channels.WeCom, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -193,7 +193,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize WeCom App channel")
|
logger.DebugC("channels", "Attempting to initialize WeCom App channel")
|
||||||
wecomApp, err := NewWeComAppChannel(m.config.Channels.WeComApp, m.bus)
|
wecomApp, err := NewWeComAppChannel(m.config.Channels.WeComApp, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} 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),
|
"enabled_channels": len(m.channels),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -226,11 +226,11 @@ func (m *Manager) StartAll(ctx context.Context) error {
|
||||||
go m.dispatchOutbound(dispatchCtx)
|
go m.dispatchOutbound(dispatchCtx)
|
||||||
|
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
logger.InfoCF("channels", "Starting channel", map[string]interface{}{
|
logger.InfoCF("channels", "Starting channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
})
|
})
|
||||||
if err := channel.Start(ctx); err != nil {
|
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,
|
"channel": name,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -253,11 +253,11 @@ func (m *Manager) StopAll(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
logger.InfoCF("channels", "Stopping channel", map[string]interface{}{
|
logger.InfoCF("channels", "Stopping channel", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
})
|
})
|
||||||
if err := channel.Stop(ctx); err != nil {
|
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,
|
"channel": name,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -292,14 +292,14 @@ func (m *Manager) dispatchOutbound(ctx context.Context) {
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
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,
|
"channel": msg.Channel,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := channel.Send(ctx, msg); err != nil {
|
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,
|
"channel": msg.Channel,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -315,13 +315,13 @@ func (m *Manager) GetChannel(name string) (Channel, bool) {
|
||||||
return channel, ok
|
return channel, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetStatus() map[string]interface{} {
|
func (m *Manager) GetStatus() map[string]any {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
status := make(map[string]interface{})
|
status := make(map[string]any)
|
||||||
for name, channel := range m.channels {
|
for name, channel := range m.channels {
|
||||||
status[name] = map[string]interface{}{
|
status[name] = map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"running": channel.IsRunning(),
|
"running": channel.IsRunning(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.setRunning(true)
|
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,
|
"address": addr,
|
||||||
"path": webhookPath,
|
"path": webhookPath,
|
||||||
})
|
})
|
||||||
|
|
@ -142,7 +142,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error {
|
||||||
// Start server in goroutine
|
// Start server in goroutine
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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(),
|
"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")
|
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,
|
"chat_id": msg.ChatID,
|
||||||
"preview": utils.Truncate(msg.Content, 100),
|
"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
|
// Reference: https://developer.work.weixin.qq.com/document/path/101033
|
||||||
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, "")
|
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, "")
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Decryption failed", http.StatusInternalServerError)
|
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 {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Invalid XML", http.StatusBadRequest)
|
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
|
// Reference: https://developer.work.weixin.qq.com/document/path/101033
|
||||||
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, "")
|
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, "")
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Decryption failed", http.StatusInternalServerError)
|
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)
|
// Parse decrypted JSON message (AIBOT uses JSON format)
|
||||||
var msg WeComBotMessage
|
var msg WeComBotMessage
|
||||||
if err := json.Unmarshal([]byte(decryptedMsg), &msg); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Invalid message format", http.StatusBadRequest)
|
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
|
// processMessage processes the received message
|
||||||
func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessage) {
|
func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessage) {
|
||||||
// Skip unsupported message types
|
// Skip unsupported message types
|
||||||
if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" && msg.MsgType != "mixed" {
|
if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" &&
|
||||||
logger.DebugCF("wecom", "Skipping non-supported message type", map[string]interface{}{
|
msg.MsgType != "mixed" {
|
||||||
|
logger.DebugCF("wecom", "Skipping non-supported message type", map[string]any{
|
||||||
"msg_type": msg.MsgType,
|
"msg_type": msg.MsgType,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -332,7 +333,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag
|
||||||
c.msgMu.Lock()
|
c.msgMu.Lock()
|
||||||
if c.processedMsgs[msgID] {
|
if c.processedMsgs[msgID] {
|
||||||
c.msgMu.Unlock()
|
c.msgMu.Unlock()
|
||||||
logger.DebugCF("wecom", "Skipping duplicate message", map[string]interface{}{
|
logger.DebugCF("wecom", "Skipping duplicate message", map[string]any{
|
||||||
"msg_id": msgID,
|
"msg_id": msgID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -399,7 +400,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag
|
||||||
metadata["sender_id"] = senderID
|
metadata["sender_id"] = senderID
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("wecom", "Received message", map[string]interface{}{
|
logger.DebugCF("wecom", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"msg_type": msg.MsgType,
|
"msg_type": msg.MsgType,
|
||||||
"peer_kind": peerKind,
|
"peer_kind": peerKind,
|
||||||
|
|
@ -468,7 +469,7 @@ func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID, content
|
||||||
|
|
||||||
// handleHealth handles health check requests
|
// handleHealth handles health check requests
|
||||||
func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
status := map[string]interface{}{
|
status := map[string]any{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"running": c.IsRunning(),
|
"running": c.IsRunning(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
// Get initial access token
|
// Get initial access token
|
||||||
if err := c.refreshAccessToken(); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +171,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.setRunning(true)
|
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,
|
"address": addr,
|
||||||
"path": webhookPath,
|
"path": webhookPath,
|
||||||
})
|
})
|
||||||
|
|
@ -179,7 +179,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
|
||||||
// Start server in goroutine
|
// Start server in goroutine
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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(),
|
"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")
|
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,
|
"chat_id": msg.ChatID,
|
||||||
"preview": utils.Truncate(msg.Content, 100),
|
"preview": utils.Truncate(msg.Content, 100),
|
||||||
})
|
})
|
||||||
|
|
@ -231,7 +231,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request)
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
// Log all incoming requests for debugging
|
// 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,
|
"method": r.Method,
|
||||||
"url": r.URL.String(),
|
"url": r.URL.String(),
|
||||||
"path": r.URL.Path,
|
"path": r.URL.Path,
|
||||||
|
|
@ -250,7 +250,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WarnCF("wecom_app", "Method not allowed", map[string]interface{}{
|
logger.WarnCF("wecom_app", "Method not allowed", map[string]any{
|
||||||
"method": r.Method,
|
"method": r.Method,
|
||||||
})
|
})
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
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")
|
nonce := query.Get("nonce")
|
||||||
echostr := query.Get("echostr")
|
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,
|
"msg_signature": msgSignature,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"nonce": nonce,
|
"nonce": nonce,
|
||||||
|
|
@ -280,7 +280,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
if !WeComVerifySignature(c.config.Token, msgSignature, timestamp, nonce, echostr) {
|
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,
|
"token": c.config.Token,
|
||||||
"msg_signature": msgSignature,
|
"msg_signature": msgSignature,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
|
@ -294,13 +294,13 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
|
||||||
|
|
||||||
// Decrypt echostr with CorpID verification
|
// Decrypt echostr with CorpID verification
|
||||||
// For WeCom App (自建应用), receiveid should be corp_id
|
// 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,
|
"encoding_aes_key": c.config.EncodingAESKey,
|
||||||
"corp_id": c.config.CorpID,
|
"corp_id": c.config.CorpID,
|
||||||
})
|
})
|
||||||
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, c.config.CorpID)
|
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, c.config.CorpID)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
"encoding_aes_key": c.config.EncodingAESKey,
|
"encoding_aes_key": c.config.EncodingAESKey,
|
||||||
"corp_id": c.config.CorpID,
|
"corp_id": c.config.CorpID,
|
||||||
|
|
@ -309,7 +309,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]interface{}{
|
logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]any{
|
||||||
"decrypted": decryptedEchoStr,
|
"decrypted": decryptedEchoStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -349,7 +349,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := xml.Unmarshal(body, &encryptedMsg); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Invalid XML", http.StatusBadRequest)
|
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
|
// For WeCom App (自建应用), receiveid should be corp_id
|
||||||
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, c.config.CorpID)
|
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, c.config.CorpID)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Decryption failed", http.StatusInternalServerError)
|
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
|
// Parse decrypted XML message
|
||||||
var msg WeComXMLMessage
|
var msg WeComXMLMessage
|
||||||
if err := xml.Unmarshal([]byte(decryptedMsg), &msg); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
http.Error(w, "Invalid message format", http.StatusBadRequest)
|
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) {
|
func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessage) {
|
||||||
// Skip non-text messages for now (can be extended)
|
// Skip non-text messages for now (can be extended)
|
||||||
if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" {
|
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,
|
"msg_type": msg.MsgType,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -408,7 +408,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag
|
||||||
c.msgMu.Lock()
|
c.msgMu.Lock()
|
||||||
if c.processedMsgs[msgID] {
|
if c.processedMsgs[msgID] {
|
||||||
c.msgMu.Unlock()
|
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,
|
"msg_id": msgID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -441,7 +441,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag
|
||||||
|
|
||||||
content := msg.Content
|
content := msg.Content
|
||||||
|
|
||||||
logger.DebugCF("wecom_app", "Received message", map[string]interface{}{
|
logger.DebugCF("wecom_app", "Received message", map[string]any{
|
||||||
"sender_id": senderID,
|
"sender_id": senderID,
|
||||||
"msg_type": msg.MsgType,
|
"msg_type": msg.MsgType,
|
||||||
"preview": utils.Truncate(content, 50),
|
"preview": utils.Truncate(content, 50),
|
||||||
|
|
@ -462,7 +462,7 @@ func (c *WeComAppChannel) tokenRefreshLoop() {
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if err := c.refreshAccessToken(); err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -628,7 +628,7 @@ func (c *WeComAppChannel) sendMarkdownMessage(ctx context.Context, accessToken,
|
||||||
|
|
||||||
// handleHealth handles health check requests
|
// handleHealth handles health check requests
|
||||||
func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
status := map[string]interface{}{
|
status := map[string]any{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"running": c.IsRunning(),
|
"running": c.IsRunning(),
|
||||||
"has_token": c.getAccessToken() != "",
|
"has_token": c.getAccessToken() != "",
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,11 @@ func TestWeComAppHandleVerification(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignatureApp("test_token", timestamp, nonce, encryptedEchostr)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleVerification(context.Background(), w, req)
|
ch.handleVerification(context.Background(), w, req)
|
||||||
|
|
@ -429,7 +433,11 @@ func TestWeComAppHandleVerification(t *testing.T) {
|
||||||
timestamp := "1234567890"
|
timestamp := "1234567890"
|
||||||
nonce := "test_nonce"
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleVerification(context.Background(), w, req)
|
ch.handleVerification(context.Background(), w, req)
|
||||||
|
|
@ -481,7 +489,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignatureApp("test_token", timestamp, nonce, encrypted)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -510,7 +522,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignatureApp("test_token", timestamp, 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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -532,7 +548,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
|
||||||
timestamp := "1234567890"
|
timestamp := "1234567890"
|
||||||
nonce := "test_nonce"
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -646,7 +666,11 @@ func TestWeComAppHandleWebhook(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignatureApp("test_token", timestamp, nonce, encoded)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleWebhook(w, req)
|
ch.handleWebhook(w, req)
|
||||||
|
|
@ -669,7 +693,11 @@ func TestWeComAppHandleWebhook(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignatureApp("test_token", timestamp, nonce, encryptedWrapper.Encrypt)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleWebhook(w, req)
|
ch.handleWebhook(w, req)
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,11 @@ func TestWeComBotHandleVerification(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, nonce, encryptedEchostr)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleVerification(context.Background(), w, req)
|
ch.handleVerification(context.Background(), w, req)
|
||||||
|
|
@ -388,7 +392,11 @@ func TestWeComBotHandleVerification(t *testing.T) {
|
||||||
timestamp := "1234567890"
|
timestamp := "1234567890"
|
||||||
nonce := "test_nonce"
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleVerification(context.Background(), w, req)
|
ch.handleVerification(context.Background(), w, req)
|
||||||
|
|
@ -437,7 +445,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, nonce, encrypted)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -479,7 +491,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, nonce, encrypted)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -508,7 +524,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, 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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -530,7 +550,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
|
||||||
timestamp := "1234567890"
|
timestamp := "1234567890"
|
||||||
nonce := "test_nonce"
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleMessageCallback(context.Background(), w, req)
|
ch.handleMessageCallback(context.Background(), w, req)
|
||||||
|
|
@ -625,7 +649,11 @@ func TestWeComBotHandleWebhook(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, nonce, encoded)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleWebhook(w, req)
|
ch.handleWebhook(w, req)
|
||||||
|
|
@ -648,7 +676,11 @@ func TestWeComBotHandleWebhook(t *testing.T) {
|
||||||
nonce := "test_nonce"
|
nonce := "test_nonce"
|
||||||
signature := generateSignature("test_token", timestamp, nonce, encryptedWrapper.Encrypt)
|
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()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
ch.handleWebhook(w, req)
|
ch.handleWebhook(w, req)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try []interface{} to handle mixed types
|
// Try []interface{} to handle mixed types
|
||||||
var raw []interface{}
|
var raw []any
|
||||||
if err := json.Unmarshal(data, &raw); err != nil {
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -363,7 +363,7 @@ type ProviderConfig struct {
|
||||||
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
||||||
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
||||||
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
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`
|
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIProviderConfig struct {
|
type OpenAIProviderConfig struct {
|
||||||
|
|
@ -517,11 +517,11 @@ func SaveConfig(path string, cfg *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0600)
|
return os.WriteFile(path, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) WorkspacePath() string {
|
func (c *Config) WorkspacePath() string {
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
|
||||||
Agents: AgentsConfig{
|
Agents: AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
Provider: tt.providerAlias,
|
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{},
|
Providers: ProvidersConfig{},
|
||||||
|
|
@ -382,7 +385,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to fix the model name in config
|
// 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)
|
result := ConvertProvidersToModelList(cfg)
|
||||||
if len(result) != 1 {
|
if len(result) != 1 {
|
||||||
|
|
@ -515,7 +521,11 @@ func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) {
|
||||||
func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
|
func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
|
||||||
result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4.6")
|
result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4.6")
|
||||||
if result != "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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,20 @@ func TestCamelToSnake(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertKeysToSnake(t *testing.T) {
|
func TestConvertKeysToSnake(t *testing.T) {
|
||||||
input := map[string]interface{}{
|
input := map[string]any{
|
||||||
"apiKey": "test-key",
|
"apiKey": "test-key",
|
||||||
"apiBase": "https://example.com",
|
"apiBase": "https://example.com",
|
||||||
"nested": map[string]interface{}{
|
"nested": map[string]any{
|
||||||
"maxTokens": float64(8192),
|
"maxTokens": float64(8192),
|
||||||
"allowFrom": []interface{}{"user1", "user2"},
|
"allowFrom": []any{"user1", "user2"},
|
||||||
"deeperLevel": map[string]interface{}{
|
"deeperLevel": map[string]any{
|
||||||
"clientId": "abc",
|
"clientId": "abc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := convertKeysToSnake(input)
|
result := convertKeysToSnake(input)
|
||||||
m, ok := result.(map[string]interface{})
|
m, ok := result.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected map[string]interface{}")
|
t.Fatal("expected map[string]interface{}")
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ func TestConvertKeysToSnake(t *testing.T) {
|
||||||
t.Error("expected key 'api_base' after conversion")
|
t.Error("expected key 'api_base' after conversion")
|
||||||
}
|
}
|
||||||
|
|
||||||
nested, ok := m["nested"].(map[string]interface{})
|
nested, ok := m["nested"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected nested map")
|
t.Fatal("expected nested map")
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ func TestConvertKeysToSnake(t *testing.T) {
|
||||||
t.Error("expected key 'allow_from' in nested map")
|
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 {
|
if !ok {
|
||||||
t.Fatal("expected deeper_level map")
|
t.Fatal("expected deeper_level map")
|
||||||
}
|
}
|
||||||
|
|
@ -89,15 +89,15 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
|
|
||||||
openclawConfig := map[string]interface{}{
|
openclawConfig := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ant-test123",
|
"apiKey": "sk-ant-test123",
|
||||||
"apiBase": "https://api.anthropic.com",
|
"apiBase": "https://api.anthropic.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"agents": map[string]interface{}{
|
"agents": map[string]any{
|
||||||
"defaults": map[string]interface{}{
|
"defaults": map[string]any{
|
||||||
"maxTokens": float64(4096),
|
"maxTokens": float64(4096),
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +108,7 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
if err := os.WriteFile(configPath, data, 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,11 +117,11 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
t.Fatalf("LoadOpenClawConfig: %v", err)
|
t.Fatalf("LoadOpenClawConfig: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
providers, ok := result["providers"].(map[string]interface{})
|
providers, ok := result["providers"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected providers map")
|
t.Fatal("expected providers map")
|
||||||
}
|
}
|
||||||
anthropic, ok := providers["anthropic"].(map[string]interface{})
|
anthropic, ok := providers["anthropic"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected anthropic map")
|
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"])
|
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 {
|
if !ok {
|
||||||
t.Fatal("expected agents map")
|
t.Fatal("expected agents map")
|
||||||
}
|
}
|
||||||
defaults, ok := agents["defaults"].(map[string]interface{})
|
defaults, ok := agents["defaults"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected defaults map")
|
t.Fatal("expected defaults map")
|
||||||
}
|
}
|
||||||
|
|
@ -144,16 +144,16 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
||||||
|
|
||||||
func TestConvertConfig(t *testing.T) {
|
func TestConvertConfig(t *testing.T) {
|
||||||
t.Run("providers mapping", func(t *testing.T) {
|
t.Run("providers mapping", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"api_key": "sk-ant-test",
|
"api_key": "sk-ant-test",
|
||||||
"api_base": "https://api.anthropic.com",
|
"api_base": "https://api.anthropic.com",
|
||||||
},
|
},
|
||||||
"openrouter": map[string]interface{}{
|
"openrouter": map[string]any{
|
||||||
"api_key": "sk-or-test",
|
"api_key": "sk-or-test",
|
||||||
},
|
},
|
||||||
"groq": map[string]interface{}{
|
"groq": map[string]any{
|
||||||
"api_key": "gsk-test",
|
"api_key": "gsk-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -178,9 +178,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported provider warning", func(t *testing.T) {
|
t.Run("unsupported provider warning", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"unknown_provider": map[string]interface{}{
|
"unknown_provider": map[string]any{
|
||||||
"api_key": "sk-test",
|
"api_key": "sk-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -199,14 +199,14 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("channels mapping", func(t *testing.T) {
|
t.Run("channels mapping", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"telegram": map[string]interface{}{
|
"telegram": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "tg-token-123",
|
"token": "tg-token-123",
|
||||||
"allow_from": []interface{}{"user1"},
|
"allow_from": []any{"user1"},
|
||||||
},
|
},
|
||||||
"discord": map[string]interface{}{
|
"discord": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "disc-token-456",
|
"token": "disc-token-456",
|
||||||
},
|
},
|
||||||
|
|
@ -232,9 +232,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsupported channel warning", func(t *testing.T) {
|
t.Run("unsupported channel warning", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"email": map[string]interface{}{
|
"email": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -253,9 +253,9 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("agent defaults", func(t *testing.T) {
|
t.Run("agent defaults", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"agents": map[string]interface{}{
|
"agents": map[string]any{
|
||||||
"defaults": map[string]interface{}{
|
"defaults": map[string]any{
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
"max_tokens": float64(4096),
|
"max_tokens": float64(4096),
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
|
|
@ -287,7 +287,7 @@ func TestConvertConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty config", func(t *testing.T) {
|
t.Run("empty config", func(t *testing.T) {
|
||||||
data := map[string]interface{}{}
|
data := map[string]any{}
|
||||||
|
|
||||||
cfg, warnings, err := ConvertConfig(data)
|
cfg, warnings, err := ConvertConfig(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -389,9 +389,9 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -420,8 +420,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0644)
|
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -443,8 +443,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0644)
|
os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, true)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -463,8 +463,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
memDir := filepath.Join(srcDir, "memory")
|
memDir := filepath.Join(srcDir, "memory")
|
||||||
os.MkdirAll(memDir, 0755)
|
os.MkdirAll(memDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0644)
|
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -494,8 +494,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
|
||||||
dstDir := t.TempDir()
|
dstDir := t.TempDir()
|
||||||
|
|
||||||
skillDir := filepath.Join(srcDir, "skills", "weather")
|
skillDir := filepath.Join(srcDir, "skills", "weather")
|
||||||
os.MkdirAll(skillDir, 0755)
|
os.MkdirAll(skillDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0644)
|
os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0o644)
|
||||||
|
|
||||||
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -518,7 +518,7 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("finds openclaw.json", func(t *testing.T) {
|
t.Run("finds openclaw.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
os.WriteFile(configPath, []byte("{}"), 0644)
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -532,7 +532,7 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("falls back to config.json", func(t *testing.T) {
|
t.Run("falls back to config.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "config.json")
|
configPath := filepath.Join(tmpDir, "config.json")
|
||||||
os.WriteFile(configPath, []byte("{}"), 0644)
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -546,8 +546,8 @@ func TestFindOpenClawConfig(t *testing.T) {
|
||||||
t.Run("prefers openclaw.json over config.json", func(t *testing.T) {
|
t.Run("prefers openclaw.json over config.json", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
openclawPath := filepath.Join(tmpDir, "openclaw.json")
|
openclawPath := filepath.Join(tmpDir, "openclaw.json")
|
||||||
os.WriteFile(openclawPath, []byte("{}"), 0644)
|
os.WriteFile(openclawPath, []byte("{}"), 0o644)
|
||||||
os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0644)
|
os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0o644)
|
||||||
|
|
||||||
found, err := findOpenClawConfig(tmpDir)
|
found, err := findOpenClawConfig(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -593,19 +593,19 @@ func TestRunDryRun(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "test-key",
|
"apiKey": "test-key",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
|
|
@ -634,33 +634,33 @@ func TestRunFullMigration(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
|
||||||
os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0o644)
|
||||||
|
|
||||||
memDir := filepath.Join(wsDir, "memory")
|
memDir := filepath.Join(wsDir, "memory")
|
||||||
os.MkdirAll(memDir, 0755)
|
os.MkdirAll(memDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0644)
|
os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ant-migrate-test",
|
"apiKey": "sk-ant-migrate-test",
|
||||||
},
|
},
|
||||||
"openrouter": map[string]interface{}{
|
"openrouter": map[string]any{
|
||||||
"apiKey": "sk-or-migrate-test",
|
"apiKey": "sk-or-migrate-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"channels": map[string]interface{}{
|
"channels": map[string]any{
|
||||||
"telegram": map[string]interface{}{
|
"telegram": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "tg-migrate-test",
|
"token": "tg-migrate-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
@ -754,7 +754,7 @@ func TestRunMutuallyExclusiveFlags(t *testing.T) {
|
||||||
func TestBackupFile(t *testing.T) {
|
func TestBackupFile(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
filePath := filepath.Join(tmpDir, "test.md")
|
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 {
|
if err := backupFile(filePath); err != nil {
|
||||||
t.Fatalf("backupFile: %v", err)
|
t.Fatalf("backupFile: %v", err)
|
||||||
|
|
@ -775,7 +775,7 @@ func TestCopyFile(t *testing.T) {
|
||||||
srcPath := filepath.Join(tmpDir, "src.md")
|
srcPath := filepath.Join(tmpDir, "src.md")
|
||||||
dstPath := filepath.Join(tmpDir, "dst.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 {
|
if err := copyFile(srcPath, dstPath); err != nil {
|
||||||
t.Fatalf("copyFile: %v", err)
|
t.Fatalf("copyFile: %v", err)
|
||||||
|
|
@ -795,18 +795,18 @@ func TestRunConfigOnly(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-config-only",
|
"apiKey": "sk-config-only",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
@ -835,18 +835,18 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
||||||
picoClawHome := t.TempDir()
|
picoClawHome := t.TempDir()
|
||||||
|
|
||||||
wsDir := filepath.Join(openclawHome, "workspace")
|
wsDir := filepath.Join(openclawHome, "workspace")
|
||||||
os.MkdirAll(wsDir, 0755)
|
os.MkdirAll(wsDir, 0o755)
|
||||||
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
|
os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
|
||||||
|
|
||||||
configData := map[string]interface{}{
|
configData := map[string]any{
|
||||||
"providers": map[string]interface{}{
|
"providers": map[string]any{
|
||||||
"anthropic": map[string]interface{}{
|
"anthropic": map[string]any{
|
||||||
"apiKey": "sk-ws-only",
|
"apiKey": "sk-ws-only",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(configData)
|
data, _ := json.Marshal(configData)
|
||||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
|
||||||
|
|
||||||
opts := Options{
|
opts := Options{
|
||||||
Force: true,
|
Force: true,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func TestBuildParams_BasicMessage(t *testing.T) {
|
||||||
messages := []Message{
|
messages := []Message{
|
||||||
{Role: "user", Content: "Hello"},
|
{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,
|
"max_tokens": 1024,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -37,7 +37,7 @@ func TestBuildParams_SystemMessage(t *testing.T) {
|
||||||
{Role: "system", Content: "You are helpful"},
|
{Role: "system", Content: "You are helpful"},
|
||||||
{Role: "user", Content: "Hi"},
|
{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 {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -62,13 +62,13 @@ func TestBuildParams_ToolCallMessage(t *testing.T) {
|
||||||
{
|
{
|
||||||
ID: "call_1",
|
ID: "call_1",
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Arguments: map[string]interface{}{"city": "SF"},
|
Arguments: map[string]any{"city": "SF"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
{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 {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -84,17 +84,17 @@ func TestBuildParams_WithTools(t *testing.T) {
|
||||||
Function: ToolFunctionDefinition{
|
Function: ToolFunctionDefinition{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get weather for a city",
|
Description: "Get weather for a city",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"city": map[string]interface{}{"type": "string"},
|
"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 {
|
if err != nil {
|
||||||
t.Fatalf("buildParams() error: %v", err)
|
t.Fatalf("buildParams() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -154,19 +154,19 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "Hello! How can I help you?"},
|
{"type": "text", "text": "Hello! How can I help you?"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 15,
|
"input_tokens": 15,
|
||||||
"output_tokens": 8,
|
"output_tokens": 8,
|
||||||
},
|
},
|
||||||
|
|
@ -178,7 +178,7 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
|
||||||
|
|
||||||
provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token"))
|
provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token"))
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -221,19 +221,19 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "ok"},
|
{"type": "text", "text": "ok"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 1,
|
"input_tokens": 1,
|
||||||
"output_tokens": 1,
|
"output_tokens": 1,
|
||||||
},
|
},
|
||||||
|
|
@ -247,7 +247,13 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
|
||||||
return "refreshed-token", nil
|
return "refreshed-token", nil
|
||||||
}, server.URL)
|
}, 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 {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,13 @@ func NewAntigravityProvider() *AntigravityProvider {
|
||||||
// Chat implements LLMProvider.Chat using the Cloud Code Assist v1internal API.
|
// Chat implements LLMProvider.Chat using the Cloud Code Assist v1internal API.
|
||||||
// The v1internal endpoint wraps the standard Gemini request in an envelope with
|
// The v1internal endpoint wraps the standard Gemini request in an envelope with
|
||||||
// project, model, request, requestType, userAgent, and requestId fields.
|
// 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()
|
accessToken, projectID, err := p.tokenSource()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("antigravity auth: %w", err)
|
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, "google-antigravity/")
|
||||||
model = strings.TrimPrefix(model, "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,
|
"model": model,
|
||||||
"project": projectID,
|
"project": projectID,
|
||||||
"requestId": fmt.Sprintf("agent-%d-%s", time.Now().UnixMilli(), randomString(9)),
|
"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)
|
innerRequest := p.buildRequest(messages, tools, model, options)
|
||||||
|
|
||||||
// Wrap in v1internal envelope (matches pi-ai SDK format)
|
// Wrap in v1internal envelope (matches pi-ai SDK format)
|
||||||
envelope := map[string]interface{}{
|
envelope := map[string]any{
|
||||||
"project": projectID,
|
"project": projectID,
|
||||||
"model": model,
|
"model": model,
|
||||||
"request": innerRequest,
|
"request": innerRequest,
|
||||||
|
|
@ -115,7 +121,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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,
|
"status_code": resp.StatusCode,
|
||||||
"response": string(respBody),
|
"response": string(respBody),
|
||||||
"model": model,
|
"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)
|
// Check for empty response (some models might return valid success but empty text)
|
||||||
if llmResp.Content == "" && len(llmResp.ToolCalls) == 0 {
|
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
|
return llmResp, nil
|
||||||
|
|
@ -168,12 +176,12 @@ type antigravityPart struct {
|
||||||
|
|
||||||
type antigravityFunctionCall struct {
|
type antigravityFunctionCall struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Args map[string]interface{} `json:"args"`
|
Args map[string]any `json:"args"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type antigravityFunctionResponse struct {
|
type antigravityFunctionResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Response map[string]interface{} `json:"response"`
|
Response map[string]any `json:"response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type antigravityTool struct {
|
type antigravityTool struct {
|
||||||
|
|
@ -183,7 +191,7 @@ type antigravityTool struct {
|
||||||
type antigravityFuncDecl struct {
|
type antigravityFuncDecl struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Parameters interface{} `json:"parameters,omitempty"`
|
Parameters any `json:"parameters,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type antigravitySystemPrompt struct {
|
type antigravitySystemPrompt struct {
|
||||||
|
|
@ -195,7 +203,12 @@ type antigravityGenConfig struct {
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
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{}
|
req := antigravityRequest{}
|
||||||
toolCallNames := make(map[string]string)
|
toolCallNames := make(map[string]string)
|
||||||
|
|
||||||
|
|
@ -215,7 +228,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
Parts: []antigravityPart{{
|
Parts: []antigravityPart{{
|
||||||
FunctionResponse: &antigravityFunctionResponse{
|
FunctionResponse: &antigravityFunctionResponse{
|
||||||
Name: toolName,
|
Name: toolName,
|
||||||
Response: map[string]interface{}{
|
Response: map[string]any{
|
||||||
"result": msg.Content,
|
"result": msg.Content,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -237,9 +250,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
for _, tc := range msg.ToolCalls {
|
for _, tc := range msg.ToolCalls {
|
||||||
toolName, toolArgs, thoughtSignature := normalizeStoredToolCall(tc)
|
toolName, toolArgs, thoughtSignature := normalizeStoredToolCall(tc)
|
||||||
if toolName == "" {
|
if toolName == "" {
|
||||||
logger.WarnCF("provider.antigravity", "Skipping tool call with empty name in history", map[string]interface{}{
|
logger.WarnCF(
|
||||||
|
"provider.antigravity",
|
||||||
|
"Skipping tool call with empty name in history",
|
||||||
|
map[string]any{
|
||||||
"tool_call_id": tc.ID,
|
"tool_call_id": tc.ID,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tc.ID != "" {
|
if tc.ID != "" {
|
||||||
|
|
@ -264,7 +281,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
Parts: []antigravityPart{{
|
Parts: []antigravityPart{{
|
||||||
FunctionResponse: &antigravityFunctionResponse{
|
FunctionResponse: &antigravityFunctionResponse{
|
||||||
Name: toolName,
|
Name: toolName,
|
||||||
Response: map[string]interface{}{
|
Response: map[string]any{
|
||||||
"result": msg.Content,
|
"result": msg.Content,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -311,7 +328,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, string) {
|
func normalizeStoredToolCall(tc ToolCall) (string, map[string]any, string) {
|
||||||
name := tc.Name
|
name := tc.Name
|
||||||
args := tc.Arguments
|
args := tc.Arguments
|
||||||
thoughtSignature := ""
|
thoughtSignature := ""
|
||||||
|
|
@ -324,11 +341,11 @@ func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, strin
|
||||||
}
|
}
|
||||||
|
|
||||||
if args == nil {
|
if args == nil {
|
||||||
args = map[string]interface{}{}
|
args = map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
|
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 {
|
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
args = parsed
|
args = parsed
|
||||||
}
|
}
|
||||||
|
|
@ -485,7 +502,10 @@ func (p *AntigravityProvider) parseSSEResponse(body string) (*LLMResponse, error
|
||||||
Function: &FunctionCall{
|
Function: &FunctionCall{
|
||||||
Name: part.FunctionCall.Name,
|
Name: part.FunctionCall.Name,
|
||||||
Arguments: string(argumentsJSON),
|
Arguments: string(argumentsJSON),
|
||||||
ThoughtSignature: extractPartThoughtSignature(part.ThoughtSignature, part.ThoughtSignatureSnake),
|
ThoughtSignature: extractPartThoughtSignature(
|
||||||
|
part.ThoughtSignature,
|
||||||
|
part.ThoughtSignatureSnake,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -556,24 +576,24 @@ var geminiUnsupportedKeywords = map[string]bool{
|
||||||
"maxProperties": true,
|
"maxProperties": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeSchemaForGemini(schema map[string]interface{}) map[string]interface{} {
|
func sanitizeSchemaForGemini(schema map[string]any) map[string]any {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]any)
|
||||||
for k, v := range schema {
|
for k, v := range schema {
|
||||||
if geminiUnsupportedKeywords[k] {
|
if geminiUnsupportedKeywords[k] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Recursively sanitize nested objects
|
// Recursively sanitize nested objects
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
result[k] = sanitizeSchemaForGemini(val)
|
result[k] = sanitizeSchemaForGemini(val)
|
||||||
case []interface{}:
|
case []any:
|
||||||
sanitized := make([]interface{}, len(val))
|
sanitized := make([]any, len(val))
|
||||||
for i, item := range 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)
|
sanitized[i] = sanitizeSchemaForGemini(m)
|
||||||
} else {
|
} else {
|
||||||
sanitized[i] = item
|
sanitized[i] = item
|
||||||
|
|
@ -604,7 +624,9 @@ func createAntigravityTokenSource() func() (string, string, error) {
|
||||||
return "", "", fmt.Errorf("loading auth credentials: %w", err)
|
return "", "", fmt.Errorf("loading auth credentials: %w", err)
|
||||||
}
|
}
|
||||||
if cred == nil {
|
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
|
// Refresh if needed
|
||||||
|
|
@ -625,7 +647,9 @@ func createAntigravityTokenSource() func() (string, string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if cred.IsExpired() {
|
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
|
projectID := cred.ProjectID
|
||||||
|
|
@ -633,7 +657,7 @@ func createAntigravityTokenSource() func() (string, string, error) {
|
||||||
// Try to fetch project ID from API
|
// Try to fetch project ID from API
|
||||||
fetchedID, err := FetchAntigravityProjectID(cred.AccessToken)
|
fetchedID, err := FetchAntigravityProjectID(cred.AccessToken)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
projectID = "rising-fact-p41fc" // Default fallback (same as OpenCode)
|
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.
|
// FetchAntigravityProjectID retrieves the Google Cloud project ID from the loadCodeAssist endpoint.
|
||||||
func FetchAntigravityProjectID(accessToken string) (string, error) {
|
func FetchAntigravityProjectID(accessToken string) (string, error) {
|
||||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
reqBody, _ := json.Marshal(map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"ideType": "IDE_UNSPECIFIED",
|
"ideType": "IDE_UNSPECIFIED",
|
||||||
"platform": "PLATFORM_UNSPECIFIED",
|
"platform": "PLATFORM_UNSPECIFIED",
|
||||||
"pluginType": "GEMINI",
|
"pluginType": "GEMINI",
|
||||||
|
|
@ -695,7 +719,7 @@ func FetchAntigravityProjectID(accessToken string) (string, error) {
|
||||||
|
|
||||||
// FetchAntigravityModels fetches available models from the Cloud Code Assist API.
|
// FetchAntigravityModels fetches available models from the Cloud Code Assist API.
|
||||||
func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) {
|
func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) {
|
||||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
reqBody, _ := json.Marshal(map[string]any{
|
||||||
"project": projectID,
|
"project": projectID,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -717,14 +741,18 @@ func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelIn
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode != http.StatusOK {
|
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 {
|
var result struct {
|
||||||
Models map[string]struct {
|
Models map[string]struct {
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
QuotaInfo struct {
|
QuotaInfo struct {
|
||||||
RemainingFraction interface{} `json:"remainingFraction"`
|
RemainingFraction any `json:"remainingFraction"`
|
||||||
ResetTime string `json:"resetTime"`
|
ResetTime string `json:"resetTime"`
|
||||||
IsExhausted bool `json:"isExhausted"`
|
IsExhausted bool `json:"isExhausted"`
|
||||||
} `json:"quotaInfo"`
|
} `json:"quotaInfo"`
|
||||||
|
|
@ -800,7 +828,7 @@ func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte)
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Details []map[string]interface{} `json:"details"`
|
Details []map[string]any `json:"details"`
|
||||||
} `json:"error"`
|
} `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -813,7 +841,7 @@ func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte)
|
||||||
// Try to extract quota reset info
|
// Try to extract quota reset info
|
||||||
for _, detail := range errResp.Error.Details {
|
for _, detail := range errResp.Error.Details {
|
||||||
if typeVal, ok := detail["@type"].(string); ok && strings.HasSuffix(typeVal, "ErrorInfo") {
|
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 {
|
if delay, ok := metadata["quotaResetDelay"].(string); ok {
|
||||||
return fmt.Errorf("antigravity rate limit exceeded: %s (reset in %s)", msg, delay)
|
return fmt.Errorf("antigravity rate limit exceeded: %s (reset in %s)", msg, delay)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/anthropics/anthropic-sdk-go"
|
"github.com/anthropics/anthropic-sdk-go"
|
||||||
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
|
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
|
||||||
|
|
||||||
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,19 +23,19 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody map[string]interface{}
|
var reqBody map[string]any
|
||||||
json.NewDecoder(r.Body).Decode(&reqBody)
|
json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"id": "msg_test",
|
"id": "msg_test",
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"model": reqBody["model"],
|
"model": reqBody["model"],
|
||||||
"stop_reason": "end_turn",
|
"stop_reason": "end_turn",
|
||||||
"content": []map[string]interface{}{
|
"content": []map[string]any{
|
||||||
{"type": "text", "text": "Hello! How can I help you?"},
|
{"type": "text", "text": "Hello! How can I help you?"},
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]any{
|
||||||
"input_tokens": 15,
|
"input_tokens": 15,
|
||||||
"output_tokens": 8,
|
"output_tokens": 8,
|
||||||
},
|
},
|
||||||
|
|
@ -48,7 +49,7 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
|
||||||
provider := newClaudeProviderWithDelegate(delegate)
|
provider := newClaudeProviderWithDelegate(delegate)
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
return p.delegate.Chat(ctx, messages, tools, model, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,17 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ToolCall = protocoltypes.ToolCall
|
type (
|
||||||
type FunctionCall = protocoltypes.FunctionCall
|
ToolCall = protocoltypes.ToolCall
|
||||||
type LLMResponse = protocoltypes.LLMResponse
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
type UsageInfo = protocoltypes.UsageInfo
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
type Message = protocoltypes.Message
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
type ToolDefinition = protocoltypes.ToolDefinition
|
Message = protocoltypes.Message
|
||||||
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
type ExtraContent = protocoltypes.ExtraContent
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
type GoogleExtra = protocoltypes.GoogleExtra
|
ExtraContent = protocoltypes.ExtraContent
|
||||||
|
GoogleExtra = protocoltypes.GoogleExtra
|
||||||
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
apiKey string
|
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 == "" {
|
if p.apiBase == "" {
|
||||||
return nil, fmt.Errorf("API base not configured")
|
return nil, fmt.Errorf("API base not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
model = normalizeModel(model, p.apiBase)
|
model = normalizeModel(model, p.apiBase)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]any{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +91,8 @@ func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDef
|
||||||
if fieldName == "" {
|
if fieldName == "" {
|
||||||
// Fallback: detect from model name for backward compatibility
|
// Fallback: detect from model name for backward compatibility
|
||||||
lowerModel := strings.ToLower(model)
|
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"
|
fieldName = "max_completion_tokens"
|
||||||
} else {
|
} else {
|
||||||
fieldName = "max_tokens"
|
fieldName = "max_tokens"
|
||||||
|
|
@ -173,7 +182,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
choice := apiResponse.Choices[0]
|
choice := apiResponse.Choices[0]
|
||||||
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
|
||||||
for _, tc := range choice.Message.ToolCalls {
|
for _, tc := range choice.Message.ToolCalls {
|
||||||
arguments := make(map[string]interface{})
|
arguments := make(map[string]any)
|
||||||
name := ""
|
name := ""
|
||||||
|
|
||||||
// Extract thought_signature from Gemini/Google-specific extra content
|
// 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) {
|
switch val := v.(type) {
|
||||||
case int:
|
case int:
|
||||||
return val, true
|
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) {
|
switch val := v.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
return val, true
|
return val, true
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ type ToolCall struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Function *FunctionCall `json:"function,omitempty"`
|
Function *FunctionCall `json:"function,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
Arguments map[string]any `json:"arguments,omitempty"`
|
||||||
ThoughtSignature string `json:"-"` // Internal use only
|
ThoughtSignature string `json:"-"` // Internal use only
|
||||||
ExtraContent *ExtraContent `json:"extra_content,omitempty"`
|
ExtraContent *ExtraContent `json:"extra_content,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -52,5 +52,5 @@ type ToolDefinition struct {
|
||||||
type ToolFunctionDefinition struct {
|
type ToolFunctionDefinition struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Parameters map[string]interface{} `json:"parameters"`
|
Parameters map[string]any `json:"parameters"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ func NormalizeToolCall(tc ToolCall) ToolCall {
|
||||||
|
|
||||||
// Ensure Arguments is not nil
|
// Ensure Arguments is not nil
|
||||||
if normalized.Arguments == nil {
|
if normalized.Arguments == nil {
|
||||||
normalized.Arguments = map[string]interface{}{}
|
normalized.Arguments = map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Arguments from Function.Arguments if not already set
|
// Parse Arguments from Function.Arguments if not already set
|
||||||
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
|
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 {
|
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
normalized.Arguments = parsed
|
normalized.Arguments = parsed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,26 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ToolCall = protocoltypes.ToolCall
|
type (
|
||||||
type FunctionCall = protocoltypes.FunctionCall
|
ToolCall = protocoltypes.ToolCall
|
||||||
type LLMResponse = protocoltypes.LLMResponse
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
type UsageInfo = protocoltypes.UsageInfo
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
type Message = protocoltypes.Message
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
type ToolDefinition = protocoltypes.ToolDefinition
|
Message = protocoltypes.Message
|
||||||
type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
type ExtraContent = protocoltypes.ExtraContent
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
type GoogleExtra = protocoltypes.GoogleExtra
|
ExtraContent = protocoltypes.ExtraContent
|
||||||
|
GoogleExtra = protocoltypes.GoogleExtra
|
||||||
|
)
|
||||||
|
|
||||||
type LLMProvider interface {
|
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
|
GetDefaultModel() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,10 @@ func (c *ClawHubRegistry) GetSkillMeta(ctx context.Context, slug string) (*Skill
|
||||||
// DownloadAndInstall fetches metadata (with fallback), resolves version,
|
// DownloadAndInstall fetches metadata (with fallback), resolves version,
|
||||||
// downloads the skill ZIP, and extracts it to targetDir.
|
// downloads the skill ZIP, and extracts it to targetDir.
|
||||||
// Returns an InstallResult for the caller to use for moderation decisions.
|
// 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 {
|
if err := utils.ValidateSkillIdentifier(slug); err != nil {
|
||||||
return nil, fmt.Errorf("invalid slug %q: error: %s", slug, err.Error())
|
return nil, fmt.Errorf("invalid slug %q: error: %s", slug, err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestRegistry(serverURL, authToken string) *ClawHubRegistry {
|
func newTestRegistry(serverURL, authToken string) *ClawHubRegistry {
|
||||||
|
|
@ -162,7 +163,7 @@ func TestExtractZipPathTraversal(t *testing.T) {
|
||||||
|
|
||||||
// Write to temp file for extractZipFile.
|
// Write to temp file for extractZipFile.
|
||||||
tmpZip := filepath.Join(t.TempDir(), "bad.zip")
|
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()
|
tmpDir := t.TempDir()
|
||||||
err = utils.ExtractZipFile(tmpZip, tmpDir)
|
err = utils.ExtractZipFile(tmpZip, tmpDir)
|
||||||
|
|
@ -179,7 +180,7 @@ func TestExtractZipWithSubdirectories(t *testing.T) {
|
||||||
|
|
||||||
// Write to temp file for extractZipFile.
|
// Write to temp file for extractZipFile.
|
||||||
tmpZip := filepath.Join(t.TempDir(), "test.zip")
|
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()
|
tmpDir := t.TempDir()
|
||||||
targetDir := filepath.Join(tmpDir, "my-skill")
|
targetDir := filepath.Join(tmpDir, "my-skill")
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockRegistry is a test double implementing SkillRegistry.
|
// mockRegistry is a test double implementing SkillRegistry.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func TestShellTool_TimeoutKillsChildProcess(t *testing.T) {
|
||||||
tool := NewExecTool(t.TempDir(), false)
|
tool := NewExecTool(t.TempDir(), false)
|
||||||
tool.SetTimeout(500 * time.Millisecond)
|
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.
|
// Spawn a child process that would outlive the shell unless process-group kill is used.
|
||||||
"command": "sleep 60 & echo $! > child.pid; wait",
|
"command": "sleep 60 & echo $! > child.pid; wait",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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."
|
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{} {
|
func (t *InstallSkillTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"slug": map[string]interface{}{
|
"slug": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The unique slug of the skill to install (e.g., 'github', 'docker-compose')",
|
"description": "The unique slug of the skill to install (e.g., 'github', 'docker-compose')",
|
||||||
},
|
},
|
||||||
"version": map[string]interface{}{
|
"version": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Specific version to install (optional, defaults to latest)",
|
"description": "Specific version to install (optional, defaults to latest)",
|
||||||
},
|
},
|
||||||
"registry": map[string]interface{}{
|
"registry": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Registry to install from (required, e.g., 'clawhub')",
|
"description": "Registry to install from (required, e.g., 'clawhub')",
|
||||||
},
|
},
|
||||||
"force": map[string]interface{}{
|
"force": map[string]any{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Force reinstall if skill already exists (default false)",
|
"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.
|
// Install lock to prevent concurrent directory operations.
|
||||||
// Ideally this should be done at a `slug` level, currently, its at a `workspace` level.
|
// Ideally this should be done at a `slug` level, currently, its at a `workspace` level.
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
|
|
@ -94,7 +94,9 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
if _, err := os.Stat(targetDir); err == nil {
|
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 {
|
} else {
|
||||||
// Force: remove existing if present.
|
// Force: remove existing if present.
|
||||||
|
|
@ -108,7 +110,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure skills directory exists.
|
// 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))
|
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)
|
rmErr := os.RemoveAll(targetDir)
|
||||||
if rmErr != nil {
|
if rmErr != nil {
|
||||||
logger.ErrorCF("tool", "Failed to remove partial install",
|
logger.ErrorCF("tool", "Failed to remove partial install",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": "install_skill",
|
"tool": "install_skill",
|
||||||
"target_dir": targetDir,
|
"target_dir": targetDir,
|
||||||
"error": rmErr.Error(),
|
"error": rmErr.Error(),
|
||||||
|
|
@ -133,7 +135,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
|
||||||
rmErr := os.RemoveAll(targetDir)
|
rmErr := os.RemoveAll(targetDir)
|
||||||
if rmErr != nil {
|
if rmErr != nil {
|
||||||
logger.ErrorCF("tool", "Failed to remove partial install",
|
logger.ErrorCF("tool", "Failed to remove partial install",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": "install_skill",
|
"tool": "install_skill",
|
||||||
"target_dir": targetDir,
|
"target_dir": targetDir,
|
||||||
"error": rmErr.Error(),
|
"error": rmErr.Error(),
|
||||||
|
|
@ -145,7 +147,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
|
||||||
// Write origin metadata.
|
// Write origin metadata.
|
||||||
if err := writeOriginMeta(targetDir, registry.Name(), slug, result.Version); err != nil {
|
if err := writeOriginMeta(targetDir, registry.Name(), slug, result.Version); err != nil {
|
||||||
logger.ErrorCF("tool", "Failed to write origin metadata",
|
logger.ErrorCF("tool", "Failed to write origin metadata",
|
||||||
map[string]interface{}{
|
map[string]any{
|
||||||
"tool": "install_skill",
|
"tool": "install_skill",
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"target": targetDir,
|
"target": targetDir,
|
||||||
|
|
@ -195,5 +197,5 @@ func writeOriginMeta(targetDir, registryName, slug, version string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0644)
|
return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0o644)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInstallSkillToolName(t *testing.T) {
|
func TestInstallSkillToolName(t *testing.T) {
|
||||||
|
|
@ -18,14 +19,14 @@ func TestInstallSkillToolName(t *testing.T) {
|
||||||
|
|
||||||
func TestInstallSkillToolMissingSlug(t *testing.T) {
|
func TestInstallSkillToolMissingSlug(t *testing.T) {
|
||||||
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
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.True(t, result.IsError)
|
||||||
assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string")
|
assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInstallSkillToolEmptySlug(t *testing.T) {
|
func TestInstallSkillToolEmptySlug(t *testing.T) {
|
||||||
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"slug": " ",
|
"slug": " ",
|
||||||
})
|
})
|
||||||
assert.True(t, result.IsError)
|
assert.True(t, result.IsError)
|
||||||
|
|
@ -42,7 +43,7 @@ func TestInstallSkillToolUnsafeSlug(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, slug := range cases {
|
for _, slug := range cases {
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
})
|
})
|
||||||
assert.True(t, result.IsError, "slug %q should be rejected", 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) {
|
func TestInstallSkillToolAlreadyExists(t *testing.T) {
|
||||||
workspace := t.TempDir()
|
workspace := t.TempDir()
|
||||||
skillDir := filepath.Join(workspace, "skills", "existing-skill")
|
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)
|
tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace)
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"slug": "existing-skill",
|
"slug": "existing-skill",
|
||||||
"registry": "clawhub",
|
"registry": "clawhub",
|
||||||
})
|
})
|
||||||
|
|
@ -67,7 +68,7 @@ func TestInstallSkillToolAlreadyExists(t *testing.T) {
|
||||||
func TestInstallSkillToolRegistryNotFound(t *testing.T) {
|
func TestInstallSkillToolRegistryNotFound(t *testing.T) {
|
||||||
workspace := t.TempDir()
|
workspace := t.TempDir()
|
||||||
tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace)
|
tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace)
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"slug": "some-skill",
|
"slug": "some-skill",
|
||||||
"registry": "nonexistent",
|
"registry": "nonexistent",
|
||||||
})
|
})
|
||||||
|
|
@ -80,7 +81,7 @@ func TestInstallSkillToolParameters(t *testing.T) {
|
||||||
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
||||||
params := tool.Parameters()
|
params := tool.Parameters()
|
||||||
|
|
||||||
props, ok := params["properties"].(map[string]interface{})
|
props, ok := params["properties"].(map[string]any)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Contains(t, props, "slug")
|
assert.Contains(t, props, "slug")
|
||||||
assert.Contains(t, props, "version")
|
assert.Contains(t, props, "version")
|
||||||
|
|
@ -95,7 +96,7 @@ func TestInstallSkillToolParameters(t *testing.T) {
|
||||||
|
|
||||||
func TestInstallSkillToolMissingRegistry(t *testing.T) {
|
func TestInstallSkillToolMissingRegistry(t *testing.T) {
|
||||||
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
|
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",
|
"slug": "some-skill",
|
||||||
})
|
})
|
||||||
assert.True(t, result.IsError)
|
assert.True(t, result.IsError)
|
||||||
|
|
|
||||||
|
|
@ -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."
|
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{} {
|
func (t *FindSkillsTool) Parameters() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]any{
|
||||||
"query": map[string]interface{}{
|
"query": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Search query describing the desired skill capability (e.g., 'github integration', 'database management')",
|
"description": "Search query describing the desired skill capability (e.g., 'github integration', 'database management')",
|
||||||
},
|
},
|
||||||
"limit": map[string]interface{}{
|
"limit": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Maximum number of results to return (1-20, default 5)",
|
"description": "Maximum number of results to return (1-20, default 5)",
|
||||||
"minimum": 1.0,
|
"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, ok := args["query"].(string)
|
||||||
query = strings.ToLower(strings.TrimSpace(query))
|
query = strings.ToLower(strings.TrimSpace(query))
|
||||||
if !ok || query == "" {
|
if !ok || query == "" {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFindSkillsToolName(t *testing.T) {
|
func TestFindSkillsToolName(t *testing.T) {
|
||||||
|
|
@ -15,14 +16,14 @@ func TestFindSkillsToolName(t *testing.T) {
|
||||||
|
|
||||||
func TestFindSkillsToolMissingQuery(t *testing.T) {
|
func TestFindSkillsToolMissingQuery(t *testing.T) {
|
||||||
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
|
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.True(t, result.IsError)
|
||||||
assert.Contains(t, result.ForLLM, "query is required")
|
assert.Contains(t, result.ForLLM, "query is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindSkillsToolEmptyQuery(t *testing.T) {
|
func TestFindSkillsToolEmptyQuery(t *testing.T) {
|
||||||
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
|
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"query": " ",
|
"query": " ",
|
||||||
})
|
})
|
||||||
assert.True(t, result.IsError)
|
assert.True(t, result.IsError)
|
||||||
|
|
@ -35,7 +36,7 @@ func TestFindSkillsToolCacheHit(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
tool := NewFindSkillsTool(skills.NewRegistryManager(), cache)
|
tool := NewFindSkillsTool(skills.NewRegistryManager(), cache)
|
||||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"query": "github",
|
"query": "github",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ func TestFindSkillsToolParameters(t *testing.T) {
|
||||||
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
|
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
|
||||||
params := tool.Parameters()
|
params := tool.Parameters()
|
||||||
|
|
||||||
props, ok := params["properties"].(map[string]interface{})
|
props, ok := params["properties"].(map[string]any)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Contains(t, props, "query")
|
assert.Contains(t, props, "query")
|
||||||
assert.Contains(t, props, "limit")
|
assert.Contains(t, props, "limit")
|
||||||
|
|
@ -71,7 +72,14 @@ func TestFormatSearchResultsEmpty(t *testing.T) {
|
||||||
|
|
||||||
func TestFormatSearchResultsWithData(t *testing.T) {
|
func TestFormatSearchResultsWithData(t *testing.T) {
|
||||||
results := []skills.SearchResult{
|
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)
|
output := formatSearchResults("github", results, false)
|
||||||
assert.Contains(t, output, "github")
|
assert.Contains(t, output, "github")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request,
|
||||||
// Attach context.
|
// Attach context.
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
logger.DebugCF("download", "Starting download", map[string]interface{}{
|
logger.DebugCF("download", "Starting download", map[string]any{
|
||||||
"url": req.URL.String(),
|
"url": req.URL.String(),
|
||||||
"max_bytes": maxBytes,
|
"max_bytes": maxBytes,
|
||||||
})
|
})
|
||||||
|
|
@ -52,7 +52,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request,
|
||||||
}
|
}
|
||||||
tmpPath := tmpFile.Name()
|
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,
|
"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)
|
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,
|
"path": tmpPath,
|
||||||
"bytes_written": written,
|
"bytes_written": written,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,13 @@ func ExtractZipFile(zipPath string, targetDir string) error {
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
logger.DebugCF("zip", "Extracting ZIP", map[string]interface{}{
|
logger.DebugCF("zip", "Extracting ZIP", map[string]any{
|
||||||
"zip_path": zipPath,
|
"zip_path": zipPath,
|
||||||
"target_dir": targetDir,
|
"target_dir": targetDir,
|
||||||
"entries": len(reader.File),
|
"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)
|
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).
|
// Double-check the resolved path is within target directory (defense-in-depth).
|
||||||
targetDirClean := filepath.Clean(targetDir)
|
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)
|
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 f.FileInfo().IsDir() {
|
||||||
if err := os.MkdirAll(destPath, 0755); err != nil {
|
if err := os.MkdirAll(destPath, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure parent directory exists.
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +99,7 @@ func extractSingleFile(f *zip.File, destPath string) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
if cerr := outFile.Close(); cerr != nil {
|
if cerr := outFile.Close(); cerr != nil {
|
||||||
_ = os.Remove(destPath)
|
_ = 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,
|
"dest_path": destPath,
|
||||||
"error": cerr.Error(),
|
"error": cerr.Error(),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue