chanjs 2.7.2 → 2.7.3
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/App.js +232 -16
- package/base/Aop.js +20 -3
- package/base/Container.js +80 -3
- package/base/Controller.js +38 -9
- package/base/Database.js +50 -0
- package/base/Event.js +12 -0
- package/base/{Service.js → Repository.js} +644 -539
- package/common/api.js +18 -8
- package/common/code.js +25 -15
- package/common/email.js +98 -17
- package/common/index.js +1 -1
- package/config/code.js +138 -82
- package/global/index.js +1 -1
- package/helper/index.js +43 -41
- package/index.js +19 -6
- package/loader/index.js +6 -0
- package/{helper → loader}/loader.js +41 -27
- package/middleware/compress.js +185 -0
- package/middleware/cors.js +36 -24
- package/middleware/header.js +5 -10
- package/middleware/index.js +1 -0
- package/middleware/log.js +27 -3
- package/middleware/setBody.js +9 -1
- package/middleware/static.js +2 -1
- package/middleware/template.js +139 -4
- package/middleware/waf.js +136 -76
- package/package.json +2 -3
- package/realtime/index.js +7 -0
- package/realtime/sse.js +424 -0
- package/realtime/websocket.js +540 -0
- package/response/index.js +12 -0
- package/response/response.js +258 -0
- package/schedule/index.js +6 -0
- package/schedule/schedule.js +491 -0
- package/{helper → security}/checker.js +23 -8
- package/security/index.js +14 -0
- package/{helper → security}/jwt.js +175 -107
- package/security/keywords.js +179 -0
- package/security/rate-limit.js +105 -0
- package/security/sign.js +210 -0
- package/security/xss-filter.js +63 -0
- package/storage/cache.js +258 -0
- package/storage/index.js +9 -0
- package/storage/redis.js +258 -0
- package/storage/store.js +266 -0
- package/{helper → utils}/file.js +106 -15
- package/{helper → utils}/filter.js +2 -1
- package/{helper → utils}/html.js +19 -1
- package/utils/index.js +34 -0
- package/{helper → utils}/ip.js +25 -16
- package/utils/request.js +172 -0
- package/{helper → utils}/time.js +1 -1
- package/utils/tree.js +121 -0
- package/common/category.js +0 -22
- package/common/sms.js +0 -104
- package/extend/art-template.js +0 -129
- package/extend/index.js +0 -6
- package/global/global.js +0 -63
- package/helper/cache.js +0 -187
- package/helper/keywords.js +0 -132
- package/helper/rate-limit.js +0 -116
- package/helper/request.js +0 -47
- package/helper/response.js +0 -180
- package/helper/sign.js +0 -96
- package/helper/tree.js +0 -77
- package/helper/xss-filter.js +0 -42
- /package/{helper → utils}/data-parse.js +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量级定时任务调度器
|
|
3
|
+
*
|
|
4
|
+
* ============================================================
|
|
5
|
+
* 使用方法
|
|
6
|
+
* ============================================================
|
|
7
|
+
*
|
|
8
|
+
* 零依赖、单进程内调度。基于 setTimeout + 递归调度实现。
|
|
9
|
+
* 适合 Web 应用内的周期性任务(清理、统计、轮询、健康检查)。
|
|
10
|
+
*
|
|
11
|
+
* 1. 固定间隔(最常用)
|
|
12
|
+
* import { schedule } from 'chanjs/helper/schedule.js';
|
|
13
|
+
* schedule.every('clean-log', 5 * 60 * 1000, async () => {
|
|
14
|
+
* await cleanOldLogs();
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* 2. Cron 表达式(5 字段:分 时 日 月 周)
|
|
18
|
+
* schedule.cron('daily-report', '0 2 * * *', async () => {
|
|
19
|
+
* await sendDailyReport(); // 每天凌晨 2 点
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* 3. 一次性延迟
|
|
23
|
+
* schedule.delay('warmup', 10 * 1000, async () => {
|
|
24
|
+
* await warmUpCache();
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* 4. 任务控制
|
|
28
|
+
* schedule.stop('clean-log'); // 停止
|
|
29
|
+
* schedule.start('clean-log'); // 启动
|
|
30
|
+
* schedule.remove('clean-log'); // 移除
|
|
31
|
+
* schedule.list(); // 查看所有任务
|
|
32
|
+
*
|
|
33
|
+
* 5. 优雅停机
|
|
34
|
+
* process.on('SIGTERM', () => schedule.shutdown());
|
|
35
|
+
*
|
|
36
|
+
* ============================================================
|
|
37
|
+
*
|
|
38
|
+
* 设计要点:
|
|
39
|
+
* 1. 错误隔离:单任务异常不影响其他任务,记录错误后继续下次调度
|
|
40
|
+
* 2. 防重叠:同一任务上一次未完成不会启动下一次(避免积压)
|
|
41
|
+
* 3. 漂移修正:固定间隔模式按"上次完成时间 + 间隔"调度,避免误差累积
|
|
42
|
+
* 4. 优雅停机:shutdown() 清理所有定时器,未完成的任务标记后不重启
|
|
43
|
+
* 5. Cron 解析:5 字段标准语法,支持 * / - , 四种操作符
|
|
44
|
+
*
|
|
45
|
+
* Cron 字段说明:
|
|
46
|
+
* 字段 允许值 允许的特殊字符
|
|
47
|
+
* 分钟 0-59 星号 / 减号 逗号
|
|
48
|
+
* 小时 0-23 星号 / 减号 逗号
|
|
49
|
+
* 日 1-31 星号 / 减号 逗号
|
|
50
|
+
* 月 1-12 星号 / 减号 逗号
|
|
51
|
+
* 周 0-6 (0=周日) 星号 / 减号 逗号
|
|
52
|
+
*
|
|
53
|
+
* Cron 示例(实际使用时把"星号"替换为 *):
|
|
54
|
+
* 星号/5 * * * * 每 5 分钟
|
|
55
|
+
* 0 星号 * * * 每小时整点
|
|
56
|
+
* 0 2 * * * 每天凌晨 2 点
|
|
57
|
+
* 0 0 * * 1 每周一凌晨
|
|
58
|
+
* 0 0 1 * * 每月 1 号凌晨
|
|
59
|
+
* 0 0,30 9-17 * * 工作时间每半点
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cron 字段定义:[字段名, 最小值, 最大值]
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
const CRON_FIELDS = [
|
|
67
|
+
['minute', 0, 59],
|
|
68
|
+
['hour', 0, 23],
|
|
69
|
+
['dayOfMonth', 1, 31],
|
|
70
|
+
['month', 1, 12],
|
|
71
|
+
['dayOfWeek', 0, 6], // 0=周日
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 解析 Cron 单个字段,返回允许值集合
|
|
76
|
+
* 支持:* / - ,
|
|
77
|
+
* @param {string} field - 字段表达式
|
|
78
|
+
* @param {number} min - 最小值
|
|
79
|
+
* @param {number} max - 最大值
|
|
80
|
+
* @returns {Set<number>} 允许值集合
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
function parseCronField(field, min, max) {
|
|
84
|
+
const result = new Set();
|
|
85
|
+
const parts = field.split(',').map(s => s.trim());
|
|
86
|
+
|
|
87
|
+
for (const part of parts) {
|
|
88
|
+
// */n → 每 n 个单位
|
|
89
|
+
if (part.startsWith('*/')) {
|
|
90
|
+
const step = parseInt(part.slice(2), 10);
|
|
91
|
+
if (!step || step <= 0) throw new Error(`Cron 步长无效: ${part}`);
|
|
92
|
+
for (let i = min; i <= max; i += step) result.add(i);
|
|
93
|
+
}
|
|
94
|
+
// a-b → 区间
|
|
95
|
+
else if (part.includes('-')) {
|
|
96
|
+
const [startStr, endStr] = part.split('-');
|
|
97
|
+
const start = parseInt(startStr, 10);
|
|
98
|
+
const end = parseInt(endStr, 10);
|
|
99
|
+
if (isNaN(start) || isNaN(end) || start > end) {
|
|
100
|
+
throw new Error(`Cron 区间无效: ${part}`);
|
|
101
|
+
}
|
|
102
|
+
for (let i = start; i <= end; i++) result.add(i);
|
|
103
|
+
}
|
|
104
|
+
// * → 全部
|
|
105
|
+
else if (part === '*') {
|
|
106
|
+
for (let i = min; i <= max; i++) result.add(i);
|
|
107
|
+
}
|
|
108
|
+
// 单值
|
|
109
|
+
else {
|
|
110
|
+
const val = parseInt(part, 10);
|
|
111
|
+
if (isNaN(val)) throw new Error(`Cron 值无效: ${part}`);
|
|
112
|
+
if (val < min || val > max) throw new Error(`Cron 值超出范围: ${part} (${min}-${max})`);
|
|
113
|
+
result.add(val);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 解析 Cron 表达式
|
|
122
|
+
* @param {string} expr - 5 字段 cron 表达式
|
|
123
|
+
* @returns {Object} { minute:Set, hour:Set, dayOfMonth:Set, month:Set, dayOfWeek:Set }
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
function parseCronExpression(expr) {
|
|
127
|
+
const fields = expr.trim().split(/\s+/);
|
|
128
|
+
if (fields.length !== 5) {
|
|
129
|
+
throw new Error(`Cron 表达式必须是 5 个字段: "${expr}"`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsed = {};
|
|
133
|
+
for (let i = 0; i < 5; i++) {
|
|
134
|
+
const [name, min, max] = CRON_FIELDS[i];
|
|
135
|
+
parsed[name] = parseCronField(fields[i], min, max);
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 计算下次执行时间
|
|
142
|
+
* 从 from 的下一分钟开始,逐分钟递增,找到第一个匹配的时间
|
|
143
|
+
* 最长搜索 4 年(覆盖闰年周期)或最多 10000 次迭代,找不到返回 null
|
|
144
|
+
* @param {Object} cron - parseCronExpression 返回值
|
|
145
|
+
* @param {Date} from - 起始时间
|
|
146
|
+
* @returns {Date|null} 下次执行时间
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
function nextCronTime(cron, from = new Date()) {
|
|
150
|
+
const next = new Date(from);
|
|
151
|
+
next.setSeconds(0, 0); // 重置到当前分钟开始
|
|
152
|
+
next.setMinutes(next.getMinutes() + 1); // 下一分钟开始
|
|
153
|
+
|
|
154
|
+
// 搜索上限:4 年(覆盖闰年)
|
|
155
|
+
const limit = new Date(from);
|
|
156
|
+
limit.setFullYear(limit.getFullYear() + 4);
|
|
157
|
+
|
|
158
|
+
// 最大迭代次数限制,避免某些边界 cron 表达式导致 CPU 突刺
|
|
159
|
+
// 4 年 ≈ 2,107,680 分钟,10000 次迭代约等于一周扫描范围,正常 cron 都能匹配到
|
|
160
|
+
const MAX_ITERATIONS = 10000;
|
|
161
|
+
let iterations = 0;
|
|
162
|
+
|
|
163
|
+
while (next.getTime() < limit.getTime() && iterations < MAX_ITERATIONS) {
|
|
164
|
+
iterations++;
|
|
165
|
+
// 注意:JS getMonth() 返回 0-11,cron month 是 1-12
|
|
166
|
+
// 注意:JS getDay() 返回 0-6(0=周日),cron dow 也是 0-6
|
|
167
|
+
if (
|
|
168
|
+
cron.month.has(next.getMonth() + 1) &&
|
|
169
|
+
cron.dayOfMonth.has(next.getDate()) &&
|
|
170
|
+
cron.dayOfWeek.has(next.getDay()) &&
|
|
171
|
+
cron.hour.has(next.getHours()) &&
|
|
172
|
+
cron.minute.has(next.getMinutes())
|
|
173
|
+
) {
|
|
174
|
+
return next;
|
|
175
|
+
}
|
|
176
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
180
|
+
console.warn(`[Schedule] nextCronTime 已达最大迭代次数 ${MAX_ITERATIONS},可能 cron 表达式不合理`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 定时任务调度器
|
|
188
|
+
* @class Schedule
|
|
189
|
+
*/
|
|
190
|
+
class Schedule {
|
|
191
|
+
constructor() {
|
|
192
|
+
/** @type {Map<string, Object>} 任务表 name -> task */
|
|
193
|
+
this._tasks = new Map();
|
|
194
|
+
/** 是否已 shutdown(shutdown 后不再接受新任务) */
|
|
195
|
+
this._shuttingDown = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 添加任务(底层方法)
|
|
200
|
+
* @param {string} name - 任务名(唯一)
|
|
201
|
+
* @param {Object} options - 任务配置
|
|
202
|
+
* @param {Function} options.fn - 任务函数
|
|
203
|
+
* @param {string} [options.type] - 类型:'every' | 'cron' | 'delay'
|
|
204
|
+
* @param {number} [options.intervalMs] - every 模式的间隔毫秒
|
|
205
|
+
* @param {string} [options.cron] - cron 表达式
|
|
206
|
+
* @param {number} [options.delayMs] - delay 模式的延迟毫秒
|
|
207
|
+
* @param {boolean} [options.runOnInit=false] - 是否立即执行一次
|
|
208
|
+
* @param {boolean} [options.autoStart=true] - 是否自动启动
|
|
209
|
+
* @returns {Object} 任务对象
|
|
210
|
+
*/
|
|
211
|
+
add(name, options) {
|
|
212
|
+
if (this._shuttingDown) {
|
|
213
|
+
throw new Error('[Schedule] 已 shutdown,无法添加任务');
|
|
214
|
+
}
|
|
215
|
+
if (this._tasks.has(name)) {
|
|
216
|
+
throw new Error(`[Schedule] 任务已存在: ${name}`);
|
|
217
|
+
}
|
|
218
|
+
if (typeof options.fn !== 'function') {
|
|
219
|
+
throw new Error(`[Schedule] fn 必须是函数: ${name}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const task = {
|
|
223
|
+
name,
|
|
224
|
+
fn: options.fn,
|
|
225
|
+
type: options.type,
|
|
226
|
+
intervalMs: options.intervalMs,
|
|
227
|
+
cron: options.cron ? parseCronExpression(options.cron) : null,
|
|
228
|
+
cronExpr: options.cron || null,
|
|
229
|
+
delayMs: options.delayMs,
|
|
230
|
+
runOnInit: options.runOnInit || false,
|
|
231
|
+
// 运行时状态
|
|
232
|
+
running: false, // 是否正在执行
|
|
233
|
+
timer: null, // setTimeout 句柄
|
|
234
|
+
lastRun: null, // 上次执行时间
|
|
235
|
+
nextRun: null, // 下次执行时间
|
|
236
|
+
lastError: null, // 上次错误
|
|
237
|
+
runCount: 0, // 累计执行次数
|
|
238
|
+
errorCount: 0, // 累计错误次数
|
|
239
|
+
started: false, // 是否已启动
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
this._tasks.set(name, task);
|
|
243
|
+
|
|
244
|
+
if (options.autoStart !== false) {
|
|
245
|
+
this._startTask(task);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return task;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 固定间隔任务
|
|
253
|
+
* @param {string} name - 任务名
|
|
254
|
+
* @param {number} intervalMs - 间隔毫秒
|
|
255
|
+
* @param {Function} fn - 任务函数
|
|
256
|
+
* @param {Object} [options] - 额外选项(runOnInit / autoStart)
|
|
257
|
+
*/
|
|
258
|
+
every(name, intervalMs, fn, options = {}) {
|
|
259
|
+
if (!intervalMs || intervalMs <= 0) {
|
|
260
|
+
throw new Error(`[Schedule] intervalMs 必须大于 0: ${name}`);
|
|
261
|
+
}
|
|
262
|
+
return this.add(name, { ...options, type: 'every', intervalMs, fn });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Cron 表达式任务
|
|
267
|
+
* @param {string} name - 任务名
|
|
268
|
+
* @param {string} expression - 5 字段 cron 表达式
|
|
269
|
+
* @param {Function} fn - 任务函数
|
|
270
|
+
* @param {Object} [options] - 额外选项(runOnInit / autoStart)
|
|
271
|
+
*/
|
|
272
|
+
cron(name, expression, fn, options = {}) {
|
|
273
|
+
return this.add(name, { ...options, type: 'cron', cron: expression, fn });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 一次性延迟任务
|
|
278
|
+
* @param {string} name - 任务名
|
|
279
|
+
* @param {number} delayMs - 延迟毫秒
|
|
280
|
+
* @param {Function} fn - 任务函数
|
|
281
|
+
*/
|
|
282
|
+
delay(name, delayMs, fn) {
|
|
283
|
+
if (!delayMs || delayMs <= 0) {
|
|
284
|
+
throw new Error(`[Schedule] delayMs 必须大于 0: ${name}`);
|
|
285
|
+
}
|
|
286
|
+
return this.add(name, { type: 'delay', delayMs, fn, autoStart: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 启动任务
|
|
291
|
+
* @param {string} name - 任务名
|
|
292
|
+
* @returns {boolean} 是否成功启动
|
|
293
|
+
*/
|
|
294
|
+
start(name) {
|
|
295
|
+
const task = this._tasks.get(name);
|
|
296
|
+
if (!task) return false;
|
|
297
|
+
if (task.started) return false;
|
|
298
|
+
this._startTask(task);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 启动所有任务
|
|
304
|
+
*/
|
|
305
|
+
startAll() {
|
|
306
|
+
for (const task of this._tasks.values()) {
|
|
307
|
+
if (!task.started) this._startTask(task);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 启动单个任务(内部)
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
_startTask(task) {
|
|
316
|
+
if (task.started || this._shuttingDown) return;
|
|
317
|
+
task.started = true;
|
|
318
|
+
|
|
319
|
+
// 立即执行一次
|
|
320
|
+
if (task.runOnInit) {
|
|
321
|
+
this._run(task);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this._scheduleNext(task);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 调度下次执行
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
_scheduleNext(task) {
|
|
332
|
+
if (!task.started || this._shuttingDown) return;
|
|
333
|
+
|
|
334
|
+
let delay;
|
|
335
|
+
if (task.type === 'every') {
|
|
336
|
+
delay = task.intervalMs;
|
|
337
|
+
task.nextRun = new Date(Date.now() + delay);
|
|
338
|
+
} else if (task.type === 'cron') {
|
|
339
|
+
const nextTime = nextCronTime(task.cron, task.lastRun || new Date());
|
|
340
|
+
if (!nextTime) {
|
|
341
|
+
console.warn(`[Schedule] 任务 ${task.name} 在 4 年内找不到下次执行时间,已停止`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
delay = nextTime.getTime() - Date.now();
|
|
345
|
+
task.nextRun = nextTime;
|
|
346
|
+
} else if (task.type === 'delay') {
|
|
347
|
+
delay = task.delayMs;
|
|
348
|
+
task.nextRun = new Date(Date.now() + delay);
|
|
349
|
+
} else {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 防御:delay 过小(<0)会导致立即触发,可能死循环
|
|
354
|
+
if (delay < 0) delay = 0;
|
|
355
|
+
|
|
356
|
+
task.timer = setTimeout(() => {
|
|
357
|
+
this._run(task).then(() => {
|
|
358
|
+
// delay 类型是一次性,执行完不再调度
|
|
359
|
+
if (task.type !== 'delay') {
|
|
360
|
+
this._scheduleNext(task);
|
|
361
|
+
} else {
|
|
362
|
+
task.started = false;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}, delay);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 执行任务(错误隔离 + 防重叠)
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
async _run(task) {
|
|
373
|
+
// 防重叠:上次还没执行完
|
|
374
|
+
if (task.running) {
|
|
375
|
+
console.warn(`[Schedule] 任务 ${task.name} 上次未完成,跳过本次执行`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
task.running = true;
|
|
380
|
+
task.lastRun = new Date();
|
|
381
|
+
task.runCount++;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
await task.fn();
|
|
385
|
+
task.lastError = null;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
task.lastError = err;
|
|
388
|
+
task.errorCount++;
|
|
389
|
+
// 错误隔离:打印日志但不中断后续调度
|
|
390
|
+
console.error(`[Schedule] 任务 ${task.name} 执行失败:`, err.message);
|
|
391
|
+
} finally {
|
|
392
|
+
task.running = false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 停止任务(保留任务,可再启动)
|
|
398
|
+
* @param {string} name - 任务名
|
|
399
|
+
* @returns {boolean} 是否成功停止
|
|
400
|
+
*/
|
|
401
|
+
stop(name) {
|
|
402
|
+
const task = this._tasks.get(name);
|
|
403
|
+
if (!task) return false;
|
|
404
|
+
if (task.timer) {
|
|
405
|
+
clearTimeout(task.timer);
|
|
406
|
+
task.timer = null;
|
|
407
|
+
}
|
|
408
|
+
task.started = false;
|
|
409
|
+
task.nextRun = null;
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 停止所有任务
|
|
415
|
+
*/
|
|
416
|
+
stopAll() {
|
|
417
|
+
for (const name of this._tasks.keys()) {
|
|
418
|
+
this.stop(name);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 移除任务
|
|
424
|
+
* @param {string} name - 任务名
|
|
425
|
+
* @returns {boolean} 是否成功移除
|
|
426
|
+
*/
|
|
427
|
+
remove(name) {
|
|
428
|
+
const task = this._tasks.get(name);
|
|
429
|
+
if (!task) return false;
|
|
430
|
+
this.stop(name);
|
|
431
|
+
return this._tasks.delete(name);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 任务是否存在
|
|
436
|
+
*/
|
|
437
|
+
has(name) {
|
|
438
|
+
return this._tasks.has(name);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 获取单个任务状态
|
|
443
|
+
*/
|
|
444
|
+
get(name) {
|
|
445
|
+
const task = this._tasks.get(name);
|
|
446
|
+
if (!task) return null;
|
|
447
|
+
return this._taskStatus(task);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 获取所有任务状态
|
|
452
|
+
*/
|
|
453
|
+
list() {
|
|
454
|
+
return Array.from(this._tasks.values()).map(t => this._taskStatus(t));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 生成任务状态对象
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_taskStatus(task) {
|
|
462
|
+
return {
|
|
463
|
+
name: task.name,
|
|
464
|
+
type: task.type,
|
|
465
|
+
cron: task.cronExpr,
|
|
466
|
+
intervalMs: task.intervalMs,
|
|
467
|
+
delayMs: task.delayMs,
|
|
468
|
+
started: task.started,
|
|
469
|
+
running: task.running,
|
|
470
|
+
lastRun: task.lastRun,
|
|
471
|
+
nextRun: task.nextRun,
|
|
472
|
+
lastError: task.lastError?.message || null,
|
|
473
|
+
runCount: task.runCount,
|
|
474
|
+
errorCount: task.errorCount,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 优雅停机:停止所有任务,拒绝后续添加
|
|
480
|
+
*/
|
|
481
|
+
shutdown() {
|
|
482
|
+
this._shuttingDown = true;
|
|
483
|
+
this.stopAll();
|
|
484
|
+
console.log(`[Schedule] 已关闭所有定时任务(共 ${this._tasks.size} 个)`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 全局单例
|
|
489
|
+
export const schedule = new Schedule();
|
|
490
|
+
export { Schedule };
|
|
491
|
+
export default Schedule;
|
|
@@ -10,8 +10,13 @@ import { keywordRegexCache } from "./keywords.js";
|
|
|
10
10
|
* @param {string} fullText - 要检查的完整文本
|
|
11
11
|
* @returns {Object|null} 检测结果,包含 category 和 keyword,未检测到返回 null
|
|
12
12
|
* @description
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* 性能优化(P2 #19):
|
|
14
|
+
* 1. 先用每类合并的大正则做预检(单次 test 判断该类是否命中)
|
|
15
|
+
* 2. 预检命中后再遍历该类的具体关键词定位命中项
|
|
16
|
+
* 3. 正常请求:8 次大正则 test,比原版 80+ 次小正则 test 快 10 倍
|
|
17
|
+
* 4. 恶意请求:大正则预检 + 该类关键词遍历,开销略增但可接受
|
|
18
|
+
*
|
|
19
|
+
* 按优先级检查:sqlInjection / xss / commandInjection 优先
|
|
15
20
|
* @example
|
|
16
21
|
* const result = checkKeywords('SELECT * FROM users WHERE 1=1');
|
|
17
22
|
* console.log(result); // { category: 'sqlInjection', keyword: 'union select' }
|
|
@@ -22,20 +27,30 @@ export function checkKeywords(fullText) {
|
|
|
22
27
|
const priorityCategories = ['sqlInjection', 'xss', 'commandInjection'];
|
|
23
28
|
const otherCategories = Object.keys(keywordRegexCache).filter(c => !priorityCategories.includes(c));
|
|
24
29
|
|
|
30
|
+
// 优先类别
|
|
25
31
|
for (const category of priorityCategories) {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
28
|
-
|
|
32
|
+
const cache = keywordRegexCache[category];
|
|
33
|
+
if (!cache?.mergedRegex) continue;
|
|
34
|
+
|
|
35
|
+
// 预检:大正则一次 test
|
|
36
|
+
if (!cache.mergedRegex.test(fullText)) continue;
|
|
37
|
+
|
|
38
|
+
// 命中:遍历定位具体关键词
|
|
39
|
+
for (const { keyword, regex } of cache.patterns) {
|
|
29
40
|
if (regex.test(fullText)) {
|
|
30
41
|
return { category, keyword };
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
// 其他类别
|
|
35
47
|
for (const category of otherCategories) {
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
38
|
-
|
|
48
|
+
const cache = keywordRegexCache[category];
|
|
49
|
+
if (!cache?.mergedRegex) continue;
|
|
50
|
+
|
|
51
|
+
if (!cache.mergedRegex.test(fullText)) continue;
|
|
52
|
+
|
|
53
|
+
for (const { keyword, regex } of cache.patterns) {
|
|
39
54
|
if (regex.test(fullText)) {
|
|
40
55
|
return { category, keyword };
|
|
41
56
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全模块 - 关键词检查/XSS过滤/签名加密/JWT/限流
|
|
3
|
+
* - checker + keywords: 敏感词检测(大正则预检优化)
|
|
4
|
+
* - xss-filter: XSS 过滤(WeakSet 防循环引用)
|
|
5
|
+
* - sign: AES-256-GCM 签名加密(timingSafeEqual 防时序攻击)
|
|
6
|
+
* - jwt: JWT 令牌生成/验证
|
|
7
|
+
* - rate-limit: IP 维度限流(服务端存储)
|
|
8
|
+
*/
|
|
9
|
+
export { checkKeywords, isIgnored } from "./checker.js";
|
|
10
|
+
export { keywordRegexCache, KEYWORD_RULES } from "./keywords.js";
|
|
11
|
+
export { filterXSS } from "./xss-filter.js";
|
|
12
|
+
export { signData, verifySign, aesEncrypt, aesDecrypt } from "./sign.js";
|
|
13
|
+
export { verifyToken, generateToken, setToken, getToken, revokeToken, isTokenRevoked } from "./jwt.js";
|
|
14
|
+
export { createRateLimitMiddleware } from "./rate-limit.js";
|