Browser Sub-Agent とは
Google Antigravity IDE の Browser Sub-Agent は、AI エージェントに「手」を与えるための機能です。従来の Web スクレイピングツールやテスト自動化ツールと異なり、自然言語命令を理解した AI が、ブラウザを通じて人間のように Web サイトを操作します。
具体的には以下が可能になります:
- Web スクレイピング: JavaScript レンダリングに対応した複雑なサイトからのデータ抽出
- フォーム自動入力: CAPTCHA 回避(自社サイトの場合)、多段階ログイン、複雑なチェックボックス選択
- E2E テスト: ユーザーシナリオに基づいた自動テスト実行・スクリーンショット取得
- 自動化ワークフロー: 日次業務の完全自動化(例: 競合商品の価格監視、定期レポート生成)
Browser Sub-Agent のアーキテクチャ
内部動作フロー
┌─────────────────────────────────────┐
│ AI Agent (自然言語命令受け取り) │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Intent Parser │
│ 命令を「アクション」に分解 │
│ - "ボタンをクリック" → click(xy) │
│ - "テキスト入力" → type(selector) │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ DOM Interpreter │
│ セレクター解析・要素位置計算 │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Chromium Browser Instance │
│ (Headless or Headful mode) │
│ - ページロード │
│ - インタラクション実行 │
│ - スクリーンショット取得 │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Output Processor │
│ - DOM スナップショット │
│ - スクリーンショット │
│ - テキスト抽出 │
└─────────────────────────────────────┘
重要な制限と対応策
| 制限事項 | 対応策 |
|---|---|
| JavaScript レンダリング遅延(3~5秒) | waitForTimeout・waitForSelector で待機 |
| ネットワーク速度に依存 | タイムアウト値を長めに設定(デフォルト 30秒) |
| CORS ポリシーによる API 呼び出し不可 | Server-side proxy を用意 |
| セッション保持(クッキー) | persistCookies: true で Cookie を保存 |
Web スクレイピング:実装例
基本パターン
// browser-scraper.ts
import { launch } from 'puppeteer';
interface ScrapedProduct {
title: string;
price: number;
rating: number;
url: string;
}
class EcommerceScraper {
private browser: any;
async initialize() {
this.browser = await launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled', // ロボット検出回避
],
});
}
async scrapeProductListing(
baseUrl: string,
pageCount: number = 3
): Promise<ScrapedProduct[]> {
const results: ScrapedProduct[] = [];
const page = await this.browser.newPage();
// User-Agent をスプーフィング
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
for (let i = 1; i <= pageCount; i++) {
const url = `${baseUrl}?page=${i}`;
console.log(`Scraping: ${url}`);
try {
// ページ読み込み待機
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
// 動的コンテンツの読み込み待機
await page.waitForSelector('.product-item', { timeout: 10000 });
// DOM から製品情報を抽出
const products = await page.evaluate(() => {
const items = document.querySelectorAll('.product-item');
return Array.from(items).map((item) => {
const titleEl = item.querySelector('.product-title');
const priceEl = item.querySelector('.product-price');
const ratingEl = item.querySelector('.product-rating');
const linkEl = item.querySelector('a.product-link');
return {
title: titleEl?.textContent?.trim() || 'N/A',
price: parseFloat(
priceEl?.textContent?.replace(/[^\d.]/g, '') || '0'
),
rating: parseFloat(ratingEl?.textContent || '0'),
url: linkEl?.getAttribute('href') || '',
};
});
});
results.push(...products);
console.log(`Page ${i}: ${products.length} products found`);
// レート制限回避: ページ間に遅延を入れる
await page.waitForTimeout(2000 + Math.random() * 3000);
} catch (error) {
console.error(`Error scraping page ${i}:`, error);
// フォールバック: AI に手動確認を依頼
}
}
await page.close();
return results;
}
async close() {
await this.browser.close();
}
}
// 使用例
async function main() {
const scraper = new EcommerceScraper();
await scraper.initialize();
const products = await scraper.scrapeProductListing(
'https://example-ecommerce.com/products',
3
);
console.log(`Total products scraped: ${products.length}`);
console.log(JSON.stringify(products, null, 2));
await scraper.close();
}
main().catch(console.error);レート制限への対応
class RateLimitedScraper {
private requestTimes: number[] = [];
private maxRequestsPerMinute: number = 10;
async ensureRateLimit() {
const now = Date.now();
const oneMinuteAgo = now - 60 * 1000;
// 1分以内のリクエストを数える
this.requestTimes = this.requestTimes.filter((t) => t > oneMinuteAgo);
if (this.requestTimes.length >= this.maxRequestsPerMinute) {
// レート制限に引っかかった場合、待機
const waitTime = 60 * 1000 - (now - this.requestTimes[0]);
console.log(`Rate limit reached. Waiting ${waitTime}ms...`);
await new Promise((r) => setTimeout(r, waitTime));
}
this.requestTimes.push(now);
}
async scrapePage(url: string) {
await this.ensureRateLimit();
// スクレイピング処理...
}
}出力例:
[
{
"title": "高性能 AI ノートパソコン",
"price": 1580,
"rating": 4.8,
"url": "https://example.com/product/ai-laptop-pro"
},
{
"title": "USB-C ドッキングステーション",
"price": 25800,
"rating": 4.5,
"url": "https://example.com/product/usb-c-dock"
}
]フォーム自動入力とログイン
複雑なチェックボックスと条件分岐
class FormFiller {
async fillComplexForm(page: any) {
// ステップ1: テキスト入力フィールドを埋める
await page.type('#name-input', 'Masaki Hirokawa');
await page.type('#email-input', 'user@example.com');
// ステップ2: ラジオボタン選択
await page.click('input[value="business"]');
// ステップ3: 条件分岐(選択内容に応じて出現するフィールド)
await page.waitForSelector('#company-name-field', { timeout: 5000 });
await page.type('#company-name-field', 'Dolice Labs');
// ステップ4: マルチセレクト(複数選択可能)
const tags = ['AI Development', 'Web Automation', 'Agent System'];
for (const tag of tags) {
// Cmd/Ctrl + click で複数選択
await page.click(`label:contains("${tag}")`, { modifiers: ['Control'] });
}
// ステップ5: ファイルアップロード
const uploadInput = await page.$('input[type="file"]');
await uploadInput.uploadFile('/path/to/resume.pdf');
// ステップ6: 利用規約に同意
await page.click('#terms-checkbox');
await page.click('#privacy-checkbox');
// ステップ7: 送信ボタンをクリック
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle0' }),
page.click('#submit-button'),
]);
console.log('Form submitted successfully');
}
}セッション保持とログイン状態の維持
class AuthenticatedSession {
private cookiesFile = './session-cookies.json';
async saveCookies(page: any) {
const cookies = await page.cookies();
require('fs').writeFileSync(this.cookiesFile, JSON.stringify(cookies, null, 2));
}
async restoreCookies(page: any) {
if (require('fs').existsSync(this.cookiesFile)) {
const cookies = JSON.parse(
require('fs').readFileSync(this.cookiesFile, 'utf8')
);
await page.setCookie(...cookies);
console.log('Cookies restored from previous session');
}
}
async login(page: any, email: string, password: string) {
await page.goto('https://example.com/login');
// ログイン処理
await page.type('#email', email);
await page.type('#password', password);
await page.click('button[type="submit"]');
// ログイン完了待機
await page.waitForNavigation({ waitUntil: 'networkidle0' });
// クッキーを保存(次回以降の復元用)
await this.saveCookies(page);
}
async accessProtectedPage(url: string) {
const browser = await launch();
const page = await browser.newPage();
// 前回のセッションを復元
await this.restoreCookies(page);
await page.goto(url);
return await page.content();
}
}E2E テスト自動化
ユーザーシナリオベースのテスト
class E2ETestRunner {
async testCheckoutFlow(baseUrl: string) {
const browser = await launch();
const page = await browser.newPage();
try {
// シナリオ1: 商品検索
console.log('Test 1: Search for product');
await page.goto(`${baseUrl}/`);
await page.type('#search-input', 'AI notebook');
await page.click('#search-button');
await page.waitForSelector('.product-grid', { timeout: 10000 });
// シナリオ2: 商品詳細ページへ移動
console.log('Test 2: Navigate to product detail');
await page.click('a.product-link:first-child');
await page.waitForSelector('.product-detail', { timeout: 5000 });
// シナリオ3: カートに追加
console.log('Test 3: Add to cart');
await page.click('#add-to-cart-button');
const cartBadge = await page.$eval(
'.cart-count',
(el) => el.textContent
);
console.assert(cartBadge === '1', 'Cart should have 1 item');
// シナリオ4: チェックアウト
console.log('Test 4: Proceed to checkout');
await page.click('a[href="/checkout"]');
await page.waitForSelector('#shipping-form', { timeout: 5000 });
// シナリオ5: 配送情報入力
console.log('Test 5: Fill shipping info');
await page.type('#address', '123 Tech Street, San Francisco');
await page.select('#country', 'US');
// シナリオ6: 決済情報入力(本番では Stripe テストキーを使用)
console.log('Test 6: Fill payment info');
// iframe 内の Stripe フォーム
const stripeFrame = page.frames().find((f) =>
f.url().includes('stripe.com')
);
if (stripeFrame) {
await stripeFrame.type('[placeholder="Card number"]', 'YOUR_TEST_CARD_NUM');
}
// シナリオ7: 注文完了
console.log('Test 7: Complete order');
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle0' }),
page.click('#place-order-button'),
]);
// 確認ページのスクリーンショット
const screenshot = await page.screenshot({
path: './test-results/order-confirmation.png',
});
console.log('✓ Checkout flow completed successfully');
return true;
} catch (error) {
console.error('✗ Test failed:', error);
await page.screenshot({ path: './test-results/error-screenshot.png' });
return false;
} finally {
await browser.close();
}
}
}
// テスト実行
const runner = new E2ETestRunner();
runner.testCheckoutFlow('https://example-ecommerce.com')
.then((success) => process.exit(success ? 0 : 1));本番環境での セキュリティ設計
認証情報の安全な管理
class SecureAutomation {
private credentialsManager: CredentialsManager;
async runSecurelyWithCredentials(task: string) {
// 認証情報を環境変数または Vault から取得
const credentials = await this.credentialsManager.get(task);
// メモリ内暗号化
const decrypted = await this.credentialsManager.decrypt(credentials);
// 使用後は即座にメモリをクリア
try {
return await this.executeTask(task, decrypted);
} finally {
// セキュアクリア(オーバーライト)
decrypted.password = Buffer.alloc(decrypted.password.length, '\0');
}
}
}IP ローテーションとプロキシ
class ProxyRotatingBrowser {
private proxyList = [
'http://proxy1.example.com:8080',
'http://proxy2.example.com:8080',
'http://proxy3.example.com:8080',
];
async launchWithProxy(proxyIndex: number = 0) {
const proxy = this.proxyList[proxyIndex % this.proxyList.length];
return await launch({
args: [`--proxy-server=${proxy}`],
});
}
async scrapeWithRotation(urls: string[]) {
for (let i = 0; i < urls.length; i++) {
const browser = await this.launchWithProxy(i);
const page = await browser.newPage();
try {
await page.goto(urls[i]);
// スクレイピング処理...
} finally {
await browser.close();
}
}
}
}リクエストヘッダースプーフィング
async function setupRealisticHeaders(page: any) {
// 人間らしいリクエストヘッダーを設定
await page.setExtraHTTPHeaders({
'Accept-Language': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Cache-Control': 'max-age=0',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
});
// JavaScript による ブラウザ自動化検出回避
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5], // ダミープラグイン
});
// その他のチェック回避...
});
}ベストプラクティス
-
エラーハンドリングは徹底的に
- ネットワーク障害・タイムアウト・CAPTCHA への対応を実装
- フェイルオーバー戦略を用意(リトライ・ログと通知)
-
ページ読み込み待機を適切に
waitForNavigation(): ページ遷移時waitForSelector(): 動的コンテンツ読み込み時waitForTimeout(): は最後の手段(できるだけ避ける)
-
メモリリークを防ぐ
- ブラウザインスタンスを適切にクローズ
- 大量の Cookie/ローカルストレージをクリア
-
ログを活用する
- 各ステップでログを記録
- スクリーンショットを取得(失敗時は必須)
個人開発12年の現場で実感したこと
線引きするときの3つの判断軸
- 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
- 復旧オペレーションが明文化されているか
- 観測ログから人間が再現できる粒度に整っているか
個人開発12年とアーティスト活動から見るエージェント設計
線引きするときの3つの判断軸
- 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
- 復旧オペレーションが明文化されているか
- 観測ログから人間が再現できる粒度に整っているか
まとめ — 検証ステップの推奨
- 観測メトリクスとアラートを設置
- 限定ロールアウトで本番検証