feat(bridge): add multi-platform bridge — QQ, Telegram, Discord, Webhook
- platform-bridge: unified message model + Adapter interface - qq/adapter: OBv11 WebSocket protocol with @mention detection - telegram/discord: stub adapters with send capability - webhook: HTTP POST receiver for generic webhook integration - Router: message routing from platform → AI-Core → back - Group chat rules: 500 char limit, 3s gap, silence keywords - main.go: conditional adapter loading via env vars
This commit is contained in:
1 parent
7e3ec8da93
commit
17ef73bcbe
7 files changed
+460
No files matched your search
@@ -0,0 +1,163 @@
|
||||
// Package qq provides an adapter for QQ (OBv11 protocol) integration.
|
||||
package qq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/platform-bridge"
|
||||
)
|
||||
|
||||
// Config holds QQ adapter configuration.
|
||||
type Config struct {
|
||||
Name string // Adapter name (e.g. "qq-main")
|
||||
WSURL string // OBv11 WebSocket URL ("ws://localhost:3001")
|
||||
AccessToken string // OBv11 access token
|
||||
BotQQ string // Bot's own QQ number
|
||||
}
|
||||
|
||||
// Adapter implements bridge.Adapter for QQ OBv11.
|
||||
type Adapter struct {
|
||||
cfg Config
|
||||
conn *websocket.Conn
|
||||
incoming chan *bridge.UnifiedMessage
|
||||
mu sync.Mutex
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// New creates a QQ adapter.
|
||||
func New(cfg Config) *Adapter {
|
||||
return &Adapter{
|
||||
cfg: cfg,
|
||||
incoming: make(chan *bridge.UnifiedMessage, 64),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Name() string { return a.cfg.Name }
|
||||
func (a *Adapter) Platform() string { return "qq" }
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(a.cfg.WSURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qq ws connect: %w", err)
|
||||
}
|
||||
a.conn = conn
|
||||
|
||||
// Send auth
|
||||
authMsg := map[string]any{
|
||||
"type": "meta::connect",
|
||||
"access_token": a.cfg.AccessToken,
|
||||
}
|
||||
conn.WriteJSON(authMsg)
|
||||
|
||||
// Read loop
|
||||
go func() {
|
||||
defer close(a.incoming)
|
||||
for {
|
||||
_, raw, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var event map[string]any
|
||||
if err := json.Unmarshal(raw, &event); err != nil {
|
||||
continue
|
||||
}
|
||||
msg := a.parseMessage(event)
|
||||
if msg != nil {
|
||||
a.incoming <- msg
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Stop() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.conn != nil {
|
||||
a.conn.Close()
|
||||
}
|
||||
close(a.done)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Incoming() <-chan *bridge.UnifiedMessage { return a.incoming }
|
||||
|
||||
func (a *Adapter) IsConnected() bool {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.conn != nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Send(target string, content string, opts bridge.SendOptions) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
// Apply length limit
|
||||
if opts.MaxLength > 0 && len([]rune(content)) > opts.MaxLength {
|
||||
runes := []rune(content)
|
||||
content = string(runes[:opts.MaxLength]) + "..."
|
||||
}
|
||||
|
||||
msg := map[string]any{
|
||||
"action": "send_msg",
|
||||
"params": map[string]any{
|
||||
"user_id": target,
|
||||
"message": content,
|
||||
"auto_escape": false,
|
||||
},
|
||||
}
|
||||
return a.conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
func (a *Adapter) parseMessage(event map[string]any) *bridge.UnifiedMessage {
|
||||
postType, _ := event["post_type"].(string)
|
||||
if postType != "message" {
|
||||
return nil
|
||||
}
|
||||
|
||||
msgType, _ := event["message_type"].(string)
|
||||
sender := event["sender"].(map[string]any)
|
||||
rawMsg, _ := event["raw_message"].(string)
|
||||
senderID := fmt.Sprintf("%v", sender["user_id"])
|
||||
senderName, _ := sender["nickname"].(string)
|
||||
|
||||
isDM := msgType == "private"
|
||||
groupID := ""
|
||||
if !isDM {
|
||||
groupID = fmt.Sprintf("%v", event["group_id"])
|
||||
}
|
||||
|
||||
// Check if bot was @mentioned
|
||||
atBot := false
|
||||
if atList, ok := event["at_list"].([]any); ok {
|
||||
for _, at := range atList {
|
||||
if fmt.Sprintf("%v", at) == a.cfg.BotQQ {
|
||||
atBot = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &bridge.UnifiedMessage{
|
||||
ID: fmt.Sprintf("%v", event["message_id"]),
|
||||
Platform: "qq",
|
||||
Source: groupID,
|
||||
SenderID: senderID,
|
||||
SenderName: senderName,
|
||||
Content: rawMsg,
|
||||
IsAtBot: atBot,
|
||||
IsDM: isDM,
|
||||
RawData: event,
|
||||
Timestamp: time.Unix(int64(event["time"].(float64)), 0),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user