Compare commits
7
Commits
Ver.0.2.2.7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ff252f45 | ||
|
|
03b67dc3c0 | ||
|
|
c5c2c2e2f3 | ||
|
|
064524e991 | ||
|
|
a7f5fac1d4 | ||
|
|
a1a69d95f5 | ||
|
|
734866164b |
No files matched your search
@@ -1,42 +1,53 @@
|
||||
# Nukumizu Backend
|
||||
|
||||
English | [简体中文](README.zh-CN.md)
|
||||
|
||||
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.
|
||||
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/) or any OneBot 11 implementation) and Telegram so operators can control servers from chat. A Vue console embedded in the binary provides the same overview and settings from a browser.
|
||||
|
||||
## 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.
|
||||
- **Komari integration** — logs into the Komari Dashboard, refreshing the node list on startup and every 5 minutes, and pulls each node's latest status over the Komari WebSocket every 5 seconds.
|
||||
- **Resilient upstream access** — login and node fetching are retried up to 6 attempts with linear backoff (0.5s × attempt) on transport errors, `429` and `5xx`. Credential failures, `4xx` responses and JSON-RPC errors fail immediately instead of hammering the dashboard.
|
||||
- **Status tracking** — a thread-safe node tracker keeps the latest report and static info per server and detects `Online` / `Offline` transitions. The first snapshot after startup is treated as a baseline, so a restart does not produce a wave of false "offline" alerts.
|
||||
- **Per-node notification control** — `bot_node_config.json` can silence status pushes for an individual node, and can hold an `Offline` notification for a configurable window so a quick reconnect produces no alert at all (flap inhibition).
|
||||
- **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.
|
||||
- **Interactive bots** — QQ (OneBot 11, e.g. NapCat) and Telegram bots for `/list`, `/status`, `/info`, `/run`, `/shutdown`, `/reboot`, and more, protected by an admin / trusted-group permission model.
|
||||
- **Notification channels** — 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).
|
||||
- **Hot reload** — saving settings applies almost everything immediately; the few keys that are only read at startup are reported back as `restartRequired`.
|
||||
- **Storage** — SQLite (pure-Go driver) for `user.db` and `log.db`; the rollback journal is used instead of WAL so the files stay safe on network shares.
|
||||
- **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.
|
||||
- **Web console** — a Vue 3 admin UI for browsing nodes and their per-node options, editing settings, 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.
|
||||
- **Tests** — the Go packages ship with unit tests (`go test ./...`), including the retry, flap-inhibition and settings-merge paths.
|
||||
|
||||
## 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.
|
||||
1. On startup Nukumizu loads `config.json`, `bot_user_config.json` and (optionally) `bot_node_config.json`, then logs in to the Komari Dashboard. Login retries transport errors / `429` / `5xx` up to 6 times; a login that still fails aborts the process.
|
||||
2. It fetches the node list (name → UUID, static info) and stores it in memory, then opens a WebSocket to `/api/rpc2` and requests the latest status every 5 seconds. If the connection drops it reconnects with exponential backoff (1s, 2s, 4s … capped at 30s); after 5 failed attempts it logs an error and keeps retrying every 30 seconds.
|
||||
3. The node tracker waits for the initial snapshot covering every known node before enabling notifications (with a 2-minute safety net), and always treats the very first snapshot as the startup baseline — no alerts are generated for the state found at boot.
|
||||
4. Each real transition is rendered through the `SERVER_STATUS_CHANGED` template and sent to all enabled controllers. Before that, the per-node gates from `bot_node_config.json` are applied: `enableStatusNotify` can drop the change entirely, and `flap_tolerant` holds an `Offline` for N seconds — if the node returns in time, both the `Offline` and the `Online` are suppressed.
|
||||
5. QQ / Telegram additionally receive a startup welcome message and the initial server list once the first refresh completes, honoring each member's `event_bot_started` preference.
|
||||
6. 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
|
||||
├── main.go # Entry point, startup sequence, background tasks, graceful shutdown
|
||||
├── router.go # HTTP route registration (main + webhook API)
|
||||
├── config/
|
||||
│ ├── config.go # Load config files, apply defaults
|
||||
│ └── variables.go # Config schema structs + globals
|
||||
│ ├── config.go # Load config files, apply defaults, per-node helpers
|
||||
│ ├── variables.go # Config schema structs + atomic config singletons
|
||||
│ ├── settings.go # Settings file read/merge/write, startup-only key detection
|
||||
│ ├── webhook.go # Incoming webhook endpoint helpers (add/modify/delete)
|
||||
│ ├── reload.go # Reload hooks (logger, controller rebuild)
|
||||
│ └── *_test.go # Settings merge, concurrency and flap tests
|
||||
├── global/
|
||||
│ └── variables.go # Software build metadata (name/version/developer)
|
||||
│ └── variables.go # Build metadata (name/version/developer) + config paths
|
||||
├── handler/
|
||||
│ ├── user.go # /api/user/login, /api/user/register
|
||||
│ ├── server.go # /api/server/list, getInfo, getStatus, exec
|
||||
@@ -48,7 +59,7 @@ nukumizu-backend/
|
||||
│ └── 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
|
||||
│ └── middleware.go # Rate limit, CORS, security headers
|
||||
├── postLog/ # Logging subsystem
|
||||
│ ├── postLog.go # Leveled logger (stdout + broadcast)
|
||||
│ ├── database.go # log.db (SQLite, one table per run)
|
||||
@@ -63,13 +74,13 @@ nukumizu-backend/
|
||||
│ │ ├── 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
|
||||
│ │ └── tracker.go # Thread-safe server state, baseline gating, transitions
|
||||
│ ├── netproxy/
|
||||
│ │ └── netproxy.go # Unified network proxy for controllers
|
||||
│ ├── template/
|
||||
│ │ └── template.go # Message template renderer ({{ variables }})
|
||||
│ └── controller/
|
||||
│ ├── controller.go # Manager, Controller / BotController interfaces, alerts
|
||||
│ ├── controller.go # Manager, interfaces, alert routing, flap inhibition
|
||||
│ ├── trigger.go # Command parsing, authorization, routing
|
||||
│ ├── processor.go # Command handlers
|
||||
│ ├── utils.go
|
||||
@@ -78,8 +89,8 @@ nukumizu-backend/
|
||||
│ ├── ntfy.go # ntfy notification pipe
|
||||
│ ├── webhook.go # Outgoing webhook notification pipe
|
||||
│ ├── qq_onebot/
|
||||
│ │ ├── qq.go # QQ (NapCat / OneBot 11) bot controller
|
||||
│ │ └── onebot.go # OneBot 11 WebSocket client (events + API)
|
||||
│ │ ├── qq.go # QQ controller (OneBot 11, e.g. NapCat)
|
||||
│ │ └── onebot.go # OneBot 11 WebSocket client (events + actions)
|
||||
│ └── telegram/
|
||||
│ ├── telegram.go # Telegram bot controller (go-telegram/bot, long polling)
|
||||
│ └── send.go # Message sending / splitting (Telegram Markdown)
|
||||
@@ -91,31 +102,33 @@ nukumizu-backend/
|
||||
├── App.vue # Root component + toast host
|
||||
├── api/index.js # Wrappers around the REST endpoints
|
||||
├── router/index.js # Routes and the login guard
|
||||
├── styles/ # theme.css + ui.css
|
||||
├── utils/ # http/auth/theme/toast/format/runtime helpers
|
||||
├── components/ # Modal, Toggle, editors, ConfigSection, top bar, side bar
|
||||
└── views/ # Login, Overview, Trusted, Settings, WebHooks, Logs
|
||||
└── views/ # Login, Layout, Overview, Trusted, Settings, WebHooks, Logs
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go **1.25** or newer
|
||||
- Go **1.27.3** or newer (see `go.mod`)
|
||||
- [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 forward WebSocket endpoint (`/api` and `/event`)
|
||||
- For QQ: an OneBot 11 implementation exposing a forward WebSocket endpoint, e.g. [NapCat](https://napneko.github.io/) (`/api` and `/event`)
|
||||
- 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:
|
||||
Three JSON files are read from the working directory. **There are no command-line flags**: the paths are fixed (`config.json`, `bot_user_config.json`, `bot_node_config.json`), so start the program from the directory that holds them.
|
||||
|
||||
| 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 |
|
||||
| File | Required | Purpose |
|
||||
|---|---|---|
|
||||
| `config.json` | Yes | Core settings: system, debug switches, Komari, webhook listener, controllers, message templates, storage paths |
|
||||
| `bot_user_config.json` | Yes | Per-bot admins / trusted groups and their notification preferences |
|
||||
| `bot_node_config.json` | No | Per-node options (notification switch, flap window). Auto-created on the first node fetch |
|
||||
|
||||
> 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.
|
||||
> All three files are in `.gitignore` because they contain credentials (Komari password, bot tokens, proxy auth). Start from the samples below and never commit real secrets. `bot_node_config.json` is safe to keep, but it is ignored along with the others.
|
||||
|
||||
### `config.json`
|
||||
|
||||
@@ -128,12 +141,13 @@ There are two configuration files, both read from the working directory unless o
|
||||
"networkProxy": "http://127.0.0.1:7890"
|
||||
},
|
||||
"debug": {
|
||||
"showNapcatMsg": false,
|
||||
"showNapcatAction": false,
|
||||
"showOneBotMsg": false,
|
||||
"showOneBotAction": false,
|
||||
"showTelegramMsg": false,
|
||||
"showTriggerCmdEcho": true,
|
||||
"showKomariTaskEcho": false,
|
||||
"napcatIgnoreSelfMsg": false
|
||||
"oneBotIgnoreSelfMsg": false,
|
||||
"showFlapTolerantEcho": false
|
||||
},
|
||||
"komari": {
|
||||
"dashboardURL": "https://status.example.com",
|
||||
@@ -150,18 +164,18 @@ There are two configuration files, both read from the working directory unless o
|
||||
"example": {
|
||||
"enabled": true,
|
||||
"token": "CHANGE_ME",
|
||||
"notifyPipes": ["qq(napcat)", "telegram", "email", "ntfy"]
|
||||
"notifyPipes": ["qq_onebot", "telegram", "email", "ntfy"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controllerMethod": {
|
||||
"qq(napcat)": {
|
||||
"qq_onebot": {
|
||||
"enabled": false,
|
||||
"markdown": false,
|
||||
"networkUseProxy": false,
|
||||
"napcatAddr": "127.0.0.1",
|
||||
"napcatPort": "3000",
|
||||
"napcatToken": "",
|
||||
"oneBotAddr": "127.0.0.1",
|
||||
"oneBotPort": "3000",
|
||||
"oneBotToken": "",
|
||||
"botQQID": 0,
|
||||
"listenMethod": "global"
|
||||
},
|
||||
@@ -218,14 +232,21 @@ There are two configuration files, both read from the working directory unless o
|
||||
|
||||
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, QQ OneBot WebSocket, ntfy and webhook requests, and Email SMTP (tunneled via HTTP CONNECT).
|
||||
- `controllerMethod` is keyed by channel name: `qq_onebot`, `telegram`, `email`, `ntfy`, `webhook`. The same names are used in `notifyPipes` and in `bot_user_config.json`. The QQ channel speaks OneBot 11, so `oneBotAddr` / `oneBotPort` / `oneBotToken` point at the OneBot implementation (NapCat by default on `127.0.0.1:3000`).
|
||||
- `system.networkProxy` is a **system-wide** proxy URL. A controller only uses it when its own `networkUseProxy` is `true`. Applied to Telegram HTTP polling, the QQ OneBot 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`.
|
||||
- `controllerMethod.qq_onebot.listenMethod` / `telegram.listenMethod` — see [Bot recognition modes](#bot-recognition-modes).
|
||||
- `debug` toggles verbose per-module logging; these only matter when `system.debugMode` is `true`:
|
||||
- `showOneBotMsg` / `showOneBotAction` — incoming OneBot messages / outgoing OneBot API calls.
|
||||
- `showTelegramMsg` — incoming Telegram updates.
|
||||
- `showTriggerCmdEcho` — the parsed command handed to the trigger.
|
||||
- `showKomariTaskEcho` — Komari task creation and polling.
|
||||
- `oneBotIgnoreSelfMsg` — ignore the bot's own messages echoed back by OneBot.
|
||||
- `showFlapTolerantEcho` — the flap-inhibition decisions (held / released / reported).
|
||||
- `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.
|
||||
- `dbPath` defaults to `./db`; `user.db` and `log.db` are created under it. `dataPath` defaults to `./data` and is reserved for future file storage — nothing is written there yet.
|
||||
- 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, OneBot `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`
|
||||
|
||||
@@ -233,7 +254,7 @@ Admins and trusted groups are defined **per bot channel** and map a member ID to
|
||||
|
||||
```json
|
||||
{
|
||||
"qq(napcat)": {
|
||||
"qq_onebot": {
|
||||
"admins": {
|
||||
"123456789": {
|
||||
"event_status_notify": true,
|
||||
@@ -272,10 +293,31 @@ Admins and trusted groups are defined **per bot channel** and map a member ID to
|
||||
- **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_bot_started` — receive the automatic startup messages (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.
|
||||
|
||||
### `bot_node_config.json`
|
||||
|
||||
Per-node options, keyed by the node UUID that Komari reports:
|
||||
|
||||
```json
|
||||
{
|
||||
"0f2a9c1e-1234-4a5b-8c9d-000000000001": {
|
||||
"enableStatusNotify": false,
|
||||
"flap_tolerant": 30
|
||||
},
|
||||
"7d3b8f20-5678-4c9d-9e0f-000000000002": {}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `enableStatusNotify` | `true` | When `false`, **no** status-change notification is sent for this node — through chat bots and notification pipes alike. Omit it (or the whole entry) to keep notifications on. |
|
||||
| `flap_tolerant` | `0` | Seconds an `Offline` notification is held (float allowed, capped at 86400). If the node comes back within the window, both the `Offline` and the `Online` are suppressed; if it is still down when the window elapses, the `Offline` is sent then. `0` or an absent field notifies immediately. |
|
||||
|
||||
The file is auto-generated: every node fetch (`FetchNodes` — startup, reconnect and the 5-minute refresh) adds newly seen UUIDs as `{}` and never overwrites an existing entry, so hand-written options survive. Missing or empty files are fine — every node then keeps the defaults above. The **Nodes** page of the console edits both options, or use `POST /api/settings/set?type=bot_node_config`.
|
||||
|
||||
### 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):
|
||||
@@ -300,11 +342,11 @@ 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. |
|
||||
| `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, OneBot 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. A flap window or notification switch edited while an `Offline` is held is re-read when the hold fires. |
|
||||
| `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. |
|
||||
| `system.listenAddr` / `listenPort`, `webhook.enabled` / `listenAddr` / `listenPort`, `komari.dashboardURL`, `dataPath`, `dbPath` | **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/settings/set` reports them back in `restartRequired`. |
|
||||
|
||||
## API
|
||||
|
||||
@@ -329,12 +371,12 @@ Browser WebSocket handshakes cannot carry custom headers, so `/api/system/getLog
|
||||
|---|---|---|---|
|
||||
| `/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/list` | GET | bot / admin | List all monitored servers as rendered text (`data.list`, same output as the bot's `/list`). |
|
||||
| `/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/settings/get` | GET | admin | `?type=global\|bot_user_config\|bot_node_config` — returns `data: {config}`, 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 file, persists it, and reloads it in memory. Only the given keys change; arrays replace, and a `null` deletes a key. 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. |
|
||||
@@ -344,10 +386,10 @@ Browser WebSocket handshakes cannot carry custom headers, so `/api/system/getLog
|
||||
|
||||
Middleware applied to the whole server:
|
||||
|
||||
- **Rate limit** — token bucket, 100 requests/minute per client IP.
|
||||
- **Rate limit** — token bucket, 100 requests/minute per client IP (`429` when exceeded).
|
||||
- **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.
|
||||
- **WebSocket auth** — the log endpoint is wrapped in an admin-only auth middleware at route level, so an unauthorized client is rejected before the upgrade.
|
||||
|
||||
### Incoming webhook API
|
||||
|
||||
@@ -359,15 +401,15 @@ The console's **WebHooks** page manages the listener settings and the endpoints.
|
||||
|---|---|---|---|
|
||||
| `/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:
|
||||
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, 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. |
|
||||
| `notifyPipes` | The channels the alert is delivered to, named as in `controllerMethod`: `qq_onebot`, `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 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. Request bodies are capped at 1 MiB.
|
||||
|
||||
The alert is rendered per channel as:
|
||||
|
||||
@@ -387,7 +429,7 @@ Status codes: `200` delivered, `400` malformed body or empty `subject`/`content`
|
||||
|
||||
## Bots
|
||||
|
||||
QQ (NapCat) and Telegram bots share one command engine and authorization pipeline, implemented in `internal/controller/`. NapCat speaks OneBot 11 over WebSocket (event stream + actions); Telegram uses `go-telegram/bot` long polling.
|
||||
QQ (OneBot 11) and Telegram bots share one command engine and authorization pipeline, implemented in `internal/controller/`. The QQ controller speaks OneBot 11 over WebSocket (event stream + actions), so it works with NapCat or any compatible implementation; Telegram uses `go-telegram/bot` long polling.
|
||||
|
||||
### Permission model
|
||||
|
||||
@@ -416,13 +458,15 @@ QQ (NapCat) and Telegram bots share one command engine and authorization pipelin
|
||||
|
||||
## 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.
|
||||
QQ and Telegram are *interactive* channels. Email, ntfy, and webhook are **status-only** channels — they receive 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.
|
||||
|
||||
Status-change notifications pass through the per-node gates in `bot_node_config.json` first (`enableStatusNotify`, `flap_tolerant`), so both chat bots and status-only channels see exactly the same filtered stream.
|
||||
|
||||
## Building
|
||||
|
||||
Requires Go 1.25+ and — to build the web console — Node.js 22+.
|
||||
Requires Go 1.27.3+ 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.
|
||||
|
||||
@@ -469,25 +513,28 @@ CI (`.github/workflows/build.yml`) runs both flavors — `.sh` on `ubuntu-latest
|
||||
|
||||
## Running
|
||||
|
||||
Both `config.json` and `bot_user_config.json` must exist in the working directory (or be passed explicitly):
|
||||
All three config files are read from the working directory; there are no path flags:
|
||||
|
||||
```bash
|
||||
# Development (uses go run, so config files must be in the CWD)
|
||||
run.bat
|
||||
# Development (go run, so the config files must be in the CWD)
|
||||
./run.sh # Linux / macOS
|
||||
run.bat # Windows
|
||||
|
||||
# Or build first, then run a binary
|
||||
./nukumizu-linux-amd64 -config config.json -bot-user-config bot_user_config.json
|
||||
# Or build first, then run the binary
|
||||
./nukumizu-linux-amd64
|
||||
```
|
||||
|
||||
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.
|
||||
`run.sh` / `run.bat` only run the Go backend — they do not build the console, so `web/dist` must already exist (run any `build-*` script once, or `npm run build` in `frontend/`) or the build fails.
|
||||
|
||||
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. The incoming webhook listener starts separately when `webhook.enabled` is `true`; a failure to bind it is logged without stopping the rest of the program.
|
||||
|
||||
### 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:
|
||||
While working on the frontend, run the two sides separately:
|
||||
|
||||
```bash
|
||||
# Terminal 1 — backend (API + WebSocket) on :8080
|
||||
run.bat
|
||||
./run.sh
|
||||
|
||||
# Terminal 2 — Vite dev server with hot reload on :5173
|
||||
cd frontend
|
||||
@@ -503,6 +550,18 @@ 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.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
The suite runs against the config, handler, node-tracker, controller and pipe packages. It needs no Komari instance or network access.
|
||||
|
||||
## Changelog
|
||||
|
||||
Release notes and the commits behind each version live in [changeLogs.md](changeLogs.md).
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
# Nukumizu 后端
|
||||
|
||||
[English](README.md) | 简体中文
|
||||
|
||||
面向 [Komari](https://www.komari.wiki) 的远程服务器监控与命令执行子系统。
|
||||
|
||||
Nukumizu 会连接 Komari 面板,在内存中维护所有被监控服务器的状态,并把告警推送到多个通知渠道。同时它还在 QQ(通过 [NapCat](https://napneko.github.io/) 或其他 OneBot 11 实现)和 Telegram 上运行交互式机器人,让你可以直接在聊天里控制服务器。前端控制台(Vue 3)编译后内嵌进二进制,浏览器里也能完成同样的监控与配置。
|
||||
|
||||
## 功能
|
||||
|
||||
- **Komari 集成** — 登录 Komari 面板,启动时及之后每 5 分钟刷新节点列表,并通过 Komari WebSocket 每 5 秒拉取一次各节点最新状态。
|
||||
- **上游请求重试** — 登录与节点获取在遇到传输层错误、`429` 或 `5xx` 时最多重试 6 次,采用线性退避(0.5s × 第几次)。凭据错误、`4xx` 响应和 JSON-RPC 错误会立即失败,不会反复冲击面板。
|
||||
- **状态跟踪** — 线程安全的节点跟踪器保存每个服务器的最新报告与静态信息,并检测 `Online` / `Offline` 状态切换。启动后的第一份快照被视为基线,因此重启不会产生一堆误报的“离线”告警。
|
||||
- **单节点通知控制** — `bot_node_config.json` 可以单独关闭某个节点的状态推送,也可以把 `Offline` 通知暂缓一段可配置的时间:节点在窗口内恢复时,这条告警完全不会发出(抖动抑制)。
|
||||
- **远程命令执行** — 通过 Komari 任务接口下发命令,并轮询结果(每秒一次,最长 60 秒超时)。
|
||||
- **交互式机器人** — QQ(OneBot 11,例如 NapCat)与 Telegram 机器人,支持 `/list`、`/status`、`/info`、`/run`、`/shutdown`、`/reboot` 等命令,并带有管理员 / 可信群组的权限模型。
|
||||
- **通知渠道** — 状态变更会推送到所有已启用的渠道:QQ、Telegram、邮件(SMTP)、[ntfy](https://ntfy.sh) 和 Webhook。
|
||||
- **入站 Webhook API** — 外部应用可以通过 `POST /api/webhook/post/<name>` 推送自己的告警,Nukumizu 会转发给该端点配置的渠道。每个端点有独立的 token 和目标渠道,且该 API 运行在**独立监听端口**上,可以只对外开放它而不暴露管理 API。
|
||||
- **网络代理** — 可配置全局代理地址,并在每个渠道上通过 `networkUseProxy` 单独开启;支持 HTTP、WebSocket,甚至 SMTP(通过 HTTP CONNECT 隧道)。
|
||||
- **可自定义的消息模板** — 所有机器人 / 通知消息都由 `config.json` 中的模板渲染,Markdown 格式按渠道单独开关。
|
||||
- **热重载** — 保存设置后绝大部分配置立即生效;少数只在启动时读取的键会通过 `restartRequired` 返回给调用方。
|
||||
- **存储** — `user.db` 与 `log.db` 使用 SQLite(纯 Go 驱动);采用回滚日志而非 WAL,避免在网络共享盘上损坏数据库。
|
||||
- **管理 API** — 基于 token 的 REST API,以及仅管理员可用的实时日志 WebSocket。
|
||||
- **Web 控制台** — Vue 3 管理界面,可浏览节点及其单节点选项、编辑设置、管理机器人信任与 Webhook 端点、实时查看日志。构建产物内嵌在二进制中,单个可执行文件即可同时提供 API 与控制台。
|
||||
- **测试** — 各 Go 包带有单元测试(`go test ./...`),覆盖重试、抖动抑制和设置合并等路径。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 启动时 Nukumizu 加载 `config.json`、`bot_user_config.json` 和(可选的)`bot_node_config.json`,然后登录 Komari 面板。登录遇到传输错误 / `429` / `5xx` 时最多重试 6 次;仍然失败则进程退出。
|
||||
2. 获取节点列表(名称 → UUID、静态信息)并存入内存,随后连接 `/api/rpc2` 的 WebSocket,每 5 秒请求一次最新状态。连接断开后按指数退避重连(1s、2s、4s……最大 30s);连续 5 次失败会记录错误日志,并继续每 30 秒重试。
|
||||
3. 节点跟踪器会等待覆盖所有已知节点的首份快照后再启用通知(另有两分钟兜底),并且始终把第一份快照当作启动基线 —— 不会因为启动时的状态而产生告警。
|
||||
4. 真实的在线状态切换会通过 `SERVER_STATUS_CHANGED` 模板渲染,并发送给所有已启用的渠道。在此之前会先经过 `bot_node_config.json` 的单节点开关:`enableStatusNotify` 可以直接丢弃这次变更,`flap_tolerant` 会把 `Offline` 暂缓 N 秒 —— 节点在窗口内恢复时,`Offline` 和随后的 `Online` 都不会发送。
|
||||
5. 首次状态刷新完成后,QQ / Telegram 还会收到启动欢迎消息和初始服务器列表,并遵循每个成员的 `event_bot_started` 偏好。
|
||||
6. 每 12 小时重新登录一次 Komari;节点列表每 5 分钟刷新一次。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
nukumizu-backend/
|
||||
├── main.go # 入口、启动流程、后台任务、优雅退出
|
||||
├── router.go # HTTP 路由注册(主监听 + webhook 监听)
|
||||
├── config/
|
||||
│ ├── config.go # 加载配置文件、填充默认值、单节点选项读取
|
||||
│ ├── variables.go # 配置结构体 + 原子配置单例
|
||||
│ ├── settings.go # 设置文件读取/合并/写入、启动项检测
|
||||
│ ├── webhook.go # 入站 Webhook 端点增删改辅助函数
|
||||
│ ├── reload.go # 重载钩子(日志级别、渠道重建)
|
||||
│ └── *_test.go # 设置合并、并发与抖动抑制测试
|
||||
├── global/
|
||||
│ └── variables.go # 构建元信息(名称/版本/开发者)+ 配置路径
|
||||
├── 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}(入站 Webhook API)
|
||||
│ ├── webhook_endpoints.go # /api/webhook/add、modify、delete、list
|
||||
│ └── health.go # /health
|
||||
├── database/
|
||||
│ └── user.go # user.db(SQLite)用户存储
|
||||
├── utils/
|
||||
│ ├── auth.go # Token 管理、鉴权中间件、JSON 响应
|
||||
│ └── middleware.go # 限流、CORS、安全响应头
|
||||
├── postLog/ # 日志子系统
|
||||
│ ├── postLog.go # 分级日志(标准输出 + 广播)
|
||||
│ ├── database.go # log.db(SQLite,每次运行一张表)
|
||||
│ ├── logBroadcaster.go # 向 WebSocket 客户端分发
|
||||
│ └── logSocketHandler.go # /api/system/getLogs WebSocket(仅管理员)
|
||||
├── web/
|
||||
│ ├── embed.go # 将构建好的控制台(web/dist)嵌入二进制
|
||||
│ ├── dist/ # Vite 构建输出 —— 生成物,已 gitignore
|
||||
│ └── handler.go # 静态文件服务 + SPA 回退
|
||||
├── internal/
|
||||
│ ├── komari/
|
||||
│ │ ├── client.go # Komari HTTP/JSON-RPC 客户端(登录、节点、任务执行/轮询)
|
||||
│ │ └── ws.go # Komari 状态 WebSocket(轮询 + 重连)
|
||||
│ ├── node/
|
||||
│ │ └── tracker.go # 线程安全节点状态、基线门控、状态切换
|
||||
│ ├── netproxy/
|
||||
│ │ └── netproxy.go # 各渠道统一网络代理
|
||||
│ ├── template/
|
||||
│ │ └── template.go # 消息模板渲染({{ 变量 }})
|
||||
│ └── controller/
|
||||
│ ├── controller.go # Manager、接口、告警路由、抖动抑制
|
||||
│ ├── trigger.go # 命令解析、鉴权、路由
|
||||
│ ├── processor.go # 命令处理
|
||||
│ ├── utils.go
|
||||
│ └── pipes/
|
||||
│ ├── email.go # 邮件通知渠道
|
||||
│ ├── ntfy.go # ntfy 通知渠道
|
||||
│ ├── webhook.go # 出站 Webhook 通知渠道
|
||||
│ ├── qq_onebot/
|
||||
│ │ ├── qq.go # QQ 渠道(OneBot 11,例如 NapCat)
|
||||
│ │ └── onebot.go # OneBot 11 WebSocket 客户端(事件 + API)
|
||||
│ └── telegram/
|
||||
│ ├── telegram.go # Telegram 机器人(go-telegram/bot,长轮询)
|
||||
│ └── send.go # 消息发送 / 拆分(Telegram Markdown)
|
||||
└── frontend/ # Vue 3 管理控制台(Vite)
|
||||
├── index.html
|
||||
├── vite.config.js # 开发服务器;把 /api 代理到后端
|
||||
└── src/
|
||||
├── main.js # 启动:主题 + 运行时开关
|
||||
├── App.vue # 根组件 + toast 容器
|
||||
├── api/index.js # REST 接口封装
|
||||
├── router/index.js # 路由与登录守卫
|
||||
├── styles/ # theme.css + ui.css
|
||||
├── utils/ # http/auth/theme/toast/format/runtime 辅助
|
||||
├── components/ # Modal、Toggle、编辑器、ConfigSection、顶栏、侧栏
|
||||
└── views/ # Login、Layout、Overview、Trusted、Settings、WebHooks、Logs
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Go **1.27.3** 或更高版本(见 `go.mod`)
|
||||
- [Node.js](https://nodejs.org) **22** 或更高版本 —— 仅在构建 Web 控制台时需要,预编译的二进制不需要
|
||||
- 一台可从本机访问的 [Komari](https://www.komari.wiki) 面板
|
||||
- QQ:一个提供正向 WebSocket 端点的 OneBot 11 实现,例如 [NapCat](https://napneko.github.io/)(`/api` 与 `/event`)
|
||||
- Telegram:来自 [@BotFather](https://t.me/BotFather) 的 bot token
|
||||
|
||||
主要依赖:`github.com/go-telegram/bot`、`github.com/gorilla/websocket`、`gopkg.in/mail.v2`、`modernc.org/sqlite`。
|
||||
|
||||
## 配置
|
||||
|
||||
程序从工作目录读取三个 JSON 文件。**没有命令行参数**:文件名固定为 `config.json`、`bot_user_config.json`、`bot_node_config.json`,因此请在存放这些文件的目录下启动程序。
|
||||
|
||||
| 文件 | 是否必需 | 用途 |
|
||||
|---|---|---|
|
||||
| `config.json` | 是 | 核心配置:系统、调试开关、Komari、Webhook 监听、渠道、消息模板、存储路径 |
|
||||
| `bot_user_config.json` | 是 | 各机器人的管理员 / 可信群组及其通知偏好 |
|
||||
| `bot_node_config.json` | 否 | 单节点选项(通知开关、抖动抑制窗口)。首次获取节点时自动创建 |
|
||||
|
||||
> 三个文件都在 `.gitignore` 中,因为它们包含凭据(Komari 密码、bot token、代理认证)。请参考下面的示例,切勿提交真实密钥。`bot_node_config.json` 本身不含敏感信息,但也一并被忽略。
|
||||
|
||||
### `config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"system": {
|
||||
"debugMode": true,
|
||||
"listenAddr": "0.0.0.0",
|
||||
"listenPort": "8080",
|
||||
"networkProxy": "http://127.0.0.1:7890"
|
||||
},
|
||||
"debug": {
|
||||
"showOneBotMsg": false,
|
||||
"showOneBotAction": false,
|
||||
"showTelegramMsg": false,
|
||||
"showTriggerCmdEcho": true,
|
||||
"showKomariTaskEcho": false,
|
||||
"oneBotIgnoreSelfMsg": false,
|
||||
"showFlapTolerantEcho": 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_onebot", "telegram", "email", "ntfy"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controllerMethod": {
|
||||
"qq_onebot": {
|
||||
"enabled": false,
|
||||
"markdown": false,
|
||||
"networkUseProxy": false,
|
||||
"oneBotAddr": "127.0.0.1",
|
||||
"oneBotPort": "3000",
|
||||
"oneBotToken": "",
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `controllerMethod` 以渠道名为键:`qq_onebot`、`telegram`、`email`、`ntfy`、`webhook`。同样的名称也用于 `notifyPipes` 和 `bot_user_config.json`。QQ 渠道使用 OneBot 11 协议,`oneBotAddr` / `oneBotPort` / `oneBotToken` 指向 OneBot 实现(NapCat 默认在 `127.0.0.1:3000`)。
|
||||
- `system.networkProxy` 是**全局**代理地址。只有当渠道自身的 `networkUseProxy` 为 `true` 时才会使用它,作用于 Telegram HTTP 轮询、QQ OneBot WebSocket、ntfy 与 Webhook 请求,以及邮件 SMTP(通过 HTTP CONNECT 隧道)。
|
||||
- `webhook` 配置的是**入站** Webhook API(见 [入站 Webhook API](#入站-webhook-api));`controllerMethod.webhook` 配置的是出站 Webhook 通知渠道。两者互不相关。
|
||||
- `markdown` 是五个渠道各自独立的开关。为 `false`(默认)时所有渲染值都以纯文本插入;为 `true` 时,需要原样阅读的值(UUID、事件消息、命令、命令结果、告警来源与内容)会被包裹为 Markdown 行内代码 / 代码块。程序不会根据渠道名推断,因此渠道只会得到你明确要求的格式 —— 平台不支持 Markdown 时请关闭它。在 Telegram 上它同时决定 `parse_mode`:关闭 `markdown` 时不发送 `parse_mode`,含 `*` 或 `_` 的文本会原样送达,而不会被 API 以“Markdown 格式错误”拒绝。
|
||||
- `controllerMethod.qq_onebot.listenMethod` / `telegram.listenMethod` —— 见 [机器人响应模式](#机器人响应模式)。
|
||||
- `debug` 控制各模块的详细日志,仅在 `system.debugMode` 为 `true` 时生效:
|
||||
- `showOneBotMsg` / `showOneBotAction` —— 收到的 OneBot 消息 / 发出的 OneBot API 调用。
|
||||
- `showTelegramMsg` —— 收到的 Telegram 更新。
|
||||
- `showTriggerCmdEcho` —— 交给触发器解析后的命令。
|
||||
- `showKomariTaskEcho` —— Komari 任务的创建与轮询。
|
||||
- `oneBotIgnoreSelfMsg` —— 忽略 OneBot 回显的机器人自身消息。
|
||||
- `showFlapTolerantEcho` —— 抖动抑制的决策(暂缓 / 释放 / 补发)。
|
||||
- `email.useTLS` 仅为配置兼容而保留。
|
||||
- `dbPath` 默认为 `./db`,`user.db` 与 `log.db` 会创建在其中。`dataPath` 默认为 `./data`,目前尚未使用,预留给后续的文件存储。
|
||||
- 缺失的键会使用内置默认值(地址 `0.0.0.0`、端口 `8080`、Webhook API `0.0.0.0:8081`、无 Webhook 端点、OneBot `127.0.0.1:3000`、ntfy 服务器 `https://ntfy.sh`、Webhook 方法 `POST` 等)。消息模板也有内置默认值。`markdown` 默认为 `false`,需要 Telegram 格式时请像示例那样显式开启。
|
||||
|
||||
### `bot_user_config.json`
|
||||
|
||||
管理员与可信群组按**机器人渠道**分别定义,并把成员 ID 映射到该成员的通知偏好:
|
||||
|
||||
```json
|
||||
{
|
||||
"qq_onebot": {
|
||||
"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**:成员 ID 为 QQ 号;群 ID 为数字群号。
|
||||
- **Telegram**:成员 ID 可以是 `@username`(该用户给机器人发过消息后会解析为数字用户 ID)或数字用户 ID;群 ID 为数字聊天 ID(超级群为负数)。
|
||||
- 每个成员的选项:
|
||||
- `event_status_notify` —— 接收服务器 `Online`/`Offline` 推送。
|
||||
- `event_bot_started` —— 接收启动时的自动消息(欢迎语 + 初始服务器列表)。
|
||||
- `event_reply` —— 预留,用于关闭对该成员命令的自动回复。
|
||||
- 某渠道的管理员也会出现在机器人发送通知的所有可信群组 / 私聊上下文中。
|
||||
|
||||
### `bot_node_config.json`
|
||||
|
||||
单节点选项,以 Komari 上报的节点 UUID 为键:
|
||||
|
||||
```json
|
||||
{
|
||||
"0f2a9c1e-1234-4a5b-8c9d-000000000001": {
|
||||
"enableStatusNotify": false,
|
||||
"flap_tolerant": 30
|
||||
},
|
||||
"7d3b8f20-5678-4c9d-9e0f-000000000002": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `enableStatusNotify` | `true` | 为 `false` 时该节点的状态变更**不会**推送 —— 聊天机器人和通知渠道都不会发送。省略该字段(或整个条目)即为开启。 |
|
||||
| `flap_tolerant` | `0` | `Offline` 通知的暂缓秒数(允许小数,上限 86400)。节点在窗口内恢复时,`Offline` 与 `Online` 都会被抑制;窗口结束时仍未恢复则补发 `Offline`。为 `0` 或省略时立即通知。 |
|
||||
|
||||
该文件由程序自动维护:每次获取节点(`FetchNodes` —— 启动、重连和每 5 分钟刷新)都会把新出现的 UUID 以 `{}` 形式加入,且从不覆盖已有条目,因此手写的选项不会被冲掉。文件缺失或为空也没问题 —— 所有节点使用上述默认值。可通过控制台的 **Nodes** 页面修改这两项,或调用 `POST /api/settings/set?type=bot_node_config`。
|
||||
|
||||
### 消息模板
|
||||
|
||||
`controllerMessage` 中的模板会在发送前渲染。可用变量如下(`markdown: true` 的渠道还会对需要原样阅读的值加 Markdown 包裹,见上文):
|
||||
|
||||
| 变量 | 含义 |
|
||||
|---|---|
|
||||
| `{{ time }}` | 服务器当前时间 |
|
||||
| `{{ serverName }}` | 目标服务器名称 |
|
||||
| `{{ serverUUID }}` | 目标服务器 UUID |
|
||||
| `{{ upStatus }}` | `Online` / `Offline` |
|
||||
| `{{ event }}` | 状态变更事件(`Online` / `Offline`) |
|
||||
| `{{ message }}` | 状态事件附带的消息 |
|
||||
| `{{ command }}` | 被执行的命令 |
|
||||
| `{{ result }}` | 命令执行结果 |
|
||||
| `{{ list.onlineServers }}` | 格式化后的在线服务器列表(`- 名称 (uuid)`) |
|
||||
| `{{ list.offlineServers }}` | 格式化后的离线服务器列表 |
|
||||
| `{{ softwareVersion }}`、`{{ softwareBuildVer }}`、`{{ softwareCommitHash }}`、`{{ softwareBuildType }}`、`{{ softwareBuildTime }}`、`{{ softwareDeveloper }}`、`{{ softwareDescription }}` | 构建元信息(commit hash 与构建时间在编译期注入) |
|
||||
|
||||
### 无需重启即可生效的配置
|
||||
|
||||
保存设置后,大部分配置立即生效:
|
||||
|
||||
| 配置 | 生效方式 |
|
||||
|---|---|
|
||||
| `controllerMethod`(全部五个渠道) | 所有渠道会被**重建**:停止正在运行的控制器,再按新设置构建一套。持有连接的渠道会重新连接 —— Telegram 重新执行 `getMe` 握手,OneBot 重新建立 WebSocket —— 因此切换期间的推送会丢失。仅当该配置节确实发生变化时才重建;只保存消息模板不会影响渠道。 |
|
||||
| `controllerMessage`、`debug`、`bot_user_config.json`、`bot_node_config.json`、`webhook.endpoints` | 使用时即时读取,不会重启任何东西。若在 `Offline` 暂缓期间修改通知开关或抖动窗口,补发时会重新读取。 |
|
||||
| `system.debugMode` | 同时影响行为与日志过滤。 |
|
||||
| `system.networkProxy` | 每次请求、每次拨号都会重新读取,因此新地址对已在运行的渠道也生效。只有渠道级的 `networkUseProxy` 在构建渠道时固定,切换它仍需上面的重建。 |
|
||||
| `system.listenAddr` / `listenPort`、`webhook.enabled` / `listenAddr` / `listenPort`、`komari.dashboardURL`、`dataPath`、`dbPath` | **仅在启动时生效。** 保存会写入文件并更新内存配置,但不会改变正在运行的监听、数据库或 Komari 客户端 —— 需要重启。`/api/settings/set` 会通过 `restartRequired` 返回这些键。 |
|
||||
|
||||
## API
|
||||
|
||||
成功响应统一为 `{"success": true, "message": "...", "data": {...}}`,负载放在单个 `data` 键下。错误响应为 `{"success": false, "message": "..."}`。成功且无额外信息时 `message` 可以省略。
|
||||
|
||||
### 鉴权
|
||||
|
||||
请求通过 HTTP 头鉴权:
|
||||
|
||||
| 请求头 | 含义 |
|
||||
|---|---|
|
||||
| `X-Token` | 登录/注册返回的 token。仅保存在内存中(重启后失效)。 |
|
||||
| `X-Timestamp` | Unix 时间戳(秒);与服务器时间相差超过 ±30 分钟会被拒绝。**当 `system.debugMode` 为 `true` 时完全跳过检查。** |
|
||||
|
||||
空闲超过 1 小时的 token 会失效(每 10 分钟清理一次);任何已鉴权的调用都会刷新计时。权限级别:`None`、`bot`、`admin`。要求 `bot` 的接口接受 `bot` 与 `admin` 两种 token。目前注册/登录一律签发 `admin` 级别 token。
|
||||
|
||||
浏览器的 WebSocket 握手无法携带自定义请求头,因此 `/api/system/getLogs` 也接受 `?token=` 与 `?timestamp=` 查询参数(两者都存在时请求头优先)。该接口仅接受 `admin` token;时间戳只在握手时检查一次,已建立的连接不受后续容差窗口影响。查询串可能进入代理与访问日志,带 token 的 WebSocket URL 应视为机密。
|
||||
|
||||
### 接口列表
|
||||
|
||||
| 接口 | 方法 | 权限 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `/api/user/login` | POST | None | 登录。请求体 `{username, password}`。返回 `data: {token, userID, username, level, registerDate}`。 |
|
||||
| `/api/user/register` | POST | None | 注册首个用户。请求体 `{username, password}`。仅在尚无用户时允许,否则 `403`。返回 `data: {token, userID, username, level}`。 |
|
||||
| `/api/server/list` | GET | bot / admin | 以渲染后的文本列出所有被监控服务器(`data.list`,与机器人 `/list` 输出一致)。 |
|
||||
| `/api/server/getInfo` | GET | admin | 服务器静态信息(对应机器人 `/info`)。查询 `?uuid=<uuid>`(或 `all`)。返回 `data: {<uuid>: {uuid, name, info}}`,每个请求的服务器一项。单个未知 uuid 返回 `404`。 |
|
||||
| `/api/server/getStatus` | GET | admin | 服务器实时状态(对应机器人 `/status`)。查询 `?uuid=<uuid>`(或 `all`)。返回 `data: {<uuid>: {uuid, name, online, report}}`;节点尚未上报时 `report` 为 `null`。单个未知 uuid 返回 `404`。 |
|
||||
| `/api/server/exec` | POST | bot / admin | 执行命令。请求体 `{uuid: [<uuid>...], command}`。下发 Komari 任务并轮询至完成(或超时)。返回 `data: {taskID, results}`。 |
|
||||
| `/api/settings/get` | GET | admin | `?type=global\|bot_user_config\|bot_node_config` —— 返回 `data: {config}`,即对应配置文件的内容(布局与文件一致)。 |
|
||||
| `/api/settings/set` | POST | admin | `?type=<同上>` + 部分更新的 JSON 请求体,例如 `{"system":{"debugMode":true}}` —— 深度合并到对应文件、写盘并在内存中重载。只改动给出的键;数组整体替换,`null` 删除该键。返回 `data: {type, restartRequired}`,其中 `restartRequired` 列出本次改动中只在启动时读取的键(见 [无需重启即可生效的配置](#无需重启即可生效的配置))—— 写入始终成功,这个字段只表示哪些修改尚未生效。始终为数组,全部生效时为空。 |
|
||||
| `/api/webhook/add` | POST | admin | 新增入站 Webhook 端点。请求体 `{name, enabled?, token?, notifyPipes?}` —— 只保存给出的字段,其余使用默认值。名称已存在时返回 `409`。 |
|
||||
| `/api/webhook/modify` | POST | admin | 修改已有端点。请求体 `{name, ...}` —— 给出哪些字段就改哪些(规则同 `/api/settings/set`,但限定在单个端点)。名称不存在返回 `404`,未给出其他字段返回 `400`。 |
|
||||
| `/api/webhook/delete` | POST | admin | 删除端点。请求体 `{name}`。名称不存在返回 `404`。 |
|
||||
| `/api/webhook/list` | GET | admin | 所有已配置的入站 Webhook 端点,以名称为键,位于 `data.endpoints`。 |
|
||||
| `/health` | GET | None | 健康检查。返回 `data: {status, database}`。 |
|
||||
| `/api/system/getLogs` | WebSocket | admin | 推送日志。先发送最近 100 条缓冲,再实时推送 `{level, content, timestamp}`。凭据可通过 `X-Token`/`X-Timestamp` 请求头或 `?token=`/`?timestamp=` 查询参数提供;校验失败会返回 JSON 错误且不升级连接。 |
|
||||
|
||||
作用于整个服务的中间件:
|
||||
|
||||
- **限流** —— 令牌桶,每个客户端 IP 每分钟 100 次请求(超出返回 `429`)。
|
||||
- **CORS** —— `Access-Control-Allow-Origin: *`,允许 `Content-Type`、`X-Token`、`X-Timestamp`、`Authorization`。
|
||||
- **安全响应头** —— `X-XSS-Protection`、`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`、`Referrer-Policy` 以及严格的 CSP。
|
||||
- **WebSocket 鉴权** —— 日志端点通过路由级中间件要求管理员 token,未授权客户端在升级前即被拒绝。
|
||||
|
||||
### 入站 Webhook API
|
||||
|
||||
它使用独立监听端口,外部应用可以只访问它而接触不到管理 API。通过 `webhook.enabled` 开启,监听 `webhook.listenAddr:webhook.listenPort`(默认 `0.0.0.0:8081`);这部分配置在启动时生效,而 `webhook.endpoints` 会在配置重载时重新读取。这里只应用限流与 CORS 中间件,不涉及会话 token。
|
||||
|
||||
控制台的 **WebHooks** 页面负责监听设置与端点管理。出站 Webhook 通知渠道(`controllerMethod.webhook`)仍位于设置页,与其他通知渠道放在一起,因为它本身就是其中一个渠道。
|
||||
|
||||
| 接口 | 方法 | 权限 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `/api/webhook/post/<name>` | POST | 端点 token | 把告警转发给该端点 `notifyPipes` 中列出的渠道。请求体 `{token, subject, content}`。返回 `data: {endpoint, channels}`。 |
|
||||
|
||||
`webhook.endpoints` 下的每个条目就是一个端点,以键名作为 URL 最后一段:键 `example` 对应 `POST /api/webhook/post/example`。`post/` 这一段把端点命名空间与管理路由(`/api/webhook/add` 等,位于管理监听)区分开。端点通过管理 API 增删改,写入的正是 `config.json` 的 `webhook.endpoints` 部分;新增端点在配置重载后即可接收请求,无需重启。端点包含:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `enabled` | 是否接受请求。禁用时返回 `403`。 |
|
||||
| `token` | 调用方在请求体 `token` 字段中提供的共享密钥,使用常量时间比较。未配置 token 的端点返回 `500`,而不是对任何人开放。 |
|
||||
| `notifyPipes` | 告警投递的渠道,命名与 `controllerMethod` 一致:`qq_onebot`、`telegram`、`email`、`ntfy`、`webhook`。未知或未启用的渠道会被跳过并上报。 |
|
||||
|
||||
管理 API 只接受上述三个字段。出现其他字段,或字段类型错误(`enabled` 必须是布尔值、`token` 必须是字符串、`notifyPipes` 必须是字符串数组)时返回 `400`,不会写入 `config.json` —— 程序不认识的字段不应出现在文件里。`name` 不能为空且不能包含 `/`,因为它会成为端点 URL 的最后一段。请求体上限为 1 MiB。
|
||||
|
||||
告警按渠道渲染为:
|
||||
|
||||
```
|
||||
{{ subject }}
|
||||
- Source: {{ source }}
|
||||
- Content:
|
||||
{{ content }}
|
||||
|
||||
- Time: {{ time }}
|
||||
Sent by Nukumizu Alert System
|
||||
```
|
||||
|
||||
`{{ source }}` 是端点名称,接收方据此判断是哪个应用触发的告警。`markdown: true` 的渠道会把 source 包裹为行内代码、content 包裹为代码块;`{{ subject }}` 与 `{{ time }}` 保持纯文本。
|
||||
|
||||
状态码:`200` 已投递,`400` 请求体格式错误或 `subject`/`content` 为空,`401` token 错误,`403` 端点已禁用,`404` 端点名称不存在,`405` 非 POST 请求,`500` 端点未配置 token,`502` 没有任何渠道接受该告警。
|
||||
|
||||
## 机器人
|
||||
|
||||
QQ(OneBot 11)与 Telegram 机器人共用同一套命令引擎与鉴权流程,实现在 `internal/controller/`。QQ 渠道通过 WebSocket 使用 OneBot 11(事件流 + API),因此兼容 NapCat 及同类实现;Telegram 使用 `go-telegram/bot` 长轮询。
|
||||
|
||||
### 权限模型
|
||||
|
||||
- **可信群组**:*群内*发出的命令只有在该群位于 `trustedGroups` 中时才会得到响应,其他群的消息会被忽略。
|
||||
- **管理命令**:`/shutdown`、`/reboot` 和 `/run` 还要求*发送者*位于 `admins` 中。
|
||||
- **私聊**:非管理命令(`/help`、`/list`、`/status`、`/info`、`/getip`)对任何私聊发送者都响应;管理命令仍要求管理员。
|
||||
|
||||
### 机器人响应模式
|
||||
|
||||
- `global` —— 机器人监听可信群组中的所有消息,无需被 @ 即可响应已知命令;未知的 `/` 命令会被静默忽略。
|
||||
- `at` —— 机器人只在自己被提及时响应(QQ `@`、Telegram `@botname`)。此模式下未知命令会回复 `Unknown command: /…`。
|
||||
|
||||
### 命令
|
||||
|
||||
| 命令 | 权限 | 说明 |
|
||||
|---|---|---|
|
||||
| `/help` | 所有人 | 显示帮助消息(`BOT_HELP` 模板)。 |
|
||||
| `/list` | 所有人 | 列出所有服务器及其在线状态(`SERVER_LIST` 模板)。 |
|
||||
| `/status <uuid>` | 所有人 | 服务器实时报告(CPU、内存、磁盘、网络、运行时长、进程)。 |
|
||||
| `/info <uuid>` | 所有人 | 服务器静态信息(系统、内核、CPU、内存、交换、磁盘、计费、标签)。 |
|
||||
| `/getip <uuid>` | 所有人 | 服务器的 IPv4 / IPv6 地址。 |
|
||||
| `/shutdown <uuid>` | 管理员 | 通过 Komari 任务接口关机。 |
|
||||
| `/reboot <uuid>` | 管理员 | 通过 Komari 任务接口重启。 |
|
||||
| `/run <uuid\|all> <command>` | 管理员 | 在单台(`all` 表示全部)服务器上执行命令并回报结果(`SERVER_EXECUTE_RESULT` 模板)。 |
|
||||
| `/start` | 所有人 | 仅 Telegram —— 发送 `TG_BOT_START` 欢迎消息。 |
|
||||
|
||||
## 通知渠道
|
||||
|
||||
QQ 与 Telegram 是*交互式*渠道。邮件、ntfy 与 Webhook 是**仅状态**渠道 —— 它们只接收状态变更告警,不能执行命令。启动时,欢迎消息与初始服务器列表只发送给机器人渠道(QQ / Telegram),并遵循每个成员的 `event_bot_started` 偏好。
|
||||
|
||||
五个渠道也都能承载外部应用通过[入站 Webhook API](#入站-webhook-api)提交的告警。机器人渠道会发给该渠道配置的群组与管理员;仅状态渠道会发给各自的目的地(收件人、ntfy topic、出站 Webhook URL)。Markdown 格式由各渠道的 `markdown` 设置决定,与渠道名称无关。
|
||||
|
||||
状态变更通知会先经过 `bot_node_config.json` 的单节点开关(`enableStatusNotify`、`flap_tolerant`),因此聊天机器人与仅状态渠道看到的是同一份过滤后的数据流。
|
||||
|
||||
## 构建
|
||||
|
||||
需要 Go 1.27.3+,构建 Web 控制台时还需要 Node.js 22+。
|
||||
|
||||
仓库根目录的辅助脚本会先构建 Vue 控制台,再把后端与它一起编译;同时通过 `-ldflags` 把当前 git commit 与构建时间写进二进制。脚本名表示**目标平台**,每个目标都有 Windows(`.bat`)和 Linux/macOS(`.sh`)两种版本:请在当前宿主机上运行对应版本,因为所有脚本都会交叉编译到各自的目标平台。
|
||||
|
||||
| 脚本 | 产物 |
|
||||
|---|---|
|
||||
| `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
|
||||
```
|
||||
|
||||
控制台是**内嵌在二进制中**的。Vite 将其输出到 `web/dist`,`web/embed.go` 通过 `go:embed` 把它编译进去,因此可执行文件可以独立提供完整前端 —— 复制到任何位置,即使旁边没有 `frontend/` 或 `web/dist`,访问 `/` 仍会返回控制台。构建脚本会在 `frontend/node_modules` 缺失时执行 `npm ci`,并在每次运行时执行 `npm run build`,因此构建机需要 Node.js 22+(运行二进制的机器不需要)。
|
||||
|
||||
也正因如此,编译后端前必须至少构建过一次控制台:`web/dist` 是生成目录且被 gitignore,在它存在之前 `go build` 会报 `pattern all:dist: no matching files found`。任何 `build-*` 脚本都会替你处理好顺序。
|
||||
|
||||
等价的手动构建:
|
||||
|
||||
```bash
|
||||
# 1. 控制台(前端改动后执行一次)
|
||||
cd frontend && npm ci && npm run build && cd ..
|
||||
|
||||
# 2. 后端
|
||||
# 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` 与 `main.BuildTime` 会出现在日志和 `BOT_STARTED` 消息中。
|
||||
|
||||
CI(`.github/workflows/build.yml`)会运行两种版本 —— `.sh` 在 `ubuntu-latest`,`.bat` 在 `windows-latest` —— 并把两个自包含的二进制作为产物上传。
|
||||
|
||||
## 运行
|
||||
|
||||
三个配置文件都从工作目录读取,没有路径参数:
|
||||
|
||||
```bash
|
||||
# 开发运行(go run,因此配置文件必须在当前目录)
|
||||
./run.sh # Linux / macOS
|
||||
run.bat # Windows
|
||||
|
||||
# 或先构建,再运行二进制
|
||||
./nukumizu-linux-amd64
|
||||
```
|
||||
|
||||
`run.sh` / `run.bat` 只运行 Go 后端,不会构建控制台,因此 `web/dist` 必须已存在(执行过一次任意 `build-*` 脚本,或在 `frontend/` 中执行 `npm run build`),否则编译会失败。
|
||||
|
||||
启动时程序会登录 Komari、加载节点状态、连接状态 WebSocket,然后启动各已启用渠道以及 `listenAddr:listenPort` 上的 HTTP 服务。按 `Ctrl+C` 优雅退出。当 `webhook.enabled` 为 `true` 时,入站 Webhook 监听会单独启动;绑定失败只会记录日志,不会影响程序其余部分。
|
||||
|
||||
### 前端开发
|
||||
|
||||
开发前端时,请分别运行前后端:
|
||||
|
||||
```bash
|
||||
# 终端 1 —— 后端(API + WebSocket)监听 :8080
|
||||
./run.sh
|
||||
|
||||
# 终端 2 —— Vite 开发服务器,热更新,监听 :5173
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
打开 http://localhost:5173。开发服务器会把 `/api`(包括日志 WebSocket)代理到 `http://127.0.0.1:8080`;若后端监听在别处,用 `NUKUMIZU_API` 指向它:
|
||||
|
||||
```bash
|
||||
NUKUMIZU_API=http://192.168.1.10:8080 npm run dev
|
||||
```
|
||||
|
||||
生产构建时,Go 服务会在正常监听地址上直接提供内嵌的控制台 —— 见[构建](#构建)。运行时与 Vite 无关,因此单独执行 `npm run build` 不会改变已运行二进制的内容:需要重新构建二进制才能生效。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
测试覆盖 config、handler、节点跟踪器、控制器与各管道包,不需要 Komari 实例或网络访问。
|
||||
|
||||
## 更新日志
|
||||
|
||||
每个版本的发布说明及对应 commit 见 [changeLogs.md](changeLogs.md)。
|
||||
|
||||
## 许可证
|
||||
|
||||
见 [LICENSE](LICENSE)。
|
||||
+22
-12
@@ -1,33 +1,43 @@
|
||||
## Ver.0.2.3.8
|
||||
### Features
|
||||
- Add retry attemption when login to Komari Dashboard failed ([a7f5fac](https://github.com/MadobiNanami/Nukumizu/commit/a7f5fac1d4a33506d5b2f293ad8f8fef150d4ee9) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/a7f5fac1d4a33506d5b2f293ad8f8fef150d4ee9))
|
||||
- Add retry attemption when fetching nodes from dashboard ([064524e](https://github.com/MadobiNanami/Nukumizu/commit/064524e9912700f35b670dcd09e99199daa9c12c) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/064524e9912700f35b670dcd09e99199daa9c12c))
|
||||
|
||||
## Ver.0.2.2.7-8c2159f.pre-release
|
||||
### Features
|
||||
- Add flap inhibiton for nodes ([03e74f4](https://github.com/MadobiNanami/Nukumizu/commit/03e74f49c36ba05995651135bd0f472b95bfede6) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/03e74f49c36ba05995651135bd0f472b95bfede6))
|
||||
- Put all node-related settings into a seperate dialog box ([1a7a4cd](https://github.com/MadobiNanami/Nukumizu/commit/1a7a4cdc073cccec4aa5fb61e139919b0c757dbe) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/1a7a4cdc073cccec4aa5fb61e139919b0c757dbe))
|
||||
|
||||
## Ver.0.2.1.6-d06d0bc.pre-release
|
||||
### Features
|
||||
- Most of settings now support hot reload when changed
|
||||
- [Network proxy]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075")
|
||||
- [Controllers]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/2a5a46a9846c5afe8ac3c66dc42164ddea25c038")
|
||||
- [Only log node count when it changed]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/aa7e7f87e17f17ff2f64aa1823a00a291628d4eb")
|
||||
- Network proxy ([457ea42](https://github.com/MadobiNanami/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075))
|
||||
- Controllers ([2a5a46a](https://github.com/MadobiNanami/Nukumizu/commit/2a5a46a9846c5afe8ac3c66dc42164ddea25c038) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/2a5a46a9846c5afe8ac3c66dc42164ddea25c038))
|
||||
- Only log node count when it changed ([aa7e7f8](https://github.com/MadobiNanami/Nukumizu/commit/aa7e7f87e17f17ff2f64aa1823a00a291628d4eb) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/aa7e7f87e17f17ff2f64aa1823a00a291628d4eb))
|
||||
|
||||
### Bug Fixes
|
||||
- [Resolve the proxy address per request]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075")
|
||||
- Resolve the proxy address per request ([457ea42](https://github.com/MadobiNanami/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/457ea424178b397f5509578d6180da437aaed075))
|
||||
|
||||
## Ver.0.2.0.5-661cf08.pre-release
|
||||
### Features
|
||||
- [Enhance security of websocket log system]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/da467c9297e641e2ab9a1889252589503791b7e9")
|
||||
- [Implement incoming webhook API interface with configurable endpoints]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/48533404fac20c23134e3ec1d3305ebdf4423c80")
|
||||
- [Implement webhook API settings and configuration in frontend webpage]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/6046fb5f882c108b7e94e9537bc15de288bd204b")
|
||||
- Enhance security of websocket log system ([da467c9](https://github.com/MadobiNanami/Nukumizu/commit/da467c9297e641e2ab9a1889252589503791b7e9) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/da467c9297e641e2ab9a1889252589503791b7e9))
|
||||
- Implement incoming webhook API interface with configurable endpoints ([4853340](https://github.com/MadobiNanami/Nukumizu/commit/48533404fac20c23134e3ec1d3305ebdf4423c80) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/48533404fac20c23134e3ec1d3305ebdf4423c80))
|
||||
- Implement webhook API settings and configuration in frontend webpage ([6046fb5](https://github.com/MadobiNanami/Nukumizu/commit/6046fb5f882c108b7e94e9537bc15de288bd204b) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/6046fb5f882c108b7e94e9537bc15de288bd204b))
|
||||
|
||||
## Ver.0.1.2.4-a6f3107.pre-release
|
||||
### Bug Fixes
|
||||
- [Integrate frontend build into backend binary]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/cc4aebde6a5a4fba5eeb65ff2feccdd06dc6529d")
|
||||
- Integrate frontend build into backend binary ([cc4aebd](https://github.com/MadobiNanami/Nukumizu/commit/cc4aebde6a5a4fba5eeb65ff2feccdd06dc6529d) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/cc4aebde6a5a4fba5eeb65ff2feccdd06dc6529d))
|
||||
|
||||
|
||||
## Ver.0.1.2.3-1c4ad61.pre-release
|
||||
### Features
|
||||
- [Add frontend webpage to make everything easy to control]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/a90b4f5497dfae5f9b6a3132d091a528fdbdf612")
|
||||
- [Add scripts for building frontend and backend for Linux and Windows]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/b970d66bc091b44b29ee0781888eb5b253b78aa6")
|
||||
- Add frontend webpage to make everything easy to control ([a90b4f5](https://github.com/MadobiNanami/Nukumizu/commit/a90b4f5497dfae5f9b6a3132d091a528fdbdf612) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/a90b4f5497dfae5f9b6a3132d091a528fdbdf612))
|
||||
- Add scripts for building frontend and backend for Linux and Windows ([b970d66](https://github.com/MadobiNanami/Nukumizu/commit/b970d66bc091b44b29ee0781888eb5b253b78aa6) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/b970d66bc091b44b29ee0781888eb5b253b78aa6))
|
||||
|
||||
### Bug Fixes
|
||||
- [Update API response structure to nest payloads under a single "data" key]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/bc7eb9dfdd5b98c0264aa6b5970cf0adb106aeb1")
|
||||
- Update API response structure to nest payloads under a single "data" key ([bc7eb9d](https://github.com/MadobiNanami/Nukumizu/commit/bc7eb9dfdd5b98c0264aa6b5970cf0adb106aeb1) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/bc7eb9dfdd5b98c0264aa6b5970cf0adb106aeb1))
|
||||
|
||||
|
||||
## Ver.0.1.1.2-15f1ee3.pre-release
|
||||
### Features
|
||||
- [Add per-node notify switch controller]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/6a0c2d1fc4b0fefa7eebc0b5a25a888817a186ca")
|
||||
- Add per-node notify switch controller ([6a0c2d1](https://github.com/MadobiNanami/Nukumizu/commit/6a0c2d1fc4b0fefa7eebc0b5a25a888817a186ca) · [gitea](https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/6a0c2d1fc4b0fefa7eebc0b5a25a888817a186ca))
|
||||
+1
-1
@@ -107,7 +107,7 @@ type ControllerMethodConfig struct {
|
||||
// WebhookEndpointConfig holds a single incoming webhook endpoint. Endpoints are
|
||||
// keyed by name under webhook.endpoints; the name is the last path segment of
|
||||
// the endpoint's URL, so an endpoint named "example" is served at
|
||||
// POST /api/webhook/example. One endpoint per external application and target
|
||||
// POST /api/webhook/post/example. One endpoint per external application and target
|
||||
// channel group keeps their tokens and recipients apart.
|
||||
type WebhookEndpointConfig struct {
|
||||
// Enabled controls whether the endpoint accepts requests. A disabled
|
||||
|
||||
@@ -32,7 +32,7 @@ const sections = [
|
||||
{ key: 'showTriggerCmdEcho', type: 'bool', label: 'Trigger command echo' },
|
||||
{ key: 'showKomariTaskEcho', type: 'bool', label: 'Komari task echo' },
|
||||
{ key: 'showFlapTolerantEcho', type: 'bool', label: 'Flap inhibition echo' },
|
||||
{ key: 'onebotIgnoreSelfMsg', type: 'bool', label: 'Ignore OneBot self messages' }
|
||||
{ key: 'oneBotIgnoreSelfMsg', type: 'bool', label: 'Ignore OneBot self messages' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ type SoftwareInfoStr struct {
|
||||
|
||||
var SoftwareInfo = SoftwareInfoStr{
|
||||
Name: "Nukumizu",
|
||||
Version: "0.2.2",
|
||||
Version: "0.2.3",
|
||||
Developer: "Madobi Nanami",
|
||||
BuildVer: 7,
|
||||
BuildVer: 8,
|
||||
CommitHash: "unknown",
|
||||
Description: "Remote server monitoring and command execution subsystem for Komari",
|
||||
BuildType: "pre-release",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module nukumizu-backend
|
||||
|
||||
go 1.27.1
|
||||
go 1.27.3
|
||||
|
||||
require (
|
||||
github.com/go-telegram/bot v1.25.0
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ type webhookRequest struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// WebhookHandler handles POST /api/webhook/{name}, the incoming webhook API
|
||||
// WebhookHandler handles POST /api/webhook/post/{name}, the incoming webhook API
|
||||
// served on its own listener (see webhook in config.json). The path segment
|
||||
// selects the endpoint, which carries the token to present and the notification
|
||||
// channels to deliver to:
|
||||
//
|
||||
// POST /api/webhook/example
|
||||
// POST /api/webhook/post/example
|
||||
// {"token": "...", "subject": "...", "content": "..."}
|
||||
//
|
||||
// The alert is rendered per channel and sent through every channel the endpoint
|
||||
|
||||
+145
-27
@@ -3,6 +3,7 @@ package komari
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -136,27 +137,120 @@ func NewClient(baseURL string) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates to Komari and stores the session token cookie.
|
||||
// retryMaxAttempts bounds how many times a request is tried before giving up.
|
||||
const retryMaxAttempts = 6
|
||||
|
||||
// retryBackoff is the linear backoff step between attempts: the n-th retry
|
||||
// waits n*retryBackoff.
|
||||
const retryBackoff = 500 * time.Millisecond
|
||||
|
||||
// retryableError marks a failure worth retrying: transport errors, rate
|
||||
// limiting and 5xx responses. Rejected credentials, 4xx responses and
|
||||
// malformed payloads are not retryable, so callers fail fast instead of
|
||||
// hammering Komari (which could trip account lockout or rate limits).
|
||||
type retryableError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *retryableError) Error() string { return e.err.Error() }
|
||||
|
||||
func (e *retryableError) Unwrap() error { return e.err }
|
||||
|
||||
func retryableErrorf(format string, args ...any) error {
|
||||
return &retryableError{err: fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
// isRetryableError reports whether err was produced by a failure that another
|
||||
// attempt may resolve.
|
||||
func isRetryableError(err error) bool {
|
||||
var retryable *retryableError
|
||||
return errors.As(err, &retryable)
|
||||
}
|
||||
|
||||
// doWithRetry runs fn until it succeeds, fails with a non-retryable error, or
|
||||
// exhausts retryMaxAttempts attempts. what names the operation in logs and in
|
||||
// the returned error.
|
||||
func doWithRetry(what string, fn func() error) error {
|
||||
var lastErr error
|
||||
for attempt := 1; ; attempt++ {
|
||||
if err := fn(); err != nil {
|
||||
lastErr = err
|
||||
if !isRetryableError(err) || attempt == retryMaxAttempts {
|
||||
return fmt.Errorf("%s failed after %d attempt(s): %w", what, attempt, lastErr)
|
||||
}
|
||||
postLog.Warning(fmt.Sprintf("%s attempt %d/%d failed, retrying: %v", what, attempt, retryMaxAttempts, err))
|
||||
time.Sleep(time.Duration(attempt) * retryBackoff)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// truncateBody keeps error messages readable when a proxy or gateway answers
|
||||
// with an unexpected (possibly HTML) payload.
|
||||
func truncateBody(body []byte) string {
|
||||
const limit = 512
|
||||
if len(body) > limit {
|
||||
return string(body[:limit]) + "..."
|
||||
}
|
||||
return strings.TrimSpace(string(body))
|
||||
}
|
||||
|
||||
// Login authenticates to Komari and stores the session token cookie. Transport
|
||||
// errors, 429 and 5xx responses are retried with linear backoff; credential or
|
||||
// protocol failures return immediately.
|
||||
func (c *Client) Login(username, password string) error {
|
||||
postLog.Info("Logging into Komari Dashboard at " + c.baseURL)
|
||||
|
||||
err := doWithRetry("komari login", func() error {
|
||||
return c.loginOnce(username, password)
|
||||
})
|
||||
if err != nil {
|
||||
// Drop any stale token so callers cannot keep using a dead session.
|
||||
c.mu.Lock()
|
||||
c.sessionToken = ""
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
postLog.Info("Successfully logged into Komari Dashboard")
|
||||
return nil
|
||||
}
|
||||
|
||||
// loginOnce performs a single login round trip. The response body is closed
|
||||
// before returning so retries never accumulate open connections.
|
||||
func (c *Client) loginOnce(username, password string) error {
|
||||
body := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(body)
|
||||
bodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal komari login request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Post(c.baseURL+"/api/login", "application/json", bytes.NewReader(bodyJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("komari login request failed: %w", err)
|
||||
return retryableErrorf("komari login request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return retryableErrorf("failed to read komari login response: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusTooManyRequests, resp.StatusCode >= 500:
|
||||
return retryableErrorf("komari login request failed: unexpected status code %d: %s", resp.StatusCode, truncateBody(respBody))
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return fmt.Errorf("komari login request failed: unexpected status code %d: %s", resp.StatusCode, truncateBody(respBody))
|
||||
}
|
||||
|
||||
var kr KomariResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&kr); err != nil {
|
||||
if err := json.Unmarshal(respBody, &kr); err != nil {
|
||||
if config.IsDebugMode() {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, respBody)
|
||||
return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, truncateBody(respBody))
|
||||
}
|
||||
return fmt.Errorf("failed to parse komari login response: %w", err)
|
||||
}
|
||||
@@ -171,7 +265,6 @@ func (c *Client) Login(username, password string) error {
|
||||
c.mu.Lock()
|
||||
c.sessionToken = cookie.Value
|
||||
c.mu.Unlock()
|
||||
postLog.Info("Successfully logged into Komari Dashboard")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -188,7 +281,40 @@ func (c *Client) GetSessionToken() string {
|
||||
}
|
||||
|
||||
// FetchNodes retrieves all nodes from Komari via the JSON-RPC2 endpoint.
|
||||
// Transport errors, 429 and 5xx responses are retried with linear backoff;
|
||||
// RPC-level errors and malformed payloads fail immediately.
|
||||
func (c *Client) FetchNodes() ([]NodeInfo, error) {
|
||||
var nodes []NodeInfo
|
||||
err := doWithRetry("komari nodes fetch", func() error {
|
||||
var err error
|
||||
nodes, err = c.fetchNodesOnce()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Persist the fetched node UUIDs to bot_node_config.json on every fetch
|
||||
// (startup login, WebSocket reconnect and periodic refresh all funnel
|
||||
// through FetchNodes) so the file always reflects the nodes Komari
|
||||
// currently manages. A persistence failure is only logged: the node data
|
||||
// itself was fetched successfully and must not be discarded over a disk
|
||||
// write problem.
|
||||
uuids := make([]string, len(nodes))
|
||||
for i, n := range nodes {
|
||||
uuids[i] = n.UUID
|
||||
}
|
||||
if err := config.SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, uuids); err != nil {
|
||||
postLog.Warning("Failed to save bot node config: " + err.Error())
|
||||
}
|
||||
|
||||
// postLog.Debug(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes)))
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// fetchNodesOnce performs a single nodes RPC round trip. The response body is
|
||||
// closed before returning so retries never accumulate open connections.
|
||||
func (c *Client) fetchNodesOnce() ([]NodeInfo, error) {
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "common:getNodes",
|
||||
@@ -207,16 +333,24 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) {
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("komari nodes request failed: %w", err)
|
||||
return nil, retryableErrorf("komari nodes request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("komari nodes request failed: unexpected status code %d", resp.StatusCode)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, retryableErrorf("failed to read komari nodes response: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusTooManyRequests, resp.StatusCode >= 500:
|
||||
return nil, retryableErrorf("komari nodes request failed: unexpected status code %d: %s", resp.StatusCode, truncateBody(respBody))
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return nil, fmt.Errorf("komari nodes request failed: unexpected status code %d: %s", resp.StatusCode, truncateBody(respBody))
|
||||
}
|
||||
|
||||
var rpcResp jsonRpcResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
|
||||
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse komari nodes RPC response: %w", err)
|
||||
}
|
||||
|
||||
@@ -233,22 +367,6 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) {
|
||||
for _, n := range nodeMap {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
|
||||
// Persist the fetched node UUIDs to bot_node_config.json on every fetch
|
||||
// (startup login, WebSocket reconnect and periodic refresh all funnel
|
||||
// through FetchNodes) so the file always reflects the nodes Komari
|
||||
// currently manages. A persistence failure is only logged: the node data
|
||||
// itself was fetched successfully and must not be discarded over a disk
|
||||
// write problem.
|
||||
uuids := make([]string, len(nodes))
|
||||
for i, n := range nodes {
|
||||
uuids[i] = n.UUID
|
||||
}
|
||||
if err := config.SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, uuids); err != nil {
|
||||
postLog.Warning("Failed to save bot node config: " + err.Error())
|
||||
}
|
||||
|
||||
// postLog.Debug(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes)))
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func SetupRouter() *http.ServeMux {
|
||||
// SetupRouter it is served on its own listener (webhook.listenAddr/listenPort),
|
||||
// so external applications can be given access to the webhook port without
|
||||
// reaching the admin API. Every endpoint configured under webhook.endpoints is
|
||||
// reachable as /api/webhook/<name>.
|
||||
// reachable as /api/webhook/post/{name}.
|
||||
func SetupWebhookRouter() *http.ServeMux {
|
||||
postLog.Info("Setting up webhook routers...")
|
||||
|
||||
|
||||
Reference in new issue
Block a user