feat: update API response structure to nest payloads under a single "data" key

This commit is contained in:
2026-09-09 16:15:33 +08:00
parent c0eada9bcc
commit d96b90b5bf
8 changed files with 36 additions and 29 deletions
+8 -8
View File
@@ -257,7 +257,7 @@ Admins and trusted groups are defined **per bot channel** and map a member ID to
## API ## API
All responses follow the envelope `{"success": true|false, "message": "...", ...data}`. `message` is empty on success unless noted. Success responses follow the envelope `{"success": true, "message": "...", "data": {...}}`, with the payload nested under a single `data` key. Error responses use `{"success": false, "message": "..."}`. `message` may be omitted on success when there is nothing to report.
### Authentication ### Authentication
@@ -274,15 +274,15 @@ Tokens idle for more than 1 hour are expired (cleaned every 10 minutes); any aut
| Endpoint | Method | Permission | Description | | Endpoint | Method | Permission | Description |
|---|---|---|---| |---|---|---|---|
| `/api/user/login` | POST | None | Log in. Body `{username, password}`. Returns `{token, userID, username, level, registerDate}`. | | `/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 `{token, userID, username, level}`. | | `/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/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 `{success, <uuid>: {uuid, name, info}}` — one entry per requested server. `404` for an unknown single uuid. | | `/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 `{success, <uuid>: {uuid, name, online, report}}`; `report` is `null` when the node has not reported yet. `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 `{taskID, results}`. | | `/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 `{success, config}`, where `config` is the selected config file's content (same layout as the JSON file). | | `/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. | | `/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. |
| `/health` | GET | None | Health check. Returns `{status, database}`. | | `/health` | GET | None | Health check. Returns `data: {status, database}`. |
| `/api/system/getLogs` | WebSocket | None | Streams logs. Sends the last 100 buffered entries, then live `{level, content, timestamp}` events. | | `/api/system/getLogs` | WebSocket | None | Streams logs. Sends the last 100 buffered entries, then live `{level, content, timestamp}` events. |
Middleware applied to the whole server: Middleware applied to the whole server:
+2 -1
View File
@@ -5,13 +5,14 @@ export const authApi = {
register: (username, password) => http.post('/user/register', { username, password }) register: (username, password) => http.post('/user/register', { username, password })
}; };
// /api/server/getStatus?uuid=all → { success, "<uuid>": { uuid, name, online, report } } // /api/server/getStatus?uuid=all → { success, message, data: { "<uuid>": { uuid, name, online, report } } }
export const serverApi = { export const serverApi = {
statusAll: () => http.get('/server/getStatus?uuid=all'), statusAll: () => http.get('/server/getStatus?uuid=all'),
infoAll: () => http.get('/server/getInfo?uuid=all') infoAll: () => http.get('/server/getInfo?uuid=all')
}; };
// /api/settings/get?type=… / /api/settings/set?type=… // /api/settings/get?type=… / /api/settings/set?type=…
// get → { success, message, data: { config } }.
// `type` is one of global | bot_user_config | bot_node_config. // `type` is one of global | bot_user_config | bot_node_config.
// For set, pass a partial object; a JSON null value removes that key. // For set, pass a partial object; a JSON null value removes that key.
export const settingsApi = { export const settingsApi = {
+11 -9
View File
@@ -34,18 +34,20 @@ async function submit() {
try { try {
if (mode.value === 'login') { if (mode.value === 'login') {
const res = await authApi.login(form.username.trim(), form.password); const res = await authApi.login(form.username.trim(), form.password);
afterLogin(res.token, { const d = res.data;
userID: res.userID, afterLogin(d.token, {
username: res.username, userID: d.userID,
level: res.level, username: d.username,
registerDate: res.registerDate level: d.level,
registerDate: d.registerDate
}); });
} else { } else {
const res = await authApi.register(form.username.trim(), form.password); const res = await authApi.register(form.username.trim(), form.password);
afterLogin(res.token, { const d = res.data;
userID: res.userID, afterLogin(d.token, {
username: res.username, userID: d.userID,
level: res.level username: d.username,
level: d.level
}); });
toast.success('Admin created — welcome to Nukumizu'); toast.success('Admin created — welcome to Nukumizu');
} }
+6 -5
View File
@@ -43,12 +43,13 @@ async function load() {
serverApi.infoAll(), serverApi.infoAll(),
settingsApi.get('bot_node_config') settingsApi.get('bot_node_config')
]); ]);
const nodeConf = (nodes && nodes.config) || {}; const statusData = (status && status.data) || {};
const infoData = (info && info.data) || {};
const nodeConf = (nodes && nodes.data && nodes.data.config) || {};
const list = []; const list = [];
for (const key of Object.keys(status)) { for (const key of Object.keys(statusData)) {
if (key === 'success') continue; const s = statusData[key] || {};
const s = status[key] || {}; const infoEntry = infoData[key] || {};
const infoEntry = info[key] || {};
const conf = nodeConf[key] || {}; const conf = nodeConf[key] || {};
list.push({ list.push({
uuid: s.uuid || key, uuid: s.uuid || key,
+1 -1
View File
@@ -168,7 +168,7 @@ function defaults(f) {
async function load() { async function load() {
try { try {
const res = await settingsApi.get('global'); const res = await settingsApi.get('global');
const cfg = (res && res.config) || {}; const cfg = (res && res.data && res.data.config) || {};
for (const s of sections) { for (const s of sections) {
const obj = {}; const obj = {};
for (const f of s.fields) { for (const f of s.fields) {
+1 -1
View File
@@ -50,7 +50,7 @@ const defaultOpts = () => ({
async function load() { async function load() {
try { try {
const res = await settingsApi.get('bot_user_config'); const res = await settingsApi.get('bot_user_config');
const cfg = (res && res.config) || {}; const cfg = (res && res.data && res.data.config) || {};
const flat = []; const flat = [];
for (const c of CHANNELS) { for (const c of CHANNELS) {
const chData = cfg[c.key] || {}; const chData = cfg[c.key] || {};
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// SettingsGetHandler handles GET /api/settings/get?type=xxx. // SettingsGetHandler handles GET /api/settings/get?type=xxx.
// type selects which config file to return and may be one of // type selects which config file to return and may be one of
// "global", "bot_user_config" or "bot_node_config"; the returned "config" // "global", "bot_user_config" or "bot_node_config"; the returned data.config
// object has the same layout as the source JSON file. // object has the same layout as the source JSON file.
func SettingsGetHandler(w http.ResponseWriter, r *http.Request) { func SettingsGetHandler(w http.ResponseWriter, r *http.Request) {
if !utils.Auth(w, r, "GET", "admin") { if !utils.Auth(w, r, "GET", "admin") {
+6 -3
View File
@@ -203,7 +203,9 @@ func StartTokenCleaner() {
}() }()
} }
// SendSuccessResponse sends a standardized JSON success response. // SendSuccessResponse sends a standardized JSON success response. Every payload
// is nested under a single "data" key, so success responses use the envelope
// {"success": true, "message": "...", "data": {...}}.
func SendSuccessResponse(w http.ResponseWriter, message string, data map[string]interface{}) { func SendSuccessResponse(w http.ResponseWriter, message string, data map[string]interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
resp := map[string]interface{}{ resp := map[string]interface{}{
@@ -212,9 +214,10 @@ func SendSuccessResponse(w http.ResponseWriter, message string, data map[string]
if message != "" { if message != "" {
resp["message"] = message resp["message"] = message
} }
for k, v := range data { if data == nil {
resp[k] = v data = map[string]interface{}{}
} }
resp["data"] = data
json.NewEncoder(w).Encode(resp) json.NewEncoder(w).Encode(resp)
} }