zen-gitsync 2.14.2 → 2.14.3
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/package.json +1 -1
- package/src/ui/public/assets/{AppVersionBadge-M9ydXpd9.js → AppVersionBadge-z2kqhmtD.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-scIk6OeM.js → BranchSelector-CSjsCeeJ.js} +1 -1
- package/src/ui/public/assets/{CommandConsole-BikQuarw.js → CommandConsole-96MUoG1F.js} +2 -2
- package/src/ui/public/assets/{CommitForm-DuvNxUdP.js → CommitForm-BBKQ0GqO.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-G17HYMln.js → CommonDialog-CgQovpCJ.js} +1 -1
- package/src/ui/public/assets/{EditorView-rGLnccH3.js → EditorView-aoA30emq.js} +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-B4BADP8e.js → FlowExecutionViewer-BvXlaQai.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-C3l3Jco7.js → FlowOrchestrationWorkspace-CgWUF1Jc.js} +1 -1
- package/src/ui/public/assets/{LogList-7agk5Obq.js → LogList-uF4tir23.js} +1 -1
- package/src/ui/public/assets/{MindmapView-mOFG9TmR.js → MindmapView-B8wr0EFL.js} +1 -1
- package/src/ui/public/assets/{MonitorView-Dlpgy8Bb.js → MonitorView-DaJ0h8Im.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-CgS14qyg.js → ProjectStartupButton-CcH1NL2W.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-CFKl6CB9.js → RemoteRepoCard-BDbJ76uf.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Dsc_a3t7.js → SourceMapView-2HjAlHnp.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Cw2mL_zx.js → SvgIcon-CK6VGHb5.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-yBT2005h.js → UserInputNode-Cx8r5geA.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-CiG-qMl7.js → WorkbenchView-CG54iND-.js} +4 -4
- package/src/ui/public/assets/WorkbenchView-CvHPPs8Q.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-BMXWW0fG.js → _plugin-vue_export-helper-CJMndG_W.js} +2 -2
- package/src/ui/public/assets/{configStore-Cq6Qwn35.js → configStore-DDnE5eie.js} +1 -1
- package/src/ui/public/assets/index-B7FGNx_l.js +23 -0
- package/src/ui/public/assets/{index-gbRjS0sL.css → index-f6UhuE5j.css} +1 -1
- package/src/ui/public/index.html +6 -6
- package/src/ui/server/index.js +632 -625
- package/src/ui/server/middleware/errorHandler.js +84 -0
- package/src/ui/server/middleware/errorHandler.test.js +114 -0
- package/src/ui/server/routes/exec.js +0 -21
- package/src/ui/server/routes/gitOps.js +62 -0
- package/src/ui/server/routes/monitor.js +42 -26
- package/src/ui/server/routes/terminal.js +318 -320
- package/src/ui/server/routes/workbench/attachmentUtils.js +76 -0
- package/src/ui/server/routes/workbench/index.js +1680 -0
- package/src/ui/server/routes/workbench/instructionStore.js +175 -0
- package/src/ui/server/routes/workbench/jobStore.js +225 -0
- package/src/ui/server/routes/workbench/jsonParse.js +219 -0
- package/src/ui/server/routes/workbench/llmClient.js +214 -0
- package/src/ui/server/routes/workbench/projectScan.js +191 -0
- package/src/ui/server/routes/workbench/sessionStore.js +126 -0
- package/src/ui/server/routes/workbench/shared.js +115 -0
- package/src/ui/server/routes/workbench/taskRunner.js +493 -0
- package/src/ui/server/routes/workbench.js +16 -3523
- package/src/ui/public/assets/WorkbenchView-OmJ1eM5g.css +0 -1
- package/src/ui/public/assets/index-CPqKvZBK.js +0 -23
package/src/ui/server/index.js
CHANGED
|
@@ -1,625 +1,632 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
import express from 'express';
|
|
16
|
-
import logger from './utils/logger.js'
|
|
17
|
-
import { createServer } from 'http';
|
|
18
|
-
import { fileURLToPath } from 'url';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { execGitCommand, getCommandHistory, addCommandToHistory, clearCommandHistory, registerSocketIO, execGitAddWithLockFilter, checkAndClearGitLock } from '../../utils/index.js';
|
|
21
|
-
import open from 'open';
|
|
22
|
-
import config from '../../config.js';
|
|
23
|
-
import chalk from 'chalk';
|
|
24
|
-
import fs from 'fs/promises';
|
|
25
|
-
import fsSync from 'fs';
|
|
26
|
-
import os from 'os';
|
|
27
|
-
import { Server } from 'socket.io';
|
|
28
|
-
import { spawn, exec } from 'child_process';
|
|
29
|
-
import iconv from 'iconv-lite';
|
|
30
|
-
import { createRequestLogger } from './middleware/requestLogger.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
* @param {
|
|
70
|
-
* @
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
done
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* @param {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
_fatalExitInProgress
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
fatalExit
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(chalk.blue(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
//
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import logger from './utils/logger.js'
|
|
17
|
+
import { createServer } from 'http';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { execGitCommand, getCommandHistory, addCommandToHistory, clearCommandHistory, registerSocketIO, execGitAddWithLockFilter, checkAndClearGitLock } from '../../utils/index.js';
|
|
21
|
+
import open from 'open';
|
|
22
|
+
import config from '../../config.js';
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
import fs from 'fs/promises';
|
|
25
|
+
import fsSync from 'fs';
|
|
26
|
+
import os from 'os';
|
|
27
|
+
import { Server } from 'socket.io';
|
|
28
|
+
import { spawn, exec } from 'child_process';
|
|
29
|
+
import iconv from 'iconv-lite';
|
|
30
|
+
import { createRequestLogger } from './middleware/requestLogger.js';
|
|
31
|
+
import { createErrorHandler } from './middleware/errorHandler.js';
|
|
32
|
+
import { registerUiSocketHandlers } from './socket/registerUiSocketHandlers.js';
|
|
33
|
+
import { registerExecRoutes } from './routes/exec.js';
|
|
34
|
+
import { registerTerminalRoutes } from './routes/terminal.js';
|
|
35
|
+
import { registerProcessRoutes } from './routes/process.js';
|
|
36
|
+
import { registerStatusRoutes } from './routes/status.js';
|
|
37
|
+
import { registerBranchStatusRoutes } from './routes/branchStatus.js';
|
|
38
|
+
import { registerConfigRoutes } from './routes/config.js';
|
|
39
|
+
import { registerGitRoutes } from './routes/git.js';
|
|
40
|
+
import { registerFsRoutes } from './routes/fs.js';
|
|
41
|
+
import { registerNpmRoutes } from './routes/npm.js';
|
|
42
|
+
import { registerFileOpenRoutes } from './routes/fileOpen.js';
|
|
43
|
+
import { registerWorkbenchRoutes } from './routes/workbench.js';
|
|
44
|
+
import { registerGitOpsRoutes } from './routes/gitOps.js';
|
|
45
|
+
import { registerCodeRoutes } from './routes/code.js';
|
|
46
|
+
import { registerCodeAnalysisRoutes } from './routes/codeAnalysis.js';
|
|
47
|
+
import { registerInstancesRoutes } from './routes/instances.js';
|
|
48
|
+
import { registerMonitorRoutes } from './routes/monitor.js';
|
|
49
|
+
import { registerMindmapRoutes } from './routes/mindmap.js';
|
|
50
|
+
import { createInstanceRegistry } from './utils/instanceRegistry.js';
|
|
51
|
+
import { createSavePortToFile } from './utils/createSavePortToFile.js';
|
|
52
|
+
import { startServerOnAvailablePort } from './utils/startServerOnAvailablePort.js';
|
|
53
|
+
import { resolveStartPort } from './utils/randomStartPort.js';
|
|
54
|
+
import { createFilePickerMiddleware } from 'local-file-picker';
|
|
55
|
+
import { createAiModelMiddleware } from 'ai-model-form';
|
|
56
|
+
|
|
57
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
58
|
+
const __dirname = path.dirname(__filename);
|
|
59
|
+
const configManager = config; // 确保 configManager 可用
|
|
60
|
+
// 存储正在运行的进程(用于停止功能)
|
|
61
|
+
const runningProcesses = new Map(); // key: processId, value: { childProcess, command, startTime }
|
|
62
|
+
let processIdCounter = 0;
|
|
63
|
+
|
|
64
|
+
const terminalSessions = new Map(); // key: terminalSessionId, value: { id, command, workingDirectory, pid, createdAt, lastStartedAt }
|
|
65
|
+
let terminalSessionIdCounter = 0;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 终止并等待单个子进程退出
|
|
69
|
+
* @param {import('child_process').ChildProcess} child
|
|
70
|
+
* @param {number} timeoutMs 等待 SIGTERM 生效的最大时间,超时后发 SIGKILL
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*/
|
|
73
|
+
function killChildProcess(child, timeoutMs = 3000) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
if (!child || (child.exitCode !== null && child.exitCode !== undefined)) {
|
|
76
|
+
return resolve();
|
|
77
|
+
}
|
|
78
|
+
let done = false;
|
|
79
|
+
const finish = () => {
|
|
80
|
+
if (done) return;
|
|
81
|
+
done = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
child.removeListener('exit', finish);
|
|
84
|
+
resolve();
|
|
85
|
+
};
|
|
86
|
+
child.once('exit', finish);
|
|
87
|
+
try {
|
|
88
|
+
// SIGTERM 优雅退出,被 Node 在 Windows 上转 TerminateProcess
|
|
89
|
+
child.kill('SIGTERM');
|
|
90
|
+
} catch (_) {
|
|
91
|
+
finish();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// 超时未退就 SIGKILL 强杀
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
97
|
+
// 给 SIGKILL 200ms 兜底,再不退也不等了
|
|
98
|
+
setTimeout(finish, 200);
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 并行终止 runningProcesses 内所有子进程,总等待时间不超过 timeoutMs。
|
|
105
|
+
* 防止 SIGINT/SIGTERM 关闭 server 时,正在跑的 claude/npm/git 子进程变孤儿。
|
|
106
|
+
*
|
|
107
|
+
* 注意:返回值是"尝试终止的进程数",不等于"已实际退出数"。内部用
|
|
108
|
+
* Promise.race 卡总时长,timeoutMs 到了就返回,此时可能仍有子进程
|
|
109
|
+
* 处于 SIGTERM → exit 的过渡中(尤其 Windows 上 TerminateProcess 是
|
|
110
|
+
* 异步的)。调用方日志应使用"已尝试终止"措辞,避免排查孤儿进程时被误导。
|
|
111
|
+
*
|
|
112
|
+
* @param {number} timeoutMs
|
|
113
|
+
* @returns {Promise<number>} 被尝试终止的进程数(entries.length)
|
|
114
|
+
*/
|
|
115
|
+
async function drainRunningProcesses(timeoutMs = 5000) {
|
|
116
|
+
const entries = Array.from(runningProcesses.entries());
|
|
117
|
+
if (entries.length === 0) return 0;
|
|
118
|
+
// 先并行发 SIGTERM,再并行等;整批用 Promise.race 卡最大时长
|
|
119
|
+
const killPromises = entries.map(([, info]) => killChildProcess(info.childProcess, timeoutMs).catch(() => {}));
|
|
120
|
+
await Promise.race([
|
|
121
|
+
Promise.all(killPromises),
|
|
122
|
+
new Promise((r) => setTimeout(r, timeoutMs))
|
|
123
|
+
]);
|
|
124
|
+
return entries.length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 全局错误处理 — 幂等,只允许进入 fatalExit 一次
|
|
128
|
+
let _fatalExitInProgress = false;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 注册 process.on('uncaughtException') / unhandledRejection 处理器
|
|
132
|
+
*
|
|
133
|
+
* 触发时的清理链路(防止留下孤儿子进程 / 假死 registry / 端口文件残留):
|
|
134
|
+
* 1) drainRunningProcesses(3s) — 并行 SIGTERM 杀掉所有 claude/npm/git 子进程
|
|
135
|
+
* 2) instanceRegistry.unregister — 把自己从实例表里摘掉
|
|
136
|
+
* 3) setTimeout 250ms 退 — 给 logger/console 一点时间把日志刷盘
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} opts
|
|
139
|
+
* @param {ReturnType<typeof createInstanceRegistry>} opts.instanceRegistry
|
|
140
|
+
*/
|
|
141
|
+
function setupGlobalErrorHandlers({ instanceRegistry }) {
|
|
142
|
+
// 幂等:多个错误同时触发时只走一次清理,避免清理动作自身再次触发 fatalExit
|
|
143
|
+
const fatalExit = async (reason, err) => {
|
|
144
|
+
if (_fatalExitInProgress) return;
|
|
145
|
+
_fatalExitInProgress = true;
|
|
146
|
+
try {
|
|
147
|
+
logger.error(`[FATAL] ${reason}: ${err?.message || err}`);
|
|
148
|
+
if (err?.stack) logger.error(err.stack);
|
|
149
|
+
} catch (_) { /* logger 自身炸了,放弃 */ }
|
|
150
|
+
// 1) 杀光所有子进程
|
|
151
|
+
try {
|
|
152
|
+
const killed = await drainRunningProcesses(3000);
|
|
153
|
+
if (killed > 0) logger.warn(`[FATAL] 已尝试终止 ${killed} 个运行中的子进程`);
|
|
154
|
+
} catch (_) {}
|
|
155
|
+
// 2) 反注册实例
|
|
156
|
+
if (instanceRegistry) {
|
|
157
|
+
try { await instanceRegistry.unregister(process.pid); } catch (_) {}
|
|
158
|
+
}
|
|
159
|
+
// 3) 给 OS 250ms 刷盘日志,然后非 0 退出
|
|
160
|
+
setTimeout(() => process.exit(1), 250);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.on('uncaughtException', (err, origin) => {
|
|
164
|
+
// 同步处理路径:直接调 fatalExit,内部 await 会进入 microtask
|
|
165
|
+
fatalExit(`uncaughtException (${origin || 'uncaught'})`, err);
|
|
166
|
+
});
|
|
167
|
+
process.on('unhandledRejection', (reason) => {
|
|
168
|
+
fatalExit('unhandledRejection', reason instanceof Error ? reason : new Error(String(reason)));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 分支状态缓存
|
|
173
|
+
let branchStatusCache = {
|
|
174
|
+
currentBranch: null,
|
|
175
|
+
upstreamBranch: null,
|
|
176
|
+
lastUpdate: 0,
|
|
177
|
+
cacheTimeout: 5000 // 5秒缓存
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 推送状态标记 - 用于优化推送后的分支状态查询
|
|
181
|
+
let recentPushStatus = {
|
|
182
|
+
justPushed: false,
|
|
183
|
+
pushTime: 0,
|
|
184
|
+
validDuration: 10000 // 推送后10秒内认为分支状态是同步的
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const showConsole = true;
|
|
188
|
+
async function startUIServer(noOpen = false, savePort = false) {
|
|
189
|
+
const app = express();
|
|
190
|
+
const httpServer = createServer(app);
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
// Socket.IO CORS / origin 收紧
|
|
194
|
+
//
|
|
195
|
+
// 默认 Socket.IO 接受任意 origin,跨域页面也能连上 → CSRF / 跨站攻击面
|
|
196
|
+
// 暴露给恶意页面。这里只允许同源 + 127.0.0.1 / localhost。
|
|
197
|
+
// 用环境变量 ZEN_ALLOWED_ORIGINS 可以追加自定义来源(逗号分隔),
|
|
198
|
+
// CI / 远程调试场景用。
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
const DEFAULT_ALLOWED_ORIGINS = [
|
|
201
|
+
'http://localhost',
|
|
202
|
+
'https://localhost',
|
|
203
|
+
'http://127.0.0.1',
|
|
204
|
+
'https://127.0.0.1',
|
|
205
|
+
'http://[::1]',
|
|
206
|
+
'null', // 同源 file:// 等场景
|
|
207
|
+
];
|
|
208
|
+
const extraOrigins = String(process.env.ZEN_ALLOWED_ORIGINS || '')
|
|
209
|
+
.split(',').map(s => s.trim()).filter(Boolean);
|
|
210
|
+
const allowedOrigins = new Set([...DEFAULT_ALLOWED_ORIGINS, ...extraOrigins]);
|
|
211
|
+
|
|
212
|
+
function isOriginAllowed(origin) {
|
|
213
|
+
if (!origin) return true; // 同源 / 非浏览器客户端不传 Origin
|
|
214
|
+
if (allowedOrigins.has(origin)) return true;
|
|
215
|
+
// 允许 localhost/127.0.0.1/[::1] 上的任意端口(开发常换端口)
|
|
216
|
+
try {
|
|
217
|
+
const u = new URL(origin);
|
|
218
|
+
if (['localhost', '127.0.0.1', '[::1]'].includes(u.hostname)) return true;
|
|
219
|
+
} catch { /* ignore */ }
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const io = new Server(httpServer, {
|
|
224
|
+
cors: {
|
|
225
|
+
origin: (origin, cb) => {
|
|
226
|
+
if (isOriginAllowed(origin)) return cb(null, true);
|
|
227
|
+
logger.warn(`[Socket.IO] 拒绝跨域 origin: ${origin}`);
|
|
228
|
+
return cb(new Error('Origin not allowed'), false);
|
|
229
|
+
},
|
|
230
|
+
methods: ['GET', 'POST'],
|
|
231
|
+
credentials: false,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
if (showConsole) logger.info(`创建服务成功`)
|
|
235
|
+
|
|
236
|
+
// 获取当前项目的唯一标识(使用工作目录路径)
|
|
237
|
+
// 需要在切换目录时更新,故使用 let
|
|
238
|
+
let currentProjectPath = process.cwd();
|
|
239
|
+
let projectRoomId = `project:${currentProjectPath.replace(/[\\/:\s]/g, '_')}`;
|
|
240
|
+
|
|
241
|
+
console.log(chalk.blue(`项目房间ID: ${projectRoomId}`));
|
|
242
|
+
console.log(chalk.blue(`项目路径: ${currentProjectPath}`));
|
|
243
|
+
|
|
244
|
+
// 注册Socket.io实例,用于命令历史通知
|
|
245
|
+
registerSocketIO(io);
|
|
246
|
+
|
|
247
|
+
// 构建实例注册表(用于跨进程共享"当前运行中的 GUI"信息)
|
|
248
|
+
// 注册表文件位于用户主目录,所有 g ui 进程共享写入
|
|
249
|
+
const instanceRegistry = createInstanceRegistry({
|
|
250
|
+
fs,
|
|
251
|
+
path,
|
|
252
|
+
os,
|
|
253
|
+
registryPath: path.join(os.homedir(), '.zen-gitsync-instances.json')
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 注册全局错误处理器:任意未捕获异常/拒绝都会触发 fatalExit,
|
|
257
|
+
// 走 drainRunningProcesses + unregister + 延迟 250ms 退出的链路。
|
|
258
|
+
// 在 instanceRegistry 创建之后立即注册,这样后续 route 注册阶段抛出
|
|
259
|
+
// 也能被同一个处理器捕获(否则会无声崩,留下子进程孤儿与 .port 残留)。
|
|
260
|
+
setupGlobalErrorHandlers({ instanceRegistry });
|
|
261
|
+
|
|
262
|
+
// 添加全局中间件来解析JSON请求体
|
|
263
|
+
app.use(express.json());
|
|
264
|
+
|
|
265
|
+
// 记录最近打开的目录(优先 Git 根目录,其次当前工作目录)
|
|
266
|
+
try {
|
|
267
|
+
let dirPath = process.cwd();
|
|
268
|
+
try {
|
|
269
|
+
if(showConsole) logger.info(`记录最近打开目录`)
|
|
270
|
+
const { stdout } = await execGitCommand(['rev-parse', '--show-toplevel']);
|
|
271
|
+
const root = stdout?.trim();
|
|
272
|
+
if (root) dirPath = root;
|
|
273
|
+
} catch (_) {
|
|
274
|
+
// 非Git仓库或命令失败,使用 CWD 即可
|
|
275
|
+
}
|
|
276
|
+
if (showConsole) logger.info(`记录最近打开目录: ${dirPath}`)
|
|
277
|
+
await configManager.saveRecentDirectory(dirPath);
|
|
278
|
+
if (showConsole) console.log(chalk.gray(`已记录最近打开目录: ${dirPath}`));
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.warn(chalk.yellow(`记录最近目录失败: ${e?.message || e}`));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 添加请求日志中间件
|
|
284
|
+
app.use(createRequestLogger({ chalk }));
|
|
285
|
+
|
|
286
|
+
// 静态文件服务
|
|
287
|
+
app.use(express.static(path.join(__dirname, '../public')));
|
|
288
|
+
|
|
289
|
+
registerExecRoutes({
|
|
290
|
+
app,
|
|
291
|
+
execGitCommand,
|
|
292
|
+
addCommandToHistory,
|
|
293
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
294
|
+
// 用 pre-increment 从 1 起,避免首进程拿到 id=0(0 在 `if (processId)` 这类真值检查里会被吞)
|
|
295
|
+
nextProcessId: () => ++processIdCounter,
|
|
296
|
+
runningProcesses
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
registerCodeRoutes({
|
|
300
|
+
app
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
registerTerminalRoutes({
|
|
304
|
+
app,
|
|
305
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
306
|
+
nextTerminalSessionId: () => terminalSessionIdCounter++,
|
|
307
|
+
terminalSessions
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
registerProcessRoutes({
|
|
311
|
+
app,
|
|
312
|
+
runningProcesses,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// API路由
|
|
316
|
+
// 移除了 /api/status 端点,因为前端只使用 porcelain 格式
|
|
317
|
+
registerStatusRoutes({
|
|
318
|
+
app,
|
|
319
|
+
getCommandHistory,
|
|
320
|
+
execGitCommand
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
registerBranchStatusRoutes({
|
|
324
|
+
app,
|
|
325
|
+
execGitCommand,
|
|
326
|
+
getIsGitRepo: () => isGitRepo,
|
|
327
|
+
getBranchStatusCache: () => branchStatusCache,
|
|
328
|
+
setBranchStatusCache: (v) => { branchStatusCache = v; },
|
|
329
|
+
getRecentPushStatus: () => recentPushStatus,
|
|
330
|
+
setRecentPushStatus: (v) => { recentPushStatus = v; }
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// 清除分支缓存的函数(在分支切换时调用)
|
|
334
|
+
function clearBranchCache() {
|
|
335
|
+
logger.info('清除分支缓存');
|
|
336
|
+
// 清除5秒分支状态缓存
|
|
337
|
+
branchStatusCache = {
|
|
338
|
+
currentBranch: null,
|
|
339
|
+
upstreamBranch: null,
|
|
340
|
+
lastUpdate: 0,
|
|
341
|
+
cacheTimeout: 5000
|
|
342
|
+
};
|
|
343
|
+
// 清除推送状态标记
|
|
344
|
+
recentPushStatus = {
|
|
345
|
+
justPushed: false,
|
|
346
|
+
pushTime: 0,
|
|
347
|
+
validDuration: 10000
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
registerGitRoutes({
|
|
352
|
+
app,
|
|
353
|
+
execGitCommand,
|
|
354
|
+
clearBranchCache
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
registerFsRoutes({
|
|
358
|
+
app,
|
|
359
|
+
execGitCommand,
|
|
360
|
+
configManager,
|
|
361
|
+
io,
|
|
362
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
363
|
+
setCurrentProjectPath: (v) => {
|
|
364
|
+
currentProjectPath = v;
|
|
365
|
+
// project 切换后清掉 _lastBroadcastedPorcelain 缓存,
|
|
366
|
+
// 否则两个 project 都是 clean 时(porcelain 都是 '')会误判
|
|
367
|
+
// 内容未变 → 漏推新 projectPath
|
|
368
|
+
_lastBroadcastedPorcelain = null;
|
|
369
|
+
},
|
|
370
|
+
getProjectRoomId: () => projectRoomId,
|
|
371
|
+
setProjectRoomId: (v) => { projectRoomId = v; },
|
|
372
|
+
setIsGitRepo: (v) => { isGitRepo = v; }
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
registerConfigRoutes({
|
|
376
|
+
app,
|
|
377
|
+
configManager,
|
|
378
|
+
execGitCommand,
|
|
379
|
+
getCurrentProjectPath: () => currentProjectPath
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
registerNpmRoutes({
|
|
383
|
+
app,
|
|
384
|
+
getCurrentProjectPath: () => currentProjectPath
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
registerFileOpenRoutes({
|
|
388
|
+
app
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
registerWorkbenchRoutes({
|
|
392
|
+
app,
|
|
393
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
394
|
+
getProjectRoomId: () => projectRoomId,
|
|
395
|
+
io,
|
|
396
|
+
configManager
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// local-file-picker 中间件,提供 /api/fs/* 文件浏览路由
|
|
400
|
+
app.use('/api', createFilePickerMiddleware());
|
|
401
|
+
|
|
402
|
+
// ai-model-form 中间件:提供 /api/ai-model/* 路由(模型列表/测试/暂存保存)
|
|
403
|
+
app.use('/api', createAiModelMiddleware());
|
|
404
|
+
|
|
405
|
+
registerCodeAnalysisRoutes({ app, configManager, getCurrentProjectPath: () => currentProjectPath });
|
|
406
|
+
|
|
407
|
+
// 实例注册表 API:列出当前所有运行中的 GUI
|
|
408
|
+
registerInstancesRoutes({
|
|
409
|
+
app,
|
|
410
|
+
registry: instanceRegistry,
|
|
411
|
+
getCurrentInstanceId: () => process.pid
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
registerGitOpsRoutes({
|
|
415
|
+
app,
|
|
416
|
+
execGitCommand,
|
|
417
|
+
configManager,
|
|
418
|
+
execGitAddWithLockFilter,
|
|
419
|
+
addCommandToHistory,
|
|
420
|
+
clearCommandHistory,
|
|
421
|
+
checkAndClearGitLock,
|
|
422
|
+
getIsGitRepo: () => isGitRepo,
|
|
423
|
+
setRecentPushStatus: (v) => { recentPushStatus = v; }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// 系统监控:CPU / 内存 / 端口占用 + kill 进程
|
|
427
|
+
registerMonitorRoutes({ app });
|
|
428
|
+
|
|
429
|
+
// 思维导图:.mindmap.json 文件的列/读/写/建/删/重命名
|
|
430
|
+
registerMindmapRoutes({ app });
|
|
431
|
+
|
|
432
|
+
// 全局错误处理中间件:必须放在所有 register*Routes 之后注册,
|
|
433
|
+
// 作为最后一道兜底捕获 asyncRoute 之外抛出的异常(headersSent 走 express 默认关闭)。
|
|
434
|
+
// 与 asyncRoute 共享同一套错误响应格式 {success:false, error}。
|
|
435
|
+
app.use(createErrorHandler());
|
|
436
|
+
|
|
437
|
+
registerUiSocketHandlers({
|
|
438
|
+
io,
|
|
439
|
+
getProjectRoomId: () => projectRoomId,
|
|
440
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
441
|
+
getAndBroadcastStatus,
|
|
442
|
+
getCommandHistory,
|
|
443
|
+
clearCommandHistory,
|
|
444
|
+
addCommandToHistory,
|
|
445
|
+
runningProcesses,
|
|
446
|
+
nextProcessId: () => ++processIdCounter,
|
|
447
|
+
spawn,
|
|
448
|
+
exec,
|
|
449
|
+
path,
|
|
450
|
+
iconv
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// 获取并广播Git状态 (优化版本 - 只获取porcelain格式)
|
|
454
|
+
// 去重:50ms 内的多次调用合并为一次 git status + 一次广播。
|
|
455
|
+
// 触发场景:页面刷新风暴(打开 GUI 后浏览器自动重连 3-5 次),
|
|
456
|
+
// 或前端多 tab 同时连接。N 个客户端连进来 = N 次 git status 子进程,
|
|
457
|
+
// 每次 ~30-80ms,合并后只跑 1 次。
|
|
458
|
+
let _statusBroadcastPending = false;
|
|
459
|
+
let _lastBroadcastedPorcelain = null;
|
|
460
|
+
async function getAndBroadcastStatus() {
|
|
461
|
+
if (_statusBroadcastPending) return;
|
|
462
|
+
_statusBroadcastPending = true;
|
|
463
|
+
// 用 setImmediate 让同一 tick 内的多次调用合并(更短的等待)
|
|
464
|
+
setImmediate(async () => {
|
|
465
|
+
_statusBroadcastPending = false;
|
|
466
|
+
try {
|
|
467
|
+
// 如果不是Git仓库,发送特殊状态
|
|
468
|
+
if (!isGitRepo) {
|
|
469
|
+
io.to(projectRoomId).emit('git_status_update', {
|
|
470
|
+
isGitRepo: false,
|
|
471
|
+
porcelain: '',
|
|
472
|
+
timestamp: new Date().toISOString(),
|
|
473
|
+
projectPath: currentProjectPath
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 只获取porcelain格式状态,不再获取完整的git status
|
|
479
|
+
const { stdout: porcelainOutput } = await execGitCommand(['status', '--porcelain', '--untracked-files=all']);
|
|
480
|
+
|
|
481
|
+
// 内容未变就不广播 — 防止每次前端 reconnect 都推一份完全一样的字符串
|
|
482
|
+
// 跨项目切换时 isGitRepo/projectPath 会变,这里只按内容去重
|
|
483
|
+
if (porcelainOutput === _lastBroadcastedPorcelain) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
_lastBroadcastedPorcelain = porcelainOutput;
|
|
487
|
+
|
|
488
|
+
// 广播到当前项目房间的所有客户端
|
|
489
|
+
io.to(projectRoomId).emit('git_status_update', {
|
|
490
|
+
isGitRepo: true,
|
|
491
|
+
porcelain: porcelainOutput,
|
|
492
|
+
timestamp: new Date().toISOString(),
|
|
493
|
+
projectPath: currentProjectPath
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
logger.info(`已广播Git状态更新到房间: ${projectRoomId}`);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
logger.error('获取或广播Git状态失败:', error);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 检查当前目录是否是Git仓库
|
|
504
|
+
let isGitRepo = false;
|
|
505
|
+
try {
|
|
506
|
+
const { stdout } = await execGitCommand(['rev-parse', '--is-inside-work-tree'], { log: false });
|
|
507
|
+
isGitRepo = stdout.trim() === 'true';
|
|
508
|
+
} catch (error) {
|
|
509
|
+
isGitRepo = false;
|
|
510
|
+
console.log(chalk.yellow('======================================'));
|
|
511
|
+
console.log(chalk.yellow(` 提示: 当前目录不是Git仓库`));
|
|
512
|
+
console.log(chalk.yellow(` 目录: ${process.cwd()}`));
|
|
513
|
+
console.log(chalk.yellow('======================================'));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 启动服务器
|
|
517
|
+
// 端口策略:默认从 [4000, 6000) 随机挑起点,再顺序扫描 EADDRINUSE;
|
|
518
|
+
// 可通过 PORT 环境变量强制使用固定端口(向后兼容 + 便于书签/调试)
|
|
519
|
+
const portStrategy = resolveStartPort();
|
|
520
|
+
if (portStrategy.source === 'env') {
|
|
521
|
+
console.log(chalk.cyan(`[端口] 使用环境变量 PORT=${portStrategy.startPort}`));
|
|
522
|
+
} else {
|
|
523
|
+
console.log(chalk.cyan(`[端口] 随机起点 ${portStrategy.startPort}(范围 ${portStrategy.min}-${portStrategy.max},遇到占用会顺延)`));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 创建一个函数来保存端口号到文件和环境变量
|
|
527
|
+
// 使用闭包保存端口状态,防止多次写入相同端口
|
|
528
|
+
const savePortToFile = createSavePortToFile({ savePort, fs, path });
|
|
529
|
+
// 使用变量标记回调是否已执行,防止多次触发
|
|
530
|
+
const callbackExecutedRef = { value: false };
|
|
531
|
+
|
|
532
|
+
// 用 'listening' 事件做注册触发:startServerOnAvailablePort 的 await
|
|
533
|
+
// 在端口重试场景下不一定按时返回,但 'listening' 事件只在服务器真正绑定端口时触发
|
|
534
|
+
let registerDone = false;
|
|
535
|
+
httpServer.once('listening', () => {
|
|
536
|
+
if (registerDone) return;
|
|
537
|
+
registerDone = true;
|
|
538
|
+
registerCurrentInstance().catch((e) => {
|
|
539
|
+
console.warn(chalk.yellow(`[instanceRegistry] 启动注册流程失败: ${e?.message || e}`));
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// 尝试在可用端口上启动服务器(不等待;listen 事件会驱动后续逻辑)
|
|
544
|
+
startServerOnAvailablePort({
|
|
545
|
+
httpServer,
|
|
546
|
+
startPort: portStrategy.startPort,
|
|
547
|
+
chalk,
|
|
548
|
+
open,
|
|
549
|
+
noOpen,
|
|
550
|
+
isGitRepo,
|
|
551
|
+
savePortToFile,
|
|
552
|
+
fsSync,
|
|
553
|
+
maxTries: 100,
|
|
554
|
+
callbackExecutedRef
|
|
555
|
+
}).catch((e) => {
|
|
556
|
+
logger.error('启动服务器失败:', e);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// 把所有注册/心跳/watch 逻辑封装到独立函数,由 listening 事件触发
|
|
560
|
+
async function registerCurrentInstance() {
|
|
561
|
+
const addr = httpServer.address();
|
|
562
|
+
const currentPort = addr && addr.port;
|
|
563
|
+
if (!currentPort) {
|
|
564
|
+
console.warn(chalk.yellow('[instanceRegistry] 无法获取当前端口,跳过注册'));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
const projectName = await instanceRegistry._resolveProjectName(currentProjectPath);
|
|
570
|
+
await instanceRegistry.register({
|
|
571
|
+
pid: process.pid,
|
|
572
|
+
port: currentPort,
|
|
573
|
+
projectPath: currentProjectPath,
|
|
574
|
+
projectName,
|
|
575
|
+
hostname: os.hostname()
|
|
576
|
+
});
|
|
577
|
+
console.log(chalk.green(`[instanceRegistry] 已注册 pid=${process.pid} port=${currentPort} name=${projectName}`));
|
|
578
|
+
} catch (e) {
|
|
579
|
+
console.warn(chalk.yellow(`[instanceRegistry] 注册失败: ${e?.message || e}`));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 5 秒心跳
|
|
584
|
+
const heartbeatTimer = setInterval(() => {
|
|
585
|
+
instanceRegistry.heartbeat(process.pid, { projectPath: currentProjectPath })
|
|
586
|
+
.catch((e) => console.warn(chalk.yellow(`[instanceRegistry] 心跳失败: ${e?.message || e}`)));
|
|
587
|
+
}, 5000);
|
|
588
|
+
|
|
589
|
+
// 监听注册表文件变化:任何进程写入都会触发,向本进程的所有客户端广播
|
|
590
|
+
// fs.watch 在 Windows 上偶有不可靠,Socket.IO 推送 + 前端轮询(15s)兜底
|
|
591
|
+
const stopWatcher = instanceRegistry.watch(async (fresh) => {
|
|
592
|
+
try {
|
|
593
|
+
io.emit('instances_changed', { instances: fresh });
|
|
594
|
+
} catch (e) {
|
|
595
|
+
console.warn(chalk.yellow(`[instanceRegistry] 广播失败: ${e?.message || e}`));
|
|
596
|
+
}
|
|
597
|
+
}, fsSync.watch);
|
|
598
|
+
|
|
599
|
+
// 优雅退出:SIGINT/SIGTERM 触发 drain 子进程 + unregister + 清心跳
|
|
600
|
+
// 幂等保护:连按 Ctrl+C 或 SIGINT+SIGTERM 同来时只走一次清理,
|
|
601
|
+
// 避免重复 drain(3s)、重复 unregister、排多个 setTimeout(exit)。
|
|
602
|
+
// 与上方 _fatalExitInProgress 同思路,但作用域在本次 startUIServer 调用内。
|
|
603
|
+
let _shuttingDown = false;
|
|
604
|
+
const shutdown = async (signal) => {
|
|
605
|
+
if (_shuttingDown) return;
|
|
606
|
+
_shuttingDown = true;
|
|
607
|
+
console.log(chalk.gray(`[shutdown] 收到 ${signal},开始清理…`));
|
|
608
|
+
// 1) 先并行 SIGTERM 所有正在跑的子进程(限时 3s),防止 claude/npm/git 变孤儿。
|
|
609
|
+
// 注意:返回值是"尝试终止数"而非"实际终止数" — drain 内部用
|
|
610
|
+
// Promise.race 卡总时长,3s 到了就返回,可能仍有进程在 SIGTERM 退出中。
|
|
611
|
+
try {
|
|
612
|
+
const killed = await drainRunningProcesses(3000);
|
|
613
|
+
if (killed > 0) console.log(chalk.gray(`[shutdown] 已尝试终止 ${killed} 个子进程(实际终止情况以 exit 事件为准)`));
|
|
614
|
+
} catch (_) {}
|
|
615
|
+
// 2) 清心跳 + 反注册实例
|
|
616
|
+
try { clearInterval(heartbeatTimer); } catch (_) {}
|
|
617
|
+
try { await instanceRegistry.unregister(process.pid); } catch (_) {}
|
|
618
|
+
console.log(chalk.gray(`[shutdown] 收到 ${signal},已清理本实例`));
|
|
619
|
+
// 3) 给 OS 100ms 刷盘日志
|
|
620
|
+
setTimeout(() => process.exit(0), 100);
|
|
621
|
+
};
|
|
622
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
623
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
624
|
+
// 'exit' 是同步钩子,无法 await:仅关闭 watcher;kill -9 走心跳超时
|
|
625
|
+
process.on('exit', () => {
|
|
626
|
+
try { stopWatcher && stopWatcher(); } catch (_) {}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export default startUIServer;
|