figranium 0.9.2 → 0.10.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.
- package/LICENSE +674 -674
- package/README.md +336 -318
- package/agent.js +1 -1
- package/common-utils.js +211 -166
- package/dist/assets/index--OZi5-p_.css +1 -0
- package/dist/assets/index-BwaOqbmy.js +15 -0
- package/dist/index.html +26 -26
- package/dist/novnc.html +108 -108
- package/extraction-worker.js +204 -197
- package/headful.js +522 -219
- package/html-utils.js +24 -24
- package/package.json +81 -78
- package/proxy-rotation.js +261 -261
- package/proxy-utils.js +84 -84
- package/public/novnc.html +108 -108
- package/scrape.js +371 -374
- package/server.js +517 -404
- package/src/server/cron-parser.js +316 -0
- package/src/server/routes/schedules.js +171 -0
- package/src/server/scheduler.js +381 -0
- package/url-utils.js +137 -116
- package/user-agent-settings.js +76 -76
- package/dist/assets/index-BmL_TAii.css +0 -1
- package/dist/assets/index-Cr7IFGdr.js +0 -15
package/headful.js
CHANGED
|
@@ -1,219 +1,522 @@
|
|
|
1
|
-
const { chromium } = require('
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { getProxySelection } = require('./proxy-rotation');
|
|
5
|
-
const { selectUserAgent } = require('./user-agent-settings');
|
|
6
|
-
const { validateUrl } = require('./url-utils');
|
|
7
|
-
const { parseBooleanFlag } = require('./common-utils');
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
1
|
+
const { chromium } = require('./stealth-chromium');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getProxySelection } = require('./proxy-rotation');
|
|
5
|
+
const { selectUserAgent } = require('./user-agent-settings');
|
|
6
|
+
const { validateUrl } = require('./url-utils');
|
|
7
|
+
const { parseBooleanFlag } = require('./common-utils');
|
|
8
|
+
const { Mutex } = require('./src/server/utils');
|
|
9
|
+
|
|
10
|
+
const HEADFUL_PROFILE_DIR = path.join(__dirname, 'data', 'browser-profile-headful');
|
|
11
|
+
|
|
12
|
+
const headfulMutex = new Mutex();
|
|
13
|
+
|
|
14
|
+
const EventEmitter = require('events');
|
|
15
|
+
const headfulEventEmitter = new EventEmitter();
|
|
16
|
+
|
|
17
|
+
let activeSession = null;
|
|
18
|
+
|
|
19
|
+
const teardownActiveSession = async () => {
|
|
20
|
+
if (!activeSession) return;
|
|
21
|
+
try {
|
|
22
|
+
if (activeSession.interval) clearInterval(activeSession.interval);
|
|
23
|
+
} catch { }
|
|
24
|
+
try {
|
|
25
|
+
if (activeSession.browser) {
|
|
26
|
+
await activeSession.browser.close();
|
|
27
|
+
}
|
|
28
|
+
} catch { }
|
|
29
|
+
activeSession = null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function runHeadful(data, options = {}) {
|
|
33
|
+
const { res } = options;
|
|
34
|
+
if (activeSession) {
|
|
35
|
+
await teardownActiveSession();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const url = data.url || 'https://www.google.com';
|
|
39
|
+
|
|
40
|
+
await validateUrl(url);
|
|
41
|
+
|
|
42
|
+
const rotateProxiesRaw = data.rotateProxies;
|
|
43
|
+
const rotateProxies = String(rotateProxiesRaw).toLowerCase() === 'true' || rotateProxiesRaw === true;
|
|
44
|
+
const statelessExecutionRaw = data.statelessExecution;
|
|
45
|
+
const statelessExecution = parseBooleanFlag(statelessExecutionRaw);
|
|
46
|
+
|
|
47
|
+
const inspectModeEnabled = true;
|
|
48
|
+
|
|
49
|
+
activeSession = { status: 'starting', startedAt: Date.now(), inspectModeEnabled };
|
|
50
|
+
|
|
51
|
+
const selectedUA = await selectUserAgent(false);
|
|
52
|
+
|
|
53
|
+
let browser;
|
|
54
|
+
let context;
|
|
55
|
+
let page;
|
|
56
|
+
let navigated = false;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (data.targetActionId && data.taskSnapshot) {
|
|
60
|
+
const { runAgent } = require('./src/agent');
|
|
61
|
+
try {
|
|
62
|
+
const reqScope = { ...data.taskSnapshot, variables: data.variables || data.taskVariables || {} };
|
|
63
|
+
if (data.url) reqScope.url = data.url;
|
|
64
|
+
|
|
65
|
+
const result = await runAgent(reqScope, {
|
|
66
|
+
headless: false,
|
|
67
|
+
handoffContext: true,
|
|
68
|
+
stopAtActionId: data.targetActionId
|
|
69
|
+
});
|
|
70
|
+
if (result && result._handoff) {
|
|
71
|
+
browser = result._handoff.browser;
|
|
72
|
+
context = result._handoff.context;
|
|
73
|
+
page = result._handoff.page;
|
|
74
|
+
navigated = true;
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error("Agent handoff failed:", e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!browser) {
|
|
82
|
+
const selection = getProxySelection(rotateProxies);
|
|
83
|
+
const hasProxy = !!selection.proxy;
|
|
84
|
+
|
|
85
|
+
const args = [
|
|
86
|
+
'--no-sandbox',
|
|
87
|
+
'--disable-setuid-sandbox',
|
|
88
|
+
'--disable-dev-shm-usage',
|
|
89
|
+
'--disable-gpu',
|
|
90
|
+
'--window-size=1920,1080',
|
|
91
|
+
'--window-position=0,0',
|
|
92
|
+
'--start-maximized',
|
|
93
|
+
'--dns-prefetch-disable',
|
|
94
|
+
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
|
|
95
|
+
];
|
|
96
|
+
if (!hasProxy) {
|
|
97
|
+
args.push(
|
|
98
|
+
'--enable-features=DnsOverHttps',
|
|
99
|
+
'--dns-over-https-mode=secure',
|
|
100
|
+
'--dns-over-https-templates=https://cloudflare-dns.com/dns-query'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contextOptions = {
|
|
105
|
+
viewport: null,
|
|
106
|
+
userAgent: selectedUA,
|
|
107
|
+
locale: 'en-US',
|
|
108
|
+
timezoneId: 'America/New_York',
|
|
109
|
+
permissions: ['clipboard-read', 'clipboard-write'],
|
|
110
|
+
...(selection.proxy ? { proxy: selection.proxy } : {})
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (statelessExecution) {
|
|
114
|
+
browser = await chromium.launch({ headless: false, args, ...(selection.proxy ? { proxy: selection.proxy } : {}) });
|
|
115
|
+
context = await browser.newContext(contextOptions);
|
|
116
|
+
} else {
|
|
117
|
+
await fs.promises.mkdir(HEADFUL_PROFILE_DIR, { recursive: true });
|
|
118
|
+
context = await chromium.launchPersistentContext(HEADFUL_PROFILE_DIR, { headless: false, args, ...contextOptions });
|
|
119
|
+
browser = context.browser();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const inspectInitFn = () => {
|
|
124
|
+
Object.defineProperty(window, 'open', { writable: true, configurable: true, value: () => null });
|
|
125
|
+
const handleLinkClick = (event) => {
|
|
126
|
+
const path = event.composedPath ? event.composedPath() : [];
|
|
127
|
+
const anchor = path.find(el => el.tagName === 'A');
|
|
128
|
+
if (anchor && anchor.target === '_blank') {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (event.type === 'auxclick' && event.button === 1 && anchor) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
document.addEventListener('click', handleLinkClick, true);
|
|
137
|
+
document.addEventListener('auxclick', handleLinkClick, true);
|
|
138
|
+
|
|
139
|
+
window.__figraniumInspectInit = () => {
|
|
140
|
+
if (window._figraniumInspectHandler) return;
|
|
141
|
+
|
|
142
|
+
const overlay = document.createElement('div');
|
|
143
|
+
overlay.id = 'figranium-inspect-overlay';
|
|
144
|
+
overlay.style.position = 'fixed';
|
|
145
|
+
overlay.style.pointerEvents = 'none';
|
|
146
|
+
overlay.style.zIndex = '2147483646';
|
|
147
|
+
overlay.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
|
148
|
+
overlay.style.border = '1px solid rgb(96, 165, 250)';
|
|
149
|
+
overlay.style.boxSizing = 'border-box';
|
|
150
|
+
overlay.style.transition = 'all 0.1s ease';
|
|
151
|
+
overlay.style.display = 'none';
|
|
152
|
+
document.body.appendChild(overlay);
|
|
153
|
+
|
|
154
|
+
const tooltip = document.createElement('div');
|
|
155
|
+
tooltip.id = 'figranium-inspect-tooltip';
|
|
156
|
+
tooltip.style.position = 'fixed';
|
|
157
|
+
tooltip.style.pointerEvents = 'none';
|
|
158
|
+
tooltip.style.zIndex = '2147483647';
|
|
159
|
+
tooltip.style.backgroundColor = '#1e293b';
|
|
160
|
+
tooltip.style.color = '#f8fafc';
|
|
161
|
+
tooltip.style.padding = '4px 8px';
|
|
162
|
+
tooltip.style.borderRadius = '4px';
|
|
163
|
+
tooltip.style.fontSize = '12px';
|
|
164
|
+
tooltip.style.fontFamily = 'monospace';
|
|
165
|
+
tooltip.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
|
|
166
|
+
tooltip.style.display = 'none';
|
|
167
|
+
tooltip.style.whiteSpace = 'nowrap';
|
|
168
|
+
tooltip.style.lineHeight = '1.4';
|
|
169
|
+
document.body.appendChild(tooltip);
|
|
170
|
+
|
|
171
|
+
window._figraniumGetSelectors = (el) => {
|
|
172
|
+
const isRandomId = (id) => {
|
|
173
|
+
if (!id) return true;
|
|
174
|
+
// Long numbers, UUIDs, explicit long strings
|
|
175
|
+
if (/\d{4,}/.test(id) || /^[0-9a-f]{8}-/i.test(id) || id.length > 30 || /[0-9]{3,}/.test(id)) return true;
|
|
176
|
+
// Google-style obfuscated classes (e.g. gLFyf, APjFqb)
|
|
177
|
+
if (/^[a-zA-Z]{4,8}$/.test(id) && /[A-Z]/.test(id) && /[a-z]/.test(id)) return true;
|
|
178
|
+
// Styled-components or CSS modules with hashes like css-1n7jcv, style_module__1xyz
|
|
179
|
+
if (/^css-[a-zA-Z0-9]+/.test(id) || /^sc-[a-zA-Z0-9]+/.test(id) || /_[a-zA-Z0-9]{5,}$/.test(id) || /-[a-zA-Z0-9]{5,}$/.test(id)) return true;
|
|
180
|
+
// Tailwind arbitrary values or very complex utility classes
|
|
181
|
+
if (id.includes('[') || id.includes(']')) return true;
|
|
182
|
+
return false;
|
|
183
|
+
};
|
|
184
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
185
|
+
if (!tag || tag === 'html' || tag === 'body') return [tag];
|
|
186
|
+
|
|
187
|
+
const selectors = new Set();
|
|
188
|
+
|
|
189
|
+
const isUnique = (sel) => {
|
|
190
|
+
try {
|
|
191
|
+
const nodes = document.querySelectorAll(sel);
|
|
192
|
+
return nodes.length === 1 && nodes[0] === el;
|
|
193
|
+
} catch (e) { return false; }
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const addIfUnique = (sel) => {
|
|
197
|
+
if (isUnique(sel)) selectors.add(sel);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 1. Name & placeholder (highest priority — most human-readable)
|
|
201
|
+
const topAttrs = ['name', 'placeholder'];
|
|
202
|
+
for (const attr of topAttrs) {
|
|
203
|
+
const val = el.getAttribute(attr);
|
|
204
|
+
if (val && val.length < 50 && !val.includes('"') && !val.includes('\n')) {
|
|
205
|
+
addIfUnique(`[${attr}="${val}"]`);
|
|
206
|
+
addIfUnique(`${tag}[${attr}="${val}"]`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Text content (:has-text — very readable)
|
|
211
|
+
if ((tag === 'button' || tag === 'a' || tag === 'span' || tag === 'div' || tag === 'label' || tag === 'li' || tag === 'p' || tag === 'h1' || tag === 'h2' || tag === 'h3') && el.textContent) {
|
|
212
|
+
const text = el.textContent.trim().substring(0, 40);
|
|
213
|
+
if (text && !text.includes('\n') && !text.includes('"') && text.length > 1) {
|
|
214
|
+
const allTags = Array.from(document.querySelectorAll(tag));
|
|
215
|
+
const matches = allTags.filter(t => t.textContent.trim() === text);
|
|
216
|
+
if (matches.length === 1 && matches[0] === el) {
|
|
217
|
+
selectors.add(`${tag}:has-text("${text}")`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3. Other semantic attributes
|
|
223
|
+
const semanticAttrs = ['aria-label', 'title', 'alt'];
|
|
224
|
+
for (const attr of semanticAttrs) {
|
|
225
|
+
const val = el.getAttribute(attr);
|
|
226
|
+
if (val && val.length < 50 && !val.includes('"') && !val.includes('\n')) {
|
|
227
|
+
addIfUnique(`[${attr}="${val}"]`);
|
|
228
|
+
addIfUnique(`${tag}[${attr}="${val}"]`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 4. Data attributes
|
|
233
|
+
const dataAttrs = ['data-testid', 'data-test-id', 'data-qa', 'data-cy'];
|
|
234
|
+
for (const attr of dataAttrs) {
|
|
235
|
+
const val = el.getAttribute(attr);
|
|
236
|
+
if (val) {
|
|
237
|
+
addIfUnique(`[${attr}="${val}"]`);
|
|
238
|
+
addIfUnique(`${tag}[${attr}="${val}"]`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 5. IDs
|
|
243
|
+
const id = el.id;
|
|
244
|
+
if (id && !isRandomId(id)) {
|
|
245
|
+
addIfUnique(`#${id}`);
|
|
246
|
+
addIfUnique(`${tag}#${id}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 6. Other basic attributes
|
|
250
|
+
const otherAttrs = ['type', 'value', 'href', 'src'];
|
|
251
|
+
for (const attr of otherAttrs) {
|
|
252
|
+
const val = el.getAttribute(attr);
|
|
253
|
+
if (val && val.length < 50 && !val.includes('"') && !val.includes('\n') && !val.startsWith('data:')) {
|
|
254
|
+
addIfUnique(`${tag}[${attr}="${val}"]`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 7. Classes
|
|
259
|
+
const classes = el.className && typeof el.className === 'string' ?
|
|
260
|
+
el.className.trim().split(/\s+/).filter(c => c && !isRandomId(c)) : [];
|
|
261
|
+
const classStr = classes.length > 0 ? '.' + classes.join('.') : '';
|
|
262
|
+
|
|
263
|
+
if (classStr) {
|
|
264
|
+
addIfUnique(`${tag}${classStr}`);
|
|
265
|
+
if (classes.length === 1) addIfUnique(`${classStr}`);
|
|
266
|
+
if (classes.length > 1) {
|
|
267
|
+
for (let c of classes) addIfUnique(`${tag}.${c}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
addIfUnique(tag);
|
|
272
|
+
|
|
273
|
+
// 7. Structural
|
|
274
|
+
if (el.parentElement) {
|
|
275
|
+
const siblings = Array.from(el.parentElement.children).filter(c => c.tagName === el.tagName);
|
|
276
|
+
const index = siblings.indexOf(el) + 1;
|
|
277
|
+
addIfUnique(`${tag}:nth-of-type(${index})`);
|
|
278
|
+
if (classStr) addIfUnique(`${tag}${classStr}:nth-of-type(${index})`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 8. Combinations with parents (Basic Path generation fallback)
|
|
282
|
+
if (selectors.size < 3) {
|
|
283
|
+
let path = '';
|
|
284
|
+
let current = el;
|
|
285
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
286
|
+
let step = current.tagName.toLowerCase();
|
|
287
|
+
|
|
288
|
+
// add id if good
|
|
289
|
+
if (current.id && !isRandomId(current.id)) {
|
|
290
|
+
step += `#${current.id}`;
|
|
291
|
+
} else {
|
|
292
|
+
// Add nth-of-type if no id and has siblings of same tag
|
|
293
|
+
if (current.parentElement) {
|
|
294
|
+
const sibs = Array.from(current.parentElement.children).filter(c => c.tagName === current.tagName);
|
|
295
|
+
if (sibs.length > 1) step += `:nth-of-type(${sibs.indexOf(current) + 1})`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
path = path ? `${step} > ${path}` : step;
|
|
300
|
+
if (isUnique(path)) {
|
|
301
|
+
selectors.add(path);
|
|
302
|
+
break; // Stop as soon as we found a unique path
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Try ID anchor
|
|
306
|
+
if (current.id && !isRandomId(current.id) && isUnique(`#${current.id}`)) {
|
|
307
|
+
break; // We anchored on a unique ID
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
current = current.parentElement;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return Array.from(selectors).slice(0, 5);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
window._figraniumInspectHandler = (e) => {
|
|
318
|
+
const element = e.composedPath ? e.composedPath()[0] : e.target;
|
|
319
|
+
if (!element || element === document || element === document.body) {
|
|
320
|
+
overlay.style.display = 'none';
|
|
321
|
+
tooltip.style.display = 'none';
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const rect = element.getBoundingClientRect();
|
|
326
|
+
overlay.style.display = 'block';
|
|
327
|
+
overlay.style.top = rect.top + 'px';
|
|
328
|
+
overlay.style.left = rect.left + 'px';
|
|
329
|
+
overlay.style.width = rect.width + 'px';
|
|
330
|
+
overlay.style.height = rect.height + 'px';
|
|
331
|
+
|
|
332
|
+
const selectors = window._figraniumGetSelectors(element);
|
|
333
|
+
tooltip.style.display = 'block';
|
|
334
|
+
tooltip.innerHTML = selectors.map((s, i) => i === 0 ? `<strong>${s}</strong>` : `<span style="opacity:0.7">${s}</span>`).join('<br/>');
|
|
335
|
+
|
|
336
|
+
let tipTop = e.clientY + 15;
|
|
337
|
+
let tipLeft = e.clientX + 15;
|
|
338
|
+
|
|
339
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
340
|
+
if (tipLeft + tooltipRect.width > window.innerWidth) {
|
|
341
|
+
tipLeft = e.clientX - tooltipRect.width - 15;
|
|
342
|
+
}
|
|
343
|
+
if (tipTop + tooltipRect.height > window.innerHeight) {
|
|
344
|
+
tipTop = e.clientY - tooltipRect.height - 15;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
tooltip.style.top = tipTop + 'px';
|
|
348
|
+
tooltip.style.left = tipLeft + 'px';
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
window._figraniumInspectClickHandler = async (e) => {
|
|
352
|
+
if (!window._figraniumInspectHandler) return;
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.stopPropagation();
|
|
355
|
+
e.stopImmediatePropagation();
|
|
356
|
+
|
|
357
|
+
const element = e.composedPath ? e.composedPath()[0] : e.target;
|
|
358
|
+
const selectors = window._figraniumGetSelectors(element);
|
|
359
|
+
const bestSelector = selectors[0] || '';
|
|
360
|
+
|
|
361
|
+
// Push to backend via Playwright binding
|
|
362
|
+
if (window.__figraniumOnElementSelected && selectors.length > 0) {
|
|
363
|
+
try {
|
|
364
|
+
await window.__figraniumOnElementSelected(JSON.stringify(selectors));
|
|
365
|
+
} catch (err) { }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
if (bestSelector && navigator.clipboard && navigator.clipboard.writeText) {
|
|
370
|
+
await navigator.clipboard.writeText(bestSelector);
|
|
371
|
+
}
|
|
372
|
+
} catch (err) { }
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
document.addEventListener('mousemove', window._figraniumInspectHandler, true);
|
|
376
|
+
document.addEventListener('click', window._figraniumInspectClickHandler, true);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
window.__figraniumInspectDestroy = () => {
|
|
380
|
+
const overlay = document.getElementById('figranium-inspect-overlay');
|
|
381
|
+
if (overlay) overlay.remove();
|
|
382
|
+
const tooltip = document.getElementById('figranium-inspect-tooltip');
|
|
383
|
+
if (tooltip) tooltip.remove();
|
|
384
|
+
if (window._figraniumInspectHandler) {
|
|
385
|
+
document.removeEventListener('mousemove', window._figraniumInspectHandler, true);
|
|
386
|
+
delete window._figraniumInspectHandler;
|
|
387
|
+
}
|
|
388
|
+
if (window._figraniumInspectClickHandler) {
|
|
389
|
+
document.removeEventListener('click', window._figraniumInspectClickHandler, true);
|
|
390
|
+
delete window._figraniumInspectClickHandler;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
395
|
+
if (window.__figraniumIsInspectEnabled) {
|
|
396
|
+
const enabled = await window.__figraniumIsInspectEnabled();
|
|
397
|
+
if (enabled) {
|
|
398
|
+
window.__figraniumInspectInit();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
await context.addInitScript(inspectInitFn);
|
|
405
|
+
|
|
406
|
+
await context.exposeBinding('__figraniumIsInspectEnabled', () => {
|
|
407
|
+
return activeSession ? !!activeSession.inspectModeEnabled : false;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await context.exposeBinding('__figraniumOnElementSelected', (source, selector) => {
|
|
411
|
+
headfulEventEmitter.emit('selectorSelected', selector);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!page) {
|
|
415
|
+
// Persistent context auto-creates a blank page; reuse it or open a new one
|
|
416
|
+
const existingPages = context.pages();
|
|
417
|
+
page = existingPages.length > 0 ? existingPages[0] : await context.newPage();
|
|
418
|
+
try {
|
|
419
|
+
const cdp = await context.newCDPSession(page);
|
|
420
|
+
const { windowId } = await cdp.send('Browser.getWindowForTarget');
|
|
421
|
+
await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'maximized' } });
|
|
422
|
+
} catch (e) { }
|
|
423
|
+
} else {
|
|
424
|
+
try { await page.evaluate(inspectInitFn); } catch (e) { }
|
|
425
|
+
try {
|
|
426
|
+
await page.evaluate(() => {
|
|
427
|
+
if (window.__figraniumInspectInit) window.__figraniumInspectInit();
|
|
428
|
+
});
|
|
429
|
+
} catch (e) { }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const closeIfExtra = async (extraPage) => {
|
|
433
|
+
if (!extraPage || extraPage === page) return;
|
|
434
|
+
try { await extraPage.close(); } catch { }
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
context.on('page', closeIfExtra);
|
|
438
|
+
page.on('popup', async (popup) => {
|
|
439
|
+
try { popup.close().catch(() => { }); } catch { }
|
|
440
|
+
await closeIfExtra(popup);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!navigated && url) {
|
|
444
|
+
await page.goto(url).catch(() => { });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
activeSession = { browser, context, page, status: 'running', startedAt: activeSession.startedAt, inspectModeEnabled: activeSession.inspectModeEnabled };
|
|
448
|
+
|
|
449
|
+
page.on('close', async () => { });
|
|
450
|
+
|
|
451
|
+
const responseData = {
|
|
452
|
+
message: 'Headful session started.',
|
|
453
|
+
userAgentUsed: selectedUA
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (res) {
|
|
457
|
+
res.json(responseData);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await new Promise((resolve) => browser.on('disconnected', resolve));
|
|
461
|
+
activeSession = null;
|
|
462
|
+
return responseData;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (browser) await browser.close();
|
|
465
|
+
activeSession = null;
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function handleHeadful(req, res) {
|
|
471
|
+
await headfulMutex.lock();
|
|
472
|
+
try {
|
|
473
|
+
const data = { ...req.body, ...req.query };
|
|
474
|
+
await runHeadful(data, { res });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const message = String(error && error.message ? error.message : error);
|
|
477
|
+
const displayUnavailable = /missing x server|\$display|platform failed to initialize/i.test(message);
|
|
478
|
+
if (!res.headersSent && displayUnavailable) {
|
|
479
|
+
return res.status(409).json({ error: 'HEADFUL_DISPLAY_UNAVAILABLE', details: message });
|
|
480
|
+
}
|
|
481
|
+
if (!res.headersSent) {
|
|
482
|
+
res.status(500).json({ error: 'Failed to start headful session', details: message });
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
headfulMutex.unlock();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function stopHeadful(req, res) {
|
|
490
|
+
if (!activeSession) {
|
|
491
|
+
return res.status(200).json({ message: 'No active headful session.' });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await teardownActiveSession();
|
|
495
|
+
if (res) res.json({ message: 'Headful session stopped.' });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function toggleInspectMode(req, res) {
|
|
499
|
+
if (!activeSession || !activeSession.context) {
|
|
500
|
+
return res.status(400).json({ error: 'No active headful session.' });
|
|
501
|
+
}
|
|
502
|
+
const enabled = req.body.enabled === true || req.body.enabled === 'true';
|
|
503
|
+
activeSession.inspectModeEnabled = enabled;
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const pages = activeSession.context.pages();
|
|
507
|
+
for (const page of pages) {
|
|
508
|
+
await page.evaluate((enabled) => {
|
|
509
|
+
if (enabled) {
|
|
510
|
+
if (window.__figraniumInspectInit) window.__figraniumInspectInit();
|
|
511
|
+
} else {
|
|
512
|
+
if (window.__figraniumInspectDestroy) window.__figraniumInspectDestroy();
|
|
513
|
+
}
|
|
514
|
+
}, enabled);
|
|
515
|
+
}
|
|
516
|
+
res.json({ message: `Inspect mode ${enabled ? 'enabled' : 'disabled'}` });
|
|
517
|
+
} catch (error) {
|
|
518
|
+
res.status(500).json({ error: 'Failed to toggle inspect mode', details: String(error) });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = { runHeadful, handleHeadful, stopHeadful, toggleInspectMode, headfulEventEmitter };
|