@aiscene/aiserver 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,7 +38,7 @@ html,body{overflow:hidden;margin:0;padding:0}
38
38
  .debug-terminal-body .log-error{color:#dc2626}
39
39
  .debug-terminal-body .log-success{color:#16a34a}
40
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}
41
+ .debug-report-panel{width:640px;min-width:520px;max-width:800px;border-left:1px solid #e2e8f0;display:flex;flex-direction:column;background:#fff}
42
42
  .debug-report-panel.hidden{display:none}
43
43
  .debug-report-header{padding:8px 16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;background:#fff}
44
44
  .debug-report-header .title{font-size:11px;color:#1e293b;display:flex;align-items:center;gap:6px;font-weight:600}
@@ -88,10 +88,11 @@ html,body{overflow:hidden;margin:0;padding:0}
88
88
  .rrd-progress-fill{height:100%;background:#2563eb;border-radius:2px;transition:width .3s}
89
89
  .rrd-empty{text-align:center;color:#94a3b8;padding:40px 20px;font-size:13px}
90
90
  /* 截图展示 */
91
- .rrd-screenshot{margin-top:8px;border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;background:#fff}
91
+ .rrd-screenshot{margin-top:8px;border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;background:#fff;position:relative}
92
92
  .rrd-screenshot img{width:100%;display:block;cursor:pointer;transition:opacity .2s}
93
93
  .rrd-screenshot img:hover{opacity:0.9}
94
94
  .rrd-screenshot-label{font-size:10px;color:#64748b;padding:4px 8px;text-align:center}
95
+ .rrd-screenshot-canvas{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
95
96
  .debug-statusbar{height:28px;background:#fff;border-top:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;padding:0 16px}
96
97
  .debug-statusbar .left{display:flex;align-items:center;gap:12px;font-size:11px;color:#64748b}
97
98
  .debug-statusbar .right{display:flex;align-items:center;gap:8px}
@@ -163,27 +164,20 @@ html,body{overflow:hidden;margin:0;padding:0}
163
164
  .btn-blue:hover{background:#1d4ed8}
164
165
 
165
166
  /* AI生成脚本对话框样式 */
166
- .ai-generator-panel{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid #e2e8f0;z-index:100;transition:transform .3s;transform:translateY(calc(100% - 44px))}
167
- .ai-generator-panel.expanded{transform:translateY(0)}
168
- .ai-generator-header{padding:10px 16px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;background:#f8fafc}
167
+ .ai-generator-panel{border-top:1px solid #e2e8f0;background:#fff;display:flex;flex-direction:column}
168
+ .ai-generator-header{padding:8px 16px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;background:#f8fafc;border-bottom:1px solid #e2e8f0}
169
169
  .ai-generator-header:hover{background:#f1f5f9}
170
170
  .ai-generator-header .title{font-size:13px;font-weight:600;color:#1e293b;display:flex;align-items:center;gap:8px}
171
171
  .ai-generator-header .toggle-icon{font-size:12px;color:#64748b;transition:transform .2s}
172
- .ai-generator-panel.expanded .toggle-icon{transform:rotate(180deg)}
173
- .ai-generator-body{padding:16px;border-top:1px solid #e2e8f0;max-height:300px;overflow-y:auto}
174
- .ai-generator-input-row{display:flex;gap:12px;margin-bottom:12px}
175
- .ai-generator-input-row input{flex:1;padding:10px 14px;border:1px solid #cbd5e1;border-radius:8px;font-size:13px;color:#1e293b;outline:none}
172
+ .ai-generator-panel.collapsed .ai-generator-body{display:none}
173
+ .ai-generator-panel.collapsed .toggle-icon{transform:rotate(180deg)}
174
+ .ai-generator-body{padding:12px 16px;overflow-y:auto;flex:1;min-height:0}
175
+ .ai-generator-input-row{display:flex;gap:8px;margin-bottom:8px}
176
+ .ai-generator-input-row input{flex:1;padding:8px 12px;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;color:#1e293b;outline:none}
176
177
  .ai-generator-input-row input:focus{border-color:#38bdf8}
177
- .ai-generator-input-row button{padding:10px 20px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:background .2s;white-space:nowrap}
178
+ .ai-generator-input-row button{padding:8px 16px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;transition:background .2s;white-space:nowrap}
178
179
  .ai-generator-input-row button:hover{background:#1d4ed8}
179
180
  .ai-generator-input-row button:disabled{background:#94a3b8;cursor:not-allowed}
180
- .ai-generator-hint{font-size:11px;color:#94a3b8;margin-bottom:12px}
181
- .ai-generator-result{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden}
182
- .ai-generator-result-header{padding:8px 12px;background:#f8fafc;border-bottom:1px solid #e2e8f0;font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:space-between}
183
- .ai-generator-result-body{padding:12px;font-size:12px;line-height:1.6;color:#1e293b;white-space:pre-wrap;font-family:'SF Mono',Monaco,Consolas,monospace;max-height:200px;overflow-y:auto;background:#fff}
184
- .ai-generator-result-actions{display:flex;gap:8px;padding:8px 12px;background:#f8fafc;border-top:1px solid #e2e8f0}
185
- .ai-generator-loading{display:flex;align-items:center;gap:8px;padding:12px;color:#64748b;font-size:12px}
186
- .ai-generator-loading .spinner{width:16px;height:16px;border:2px solid #e2e8f0;border-top-color:#38bdf8;border-radius:50%;animation:spin 1s linear infinite}
187
181
  </style>
188
182
 
189
183
  <div class="debug-page" id="debug-page-content">
@@ -240,11 +234,6 @@ html,body{overflow:hidden;margin:0;padding:0}
240
234
  </div>
241
235
  </div>
242
236
 
243
- <div class="checkbox-group">
244
- <input type="checkbox" id="debug-skipAppium" checked>
245
- <label for="debug-skipAppium">跳过 Appium 服务</label>
246
- </div>
247
-
248
237
  <div class="form-group">
249
238
  <label>登录用户名(选填)</label>
250
239
  <input id="debug-loginUser" placeholder="输入用户名">
@@ -270,7 +259,6 @@ html,body{overflow:hidden;margin:0;padding:0}
270
259
  <div class="actions">
271
260
  <button class="btn-icon btn-ghost" onclick="openCaseSelector()">&#128193; 选择用例</button>
272
261
  <button class="btn-icon btn-ghost" onclick="openSaveCaseDialog()">&#128190; 保存用例</button>
273
- <button class="btn-icon btn-ghost" onclick="handleGenerateScript()">&#9889; 生成脚本</button>
274
262
  <button class="btn-icon btn-ghost" onclick="handleClearEditor()">&#128465; 清空</button>
275
263
  </div>
276
264
  </div>
@@ -280,6 +268,24 @@ html,body{overflow:hidden;margin:0;padding:0}
280
268
  <textarea id="debug-script" placeholder="// 在此编写调试脚本\n\n" spellcheck="false"></textarea>
281
269
  </div>
282
270
 
271
+ <!-- AI脚本生成器 - 内嵌在脚本编辑器下方 -->
272
+ <div class="ai-generator-panel" id="ai-generator-panel">
273
+ <div class="ai-generator-header" onclick="toggleAiGenerator()">
274
+ <div class="title">
275
+ <span style="font-size:14px">&#9889;</span>
276
+ <span>AI 脚本生成器</span>
277
+ <span style="font-size:11px;color:#94a3b8;font-weight:400">- 输入自然语言描述,生成脚本直接写入编辑器</span>
278
+ </div>
279
+ <div class="toggle-icon">&#9650;</div>
280
+ </div>
281
+ <div class="ai-generator-body">
282
+ <div class="ai-generator-input-row">
283
+ <input type="text" id="ai-generator-input" placeholder="输入自然语言描述,例如:点击搜索按钮,输入'京东',然后点击搜索" onkeypress="if(event.key==='Enter')handleAiGenerate()">
284
+ <button id="ai-generator-btn" onclick="handleAiGenerate()">&#9889; 生成</button>
285
+ </div>
286
+ </div>
287
+ </div>
288
+
283
289
  <!-- 终端面板 -->
284
290
  <div class="debug-terminal" id="debug-terminal">
285
291
  <div class="debug-terminal-header">
@@ -310,28 +316,6 @@ html,body{overflow:hidden;margin:0;padding:0}
310
316
  </div>
311
317
  </div>
312
318
 
313
- <!-- AI生成脚本对话框 -->
314
- <div class="ai-generator-panel" id="ai-generator-panel">
315
- <div class="ai-generator-header" onclick="toggleAiGenerator()">
316
- <div class="title">
317
- <span style="font-size:16px">&#9889;</span>
318
- <span>AI 脚本生成器</span>
319
- <span style="font-size:11px;color:#94a3b8;font-weight:400">- 输入自然语言描述生成自动化脚本</span>
320
- </div>
321
- <div class="toggle-icon">&#9650;</div>
322
- </div>
323
- <div class="ai-generator-body">
324
- <div class="ai-generator-input-row">
325
- <input type="text" id="ai-generator-input" placeholder="输入自然语言描述,例如:点击搜索按钮,输入"京东",然后点击搜索" onkeypress="if(event.key==='Enter')handleAiGenerate()">
326
- <button id="ai-generator-btn" onclick="handleAiGenerate()">&#9889; 生成脚本</button>
327
- </div>
328
- <div class="ai-generator-hint">
329
- &#128161; 提示:描述越详细,生成的脚本越准确。支持描述页面操作、数据验证、流程步骤等
330
- </div>
331
- <div id="ai-generator-result"></div>
332
- </div>
333
- </div>
334
-
335
319
  <!-- 实时报告面板 - 右侧独立列 -->
336
320
  <div class="debug-report-panel hidden" id="debug-report-panel">
337
321
  <div class="debug-report-header">
@@ -420,7 +404,7 @@ html,body{overflow:hidden;margin:0;padding:0}
420
404
  </div>
421
405
  <div class="modal-footer">
422
406
  <button class="btn btn-cancel" onclick="closeSaveCaseDialog()">取消</button>
423
- <button class="btn btn-primary" onclick="handleSaveCase()">保存</button>
407
+ <button class="btn btn-primary" id="save-case-submit-btn" onclick="handleSaveCase()">保存</button>
424
408
  </div>
425
409
  </div>
426
410
  </div>
@@ -953,14 +937,24 @@ function renderStepDetail() {
953
937
  if(task.usage) {
954
938
  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>';
955
939
  }
956
- // 截图展示 - 图片可点击在新窗口打开
940
+ // 截图展示 - 图片可点击在新窗口打开,并在图片上绘制定位矩形
957
941
  if(task.uiContext && task.uiContext.screenshot) {
958
942
  const ss = task.uiContext.screenshot;
943
+ // 提取 element 定位信息(从 task.output.element 或 task.log 中)
944
+ let elementInfo = null;
945
+ if(task.output && task.output.element) {
946
+ elementInfo = task.output.element;
947
+ } else if(task.log && task.log.data && task.log.data.element) {
948
+ elementInfo = task.log.data.element;
949
+ }
950
+ const elementJson = elementInfo ? escHtml(JSON.stringify(elementInfo)) : '';
959
951
  if(ss.base64) {
960
952
  const sizeLabel = ss.width ? ss.width + "x" + (ss.height || "") : "";
961
- html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.base64) + '" title="点击查看大图" /><div class="rrd-screenshot-label">UI截图 ' + sizeLabel + '</div></div>';
953
+ const canvasId = 'ss-canvas-' + index + '-' + (exec.tasks || []).indexOf(task);
954
+ html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.base64) + '" title="点击查看大图" data-canvas-id="' + canvasId + '" data-element="' + elementJson + '" onload="drawElementRect(this)" /><canvas id="' + canvasId + '" class="rrd-screenshot-canvas"></canvas><div class="rrd-screenshot-label">UI截图 ' + sizeLabel + '</div></div>';
962
955
  } else if(ss.url) {
963
- html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.url) + '" title="点击查看大图" /><div class="rrd-screenshot-label">UI截图</div></div>';
956
+ const canvasId = 'ss-canvas-' + index + '-' + (exec.tasks || []).indexOf(task);
957
+ html += '<div class="rrd-screenshot"><img src="' + escHtml(ss.url) + '" title="点击查看大图" data-canvas-id="' + canvasId + '" data-element="' + elementJson + '" onload="drawElementRect(this)" /><canvas id="' + canvasId + '" class="rrd-screenshot-canvas"></canvas><div class="rrd-screenshot-label">UI截图</div></div>';
964
958
  }
965
959
  }
966
960
  html += '</div></div>';
@@ -976,6 +970,112 @@ function openReport() {
976
970
  }
977
971
  }
978
972
 
973
+ // 在截图上绘制元素定位矩形
974
+ function drawElementRect(imgEl) {
975
+ const canvasId = imgEl.dataset.canvasId;
976
+ const elementJsonStr = imgEl.dataset.element;
977
+ if(!elementJsonStr) return;
978
+ let element;
979
+ try { element = JSON.parse(elementJsonStr); } catch(e) { return; }
980
+ if(!element.rect && !element.center) return;
981
+
982
+ const canvas = document.getElementById(canvasId);
983
+ if(!canvas) return;
984
+
985
+ // 等待图片加载完成后获取显示尺寸
986
+ const draw = () => {
987
+ const displayW = imgEl.clientWidth;
988
+ const displayH = imgEl.clientHeight;
989
+ if(!displayW || !displayH) return;
990
+
991
+ // 获取原始截图尺寸(dpr相关)
992
+ const dpr = element.dpr || 1;
993
+ const origW = imgEl.naturalWidth / dpr;
994
+ const origH = imgEl.naturalHeight / dpr;
995
+
996
+ // 设置 canvas 尺寸与图片显示区域一致
997
+ canvas.width = displayW;
998
+ canvas.height = displayH;
999
+ canvas.style.width = displayW + 'px';
1000
+ canvas.style.height = displayH + 'px';
1001
+
1002
+ const ctx = canvas.getContext('2d');
1003
+ if(!ctx) return;
1004
+
1005
+ // 缩放比例:从原始坐标到显示坐标
1006
+ const scaleX = displayW / origW;
1007
+ const scaleY = displayH / origH;
1008
+
1009
+ if(element.rect) {
1010
+ const r = element.rect;
1011
+ const x = r.left * scaleX;
1012
+ const y = r.top * scaleY;
1013
+ const w = r.width * scaleX;
1014
+ const h = r.height * scaleY;
1015
+
1016
+ // 半透明填充
1017
+ ctx.fillStyle = 'rgba(37, 99, 235, 0.12)';
1018
+ ctx.fillRect(x, y, w, h);
1019
+
1020
+ // 边框
1021
+ ctx.strokeStyle = '#2563eb';
1022
+ ctx.lineWidth = 2.5;
1023
+ ctx.strokeRect(x, y, w, h);
1024
+
1025
+ // 角标(左上角小方块)
1026
+ const cornerSize = 6;
1027
+ ctx.fillStyle = '#2563eb';
1028
+ ctx.fillRect(x - 1, y - 1, cornerSize, cornerSize);
1029
+ ctx.fillRect(x + w - cornerSize + 1, y - 1, cornerSize, cornerSize);
1030
+ ctx.fillRect(x - 1, y + h - cornerSize + 1, cornerSize, cornerSize);
1031
+ ctx.fillRect(x + w - cornerSize + 1, y + h - cornerSize + 1, cornerSize, cornerSize);
1032
+ }
1033
+
1034
+ if(element.center) {
1035
+ const cx = element.center[0] * scaleX;
1036
+ const cy = element.center[1] * scaleY;
1037
+
1038
+ // 十字标记
1039
+ ctx.strokeStyle = '#dc2626';
1040
+ ctx.lineWidth = 2;
1041
+ ctx.beginPath();
1042
+ ctx.moveTo(cx - 10, cy);
1043
+ ctx.lineTo(cx + 10, cy);
1044
+ ctx.moveTo(cx, cy - 10);
1045
+ ctx.lineTo(cx, cy + 10);
1046
+ ctx.stroke();
1047
+
1048
+ // 中心圆点
1049
+ ctx.fillStyle = '#dc2626';
1050
+ ctx.beginPath();
1051
+ ctx.arc(cx, cy, 4, 0, Math.PI * 2);
1052
+ ctx.fill();
1053
+ }
1054
+
1055
+ // 标签描述
1056
+ if(element.description) {
1057
+ const labelX = element.rect ? element.rect.left * scaleX : (element.center ? element.center[0] * scaleX - 30 : 4);
1058
+ const labelY = element.rect ? element.rect.top * scaleY - 6 : (element.center ? element.center[1] * scaleY - 16 : 4);
1059
+ ctx.font = '600 11px -apple-system, sans-serif';
1060
+ const textW = ctx.measureText(element.description).width + 10;
1061
+ // 标签背景
1062
+ ctx.fillStyle = '#2563eb';
1063
+ const labelDrawY = labelY > 18 ? labelY - 14 : labelY + (element.rect ? element.rect.height * scaleY + 14 : 0);
1064
+ ctx.fillRect(labelX, labelDrawY, textW, 16);
1065
+ // 标签文字
1066
+ ctx.fillStyle = '#fff';
1067
+ ctx.fillText(element.description, labelX + 5, labelDrawY + 12);
1068
+ }
1069
+ };
1070
+
1071
+ // 确保 img 已加载
1072
+ if(imgEl.complete && imgEl.naturalWidth > 0) {
1073
+ draw();
1074
+ } else {
1075
+ imgEl.addEventListener('load', draw);
1076
+ }
1077
+ }
1078
+
979
1079
  // ===== 历史记录 =====
980
1080
  function saveToHistory(status) {
981
1081
  const item = {
@@ -1077,17 +1177,26 @@ async function loadFolderTree() {
1077
1177
 
1078
1178
  function renderFolderTree() {
1079
1179
  const list = document.getElementById('case-folder-list');
1180
+ list.innerHTML = '';
1080
1181
  if(caseSelectorState.folders.length === 0) {
1081
1182
  list.innerHTML = '<div style="text-align:center;color:#64748b;padding:20px">暂无文件夹</div>';
1082
1183
  return;
1083
1184
  }
1084
- list.innerHTML = '<div class="folder-item" onclick="loadAllCases()" style="color:#22c55e">&#128196; 全部用例</div>';
1085
- caseSelectorState.folders.forEach((folder, idx) => {
1086
- list.innerHTML += renderFolderNode(folder, 0, 'root-' + idx);
1185
+ // 全部用例
1186
+ const allItem = document.createElement('div');
1187
+ allItem.className = 'folder-item';
1188
+ allItem.style.color = '#22c55e';
1189
+ allItem.innerHTML = '&#128196; 全部用例';
1190
+ allItem.onclick = function() { loadAllCases(); };
1191
+ list.appendChild(allItem);
1192
+
1193
+ caseSelectorState.folders.forEach(function(folder, idx) {
1194
+ const node = buildFolderNode(folder, 0, 'root-' + idx);
1195
+ list.appendChild(node);
1087
1196
  });
1088
1197
  }
1089
1198
 
1090
- function renderFolderNode(node, level, key) {
1199
+ function buildFolderNode(node, level, key) {
1091
1200
  const info = node.folder || {};
1092
1201
  const folderId = info.folderId || info.id;
1093
1202
  const name = (!info.folderName || info.folderName === '未命名文件夹') ? '未分类' : info.folderName;
@@ -1096,29 +1205,56 @@ function renderFolderNode(node, level, key) {
1096
1205
  const isActive = caseSelectorState.selectedFolderId === folderId;
1097
1206
  const indent = level * 16 + 8;
1098
1207
 
1099
- let html = '<div class="folder-item' + (isActive ? ' active' : '') + '" style="padding-left:' + indent + 'px" onclick="selectFolder(&apos;' + folderId + '&apos;, this)">';
1100
- if(hasChildren) html += '<span class="arrow" onclick="event.stopPropagation();toggleFolder(this)">&#9654;</span>';
1101
- else html += '<span style="width:10px;display:inline-block"></span>';
1102
- html += '&#128193; ' + escHtml(name);
1103
- if(count > 0) html += ' <span style="color:#64748b;font-size:11px">(' + count + ')</span>';
1104
- html += '</div>';
1208
+ const item = document.createElement('div');
1209
+ item.className = 'folder-item' + (isActive ? ' active' : '');
1210
+ item.style.paddingLeft = indent + 'px';
1105
1211
 
1106
1212
  if(hasChildren) {
1107
- html += '<div class="folder-children" style="display:none">';
1108
- node.children.forEach((child, idx) => {
1109
- html += renderFolderNode(child, level + 1, key + '-' + idx);
1110
- });
1111
- html += '</div>';
1213
+ const arrow = document.createElement('span');
1214
+ arrow.className = 'arrow';
1215
+ arrow.innerHTML = '&#9654;';
1216
+ arrow.onclick = function(e) {
1217
+ e.stopPropagation();
1218
+ arrow.classList.toggle('expanded');
1219
+ const childrenDiv = item.nextElementSibling;
1220
+ if(childrenDiv && childrenDiv.classList.contains('folder-children')) {
1221
+ childrenDiv.style.display = childrenDiv.style.display === 'none' ? '' : 'none';
1222
+ }
1223
+ };
1224
+ item.appendChild(arrow);
1225
+ } else {
1226
+ const spacer = document.createElement('span');
1227
+ spacer.style.cssText = 'width:10px;display:inline-block';
1228
+ item.appendChild(spacer);
1112
1229
  }
1113
- return html;
1114
- }
1115
1230
 
1116
- function toggleFolder(arrow) {
1117
- arrow.classList.toggle('expanded');
1118
- const children = arrow.closest('.folder-item').nextElementSibling;
1119
- if(children && children.classList.contains('folder-children')) {
1120
- children.style.display = children.style.display === 'none' ? '' : 'none';
1231
+ const text = document.createTextNode('\u{1F4C1} ' + name);
1232
+ item.appendChild(text);
1233
+
1234
+ if(count > 0) {
1235
+ const countSpan = document.createElement('span');
1236
+ countSpan.style.cssText = 'color:#64748b;font-size:11px';
1237
+ countSpan.textContent = ' (' + count + ')';
1238
+ item.appendChild(countSpan);
1239
+ }
1240
+
1241
+ item.onclick = function() { selectFolder(String(folderId)); };
1242
+
1243
+ // 容器:先放item,再放children
1244
+ const wrapper = document.createDocumentFragment();
1245
+ wrapper.appendChild(item);
1246
+
1247
+ if(hasChildren) {
1248
+ const childrenDiv = document.createElement('div');
1249
+ childrenDiv.className = 'folder-children';
1250
+ childrenDiv.style.display = 'none';
1251
+ node.children.forEach(function(child, idx) {
1252
+ childrenDiv.appendChild(buildFolderNode(child, level + 1, key + '-' + idx));
1253
+ });
1254
+ wrapper.appendChild(childrenDiv);
1121
1255
  }
1256
+
1257
+ return wrapper;
1122
1258
  }
1123
1259
 
1124
1260
  async function selectFolder(folderId) {
@@ -1245,15 +1381,22 @@ function confirmCaseSelection() {
1245
1381
  }
1246
1382
 
1247
1383
  // ===== 保存用例 =====
1248
- function openSaveCaseDialog() {
1384
+ async function openSaveCaseDialog() {
1385
+ const saveBtn = document.getElementById('save-case-submit-btn');
1249
1386
  if(debugState.currentTestCase) {
1250
1387
  document.getElementById('save-case-title').textContent = '修改测试用例';
1251
1388
  document.getElementById('save-case-name').value = debugState.currentTestCase.configName || '';
1389
+ saveBtn.textContent = '更新';
1252
1390
  } else {
1253
1391
  document.getElementById('save-case-title').textContent = '保存测试用例';
1254
1392
  document.getElementById('save-case-name').value = '';
1393
+ saveBtn.textContent = '保存';
1255
1394
  }
1256
1395
  document.getElementById('save-case-modal').style.display = '';
1396
+ // 确保文件夹树已加载
1397
+ if(!debugState.folderTree || debugState.folderTree.length === 0) {
1398
+ await loadFolderTree();
1399
+ }
1257
1400
  loadSaveFolderOptions();
1258
1401
  }
1259
1402
 
@@ -1284,6 +1427,10 @@ function loadSaveFolderOptions() {
1284
1427
  });
1285
1428
  }
1286
1429
  addFolders(debugState.folderTree, '');
1430
+ // 如果有当前用例,自动选中其所在文件夹
1431
+ if(debugState.currentTestCase && debugState.currentTestCase.folderId) {
1432
+ sel.value = debugState.currentTestCase.folderId;
1433
+ }
1287
1434
  }
1288
1435
 
1289
1436
  async function handleSaveCase() {
@@ -1423,159 +1570,141 @@ function handleClearEditor() {
1423
1570
  // ===== AI脚本生成器 =====
1424
1571
  function toggleAiGenerator() {
1425
1572
  const panel = document.getElementById('ai-generator-panel');
1426
- panel.classList.toggle('expanded');
1573
+ panel.classList.toggle('collapsed');
1574
+ }
1575
+
1576
+ // 清理Markdown代码块标记
1577
+ function cleanCodeBlock(str) {
1578
+ var bt = String.fromCharCode(96);
1579
+ var pattern1 = bt+bt+bt+'javascript';
1580
+ var pattern2 = bt+bt+bt;
1581
+ return str.split(pattern1).join('').split(pattern2).join('').trim();
1427
1582
  }
1428
1583
 
1429
1584
  async function handleAiGenerate() {
1430
1585
  const input = document.getElementById('ai-generator-input');
1431
1586
  const btn = document.getElementById('ai-generator-btn');
1432
- const resultDiv = document.getElementById('ai-generator-result');
1433
- const naturalLanguage = input.value.trim();
1587
+ const keyword = input.value.trim();
1434
1588
 
1435
- if(!naturalLanguage) {
1436
- alert('请输入自然语言描述');
1589
+ if (!keyword) {
1590
+ addDebugLog('请输入代码描述', 'warn');
1437
1591
  return;
1438
1592
  }
1439
1593
 
1440
1594
  // 显示加载状态
1441
1595
  btn.disabled = true;
1442
- btn.innerHTML = '<span class="spinner"></span> 生成中...';
1443
- resultDiv.innerHTML = '<div class="ai-generator-loading"><span class="spinner"></span> 正在生成脚本,请稍候...</div>';
1596
+ btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border:2px solid #fef3c7;border-top-color:#d97706;border-radius:50%;display:inline-block;vertical-align:middle;animation:spin 1s linear infinite"></span> 生成中...';
1597
+ addDebugLog('正在生成脚本...', 'info');
1444
1598
 
1445
1599
  try {
1446
1600
  // 获取当前配置信息
1447
- const runMode = document.getElementById('debug-runMode').value;
1448
1601
  const platform = document.getElementById('debug-platform').value;
1449
- const url = document.getElementById('debug-url').value || '';
1450
- const deviceId = runMode === 'device' ? document.getElementById('debug-device').value : '';
1451
- const packageName = runMode === 'device' ? getPackageName() : '';
1602
+ const packageName = getPackageName();
1603
+ const testUrl = document.getElementById('debug-url').value || '';
1604
+ const erp = 'system';
1605
+
1606
+ // 构建请求参数
1607
+ const body = {
1608
+ traceId: Date.now().toString() + Math.random().toString(36).substr(2, 9),
1609
+ reqId: Date.now().toString(),
1610
+ erp: erp,
1611
+ keyword: keyword,
1612
+ platform: platform,
1613
+ packageName: packageName,
1614
+ testUrl: testUrl
1615
+ };
1452
1616
 
1453
- // 调用API生成脚本
1454
- const response = await fetch('/api/ai/generate-script', {
1617
+ const headers = {
1618
+ 'Content-Type': 'application/json',
1619
+ 'Accept': 'text/event-stream',
1620
+ 'autobots-agent-id': '30323',
1621
+ 'autobots-token': '60e7b3f68f654e9992ae0d403d2d90e5'
1622
+ };
1623
+
1624
+ // 直接发送fetch请求处理SSE
1625
+ const response = await fetch('http://autobots-bk.jd.local/autobots/api/v1/searchAiSse', {
1455
1626
  method: 'POST',
1456
- headers: { 'Content-Type': 'application/json' },
1457
- body: JSON.stringify({
1458
- naturalLanguage,
1459
- runMode,
1460
- platform,
1461
- url,
1462
- deviceId,
1463
- packageName
1464
- })
1627
+ headers: headers,
1628
+ body: JSON.stringify(body),
1465
1629
  });
1466
1630
 
1467
- const data = await response.json();
1631
+ if (!response.ok) {
1632
+ throw new Error('HTTP ' + response.status);
1633
+ }
1468
1634
 
1469
- if(data.success && data.script) {
1470
- // 显示生成的脚本
1471
- resultDiv.innerHTML = '<div class="ai-generator-result" id="ai-result-container" data-script="' + btoa(encodeURIComponent(data.script)) + '">' +
1472
- '<div class="ai-generator-result-header">' +
1473
- '<span>&#9989; 脚本已生成</span>' +
1474
- '<span style="font-size:10px;color:#94a3b8;font-weight:400">点击"添加到脚本框"将脚本加入编辑器</span>' +
1475
- '</div>' +
1476
- '<div class="ai-generator-result-body">' + escHtml(data.script) + '</div>' +
1477
- '<div class="ai-generator-result-actions">' +
1478
- '<button class="btn-icon btn-blue" style="padding:6px 12px;font-size:12px" onclick="handleAppendScript()">&#8594; 添加到脚本框</button>' +
1479
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="handleCopyScript()">&#128203; 复制</button>' +
1480
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="clearAiResult()">&#10005; 清空</button>' +
1481
- '</div>' +
1482
- '</div>';
1483
- addDebugLog('AI脚本生成成功', 'success');
1484
- } else {
1485
- // API不支持或失败,使用本地生成
1486
- const localScript = generateScriptFromNaturalLanguage(naturalLanguage);
1487
- resultDiv.innerHTML = '<div class="ai-generator-result" id="ai-result-container" data-script="' + btoa(encodeURIComponent(localScript)) + '">' +
1488
- '<div class="ai-generator-result-header">' +
1489
- '<span>&#9989; 脚本已生成(本地模式)</span>' +
1490
- '<span style="font-size:10px;color:#94a3b8;font-weight:400">提示:配置AI服务可获得更精准的脚本生成</span>' +
1491
- '</div>' +
1492
- '<div class="ai-generator-result-body">' + escHtml(localScript) + '</div>' +
1493
- '<div class="ai-generator-result-actions">' +
1494
- '<button class="btn-icon btn-blue" style="padding:6px 12px;font-size:12px" onclick="handleAppendScript()">&#8594; 添加到脚本框</button>' +
1495
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="handleCopyScript()">&#128203; 复制</button>' +
1496
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="clearAiResult()">&#10005; 清空</button>' +
1497
- '</div>' +
1498
- '</div>';
1499
- addDebugLog('使用本地模式生成脚本', 'info');
1635
+ if (!response.body) {
1636
+ throw new Error('No response body');
1500
1637
  }
1501
- } catch(error) {
1502
- // 发生错误,使用本地生成
1503
- console.error('AI脚本生成失败:', error);
1504
- const localScript = generateScriptFromNaturalLanguage(naturalLanguage);
1505
- resultDiv.innerHTML = '<div class="ai-generator-result" id="ai-result-container" data-script="' + btoa(encodeURIComponent(localScript)) + '">' +
1506
- '<div class="ai-generator-result-header">' +
1507
- '<span>&#9888; 使用本地模式生成</span>' +
1508
- '<span style="font-size:10px;color:#f59e0b;font-weight:400">AI服务不可用</span>' +
1509
- '</div>' +
1510
- '<div class="ai-generator-result-body">' + escHtml(localScript) + '</div>' +
1511
- '<div class="ai-generator-result-actions">' +
1512
- '<button class="btn-icon btn-blue" style="padding:6px 12px;font-size:12px" onclick="handleAppendScript()">&#8594; 添加到脚本框</button>' +
1513
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="handleCopyScript()">&#128203; 复制</button>' +
1514
- '<button class="btn-icon btn-ghost" style="padding:6px 12px;font-size:12px" onclick="clearAiResult()">&#10005; 清空</button>' +
1515
- '</div>' +
1516
- '</div>';
1517
- addDebugLog('AI服务不可用,使用本地模式生成脚本', 'warn');
1518
- }
1519
1638
 
1520
- // 恢复按钮状态
1521
- btn.disabled = false;
1522
- btn.innerHTML = '&#9889; 生成脚本';
1523
- }
1639
+ const reader = response.body.getReader();
1640
+ const decoder = new TextDecoder();
1641
+ let buffer = '';
1642
+ let finalContent = '';
1524
1643
 
1525
- function getCurrentGeneratedScript() {
1526
- const container = document.getElementById('ai-result-container');
1527
- if (!container) return '';
1528
- try {
1529
- const encoded = container.getAttribute('data-script');
1530
- return decodeURIComponent(atob(encoded));
1531
- } catch(e) {
1532
- return '';
1533
- }
1534
- }
1644
+ // 脚本编辑器 - 记住原有内容
1645
+ const editor = document.getElementById('debug-script');
1646
+ const existingContent = editor.value.trim();
1535
1647
 
1536
- function handleAppendScript() {
1537
- const script = getCurrentGeneratedScript();
1538
- if (script) {
1539
- appendToScriptEditor(script);
1540
- }
1541
- }
1648
+ while (true) {
1649
+ const { done, value } = await reader.read();
1650
+ if (done) {
1651
+ addDebugLog('脚本生成完成', 'success');
1652
+ break;
1653
+ }
1542
1654
 
1543
- function handleCopyScript() {
1544
- const script = getCurrentGeneratedScript();
1545
- if (script) {
1546
- copyScriptToClipboard(script);
1547
- }
1548
- }
1655
+ buffer += decoder.decode(value, { stream: true });
1656
+
1657
+ // 按行分割处理
1658
+ const lines = buffer.split('\\n');
1659
+ buffer = lines.pop() || '';
1660
+
1661
+ for (const line of lines) {
1662
+ if (line.startsWith('data:')) {
1663
+ try {
1664
+ const jsonStr = line.slice(5).trim();
1665
+ const parsedData = JSON.parse(jsonStr);
1666
+ const responseContent = parsedData.data?.response || parsedData.response || '';
1667
+ const responseAll = parsedData.data?.responseAll || parsedData.responseAll || '';
1668
+
1669
+ if (responseContent) {
1670
+ const cleanContent = cleanCodeBlock(responseContent);
1671
+ finalContent = cleanContent;
1672
+ }
1673
+
1674
+ if (parsedData.data?.status === 'finished' || parsedData.status === 'finished') {
1675
+ if (responseAll) {
1676
+ const cleanContent = cleanCodeBlock(responseAll);
1677
+ finalContent = cleanContent;
1678
+ }
1679
+ }
1680
+
1681
+ // 流式过程中实时预览生成内容
1682
+ if (finalContent) {
1683
+ editor.value = finalContent;
1684
+ editor.scrollTop = editor.scrollHeight;
1685
+ }
1686
+ } catch (error) {
1687
+ // 忽略解析错误
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1549
1692
 
1550
- function appendToScriptEditor(script) {
1551
- const editor = document.getElementById('debug-script');
1552
- const currentContent = editor.value.trim();
1553
- const separator = currentContent ? '\\n\\n// ===== 新增脚本 =====\\n\\n' : '';
1693
+ // 生成完成,将最终内容追加到编辑器原有内容后面
1694
+ if (finalContent) {
1695
+ editor.value = existingContent ? existingContent + '\\n\\n' + finalContent : finalContent;
1696
+ }
1697
+ input.value = '';
1698
+ addDebugLog('脚本已追加到编辑器', 'success');
1554
1699
 
1555
- if(currentContent) {
1556
- editor.value = currentContent + separator + script;
1557
- } else {
1558
- editor.value = script;
1700
+ } catch(error) {
1701
+ console.error('AI脚本生成失败:', error);
1702
+ addDebugLog('脚本生成失败: ' + error.message, 'error');
1559
1703
  }
1560
1704
 
1561
- // 清空输入框和结果
1562
- document.getElementById('ai-generator-input').value = '';
1563
- clearAiResult();
1564
-
1565
- addDebugLog('脚本已添加到编辑器', 'success');
1566
- }
1567
-
1568
- function copyScriptToClipboard(text) {
1569
- navigator.clipboard.writeText(text).then(() => {
1570
- addDebugLog('脚本已复制到剪贴板', 'success');
1571
- }).catch(err => {
1572
- console.error('复制失败:', err);
1573
- addDebugLog('复制失败,请手动复制', 'error');
1574
- });
1575
- }
1576
-
1577
- function clearAiResult() {
1578
- document.getElementById('ai-generator-result').innerHTML = '';
1705
+ // 恢复按钮状态
1706
+ btn.disabled = false;
1707
+ btn.innerHTML = '&#9889; 生成';
1579
1708
  }
1580
1709
 
1581
1710
  // ===== 工具函数 =====