- main.go: 30+ log.Printf → logger.Infof - plugin/registry: 12 log.Printf → logger.Infof - db/db.go, ws/hub.go: log → logger - platform-bridge: fmt.Printf/println → logger.Info - Logs now: time=... level=INFO msg=... - 16 packages pass, vet clean
140 lines
4.6 KiB
Go
140 lines
4.6 KiB
Go
// 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,
|
|
}
|
|
}
|