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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输出格式化工具
|
|
3
|
+
* 支持: interactive, json, quiet, table
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 格式化器
|
|
8
|
+
*/
|
|
9
|
+
class Formatter {
|
|
10
|
+
constructor(format = 'interactive') {
|
|
11
|
+
this.format = format;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 输出数据
|
|
16
|
+
*/
|
|
17
|
+
output(data, options = {}) {
|
|
18
|
+
switch (this.format) {
|
|
19
|
+
case 'json':
|
|
20
|
+
this.outputJSON(data);
|
|
21
|
+
break;
|
|
22
|
+
case 'quiet':
|
|
23
|
+
this.outputQuiet(data, options);
|
|
24
|
+
break;
|
|
25
|
+
case 'table':
|
|
26
|
+
this.outputTable(data, options);
|
|
27
|
+
break;
|
|
28
|
+
case 'interactive':
|
|
29
|
+
default:
|
|
30
|
+
this.outputInteractive(data, options);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 交互式输出(默认)
|
|
37
|
+
*/
|
|
38
|
+
outputInteractive(data, options) {
|
|
39
|
+
if (options.title) {
|
|
40
|
+
console.log(options.title);
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof data === 'object') {
|
|
45
|
+
console.log(JSON.stringify(data, null, 2));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* JSON 输出
|
|
53
|
+
*/
|
|
54
|
+
outputJSON(data) {
|
|
55
|
+
console.log(JSON.stringify(data, null, 2));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 静默输出
|
|
60
|
+
*/
|
|
61
|
+
outputQuiet(data, options) {
|
|
62
|
+
if (options.quietValue) {
|
|
63
|
+
// 只输出特定值
|
|
64
|
+
const value = this.getNestedValue(data, options.quietValue);
|
|
65
|
+
if (value !== undefined) {
|
|
66
|
+
console.log(value);
|
|
67
|
+
}
|
|
68
|
+
} else if (options.quietField) {
|
|
69
|
+
// 只输出特定字段
|
|
70
|
+
if (data[options.quietField] !== undefined) {
|
|
71
|
+
console.log(data[options.quietField]);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// 输出基本值
|
|
75
|
+
if (typeof data === 'object') {
|
|
76
|
+
// 如果是对象,输出 id 或第一个字段
|
|
77
|
+
console.log(data.id || data.taskId || data.status || JSON.stringify(data));
|
|
78
|
+
} else {
|
|
79
|
+
console.log(data);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 表格输出
|
|
86
|
+
*/
|
|
87
|
+
outputTable(data, options) {
|
|
88
|
+
const items = Array.isArray(data) ? data : [data];
|
|
89
|
+
|
|
90
|
+
if (items.length === 0) {
|
|
91
|
+
console.log('暂无数据');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const columns = options.columns || this.detectColumns(items[0]);
|
|
96
|
+
|
|
97
|
+
// 计算列宽
|
|
98
|
+
const widths = {};
|
|
99
|
+
columns.forEach(col => {
|
|
100
|
+
widths[col] = col.length;
|
|
101
|
+
items.forEach(item => {
|
|
102
|
+
const value = String(this.getNestedValue(item, col) || '');
|
|
103
|
+
widths[col] = Math.max(widths[col], value.length);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 输出表头
|
|
108
|
+
const header = columns.map(col => col.padEnd(widths[col])).join(' | ');
|
|
109
|
+
console.log(header);
|
|
110
|
+
console.log('-'.repeat(header.length));
|
|
111
|
+
|
|
112
|
+
// 输出数据行
|
|
113
|
+
items.forEach(item => {
|
|
114
|
+
const row = columns.map(col => {
|
|
115
|
+
const value = String(this.getNestedValue(item, col) || '');
|
|
116
|
+
return value.padEnd(widths[col]);
|
|
117
|
+
}).join(' | ');
|
|
118
|
+
console.log(row);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 检测对象列
|
|
124
|
+
*/
|
|
125
|
+
detectColumns(obj) {
|
|
126
|
+
if (!obj || typeof obj !== 'object') {
|
|
127
|
+
return ['value'];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Object.keys(obj).filter(key =>
|
|
131
|
+
!['changes', 'result', 'error'].includes(key)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取嵌套值
|
|
137
|
+
*/
|
|
138
|
+
getNestedValue(obj, path) {
|
|
139
|
+
const keys = path.split('.');
|
|
140
|
+
let value = obj;
|
|
141
|
+
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
if (value && typeof value === 'object') {
|
|
144
|
+
value = value[key];
|
|
145
|
+
} else {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 格式化错误
|
|
155
|
+
*/
|
|
156
|
+
error(message, error) {
|
|
157
|
+
if (this.format === 'json') {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
success: false,
|
|
160
|
+
error: message,
|
|
161
|
+
details: error?.message
|
|
162
|
+
}, null, 2));
|
|
163
|
+
} else if (this.format === 'quiet') {
|
|
164
|
+
console.error(message);
|
|
165
|
+
} else {
|
|
166
|
+
console.error(`❌ ${message}`);
|
|
167
|
+
if (error?.message) {
|
|
168
|
+
console.error(` ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 格式化成功
|
|
175
|
+
*/
|
|
176
|
+
success(message, data) {
|
|
177
|
+
if (this.format === 'json') {
|
|
178
|
+
console.log(JSON.stringify({
|
|
179
|
+
success: true,
|
|
180
|
+
message,
|
|
181
|
+
data
|
|
182
|
+
}, null, 2));
|
|
183
|
+
} else if (this.format === 'quiet') {
|
|
184
|
+
console.log(message);
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`✅ ${message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 格式化警告
|
|
192
|
+
*/
|
|
193
|
+
warning(message) {
|
|
194
|
+
if (this.format === 'json') {
|
|
195
|
+
console.log(JSON.stringify({
|
|
196
|
+
warning: message
|
|
197
|
+
}, null, 2));
|
|
198
|
+
} else if (this.format !== 'quiet') {
|
|
199
|
+
console.log(`⚠️ ${message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 格式化信息
|
|
205
|
+
*/
|
|
206
|
+
info(message) {
|
|
207
|
+
if (this.format === 'json') {
|
|
208
|
+
console.log(JSON.stringify({
|
|
209
|
+
info: message
|
|
210
|
+
}, null, 2));
|
|
211
|
+
} else if (this.format !== 'quiet') {
|
|
212
|
+
console.log(`ℹ️ ${message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 创建格式化器
|
|
219
|
+
*/
|
|
220
|
+
function createFormatter(format) {
|
|
221
|
+
return new Formatter(format);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { Formatter, createFormatter };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程管理器
|
|
3
|
+
* 管理后台服务进程的启动、停止和监控
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 进程管理器类
|
|
12
|
+
*/
|
|
13
|
+
class ProcessManager {
|
|
14
|
+
constructor(configManager) {
|
|
15
|
+
this.config = configManager;
|
|
16
|
+
this.pidFile = path.join(this.config.getConfigPath(), '..', '.service.pid');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 检查服务是否运行
|
|
21
|
+
* @returns {Promise<boolean>}
|
|
22
|
+
*/
|
|
23
|
+
async isRunning() {
|
|
24
|
+
if (!fs.existsSync(this.pidFile)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const pid = parseInt(fs.readFileSync(this.pidFile, 'utf-8'));
|
|
30
|
+
process.kill(pid, 0); // 检查进程是否存在
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
// PID 文件存在但进程不存在,清理文件
|
|
34
|
+
fs.unlinkSync(this.pidFile);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 获取服务 PID
|
|
41
|
+
* @returns {number|null}
|
|
42
|
+
*/
|
|
43
|
+
getPid() {
|
|
44
|
+
if (!fs.existsSync(this.pidFile)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return parseInt(fs.readFileSync(this.pidFile, 'utf-8'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 启动服务
|
|
52
|
+
* @param {Object} options - 启动选项
|
|
53
|
+
* @returns {Promise<number>} 进程 PID
|
|
54
|
+
*/
|
|
55
|
+
async start(options = {}) {
|
|
56
|
+
// 检查是否已运行
|
|
57
|
+
if (await this.isRunning()) {
|
|
58
|
+
const pid = this.getPid();
|
|
59
|
+
throw new Error(`服务已在运行 (PID: ${pid})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rootDir = process.env.CFIX_ROOT || path.resolve(__dirname, '../..');
|
|
63
|
+
const serviceScript = path.join(rootDir, 'src', 'index.js');
|
|
64
|
+
|
|
65
|
+
// 检查服务脚本是否存在
|
|
66
|
+
if (!fs.existsSync(serviceScript)) {
|
|
67
|
+
throw new Error(`服务脚本不存在: ${serviceScript}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 合并环境变量
|
|
71
|
+
const env = {
|
|
72
|
+
...process.env,
|
|
73
|
+
NODE_ENV: options.daemon ? 'production' : 'development',
|
|
74
|
+
...(options.env || {})
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 启动子进程
|
|
78
|
+
const child = spawn('node', [serviceScript], {
|
|
79
|
+
detached: options.daemon !== false, // 默认以守护进程方式运行
|
|
80
|
+
stdio: options.daemon === false ? 'inherit' : 'ignore',
|
|
81
|
+
cwd: rootDir,
|
|
82
|
+
env
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 保存 PID
|
|
86
|
+
fs.writeFileSync(this.pidFile, String(child.pid));
|
|
87
|
+
|
|
88
|
+
// 如果是守护进程,取消引用
|
|
89
|
+
if (options.daemon !== false) {
|
|
90
|
+
child.unref();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 等待服务启动
|
|
94
|
+
if (options.wait) {
|
|
95
|
+
await this.waitForReady();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return child.pid;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 停止服务
|
|
103
|
+
* @param {Object} options - 停止选项
|
|
104
|
+
*/
|
|
105
|
+
async stop(options = {}) {
|
|
106
|
+
if (!await this.isRunning()) {
|
|
107
|
+
if (!options.quiet) {
|
|
108
|
+
console.log('服务未运行');
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pid = this.getPid();
|
|
114
|
+
if (!options.quiet) {
|
|
115
|
+
console.log(`正在停止服务 (PID: ${pid})...`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// 先尝试 SIGTERM
|
|
120
|
+
process.kill(pid, 'SIGTERM');
|
|
121
|
+
|
|
122
|
+
// 等待最多 10 秒
|
|
123
|
+
for (let i = 0; i < 10; i++) {
|
|
124
|
+
await this.sleep(1000);
|
|
125
|
+
try {
|
|
126
|
+
process.kill(pid, 0);
|
|
127
|
+
} catch {
|
|
128
|
+
// 进程已结束
|
|
129
|
+
fs.unlinkSync(this.pidFile);
|
|
130
|
+
if (!options.quiet) {
|
|
131
|
+
console.log('服务已停止');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 如果还在运行,强制终止
|
|
138
|
+
process.kill(pid, 'SIGKILL');
|
|
139
|
+
await this.sleep(500);
|
|
140
|
+
fs.unlinkSync(this.pidFile);
|
|
141
|
+
|
|
142
|
+
if (!options.quiet) {
|
|
143
|
+
console.log('服务已强制停止');
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(`停止服务失败: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 重启服务
|
|
152
|
+
* @param {Object} options - 重启选项
|
|
153
|
+
*/
|
|
154
|
+
async restart(options = {}) {
|
|
155
|
+
if (await this.isRunning()) {
|
|
156
|
+
await this.stop({ quiet: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await this.sleep(1000);
|
|
160
|
+
return await this.start(options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 等待服务就绪
|
|
165
|
+
* @param {number} timeout - 超时时间(毫秒)
|
|
166
|
+
*/
|
|
167
|
+
async waitForReady(timeout = 30000) {
|
|
168
|
+
const serviceConfig = this.config.getServiceConfig();
|
|
169
|
+
const baseURL = `http://${serviceConfig.host}:${serviceConfig.port}`;
|
|
170
|
+
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
|
|
173
|
+
while (Date.now() - start < timeout) {
|
|
174
|
+
try {
|
|
175
|
+
const axios = require('axios');
|
|
176
|
+
await axios.get(`${baseURL}/health`, { timeout: 1000 });
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
await this.sleep(500);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new Error('服务启动超时');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 获取服务健康状态
|
|
188
|
+
*/
|
|
189
|
+
async getHealthStatus() {
|
|
190
|
+
const serviceConfig = this.config.getServiceConfig();
|
|
191
|
+
const baseURL = `http://${serviceConfig.host}:${serviceConfig.port}`;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const axios = require('axios');
|
|
195
|
+
const response = await axios.get(`${baseURL}/health`, { timeout: 3000 });
|
|
196
|
+
return response.data;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new Error(`无法连接到服务: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 发送信号到服务进程
|
|
204
|
+
* @param {string} signal - 信号名称
|
|
205
|
+
*/
|
|
206
|
+
async sendSignal(signal) {
|
|
207
|
+
if (!await this.isRunning()) {
|
|
208
|
+
throw new Error('服务未运行');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pid = this.getPid();
|
|
212
|
+
process.kill(pid, signal);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 睡眠函数
|
|
217
|
+
* @param {number} ms - 毫秒数
|
|
218
|
+
*/
|
|
219
|
+
sleep(ms) {
|
|
220
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 清理 PID 文件
|
|
225
|
+
*/
|
|
226
|
+
cleanupPidFile() {
|
|
227
|
+
if (fs.existsSync(this.pidFile)) {
|
|
228
|
+
fs.unlinkSync(this.pidFile);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = ProcessManager;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务通信客户端
|
|
3
|
+
* 与后台服务通信
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const config = require('./config-manager');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 服务客户端类
|
|
11
|
+
*/
|
|
12
|
+
class ServiceClient {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.serviceConfig = config.getServiceConfig();
|
|
15
|
+
this.baseURL = `http://${this.serviceConfig.host}:${this.serviceConfig.port}`;
|
|
16
|
+
this.timeout = this.serviceConfig.timeout || 30000;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建 axios 实例
|
|
21
|
+
*/
|
|
22
|
+
createInstance() {
|
|
23
|
+
return axios.create({
|
|
24
|
+
baseURL: this.baseURL,
|
|
25
|
+
timeout: this.timeout,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json'
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 健康检查
|
|
34
|
+
*/
|
|
35
|
+
async healthCheck() {
|
|
36
|
+
try {
|
|
37
|
+
const instance = this.createInstance();
|
|
38
|
+
const response = await instance.get('/health');
|
|
39
|
+
return response.data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`服务健康检查失败: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建修复任务
|
|
47
|
+
*/
|
|
48
|
+
async createFixTask(options) {
|
|
49
|
+
try {
|
|
50
|
+
const instance = this.createInstance();
|
|
51
|
+
const response = await instance.post('/api/fix-bug', {
|
|
52
|
+
projectPath: options.projectPath,
|
|
53
|
+
repoUrl: options.repoUrl,
|
|
54
|
+
requirement: options.requirement,
|
|
55
|
+
runTests: options.runTests,
|
|
56
|
+
mergeToAlpha: options.mergeToAlpha,
|
|
57
|
+
mergeToBeta: options.mergeToBeta,
|
|
58
|
+
autoConfirm: options.autoConfirm,
|
|
59
|
+
aiEngine: options.aiEngine,
|
|
60
|
+
maxTurns: options.maxTurns
|
|
61
|
+
});
|
|
62
|
+
return response.data;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.response) {
|
|
65
|
+
throw new Error(`创建任务失败: ${error.response.data.message || error.message}`);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`创建任务失败: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 获取任务列表
|
|
73
|
+
*/
|
|
74
|
+
async getTasks(filters = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const instance = this.createInstance();
|
|
77
|
+
const response = await instance.get('/api/tasks', { params: filters });
|
|
78
|
+
return response.data;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`获取任务列表失败: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取任务详情
|
|
86
|
+
*/
|
|
87
|
+
async getTask(taskId) {
|
|
88
|
+
try {
|
|
89
|
+
const instance = this.createInstance();
|
|
90
|
+
const response = await instance.get(`/api/tasks/${taskId}`);
|
|
91
|
+
return response.data;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.response && error.response.status === 404) {
|
|
94
|
+
throw new Error(`任务不存在: ${taskId}`);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`获取任务详情失败: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 取消任务
|
|
102
|
+
*/
|
|
103
|
+
async cancelTask(taskId) {
|
|
104
|
+
try {
|
|
105
|
+
const instance = this.createInstance();
|
|
106
|
+
const response = await instance.post(`/api/tasks/${taskId}/cancel`);
|
|
107
|
+
return response.data;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`取消任务失败: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 批准任务
|
|
115
|
+
*/
|
|
116
|
+
async approveTask(taskId) {
|
|
117
|
+
try {
|
|
118
|
+
const instance = this.createInstance();
|
|
119
|
+
const response = await instance.post(`/api/tasks/${taskId}/approve`);
|
|
120
|
+
return response.data;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(`批准任务失败: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 拒绝任务
|
|
128
|
+
*/
|
|
129
|
+
async rejectTask(taskId, reason) {
|
|
130
|
+
try {
|
|
131
|
+
const instance = this.createInstance();
|
|
132
|
+
const response = await instance.post(`/api/tasks/${taskId}/reject`, { reason });
|
|
133
|
+
return response.data;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(`拒绝任务失败: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 获取任务修改内容
|
|
141
|
+
*/
|
|
142
|
+
async getTaskChanges(taskId) {
|
|
143
|
+
try {
|
|
144
|
+
const instance = this.createInstance();
|
|
145
|
+
const response = await instance.get(`/api/tasks/${taskId}/changes`);
|
|
146
|
+
return response.data;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(`获取修改内容失败: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 获取队列统计
|
|
154
|
+
*/
|
|
155
|
+
async getQueueStats() {
|
|
156
|
+
try {
|
|
157
|
+
const instance = this.createInstance();
|
|
158
|
+
const response = await instance.get('/api/queue/stats');
|
|
159
|
+
return response.data;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(`获取队列统计失败: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 获取项目锁统计
|
|
167
|
+
*/
|
|
168
|
+
async getProjectLocksStats() {
|
|
169
|
+
try {
|
|
170
|
+
const instance = this.createInstance();
|
|
171
|
+
const response = await instance.get('/api/project-locks/stats');
|
|
172
|
+
return response.data;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new Error(`获取项目锁统计失败: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 获取性能指标
|
|
180
|
+
*/
|
|
181
|
+
async getMetrics() {
|
|
182
|
+
try {
|
|
183
|
+
const instance = this.createInstance();
|
|
184
|
+
const response = await instance.get('/api/metrics');
|
|
185
|
+
return response.data;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error(`获取性能指标失败: ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 检查服务是否可用
|
|
193
|
+
*/
|
|
194
|
+
async isAvailable() {
|
|
195
|
+
try {
|
|
196
|
+
await this.healthCheck();
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 等待服务就绪
|
|
205
|
+
*/
|
|
206
|
+
async waitForReady(maxWait = 30000, interval = 500) {
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
|
|
209
|
+
while (Date.now() - start < maxWait) {
|
|
210
|
+
if (await this.isAvailable()) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
await this.sleep(interval);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error('服务启动超时');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 等待任务完成
|
|
221
|
+
*/
|
|
222
|
+
async waitForTask(taskId, options = {}) {
|
|
223
|
+
const {
|
|
224
|
+
timeout = 1800000, // 30分钟
|
|
225
|
+
interval = 2000, // 2秒
|
|
226
|
+
onProgress
|
|
227
|
+
} = options;
|
|
228
|
+
|
|
229
|
+
const start = Date.now();
|
|
230
|
+
|
|
231
|
+
while (Date.now() - start < timeout) {
|
|
232
|
+
const task = await this.getTask(taskId);
|
|
233
|
+
|
|
234
|
+
if (onProgress) {
|
|
235
|
+
onProgress(task);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 任务完成
|
|
239
|
+
if (task.status === 'completed') {
|
|
240
|
+
return task;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 任务失败
|
|
244
|
+
if (task.status === 'failed' || task.status === 'cancelled') {
|
|
245
|
+
throw new Error(`任务${task.status === 'failed' ? '失败' : '已取消'}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await this.sleep(interval);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
throw new Error('等待任务超时');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 睡眠函数
|
|
256
|
+
*/
|
|
257
|
+
sleep(ms) {
|
|
258
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 更新服务配置(重新加载)
|
|
263
|
+
*/
|
|
264
|
+
reloadConfig() {
|
|
265
|
+
this.serviceConfig = config.getServiceConfig();
|
|
266
|
+
this.baseURL = `http://${this.serviceConfig.host}:${this.serviceConfig.port}`;
|
|
267
|
+
this.timeout = this.serviceConfig.timeout || 30000;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = ServiceClient;
|