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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务队列集成示例
|
|
3
|
+
*
|
|
4
|
+
* 这个文件展示如何将任务队列集成到 index.js 中
|
|
5
|
+
* 不需要修改现有代码,只需添加队列层
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const taskQueue = require('./queue/task-queue');
|
|
9
|
+
const taskManager = require('./task-manager');
|
|
10
|
+
const { logger } = require('./logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 初始化任务队列处理器
|
|
14
|
+
*/
|
|
15
|
+
async function initializeQueue() {
|
|
16
|
+
// 设置队列处理函数
|
|
17
|
+
taskQueue.process(async (job) => {
|
|
18
|
+
const { taskId, taskData } = job.data;
|
|
19
|
+
|
|
20
|
+
logger.info(`🎯 队列开始处理任务: ${taskId}`);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// 执行实际的修复任务
|
|
24
|
+
await executeFixTask(taskData);
|
|
25
|
+
|
|
26
|
+
logger.info(`✅ 队列任务完成: ${taskId}`);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(`❌ 队列任务失败: ${taskId}`, error);
|
|
29
|
+
throw error; // 重新抛出让队列处理重试
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
logger.info('✅ 任务队列处理器已初始化');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 提交任务到队列(替代直接执行)
|
|
38
|
+
*/
|
|
39
|
+
async function submitTask(taskId, taskData) {
|
|
40
|
+
try {
|
|
41
|
+
const queueJob = await taskQueue.addTask({
|
|
42
|
+
taskId,
|
|
43
|
+
taskData,
|
|
44
|
+
priority: taskData.priority || 0,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
logger.info(`📨 任务已提交到队列: ${taskId} (队列ID: ${queueJob.id})`);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
taskId,
|
|
52
|
+
queueJobId: queueJob.id,
|
|
53
|
+
queueType: queueJob.queueType,
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(`提交任务到队列失败: ${taskId}`, error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 执行实际的修复任务
|
|
63
|
+
*/
|
|
64
|
+
async function executeFixTask(taskData) {
|
|
65
|
+
const {
|
|
66
|
+
taskId,
|
|
67
|
+
requirement,
|
|
68
|
+
projectPath,
|
|
69
|
+
repoUrl,
|
|
70
|
+
runTests,
|
|
71
|
+
autoPush,
|
|
72
|
+
aiEngine,
|
|
73
|
+
baseBranch,
|
|
74
|
+
createNewBranch,
|
|
75
|
+
username,
|
|
76
|
+
mergeToAlpha,
|
|
77
|
+
mergeToBeta,
|
|
78
|
+
mergeToMaster,
|
|
79
|
+
} = taskData;
|
|
80
|
+
|
|
81
|
+
// 这里是原有的任务执行逻辑
|
|
82
|
+
// 从 index.js 的 processFixTask 函数中提取
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
taskManager.startTask(taskId);
|
|
86
|
+
taskManager.updateProgress(taskId, 'preparing', '准备项目环境');
|
|
87
|
+
|
|
88
|
+
// ... 执行修复逻辑 ...
|
|
89
|
+
|
|
90
|
+
taskManager.completeTask(taskId, {
|
|
91
|
+
success: true,
|
|
92
|
+
// ... 结果数据 ...
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
} catch (error) {
|
|
96
|
+
taskManager.failTask(taskId, error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 在 router.post('/api/fix-bug') 中的使用示例
|
|
103
|
+
*/
|
|
104
|
+
function exampleRouteHandler() {
|
|
105
|
+
router.post("/api/fix-bug", async (ctx) => {
|
|
106
|
+
try {
|
|
107
|
+
// 1. 创建任务记录
|
|
108
|
+
const taskId = taskManager.createTask(
|
|
109
|
+
sanitizer.sanitizeString(requirement),
|
|
110
|
+
finalProjectPath,
|
|
111
|
+
{
|
|
112
|
+
repoUrl: repoUrl ? sanitizer.sanitizeRepoUrl(repoUrl) : null,
|
|
113
|
+
runTests,
|
|
114
|
+
autoPush,
|
|
115
|
+
aiEngine,
|
|
116
|
+
baseBranch,
|
|
117
|
+
createNewBranch,
|
|
118
|
+
username,
|
|
119
|
+
mergeToAlpha,
|
|
120
|
+
mergeToBeta,
|
|
121
|
+
mergeToMaster,
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 2. 提交到队列(替代原来的 processFixTask(taskId))
|
|
126
|
+
const queueResult = await submitTask(taskId, {
|
|
127
|
+
taskId,
|
|
128
|
+
requirement,
|
|
129
|
+
projectPath: finalProjectPath,
|
|
130
|
+
repoUrl,
|
|
131
|
+
runTests,
|
|
132
|
+
autoPush,
|
|
133
|
+
aiEngine,
|
|
134
|
+
baseBranch,
|
|
135
|
+
createNewBranch,
|
|
136
|
+
username,
|
|
137
|
+
mergeToAlpha,
|
|
138
|
+
mergeToBeta,
|
|
139
|
+
mergeToMaster,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 3. 立即返回响应
|
|
143
|
+
ctx.body = {
|
|
144
|
+
success: true,
|
|
145
|
+
id: taskId,
|
|
146
|
+
taskId: taskId,
|
|
147
|
+
message: "任务已加入队列,正在等待处理",
|
|
148
|
+
statusUrl: `/api/tasks/${taskId}`,
|
|
149
|
+
queueInfo: {
|
|
150
|
+
queueJobId: queueResult.queueJobId,
|
|
151
|
+
queueType: queueResult.queueType,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.error("创建任务失败:", error);
|
|
157
|
+
ctx.status = 500;
|
|
158
|
+
ctx.body = {
|
|
159
|
+
success: false,
|
|
160
|
+
error: error.message,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 新增:查询队列状态接口
|
|
166
|
+
router.get("/api/queue/stats", async (ctx) => {
|
|
167
|
+
try {
|
|
168
|
+
const stats = await taskQueue.getStats();
|
|
169
|
+
|
|
170
|
+
ctx.body = {
|
|
171
|
+
success: true,
|
|
172
|
+
stats,
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error("获取队列状态失败:", error);
|
|
177
|
+
ctx.status = 500;
|
|
178
|
+
ctx.body = {
|
|
179
|
+
success: false,
|
|
180
|
+
error: error.message,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 新增:清空队列接口(需要认证)
|
|
186
|
+
router.post("/api/queue/clear", authenticateAPI, async (ctx) => {
|
|
187
|
+
try {
|
|
188
|
+
await taskQueue.clear();
|
|
189
|
+
|
|
190
|
+
ctx.body = {
|
|
191
|
+
success: true,
|
|
192
|
+
message: "队列已清空",
|
|
193
|
+
};
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger.error("清空队列失败:", error);
|
|
196
|
+
ctx.status = 500;
|
|
197
|
+
ctx.body = {
|
|
198
|
+
success: false,
|
|
199
|
+
error: error.message,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 新增:暂停队列接口(需要认证)
|
|
205
|
+
router.post("/api/queue/pause", authenticateAPI, async (ctx) => {
|
|
206
|
+
try {
|
|
207
|
+
await taskQueue.pause();
|
|
208
|
+
|
|
209
|
+
ctx.body = {
|
|
210
|
+
success: true,
|
|
211
|
+
message: "队列已暂停",
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
logger.error("暂停队列失败:", error);
|
|
215
|
+
ctx.status = 500;
|
|
216
|
+
ctx.body = {
|
|
217
|
+
success: false,
|
|
218
|
+
error: error.message,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 新增:恢复队列接口(需要认证)
|
|
224
|
+
router.post("/api/queue/resume", authenticateAPI, async (ctx) => {
|
|
225
|
+
try {
|
|
226
|
+
await taskQueue.resume();
|
|
227
|
+
|
|
228
|
+
ctx.body = {
|
|
229
|
+
success: true,
|
|
230
|
+
message: "队列已恢复",
|
|
231
|
+
};
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.error("恢复队列失败:", error);
|
|
234
|
+
ctx.status = 500;
|
|
235
|
+
ctx.body = {
|
|
236
|
+
success: false,
|
|
237
|
+
error: error.message,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 服务启动时初始化队列
|
|
245
|
+
*/
|
|
246
|
+
async function startServer() {
|
|
247
|
+
// ... 其他初始化代码 ...
|
|
248
|
+
|
|
249
|
+
// 初始化任务队列
|
|
250
|
+
await initializeQueue();
|
|
251
|
+
|
|
252
|
+
// ... 启动服务器 ...
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 优雅关闭
|
|
257
|
+
*/
|
|
258
|
+
async function gracefulShutdown() {
|
|
259
|
+
logger.info('📴 开始优雅关闭...');
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
// 关闭队列
|
|
263
|
+
await taskQueue.close();
|
|
264
|
+
logger.info('✅ 队列已关闭');
|
|
265
|
+
|
|
266
|
+
// ... 其他清理工作 ...
|
|
267
|
+
|
|
268
|
+
process.exit(0);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger.error('关闭时发生错误:', error);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 监听退出信号
|
|
276
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
277
|
+
process.on('SIGINT', gracefulShutdown);
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
initializeQueue,
|
|
281
|
+
submitTask,
|
|
282
|
+
executeFixTask,
|
|
283
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
const { logger } = require('../logger');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 任务队列管理器
|
|
6
|
+
* 支持 Redis 队列和本地降级队列
|
|
7
|
+
*/
|
|
8
|
+
class TaskQueue {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.redisClient = null;
|
|
11
|
+
this.localQueue = []; // 本地队列降级方案
|
|
12
|
+
this.processing = false;
|
|
13
|
+
this.maxConcurrent = 3; // 最大并发任务数
|
|
14
|
+
this.currentConcurrent = 0;
|
|
15
|
+
this.useRedis = false;
|
|
16
|
+
|
|
17
|
+
this.initialize();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 初始化队列
|
|
22
|
+
*/
|
|
23
|
+
async initialize() {
|
|
24
|
+
try {
|
|
25
|
+
// 尝试连接 Redis
|
|
26
|
+
const redisConfig = config.get('redis');
|
|
27
|
+
|
|
28
|
+
if (redisConfig && redisConfig.host) {
|
|
29
|
+
await this.initializeRedis(redisConfig);
|
|
30
|
+
} else {
|
|
31
|
+
logger.info('📋 未配置 Redis,使用本地任务队列');
|
|
32
|
+
this.useLocalQueue();
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.warn(`⚠️ Redis 连接失败,降级使用本地队列: ${error.message}`);
|
|
36
|
+
this.useLocalQueue();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 初始化 Redis 队列
|
|
42
|
+
*/
|
|
43
|
+
async initializeRedis(redisConfig) {
|
|
44
|
+
try {
|
|
45
|
+
// 动态加载 Redis 相关依赖
|
|
46
|
+
const Redis = require('ioredis');
|
|
47
|
+
const Queue = require('bull');
|
|
48
|
+
|
|
49
|
+
// 创建 Redis 客户端
|
|
50
|
+
this.redisClient = new Redis({
|
|
51
|
+
host: redisConfig.host,
|
|
52
|
+
port: redisConfig.port,
|
|
53
|
+
password: redisConfig.password,
|
|
54
|
+
retryStrategy: (times) => {
|
|
55
|
+
if (times > 3) {
|
|
56
|
+
logger.error('Redis 连接失败次数过多,切换到本地队列');
|
|
57
|
+
this.useLocalQueue();
|
|
58
|
+
return null; // 停止重试
|
|
59
|
+
}
|
|
60
|
+
return Math.min(times * 1000, 3000);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 测试连接
|
|
65
|
+
await this.redisClient.ping();
|
|
66
|
+
|
|
67
|
+
// 创建 Bull 队列
|
|
68
|
+
this.queue = new Queue('auto-fix-tasks', {
|
|
69
|
+
redis: redisConfig,
|
|
70
|
+
defaultJobOptions: {
|
|
71
|
+
attempts: 3,
|
|
72
|
+
backoff: {
|
|
73
|
+
type: 'exponential',
|
|
74
|
+
delay: 2000,
|
|
75
|
+
},
|
|
76
|
+
removeOnComplete: 100, // 保留最近 100 个已完成任务
|
|
77
|
+
removeOnFail: 50, // 保留最近 50 个失败任务
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 监听队列事件
|
|
82
|
+
this.queue.on('error', (error) => {
|
|
83
|
+
logger.error('队列错误:', error);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.queue.on('failed', (job, error) => {
|
|
87
|
+
logger.error(`任务失败: ${job.id}`, error);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.queue.on('completed', (job) => {
|
|
91
|
+
logger.info(`✅ 队列任务完成: ${job.id}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.useRedis = true;
|
|
95
|
+
logger.info('✅ Redis 队列已启用');
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === 'MODULE_NOT_FOUND') {
|
|
99
|
+
logger.warn('⚠️ 未安装 Redis 依赖 (ioredis, bull),使用本地队列');
|
|
100
|
+
} else {
|
|
101
|
+
logger.error(`Redis 初始化失败: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 使用本地队列
|
|
109
|
+
*/
|
|
110
|
+
useLocalQueue() {
|
|
111
|
+
this.useRedis = false;
|
|
112
|
+
this.localQueue = [];
|
|
113
|
+
logger.info('✅ 本地任务队列已启用');
|
|
114
|
+
|
|
115
|
+
// 启动本地队列处理器
|
|
116
|
+
this.startLocalQueueProcessor();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 启动本地队列处理器
|
|
121
|
+
*/
|
|
122
|
+
startLocalQueueProcessor() {
|
|
123
|
+
if (this.processing) return;
|
|
124
|
+
|
|
125
|
+
this.processing = true;
|
|
126
|
+
|
|
127
|
+
// 每秒检查一次队列
|
|
128
|
+
setInterval(() => {
|
|
129
|
+
this.processLocalQueue();
|
|
130
|
+
}, 1000);
|
|
131
|
+
|
|
132
|
+
logger.info('🔄 本地队列处理器已启动');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 处理本地队列
|
|
137
|
+
*/
|
|
138
|
+
async processLocalQueue() {
|
|
139
|
+
// 如果没有待处理任务或并发已满,跳过
|
|
140
|
+
if (this.localQueue.length === 0 || this.currentConcurrent >= this.maxConcurrent) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 获取队列头部任务
|
|
145
|
+
const job = this.localQueue.shift();
|
|
146
|
+
|
|
147
|
+
if (!job) return;
|
|
148
|
+
|
|
149
|
+
this.currentConcurrent++;
|
|
150
|
+
logger.info(`▶️ 开始处理本地队列任务: ${job.id} (并发: ${this.currentConcurrent}/${this.maxConcurrent})`);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// 执行任务处理函数
|
|
154
|
+
if (this.processor) {
|
|
155
|
+
await this.processor(job);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error(`本地队列任务失败: ${job.id}`, error);
|
|
159
|
+
|
|
160
|
+
// 重试逻辑
|
|
161
|
+
if (job.attempts < 3) {
|
|
162
|
+
job.attempts++;
|
|
163
|
+
logger.warn(`重试任务 ${job.id},第 ${job.attempts} 次尝试`);
|
|
164
|
+
this.localQueue.push(job); // 重新加入队列尾部
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
this.currentConcurrent--;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 添加任务到队列
|
|
173
|
+
* @param {Object} taskData - 任务数据
|
|
174
|
+
* @returns {Promise<Object>} 任务信息
|
|
175
|
+
*/
|
|
176
|
+
async addTask(taskData) {
|
|
177
|
+
if (this.useRedis && this.queue) {
|
|
178
|
+
// 使用 Redis 队列
|
|
179
|
+
try {
|
|
180
|
+
const job = await this.queue.add(taskData, {
|
|
181
|
+
priority: taskData.priority || 0,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
logger.info(`📨 任务已加入 Redis 队列: ${job.id}`);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
id: job.id,
|
|
188
|
+
queueType: 'redis',
|
|
189
|
+
};
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.error('Redis 队列添加失败,降级到本地队列:', error);
|
|
192
|
+
// 降级到本地队列
|
|
193
|
+
return this.addToLocalQueue(taskData);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// 使用本地队列
|
|
197
|
+
return this.addToLocalQueue(taskData);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 添加到本地队列
|
|
203
|
+
*/
|
|
204
|
+
addToLocalQueue(taskData) {
|
|
205
|
+
const job = {
|
|
206
|
+
id: `local-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
207
|
+
data: taskData,
|
|
208
|
+
attempts: 0,
|
|
209
|
+
createdAt: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this.localQueue.push(job);
|
|
213
|
+
|
|
214
|
+
logger.info(`📨 任务已加入本地队列: ${job.id} (队列长度: ${this.localQueue.length})`);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
id: job.id,
|
|
218
|
+
queueType: 'local',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 设置任务处理器
|
|
224
|
+
* @param {Function} processor - 处理函数 async (job) => {}
|
|
225
|
+
*/
|
|
226
|
+
process(processor) {
|
|
227
|
+
this.processor = processor;
|
|
228
|
+
|
|
229
|
+
if (this.useRedis && this.queue) {
|
|
230
|
+
// Redis 队列处理器
|
|
231
|
+
this.queue.process(this.maxConcurrent, async (job) => {
|
|
232
|
+
logger.info(`▶️ 开始处理 Redis 队列任务: ${job.id}`);
|
|
233
|
+
return await processor(job);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// 本地队列已经在 startLocalQueueProcessor 中处理
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 获取队列统计信息
|
|
241
|
+
*/
|
|
242
|
+
async getStats() {
|
|
243
|
+
if (this.useRedis && this.queue) {
|
|
244
|
+
try {
|
|
245
|
+
const [waiting, active, completed, failed] = await Promise.all([
|
|
246
|
+
this.queue.getWaitingCount(),
|
|
247
|
+
this.queue.getActiveCount(),
|
|
248
|
+
this.queue.getCompletedCount(),
|
|
249
|
+
this.queue.getFailedCount(),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
queueType: 'redis',
|
|
254
|
+
waiting,
|
|
255
|
+
active,
|
|
256
|
+
completed,
|
|
257
|
+
failed,
|
|
258
|
+
total: waiting + active + completed + failed,
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger.error('获取 Redis 队列状态失败:', error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 本地队列统计
|
|
266
|
+
return {
|
|
267
|
+
queueType: 'local',
|
|
268
|
+
waiting: this.localQueue.length,
|
|
269
|
+
active: this.currentConcurrent,
|
|
270
|
+
completed: 0, // 本地队列不保存历史
|
|
271
|
+
failed: 0,
|
|
272
|
+
total: this.localQueue.length + this.currentConcurrent,
|
|
273
|
+
maxConcurrent: this.maxConcurrent,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 清空队列
|
|
279
|
+
*/
|
|
280
|
+
async clear() {
|
|
281
|
+
if (this.useRedis && this.queue) {
|
|
282
|
+
await this.queue.empty();
|
|
283
|
+
logger.info('🧹 Redis 队列已清空');
|
|
284
|
+
} else {
|
|
285
|
+
this.localQueue = [];
|
|
286
|
+
logger.info('🧹 本地队列已清空');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 暂停队列
|
|
292
|
+
*/
|
|
293
|
+
async pause() {
|
|
294
|
+
if (this.useRedis && this.queue) {
|
|
295
|
+
await this.queue.pause();
|
|
296
|
+
logger.info('⏸️ Redis 队列已暂停');
|
|
297
|
+
} else {
|
|
298
|
+
this.processing = false;
|
|
299
|
+
logger.info('⏸️ 本地队列已暂停');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 恢复队列
|
|
305
|
+
*/
|
|
306
|
+
async resume() {
|
|
307
|
+
if (this.useRedis && this.queue) {
|
|
308
|
+
await this.queue.resume();
|
|
309
|
+
logger.info('▶️ Redis 队列已恢复');
|
|
310
|
+
} else {
|
|
311
|
+
this.processing = true;
|
|
312
|
+
logger.info('▶️ 本地队列已恢复');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 关闭队列
|
|
318
|
+
*/
|
|
319
|
+
async close() {
|
|
320
|
+
if (this.useRedis && this.queue) {
|
|
321
|
+
await this.queue.close();
|
|
322
|
+
if (this.redisClient) {
|
|
323
|
+
await this.redisClient.quit();
|
|
324
|
+
}
|
|
325
|
+
logger.info('👋 Redis 队列已关闭');
|
|
326
|
+
} else {
|
|
327
|
+
this.processing = false;
|
|
328
|
+
logger.info('👋 本地队列已关闭');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = new TaskQueue();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通知限流器
|
|
3
|
+
* 防止短时间内通知过多骚扰用户
|
|
4
|
+
*/
|
|
5
|
+
class NotificationLimiter {
|
|
6
|
+
constructor(maxPerMinute = 5) {
|
|
7
|
+
this.queue = [];
|
|
8
|
+
this.maxPerMinute = maxPerMinute;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 检查是否允许发送通知
|
|
13
|
+
* @returns {boolean} 是否允许发送
|
|
14
|
+
*/
|
|
15
|
+
canNotify() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
|
|
18
|
+
// 清理 1 分钟前的记录
|
|
19
|
+
this.queue = this.queue.filter(timestamp => now - timestamp < 60000);
|
|
20
|
+
|
|
21
|
+
// 检查是否超过限制
|
|
22
|
+
if (this.queue.length >= this.maxPerMinute) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 记录当前通知
|
|
27
|
+
this.queue.push(now);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 重置限流器
|
|
33
|
+
*/
|
|
34
|
+
reset() {
|
|
35
|
+
this.queue = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 获取当前队列长度
|
|
40
|
+
*/
|
|
41
|
+
getQueueLength() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
this.queue = this.queue.filter(timestamp => now - timestamp < 60000);
|
|
44
|
+
return this.queue.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = NotificationLimiter;
|