chromex-mcp 1.0.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 +21 -0
- package/README.md +411 -0
- package/package.json +50 -0
- package/plugins/chromex/skills/chromex/scripts/chromex.mjs +343 -0
- package/plugins/chromex/skills/chromex/scripts/lib/browser.mjs +66 -0
- package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +98 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +37 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +77 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/coverage.mjs +95 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/cpu.mjs +14 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/dialog.mjs +38 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/domsnapshot.mjs +84 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/download.mjs +25 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/drag.mjs +71 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +44 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/evaluate.mjs +31 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/form.mjs +163 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/geo.mjs +37 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/har.mjs +101 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/heap.mjs +24 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/highlight.mjs +36 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/html.mjs +10 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/inject.mjs +39 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +88 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/intercept.mjs +99 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +45 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +13 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/pdf.mjs +16 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +98 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +67 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +54 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/scroll.mjs +44 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +100 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +47 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/tab.mjs +31 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/throttle.mjs +38 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/touch.mjs +62 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/trace.mjs +51 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/upload.mjs +43 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/wait.mjs +84 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/webauthn.mjs +47 -0
- package/plugins/chromex/skills/chromex/scripts/lib/config.mjs +100 -0
- package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +368 -0
- package/plugins/chromex/skills/chromex/scripts/lib/ipc.mjs +178 -0
- package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +111 -0
- package/plugins/chromex/skills/chromex/scripts/lib/security.mjs +48 -0
- package/plugins/chromex/skills/chromex/scripts/lib/utils.mjs +47 -0
- package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +726 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// CSS/JS coverage reporting via Profiler and CSS domains
|
|
2
|
+
|
|
3
|
+
let active = false;
|
|
4
|
+
|
|
5
|
+
export async function coverageStr(cdp, sid, action) {
|
|
6
|
+
if (!action) throw new Error('Usage: coverage <target> start | stop');
|
|
7
|
+
|
|
8
|
+
switch (action) {
|
|
9
|
+
case 'start': {
|
|
10
|
+
if (active) return 'Coverage collection already active.';
|
|
11
|
+
await cdp.send('DOM.enable', {}, sid);
|
|
12
|
+
await cdp.send('Profiler.enable', {}, sid);
|
|
13
|
+
await cdp.send('Debugger.enable', {}, sid);
|
|
14
|
+
await cdp.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }, sid);
|
|
15
|
+
await cdp.send('CSS.enable', {}, sid);
|
|
16
|
+
await cdp.send('CSS.startRuleUsageTracking', {}, sid);
|
|
17
|
+
active = true;
|
|
18
|
+
return 'Coverage collection started. Navigate/interact, then "coverage <target> stop" for report.';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case 'stop': {
|
|
22
|
+
if (!active) return 'No coverage collection active.';
|
|
23
|
+
active = false;
|
|
24
|
+
|
|
25
|
+
// JS Coverage
|
|
26
|
+
const { result: jsCoverage } = await cdp.send('Profiler.takePreciseCoverage', {}, sid);
|
|
27
|
+
await cdp.send('Profiler.stopPreciseCoverage', {}, sid);
|
|
28
|
+
await cdp.send('Profiler.disable', {}, sid);
|
|
29
|
+
await cdp.send('Debugger.disable', {}, sid);
|
|
30
|
+
|
|
31
|
+
// CSS Coverage
|
|
32
|
+
const { ruleUsage } = await cdp.send('CSS.stopRuleUsageTracking', {}, sid);
|
|
33
|
+
await cdp.send('CSS.disable', {}, sid);
|
|
34
|
+
|
|
35
|
+
// JS report
|
|
36
|
+
const jsFiles = [];
|
|
37
|
+
let jsTotalBytes = 0;
|
|
38
|
+
let jsUsedBytes = 0;
|
|
39
|
+
for (const script of jsCoverage) {
|
|
40
|
+
if (!script.url || script.url.startsWith('extensions://')) continue;
|
|
41
|
+
let scriptTotal = 0;
|
|
42
|
+
let scriptUsed = 0;
|
|
43
|
+
for (const fn of script.functions) {
|
|
44
|
+
for (const range of fn.ranges) {
|
|
45
|
+
const size = range.endOffset - range.startOffset;
|
|
46
|
+
scriptTotal += size;
|
|
47
|
+
if (range.count > 0) scriptUsed += size;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
jsTotalBytes += scriptTotal;
|
|
51
|
+
jsUsedBytes += scriptUsed;
|
|
52
|
+
if (scriptTotal > 0) {
|
|
53
|
+
jsFiles.push({
|
|
54
|
+
url: script.url.substring(0, 80),
|
|
55
|
+
total: scriptTotal,
|
|
56
|
+
used: scriptUsed,
|
|
57
|
+
pct: Math.round((scriptUsed / scriptTotal) * 100),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// CSS report
|
|
63
|
+
const cssUsed = ruleUsage ? ruleUsage.filter(r => r.used).length : 0;
|
|
64
|
+
const cssTotal = ruleUsage ? ruleUsage.length : 0;
|
|
65
|
+
|
|
66
|
+
const lines = ['## JavaScript Coverage'];
|
|
67
|
+
lines.push(`Total: ${formatBytes(jsTotalBytes)}, Used: ${formatBytes(jsUsedBytes)} (${jsTotalBytes > 0 ? Math.round((jsUsedBytes / jsTotalBytes) * 100) : 0}%)`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
|
|
70
|
+
// Top unused files
|
|
71
|
+
const unused = jsFiles.filter(f => f.pct < 50).sort((a, b) => (a.pct - b.pct));
|
|
72
|
+
if (unused.length > 0) {
|
|
73
|
+
lines.push('Files with <50% usage:');
|
|
74
|
+
for (const f of unused.slice(0, 10)) {
|
|
75
|
+
lines.push(` ${String(f.pct).padStart(3)}% ${formatBytes(f.total).padStart(8)} ${f.url}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push('## CSS Coverage');
|
|
81
|
+
lines.push(`Rules: ${cssTotal} total, ${cssUsed} used (${cssTotal > 0 ? Math.round((cssUsed / cssTotal) * 100) : 0}%)`);
|
|
82
|
+
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
default:
|
|
87
|
+
throw new Error('Usage: coverage <target> start | stop');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatBytes(bytes) {
|
|
92
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
93
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
94
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
95
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// CPU throttling via Emulation domain
|
|
2
|
+
|
|
3
|
+
export async function cpuStr(cdp, sid, rate) {
|
|
4
|
+
if (!rate || rate === 'reset') {
|
|
5
|
+
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 }, sid);
|
|
6
|
+
return 'CPU throttling reset to normal.';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const r = parseFloat(rate);
|
|
10
|
+
if (isNaN(r) || r < 1) throw new Error('Rate must be >= 1 (1=normal, 4=4x slower, 6=mobile sim)');
|
|
11
|
+
|
|
12
|
+
await cdp.send('Emulation.setCPUThrottlingRate', { rate: r }, sid);
|
|
13
|
+
return `CPU throttled to ${r}x slower.`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Dialog handling (alert/confirm/prompt) via Page domain
|
|
2
|
+
|
|
3
|
+
export async function dialogStr(cdp, sid, action, text) {
|
|
4
|
+
await cdp.send('Page.enable', {}, sid);
|
|
5
|
+
|
|
6
|
+
switch (action) {
|
|
7
|
+
case 'accept':
|
|
8
|
+
await cdp.send('Page.handleJavaScriptDialog', {
|
|
9
|
+
accept: true,
|
|
10
|
+
promptText: text || '',
|
|
11
|
+
}, sid);
|
|
12
|
+
return `Dialog accepted${text ? ` with text "${text}"` : ''}.`;
|
|
13
|
+
|
|
14
|
+
case 'dismiss':
|
|
15
|
+
await cdp.send('Page.handleJavaScriptDialog', { accept: false }, sid);
|
|
16
|
+
return 'Dialog dismissed.';
|
|
17
|
+
|
|
18
|
+
case 'auto':
|
|
19
|
+
// Retorna flag para o daemon registrar auto-handler
|
|
20
|
+
return '__AUTO_DIALOG__';
|
|
21
|
+
|
|
22
|
+
default:
|
|
23
|
+
throw new Error('Usage: dialog <target> accept [text] | dismiss | auto');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Registra handler permanente que auto-aceita dialogs
|
|
28
|
+
export function setupAutoDialog(cdp, sid) {
|
|
29
|
+
cdp.onEvent('Page.javascriptDialogOpening', async (params) => {
|
|
30
|
+
try {
|
|
31
|
+
await cdp.send('Page.handleJavaScriptDialog', {
|
|
32
|
+
accept: true,
|
|
33
|
+
promptText: '',
|
|
34
|
+
}, sid);
|
|
35
|
+
} catch { /* dialog ja foi tratado */ }
|
|
36
|
+
});
|
|
37
|
+
return 'Auto-dialog enabled: all dialogs (alert/confirm/prompt) will be auto-accepted.';
|
|
38
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Structured DOM snapshot via DOMSnapshot domain
|
|
2
|
+
|
|
3
|
+
export async function domsnapshotStr(cdp, sid, includeStyles) {
|
|
4
|
+
const computedStyles = includeStyles
|
|
5
|
+
? ['display', 'visibility', 'opacity', 'overflow', 'position', 'z-index', 'font-size', 'color', 'background-color']
|
|
6
|
+
: ['display', 'visibility'];
|
|
7
|
+
|
|
8
|
+
const snapshot = await cdp.send('DOMSnapshot.captureSnapshot', {
|
|
9
|
+
computedStyles,
|
|
10
|
+
includeDOMRects: true,
|
|
11
|
+
includePaintOrder: true,
|
|
12
|
+
}, sid);
|
|
13
|
+
|
|
14
|
+
const { documents, strings } = snapshot;
|
|
15
|
+
if (!documents || documents.length === 0) return 'No DOM snapshot available.';
|
|
16
|
+
|
|
17
|
+
const doc = documents[0];
|
|
18
|
+
const nodes = doc.nodes;
|
|
19
|
+
const layout = doc.layout;
|
|
20
|
+
const lines = [];
|
|
21
|
+
|
|
22
|
+
// Build layout index by nodeIndex
|
|
23
|
+
const layoutMap = new Map();
|
|
24
|
+
if (layout && layout.nodeIndex && layout.bounds) {
|
|
25
|
+
for (let i = 0; i < layout.nodeIndex.length; i++) {
|
|
26
|
+
const ni = layout.nodeIndex[i];
|
|
27
|
+
const b = layout.bounds[i]; // Each bounds is [x, y, w, h]
|
|
28
|
+
if (Array.isArray(b) && b.length >= 4) {
|
|
29
|
+
layoutMap.set(ni, { bounds: b });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Build indented tree
|
|
35
|
+
const parentIndex = nodes.parentIndex || [];
|
|
36
|
+
const nodeNames = nodes.nodeName || [];
|
|
37
|
+
const nodeValues = nodes.nodeValue || [];
|
|
38
|
+
const attrs = nodes.attributes || [];
|
|
39
|
+
|
|
40
|
+
// Calcular profundidade
|
|
41
|
+
const depth = new Array(nodeNames.length).fill(0);
|
|
42
|
+
for (let i = 1; i < nodeNames.length; i++) {
|
|
43
|
+
if (parentIndex[i] >= 0) depth[i] = depth[parentIndex[i]] + 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < nodeNames.length; i++) {
|
|
47
|
+
const name = strings[nodeNames[i]] || '';
|
|
48
|
+
if (!name || name === '#document' || name === '#comment') continue;
|
|
49
|
+
|
|
50
|
+
const indent = ' '.repeat(Math.min(depth[i], 8));
|
|
51
|
+
const lay = layoutMap.get(i);
|
|
52
|
+
|
|
53
|
+
if (name === '#text') {
|
|
54
|
+
const text = strings[nodeValues[i]] || '';
|
|
55
|
+
if (text.trim()) {
|
|
56
|
+
lines.push(`${indent}"${text.trim().substring(0, 80)}"`);
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Atributos do nó
|
|
62
|
+
const nodeAttrs = attrs[i] || [];
|
|
63
|
+
let attrStr = '';
|
|
64
|
+
for (let a = 0; a < nodeAttrs.length; a += 2) {
|
|
65
|
+
const attrName = strings[nodeAttrs[a]] || '';
|
|
66
|
+
const attrVal = strings[nodeAttrs[a + 1]] || '';
|
|
67
|
+
if (['id', 'class', 'name', 'type', 'href', 'src', 'role'].includes(attrName) && attrVal) {
|
|
68
|
+
attrStr += ` ${attrName}="${attrVal.substring(0, 40)}"`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let line = `${indent}<${name.toLowerCase()}${attrStr}>`;
|
|
73
|
+
|
|
74
|
+
// Adicionar bounding rect se disponível
|
|
75
|
+
if (lay?.bounds) {
|
|
76
|
+
const [x, y, w, h] = lay.bounds;
|
|
77
|
+
line += ` [${Math.round(x)},${Math.round(y)} ${Math.round(w)}x${Math.round(h)}]`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push(line);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Download control via Browser domain
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
export async function downloadStr(cdp, sid, action, path) {
|
|
6
|
+
switch (action) {
|
|
7
|
+
case 'allow': {
|
|
8
|
+
const downloadPath = path || '/tmp/chromex-downloads';
|
|
9
|
+
if (!existsSync(downloadPath)) mkdirSync(downloadPath, { recursive: true });
|
|
10
|
+
await cdp.send('Browser.setDownloadBehavior', {
|
|
11
|
+
behavior: 'allowAndName',
|
|
12
|
+
downloadPath,
|
|
13
|
+
}, sid);
|
|
14
|
+
return `Downloads allowed. Path: ${downloadPath}`;
|
|
15
|
+
}
|
|
16
|
+
case 'deny':
|
|
17
|
+
await cdp.send('Browser.setDownloadBehavior', { behavior: 'deny' }, sid);
|
|
18
|
+
return 'Downloads blocked.';
|
|
19
|
+
case 'reset':
|
|
20
|
+
await cdp.send('Browser.setDownloadBehavior', { behavior: 'default' }, sid);
|
|
21
|
+
return 'Download behavior reset to default.';
|
|
22
|
+
default:
|
|
23
|
+
throw new Error('Usage: download <target> allow [path] | deny | reset');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Drag & Drop via Input domain
|
|
2
|
+
|
|
3
|
+
import { sleep } from '../utils.mjs';
|
|
4
|
+
import { evalStr } from './evaluate.mjs';
|
|
5
|
+
|
|
6
|
+
export async function dragStr(cdp, sid, from, to) {
|
|
7
|
+
if (!from || !to) throw new Error('Usage: drag <target> <from_selector> <to_selector> or drag <target> x1,y1 x2,y2');
|
|
8
|
+
|
|
9
|
+
let fromX, fromY, toX, toY;
|
|
10
|
+
|
|
11
|
+
// Coordenadas diretas: "100,200"
|
|
12
|
+
if (from.includes(',') && to.includes(',')) {
|
|
13
|
+
[fromX, fromY] = from.split(',').map(Number);
|
|
14
|
+
[toX, toY] = to.split(',').map(Number);
|
|
15
|
+
if (isNaN(fromX) || isNaN(fromY) || isNaN(toX) || isNaN(toY)) {
|
|
16
|
+
throw new Error('Invalid coordinates. Use: x1,y1 x2,y2');
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
// CSS selectors: resolver para coordenadas centrais
|
|
20
|
+
fromX = await getCenterX(cdp, sid, from);
|
|
21
|
+
fromY = await getCenterY(cdp, sid, from);
|
|
22
|
+
toX = await getCenterX(cdp, sid, to);
|
|
23
|
+
toY = await getCenterY(cdp, sid, to);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const base = { button: 'left', clickCount: 1, modifiers: 0 };
|
|
27
|
+
|
|
28
|
+
// Mouse down no ponto de origem
|
|
29
|
+
await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mousePressed', x: fromX, y: fromY }, sid);
|
|
30
|
+
await sleep(100);
|
|
31
|
+
|
|
32
|
+
// Mover em passos intermediários para simular arraste real
|
|
33
|
+
const steps = 5;
|
|
34
|
+
for (let i = 1; i <= steps; i++) {
|
|
35
|
+
const x = fromX + (toX - fromX) * (i / steps);
|
|
36
|
+
const y = fromY + (toY - fromY) * (i / steps);
|
|
37
|
+
await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseMoved', x, y }, sid);
|
|
38
|
+
await sleep(50);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Mouse up no destino
|
|
42
|
+
await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseReleased', x: toX, y: toY }, sid);
|
|
43
|
+
|
|
44
|
+
return `Dragged from (${Math.round(fromX)},${Math.round(fromY)}) to (${Math.round(toX)},${Math.round(toY)}).`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getCenterX(cdp, sid, selector) {
|
|
48
|
+
const raw = await evalStr(cdp, sid, `
|
|
49
|
+
(function() {
|
|
50
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
51
|
+
if (!el) return null;
|
|
52
|
+
const r = el.getBoundingClientRect();
|
|
53
|
+
return Math.round(r.left + r.width / 2);
|
|
54
|
+
})()
|
|
55
|
+
`);
|
|
56
|
+
if (raw === 'null') throw new Error(`Element not found: ${selector}`);
|
|
57
|
+
return parseFloat(raw);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getCenterY(cdp, sid, selector) {
|
|
61
|
+
const raw = await evalStr(cdp, sid, `
|
|
62
|
+
(function() {
|
|
63
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
64
|
+
if (!el) return null;
|
|
65
|
+
const r = el.getBoundingClientRect();
|
|
66
|
+
return Math.round(r.top + r.height / 2);
|
|
67
|
+
})()
|
|
68
|
+
`);
|
|
69
|
+
if (raw === 'null') throw new Error(`Element not found: ${selector}`);
|
|
70
|
+
return parseFloat(raw);
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Device emulation via CDP
|
|
2
|
+
|
|
3
|
+
const DEVICES = {
|
|
4
|
+
'iphone-14': { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
|
|
5
|
+
'iphone-15-pro': { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
|
|
6
|
+
'ipad-pro': { width: 1024, height: 1366, deviceScaleFactor: 2, mobile: true, ua: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
|
|
7
|
+
'pixel-7': { width: 412, height: 915, deviceScaleFactor: 2.625, mobile: true, ua: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36' },
|
|
8
|
+
'galaxy-s23': { width: 360, height: 780, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36' },
|
|
9
|
+
'macbook-air': { width: 1440, height: 900, deviceScaleFactor: 2, mobile: false, ua: '' },
|
|
10
|
+
'desktop-1080p': { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false, ua: '' },
|
|
11
|
+
'desktop-4k': { width: 3840, height: 2160, deviceScaleFactor: 1, mobile: false, ua: '' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function emulateStr(cdp, sid, device) {
|
|
15
|
+
if (!device) {
|
|
16
|
+
const list = Object.keys(DEVICES).join(', ');
|
|
17
|
+
throw new Error(`Device name required. Available: ${list}, reset`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (device === 'reset') {
|
|
21
|
+
await cdp.send('Emulation.clearDeviceMetricsOverride', {}, sid);
|
|
22
|
+
await cdp.send('Emulation.setUserAgentOverride', { userAgent: '' }, sid);
|
|
23
|
+
return 'Device emulation reset to default.';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const preset = DEVICES[device.toLowerCase()];
|
|
27
|
+
if (!preset) {
|
|
28
|
+
const list = Object.keys(DEVICES).join(', ');
|
|
29
|
+
throw new Error(`Unknown device: ${device}. Available: ${list}, reset`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
33
|
+
width: preset.width,
|
|
34
|
+
height: preset.height,
|
|
35
|
+
deviceScaleFactor: preset.deviceScaleFactor,
|
|
36
|
+
mobile: preset.mobile,
|
|
37
|
+
}, sid);
|
|
38
|
+
|
|
39
|
+
if (preset.ua) {
|
|
40
|
+
await cdp.send('Emulation.setUserAgentOverride', { userAgent: preset.ua }, sid);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `Emulating ${device}: ${preset.width}x${preset.height} @${preset.deviceScaleFactor}x${preset.mobile ? ' (mobile)' : ''}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Eval JS + evalraw CDP
|
|
2
|
+
|
|
3
|
+
import { isCdpMethodBlocked } from '../security.mjs';
|
|
4
|
+
|
|
5
|
+
export async function evalStr(cdp, sid, expression) {
|
|
6
|
+
await cdp.send('Runtime.enable', {}, sid);
|
|
7
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
8
|
+
expression, returnByValue: true, awaitPromise: true,
|
|
9
|
+
}, sid);
|
|
10
|
+
if (result.exceptionDetails) {
|
|
11
|
+
throw new Error(result.exceptionDetails.text || result.exceptionDetails.exception?.description);
|
|
12
|
+
}
|
|
13
|
+
const val = result.result.value;
|
|
14
|
+
return typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val ?? '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function evalRawStr(cdp, sid, method, paramsJson, config) {
|
|
18
|
+
if (!method) throw new Error('CDP method required (e.g. "DOM.getDocument")');
|
|
19
|
+
|
|
20
|
+
if (isCdpMethodBlocked(method, config)) {
|
|
21
|
+
throw new Error(`CDP method "${method}" is blocked by security config. Edit ~/.chromex/config.json to change.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let params = {};
|
|
25
|
+
if (paramsJson) {
|
|
26
|
+
try { params = JSON.parse(paramsJson); }
|
|
27
|
+
catch { throw new Error(`Invalid JSON params: ${paramsJson}`); }
|
|
28
|
+
}
|
|
29
|
+
const result = await cdp.send(method, params, sid);
|
|
30
|
+
return JSON.stringify(result, null, 2);
|
|
31
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Form filling: fill, clear, select, check, form (batch)
|
|
2
|
+
|
|
3
|
+
import { sleep } from '../utils.mjs';
|
|
4
|
+
import { evalStr } from './evaluate.mjs';
|
|
5
|
+
|
|
6
|
+
export async function fillStr(cdp, sid, selector, value) {
|
|
7
|
+
if (!selector) throw new Error('CSS selector required');
|
|
8
|
+
if (value == null) throw new Error('Value required');
|
|
9
|
+
|
|
10
|
+
// Focar e limpar o campo
|
|
11
|
+
const info = await evalStr(cdp, sid, `
|
|
12
|
+
(function() {
|
|
13
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
14
|
+
if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
15
|
+
el.scrollIntoView({ block: 'center' });
|
|
16
|
+
el.focus();
|
|
17
|
+
if (el.select) el.select();
|
|
18
|
+
return { ok: true, tag: el.tagName, type: el.type || '', name: el.name || '' };
|
|
19
|
+
})()
|
|
20
|
+
`);
|
|
21
|
+
const r = JSON.parse(info);
|
|
22
|
+
if (!r.ok) throw new Error(r.error);
|
|
23
|
+
|
|
24
|
+
// Selecionar tudo e deletar (funciona em campos React controlled)
|
|
25
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
26
|
+
type: 'keyDown', key: 'a', code: 'KeyA', modifiers: getModifierKey(),
|
|
27
|
+
}, sid);
|
|
28
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
29
|
+
type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0,
|
|
30
|
+
}, sid);
|
|
31
|
+
await sleep(50);
|
|
32
|
+
|
|
33
|
+
// Inserir texto
|
|
34
|
+
await cdp.send('Input.insertText', { text: String(value) }, sid);
|
|
35
|
+
|
|
36
|
+
// Disparar eventos de mudança para frameworks reativos
|
|
37
|
+
await evalStr(cdp, sid, `
|
|
38
|
+
(function() {
|
|
39
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
40
|
+
if (!el) return;
|
|
41
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
42
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
43
|
+
})()
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
return `Filled <${r.tag}${r.name ? ` name="${r.name}"` : ''}> with "${String(value).substring(0, 50)}"`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function clearStr(cdp, sid, selector) {
|
|
50
|
+
if (!selector) throw new Error('CSS selector required');
|
|
51
|
+
|
|
52
|
+
const info = await evalStr(cdp, sid, `
|
|
53
|
+
(function() {
|
|
54
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
55
|
+
if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
56
|
+
el.scrollIntoView({ block: 'center' });
|
|
57
|
+
el.focus();
|
|
58
|
+
if (el.select) el.select();
|
|
59
|
+
return { ok: true, tag: el.tagName, name: el.name || '' };
|
|
60
|
+
})()
|
|
61
|
+
`);
|
|
62
|
+
const r = JSON.parse(info);
|
|
63
|
+
if (!r.ok) throw new Error(r.error);
|
|
64
|
+
|
|
65
|
+
// Selecionar tudo + Delete
|
|
66
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
67
|
+
type: 'keyDown', key: 'a', code: 'KeyA', modifiers: getModifierKey(),
|
|
68
|
+
}, sid);
|
|
69
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
70
|
+
type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0,
|
|
71
|
+
}, sid);
|
|
72
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
73
|
+
type: 'keyDown', key: 'Delete', code: 'Delete',
|
|
74
|
+
}, sid);
|
|
75
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
76
|
+
type: 'keyUp', key: 'Delete', code: 'Delete',
|
|
77
|
+
}, sid);
|
|
78
|
+
|
|
79
|
+
await evalStr(cdp, sid, `
|
|
80
|
+
(function() {
|
|
81
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
82
|
+
if (!el) return;
|
|
83
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
84
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
85
|
+
})()
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
return `Cleared <${r.tag}${r.name ? ` name="${r.name}"` : ''}>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function selectStr(cdp, sid, selector, value) {
|
|
92
|
+
if (!selector) throw new Error('CSS selector required');
|
|
93
|
+
if (value == null) throw new Error('Value required');
|
|
94
|
+
|
|
95
|
+
const result = await evalStr(cdp, sid, `
|
|
96
|
+
(function() {
|
|
97
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
98
|
+
if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
99
|
+
if (el.tagName !== 'SELECT') return { ok: false, error: 'Element is not a <select>' };
|
|
100
|
+
const val = ${JSON.stringify(String(value))};
|
|
101
|
+
const option = Array.from(el.options).find(o => o.value === val || o.textContent.trim() === val);
|
|
102
|
+
if (!option) {
|
|
103
|
+
const opts = Array.from(el.options).map(o => o.value || o.textContent.trim()).slice(0, 10);
|
|
104
|
+
return { ok: false, error: 'Option not found: ' + val + '. Available: ' + opts.join(', ') };
|
|
105
|
+
}
|
|
106
|
+
el.value = option.value;
|
|
107
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
108
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
109
|
+
return { ok: true, selected: option.textContent.trim(), value: option.value };
|
|
110
|
+
})()
|
|
111
|
+
`);
|
|
112
|
+
const r = JSON.parse(result);
|
|
113
|
+
if (!r.ok) throw new Error(r.error);
|
|
114
|
+
return `Selected "${r.selected}" (value="${r.value}")`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function checkStr(cdp, sid, selector, checked = true) {
|
|
118
|
+
if (!selector) throw new Error('CSS selector required');
|
|
119
|
+
|
|
120
|
+
const result = await evalStr(cdp, sid, `
|
|
121
|
+
(function() {
|
|
122
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
123
|
+
if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
124
|
+
const want = ${JSON.stringify(checked)};
|
|
125
|
+
if (el.type !== 'checkbox' && el.type !== 'radio') {
|
|
126
|
+
return { ok: false, error: 'Element is not a checkbox/radio (type: ' + el.type + ')' };
|
|
127
|
+
}
|
|
128
|
+
if (el.checked !== want) {
|
|
129
|
+
el.click();
|
|
130
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, type: el.type, checked: el.checked, name: el.name || '' };
|
|
133
|
+
})()
|
|
134
|
+
`);
|
|
135
|
+
const r = JSON.parse(result);
|
|
136
|
+
if (!r.ok) throw new Error(r.error);
|
|
137
|
+
return `${r.type} ${r.name ? `"${r.name}" ` : ''}is now ${r.checked ? 'checked' : 'unchecked'}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function formStr(cdp, sid, fieldsJson) {
|
|
141
|
+
let fields;
|
|
142
|
+
try {
|
|
143
|
+
fields = JSON.parse(fieldsJson);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`Invalid JSON: ${fieldsJson}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = [];
|
|
149
|
+
for (const [selector, value] of Object.entries(fields)) {
|
|
150
|
+
if (typeof value === 'boolean') {
|
|
151
|
+
results.push(await checkStr(cdp, sid, selector, value));
|
|
152
|
+
} else {
|
|
153
|
+
results.push(await fillStr(cdp, sid, selector, String(value)));
|
|
154
|
+
}
|
|
155
|
+
await sleep(100); // Pausa entre campos para frameworks reativos
|
|
156
|
+
}
|
|
157
|
+
return results.join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// macOS usa Meta (Cmd), Linux usa Control
|
|
161
|
+
function getModifierKey() {
|
|
162
|
+
return process.platform === 'darwin' ? 4 : 2; // 4 = Meta, 2 = Control
|
|
163
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Geolocation, timezone, and locale override via Emulation domain
|
|
2
|
+
|
|
3
|
+
export async function geoStr(cdp, sid, lat, lon, accuracy) {
|
|
4
|
+
if (lat === 'reset') {
|
|
5
|
+
await cdp.send('Emulation.clearGeolocationOverride', {}, sid);
|
|
6
|
+
return 'Geolocation override cleared.';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const latitude = parseFloat(lat);
|
|
10
|
+
const longitude = parseFloat(lon);
|
|
11
|
+
if (isNaN(latitude) || isNaN(longitude)) {
|
|
12
|
+
throw new Error('Usage: geo <target> <latitude> <longitude> [accuracy] | reset');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await cdp.send('Emulation.setGeolocationOverride', {
|
|
16
|
+
latitude, longitude, accuracy: parseFloat(accuracy) || 100,
|
|
17
|
+
}, sid);
|
|
18
|
+
return `Geolocation set to (${latitude}, ${longitude}).`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function timezoneStr(cdp, sid, tz) {
|
|
22
|
+
if (!tz || tz === 'reset') {
|
|
23
|
+
await cdp.send('Emulation.setTimezoneOverride', { timezoneId: '' }, sid);
|
|
24
|
+
return 'Timezone override cleared.';
|
|
25
|
+
}
|
|
26
|
+
await cdp.send('Emulation.setTimezoneOverride', { timezoneId: tz }, sid);
|
|
27
|
+
return `Timezone set to ${tz}.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function localeStr(cdp, sid, locale) {
|
|
31
|
+
if (!locale || locale === 'reset') {
|
|
32
|
+
await cdp.send('Emulation.setLocaleOverride', { locale: '' }, sid);
|
|
33
|
+
return 'Locale override cleared.';
|
|
34
|
+
}
|
|
35
|
+
await cdp.send('Emulation.setLocaleOverride', { locale }, sid);
|
|
36
|
+
return `Locale set to ${locale}.`;
|
|
37
|
+
}
|