cfix 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/.env.example +69 -0
- package/README.md +1590 -0
- package/bin/cfix +14 -0
- package/bin/cfix.cmd +6 -0
- package/cli/commands/config.js +58 -0
- package/cli/commands/doctor.js +240 -0
- package/cli/commands/fix.js +211 -0
- package/cli/commands/help.js +62 -0
- package/cli/commands/init.js +226 -0
- package/cli/commands/logs.js +161 -0
- package/cli/commands/monitor.js +151 -0
- package/cli/commands/project.js +331 -0
- package/cli/commands/service.js +133 -0
- package/cli/commands/status.js +115 -0
- package/cli/commands/task.js +412 -0
- package/cli/commands/version.js +19 -0
- package/cli/index.js +269 -0
- package/cli/lib/config-manager.js +612 -0
- package/cli/lib/formatter.js +224 -0
- package/cli/lib/process-manager.js +233 -0
- package/cli/lib/service-client.js +271 -0
- package/cli/scripts/install-completion.js +133 -0
- package/package.json +85 -0
- package/public/monitor.html +1096 -0
- package/scripts/completion.bash +87 -0
- package/scripts/completion.zsh +102 -0
- package/src/assets/README.md +32 -0
- package/src/assets/error.png +0 -0
- package/src/assets/icon.png +0 -0
- package/src/assets/success.png +0 -0
- package/src/claude-cli-service.js +216 -0
- package/src/config/index.js +69 -0
- package/src/database/manager.js +391 -0
- package/src/database/migration.js +252 -0
- package/src/git-service.js +1278 -0
- package/src/index.js +1658 -0
- package/src/logger.js +139 -0
- package/src/metrics/collector.js +184 -0
- package/src/middleware/auth.js +86 -0
- package/src/middleware/rate-limit.js +85 -0
- package/src/queue/integration-example.js +283 -0
- package/src/queue/task-queue.js +333 -0
- package/src/services/notification-limiter.js +48 -0
- package/src/services/notification-service.js +115 -0
- package/src/services/system-notifier.js +130 -0
- package/src/task-manager.js +289 -0
- package/src/utils/exec.js +87 -0
- package/src/utils/project-lock.js +246 -0
- package/src/utils/retry.js +110 -0
- package/src/utils/sanitizer.js +174 -0
- package/src/websocket/notifier.js +363 -0
- package/src/wechat-notifier.js +97 -0
package/src/logger.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const winston = require("winston");
|
|
2
|
+
const DailyRotateFile = require("winston-daily-rotate-file");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
// 日志目录
|
|
7
|
+
const logDir = path.join(__dirname, "../logs");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 生成请求ID
|
|
11
|
+
*/
|
|
12
|
+
function generateRequestId() {
|
|
13
|
+
return crypto.randomBytes(16).toString('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 结构化日志格式
|
|
17
|
+
const logFormat = winston.format.combine(
|
|
18
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
19
|
+
winston.format.errors({ stack: true }),
|
|
20
|
+
winston.format.printf(({ timestamp, level, message, requestId, taskId, ...meta }) => {
|
|
21
|
+
// 开发环境:易读格式
|
|
22
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
23
|
+
let log = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
24
|
+
|
|
25
|
+
if (requestId) {
|
|
26
|
+
log += ` [${requestId.substring(0, 8)}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (taskId) {
|
|
30
|
+
log += ` [Task: ${taskId}]`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
log += `: ${message}`;
|
|
34
|
+
|
|
35
|
+
// 添加其他元数据
|
|
36
|
+
const otherMeta = { ...meta };
|
|
37
|
+
delete otherMeta.requestId;
|
|
38
|
+
delete otherMeta.taskId;
|
|
39
|
+
|
|
40
|
+
if (Object.keys(otherMeta).length > 0) {
|
|
41
|
+
log += ` ${JSON.stringify(otherMeta)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return log;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 生产环境:JSON 格式
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
timestamp,
|
|
50
|
+
level: level.toUpperCase(),
|
|
51
|
+
message,
|
|
52
|
+
requestId,
|
|
53
|
+
taskId,
|
|
54
|
+
...meta,
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 创建 logger 实例
|
|
60
|
+
const logger = winston.createLogger({
|
|
61
|
+
level: process.env.LOG_LEVEL || "info",
|
|
62
|
+
format: logFormat,
|
|
63
|
+
transports: [
|
|
64
|
+
// 控制台输出(开发模式)
|
|
65
|
+
new winston.transports.Console({
|
|
66
|
+
format: winston.format.combine(
|
|
67
|
+
winston.format.colorize(),
|
|
68
|
+
logFormat
|
|
69
|
+
),
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
// 所有日志 - 按天轮转
|
|
73
|
+
new DailyRotateFile({
|
|
74
|
+
dirname: logDir,
|
|
75
|
+
filename: "combined-%DATE%.log",
|
|
76
|
+
datePattern: "YYYY-MM-DD",
|
|
77
|
+
maxSize: "20m",
|
|
78
|
+
maxFiles: "14d", // 保留14天
|
|
79
|
+
format: logFormat,
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// 错误日志 - 单独文件
|
|
83
|
+
new DailyRotateFile({
|
|
84
|
+
dirname: logDir,
|
|
85
|
+
filename: "error-%DATE%.log",
|
|
86
|
+
datePattern: "YYYY-MM-DD",
|
|
87
|
+
level: "error",
|
|
88
|
+
maxSize: "20m",
|
|
89
|
+
maxFiles: "30d", // 错误日志保留30天
|
|
90
|
+
format: logFormat,
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 如果是生产环境,移除控制台输出
|
|
96
|
+
if (process.env.NODE_ENV === "production") {
|
|
97
|
+
logger.remove(logger.transports[0]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 导出便捷方法
|
|
101
|
+
module.exports = {
|
|
102
|
+
logger,
|
|
103
|
+
|
|
104
|
+
// 生成请求ID
|
|
105
|
+
generateRequestId,
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 创建带上下文的 logger
|
|
109
|
+
*/
|
|
110
|
+
createContextLogger(context) {
|
|
111
|
+
return {
|
|
112
|
+
debug: (message, meta = {}) => logger.debug(message, { ...context, ...meta }),
|
|
113
|
+
info: (message, meta = {}) => logger.info(message, { ...context, ...meta }),
|
|
114
|
+
warn: (message, meta = {}) => logger.warn(message, { ...context, ...meta }),
|
|
115
|
+
error: (message, meta = {}) => logger.error(message, { ...context, ...meta }),
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// 带任务ID的日志方法
|
|
120
|
+
logWithTask(taskId, level, message, meta = {}) {
|
|
121
|
+
logger.log(level, message, { taskId, ...meta });
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
info(message, meta = {}) {
|
|
125
|
+
logger.info(message, meta);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
error(message, meta = {}) {
|
|
129
|
+
logger.error(message, meta);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
warn(message, meta = {}) {
|
|
133
|
+
logger.warn(message, meta);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
debug(message, meta = {}) {
|
|
137
|
+
logger.debug(message, meta);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const { logger } = require('../logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 指标收集器
|
|
5
|
+
*/
|
|
6
|
+
class MetricsCollector {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.metrics = {
|
|
9
|
+
tasks: {
|
|
10
|
+
total: 0,
|
|
11
|
+
completed: 0,
|
|
12
|
+
failed: 0,
|
|
13
|
+
cancelled: 0,
|
|
14
|
+
},
|
|
15
|
+
durations: [],
|
|
16
|
+
aiCalls: {
|
|
17
|
+
success: 0,
|
|
18
|
+
failure: 0,
|
|
19
|
+
totalTime: 0,
|
|
20
|
+
},
|
|
21
|
+
git: {
|
|
22
|
+
pushSuccess: 0,
|
|
23
|
+
pushFailure: 0,
|
|
24
|
+
mergeSuccess: 0,
|
|
25
|
+
mergeFailure: 0,
|
|
26
|
+
},
|
|
27
|
+
http: {
|
|
28
|
+
requests: 0,
|
|
29
|
+
errors: 0,
|
|
30
|
+
responseTime: [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 记录任务完成
|
|
37
|
+
*/
|
|
38
|
+
recordTaskComplete(duration, success) {
|
|
39
|
+
this.metrics.tasks.total++;
|
|
40
|
+
|
|
41
|
+
if (success) {
|
|
42
|
+
this.metrics.tasks.completed++;
|
|
43
|
+
} else {
|
|
44
|
+
this.metrics.tasks.failed++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.metrics.durations.push(duration);
|
|
48
|
+
|
|
49
|
+
// 只保留最近 100 条
|
|
50
|
+
if (this.metrics.durations.length > 100) {
|
|
51
|
+
this.metrics.durations.shift();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 记录任务取消
|
|
57
|
+
*/
|
|
58
|
+
recordTaskCancelled() {
|
|
59
|
+
this.metrics.tasks.total++;
|
|
60
|
+
this.metrics.tasks.cancelled++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 记录 AI 调用
|
|
65
|
+
*/
|
|
66
|
+
recordAICall(duration, success) {
|
|
67
|
+
if (success) {
|
|
68
|
+
this.metrics.aiCalls.success++;
|
|
69
|
+
} else {
|
|
70
|
+
this.metrics.aiCalls.failure++;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.metrics.aiCalls.totalTime += duration;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 记录 Git 操作
|
|
78
|
+
*/
|
|
79
|
+
recordGitOperation(operation, success) {
|
|
80
|
+
const key = `${operation}${success ? 'Success' : 'Failure'}`;
|
|
81
|
+
|
|
82
|
+
if (this.metrics.git[key] !== undefined) {
|
|
83
|
+
this.metrics.git[key]++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 记录 HTTP 请求
|
|
89
|
+
*/
|
|
90
|
+
recordHTTPRequest(duration, isError = false) {
|
|
91
|
+
this.metrics.http.requests++;
|
|
92
|
+
|
|
93
|
+
if (isError) {
|
|
94
|
+
this.metrics.http.errors++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.metrics.http.responseTime.push(duration);
|
|
98
|
+
|
|
99
|
+
if (this.metrics.http.responseTime.length > 100) {
|
|
100
|
+
this.metrics.http.responseTime.shift();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 获取平均任务耗时
|
|
106
|
+
*/
|
|
107
|
+
getAverageTaskDuration() {
|
|
108
|
+
if (this.metrics.durations.length === 0) return 0;
|
|
109
|
+
|
|
110
|
+
const sum = this.metrics.durations.reduce((a, b) => a + b, 0);
|
|
111
|
+
return Math.round(sum / this.metrics.durations.length);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取平均响应时间
|
|
116
|
+
*/
|
|
117
|
+
getAverageResponseTime() {
|
|
118
|
+
if (this.metrics.http.responseTime.length === 0) return 0;
|
|
119
|
+
|
|
120
|
+
const sum = this.metrics.http.responseTime.reduce((a, b) => a + b, 0);
|
|
121
|
+
return Math.round(sum / this.metrics.http.responseTime.length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取成功率
|
|
126
|
+
*/
|
|
127
|
+
getSuccessRate() {
|
|
128
|
+
if (this.metrics.tasks.total === 0) return 100;
|
|
129
|
+
|
|
130
|
+
return Math.round(
|
|
131
|
+
(this.metrics.tasks.completed / this.metrics.tasks.total) * 100
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取所有指标
|
|
137
|
+
*/
|
|
138
|
+
getMetrics() {
|
|
139
|
+
return {
|
|
140
|
+
tasks: this.metrics.tasks,
|
|
141
|
+
performance: {
|
|
142
|
+
avgTaskDuration: this.getAverageTaskDuration(),
|
|
143
|
+
avgResponseTime: this.getAverageResponseTime(),
|
|
144
|
+
successRate: this.getSuccessRate(),
|
|
145
|
+
},
|
|
146
|
+
ai: {
|
|
147
|
+
...this.metrics.aiCalls,
|
|
148
|
+
avgTime: this.metrics.aiCalls.success > 0
|
|
149
|
+
? Math.round(this.metrics.aiCalls.totalTime / this.metrics.aiCalls.success)
|
|
150
|
+
: 0,
|
|
151
|
+
},
|
|
152
|
+
git: this.metrics.git,
|
|
153
|
+
http: {
|
|
154
|
+
total: this.metrics.http.requests,
|
|
155
|
+
errors: this.metrics.http.errors,
|
|
156
|
+
errorRate: this.metrics.http.requests > 0
|
|
157
|
+
? Math.round((this.metrics.http.errors / this.metrics.http.requests) * 100)
|
|
158
|
+
: 0,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 重置指标
|
|
165
|
+
*/
|
|
166
|
+
reset() {
|
|
167
|
+
this.metrics = {
|
|
168
|
+
tasks: { total: 0, completed: 0, failed: 0, cancelled: 0 },
|
|
169
|
+
durations: [],
|
|
170
|
+
aiCalls: { success: 0, failure: 0, totalTime: 0 },
|
|
171
|
+
git: {
|
|
172
|
+
pushSuccess: 0,
|
|
173
|
+
pushFailure: 0,
|
|
174
|
+
mergeSuccess: 0,
|
|
175
|
+
mergeFailure: 0,
|
|
176
|
+
},
|
|
177
|
+
http: { requests: 0, errors: 0, responseTime: [] },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
logger.info('📊 指标已重置');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = new MetricsCollector();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { logger } = require('../logger');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API Key 认证中间件(Koa 版本)
|
|
6
|
+
*/
|
|
7
|
+
function authenticateAPI(ctx, next) {
|
|
8
|
+
const apiKey = ctx.headers['x-api-key'];
|
|
9
|
+
const expectedKey = process.env.API_KEY;
|
|
10
|
+
|
|
11
|
+
// 如果未配置 API Key,则跳过验证
|
|
12
|
+
if (!expectedKey) {
|
|
13
|
+
logger.warn('⚠️ API_KEY 未配置,建议在生产环境中配置');
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
logger.warn('未授权的访问尝试 - 缺少 API Key', {
|
|
19
|
+
ip: ctx.ip,
|
|
20
|
+
path: ctx.path
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
ctx.status = 401;
|
|
24
|
+
ctx.body = {
|
|
25
|
+
success: false,
|
|
26
|
+
error: '未授权访问:缺少 API Key'
|
|
27
|
+
};
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 使用时间常数比较防止时序攻击
|
|
32
|
+
const apiKeyBuffer = Buffer.from(apiKey);
|
|
33
|
+
const expectedKeyBuffer = Buffer.from(expectedKey);
|
|
34
|
+
|
|
35
|
+
if (apiKeyBuffer.length !== expectedKeyBuffer.length ||
|
|
36
|
+
!crypto.timingSafeEqual(apiKeyBuffer, expectedKeyBuffer)) {
|
|
37
|
+
logger.warn('未授权的访问尝试 - API Key 无效', {
|
|
38
|
+
ip: ctx.ip,
|
|
39
|
+
path: ctx.path
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
ctx.status = 401;
|
|
43
|
+
ctx.body = {
|
|
44
|
+
success: false,
|
|
45
|
+
error: '未授权访问:API Key 无效'
|
|
46
|
+
};
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* IP 白名单中间件(Koa 版本)
|
|
55
|
+
*/
|
|
56
|
+
function ipWhitelist(ctx, next) {
|
|
57
|
+
const allowedIPs = (process.env.ALLOWED_IPS || '').split(',').map(ip => ip.trim()).filter(ip => ip);
|
|
58
|
+
|
|
59
|
+
// 如果未配置白名单,则跳过检查
|
|
60
|
+
if (allowedIPs.length === 0) {
|
|
61
|
+
return next();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const clientIP = ctx.ip;
|
|
65
|
+
|
|
66
|
+
if (!allowedIPs.includes(clientIP)) {
|
|
67
|
+
logger.warn('IP 不在白名单中', {
|
|
68
|
+
ip: clientIP,
|
|
69
|
+
path: ctx.path
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
ctx.status = 403;
|
|
73
|
+
ctx.body = {
|
|
74
|
+
success: false,
|
|
75
|
+
error: '访问被拒绝:IP 不在白名单中'
|
|
76
|
+
};
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return next();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
authenticateAPI,
|
|
85
|
+
ipWhitelist
|
|
86
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const ratelimit = require("koa-ratelimit");
|
|
2
|
+
const { logger } = require("../logger");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 内存存储 Map(用于频率限制)
|
|
6
|
+
*/
|
|
7
|
+
const rateLimitDB = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 创建速率限制器
|
|
11
|
+
* @param {Object} options - 配置选项
|
|
12
|
+
* @param {number} options.duration - 时间窗口(毫秒)
|
|
13
|
+
* @param {number} options.max - 最大请求数
|
|
14
|
+
* @param {string} options.name - 限制器名称
|
|
15
|
+
* @returns {Function} Koa 中间件
|
|
16
|
+
*/
|
|
17
|
+
function createRateLimiter(options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
duration = 60 * 60 * 1000, // 0.5 分钟
|
|
20
|
+
max = 30, // 最多 30 个请求
|
|
21
|
+
name = "default",
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
return ratelimit({
|
|
25
|
+
driver: "memory",
|
|
26
|
+
db: rateLimitDB,
|
|
27
|
+
duration, // 时间窗口(毫秒)
|
|
28
|
+
max, // 最大请求数
|
|
29
|
+
errorMessage: "请求过于频繁,请稍后再试",
|
|
30
|
+
id: (ctx) => ctx.ip, // 使用 IP 地址作为标识
|
|
31
|
+
headers: {
|
|
32
|
+
remaining: "Rate-Limit-Remaining",
|
|
33
|
+
reset: "Rate-Limit-Reset",
|
|
34
|
+
total: "Rate-Limit-Total",
|
|
35
|
+
},
|
|
36
|
+
disableHeader: false,
|
|
37
|
+
whitelist: (ctx) => {
|
|
38
|
+
// 跳过健康检查接口
|
|
39
|
+
if (ctx.path === "/health") {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
handler: async (ctx) => {
|
|
45
|
+
logger.warn("请求频率超限", {
|
|
46
|
+
ip: ctx.ip,
|
|
47
|
+
path: ctx.path,
|
|
48
|
+
limiter: name,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
ctx.status = 429;
|
|
52
|
+
ctx.body = {
|
|
53
|
+
success: false,
|
|
54
|
+
error: "请求过于频繁,请稍后再试",
|
|
55
|
+
retryAfter: Math.ceil(duration / 1000),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 通用 API 限流器
|
|
63
|
+
* 15 分钟内最多 20 个请求
|
|
64
|
+
*/
|
|
65
|
+
const apiLimiter = createRateLimiter({
|
|
66
|
+
duration: 10 * 1000,
|
|
67
|
+
max: 30,
|
|
68
|
+
name: "api",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 严格限流器(用于敏感接口)
|
|
73
|
+
* 1 小时内最多 20 个请求
|
|
74
|
+
*/
|
|
75
|
+
const strictLimiter = createRateLimiter({
|
|
76
|
+
duration: 60 * 60 * 1000,
|
|
77
|
+
max: 20,
|
|
78
|
+
name: "strict",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
apiLimiter,
|
|
83
|
+
strictLimiter,
|
|
84
|
+
createRateLimiter,
|
|
85
|
+
};
|