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.
Files changed (52) hide show
  1. package/.env.example +69 -0
  2. package/README.md +1590 -0
  3. package/bin/cfix +14 -0
  4. package/bin/cfix.cmd +6 -0
  5. package/cli/commands/config.js +58 -0
  6. package/cli/commands/doctor.js +240 -0
  7. package/cli/commands/fix.js +211 -0
  8. package/cli/commands/help.js +62 -0
  9. package/cli/commands/init.js +226 -0
  10. package/cli/commands/logs.js +161 -0
  11. package/cli/commands/monitor.js +151 -0
  12. package/cli/commands/project.js +331 -0
  13. package/cli/commands/service.js +133 -0
  14. package/cli/commands/status.js +115 -0
  15. package/cli/commands/task.js +412 -0
  16. package/cli/commands/version.js +19 -0
  17. package/cli/index.js +269 -0
  18. package/cli/lib/config-manager.js +612 -0
  19. package/cli/lib/formatter.js +224 -0
  20. package/cli/lib/process-manager.js +233 -0
  21. package/cli/lib/service-client.js +271 -0
  22. package/cli/scripts/install-completion.js +133 -0
  23. package/package.json +85 -0
  24. package/public/monitor.html +1096 -0
  25. package/scripts/completion.bash +87 -0
  26. package/scripts/completion.zsh +102 -0
  27. package/src/assets/README.md +32 -0
  28. package/src/assets/error.png +0 -0
  29. package/src/assets/icon.png +0 -0
  30. package/src/assets/success.png +0 -0
  31. package/src/claude-cli-service.js +216 -0
  32. package/src/config/index.js +69 -0
  33. package/src/database/manager.js +391 -0
  34. package/src/database/migration.js +252 -0
  35. package/src/git-service.js +1278 -0
  36. package/src/index.js +1658 -0
  37. package/src/logger.js +139 -0
  38. package/src/metrics/collector.js +184 -0
  39. package/src/middleware/auth.js +86 -0
  40. package/src/middleware/rate-limit.js +85 -0
  41. package/src/queue/integration-example.js +283 -0
  42. package/src/queue/task-queue.js +333 -0
  43. package/src/services/notification-limiter.js +48 -0
  44. package/src/services/notification-service.js +115 -0
  45. package/src/services/system-notifier.js +130 -0
  46. package/src/task-manager.js +289 -0
  47. package/src/utils/exec.js +87 -0
  48. package/src/utils/project-lock.js +246 -0
  49. package/src/utils/retry.js +110 -0
  50. package/src/utils/sanitizer.js +174 -0
  51. package/src/websocket/notifier.js +363 -0
  52. package/src/wechat-notifier.js +97 -0
@@ -0,0 +1,363 @@
1
+ const WebSocket = require('ws');
2
+ const { logger } = require('../logger');
3
+ const EventEmitter = require('events');
4
+
5
+ /**
6
+ * WebSocket 通知服务
7
+ * 支持任务状态实时推送
8
+ */
9
+ class WebSocketNotifier extends EventEmitter {
10
+ constructor() {
11
+ super();
12
+ this.wss = null;
13
+ this.clients = new Map(); // taskId -> Set<ws>
14
+ this.heartbeatInterval = null;
15
+ }
16
+
17
+ /**
18
+ * 初始化 WebSocket 服务器
19
+ * @param {Object} server - HTTP 服务器实例
20
+ */
21
+ initialize(server) {
22
+ this.wss = new WebSocket.Server({
23
+ server,
24
+ path: '/ws',
25
+ verifyClient: (info, callback) => {
26
+ // 可选:验证客户端连接
27
+ const apiKey = new URL(info.req.url, 'ws://localhost').searchParams.get('apiKey');
28
+
29
+ // 如果配置了 API_KEY,则验证
30
+ if (process.env.API_KEY && apiKey !== process.env.API_KEY) {
31
+ callback(false, 401, 'Unauthorized');
32
+ return;
33
+ }
34
+
35
+ callback(true);
36
+ }
37
+ });
38
+
39
+ this.wss.on('connection', (ws, req) => {
40
+ this.handleConnection(ws, req);
41
+ });
42
+
43
+ // 启动心跳检测
44
+ this.startHeartbeat();
45
+
46
+ logger.info('✅ WebSocket 服务已启动: /ws');
47
+ }
48
+
49
+ /**
50
+ * 处理客户端连接
51
+ */
52
+ handleConnection(ws, req) {
53
+ const url = new URL(req.url, 'ws://localhost');
54
+ const taskId = url.searchParams.get('taskId');
55
+ const clientId = Math.random().toString(36).substr(2, 9);
56
+
57
+ ws.isAlive = true;
58
+ ws.clientId = clientId;
59
+ ws.subscribedTasks = new Set();
60
+
61
+ logger.info(`WebSocket 客户端已连接: ${clientId}`, { taskId });
62
+
63
+ // 如果指定了 taskId,自动订阅
64
+ if (taskId) {
65
+ this.subscribe(ws, taskId);
66
+ }
67
+
68
+ // 发送欢迎消息
69
+ this.send(ws, {
70
+ type: 'connected',
71
+ clientId,
72
+ timestamp: new Date().toISOString(),
73
+ });
74
+
75
+ // 心跳响应
76
+ ws.on('pong', () => {
77
+ ws.isAlive = true;
78
+ });
79
+
80
+ // 处理客户端消息
81
+ ws.on('message', (message) => {
82
+ this.handleMessage(ws, message);
83
+ });
84
+
85
+ // 处理断开连接
86
+ ws.on('close', () => {
87
+ this.handleDisconnect(ws);
88
+ });
89
+
90
+ // 处理错误
91
+ ws.on('error', (error) => {
92
+ logger.error(`WebSocket 客户端错误: ${clientId}`, error);
93
+ });
94
+ }
95
+
96
+ /**
97
+ * 处理客户端消息
98
+ */
99
+ handleMessage(ws, message) {
100
+ try {
101
+ const data = JSON.parse(message);
102
+
103
+ switch (data.type) {
104
+ case 'subscribe':
105
+ // 订阅任务
106
+ if (data.taskId) {
107
+ this.subscribe(ws, data.taskId);
108
+ this.send(ws, {
109
+ type: 'subscribed',
110
+ taskId: data.taskId,
111
+ timestamp: new Date().toISOString(),
112
+ });
113
+ }
114
+ break;
115
+
116
+ case 'unsubscribe':
117
+ // 取消订阅任务
118
+ if (data.taskId) {
119
+ this.unsubscribe(ws, data.taskId);
120
+ this.send(ws, {
121
+ type: 'unsubscribed',
122
+ taskId: data.taskId,
123
+ timestamp: new Date().toISOString(),
124
+ });
125
+ }
126
+ break;
127
+
128
+ case 'ping':
129
+ // 心跳
130
+ this.send(ws, {
131
+ type: 'pong',
132
+ timestamp: new Date().toISOString(),
133
+ });
134
+ break;
135
+
136
+ default:
137
+ logger.warn(`未知的消息类型: ${data.type}`);
138
+ }
139
+ } catch (error) {
140
+ logger.error('处理 WebSocket 消息失败:', error);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 订阅任务
146
+ */
147
+ subscribe(ws, taskId) {
148
+ if (!this.clients.has(taskId)) {
149
+ this.clients.set(taskId, new Set());
150
+ }
151
+
152
+ this.clients.get(taskId).add(ws);
153
+ ws.subscribedTasks.add(taskId);
154
+
155
+ logger.debug(`客户端 ${ws.clientId} 订阅任务: ${taskId}`);
156
+ }
157
+
158
+ /**
159
+ * 取消订阅任务
160
+ */
161
+ unsubscribe(ws, taskId) {
162
+ if (this.clients.has(taskId)) {
163
+ this.clients.get(taskId).delete(ws);
164
+
165
+ // 如果没有订阅者,删除任务记录
166
+ if (this.clients.get(taskId).size === 0) {
167
+ this.clients.delete(taskId);
168
+ }
169
+ }
170
+
171
+ ws.subscribedTasks.delete(taskId);
172
+
173
+ logger.debug(`客户端 ${ws.clientId} 取消订阅任务: ${taskId}`);
174
+ }
175
+
176
+ /**
177
+ * 处理客户端断开
178
+ */
179
+ handleDisconnect(ws) {
180
+ // 取消所有订阅
181
+ ws.subscribedTasks.forEach(taskId => {
182
+ this.unsubscribe(ws, taskId);
183
+ });
184
+
185
+ logger.info(`WebSocket 客户端已断开: ${ws.clientId}`);
186
+ }
187
+
188
+ /**
189
+ * 发送消息到客户端
190
+ */
191
+ send(ws, data) {
192
+ if (ws.readyState === WebSocket.OPEN) {
193
+ try {
194
+ ws.send(JSON.stringify(data));
195
+ } catch (error) {
196
+ logger.error('发送 WebSocket 消息失败:', error);
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * 广播消息到所有订阅者
203
+ */
204
+ broadcast(taskId, data) {
205
+ const clients = this.clients.get(taskId);
206
+
207
+ if (!clients || clients.size === 0) {
208
+ return;
209
+ }
210
+
211
+ const message = {
212
+ ...data,
213
+ taskId,
214
+ timestamp: new Date().toISOString(),
215
+ };
216
+
217
+ clients.forEach(ws => {
218
+ this.send(ws, message);
219
+ });
220
+
221
+ logger.debug(`广播消息到 ${clients.size} 个订阅者`, { taskId, type: data.type });
222
+ }
223
+
224
+ /**
225
+ * 通知任务状态变更
226
+ */
227
+ notifyTaskStatus(taskId, status, data = {}) {
228
+ this.broadcast(taskId, {
229
+ type: 'task_status',
230
+ status,
231
+ ...data,
232
+ });
233
+ }
234
+
235
+ /**
236
+ * 通知任务进度
237
+ */
238
+ notifyTaskProgress(taskId, step, message, percent = null) {
239
+ this.broadcast(taskId, {
240
+ type: 'task_progress',
241
+ step,
242
+ message,
243
+ percent,
244
+ });
245
+ }
246
+
247
+ /**
248
+ * 通知任务完成
249
+ */
250
+ notifyTaskComplete(taskId, result) {
251
+ this.broadcast(taskId, {
252
+ type: 'task_complete',
253
+ result,
254
+ });
255
+ }
256
+
257
+ /**
258
+ * 通知任务失败
259
+ */
260
+ notifyTaskFailed(taskId, error) {
261
+ this.broadcast(taskId, {
262
+ type: 'task_failed',
263
+ error: error.message || String(error),
264
+ });
265
+ }
266
+
267
+ /**
268
+ * 通知队列状态
269
+ */
270
+ notifyQueueStats(stats) {
271
+ // 广播到所有连接的客户端
272
+ if (!this.wss) return;
273
+
274
+ this.wss.clients.forEach(ws => {
275
+ if (ws.readyState === WebSocket.OPEN) {
276
+ this.send(ws, {
277
+ type: 'queue_stats',
278
+ stats,
279
+ timestamp: new Date().toISOString(),
280
+ });
281
+ }
282
+ });
283
+ }
284
+
285
+ /**
286
+ * 启动心跳检测
287
+ */
288
+ startHeartbeat() {
289
+ this.heartbeatInterval = setInterval(() => {
290
+ if (!this.wss) return;
291
+
292
+ this.wss.clients.forEach(ws => {
293
+ if (ws.isAlive === false) {
294
+ logger.warn(`客户端心跳超时,断开连接: ${ws.clientId}`);
295
+ return ws.terminate();
296
+ }
297
+
298
+ ws.isAlive = false;
299
+ ws.ping();
300
+ });
301
+ }, 30000); // 30 秒心跳
302
+
303
+ logger.debug('WebSocket 心跳检测已启动');
304
+ }
305
+
306
+ /**
307
+ * 停止心跳检测
308
+ */
309
+ stopHeartbeat() {
310
+ if (this.heartbeatInterval) {
311
+ clearInterval(this.heartbeatInterval);
312
+ this.heartbeatInterval = null;
313
+ logger.debug('WebSocket 心跳检测已停止');
314
+ }
315
+ }
316
+
317
+ /**
318
+ * 获取统计信息
319
+ */
320
+ getStats() {
321
+ if (!this.wss) {
322
+ return {
323
+ enabled: false,
324
+ };
325
+ }
326
+
327
+ return {
328
+ enabled: true,
329
+ totalClients: this.wss.clients.size,
330
+ subscribedTasks: this.clients.size,
331
+ subscriptions: Array.from(this.clients.entries()).map(([taskId, clients]) => ({
332
+ taskId,
333
+ subscribers: clients.size,
334
+ })),
335
+ };
336
+ }
337
+
338
+ /**
339
+ * 关闭 WebSocket 服务器
340
+ */
341
+ close() {
342
+ this.stopHeartbeat();
343
+
344
+ if (this.wss) {
345
+ this.wss.clients.forEach(ws => {
346
+ this.send(ws, {
347
+ type: 'server_shutdown',
348
+ message: '服务器正在关闭',
349
+ timestamp: new Date().toISOString(),
350
+ });
351
+ ws.close();
352
+ });
353
+
354
+ this.wss.close(() => {
355
+ logger.info('WebSocket 服务器已关闭');
356
+ });
357
+ }
358
+
359
+ this.clients.clear();
360
+ }
361
+ }
362
+
363
+ module.exports = new WebSocketNotifier();
@@ -0,0 +1,97 @@
1
+ const axios = require('axios');
2
+ const { logger } = require('./logger');
3
+
4
+ /**
5
+ * 企业微信通知服务
6
+ */
7
+ class WeChatNotifier {
8
+ constructor(webhookUrl) {
9
+ this.webhookUrl = webhookUrl;
10
+ this.enabled = !!webhookUrl;
11
+ }
12
+
13
+ /**
14
+ * 发送 Markdown 消息到企业微信
15
+ * @param {string} content - Markdown 格式的消息内容
16
+ */
17
+ async sendMarkdown(content) {
18
+ if (!this.enabled) {
19
+ logger.info('企业微信通知未配置,跳过发送');
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const response = await axios.post(this.webhookUrl, {
25
+ msgtype: 'markdown',
26
+ markdown: {
27
+ content
28
+ }
29
+ });
30
+
31
+ if (response.data.errcode === 0) {
32
+ logger.info('✅ 企业微信通知发送成功');
33
+ } else {
34
+ logger.error('❌ 企业微信通知发送失败:', response.data);
35
+ }
36
+ } catch (error) {
37
+ logger.error('❌ 企业微信通知发送异常:', error.message);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 发送任务开始通知
43
+ */
44
+ async notifyTaskStart(projectName, requirement) {
45
+ const content = `## 🚀 自动修复任务已启动
46
+
47
+ **项目名称**: ${projectName}
48
+ **任务描述**: ${requirement}
49
+ **状态**: 进行中...
50
+ **时间**: ${new Date().toLocaleString('zh-CN')}`;
51
+
52
+ await this.sendMarkdown(content);
53
+ }
54
+
55
+ /**
56
+ * 发送任务成功通知
57
+ */
58
+ async notifyTaskSuccess(projectName, branchName, changedFiles, summary) {
59
+ const filesText = changedFiles.length > 0
60
+ ? changedFiles.map(f => `- ${f}`).join('\n')
61
+ : '无文件修改';
62
+
63
+ const content = `## ✅ 自动修复任务完成
64
+
65
+ **项目名称**: ${projectName}
66
+ **分支**: \`${branchName}\`
67
+ **修改文件数**: ${changedFiles.length}
68
+
69
+ ### 修改的文件
70
+ ${filesText}
71
+
72
+ ### 修改说明
73
+ ${summary}
74
+
75
+ **完成时间**: ${new Date().toLocaleString('zh-CN')}`;
76
+
77
+ await this.sendMarkdown(content);
78
+ }
79
+
80
+ /**
81
+ * 发送任务失败通知
82
+ */
83
+ async notifyTaskFailure(projectName, requirement, error) {
84
+ const content = `## ❌ 自动修复任务失败
85
+
86
+ **项目名称**: ${projectName}
87
+ **任务描述**: ${requirement}
88
+ **错误信息**: ${error}
89
+ **失败时间**: ${new Date().toLocaleString('zh-CN')}
90
+
91
+ 请检查日志并手动处理。`;
92
+
93
+ await this.sendMarkdown(content);
94
+ }
95
+ }
96
+
97
+ module.exports = WeChatNotifier;