@onebots/core 0.5.0 → 1.0.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/README.md +3 -3
- package/lib/__tests__/config-validator.test.d.ts +4 -0
- package/lib/__tests__/config-validator.test.js +152 -0
- package/lib/__tests__/di-container.test.d.ts +4 -0
- package/lib/__tests__/di-container.test.js +114 -0
- package/lib/__tests__/errors.test.d.ts +4 -0
- package/lib/__tests__/errors.test.js +111 -0
- package/lib/__tests__/integration.test.d.ts +5 -0
- package/lib/__tests__/integration.test.js +112 -0
- package/lib/__tests__/lifecycle.test.d.ts +4 -0
- package/lib/__tests__/lifecycle.test.js +163 -0
- package/lib/account.js +2 -1
- package/lib/base-app.d.ts +14 -3
- package/lib/base-app.js +215 -137
- package/lib/config-validator.d.ts +45 -0
- package/lib/config-validator.js +173 -0
- package/lib/db.d.ts +1 -1
- package/lib/di-container.d.ts +60 -0
- package/lib/di-container.js +103 -0
- package/lib/errors.d.ts +157 -0
- package/lib/errors.js +257 -0
- package/lib/index.d.ts +8 -4
- package/lib/index.js +11 -3
- package/lib/lifecycle.d.ts +75 -0
- package/lib/lifecycle.js +175 -0
- package/lib/logger.d.ts +76 -0
- package/lib/logger.js +156 -0
- package/lib/protocol.d.ts +2 -3
- package/lib/protocol.js +4 -0
- package/lib/rate-limiter.d.ts +88 -0
- package/lib/rate-limiter.js +196 -0
- package/lib/registry.js +0 -5
- package/lib/retry.d.ts +87 -0
- package/lib/retry.js +205 -0
- package/lib/router.d.ts +43 -6
- package/lib/router.js +139 -12
- package/lib/types.d.ts +1 -0
- package/lib/types.js +2 -1
- package/package.json +19 -10
|
@@ -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.js
CHANGED
|
@@ -86,9 +86,10 @@ export class Account extends EventEmitter {
|
|
|
86
86
|
return this.adapter.getFriendList(this.account_id);
|
|
87
87
|
}
|
|
88
88
|
async dispatch(commonEvent) {
|
|
89
|
-
this.logger.debug(`Dispatching event: ${commonEvent.type}`);
|
|
89
|
+
this.logger.debug(`Dispatching event: ${commonEvent.type} to ${this.protocols.length} protocol(s)`);
|
|
90
90
|
// Each protocol instance formats the common event to its own standard
|
|
91
91
|
for (const protocol of this.protocols) {
|
|
92
|
+
this.logger.debug(`Dispatching to protocol: ${protocol.name}/${protocol.version}`);
|
|
92
93
|
protocol.dispatch(commonEvent);
|
|
93
94
|
}
|
|
94
95
|
}
|
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
|
|
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,20 @@ 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:
|
|
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;
|
|
56
67
|
get adapterConfigs(): Map<string, Account.Config[]>;
|
|
57
68
|
private initAdapters;
|
|
58
69
|
addAccount<P extends keyof Adapter.Configs>(config: Account.Config<P>): void;
|
package/lib/base-app.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import Koa from "koa";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import "reflect-metadata";
|
|
4
|
-
import * as fs from "fs";
|
|
5
4
|
import { writeFileSync } from "fs";
|
|
6
5
|
import log4js from "log4js";
|
|
7
6
|
import { createServer } from "http";
|
|
8
7
|
import yaml from "js-yaml";
|
|
9
|
-
import
|
|
8
|
+
import KoaBody from "koa-body";
|
|
10
9
|
import basicAuth from "koa-basic-auth";
|
|
11
10
|
const { configure, connectLogger, getLogger } = log4js;
|
|
12
|
-
import { deepClone, deepMerge
|
|
11
|
+
import { deepClone, deepMerge } from "./utils.js";
|
|
13
12
|
import { Router } from "./router.js";
|
|
14
13
|
import * as path from "path";
|
|
15
14
|
import process from "process";
|
|
16
15
|
import { SqliteDB } from "./db.js";
|
|
17
|
-
import
|
|
16
|
+
import pkg from "../package.json" with { type: "json" };
|
|
17
|
+
import { AdapterRegistry, ProtocolRegistry } from "./registry.js";
|
|
18
|
+
import { ConfigValidator, BaseAppConfigSchema } from "./config-validator.js";
|
|
19
|
+
import { LifecycleManager } from "./lifecycle.js";
|
|
20
|
+
import { ErrorHandler, ConfigError } from "./errors.js";
|
|
21
|
+
import { createLogger } from "./logger.js";
|
|
18
22
|
export { configure, yaml, connectLogger };
|
|
19
23
|
export class BaseApp extends Koa {
|
|
20
24
|
config;
|
|
21
25
|
httpServer;
|
|
22
26
|
isStarted = false;
|
|
23
27
|
logger;
|
|
28
|
+
enhancedLogger;
|
|
29
|
+
lifecycle;
|
|
24
30
|
static get configPath() {
|
|
25
31
|
return path.join(BaseApp.configDir, "config.yaml");
|
|
26
32
|
}
|
|
@@ -32,10 +38,8 @@ export class BaseApp extends Koa {
|
|
|
32
38
|
}
|
|
33
39
|
db;
|
|
34
40
|
adapters = new Map();
|
|
35
|
-
ws;
|
|
36
41
|
router;
|
|
37
42
|
get info() {
|
|
38
|
-
const pkg = require(path.resolve(__dirname, "../../package.json"));
|
|
39
43
|
const free_memory = os.freemem();
|
|
40
44
|
const total_memory = os.totalmem();
|
|
41
45
|
return {
|
|
@@ -58,35 +62,176 @@ export class BaseApp extends Koa {
|
|
|
58
62
|
}
|
|
59
63
|
constructor(config = {}) {
|
|
60
64
|
super(config);
|
|
61
|
-
|
|
65
|
+
// 初始化生命周期管理器
|
|
66
|
+
this.lifecycle = new LifecycleManager();
|
|
67
|
+
// 合并配置并验证
|
|
68
|
+
const mergedConfig = deepMerge(deepClone(BaseApp.defaultConfig), config);
|
|
69
|
+
try {
|
|
70
|
+
this.config = ConfigValidator.validateWithDefaults(mergedConfig, BaseAppConfigSchema);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const configError = ErrorHandler.wrap(error, { config: mergedConfig });
|
|
74
|
+
throw new ConfigError('Configuration validation failed', {
|
|
75
|
+
context: { originalError: configError.toJSON() },
|
|
76
|
+
cause: configError,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
62
79
|
this.init();
|
|
63
80
|
}
|
|
64
81
|
init() {
|
|
65
|
-
|
|
66
|
-
this.
|
|
82
|
+
// 初始化传统日志(保持兼容性)
|
|
83
|
+
this.logger = getLogger("[onebots]");
|
|
67
84
|
this.logger.level = this.config.log_level;
|
|
85
|
+
// 初始化增强日志
|
|
86
|
+
this.enhancedLogger = createLogger("[onebots]", this.config.log_level);
|
|
87
|
+
// 注册数据库资源到生命周期管理器
|
|
88
|
+
this.db = new SqliteDB(path.resolve(BaseApp.dataDir, this.config.database));
|
|
89
|
+
this.lifecycle.register('database', () => {
|
|
90
|
+
// 数据库清理逻辑(如果需要)
|
|
91
|
+
});
|
|
92
|
+
// 创建 HTTP 服务器
|
|
68
93
|
this.httpServer = createServer(this.callback());
|
|
69
|
-
this.router = new Router(this.httpServer
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
this.router = new Router(this.httpServer);
|
|
95
|
+
// 注册路由清理
|
|
96
|
+
this.lifecycle.register('router', () => {
|
|
97
|
+
return this.router.cleanupAsync();
|
|
98
|
+
});
|
|
99
|
+
// 注册 HTTP 服务器清理
|
|
100
|
+
this.lifecycle.register('httpServer', () => {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
this.httpServer.close(() => resolve());
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
// 注册健康检查端点(无需认证)
|
|
106
|
+
this.setupHealthEndpoints();
|
|
107
|
+
this.use(KoaBody())
|
|
73
108
|
.use(async (ctx, next) => {
|
|
74
|
-
|
|
75
|
-
if (
|
|
109
|
+
// 健康检查端点跳过认证
|
|
110
|
+
if (ctx.path === '/health' || ctx.path === '/ready' || ctx.path === '/metrics') {
|
|
111
|
+
return next();
|
|
112
|
+
}
|
|
113
|
+
// 检查是否是协议路径格式: /{platform}/{accountId}/{protocol}/{version}/...
|
|
114
|
+
const pathParts = ctx.path?.split("/").filter(p => p) || [];
|
|
115
|
+
const [_platform, _accountId, protocol, version] = pathParts;
|
|
116
|
+
if (ProtocolRegistry.has(protocol, version)) {
|
|
76
117
|
return next();
|
|
77
|
-
|
|
118
|
+
}
|
|
119
|
+
return await basicAuth({
|
|
78
120
|
name: this.config.username,
|
|
79
121
|
pass: this.config.password,
|
|
80
122
|
})(ctx, next);
|
|
123
|
+
})
|
|
124
|
+
.use(this.router.routes())
|
|
125
|
+
.use(this.router.allowedMethods());
|
|
126
|
+
this.enhancedLogger.info('Application initialized', {
|
|
127
|
+
username: this.config.username,
|
|
128
|
+
port: this.config.port,
|
|
81
129
|
});
|
|
82
|
-
this.ws = this.router.ws("/");
|
|
83
130
|
this.initAdapters();
|
|
84
131
|
}
|
|
85
132
|
getLogger(patform) {
|
|
86
|
-
const logger = getLogger(`[
|
|
133
|
+
const logger = getLogger(`[onebots:${patform}]`);
|
|
87
134
|
logger.level = this.config.log_level;
|
|
88
135
|
return logger;
|
|
89
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* 获取增强的 Logger 实例
|
|
139
|
+
*/
|
|
140
|
+
getEnhancedLogger(name) {
|
|
141
|
+
return createLogger(`[onebots:${name}]`, this.config.log_level);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 设置健康检查端点
|
|
145
|
+
*/
|
|
146
|
+
setupHealthEndpoints() {
|
|
147
|
+
// /health - 基础健康检查(存活探针)
|
|
148
|
+
this.router.get('/health', (ctx) => {
|
|
149
|
+
ctx.body = {
|
|
150
|
+
status: 'ok',
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
uptime: process.uptime(),
|
|
153
|
+
version: pkg.version,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
// /ready - 就绪检查(就绪探针)
|
|
157
|
+
this.router.get('/ready', (ctx) => {
|
|
158
|
+
const adaptersStatus = {};
|
|
159
|
+
let allReady = true;
|
|
160
|
+
let totalOnline = 0;
|
|
161
|
+
let totalAccounts = 0;
|
|
162
|
+
for (const [platform, adapter] of this.adapters) {
|
|
163
|
+
let online = 0;
|
|
164
|
+
let offline = 0;
|
|
165
|
+
for (const account of adapter.accounts.values()) {
|
|
166
|
+
totalAccounts++;
|
|
167
|
+
if (account.status === 'online') {
|
|
168
|
+
online++;
|
|
169
|
+
totalOnline++;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
offline++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
adaptersStatus[platform] = { online, offline, total: online + offline };
|
|
176
|
+
if (offline > 0)
|
|
177
|
+
allReady = false;
|
|
178
|
+
}
|
|
179
|
+
ctx.status = allReady && this.isStarted ? 200 : 503;
|
|
180
|
+
ctx.body = {
|
|
181
|
+
ready: allReady && this.isStarted,
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
server: this.isStarted,
|
|
184
|
+
adapters: adaptersStatus,
|
|
185
|
+
summary: {
|
|
186
|
+
total_adapters: this.adapters.size,
|
|
187
|
+
total_accounts: totalAccounts,
|
|
188
|
+
online_accounts: totalOnline,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
// /metrics - 简单的 Prometheus 格式指标
|
|
193
|
+
this.router.get('/metrics', (ctx) => {
|
|
194
|
+
const metrics = [];
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
// 基础指标
|
|
197
|
+
metrics.push(`# HELP onebots_info OneBots application info`);
|
|
198
|
+
metrics.push(`# TYPE onebots_info gauge`);
|
|
199
|
+
metrics.push(`onebots_info{version="${pkg.version}"} 1`);
|
|
200
|
+
metrics.push(`# HELP onebots_uptime_seconds Application uptime in seconds`);
|
|
201
|
+
metrics.push(`# TYPE onebots_uptime_seconds gauge`);
|
|
202
|
+
metrics.push(`onebots_uptime_seconds ${process.uptime()}`);
|
|
203
|
+
metrics.push(`# HELP onebots_started Whether the application is started`);
|
|
204
|
+
metrics.push(`# TYPE onebots_started gauge`);
|
|
205
|
+
metrics.push(`onebots_started ${this.isStarted ? 1 : 0}`);
|
|
206
|
+
// 内存使用
|
|
207
|
+
const memUsage = process.memoryUsage();
|
|
208
|
+
metrics.push(`# HELP onebots_memory_bytes Memory usage in bytes`);
|
|
209
|
+
metrics.push(`# TYPE onebots_memory_bytes gauge`);
|
|
210
|
+
metrics.push(`onebots_memory_bytes{type="rss"} ${memUsage.rss}`);
|
|
211
|
+
metrics.push(`onebots_memory_bytes{type="heapTotal"} ${memUsage.heapTotal}`);
|
|
212
|
+
metrics.push(`onebots_memory_bytes{type="heapUsed"} ${memUsage.heapUsed}`);
|
|
213
|
+
// 适配器和账号指标
|
|
214
|
+
metrics.push(`# HELP onebots_adapters_total Total number of adapters`);
|
|
215
|
+
metrics.push(`# TYPE onebots_adapters_total gauge`);
|
|
216
|
+
metrics.push(`onebots_adapters_total ${this.adapters.size}`);
|
|
217
|
+
metrics.push(`# HELP onebots_accounts_total Total accounts by platform and status`);
|
|
218
|
+
metrics.push(`# TYPE onebots_accounts_total gauge`);
|
|
219
|
+
for (const [platform, adapter] of this.adapters) {
|
|
220
|
+
let online = 0;
|
|
221
|
+
let offline = 0;
|
|
222
|
+
for (const account of adapter.accounts.values()) {
|
|
223
|
+
if (account.status === 'online')
|
|
224
|
+
online++;
|
|
225
|
+
else
|
|
226
|
+
offline++;
|
|
227
|
+
}
|
|
228
|
+
metrics.push(`onebots_accounts_total{platform="${platform}",status="online"} ${online}`);
|
|
229
|
+
metrics.push(`onebots_accounts_total{platform="${platform}",status="offline"} ${offline}`);
|
|
230
|
+
}
|
|
231
|
+
ctx.type = 'text/plain; charset=utf-8';
|
|
232
|
+
ctx.body = metrics.join('\n') + '\n';
|
|
233
|
+
});
|
|
234
|
+
}
|
|
90
235
|
get adapterConfigs() {
|
|
91
236
|
const map = new Map();
|
|
92
237
|
Object.keys(this.config).forEach(key => {
|
|
@@ -169,124 +314,39 @@ export class BaseApp extends Koa {
|
|
|
169
314
|
return adapter;
|
|
170
315
|
}
|
|
171
316
|
async start() {
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
317
|
+
const stopTimer = this.enhancedLogger.start('Application start');
|
|
318
|
+
try {
|
|
319
|
+
// 执行启动钩子
|
|
320
|
+
await this.lifecycle.start();
|
|
321
|
+
// 启动 HTTP 服务器
|
|
322
|
+
await new Promise((resolve, reject) => {
|
|
323
|
+
this.httpServer.once('error', reject);
|
|
324
|
+
this.httpServer.listen(this.config.port, () => {
|
|
325
|
+
this.httpServer.removeListener('error', reject);
|
|
326
|
+
resolve();
|
|
180
327
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
186
|
-
process.once("disconnect", () => {
|
|
187
|
-
fs.unwatchFile(BaseApp.logFile, fileListener);
|
|
188
|
-
});
|
|
189
|
-
this.ws.on("connection", async (client) => {
|
|
190
|
-
client.send(JSON.stringify({
|
|
191
|
-
event: "system.sync",
|
|
192
|
-
data: {
|
|
193
|
-
config: fs.readFileSync(BaseApp.configPath, "utf8"),
|
|
194
|
-
adapters: [...this.adapters.values()].map(adapter => {
|
|
195
|
-
return adapter.info;
|
|
196
|
-
}),
|
|
197
|
-
app: this.info,
|
|
198
|
-
logs: fs.existsSync(BaseApp.logFile) ? await readLine(100, BaseApp.logFile) : "",
|
|
199
|
-
},
|
|
200
|
-
}));
|
|
201
|
-
client.on("message", async (raw) => {
|
|
202
|
-
let payload = {};
|
|
328
|
+
});
|
|
329
|
+
this.enhancedLogger.mark(`Server listening at http://0.0.0.0:${this.config.port}/${this.config.path ? this.config.path : ""}`, { port: this.config.port, path: this.config.path });
|
|
330
|
+
// 启动所有适配器
|
|
331
|
+
for (const [platform, adapter] of this.adapters) {
|
|
203
332
|
try {
|
|
204
|
-
|
|
333
|
+
await adapter.start();
|
|
334
|
+
this.enhancedLogger.info(`Adapter started`, { platform });
|
|
205
335
|
}
|
|
206
|
-
catch {
|
|
207
|
-
|
|
336
|
+
catch (error) {
|
|
337
|
+
const wrappedError = ErrorHandler.wrap(error, { platform });
|
|
338
|
+
this.enhancedLogger.error(wrappedError, { platform });
|
|
339
|
+
// 继续启动其他适配器
|
|
208
340
|
}
|
|
209
|
-
switch (payload.action) {
|
|
210
|
-
case "system.input":
|
|
211
|
-
// 将流的模式切换到“流动模式”
|
|
212
|
-
process.stdin.resume();
|
|
213
|
-
// 使用以下函数来模拟输入数据
|
|
214
|
-
function simulateInput(data) {
|
|
215
|
-
process.nextTick(() => {
|
|
216
|
-
process.stdin.emit("data", data);
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
simulateInput(Buffer.from(payload.data + "\n", "utf8"));
|
|
220
|
-
// 模拟结束
|
|
221
|
-
process.nextTick(() => {
|
|
222
|
-
process.stdin.emit("end");
|
|
223
|
-
});
|
|
224
|
-
return true;
|
|
225
|
-
case "system.saveConfig":
|
|
226
|
-
return fs.writeFileSync(BaseApp.configPath, payload.data, "utf8");
|
|
227
|
-
case "system.reload":
|
|
228
|
-
const config = yaml.load(fs.readFileSync(BaseApp.configPath, "utf8"));
|
|
229
|
-
return this.reload(config);
|
|
230
|
-
case "bot.start": {
|
|
231
|
-
const { platform, uin } = JSON.parse(payload.data);
|
|
232
|
-
await this.adapters.get(platform)?.setOnline(uin);
|
|
233
|
-
return client.send(JSON.stringify({
|
|
234
|
-
event: "bot.change",
|
|
235
|
-
data: this.adapters.get(platform).getAccount(uin).info,
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
case "bot.stop": {
|
|
239
|
-
const { platform, uin } = JSON.parse(payload.data);
|
|
240
|
-
await this.adapters.get(platform)?.setOffline(uin);
|
|
241
|
-
return client.send(JSON.stringify({
|
|
242
|
-
event: "bot.change",
|
|
243
|
-
data: this.adapters.get(platform).getAccount(uin).info,
|
|
244
|
-
}));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
this.router.get("/list", ctx => {
|
|
250
|
-
ctx.body = this.accounts.map(bot => {
|
|
251
|
-
return bot.info;
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
this.router.post("/add", (ctx, next) => {
|
|
255
|
-
const config = (ctx.request.body || {});
|
|
256
|
-
try {
|
|
257
|
-
this.addAccount(config);
|
|
258
|
-
ctx.body = `添加成功`;
|
|
259
|
-
}
|
|
260
|
-
catch (e) {
|
|
261
|
-
ctx.body = e.message;
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
this.router.post("/edit", (ctx, next) => {
|
|
265
|
-
const config = (ctx.request.body || {});
|
|
266
|
-
try {
|
|
267
|
-
this.updateAccount(config);
|
|
268
|
-
ctx.body = `修改成功`;
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
ctx.body = e.message;
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
this.router.get("/remove", (ctx, next) => {
|
|
275
|
-
const { uin, platform, force } = ctx.request.query;
|
|
276
|
-
try {
|
|
277
|
-
this.removeAccount(String(platform), String(uin), Boolean(force));
|
|
278
|
-
ctx.body = `移除成功`;
|
|
279
|
-
}
|
|
280
|
-
catch (e) {
|
|
281
|
-
console.log(e);
|
|
282
|
-
ctx.body = e.message;
|
|
283
341
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
342
|
+
this.isStarted = true;
|
|
343
|
+
stopTimer();
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const wrappedError = ErrorHandler.wrap(error);
|
|
347
|
+
this.enhancedLogger.fatal(wrappedError);
|
|
348
|
+
throw wrappedError;
|
|
288
349
|
}
|
|
289
|
-
this.isStarted = true;
|
|
290
350
|
}
|
|
291
351
|
async reload(config) {
|
|
292
352
|
await this.stop();
|
|
@@ -295,14 +355,32 @@ export class BaseApp extends Koa {
|
|
|
295
355
|
await this.start();
|
|
296
356
|
}
|
|
297
357
|
async stop() {
|
|
298
|
-
|
|
299
|
-
|
|
358
|
+
const stopTimer = this.enhancedLogger.start('Application stop');
|
|
359
|
+
try {
|
|
360
|
+
// 执行停止钩子
|
|
361
|
+
await this.lifecycle.stop();
|
|
362
|
+
// 停止所有适配器
|
|
363
|
+
const stopPromises = [];
|
|
364
|
+
for (const [platform, adapter] of this.adapters) {
|
|
365
|
+
stopPromises.push(Promise.resolve(adapter.stop()).catch(error => {
|
|
366
|
+
const wrappedError = ErrorHandler.wrap(error, { platform });
|
|
367
|
+
this.enhancedLogger.error(wrappedError, { platform });
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
await Promise.all(stopPromises);
|
|
371
|
+
this.adapters.clear();
|
|
372
|
+
// 清理资源
|
|
373
|
+
await this.lifecycle.cleanup();
|
|
374
|
+
this.emit("close");
|
|
375
|
+
this.isStarted = false;
|
|
376
|
+
stopTimer();
|
|
377
|
+
this.enhancedLogger.info('Application stopped');
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
const wrappedError = ErrorHandler.wrap(error);
|
|
381
|
+
this.enhancedLogger.error(wrappedError);
|
|
382
|
+
throw wrappedError;
|
|
300
383
|
}
|
|
301
|
-
this.adapters.clear();
|
|
302
|
-
// this.ws.close()
|
|
303
|
-
this.httpServer.close();
|
|
304
|
-
this.emit("close");
|
|
305
|
-
this.isStarted = false;
|
|
306
384
|
}
|
|
307
385
|
}
|
|
308
386
|
(function (BaseApp) {
|