This commit is contained in:
avaksru 2026-03-19 10:28:05 +03:00
parent 997c888a5c
commit c240378200
6 changed files with 525 additions and 75 deletions

7
go.mod
View file

@ -94,15 +94,8 @@ require (
github.com/valyala/fastjson v1.6.10 // indirect github.com/valyala/fastjson v1.6.10 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/arch v0.24.0 // indirect golang.org/x/arch v0.24.0 // indirect
<<<<<<< HEAD
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0 golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
=======
golang.org/x/crypto v0.48.0
golang.org/x/net v0.51.0
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
>>>>>>> 1038a05 (channel mqtt)
) )

View file

@ -158,8 +158,13 @@ func (m *Manager) RecordReactionUndo(channel, chatID string, undo func()) {
} }
// preSend handles typing stop, reaction undo, and placeholder editing before sending a message. // preSend handles typing stop, reaction undo, and placeholder editing before sending a message.
// Returns true if the message was already delivered (skip Send). // Returns true if the message was edited into a placeholder (skip Send).
func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMessage, ch Channel) bool { func (m *Manager) preSend(
ctx context.Context,
name string,
msg bus.OutboundMessage,
ch Channel,
) bool {
key := name + ":" + msg.ChatID key := name + ":" + msg.ChatID
// 1. Stop typing // 1. Stop typing
@ -206,7 +211,11 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess
return false return false
} }
func NewManager(cfg *config.Config, messageBus *bus.MessageBus, store media.MediaStore) (*Manager, error) { func NewManager(
cfg *config.Config,
messageBus *bus.MessageBus,
store media.MediaStore,
) (*Manager, error) {
m := &Manager{ m := &Manager{
channels: make(map[string]Channel), channels: make(map[string]Channel),
workers: make(map[string]*channelWorker), workers: make(map[string]*channelWorker),
@ -401,7 +410,9 @@ func (m *Manager) initChannels(channels *config.ChannelsConfig) error {
m.initChannel("irc", "IRC") m.initChannel("irc", "IRC")
} }
if m.config.Channels.MQTT.Enabled && m.config.Channels.MQTT.Broker != "" && m.config.Channels.MQTT.ClientID != "" && len(m.config.Channels.MQTT.SubscribeTopics) > 0 { if m.config.Channels.MQTT.Enabled && m.config.Channels.MQTT.Broker != "" &&
m.config.Channels.MQTT.ClientID != "" &&
len(m.config.Channels.MQTT.SubscribeTopics) > 0 {
m.initChannel("mqtt", "MQTT") m.initChannel("mqtt", "MQTT")
} }
@ -623,7 +634,12 @@ func (m *Manager) runWorker(ctx context.Context, name string, w *channelWorker)
// - ErrNotRunning / ErrSendFailed: permanent, no retry // - ErrNotRunning / ErrSendFailed: permanent, no retry
// - ErrRateLimit: fixed delay retry // - ErrRateLimit: fixed delay retry
// - ErrTemporary / unknown: exponential backoff retry // - ErrTemporary / unknown: exponential backoff retry
func (m *Manager) sendWithRetry(ctx context.Context, name string, w *channelWorker, msg bus.OutboundMessage) { func (m *Manager) sendWithRetry(
ctx context.Context,
name string,
w *channelWorker,
msg bus.OutboundMessage,
) {
// Rate limit: wait for token // Rate limit: wait for token
if err := w.limiter.Wait(ctx); err != nil { if err := w.limiter.Wait(ctx); err != nil {
// ctx canceled, shutting down // ctx canceled, shutting down
@ -663,7 +679,10 @@ func (m *Manager) sendWithRetry(ctx context.Context, name string, w *channelWork
} }
// ErrTemporary or unknown error — exponential backoff // ErrTemporary or unknown error — exponential backoff
backoff := min(time.Duration(float64(baseBackoff)*math.Pow(2, float64(attempt))), maxBackoff) backoff := min(
time.Duration(float64(baseBackoff)*math.Pow(2, float64(attempt))),
maxBackoff,
)
select { select {
case <-time.After(backoff): case <-time.After(backoff):
case <-ctx.Done(): case <-ctx.Done():
@ -788,12 +807,21 @@ func (m *Manager) runMediaWorker(ctx context.Context, name string, w *channelWor
// sendMediaWithRetry sends a media message through the channel with rate limiting and // sendMediaWithRetry sends a media message through the channel with rate limiting and
// retry logic. If the channel does not implement MediaSender, it silently skips. // retry logic. If the channel does not implement MediaSender, it silently skips.
func (m *Manager) sendMediaWithRetry(ctx context.Context, name string, w *channelWorker, msg bus.OutboundMediaMessage) { func (m *Manager) sendMediaWithRetry(
ctx context.Context,
name string,
w *channelWorker,
msg bus.OutboundMediaMessage,
) {
ms, ok := w.ch.(MediaSender) ms, ok := w.ch.(MediaSender)
if !ok { if !ok {
logger.DebugCF("channels", "Channel does not support MediaSender, skipping media", map[string]any{ logger.DebugCF(
"channels",
"Channel does not support MediaSender, skipping media",
map[string]any{
"channel": name, "channel": name,
}) },
)
return return
} }
@ -830,7 +858,10 @@ func (m *Manager) sendMediaWithRetry(ctx context.Context, name string, w *channe
} }
// ErrTemporary or unknown error — exponential backoff // ErrTemporary or unknown error — exponential backoff
backoff := min(time.Duration(float64(baseBackoff)*math.Pow(2, float64(attempt))), maxBackoff) backoff := min(
time.Duration(float64(baseBackoff)*math.Pow(2, float64(attempt))),
maxBackoff,
)
select { select {
case <-time.After(backoff): case <-time.After(backoff):
case <-ctx.Done(): case <-ctx.Done():

View file

@ -7,10 +7,17 @@ import (
) )
func init() { func init() {
channels.RegisterFactory("mqtt", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) { channels.RegisterFactory("mqtt", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
if !cfg.Channels.MQTT.Enabled { if !cfg.Channels.MQTT.Enabled {
return nil, nil return nil, nil
} }
return NewMQTTChannel(cfg.Channels.MQTT, b) return NewMQTTChannel(cfg.Channels.MQTT, b)
}) })
} }

View file

@ -11,7 +11,6 @@ import (
"time" "time"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
@ -191,7 +190,6 @@ func (c *MQTTChannel) Stop(ctx context.Context) error {
return nil return nil
} }
// onMessage handles incoming MQTT messages. // onMessage handles incoming MQTT messages.
func (c *MQTTChannel) onMessage(client mqtt.Client, msg mqtt.Message) { func (c *MQTTChannel) onMessage(client mqtt.Client, msg mqtt.Message) {
logger.DebugCF("mqtt", "Received message", map[string]any{ logger.DebugCF("mqtt", "Received message", map[string]any{

View file

@ -825,6 +825,7 @@ type BaiduSearchConfig struct {
} }
type WebToolsConfig struct { type WebToolsConfig struct {
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"` ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"`
Brave BraveConfig ` json:"brave"` Brave BraveConfig ` json:"brave"`
Tavily TavilyConfig ` json:"tavily"` Tavily TavilyConfig ` json:"tavily"`
@ -833,6 +834,7 @@ type WebToolsConfig struct {
SearXNG SearXNGConfig ` json:"searxng"` SearXNG SearXNGConfig ` json:"searxng"`
GLMSearch GLMSearchConfig ` json:"glm_search"` GLMSearch GLMSearchConfig ` json:"glm_search"`
BaiduSearch BaiduSearchConfig ` json:"baidu_search"` BaiduSearch BaiduSearchConfig ` json:"baidu_search"`
// PreferNative controls whether to use provider-native web search when // PreferNative controls whether to use provider-native web search when
// the active LLM supports it (e.g. OpenAI web_search_preview). When true, // the active LLM supports it (e.g. OpenAI web_search_preview). When true,
// the client-side web_search tool is hidden to avoid duplicate search surfaces, // the client-side web_search tool is hidden to avoid duplicate search surfaces,

File diff suppressed because it is too large Load diff