chore(internal/controller): split command processor from pipes as an individual module

This commit is contained in:
2026-08-06 17:53:53 +08:00
parent 10c3727b01
commit 30ed0bd9ec
7 changed files with 356 additions and 461 deletions
+2
View File
@@ -12,6 +12,8 @@ type DebugConfig struct {
ShowNapcatMsg bool `json:"showNapcatMsg"` ShowNapcatMsg bool `json:"showNapcatMsg"`
ShowNapcatAction bool `json:"showNapcatAction"` ShowNapcatAction bool `json:"showNapcatAction"`
ShowTelegramMsg bool `json:"showTelegramMsg"` ShowTelegramMsg bool `json:"showTelegramMsg"`
ShowTriggerCmdEcho bool `json:"showTriggerCmdEcho"`
NapcatIgnoreSelfMsg bool `json:"napcatIgnoreSelfMsg"`
} }
// KomariAccount holds Komari login credentials. // KomariAccount holds Komari login credentials.
-6
View File
@@ -31,12 +31,6 @@ type Controller interface {
SendExecuteResult(serverName, serverUUID, command, result string) error SendExecuteResult(serverName, serverUUID, command, result string) error
} }
// CommandController extends Controller for bidirectional channels (QQ, Telegram).
type CommandController interface {
Controller
HandleCommand(cmd Command) (response string, err error)
}
// Manager manages all controller instances and routes events. // Manager manages all controller instances and routes events.
type Manager struct { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
-3
View File
@@ -46,9 +46,6 @@ func (e *EmailController) IsEnabled() bool {
return e.cfg.Enabled return e.cfg.Enabled
} }
// HandleCommand is not supported for Email (status-only controller).
// This controller does not implement CommandController.
// SendStatusChange sends a status change notification via Email. // SendStatusChange sends a status change notification via Email.
func (e *EmailController) SendStatusChange(change node.StatusChange) error { func (e *EmailController) SendStatusChange(change node.StatusChange) error {
if !e.cfg.Enabled { if !e.cfg.Enabled {
+73 -224
View File
@@ -7,7 +7,6 @@ import (
"nukumizu-backend/config" "nukumizu-backend/config"
"nukumizu-backend/internal/controller" "nukumizu-backend/internal/controller"
"nukumizu-backend/internal/komari"
"nukumizu-backend/internal/node" "nukumizu-backend/internal/node"
"nukumizu-backend/internal/template" "nukumizu-backend/internal/template"
"nukumizu-backend/postLog" "nukumizu-backend/postLog"
@@ -16,18 +15,18 @@ import (
// oneBotEvent mirrors a OneBot 11 event pushed over the NapCat WebSocket. // oneBotEvent mirrors a OneBot 11 event pushed over the NapCat WebSocket.
// Only "message" events are handled; notice/request/meta_event are ignored. // Only "message" events are handled; notice/request/meta_event are ignored.
type oneBotEvent struct { type oneBotEvent struct {
PostType string `json:"post_type"` PostType string `json:"post_type"` // Identifies "message", "notice", "request", or "meta_event"
MessageType string `json:"message_type"` MessageType string `json:"message_type"` // Identifies "private" or "group"
GroupID int64 `json:"group_id"` GroupID int64 `json:"group_id"` // Only present for group messages
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"` // The sender's QQ ID
RawMessage string `json:"raw_message"` RawMessage string `json:"raw_message"` // The raw message text
Message json.RawMessage `json:"message"` Message json.RawMessage `json:"message"` // The message content, which may include CQ codes
Sender struct { Sender struct { // The sender's information
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"` // The sender's QQ ID
Nickname string `json:"nickname"` Nickname string `json:"nickname"` // The sender's nickname
} `json:"sender"` } `json:"sender"`
SelfID int64 `json:"self_id"` SelfID int64 `json:"self_id"` // The bot's QQ ID
SubType string `json:"sub_type"` SubType string `json:"sub_type"` // The subtype of the message, e.g., "normal", "anonymous", etc.
} }
// QQController handles QQ Bot interactions by connecting directly to NapCat. // QQController handles QQ Bot interactions by connecting directly to NapCat.
@@ -101,11 +100,20 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
// Only handle message events; ignore notice/request/meta_event. // Only handle message events; ignore notice/request/meta_event.
if ev.PostType != "message" { if ev.PostType != "message" {
if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction {
postLog.Debug("Ignoring Napcat WS event: " + string(raw))
}
return return
} }
// Ignore messages the bot itself sent (echo prevention). // Ignore messages the bot itself sent (echo prevention).
if q.isSelfMessage(ev) { if q.isSelfMessage(ev) && !config.GetConfig().System.DebugMode {
return
}
if q.isSelfMessage(ev) && config.GetConfig().System.DebugMode && config.GetConfig().Debug.NapcatIgnoreSelfMsg {
if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction {
postLog.Debug("Ignoring Napcat WS self message: " + string(raw))
}
return return
} }
@@ -115,7 +123,6 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
chatID = ev.UserID chatID = ev.UserID
} }
// Build a command from the raw message.
cmd := controller.Command{ cmd := controller.Command{
RawText: ev.RawMessage, RawText: ev.RawMessage,
ChatID: chatID, ChatID: chatID,
@@ -123,13 +130,11 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
SenderID: ev.UserID, SenderID: ev.UserID,
} }
response, err := q.HandleCommand(cmd) response := q.processCommand(cmd)
if err != nil {
postLog.Error("Napcat command handling failed: " + err.Error())
return
}
if response == "" { if response == "" {
if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction {
postLog.Debug("Napcat WS command discarded: " + string(raw))
}
return return
} }
@@ -145,6 +150,54 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
} }
} }
// processCommand validates an incoming message as a command and hands the
// complete command to the unified processor. It returns the response text to
// reply with; an empty response means the message was discarded.
func (q *QQController) processCommand(cmd controller.Command) string {
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
}
text := cmd.RawText
// In "at" listen mode, require an @mention of the bot and strip it 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) {
if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction {
postLog.Debug("Napcat WS message ignored (no @mention): " + text)
}
return "" // Not mentioned, ignore.
}
text = strings.ReplaceAll(text, atMention, "")
}
// First check whether the received message is a command.
parsed, ok := controller.ParseCommand(text)
if !ok {
return "" // Not a command, discard.
}
parsed.ChatID = cmd.ChatID
parsed.ChatType = cmd.ChatType
parsed.SenderID = cmd.SenderID
// Hand the complete command to the unified processor, which checks group
// vs private, trusted groups, admin permissions, and executes it.
response, err := controller.GetManager().RouteCommand(parsed, q.cfg.TrustedGroups, q.cfg.Admins, q.cfg.ListenMethod)
if err != nil {
postLog.Error("Napcat command processing failed: " + err.Error())
return ""
}
return response
}
// isSelfMessage returns true when the event was generated by the bot itself. // isSelfMessage returns true when the event was generated by the bot itself.
func (q *QQController) isSelfMessage(ev oneBotEvent) bool { func (q *QQController) isSelfMessage(ev oneBotEvent) bool {
if q.cfg.BotQQID > 0 && ev.SelfID == q.cfg.BotQQID { if q.cfg.BotQQID > 0 && ev.SelfID == q.cfg.BotQQID {
@@ -156,210 +209,6 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool {
return false return false
} }
// HandleCommand processes a bot command and returns a response string.
func (q *QQController) HandleCommand(cmd controller.Command) (string, error) {
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
}
parsed.ChatID = cmd.ChatID
parsed.ChatType = cmd.ChatType
parsed.SenderID = cmd.SenderID
return q.executeCommand(parsed)
}
func (q *QQController) executeCommand(cmd controller.Command) (string, error) {
// Validate against supported commands.
switch cmd.Command {
case "list":
return q.handleList()
case "status":
return q.handleStatus(cmd)
case "shutdown":
return q.handleShutdown(cmd)
case "reboot":
return q.handleReboot(cmd)
case "run":
return q.handleRun(cmd)
default:
if q.cfg.ListenMethod == "at" {
return "Unknown command: /" + cmd.Command, nil
}
return "", nil // Global mode: silently ignore unknown commands.
}
}
func (q *QQController) isAdmin(senderID int64) bool {
senderStr := fmt.Sprintf("%d", senderID)
for _, admin := range q.cfg.Admins {
if admin == senderStr {
return true
}
}
return false
}
func (q *QQController) handleList() (string, error) {
cfg := config.GetConfig()
params := template.BuildParamsFromServerList()
return template.Render(cfg.ControllerMessage.ServerList, params), nil
}
func (q *QQController) handleStatus(cmd controller.Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /status <uuid>", nil
}
uuid := cmd.Args[0]
tracker := node.GetTracker()
n, exists := tracker.GetNode(uuid)
if !exists {
return fmt.Sprintf("Server with UUID %s not found", uuid), nil
}
statusStr := "Offline"
if n.Online {
statusStr = "Online"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID))
sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
if n.LatestReport != nil {
r := n.LatestReport
sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage))
sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total))
sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total))
sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down))
sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime))
sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process))
if r.Message != "" {
sb.WriteString(fmt.Sprintf("Message: %s\n", r.Message))
}
}
return sb.String(), nil
}
func (q *QQController) handleShutdown(cmd controller.Command) (string, error) {
if !q.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 1 {
return "Usage: /shutdown <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "shutdown")
if err != nil {
return fmt.Sprintf("Error: %v", err), nil
}
return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil
}
func (q *QQController) handleReboot(cmd controller.Command) (string, error) {
if !q.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 1 {
return "Usage: /reboot <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "reboot")
if err != nil {
return fmt.Sprintf("Error: %v", err), nil
}
return fmt.Sprintf("Reboot command sent to server %s", uuid), nil
}
func (q *QQController) handleRun(cmd controller.Command) (string, error) {
if !q.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 2 {
return "Usage: /run <uuid|all> <command>", nil
}
uuidArg := cmd.Args[0]
command := cmd.Args[1]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
var uuids []string
if uuidArg == "all" {
tracker := node.GetTracker()
for _, n := range tracker.GetAllNodes() {
uuids = append(uuids, n.UUID)
}
} else {
uuids = []string{uuidArg}
}
taskID, err := client.ExecTask(uuids, command)
if err != nil {
return fmt.Sprintf("Error executing command: %v", err), nil
}
results, err := client.PollTaskResult(taskID)
if err != nil {
return fmt.Sprintf("Error getting results: %v", err), nil
}
cfg := config.GetConfig()
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results))
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil
}
func formatTaskResults(results []komari.TaskResult) string {
var sb strings.Builder
for _, r := range results {
sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client))
sb.WriteString(r.Result)
sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode))
}
return sb.String()
}
// SendStatusChange sends a status change notification via QQ. // SendStatusChange sends a status change notification via QQ.
func (q *QQController) SendStatusChange(change node.StatusChange) error { func (q *QQController) SendStatusChange(change node.StatusChange) error {
if !q.cfg.Enabled { if !q.cfg.Enabled {
+13 -171
View File
@@ -2,14 +2,12 @@ package pipes
import ( import (
"fmt" "fmt"
"strings"
"nukumizu-backend/config" "nukumizu-backend/config"
"nukumizu-backend/internal/komari" "nukumizu-backend/internal/controller"
"nukumizu-backend/internal/node" "nukumizu-backend/internal/node"
"nukumizu-backend/internal/template" "nukumizu-backend/internal/template"
"nukumizu-backend/postLog" "nukumizu-backend/postLog"
"nukumizu-backend/internal/controller"
) )
// TelegramController handles Telegram Bot interactions via long polling. // TelegramController handles Telegram Bot interactions via long polling.
@@ -70,184 +68,28 @@ func (t *TelegramController) pollLoop() {
postLog.Info("Telegram polling started") postLog.Info("Telegram polling started")
} }
// HandleCommand processes a bot command and returns a response string. // processCommand validates an incoming message as a command and hands the
func (t *TelegramController) HandleCommand(cmd controller.Command) (string, error) { // complete command to the unified processor. It returns the response text to
// reply with; an empty response means the message was discarded.
func (t *TelegramController) processCommand(cmd controller.Command) string {
// First check whether the received message is a command.
parsed, ok := controller.ParseCommand(cmd.RawText) parsed, ok := controller.ParseCommand(cmd.RawText)
if !ok { if !ok {
if t.cfg.ListenMethod == "at" { return "" // Not a command, discard.
return "Unknown command format. Use /command args", nil
}
return "", nil
} }
parsed.ChatID = cmd.ChatID parsed.ChatID = cmd.ChatID
parsed.ChatType = cmd.ChatType parsed.ChatType = cmd.ChatType
parsed.SenderID = cmd.SenderID parsed.SenderID = cmd.SenderID
return t.executeCommand(parsed) // Hand the complete command to the unified processor, which checks group
} // vs private, trusted groups, admin permissions, and executes it.
response, err := controller.GetManager().RouteCommand(parsed, t.cfg.TrustedGroups, t.cfg.Admins, t.cfg.ListenMethod)
func (t *TelegramController) executeCommand(cmd controller.Command) (string, error) {
switch cmd.Command {
case "list":
return t.handleList()
case "status":
return t.handleStatus(cmd)
case "shutdown":
return t.handleShutdown(cmd)
case "reboot":
return t.handleReboot(cmd)
case "run":
return t.handleRun(cmd)
default:
if t.cfg.ListenMethod == "at" {
return "Unknown command: /" + cmd.Command, nil
}
return "", nil
}
}
func (t *TelegramController) isAdmin(senderID int64) bool {
senderStr := fmt.Sprintf("%d", senderID)
for _, admin := range t.cfg.Admins {
if admin == senderStr {
return true
}
}
return false
}
func (t *TelegramController) handleList() (string, error) {
cfg := config.GetConfig()
params := template.BuildParamsFromServerList()
return template.Render(cfg.ControllerMessage.ServerList, params), nil
}
func (t *TelegramController) handleStatus(cmd controller.Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /status <uuid>", nil
}
uuid := cmd.Args[0]
tracker := node.GetTracker()
n, exists := tracker.GetNode(uuid)
if !exists {
return fmt.Sprintf("Server with UUID %s not found", uuid), nil
}
statusStr := "Offline"
if n.Online {
statusStr = "Online"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID))
sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
if n.LatestReport != nil {
r := n.LatestReport
sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage))
sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total))
sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total))
sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down))
sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime))
sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process))
}
return sb.String(), nil
}
func (t *TelegramController) handleShutdown(cmd controller.Command) (string, error) {
if !t.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 1 {
return "Usage: /shutdown <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "shutdown")
if err != nil { if err != nil {
return fmt.Sprintf("Error: %v", err), nil postLog.Error("Telegram command processing failed: " + err.Error())
return ""
} }
return response
return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil
}
func (t *TelegramController) handleReboot(cmd controller.Command) (string, error) {
if !t.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 1 {
return "Usage: /reboot <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "reboot")
if err != nil {
return fmt.Sprintf("Error: %v", err), nil
}
return fmt.Sprintf("Reboot command sent to server %s", uuid), nil
}
func (t *TelegramController) handleRun(cmd controller.Command) (string, error) {
if !t.isAdmin(cmd.SenderID) {
return "Permission denied: admin only", nil
}
if len(cmd.Args) < 2 {
return "Usage: /run <uuid|all> <command>", nil
}
uuidArg := cmd.Args[0]
command := cmd.Args[1]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
var uuids []string
if uuidArg == "all" {
tracker := node.GetTracker()
for _, n := range tracker.GetAllNodes() {
uuids = append(uuids, n.UUID)
}
} else {
uuids = []string{uuidArg}
}
taskID, err := client.ExecTask(uuids, command)
if err != nil {
return fmt.Sprintf("Error executing command: %v", err), nil
}
results, err := client.PollTaskResult(taskID)
if err != nil {
return fmt.Sprintf("Error getting results: %v", err), nil
}
cfg := config.GetConfig()
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTelegramResults(results))
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil
}
func formatTelegramResults(results []komari.TaskResult) string {
var sb strings.Builder
for _, r := range results {
sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client))
sb.WriteString(r.Result)
sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode))
}
return sb.String()
} }
// SendStatusChange sends a status change notification via Telegram. // SendStatusChange sends a status change notification via Telegram.
+198
View File
@@ -0,0 +1,198 @@
package controller
import (
"fmt"
"strings"
"nukumizu-backend/config"
"nukumizu-backend/internal/komari"
"nukumizu-backend/internal/node"
"nukumizu-backend/internal/template"
"nukumizu-backend/postLog"
)
// RouteCommand validates an incoming command against the pipe's authorization
// config (trusted groups / admins) and, if authorized, processes it through
// Trigger. It returns the response text to reply with; an empty response means
// the command was discarded.
//
// All command handling across pipes is unified here: pipes only decide whether
// a received message is a command, then hand the complete command over.
func (m *Manager) RouteCommand(cmd Command, trustedGroups, admins []string, listenMethod string) (string, error) {
// Check whether the command arrived in a group or a private chat.
if cmd.ChatType == "group" {
// Group commands are only honored from trusted groups, otherwise discard.
if !IsTrustedGroup(cmd.ChatID, trustedGroups) {
return "", nil
}
}
// Commands that require special permission must be sent by an admin.
if IsAdminCommand(cmd.Command) {
if !IsAdmin(cmd.SenderID, admins) {
return "", nil
}
}
response, err := m.Trigger(cmd)
if err != nil {
return "", err
}
// In "at" listen mode, give feedback for unknown commands.
if response == "" && listenMethod == "at" {
return "Unknown command: /" + cmd.Command, nil
}
return response, nil
}
// Trigger processes a parsed bot command and returns the response text.
// The actual command execution for every pipe is unified here.
func (m *Manager) Trigger(cmd Command) (string, error) {
cfg := config.GetConfig()
if cfg.System.DebugMode && cfg.Debug.ShowTriggerCmdEcho {
postLog.Debug("Trigger received command: /" + cmd.Command + " with args: " + strings.Join(cmd.Args, ", "))
}
switch cmd.Command {
case "list":
return handleList()
case "status":
return handleStatus(cmd)
case "shutdown":
return handleShutdown(cmd)
case "reboot":
return handleReboot(cmd)
case "run":
return handleRun(cmd)
default:
return "", nil
}
}
func handleList() (string, error) {
cfg := config.GetConfig()
params := template.BuildParamsFromServerList()
return template.Render(cfg.ControllerMessage.ServerList, params), nil
}
func handleStatus(cmd Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /status <uuid>", nil
}
uuid := cmd.Args[0]
tracker := node.GetTracker()
n, exists := tracker.GetNode(uuid)
if !exists {
return fmt.Sprintf("Server with UUID %s not found", uuid), nil
}
statusStr := "Offline"
if n.Online {
statusStr = "Online"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID))
sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
if n.LatestReport != nil {
r := n.LatestReport
sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage))
sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total))
sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total))
sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down))
sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime))
sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process))
if r.Message != "" {
sb.WriteString(fmt.Sprintf("Message: %s\n", r.Message))
}
}
return sb.String(), nil
}
func handleShutdown(cmd Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /shutdown <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "shutdown")
if err != nil {
return fmt.Sprintf("Error: %v", err), nil
}
return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil
}
func handleReboot(cmd Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /reboot <uuid>", nil
}
uuid := cmd.Args[0]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
_, err := client.ExecTask([]string{uuid}, "reboot")
if err != nil {
return fmt.Sprintf("Error: %v", err), nil
}
return fmt.Sprintf("Reboot command sent to server %s", uuid), nil
}
func handleRun(cmd Command) (string, error) {
if len(cmd.Args) < 2 {
return "Usage: /run <uuid|all> <command>", nil
}
uuidArg := cmd.Args[0]
command := cmd.Args[1]
client := komari.GetClient()
if client == nil {
return "Error: Komari client not initialized", nil
}
var uuids []string
if uuidArg == "all" {
tracker := node.GetTracker()
for _, n := range tracker.GetAllNodes() {
uuids = append(uuids, n.UUID)
}
} else {
uuids = []string{uuidArg}
}
taskID, err := client.ExecTask(uuids, command)
if err != nil {
return fmt.Sprintf("Error executing command: %v", err), nil
}
results, err := client.PollTaskResult(taskID)
if err != nil {
return fmt.Sprintf("Error getting results: %v", err), nil
}
cfg := config.GetConfig()
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results))
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil
}
func formatTaskResults(results []komari.TaskResult) string {
var sb strings.Builder
for _, r := range results {
sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client))
sb.WriteString(r.Result)
sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode))
}
return sb.String()
}
+70 -57
View File
@@ -1,57 +1,70 @@
package controller package controller
import ( import (
"strings" "fmt"
"strings"
"nukumizu-backend/config" )
"nukumizu-backend/postLog"
) // ParseCommand parses a raw message text into a Command.
// Format: /{{command}} {{args...}}
// Trigger is a placeholder for triggering commands across all controllers. func ParseCommand(rawText string) (cmd Command, ok bool) {
func (m *Manager) Trigger (command string, args []string) { rawText = strings.TrimSpace(rawText)
if config.GetConfig().System.DebugMode { if !strings.HasPrefix(rawText, "/") {
postLog.Debug("Received command: " + command + " with args: " + strings.Join(args, ", ")) return Command{}, false
} }
}
// Remove the leading slash and split.
// ParseCommand parses a raw message text into a Command. parts := strings.SplitN(rawText[1:], " ", 2)
// Format: /{{command}} {{args...}} cmd.Command = strings.ToLower(parts[0])
func ParseCommand(rawText string) (cmd Command, ok bool) { cmd.RawText = rawText
rawText = strings.TrimSpace(rawText)
if !strings.HasPrefix(rawText, "/") { if len(parts) > 1 {
return Command{}, false argStr := strings.TrimSpace(parts[1])
}
// Special handling for /run: first arg is uuid, rest is command.
// Remove the leading slash and split. if cmd.Command == "run" {
parts := strings.SplitN(rawText[1:], " ", 2) spaceIdx := strings.Index(argStr, " ")
cmd.Command = strings.ToLower(parts[0]) if spaceIdx > 0 {
cmd.RawText = rawText cmd.Args = []string{argStr[:spaceIdx], strings.TrimSpace(argStr[spaceIdx+1:])}
} else {
if len(parts) > 1 { cmd.Args = []string{argStr}
argStr := strings.TrimSpace(parts[1]) }
} else {
// Special handling for /run: first arg is uuid, rest is command. cmd.Args = strings.Fields(argStr)
if cmd.Command == "run" { }
spaceIdx := strings.Index(argStr, " ") }
if spaceIdx > 0 {
cmd.Args = []string{argStr[:spaceIdx], strings.TrimSpace(argStr[spaceIdx+1:])} return cmd, true
} else { }
cmd.Args = []string{argStr}
} // IsAdminCommand returns whether the given command requires admin privileges.
} else { func IsAdminCommand(command string) bool {
cmd.Args = strings.Fields(argStr) switch command {
} case "shutdown", "reboot", "run":
} return true
default:
return cmd, true return false
} }
}
// IsAdminCommand returns whether the given command requires admin privileges.
func IsAdminCommand(command string) bool { // IsAdmin returns whether the sender is present in the given admins list.
switch command { func IsAdmin(senderID int64, admins []string) bool {
case "shutdown", "reboot", "run": senderStr := fmt.Sprintf("%d", senderID)
return true for _, admin := range admins {
default: if admin == senderStr {
return false return true
} }
} }
return false
}
// IsTrustedGroup returns whether the chat ID is present in the given trusted groups list.
func IsTrustedGroup(chatID int64, trustedGroups []string) bool {
chatStr := fmt.Sprintf("%d", chatID)
for _, group := range trustedGroups {
if group == chatStr {
return true
}
}
return false
}