@aiscene/aiserver 1.2.5 → 1.2.7
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/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -49
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +1 -7
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/websocket-server.d.ts +5 -0
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +100 -13
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +18 -17
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/debug-page.d.ts.map +1 -1
- package/dist/web/debug-page.js +325 -196
- package/dist/web/debug-page.js.map +1 -1
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +313 -203
- package/dist/web/server.js.map +1 -1
- package/package.json +2 -2
package/dist/web/server.js
CHANGED
|
@@ -213,6 +213,7 @@ export class WebServer {
|
|
|
213
213
|
this.app = express();
|
|
214
214
|
this.setupMiddleware();
|
|
215
215
|
this.setupApiRoutes();
|
|
216
|
+
this.setupDetailRoutes();
|
|
216
217
|
this.setupStaticFiles();
|
|
217
218
|
}
|
|
218
219
|
setupMiddleware() {
|
|
@@ -453,6 +454,26 @@ export class WebServer {
|
|
|
453
454
|
});
|
|
454
455
|
this.app.use('/api', api);
|
|
455
456
|
}
|
|
457
|
+
setupDetailRoutes() {
|
|
458
|
+
// 任务详情页
|
|
459
|
+
this.app.get('/task-detail', (req, res) => {
|
|
460
|
+
const taskId = req.query.id;
|
|
461
|
+
if (!taskId) {
|
|
462
|
+
res.status(400).send('Missing id');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
res.status(200).send(this.getTaskDetailHtml());
|
|
466
|
+
});
|
|
467
|
+
// 调试会话详情页
|
|
468
|
+
this.app.get('/debug-detail', (req, res) => {
|
|
469
|
+
const sessionId = req.query.id;
|
|
470
|
+
if (!sessionId) {
|
|
471
|
+
res.status(400).send('Missing id');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
res.status(200).send(this.getDebugDetailHtml());
|
|
475
|
+
});
|
|
476
|
+
}
|
|
456
477
|
setupStaticFiles() {
|
|
457
478
|
const distPath = path.resolve(import.meta.dirname, 'dist');
|
|
458
479
|
this.app.use(express.static(distPath));
|
|
@@ -473,63 +494,72 @@ export class WebServer {
|
|
|
473
494
|
<title>AIServer 管理面板</title>
|
|
474
495
|
<style>
|
|
475
496
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
476
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#
|
|
477
|
-
.container{max-width:1400px;margin:0 auto;padding:20px}
|
|
497
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8fafc;color:#1e293b;min-height:100vh}
|
|
498
|
+
.container{max-width:1400px;margin:0 auto;padding:20px;height:calc(100vh - 40px);display:flex;flex-direction:column}
|
|
499
|
+
.container.debugger-full{max-width:100%;padding:0;height:100vh;overflow:hidden}
|
|
500
|
+
.container.debugger-full .header{display:none}
|
|
501
|
+
.container.debugger-full .tabs{display:none}
|
|
502
|
+
.container.debugger-full>[id^="page-"]{flex:1;min-height:0}
|
|
478
503
|
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
|
479
|
-
.header h1{font-size:24px;color:#
|
|
480
|
-
.tabs{display:flex;gap:4px;background:#
|
|
481
|
-
.tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#
|
|
482
|
-
.tab.active{background:#38bdf8;color:#
|
|
483
|
-
.tab:hover:not(.active){color:#
|
|
504
|
+
.header h1{font-size:24px;color:#0f172a}
|
|
505
|
+
.tabs{display:flex;gap:4px;background:#f1f5f9;border-radius:8px;padding:4px;margin-bottom:24px}
|
|
506
|
+
.tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#64748b;border:none;background:none;transition:all .2s}
|
|
507
|
+
.tab.active{background:#38bdf8;color:#fff}
|
|
508
|
+
.tab:hover:not(.active){color:#334155}
|
|
484
509
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
|
|
485
|
-
.card{background:#fff;border-radius:8px;padding:20px;border:1px solid #
|
|
486
|
-
.card h3{font-size:13px;color:#
|
|
487
|
-
.card .value{font-size:32px;font-weight:700;color:#
|
|
488
|
-
.card .sub{font-size:12px;color:#
|
|
510
|
+
.card{background:#fff;border-radius:8px;padding:20px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
|
|
511
|
+
.card h3{font-size:13px;color:#64748b;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|
512
|
+
.card .value{font-size:32px;font-weight:700;color:#0f172a}
|
|
513
|
+
.card .sub{font-size:12px;color:#94a3b8;margin-top:4px}
|
|
489
514
|
table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
490
|
-
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #
|
|
491
|
-
th{color:#
|
|
492
|
-
tr:hover{background:#
|
|
515
|
+
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:13px;color:#1e293b}
|
|
516
|
+
th{color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#f8fafc;position:sticky;top:0}
|
|
517
|
+
tr:hover{background:#f8fafc}
|
|
493
518
|
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}
|
|
494
|
-
.b-online{background:#
|
|
495
|
-
.b-offline{background:#
|
|
496
|
-
.b-running{background:#
|
|
497
|
-
.b-completed{background:#
|
|
498
|
-
.b-failed{background:#
|
|
499
|
-
.b-pending{background:#
|
|
500
|
-
.b-idle{background:#
|
|
501
|
-
.b-stopped{background:#
|
|
519
|
+
.b-online{background:#dcfce7;color:#16a34a}
|
|
520
|
+
.b-offline{background:#fee2e2;color:#dc2626}
|
|
521
|
+
.b-running{background:#fef3c7;color:#d97706}
|
|
522
|
+
.b-completed{background:#dcfce7;color:#16a34a}
|
|
523
|
+
.b-failed{background:#fee2e2;color:#dc2626}
|
|
524
|
+
.b-pending{background:#dbeafe;color:#2563eb}
|
|
525
|
+
.b-idle{background:#f1f5f9;color:#64748b}
|
|
526
|
+
.b-stopped{background:#f1f5f9;color:#64748b}
|
|
502
527
|
.btn{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s}
|
|
503
|
-
.btn-primary{background:#
|
|
504
|
-
.btn-primary:hover{background:#
|
|
528
|
+
.btn-primary{background:#2563eb;color:#fff}
|
|
529
|
+
.btn-primary:hover{background:#1d4ed8}
|
|
505
530
|
.btn-sm{padding:4px 10px;font-size:11px}
|
|
506
|
-
.section-title{font-size:16px;font-weight:600;color:#
|
|
531
|
+
.section-title{font-size:16px;font-weight:600;color:#1e293b;margin:20px 0 12px;display:flex;align-items:center;gap:8px}
|
|
507
532
|
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
508
|
-
.log-viewer{background:#fff;border:1px solid #
|
|
509
|
-
.log-viewer .log-line{padding:2px 0;border-bottom:1px solid #
|
|
510
|
-
.log-viewer .log-line:hover{background:#
|
|
511
|
-
.log-time{color:#
|
|
512
|
-
.log-level-info{color:#
|
|
513
|
-
.log-level-warn{color:#
|
|
514
|
-
.log-level-error{color:#
|
|
533
|
+
.log-viewer{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px 16px 40px;max-height:600px;overflow-y:auto;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6}
|
|
534
|
+
.log-viewer .log-line{padding:2px 0;border-bottom:1px solid #f1f5f9}
|
|
535
|
+
.log-viewer .log-line:hover{background:#f8fafc}
|
|
536
|
+
.log-time{color:#94a3b8;margin-right:8px}
|
|
537
|
+
.log-level-info{color:#0284c7}
|
|
538
|
+
.log-level-warn{color:#d97706}
|
|
539
|
+
.log-level-error{color:#dc2626}
|
|
515
540
|
.log-level-debug{color:#94a3b8}
|
|
516
|
-
.log-content{color:#
|
|
541
|
+
.log-content{color:#334155}
|
|
517
542
|
.log-new{animation:flashLog 0.5s ease-out}
|
|
518
|
-
@keyframes flashLog{0%{background:#
|
|
519
|
-
.log-live{color:#
|
|
543
|
+
@keyframes flashLog{0%{background:#dbeafe}100%{background:transparent}}
|
|
544
|
+
.log-live{color:#16a34a;font-size:10px;margin-left:8px;animation:pulse 2s infinite}
|
|
520
545
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
521
|
-
.detail-panel{background:#fff;border:1px solid #
|
|
522
|
-
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #
|
|
523
|
-
.detail-label{width:140px;color:#
|
|
524
|
-
.detail-value{color:#
|
|
525
|
-
.empty-state{text-align:center;padding:60px 20px;color:#
|
|
546
|
+
.detail-panel{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:20px;margin-top:16px;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
|
|
547
|
+
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
548
|
+
.detail-label{width:140px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
549
|
+
.detail-value{color:#1e293b;font-size:13px;word-break:break-all}
|
|
550
|
+
.empty-state{text-align:center;padding:60px 20px;color:#94a3b8}
|
|
526
551
|
.empty-state .icon{font-size:48px;margin-bottom:16px}
|
|
527
552
|
.refresh-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
|
528
|
-
.refresh-bar .auto{font-size:12px;color:#
|
|
529
|
-
.refresh-bar .auto.on{color:#
|
|
530
|
-
.search{background:#fff;border:1px solid #
|
|
553
|
+
.refresh-bar .auto{font-size:12px;color:#94a3b8}
|
|
554
|
+
.refresh-bar .auto.on{color:#16a34a}
|
|
555
|
+
.search{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:8px 14px;color:#1e293b;font-size:13px;width:220px}
|
|
531
556
|
.search:focus{outline:none;border-color:#38bdf8}
|
|
532
557
|
.collapsed{display:none}
|
|
558
|
+
.table-wrap{max-height:480px;overflow-y:auto;border:1px solid #e2e8f0;border-radius:8px;margin-top:8px}
|
|
559
|
+
.table-wrap table{margin-top:0}
|
|
560
|
+
.pager{display:flex;align-items:center;justify-content:space-between;padding:8px 0;font-size:12px;color:#64748b}
|
|
561
|
+
.pager .btn{margin:0 2px}
|
|
562
|
+
.pager .info{display:flex;align-items:center;gap:8px}
|
|
533
563
|
</style>
|
|
534
564
|
</head>
|
|
535
565
|
<body>
|
|
@@ -558,7 +588,6 @@ tr:hover{background:#fff}
|
|
|
558
588
|
let currentTab='dashboard';
|
|
559
589
|
let autoRefresh=true;
|
|
560
590
|
let refreshTimer=null;
|
|
561
|
-
let detailOpen=false; // track if a detail panel is open
|
|
562
591
|
|
|
563
592
|
// ===== WebSocket for Real-time Task Logs =====
|
|
564
593
|
let ws=null;
|
|
@@ -671,13 +700,9 @@ function updateTaskLogUI(taskId){
|
|
|
671
700
|
logViewer.scrollTop=logViewer.scrollHeight;
|
|
672
701
|
}
|
|
673
702
|
|
|
674
|
-
// ===== Tab
|
|
675
|
-
let currentDetailTaskId=null;
|
|
676
|
-
let currentDetailTaskStatus=null;
|
|
677
|
-
|
|
703
|
+
// ===== Tab switching =====
|
|
678
704
|
function switchTab(tab){
|
|
679
705
|
currentTab=tab;
|
|
680
|
-
detailOpen=false;
|
|
681
706
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
682
707
|
document.querySelectorAll('.tab').forEach(t=>{
|
|
683
708
|
var txt=t.textContent.toLowerCase();
|
|
@@ -689,6 +714,9 @@ function switchTab(tab){
|
|
|
689
714
|
});
|
|
690
715
|
document.querySelectorAll('[id^="page-"]').forEach(p=>p.classList.add('collapsed'));
|
|
691
716
|
document.getElementById('page-'+tab).classList.remove('collapsed');
|
|
717
|
+
// 调试执行页面全屏展示
|
|
718
|
+
const container=document.querySelector('.container');
|
|
719
|
+
if(tab==='debugger'){container.classList.add('debugger-full')}else{container.classList.remove('debugger-full')}
|
|
692
720
|
if(tab!=='debugger') refresh();
|
|
693
721
|
}
|
|
694
722
|
|
|
@@ -697,7 +725,9 @@ function badge(s){
|
|
|
697
725
|
return '<span class="badge '+cls+'">'+s+'</span>';
|
|
698
726
|
}
|
|
699
727
|
|
|
700
|
-
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'
|
|
728
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
729
|
+
|
|
730
|
+
function openDetail(el){window.open(el.dataset.type+'?id='+encodeURIComponent(el.dataset.id),'_blank')}
|
|
701
731
|
|
|
702
732
|
function timeAgo(d){
|
|
703
733
|
if(!d)return '-';
|
|
@@ -716,9 +746,9 @@ async function api(path){
|
|
|
716
746
|
|
|
717
747
|
async function refresh(){
|
|
718
748
|
if(currentTab==='dashboard')await loadDashboard();
|
|
719
|
-
else if(currentTab==='devices')await loadDevices();
|
|
720
|
-
else if(currentTab==='tasks')await loadTasks();
|
|
721
|
-
else if(currentTab==='debug')await loadDebug();
|
|
749
|
+
else if(currentTab==='devices'){await loadDevicesData();loadDevices();}
|
|
750
|
+
else if(currentTab==='tasks'){await loadTasksData();loadTasks();}
|
|
751
|
+
else if(currentTab==='debug'){await loadDebugData();loadDebug();}
|
|
722
752
|
else if(currentTab==='debugger')loadDebugger();
|
|
723
753
|
}
|
|
724
754
|
|
|
@@ -739,41 +769,46 @@ async function loadDashboard(){
|
|
|
739
769
|
|
|
740
770
|
// Devices overview
|
|
741
771
|
if(d.devices.list&&d.devices.list.length>0){
|
|
742
|
-
h+='<div class="section-title">设备列表</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
|
|
743
|
-
d.devices.list.forEach(dev=>{
|
|
772
|
+
h+='<div class="section-title">设备列表</div><div class="table-wrap"><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
|
|
773
|
+
d.devices.list.slice(0,20).forEach(dev=>{
|
|
744
774
|
h+='<tr><td class="mono">'+esc(dev.serialNumber)+'</td><td>'+esc(dev.model||'-')+'</td><td>'+esc(dev.brand||'-')+'</td><td>'+esc(dev.platform)+'</td><td>'+badge(dev.status)+'</td><td>'+timeAgo(dev.lastHeartbeatAt)+'</td></tr>';
|
|
745
775
|
});
|
|
746
|
-
h+='</table>';
|
|
776
|
+
h+='</table></div>';
|
|
747
777
|
}
|
|
748
778
|
|
|
749
779
|
// Recent tasks
|
|
750
780
|
if(d.tasks.recent&&d.tasks.recent.length>0){
|
|
751
|
-
h+='<div class="section-title">近期任务</div><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
752
|
-
d.tasks.recent.forEach(t=>{
|
|
753
|
-
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td><button class="btn btn-primary btn-sm"
|
|
781
|
+
h+='<div class="section-title">近期任务</div><div class="table-wrap"><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
782
|
+
d.tasks.recent.slice(0,20).forEach(t=>{
|
|
783
|
+
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td><button class="btn btn-primary btn-sm" data-type="/task-detail" data-id="'+esc(t.taskId)+'" onclick="openDetail(this)">日志</button></td></tr>';
|
|
754
784
|
});
|
|
755
|
-
h+='</table>';
|
|
785
|
+
h+='</table></div>';
|
|
756
786
|
}
|
|
757
787
|
|
|
758
788
|
// Recent debug sessions
|
|
759
789
|
if(d.debugSessions&&d.debugSessions.length>0){
|
|
760
|
-
h+='<div class="section-title">近期调试会话</div><table><tr><th>会话ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>操作</th></tr>';
|
|
790
|
+
h+='<div class="section-title">近期调试会话</div><div class="table-wrap"><table><tr><th>会话ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>操作</th></tr>';
|
|
761
791
|
d.debugSessions.slice(0,10).forEach(s=>{
|
|
762
|
-
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,24))+'...</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td><button class="btn btn-primary btn-sm"
|
|
792
|
+
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,24))+'...</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td><button class="btn btn-primary btn-sm" data-type="/debug-detail" data-id="'+esc(s.sessionId)+'" onclick="openDetail(this)">日志</button></td></tr>';
|
|
763
793
|
});
|
|
764
|
-
h+='</table>';
|
|
794
|
+
h+='</table></div>';
|
|
765
795
|
}
|
|
766
796
|
|
|
767
|
-
h+='<div id="detail-panel"></div>';
|
|
768
797
|
p.innerHTML=h;
|
|
769
798
|
}
|
|
770
799
|
|
|
771
800
|
// ===== Devices =====
|
|
772
|
-
async function
|
|
801
|
+
async function loadDevicesData(){
|
|
773
802
|
const devs=await api('/devices');
|
|
774
|
-
if(!devs)return;
|
|
775
|
-
|
|
803
|
+
if(!devs)return[];
|
|
804
|
+
allDevices=devs;
|
|
805
|
+
return devs;
|
|
806
|
+
}
|
|
807
|
+
function loadDevices(page){
|
|
808
|
+
if(page)devicePage=page;
|
|
809
|
+
const devs=allDevices;
|
|
776
810
|
const online=devs.filter(d=>d.status==='online').length;
|
|
811
|
+
const p=document.getElementById('page-devices');
|
|
777
812
|
let h='<div class="grid">';
|
|
778
813
|
h+='<div class="card"><h3>设备总数</h3><div class="value">'+devs.length+'</div></div>';
|
|
779
814
|
h+='<div class="card"><h3>在线</h3><div class="value">'+online+'</div></div>';
|
|
@@ -781,11 +816,13 @@ async function loadDevices(){
|
|
|
781
816
|
h+='</div>';
|
|
782
817
|
|
|
783
818
|
if(devs.length>0){
|
|
784
|
-
|
|
785
|
-
devs.
|
|
819
|
+
const slice=paginate(devs,devicePage);
|
|
820
|
+
h+='<div class="section-title">所有设备</div>'+renderPager('dev',devs.length,devicePage,'loadDevices');
|
|
821
|
+
h+='<div class="table-wrap"><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th><th>创建时间</th></tr>';
|
|
822
|
+
slice.forEach(d=>{
|
|
786
823
|
h+='<tr><td class="mono">'+esc(d.serialNumber)+'</td><td>'+esc(d.model||'-')+'</td><td>'+esc(d.brand||'-')+'</td><td>'+esc(d.platform)+'</td><td>'+badge(d.status)+'</td><td>'+timeAgo(d.lastHeartbeatAt)+'</td><td>'+fmtTime(d.createdAt)+'</td></tr>';
|
|
787
824
|
});
|
|
788
|
-
h+='</table>';
|
|
825
|
+
h+='</table></div>';
|
|
789
826
|
}else{
|
|
790
827
|
h+='<div class="empty-state"><div class="icon">📱</div><p>暂无设备</p></div>';
|
|
791
828
|
}
|
|
@@ -793,10 +830,15 @@ async function loadDevices(){
|
|
|
793
830
|
}
|
|
794
831
|
|
|
795
832
|
// ===== Tasks =====
|
|
796
|
-
async function
|
|
797
|
-
const tasks=await api('/tasks?limit=
|
|
798
|
-
if(!tasks)return;
|
|
799
|
-
|
|
833
|
+
async function loadTasksData(){
|
|
834
|
+
const tasks=await api('/tasks?limit=200');
|
|
835
|
+
if(!tasks)return[];
|
|
836
|
+
allTasks=tasks;
|
|
837
|
+
return tasks;
|
|
838
|
+
}
|
|
839
|
+
function loadTasks(page){
|
|
840
|
+
if(page)taskPage=page;
|
|
841
|
+
const tasks=allTasks;
|
|
800
842
|
const running=tasks.filter(t=>t.status==='running').length;
|
|
801
843
|
const completed=tasks.filter(t=>t.status==='completed').length;
|
|
802
844
|
const failed=tasks.filter(t=>t.status==='failed').length;
|
|
@@ -809,91 +851,31 @@ async function loadTasks(){
|
|
|
809
851
|
h+='</div>';
|
|
810
852
|
|
|
811
853
|
if(tasks.length>0){
|
|
812
|
-
|
|
813
|
-
tasks.
|
|
854
|
+
const slice=paginate(tasks,taskPage);
|
|
855
|
+
h+='<div class="section-title">所有任务</div>'+renderPager('task',tasks.length,taskPage,'loadTasks');
|
|
856
|
+
h+='<div class="table-wrap"><table><tr><th>任务ID</th><th>执行ID</th><th>类型</th><th>状态</th><th>优先级</th><th>开始时间</th><th>完成时间</th><th>错误</th><th>操作</th></tr>';
|
|
857
|
+
slice.forEach(t=>{
|
|
814
858
|
const err=t.errorMessage?(t.errorMessage.length>50?esc(t.errorMessage.substring(0,50))+'...':esc(t.errorMessage)):'-';
|
|
815
|
-
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td class="mono">'+esc(t.executionId||'-')+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+t.priority+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td style="color:#ef4444;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+err+'</td><td><button class="btn btn-primary btn-sm"
|
|
859
|
+
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td class="mono">'+esc(t.executionId||'-')+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+t.priority+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td style="color:#ef4444;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+err+'</td><td><button class="btn btn-primary btn-sm" data-type="/task-detail" data-id="'+esc(t.taskId)+'" onclick="openDetail(this)">详情</button></td></tr>';
|
|
816
860
|
});
|
|
817
|
-
h+='</table>';
|
|
861
|
+
h+='</table></div>';
|
|
818
862
|
}else{
|
|
819
863
|
h+='<div class="empty-state"><div class="icon">📋</div><p>暂无任务</p></div>';
|
|
820
864
|
}
|
|
821
|
-
|
|
865
|
+
const p=document.getElementById('page-tasks');
|
|
822
866
|
p.innerHTML=h;
|
|
823
867
|
}
|
|
824
868
|
|
|
825
|
-
async function showTaskDetail(taskId){
|
|
826
|
-
detailOpen=true;
|
|
827
|
-
currentDetailTaskId=taskId;
|
|
828
|
-
currentDetailTaskStatus=null;
|
|
829
|
-
const task=await api('/tasks/'+encodeURIComponent(taskId));
|
|
830
|
-
const logs=await api('/tasks/'+encodeURIComponent(taskId)+'/logs');
|
|
831
|
-
const panel=document.getElementById('task-detail');
|
|
832
|
-
if(!task){panel.innerHTML='<p style="color:#ef4444">任务未找到</p>';detailOpen=false;currentDetailTaskId=null;return}
|
|
833
|
-
|
|
834
|
-
currentDetailTaskStatus=task.status;
|
|
835
|
-
const isRunning=task.status==='running'||task.status==='pending';
|
|
836
|
-
|
|
837
|
-
let h='<div class="detail-panel"><div class="section-title">任务详情: '+esc(task.taskId)+' <button class="btn btn-sm" style="background:#334155;color:#94a3b8;margin-left:12px" onclick="closeTaskDetail()">关闭</button></div>';
|
|
838
|
-
h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(task.taskId)+'</div></div>';
|
|
839
|
-
h+='<div class="detail-row"><div class="detail-label">执行ID</div><div class="detail-value mono">'+esc(task.executionId||'-')+'</div></div>';
|
|
840
|
-
h+='<div class="detail-row"><div class="detail-label">类型</div><div class="detail-value">'+esc(task.type)+'</div></div>';
|
|
841
|
-
h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(task.status)+'</div></div>';
|
|
842
|
-
h+='<div class="detail-row"><div class="detail-label">优先级</div><div class="detail-value">'+task.priority+'</div></div>';
|
|
843
|
-
h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(task.startedAt)+'</div></div>';
|
|
844
|
-
h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(task.completedAt)+'</div></div>';
|
|
845
|
-
h+='<div class="detail-row"><div class="detail-label">创建时间</div><div class="detail-value">'+fmtTime(task.createdAt)+'</div></div>';
|
|
846
|
-
if(task.errorMessage)h+='<div class="detail-row"><div class="detail-label">错误信息</div><div class="detail-value" style="color:#ef4444">'+esc(task.errorMessage)+'</div></div>';
|
|
847
|
-
if(task.config){
|
|
848
|
-
h+='<div class="detail-row"><div class="detail-label">配置</div><div class="detail-value mono" style="white-space:pre-wrap;font-size:11px">'+esc(typeof task.config==='object'?JSON.stringify(task.config,null,2):task.config)+'</div></div>';
|
|
849
|
-
}
|
|
850
|
-
if(task.result){
|
|
851
|
-
h+='<div class="detail-row"><div class="detail-label">结果</div><div class="detail-value mono" style="white-space:pre-wrap;font-size:11px">'+esc(typeof task.result==='object'?JSON.stringify(task.result,null,2):task.result)+'</div></div>';
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Execution logs with real-time support
|
|
855
|
-
const logSafeId=esc(taskId).replace(/[^a-zA-Z0-9-_]/g,'_');
|
|
856
|
-
const existingLogsCount=logs?logs.length:0;
|
|
857
|
-
if(existingLogsCount>0||isRunning){
|
|
858
|
-
h+='<div class="section-title" style="margin-top:20px">执行日志 <span id="task-log-count-'+logSafeId+'" style="color:#64748b;font-size:12px">'+(existingLogsCount||'')+'</span>';
|
|
859
|
-
if(isRunning)h+=' <span style="color:#22c55e;font-size:12px">● LIVE</span>';
|
|
860
|
-
h+='</div>';
|
|
861
|
-
h+='<div class="log-viewer" id="task-log-'+logSafeId+'" style="max-height:400px">';
|
|
862
|
-
if(logs&&logs.length>0){
|
|
863
|
-
logs.forEach(l=>{
|
|
864
|
-
const lvl='log-level-'+esc(l.level||'info');
|
|
865
|
-
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.level||'info').toUpperCase()+']</span> <span class="log-content">'+esc(l.content)+'</span></div>';
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
h+='</div>';
|
|
869
|
-
}else{
|
|
870
|
-
h+='<div class="section-title" style="margin-top:20px">执行日志</div><p style="color:#64748b">暂无日志</p>';
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
h+='</div>';
|
|
874
|
-
panel.innerHTML=h;
|
|
875
|
-
panel.scrollIntoView({behavior:'smooth'});
|
|
876
|
-
|
|
877
|
-
// 滚动到日志底部
|
|
878
|
-
const logViewer=document.getElementById('task-log-'+logSafeId);
|
|
879
|
-
if(logViewer)logViewer.scrollTop=logViewer.scrollHeight;
|
|
880
|
-
|
|
881
|
-
// 订阅实时日志
|
|
882
|
-
if(isRunning){
|
|
883
|
-
subscribeTaskLogs(taskId);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function showTaskLogs(taskId){
|
|
888
|
-
if(currentTab!=='tasks'){switchTab('tasks');setTimeout(()=>showTaskDetail(taskId),500);return}
|
|
889
|
-
showTaskDetail(taskId);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
869
|
// ===== Debug Sessions =====
|
|
893
|
-
async function
|
|
870
|
+
async function loadDebugData(){
|
|
894
871
|
const sessions=await api('/debug/sessions');
|
|
895
|
-
if(!sessions)return;
|
|
896
|
-
|
|
872
|
+
if(!sessions)return[];
|
|
873
|
+
allDebugSessions=sessions;
|
|
874
|
+
return sessions;
|
|
875
|
+
}
|
|
876
|
+
function loadDebug(page){
|
|
877
|
+
if(page)debugPage=page;
|
|
878
|
+
const sessions=allDebugSessions;
|
|
897
879
|
const running=sessions.filter(s=>s.status==='running').length;
|
|
898
880
|
|
|
899
881
|
let h='<div class="grid">';
|
|
@@ -902,80 +884,208 @@ async function loadDebug(){
|
|
|
902
884
|
h+='</div>';
|
|
903
885
|
|
|
904
886
|
if(sessions.length>0){
|
|
905
|
-
|
|
906
|
-
sessions.
|
|
907
|
-
|
|
887
|
+
const slice=paginate(sessions,debugPage);
|
|
888
|
+
h+='<div class="section-title">所有调试会话</div>'+renderPager('dbg',sessions.length,debugPage,'loadDebug');
|
|
889
|
+
h+='<div class="table-wrap"><table><tr><th>会话ID</th><th>任务ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
890
|
+
slice.forEach(s=>{
|
|
891
|
+
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,28))+'...</td><td class="mono">'+esc(s.taskId||'-')+'</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td>'+fmtTime(s.completedAt)+'</td><td><button class="btn btn-primary btn-sm" data-type="/debug-detail" data-id="'+esc(s.sessionId)+'" onclick="openDetail(this)">详情</button></td></tr>';
|
|
908
892
|
});
|
|
909
|
-
h+='</table>';
|
|
893
|
+
h+='</table></div>';
|
|
910
894
|
}else{
|
|
911
895
|
h+='<div class="empty-state"><div class="icon">🐛</div><p>暂无调试会话</p></div>';
|
|
912
896
|
}
|
|
913
|
-
|
|
897
|
+
const p=document.getElementById('page-debug');
|
|
914
898
|
p.innerHTML=h;
|
|
915
899
|
}
|
|
916
900
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
h
|
|
926
|
-
h+='<
|
|
927
|
-
h+='<
|
|
928
|
-
h+='<
|
|
929
|
-
h+='<
|
|
930
|
-
h+='
|
|
931
|
-
h
|
|
901
|
+
// ===== Pagination State =====
|
|
902
|
+
const PAGE_SIZE=20;
|
|
903
|
+
let taskPage=1, devicePage=1, debugPage=1;
|
|
904
|
+
let allTasks=[], allDevices=[], allDebugSessions=[];
|
|
905
|
+
|
|
906
|
+
function renderPager(prefix,total,page,loadFn){
|
|
907
|
+
const pages=Math.ceil(total/PAGE_SIZE)||1;
|
|
908
|
+
if(total<=PAGE_SIZE)return '<div class="pager"><div class="info">共 '+total+' 条</div></div>';
|
|
909
|
+
let h='<div class="pager"><div class="info">共 '+total+' 条,第 '+page+'/'+pages+' 页</div><div>';
|
|
910
|
+
if(page>1)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'(1)">首页</button>';
|
|
911
|
+
if(page>1)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+(page-1)+')">上一页</button>';
|
|
912
|
+
if(page<pages)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+(page+1)+')">下一页</button>';
|
|
913
|
+
if(page<pages)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+pages+')">末页</button>';
|
|
914
|
+
h+='</div></div>';
|
|
915
|
+
return h;
|
|
916
|
+
}
|
|
917
|
+
function paginate(arr,page){const s=(page-1)*PAGE_SIZE;return arr.slice(s,s+PAGE_SIZE)}
|
|
918
|
+
|
|
919
|
+
// ===== Init =====
|
|
920
|
+
initWebSocket(); // 初始化 WebSocket 连接以接收实时日志
|
|
921
|
+
refresh();
|
|
922
|
+
refreshTimer=setInterval(()=>{if(autoRefresh)refresh()},5000);
|
|
923
|
+
</script>
|
|
924
|
+
</body>
|
|
925
|
+
</html>`;
|
|
926
|
+
}
|
|
927
|
+
getTaskDetailHtml() {
|
|
928
|
+
return `<!DOCTYPE html>
|
|
929
|
+
<html lang="zh-CN">
|
|
930
|
+
<head>
|
|
931
|
+
<meta charset="UTF-8">
|
|
932
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
933
|
+
<title>任务详情</title>
|
|
934
|
+
<style>
|
|
935
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
936
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8fafc;color:#1e293b;padding:24px}
|
|
937
|
+
h2{font-size:18px;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px}
|
|
938
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
|
|
939
|
+
.b-running{background:#fef3c7;color:#d97706}.b-completed{background:#dcfce7;color:#16a34a}.b-failed{background:#fee2e2;color:#dc2626}.b-pending{background:#f1f5f9;color:#64748b}.b-unknown{background:#f1f5f9;color:#64748b}
|
|
940
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px}
|
|
941
|
+
.section-title{font-size:13px;font-weight:600;color:#64748b;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
|
|
942
|
+
.row{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
943
|
+
.row:last-child{border-bottom:none}
|
|
944
|
+
.label{width:100px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
945
|
+
.value{color:#1e293b;font-size:13px;word-break:break-all;flex:1}
|
|
946
|
+
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
947
|
+
.log-viewer{max-height:500px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
|
|
948
|
+
.log-line{padding:4px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.5}
|
|
949
|
+
.log-time{color:#94a3b8;margin-right:8px;font-size:11px}
|
|
950
|
+
.log-level-info{color:#0284c7}.log-level-warn{color:#d97706}.log-level-error{color:#dc2626}
|
|
951
|
+
.log-content{color:#475569}
|
|
952
|
+
.loading{text-align:center;padding:60px;color:#94a3b8}
|
|
953
|
+
.error{color:#dc2626;padding:20px}
|
|
954
|
+
</style>
|
|
955
|
+
</head>
|
|
956
|
+
<body>
|
|
957
|
+
<h2>📋 任务详情</h2>
|
|
958
|
+
<div id="content"><div class="loading">加载中...</div></div>
|
|
959
|
+
<script>
|
|
960
|
+
const params=new URLSearchParams(location.search);
|
|
961
|
+
const taskId=params.get('id');
|
|
962
|
+
if(!taskId){document.getElementById('content').innerHTML='<div class="error">缺少任务ID</div>';}
|
|
963
|
+
|
|
964
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
965
|
+
|
|
966
|
+
function fmtTime(d){if(!d)return '-';return new Date(d).toLocaleString('zh-CN',{hour12:false})}
|
|
967
|
+
function badge(s){const cls='b-'+(s||'unknown');return '<span class="badge '+cls+'">'+esc(s)+'</span>'}
|
|
968
|
+
|
|
969
|
+
async function load(){
|
|
970
|
+
const task=await (await fetch('/api/tasks/'+encodeURIComponent(taskId))).json();
|
|
971
|
+
const logs=await (await fetch('/api/tasks/'+encodeURIComponent(taskId)+'/logs')).json();
|
|
972
|
+
const c=document.getElementById('content');
|
|
973
|
+
if(!task){c.innerHTML='<div class="error">任务未找到</div>';return}
|
|
974
|
+
|
|
975
|
+
document.title='任务详情: '+task.taskId;
|
|
976
|
+
let h='<div class="section"><div class="section-title">基本信息</div>';
|
|
977
|
+
h+='<div class="row"><div class="label">任务ID</div><div class="value mono">'+esc(task.taskId)+'</div></div>';
|
|
978
|
+
h+='<div class="row"><div class="label">执行ID</div><div class="value mono">'+esc(task.executionId||'-')+'</div></div>';
|
|
979
|
+
h+='<div class="row"><div class="label">类型</div><div class="value">'+esc(task.type)+'</div></div>';
|
|
980
|
+
h+='<div class="row"><div class="label">状态</div><div class="value">'+badge(task.status)+'</div></div>';
|
|
981
|
+
h+='<div class="row"><div class="label">优先级</div><div class="value">'+task.priority+'</div></div>';
|
|
982
|
+
h+='<div class="row"><div class="label">开始时间</div><div class="value">'+fmtTime(task.startedAt)+'</div></div>';
|
|
983
|
+
h+='<div class="row"><div class="label">完成时间</div><div class="value">'+fmtTime(task.completedAt)+'</div></div>';
|
|
984
|
+
h+='<div class="row"><div class="label">创建时间</div><div class="value">'+fmtTime(task.createdAt)+'</div></div>';
|
|
985
|
+
if(task.errorMessage)h+='<div class="row"><div class="label">错误信息</div><div class="value" style="color:#dc2626">'+esc(task.errorMessage)+'</div></div>';
|
|
932
986
|
h+='</div>';
|
|
933
987
|
|
|
934
|
-
|
|
988
|
+
if(task.config){
|
|
989
|
+
h+='<div class="section"><div class="section-title">配置</div><pre class="mono" style="white-space:pre-wrap;font-size:11px;background:#f8fafc;padding:12px;border-radius:4px">'+esc(typeof task.config==='object'?JSON.stringify(task.config,null,2):task.config)+'</pre></div>';
|
|
990
|
+
}
|
|
991
|
+
if(task.result){
|
|
992
|
+
h+='<div class="section"><div class="section-title">结果</div><pre class="mono" style="white-space:pre-wrap;font-size:11px;background:#f8fafc;padding:12px;border-radius:4px">'+esc(typeof task.result==='object'?JSON.stringify(task.result,null,2):task.result)+'</pre></div>';
|
|
993
|
+
}
|
|
994
|
+
|
|
935
995
|
if(logs&&logs.length>0){
|
|
936
|
-
h+='<div class="
|
|
937
|
-
h+='<div class="log-viewer">';
|
|
996
|
+
h+='<div class="section"><div class="section-title">执行日志 ('+logs.length+')</div><div class="log-viewer">';
|
|
938
997
|
logs.forEach(l=>{
|
|
939
|
-
const lvl='log-level-'+esc(l.
|
|
940
|
-
|
|
941
|
-
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span> <span class="log-content">'+esc(content)+'</span></div>';
|
|
998
|
+
const lvl='log-level-'+esc(l.level||'info');
|
|
999
|
+
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.level||'info').toUpperCase()+']</span> <span class="log-content">'+esc(l.content)+'</span></div>';
|
|
942
1000
|
});
|
|
943
1001
|
h+='</div></div>';
|
|
944
1002
|
}else{
|
|
945
|
-
h+='<div class="
|
|
1003
|
+
h+='<div class="section"><div class="section-title">执行日志</div><p style="color:#94a3b8">暂无日志</p></div>';
|
|
946
1004
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1005
|
+
c.innerHTML=h;
|
|
1006
|
+
const lv=c.querySelector('.log-viewer');
|
|
1007
|
+
if(lv)lv.scrollTop=lv.scrollHeight;
|
|
950
1008
|
}
|
|
1009
|
+
load();
|
|
1010
|
+
</script>
|
|
1011
|
+
</body>
|
|
1012
|
+
</html>`;
|
|
1013
|
+
}
|
|
1014
|
+
getDebugDetailHtml() {
|
|
1015
|
+
return `<!DOCTYPE html>
|
|
1016
|
+
<html lang="zh-CN">
|
|
1017
|
+
<head>
|
|
1018
|
+
<meta charset="UTF-8">
|
|
1019
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
1020
|
+
<title>调试会话详情</title>
|
|
1021
|
+
<style>
|
|
1022
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
1023
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8fafc;color:#1e293b;padding:24px}
|
|
1024
|
+
h2{font-size:18px;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px}
|
|
1025
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
|
|
1026
|
+
.b-running{background:#fef3c7;color:#d97706}.b-completed{background:#dcfce7;color:#16a34a}.b-failed{background:#fee2e2;color:#dc2626}.b-pending{background:#f1f5f9;color:#64748b}.b-unknown{background:#f1f5f9;color:#64748b}
|
|
1027
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px}
|
|
1028
|
+
.section-title{font-size:13px;font-weight:600;color:#64748b;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
|
|
1029
|
+
.row{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
1030
|
+
.row:last-child{border-bottom:none}
|
|
1031
|
+
.label{width:100px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
1032
|
+
.value{color:#1e293b;font-size:13px;word-break:break-all;flex:1}
|
|
1033
|
+
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
1034
|
+
.log-viewer{max-height:500px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
|
|
1035
|
+
.log-line{padding:4px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.5}
|
|
1036
|
+
.log-time{color:#94a3b8;margin-right:8px;font-size:11px}
|
|
1037
|
+
.log-level-info{color:#0284c7}.log-level-warn{color:#d97706}.log-level-error{color:#dc2626}
|
|
1038
|
+
.log-content{color:#475569}
|
|
1039
|
+
.loading{text-align:center;padding:60px;color:#94a3b8}
|
|
1040
|
+
.error{color:#dc2626;padding:20px}
|
|
1041
|
+
</style>
|
|
1042
|
+
</head>
|
|
1043
|
+
<body>
|
|
1044
|
+
<h2>🐛 调试会话详情</h2>
|
|
1045
|
+
<div id="content"><div class="loading">加载中...</div></div>
|
|
1046
|
+
<script>
|
|
1047
|
+
const params=new URLSearchParams(location.search);
|
|
1048
|
+
const sessionId=params.get('id');
|
|
1049
|
+
if(!sessionId){document.getElementById('content').innerHTML='<div class="error">缺少会话ID</div>';}
|
|
951
1050
|
|
|
952
|
-
function
|
|
953
|
-
if(currentTab!=='debug'){switchTab('debug');setTimeout(()=>showDebugDetail(sessionId),500);return}
|
|
954
|
-
showDebugDetail(sessionId);
|
|
955
|
-
}
|
|
1051
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
956
1052
|
|
|
957
|
-
function
|
|
958
|
-
|
|
959
|
-
if(currentDetailTaskId&&(currentDetailTaskStatus==='running'||currentDetailTaskStatus==='pending')){
|
|
960
|
-
unsubscribeTaskLogs(currentDetailTaskId);
|
|
961
|
-
}
|
|
962
|
-
const panel=document.getElementById('task-detail');
|
|
963
|
-
if(panel)panel.innerHTML='';
|
|
964
|
-
detailOpen=false;
|
|
965
|
-
currentDetailTaskId=null;
|
|
966
|
-
currentDetailTaskStatus=null;
|
|
967
|
-
}
|
|
1053
|
+
function fmtTime(d){if(!d)return '-';return new Date(d).toLocaleString('zh-CN',{hour12:false})}
|
|
1054
|
+
function badge(s){const cls='b-'+(s||'unknown');return '<span class="badge '+cls+'">'+esc(s)+'</span>'}
|
|
968
1055
|
|
|
969
|
-
function
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
1056
|
+
async function load(){
|
|
1057
|
+
const session=await (await fetch('/api/debug/sessions/'+encodeURIComponent(sessionId))).json();
|
|
1058
|
+
const logs=await (await fetch('/api/debug/sessions/'+encodeURIComponent(sessionId)+'/logs')).json();
|
|
1059
|
+
const c=document.getElementById('content');
|
|
1060
|
+
if(!session){c.innerHTML='<div class="error">会话未找到</div>';return}
|
|
974
1061
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1062
|
+
document.title='调试会话详情: '+session.sessionId;
|
|
1063
|
+
let h='<div class="section"><div class="section-title">基本信息</div>';
|
|
1064
|
+
h+='<div class="row"><div class="label">会话ID</div><div class="value mono">'+esc(session.sessionId)+'</div></div>';
|
|
1065
|
+
h+='<div class="row"><div class="label">任务ID</div><div class="value mono">'+esc(session.taskId||'-')+'</div></div>';
|
|
1066
|
+
h+='<div class="row"><div class="label">设备</div><div class="value mono">'+esc(session.deviceId||'-')+'</div></div>';
|
|
1067
|
+
h+='<div class="row"><div class="label">平台</div><div class="value">'+esc(session.platform||'-')+'</div></div>';
|
|
1068
|
+
h+='<div class="row"><div class="label">状态</div><div class="value">'+badge(session.status)+'</div></div>';
|
|
1069
|
+
h+='<div class="row"><div class="label">开始时间</div><div class="value">'+fmtTime(session.startedAt)+'</div></div>';
|
|
1070
|
+
h+='<div class="row"><div class="label">完成时间</div><div class="value">'+fmtTime(session.completedAt)+'</div></div>';
|
|
1071
|
+
h+='</div>';
|
|
1072
|
+
|
|
1073
|
+
if(logs&&logs.length>0){
|
|
1074
|
+
h+='<div class="section"><div class="section-title">会话日志 ('+logs.length+')</div><div class="log-viewer">';
|
|
1075
|
+
logs.forEach(l=>{
|
|
1076
|
+
const lvl='log-level-'+(l.type==='error'?'error':'info');
|
|
1077
|
+
const content=l.content||'';
|
|
1078
|
+
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.type==='log_output'?'info':l.type==='error'?'error':'info').toUpperCase()+']</span> <span class="log-content">'+esc(content)+'</span></div>';
|
|
1079
|
+
});
|
|
1080
|
+
h+='</div></div>';
|
|
1081
|
+
}else{
|
|
1082
|
+
h+='<div class="section"><div class="section-title">会话日志</div><p style="color:#94a3b8">暂无日志</p></div>';
|
|
1083
|
+
}
|
|
1084
|
+
c.innerHTML=h;
|
|
1085
|
+
const lv=c.querySelector('.log-viewer');
|
|
1086
|
+
if(lv)lv.scrollTop=lv.scrollHeight;
|
|
1087
|
+
}
|
|
1088
|
+
load();
|
|
979
1089
|
</script>
|
|
980
1090
|
</body>
|
|
981
1091
|
</html>`;
|