Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pnpm run build
pnpm run test

# Lint code
pnpm run lint
pnpm run format
```

<br />
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pnpm run build
pnpm run test

# 代码检查
pnpm run lint
pnpm run format
```

<br />
Expand Down
27 changes: 27 additions & 0 deletions packages/browser-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,33 @@ BrowserUI.create({
});
```

Or use the unpkg CDN to use it on any webpage:

- **CDN URL**: https://unpkg.com/@agent-infra/browser/dist/bundle/index.js

```html
<!doctype html>
<html lang="en">
<body>
<div id="browserContainer"></div>
<script src="https://unpkg.com/@agent-infra/browser/dist/bundle/index.js"></script>
<script>
const BrowserUI = window.agent_infra_browser_ui.BrowserUI;

BrowserUI.create({
root: document.getElementById('browserContainer'),
browserOptions: {
connect: {
// @ts-ignore
browserWSEndpoint: 'https://example.com/ws/url',
},
},
});
</script>
</body>
</html>
```

A complete usable example, which can be run directly with `npm run dev` in the current directory or viewed in the `/examples` directory within the package.

## Features
Expand Down
29 changes: 29 additions & 0 deletions packages/browser-ui/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,37 @@ BrowserUI.create({
});
```

或者直接使用 unpkg CDN 在任意网页中使用:

- **CDN URL**: https://unpkg.com/@agent-infra/browser/dist/bundle/index.js

```html
<!doctype html>
<html lang="en">
<body>
<div id="browserContainer"></div>
<script src="https://unpkg.com/@agent-infra/browser/dist/bundle/index.js"></script>
<script>
const BrowserUI = window.agent_infra_browser_ui.BrowserUI;

BrowserUI.create({
root: document.getElementById('browserContainer'),
browserOptions: {
connect: {
// @ts-ignore
browserWSEndpoint: 'https://example.com/ws/url',
},
},
});
</script>
</body>
</html>
```

完整的可用示例,可直接在当前目录下运行 `npm run dev` 或查看包中的 `/examples` 目录。

<br />

## 核心功能

关于所有功能的详细文档和 API 参考,请访问我们的[完整文档](https://github.com/agent-infra/browser/blob/main/docs/browser-ui.zh-CN.md)。
Expand Down
125 changes: 125 additions & 0 deletions packages/browser-ui/examples/bundle/boot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
* SPDX-License-Identifier: Apache-2.0
*/
import puppeteer from 'puppeteer-core';
import { createServer } from 'http';
import { readFile, stat } from 'fs/promises';
import { URL, fileURLToPath } from 'url';
import { join, dirname } from 'path';

async function main() {
console.log('🚀 launch ...');

const browser = await puppeteer.launch({
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
headless: false,
defaultViewport: {
width: 900,
height: 900,
deviceScaleFactor: 0,
},
args: [
'--mute-audio',
'--no-default-browser-check',
'--window-size=900,990',
'--remote-allow-origins=http://127.0.0.1:3000',
'https://www.bytedance.com/en/',
],
ignoreDefaultArgs: ['--enable-automation'],
});

const wsEndpoint = browser.wsEndpoint();

console.log('✅ launched successfully!');
console.log('📡 CDP WebSocket:', wsEndpoint);

const port = 3000;
const server = createServer(async (req, res) => {
try {
console.log(`Request: ${req.method} ${req.url}`);

if (req.url === '/' || req.url === '/index.html') {
const indexPath = new URL('./index.html', import.meta.url).pathname;
console.log(`Reading file: ${indexPath}`);

let html = await readFile(indexPath, 'utf-8');
console.log('File read successfully');

// Replace the import.meta.WSEndpoint with the actual wsEndpoint
html = html.replace(
'import.meta.WSEndpoint',
JSON.stringify(wsEndpoint),
);

console.log('HTML length:', html.length);

res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
} else if (req.url?.startsWith('/dist/')) {
// Handle static files from /dist/ directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const staticPath = join(
__dirname,
'..',
'..',
'dist',
req.url.replace('/dist/', ''),
);

console.log(`Static file request: ${staticPath}`);

try {
const fileStat = await stat(staticPath);
if (fileStat.isFile()) {
const content = await readFile(staticPath);
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(content);
return;
}
} catch (staticError) {
console.error('Static file not found:', staticError);
}

res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Static file not found');
} else {
console.log('404 for:', req.url);
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
} catch (error) {
console.error('Server error:', error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Internal Server Error: ${(error as Error).message}`);
}
});

server.listen(port, () => {
console.log(`🌐 Server running at http://localhost:${port}`);
console.log('🔄 Ctrl+C to exit...\n');
});

const cleanup = async () => {
try {
console.log('\n🛑 closing server and browser...');
if (server) {
server.close();
}
if (browser) {
await browser.close();
}
} catch (e) {
// ignore
} finally {
process.exit(0);
}
};

process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}

main().catch(console.error);
47 changes: 47 additions & 0 deletions packages/browser-ui/examples/bundle/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CanvasBrowser Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>
</head>

<body>
<div id="browserContainer">
<!-- Browser UI will be automatically mounted here -->
</div>

<script src="../dist/bundle/index.js"></script>
<script>
const container = document.getElementById('browserContainer');
if (!container) {
throw new Error('Browser container element not found');
}

const BrowserUI = window.agent_infra_browser_ui.BrowserUI;

BrowserUI.create({
root: container,
browserOptions: {
connect: {
// @ts-ignore
browserWSEndpoint: import.meta.WSEndpoint,
defaultViewport: {
width: 900,
height: 900,
},
},
searchEngine: 'baidu',
},
});
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions packages/browser-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
],
"scripts": {
"prepublishOnly": "pnpm run build",
"build": "rslib build",
"dev": "tsx examples/boot.ts",
"build": "rslib build && rslib build --config rslib.config.bundle.ts",
"dev": "tsx examples/core/boot.ts",
"test": "vitest run"
},
"dependencies": {
Expand Down
46 changes: 46 additions & 0 deletions packages/browser-ui/rslib.config.bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
* SPDX-License-Identifier: Apache-2.0
*/
// import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';

const BANNER = `/**
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
* SPDX-License-Identifier: Apache-2.0
*/`;

export default defineConfig({
source: {
entry: {
index: 'src/index.ts',
},
decorators: {
version: 'legacy',
},
},
lib: [
{
format: 'umd',
syntax: 'es2021',
bundle: true,
dts: false,
banner: { js: BANNER },
umdName: 'agent_infra_browser_ui',
output: {
minify: true,
externals: ['chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js'],
distPath: {
root: 'dist/bundle/',
},
},
},
],
// performance: {
// bundleAnalyze: {},
// },
output: {
target: 'web',
sourceMap: true,
},
});
1 change: 0 additions & 1 deletion packages/browser-ui/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ export default defineConfig({
target: 'web',
cleanDistPath: true,
sourceMap: true,
externals: ['chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js'],
},
});
18 changes: 15 additions & 3 deletions packages/browser/src/actions/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export class Keyboard {
const formattedHotkey = this.#formatHotkey(key);

if (this.#env.osName === 'macOS' && this.#env.browserName === 'Chrome') {
const success = await this.#macOSCDPHotKey(formattedHotkey, options);
const success = await this.#macOSCDPHotKey(
formattedHotkey,
options,
true,
);
if (success) {
return { success: true };
}
Expand Down Expand Up @@ -78,7 +82,11 @@ export class Keyboard {
const formattedHotkey = this.#formatHotkey(key);

if (this.#env.osName === 'macOS' && this.#env.browserName === 'Chrome') {
const success = await this.#macOSCDPHotKey(formattedHotkey, options);
const success = await this.#macOSCDPHotKey(
formattedHotkey,
options,
false,
);
if (success) {
return { success: true };
}
Expand Down Expand Up @@ -163,6 +171,7 @@ export class Keyboard {
async #macOSCDPHotKey(
keys: KeyInput[],
options: Readonly<KeyboardOptions>,
isPress: boolean,
): Promise<boolean> {
const hotkey = keys
.map((key) => {
Expand All @@ -180,7 +189,10 @@ export class Keyboard {
commands: [command.commands],
});
await delay(options.delay ?? 0);
await this.#page.keyboard.up(command.key);

if (isPress) {
await this.#page.keyboard.up(command.key);
}

return true;
}
Expand Down