@onebots/core 1.0.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/lib/__tests__/integration.test.js +2 -2
- package/lib/account.d.ts +4 -1
- package/lib/account.js +4 -2
- package/lib/adapter.d.ts +67 -1
- package/lib/adapter.js +31 -4
- package/lib/base-app.d.ts +16 -0
- package/lib/base-app.js +120 -45
- package/lib/circuit-breaker.d.ts +94 -0
- package/lib/circuit-breaker.js +206 -0
- package/lib/config-validator.d.ts +6 -0
- package/lib/config-validator.js +15 -4
- package/lib/connection-pool.d.ts +68 -0
- package/lib/connection-pool.js +202 -0
- package/lib/db.d.ts +1 -0
- package/lib/db.js +11 -2
- package/lib/index.d.ts +5 -0
- package/lib/index.js +6 -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/registry.d.ts +27 -0
- package/lib/registry.js +40 -0
- package/lib/timestamp.d.ts +42 -0
- package/lib/timestamp.js +72 -0
- package/lib/utils.d.ts +2 -1
- package/lib/utils.js +11 -19
- package/package.json +9 -3
|
@@ -15,8 +15,8 @@ describe('Integration Tests', () => {
|
|
|
15
15
|
const validated = ConfigValidator.validateWithDefaults(config, BaseAppConfigSchema);
|
|
16
16
|
expect(validated.port).toBe(6727);
|
|
17
17
|
expect(validated.database).toBe('onebots.db');
|
|
18
|
-
expect(validated.username).
|
|
19
|
-
expect(validated.password).
|
|
18
|
+
expect(validated.username).toBeUndefined();
|
|
19
|
+
expect(validated.password).toBeUndefined();
|
|
20
20
|
expect(validated.log_level).toBe('info');
|
|
21
21
|
});
|
|
22
22
|
it('should throw ValidationError on invalid config', () => {
|
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
|
-
|
|
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,9 +85,11 @@ export class Account extends EventEmitter {
|
|
|
85
85
|
getFriendList() {
|
|
86
86
|
return this.adapter.getFriendList(this.account_id);
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 将通用事件同步派发到本账号绑定的各协议(协议内自行 catch,避免一次失败阻断其它协议)
|
|
90
|
+
*/
|
|
91
|
+
dispatch(commonEvent) {
|
|
89
92
|
this.logger.debug(`Dispatching event: ${commonEvent.type} to ${this.protocols.length} protocol(s)`);
|
|
90
|
-
// Each protocol instance formats the common event to its own standard
|
|
91
93
|
for (const protocol of this.protocols) {
|
|
92
94
|
this.logger.debug(`Dispatching to protocol: ${protocol.name}/${protocol.version}`);
|
|
93
95
|
protocol.dispatch(commonEvent);
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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
|
@@ -64,6 +64,14 @@ export declare class BaseApp extends Koa {
|
|
|
64
64
|
* 设置健康检查端点
|
|
65
65
|
*/
|
|
66
66
|
private setupHealthEndpoints;
|
|
67
|
+
/**
|
|
68
|
+
* 解析 public_static_dir:相对路径基于配置文件目录(configDir),禁止相对路径穿越出 configDir;绝对路径按原样使用
|
|
69
|
+
*/
|
|
70
|
+
private resolvePublicStaticDirectory;
|
|
71
|
+
/**
|
|
72
|
+
* 管理端 API:当前解析后的站点根静态目录(未配置或无效时为 null)
|
|
73
|
+
*/
|
|
74
|
+
getPublicStaticRoot(): string | null;
|
|
67
75
|
get adapterConfigs(): Map<string, Account.Config[]>;
|
|
68
76
|
private initAdapters;
|
|
69
77
|
addAccount<P extends keyof Adapter.Configs>(config: Account.Config<P>): void;
|
|
@@ -71,6 +79,10 @@ export declare class BaseApp extends Koa {
|
|
|
71
79
|
removeAccount(p: string, uin: string, force?: boolean): void;
|
|
72
80
|
get accounts(): Account<string | number, any>[];
|
|
73
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;
|
|
74
86
|
start(): Promise<void>;
|
|
75
87
|
reload(config: BaseApp.Config): Promise<void>;
|
|
76
88
|
stop(): Promise<void>;
|
|
@@ -86,7 +98,11 @@ export declare namespace BaseApp {
|
|
|
86
98
|
timeout?: number;
|
|
87
99
|
username?: string;
|
|
88
100
|
password?: string;
|
|
101
|
+
/** 管理端 Bearer 鉴权码,配置后可使用 Authorization: Bearer <access_token> 访问 API,无需用户名密码 */
|
|
102
|
+
access_token?: string;
|
|
89
103
|
log_level?: LogLevel;
|
|
104
|
+
/** 站点根静态目录,相对配置文件所在目录(configDir)或绝对路径;用于企业微信等可信域名校验文件 */
|
|
105
|
+
public_static_dir?: string;
|
|
90
106
|
general?: Protocol.Configs;
|
|
91
107
|
} & KoaOptions & AdapterConfig;
|
|
92
108
|
const defaultConfig: Config;
|
package/lib/base-app.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import Koa from "koa";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import "reflect-metadata";
|
|
4
|
+
import * as fs from "fs";
|
|
4
5
|
import { writeFileSync } from "fs";
|
|
5
6
|
import log4js from "log4js";
|
|
6
7
|
import { createServer } from "http";
|
|
7
8
|
import yaml from "js-yaml";
|
|
8
9
|
import KoaBody from "koa-body";
|
|
9
|
-
import
|
|
10
|
+
import koaStatic from "koa-static";
|
|
10
11
|
const { configure, connectLogger, getLogger } = log4js;
|
|
11
12
|
import { deepClone, deepMerge } from "./utils.js";
|
|
12
13
|
import { Router } from "./router.js";
|
|
@@ -14,11 +15,15 @@ import * as path from "path";
|
|
|
14
15
|
import process from "process";
|
|
15
16
|
import { SqliteDB } from "./db.js";
|
|
16
17
|
import pkg from "../package.json" with { type: "json" };
|
|
17
|
-
import { AdapterRegistry
|
|
18
|
+
import { AdapterRegistry } from "./registry.js";
|
|
18
19
|
import { ConfigValidator, BaseAppConfigSchema } from "./config-validator.js";
|
|
19
20
|
import { LifecycleManager } from "./lifecycle.js";
|
|
20
21
|
import { ErrorHandler, ConfigError } from "./errors.js";
|
|
21
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";
|
|
22
27
|
export { configure, yaml, connectLogger };
|
|
23
28
|
export class BaseApp extends Koa {
|
|
24
29
|
config;
|
|
@@ -102,24 +107,33 @@ export class BaseApp extends Koa {
|
|
|
102
107
|
this.httpServer.close(() => resolve());
|
|
103
108
|
});
|
|
104
109
|
});
|
|
110
|
+
// 初始化安全审计日志
|
|
111
|
+
initSecurityAudit(path.join(BaseApp.dataDir, 'audit'));
|
|
105
112
|
// 注册健康检查端点(无需认证)
|
|
106
113
|
this.setupHealthEndpoints();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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();
|
|
123
137
|
})
|
|
124
138
|
.use(this.router.routes())
|
|
125
139
|
.use(this.router.allowedMethods());
|
|
@@ -189,33 +203,33 @@ export class BaseApp extends Koa {
|
|
|
189
203
|
},
|
|
190
204
|
};
|
|
191
205
|
});
|
|
192
|
-
// /metrics -
|
|
206
|
+
// /metrics - Prometheus 格式指标
|
|
193
207
|
this.router.get('/metrics', (ctx) => {
|
|
194
|
-
const
|
|
195
|
-
const now = Date.now();
|
|
208
|
+
const metricLines = [];
|
|
196
209
|
// 基础指标
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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}`);
|
|
206
219
|
// 内存使用
|
|
207
220
|
const memUsage = process.memoryUsage();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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}`);
|
|
213
227
|
// 适配器和账号指标
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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`);
|
|
219
233
|
for (const [platform, adapter] of this.adapters) {
|
|
220
234
|
let online = 0;
|
|
221
235
|
let offline = 0;
|
|
@@ -225,13 +239,69 @@ export class BaseApp extends Koa {
|
|
|
225
239
|
else
|
|
226
240
|
offline++;
|
|
227
241
|
}
|
|
228
|
-
|
|
229
|
-
|
|
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);
|
|
230
250
|
}
|
|
231
251
|
ctx.type = 'text/plain; charset=utf-8';
|
|
232
|
-
ctx.body =
|
|
252
|
+
ctx.body = metricLines.join('\n') + '\n';
|
|
233
253
|
});
|
|
234
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
|
+
}
|
|
235
305
|
get adapterConfigs() {
|
|
236
306
|
const map = new Map();
|
|
237
307
|
Object.keys(this.config).forEach(key => {
|
|
@@ -311,8 +381,13 @@ export class BaseApp extends Koa {
|
|
|
311
381
|
return this.adapters.get(platform);
|
|
312
382
|
const adapter = AdapterRegistry.create(`${platform}`, this);
|
|
313
383
|
this.adapters.set(platform, adapter);
|
|
384
|
+
this.onAdapterCreated(adapter);
|
|
314
385
|
return adapter;
|
|
315
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* 适配器首次创建后的钩子,子类可覆写以订阅该适配器事件(如 verification:request)
|
|
389
|
+
*/
|
|
390
|
+
onAdapterCreated(_adapter) { }
|
|
316
391
|
async start() {
|
|
317
392
|
const stopTimer = this.enhancedLogger.start('Application start');
|
|
318
393
|
try {
|
|
@@ -371,6 +446,8 @@ export class BaseApp extends Koa {
|
|
|
371
446
|
this.adapters.clear();
|
|
372
447
|
// 清理资源
|
|
373
448
|
await this.lifecycle.cleanup();
|
|
449
|
+
// 关闭安全审计日志
|
|
450
|
+
closeSecurityAudit();
|
|
374
451
|
this.emit("close");
|
|
375
452
|
this.isStarted = false;
|
|
376
453
|
stopTimer();
|
|
@@ -387,8 +464,6 @@ export class BaseApp extends Koa {
|
|
|
387
464
|
BaseApp.defaultConfig = {
|
|
388
465
|
port: 6727,
|
|
389
466
|
database: "onebots.db",
|
|
390
|
-
username: "admin",
|
|
391
|
-
password: "123456",
|
|
392
467
|
timeout: 30,
|
|
393
468
|
general: {},
|
|
394
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;
|