feat: MCP Browser - servidor MCP para automação de navegador com Playwright
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
const openUrl = require('./openUrl');
|
||||
const smartClick = require('./smartClick');
|
||||
const smartType = require('./smartType');
|
||||
const fillFormAuto = require('./fillFormAuto');
|
||||
const smartWaitNavigation = require('./smartWaitNavigation');
|
||||
const getButtons = require('./getButtons');
|
||||
const getForms = require('./getForms');
|
||||
|
||||
module.exports = async ({ goal, data = {} }) => {
|
||||
if (!goal) {
|
||||
throw new Error('Goal é obrigatório');
|
||||
}
|
||||
|
||||
let log = [];
|
||||
|
||||
try {
|
||||
if (goal.includes('http')) {
|
||||
await openUrl({ url: goal });
|
||||
log.push("Abriu URL");
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const forms = await getForms();
|
||||
|
||||
if (forms.count > 0) {
|
||||
await fillFormAuto({ data });
|
||||
log.push("Formulário preenchido automaticamente");
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = await getButtons();
|
||||
|
||||
const keywords = ['login', 'entrar', 'continuar', 'enviar', 'buscar', 'submit', 'ok', 'confirmar'];
|
||||
|
||||
for (const btn of buttons.buttons) {
|
||||
const match = keywords.find(k =>
|
||||
btn.text.toLowerCase().includes(k)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
await smartClick({ text: btn.text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Clicou em: ${btn.text}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "agent_flow",
|
||||
goal,
|
||||
steps: log
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
steps: log
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
const openUrl = require('./openUrl');
|
||||
const smartClick = require('./smartClick');
|
||||
const smartType = require('./smartType');
|
||||
const fillFormAuto = require('./fillFormAuto');
|
||||
const smartWaitNavigation = require('./smartWaitNavigation');
|
||||
const getButtons = require('./getButtons');
|
||||
const getForms = require('./getForms');
|
||||
|
||||
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
module.exports = async ({ goal, data = {}, retries = 3 }) => {
|
||||
if (!goal) {
|
||||
throw new Error('Goal é obrigatório');
|
||||
}
|
||||
|
||||
let log = [];
|
||||
let attempt = 0;
|
||||
|
||||
const keywords = ['login', 'entrar', 'continuar', 'enviar', 'buscar', 'submit', 'ok', 'confirmar'];
|
||||
|
||||
while (attempt < retries) {
|
||||
try {
|
||||
log.push(`Tentativa ${attempt + 1}`);
|
||||
|
||||
if (goal.includes('http')) {
|
||||
await openUrl({ url: goal });
|
||||
log.push("Abriu URL");
|
||||
await delay(1500);
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const forms = await getForms();
|
||||
|
||||
if (forms.count > 0) {
|
||||
await fillFormAuto({ data });
|
||||
log.push("Formulário preenchido");
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = await getButtons();
|
||||
|
||||
let clicked = false;
|
||||
|
||||
for (const btn of buttons.buttons) {
|
||||
const match = keywords.find(k =>
|
||||
btn.text.toLowerCase().includes(k)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
await smartClick({ text: btn.text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Clicou em: ${btn.text}`);
|
||||
clicked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked && buttons.buttons.length > 0) {
|
||||
await smartClick({ text: buttons.buttons[0].text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Fallback click: ${buttons.buttons[0].text}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "agent_flow_v2",
|
||||
attempts: attempt + 1,
|
||||
steps: log
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log.push(`Erro: ${error.message}`);
|
||||
attempt++;
|
||||
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
action: "agent_flow_v2",
|
||||
attempts: attempt,
|
||||
steps: log
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector }) => {
|
||||
if (!selector) {
|
||||
throw new Error('Selector é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.click(selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "click",
|
||||
selector
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "close_browser",
|
||||
message: "Navegador fechado com sucesso"
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, attributes = [] }) => {
|
||||
if (!selector) {
|
||||
throw new Error('Selector é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const attrsStr = JSON.stringify(Array.isArray(attributes) ? attributes : []);
|
||||
|
||||
const elements = await p.evaluate((sel) => {
|
||||
const attrs = JSON.parse(sel.__attrs);
|
||||
const els = Array.from(document.querySelectorAll(sel.__sel));
|
||||
|
||||
return els.map(el => {
|
||||
const data = {
|
||||
text: (el.innerText || el.textContent || '').trim()
|
||||
};
|
||||
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
data[attrs[i]] = el.getAttribute(attrs[i]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}, { __sel: selector, __attrs: attrsStr });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "extract_elements",
|
||||
count: elements.length,
|
||||
elements
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ data }) => {
|
||||
if (!data) {
|
||||
throw new Error('Data é obrigatória');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const result = await p.evaluate((formData) => {
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
||||
|
||||
let filled = [];
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = Object.keys(formData).find(k =>
|
||||
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.placeholder && input.placeholder.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.id && input.id.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase())
|
||||
);
|
||||
|
||||
if (key) {
|
||||
const value = formData[key];
|
||||
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
if (input.tagName === 'INPUT' && nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(input, value);
|
||||
} else if (input.tagName === 'TEXTAREA' && nativeTextAreaValueSetter) {
|
||||
nativeTextAreaValueSetter.call(input, value);
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
filled.push({
|
||||
field: input.name || input.id || input.placeholder || 'unknown',
|
||||
value: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return filled;
|
||||
}, data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "fill_form_auto",
|
||||
filled: result,
|
||||
count: result.length
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const buttons = await p.evaluate(() => {
|
||||
const elements = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button'));
|
||||
|
||||
return elements.map(el => ({
|
||||
text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(),
|
||||
type: el.tagName.toLowerCase(),
|
||||
id: el.id || null
|
||||
})).filter(b => b.text !== '');
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_buttons",
|
||||
count: buttons.length,
|
||||
buttons
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const forms = await p.evaluate(() => {
|
||||
const formElements = Array.from(document.querySelectorAll('form'));
|
||||
|
||||
return formElements.map(form => {
|
||||
const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
|
||||
|
||||
return {
|
||||
action: form.action || null,
|
||||
method: form.method || 'GET',
|
||||
fields: inputs.map(input => ({
|
||||
name: input.name || null,
|
||||
type: input.type || input.tagName.toLowerCase(),
|
||||
placeholder: input.placeholder || '',
|
||||
id: input.id || null
|
||||
}))
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_forms",
|
||||
count: forms.length,
|
||||
forms
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'a' }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const links = await p.evaluate((sel) => {
|
||||
const elements = Array.from(document.querySelectorAll(sel));
|
||||
|
||||
return elements.map(el => ({
|
||||
text: (el.innerText || el.textContent || '').trim(),
|
||||
href: el.href || el.getAttribute('href') || ''
|
||||
})).filter(link => link.href && link.href !== '');
|
||||
}, selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_links",
|
||||
count: links.length,
|
||||
links
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'body' }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const text = await p.evaluate((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
|
||||
const t = el.innerText || el.textContent || '';
|
||||
return t.trim() || null;
|
||||
}, selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_text",
|
||||
selector,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ url }) => {
|
||||
if (!url) {
|
||||
throw new Error('URL é obrigatória');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const currentUrl = p.url();
|
||||
|
||||
if (currentUrl && currentUrl !== 'about:blank') {
|
||||
if (currentUrl.includes(new URL(url).hostname)) {
|
||||
await p.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await p.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await p.title(),
|
||||
tab: 'reused'
|
||||
};
|
||||
}
|
||||
|
||||
const newPage = await browser.newTab();
|
||||
await newPage.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await newPage.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await newPage.title(),
|
||||
tab: 'new'
|
||||
};
|
||||
}
|
||||
|
||||
await p.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await p.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await p.title(),
|
||||
tab: 'existing'
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ fullPage = true }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const path = `screenshot-${Date.now()}.png`;
|
||||
|
||||
await p.screenshot({ path, fullPage });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "screenshot",
|
||||
file: path
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ text }) => {
|
||||
if (!text) {
|
||||
throw new Error('Texto é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const clicked = await p.evaluate((targetText) => {
|
||||
const elements = Array.from(document.querySelectorAll(
|
||||
'a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]'
|
||||
));
|
||||
|
||||
const el = elements.find(e => {
|
||||
const txt = (e.innerText || e.textContent || e.value || e.getAttribute('aria-label') || '').trim().toLowerCase();
|
||||
return txt.includes(targetText.toLowerCase());
|
||||
});
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.click();
|
||||
|
||||
if (el.tagName === 'A' && el.href) {
|
||||
return { clicked: true, href: el.href };
|
||||
}
|
||||
|
||||
return { clicked: true, href: null };
|
||||
}
|
||||
|
||||
return { clicked: false, href: null };
|
||||
}, text);
|
||||
|
||||
if (!clicked.clicked) {
|
||||
throw new Error(`Elemento não encontrado pelo texto: "${text}"`);
|
||||
}
|
||||
|
||||
if (clicked.href) {
|
||||
try {
|
||||
await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' });
|
||||
} catch (e) {
|
||||
}
|
||||
} else {
|
||||
await p.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_click",
|
||||
text,
|
||||
navigatedTo: clicked.href || null
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ label, text }) => {
|
||||
if (!label || !text) {
|
||||
throw new Error('Label e text são obrigatórios');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const success = await p.evaluate(({ labelText, inputText }) => {
|
||||
function setInputValue(input, value) {
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
if (input.tagName === 'INPUT' && nativeSetter) {
|
||||
nativeSetter.call(input, value);
|
||||
} else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) {
|
||||
nativeTextAreaSetter.call(input, value);
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
const labels = Array.from(document.querySelectorAll('label'));
|
||||
|
||||
const foundLabel = labels.find(l =>
|
||||
(l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundLabel) {
|
||||
const forAttr = foundLabel.getAttribute('for');
|
||||
|
||||
if (forAttr) {
|
||||
const input = document.getElementById(forAttr);
|
||||
if (input) {
|
||||
setInputValue(input, inputText);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const input = foundLabel.querySelector('input, textarea');
|
||||
if (input) {
|
||||
setInputValue(input, inputText);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea'));
|
||||
|
||||
const foundInput = inputs.find(i =>
|
||||
(i.placeholder || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.name || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.id || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.getAttribute('aria-label') || '').toLowerCase().includes(labelText.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundInput) {
|
||||
setInputValue(foundInput, inputText);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, { labelText: label, inputText: text });
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Campo não encontrado');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_type",
|
||||
label,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ timeout = 15000 }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const oldUrl = p.url();
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }),
|
||||
p.waitForLoadState('domcontentloaded', { timeout })
|
||||
]);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
const newUrl = p.url();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_wait_navigation",
|
||||
oldUrl,
|
||||
newUrl,
|
||||
changed: oldUrl !== newUrl
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, text }) => {
|
||||
if (!selector || !text) {
|
||||
throw new Error('Selector e text são obrigatórios');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.fill(selector, text);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "type",
|
||||
selector,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, timeout = 15000 }) => {
|
||||
if (!selector) {
|
||||
throw new Error('Selector é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const el = await p.waitForSelector(selector, {
|
||||
timeout,
|
||||
state: 'attached'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "wait_for_selector",
|
||||
selector,
|
||||
found: !!el
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user