@yusufffararatt/dombridge-mcp 2.7.5
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/README.md +559 -0
- package/bin/cli.js +88 -0
- package/package.json +54 -0
- package/src/bridge/http-server.js +290 -0
- package/src/bridge/middleware.js +56 -0
- package/src/bridge/routes.js +1003 -0
- package/src/bridge-daemon.js +172 -0
- package/src/cli/auto-config.js +120 -0
- package/src/constants.js +13 -0
- package/src/index.js +279 -0
- package/src/mcp-bridge.js +136 -0
- package/src/metrics/error-codes.js +44 -0
- package/src/metrics/index.js +3 -0
- package/src/metrics/metrics-db.js +269 -0
- package/src/metrics/metrics-recorder.js +240 -0
- package/src/metrics/metrics-report.js +146 -0
- package/src/profiles/profile-db.js +159 -0
- package/src/profiles/profile-enricher.js +333 -0
- package/src/profiles/profile-manager.js +563 -0
- package/src/profiles/profile-repo.js +183 -0
- package/src/state/bridge-client.js +272 -0
- package/src/state/bridge-persistence.js +205 -0
- package/src/state/cache.js +38 -0
- package/src/state/extension-state.js +321 -0
- package/src/tools/action_tools.js +218 -0
- package/src/tools/analyze-page.js +247 -0
- package/src/tools/debug-mcp-state.js +172 -0
- package/src/tools/discover-apis.js +186 -0
- package/src/tools/execute-js.js +284 -0
- package/src/tools/export-session.js +171 -0
- package/src/tools/extract-data.js +395 -0
- package/src/tools/get-element.js +281 -0
- package/src/tools/get-network-trace.js +471 -0
- package/src/tools/index.js +110 -0
- package/src/tools/manage-site-profile.js +153 -0
- package/src/tools/paginate.js +444 -0
- package/src/tools/quick-scan.js +418 -0
- package/src/tools/screenshot_tools.js +117 -0
- package/src/utils/circuit-breaker.js +112 -0
- package/src/utils/extract-density.js +21 -0
- package/src/utils/logger.js +31 -0
- package/src/utils/paginate-detector.js +24 -0
- package/src/utils/rate-limiter.js +244 -0
- package/src/utils/run-script.js +37 -0
- package/src/utils/selector-validator.js +95 -0
- package/src/utils/state-validator.js +354 -0
- package/src/utils/tab-resolver.js +70 -0
- package/src/utils/workflow-helper.js +292 -0
- package/src/utils/workflow-state.js +177 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-compliant logger
|
|
3
|
+
*
|
|
4
|
+
* MCP stdio transport uses stdout exclusively for JSON-RPC messages.
|
|
5
|
+
* ALL diagnostic output MUST go to stderr — never stdout.
|
|
6
|
+
*
|
|
7
|
+
* Levels:
|
|
8
|
+
* info — startup, lifecycle events (always visible)
|
|
9
|
+
* warn — recoverable issues (always visible)
|
|
10
|
+
* error — failures (always visible)
|
|
11
|
+
* debug — verbose detail (only when MCP_DEBUG=1)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const isDebug = () => process.env.MCP_DEBUG === '1';
|
|
15
|
+
|
|
16
|
+
const PREFIX = '[MCP]';
|
|
17
|
+
|
|
18
|
+
function fmt(level, context, ...args) {
|
|
19
|
+
const tag = context ? `${PREFIX}[${context}]` : PREFIX;
|
|
20
|
+
process.stderr.write(`${tag} ${level} ${args.map(a =>
|
|
21
|
+
a instanceof Error ? a.stack || a.message :
|
|
22
|
+
typeof a === 'object' ? JSON.stringify(a) : String(a)
|
|
23
|
+
).join(' ')}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const logger = {
|
|
27
|
+
info: (context, ...args) => fmt('INFO ', context, ...args),
|
|
28
|
+
warn: (context, ...args) => fmt('WARN ', context, ...args),
|
|
29
|
+
error: (context, ...args) => fmt('ERROR', context, ...args),
|
|
30
|
+
debug: (context, ...args) => isDebug() && fmt('DEBUG', context, ...args),
|
|
31
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Strategy Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects the best pagination strategy for a page based on probed page state,
|
|
5
|
+
* next button presence, and infinite scroll detection.
|
|
6
|
+
*
|
|
7
|
+
* Used by tools/paginate.js when strategy='auto' to avoid defaulting to scroll
|
|
8
|
+
* when the page has explicit URL-based pagination metadata (Trendyol, etc.).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function detectPaginationStrategy({ pageState, nextButtonSelector, hasInfiniteScroll } = {}) {
|
|
12
|
+
if (nextButtonSelector) return 'button';
|
|
13
|
+
|
|
14
|
+
const totalPages = pageState?.widgetList?.totalPages
|
|
15
|
+
|| pageState?.__NEXT_DATA__?.props?.pageProps?.totalPages
|
|
16
|
+
|| pageState?.pagination?.totalPages
|
|
17
|
+
|| 0;
|
|
18
|
+
|
|
19
|
+
if (totalPages > 1) return 'url_increment';
|
|
20
|
+
|
|
21
|
+
if (hasInfiniteScroll) return 'scroll';
|
|
22
|
+
|
|
23
|
+
return 'auto';
|
|
24
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter & Retry Strategy
|
|
3
|
+
* Implements exponential backoff with jitter to prevent rapid sequential calls
|
|
4
|
+
*
|
|
5
|
+
* Best Practices:
|
|
6
|
+
* - Exponential backoff: 1s, 2s, 4s, 8s
|
|
7
|
+
* - Jitter: +/- 20% randomization
|
|
8
|
+
* - Per-tool rate limits
|
|
9
|
+
* - Retry-After header support
|
|
10
|
+
*
|
|
11
|
+
* References:
|
|
12
|
+
* - https://docs.anthropic.com/en/api/rate-limits
|
|
13
|
+
* - https://www.anthropic.com/engineering/code-execution-with-mcp
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rate limit configuration per tool
|
|
18
|
+
*/
|
|
19
|
+
const RATE_LIMITS = {
|
|
20
|
+
// Discovery tools
|
|
21
|
+
'get_selected_element': { minInterval: 500, maxRetries: 5 },
|
|
22
|
+
'get_network_trace': { minInterval: 1000, maxRetries: 3 },
|
|
23
|
+
'get_websocket_trace': { minInterval: 1000, maxRetries: 3 },
|
|
24
|
+
|
|
25
|
+
// Low-frequency tools (expensive operations)
|
|
26
|
+
'execute_js': { minInterval: 2000, maxRetries: 3 },
|
|
27
|
+
'debug_mcp_state': { minInterval: 100, maxRetries: 2 },
|
|
28
|
+
|
|
29
|
+
// Default fallback
|
|
30
|
+
'default': { minInterval: 1000, maxRetries: 3 }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Track last execution time per tool
|
|
35
|
+
*/
|
|
36
|
+
const lastExecutionTime = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Track retry counts per tool
|
|
40
|
+
*/
|
|
41
|
+
const retryCount = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rate Limiter Class
|
|
45
|
+
*/
|
|
46
|
+
export class RateLimiter {
|
|
47
|
+
/**
|
|
48
|
+
* Execute a function with rate limiting and retry logic
|
|
49
|
+
* @param {string} toolName - Name of the tool
|
|
50
|
+
* @param {Function} handler - Async function to execute
|
|
51
|
+
* @param {object} options - { maxRetries, minInterval, onRetry }
|
|
52
|
+
* @returns {Promise<any>} - Handler result
|
|
53
|
+
*/
|
|
54
|
+
static async executeWithRetry(toolName, handler, options = {}) {
|
|
55
|
+
const config = RATE_LIMITS[toolName] || RATE_LIMITS.default;
|
|
56
|
+
const maxRetries = options.maxRetries ?? config?.maxRetries ?? 3;
|
|
57
|
+
const minInterval = options.minInterval ?? config?.minInterval ?? 1000;
|
|
58
|
+
|
|
59
|
+
let attempt = 0;
|
|
60
|
+
let lastError = null;
|
|
61
|
+
|
|
62
|
+
while (attempt < maxRetries) {
|
|
63
|
+
try {
|
|
64
|
+
// Rate limiting: wait if called too soon
|
|
65
|
+
await this.enforceRateLimit(toolName, minInterval);
|
|
66
|
+
|
|
67
|
+
// Execute handler
|
|
68
|
+
const result = await handler();
|
|
69
|
+
|
|
70
|
+
// Success: reset retry count
|
|
71
|
+
retryCount.delete(toolName);
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error;
|
|
77
|
+
attempt++;
|
|
78
|
+
|
|
79
|
+
// Check if error is retryable
|
|
80
|
+
if (!this.isRetryableError(error) || attempt >= maxRetries) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Calculate backoff delay
|
|
85
|
+
const delay = this.calculateBackoff(attempt, minInterval);
|
|
86
|
+
|
|
87
|
+
// Call retry callback if provided
|
|
88
|
+
if (options.onRetry) {
|
|
89
|
+
options.onRetry(attempt, delay, error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Wait before retrying
|
|
93
|
+
await this.sleep(delay);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// All retries exhausted
|
|
98
|
+
throw new Error(`${toolName} failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Enforce rate limit for a tool
|
|
103
|
+
* @param {string} toolName - Tool name
|
|
104
|
+
* @param {number} minInterval - Minimum interval in ms
|
|
105
|
+
*/
|
|
106
|
+
static async enforceRateLimit(toolName, minInterval) {
|
|
107
|
+
const lastTime = lastExecutionTime.get(toolName);
|
|
108
|
+
|
|
109
|
+
if (lastTime) {
|
|
110
|
+
const elapsed = Date.now() - lastTime;
|
|
111
|
+
const remaining = minInterval - elapsed;
|
|
112
|
+
|
|
113
|
+
if (remaining > 0) {
|
|
114
|
+
await this.sleep(remaining);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Update last execution time
|
|
119
|
+
lastExecutionTime.set(toolName, Date.now());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculate exponential backoff with jitter
|
|
124
|
+
* @param {number} attempt - Retry attempt number (1-indexed)
|
|
125
|
+
* @param {number} baseDelay - Base delay in ms
|
|
126
|
+
* @returns {number} - Delay in ms
|
|
127
|
+
*/
|
|
128
|
+
static calculateBackoff(attempt, baseDelay = 1000) {
|
|
129
|
+
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
130
|
+
// attempt 1: 1s, attempt 2: 2s, attempt 3: 4s, attempt 4: 8s
|
|
131
|
+
const exponential = baseDelay * Math.pow(2, attempt - 1);
|
|
132
|
+
|
|
133
|
+
// Add jitter: +/- 20%
|
|
134
|
+
const jitter = exponential * 0.2 * (Math.random() * 2 - 1);
|
|
135
|
+
|
|
136
|
+
// Cap at 30 seconds
|
|
137
|
+
const delay = Math.min(exponential + jitter, 30000);
|
|
138
|
+
|
|
139
|
+
return Math.floor(delay);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if error is retryable
|
|
144
|
+
* @param {Error} error - Error object
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
static isRetryableError(error) {
|
|
148
|
+
const message = error?.message?.toLowerCase() || '';
|
|
149
|
+
|
|
150
|
+
// Retryable errors:
|
|
151
|
+
// - Timeout errors
|
|
152
|
+
// - Connection errors
|
|
153
|
+
// - Rate limit errors (429)
|
|
154
|
+
// - Server errors (5xx)
|
|
155
|
+
// - "Waiting for extension" errors
|
|
156
|
+
const retryablePatterns = [
|
|
157
|
+
'timeout',
|
|
158
|
+
'timed out',
|
|
159
|
+
'connection',
|
|
160
|
+
'network',
|
|
161
|
+
'rate limit',
|
|
162
|
+
'too many requests',
|
|
163
|
+
'server error',
|
|
164
|
+
'5xx',
|
|
165
|
+
'waiting for',
|
|
166
|
+
'no data',
|
|
167
|
+
'not ready'
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const isRetryable = retryablePatterns.some(pattern => message.includes(pattern));
|
|
171
|
+
|
|
172
|
+
// Non-retryable errors:
|
|
173
|
+
// - Validation errors
|
|
174
|
+
// - Missing prerequisites
|
|
175
|
+
// - Invalid arguments
|
|
176
|
+
const nonRetryablePatterns = [
|
|
177
|
+
'invalid',
|
|
178
|
+
'missing required',
|
|
179
|
+
'prerequisite',
|
|
180
|
+
'not allowed',
|
|
181
|
+
'forbidden'
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const isNonRetryable = nonRetryablePatterns.some(pattern => message.includes(pattern));
|
|
185
|
+
|
|
186
|
+
return isRetryable && !isNonRetryable;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Sleep utility
|
|
191
|
+
* @param {number} ms - Milliseconds to sleep
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
static sleep(ms) {
|
|
195
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get rate limit stats
|
|
200
|
+
* @returns {object} - Stats object
|
|
201
|
+
*/
|
|
202
|
+
static getStats() {
|
|
203
|
+
return {
|
|
204
|
+
lastExecutionTimes: Object.fromEntries(lastExecutionTime),
|
|
205
|
+
retryCounts: Object.fromEntries(retryCount)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reset rate limiter state
|
|
211
|
+
* @param {string|null} toolName - Tool name to reset, or null for all
|
|
212
|
+
*/
|
|
213
|
+
static reset(toolName = null) {
|
|
214
|
+
if (toolName) {
|
|
215
|
+
lastExecutionTime.delete(toolName);
|
|
216
|
+
retryCount.delete(toolName);
|
|
217
|
+
} else {
|
|
218
|
+
lastExecutionTime.clear();
|
|
219
|
+
retryCount.clear();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Simple helper: wait with validation
|
|
226
|
+
* @param {number} ms - Milliseconds
|
|
227
|
+
* @param {BridgeClient|object} bridgeClient - State source to check
|
|
228
|
+
* @param {Function} validation - Validation function that returns boolean
|
|
229
|
+
* @param {number} maxWait - Maximum wait time in ms
|
|
230
|
+
* @returns {Promise<boolean>} - True if validation passed, false if timeout
|
|
231
|
+
*/
|
|
232
|
+
export async function waitForCondition(ms, bridgeClient, validation, maxWait = 5000) {
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
|
|
235
|
+
while (Date.now() - startTime < maxWait) {
|
|
236
|
+
if (validation(bridgeClient)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await RateLimiter.sleep(ms);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runScript — execute_js pipeline shared helper
|
|
3
|
+
* Tüm tool'lar bu helper'ı kullanır: extract_data, check_site_changes, vb.
|
|
4
|
+
*
|
|
5
|
+
* Phase 2.4: Refactored from (code, extensionData, httpPort, ...) to (code, bridgeClient, ...).
|
|
6
|
+
* Now uses bridgeClient.queueRequest() and bridgeClient.waitForResult() instead of
|
|
7
|
+
* direct HTTP fetch and in-memory result polling.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export async function runScript(code, bridgeClient, timeoutMs = 12000, options = {}) {
|
|
11
|
+
const requestId = `script-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
12
|
+
|
|
13
|
+
await bridgeClient.queueRequest('execute-js', {
|
|
14
|
+
code,
|
|
15
|
+
timeout: timeoutMs,
|
|
16
|
+
id: requestId,
|
|
17
|
+
...(options.tabId !== undefined ? { tabId: options.tabId } : {})
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const resultItem = await bridgeClient.waitForResult('js-execution', requestId, timeoutMs + 3000);
|
|
21
|
+
|
|
22
|
+
if (!resultItem) {
|
|
23
|
+
throw new Error('Timeout');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// CSP auto-bypass wraps result in { _cspBypassed, result }. Unwrap it.
|
|
27
|
+
let rawResult = resultItem.result;
|
|
28
|
+
if (rawResult && rawResult._cspBypassed !== undefined && rawResult.result !== undefined) {
|
|
29
|
+
rawResult = rawResult.result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (rawResult && rawResult.error) {
|
|
33
|
+
throw new Error(rawResult.error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return rawResult;
|
|
37
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared selector validation script.
|
|
3
|
+
* Injected into page context to check element existence, visibility, and suggest alternatives.
|
|
4
|
+
* Used by execute_action (pre-action) and get_element (post-failure).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const VALIDATION_SCRIPT = (css) => `(function() {
|
|
8
|
+
var all = document.querySelectorAll(${JSON.stringify(css)});
|
|
9
|
+
var matchCount = all.length;
|
|
10
|
+
var el = all[0];
|
|
11
|
+
if (!el) {
|
|
12
|
+
var tagMatch = ${JSON.stringify(css)}.match(/^[a-zA-Z]*/);
|
|
13
|
+
var tag = (tagMatch && tagMatch[0]) ? tagMatch[0] : '*';
|
|
14
|
+
var STRUCTURAL_TAGS = ['html','head','body','script','style','meta','link','noscript','title','base','template'];
|
|
15
|
+
var similar = Array.from(document.querySelectorAll(tag))
|
|
16
|
+
.filter(function(e) { return STRUCTURAL_TAGS.indexOf(e.tagName.toLowerCase()) === -1; })
|
|
17
|
+
.slice(0, 3)
|
|
18
|
+
.map(function(e) {
|
|
19
|
+
return {
|
|
20
|
+
tag: e.tagName.toLowerCase(),
|
|
21
|
+
id: e.id || null,
|
|
22
|
+
cls: Array.from(e.classList).slice(0, 3).join('.'),
|
|
23
|
+
text: (e.textContent || '').trim().substring(0, 40)
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
return { found: false, matchCount: 0, alternatives: similar };
|
|
27
|
+
}
|
|
28
|
+
var rect = el.getBoundingClientRect();
|
|
29
|
+
var visible = rect.width > 0 && rect.height > 0 && el.offsetParent !== null;
|
|
30
|
+
var cx = rect.left + rect.width / 2;
|
|
31
|
+
var cy = rect.top + rect.height / 2;
|
|
32
|
+
var topEl = document.elementFromPoint(cx, cy);
|
|
33
|
+
var occluded = topEl !== null && topEl !== el && !el.contains(topEl);
|
|
34
|
+
return {
|
|
35
|
+
found: true,
|
|
36
|
+
matchCount: matchCount,
|
|
37
|
+
visible: visible,
|
|
38
|
+
occluded: occluded,
|
|
39
|
+
occluder: occluded ? (topEl.className || topEl.tagName.toLowerCase()) : null,
|
|
40
|
+
disabled: !!(el.disabled || el.getAttribute('aria-disabled') === 'true')
|
|
41
|
+
};
|
|
42
|
+
})()`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format alternatives list from VALIDATION_SCRIPT result.
|
|
46
|
+
* @param {Array} alternatives
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function formatAlternatives(alternatives) {
|
|
50
|
+
if (!alternatives || alternatives.length === 0) return '';
|
|
51
|
+
// Bug #2 fix: dedup by tag#id.class key to avoid duplicate similar-element lines
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const unique = alternatives.filter(a => {
|
|
54
|
+
const key = `${a.tag}${a.id ? '#' + a.id : ''}${a.cls ? '.' + a.cls : ''}`;
|
|
55
|
+
if (seen.has(key)) return false;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
return unique.map(a =>
|
|
60
|
+
` - \`${a.tag}${a.id ? '#' + a.id : ''}${a.cls ? '.' + a.cls : ''}\` — "${a.text}"`
|
|
61
|
+
).join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rank similar element candidates by semantic relevance.
|
|
66
|
+
* Penalizes hidden/cookie/OneTrust patterns and rewards search/semantic
|
|
67
|
+
* indicators (placeholder, aria-label, type=search, name=q, etc.).
|
|
68
|
+
*
|
|
69
|
+
* @param {Array} candidates - Array of { selector, text, attributes } objects.
|
|
70
|
+
* @param {string} originalSelector - The CSS selector that failed (context hint).
|
|
71
|
+
* @returns {Array} New array sorted by descending relevance score.
|
|
72
|
+
*/
|
|
73
|
+
export function rankSimilarElements(candidates, _originalSelector) {
|
|
74
|
+
const score = (c) => {
|
|
75
|
+
let s = 0;
|
|
76
|
+
const sel = c.selector || '';
|
|
77
|
+
const attrs = c.attributes || {};
|
|
78
|
+
|
|
79
|
+
// Penalize hidden/cookie/OT patterns
|
|
80
|
+
if (/type=["']?(hidden|checkbox)["']?/i.test(sel)) s -= 50;
|
|
81
|
+
if (/ot-group-id|onetrust|cookie|category-switch-handler/i.test(sel)) s -= 40;
|
|
82
|
+
if (/type=["']?submit["']?/i.test(sel)) s -= 30;
|
|
83
|
+
|
|
84
|
+
// Reward search/semantic
|
|
85
|
+
if (attrs.placeholder || /placeholder/i.test(sel)) s += 30;
|
|
86
|
+
if (attrs['aria-label'] || /aria-label/i.test(sel)) s += 20;
|
|
87
|
+
if (attrs.type === 'search' || /type=["']?search["']?/i.test(sel)) s += 25;
|
|
88
|
+
if (attrs.name === 'q' || /name=["']?q["']?/i.test(sel)) s += 20;
|
|
89
|
+
if (/search|ara|query|find/i.test(sel)) s += 15;
|
|
90
|
+
|
|
91
|
+
return s;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return [...candidates].sort((a, b) => score(b) - score(a));
|
|
95
|
+
}
|