Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4ba7e55
refactor(channels): add factory registry and export SetRunning on Bas…
alexhoshina Feb 20, 2026
7de7f0f
refactor(channels): replace direct constructors with factory registry…
alexhoshina Feb 20, 2026
8db88e8
refactor(channels): add channel subpackages and update gateway imports
alexhoshina Feb 20, 2026
eddb3c9
refactor(channels): remove old channel files from parent package
alexhoshina Feb 20, 2026
962865a
refactor(channels): remove redundant setRunning method from BaseChannel
alexhoshina Feb 20, 2026
4dd8201
refactor(channels): replace bool with atomic.Bool for running state i…
alexhoshina Feb 20, 2026
31cb147
fix: golangci-lint run --fix
alexhoshina Feb 21, 2026
bae613b
refactor(bus,channels): promote peer and messageID from metadata to s…
alexhoshina Feb 22, 2026
9d19c89
refactor(channels): unify Start/Stop lifecycle and fix goroutine/cont…
alexhoshina Feb 22, 2026
d7a288d
refactor(channels): unify message splitting and add per-channel worke…
alexhoshina Feb 22, 2026
d3000c4
refactor(media): add MediaStore for unified media file lifecycle mana…
alexhoshina Feb 22, 2026
013b58a
refactor(channels): add per-channel rate limiting and send retry with…
alexhoshina Feb 22, 2026
52e8d17
refactor(bus): fix deadlock and concurrency issues in MessageBus
alexhoshina Feb 22, 2026
1d57b38
refactor(channels): standardize Send error classification with sentin…
alexhoshina Feb 22, 2026
9a3e352
refactor(channels): consolidate HTTP servers into shared server manag…
alexhoshina Feb 22, 2026
7910955
feat(channels): add MediaSender optional interface for outbound media
alexhoshina Feb 22, 2026
97b1b30
refactor(channels): remove channel-side voice transcription (Phase 12)
alexhoshina Feb 22, 2026
f16224d
refactor(channels): standardize group chat trigger filtering (Phase 8)
alexhoshina Feb 22, 2026
37f0d99
feat(channels): add typing/placeholder automation and Pico Protocol c…
alexhoshina Feb 22, 2026
5bcb6f5
fix: resolve golangci-lint issues in channel system
alexhoshina Feb 22, 2026
770e00a
refactor(channels): move SplitMessage from pkg/utils to pkg/channels
alexhoshina Feb 22, 2026
67c6e3c
fix: address PR review feedback across channel system
alexhoshina Feb 22, 2026
42170d2
feat(identity): add unified user identity with canonical platform:id …
alexhoshina Feb 22, 2026
7b44d86
refactor(loop): disable media cleanup to prevent premature file deletion
alexhoshina Feb 23, 2026
dc919a6
fix: address PR #662 review comments (bus drain, context timeouts, on…
alexhoshina Feb 23, 2026
21f0c45
chore: apply PR #697 comment translations to refactored channel subpa…
alexhoshina Feb 24, 2026
c1e4980
feat(line): add StartTyping and PlaceholderRecorder integration
ex-takashima Feb 26, 2026
dd1cd11
fix(line): log loading refresh errors, skip typing without recorder
ex-takashima Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/picoclaw/internal/agent/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func agentCmd(message, sessionKey, model string, debug bool) error {
}

msgBus := bus.NewMessageBus()
defer msgBus.Close()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)

// Print agent startup info (only for interactive mode)
Expand Down
81 changes: 32 additions & 49 deletions cmd/picoclaw/internal/gateway/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,39 @@

import (
"context"
"errors"

Check failure on line 5 in cmd/picoclaw/internal/gateway/helpers.go

View workflow job for this annotation

GitHub Actions / Linter

"errors" imported and not used (typecheck)

Check failure on line 5 in cmd/picoclaw/internal/gateway/helpers.go

View workflow job for this annotation

GitHub Actions / Tests

"errors" imported and not used
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"time"

"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
_ "github.com/sipeed/picoclaw/pkg/channels/line"
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
_ "github.com/sipeed/picoclaw/pkg/channels/onebot"
_ "github.com/sipeed/picoclaw/pkg/channels/pico"
_ "github.com/sipeed/picoclaw/pkg/channels/qq"
_ "github.com/sipeed/picoclaw/pkg/channels/slack"
_ "github.com/sipeed/picoclaw/pkg/channels/telegram"
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices"
"github.com/sipeed/picoclaw/pkg/health"
"github.com/sipeed/picoclaw/pkg/heartbeat"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/media"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/voice"
)

func gatewayCmd(debug bool) error {
Expand Down Expand Up @@ -105,49 +115,17 @@
return tools.SilentResult(response)
})

channelManager, err := channels.NewManager(cfg, msgBus)
// Create media store for file lifecycle management
mediaStore := media.NewFileMediaStore()

channelManager, err := channels.NewManager(cfg, msgBus, mediaStore)
if err != nil {
return fmt.Errorf("error creating channel manager: %w", err)
}

// Inject channel manager into agent loop for command handling
// Inject channel manager and media store into agent loop
agentLoop.SetChannelManager(channelManager)

var transcriber *voice.GroqTranscriber
groqAPIKey := cfg.Providers.Groq.APIKey
if groqAPIKey == "" {
for _, mc := range cfg.ModelList {
if strings.HasPrefix(mc.Model, "groq/") && mc.APIKey != "" {
groqAPIKey = mc.APIKey
break
}
}
}
if groqAPIKey != "" {
transcriber = voice.NewGroqTranscriber(groqAPIKey)
logger.InfoC("voice", "Groq voice transcription enabled")
}

if transcriber != nil {
if telegramChannel, ok := channelManager.GetChannel("telegram"); ok {
if tc, ok := telegramChannel.(*channels.TelegramChannel); ok {
tc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Telegram channel")
}
}
if discordChannel, ok := channelManager.GetChannel("discord"); ok {
if dc, ok := discordChannel.(*channels.DiscordChannel); ok {
dc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Discord channel")
}
}
if slackChannel, ok := channelManager.GetChannel("slack"); ok {
if sc, ok := slackChannel.(*channels.SlackChannel); ok {
sc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Slack channel")
}
}
}
agentLoop.SetMediaStore(mediaStore)

enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 {
Expand Down Expand Up @@ -184,16 +162,15 @@
fmt.Println("✓ Device event service started")
}

// Setup shared HTTP server with health endpoints and webhook handlers
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
addr := fmt.Sprintf("%s:%d", cfg.Gateway.Host, cfg.Gateway.Port)
channelManager.SetupHTTPServer(addr, healthServer)

if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("Error starting channels: %v\n", err)
}

healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
go func() {
if err := healthServer.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
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)

go agentLoop.Run(ctx)
Expand All @@ -207,12 +184,18 @@
cp.Close()
}
cancel()
healthServer.Stop(context.Background())
msgBus.Close()

// Use a fresh context with timeout for graceful shutdown,
// since the original ctx is already cancelled.
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer shutdownCancel()

channelManager.StopAll(shutdownCtx)
deviceService.Stop()
heartbeatService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("✓ Gateway stopped")

return nil
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
Expand Down
Loading
Loading