@onebots/core 0.5.0 → 1.0.4

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.
Files changed (66) hide show
  1. package/README.md +3 -3
  2. package/lib/__tests__/config-validator.test.d.ts +4 -0
  3. package/lib/__tests__/config-validator.test.js +152 -0
  4. package/lib/__tests__/di-container.test.d.ts +4 -0
  5. package/lib/__tests__/di-container.test.js +114 -0
  6. package/lib/__tests__/errors.test.d.ts +4 -0
  7. package/lib/__tests__/errors.test.js +111 -0
  8. package/lib/__tests__/integration.test.d.ts +5 -0
  9. package/lib/__tests__/integration.test.js +112 -0
  10. package/lib/__tests__/lifecycle.test.d.ts +4 -0
  11. package/lib/__tests__/lifecycle.test.js +163 -0
  12. package/lib/account.d.ts +4 -1
  13. package/lib/account.js +6 -3
  14. package/lib/adapter.d.ts +67 -1
  15. package/lib/adapter.js +31 -4
  16. package/lib/base-app.d.ts +30 -3
  17. package/lib/base-app.js +295 -142
  18. package/lib/circuit-breaker.d.ts +94 -0
  19. package/lib/circuit-breaker.js +206 -0
  20. package/lib/config-validator.d.ts +51 -0
  21. package/lib/config-validator.js +184 -0
  22. package/lib/connection-pool.d.ts +68 -0
  23. package/lib/connection-pool.js +202 -0
  24. package/lib/db.d.ts +2 -1
  25. package/lib/db.js +11 -2
  26. package/lib/di-container.d.ts +60 -0
  27. package/lib/di-container.js +103 -0
  28. package/lib/errors.d.ts +157 -0
  29. package/lib/errors.js +257 -0
  30. package/lib/index.d.ts +13 -4
  31. package/lib/index.js +17 -3
  32. package/lib/lifecycle.d.ts +75 -0
  33. package/lib/lifecycle.js +175 -0
  34. package/lib/logger.d.ts +76 -0
  35. package/lib/logger.js +156 -0
  36. package/lib/metrics.d.ts +80 -0
  37. package/lib/metrics.js +201 -0
  38. package/lib/middleware/index.d.ts +8 -0
  39. package/lib/middleware/index.js +8 -0
  40. package/lib/middleware/metrics-collector.d.ts +9 -0
  41. package/lib/middleware/metrics-collector.js +64 -0
  42. package/lib/middleware/rate-limit.d.ts +32 -0
  43. package/lib/middleware/rate-limit.js +149 -0
  44. package/lib/middleware/security-audit.d.ts +33 -0
  45. package/lib/middleware/security-audit.js +223 -0
  46. package/lib/middleware/token-manager.d.ts +73 -0
  47. package/lib/middleware/token-manager.js +186 -0
  48. package/lib/middleware/token-validator.d.ts +42 -0
  49. package/lib/middleware/token-validator.js +198 -0
  50. package/lib/protocol.d.ts +2 -3
  51. package/lib/protocol.js +4 -0
  52. package/lib/rate-limiter.d.ts +88 -0
  53. package/lib/rate-limiter.js +196 -0
  54. package/lib/registry.d.ts +27 -0
  55. package/lib/registry.js +40 -5
  56. package/lib/retry.d.ts +87 -0
  57. package/lib/retry.js +205 -0
  58. package/lib/router.d.ts +43 -6
  59. package/lib/router.js +139 -12
  60. package/lib/timestamp.d.ts +42 -0
  61. package/lib/timestamp.js +72 -0
  62. package/lib/types.d.ts +1 -0
  63. package/lib/types.js +2 -1
  64. package/lib/utils.d.ts +2 -1
  65. package/lib/utils.js +11 -19
  66. package/package.json +24 -9
@@ -0,0 +1,163 @@
1
+ /**
2
+ * 生命周期管理测试
3
+ */
4
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
5
+ import { LifecycleManager } from '../lifecycle.js';
6
+ describe('Lifecycle Manager', () => {
7
+ let lifecycle;
8
+ beforeEach(() => {
9
+ lifecycle = new LifecycleManager();
10
+ });
11
+ describe('Resource Management', () => {
12
+ it('should register and cleanup resources', async () => {
13
+ let cleaned = false;
14
+ lifecycle.register('test-resource', () => {
15
+ cleaned = true;
16
+ });
17
+ expect(lifecycle.getResourceCount()).toBe(1);
18
+ expect(lifecycle.getResourceNames()).toContain('test-resource');
19
+ await lifecycle.cleanup();
20
+ expect(cleaned).toBe(true);
21
+ expect(lifecycle.getResourceCount()).toBe(0);
22
+ });
23
+ it('should handle multiple resources', async () => {
24
+ const cleaned = [];
25
+ lifecycle.register('resource1', () => {
26
+ cleaned.push('resource1');
27
+ });
28
+ lifecycle.register('resource2', () => {
29
+ cleaned.push('resource2');
30
+ });
31
+ await lifecycle.cleanup();
32
+ expect(cleaned).toContain('resource1');
33
+ expect(cleaned).toContain('resource2');
34
+ });
35
+ it('should handle resource cleanup errors', async () => {
36
+ const errorHandler = vi.fn();
37
+ lifecycle.on('cleanupError', errorHandler);
38
+ lifecycle.register('error-resource', () => {
39
+ throw new Error('Cleanup failed');
40
+ });
41
+ // 清理不应该抛出错误,应该触发事件
42
+ await expect(lifecycle.cleanup()).resolves.not.toThrow();
43
+ expect(errorHandler).toHaveBeenCalled();
44
+ });
45
+ it('should unregister resource', () => {
46
+ lifecycle.register('resource1', () => { });
47
+ lifecycle.register('resource2', () => { });
48
+ expect(lifecycle.unregister('resource1')).toBe(true);
49
+ expect(lifecycle.getResourceCount()).toBe(1);
50
+ expect(lifecycle.getResourceNames()).not.toContain('resource1');
51
+ });
52
+ });
53
+ describe('Lifecycle Hooks', () => {
54
+ it('should execute init hooks', async () => {
55
+ const initHook = vi.fn();
56
+ lifecycle.addHook({ onInit: initHook });
57
+ await lifecycle.init();
58
+ expect(initHook).toHaveBeenCalled();
59
+ });
60
+ it('should execute start hooks', async () => {
61
+ const startHook = vi.fn();
62
+ lifecycle.addHook({ onStart: startHook });
63
+ await lifecycle.start();
64
+ expect(startHook).toHaveBeenCalled();
65
+ });
66
+ it('should execute stop hooks', async () => {
67
+ const stopHook = vi.fn();
68
+ lifecycle.addHook({ onStop: stopHook });
69
+ await lifecycle.stop();
70
+ expect(stopHook).toHaveBeenCalled();
71
+ });
72
+ it('should execute cleanup hooks', async () => {
73
+ const cleanupHook = vi.fn();
74
+ lifecycle.addHook({ onCleanup: cleanupHook });
75
+ await lifecycle.cleanup();
76
+ expect(cleanupHook).toHaveBeenCalled();
77
+ });
78
+ it('should execute all hooks in order', async () => {
79
+ const order = [];
80
+ lifecycle.addHook({
81
+ onInit: () => {
82
+ order.push('init');
83
+ },
84
+ onStart: () => {
85
+ order.push('start');
86
+ },
87
+ onStop: () => {
88
+ order.push('stop');
89
+ },
90
+ onCleanup: () => {
91
+ order.push('cleanup');
92
+ },
93
+ });
94
+ await lifecycle.init();
95
+ await lifecycle.start();
96
+ await lifecycle.stop();
97
+ await lifecycle.cleanup();
98
+ expect(order).toEqual(['init', 'start', 'stop', 'cleanup']);
99
+ });
100
+ });
101
+ describe('Graceful Shutdown', () => {
102
+ it('should perform graceful shutdown', async () => {
103
+ const stopHook = vi.fn();
104
+ const cleanupHook = vi.fn();
105
+ lifecycle.addHook({
106
+ onStop: stopHook,
107
+ onCleanup: cleanupHook,
108
+ });
109
+ await lifecycle.gracefulShutdown('SIGTERM');
110
+ expect(stopHook).toHaveBeenCalled();
111
+ expect(cleanupHook).toHaveBeenCalled();
112
+ });
113
+ it('should emit shutdown events', async () => {
114
+ const shutdownHandler = vi.fn();
115
+ const shutdownCompleteHandler = vi.fn();
116
+ lifecycle.on('shutdown', shutdownHandler);
117
+ lifecycle.on('shutdownComplete', shutdownCompleteHandler);
118
+ await lifecycle.gracefulShutdown('SIGTERM');
119
+ expect(shutdownHandler).toHaveBeenCalledWith('SIGTERM');
120
+ expect(shutdownCompleteHandler).toHaveBeenCalled();
121
+ });
122
+ it('should handle shutdown timeout', async () => {
123
+ lifecycle.setShutdownTimeout(100);
124
+ const timeoutHandler = vi.fn();
125
+ lifecycle.on('shutdownTimeout', timeoutHandler);
126
+ // 添加一个永远不会完成的钩子
127
+ lifecycle.addHook({
128
+ onStop: () => new Promise(() => { }), // 永远pending
129
+ });
130
+ // 使用 vi.useFakeTimers 来测试超时
131
+ vi.useFakeTimers();
132
+ // 启动关闭流程
133
+ const shutdownPromise = lifecycle.gracefulShutdown(undefined, { exitOnTimeout: false });
134
+ // 等待超时事件触发
135
+ const timeoutPromise = new Promise((resolve) => {
136
+ lifecycle.once('shutdownTimeout', () => {
137
+ timeoutHandler();
138
+ resolve();
139
+ });
140
+ });
141
+ // 推进时间
142
+ vi.advanceTimersByTime(150);
143
+ // 等待超时事件
144
+ await timeoutPromise;
145
+ expect(timeoutHandler).toHaveBeenCalled();
146
+ // 清理
147
+ vi.useRealTimers();
148
+ // 取消关闭流程(避免测试挂起)
149
+ lifecycle.removeHook(lifecycle['hooks'][0]);
150
+ });
151
+ });
152
+ describe('Event Emission', () => {
153
+ it('should emit lifecycle events', async () => {
154
+ const beforeInit = vi.fn();
155
+ const afterInit = vi.fn();
156
+ lifecycle.on('beforeInit', beforeInit);
157
+ lifecycle.on('afterInit', afterInit);
158
+ await lifecycle.init();
159
+ expect(beforeInit).toHaveBeenCalled();
160
+ expect(afterInit).toHaveBeenCalled();
161
+ });
162
+ });
163
+ });
package/lib/account.d.ts CHANGED
@@ -36,7 +36,10 @@ export declare class Account<P extends keyof Adapter.Configs = keyof Adapter.Con
36
36
  stop(force?: boolean): Promise<void>;
37
37
  getGroupList(): Promise<Adapter.GroupInfo[]>;
38
38
  getFriendList(): Promise<Adapter.FriendInfo[]>;
39
- dispatch(commonEvent: CommonEvent.Base): Promise<void>;
39
+ /**
40
+ * 将通用事件同步派发到本账号绑定的各协议(协议内自行 catch,避免一次失败阻断其它协议)
41
+ */
42
+ dispatch(commonEvent: CommonEvent.Base): void;
40
43
  }
41
44
  export declare enum AccountStatus {
42
45
  Pending = "pending",// 上线中
package/lib/account.js CHANGED
@@ -85,10 +85,13 @@ export class Account extends EventEmitter {
85
85
  getFriendList() {
86
86
  return this.adapter.getFriendList(this.account_id);
87
87
  }
88
- async dispatch(commonEvent) {
89
- this.logger.debug(`Dispatching event: ${commonEvent.type}`);
90
- // Each protocol instance formats the common event to its own standard
88
+ /**
89
+ * 将通用事件同步派发到本账号绑定的各协议(协议内自行 catch,避免一次失败阻断其它协议)
90
+ */
91
+ dispatch(commonEvent) {
92
+ this.logger.debug(`Dispatching event: ${commonEvent.type} to ${this.protocols.length} protocol(s)`);
91
93
  for (const protocol of this.protocols) {
94
+ this.logger.debug(`Dispatching to protocol: ${protocol.name}/${protocol.version}`);
92
95
  protocol.dispatch(commonEvent);
93
96
  }
94
97
  }
package/lib/adapter.d.ts CHANGED
@@ -24,7 +24,18 @@ export declare abstract class Adapter<C = any, T extends keyof Adapter.Configs =
24
24
  get tableName(): string;
25
25
  protected constructor(app: I, platform: T);
26
26
  createId(id: string | number): CommonTypes.Id;
27
- resolveId(id: string | number): CommonTypes.Id;
27
+ /**
28
+ * 将 string / number / 已是框架层的 Id 归一到当前适配器的 Id。
29
+ * - 已带有 string+number 的 Id:原样返回(避免被当成 string 键查错)。
30
+ * - string / number:查 id_map,无则 createId。
31
+ */
32
+ resolveId(id: string | number | CommonTypes.Id): CommonTypes.Id;
33
+ /**
34
+ * 将协议传入的 scene_id / user_id 归一为 CommonTypes.Id。
35
+ * - 事件侧经 createId 上报的 Id:直接可用,.string / .source 存平台原始标识。
36
+ * - Milky 等若传入 JSON 原始 string/number:需 resolveId 查表或建档,才能与 passiveReply 等场景用的 openid 一致。
37
+ */
38
+ protected coerceId(value: CommonTypes.Id | string | number): CommonTypes.Id;
28
39
  /**
29
40
  * 1. 发送消息
30
41
  * OneBot V11: send_private_msg, send_group_msg, send_msg
@@ -493,6 +504,15 @@ export declare abstract class Adapter<C = any, T extends keyof Adapter.Configs =
493
504
  };
494
505
  setOnline(uin: string): Promise<void>;
495
506
  setOffline(uin: string): Promise<void>;
507
+ /**
508
+ * Web 验证提交 - 可选实现。支持 Web 端完成登录验证的适配器实现此方法,
509
+ * 根据 type 将 data 转交给平台 Bot(如 submitSlider / submitSmsCode)。
510
+ */
511
+ submitVerification?(accountId: string, type: string, data: Record<string, unknown>): void | Promise<void>;
512
+ /**
513
+ * 请求发送短信验证码 - 可选实现。设备锁带手机号时,用户选择短信验证前需先调用此方法。
514
+ */
515
+ requestSmsCode?(accountId: string): void | Promise<void>;
496
516
  /**
497
517
  * 创建账号 - 必须由平台适配器实现
498
518
  */
@@ -508,6 +528,51 @@ export type AdapterClient<T extends Adapter = Adapter> = T extends Adapter<infer
508
528
  export declare namespace Adapter {
509
529
  interface Configs extends Record<string, any> {
510
530
  }
531
+ /**
532
+ * 验证请求的展示块(Web 按 type 通用渲染,适配器按需组合)
533
+ */
534
+ type VerificationBlock = {
535
+ type: 'image';
536
+ base64: string;
537
+ alt?: string;
538
+ } | {
539
+ type: 'link';
540
+ url: string;
541
+ label?: string;
542
+ } | {
543
+ type: 'text';
544
+ content: string;
545
+ } | {
546
+ type: 'input';
547
+ key: string;
548
+ placeholder?: string;
549
+ maxLength?: number;
550
+ secret?: boolean;
551
+ };
552
+ /**
553
+ * 验证请求的展示配置(全平台通用,由适配器提供)
554
+ */
555
+ interface VerificationRequestOptions {
556
+ blocks?: VerificationBlock[];
557
+ }
558
+ /**
559
+ * 统一登录验证请求(适配器 emit('verification:request', payload) 时使用)
560
+ * hint、options 由适配器提供,Web 仅做通用展示;onApprove/onReject 由前端绑定。
561
+ */
562
+ interface VerificationRequest {
563
+ platform: string;
564
+ account_id: string;
565
+ type: string;
566
+ /** 说明文案,由适配器提供,适用于全平台 */
567
+ hint: string;
568
+ /** 展示配置(链接、图片、输入框等),由适配器提供 */
569
+ options?: VerificationRequestOptions;
570
+ /** 为 true 时 Web 显示「发送验证码」按钮,需配合 requestSmsCode 使用 */
571
+ requestSmsAvailable?: boolean;
572
+ /** 扩展数据,可选 */
573
+ data?: Record<string, unknown>;
574
+ request_id?: string;
575
+ }
511
576
  interface SendMessageParams {
512
577
  scene_type: CommonTypes.Scene;
513
578
  scene_id: CommonTypes.Id;
@@ -552,6 +617,7 @@ export declare namespace Adapter {
552
617
  sender_name: string;
553
618
  scene_name: string;
554
619
  }
620
+ /** 单条消息详情(getMessage 等);time 为 **Unix 秒**,与 OneBot 等协议常见约定一致 */
555
621
  interface MessageInfo {
556
622
  message_id: CommonTypes.Id;
557
623
  time: number;
package/lib/adapter.js CHANGED
@@ -36,6 +36,9 @@ export class Adapter extends EventEmitter {
36
36
  // ID 管理方法
37
37
  // ============================================
38
38
  createId(id) {
39
+ if (id === undefined || id === null) {
40
+ throw new Error('createId: id 不能为 undefined 或 null');
41
+ }
39
42
  if (typeof id === "number")
40
43
  return { string: id.toString(), number: id, source: id };
41
44
  const [existData] = this.db.select('*').from(this.tableName).where({
@@ -57,13 +60,37 @@ export class Adapter extends EventEmitter {
57
60
  this.db.insert(this.tableName).values(newId).run();
58
61
  return newId;
59
62
  }
63
+ /**
64
+ * 将 string / number / 已是框架层的 Id 归一到当前适配器的 Id。
65
+ * - 已带有 string+number 的 Id:原样返回(避免被当成 string 键查错)。
66
+ * - string / number:查 id_map,无则 createId。
67
+ */
60
68
  resolveId(id) {
61
- const [dbRecord] = this.db.select('*').from(this.tableName).where({
62
- [typeof id === "number" ? "number" : "string"]: id
63
- }).run();
69
+ if (typeof id === "object" &&
70
+ id !== null &&
71
+ typeof id.string === "string" &&
72
+ typeof id.number === "number") {
73
+ return id;
74
+ }
75
+ const primitive = id;
76
+ const [dbRecord] = this.db
77
+ .select("*")
78
+ .from(this.tableName)
79
+ .where({
80
+ [typeof primitive === "number" ? "number" : "string"]: primitive,
81
+ })
82
+ .run();
64
83
  if (dbRecord)
65
84
  return dbRecord;
66
- return this.createId(id);
85
+ return this.createId(primitive);
86
+ }
87
+ /**
88
+ * 将协议传入的 scene_id / user_id 归一为 CommonTypes.Id。
89
+ * - 事件侧经 createId 上报的 Id:直接可用,.string / .source 存平台原始标识。
90
+ * - Milky 等若传入 JSON 原始 string/number:需 resolveId 查表或建档,才能与 passiveReply 等场景用的 openid 一致。
91
+ */
92
+ coerceId(value) {
93
+ return this.resolveId(value);
67
94
  }
68
95
  // ============================================
69
96
  // 消息相关方法 (Message - 7个)
package/lib/base-app.d.ts CHANGED
@@ -6,12 +6,14 @@ import type { Logger } from "log4js";
6
6
  import { Server } from "http";
7
7
  import yaml from "js-yaml";
8
8
  declare const configure: typeof log4js.configure, connectLogger: typeof log4js.connectLogger;
9
- import { Router, WsServer } from "./router.js";
9
+ import { Router } from "./router.js";
10
10
  import { LogLevel } from "./types.js";
11
11
  import { Adapter } from "./adapter.js";
12
12
  import { Protocol } from "./protocol.js";
13
13
  import { Account } from "./account.js";
14
14
  import { SqliteDB } from "./db.js";
15
+ import { LifecycleManager } from "./lifecycle.js";
16
+ import { Logger as EnhancedLogger } from "./logger.js";
15
17
  export { configure, yaml, connectLogger };
16
18
  export interface KoaOptions {
17
19
  env?: string;
@@ -26,12 +28,13 @@ export declare class BaseApp extends Koa {
26
28
  httpServer: Server;
27
29
  isStarted: boolean;
28
30
  logger: Logger;
31
+ enhancedLogger: EnhancedLogger;
32
+ lifecycle: LifecycleManager;
29
33
  static get configPath(): string;
30
34
  static get dataDir(): string;
31
35
  static get logFile(): string;
32
36
  db: SqliteDB;
33
37
  adapters: Map<keyof Adapter.Configs, Adapter>;
34
- ws: WsServer;
35
38
  router: Router;
36
39
  get info(): {
37
40
  system_platform: NodeJS.Platform;
@@ -47,12 +50,28 @@ export declare class BaseApp extends Koa {
47
50
  process_cwd: string;
48
51
  process_use_memory: number;
49
52
  node_version: string;
50
- sdk_version: any;
53
+ sdk_version: string;
51
54
  uptime: number;
52
55
  };
53
56
  constructor(config?: BaseApp.Config);
54
57
  init(): void;
55
58
  getLogger(patform: string): log4js.Logger;
59
+ /**
60
+ * 获取增强的 Logger 实例
61
+ */
62
+ getEnhancedLogger(name: string): EnhancedLogger;
63
+ /**
64
+ * 设置健康检查端点
65
+ */
66
+ private setupHealthEndpoints;
67
+ /**
68
+ * 解析 public_static_dir:相对路径基于配置文件目录(configDir),禁止相对路径穿越出 configDir;绝对路径按原样使用
69
+ */
70
+ private resolvePublicStaticDirectory;
71
+ /**
72
+ * 管理端 API:当前解析后的站点根静态目录(未配置或无效时为 null)
73
+ */
74
+ getPublicStaticRoot(): string | null;
56
75
  get adapterConfigs(): Map<string, Account.Config[]>;
57
76
  private initAdapters;
58
77
  addAccount<P extends keyof Adapter.Configs>(config: Account.Config<P>): void;
@@ -60,6 +79,10 @@ export declare class BaseApp extends Koa {
60
79
  removeAccount(p: string, uin: string, force?: boolean): void;
61
80
  get accounts(): Account<string | number, any>[];
62
81
  findOrCreateAdapter<P extends keyof Adapter.Configs>(platform: P): Adapter<any, string | number, BaseApp>;
82
+ /**
83
+ * 适配器首次创建后的钩子,子类可覆写以订阅该适配器事件(如 verification:request)
84
+ */
85
+ protected onAdapterCreated(_adapter: Adapter): void;
63
86
  start(): Promise<void>;
64
87
  reload(config: BaseApp.Config): Promise<void>;
65
88
  stop(): Promise<void>;
@@ -75,7 +98,11 @@ export declare namespace BaseApp {
75
98
  timeout?: number;
76
99
  username?: string;
77
100
  password?: string;
101
+ /** 管理端 Bearer 鉴权码,配置后可使用 Authorization: Bearer <access_token> 访问 API,无需用户名密码 */
102
+ access_token?: string;
78
103
  log_level?: LogLevel;
104
+ /** 站点根静态目录,相对配置文件所在目录(configDir)或绝对路径;用于企业微信等可信域名校验文件 */
105
+ public_static_dir?: string;
79
106
  general?: Protocol.Configs;
80
107
  } & KoaOptions & AdapterConfig;
81
108
  const defaultConfig: Config;