@dyyz1993/agent-browser 0.13.2 → 0.24.0

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.
Files changed (164) hide show
  1. package/README.md +108 -0
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/dist/__tests__/e2e/utils/test-helpers.d.ts +1 -0
  8. package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
  9. package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
  10. package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
  11. package/dist/__tests__/utils/free-port.d.ts +2 -0
  12. package/dist/__tests__/utils/free-port.d.ts.map +1 -0
  13. package/dist/__tests__/utils/free-port.js +18 -0
  14. package/dist/__tests__/utils/free-port.js.map +1 -0
  15. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  16. package/dist/__tests__/utils/parseCli.js +83 -9
  17. package/dist/__tests__/utils/parseCli.js.map +1 -1
  18. package/dist/actions.d.ts.map +1 -1
  19. package/dist/actions.js +298 -9
  20. package/dist/actions.js.map +1 -1
  21. package/dist/browser.d.ts +11 -1
  22. package/dist/browser.d.ts.map +1 -1
  23. package/dist/browser.js +75 -19
  24. package/dist/browser.js.map +1 -1
  25. package/dist/cli/commands.d.ts.map +1 -1
  26. package/dist/cli/commands.js +172 -15
  27. package/dist/cli/commands.js.map +1 -1
  28. package/dist/cli/connection.d.ts +13 -0
  29. package/dist/cli/connection.d.ts.map +1 -1
  30. package/dist/cli/connection.js +137 -48
  31. package/dist/cli/connection.js.map +1 -1
  32. package/dist/cli/flags.d.ts.map +1 -1
  33. package/dist/cli/flags.js +0 -1
  34. package/dist/cli/flags.js.map +1 -1
  35. package/dist/cli/help.d.ts.map +1 -1
  36. package/dist/cli/help.js +63 -22
  37. package/dist/cli/help.js.map +1 -1
  38. package/dist/cli/output.d.ts.map +1 -1
  39. package/dist/cli/output.js +0 -32
  40. package/dist/cli/output.js.map +1 -1
  41. package/dist/cli.js +20 -2
  42. package/dist/cli.js.map +1 -1
  43. package/dist/daemon.d.ts +1 -0
  44. package/dist/daemon.d.ts.map +1 -1
  45. package/dist/daemon.js +291 -264
  46. package/dist/daemon.js.map +1 -1
  47. package/dist/diff.d.ts.map +1 -1
  48. package/dist/diff.js +1 -1
  49. package/dist/diff.js.map +1 -1
  50. package/dist/flow/exporters/cypress.d.ts +9 -0
  51. package/dist/flow/exporters/cypress.d.ts.map +1 -0
  52. package/dist/flow/exporters/cypress.js +256 -0
  53. package/dist/flow/exporters/cypress.js.map +1 -0
  54. package/dist/flow/exporters/index.d.ts +6 -0
  55. package/dist/flow/exporters/index.d.ts.map +1 -0
  56. package/dist/flow/exporters/index.js +5 -0
  57. package/dist/flow/exporters/index.js.map +1 -0
  58. package/dist/flow/exporters/playwright.d.ts +20 -0
  59. package/dist/flow/exporters/playwright.d.ts.map +1 -0
  60. package/dist/flow/exporters/playwright.js +175 -0
  61. package/dist/flow/exporters/playwright.js.map +1 -0
  62. package/dist/flow/exporters/python.d.ts +20 -0
  63. package/dist/flow/exporters/python.d.ts.map +1 -0
  64. package/dist/flow/exporters/python.js +163 -0
  65. package/dist/flow/exporters/python.js.map +1 -0
  66. package/dist/flow/exporters/selenium.d.ts +9 -0
  67. package/dist/flow/exporters/selenium.d.ts.map +1 -0
  68. package/dist/flow/exporters/selenium.js +298 -0
  69. package/dist/flow/exporters/selenium.js.map +1 -0
  70. package/dist/flow/exporters/types.d.ts +13 -0
  71. package/dist/flow/exporters/types.d.ts.map +1 -0
  72. package/dist/flow/exporters/types.js +2 -0
  73. package/dist/flow/exporters/types.js.map +1 -0
  74. package/dist/flow/flow-executor.d.ts +57 -0
  75. package/dist/flow/flow-executor.d.ts.map +1 -0
  76. package/dist/flow/flow-executor.js +1263 -0
  77. package/dist/flow/flow-executor.js.map +1 -0
  78. package/dist/flow/index.d.ts +15 -0
  79. package/dist/flow/index.d.ts.map +1 -0
  80. package/dist/flow/index.js +10 -0
  81. package/dist/flow/index.js.map +1 -0
  82. package/dist/flow/output.d.ts +11 -0
  83. package/dist/flow/output.d.ts.map +1 -0
  84. package/dist/flow/output.js +84 -0
  85. package/dist/flow/output.js.map +1 -0
  86. package/dist/flow/plugin-system.d.ts +48 -0
  87. package/dist/flow/plugin-system.d.ts.map +1 -0
  88. package/dist/flow/plugin-system.js +132 -0
  89. package/dist/flow/plugin-system.js.map +1 -0
  90. package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
  91. package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
  92. package/dist/flow/plugins/file-output-plugin.js +31 -0
  93. package/dist/flow/plugins/file-output-plugin.js.map +1 -0
  94. package/dist/flow/plugins/index.d.ts +4 -0
  95. package/dist/flow/plugins/index.d.ts.map +1 -0
  96. package/dist/flow/plugins/index.js +4 -0
  97. package/dist/flow/plugins/index.js.map +1 -0
  98. package/dist/flow/plugins/logging-plugin.d.ts +7 -0
  99. package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
  100. package/dist/flow/plugins/logging-plugin.js +40 -0
  101. package/dist/flow/plugins/logging-plugin.js.map +1 -0
  102. package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
  103. package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
  104. package/dist/flow/plugins/webhook-plugin.js +24 -0
  105. package/dist/flow/plugins/webhook-plugin.js.map +1 -0
  106. package/dist/flow/presets/index.d.ts +10 -0
  107. package/dist/flow/presets/index.d.ts.map +1 -0
  108. package/dist/flow/presets/index.js +29 -0
  109. package/dist/flow/presets/index.js.map +1 -0
  110. package/dist/flow/recorder-to-flow.d.ts +70 -0
  111. package/dist/flow/recorder-to-flow.d.ts.map +1 -0
  112. package/dist/flow/recorder-to-flow.js +392 -0
  113. package/dist/flow/recorder-to-flow.js.map +1 -0
  114. package/dist/flow/site-manager.d.ts +24 -0
  115. package/dist/flow/site-manager.d.ts.map +1 -0
  116. package/dist/flow/site-manager.js +125 -0
  117. package/dist/flow/site-manager.js.map +1 -0
  118. package/dist/flow/types.d.ts +196 -0
  119. package/dist/flow/types.d.ts.map +1 -0
  120. package/dist/flow/types.js +2 -0
  121. package/dist/flow/types.js.map +1 -0
  122. package/dist/flow/yaml-parser.d.ts +15 -0
  123. package/dist/flow/yaml-parser.d.ts.map +1 -0
  124. package/dist/flow/yaml-parser.js +216 -0
  125. package/dist/flow/yaml-parser.js.map +1 -0
  126. package/dist/human-mouse.d.ts.map +1 -1
  127. package/dist/protocol.d.ts.map +1 -1
  128. package/dist/protocol.js +15 -11
  129. package/dist/protocol.js.map +1 -1
  130. package/dist/rc-config.d.ts.map +1 -1
  131. package/dist/rc-config.js +1 -2
  132. package/dist/rc-config.js.map +1 -1
  133. package/dist/recorder/inject.js +730 -332
  134. package/dist/snapshot-store.d.ts +83 -0
  135. package/dist/snapshot-store.d.ts.map +1 -0
  136. package/dist/snapshot-store.js +112 -0
  137. package/dist/snapshot-store.js.map +1 -0
  138. package/dist/snapshot.d.ts +6 -7
  139. package/dist/snapshot.d.ts.map +1 -1
  140. package/dist/snapshot.js +471 -17
  141. package/dist/snapshot.js.map +1 -1
  142. package/dist/stream-server-standalone.d.ts.map +1 -1
  143. package/dist/stream-server-standalone.js.map +1 -1
  144. package/dist/stream-server.d.ts.map +1 -1
  145. package/dist/stream-server.js +38 -13
  146. package/dist/stream-server.js.map +1 -1
  147. package/dist/test-live.js +5 -5
  148. package/dist/test-live.js.map +1 -1
  149. package/dist/types.d.ts +13 -9
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/types.js.map +1 -1
  152. package/dist/viewer-script.d.ts.map +1 -1
  153. package/dist/viewer-script.js +12 -6
  154. package/dist/viewer-script.js.map +1 -1
  155. package/package.json +18 -5
  156. package/skills/agent-browser/SKILL.md +151 -3
  157. package/dist/ios-actions.d.ts +0 -11
  158. package/dist/ios-actions.d.ts.map +0 -1
  159. package/dist/ios-actions.js +0 -228
  160. package/dist/ios-actions.js.map +0 -1
  161. package/dist/ios-manager.d.ts +0 -266
  162. package/dist/ios-manager.d.ts.map +0 -1
  163. package/dist/ios-manager.js +0 -1076
  164. package/dist/ios-manager.js.map +0 -1
@@ -1,4 +1,4 @@
1
- (function() {
1
+ (function () {
2
2
  'use strict';
3
3
 
4
4
  // 配置常量
@@ -76,6 +76,8 @@
76
76
  let pendingScroll = null;
77
77
  let lastFillSelector = null;
78
78
  let lastFillValue = '';
79
+ let lastFillFallbacks = [];
80
+ let lastFillIdentity = null;
79
81
  let fillTimeout = null;
80
82
  let currentViewport = { width: window.innerWidth, height: window.innerHeight };
81
83
  let pendingResize = null;
@@ -89,7 +91,25 @@
89
91
  // 暴露初始视口(隐蔽名称)
90
92
  window.xyzVp = { ...currentViewport };
91
93
 
92
- const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'NOSCRIPT', 'BR', 'HR', 'SVG', 'PATH', 'TITLE', 'BASE', 'WBR', 'AREA', 'MAP', 'COL', 'COLGROUP']);
94
+ const SKIP_TAGS = new Set([
95
+ 'SCRIPT',
96
+ 'STYLE',
97
+ 'META',
98
+ 'LINK',
99
+ 'HEAD',
100
+ 'NOSCRIPT',
101
+ 'BR',
102
+ 'HR',
103
+ 'SVG',
104
+ 'PATH',
105
+ 'TITLE',
106
+ 'BASE',
107
+ 'WBR',
108
+ 'AREA',
109
+ 'MAP',
110
+ 'COL',
111
+ 'COLGROUP',
112
+ ]);
93
113
 
94
114
  // ============ 私有函数:统一事件 API ============
95
115
  function pushEvent(action) {
@@ -113,7 +133,7 @@
113
133
  if (!action.id) {
114
134
  return { success: false, steps, error: 'Missing id for update action' };
115
135
  }
116
- const updateIndex = steps.findIndex(s => s.id === action.id);
136
+ const updateIndex = steps.findIndex((s) => s.id === action.id);
117
137
  if (updateIndex >= 0) {
118
138
  steps[updateIndex] = { ...steps[updateIndex], ...action.data };
119
139
  window.xyzQueue = steps;
@@ -125,7 +145,7 @@
125
145
  if (!action.id) {
126
146
  return { success: false, steps, error: 'Missing id for delete action' };
127
147
  }
128
- const deleteIndex = steps.findIndex(s => s.id === action.id);
148
+ const deleteIndex = steps.findIndex((s) => s.id === action.id);
129
149
  if (deleteIndex >= 0) {
130
150
  steps.splice(deleteIndex, 1);
131
151
  window.xyzQueue = steps;
@@ -151,7 +171,7 @@
151
171
 
152
172
  const now = Date.now();
153
173
  const cached = highlightCache.get(element);
154
- if (cached && (now - cached.time) < CACHE_TTL) {
174
+ if (cached && now - cached.time < CACHE_TTL) {
155
175
  return cached.result;
156
176
  }
157
177
 
@@ -180,7 +200,8 @@
180
200
  }
181
201
 
182
202
  const style = window.getComputedStyle(element);
183
- const result = style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) !== 0;
203
+ const result =
204
+ style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) !== 0;
184
205
 
185
206
  highlightCache.set(element, { time: now, result });
186
207
  return result;
@@ -194,7 +215,9 @@
194
215
  // 使用动态绑定名称
195
216
  const bindingName = window.xyzBindingName || 'xyzTrack';
196
217
  if (typeof window[bindingName] === 'function') {
197
- try { window[bindingName](JSON.stringify(event.data.step)); } catch (e) {}
218
+ try {
219
+ window[bindingName](JSON.stringify(event.data.step));
220
+ } catch (e) {}
198
221
  }
199
222
  } else {
200
223
  try {
@@ -204,26 +227,30 @@
204
227
  }
205
228
  });
206
229
 
207
- document.addEventListener('mousemove', (e) => {
208
- // 检查录制会话是否仍然活跃
209
- // 注意:xyzActive 可能是 undefined(在 iframe 中),所以只检查明确为 false 的情况
210
- if (window.xyzActive === false || window.xyzStopped) return;
211
-
212
- // 检查当前会话是否是最新的
213
- // 由于 addInitScript 是累积的,旧的监听器可能会继续工作
214
- // 通过比较时间戳来确保只有最新的会话记录事件
215
- const currentTimestamp = parseInt((window.xyzSessionId || '').replace('recorder-', '')) || 0;
216
- if (thisTimestamp > 0 && currentTimestamp > thisTimestamp) return;
217
-
218
- const now = Date.now();
219
- if (now - lastTime > TRAJECTORY_INTERVAL) {
220
- mousePath.push({ x: e.clientX, y: e.clientY, t: now });
221
- if (mousePath.length > MAX_TRAJECTORY_POINTS) {
222
- mousePath.shift();
230
+ document.addEventListener(
231
+ 'mousemove',
232
+ (e) => {
233
+ // 检查录制会话是否仍然活跃
234
+ // 注意:xyzActive 可能是 undefined(在 iframe 中),所以只检查明确为 false 的情况
235
+ if (window.xyzActive === false || window.xyzStopped) return;
236
+
237
+ // 检查当前会话是否是最新的
238
+ // 由于 addInitScript 是累积的,旧的监听器可能会继续工作
239
+ // 通过比较时间戳来确保只有最新的会话记录事件
240
+ const currentTimestamp = parseInt((window.xyzSessionId || '').replace('recorder-', '')) || 0;
241
+ if (thisTimestamp > 0 && currentTimestamp > thisTimestamp) return;
242
+
243
+ const now = Date.now();
244
+ if (now - lastTime > TRAJECTORY_INTERVAL) {
245
+ mousePath.push({ x: e.clientX, y: e.clientY, t: now });
246
+ if (mousePath.length > MAX_TRAJECTORY_POINTS) {
247
+ mousePath.shift();
248
+ }
249
+ lastTime = now;
223
250
  }
224
- lastTime = now;
225
- }
226
- }, true);
251
+ },
252
+ true
253
+ );
227
254
 
228
255
  function getTrajectory() {
229
256
  // 检查当前会话是否是最新的
@@ -234,7 +261,7 @@
234
261
  mousePath = [];
235
262
  return [];
236
263
  }
237
-
264
+
238
265
  const points = mousePath.slice(-4);
239
266
  mousePath = [];
240
267
  return points;
@@ -281,11 +308,21 @@
281
308
 
282
309
  function syncStep(step) {
283
310
  if (pendingResize) {
284
- syncStepDirect({ timestamp: Date.now(), action: 'resize', from: pendingResize.from, to: pendingResize.to });
311
+ syncStepDirect({
312
+ timestamp: Date.now(),
313
+ action: 'resize',
314
+ from: pendingResize.from,
315
+ to: pendingResize.to,
316
+ });
285
317
  pendingResize = null;
286
318
  }
287
319
  if (pendingScroll) {
288
- syncStepDirect({ timestamp: Date.now(), action: 'scroll', x: pendingScroll.x, y: pendingScroll.y });
320
+ syncStepDirect({
321
+ timestamp: Date.now(),
322
+ action: 'scroll',
323
+ x: pendingScroll.x,
324
+ y: pendingScroll.y,
325
+ });
289
326
  pendingScroll = null;
290
327
  }
291
328
  const trajectory = getTrajectory();
@@ -295,41 +332,52 @@
295
332
  syncStepDirect(step);
296
333
  }
297
334
 
298
- window.addEventListener('resize', () => {
299
- clearTimeout(resizeTimeout);
300
- resizeTimeout = setTimeout(() => {
301
- const newWidth = window.innerWidth;
302
- const newHeight = window.innerHeight;
303
- if (newWidth !== currentViewport.width || newHeight !== currentViewport.height) {
304
- pendingResize = { from: { ...currentViewport }, to: { width: newWidth, height: newHeight } };
305
- currentViewport = { width: newWidth, height: newHeight };
306
- }
307
- }, 100);
308
- }, true);
309
-
310
- window.addEventListener('scroll', () => {
311
- clearTimeout(scrollTimeout);
312
- scrollTimeout = setTimeout(() => {
313
- const scrollX = window.scrollX;
314
- const scrollY = window.scrollY;
315
- if (Math.abs(scrollY - lastScrollY) > SCROLL_THRESHOLD || Math.abs(scrollX - lastScrollX) > SCROLL_THRESHOLD) {
316
- pendingScroll = { x: scrollX, y: scrollY };
317
- lastScrollX = scrollX;
318
- lastScrollY = scrollY;
319
- }
320
- }, 100);
321
- }, true);
335
+ window.addEventListener(
336
+ 'resize',
337
+ () => {
338
+ clearTimeout(resizeTimeout);
339
+ resizeTimeout = setTimeout(() => {
340
+ const newWidth = window.innerWidth;
341
+ const newHeight = window.innerHeight;
342
+ if (newWidth !== currentViewport.width || newHeight !== currentViewport.height) {
343
+ pendingResize = {
344
+ from: { ...currentViewport },
345
+ to: { width: newWidth, height: newHeight },
346
+ };
347
+ currentViewport = { width: newWidth, height: newHeight };
348
+ }
349
+ }, 100);
350
+ },
351
+ true
352
+ );
353
+
354
+ window.addEventListener(
355
+ 'scroll',
356
+ () => {
357
+ clearTimeout(scrollTimeout);
358
+ scrollTimeout = setTimeout(() => {
359
+ const scrollX = window.scrollX;
360
+ const scrollY = window.scrollY;
361
+ if (
362
+ Math.abs(scrollY - lastScrollY) > SCROLL_THRESHOLD ||
363
+ Math.abs(scrollX - lastScrollX) > SCROLL_THRESHOLD
364
+ ) {
365
+ pendingScroll = { x: scrollX, y: scrollY };
366
+ lastScrollX = scrollX;
367
+ lastScrollY = scrollY;
368
+ }
369
+ }, 100);
370
+ },
371
+ true
372
+ );
322
373
 
323
374
  // ============ XPath 和 Selector 工具函数 ============
324
375
  function isUniqueXPath(xpath) {
325
376
  try {
326
- return document.evaluate(
327
- 'count(' + xpath + ')',
328
- document,
329
- null,
330
- XPathResult.NUMBER_TYPE,
331
- null
332
- ).numberValue === 1;
377
+ return (
378
+ document.evaluate('count(' + xpath + ')', document, null, XPathResult.NUMBER_TYPE, null)
379
+ .numberValue === 1
380
+ );
333
381
  } catch (e) {
334
382
  return false;
335
383
  }
@@ -392,7 +440,16 @@
392
440
  }
393
441
 
394
442
  if (!result) {
395
- const semanticAttrs = ['data-testid', 'data-test', 'data-cy', 'aria-label', 'name', 'role', 'title', 'placeholder'];
443
+ const semanticAttrs = [
444
+ 'data-testid',
445
+ 'data-test',
446
+ 'data-cy',
447
+ 'aria-label',
448
+ 'name',
449
+ 'role',
450
+ 'title',
451
+ 'placeholder',
452
+ ];
396
453
  for (const attr of semanticAttrs) {
397
454
  const value = element.getAttribute(attr);
398
455
  if (value) {
@@ -408,7 +465,8 @@
408
465
  if (!result) {
409
466
  const text = element.innerText?.trim();
410
467
  if (text && text.length < 30 && ['BUTTON', 'A', 'SPAN', 'LABEL'].includes(element.tagName)) {
411
- const xpath = '//' + element.tagName.toLowerCase() + '[contains(text(), "' + text.slice(0, 20) + '")]';
468
+ const xpath =
469
+ '//' + element.tagName.toLowerCase() + '[contains(text(), "' + text.slice(0, 20) + '")]';
412
470
  if (isUniqueXPath(xpath)) result = xpath;
413
471
  }
414
472
  }
@@ -441,19 +499,27 @@
441
499
 
442
500
  // 语义属性优先级列表
443
501
  const SEMANTIC_ATTRS = [
444
- 'data-testid', 'data-test', 'data-cy',
445
- 'name', 'aria-label', 'aria-labelledby',
446
- 'role', 'type', 'placeholder', 'title', 'alt'
502
+ 'data-testid',
503
+ 'data-test',
504
+ 'data-cy',
505
+ 'name',
506
+ 'aria-label',
507
+ 'aria-labelledby',
508
+ 'role',
509
+ 'type',
510
+ 'placeholder',
511
+ 'title',
512
+ 'alt',
447
513
  ];
448
514
 
449
515
  // 工具类名排除规则
450
516
  const UTILITY_CLASS_PATTERNS = [
451
- /^_/, // 下划线开头
452
- /^css-/, // CSS Modules
517
+ /^_/, // 下划线开头
518
+ /^css-/, // CSS Modules
453
519
  /^[a-z]{1,2}$/, // 1-2个字符的短类名
454
520
  /^(active|disabled|hidden|visible|selected|hover|focus|current|open|closed)$/i,
455
521
  /^(text-|font-|bg-|p-|m-|w-|h-|flex|grid|border|rounded|shadow|opacity|z-)/,
456
- /^(sm:|md:|lg:|xl:|2xl:)/ // 响应式前缀
522
+ /^(sm:|md:|lg:|xl:|2xl:)/, // 响应式前缀
457
523
  ];
458
524
 
459
525
  // 检测高熵类名(CSS Modules/Emotion/Styled Components 自动生成的随机类名)
@@ -493,14 +559,17 @@
493
559
  // 过滤有用的类名
494
560
  function filterUsefulClasses(element) {
495
561
  if (!element.className || typeof element.className !== 'string') return [];
496
- return element.className.trim().split(/\s+/).filter(c => {
497
- if (!c) return false;
498
- // 过滤工具类名
499
- if (UTILITY_CLASS_PATTERNS.some(p => p.test(c))) return false;
500
- // 过滤高熵类名
501
- if (isHighEntropyClassName(c)) return false;
502
- return true;
503
- });
562
+ return element.className
563
+ .trim()
564
+ .split(/\s+/)
565
+ .filter((c) => {
566
+ if (!c) return false;
567
+ // 过滤工具类名
568
+ if (UTILITY_CLASS_PATTERNS.some((p) => p.test(c))) return false;
569
+ // 过滤高熵类名
570
+ if (isHighEntropyClassName(c)) return false;
571
+ return true;
572
+ });
504
573
  }
505
574
 
506
575
  // 策略1: 多属性组合选择器
@@ -527,9 +596,18 @@
527
596
  if (attrs.length >= 2) {
528
597
  for (let i = 0; i < attrs.length; i++) {
529
598
  for (let j = i + 1; j < attrs.length; j++) {
530
- const selector = tag +
531
- '[' + attrs[i].attr + '="' + CSS.escape(attrs[i].value) + '"]' +
532
- '[' + attrs[j].attr + '="' + CSS.escape(attrs[j].value) + '"]';
599
+ const selector =
600
+ tag +
601
+ '[' +
602
+ attrs[i].attr +
603
+ '="' +
604
+ CSS.escape(attrs[i].value) +
605
+ '"]' +
606
+ '[' +
607
+ attrs[j].attr +
608
+ '="' +
609
+ CSS.escape(attrs[j].value) +
610
+ '"]';
533
611
  if (isUniqueSelector(selector)) return selector;
534
612
  }
535
613
  }
@@ -552,7 +630,8 @@
552
630
  for (const attr of SEMANTIC_ATTRS) {
553
631
  const value = element.getAttribute(attr);
554
632
  if (value) {
555
- const selector = tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
633
+ const selector =
634
+ tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
556
635
  if (isUniqueSelector(selector)) return selector;
557
636
  }
558
637
  }
@@ -578,7 +657,13 @@
578
657
 
579
658
  // 尝试组合多个类名
580
659
  for (let i = 2; i <= Math.min(3, classes.length); i++) {
581
- const selector = tag + '.' + classes.slice(0, i).map(c => CSS.escape(c)).join('.');
660
+ const selector =
661
+ tag +
662
+ '.' +
663
+ classes
664
+ .slice(0, i)
665
+ .map((c) => CSS.escape(c))
666
+ .join('.');
582
667
  if (isUniqueSelector(selector)) return selector;
583
668
  }
584
669
 
@@ -659,7 +744,12 @@
659
744
  if (classes.length > 0) {
660
745
  // 按长度排序,取最具体的类名
661
746
  classes.sort((a, b) => b.length - a.length);
662
- selector += '.' + classes.slice(0, 2).map(c => CSS.escape(c)).join('.');
747
+ selector +=
748
+ '.' +
749
+ classes
750
+ .slice(0, 2)
751
+ .map((c) => CSS.escape(c))
752
+ .join('.');
663
753
  }
664
754
  return selector;
665
755
  }
@@ -669,7 +759,7 @@
669
759
  if (!parent) return baseSelector;
670
760
 
671
761
  const siblings = Array.from(parent.children);
672
- const sameTagSiblings = siblings.filter(s => s.tagName === element.tagName);
762
+ const sameTagSiblings = siblings.filter((s) => s.tagName === element.tagName);
673
763
 
674
764
  if (sameTagSiblings.length === 1) {
675
765
  return baseSelector;
@@ -843,13 +933,110 @@
843
933
  return getSelectorWithShadow(element);
844
934
  }
845
935
 
936
+ // === Fallback Selectors (Enhancement 1) ===
937
+ function getFallbackSelectors(element) {
938
+ const primary = getSelectorInternal(element);
939
+ const candidates = [];
940
+
941
+ const tryCandidate = (selector) => {
942
+ if (selector && selector !== primary && !candidates.includes(selector)) {
943
+ candidates.push(selector);
944
+ }
945
+ };
946
+
947
+ if (element.id) {
948
+ const sel = '#' + CSS.escape(element.id);
949
+ try {
950
+ if (document.querySelectorAll(sel).length === 1) tryCandidate(sel);
951
+ } catch (e) {}
952
+ }
953
+
954
+ tryCandidate(getMultiAttributeSelector(element));
955
+ tryCandidate(getAttributeClassComboSelector(element));
956
+ tryCandidate(getBestClassSelector(element));
957
+ tryCandidate(getSiblingBasedSelector(element));
958
+ tryCandidate(buildComposedSelector(element));
959
+
960
+ const base = getBaseSelector(element);
961
+ const nthSel = makeUniqueWithNth(element, base);
962
+ try {
963
+ if (document.querySelectorAll(nthSel).length === 1) tryCandidate(nthSel);
964
+ } catch (e) {}
965
+
966
+ tryCandidate(buildUniquePath(element));
967
+
968
+ return candidates.slice(0, 3);
969
+ }
970
+
971
+ // === Element Identity Capture (Enhancement 2) ===
972
+ function captureSemanticAttributes(element) {
973
+ const attrs = {};
974
+ const semanticAttrs = [
975
+ 'name',
976
+ 'aria-label',
977
+ 'data-testid',
978
+ 'data-test',
979
+ 'placeholder',
980
+ 'type',
981
+ 'role',
982
+ 'title',
983
+ 'href',
984
+ ];
985
+ for (const attr of semanticAttrs) {
986
+ const value = element.getAttribute(attr);
987
+ if (value) attrs[attr] = value;
988
+ }
989
+ return attrs;
990
+ }
991
+
992
+ function getParentSignature(element) {
993
+ let current = element.parentElement;
994
+ let depth = 0;
995
+ while (current && current !== document.body && depth < 10) {
996
+ if (current.id) {
997
+ return '#' + CSS.escape(current.id);
998
+ }
999
+ const testId = current.getAttribute('data-testid') || current.getAttribute('data-test');
1000
+ if (testId) {
1001
+ return current.tagName.toLowerCase() + '[data-testid="' + testId + '"]';
1002
+ }
1003
+ const classes = filterUsefulClasses(current);
1004
+ if (classes.length > 0) {
1005
+ const sel = current.tagName.toLowerCase() + '.' + CSS.escape(classes[0]);
1006
+ try {
1007
+ if (document.querySelectorAll(sel).length === 1) return sel;
1008
+ } catch (e) {}
1009
+ }
1010
+ current = current.parentElement;
1011
+ depth++;
1012
+ }
1013
+ return null;
1014
+ }
1015
+
1016
+ function captureElementIdentity(element) {
1017
+ const r = element.getBoundingClientRect();
1018
+ return {
1019
+ tagName: element.tagName.toLowerCase(),
1020
+ textContent: (element.textContent || '').trim().substring(0, 100),
1021
+ attributes: captureSemanticAttributes(element),
1022
+ classes: filterUsefulClasses(element).slice(0, 5),
1023
+ boundingRect: {
1024
+ x: Math.round(r.x),
1025
+ y: Math.round(r.y),
1026
+ width: Math.round(r.width),
1027
+ height: Math.round(r.height),
1028
+ },
1029
+ parentSignature: getParentSignature(element),
1030
+ };
1031
+ }
1032
+
846
1033
  function getElementInfo(element) {
847
1034
  return {
848
1035
  tagName: element.tagName.toLowerCase(),
849
1036
  id: element.id,
850
1037
  className: element.className,
851
1038
  text: element.innerText ? element.innerText.slice(0, 50) : '',
852
- xpath: getXPath(element)
1039
+ xpath: getXPath(element),
853
1040
  };
854
1041
  }
855
1042
 
@@ -874,13 +1061,19 @@
874
1061
  value: data.value,
875
1062
  elementInfo: data.elementInfo,
876
1063
  annotation: data.annotation,
877
- iframe: isInIframe
1064
+ iframe: isInIframe,
1065
+ fallbackSelectors: data.fallbackSelectors
1066
+ ? data.fallbackSelectors.map((s) => iframePrefix + s)
1067
+ : undefined,
1068
+ elementIdentity: data.elementIdentity || undefined,
878
1069
  };
879
1070
 
880
1071
  if (action === 'keyboard') {
881
1072
  delete step.selector;
882
1073
  delete step.xpath;
883
1074
  delete step.elementInfo;
1075
+ delete step.fallbackSelectors;
1076
+ delete step.elementIdentity;
884
1077
  // 复制键盘事件相关属性
885
1078
  step.key = data.key;
886
1079
  step.code = data.code;
@@ -893,177 +1086,245 @@
893
1086
  syncStep(step);
894
1087
  }
895
1088
 
896
- document.addEventListener('click', (e) => {
897
- const path = e.composedPath();
898
- const element = path[0] || e.target;
1089
+ document.addEventListener(
1090
+ 'click',
1091
+ (e) => {
1092
+ const path = e.composedPath();
1093
+ const element = path[0] || e.target;
899
1094
 
900
- if (isInPanel(element)) {
901
- return;
902
- }
903
- if (element === document.body || element === document.documentElement) {
904
- return;
905
- }
1095
+ if (isInPanel(element)) {
1096
+ return;
1097
+ }
1098
+ if (element === document.body || element === document.documentElement) {
1099
+ return;
1100
+ }
906
1101
 
907
- const link = element.closest('a[href]');
908
- if (link) {
909
- const href = link.href;
910
- const target = link.target || '_self';
911
- let isExternal = target === '_blank';
912
- if (!isExternal && href.startsWith('http')) {
913
- try {
914
- const linkHost = new URL(href).host;
915
- isExternal = linkHost !== window.location.host;
916
- } catch (e) {}
1102
+ const link = element.closest('a[href]');
1103
+ if (link) {
1104
+ const href = link.href;
1105
+ const target = link.target || '_self';
1106
+ let isExternal = target === '_blank';
1107
+ if (!isExternal && href.startsWith('http')) {
1108
+ try {
1109
+ const linkHost = new URL(href).host;
1110
+ isExternal = linkHost !== window.location.host;
1111
+ } catch (e) {}
1112
+ }
1113
+
1114
+ recordStep('link_click', {
1115
+ selector: getSelector(link),
1116
+ xpath: getXPath(link),
1117
+ value: href,
1118
+ elementInfo: { ...getElementInfo(link), target: target, isExternal: isExternal },
1119
+ fallbackSelectors: getFallbackSelectors(link),
1120
+ elementIdentity: captureElementIdentity(link),
1121
+ });
1122
+ return;
917
1123
  }
918
1124
 
919
- recordStep('link_click', {
920
- selector: getSelector(link),
921
- xpath: getXPath(link),
922
- value: href,
923
- elementInfo: { ...getElementInfo(link), target: target, isExternal: isExternal }
924
- });
925
- return;
926
- }
1125
+ const tag = element.tagName;
1126
+ const inputType = (element.type || '').toLowerCase();
927
1127
 
928
- const tag = element.tagName;
929
- const inputType = (element.type || '').toLowerCase();
1128
+ if (tag === 'INPUT' && (inputType === 'checkbox' || inputType === 'radio')) {
1129
+ const isChecked = element.checked;
1130
+ const action = inputType === 'radio' ? 'check' : isChecked ? 'check' : 'uncheck';
1131
+ recordStep(action, {
1132
+ selector: getSelector(element),
1133
+ xpath: getXPath(element),
1134
+ elementInfo: getElementInfo(element),
1135
+ fallbackSelectors: getFallbackSelectors(element),
1136
+ elementIdentity: captureElementIdentity(element),
1137
+ });
1138
+ if (!HIDE_UI && !isInIframe && typeof addMarker === 'function') {
1139
+ addMarker(element, action);
1140
+ }
1141
+ return;
1142
+ }
930
1143
 
931
- if (tag === 'INPUT' && (inputType === 'checkbox' || inputType === 'radio')) {
932
- const isChecked = element.checked;
933
- const action = inputType === 'radio' ? 'check' : (isChecked ? 'check' : 'uncheck');
934
- recordStep(action, {
1144
+ recordStep('click', {
935
1145
  selector: getSelector(element),
936
1146
  xpath: getXPath(element),
937
- elementInfo: getElementInfo(element)
1147
+ elementInfo: getElementInfo(element),
1148
+ fallbackSelectors: getFallbackSelectors(element),
1149
+ elementIdentity: captureElementIdentity(element),
938
1150
  });
1151
+
1152
+ // 非隐藏模式下添加标记
939
1153
  if (!HIDE_UI && !isInIframe && typeof addMarker === 'function') {
940
- addMarker(element, action);
1154
+ addMarker(element, 'default');
941
1155
  }
942
- return;
943
- }
944
-
945
- recordStep('click', {
946
- selector: getSelector(element),
947
- xpath: getXPath(element),
948
- elementInfo: getElementInfo(element)
949
- });
950
-
951
- // 非隐藏模式下添加标记
952
- if (!HIDE_UI && !isInIframe && typeof addMarker === 'function') {
953
- addMarker(element, 'default');
954
- }
955
- }, true);
956
-
957
- document.addEventListener('input', (e) => {
958
- const element = e.target;
959
- if (!element || !element.tagName) return;
960
- if (isInPanel(element)) return;
961
-
962
- // Skip checkbox, radio, and select - they are handled by click and change events
963
- if (element.tagName === 'SELECT') return;
964
- const inputType = (element.type || '').toLowerCase();
965
- if (inputType === 'checkbox' || inputType === 'radio') return;
966
-
967
- const selector = getSelector(element);
968
- const value = element.value;
969
-
970
- clearTimeout(fillTimeout);
971
-
972
- if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
973
- recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
974
- }
1156
+ },
1157
+ true
1158
+ );
1159
+
1160
+ document.addEventListener(
1161
+ 'input',
1162
+ (e) => {
1163
+ const element = e.target;
1164
+ if (!element || !element.tagName) return;
1165
+ if (isInPanel(element)) return;
1166
+
1167
+ // Skip checkbox, radio, and select - they are handled by click and change events
1168
+ if (element.tagName === 'SELECT') return;
1169
+ const inputType = (element.type || '').toLowerCase();
1170
+ if (inputType === 'checkbox' || inputType === 'radio') return;
1171
+
1172
+ const selector = getSelector(element);
1173
+ const value = element.value;
1174
+ const fallbacks = getFallbackSelectors(element);
1175
+ const identity = captureElementIdentity(element);
975
1176
 
976
- lastFillSelector = selector;
977
- lastFillValue = value;
1177
+ clearTimeout(fillTimeout);
978
1178
 
979
- fillTimeout = setTimeout(() => {
980
- if (lastFillSelector && lastFillValue) {
981
- recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
982
- lastFillSelector = null;
983
- lastFillValue = '';
1179
+ if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
1180
+ recordStep('fill', {
1181
+ selector: lastFillSelector,
1182
+ value: lastFillValue,
1183
+ fallbackSelectors: lastFillFallbacks,
1184
+ elementIdentity: lastFillIdentity,
1185
+ });
984
1186
  }
985
- }, 300);
986
- }, true); // capture phase
987
-
988
- // Also listen in bubbling phase to catch programmatically dispatched events
989
- document.addEventListener('input', (e) => {
990
- const element = e.target;
991
- if (!element || !element.tagName) return;
992
- if (isInPanel(element)) return;
993
-
994
- // Skip checkbox, radio, and select - they are handled by click and change events
995
- if (element.tagName === 'SELECT') return;
996
- const inputType = (element.type || '').toLowerCase();
997
- if (inputType === 'checkbox' || inputType === 'radio') return;
998
-
999
- const selector = getSelector(element);
1000
- const value = element.value;
1001
1187
 
1002
- // Only process if not already processed in capture phase
1003
- if (lastFillSelector === selector && lastFillValue === value) {
1004
- return;
1005
- }
1006
-
1007
- clearTimeout(fillTimeout);
1188
+ lastFillSelector = selector;
1189
+ lastFillValue = value;
1190
+ lastFillFallbacks = fallbacks;
1191
+ lastFillIdentity = identity;
1192
+
1193
+ fillTimeout = setTimeout(() => {
1194
+ if (lastFillSelector && lastFillValue) {
1195
+ recordStep('fill', {
1196
+ selector: lastFillSelector,
1197
+ value: lastFillValue,
1198
+ fallbackSelectors: lastFillFallbacks,
1199
+ elementIdentity: lastFillIdentity,
1200
+ });
1201
+ lastFillSelector = null;
1202
+ lastFillValue = '';
1203
+ lastFillFallbacks = [];
1204
+ lastFillIdentity = null;
1205
+ }
1206
+ }, 300);
1207
+ },
1208
+ true
1209
+ ); // capture phase
1008
1210
 
1009
- if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
1010
- recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
1011
- }
1211
+ // Also listen in bubbling phase to catch programmatically dispatched events
1212
+ document.addEventListener(
1213
+ 'input',
1214
+ (e) => {
1215
+ const element = e.target;
1216
+ if (!element || !element.tagName) return;
1217
+ if (isInPanel(element)) return;
1218
+
1219
+ // Skip checkbox, radio, and select - they are handled by click and change events
1220
+ if (element.tagName === 'SELECT') return;
1221
+ const inputType = (element.type || '').toLowerCase();
1222
+ if (inputType === 'checkbox' || inputType === 'radio') return;
1223
+
1224
+ const selector = getSelector(element);
1225
+ const value = element.value;
1226
+ const fallbacks = getFallbackSelectors(element);
1227
+ const identity = captureElementIdentity(element);
1228
+
1229
+ // Only process if not already processed in capture phase
1230
+ if (lastFillSelector === selector && lastFillValue === value) {
1231
+ return;
1232
+ }
1012
1233
 
1013
- lastFillSelector = selector;
1014
- lastFillValue = value;
1234
+ clearTimeout(fillTimeout);
1015
1235
 
1016
- fillTimeout = setTimeout(() => {
1017
- if (lastFillSelector && lastFillValue) {
1018
- recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
1019
- lastFillSelector = null;
1020
- lastFillValue = '';
1236
+ if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
1237
+ recordStep('fill', {
1238
+ selector: lastFillSelector,
1239
+ value: lastFillValue,
1240
+ fallbackSelectors: lastFillFallbacks,
1241
+ elementIdentity: lastFillIdentity,
1242
+ });
1021
1243
  }
1022
- }, 300);
1023
- }, false); // bubbling phase
1244
+
1245
+ lastFillSelector = selector;
1246
+ lastFillValue = value;
1247
+ lastFillFallbacks = fallbacks;
1248
+ lastFillIdentity = identity;
1249
+
1250
+ fillTimeout = setTimeout(() => {
1251
+ if (lastFillSelector && lastFillValue) {
1252
+ recordStep('fill', {
1253
+ selector: lastFillSelector,
1254
+ value: lastFillValue,
1255
+ fallbackSelectors: lastFillFallbacks,
1256
+ elementIdentity: lastFillIdentity,
1257
+ });
1258
+ lastFillSelector = null;
1259
+ lastFillValue = '';
1260
+ lastFillFallbacks = [];
1261
+ lastFillIdentity = null;
1262
+ }
1263
+ }, 300);
1264
+ },
1265
+ false
1266
+ ); // bubbling phase
1024
1267
 
1025
1268
  // 标记事件监听器已注册
1026
1269
  window.xyzHasInputListener = true;
1027
1270
 
1028
- document.addEventListener('change', (e) => {
1029
- const element = e.target;
1030
- if (!element || element.tagName !== 'SELECT') return;
1031
- if (isInPanel(element)) return;
1032
-
1033
- recordStep('select', {
1034
- selector: getSelector(element),
1035
- xpath: getXPath(element),
1036
- value: element.value,
1037
- elementInfo: getElementInfo(element)
1038
- });
1039
- }, true);
1040
-
1041
- document.addEventListener('keydown', (e) => {
1042
- const element = document.activeElement;
1271
+ document.addEventListener(
1272
+ 'change',
1273
+ (e) => {
1274
+ const element = e.target;
1275
+ if (!element || element.tagName !== 'SELECT') return;
1276
+ if (isInPanel(element)) return;
1043
1277
 
1044
- if (isInPanel(element)) return;
1278
+ recordStep('select', {
1279
+ selector: getSelector(element),
1280
+ xpath: getXPath(element),
1281
+ value: element.value,
1282
+ elementInfo: getElementInfo(element),
1283
+ fallbackSelectors: getFallbackSelectors(element),
1284
+ elementIdentity: captureElementIdentity(element),
1285
+ });
1286
+ },
1287
+ true
1288
+ );
1289
+
1290
+ document.addEventListener(
1291
+ 'keydown',
1292
+ (e) => {
1293
+ const element = document.activeElement;
1294
+
1295
+ if (isInPanel(element)) return;
1296
+
1297
+ const specialKeys = [
1298
+ 'Enter',
1299
+ 'Tab',
1300
+ 'Escape',
1301
+ 'Backspace',
1302
+ 'ArrowUp',
1303
+ 'ArrowDown',
1304
+ 'ArrowLeft',
1305
+ 'ArrowRight',
1306
+ ];
1045
1307
 
1046
- const specialKeys = ['Enter', 'Tab', 'Escape', 'Backspace', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
1308
+ if (specialKeys.includes(e.key) || e.ctrlKey || e.metaKey || e.altKey) {
1309
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && !specialKeys.includes(e.key)) {
1310
+ return;
1311
+ }
1047
1312
 
1048
- if (specialKeys.includes(e.key) || e.ctrlKey || e.metaKey || e.altKey) {
1049
- if (!e.ctrlKey && !e.metaKey && !e.altKey && !specialKeys.includes(e.key)) {
1050
- return;
1313
+ recordStep('keyboard', {
1314
+ key: e.key,
1315
+ code: e.code,
1316
+ ctrlKey: e.ctrlKey,
1317
+ metaKey: e.metaKey,
1318
+ altKey: e.altKey,
1319
+ shiftKey: e.shiftKey,
1320
+ selector: element ? getSelector(element) : '',
1321
+ xpath: element ? getXPath(element) : '',
1322
+ elementInfo: element ? getElementInfo(element) : null,
1323
+ });
1051
1324
  }
1052
-
1053
-
1054
- recordStep('keyboard', {
1055
- key: e.key,
1056
- code: e.code,
1057
- ctrlKey: e.ctrlKey,
1058
- metaKey: e.metaKey,
1059
- altKey: e.altKey,
1060
- shiftKey: e.shiftKey,
1061
- selector: element ? getSelector(element) : '',
1062
- xpath: element ? getXPath(element) : '',
1063
- elementInfo: element ? getElementInfo(element) : null
1064
- });
1065
- }
1066
- }, true);
1325
+ },
1326
+ true
1327
+ );
1067
1328
 
1068
1329
  window.addEventListener('beforeunload', () => {
1069
1330
  if (lastFillSelector && lastFillValue) {
@@ -1072,22 +1333,31 @@
1072
1333
  timestamp: Date.now(),
1073
1334
  action: 'fill',
1074
1335
  selector: lastFillSelector,
1075
- value: lastFillValue
1336
+ value: lastFillValue,
1337
+ fallbackSelectors: lastFillFallbacks,
1338
+ elementIdentity: lastFillIdentity,
1076
1339
  });
1077
1340
  }
1078
1341
  syncStepDirect({
1079
1342
  id: 'step-' + Date.now(),
1080
1343
  timestamp: Date.now(),
1081
1344
  action: 'navigate',
1082
- value: window.location.href
1345
+ value: window.location.href,
1083
1346
  });
1084
1347
  });
1085
1348
 
1086
- window.xyzFlushPending = function() {
1349
+ window.xyzFlushPending = function () {
1087
1350
  if (lastFillSelector && lastFillValue) {
1088
- recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
1351
+ recordStep('fill', {
1352
+ selector: lastFillSelector,
1353
+ value: lastFillValue,
1354
+ fallbackSelectors: lastFillFallbacks,
1355
+ elementIdentity: lastFillIdentity,
1356
+ });
1089
1357
  lastFillSelector = null;
1090
1358
  lastFillValue = '';
1359
+ lastFillFallbacks = [];
1360
+ lastFillIdentity = null;
1091
1361
  }
1092
1362
  if (fillTimeout) {
1093
1363
  clearTimeout(fillTimeout);
@@ -1103,7 +1373,7 @@
1103
1373
  let _checkPanelInterval = null;
1104
1374
 
1105
1375
  // 关闭面板函数(暴露给外部调用)
1106
- window.xyzClose = function() {
1376
+ window.xyzClose = function () {
1107
1377
  if (_animationFrameId) {
1108
1378
  cancelAnimationFrame(_animationFrameId);
1109
1379
  _animationFrameId = null;
@@ -1129,10 +1399,10 @@
1129
1399
  document.getElementById('xyzMk'),
1130
1400
  document.getElementById('xyzCv'),
1131
1401
  document.getElementById('xyzSh'),
1132
- document.getElementById('xyzSt')
1402
+ document.getElementById('xyzSt'),
1133
1403
  ];
1134
1404
 
1135
- elements.forEach(el => {
1405
+ elements.forEach((el) => {
1136
1406
  if (el && el.parentNode) {
1137
1407
  el.parentNode.removeChild(el);
1138
1408
  }
@@ -1140,24 +1410,23 @@
1140
1410
 
1141
1411
  window.xyzInited = false;
1142
1412
  window.xyzQueue = [];
1143
-
1144
1413
  };
1145
1414
 
1146
1415
  // 检查录制会话是否已停止
1147
1416
  if (window.xyzStopped) {
1148
-
1149
1417
  return;
1150
1418
  }
1151
1419
 
1152
1420
  // 检查录制会话是否激活
1153
1421
  if (!window.xyzActive) {
1154
-
1155
1422
  return;
1156
1423
  }
1157
1424
 
1158
1425
  let uiElements = {};
1159
1426
  let currentElement = null;
1160
- let mouseX = 0, mouseY = 0, currentEdge = null;
1427
+ let mouseX = 0,
1428
+ mouseY = 0,
1429
+ currentEdge = null;
1161
1430
  const EDGE_THRESHOLD = 30;
1162
1431
  let animationFrameId = null;
1163
1432
  let highlightRafId = null;
@@ -1311,24 +1580,29 @@
1311
1580
  });
1312
1581
 
1313
1582
  // Prevent scroll penetration
1314
- panelBody.addEventListener('wheel', (e) => {
1315
- const { scrollTop, scrollHeight, clientHeight } = panelBody;
1316
- const atTop = scrollTop === 0;
1317
- const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
1318
-
1319
- if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
1320
- e.preventDefault();
1321
- }
1322
- }, { passive: false });
1583
+ panelBody.addEventListener(
1584
+ 'wheel',
1585
+ (e) => {
1586
+ const { scrollTop, scrollHeight, clientHeight } = panelBody;
1587
+ const atTop = scrollTop === 0;
1588
+ const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
1589
+
1590
+ if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
1591
+ e.preventDefault();
1592
+ }
1593
+ },
1594
+ { passive: false }
1595
+ );
1323
1596
 
1324
1597
  // Track scroll position for auto-scroll
1325
1598
  panelBody.addEventListener('scroll', () => {
1326
- const isAtBottom = panelBody.scrollHeight - panelBody.scrollTop - panelBody.clientHeight < 10;
1599
+ const isAtBottom =
1600
+ panelBody.scrollHeight - panelBody.scrollTop - panelBody.clientHeight < 10;
1327
1601
  autoScroll = isAtBottom;
1328
1602
  });
1329
1603
 
1330
1604
  // Tool selection for current step
1331
- panelTools.querySelectorAll('.tool-btn').forEach(btn => {
1605
+ panelTools.querySelectorAll('.tool-btn').forEach((btn) => {
1332
1606
  btn.addEventListener('click', () => {
1333
1607
  if (!currentStepId) {
1334
1608
  const steps = window.xyzQueue || [];
@@ -1345,8 +1619,10 @@
1345
1619
 
1346
1620
  // Panel drag functionality
1347
1621
  let isDragging = false;
1348
- let dragStartX = 0, dragStartY = 0;
1349
- let panelStartX = 0, panelStartY = 0;
1622
+ let dragStartX = 0,
1623
+ dragStartY = 0;
1624
+ let panelStartX = 0,
1625
+ panelStartY = 0;
1350
1626
 
1351
1627
  const header = panel.querySelector('.xyzPnl-hdr');
1352
1628
  header.addEventListener('mousedown', (e) => {
@@ -1375,11 +1651,14 @@
1375
1651
  if (isDragging) {
1376
1652
  isDragging = false;
1377
1653
  try {
1378
- localStorage.setItem('xyzPnl-pos', JSON.stringify({
1379
- left: panel.style.left,
1380
- top: panel.style.top
1381
- }));
1382
- } catch(e) {}
1654
+ localStorage.setItem(
1655
+ 'xyzPnl-pos',
1656
+ JSON.stringify({
1657
+ left: panel.style.left,
1658
+ top: panel.style.top,
1659
+ })
1660
+ );
1661
+ } catch (e) {}
1383
1662
  }
1384
1663
  });
1385
1664
 
@@ -1394,7 +1673,7 @@
1394
1673
  panel.style.right = 'auto';
1395
1674
  }
1396
1675
  }
1397
- } catch(e) {}
1676
+ } catch (e) {}
1398
1677
 
1399
1678
  const markersContainer = document.createElement('div');
1400
1679
  markersContainer.className = 'xyzMk';
@@ -1431,38 +1710,60 @@
1431
1710
 
1432
1711
  const displaySteps = steps.slice(-20);
1433
1712
 
1434
- container.innerHTML = displaySteps.map((step) => {
1435
- const action = step.action || 'unknown';
1436
- const selector = step.selector || '';
1437
- const value = step.value || '';
1438
- const stepId = step.id || '';
1439
-
1440
- let extra = '';
1441
- if (action === 'trajectory' && step.points) {
1442
- extra = '<div class="selector">🖱️ ' + step.points.length + ' points</div>';
1443
- } else if (action === 'scroll') {
1444
- extra = '<div class="selector">📜 (' + step.x + ', ' + step.y + ')</div>';
1445
- } else if (action === 'resize') {
1446
- extra = '<div class="selector">📐 ' + step.to.width + 'x' + step.to.height + '</div>';
1447
- } else if (action === 'link_click') {
1448
- extra = '<div class="selector">🔗 ' + (value || '') + '</div>';
1449
- }
1450
-
1451
- const hasAnnotation = step.annotation && step.annotation.label;
1452
- const isSelected = stepId === currentStepId;
1713
+ container.innerHTML = displaySteps
1714
+ .map((step) => {
1715
+ const action = step.action || 'unknown';
1716
+ const selector = step.selector || '';
1717
+ const value = step.value || '';
1718
+ const stepId = step.id || '';
1719
+
1720
+ let extra = '';
1721
+ if (action === 'trajectory' && step.points) {
1722
+ extra = '<div class="selector">🖱️ ' + step.points.length + ' points</div>';
1723
+ } else if (action === 'scroll') {
1724
+ extra = '<div class="selector">📜 (' + step.x + ', ' + step.y + ')</div>';
1725
+ } else if (action === 'resize') {
1726
+ extra = '<div class="selector">📐 ' + step.to.width + 'x' + step.to.height + '</div>';
1727
+ } else if (action === 'link_click') {
1728
+ extra = '<div class="selector">🔗 ' + (value || '') + '</div>';
1729
+ }
1453
1730
 
1454
- return '<div class="xyzStp ' + action + (isSelected ? ' selected' : '') + '" data-step-id="' + stepId + '">' +
1455
- '<div class="action">' + action.toUpperCase() + '</div>' +
1456
- (selector ? '<div class="selector">' + selector + '</div>' : '') +
1457
- (value && !['trajectory', 'scroll', 'resize', 'link_click'].includes(action) ? '<div class="value">"' + value.slice(0, 30) + (value.length > 30 ? '...' : '') + '"</div>' : '') +
1458
- extra +
1459
- (hasAnnotation ? '<span class="annotation">🏷️ ' + step.annotation.label + '</span>' : '') +
1460
- (isSelected ? '<button class="xyzDelBtn" data-step-id="' + stepId + '" title="Delete step">🗑️</button>' : '') +
1461
- '</div>';
1462
- }).join('');
1731
+ const hasAnnotation = step.annotation && step.annotation.label;
1732
+ const isSelected = stepId === currentStepId;
1733
+
1734
+ return (
1735
+ '<div class="xyzStp ' +
1736
+ action +
1737
+ (isSelected ? ' selected' : '') +
1738
+ '" data-step-id="' +
1739
+ stepId +
1740
+ '">' +
1741
+ '<div class="action">' +
1742
+ action.toUpperCase() +
1743
+ '</div>' +
1744
+ (selector ? '<div class="selector">' + selector + '</div>' : '') +
1745
+ (value && !['trajectory', 'scroll', 'resize', 'link_click'].includes(action)
1746
+ ? '<div class="value">"' +
1747
+ value.slice(0, 30) +
1748
+ (value.length > 30 ? '...' : '') +
1749
+ '"</div>'
1750
+ : '') +
1751
+ extra +
1752
+ (hasAnnotation
1753
+ ? '<span class="annotation">🏷️ ' + step.annotation.label + '</span>'
1754
+ : '') +
1755
+ (isSelected
1756
+ ? '<button class="xyzDelBtn" data-step-id="' +
1757
+ stepId +
1758
+ '" title="Delete step">🗑️</button>'
1759
+ : '') +
1760
+ '</div>'
1761
+ );
1762
+ })
1763
+ .join('');
1463
1764
 
1464
1765
  // Click to select step
1465
- container.querySelectorAll('.xyzStp').forEach(stepEl => {
1766
+ container.querySelectorAll('.xyzStp').forEach((stepEl) => {
1466
1767
  stepEl.addEventListener('click', (e) => {
1467
1768
  if (e.target.classList.contains('xyzDelBtn')) return;
1468
1769
 
@@ -1473,7 +1774,7 @@
1473
1774
  });
1474
1775
 
1475
1776
  // Delete button click
1476
- container.querySelectorAll('.xyzDelBtn').forEach(btn => {
1777
+ container.querySelectorAll('.xyzDelBtn').forEach((btn) => {
1477
1778
  btn.addEventListener('click', (e) => {
1478
1779
  e.stopPropagation();
1479
1780
  const stepId = btn.dataset.stepId;
@@ -1500,7 +1801,7 @@
1500
1801
  }
1501
1802
 
1502
1803
  const steps = window.xyzQueue || [];
1503
- const step = steps.find(s => s.id === stepId);
1804
+ const step = steps.find((s) => s.id === stepId);
1504
1805
  if (!step) return;
1505
1806
 
1506
1807
  const labels = {
@@ -1510,7 +1811,7 @@
1510
1811
  pagination: 'Pagination',
1511
1812
  login_check: 'Login',
1512
1813
  checkpoint: 'Check',
1513
- custom: 'Custom'
1814
+ custom: 'Custom',
1514
1815
  };
1515
1816
 
1516
1817
  let annotation = null;
@@ -1527,14 +1828,14 @@
1527
1828
  annotation = {
1528
1829
  type: toolType,
1529
1830
  label: labels[toolType],
1530
- waitTimeout: parseInt(timeout) || 10000
1831
+ waitTimeout: parseInt(timeout) || 10000,
1531
1832
  };
1532
1833
  } else if (toolType === 'data_container') {
1533
1834
  const itemSelector = prompt('Enter item selector (e.g., .product-item):');
1534
1835
  annotation = {
1535
1836
  type: toolType,
1536
1837
  label: labels[toolType],
1537
- itemSelector: itemSelector || ''
1838
+ itemSelector: itemSelector || '',
1538
1839
  };
1539
1840
  } else {
1540
1841
  annotation = { type: toolType, label: labels[toolType] };
@@ -1545,7 +1846,9 @@
1545
1846
  if (result.success) {
1546
1847
  if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
1547
1848
  try {
1548
- window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzUpdate', id: stepId, data: { annotation } }));
1849
+ window[window.xyzBindingName || 'xyzTrack'](
1850
+ JSON.stringify({ action: 'xyzUpdate', id: stepId, data: { annotation } })
1851
+ );
1549
1852
  } catch (e) {}
1550
1853
  }
1551
1854
 
@@ -1561,7 +1864,9 @@
1561
1864
  if (result.success) {
1562
1865
  if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
1563
1866
  try {
1564
- window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzDelete', id: stepId }));
1867
+ window[window.xyzBindingName || 'xyzTrack'](
1868
+ JSON.stringify({ action: 'xyzDelete', id: stepId })
1869
+ );
1565
1870
  } catch (e) {}
1566
1871
  }
1567
1872
 
@@ -1572,7 +1877,7 @@
1572
1877
  }
1573
1878
  }
1574
1879
 
1575
- window.addEventListener('xyzEvt', function(e) {
1880
+ window.addEventListener('xyzEvt', function (e) {
1576
1881
  window.xyzQueue = e.detail;
1577
1882
  updateUI();
1578
1883
  });
@@ -1581,14 +1886,16 @@
1581
1886
  window[window.xyzBindingName || 'xyzTrack']('');
1582
1887
  }
1583
1888
 
1584
- document.getElementById('xyzClear').addEventListener('click', function() {
1889
+ document.getElementById('xyzClear').addEventListener('click', function () {
1585
1890
  window.xyzQueue = [];
1586
1891
  document.getElementById('xyzMk').innerHTML = '';
1587
1892
  markedElements.clear();
1588
1893
  annotations.clear();
1589
1894
  updateUI();
1590
1895
  if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
1591
- try { window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzClear' })); } catch (e) {}
1896
+ try {
1897
+ window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzClear' }));
1898
+ } catch (e) {}
1592
1899
  }
1593
1900
  });
1594
1901
 
@@ -1635,7 +1942,7 @@
1635
1942
  pagination: 'Pagination',
1636
1943
  login_check: 'Login',
1637
1944
  checkpoint: 'Check',
1638
- custom: 'Note'
1945
+ custom: 'Note',
1639
1946
  };
1640
1947
  let annotation = null;
1641
1948
  if (type === 'custom') {
@@ -1648,7 +1955,7 @@
1648
1955
  type,
1649
1956
  label: labels[type],
1650
1957
  selector: selector,
1651
- waitTimeout: parseInt(timeout) || 10000
1958
+ waitTimeout: parseInt(timeout) || 10000,
1652
1959
  };
1653
1960
  } else if (type === 'data_container') {
1654
1961
  const itemSelector = prompt('Enter item selector (e.g., .product-item):');
@@ -1656,7 +1963,7 @@
1656
1963
  type,
1657
1964
  label: labels[type],
1658
1965
  selector: selector,
1659
- itemSelector: itemSelector || ''
1966
+ itemSelector: itemSelector || '',
1660
1967
  };
1661
1968
  } else {
1662
1969
  annotation = { type, label: labels[type], selector: selector };
@@ -1668,12 +1975,17 @@
1668
1975
  selector: selector,
1669
1976
  xpath: getXPath(element),
1670
1977
  annotation: annotation,
1671
- elementInfo: getElementInfo(element)
1978
+ elementInfo: getElementInfo(element),
1979
+ fallbackSelectors: getFallbackSelectors(element),
1980
+ elementIdentity: captureElementIdentity(element),
1672
1981
  });
1673
1982
 
1674
1983
  shadowBox.style.transition = 'none';
1675
1984
  shadowBox.style.boxShadow = '0 0 20px 5px rgba(76, 175, 80, 0.8)';
1676
- setTimeout(() => { shadowBox.style.transition = 'box-shadow 0.3s ease'; shadowBox.style.boxShadow = ''; }, 200);
1985
+ setTimeout(() => {
1986
+ shadowBox.style.transition = 'box-shadow 0.3s ease';
1987
+ shadowBox.style.boxShadow = '';
1988
+ }, 200);
1677
1989
 
1678
1990
  updateShadowBox(element);
1679
1991
  }
@@ -1695,30 +2007,34 @@
1695
2007
  }
1696
2008
  }
1697
2009
 
1698
- document.addEventListener('mousemove', (e) => {
1699
- const element = e.composedPath()[0] || e.target;
1700
- mouseX = e.clientX;
1701
- mouseY = e.clientY;
2010
+ document.addEventListener(
2011
+ 'mousemove',
2012
+ (e) => {
2013
+ const element = e.composedPath()[0] || e.target;
2014
+ mouseX = e.clientX;
2015
+ mouseY = e.clientY;
1702
2016
 
1703
- if (element === shadowBox) return;
1704
- if (isInPanel(element)) {
1705
- throttledHighlight(null);
1706
- return;
1707
- }
1708
- if (element === document.body || element === document.documentElement) {
1709
- throttledHighlight(null);
1710
- currentEdge = null;
1711
- return;
1712
- }
2017
+ if (element === shadowBox) return;
2018
+ if (isInPanel(element)) {
2019
+ throttledHighlight(null);
2020
+ return;
2021
+ }
2022
+ if (element === document.body || element === document.documentElement) {
2023
+ throttledHighlight(null);
2024
+ currentEdge = null;
2025
+ return;
2026
+ }
1713
2027
 
1714
- if (!shouldHighlightElement(element)) {
1715
- throttledHighlight(null);
1716
- return;
1717
- }
2028
+ if (!shouldHighlightElement(element)) {
2029
+ throttledHighlight(null);
2030
+ return;
2031
+ }
1718
2032
 
1719
- currentElement = element;
1720
- throttledHighlight(element);
1721
- }, true);
2033
+ currentElement = element;
2034
+ throttledHighlight(element);
2035
+ },
2036
+ true
2037
+ );
1722
2038
 
1723
2039
  const ctx = canvas.getContext('2d');
1724
2040
  function resizeCanvas() {
@@ -1780,11 +2096,9 @@
1780
2096
  // 延迟创建面板
1781
2097
  setTimeout(() => {
1782
2098
  if (window.xyzStopped) {
1783
-
1784
2099
  return;
1785
2100
  }
1786
2101
  if (!window.xyzActive) {
1787
-
1788
2102
  return;
1789
2103
  }
1790
2104
  createRecorderOverlay();
@@ -1805,7 +2119,6 @@
1805
2119
  panelObserver = new MutationObserver((mutations) => {
1806
2120
  // 检查录制会话是否已停止
1807
2121
  if (window.xyzStopped) {
1808
-
1809
2122
  if (typeof window.xyzClose === 'function') {
1810
2123
  window.xyzClose();
1811
2124
  }
@@ -1818,7 +2131,6 @@
1818
2131
 
1819
2132
  // 检查录制会话是否激活
1820
2133
  if (!window.xyzActive) {
1821
-
1822
2134
  return;
1823
2135
  }
1824
2136
 
@@ -1826,7 +2138,6 @@
1826
2138
  const panel = document.getElementById('xyzPnl');
1827
2139
  const style = document.getElementById('xyzSt');
1828
2140
  if (document.body && (!panel || !style)) {
1829
-
1830
2141
  createRecorderOverlay();
1831
2142
  }
1832
2143
  });
@@ -1834,13 +2145,100 @@
1834
2145
  // 启动观察器
1835
2146
  panelObserver.observe(document.body, {
1836
2147
  childList: true,
1837
- subtree: false
2148
+ subtree: false,
1838
2149
  });
1839
-
1840
-
1841
2150
  }
1842
2151
 
1843
2152
  // 启动 MutationObserver
1844
2153
  startPanelObserver();
1845
2154
  }
2155
+
2156
+ // === SPA URL Change Detection (Enhancement 3) ===
2157
+ (function setupURLChangeDetection() {
2158
+ function pushSignal(data) {
2159
+ if (!window.xyzActive) return;
2160
+ const step = {
2161
+ id: 'env-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5),
2162
+ timestamp: Date.now(),
2163
+ action: 'environment_signal',
2164
+ signalType: 'url_change',
2165
+ data: {
2166
+ from: data.from || '',
2167
+ to: data.to || window.location.href,
2168
+ },
2169
+ url: window.location.href,
2170
+ };
2171
+ syncStepDirect(step);
2172
+ }
2173
+
2174
+ const origPushState = history.pushState;
2175
+ if (origPushState) {
2176
+ history.pushState = function () {
2177
+ const from = window.location.href;
2178
+ const result = origPushState.apply(this, arguments);
2179
+ pushSignal({ from: from, to: window.location.href });
2180
+ return result;
2181
+ };
2182
+ }
2183
+
2184
+ const origReplaceState = history.replaceState;
2185
+ if (origReplaceState) {
2186
+ history.replaceState = function () {
2187
+ const from = window.location.href;
2188
+ const result = origReplaceState.apply(this, arguments);
2189
+ pushSignal({ from: from, to: window.location.href });
2190
+ return result;
2191
+ };
2192
+ }
2193
+
2194
+ window.addEventListener('popstate', function () {
2195
+ pushSignal({ to: window.location.href });
2196
+ });
2197
+ })();
2198
+
2199
+ // === DOM Stability Detection (Enhancement 4) ===
2200
+ (function setupDOMStabilityDetection() {
2201
+ let mutationTimer = null;
2202
+ let lastMutationTime = 0;
2203
+
2204
+ function computeContentHash() {
2205
+ var main =
2206
+ document.querySelector('main, [role="main"], #content, .content, article') || document.body;
2207
+ if (!main) return '0';
2208
+ var html = main.innerHTML.substring(0, 10240);
2209
+ var hash = 0;
2210
+ for (var i = 0; i < html.length; i++) {
2211
+ hash = ((hash << 5) - hash + html.charCodeAt(i)) | 0;
2212
+ }
2213
+ return hash.toString(36);
2214
+ }
2215
+
2216
+ var observer = new MutationObserver(function () {
2217
+ lastMutationTime = Date.now();
2218
+ clearTimeout(mutationTimer);
2219
+ mutationTimer = setTimeout(function () {
2220
+ if (!window.xyzActive) return;
2221
+ var step = {
2222
+ id: 'env-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5),
2223
+ timestamp: Date.now(),
2224
+ action: 'environment_signal',
2225
+ signalType: 'dom_stable',
2226
+ data: {
2227
+ stableAfterMs: Date.now() - lastMutationTime,
2228
+ contentHash: computeContentHash(),
2229
+ },
2230
+ url: window.location.href,
2231
+ };
2232
+ syncStepDirect(step);
2233
+ }, 300);
2234
+ });
2235
+
2236
+ if (document.body) {
2237
+ observer.observe(document.body, { childList: true, subtree: true });
2238
+ } else {
2239
+ document.addEventListener('DOMContentLoaded', function () {
2240
+ observer.observe(document.body, { childList: true, subtree: true });
2241
+ });
2242
+ }
2243
+ })();
1846
2244
  })();