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/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((resolve, reject) => {
983
- listeners[data.echo] = resolve;
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
- port: import_koishi10.Schema.natural().default(8212).description("面板端口。"),
1368
- basePath: import_koishi10.Schema.string().default("/status").description("面板路径。")
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
- this.start();
1380
- ctx.on("dispose", () => this.stop());
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 = null;
1404
+ server;
1387
1405
  async parseJsonBody(ctx) {
1388
- return new Promise((resolve, reject) => {
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
- resolve(body ? JSON.parse(body) : {});
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
- // 不返回: protocol, endpoint, path, token 等敏感信息
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
- const adminPath = `${basePath}/admin`;
1427
- const authMiddleware = /* @__PURE__ */ __name(async (ctx, next) => {
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
- ctx.body = { needsInit: !hasPassword };
1441
- });
1442
- router.get(`${adminPath}/api/check-auth`, async (ctx) => {
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/init-password`, async (ctx) => {
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=/; HttpOnly; SameSite=Lax; Max-Age=${TOKEN_EXPIRY / 1e3}`);
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.debug(`登录尝试: ${valid ? "成功" : "失败"}`);
1480
+ this.ctx.logger.info(`登录尝试: ${valid ? "成功" : "失败"}`);
1471
1481
  if (valid) {
1472
1482
  const token = generateSignedToken();
1473
- this.ctx.logger.debug(`生成签名 token`);
1474
- ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${TOKEN_EXPIRY / 1e3}`);
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
- if (!hasPassword) {
1491
- ctx.body = this.renderSetupPage();
1492
- } else {
1493
- const cookies = ctx.headers.cookie || "";
1494
- const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
1495
- const token = tokenMatch ? tokenMatch[1] : null;
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: botStatus?.nickname,
1522
+ nickname: runtime?.nickname,
1513
1523
  protocol: config.protocol,
1514
- status: botStatus?.status || "offline",
1524
+ status: runtime?.status || "offline",
1515
1525
  endpoint: config.endpoint,
1516
1526
  path: config.path,
1517
1527
  enabled: config.enabled,
1518
- messageReceived: botStatus?.messageReceived,
1519
- messageSent: botStatus?.messageSent,
1520
- lastMessageTime: botStatus?.lastMessageTime,
1521
- startupTime: botStatus?.startupTime,
1522
- groupCount: botStatus?.groupCount,
1523
- friendCount: botStatus?.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
- const data = await this.parseJsonBody(ctx);
1530
- if (!data.selfId) {
1531
- ctx.status = 400;
1532
- ctx.body = { error: "selfId 不能为空" };
1533
- return;
1534
- }
1535
- const existing = await this.configManager.getConfig(data.selfId);
1536
- if (existing) {
1537
- ctx.status = 400;
1538
- ctx.body = { error: `Bot ${data.selfId} 已存在` };
1539
- return;
1540
- }
1541
- if (data.protocol === "ws" && !data.endpoint) {
1542
- ctx.status = 400;
1543
- ctx.body = { error: "WS 协议需要配置 endpoint" };
1544
- return;
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/update`, authMiddleware, async (ctx) => {
1570
+ router.post(`${adminPath}/api/bots/delete`, authMiddleware, async (ctx) => {
1559
1571
  const data = await this.parseJsonBody(ctx);
1560
- const existing = await this.configManager.getConfig(data.selfId);
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: `Bot ${data.selfId} 不存在` };
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
- await this.configManager.updateConfig(data.selfId, data.updates);
1572
- this.ctx.logger.info(`更新 Bot 配置: ${data.selfId}`);
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.stopBot(selfId);
1585
- await this.configManager.deleteConfig(selfId);
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 selfId = data.selfId;
1592
- const existing = await this.configManager.getConfig(selfId);
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: `Bot ${selfId} 不存在` };
1592
+ ctx.body = { error: `配置 #${id} 不存在` };
1596
1593
  return;
1597
1594
  }
1598
1595
  const newEnabled = !existing.enabled;
1599
- await this.configManager.updateConfig(selfId, { enabled: newEnabled });
1596
+ await this.configManager.updateConfigById(id, { enabled: newEnabled });
1600
1597
  if (newEnabled) {
1601
- const config = await this.configManager.getConfig(selfId);
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
- await this.stopBot(selfId);
1603
+ if (existing.selfId) {
1604
+ await this.stopBot(existing.selfId);
1605
+ }
1607
1606
  }
1608
- this.ctx.logger.info(`${newEnabled ? "启用" : "禁用"} Bot: ${selfId}`);
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 selfId = data.selfId;
1614
- const config = await this.configManager.getConfig(selfId);
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: `Bot ${selfId} 不存在` };
1616
+ ctx.body = { error: `配置 #${id} 不存在` };
1618
1617
  return;
1619
1618
  }
1620
1619
  if (!config.enabled) {
1621
1620
  ctx.status = 400;
1622
- ctx.body = { error: `Bot ${selfId} 未启用` };
1621
+ ctx.body = { error: `配置 #${id} 未启用` };
1623
1622
  return;
1624
1623
  }
1625
- await this.stopBot(selfId);
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: ${selfId}`);
1628
+ this.ctx.logger.info(`重启 Bot 配置: #${id} ${config.name}`);
1628
1629
  ctx.body = { success: true };
1629
1630
  });
1630
- this.app.use(router.routes());
1631
- this.app.use(router.allowedMethods());
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
- start() {
1655
- const port = this.config.port || 8212;
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 - 管理登录</title>
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: 400px;
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: 2rem;
1714
+ margin-bottom: 1rem;
1719
1715
  text-shadow: 2px 2px 0 #fff;
1720
1716
  }
1721
-
1722
- .form-group {
1723
- margin-bottom: 1.5rem;
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
- .btn:hover {
1762
- transform: translate(-1px, -1px);
1763
- box-shadow: 5px 5px 0 #451a03;
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
- .btn:active {
1767
- transform: translate(1px, 1px);
1768
- box-shadow: 2px 2px 0 #451a03;
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
- .error {
1772
- color: #b91c1c;
1773
- margin-top: 1rem;
1774
- display: none;
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
- <div class="login-box">
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="adminKey">管理密钥</label>
1784
- <input type="password" id="adminKey" placeholder="请输入管理密钥" required>
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="error">密钥错误</p>
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
- <script>
1792
- document.getElementById('loginForm').addEventListener('submit', async function(e) {
1793
- e.preventDefault()
1794
- const password = document.getElementById('adminKey').value
1795
- const errorEl = document.getElementById('error')
1796
- const btn = this.querySelector('button[type="submit"]')
1797
- btn.disabled = true
1798
- btn.textContent = '登录中...'
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
- try {
1801
- const res = await fetch('${basePath}/admin/api/login', {
1802
- method: 'POST',
1803
- headers: { 'Content-Type': 'application/json' },
1804
- body: JSON.stringify({ password }),
1805
- credentials: 'include'
1806
- })
1807
- const data = await res.json()
1808
- if (data.success) {
1809
- // 短暂延迟确保 Cookie 已设置
1810
- setTimeout(() => {
1811
- window.location.href = '${basePath}/admin'
1812
- }, 100)
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
- errorEl.textContent = data.error || '登录失败'
1815
- errorEl.style.display = 'block'
1816
- btn.disabled = false
1817
- btn.textContent = '登录'
1921
+ el.style.display = 'none'
1922
+ el.classList.remove('show')
1818
1923
  }
1819
- } catch (e) {
1820
- errorEl.textContent = '网络错误'
1821
- errorEl.style.display = 'block'
1822
- btn.disabled = false
1823
- btn.textContent = '登录'
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
- * { margin: 0; padding: 0; box-sizing: border-box; }
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
- body {
1852
- font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
1853
- background-color: var(--nl-bg);
1854
- background-image:
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
- .setup-box {
1868
- background: var(--nl-surface);
1869
- padding: 3rem;
1870
- border: var(--nl-border);
1871
- box-shadow: var(--nl-shadow);
1872
- border-radius: 16px;
1873
- max-width: 450px;
1874
- width: 100%;
1875
- text-align: center;
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
- h1 {
1879
- font-family: 'Fredoka One', 'Noto Sans SC', cursive;
1880
- font-size: 1.8rem;
1881
- margin-bottom: 1rem;
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
- .desc {
1886
- color: #92400e;
1887
- margin-bottom: 2rem;
1888
- font-weight: 600;
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
- .form-group {
1892
- margin-bottom: 1.5rem;
1893
- text-align: left;
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
- label {
1897
- display: block;
1898
- font-weight: 700;
1899
- margin-bottom: 0.5rem;
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
- input[type="password"] {
1903
- width: 100%;
1904
- padding: 1rem;
1905
- border: var(--nl-border);
1906
- border-radius: 8px;
1907
- font-size: 1rem;
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
- input[type="password"]:focus {
1912
- outline: none;
1913
- box-shadow: 0 0 0 3px #fbbf24;
1914
- }
2001
+ if (password !== confirm) {
2002
+ errorEl.textContent = '两次密码不一致'
2003
+ errorEl.classList.add('show')
2004
+ return
2005
+ }
1915
2006
 
1916
- .btn {
1917
- background: var(--nl-primary);
1918
- border: var(--nl-border);
1919
- color: var(--nl-text);
1920
- padding: 1rem 2rem;
1921
- border-radius: 12px;
1922
- cursor: pointer;
1923
- font-size: 1.1rem;
1924
- font-weight: 800;
1925
- box-shadow: var(--nl-shadow);
1926
- transition: all 0.1s;
1927
- width: 100%;
1928
- }
1929
-
1930
- .btn:hover {
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
- <script>
2721
- const BASE_PATH = '${basePath}/admin'
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
- async function apiCall(endpoint, data = null) {
2724
- const options = {
2725
- method: data ? 'POST' : 'GET',
2726
- headers: {
2727
- 'Content-Type': 'application/json'
2728
- },
2729
- credentials: 'include'
2730
- }
2731
- if (data) {
2732
- options.body = JSON.stringify(data)
2733
- }
2734
- const res = await fetch(BASE_PATH + endpoint, options)
2735
- if (res.status === 401) {
2736
- // 认证失败,重定向到登录页
2737
- window.location.href = '${basePath}/admin'
2738
- throw new Error('认证失败')
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
- return res.json()
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
- function formatUptime(startTime) {
2751
- const diff = Math.floor(Date.now() / 1000 - startTime)
2752
- const hours = Math.floor(diff / 3600)
2753
- const minutes = Math.floor((diff % 3600) / 60)
2754
- if (hours > 0) return hours + 'h ' + minutes + 'm'
2755
- return minutes + 'm'
2053
+ // 登出
2054
+ async function logout() {
2055
+ await api('/auth/logout', {})
2056
+ clearToken()
2057
+ showView('loginView')
2756
2058
  }
2757
2059
 
2758
- async function refreshList() {
2060
+ // 刷新 Bot 列表
2061
+ async function refreshBots() {
2759
2062
  try {
2760
- const data = await apiCall('/api/list')
2761
- if (data.error) {
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
- console.error('refreshList error:', e)
2768
- showToast('加载失败: ' + e.message, 'error')
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</h3><p>点击"添加 Bot"按钮创建</p></div>'
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.selfId
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 disabledClass = !bot.enabled ? 'disabled' : ''
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 ' + disabledClass + '">' +
2090
+ return '<div class="bot-card' + (!bot.enabled ? ' disabled' : '') + '">' +
2786
2091
  '<div class="bot-header">' +
2787
- '<img class="bot-avatar" src="http://q.qlogo.cn/headimg_dl?dst_uin=' + bot.selfId + '&spec=640" alt="avatar">' +
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() + ' | QQ: ' + bot.selfId + '</div>' +
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('' + bot.selfId + '')">' + (bot.enabled ? '禁用' : '启用') + '</button>' +
2804
- '<button class="btn btn-secondary" onclick="restartBot('' + bot.selfId + '')">重启</button>' +
2805
- '<button class="btn btn-danger" onclick="deleteBot('' + bot.selfId + '')">删除</button>' +
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
- selfId: document.getElementById('selfId').value,
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
- const result = await apiCall('/api/create', data)
2839
- if (result.error) throw new Error(result.error)
2840
- showToast('Bot 添加成功')
2184
+ await api('/bots/create', data)
2185
+ showToast('配置添加成功')
2841
2186
  hideAddModal()
2842
- refreshList()
2187
+ refreshBots()
2843
2188
  } catch (e) {
2844
2189
  showToast('添加失败: ' + e.message, 'error')
2845
2190
  }
2846
2191
  })
2847
2192
 
2848
- async function toggleBot(selfId) {
2849
- try {
2850
- const result = await apiCall('/api/toggle', { selfId })
2851
- if (result.error) throw new Error(result.error)
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
- async function restartBot(selfId) {
2860
- try {
2861
- const result = await apiCall('/api/restart', { selfId })
2862
- if (result.error) throw new Error(result.error)
2863
- showToast('Bot 正在重启')
2864
- setTimeout(refreshList, 1000)
2865
- } catch (e) {
2866
- showToast('重启失败: ' + e.message, 'error')
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
- async function deleteBot(selfId) {
2871
- if (!confirm('确定要删除 Bot ' + selfId + ' 吗?')) return
2872
- try {
2873
- const result = await apiCall('/api/delete', { selfId })
2874
- if (result.error) throw new Error(result.error)
2875
- showToast('Bot 已删除')
2876
- refreshList()
2877
- } catch (e) {
2878
- showToast('删除失败: ' + e.message, 'error')
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
- refreshList()
2884
- // 自动刷新
2885
- setInterval(refreshList, 30000)
2886
- </script>
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 SYSTEM_CONFIG_ID = "_system_";
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
- selfId: "string",
2928
- token: "string",
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", { selfId: SYSTEM_CONFIG_ID });
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", { selfId: SYSTEM_CONFIG_ID });
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", { selfId: SYSTEM_CONFIG_ID }, { token: hashedPassword, updatedAt: /* @__PURE__ */ new Date() });
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
- selfId: SYSTEM_CONFIG_ID,
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.selfId !== SYSTEM_CONFIG_ID);
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.selfId !== SYSTEM_CONFIG_ID);
2512
+ return all.filter((r) => r.name !== SYSTEM_CONFIG_NAME);
3007
2513
  }
3008
2514
  /**
3009
- * 获取单个 Bot 配置
2515
+ * 根据 ID 获取单个 Bot 配置
3010
2516
  */
3011
- async getConfig(selfId) {
3012
- const results = await this.ctx.database.get("onebot_multi_bots", { selfId });
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
- ...config,
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 updateConfig(selfId, updates) {
3031
- const result = await this.ctx.database.set("onebot_multi_bots", { selfId }, {
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 deleteConfig(selfId) {
3041
- const result = await this.ctx.database.remove("onebot_multi_bots", { selfId });
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(selfId) {
3048
- const config = await this.getConfig(selfId);
2562
+ async toggleEnabled(id) {
2563
+ const config = await this.getConfigById(id);
3049
2564
  if (!config) return false;
3050
- return this.updateConfig(selfId, { enabled: !config.enabled });
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
- selfId: record.selfId,
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(`Bot ${botConfig.selfId} 使用 ws 协议但未配置 endpoint,跳过`);
2677
+ logger.warn(`配置 #${botConfig.configId} 使用 ws 协议但未配置 endpoint,跳过`);
3135
2678
  return;
3136
2679
  }
3137
- logger.info(`创建 Bot: ${botConfig.selfId} (ws → ${botConfig.endpoint})`);
2680
+ logger.info(`启动配置 #${botConfig.configId} (ws → ${botConfig.endpoint})`);
3138
2681
  } else {
3139
- logger.info(`创建 Bot: ${botConfig.selfId} (ws-reverse ← ${botConfig.path || "/onebot"})`);
2682
+ logger.info(`启动配置 #${botConfig.configId} (ws-reverse ← ${botConfig.path || "/onebot"})`);
3140
2683
  }
3141
2684
  ctx.plugin(OneBotBot, fullConfig);
3142
2685
  }