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,246 @@
1
+ const { logger } = require('../logger');
2
+
3
+ /**
4
+ * 项目锁管理器
5
+ * 用于防止同一项目的多个任务并发执行,避免 Git 冲突
6
+ */
7
+ class ProjectLockManager {
8
+ constructor() {
9
+ // 项目锁映射:{ projectPath: { taskId, promise, waitingTasks: [] } }
10
+ this.locks = new Map();
11
+
12
+ // 统计信息
13
+ this.stats = {
14
+ totalLocks: 0,
15
+ currentLocks: 0,
16
+ waitingTasks: 0,
17
+ maxWaitTime: 0,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * 获取项目的唯一标识
23
+ * @param {string} projectPath - 项目路径
24
+ * @returns {string} 标准化的项目标识
25
+ */
26
+ getProjectKey(projectPath) {
27
+ if (!projectPath) {
28
+ return null;
29
+ }
30
+
31
+ // 标准化路径(处理 Windows/Linux 路径差异)
32
+ const normalized = projectPath
33
+ .replace(/\\/g, '/') // 统一使用 /
34
+ .toLowerCase() // 不区分大小写
35
+ .replace(/\/$/, ''); // 移除尾部斜杠
36
+
37
+ return normalized;
38
+ }
39
+
40
+ /**
41
+ * 尝试获取项目锁
42
+ * @param {string} projectPath - 项目路径
43
+ * @param {string} taskId - 任务ID
44
+ * @returns {Promise<void>} 获取锁成功后 resolve
45
+ */
46
+ async acquireLock(projectPath, taskId) {
47
+ const projectKey = this.getProjectKey(projectPath);
48
+
49
+ if (!projectKey) {
50
+ // 没有项目路径,直接通过(如首次克隆)
51
+ return;
52
+ }
53
+
54
+ const existingLock = this.locks.get(projectKey);
55
+
56
+ if (!existingLock) {
57
+ // 没有锁,直接获取
58
+ logger.info(`🔓 项目锁已获取: ${projectKey}`, { taskId, projectPath });
59
+
60
+ this.locks.set(projectKey, {
61
+ taskId,
62
+ projectPath,
63
+ acquiredAt: Date.now(),
64
+ waitingTasks: [],
65
+ });
66
+
67
+ this.stats.totalLocks++;
68
+ this.stats.currentLocks++;
69
+
70
+ return;
71
+ }
72
+
73
+ // 已有锁,需要等待
74
+ logger.warn(`⏳ 项目已被锁定,任务进入等待队列`, {
75
+ taskId,
76
+ projectPath,
77
+ lockHolder: existingLock.taskId,
78
+ waitingCount: existingLock.waitingTasks.length,
79
+ });
80
+
81
+ this.stats.waitingTasks++;
82
+
83
+ // 创建一个 Promise,等待前面的任务完成
84
+ return new Promise((resolve) => {
85
+ const waitStartTime = Date.now();
86
+
87
+ existingLock.waitingTasks.push({
88
+ taskId,
89
+ projectPath,
90
+ resolve,
91
+ waitStartTime,
92
+ });
93
+
94
+ logger.info(`📋 任务已加入等待队列 (位置: ${existingLock.waitingTasks.length})`, {
95
+ taskId,
96
+ projectPath,
97
+ });
98
+ });
99
+ }
100
+
101
+ /**
102
+ * 释放项目锁
103
+ * @param {string} projectPath - 项目路径
104
+ * @param {string} taskId - 任务ID
105
+ */
106
+ releaseLock(projectPath, taskId) {
107
+ const projectKey = this.getProjectKey(projectPath);
108
+
109
+ if (!projectKey) {
110
+ return;
111
+ }
112
+
113
+ const lock = this.locks.get(projectKey);
114
+
115
+ if (!lock) {
116
+ logger.warn(`⚠️ 尝试释放不存在的锁`, { taskId, projectPath });
117
+ return;
118
+ }
119
+
120
+ if (lock.taskId !== taskId) {
121
+ logger.warn(`⚠️ 任务不持有此锁,无法释放`, {
122
+ taskId,
123
+ lockHolder: lock.taskId,
124
+ projectPath,
125
+ });
126
+ return;
127
+ }
128
+
129
+ const lockDuration = Date.now() - lock.acquiredAt;
130
+
131
+ logger.info(`🔓 项目锁已释放`, {
132
+ taskId,
133
+ projectPath,
134
+ duration: `${(lockDuration / 1000).toFixed(2)}s`,
135
+ waitingCount: lock.waitingTasks.length,
136
+ });
137
+
138
+ this.stats.currentLocks--;
139
+
140
+ // 如果有等待的任务,唤醒第一个
141
+ if (lock.waitingTasks.length > 0) {
142
+ const nextTask = lock.waitingTasks.shift();
143
+ this.stats.waitingTasks--;
144
+
145
+ const waitTime = Date.now() - nextTask.waitStartTime;
146
+
147
+ if (waitTime > this.stats.maxWaitTime) {
148
+ this.stats.maxWaitTime = waitTime;
149
+ }
150
+
151
+ logger.info(`🔓 唤醒等待任务`, {
152
+ taskId: nextTask.taskId,
153
+ projectPath,
154
+ waitTime: `${(waitTime / 1000).toFixed(2)}s`,
155
+ remainingWaiting: lock.waitingTasks.length,
156
+ });
157
+
158
+ // 更新锁的持有者
159
+ this.locks.set(projectKey, {
160
+ taskId: nextTask.taskId,
161
+ projectPath: nextTask.projectPath,
162
+ acquiredAt: Date.now(),
163
+ waitingTasks: lock.waitingTasks, // 保留剩余的等待队列
164
+ });
165
+
166
+ this.stats.totalLocks++;
167
+ this.stats.currentLocks++;
168
+
169
+ // 唤醒等待的任务
170
+ nextTask.resolve();
171
+ } else {
172
+ // 没有等待的任务,删除锁
173
+ this.locks.delete(projectKey);
174
+ logger.debug(`🗑️ 项目锁已删除(无等待任务)`, { projectPath });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 检查项目是否被锁定
180
+ * @param {string} projectPath - 项目路径
181
+ * @returns {Object|null} 锁信息或 null
182
+ */
183
+ isLocked(projectPath) {
184
+ const projectKey = this.getProjectKey(projectPath);
185
+
186
+ if (!projectKey) {
187
+ return null;
188
+ }
189
+
190
+ const lock = this.locks.get(projectKey);
191
+
192
+ if (!lock) {
193
+ return null;
194
+ }
195
+
196
+ return {
197
+ locked: true,
198
+ taskId: lock.taskId,
199
+ waitingCount: lock.waitingTasks.length,
200
+ duration: Date.now() - lock.acquiredAt,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * 获取统计信息
206
+ * @returns {Object} 统计数据
207
+ */
208
+ getStats() {
209
+ return {
210
+ ...this.stats,
211
+ activeLocks: this.locks.size,
212
+ locks: Array.from(this.locks.entries()).map(([projectKey, lock]) => ({
213
+ projectKey,
214
+ taskId: lock.taskId,
215
+ waitingCount: lock.waitingTasks.length,
216
+ duration: Date.now() - lock.acquiredAt,
217
+ })),
218
+ };
219
+ }
220
+
221
+ /**
222
+ * 强制释放所有锁(用于紧急情况)
223
+ */
224
+ releaseAll() {
225
+ logger.warn('⚠️ 强制释放所有项目锁');
226
+
227
+ for (const [projectKey, lock] of this.locks.entries()) {
228
+ logger.warn(`🔓 强制释放锁: ${projectKey}`, {
229
+ taskId: lock.taskId,
230
+ waitingCount: lock.waitingTasks.length,
231
+ });
232
+
233
+ // 拒绝所有等待的任务
234
+ lock.waitingTasks.forEach((waitingTask) => {
235
+ logger.error(`❌ 等待任务被强制中止: ${waitingTask.taskId}`);
236
+ });
237
+ }
238
+
239
+ this.locks.clear();
240
+ this.stats.currentLocks = 0;
241
+ this.stats.waitingTasks = 0;
242
+ }
243
+ }
244
+
245
+ // 单例模式
246
+ module.exports = new ProjectLockManager();
@@ -0,0 +1,110 @@
1
+ const { logger } = require('../logger');
2
+
3
+ /**
4
+ * 重试工具类
5
+ * 提供带重试机制的异步函数执行
6
+ */
7
+ class RetryUtil {
8
+ /**
9
+ * 带重试的异步函数执行
10
+ * @param {Function} fn - 要执行的异步函数
11
+ * @param {Object} options - 配置选项
12
+ * @param {number} options.maxRetries - 最大重试次数,默认 3
13
+ * @param {number} options.delay - 初始延迟时间(毫秒),默认 1000
14
+ * @param {boolean} options.exponentialBackoff - 是否使用指数退避,默认 true
15
+ * @param {string} options.operation - 操作名称(用于日志),默认 '操作'
16
+ * @param {Function} options.shouldRetry - 判断是否应该重试的函数,默认总是重试
17
+ * @returns {Promise<*>} 函数执行结果
18
+ */
19
+ static async withRetry(fn, options = {}) {
20
+ const {
21
+ maxRetries = 3,
22
+ delay = 1000,
23
+ exponentialBackoff = true,
24
+ operation = '操作',
25
+ shouldRetry = () => true,
26
+ } = options;
27
+
28
+ let lastError;
29
+
30
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
31
+ try {
32
+ return await fn();
33
+ } catch (error) {
34
+ lastError = error;
35
+
36
+ // 判断是否应该重试
37
+ if (!shouldRetry(error)) {
38
+ logger.error(`${operation}失败且不可重试`, {
39
+ error: error.message,
40
+ attempt,
41
+ });
42
+ throw error;
43
+ }
44
+
45
+ // 如果已达最大重试次数,抛出错误
46
+ if (attempt === maxRetries) {
47
+ logger.error(`${operation}失败,已达最大重试次数 ${maxRetries}`, {
48
+ error: error.message,
49
+ });
50
+ throw error;
51
+ }
52
+
53
+ // 计算等待时间
54
+ const waitTime = exponentialBackoff
55
+ ? delay * Math.pow(2, attempt - 1)
56
+ : delay;
57
+
58
+ logger.warn(
59
+ `${operation}失败,${waitTime}ms 后进行第 ${attempt}/${maxRetries} 次重试`,
60
+ { error: error.message }
61
+ );
62
+
63
+ // 等待后重试
64
+ await new Promise(resolve => setTimeout(resolve, waitTime));
65
+ }
66
+ }
67
+
68
+ // 理论上不会到这里,但为了类型安全
69
+ throw lastError;
70
+ }
71
+
72
+ /**
73
+ * 带超时的异步函数执行
74
+ * @param {Function} fn - 要执行的异步函数
75
+ * @param {number} timeoutMs - 超时时间(毫秒)
76
+ * @param {string} operation - 操作名称
77
+ * @returns {Promise<*>} 函数执行结果
78
+ */
79
+ static async withTimeout(fn, timeoutMs, operation = '操作') {
80
+ return Promise.race([
81
+ fn(),
82
+ new Promise((_, reject) =>
83
+ setTimeout(() => {
84
+ reject(new Error(`${operation}超时 (${timeoutMs}ms)`));
85
+ }, timeoutMs)
86
+ ),
87
+ ]);
88
+ }
89
+
90
+ /**
91
+ * 带重试和超时的异步函数执行
92
+ * @param {Function} fn - 要执行的异步函数
93
+ * @param {Object} options - 配置选项
94
+ * @returns {Promise<*>} 函数执行结果
95
+ */
96
+ static async withRetryAndTimeout(fn, options = {}) {
97
+ const {
98
+ timeoutMs = 30000,
99
+ operation = '操作',
100
+ ...retryOptions
101
+ } = options;
102
+
103
+ return this.withRetry(
104
+ () => this.withTimeout(fn, timeoutMs, operation),
105
+ { ...retryOptions, operation }
106
+ );
107
+ }
108
+ }
109
+
110
+ module.exports = RetryUtil;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 输入清理工具
3
+ * 用于验证和清理用户输入,防止安全漏洞
4
+ */
5
+ class InputSanitizer {
6
+ /**
7
+ * 清理需求描述
8
+ * @param {string} requirement - 需求描述
9
+ * @returns {string} 清理后的需求描述
10
+ */
11
+ static sanitizeRequirement(requirement) {
12
+ if (typeof requirement !== 'string') {
13
+ throw new Error('需求描述必须是字符串');
14
+ }
15
+
16
+ // 长度限制
17
+ if (requirement.length === 0) {
18
+ throw new Error('需求描述不能为空');
19
+ }
20
+
21
+ if (requirement.length > 2000) {
22
+ throw new Error('需求描述过长,最多 2000 字符');
23
+ }
24
+
25
+ // 移除危险字符
26
+ return requirement
27
+ .replace(/[`$]/g, '') // 移除反引号和美元符号,防止命令注入
28
+ .replace(/\n{3,}/g, '\n\n') // 限制连续换行
29
+ .trim();
30
+ }
31
+
32
+ /**
33
+ * 清理用户名(用于分支命名)
34
+ * @param {string} username - 用户名
35
+ * @returns {string} 清理后的用户名
36
+ */
37
+ static sanitizeUsername(username) {
38
+ if (typeof username !== 'string') {
39
+ return 'system';
40
+ }
41
+
42
+ // 只允许字母、数字、下划线、连字符
43
+ const cleaned = username.replace(/[^a-zA-Z0-9_-]/g, '_');
44
+
45
+ if (cleaned.length === 0 || cleaned.length > 50) {
46
+ return 'system';
47
+ }
48
+
49
+ return cleaned.toLowerCase();
50
+ }
51
+
52
+ /**
53
+ * 清理分支名称
54
+ * @param {string} name - 分支名称
55
+ * @returns {string} 清理后的分支名称
56
+ */
57
+ static sanitizeBranchName(name) {
58
+ // Git 分支名安全字符
59
+ return name
60
+ .replace(/[^a-zA-Z0-9_\-\/]/g, '_')
61
+ .replace(/_{2,}/g, '_')
62
+ .replace(/^[_\-]+|[_\-]+$/g, '');
63
+ }
64
+
65
+ /**
66
+ * 清理项目路径
67
+ * @param {string} projectPath - 项目路径
68
+ * @returns {string} 清理后的项目路径
69
+ */
70
+ static sanitizeProjectPath(projectPath) {
71
+ if (typeof projectPath !== 'string') {
72
+ throw new Error('项目路径必须是字符串');
73
+ }
74
+
75
+ // 防止路径遍历攻击
76
+ if (projectPath.includes('..')) {
77
+ throw new Error('项目路径不能包含 ..');
78
+ }
79
+
80
+ // Windows 路径长度限制
81
+ if (projectPath.length > 260) {
82
+ throw new Error('项目路径过长');
83
+ }
84
+
85
+ return projectPath.trim();
86
+ }
87
+
88
+ /**
89
+ * 清理仓库 URL
90
+ * @param {string} repoUrl - 仓库地址
91
+ * @returns {string} 清理后的仓库地址
92
+ */
93
+ static sanitizeRepoUrl(repoUrl) {
94
+ if (typeof repoUrl !== 'string') {
95
+ throw new Error('仓库地址必须是字符串');
96
+ }
97
+
98
+ const trimmed = repoUrl.trim();
99
+
100
+ // 支持多种 Git 仓库地址格式:
101
+ // 1. HTTP/HTTPS: https://github.com/user/repo.git
102
+ // 2. Git 协议: git://github.com/user/repo.git
103
+ // 3. SSH (git@): git@github.com:user/repo.git
104
+ // 4. SSH (ssh://): ssh://git@github.com/user/repo.git
105
+ const patterns = [
106
+ /^https?:\/\/.+/i, // HTTP/HTTPS
107
+ /^git:\/\/.+/i, // Git 协议
108
+ /^git@[\w\.\-]+:.+/i, // SSH git@
109
+ /^ssh:\/\/(git@)?[\w\.\-]+\/.+/i // SSH ssh://
110
+ ];
111
+
112
+ const isValid = patterns.some(pattern => pattern.test(trimmed));
113
+
114
+ if (!isValid) {
115
+ throw new Error('无效的仓库地址格式,支持格式:https://、git://、git@ 或 ssh://');
116
+ }
117
+
118
+ return trimmed;
119
+ }
120
+
121
+ /**
122
+ * 验证 AI 引擎
123
+ * @param {string} engine - AI 引擎名称
124
+ * @returns {string} 验证后的引擎名称
125
+ */
126
+ static validateAIEngine(engine) {
127
+ const validEngines = ['claude-cli', 'zhipu', 'claude'];
128
+
129
+ if (engine && !validEngines.includes(engine)) {
130
+ throw new Error(`无效的 AI 引擎: ${engine},支持的引擎: ${validEngines.join(', ')}`);
131
+ }
132
+
133
+ return engine;
134
+ }
135
+
136
+ /**
137
+ * 验证基准分支名称
138
+ * @param {string} branch - 分支名称
139
+ * @returns {string} 验证后的分支名称
140
+ */
141
+ static validateBranchName(branch) {
142
+ if (!branch) {
143
+ return null;
144
+ }
145
+
146
+ if (typeof branch !== 'string') {
147
+ throw new Error('分支名称必须是字符串');
148
+ }
149
+
150
+ // Git 分支名规则
151
+ const invalidPatterns = [
152
+ /\.\./, // 不能包含 ..
153
+ /^[.-]/, // 不能以 . 或 - 开头
154
+ /[\/.]$/, // 不能以 / 或 . 结尾
155
+ /\/\//, // 不能包含连续的 /
156
+ /[@{\\\^~:?*\[\]]/, // 不能包含特殊字符
157
+ ];
158
+
159
+ for (const pattern of invalidPatterns) {
160
+ if (pattern.test(branch)) {
161
+ throw new Error(`无效的分支名称: ${branch}`);
162
+ }
163
+ }
164
+
165
+ // 长度限制
166
+ if (branch.length > 255) {
167
+ throw new Error('分支名称过长,最多 255 字符');
168
+ }
169
+
170
+ return branch.trim();
171
+ }
172
+ }
173
+
174
+ module.exports = InputSanitizer;