@aiscene/aiserver 1.2.3 → 1.2.4
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/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/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 +1388 -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 +183 -86
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 调试页面前端HTML - 仿照 electron-shadcn 项目的调试页面
|
|
3
|
+
* 功能:脚本编辑、用例管理、调试执行、终端输出
|
|
4
|
+
* 所有文本使用中文
|
|
5
|
+
*/
|
|
6
|
+
export function getDebugPageHtml() {
|
|
7
|
+
return `
|
|
8
|
+
<style>
|
|
9
|
+
/* ===== 调试页面样式 - 全屏白色布局 ===== */
|
|
10
|
+
.debug-page{display:flex;height:100vh;gap:0;overflow:hidden;background:#f8fafc}
|
|
11
|
+
html,body{overflow:hidden;margin:0;padding:0}
|
|
12
|
+
.debug-sidebar{width:320px;min-width:280px;max-width:400px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;background:#fff}
|
|
13
|
+
.debug-sidebar-header{padding:12px 16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;background:#fff}
|
|
14
|
+
.debug-sidebar-header h3{font-size:14px;font-weight:600;color:#1e293b}
|
|
15
|
+
.debug-sidebar-body{flex:1;overflow-y:auto;padding:16px}
|
|
16
|
+
.debug-sidebar-body .form-group{margin-bottom:16px}
|
|
17
|
+
.debug-sidebar-body .form-group label{display:block;font-size:12px;color:#64748b;margin-bottom:6px;font-weight:500}
|
|
18
|
+
.debug-sidebar-body .form-group input,.debug-sidebar-body .form-group select{width:100%;padding:8px 12px;background:#fff;border:1px solid #cbd5e1;border-radius:6px;color:#1e293b;font-size:13px;outline:none;transition:border-color .2s}
|
|
19
|
+
.debug-sidebar-body .form-group input:focus,.debug-sidebar-body .form-group select:focus{border-color:#38bdf8}
|
|
20
|
+
.debug-sidebar-body .form-group select option{background:#fff;color:#1e293b}
|
|
21
|
+
.debug-sidebar-body .checkbox-group{display:flex;align-items:center;gap:8px;margin-bottom:12px}
|
|
22
|
+
.debug-sidebar-body .checkbox-group input[type=checkbox]{accent-color:#38bdf8;width:16px;height:16px}
|
|
23
|
+
.debug-sidebar-body .checkbox-group label{font-size:13px;color:#475569;cursor:pointer}
|
|
24
|
+
.debug-content{flex:1;display:flex;flex-direction:column;min-width:0;background:#fff}
|
|
25
|
+
.debug-editor-header{padding:8px 16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;background:#fff}
|
|
26
|
+
.debug-editor-header .title{font-size:13px;font-weight:600;color:#1e293b;display:flex;align-items:center;gap:6px}
|
|
27
|
+
.debug-editor-header .actions{display:flex;gap:4px}
|
|
28
|
+
.debug-editor-area{flex:1;position:relative;min-height:0}
|
|
29
|
+
.debug-editor-area textarea{width:100%;height:100%;background:#fff;color:#1e293b;border:1px solid #e2e8f0;padding:16px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:13px;line-height:1.6;resize:none;outline:none}
|
|
30
|
+
.debug-terminal{height:280px;min-height:180px;border-top:1px solid #e2e8f0;display:flex;flex-direction:column;background:#f8fafc}
|
|
31
|
+
.debug-terminal-header{padding:8px 16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;background:#fff}
|
|
32
|
+
.debug-terminal-header .title{font-size:11px;color:#64748b;display:flex;align-items:center;gap:6px}
|
|
33
|
+
.debug-terminal-body{flex:1;overflow-y:auto;padding:12px 16px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6;background:#fff}
|
|
34
|
+
.debug-terminal-body .log-line{padding:2px 0;border-bottom:1px solid #f1f5f9}
|
|
35
|
+
.debug-terminal-body .log-time{color:#94a3b8;margin-right:8px}
|
|
36
|
+
.debug-terminal-body .log-info{color:#0284c7}
|
|
37
|
+
.debug-terminal-body .log-warn{color:#d97706}
|
|
38
|
+
.debug-terminal-body .log-error{color:#dc2626}
|
|
39
|
+
.debug-terminal-body .log-success{color:#16a34a}
|
|
40
|
+
/* 实时报告面板 - 右侧独立列(白色主题) */
|
|
41
|
+
.debug-report-panel{width:480px;min-width:400px;max-width:600px;border-left:1px solid #e2e8f0;display:flex;flex-direction:column;background:#fff}
|
|
42
|
+
.debug-report-panel.hidden{display:none}
|
|
43
|
+
.debug-report-header{padding:8px 16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;background:#fff}
|
|
44
|
+
.debug-report-header .title{font-size:11px;color:#1e293b;display:flex;align-items:center;gap:6px;font-weight:600}
|
|
45
|
+
.debug-report-body{flex:1;overflow-y:auto;display:flex;min-height:0;background:#fff}
|
|
46
|
+
.debug-report-timeline{width:260px;min-width:200px;border-right:1px solid #e2e8f0;overflow-y:auto;padding:8px 0;background:#f8fafc}
|
|
47
|
+
.debug-report-detail{flex:1;overflow-y:auto;padding:16px;background:#fff}
|
|
48
|
+
.rtl-step{padding:6px 12px;cursor:pointer;display:flex;align-items:flex-start;gap:8px;border-left:2px solid transparent;transition:background .15s}
|
|
49
|
+
.rtl-step:hover{background:#e2e8f0}
|
|
50
|
+
.rtl-step.active{background:#dbeafe;border-left-color:#2563eb}
|
|
51
|
+
.rtl-step-dot{width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0;margin-top:1px}
|
|
52
|
+
.rtl-step-dot.status-finished{background:#dcfce7;color:#16a34a}
|
|
53
|
+
.rtl-step-dot.status-running{background:#fef3c7;color:#d97706}
|
|
54
|
+
.rtl-step-dot.status-failed{background:#fee2e2;color:#dc2626}
|
|
55
|
+
.rtl-step-dot.status-pending{background:#f1f5f9;color:#64748b}
|
|
56
|
+
.rtl-step-info{flex:1;min-width:0}
|
|
57
|
+
.rtl-step-name{font-size:12px;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
58
|
+
.rtl-step-meta{font-size:10px;color:#64748b;margin-top:2px;display:flex;gap:6px;align-items:center}
|
|
59
|
+
.rtl-step-type{padding:1px 4px;border-radius:3px;font-size:9px;font-weight:600}
|
|
60
|
+
.rtl-step-type.type-insight{background:#dbeafe;color:#2563eb}
|
|
61
|
+
.rtl-step-type.type-planning{background:#fef3c7;color:#d97706}
|
|
62
|
+
.rtl-step-type.type-action{background:#dcfce7;color:#16a34a}
|
|
63
|
+
.rtl-step-type.type-other{background:#f1f5f9;color:#64748b}
|
|
64
|
+
.rrd-header{margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
65
|
+
.rrd-index{width:22px;height:22px;border-radius:50%;background:#2563eb;color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0}
|
|
66
|
+
.rrd-name{font-size:14px;font-weight:600;color:#1e293b}
|
|
67
|
+
.rrd-section{margin-bottom:12px;background:#f8fafc;border-radius:6px;padding:12px}
|
|
68
|
+
.rrd-section-title{font-size:11px;color:#64748b;font-weight:600;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|
69
|
+
.rrd-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
|
|
70
|
+
.rrd-info-item{font-size:11px}
|
|
71
|
+
.rrd-info-label{color:#94a3b8;margin-bottom:2px}
|
|
72
|
+
.rrd-info-value{color:#1e293b}
|
|
73
|
+
.rrd-thought{background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:10px;font-size:12px;color:#475569;white-space:pre-wrap;line-height:1.5}
|
|
74
|
+
.rrd-task{border-bottom:1px solid #e2e8f0;padding:8px 0}
|
|
75
|
+
.rrd-task:last-child{border-bottom:none}
|
|
76
|
+
.rrd-task-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
|
77
|
+
.rrd-task-type{font-size:11px;color:#1e293b;font-weight:500}
|
|
78
|
+
.rrd-task-status{font-size:10px;padding:2px 6px;border-radius:8px;font-weight:600}
|
|
79
|
+
.rrd-task-status.status-finished{background:#dcfce7;color:#16a34a}
|
|
80
|
+
.rrd-task-status.status-running{background:#fef3c7;color:#d97706}
|
|
81
|
+
.rrd-task-status.status-failed{background:#fee2e2;color:#dc2626}
|
|
82
|
+
.rrd-task-status.status-pending{background:#f1f5f9;color:#64748b}
|
|
83
|
+
.rrd-task-body{font-size:11px;color:#475569}
|
|
84
|
+
.rrd-task-param,.rrd-task-output,.rrd-task-error{margin-top:4px;padding:6px;background:#f8fafc;border-radius:4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:10px;white-space:pre-wrap;word-break:break-all}
|
|
85
|
+
.rrd-task-error{border:1px solid #fecaca;color:#dc2626}
|
|
86
|
+
.rrd-progress{display:flex;align-items:center;gap:8px;font-size:11px;color:#64748b}
|
|
87
|
+
.rrd-progress-bar{flex:1;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden}
|
|
88
|
+
.rrd-progress-fill{height:100%;background:#2563eb;border-radius:2px;transition:width .3s}
|
|
89
|
+
.rrd-empty{text-align:center;color:#94a3b8;padding:40px 20px;font-size:13px}
|
|
90
|
+
/* 截图展示 */
|
|
91
|
+
.rrd-screenshot{margin-top:8px;border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;background:#fff}
|
|
92
|
+
.rrd-screenshot img{width:100%;display:block;cursor:pointer;transition:opacity .2s}
|
|
93
|
+
.rrd-screenshot img:hover{opacity:0.9}
|
|
94
|
+
.rrd-screenshot-label{font-size:10px;color:#64748b;padding:4px 8px;text-align:center}
|
|
95
|
+
.debug-statusbar{height:28px;background:#fff;border-top:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;padding:0 16px}
|
|
96
|
+
.debug-statusbar .left{display:flex;align-items:center;gap:12px;font-size:11px;color:#64748b}
|
|
97
|
+
.debug-statusbar .right{display:flex;align-items:center;gap:8px}
|
|
98
|
+
.debug-statusbar .executing{color:#d97706;display:flex;align-items:center;gap:4px}
|
|
99
|
+
.debug-statusbar .executing .spinner{width:12px;height:12px;border:2px solid #fef3c7;border-top-color:#d97706;border-radius:50%;animation:spin 1s linear infinite}
|
|
100
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
101
|
+
|
|
102
|
+
/* 弹窗样式 */
|
|
103
|
+
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center}
|
|
104
|
+
.modal{background:#fff;border:1px solid #e2e8f0;border-radius:12px;max-width:900px;width:90%;max-height:80vh;display:flex;flex-direction:column;overflow:hidden}
|
|
105
|
+
.modal-header{padding:16px 24px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between}
|
|
106
|
+
.modal-header h3{font-size:16px;font-weight:600;color:#1e293b}
|
|
107
|
+
.modal-header .close-btn{background:none;border:none;color:#64748b;cursor:pointer;font-size:20px;padding:4px 8px;border-radius:4px}
|
|
108
|
+
.modal-header .close-btn:hover{color:#1e293b;background:#f1f5f9}
|
|
109
|
+
.modal-body{flex:1;overflow-y:auto;padding:24px}
|
|
110
|
+
.modal-footer{padding:16px 24px;border-top:1px solid #e2e8f0;display:flex;justify-content:flex-end;gap:8px}
|
|
111
|
+
.modal-footer .btn{padding:8px 20px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .2s}
|
|
112
|
+
.modal-footer .btn-cancel{background:#f1f5f9;color:#64748b}
|
|
113
|
+
.modal-footer .btn-cancel:hover{background:#e2e8f0}
|
|
114
|
+
.modal-footer .btn-primary{background:#2563eb;color:#fff}
|
|
115
|
+
.modal-footer .btn-primary:hover{background:#1d4ed8}
|
|
116
|
+
|
|
117
|
+
/* 用例选择器 */
|
|
118
|
+
.case-selector{display:flex;gap:16px;height:60vh}
|
|
119
|
+
.case-selector-left{width:35%;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;display:flex;flex-direction:column;background:#fff}
|
|
120
|
+
.case-selector-right{flex:1;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;display:flex;flex-direction:column;background:#fff}
|
|
121
|
+
.case-selector-header{padding:8px 12px;background:#f8fafc;border-bottom:1px solid #e2e8f0;font-size:12px;font-weight:600;color:#64748b}
|
|
122
|
+
.case-selector-list{flex:1;overflow-y:auto;padding:8px}
|
|
123
|
+
.folder-item{padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#475569;display:flex;align-items:center;gap:6px;transition:background .2s}
|
|
124
|
+
.folder-item:hover{background:#f1f5f9}
|
|
125
|
+
.folder-item.active{background:#dbeafe;color:#2563eb}
|
|
126
|
+
.folder-item .arrow{font-size:10px;color:#64748b;transition:transform .2s}
|
|
127
|
+
.folder-item .arrow.expanded{transform:rotate(90deg)}
|
|
128
|
+
.case-item{padding:10px 12px;border-radius:6px;cursor:pointer;font-size:13px;color:#475569;transition:background .2s;border:1px solid transparent}
|
|
129
|
+
.case-item:hover{background:#f1f5f9}
|
|
130
|
+
.case-item.selected{background:#dbeafe;border-color:#bfdbfe}
|
|
131
|
+
.case-item .case-name{font-weight:500;margin-bottom:2px}
|
|
132
|
+
.case-item .case-desc{font-size:11px;color:#94a3b8}
|
|
133
|
+
|
|
134
|
+
/* 保存用例弹窗 */
|
|
135
|
+
.save-case-form .form-group{margin-bottom:16px}
|
|
136
|
+
.save-case-form .form-group label{display:block;font-size:12px;color:#64748b;margin-bottom:6px;font-weight:500}
|
|
137
|
+
.save-case-form .form-group input,.save-case-form .form-group select{width:100%;padding:8px 12px;background:#fff;border:1px solid #cbd5e1;border-radius:6px;color:#1e293b;font-size:13px;outline:none}
|
|
138
|
+
.save-case-form .form-group input:focus,.save-case-form .form-group select:focus{border-color:#2563eb}
|
|
139
|
+
|
|
140
|
+
/* 历史记录侧边栏 */
|
|
141
|
+
.history-drawer{position:fixed;top:0;right:0;width:360px;height:100vh;background:#fff;border-left:1px solid #e2e8f0;z-index:999;transform:translateX(100%);transition:transform .3s;display:flex;flex-direction:column}
|
|
142
|
+
.history-drawer.open{transform:translateX(0)}
|
|
143
|
+
.history-drawer-header{padding:16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between}
|
|
144
|
+
.history-drawer-header h3{font-size:14px;font-weight:600;color:#1e293b}
|
|
145
|
+
.history-drawer-body{flex:1;overflow-y:auto;padding:12px}
|
|
146
|
+
.history-item{padding:12px;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:background .2s;margin-bottom:8px}
|
|
147
|
+
.history-item:hover{background:#f8fafc}
|
|
148
|
+
.history-item .meta{display:flex;justify-content:space-between;margin-bottom:4px}
|
|
149
|
+
.history-item .status{font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600}
|
|
150
|
+
.history-item .status.success{background:#dcfce7;color:#16a34a}
|
|
151
|
+
.history-item .status.failed{background:#fee2e2;color:#dc2626}
|
|
152
|
+
.history-item .time{font-size:11px;color:#94a3b8}
|
|
153
|
+
.history-item .url{font-size:13px;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
154
|
+
.history-item .detail{font-size:11px;color:#94a3b8;margin-top:4px}
|
|
155
|
+
|
|
156
|
+
/* 生成脚本按钮样式 */
|
|
157
|
+
.btn-icon{padding:4px 10px;border-radius:4px;border:none;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s;display:flex;align-items:center;gap:4px}
|
|
158
|
+
.btn-ghost{background:#f1f5f9;color:#475569}
|
|
159
|
+
.btn-ghost:hover{background:#e2e8f0;color:#1e293b}
|
|
160
|
+
.btn-danger{background:#dc2626;color:#fff}
|
|
161
|
+
.btn-danger:hover{background:#ef4444}
|
|
162
|
+
.btn-blue{background:#2563eb;color:#fff}
|
|
163
|
+
.btn-blue:hover{background:#1d4ed8}
|
|
164
|
+
</style>
|
|
165
|
+
|
|
166
|
+
<div class="debug-page" id="debug-page-content">
|
|
167
|
+
<!-- 左侧配置面板 -->
|
|
168
|
+
<div class="debug-sidebar">
|
|
169
|
+
<div class="debug-sidebar-header">
|
|
170
|
+
<h3>调试面板</h3>
|
|
171
|
+
<button class="btn-icon btn-ghost" onclick="toggleHistoryDrawer()" title="历史记录">📚</button>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="debug-sidebar-body">
|
|
174
|
+
<div class="form-group">
|
|
175
|
+
<label>运行模式</label>
|
|
176
|
+
<select id="debug-runMode" onchange="onRunModeChange()">
|
|
177
|
+
<option value="device">真机</option>
|
|
178
|
+
<option value="browser">浏览器</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div id="debug-device-section">
|
|
183
|
+
<div class="form-group">
|
|
184
|
+
<label>平台</label>
|
|
185
|
+
<select id="debug-platform" onchange="onPlatformChange()">
|
|
186
|
+
<option value="android">Android</option>
|
|
187
|
+
<option value="ios">iOS</option>
|
|
188
|
+
</select>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="form-group">
|
|
191
|
+
<label>设备 <button class="btn-icon btn-ghost" onclick="refreshDevices()" style="float:right;font-size:11px">↻ 刷新</button></label>
|
|
192
|
+
<select id="debug-device">
|
|
193
|
+
<option value="">加载中...</option>
|
|
194
|
+
</select>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="form-group" id="debug-package-section">
|
|
197
|
+
<label id="debug-package-label">包名</label>
|
|
198
|
+
<select id="debug-package-preset" onchange="onPackagePresetChange()">
|
|
199
|
+
<option value="com.jingdong.app.mall">京东</option>
|
|
200
|
+
<option value="com.jd.dh">京东医生</option>
|
|
201
|
+
<option value="com.jd.jdhealth">京东健康</option>
|
|
202
|
+
<option value="custom">自定义</option>
|
|
203
|
+
</select>
|
|
204
|
+
<input id="debug-package-custom" style="display:none;margin-top:8px" placeholder="输入包名" oninput="syncPackageValue()">
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div class="form-group">
|
|
209
|
+
<label>测试地址</label>
|
|
210
|
+
<input id="debug-url" placeholder="输入测试URL">
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div id="debug-browser-options" style="display:none">
|
|
214
|
+
<div class="checkbox-group">
|
|
215
|
+
<input type="checkbox" id="debug-mobileMode">
|
|
216
|
+
<label for="debug-mobileMode">模拟移动端</label>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div class="checkbox-group">
|
|
221
|
+
<input type="checkbox" id="debug-skipAppium" checked>
|
|
222
|
+
<label for="debug-skipAppium">跳过 Appium 服务</label>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div class="form-group">
|
|
226
|
+
<label>登录用户名(选填)</label>
|
|
227
|
+
<input id="debug-loginUser" placeholder="输入用户名">
|
|
228
|
+
</div>
|
|
229
|
+
<div class="form-group">
|
|
230
|
+
<label>登录密码(选填)</label>
|
|
231
|
+
<input id="debug-loginPass" type="password" placeholder="输入密码">
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div style="padding-top:16px;display:flex;flex-direction:column;gap:8px">
|
|
235
|
+
<button id="debug-execute-btn" class="btn-icon btn-blue" style="width:100%;justify-content:center;padding:10px" onclick="handleDebugExecute()">
|
|
236
|
+
▶ 运行任务
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- 右侧内容区 -->
|
|
243
|
+
<div class="debug-content">
|
|
244
|
+
<!-- 脚本编辑器头部 -->
|
|
245
|
+
<div class="debug-editor-header">
|
|
246
|
+
<div class="title">📝 脚本编辑器</div>
|
|
247
|
+
<div class="actions">
|
|
248
|
+
<button class="btn-icon btn-ghost" onclick="openCaseSelector()">📁 选择用例</button>
|
|
249
|
+
<button class="btn-icon btn-ghost" onclick="openSaveCaseDialog()">💾 保存用例</button>
|
|
250
|
+
<button class="btn-icon btn-ghost" onclick="handleGenerateScript()">⚡ 生成脚本</button>
|
|
251
|
+
<button class="btn-icon btn-ghost" onclick="handleClearEditor()">🗑 清空</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- 脚本编辑区 -->
|
|
256
|
+
<div class="debug-editor-area">
|
|
257
|
+
<textarea id="debug-script" placeholder="// 在此编写调试脚本\n\n" spellcheck="false"></textarea>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- 终端面板 -->
|
|
261
|
+
<div class="debug-terminal" id="debug-terminal">
|
|
262
|
+
<div class="debug-terminal-header">
|
|
263
|
+
<div class="title">💻 终端输出</div>
|
|
264
|
+
<div style="display:flex;gap:4px">
|
|
265
|
+
<button id="debug-report-btn" class="btn-icon btn-ghost" style="display:none" onclick="openReport()">📊 报告</button>
|
|
266
|
+
<button class="btn-icon btn-ghost" onclick="clearTerminal()">🗑 清空</button>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="debug-terminal-body" id="debug-terminal-body">
|
|
270
|
+
<div style="text-align:center;color:#64748b;padding:40px 20px">
|
|
271
|
+
<div style="font-size:32px;margin-bottom:8px;opacity:0.5">💻</div>
|
|
272
|
+
<div style="font-style:italic">暂无输出...</div>
|
|
273
|
+
<div style="font-size:11px;margin-top:8px;color:#475569">运行任务后,输出将显示在这里</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- 底部状态栏 -->
|
|
279
|
+
<div class="debug-statusbar">
|
|
280
|
+
<div class="left">
|
|
281
|
+
<span id="debug-exec-status"></span>
|
|
282
|
+
<span id="debug-task-id" style="color:#475569"></span>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="right">
|
|
285
|
+
<button class="btn-icon btn-ghost" onclick="toggleTerminal()" style="font-size:11px">💻 终端 ▼</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- 实时报告面板 - 右侧独立列 -->
|
|
291
|
+
<div class="debug-report-panel hidden" id="debug-report-panel">
|
|
292
|
+
<div class="debug-report-header">
|
|
293
|
+
<div class="title">📊 执行报告</div>
|
|
294
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
295
|
+
<div class="rrd-progress" id="report-progress"></div>
|
|
296
|
+
<button class="btn-icon btn-ghost" onclick="toggleReportPanel()">✕</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="debug-report-body" id="debug-report-body">
|
|
300
|
+
<div class="debug-report-timeline" id="report-timeline"></div>
|
|
301
|
+
<div class="debug-report-detail" id="report-detail">
|
|
302
|
+
<div class="rrd-empty">等待执行步骤...</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<!-- 历史记录侧边栏 -->
|
|
309
|
+
<div class="history-drawer" id="history-drawer">
|
|
310
|
+
<div class="history-drawer-header">
|
|
311
|
+
<h3>历史记录</h3>
|
|
312
|
+
<button class="btn-icon btn-ghost" onclick="toggleHistoryDrawer()">✕</button>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="history-drawer-body" id="history-list">
|
|
315
|
+
<div style="text-align:center;color:#64748b;padding:40px 20px">暂无历史记录</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- 用例选择弹窗 -->
|
|
320
|
+
<div class="modal-overlay" id="case-selector-modal" style="display:none" onclick="closeCaseSelectorOnOverlay(event)">
|
|
321
|
+
<div class="modal" style="max-width:900px">
|
|
322
|
+
<div class="modal-header">
|
|
323
|
+
<h3>选择测试用例</h3>
|
|
324
|
+
<button class="close-btn" onclick="closeCaseSelector()">✕</button>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="modal-body" style="padding:0">
|
|
327
|
+
<div class="case-selector">
|
|
328
|
+
<div class="case-selector-left">
|
|
329
|
+
<div class="case-selector-header">文件夹</div>
|
|
330
|
+
<div class="case-selector-list" id="case-folder-list">
|
|
331
|
+
<div style="text-align:center;color:#64748b;padding:20px">加载中...</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="case-selector-right">
|
|
335
|
+
<div class="case-selector-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
336
|
+
<span id="case-list-title">测试用例</span>
|
|
337
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
338
|
+
<input id="case-search-input" placeholder="搜索用例" style="padding:4px 8px;background:#fff;border:1px solid #cbd5e1;border-radius:4px;color:#1e293b;font-size:12px;width:160px">
|
|
339
|
+
<button class="btn-icon btn-ghost" onclick="searchCases()" style="font-size:11px">搜索</button>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="case-selector-list" id="case-list">
|
|
343
|
+
<div style="text-align:center;color:#64748b;padding:20px">请从左侧选择文件夹</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="modal-footer">
|
|
349
|
+
<button class="btn btn-cancel" onclick="closeCaseSelector()">取消</button>
|
|
350
|
+
<button class="btn btn-primary" onclick="confirmCaseSelection()">确定</button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<!-- 保存用例弹窗 -->
|
|
356
|
+
<div class="modal-overlay" id="save-case-modal" style="display:none" onclick="closeSaveCaseOnOverlay(event)">
|
|
357
|
+
<div class="modal" style="max-width:500px">
|
|
358
|
+
<div class="modal-header">
|
|
359
|
+
<h3 id="save-case-title">保存测试用例</h3>
|
|
360
|
+
<button class="close-btn" onclick="closeSaveCaseDialog()">✕</button>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="modal-body">
|
|
363
|
+
<div class="save-case-form">
|
|
364
|
+
<div class="form-group">
|
|
365
|
+
<label>用例名称</label>
|
|
366
|
+
<input id="save-case-name" placeholder="例如:登录测试">
|
|
367
|
+
</div>
|
|
368
|
+
<div class="form-group">
|
|
369
|
+
<label>所属文件夹</label>
|
|
370
|
+
<select id="save-case-folder">
|
|
371
|
+
<option value="">加载中...</option>
|
|
372
|
+
</select>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="modal-footer">
|
|
377
|
+
<button class="btn btn-cancel" onclick="closeSaveCaseDialog()">取消</button>
|
|
378
|
+
<button class="btn btn-primary" onclick="handleSaveCase()">保存</button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<script>
|
|
384
|
+
// ===== 调试页面状态 =====
|
|
385
|
+
const debugState = {
|
|
386
|
+
isExecuting: false,
|
|
387
|
+
currentTaskId: null,
|
|
388
|
+
logs: [],
|
|
389
|
+
reportPath: '',
|
|
390
|
+
ws: null,
|
|
391
|
+
wsConnected: false,
|
|
392
|
+
// 配置
|
|
393
|
+
runMode: 'device',
|
|
394
|
+
platform: 'android',
|
|
395
|
+
selectedDevice: '',
|
|
396
|
+
devices: [],
|
|
397
|
+
// 用例
|
|
398
|
+
currentTestCase: null,
|
|
399
|
+
folderTree: [],
|
|
400
|
+
selectedFolderId: null,
|
|
401
|
+
// 历史
|
|
402
|
+
history: [],
|
|
403
|
+
// 实时报告
|
|
404
|
+
reportData: null, // { dump, sessionId, deviceId }
|
|
405
|
+
executions: [], // 解析后的执行步骤
|
|
406
|
+
activeStepIndex: -1,
|
|
407
|
+
reportPanelVisible: false,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// 初始化加载历史
|
|
411
|
+
(function(){
|
|
412
|
+
try {
|
|
413
|
+
const saved = localStorage.getItem('debug_history');
|
|
414
|
+
if(saved) debugState.history = JSON.parse(saved);
|
|
415
|
+
} catch(e){}
|
|
416
|
+
})();
|
|
417
|
+
|
|
418
|
+
// ===== 运行模式切换 =====
|
|
419
|
+
function onRunModeChange() {
|
|
420
|
+
debugState.runMode = document.getElementById('debug-runMode').value;
|
|
421
|
+
document.getElementById('debug-device-section').style.display = debugState.runMode === 'device' ? '' : 'none';
|
|
422
|
+
document.getElementById('debug-browser-options').style.display = debugState.runMode === 'browser' ? '' : 'none';
|
|
423
|
+
if(debugState.runMode === 'device') refreshDevices();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function onPlatformChange() {
|
|
427
|
+
debugState.platform = document.getElementById('debug-platform').value;
|
|
428
|
+
const label = document.getElementById('debug-package-label');
|
|
429
|
+
label.textContent = debugState.platform === 'android' ? '包名' : 'Bundle ID';
|
|
430
|
+
document.getElementById('debug-package-preset').querySelector('option[value=custom]').textContent = '自定义';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function onPackagePresetChange() {
|
|
434
|
+
const v = document.getElementById('debug-package-preset').value;
|
|
435
|
+
const custom = document.getElementById('debug-package-custom');
|
|
436
|
+
if(v === 'custom') {
|
|
437
|
+
custom.style.display = '';
|
|
438
|
+
custom.focus();
|
|
439
|
+
} else {
|
|
440
|
+
custom.style.display = 'none';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function syncPackageValue() {}
|
|
445
|
+
|
|
446
|
+
function getPackageName() {
|
|
447
|
+
const preset = document.getElementById('debug-package-preset').value;
|
|
448
|
+
if(preset === 'custom') return document.getElementById('debug-package-custom').value;
|
|
449
|
+
return preset;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ===== 设备加载 =====
|
|
453
|
+
async function refreshDevices() {
|
|
454
|
+
try {
|
|
455
|
+
const devs = await (await fetch('/api/debug/devices')).json();
|
|
456
|
+
debugState.devices = devs || [];
|
|
457
|
+
const sel = document.getElementById('debug-device');
|
|
458
|
+
sel.innerHTML = '';
|
|
459
|
+
if(debugState.devices.length === 0) {
|
|
460
|
+
sel.innerHTML = '<option value="">暂无在线设备</option>';
|
|
461
|
+
} else {
|
|
462
|
+
debugState.devices.forEach(d => {
|
|
463
|
+
const opt = document.createElement('option');
|
|
464
|
+
opt.value = d.serialNumber;
|
|
465
|
+
opt.textContent = (d.model || d.serialNumber) + ' (' + d.serialNumber + ')';
|
|
466
|
+
sel.appendChild(opt);
|
|
467
|
+
});
|
|
468
|
+
if(!debugState.selectedDevice && debugState.devices.length > 0) {
|
|
469
|
+
debugState.selectedDevice = debugState.devices[0].serialNumber;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch(e) {
|
|
473
|
+
console.error('加载设备失败:', e);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ===== WebSocket 调试连接 =====
|
|
478
|
+
function getDebugWsUrl() {
|
|
479
|
+
const proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
480
|
+
return proto + (location.hostname || 'localhost') + ':8002';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function connectDebugWs() {
|
|
484
|
+
if(debugState.ws && debugState.ws.readyState === WebSocket.OPEN) return;
|
|
485
|
+
try {
|
|
486
|
+
debugState.ws = new WebSocket(getDebugWsUrl());
|
|
487
|
+
debugState.ws.onopen = () => {
|
|
488
|
+
debugState.wsConnected = true;
|
|
489
|
+
addDebugLog('WebSocket 已连接', 'info');
|
|
490
|
+
};
|
|
491
|
+
debugState.ws.onmessage = (event) => {
|
|
492
|
+
try {
|
|
493
|
+
const msg = JSON.parse(event.data);
|
|
494
|
+
handleDebugWsMessage(msg);
|
|
495
|
+
} catch(e) {}
|
|
496
|
+
};
|
|
497
|
+
debugState.ws.onclose = () => {
|
|
498
|
+
debugState.wsConnected = false;
|
|
499
|
+
addDebugLog('WebSocket 已断开', 'warn');
|
|
500
|
+
setTimeout(() => { if(debugState.isExecuting) connectDebugWs(); }, 3000);
|
|
501
|
+
};
|
|
502
|
+
debugState.ws.onerror = () => {
|
|
503
|
+
debugState.wsConnected = false;
|
|
504
|
+
};
|
|
505
|
+
} catch(e) {
|
|
506
|
+
console.error('WebSocket 连接失败:', e);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function handleDebugWsMessage(msg) {
|
|
511
|
+
switch(msg.type) {
|
|
512
|
+
case 'session_created':
|
|
513
|
+
debugState.currentTaskId = msg.sessionId;
|
|
514
|
+
updateTaskIdDisplay();
|
|
515
|
+
break;
|
|
516
|
+
case 'debug_started':
|
|
517
|
+
addDebugLog('调试会话已启动: ' + msg.sessionId, 'info');
|
|
518
|
+
break;
|
|
519
|
+
case 'log_output':
|
|
520
|
+
addDebugLog(msg.content || '', msg.level === 'error' ? 'error' : 'info');
|
|
521
|
+
break;
|
|
522
|
+
case 'debug_completed':
|
|
523
|
+
debugState.isExecuting = false;
|
|
524
|
+
updateExecuteButton();
|
|
525
|
+
addDebugLog('调试执行完成' + (msg.success ? ' (成功)' : ' (失败)'), msg.success ? 'success' : 'error');
|
|
526
|
+
saveToHistory(msg.success ? 'success' : 'failed');
|
|
527
|
+
// 最终报告更新
|
|
528
|
+
if(msg.dump) {
|
|
529
|
+
updateReportDump(msg.dump, msg.sessionId);
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
case 'debug_error':
|
|
533
|
+
debugState.isExecuting = false;
|
|
534
|
+
updateExecuteButton();
|
|
535
|
+
addDebugLog('调试错误: ' + (msg.error || ''), 'error');
|
|
536
|
+
saveToHistory('failed');
|
|
537
|
+
break;
|
|
538
|
+
case 'action_result':
|
|
539
|
+
addDebugLog('动作执行' + (msg.success ? '成功' : '失败'), msg.success ? 'success' : 'error');
|
|
540
|
+
// 更新报告数据
|
|
541
|
+
if(msg.dump) {
|
|
542
|
+
updateReportDump(msg.dump, msg.sessionId);
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
case 'action_dump':
|
|
546
|
+
// 实时dump数据更新
|
|
547
|
+
if(msg.dump) {
|
|
548
|
+
updateReportDump(msg.dump, msg.sessionId);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
case 'task_log':
|
|
552
|
+
addDebugLog(msg.content || '', msg.level || 'info');
|
|
553
|
+
break;
|
|
554
|
+
default:
|
|
555
|
+
// 其他消息忽略
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
// 检查报告路径
|
|
559
|
+
if(msg.content && typeof msg.content === 'string') {
|
|
560
|
+
const reportMatch = msg.content.match(/Midscene - report file updated:\\s*(\\/[^\\s]+\\.html)/g);
|
|
561
|
+
if(reportMatch && reportMatch.length > 0) {
|
|
562
|
+
debugState.reportPath = reportMatch[reportMatch.length - 1].replace(/Midscene - report file updated:\\s*/, '');
|
|
563
|
+
document.getElementById('debug-report-btn').style.display = '';
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ===== 调试执行 =====
|
|
569
|
+
async function handleDebugExecute() {
|
|
570
|
+
const script = document.getElementById('debug-script').value.trim();
|
|
571
|
+
if(!script || script === '// 在此编写调试脚本') {
|
|
572
|
+
alert('请输入脚本内容');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const runMode = document.getElementById('debug-runMode').value;
|
|
577
|
+
const deviceId = runMode === 'device' ? document.getElementById('debug-device').value : undefined;
|
|
578
|
+
|
|
579
|
+
if(runMode === 'device' && !deviceId) {
|
|
580
|
+
alert('请选择设备');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
debugState.isExecuting = true;
|
|
585
|
+
debugState.logs = [];
|
|
586
|
+
debugState.reportPath = '';
|
|
587
|
+
clearTerminal();
|
|
588
|
+
updateExecuteButton();
|
|
589
|
+
addDebugLog('开始执行 (' + (runMode === 'device' ? '真机' : '浏览器') + ')...', 'info');
|
|
590
|
+
|
|
591
|
+
// 连接WebSocket
|
|
592
|
+
connectDebugWs();
|
|
593
|
+
|
|
594
|
+
// 等待连接就绪
|
|
595
|
+
const waitForWs = () => new Promise(resolve => {
|
|
596
|
+
if(debugState.wsConnected) return resolve();
|
|
597
|
+
const check = setInterval(() => {
|
|
598
|
+
if(debugState.wsConnected) { clearInterval(check); resolve(); }
|
|
599
|
+
}, 100);
|
|
600
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await waitForWs();
|
|
604
|
+
|
|
605
|
+
if(!debugState.wsConnected) {
|
|
606
|
+
addDebugLog('WebSocket 连接失败,请检查服务是否运行', 'error');
|
|
607
|
+
debugState.isExecuting = false;
|
|
608
|
+
updateExecuteButton();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 发送调试请求
|
|
613
|
+
const request = {
|
|
614
|
+
type: 'start_debug',
|
|
615
|
+
naturalLanguage: script,
|
|
616
|
+
runMode: runMode,
|
|
617
|
+
deviceId: deviceId,
|
|
618
|
+
url: document.getElementById('debug-url').value || undefined,
|
|
619
|
+
platform: runMode === 'device' ? document.getElementById('debug-platform').value : undefined,
|
|
620
|
+
packageName: runMode === 'device' ? getPackageName() : undefined,
|
|
621
|
+
mobileMode: document.getElementById('debug-mobileMode')?.checked || false,
|
|
622
|
+
isRnUrl: false,
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
debugState.ws.send(JSON.stringify(request));
|
|
626
|
+
addDebugLog('调试请求已发送', 'info');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function handleDebugStop() {
|
|
630
|
+
if(debugState.currentTaskId && debugState.ws && debugState.wsConnected) {
|
|
631
|
+
debugState.ws.send(JSON.stringify({
|
|
632
|
+
type: 'stop_debug',
|
|
633
|
+
sessionId: debugState.currentTaskId,
|
|
634
|
+
}));
|
|
635
|
+
addDebugLog('正在停止执行...', 'info');
|
|
636
|
+
}
|
|
637
|
+
debugState.isExecuting = false;
|
|
638
|
+
debugState.currentTaskId = null;
|
|
639
|
+
updateExecuteButton();
|
|
640
|
+
updateTaskIdDisplay();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function updateExecuteButton() {
|
|
644
|
+
const btn = document.getElementById('debug-execute-btn');
|
|
645
|
+
if(debugState.isExecuting) {
|
|
646
|
+
btn.innerHTML = '■ 停止任务';
|
|
647
|
+
btn.className = 'btn-icon btn-danger';
|
|
648
|
+
btn.style = 'width:100%;justify-content:center;padding:10px';
|
|
649
|
+
btn.onclick = handleDebugStop;
|
|
650
|
+
document.getElementById('debug-exec-status').innerHTML = '<span class="executing"><span class="spinner"></span> 执行中...</span>';
|
|
651
|
+
} else {
|
|
652
|
+
btn.innerHTML = '▶ 运行任务';
|
|
653
|
+
btn.className = 'btn-icon btn-blue';
|
|
654
|
+
btn.style = 'width:100%;justify-content:center;padding:10px';
|
|
655
|
+
btn.onclick = handleDebugExecute;
|
|
656
|
+
document.getElementById('debug-exec-status').innerHTML = '';
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function updateTaskIdDisplay() {
|
|
661
|
+
const el = document.getElementById('debug-task-id');
|
|
662
|
+
el.textContent = debugState.currentTaskId ? '任务ID: ' + debugState.currentTaskId.slice(0, 12) : '';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ===== 终端输出 =====
|
|
666
|
+
function addDebugLog(message, level) {
|
|
667
|
+
const time = new Date().toLocaleTimeString();
|
|
668
|
+
debugState.logs.push({ message, level, time });
|
|
669
|
+
|
|
670
|
+
const body = document.getElementById('debug-terminal-body');
|
|
671
|
+
// 第一次添加时清除占位
|
|
672
|
+
if(debugState.logs.length === 1) body.innerHTML = '';
|
|
673
|
+
|
|
674
|
+
const div = document.createElement('div');
|
|
675
|
+
div.className = 'log-line';
|
|
676
|
+
const levelClass = level === 'error' ? 'log-error' : level === 'warn' ? 'log-warn' : level === 'success' ? 'log-success' : 'log-info';
|
|
677
|
+
div.innerHTML = '<span class="log-time">' + escHtml(time) + '</span><span class="' + levelClass + '">[' + (level || 'info').toUpperCase() + ']</span> ' + escHtml(message);
|
|
678
|
+
body.appendChild(div);
|
|
679
|
+
body.scrollTop = body.scrollHeight;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function clearTerminal() {
|
|
683
|
+
debugState.logs = [];
|
|
684
|
+
debugState.reportPath = '';
|
|
685
|
+
debugState.reportData = null;
|
|
686
|
+
debugState.executions = [];
|
|
687
|
+
debugState.activeStepIndex = -1;
|
|
688
|
+
const reportBody = document.getElementById('debug-report-body');
|
|
689
|
+
if(reportBody) {
|
|
690
|
+
// 恢复子元素结构(不能直接清空innerHTML,否则后续getElementById找不到)
|
|
691
|
+
reportBody.innerHTML = '<div class="debug-report-timeline" id="report-timeline"></div><div class="debug-report-detail" id="report-detail"><div class="rrd-empty">等待执行步骤...</div></div>';
|
|
692
|
+
}
|
|
693
|
+
const reportPanel = document.getElementById('debug-report-panel');
|
|
694
|
+
if(reportPanel) reportPanel.classList.add('hidden');
|
|
695
|
+
debugState.reportPanelVisible = false;
|
|
696
|
+
const reportBtn = document.getElementById('debug-report-btn');
|
|
697
|
+
if(reportBtn) reportBtn.style.display = 'none';
|
|
698
|
+
document.getElementById('debug-terminal-body').innerHTML = '<div style="text-align:center;color:#64748b;padding:40px 20px"><div style="font-size:32px;margin-bottom:8px;opacity:0.5">💻</div><div style="font-style:italic">暂无输出...</div><div style="font-size:11px;margin-top:8px;color:#475569">运行任务后,输出将显示在这里</div></div>';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function toggleTerminal() {
|
|
702
|
+
const t = document.getElementById('debug-terminal');
|
|
703
|
+
t.style.display = t.style.display === 'none' ? '' : 'none';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ===== 实时报告 =====
|
|
707
|
+
function toggleReportPanel() {
|
|
708
|
+
const panel = document.getElementById('debug-report-panel');
|
|
709
|
+
debugState.reportPanelVisible = !debugState.reportPanelVisible;
|
|
710
|
+
if(debugState.reportPanelVisible) {
|
|
711
|
+
panel.classList.remove('hidden');
|
|
712
|
+
} else {
|
|
713
|
+
panel.classList.add('hidden');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function updateReportDump(dumpStr, sessionId) {
|
|
718
|
+
try {
|
|
719
|
+
// 显示报告面板
|
|
720
|
+
if(!debugState.reportPanelVisible) {
|
|
721
|
+
const panel = document.getElementById('debug-report-panel');
|
|
722
|
+
panel.classList.remove('hidden');
|
|
723
|
+
debugState.reportPanelVisible = true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 解析 dump
|
|
727
|
+
let dump = dumpStr;
|
|
728
|
+
if(typeof dump === 'string') {
|
|
729
|
+
try { dump = JSON.parse(dump); } catch(e) { return; }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const executions = dump.executions || [];
|
|
733
|
+
debugState.reportData = { dump, sessionId };
|
|
734
|
+
debugState.executions = executions;
|
|
735
|
+
|
|
736
|
+
// 自动选中最新步骤
|
|
737
|
+
if(executions.length > 0 && debugState.activeStepIndex === -1) {
|
|
738
|
+
debugState.activeStepIndex = executions.length - 1;
|
|
739
|
+
} else if(executions.length > (debugState.executions.length || 0)) {
|
|
740
|
+
debugState.activeStepIndex = executions.length - 1;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
renderReportTimeline();
|
|
744
|
+
renderStepDetail();
|
|
745
|
+
renderReportProgress();
|
|
746
|
+
} catch(e) {
|
|
747
|
+
console.error('更新报告失败:', e);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function getExecutionStatus(exec) {
|
|
752
|
+
if(!exec || !exec.tasks || !exec.tasks.length) return 'pending';
|
|
753
|
+
const statuses = exec.tasks.map(t => t.status);
|
|
754
|
+
if(statuses.some(s => s === 'failed')) return 'failed';
|
|
755
|
+
if(statuses.some(s => s === 'running')) return 'running';
|
|
756
|
+
if(statuses.every(s => s === 'finished')) return 'finished';
|
|
757
|
+
return 'pending';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getTaskTypeCategory(exec) {
|
|
761
|
+
const mainTask = exec.tasks && exec.tasks[0];
|
|
762
|
+
if(!mainTask) return 'other';
|
|
763
|
+
if(mainTask.type === 'Insight') return 'insight';
|
|
764
|
+
if(mainTask.type === 'Planning') return 'planning';
|
|
765
|
+
if(mainTask.type === 'Action Space') return 'action';
|
|
766
|
+
return 'other';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function getTaskTypeLabel(exec) {
|
|
770
|
+
const mainTask = exec.tasks && exec.tasks[0];
|
|
771
|
+
if(!mainTask) return '未知';
|
|
772
|
+
if(mainTask.subType) return mainTask.subType;
|
|
773
|
+
if(mainTask.type === 'Insight') return '洞察';
|
|
774
|
+
if(mainTask.type === 'Planning') return '规划';
|
|
775
|
+
if(mainTask.type === 'Action Space') return '操作';
|
|
776
|
+
return mainTask.type || '未知';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function formatDuration(ms) {
|
|
780
|
+
if(!ms) return '';
|
|
781
|
+
if(ms < 1000) return ms + 'ms';
|
|
782
|
+
if(ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
783
|
+
return Math.floor(ms / 60000) + 'm' + Math.floor((ms % 60000) / 1000) + 's';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function formatTimestamp(ts) {
|
|
787
|
+
if(!ts) return '-';
|
|
788
|
+
const d = new Date(ts);
|
|
789
|
+
return d.toLocaleString('zh-CN', { hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function renderReportProgress() {
|
|
793
|
+
const el = document.getElementById('report-progress');
|
|
794
|
+
const executions = debugState.executions || [];
|
|
795
|
+
if(!executions.length) { el.innerHTML = ''; return; }
|
|
796
|
+
const finished = executions.filter(e => getExecutionStatus(e) === 'finished').length;
|
|
797
|
+
const total = executions.length;
|
|
798
|
+
const pct = Math.round((finished / total) * 100);
|
|
799
|
+
el.innerHTML = '<div class="rrd-progress-bar"><div class="rrd-progress-fill" style="width:' + pct + '%"></div></div><span>' + finished + '/' + total + '</span>';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function renderReportTimeline() {
|
|
803
|
+
const container = document.getElementById('report-timeline');
|
|
804
|
+
const executions = debugState.executions || [];
|
|
805
|
+
if(!executions.length) {
|
|
806
|
+
container.innerHTML = '<div class="rrd-empty">等待执行步骤...</div>';
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
container.innerHTML = executions.map((exec, i) => {
|
|
810
|
+
const status = getExecutionStatus(exec);
|
|
811
|
+
const typeCat = getTaskTypeCategory(exec);
|
|
812
|
+
const typeLabel = getTaskTypeLabel(exec);
|
|
813
|
+
const isActive = i === debugState.activeStepIndex;
|
|
814
|
+
const totalCost = (exec.tasks || []).reduce((s, t) => s + (t.timing && t.timing.cost || 0), 0);
|
|
815
|
+
const dotIcon = status === 'finished' ? '✓' : status === 'running' ? '●' : status === 'failed' ? '✗' : '●';
|
|
816
|
+
return '<div class="rtl-step' + (isActive ? ' active' : '') + '" onclick="selectReportStep(' + i + ')">' +
|
|
817
|
+
'<div class="rtl-step-dot status-' + status + '">' + dotIcon + '</div>' +
|
|
818
|
+
'<div class="rtl-step-info">' +
|
|
819
|
+
'<div class="rtl-step-name" title="' + escHtml(exec.name || '步骤 ' + (i+1)) + '">' + escHtml(exec.name || '步骤 ' + (i+1)) + '</div>' +
|
|
820
|
+
'<div class="rtl-step-meta">' +
|
|
821
|
+
'<span class="rtl-step-type type-' + typeCat + '">' + typeLabel + '</span>' +
|
|
822
|
+
(totalCost > 0 ? '<span>' + formatDuration(totalCost) + '</span>' : '') +
|
|
823
|
+
(exec.tasks && exec.tasks.length > 1 ? '<span>' + exec.tasks.length + '子任务</span>' : '') +
|
|
824
|
+
'</div>' +
|
|
825
|
+
'</div>' +
|
|
826
|
+
'</div>';
|
|
827
|
+
}).join('');
|
|
828
|
+
|
|
829
|
+
// 自动滚动到底部
|
|
830
|
+
container.scrollTop = container.scrollHeight;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function selectReportStep(index) {
|
|
834
|
+
debugState.activeStepIndex = index;
|
|
835
|
+
renderReportTimeline();
|
|
836
|
+
renderStepDetail();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function renderStepDetail() {
|
|
840
|
+
const container = document.getElementById('report-detail');
|
|
841
|
+
const index = debugState.activeStepIndex;
|
|
842
|
+
const executions = debugState.executions || [];
|
|
843
|
+
if(index < 0 || index >= executions.length) {
|
|
844
|
+
container.innerHTML = '<div class="rrd-empty">请选择一个步骤查看详情</div>';
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const exec = executions[index];
|
|
848
|
+
const firstTask = exec.tasks && exec.tasks[0];
|
|
849
|
+
const lastTask = exec.tasks && exec.tasks[exec.tasks.length - 1];
|
|
850
|
+
const totalCost = (exec.tasks || []).reduce((s, t) => s + (t.timing && t.timing.cost || 0), 0);
|
|
851
|
+
const startTime = firstTask && firstTask.timing && firstTask.timing.start;
|
|
852
|
+
const endTime = lastTask && lastTask.timing && lastTask.timing.end;
|
|
853
|
+
const mainType = firstTask && firstTask.type;
|
|
854
|
+
const mainSubType = firstTask && firstTask.subType;
|
|
855
|
+
|
|
856
|
+
// 提取AI思考内容
|
|
857
|
+
let thoughtContent = '';
|
|
858
|
+
for(const task of exec.tasks || []) {
|
|
859
|
+
if(task.thought) { thoughtContent = task.thought; break; }
|
|
860
|
+
if(task.log && task.log.data && task.log.data.formatResponse) {
|
|
861
|
+
try {
|
|
862
|
+
const parsed = JSON.parse(task.log.data.formatResponse);
|
|
863
|
+
if(parsed.thought) { thoughtContent = parsed.thought; break; }
|
|
864
|
+
} catch(e) {}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
let html = '<div class="rrd-header"><div class="rrd-index">' + (index + 1) + '</div><div class="rrd-name">' + escHtml(exec.name || '步骤 ' + (index+1)) + '</div></div>';
|
|
869
|
+
|
|
870
|
+
// 基本信息
|
|
871
|
+
html += '<div class="rrd-section"><div class="rrd-section-title">基本信息</div><div class="rrd-info-grid">' +
|
|
872
|
+
'<div class="rrd-info-item"><div class="rrd-info-label">类型</div><div class="rrd-info-value">' + escHtml(mainType || '-') + (mainSubType ? ' / ' + escHtml(mainSubType) : '') + '</div></div>' +
|
|
873
|
+
'<div class="rrd-info-item"><div class="rrd-info-label">子任务数</div><div class="rrd-info-value">' + (exec.tasks ? exec.tasks.length : 0) + '</div></div>' +
|
|
874
|
+
'<div class="rrd-info-item"><div class="rrd-info-label">总耗时</div><div class="rrd-info-value">' + formatDuration(totalCost) + '</div></div>' +
|
|
875
|
+
(startTime ? '<div class="rrd-info-item"><div class="rrd-info-label">开始</div><div class="rrd-info-value">' + formatTimestamp(startTime) + '</div></div>' : '') +
|
|
876
|
+
(endTime ? '<div class="rrd-info-item"><div class="rrd-info-label">结束</div><div class="rrd-info-value">' + formatTimestamp(endTime) + '</div></div>' : '') +
|
|
877
|
+
'</div></div>';
|
|
878
|
+
|
|
879
|
+
// AI思考
|
|
880
|
+
if(thoughtContent) {
|
|
881
|
+
html += '<div class="rrd-section"><div class="rrd-section-title">AI 思考过程</div><div class="rrd-thought">' + escHtml(thoughtContent) + '</div></div>';
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// 任务列表
|
|
885
|
+
html += '<div class="rrd-section"><div class="rrd-section-title">任务详情</div>';
|
|
886
|
+
for(const task of exec.tasks || []) {
|
|
887
|
+
const statusInfo = { finished: { cls: 'status-finished', text: '已完成' }, running: { cls: 'status-running', text: '执行中' }, failed: { cls: 'status-failed', text: '失败' }, pending: { cls: 'status-pending', text: '等待中' } };
|
|
888
|
+
const si = statusInfo[task.status] || statusInfo.pending;
|
|
889
|
+
html += '<div class="rrd-task"><div class="rrd-task-header"><span class="rrd-task-type">' + escHtml(task.type || '') + (task.subType ? ' / ' + escHtml(task.subType) : '') + '</span><span class="rrd-task-status ' + si.cls + '">' + si.text + '</span></div>';
|
|
890
|
+
html += '<div class="rrd-task-body">';
|
|
891
|
+
if(task.timing && task.timing.cost) html += '<div>耗时: ' + formatDuration(task.timing.cost) + '</div>';
|
|
892
|
+
if(task.param) {
|
|
893
|
+
html += '<div class="rrd-task-param">';
|
|
894
|
+
if(task.param.dataDemand) html += '需求: ' + escHtml(task.param.dataDemand) + '\\n';
|
|
895
|
+
if(task.param.locator) html += '定位: ' + escHtml(task.param.locator) + '\\n';
|
|
896
|
+
if(task.param.command) html += '命令: ' + escHtml(task.param.command) + '\\n';
|
|
897
|
+
html += '</div>';
|
|
898
|
+
}
|
|
899
|
+
if(task.output && typeof task.output === 'object') {
|
|
900
|
+
html += '<div class="rrd-task-output">' + escHtml(JSON.stringify(task.output, null, 2)) + '</div>';
|
|
901
|
+
}
|
|
902
|
+
if(task.log && task.log.data && task.log.data.data) {
|
|
903
|
+
html += '<div class="rrd-task-output">断言结果:\\n' + escHtml(JSON.stringify(task.log.data.data, null, 2)) + '</div>';
|
|
904
|
+
}
|
|
905
|
+
if(task.errorMessage) {
|
|
906
|
+
html += '<div class="rrd-task-error">' + escHtml(task.errorMessage) + '</div>';
|
|
907
|
+
}
|
|
908
|
+
if(task.usage) {
|
|
909
|
+
html += '<div style="margin-top:4px;font-size:10px;color:#64748b">模型: ' + escHtml(task.usage.model_name || task.usage.model_description || '-') + ' | Token: ' + (task.usage.prompt_tokens || 0) + '+' + (task.usage.completion_tokens || 0) + '=' + (task.usage.total_tokens || 0) + '</div>';
|
|
910
|
+
}
|
|
911
|
+
// 截图展示 - 图片可点击在新窗口打开
|
|
912
|
+
if(task.uiContext && task.uiContext.screenshot) {
|
|
913
|
+
const ss = task.uiContext.screenshot;
|
|
914
|
+
if(ss.base64) {
|
|
915
|
+
const sizeLabel = ss.width ? ss.width + "x" + (ss.height || "") : "";
|
|
916
|
+
html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.base64) + '" title="点击查看大图" /><div class="rrd-screenshot-label">UI截图 ' + sizeLabel + '</div></div>';
|
|
917
|
+
} else if(ss.url) {
|
|
918
|
+
html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.url) + '" title="点击查看大图" /><div class="rrd-screenshot-label">UI截图</div></div>';
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
html += '</div></div>';
|
|
922
|
+
}
|
|
923
|
+
html += '</div>';
|
|
924
|
+
|
|
925
|
+
container.innerHTML = html;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function openReport() {
|
|
929
|
+
if(debugState.reportPath) {
|
|
930
|
+
window.open('file://' + debugState.reportPath, '_blank');
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ===== 历史记录 =====
|
|
935
|
+
function saveToHistory(status) {
|
|
936
|
+
const item = {
|
|
937
|
+
id: Date.now().toString(),
|
|
938
|
+
timestamp: Date.now(),
|
|
939
|
+
runMode: document.getElementById('debug-runMode').value,
|
|
940
|
+
platform: document.getElementById('debug-platform').value,
|
|
941
|
+
url: document.getElementById('debug-url').value,
|
|
942
|
+
script: document.getElementById('debug-script').value,
|
|
943
|
+
deviceId: document.getElementById('debug-device').value,
|
|
944
|
+
status: status,
|
|
945
|
+
};
|
|
946
|
+
debugState.history.unshift(item);
|
|
947
|
+
if(debugState.history.length > 50) debugState.history = debugState.history.slice(0, 50);
|
|
948
|
+
localStorage.setItem('debug_history', JSON.stringify(debugState.history));
|
|
949
|
+
renderHistory();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function renderHistory() {
|
|
953
|
+
const list = document.getElementById('history-list');
|
|
954
|
+
if(debugState.history.length === 0) {
|
|
955
|
+
list.innerHTML = '<div style="text-align:center;color:#64748b;padding:40px 20px">暂无历史记录</div>';
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
list.innerHTML = debugState.history.map(item => {
|
|
959
|
+
const statusClass = item.status === 'success' ? 'success' : 'failed';
|
|
960
|
+
const statusText = item.status === 'success' ? '成功' : '失败';
|
|
961
|
+
const modeText = item.runMode === 'device' ? '真机' + (item.platform ? ' • ' + item.platform : '') : '浏览器';
|
|
962
|
+
return '<div class="history-item" onclick="restoreFromHistory('' + item.id + '')">' +
|
|
963
|
+
'<div class="meta"><span class="status ' + statusClass + '">' + statusText + '</span><span class="time">' + new Date(item.timestamp).toLocaleTimeString() + '</span></div>' +
|
|
964
|
+
'<div class="url">' + escHtml(item.url || '-') + '</div>' +
|
|
965
|
+
'<div class="detail">' + modeText + '</div></div>';
|
|
966
|
+
}).join('');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function restoreFromHistory(id) {
|
|
970
|
+
const item = debugState.history.find(h => h.id === id);
|
|
971
|
+
if(!item) return;
|
|
972
|
+
document.getElementById('debug-runMode').value = item.runMode || 'device';
|
|
973
|
+
onRunModeChange();
|
|
974
|
+
document.getElementById('debug-platform').value = item.platform || 'android';
|
|
975
|
+
document.getElementById('debug-url').value = item.url || '';
|
|
976
|
+
document.getElementById('debug-script').value = item.script || '';
|
|
977
|
+
if(item.deviceId) {
|
|
978
|
+
document.getElementById('debug-device').value = item.deviceId;
|
|
979
|
+
}
|
|
980
|
+
toggleHistoryDrawer();
|
|
981
|
+
addDebugLog('已恢复历史配置', 'success');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function toggleHistoryDrawer() {
|
|
985
|
+
document.getElementById('history-drawer').classList.toggle('open');
|
|
986
|
+
renderHistory();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ===== 用例选择 =====
|
|
990
|
+
let caseSelectorState = {
|
|
991
|
+
folders: [],
|
|
992
|
+
testCases: [],
|
|
993
|
+
selectedFolderId: null,
|
|
994
|
+
selectedCase: null,
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
async function openCaseSelector() {
|
|
998
|
+
document.getElementById('case-selector-modal').style.display = '';
|
|
999
|
+
caseSelectorState.selectedCase = null;
|
|
1000
|
+
await loadFolderTree();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function closeCaseSelector() {
|
|
1004
|
+
document.getElementById('case-selector-modal').style.display = 'none';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function closeCaseSelectorOnOverlay(e) {
|
|
1008
|
+
if(e.target === e.currentTarget) closeCaseSelector();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function loadFolderTree() {
|
|
1012
|
+
try {
|
|
1013
|
+
const res = await (await fetch('/api/testcases/folder-tree')).json();
|
|
1014
|
+
if(res.success && res.data) {
|
|
1015
|
+
let treeData;
|
|
1016
|
+
if(Array.isArray(res.data)) treeData = res.data;
|
|
1017
|
+
else if(res.data.data && Array.isArray(res.data.data)) treeData = res.data.data;
|
|
1018
|
+
else treeData = [];
|
|
1019
|
+
caseSelectorState.folders = treeData;
|
|
1020
|
+
debugState.folderTree = treeData;
|
|
1021
|
+
renderFolderTree();
|
|
1022
|
+
} else {
|
|
1023
|
+
caseSelectorState.folders = [];
|
|
1024
|
+
renderFolderTree();
|
|
1025
|
+
}
|
|
1026
|
+
} catch(e) {
|
|
1027
|
+
console.error('加载文件夹树失败:', e);
|
|
1028
|
+
caseSelectorState.folders = [];
|
|
1029
|
+
renderFolderTree();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function renderFolderTree() {
|
|
1034
|
+
const list = document.getElementById('case-folder-list');
|
|
1035
|
+
if(caseSelectorState.folders.length === 0) {
|
|
1036
|
+
list.innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">暂无文件夹</div>';
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
list.innerHTML = '<div class="folder-item" onclick="loadAllCases()" style="color:#22c55e">📄 全部用例</div>';
|
|
1040
|
+
caseSelectorState.folders.forEach((folder, idx) => {
|
|
1041
|
+
list.innerHTML += renderFolderNode(folder, 0, 'root-' + idx);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function renderFolderNode(node, level, key) {
|
|
1046
|
+
const info = node.folder || {};
|
|
1047
|
+
const folderId = info.folderId || info.id;
|
|
1048
|
+
const name = (!info.folderName || info.folderName === '未命名文件夹') ? '未分类' : info.folderName;
|
|
1049
|
+
const count = info.configCount || (node.configs ? node.configs.length : 0);
|
|
1050
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
1051
|
+
const isActive = caseSelectorState.selectedFolderId === folderId;
|
|
1052
|
+
const indent = level * 16 + 8;
|
|
1053
|
+
|
|
1054
|
+
let html = '<div class="folder-item' + (isActive ? ' active' : '') + '" style="padding-left:' + indent + 'px" onclick="selectFolder('' + folderId + '', this)">';
|
|
1055
|
+
if(hasChildren) html += '<span class="arrow" onclick="event.stopPropagation();toggleFolder(this)">▶</span>';
|
|
1056
|
+
else html += '<span style="width:10px;display:inline-block"></span>';
|
|
1057
|
+
html += '📁 ' + escHtml(name);
|
|
1058
|
+
if(count > 0) html += ' <span style="color:#64748b;font-size:11px">(' + count + ')</span>';
|
|
1059
|
+
html += '</div>';
|
|
1060
|
+
|
|
1061
|
+
if(hasChildren) {
|
|
1062
|
+
html += '<div class="folder-children" style="display:none">';
|
|
1063
|
+
node.children.forEach((child, idx) => {
|
|
1064
|
+
html += renderFolderNode(child, level + 1, key + '-' + idx);
|
|
1065
|
+
});
|
|
1066
|
+
html += '</div>';
|
|
1067
|
+
}
|
|
1068
|
+
return html;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function toggleFolder(arrow) {
|
|
1072
|
+
arrow.classList.toggle('expanded');
|
|
1073
|
+
const children = arrow.closest('.folder-item').nextElementSibling;
|
|
1074
|
+
if(children && children.classList.contains('folder-children')) {
|
|
1075
|
+
children.style.display = children.style.display === 'none' ? '' : 'none';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function selectFolder(folderId) {
|
|
1080
|
+
caseSelectorState.selectedFolderId = folderId;
|
|
1081
|
+
renderFolderTree();
|
|
1082
|
+
document.getElementById('case-list-title').textContent = '加载中...';
|
|
1083
|
+
document.getElementById('case-list').innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">加载中...</div>';
|
|
1084
|
+
|
|
1085
|
+
try {
|
|
1086
|
+
const res = await (await fetch('/api/testcases/folder/' + folderId)).json();
|
|
1087
|
+
if(res.success && res.data) {
|
|
1088
|
+
let cases;
|
|
1089
|
+
// 后端返回 res.data = response.data,response.data 本身可能是 {data: {configs, folder}, success, message}
|
|
1090
|
+
// 所以需要检查 res.data.data.configs 或 res.data.configs
|
|
1091
|
+
const inner = res.data.data || res.data;
|
|
1092
|
+
if(inner && inner.configs && Array.isArray(inner.configs)) cases = inner.configs;
|
|
1093
|
+
else if(res.data.data && Array.isArray(res.data.data)) cases = res.data.data;
|
|
1094
|
+
else if(Array.isArray(res.data)) cases = res.data;
|
|
1095
|
+
else if(res.data.folder && res.data.folder.configs) cases = res.data.folder.configs;
|
|
1096
|
+
else if(res.data.configs && Array.isArray(res.data.configs)) cases = res.data.configs;
|
|
1097
|
+
else cases = [];
|
|
1098
|
+
caseSelectorState.testCases = cases;
|
|
1099
|
+
} else {
|
|
1100
|
+
caseSelectorState.testCases = [];
|
|
1101
|
+
}
|
|
1102
|
+
} catch(e) {
|
|
1103
|
+
console.error('加载用例列表失败:', e);
|
|
1104
|
+
caseSelectorState.testCases = [];
|
|
1105
|
+
}
|
|
1106
|
+
renderCaseList();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function loadAllCases() {
|
|
1110
|
+
caseSelectorState.selectedFolderId = null;
|
|
1111
|
+
renderFolderTree();
|
|
1112
|
+
document.getElementById('case-list-title').textContent = '全部用例';
|
|
1113
|
+
document.getElementById('case-list').innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">加载中...</div>';
|
|
1114
|
+
|
|
1115
|
+
// 从所有文件夹收集用例
|
|
1116
|
+
const allCases = [];
|
|
1117
|
+
function collectFromTree(nodes) {
|
|
1118
|
+
if(!nodes) return;
|
|
1119
|
+
nodes.forEach(node => {
|
|
1120
|
+
if(node.configs && Array.isArray(node.configs)) allCases.push(...node.configs);
|
|
1121
|
+
if(node.children) collectFromTree(node.children);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
collectFromTree(caseSelectorState.folders);
|
|
1125
|
+
caseSelectorState.testCases = allCases;
|
|
1126
|
+
renderCaseList();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function searchCases() {
|
|
1130
|
+
const keyword = document.getElementById('case-search-input').value.trim();
|
|
1131
|
+
if(!keyword) return;
|
|
1132
|
+
document.getElementById('case-list-title').textContent = '搜索: "' + keyword + '"';
|
|
1133
|
+
document.getElementById('case-list').innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">搜索中...</div>';
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const res = await (await fetch('/api/testcases/search?configName=' + encodeURIComponent(keyword))).json();
|
|
1137
|
+
if(res.success && res.data) {
|
|
1138
|
+
caseSelectorState.testCases = Array.isArray(res.data) ? res.data : [];
|
|
1139
|
+
} else {
|
|
1140
|
+
caseSelectorState.testCases = [];
|
|
1141
|
+
}
|
|
1142
|
+
} catch(e) {
|
|
1143
|
+
caseSelectorState.testCases = [];
|
|
1144
|
+
}
|
|
1145
|
+
renderCaseList();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function renderCaseList() {
|
|
1149
|
+
const list = document.getElementById('case-list');
|
|
1150
|
+
const title = document.getElementById('case-list-title');
|
|
1151
|
+
if(caseSelectorState.testCases.length === 0) {
|
|
1152
|
+
title.textContent = '测试用例 (0)';
|
|
1153
|
+
list.innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">暂无测试用例</div>';
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
title.textContent = '测试用例 (' + caseSelectorState.testCases.length + ')';
|
|
1157
|
+
list.innerHTML = caseSelectorState.testCases.map(tc => {
|
|
1158
|
+
const isSelected = caseSelectorState.selectedCase && String(caseSelectorState.selectedCase.id) === String(tc.id);
|
|
1159
|
+
return '<div class="case-item' + (isSelected ? ' selected' : '') + '" onclick="selectTestCase('' + tc.id + '')">' +
|
|
1160
|
+
'<div class="case-name">' + escHtml(tc.configName || '未命名') + '</div>' +
|
|
1161
|
+
'<div class="case-desc">' + escHtml(tc.description || tc.testUrl || '无描述') + '</div></div>';
|
|
1162
|
+
}).join('');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function selectTestCase(id) {
|
|
1166
|
+
const strId = String(id);
|
|
1167
|
+
caseSelectorState.selectedCase = caseSelectorState.testCases.find(tc => String(tc.id) === strId) || null;
|
|
1168
|
+
renderCaseList();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function confirmCaseSelection() {
|
|
1172
|
+
if(!caseSelectorState.selectedCase) {
|
|
1173
|
+
alert('请选择一个用例');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const tc = caseSelectorState.selectedCase;
|
|
1177
|
+
debugState.currentTestCase = tc;
|
|
1178
|
+
|
|
1179
|
+
// 恢复用例配置
|
|
1180
|
+
const runMode = tc.runMode || (tc.isRealDevice === false ? 'browser' : 'device');
|
|
1181
|
+
document.getElementById('debug-runMode').value = runMode;
|
|
1182
|
+
onRunModeChange();
|
|
1183
|
+
if(tc.platform) document.getElementById('debug-platform').value = tc.platform;
|
|
1184
|
+
if(tc.testUrl) document.getElementById('debug-url').value = tc.testUrl;
|
|
1185
|
+
if(tc.packageName) {
|
|
1186
|
+
const presetSel = document.getElementById('debug-package-preset');
|
|
1187
|
+
const found = Array.from(presetSel.options).find(o => o.value === tc.packageName);
|
|
1188
|
+
if(found) { presetSel.value = tc.packageName; }
|
|
1189
|
+
else { presetSel.value = 'custom'; document.getElementById('debug-package-custom').style.display = ''; document.getElementById('debug-package-custom').value = tc.packageName; }
|
|
1190
|
+
}
|
|
1191
|
+
if(tc.mobileMode !== undefined) document.getElementById('debug-mobileMode').checked = tc.mobileMode;
|
|
1192
|
+
if(tc.naturalLanguage) {
|
|
1193
|
+
document.getElementById('debug-script').value = tc.naturalLanguage;
|
|
1194
|
+
}
|
|
1195
|
+
if(tc.loginUsername) document.getElementById('debug-loginUser').value = tc.loginUsername;
|
|
1196
|
+
if(tc.loginPassword) document.getElementById('debug-loginPass').value = tc.loginPassword;
|
|
1197
|
+
|
|
1198
|
+
closeCaseSelector();
|
|
1199
|
+
addDebugLog('已加载用例: ' + tc.configName, 'success');
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ===== 保存用例 =====
|
|
1203
|
+
function openSaveCaseDialog() {
|
|
1204
|
+
if(debugState.currentTestCase) {
|
|
1205
|
+
document.getElementById('save-case-title').textContent = '修改测试用例';
|
|
1206
|
+
document.getElementById('save-case-name').value = debugState.currentTestCase.configName || '';
|
|
1207
|
+
} else {
|
|
1208
|
+
document.getElementById('save-case-title').textContent = '保存测试用例';
|
|
1209
|
+
document.getElementById('save-case-name').value = '';
|
|
1210
|
+
}
|
|
1211
|
+
document.getElementById('save-case-modal').style.display = '';
|
|
1212
|
+
loadSaveFolderOptions();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function closeSaveCaseDialog() {
|
|
1216
|
+
document.getElementById('save-case-modal').style.display = 'none';
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function closeSaveCaseOnOverlay(e) {
|
|
1220
|
+
if(e.target === e.currentTarget) closeSaveCaseDialog();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function loadSaveFolderOptions() {
|
|
1224
|
+
const sel = document.getElementById('save-case-folder');
|
|
1225
|
+
sel.innerHTML = '<option value="">请选择文件夹</option>';
|
|
1226
|
+
function addFolders(nodes, prefix) {
|
|
1227
|
+
if(!nodes) return;
|
|
1228
|
+
nodes.forEach(node => {
|
|
1229
|
+
const info = node.folder || {};
|
|
1230
|
+
const fid = info.folderId || info.id;
|
|
1231
|
+
const name = info.folderName || '未分类';
|
|
1232
|
+
if(fid) {
|
|
1233
|
+
const opt = document.createElement('option');
|
|
1234
|
+
opt.value = fid;
|
|
1235
|
+
opt.textContent = prefix + name;
|
|
1236
|
+
sel.appendChild(opt);
|
|
1237
|
+
}
|
|
1238
|
+
if(node.children) addFolders(node.children, prefix + ' ');
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
addFolders(debugState.folderTree, '');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function handleSaveCase() {
|
|
1245
|
+
const name = document.getElementById('save-case-name').value.trim();
|
|
1246
|
+
const folderId = parseInt(document.getElementById('save-case-folder').value);
|
|
1247
|
+
|
|
1248
|
+
if(!name) { alert('请输入用例名称'); return; }
|
|
1249
|
+
if(!folderId) { alert('请选择文件夹'); return; }
|
|
1250
|
+
|
|
1251
|
+
const runMode = document.getElementById('debug-runMode').value;
|
|
1252
|
+
const testCaseData = {
|
|
1253
|
+
id: debugState.currentTestCase?.id || undefined,
|
|
1254
|
+
configName: name,
|
|
1255
|
+
folderId: folderId,
|
|
1256
|
+
testUrl: document.getElementById('debug-url').value || '',
|
|
1257
|
+
naturalLanguage: document.getElementById('debug-script').value || '',
|
|
1258
|
+
description: document.getElementById('debug-script').value.substring(0, 200),
|
|
1259
|
+
runMode: runMode,
|
|
1260
|
+
platform: runMode === 'device' ? document.getElementById('debug-platform').value : undefined,
|
|
1261
|
+
packageName: runMode === 'device' ? getPackageName() : undefined,
|
|
1262
|
+
mobileMode: document.getElementById('debug-mobileMode')?.checked || false,
|
|
1263
|
+
isRnUrl: false,
|
|
1264
|
+
loginUsername: document.getElementById('debug-loginUser').value || undefined,
|
|
1265
|
+
loginPassword: document.getElementById('debug-loginPass').value || undefined,
|
|
1266
|
+
loginUrl: '',
|
|
1267
|
+
// 后端必需字段
|
|
1268
|
+
status: 1,
|
|
1269
|
+
windowWidth: 1920,
|
|
1270
|
+
windowHeight: 1080,
|
|
1271
|
+
rollingDistance: 300,
|
|
1272
|
+
waitTime: 5000,
|
|
1273
|
+
implicitlyWait: 10000,
|
|
1274
|
+
isRealDevice: runMode === 'device',
|
|
1275
|
+
headlessMode: false,
|
|
1276
|
+
cdpPort: 9222,
|
|
1277
|
+
configType: 'SIMPLE',
|
|
1278
|
+
expectedDuration: 30000,
|
|
1279
|
+
scriptType: 'SELENIUM',
|
|
1280
|
+
extensionFields: '{}',
|
|
1281
|
+
deepId: '',
|
|
1282
|
+
deepMap: '',
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
let res;
|
|
1287
|
+
if(testCaseData.id) {
|
|
1288
|
+
res = await (await fetch('/api/testcases/' + testCaseData.id, {
|
|
1289
|
+
method: 'PUT',
|
|
1290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1291
|
+
body: JSON.stringify(testCaseData),
|
|
1292
|
+
})).json();
|
|
1293
|
+
} else {
|
|
1294
|
+
res = await (await fetch('/api/testcases', {
|
|
1295
|
+
method: 'POST',
|
|
1296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1297
|
+
body: JSON.stringify(testCaseData),
|
|
1298
|
+
})).json();
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if(res.success) {
|
|
1302
|
+
addDebugLog('用例' + (testCaseData.id ? '修改' : '保存') + '成功: ' + name, 'success');
|
|
1303
|
+
} else {
|
|
1304
|
+
addDebugLog('用例保存失败: ' + (res.message || '未知错误'), 'error');
|
|
1305
|
+
}
|
|
1306
|
+
} catch(e) {
|
|
1307
|
+
addDebugLog('用例保存失败: ' + e.message, 'error');
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
closeSaveCaseDialog();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ===== 生成脚本 =====
|
|
1314
|
+
function handleGenerateScript() {
|
|
1315
|
+
const script = document.getElementById('debug-script').value.trim();
|
|
1316
|
+
if(!script) {
|
|
1317
|
+
alert('请先输入自然语言描述');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
// 提示用户:脚本生成功能需要配置AI服务
|
|
1321
|
+
addDebugLog('脚本生成功能:请确保已配置AI模型,当前脚本将作为自然语言输入生成自动化脚本', 'info');
|
|
1322
|
+
|
|
1323
|
+
// 将当前脚本内容包装为标准的midscene脚本
|
|
1324
|
+
const generatedScript = generateScriptFromNaturalLanguage(script);
|
|
1325
|
+
document.getElementById('debug-script').value = generatedScript;
|
|
1326
|
+
addDebugLog('脚本已生成', 'success');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function generateScriptFromNaturalLanguage(naturalLanguage) {
|
|
1330
|
+
var runMode = document.getElementById('debug-runMode').value;
|
|
1331
|
+
var platform = document.getElementById('debug-platform').value;
|
|
1332
|
+
var url = document.getElementById('debug-url').value;
|
|
1333
|
+
var deviceId = document.getElementById('debug-device').value;
|
|
1334
|
+
var packageName = getPackageName();
|
|
1335
|
+
|
|
1336
|
+
if(runMode === 'browser') {
|
|
1337
|
+
var lines = [];
|
|
1338
|
+
lines.push('// 自动生成的浏览器测试脚本');
|
|
1339
|
+
lines.push("const { PlaywrightAiFixture } = require('@midscene/web/playwright');");
|
|
1340
|
+
lines.push("const { test, expect } = require('@playwright/test');");
|
|
1341
|
+
lines.push('');
|
|
1342
|
+
lines.push("test('AI自动化测试', async ({ page }) => {");
|
|
1343
|
+
lines.push(' const ai = new PlaywrightAiFixture(page);');
|
|
1344
|
+
if(url) lines.push(" await page.goto('" + url + "');");
|
|
1345
|
+
lines.push(' // 自然语言指令:');
|
|
1346
|
+
lines.push(' // ' + naturalLanguage);
|
|
1347
|
+
lines.push(" await ai.aiAction('" + naturalLanguage.replace(/'/g, "\\'") + "');");
|
|
1348
|
+
lines.push('});');
|
|
1349
|
+
return lines.join('\\n');
|
|
1350
|
+
} else {
|
|
1351
|
+
var lines = [];
|
|
1352
|
+
lines.push('// 自动生成的' + (platform === 'ios' ? 'iOS' : 'Android') + '测试脚本');
|
|
1353
|
+
lines.push("const { AndroidAgent } = require('@aiscene/android');");
|
|
1354
|
+
lines.push("// const { IOSAgent } = require('@midscene/ios');");
|
|
1355
|
+
lines.push('');
|
|
1356
|
+
lines.push('async function main() {');
|
|
1357
|
+
lines.push(' const agent = new AndroidAgent();');
|
|
1358
|
+
if(deviceId) lines.push(' // 设备: ' + deviceId);
|
|
1359
|
+
if(packageName) lines.push(' // 包名: ' + packageName);
|
|
1360
|
+
if(url) lines.push(' // URL: ' + url);
|
|
1361
|
+
lines.push(' // 自然语言指令:');
|
|
1362
|
+
lines.push(' // ' + naturalLanguage);
|
|
1363
|
+
lines.push(" await agent.aiAction('" + naturalLanguage.replace(/'/g, "\\'") + "');");
|
|
1364
|
+
lines.push('}');
|
|
1365
|
+
lines.push('');
|
|
1366
|
+
lines.push('main().catch(console.error);');
|
|
1367
|
+
return lines.join('\\n');
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ===== 清空编辑器 =====
|
|
1372
|
+
function handleClearEditor() {
|
|
1373
|
+
document.getElementById('debug-script').value = '';
|
|
1374
|
+
debugState.currentTestCase = null;
|
|
1375
|
+
addDebugLog('编辑器已清空', 'info');
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// ===== 工具函数 =====
|
|
1379
|
+
function escHtml(s) {
|
|
1380
|
+
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// 初始化
|
|
1384
|
+
refreshDevices();
|
|
1385
|
+
</script>
|
|
1386
|
+
`;
|
|
1387
|
+
}
|
|
1388
|
+
//# sourceMappingURL=debug-page.js.map
|