首页
/ 让 UI-TARS 跑在 Web 上:UI-TARS-desktop 仓库 Open Operator 示例,基于 Browserbase 与 Stagehand 构建 Web GUI Agent

让 UI-TARS 跑在 Web 上:UI-TARS-desktop 仓库 Open Operator 示例,基于 Browserbase 与 Stagehand 构建 Web GUI Agent

2026-09-05 17:24:46作者:范靓好Udolf

本文围绕 UI-TARS-desktop 仓库中的 Open Operator 示例 展开。该示例是一个 Next.js 全栈应用,把 @ui-tars/sdk 中的 GUIAgent@ui-tars/operator-browserbase(Browserbase + Stagehand)组合起来,实现"用户输入自然语言目标 → 云端浏览器实时操作 → SSE 流式回传每一步动作与推理"的完整链路。读完本文,你将掌握该示例的本地运行与配置方法、/api/session/api/agent 两个服务端接口的实现原理,以及 BrowserbaseOperator 的动作空间与截图/执行机制。

Open Operator 原始流程示意:构建 Web Agent 涉及的复杂环节 引入 Stagehand 与 Browserbase 后被简化的 Agent 架构 Agent Loop:模型决策与浏览器执行交替进行

项目定位:一个明确的 PoC 示例

该目录的 README 开宗明义:它是 fork 自 Browserbase 的 open-operator 项目,并且"this is simply a proof of concept(这只是一个概念验证)"。上游定位是"不与 Web Agent 竞争,而是提供构建 Web Agent 所需的全部工具",本 fork 的差异在于把模型层换成了 UI-TARS 系列服务——README 明确要求读者从 UI-TARS 项目获取 UI_TARS_BASE_URLUI_TARS_API_KEYUI_TARS_MODEL 三项配置,而不是只配置一个通用 LLM 的 key。

依赖清单 看,该示例与主仓库 monorepo 的衔接点非常清晰:

  • @ui-tars/sdk@ui-tars/operator-browserbase,版本均为 ^1.2.0-beta.12,即仓库 packages/ui-tars/sdkpackages/ui-tars/operators/browserbase 发布的 npm 包;
  • @browserbasehq/sdk^2.0.0),用于服务端直接创建/释放 Browserbase 会话;
  • next(15.1.9)+ react^19.0.0)+ framer-motion + jotai + zod,构成前端框架与状态管理。

@ui-tars/operator-browserbase 包自身的 README 也指向本示例作为用法参考,两者互为印证。

五分钟跑起来:安装、环境变量与启动

安装依赖(必须使用 pnpm)

README 中特别注明"works with pnpm only,NPM 不可用,yarn 未测试"。因此克隆仓库后进入该目录执行:

pnpm install

准备环境变量

README 给出的步骤是先复制示例环境文件(cp .env.example .env.local),再把以下 5 个变量写入 .env.local。这些变量的实际消费点都在服务端路由代码里,逐一对应关系如下:

变量 用途 在源码中的消费位置
UI_TARS_BASE_URL UI-TARS 模型服务的 Base URL agent 路由 中传入 GUIAgentmodel.baseURL
UI_TARS_API_KEY UI-TARS 模型服务的 API Key 同上,model.apiKey
UI_TARS_MODEL 使用的 UI-TARS 模型名 同上,model.model
BROWSERBASE_API_KEY Browserbase API Key session 路由 中构造 Browserbase SDK 客户端
BROWSERBASE_PROJECT_ID Browserbase 项目 ID 同上,创建 context 与 session 时必填

启动开发服务器

pnpm dev

对应 package.json 中的 next dev --turbopack。启动后打开 http://localhost:3000 即可看到首页:一个输入框加"Run"按钮,以及"Who is the top contributor to Stagehand?"等几个预设示例任务(见 首页组件)。

端到端架构:前端、两个 API 路由与 SSE 流

整体数据流可以从 ChatFeed 组件initializeSession 逻辑完整还原,顺序如下:

  1. 创建云端浏览器会话:前端以 POST /api/session 提交 { timezone, contextId },其中 timezone 取自浏览器的 Intl.DateTimeFormat().resolvedOptions().timeZone。后端返回 { sessionId, sessionUrl, contextId }
  2. 内嵌实时画面:前端把 sessionUrl 做了一次字符串替换(把 devtools-fullscreen 的 inspector 页面替换为 devtools-internal-compiled/index.html),再放进 <iframe sandbox="allow-same-origin allow-scripts allow-forms"> 中,实现"边操作边观看"的直播效果。
  3. 发起 Agent 任务POST /api/agent,请求体为 { goal, sessionId, action: 'START' },请求头声明 Accept: text/event-stream
  4. 消费 SSE 流:前端用 XStream 工具(改编自 Ant Design X 的 x-stream,按 \n\n 分段解析 data: 前缀的 SSE 帧)逐帧取出 steps,渲染为"Step N + 动作文本 + Reasoning"卡片;当某步 tool === 'CLOSE'done === true 时停止消费,并通过 DELETE /api/session 释放浏览器会话。

会话路由:时区选区、Context 持久化与调试画面

session 路由 是整个示例中最"工程化"的一段,值得逐点拆解:

  • 时区 → 区域映射getClosestRegion(timezone) 三级降级——先精确匹配(如 America/New_Yorkus-east-1),再按前缀映射(America/US/Canadaus-west-2Europe/Africaeu-central-1Asia/Australia/Pacificap-southeast-1),最后用 Intl.DateTimeFormat 算出 UTC 偏移量,落在 -24~-4 / -3~4 / 5~24 三个区间分别对应美西/欧洲/新加坡区域;无法解析时默认 us-west-2。目的是让云端浏览器尽量靠近用户所在地,降低直播延迟。
  • Context 复用createSession 支持传入已有 contextId。传入时直接 { context: { id, persist: true } };否则调用 bb.contexts.create({ projectId }) 新建一个持久化 context 再挂载。这使浏览器的登录态、Cookie 可以在多次会话间延续。
  • 会话参数bb.sessions.create 时显式设置 keepAlive: trueregion,保证 Agent 执行期间会话不被回收。
  • 调试 URLgetDebugUrl 调用 bb.sessions.debug(sessionId) 拿到 debuggerFullscreenUrl,这就是前端 iframe 里看到的实时画面来源。
  • 释放DELETE 通过 bb.sessions.update(sessionId, { status: 'REQUEST_RELEASE' }) 释放会话。

前端侧还有一个细节:atoms.ts 用 jotai 的 atomWithStorage('contextId', '')contextId 持久化到浏览器本地存储,刷新页面后仍能复用同一个 Browserbase context,避免每次都从零开始。

Agent 路由:GUIAgent + BrowserbaseOperator + SSE

agent 路由 只有百余行,却包含了完整的 Agent 装配逻辑:

1. 系统提示词由动作空间自动拼装

SYSTEM_PROMPT 要求模型输出 Thought: ... / Action: ... 两段式格式,其中动作空间与示例都来自 BrowserbaseOperator.MANUAL 静态属性,而非手写:

## Action Space
${BrowserbaseOperator.MANUAL.ACTION_SPACES.join('\n')}

## Note
- The first step should be to GOTO a specific website
- Write a small plan and finally summarize your next action (with its target element)
  in one sentence in `Thought` part.

## Example
${BrowserbaseOperator.MANUAL.EXAMPLES.join('\n')}

## User Instruction

这个设计保证了"提示词里承诺的动作空间"与"Operator 实际能执行的动作"永远一致。

2. 装配 GUIAgent

const operator = new BrowserbaseOperator({ env: 'LOCAL' });

const guiAgent = new GUIAgent({
  systemPrompt: SYSTEM_PROMPT,
  model: {
    baseURL: process.env.UI_TARS_BASE_URL,
    apiKey: process.env.UI_TARS_API_KEY,
    model: process.env.UI_TARS_MODEL!,
  },
  operator,
  onData: async ({ data }) => { /* 解析并写入 SSE 流 */ },
  onError: ({ error }) => { /* 写 error 帧 */ },
});

guiAgent.run(goal);

其中 GUIAgent 是 monorepo 中 SDK 的核心类:构造时若 config.model 不是 UITarsModel 实例会自动包一层,systemPrompt 缺省时用内置模板构建;run(instruction) 启动"截图 → 模型推理 → 解析动作 → Operator 执行"的循环,循环上限由 maxLoopCount 控制,每轮通过 onData 回调抛出 GUIAgentData(含 statusconversations 等字段)。

3. 把 Agent 回调翻译成 SSE 帧

onData 中取 data.conversations 的最后一条 lastConversation,若存在 predictionParsed 则映射为前端可渲染的 step:

const steps = lastConversation?.predictionParsed?.map((p) => ({
  text: `${p.action_type}: ${JSON.stringify(p.action_inputs)}`,
  reasoning: p.thought,
  tool: p.action_type,
  instruction: p.action_inputs?.content,
}));

const nextData = {
  success: true,
  ...(lastConversation?.from === 'gpt' && lastConversation?.value && {
    reasoning: lastConversation.value,
  }),
  ...(steps.length > 0 && { steps, result: steps[0] }),
  done: [StatusEnum.END, StatusEnum.MAX_LOOP].includes(data.status),
};
await writer.write(encoder.encode(`data: ${JSON.stringify(nextData)}\n\n`));

注意结束判定:状态到达 StatusEnum.END(任务完成)或 StatusEnum.MAX_LOOP(达到最大循环数)时 done 为 true;仅在 END 时关闭写端。响应头声明 Content-Type: text/event-streamCache-Control: no-cache, no-transform,配合 vercel.json 中把 app/api/**/*maxDuration 提到 300 秒——长耗时 Agent 循环不会提前被函数超时掐断。

BrowserbaseOperator:动作空间与执行细节

@ui-tars/operator-browserbase 的源码在 packages/ui-tars/operators/browserbase/src/index.ts,它继承 SDK 的 Operator 基类,内部惰性初始化一个 Stagehand 实例(new Stagehand(options)await init()),并实现三个关键成员:

1. 动作空间声明(MANUAL.ACTION_SPACES

动作 参数 语义
GOTO url='' 确定最佳起始 URL 并导航到目标页面
ACT description='' 对当前页面执行一步自然语言描述的操作
EXTRACT description='' 从页面中抽取信息
OBSERVE description='' 观察并分析网页以决定下一步
CLOSE 任务完成,不再执行动作
NAVBACK 返回上一页

MANUAL.EXAMPLES 则给出一段完整的 few-shot:查"Stagehand 项目 GitHub 头号贡献者",依次 GOTO 仓库 → ACT 点 Insights → ACT 点 Contributors → CLOSE 收尾,与提示词中"第一步必须 GOTO"的约束相呼应。

2. 截图:走 CDP 而非 Stagehand API

public async screenshot(): Promise<ScreenshotOutput> {
  const stagehand = await this.getStagehand();
  const page = stagehand.page;
  const cdpSession = await page.context().newCDPSession(page);
  const { data: base64 } = await cdpSession.send('Page.captureScreenshot');
  return { base64, scaleFactor: 1 };
}

从源码结构看,这里绕开高层 API 直接用 Playwright 的 page 对象建 CDP 会话调 Page.captureScreenshot,返回的 { base64, scaleFactor: 1 } 正是 SDK ScreenshotOutput 约定格式——这个 base64 会被 GUIAgent 拼入多模态消息发给 UI-TARS 模型。

3. execute:动作分发

execute(params) 读取 params.parsedPrediction(即模型输出的解析结果),按 action_type 分派:

  • GOTOpage.goto(url, { waitUntil: 'commit', timeout: 60000 }),60 秒超时;
  • ACTpage.act(description),把自然语言动作交给 Stagehand 转成具体 DOM 操作;
  • EXTRACTpage.extract(description) 拿结构化结果(源码中标注 TODO:返回值的 ObserveResult 尚未完整回传给 Agent 循环);
  • OBSERVEpage.observe({ instruction, useAccessibilityTree: true }),基于可访问性树观察页面;
  • WAITsetTimeout 毫秒级休眠(注意:WAITexecute 的 switch 中可执行,但未列入 MANUAL.ACTION_SPACES,即不会主动写进提示词的动作空间);
  • NAVBACKpage.goBack()
  • CLOSEstagehand.close() 并返回 { status: StatusEnum.END },这一状态最终触发 agent 路由的 done 帧,前端随之 DELETE /api/session

前端呈现:步骤卡片与直播画面的联动

ChatFeed 定义了 BrowserStep 类型(tool 取值 GOTO | ACT | EXTRACT | OBSERVE | CLOSE | WAIT | NAVBACK),每收到一个 SSE 帧就把 result 追加为带 stepNumber 的卡片,左侧是 iframe 直播画面(任务未完成时)或完成提示("The agent has completed the task"),右侧是滚动到底部的步骤列表。此外组件里还有几个可观察的交互约定:

  • 输入页支持 Cmd/Ctrl+Enter 提交、Cmd/Ctrl+K 聚焦输入框、Esc 关闭对话视图(见 page.tsx 的全局键盘监听);
  • 任务完成(末步为 CLOSE)时自动触发 DELETE /api/session,与后端 REQUEST_RELEASE 对接,形成资源释放闭环。

关键技术栈与和上游 README 的一处差异

README 的 "Key Technologies" 一节列出:Browserbase(核心浏览器自动化与交互能力)、Stagehand(精确 DOM 操作与状态管理)、Next.js(Web 框架基座)、OpenAI(自然语言理解与决策)。需要说明的是,这一节继承自上游 open-operator 的描述;而本 fork 的实际代码里,"理解用户意图"的模型调用走的是 UI_TARS_BASE_URL / UI_TARS_API_KEY / UI_TARS_MODEL 指向的 UI-TARS 服务(见 agent 路由的 GUIAgent 配置),因此可以把该示例理解为"上游 Stagehand/Browserbase 架构 + UI-TARS 模型层"的组合,这也是它放在 UI-TARS-desktop 仓库 examples 目录下的意义:展示如何用 @ui-tars/sdk 把 UI-TARS 模型接到云浏览器上,构建一个可被他人复用的 Web Agent 样板。

适用前提与限制

  • 该示例是显式声明的 PoC:README 的 WARNING 提示其目标是演示工具链,而非生产级产品;
  • 依赖 pnpm(README 注明 NPM 不可用),并需要可访问的 UI-TARS 模型服务Browserbase 账号(API Key + Project ID)两套凭据;
  • 云端会话的创建、直播画面(devtools 调试 URL 的替换)、会话释放都依赖 Browserbase SDK 的 sessions/contexts 接口行为;
  • 部署到 Vercel 时,长任务能力由 vercel.jsonmaxDuration: 300 保障,本地 pnpm dev 则无此限制。

小结

这个不到 20 个源文件的 Next.js 示例,串起了 UI-TARS-desktop 仓库中几个重要构件:@ui-tars/sdkGUIAgent 主循环、@ui-tars/operator-browserbase 的 Stagehand/CDP 执行层、Browserbase 的会话与 context 管理、以及 SSE 流式 UI。若你希望在浏览器环境中运行 UI-TARS GUI Agent 并让用户实时观看执行过程,可以从 examples/operator-browserbase 入手,按"配置 5 个环境变量 → pnpm installpnpm dev"三步跑通,再顺着 agent 路由BrowserbaseOperator 源码 两条线深入阅读。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384