@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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 访问令牌验证中间件
|
|
3
|
+
* 支持多种令牌格式和验证方式
|
|
4
|
+
*/
|
|
5
|
+
import { createLogger } from '../logger.js';
|
|
6
|
+
import { logInvalidToken, logAuthFailure } from './security-audit.js';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
const logger = createLogger('TokenValidator');
|
|
9
|
+
/**
|
|
10
|
+
* 创建令牌验证中间件
|
|
11
|
+
*/
|
|
12
|
+
export function createTokenValidator(config = {}) {
|
|
13
|
+
const { tokenName = 'access_token', fromHeader = true, fromQuery = true, validator, required = true, errorMessage = 'Invalid or missing access token', } = config;
|
|
14
|
+
return async (ctx, next) => {
|
|
15
|
+
// 从多个位置提取令牌
|
|
16
|
+
let token;
|
|
17
|
+
if (fromHeader) {
|
|
18
|
+
// 从 Authorization header 获取
|
|
19
|
+
const authHeader = ctx.get('authorization');
|
|
20
|
+
if (authHeader) {
|
|
21
|
+
// 支持 Bearer token 格式
|
|
22
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
23
|
+
if (match) {
|
|
24
|
+
token = match[1];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
token = authHeader;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!token && fromQuery) {
|
|
32
|
+
// 从 query 参数获取
|
|
33
|
+
token = ctx.query[tokenName];
|
|
34
|
+
}
|
|
35
|
+
// 检查令牌是否存在
|
|
36
|
+
if (!token) {
|
|
37
|
+
if (required) {
|
|
38
|
+
logInvalidToken(ctx);
|
|
39
|
+
ctx.status = 401;
|
|
40
|
+
ctx.body = {
|
|
41
|
+
error: 'Unauthorized',
|
|
42
|
+
message: errorMessage,
|
|
43
|
+
};
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// 可选令牌,继续执行
|
|
48
|
+
return next();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 验证令牌
|
|
52
|
+
let isValid = true;
|
|
53
|
+
if (validator) {
|
|
54
|
+
try {
|
|
55
|
+
isValid = await validator(token, ctx);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
logger.error('Token validation error', {
|
|
59
|
+
error: error.message,
|
|
60
|
+
path: ctx.path,
|
|
61
|
+
});
|
|
62
|
+
isValid = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// 默认验证:检查令牌不为空
|
|
67
|
+
isValid = token.length > 0;
|
|
68
|
+
}
|
|
69
|
+
if (!isValid) {
|
|
70
|
+
logInvalidToken(ctx, token);
|
|
71
|
+
ctx.status = 401;
|
|
72
|
+
ctx.body = {
|
|
73
|
+
error: 'Unauthorized',
|
|
74
|
+
message: errorMessage,
|
|
75
|
+
};
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// 将令牌存储到 context 中,供后续使用
|
|
79
|
+
ctx.state.token = token;
|
|
80
|
+
await next();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 创建基于配置的令牌验证器
|
|
85
|
+
* 从配置中读取 access_token 列表进行验证
|
|
86
|
+
*/
|
|
87
|
+
export function createConfigTokenValidator(expectedTokens) {
|
|
88
|
+
const tokenSet = new Set(expectedTokens.filter(Boolean));
|
|
89
|
+
return createTokenValidator({
|
|
90
|
+
validator: (token) => {
|
|
91
|
+
return tokenSet.has(token);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* HMAC 签名验证
|
|
97
|
+
*/
|
|
98
|
+
export function createHMACValidator(secret, algorithm = 'sha256') {
|
|
99
|
+
return async (ctx, next) => {
|
|
100
|
+
const signature = ctx.get('x-signature') || ctx.query.signature;
|
|
101
|
+
if (!signature) {
|
|
102
|
+
logAuthFailure(ctx, 'Missing signature');
|
|
103
|
+
ctx.status = 401;
|
|
104
|
+
ctx.body = {
|
|
105
|
+
error: 'Unauthorized',
|
|
106
|
+
message: 'Missing signature',
|
|
107
|
+
};
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// 获取请求体
|
|
111
|
+
const body = ctx.request.body || ctx.request.rawBody || '';
|
|
112
|
+
const bodyString = typeof body === 'string' ? body : JSON.stringify(body);
|
|
113
|
+
// 计算 HMAC
|
|
114
|
+
const hmac = crypto.createHmac(algorithm, secret);
|
|
115
|
+
hmac.update(bodyString);
|
|
116
|
+
const expectedSignature = hmac.digest('hex');
|
|
117
|
+
// 使用时间安全比较
|
|
118
|
+
const isValid = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
|
|
119
|
+
if (!isValid) {
|
|
120
|
+
logAuthFailure(ctx, 'Invalid signature');
|
|
121
|
+
ctx.status = 401;
|
|
122
|
+
ctx.body = {
|
|
123
|
+
error: 'Unauthorized',
|
|
124
|
+
message: 'Invalid signature',
|
|
125
|
+
};
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await next();
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 创建带令牌管理的验证器
|
|
133
|
+
* 支持令牌过期检查和自动刷新
|
|
134
|
+
*/
|
|
135
|
+
export function createManagedTokenValidator(tokenManager, config = {}) {
|
|
136
|
+
const baseValidator = createTokenValidator(config);
|
|
137
|
+
return async (ctx, next) => {
|
|
138
|
+
const token = ctx.get('authorization')?.replace(/^Bearer\s+/i, '') ||
|
|
139
|
+
ctx.query[config.tokenName || 'access_token'];
|
|
140
|
+
if (!token) {
|
|
141
|
+
if (config.required !== false) {
|
|
142
|
+
logInvalidToken(ctx);
|
|
143
|
+
ctx.status = 401;
|
|
144
|
+
ctx.body = {
|
|
145
|
+
error: 'Unauthorized',
|
|
146
|
+
message: config.errorMessage || 'Invalid or missing access token',
|
|
147
|
+
};
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
return next();
|
|
151
|
+
}
|
|
152
|
+
// 验证令牌
|
|
153
|
+
const validation = tokenManager.validateToken(token);
|
|
154
|
+
if (!validation.valid) {
|
|
155
|
+
if (validation.expired) {
|
|
156
|
+
logInvalidToken(ctx, token);
|
|
157
|
+
ctx.status = 401;
|
|
158
|
+
ctx.body = {
|
|
159
|
+
error: 'Unauthorized',
|
|
160
|
+
message: 'Token expired',
|
|
161
|
+
code: 'TOKEN_EXPIRED',
|
|
162
|
+
};
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
logInvalidToken(ctx, token);
|
|
167
|
+
ctx.status = 401;
|
|
168
|
+
ctx.body = {
|
|
169
|
+
error: 'Unauthorized',
|
|
170
|
+
message: config.errorMessage || 'Invalid access token',
|
|
171
|
+
};
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 将令牌信息存储到 context
|
|
176
|
+
ctx.state.token = token;
|
|
177
|
+
ctx.state.tokenInfo = validation.info;
|
|
178
|
+
await next();
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 组合多个验证器
|
|
183
|
+
*/
|
|
184
|
+
export function combineValidators(...validators) {
|
|
185
|
+
return async (ctx, next) => {
|
|
186
|
+
for (const validator of validators) {
|
|
187
|
+
await validator(ctx, async () => {
|
|
188
|
+
// 空函数,用于链式调用
|
|
189
|
+
});
|
|
190
|
+
// 如果验证失败,响应已设置,直接返回
|
|
191
|
+
if (ctx.status === 401) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// 所有验证通过,继续执行
|
|
196
|
+
await next();
|
|
197
|
+
};
|
|
198
|
+
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { Account } from "./account.js";
|
|
|
3
3
|
import { Adapter } from "./adapter.js";
|
|
4
4
|
import { Dict } from "./types.js";
|
|
5
5
|
import { Router } from "./router.js";
|
|
6
|
-
import { BaseApp } from "./base-app.js";
|
|
7
6
|
/**
|
|
8
7
|
* Base Protocol class
|
|
9
8
|
* Represents a communication protocol (e.g., OneBot, Milky, Satori)
|
|
@@ -14,7 +13,7 @@ export declare abstract class Protocol<V extends string = string, C = any> exten
|
|
|
14
13
|
config: Protocol.FullConfig<C>;
|
|
15
14
|
abstract readonly name: string;
|
|
16
15
|
abstract readonly version: V;
|
|
17
|
-
get app(): BaseApp;
|
|
16
|
+
get app(): import("./base-app.js").BaseApp;
|
|
18
17
|
get router(): Router;
|
|
19
18
|
get logger(): import("log4js").Logger;
|
|
20
19
|
constructor(adapter: Adapter, account: Account, config: Protocol.FullConfig<C>);
|
|
@@ -95,6 +94,6 @@ export declare namespace Protocol {
|
|
|
95
94
|
*/
|
|
96
95
|
export type Factory<T extends Protocol = Protocol> = Creator<T> | Construct<T>;
|
|
97
96
|
export function isClassFactory<T extends Protocol = Protocol>(factory: Factory<T>): factory is Construct<T>;
|
|
98
|
-
export function createFilter(filters
|
|
97
|
+
export function createFilter(filters?: Filters): any;
|
|
99
98
|
export {};
|
|
100
99
|
}
|
package/lib/protocol.js
CHANGED
|
@@ -41,6 +41,10 @@ export class Protocol extends EventEmitter {
|
|
|
41
41
|
}
|
|
42
42
|
Protocol.isClassFactory = isClassFactory;
|
|
43
43
|
function createFilter(filters) {
|
|
44
|
+
// 如果没有 filters,返回始终为 true 的过滤器
|
|
45
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
46
|
+
return () => true;
|
|
47
|
+
}
|
|
44
48
|
const isLogicKey = (key) => {
|
|
45
49
|
return [
|
|
46
50
|
"$and",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 速率限制器
|
|
3
|
+
* 防止触发平台的速率限制
|
|
4
|
+
*/
|
|
5
|
+
export interface RateLimiterOptions {
|
|
6
|
+
/** 时间窗口内允许的最大请求数 */
|
|
7
|
+
maxRequests: number;
|
|
8
|
+
/** 时间窗口大小(毫秒) */
|
|
9
|
+
windowMs: number;
|
|
10
|
+
/** 请求之间的最小间隔(毫秒) */
|
|
11
|
+
minInterval?: number;
|
|
12
|
+
/** 超出限制时是否排队等待 */
|
|
13
|
+
queueExcess?: boolean;
|
|
14
|
+
/** 队列最大长度 */
|
|
15
|
+
maxQueueSize?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 令牌桶速率限制器
|
|
19
|
+
*/
|
|
20
|
+
export declare class RateLimiter {
|
|
21
|
+
private tokens;
|
|
22
|
+
private lastRefill;
|
|
23
|
+
private queue;
|
|
24
|
+
private processing;
|
|
25
|
+
private lastRequest;
|
|
26
|
+
private readonly maxRequests;
|
|
27
|
+
private readonly windowMs;
|
|
28
|
+
private readonly minInterval;
|
|
29
|
+
private readonly queueExcess;
|
|
30
|
+
private readonly maxQueueSize;
|
|
31
|
+
constructor(options: RateLimiterOptions);
|
|
32
|
+
/**
|
|
33
|
+
* 补充令牌
|
|
34
|
+
*/
|
|
35
|
+
private refillTokens;
|
|
36
|
+
/**
|
|
37
|
+
* 尝试获取令牌
|
|
38
|
+
*/
|
|
39
|
+
private tryAcquire;
|
|
40
|
+
/**
|
|
41
|
+
* 计算需要等待的时间
|
|
42
|
+
*/
|
|
43
|
+
private getWaitTime;
|
|
44
|
+
/**
|
|
45
|
+
* 执行受限函数
|
|
46
|
+
*/
|
|
47
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
48
|
+
/**
|
|
49
|
+
* 处理队列
|
|
50
|
+
*/
|
|
51
|
+
private processQueue;
|
|
52
|
+
/**
|
|
53
|
+
* 获取当前状态
|
|
54
|
+
*/
|
|
55
|
+
getStatus(): {
|
|
56
|
+
availableTokens: number;
|
|
57
|
+
queueLength: number;
|
|
58
|
+
isProcessing: boolean;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* 清空队列
|
|
62
|
+
*/
|
|
63
|
+
clearQueue(): void;
|
|
64
|
+
private sleep;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 速率限制错误
|
|
68
|
+
*/
|
|
69
|
+
export declare class RateLimitError extends Error {
|
|
70
|
+
constructor(message: string);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 平台速率限制预设
|
|
74
|
+
*/
|
|
75
|
+
export declare const RateLimitPresets: {
|
|
76
|
+
/** Discord - 50 请求/秒 */
|
|
77
|
+
discord: RateLimiterOptions;
|
|
78
|
+
/** Telegram - 30 消息/秒(群组 20/分钟) */
|
|
79
|
+
telegram: RateLimiterOptions;
|
|
80
|
+
/** QQ - 5 消息/秒 */
|
|
81
|
+
qq: RateLimiterOptions;
|
|
82
|
+
/** 通用 - 10 请求/秒 */
|
|
83
|
+
default: RateLimiterOptions;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 创建带速率限制的函数包装器
|
|
87
|
+
*/
|
|
88
|
+
export declare function withRateLimit<T extends (...args: any[]) => Promise<any>>(fn: T, limiter: RateLimiter): T;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 速率限制器
|
|
3
|
+
* 防止触发平台的速率限制
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 令牌桶速率限制器
|
|
7
|
+
*/
|
|
8
|
+
export class RateLimiter {
|
|
9
|
+
tokens;
|
|
10
|
+
lastRefill;
|
|
11
|
+
queue = [];
|
|
12
|
+
processing = false;
|
|
13
|
+
lastRequest = 0;
|
|
14
|
+
maxRequests;
|
|
15
|
+
windowMs;
|
|
16
|
+
minInterval;
|
|
17
|
+
queueExcess;
|
|
18
|
+
maxQueueSize;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.maxRequests = options.maxRequests;
|
|
21
|
+
this.windowMs = options.windowMs;
|
|
22
|
+
this.minInterval = options.minInterval ?? 0;
|
|
23
|
+
this.queueExcess = options.queueExcess ?? true;
|
|
24
|
+
this.maxQueueSize = options.maxQueueSize ?? 100;
|
|
25
|
+
this.tokens = this.maxRequests;
|
|
26
|
+
this.lastRefill = Date.now();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 补充令牌
|
|
30
|
+
*/
|
|
31
|
+
refillTokens() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const elapsed = now - this.lastRefill;
|
|
34
|
+
const tokensToAdd = (elapsed / this.windowMs) * this.maxRequests;
|
|
35
|
+
this.tokens = Math.min(this.maxRequests, this.tokens + tokensToAdd);
|
|
36
|
+
this.lastRefill = now;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 尝试获取令牌
|
|
40
|
+
*/
|
|
41
|
+
tryAcquire() {
|
|
42
|
+
this.refillTokens();
|
|
43
|
+
if (this.tokens >= 1) {
|
|
44
|
+
this.tokens -= 1;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 计算需要等待的时间
|
|
51
|
+
*/
|
|
52
|
+
getWaitTime() {
|
|
53
|
+
this.refillTokens();
|
|
54
|
+
if (this.tokens >= 1) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
// 计算获得一个令牌需要的时间
|
|
58
|
+
const tokensNeeded = 1 - this.tokens;
|
|
59
|
+
return (tokensNeeded / this.maxRequests) * this.windowMs;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 执行受限函数
|
|
63
|
+
*/
|
|
64
|
+
async execute(fn) {
|
|
65
|
+
// 检查最小间隔
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const timeSinceLastRequest = now - this.lastRequest;
|
|
68
|
+
if (timeSinceLastRequest < this.minInterval) {
|
|
69
|
+
await this.sleep(this.minInterval - timeSinceLastRequest);
|
|
70
|
+
}
|
|
71
|
+
// 尝试获取令牌
|
|
72
|
+
if (this.tryAcquire()) {
|
|
73
|
+
this.lastRequest = Date.now();
|
|
74
|
+
return fn();
|
|
75
|
+
}
|
|
76
|
+
// 如果不排队,直接抛出错误
|
|
77
|
+
if (!this.queueExcess) {
|
|
78
|
+
throw new RateLimitError('Rate limit exceeded');
|
|
79
|
+
}
|
|
80
|
+
// 检查队列大小
|
|
81
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
82
|
+
throw new RateLimitError('Rate limit queue is full');
|
|
83
|
+
}
|
|
84
|
+
// 加入队列
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
this.queue.push({ execute: fn, resolve, reject });
|
|
87
|
+
this.processQueue();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 处理队列
|
|
92
|
+
*/
|
|
93
|
+
async processQueue() {
|
|
94
|
+
if (this.processing || this.queue.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.processing = true;
|
|
98
|
+
while (this.queue.length > 0) {
|
|
99
|
+
const waitTime = this.getWaitTime();
|
|
100
|
+
if (waitTime > 0) {
|
|
101
|
+
await this.sleep(waitTime);
|
|
102
|
+
}
|
|
103
|
+
// 检查最小间隔
|
|
104
|
+
const timeSinceLastRequest = Date.now() - this.lastRequest;
|
|
105
|
+
if (timeSinceLastRequest < this.minInterval) {
|
|
106
|
+
await this.sleep(this.minInterval - timeSinceLastRequest);
|
|
107
|
+
}
|
|
108
|
+
if (this.tryAcquire()) {
|
|
109
|
+
const request = this.queue.shift();
|
|
110
|
+
if (request) {
|
|
111
|
+
this.lastRequest = Date.now();
|
|
112
|
+
try {
|
|
113
|
+
const result = await request.execute();
|
|
114
|
+
request.resolve(result);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.processing = false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 获取当前状态
|
|
126
|
+
*/
|
|
127
|
+
getStatus() {
|
|
128
|
+
this.refillTokens();
|
|
129
|
+
return {
|
|
130
|
+
availableTokens: Math.floor(this.tokens),
|
|
131
|
+
queueLength: this.queue.length,
|
|
132
|
+
isProcessing: this.processing,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 清空队列
|
|
137
|
+
*/
|
|
138
|
+
clearQueue() {
|
|
139
|
+
const error = new RateLimitError('Queue cleared');
|
|
140
|
+
this.queue.forEach(request => request.reject(error));
|
|
141
|
+
this.queue = [];
|
|
142
|
+
}
|
|
143
|
+
sleep(ms) {
|
|
144
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 速率限制错误
|
|
149
|
+
*/
|
|
150
|
+
export class RateLimitError extends Error {
|
|
151
|
+
constructor(message) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.name = 'RateLimitError';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 平台速率限制预设
|
|
158
|
+
*/
|
|
159
|
+
export const RateLimitPresets = {
|
|
160
|
+
/** Discord - 50 请求/秒 */
|
|
161
|
+
discord: {
|
|
162
|
+
maxRequests: 50,
|
|
163
|
+
windowMs: 1000,
|
|
164
|
+
minInterval: 50,
|
|
165
|
+
queueExcess: true,
|
|
166
|
+
},
|
|
167
|
+
/** Telegram - 30 消息/秒(群组 20/分钟) */
|
|
168
|
+
telegram: {
|
|
169
|
+
maxRequests: 30,
|
|
170
|
+
windowMs: 1000,
|
|
171
|
+
minInterval: 50,
|
|
172
|
+
queueExcess: true,
|
|
173
|
+
},
|
|
174
|
+
/** QQ - 5 消息/秒 */
|
|
175
|
+
qq: {
|
|
176
|
+
maxRequests: 5,
|
|
177
|
+
windowMs: 1000,
|
|
178
|
+
minInterval: 200,
|
|
179
|
+
queueExcess: true,
|
|
180
|
+
},
|
|
181
|
+
/** 通用 - 10 请求/秒 */
|
|
182
|
+
default: {
|
|
183
|
+
maxRequests: 10,
|
|
184
|
+
windowMs: 1000,
|
|
185
|
+
minInterval: 100,
|
|
186
|
+
queueExcess: true,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* 创建带速率限制的函数包装器
|
|
191
|
+
*/
|
|
192
|
+
export function withRateLimit(fn, limiter) {
|
|
193
|
+
return ((...args) => {
|
|
194
|
+
return limiter.execute(() => fn(...args));
|
|
195
|
+
});
|
|
196
|
+
}
|
package/lib/registry.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Protocol } from "./protocol.js";
|
|
|
2
2
|
import { Adapter } from "./adapter.js";
|
|
3
3
|
import { BaseApp } from "./base-app.js";
|
|
4
4
|
import { Account } from "./account.js";
|
|
5
|
+
import type { Schema } from "./config-validator.js";
|
|
5
6
|
/**
|
|
6
7
|
* Protocol Registry
|
|
7
8
|
* Manages registration and retrieval of protocol implementations
|
|
@@ -9,6 +10,7 @@ import { Account } from "./account.js";
|
|
|
9
10
|
export declare class ProtocolRegistry {
|
|
10
11
|
private static protocols;
|
|
11
12
|
private static metadata;
|
|
13
|
+
private static schemas;
|
|
12
14
|
/**
|
|
13
15
|
* Register a protocol implementation
|
|
14
16
|
* @param name Protocol name (e.g., 'onebot', 'milky', 'satori')
|
|
@@ -17,6 +19,18 @@ export declare class ProtocolRegistry {
|
|
|
17
19
|
* @param metadata Optional protocol metadata
|
|
18
20
|
*/
|
|
19
21
|
static register(name: string, version: string, factory: Protocol.Factory, metadata?: Partial<Protocol.Metadata>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Register a protocol config schema (key format: name.version)
|
|
24
|
+
*/
|
|
25
|
+
static registerSchema(key: string, schema: Schema): void;
|
|
26
|
+
/**
|
|
27
|
+
* Get a protocol config schema by key
|
|
28
|
+
*/
|
|
29
|
+
static getSchema(key: string): Schema | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Get all protocol config schemas
|
|
32
|
+
*/
|
|
33
|
+
static getAllSchemas(): Record<string, Schema>;
|
|
20
34
|
/**
|
|
21
35
|
* Get a protocol factory
|
|
22
36
|
* @param name Protocol name
|
|
@@ -63,6 +77,7 @@ export declare class ProtocolRegistry {
|
|
|
63
77
|
export declare class AdapterRegistry {
|
|
64
78
|
private static adapters;
|
|
65
79
|
private static metadata;
|
|
80
|
+
private static schemas;
|
|
66
81
|
/**
|
|
67
82
|
* Register an adapter implementation
|
|
68
83
|
* @param name Adapter name/platform (e.g., 'wechat', 'dingtalk', 'qq')
|
|
@@ -70,6 +85,18 @@ export declare class AdapterRegistry {
|
|
|
70
85
|
* @param metadata Optional adapter metadata
|
|
71
86
|
*/
|
|
72
87
|
static register(name: string, factory: Adapter.Factory, metadata?: Partial<Adapter.Metadata>): void;
|
|
88
|
+
/**
|
|
89
|
+
* Register an adapter config schema
|
|
90
|
+
*/
|
|
91
|
+
static registerSchema(name: string, schema: Schema): void;
|
|
92
|
+
/**
|
|
93
|
+
* Get an adapter config schema
|
|
94
|
+
*/
|
|
95
|
+
static getSchema(name: string): Schema | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Get all adapter config schemas
|
|
98
|
+
*/
|
|
99
|
+
static getAllSchemas(): Record<string, Schema>;
|
|
73
100
|
/**
|
|
74
101
|
* Get an adapter factory
|
|
75
102
|
* @param name Adapter name/platform
|
package/lib/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Adapter } from "./adapter.js";
|
|
|
7
7
|
export class ProtocolRegistry {
|
|
8
8
|
static protocols = new Map();
|
|
9
9
|
static metadata = new Map();
|
|
10
|
+
static schemas = new Map();
|
|
10
11
|
/**
|
|
11
12
|
* Register a protocol implementation
|
|
12
13
|
* @param name Protocol name (e.g., 'onebot', 'milky', 'satori')
|
|
@@ -36,6 +37,24 @@ export class ProtocolRegistry {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Register a protocol config schema (key format: name.version)
|
|
42
|
+
*/
|
|
43
|
+
static registerSchema(key, schema) {
|
|
44
|
+
this.schemas.set(key, schema);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get a protocol config schema by key
|
|
48
|
+
*/
|
|
49
|
+
static getSchema(key) {
|
|
50
|
+
return this.schemas.get(key);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all protocol config schemas
|
|
54
|
+
*/
|
|
55
|
+
static getAllSchemas() {
|
|
56
|
+
return Object.fromEntries(this.schemas.entries());
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Get a protocol factory
|
|
41
60
|
* @param name Protocol name
|
|
@@ -83,11 +102,6 @@ export class ProtocolRegistry {
|
|
|
83
102
|
*/
|
|
84
103
|
static create(name, version, adapter, account, config) {
|
|
85
104
|
const factory = this.get(name, version);
|
|
86
|
-
console.log({
|
|
87
|
-
name,
|
|
88
|
-
version,
|
|
89
|
-
account: account.account_id
|
|
90
|
-
});
|
|
91
105
|
if (!factory) {
|
|
92
106
|
throw new Error(`Protocol ${name}/${version} not registered`);
|
|
93
107
|
}
|
|
@@ -127,6 +141,7 @@ export class ProtocolRegistry {
|
|
|
127
141
|
static clear() {
|
|
128
142
|
this.protocols.clear();
|
|
129
143
|
this.metadata.clear();
|
|
144
|
+
this.schemas.clear();
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
/**
|
|
@@ -136,6 +151,7 @@ export class ProtocolRegistry {
|
|
|
136
151
|
export class AdapterRegistry {
|
|
137
152
|
static adapters = new Map();
|
|
138
153
|
static metadata = new Map();
|
|
154
|
+
static schemas = new Map();
|
|
139
155
|
/**
|
|
140
156
|
* Register an adapter implementation
|
|
141
157
|
* @param name Adapter name/platform (e.g., 'wechat', 'dingtalk', 'qq')
|
|
@@ -156,6 +172,24 @@ export class AdapterRegistry {
|
|
|
156
172
|
});
|
|
157
173
|
}
|
|
158
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Register an adapter config schema
|
|
177
|
+
*/
|
|
178
|
+
static registerSchema(name, schema) {
|
|
179
|
+
this.schemas.set(name, schema);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get an adapter config schema
|
|
183
|
+
*/
|
|
184
|
+
static getSchema(name) {
|
|
185
|
+
return this.schemas.get(name);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get all adapter config schemas
|
|
189
|
+
*/
|
|
190
|
+
static getAllSchemas() {
|
|
191
|
+
return Object.fromEntries(this.schemas.entries());
|
|
192
|
+
}
|
|
159
193
|
/**
|
|
160
194
|
* Get an adapter factory
|
|
161
195
|
* @param name Adapter name/platform
|
|
@@ -213,5 +247,6 @@ export class AdapterRegistry {
|
|
|
213
247
|
static clear() {
|
|
214
248
|
this.adapters.clear();
|
|
215
249
|
this.metadata.clear();
|
|
250
|
+
this.schemas.clear();
|
|
216
251
|
}
|
|
217
252
|
}
|