@aiscene/aiserver 1.2.3 → 1.2.5

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.
@@ -0,0 +1,1069 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>AI测试平台 - 调试页面</title>
7
+ <style>
8
+ *{margin:0;padding:0;box-sizing:border-box}
9
+ :root {
10
+ --bg-primary: #0f172a;
11
+ --bg-secondary: #1e293b;
12
+ --bg-tertiary: #334155;
13
+ --text-primary: #e2e8f0;
14
+ --text-secondary: #94a3b8;
15
+ --text-muted: #64748b;
16
+ --accent: #38bdf8;
17
+ --accent-hover: #7dd3fc;
18
+ --success: #22c55e;
19
+ --warning: #f59e0b;
20
+ --error: #ef4444;
21
+ --border: #334155;
22
+ }
23
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg-primary);color:var(--text-primary);min-height:100vh;overflow:hidden}
24
+ .container{display:flex;height:100vh}
25
+ /* 左侧配置面板 */
26
+ .sidebar{width:340px;min-width:300px;background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
27
+ .sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--border);background:var(--bg-tertiary)}
28
+ .sidebar-header h2{font-size:16px;font-weight:600;color:var(--accent)}
29
+ .sidebar-content{flex:1;overflow-y:auto;padding:16px}
30
+ .config-section{margin-bottom:20px}
31
+ .config-section label{display:block;font-size:13px;color:var(--text-secondary);margin-bottom:6px;font-weight:500}
32
+ .config-section select,.config-section input{width:100%;padding:10px 12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:14px}
33
+ .config-section select:focus,.config-section input:focus{outline:none;border-color:var(--accent)}
34
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 16px;border-radius:6px;border:none;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s}
35
+ .btn-primary{background:var(--accent);color:var(--bg-primary)}
36
+ .btn-primary:hover{background:var(--accent-hover)}
37
+ .btn-danger{background:var(--error);color:white}
38
+ .btn-danger:hover{background:#dc2626}
39
+ .btn-secondary{background:var(--bg-tertiary);color:var(--text-primary)}
40
+ .btn-secondary:hover{background:var(--border)}
41
+ .btn-sm{padding:6px 12px;font-size:12px}
42
+ .btn-block{width:100%}
43
+ .checkbox-wrapper{display:flex;align-items:center;gap:8px;margin:8px 0}
44
+ .checkbox-wrapper input[type="checkbox"]{width:16px;height:16px;accent-color:var(--accent)}
45
+ /* 主内容区 */
46
+ .main{flex:1;display:flex;flex-direction:column;overflow:hidden}
47
+ /* 脚本编辑器区域 */
48
+ .editor-section{flex:1;min-height:0;display:flex;flex-direction:column;border-bottom:1px solid var(--border)}
49
+ .editor-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:var(--bg-secondary);border-bottom:1px solid var(--border)}
50
+ .editor-title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:500}
51
+ .editor-actions{display:flex;gap:8px}
52
+ .editor-content{flex:1;overflow:hidden;padding:0}
53
+ .code-editor{width:100%;height:100%;background:var(--bg-primary);color:var(--text-primary);font-family:'SF Mono',Monaco,Consolas,monospace;font-size:13px;line-height:1.6;padding:16px;border:none;resize:none}
54
+ .code-editor:focus{outline:none}
55
+ /* 终端区域 */
56
+ .terminal-section{height:300px;min-height:200px;background:#0c0f1a;border-top:1px solid var(--border);display:flex;flex-direction:column}
57
+ .terminal-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:#1e293b;border-bottom:1px solid var(--border)}
58
+ .terminal-title{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-secondary)}
59
+ .terminal-content{flex:1;overflow-y:auto;padding:12px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6}
60
+ .log-line{padding:2px 0;border-bottom:1px solid #1e293b20}
61
+ .log-time{color:var(--text-muted);margin-right:8px}
62
+ .log-level{font-weight:600}
63
+ .log-level-info{color:var(--accent)}
64
+ .log-level-warn{color:var(--warning)}
65
+ .log-level-error{color:var(--error)}
66
+ .log-level-success{color:var(--success)}
67
+ .log-content{color:var(--text-primary)}
68
+ /* 状态栏 */
69
+ .statusbar{height:28px;background:#1e293b;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-size:12px;color:var(--text-secondary)}
70
+ /* 模态框 */
71
+ .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;visibility:hidden;transition:all .2s}
72
+ .modal-overlay.active{opacity:1;visibility:visible}
73
+ .modal{background:var(--bg-secondary);border-radius:12px;border:1px solid var(--border);width:90%;max-width:900px;max-height:80vh;display:flex;flex-direction:column;overflow:hidden}
74
+ .modal-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
75
+ .modal-header h3{font-size:16px;font-weight:600}
76
+ .modal-body{flex:1;overflow-y:auto;padding:20px}
77
+ .modal-footer{padding:16px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
78
+ /* 用例选择器 */
79
+ .testcase-selector{display:flex;height:100%}
80
+ .folder-tree{width:35%;border-right:1px solid var(--border);overflow-y:auto;padding:12px}
81
+ .folder-item{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:6px;cursor:pointer;margin-bottom:4px;font-size:13px}
82
+ .folder-item:hover{background:var(--bg-tertiary)}
83
+ .folder-item.active{background:var(--accent);color:var(--bg-primary)}
84
+ .case-list{flex:1;overflow-y:auto;padding:12px}
85
+ .case-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:6px;border:1px solid var(--border);margin-bottom:8px;cursor:pointer;transition:all .2s}
86
+ .case-item:hover{border-color:var(--accent)}
87
+ .case-item.selected{border-color:var(--accent);background:rgba(56,189,248,0.1)}
88
+ .case-info{flex:1}
89
+ .case-name{font-size:14px;font-weight:500;margin-bottom:4px}
90
+ .case-desc{font-size:12px;color:var(--text-secondary)}
91
+ /* 加载状态 */
92
+ .loading{display:flex;align-items:center;justify-content:center;padding:40px;color:var(--text-secondary)}
93
+ .loading::before{content:'';width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite;margin-right:12px}
94
+ @keyframes spin{to{transform:rotate(360deg)}}
95
+ /* 空状态 */
96
+ .empty-state{text-align:center;padding:60px 20px;color:var(--text-secondary)}
97
+ .empty-state .icon{font-size:48px;margin-bottom:16px;opacity:0.5}
98
+ /* 历史记录 */
99
+ .history-list{max-height:400px;overflow-y:auto}
100
+ .history-item{padding:12px;border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:all .2s}
101
+ .history-item:hover{border-color:var(--accent)}
102
+ .history-header{display:flex;justify-content:space-between;margin-bottom:4px}
103
+ .history-status{padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600}
104
+ .history-status.success{background:rgba(34,197,94,0.2);color:var(--success)}
105
+ .history-status.failed{background:rgba(239,68,68,0.2);color:var(--error)}
106
+ .history-url{font-size:13px;font-weight:500;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
107
+ .history-info{font-size:11px;color:var(--text-secondary)}
108
+ /* Toast 通知 */
109
+ .toast{position:fixed;bottom:60px;right:20px;padding:12px 20px;border-radius:8px;font-size:13px;z-index:2000;transform:translateX(120%);transition:transform .3s}
110
+ .toast.show{transform:translateX(0)}
111
+ .toast.success{background:var(--success);color:white}
112
+ .toast.error{background:var(--error);color:white}
113
+ .toast.info{background:var(--accent);color:var(--bg-primary)}
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <div class="container">
118
+ <!-- 左侧配置面板 -->
119
+ <div class="sidebar">
120
+ <div class="sidebar-header">
121
+ <h2>调试面板</h2>
122
+ <button class="btn btn-sm btn-secondary" onclick="toggleHistory()" title="历史记录">📋</button>
123
+ </div>
124
+ <div class="sidebar-content">
125
+ <!-- 运行模式 -->
126
+ <div class="config-section">
127
+ <label>运行模式</label>
128
+ <select id="runMode" onchange="onRunModeChange()">
129
+ <option value="browser">🌐 浏览器</option>
130
+ <option value="device">📱 真机</option>
131
+ </select>
132
+ </div>
133
+
134
+ <!-- 真机配置 -->
135
+ <div id="deviceConfig" style="display:none">
136
+ <div class="config-section">
137
+ <label>平台</label>
138
+ <select id="platform">
139
+ <option value="android">Android</option>
140
+ <option value="ios">iOS</option>
141
+ </select>
142
+ </div>
143
+ <div class="config-section">
144
+ <div style="display:flex;justify-content:space-between;align-items:center">
145
+ <label style="margin:0">设备</label>
146
+ <button class="btn btn-sm btn-secondary" onclick="refreshDevices()" title="刷新设备">🔄</button>
147
+ </div>
148
+ <select id="deviceSelect">
149
+ <option value="">选择设备...</option>
150
+ </select>
151
+ </div>
152
+ <div class="config-section">
153
+ <label>包名 / Bundle ID</label>
154
+ <select id="packageSelect" onchange="onPackageChange()">
155
+ <option value="com.jingdong.app.mall">京东</option>
156
+ <option value="com.jd.dh">京东医生</option>
157
+ <option value="com.jd.jdhealth">京东健康</option>
158
+ <option value="custom">自定义...</option>
159
+ </select>
160
+ <input type="text" id="customPackage" placeholder="输入包名" style="margin-top:8px;display:none">
161
+ </div>
162
+ </div>
163
+
164
+ <!-- 浏览器配置 -->
165
+ <div id="browserConfig">
166
+ <div class="config-section">
167
+ <label>测试地址</label>
168
+ <input type="text" id="testUrl" placeholder="输入测试URL">
169
+ </div>
170
+ <div class="checkbox-wrapper">
171
+ <input type="checkbox" id="mobileMode">
172
+ <label for="mobileMode" style="margin:0;font-size:13px">模拟移动端</label>
173
+ </div>
174
+ </div>
175
+
176
+ <!-- 登录信息 -->
177
+ <div class="config-section">
178
+ <label>登录用户名(选填)</label>
179
+ <input type="text" id="loginUsername" placeholder="请输入用户名">
180
+ </div>
181
+ <div class="config-section">
182
+ <label>登录密码(选填)</label>
183
+ <input type="password" id="loginPassword" placeholder="请输入密码">
184
+ </div>
185
+
186
+ <!-- Appium 选项 -->
187
+ <div class="checkbox-wrapper">
188
+ <input type="checkbox" id="skipAppiumDriver" checked>
189
+ <label for="skipAppiumDriver" style="margin:0;font-size:13px">跳过 Appium 服务</label>
190
+ </div>
191
+
192
+ <!-- 执行按钮 -->
193
+ <div style="margin-top:24px">
194
+ <button id="executeBtn" class="btn btn-primary btn-block" onclick="executeTask()">
195
+ ▶ 执行任务
196
+ </button>
197
+ <button id="stopBtn" class="btn btn-danger btn-block" onclick="stopTask()" style="display:none;margin-top:8px">
198
+ ⏹ 停止任务
199
+ </button>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- 主内容区 -->
205
+ <div class="main">
206
+ <!-- 脚本编辑器 -->
207
+ <div class="editor-section">
208
+ <div class="editor-header">
209
+ <div class="editor-title">
210
+ <span>📝</span>
211
+ <span>脚本编辑器</span>
212
+ </div>
213
+ <div class="editor-actions">
214
+ <button class="btn btn-sm btn-secondary" onclick="selectTestCase()">📂 选择用例</button>
215
+ <button class="btn btn-sm btn-secondary" onclick="saveTestCase()">💾 保存用例</button>
216
+ <button class="btn btn-sm btn-secondary" onclick="clearEditor()">🗑 清空</button>
217
+ </div>
218
+ </div>
219
+ <div class="editor-content">
220
+ <textarea id="scriptEditor" class="code-editor" placeholder="// 在此编写或生成测试脚本...
221
+ // 支持的操作:
222
+ // - aiTap('点击按钮') - AI点击
223
+ // - aiInput('搜索内容') - AI输入
224
+ // - aiAssert('验证文本') - AI断言
225
+ // - aiQuery('查询信息') - AI查询
226
+ // - aiScroll({}) - AI滚动
227
+ // - aiWaitFor('等待条件') - AI等待"></textarea>
228
+ </div>
229
+ </div>
230
+
231
+ <!-- 终端输出 -->
232
+ <div class="terminal-section">
233
+ <div class="terminal-header">
234
+ <div class="terminal-title">
235
+ <span>🖥</span>
236
+ <span>终端输出</span>
237
+ </div>
238
+ <div class="editor-actions">
239
+ <button id="reportBtn" class="btn btn-sm btn-secondary" onclick="openReport()" style="display:none">📊 查看报告</button>
240
+ <button class="btn btn-sm btn-secondary" onclick="clearLogs()">🗑 清空</button>
241
+ </div>
242
+ </div>
243
+ <div id="terminalContent" class="terminal-content">
244
+ <div class="empty-state">
245
+ <div class="icon">📟</div>
246
+ <p>暂无输出...</p>
247
+ <p style="font-size:11px;margin-top:8px">执行任务后,输出将显示在这里</p>
248
+ </div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- 状态栏 -->
253
+ <div class="statusbar">
254
+ <div id="statusInfo">
255
+ <span id="connectionStatus">⚡ 已连接</span>
256
+ <span id="taskStatus" style="margin-left:16px"></span>
257
+ </div>
258
+ <div>
259
+ <button class="btn btn-sm btn-secondary" onclick="toggleTerminal()">
260
+ <span id="terminalToggle">▼</span> 终端
261
+ </button>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- 用例选择器模态框 -->
268
+ <div id="testCaseModal" class="modal-overlay">
269
+ <div class="modal">
270
+ <div class="modal-header">
271
+ <h3>选择测试用例</h3>
272
+ <button class="btn btn-sm btn-secondary" onclick="closeTestCaseModal()">✕</button>
273
+ </div>
274
+ <div class="modal-body" style="padding:0">
275
+ <div class="testcase-selector">
276
+ <div class="folder-tree" id="folderTree">
277
+ <div class="loading">加载中...</div>
278
+ </div>
279
+ <div class="case-list" id="caseList">
280
+ <div class="empty-state">请选择文件夹</div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ <div class="modal-footer">
285
+ <button class="btn btn-secondary" onclick="closeTestCaseModal()">取消</button>
286
+ <button class="btn btn-primary" onclick="confirmTestCase()">确定</button>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- 保存用例模态框 -->
292
+ <div id="saveModal" class="modal-overlay">
293
+ <div class="modal">
294
+ <div class="modal-header">
295
+ <h3>保存测试用例</h3>
296
+ <button class="btn btn-sm btn-secondary" onclick="closeSaveModal()">✕</button>
297
+ </div>
298
+ <div class="modal-body">
299
+ <div class="config-section">
300
+ <label>用例名称</label>
301
+ <input type="text" id="caseName" placeholder="例如:登录测试">
302
+ </div>
303
+ <div class="config-section">
304
+ <label>所属文件夹</label>
305
+ <select id="folderSelect">
306
+ <option value="">选择文件夹...</option>
307
+ </select>
308
+ </div>
309
+ </div>
310
+ <div class="modal-footer">
311
+ <button class="btn btn-secondary" onclick="closeSaveModal()">取消</button>
312
+ <button class="btn btn-primary" onclick="confirmSave()">保存</button>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <!-- 历史记录模态框 -->
318
+ <div id="historyModal" class="modal-overlay">
319
+ <div class="modal">
320
+ <div class="modal-header">
321
+ <h3>历史记录</h3>
322
+ <button class="btn btn-sm btn-secondary" onclick="closeHistoryModal()">✕</button>
323
+ </div>
324
+ <div class="modal-body">
325
+ <div id="historyList" class="history-list">
326
+ <div class="empty-state">暂无历史记录</div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- Toast 通知 -->
333
+ <div id="toast" class="toast"></div>
334
+
335
+ <script>
336
+ // ===== 全局变量 =====
337
+ // @ts-ignore
338
+ let ws = null;
339
+ // @ts-ignore
340
+ let currentTaskId = null;
341
+ // @ts-ignore
342
+ let isExecuting = false;
343
+ // @ts-ignore
344
+ let terminalMinimized = false;
345
+ // @ts-ignore
346
+ let reportPath = '';
347
+ // @ts-ignore
348
+ let history = [];
349
+ // @ts-ignore
350
+ let folders = [];
351
+ // @ts-ignore
352
+ let testCases = [];
353
+ // @ts-ignore
354
+ let selectedTestCase = null;
355
+ // @ts-ignore
356
+ let selectedFolderId = null;
357
+ // @ts-ignore
358
+ let pollingInterval = null;
359
+
360
+ // ===== WebSocket 连接 =====
361
+ // @ts-ignore
362
+ function initWebSocket() {
363
+ var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
364
+ var wsHost = location.hostname;
365
+ var wsPort = 8002;
366
+
367
+ // @ts-ignore
368
+ ws = new WebSocket(wsProtocol + '//' + wsHost + ':' + wsPort);
369
+
370
+ ws.onopen = function() {
371
+ console.log('[WS] 连接成功');
372
+ document.getElementById('connectionStatus').textContent = '⚡ 已连接';
373
+ };
374
+
375
+ ws.onclose = function() {
376
+ console.log('[WS] 连接断开,3秒后重连...');
377
+ document.getElementById('connectionStatus').textContent = '🔴 已断开';
378
+ setTimeout(initWebSocket, 3000);
379
+ };
380
+
381
+ ws.onerror = function(e) {
382
+ console.error('[WS] 错误:', e);
383
+ };
384
+
385
+ ws.onmessage = function(event) {
386
+ try {
387
+ var msg = JSON.parse(event.data);
388
+ handleWsMessage(msg);
389
+ } catch (e) {
390
+ console.error('[WS] 解析消息失败:', e);
391
+ }
392
+ };
393
+ }
394
+
395
+ // ===== WebSocket 消息处理 =====
396
+ // @ts-ignore
397
+ function handleWsMessage(msg) {
398
+ switch (msg.type) {
399
+ case 'session_created':
400
+ console.log('[WS] 会话已创建:', msg.sessionId);
401
+ break;
402
+ case 'debug_started':
403
+ addLog('任务已开始执行', 'info');
404
+ break;
405
+ case 'log_output':
406
+ addLog(msg.content, 'info');
407
+ // 提取报告路径
408
+ if (msg.content && msg.content.includes('report file updated:')) {
409
+ var match = msg.content.match(/report file updated:\s*(\/[^\s]+\.html)/);
410
+ if (match) {
411
+ // @ts-ignore
412
+ reportPath = match[1];
413
+ document.getElementById('reportBtn').style.display = 'inline-flex';
414
+ }
415
+ }
416
+ break;
417
+ case 'debug_completed':
418
+ // @ts-ignore
419
+ isExecuting = false;
420
+ updateExecuteButton();
421
+ if (msg.success) {
422
+ addLog('任务执行完成 ✓', 'success');
423
+ } else {
424
+ addLog('任务执行失败 (退出码: ' + msg.exitCode + ')', 'error');
425
+ }
426
+ break;
427
+ case 'debug_error':
428
+ // @ts-ignore
429
+ isExecuting = false;
430
+ updateExecuteButton();
431
+ addLog('错误: ' + msg.error, 'error');
432
+ break;
433
+ case 'action_result':
434
+ handleActionResult(msg);
435
+ break;
436
+ case 'task_log':
437
+ addLog(msg.content, msg.level || 'info');
438
+ break;
439
+ }
440
+ }
441
+
442
+ // ===== 动作执行结果处理 =====
443
+ // @ts-ignore
444
+ function handleActionResult(msg) {
445
+ if (msg.success) {
446
+ addLog('动作执行成功 ✓', 'success');
447
+ } else {
448
+ addLog('动作执行失败: ' + (msg.error || '未知错误'), 'error');
449
+ }
450
+ }
451
+
452
+ // ===== 任务执行 =====
453
+ // @ts-ignore
454
+ function executeTask() {
455
+ var script = document.getElementById('scriptEditor').value.trim();
456
+ if (!script) {
457
+ showToast('请输入脚本内容', 'error');
458
+ return;
459
+ }
460
+
461
+ var runMode = document.getElementById('runMode').value;
462
+ if (runMode === 'device') {
463
+ var device = document.getElementById('deviceSelect').value;
464
+ if (!device) {
465
+ showToast('请选择设备', 'error');
466
+ return;
467
+ }
468
+ }
469
+
470
+ // @ts-ignore
471
+ isExecuting = true;
472
+ // @ts-ignore
473
+ currentTaskId = 'debug-' + Date.now();
474
+ updateExecuteButton();
475
+
476
+ // 清空日志
477
+ clearLogs();
478
+ addLog('开始执行任务 (模式: ' + (runMode === 'device' ? '真机' : '浏览器') + ')...', 'info');
479
+
480
+ // 构建请求
481
+ var packageSelect = document.getElementById('packageSelect');
482
+ var packageValue = packageSelect.value === 'custom'
483
+ ? document.getElementById('customPackage').value
484
+ : packageSelect.value;
485
+
486
+ var request = {
487
+ type: 'start_debug',
488
+ sessionId: currentTaskId,
489
+ deviceId: runMode === 'device' ? document.getElementById('deviceSelect').value : undefined,
490
+ url: document.getElementById('testUrl').value,
491
+ prompt: script,
492
+ naturalLanguage: script,
493
+ mobileMode: document.getElementById('mobileMode').checked,
494
+ packageName: runMode === 'device' ? packageValue : undefined,
495
+ platform: runMode === 'device' ? document.getElementById('platform').value : 'web',
496
+ skipAppiumDriver: document.getElementById('skipAppiumDriver').checked,
497
+ loginUsername: document.getElementById('loginUsername').value || undefined,
498
+ document.getElementById('loginPassword').value || undefined,
499
+ };
500
+
501
+ // @ts-ignore
502
+ if (ws && ws.readyState === WebSocket.OPEN) {
503
+ ws.send(JSON.stringify(request));
504
+
505
+ // 保存到历史记录
506
+ saveToHistory(request);
507
+
508
+ // 启动日志轮询
509
+ startLogPolling();
510
+ } else {
511
+ addLog('WebSocket 未连接', 'error');
512
+ // @ts-ignore
513
+ isExecuting = false;
514
+ updateExecuteButton();
515
+ }
516
+ }
517
+
518
+ // ===== 停止任务 =====
519
+ // @ts-ignore
520
+ function stopTask() {
521
+ // @ts-ignore
522
+ if (currentTaskId && ws && ws.readyState === WebSocket.OPEN) {
523
+ ws.send(JSON.stringify({
524
+ type: 'stop_debug',
525
+ sessionId: currentTaskId
526
+ }));
527
+ addLog('正在停止任务...', 'info');
528
+ }
529
+
530
+ // @ts-ignore
531
+ isExecuting = false;
532
+ stopLogPolling();
533
+ updateExecuteButton();
534
+ }
535
+
536
+ // ===== 日志轮询 =====
537
+ // @ts-ignore
538
+ function startLogPolling() {
539
+ // @ts-ignore
540
+ if (pollingInterval) clearInterval(pollingInterval);
541
+ // @ts-ignore
542
+ pollingInterval = setInterval(function() {
543
+ // @ts-ignore
544
+ if (currentTaskId && ws && ws.readyState === WebSocket.OPEN) {
545
+ ws.send(JSON.stringify({
546
+ type: 'get_logs',
547
+ sessionId: currentTaskId
548
+ }));
549
+ }
550
+ }, 1000);
551
+ }
552
+
553
+ // @ts-ignore
554
+ function stopLogPolling() {
555
+ // @ts-ignore
556
+ if (pollingInterval) {
557
+ clearInterval(pollingInterval);
558
+ // @ts-ignore
559
+ pollingInterval = null;
560
+ }
561
+ }
562
+
563
+ // ===== 日志显示 =====
564
+ // @ts-ignore
565
+ function addLog(message, level) {
566
+ if (level === undefined) level = 'info';
567
+ var terminal = document.getElementById('terminalContent');
568
+
569
+ // 移除空状态
570
+ var emptyState = terminal.querySelector('.empty-state');
571
+ if (emptyState) emptyState.remove();
572
+
573
+ var line = document.createElement('div');
574
+ line.className = 'log-line';
575
+
576
+ var time = new Date().toLocaleTimeString();
577
+ var levelClass = 'log-level-' + level;
578
+
579
+ line.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-level ' + levelClass + '">[' + level.toUpperCase() + ']</span><span class="log-content">' + escapeHtml(message) + '</span>';
580
+
581
+ terminal.appendChild(line);
582
+ terminal.scrollTop = terminal.scrollHeight;
583
+
584
+ // 限制日志数量
585
+ while (terminal.children.length > 500) {
586
+ terminal.removeChild(terminal.firstChild);
587
+ }
588
+ }
589
+
590
+ // @ts-ignore
591
+ function escapeHtml(text) {
592
+ var div = document.createElement('div');
593
+ div.textContent = text;
594
+ return div.innerHTML;
595
+ }
596
+
597
+ // ===== 历史记录 =====
598
+ // @ts-ignore
599
+ function saveToHistory(request) {
600
+ var item = {
601
+ id: Date.now(),
602
+ timestamp: Date.now(),
603
+ request: request,
604
+ status: 'pending'
605
+ };
606
+
607
+ // @ts-ignore
608
+ history.unshift(item);
609
+ // @ts-ignore
610
+ if (history.length > 50) history.pop();
611
+
612
+ localStorage.setItem('debug_history', JSON.stringify(history));
613
+ }
614
+
615
+ // @ts-ignore
616
+ function loadHistory() {
617
+ var saved = localStorage.getItem('debug_history');
618
+ if (saved) {
619
+ // @ts-ignore
620
+ history = JSON.parse(saved);
621
+ }
622
+ }
623
+
624
+ // @ts-ignore
625
+ function renderHistory() {
626
+ var container = document.getElementById('historyList');
627
+
628
+ // @ts-ignore
629
+ if (history.length === 0) {
630
+ container.innerHTML = '<div class="empty-state">暂无历史记录</div>';
631
+ return;
632
+ }
633
+
634
+ // @ts-ignore
635
+ var html = history.map(function(item) {
636
+ return '<div class="history-item" onclick="restoreHistory(' + item.id + ')">' +
637
+ '<div class="history-header">' +
638
+ '<span class="history-status ' + (item.status === 'success' ? 'success' : item.status === 'failed' ? 'failed' : '') + '">' +
639
+ (item.status === 'success' ? '成功' : item.status === 'failed' ? '失败' : '进行中') +
640
+ '</span>' +
641
+ '<span style="font-size:11px;color:var(--text-muted)">' + new Date(item.timestamp).toLocaleString() + '</span>' +
642
+ '</div>' +
643
+ '<div class="history-url">' + escapeHtml(item.request.url || item.request.testUrl || '未指定URL') + '</div>' +
644
+ '<div class="history-info">' +
645
+ (item.request.run_mode === 'device' ? '真机 • ' + item.request.platform : '浏览器') +
646
+ (item.request.prompt ? ' • ' + item.request.prompt.substring(0, 50) + '...' : '') +
647
+ '</div>' +
648
+ '</div>';
649
+ }).join('');
650
+
651
+ container.innerHTML = html;
652
+ }
653
+
654
+ // @ts-ignore
655
+ function restoreHistory(id) {
656
+ // @ts-ignore
657
+ var item = history.find(function(h) { return h.id === id; });
658
+ if (!item) return;
659
+
660
+ var req = item.request;
661
+ document.getElementById('runMode').value = req.run_mode || 'browser';
662
+ onRunModeChange();
663
+ document.getElementById('testUrl').value = req.testUrl || req.url || '';
664
+ document.getElementById('scriptEditor').value = req.prompt || '';
665
+ document.getElementById('mobileMode').checked = req.mobileMode || false;
666
+ document.getElementById('skipAppiumDriver').checked = req.skipAppiumDriver !== false;
667
+ document.getElementById('loginUsername').value = req.loginUsername || '';
668
+ document.getElementById('loginPassword').value = req.loginPassword || '';
669
+
670
+ if (req.run_mode === 'device') {
671
+ document.getElementById('platform').value = req.platform || 'android';
672
+ document.getElementById('deviceSelect').value = req.device_udid || '';
673
+ if (req.package_name) {
674
+ document.getElementById('packageSelect').value = 'custom';
675
+ document.getElementById('customPackage').value = req.package_name;
676
+ document.getElementById('customPackage').style.display = 'block';
677
+ }
678
+ }
679
+
680
+ closeHistoryModal();
681
+ showToast('已恢复历史配置', 'info');
682
+ }
683
+
684
+ // ===== 用例选择 =====
685
+ // @ts-ignore
686
+ function selectTestCase() {
687
+ document.getElementById('testCaseModal').classList.add('active');
688
+ loadFolderTree();
689
+ }
690
+
691
+ // @ts-ignore
692
+ function closeTestCaseModal() {
693
+ document.getElementById('testCaseModal').classList.remove('active');
694
+ }
695
+
696
+ // @ts-ignore
697
+ function loadFolderTree() {
698
+ var container = document.getElementById('folderTree');
699
+ container.innerHTML = '<div class="loading">加载中...</div>';
700
+
701
+ fetch('/api/tasks/folder-tree')
702
+ .then(function(response) { return response.json(); })
703
+ .then(function(data) {
704
+ if (data.success && data.data) {
705
+ // @ts-ignore
706
+ folders = Array.isArray(data.data) ? data.data : (data.data.data || []);
707
+ renderFolderTree();
708
+ } else {
709
+ container.innerHTML = '<div class="empty-state">暂无文件夹</div>';
710
+ }
711
+ })
712
+ .catch(function(error) {
713
+ console.error('加载文件夹失败:', error);
714
+ container.innerHTML = '<div class="empty-state">加载失败</div>';
715
+ });
716
+ }
717
+
718
+ // @ts-ignore
719
+ function renderFolderTree() {
720
+ var container = document.getElementById('folderTree');
721
+
722
+ // 添加全部用例选项
723
+ var html = '<div class="folder-item ' + (selectedFolderId === null ? 'active' : '') + '" onclick="selectFolder(null)">📋 全部用例</div>';
724
+
725
+ // @ts-ignore
726
+ folders.forEach(function(folder) {
727
+ var folderInfo = folder.folder || {};
728
+ var folderId = folderInfo.folderId;
729
+ var folderName = folderInfo.folderName || '未命名文件夹';
730
+ var configCount = folderInfo.configCount || 0;
731
+
732
+ html += '<div class="folder-item ' + (selectedFolderId === folderId ? 'active' : '') + '" onclick="selectFolder(' + folderId + ')">📁 ' + folderName + (configCount > 0 ? ' (' + configCount + ')' : '') + '</div>';
733
+ });
734
+
735
+ container.innerHTML = html;
736
+ }
737
+
738
+ // @ts-ignore
739
+ function selectFolder(folderId) {
740
+ // @ts-ignore
741
+ selectedFolderId = folderId;
742
+ renderFolderTree();
743
+
744
+ var container = document.getElementById('caseList');
745
+ container.innerHTML = '<div class="loading">加载中...</div>';
746
+
747
+ var url = '/api/tasks/test-cases';
748
+ if (folderId !== null) {
749
+ url += '?folderId=' + folderId;
750
+ }
751
+
752
+ fetch(url)
753
+ .then(function(response) { return response.json(); })
754
+ .then(function(data) {
755
+ if (data.success && data.data) {
756
+ // @ts-ignore
757
+ testCases = Array.isArray(data.data) ? data.data : (data.data.data || []);
758
+ renderCaseList();
759
+ } else {
760
+ container.innerHTML = '<div class="empty-state">该文件夹下暂无用例</div>';
761
+ }
762
+ })
763
+ .catch(function(error) {
764
+ console.error('加载用例失败:', error);
765
+ container.innerHTML = '<div class="empty-state">加载失败</div>';
766
+ });
767
+ }
768
+
769
+ // @ts-ignore
770
+ function renderCaseList() {
771
+ var container = document.getElementById('caseList');
772
+
773
+ // @ts-ignore
774
+ if (testCases.length === 0) {
775
+ container.innerHTML = '<div class="empty-state">暂无用例</div>';
776
+ return;
777
+ }
778
+
779
+ // @ts-ignore
780
+ var html = testCases.map(function(caseItem) {
781
+ return '<div class="case-item ' + (selectedTestCase && selectedTestCase.id === caseItem.id ? 'selected' : '') + '" onclick="selectCaseItem(' + caseItem.id + ')">' +
782
+ '<div class="case-info">' +
783
+ '<div class="case-name">' + escapeHtml(caseItem.configName || '未命名用例') + '</div>' +
784
+ '<div class="case-desc">' + escapeHtml(caseItem.description || '无描述') + '</div>' +
785
+ '</div>' +
786
+ '</div>';
787
+ }).join('');
788
+
789
+ container.innerHTML = html;
790
+ }
791
+
792
+ // @ts-ignore
793
+ function selectCaseItem(id) {
794
+ // @ts-ignore
795
+ selectedTestCase = testCases.find(function(c) { return c.id === id; });
796
+ renderCaseList();
797
+ }
798
+
799
+ // @ts-ignore
800
+ function confirmTestCase() {
801
+ // @ts-ignore
802
+ if (!selectedTestCase) {
803
+ showToast('请选择用例', 'error');
804
+ return;
805
+ }
806
+
807
+ document.getElementById('testUrl').value = selectedTestCase.testUrl || '';
808
+ document.getElementById('scriptEditor').value = selectedTestCase.naturalLanguage || selectedTestCase.prompt || '';
809
+ document.getElementById('mobileMode').checked = selectedTestCase.mobileMode || false;
810
+
811
+ if (selectedTestCase.platform) {
812
+ document.getElementById('platform').value = selectedTestCase.platform;
813
+ }
814
+
815
+ if (selectedTestCase.packageName) {
816
+ document.getElementById('packageSelect').value = 'custom';
817
+ document.getElementById('customPackage').value = selectedTestCase.packageName;
818
+ document.getElementById('customPackage').style.display = 'block';
819
+ }
820
+
821
+ document.getElementById('loginUsername').value = selectedTestCase.loginUsername || '';
822
+ document.getElementById('loginPassword').value = selectedTestCase.loginPassword || '';
823
+
824
+ closeTestCaseModal();
825
+ showToast('已加载用例: ' + selectedTestCase.configName, 'success');
826
+ }
827
+
828
+ // ===== 保存用例 =====
829
+ // @ts-ignore
830
+ function saveTestCase() {
831
+ document.getElementById('saveModal').classList.add('active');
832
+ loadFoldersForSave();
833
+ }
834
+
835
+ // @ts-ignore
836
+ function closeSaveModal() {
837
+ document.getElementById('saveModal').classList.remove('active');
838
+ }
839
+
840
+ // @ts-ignore
841
+ function loadFoldersForSave() {
842
+ var select = document.getElementById('folderSelect');
843
+ select.innerHTML = '<option value="">选择文件夹...</option>';
844
+
845
+ fetch('/api/tasks/folder-tree')
846
+ .then(function(response) { return response.json(); })
847
+ .then(function(data) {
848
+ if (data.success && data.data) {
849
+ var folderList = Array.isArray(data.data) ? data.data : (data.data.data || []);
850
+ folderList.forEach(function(folder) {
851
+ var folderInfo = folder.folder || {};
852
+ var option = document.createElement('option');
853
+ option.value = folderInfo.folderId;
854
+ option.textContent = folderInfo.folderName || '未命名文件夹';
855
+ select.appendChild(option);
856
+ });
857
+ }
858
+ })
859
+ .catch(function(error) {
860
+ console.error('加载文件夹失败:', error);
861
+ });
862
+ }
863
+
864
+ // @ts-ignore
865
+ function confirmSave() {
866
+ var caseName = document.getElementById('caseName').value.trim();
867
+ var folderId = document.getElementById('folderSelect').value;
868
+
869
+ if (!caseName) {
870
+ showToast('请输入用例名称', 'error');
871
+ return;
872
+ }
873
+
874
+ if (!folderId) {
875
+ showToast('请选择文件夹', 'error');
876
+ return;
877
+ }
878
+
879
+ var runMode = document.getElementById('runMode').value;
880
+ var packageSelect = document.getElementById('packageSelect');
881
+ var packageValue = packageSelect.value === 'custom'
882
+ ? document.getElementById('customPackage').value
883
+ : packageSelect.value;
884
+
885
+ var saveData = {
886
+ configName: caseName,
887
+ folderId: parseInt(folderId),
888
+ testUrl: document.getElementById('testUrl').value,
889
+ naturalLanguage: document.getElementById('scriptEditor').value,
890
+ prompt: document.getElementById('scriptEditor').value,
891
+ runMode: runMode,
892
+ platform: document.getElementById('platform').value,
893
+ packageName: packageValue,
894
+ mobileMode: document.getElementById('mobileMode').checked,
895
+ loginUsername: document.getElementById('loginUsername').value,
896
+ document.getElementById('loginPassword').value,
897
+ status: 1,
898
+ isRealDevice: runMode === 'device',
899
+ };
900
+
901
+ fetch('/api/tasks/test-cases', {
902
+ method: 'POST',
903
+ headers: { 'Content-Type': 'application/json' },
904
+ body: JSON.stringify(saveData)
905
+ })
906
+ .then(function(response) { return response.json(); })
907
+ .then(function(data) {
908
+ if (data.success) {
909
+ showToast('用例保存成功', 'success');
910
+ closeSaveModal();
911
+ document.getElementById('caseName').value = '';
912
+ } else {
913
+ showToast('保存失败: ' + (data.message || '未知错误'), 'error');
914
+ }
915
+ })
916
+ .catch(function(error) {
917
+ console.error('保存用例失败:', error);
918
+ showToast('保存失败', 'error');
919
+ });
920
+ }
921
+
922
+ // ===== 设备列表 =====
923
+ // @ts-ignore
924
+ function refreshDevices() {
925
+ var select = document.getElementById('deviceSelect');
926
+ select.innerHTML = '<option value="">加载中...</option>';
927
+
928
+ fetch('/api/devices/online')
929
+ .then(function(response) { return response.json(); })
930
+ .then(function(devices) {
931
+ select.innerHTML = '<option value="">选择设备...</option>';
932
+
933
+ if (devices.length === 0) {
934
+ select.innerHTML = '<option value="">暂无设备</option>';
935
+ showToast('未检测到设备', 'error');
936
+ return;
937
+ }
938
+
939
+ devices.forEach(function(device) {
940
+ var option = document.createElement('option');
941
+ option.value = device.serialNumber;
942
+ option.textContent = (device.model || device.name || '未知设备') + ' (' + device.serialNumber + ')';
943
+ select.appendChild(option);
944
+ });
945
+
946
+ showToast('已加载 ' + devices.length + ' 个设备', 'info');
947
+ })
948
+ .catch(function(error) {
949
+ console.error('加载设备失败:', error);
950
+ select.innerHTML = '<option value="">加载失败</option>';
951
+ });
952
+ }
953
+
954
+ // ===== 界面更新 =====
955
+ // @ts-ignore
956
+ function onRunModeChange() {
957
+ var runMode = document.getElementById('runMode').value;
958
+ document.getElementById('deviceConfig').style.display = runMode === 'device' ? 'block' : 'none';
959
+ document.getElementById('browserConfig').style.display = runMode === 'browser' ? 'block' : 'none';
960
+
961
+ if (runMode === 'device') {
962
+ refreshDevices();
963
+ }
964
+ }
965
+
966
+ // @ts-ignore
967
+ function onPackageChange() {
968
+ var packageSelect = document.getElementById('packageSelect');
969
+ var customPackage = document.getElementById('customPackage');
970
+ customPackage.style.display = packageSelect.value === 'custom' ? 'block' : 'none';
971
+ }
972
+
973
+ // @ts-ignore
974
+ function updateExecuteButton() {
975
+ var executeBtn = document.getElementById('executeBtn');
976
+ var stopBtn = document.getElementById('stopBtn');
977
+ var taskStatus = document.getElementById('taskStatus');
978
+
979
+ // @ts-ignore
980
+ if (isExecuting) {
981
+ executeBtn.style.display = 'none';
982
+ stopBtn.style.display = 'block';
983
+ taskStatus.textContent = '⚙️ 执行中...';
984
+ } else {
985
+ executeBtn.style.display = 'block';
986
+ stopBtn.style.display = 'none';
987
+ taskStatus.textContent = '';
988
+ }
989
+ }
990
+
991
+ // @ts-ignore
992
+ function toggleTerminal() {
993
+ var terminal = document.querySelector('.terminal-section');
994
+ var toggle = document.getElementById('terminalToggle');
995
+
996
+ // @ts-ignore
997
+ terminalMinimized = !terminalMinimized;
998
+
999
+ // @ts-ignore
1000
+ if (terminalMinimized) {
1001
+ terminal.style.height = '36px';
1002
+ terminal.style.overflow = 'hidden';
1003
+ toggle.textContent = '▲';
1004
+ } else {
1005
+ terminal.style.height = '300px';
1006
+ terminal.style.overflow = 'hidden';
1007
+ toggle.textContent = '▼';
1008
+ }
1009
+ }
1010
+
1011
+ // @ts-ignore
1012
+ function toggleHistory() {
1013
+ document.getElementById('historyModal').classList.add('active');
1014
+ renderHistory();
1015
+ }
1016
+
1017
+ // @ts-ignore
1018
+ function closeHistoryModal() {
1019
+ document.getElementById('historyModal').classList.remove('active');
1020
+ }
1021
+
1022
+ // @ts-ignore
1023
+ function clearEditor() {
1024
+ document.getElementById('scriptEditor').value = '';
1025
+ // @ts-ignore
1026
+ selectedTestCase = null;
1027
+ showToast('编辑器已清空', 'info');
1028
+ }
1029
+
1030
+ // @ts-ignore
1031
+ function clearLogs() {
1032
+ var terminal = document.getElementById('terminalContent');
1033
+ terminal.innerHTML = '<div class="empty-state"><div class="icon">📟</div><p>暂无输出...</p><p style="font-size:11px;margin-top:8px">执行任务后,输出将显示在这里</p></div>';
1034
+ // @ts-ignore
1035
+ reportPath = '';
1036
+ document.getElementById('reportBtn').style.display = 'none';
1037
+ }
1038
+
1039
+ // @ts-ignore
1040
+ function openReport() {
1041
+ // @ts-ignore
1042
+ if (reportPath) {
1043
+ window.open('file://' + reportPath, '_blank');
1044
+ }
1045
+ }
1046
+
1047
+ // @ts-ignore
1048
+ function showToast(message, type) {
1049
+ if (type === undefined) type = 'info';
1050
+ var toast = document.getElementById('toast');
1051
+ toast.textContent = message;
1052
+ toast.className = 'toast ' + type + ' show';
1053
+
1054
+ setTimeout(function() {
1055
+ toast.classList.remove('show');
1056
+ }, 3000);
1057
+ }
1058
+
1059
+ // ===== 初始化 =====
1060
+ document.addEventListener('DOMContentLoaded', function() {
1061
+ initWebSocket();
1062
+ loadHistory();
1063
+
1064
+ // 初始化运行模式
1065
+ onRunModeChange();
1066
+ });
1067
+ </script>
1068
+ </body>
1069
+ </html>