@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.
@@ -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:#fff;color:#111;min-height:100vh}
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:#38bdf8}
480
- .tabs{display:flex;gap:4px;background:#fff;border-radius:8px;padding:4px;margin-bottom:24px}
481
- .tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#94a3b8;border:none;background:none;transition:all .2s}
482
- .tab.active{background:#38bdf8;color:#0f172a}
483
- .tab:hover:not(.active){color:#e2e8f0}
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 #334155}
486
- .card h3{font-size:13px;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
487
- .card .value{font-size:32px;font-weight:700;color:#38bdf8}
488
- .card .sub{font-size:12px;color:#64748b;margin-top:4px}
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 #1e293b;font-size:13px}
491
- th{color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#fff;position:sticky;top:0}
492
- tr:hover{background:#fff}
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:#fff;color:#22c55e}
495
- .b-offline{background:#fff;color:#ef4444}
496
- .b-running{background:#fff;color:#f59e0b}
497
- .b-completed{background:#fff;color:#22c55e}
498
- .b-failed{background:#fff;color:#ef4444}
499
- .b-pending{background:#fff;color:#38bdf8}
500
- .b-idle{background:#fff;color:#94a3b8}
501
- .b-stopped{background:#fff;color:#94a3b8}
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:#fff;color:#0f172a}
504
- .btn-primary:hover{background:#fff}
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:#e2e8f0;margin:20px 0 12px;display:flex;align-items:center;gap:8px}
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 #334155;border-radius:8px;padding:16px;max-height:600px;overflow-y:auto;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6}
509
- .log-viewer .log-line{padding:2px 0;border-bottom:1px solid #1e293b20}
510
- .log-viewer .log-line:hover{background:#fff}
511
- .log-time{color:#64748b;margin-right:8px}
512
- .log-level-info{color:#38bdf8}
513
- .log-level-warn{color:#f59e0b}
514
- .log-level-error{color:#ef4444}
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:#cbd5e1}
541
+ .log-content{color:#334155}
517
542
  .log-new{animation:flashLog 0.5s ease-out}
518
- @keyframes flashLog{0%{background:#fff}100%{background:transparent}}
519
- .log-live{color:#22c55e;font-size:10px;margin-left:8px;animation:pulse 2s infinite}
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 #334155;border-radius:8px;padding:20px;margin-top:16px}
522
- .detail-row{display:flex;padding:8px 0;border-bottom:1px solid #33415520}
523
- .detail-label{width:140px;color:#94a3b8;font-size:13px;flex-shrink:0}
524
- .detail-value{color:#e2e8f0;font-size:13px;word-break:break-all}
525
- .empty-state{text-align:center;padding:60px 20px;color:#64748b}
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:#64748b}
529
- .refresh-bar .auto.on{color:#22c55e}
530
- .search{background:#fff;border:1px solid #334155;border-radius:6px;padding:8px 14px;color:#e2e8f0;font-size:13px;width:220px}
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: load realtime logs for running tasks =====
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,'<').replace(/>/g,'>')}
728
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
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" onclick="showTaskLogs(\\''+esc(t.taskId)+'\\')">日志</button></td></tr>';
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" onclick="showDebugLogs(\\''+esc(s.sessionId)+'\\')">日志</button></td></tr>';
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 loadDevices(){
801
+ async function loadDevicesData(){
773
802
  const devs=await api('/devices');
774
- if(!devs)return;
775
- const p=document.getElementById('page-devices');
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
- h+='<div class="section-title">所有设备</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th><th>创建时间</th></tr>';
785
- devs.forEach(d=>{
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">&#128241;</div><p>暂无设备</p></div>';
791
828
  }
@@ -793,10 +830,15 @@ async function loadDevices(){
793
830
  }
794
831
 
795
832
  // ===== Tasks =====
796
- async function loadTasks(){
797
- const tasks=await api('/tasks?limit=50');
798
- if(!tasks)return;
799
- const p=document.getElementById('page-tasks');
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
- h+='<div class="section-title">所有任务</div><table><tr><th>任务ID</th><th>执行ID</th><th>类型</th><th>状态</th><th>优先级</th><th>开始时间</th><th>完成时间</th><th>错误</th><th>操作</th></tr>';
813
- tasks.forEach(t=>{
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" onclick="showTaskDetail(\\''+esc(t.taskId)+'\\')">详情</button></td></tr>';
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">&#128203;</div><p>暂无任务</p></div>';
820
864
  }
821
- h+='<div id="task-detail"></div>';
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 loadDebug(){
870
+ async function loadDebugData(){
894
871
  const sessions=await api('/debug/sessions');
895
- if(!sessions)return;
896
- const p=document.getElementById('page-debug');
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
- h+='<div class="section-title">所有调试会话</div><table><tr><th>会话ID</th><th>任务ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
906
- sessions.forEach(s=>{
907
- 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" onclick="showDebugDetail(\\''+esc(s.sessionId)+'\\')">详情</button></td></tr>';
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">&#128027;</div><p>暂无调试会话</p></div>';
912
896
  }
913
- h+='<div id="debug-detail"></div>';
897
+ const p=document.getElementById('page-debug');
914
898
  p.innerHTML=h;
915
899
  }
916
900
 
917
- async function showDebugDetail(sessionId){
918
- detailOpen=true;
919
- const session=await api('/debug/sessions/'+encodeURIComponent(sessionId));
920
- const logs=await api('/debug/sessions/'+encodeURIComponent(sessionId)+'/logs');
921
- const panel=document.getElementById('debug-detail');
922
- if(!session){panel.innerHTML='<p style="color:#ef4444">会话未找到</p>';detailOpen=false;return}
923
-
924
- let h='<div class="detail-panel"><div class="section-title">调试会话详情: '+esc(sessionId)+' <button class="btn btn-sm" style="background:#334155;color:#94a3b8;margin-left:12px" onclick="closeDebugDetail()">关闭</button></div>';
925
- h+='<div class="detail-row"><div class="detail-label">会话ID</div><div class="detail-value mono">'+esc(session.sessionId)+'</div></div>';
926
- h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(session.taskId||'-')+'</div></div>';
927
- h+='<div class="detail-row"><div class="detail-label">设备</div><div class="detail-value mono">'+esc(session.deviceId||'-')+'</div></div>';
928
- h+='<div class="detail-row"><div class="detail-label">平台</div><div class="detail-value">'+esc(session.platform||'-')+'</div></div>';
929
- h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(session.status)+'</div></div>';
930
- h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(session.startedAt)+'</div></div>';
931
- h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(session.completedAt)+'</div></div>';
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>&#128203; 任务详情</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
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
- // Session logs
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="detail-panel" style="margin-top:16px"><div class="section-title">会话日志 ('+logs.length+')</div>';
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.type==='log_output'?'info':l.type==='error'?'error':'info');
940
- const content=l.content||'';
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="detail-panel" style="margin-top:16px"><div class="section-title">会话日志</div><p style="color:#64748b">暂无日志</p></div>';
1003
+ h+='<div class="section"><div class="section-title">执行日志</div><p style="color:#94a3b8">暂无日志</p></div>';
946
1004
  }
947
-
948
- panel.innerHTML=h;
949
- panel.scrollIntoView({behavior:'smooth'});
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>&#128027; 调试会话详情</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 showDebugLogs(sessionId){
953
- if(currentTab!=='debug'){switchTab('debug');setTimeout(()=>showDebugDetail(sessionId),500);return}
954
- showDebugDetail(sessionId);
955
- }
1051
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
956
1052
 
957
- function closeTaskDetail(){
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 closeDebugDetail(){
970
- const panel=document.getElementById('debug-detail');
971
- if(panel)panel.innerHTML='';
972
- detailOpen=false;
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
- // ===== Init =====
976
- initWebSocket(); // 初始化 WebSocket 连接以接收实时日志
977
- refresh();
978
- refreshTimer=setInterval(()=>{if(autoRefresh&&!detailOpen)refresh()},5000);
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>`;