项目地址:https://github.com/ntegrals/openbrowser
技术栈:TypeScript + Playwright + Vercel AI SDK + Bun
许可证:MIT | 版本:v1.0+ (Production-ready)
最近更新:2026-03-31
一、项目介绍
你给 AI 一个浏览器,它帮你点击、输入、导航、提取数据------全程自主完成任意网站上的任务。这就是 OpenBrowser 的核心理念。
一句话概括:基于 Playwright 构建的、支持多模型的 AI 自主网页浏览框架。
它和传统的 Selenium 脚本、Playwright 录制器有本质区别:
| 维度 | 传统浏览器自动化 | OpenBrowser |
|---|---|---|
| 任务定义 | 硬编码选择器 + 脚本步骤 | 自然语言描述任务 |
| 适应性 | 页面结构变了就挂掉 | AI 实时分析 DOM,动态决策 |
| 模型 | 无 | OpenAI / Anthropic / Google |
| 错误处理 | 脚本中断 | LLM 诊断 + 自动重试 + 错误分类 |
| 成本感知 | 无 | Token 级成本追踪 |
| 循环检测 | 无 | 多维指纹 + 升级提醒 |
项目采用 Monorepo 结构,三个包各司其职:
packages/
├── core/ # 核心库 (open-browser)
│ └── src/
│ ├── agent/ # Agent 逻辑、对话管理、停滞检测、结果评估
│ ├── commands/ # 25+ 命令的 Schema 和执行器
│ ├── viewport/ # 浏览器控制、事件总线、10 个守卫
│ ├── page/ # DOM 分析、内容提取
│ ├── model/ # LLM 适配器、消息格式化
│ ├── metering/ # 成本追踪
│ ├── bridge/ # IPC 服务端/客户端
│ └── config/ # 配置类型
├── cli/ # 命令行 (@open-browser/cli)
└── sandbox/ # 沙盒 (@open-browser/sandbox)
二、整体架构
先看数据流,理解全局再钻细节:
┌─────────────┐
"帮我查 MacBook 价格" │ │
───────────────► │ Agent │ ◄── LLM (OpenAI / Anthropic / Google)
│ │
└──────┬──────┘
│ ① 发送页面状态 + 任务描述
┌──────▼──────┐
│ LLM 决策 │ 返回 JSON: { currentState, actions[] }
└──────┬──────┘
│ ② 解析命令列表
┌──────▼──────┐
│ Commands │ click, type, scroll, extract, navigate...
└──────┬──────┘
│ ③ 执行命令
┌──────▼──────┐
│ Viewport │ Playwright 浏览器实例 + CDP 会话
└──────┬──────┘
│ ④ 观察结果
┌──────▼──────┐
│ DOM / Page │ 快照、可交互元素列表、截图
└─────────────┘
│
⑤ 循环检测 → ⑥ 成本追踪 → 回到 ①
核心循环非常清晰:感知 → 决策 → 执行 → 观察 → 再决策。这是经典的 Agent Loop 模式,和 ReAct(Reasoning + Acting)范式一脉相承。
三、核心原理深度拆解
3.1 Agent 主循环 ------ run() 方法
Agent 类的核心是 run() 方法,这是整个框架的心脏。我把它拆成五个关键阶段:
typescript
async run(stepLimit?: number): Promise<RunOutcome> {
// 阶段 1:初始化
await this.browser.start(); // 启动浏览器
this.rebuildInstructionBuilder(); // 构建系统提示词
await this.autoNavigateFromTask(); // 从任务文本提取 URL 自动导航
await this.executeInitialActions(); // 执行预配置命令
// 阶段 2:主循环
for (let step = 1; step <= effectiveMaxSteps; step++) {
// 暂停支持
while (this.state.isPaused) await sleep(100);
try {
const result = await this.executeStep(step, effectiveMaxSteps);
// 检查是否完成
const doneResult = result.find(r => r.isDone);
if (doneResult) {
// Simple Judge 快速验证
if (this.settings.enableSimpleJudge && this.judge) {
const quickCheck = await this.judge.simpleEvaluate(task, finalResult);
if (quickCheck.shouldRetry) continue; // 结果不够好,继续
}
break;
}
// 阶段 3:计划更新(周期性)
if (this.shouldUpdatePlan(step)) await this.updatePlan(step);
// 阶段 4:停滞时重新规划
if (this.settings.restrategizeOnStall) {
const loopCheck = this.loopDetector.isStuck();
if (loopCheck.stuck && loopCheck.severity >= 2) {
await this.updatePlan(step); // 卡住了,重新规划
}
}
// 阶段 5:对话压缩(防 context 爆炸)
if (this.messageManager.shouldCompactWithLlm()) {
await this.messageManager.compactWithLlm(this.model);
}
} catch (error) {
if (error instanceof ModelThrottledError) {
// 指数退避重试
const waitMs = error.retryAfterMs ??
Math.min(60_000, this.settings.retryDelay * 1000 * 2 ** this.state.consecutiveFailures);
await sleep(waitMs);
continue; // 限流不计入失败次数
}
this.state.consecutiveFailures++;
if (this.state.consecutiveFailures >= this.settings.failureThreshold) {
// 最后一次 LLM 调用:诊断失败原因
const diagnosis = await this.makeFailureRecoveryCall(errors);
throw new AgentError(`Too many consecutive failures`);
}
}
}
}
五个关键设计决策:
-
Simple Judge 机制:任务"完成"不代表做对了。Agent 完成后会用另一个 LLM 调用做快速验证,不达标就继续。这避免了"表面完成但实际错误"的问题。
-
限流不计入失败 :API 限流是临时问题,不该和逻辑错误混为一谈。指数退避后直接
continue,不增加consecutiveFailures。 -
动态重规划 :检测到停滞时自动触发
updatePlan(),用 LLM 重新审视策略,而不是傻循环。 -
对话压缩:随着步数增加,对话历史会膨胀到爆 context。通过 LLM 压缩历史消息,保留关键信息。
-
失败诊断:连续失败到达阈值时,不是直接抛错退出,而是让 LLM 做最后一次诊断调用,给出"出了什么问题"和"可以怎么试"。
3.2 单步执行 ------ executeStep()
每一步的执行流程是一个完整的感知-决策-执行闭环:
typescript
private async executeStep(step: number, stepLimit: number): Promise<CommandResult[]> {
const timer = new Timer();
// ── 感知层 ──
// 1. 获取浏览器状态
const browserState = await this.browser.getState();
// 2. 动态命令 Schema:按当前 URL 重建系统提示
if (this.settings.dynamicCommandSchema) {
this.rebuildInstructionBuilder(browserState.url);
}
// 3. 提取 DOM(通过 CDP 协议获取可访问性树)
const domState = await this.domService.extractState(
this.browser.currentPage, this.browser.cdp!
);
// 4. 截图(如果启用视觉模式)
let screenshot: string | undefined;
if (this.settings.enableScreenshots) {
const result = await this.browser.screenshot();
screenshot = result.base64;
if (this.gifRecorder) {
this.gifRecorder.addFrame(screenshot, step, browserState.url);
}
}
// ── 决策层 ──
// 5. 构建状态消息(URL + 标题 + 标签页 + DOM 树 + 滚动位置)
const stateText = InstructionBuilder.buildTaskPrompt(this.settings.task) + '\n\n' +
InstructionBuilder.buildStatePrompt(
browserState.url, browserState.title, browserState.tabs,
domState.tree, step, stepLimit,
domState.pixelsAbove, domState.pixelsBelow
);
// 6. 循环检测:注入提醒
const loopCheck = this.loopDetector.isStuck();
if (loopCheck.stuck) {
stateText += InstructionBuilder.buildLoopNudge(
this.loopDetector.getLoopNudgeMessage()
);
if (loopCheck.severity >= 3) throw new AgentStalledError(...);
}
// 7. 加入策略上下文
if (this.settings.enableStrategy && this.state.currentPlan) {
stateText += InstructionBuilder.buildPlanPrompt(this.state.currentPlan);
}
// 8. 添加到对话管理器
this.messageManager.addStateMessage(stateText, screenshot, step);
// 9. 调用 LLM(带 Zod 验证恢复)
const completion = await this.invokeLlmWithRecovery(outputSchema, step);
// ── 执行层 ──
// 10. 标准化输出 + 执行命令
const normalizedOutput = this.normalizeOutput(completion.parsed);
const results = await this.tools.executeActions(
normalizedOutput.actions as Command[], context
);
// 11. 记录指纹用于循环检测
this.loopDetector.recordAction(actions);
this.loopDetector.recordFingerprint({
url: browserState.url,
domHash: hashPageTree(domState.tree),
scrollY: domState.scrollPosition.y,
elementCount: domState.elementCount,
textHash: hashTextContent(domState.tree.slice(0, 2000)),
});
// 12. 过滤敏感数据 + 记录历史
const filteredResults = this.filterSensitiveData(results);
this.historyList.addEntry(entry);
return results;
}
3.3 LLM 调用与 Zod 恢复 ------ invokeLlmWithRecovery()
这是我觉得最精巧的设计之一。LLM 返回的 JSON 可能不符合 Schema,怎么办?
typescript
private async invokeLlmWithRecovery(
outputSchema: z.ZodType<unknown>,
step: number,
retryCount = 0 // 最多重试 2 次
): Promise<{ parsed, usage }> {
const messages = this.messageManager.getMessages();
const invokeOptions: InferenceOptions<unknown> = {
messages,
responseSchema: outputSchema,
schemaName: this.getSchemaName(),
schemaDescription: 'Agent decision with current state assessment and actions',
};
// 深度推理:传递 thinking budget
if (this.settings.enableDeepReasoning && supportsDeepReasoning(this.model.modelId)) {
invokeOptions.maxTokens = this.settings.reasoningBudget;
}
try {
const completion = this.settings.modelDeadlineMs > 0
? await withDeadline(this.model.invoke(invokeOptions), this.settings.modelDeadlineMs, ...)
: await this.model.invoke(invokeOptions);
return { parsed: completion.parsed, usage: completion.usage };
} catch (error) {
// Zod 验证失败 → 把错误信息喂回 LLM 让它修
if (error instanceof ZodError && retryCount < 2) {
const issues = error.issues
.map(i => `- ${i.path.join('.')}: ${i.message}`)
.join('\n');
this.messageManager.addCommandResultMessage(
'Your previous response had a validation error. ' +
'Please fix the following issues and respond again:\n' + issues,
step
);
return this.invokeLlmWithRecovery(outputSchema, step, retryCount + 1);
}
throw error;
}
}
设计亮点:不是简单重试,而是把 Zod 的具体验证错误信息(路径 + 消息)回传给 LLM,让它"看着错误修"。这比盲目重试的成功率高得多。
3.4 三种输出 Schema ------ 适配不同模型能力
Agent 根据模型能力动态选择输出 Schema:
typescript
private getOutputSchema(): z.ZodType<unknown> {
// 模式 1:紧凑模式(便宜/快速模型)
// 只要求 goal + actions,不需要推理过程
if (this.settings.compactMode || isCompactModel(this.model.modelId)) {
return AgentDecisionCompactSchema;
}
// 模式 2:深度推理模式(支持 thinking 的模型)
// 跳过显式 brain schema,让模型内部推理
if (this.settings.enableDeepReasoning && supportsDeepReasoning(this.model.modelId)) {
return AgentDecisionDirectSchema;
}
// 模式 3:标准模式(完整推理 + 动作)
return z.object({
currentState: ReasoningSchema, // 评估 + 记忆 + 下一步目标
actions: z.array(CommandSchema), // 命令数组
});
}
这意味着你可以用 gpt-4o-mini 跑低成本任务(紧凑模式),也可以用 o1 跑复杂推理任务(深度模式),同一套代码自适应。
四、命令执行器 ------ 25+ 命令的注册与调度
4.1 命令注册模式
CommandExecutor 使用注册表模式(Registry Pattern),所有命令通过统一接口注册:
typescript
this.registry.register({
name: 'tap',
description: 'Click on an element by its index',
schema: TapCommandSchema.omit({ action: true }),
handler: async (params, ctx) => {
const { index, clickCount, coordinateX, coordinateY } = params;
// 坐标点击模式(视觉模型直接给坐标)
if (this.coordinateClickingEnabled &&
coordinateX !== undefined && coordinateY !== undefined) {
const clicks = clickCount ?? 1;
for (let i = 0; i < clicks; i++) {
await ctx.page.mouse.click(coordinateX, coordinateY);
}
return { success: true };
}
// 索引点击模式(通过 DOM 索引定位)
await ctx.domService.clickElementByIndex(ctx.page, ctx.cdpSession, index);
return { success: true };
},
});
完整命令清单:
| 类别 | 命令 | 说明 |
|---|---|---|
| 交互 | tap |
点击元素(支持索引和坐标两种模式) |
type_text |
输入文本(支持先清空) | |
press_keys |
键盘按键(Enter, Escape, Ctrl+A...) | |
select |
选择下拉框选项 | |
pick_option |
按文本匹配选择下拉选项 | |
upload |
上传文件 | |
| 导航 | navigate |
导航到 URL(带 URL 白名单/黑名单校验) |
back |
返回上一页 | |
web_search |
Google 搜索 | |
search |
多搜索引擎搜索(Google/DuckDuckGo/Bing) | |
| 标签页 | focus_tab |
切换标签页 |
new_tab |
新开标签页 | |
close_tab |
关闭标签页 | |
| 滚动 | scroll |
页面/元素滚动(上下) |
scroll_to |
滚动到指定文本位置 | |
| 提取 | extract |
LLM 辅助内容提取 |
extract_structured |
结构化数据提取(JSON Schema) | |
read_page |
读取页面 Markdown 文本 | |
find |
查找匹配描述的元素 | |
list_options |
获取下拉框所有选项 | |
| 其他 | capture |
截图 |
wait |
等待 | |
finish |
标记任务完成(终止序列) |
4.2 执行调度与错误分类
命令批量执行时有完善的错误处理:
typescript
async executeActions(actions: Command[], context: ExecutionContext): Promise<CommandResult[]> {
const results: CommandResult[] = [];
const limit = Math.min(actions.length, this.commandsPerStep);
for (let i = 0; i < limit; i++) {
try {
const result = await this.executeAction(actions[i], context);
results.push(this.maskSensitiveResult(result, context));
// 遇到终止命令(如 finish)停止
if (result.isDone) break;
if (this.registry.isTerminating(actions[i].action)) break;
} catch (error) {
// 错误分类:把浏览器原始错误翻译成人话
const interpreted = classifyViewportError(error);
results.push({
success: false,
error: `${interpreted.message} | Suggestion: ${interpreted.suggestion}`,
});
// 不可重试的错误(如浏览器崩溃)直接中断
if (!interpreted.isRetryable) break;
}
}
return results;
}
classifyViewportError 是一个精心设计的错误模式匹配器,覆盖了 20+ 种常见浏览器错误:
typescript
const ERROR_PATTERNS = [
{
pattern: /net::ERR_NAME_NOT_RESOLVED/i,
category: 'network',
message: () => 'DNS resolution failed - the domain could not be found.',
suggestion: 'Check the URL for typos or try a different URL.',
isRetryable: false, // DNS 错了重试也没用
},
{
pattern: /Element is not visible/i,
category: 'element_not_interactable',
message: () => 'The element exists but is not visible.',
suggestion: 'Try scrolling to make the element visible, or use a different element.',
isRetryable: true, // 滚动后可以重试
},
{
pattern: /intercepts pointer events/i,
category: 'element_not_interactable',
message: () => 'Another element is covering the target element.',
suggestion: 'An overlay or dialog may be blocking the click. Try closing it first.',
isRetryable: true,
},
// ... 20+ 个模式
];
这个设计的价值 :LLM 收到的是 Element is not visible | Suggestion: Try scrolling to make the element visible,而不是 Playwright 的原始堆栈。LLM 能据此做出下一步决策(比如先 scroll 再 click)。
五、Viewport ------ 浏览器控制层
5.1 生命周期与连接方式
Viewport 支持三种连接方式:
typescript
async start(): Promise<void> {
if (this.options.wsEndpoint) {
// 方式 1:WebSocket 连接远程浏览器
this.browser = await chromium.connect(this.options.wsEndpoint);
} else if (this.options.cdpUrl) {
// 方式 2:CDP 协议连接
this.browser = await chromium.connectOverCDP(this.options.cdpUrl);
} else {
// 方式 3:本地启动 Chromium
this.browser = await this.launchBrowser();
}
// 创建/复用上下文 + 页面 + CDP 会话
this.context = await this.createContext();
this._currentPage = await this.context.newPage();
this.cdpSession = await this._currentPage.context().newCDPSession(this._currentPage);
// 初始化 10 个守卫(Watchdogs)
await this.initializeWatchdogs();
}
5.2 十大守卫(Watchdogs)
Viewport 初始化了 10 个守卫,各自监听不同的浏览器事件:
typescript
this.watchdogs = [
new LocalInstanceGuard(), // 防止多实例冲突
new UrlPolicyGuard(...), // URL 白名单/黑名单执行
new DefaultHandlerGuard(), // 默认下载/弹窗处理器
new PopupGuard(), // 弹窗拦截
new PageReadyGuard(), // 页面就绪检测
new DownloadGuard(), // 下载管理
new BlankPageGuard(), // 空白页处理
new CrashGuard(), // 崩溃检测
new ScreenshotGuard(), // 截图能力保障
new PersistenceGuard(...), // 持久化(cookie/storage)
];
守卫按优先级排序(priority 越小越优先),通过统一的 GuardContext 接口 attach 到浏览器上下文。这是一个典型的 责任链 + 观察者 混合模式。
5.3 重连机制 ------ 指数退避
浏览器断连时的自动恢复:
typescript
async reconnect(): Promise<boolean> {
if (this.reconnecting) return false; // 防重入
this.reconnecting = true;
try {
await this.cleanupForReconnect(); // 清理但不发事件
let delay = this.reconnectDelayMs; // 初始延迟 1000ms
for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt++) {
try {
// 重新连接...
this._isConnected = true;
await this.initializeWatchdogs(); // 重新挂载守卫
return true;
} catch (error) {
if (attempt < this.maxReconnectAttempts) {
await sleep(delay);
delay *= 2; // 指数退避:1s → 2s → 4s
}
}
}
return false;
} finally {
this.reconnecting = false;
}
}
5.4 DOM 稳定性等待
这是处理动态页面(SPA、Ajax 加载)的关键:
typescript
async waitForStableDOM(timeout = 3000, quietPeriodMs = 300): Promise<void> {
await page.evaluate(({ timeoutMs, quietMs }) => {
return new Promise<void>((resolve) => {
let timer, overallTimer;
const observer = new MutationObserver(() => {
clearTimeout(timer);
// DOM 停止变化 quietMs 后认为稳定
timer = setTimeout(() => {
observer.disconnect();
resolve();
}, quietMs);
});
observer.observe(document.body, {
childList: true, subtree: true,
attributes: true, characterData: true,
});
// 超时兜底:即使一直在变,超时也放行
overallTimer = setTimeout(() => {
observer.disconnect();
resolve();
}, timeoutMs);
});
}, { timeoutMs: timeout, quietMs: quietPeriodMs });
}
原理 :用 MutationObserver 监听 DOM 变化,当连续 300ms 没有变化时认为页面"稳定"了。这对于 React/Vue 等框架渲染的 SPA 尤为重要------DOM 还在异步更新时就提取状态会拿到不完整的数据。
六、停滞检测 ------ StallDetector
这是整个框架里我认为设计最精巧的模块之一。AI Agent 最大的问题之一就是陷入死循环------反复点击同一个按钮、在两个页面之间来回跳。
6.1 多维指纹
StallDetector 不是简单地检测"重复动作",而是构建多维指纹:
typescript
export interface PageSignature {
url: string; // 当前 URL
domHash: string; // DOM 树哈希(快速 32-bit hash)
scrollY: number; // 滚动位置(按 200px 分桶)
elementCount?: number; // 元素数量
textHash?: string; // 可见文本哈希(归一化后)
}
6.2 四种循环检测策略
typescript
isStuck(): StallCheckResult {
// 策略 1:连续重复动作
// 同一个 action hash 连续出现 N 次
const actionRepetitions = this.countTrailingRepetitions(this.actionHistory);
if (actionRepetitions >= this.options.maxRepeatedActions) { ... }
// 策略 2:双步循环(A → B → A → B)
if (this.actionHistory.length >= 4) {
const last4 = this.actionHistory.slice(-4);
if (last4[0] === last4[2] && last4[1] === last4[3]) { ... }
}
// 策略 3:三步循环(A → B → C → A → B → C)
if (this.actionHistory.length >= 6) {
const last6 = this.actionHistory.slice(-6);
if (last6[0] === last6[3] && last6[1] === last6[4] && last6[2] === last6[5]) { ... }
}
// 策略 4:页面状态不变(URL + 元素数都没变)
const fpRepetitions = this.countTrailingRepetitions(this.fingerprintHashes);
if (fpRepetitions >= this.options.maxRepeatedFingerprints) { ... }
}
6.3 动作归一化
为了减少误报,动作哈希做了智能归一化:
typescript
private normalizeActionHash(actions: Command[]): string {
return actions.map(action => {
switch (action.action) {
case 'tap':
return `click:${action.index}`; // 忽略 clickCount 等瞬态参数
case 'web_search':
// 搜索词排序后比较:"best pizza NYC" == "NYC best pizza"
return `search_google:${this.normalizeSearchQuery(action.query)}`;
case 'navigate':
return `go_to_url:${action.url}`; // 只用 URL
default:
return JSON.stringify(action);
}
}).join('|');
}
6.4 升级提醒机制
检测到循环后,不是直接报错退出,而是渐进式提醒:
typescript
const ESCALATING_NUDGES = [
{
threshold: 5, // 重复 5 次触发
severity: 1, // 轻度
message: 'You seem to be repeating similar actions. Consider trying a different approach...',
},
{
threshold: 8, // 重复 8 次触发
severity: 2, // 中等
message: 'WARNING: You are stuck in a loop... You MUST change your approach...',
},
{
threshold: 12, // 重复 12 次触发
severity: 3, // 严重 → 抛 AgentStalledError
message: 'CRITICAL: You have been stuck for many steps. This approach is NOT working...',
},
];
这个梯度设计很关键:轻度时给建议(换个元素试试),中等时给警告(你必须改变策略),严重时直接终止(避免烧钱)。这在 Agent 框架里是少见的------大部分框架要么不检测循环,要么检测到就硬退出。
七、提示词工程 ------ InstructionBuilder
7.1 模板系统
系统提示词使用 .md 模板文件 + 变量插值:
typescript
const TEMPLATE_FILES: Record<PromptTemplate, string> = {
default: 'instructions.md', // 标准模式
flash: 'instructions-compact.md', // 紧凑模式(小模型)
'no-thinking': 'instructions-direct.md', // 直接模式(深度推理)
};
模板加载后有缓存,支持 clearTemplateCache() 热重载。
7.2 动态命令 Schema
最有趣的是 dynamicCommandSchema 功能------系统提示词会根据当前 URL 动态过滤可用命令:
typescript
private rebuildInstructionBuilder(pageUrl?: string): void {
const systemPrompt = InstructionBuilder.fromSettings(
this.settings,
this.tools.registry,
pageUrl, // ← 传入当前 URL
);
this.messageManager.setInstructionBuilder(systemPrompt.build());
}
CommandCatalog.getPromptDescription(pageUrl) 会根据域名返回相关命令。比如在 google.com 上可能隐藏 upload 命令,在表单页面上突出 pick_option 命令。这减少了 LLM 需要处理的 Schema 体积,提高决策准确性。
7.3 每步状态消息结构
每一步注入 LLM 的用户消息有清晰的结构化标签:
xml
<agent_history>
<!-- 之前的动作摘要 -->
</agent_history>
<agent_state>
<user_request>帮我查 MacBook 价格</user_request>
<plan>1. 打开 apple.com 2. 找到 MacBook Pro 3. 提取价格</plan>
<step_info>Step 5 of 25 | Today: 2026-03-31</step_info>
</agent_state>
<browser_state>
Current tab: ab3f
Available tabs:
Tab ab3f: https://apple.com/shop/mac - Mac - Apple
<page_info>2.3 pages below</page_info>
Interactive elements (truncated to 40000 characters):
[Start of page]
[0] <button> Buy </button>
[1] <a href="/macbook-pro"> MacBook Pro </a>
...
[End of page]
</browser_state>
<page_specific_actions>
<!-- 域名特定命令 -->
</page_specific_actions>
XML 标签结构让 LLM 更容易区分上下文的不同部分,这比纯文本拼接效果好很多。
八、成本追踪与敏感数据
8.1 Token 级成本追踪
typescript
private updateCostTracking(inputTokens: number, outputTokens: number, step: number): void {
const stepCost = calculateStepCost(inputTokens, outputTokens, this.model.modelId);
this.state.cumulativeCost.totalInputTokens += inputTokens;
this.state.cumulativeCost.totalOutputTokens += outputTokens;
if (stepCost) {
this.state.cumulativeCost.totalInputCost += stepCost.inputCost;
this.state.cumulativeCost.totalOutputCost += stepCost.outputCost;
this.state.cumulativeCost.totalCost += stepCost.totalCost;
logger.debug(
`Step ${step} cost: $${stepCost.totalCost.toFixed(4)} ` +
`(cumulative: $${this.state.cumulativeCost.totalCost.toFixed(4)})`
);
}
}
内置了 PRICING_TABLE,按模型 ID 查询单价。这让你能实时看到一次任务花了多少钱,以及在成本失控时及时叫停。
8.2 敏感数据脱敏
如果你在任务中输入了密码、API key 等敏感信息,框架会在结果中自动遮罩:
typescript
private filterSensitiveData(results: CommandResult[]): CommandResult[] {
if (!this.settings.maskedValues) return results;
return results.map(r => {
if (!r.extractedContent) return r;
let content = r.extractedContent;
for (const [key, value] of Object.entries(this.settings.maskedValues!)) {
content = content.replace(new RegExp(escapeRegExp(value), 'g'), `<${key}>`);
}
return { ...r, extractedContent: content };
});
}
配置方式:
typescript
const agent = new Agent({
settings: {
maskedValues: {
password: 'mySecretPassword',
apiKey: 'sk-xxxx'
}
}
});
// 结果中 "mySecretPassword" 会被替换为 "<password>"
九、设计亮点与踩坑点
设计亮点
-
三种 Schema 自适应:紧凑/直接/标准三种模式适配不同能力层级的模型,一套代码跑 gpt-4o-mini 到 o1。
-
Zod 错误回传修复:不盲目重试 LLM 调用,而是把具体的 Zod 验证错误(路径 + 消息)喂回去,让 LLM "看着改"。成功率显著高于盲目重试。
-
渐进式循环检测:5/8/12 三级阈值 + 四种检测策略(重复动作/双步循环/三步循环/页面停滞),既不过度敏感也不放过真死循环。
-
错误翻译 :把 Playwright 的原始错误(
net::ERR_NAME_NOT_RESOLVED)翻译成 LLM 能理解的自然语言 + 可操作建议,让 Agent 能自行从错误中恢复。 -
DOM 稳定性等待:用 MutationObserver 等待 SPA 渲染稳定,比固定 sleep 靠谱得多。
-
守卫链设计:10 个独立守卫按优先级排列,各自关注一个维度(URL 策略、弹窗、崩溃、下载...),符合单一职责原则。
-
对话压缩:长任务下 context 会爆炸,用 LLM 压缩历史消息而非简单截断,保留语义。
踩坑点
-
CDP 会话管理复杂 :每次切换标签页都要重新创建 CDP 会话(
newCDPSession),如果忘记这一步,后续的 DOM 提取会操作在错误的页面上。源码中switchTab()和closeTab()都仔细处理了这一点。 -
hashPageTree用 32-bit 整数哈希 :这是一个快速的 djb2 变体(hash = ((hash << 5) - hash + char) | 0),速度快但碰撞率不低。对于超长 DOM 树可能有误判,不过配合 textHash 和 elementCount 多维指纹,实际影响可控。 -
commandsPerStep默认 10 :LLM 单步最多执行 10 个命令,但遇到finish或terminatesSequence的命令会提前中断。如果 LLM 在第 10 个命令才输出finish,前面的 9 个命令已经执行了------成本和副作用都需注意。 -
scroll_to用 TreeWalker 查文本 :通过NodeFilter.SHOW_TEXT遍历文本节点,只做toLowerCase().includes()匹配。对于动态加载的内容(滚动后才渲染的元素),可能找不到目标文本。 -
重连后守卫全部重建 :
reconnect()里cleanupForReconnect()会 detach 所有守卫然后重建。这意味着重连期间的浏览器事件(弹窗、下载等)可能丢失。对于需要严格事件追溯的场景需注意。 -
web_search硬编码 Google URL :buildGoogleSearchUrl拼接的是https://www.google.com/search?q=...&udm=14,其中udm=14是 Google 的 "no AI overview" 参数。如果 Google 改了参数名,这里就挂了。search命令虽然支持多引擎但默认也是 Google。
十、与同类项目对比
| 特性 | OpenBrowser | Browser Use (Python) | Playwright MCP |
|---|---|---|---|
| 语言 | TypeScript | Python | 多语言 |
| 运行时 | Bun | Python | Node.js |
| 浏览器引擎 | Playwright | Playwright | Playwright |
| 多模型 | OpenAI/Anthropic/Google | OpenAI/Anthropic/Google | 依赖宿主 |
| 循环检测 | 四维指纹 + 三级升级 | 基础 | 无 |
| 成本追踪 | Token 级 + 价格表 | 基础 | 无 |
| 错误恢复 | LLM 诊断 + 分类建议 | 基础重试 | 无 |
| 对话压缩 | LLM 压缩 | 无 | N/A |
| 沙盒执行 | 独立包 | 无 | 无 |
| 交互式 REPL | 有 | 无 | 无 |
| 许可证 | MIT | MIT | Apache-2.0 |
OpenBrowser 的优势在于工程化程度------它不是一个 demo,而是一个考虑了成本、错误、循环、重连、安全的生产级框架。
十一、总结
OpenBrowser 的核心价值在于:它把 LLM 的"理解网页"能力 和 Playwright 的"操作浏览器"能力,用一个精心设计的 Agent Loop 粘合在了一起。
关键架构决策的回顾:
- Agent Loop:感知(DOM+截图)→ 决策(LLM+Schema)→ 执行(25+ 命令)→ 观察(结果+指纹)→ 循环
- 鲁棒性三件套:StallDetector(防死循环)+ classifyViewportError(错误翻译)+ invokeLlmWithRecovery(Schema 修复)
- 成本意识:Token 级追踪 + 预算策略 + 对话压缩
- 安全意识:URL 白名单/黑名单 + 敏感数据脱敏 + 10 个守卫
如果你在做 AI 浏览器自动化、网页数据采集、或者想理解 Agent 框架的工程实践,这个项目值得仔细读一遍源码。尤其是 agent.ts、executor.ts、stall-detector.ts 这三个文件,浓缩了大量实战经验。
本文基于 OpenBrowser master 分支 (commit 067fc45, 2026-03-31) 源码分析撰写。