@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.
- 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.d.ts +4 -1
- package/lib/account.js +6 -3
- package/lib/adapter.d.ts +67 -1
- package/lib/adapter.js +31 -4
- package/lib/base-app.d.ts +30 -3
- package/lib/base-app.js +295 -142
- package/lib/circuit-breaker.d.ts +94 -0
- package/lib/circuit-breaker.js +206 -0
- package/lib/config-validator.d.ts +51 -0
- package/lib/config-validator.js +184 -0
- package/lib/connection-pool.d.ts +68 -0
- package/lib/connection-pool.js +202 -0
- package/lib/db.d.ts +2 -1
- package/lib/db.js +11 -2
- 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 +13 -4
- package/lib/index.js +17 -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/metrics.d.ts +80 -0
- package/lib/metrics.js +201 -0
- package/lib/middleware/index.d.ts +8 -0
- package/lib/middleware/index.js +8 -0
- package/lib/middleware/metrics-collector.d.ts +9 -0
- package/lib/middleware/metrics-collector.js +64 -0
- package/lib/middleware/rate-limit.d.ts +32 -0
- package/lib/middleware/rate-limit.js +149 -0
- package/lib/middleware/security-audit.d.ts +33 -0
- package/lib/middleware/security-audit.js +223 -0
- package/lib/middleware/token-manager.d.ts +73 -0
- package/lib/middleware/token-manager.js +186 -0
- package/lib/middleware/token-validator.d.ts +42 -0
- package/lib/middleware/token-validator.js +198 -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.d.ts +27 -0
- package/lib/registry.js +40 -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/timestamp.d.ts +42 -0
- package/lib/timestamp.js +72 -0
- package/lib/types.d.ts +1 -0
- package/lib/types.js +2 -1
- package/lib/utils.d.ts +2 -1
- package/lib/utils.js +11 -19
- package/package.json +24 -9
package/lib/base-app.js
CHANGED
|
@@ -6,21 +6,32 @@ import { writeFileSync } from "fs";
|
|
|
6
6
|
import log4js from "log4js";
|
|
7
7
|
import { createServer } from "http";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import KoaBody from "koa-body";
|
|
10
|
+
import koaStatic from "koa-static";
|
|
11
11
|
const { configure, connectLogger, getLogger } = log4js;
|
|
12
|
-
import { deepClone, deepMerge
|
|
12
|
+
import { deepClone, deepMerge } from "./utils.js";
|
|
13
13
|
import { Router } from "./router.js";
|
|
14
14
|
import * as path from "path";
|
|
15
15
|
import process from "process";
|
|
16
16
|
import { SqliteDB } from "./db.js";
|
|
17
|
+
import pkg from "../package.json" with { type: "json" };
|
|
17
18
|
import { AdapterRegistry } from "./registry.js";
|
|
19
|
+
import { ConfigValidator, BaseAppConfigSchema } from "./config-validator.js";
|
|
20
|
+
import { LifecycleManager } from "./lifecycle.js";
|
|
21
|
+
import { ErrorHandler, ConfigError } from "./errors.js";
|
|
22
|
+
import { createLogger } from "./logger.js";
|
|
23
|
+
import { initSecurityAudit, securityAudit, closeSecurityAudit } from "./middleware/security-audit.js";
|
|
24
|
+
import { defaultRateLimit } from "./middleware/rate-limit.js";
|
|
25
|
+
import { metricsCollector } from "./middleware/metrics-collector.js";
|
|
26
|
+
import { metrics } from "./metrics.js";
|
|
18
27
|
export { configure, yaml, connectLogger };
|
|
19
28
|
export class BaseApp extends Koa {
|
|
20
29
|
config;
|
|
21
30
|
httpServer;
|
|
22
31
|
isStarted = false;
|
|
23
32
|
logger;
|
|
33
|
+
enhancedLogger;
|
|
34
|
+
lifecycle;
|
|
24
35
|
static get configPath() {
|
|
25
36
|
return path.join(BaseApp.configDir, "config.yaml");
|
|
26
37
|
}
|
|
@@ -32,10 +43,8 @@ export class BaseApp extends Koa {
|
|
|
32
43
|
}
|
|
33
44
|
db;
|
|
34
45
|
adapters = new Map();
|
|
35
|
-
ws;
|
|
36
46
|
router;
|
|
37
47
|
get info() {
|
|
38
|
-
const pkg = require(path.resolve(__dirname, "../../package.json"));
|
|
39
48
|
const free_memory = os.freemem();
|
|
40
49
|
const total_memory = os.totalmem();
|
|
41
50
|
return {
|
|
@@ -58,35 +67,241 @@ export class BaseApp extends Koa {
|
|
|
58
67
|
}
|
|
59
68
|
constructor(config = {}) {
|
|
60
69
|
super(config);
|
|
61
|
-
|
|
70
|
+
// 初始化生命周期管理器
|
|
71
|
+
this.lifecycle = new LifecycleManager();
|
|
72
|
+
// 合并配置并验证
|
|
73
|
+
const mergedConfig = deepMerge(deepClone(BaseApp.defaultConfig), config);
|
|
74
|
+
try {
|
|
75
|
+
this.config = ConfigValidator.validateWithDefaults(mergedConfig, BaseAppConfigSchema);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const configError = ErrorHandler.wrap(error, { config: mergedConfig });
|
|
79
|
+
throw new ConfigError('Configuration validation failed', {
|
|
80
|
+
context: { originalError: configError.toJSON() },
|
|
81
|
+
cause: configError,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
62
84
|
this.init();
|
|
63
85
|
}
|
|
64
86
|
init() {
|
|
65
|
-
|
|
66
|
-
this.
|
|
87
|
+
// 初始化传统日志(保持兼容性)
|
|
88
|
+
this.logger = getLogger("[onebots]");
|
|
67
89
|
this.logger.level = this.config.log_level;
|
|
90
|
+
// 初始化增强日志
|
|
91
|
+
this.enhancedLogger = createLogger("[onebots]", this.config.log_level);
|
|
92
|
+
// 注册数据库资源到生命周期管理器
|
|
93
|
+
this.db = new SqliteDB(path.resolve(BaseApp.dataDir, this.config.database));
|
|
94
|
+
this.lifecycle.register('database', () => {
|
|
95
|
+
// 数据库清理逻辑(如果需要)
|
|
96
|
+
});
|
|
97
|
+
// 创建 HTTP 服务器
|
|
68
98
|
this.httpServer = createServer(this.callback());
|
|
69
|
-
this.router = new Router(this.httpServer
|
|
70
|
-
|
|
99
|
+
this.router = new Router(this.httpServer);
|
|
100
|
+
// 注册路由清理
|
|
101
|
+
this.lifecycle.register('router', () => {
|
|
102
|
+
return this.router.cleanupAsync();
|
|
103
|
+
});
|
|
104
|
+
// 注册 HTTP 服务器清理
|
|
105
|
+
this.lifecycle.register('httpServer', () => {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
this.httpServer.close(() => resolve());
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// 初始化安全审计日志
|
|
111
|
+
initSecurityAudit(path.join(BaseApp.dataDir, 'audit'));
|
|
112
|
+
// 注册健康检查端点(无需认证)
|
|
113
|
+
this.setupHealthEndpoints();
|
|
114
|
+
// 用户配置的站点根静态目录(需在 Router 等功能路由之前,便于 GET /xxx.txt 等直出)
|
|
115
|
+
const publicStaticDir = this.resolvePublicStaticDirectory();
|
|
116
|
+
if (publicStaticDir) {
|
|
117
|
+
this.enhancedLogger.info('已启用站点根静态目录', { dir: publicStaticDir });
|
|
118
|
+
this.use(koaStatic(publicStaticDir));
|
|
119
|
+
}
|
|
120
|
+
// 中间件链(multipart:管理端上传站点静态文件等)
|
|
121
|
+
this.use(KoaBody({
|
|
122
|
+
multipart: true,
|
|
123
|
+
formidable: {
|
|
124
|
+
maxFileSize: 2 * 1024 * 1024,
|
|
125
|
+
keepExtensions: true,
|
|
126
|
+
},
|
|
127
|
+
}))
|
|
128
|
+
// 性能指标收集(最早执行,以便记录所有请求)
|
|
129
|
+
.use(metricsCollector())
|
|
130
|
+
// 安全审计日志
|
|
131
|
+
.use(securityAudit())
|
|
132
|
+
// 速率限制(在认证之前,防止暴力破解)
|
|
133
|
+
.use(defaultRateLimit)
|
|
134
|
+
.use(async (_ctx, next) => {
|
|
135
|
+
// 本层不做鉴权。管理端鉴权仅针对 /api(由 onebots 应用层负责);各平台对外 API(如 /{platform}/{accountId}/onebot/v11/...)由各自协议/适配器单独鉴权。
|
|
136
|
+
return next();
|
|
137
|
+
})
|
|
71
138
|
.use(this.router.routes())
|
|
72
|
-
.use(this.router.allowedMethods())
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return next();
|
|
77
|
-
return basicAuth({
|
|
78
|
-
name: this.config.username,
|
|
79
|
-
pass: this.config.password,
|
|
80
|
-
})(ctx, next);
|
|
139
|
+
.use(this.router.allowedMethods());
|
|
140
|
+
this.enhancedLogger.info('Application initialized', {
|
|
141
|
+
username: this.config.username,
|
|
142
|
+
port: this.config.port,
|
|
81
143
|
});
|
|
82
|
-
this.ws = this.router.ws("/");
|
|
83
144
|
this.initAdapters();
|
|
84
145
|
}
|
|
85
146
|
getLogger(patform) {
|
|
86
|
-
const logger = getLogger(`[
|
|
147
|
+
const logger = getLogger(`[onebots:${patform}]`);
|
|
87
148
|
logger.level = this.config.log_level;
|
|
88
149
|
return logger;
|
|
89
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* 获取增强的 Logger 实例
|
|
153
|
+
*/
|
|
154
|
+
getEnhancedLogger(name) {
|
|
155
|
+
return createLogger(`[onebots:${name}]`, this.config.log_level);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 设置健康检查端点
|
|
159
|
+
*/
|
|
160
|
+
setupHealthEndpoints() {
|
|
161
|
+
// /health - 基础健康检查(存活探针)
|
|
162
|
+
this.router.get('/health', (ctx) => {
|
|
163
|
+
ctx.body = {
|
|
164
|
+
status: 'ok',
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
uptime: process.uptime(),
|
|
167
|
+
version: pkg.version,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
// /ready - 就绪检查(就绪探针)
|
|
171
|
+
this.router.get('/ready', (ctx) => {
|
|
172
|
+
const adaptersStatus = {};
|
|
173
|
+
let allReady = true;
|
|
174
|
+
let totalOnline = 0;
|
|
175
|
+
let totalAccounts = 0;
|
|
176
|
+
for (const [platform, adapter] of this.adapters) {
|
|
177
|
+
let online = 0;
|
|
178
|
+
let offline = 0;
|
|
179
|
+
for (const account of adapter.accounts.values()) {
|
|
180
|
+
totalAccounts++;
|
|
181
|
+
if (account.status === 'online') {
|
|
182
|
+
online++;
|
|
183
|
+
totalOnline++;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
offline++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
adaptersStatus[platform] = { online, offline, total: online + offline };
|
|
190
|
+
if (offline > 0)
|
|
191
|
+
allReady = false;
|
|
192
|
+
}
|
|
193
|
+
ctx.status = allReady && this.isStarted ? 200 : 503;
|
|
194
|
+
ctx.body = {
|
|
195
|
+
ready: allReady && this.isStarted,
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
server: this.isStarted,
|
|
198
|
+
adapters: adaptersStatus,
|
|
199
|
+
summary: {
|
|
200
|
+
total_adapters: this.adapters.size,
|
|
201
|
+
total_accounts: totalAccounts,
|
|
202
|
+
online_accounts: totalOnline,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
// /metrics - Prometheus 格式指标
|
|
207
|
+
this.router.get('/metrics', (ctx) => {
|
|
208
|
+
const metricLines = [];
|
|
209
|
+
// 基础指标
|
|
210
|
+
metricLines.push(`# HELP onebots_info OneBots application info`);
|
|
211
|
+
metricLines.push(`# TYPE onebots_info gauge`);
|
|
212
|
+
metricLines.push(`onebots_info{version="${pkg.version}"} 1`);
|
|
213
|
+
metricLines.push(`# HELP onebots_uptime_seconds Application uptime in seconds`);
|
|
214
|
+
metricLines.push(`# TYPE onebots_uptime_seconds gauge`);
|
|
215
|
+
metricLines.push(`onebots_uptime_seconds ${process.uptime()}`);
|
|
216
|
+
metricLines.push(`# HELP onebots_started Whether the application is started`);
|
|
217
|
+
metricLines.push(`# TYPE onebots_started gauge`);
|
|
218
|
+
metricLines.push(`onebots_started ${this.isStarted ? 1 : 0}`);
|
|
219
|
+
// 内存使用
|
|
220
|
+
const memUsage = process.memoryUsage();
|
|
221
|
+
metricLines.push(`# HELP onebots_memory_bytes Memory usage in bytes`);
|
|
222
|
+
metricLines.push(`# TYPE onebots_memory_bytes gauge`);
|
|
223
|
+
metricLines.push(`onebots_memory_bytes{type="rss"} ${memUsage.rss}`);
|
|
224
|
+
metricLines.push(`onebots_memory_bytes{type="heapTotal"} ${memUsage.heapTotal}`);
|
|
225
|
+
metricLines.push(`onebots_memory_bytes{type="heapUsed"} ${memUsage.heapUsed}`);
|
|
226
|
+
metricLines.push(`onebots_memory_bytes{type="external"} ${memUsage.external}`);
|
|
227
|
+
// 适配器和账号指标
|
|
228
|
+
metricLines.push(`# HELP onebots_adapters_total Total number of adapters`);
|
|
229
|
+
metricLines.push(`# TYPE onebots_adapters_total gauge`);
|
|
230
|
+
metricLines.push(`onebots_adapters_total ${this.adapters.size}`);
|
|
231
|
+
metricLines.push(`# HELP onebots_accounts_total Total accounts by platform and status`);
|
|
232
|
+
metricLines.push(`# TYPE onebots_accounts_total gauge`);
|
|
233
|
+
for (const [platform, adapter] of this.adapters) {
|
|
234
|
+
let online = 0;
|
|
235
|
+
let offline = 0;
|
|
236
|
+
for (const account of adapter.accounts.values()) {
|
|
237
|
+
if (account.status === 'online')
|
|
238
|
+
online++;
|
|
239
|
+
else
|
|
240
|
+
offline++;
|
|
241
|
+
}
|
|
242
|
+
metricLines.push(`onebots_accounts_total{platform="${platform}",status="online"} ${online}`);
|
|
243
|
+
metricLines.push(`onebots_accounts_total{platform="${platform}",status="offline"} ${offline}`);
|
|
244
|
+
}
|
|
245
|
+
// 添加性能指标
|
|
246
|
+
const prometheusMetrics = metrics.exportPrometheus();
|
|
247
|
+
if (prometheusMetrics.trim()) {
|
|
248
|
+
metricLines.push('\n# Performance metrics');
|
|
249
|
+
metricLines.push(prometheusMetrics);
|
|
250
|
+
}
|
|
251
|
+
ctx.type = 'text/plain; charset=utf-8';
|
|
252
|
+
ctx.body = metricLines.join('\n') + '\n';
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 解析 public_static_dir:相对路径基于配置文件目录(configDir),禁止相对路径穿越出 configDir;绝对路径按原样使用
|
|
257
|
+
*/
|
|
258
|
+
resolvePublicStaticDirectory() {
|
|
259
|
+
const raw = this.config.public_static_dir;
|
|
260
|
+
if (raw == null || String(raw).trim() === '')
|
|
261
|
+
return null;
|
|
262
|
+
const trimmed = String(raw).trim();
|
|
263
|
+
const configDirResolved = path.resolve(BaseApp.configDir);
|
|
264
|
+
const abs = path.isAbsolute(trimmed)
|
|
265
|
+
? path.resolve(trimmed)
|
|
266
|
+
: path.resolve(configDirResolved, trimmed);
|
|
267
|
+
if (!path.isAbsolute(trimmed)) {
|
|
268
|
+
const rel = path.relative(configDirResolved, abs);
|
|
269
|
+
// 禁止 `.`、等价空相对路径,避免整个配置目录被当作静态站点根
|
|
270
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
271
|
+
this.enhancedLogger.warn('public_static_dir 必须为配置目录下的子目录(不能为 . 或路径穿越),已忽略', {
|
|
272
|
+
configured: trimmed,
|
|
273
|
+
resolved: abs,
|
|
274
|
+
configDir: configDirResolved,
|
|
275
|
+
});
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
let st;
|
|
280
|
+
try {
|
|
281
|
+
st = fs.statSync(abs);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
286
|
+
st = fs.statSync(abs);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
this.enhancedLogger.warn('public_static_dir 无法创建或访问,静态托管已跳过', { abs, error: e });
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!st.isDirectory()) {
|
|
294
|
+
this.enhancedLogger.warn('public_static_dir 不是目录,已忽略', { abs });
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return abs;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 管理端 API:当前解析后的站点根静态目录(未配置或无效时为 null)
|
|
301
|
+
*/
|
|
302
|
+
getPublicStaticRoot() {
|
|
303
|
+
return this.resolvePublicStaticDirectory();
|
|
304
|
+
}
|
|
90
305
|
get adapterConfigs() {
|
|
91
306
|
const map = new Map();
|
|
92
307
|
Object.keys(this.config).forEach(key => {
|
|
@@ -166,127 +381,47 @@ export class BaseApp extends Koa {
|
|
|
166
381
|
return this.adapters.get(platform);
|
|
167
382
|
const adapter = AdapterRegistry.create(`${platform}`, this);
|
|
168
383
|
this.adapters.set(platform, adapter);
|
|
384
|
+
this.onAdapterCreated(adapter);
|
|
169
385
|
return adapter;
|
|
170
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* 适配器首次创建后的钩子,子类可覆写以订阅该适配器事件(如 verification:request)
|
|
389
|
+
*/
|
|
390
|
+
onAdapterCreated(_adapter) { }
|
|
171
391
|
async start() {
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
392
|
+
const stopTimer = this.enhancedLogger.start('Application start');
|
|
393
|
+
try {
|
|
394
|
+
// 执行启动钩子
|
|
395
|
+
await this.lifecycle.start();
|
|
396
|
+
// 启动 HTTP 服务器
|
|
397
|
+
await new Promise((resolve, reject) => {
|
|
398
|
+
this.httpServer.once('error', reject);
|
|
399
|
+
this.httpServer.listen(this.config.port, () => {
|
|
400
|
+
this.httpServer.removeListener('error', reject);
|
|
401
|
+
resolve();
|
|
180
402
|
});
|
|
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 = {};
|
|
403
|
+
});
|
|
404
|
+
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 });
|
|
405
|
+
// 启动所有适配器
|
|
406
|
+
for (const [platform, adapter] of this.adapters) {
|
|
203
407
|
try {
|
|
204
|
-
|
|
408
|
+
await adapter.start();
|
|
409
|
+
this.enhancedLogger.info(`Adapter started`, { platform });
|
|
205
410
|
}
|
|
206
|
-
catch {
|
|
207
|
-
|
|
411
|
+
catch (error) {
|
|
412
|
+
const wrappedError = ErrorHandler.wrap(error, { platform });
|
|
413
|
+
this.enhancedLogger.error(wrappedError, { platform });
|
|
414
|
+
// 继续启动其他适配器
|
|
208
415
|
}
|
|
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
416
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
}
|
|
284
|
-
});
|
|
285
|
-
this.logger.mark(`server listen at http://0.0.0.0:${this.config.port}/${this.config.path ? this.config.path : ""}`);
|
|
286
|
-
for (const [_, adapter] of this.adapters) {
|
|
287
|
-
await adapter.start();
|
|
417
|
+
this.isStarted = true;
|
|
418
|
+
stopTimer();
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
const wrappedError = ErrorHandler.wrap(error);
|
|
422
|
+
this.enhancedLogger.fatal(wrappedError);
|
|
423
|
+
throw wrappedError;
|
|
288
424
|
}
|
|
289
|
-
this.isStarted = true;
|
|
290
425
|
}
|
|
291
426
|
async reload(config) {
|
|
292
427
|
await this.stop();
|
|
@@ -295,22 +430,40 @@ export class BaseApp extends Koa {
|
|
|
295
430
|
await this.start();
|
|
296
431
|
}
|
|
297
432
|
async stop() {
|
|
298
|
-
|
|
299
|
-
|
|
433
|
+
const stopTimer = this.enhancedLogger.start('Application stop');
|
|
434
|
+
try {
|
|
435
|
+
// 执行停止钩子
|
|
436
|
+
await this.lifecycle.stop();
|
|
437
|
+
// 停止所有适配器
|
|
438
|
+
const stopPromises = [];
|
|
439
|
+
for (const [platform, adapter] of this.adapters) {
|
|
440
|
+
stopPromises.push(Promise.resolve(adapter.stop()).catch(error => {
|
|
441
|
+
const wrappedError = ErrorHandler.wrap(error, { platform });
|
|
442
|
+
this.enhancedLogger.error(wrappedError, { platform });
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
await Promise.all(stopPromises);
|
|
446
|
+
this.adapters.clear();
|
|
447
|
+
// 清理资源
|
|
448
|
+
await this.lifecycle.cleanup();
|
|
449
|
+
// 关闭安全审计日志
|
|
450
|
+
closeSecurityAudit();
|
|
451
|
+
this.emit("close");
|
|
452
|
+
this.isStarted = false;
|
|
453
|
+
stopTimer();
|
|
454
|
+
this.enhancedLogger.info('Application stopped');
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
const wrappedError = ErrorHandler.wrap(error);
|
|
458
|
+
this.enhancedLogger.error(wrappedError);
|
|
459
|
+
throw wrappedError;
|
|
300
460
|
}
|
|
301
|
-
this.adapters.clear();
|
|
302
|
-
// this.ws.close()
|
|
303
|
-
this.httpServer.close();
|
|
304
|
-
this.emit("close");
|
|
305
|
-
this.isStarted = false;
|
|
306
461
|
}
|
|
307
462
|
}
|
|
308
463
|
(function (BaseApp) {
|
|
309
464
|
BaseApp.defaultConfig = {
|
|
310
465
|
port: 6727,
|
|
311
466
|
database: "onebots.db",
|
|
312
|
-
username: "admin",
|
|
313
|
-
password: "123456",
|
|
314
467
|
timeout: 30,
|
|
315
468
|
general: {},
|
|
316
469
|
log_level: "info",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 熔断器模式实现
|
|
3
|
+
* 防止级联故障,提高系统稳定性
|
|
4
|
+
*/
|
|
5
|
+
export declare enum CircuitState {
|
|
6
|
+
/** 关闭状态:正常处理请求 */
|
|
7
|
+
CLOSED = "CLOSED",
|
|
8
|
+
/** 开启状态:拒绝所有请求 */
|
|
9
|
+
OPEN = "OPEN",
|
|
10
|
+
/** 半开状态:尝试恢复,允许部分请求通过 */
|
|
11
|
+
HALF_OPEN = "HALF_OPEN"
|
|
12
|
+
}
|
|
13
|
+
export interface CircuitBreakerOptions {
|
|
14
|
+
/** 失败阈值:连续失败多少次后开启熔断 */
|
|
15
|
+
failureThreshold?: number;
|
|
16
|
+
/** 成功阈值:半开状态下成功多少次后关闭熔断 */
|
|
17
|
+
successThreshold?: number;
|
|
18
|
+
/** 超时时间:开启状态持续多久后进入半开状态(毫秒) */
|
|
19
|
+
timeout?: number;
|
|
20
|
+
/** 监控窗口大小(毫秒) */
|
|
21
|
+
monitoringPeriod?: number;
|
|
22
|
+
/** 最小请求数:在监控窗口内至少需要多少请求才触发熔断 */
|
|
23
|
+
minimumRequests?: number;
|
|
24
|
+
/** 错误率阈值:错误率超过多少时开启熔断(0-1) */
|
|
25
|
+
errorRateThreshold?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 熔断器类
|
|
29
|
+
*/
|
|
30
|
+
export declare class CircuitBreaker {
|
|
31
|
+
private state;
|
|
32
|
+
private failureCount;
|
|
33
|
+
private successCount;
|
|
34
|
+
private lastFailureTime;
|
|
35
|
+
private records;
|
|
36
|
+
private options;
|
|
37
|
+
constructor(options?: CircuitBreakerOptions);
|
|
38
|
+
/**
|
|
39
|
+
* 执行函数,带熔断保护
|
|
40
|
+
*/
|
|
41
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* 成功回调
|
|
44
|
+
*/
|
|
45
|
+
private onSuccess;
|
|
46
|
+
/**
|
|
47
|
+
* 失败回调
|
|
48
|
+
*/
|
|
49
|
+
private onFailure;
|
|
50
|
+
/**
|
|
51
|
+
* 判断是否应该开启熔断
|
|
52
|
+
*/
|
|
53
|
+
private shouldOpen;
|
|
54
|
+
/**
|
|
55
|
+
* 获取错误率
|
|
56
|
+
*/
|
|
57
|
+
private getErrorRate;
|
|
58
|
+
/**
|
|
59
|
+
* 获取最近的请求记录
|
|
60
|
+
*/
|
|
61
|
+
private getRecentRequests;
|
|
62
|
+
/**
|
|
63
|
+
* 清理过期记录
|
|
64
|
+
*/
|
|
65
|
+
private cleanupOldRecords;
|
|
66
|
+
/**
|
|
67
|
+
* 获取当前状态
|
|
68
|
+
*/
|
|
69
|
+
getState(): CircuitState;
|
|
70
|
+
/**
|
|
71
|
+
* 获取统计信息
|
|
72
|
+
*/
|
|
73
|
+
getStats(): {
|
|
74
|
+
state: CircuitState;
|
|
75
|
+
failureCount: number;
|
|
76
|
+
successCount: number;
|
|
77
|
+
errorRate: number;
|
|
78
|
+
totalRequests: number;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 手动重置熔断器
|
|
82
|
+
*/
|
|
83
|
+
reset(): void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 熔断器开启错误
|
|
87
|
+
*/
|
|
88
|
+
export declare class CircuitBreakerOpenError extends Error {
|
|
89
|
+
constructor(message: string);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 创建带熔断器的函数包装器
|
|
93
|
+
*/
|
|
94
|
+
export declare function withCircuitBreaker<T extends (...args: any[]) => Promise<any>>(fn: T, breaker: CircuitBreaker): T;
|