@dyyz1993/agent-browser 0.9.2
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/LICENSE +202 -0
- package/README.md +907 -0
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser.js +120 -0
- package/dist/__tests__/e2e/utils/test-helpers.d.ts +5 -0
- package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -0
- package/dist/__tests__/e2e/utils/test-helpers.js +22 -0
- package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -0
- package/dist/__tests__/test-iframe.d.ts +2 -0
- package/dist/__tests__/test-iframe.d.ts.map +1 -0
- package/dist/__tests__/test-iframe.js +52 -0
- package/dist/__tests__/test-iframe.js.map +1 -0
- package/dist/__tests__/utils/parseCli.d.ts +20 -0
- package/dist/__tests__/utils/parseCli.d.ts.map +1 -0
- package/dist/__tests__/utils/parseCli.js +1086 -0
- package/dist/__tests__/utils/parseCli.js.map +1 -0
- package/dist/actions.d.ts +50 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +2164 -0
- package/dist/actions.js.map +1 -0
- package/dist/browser.d.ts +556 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +2599 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +1038 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/connection.d.ts +50 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +595 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/flags.d.ts +36 -0
- package/dist/cli/flags.d.ts.map +1 -0
- package/dist/cli/flags.js +206 -0
- package/dist/cli/flags.js.map +1 -0
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +1024 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output.d.ts +14 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +456 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli-new.d.ts +3 -0
- package/dist/cli-new.d.ts.map +1 -0
- package/dist/cli-new.js +308 -0
- package/dist/cli-new.js.map +1 -0
- package/dist/cli-old.d.ts +3 -0
- package/dist/cli-old.d.ts.map +1 -0
- package/dist/cli-old.js +1101 -0
- package/dist/cli-old.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +403 -0
- package/dist/cli.js.map +1 -0
- package/dist/content-detection.d.ts +18 -0
- package/dist/content-detection.d.ts.map +1 -0
- package/dist/content-detection.js +68 -0
- package/dist/content-detection.js.map +1 -0
- package/dist/daemon.d.ts +55 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +426 -0
- package/dist/daemon.js.map +1 -0
- package/dist/diff.d.ts +42 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +166 -0
- package/dist/diff.js.map +1 -0
- package/dist/human-mouse.d.ts +31 -0
- package/dist/human-mouse.d.ts.map +1 -0
- package/dist/human-mouse.js +184 -0
- package/dist/human-mouse.js.map +1 -0
- package/dist/ios-actions.d.ts +11 -0
- package/dist/ios-actions.d.ts.map +1 -0
- package/dist/ios-actions.js +228 -0
- package/dist/ios-actions.js.map +1 -0
- package/dist/ios-manager.d.ts +266 -0
- package/dist/ios-manager.d.ts.map +1 -0
- package/dist/ios-manager.js +1076 -0
- package/dist/ios-manager.js.map +1 -0
- package/dist/message-bridge.d.ts +10 -0
- package/dist/message-bridge.d.ts.map +1 -0
- package/dist/message-bridge.js +60 -0
- package/dist/message-bridge.js.map +1 -0
- package/dist/protocol.d.ts +26 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +912 -0
- package/dist/protocol.js.map +1 -0
- package/dist/recorder/binding.d.ts +24 -0
- package/dist/recorder/binding.d.ts.map +1 -0
- package/dist/recorder/binding.js +215 -0
- package/dist/recorder/binding.js.map +1 -0
- package/dist/recorder/index.d.ts +4 -0
- package/dist/recorder/index.d.ts.map +1 -0
- package/dist/recorder/index.js +4 -0
- package/dist/recorder/index.js.map +1 -0
- package/dist/recorder/inject.js +1913 -0
- package/dist/recorder/recorder.d.ts +19 -0
- package/dist/recorder/recorder.d.ts.map +1 -0
- package/dist/recorder/recorder.js +101 -0
- package/dist/recorder/recorder.js.map +1 -0
- package/dist/recorder/store.d.ts +22 -0
- package/dist/recorder/store.d.ts.map +1 -0
- package/dist/recorder/store.js +150 -0
- package/dist/recorder/store.js.map +1 -0
- package/dist/recorder/types.d.ts +73 -0
- package/dist/recorder/types.d.ts.map +1 -0
- package/dist/recorder/types.js +5 -0
- package/dist/recorder/types.js.map +1 -0
- package/dist/snapshot.d.ts +81 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +1348 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/stream-server-standalone.d.ts +38 -0
- package/dist/stream-server-standalone.d.ts.map +1 -0
- package/dist/stream-server-standalone.js +494 -0
- package/dist/stream-server-standalone.js.map +1 -0
- package/dist/stream-server.d.ts +214 -0
- package/dist/stream-server.d.ts.map +1 -0
- package/dist/stream-server.js +811 -0
- package/dist/stream-server.js.map +1 -0
- package/dist/types.d.ts +914 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/viewer-html.d.ts +2 -0
- package/dist/viewer-html.d.ts.map +1 -0
- package/dist/viewer-html.js +185 -0
- package/dist/viewer-html.js.map +1 -0
- package/dist/viewer-script.d.ts +47 -0
- package/dist/viewer-script.d.ts.map +1 -0
- package/dist/viewer-script.js +586 -0
- package/dist/viewer-script.js.map +1 -0
- package/package.json +86 -0
- package/scripts/build-all-platforms.sh +68 -0
- package/scripts/check-version-sync.js +39 -0
- package/scripts/check_goods_container.js +35 -0
- package/scripts/check_page_content.js +36 -0
- package/scripts/click_applause_rate.js +30 -0
- package/scripts/copy-native.js +36 -0
- package/scripts/copy-recorder.js +21 -0
- package/scripts/e2e-test-recorder.ts +584 -0
- package/scripts/explore_jd_page.js +31 -0
- package/scripts/extract_all_jd_data.js +80 -0
- package/scripts/extract_jd_product_detail.js +62 -0
- package/scripts/extract_jd_products_correct_links.js +78 -0
- package/scripts/extract_jd_products_final.js +80 -0
- package/scripts/extract_jd_reviews.js +48 -0
- package/scripts/extract_jd_seafood_final.js +78 -0
- package/scripts/extract_multiple_products.js +77 -0
- package/scripts/extract_products_no_scroll.js +68 -0
- package/scripts/extract_products_simple.js +68 -0
- package/scripts/find_applause_rate.js +26 -0
- package/scripts/find_jd_links.js +28 -0
- package/scripts/find_main_content.js +20 -0
- package/scripts/find_product_cards.js +38 -0
- package/scripts/find_root_content.js +26 -0
- package/scripts/find_unique_products.js +55 -0
- package/scripts/get_jd_product_detail.js +16 -0
- package/scripts/get_jd_products.js +23 -0
- package/scripts/get_jd_seafood_products.js +44 -0
- package/scripts/get_product_details_from_images.js +54 -0
- package/scripts/postinstall.js +235 -0
- package/scripts/scroll_and_get_products.js +47 -0
- package/scripts/scroll_deep_and_find.js +45 -0
- package/scripts/sync-version.js +69 -0
- package/scripts/verify-baidu-enter.ts +116 -0
- package/skills/agent-browser/SKILL.md +310 -0
- package/skills/agent-browser/references/authentication.md +198 -0
- package/skills/agent-browser/references/commands.md +471 -0
- package/skills/agent-browser/references/data-extraction.md +377 -0
- package/skills/agent-browser/references/proxy-support.md +188 -0
- package/skills/agent-browser/references/session-management.md +197 -0
- package/skills/agent-browser/references/snapshot-refs.md +379 -0
- package/skills/agent-browser/references/video-recording.md +173 -0
- package/skills/agent-browser/templates/api-interception.sh +53 -0
- package/skills/agent-browser/templates/authenticated-session.sh +97 -0
- package/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/skills/agent-browser/templates/data-extraction.sh +210 -0
- package/skills/agent-browser/templates/form-automation.sh +62 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +356 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/package_skill.py +113 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// 配置常量
|
|
5
|
+
const TRAJECTORY_INTERVAL = 50;
|
|
6
|
+
const MAX_TRAJECTORY_POINTS = 10;
|
|
7
|
+
const SCROLL_THRESHOLD = 50;
|
|
8
|
+
const HIGHLIGHT_THROTTLE = 100;
|
|
9
|
+
const TOOLBAR_HIDE_DELAY = 500;
|
|
10
|
+
const CACHE_TTL = 100;
|
|
11
|
+
|
|
12
|
+
// 是否隐藏 UI
|
|
13
|
+
const HIDE_UI = window.xyzHide === true;
|
|
14
|
+
|
|
15
|
+
const isInIframe = window.self !== window.top;
|
|
16
|
+
const iframePrefix = isInIframe ? 'iframe >> ' : '';
|
|
17
|
+
|
|
18
|
+
// 当前脚本的会话 ID(在脚本注入时设置)
|
|
19
|
+
// 使用闭包变量保存当前会话 ID,避免被后续脚本覆盖
|
|
20
|
+
// xyzInjectedSessionId 是在脚本字符串中直接嵌入的值
|
|
21
|
+
const thisSessionId = window.xyzInjectedSessionId;
|
|
22
|
+
|
|
23
|
+
// 如果没有会话 ID,跳过
|
|
24
|
+
if (!thisSessionId) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 解析会话 ID 中的时间戳
|
|
29
|
+
const thisTimestamp = parseInt(thisSessionId.replace('recorder-', '')) || 0;
|
|
30
|
+
const currentTimestamp = parseInt((window.xyzSessionId || '').replace('recorder-', '')) || 0;
|
|
31
|
+
|
|
32
|
+
// 检查是否有更新的会话 ID
|
|
33
|
+
// 如果 window.xyzSessionId 存在且时间戳比当前脚本的新,说明有更新的会话
|
|
34
|
+
// 这种情况下,旧的脚本应该跳过初始化
|
|
35
|
+
// 注意:window.xyzSessionId 是由 addInitScript 的状态设置脚本设置的
|
|
36
|
+
// 由于 addInitScript 是累积的,我们需要检查最新的会话 ID
|
|
37
|
+
if (currentTimestamp > thisTimestamp) {
|
|
38
|
+
// 旧脚本,跳过初始化
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 检查录制会话是否有效
|
|
43
|
+
// 注意:只有当会话 ID 匹配时,才检查 xyzStopped
|
|
44
|
+
// 如果会话 ID 不匹配,说明这是旧脚本,应该跳过
|
|
45
|
+
// 如果 xyzStopped 为 true 且没有新的会话 ID,说明录制已停止
|
|
46
|
+
if (window.xyzStopped && (!window.xyzSessionId || window.xyzSessionId === thisSessionId)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 如果已经初始化且会话 ID 相同,跳过
|
|
51
|
+
// 注意:如果 xyzInited 为 false,说明需要重新初始化
|
|
52
|
+
if (window.xyzInited === true && window.xyzInitializedSessionId === thisSessionId) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 标记为已初始化,并记录当前会话 ID
|
|
57
|
+
window.xyzInited = true;
|
|
58
|
+
window.xyzInitializedSessionId = thisSessionId;
|
|
59
|
+
|
|
60
|
+
// ============ 闭包内私有变量 ============
|
|
61
|
+
let stepIdCounter = 0;
|
|
62
|
+
function generateStepId() {
|
|
63
|
+
stepIdCounter = (stepIdCounter + 1) % 1000000;
|
|
64
|
+
return String(stepIdCounter).padStart(6, '0');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 事件队列(步骤存储)
|
|
68
|
+
window.xyzQueue = window.xyzQueue || [];
|
|
69
|
+
|
|
70
|
+
// 鼠标轨迹
|
|
71
|
+
let mousePath = [];
|
|
72
|
+
let lastTime = 0;
|
|
73
|
+
let lastScrollX = window.scrollX;
|
|
74
|
+
let lastScrollY = window.scrollY;
|
|
75
|
+
let scrollTimeout = null;
|
|
76
|
+
let pendingScroll = null;
|
|
77
|
+
let lastFillSelector = null;
|
|
78
|
+
let lastFillValue = '';
|
|
79
|
+
let fillTimeout = null;
|
|
80
|
+
let currentViewport = { width: window.innerWidth, height: window.innerHeight };
|
|
81
|
+
let pendingResize = null;
|
|
82
|
+
let resizeTimeout = null;
|
|
83
|
+
const annotations = new Map();
|
|
84
|
+
const markedElements = new Map();
|
|
85
|
+
const selectorCache = new WeakMap();
|
|
86
|
+
const xpathCache = new WeakMap();
|
|
87
|
+
const highlightCache = new WeakMap();
|
|
88
|
+
|
|
89
|
+
// 暴露初始视口(隐蔽名称)
|
|
90
|
+
window.xyzVp = { ...currentViewport };
|
|
91
|
+
|
|
92
|
+
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'NOSCRIPT', 'BR', 'HR', 'SVG', 'PATH', 'TITLE', 'BASE', 'WBR', 'AREA', 'MAP', 'COL', 'COLGROUP']);
|
|
93
|
+
|
|
94
|
+
// ============ 私有函数:统一事件 API ============
|
|
95
|
+
function pushEvent(action) {
|
|
96
|
+
if (!action || !action.type) {
|
|
97
|
+
return { success: false, steps: window.xyzQueue || [], error: 'Invalid action' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const steps = window.xyzQueue || [];
|
|
101
|
+
|
|
102
|
+
switch (action.type) {
|
|
103
|
+
case 'add':
|
|
104
|
+
if (!action.data) {
|
|
105
|
+
return { success: false, steps, error: 'Missing data for add action' };
|
|
106
|
+
}
|
|
107
|
+
const newStep = { ...action.data, id: action.data.id || generateStepId() };
|
|
108
|
+
steps.push(newStep);
|
|
109
|
+
window.xyzQueue = steps;
|
|
110
|
+
return { success: true, steps };
|
|
111
|
+
|
|
112
|
+
case 'update':
|
|
113
|
+
if (!action.id) {
|
|
114
|
+
return { success: false, steps, error: 'Missing id for update action' };
|
|
115
|
+
}
|
|
116
|
+
const updateIndex = steps.findIndex(s => s.id === action.id);
|
|
117
|
+
if (updateIndex >= 0) {
|
|
118
|
+
steps[updateIndex] = { ...steps[updateIndex], ...action.data };
|
|
119
|
+
window.xyzQueue = steps;
|
|
120
|
+
return { success: true, steps };
|
|
121
|
+
}
|
|
122
|
+
return { success: false, steps, error: 'Step not found' };
|
|
123
|
+
|
|
124
|
+
case 'delete':
|
|
125
|
+
if (!action.id) {
|
|
126
|
+
return { success: false, steps, error: 'Missing id for delete action' };
|
|
127
|
+
}
|
|
128
|
+
const deleteIndex = steps.findIndex(s => s.id === action.id);
|
|
129
|
+
if (deleteIndex >= 0) {
|
|
130
|
+
steps.splice(deleteIndex, 1);
|
|
131
|
+
window.xyzQueue = steps;
|
|
132
|
+
return { success: true, steps };
|
|
133
|
+
}
|
|
134
|
+
return { success: false, steps, error: 'Step not found' };
|
|
135
|
+
|
|
136
|
+
case 'list':
|
|
137
|
+
return { success: true, steps };
|
|
138
|
+
|
|
139
|
+
case 'clear':
|
|
140
|
+
window.xyzQueue = [];
|
|
141
|
+
return { success: true, steps: [] };
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
return { success: false, steps, error: 'Unknown action type' };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function shouldHighlightElement(element) {
|
|
149
|
+
if (!element || !element.tagName) return false;
|
|
150
|
+
if (SKIP_TAGS.has(element.tagName)) return false;
|
|
151
|
+
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
const cached = highlightCache.get(element);
|
|
154
|
+
if (cached && (now - cached.time) < CACHE_TTL) {
|
|
155
|
+
return cached.result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (element.closest) {
|
|
159
|
+
const recorderEl = element.closest('.xyzPnl, .xyzTb, .xyzSh, .xyzMk');
|
|
160
|
+
if (recorderEl) {
|
|
161
|
+
highlightCache.set(element, { time: now, result: false });
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const rect = element.getBoundingClientRect();
|
|
167
|
+
if (rect.width < 5 || rect.height < 5) {
|
|
168
|
+
highlightCache.set(element, { time: now, result: false });
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 排除尺寸接近视口的大元素
|
|
173
|
+
const viewportWidth = window.innerWidth;
|
|
174
|
+
const viewportHeight = window.innerHeight;
|
|
175
|
+
const widthRatio = rect.width / viewportWidth;
|
|
176
|
+
const heightRatio = rect.height / viewportHeight;
|
|
177
|
+
if (widthRatio > 0.7 || heightRatio > 0.7) {
|
|
178
|
+
highlightCache.set(element, { time: now, result: false });
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const style = window.getComputedStyle(element);
|
|
183
|
+
const result = style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) !== 0;
|
|
184
|
+
|
|
185
|
+
highlightCache.set(element, { time: now, result });
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const MESSAGE_TYPE = 'xyzMsg';
|
|
190
|
+
|
|
191
|
+
window.addEventListener('message', (event) => {
|
|
192
|
+
if (event.data && event.data.type === MESSAGE_TYPE && event.data.step) {
|
|
193
|
+
if (!isInIframe) {
|
|
194
|
+
// 使用动态绑定名称
|
|
195
|
+
const bindingName = window.xyzBindingName || 'xyzTrack';
|
|
196
|
+
if (typeof window[bindingName] === 'function') {
|
|
197
|
+
try { window[bindingName](JSON.stringify(event.data.step)); } catch (e) {}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
window.parent.postMessage({ type: MESSAGE_TYPE, step: event.data.step }, '*');
|
|
202
|
+
} catch (e) {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
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();
|
|
223
|
+
}
|
|
224
|
+
lastTime = now;
|
|
225
|
+
}
|
|
226
|
+
}, true);
|
|
227
|
+
|
|
228
|
+
function getTrajectory() {
|
|
229
|
+
// 检查当前会话是否是最新的
|
|
230
|
+
// 由于 addInitScript 是累积的,旧的监听器可能会继续工作
|
|
231
|
+
// 通过比较时间戳来确保只有最新的会话记录事件
|
|
232
|
+
const currentTimestamp = parseInt((window.xyzSessionId || '').replace('recorder-', '')) || 0;
|
|
233
|
+
if (thisTimestamp > 0 && currentTimestamp > thisTimestamp) {
|
|
234
|
+
mousePath = [];
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const points = mousePath.slice(-4);
|
|
239
|
+
mousePath = [];
|
|
240
|
+
return points;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let panelElement = null;
|
|
244
|
+
function isInPanel(element) {
|
|
245
|
+
if (!element) return false;
|
|
246
|
+
|
|
247
|
+
if (!panelElement) {
|
|
248
|
+
panelElement = document.querySelector('.xyzPnl');
|
|
249
|
+
}
|
|
250
|
+
if (panelElement && (element === panelElement || panelElement.contains(element))) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const toolbar = document.querySelector('.xyzTb');
|
|
255
|
+
if (toolbar && (element === toolbar || toolbar.contains(element))) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function syncStepDirect(step) {
|
|
263
|
+
if (!step.id) {
|
|
264
|
+
step.id = 'step-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
|
|
265
|
+
}
|
|
266
|
+
step.viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
267
|
+
step.url = window.location.href;
|
|
268
|
+
step.iframe = isInIframe;
|
|
269
|
+
|
|
270
|
+
pushEvent({ type: 'add', data: step });
|
|
271
|
+
|
|
272
|
+
const bindingName = window.xyzBindingName || 'xyzTrack';
|
|
273
|
+
|
|
274
|
+
if (isInIframe) {
|
|
275
|
+
try {
|
|
276
|
+
window.parent.postMessage({ type: MESSAGE_TYPE, step: step }, '*');
|
|
277
|
+
} catch (e) {}
|
|
278
|
+
} else if (typeof window[bindingName] === 'function') {
|
|
279
|
+
try {
|
|
280
|
+
window[bindingName](JSON.stringify(step));
|
|
281
|
+
} catch (e) {
|
|
282
|
+
console.error('[Sync] failed:', e);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function syncStep(step) {
|
|
288
|
+
if (pendingResize) {
|
|
289
|
+
syncStepDirect({ timestamp: Date.now(), action: 'resize', from: pendingResize.from, to: pendingResize.to });
|
|
290
|
+
pendingResize = null;
|
|
291
|
+
}
|
|
292
|
+
if (pendingScroll) {
|
|
293
|
+
syncStepDirect({ timestamp: Date.now(), action: 'scroll', x: pendingScroll.x, y: pendingScroll.y });
|
|
294
|
+
pendingScroll = null;
|
|
295
|
+
}
|
|
296
|
+
const trajectory = getTrajectory();
|
|
297
|
+
if (trajectory.length > 0) {
|
|
298
|
+
syncStepDirect({ timestamp: Date.now(), action: 'trajectory', points: trajectory });
|
|
299
|
+
}
|
|
300
|
+
syncStepDirect(step);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
window.addEventListener('resize', () => {
|
|
304
|
+
clearTimeout(resizeTimeout);
|
|
305
|
+
resizeTimeout = setTimeout(() => {
|
|
306
|
+
const newWidth = window.innerWidth;
|
|
307
|
+
const newHeight = window.innerHeight;
|
|
308
|
+
if (newWidth !== currentViewport.width || newHeight !== currentViewport.height) {
|
|
309
|
+
pendingResize = { from: { ...currentViewport }, to: { width: newWidth, height: newHeight } };
|
|
310
|
+
currentViewport = { width: newWidth, height: newHeight };
|
|
311
|
+
}
|
|
312
|
+
}, 100);
|
|
313
|
+
}, true);
|
|
314
|
+
|
|
315
|
+
window.addEventListener('scroll', () => {
|
|
316
|
+
clearTimeout(scrollTimeout);
|
|
317
|
+
scrollTimeout = setTimeout(() => {
|
|
318
|
+
const scrollX = window.scrollX;
|
|
319
|
+
const scrollY = window.scrollY;
|
|
320
|
+
if (Math.abs(scrollY - lastScrollY) > SCROLL_THRESHOLD || Math.abs(scrollX - lastScrollX) > SCROLL_THRESHOLD) {
|
|
321
|
+
pendingScroll = { x: scrollX, y: scrollY };
|
|
322
|
+
lastScrollX = scrollX;
|
|
323
|
+
lastScrollY = scrollY;
|
|
324
|
+
}
|
|
325
|
+
}, 100);
|
|
326
|
+
}, true);
|
|
327
|
+
|
|
328
|
+
// ============ XPath 和 Selector 工具函数 ============
|
|
329
|
+
function isUniqueXPath(xpath) {
|
|
330
|
+
try {
|
|
331
|
+
return document.evaluate(
|
|
332
|
+
'count(' + xpath + ')',
|
|
333
|
+
document,
|
|
334
|
+
null,
|
|
335
|
+
XPathResult.NUMBER_TYPE,
|
|
336
|
+
null
|
|
337
|
+
).numberValue === 1;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildUniqueXPath(element, maxDepth = 5) {
|
|
344
|
+
const parts = [];
|
|
345
|
+
let current = element;
|
|
346
|
+
let depth = 0;
|
|
347
|
+
|
|
348
|
+
while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
|
|
349
|
+
let index = 1;
|
|
350
|
+
let sibling = current.previousSibling;
|
|
351
|
+
while (sibling) {
|
|
352
|
+
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) index++;
|
|
353
|
+
sibling = sibling.previousSibling;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let part = current.tagName.toLowerCase() + '[' + index + ']';
|
|
357
|
+
|
|
358
|
+
if (current.id) {
|
|
359
|
+
part = '*[@id="' + current.id + '"]';
|
|
360
|
+
parts.unshift(part);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const semanticAttrs = ['data-testid', 'data-test', 'data-cy', 'aria-label', 'name'];
|
|
365
|
+
for (const attr of semanticAttrs) {
|
|
366
|
+
const value = current.getAttribute(attr);
|
|
367
|
+
if (value) {
|
|
368
|
+
part = '*[@' + attr + '="' + value + '"]';
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
parts.unshift(part);
|
|
374
|
+
|
|
375
|
+
const fullXPath = '/' + parts.join('/');
|
|
376
|
+
if (isUniqueXPath(fullXPath)) {
|
|
377
|
+
return fullXPath;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
current = current.parentNode;
|
|
381
|
+
depth++;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return '/' + parts.join('/');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getXPath(element) {
|
|
388
|
+
if (xpathCache.has(element)) {
|
|
389
|
+
return xpathCache.get(element);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let result = null;
|
|
393
|
+
|
|
394
|
+
if (element.id) {
|
|
395
|
+
const xpath = '//*[@id="' + element.id + '"]';
|
|
396
|
+
if (isUniqueXPath(xpath)) result = xpath;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!result) {
|
|
400
|
+
const semanticAttrs = ['data-testid', 'data-test', 'data-cy', 'aria-label', 'name', 'role', 'title', 'placeholder'];
|
|
401
|
+
for (const attr of semanticAttrs) {
|
|
402
|
+
const value = element.getAttribute(attr);
|
|
403
|
+
if (value) {
|
|
404
|
+
const xpath = '//*[@' + attr + '="' + value + '"]';
|
|
405
|
+
if (isUniqueXPath(xpath)) {
|
|
406
|
+
result = xpath;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!result) {
|
|
414
|
+
const text = element.innerText?.trim();
|
|
415
|
+
if (text && text.length < 30 && ['BUTTON', 'A', 'SPAN', 'LABEL'].includes(element.tagName)) {
|
|
416
|
+
const xpath = '//' + element.tagName.toLowerCase() + '[contains(text(), "' + text.slice(0, 20) + '")]';
|
|
417
|
+
if (isUniqueXPath(xpath)) result = xpath;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!result) {
|
|
422
|
+
const classes = filterUsefulClasses(element);
|
|
423
|
+
if (classes.length > 0) {
|
|
424
|
+
const xpath = '//*[contains(@class, "' + classes[0] + '")]';
|
|
425
|
+
if (isUniqueXPath(xpath)) result = xpath;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!result) {
|
|
430
|
+
result = buildUniqueXPath(element);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
xpathCache.set(element, result);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isUniqueSelector(selector) {
|
|
438
|
+
try {
|
|
439
|
+
return document.querySelectorAll(selector).length === 1;
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ============ 增强的选择器生成函数 ============
|
|
446
|
+
|
|
447
|
+
// 语义属性优先级列表
|
|
448
|
+
const SEMANTIC_ATTRS = [
|
|
449
|
+
'data-testid', 'data-test', 'data-cy',
|
|
450
|
+
'name', 'aria-label', 'aria-labelledby',
|
|
451
|
+
'role', 'type', 'placeholder', 'title', 'alt'
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
// 工具类名排除规则
|
|
455
|
+
const UTILITY_CLASS_PATTERNS = [
|
|
456
|
+
/^_/, // 下划线开头
|
|
457
|
+
/^css-/, // CSS Modules
|
|
458
|
+
/^[a-z]{1,2}$/, // 1-2个字符的短类名
|
|
459
|
+
/^(active|disabled|hidden|visible|selected|hover|focus|current|open|closed)$/i,
|
|
460
|
+
/^(text-|font-|bg-|p-|m-|w-|h-|flex|grid|border|rounded|shadow|opacity|z-)/,
|
|
461
|
+
/^(sm:|md:|lg:|xl:|2xl:)/ // 响应式前缀
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
// 检测高熵类名(CSS Modules/Emotion/Styled Components 自动生成的随机类名)
|
|
465
|
+
// 例如: oMpq4HiN, YoNA2Hyj, qKr0RhiL, GzPW6isY, sc-dkzDqf, css-1a2b3c
|
|
466
|
+
function isHighEntropyClassName(className) {
|
|
467
|
+
if (!className || className.length < 4 || className.length > 15) return false;
|
|
468
|
+
|
|
469
|
+
// CSS Modules: xxx_yyy__zzz 格式
|
|
470
|
+
if (/^[a-zA-Z]+_[a-zA-Z]+_{2}[a-zA-Z0-9]+$/.test(className)) return true;
|
|
471
|
+
|
|
472
|
+
// Emotion/Styled Components: sc-xxxxx 或纯随机字符
|
|
473
|
+
if (/^sc-[a-zA-Z0-9]+$/.test(className)) return true;
|
|
474
|
+
|
|
475
|
+
// 高熵类名特征:混合大小写+数字,无语义分隔符
|
|
476
|
+
// 模式1: 纯字母混合大小写,长度6-12,如 YoNA2Hyj, oMpq4HiN
|
|
477
|
+
const hasUpper = /[A-Z]/.test(className);
|
|
478
|
+
const hasLower = /[a-z]/.test(className);
|
|
479
|
+
const hasDigit = /[0-9]/.test(className);
|
|
480
|
+
const hasSeparator = /[-_]/.test(className);
|
|
481
|
+
|
|
482
|
+
// 如果有分隔符,可能是有意义的(如 btn-primary),不过滤
|
|
483
|
+
if (hasSeparator) return false;
|
|
484
|
+
|
|
485
|
+
// 混合大小写且包含数字,且没有分隔符 -> 高概率是生成的类名
|
|
486
|
+
if (hasUpper && hasLower && hasDigit) return true;
|
|
487
|
+
|
|
488
|
+
// 纯大写字母+数字混合,长度6-10
|
|
489
|
+
if (/^[A-Z][a-z0-9]+[A-Z]/.test(className) && className.length <= 12) return true;
|
|
490
|
+
|
|
491
|
+
// 以小写字母开头,后面有连续大写字母切换的驼峰模式(非语义)
|
|
492
|
+
// 如 "xYzAbC" 这种无意义的交替模式
|
|
493
|
+
if (/^[a-z]/.test(className) && /[a-z][A-Z][a-z][A-Z]/.test(className)) return true;
|
|
494
|
+
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 过滤有用的类名
|
|
499
|
+
function filterUsefulClasses(element) {
|
|
500
|
+
if (!element.className || typeof element.className !== 'string') return [];
|
|
501
|
+
return element.className.trim().split(/\s+/).filter(c => {
|
|
502
|
+
if (!c) return false;
|
|
503
|
+
// 过滤工具类名
|
|
504
|
+
if (UTILITY_CLASS_PATTERNS.some(p => p.test(c))) return false;
|
|
505
|
+
// 过滤高熵类名
|
|
506
|
+
if (isHighEntropyClassName(c)) return false;
|
|
507
|
+
return true;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 策略1: 多属性组合选择器
|
|
512
|
+
function getMultiAttributeSelector(element) {
|
|
513
|
+
const tag = element.tagName.toLowerCase();
|
|
514
|
+
const attrs = [];
|
|
515
|
+
|
|
516
|
+
for (const attr of SEMANTIC_ATTRS) {
|
|
517
|
+
const value = element.getAttribute(attr);
|
|
518
|
+
if (value) {
|
|
519
|
+
attrs.push({ attr, value });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (attrs.length === 0) return null;
|
|
524
|
+
|
|
525
|
+
// 尝试单属性
|
|
526
|
+
for (const { attr, value } of attrs) {
|
|
527
|
+
const selector = tag + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
528
|
+
if (isUniqueSelector(selector)) return selector;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 尝试双属性组合
|
|
532
|
+
if (attrs.length >= 2) {
|
|
533
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
534
|
+
for (let j = i + 1; j < attrs.length; j++) {
|
|
535
|
+
const selector = tag +
|
|
536
|
+
'[' + attrs[i].attr + '="' + CSS.escape(attrs[i].value) + '"]' +
|
|
537
|
+
'[' + attrs[j].attr + '="' + CSS.escape(attrs[j].value) + '"]';
|
|
538
|
+
if (isUniqueSelector(selector)) return selector;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 策略2: 属性 + 类名组合选择器
|
|
547
|
+
function getAttributeClassComboSelector(element) {
|
|
548
|
+
const tag = element.tagName.toLowerCase();
|
|
549
|
+
const classes = filterUsefulClasses(element);
|
|
550
|
+
if (classes.length === 0) return null;
|
|
551
|
+
|
|
552
|
+
// 按长度排序(更长的类名通常更具体)
|
|
553
|
+
classes.sort((a, b) => b.length - a.length);
|
|
554
|
+
const bestClass = classes[0];
|
|
555
|
+
|
|
556
|
+
// 查找可用属性
|
|
557
|
+
for (const attr of SEMANTIC_ATTRS) {
|
|
558
|
+
const value = element.getAttribute(attr);
|
|
559
|
+
if (value) {
|
|
560
|
+
const selector = tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
561
|
+
if (isUniqueSelector(selector)) return selector;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 策略3: 智能类名选择
|
|
569
|
+
function getBestClassSelector(element) {
|
|
570
|
+
const classes = filterUsefulClasses(element);
|
|
571
|
+
if (classes.length === 0) return null;
|
|
572
|
+
|
|
573
|
+
// 按区分度排序(更长的类名通常更具体)
|
|
574
|
+
classes.sort((a, b) => b.length - a.length);
|
|
575
|
+
|
|
576
|
+
const tag = element.tagName.toLowerCase();
|
|
577
|
+
|
|
578
|
+
// 尝试单个类名
|
|
579
|
+
for (const cls of classes) {
|
|
580
|
+
const selector = tag + '.' + CSS.escape(cls);
|
|
581
|
+
if (isUniqueSelector(selector)) return selector;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 尝试组合多个类名
|
|
585
|
+
for (let i = 2; i <= Math.min(3, classes.length); i++) {
|
|
586
|
+
const selector = tag + '.' + classes.slice(0, i).map(c => CSS.escape(c)).join('.');
|
|
587
|
+
if (isUniqueSelector(selector)) return selector;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 策略4: 文本内容选择器
|
|
594
|
+
function getTextBasedSelector(element) {
|
|
595
|
+
const text = element.innerText?.trim();
|
|
596
|
+
if (!text || text.length > 30) return null;
|
|
597
|
+
|
|
598
|
+
// 只对特定标签使用文本选择器
|
|
599
|
+
const textTags = ['BUTTON', 'A', 'SPAN', 'LABEL', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
|
|
600
|
+
if (!textTags.includes(element.tagName)) return null;
|
|
601
|
+
|
|
602
|
+
const tag = element.tagName.toLowerCase();
|
|
603
|
+
const escapedText = text.replace(/"/g, '\\"').slice(0, 20);
|
|
604
|
+
|
|
605
|
+
// Playwright 风格的文本选择器
|
|
606
|
+
const selector = tag + ':has-text("' + escapedText + '")';
|
|
607
|
+
// 注意:这种选择器在 querySelectorAll 中不直接支持,仅作为备选记录
|
|
608
|
+
return null; // 暂时返回 null,因为标准 CSS 不支持 :has-text
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 策略5: 兄弟元素定位
|
|
612
|
+
function getSiblingBasedSelector(element) {
|
|
613
|
+
const sibling = element.previousElementSibling;
|
|
614
|
+
if (!sibling) return null;
|
|
615
|
+
|
|
616
|
+
// 查找前面有特征的兄弟元素
|
|
617
|
+
let prevSibling = sibling;
|
|
618
|
+
let attempts = 0;
|
|
619
|
+
while (prevSibling && attempts < 3) {
|
|
620
|
+
const siblingSelector = getFeatureSelector(prevSibling);
|
|
621
|
+
if (siblingSelector && isUniqueSelector(siblingSelector)) {
|
|
622
|
+
const elementSelector = getBaseSelector(element);
|
|
623
|
+
const combined = siblingSelector + ' + ' + elementSelector;
|
|
624
|
+
if (isUniqueSelector(combined)) return combined;
|
|
625
|
+
}
|
|
626
|
+
prevSibling = prevSibling.previousElementSibling;
|
|
627
|
+
attempts++;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 获取元素的特征选择器(用于父元素或兄弟元素)
|
|
634
|
+
function getFeatureSelector(element) {
|
|
635
|
+
if (!element || element === document.body) return null;
|
|
636
|
+
|
|
637
|
+
// ID 优先
|
|
638
|
+
if (element.id) {
|
|
639
|
+
return '#' + CSS.escape(element.id);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 有特征的属性
|
|
643
|
+
for (const attr of ['data-testid', 'data-test', 'name', 'role', 'aria-label']) {
|
|
644
|
+
const value = element.getAttribute(attr);
|
|
645
|
+
if (value) {
|
|
646
|
+
return element.tagName.toLowerCase() + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 唯一的类名选择器
|
|
651
|
+
const classes = filterUsefulClasses(element);
|
|
652
|
+
if (classes.length > 0) {
|
|
653
|
+
classes.sort((a, b) => b.length - a.length);
|
|
654
|
+
const selector = element.tagName.toLowerCase() + '.' + CSS.escape(classes[0]);
|
|
655
|
+
if (isUniqueSelector(selector)) return selector;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function getBaseSelector(element) {
|
|
662
|
+
let selector = element.tagName.toLowerCase();
|
|
663
|
+
const classes = filterUsefulClasses(element);
|
|
664
|
+
if (classes.length > 0) {
|
|
665
|
+
// 按长度排序,取最具体的类名
|
|
666
|
+
classes.sort((a, b) => b.length - a.length);
|
|
667
|
+
selector += '.' + classes.slice(0, 2).map(c => CSS.escape(c)).join('.');
|
|
668
|
+
}
|
|
669
|
+
return selector;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function makeUniqueWithNth(element, baseSelector) {
|
|
673
|
+
const parent = element.parentElement;
|
|
674
|
+
if (!parent) return baseSelector;
|
|
675
|
+
|
|
676
|
+
const siblings = Array.from(parent.children);
|
|
677
|
+
const sameTagSiblings = siblings.filter(s => s.tagName === element.tagName);
|
|
678
|
+
|
|
679
|
+
if (sameTagSiblings.length === 1) {
|
|
680
|
+
return baseSelector;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const index = siblings.indexOf(element) + 1;
|
|
684
|
+
return baseSelector + ':nth-child(' + index + ')';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// 策略6: 智能父子组合选择器
|
|
688
|
+
function buildComposedSelector(element, maxDepth = 3) {
|
|
689
|
+
const parts = [];
|
|
690
|
+
let current = element;
|
|
691
|
+
let depth = 0;
|
|
692
|
+
|
|
693
|
+
// 先尝试元素自身的选择器
|
|
694
|
+
const selfSelector = getBestClassSelector(element);
|
|
695
|
+
if (selfSelector && isUniqueSelector(selfSelector)) {
|
|
696
|
+
return selfSelector;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 向上查找有特征的祖先
|
|
700
|
+
while (current && current !== document.body && depth < maxDepth) {
|
|
701
|
+
const featureSelector = getFeatureSelector(current);
|
|
702
|
+
if (featureSelector) {
|
|
703
|
+
parts.unshift(featureSelector);
|
|
704
|
+
const combined = parts.join(' > ');
|
|
705
|
+
// 添加当前元素的类名选择器
|
|
706
|
+
const elementSelector = depth === 0 ? getBaseSelector(element) : getBaseSelector(current);
|
|
707
|
+
const fullSelector = combined + (depth > 0 ? '' : ' > ' + elementSelector);
|
|
708
|
+
if (isUniqueSelector(fullSelector)) {
|
|
709
|
+
return fullSelector;
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
// 如果没有特征选择器,使用基本选择器
|
|
713
|
+
const baseSelector = getBaseSelector(current);
|
|
714
|
+
const selector = makeUniqueWithNth(current, baseSelector);
|
|
715
|
+
parts.unshift(selector);
|
|
716
|
+
|
|
717
|
+
const fullSelector = parts.join(' > ');
|
|
718
|
+
if (isUniqueSelector(fullSelector)) {
|
|
719
|
+
return fullSelector;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
current = current.parentElement;
|
|
724
|
+
depth++;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (parts.length > 0) {
|
|
728
|
+
return parts.join(' > ');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function buildUniquePath(element, maxDepth = 5) {
|
|
735
|
+
const parts = [];
|
|
736
|
+
let current = element;
|
|
737
|
+
let depth = 0;
|
|
738
|
+
|
|
739
|
+
while (current && current !== document.body && depth < maxDepth) {
|
|
740
|
+
const baseSelector = getBaseSelector(current);
|
|
741
|
+
const selector = makeUniqueWithNth(current, baseSelector);
|
|
742
|
+
parts.unshift(selector);
|
|
743
|
+
|
|
744
|
+
const fullSelector = parts.join(' > ');
|
|
745
|
+
if (isUniqueSelector(fullSelector)) {
|
|
746
|
+
return fullSelector;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
current = current.parentElement;
|
|
750
|
+
depth++;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (parts.length > 0) {
|
|
754
|
+
return parts.join(' > ');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getShadowHost(element) {
|
|
761
|
+
let current = element;
|
|
762
|
+
while (current) {
|
|
763
|
+
if (current.getRootNode() instanceof ShadowRoot) {
|
|
764
|
+
return current.getRootNode().host;
|
|
765
|
+
}
|
|
766
|
+
current = current.parentElement;
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function getSelectorWithShadow(element) {
|
|
772
|
+
const shadowHost = getShadowHost(element);
|
|
773
|
+
if (shadowHost) {
|
|
774
|
+
const hostSelector = getSelector(shadowHost);
|
|
775
|
+
const innerSelector = getSelectorInternal(element);
|
|
776
|
+
return hostSelector + ' >>> ' + innerSelector;
|
|
777
|
+
}
|
|
778
|
+
return getSelectorInternal(element);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 优化后的主选择器生成函数
|
|
782
|
+
function getSelectorInternal(element) {
|
|
783
|
+
if (selectorCache.has(element)) {
|
|
784
|
+
return selectorCache.get(element);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
let result = null;
|
|
788
|
+
const root = element.getRootNode();
|
|
789
|
+
|
|
790
|
+
// 策略1: ID 选择器(最高优先级)
|
|
791
|
+
if (element.id) {
|
|
792
|
+
const selector = '#' + CSS.escape(element.id);
|
|
793
|
+
try {
|
|
794
|
+
if (root.querySelectorAll(selector).length === 1) result = selector;
|
|
795
|
+
} catch (e) {}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 策略2: 多属性组合选择器
|
|
799
|
+
if (!result) {
|
|
800
|
+
result = getMultiAttributeSelector(element);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 策略3: 属性 + 类名组合
|
|
804
|
+
if (!result) {
|
|
805
|
+
result = getAttributeClassComboSelector(element);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// 策略4: 智能类名选择
|
|
809
|
+
if (!result) {
|
|
810
|
+
result = getBestClassSelector(element);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 策略5: 兄弟元素定位
|
|
814
|
+
if (!result) {
|
|
815
|
+
result = getSiblingBasedSelector(element);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 策略6: 智能父子组合
|
|
819
|
+
if (!result) {
|
|
820
|
+
result = buildComposedSelector(element);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 策略7: 基本类名 + nth-child
|
|
824
|
+
if (!result) {
|
|
825
|
+
const baseSelector = getBaseSelector(element);
|
|
826
|
+
const uniqueSelector = makeUniqueWithNth(element, baseSelector);
|
|
827
|
+
try {
|
|
828
|
+
if (root.querySelectorAll(uniqueSelector).length === 1) result = uniqueSelector;
|
|
829
|
+
} catch (e) {}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 策略8: 路径选择器(兜底)
|
|
833
|
+
if (!result) {
|
|
834
|
+
const pathSelector = buildUniquePath(element);
|
|
835
|
+
if (pathSelector) result = pathSelector;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// 最终兜底
|
|
839
|
+
if (!result) {
|
|
840
|
+
result = element.tagName.toLowerCase();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
selectorCache.set(element, result);
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function getSelector(element) {
|
|
848
|
+
return getSelectorWithShadow(element);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function getElementInfo(element) {
|
|
852
|
+
return {
|
|
853
|
+
tagName: element.tagName.toLowerCase(),
|
|
854
|
+
id: element.id,
|
|
855
|
+
className: element.className,
|
|
856
|
+
text: element.innerText ? element.innerText.slice(0, 50) : '',
|
|
857
|
+
xpath: getXPath(element)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ============ 录制核心逻辑 ============
|
|
862
|
+
function recordStep(action, data) {
|
|
863
|
+
// 检查是否暂停录制或已停止
|
|
864
|
+
// 注意:xyzActive 可能是 undefined(在 iframe 中),所以只检查明确为 false 的情况
|
|
865
|
+
if (window.xyzActive === false || window.xyzPaused || window.xyzStopped) return;
|
|
866
|
+
|
|
867
|
+
// 检查当前会话是否是最新的
|
|
868
|
+
// 由于 addInitScript 是累积的,旧的监听器可能会继续工作
|
|
869
|
+
// 通过比较时间戳来确保只有最新的会话记录事件
|
|
870
|
+
const currentTimestamp = parseInt((window.xyzSessionId || '').replace('recorder-', '')) || 0;
|
|
871
|
+
if (thisTimestamp > 0 && currentTimestamp > thisTimestamp) return;
|
|
872
|
+
|
|
873
|
+
const step = {
|
|
874
|
+
id: 'step-' + Date.now() + '-' + Math.random().toString(36).substr(2, 10),
|
|
875
|
+
timestamp: Date.now(),
|
|
876
|
+
action: action,
|
|
877
|
+
selector: iframePrefix + (data.selector || ''),
|
|
878
|
+
xpath: iframePrefix + (data.xpath || ''),
|
|
879
|
+
value: data.value,
|
|
880
|
+
elementInfo: data.elementInfo,
|
|
881
|
+
annotation: data.annotation,
|
|
882
|
+
iframe: isInIframe
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
if (action === 'keyboard') {
|
|
886
|
+
delete step.selector;
|
|
887
|
+
delete step.xpath;
|
|
888
|
+
delete step.elementInfo;
|
|
889
|
+
// 复制键盘事件相关属性
|
|
890
|
+
step.key = data.key;
|
|
891
|
+
step.code = data.code;
|
|
892
|
+
step.ctrlKey = data.ctrlKey;
|
|
893
|
+
step.metaKey = data.metaKey;
|
|
894
|
+
step.altKey = data.altKey;
|
|
895
|
+
step.shiftKey = data.shiftKey;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
syncStep(step);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
document.addEventListener('click', (e) => {
|
|
902
|
+
const path = e.composedPath();
|
|
903
|
+
const element = path[0] || e.target;
|
|
904
|
+
|
|
905
|
+
if (isInPanel(element)) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (element === document.body || element === document.documentElement) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const link = element.closest('a[href]');
|
|
913
|
+
if (link) {
|
|
914
|
+
const href = link.href;
|
|
915
|
+
const target = link.target || '_self';
|
|
916
|
+
let isExternal = target === '_blank';
|
|
917
|
+
if (!isExternal && href.startsWith('http')) {
|
|
918
|
+
try {
|
|
919
|
+
const linkHost = new URL(href).host;
|
|
920
|
+
isExternal = linkHost !== window.location.host;
|
|
921
|
+
} catch (e) {}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
recordStep('link_click', {
|
|
925
|
+
selector: getSelector(link),
|
|
926
|
+
xpath: getXPath(link),
|
|
927
|
+
value: href,
|
|
928
|
+
elementInfo: { ...getElementInfo(link), target: target, isExternal: isExternal }
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
recordStep('click', {
|
|
934
|
+
selector: getSelector(element),
|
|
935
|
+
xpath: getXPath(element),
|
|
936
|
+
elementInfo: getElementInfo(element)
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// 非隐藏模式下添加标记
|
|
940
|
+
if (!HIDE_UI && !isInIframe && typeof addMarker === 'function') {
|
|
941
|
+
addMarker(element, 'default');
|
|
942
|
+
}
|
|
943
|
+
}, true);
|
|
944
|
+
|
|
945
|
+
document.addEventListener('input', (e) => {
|
|
946
|
+
const element = e.target;
|
|
947
|
+
if (!element || !element.tagName) return;
|
|
948
|
+
if (isInPanel(element)) return;
|
|
949
|
+
|
|
950
|
+
// Skip checkbox, radio, and select - they are handled by click and change events
|
|
951
|
+
if (element.tagName === 'SELECT') return;
|
|
952
|
+
const inputType = (element.type || '').toLowerCase();
|
|
953
|
+
if (inputType === 'checkbox' || inputType === 'radio') return;
|
|
954
|
+
|
|
955
|
+
const selector = getSelector(element);
|
|
956
|
+
const value = element.value;
|
|
957
|
+
|
|
958
|
+
clearTimeout(fillTimeout);
|
|
959
|
+
|
|
960
|
+
if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
|
|
961
|
+
recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
lastFillSelector = selector;
|
|
965
|
+
lastFillValue = value;
|
|
966
|
+
|
|
967
|
+
fillTimeout = setTimeout(() => {
|
|
968
|
+
if (lastFillSelector && lastFillValue) {
|
|
969
|
+
recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
|
|
970
|
+
lastFillSelector = null;
|
|
971
|
+
lastFillValue = '';
|
|
972
|
+
}
|
|
973
|
+
}, 300);
|
|
974
|
+
}, true); // capture phase
|
|
975
|
+
|
|
976
|
+
// Also listen in bubbling phase to catch programmatically dispatched events
|
|
977
|
+
document.addEventListener('input', (e) => {
|
|
978
|
+
const element = e.target;
|
|
979
|
+
if (!element || !element.tagName) return;
|
|
980
|
+
if (isInPanel(element)) return;
|
|
981
|
+
|
|
982
|
+
// Skip checkbox, radio, and select - they are handled by click and change events
|
|
983
|
+
if (element.tagName === 'SELECT') return;
|
|
984
|
+
const inputType = (element.type || '').toLowerCase();
|
|
985
|
+
if (inputType === 'checkbox' || inputType === 'radio') return;
|
|
986
|
+
|
|
987
|
+
const selector = getSelector(element);
|
|
988
|
+
const value = element.value;
|
|
989
|
+
|
|
990
|
+
// Only process if not already processed in capture phase
|
|
991
|
+
if (lastFillSelector === selector && lastFillValue === value) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
clearTimeout(fillTimeout);
|
|
996
|
+
|
|
997
|
+
if (lastFillSelector && lastFillSelector !== selector && lastFillValue) {
|
|
998
|
+
recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
lastFillSelector = selector;
|
|
1002
|
+
lastFillValue = value;
|
|
1003
|
+
|
|
1004
|
+
fillTimeout = setTimeout(() => {
|
|
1005
|
+
if (lastFillSelector && lastFillValue) {
|
|
1006
|
+
recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
|
|
1007
|
+
lastFillSelector = null;
|
|
1008
|
+
lastFillValue = '';
|
|
1009
|
+
}
|
|
1010
|
+
}, 300);
|
|
1011
|
+
}, false); // bubbling phase
|
|
1012
|
+
|
|
1013
|
+
// 标记事件监听器已注册
|
|
1014
|
+
window.xyzHasInputListener = true;
|
|
1015
|
+
|
|
1016
|
+
document.addEventListener('change', (e) => {
|
|
1017
|
+
const element = e.target;
|
|
1018
|
+
if (!element || element.tagName !== 'SELECT') return;
|
|
1019
|
+
if (isInPanel(element)) return;
|
|
1020
|
+
|
|
1021
|
+
recordStep('select', {
|
|
1022
|
+
selector: getSelector(element),
|
|
1023
|
+
xpath: getXPath(element),
|
|
1024
|
+
value: element.value,
|
|
1025
|
+
elementInfo: getElementInfo(element)
|
|
1026
|
+
});
|
|
1027
|
+
}, true);
|
|
1028
|
+
|
|
1029
|
+
document.addEventListener('keydown', (e) => {
|
|
1030
|
+
const element = document.activeElement;
|
|
1031
|
+
console.log('[Recorder] keydown event:', e.key, 'target:', e.target, 'activeElement:', element?.tagName);
|
|
1032
|
+
if (isInPanel(element)) return;
|
|
1033
|
+
|
|
1034
|
+
const specialKeys = ['Enter', 'Tab', 'Escape', 'Backspace', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
|
1035
|
+
|
|
1036
|
+
if (specialKeys.includes(e.key) || e.ctrlKey || e.metaKey || e.altKey) {
|
|
1037
|
+
if (!e.ctrlKey && !e.metaKey && !e.altKey && !specialKeys.includes(e.key)) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
console.log('[Recorder] Recording keyboard step:', e.key);
|
|
1042
|
+
recordStep('keyboard', {
|
|
1043
|
+
key: e.key,
|
|
1044
|
+
code: e.code,
|
|
1045
|
+
ctrlKey: e.ctrlKey,
|
|
1046
|
+
metaKey: e.metaKey,
|
|
1047
|
+
altKey: e.altKey,
|
|
1048
|
+
shiftKey: e.shiftKey,
|
|
1049
|
+
selector: element ? getSelector(element) : '',
|
|
1050
|
+
xpath: element ? getXPath(element) : '',
|
|
1051
|
+
elementInfo: element ? getElementInfo(element) : null
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}, true);
|
|
1055
|
+
|
|
1056
|
+
window.addEventListener('beforeunload', () => {
|
|
1057
|
+
if (lastFillSelector && lastFillValue) {
|
|
1058
|
+
syncStepDirect({
|
|
1059
|
+
id: 'step-' + Date.now(),
|
|
1060
|
+
timestamp: Date.now(),
|
|
1061
|
+
action: 'fill',
|
|
1062
|
+
selector: lastFillSelector,
|
|
1063
|
+
value: lastFillValue
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
syncStepDirect({
|
|
1067
|
+
id: 'step-' + Date.now(),
|
|
1068
|
+
timestamp: Date.now(),
|
|
1069
|
+
action: 'navigate',
|
|
1070
|
+
value: window.location.href
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
window.xyzFlushPending = function() {
|
|
1075
|
+
if (lastFillSelector && lastFillValue) {
|
|
1076
|
+
recordStep('fill', { selector: lastFillSelector, value: lastFillValue });
|
|
1077
|
+
lastFillSelector = null;
|
|
1078
|
+
lastFillValue = '';
|
|
1079
|
+
}
|
|
1080
|
+
if (fillTimeout) {
|
|
1081
|
+
clearTimeout(fillTimeout);
|
|
1082
|
+
fillTimeout = null;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// ============ UI 部分(仅在非隐藏模式下创建)============
|
|
1087
|
+
if (!isInIframe && !HIDE_UI) {
|
|
1088
|
+
let _animationFrameId = null;
|
|
1089
|
+
let _highlightRafId = null;
|
|
1090
|
+
let _toolbarHideTimeout = null;
|
|
1091
|
+
let _pollInterval = null;
|
|
1092
|
+
let _checkPanelInterval = null;
|
|
1093
|
+
|
|
1094
|
+
// 关闭面板函数(暴露给外部调用)
|
|
1095
|
+
window.xyzClose = function() {
|
|
1096
|
+
if (_animationFrameId) {
|
|
1097
|
+
cancelAnimationFrame(_animationFrameId);
|
|
1098
|
+
_animationFrameId = null;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (_highlightRafId) {
|
|
1102
|
+
cancelAnimationFrame(_highlightRafId);
|
|
1103
|
+
_highlightRafId = null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
clearTimeout(_toolbarHideTimeout);
|
|
1107
|
+
|
|
1108
|
+
if (_pollInterval) {
|
|
1109
|
+
clearInterval(_pollInterval);
|
|
1110
|
+
_pollInterval = null;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (_checkPanelInterval) {
|
|
1114
|
+
clearInterval(_checkPanelInterval);
|
|
1115
|
+
_checkPanelInterval = null;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const elements = [
|
|
1119
|
+
document.getElementById('xyzPnl'),
|
|
1120
|
+
document.getElementById('xyzMk'),
|
|
1121
|
+
document.getElementById('xyzCv'),
|
|
1122
|
+
document.getElementById('xyzSh'),
|
|
1123
|
+
document.getElementById('xyzTb'),
|
|
1124
|
+
document.getElementById('xyzSt')
|
|
1125
|
+
];
|
|
1126
|
+
|
|
1127
|
+
elements.forEach(el => {
|
|
1128
|
+
if (el && el.parentNode) {
|
|
1129
|
+
el.parentNode.removeChild(el);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
window.xyzInited = false;
|
|
1134
|
+
window.xyzQueue = [];
|
|
1135
|
+
console.log('[Panel] closed');
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// 检查录制会话是否已停止
|
|
1139
|
+
if (window.xyzStopped) {
|
|
1140
|
+
console.log('[Panel] Session was stopped, skipping panel creation');
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// 检查录制会话是否激活
|
|
1145
|
+
if (!window.xyzActive) {
|
|
1146
|
+
console.log('[Panel] Session not active, skipping panel creation');
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
let uiElements = {};
|
|
1151
|
+
let toolbarHideTimeout = null;
|
|
1152
|
+
let isOverToolbar = false;
|
|
1153
|
+
let currentElement = null;
|
|
1154
|
+
let mouseX = 0, mouseY = 0, currentEdge = null;
|
|
1155
|
+
const EDGE_THRESHOLD = 30;
|
|
1156
|
+
let animationFrameId = null;
|
|
1157
|
+
let highlightRafId = null;
|
|
1158
|
+
let pendingHighlightElement = null;
|
|
1159
|
+
let lastTrajectoryLength = 0;
|
|
1160
|
+
let currentStepIndex = -1;
|
|
1161
|
+
let currentStepId = null;
|
|
1162
|
+
|
|
1163
|
+
function addMarker(element, type) {
|
|
1164
|
+
if (!element || markedElements.has(element)) return;
|
|
1165
|
+
const markersContainer = document.getElementById('xyzMk');
|
|
1166
|
+
if (!markersContainer) return;
|
|
1167
|
+
|
|
1168
|
+
const marker = document.createElement('div');
|
|
1169
|
+
marker.className = 'xyzMrk' + (type !== 'default' ? ' ' + type : '');
|
|
1170
|
+
markersContainer.appendChild(marker);
|
|
1171
|
+
markedElements.set(element, { marker, type });
|
|
1172
|
+
|
|
1173
|
+
const rect = element.getBoundingClientRect();
|
|
1174
|
+
marker.style.left = rect.left + 'px';
|
|
1175
|
+
marker.style.top = rect.top + 'px';
|
|
1176
|
+
marker.style.width = rect.width + 'px';
|
|
1177
|
+
marker.style.height = rect.height + 'px';
|
|
1178
|
+
marker.style.display = 'block';
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function createRecorderOverlay() {
|
|
1182
|
+
if (!document.body) {
|
|
1183
|
+
if (document.readyState === 'loading') {
|
|
1184
|
+
document.addEventListener('DOMContentLoaded', createRecorderOverlay);
|
|
1185
|
+
} else {
|
|
1186
|
+
setTimeout(createRecorderOverlay, 10);
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const existingPanel = document.getElementById('xyzPnl');
|
|
1192
|
+
|
|
1193
|
+
// 检查并创建样式
|
|
1194
|
+
let style = document.getElementById('xyzSt');
|
|
1195
|
+
if (!style) {
|
|
1196
|
+
style = document.createElement('style');
|
|
1197
|
+
style.id = 'xyzSt';
|
|
1198
|
+
style.textContent = `
|
|
1199
|
+
.xyzPnl { position: fixed; right: 20px; top: 20px; width: 320px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 2147483647; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
1200
|
+
.xyzPnl-hdr { padding: 12px 15px; background: #333; color: white; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; }
|
|
1201
|
+
.xyzPnl-hdr h3 { font-size: 14px; font-weight: 500; margin: 0; }
|
|
1202
|
+
.xyzPnl-hdr button { padding: 4px 10px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; background: #555; color: white; }
|
|
1203
|
+
.xyzPnl-hdr button:hover { background: #666; }
|
|
1204
|
+
.xyzPnl-bdy { flex: 1; overflow-y: auto; padding: 10px; max-height: 400px; }
|
|
1205
|
+
.xyzStp { padding: 8px 10px; border-radius: 4px; margin-bottom: 6px; background: #f5f5f5; font-size: 12px; border-left: 3px solid #4CAF50; position: relative; }
|
|
1206
|
+
.xyzStp.click { border-left-color: #4CAF50; }
|
|
1207
|
+
.xyzStp.fill { border-left-color: #2196F3; }
|
|
1208
|
+
.xyzStp.select { border-left-color: #FF9800; }
|
|
1209
|
+
.xyzStp.link_click { border-left-color: #9C27B0; }
|
|
1210
|
+
.xyzStp.navigate { border-left-color: #607D8B; }
|
|
1211
|
+
.xyzStp.annotate { border-left-color: #E91E63; }
|
|
1212
|
+
.xyzStp .action { font-weight: 500; color: #333; }
|
|
1213
|
+
.xyzStp .selector { color: #666; word-break: break-all; margin-top: 4px; font-size: 11px; }
|
|
1214
|
+
.xyzStp .value { color: #888; margin-top: 2px; font-size: 11px; }
|
|
1215
|
+
.xyzStp .annotation { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-top: 4px; background: #E8F5E9; color: #388E3C; }
|
|
1216
|
+
.xyzStp.selected { background: #e3f2fd; border-left-color: #2196F3; }
|
|
1217
|
+
.xyzStp:hover { background: #f0f0f0; }
|
|
1218
|
+
.xyzStp.selected:hover { background: #e3f2fd; }
|
|
1219
|
+
.xyzDelBtn { position: absolute; right: 8px; bottom: 8px; padding: 2px 6px; font-size: 12px; border: none; border-radius: 3px; background: #ffebee; cursor: pointer; opacity: 0.8; }
|
|
1220
|
+
.xyzDelBtn:hover { background: #ffcdd2; opacity: 1; }
|
|
1221
|
+
.xyzEmpty { color: #999; text-align: center; padding: 20px; font-size: 13px; }
|
|
1222
|
+
.xyzStatus { padding: 8px 15px; background: #f0f0f0; font-size: 11px; color: #666; border-top: 1px solid #eee; }
|
|
1223
|
+
.xyzPnl-tools { padding: 8px 10px; background: #fafafa; border-top: 1px solid #eee; }
|
|
1224
|
+
.xyzTools-label { font-size: 11px; color: #666; margin-bottom: 6px; font-weight: 500; }
|
|
1225
|
+
.xyzTools-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
1226
|
+
.xyzTools-list .tool-btn { padding: 4px 8px; font-size: 10px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; transition: all 0.2s; }
|
|
1227
|
+
.xyzTools-list .tool-btn:hover { background: #f0f0f0; border-color: #bbb; }
|
|
1228
|
+
.xyzTools-list .tool-btn:active { transform: scale(0.95); }
|
|
1229
|
+
#xyzCollapse { padding: 4px 8px; font-size: 14px; font-weight: bold; border: none; border-radius: 4px; cursor: pointer; background: #555; color: white; margin-right: 5px; }
|
|
1230
|
+
#xyzCollapse:hover { background: #666; }
|
|
1231
|
+
.xyzSh { position: absolute; pointer-events: none; border: 2px solid #4CAF50; background: rgba(76, 175, 80, 0.1); border-radius: 4px; z-index: 2147483646; transition: all 0.2s ease-out; will-change: transform, width, height; }
|
|
1232
|
+
.xyzSh.login { border-color: #2196F3; background: rgba(33, 150, 243, 0.1); }
|
|
1233
|
+
.xyzSh.data { border-color: #FF9800; background: rgba(255, 152, 0, 0.1); }
|
|
1234
|
+
.xyzSh.pagination { border-color: #9C27B0; background: rgba(156, 39, 176, 0.1); }
|
|
1235
|
+
.xyzTb { position: absolute; z-index: 2147483647; background: white; border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); padding: 4px; display: flex; gap: 2px; pointer-events: auto; }
|
|
1236
|
+
.xyzTb.horizontal { flex-direction: row; }
|
|
1237
|
+
.xyzTb.vertical { flex-direction: column; }
|
|
1238
|
+
.xyzTb button { padding: 5px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; white-space: nowrap; pointer-events: auto; }
|
|
1239
|
+
.xyzTb button:hover { transform: scale(1.02); }
|
|
1240
|
+
.xyzTb .btn-login { background: #E3F2FD; color: #1976D2; }
|
|
1241
|
+
.xyzTb .btn-data { background: #FFF3E0; color: #F57C00; }
|
|
1242
|
+
.xyzTb .btn-page { background: #F3E5F5; color: #7B1FA2; }
|
|
1243
|
+
.xyzTb .btn-note { background: #E8F5E9; color: #388E3C; }
|
|
1244
|
+
.xyzTb .btn-wait { background: #FFF8E1; color: #F9A825; }
|
|
1245
|
+
.xyzTb .btn-container { background: #E0F7FA; color: #00838F; }
|
|
1246
|
+
.xyzTb .btn-item { background: #FFF3E0; color: #F57C00; }
|
|
1247
|
+
.xyzTb .btn-check { background: #E8F5E9; color: #2E7D32; }
|
|
1248
|
+
.xyzMk { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2147483645; }
|
|
1249
|
+
.xyzMrk { position: absolute; pointer-events: none; border: 2px solid rgba(76, 175, 80, 0.6); border-radius: 4px; transition: all 0.2s ease-out; }
|
|
1250
|
+
.xyzMrk.login { border-color: rgba(33, 150, 243, 0.8); }
|
|
1251
|
+
.xyzMrk.data { border-color: rgba(255, 152, 0, 0.8); }
|
|
1252
|
+
.xyzMrk.pagination { border-color: rgba(156, 39, 176, 0.8); }
|
|
1253
|
+
.xyzMrk.custom { border-color: rgba(76, 175, 80, 0.8); }
|
|
1254
|
+
#xyzCv { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2147483644; }
|
|
1255
|
+
`;
|
|
1256
|
+
(document.head || document.documentElement).appendChild(style);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (existingPanel) {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const panel = document.createElement('div');
|
|
1264
|
+
panel.className = 'xyzPnl';
|
|
1265
|
+
panel.id = 'xyzPnl';
|
|
1266
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
1267
|
+
panel.innerHTML = `
|
|
1268
|
+
<div class="xyzPnl-hdr">
|
|
1269
|
+
<h3>📝 Recorder</h3>
|
|
1270
|
+
<div>
|
|
1271
|
+
<button id="xyzCollapse">−</button>
|
|
1272
|
+
<button id="xyzClear">Clear</button>
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
<div class="xyzPnl-bdy" id="xyzSteps">
|
|
1276
|
+
<div class="xyzEmpty">No steps recorded yet</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
<div class="xyzPnl-tools" id="xyzTools">
|
|
1279
|
+
<div class="xyzTools-label">+ Tool:</div>
|
|
1280
|
+
<div class="xyzTools-list">
|
|
1281
|
+
<button class="tool-btn" data-tool="wait_element">⏳ Wait</button>
|
|
1282
|
+
<button class="tool-btn" data-tool="data_container">📦 Container</button>
|
|
1283
|
+
<button class="tool-btn" data-tool="data_item">📊 Item</button>
|
|
1284
|
+
<button class="tool-btn" data-tool="pagination">📄 Page</button>
|
|
1285
|
+
<button class="tool-btn" data-tool="login_check">🔐 Login</button>
|
|
1286
|
+
<button class="tool-btn" data-tool="checkpoint">✅ Check</button>
|
|
1287
|
+
<button class="tool-btn" data-tool="custom">📝 Note</button>
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
<div class="xyzStatus" id="xyzStatus">Steps: 0</div>
|
|
1291
|
+
`;
|
|
1292
|
+
document.body.appendChild(panel);
|
|
1293
|
+
panelElement = panel;
|
|
1294
|
+
|
|
1295
|
+
// Panel collapse functionality
|
|
1296
|
+
let isCollapsed = false;
|
|
1297
|
+
let autoScroll = true;
|
|
1298
|
+
const collapseBtn = document.getElementById('xyzCollapse');
|
|
1299
|
+
const panelBody = document.getElementById('xyzSteps');
|
|
1300
|
+
const panelTools = document.getElementById('xyzTools');
|
|
1301
|
+
const panelStatus = document.getElementById('xyzStatus');
|
|
1302
|
+
|
|
1303
|
+
collapseBtn.addEventListener('click', () => {
|
|
1304
|
+
isCollapsed = !isCollapsed;
|
|
1305
|
+
if (isCollapsed) {
|
|
1306
|
+
panelBody.style.display = 'none';
|
|
1307
|
+
panelTools.style.display = 'none';
|
|
1308
|
+
panelStatus.style.display = 'none';
|
|
1309
|
+
collapseBtn.textContent = '+';
|
|
1310
|
+
} else {
|
|
1311
|
+
panelBody.style.display = 'block';
|
|
1312
|
+
panelTools.style.display = 'block';
|
|
1313
|
+
panelStatus.style.display = 'block';
|
|
1314
|
+
collapseBtn.textContent = '−';
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Prevent scroll penetration
|
|
1319
|
+
panelBody.addEventListener('wheel', (e) => {
|
|
1320
|
+
const { scrollTop, scrollHeight, clientHeight } = panelBody;
|
|
1321
|
+
const atTop = scrollTop === 0;
|
|
1322
|
+
const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
|
1323
|
+
|
|
1324
|
+
if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
|
|
1325
|
+
e.preventDefault();
|
|
1326
|
+
}
|
|
1327
|
+
}, { passive: false });
|
|
1328
|
+
|
|
1329
|
+
// Track scroll position for auto-scroll
|
|
1330
|
+
panelBody.addEventListener('scroll', () => {
|
|
1331
|
+
const isAtBottom = panelBody.scrollHeight - panelBody.scrollTop - panelBody.clientHeight < 10;
|
|
1332
|
+
autoScroll = isAtBottom;
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
// Tool selection for current step
|
|
1336
|
+
panelTools.querySelectorAll('.tool-btn').forEach(btn => {
|
|
1337
|
+
btn.addEventListener('click', () => {
|
|
1338
|
+
if (!currentStepId) {
|
|
1339
|
+
const steps = window.xyzQueue || [];
|
|
1340
|
+
if (steps.length > 0) {
|
|
1341
|
+
currentStepId = steps[steps.length - 1].id;
|
|
1342
|
+
} else {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const toolType = btn.dataset.tool;
|
|
1347
|
+
addToolAnnotation(currentStepId, toolType);
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// Panel drag functionality
|
|
1352
|
+
let isDragging = false;
|
|
1353
|
+
let dragStartX = 0, dragStartY = 0;
|
|
1354
|
+
let panelStartX = 0, panelStartY = 0;
|
|
1355
|
+
|
|
1356
|
+
const header = panel.querySelector('.xyzPnl-hdr');
|
|
1357
|
+
header.addEventListener('mousedown', (e) => {
|
|
1358
|
+
if (e.target.tagName === 'BUTTON') return;
|
|
1359
|
+
isDragging = true;
|
|
1360
|
+
dragStartX = e.clientX;
|
|
1361
|
+
dragStartY = e.clientY;
|
|
1362
|
+
const rect = panel.getBoundingClientRect();
|
|
1363
|
+
panelStartX = rect.left;
|
|
1364
|
+
panelStartY = rect.top;
|
|
1365
|
+
e.preventDefault();
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
document.addEventListener('mousemove', (e) => {
|
|
1369
|
+
if (!isDragging) return;
|
|
1370
|
+
const dx = e.clientX - dragStartX;
|
|
1371
|
+
const dy = e.clientY - dragStartY;
|
|
1372
|
+
const newLeft = Math.max(0, Math.min(window.innerWidth - 320, panelStartX + dx));
|
|
1373
|
+
const newTop = Math.max(0, Math.min(window.innerHeight - 100, panelStartY + dy));
|
|
1374
|
+
panel.style.left = newLeft + 'px';
|
|
1375
|
+
panel.style.top = newTop + 'px';
|
|
1376
|
+
panel.style.right = 'auto';
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
document.addEventListener('mouseup', () => {
|
|
1380
|
+
if (isDragging) {
|
|
1381
|
+
isDragging = false;
|
|
1382
|
+
try {
|
|
1383
|
+
localStorage.setItem('xyzPnl-pos', JSON.stringify({
|
|
1384
|
+
left: panel.style.left,
|
|
1385
|
+
top: panel.style.top
|
|
1386
|
+
}));
|
|
1387
|
+
} catch(e) {}
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// Restore saved position
|
|
1392
|
+
try {
|
|
1393
|
+
const savedPos = localStorage.getItem('xyzPnl-pos');
|
|
1394
|
+
if (savedPos) {
|
|
1395
|
+
const pos = JSON.parse(savedPos);
|
|
1396
|
+
if (pos.left && pos.top) {
|
|
1397
|
+
panel.style.left = pos.left;
|
|
1398
|
+
panel.style.top = pos.top;
|
|
1399
|
+
panel.style.right = 'auto';
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
} catch(e) {}
|
|
1403
|
+
|
|
1404
|
+
const markersContainer = document.createElement('div');
|
|
1405
|
+
markersContainer.className = 'xyzMk';
|
|
1406
|
+
markersContainer.id = 'xyzMk';
|
|
1407
|
+
markersContainer.setAttribute('aria-hidden', 'true');
|
|
1408
|
+
document.body.appendChild(markersContainer);
|
|
1409
|
+
|
|
1410
|
+
const canvas = document.createElement('canvas');
|
|
1411
|
+
canvas.id = 'xyzCv';
|
|
1412
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
1413
|
+
document.body.appendChild(canvas);
|
|
1414
|
+
|
|
1415
|
+
const shadowBox = document.createElement('div');
|
|
1416
|
+
shadowBox.className = 'xyzSh';
|
|
1417
|
+
shadowBox.id = 'xyzSh';
|
|
1418
|
+
shadowBox.style.display = 'none';
|
|
1419
|
+
shadowBox.setAttribute('aria-hidden', 'true');
|
|
1420
|
+
document.body.appendChild(shadowBox);
|
|
1421
|
+
|
|
1422
|
+
const toolbar = document.createElement('div');
|
|
1423
|
+
toolbar.className = 'xyzTb';
|
|
1424
|
+
toolbar.id = 'xyzTb';
|
|
1425
|
+
toolbar.setAttribute('aria-hidden', 'true');
|
|
1426
|
+
toolbar.innerHTML = `
|
|
1427
|
+
<button class="btn-wait" data-type="wait_element">⏳ Wait</button>
|
|
1428
|
+
<button class="btn-container" data-type="data_container">📦 Container</button>
|
|
1429
|
+
<button class="btn-item" data-type="data_item">📊 Item</button>
|
|
1430
|
+
<button class="btn-page" data-type="pagination">📄 Page</button>
|
|
1431
|
+
<button class="btn-login" data-type="login_check">🔐 Login</button>
|
|
1432
|
+
<button class="btn-check" data-type="checkpoint">✅ Check</button>
|
|
1433
|
+
<button class="btn-note" data-type="custom">📝 Note</button>
|
|
1434
|
+
`;
|
|
1435
|
+
toolbar.style.display = 'none';
|
|
1436
|
+
document.body.appendChild(toolbar);
|
|
1437
|
+
|
|
1438
|
+
uiElements = { panel, markersContainer, canvas, shadowBox, toolbar, style };
|
|
1439
|
+
|
|
1440
|
+
function updateUI() {
|
|
1441
|
+
const container = document.getElementById('xyzSteps');
|
|
1442
|
+
const status = document.getElementById('xyzStatus');
|
|
1443
|
+
if (!container || !status) return;
|
|
1444
|
+
|
|
1445
|
+
const steps = window.xyzQueue || [];
|
|
1446
|
+
status.textContent = 'Steps: ' + steps.length;
|
|
1447
|
+
|
|
1448
|
+
if (steps.length === 0) {
|
|
1449
|
+
container.innerHTML = '<div class="xyzEmpty">No steps recorded yet</div>';
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const displaySteps = steps.slice(-20);
|
|
1454
|
+
|
|
1455
|
+
container.innerHTML = displaySteps.map((step) => {
|
|
1456
|
+
const action = step.action || 'unknown';
|
|
1457
|
+
const selector = step.selector || '';
|
|
1458
|
+
const value = step.value || '';
|
|
1459
|
+
const stepId = step.id || '';
|
|
1460
|
+
|
|
1461
|
+
let extra = '';
|
|
1462
|
+
if (action === 'trajectory' && step.points) {
|
|
1463
|
+
extra = '<div class="selector">🖱️ ' + step.points.length + ' points</div>';
|
|
1464
|
+
} else if (action === 'scroll') {
|
|
1465
|
+
extra = '<div class="selector">📜 (' + step.x + ', ' + step.y + ')</div>';
|
|
1466
|
+
} else if (action === 'resize') {
|
|
1467
|
+
extra = '<div class="selector">📐 ' + step.to.width + 'x' + step.to.height + '</div>';
|
|
1468
|
+
} else if (action === 'link_click') {
|
|
1469
|
+
extra = '<div class="selector">🔗 ' + (value || '') + '</div>';
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const hasAnnotation = step.annotation && step.annotation.label;
|
|
1473
|
+
const isSelected = stepId === currentStepId;
|
|
1474
|
+
|
|
1475
|
+
return '<div class="xyzStp ' + action + (isSelected ? ' selected' : '') + '" data-step-id="' + stepId + '">' +
|
|
1476
|
+
'<div class="action">' + action.toUpperCase() + '</div>' +
|
|
1477
|
+
(selector ? '<div class="selector">' + selector + '</div>' : '') +
|
|
1478
|
+
(value && !['trajectory', 'scroll', 'resize', 'link_click'].includes(action) ? '<div class="value">"' + value.slice(0, 30) + (value.length > 30 ? '...' : '') + '"</div>' : '') +
|
|
1479
|
+
extra +
|
|
1480
|
+
(hasAnnotation ? '<span class="annotation">🏷️ ' + step.annotation.label + '</span>' : '') +
|
|
1481
|
+
(isSelected ? '<button class="xyzDelBtn" data-step-id="' + stepId + '" title="Delete step">🗑️</button>' : '') +
|
|
1482
|
+
'</div>';
|
|
1483
|
+
}).join('');
|
|
1484
|
+
|
|
1485
|
+
// Click to select step
|
|
1486
|
+
container.querySelectorAll('.xyzStp').forEach(stepEl => {
|
|
1487
|
+
stepEl.addEventListener('click', (e) => {
|
|
1488
|
+
if (e.target.classList.contains('xyzDelBtn')) return;
|
|
1489
|
+
|
|
1490
|
+
const stepId = stepEl.dataset.stepId;
|
|
1491
|
+
currentStepId = stepId;
|
|
1492
|
+
updateUI();
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// Delete button click
|
|
1497
|
+
container.querySelectorAll('.xyzDelBtn').forEach(btn => {
|
|
1498
|
+
btn.addEventListener('click', (e) => {
|
|
1499
|
+
e.stopPropagation();
|
|
1500
|
+
const stepId = btn.dataset.stepId;
|
|
1501
|
+
deleteStep(stepId);
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
if (typeof autoScroll !== 'undefined' && autoScroll) {
|
|
1506
|
+
container.scrollTop = container.scrollHeight;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function addToolAnnotation(stepId, toolType) {
|
|
1511
|
+
if (!stepId) {
|
|
1512
|
+
stepId = currentStepId;
|
|
1513
|
+
if (!stepId) {
|
|
1514
|
+
const steps = window.xyzQueue || [];
|
|
1515
|
+
if (steps.length > 0) {
|
|
1516
|
+
stepId = steps[steps.length - 1].id;
|
|
1517
|
+
} else {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const steps = window.xyzQueue || [];
|
|
1524
|
+
const step = steps.find(s => s.id === stepId);
|
|
1525
|
+
if (!step) return;
|
|
1526
|
+
|
|
1527
|
+
const labels = {
|
|
1528
|
+
wait_element: 'Wait',
|
|
1529
|
+
data_container: 'Container',
|
|
1530
|
+
data_item: 'Item',
|
|
1531
|
+
pagination: 'Pagination',
|
|
1532
|
+
login_check: 'Login',
|
|
1533
|
+
checkpoint: 'Check',
|
|
1534
|
+
custom: 'Custom'
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
let annotation = null;
|
|
1538
|
+
|
|
1539
|
+
if (toolType === 'custom') {
|
|
1540
|
+
const note = prompt('Enter custom annotation:');
|
|
1541
|
+
if (note) {
|
|
1542
|
+
annotation = { type: 'custom', label: note };
|
|
1543
|
+
} else {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
} else if (toolType === 'wait_element') {
|
|
1547
|
+
const timeout = prompt('Enter wait timeout (ms, default 10000):', '10000');
|
|
1548
|
+
annotation = {
|
|
1549
|
+
type: toolType,
|
|
1550
|
+
label: labels[toolType],
|
|
1551
|
+
waitTimeout: parseInt(timeout) || 10000
|
|
1552
|
+
};
|
|
1553
|
+
} else if (toolType === 'data_container') {
|
|
1554
|
+
const itemSelector = prompt('Enter item selector (e.g., .product-item):');
|
|
1555
|
+
annotation = {
|
|
1556
|
+
type: toolType,
|
|
1557
|
+
label: labels[toolType],
|
|
1558
|
+
itemSelector: itemSelector || ''
|
|
1559
|
+
};
|
|
1560
|
+
} else {
|
|
1561
|
+
annotation = { type: toolType, label: labels[toolType] };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const result = pushEvent({ type: 'update', id: stepId, data: { annotation } });
|
|
1565
|
+
|
|
1566
|
+
if (result.success) {
|
|
1567
|
+
if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
|
|
1568
|
+
try {
|
|
1569
|
+
window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzUpdate', id: stepId, data: { annotation } }));
|
|
1570
|
+
} catch (e) {}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
updateUI();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function deleteStep(stepId) {
|
|
1578
|
+
if (!stepId) return;
|
|
1579
|
+
|
|
1580
|
+
const result = pushEvent({ type: 'delete', id: stepId });
|
|
1581
|
+
|
|
1582
|
+
if (result.success) {
|
|
1583
|
+
if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
|
|
1584
|
+
try {
|
|
1585
|
+
window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzDelete', id: stepId }));
|
|
1586
|
+
} catch (e) {}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
currentStepIndex = -1;
|
|
1590
|
+
currentStepId = null;
|
|
1591
|
+
|
|
1592
|
+
updateUI();
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
window.addEventListener('xyzEvt', function(e) {
|
|
1597
|
+
window.xyzQueue = e.detail;
|
|
1598
|
+
updateUI();
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
|
|
1602
|
+
window[window.xyzBindingName || 'xyzTrack']('');
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
document.getElementById('xyzClear').addEventListener('click', function() {
|
|
1606
|
+
window.xyzQueue = [];
|
|
1607
|
+
document.getElementById('xyzMk').innerHTML = '';
|
|
1608
|
+
markedElements.clear();
|
|
1609
|
+
annotations.clear();
|
|
1610
|
+
updateUI();
|
|
1611
|
+
if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
|
|
1612
|
+
try { window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzClear' })); } catch (e) {}
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
function updateMarkerPosition(element) {
|
|
1617
|
+
const data = markedElements.get(element);
|
|
1618
|
+
if (!data) return;
|
|
1619
|
+
const rect = element.getBoundingClientRect();
|
|
1620
|
+
data.marker.style.left = rect.left + 'px';
|
|
1621
|
+
data.marker.style.top = rect.top + 'px';
|
|
1622
|
+
data.marker.style.width = rect.width + 'px';
|
|
1623
|
+
data.marker.style.height = rect.height + 'px';
|
|
1624
|
+
data.marker.style.display = 'block';
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function updateAllMarkers() {
|
|
1628
|
+
markedElements.forEach((data, element) => updateMarkerPosition(element));
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
window.addEventListener('scroll', updateAllMarkers, true);
|
|
1632
|
+
window.addEventListener('resize', updateAllMarkers);
|
|
1633
|
+
|
|
1634
|
+
function updateShadowBox(element) {
|
|
1635
|
+
if (!element) {
|
|
1636
|
+
shadowBox.style.display = 'none';
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const rect = element.getBoundingClientRect();
|
|
1640
|
+
shadowBox.style.left = rect.left + window.scrollX + 'px';
|
|
1641
|
+
shadowBox.style.top = rect.top + window.scrollY + 'px';
|
|
1642
|
+
shadowBox.style.width = rect.width + 'px';
|
|
1643
|
+
shadowBox.style.height = rect.height + 'px';
|
|
1644
|
+
shadowBox.style.display = 'block';
|
|
1645
|
+
const annotation = annotations.get(element);
|
|
1646
|
+
shadowBox.className = 'xyzSh' + (annotation ? ' ' + annotation.type : '');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function calculateToolbarPosition(rect) {
|
|
1650
|
+
const GAP = 10;
|
|
1651
|
+
const TOOLBAR_W = 280;
|
|
1652
|
+
const TOOLBAR_H = 32;
|
|
1653
|
+
const scrollX = window.scrollX, scrollY = window.scrollY;
|
|
1654
|
+
|
|
1655
|
+
let left = mouseX + GAP;
|
|
1656
|
+
let top = mouseY + GAP;
|
|
1657
|
+
|
|
1658
|
+
if (left + TOOLBAR_W > window.innerWidth - 10) {
|
|
1659
|
+
left = mouseX - TOOLBAR_W - GAP;
|
|
1660
|
+
}
|
|
1661
|
+
if (top + TOOLBAR_H > window.innerHeight - 10) {
|
|
1662
|
+
top = mouseY - TOOLBAR_H - GAP;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
left = Math.max(10, Math.min(window.innerWidth - TOOLBAR_W - 10, left));
|
|
1666
|
+
top = Math.max(10, Math.min(window.innerHeight - TOOLBAR_H - 10, top));
|
|
1667
|
+
|
|
1668
|
+
return { left: left + scrollX, top: top + scrollY, orientation: 'horizontal' };
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function updateToolbar(element) {
|
|
1672
|
+
if (!element) {
|
|
1673
|
+
toolbar.style.display = 'none';
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const rect = element.getBoundingClientRect();
|
|
1677
|
+
const pos = calculateToolbarPosition(rect);
|
|
1678
|
+
toolbar.style.left = pos.left + 'px';
|
|
1679
|
+
toolbar.style.top = pos.top + 'px';
|
|
1680
|
+
toolbar.className = 'xyzTb ' + pos.orientation;
|
|
1681
|
+
toolbar.style.display = 'flex';
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function showToolbar() {
|
|
1685
|
+
clearTimeout(toolbarHideTimeout);
|
|
1686
|
+
toolbar.style.display = 'flex';
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function hideToolbarDelayed() {
|
|
1690
|
+
clearTimeout(toolbarHideTimeout);
|
|
1691
|
+
toolbarHideTimeout = setTimeout(() => {
|
|
1692
|
+
if (!isOverToolbar) {
|
|
1693
|
+
toolbar.style.display = 'none';
|
|
1694
|
+
}
|
|
1695
|
+
}, TOOLBAR_HIDE_DELAY);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function annotateElement(element, type) {
|
|
1699
|
+
if (!element) return;
|
|
1700
|
+
const selector = getSelector(element);
|
|
1701
|
+
const labels = {
|
|
1702
|
+
wait_element: 'Wait',
|
|
1703
|
+
data_container: 'Container',
|
|
1704
|
+
data_item: 'Item',
|
|
1705
|
+
pagination: 'Pagination',
|
|
1706
|
+
login_check: 'Login',
|
|
1707
|
+
checkpoint: 'Check',
|
|
1708
|
+
custom: 'Note'
|
|
1709
|
+
};
|
|
1710
|
+
let annotation = null;
|
|
1711
|
+
if (type === 'custom') {
|
|
1712
|
+
const note = prompt('Enter note:');
|
|
1713
|
+
if (note) annotation = { type: 'custom', label: note };
|
|
1714
|
+
else return;
|
|
1715
|
+
} else if (type === 'wait_element') {
|
|
1716
|
+
const timeout = prompt('Enter wait timeout (ms, default 10000):', '10000');
|
|
1717
|
+
annotation = {
|
|
1718
|
+
type,
|
|
1719
|
+
label: labels[type],
|
|
1720
|
+
selector: selector,
|
|
1721
|
+
waitTimeout: parseInt(timeout) || 10000
|
|
1722
|
+
};
|
|
1723
|
+
} else if (type === 'data_container') {
|
|
1724
|
+
const itemSelector = prompt('Enter item selector (e.g., .product-item):');
|
|
1725
|
+
annotation = {
|
|
1726
|
+
type,
|
|
1727
|
+
label: labels[type],
|
|
1728
|
+
selector: selector,
|
|
1729
|
+
itemSelector: itemSelector || ''
|
|
1730
|
+
};
|
|
1731
|
+
} else {
|
|
1732
|
+
annotation = { type, label: labels[type], selector: selector };
|
|
1733
|
+
}
|
|
1734
|
+
annotations.set(element, annotation);
|
|
1735
|
+
addMarker(element, type);
|
|
1736
|
+
|
|
1737
|
+
recordStep('annotate', {
|
|
1738
|
+
selector: selector,
|
|
1739
|
+
xpath: getXPath(element),
|
|
1740
|
+
annotation: annotation,
|
|
1741
|
+
elementInfo: getElementInfo(element)
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
shadowBox.style.transition = 'none';
|
|
1745
|
+
shadowBox.style.boxShadow = '0 0 20px 5px rgba(76, 175, 80, 0.8)';
|
|
1746
|
+
setTimeout(() => { shadowBox.style.transition = 'box-shadow 0.3s ease'; shadowBox.style.boxShadow = ''; }, 200);
|
|
1747
|
+
|
|
1748
|
+
updateShadowBox(element);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
toolbar.addEventListener('mouseenter', () => {
|
|
1752
|
+
isOverToolbar = true;
|
|
1753
|
+
showToolbar();
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
toolbar.addEventListener('mouseleave', () => {
|
|
1757
|
+
isOverToolbar = false;
|
|
1758
|
+
hideToolbarDelayed();
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
toolbar.querySelectorAll('button').forEach(btn => {
|
|
1762
|
+
btn.addEventListener('click', (e) => {
|
|
1763
|
+
e.stopPropagation();
|
|
1764
|
+
annotateElement(currentElement, btn.dataset.type);
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
let lastHighlightTime = 0;
|
|
1769
|
+
|
|
1770
|
+
function throttledHighlight(element) {
|
|
1771
|
+
const now = Date.now();
|
|
1772
|
+
pendingHighlightElement = element;
|
|
1773
|
+
|
|
1774
|
+
if (now - lastHighlightTime >= HIGHLIGHT_THROTTLE) {
|
|
1775
|
+
lastHighlightTime = now;
|
|
1776
|
+
if (highlightRafId === null) {
|
|
1777
|
+
highlightRafId = requestAnimationFrame(() => {
|
|
1778
|
+
highlightRafId = null;
|
|
1779
|
+
updateShadowBox(pendingHighlightElement);
|
|
1780
|
+
updateToolbar(pendingHighlightElement);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
document.addEventListener('mousemove', (e) => {
|
|
1787
|
+
if (isOverToolbar) return;
|
|
1788
|
+
|
|
1789
|
+
const element = e.composedPath()[0] || e.target;
|
|
1790
|
+
mouseX = e.clientX;
|
|
1791
|
+
mouseY = e.clientY;
|
|
1792
|
+
|
|
1793
|
+
if (element === shadowBox || element === toolbar || toolbar.contains(element)) return;
|
|
1794
|
+
if (isInPanel(element)) {
|
|
1795
|
+
throttledHighlight(null);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (element === document.body || element === document.documentElement) {
|
|
1799
|
+
throttledHighlight(null);
|
|
1800
|
+
currentEdge = null;
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (!shouldHighlightElement(element)) {
|
|
1805
|
+
throttledHighlight(null);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
currentElement = element;
|
|
1810
|
+
throttledHighlight(element);
|
|
1811
|
+
}, true);
|
|
1812
|
+
|
|
1813
|
+
const ctx = canvas.getContext('2d');
|
|
1814
|
+
function resizeCanvas() {
|
|
1815
|
+
canvas.width = window.innerWidth;
|
|
1816
|
+
canvas.height = window.innerHeight;
|
|
1817
|
+
}
|
|
1818
|
+
resizeCanvas();
|
|
1819
|
+
window.addEventListener('resize', resizeCanvas);
|
|
1820
|
+
|
|
1821
|
+
function drawCanvas() {
|
|
1822
|
+
const points = mousePath;
|
|
1823
|
+
const currentLength = points ? points.length : 0;
|
|
1824
|
+
|
|
1825
|
+
if (currentLength < 2) {
|
|
1826
|
+
if (lastTrajectoryLength > 0) {
|
|
1827
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1828
|
+
lastTrajectoryLength = 0;
|
|
1829
|
+
}
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
lastTrajectoryLength = currentLength;
|
|
1834
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1835
|
+
ctx.beginPath();
|
|
1836
|
+
ctx.strokeStyle = 'rgba(76, 175, 80, 0.5)';
|
|
1837
|
+
ctx.lineWidth = 2;
|
|
1838
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
1839
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
1840
|
+
ctx.stroke();
|
|
1841
|
+
points.forEach((p, i) => {
|
|
1842
|
+
ctx.beginPath();
|
|
1843
|
+
ctx.fillStyle = 'rgba(76, 175, 80, ' + (0.3 + (i / points.length) * 0.7) + ')';
|
|
1844
|
+
ctx.arc(p.x, p.y, 3 + (i / points.length) * 3, 0, Math.PI * 2);
|
|
1845
|
+
ctx.fill();
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function animateTrajectory() {
|
|
1850
|
+
drawCanvas();
|
|
1851
|
+
animationFrameId = requestAnimationFrame(animateTrajectory);
|
|
1852
|
+
}
|
|
1853
|
+
animateTrajectory();
|
|
1854
|
+
|
|
1855
|
+
let pollInterval = null;
|
|
1856
|
+
|
|
1857
|
+
function startPolling() {
|
|
1858
|
+
if (pollInterval) return;
|
|
1859
|
+
|
|
1860
|
+
pollInterval = setInterval(() => {
|
|
1861
|
+
if (typeof window[window.xyzBindingName || 'xyzTrack'] === 'function') {
|
|
1862
|
+
window[window.xyzBindingName || 'xyzTrack'](JSON.stringify({ action: 'xyzPoll' }));
|
|
1863
|
+
}
|
|
1864
|
+
}, 500);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
startPolling();
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// 延迟创建面板
|
|
1871
|
+
setTimeout(() => {
|
|
1872
|
+
if (window.xyzStopped) {
|
|
1873
|
+
console.log('[Panel] Session was stopped during init, skipping panel creation');
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (!window.xyzActive) {
|
|
1877
|
+
console.log('[Panel] Session not active during init, skipping panel creation');
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
createRecorderOverlay();
|
|
1881
|
+
}, 0);
|
|
1882
|
+
|
|
1883
|
+
// 监听页面变化
|
|
1884
|
+
let lastUrl = window.location.href;
|
|
1885
|
+
_checkPanelInterval = setInterval(() => {
|
|
1886
|
+
if (window.xyzStopped) {
|
|
1887
|
+
console.log('[Panel] Session was stopped, removing panel');
|
|
1888
|
+
if (typeof window.xyzClose === 'function') {
|
|
1889
|
+
window.xyzClose();
|
|
1890
|
+
}
|
|
1891
|
+
clearInterval(_checkPanelInterval);
|
|
1892
|
+
_checkPanelInterval = null;
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
if (!window.xyzActive) {
|
|
1897
|
+
clearInterval(_checkPanelInterval);
|
|
1898
|
+
_checkPanelInterval = null;
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (window.location.href !== lastUrl) {
|
|
1903
|
+
lastUrl = window.location.href;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const panel = document.getElementById('xyzPnl');
|
|
1907
|
+
const style = document.getElementById('xyzSt');
|
|
1908
|
+
if (document.body && (!panel || !style)) {
|
|
1909
|
+
createRecorderOverlay();
|
|
1910
|
+
}
|
|
1911
|
+
}, 1000);
|
|
1912
|
+
}
|
|
1913
|
+
})();
|