37 KiB
Nukumizu 后端
English | 简体中文
面向 Komari 的远程服务器监控与命令执行子系统。
Nukumizu 会连接 Komari 面板,在内存中维护所有被监控服务器的状态,并把告警推送到多个通知渠道。同时它还在 QQ(通过 NapCat 或其他 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 和 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 ./...),覆盖重试、抖动抑制和设置合并等路径。
工作原理
- 启动时 Nukumizu 加载
config.json、bot_user_config.json和(可选的)bot_node_config.json,然后登录 Komari 面板。登录遇到传输错误 /429/5xx时最多重试 6 次;仍然失败则进程退出。 - 获取节点列表(名称 → UUID、静态信息)并存入内存,随后连接
/api/rpc2的 WebSocket,每 5 秒请求一次最新状态。连接断开后按指数退避重连(1s、2s、4s……最大 30s);连续 5 次失败会记录错误日志,并继续每 30 秒重试。 - 节点跟踪器会等待覆盖所有已知节点的首份快照后再启用通知(另有两分钟兜底),并且始终把第一份快照当作启动基线 —— 不会因为启动时的状态而产生告警。
- 真实的在线状态切换会通过
SERVER_STATUS_CHANGED模板渲染,并发送给所有已启用的渠道。在此之前会先经过bot_node_config.json的单节点开关:enableStatusNotify可以直接丢弃这次变更,flap_tolerant会把Offline暂缓 N 秒 —— 节点在窗口内恢复时,Offline和随后的Online都不会发送。 - 首次状态刷新完成后,QQ / Telegram 还会收到启动欢迎消息和初始服务器列表,并遵循每个成员的
event_bot_started偏好。 - 每 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 22 或更高版本 —— 仅在构建 Web 控制台时需要,预编译的二进制不需要
- 一台可从本机访问的 Komari 面板
- QQ:一个提供正向 WebSocket 端点的 OneBot 11 实现,例如 NapCat(
/api与/event) - Telegram:来自 @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
{
"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);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 API0.0.0.0:8081、无 Webhook 端点、OneBot127.0.0.1:3000、ntfy 服务器https://ntfy.sh、Webhook 方法POST等)。消息模板也有内置默认值。markdown默认为false,需要 Telegram 格式时请像示例那样显式开启。
bot_user_config.json
管理员与可信群组按机器人渠道分别定义,并把成员 ID 映射到该成员的通知偏好:
{
"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 为键:
{
"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提交的告警。机器人渠道会发给该渠道配置的群组与管理员;仅状态渠道会发给各自的目的地(收件人、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 |
# 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-* 脚本都会替你处理好顺序。
等价的手动构建:
# 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 —— 并把两个自包含的二进制作为产物上传。
运行
三个配置文件都从工作目录读取,没有路径参数:
# 开发运行(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 监听会单独启动;绑定失败只会记录日志,不会影响程序其余部分。
前端开发
开发前端时,请分别运行前后端:
# 终端 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 指向它:
NUKUMIZU_API=http://192.168.1.10:8080 npm run dev
生产构建时,Go 服务会在正常监听地址上直接提供内嵌的控制台 —— 见构建。运行时与 Vite 无关,因此单独执行 npm run build 不会改变已运行二进制的内容:需要重新构建二进制才能生效。
测试
go test ./...
测试覆盖 config、handler、节点跟踪器、控制器与各管道包,不需要 Komari 实例或网络访问。
更新日志
每个版本的发布说明及对应 commit 见 changeLogs.md。
许可证
见 LICENSE。