koishi-plugin-adapter-onebot-multi 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-manager.d.ts +24 -11
- package/lib/index.d.ts +9 -0
- package/lib/index.js +775 -1232
- package/lib/panel.d.ts +4 -11
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -52,6 +52,7 @@ __export(src_exports, {
|
|
|
52
52
|
});
|
|
53
53
|
module.exports = __toCommonJS(src_exports);
|
|
54
54
|
var import_koishi11 = require("koishi");
|
|
55
|
+
var import_path = require("path");
|
|
55
56
|
|
|
56
57
|
// src/bot/index.ts
|
|
57
58
|
var import_koishi8 = require("koishi");
|
|
@@ -979,8 +980,8 @@ function accept(socket, bot) {
|
|
|
979
980
|
bot.internal._request = (action, params) => {
|
|
980
981
|
const data = { action, params, echo: ++counter };
|
|
981
982
|
data.echo = ++counter;
|
|
982
|
-
return new Promise((
|
|
983
|
-
listeners[data.echo] =
|
|
983
|
+
return new Promise((resolve2, reject) => {
|
|
984
|
+
listeners[data.echo] = resolve2;
|
|
984
985
|
setTimeout(() => {
|
|
985
986
|
delete listeners[data.echo];
|
|
986
987
|
reject(new TimeoutError(params, action));
|
|
@@ -1332,8 +1333,8 @@ var StatusManager = class {
|
|
|
1332
1333
|
};
|
|
1333
1334
|
|
|
1334
1335
|
// src/panel.ts
|
|
1335
|
-
var import_koa = __toESM(require("koa"));
|
|
1336
1336
|
var import_router = __toESM(require("@koa/router"));
|
|
1337
|
+
var import_koa = __toESM(require("koa"));
|
|
1337
1338
|
var import_koishi10 = require("koishi");
|
|
1338
1339
|
var import_crypto = require("crypto");
|
|
1339
1340
|
var SERVER_SECRET = (0, import_crypto.randomBytes)(32).toString("hex");
|
|
@@ -1362,10 +1363,23 @@ function verifySignedToken(token) {
|
|
|
1362
1363
|
}
|
|
1363
1364
|
}
|
|
1364
1365
|
__name(verifySignedToken, "verifySignedToken");
|
|
1366
|
+
function getCookieToken(ctx) {
|
|
1367
|
+
const headerToken = ctx.headers["x-admin-token"];
|
|
1368
|
+
if (headerToken) return headerToken;
|
|
1369
|
+
const cookies = ctx.headers.cookie || "";
|
|
1370
|
+
const match = cookies.match(/ob_admin_token=([^;]+)/);
|
|
1371
|
+
return match ? match[1] : null;
|
|
1372
|
+
}
|
|
1373
|
+
__name(getCookieToken, "getCookieToken");
|
|
1374
|
+
function isAuthenticated(ctx) {
|
|
1375
|
+
const token = getCookieToken(ctx);
|
|
1376
|
+
return token ? verifySignedToken(token) : false;
|
|
1377
|
+
}
|
|
1378
|
+
__name(isAuthenticated, "isAuthenticated");
|
|
1365
1379
|
var PanelConfig = import_koishi10.Schema.object({
|
|
1366
1380
|
enabled: import_koishi10.Schema.boolean().default(false).description("是否启用状态展示面板。"),
|
|
1367
|
-
|
|
1368
|
-
|
|
1381
|
+
basePath: import_koishi10.Schema.string().default("/status").description("面板路径。"),
|
|
1382
|
+
port: import_koishi10.Schema.natural().description("独立端口(留空则使用 Koishi 内置服务器)。")
|
|
1369
1383
|
}).description("展示面板");
|
|
1370
1384
|
var StatusPanel = class {
|
|
1371
1385
|
constructor(ctx, config, statusManager, configManager) {
|
|
@@ -1374,25 +1388,29 @@ var StatusPanel = class {
|
|
|
1374
1388
|
this.statusManager = statusManager;
|
|
1375
1389
|
this.configManager = configManager;
|
|
1376
1390
|
if (!config.enabled) return;
|
|
1377
|
-
this.app = new import_koa.default();
|
|
1378
1391
|
this.setupRoutes();
|
|
1379
|
-
|
|
1380
|
-
|
|
1392
|
+
if (config.port) {
|
|
1393
|
+
ctx.logger.info(`状态面板已启用: http://localhost:${config.port}${config.basePath}`);
|
|
1394
|
+
ctx.logger.info(`管理面板已启用: http://localhost:${config.port}${config.basePath}/admin`);
|
|
1395
|
+
} else {
|
|
1396
|
+
ctx.logger.info(`状态面板已启用: ${config.basePath}`);
|
|
1397
|
+
ctx.logger.info(`管理面板已启用: ${config.basePath}/admin`);
|
|
1398
|
+
}
|
|
1381
1399
|
}
|
|
1382
1400
|
static {
|
|
1383
1401
|
__name(this, "StatusPanel");
|
|
1384
1402
|
}
|
|
1385
1403
|
app;
|
|
1386
|
-
server
|
|
1404
|
+
server;
|
|
1387
1405
|
async parseJsonBody(ctx) {
|
|
1388
|
-
return new Promise((
|
|
1406
|
+
return new Promise((resolve2, reject) => {
|
|
1389
1407
|
let body = "";
|
|
1390
1408
|
ctx.req.on("data", (chunk) => {
|
|
1391
1409
|
body += chunk.toString();
|
|
1392
1410
|
});
|
|
1393
1411
|
ctx.req.on("end", () => {
|
|
1394
1412
|
try {
|
|
1395
|
-
|
|
1413
|
+
resolve2(body ? JSON.parse(body) : {});
|
|
1396
1414
|
} catch (e) {
|
|
1397
1415
|
reject(new Error("Invalid JSON"));
|
|
1398
1416
|
}
|
|
@@ -1403,7 +1421,11 @@ var StatusPanel = class {
|
|
|
1403
1421
|
setupRoutes() {
|
|
1404
1422
|
const router = new import_router.default();
|
|
1405
1423
|
const basePath = this.config.basePath || "/status";
|
|
1424
|
+
const adminPath = `${basePath}/admin`;
|
|
1406
1425
|
router.get(`${basePath}/api/status`, (ctx) => {
|
|
1426
|
+
ctx.set("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
1427
|
+
ctx.set("Pragma", "no-cache");
|
|
1428
|
+
ctx.set("Expires", "0");
|
|
1407
1429
|
const status = this.statusManager.getStatus();
|
|
1408
1430
|
ctx.body = {
|
|
1409
1431
|
bots: status.bots.map((bot) => ({
|
|
@@ -1413,8 +1435,9 @@ var StatusPanel = class {
|
|
|
1413
1435
|
groupCount: bot.groupCount,
|
|
1414
1436
|
friendCount: bot.friendCount,
|
|
1415
1437
|
messageReceived: bot.messageReceived,
|
|
1416
|
-
messageSent: bot.messageSent
|
|
1417
|
-
|
|
1438
|
+
messageSent: bot.messageSent,
|
|
1439
|
+
lastMessageTime: bot.lastMessageTime,
|
|
1440
|
+
startupTime: bot.startupTime
|
|
1418
1441
|
})),
|
|
1419
1442
|
updatedAt: status.updatedAt
|
|
1420
1443
|
};
|
|
@@ -1423,29 +1446,14 @@ var StatusPanel = class {
|
|
|
1423
1446
|
ctx.type = "html";
|
|
1424
1447
|
ctx.body = this.renderStatusPage();
|
|
1425
1448
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const cookies = ctx.headers.cookie || "";
|
|
1429
|
-
const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
|
|
1430
|
-
const token = tokenMatch ? tokenMatch[1] : null;
|
|
1431
|
-
if (!token || !verifySignedToken(token)) {
|
|
1432
|
-
ctx.status = 401;
|
|
1433
|
-
ctx.body = { error: "未授权访问" };
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
await next();
|
|
1437
|
-
}, "authMiddleware");
|
|
1438
|
-
router.get(`${adminPath}/api/check-init`, async (ctx) => {
|
|
1449
|
+
router.get(`${adminPath}/api/auth/check`, async (ctx) => {
|
|
1450
|
+
this.ctx.logger.info("检查认证状态...");
|
|
1439
1451
|
const hasPassword = await this.configManager.hasAdminPassword();
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
const cookies = ctx.headers.cookie || "";
|
|
1444
|
-
const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
|
|
1445
|
-
const token = tokenMatch ? tokenMatch[1] : null;
|
|
1446
|
-
ctx.body = { authenticated: token && verifySignedToken(token) };
|
|
1452
|
+
const authenticated = isAuthenticated(ctx);
|
|
1453
|
+
this.ctx.logger.info(`hasPassword=${hasPassword}, authenticated=${authenticated}`);
|
|
1454
|
+
ctx.body = { hasPassword, authenticated };
|
|
1447
1455
|
});
|
|
1448
|
-
router.post(`${adminPath}/api/
|
|
1456
|
+
router.post(`${adminPath}/api/auth/setup`, async (ctx) => {
|
|
1449
1457
|
const hasPassword = await this.configManager.hasAdminPassword();
|
|
1450
1458
|
if (hasPassword) {
|
|
1451
1459
|
ctx.status = 400;
|
|
@@ -1461,174 +1469,177 @@ var StatusPanel = class {
|
|
|
1461
1469
|
await this.configManager.setAdminPassword(data.password);
|
|
1462
1470
|
this.ctx.logger.info("管理密码已设置");
|
|
1463
1471
|
const token = generateSignedToken();
|
|
1464
|
-
ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/;
|
|
1465
|
-
ctx.body = { success: true };
|
|
1472
|
+
ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/; Max-Age=86400`);
|
|
1473
|
+
ctx.body = { success: true, token };
|
|
1466
1474
|
});
|
|
1467
|
-
router.post(`${adminPath}/api/login`, async (ctx) => {
|
|
1475
|
+
router.post(`${adminPath}/api/auth/login`, async (ctx) => {
|
|
1476
|
+
this.ctx.logger.info("收到登录请求");
|
|
1468
1477
|
const data = await this.parseJsonBody(ctx);
|
|
1478
|
+
this.ctx.logger.info(`登录数据: ${JSON.stringify(data)}`);
|
|
1469
1479
|
const valid = await this.configManager.verifyAdminPassword(data.password || "");
|
|
1470
|
-
this.ctx.logger.
|
|
1480
|
+
this.ctx.logger.info(`登录尝试: ${valid ? "成功" : "失败"}`);
|
|
1471
1481
|
if (valid) {
|
|
1472
1482
|
const token = generateSignedToken();
|
|
1473
|
-
this.ctx.logger.
|
|
1474
|
-
ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/;
|
|
1475
|
-
ctx.body = { success: true };
|
|
1483
|
+
this.ctx.logger.info(`生成 token: ${token.substring(0, 20)}...`);
|
|
1484
|
+
ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/; Max-Age=86400`);
|
|
1485
|
+
ctx.body = { success: true, token };
|
|
1486
|
+
this.ctx.logger.info("登录响应已发送");
|
|
1476
1487
|
} else {
|
|
1477
1488
|
ctx.status = 401;
|
|
1478
1489
|
ctx.body = { error: "密码错误" };
|
|
1479
1490
|
}
|
|
1480
1491
|
});
|
|
1481
|
-
router.post(`${adminPath}/api/logout`, async (ctx) => {
|
|
1492
|
+
router.post(`${adminPath}/api/auth/logout`, async (ctx) => {
|
|
1482
1493
|
ctx.set("Set-Cookie", `ob_admin_token=; Path=/; HttpOnly; Max-Age=0`);
|
|
1483
1494
|
ctx.body = { success: true };
|
|
1484
1495
|
});
|
|
1485
1496
|
router.get(adminPath, async (ctx) => {
|
|
1486
|
-
const hasPassword = await this.configManager.hasAdminPassword();
|
|
1487
1497
|
ctx.type = "html";
|
|
1498
|
+
ctx.set("Cache-Control", "no-store");
|
|
1499
|
+
ctx.body = this.renderAdminApp();
|
|
1500
|
+
});
|
|
1501
|
+
const authMiddleware = /* @__PURE__ */ __name(async (ctx, next) => {
|
|
1502
|
+
if (!isAuthenticated(ctx)) {
|
|
1503
|
+
ctx.status = 401;
|
|
1504
|
+
ctx.body = { error: "未授权访问" };
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
await next();
|
|
1508
|
+
}, "authMiddleware");
|
|
1509
|
+
router.get(`${adminPath}/api/bots`, authMiddleware, async (ctx) => {
|
|
1488
1510
|
ctx.set("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
1489
1511
|
ctx.set("Pragma", "no-cache");
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const
|
|
1496
|
-
const isValid = token && verifySignedToken(token);
|
|
1497
|
-
this.ctx.logger.debug(`访问管理面板: token=${token ? "存在" : "无"}, 有效=${isValid}`);
|
|
1498
|
-
if (isValid) {
|
|
1499
|
-
ctx.body = this.renderAdminPage();
|
|
1500
|
-
} else {
|
|
1501
|
-
ctx.body = this.renderLoginPage();
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
});
|
|
1505
|
-
router.get(`${adminPath}/api/list`, authMiddleware, async (ctx) => {
|
|
1506
|
-
const configs = await this.configManager.getAllConfigs();
|
|
1507
|
-
const status = this.statusManager.getStatus();
|
|
1508
|
-
const bots = configs.map((config) => {
|
|
1509
|
-
const botStatus = status.bots.find((b) => b.selfId === config.selfId);
|
|
1512
|
+
ctx.set("Expires", "0");
|
|
1513
|
+
const dbConfigs = await this.configManager.getAllConfigs();
|
|
1514
|
+
const runtimeStatus = this.statusManager.getStatus();
|
|
1515
|
+
const runtimeMap = new Map(runtimeStatus.bots.map((b) => [String(b.selfId), b]));
|
|
1516
|
+
const bots = dbConfigs.map((config) => {
|
|
1517
|
+
const runtime = config.selfId ? runtimeMap.get(String(config.selfId)) : null;
|
|
1510
1518
|
return {
|
|
1519
|
+
id: config.id,
|
|
1520
|
+
name: config.name,
|
|
1511
1521
|
selfId: config.selfId,
|
|
1512
|
-
nickname:
|
|
1522
|
+
nickname: runtime?.nickname,
|
|
1513
1523
|
protocol: config.protocol,
|
|
1514
|
-
status:
|
|
1524
|
+
status: runtime?.status || "offline",
|
|
1515
1525
|
endpoint: config.endpoint,
|
|
1516
1526
|
path: config.path,
|
|
1517
1527
|
enabled: config.enabled,
|
|
1518
|
-
messageReceived:
|
|
1519
|
-
messageSent:
|
|
1520
|
-
lastMessageTime:
|
|
1521
|
-
startupTime:
|
|
1522
|
-
groupCount:
|
|
1523
|
-
friendCount:
|
|
1528
|
+
messageReceived: runtime?.messageReceived,
|
|
1529
|
+
messageSent: runtime?.messageSent,
|
|
1530
|
+
lastMessageTime: runtime?.lastMessageTime,
|
|
1531
|
+
startupTime: runtime?.startupTime,
|
|
1532
|
+
groupCount: runtime?.groupCount,
|
|
1533
|
+
friendCount: runtime?.friendCount
|
|
1524
1534
|
};
|
|
1525
1535
|
});
|
|
1526
1536
|
ctx.body = { bots, updatedAt: Date.now() };
|
|
1527
1537
|
});
|
|
1528
|
-
router.post(`${adminPath}/api/create`, authMiddleware, async (ctx) => {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1538
|
+
router.post(`${adminPath}/api/bots/create`, authMiddleware, async (ctx) => {
|
|
1539
|
+
try {
|
|
1540
|
+
const data = await this.parseJsonBody(ctx);
|
|
1541
|
+
if (!data.name) {
|
|
1542
|
+
ctx.status = 400;
|
|
1543
|
+
ctx.body = { error: "配置名称不能为空" };
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (data.protocol === "ws" && !data.endpoint) {
|
|
1547
|
+
ctx.status = 400;
|
|
1548
|
+
ctx.body = { error: "WS 协议需要配置 endpoint" };
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const record = await this.configManager.createConfig({
|
|
1552
|
+
name: data.name,
|
|
1553
|
+
selfId: void 0,
|
|
1554
|
+
// 连接成功后自动填充
|
|
1555
|
+
token: data.token,
|
|
1556
|
+
protocol: data.protocol || "ws-reverse",
|
|
1557
|
+
endpoint: data.endpoint,
|
|
1558
|
+
path: data.path || "/onebot",
|
|
1559
|
+
enabled: true
|
|
1560
|
+
});
|
|
1561
|
+
this.ctx.logger.info(`创建 Bot 配置: #${record.id} ${data.name}`);
|
|
1562
|
+
await this.startBot(this.configManager.toBotConfig(record));
|
|
1563
|
+
ctx.body = { success: true, id: record.id };
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
this.ctx.logger.error("创建 Bot 配置失败:", error);
|
|
1566
|
+
ctx.status = 500;
|
|
1567
|
+
ctx.body = { error: "创建配置失败: " + error.message };
|
|
1545
1568
|
}
|
|
1546
|
-
const record = await this.configManager.createConfig({
|
|
1547
|
-
selfId: data.selfId,
|
|
1548
|
-
token: data.token,
|
|
1549
|
-
protocol: data.protocol || "ws-reverse",
|
|
1550
|
-
endpoint: data.endpoint,
|
|
1551
|
-
path: data.path || "/onebot",
|
|
1552
|
-
enabled: true
|
|
1553
|
-
});
|
|
1554
|
-
this.ctx.logger.info(`创建 Bot 配置: ${data.selfId}`);
|
|
1555
|
-
await this.startBot(this.configManager.toBotConfig(record));
|
|
1556
|
-
ctx.body = { success: true };
|
|
1557
1569
|
});
|
|
1558
|
-
router.post(`${adminPath}/api/
|
|
1570
|
+
router.post(`${adminPath}/api/bots/delete`, authMiddleware, async (ctx) => {
|
|
1559
1571
|
const data = await this.parseJsonBody(ctx);
|
|
1560
|
-
const
|
|
1572
|
+
const id = Number(data.id);
|
|
1573
|
+
const existing = await this.configManager.getConfigById(id);
|
|
1561
1574
|
if (!existing) {
|
|
1562
1575
|
ctx.status = 404;
|
|
1563
|
-
ctx.body = { error:
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
if (data.updates?.protocol === "ws" && !data.updates?.endpoint && !existing.endpoint) {
|
|
1567
|
-
ctx.status = 400;
|
|
1568
|
-
ctx.body = { error: "WS 协议需要配置 endpoint" };
|
|
1576
|
+
ctx.body = { error: `配置 #${id} 不存在` };
|
|
1569
1577
|
return;
|
|
1570
1578
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
ctx.body = { success: true, needRestart: true };
|
|
1574
|
-
});
|
|
1575
|
-
router.post(`${adminPath}/api/delete`, authMiddleware, async (ctx) => {
|
|
1576
|
-
const data = await this.parseJsonBody(ctx);
|
|
1577
|
-
const selfId = data.selfId;
|
|
1578
|
-
const existing = await this.configManager.getConfig(selfId);
|
|
1579
|
-
if (!existing) {
|
|
1580
|
-
ctx.status = 404;
|
|
1581
|
-
ctx.body = { error: `Bot ${selfId} 不存在` };
|
|
1582
|
-
return;
|
|
1579
|
+
if (existing.selfId) {
|
|
1580
|
+
await this.stopBot(existing.selfId);
|
|
1583
1581
|
}
|
|
1584
|
-
await this.
|
|
1585
|
-
|
|
1586
|
-
this.ctx.logger.info(`删除 Bot 配置: ${selfId}`);
|
|
1582
|
+
await this.configManager.deleteConfigById(id);
|
|
1583
|
+
this.ctx.logger.info(`删除 Bot 配置: #${id} ${existing.name}`);
|
|
1587
1584
|
ctx.body = { success: true };
|
|
1588
1585
|
});
|
|
1589
|
-
router.post(`${adminPath}/api/toggle`, authMiddleware, async (ctx) => {
|
|
1586
|
+
router.post(`${adminPath}/api/bots/toggle`, authMiddleware, async (ctx) => {
|
|
1590
1587
|
const data = await this.parseJsonBody(ctx);
|
|
1591
|
-
const
|
|
1592
|
-
const existing = await this.configManager.
|
|
1588
|
+
const id = Number(data.id);
|
|
1589
|
+
const existing = await this.configManager.getConfigById(id);
|
|
1593
1590
|
if (!existing) {
|
|
1594
1591
|
ctx.status = 404;
|
|
1595
|
-
ctx.body = { error:
|
|
1592
|
+
ctx.body = { error: `配置 #${id} 不存在` };
|
|
1596
1593
|
return;
|
|
1597
1594
|
}
|
|
1598
1595
|
const newEnabled = !existing.enabled;
|
|
1599
|
-
await this.configManager.
|
|
1596
|
+
await this.configManager.updateConfigById(id, { enabled: newEnabled });
|
|
1600
1597
|
if (newEnabled) {
|
|
1601
|
-
const config = await this.configManager.
|
|
1598
|
+
const config = await this.configManager.getConfigById(id);
|
|
1602
1599
|
if (config) {
|
|
1603
1600
|
await this.startBot(this.configManager.toBotConfig(config));
|
|
1604
1601
|
}
|
|
1605
1602
|
} else {
|
|
1606
|
-
|
|
1603
|
+
if (existing.selfId) {
|
|
1604
|
+
await this.stopBot(existing.selfId);
|
|
1605
|
+
}
|
|
1607
1606
|
}
|
|
1608
|
-
this.ctx.logger.info(`${newEnabled ? "启用" : "禁用"} Bot
|
|
1607
|
+
this.ctx.logger.info(`${newEnabled ? "启用" : "禁用"} Bot 配置: #${id} ${existing.name}`);
|
|
1609
1608
|
ctx.body = { success: true, enabled: newEnabled };
|
|
1610
1609
|
});
|
|
1611
|
-
router.post(`${adminPath}/api/restart`, authMiddleware, async (ctx) => {
|
|
1610
|
+
router.post(`${adminPath}/api/bots/restart`, authMiddleware, async (ctx) => {
|
|
1612
1611
|
const data = await this.parseJsonBody(ctx);
|
|
1613
|
-
const
|
|
1614
|
-
const config = await this.configManager.
|
|
1612
|
+
const id = Number(data.id);
|
|
1613
|
+
const config = await this.configManager.getConfigById(id);
|
|
1615
1614
|
if (!config) {
|
|
1616
1615
|
ctx.status = 404;
|
|
1617
|
-
ctx.body = { error:
|
|
1616
|
+
ctx.body = { error: `配置 #${id} 不存在` };
|
|
1618
1617
|
return;
|
|
1619
1618
|
}
|
|
1620
1619
|
if (!config.enabled) {
|
|
1621
1620
|
ctx.status = 400;
|
|
1622
|
-
ctx.body = { error:
|
|
1621
|
+
ctx.body = { error: `配置 #${id} 未启用` };
|
|
1623
1622
|
return;
|
|
1624
1623
|
}
|
|
1625
|
-
|
|
1624
|
+
if (config.selfId) {
|
|
1625
|
+
await this.stopBot(config.selfId);
|
|
1626
|
+
}
|
|
1626
1627
|
await this.startBot(this.configManager.toBotConfig(config));
|
|
1627
|
-
this.ctx.logger.info(`重启 Bot
|
|
1628
|
+
this.ctx.logger.info(`重启 Bot 配置: #${id} ${config.name}`);
|
|
1628
1629
|
ctx.body = { success: true };
|
|
1629
1630
|
});
|
|
1630
|
-
this.
|
|
1631
|
-
|
|
1631
|
+
if (this.config.port) {
|
|
1632
|
+
this.app = new import_koa.default();
|
|
1633
|
+
this.app.use(router.routes());
|
|
1634
|
+
this.app.use(router.allowedMethods());
|
|
1635
|
+
this.server = this.app.listen(this.config.port);
|
|
1636
|
+
this.ctx.on("dispose", () => {
|
|
1637
|
+
this.server?.close();
|
|
1638
|
+
});
|
|
1639
|
+
} else {
|
|
1640
|
+
this.ctx.server.use(router.routes());
|
|
1641
|
+
this.ctx.server.use(router.allowedMethods());
|
|
1642
|
+
}
|
|
1632
1643
|
}
|
|
1633
1644
|
async startBot(config) {
|
|
1634
1645
|
const existing = this.ctx.bots.find((b) => b.selfId === config.selfId && b.platform === "onebot");
|
|
@@ -1651,27 +1662,15 @@ var StatusPanel = class {
|
|
|
1651
1662
|
delete this.ctx.bots[bot.sid];
|
|
1652
1663
|
}
|
|
1653
1664
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
this.server = this.app.listen(port, () => {
|
|
1657
|
-
this.ctx.logger.info(`状态面板已启动: http://localhost:${port}${this.config.basePath}`);
|
|
1658
|
-
this.ctx.logger.info(`管理面板已启动: http://localhost:${port}${this.config.basePath}/admin`);
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
stop() {
|
|
1662
|
-
if (this.server) {
|
|
1663
|
-
this.server.close();
|
|
1664
|
-
this.ctx.logger.info("状态面板已关闭");
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
renderLoginPage() {
|
|
1665
|
+
// ==================== 页面渲染 ====================
|
|
1666
|
+
renderAdminApp() {
|
|
1668
1667
|
const basePath = this.config.basePath || "/status";
|
|
1669
1668
|
return `<!DOCTYPE html>
|
|
1670
1669
|
<html lang="zh-CN">
|
|
1671
1670
|
<head>
|
|
1672
1671
|
<meta charset="UTF-8">
|
|
1673
1672
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1674
|
-
<title>OneBot Multi -
|
|
1673
|
+
<title>OneBot Multi - 管理面板</title>
|
|
1675
1674
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
|
|
1676
1675
|
<style>
|
|
1677
1676
|
:root {
|
|
@@ -1681,16 +1680,15 @@ var StatusPanel = class {
|
|
|
1681
1680
|
--nl-text: #451a03;
|
|
1682
1681
|
--nl-border: 3px solid #451a03;
|
|
1683
1682
|
--nl-shadow: 4px 4px 0 #451a03;
|
|
1683
|
+
--status-online: #32CD32;
|
|
1684
|
+
--status-offline: #FF4500;
|
|
1685
|
+
--status-connecting: #FFA500;
|
|
1684
1686
|
}
|
|
1685
|
-
|
|
1686
1687
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1687
|
-
|
|
1688
1688
|
body {
|
|
1689
1689
|
font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
|
|
1690
1690
|
background-color: var(--nl-bg);
|
|
1691
|
-
background-image:
|
|
1692
|
-
radial-gradient(#fde68a 1px, transparent 1px),
|
|
1693
|
-
radial-gradient(#fde68a 1px, transparent 1px);
|
|
1691
|
+
background-image: radial-gradient(#fde68a 1px, transparent 1px), radial-gradient(#fde68a 1px, transparent 1px);
|
|
1694
1692
|
background-size: 20px 20px;
|
|
1695
1693
|
background-position: 0 0, 10px 10px;
|
|
1696
1694
|
color: var(--nl-text);
|
|
@@ -1700,36 +1698,25 @@ var StatusPanel = class {
|
|
|
1700
1698
|
justify-content: center;
|
|
1701
1699
|
padding: 2rem;
|
|
1702
1700
|
}
|
|
1703
|
-
|
|
1704
|
-
.login-box {
|
|
1701
|
+
.auth-box {
|
|
1705
1702
|
background: var(--nl-surface);
|
|
1706
1703
|
padding: 3rem;
|
|
1707
1704
|
border: var(--nl-border);
|
|
1708
1705
|
box-shadow: var(--nl-shadow);
|
|
1709
1706
|
border-radius: 16px;
|
|
1710
|
-
max-width:
|
|
1707
|
+
max-width: 450px;
|
|
1711
1708
|
width: 100%;
|
|
1712
1709
|
text-align: center;
|
|
1713
1710
|
}
|
|
1714
|
-
|
|
1715
1711
|
h1 {
|
|
1716
1712
|
font-family: 'Fredoka One', 'Noto Sans SC', cursive;
|
|
1717
1713
|
font-size: 1.8rem;
|
|
1718
|
-
margin-bottom:
|
|
1714
|
+
margin-bottom: 1rem;
|
|
1719
1715
|
text-shadow: 2px 2px 0 #fff;
|
|
1720
1716
|
}
|
|
1721
|
-
|
|
1722
|
-
.form-group {
|
|
1723
|
-
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
label {
|
|
1727
|
-
display: block;
|
|
1728
|
-
font-weight: 700;
|
|
1729
|
-
margin-bottom: 0.5rem;
|
|
1730
|
-
text-align: left;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1717
|
+
.desc { color: #92400e; margin-bottom: 2rem; font-weight: 600; }
|
|
1718
|
+
.form-group { margin-bottom: 1.5rem; text-align: left; }
|
|
1719
|
+
label { display: block; font-weight: 700; margin-bottom: 0.5rem; }
|
|
1733
1720
|
input[type="password"] {
|
|
1734
1721
|
width: 100%;
|
|
1735
1722
|
padding: 1rem;
|
|
@@ -1738,12 +1725,7 @@ var StatusPanel = class {
|
|
|
1738
1725
|
font-size: 1rem;
|
|
1739
1726
|
font-family: inherit;
|
|
1740
1727
|
}
|
|
1741
|
-
|
|
1742
|
-
input[type="password"]:focus {
|
|
1743
|
-
outline: none;
|
|
1744
|
-
box-shadow: 0 0 0 3px #fbbf24;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1728
|
+
input[type="password"]:focus { outline: none; box-shadow: 0 0 0 3px #fbbf24; }
|
|
1747
1729
|
.btn {
|
|
1748
1730
|
background: var(--nl-primary);
|
|
1749
1731
|
border: var(--nl-border);
|
|
@@ -1757,1057 +1739,421 @@ var StatusPanel = class {
|
|
|
1757
1739
|
transition: all 0.1s;
|
|
1758
1740
|
width: 100%;
|
|
1759
1741
|
}
|
|
1742
|
+
.btn:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 #451a03; }
|
|
1743
|
+
.btn:active { transform: translate(1px, 1px); box-shadow: 2px 2px 0 #451a03; }
|
|
1744
|
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
1745
|
+
.error { color: #b91c1c; margin-top: 1rem; display: none; font-weight: 600; }
|
|
1746
|
+
.error.show { display: block; }
|
|
1747
|
+
.loading { display: none; }
|
|
1748
|
+
.loading.show { display: block; }
|
|
1760
1749
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1750
|
+
/* 管理面板样式 */
|
|
1751
|
+
.admin-container {
|
|
1752
|
+
display: none;
|
|
1753
|
+
max-width: 1200px;
|
|
1754
|
+
width: 100%;
|
|
1755
|
+
background: var(--nl-surface);
|
|
1756
|
+
padding: 2rem;
|
|
1757
|
+
border: var(--nl-border);
|
|
1758
|
+
box-shadow: var(--nl-shadow);
|
|
1759
|
+
border-radius: 16px;
|
|
1764
1760
|
}
|
|
1765
|
-
|
|
1766
|
-
.
|
|
1767
|
-
|
|
1768
|
-
|
|
1761
|
+
.admin-container.show { display: block; }
|
|
1762
|
+
.admin-container h1 { font-size: 2.5rem; margin-bottom: 2rem; }
|
|
1763
|
+
.toolbar { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
|
|
1764
|
+
.toolbar .btn { width: auto; padding: 0.8rem 1.5rem; font-size: 1rem; box-shadow: 3px 3px 0 #451a03; }
|
|
1765
|
+
.btn-secondary { background: #f3f4f6; }
|
|
1766
|
+
.btn-danger { background: #fee2e2; }
|
|
1767
|
+
.bots { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 2rem; }
|
|
1768
|
+
.bot-card {
|
|
1769
|
+
background: var(--nl-surface);
|
|
1770
|
+
border: var(--nl-border);
|
|
1771
|
+
border-radius: 16px;
|
|
1772
|
+
padding: 1.5rem;
|
|
1773
|
+
box-shadow: var(--nl-shadow);
|
|
1774
|
+
transition: all 0.2s ease;
|
|
1769
1775
|
}
|
|
1770
|
-
|
|
1771
|
-
.
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1776
|
+
.bot-card.disabled { opacity: 0.6; }
|
|
1777
|
+
.bot-card:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 #451a03; }
|
|
1778
|
+
.bot-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
|
1779
|
+
.bot-avatar { width: 64px; height: 64px; border: var(--nl-border); border-radius: 50%; background: #fff; }
|
|
1780
|
+
.bot-info { flex: 1; }
|
|
1781
|
+
.bot-id { font-weight: 900; font-size: 1.2rem; }
|
|
1782
|
+
.bot-protocol { color: #92400e; font-size: 0.9rem; font-weight: 700; background: #fffbeb; display: inline-block; padding: 2px 6px; border: 2px solid #451a03; border-radius: 6px; margin-top: 4px; }
|
|
1783
|
+
.status-badge { padding: 0.4rem 0.8rem; border: 2px solid #451a03; border-radius: 20px; font-size: 0.8rem; font-weight: 900; }
|
|
1784
|
+
.status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
|
|
1785
|
+
.status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
|
|
1786
|
+
.status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
|
|
1787
|
+
.bot-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; background: #fff7ed; padding: 1rem; border: 2px solid #451a03; border-radius: 12px; margin-bottom: 1rem; }
|
|
1788
|
+
.bot-stat { display: flex; flex-direction: column; }
|
|
1789
|
+
.bot-stat-label { font-size: 0.75rem; font-weight: 800; color: #92400e; }
|
|
1790
|
+
.bot-stat-value { font-weight: 700; font-size: 1rem; }
|
|
1791
|
+
.bot-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
1792
|
+
.bot-actions .btn { width: auto; padding: 0.5rem 1rem; font-size: 0.85rem; }
|
|
1793
|
+
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }
|
|
1794
|
+
.modal.active { display: flex; }
|
|
1795
|
+
.modal-content { background: var(--nl-surface); border: var(--nl-border); box-shadow: var(--nl-shadow); border-radius: 16px; padding: 2rem; max-width: 500px; width: 90%; }
|
|
1796
|
+
.modal h2 { font-family: 'Fredoka One', cursive; margin-bottom: 1.5rem; }
|
|
1797
|
+
.modal .form-group input, .modal .form-group select { width: 100%; padding: 0.8rem; border: var(--nl-border); border-radius: 8px; font-size: 1rem; font-family: inherit; }
|
|
1798
|
+
.modal-actions { display: flex; gap: 1rem; margin-top: 1.5rem; }
|
|
1799
|
+
.modal-actions .btn { flex: 1; }
|
|
1800
|
+
.toast { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); background: var(--nl-surface); border: var(--nl-border); box-shadow: var(--nl-shadow); padding: 1rem 2rem; border-radius: 12px; font-weight: 700; z-index: 2000; display: none; }
|
|
1801
|
+
.toast.show { display: block; }
|
|
1802
|
+
.toast.error { background: #fee2e2; }
|
|
1803
|
+
.toast.success { background: #dcfce7; }
|
|
1804
|
+
.empty-state { text-align: center; padding: 3rem; color: #92400e; }
|
|
1805
|
+
.empty-state h3 { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
1806
|
+
.footer { margin-top: 3rem; text-align: center; font-weight: 700; color: #92400e; }
|
|
1807
|
+
@media (max-width: 768px) {
|
|
1808
|
+
body { padding: 1rem; align-items: flex-start; }
|
|
1809
|
+
.admin-container { padding: 1rem; }
|
|
1810
|
+
.bots { grid-template-columns: 1fr; }
|
|
1775
1811
|
}
|
|
1776
1812
|
</style>
|
|
1777
1813
|
</head>
|
|
1778
1814
|
<body>
|
|
1779
|
-
|
|
1815
|
+
<!-- 加载状态 -->
|
|
1816
|
+
<div class="auth-box loading" id="loadingView">
|
|
1817
|
+
<h1>加载中...</h1>
|
|
1818
|
+
</div>
|
|
1819
|
+
|
|
1820
|
+
<!-- 设置密码页面 -->
|
|
1821
|
+
<div class="auth-box" id="setupView" style="display:none">
|
|
1822
|
+
<h1>🔧 初始设置</h1>
|
|
1823
|
+
<p class="desc">首次使用,请设置管理密码</p>
|
|
1824
|
+
<form id="setupForm">
|
|
1825
|
+
<div class="form-group">
|
|
1826
|
+
<label for="setupPassword">设置密码</label>
|
|
1827
|
+
<input type="password" id="setupPassword" placeholder="至少4位" required minlength="4">
|
|
1828
|
+
</div>
|
|
1829
|
+
<div class="form-group">
|
|
1830
|
+
<label for="confirmPassword">确认密码</label>
|
|
1831
|
+
<input type="password" id="confirmPassword" placeholder="再次输入密码" required>
|
|
1832
|
+
</div>
|
|
1833
|
+
<button type="submit" class="btn" id="setupBtn">确认设置</button>
|
|
1834
|
+
<p class="error" id="setupError"></p>
|
|
1835
|
+
</form>
|
|
1836
|
+
</div>
|
|
1837
|
+
|
|
1838
|
+
<!-- 登录页面 -->
|
|
1839
|
+
<div class="auth-box" id="loginView" style="display:none">
|
|
1780
1840
|
<h1>🔐 管理登录</h1>
|
|
1781
1841
|
<form id="loginForm">
|
|
1782
1842
|
<div class="form-group">
|
|
1783
|
-
<label for="
|
|
1784
|
-
<input type="password" id="
|
|
1843
|
+
<label for="loginPassword">管理密钥</label>
|
|
1844
|
+
<input type="password" id="loginPassword" placeholder="请输入管理密钥" required>
|
|
1785
1845
|
</div>
|
|
1786
|
-
<button type="submit" class="btn">登录</button>
|
|
1787
|
-
<p class="error" id="
|
|
1846
|
+
<button type="submit" class="btn" id="loginBtn">登录</button>
|
|
1847
|
+
<p class="error" id="loginError"></p>
|
|
1788
1848
|
</form>
|
|
1789
1849
|
</div>
|
|
1790
1850
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
btn
|
|
1851
|
+
<!-- 管理面板 -->
|
|
1852
|
+
<div class="admin-container" id="adminView">
|
|
1853
|
+
<h1>🛠️ OneBot Multi 管理面板</h1>
|
|
1854
|
+
<div class="toolbar">
|
|
1855
|
+
<button class="btn" onclick="showAddModal()">➕ 添加 Bot</button>
|
|
1856
|
+
<button class="btn btn-secondary" onclick="refreshBots()">🔄 刷新</button>
|
|
1857
|
+
<a href="${basePath}" class="btn btn-secondary">📊 状态面板</a>
|
|
1858
|
+
<button class="btn btn-danger" onclick="logout()">🚪 登出</button>
|
|
1859
|
+
</div>
|
|
1860
|
+
<div class="bots" id="botList">
|
|
1861
|
+
<div class="empty-state"><h3>加载中...</h3></div>
|
|
1862
|
+
</div>
|
|
1863
|
+
<div class="footer">OneBot Multi 管理面板</div>
|
|
1864
|
+
</div>
|
|
1799
1865
|
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1866
|
+
<!-- 添加 Bot 模态框 -->
|
|
1867
|
+
<div class="modal" id="addModal">
|
|
1868
|
+
<div class="modal-content">
|
|
1869
|
+
<h2>添加 Bot 配置</h2>
|
|
1870
|
+
<form id="addForm">
|
|
1871
|
+
<div class="form-group">
|
|
1872
|
+
<label for="configName">配置名称 *</label>
|
|
1873
|
+
<input type="text" id="configName" required placeholder="例如: 主号、小号">
|
|
1874
|
+
</div>
|
|
1875
|
+
<div class="form-group">
|
|
1876
|
+
<label for="protocol">协议</label>
|
|
1877
|
+
<select id="protocol" onchange="toggleEndpoint()">
|
|
1878
|
+
<option value="ws-reverse">反向 WebSocket (ws-reverse)</option>
|
|
1879
|
+
<option value="ws">正向 WebSocket (ws)</option>
|
|
1880
|
+
</select>
|
|
1881
|
+
</div>
|
|
1882
|
+
<div class="form-group" id="endpointGroup" style="display: none;">
|
|
1883
|
+
<label for="endpoint">Endpoint *</label>
|
|
1884
|
+
<input type="text" id="endpoint" placeholder="例如: ws://127.0.0.1:6700">
|
|
1885
|
+
</div>
|
|
1886
|
+
<div class="form-group" id="pathGroup">
|
|
1887
|
+
<label for="path">路径</label>
|
|
1888
|
+
<input type="text" id="path" value="/onebot" placeholder="例如: /onebot">
|
|
1889
|
+
</div>
|
|
1890
|
+
<div class="form-group">
|
|
1891
|
+
<label for="token">Token(可选)</label>
|
|
1892
|
+
<input type="password" id="token" placeholder="访问令牌">
|
|
1893
|
+
</div>
|
|
1894
|
+
<div class="modal-actions">
|
|
1895
|
+
<button type="submit" class="btn">确定</button>
|
|
1896
|
+
<button type="button" class="btn btn-secondary" onclick="hideAddModal()">取消</button>
|
|
1897
|
+
</div>
|
|
1898
|
+
</form>
|
|
1899
|
+
</div>
|
|
1900
|
+
</div>
|
|
1901
|
+
|
|
1902
|
+
<!-- Toast -->
|
|
1903
|
+
<div class="toast" id="toast"></div>
|
|
1904
|
+
|
|
1905
|
+
<script>
|
|
1906
|
+
const API_BASE = '${basePath}/admin/api'
|
|
1907
|
+
|
|
1908
|
+
// 显示视图
|
|
1909
|
+
function showView(viewId) {
|
|
1910
|
+
['loadingView', 'setupView', 'loginView', 'adminView'].forEach(id => {
|
|
1911
|
+
const el = document.getElementById(id)
|
|
1912
|
+
if (id === viewId) {
|
|
1913
|
+
if (id === 'adminView') {
|
|
1914
|
+
el.classList.add('show')
|
|
1915
|
+
el.style.display = ''
|
|
1916
|
+
} else {
|
|
1917
|
+
el.style.display = ''
|
|
1918
|
+
el.classList.add('show')
|
|
1919
|
+
}
|
|
1813
1920
|
} else {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
btn.disabled = false
|
|
1817
|
-
btn.textContent = '登录'
|
|
1921
|
+
el.style.display = 'none'
|
|
1922
|
+
el.classList.remove('show')
|
|
1818
1923
|
}
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1924
|
+
})
|
|
1925
|
+
// 特殊处理 body 对齐
|
|
1926
|
+
if (viewId === 'adminView') {
|
|
1927
|
+
document.body.style.alignItems = 'flex-start'
|
|
1928
|
+
} else {
|
|
1929
|
+
document.body.style.alignItems = 'center'
|
|
1824
1930
|
}
|
|
1825
|
-
})
|
|
1826
|
-
</script>
|
|
1827
|
-
</body>
|
|
1828
|
-
</html>`;
|
|
1829
|
-
}
|
|
1830
|
-
renderSetupPage() {
|
|
1831
|
-
const basePath = this.config.basePath || "/status";
|
|
1832
|
-
return `<!DOCTYPE html>
|
|
1833
|
-
<html lang="zh-CN">
|
|
1834
|
-
<head>
|
|
1835
|
-
<meta charset="UTF-8">
|
|
1836
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1837
|
-
<title>OneBot Multi - 初始设置</title>
|
|
1838
|
-
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
|
|
1839
|
-
<style>
|
|
1840
|
-
:root {
|
|
1841
|
-
--nl-primary: #fbbf24;
|
|
1842
|
-
--nl-bg: #fffbeb;
|
|
1843
|
-
--nl-surface: #ffffff;
|
|
1844
|
-
--nl-text: #451a03;
|
|
1845
|
-
--nl-border: 3px solid #451a03;
|
|
1846
|
-
--nl-shadow: 4px 4px 0 #451a03;
|
|
1847
1931
|
}
|
|
1848
1932
|
|
|
1849
|
-
|
|
1933
|
+
// API 调用 - 使用 token 头部认证(兼容所有浏览器)
|
|
1934
|
+
async function api(endpoint, data = null) {
|
|
1935
|
+
// 从 cookie 或 localStorage 获取 token
|
|
1936
|
+
const cookieToken = document.cookie.match(/ob_admin_token=([^;]+)/)
|
|
1937
|
+
const token = cookieToken ? cookieToken[1] : localStorage.getItem('ob_admin_token')
|
|
1850
1938
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
radial-gradient(#fde68a 1px, transparent 1px),
|
|
1856
|
-
radial-gradient(#fde68a 1px, transparent 1px);
|
|
1857
|
-
background-size: 20px 20px;
|
|
1858
|
-
background-position: 0 0, 10px 10px;
|
|
1859
|
-
color: var(--nl-text);
|
|
1860
|
-
min-height: 100vh;
|
|
1861
|
-
display: flex;
|
|
1862
|
-
align-items: center;
|
|
1863
|
-
justify-content: center;
|
|
1864
|
-
padding: 2rem;
|
|
1865
|
-
}
|
|
1939
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
1940
|
+
if (token) {
|
|
1941
|
+
headers['X-Admin-Token'] = token
|
|
1942
|
+
}
|
|
1866
1943
|
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1944
|
+
const options = {
|
|
1945
|
+
method: data ? 'POST' : 'GET',
|
|
1946
|
+
headers,
|
|
1947
|
+
credentials: 'include'
|
|
1948
|
+
}
|
|
1949
|
+
if (data) options.body = JSON.stringify(data)
|
|
1950
|
+
const res = await fetch(API_BASE + endpoint, options)
|
|
1951
|
+
const json = await res.json()
|
|
1952
|
+
if (!res.ok && json.error) throw new Error(json.error)
|
|
1953
|
+
return json
|
|
1876
1954
|
}
|
|
1877
1955
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
text-shadow: 2px 2px 0 #fff;
|
|
1956
|
+
// 保存 token(同时保存到 cookie 和 localStorage)
|
|
1957
|
+
function saveToken(token) {
|
|
1958
|
+
document.cookie = 'ob_admin_token=' + token + '; path=/; max-age=86400'
|
|
1959
|
+
localStorage.setItem('ob_admin_token', token)
|
|
1883
1960
|
}
|
|
1884
1961
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1962
|
+
// 清除 token
|
|
1963
|
+
function clearToken() {
|
|
1964
|
+
document.cookie = 'ob_admin_token=; path=/; max-age=0'
|
|
1965
|
+
localStorage.removeItem('ob_admin_token')
|
|
1889
1966
|
}
|
|
1890
1967
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1968
|
+
// Toast 提示
|
|
1969
|
+
function showToast(message, type = 'success') {
|
|
1970
|
+
const toast = document.getElementById('toast')
|
|
1971
|
+
toast.textContent = message
|
|
1972
|
+
toast.className = 'toast show ' + type
|
|
1973
|
+
setTimeout(() => toast.className = 'toast', 3000)
|
|
1894
1974
|
}
|
|
1895
1975
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1976
|
+
// 初始化
|
|
1977
|
+
async function init() {
|
|
1978
|
+
showView('loadingView')
|
|
1979
|
+
try {
|
|
1980
|
+
const { hasPassword, authenticated } = await api('/auth/check')
|
|
1981
|
+
if (!hasPassword) {
|
|
1982
|
+
showView('setupView')
|
|
1983
|
+
} else if (authenticated) {
|
|
1984
|
+
showView('adminView')
|
|
1985
|
+
refreshBots()
|
|
1986
|
+
} else {
|
|
1987
|
+
showView('loginView')
|
|
1988
|
+
}
|
|
1989
|
+
} catch (e) {
|
|
1990
|
+
showToast('加载失败: ' + e.message, 'error')
|
|
1991
|
+
}
|
|
1900
1992
|
}
|
|
1901
1993
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
font-family: inherit;
|
|
1909
|
-
}
|
|
1994
|
+
// 设置密码
|
|
1995
|
+
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
|
1996
|
+
e.preventDefault()
|
|
1997
|
+
const password = document.getElementById('setupPassword').value
|
|
1998
|
+
const confirm = document.getElementById('confirmPassword').value
|
|
1999
|
+
const errorEl = document.getElementById('setupError')
|
|
1910
2000
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2001
|
+
if (password !== confirm) {
|
|
2002
|
+
errorEl.textContent = '两次密码不一致'
|
|
2003
|
+
errorEl.classList.add('show')
|
|
2004
|
+
return
|
|
2005
|
+
}
|
|
1915
2006
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
transform: translate(-1px, -1px);
|
|
1932
|
-
box-shadow: 5px 5px 0 #451a03;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
.btn:active {
|
|
1936
|
-
transform: translate(1px, 1px);
|
|
1937
|
-
box-shadow: 2px 2px 0 #451a03;
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
.error {
|
|
1941
|
-
color: #b91c1c;
|
|
1942
|
-
margin-top: 1rem;
|
|
1943
|
-
display: none;
|
|
1944
|
-
}
|
|
1945
|
-
</style>
|
|
1946
|
-
</head>
|
|
1947
|
-
<body>
|
|
1948
|
-
<div class="setup-box">
|
|
1949
|
-
<h1>🔧 初始设置</h1>
|
|
1950
|
-
<p class="desc">首次使用,请设置管理密码</p>
|
|
1951
|
-
<form id="setupForm">
|
|
1952
|
-
<div class="form-group">
|
|
1953
|
-
<label for="password">设置密码</label>
|
|
1954
|
-
<input type="password" id="password" placeholder="至少4位" required minlength="4">
|
|
1955
|
-
</div>
|
|
1956
|
-
<div class="form-group">
|
|
1957
|
-
<label for="confirmPassword">确认密码</label>
|
|
1958
|
-
<input type="password" id="confirmPassword" placeholder="再次输入密码" required>
|
|
1959
|
-
</div>
|
|
1960
|
-
<button type="submit" class="btn">确认设置</button>
|
|
1961
|
-
<p class="error" id="error"></p>
|
|
1962
|
-
</form>
|
|
1963
|
-
</div>
|
|
1964
|
-
|
|
1965
|
-
<script>
|
|
1966
|
-
document.getElementById('setupForm').addEventListener('submit', async function(e) {
|
|
1967
|
-
e.preventDefault()
|
|
1968
|
-
const password = document.getElementById('password').value
|
|
1969
|
-
const confirmPassword = document.getElementById('confirmPassword').value
|
|
1970
|
-
const errorEl = document.getElementById('error')
|
|
1971
|
-
|
|
1972
|
-
if (password !== confirmPassword) {
|
|
1973
|
-
errorEl.textContent = '两次密码不一致'
|
|
1974
|
-
errorEl.style.display = 'block'
|
|
1975
|
-
return
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
if (password.length < 4) {
|
|
1979
|
-
errorEl.textContent = '密码至少需要4位'
|
|
1980
|
-
errorEl.style.display = 'block'
|
|
1981
|
-
return
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
try {
|
|
1985
|
-
const res = await fetch('${basePath}/admin/api/init-password', {
|
|
1986
|
-
method: 'POST',
|
|
1987
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1988
|
-
body: JSON.stringify({ password }),
|
|
1989
|
-
credentials: 'include'
|
|
1990
|
-
})
|
|
1991
|
-
const data = await res.json()
|
|
1992
|
-
if (data.success) {
|
|
1993
|
-
window.location.href = '${basePath}/admin'
|
|
1994
|
-
} else {
|
|
1995
|
-
errorEl.textContent = data.error || '设置失败'
|
|
1996
|
-
errorEl.style.display = 'block'
|
|
1997
|
-
}
|
|
1998
|
-
} catch (e) {
|
|
1999
|
-
errorEl.textContent = '网络错误'
|
|
2000
|
-
errorEl.style.display = 'block'
|
|
2001
|
-
}
|
|
2002
|
-
})
|
|
2003
|
-
</script>
|
|
2004
|
-
</body>
|
|
2005
|
-
</html>`;
|
|
2006
|
-
}
|
|
2007
|
-
renderStatusPage() {
|
|
2008
|
-
const basePath = this.config.basePath || "/status";
|
|
2009
|
-
const status = this.statusManager.getStatus();
|
|
2010
|
-
const onlineCount = status.bots.filter((b) => b.status === "online").length;
|
|
2011
|
-
const offlineCount = status.bots.filter((b) => b.status === "offline").length;
|
|
2012
|
-
const totalCount = status.bots.length;
|
|
2013
|
-
const botCardsHtml = status.bots.map((bot) => {
|
|
2014
|
-
const statusText = bot.status === "online" ? "在线" : bot.status === "offline" ? "离线" : "连接中";
|
|
2015
|
-
const displayName = bot.nickname || bot.selfId;
|
|
2016
|
-
const groupCount = bot.groupCount ?? "-";
|
|
2017
|
-
const friendCount = bot.friendCount ?? "-";
|
|
2018
|
-
const messageReceived = bot.messageReceived ?? "-";
|
|
2019
|
-
const messageSent = bot.messageSent ?? "-";
|
|
2020
|
-
const lastMessageTime = bot.lastMessageTime ? new Date(bot.lastMessageTime * 1e3).toLocaleTimeString() : "-";
|
|
2021
|
-
const uptime = bot.startupTime ? this.formatUptime(bot.startupTime) : "-";
|
|
2022
|
-
return `
|
|
2023
|
-
<div class="bot-card">
|
|
2024
|
-
<div class="bot-header">
|
|
2025
|
-
<img class="bot-avatar" src="http://q.qlogo.cn/headimg_dl?dst_uin=${bot.selfId}&spec=640" alt="avatar">
|
|
2026
|
-
<div class="bot-info">
|
|
2027
|
-
<div class="bot-id">${displayName}</div>
|
|
2028
|
-
<div class="bot-protocol">QQ: ${bot.selfId}</div>
|
|
2029
|
-
</div>
|
|
2030
|
-
<span class="status-badge status-${bot.status}">${statusText}</span>
|
|
2031
|
-
</div>
|
|
2032
|
-
<div class="bot-stats">
|
|
2033
|
-
<div class="bot-stat">
|
|
2034
|
-
<span class="bot-stat-label">群聊</span>
|
|
2035
|
-
<span class="bot-stat-value">${groupCount}</span>
|
|
2036
|
-
</div>
|
|
2037
|
-
<div class="bot-stat">
|
|
2038
|
-
<span class="bot-stat-label">好友</span>
|
|
2039
|
-
<span class="bot-stat-value">${friendCount}</span>
|
|
2040
|
-
</div>
|
|
2041
|
-
<div class="bot-stat">
|
|
2042
|
-
<span class="bot-stat-label">收到</span>
|
|
2043
|
-
<span class="bot-stat-value">${messageReceived}</span>
|
|
2044
|
-
</div>
|
|
2045
|
-
<div class="bot-stat">
|
|
2046
|
-
<span class="bot-stat-label">发送</span>
|
|
2047
|
-
<span class="bot-stat-value">${messageSent}</span>
|
|
2048
|
-
</div>
|
|
2049
|
-
<div class="bot-stat">
|
|
2050
|
-
<span class="bot-stat-label">最后消息</span>
|
|
2051
|
-
<span class="bot-stat-value">${lastMessageTime}</span>
|
|
2052
|
-
</div>
|
|
2053
|
-
<div class="bot-stat">
|
|
2054
|
-
<span class="bot-stat-label">运行时间</span>
|
|
2055
|
-
<span class="bot-stat-value">${uptime}</span>
|
|
2056
|
-
</div>
|
|
2057
|
-
</div>
|
|
2058
|
-
</div>`;
|
|
2059
|
-
}).join("");
|
|
2060
|
-
const updatedAtStr = new Date(status.updatedAt).toLocaleString();
|
|
2061
|
-
return `<!DOCTYPE html>
|
|
2062
|
-
<html lang="zh-CN">
|
|
2063
|
-
<head>
|
|
2064
|
-
<meta charset="UTF-8">
|
|
2065
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2066
|
-
<title>OneBot Multi - 状态面板</title>
|
|
2067
|
-
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
|
|
2068
|
-
<style>
|
|
2069
|
-
:root {
|
|
2070
|
-
--nl-primary: #fbbf24;
|
|
2071
|
-
--nl-bg: #fffbeb;
|
|
2072
|
-
--nl-surface: #ffffff;
|
|
2073
|
-
--nl-text: #451a03;
|
|
2074
|
-
--nl-border: 3px solid #451a03;
|
|
2075
|
-
--nl-shadow: 4px 4px 0 #451a03;
|
|
2076
|
-
|
|
2077
|
-
--status-online: #32CD32;
|
|
2078
|
-
--status-offline: #FF4500;
|
|
2079
|
-
--status-connecting: #FFA500;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2083
|
-
|
|
2084
|
-
body {
|
|
2085
|
-
font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
|
|
2086
|
-
background-color: var(--nl-bg);
|
|
2087
|
-
background-image:
|
|
2088
|
-
radial-gradient(#fde68a 1px, transparent 1px),
|
|
2089
|
-
radial-gradient(#fde68a 1px, transparent 1px);
|
|
2090
|
-
background-size: 20px 20px;
|
|
2091
|
-
background-position: 0 0, 10px 10px;
|
|
2092
|
-
color: var(--nl-text);
|
|
2093
|
-
min-height: 100vh;
|
|
2094
|
-
padding: 2rem;
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
.container {
|
|
2098
|
-
max-width: 1200px;
|
|
2099
|
-
margin: 0 auto;
|
|
2100
|
-
background: var(--nl-surface);
|
|
2101
|
-
padding: 2rem;
|
|
2102
|
-
border: var(--nl-border);
|
|
2103
|
-
box-shadow: var(--nl-shadow);
|
|
2104
|
-
border-radius: 16px;
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
h1 {
|
|
2108
|
-
font-family: 'Fredoka One', 'Noto Sans SC', cursive;
|
|
2109
|
-
font-size: 2.5rem;
|
|
2110
|
-
margin-bottom: 2rem;
|
|
2111
|
-
text-align: center;
|
|
2112
|
-
text-shadow: 2px 2px 0 #fff;
|
|
2113
|
-
color: var(--nl-text);
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
.stats {
|
|
2117
|
-
display: grid;
|
|
2118
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
2119
|
-
gap: 1.5rem;
|
|
2120
|
-
margin-bottom: 3rem;
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
.stat-card {
|
|
2124
|
-
background: var(--nl-surface);
|
|
2125
|
-
border: var(--nl-border);
|
|
2126
|
-
padding: 1rem;
|
|
2127
|
-
text-align: center;
|
|
2128
|
-
box-shadow: 2px 2px 0 #451a03;
|
|
2129
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
2130
|
-
border-radius: 16px;
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
.stat-card:hover {
|
|
2134
|
-
transform: translate(-2px, -2px);
|
|
2135
|
-
box-shadow: 4px 4px 0 #451a03;
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
.stat-value {
|
|
2139
|
-
font-family: 'Fredoka One', sans-serif;
|
|
2140
|
-
font-size: 2.5rem;
|
|
2141
|
-
line-height: 1.2;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
.stat-label {
|
|
2145
|
-
font-weight: 800;
|
|
2146
|
-
font-size: 14px;
|
|
2147
|
-
color: #92400e;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
.stat-online .stat-value { color: var(--status-online); }
|
|
2151
|
-
.stat-offline .stat-value { color: var(--status-offline); }
|
|
2152
|
-
.stat-total .stat-value { color: #00BFFF; }
|
|
2153
|
-
|
|
2154
|
-
.bots {
|
|
2155
|
-
display: grid;
|
|
2156
|
-
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
2157
|
-
gap: 2rem;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
.bot-card {
|
|
2161
|
-
background: var(--nl-surface);
|
|
2162
|
-
border: var(--nl-border);
|
|
2163
|
-
border-radius: 16px;
|
|
2164
|
-
padding: 1.5rem;
|
|
2165
|
-
position: relative;
|
|
2166
|
-
box-shadow: var(--nl-shadow);
|
|
2167
|
-
transition: all 0.2s ease;
|
|
2168
|
-
overflow: hidden;
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
.bot-card:hover {
|
|
2172
|
-
transform: translate(-2px, -2px);
|
|
2173
|
-
box-shadow: 6px 6px 0 #451a03;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
.bot-header {
|
|
2177
|
-
display: flex;
|
|
2178
|
-
align-items: center;
|
|
2179
|
-
gap: 1rem;
|
|
2180
|
-
margin-bottom: 1.5rem;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
.bot-avatar {
|
|
2184
|
-
width: 64px;
|
|
2185
|
-
height: 64px;
|
|
2186
|
-
border: var(--nl-border);
|
|
2187
|
-
border-radius: 50%;
|
|
2188
|
-
background: #fff;
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
.bot-info { flex: 1; }
|
|
2192
|
-
|
|
2193
|
-
.bot-id {
|
|
2194
|
-
font-weight: 900;
|
|
2195
|
-
font-size: 1.2rem;
|
|
2196
|
-
color: var(--nl-text);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
.bot-protocol {
|
|
2200
|
-
color: #92400e;
|
|
2201
|
-
font-size: 0.9rem;
|
|
2202
|
-
font-weight: 700;
|
|
2203
|
-
background: #fffbeb;
|
|
2204
|
-
display: inline-block;
|
|
2205
|
-
padding: 2px 6px;
|
|
2206
|
-
border: 2px solid #451a03;
|
|
2207
|
-
border-radius: 6px;
|
|
2208
|
-
margin-top: 4px;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
.status-badge {
|
|
2212
|
-
padding: 0.4rem 0.8rem;
|
|
2213
|
-
border: 2px solid #451a03;
|
|
2214
|
-
border-radius: 20px;
|
|
2215
|
-
font-size: 0.8rem;
|
|
2216
|
-
font-weight: 900;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
.status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
|
|
2220
|
-
.status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
|
|
2221
|
-
.status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
|
|
2222
|
-
|
|
2223
|
-
.bot-stats {
|
|
2224
|
-
display: grid;
|
|
2225
|
-
grid-template-columns: 1fr 1fr;
|
|
2226
|
-
gap: 10px;
|
|
2227
|
-
background: #fff7ed;
|
|
2228
|
-
padding: 1rem;
|
|
2229
|
-
border: 2px solid #451a03;
|
|
2230
|
-
border-radius: 12px;
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
.bot-stat {
|
|
2234
|
-
display: flex;
|
|
2235
|
-
flex-direction: column;
|
|
2236
|
-
align-items: flex-start;
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
.bot-stat-label {
|
|
2240
|
-
font-size: 0.75rem;
|
|
2241
|
-
font-weight: 800;
|
|
2242
|
-
color: #92400e;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
.bot-stat-value {
|
|
2246
|
-
font-weight: 700;
|
|
2247
|
-
font-size: 1rem;
|
|
2248
|
-
color: var(--nl-text);
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
.footer {
|
|
2252
|
-
margin-top: 3rem;
|
|
2253
|
-
text-align: center;
|
|
2254
|
-
font-weight: 700;
|
|
2255
|
-
color: #92400e;
|
|
2256
|
-
padding: 0.5rem;
|
|
2257
|
-
width: 100%;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
.refresh-btn {
|
|
2261
|
-
position: fixed;
|
|
2262
|
-
bottom: 2rem;
|
|
2263
|
-
right: 2rem;
|
|
2264
|
-
background: var(--nl-primary);
|
|
2265
|
-
border: var(--nl-border);
|
|
2266
|
-
color: var(--nl-text);
|
|
2267
|
-
padding: 1rem 2rem;
|
|
2268
|
-
border-radius: 12px;
|
|
2269
|
-
cursor: pointer;
|
|
2270
|
-
font-size: 1.1rem;
|
|
2271
|
-
font-weight: 800;
|
|
2272
|
-
box-shadow: var(--nl-shadow);
|
|
2273
|
-
transition: all 0.1s;
|
|
2274
|
-
z-index: 100;
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
.refresh-btn:hover {
|
|
2278
|
-
transform: translate(-1px, -1px);
|
|
2279
|
-
box-shadow: 5px 5px 0 #451a03;
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
.refresh-btn:active {
|
|
2283
|
-
transform: translate(1px, 1px);
|
|
2284
|
-
box-shadow: 2px 2px 0 #451a03;
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
@media (max-width: 768px) {
|
|
2288
|
-
body { padding: 1rem; }
|
|
2289
|
-
.container { padding: 1rem; }
|
|
2290
|
-
.stats { grid-template-columns: 1fr; }
|
|
2291
|
-
.bot-card { margin-bottom: 1rem; }
|
|
2292
|
-
}
|
|
2293
|
-
</style>
|
|
2294
|
-
</head>
|
|
2295
|
-
<body>
|
|
2296
|
-
<div class="container">
|
|
2297
|
-
<h1 ondblclick="window.location='${basePath}/admin'" style="cursor: default;" title="双击进入管理">OneBot Multi 状态面板</h1>
|
|
2298
|
-
|
|
2299
|
-
<div class="stats">
|
|
2300
|
-
<div class="stat-card stat-online">
|
|
2301
|
-
<div class="stat-value">${onlineCount}</div>
|
|
2302
|
-
<div class="stat-label">在线</div>
|
|
2303
|
-
</div>
|
|
2304
|
-
<div class="stat-card stat-offline">
|
|
2305
|
-
<div class="stat-value">${offlineCount}</div>
|
|
2306
|
-
<div class="stat-label">离线</div>
|
|
2307
|
-
</div>
|
|
2308
|
-
<div class="stat-card stat-total">
|
|
2309
|
-
<div class="stat-value">${totalCount}</div>
|
|
2310
|
-
<div class="stat-label">总计</div>
|
|
2311
|
-
</div>
|
|
2312
|
-
</div>
|
|
2313
|
-
|
|
2314
|
-
<div class="bots">
|
|
2315
|
-
${botCardsHtml}
|
|
2316
|
-
</div>
|
|
2317
|
-
|
|
2318
|
-
<div class="footer">
|
|
2319
|
-
更新时间: ${updatedAtStr}
|
|
2320
|
-
</div>
|
|
2321
|
-
</div>
|
|
2322
|
-
|
|
2323
|
-
<button class="refresh-btn" onclick="location.reload()">刷新 ⚡</button>
|
|
2324
|
-
|
|
2325
|
-
<script>
|
|
2326
|
-
// 自动刷新
|
|
2327
|
-
setTimeout(() => location.reload(), 30000)
|
|
2328
|
-
</script>
|
|
2329
|
-
</body>
|
|
2330
|
-
</html>`;
|
|
2331
|
-
}
|
|
2332
|
-
renderAdminPage() {
|
|
2333
|
-
const basePath = this.config.basePath || "/status";
|
|
2334
|
-
return `<!DOCTYPE html>
|
|
2335
|
-
<html lang="zh-CN">
|
|
2336
|
-
<head>
|
|
2337
|
-
<meta charset="UTF-8">
|
|
2338
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2339
|
-
<title>OneBot Multi - 管理面板</title>
|
|
2340
|
-
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
|
|
2341
|
-
<style>
|
|
2342
|
-
:root {
|
|
2343
|
-
--nl-primary: #fbbf24;
|
|
2344
|
-
--nl-bg: #fffbeb;
|
|
2345
|
-
--nl-surface: #ffffff;
|
|
2346
|
-
--nl-text: #451a03;
|
|
2347
|
-
--nl-border: 3px solid #451a03;
|
|
2348
|
-
--nl-shadow: 4px 4px 0 #451a03;
|
|
2349
|
-
|
|
2350
|
-
--status-online: #32CD32;
|
|
2351
|
-
--status-offline: #FF4500;
|
|
2352
|
-
--status-connecting: #FFA500;
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2356
|
-
|
|
2357
|
-
body {
|
|
2358
|
-
font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
|
|
2359
|
-
background-color: var(--nl-bg);
|
|
2360
|
-
background-image:
|
|
2361
|
-
radial-gradient(#fde68a 1px, transparent 1px),
|
|
2362
|
-
radial-gradient(#fde68a 1px, transparent 1px);
|
|
2363
|
-
background-size: 20px 20px;
|
|
2364
|
-
background-position: 0 0, 10px 10px;
|
|
2365
|
-
color: var(--nl-text);
|
|
2366
|
-
min-height: 100vh;
|
|
2367
|
-
padding: 2rem;
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
.container {
|
|
2371
|
-
max-width: 1200px;
|
|
2372
|
-
margin: 0 auto;
|
|
2373
|
-
background: var(--nl-surface);
|
|
2374
|
-
padding: 2rem;
|
|
2375
|
-
border: var(--nl-border);
|
|
2376
|
-
box-shadow: var(--nl-shadow);
|
|
2377
|
-
border-radius: 16px;
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
h1 {
|
|
2381
|
-
font-family: 'Fredoka One', 'Noto Sans SC', cursive;
|
|
2382
|
-
font-size: 2.5rem;
|
|
2383
|
-
margin-bottom: 2rem;
|
|
2384
|
-
text-align: center;
|
|
2385
|
-
text-shadow: 2px 2px 0 #fff;
|
|
2386
|
-
color: var(--nl-text);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
.toolbar {
|
|
2390
|
-
display: flex;
|
|
2391
|
-
gap: 1rem;
|
|
2392
|
-
margin-bottom: 2rem;
|
|
2393
|
-
flex-wrap: wrap;
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
.btn {
|
|
2397
|
-
background: var(--nl-primary);
|
|
2398
|
-
border: var(--nl-border);
|
|
2399
|
-
color: var(--nl-text);
|
|
2400
|
-
padding: 0.8rem 1.5rem;
|
|
2401
|
-
border-radius: 12px;
|
|
2402
|
-
cursor: pointer;
|
|
2403
|
-
font-size: 1rem;
|
|
2404
|
-
font-weight: 800;
|
|
2405
|
-
box-shadow: 3px 3px 0 #451a03;
|
|
2406
|
-
transition: all 0.1s;
|
|
2407
|
-
text-decoration: none;
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
.btn:hover {
|
|
2411
|
-
transform: translate(-1px, -1px);
|
|
2412
|
-
box-shadow: 4px 4px 0 #451a03;
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
.btn:active {
|
|
2416
|
-
transform: translate(1px, 1px);
|
|
2417
|
-
box-shadow: 2px 2px 0 #451a03;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
.btn-danger {
|
|
2421
|
-
background: #fee2e2;
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
.btn-success {
|
|
2425
|
-
background: #dcfce7;
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
.btn-secondary {
|
|
2429
|
-
background: #f3f4f6;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
.bots {
|
|
2433
|
-
display: grid;
|
|
2434
|
-
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
2435
|
-
gap: 2rem;
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
.bot-card {
|
|
2439
|
-
background: var(--nl-surface);
|
|
2440
|
-
border: var(--nl-border);
|
|
2441
|
-
border-radius: 16px;
|
|
2442
|
-
padding: 1.5rem;
|
|
2443
|
-
position: relative;
|
|
2444
|
-
box-shadow: var(--nl-shadow);
|
|
2445
|
-
transition: all 0.2s ease;
|
|
2446
|
-
overflow: hidden;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
.bot-card.disabled {
|
|
2450
|
-
opacity: 0.6;
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
.bot-card:hover {
|
|
2454
|
-
transform: translate(-2px, -2px);
|
|
2455
|
-
box-shadow: 6px 6px 0 #451a03;
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
.bot-header {
|
|
2459
|
-
display: flex;
|
|
2460
|
-
align-items: center;
|
|
2461
|
-
gap: 1rem;
|
|
2462
|
-
margin-bottom: 1rem;
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
.bot-avatar {
|
|
2466
|
-
width: 64px;
|
|
2467
|
-
height: 64px;
|
|
2468
|
-
border: var(--nl-border);
|
|
2469
|
-
border-radius: 50%;
|
|
2470
|
-
background: #fff;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
.bot-info { flex: 1; }
|
|
2474
|
-
|
|
2475
|
-
.bot-id {
|
|
2476
|
-
font-weight: 900;
|
|
2477
|
-
font-size: 1.2rem;
|
|
2478
|
-
color: var(--nl-text);
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
.bot-protocol {
|
|
2482
|
-
color: #92400e;
|
|
2483
|
-
font-size: 0.9rem;
|
|
2484
|
-
font-weight: 700;
|
|
2485
|
-
background: #fffbeb;
|
|
2486
|
-
display: inline-block;
|
|
2487
|
-
padding: 2px 6px;
|
|
2488
|
-
border: 2px solid #451a03;
|
|
2489
|
-
border-radius: 6px;
|
|
2490
|
-
margin-top: 4px;
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
.status-badge {
|
|
2494
|
-
padding: 0.4rem 0.8rem;
|
|
2495
|
-
border: 2px solid #451a03;
|
|
2496
|
-
border-radius: 20px;
|
|
2497
|
-
font-size: 0.8rem;
|
|
2498
|
-
font-weight: 900;
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
.status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
|
|
2502
|
-
.status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
|
|
2503
|
-
.status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
|
|
2504
|
-
|
|
2505
|
-
.bot-stats {
|
|
2506
|
-
display: grid;
|
|
2507
|
-
grid-template-columns: 1fr 1fr;
|
|
2508
|
-
gap: 10px;
|
|
2509
|
-
background: #fff7ed;
|
|
2510
|
-
padding: 1rem;
|
|
2511
|
-
border: 2px solid #451a03;
|
|
2512
|
-
border-radius: 12px;
|
|
2513
|
-
margin-bottom: 1rem;
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
.bot-stat {
|
|
2517
|
-
display: flex;
|
|
2518
|
-
flex-direction: column;
|
|
2519
|
-
align-items: flex-start;
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
.bot-stat-label {
|
|
2523
|
-
font-size: 0.75rem;
|
|
2524
|
-
font-weight: 800;
|
|
2525
|
-
color: #92400e;
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
.bot-stat-value {
|
|
2529
|
-
font-weight: 700;
|
|
2530
|
-
font-size: 1rem;
|
|
2531
|
-
color: var(--nl-text);
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
.bot-actions {
|
|
2535
|
-
display: flex;
|
|
2536
|
-
gap: 0.5rem;
|
|
2537
|
-
flex-wrap: wrap;
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
.bot-actions .btn {
|
|
2541
|
-
padding: 0.5rem 1rem;
|
|
2542
|
-
font-size: 0.85rem;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
/* Modal */
|
|
2546
|
-
.modal {
|
|
2547
|
-
display: none;
|
|
2548
|
-
position: fixed;
|
|
2549
|
-
top: 0;
|
|
2550
|
-
left: 0;
|
|
2551
|
-
width: 100%;
|
|
2552
|
-
height: 100%;
|
|
2553
|
-
background: rgba(0,0,0,0.5);
|
|
2554
|
-
z-index: 1000;
|
|
2555
|
-
align-items: center;
|
|
2556
|
-
justify-content: center;
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
.modal.active {
|
|
2560
|
-
display: flex;
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
.modal-content {
|
|
2564
|
-
background: var(--nl-surface);
|
|
2565
|
-
border: var(--nl-border);
|
|
2566
|
-
box-shadow: var(--nl-shadow);
|
|
2567
|
-
border-radius: 16px;
|
|
2568
|
-
padding: 2rem;
|
|
2569
|
-
max-width: 500px;
|
|
2570
|
-
width: 90%;
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
.modal h2 {
|
|
2574
|
-
font-family: 'Fredoka One', cursive;
|
|
2575
|
-
margin-bottom: 1.5rem;
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
.form-group {
|
|
2579
|
-
margin-bottom: 1rem;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
.form-group label {
|
|
2583
|
-
display: block;
|
|
2584
|
-
font-weight: 700;
|
|
2585
|
-
margin-bottom: 0.5rem;
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
.form-group input, .form-group select {
|
|
2589
|
-
width: 100%;
|
|
2590
|
-
padding: 0.8rem;
|
|
2591
|
-
border: var(--nl-border);
|
|
2592
|
-
border-radius: 8px;
|
|
2593
|
-
font-size: 1rem;
|
|
2594
|
-
font-family: inherit;
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
.form-group input:focus, .form-group select:focus {
|
|
2598
|
-
outline: none;
|
|
2599
|
-
box-shadow: 0 0 0 3px #fbbf24;
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
.modal-actions {
|
|
2603
|
-
display: flex;
|
|
2604
|
-
gap: 1rem;
|
|
2605
|
-
margin-top: 1.5rem;
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
.footer {
|
|
2609
|
-
margin-top: 3rem;
|
|
2610
|
-
text-align: center;
|
|
2611
|
-
font-weight: 700;
|
|
2612
|
-
color: #92400e;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
.toast {
|
|
2616
|
-
position: fixed;
|
|
2617
|
-
bottom: 2rem;
|
|
2618
|
-
left: 50%;
|
|
2619
|
-
transform: translateX(-50%);
|
|
2620
|
-
background: var(--nl-surface);
|
|
2621
|
-
border: var(--nl-border);
|
|
2622
|
-
box-shadow: var(--nl-shadow);
|
|
2623
|
-
padding: 1rem 2rem;
|
|
2624
|
-
border-radius: 12px;
|
|
2625
|
-
font-weight: 700;
|
|
2626
|
-
z-index: 2000;
|
|
2627
|
-
display: none;
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
.toast.show {
|
|
2631
|
-
display: block;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
.toast.error {
|
|
2635
|
-
background: #fee2e2;
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
.toast.success {
|
|
2639
|
-
background: #dcfce7;
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
.empty-state {
|
|
2643
|
-
text-align: center;
|
|
2644
|
-
padding: 3rem;
|
|
2645
|
-
color: #92400e;
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
.empty-state h3 {
|
|
2649
|
-
font-size: 1.5rem;
|
|
2650
|
-
margin-bottom: 1rem;
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
@media (max-width: 768px) {
|
|
2654
|
-
body { padding: 1rem; }
|
|
2655
|
-
.container { padding: 1rem; }
|
|
2656
|
-
.bots { grid-template-columns: 1fr; }
|
|
2657
|
-
}
|
|
2658
|
-
</style>
|
|
2659
|
-
</head>
|
|
2660
|
-
<body>
|
|
2661
|
-
<div class="container">
|
|
2662
|
-
<h1>🛠️ OneBot Multi 管理面板</h1>
|
|
2663
|
-
|
|
2664
|
-
<div class="toolbar">
|
|
2665
|
-
<button class="btn" onclick="showAddModal()">➕ 添加 Bot</button>
|
|
2666
|
-
<button class="btn btn-secondary" onclick="refreshList()">🔄 刷新</button>
|
|
2667
|
-
<a href="${basePath}" class="btn btn-secondary">📊 状态面板</a>
|
|
2668
|
-
</div>
|
|
2669
|
-
|
|
2670
|
-
<div class="bots" id="botList">
|
|
2671
|
-
<div class="empty-state">
|
|
2672
|
-
<h3>加载中...</h3>
|
|
2673
|
-
</div>
|
|
2674
|
-
</div>
|
|
2675
|
-
|
|
2676
|
-
<div class="footer">
|
|
2677
|
-
OneBot Multi 管理面板
|
|
2678
|
-
</div>
|
|
2679
|
-
</div>
|
|
2680
|
-
|
|
2681
|
-
<!-- Add Modal -->
|
|
2682
|
-
<div class="modal" id="addModal">
|
|
2683
|
-
<div class="modal-content">
|
|
2684
|
-
<h2>添加 Bot</h2>
|
|
2685
|
-
<form id="addForm">
|
|
2686
|
-
<div class="form-group">
|
|
2687
|
-
<label for="selfId">QQ 号 *</label>
|
|
2688
|
-
<input type="text" id="selfId" required placeholder="例如: 123456789">
|
|
2689
|
-
</div>
|
|
2690
|
-
<div class="form-group">
|
|
2691
|
-
<label for="protocol">协议</label>
|
|
2692
|
-
<select id="protocol" onchange="toggleEndpoint()">
|
|
2693
|
-
<option value="ws-reverse">反向 WebSocket (ws-reverse)</option>
|
|
2694
|
-
<option value="ws">正向 WebSocket (ws)</option>
|
|
2695
|
-
</select>
|
|
2696
|
-
</div>
|
|
2697
|
-
<div class="form-group" id="endpointGroup" style="display: none;">
|
|
2698
|
-
<label for="endpoint">Endpoint *</label>
|
|
2699
|
-
<input type="text" id="endpoint" placeholder="例如: ws://127.0.0.1:6700">
|
|
2700
|
-
</div>
|
|
2701
|
-
<div class="form-group" id="pathGroup">
|
|
2702
|
-
<label for="path">路径</label>
|
|
2703
|
-
<input type="text" id="path" value="/onebot" placeholder="例如: /onebot">
|
|
2704
|
-
</div>
|
|
2705
|
-
<div class="form-group">
|
|
2706
|
-
<label for="token">Token(可选)</label>
|
|
2707
|
-
<input type="password" id="token" placeholder="访问令牌">
|
|
2708
|
-
</div>
|
|
2709
|
-
<div class="modal-actions">
|
|
2710
|
-
<button type="submit" class="btn">确定</button>
|
|
2711
|
-
<button type="button" class="btn btn-secondary" onclick="hideAddModal()">取消</button>
|
|
2712
|
-
</div>
|
|
2713
|
-
</form>
|
|
2714
|
-
</div>
|
|
2715
|
-
</div>
|
|
2716
|
-
|
|
2717
|
-
<!-- Toast -->
|
|
2718
|
-
<div class="toast" id="toast"></div>
|
|
2007
|
+
try {
|
|
2008
|
+
document.getElementById('setupBtn').disabled = true
|
|
2009
|
+
const result = await api('/auth/setup', { password })
|
|
2010
|
+
if (result.token) {
|
|
2011
|
+
saveToken(result.token)
|
|
2012
|
+
}
|
|
2013
|
+
showView('adminView')
|
|
2014
|
+
refreshBots()
|
|
2015
|
+
} catch (e) {
|
|
2016
|
+
errorEl.textContent = e.message
|
|
2017
|
+
errorEl.classList.add('show')
|
|
2018
|
+
} finally {
|
|
2019
|
+
document.getElementById('setupBtn').disabled = false
|
|
2020
|
+
}
|
|
2021
|
+
})
|
|
2719
2022
|
|
|
2720
|
-
|
|
2721
|
-
|
|
2023
|
+
// 登录
|
|
2024
|
+
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
2025
|
+
e.preventDefault()
|
|
2026
|
+
console.log('登录表单提交')
|
|
2027
|
+
const password = document.getElementById('loginPassword').value
|
|
2028
|
+
const errorEl = document.getElementById('loginError')
|
|
2722
2029
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2030
|
+
try {
|
|
2031
|
+
document.getElementById('loginBtn').disabled = true
|
|
2032
|
+
console.log('调用登录 API...')
|
|
2033
|
+
const result = await api('/auth/login', { password })
|
|
2034
|
+
console.log('登录结果:', result)
|
|
2035
|
+
// 保存 token(cookie + localStorage 双保险)
|
|
2036
|
+
if (result.token) {
|
|
2037
|
+
saveToken(result.token)
|
|
2038
|
+
console.log('已保存 token')
|
|
2039
|
+
}
|
|
2040
|
+
console.log('切换到管理面板...')
|
|
2041
|
+
showView('adminView')
|
|
2042
|
+
console.log('刷新 Bot 列表...')
|
|
2043
|
+
refreshBots()
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
console.error('登录失败:', e)
|
|
2046
|
+
errorEl.textContent = e.message
|
|
2047
|
+
errorEl.classList.add('show')
|
|
2048
|
+
} finally {
|
|
2049
|
+
document.getElementById('loginBtn').disabled = false
|
|
2739
2050
|
}
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
function showToast(message, type = 'success') {
|
|
2744
|
-
const toast = document.getElementById('toast')
|
|
2745
|
-
toast.textContent = message
|
|
2746
|
-
toast.className = 'toast show ' + type
|
|
2747
|
-
setTimeout(() => toast.className = 'toast', 3000)
|
|
2748
|
-
}
|
|
2051
|
+
})
|
|
2749
2052
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
return minutes + 'm'
|
|
2053
|
+
// 登出
|
|
2054
|
+
async function logout() {
|
|
2055
|
+
await api('/auth/logout', {})
|
|
2056
|
+
clearToken()
|
|
2057
|
+
showView('loginView')
|
|
2756
2058
|
}
|
|
2757
2059
|
|
|
2758
|
-
|
|
2060
|
+
// 刷新 Bot 列表
|
|
2061
|
+
async function refreshBots() {
|
|
2759
2062
|
try {
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
showToast('加载失败: ' + data.error, 'error')
|
|
2763
|
-
return
|
|
2764
|
-
}
|
|
2765
|
-
renderBots(data.bots || [])
|
|
2063
|
+
const { bots } = await api('/bots')
|
|
2064
|
+
renderBots(bots || [])
|
|
2766
2065
|
} catch (e) {
|
|
2767
|
-
|
|
2768
|
-
|
|
2066
|
+
if (e.message === '未授权访问') {
|
|
2067
|
+
showView('loginView')
|
|
2068
|
+
} else {
|
|
2069
|
+
showToast('加载失败: ' + e.message, 'error')
|
|
2070
|
+
}
|
|
2769
2071
|
}
|
|
2770
2072
|
}
|
|
2771
2073
|
|
|
2074
|
+
// 渲染 Bot 列表
|
|
2772
2075
|
function renderBots(bots) {
|
|
2773
2076
|
const container = document.getElementById('botList')
|
|
2774
2077
|
if (bots.length === 0) {
|
|
2775
|
-
container.innerHTML = '<div class="empty-state"><h3>暂无 Bot
|
|
2078
|
+
container.innerHTML = '<div class="empty-state"><h3>暂无 Bot 配置</h3><p>点击"添加 Bot"按钮创建</p></div>'
|
|
2776
2079
|
return
|
|
2777
2080
|
}
|
|
2778
2081
|
|
|
2779
2082
|
container.innerHTML = bots.map(bot => {
|
|
2780
2083
|
const statusText = bot.status === 'online' ? '在线' : bot.status === 'offline' ? '离线' : '连接中'
|
|
2781
|
-
const displayName = bot.nickname || bot.
|
|
2084
|
+
const displayName = bot.nickname || bot.name || '未连接'
|
|
2085
|
+
const selfIdDisplay = bot.selfId ? ('QQ: ' + bot.selfId) : '等待连接...'
|
|
2782
2086
|
const uptime = bot.startupTime ? formatUptime(bot.startupTime) : '-'
|
|
2783
|
-
const
|
|
2087
|
+
const lastMsg = bot.lastMessageTime ? new Date(bot.lastMessageTime * 1000).toLocaleTimeString() : '-'
|
|
2088
|
+
const avatarUrl = bot.selfId ? ('http://q.qlogo.cn/headimg_dl?dst_uin=' + bot.selfId + '&spec=640') : ''
|
|
2784
2089
|
|
|
2785
|
-
return '<div class="bot-card
|
|
2090
|
+
return '<div class="bot-card' + (!bot.enabled ? ' disabled' : '') + '">' +
|
|
2786
2091
|
'<div class="bot-header">' +
|
|
2787
|
-
'<img class="bot-avatar" src="
|
|
2092
|
+
(avatarUrl ? '<img class="bot-avatar" src="' + avatarUrl + '" alt="avatar">' : '<div class="bot-avatar" style="background:#ddd;display:flex;align-items:center;justify-content:center;font-size:24px;">?</div>') +
|
|
2788
2093
|
'<div class="bot-info">' +
|
|
2789
2094
|
'<div class="bot-id">' + displayName + '</div>' +
|
|
2790
|
-
'<div class="bot-protocol">' + bot.protocol.toUpperCase() + ' |
|
|
2095
|
+
'<div class="bot-protocol">' + (bot.protocol || 'WS-REVERSE').toUpperCase() + ' | ' + selfIdDisplay + '</div>' +
|
|
2791
2096
|
'</div>' +
|
|
2792
2097
|
'<span class="status-badge status-' + bot.status + '">' + statusText + '</span>' +
|
|
2793
2098
|
'</div>' +
|
|
2794
2099
|
'<div class="bot-stats">' +
|
|
2100
|
+
'<div class="bot-stat"><span class="bot-stat-label">配置名</span><span class="bot-stat-value">' + bot.name + '</span></div>' +
|
|
2101
|
+
'<div class="bot-stat"><span class="bot-stat-label">连接</span><span class="bot-stat-value">' + (bot.endpoint || bot.path || '-') + '</span></div>' +
|
|
2795
2102
|
'<div class="bot-stat"><span class="bot-stat-label">群聊</span><span class="bot-stat-value">' + (bot.groupCount ?? '-') + '</span></div>' +
|
|
2796
2103
|
'<div class="bot-stat"><span class="bot-stat-label">好友</span><span class="bot-stat-value">' + (bot.friendCount ?? '-') + '</span></div>' +
|
|
2797
2104
|
'<div class="bot-stat"><span class="bot-stat-label">收到</span><span class="bot-stat-value">' + (bot.messageReceived ?? '-') + '</span></div>' +
|
|
2798
2105
|
'<div class="bot-stat"><span class="bot-stat-label">发送</span><span class="bot-stat-value">' + (bot.messageSent ?? '-') + '</span></div>' +
|
|
2799
|
-
'<div class="bot-stat"><span class="bot-stat-label">连接</span><span class="bot-stat-value">' + (bot.endpoint || bot.path || '-') + '</span></div>' +
|
|
2800
|
-
'<div class="bot-stat"><span class="bot-stat-label">运行时间</span><span class="bot-stat-value">' + uptime + '</span></div>' +
|
|
2801
2106
|
'</div>' +
|
|
2802
2107
|
'<div class="bot-actions">' +
|
|
2803
|
-
'<button class="btn btn-secondary" onclick="toggleBot('
|
|
2804
|
-
'<button class="btn btn-secondary" onclick="restartBot('
|
|
2805
|
-
'<button class="btn btn-danger" onclick="deleteBot('' + bot.
|
|
2108
|
+
'<button class="btn btn-secondary" onclick="toggleBot(' + bot.id + ')">' + (bot.enabled ? '禁用' : '启用') + '</button>' +
|
|
2109
|
+
'<button class="btn btn-secondary" onclick="restartBot(' + bot.id + ')">重启</button>' +
|
|
2110
|
+
'<button class="btn btn-danger" onclick="deleteBot(' + bot.id + ', \\'' + bot.name + '\\')">删除</button>' +
|
|
2806
2111
|
'</div>' +
|
|
2807
2112
|
'</div>'
|
|
2808
2113
|
}).join('')
|
|
2809
2114
|
}
|
|
2810
2115
|
|
|
2116
|
+
function formatUptime(startTime) {
|
|
2117
|
+
const diff = Math.floor(Date.now() / 1000 - startTime)
|
|
2118
|
+
const hours = Math.floor(diff / 3600)
|
|
2119
|
+
const minutes = Math.floor((diff % 3600) / 60)
|
|
2120
|
+
if (hours > 0) return hours + 'h ' + minutes + 'm'
|
|
2121
|
+
return minutes + 'm'
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Bot 操作(使用配置 ID)
|
|
2125
|
+
async function toggleBot(id) {
|
|
2126
|
+
try {
|
|
2127
|
+
const result = await api('/bots/toggle', { id })
|
|
2128
|
+
showToast(result.enabled ? '配置已启用' : '配置已禁用')
|
|
2129
|
+
refreshBots()
|
|
2130
|
+
} catch (e) {
|
|
2131
|
+
showToast('操作失败: ' + e.message, 'error')
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
async function restartBot(id) {
|
|
2136
|
+
try {
|
|
2137
|
+
await api('/bots/restart', { id })
|
|
2138
|
+
showToast('Bot 正在重启')
|
|
2139
|
+
setTimeout(refreshBots, 1000)
|
|
2140
|
+
} catch (e) {
|
|
2141
|
+
showToast('重启失败: ' + e.message, 'error')
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
async function deleteBot(id, name) {
|
|
2146
|
+
if (!confirm('确定要删除配置 "' + name + '" 吗?')) return
|
|
2147
|
+
try {
|
|
2148
|
+
await api('/bots/delete', { id })
|
|
2149
|
+
showToast('配置已删除')
|
|
2150
|
+
refreshBots()
|
|
2151
|
+
} catch (e) {
|
|
2152
|
+
showToast('删除失败: ' + e.message, 'error')
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// 添加 Bot 模态框
|
|
2811
2157
|
function showAddModal() {
|
|
2812
2158
|
document.getElementById('addModal').classList.add('active')
|
|
2813
2159
|
document.getElementById('addForm').reset()
|
|
@@ -2827,7 +2173,7 @@ var StatusPanel = class {
|
|
|
2827
2173
|
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
|
2828
2174
|
e.preventDefault()
|
|
2829
2175
|
const data = {
|
|
2830
|
-
|
|
2176
|
+
name: document.getElementById('configName').value,
|
|
2831
2177
|
protocol: document.getElementById('protocol').value,
|
|
2832
2178
|
endpoint: document.getElementById('endpoint').value || undefined,
|
|
2833
2179
|
path: document.getElementById('path').value || undefined,
|
|
@@ -2835,55 +2181,213 @@ var StatusPanel = class {
|
|
|
2835
2181
|
}
|
|
2836
2182
|
|
|
2837
2183
|
try {
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
showToast('Bot 添加成功')
|
|
2184
|
+
await api('/bots/create', data)
|
|
2185
|
+
showToast('配置添加成功')
|
|
2841
2186
|
hideAddModal()
|
|
2842
|
-
|
|
2187
|
+
refreshBots()
|
|
2843
2188
|
} catch (e) {
|
|
2844
2189
|
showToast('添加失败: ' + e.message, 'error')
|
|
2845
2190
|
}
|
|
2846
2191
|
})
|
|
2847
2192
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
showToast(result.enabled ? 'Bot 已启用' : 'Bot 已禁用')
|
|
2853
|
-
refreshList()
|
|
2854
|
-
} catch (e) {
|
|
2855
|
-
showToast('操作失败: ' + e.message, 'error')
|
|
2193
|
+
// 自动刷新
|
|
2194
|
+
setInterval(() => {
|
|
2195
|
+
if (document.getElementById('adminView').classList.contains('show')) {
|
|
2196
|
+
refreshBots()
|
|
2856
2197
|
}
|
|
2857
|
-
}
|
|
2198
|
+
}, 30000)
|
|
2858
2199
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2200
|
+
// 启动
|
|
2201
|
+
init()
|
|
2202
|
+
</script>
|
|
2203
|
+
</body>
|
|
2204
|
+
</html>`;
|
|
2205
|
+
}
|
|
2206
|
+
renderStatusPage() {
|
|
2207
|
+
const basePath = this.config.basePath || "/status";
|
|
2208
|
+
const status = this.statusManager.getStatus();
|
|
2209
|
+
const onlineCount = status.bots.filter((b) => b.status === "online").length;
|
|
2210
|
+
const offlineCount = status.bots.filter((b) => b.status === "offline").length;
|
|
2211
|
+
const totalCount = status.bots.length;
|
|
2212
|
+
const botCardsHtml = status.bots.map((bot) => {
|
|
2213
|
+
const statusText = bot.status === "online" ? "在线" : bot.status === "offline" ? "离线" : "连接中";
|
|
2214
|
+
const displayName = bot.nickname || bot.selfId;
|
|
2215
|
+
const groupCount = bot.groupCount ?? "-";
|
|
2216
|
+
const friendCount = bot.friendCount ?? "-";
|
|
2217
|
+
const messageReceived = bot.messageReceived ?? "-";
|
|
2218
|
+
const messageSent = bot.messageSent ?? "-";
|
|
2219
|
+
const lastMessageTime = bot.lastMessageTime ? new Date(bot.lastMessageTime * 1e3).toLocaleTimeString() : "-";
|
|
2220
|
+
const uptime = bot.startupTime ? this.formatUptime(bot.startupTime) : "-";
|
|
2221
|
+
return `
|
|
2222
|
+
<div class="bot-card">
|
|
2223
|
+
<div class="bot-header">
|
|
2224
|
+
<img class="bot-avatar" src="http://q.qlogo.cn/headimg_dl?dst_uin=${bot.selfId}&spec=640" alt="avatar">
|
|
2225
|
+
<div class="bot-info">
|
|
2226
|
+
<div class="bot-id">${displayName}</div>
|
|
2227
|
+
<div class="bot-protocol">QQ: ${bot.selfId}</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
<span class="status-badge status-${bot.status}">${statusText}</span>
|
|
2230
|
+
</div>
|
|
2231
|
+
<div class="bot-stats">
|
|
2232
|
+
<div class="bot-stat"><span class="bot-stat-label">群聊</span><span class="bot-stat-value">${groupCount}</span></div>
|
|
2233
|
+
<div class="bot-stat"><span class="bot-stat-label">好友</span><span class="bot-stat-value">${friendCount}</span></div>
|
|
2234
|
+
<div class="bot-stat"><span class="bot-stat-label">收到</span><span class="bot-stat-value">${messageReceived}</span></div>
|
|
2235
|
+
<div class="bot-stat"><span class="bot-stat-label">发送</span><span class="bot-stat-value">${messageSent}</span></div>
|
|
2236
|
+
<div class="bot-stat"><span class="bot-stat-label">最后消息</span><span class="bot-stat-value">${lastMessageTime}</span></div>
|
|
2237
|
+
<div class="bot-stat"><span class="bot-stat-label">运行时间</span><span class="bot-stat-value">${uptime}</span></div>
|
|
2238
|
+
</div>
|
|
2239
|
+
</div>`;
|
|
2240
|
+
}).join("");
|
|
2241
|
+
const updatedAtStr = new Date(status.updatedAt).toLocaleString();
|
|
2242
|
+
return `<!DOCTYPE html>
|
|
2243
|
+
<html lang="zh-CN">
|
|
2244
|
+
<head>
|
|
2245
|
+
<meta charset="UTF-8">
|
|
2246
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2247
|
+
<title>OneBot Multi - 状态面板</title>
|
|
2248
|
+
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
|
|
2249
|
+
<style>
|
|
2250
|
+
:root {
|
|
2251
|
+
--nl-primary: #fbbf24;
|
|
2252
|
+
--nl-bg: #fffbeb;
|
|
2253
|
+
--nl-surface: #ffffff;
|
|
2254
|
+
--nl-text: #451a03;
|
|
2255
|
+
--nl-border: 3px solid #451a03;
|
|
2256
|
+
--nl-shadow: 4px 4px 0 #451a03;
|
|
2257
|
+
--status-online: #32CD32;
|
|
2258
|
+
--status-offline: #FF4500;
|
|
2259
|
+
--status-connecting: #FFA500;
|
|
2868
2260
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
}
|
|
2261
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2262
|
+
body {
|
|
2263
|
+
font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
|
|
2264
|
+
background-color: var(--nl-bg);
|
|
2265
|
+
background-image: radial-gradient(#fde68a 1px, transparent 1px), radial-gradient(#fde68a 1px, transparent 1px);
|
|
2266
|
+
background-size: 20px 20px;
|
|
2267
|
+
background-position: 0 0, 10px 10px;
|
|
2268
|
+
color: var(--nl-text);
|
|
2269
|
+
min-height: 100vh;
|
|
2270
|
+
padding: 2rem;
|
|
2880
2271
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2272
|
+
.container {
|
|
2273
|
+
max-width: 1200px;
|
|
2274
|
+
margin: 0 auto;
|
|
2275
|
+
background: var(--nl-surface);
|
|
2276
|
+
padding: 2rem;
|
|
2277
|
+
border: var(--nl-border);
|
|
2278
|
+
box-shadow: var(--nl-shadow);
|
|
2279
|
+
border-radius: 16px;
|
|
2280
|
+
}
|
|
2281
|
+
h1 {
|
|
2282
|
+
font-family: 'Fredoka One', 'Noto Sans SC', cursive;
|
|
2283
|
+
font-size: 2.5rem;
|
|
2284
|
+
margin-bottom: 2rem;
|
|
2285
|
+
text-align: center;
|
|
2286
|
+
text-shadow: 2px 2px 0 #fff;
|
|
2287
|
+
}
|
|
2288
|
+
.stats {
|
|
2289
|
+
display: grid;
|
|
2290
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
2291
|
+
gap: 1.5rem;
|
|
2292
|
+
margin-bottom: 3rem;
|
|
2293
|
+
}
|
|
2294
|
+
.stat-card {
|
|
2295
|
+
background: var(--nl-surface);
|
|
2296
|
+
border: var(--nl-border);
|
|
2297
|
+
padding: 1rem;
|
|
2298
|
+
text-align: center;
|
|
2299
|
+
box-shadow: 2px 2px 0 #451a03;
|
|
2300
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
2301
|
+
border-radius: 16px;
|
|
2302
|
+
}
|
|
2303
|
+
.stat-card:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 #451a03; }
|
|
2304
|
+
.stat-value { font-family: 'Fredoka One', sans-serif; font-size: 2.5rem; line-height: 1.2; }
|
|
2305
|
+
.stat-label { font-weight: 800; font-size: 14px; color: #92400e; }
|
|
2306
|
+
.stat-online .stat-value { color: var(--status-online); }
|
|
2307
|
+
.stat-offline .stat-value { color: var(--status-offline); }
|
|
2308
|
+
.stat-total .stat-value { color: #00BFFF; }
|
|
2309
|
+
.bots { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 2rem; }
|
|
2310
|
+
.bot-card {
|
|
2311
|
+
background: var(--nl-surface);
|
|
2312
|
+
border: var(--nl-border);
|
|
2313
|
+
border-radius: 16px;
|
|
2314
|
+
padding: 1.5rem;
|
|
2315
|
+
box-shadow: var(--nl-shadow);
|
|
2316
|
+
transition: all 0.2s ease;
|
|
2317
|
+
}
|
|
2318
|
+
.bot-card:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 #451a03; }
|
|
2319
|
+
.bot-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
|
|
2320
|
+
.bot-avatar { width: 64px; height: 64px; border: var(--nl-border); border-radius: 50%; background: #fff; }
|
|
2321
|
+
.bot-info { flex: 1; }
|
|
2322
|
+
.bot-id { font-weight: 900; font-size: 1.2rem; }
|
|
2323
|
+
.bot-protocol { color: #92400e; font-size: 0.9rem; font-weight: 700; background: #fffbeb; display: inline-block; padding: 2px 6px; border: 2px solid #451a03; border-radius: 6px; margin-top: 4px; }
|
|
2324
|
+
.status-badge { padding: 0.4rem 0.8rem; border: 2px solid #451a03; border-radius: 20px; font-size: 0.8rem; font-weight: 900; }
|
|
2325
|
+
.status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
|
|
2326
|
+
.status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
|
|
2327
|
+
.status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
|
|
2328
|
+
.bot-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; background: #fff7ed; padding: 1rem; border: 2px solid #451a03; border-radius: 12px; }
|
|
2329
|
+
.bot-stat { display: flex; flex-direction: column; }
|
|
2330
|
+
.bot-stat-label { font-size: 0.75rem; font-weight: 800; color: #92400e; }
|
|
2331
|
+
.bot-stat-value { font-weight: 700; font-size: 1rem; }
|
|
2332
|
+
.footer { margin-top: 3rem; text-align: center; font-weight: 700; color: #92400e; }
|
|
2333
|
+
.refresh-btn {
|
|
2334
|
+
position: fixed;
|
|
2335
|
+
bottom: 2rem;
|
|
2336
|
+
right: 2rem;
|
|
2337
|
+
background: var(--nl-primary);
|
|
2338
|
+
border: var(--nl-border);
|
|
2339
|
+
color: var(--nl-text);
|
|
2340
|
+
padding: 1rem 2rem;
|
|
2341
|
+
border-radius: 12px;
|
|
2342
|
+
cursor: pointer;
|
|
2343
|
+
font-size: 1.1rem;
|
|
2344
|
+
font-weight: 800;
|
|
2345
|
+
box-shadow: var(--nl-shadow);
|
|
2346
|
+
transition: all 0.1s;
|
|
2347
|
+
z-index: 100;
|
|
2348
|
+
}
|
|
2349
|
+
.refresh-btn:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 #451a03; }
|
|
2350
|
+
.refresh-btn:active { transform: translate(1px, 1px); box-shadow: 2px 2px 0 #451a03; }
|
|
2351
|
+
.admin-btn {
|
|
2352
|
+
position: fixed;
|
|
2353
|
+
bottom: 2rem;
|
|
2354
|
+
right: 9rem;
|
|
2355
|
+
background: #f3f4f6;
|
|
2356
|
+
border: var(--nl-border);
|
|
2357
|
+
color: var(--nl-text);
|
|
2358
|
+
padding: 1rem 2rem;
|
|
2359
|
+
border-radius: 12px;
|
|
2360
|
+
cursor: pointer;
|
|
2361
|
+
font-size: 1.1rem;
|
|
2362
|
+
font-weight: 800;
|
|
2363
|
+
box-shadow: var(--nl-shadow);
|
|
2364
|
+
transition: all 0.1s;
|
|
2365
|
+
z-index: 100;
|
|
2366
|
+
text-decoration: none;
|
|
2367
|
+
}
|
|
2368
|
+
.admin-btn:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 #451a03; }
|
|
2369
|
+
.admin-btn:active { transform: translate(1px, 1px); box-shadow: 2px 2px 0 #451a03; }
|
|
2370
|
+
@media (max-width: 768px) {
|
|
2371
|
+
body { padding: 1rem; }
|
|
2372
|
+
.container { padding: 1rem; }
|
|
2373
|
+
.stats { grid-template-columns: 1fr; }
|
|
2374
|
+
}
|
|
2375
|
+
</style>
|
|
2376
|
+
</head>
|
|
2377
|
+
<body>
|
|
2378
|
+
<div class="container">
|
|
2379
|
+
<h1 ondblclick="window.location='${basePath}/admin'" style="cursor: default;" title="双击进入管理">OneBot Multi 状态面板</h1>
|
|
2380
|
+
<div class="stats">
|
|
2381
|
+
<div class="stat-card stat-online"><div class="stat-value">${onlineCount}</div><div class="stat-label">在线</div></div>
|
|
2382
|
+
<div class="stat-card stat-offline"><div class="stat-value">${offlineCount}</div><div class="stat-label">离线</div></div>
|
|
2383
|
+
<div class="stat-card stat-total"><div class="stat-value">${totalCount}</div><div class="stat-label">总计</div></div>
|
|
2384
|
+
</div>
|
|
2385
|
+
<div class="bots">${botCardsHtml}</div>
|
|
2386
|
+
<div class="footer">更新时间: ${updatedAtStr}</div>
|
|
2387
|
+
</div>
|
|
2388
|
+
<a href="${basePath}/admin" class="admin-btn">管理 🛠️</a>
|
|
2389
|
+
<button class="refresh-btn" onclick="location.reload()">刷新 ⚡</button>
|
|
2390
|
+
<script>setTimeout(() => location.reload(), 30000)</script>
|
|
2887
2391
|
</body>
|
|
2888
2392
|
</html>`;
|
|
2889
2393
|
}
|
|
@@ -2898,7 +2402,7 @@ var StatusPanel = class {
|
|
|
2898
2402
|
|
|
2899
2403
|
// src/config-manager.ts
|
|
2900
2404
|
var import_crypto2 = require("crypto");
|
|
2901
|
-
var
|
|
2405
|
+
var SYSTEM_CONFIG_NAME = "_system_";
|
|
2902
2406
|
function generateSalt() {
|
|
2903
2407
|
return (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
2904
2408
|
}
|
|
@@ -2924,18 +2428,20 @@ var ConfigManager = class {
|
|
|
2924
2428
|
this.ctx = ctx;
|
|
2925
2429
|
ctx.model.extend("onebot_multi_bots", {
|
|
2926
2430
|
id: "unsigned",
|
|
2927
|
-
|
|
2928
|
-
|
|
2431
|
+
name: "string",
|
|
2432
|
+
// 配置名称/别名
|
|
2433
|
+
selfId: { type: "string", nullable: true },
|
|
2434
|
+
// 连接成功后自动填充,可为空
|
|
2435
|
+
token: { type: "string", nullable: true },
|
|
2929
2436
|
protocol: "string",
|
|
2930
|
-
endpoint: "string",
|
|
2931
|
-
path: "string",
|
|
2437
|
+
endpoint: { type: "string", nullable: true },
|
|
2438
|
+
path: { type: "string", nullable: true },
|
|
2932
2439
|
enabled: "boolean",
|
|
2933
2440
|
createdAt: "timestamp",
|
|
2934
2441
|
updatedAt: "timestamp"
|
|
2935
2442
|
}, {
|
|
2936
2443
|
autoInc: true,
|
|
2937
|
-
primary: "id"
|
|
2938
|
-
unique: [["selfId"]]
|
|
2444
|
+
primary: "id"
|
|
2939
2445
|
});
|
|
2940
2446
|
}
|
|
2941
2447
|
static {
|
|
@@ -2946,7 +2452,7 @@ var ConfigManager = class {
|
|
|
2946
2452
|
* 获取存储的密码哈希
|
|
2947
2453
|
*/
|
|
2948
2454
|
async getStoredPasswordHash() {
|
|
2949
|
-
const results = await this.ctx.database.get("onebot_multi_bots", {
|
|
2455
|
+
const results = await this.ctx.database.get("onebot_multi_bots", { name: SYSTEM_CONFIG_NAME });
|
|
2950
2456
|
return results[0]?.token || null;
|
|
2951
2457
|
}
|
|
2952
2458
|
/**
|
|
@@ -2954,12 +2460,12 @@ var ConfigManager = class {
|
|
|
2954
2460
|
*/
|
|
2955
2461
|
async setAdminPassword(password) {
|
|
2956
2462
|
const hashedPassword = createPasswordHash(password);
|
|
2957
|
-
const existing = await this.ctx.database.get("onebot_multi_bots", {
|
|
2463
|
+
const existing = await this.ctx.database.get("onebot_multi_bots", { name: SYSTEM_CONFIG_NAME });
|
|
2958
2464
|
if (existing.length > 0) {
|
|
2959
|
-
await this.ctx.database.set("onebot_multi_bots", {
|
|
2465
|
+
await this.ctx.database.set("onebot_multi_bots", { name: SYSTEM_CONFIG_NAME }, { token: hashedPassword, updatedAt: /* @__PURE__ */ new Date() });
|
|
2960
2466
|
} else {
|
|
2961
2467
|
await this.ctx.database.create("onebot_multi_bots", {
|
|
2962
|
-
|
|
2468
|
+
name: SYSTEM_CONFIG_NAME,
|
|
2963
2469
|
token: hashedPassword,
|
|
2964
2470
|
protocol: "ws-reverse",
|
|
2965
2471
|
enabled: false,
|
|
@@ -2996,65 +2502,82 @@ var ConfigManager = class {
|
|
|
2996
2502
|
*/
|
|
2997
2503
|
async getAllConfigs() {
|
|
2998
2504
|
const all = await this.ctx.database.get("onebot_multi_bots", {});
|
|
2999
|
-
return all.filter((r) => r.
|
|
2505
|
+
return all.filter((r) => r.name !== SYSTEM_CONFIG_NAME);
|
|
3000
2506
|
}
|
|
3001
2507
|
/**
|
|
3002
2508
|
* 获取启用的 Bot 配置(排除系统配置)
|
|
3003
2509
|
*/
|
|
3004
2510
|
async getEnabledConfigs() {
|
|
3005
2511
|
const all = await this.ctx.database.get("onebot_multi_bots", { enabled: true });
|
|
3006
|
-
return all.filter((r) => r.
|
|
2512
|
+
return all.filter((r) => r.name !== SYSTEM_CONFIG_NAME);
|
|
3007
2513
|
}
|
|
3008
2514
|
/**
|
|
3009
|
-
* 获取单个 Bot 配置
|
|
2515
|
+
* 根据 ID 获取单个 Bot 配置
|
|
3010
2516
|
*/
|
|
3011
|
-
async
|
|
3012
|
-
const results = await this.ctx.database.get("onebot_multi_bots", {
|
|
2517
|
+
async getConfigById(id) {
|
|
2518
|
+
const results = await this.ctx.database.get("onebot_multi_bots", { id });
|
|
3013
2519
|
return results[0];
|
|
3014
2520
|
}
|
|
2521
|
+
/**
|
|
2522
|
+
* 根据 selfId 获取配置(用于关联运行时 Bot)
|
|
2523
|
+
*/
|
|
2524
|
+
async getConfigBySelfId(selfId) {
|
|
2525
|
+
const results = await this.ctx.database.get("onebot_multi_bots", { selfId });
|
|
2526
|
+
const filtered = results.filter((r) => r.name !== SYSTEM_CONFIG_NAME);
|
|
2527
|
+
return filtered[0];
|
|
2528
|
+
}
|
|
3015
2529
|
/**
|
|
3016
2530
|
* 创建 Bot 配置
|
|
3017
2531
|
*/
|
|
3018
2532
|
async createConfig(config) {
|
|
3019
2533
|
const now = /* @__PURE__ */ new Date();
|
|
2534
|
+
const { selfId, ...rest } = config;
|
|
3020
2535
|
const record = await this.ctx.database.create("onebot_multi_bots", {
|
|
3021
|
-
...
|
|
2536
|
+
...rest,
|
|
3022
2537
|
createdAt: now,
|
|
3023
2538
|
updatedAt: now
|
|
3024
2539
|
});
|
|
3025
2540
|
return record;
|
|
3026
2541
|
}
|
|
3027
2542
|
/**
|
|
3028
|
-
* 更新 Bot
|
|
2543
|
+
* 更新 Bot 配置(根据 ID)
|
|
3029
2544
|
*/
|
|
3030
|
-
async
|
|
3031
|
-
const result = await this.ctx.database.set("onebot_multi_bots", {
|
|
2545
|
+
async updateConfigById(id, updates) {
|
|
2546
|
+
const result = await this.ctx.database.set("onebot_multi_bots", { id }, {
|
|
3032
2547
|
...updates,
|
|
3033
2548
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3034
2549
|
});
|
|
3035
2550
|
return result.modified > 0;
|
|
3036
2551
|
}
|
|
3037
2552
|
/**
|
|
3038
|
-
* 删除 Bot
|
|
2553
|
+
* 删除 Bot 配置(根据 ID)
|
|
3039
2554
|
*/
|
|
3040
|
-
async
|
|
3041
|
-
const result = await this.ctx.database.remove("onebot_multi_bots", {
|
|
2555
|
+
async deleteConfigById(id) {
|
|
2556
|
+
const result = await this.ctx.database.remove("onebot_multi_bots", { id });
|
|
3042
2557
|
return result.removed > 0;
|
|
3043
2558
|
}
|
|
3044
2559
|
/**
|
|
3045
2560
|
* 切换 Bot 启用状态
|
|
3046
2561
|
*/
|
|
3047
|
-
async toggleEnabled(
|
|
3048
|
-
const config = await this.
|
|
2562
|
+
async toggleEnabled(id) {
|
|
2563
|
+
const config = await this.getConfigById(id);
|
|
3049
2564
|
if (!config) return false;
|
|
3050
|
-
return this.
|
|
2565
|
+
return this.updateConfigById(id, { enabled: !config.enabled });
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* 更新配置的 selfId(Bot 连接成功后调用)
|
|
2569
|
+
*/
|
|
2570
|
+
async updateSelfId(id, selfId) {
|
|
2571
|
+
return this.updateConfigById(id, { selfId });
|
|
3051
2572
|
}
|
|
3052
2573
|
/**
|
|
3053
|
-
* 将数据库配置转换为 BotConfig
|
|
2574
|
+
* 将数据库配置转换为 BotConfig 格式(用于启动 Bot)
|
|
3054
2575
|
*/
|
|
3055
2576
|
toBotConfig(record) {
|
|
3056
2577
|
return {
|
|
3057
|
-
|
|
2578
|
+
configId: record.id,
|
|
2579
|
+
selfId: record.selfId || `pending_${record.id}`,
|
|
2580
|
+
// 临时 ID,连接后会更新
|
|
3058
2581
|
token: record.token,
|
|
3059
2582
|
protocol: record.protocol,
|
|
3060
2583
|
endpoint: record.endpoint,
|
|
@@ -3087,7 +2610,8 @@ var Config = import_koishi11.Schema.intersect([
|
|
|
3087
2610
|
]);
|
|
3088
2611
|
var name = "adapter-onebot-multi";
|
|
3089
2612
|
var inject = {
|
|
3090
|
-
required: ["server", "database"]
|
|
2613
|
+
required: ["server", "database"],
|
|
2614
|
+
optional: ["console"]
|
|
3091
2615
|
};
|
|
3092
2616
|
function apply(ctx, config) {
|
|
3093
2617
|
const logger = ctx.logger("adapter-onebot-multi");
|
|
@@ -3105,6 +2629,25 @@ function apply(ctx, config) {
|
|
|
3105
2629
|
if (config.panel?.enabled) {
|
|
3106
2630
|
new StatusPanel(ctx, config.panel, statusManager, configManager);
|
|
3107
2631
|
}
|
|
2632
|
+
ctx.inject(["console"], (ctx2) => {
|
|
2633
|
+
ctx2.console.addEntry({
|
|
2634
|
+
dev: (0, import_path.resolve)(__dirname, "../client/index.ts"),
|
|
2635
|
+
prod: (0, import_path.resolve)(__dirname, "../dist")
|
|
2636
|
+
});
|
|
2637
|
+
ctx2.console.addListener("onebot-multi/config", () => {
|
|
2638
|
+
return {
|
|
2639
|
+
basePath: config.panel?.basePath || "/status",
|
|
2640
|
+
enabled: config.panel?.enabled || false
|
|
2641
|
+
};
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
ctx.on("onebot-multi/bot-online", async (bot) => {
|
|
2645
|
+
const configId = bot.config.configId;
|
|
2646
|
+
if (configId && bot.selfId) {
|
|
2647
|
+
await configManager.updateSelfId(configId, bot.selfId);
|
|
2648
|
+
logger.info(`Bot 配置 #${configId} 已关联 QQ: ${bot.selfId}`);
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
3108
2651
|
const startBots = /* @__PURE__ */ __name(async () => {
|
|
3109
2652
|
const dbConfigs = await configManager.getEnabledConfigs();
|
|
3110
2653
|
if (dbConfigs.length > 0) {
|
|
@@ -3131,12 +2674,12 @@ function startBot(ctx, botConfig, globalConfig, logger) {
|
|
|
3131
2674
|
};
|
|
3132
2675
|
if (protocol === "ws") {
|
|
3133
2676
|
if (!botConfig.endpoint) {
|
|
3134
|
-
logger.warn(
|
|
2677
|
+
logger.warn(`配置 #${botConfig.configId} 使用 ws 协议但未配置 endpoint,跳过`);
|
|
3135
2678
|
return;
|
|
3136
2679
|
}
|
|
3137
|
-
logger.info(
|
|
2680
|
+
logger.info(`启动配置 #${botConfig.configId} (ws → ${botConfig.endpoint})`);
|
|
3138
2681
|
} else {
|
|
3139
|
-
logger.info(
|
|
2682
|
+
logger.info(`启动配置 #${botConfig.configId} (ws-reverse ← ${botConfig.path || "/onebot"})`);
|
|
3140
2683
|
}
|
|
3141
2684
|
ctx.plugin(OneBotBot, fullConfig);
|
|
3142
2685
|
}
|