From 1d09765a4ab8a8afcae5d7e07a183dfcfd9aa24e Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Wed, 5 Aug 2026 17:07:29 +0800 Subject: [PATCH] chore(qq_napcat): rewrite all napcat_bridge logic into this project to replace adapt into napcat_bridge --- README.md | 24 +- config/config.go | 6 + config/variables.go | 13 +- handler/bot.go | 96 ------ internal/controller/controller.go | 10 - internal/controller/pipes/qq_napcat/napcat.go | 315 ++++++++++++++++++ .../controller/pipes/{ => qq_napcat}/qq.go | 221 ++++++++---- main.go | 3 +- router.go | 3 - 9 files changed, 507 insertions(+), 184 deletions(-) delete mode 100644 handler/bot.go create mode 100644 internal/controller/pipes/qq_napcat/napcat.go rename internal/controller/pipes/{ => qq_napcat}/qq.go (63%) diff --git a/README.md b/README.md index efa2b4d..87f3a31 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ nukumizu-backend/ ├── handler/ │ ├── user.go # User login/register handlers │ ├── server.go # Server list/status/exec handlers -│ ├── bot.go # Bot message receive handler │ └── health.go # Health check endpoint ├── database/ │ └── user.go # SQLite user database @@ -33,15 +32,18 @@ nukumizu-backend/ │ ├── komari/ │ │ ├── client.go # Komari HTTP API client │ │ └── ws.go # Komari WebSocket client +│ ├── qq/ +│ │ ├── qq.go # QQ (Napcat) Bot controller +│ │ └── napcat.go # NapCat HTTP API + WebSocket client │ ├── node/ │ │ └── tracker.go # Thread-safe node state tracking │ ├── controller/ │ │ ├── controller.go # Controller interface & manager -│ │ ├── qq.go # QQ (Napcat) Bot controller -│ │ ├── telegram.go # Telegram Bot controller -│ │ ├── email.go # Email notification controller -│ │ ├── ntfy.go # Ntfy notification controller -│ │ └── webhook.go # Webhook notification controller +│ │ └── pipes/ +│ │ ├── telegram.go # Telegram Bot controller +│ │ ├── email.go # Email notification controller +│ │ ├── ntfy.go # Ntfy notification controller +│ │ └── webhook.go # Webhook notification controller │ └── template/ │ └── template.go # Message template engine ``` @@ -57,6 +59,10 @@ Copy and modify `config.json` at the project root: "listenAddr": "0.0.0.0", "listenPort": "8080" }, + "debug": { + "showNapcatMsg": false, + "showTelegramMsg": false + }, "komari": { "dashboardURL": "http://127.0.0.1:25774", "account": { @@ -67,8 +73,9 @@ Copy and modify `config.json` at the project root: "controllerMethod": { "qq(napcat)": { "enabled": false, - "url": "http://127.0.0.1:8081", - "token": "", + "napcatAddr": "127.0.0.1", + "napcatPort": "3000", + "napcatToken": "", "botQQID": 0, "listenMethod": "global", "admins": [], @@ -127,7 +134,6 @@ Authentication is via `X-Token` and `X-Timestamp` HTTP headers. | `/api/server/list` | GET | bot/admin | List all servers | | `/api/server/getStatus` | GET | bot/admin | Get server recent status | | `/api/server/exec` | POST | bot/admin | Execute command on server(s) | -| `/api/bot/msg/recv` | POST | bot | Receive bot messages (from napcat-bridge) | | `/health` | GET | None | Health check | | `/api/system/getLogs` | WS | None | Real-time log streaming | diff --git a/config/config.go b/config/config.go index c362065..0f3641b 100644 --- a/config/config.go +++ b/config/config.go @@ -31,6 +31,12 @@ func LoadConfig(configPath string) (*Config, error) { if cfg.ControllerMethod.QQ.ListenMethod == "" { cfg.ControllerMethod.QQ.ListenMethod = "global" } + if cfg.ControllerMethod.QQ.NapcatAddr == "" { + cfg.ControllerMethod.QQ.NapcatAddr = "127.0.0.1" + } + if cfg.ControllerMethod.QQ.NapcatPort == "" { + cfg.ControllerMethod.QQ.NapcatPort = "3000" + } if cfg.ControllerMethod.QQ.Admins == nil { cfg.ControllerMethod.QQ.Admins = []string{} } diff --git a/config/variables.go b/config/variables.go index 7cb049c..8bdd2f6 100644 --- a/config/variables.go +++ b/config/variables.go @@ -7,6 +7,13 @@ type SystemConfig struct { ListenPort string `json:"listenPort"` } +// DebugConfig holds debug-level configuration. +type DebugConfig struct { + ShowNapcatMsg bool `json:"showNapcatMsg"` + ShowNapcatAction bool `json:"showNapcatAction"` + ShowTelegramMsg bool `json:"showTelegramMsg"` +} + // KomariAccount holds Komari login credentials. type KomariAccount struct { Username string `json:"username"` @@ -22,8 +29,9 @@ type KomariConfig struct { // QQConfig holds QQ (Napcat) Bot controller configuration. type QQConfig struct { Enabled bool `json:"enabled"` - URL string `json:"url"` - Token string `json:"token"` + NapcatAddr string `json:"napcatAddr"` + NapcatPort string `json:"napcatPort"` + NapcatToken string `json:"napcatToken"` BotQQID int64 `json:"botQQID"` ListenMethod string `json:"listenMethod"` Admins []string `json:"admins"` @@ -88,6 +96,7 @@ type ControllerMessageConfig struct { // Config is the top-level application configuration. type Config struct { System SystemConfig `json:"system"` + Debug DebugConfig `json:"debug"` Komari KomariConfig `json:"komari"` ControllerMethod ControllerMethodConfig `json:"controllerMethod"` ControllerMessage ControllerMessageConfig `json:"controllerMessage"` diff --git a/handler/bot.go b/handler/bot.go deleted file mode 100644 index 8aded73..0000000 --- a/handler/bot.go +++ /dev/null @@ -1,96 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - - "nukumizu-backend/internal/controller" - "nukumizu-backend/postLog" - "nukumizu-backend/utils" -) - -// OneBotMessage represents a OneBot 11 message event forwarded from napcat-bridge. -type OneBotMessage struct { - PostType string `json:"post_type"` - MessageType string `json:"message_type"` - GroupID int64 `json:"group_id"` - UserID int64 `json:"user_id"` - RawMessage string `json:"raw_message"` - Message string `json:"message"` - Sender struct { - UserID int64 `json:"user_id"` - Nickname string `json:"nickname"` - } `json:"sender"` - SelfID int64 `json:"self_id"` - SubType string `json:"sub_type"` -} - -// BotMessageHandler handles POST /api/bot/msg/recv. -// Receives OneBot 11 events forwarded from napcat-bridge and -// routes them to the appropriate bot controller. -func BotMessageHandler(w http.ResponseWriter, r *http.Request) { - if !utils.Auth(w, r, "POST", "bot") { - return - } - - var event OneBotMessage - if err := json.NewDecoder(r.Body).Decode(&event); err != nil { - postLog.Debug("Failed to parse bot message: " + err.Error()) - utils.SendErrorResponse(w, http.StatusBadRequest, "invalid message format") - return - } - - // Only handle message events. - if event.PostType != "message" { - utils.SendSuccessResponse(w, "event ignored", map[string]interface{}{ - "post_type": event.PostType, - }) - return - } - - // Route to the Napcat/QQ controller. - ctrl := controller.GetController("qq(napcat)") - if ctrl == nil { - postLog.Debug("QQ controller not available, ignoring message") - utils.SendSuccessResponse(w, "controller not available", nil) - return - } - - cmdCtrl, ok := ctrl.(controller.CommandController) - if !ok { - utils.SendErrorResponse(w, http.StatusInternalServerError, "controller does not support commands") - return - } - - // Determine chat information. - chatID := event.GroupID - chatType := event.MessageType - if chatType == "private" { - chatID = event.UserID - } - - // Build a command from the raw message. - cmd := controller.Command{ - RawText: event.RawMessage, - ChatID: chatID, - ChatType: chatType, - SenderID: event.UserID, - } - - response, err := cmdCtrl.HandleCommand(cmd) - if err != nil { - postLog.Error("Bot command handling failed: " + err.Error()) - utils.SendErrorResponse(w, http.StatusInternalServerError, "command handling failed: "+err.Error()) - return - } - - if response != "" { - utils.SendSuccessResponse(w, "", map[string]interface{}{ - "response": response, - "chatID": chatID, - "chatType": chatType, - }) - } else { - utils.SendSuccessResponse(w, "no response", nil) - } -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 50c1133..884814f 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -59,16 +59,6 @@ func GetManager() *Manager { return globalManager } -// GetController returns a specific controller by name. -func GetController(name string) Controller { - if globalManager == nil { - return nil - } - globalManager.mu.RLock() - defer globalManager.mu.RUnlock() - return globalManager.controllers[name] -} - // Register adds a controller to the manager. func (m *Manager) Register(c Controller) { m.mu.Lock() diff --git a/internal/controller/pipes/qq_napcat/napcat.go b/internal/controller/pipes/qq_napcat/napcat.go new file mode 100644 index 0000000..e6d62aa --- /dev/null +++ b/internal/controller/pipes/qq_napcat/napcat.go @@ -0,0 +1,315 @@ +package qq_napcat + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "time" + + "nukumizu-backend/postLog" + "nukumizu-backend/config" + "github.com/gorilla/websocket" +) + +// APIResponse mirrors NapCat's HTTP API response envelope. +type APIResponse struct { + Status string `json:"status"` + RetCode int `json:"retcode"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} + +// Client is the HTTP + WebSocket client for a single NapCat instance. +// It both listens for incoming OneBot events over WebSocket and issues +// outbound NapCat API calls over HTTP. +type Client struct { + addr string + port string + token string + httpClient *http.Client + + connMu sync.Mutex + conn *websocket.Conn + stopCh chan struct{} + stopOnce sync.Once +} + +// NewClient creates a NapCat client for the given host/port/token. +func NewClient(addr, port, token string) *Client { + return &Client{ + addr: addr, + port: port, + token: token, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + stopCh: make(chan struct{}), + } +} + +func (c *Client) baseURL() string { + return fmt.Sprintf("http://%s:%s", c.addr, c.port) +} + +// sendRequest performs an HTTP call to NapCat, injecting access_token into POST +// bodies AND setting Authorization: Bearer (compatible with HTTP Server adapters +// created in the NapCat Web UI). +func (c *Client) sendRequest(method, endpoint string, body []byte) ([]byte, error) { + url := c.baseURL() + endpoint + + // For POST requests, inject access_token into the JSON body before sending. + var finalBody []byte + if body != nil && c.token != "" { + var bodyMap map[string]interface{} + if err := json.Unmarshal(body, &bodyMap); err == nil { + bodyMap["access_token"] = c.token + finalBody, _ = json.Marshal(bodyMap) + } + } + if finalBody == nil { + finalBody = body + } + + var req *http.Request + var err error + if finalBody != nil { + req, err = http.NewRequest(method, url, bytes.NewReader(finalBody)) + } else { + req, err = http.NewRequest(method, url, nil) + } + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request to napcat: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read napcat response: %w", err) + } + + return respBody, nil +} + +// parseResponse unmarshals NapCat's raw response body into an APIResponse. +func (c *Client) parseResponse(respBody []byte, caller string) (*APIResponse, error) { + var napcatResp APIResponse + if err := json.Unmarshal(respBody, &napcatResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal napcat response (%s): %w\n%s", caller, err, string(respBody)) + } + return &napcatResp, nil +} + +// SendMsg sends a message via NapCat. +// targetType: "group" or "private" +// targetID: QQ group ID or user ID +// msg: message content +// hasAt: whether to prepend an @mention +// atTargetID: the QQ ID to @mention +func (c *Client) SendMsg(targetType string, targetID int64, msg string, hasAt bool, atTargetID int64) (*APIResponse, error) { + // Build message with optional @mention. + message := msg + if hasAt && atTargetID > 0 { + message = fmt.Sprintf("[CQ:at,qq=%d] %s", atTargetID, msg) + } + + var endpoint string + var napcatReq map[string]interface{} + + switch targetType { + case "group": + endpoint = "/send_group_msg" + napcatReq = map[string]interface{}{ + "group_id": targetID, + "message": message, + } + case "private": + endpoint = "/send_private_msg" + napcatReq = map[string]interface{}{ + "user_id": targetID, + "message": message, + } + default: + return nil, fmt.Errorf("invalid targetType: %s, must be 'group' or 'private'", targetType) + } + + reqBody, err := json.Marshal(napcatReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + if config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug(fmt.Sprintf("[Napcat] SendMsg -> %s (%s): %s", endpoint, targetType, message)) + } + + respBody, err := c.sendRequest(http.MethodPost, endpoint, reqBody) + if err != nil { + return nil, err + } + + return c.parseResponse(respBody, "SendMsg") +} + +// RecallMsg recalls (deletes) a sent message via NapCat. +func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) { + reqBody, err := json.Marshal(map[string]interface{}{ + "message_id": msgID, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + if config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug(fmt.Sprintf("[Napcat] RecallMsg -> /delete_msg: %d", msgID)) + } + + respBody, err := c.sendRequest(http.MethodPost, "/delete_msg", reqBody) + if err != nil { + return nil, err + } + + return c.parseResponse(respBody, "RecallMsg") +} + +// GetGroupList retrieves the list of joined groups from NapCat. +func (c *Client) GetGroupList() (*APIResponse, error) { + if config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("[Napcat] GetGroupList -> /get_group_list") + } + + respBody, err := c.sendRequest(http.MethodGet, "/get_group_list", nil) + if err != nil { + return nil, err + } + + return c.parseResponse(respBody, "GetGroupList") +} + +// GetGroupInfo retrieves detailed information about a specific group from NapCat. +func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) { + reqBody, err := json.Marshal(map[string]interface{}{ + "group_id": groupID, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + if config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug(fmt.Sprintf("[Napcat] GetGroupInfo -> /get_group_info: %d", groupID)) + } + + respBody, err := c.sendRequest(http.MethodPost, "/get_group_info", reqBody) + if err != nil { + return nil, err + } + + return c.parseResponse(respBody, "GetGroupInfo") +} + +// GetFriendsList retrieves the friends list from NapCat. +func (c *Client) GetFriendsList() (*APIResponse, error) { + if config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("[Napcat] GetFriendsList -> /get_friend_list") + } + + respBody, err := c.sendRequest(http.MethodGet, "/get_friend_list", nil) + if err != nil { + return nil, err + } + + return c.parseResponse(respBody, "GetFriendsList") +} + +// Listen connects to the NapCat WebSocket server and calls onEvent for every raw +// message. It blocks forever, reconnecting every 5s after a disconnect, until +// Stop is called. Run it in a goroutine. +func (c *Client) Listen(onEvent func(raw []byte)) { + for { + select { + case <-c.stopCh: + return + default: + } + + c.listenOnce(onEvent) + + postLog.Warning("[Napcat] WebSocket disconnected, reconnecting in 5s...") + select { + case <-c.stopCh: + return + case <-time.After(5 * time.Second): + } + } +} + +// listenOnce connects to the NapCat WebSocket and reads events until the +// connection drops or Stop is called. +func (c *Client) listenOnce(onEvent func(raw []byte)) { + wsURL := fmt.Sprintf("ws://%s:%s/", c.addr, c.port) + header := http.Header{} + if c.token != "" { + wsURL += "?access_token=" + url.QueryEscape(c.token) + header.Set("Authorization", "Bearer "+c.token) + } + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, header) + if err != nil { + postLog.Error(fmt.Sprintf("[Napcat] Failed to connect to NapCat WebSocket: %v", err)) + return + } + + c.connMu.Lock() + c.conn = conn + c.connMu.Unlock() + defer func() { + c.connMu.Lock() + if c.conn == conn { + c.conn = nil + } + c.connMu.Unlock() + conn.Close() + }() + + postLog.Info(fmt.Sprintf("[Napcat] Connected to NapCat WebSocket at %s", wsURL)) + + for { + select { + case <-c.stopCh: + return + default: + } + + _, msgBytes, err := conn.ReadMessage() + if err != nil { + postLog.Error(fmt.Sprintf("[Napcat] WebSocket read error: %v", err)) + return + } + + onEvent(msgBytes) + } +} + +// Stop closes any open WebSocket connection and unblocks the Listen reconnect loop. +func (c *Client) Stop() { + c.stopOnce.Do(func() { close(c.stopCh) }) + + c.connMu.Lock() + defer c.connMu.Unlock() + if c.conn != nil { + c.conn.Close() + c.conn = nil + } +} diff --git a/internal/controller/pipes/qq.go b/internal/controller/pipes/qq_napcat/qq.go similarity index 63% rename from internal/controller/pipes/qq.go rename to internal/controller/pipes/qq_napcat/qq.go index ebf7363..f611256 100644 --- a/internal/controller/pipes/qq.go +++ b/internal/controller/pipes/qq_napcat/qq.go @@ -1,35 +1,48 @@ -package pipes +package qq_napcat import ( - "bytes" "encoding/json" "fmt" - "net/http" "strings" - "time" "nukumizu-backend/config" + "nukumizu-backend/internal/controller" "nukumizu-backend/internal/komari" "nukumizu-backend/internal/node" "nukumizu-backend/internal/template" "nukumizu-backend/postLog" - "nukumizu-backend/internal/controller" ) -// QQController handles QQ Bot interactions via napcat-bridge. +// oneBotEvent mirrors a OneBot 11 event pushed over the NapCat WebSocket. +// Only "message" events are handled; notice/request/meta_event are ignored. +type oneBotEvent struct { + PostType string `json:"post_type"` + MessageType string `json:"message_type"` + GroupID int64 `json:"group_id"` + UserID int64 `json:"user_id"` + RawMessage string `json:"raw_message"` + Message json.RawMessage `json:"message"` + Sender struct { + UserID int64 `json:"user_id"` + Nickname string `json:"nickname"` + } `json:"sender"` + SelfID int64 `json:"self_id"` + SubType string `json:"sub_type"` +} + +// QQController handles QQ Bot interactions by connecting directly to NapCat. type QQController struct { - cfg config.QQConfig - httpClient *http.Client + cfg config.QQConfig + napcatClient *Client } // NewQQController creates a new QQ (Napcat) controller. func NewQQController(cfg config.QQConfig) *QQController { - return &QQController{ - cfg: cfg, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, + q := &QQController{cfg: cfg} + if cfg.Enabled { + q.napcatClient = NewClient(cfg.NapcatAddr, cfg.NapcatPort, cfg.NapcatToken) } + return q } // Name returns the controller name. @@ -37,18 +50,35 @@ func (q *QQController) Name() string { return "qq(napcat)" } -// Start initializes the QQ controller. +// Start initializes the QQ controller and starts the NapCat WebSocket listener. func (q *QQController) Start() error { if !q.cfg.Enabled { - postLog.Info("QQ (Napcat) controller is disabled") + // postLog.Info("QQ (Napcat) controller is disabled") return nil } - postLog.Info("QQ (Napcat) controller started") + if q.napcatClient == nil { + postLog.Warning("QQ (Napcat) controller enabled but NapCat client is nil") + return nil + } + + postLog.Info("QQ (Napcat) controller started, connecting to NapCat WebSocket...") + go func() { + defer func() { + if r := recover(); r != nil { + postLog.Error(fmt.Sprintf("Napcat WS listener panic recovered: %v", r)) + } + }() + q.napcatClient.Listen(q.handleNapcatEvent) + }() + return nil } -// Stop shuts down the QQ controller. +// Stop shuts down the QQ controller and its WebSocket listener. func (q *QQController) Stop() { + if q.napcatClient != nil { + q.napcatClient.Stop() + } postLog.Info("QQ (Napcat) controller stopped") } @@ -57,24 +87,101 @@ func (q *QQController) IsEnabled() bool { return q.cfg.Enabled } +// handleNapcatEvent processes a raw OneBot event received from the NapCat WebSocket. +func (q *QQController) handleNapcatEvent(raw []byte) { + var ev oneBotEvent + if err := json.Unmarshal(raw, &ev); err != nil { + postLog.Debug("Failed to parse NapCat WS event: " + err.Error()) + return + } + + // Only handle message events; ignore notice/request/meta_event. + if ev.PostType != "message" { + return + } + + // Ignore messages the bot itself sent (echo prevention). + if q.isSelfMessage(ev) { + return + } + + // Determine chat information. + chatID := ev.GroupID + if ev.MessageType == "private" { + chatID = ev.UserID + } + + // Build a command from the raw message. + cmd := controller.Command{ + RawText: ev.RawMessage, + ChatID: chatID, + ChatType: ev.MessageType, + SenderID: ev.UserID, + } + + response, err := q.HandleCommand(cmd) + if err != nil { + postLog.Error("Napcat command handling failed: " + err.Error()) + return + } + + if response == "" { + return + } + + // Reply in the same chat. + if ev.MessageType == "private" { + if _, err := q.napcatClient.SendMsg("private", ev.UserID, response, false, 0); err != nil { + postLog.Warning("Failed to send NapCat private reply: " + err.Error()) + } + } else { + if _, err := q.napcatClient.SendMsg("group", ev.GroupID, response, false, 0); err != nil { + postLog.Warning("Failed to send NapCat group reply: " + err.Error()) + } + } +} + +// isSelfMessage returns true when the event was generated by the bot itself. +func (q *QQController) isSelfMessage(ev oneBotEvent) bool { + if q.cfg.BotQQID > 0 && ev.SelfID == q.cfg.BotQQID { + return true + } + if ev.UserID == ev.SelfID { + return true + } + return false +} + // HandleCommand processes a bot command and returns a response string. func (q *QQController) HandleCommand(cmd controller.Command) (string, error) { - parsed, ok := controller.ParseCommand(cmd.RawText) + cfg := config.GetConfig() + + // Debug: when ShowNapcatMsg is enabled, echo the received message back in real-time. + if cfg.Debug.ShowNapcatMsg { + postLog.Debug("Napcat message received: " + cmd.RawText) + return cmd.RawText, nil + } + + text := cmd.RawText + + // Check listen method. In "at" mode the @mention must be stripped before + // parsing, otherwise raw_message like "[CQ:at,qq=123] /list" fails the + // "/" prefix check. + if q.cfg.ListenMethod == "at" { + atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID) + if !strings.Contains(text, atMention) { + return "", nil // Not mentioned, ignore. + } + text = strings.ReplaceAll(text, atMention, "") + } + + parsed, ok := controller.ParseCommand(text) if !ok { // Not a command. In "global" mode, silently ignore. // In "at" mode, this would be an error - but at detection happens at the message level. return "", nil } - // Check listen method. - if q.cfg.ListenMethod == "at" { - // At detection: check if message contains an @mention for our bot. - atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID) - if !strings.Contains(cmd.RawText, atMention) { - return "", nil // Not mentioned, ignore. - } - } - parsed.ChatID = cmd.ChatID parsed.ChatType = cmd.ChatType parsed.SenderID = cmd.SenderID @@ -305,47 +412,35 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result } func (q *QQController) sendGroupMessage(groupID string, message string) { - var groupIDInt int64 - fmt.Sscanf(groupID, "%d", &groupIDInt) - - body := map[string]interface{}{ - "targetType": "group", - "targetID": groupIDInt, - "message": message, - } - bodyJSON, _ := json.Marshal(body) - - resp, err := q.httpClient.Post( - q.cfg.URL+"/api/msg/send", - "application/json", - bytes.NewReader(bodyJSON), - ) - if err != nil { - postLog.Warning(fmt.Sprintf("Failed to send QQ group message to %s: %v", groupID, err)) + if q.napcatClient == nil { + postLog.Warning("Cannot send QQ group message: NapCat client not initialized") return } - defer resp.Body.Close() + + var groupIDInt int64 + if _, err := fmt.Sscanf(groupID, "%d", &groupIDInt); err != nil || groupIDInt == 0 { + postLog.Warning(fmt.Sprintf("Invalid QQ group ID: %s", groupID)) + return + } + + if _, err := q.napcatClient.SendMsg("group", groupIDInt, message, false, 0); err != nil { + postLog.Warning(fmt.Sprintf("Failed to send QQ group message to %s: %v", groupID, err)) + } } func (q *QQController) sendPrivateMessage(userID string, message string) { - var userIDInt int64 - fmt.Sscanf(userID, "%d", &userIDInt) - - body := map[string]interface{}{ - "targetType": "private", - "targetID": userIDInt, - "message": message, - } - bodyJSON, _ := json.Marshal(body) - - resp, err := q.httpClient.Post( - q.cfg.URL+"/api/msg/send", - "application/json", - bytes.NewReader(bodyJSON), - ) - if err != nil { - postLog.Warning(fmt.Sprintf("Failed to send QQ private message to %s: %v", userID, err)) + if q.napcatClient == nil { + postLog.Warning("Cannot send QQ private message: NapCat client not initialized") return } - defer resp.Body.Close() + + var userIDInt int64 + if _, err := fmt.Sscanf(userID, "%d", &userIDInt); err != nil || userIDInt == 0 { + postLog.Warning(fmt.Sprintf("Invalid QQ user ID: %s", userID)) + return + } + + if _, err := q.napcatClient.SendMsg("private", userIDInt, message, false, 0); err != nil { + postLog.Warning(fmt.Sprintf("Failed to send QQ private message to %s: %v", userID, err)) + } } diff --git a/main.go b/main.go index 8c4a2a9..e51a1a6 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "nukumizu-backend/internal/controller" "nukumizu-backend/internal/controller/pipes" "nukumizu-backend/internal/komari" + "nukumizu-backend/internal/controller/pipes/qq_napcat" "nukumizu-backend/internal/node" "nukumizu-backend/postLog" "nukumizu-backend/utils" @@ -159,7 +160,7 @@ func initControllers() { } // QQ (Napcat) controller. - qqCtrl := pipes.NewQQController(cfg.ControllerMethod.QQ) + qqCtrl := qq_napcat.NewQQController(cfg.ControllerMethod.QQ) mgr.Register(qqCtrl) go func() { defer func() { diff --git a/router.go b/router.go index 823649d..f0ad92c 100644 --- a/router.go +++ b/router.go @@ -23,9 +23,6 @@ func SetupRouter() *http.ServeMux { mux.HandleFunc("/api/server/getStatus", handler.ServerGetStatusHandler) mux.HandleFunc("/api/server/exec", handler.ServerExecHandler) - // Bot message receive endpoint (from napcat-bridge). - mux.HandleFunc("/api/bot/msg/recv", handler.BotMessageHandler) - // Health check endpoint. mux.HandleFunc("/health", handler.HealthHandler)