onebots 1.0.6 → 1.1.0

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/app.js CHANGED
@@ -1,48 +1,20 @@
1
- import { BaseApp, yaml, ProtocolRegistry, configure, readLine, createManagedTokenValidator, initTokenManager, } from "@onebots/core";
1
+ import { BaseApp, yaml, ProtocolRegistry, configure, readLine, initTokenManager, } from "@onebots/core";
2
2
  import { getAppConfigSchema } from "./config-schema.js";
3
+ import { registerAuthRoutes } from "./routes/auth.js";
4
+ import { registerConfigRoutes } from "./routes/config.js";
5
+ import { registerAdapterRoutes } from "./routes/adapter-api.js";
6
+ import { registerVerificationRoutes } from "./routes/verification.js";
7
+ import { registerTerminalRoutes } from "./routes/terminal.js";
8
+ import { registerPublicStaticRoutes } from "./routes/public-static.js";
3
9
  import * as path from "path";
4
10
  import * as fs from "fs";
5
11
  import { createRequire } from "module";
6
12
  import { pathToFileURL } from "url";
7
13
  import koaStatic from "koa-static";
8
14
  import { copyFileSync, existsSync, writeFileSync, mkdirSync, readFileSync } from "fs";
9
- import * as pty from "@karinjs/node-pty";
10
15
  import { randomBytes } from "node:crypto";
11
16
  import { execFileSync } from "node:child_process";
12
17
  const require = createRequire(pathToFileURL(path.join(process.cwd(), 'node_modules')));
13
- /** 站点静态根目录下的文件名:禁止路径分隔与控制字符,仅使用 basename */
14
- function sanitizePublicStaticBasename(original) {
15
- if (original == null || String(original).trim() === '')
16
- return null;
17
- let raw = String(original).trim();
18
- try {
19
- raw = decodeURIComponent(raw);
20
- }
21
- catch {
22
- return null;
23
- }
24
- if (/[\\/]/.test(raw) || raw.includes('..'))
25
- return null;
26
- if (/[\x00-\x1f]/.test(raw))
27
- return null;
28
- const base = path.basename(raw);
29
- if (!base || base !== raw || base === '.' || base === '..')
30
- return null;
31
- if (base.length > 255)
32
- return null;
33
- return base;
34
- }
35
- function pickPublicStaticUpload(files) {
36
- if (!files || typeof files !== 'object')
37
- return null;
38
- const raw = files.file ?? files.upload;
39
- if (!raw)
40
- return null;
41
- const file = Array.isArray(raw) ? raw[0] : raw;
42
- if (!file || typeof file !== 'object')
43
- return null;
44
- return file;
45
- }
46
18
  // 多目录依次查找 @onebots/web/dist,找到即用(Docker/HF 从 development 启动时 cwd 下无 @onebots/web)
47
19
  const client = (() => {
48
20
  const rel = ['..', 'node_modules', '@onebots', 'web', 'dist'];
@@ -61,8 +33,12 @@ const client = (() => {
61
33
  console.log('[onebots] 未找到 @onebots/web/dist,管理端页面将不可用');
62
34
  return '';
63
35
  })();
64
- /** 当前实例是否使用了自动生成的管理端账号(用于登录成功后提示用户修改密码) */
65
- let credentialsWereAutoGenerated = false;
36
+ /**
37
+ * 当前实例是否使用了自动生成的管理端账号(用于登录成功后提示用户修改密码)。
38
+ * 由 createOnebots 设置,然后复制到 App 实例的 credentialsWereAutoGenerated 属性中。
39
+ * 此模块级变量保持仅供 createOnebots 使用;路由模块访问 app.credentialsWereAutoGenerated。
40
+ */
41
+ let _credentialsWereAutoGenerated = false;
66
42
  export class App extends BaseApp {
67
43
  ws;
68
44
  logCacheFile;
@@ -75,16 +51,24 @@ export class App extends BaseApp {
75
51
  static MAX_PENDING_VERIFICATIONS = 20; // 最多保留条数,避免堆积过多
76
52
  ptyTerminal = null;
77
53
  terminalClients = new Set();
54
+ /** 管理端 Token 默认过期时间(毫秒) */
55
+ static DEFAULT_TOKEN_EXPIRATION_MS = 12 * 60 * 60 * 1000;
56
+ /** 管理端 Token 刷新过期时间(毫秒) */
57
+ static REFRESH_TOKEN_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000;
78
58
  tokenManager = initTokenManager({
79
- defaultExpiration: 12 * 60 * 60 * 1000,
80
- refreshExpiration: 7 * 24 * 60 * 60 * 1000,
59
+ defaultExpiration: App.DEFAULT_TOKEN_EXPIRATION_MS,
60
+ refreshExpiration: App.REFRESH_TOKEN_EXPIRATION_MS,
81
61
  });
62
+ /** 当前实例是否使用了自动生成的管理端账号 */
63
+ credentialsWereAutoGenerated;
82
64
  constructor(config) {
83
65
  super(config);
84
66
  // 1. 初始化日志缓存文件
85
67
  this.logCacheFile = path.join(process.cwd(), 'data', 'terminal-logs.txt');
86
68
  this.initLogCache();
87
- // 2. 初始化 WebSocket
69
+ // 2. 从模块级变量复制自动生成凭据状态
70
+ this.credentialsWereAutoGenerated = _credentialsWereAutoGenerated;
71
+ // 3. 初始化 WebSocket
88
72
  this.ws = this.router.ws("/");
89
73
  // 3. 监听进程退出,清空缓存
90
74
  process.on('exit', () => {
@@ -226,7 +210,7 @@ export class App extends BaseApp {
226
210
  }
227
211
  }
228
212
  catch (e) {
229
- console.error('清空日志缓存失败:', e);
213
+ this.logger.error('清空日志缓存失败:', e);
230
214
  }
231
215
  }
232
216
  cacheLog(message) {
@@ -303,118 +287,13 @@ export class App extends BaseApp {
303
287
  }
304
288
  }
305
289
  async start() {
306
- const authValidator = createManagedTokenValidator(this.tokenManager, {
307
- tokenName: 'access_token',
308
- errorMessage: 'Unauthorized',
309
- });
310
- const expectedUsername = this.config.username ?? BaseApp.defaultConfig.username;
311
- const expectedPassword = this.config.password ?? BaseApp.defaultConfig.password;
312
- const expectedAccessToken = this.config.access_token?.trim()
313
- || process.env.ONEBOTS_ACCESS_TOKEN?.trim()
314
- || undefined;
315
- const getTokenFromRequest = (request) => {
316
- const authHeader = request.headers.authorization;
317
- if (authHeader && typeof authHeader === 'string') {
318
- const match = authHeader.match(/^Bearer\s+(.+)$/i);
319
- return match ? match[1] : authHeader;
320
- }
321
- try {
322
- const url = new URL(request.url || '/', 'http://localhost');
323
- return url.searchParams.get('access_token') || undefined;
324
- }
325
- catch {
326
- return undefined;
327
- }
328
- };
329
- const getTokenFromKoa = (ctx) => {
330
- const authHeader = ctx.request.headers.authorization;
331
- if (authHeader && typeof authHeader === 'string') {
332
- const match = authHeader.match(/^Bearer\s+(.+)$/i);
333
- return match ? match[1] : authHeader;
334
- }
335
- return ctx.request.query?.access_token || undefined;
336
- };
337
- this.router.post("/api/auth/login", (ctx) => {
338
- const body = ctx.request.body;
339
- // 鉴权码登录:Bearer 鉴权码,与 config 中 access_token 或环境变量 ONEBOTS_ACCESS_TOKEN 一致即可
340
- if (body.access_token != null && body.access_token !== '') {
341
- if (expectedAccessToken && body.access_token === expectedAccessToken) {
342
- ctx.body = {
343
- success: true,
344
- token: body.access_token,
345
- expiresAt: null,
346
- refreshToken: null,
347
- isDefaultCredentials: false,
348
- };
349
- return;
350
- }
351
- ctx.status = 401;
352
- ctx.body = { success: false, message: "鉴权码错误" };
353
- return;
354
- }
355
- if (!body.username || !body.password || body.username !== expectedUsername || body.password !== expectedPassword) {
356
- ctx.status = 401;
357
- ctx.body = { success: false, message: "用户名或密码错误" };
358
- return;
359
- }
360
- const tokenInfo = this.tokenManager.generateToken({ username: body.username });
361
- ctx.body = {
362
- success: true,
363
- token: tokenInfo.token,
364
- expiresAt: tokenInfo.expiresAt,
365
- refreshToken: tokenInfo.refreshToken,
366
- isDefaultCredentials: credentialsWereAutoGenerated,
367
- };
368
- });
369
- this.router.post("/api/auth/refresh", (ctx) => {
370
- const { refreshToken } = ctx.request.body;
371
- if (!refreshToken) {
372
- ctx.status = 400;
373
- ctx.body = { success: false, message: "缺少 refreshToken" };
374
- return;
375
- }
376
- const tokenInfo = this.tokenManager.refreshToken(refreshToken);
377
- if (!tokenInfo) {
378
- ctx.status = 401;
379
- ctx.body = { success: false, message: "refreshToken 无效或已过期" };
380
- return;
381
- }
382
- ctx.body = {
383
- success: true,
384
- token: tokenInfo.token,
385
- expiresAt: tokenInfo.expiresAt,
386
- refreshToken: tokenInfo.refreshToken,
387
- };
388
- });
389
- // 仅对 Web 管理端 /api 鉴权(Bearer / access_token / 登录 token);各平台对外 API(OneBot、KOOK 等)由各自协议/适配器鉴权,不经过此处
390
- this.router.use('/api', async (ctx, next) => {
391
- if (ctx.path === '/api/auth/login' || ctx.path === '/api/auth/refresh')
392
- return next();
393
- const token = getTokenFromKoa(ctx);
394
- if (expectedAccessToken && token === expectedAccessToken) {
395
- ctx.state.token = token;
396
- ctx.state.tokenInfo = { metadata: { username: 'token' }, expiresAt: null };
397
- return next();
398
- }
399
- return authValidator(ctx, next);
400
- });
401
- this.router.post("/api/auth/logout", (ctx) => {
402
- const token = ctx.state.token;
403
- if (token && token !== expectedAccessToken)
404
- this.tokenManager.revokeToken(token);
405
- ctx.body = { success: true };
406
- });
407
- this.router.get("/api/auth/me", (ctx) => {
408
- const tokenInfo = ctx.state.tokenInfo;
409
- ctx.body = {
410
- success: true,
411
- data: {
412
- username: tokenInfo?.metadata?.username ?? expectedUsername,
413
- expiresAt: tokenInfo?.expiresAt ?? null,
414
- isDefaultCredentials: credentialsWereAutoGenerated,
415
- },
416
- };
417
- });
290
+ // Route module registrations — each registers a group of routes on the router
291
+ registerAuthRoutes(this, this.router);
292
+ registerConfigRoutes(this, this.router);
293
+ registerAdapterRoutes(this, this.router);
294
+ registerVerificationRoutes(this, this.router);
295
+ registerTerminalRoutes(this, this.router);
296
+ registerPublicStaticRoutes(this, this.router);
418
297
  // WebSocket 日志监听(确保日志文件存在再 watch,避免 ENOENT)
419
298
  if (!existsSync(BaseApp.logFile)) {
420
299
  const dir = path.dirname(BaseApp.logFile);
@@ -459,6 +338,7 @@ export class App extends BaseApp {
459
338
  payload = JSON.parse(raw.toString());
460
339
  }
461
340
  catch {
341
+ // 无效的消息数据,忽略该条消息
462
342
  return;
463
343
  }
464
344
  switch (payload.action) {
@@ -479,7 +359,7 @@ export class App extends BaseApp {
479
359
  return true;
480
360
  case "system.saveConfig":
481
361
  fs.writeFileSync(BaseApp.configPath, payload.data, "utf8");
482
- credentialsWereAutoGenerated = false;
362
+ this.credentialsWereAutoGenerated = false;
483
363
  return;
484
364
  case "system.reload":
485
365
  const config = yaml.load(fs.readFileSync(BaseApp.configPath, "utf8"));
@@ -503,250 +383,6 @@ export class App extends BaseApp {
503
383
  }
504
384
  });
505
385
  });
506
- // 管理端点
507
- this.router.get("/api/adapters", (ctx) => {
508
- ctx.body = [...this.adapters.values()].map(adapter => adapter.info);
509
- });
510
- this.router.get("/api/system", ctx => {
511
- ctx.body = {
512
- ...this.info,
513
- isDefaultCredentials: credentialsWereAutoGenerated,
514
- configDir: BaseApp.configDir,
515
- configPath: BaseApp.configPath,
516
- dataDir: BaseApp.dataDir,
517
- };
518
- });
519
- /** 手动将 data 与配置备份到 HF 仓库(与保存配置时的备份逻辑一致) */
520
- this.router.post("/api/system/backup-to-hf", async (ctx) => {
521
- try {
522
- const configContent = readFileSync(BaseApp.configPath, "utf8");
523
- const result = await this.backupDataToHf(configContent);
524
- if (result.success) {
525
- ctx.body = { success: true, message: "已备份到仓库" };
526
- }
527
- else {
528
- ctx.status = 400;
529
- ctx.body = { success: false, message: result.message ?? "备份失败" };
530
- }
531
- }
532
- catch (e) {
533
- ctx.status = 500;
534
- ctx.body = { success: false, message: e.message };
535
- }
536
- });
537
- /** 重启服务:进程退出后由 Docker 的 restart 策略自动拉起容器;非 Docker 需手动重新启动 */
538
- this.router.post("/api/system/restart", (ctx) => {
539
- ctx.body = { success: true, message: "服务即将重启" };
540
- setImmediate(() => {
541
- setTimeout(() => {
542
- process.exit(0);
543
- }, 1500);
544
- });
545
- });
546
- // CLI send:通过已运行网关发信
547
- this.router.post("/api/send", async (ctx) => {
548
- try {
549
- const body = ctx.request.body || {};
550
- const channel = String(body.channel ?? "");
551
- const target_id = String(body.target_id ?? "");
552
- const target_type = String(body.target_type ?? "private");
553
- const message = String(body.message ?? "");
554
- if (!channel || !target_id) {
555
- ctx.status = 400;
556
- ctx.body = { success: false, message: "缺少 channel 或 target_id" };
557
- return;
558
- }
559
- const parts = channel.split(".");
560
- const platform = parts[0];
561
- const account_id = parts.slice(1).join(".") || parts[1];
562
- if (!platform || !account_id) {
563
- ctx.status = 400;
564
- ctx.body = { success: false, message: "channel 格式应为 platform.account_id" };
565
- return;
566
- }
567
- const adapter = this.adapters.get(platform);
568
- if (!adapter) {
569
- ctx.status = 404;
570
- ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
571
- return;
572
- }
573
- const account = adapter.getAccount(account_id);
574
- if (!account) {
575
- ctx.status = 404;
576
- ctx.body = { success: false, message: `账号 ${channel} 不存在` };
577
- return;
578
- }
579
- const segments = [{ type: "text", data: { text: message } }];
580
- const scene_id = adapter.createId(target_id);
581
- const result = await adapter.sendMessage(account_id, {
582
- scene_type: target_type,
583
- scene_id,
584
- message: segments,
585
- });
586
- ctx.body = { success: true, message_id: result?.message_id ?? null };
587
- }
588
- catch (e) {
589
- const err = e;
590
- ctx.status = 500;
591
- ctx.body = { success: false, message: err?.message ?? "发送失败" };
592
- }
593
- });
594
- // PTY 终端 WebSocket 端点
595
- const terminalWs = this.router.ws("/api/terminal");
596
- terminalWs.on("connection", (client, request) => {
597
- const token = getTokenFromRequest(request);
598
- const valid = !!token && (expectedAccessToken ? token === expectedAccessToken : this.tokenManager.validateToken(token).valid);
599
- if (!valid) {
600
- client.close(1008, "Unauthorized");
601
- return;
602
- }
603
- // 创建 PTY 终端实例(如果不存在)
604
- if (!this.ptyTerminal) {
605
- const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
606
- this.ptyTerminal = pty.spawn(shell, [], {
607
- name: "xterm-color",
608
- cols: 80,
609
- rows: 30,
610
- cwd: process.env.HOME,
611
- env: process.env,
612
- });
613
- // 监听 PTY 输出
614
- this.ptyTerminal.onData((data) => {
615
- // 广播到所有连接的客户端
616
- this.terminalClients.forEach(c => {
617
- try {
618
- c.send(JSON.stringify({ type: 'output', data }));
619
- }
620
- catch (e) {
621
- this.terminalClients.delete(c);
622
- }
623
- });
624
- });
625
- // 监听 PTY 退出
626
- this.ptyTerminal.onExit(() => {
627
- this.ptyTerminal = null;
628
- this.terminalClients.forEach(c => {
629
- try {
630
- c.send(JSON.stringify({ type: 'exit' }));
631
- }
632
- catch (e) { }
633
- });
634
- this.terminalClients.clear();
635
- });
636
- }
637
- // 添加到客户端列表
638
- this.terminalClients.add(client);
639
- // 监听客户端消息(用户输入)
640
- client.on("message", (msg) => {
641
- try {
642
- const payload = JSON.parse(msg.toString());
643
- if (payload.type === 'input' && this.ptyTerminal) {
644
- this.ptyTerminal.write(payload.data);
645
- }
646
- else if (payload.type === 'resize' && this.ptyTerminal) {
647
- this.ptyTerminal.resize(payload.cols, payload.rows);
648
- }
649
- else if (payload.type === 'restart') {
650
- // 通知所有客户端
651
- this.terminalClients.forEach(c => {
652
- try {
653
- c.send(JSON.stringify({ type: 'output', data: '\r\n\x1b[33m[服务即将重启]\x1b[0m' }));
654
- }
655
- catch (e) { }
656
- });
657
- setTimeout(() => process.exit(100), 500);
658
- }
659
- }
660
- catch (e) {
661
- console.error('终端消息处理失败:', e);
662
- }
663
- });
664
- // 监听客户端断开
665
- client.on("close", () => {
666
- this.terminalClients.delete(client);
667
- // 如果没有客户端了,关闭 PTY
668
- if (this.terminalClients.size === 0 && this.ptyTerminal) {
669
- this.ptyTerminal.kill();
670
- this.ptyTerminal = null;
671
- }
672
- });
673
- });
674
- // 日志流 SSE 端点
675
- this.router.get("/api/logs", ctx => {
676
- ctx.request.socket.setTimeout(0);
677
- ctx.req.socket.setNoDelay(true);
678
- ctx.req.socket.setKeepAlive(true);
679
- ctx.set({
680
- 'Content-Type': 'text/event-stream',
681
- 'Cache-Control': 'no-cache',
682
- 'Connection': 'keep-alive',
683
- 'Access-Control-Allow-Origin': '*',
684
- 'Access-Control-Allow-Headers': 'Content-Type'
685
- });
686
- ctx.status = 200;
687
- // 阻止 Koa 自动结束响应
688
- ctx.respond = false;
689
- // 添加到客户端列表
690
- this.logClients.add(ctx.res);
691
- // 发送缓存日志到客户端
692
- try {
693
- if (existsSync(this.logCacheFile)) {
694
- const cachedLogs = readFileSync(this.logCacheFile, 'utf-8');
695
- if (cachedLogs) {
696
- // 将历史日志的 \n 也替换为 \r\n
697
- const terminalLogs = cachedLogs.replace(/\n/g, '\r\n');
698
- ctx.res.write(`data: ${JSON.stringify({ message: terminalLogs })}\n\n`);
699
- }
700
- }
701
- }
702
- catch (error) {
703
- console.error('读取日志缓存失败:', error);
704
- }
705
- // 定时发送心跳
706
- const heartbeat = setInterval(() => {
707
- try {
708
- ctx.res.write(': heartbeat\n\n');
709
- }
710
- catch (error) {
711
- clearInterval(heartbeat);
712
- this.logClients.delete(ctx.res);
713
- }
714
- }, 30000);
715
- // 监听连接关闭
716
- ctx.req.on('close', () => {
717
- clearInterval(heartbeat);
718
- this.logClients.delete(ctx.res);
719
- });
720
- });
721
- // 验证流 SSE 端点(登录验证事件推送到 Web)
722
- this.router.get("/api/verification/stream", ctx => {
723
- ctx.request.socket.setTimeout(0);
724
- ctx.req.socket.setNoDelay(true);
725
- ctx.req.socket.setKeepAlive(true);
726
- ctx.set({
727
- 'Content-Type': 'text/event-stream',
728
- 'Cache-Control': 'no-cache',
729
- 'Connection': 'keep-alive',
730
- 'Access-Control-Allow-Origin': '*',
731
- 'Access-Control-Allow-Headers': 'Content-Type'
732
- });
733
- ctx.status = 200;
734
- ctx.respond = false;
735
- this.verificationClients.add(ctx.res);
736
- const heartbeatVerification = setInterval(() => {
737
- try {
738
- ctx.res.write(': heartbeat\n\n');
739
- }
740
- catch (error) {
741
- clearInterval(heartbeatVerification);
742
- this.verificationClients.delete(ctx.res);
743
- }
744
- }, 30000);
745
- ctx.req.on('close', () => {
746
- clearInterval(heartbeatVerification);
747
- this.verificationClients.delete(ctx.res);
748
- });
749
- });
750
386
  // 订阅已有适配器的验证事件(init 时创建的适配器已通过 onAdapterCreated 订阅,此处为兜底)
751
387
  for (const [, adapter] of this.adapters) {
752
388
  if (!adapter.listenerCount('verification:request')) {
@@ -755,278 +391,6 @@ export class App extends BaseApp {
755
391
  });
756
392
  }
757
393
  }
758
- // 待处理验证列表(Web 打开页面时拉取,避免离线期间错过验证)
759
- this.router.get("/api/verification/pending", (ctx) => {
760
- ctx.body = this.getPendingVerificationList();
761
- });
762
- // 请求发送短信验证码(设备锁带手机号时,用户选短信验证前调用)
763
- this.router.post("/api/verification/request-sms", async (ctx) => {
764
- try {
765
- const body = ctx.request.body || {};
766
- const platform = String(body.platform ?? '');
767
- const account_id = String(body.account_id ?? '');
768
- if (!platform || !account_id) {
769
- ctx.status = 400;
770
- ctx.body = { success: false, message: '缺少 platform 或 account_id' };
771
- return;
772
- }
773
- const adapter = this.adapters.get(platform);
774
- if (!adapter) {
775
- ctx.status = 404;
776
- ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
777
- return;
778
- }
779
- const requestSms = adapter.requestSmsCode;
780
- if (typeof requestSms !== 'function') {
781
- ctx.status = 501;
782
- ctx.body = { success: false, message: `适配器 ${platform} 不支持请求短信验证码` };
783
- return;
784
- }
785
- await Promise.resolve(requestSms.call(adapter, account_id));
786
- ctx.body = { success: true };
787
- }
788
- catch (e) {
789
- const err = e;
790
- ctx.status = 500;
791
- ctx.body = { success: false, message: err?.message ?? '请求失败' };
792
- }
793
- });
794
- // 验证提交接口(Web 完成滑块/短信等后提交)
795
- this.router.post("/api/verification/submit", async (ctx) => {
796
- try {
797
- const body = ctx.request.body || {};
798
- const platform = String(body.platform ?? '');
799
- const account_id = String(body.account_id ?? '');
800
- const type = String(body.type ?? '');
801
- const data = body.data && typeof body.data === 'object' ? body.data : {};
802
- if (!platform || !account_id || !type) {
803
- ctx.status = 400;
804
- ctx.body = { success: false, message: '缺少 platform、account_id 或 type' };
805
- return;
806
- }
807
- const adapter = this.adapters.get(platform);
808
- if (!adapter) {
809
- ctx.status = 404;
810
- ctx.body = { success: false, message: `适配器 ${platform} 不存在` };
811
- return;
812
- }
813
- const submit = adapter.submitVerification;
814
- if (typeof submit !== 'function') {
815
- ctx.status = 501;
816
- ctx.body = { success: false, message: `适配器 ${platform} 不支持 Web 验证提交` };
817
- return;
818
- }
819
- await Promise.resolve(submit.call(adapter, account_id, type, data));
820
- this.pendingVerifications.delete(`${platform}:${account_id}:${type}`);
821
- ctx.body = { success: true };
822
- }
823
- catch (e) {
824
- const err = e;
825
- ctx.status = 500;
826
- ctx.body = { success: false, message: err?.message ?? '提交失败' };
827
- }
828
- });
829
- // 配置接口
830
- this.router.get("/api/config", ctx => {
831
- ctx.body = fs.readFileSync(BaseApp.configPath, "utf8");
832
- });
833
- this.router.get("/api/config/schema", ctx => {
834
- ctx.body = getAppConfigSchema();
835
- });
836
- this.router.post("/api/config", async (ctx) => {
837
- try {
838
- const configContent = ctx.request.body;
839
- fs.writeFileSync(BaseApp.configPath, configContent, "utf8");
840
- credentialsWereAutoGenerated = false;
841
- const backupResult = await this.backupDataToHf(configContent);
842
- if (!backupResult.success && backupResult.message) {
843
- this.logger?.warn?.(backupResult.message);
844
- }
845
- ctx.body = { success: true, message: "配置已保存" };
846
- }
847
- catch (e) {
848
- ctx.status = 500;
849
- ctx.body = { success: false, message: e.message };
850
- }
851
- });
852
- // 站点根静态文件(public_static_dir):列表 / 上传 / 删除(需已配置并保存 public_static_dir)
853
- this.router.get("/api/public-static/files", (ctx) => {
854
- const root = this.getPublicStaticRoot();
855
- if (!root) {
856
- ctx.status = 400;
857
- ctx.body = {
858
- success: false,
859
- message: '请先在基础配置中设置 public_static_dir 并保存配置',
860
- };
861
- return;
862
- }
863
- try {
864
- const names = fs
865
- .readdirSync(root, { withFileTypes: true })
866
- .filter((d) => d.isFile())
867
- .map((d) => d.name)
868
- .sort((a, b) => a.localeCompare(b));
869
- ctx.body = { success: true, files: names, root };
870
- }
871
- catch (e) {
872
- ctx.status = 500;
873
- ctx.body = { success: false, message: e.message };
874
- }
875
- });
876
- this.router.post("/api/public-static/upload", async (ctx) => {
877
- const root = this.getPublicStaticRoot();
878
- if (!root) {
879
- ctx.status = 400;
880
- ctx.body = {
881
- success: false,
882
- message: '请先在基础配置中设置 public_static_dir 并保存配置',
883
- };
884
- return;
885
- }
886
- const file = pickPublicStaticUpload(ctx.request.files);
887
- if (!file?.filepath) {
888
- ctx.status = 400;
889
- ctx.body = { success: false, message: '缺少上传文件(字段名 file)' };
890
- return;
891
- }
892
- const safeName = sanitizePublicStaticBasename(file.originalFilename ?? file.newFilename);
893
- if (!safeName) {
894
- try {
895
- fs.unlinkSync(file.filepath);
896
- }
897
- catch {
898
- /* 忽略临时文件清理失败 */
899
- }
900
- ctx.status = 400;
901
- ctx.body = { success: false, message: '非法或无法识别的文件名' };
902
- return;
903
- }
904
- const dest = path.join(root, safeName);
905
- const tmpPath = file.filepath;
906
- try {
907
- fs.copyFileSync(tmpPath, dest);
908
- ctx.body = { success: true, message: '上传成功', filename: safeName };
909
- const hf = await this.backupDataDirToHfAfterStaticChange();
910
- if (hf.attempted) {
911
- ctx.body.hf_backup = hf;
912
- }
913
- }
914
- catch (e) {
915
- ctx.status = 500;
916
- ctx.body = { success: false, message: e.message };
917
- }
918
- finally {
919
- try {
920
- fs.unlinkSync(tmpPath);
921
- }
922
- catch {
923
- /* 忽略 */
924
- }
925
- }
926
- });
927
- this.router.delete("/api/public-static/:filename", async (ctx) => {
928
- const root = this.getPublicStaticRoot();
929
- if (!root) {
930
- ctx.status = 400;
931
- ctx.body = {
932
- success: false,
933
- message: '请先在基础配置中设置 public_static_dir 并保存配置',
934
- };
935
- return;
936
- }
937
- const safeName = sanitizePublicStaticBasename(ctx.params.filename ?? '');
938
- if (!safeName) {
939
- ctx.status = 400;
940
- ctx.body = { success: false, message: '非法文件名' };
941
- return;
942
- }
943
- const resolvedRoot = path.resolve(root);
944
- const target = path.join(root, safeName);
945
- const rel = path.relative(resolvedRoot, path.resolve(target));
946
- if (rel.startsWith('..') || path.isAbsolute(rel) || rel === '') {
947
- ctx.status = 400;
948
- ctx.body = { success: false, message: '路径非法' };
949
- return;
950
- }
951
- try {
952
- if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
953
- ctx.status = 404;
954
- ctx.body = { success: false, message: '文件不存在' };
955
- return;
956
- }
957
- fs.unlinkSync(target);
958
- ctx.body = { success: true, message: '已删除' };
959
- const hf = await this.backupDataDirToHfAfterStaticChange();
960
- if (hf.attempted) {
961
- ctx.body.hf_backup = hf;
962
- }
963
- }
964
- catch (e) {
965
- ctx.status = 500;
966
- ctx.body = { success: false, message: e.message };
967
- }
968
- });
969
- // 账号管理端点
970
- this.router.get("/api/list", ctx => {
971
- ctx.body = this.accounts.map(bot => bot.info);
972
- });
973
- this.router.post("/api/add", (ctx) => {
974
- const config = ctx.request.body;
975
- try {
976
- this.addAccount(config);
977
- ctx.body = { success: true, message: '添加成功' };
978
- }
979
- catch (e) {
980
- ctx.status = 500;
981
- ctx.body = { success: false, message: e.message };
982
- }
983
- });
984
- this.router.post("/api/edit", (ctx) => {
985
- const config = ctx.request.body;
986
- try {
987
- this.updateAccount(config);
988
- ctx.body = { success: true, message: '修改成功' };
989
- }
990
- catch (e) {
991
- ctx.status = 500;
992
- ctx.body = { success: false, message: e.message };
993
- }
994
- });
995
- this.router.get("/api/remove", ctx => {
996
- const { uin, platform, force } = ctx.request.query;
997
- try {
998
- this.removeAccount(String(platform), String(uin), Boolean(force));
999
- ctx.body = { success: true, message: '移除成功' };
1000
- }
1001
- catch (e) {
1002
- ctx.status = 500;
1003
- ctx.body = { success: false, message: e.message };
1004
- }
1005
- });
1006
- this.router.post("/api/bots/start", async (ctx) => {
1007
- const { platform, uin } = ctx.request.body;
1008
- try {
1009
- const adapter = this.adapters.get(platform);
1010
- await adapter?.setOnline(uin);
1011
- ctx.body = { success: true, data: adapter?.getAccount(uin)?.info };
1012
- }
1013
- catch (e) {
1014
- ctx.status = 500;
1015
- ctx.body = { success: false, message: e.message };
1016
- }
1017
- });
1018
- this.router.post("/api/bots/stop", async (ctx) => {
1019
- const { platform, uin } = ctx.request.body;
1020
- try {
1021
- const adapter = this.adapters.get(platform);
1022
- await adapter?.setOffline(uin);
1023
- ctx.body = { success: true, data: adapter?.getAccount(uin)?.info };
1024
- }
1025
- catch (e) {
1026
- ctx.status = 500;
1027
- ctx.body = { success: false, message: e.message };
1028
- }
1029
- });
1030
394
  // 静态文件服务
1031
395
  if (fs.existsSync(client)) {
1032
396
  this.use(koaStatic(client));
@@ -1062,7 +426,9 @@ export class App extends BaseApp {
1062
426
  try {
1063
427
  return await import(name);
1064
428
  }
1065
- catch { }
429
+ catch {
430
+ // 模块不存在,返回 undefined 表示加载失败
431
+ }
1066
432
  }
1067
433
  async function loadAdapterFactory(platform, maybeNames = [
1068
434
  `@onebots/adapter-${platform}`,
@@ -1077,7 +443,7 @@ export class App extends BaseApp {
1077
443
  return true;
1078
444
  }
1079
445
  catch (e) {
1080
- console.warn(`[onebots] Failed to load adapter ${modName}: ${e}`);
446
+ console.warn(`[onebots] 加载适配器 ${modName} 失败: ${e}`);
1081
447
  return loadAdapterFactory(platform, maybeNames);
1082
448
  }
1083
449
  }
@@ -1095,7 +461,7 @@ export class App extends BaseApp {
1095
461
  return true;
1096
462
  }
1097
463
  catch (e) {
1098
- console.warn(`[onebots] Failed to load protocol ${modName}: ${e}`);
464
+ console.warn(`[onebots] 加载协议 ${modName} 失败: ${e}`);
1099
465
  return loadProtocolFactory(name, maybeNames);
1100
466
  }
1101
467
  }
@@ -1115,11 +481,11 @@ export function createOnebots(config = "config.yaml") {
1115
481
  }
1116
482
  if (!isStartWithConfigFile) {
1117
483
  writeFileSync(BaseApp.configPath, yaml.dump(config));
1118
- console.log(`已自动保存配置到:${BaseApp.configPath}`);
484
+ console.log("[onebots] 已自动保存配置到:", BaseApp.configPath);
1119
485
  }
1120
486
  if (!existsSync(BaseApp.dataDir)) {
1121
487
  mkdirSync(BaseApp.dataDir);
1122
- console.log("已为你创建数据存储目录", BaseApp.dataDir);
488
+ console.log("[onebots] 已创建数据存储目录:", BaseApp.dataDir);
1123
489
  }
1124
490
  config = yaml.load(readFileSync(BaseApp.configPath, "utf8"));
1125
491
  const hasAccessToken = !!config.access_token?.trim();
@@ -1129,7 +495,7 @@ export function createOnebots(config = "config.yaml") {
1129
495
  config.username = generatedUser;
1130
496
  config.password = generatedPass;
1131
497
  writeFileSync(BaseApp.configPath, yaml.dump(config));
1132
- credentialsWereAutoGenerated = true;
498
+ _credentialsWereAutoGenerated = true;
1133
499
  console.log("[onebots] 已自动生成管理端账号并写入配置文件,请尽快在 Web 端修改密码:");
1134
500
  console.log(" 用户名:", generatedUser);
1135
501
  console.log(" 密码:", generatedPass);