long-git-cli 1.0.12 → 1.0.15
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/README.md +148 -0
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +33 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +413 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +9 -7
- package/dist/commands/tag.js.map +1 -1
- package/dist/devops/api/bitbucket-client.d.ts +101 -0
- package/dist/devops/api/bitbucket-client.d.ts.map +1 -0
- package/dist/devops/api/bitbucket-client.js +335 -0
- package/dist/devops/api/bitbucket-client.js.map +1 -0
- package/dist/devops/api/jenkins-client.d.ts +110 -0
- package/dist/devops/api/jenkins-client.d.ts.map +1 -0
- package/dist/devops/api/jenkins-client.js +345 -0
- package/dist/devops/api/jenkins-client.js.map +1 -0
- package/dist/devops/config/config-manager.d.ts +96 -0
- package/dist/devops/config/config-manager.d.ts.map +1 -0
- package/dist/devops/config/config-manager.js +331 -0
- package/dist/devops/config/config-manager.js.map +1 -0
- package/dist/devops/config/encryption.d.ts +39 -0
- package/dist/devops/config/encryption.d.ts.map +1 -0
- package/dist/devops/config/encryption.js +133 -0
- package/dist/devops/config/encryption.js.map +1 -0
- package/dist/devops/config/storage.d.ts +37 -0
- package/dist/devops/config/storage.d.ts.map +1 -0
- package/dist/devops/config/storage.js +132 -0
- package/dist/devops/config/storage.js.map +1 -0
- package/dist/devops/constants.d.ts +51 -0
- package/dist/devops/constants.d.ts.map +1 -0
- package/dist/devops/constants.js +95 -0
- package/dist/devops/constants.js.map +1 -0
- package/dist/devops/deployer/full-deployer.d.ts +77 -0
- package/dist/devops/deployer/full-deployer.d.ts.map +1 -0
- package/dist/devops/deployer/full-deployer.js +221 -0
- package/dist/devops/deployer/full-deployer.js.map +1 -0
- package/dist/devops/deployer/jenkins-deployer.d.ts +55 -0
- package/dist/devops/deployer/jenkins-deployer.d.ts.map +1 -0
- package/dist/devops/deployer/jenkins-deployer.js +110 -0
- package/dist/devops/deployer/jenkins-deployer.js.map +1 -0
- package/dist/devops/monitor/pipeline-monitor.d.ts +52 -0
- package/dist/devops/monitor/pipeline-monitor.d.ts.map +1 -0
- package/dist/devops/monitor/pipeline-monitor.js +205 -0
- package/dist/devops/monitor/pipeline-monitor.js.map +1 -0
- package/dist/devops/test-ui.d.ts +6 -0
- package/dist/devops/test-ui.d.ts.map +1 -0
- package/dist/devops/test-ui.js +31 -0
- package/dist/devops/test-ui.js.map +1 -0
- package/dist/devops/types.d.ts +138 -0
- package/dist/devops/types.d.ts.map +1 -0
- package/dist/devops/types.js +20 -0
- package/dist/devops/types.js.map +1 -0
- package/dist/devops/ui/server.d.ts +53 -0
- package/dist/devops/ui/server.d.ts.map +1 -0
- package/dist/devops/ui/server.js +1310 -0
- package/dist/devops/ui/server.js.map +1 -0
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/jenkins-auto-login.d.ts +41 -0
- package/dist/utils/jenkins-auto-login.d.ts.map +1 -0
- package/dist/utils/jenkins-auto-login.js +189 -0
- package/dist/utils/jenkins-auto-login.js.map +1 -0
- package/dist/utils/message.d.ts +0 -2
- package/dist/utils/message.d.ts.map +1 -1
- package/dist/utils/message.js +7 -15
- package/dist/utils/message.js.map +1 -1
- package/dist/utils/tag.d.ts +65 -6
- package/dist/utils/tag.d.ts.map +1 -1
- package/dist/utils/tag.js +148 -14
- package/dist/utils/tag.js.map +1 -1
- package/package.json +20 -2
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Web UI 服务器
|
|
4
|
+
* 提供配置管理的 Web 界面
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.WebUIServer = void 0;
|
|
44
|
+
const koa_1 = __importDefault(require("koa"));
|
|
45
|
+
const router_1 = __importDefault(require("@koa/router"));
|
|
46
|
+
const koa_bodyparser_1 = __importDefault(require("koa-bodyparser"));
|
|
47
|
+
const koa_static_1 = __importDefault(require("koa-static"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const open_1 = __importDefault(require("open"));
|
|
50
|
+
const constants_1 = require("../constants");
|
|
51
|
+
const config_manager_1 = require("../config/config-manager");
|
|
52
|
+
const bitbucket_client_1 = require("../api/bitbucket-client");
|
|
53
|
+
const jenkins_client_1 = require("../api/jenkins-client");
|
|
54
|
+
/**
|
|
55
|
+
* Web UI 服务器类
|
|
56
|
+
*/
|
|
57
|
+
class WebUIServer {
|
|
58
|
+
constructor(configManager, port = constants_1.WEB_UI_PORT, host = constants_1.WEB_UI_HOST) {
|
|
59
|
+
this.app = new koa_1.default();
|
|
60
|
+
this.router = new router_1.default();
|
|
61
|
+
this.port = port;
|
|
62
|
+
this.host = host;
|
|
63
|
+
this.configManager = configManager || new config_manager_1.ConfigManager();
|
|
64
|
+
this.setupMiddleware();
|
|
65
|
+
this.setupRoutes();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 配置中间件
|
|
69
|
+
*/
|
|
70
|
+
setupMiddleware() {
|
|
71
|
+
/** 错误处理中间件 */
|
|
72
|
+
this.app.use(async (ctx, next) => {
|
|
73
|
+
try {
|
|
74
|
+
await next();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
ctx.status = err.status || 500;
|
|
78
|
+
ctx.body = {
|
|
79
|
+
error: err.message || "Internal Server Error",
|
|
80
|
+
};
|
|
81
|
+
console.error("Server error:", err);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
/** 请求日志中间件 */
|
|
85
|
+
this.app.use(async (ctx, next) => {
|
|
86
|
+
const start = Date.now();
|
|
87
|
+
await next();
|
|
88
|
+
const ms = Date.now() - start;
|
|
89
|
+
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
|
|
90
|
+
});
|
|
91
|
+
/** Body parser 中间件 */
|
|
92
|
+
this.app.use((0, koa_bodyparser_1.default)());
|
|
93
|
+
/** 静态文件服务 */
|
|
94
|
+
/** 在开发和生产环境中都指向源代码的 public 目录 */
|
|
95
|
+
const publicPath = path.join(__dirname, "../../../src/devops/ui/public");
|
|
96
|
+
this.app.use((0, koa_static_1.default)(publicPath));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 配置路由
|
|
100
|
+
*/
|
|
101
|
+
setupRoutes() {
|
|
102
|
+
/** API 路由前缀 - 必须在注册路由之前设置 */
|
|
103
|
+
this.router.prefix("/api");
|
|
104
|
+
/** 健康检查端点 */
|
|
105
|
+
this.router.get("/health", async (ctx) => {
|
|
106
|
+
ctx.body = {
|
|
107
|
+
status: "ok",
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
/** 获取配置 */
|
|
112
|
+
this.router.get("/config", async (ctx) => {
|
|
113
|
+
try {
|
|
114
|
+
const config = await this.configManager.loadConfig();
|
|
115
|
+
/** 直接返回加密后的 token(前端用于比较是否修改) */
|
|
116
|
+
const bitbucketEncryptedToken = config.bitbucket.apiTokenHash || config.bitbucket.appPasswordHash || '';
|
|
117
|
+
/** Jenkins tokens */
|
|
118
|
+
const jenkinsWithTokens = config.jenkins.map((j) => ({
|
|
119
|
+
type: j.type,
|
|
120
|
+
url: j.url,
|
|
121
|
+
username: j.username,
|
|
122
|
+
hasToken: !!j.apiTokenHash,
|
|
123
|
+
encryptedToken: j.apiTokenHash || '', // 返回加密后的 token
|
|
124
|
+
}));
|
|
125
|
+
ctx.body = {
|
|
126
|
+
version: config.version,
|
|
127
|
+
bitbucket: {
|
|
128
|
+
username: config.bitbucket.username,
|
|
129
|
+
hasPassword: !!bitbucketEncryptedToken,
|
|
130
|
+
encryptedToken: bitbucketEncryptedToken, // 返回加密后的 token
|
|
131
|
+
},
|
|
132
|
+
jenkins: jenkinsWithTokens,
|
|
133
|
+
projects: config.projects,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
ctx.status = 500;
|
|
138
|
+
ctx.body = { error: error.message };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
/** 保存配置 */
|
|
142
|
+
this.router.post("/config", async (ctx) => {
|
|
143
|
+
try {
|
|
144
|
+
const { bitbucket, jenkins } = ctx.request.body;
|
|
145
|
+
/** 更新 Bitbucket 配置 */
|
|
146
|
+
if (bitbucket?.username && bitbucket?.appPassword) {
|
|
147
|
+
// appPassword 字段兼容新旧两种方式(API Token 或 App Password)
|
|
148
|
+
await this.configManager.updateBitbucketConfig(bitbucket.username, bitbucket.appPassword);
|
|
149
|
+
}
|
|
150
|
+
/** 更新 Jenkins 配置 */
|
|
151
|
+
if (jenkins && Array.isArray(jenkins)) {
|
|
152
|
+
const jenkinsConfigs = await Promise.all(jenkins.map(async (j) => {
|
|
153
|
+
/** 缓存明文 token */
|
|
154
|
+
if (j.apiToken) {
|
|
155
|
+
await this.configManager.addJenkinsInstance(j.type, j.url, j.username, j.apiToken);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
type: j.type,
|
|
159
|
+
url: j.url,
|
|
160
|
+
username: j.username,
|
|
161
|
+
apiTokenHash: await this.configManager.encryptToken(j.apiToken),
|
|
162
|
+
};
|
|
163
|
+
}));
|
|
164
|
+
await this.configManager.updateJenkinsConfig(jenkinsConfigs);
|
|
165
|
+
}
|
|
166
|
+
ctx.body = { success: true, message: "配置保存成功" };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
ctx.status = 500;
|
|
170
|
+
ctx.body = { error: error.message };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
/** 获取项目列表 */
|
|
174
|
+
this.router.get("/projects", async (ctx) => {
|
|
175
|
+
try {
|
|
176
|
+
const config = await this.configManager.loadConfig();
|
|
177
|
+
ctx.body = { projects: config.projects };
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
ctx.status = 500;
|
|
181
|
+
ctx.body = { error: error.message };
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
/** 添加项目 */
|
|
185
|
+
this.router.post("/projects", async (ctx) => {
|
|
186
|
+
try {
|
|
187
|
+
const projectConfig = ctx.request.body;
|
|
188
|
+
console.log('添加项目请求:');
|
|
189
|
+
console.log(' 项目路径:', projectConfig.path);
|
|
190
|
+
console.log(' 项目名称:', projectConfig.name);
|
|
191
|
+
console.log(' 完整配置:', JSON.stringify(projectConfig, null, 2));
|
|
192
|
+
await this.configManager.updateProjectConfig(projectConfig.path, projectConfig);
|
|
193
|
+
console.log('项目添加成功');
|
|
194
|
+
ctx.body = { success: true, message: "项目添加成功" };
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error('添加项目失败:', error);
|
|
198
|
+
console.error('错误堆栈:', error.stack);
|
|
199
|
+
ctx.status = 500;
|
|
200
|
+
ctx.body = { error: error.message };
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
/** 更新项目 */
|
|
204
|
+
this.router.put("/projects/:path", async (ctx) => {
|
|
205
|
+
try {
|
|
206
|
+
const projectPath = decodeURIComponent(ctx.params.path);
|
|
207
|
+
const projectConfig = ctx.request.body;
|
|
208
|
+
console.log('更新项目请求:');
|
|
209
|
+
console.log(' 项目路径:', projectPath);
|
|
210
|
+
console.log(' 完整配置:', JSON.stringify(projectConfig, null, 2));
|
|
211
|
+
await this.configManager.updateProjectConfig(projectPath, projectConfig);
|
|
212
|
+
console.log('项目更新成功');
|
|
213
|
+
ctx.body = { success: true, message: "项目更新成功" };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error('更新项目失败:', error);
|
|
217
|
+
console.error('错误堆栈:', error.stack);
|
|
218
|
+
ctx.status = 500;
|
|
219
|
+
ctx.body = { error: error.message };
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
/** 删除项目 */
|
|
223
|
+
this.router.delete("/projects/:path", async (ctx) => {
|
|
224
|
+
try {
|
|
225
|
+
const projectPath = decodeURIComponent(ctx.params.path);
|
|
226
|
+
await this.configManager.deleteProjectConfig(projectPath);
|
|
227
|
+
ctx.body = { success: true, message: "项目删除成功" };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
ctx.status = 500;
|
|
231
|
+
ctx.body = { error: error.message };
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
/** 解析 Git 仓库信息 */
|
|
235
|
+
this.router.post("/parse-git-repo", async (ctx) => {
|
|
236
|
+
try {
|
|
237
|
+
const { projectPath } = ctx.request.body;
|
|
238
|
+
if (!projectPath) {
|
|
239
|
+
ctx.status = 400;
|
|
240
|
+
ctx.body = { error: "项目路径不能为空" };
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const gitInfo = await this.parseGitRepo(projectPath);
|
|
244
|
+
ctx.body = gitInfo;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
ctx.status = 500;
|
|
248
|
+
ctx.body = { error: error.message };
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
/** 选择文件夹(使用系统对话框) */
|
|
252
|
+
this.router.post("/select-folder", async (ctx) => {
|
|
253
|
+
try {
|
|
254
|
+
const folderPath = await this.selectFolder();
|
|
255
|
+
ctx.body = { path: folderPath };
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
ctx.status = 500;
|
|
259
|
+
ctx.body = { error: error.message };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
/** 测试 Jenkins 部署(不依赖配置文件) */
|
|
263
|
+
this.router.post("/test-jenkins-deploy", async (ctx) => {
|
|
264
|
+
try {
|
|
265
|
+
const { url, username, token, jobName, parameters } = ctx.request.body;
|
|
266
|
+
console.log('Jenkins 测试部署请求:');
|
|
267
|
+
console.log(' url:', url);
|
|
268
|
+
console.log(' username:', username);
|
|
269
|
+
console.log(' jobName:', jobName);
|
|
270
|
+
console.log(' parameters:', parameters);
|
|
271
|
+
if (!url || !username || !token || !jobName || !parameters) {
|
|
272
|
+
ctx.status = 400;
|
|
273
|
+
ctx.body = { error: "缺少必要参数" };
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
console.log('创建 Jenkins 客户端...');
|
|
277
|
+
/** 创建 Jenkins 客户端 */
|
|
278
|
+
const jenkinsClient = new jenkins_client_1.JenkinsClient(url, username, token);
|
|
279
|
+
console.log('创建 Jenkins 部署器...');
|
|
280
|
+
/** 创建 Jenkins 部署器 */
|
|
281
|
+
const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer")));
|
|
282
|
+
const deployer = new JenkinsDeployer(jenkinsClient);
|
|
283
|
+
console.log('开始触发部署...');
|
|
284
|
+
/** 触发部署并等待完成 */
|
|
285
|
+
const result = await deployer.deploy(jobName, parameters, {
|
|
286
|
+
pollInterval: 10000, // 10 秒
|
|
287
|
+
timeout: 30 * 60 * 1000, // 30 分钟
|
|
288
|
+
onProgress: (build) => {
|
|
289
|
+
console.log(`部署进度: #${build.number} - ${build.result || "BUILDING"}`);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
console.log('部署成功:', result);
|
|
293
|
+
ctx.body = {
|
|
294
|
+
success: true,
|
|
295
|
+
buildNumber: result.number,
|
|
296
|
+
result: result.result,
|
|
297
|
+
url: result.url,
|
|
298
|
+
message: "部署成功",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error('Jenkins 测试部署失败:', error);
|
|
303
|
+
console.error('错误堆栈:', error.stack);
|
|
304
|
+
ctx.status = 500;
|
|
305
|
+
ctx.body = { error: error.message };
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
/** 触发 Jenkins 部署 */
|
|
309
|
+
this.router.post("/deploy", async (ctx) => {
|
|
310
|
+
try {
|
|
311
|
+
const { jenkinsType, jobName, parameters } = ctx.request.body;
|
|
312
|
+
console.log('Jenkins 部署请求:');
|
|
313
|
+
console.log(' jenkinsType:', jenkinsType);
|
|
314
|
+
console.log(' jobName:', jobName);
|
|
315
|
+
console.log(' parameters:', parameters);
|
|
316
|
+
if (!jenkinsType || !jobName || !parameters) {
|
|
317
|
+
ctx.status = 400;
|
|
318
|
+
ctx.body = { error: "缺少必要参数" };
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
/** 加载配置 */
|
|
322
|
+
const config = await this.configManager.loadConfig();
|
|
323
|
+
console.log('已加载配置,Jenkins 实例数量:', config.jenkins.length);
|
|
324
|
+
/** 查找对应的 Jenkins 实例 */
|
|
325
|
+
const jenkinsConfig = config.jenkins.find((j) => j.type === jenkinsType);
|
|
326
|
+
if (!jenkinsConfig) {
|
|
327
|
+
console.error('未找到 Jenkins 实例:', jenkinsType);
|
|
328
|
+
ctx.status = 400;
|
|
329
|
+
ctx.body = { error: `未找到 Jenkins 实例: ${jenkinsType}` };
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.log('找到 Jenkins 配置:', jenkinsConfig.url);
|
|
333
|
+
/** 获取缓存的明文 token */
|
|
334
|
+
const apiToken = await this.configManager.getJenkinsToken(jenkinsType);
|
|
335
|
+
if (!apiToken) {
|
|
336
|
+
console.error('未找到 Jenkins token');
|
|
337
|
+
ctx.status = 400;
|
|
338
|
+
ctx.body = {
|
|
339
|
+
error: `未找到 Jenkins 凭证,请先在配置页面保存 Jenkins 配置`,
|
|
340
|
+
};
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log('已获取 Jenkins token');
|
|
344
|
+
/** 创建 Jenkins 客户端 */
|
|
345
|
+
const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client")));
|
|
346
|
+
const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, apiToken);
|
|
347
|
+
console.log('创建 Jenkins 客户端成功');
|
|
348
|
+
/** 创建 Jenkins 部署器 */
|
|
349
|
+
const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer")));
|
|
350
|
+
const deployer = new JenkinsDeployer(jenkinsClient);
|
|
351
|
+
console.log('开始触发部署...');
|
|
352
|
+
/** 触发部署并等待完成 */
|
|
353
|
+
const result = await deployer.deploy(jobName, parameters, {
|
|
354
|
+
pollInterval: 10000, // 10 秒
|
|
355
|
+
timeout: 30 * 60 * 1000, // 30 分钟
|
|
356
|
+
onProgress: (build) => {
|
|
357
|
+
console.log(`部署进度: #${build.number} - ${build.result || "BUILDING"}`);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
console.log('部署成功:', result);
|
|
361
|
+
ctx.body = {
|
|
362
|
+
success: true,
|
|
363
|
+
buildNumber: result.number,
|
|
364
|
+
result: result.result,
|
|
365
|
+
url: result.url,
|
|
366
|
+
message: "部署成功",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
console.error('Jenkins 部署失败:', error);
|
|
371
|
+
console.error('错误堆栈:', error.stack);
|
|
372
|
+
ctx.status = 500;
|
|
373
|
+
ctx.body = { error: error.message };
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
/** 一键部署 - SSE 版本 */
|
|
377
|
+
this.router.get("/full-deploy-stream", async (ctx) => {
|
|
378
|
+
const { projectPath, environment } = ctx.query;
|
|
379
|
+
console.log('一键部署请求 (SSE):');
|
|
380
|
+
console.log(' projectPath:', projectPath);
|
|
381
|
+
console.log(' environment:', environment);
|
|
382
|
+
if (!projectPath || !environment) {
|
|
383
|
+
ctx.status = 400;
|
|
384
|
+
ctx.body = { error: "缺少必要参数" };
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// 设置 SSE 响应头
|
|
388
|
+
ctx.set({
|
|
389
|
+
'Content-Type': 'text/event-stream',
|
|
390
|
+
'Cache-Control': 'no-cache',
|
|
391
|
+
'Connection': 'keep-alive',
|
|
392
|
+
});
|
|
393
|
+
ctx.status = 200;
|
|
394
|
+
// 发送 SSE 消息的辅助函数
|
|
395
|
+
const sendEvent = (event, data) => {
|
|
396
|
+
ctx.res.write(`event: ${event}\n`);
|
|
397
|
+
ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
sendEvent('log', { message: '开始一键部署...', type: 'info' });
|
|
401
|
+
sendEvent('log', { message: `项目: ${projectPath}`, type: 'info' });
|
|
402
|
+
sendEvent('log', { message: `环境: ${environment}`, type: 'info' });
|
|
403
|
+
/** 加载配置 */
|
|
404
|
+
const config = await this.configManager.loadConfig();
|
|
405
|
+
const projectConfig = config.projects[projectPath];
|
|
406
|
+
if (!projectConfig) {
|
|
407
|
+
sendEvent('error', { message: `未找到项目配置: ${projectPath}` });
|
|
408
|
+
ctx.res.end();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const environmentConfig = projectConfig.environments[environment];
|
|
412
|
+
if (!environmentConfig) {
|
|
413
|
+
sendEvent('error', { message: `未找到环境配置: ${environment}` });
|
|
414
|
+
ctx.res.end();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
/** 获取 Bitbucket 凭证 */
|
|
418
|
+
const bitbucketPassword = await this.configManager.getBitbucketPassword();
|
|
419
|
+
if (!bitbucketPassword) {
|
|
420
|
+
sendEvent('error', { message: '未找到 Bitbucket 凭证' });
|
|
421
|
+
ctx.res.end();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
/** 获取 Jenkins 凭证 */
|
|
425
|
+
const jenkinsConfig = config.jenkins.find((j) => j.type === projectConfig.jenkinsInstance);
|
|
426
|
+
if (!jenkinsConfig) {
|
|
427
|
+
sendEvent('error', { message: `未找到 Jenkins 实例: ${projectConfig.jenkinsInstance}` });
|
|
428
|
+
ctx.res.end();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const jenkinsToken = await this.configManager.getJenkinsToken(projectConfig.jenkinsInstance);
|
|
432
|
+
if (!jenkinsToken) {
|
|
433
|
+
sendEvent('error', { message: '未找到 Jenkins 凭证' });
|
|
434
|
+
ctx.res.end();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword);
|
|
438
|
+
const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client")));
|
|
439
|
+
const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, jenkinsToken);
|
|
440
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
441
|
+
/** 步骤 1: 创建并推送 Tag */
|
|
442
|
+
sendEvent('log', { message: '步骤 1/3: 创建并推送 Tag', type: 'info' });
|
|
443
|
+
sendEvent('log', { message: '获取最新 tag...', type: 'info' });
|
|
444
|
+
const tagFormat = environmentConfig.tagFormat;
|
|
445
|
+
const tags = execSync('git tag --sort=-version:refname', {
|
|
446
|
+
cwd: projectPath,
|
|
447
|
+
encoding: 'utf-8',
|
|
448
|
+
}).trim().split('\n').filter(t => t);
|
|
449
|
+
const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`);
|
|
450
|
+
const regex = new RegExp(`^${formatPattern}$`);
|
|
451
|
+
const matchingTags = tags.filter(tag => regex.test(tag));
|
|
452
|
+
let newTag;
|
|
453
|
+
if (matchingTags.length === 0) {
|
|
454
|
+
newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0'));
|
|
455
|
+
sendEvent('log', { message: `没有匹配的 tag,创建第一个: ${newTag}`, type: 'info' });
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const latestTag = matchingTags[0];
|
|
459
|
+
sendEvent('log', { message: `最新 tag: ${latestTag}`, type: 'info' });
|
|
460
|
+
const numbers = latestTag.match(/\d+/g);
|
|
461
|
+
if (!numbers || numbers.length === 0) {
|
|
462
|
+
throw new Error('无法从 tag 中提取数字');
|
|
463
|
+
}
|
|
464
|
+
const lastNumber = numbers[numbers.length - 1];
|
|
465
|
+
const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0');
|
|
466
|
+
let count = 0;
|
|
467
|
+
newTag = latestTag.replace(/\d+/g, (match) => {
|
|
468
|
+
count++;
|
|
469
|
+
return count === numbers.length ? incrementedNumber : match;
|
|
470
|
+
});
|
|
471
|
+
sendEvent('log', { message: `新 tag: ${newTag}`, type: 'info' });
|
|
472
|
+
}
|
|
473
|
+
sendEvent('log', { message: '创建 tag...', type: 'info' });
|
|
474
|
+
execSync(`git tag ${newTag}`, { cwd: projectPath });
|
|
475
|
+
sendEvent('log', { message: '推送 tag...', type: 'info' });
|
|
476
|
+
execSync(`git push origin ${newTag}`, { cwd: projectPath });
|
|
477
|
+
sendEvent('log', { message: `Tag 创建并推送成功: ${newTag}`, type: 'success' });
|
|
478
|
+
/** 更新部署状态到配置文件 */
|
|
479
|
+
await this.configManager.updateDeployStatus(projectPath, environment, {
|
|
480
|
+
status: 'building',
|
|
481
|
+
tagName: newTag,
|
|
482
|
+
startTime: new Date().toISOString(),
|
|
483
|
+
step: 'pipeline',
|
|
484
|
+
});
|
|
485
|
+
/** 步骤 2: 监听构建状态 */
|
|
486
|
+
sendEvent('log', { message: '步骤 2/3: 监听构建状态', type: 'info' });
|
|
487
|
+
sendEvent('log', { message: '等待构建状态...', type: 'info' });
|
|
488
|
+
const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor")));
|
|
489
|
+
const monitor = new PipelineMonitor(bitbucketClient);
|
|
490
|
+
const buildResult = await monitor.monitorBuildStatus(projectConfig.repository.workspace, projectConfig.repository.repoSlug, newTag, {
|
|
491
|
+
pollInterval: 15000, // 15 秒轮询一次
|
|
492
|
+
timeout: 30 * 60 * 1000, // 30 分钟超时
|
|
493
|
+
onProgress: (data) => {
|
|
494
|
+
const statuses = data.statuses || [];
|
|
495
|
+
if (statuses.length > 0) {
|
|
496
|
+
statuses.forEach((status) => {
|
|
497
|
+
// 计算已用时间
|
|
498
|
+
let elapsedTime = '';
|
|
499
|
+
if (status.created_on) {
|
|
500
|
+
const createdTime = new Date(status.created_on).getTime();
|
|
501
|
+
const now = Date.now();
|
|
502
|
+
const elapsed = Math.floor((now - createdTime) / 1000);
|
|
503
|
+
const minutes = Math.floor(elapsed / 60);
|
|
504
|
+
const seconds = elapsed % 60;
|
|
505
|
+
elapsedTime = ` (${minutes}分${seconds}秒)`;
|
|
506
|
+
}
|
|
507
|
+
// 直接使用 status.state,它包含完整信息
|
|
508
|
+
const stateStr = typeof status.state === 'object' ? JSON.stringify(status.state) : status.state;
|
|
509
|
+
sendEvent('log', {
|
|
510
|
+
message: `${status.name || status.key}: ${stateStr}${elapsedTime}`,
|
|
511
|
+
type: status.state === 'SUCCESSFUL' ? 'success' :
|
|
512
|
+
status.state === 'FAILED' ? 'error' : 'info'
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
sendEvent('log', { message: `构建完成`, type: 'success' });
|
|
519
|
+
/** 更新部署状态 */
|
|
520
|
+
await this.configManager.updateDeployStatus(projectPath, environment, {
|
|
521
|
+
status: 'deploying',
|
|
522
|
+
tagName: newTag,
|
|
523
|
+
step: 'jenkins',
|
|
524
|
+
});
|
|
525
|
+
/** 步骤 3: Jenkins 部署 */
|
|
526
|
+
sendEvent('log', { message: '步骤 3/3: Jenkins 部署', type: 'info' });
|
|
527
|
+
sendEvent('log', { message: `触发 Jenkins Job: ${environmentConfig.jenkinsJobName}`, type: 'info' });
|
|
528
|
+
const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer")));
|
|
529
|
+
const deployer = new JenkinsDeployer(jenkinsClient);
|
|
530
|
+
const jenkinsResult = await deployer.deploy(environmentConfig.jenkinsJobName, { action: 'approve' }, // 固定传 approve
|
|
531
|
+
{
|
|
532
|
+
pollInterval: 10000,
|
|
533
|
+
timeout: 30 * 60 * 1000,
|
|
534
|
+
onProgress: (build) => {
|
|
535
|
+
const resultIcon = build.result === 'SUCCESS' ? '' :
|
|
536
|
+
build.result === 'FAILURE' ? '' :
|
|
537
|
+
build.building ? '' : '';
|
|
538
|
+
sendEvent('log', {
|
|
539
|
+
message: `${resultIcon} 构建 #${build.number}: ${build.result || 'BUILDING'} (${new Date().toLocaleTimeString()})`,
|
|
540
|
+
type: build.result === 'SUCCESS' ? 'success' :
|
|
541
|
+
build.result === 'FAILURE' ? 'error' : 'info'
|
|
542
|
+
});
|
|
543
|
+
if (build.duration > 0) {
|
|
544
|
+
const durationMin = Math.floor(build.duration / 60000);
|
|
545
|
+
const durationSec = Math.floor((build.duration % 60000) / 1000);
|
|
546
|
+
sendEvent('log', { message: ` 耗时: ${durationMin}分${durationSec}秒`, type: 'info' });
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
sendEvent('log', { message: `Jenkins 部署成功`, type: 'success' });
|
|
551
|
+
sendEvent('log', { message: `构建号: #${jenkinsResult.number}`, type: 'info' });
|
|
552
|
+
sendEvent('log', { message: `结果: ${jenkinsResult.result}`, type: 'success' });
|
|
553
|
+
sendEvent('log', { message: `构建 URL: ${jenkinsResult.url}`, type: 'info' });
|
|
554
|
+
/** 更新部署状态为完成 */
|
|
555
|
+
await this.configManager.updateDeployStatus(projectPath, environment, {
|
|
556
|
+
status: 'success',
|
|
557
|
+
tagName: newTag,
|
|
558
|
+
step: 'completed',
|
|
559
|
+
jenkinsBuildNumber: jenkinsResult.number,
|
|
560
|
+
jenkinsBuildUrl: jenkinsResult.url,
|
|
561
|
+
completedTime: new Date().toISOString(),
|
|
562
|
+
});
|
|
563
|
+
sendEvent('complete', {
|
|
564
|
+
tagName: newTag,
|
|
565
|
+
buildStatuses: buildResult,
|
|
566
|
+
jenkinsBuildNumber: jenkinsResult.number,
|
|
567
|
+
jenkinsBuildUrl: jenkinsResult.url,
|
|
568
|
+
message: '一键部署成功',
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
console.error('一键部署失败:', error);
|
|
573
|
+
sendEvent('error', { message: error.message });
|
|
574
|
+
/** 更新部署状态为失败 */
|
|
575
|
+
try {
|
|
576
|
+
const config = await this.configManager.loadConfig();
|
|
577
|
+
const projectConfig = config.projects[projectPath];
|
|
578
|
+
if (projectConfig) {
|
|
579
|
+
const currentStatus = projectConfig.environments[environment]?.deployStatus;
|
|
580
|
+
await this.configManager.updateDeployStatus(projectPath, environment, {
|
|
581
|
+
status: 'failed',
|
|
582
|
+
tagName: currentStatus?.tagName || '',
|
|
583
|
+
step: currentStatus?.step || 'unknown',
|
|
584
|
+
error: error.message,
|
|
585
|
+
failedTime: new Date().toISOString(),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (updateError) {
|
|
590
|
+
console.error('更新失败状态时出错:', updateError);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
ctx.res.end();
|
|
594
|
+
});
|
|
595
|
+
/** 完整部署流程 */
|
|
596
|
+
this.router.post("/full-deploy", async (ctx) => {
|
|
597
|
+
try {
|
|
598
|
+
const { projectPath, environment, createNewTag, tagName } = ctx.request.body;
|
|
599
|
+
if (!projectPath || !environment) {
|
|
600
|
+
ctx.status = 400;
|
|
601
|
+
ctx.body = { error: "缺少必要参数" };
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!createNewTag && !tagName) {
|
|
605
|
+
ctx.status = 400;
|
|
606
|
+
ctx.body = { error: "使用现有 tag 时必须提供 tag 名称" };
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
/** 加载配置 */
|
|
610
|
+
const config = await this.configManager.loadConfig();
|
|
611
|
+
/** 获取项目配置 */
|
|
612
|
+
const projectConfig = config.projects[projectPath];
|
|
613
|
+
if (!projectConfig) {
|
|
614
|
+
ctx.status = 400;
|
|
615
|
+
ctx.body = { error: `未找到项目配置: ${projectPath}` };
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
/** 获取环境配置 */
|
|
619
|
+
const environmentConfig = projectConfig.environments[environment];
|
|
620
|
+
if (!environmentConfig) {
|
|
621
|
+
ctx.status = 400;
|
|
622
|
+
ctx.body = { error: `未找到环境配置: ${environment}` };
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
/** 获取 Bitbucket 凭证 */
|
|
626
|
+
const bitbucketPassword = await this.configManager.getBitbucketPassword();
|
|
627
|
+
if (!bitbucketPassword) {
|
|
628
|
+
ctx.status = 400;
|
|
629
|
+
ctx.body = {
|
|
630
|
+
error: "未找到 Bitbucket 凭证,请先在配置页面保存配置",
|
|
631
|
+
};
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
/** 获取 Jenkins 凭证 */
|
|
635
|
+
const jenkinsConfig = config.jenkins.find((j) => j.type === projectConfig.jenkinsInstance);
|
|
636
|
+
if (!jenkinsConfig) {
|
|
637
|
+
ctx.status = 400;
|
|
638
|
+
ctx.body = {
|
|
639
|
+
error: `未找到 Jenkins 实例: ${projectConfig.jenkinsInstance}`,
|
|
640
|
+
};
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const jenkinsToken = await this.configManager.getJenkinsToken(projectConfig.jenkinsInstance);
|
|
644
|
+
if (!jenkinsToken) {
|
|
645
|
+
ctx.status = 400;
|
|
646
|
+
ctx.body = {
|
|
647
|
+
error: "未找到 Jenkins 凭证,请先在配置页面保存配置",
|
|
648
|
+
};
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
/** 创建客户端 */
|
|
652
|
+
const { BitbucketClient } = await Promise.resolve().then(() => __importStar(require("../api/bitbucket-client")));
|
|
653
|
+
const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client")));
|
|
654
|
+
const bitbucketClient = new BitbucketClient(config.bitbucket.username, bitbucketPassword);
|
|
655
|
+
const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, jenkinsToken);
|
|
656
|
+
/** 创建完整部署器 */
|
|
657
|
+
const { FullDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/full-deployer")));
|
|
658
|
+
const deployer = new FullDeployer(bitbucketClient, jenkinsClient, projectConfig, environmentConfig);
|
|
659
|
+
/** 执行部署 */
|
|
660
|
+
const result = await deployer.deploy({
|
|
661
|
+
createNewTag,
|
|
662
|
+
tagName,
|
|
663
|
+
onProgress: (progress) => {
|
|
664
|
+
console.log(`[${progress.step}] ${progress.message}`);
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
ctx.body = result;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
ctx.status = 500;
|
|
671
|
+
ctx.body = { error: error.message };
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
/** 查询固定 Tag 的构建状态 - SSE 版本 */
|
|
675
|
+
this.router.get("/query-tag-status-stream", async (ctx) => {
|
|
676
|
+
const { workspace, repoSlug, fixedTag } = ctx.query;
|
|
677
|
+
console.log('查询固定 Tag 请求 (SSE):');
|
|
678
|
+
console.log(' workspace:', workspace);
|
|
679
|
+
console.log(' repoSlug:', repoSlug);
|
|
680
|
+
console.log(' fixedTag:', fixedTag);
|
|
681
|
+
if (!workspace || !repoSlug || !fixedTag) {
|
|
682
|
+
ctx.status = 400;
|
|
683
|
+
ctx.body = { error: "缺少必要参数" };
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// 设置 SSE 响应头
|
|
687
|
+
ctx.set({
|
|
688
|
+
'Content-Type': 'text/event-stream',
|
|
689
|
+
'Cache-Control': 'no-cache',
|
|
690
|
+
'Connection': 'keep-alive',
|
|
691
|
+
});
|
|
692
|
+
ctx.status = 200;
|
|
693
|
+
// 发送 SSE 消息的辅助函数
|
|
694
|
+
const sendEvent = (event, data) => {
|
|
695
|
+
ctx.res.write(`event: ${event}\n`);
|
|
696
|
+
ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
697
|
+
};
|
|
698
|
+
try {
|
|
699
|
+
sendEvent('log', { message: `查询 Tag: ${fixedTag}`, type: 'info' });
|
|
700
|
+
sendEvent('log', { message: `仓库: ${workspace}/${repoSlug}`, type: 'info' });
|
|
701
|
+
/** 获取 Bitbucket 凭证 */
|
|
702
|
+
const bitbucketPassword = await this.configManager.getBitbucketPassword();
|
|
703
|
+
if (!bitbucketPassword) {
|
|
704
|
+
sendEvent('error', { message: '未找到 Bitbucket 凭证' });
|
|
705
|
+
ctx.res.end();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const config = await this.configManager.loadConfig();
|
|
709
|
+
const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword);
|
|
710
|
+
sendEvent('log', { message: '获取 tag 对应的 commit...', type: 'info' });
|
|
711
|
+
/** 获取 tag 对应的 commit hash */
|
|
712
|
+
const commitHash = await bitbucketClient.getTagCommit(workspace, repoSlug, fixedTag);
|
|
713
|
+
sendEvent('log', { message: `Commit: ${commitHash.substring(0, 7)}`, type: 'success' });
|
|
714
|
+
sendEvent('log', { message: '查询构建状态...', type: 'info' });
|
|
715
|
+
/** 获取 commit 的 build status */
|
|
716
|
+
const statuses = await bitbucketClient.getCommitBuildStatus(workspace, repoSlug, commitHash);
|
|
717
|
+
if (statuses.length === 0) {
|
|
718
|
+
sendEvent('log', { message: '未找到构建状态', type: 'warning' });
|
|
719
|
+
sendEvent('complete', {
|
|
720
|
+
tagName: fixedTag,
|
|
721
|
+
commitHash,
|
|
722
|
+
buildStatuses: [],
|
|
723
|
+
message: '未找到构建状态',
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
sendEvent('log', { message: `找到 ${statuses.length} 个构建状态`, type: 'info' });
|
|
728
|
+
sendEvent('log', { message: '', type: 'info' }); // 空行
|
|
729
|
+
statuses.forEach((status) => {
|
|
730
|
+
// 计算已用时间
|
|
731
|
+
let elapsedTime = '';
|
|
732
|
+
if (status.created_on) {
|
|
733
|
+
const createdTime = new Date(status.created_on).getTime();
|
|
734
|
+
const now = Date.now();
|
|
735
|
+
const elapsed = Math.floor((now - createdTime) / 1000);
|
|
736
|
+
const minutes = Math.floor(elapsed / 60);
|
|
737
|
+
const seconds = elapsed % 60;
|
|
738
|
+
elapsedTime = ` (${minutes}分${seconds}秒)`;
|
|
739
|
+
}
|
|
740
|
+
// 直接使用 status.state,它包含完整信息
|
|
741
|
+
const stateStr = typeof status.state === 'object' ? JSON.stringify(status.state) : status.state;
|
|
742
|
+
sendEvent('log', {
|
|
743
|
+
message: `${status.name || status.key}: ${stateStr}${elapsedTime}`,
|
|
744
|
+
type: status.state === 'SUCCESSFUL' ? 'success' :
|
|
745
|
+
status.state === 'FAILED' ? 'error' : 'info'
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
sendEvent('log', { message: '', type: 'info' }); // 空行
|
|
749
|
+
const allSuccessful = statuses.every((s) => s.state === 'SUCCESSFUL');
|
|
750
|
+
const anyFailed = statuses.some((s) => s.state === 'FAILED');
|
|
751
|
+
const anyInProgress = statuses.some((s) => s.state === 'INPROGRESS');
|
|
752
|
+
if (allSuccessful) {
|
|
753
|
+
sendEvent('log', { message: '所有构建已成功完成', type: 'success' });
|
|
754
|
+
}
|
|
755
|
+
else if (anyFailed) {
|
|
756
|
+
sendEvent('log', { message: '部分构建失败', type: 'error' });
|
|
757
|
+
}
|
|
758
|
+
else if (anyInProgress) {
|
|
759
|
+
sendEvent('log', { message: '部分构建仍在进行中', type: 'info' });
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
sendEvent('log', { message: '构建状态未知', type: 'warning' });
|
|
763
|
+
}
|
|
764
|
+
sendEvent('complete', {
|
|
765
|
+
tagName: fixedTag,
|
|
766
|
+
commitHash,
|
|
767
|
+
buildStatuses: statuses,
|
|
768
|
+
message: '查询完成',
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
console.error('查询失败:', error);
|
|
774
|
+
sendEvent('error', { message: error.message });
|
|
775
|
+
}
|
|
776
|
+
ctx.res.end();
|
|
777
|
+
});
|
|
778
|
+
/** 测试 Pipeline(创建 Tag 并监听)- SSE 版本 */
|
|
779
|
+
this.router.get("/test-pipeline-stream", async (ctx) => {
|
|
780
|
+
const { projectPath, workspace, repoSlug, tagFormat } = ctx.query;
|
|
781
|
+
console.log('Pipeline 测试请求 (SSE):');
|
|
782
|
+
console.log(' projectPath:', projectPath);
|
|
783
|
+
console.log(' workspace:', workspace);
|
|
784
|
+
console.log(' repoSlug:', repoSlug);
|
|
785
|
+
console.log(' tagFormat:', tagFormat);
|
|
786
|
+
if (!projectPath || !workspace || !repoSlug || !tagFormat) {
|
|
787
|
+
ctx.status = 400;
|
|
788
|
+
ctx.body = { error: "缺少必要参数" };
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
// 设置 SSE 响应头
|
|
792
|
+
ctx.set({
|
|
793
|
+
'Content-Type': 'text/event-stream',
|
|
794
|
+
'Cache-Control': 'no-cache',
|
|
795
|
+
'Connection': 'keep-alive',
|
|
796
|
+
});
|
|
797
|
+
ctx.status = 200;
|
|
798
|
+
// 发送 SSE 消息的辅助函数
|
|
799
|
+
const sendEvent = (event, data) => {
|
|
800
|
+
ctx.res.write(`event: ${event}\n`);
|
|
801
|
+
ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
802
|
+
};
|
|
803
|
+
try {
|
|
804
|
+
sendEvent('log', { message: '开始创建 Tag...', type: 'info' });
|
|
805
|
+
sendEvent('log', { message: `项目: ${projectPath}`, type: 'info' });
|
|
806
|
+
sendEvent('log', { message: `仓库: ${workspace}/${repoSlug}`, type: 'info' });
|
|
807
|
+
sendEvent('log', { message: `格式: ${tagFormat}`, type: 'info' });
|
|
808
|
+
/** 获取 Bitbucket 凭证 */
|
|
809
|
+
const bitbucketPassword = await this.configManager.getBitbucketPassword();
|
|
810
|
+
if (!bitbucketPassword) {
|
|
811
|
+
sendEvent('error', { message: '未找到 Bitbucket 凭证' });
|
|
812
|
+
ctx.res.end();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const config = await this.configManager.loadConfig();
|
|
816
|
+
const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword);
|
|
817
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
818
|
+
sendEvent('log', { message: '获取最新 tag...', type: 'info' });
|
|
819
|
+
/** 获取匹配格式的最新 tag */
|
|
820
|
+
const tags = execSync('git tag --sort=-version:refname', {
|
|
821
|
+
cwd: projectPath,
|
|
822
|
+
encoding: 'utf-8',
|
|
823
|
+
}).trim().split('\n').filter(t => t);
|
|
824
|
+
const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`);
|
|
825
|
+
const regex = new RegExp(`^${formatPattern}$`);
|
|
826
|
+
const matchingTags = tags.filter(tag => regex.test(tag));
|
|
827
|
+
let newTag;
|
|
828
|
+
if (matchingTags.length === 0) {
|
|
829
|
+
newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0'));
|
|
830
|
+
sendEvent('log', { message: `没有匹配的 tag,创建第一个: ${newTag}`, type: 'info' });
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const latestTag = matchingTags[0];
|
|
834
|
+
sendEvent('log', { message: `最新 tag: ${latestTag}`, type: 'info' });
|
|
835
|
+
const numbers = latestTag.match(/\d+/g);
|
|
836
|
+
if (!numbers || numbers.length === 0) {
|
|
837
|
+
throw new Error('无法从 tag 中提取数字');
|
|
838
|
+
}
|
|
839
|
+
const lastNumber = numbers[numbers.length - 1];
|
|
840
|
+
const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0');
|
|
841
|
+
let count = 0;
|
|
842
|
+
newTag = latestTag.replace(/\d+/g, (match) => {
|
|
843
|
+
count++;
|
|
844
|
+
return count === numbers.length ? incrementedNumber : match;
|
|
845
|
+
});
|
|
846
|
+
sendEvent('log', { message: `新 tag: ${newTag}`, type: 'info' });
|
|
847
|
+
}
|
|
848
|
+
/** 创建并推送 tag */
|
|
849
|
+
sendEvent('log', { message: '创建 tag...', type: 'info' });
|
|
850
|
+
execSync(`git tag ${newTag}`, { cwd: projectPath });
|
|
851
|
+
sendEvent('log', { message: '推送 tag...', type: 'info' });
|
|
852
|
+
execSync(`git push origin ${newTag}`, { cwd: projectPath });
|
|
853
|
+
sendEvent('log', { message: `Tag 创建并推送成功: ${newTag}`, type: 'success' });
|
|
854
|
+
/** 监听 Build Status */
|
|
855
|
+
sendEvent('log', { message: '等待构建状态...', type: 'info' });
|
|
856
|
+
const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor")));
|
|
857
|
+
const monitor = new PipelineMonitor(bitbucketClient);
|
|
858
|
+
const result = await monitor.monitorBuildStatus(workspace, repoSlug, newTag, {
|
|
859
|
+
pollInterval: 15000, // 15 秒
|
|
860
|
+
timeout: 30 * 60 * 1000, // 30 分钟
|
|
861
|
+
onProgress: (data) => {
|
|
862
|
+
const statuses = data.statuses || [];
|
|
863
|
+
statuses.forEach((status) => {
|
|
864
|
+
sendEvent('log', { message: `${status.name || status.key}: ${status.state}`, type: 'info' });
|
|
865
|
+
});
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
sendEvent('log', { message: `构建完成`, type: 'success' });
|
|
869
|
+
sendEvent('complete', {
|
|
870
|
+
tagName: newTag,
|
|
871
|
+
buildStatuses: result,
|
|
872
|
+
message: 'Tag 创建成功,构建完成',
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
console.error('Pipeline 测试失败:', error);
|
|
877
|
+
sendEvent('error', { message: error.message });
|
|
878
|
+
}
|
|
879
|
+
ctx.res.end();
|
|
880
|
+
});
|
|
881
|
+
/** 测试 Pipeline(创建 Tag 并监听) */
|
|
882
|
+
this.router.post("/test-pipeline", async (ctx) => {
|
|
883
|
+
try {
|
|
884
|
+
const { projectPath, workspace, repoSlug, tagFormat } = ctx.request.body;
|
|
885
|
+
console.log('Pipeline 测试请求:');
|
|
886
|
+
console.log(' projectPath:', projectPath);
|
|
887
|
+
console.log(' workspace:', workspace);
|
|
888
|
+
console.log(' repoSlug:', repoSlug);
|
|
889
|
+
console.log(' tagFormat:', tagFormat);
|
|
890
|
+
if (!projectPath || !workspace || !repoSlug || !tagFormat) {
|
|
891
|
+
ctx.status = 400;
|
|
892
|
+
ctx.body = { error: "缺少必要参数" };
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
/** 获取 Bitbucket 凭证 */
|
|
896
|
+
const bitbucketPassword = await this.configManager.getBitbucketPassword();
|
|
897
|
+
if (!bitbucketPassword) {
|
|
898
|
+
ctx.status = 400;
|
|
899
|
+
ctx.body = {
|
|
900
|
+
error: "未找到 Bitbucket 凭证,请先在配置页面保存配置",
|
|
901
|
+
};
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const config = await this.configManager.loadConfig();
|
|
905
|
+
const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword);
|
|
906
|
+
/** 导入所需模块 */
|
|
907
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
908
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
909
|
+
console.log('获取最新 tag...');
|
|
910
|
+
/** 获取匹配格式的最新 tag */
|
|
911
|
+
try {
|
|
912
|
+
const tags = execSync('git tag --sort=-version:refname', {
|
|
913
|
+
cwd: projectPath,
|
|
914
|
+
encoding: 'utf-8',
|
|
915
|
+
}).trim().split('\n').filter(t => t);
|
|
916
|
+
console.log('所有 tags:', tags.slice(0, 10));
|
|
917
|
+
/** 提取格式中的数字部分模式 */
|
|
918
|
+
const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`);
|
|
919
|
+
const regex = new RegExp(`^${formatPattern}$`);
|
|
920
|
+
console.log('匹配模式:', formatPattern);
|
|
921
|
+
/** 找到匹配格式的最新 tag */
|
|
922
|
+
const matchingTags = tags.filter(tag => regex.test(tag));
|
|
923
|
+
console.log('匹配的 tags:', matchingTags.slice(0, 5));
|
|
924
|
+
let newTag;
|
|
925
|
+
if (matchingTags.length === 0) {
|
|
926
|
+
/** 没有匹配的 tag,使用格式本身(将第一个数字序列设为 1) */
|
|
927
|
+
newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0'));
|
|
928
|
+
console.log('没有匹配的 tag,创建第一个:', newTag);
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
/** 获取最新的 tag 并叠加 */
|
|
932
|
+
const latestTag = matchingTags[0];
|
|
933
|
+
console.log('最新 tag:', latestTag);
|
|
934
|
+
/** 提取所有数字序列并叠加最后一个 */
|
|
935
|
+
const numbers = latestTag.match(/\d+/g);
|
|
936
|
+
if (!numbers || numbers.length === 0) {
|
|
937
|
+
throw new Error('无法从 tag 中提取数字');
|
|
938
|
+
}
|
|
939
|
+
/** 叠加最后一个数字序列 */
|
|
940
|
+
const lastNumber = numbers[numbers.length - 1];
|
|
941
|
+
const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0');
|
|
942
|
+
/** 替换最后一个数字序列 */
|
|
943
|
+
let count = 0;
|
|
944
|
+
newTag = latestTag.replace(/\d+/g, (match) => {
|
|
945
|
+
count++;
|
|
946
|
+
return count === numbers.length ? incrementedNumber : match;
|
|
947
|
+
});
|
|
948
|
+
console.log('新 tag:', newTag);
|
|
949
|
+
}
|
|
950
|
+
/** 创建并推送 tag */
|
|
951
|
+
console.log('创建 tag...');
|
|
952
|
+
execSync(`git tag ${newTag}`, { cwd: projectPath });
|
|
953
|
+
console.log('推送 tag...');
|
|
954
|
+
execSync(`git push origin ${newTag}`, { cwd: projectPath });
|
|
955
|
+
console.log('Tag 创建并推送成功:', newTag);
|
|
956
|
+
/** 监听 Pipeline */
|
|
957
|
+
console.log('开始监听 Pipeline...');
|
|
958
|
+
const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor")));
|
|
959
|
+
const monitor = new PipelineMonitor(bitbucketClient);
|
|
960
|
+
const result = await monitor.monitorPipeline(workspace, repoSlug, newTag, {
|
|
961
|
+
pollInterval: 15000, // 15 秒
|
|
962
|
+
timeout: 30 * 60 * 1000, // 30 分钟
|
|
963
|
+
onProgress: (status) => {
|
|
964
|
+
console.log(`Pipeline 进度: ${status.state.name}`);
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
console.log('Pipeline 执行成功');
|
|
968
|
+
ctx.body = {
|
|
969
|
+
success: true,
|
|
970
|
+
tagName: newTag,
|
|
971
|
+
pipelineStatus: result.state.name,
|
|
972
|
+
message: "Tag 创建成功,Pipeline 执行完成",
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
console.error('执行失败:', error);
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
console.error('Pipeline 测试失败:', error);
|
|
982
|
+
console.error('错误堆栈:', error.stack);
|
|
983
|
+
ctx.status = 500;
|
|
984
|
+
ctx.body = { error: error.message };
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
/** 监听 Pipeline */
|
|
988
|
+
this.router.post("/monitor-pipeline", async (ctx) => {
|
|
989
|
+
try {
|
|
990
|
+
const { workspace, repoSlug, tagName } = ctx.request.body;
|
|
991
|
+
if (!workspace || !repoSlug || !tagName) {
|
|
992
|
+
ctx.status = 400;
|
|
993
|
+
ctx.body = { error: "缺少必要参数" };
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
/** 加载配置 */
|
|
997
|
+
const config = await this.configManager.loadConfig();
|
|
998
|
+
/** 获取缓存的明文密码 */
|
|
999
|
+
const appPassword = await this.configManager.getBitbucketPassword();
|
|
1000
|
+
if (!appPassword) {
|
|
1001
|
+
ctx.status = 400;
|
|
1002
|
+
ctx.body = {
|
|
1003
|
+
error: "未找到 Bitbucket 凭证,请先在配置页面保存 Bitbucket 配置",
|
|
1004
|
+
};
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
/** 创建 Bitbucket 客户端 */
|
|
1008
|
+
const { BitbucketClient } = await Promise.resolve().then(() => __importStar(require("../api/bitbucket-client")));
|
|
1009
|
+
const bitbucketClient = new BitbucketClient(config.bitbucket.username, appPassword);
|
|
1010
|
+
/** 创建 Pipeline 监听器 */
|
|
1011
|
+
const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor")));
|
|
1012
|
+
const monitor = new PipelineMonitor(bitbucketClient);
|
|
1013
|
+
/** 开始监听 */
|
|
1014
|
+
const result = await monitor.monitorPipeline(workspace, repoSlug, tagName, {
|
|
1015
|
+
pollInterval: 15000, // 15 秒
|
|
1016
|
+
timeout: 30 * 60 * 1000, // 30 分钟
|
|
1017
|
+
onProgress: (status) => {
|
|
1018
|
+
console.log(`Pipeline 进度: ${status.state.name}`);
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
ctx.body = {
|
|
1022
|
+
success: true,
|
|
1023
|
+
status: result.state.name,
|
|
1024
|
+
message: "Pipeline 执行成功",
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
ctx.status = 500;
|
|
1029
|
+
ctx.body = { error: error.message };
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
/** 测试 Bitbucket 连接 */
|
|
1033
|
+
this.router.post("/test-bitbucket", async (ctx) => {
|
|
1034
|
+
try {
|
|
1035
|
+
const { username, appPassword } = ctx.request.body;
|
|
1036
|
+
if (!username || !appPassword) {
|
|
1037
|
+
ctx.status = 400;
|
|
1038
|
+
ctx.body = { error: "用户名和 API Token 不能为空" };
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// 判断 appPassword 是加密的还是明文的
|
|
1042
|
+
let actualPassword = appPassword;
|
|
1043
|
+
// 如果是加密格式(包含冒号分隔的三部分),尝试解密
|
|
1044
|
+
if (appPassword.includes(':') && appPassword.split(':').length === 3) {
|
|
1045
|
+
try {
|
|
1046
|
+
actualPassword = this.configManager.decryptToken(appPassword);
|
|
1047
|
+
console.log('使用已保存的 token 测试连接');
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
console.error('解密失败,使用原始值:', error);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
console.log('使用新输入的 token 测试连接');
|
|
1055
|
+
}
|
|
1056
|
+
// 创建 Bitbucket 客户端并测试连接
|
|
1057
|
+
const client = new bitbucket_client_1.BitbucketClient(username, actualPassword);
|
|
1058
|
+
const isConnected = await client.testConnection();
|
|
1059
|
+
if (isConnected) {
|
|
1060
|
+
ctx.body = {
|
|
1061
|
+
success: true,
|
|
1062
|
+
message: "Bitbucket 连接成功",
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
ctx.status = 401;
|
|
1067
|
+
ctx.body = {
|
|
1068
|
+
success: false,
|
|
1069
|
+
error: "Bitbucket 连接失败,请检查用户名和 API Token",
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
ctx.status = 500;
|
|
1075
|
+
ctx.body = {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: `连接测试失败: ${error.message}`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
/** 测试 Jenkins 连接 */
|
|
1082
|
+
this.router.post("/test-jenkins", async (ctx) => {
|
|
1083
|
+
try {
|
|
1084
|
+
const { url, username, apiToken } = ctx.request.body;
|
|
1085
|
+
if (!url || !username || !apiToken) {
|
|
1086
|
+
ctx.status = 400;
|
|
1087
|
+
ctx.body = { error: "URL、用户名和 API Token 不能为空" };
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
// 判断 apiToken 是加密的还是明文的
|
|
1091
|
+
let actualToken = apiToken;
|
|
1092
|
+
// 如果是加密格式(包含冒号分隔的三部分),尝试解密
|
|
1093
|
+
if (apiToken.includes(':') && apiToken.split(':').length === 3) {
|
|
1094
|
+
try {
|
|
1095
|
+
actualToken = this.configManager.decryptToken(apiToken);
|
|
1096
|
+
console.log('使用已保存的 token 测试连接');
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
console.error('解密失败,使用原始值:', error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
console.log('使用新输入的 token 测试连接');
|
|
1104
|
+
}
|
|
1105
|
+
// 创建 Jenkins 客户端并测试连接
|
|
1106
|
+
const client = new jenkins_client_1.JenkinsClient(url, username, actualToken);
|
|
1107
|
+
const isConnected = await client.testConnection();
|
|
1108
|
+
if (isConnected) {
|
|
1109
|
+
ctx.body = {
|
|
1110
|
+
success: true,
|
|
1111
|
+
message: "Jenkins 连接成功",
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
ctx.status = 401;
|
|
1116
|
+
ctx.body = {
|
|
1117
|
+
success: false,
|
|
1118
|
+
error: "Jenkins 连接失败,请检查 URL、用户名和 API Token",
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
ctx.status = 500;
|
|
1124
|
+
ctx.body = {
|
|
1125
|
+
success: false,
|
|
1126
|
+
error: `连接测试失败: ${error.message}`,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
/** 应用路由 */
|
|
1131
|
+
this.app.use(this.router.routes());
|
|
1132
|
+
this.app.use(this.router.allowedMethods());
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* 启动服务器
|
|
1136
|
+
*/
|
|
1137
|
+
async start() {
|
|
1138
|
+
return new Promise((resolve, reject) => {
|
|
1139
|
+
try {
|
|
1140
|
+
this.server = this.app.listen(this.port, this.host, () => {
|
|
1141
|
+
const url = `http://${this.host}:${this.port}`;
|
|
1142
|
+
console.log(`\nWeb UI 服务器已启动: ${url}\n`);
|
|
1143
|
+
resolve();
|
|
1144
|
+
});
|
|
1145
|
+
this.server.on("error", (err) => {
|
|
1146
|
+
if (err.code === "EADDRINUSE") {
|
|
1147
|
+
console.error(`端口 ${this.port} 已被占用`);
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
console.error("服务器启动失败:", err);
|
|
1151
|
+
}
|
|
1152
|
+
reject(err);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
catch (err) {
|
|
1156
|
+
reject(err);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* 停止服务器
|
|
1162
|
+
*/
|
|
1163
|
+
async stop() {
|
|
1164
|
+
return new Promise((resolve, reject) => {
|
|
1165
|
+
if (this.server) {
|
|
1166
|
+
this.server.close((err) => {
|
|
1167
|
+
if (err) {
|
|
1168
|
+
console.error("服务器关闭失败:", err);
|
|
1169
|
+
reject(err);
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
console.log("Web UI 服务器已关闭");
|
|
1173
|
+
resolve();
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
resolve();
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 打开浏览器
|
|
1184
|
+
*/
|
|
1185
|
+
async openBrowser() {
|
|
1186
|
+
const url = `http://${this.host}:${this.port}`;
|
|
1187
|
+
try {
|
|
1188
|
+
await (0, open_1.default)(url);
|
|
1189
|
+
console.log(`🌐 已在浏览器中打开: ${url}`);
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
console.error("无法自动打开浏览器:", err);
|
|
1193
|
+
console.log(`请手动访问: ${url}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* 获取路由器实例(用于添加更多路由)
|
|
1198
|
+
*/
|
|
1199
|
+
getRouter() {
|
|
1200
|
+
return this.router;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* 解析 Git 仓库信息
|
|
1204
|
+
*/
|
|
1205
|
+
async parseGitRepo(projectPath) {
|
|
1206
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
1207
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1208
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1209
|
+
/** 检查路径是否存在 */
|
|
1210
|
+
try {
|
|
1211
|
+
await fs.access(projectPath);
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
throw new Error("项目路径不存在");
|
|
1215
|
+
}
|
|
1216
|
+
/** 检查是否是 Git 仓库 */
|
|
1217
|
+
const gitPath = path.join(projectPath, ".git");
|
|
1218
|
+
try {
|
|
1219
|
+
await fs.access(gitPath);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
throw new Error("该目录不是 Git 仓库,请先初始化 Git");
|
|
1223
|
+
}
|
|
1224
|
+
/** 获取 Git remote URL */
|
|
1225
|
+
try {
|
|
1226
|
+
const remoteUrl = execSync("git config --get remote.origin.url", {
|
|
1227
|
+
cwd: projectPath,
|
|
1228
|
+
encoding: "utf-8",
|
|
1229
|
+
}).trim();
|
|
1230
|
+
if (!remoteUrl) {
|
|
1231
|
+
throw new Error("未找到 Git remote origin");
|
|
1232
|
+
}
|
|
1233
|
+
/** 解析 Bitbucket URL */
|
|
1234
|
+
const bitbucketMatch = remoteUrl.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
|
|
1235
|
+
if (bitbucketMatch) {
|
|
1236
|
+
return {
|
|
1237
|
+
isGitRepo: true,
|
|
1238
|
+
workspace: bitbucketMatch[1],
|
|
1239
|
+
repoSlug: bitbucketMatch[2],
|
|
1240
|
+
remoteUrl,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
/** 如果不是 Bitbucket,返回基本信息 */
|
|
1244
|
+
return {
|
|
1245
|
+
isGitRepo: true,
|
|
1246
|
+
workspace: "",
|
|
1247
|
+
repoSlug: "",
|
|
1248
|
+
remoteUrl,
|
|
1249
|
+
warning: "未识别为 Bitbucket 仓库,请手动填写",
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
throw new Error(`获取 Git 信息失败: ${error.message}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* 选择文件夹(使用系统对话框)
|
|
1258
|
+
*/
|
|
1259
|
+
async selectFolder() {
|
|
1260
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1261
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1262
|
+
const platform = os.platform();
|
|
1263
|
+
try {
|
|
1264
|
+
if (platform === "darwin") {
|
|
1265
|
+
/** macOS: 使用 AppleScript */
|
|
1266
|
+
const script = `
|
|
1267
|
+
osascript -e 'POSIX path of (choose folder with prompt "选择项目文件夹")'
|
|
1268
|
+
`;
|
|
1269
|
+
const result = execSync(script, { encoding: "utf-8" }).trim();
|
|
1270
|
+
return result;
|
|
1271
|
+
}
|
|
1272
|
+
else if (platform === "win32") {
|
|
1273
|
+
/** Windows: 使用 PowerShell */
|
|
1274
|
+
const script = `
|
|
1275
|
+
Add-Type -AssemblyName System.Windows.Forms;
|
|
1276
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog;
|
|
1277
|
+
$dialog.Description = '选择项目文件夹';
|
|
1278
|
+
$dialog.ShowNewFolderButton = $false;
|
|
1279
|
+
if ($dialog.ShowDialog() -eq 'OK') {
|
|
1280
|
+
Write-Output $dialog.SelectedPath
|
|
1281
|
+
}
|
|
1282
|
+
`;
|
|
1283
|
+
const result = execSync(`powershell -Command "${script}"`, {
|
|
1284
|
+
encoding: "utf-8",
|
|
1285
|
+
}).trim();
|
|
1286
|
+
return result;
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
/** Linux: 使用 zenity(如果安装了) */
|
|
1290
|
+
try {
|
|
1291
|
+
const result = execSync('zenity --file-selection --directory --title="选择项目文件夹"', {
|
|
1292
|
+
encoding: "utf-8",
|
|
1293
|
+
}).trim();
|
|
1294
|
+
return result;
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
throw new Error("Linux 系统需要安装 zenity 才能使用文件夹选择功能");
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
if (error.message.includes("User canceled")) {
|
|
1303
|
+
throw new Error("用户取消了选择");
|
|
1304
|
+
}
|
|
1305
|
+
throw new Error(`选择文件夹失败: ${error.message}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
exports.WebUIServer = WebUIServer;
|
|
1310
|
+
//# sourceMappingURL=server.js.map
|