# 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/` 推送自己的告警,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 : Show specific server status\n- /info : Show specific server info\n- /run : Execute command on specific server. If you type \"all\" in , you will run the command on all servers.\n- /shutdown : Shutdown specific server.\n- /reboot : Reboot specific server.\n- /getip : 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=`(或 `all`)。返回 `data: {: {uuid, name, info}}`,每个请求的服务器一项。单个未知 uuid 返回 `404`。 | | `/api/server/getStatus` | GET | admin | 服务器实时状态(对应机器人 `/status`)。查询 `?uuid=`(或 `all`)。返回 `data: {: {uuid, name, online, report}}`;节点尚未上报时 `report` 为 `null`。单个未知 uuid 返回 `404`。 | | `/api/server/exec` | POST | bot / admin | 执行命令。请求体 `{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/` | 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 ` | 所有人 | 服务器实时报告(CPU、内存、磁盘、网络、运行时长、进程)。 | | `/info ` | 所有人 | 服务器静态信息(系统、内核、CPU、内存、交换、磁盘、计费、标签)。 | | `/getip ` | 所有人 | 服务器的 IPv4 / IPv6 地址。 | | `/shutdown ` | 管理员 | 通过 Komari 任务接口关机。 | | `/reboot ` | 管理员 | 通过 Komari 任务接口重启。 | | `/run ` | 管理员 | 在单台(`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= -X main.BuildTime=" \ -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)。