@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.
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +26 -5
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/autobots-client.d.ts +37 -0
- package/dist/core/autobots-client.d.ts.map +1 -0
- package/dist/core/autobots-client.js +132 -0
- package/dist/core/autobots-client.js.map +1 -0
- package/dist/debug/websocket-server.d.ts +6 -0
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +128 -59
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/executor/android-executor.d.ts.map +1 -1
- package/dist/executor/android-executor.js +45 -4
- package/dist/executor/android-executor.js.map +1 -1
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +13 -1
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/debug-api.d.ts +9 -0
- package/dist/web/debug-api.d.ts.map +1 -0
- package/dist/web/debug-api.js +368 -0
- package/dist/web/debug-api.js.map +1 -0
- package/dist/web/debug-page.d.ts +7 -0
- package/dist/web/debug-page.d.ts.map +1 -0
- package/dist/web/debug-page.js +1591 -0
- package/dist/web/debug-page.js.map +1 -0
- package/dist/web/debug.html +1069 -0
- package/dist/web/dist/debug.html +1069 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +456 -86
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -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>
|