Necore 是 NMO Ecosystem 的后端服务,为前端项目 Neco 提供用户认证、文章管理、服务器状态同步、文档管理、文件上传、Bot WebSocket 推送等能力。
项目基于 Go + Fiber + GORM + SQLite 实现,默认 API 前缀为:
/necore
-
用户与鉴权
- JWT 登录认证;
- 用户创建、删除、密码修改、头像修改;
- 用户权限组与标签管理;
- TokenVersion 机制,用于在用户权限/密码变化后使旧 JWT 失效。
-
文章/新闻管理
- 支持文章创建、编辑、删除;
- 支持文章分类、置顶、活动起止时间;
- 支持 Markdown、图片、PDF 等内容块;
- 支持文章附件上传与删除;
- 支持保存文章后通过 WebSocket 向 Bot 推送
article_updated事件; - 支持按指定 WebSocket session 定向推送。
-
服务器列表与状态同步
- 管理服务器名称、图标、描述、在线地图、实时状态地址;
- 查询 Minecraft 服务器状态;
- 返回在线人数、容量、版本、延迟、图标;
- 可返回玩家 sample 列表,供前端展示玩家头像。
-
文档管理
- 树形文档节点;
- 文件夹/文档节点;
- 公开/私有文档;
- 文档内容、贡献者、更新时间维护;
- 文档附件上传与删除。
-
Bot WebSocket
- Bot Token 创建、列表、删除;
- WebSocket 鉴权连接;
- 心跳检测与超时断开;
- 在线连接状态查询;
- 主动踢出连接;
- 连接日志记录;
- 5 分钟内连续相同日志去重,避免自动重连刷屏。
- Go 1.25+
- Fiber v2
- gofiber/contrib/websocket
- gofiber/contrib/jwt
- GORM
- SQLite
- mcstatusgo
- bcrypt
- JWT HS256
.
├── app # Fiber App 初始化与启动
├── config # .env 查找、创建、加载与默认配置
├── controller
│ ├── middleware # JWT 鉴权中间件
│ └── router # API 路由注册
├── contents # 上传文件保存目录,需要持久化备份
├── dao # 数据访问层
├── data # SQLite 数据库目录,需要持久化备份
├── database # SQLite 连接与 AutoMigrate
├── model # GORM 模型
├── service # 业务逻辑层
├── util # token、文件名安全处理等工具
├── ws # Bot WebSocket Hub
├── main.go
├── go.mod
└── routes_and_security_test.go
- Go 1.25+
- CGO 可用环境:SQLite 驱动依赖
github.com/mattn/go-sqlite3
安装依赖:
go mod download启动开发服务:
go run .运行测试:
go test ./...构建:
go build -o necore .配置文件使用 .env。服务启动时会按顺序查找或创建配置文件:
- 环境变量
NECORE_CONFIG_FILE指定的路径; - 当前工作目录下的
.env,仅当当前目录看起来像项目根目录时使用; - 可执行文件同目录下的
.env; - 用户配置目录中的
necore/.env。
环境变量优先级高于 .env 文件。
常用配置:
| 配置项 | 默认值 | 说明 |
|---|---|---|
PORT |
3000 |
HTTP 服务端口 |
SECRET |
自动生成 | JWT 签名密钥,修改后会使已有 JWT 全部失效 |
BOT_LOG_BUFFER_SIZE |
1000 或 .env 中配置值 |
Bot 连接日志最多保留条数 |
BOT_HEARTBEAT_TIMEOUT_SECONDS |
90 |
Bot 心跳超时时间,超过该时间未收到心跳则主动断开 |
.env 示例:
PORT=3000
SECRET=please-change-me
BOT_LOG_BUFFER_SIZE=2000
BOT_HEARTBEAT_TIMEOUT_SECONDS=90运行时数据默认位于:
data/*.sqlite3
contents/{objectId}/*
建议定期备份:
data/user.sqlite3data/article.sqlite3data/server.sqlite3data/document.sqlite3data/bot_connection.sqlite3contents/.env
SECRET 必须保密。泄漏后应立即更换,并要求用户重新登录。
当前项目没有内置公开注册入口。可以通过以下方式创建初始管理员:
- 临时启用或编写初始化脚本调用
dao.AddUserByUsername; - 手动向 SQLite 插入用户记录;
- 使用已有管理员调用用户创建接口。
管理员用户的 group 字段应包含:
["admin"]密码需要使用 bcrypt 哈希保存。项目中的 dao.DebugTestPassword() 可用于开发时打印测试密码哈希,生产环境不要启用调试输出。
| 权限组 | 说明 |
|---|---|
admin |
超级管理员 |
news_admin |
文章/新闻管理 |
server_admin |
服务器列表管理 |
document_admin |
文档管理 |
bot_admin |
Bot Token 与连接管理 |
所有接口默认带 /necore 前缀。
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/slogan |
获取随机标语 |
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
POST |
/auth/login |
无 | 登录 |
GET |
/auth/status |
登录 | 校验登录状态 |
POST |
/auth/register |
admin |
创建用户 |
GET |
/auth/user/:id |
无 | 获取用户公开信息 |
GET |
/auth/avatar/:id |
无 | 获取用户头像 |
GET |
/auth/userlist |
登录 | 获取用户列表 |
DELETE |
/auth/user/:id |
admin |
删除用户 |
POST |
/auth/password |
admin 或本人 |
修改密码 |
POST |
/auth/avatar |
admin 或本人 |
修改头像 |
PATCH |
/auth/user |
admin |
修改用户权限与标签 |
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
GET |
/news/total/:target |
无 | 获取分类文章数量 |
POST |
/news/list |
无 | 获取文章列表 |
GET |
/news/detail/:id |
无 | 获取文章详情 |
POST |
/news/create |
admin/news_admin |
创建空文章并返回 ID |
PATCH |
/news/:id |
admin/news_admin |
更新文章,可选择 WebSocket 推送 |
POST |
/news/upload/:id |
admin/news_admin |
上传文章附件 |
DELETE |
/news/upload/:id |
admin/news_admin |
删除文章附件 |
DELETE |
/news/:id |
admin/news_admin |
删除文章 |
文章更新推送字段:
{
"doesNotify": true,
"notifySessionIds": ["session-id-1", "session-id-2"]
}如果 doesNotify 为 false,不会推送。
如果 doesNotify 为 true 且 notifySessionIds 为空,则广播到全部在线 Bot。
如果 notifySessionIds 非空,则只推送到指定 session。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
GET |
/server/ |
无 | 获取服务器列表 |
POST |
/server/status |
无 | 查询服务器实时状态 |
GET |
/server/create |
admin/server_admin |
创建服务器条目 |
PATCH |
/server/ |
admin/server_admin |
更新服务器条目 |
DELETE |
/server/:id |
admin/server_admin |
删除服务器条目 |
/server/status 请求:
{
"serverUrl": "example.com:25565"
}响应示例:
{
"online": true,
"icon": "data:image/png;base64,...",
"playerCount": 12,
"capacity": 100,
"latency": 42,
"version": "1.20.1",
"players": [
{
"name": "Steve",
"uuid": "8667ba71-b85a-4004-af54-457a9734eed7"
}
]
}注意:players 来自 Minecraft 状态协议 sample,不保证包含全部在线玩家。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
GET |
/documents/layer/:parentId |
无 | 获取公开子节点 |
GET |
/documents/layer/private/:parentId |
登录 | 获取包含私有节点的子节点 |
GET |
/documents/:id |
无 | 获取公开文档内容 |
GET |
/documents/private/:id |
登录 | 获取私有文档内容 |
POST |
/documents/node |
登录 | 创建节点 |
POST |
/documents/node/:id |
登录 | 更新节点父级 |
PUT |
/documents/node/:id |
登录 | 更新文档内容 |
PATCH |
/documents/node/:id |
登录 | 重命名/更新节点属性 |
DELETE |
/documents/node/:id |
登录 | 删除节点 |
POST |
/documents/upload/:id |
登录 | 上传文档附件 |
DELETE |
/documents/upload/:id |
登录 | 删除文档附件 |
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/contents/* |
访问上传后的附件 |
上传文件允许后缀:
.png, .jpg, .jpeg, .webp, .pdf, .txt
文件会被重命名为 UUID 文件名。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
POST |
/bots/token |
admin/bot_admin |
创建 Bot Token |
GET |
/bots/token |
admin/bot_admin |
获取 Token 列表 |
GET |
/bots/token/:id |
admin/bot_admin |
获取 Token 记录 |
DELETE |
/bots/token/:id |
admin/bot_admin |
删除 Token |
GET |
/bots/status |
admin/bot_admin |
获取在线连接与日志 |
DELETE |
/bots/ws/kick/:session_id |
admin/bot_admin |
踢出指定连接 |
创建 Token 请求:
{
"name": "astrbot-main"
}建议响应包含明文 Token,并且明文只展示一次:
{
"token": {
"name": "astrbot-main"
},
"plain_token": "bot_xxx"
}推荐路由:
GET /necore/bots/ws/updates/:identifier
请求头:
Authorization: Bearer <bot-token>identifier 是机器人连接标识,例如:
astrbot-test
后端会为每个连接生成 session_id,管理端可通过该 ID 定向推送或踢出连接。
Bot 客户端应定期发送:
{
"type": "heartbeat",
"identifier": "astrbot-test",
"time": 1710000000
}后端收到 type=heartbeat 或 event=heartbeat 后刷新最近心跳时间。超过 BOT_HEARTBEAT_TIMEOUT_SECONDS 未收到心跳,后端会主动断开连接并记录日志。
文章更新事件:
{
"event": "article_updated",
"data": {
"id": "article-id",
"title": "文章标题",
"brief": "文章简介",
"category": "information"
}
}SECRET必须保密,泄漏后应更换并让用户重新登录。- 用户密码使用 bcrypt 保存,不应记录明文密码。
- Bot Token 应只保存 hash,创建时明文只返回一次。
- WebSocket 中的
identifier、token_name等动态字段写入 HTML 日志前必须进行转义。 - 从 Fiber
Ctx取得并跨 WebSocket 生命周期使用的字符串应使用strings.Clone复制,避免请求上下文复用导致内容污染。 - 文件上传必须使用安全文件名与后缀白名单。
- 对自动重连造成的重复日志,应在 Hub 层进行去重。
go build -o necore .
./necore[Unit]
Description=Necore Backend
After=network.target
[Service]
WorkingDirectory=/opt/necore
ExecStart=/opt/necore/necore
Restart=always
Environment=NECORE_CONFIG_FILE=/opt/necore/.env
[Install]
WantedBy=multi-user.targetlocation /necore/ {
proxy_pass http://127.0.0.1:3000/necore/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /necore/bots/ws/ {
proxy_pass http://127.0.0.1:3000/necore/bots/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}# 下载依赖
go mod download
# 开发运行
go run .
# 测试
go test ./...
# 构建
go build -o necore .检查 .env 中 SECRET 是否每次启动都变化。生产环境必须固定 SECRET。
确认:
contents/目录存在且进程有读写权限;- 前端访问路径是否补齐
/necore前缀; - 反向代理是否转发
/necore/contents/。
检查:
- WebSocket 路由是否为
/necore/bots/ws/updates/:identifier; - 请求头是否包含
Authorization: Bearer <bot-token>; - Token 是否被删除或重新生成;
- 反向代理是否正确配置
Upgrade和Connection; - 心跳间隔是否小于
BOT_HEARTBEAT_TIMEOUT_SECONDS。
确认 WebSocket 鉴权中间件中从 Fiber Ctx 取得的字符串已经使用 strings.Clone 复制,例如:
identifier := strings.TrimSpace(strings.Clone(c.Params("identifier")))
c.Locals("identifier", strings.Clone(identifier))确认:
serverUrl格式是否正确,例如example.com:25565;- 服务器是否允许 Minecraft status ping;
- 后端所在机器能否访问目标服务器;
- 查询并发过高时可能返回
429 Service busy。