Every settings update answered with a bare success, so the console could not tell a change that took effect at once from one written to the file that the running program would keep ignoring until it was restarted — the listener address, the storage paths, the Komari dashboard URL. Classify the patch instead. config.RestartRequiredKeys expands a patch into the dot-separated paths of its leaves and intersects them with the settings main reads before it starts serving, and the settings endpoint returns that list as data.restartRequired. It is a pure function of the patch, so UpdateSettings keeps its signature and nothing else has to change; the write succeeds either way, and the field only says which edits are not live. Matching is by overlap rather than equality, so a patch that names a section — replacing it, or deleting it with a null — reports the startup-only keys inside it, while a sibling subtree like webhook.endpoints is not mistaken for webhook.enabled. The result is never nil, so an update with nothing to report serializes as [] rather than null and a client can iterate it without a guard. The console reads the field in ConfigSection and warns instead of confirming, naming the keys that are pending; Settings.vue's hints for the Komari URL, the listen address and the storage paths now say which of their fields is affected rather than labelling the whole card.
509 lines
33 KiB
Markdown
509 lines
33 KiB
Markdown
# Nukumizu Backend
|
|
|
|
Remote server monitoring and command execution subsystem for [Komari](https://www.komari.wiki).
|
|
|
|
Nukumizu connects to a Komari Dashboard instance, keeps an in-memory view of every monitored server, and pushes alerts to several notification channels. It also runs interactive bots on QQ (via [NapCat](https://napneko.github.io/), OneBot 11) and Telegram so operators can control servers from chat.
|
|
|
|
## Features
|
|
|
|
- **Komari integration** — logs into the Komari Dashboard, refreshes the node list on startup and every 5 minutes, and polls each node's latest status through the Komari WebSocket every 5 seconds.
|
|
- **Status tracking** — a thread-safe node tracker keeps the latest report and static info per server and detects `Online` / `Offline` transitions.
|
|
- **Remote command execution** — dispatches commands through the Komari task API and polls the result (1s interval, up to 60s timeout).
|
|
- **Interactive bots** — QQ (NapCat / OneBot 11) and Telegram bots for `/list`, `/status`, `/info`, `/run`, `/shutdown`, `/reboot`, and more, protected by an admin / trusted-group permission model.
|
|
- **Notification channels** — server status changes are pushed to every enabled channel: QQ, Telegram, Email (SMTP), [ntfy](https://ntfy.sh), and Webhook.
|
|
- **Incoming webhook API** — external applications can push their own alerts in via `POST /api/webhook/post/<name>`, and Nukumizu relays them to the channels that endpoint lists. Each endpoint carries its own token and target channels, and the API is served on a **separate listener** so it can be exposed without exposing the admin API.
|
|
- **Network proxy** — a global proxy URL can be enabled per controller (`networkUseProxy`) for HTTP, WebSocket, and even SMTP (HTTP CONNECT tunnel).
|
|
- **Customizable message templates** — every bot/notification message is rendered from a template in `config.json`, with Markdown formatting switched on per channel.
|
|
- **Storage** — SQLite (pure-Go driver) for `user.db` and `log.db`; safe on network shares (WAL disabled).
|
|
- **Dashboard API** — token-authenticated REST API plus an admin-only live log-streaming WebSocket.
|
|
- **Web console** — a Vue 3 admin UI for browsing nodes, editing `config.json`, managing bot trust and webhook endpoints, and tailing logs. The built bundle is embedded in the binary, so a single executable serves both the API and the console.
|
|
|
|
## How it works
|
|
|
|
1. On startup Nukumizu logs in to the Komari Dashboard. A failed login aborts the process.
|
|
2. It fetches the node list (name → UUID, static info) and stores it in memory, then opens a WebSocket to poll live status every 5 seconds. If the connection drops it reconnects with exponential backoff; after 5 failed attempts it notifies and keeps retrying.
|
|
3. The node tracker uses the first received snapshot as a baseline so a restart does not produce false "offline" alerts, then fires a status-change event for every real transition.
|
|
4. Each status-change event is rendered through the `SERVER_STATUS_CHANGED` template and sent to all enabled controllers. QQ / Telegram additionally receive a startup welcome and the initial server list.
|
|
5. Every 12 hours the Komari session is re-authenticated; the node list is refreshed every 5 minutes.
|
|
|
|
## Project structure
|
|
|
|
```
|
|
nukumizu-backend/
|
|
├── main.go # Entry point, startup sequence, graceful shutdown
|
|
├── router.go # HTTP route registration (main + webhook API)
|
|
├── config/
|
|
│ ├── config.go # Load config files, apply defaults
|
|
│ └── variables.go # Config schema structs + globals
|
|
├── global/
|
|
│ └── variables.go # Software build metadata (name/version/developer)
|
|
├── handler/
|
|
│ ├── user.go # /api/user/login, /api/user/register
|
|
│ ├── server.go # /api/server/list, getInfo, getStatus, exec
|
|
│ ├── settings.go # /api/settings/get, set
|
|
│ ├── webhook.go # /api/webhook/post/{name} (incoming webhook API)
|
|
│ ├── webhook_endpoints.go # /api/webhook/add, modify, delete, list
|
|
│ └── health.go # /health
|
|
├── database/
|
|
│ └── user.go # user.db (SQLite) user store
|
|
├── utils/
|
|
│ ├── auth.go # Token management, Auth middleware, JSON responses
|
|
│ └── middleware.go # Rate limit, CORS, XSS headers, WebSocket auth
|
|
├── postLog/ # Logging subsystem
|
|
│ ├── postLog.go # Leveled logger (stdout + broadcast)
|
|
│ ├── database.go # log.db (SQLite, one table per run)
|
|
│ ├── logBroadcaster.go # Fan-out to WebSocket clients
|
|
│ └── logSocketHandler.go # /api/system/getLogs WebSocket handler (admin only)
|
|
├── web/
|
|
│ ├── embed.go # Embeds the built console (web/dist) in the binary
|
|
│ ├── dist/ # Vite build output — generated, gitignored
|
|
│ └── handler.go # Static file serving + SPA fallback
|
|
├── internal/
|
|
│ ├── komari/
|
|
│ │ ├── client.go # Komari HTTP/JSON-RPC client (login, nodes, task exec/poll)
|
|
│ │ └── ws.go # Komari status WebSocket (poll + reconnect)
|
|
│ ├── node/
|
|
│ │ └── tracker.go # Thread-safe server state, status-change detection
|
|
│ ├── netproxy/
|
|
│ │ └── netproxy.go # Unified network proxy for controllers
|
|
│ ├── template/
|
|
│ │ └── template.go # Message template renderer ({{ variables }})
|
|
│ └── controller/
|
|
│ ├── controller.go # Manager, Controller / BotController interfaces, alerts
|
|
│ ├── trigger.go # Command parsing, authorization, routing
|
|
│ ├── processor.go # Command handlers
|
|
│ ├── utils.go
|
|
│ └── pipes/
|
|
│ ├── email.go # Email notification pipe
|
|
│ ├── ntfy.go # ntfy notification pipe
|
|
│ ├── webhook.go # Outgoing webhook notification pipe
|
|
│ ├── qq_napcat/
|
|
│ │ ├── qq.go # QQ (NapCat / OneBot 11) bot controller
|
|
│ │ └── napcat.go # NapCat WebSocket + HTTP API client
|
|
│ └── telegram/
|
|
│ ├── telegram.go # Telegram bot controller (go-telegram/bot, long polling)
|
|
│ └── send.go # Message sending / splitting (Telegram Markdown)
|
|
└── frontend/ # Vue 3 admin console (Vite)
|
|
├── index.html
|
|
├── vite.config.js # Dev server; proxies /api to the backend
|
|
└── src/
|
|
├── main.js # Bootstrap: theme + runtime flags
|
|
├── App.vue # Root component + toast host
|
|
├── api/index.js # Wrappers around the REST endpoints
|
|
├── router/index.js # Routes and the login guard
|
|
├── utils/ # http/auth/theme/toast/format/runtime helpers
|
|
├── components/ # Modal, Toggle, editors, ConfigSection, top bar, side bar
|
|
└── views/ # Login, Overview, Trusted, Settings, WebHooks, Logs
|
|
```
|
|
|
|
## Requirements
|
|
|
|
- Go **1.25** or newer
|
|
- [Node.js](https://nodejs.org) **22** or newer — only needed to build the web console; a prebuilt binary does not require it
|
|
- A running [Komari](https://www.komari.wiki) Dashboard instance reachable from this host
|
|
- For QQ: a [NapCat](https://napneko.github.io/) instance exposing an OneBot 11 WebSocket + HTTP endpoint
|
|
- For Telegram: a bot token from [@BotFather](https://t.me/BotFather)
|
|
|
|
Key dependencies: `github.com/go-telegram/bot`, `github.com/gorilla/websocket`, `gopkg.in/mail.v2`, `modernc.org/sqlite`.
|
|
|
|
## Configuration
|
|
|
|
There are two configuration files, both read from the working directory unless overridden:
|
|
|
|
| File | CLI flag | Default | Purpose |
|
|
|---|---|---|---|
|
|
| `config.json` | `-config` | `config.json` | Core settings: system, Komari, controllers, message templates |
|
|
| `bot_user_config.json` | `-bot-user-config` | `bot_user_config.json` | Per-bot admins / trusted groups and their notification preferences |
|
|
|
|
> Both files are in `.gitignore` because they contain credentials (Komari password, bot tokens, proxy auth). Start from the samples below and never commit real secrets.
|
|
|
|
### `config.json`
|
|
|
|
```json
|
|
{
|
|
"system": {
|
|
"debugMode": true,
|
|
"listenAddr": "0.0.0.0",
|
|
"listenPort": "8080",
|
|
"networkProxy": "http://127.0.0.1:7890"
|
|
},
|
|
"debug": {
|
|
"showNapcatMsg": false,
|
|
"showNapcatAction": false,
|
|
"showTelegramMsg": false,
|
|
"showTriggerCmdEcho": true,
|
|
"showKomariTaskEcho": false,
|
|
"napcatIgnoreSelfMsg": false
|
|
},
|
|
"komari": {
|
|
"dashboardURL": "https://status.example.com",
|
|
"account": {
|
|
"username": "admin",
|
|
"password": "CHANGE_ME"
|
|
}
|
|
},
|
|
"webhook": {
|
|
"enabled": false,
|
|
"listenAddr": "0.0.0.0",
|
|
"listenPort": "8081",
|
|
"endpoints": {
|
|
"example": {
|
|
"enabled": true,
|
|
"token": "CHANGE_ME",
|
|
"notifyPipes": ["qq(napcat)", "telegram", "email", "ntfy"]
|
|
}
|
|
}
|
|
},
|
|
"controllerMethod": {
|
|
"qq(napcat)": {
|
|
"enabled": false,
|
|
"markdown": false,
|
|
"networkUseProxy": false,
|
|
"napcatAddr": "127.0.0.1",
|
|
"napcatPort": "3000",
|
|
"napcatToken": "",
|
|
"botQQID": 0,
|
|
"listenMethod": "global"
|
|
},
|
|
"telegram": {
|
|
"enabled": false,
|
|
"markdown": true,
|
|
"networkUseProxy": false,
|
|
"botToken": "",
|
|
"listenMethod": "global"
|
|
},
|
|
"email": {
|
|
"enabled": false,
|
|
"markdown": false,
|
|
"networkUseProxy": false,
|
|
"smtpHost": "",
|
|
"smtpPort": 587,
|
|
"username": "",
|
|
"password": "",
|
|
"from": "",
|
|
"to": [],
|
|
"useTLS": true
|
|
},
|
|
"ntfy": {
|
|
"enabled": false,
|
|
"markdown": false,
|
|
"networkUseProxy": false,
|
|
"server": "https://ntfy.sh",
|
|
"topic": "",
|
|
"token": "",
|
|
"priority": "default"
|
|
},
|
|
"webhook": {
|
|
"enabled": false,
|
|
"markdown": false,
|
|
"networkUseProxy": false,
|
|
"url": "",
|
|
"method": "POST",
|
|
"headers": {},
|
|
"template": ""
|
|
}
|
|
},
|
|
"controllerMessage": {
|
|
"BOT_STARTED": "Nukumizu Alert Bot Started\nTime: {{ time }}\n- Software Version: {{ softwareVersion }}\n- Build Version: {{ softwareBuildVer }}\n- Commit Hash: {{ softwareCommitHash }}\n- Build Type: {{ softwareBuildType }}\n- Build Time: {{ softwareBuildTime }}\n- Developer: {{ softwareDeveloper }}",
|
|
"BOT_HELP": "Nukumizu Alert Bot Ver. {{ softwareVersion }}.{{ softwareBuildVer }}.{{ softwareCommitHash }}\nCommand Lists:\n- /help: Show this help message\n- /list: List all servers and show their status\n- /status <UUID>: Show specific server status\n- /info <UUID>: Show specific server info\n- /run <UUID> <command>: Execute command on specific server. If you type \"all\" in <UUID>, you will run the command on all servers.\n- /shutdown <UUID>: Shutdown specific server.\n- /reboot <UUID>: Reboot specific server.\n- /getip <UUID>: Get specific server IP address.",
|
|
"TG_BOT_START": "Welcome to use Nukumizu Alert Bot!\nUse `/help` to get command list.",
|
|
"SERVER_STATUS_CHANGED": "Server Status Changed Alert\n{{ serverName }} - {{ upStatus }}\n- Event: {{ event }}\n- Server Name: {{ serverName }}\n- Message: {{ message }}\n- Time: {{ time }}",
|
|
"SERVER_LIST": "All server list:\n- Online:\n{{ list.onlineServers }}\n- Offline:\n{{ list.offlineServers }}",
|
|
"SERVER_EXECUTE_RESULT": "Command execute result:\n- Server ID: {{ serverName }}\n- Command: {{ command }}\n-----**Result**-----\n\n{{ result }}\n----------\n\n- Time: {{ time }}"
|
|
},
|
|
"dataPath": "./data",
|
|
"dbPath": "./db"
|
|
}
|
|
```
|
|
|
|
Field notes:
|
|
|
|
- `system.networkProxy` is a **system-wide** proxy URL. A controller only uses it when its own `networkUseProxy` is `true`. Applied to Telegram HTTP polling, NapCat HTTP/WebSocket, ntfy and webhook requests, and Email SMTP (tunneled via HTTP CONNECT).
|
|
- `webhook` configures the **incoming** webhook API (see [Incoming webhook API](#incoming-webhook-api)); `controllerMethod.webhook` configures the outgoing webhook notification channel. They are independent.
|
|
- `markdown` is a per-channel switch on all five channels. With it `false` (the default) every rendered value is inserted as plain text; with it `true` the values meant to be read verbatim (UUIDs, event messages, commands, command results, alert source and alert content) are wrapped in Markdown code spans / fenced blocks. Nothing is inferred from the channel name, so a channel only ever gets the formatting you asked for — turn it off for a channel whose platform does not render Markdown. On Telegram it also picks the `parse_mode`: with `markdown` off, messages are sent without one, so text containing `*` or `_` is delivered as-is rather than rejected by the API as malformed Markdown.
|
|
- `controllerMethod.qq(napcat).listenMethod` / `telegram.listenMethod` — see [Bot recognition modes](#bot-recognition-modes).
|
|
- `debug` toggles verbose per-channel message/action logging; these only matter in debug builds / `debugMode`.
|
|
- `email.useTLS` is kept for configuration compatibility.
|
|
- `dataPath` / `dbPath` default to `./data` and `./db`; `user.db` and `log.db` are created under `dbPath`.
|
|
- Missing keys fall back to built-in defaults (host `0.0.0.0`, port `8080`, webhook API `0.0.0.0:8081`, no webhook endpoints, NapCat `127.0.0.1:3000`, ntfy server `https://ntfy.sh`, webhook method `POST`, etc.). Message templates have built-in fallbacks too. `markdown` defaults to `false`, so add it explicitly for Telegram (see the sample above) to keep its formatting.
|
|
|
|
### `bot_user_config.json`
|
|
|
|
Admins and trusted groups are defined **per bot channel** and map a member ID to that member's notification preferences:
|
|
|
|
```json
|
|
{
|
|
"qq(napcat)": {
|
|
"admins": {
|
|
"123456789": {
|
|
"event_status_notify": true,
|
|
"event_bot_started": true,
|
|
"event_reply": true
|
|
}
|
|
},
|
|
"trustedGroups": {
|
|
"987654321": {
|
|
"event_status_notify": true,
|
|
"event_bot_started": false,
|
|
"event_reply": true
|
|
}
|
|
}
|
|
},
|
|
"telegram": {
|
|
"admins": {
|
|
"user_handle": {
|
|
"event_status_notify": true,
|
|
"event_bot_started": true,
|
|
"event_reply": true
|
|
}
|
|
},
|
|
"trustedGroups": {
|
|
"-1001234567890": {
|
|
"event_status_notify": true,
|
|
"event_bot_started": false,
|
|
"event_reply": true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- **QQ**: member IDs are QQ numbers; group IDs are the numeric group number.
|
|
- **Telegram**: member IDs may be `@username` (resolved to the numeric user ID once that user has messaged the bot) or the numeric user ID; group IDs are the numeric chat ID (supergroups are negative).
|
|
- Per-member options:
|
|
- `event_status_notify` — receive server `Online`/`Offline` push notifications.
|
|
- `event_bot_started` — receive the automatic startup message (welcome + initial server list).
|
|
- `event_reply` — reserved for opting out of replies to that member's own commands.
|
|
- Admins of a channel also appear in every trusted-group/private-chat context where the bot sends notifications.
|
|
|
|
### Message templates
|
|
|
|
`controllerMessage` templates are rendered before sending. Available variables (channels with `markdown: true` additionally wrap the verbatim values in Markdown — see the field notes above):
|
|
|
|
| Variable | Meaning |
|
|
|---|---|
|
|
| `{{ time }}` | Current server time |
|
|
| `{{ serverName }}` | Targeted server name |
|
|
| `{{ serverUUID }}` | Targeted server UUID |
|
|
| `{{ upStatus }}` | `Online` / `Offline` |
|
|
| `{{ event }}` | Status change event (`Online` / `Offline`) |
|
|
| `{{ message }}` | Message accompanying a status event |
|
|
| `{{ command }}` | The command that was executed |
|
|
| `{{ result }}` | Command execution result |
|
|
| `{{ list.onlineServers }}` | Formatted list of online servers (`- Name (uuid)`) |
|
|
| `{{ list.offlineServers }}` | Formatted list of offline servers |
|
|
| `{{ softwareVersion }}`, `{{ softwareBuildVer }}`, `{{ softwareCommitHash }}`, `{{ softwareBuildType }}`, `{{ softwareBuildTime }}`, `{{ softwareDeveloper }}`, `{{ softwareDescription }}` | Build metadata (commit hash and build time are injected at compile time) |
|
|
|
|
### What applies without a restart
|
|
|
|
Saving settings applies most of them immediately. How each group takes effect:
|
|
|
|
| Settings | How it applies |
|
|
|---|---|
|
|
| `controllerMethod` (all five channels) | Every channel is **rebuilt**: the running controllers are stopped and a fresh set is built from the new settings. A channel that owns a connection reconnects — Telegram re-runs its `getMe` handshake, NapCat opens a new WebSocket — so notifications sent during the swap are lost. The rebuild only happens when this section actually changed; saving a message template does not disturb the channels. |
|
|
| `controllerMessage`, `debug`, `bot_user_config.json`, `bot_node_config.json`, `webhook.endpoints` | Picked up as they are used; nothing is restarted. |
|
|
| `system.debugMode` | Applies to both behavior and log filtering. |
|
|
| `system.networkProxy` | Read on every request and every dial, so a new address reaches channels that are already running. Only the per-channel `networkUseProxy` opt-in is fixed when a channel is built, so toggling that still needs the rebuild above. |
|
|
| `system.listenAddr` / `listenPort`, `webhook.enabled` / `listenAddr` / `listenPort`, `dataPath`, `dbPath`, `komari.dashboardURL` | **Applied at startup only.** Saving them changes the file and the in-memory configuration but not the running listener, database or Komari client — restart to apply. |
|
|
|
|
## API
|
|
|
|
Success responses follow the envelope `{"success": true, "message": "...", "data": {...}}`, with the payload nested under a single `data` key. Error responses use `{"success": false, "message": "..."}`. `message` may be omitted on success when there is nothing to report.
|
|
|
|
### Authentication
|
|
|
|
Requests are authenticated with HTTP headers:
|
|
|
|
| Header | Meaning |
|
|
|---|---|
|
|
| `X-Token` | Token returned by login/register. Held in memory only (lost on restart). |
|
|
| `X-Timestamp` | Unix timestamp (seconds); rejected if more than ±30 minutes from server time. **Skipped entirely when `system.debugMode` is `true`.** |
|
|
|
|
Tokens idle for more than 1 hour are expired (cleaned every 10 minutes); any authenticated call refreshes the timer. Permission levels: `None`, `bot`, `admin`. Endpoints requiring `bot` accept both `bot` and `admin` tokens. Currently registration/login always issue `admin`-level tokens.
|
|
|
|
Browser WebSocket handshakes cannot carry custom headers, so `/api/system/getLogs` also accepts the same credentials as `?token=` and `?timestamp=` query parameters (headers still take precedence when both are present). Only `admin` tokens are accepted; the timestamp is checked once at handshake time, so an accepted connection stays open past its tolerance window. Because the query string can leak into proxy and access logs, a token-carrying WebSocket URL should be treated as a secret.
|
|
|
|
### Endpoints
|
|
|
|
| Endpoint | Method | Permission | Description |
|
|
|---|---|---|---|
|
|
| `/api/user/login` | POST | None | Log in. Body `{username, password}`. Returns `data: {token, userID, username, level, registerDate}`. |
|
|
| `/api/user/register` | POST | None | Register the first user. Body `{username, password}`. Only allowed while no user exists; otherwise `403`. Returns `data: {token, userID, username, level}`. |
|
|
| `/api/server/list` | GET | bot / admin | List all monitored servers. |
|
|
| `/api/server/getInfo` | GET | admin | Static server info (mirrors the Bot's `/info`). Query `?uuid=<uuid>` (or `all`). Returns `data: {<uuid>: {uuid, name, info}}` — one entry per requested server. `404` for an unknown single uuid. |
|
|
| `/api/server/getStatus` | GET | admin | Live server status (mirrors the Bot's `/status`). Query `?uuid=<uuid>` (or `all`). Returns `data: {<uuid>: {uuid, name, online, report}}`; `report` is `null` when the node has not reported yet. `404` for an unknown single uuid. |
|
|
| `/api/server/exec` | POST | bot / admin | Execute a command. Body `{uuid: [<uuid>...], command}`. Dispatches a Komari task and polls until completion (or timeout). Returns `data: {taskID, results}`. |
|
|
| `/api/settings/get` | GET | admin | `?type=global\|bot_user_config\|bot_node_config` | Returns `data: {config}`, where `config` is the selected config file's content (same layout as the JSON file). |
|
|
| `/api/settings/set` | POST | admin | `?type=<same types>` + JSON body of partial updates, e.g. `{"system":{"debugMode":true}}` | Deep-merges the body into the selected config file, persists it, and reloads it in memory. Only the given keys change; arrays replace. Returns `data: {type, restartRequired}`, where `restartRequired` lists the keys the update changed that are only read at startup (see [What applies without a restart](#what-applies-without-a-restart)) — the write succeeds regardless, this only says which edits are not live yet. Always an array, empty when everything took effect. |
|
|
| `/api/webhook/add` | POST | admin | Add an incoming webhook endpoint. Body `{name, enabled?, token?, notifyPipes?}` — only the fields given are stored, the rest start at their defaults. `409` when the name is already configured. |
|
|
| `/api/webhook/modify` | POST | admin | Change an existing endpoint. Body `{name, ...}` — the fields given are the fields that change (same partial-update rule as `/api/settings/set`, but scoped to one endpoint). `404` for an unknown name, `400` when no other field is given. |
|
|
| `/api/webhook/delete` | POST | admin | Remove an endpoint. Body `{name}`. `404` for an unknown name. |
|
|
| `/api/webhook/list` | GET | admin | Every configured incoming webhook endpoint, keyed by name, under `data.endpoints`. |
|
|
| `/health` | GET | None | Health check. Returns `data: {status, database}`. |
|
|
| `/api/system/getLogs` | WebSocket | admin | Streams logs. Sends the last 100 buffered entries, then live `{level, content, timestamp}` events. Credentials via `X-Token`/`X-Timestamp` headers or `?token=`/`?timestamp=` query parameters; a failed check answers with the JSON error and no upgrade. |
|
|
|
|
Middleware applied to the whole server:
|
|
|
|
- **Rate limit** — token bucket, 100 requests/minute per client IP.
|
|
- **CORS** — `Access-Control-Allow-Origin: *`, allows `Content-Type`, `X-Token`, `X-Timestamp`, `Authorization`.
|
|
- **Security headers** — `X-XSS-Protection`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, a restrictive CSP.
|
|
- **WebSocket auth** — `utils.WebSocketAuthMiddleware` is attached to `/api/system/getLogs` (route-level, not global): it authenticates the upgrade request and requires an `admin` token before the connection is handed to the log handler.
|
|
|
|
### Incoming webhook API
|
|
|
|
A listener of its own, so external applications can be pointed at it without being able to reach the admin API. It is switched on with `webhook.enabled` and binds `webhook.listenAddr:webhook.listenPort` (default `0.0.0.0:8081`); that half of the configuration is applied at startup, while `webhook.endpoints` is re-read whenever the config is reloaded. Only the rate limit and CORS middleware apply here — no session token is involved.
|
|
|
|
The console's **WebHooks** page manages the listener settings and the endpoints. The outgoing WebHook notification channel (`controllerMethod.webhook`) stays on the Settings page with the other notification channels, since it is one of them.
|
|
|
|
| Endpoint | Method | Permission | Description |
|
|
|---|---|---|---|
|
|
| `/api/webhook/post/<name>` | POST | Endpoint token | Relay an alert to the channels the endpoint lists in `notifyPipes`. Body `{token, subject, content}`. Returns `data: {endpoint, channels}`. |
|
|
|
|
Every entry under `webhook.endpoints` is one endpoint, addressed by its key as the last path segment: the key `example` is served at `POST /api/webhook/post/example`. The `post/` segment keeps the endpoints' own namespace separate from the management routes (`/api/webhook/add` and friends), which live on the admin listener. Endpoints are managed over the admin API (`/api/webhook/add`, `modify`, `delete` and `list` — see [Endpoints](#endpoints)), which writes the same `webhook.endpoints` section of `config.json`; a newly added endpoint accepts requests as soon as the configuration is reloaded, without a restart. An endpoint holds:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `enabled` | Whether the endpoint accepts requests. A disabled endpoint answers `403`. |
|
|
| `token` | Shared secret the caller sends as the `token` body field; compared in constant time. An endpoint with an empty token answers `500` instead of accepting requests from anyone. |
|
|
| `notifyPipes` | The channels the alert is delivered to, named as in `controllerMethod`: `qq(napcat)`, `telegram`, `email`, `ntfy`, `webhook`. A channel that is unknown or disabled is skipped and reported. |
|
|
|
|
The management API accepts exactly these three fields. A request naming any other field, or giving one of them the wrong type (`enabled` must be a boolean, `token` a string, `notifyPipes` an array of strings), is refused with `400` instead of being written to `config.json` — a field the program does not understand must not end up in the file. A `name` must be non-empty and free of `/`, since it becomes the last segment of the endpoint URL.
|
|
|
|
The alert is rendered per channel as:
|
|
|
|
```
|
|
{{ subject }}
|
|
- Source: {{ source }}
|
|
- Content:
|
|
{{ content }}
|
|
|
|
- Time: {{ time }}
|
|
Sent by Nukumizu Alert System
|
|
```
|
|
|
|
`{{ source }}` is the endpoint name, so recipients can tell which application triggered the alert. On a channel with `markdown: true` the source is wrapped in inline code and the content in a fenced code block; `{{ subject }}` and `{{ time }}` stay plain.
|
|
|
|
Status codes: `200` delivered, `400` malformed body or empty `subject`/`content`, `401` wrong token, `403` endpoint disabled, `404` unknown endpoint name, `405` non-POST request, `500` endpoint has no token configured, `502` no channel accepted the alert.
|
|
|
|
## Bots
|
|
|
|
QQ (NapCat) and Telegram bots share one command engine and authorization pipeline, implemented in `internal/controller/`. NapCat speaks OneBot 11 (WebSocket event stream + HTTP actions); Telegram uses `go-telegram/bot` long polling.
|
|
|
|
### Permission model
|
|
|
|
- **Trusted group**: any command issued *inside a group* is only answered if that group is listed in `trustedGroups`. Messages in other groups are ignored.
|
|
- **Admin commands**: `/shutdown`, `/reboot`, and `/run` additionally require the *sender* to be listed in `admins`.
|
|
- **Private chat**: non-admin commands (`/help`, `/list`, `/status`, `/info`, `/getip`) are answered for any private sender; admin commands still require admin.
|
|
|
|
### Bot recognition modes
|
|
|
|
- `global` — the bot watches all messages in trusted groups and reacts to recognized commands without being mentioned. Unknown `/`-commands are silently ignored.
|
|
- `at` — the bot only reacts when it is mentioned (QQ `@`, Telegram `@botname`). In this mode an unknown command produces an `Unknown command: /…` reply.
|
|
|
|
### Commands
|
|
|
|
| Command | Permission | Description |
|
|
|---|---|---|
|
|
| `/help` | All | Show the help message (`BOT_HELP` template). |
|
|
| `/list` | All | List all servers with online/offline state (`SERVER_LIST` template). |
|
|
| `/status <uuid>` | All | Live report for a server (CPU, RAM, disk, network, uptime, processes). |
|
|
| `/info <uuid>` | All | Static info for a server (OS, kernel, CPU, RAM, swap, disk, billing, tags). |
|
|
| `/getip <uuid>` | All | IPv4 / IPv6 address of a server. |
|
|
| `/shutdown <uuid>` | Admin | Shut the server down via the Komari task API. |
|
|
| `/reboot <uuid>` | Admin | Reboot the server via the Komari task API. |
|
|
| `/run <uuid\|all> <command>` | Admin | Execute a command on one server or on all servers (`all`), then report the result (`SERVER_EXECUTE_RESULT` template). |
|
|
| `/start` | All | Telegram only — sends the `TG_BOT_START` welcome message. |
|
|
|
|
## Notification channels
|
|
|
|
QQ and Telegram are *interactive* channels. Email, ntfy, and webhook are **status-only** channels — they receive server status-change alerts but cannot run commands. On startup, the welcome message and initial server list are delivered only to the bot channels (QQ / Telegram), honoring each member's `event_bot_started` preference.
|
|
|
|
All five channels can also carry an alert submitted by an external application through the [incoming webhook API](#incoming-webhook-api). A bot channel delivers it to the groups and admins configured for that channel; a status-only channel delivers it to its configured destination (mail recipients, ntfy topic, outgoing webhook URL). Markdown formatting is decided per channel by its `markdown` setting, never by the channel's name.
|
|
|
|
## Building
|
|
|
|
Requires Go 1.25+ and — to build the web console — Node.js 22+.
|
|
|
|
Helper scripts in the repo root build the Vue console first, then compile the backend with it embedded. They also bake the current git commit and build time into the binary via `-ldflags`. The name states the **target** platform, and each target has a Windows (`.bat`) and a Linux/macOS (`.sh`) flavor: run the flavor for the host you are building on, since every script cross-compiles to its target.
|
|
|
|
| Script | Output |
|
|
|---|---|
|
|
| `build-linux-x86_64.sh` / `build-linux-x86_64.bat` | `nukumizu-linux-amd64` |
|
|
| `build-win-x86_64.sh` / `build-win-x86_64.bat` | `nukumizu-windows-amd64.exe` |
|
|
|
|
```bash
|
|
# Linux / macOS
|
|
./build-linux-x86_64.sh
|
|
./build-win-x86_64.sh
|
|
|
|
# Windows
|
|
build-linux-x86_64.bat
|
|
build-win-x86_64.bat
|
|
```
|
|
|
|
The console is **embedded in the binary**. Vite writes it to `web/dist` and `web/embed.go` compiles that directory in with `go:embed`, so the executable serves the whole frontend on its own — copy it anywhere, with neither `frontend/` nor `web/dist` next to it, and `/` still returns the console. The build scripts run `npm ci` when `frontend/node_modules` is missing and `npm run build` on every run, so they need Node.js 22+ on the build machine (not on the machine that runs the binary).
|
|
|
|
Building the backend therefore requires the console to have been built at least once: `web/dist` is a generated, gitignored directory, and `go build` fails with `pattern all:dist: no matching files found` until it exists. Any `build-*` script handles that ordering for you.
|
|
|
|
Equivalent manual builds:
|
|
|
|
```bash
|
|
# 1. Console (once per frontend change)
|
|
cd frontend && npm ci && npm run build && cd ..
|
|
|
|
# 2. Backend
|
|
# Linux / macOS
|
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
|
go build -ldflags "-X main.CommitHash=$(git rev-parse --short HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
-o nukumizu-linux-amd64 .
|
|
|
|
# Windows
|
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
|
go build -ldflags "-X main.CommitHash=<commit> -X main.BuildTime=<utc-time>" \
|
|
-o nukumizu-windows-amd64.exe .
|
|
```
|
|
|
|
`main.CommitHash` and `main.BuildTime` are surfaced in logs and in the `BOT_STARTED` message.
|
|
|
|
CI (`.github/workflows/build.yml`) runs both flavors — `.sh` on `ubuntu-latest`, `.bat` on `windows-latest` — and uploads the two self-contained binaries as artifacts.
|
|
|
|
## Running
|
|
|
|
Both `config.json` and `bot_user_config.json` must exist in the working directory (or be passed explicitly):
|
|
|
|
```bash
|
|
# Development (uses go run, so config files must be in the CWD)
|
|
run.bat
|
|
|
|
# Or build first, then run a binary
|
|
./nukumizu-linux-amd64 -config config.json -bot-user-config bot_user_config.json
|
|
```
|
|
|
|
On startup the program logs in to Komari, loads node state, connects the status WebSocket, then starts each enabled controller and the HTTP server on `listenAddr:listenPort`. Press `Ctrl+C` for a graceful shutdown.
|
|
|
|
### Frontend development
|
|
|
|
`run.bat` only runs the Go backend — it does not build the console, so `web/dist` must already exist (run any `build-*` script once, or `npm run build` in `frontend/`) or `go run` fails to compile. While working on the frontend, run the two sides separately:
|
|
|
|
```bash
|
|
# Terminal 1 — backend (API + WebSocket) on :8080
|
|
run.bat
|
|
|
|
# Terminal 2 — Vite dev server with hot reload on :5173
|
|
cd frontend
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Open http://localhost:5173. The dev server proxies `/api` — including the log WebSocket — to `http://127.0.0.1:8080`; point it elsewhere with `NUKUMIZU_API` if the backend listens on another address:
|
|
|
|
```bash
|
|
NUKUMIZU_API=http://192.168.1.10:8080 npm run dev
|
|
```
|
|
|
|
For a production build the Go server serves the embedded console itself, on the normal listen address — see [Building](#building). Vite is not involved at runtime, so `npm run build` alone does not change what a running binary serves: rebuild the binary to pick up frontend changes.
|
|
|
|
## License
|
|
|
|
See [LICENSE](LICENSE).
|