@aiscene/aiserver 1.2.4 → 1.2.6

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.
@@ -7,7 +7,205 @@ import { taskRepo } from '../storage/repositories/task-repo.js';
7
7
  import { debugLogRepo } from '../storage/repositories/debug-log-repo.js';
8
8
  import { executionLogRepo } from '../storage/repositories/execution-log-repo.js';
9
9
  import { getDebugPageHtml } from './debug-page.js';
10
+ import { getConfig } from '../config/index.js';
11
+ import { AutobotsClient } from '../core/autobots-client.js';
10
12
  const logger = createLogger('WebServer');
13
+ /**
14
+ * 构建脚本生成的上下文信息
15
+ */
16
+ function buildScriptContext(runMode, platform, url, deviceId, packageName) {
17
+ const context = [];
18
+ context.push('请根据用户的需求生成自动化测试脚本。');
19
+ context.push('');
20
+ if (runMode === 'browser') {
21
+ context.push('运行环境:浏览器模式');
22
+ if (url) {
23
+ context.push(`测试URL:${url}`);
24
+ }
25
+ }
26
+ else {
27
+ context.push('运行环境:真机模式');
28
+ context.push(`平台:${platform === 'ios' ? 'iOS' : 'Android'}`);
29
+ if (deviceId) {
30
+ context.push(`设备ID:${deviceId}`);
31
+ }
32
+ if (packageName) {
33
+ context.push(`应用包名:${packageName}`);
34
+ }
35
+ }
36
+ context.push('');
37
+ context.push('请生成符合以下要求的脚本:');
38
+ context.push('1. 使用 @midscene/web(浏览器模式)或 @midscene/android/@midscene/ios(真机模式)');
39
+ context.push('2. 脚本应该是可执行的JavaScript代码');
40
+ context.push('3. 使用aiAction方法执行自然语言指令');
41
+ context.push('4. 包含必要的初始化和清理代码');
42
+ return context.join('\n');
43
+ }
44
+ /**
45
+ * 清理生成的脚本,移除markdown代码块标记
46
+ */
47
+ function cleanGeneratedScript(script) {
48
+ // 移除 markdown 代码块标记
49
+ let cleaned = script;
50
+ // 移除 ```javascript, ```js, ``` 等代码块开始标记
51
+ cleaned = cleaned.replace(/```javascript\s*/gi, '');
52
+ cleaned = cleaned.replace(/```js\s*/gi, '');
53
+ cleaned = cleaned.replace(/```typescript\s*/gi, '');
54
+ cleaned = cleaned.replace(/```ts\s*/gi, '');
55
+ cleaned = cleaned.replace(/```\s*/gi, '');
56
+ // 移除可能的行内代码标记
57
+ cleaned = cleaned.replace(/`/g, '');
58
+ // 移除前后空白
59
+ cleaned = cleaned.trim();
60
+ return cleaned;
61
+ }
62
+ function generateScriptFromNaturalLanguage(options) {
63
+ const { naturalLanguage, runMode, platform, url, deviceId, packageName } = options;
64
+ // 解析自然语言,提取关键动作
65
+ const nl = naturalLanguage.toLowerCase();
66
+ const lines = [];
67
+ // 生成头部注释
68
+ lines.push('// ============================================');
69
+ lines.push('// AI自动生成的自动化测试脚本');
70
+ lines.push('// ============================================');
71
+ lines.push('// 生成时间: ' + new Date().toLocaleString('zh-CN'));
72
+ lines.push('// 运行模式: ' + (runMode === 'browser' ? '浏览器' : '真机'));
73
+ lines.push('// 平台: ' + (platform === 'android' ? 'Android' : 'iOS'));
74
+ if (url)
75
+ lines.push('// 测试URL: ' + url);
76
+ if (deviceId)
77
+ lines.push('// 设备ID: ' + deviceId);
78
+ if (packageName)
79
+ lines.push('// 包名: ' + packageName);
80
+ lines.push('// ============================================');
81
+ lines.push('');
82
+ if (runMode === 'browser') {
83
+ // 浏览器模式脚本
84
+ lines.push('// Midscene Web 自动化测试脚本');
85
+ lines.push("const { Midscene } = require('@midscene/web');");
86
+ lines.push('');
87
+ lines.push('async function main() {');
88
+ lines.push(' const midscene = new Midscene();');
89
+ lines.push('');
90
+ // 解析自然语言生成对应的Web操作
91
+ if (nl.includes('点击') || nl.includes('tap') || nl.includes('click')) {
92
+ const match = naturalLanguage.match(/点击[\s\S]*?['"]([^'"]+)['"][\s\S]*?/i) ||
93
+ naturalLanguage.match(/tap[\s\S]*?['"]([^'"]+)['"]/i) ||
94
+ naturalLanguage.match(/click[\s\S]*?['"]([^'"]+)['"]/i);
95
+ if (match) {
96
+ lines.push(` // 点击操作`);
97
+ lines.push(` await midscene.aiAction('点击 "${match[1]}"');`);
98
+ }
99
+ else {
100
+ lines.push(` // 点击操作`);
101
+ lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
102
+ }
103
+ }
104
+ else if (nl.includes('输入') || nl.includes('type') || nl.includes('fill')) {
105
+ const match = naturalLanguage.match(/输入['"]([^'"]+)['"][\s\S]*?/i) ||
106
+ naturalLanguage.match(/type[\s\S]*?['"]([^'"]+)['"]/i);
107
+ if (match) {
108
+ lines.push(` // 输入操作`);
109
+ lines.push(` await midscene.aiAction('输入 "${match[1]}"');`);
110
+ }
111
+ else {
112
+ lines.push(` // 输入操作`);
113
+ lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
114
+ }
115
+ }
116
+ else if (nl.includes('等待') || nl.includes('wait')) {
117
+ lines.push(` // 等待操作`);
118
+ lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
119
+ }
120
+ else if (nl.includes('滑动') || nl.includes('scroll') || nl.includes('滚动')) {
121
+ lines.push(` // 滚动操作`);
122
+ lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
123
+ }
124
+ else {
125
+ // 默认使用AI动作
126
+ lines.push(' // 自然语言指令:');
127
+ lines.push(' // ' + naturalLanguage);
128
+ lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
129
+ }
130
+ lines.push(' ');
131
+ lines.push(' // 可添加更多操作步骤...');
132
+ lines.push(' ');
133
+ lines.push(' await midscene.sleep(2000); // 等待2秒查看效果');
134
+ lines.push(' await midscene.quit();');
135
+ lines.push('}');
136
+ lines.push('');
137
+ lines.push('main().catch(console.error);');
138
+ }
139
+ else {
140
+ // 真机模式脚本
141
+ const isAndroid = platform === 'android';
142
+ const agentLib = isAndroid ? '@midscene/android' : '@midscene/ios';
143
+ const AgentClass = isAndroid ? 'AndroidAgent' : 'IOSAgent';
144
+ lines.push(`// Midscene ${isAndroid ? 'Android' : 'iOS'} 自动化测试脚本`);
145
+ lines.push(`const { ${AgentClass} } = require('${agentLib}');`);
146
+ lines.push('');
147
+ lines.push('async function main() {');
148
+ lines.push(` const agent = new ${AgentClass}();`);
149
+ if (deviceId)
150
+ lines.push(` await agent.setDevice('${deviceId}');`);
151
+ if (packageName)
152
+ lines.push(` await agent.setPackage('${packageName}');`);
153
+ lines.push('');
154
+ // 解析自然语言生成对应的移动端操作
155
+ if (nl.includes('点击') || nl.includes('tap') || nl.includes('click')) {
156
+ const match = naturalLanguage.match(/点击[\s\S]*?['"]([^'"]+)['"][\s\S]*?/i) ||
157
+ naturalLanguage.match(/tap[\s\S]*?['"]([^'"]+)['"]/i);
158
+ if (match) {
159
+ lines.push(` // 点击操作`);
160
+ lines.push(` await agent.aiAction('点击 "${match[1]}"');`);
161
+ }
162
+ else {
163
+ lines.push(` // 点击操作`);
164
+ lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
165
+ }
166
+ }
167
+ else if (nl.includes('输入') || nl.includes('type') || nl.includes('输入文本')) {
168
+ const match = naturalLanguage.match(/输入['"]([^'"]+)['"][\s\S]*?/i) ||
169
+ naturalLanguage.match(/type[\s\S]*?['"]([^'"]+)['"]/i) ||
170
+ naturalLanguage.match(/输入文本['"]([^'"]+)['"]/i);
171
+ if (match) {
172
+ lines.push(` // 输入操作`);
173
+ lines.push(` await agent.aiAction('输入 "${match[1]}"');`);
174
+ }
175
+ else {
176
+ lines.push(` // 输入操作`);
177
+ lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
178
+ }
179
+ }
180
+ else if (nl.includes('滑动') || nl.includes('swipe') || nl.includes('滚动')) {
181
+ lines.push(` // 滑动操作`);
182
+ lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
183
+ }
184
+ else if (nl.includes('启动') || nl.includes('start') || nl.includes('launch')) {
185
+ lines.push(` // 启动应用`);
186
+ lines.push(` await agent.aiAction('启动应用');`);
187
+ }
188
+ else if (nl.includes('截图') || nl.includes('screenshot')) {
189
+ lines.push(` // 截图`);
190
+ lines.push(` await agent.screenshot();`);
191
+ }
192
+ else {
193
+ // 默认使用AI动作
194
+ lines.push(' // 自然语言指令:');
195
+ lines.push(' // ' + naturalLanguage);
196
+ lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
197
+ }
198
+ lines.push(' ');
199
+ lines.push(' // 可添加更多操作步骤...');
200
+ lines.push(' ');
201
+ lines.push(' await agent.sleep(2000); // 等待2秒查看效果');
202
+ lines.push(' await agent.quit();');
203
+ lines.push('}');
204
+ lines.push('');
205
+ lines.push('main().catch(console.error);');
206
+ }
207
+ return lines.join('\n');
208
+ }
11
209
  export class WebServer {
12
210
  app;
13
211
  config;
@@ -15,6 +213,7 @@ export class WebServer {
15
213
  this.app = express();
16
214
  this.setupMiddleware();
17
215
  this.setupApiRoutes();
216
+ this.setupDetailRoutes();
18
217
  this.setupStaticFiles();
19
218
  }
20
219
  setupMiddleware() {
@@ -178,8 +377,103 @@ export class WebServer {
178
377
  res.json({ success: false, message: error.message });
179
378
  }
180
379
  });
380
+ // AI脚本生成API
381
+ api.post('/ai/generate-script', async (req, res) => {
382
+ try {
383
+ const { naturalLanguage, runMode, platform, url, deviceId, packageName } = req.body;
384
+ if (!naturalLanguage || naturalLanguage.trim() === '') {
385
+ res.json({ success: false, message: '请输入自然语言描述' });
386
+ return;
387
+ }
388
+ // 获取脚本生成配置
389
+ const config = getConfig();
390
+ const scriptGenConfig = config.scriptGeneration;
391
+ // 如果配置了真实的AI服务,使用AutobotsClient调用
392
+ if (scriptGenConfig && scriptGenConfig.enabled) {
393
+ logger.info('[generate-script] 使用Autobots API生成脚本, baseUrl: ' + scriptGenConfig.baseUrl);
394
+ const client = new AutobotsClient({
395
+ enabled: scriptGenConfig.enabled,
396
+ baseUrl: scriptGenConfig.baseUrl,
397
+ agentId: scriptGenConfig.agentId,
398
+ token: scriptGenConfig.token,
399
+ connectTimeout: scriptGenConfig.connectTimeout || 30000,
400
+ requestTimeout: scriptGenConfig.requestTimeout || 120000,
401
+ socketTimeout: scriptGenConfig.socketTimeout || 300000,
402
+ });
403
+ // 构建上下文信息
404
+ const contextInfo = buildScriptContext(runMode, platform, url, deviceId, packageName);
405
+ const prompt = contextInfo + '\n\n用户需求:' + naturalLanguage.trim();
406
+ // 调用Autobots API生成脚本(流式)
407
+ let generatedScript = '';
408
+ for await (const result of client.generateScriptStream(prompt)) {
409
+ if (result.success) {
410
+ // 处理特殊标记
411
+ if (result.content.startsWith('FULL_CONTENT:')) {
412
+ generatedScript = result.content.substring(13);
413
+ }
414
+ else {
415
+ generatedScript += result.content;
416
+ }
417
+ }
418
+ else {
419
+ logger.error('[generate-script] Autobots API错误: ' + result.error);
420
+ throw new Error(result.error);
421
+ }
422
+ }
423
+ // 清理生成的脚本(移除可能的markdown标记)
424
+ const cleanedScript = cleanGeneratedScript(generatedScript);
425
+ logger.info('[generate-script] 脚本生成成功,长度: ' + cleanedScript.length);
426
+ res.json({
427
+ success: true,
428
+ script: cleanedScript,
429
+ message: '脚本生成成功'
430
+ });
431
+ }
432
+ else {
433
+ // 没有配置AI服务,使用本地生成
434
+ logger.info('[generate-script] 未配置Autobots API,使用本地生成');
435
+ const script = generateScriptFromNaturalLanguage({
436
+ naturalLanguage: naturalLanguage.trim(),
437
+ runMode: runMode || 'browser',
438
+ platform: platform || 'android',
439
+ url: url || '',
440
+ deviceId: deviceId || '',
441
+ packageName: packageName || ''
442
+ });
443
+ res.json({
444
+ success: true,
445
+ script,
446
+ message: '本地模式生成成功'
447
+ });
448
+ }
449
+ }
450
+ catch (error) {
451
+ logger.warn(`AI脚本生成失败: ${error.message}`);
452
+ res.json({ success: false, message: error.message });
453
+ }
454
+ });
181
455
  this.app.use('/api', api);
182
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
+ }
183
477
  setupStaticFiles() {
184
478
  const distPath = path.resolve(import.meta.dirname, 'dist');
185
479
  this.app.use(express.static(distPath));
@@ -200,63 +494,72 @@ export class WebServer {
200
494
  <title>AIServer 管理面板</title>
201
495
  <style>
202
496
  *{margin:0;padding:0;box-sizing:border-box}
203
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#fff;color:#111;min-height:100vh}
204
- .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}
205
503
  .header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
206
- .header h1{font-size:24px;color:#38bdf8}
207
- .tabs{display:flex;gap:4px;background:#fff;border-radius:8px;padding:4px;margin-bottom:24px}
208
- .tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#94a3b8;border:none;background:none;transition:all .2s}
209
- .tab.active{background:#38bdf8;color:#0f172a}
210
- .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}
211
509
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
212
- .card{background:#fff;border-radius:8px;padding:20px;border:1px solid #334155}
213
- .card h3{font-size:13px;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
214
- .card .value{font-size:32px;font-weight:700;color:#38bdf8}
215
- .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}
216
514
  table{width:100%;border-collapse:collapse;margin-top:8px}
217
- th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #1e293b;font-size:13px}
218
- th{color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#fff;position:sticky;top:0}
219
- 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}
220
518
  .badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}
221
- .b-online{background:#fff;color:#22c55e}
222
- .b-offline{background:#fff;color:#ef4444}
223
- .b-running{background:#fff;color:#f59e0b}
224
- .b-completed{background:#fff;color:#22c55e}
225
- .b-failed{background:#fff;color:#ef4444}
226
- .b-pending{background:#fff;color:#38bdf8}
227
- .b-idle{background:#fff;color:#94a3b8}
228
- .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}
229
527
  .btn{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s}
230
- .btn-primary{background:#fff;color:#0f172a}
231
- .btn-primary:hover{background:#fff}
528
+ .btn-primary{background:#2563eb;color:#fff}
529
+ .btn-primary:hover{background:#1d4ed8}
232
530
  .btn-sm{padding:4px 10px;font-size:11px}
233
- .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}
234
532
  .mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
235
- .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}
236
- .log-viewer .log-line{padding:2px 0;border-bottom:1px solid #1e293b20}
237
- .log-viewer .log-line:hover{background:#fff}
238
- .log-time{color:#64748b;margin-right:8px}
239
- .log-level-info{color:#38bdf8}
240
- .log-level-warn{color:#f59e0b}
241
- .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}
242
540
  .log-level-debug{color:#94a3b8}
243
- .log-content{color:#cbd5e1}
541
+ .log-content{color:#334155}
244
542
  .log-new{animation:flashLog 0.5s ease-out}
245
- @keyframes flashLog{0%{background:#fff}100%{background:transparent}}
246
- .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}
247
545
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
248
- .detail-panel{background:#fff;border:1px solid #334155;border-radius:8px;padding:20px;margin-top:16px}
249
- .detail-row{display:flex;padding:8px 0;border-bottom:1px solid #33415520}
250
- .detail-label{width:140px;color:#94a3b8;font-size:13px;flex-shrink:0}
251
- .detail-value{color:#e2e8f0;font-size:13px;word-break:break-all}
252
- .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}
253
551
  .empty-state .icon{font-size:48px;margin-bottom:16px}
254
552
  .refresh-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px}
255
- .refresh-bar .auto{font-size:12px;color:#64748b}
256
- .refresh-bar .auto.on{color:#22c55e}
257
- .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}
258
556
  .search:focus{outline:none;border-color:#38bdf8}
259
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}
260
563
  </style>
261
564
  </head>
262
565
  <body>
@@ -285,7 +588,6 @@ tr:hover{background:#fff}
285
588
  let currentTab='dashboard';
286
589
  let autoRefresh=true;
287
590
  let refreshTimer=null;
288
- let detailOpen=false; // track if a detail panel is open
289
591
 
290
592
  // ===== WebSocket for Real-time Task Logs =====
291
593
  let ws=null;
@@ -398,13 +700,9 @@ function updateTaskLogUI(taskId){
398
700
  logViewer.scrollTop=logViewer.scrollHeight;
399
701
  }
400
702
 
401
- // ===== Tab: load realtime logs for running tasks =====
402
- let currentDetailTaskId=null;
403
- let currentDetailTaskStatus=null;
404
-
703
+ // ===== Tab switching =====
405
704
  function switchTab(tab){
406
705
  currentTab=tab;
407
- detailOpen=false;
408
706
  document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
409
707
  document.querySelectorAll('.tab').forEach(t=>{
410
708
  var txt=t.textContent.toLowerCase();
@@ -416,6 +714,9 @@ function switchTab(tab){
416
714
  });
417
715
  document.querySelectorAll('[id^="page-"]').forEach(p=>p.classList.add('collapsed'));
418
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')}
419
720
  if(tab!=='debugger') refresh();
420
721
  }
421
722
 
@@ -424,7 +725,9 @@ function badge(s){
424
725
  return '<span class="badge '+cls+'">'+s+'</span>';
425
726
  }
426
727
 
427
- 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')}
428
731
 
429
732
  function timeAgo(d){
430
733
  if(!d)return '-';
@@ -443,9 +746,9 @@ async function api(path){
443
746
 
444
747
  async function refresh(){
445
748
  if(currentTab==='dashboard')await loadDashboard();
446
- else if(currentTab==='devices')await loadDevices();
447
- else if(currentTab==='tasks')await loadTasks();
448
- 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();}
449
752
  else if(currentTab==='debugger')loadDebugger();
450
753
  }
451
754
 
@@ -466,41 +769,46 @@ async function loadDashboard(){
466
769
 
467
770
  // Devices overview
468
771
  if(d.devices.list&&d.devices.list.length>0){
469
- h+='<div class="section-title">设备列表</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
470
- 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=>{
471
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>';
472
775
  });
473
- h+='</table>';
776
+ h+='</table></div>';
474
777
  }
475
778
 
476
779
  // Recent tasks
477
780
  if(d.tasks.recent&&d.tasks.recent.length>0){
478
- h+='<div class="section-title">近期任务</div><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
479
- d.tasks.recent.forEach(t=>{
480
- 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>';
481
784
  });
482
- h+='</table>';
785
+ h+='</table></div>';
483
786
  }
484
787
 
485
788
  // Recent debug sessions
486
789
  if(d.debugSessions&&d.debugSessions.length>0){
487
- 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>';
488
791
  d.debugSessions.slice(0,10).forEach(s=>{
489
- 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>';
490
793
  });
491
- h+='</table>';
794
+ h+='</table></div>';
492
795
  }
493
796
 
494
- h+='<div id="detail-panel"></div>';
495
797
  p.innerHTML=h;
496
798
  }
497
799
 
498
800
  // ===== Devices =====
499
- async function loadDevices(){
801
+ async function loadDevicesData(){
500
802
  const devs=await api('/devices');
501
- if(!devs)return;
502
- 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;
503
810
  const online=devs.filter(d=>d.status==='online').length;
811
+ const p=document.getElementById('page-devices');
504
812
  let h='<div class="grid">';
505
813
  h+='<div class="card"><h3>设备总数</h3><div class="value">'+devs.length+'</div></div>';
506
814
  h+='<div class="card"><h3>在线</h3><div class="value">'+online+'</div></div>';
@@ -508,11 +816,13 @@ async function loadDevices(){
508
816
  h+='</div>';
509
817
 
510
818
  if(devs.length>0){
511
- h+='<div class="section-title">所有设备</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th><th>创建时间</th></tr>';
512
- 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=>{
513
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>';
514
824
  });
515
- h+='</table>';
825
+ h+='</table></div>';
516
826
  }else{
517
827
  h+='<div class="empty-state"><div class="icon">&#128241;</div><p>暂无设备</p></div>';
518
828
  }
@@ -520,10 +830,15 @@ async function loadDevices(){
520
830
  }
521
831
 
522
832
  // ===== Tasks =====
523
- async function loadTasks(){
524
- const tasks=await api('/tasks?limit=50');
525
- if(!tasks)return;
526
- 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;
527
842
  const running=tasks.filter(t=>t.status==='running').length;
528
843
  const completed=tasks.filter(t=>t.status==='completed').length;
529
844
  const failed=tasks.filter(t=>t.status==='failed').length;
@@ -536,91 +851,31 @@ async function loadTasks(){
536
851
  h+='</div>';
537
852
 
538
853
  if(tasks.length>0){
539
- 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>';
540
- 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=>{
541
858
  const err=t.errorMessage?(t.errorMessage.length>50?esc(t.errorMessage.substring(0,50))+'...':esc(t.errorMessage)):'-';
542
- 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>';
543
860
  });
544
- h+='</table>';
861
+ h+='</table></div>';
545
862
  }else{
546
863
  h+='<div class="empty-state"><div class="icon">&#128203;</div><p>暂无任务</p></div>';
547
864
  }
548
- h+='<div id="task-detail"></div>';
865
+ const p=document.getElementById('page-tasks');
549
866
  p.innerHTML=h;
550
867
  }
551
868
 
552
- async function showTaskDetail(taskId){
553
- detailOpen=true;
554
- currentDetailTaskId=taskId;
555
- currentDetailTaskStatus=null;
556
- const task=await api('/tasks/'+encodeURIComponent(taskId));
557
- const logs=await api('/tasks/'+encodeURIComponent(taskId)+'/logs');
558
- const panel=document.getElementById('task-detail');
559
- if(!task){panel.innerHTML='<p style="color:#ef4444">任务未找到</p>';detailOpen=false;currentDetailTaskId=null;return}
560
-
561
- currentDetailTaskStatus=task.status;
562
- const isRunning=task.status==='running'||task.status==='pending';
563
-
564
- 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>';
565
- h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(task.taskId)+'</div></div>';
566
- h+='<div class="detail-row"><div class="detail-label">执行ID</div><div class="detail-value mono">'+esc(task.executionId||'-')+'</div></div>';
567
- h+='<div class="detail-row"><div class="detail-label">类型</div><div class="detail-value">'+esc(task.type)+'</div></div>';
568
- h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(task.status)+'</div></div>';
569
- h+='<div class="detail-row"><div class="detail-label">优先级</div><div class="detail-value">'+task.priority+'</div></div>';
570
- h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(task.startedAt)+'</div></div>';
571
- h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(task.completedAt)+'</div></div>';
572
- h+='<div class="detail-row"><div class="detail-label">创建时间</div><div class="detail-value">'+fmtTime(task.createdAt)+'</div></div>';
573
- 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>';
574
- if(task.config){
575
- 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>';
576
- }
577
- if(task.result){
578
- 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>';
579
- }
580
-
581
- // Execution logs with real-time support
582
- const logSafeId=esc(taskId).replace(/[^a-zA-Z0-9-_]/g,'_');
583
- const existingLogsCount=logs?logs.length:0;
584
- if(existingLogsCount>0||isRunning){
585
- h+='<div class="section-title" style="margin-top:20px">执行日志 <span id="task-log-count-'+logSafeId+'" style="color:#64748b;font-size:12px">'+(existingLogsCount||'')+'</span>';
586
- if(isRunning)h+=' <span style="color:#22c55e;font-size:12px">● LIVE</span>';
587
- h+='</div>';
588
- h+='<div class="log-viewer" id="task-log-'+logSafeId+'" style="max-height:400px">';
589
- if(logs&&logs.length>0){
590
- logs.forEach(l=>{
591
- const lvl='log-level-'+esc(l.level||'info');
592
- 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>';
593
- });
594
- }
595
- h+='</div>';
596
- }else{
597
- h+='<div class="section-title" style="margin-top:20px">执行日志</div><p style="color:#64748b">暂无日志</p>';
598
- }
599
-
600
- h+='</div>';
601
- panel.innerHTML=h;
602
- panel.scrollIntoView({behavior:'smooth'});
603
-
604
- // 滚动到日志底部
605
- const logViewer=document.getElementById('task-log-'+logSafeId);
606
- if(logViewer)logViewer.scrollTop=logViewer.scrollHeight;
607
-
608
- // 订阅实时日志
609
- if(isRunning){
610
- subscribeTaskLogs(taskId);
611
- }
612
- }
613
-
614
- function showTaskLogs(taskId){
615
- if(currentTab!=='tasks'){switchTab('tasks');setTimeout(()=>showTaskDetail(taskId),500);return}
616
- showTaskDetail(taskId);
617
- }
618
-
619
869
  // ===== Debug Sessions =====
620
- async function loadDebug(){
870
+ async function loadDebugData(){
621
871
  const sessions=await api('/debug/sessions');
622
- if(!sessions)return;
623
- 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;
624
879
  const running=sessions.filter(s=>s.status==='running').length;
625
880
 
626
881
  let h='<div class="grid">';
@@ -629,80 +884,208 @@ async function loadDebug(){
629
884
  h+='</div>';
630
885
 
631
886
  if(sessions.length>0){
632
- 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>';
633
- sessions.forEach(s=>{
634
- 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>';
635
892
  });
636
- h+='</table>';
893
+ h+='</table></div>';
637
894
  }else{
638
895
  h+='<div class="empty-state"><div class="icon">&#128027;</div><p>暂无调试会话</p></div>';
639
896
  }
640
- h+='<div id="debug-detail"></div>';
897
+ const p=document.getElementById('page-debug');
641
898
  p.innerHTML=h;
642
899
  }
643
900
 
644
- async function showDebugDetail(sessionId){
645
- detailOpen=true;
646
- const session=await api('/debug/sessions/'+encodeURIComponent(sessionId));
647
- const logs=await api('/debug/sessions/'+encodeURIComponent(sessionId)+'/logs');
648
- const panel=document.getElementById('debug-detail');
649
- if(!session){panel.innerHTML='<p style="color:#ef4444">会话未找到</p>';detailOpen=false;return}
650
-
651
- 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>';
652
- h+='<div class="detail-row"><div class="detail-label">会话ID</div><div class="detail-value mono">'+esc(session.sessionId)+'</div></div>';
653
- h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(session.taskId||'-')+'</div></div>';
654
- h+='<div class="detail-row"><div class="detail-label">设备</div><div class="detail-value mono">'+esc(session.deviceId||'-')+'</div></div>';
655
- h+='<div class="detail-row"><div class="detail-label">平台</div><div class="detail-value">'+esc(session.platform||'-')+'</div></div>';
656
- h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(session.status)+'</div></div>';
657
- h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(session.startedAt)+'</div></div>';
658
- 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>';
659
986
  h+='</div>';
660
987
 
661
- // 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
+
662
995
  if(logs&&logs.length>0){
663
- h+='<div class="detail-panel" style="margin-top:16px"><div class="section-title">会话日志 ('+logs.length+')</div>';
664
- h+='<div class="log-viewer">';
996
+ h+='<div class="section"><div class="section-title">执行日志 ('+logs.length+')</div><div class="log-viewer">';
665
997
  logs.forEach(l=>{
666
- const lvl='log-level-'+esc(l.type==='log_output'?'info':l.type==='error'?'error':'info');
667
- const content=l.content||'';
668
- 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>';
669
1000
  });
670
1001
  h+='</div></div>';
671
1002
  }else{
672
- 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>';
673
1004
  }
674
-
675
- panel.innerHTML=h;
676
- panel.scrollIntoView({behavior:'smooth'});
1005
+ c.innerHTML=h;
1006
+ const lv=c.querySelector('.log-viewer');
1007
+ if(lv)lv.scrollTop=lv.scrollHeight;
677
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>';}
678
1050
 
679
- function showDebugLogs(sessionId){
680
- if(currentTab!=='debug'){switchTab('debug');setTimeout(()=>showDebugDetail(sessionId),500);return}
681
- showDebugDetail(sessionId);
682
- }
1051
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
683
1052
 
684
- function closeTaskDetail(){
685
- // 取消订阅实时日志
686
- if(currentDetailTaskId&&(currentDetailTaskStatus==='running'||currentDetailTaskStatus==='pending')){
687
- unsubscribeTaskLogs(currentDetailTaskId);
688
- }
689
- const panel=document.getElementById('task-detail');
690
- if(panel)panel.innerHTML='';
691
- detailOpen=false;
692
- currentDetailTaskId=null;
693
- currentDetailTaskStatus=null;
694
- }
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>'}
695
1055
 
696
- function closeDebugDetail(){
697
- const panel=document.getElementById('debug-detail');
698
- if(panel)panel.innerHTML='';
699
- detailOpen=false;
700
- }
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}
701
1061
 
702
- // ===== Init =====
703
- initWebSocket(); // 初始化 WebSocket 连接以接收实时日志
704
- refresh();
705
- 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();
706
1089
  </script>
707
1090
  </body>
708
1091
  </html>`;