月初她得一家一家登入廠商的後台網站,把發票號碼、金額、是否已付款抄進試算表,一次要花掉大半個下午
Stagehand 是一套專門讓 AI agent能自己看情況、決定下一步並動手做事的 AI 程式 操作網頁的 SDK軟體開發套件,別人先寫好、讓你直接呼叫的工具箱。你用一句白話(素材的範例是英文句子)告訴它「找出 email 輸入框」「點登入按鈕」「把表格裡的發票全部抓出來」,它就替你操作瀏覽器完成。它目前有 TypeScript、Python、Go 三種語言版本。
它的對照組是 Playwright。Playwright 是一套 瀏覽器自動化用程式代替人手去點擊、輸入、翻頁的技術 工具,出發點是「測試網站」,測試人員會事先寫死每個按鈕的位置。Stagehand 的自我介紹很直接:Playwright 為測試而生,Stagehand 為 agent 而生。這則 Hacker News 熱門文章的標題則宣稱:在 agent 的使用情境下,速度快 2 倍,tokenAI 模型計費與運算的文字單位,大約一個字或半個詞,送得越多越貴越慢 用量少 80%。
它的核心只有三個動詞。observe() 回答「這個東西在頁面的哪裡」,回傳的是真正的 選擇器selector,網頁元素的定位地址,例如某個輸入框的定位字串;act() 負責「做一個動作」,例如點按鈕、打開帳單頁;extract() 負責「把資料撈出來」,並且用 schema資料格式的規格表,規定每個欄位的名稱與型別,例如 amount 必須是數字 驗收,格式不對就不放行。
登入這件事也被設計進去了。設定 userDataDir 後,cookies網站存在瀏覽器裡的小紀錄,用來記住你已經登入 會留在你電腦的資料夾裡,下次啟動一開始就是登入狀態。密碼則由你自己的程式從 環境變數作業系統替程式保管的設定值,不寫在程式檔裡,適合放金鑰與密碼 讀出來親手填進去,素材特別強調:因為 observe() 只回傳欄位的位置,帳號密碼不會送到 AI 模型那邊。
| 面向 | 傳統 Playwright 寫法 | Stagehand 寫法 |
|---|---|---|
| 設計初衷 | 為測試網站而生 | 為 AI agent 而生,提供 TypeScript/Python/Go |
| 找到元素 | 由人手寫選擇器 | 用一句話描述,observe() 回傳真的選擇器 |
| 網站改版 | 手寫的選擇器一改版就可能失效 | act() 標榜會自我修復 |
| 取得資料 | 自己寫程式解析頁面 | extract() 搭配 schema,回傳驗證過的資料 |
| 登入機密 | 由程式填入 | 同樣由程式填入;observe() 只回傳位置,密碼不進模型 |
這是素材 README 裡「登入一次、保存登入狀態、再把資料抓出來」的 TypeScript 範例(略作精簡)。請特別留意哪幾行交給 AI、哪幾行由你自己的程式精準執行。
import { localBrowser, Stagehand } from '@browserbasehq/stagehand';import { z } from 'zod/v4';const browser = await localBrowser.launch({ userDataDir: './browser-data' });const stagehand = await Stagehand.create({ browser, model: { modelName: 'openai/gpt-5.4-mini', apiKey: process.env.OPENAI_API_KEY },});const [page] = await browser.context.pages();await page.goto('https://app.example.com/login');const { data: email } = await stagehand.observe('find the email input');const { data: password } = await stagehand.observe('find the password input');await page.locator(email[0].selector).fill(process.env.APP_EMAIL);await page.locator(password[0].selector).fill(process.env.APP_PASSWORD);await stagehand.act('click the sign in button');await stagehand.act('open the billing page');const { data } = await stagehand.extract( 'extract every invoice in the table', z.object({ invoices: z.array(z.object({ number: z.string(), amount: z.number(), paid: z.boolean() })) }),);console.log(data.invoices);這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。
{
"name": "stagehand-lab",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@browserbasehq/stagehand": "latest",
"zod": "latest"
}
}
import { localBrowser, Stagehand } from '@browserbasehq/stagehand';
import { z } from 'zod/v4';
// 為什麼先定義 schema:它規定 AI 要回傳的欄位與型別,拿回來的資料也靠它驗收
const Stories = z.object({
stories: z.array(z.object({ title: z.string(), points: z.number() })),
});
if (!process.env.OPENAI_API_KEY) {
console.error('找不到 OPENAI_API_KEY:請先在本視窗設定環境變數後再執行');
process.exit(1);
}
// userDataDir 讓 cookies 留在本機資料夾,下次執行不必重新登入
const browser = await localBrowser.launch({ userDataDir: './browser-data' });
try {
const stagehand = await Stagehand.create({
browser,
model: { modelName: 'openai/gpt-5.4-mini', apiKey: process.env.OPENAI_API_KEY },
});
try {
const [page] = await browser.context.pages();
await page.goto('https://news.ycombinator.com/');
const { data } = await stagehand.extract('extract the top 5 stories on the front page', Stories);
data.stories.forEach((s, i) => console.log(`${i + 1}. ${s.title}(${s.points} 分)`));
} finally {
await stagehand.close();
}
} finally {
await browser.close();
}
import { localBrowser, Stagehand } from '@browserbasehq/stagehand';
// 機密只從環境變數讀,不寫進程式碼,也不放進任何交給模型的指令字串
const { OPENAI_API_KEY, LOGIN_URL, APP_EMAIL, APP_PASSWORD } = process.env;
if (!OPENAI_API_KEY || !LOGIN_URL || !APP_EMAIL || !APP_PASSWORD) {
console.error('缺少環境變數:需要 OPENAI_API_KEY、LOGIN_URL、APP_EMAIL、APP_PASSWORD');
process.exit(1);
}
const browser = await localBrowser.launch({ userDataDir: './browser-data' });
try {
const stagehand = await Stagehand.create({
browser,
model: { modelName: 'openai/gpt-5.4-mini', apiKey: OPENAI_API_KEY },
});
try {
const [page] = await browser.context.pages();
await page.goto(LOGIN_URL);
// observe 只回傳「輸入框在哪」(選擇器),模型看不到我們接著要填的內容
const { data: email } = await stagehand.observe('find the email input');
const { data: password } = await stagehand.observe('find the password input');
if (email.length === 0 || password.length === 0) {
console.log('找不到登入輸入框:可能上次的登入狀態被保留在 browser-data,或這個網址不是登入頁。');
} else {
await page.locator(email[0].selector).fill(APP_EMAIL);
await page.locator(password[0].selector).fill(APP_PASSWORD);
await stagehand.act('click the sign in button');
console.log('登入動作已送出,登入狀態會保存在 ./browser-data');
}
} finally {
await stagehand.close();
}
} finally {
await browser.close();
}
# browser-data 裡是登入用的 cookies,等同你的登入憑證,絕對不能上傳到版本庫
node_modules/
browser-data/
.env