cfix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.env.example +69 -0
  2. package/README.md +1590 -0
  3. package/bin/cfix +14 -0
  4. package/bin/cfix.cmd +6 -0
  5. package/cli/commands/config.js +58 -0
  6. package/cli/commands/doctor.js +240 -0
  7. package/cli/commands/fix.js +211 -0
  8. package/cli/commands/help.js +62 -0
  9. package/cli/commands/init.js +226 -0
  10. package/cli/commands/logs.js +161 -0
  11. package/cli/commands/monitor.js +151 -0
  12. package/cli/commands/project.js +331 -0
  13. package/cli/commands/service.js +133 -0
  14. package/cli/commands/status.js +115 -0
  15. package/cli/commands/task.js +412 -0
  16. package/cli/commands/version.js +19 -0
  17. package/cli/index.js +269 -0
  18. package/cli/lib/config-manager.js +612 -0
  19. package/cli/lib/formatter.js +224 -0
  20. package/cli/lib/process-manager.js +233 -0
  21. package/cli/lib/service-client.js +271 -0
  22. package/cli/scripts/install-completion.js +133 -0
  23. package/package.json +85 -0
  24. package/public/monitor.html +1096 -0
  25. package/scripts/completion.bash +87 -0
  26. package/scripts/completion.zsh +102 -0
  27. package/src/assets/README.md +32 -0
  28. package/src/assets/error.png +0 -0
  29. package/src/assets/icon.png +0 -0
  30. package/src/assets/success.png +0 -0
  31. package/src/claude-cli-service.js +216 -0
  32. package/src/config/index.js +69 -0
  33. package/src/database/manager.js +391 -0
  34. package/src/database/migration.js +252 -0
  35. package/src/git-service.js +1278 -0
  36. package/src/index.js +1658 -0
  37. package/src/logger.js +139 -0
  38. package/src/metrics/collector.js +184 -0
  39. package/src/middleware/auth.js +86 -0
  40. package/src/middleware/rate-limit.js +85 -0
  41. package/src/queue/integration-example.js +283 -0
  42. package/src/queue/task-queue.js +333 -0
  43. package/src/services/notification-limiter.js +48 -0
  44. package/src/services/notification-service.js +115 -0
  45. package/src/services/system-notifier.js +130 -0
  46. package/src/task-manager.js +289 -0
  47. package/src/utils/exec.js +87 -0
  48. package/src/utils/project-lock.js +246 -0
  49. package/src/utils/retry.js +110 -0
  50. package/src/utils/sanitizer.js +174 -0
  51. package/src/websocket/notifier.js +363 -0
  52. package/src/wechat-notifier.js +97 -0
@@ -0,0 +1,1096 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CodeFix - 实时监控面板</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ color: #e4e4e7;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ margin-bottom: 30px;
32
+ padding: 20px;
33
+ background: rgba(255, 255, 255, 0.05);
34
+ border-radius: 12px;
35
+ backdrop-filter: blur(10px);
36
+ }
37
+
38
+ .header-left {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 15px;
42
+ }
43
+
44
+ .logo {
45
+ font-size: 2em;
46
+ }
47
+
48
+ h1 {
49
+ font-size: 1.8em;
50
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
51
+ -webkit-background-clip: text;
52
+ -webkit-text-fill-color: transparent;
53
+ background-clip: text;
54
+ }
55
+
56
+ .header-right {
57
+ display: flex;
58
+ gap: 10px;
59
+ align-items: center;
60
+ }
61
+
62
+ .status-badge {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 6px;
66
+ padding: 8px 16px;
67
+ border-radius: 20px;
68
+ font-size: 13px;
69
+ font-weight: 500;
70
+ }
71
+
72
+ .status-badge.online {
73
+ background: rgba(16, 185, 129, 0.2);
74
+ color: #10b981;
75
+ border: 1px solid rgba(16, 185, 129, 0.3);
76
+ }
77
+
78
+ .status-badge.offline {
79
+ background: rgba(239, 68, 68, 0.2);
80
+ color: #ef4444;
81
+ border: 1px solid rgba(239, 68, 68, 0.3);
82
+ }
83
+
84
+ .status-dot {
85
+ width: 8px;
86
+ height: 8px;
87
+ border-radius: 50%;
88
+ background: currentColor;
89
+ animation: pulse 2s infinite;
90
+ }
91
+
92
+ @keyframes pulse {
93
+ 0%, 100% { opacity: 1; }
94
+ 50% { opacity: 0.5; }
95
+ }
96
+
97
+ .grid {
98
+ display: grid;
99
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
100
+ gap: 20px;
101
+ margin-bottom: 20px;
102
+ }
103
+
104
+ .card {
105
+ background: rgba(255, 255, 255, 0.05);
106
+ border-radius: 12px;
107
+ padding: 20px;
108
+ border: 1px solid rgba(255, 255, 255, 0.1);
109
+ backdrop-filter: blur(10px);
110
+ }
111
+
112
+ .card-header {
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ margin-bottom: 15px;
117
+ }
118
+
119
+ .card-title {
120
+ font-size: 1.1em;
121
+ font-weight: 600;
122
+ color: #a1a1aa;
123
+ }
124
+
125
+ .refresh-btn {
126
+ background: rgba(102, 126, 234, 0.2);
127
+ color: #667eea;
128
+ border: 1px solid rgba(102, 126, 234, 0.3);
129
+ padding: 6px 12px;
130
+ border-radius: 6px;
131
+ cursor: pointer;
132
+ font-size: 12px;
133
+ transition: all 0.3s;
134
+ }
135
+
136
+ .refresh-btn:hover {
137
+ background: rgba(102, 126, 234, 0.3);
138
+ }
139
+
140
+ .stats-grid {
141
+ display: grid;
142
+ grid-template-columns: repeat(2, 1fr);
143
+ gap: 15px;
144
+ }
145
+
146
+ .stat-item {
147
+ background: rgba(255, 255, 255, 0.03);
148
+ border-radius: 8px;
149
+ padding: 15px;
150
+ text-align: center;
151
+ }
152
+
153
+ .stat-value {
154
+ font-size: 2em;
155
+ font-weight: 700;
156
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
157
+ -webkit-background-clip: text;
158
+ -webkit-text-fill-color: transparent;
159
+ background-clip: text;
160
+ }
161
+
162
+ .stat-label {
163
+ font-size: 0.85em;
164
+ color: #71717a;
165
+ margin-top: 5px;
166
+ }
167
+
168
+ .stat-change {
169
+ font-size: 0.75em;
170
+ color: #10b981;
171
+ margin-top: 5px;
172
+ }
173
+
174
+ .task-list {
175
+ max-height: 400px;
176
+ overflow-y: auto;
177
+ }
178
+
179
+ .task-item {
180
+ background: rgba(255, 255, 255, 0.03);
181
+ border: 1px solid rgba(255, 255, 255, 0.08);
182
+ border-radius: 8px;
183
+ padding: 15px;
184
+ margin-bottom: 10px;
185
+ transition: all 0.3s;
186
+ }
187
+
188
+ .task-item:hover {
189
+ background: rgba(255, 255, 255, 0.05);
190
+ border-color: rgba(102, 126, 234, 0.3);
191
+ }
192
+
193
+ .task-header {
194
+ display: flex;
195
+ justify-content: space-between;
196
+ align-items: center;
197
+ margin-bottom: 10px;
198
+ }
199
+
200
+ .task-id {
201
+ font-family: 'Courier New', monospace;
202
+ font-weight: 600;
203
+ color: #a1a1aa;
204
+ }
205
+
206
+ .task-status {
207
+ padding: 4px 12px;
208
+ border-radius: 12px;
209
+ font-size: 11px;
210
+ font-weight: 500;
211
+ text-transform: uppercase;
212
+ }
213
+
214
+ .task-status.pending {
215
+ background: rgba(251, 191, 36, 0.2);
216
+ color: #fbbf24;
217
+ }
218
+
219
+ .task-status.running {
220
+ background: rgba(59, 130, 246, 0.2);
221
+ color: #3b82f6;
222
+ }
223
+
224
+ .task-status.waiting_approval {
225
+ background: rgba(168, 85, 247, 0.2);
226
+ color: #a855f7;
227
+ }
228
+
229
+ .task-status.completed {
230
+ background: rgba(16, 185, 129, 0.2);
231
+ color: #10b981;
232
+ }
233
+
234
+ .task-status.failed {
235
+ background: rgba(239, 68, 68, 0.2);
236
+ color: #ef4444;
237
+ }
238
+
239
+ .task-info {
240
+ display: grid;
241
+ grid-template-columns: 1fr 1fr;
242
+ gap: 10px;
243
+ margin-bottom: 10px;
244
+ font-size: 0.85em;
245
+ }
246
+
247
+ .task-info div {
248
+ color: #71717a;
249
+ }
250
+
251
+ .task-info span {
252
+ color: #a1a1aa;
253
+ }
254
+
255
+ .progress-bar {
256
+ width: 100%;
257
+ height: 6px;
258
+ background: rgba(255, 255, 255, 0.1);
259
+ border-radius: 3px;
260
+ overflow: hidden;
261
+ margin-top: 10px;
262
+ }
263
+
264
+ .progress-fill {
265
+ height: 100%;
266
+ background: linear-gradient(90deg, #667eea, #764ba2);
267
+ transition: width 0.5s ease;
268
+ border-radius: 3px;
269
+ }
270
+
271
+ .task-actions {
272
+ display: flex;
273
+ gap: 8px;
274
+ margin-top: 12px;
275
+ }
276
+
277
+ .task-btn {
278
+ flex: 1;
279
+ padding: 8px;
280
+ border: none;
281
+ border-radius: 6px;
282
+ cursor: pointer;
283
+ font-size: 12px;
284
+ transition: all 0.3s;
285
+ }
286
+
287
+ .task-btn.approve {
288
+ background: rgba(16, 185, 129, 0.2);
289
+ color: #10b981;
290
+ border: 1px solid rgba(16, 185, 129, 0.3);
291
+ }
292
+
293
+ .task-btn.approve:hover {
294
+ background: rgba(16, 185, 129, 0.3);
295
+ }
296
+
297
+ .task-btn.reject {
298
+ background: rgba(239, 68, 68, 0.2);
299
+ color: #ef4444;
300
+ border: 1px solid rgba(239, 68, 68, 0.3);
301
+ }
302
+
303
+ .task-btn.reject:hover {
304
+ background: rgba(239, 68, 68, 0.3);
305
+ }
306
+
307
+ .task-btn.view {
308
+ background: rgba(102, 126, 234, 0.2);
309
+ color: #667eea;
310
+ border: 1px solid rgba(102, 126, 234, 0.3);
311
+ }
312
+
313
+ .task-btn.view:hover {
314
+ background: rgba(102, 126, 234, 0.3);
315
+ }
316
+
317
+ .log-container {
318
+ background: rgba(0, 0, 0, 0.3);
319
+ border-radius: 8px;
320
+ padding: 15px;
321
+ max-height: 300px;
322
+ overflow-y: auto;
323
+ font-family: 'Courier New', monospace;
324
+ font-size: 12px;
325
+ }
326
+
327
+ .log-entry {
328
+ padding: 8px;
329
+ margin-bottom: 5px;
330
+ border-left: 3px solid #71717a;
331
+ background: rgba(255, 255, 255, 0.02);
332
+ border-radius: 4px;
333
+ }
334
+
335
+ .log-entry.info {
336
+ border-left-color: #3b82f6;
337
+ }
338
+
339
+ .log-entry.success {
340
+ border-left-color: #10b981;
341
+ }
342
+
343
+ .log-entry.error {
344
+ border-left-color: #ef4444;
345
+ }
346
+
347
+ .log-entry.warning {
348
+ border-left-color: #fbbf24;
349
+ }
350
+
351
+ .log-time {
352
+ color: #71717a;
353
+ margin-right: 10px;
354
+ }
355
+
356
+ .control-panel {
357
+ display: flex;
358
+ gap: 10px;
359
+ margin-bottom: 15px;
360
+ }
361
+
362
+ input, button {
363
+ padding: 10px 16px;
364
+ border: 1px solid rgba(255, 255, 255, 0.1);
365
+ border-radius: 8px;
366
+ background: rgba(255, 255, 255, 0.05);
367
+ color: #e4e4e7;
368
+ font-size: 14px;
369
+ }
370
+
371
+ input {
372
+ flex: 1;
373
+ }
374
+
375
+ input:focus {
376
+ outline: none;
377
+ border-color: #667eea;
378
+ }
379
+
380
+ button {
381
+ background: rgba(102, 126, 234, 0.2);
382
+ color: #667eea;
383
+ border: 1px solid rgba(102, 126, 234, 0.3);
384
+ cursor: pointer;
385
+ transition: all 0.3s;
386
+ }
387
+
388
+ button:hover:not(:disabled) {
389
+ background: rgba(102, 126, 234, 0.3);
390
+ }
391
+
392
+ button:disabled {
393
+ opacity: 0.5;
394
+ cursor: not-allowed;
395
+ }
396
+
397
+ .empty-state {
398
+ text-align: center;
399
+ padding: 40px 20px;
400
+ color: #71717a;
401
+ }
402
+
403
+ .empty-state-icon {
404
+ font-size: 3em;
405
+ margin-bottom: 15px;
406
+ }
407
+
408
+ .filter-bar {
409
+ display: flex;
410
+ gap: 10px;
411
+ margin-bottom: 15px;
412
+ }
413
+
414
+ .filter-btn {
415
+ padding: 6px 12px;
416
+ border: 1px solid rgba(255, 255, 255, 0.1);
417
+ border-radius: 6px;
418
+ background: transparent;
419
+ color: #71717a;
420
+ cursor: pointer;
421
+ font-size: 12px;
422
+ transition: all 0.3s;
423
+ }
424
+
425
+ .filter-btn.active {
426
+ background: rgba(102, 126, 234, 0.2);
427
+ color: #667eea;
428
+ border-color: rgba(102, 126, 234, 0.3);
429
+ }
430
+
431
+ .filter-btn:hover:not(.active) {
432
+ background: rgba(255, 255, 255, 0.05);
433
+ }
434
+
435
+ ::-webkit-scrollbar {
436
+ width: 8px;
437
+ }
438
+
439
+ ::-webkit-scrollbar-track {
440
+ background: rgba(255, 255, 255, 0.05);
441
+ border-radius: 4px;
442
+ }
443
+
444
+ ::-webkit-scrollbar-thumb {
445
+ background: rgba(102, 126, 234, 0.3);
446
+ border-radius: 4px;
447
+ }
448
+
449
+ ::-webkit-scrollbar-thumb:hover {
450
+ background: rgba(102, 126, 234, 0.5);
451
+ }
452
+
453
+ /* 模态框样式 */
454
+ .modal-overlay {
455
+ position: fixed;
456
+ top: 0;
457
+ left: 0;
458
+ right: 0;
459
+ bottom: 0;
460
+ background: rgba(0, 0, 0, 0.7);
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: center;
464
+ z-index: 1000;
465
+ opacity: 0;
466
+ visibility: hidden;
467
+ transition: all 0.3s;
468
+ }
469
+
470
+ .modal-overlay.active {
471
+ opacity: 1;
472
+ visibility: visible;
473
+ }
474
+
475
+ .modal {
476
+ background: rgba(30, 30, 50, 0.95);
477
+ border-radius: 12px;
478
+ padding: 25px;
479
+ max-width: 500px;
480
+ width: 90%;
481
+ max-height: 80vh;
482
+ overflow-y: auto;
483
+ border: 1px solid rgba(255, 255, 255, 0.1);
484
+ backdrop-filter: blur(10px);
485
+ transform: scale(0.9);
486
+ transition: transform 0.3s;
487
+ }
488
+
489
+ .modal-overlay.active .modal {
490
+ transform: scale(1);
491
+ }
492
+
493
+ .modal-header {
494
+ display: flex;
495
+ justify-content: space-between;
496
+ align-items: center;
497
+ margin-bottom: 20px;
498
+ padding-bottom: 15px;
499
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
500
+ }
501
+
502
+ .modal-title {
503
+ font-size: 1.2em;
504
+ font-weight: 600;
505
+ color: #a1a1aa;
506
+ }
507
+
508
+ .modal-close {
509
+ background: none;
510
+ border: none;
511
+ color: #71717a;
512
+ font-size: 1.5em;
513
+ cursor: pointer;
514
+ padding: 0;
515
+ width: 30px;
516
+ height: 30px;
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ border-radius: 6px;
521
+ transition: all 0.3s;
522
+ }
523
+
524
+ .modal-close:hover {
525
+ background: rgba(255, 255, 255, 0.1);
526
+ color: #e4e4e7;
527
+ }
528
+
529
+ .modal-body {
530
+ color: #e4e4e7;
531
+ line-height: 1.6;
532
+ }
533
+
534
+ .modal-body p {
535
+ margin-bottom: 10px;
536
+ }
537
+
538
+ .modal-body strong {
539
+ color: #a1a1aa;
540
+ }
541
+
542
+ .modal-footer {
543
+ margin-top: 20px;
544
+ padding-top: 15px;
545
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
546
+ display: flex;
547
+ justify-content: flex-end;
548
+ gap: 10px;
549
+ }
550
+
551
+ .modal-btn {
552
+ padding: 10px 20px;
553
+ border-radius: 8px;
554
+ border: 1px solid rgba(255, 255, 255, 0.1);
555
+ background: rgba(255, 255, 255, 0.05);
556
+ color: #e4e4e7;
557
+ cursor: pointer;
558
+ font-size: 14px;
559
+ transition: all 0.3s;
560
+ }
561
+
562
+ .modal-btn:hover {
563
+ background: rgba(255, 255, 255, 0.1);
564
+ }
565
+
566
+ .modal-btn.primary {
567
+ background: rgba(102, 126, 234, 0.3);
568
+ border-color: rgba(102, 126, 234, 0.4);
569
+ color: #667eea;
570
+ }
571
+
572
+ .modal-btn.primary:hover {
573
+ background: rgba(102, 126, 234, 0.4);
574
+ }
575
+
576
+ .modal-btn.danger {
577
+ background: rgba(239, 68, 68, 0.2);
578
+ border-color: rgba(239, 68, 68, 0.3);
579
+ color: #ef4444;
580
+ }
581
+
582
+ .modal-btn.danger:hover {
583
+ background: rgba(239, 68, 68, 0.3);
584
+ }
585
+
586
+ .modal-input {
587
+ width: 100%;
588
+ padding: 12px;
589
+ border: 1px solid rgba(255, 255, 255, 0.1);
590
+ border-radius: 8px;
591
+ background: rgba(255, 255, 255, 0.05);
592
+ color: #e4e4e7;
593
+ font-size: 14px;
594
+ margin-top: 10px;
595
+ resize: vertical;
596
+ min-height: 80px;
597
+ font-family: inherit;
598
+ }
599
+
600
+ .modal-input:focus {
601
+ outline: none;
602
+ border-color: #667eea;
603
+ }
604
+
605
+ .modal-input::placeholder {
606
+ color: #71717a;
607
+ }
608
+
609
+ .modal-hint {
610
+ font-size: 0.85em;
611
+ color: #71717a;
612
+ margin-top: 8px;
613
+ }
614
+ </style>
615
+ </head>
616
+ <body>
617
+ <div class="container">
618
+ <!-- 头部 -->
619
+ <div class="header">
620
+ <div class="header-left">
621
+ <div class="logo">🤖</div>
622
+ <div>
623
+ <h1>CodeFix</h1>
624
+ <div style="font-size: 0.85em; color: #71717a;">AI 自动代码修复 - 实时监控</div>
625
+ </div>
626
+ </div>
627
+ <div class="header-right">
628
+ <div class="status-badge online" id="serviceStatus">
629
+ <span class="status-dot"></span>
630
+ <span>服务在线</span>
631
+ </div>
632
+ <div style="font-size: 0.85em; color: #71717a;" id="uptime">运行时间: --</div>
633
+ </div>
634
+ </div>
635
+
636
+ <!-- 统计卡片 -->
637
+ <div class="grid">
638
+ <div class="card">
639
+ <div class="card-header">
640
+ <div class="card-title">📊 任务统计</div>
641
+ <button class="refresh-btn" onclick="refreshStats()">刷新</button>
642
+ </div>
643
+ <div class="stats-grid">
644
+ <div class="stat-item">
645
+ <div class="stat-value" id="totalTasks">-</div>
646
+ <div class="stat-label">总计</div>
647
+ </div>
648
+ <div class="stat-item">
649
+ <div class="stat-value" id="pendingTasks">-</div>
650
+ <div class="stat-label">等待中</div>
651
+ </div>
652
+ <div class="stat-item">
653
+ <div class="stat-value" id="runningTasks">-</div>
654
+ <div class="stat-label">运行中</div>
655
+ </div>
656
+ <div class="stat-item">
657
+ <div class="stat-value" id="completedTasks">-</div>
658
+ <div class="stat-label">已完成</div>
659
+ </div>
660
+ </div>
661
+ </div>
662
+
663
+ <div class="card">
664
+ <div class="card-header">
665
+ <div class="card-title">⚡ 队列状态</div>
666
+ </div>
667
+ <div class="stats-grid">
668
+ <div class="stat-item">
669
+ <div class="stat-value" id="waitingQueue">-</div>
670
+ <div class="stat-label">等待队列</div>
671
+ </div>
672
+ <div class="stat-item">
673
+ <div class="stat-value" id="activeQueue">-</div>
674
+ <div class="stat-label">活跃任务</div>
675
+ </div>
676
+ <div class="stat-item">
677
+ <div class="stat-value" id="failedTasks">-</div>
678
+ <div class="stat-label">失败任务</div>
679
+ </div>
680
+ <div class="stat-item">
681
+ <div class="stat-value" id="waitingApproval">-</div>
682
+ <div class="stat-label">待审批</div>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+
688
+ <!-- 任务列表 -->
689
+ <div class="card" style="margin-bottom: 20px;">
690
+ <div class="card-header">
691
+ <div class="card-title">📋 任务列表</div>
692
+ <div class="filter-bar">
693
+ <button class="filter-btn active" onclick="filterTasks('all')">全部</button>
694
+ <button class="filter-btn" onclick="filterTasks('pending')">等待中</button>
695
+ <button class="filter-btn" onclick="filterTasks('running')">运行中</button>
696
+ <button class="filter-btn" onclick="filterTasks('waiting_approval')">待审批</button>
697
+ <button class="filter-btn" onclick="filterTasks('completed')">已完成</button>
698
+ <button class="filter-btn" onclick="filterTasks('failed')">失败</button>
699
+ </div>
700
+ </div>
701
+ <div class="task-list" id="taskList">
702
+ <div class="empty-state">
703
+ <div class="empty-state-icon">📭</div>
704
+ <div>暂无任务</div>
705
+ <div style="font-size: 0.85em; margin-top: 10px;">使用 <code style="background: rgba(255,255,255,0.1); padding: 4px 8px; border-radius: 4px;">cfix "你的需求"</code> 创建任务</div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+
710
+ <!-- 实时日志 -->
711
+ <div class="card">
712
+ <div class="card-header">
713
+ <div class="card-title">📝 实时日志</div>
714
+ <button class="refresh-btn" onclick="clearLogs()">清空</button>
715
+ </div>
716
+ <div class="log-container" id="logContainer"></div>
717
+ </div>
718
+ </div>
719
+
720
+ <!-- 任务详情模态框 -->
721
+ <div class="modal-overlay" id="taskDetailModal">
722
+ <div class="modal">
723
+ <div class="modal-header">
724
+ <div class="modal-title">📄 任务详情</div>
725
+ <button class="modal-close" onclick="closeTaskDetailModal()">&times;</button>
726
+ </div>
727
+ <div class="modal-body" id="taskDetailBody"></div>
728
+ <div class="modal-footer">
729
+ <button class="modal-btn" onclick="closeTaskDetailModal()">关闭</button>
730
+ </div>
731
+ </div>
732
+ </div>
733
+
734
+ <!-- 拒绝任务模态框 -->
735
+ <div class="modal-overlay" id="rejectModal">
736
+ <div class="modal">
737
+ <div class="modal-header">
738
+ <div class="modal-title">❌ 拒绝任务</div>
739
+ <button class="modal-close" onclick="closeRejectModal()">&times;</button>
740
+ </div>
741
+ <div class="modal-body">
742
+ <p>请输入拒绝原因(可选):</p>
743
+ <textarea class="modal-input" id="rejectReason" placeholder="简要说明拒绝原因,留空则不提供原因"></textarea>
744
+ <div class="modal-hint">💡 提示:拒绝后任务将被还原到修改前的状态</div>
745
+ </div>
746
+ <div class="modal-footer">
747
+ <button class="modal-btn" onclick="closeRejectModal()">取消</button>
748
+ <button class="modal-btn danger" onclick="confirmReject()">确认拒绝</button>
749
+ </div>
750
+ </div>
751
+ </div>
752
+
753
+ <script>
754
+ // API 基础 URL
755
+ const API_BASE = window.location.origin;
756
+ let currentFilter = 'all';
757
+ let refreshInterval = null;
758
+ let currentRejectTaskId = null;
759
+
760
+ // 获取任务状态图标
761
+ function getStatusIcon(status) {
762
+ const icons = {
763
+ pending: '⏳',
764
+ running: '🔄',
765
+ waiting_approval: '⏸️',
766
+ completed: '✅',
767
+ failed: '❌',
768
+ cancelled: '🚫'
769
+ };
770
+ return icons[status] || '❓';
771
+ }
772
+
773
+ // 获取服务状态
774
+ async function getServiceStatus() {
775
+ try {
776
+ const response = await fetch(`${API_BASE}/health`);
777
+ const data = await response.json();
778
+
779
+ // 更新服务状态
780
+ document.getElementById('serviceStatus').className = 'status-badge online';
781
+ document.getElementById('uptime').textContent = `运行时间: ${formatUptime(data.uptime)}`;
782
+
783
+ addLog('info', '服务状态已更新');
784
+ } catch (error) {
785
+ document.getElementById('serviceStatus').className = 'status-badge offline';
786
+ addLog('error', `无法获取服务状态: ${error.message}`);
787
+ }
788
+ }
789
+
790
+ // 更新统计信息(从任务列表接口获取)
791
+ async function updateStats() {
792
+ try {
793
+ const response = await fetch(`${API_BASE}/api/tasks`);
794
+ const data = await response.json();
795
+
796
+ if (data.success && data.stats) {
797
+ const stats = data.stats;
798
+
799
+ // 更新任务统计
800
+ document.getElementById('totalTasks').textContent = stats.total || 0;
801
+ document.getElementById('pendingTasks').textContent = stats.pending || 0;
802
+ document.getElementById('runningTasks').textContent = stats.running || 0;
803
+ document.getElementById('completedTasks').textContent = stats.completed || 0;
804
+ document.getElementById('failedTasks').textContent = stats.failed || 0;
805
+ document.getElementById('waitingApproval').textContent = stats.waiting_approval || 0;
806
+
807
+ // 队列状态(等待队列 = pending,活跃任务 = running)
808
+ document.getElementById('waitingQueue').textContent = stats.pending || 0;
809
+ document.getElementById('activeQueue').textContent = stats.running || 0;
810
+ }
811
+ } catch (error) {
812
+ addLog('error', `更新统计失败: ${error.message}`);
813
+ }
814
+ }
815
+
816
+ // 获取任务列表
817
+ async function getTasks() {
818
+ try {
819
+ const response = await fetch(`${API_BASE}/api/tasks`);
820
+ const data = await response.json();
821
+
822
+ if (data.success && data.tasks) {
823
+ renderTasks(data.tasks);
824
+ }
825
+ } catch (error) {
826
+ addLog('error', `获取任务列表失败: ${error.message}`);
827
+ }
828
+ }
829
+
830
+ // 渲染任务列表
831
+ function renderTasks(tasks) {
832
+ const container = document.getElementById('taskList');
833
+
834
+ // 过滤任务
835
+ let filteredTasks = tasks;
836
+ if (currentFilter !== 'all') {
837
+ filteredTasks = tasks.filter(t => t.status === currentFilter);
838
+ }
839
+
840
+ if (filteredTasks.length === 0) {
841
+ container.innerHTML = `
842
+ <div class="empty-state">
843
+ <div class="empty-state-icon">📭</div>
844
+ <div>暂无任务</div>
845
+ <div style="font-size: 0.85em; margin-top: 10px;">使用 <code style="background: rgba(255,255,255,0.1); padding: 4px 8px; border-radius: 4px;">cfix "你的需求"</code> 创建任务</div>
846
+ </div>
847
+ `;
848
+ return;
849
+ }
850
+
851
+ container.innerHTML = filteredTasks.map(task => `
852
+ <div class="task-item">
853
+ <div class="task-header">
854
+ <div>
855
+ <span class="task-id">${task.taskId}</span>
856
+ <div style="font-size: 0.85em; color: #71717a; margin-top: 4px;">${task.requirement || '无需求描述'}</div>
857
+ </div>
858
+ <span class="task-status ${task.status}">${getStatusIcon(task.status)} ${task.status}</span>
859
+ </div>
860
+ <div class="task-info">
861
+ <div>项目: <span>${task.projectPath || '-'}</span></div>
862
+ <div>创建: <span>${formatDate(task.createdAt)}</span></div>
863
+ </div>
864
+ ${task.progress ? `
865
+ <div class="progress-bar">
866
+ <div class="progress-fill" style="width: ${task.progress}%"></div>
867
+ </div>
868
+ <div style="text-align: right; font-size: 0.75em; color: #71717a; margin-top: 5px;">${task.progress}%</div>
869
+ ` : ''}
870
+ ${task.status === 'waiting_approval' ? `
871
+ <div class="task-actions">
872
+ <button class="task-btn approve" onclick="approveTask('${task.taskId}')">✓ 批准</button>
873
+ <button class="task-btn reject" onclick="rejectTask('${task.taskId}')">✕ 拒绝</button>
874
+ <button class="task-btn view" onclick="viewTask('${task.taskId}')">👁 查看</button>
875
+ </div>
876
+ ` : ''}
877
+ </div>
878
+ `).join('');
879
+ }
880
+
881
+ // 过滤任务
882
+ function filterTasks(status) {
883
+ currentFilter = status;
884
+
885
+ // 更新按钮状态
886
+ document.querySelectorAll('.filter-btn').forEach(btn => {
887
+ btn.classList.remove('active');
888
+ });
889
+ event.target.classList.add('active');
890
+
891
+ // 重新获取任务
892
+ getTasks();
893
+ }
894
+
895
+ // 批准任务
896
+ async function approveTask(taskId) {
897
+ try {
898
+ const response = await fetch(`${API_BASE}/api/tasks/${taskId}/approve`, {
899
+ method: 'POST'
900
+ });
901
+ const data = await response.json();
902
+
903
+ if (data.success) {
904
+ addLog('success', `任务 ${taskId} 已批准`);
905
+ getTasks();
906
+ } else {
907
+ addLog('error', `批准失败: ${data.error || '未知错误'}`);
908
+ }
909
+ } catch (error) {
910
+ addLog('error', `批准任务失败: ${error.message}`);
911
+ }
912
+ }
913
+
914
+ // 拒绝任务
915
+ async function rejectTask(taskId) {
916
+ currentRejectTaskId = taskId;
917
+ document.getElementById('rejectReason').value = '';
918
+ document.getElementById('rejectModal').classList.add('active');
919
+ }
920
+
921
+ // 关闭拒绝模态框
922
+ function closeRejectModal() {
923
+ document.getElementById('rejectModal').classList.remove('active');
924
+ currentRejectTaskId = null;
925
+ }
926
+
927
+ // 确认拒绝
928
+ async function confirmReject() {
929
+ if (!currentRejectTaskId) return;
930
+
931
+ const reason = document.getElementById('rejectReason').value.trim();
932
+
933
+ try {
934
+ const response = await fetch(`${API_BASE}/api/tasks/${currentRejectTaskId}/reject`, {
935
+ method: 'POST',
936
+ headers: { 'Content-Type': 'application/json' },
937
+ body: JSON.stringify({ reason })
938
+ });
939
+ const data = await response.json();
940
+
941
+ if (data.success) {
942
+ addLog('warning', `任务 ${currentRejectTaskId} 已拒绝${reason ? ': ' + reason : ''}`);
943
+ getTasks();
944
+ } else {
945
+ addLog('error', `拒绝失败: ${data.error || '未知错误'}`);
946
+ }
947
+ } catch (error) {
948
+ addLog('error', `拒绝任务失败: ${error.message}`);
949
+ } finally {
950
+ closeRejectModal();
951
+ }
952
+ }
953
+
954
+ // 查看任务
955
+ async function viewTask(taskId) {
956
+ try {
957
+ const response = await fetch(`${API_BASE}/api/tasks/${taskId}`);
958
+ const data = await response.json();
959
+
960
+ if (data.success) {
961
+ const task = data.task;
962
+ const body = document.getElementById('taskDetailBody');
963
+
964
+ let html = `<p><strong>任务 ID:</strong> ${task.taskId}</p>`;
965
+ html += `<p><strong>需求:</strong> ${task.requirement || '无'}</p>`;
966
+ html += `<p><strong>状态:</strong> ${getStatusIcon(task.status)} ${task.status}</p>`;
967
+ html += `<p><strong>项目:</strong> ${task.projectPath || '-'}</p>`;
968
+ html += `<p><strong>创建时间:</strong> ${formatDate(task.createdAt)}</p>`;
969
+ html += `<p><strong>更新时间:</strong> ${formatDate(task.updatedAt)}</p>`;
970
+
971
+ if (task.completedAt) {
972
+ html += `<p><strong>完成时间:</strong> ${formatDate(task.completedAt)}</p>`;
973
+ }
974
+
975
+ if (task.duration) {
976
+ html += `<p><strong>执行耗时:</strong> ${task.duration}</p>`;
977
+ }
978
+
979
+ if (task.error) {
980
+ html += `<p><strong>错误信息:</strong> <span style="color: #ef4444;">${task.error}</span></p>`;
981
+ }
982
+
983
+ if (task.changes) {
984
+ html += `<p><strong>修改摘要:</strong> ${task.changes.summary || '无'}</p>`;
985
+ if (task.changes.files && task.changes.files.length > 0) {
986
+ html += `<p><strong>修改文件 (${task.changes.files.length}):</strong></p>`;
987
+ html += `<ul style="margin-left: 20px; margin-top: 5px;">`;
988
+ task.changes.files.forEach(file => {
989
+ html += `<li style="color: #a1a1aa;">${file}</li>`;
990
+ });
991
+ html += `</ul>`;
992
+ }
993
+ }
994
+
995
+ if (task.result) {
996
+ html += `<p><strong>执行结果:</strong></p>`;
997
+ html += `<ul style="margin-left: 20px; margin-top: 5px;">`;
998
+ html += `<li style="color: ${task.result.success ? '#10b981' : '#ef4444'};">${task.result.success ? '成功' : '失败'}</li>`;
999
+ if (task.result.message) {
1000
+ html += `<li style="color: #a1a1aa;">${task.result.message}</li>`;
1001
+ }
1002
+ if (task.result.files) {
1003
+ html += `<li style="color: #a1a1aa;">修改: ${task.result.files.modified?.length || 0} 个</li>`;
1004
+ html += `<li style="color: #a1a1aa;">新增: ${task.result.files.added?.length || 0} 个</li>`;
1005
+ html += `<li style="color: #a1a1aa;">删除: ${task.result.files.deleted?.length || 0} 个</li>`;
1006
+ }
1007
+ html += `</ul>`;
1008
+ }
1009
+
1010
+ body.innerHTML = html;
1011
+ document.getElementById('taskDetailModal').classList.add('active');
1012
+ } else {
1013
+ addLog('error', `获取任务详情失败: ${data.error || '未知错误'}`);
1014
+ }
1015
+ } catch (error) {
1016
+ addLog('error', `查看任务失败: ${error.message}`);
1017
+ }
1018
+ }
1019
+
1020
+ // 关闭任务详情模态框
1021
+ function closeTaskDetailModal() {
1022
+ document.getElementById('taskDetailModal').classList.remove('active');
1023
+ }
1024
+
1025
+ // 刷新统计
1026
+ function refreshStats() {
1027
+ getServiceStatus();
1028
+ updateStats();
1029
+ getTasks();
1030
+ }
1031
+
1032
+ // 添加日志
1033
+ function addLog(type, message) {
1034
+ const container = document.getElementById('logContainer');
1035
+ const time = new Date().toLocaleTimeString('zh-CN');
1036
+
1037
+ const entry = document.createElement('div');
1038
+ entry.className = 'log-entry ' + type;
1039
+ entry.innerHTML = `<span class="log-time">[${time}]</span>${message}`;
1040
+
1041
+ container.appendChild(entry);
1042
+ container.scrollTop = container.scrollHeight;
1043
+
1044
+ // 限制日志数量
1045
+ while (container.children.length > 100) {
1046
+ container.removeChild(container.firstChild);
1047
+ }
1048
+ }
1049
+
1050
+ // 清空日志
1051
+ function clearLogs() {
1052
+ document.getElementById('logContainer').innerHTML = '';
1053
+ }
1054
+
1055
+ // 格式化时间
1056
+ function formatUptime(seconds) {
1057
+ if (seconds < 60) return `${Math.floor(seconds)}秒`;
1058
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
1059
+ const hours = Math.floor(seconds / 3600);
1060
+ const minutes = Math.floor((seconds % 3600) / 60);
1061
+ return `${hours}小时${minutes}分钟`;
1062
+ }
1063
+
1064
+ // 格式化日期
1065
+ function formatDate(dateString) {
1066
+ if (!dateString) return '-';
1067
+ return new Date(dateString).toLocaleString('zh-CN');
1068
+ }
1069
+
1070
+ // 初始化
1071
+ function init() {
1072
+ addLog('info', '欢迎使用 CodeFix 监控面板');
1073
+ getServiceStatus();
1074
+ updateStats();
1075
+ getTasks();
1076
+
1077
+ // 定时刷新
1078
+ refreshInterval = setInterval(() => {
1079
+ getServiceStatus();
1080
+ updateStats();
1081
+ getTasks();
1082
+ }, 5000);
1083
+ }
1084
+
1085
+ // 页面加载时初始化
1086
+ window.addEventListener('load', init);
1087
+
1088
+ // 页面卸载时清理
1089
+ window.addEventListener('beforeunload', () => {
1090
+ if (refreshInterval) {
1091
+ clearInterval(refreshInterval);
1092
+ }
1093
+ });
1094
+ </script>
1095
+ </body>
1096
+ </html>