// Package bridge provides multi-platform message bridging for Cyrene. // It defines unified message types and adapter interfaces for QQ, Telegram, Discord, and Webhook. package bridge import ( "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // UnifiedMessage is the internal representation of a message from any platform. type UnifiedMessage struct { ID string `json:"id"` Platform string `json:"platform"` // "qq" | "telegram" | "discord" | "webhook" Source string `json:"source"` // Group ID or user ID SenderID string `json:"sender_id"` // Platform-specific sender ID SenderName string `json:"sender_name"` // Display name Content string `json:"content"` IsAtBot bool `json:"is_at_bot"` // Whether @bot was used IsDM bool `json:"is_dm"` // Private message IsAdmin bool `json:"is_admin"` // Sender is admin Attachments []Attachment `json:"attachments,omitempty"` RawData any `json:"raw_data,omitempty"` // Platform-specific raw payload Timestamp time.Time `json:"timestamp"` } // Attachment represents a file, image, or voice attachment. type Attachment struct { Type string `json:"type"` // "image" | "file" | "voice" | "video" URL string `json:"url"` Mime string `json:"mime,omitempty"` Size int64 `json:"size,omitempty"` } // SendOptions controls how a message is sent to a platform. type SendOptions struct { ReplyTo string // Message ID to reply to MaxLength int // Max content length (default 500 for group chats) MinGap time.Duration // Min interval between messages (default 3s for QQ groups) } // Adapter is the interface for all platform adapters. type Adapter interface { // Name returns the unique adapter identifier (e.g. "qq-main", "telegram-bot") Name() string // Platform returns the platform type: "qq", "telegram", "discord", "webhook" Platform() string // Start begins listening for messages (WebSocket connect / long polling / HTTP server) Start() error // Stop shuts down the adapter Stop() error // Send delivers a message to the platform Send(target string, content string, opts SendOptions) error // Incoming returns the channel of incoming messages from this adapter Incoming() <-chan *UnifiedMessage // IsConnected returns whether the adapter is currently connected IsConnected() bool } // Router matches incoming messages to the AI pipeline and routes responses back. type Router struct { adapters map[string]Adapter onMessage func(msg *UnifiedMessage) // Called when a message should be sent to AI-Core } // NewRouter creates a new bridge router. func NewRouter() *Router { return &Router{adapters: make(map[string]Adapter)} } // Register adds an adapter to the router. func (r *Router) Register(a Adapter) { r.adapters[a.Name()] = a } // SetMessageHandler sets the callback for incoming messages. func (r *Router) SetMessageHandler(fn func(*UnifiedMessage)) { r.onMessage = fn } // StartAll starts all registered adapters. func (r *Router) StartAll() { for name, a := range r.adapters { go func(name string, a Adapter) { if err := a.Start(); err != nil { logger.Info("[bridge]", name, "start error:", err.Error()) return } for msg := range a.Incoming() { if r.onMessage != nil { r.onMessage(msg) } } }(name, a) } } // StopAll stops all adapters. func (r *Router) StopAll() { for _, a := range r.adapters { a.Stop() } } // SendTo routes a response message to the appropriate adapter. func (r *Router) SendTo(platform, target, content string, opts SendOptions) error { for _, a := range r.adapters { if a.Platform() == platform { return a.Send(target, content, opts) } } return nil } // GroupChatRules defines the behavior constraints for group chat interactions. type GroupChatRules struct { MaxLength int // Max characters per message MinGap time.Duration // Min interval between messages MaxPerRound int // Max messages per conversation round SilenceKeywords []string // Keywords that trigger silence mode ("闭嘴", "别说话") AmbientMode bool // Whether to use ambient/审查 mode (reply only when mentioned or LLM decides) } // DefaultGroupRules returns sensible defaults for QQ group chats. func DefaultGroupRules() GroupChatRules { return GroupChatRules{ MaxLength: 500, MinGap: 3 * time.Second, MaxPerRound: 2, SilenceKeywords: []string{"闭嘴", "别说话", "别说了", "安静", "shut up", "stop"}, AmbientMode: true, } }