@eko-ai/eko-nodejs
Version:
Empowering language to transform human words into action.
512 lines (504 loc) • 18 kB
JavaScript
'use strict';
require('fs');
require('os');
require('path');
var eko = require('@eko-ai/eko');
var playwrightExtra = require('playwright-extra');
var StealthPlugin = require('puppeteer-extra-plugin-stealth');
var child_process = require('child_process');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var StealthPlugin__default = /*#__PURE__*/_interopDefault(StealthPlugin);
async function getCdpWsEndpoint(port) {
// Example => ws://localhost:9222/devtools/browser/{session-id}
const response = await fetch(`http://localhost:${port}/json/version`);
const browserInfo = await response.json();
eko.Log.info("browserInfo: ", browserInfo);
return browserInfo.webSocketDebuggerUrl;
}
var tabId = 1000;
class BrowserAgent extends eko.BaseBrowserLabelsAgent {
constructor() {
super(...arguments);
this.browser = null;
this.browserContext = null;
this.activePage = null;
this.headless = false;
this.pageMap = new Map();
this.lastSwitchedPage = null;
}
setHeadless(headless) {
this.headless = headless;
}
setCdpWsEndpoint(cdpWsEndpoint) {
this.cdpWsEndpoint = cdpWsEndpoint;
}
initUserDataDir(userDataDir) {
this.userDataDir = userDataDir;
return this.userDataDir;
}
setCookies(cookies) {
this.cookies = cookies;
}
setOptions(options) {
this.options = options;
}
async screenshot(agentContext) {
const page = await this.currentPage();
const screenshotBuffer = await page.screenshot({
fullPage: false,
type: "jpeg",
quality: 60,
animations: "disabled",
});
const base64 = screenshotBuffer.toString("base64");
return {
imageType: "image/jpeg",
imageBase64: base64,
};
}
async navigate_to(agentContext, url) {
const page = await this.open_url(agentContext, url);
this.lastSwitchedPage = page;
await this.sleep(200);
return {
url: page.url(),
title: await page.title(),
};
}
async loadTabs() {
return await this.get_all_tabs(undefined);
}
getPageMap() {
return this.pageMap;
}
async get_all_tabs(agentContext) {
if (!this.browserContext) {
return [];
}
const result = [];
const pages = this.browserContext.pages();
for (let i = 0; i < pages.length; i++) {
let page = pages[i];
let pageInfo = this.pageMap.get(page);
if (!pageInfo) {
pageInfo = {
tabId: tabId++,
lastAccessed: Date.now(),
};
this.pageMap.set(page, pageInfo);
}
result.push({
tabId: pageInfo.tabId,
url: page.url(),
title: await page.title(),
lastAccessed: pageInfo.lastAccessed,
active: page === this.activePage,
});
}
return result;
}
async switch_tab(agentContext, tabId) {
if (!this.browserContext) {
throw new Error("tabId does not exist: " + tabId);
}
let switchPage = null;
this.pageMap.forEach((pageInfo, page) => {
if (pageInfo.tabId === tabId) {
switchPage = page;
return;
}
});
if (!switchPage) {
throw new Error("tabId does not exist: " + tabId);
}
this.activePage = switchPage;
this.lastSwitchedPage = switchPage;
return {
tabId: tabId,
url: switchPage.url(),
title: await switchPage.title(),
};
}
async input_text(agentContext, index, text, enter) {
try {
const elementHandle = await this.get_element(index, true);
await elementHandle.fill("");
await elementHandle.fill(text);
if (enter) {
await elementHandle.press("Enter");
await this.sleep(200);
}
}
catch (e) {
await super.input_text(agentContext, index, text, enter);
}
}
async click_element(agentContext, index, num_clicks, button) {
try {
const elementHandle = await this.get_element(index, true);
const box = await elementHandle.boundingBox();
if (box) {
const page = await this.currentPage();
page.mouse.move(box.x + box.width / 2 + (Math.random() * 10 - 5), box.y + box.height / 2 + (Math.random() * 10 - 5), { steps: Math.floor(Math.random() * 5) + 3 });
}
await elementHandle.click({
button,
clickCount: num_clicks,
force: false,
delay: Math.random() * 50 + 20,
});
}
catch (e) {
await super.click_element(agentContext, index, num_clicks, button);
}
}
async hover_to_element(agentContext, index) {
try {
const elementHandle = await this.get_element(index, true);
elementHandle.hover({ force: true });
}
catch (e) {
await super.hover_to_element(agentContext, index);
}
}
async execute_script(agentContext, func, args) {
const page = await this.currentPage();
return await page.evaluate(func, ...args);
}
async open_url(agentContext, url) {
const browser_context = await this.getBrowserContext();
const page = await browser_context.newPage();
try {
await this.autoLoadCookies(url);
await this.autoLoadLocalStorage(page, url);
await page.goto(url, {
waitUntil: "domcontentloaded",
timeout: 10000,
});
await page.waitForLoadState("networkidle", { timeout: 5000 });
}
catch (e) {
if ((e + "").indexOf("Timeout") == -1) {
throw e;
}
}
this.activePage = page;
return page;
}
async currentPage() {
if (this.activePage == null) {
throw new Error("There is no page, please call navigate_to first");
}
const page = this.activePage;
try {
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
}
catch (e) { }
return page;
}
async get_element(index, findInput) {
const page = await this.currentPage();
return await page.evaluateHandle((params) => {
let element = window.get_highlight_element(params.index);
if (element && params.findInput) {
if (element.tagName != "INPUT" &&
element.tagName != "TEXTAREA" &&
element.childElementCount != 0) {
element =
element.querySelector("input") ||
element.querySelector("textarea") ||
element;
}
}
return element;
}, { index, findInput });
}
async go_back(agentContext) {
try {
await this.execute_script(agentContext, async () => {
await window.navigation.back().finished;
}, []);
await this.sleep(100);
}
catch (e) {
if (e?.name == "InvalidStateError" && this.lastSwitchedPage) {
this.activePage && (await this.activePage.close());
this.activePage = this.lastSwitchedPage;
this.lastSwitchedPage = null;
}
else {
throw new Error("Cannot go back");
}
}
}
sleep(time) {
return new Promise((resolve) => setTimeout(() => resolve(), time));
}
async getBrowserContext() {
if (!this.browserContext) {
this.activePage = null;
this.browserContext = null;
const viewport = this.options?.viewport || { width: 1536, height: 864 };
if (this.cdpWsEndpoint) {
this.browser = (await playwrightExtra.chromium.connectOverCDP(this.cdpWsEndpoint, this.options));
this.browserContext = await this.browser.newContext({
userAgent: this.getUserAgent(),
viewport: viewport,
});
}
else if (this.userDataDir) {
this.browserContext = (await playwrightExtra.chromium.launchPersistentContext(this.userDataDir, {
headless: this.headless,
channel: "chrome",
args: this.getChromiumArgs(),
viewport: viewport,
...this.options,
}));
}
else {
this.browser = (await playwrightExtra.chromium.launch({
headless: this.headless,
args: this.getChromiumArgs(),
...this.options,
}));
this.browserContext = await this.browser.newContext({
userAgent: this.getUserAgent(),
viewport: viewport,
});
}
// Anti-crawling detection website:
// https://bot.sannysoft.com/
// https://www.browserscan.net/
playwrightExtra.chromium.use(StealthPlugin__default.default());
const init_script = await this.initScript();
if (init_script.content || init_script.path) {
this.browserContext.addInitScript(init_script);
}
this.browserContext.on("page", async (page) => {
this.activePage = page;
this.pageMap.set(page, {
tabId: tabId++,
lastAccessed: Date.now(),
});
page.on("framenavigated", async (frame) => {
if (frame === page.mainFrame()) {
const url = frame.url();
if (url.startsWith("http")) {
await this.autoLoadCookies(url);
await this.autoLoadLocalStorage(page, url);
}
}
});
page.on("close", () => {
this.pageMap.delete(page);
});
});
}
if (this.cookies) {
this.browserContext?.addCookies(this.cookies);
}
return this.browserContext;
}
async autoLoadCookies(url) {
try {
const cookies = await this.loadCookiesWithUrl(url);
if (cookies && cookies.length > 0) {
await this.browserContext?.clearCookies();
await this.browserContext?.addCookies(cookies);
}
}
catch (e) {
eko.Log.error("Failed to auto load cookies: " + url, e);
}
}
async autoLoadLocalStorage(page, url) {
try {
const localStorageData = await this.loadLocalStorageWithUrl(url);
await page.addInitScript((storage) => {
try {
for (const [key, value] of Object.entries(storage)) {
localStorage.setItem(key, value);
}
}
catch (e) {
eko.Log.error("Failed to inject localStorage: " + url, e);
}
}, localStorageData);
}
catch (e) {
eko.Log.error("Failed to auto load localStorage: " + url, e);
}
}
async loadCookiesWithUrl(url) {
return [];
}
async loadLocalStorageWithUrl(url) {
return {};
}
getChromiumArgs() {
return [
"--no-sandbox",
"--remote-allow-origins=*",
"--disable-dev-shm-usage",
"--disable-popup-blocking",
"--ignore-ssl-errors",
"--ignore-certificate-errors",
"--ignore-certificate-errors-spki-list",
"--disable-blink-features=AutomationControlled",
"--disable-infobars",
"--disable-notifications",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
];
}
getUserAgent() {
// const userAgents = [
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
// ];
// return userAgents[Math.floor(Math.random() * userAgents.length)];
return undefined;
}
async initScript() {
return {};
}
async getBrowser() {
return this.browser;
}
async getActivePage() {
return this.activePage;
}
async close() {
if (this.browserContext) {
await this.browserContext.close();
}
if (this.browser) {
await this.browser.close();
}
}
}
class SimpleStdioMcpClient {
constructor(command, args, options) {
this.process = null;
this.command = command;
this.args = args || [];
this.options = options || {
stdio: ["pipe", "pipe", "pipe"],
};
this.requestMap = new Map();
}
async connect(signal) {
if (this.process) {
try {
this.process.kill();
}
catch (e) { }
}
this.process = child_process.spawn(this.command, this.args, this.options);
this.process.stdout.on("data", (data) => {
const response = data.toString().trim();
eko.Log.debug("MCP Client, onmessage", this.command, this.args, response);
if (!response.startsWith("{")) {
return;
}
const message = JSON.parse(response);
if (message.id) {
const callback = this.requestMap.get(message.id);
if (callback) {
callback(message);
}
}
});
this.process.on("error", (error) => {
eko.Log.error("MCP process error:", this.command, this.args, error);
});
eko.Log.info("MCP Client, connection successful:", this.command, this.args);
}
async listTools(param, signal) {
const message = await this.sendMessage("tools/list", {
...param,
}, signal);
return message.result.tools || [];
}
async callTool(param, signal) {
const message = await this.sendMessage("tools/call", {
...param,
}, signal);
return message.result;
}
async sendMessage(method, params = {}, signal) {
if (!this.process) {
await this.connect();
if (!this.process) {
throw new Error("Failed to connect to MCP server");
}
}
const id = eko.uuidv4();
try {
const callback = new Promise((resolve, reject) => {
if (signal) {
signal.addEventListener("abort", () => {
const error = new Error("Operation was interrupted");
error.name = "AbortError";
reject(error);
});
}
this.requestMap.set(id, resolve);
});
const message = JSON.stringify({
jsonrpc: "2.0",
id: id,
method: method,
params: {
...params,
},
});
eko.Log.debug(`MCP Client, ${method}`, id, params);
const suc = this.process.stdin.write(message + "\n", "utf-8");
if (!suc) {
throw new Error("SseClient Response Exception: " + message);
}
const messageData = await callback;
this.handleError(method, messageData);
return messageData;
}
finally {
this.requestMap.delete(id);
}
}
handleError(method, message) {
if (!message) {
throw new Error(`MCP ${method} error: no response`);
}
if (message?.error) {
eko.Log.error(`MCP ${method} error: ` + message.error);
throw new Error(`MCP ${method} error: ` +
(typeof message.error === "string"
? message.error
: message.error.message));
}
if (message.result?.isError == true) {
if (message.result.content) {
throw new Error(`MCP ${method} error: ` +
(typeof message.result.content === "string"
? message.result.content
: message.result.content[0].text));
}
else {
throw new Error(`MCP ${method} error: ` + JSON.stringify(message.result));
}
}
}
isConnected() {
return (this.process != null && !this.process.killed && !this.process.exitCode);
}
async close() {
this.process && this.process.kill();
}
}
exports.BrowserAgent = BrowserAgent;
exports.SimpleStdioMcpClient = SimpleStdioMcpClient;
exports.getCdpWsEndpoint = getCdpWsEndpoint;
//# sourceMappingURL=index.cjs.map