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,115 @@
|
|
|
1
|
+
const SystemNotifier = require('./system-notifier');
|
|
2
|
+
const NotificationLimiter = require('./notification-limiter');
|
|
3
|
+
const { logger } = require('../logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 统一通知服务
|
|
7
|
+
* 整合系统通知、WebSocket 和企业微信通知
|
|
8
|
+
*/
|
|
9
|
+
class NotificationService {
|
|
10
|
+
constructor(config, wechatNotifier, wsNotifier) {
|
|
11
|
+
this.systemNotifier = new SystemNotifier(config.systemNotification || {});
|
|
12
|
+
this.wechatNotifier = wechatNotifier;
|
|
13
|
+
this.wsNotifier = wsNotifier;
|
|
14
|
+
this.limiter = new NotificationLimiter(5); // 每分钟最多 5 条通知
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 任务开始通知
|
|
19
|
+
*/
|
|
20
|
+
async notifyTaskStart(taskId, projectName, requirement) {
|
|
21
|
+
// WebSocket 实时推送
|
|
22
|
+
if (this.wsNotifier) {
|
|
23
|
+
this.wsNotifier.notifyTaskStatus(taskId, 'running', {
|
|
24
|
+
message: '任务已开始',
|
|
25
|
+
requirement,
|
|
26
|
+
projectName,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 企业微信通知
|
|
31
|
+
await this.wechatNotifier.notifyTaskStart(projectName, requirement);
|
|
32
|
+
|
|
33
|
+
// 系统 Toast 通知(限流)
|
|
34
|
+
if (this.limiter.canNotify()) {
|
|
35
|
+
this.systemNotifier.notifyTaskStart(projectName, requirement);
|
|
36
|
+
} else {
|
|
37
|
+
logger.warn('系统通知被限流,跳过发送', { taskId });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 任务完成通知
|
|
43
|
+
*/
|
|
44
|
+
async notifyTaskComplete(taskId, result) {
|
|
45
|
+
const { projectName, branchName, changedFiles, summary } = result;
|
|
46
|
+
|
|
47
|
+
// WebSocket 推送
|
|
48
|
+
if (this.wsNotifier) {
|
|
49
|
+
this.wsNotifier.notifyTaskComplete(taskId, result);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 企业微信通知
|
|
53
|
+
await this.wechatNotifier.notifyTaskSuccess(
|
|
54
|
+
projectName,
|
|
55
|
+
branchName,
|
|
56
|
+
changedFiles,
|
|
57
|
+
summary
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 系统 Toast 通知(限流)
|
|
61
|
+
if (this.limiter.canNotify()) {
|
|
62
|
+
this.systemNotifier.notifyTaskComplete(projectName, changedFiles);
|
|
63
|
+
} else {
|
|
64
|
+
logger.warn('系统通知被限流,跳过发送', { taskId });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 任务失败通知
|
|
70
|
+
*/
|
|
71
|
+
async notifyTaskFailure(taskId, projectName, requirement, error) {
|
|
72
|
+
// WebSocket 推送
|
|
73
|
+
if (this.wsNotifier) {
|
|
74
|
+
this.wsNotifier.notifyTaskFailed(taskId, error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 企业微信通知
|
|
78
|
+
await this.wechatNotifier.notifyTaskFailure(
|
|
79
|
+
projectName,
|
|
80
|
+
requirement,
|
|
81
|
+
error.message
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 系统 Toast 通知(限流)
|
|
85
|
+
if (this.limiter.canNotify()) {
|
|
86
|
+
this.systemNotifier.notifyTaskFailure(projectName, error.message);
|
|
87
|
+
} else {
|
|
88
|
+
logger.warn('系统通知被限流,跳过发送', { taskId });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 等待确认通知
|
|
94
|
+
*/
|
|
95
|
+
notifyWaitingApproval(taskId, changedFiles) {
|
|
96
|
+
// WebSocket 推送
|
|
97
|
+
if (this.wsNotifier) {
|
|
98
|
+
this.wsNotifier.notifyTaskProgress(
|
|
99
|
+
taskId,
|
|
100
|
+
'waiting_approval',
|
|
101
|
+
'AI 修复完成,等待开发者确认',
|
|
102
|
+
50
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 系统 Toast 通知(限流)
|
|
107
|
+
if (this.limiter.canNotify()) {
|
|
108
|
+
this.systemNotifier.notifyWaitingApproval(taskId, changedFiles);
|
|
109
|
+
} else {
|
|
110
|
+
logger.warn('系统通知被限流,跳过发送', { taskId });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = NotificationService;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const notifier = require('node-notifier');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { logger } = require('../logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 系统通知服务
|
|
8
|
+
* 使用 node-notifier 发送 Windows Toast 通知
|
|
9
|
+
*/
|
|
10
|
+
class SystemNotifier {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.enabled = config.enabled || false;
|
|
13
|
+
this.showOnStart = config.showOnTaskStart !== false;
|
|
14
|
+
this.showOnComplete = config.showOnTaskComplete !== false;
|
|
15
|
+
this.showOnFailure = config.showOnTaskFailure !== false;
|
|
16
|
+
this.showOnApproval = config.showOnApproval !== false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 发送系统通知
|
|
21
|
+
* @param {Object} options - 通知选项
|
|
22
|
+
* @param {string} options.title - 通知标题
|
|
23
|
+
* @param {string} options.message - 通知内容
|
|
24
|
+
* @param {string} options.icon - 图标路径(可选)
|
|
25
|
+
* @param {boolean} options.sound - 是否播放声音
|
|
26
|
+
* @param {number} options.timeout - 显示时长(秒)
|
|
27
|
+
* @param {Function} options.onClick - 点击回调
|
|
28
|
+
*/
|
|
29
|
+
notify(options) {
|
|
30
|
+
if (!this.enabled) {
|
|
31
|
+
logger.debug('系统通知已禁用,跳过发送');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 检查图标路径是否存在
|
|
36
|
+
const defaultIconPath = path.join(__dirname, '../assets/icon.png');
|
|
37
|
+
const iconPath = options.icon || (fs.existsSync(defaultIconPath) ? defaultIconPath : undefined);
|
|
38
|
+
|
|
39
|
+
notifier.notify(
|
|
40
|
+
{
|
|
41
|
+
title: options.title,
|
|
42
|
+
message: options.message,
|
|
43
|
+
icon: iconPath,
|
|
44
|
+
sound: options.sound || false,
|
|
45
|
+
wait: false,
|
|
46
|
+
timeout: options.timeout || 5,
|
|
47
|
+
},
|
|
48
|
+
(err, response, metadata) => {
|
|
49
|
+
if (err) {
|
|
50
|
+
logger.error('系统通知发送失败:', err.message);
|
|
51
|
+
} else {
|
|
52
|
+
logger.debug('系统通知已发送:', { title: options.title });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// 监听点击事件
|
|
58
|
+
if (options.onClick) {
|
|
59
|
+
notifier.on('click', options.onClick);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 任务开始通知
|
|
65
|
+
*/
|
|
66
|
+
notifyTaskStart(projectName, requirement) {
|
|
67
|
+
if (!this.showOnStart) return;
|
|
68
|
+
|
|
69
|
+
const shortReq = requirement.length > 50
|
|
70
|
+
? `${requirement.substring(0, 50)}...`
|
|
71
|
+
: requirement;
|
|
72
|
+
|
|
73
|
+
this.notify({
|
|
74
|
+
title: '🚀 修复任务已启动',
|
|
75
|
+
message: `项目: ${projectName}\n${shortReq}`,
|
|
76
|
+
sound: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 任务完成通知
|
|
82
|
+
*/
|
|
83
|
+
notifyTaskComplete(projectName, changedFiles) {
|
|
84
|
+
if (!this.showOnComplete) return;
|
|
85
|
+
|
|
86
|
+
this.notify({
|
|
87
|
+
title: '✅ 修复任务完成',
|
|
88
|
+
message: `项目: ${projectName}\n修改文件: ${changedFiles.length} 个`,
|
|
89
|
+
sound: true,
|
|
90
|
+
onClick: () => {
|
|
91
|
+
logger.info('用户点击了任务完成通知');
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 任务失败通知
|
|
98
|
+
*/
|
|
99
|
+
notifyTaskFailure(projectName, error) {
|
|
100
|
+
if (!this.showOnFailure) return;
|
|
101
|
+
|
|
102
|
+
const shortError = error.length > 50
|
|
103
|
+
? `${error.substring(0, 50)}...`
|
|
104
|
+
: error;
|
|
105
|
+
|
|
106
|
+
this.notify({
|
|
107
|
+
title: '❌ 修复任务失败',
|
|
108
|
+
message: `项目: ${projectName}\n${shortError}`,
|
|
109
|
+
sound: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 等待确认通知
|
|
115
|
+
*/
|
|
116
|
+
notifyWaitingApproval(taskId, changedFiles) {
|
|
117
|
+
if (!this.showOnApproval) return;
|
|
118
|
+
|
|
119
|
+
this.notify({
|
|
120
|
+
title: '⏸️ 等待开发者确认',
|
|
121
|
+
message: `修改了 ${changedFiles.length} 个文件\n点击查看详情`,
|
|
122
|
+
sound: true,
|
|
123
|
+
onClick: () => {
|
|
124
|
+
logger.info(`用户点击了等待确认通知: ${taskId}`);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = SystemNotifier;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { logger } = require('./logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 任务管理器 - 管理异步修复任务
|
|
7
|
+
* 支持任务持久化到本地文件
|
|
8
|
+
*/
|
|
9
|
+
class TaskManager {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.tasks = new Map(); // taskId -> task info
|
|
12
|
+
this.runningProcesses = new Map(); // taskId -> childProcess (用于任务取消)
|
|
13
|
+
this.storageFile = path.join(__dirname, '../data/tasks.json');
|
|
14
|
+
this.loadTasks(); // 启动时加载历史任务
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从文件加载任务
|
|
19
|
+
*/
|
|
20
|
+
loadTasks() {
|
|
21
|
+
try {
|
|
22
|
+
// 确保数据目录存在
|
|
23
|
+
const dataDir = path.dirname(this.storageFile);
|
|
24
|
+
if (!fs.existsSync(dataDir)) {
|
|
25
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 加载任务文件
|
|
29
|
+
if (fs.existsSync(this.storageFile)) {
|
|
30
|
+
const data = fs.readFileSync(this.storageFile, 'utf-8');
|
|
31
|
+
const tasksArray = JSON.parse(data);
|
|
32
|
+
|
|
33
|
+
// 转换为 Map
|
|
34
|
+
this.tasks = new Map(tasksArray.map(task => [task.id, task]));
|
|
35
|
+
|
|
36
|
+
logger.info(`📂 已加载 ${this.tasks.size} 条历史任务记录`);
|
|
37
|
+
} else {
|
|
38
|
+
logger.info('📂 未找到历史任务记录,将创建新文件');
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error(`❌ 加载任务记录失败: ${error.message}`);
|
|
42
|
+
this.tasks = new Map(); // 加载失败时使用空 Map
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 保存任务到文件
|
|
48
|
+
*/
|
|
49
|
+
saveTasks() {
|
|
50
|
+
try {
|
|
51
|
+
// 确保数据目录存在
|
|
52
|
+
const dataDir = path.dirname(this.storageFile);
|
|
53
|
+
if (!fs.existsSync(dataDir)) {
|
|
54
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 转换 Map 为数组并保存
|
|
58
|
+
const tasksArray = Array.from(this.tasks.values());
|
|
59
|
+
fs.writeFileSync(this.storageFile, JSON.stringify(tasksArray, null, 2), 'utf-8');
|
|
60
|
+
|
|
61
|
+
logger.debug(`💾 任务记录已保存 (共 ${this.tasks.size} 条)`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error(`❌ 保存任务记录失败: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 创建新任务
|
|
69
|
+
*/
|
|
70
|
+
createTask(requirement, projectPath, options = {}) {
|
|
71
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
72
|
+
|
|
73
|
+
const task = {
|
|
74
|
+
id: taskId,
|
|
75
|
+
status: 'pending', // pending, running, completed, failed
|
|
76
|
+
requirement,
|
|
77
|
+
projectPath: projectPath || options.projectPath,
|
|
78
|
+
repoUrl: options.repoUrl || null,
|
|
79
|
+
// 展开 options 中的参数到任务对象
|
|
80
|
+
runTests: options.runTests || false,
|
|
81
|
+
autoPush: options.autoPush !== undefined ? options.autoPush : true,
|
|
82
|
+
aiEngine: options.aiEngine || null,
|
|
83
|
+
baseBranch: options.baseBranch || null,
|
|
84
|
+
createNewBranch: options.createNewBranch !== undefined ? options.createNewBranch : true, // 新增:是否创建新分支
|
|
85
|
+
username: options.username || 'system', // 新增:用户名
|
|
86
|
+
mergeToAlpha: options.mergeToAlpha || false,
|
|
87
|
+
mergeToBeta: options.mergeToBeta || false,
|
|
88
|
+
mergeToMaster: options.mergeToMaster || false,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
startedAt: null,
|
|
91
|
+
completedAt: null,
|
|
92
|
+
duration: null, // 执行耗时
|
|
93
|
+
result: null,
|
|
94
|
+
error: null,
|
|
95
|
+
progress: {
|
|
96
|
+
step: 'created',
|
|
97
|
+
message: '任务已创建',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.tasks.set(taskId, task);
|
|
102
|
+
this.saveTasks(); // 立即保存
|
|
103
|
+
return taskId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取任务状态
|
|
108
|
+
*/
|
|
109
|
+
getTask(taskId) {
|
|
110
|
+
return this.tasks.get(taskId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 更新任务状态
|
|
115
|
+
*/
|
|
116
|
+
updateTask(taskId, updates) {
|
|
117
|
+
const task = this.tasks.get(taskId);
|
|
118
|
+
if (task) {
|
|
119
|
+
Object.assign(task, updates);
|
|
120
|
+
this.tasks.set(taskId, task);
|
|
121
|
+
this.saveTasks(); // 每次更新都保存
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 更新任务进度
|
|
127
|
+
*/
|
|
128
|
+
updateProgress(taskId, step, message) {
|
|
129
|
+
const task = this.tasks.get(taskId);
|
|
130
|
+
if (task) {
|
|
131
|
+
task.progress = { step, message };
|
|
132
|
+
this.tasks.set(taskId, task);
|
|
133
|
+
// 进度更新频繁,不立即保存,只在状态变更时保存
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 标记任务开始
|
|
139
|
+
*/
|
|
140
|
+
startTask(taskId) {
|
|
141
|
+
this.updateTask(taskId, {
|
|
142
|
+
status: 'running',
|
|
143
|
+
startedAt: new Date().toISOString(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 标记任务完成
|
|
149
|
+
*/
|
|
150
|
+
completeTask(taskId, result) {
|
|
151
|
+
const task = this.tasks.get(taskId);
|
|
152
|
+
if (!task) return;
|
|
153
|
+
|
|
154
|
+
const completedAt = new Date().toISOString();
|
|
155
|
+
let duration = null;
|
|
156
|
+
|
|
157
|
+
// 计算执行耗时
|
|
158
|
+
if (task.startedAt) {
|
|
159
|
+
const startTime = new Date(task.startedAt).getTime();
|
|
160
|
+
const endTime = new Date(completedAt).getTime();
|
|
161
|
+
duration = ((endTime - startTime) / 1000).toFixed(2) + 's';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.updateTask(taskId, {
|
|
165
|
+
status: 'completed',
|
|
166
|
+
completedAt,
|
|
167
|
+
duration,
|
|
168
|
+
result,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 标记任务失败
|
|
174
|
+
*/
|
|
175
|
+
failTask(taskId, error) {
|
|
176
|
+
const task = this.tasks.get(taskId);
|
|
177
|
+
if (!task) return;
|
|
178
|
+
|
|
179
|
+
const completedAt = new Date().toISOString();
|
|
180
|
+
let duration = null;
|
|
181
|
+
|
|
182
|
+
// 计算执行耗时
|
|
183
|
+
if (task.startedAt) {
|
|
184
|
+
const startTime = new Date(task.startedAt).getTime();
|
|
185
|
+
const endTime = new Date(completedAt).getTime();
|
|
186
|
+
duration = ((endTime - startTime) / 1000).toFixed(2) + 's';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.updateTask(taskId, {
|
|
190
|
+
status: 'failed',
|
|
191
|
+
completedAt,
|
|
192
|
+
duration,
|
|
193
|
+
error: error.message || String(error),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 清理旧任务(保留最近 100 个)
|
|
199
|
+
*/
|
|
200
|
+
cleanup() {
|
|
201
|
+
if (this.tasks.size > 100) {
|
|
202
|
+
const sortedTasks = Array.from(this.tasks.entries())
|
|
203
|
+
.sort((a, b) => new Date(b[1].createdAt) - new Date(a[1].createdAt));
|
|
204
|
+
|
|
205
|
+
this.tasks.clear();
|
|
206
|
+
sortedTasks.slice(0, 100).forEach(([id, task]) => {
|
|
207
|
+
this.tasks.set(id, task);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.saveTasks(); // 清理后保存
|
|
211
|
+
logger.info(`🧹 已清理旧任务,保留最近 100 条记录`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 获取所有任务列表
|
|
217
|
+
*/
|
|
218
|
+
getAllTasks(limit = 20) {
|
|
219
|
+
const sortedTasks = Array.from(this.tasks.values())
|
|
220
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
221
|
+
.slice(0, limit);
|
|
222
|
+
|
|
223
|
+
return sortedTasks;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 注册运行中的进程
|
|
228
|
+
* @param {string} taskId - 任务ID
|
|
229
|
+
* @param {Object} processInfo - 进程信息(包含取消函数)
|
|
230
|
+
*/
|
|
231
|
+
registerProcess(taskId, processInfo) {
|
|
232
|
+
this.runningProcesses.set(taskId, processInfo);
|
|
233
|
+
logger.debug(`注册任务进程: ${taskId}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 注销运行中的进程
|
|
238
|
+
* @param {string} taskId - 任务ID
|
|
239
|
+
*/
|
|
240
|
+
unregisterProcess(taskId) {
|
|
241
|
+
this.runningProcesses.delete(taskId);
|
|
242
|
+
logger.debug(`注销任务进程: ${taskId}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 取消任务
|
|
247
|
+
* @param {string} taskId - 任务ID
|
|
248
|
+
* @returns {boolean} 是否成功取消
|
|
249
|
+
*/
|
|
250
|
+
cancelTask(taskId) {
|
|
251
|
+
const task = this.tasks.get(taskId);
|
|
252
|
+
|
|
253
|
+
if (!task) {
|
|
254
|
+
throw new Error('任务不存在');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (task.status !== 'running' && task.status !== 'pending') {
|
|
258
|
+
throw new Error(`任务状态为 ${task.status},无法取消`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const processInfo = this.runningProcesses.get(taskId);
|
|
262
|
+
|
|
263
|
+
if (processInfo && processInfo.cancel) {
|
|
264
|
+
try {
|
|
265
|
+
// 调用取消函数
|
|
266
|
+
processInfo.cancel();
|
|
267
|
+
logger.info(`🛑 任务 ${taskId} 已请求取消`);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.error(`取消任务失败: ${error.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 更新任务状态
|
|
274
|
+
this.updateTask(taskId, {
|
|
275
|
+
status: 'cancelled',
|
|
276
|
+
completedAt: new Date().toISOString(),
|
|
277
|
+
error: '任务已被用户取消',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 注销进程
|
|
281
|
+
this.unregisterProcess(taskId);
|
|
282
|
+
|
|
283
|
+
logger.info(`✅ 任务 ${taskId} 已取消`);
|
|
284
|
+
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = new TaskManager();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { execSync, spawn } = require('child_process');
|
|
2
|
+
const { logger } = require('../logger');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 统一的进程执行工具类
|
|
6
|
+
* 在 Windows 上隐藏 CMD 窗口,同时保持日志输出正常
|
|
7
|
+
*/
|
|
8
|
+
class ExecUtil {
|
|
9
|
+
/**
|
|
10
|
+
* 同步执行命令(隐藏窗口)
|
|
11
|
+
* @param {string} command - 要执行的命令
|
|
12
|
+
* @param {Object} options - execSync 选项
|
|
13
|
+
* @returns {string} 命令输出
|
|
14
|
+
*/
|
|
15
|
+
static execSync(command, options = {}) {
|
|
16
|
+
const mergedOptions = {
|
|
17
|
+
// 🔑 关键:在 Windows 上隐藏 CMD 窗口
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
|
|
20
|
+
// 默认编码
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
|
|
23
|
+
// 默认不显示实时输出(通过 logger 记录)
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
|
|
26
|
+
// 合并用户自定义选项
|
|
27
|
+
...options,
|
|
28
|
+
|
|
29
|
+
// 确保 windowsHide 不会被覆盖
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const output = execSync(command, mergedOptions);
|
|
35
|
+
|
|
36
|
+
// 如果需要,可以在这里记录命令执行日志
|
|
37
|
+
if (options.debug) {
|
|
38
|
+
logger.debug(`[Exec] 命令执行成功: ${command.substring(0, 100)}...`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return output;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// 错误日志由调用方处理
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 异步执行命令(隐藏窗口)
|
|
50
|
+
* @param {string} command - 要执行的命令
|
|
51
|
+
* @param {Array} args - 命令参数数组
|
|
52
|
+
* @param {Object} options - spawn 选项
|
|
53
|
+
* @returns {ChildProcess} 子进程实例
|
|
54
|
+
*/
|
|
55
|
+
static spawn(command, args = [], options = {}) {
|
|
56
|
+
const mergedOptions = {
|
|
57
|
+
// 🔑 关键:在 Windows 上隐藏 CMD 窗口
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
|
|
60
|
+
// 默认的 stdio 配置
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
|
|
63
|
+
// 合并用户自定义选项
|
|
64
|
+
...options,
|
|
65
|
+
|
|
66
|
+
// 确保 windowsHide 不会被覆盖
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return spawn(command, args, mergedOptions);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 使用 shell 模式执行命令(隐藏窗口)
|
|
75
|
+
* @param {string} command - 完整的 shell 命令字符串
|
|
76
|
+
* @param {Object} options - spawn 选项
|
|
77
|
+
* @returns {ChildProcess} 子进程实例
|
|
78
|
+
*/
|
|
79
|
+
static spawnShell(command, options = {}) {
|
|
80
|
+
return this.spawn(command, [], {
|
|
81
|
+
...options,
|
|
82
|
+
shell: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = ExecUtil;
|