mirror of https://github.com/kortix-ai/suna.git
321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import express from 'express';
|
|
import { Stagehand, type LogLine, type Page } from '@browserbasehq/stagehand';
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
interface BrowserActionResult {
|
|
success: boolean;
|
|
message: string;
|
|
error?: string;
|
|
url: string;
|
|
title: string;
|
|
screenshot_base64?: string;
|
|
action?: string;
|
|
}
|
|
|
|
class BrowserAutomation {
|
|
public router: express.Router;
|
|
|
|
private stagehand: Stagehand | null;
|
|
public browserInitialized: boolean;
|
|
private currentApiKey: string | null;
|
|
private page: Page | null;
|
|
constructor() {
|
|
this.router = express.Router();
|
|
this.browserInitialized = false;
|
|
this.currentApiKey = null;
|
|
this.stagehand = null;
|
|
this.page = null;
|
|
|
|
this.router.post('/navigate', this.navigate.bind(this));
|
|
this.router.post('/screenshot', this.screenshot.bind(this));
|
|
this.router.post('/act', this.act.bind(this));
|
|
this.router.post('/extract', this.extract.bind(this));
|
|
|
|
}
|
|
|
|
async init(apiKey: string): Promise<{status: string, message: string}> {
|
|
try{
|
|
if (!this.browserInitialized) {
|
|
this.currentApiKey = apiKey;
|
|
console.log("Initializing browser with api key");
|
|
this.stagehand = new Stagehand({
|
|
env: "LOCAL",
|
|
enableCaching: true,
|
|
verbose: 2,
|
|
logger: (logLine: LogLine) => {
|
|
console.log(`[${logLine.category}] ${logLine.message}`);
|
|
},
|
|
modelName: "claude-3-7-sonnet-20250219",
|
|
modelClientOptions: {
|
|
apiKey
|
|
},
|
|
localBrowserLaunchOptions: {
|
|
headless: false,
|
|
viewport: {
|
|
width: 1024,
|
|
height: 768
|
|
},
|
|
downloadsPath: '/workspace/downloads',
|
|
acceptDownloads: true,
|
|
preserveUserDataDir: true,
|
|
args: [
|
|
"--remote-debugging-port=9222",
|
|
"--no-sandbox",
|
|
"--disable-setuid-sandbox",
|
|
"--disable-dev-shm-usage",
|
|
"--disable-gpu"
|
|
]
|
|
}
|
|
});
|
|
await this.stagehand.init();
|
|
this.browserInitialized = true;
|
|
this.page = this.stagehand.page;
|
|
|
|
// Attach listeners to detect browser or page crashes and reset state accordingly
|
|
if (this.page) {
|
|
// If the browser page itself closes we mark the automation as un-initialised
|
|
this.page.on('close', () => {
|
|
console.log('Browser page closed - resetting state');
|
|
this.browserInitialized = false;
|
|
});
|
|
|
|
// If the underlying browser disconnects (e.g. crashes) we also reset state
|
|
try {
|
|
const browserInstance = this.page.context().browser();
|
|
browserInstance?.on('disconnected', () => {
|
|
console.log('Browser disconnected - resetting state');
|
|
this.browserInitialized = false;
|
|
});
|
|
} catch (err) {
|
|
console.error('Failed to attach browser disconnect handler', err);
|
|
}
|
|
}
|
|
|
|
await this.page.goto('https://www.google.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
return {
|
|
status: "initialized",
|
|
message: "Browser initialized"
|
|
}
|
|
}
|
|
return {
|
|
status: "healthy",
|
|
message: "Browser already initialized"
|
|
}
|
|
} catch (error) {
|
|
console.error("Error initializing browser", error);
|
|
return {
|
|
status: "error",
|
|
message: "Failed to initialize browser"
|
|
}
|
|
}
|
|
}
|
|
|
|
health(): {status: string} {
|
|
if (this.browserInitialized && this.page && !this.page.isClosed()) {
|
|
return {
|
|
status: "healthy"
|
|
}
|
|
}
|
|
return {
|
|
status: "unhealthy"
|
|
}
|
|
}
|
|
|
|
async shutdown() {
|
|
console.log("Shutting down browser");
|
|
this.browserInitialized = false;
|
|
this.stagehand?.close();
|
|
this.stagehand = null;
|
|
this.page = null;
|
|
this.currentApiKey = null;
|
|
return {
|
|
status: "shutdown",
|
|
message: "Browser shutdown"
|
|
}
|
|
}
|
|
|
|
async get_stagehand_state() {
|
|
try{
|
|
const health = this.health();
|
|
if (this.page && health.status === "healthy") {
|
|
const screenshot_base64 = await this.page.screenshot({ fullPage: false }).then(buffer => buffer.toString('base64'));
|
|
const page_info = {
|
|
url: await this.page.url(),
|
|
title: await this.page.title(),
|
|
screenshot_base64: screenshot_base64,
|
|
};
|
|
return page_info;
|
|
}
|
|
return {
|
|
url: "",
|
|
title: "",
|
|
screenshot_base64: "",
|
|
}
|
|
} catch (error) {
|
|
console.error("Error capturing stagehand state", error);
|
|
return {
|
|
url: "",
|
|
title: "",
|
|
screenshot_base64: "",
|
|
}
|
|
}
|
|
}
|
|
|
|
async navigate(req: express.Request, res: express.Response): Promise<void> {
|
|
try {
|
|
if (this.page && this.browserInitialized) {
|
|
const { url } = req.body;
|
|
await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
const page_info = await this.get_stagehand_state();
|
|
const result: BrowserActionResult = {
|
|
success: true,
|
|
message: "Navigated to " + url,
|
|
error: "",
|
|
url: page_info.url,
|
|
title: page_info.title,
|
|
screenshot_base64: page_info?.screenshot_base64,
|
|
}
|
|
res.json(result);
|
|
} else {
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Browser not initialized"
|
|
})
|
|
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Failed to navigate to " + req.body.url
|
|
})
|
|
}
|
|
}
|
|
|
|
async screenshot(req: express.Request, res: express.Response): Promise<void> {
|
|
try {
|
|
if (this.page && this.browserInitialized) {
|
|
const page_info = await this.get_stagehand_state();
|
|
const result: BrowserActionResult = {
|
|
success: true,
|
|
message: "Screenshot taken",
|
|
url: page_info.url,
|
|
title: page_info.title,
|
|
screenshot_base64: page_info.screenshot_base64,
|
|
}
|
|
res.json(result);
|
|
} else {
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Browser not initialized"
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Failed to take screenshot"
|
|
})
|
|
}
|
|
}
|
|
|
|
async act(req: express.Request, res: express.Response): Promise<void> {
|
|
try {
|
|
if (this.page && this.browserInitialized) {
|
|
const { action, iframes, variables } = req.body;
|
|
const result = await this.page.act({action, iframes: iframes || true, variables});
|
|
const page_info = await this.get_stagehand_state();
|
|
const response: BrowserActionResult = {
|
|
success: result.success,
|
|
message: result.message,
|
|
action: result.action,
|
|
url: page_info.url,
|
|
title: page_info.title,
|
|
screenshot_base64: page_info.screenshot_base64,
|
|
}
|
|
res.json(response);
|
|
} else {
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Browser not initialized"
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Failed to act"
|
|
})
|
|
}
|
|
}
|
|
|
|
async extract(req: express.Request, res: express.Response): Promise<void> {
|
|
try {
|
|
if (this.page && this.browserInitialized) {
|
|
const { instruction, iframes, selector } = req.body;
|
|
const result = await this.page.extract({ instruction, iframes, selector });
|
|
const page_info = await this.get_stagehand_state();
|
|
const response: BrowserActionResult = {
|
|
success: result.success,
|
|
message: result.message,
|
|
action: result.action,
|
|
url: page_info.url,
|
|
title: page_info.title,
|
|
screenshot_base64: page_info.screenshot_base64,
|
|
}
|
|
res.json(response);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": "Failed to extract"
|
|
})
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const browserAutomation = new BrowserAutomation();
|
|
|
|
app.use('/api', browserAutomation.router);
|
|
|
|
app.get('/api', (req, res) => {
|
|
console.log("Health check");
|
|
const health = browserAutomation.health();
|
|
if (health.status === "healthy") {
|
|
res.status(200).json({
|
|
"status": "healthy",
|
|
"service": "browserApi"
|
|
})
|
|
} else {
|
|
res.status(500).json({
|
|
"status": "unhealthy",
|
|
"service": "browserApi"
|
|
})
|
|
}
|
|
});
|
|
|
|
app.post('/api/init', async (req, res) => {
|
|
console.log("Initializing browser");
|
|
const {api_key} = req.body;
|
|
const result = await browserAutomation.init(api_key);
|
|
|
|
if (result.status === "initialized") {
|
|
res.status(200).json({
|
|
"status": "healthy",
|
|
"service": "browserApi"
|
|
})
|
|
} else {
|
|
res.status(500).json({
|
|
"status": "error",
|
|
"message": result.message
|
|
})
|
|
}
|
|
});
|
|
|
|
app.listen(8004, () => {
|
|
console.log('Starting browser server on port 8004');
|
|
}); |